refactor(native-player): share player helpers
This commit is contained in:
@@ -9,11 +9,7 @@
|
|||||||
<!-- Camera permission for QR code scanning (companion remote pairing) -->
|
<!-- Camera permission for QR code scanning (companion remote pairing) -->
|
||||||
<uses-permission android:name="android.permission.CAMERA"/>
|
<uses-permission android:name="android.permission.CAMERA"/>
|
||||||
|
|
||||||
<!-- PIP Permissions -->
|
<!-- PIP and media session foreground service permissions -->
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK"/>
|
|
||||||
|
|
||||||
<!-- Media session permissions for OS media controls -->
|
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK"/>
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK"/>
|
||||||
|
|
||||||
|
|||||||
@@ -27,9 +27,9 @@ import java.io.File
|
|||||||
|
|
||||||
class MainActivity : FlutterActivity() {
|
class MainActivity : FlutterActivity() {
|
||||||
|
|
||||||
private val PIP_CHANNEL = "app.plezy/pip"
|
private val PIP_CHANNEL = "com.plezy/pip"
|
||||||
private val EXTERNAL_PLAYER_CHANNEL = "app.plezy/external_player"
|
private val EXTERNAL_PLAYER_CHANNEL = "com.plezy/external_player"
|
||||||
private val THEME_CHANNEL = "app.plezy/theme"
|
private val THEME_CHANNEL = "com.plezy/theme"
|
||||||
private var watchNextPlugin: WatchNextPlugin? = null
|
private var watchNextPlugin: WatchNextPlugin? = null
|
||||||
|
|
||||||
// Auto PiP state
|
// Auto PiP state
|
||||||
|
|||||||
@@ -55,9 +55,7 @@ import io.github.peerless2012.ass.media.parser.AssSubtitleParserFactory
|
|||||||
import io.github.peerless2012.ass.media.type.AssRenderType
|
import io.github.peerless2012.ass.media.type.AssRenderType
|
||||||
import io.github.peerless2012.ass.media.widget.AssSubtitleView
|
import io.github.peerless2012.ass.media.widget.AssSubtitleView
|
||||||
|
|
||||||
interface ExoPlayerDelegate {
|
interface ExoPlayerDelegate : com.edde746.plezy.shared.PlayerDelegate {
|
||||||
fun onPropertyChange(name: String, value: Any?)
|
|
||||||
fun onEvent(name: String, data: Map<String, Any>?)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when ExoPlayer encounters a format it cannot play.
|
* Called when ExoPlayer encounters a format it cannot play.
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import android.content.Context
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.edde746.plezy.mpv.MpvPlayerCore
|
import com.edde746.plezy.mpv.MpvPlayerCore
|
||||||
import com.edde746.plezy.mpv.MpvPlayerDelegate
|
|
||||||
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
||||||
import io.flutter.embedding.engine.plugins.activity.ActivityAware
|
import io.flutter.embedding.engine.plugins.activity.ActivityAware
|
||||||
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
|
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
|
||||||
@@ -15,7 +14,7 @@ import io.flutter.plugin.common.MethodCall
|
|||||||
import io.flutter.plugin.common.MethodChannel
|
import io.flutter.plugin.common.MethodChannel
|
||||||
|
|
||||||
class ExoPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
|
class ExoPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
|
||||||
EventChannel.StreamHandler, ActivityAware, ExoPlayerDelegate, MpvPlayerDelegate {
|
EventChannel.StreamHandler, ActivityAware, ExoPlayerDelegate {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "ExoPlayerPlugin"
|
private const val TAG = "ExoPlayerPlugin"
|
||||||
@@ -34,7 +33,6 @@ class ExoPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
|
|||||||
private var activityBinding: ActivityPluginBinding? = null
|
private var activityBinding: ActivityPluginBinding? = null
|
||||||
private val nameToId = mutableMapOf<String, Int>()
|
private val nameToId = mutableMapOf<String, Int>()
|
||||||
private var configuredBufferSizeBytes: Int? = null
|
private var configuredBufferSizeBytes: Int? = null
|
||||||
private var configuredTunnelingEnabled: Boolean = true
|
|
||||||
|
|
||||||
private var debugLoggingEnabled: Boolean = false
|
private var debugLoggingEnabled: Boolean = false
|
||||||
private val pendingMpvProperties = mutableListOf<Pair<String, String>>()
|
private val pendingMpvProperties = mutableListOf<Pair<String, String>>()
|
||||||
@@ -161,7 +159,6 @@ class ExoPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
|
|||||||
val bufferSizeBytes = call.argument<Int>("bufferSizeBytes")
|
val bufferSizeBytes = call.argument<Int>("bufferSizeBytes")
|
||||||
val tunnelingEnabled = call.argument<Boolean>("tunnelingEnabled") ?: true
|
val tunnelingEnabled = call.argument<Boolean>("tunnelingEnabled") ?: true
|
||||||
configuredBufferSizeBytes = bufferSizeBytes
|
configuredBufferSizeBytes = bufferSizeBytes
|
||||||
configuredTunnelingEnabled = tunnelingEnabled
|
|
||||||
|
|
||||||
currentActivity.runOnUiThread {
|
currentActivity.runOnUiThread {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -16,15 +16,11 @@ import android.view.ViewTreeObserver
|
|||||||
import com.edde746.plezy.shared.AudioFocusManager
|
import com.edde746.plezy.shared.AudioFocusManager
|
||||||
import com.edde746.plezy.shared.FlutterOverlayHelper
|
import com.edde746.plezy.shared.FlutterOverlayHelper
|
||||||
import com.edde746.plezy.shared.FrameRateManager
|
import com.edde746.plezy.shared.FrameRateManager
|
||||||
|
import com.edde746.plezy.shared.PlayerDelegate
|
||||||
import dev.jdtech.mpv.MPVLib
|
import dev.jdtech.mpv.MPVLib
|
||||||
import io.flutter.plugin.common.MethodChannel
|
import io.flutter.plugin.common.MethodChannel
|
||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
|
|
||||||
interface MpvPlayerDelegate {
|
|
||||||
fun onPropertyChange(name: String, value: Any?)
|
|
||||||
fun onEvent(name: String, data: Map<String, Any>?)
|
|
||||||
}
|
|
||||||
|
|
||||||
class MpvPlayerCore(private val activity: Activity) :
|
class MpvPlayerCore(private val activity: Activity) :
|
||||||
SurfaceHolder.Callback,
|
SurfaceHolder.Callback,
|
||||||
MPVLib.EventObserver,
|
MPVLib.EventObserver,
|
||||||
@@ -59,7 +55,7 @@ class MpvPlayerCore(private val activity: Activity) :
|
|||||||
@Volatile private var disposing: Boolean = false
|
@Volatile private var disposing: Boolean = false
|
||||||
private var pendingSurface: Surface? = null
|
private var pendingSurface: Surface? = null
|
||||||
private var lastSurfaceSize: String? = null
|
private var lastSurfaceSize: String? = null
|
||||||
var delegate: MpvPlayerDelegate? = null
|
var delegate: PlayerDelegate? = null
|
||||||
var isInitialized: Boolean = false
|
var isInitialized: Boolean = false
|
||||||
private set
|
private set
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import io.flutter.plugin.common.MethodCall
|
|||||||
import io.flutter.plugin.common.MethodChannel
|
import io.flutter.plugin.common.MethodChannel
|
||||||
|
|
||||||
class MpvPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
|
class MpvPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
|
||||||
EventChannel.StreamHandler, ActivityAware, MpvPlayerDelegate {
|
EventChannel.StreamHandler, ActivityAware, com.edde746.plezy.shared.PlayerDelegate {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "MpvPlayerPlugin"
|
private const val TAG = "MpvPlayerPlugin"
|
||||||
@@ -281,7 +281,7 @@ class MpvPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
|
|||||||
} ?: result.error("NO_PLAYER", "Player not initialized", null)
|
} ?: result.error("NO_PLAYER", "Player not initialized", null)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MpvPlayerDelegate
|
// PlayerDelegate
|
||||||
|
|
||||||
override fun onPropertyChange(name: String, value: Any?) {
|
override fun onPropertyChange(name: String, value: Any?) {
|
||||||
val propId = nameToId[name] ?: return
|
val propId = nameToId[name] ?: return
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package com.edde746.plezy.shared
|
||||||
|
|
||||||
|
interface PlayerDelegate {
|
||||||
|
fun onPropertyChange(name: String, value: Any?)
|
||||||
|
fun onEvent(name: String, data: Map<String, Any>?)
|
||||||
|
}
|
||||||
@@ -17,7 +17,7 @@ class WatchNextPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "WatchNextPlugin"
|
private const val TAG = "WatchNextPlugin"
|
||||||
private const val METHOD_CHANNEL = "app.plezy/watch_next"
|
private const val METHOD_CHANNEL = "com.plezy/watch_next"
|
||||||
|
|
||||||
private var pendingDeepLink: String? = null
|
private var pendingDeepLink: String? = null
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
6A8A46252EDB370C0057B88C /* MpvPlayerPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A8A46232EDB370C0057B88C /* MpvPlayerPlugin.swift */; };
|
6A8A46252EDB370C0057B88C /* MpvPlayerPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A8A46232EDB370C0057B88C /* MpvPlayerPlugin.swift */; };
|
||||||
6A8A46262EDB370C0057B88C /* MpvPlayerCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A8A46222EDB370C0057B88C /* MpvPlayerCore.swift */; };
|
6A8A46262EDB370C0057B88C /* MpvPlayerCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A8A46222EDB370C0057B88C /* MpvPlayerCore.swift */; };
|
||||||
B1D51A6A2F00110000000001 /* MpvPlayerCoreBase.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1D51A6A2F00110000000002 /* MpvPlayerCoreBase.swift */; };
|
B1D51A6A2F00110000000001 /* MpvPlayerCoreBase.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1D51A6A2F00110000000002 /* MpvPlayerCoreBase.swift */; };
|
||||||
|
B1D51A6A2F00110000000005 /* MpvPlayerPluginShared.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1D51A6A2F00110000000006 /* MpvPlayerPluginShared.swift */; };
|
||||||
92F969587D0E464D999910F5 /* MpvPipController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92F969587D0E464D999910F4 /* MpvPipController.swift */; };
|
92F969587D0E464D999910F5 /* MpvPipController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92F969587D0E464D999910F4 /* MpvPipController.swift */; };
|
||||||
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
|
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
|
||||||
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
|
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
|
||||||
@@ -59,7 +60,8 @@
|
|||||||
6A48DFAF2EA70C7100C1F7CD /* plezy.icon */ = {isa = PBXFileReference; lastKnownFileType = folder.iconcomposer.icon; path = plezy.icon; sourceTree = "<group>"; };
|
6A48DFAF2EA70C7100C1F7CD /* plezy.icon */ = {isa = PBXFileReference; lastKnownFileType = folder.iconcomposer.icon; path = plezy.icon; sourceTree = "<group>"; };
|
||||||
6A8A46222EDB370C0057B88C /* MpvPlayerCore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MpvPlayerCore.swift; sourceTree = "<group>"; };
|
6A8A46222EDB370C0057B88C /* MpvPlayerCore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MpvPlayerCore.swift; sourceTree = "<group>"; };
|
||||||
6A8A46232EDB370C0057B88C /* MpvPlayerPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MpvPlayerPlugin.swift; sourceTree = "<group>"; };
|
6A8A46232EDB370C0057B88C /* MpvPlayerPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MpvPlayerPlugin.swift; sourceTree = "<group>"; };
|
||||||
B1D51A6A2F00110000000002 /* MpvPlayerCoreBase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MpvPlayerCoreBase.swift; path = ../apple/Shared/MpvPlayer/MpvPlayerCoreBase.swift; sourceTree = SOURCE_ROOT; };
|
B1D51A6A2F00110000000002 /* MpvPlayerCoreBase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MpvPlayerCoreBase.swift; path = ../shared/apple/MpvPlayer/MpvPlayerCoreBase.swift; sourceTree = SOURCE_ROOT; };
|
||||||
|
B1D51A6A2F00110000000006 /* MpvPlayerPluginShared.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MpvPlayerPluginShared.swift; path = ../shared/apple/MpvPlayer/MpvPlayerPluginShared.swift; sourceTree = SOURCE_ROOT; };
|
||||||
92F969587D0E464D999910F4 /* MpvPipController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MpvPipController.swift; sourceTree = "<group>"; };
|
92F969587D0E464D999910F4 /* MpvPipController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MpvPipController.swift; sourceTree = "<group>"; };
|
||||||
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
|
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||||
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||||
@@ -118,6 +120,7 @@
|
|||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
B1D51A6A2F00110000000002 /* MpvPlayerCoreBase.swift */,
|
B1D51A6A2F00110000000002 /* MpvPlayerCoreBase.swift */,
|
||||||
|
B1D51A6A2F00110000000006 /* MpvPlayerPluginShared.swift */,
|
||||||
6A8A46222EDB370C0057B88C /* MpvPlayerCore.swift */,
|
6A8A46222EDB370C0057B88C /* MpvPlayerCore.swift */,
|
||||||
6A8A46232EDB370C0057B88C /* MpvPlayerPlugin.swift */,
|
6A8A46232EDB370C0057B88C /* MpvPlayerPlugin.swift */,
|
||||||
92F969587D0E464D999910F4 /* MpvPipController.swift */,
|
92F969587D0E464D999910F4 /* MpvPipController.swift */,
|
||||||
@@ -420,6 +423,7 @@
|
|||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
B1D51A6A2F00110000000001 /* MpvPlayerCoreBase.swift in Sources */,
|
B1D51A6A2F00110000000001 /* MpvPlayerCoreBase.swift in Sources */,
|
||||||
|
B1D51A6A2F00110000000005 /* MpvPlayerPluginShared.swift in Sources */,
|
||||||
6A8A46252EDB370C0057B88C /* MpvPlayerPlugin.swift in Sources */,
|
6A8A46252EDB370C0057B88C /* MpvPlayerPlugin.swift in Sources */,
|
||||||
6A8A46262EDB370C0057B88C /* MpvPlayerCore.swift in Sources */,
|
6A8A46262EDB370C0057B88C /* MpvPlayerCore.swift in Sources */,
|
||||||
92F969587D0E464D999910F5 /* MpvPipController.swift in Sources */,
|
92F969587D0E464D999910F5 /* MpvPipController.swift in Sources */,
|
||||||
|
|||||||
@@ -3,14 +3,19 @@ import Flutter
|
|||||||
import AVKit
|
import AVKit
|
||||||
|
|
||||||
/// Flutter plugin that bridges MPV player to Dart via method and event channels
|
/// Flutter plugin that bridges MPV player to Dart via method and event channels
|
||||||
class MpvPlayerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, MpvPlayerDelegate {
|
class MpvPlayerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, MpvPluginShared {
|
||||||
|
|
||||||
// MARK: - Properties
|
// MARK: - Properties
|
||||||
|
|
||||||
private var playerCore: MpvPlayerCore?
|
private var playerCore: MpvPlayerCore?
|
||||||
private var eventSink: FlutterEventSink?
|
var eventSink: FlutterEventSink?
|
||||||
private weak var registrar: FlutterPluginRegistrar?
|
private weak var registrar: FlutterPluginRegistrar?
|
||||||
private var nameToId: [String: Int] = [:]
|
var nameToId: [String: Int] = [:]
|
||||||
|
|
||||||
|
// MpvPluginShared conformance
|
||||||
|
var coreBase: MpvPlayerCoreBase? { playerCore }
|
||||||
|
func setPlayerVisible(_ visible: Bool) { playerCore?.setVisible(visible) }
|
||||||
|
func updatePlayerFrame() { playerCore?.updateFrame() }
|
||||||
|
|
||||||
// PiP
|
// PiP
|
||||||
private var pipController: MpvPipController?
|
private var pipController: MpvPipController?
|
||||||
@@ -33,7 +38,7 @@ class MpvPlayerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, MpvPlayerD
|
|||||||
binaryMessenger: registrar.messenger()
|
binaryMessenger: registrar.messenger()
|
||||||
)
|
)
|
||||||
let pipChannel = FlutterMethodChannel(
|
let pipChannel = FlutterMethodChannel(
|
||||||
name: "app.plezy/pip",
|
name: "com.plezy/pip",
|
||||||
binaryMessenger: registrar.messenger()
|
binaryMessenger: registrar.messenger()
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -252,7 +257,7 @@ class MpvPlayerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, MpvPlayerD
|
|||||||
pipTimebaseSyncTimer = nil
|
pipTimebaseSyncTimer = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Method Handlers
|
// MARK: - Platform-Specific Method Handlers
|
||||||
|
|
||||||
private func handleInitialize(result: @escaping FlutterResult) {
|
private func handleInitialize(result: @escaping FlutterResult) {
|
||||||
DispatchQueue.main.async { [weak self] in
|
DispatchQueue.main.async { [weak self] in
|
||||||
@@ -320,91 +325,6 @@ class MpvPlayerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, MpvPlayerD
|
|||||||
result(nil)
|
result(nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func handleGetProperty(call: FlutterMethodCall, result: @escaping FlutterResult) {
|
|
||||||
guard let args = call.arguments as? [String: Any],
|
|
||||||
let name = args["name"] as? String else {
|
|
||||||
result(FlutterError(code: "INVALID_ARGS", message: "Missing 'name' argument", details: nil))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
result(playerCore?.getProperty(name))
|
|
||||||
}
|
|
||||||
|
|
||||||
private func handleObserveProperty(call: FlutterMethodCall, result: @escaping FlutterResult) {
|
|
||||||
guard let args = call.arguments as? [String: Any],
|
|
||||||
let name = args["name"] as? String,
|
|
||||||
let format = args["format"] as? String,
|
|
||||||
let id = args["id"] as? Int else {
|
|
||||||
result(FlutterError(code: "INVALID_ARGS", message: "Missing 'name', 'format', or 'id' argument", details: nil))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
nameToId[name] = id
|
|
||||||
playerCore?.observeProperty(name, format: format)
|
|
||||||
result(nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func handleCommand(call: FlutterMethodCall, result: @escaping FlutterResult) {
|
|
||||||
guard let args = call.arguments as? [String: Any],
|
|
||||||
let commandArgs = args["args"] as? [String] else {
|
|
||||||
result(FlutterError(code: "INVALID_ARGS", message: "Missing 'args' argument", details: nil))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
playerCore?.commandAsync(commandArgs) { commandResult in
|
|
||||||
switch commandResult {
|
|
||||||
case .success:
|
|
||||||
result(nil)
|
|
||||||
case .failure(let error):
|
|
||||||
result(FlutterError(code: "COMMAND_FAILED", message: error.localizedDescription, details: nil))
|
|
||||||
}
|
|
||||||
} ?? result(nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func handleSetVisible(call: FlutterMethodCall, result: @escaping FlutterResult) {
|
|
||||||
guard let args = call.arguments as? [String: Any],
|
|
||||||
let visible = args["visible"] as? Bool else {
|
|
||||||
result(FlutterError(code: "INVALID_ARGS", message: "Missing 'visible' argument", details: nil))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
DispatchQueue.main.async { [weak self] in
|
|
||||||
self?.playerCore?.setVisible(visible)
|
|
||||||
if visible { self?.playerCore?.updateFrame() }
|
|
||||||
result(nil)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func handleUpdateFrame(result: @escaping FlutterResult) {
|
|
||||||
DispatchQueue.main.async { [weak self] in
|
|
||||||
self?.playerCore?.updateFrame()
|
|
||||||
result(nil)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func handleSetLogLevel(call: FlutterMethodCall, result: @escaping FlutterResult) {
|
|
||||||
guard let args = call.arguments as? [String: Any],
|
|
||||||
let level = args["level"] as? String else {
|
|
||||||
result(FlutterError(code: "INVALID_ARGS", message: "Missing 'level'", details: nil))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
playerCore?.setLogLevel(level)
|
|
||||||
result(nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - MpvPlayerDelegate
|
|
||||||
|
|
||||||
func onPropertyChange(name: String, value: Any?) {
|
|
||||||
guard let eventSink = eventSink, let propId = nameToId[name] else { return }
|
|
||||||
eventSink([propId, value as Any])
|
|
||||||
}
|
|
||||||
|
|
||||||
func onEvent(name: String, data: [String: Any]?) {
|
|
||||||
guard let eventSink = eventSink else { return }
|
|
||||||
var event: [String: Any] = ["type": "event", "name": name]
|
|
||||||
if let data = data { event["data"] = data }
|
|
||||||
eventSink(event)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Helpers
|
// MARK: - Helpers
|
||||||
|
|
||||||
private func findKeyWindow() -> UIWindow? {
|
private func findKeyWindow() -> UIWindow? {
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ class ThemeProvider extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static const _themeChannel = MethodChannel('app.plezy/theme');
|
static const _themeChannel = MethodChannel('com.plezy/theme');
|
||||||
|
|
||||||
Future<void> setThemeMode(settings.ThemeMode mode) async {
|
Future<void> setThemeMode(settings.ThemeMode mode) async {
|
||||||
if (_themeMode != mode) {
|
if (_themeMode != mode) {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import '../i18n/strings.g.dart';
|
|||||||
import 'plex_client.dart';
|
import 'plex_client.dart';
|
||||||
import 'settings_service.dart';
|
import 'settings_service.dart';
|
||||||
|
|
||||||
const _externalPlayerChannel = MethodChannel('app.plezy/external_player');
|
const _externalPlayerChannel = MethodChannel('com.plezy/external_player');
|
||||||
|
|
||||||
class ExternalPlayerService {
|
class ExternalPlayerService {
|
||||||
/// Launch an external player with either a pre-resolved [videoUrl] (e.g. local
|
/// Launch an external player with either a pre-resolved [videoUrl] (e.g. local
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import 'package:flutter/services.dart';
|
|||||||
import 'package:plezy/i18n/strings.g.dart';
|
import 'package:plezy/i18n/strings.g.dart';
|
||||||
|
|
||||||
class PipService {
|
class PipService {
|
||||||
static const MethodChannel _channel = MethodChannel('app.plezy/pip');
|
static const MethodChannel _channel = MethodChannel('com.plezy/pip');
|
||||||
|
|
||||||
/// PiP is only implemented natively on Android, iOS, and macOS.
|
/// PiP is only implemented natively on Android, iOS, and macOS.
|
||||||
static bool get _isAvailable => Platform.isAndroid || Platform.isIOS || Platform.isMacOS;
|
static bool get _isAvailable => Platform.isAndroid || Platform.isIOS || Platform.isMacOS;
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import 'settings_service.dart' show EpisodePosterMode;
|
|||||||
|
|
||||||
/// Service for syncing Plex "On Deck" content to Android TV's Watch Next row.
|
/// Service for syncing Plex "On Deck" content to Android TV's Watch Next row.
|
||||||
class WatchNextService {
|
class WatchNextService {
|
||||||
static const MethodChannel _channel = MethodChannel('app.plezy/watch_next');
|
static const MethodChannel _channel = MethodChannel('com.plezy/watch_next');
|
||||||
|
|
||||||
static final WatchNextService _instance = WatchNextService._internal();
|
static final WatchNextService _instance = WatchNextService._internal();
|
||||||
factory WatchNextService() => _instance;
|
factory WatchNextService() => _instance;
|
||||||
|
|||||||
@@ -43,3 +43,4 @@ target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::EPOXY)
|
|||||||
target_link_libraries(${BINARY_NAME} PRIVATE simdutf)
|
target_link_libraries(${BINARY_NAME} PRIVATE simdutf)
|
||||||
|
|
||||||
target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}")
|
target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}")
|
||||||
|
target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}/../shared/cpp")
|
||||||
|
|||||||
@@ -11,49 +11,8 @@
|
|||||||
#include <gdk/gdkwayland.h>
|
#include <gdk/gdkwayland.h>
|
||||||
#endif
|
#endif
|
||||||
#include <clocale>
|
#include <clocale>
|
||||||
#include <cstring>
|
|
||||||
#include <string>
|
|
||||||
#include <simdutf.h>
|
|
||||||
|
|
||||||
// Sanitize a C string that may contain invalid UTF-8 sequences.
|
#include "sanitize_utf8.h"
|
||||||
// Uses simdutf for SIMD-accelerated validation (fast path for valid strings),
|
|
||||||
// then falls back to iterative replacement with U+FFFD on the rare invalid case.
|
|
||||||
// mpv does not guarantee UTF-8 for log messages, error strings, or
|
|
||||||
// system-encoded paths — sending these unsanitized through Flutter's
|
|
||||||
// StandardMessageCodec causes FormatException crashes.
|
|
||||||
static std::string SanitizeUtf8(const char* input) {
|
|
||||||
if (!input) return std::string();
|
|
||||||
size_t len = strlen(input);
|
|
||||||
if (len == 0) return std::string();
|
|
||||||
|
|
||||||
// Fast path: SIMD-accelerated validation — almost all strings pass this
|
|
||||||
if (simdutf::validate_utf8(input, len)) {
|
|
||||||
return std::string(input, len);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Slow path: find each invalid position, copy valid prefix, insert U+FFFD,
|
|
||||||
// skip the bad byte, and repeat.
|
|
||||||
std::string result;
|
|
||||||
result.reserve(len);
|
|
||||||
size_t pos = 0;
|
|
||||||
|
|
||||||
while (pos < len) {
|
|
||||||
auto r = simdutf::validate_utf8_with_errors(input + pos, len - pos);
|
|
||||||
// Copy the valid prefix up to the error
|
|
||||||
if (r.count > 0) {
|
|
||||||
result.append(input + pos, r.count);
|
|
||||||
}
|
|
||||||
pos += r.count;
|
|
||||||
if (r.error == simdutf::error_code::SUCCESS) {
|
|
||||||
break; // remaining tail is valid
|
|
||||||
}
|
|
||||||
// Replace the invalid byte with U+FFFD and skip it
|
|
||||||
result.append("\xEF\xBF\xBD");
|
|
||||||
pos++;
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Flutter on Linux uses EGL (OpenGL ES) for both X11 and Wayland.
|
// Flutter on Linux uses EGL (OpenGL ES) for both X11 and Wayland.
|
||||||
static void* get_opengl_proc_address(void* ctx, const char* name) {
|
static void* get_opengl_proc_address(void* ctx, const char* name) {
|
||||||
|
|||||||
@@ -32,6 +32,7 @@
|
|||||||
6AD8B1612ED7B50000E9E1B4 /* MpvPlayerCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AD8B1632ED7B50000E9E1B4 /* MpvPlayerCore.swift */; };
|
6AD8B1612ED7B50000E9E1B4 /* MpvPlayerCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AD8B1632ED7B50000E9E1B4 /* MpvPlayerCore.swift */; };
|
||||||
6AD8B1622ED7B50000E9E1B4 /* MpvPlayerPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AD8B1642ED7B50000E9E1B4 /* MpvPlayerPlugin.swift */; };
|
6AD8B1622ED7B50000E9E1B4 /* MpvPlayerPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AD8B1642ED7B50000E9E1B4 /* MpvPlayerPlugin.swift */; };
|
||||||
B1D51A6A2F00110000000003 /* MpvPlayerCoreBase.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1D51A6A2F00110000000004 /* MpvPlayerCoreBase.swift */; };
|
B1D51A6A2F00110000000003 /* MpvPlayerCoreBase.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1D51A6A2F00110000000004 /* MpvPlayerCoreBase.swift */; };
|
||||||
|
B1D51A6A2F00110000000007 /* MpvPlayerPluginShared.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1D51A6A2F00110000000008 /* MpvPlayerPluginShared.swift */; };
|
||||||
6AD8B1662ED7B50000E9E1B5 /* WindowUtilsPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AD8B1682ED7B50000E9E1B5 /* WindowUtilsPlugin.swift */; };
|
6AD8B1662ED7B50000E9E1B5 /* WindowUtilsPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AD8B1682ED7B50000E9E1B5 /* WindowUtilsPlugin.swift */; };
|
||||||
6AD8B1672ED7B50000E9E1B5 /* WindowDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AD8B1692ED7B50000E9E1B5 /* WindowDelegate.swift */; };
|
6AD8B1672ED7B50000E9E1B5 /* WindowDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AD8B1692ED7B50000E9E1B5 /* WindowDelegate.swift */; };
|
||||||
6AD8B16A2ED7B50000E9E1B6 /* MpvPipController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AD8B16B2ED7B50000E9E1B6 /* MpvPipController.swift */; };
|
6AD8B16A2ED7B50000E9E1B6 /* MpvPipController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AD8B16B2ED7B50000E9E1B6 /* MpvPipController.swift */; };
|
||||||
@@ -89,7 +90,8 @@
|
|||||||
6AC86ED62EA70B4C0067BC66 /* plezy.icon */ = {isa = PBXFileReference; lastKnownFileType = folder.iconcomposer.icon; path = plezy.icon; sourceTree = "<group>"; };
|
6AC86ED62EA70B4C0067BC66 /* plezy.icon */ = {isa = PBXFileReference; lastKnownFileType = folder.iconcomposer.icon; path = plezy.icon; sourceTree = "<group>"; };
|
||||||
6AD8B1632ED7B50000E9E1B4 /* MpvPlayerCore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MpvPlayerCore.swift; sourceTree = "<group>"; };
|
6AD8B1632ED7B50000E9E1B4 /* MpvPlayerCore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MpvPlayerCore.swift; sourceTree = "<group>"; };
|
||||||
6AD8B1642ED7B50000E9E1B4 /* MpvPlayerPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MpvPlayerPlugin.swift; sourceTree = "<group>"; };
|
6AD8B1642ED7B50000E9E1B4 /* MpvPlayerPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MpvPlayerPlugin.swift; sourceTree = "<group>"; };
|
||||||
B1D51A6A2F00110000000004 /* MpvPlayerCoreBase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MpvPlayerCoreBase.swift; path = ../apple/Shared/MpvPlayer/MpvPlayerCoreBase.swift; sourceTree = SOURCE_ROOT; };
|
B1D51A6A2F00110000000004 /* MpvPlayerCoreBase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MpvPlayerCoreBase.swift; path = ../shared/apple/MpvPlayer/MpvPlayerCoreBase.swift; sourceTree = SOURCE_ROOT; };
|
||||||
|
B1D51A6A2F00110000000008 /* MpvPlayerPluginShared.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MpvPlayerPluginShared.swift; path = ../shared/apple/MpvPlayer/MpvPlayerPluginShared.swift; sourceTree = SOURCE_ROOT; };
|
||||||
6AD8B1682ED7B50000E9E1B5 /* WindowUtilsPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowUtilsPlugin.swift; sourceTree = "<group>"; };
|
6AD8B1682ED7B50000E9E1B5 /* WindowUtilsPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowUtilsPlugin.swift; sourceTree = "<group>"; };
|
||||||
6AD8B1692ED7B50000E9E1B5 /* WindowDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowDelegate.swift; sourceTree = "<group>"; };
|
6AD8B1692ED7B50000E9E1B5 /* WindowDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowDelegate.swift; sourceTree = "<group>"; };
|
||||||
6AD8B16B2ED7B50000E9E1B6 /* MpvPipController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MpvPipController.swift; sourceTree = "<group>"; };
|
6AD8B16B2ED7B50000E9E1B6 /* MpvPipController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MpvPipController.swift; sourceTree = "<group>"; };
|
||||||
@@ -221,6 +223,7 @@
|
|||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
B1D51A6A2F00110000000004 /* MpvPlayerCoreBase.swift */,
|
B1D51A6A2F00110000000004 /* MpvPlayerCoreBase.swift */,
|
||||||
|
B1D51A6A2F00110000000008 /* MpvPlayerPluginShared.swift */,
|
||||||
6AD8B1632ED7B50000E9E1B4 /* MpvPlayerCore.swift */,
|
6AD8B1632ED7B50000E9E1B4 /* MpvPlayerCore.swift */,
|
||||||
6AD8B16B2ED7B50000E9E1B6 /* MpvPipController.swift */,
|
6AD8B16B2ED7B50000E9E1B6 /* MpvPipController.swift */,
|
||||||
6AD8B1642ED7B50000E9E1B4 /* MpvPlayerPlugin.swift */,
|
6AD8B1642ED7B50000E9E1B4 /* MpvPlayerPlugin.swift */,
|
||||||
@@ -473,6 +476,7 @@
|
|||||||
33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */,
|
33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */,
|
||||||
335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */,
|
335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */,
|
||||||
B1D51A6A2F00110000000003 /* MpvPlayerCoreBase.swift in Sources */,
|
B1D51A6A2F00110000000003 /* MpvPlayerCoreBase.swift in Sources */,
|
||||||
|
B1D51A6A2F00110000000007 /* MpvPlayerPluginShared.swift in Sources */,
|
||||||
6AD8B1612ED7B50000E9E1B4 /* MpvPlayerCore.swift in Sources */,
|
6AD8B1612ED7B50000E9E1B4 /* MpvPlayerCore.swift in Sources */,
|
||||||
6AD8B1622ED7B50000E9E1B4 /* MpvPlayerPlugin.swift in Sources */,
|
6AD8B1622ED7B50000E9E1B4 /* MpvPlayerPlugin.swift in Sources */,
|
||||||
6AD8B1662ED7B50000E9E1B5 /* WindowUtilsPlugin.swift in Sources */,
|
6AD8B1662ED7B50000E9E1B5 /* WindowUtilsPlugin.swift in Sources */,
|
||||||
|
|||||||
@@ -2,14 +2,19 @@ import Cocoa
|
|||||||
import FlutterMacOS
|
import FlutterMacOS
|
||||||
|
|
||||||
/// Flutter plugin that bridges MPV player to Dart via method and event channels
|
/// Flutter plugin that bridges MPV player to Dart via method and event channels
|
||||||
class MpvPlayerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, MpvPlayerDelegate {
|
class MpvPlayerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, MpvPluginShared {
|
||||||
|
|
||||||
// MARK: - Properties
|
// MARK: - Properties
|
||||||
|
|
||||||
private var playerCore: MpvPlayerCore?
|
private var playerCore: MpvPlayerCore?
|
||||||
private var eventSink: FlutterEventSink?
|
var eventSink: FlutterEventSink?
|
||||||
private weak var registrar: FlutterPluginRegistrar?
|
private weak var registrar: FlutterPluginRegistrar?
|
||||||
private var nameToId: [String: Int] = [:]
|
var nameToId: [String: Int] = [:]
|
||||||
|
|
||||||
|
// MpvPluginShared conformance
|
||||||
|
var coreBase: MpvPlayerCoreBase? { playerCore }
|
||||||
|
func setPlayerVisible(_ visible: Bool) { playerCore?.setVisible(visible) }
|
||||||
|
func updatePlayerFrame() { playerCore?.updateFrame() }
|
||||||
|
|
||||||
// PiP
|
// PiP
|
||||||
private var pipController: MpvPipController?
|
private var pipController: MpvPipController?
|
||||||
@@ -33,7 +38,7 @@ class MpvPlayerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, MpvPlayerD
|
|||||||
)
|
)
|
||||||
|
|
||||||
let pipChannel = FlutterMethodChannel(
|
let pipChannel = FlutterMethodChannel(
|
||||||
name: "app.plezy/pip",
|
name: "com.plezy/pip",
|
||||||
binaryMessenger: registrar.messenger
|
binaryMessenger: registrar.messenger
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -198,7 +203,7 @@ class MpvPlayerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, MpvPlayerD
|
|||||||
pip.stopPip()
|
pip.stopPip()
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Method Handlers
|
// MARK: - Platform-Specific Method Handlers
|
||||||
|
|
||||||
private func handleInitialize(result: @escaping FlutterResult) {
|
private func handleInitialize(result: @escaping FlutterResult) {
|
||||||
DispatchQueue.main.async { [weak self] in
|
DispatchQueue.main.async { [weak self] in
|
||||||
@@ -277,110 +282,6 @@ class MpvPlayerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, MpvPlayerD
|
|||||||
result(nil)
|
result(nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func handleGetProperty(call: FlutterMethodCall, result: @escaping FlutterResult) {
|
|
||||||
guard let args = call.arguments as? [String: Any],
|
|
||||||
let name = args["name"] as? String else {
|
|
||||||
result(FlutterError(code: "INVALID_ARGS", message: "Missing 'name' argument", details: nil))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let value = playerCore?.getProperty(name)
|
|
||||||
result(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func handleObserveProperty(call: FlutterMethodCall, result: @escaping FlutterResult) {
|
|
||||||
guard let args = call.arguments as? [String: Any],
|
|
||||||
let name = args["name"] as? String,
|
|
||||||
let format = args["format"] as? String,
|
|
||||||
let id = args["id"] as? Int else {
|
|
||||||
result(FlutterError(code: "INVALID_ARGS", message: "Missing 'name', 'format', or 'id' argument", details: nil))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
nameToId[name] = id
|
|
||||||
playerCore?.observeProperty(name, format: format)
|
|
||||||
result(nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func handleCommand(call: FlutterMethodCall, result: @escaping FlutterResult) {
|
|
||||||
guard let args = call.arguments as? [String: Any],
|
|
||||||
let commandArgs = args["args"] as? [String] else {
|
|
||||||
result(FlutterError(code: "INVALID_ARGS", message: "Missing 'args' argument", details: nil))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use async command to prevent UI blocking during network operations
|
|
||||||
playerCore?.commandAsync(commandArgs) { commandResult in
|
|
||||||
switch commandResult {
|
|
||||||
case .success:
|
|
||||||
result(nil)
|
|
||||||
case .failure(let error):
|
|
||||||
result(FlutterError(code: "COMMAND_FAILED", message: error.localizedDescription, details: nil))
|
|
||||||
}
|
|
||||||
} ?? result(nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func handleSetVisible(call: FlutterMethodCall, result: @escaping FlutterResult) {
|
|
||||||
guard let args = call.arguments as? [String: Any],
|
|
||||||
let visible = args["visible"] as? Bool else {
|
|
||||||
result(FlutterError(code: "INVALID_ARGS", message: "Missing 'visible' argument", details: nil))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
DispatchQueue.main.async { [weak self] in
|
|
||||||
self?.playerCore?.setVisible(visible)
|
|
||||||
|
|
||||||
// Update frame when becoming visible
|
|
||||||
if visible {
|
|
||||||
self?.playerCore?.updateFrame()
|
|
||||||
}
|
|
||||||
|
|
||||||
result(nil)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func handleUpdateFrame(result: @escaping FlutterResult) {
|
|
||||||
DispatchQueue.main.async { [weak self] in
|
|
||||||
self?.playerCore?.updateFrame()
|
|
||||||
result(nil)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func handleSetLogLevel(call: FlutterMethodCall, result: @escaping FlutterResult) {
|
|
||||||
guard let playerCore = playerCore else {
|
|
||||||
result(FlutterError(code: "NOT_INITIALIZED", message: "Player not initialized", details: nil))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
guard let args = call.arguments as? [String: Any],
|
|
||||||
let level = args["level"] as? String else {
|
|
||||||
result(FlutterError(code: "INVALID_ARGS", message: "Missing 'level'", details: nil))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
playerCore.setLogLevel(level)
|
|
||||||
result(nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - MpvPlayerDelegate
|
|
||||||
|
|
||||||
func onPropertyChange(name: String, value: Any?) {
|
|
||||||
guard let eventSink = eventSink else { return }
|
|
||||||
|
|
||||||
if let propId = nameToId[name] {
|
|
||||||
eventSink([propId, value as Any])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func onEvent(name: String, data: [String: Any]?) {
|
|
||||||
guard let eventSink = eventSink else { return }
|
|
||||||
|
|
||||||
var event: [String: Any] = ["type": "event", "name": name]
|
|
||||||
if let data = data {
|
|
||||||
event["data"] = data
|
|
||||||
}
|
|
||||||
|
|
||||||
eventSink(event)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Helpers
|
// MARK: - Helpers
|
||||||
|
|
||||||
private func findFlutterWindow() -> (NSWindow, NSView, NSView)? {
|
private func findFlutterWindow() -> (NSWindow, NSView, NSView)? {
|
||||||
|
|||||||
@@ -0,0 +1,105 @@
|
|||||||
|
#if os(iOS)
|
||||||
|
import Flutter
|
||||||
|
#elseif os(macOS)
|
||||||
|
import FlutterMacOS
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/// Protocol for shared MpvPlayerPlugin method handlers across iOS and macOS.
|
||||||
|
/// Platform-specific methods (PiP, initialization, window finding) remain
|
||||||
|
/// in the per-platform MpvPlayerPlugin files.
|
||||||
|
protocol MpvPluginShared: AnyObject, MpvPlayerDelegate {
|
||||||
|
var coreBase: MpvPlayerCoreBase? { get }
|
||||||
|
var eventSink: FlutterEventSink? { get }
|
||||||
|
var nameToId: [String: Int] { get set }
|
||||||
|
|
||||||
|
func setPlayerVisible(_ visible: Bool)
|
||||||
|
func updatePlayerFrame()
|
||||||
|
}
|
||||||
|
|
||||||
|
extension MpvPluginShared {
|
||||||
|
|
||||||
|
func handleGetProperty(call: FlutterMethodCall, result: @escaping FlutterResult) {
|
||||||
|
guard let args = call.arguments as? [String: Any],
|
||||||
|
let name = args["name"] as? String else {
|
||||||
|
result(FlutterError(code: "INVALID_ARGS", message: "Missing 'name' argument", details: nil))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
result(coreBase?.getProperty(name))
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleObserveProperty(call: FlutterMethodCall, result: @escaping FlutterResult) {
|
||||||
|
guard let args = call.arguments as? [String: Any],
|
||||||
|
let name = args["name"] as? String,
|
||||||
|
let format = args["format"] as? String,
|
||||||
|
let id = args["id"] as? Int else {
|
||||||
|
result(FlutterError(code: "INVALID_ARGS", message: "Missing 'name', 'format', or 'id' argument", details: nil))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
nameToId[name] = id
|
||||||
|
coreBase?.observeProperty(name, format: format)
|
||||||
|
result(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleCommand(call: FlutterMethodCall, result: @escaping FlutterResult) {
|
||||||
|
guard let args = call.arguments as? [String: Any],
|
||||||
|
let commandArgs = args["args"] as? [String] else {
|
||||||
|
result(FlutterError(code: "INVALID_ARGS", message: "Missing 'args' argument", details: nil))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
coreBase?.commandAsync(commandArgs) { commandResult in
|
||||||
|
switch commandResult {
|
||||||
|
case .success:
|
||||||
|
result(nil)
|
||||||
|
case .failure(let error):
|
||||||
|
result(FlutterError(code: "COMMAND_FAILED", message: error.localizedDescription, details: nil))
|
||||||
|
}
|
||||||
|
} ?? result(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleSetVisible(call: FlutterMethodCall, result: @escaping FlutterResult) {
|
||||||
|
guard let args = call.arguments as? [String: Any],
|
||||||
|
let visible = args["visible"] as? Bool else {
|
||||||
|
result(FlutterError(code: "INVALID_ARGS", message: "Missing 'visible' argument", details: nil))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
DispatchQueue.main.async { [weak self] in
|
||||||
|
self?.setPlayerVisible(visible)
|
||||||
|
if visible { self?.updatePlayerFrame() }
|
||||||
|
result(nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleUpdateFrame(result: @escaping FlutterResult) {
|
||||||
|
DispatchQueue.main.async { [weak self] in
|
||||||
|
self?.updatePlayerFrame()
|
||||||
|
result(nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleSetLogLevel(call: FlutterMethodCall, result: @escaping FlutterResult) {
|
||||||
|
guard let args = call.arguments as? [String: Any],
|
||||||
|
let level = args["level"] as? String else {
|
||||||
|
result(FlutterError(code: "INVALID_ARGS", message: "Missing 'level'", details: nil))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
coreBase?.setLogLevel(level)
|
||||||
|
result(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - MpvPlayerDelegate
|
||||||
|
|
||||||
|
func onPropertyChange(name: String, value: Any?) {
|
||||||
|
guard let eventSink = eventSink, let propId = nameToId[name] else { return }
|
||||||
|
eventSink([propId, value as Any])
|
||||||
|
}
|
||||||
|
|
||||||
|
func onEvent(name: String, data: [String: Any]?) {
|
||||||
|
guard let eventSink = eventSink else { return }
|
||||||
|
var event: [String: Any] = ["type": "event", "name": name]
|
||||||
|
if let data = data { event["data"] = data }
|
||||||
|
eventSink(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
#ifndef SANITIZE_UTF8_H_
|
||||||
|
#define SANITIZE_UTF8_H_
|
||||||
|
|
||||||
|
#include <cstring>
|
||||||
|
#include <string>
|
||||||
|
#include <simdutf.h>
|
||||||
|
|
||||||
|
// Sanitize a C string that may contain invalid UTF-8 sequences.
|
||||||
|
// Uses simdutf for SIMD-accelerated validation (fast path for valid strings),
|
||||||
|
// then falls back to iterative replacement with U+FFFD on the rare invalid case.
|
||||||
|
// mpv does not guarantee UTF-8 for log messages, error strings, or
|
||||||
|
// system-encoded paths — sending these unsanitized through Flutter's
|
||||||
|
// StandardMessageCodec causes FormatException crashes.
|
||||||
|
static inline std::string SanitizeUtf8(const char* input) {
|
||||||
|
if (!input) return std::string();
|
||||||
|
size_t len = strlen(input);
|
||||||
|
if (len == 0) return std::string();
|
||||||
|
|
||||||
|
// Fast path: SIMD-accelerated validation — almost all strings pass this
|
||||||
|
if (simdutf::validate_utf8(input, len)) {
|
||||||
|
return std::string(input, len);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Slow path: find each invalid position, copy valid prefix, insert U+FFFD,
|
||||||
|
// skip the bad byte, and repeat.
|
||||||
|
std::string result;
|
||||||
|
result.reserve(len);
|
||||||
|
size_t pos = 0;
|
||||||
|
|
||||||
|
while (pos < len) {
|
||||||
|
auto r = simdutf::validate_utf8_with_errors(input + pos, len - pos);
|
||||||
|
// Copy the valid prefix up to the error
|
||||||
|
if (r.count > 0) {
|
||||||
|
result.append(input + pos, r.count);
|
||||||
|
}
|
||||||
|
pos += r.count;
|
||||||
|
if (r.error == simdutf::error_code::SUCCESS) {
|
||||||
|
break; // remaining tail is valid
|
||||||
|
}
|
||||||
|
// Replace the invalid byte with U+FFFD and skip it
|
||||||
|
result.append("\xEF\xBF\xBD");
|
||||||
|
pos++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // SANITIZE_UTF8_H_
|
||||||
@@ -51,6 +51,7 @@ target_link_libraries(${BINARY_NAME} PRIVATE simdutf)
|
|||||||
target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}")
|
target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}")
|
||||||
target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}")
|
target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}")
|
||||||
target_include_directories(${BINARY_NAME} PRIVATE "${MPV_INCLUDE_DIR}")
|
target_include_directories(${BINARY_NAME} PRIVATE "${MPV_INCLUDE_DIR}")
|
||||||
|
target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}/../shared/cpp")
|
||||||
|
|
||||||
# Run the Flutter tool portions of the build. This must not be removed.
|
# Run the Flutter tool portions of the build. This must not be removed.
|
||||||
add_dependencies(${BINARY_NAME} flutter_assemble)
|
add_dependencies(${BINARY_NAME} flutter_assemble)
|
||||||
|
|||||||
@@ -1,47 +1,6 @@
|
|||||||
#include "mpv_player.h"
|
#include "mpv_player.h"
|
||||||
|
|
||||||
#include <cstring>
|
#include "sanitize_utf8.h"
|
||||||
#include <simdutf.h>
|
|
||||||
|
|
||||||
// Sanitize a C string that may contain invalid UTF-8 sequences.
|
|
||||||
// Uses simdutf for SIMD-accelerated validation (fast path for valid strings),
|
|
||||||
// then falls back to iterative replacement with U+FFFD on the rare invalid case.
|
|
||||||
// mpv does not guarantee UTF-8 for log messages, error strings, or
|
|
||||||
// system-encoded paths — sending these unsanitized through Flutter's
|
|
||||||
// StandardMessageCodec causes FormatException crashes.
|
|
||||||
static std::string SanitizeUtf8(const char* input) {
|
|
||||||
if (!input) return std::string();
|
|
||||||
size_t len = strlen(input);
|
|
||||||
if (len == 0) return std::string();
|
|
||||||
|
|
||||||
// Fast path: SIMD-accelerated validation — almost all strings pass this
|
|
||||||
if (simdutf::validate_utf8(input, len)) {
|
|
||||||
return std::string(input, len);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Slow path: find each invalid position, copy valid prefix, insert U+FFFD,
|
|
||||||
// skip the bad byte, and repeat.
|
|
||||||
std::string result;
|
|
||||||
result.reserve(len);
|
|
||||||
size_t pos = 0;
|
|
||||||
|
|
||||||
while (pos < len) {
|
|
||||||
auto r = simdutf::validate_utf8_with_errors(input + pos, len - pos);
|
|
||||||
// Copy the valid prefix up to the error
|
|
||||||
if (r.count > 0) {
|
|
||||||
result.append(input + pos, r.count);
|
|
||||||
}
|
|
||||||
pos += r.count;
|
|
||||||
if (r.error == simdutf::error_code::SUCCESS) {
|
|
||||||
break; // remaining tail is valid
|
|
||||||
}
|
|
||||||
// Replace the invalid byte with U+FFFD and skip it
|
|
||||||
result.append("\xEF\xBF\xBD");
|
|
||||||
pos++;
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace mpv {
|
namespace mpv {
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user