Live-TV clock generations were matched to mpv sources first-in-first-out: every start-file popped the oldest registered open, on the assumption of exactly one start-file per open in dispatch order. Opens without a generation on the same player, an Android loadfile rejected silently by nativeCommand, and the independent delivery of the command ack and the start-file event all broke that, so a seek that reopened the stream could calibrate against the wrong source. The loadfile reply now carries mpv's playlist_entry_id on Android, Apple, Linux and Windows, PlayerNative.open resolves with it, and the live session binds each generation to that id explicitly. Source events that land before the reply are buffered per id and replayed on binding; a rejected or unreachable load fails its generation instead of leaving a phantom; opens with no generation are invisible to clock binding. Android now reports a rejected mpv command as COMMAND_FAILED like the other cores.
179 lines
5.7 KiB
Swift
179 lines
5.7 KiB
Swift
#if os(iOS) || os(tvOS)
|
|
import Flutter
|
|
#elseif os(macOS)
|
|
import FlutterMacOS
|
|
#endif
|
|
|
|
/// Protocol for shared MpvPlayerPlugin method handlers across iOS, tvOS, 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, restoreOnWindowVisible: Bool)
|
|
func updatePlayerFrame()
|
|
|
|
/// Invoked after the `pause` property is applied via setProperty so each
|
|
/// platform can sync its PiP/idle bookkeeping (iOS: invalidate the PiP
|
|
/// playback state + timebase; macOS: setPlaying/setPaused).
|
|
func didSetPauseProperty(value: String)
|
|
}
|
|
|
|
extension MpvPluginShared {
|
|
|
|
func handleSetProperty(call: FlutterMethodCall, result: @escaping FlutterResult) {
|
|
guard let args = call.arguments as? [String: Any],
|
|
let name = args["name"] as? String,
|
|
let value = args["value"] as? String
|
|
else {
|
|
result(
|
|
FlutterError(
|
|
code: "INVALID_ARGS", message: "Missing 'name' or 'value' argument",
|
|
details: nil))
|
|
return
|
|
}
|
|
|
|
guard let core = coreBase else {
|
|
result(
|
|
FlutterError(
|
|
code: "NOT_INITIALIZED", message: "MPV player is not initialized", details: nil))
|
|
return
|
|
}
|
|
|
|
core.setPropertyAsync(name, value: value) { [weak self] propertyResult in
|
|
switch propertyResult {
|
|
case .success:
|
|
if name == "pause" {
|
|
self?.didSetPauseProperty(value: value)
|
|
}
|
|
result(nil)
|
|
case .failure(let error):
|
|
let lifecycleUnavailable = error is MpvLifecycleUnavailableError
|
|
result(
|
|
FlutterError(
|
|
code: lifecycleUnavailable ? "NOT_INITIALIZED" : "SET_PROPERTY_FAILED",
|
|
message:
|
|
lifecycleUnavailable
|
|
? "MPV player is not initialized"
|
|
: "MPV rejected or cancelled the property write",
|
|
details: nil))
|
|
}
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
coreBase?.getPropertyAsync(name) { propertyResult in
|
|
switch propertyResult {
|
|
case .success(let value):
|
|
result(value)
|
|
case .failure:
|
|
result(nil)
|
|
}
|
|
} ?? result(nil)
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// `loadfile` answers with the playlist entry it created so Dart can tie
|
|
// the load to that source's start-file/playback-restart/end-file events;
|
|
// every other command answers nil.
|
|
coreBase?.commandAsync(commandArgs) { commandResult in
|
|
switch commandResult {
|
|
case .success(let playlistEntryId):
|
|
result(playlistEntryId.map { ["playlistEntryId": $0] })
|
|
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
|
|
}
|
|
let restoreOnWindowVisible = args["restoreOnWindowVisible"] as? Bool ?? false
|
|
|
|
DispatchQueue.main.async { [weak self] in
|
|
self?.setPlayerVisible(visible, restoreOnWindowVisible: restoreOnWindowVisible)
|
|
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?, sourceId: Int64?) {
|
|
guard let eventSink = eventSink, let propId = nameToId[name] else { return }
|
|
let message: [Any?] = [propId, value, sourceId]
|
|
eventSink(message)
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|