Files
plezy/shared/apple/MpvPlayer/MpvPlayerPluginShared.swift
T
edde746 baa31742cb fix(player): bind live TV clock generations to the source the load reports
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.
2026-09-06 13:29:37 +02:00

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)
}
}