This commit is contained in:
edde746
2025-12-02 15:46:19 +01:00
parent d9cdde66d8
commit 1daded6ec6
13 changed files with 1244 additions and 571 deletions
-24
View File
@@ -2,10 +2,6 @@ PODS:
- connectivity_plus (0.0.1):
- Flutter
- Flutter (1.0.0)
- media_kit_libs_ios_video (1.0.4):
- Flutter
- media_kit_video (0.0.1):
- Flutter
- os_media_controls (0.0.1):
- Flutter
- package_info_plus (0.4.5):
@@ -21,34 +17,22 @@ PODS:
- FlutterMacOS
- url_launcher_ios (0.0.1):
- Flutter
- volume_controller (0.0.1):
- Flutter
- wakelock_plus (0.0.1):
- Flutter
DEPENDENCIES:
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
- Flutter (from `Flutter`)
- media_kit_libs_ios_video (from `.symlinks/plugins/media_kit_libs_ios_video/ios`)
- media_kit_video (from `.symlinks/plugins/media_kit_video/ios`)
- os_media_controls (from `.symlinks/plugins/os_media_controls/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
- volume_controller (from `.symlinks/plugins/volume_controller/ios`)
- wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
EXTERNAL SOURCES:
connectivity_plus:
:path: ".symlinks/plugins/connectivity_plus/ios"
Flutter:
:path: Flutter
media_kit_libs_ios_video:
:path: ".symlinks/plugins/media_kit_libs_ios_video/ios"
media_kit_video:
:path: ".symlinks/plugins/media_kit_video/ios"
os_media_controls:
:path: ".symlinks/plugins/os_media_controls/ios"
package_info_plus:
@@ -61,24 +45,16 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/sqflite_darwin/darwin"
url_launcher_ios:
:path: ".symlinks/plugins/url_launcher_ios/ios"
volume_controller:
:path: ".symlinks/plugins/volume_controller/ios"
wakelock_plus:
:path: ".symlinks/plugins/wakelock_plus/ios"
SPEC CHECKSUMS:
connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
media_kit_libs_ios_video: 5a18affdb97d1f5d466dc79988b13eff6c5e2854
media_kit_video: 1746e198cb697d1ffb734b1d05ec429d1fcd1474
os_media_controls: 86dceab6245a5325af90fc0fdebe243c42d789b4
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
volume_controller: 3657a1f65bedb98fa41ff7dc5793537919f31b12
wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556
PODFILE CHECKSUM: 3c63482e143d1b91d2d2560aee9fb04ecc74ac7e
+40
View File
@@ -12,6 +12,9 @@
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
50DEEA8E23D1B433DE5341D6 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2C12D873DE8572579684A82C /* Pods_Runner.framework */; };
6A48DFB02EA70C7100C1F7CD /* plezy.icon in Resources */ = {isa = PBXBuildFile; fileRef = 6A48DFAF2EA70C7100C1F7CD /* plezy.icon */; };
6A8A46212EDB320D0057B88C /* MPVKit-GPL in Frameworks */ = {isa = PBXBuildFile; productRef = 6A8A46202EDB320D0057B88C /* MPVKit-GPL */; };
6A8A46252EDB370C0057B88C /* MpvPlayerPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A8A46232EDB370C0057B88C /* MpvPlayerPlugin.swift */; };
6A8A46262EDB370C0057B88C /* MpvPlayerCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A8A46222EDB370C0057B88C /* MpvPlayerCore.swift */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
@@ -52,6 +55,8 @@
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
5B81FA7048BFA976F49D555E /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; 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>"; };
6A8A46232EDB370C0057B88C /* MpvPlayerPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MpvPlayerPlugin.swift; 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>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
@@ -81,6 +86,7 @@
buildActionMask = 2147483647;
files = (
50DEEA8E23D1B433DE5341D6 /* Pods_Runner.framework in Frameworks */,
6A8A46212EDB320D0057B88C /* MPVKit-GPL in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -104,6 +110,15 @@
name = Frameworks;
sourceTree = "<group>";
};
6A8A46242EDB370C0057B88C /* MpvPlayer */ = {
isa = PBXGroup;
children = (
6A8A46222EDB370C0057B88C /* MpvPlayerCore.swift */,
6A8A46232EDB370C0057B88C /* MpvPlayerPlugin.swift */,
);
path = MpvPlayer;
sourceTree = "<group>";
};
875C8AEFBBA3759A1849D674 /* Pods */ = {
isa = PBXGroup;
children = (
@@ -153,6 +168,7 @@
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
6A8A46242EDB370C0057B88C /* MpvPlayer */,
97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
97C147021CF9000F007C117D /* Info.plist */,
@@ -237,6 +253,9 @@
Base,
);
mainGroup = 97C146E51CF9000F007C117D;
packageReferences = (
6A8A461F2EDB320D0057B88C /* XCRemoteSwiftPackageReference "MPVKit" */,
);
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
projectDirPath = "";
projectRoot = "";
@@ -376,6 +395,8 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
6A8A46252EDB370C0057B88C /* MpvPlayerPlugin.swift in Sources */,
6A8A46262EDB370C0057B88C /* MpvPlayerCore.swift in Sources */,
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
);
@@ -734,6 +755,25 @@
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */
6A8A461F2EDB320D0057B88C /* XCRemoteSwiftPackageReference "MPVKit" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/edde746/MPVKit";
requirement = {
kind = revision;
revision = a8ae0d2f08df6188c662b7670bddb97131db8c5a;
};
};
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
6A8A46202EDB320D0057B88C /* MPVKit-GPL */ = {
isa = XCSwiftPackageProductDependency;
package = 6A8A461F2EDB320D0057B88C /* XCRemoteSwiftPackageReference "MPVKit" */;
productName = "MPVKit-GPL";
};
/* End XCSwiftPackageProductDependency section */
};
rootObject = 97C146E61CF9000F007C117D /* Project object */;
}
@@ -0,0 +1,14 @@
{
"originHash" : "c535d3cdb62bd4e11e67f3c9e68290fa7ee9306634c5f56d2b0396eb75ed41ee",
"pins" : [
{
"identity" : "mpvkit",
"kind" : "remoteSourceControl",
"location" : "https://github.com/edde746/MPVKit",
"state" : {
"revision" : "a8ae0d2f08df6188c662b7670bddb97131db8c5a"
}
}
],
"version" : 3
}
@@ -0,0 +1,14 @@
{
"originHash" : "c535d3cdb62bd4e11e67f3c9e68290fa7ee9306634c5f56d2b0396eb75ed41ee",
"pins" : [
{
"identity" : "mpvkit",
"kind" : "remoteSourceControl",
"location" : "https://github.com/edde746/MPVKit",
"state" : {
"revision" : "a8ae0d2f08df6188c662b7670bddb97131db8c5a"
}
}
],
"version" : 3
}
+5
View File
@@ -10,6 +10,11 @@ import AVFoundation
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
// Register MPV player plugin
if let registrar = self.registrar(forPlugin: "MpvPlayerPlugin") {
MpvPlayerPlugin.register(with: registrar)
}
// Configure audio session for media playback
do {
let session = AVAudioSession.sharedInstance()
+411
View File
@@ -0,0 +1,411 @@
import UIKit
import Libmpv
/// Protocol for receiving player events
protocol MpvPlayerDelegate: AnyObject {
func onPropertyChange(name: String, value: Any?)
func onEvent(name: String, data: [String: Any]?)
}
// Workaround for MoltenVK problems that cause flicker
// https://github.com/mpv-player/mpv/pull/13651
private class MetalLayer: CAMetalLayer {
override var drawableSize: CGSize {
get { return super.drawableSize }
set {
if Int(newValue.width) > 1 && Int(newValue.height) > 1 {
super.drawableSize = newValue
}
}
}
// Fix for target-colorspace-hint - needs main thread for EDR
@available(iOS 16.0, *)
override var wantsExtendedDynamicRangeContent: Bool {
get { return super.wantsExtendedDynamicRangeContent }
set {
if Thread.isMainThread {
super.wantsExtendedDynamicRangeContent = newValue
} else {
DispatchQueue.main.sync {
super.wantsExtendedDynamicRangeContent = newValue
}
}
}
}
}
/// Core MPV player using Metal rendering for iOS
class MpvPlayerCore: NSObject {
// MARK: - Properties
private var metalLayer: MetalLayer?
private var mpv: OpaquePointer?
private weak var window: UIWindow?
private lazy var queue = DispatchQueue(label: "mpv", qos: .userInitiated)
weak var delegate: MpvPlayerDelegate?
private(set) var isInitialized = false
// MARK: - Initialization
func initialize(in window: UIWindow) -> Bool {
guard !isInitialized else {
print("[MpvPlayerCore] Already initialized")
return true
}
self.window = window
// Create Metal layer for video rendering
let layer = MetalLayer()
layer.frame = window.bounds
layer.contentsScale = UIScreen.main.nativeScale
layer.framebufferOnly = true
layer.backgroundColor = UIColor.black.cgColor
metalLayer = layer
// Add Metal layer to WINDOW layer (behind Flutter's view controller)
window.layer.insertSublayer(layer, at: 0)
print("[MpvPlayerCore] Metal layer added to window, frame: \(layer.frame)")
// Initialize MPV with this Metal layer
guard setupMpv() else {
print("[MpvPlayerCore] Failed to setup MPV")
layer.removeFromSuperlayer()
metalLayer = nil
return false
}
// Setup background/foreground notifications
setupNotifications()
isInitialized = true
print("[MpvPlayerCore] Initialized successfully with MPV")
return true
}
private func setupMpv() -> Bool {
guard let metalLayer = metalLayer else { return false }
mpv = mpv_create()
guard mpv != nil else {
print("[MpvPlayerCore] Failed to create MPV context")
return false
}
// Logging
#if DEBUG
checkError(mpv_request_log_messages(mpv, "info"))
#else
checkError(mpv_request_log_messages(mpv, "warn"))
#endif
// Set the Metal layer as the render target (must use local var for &)
var layer = metalLayer
checkError(mpv_set_option(mpv, "wid", MPV_FORMAT_INT64, &layer))
// Video output settings for Metal/Vulkan
checkError(mpv_set_option_string(mpv, "vo", "gpu-next"))
checkError(mpv_set_option_string(mpv, "gpu-api", "vulkan"))
checkError(mpv_set_option_string(mpv, "gpu-context", "moltenvk"))
checkError(mpv_set_option_string(mpv, "hwdec", "videotoolbox"))
// Initialize MPV
let initResult = mpv_initialize(mpv)
if initResult < 0 {
print("[MpvPlayerCore] mpv_initialize failed: \(String(cString: mpv_error_string(initResult)))")
mpv_terminate_destroy(mpv)
mpv = nil
return false
}
// Set up wakeup callback for event handling
mpv_set_wakeup_callback(mpv, { ctx in
let core = Unmanaged<MpvPlayerCore>.fromOpaque(ctx!).takeUnretainedValue()
core.readEvents()
}, UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque()))
print("[MpvPlayerCore] MPV initialized successfully")
return true
}
// MARK: - Background/Foreground Handling
private func setupNotifications() {
NotificationCenter.default.addObserver(
self,
selector: #selector(enterBackground),
name: UIApplication.didEnterBackgroundNotification,
object: nil
)
NotificationCenter.default.addObserver(
self,
selector: #selector(enterForeground),
name: UIApplication.willEnterForegroundNotification,
object: nil
)
}
@objc private func enterBackground() {
// Disable video output to fix black screen when returning from background
print("[MpvPlayerCore] Entering background - disabling video")
if mpv != nil {
mpv_set_option_string(mpv, "vid", "no")
}
}
@objc private func enterForeground() {
// Re-enable video output
print("[MpvPlayerCore] Entering foreground - enabling video")
if mpv != nil {
mpv_set_option_string(mpv, "vid", "auto")
}
}
// MARK: - MPV Properties and Commands
func setProperty(_ name: String, value: String) {
guard mpv != nil else { return }
mpv_set_property_string(mpv, name, value)
}
func getProperty(_ name: String) -> String? {
guard mpv != nil else { return nil }
let cstr = mpv_get_property_string(mpv, name)
defer { mpv_free(cstr) }
return cstr.map { String(cString: $0) }
}
func observeProperty(_ name: String, format: String) {
guard mpv != nil else { return }
let mpvFormat: mpv_format
switch format {
case "double": mpvFormat = MPV_FORMAT_DOUBLE
case "flag": mpvFormat = MPV_FORMAT_FLAG
case "node": mpvFormat = MPV_FORMAT_NODE
case "string": mpvFormat = MPV_FORMAT_STRING
default: return
}
mpv_observe_property(mpv, 0, name, mpvFormat)
}
func command(_ args: [String]) {
guard mpv != nil, !args.isEmpty else { return }
command(args[0], args: Array(args.dropFirst()))
}
// MARK: - Visibility
func setVisible(_ visible: Bool) {
guard let layer = metalLayer else { return }
if visible {
// Re-insert at the bottom of the window layer stack
layer.removeFromSuperlayer()
if let windowLayer = window?.layer {
windowLayer.insertSublayer(layer, at: 0)
}
}
layer.isHidden = !visible
print("[MpvPlayerCore] setVisible(\(visible))")
}
func updateFrame(_ frame: CGRect? = nil) {
guard let metalLayer = metalLayer else { return }
if let frame = frame {
metalLayer.frame = frame
} else if let window = window {
metalLayer.frame = window.bounds
}
// Update drawable size for proper scaling
let scale = UIScreen.main.nativeScale
metalLayer.drawableSize = CGSize(
width: metalLayer.frame.width * scale,
height: metalLayer.frame.height * scale
)
print("[MpvPlayerCore] updateFrame: \(metalLayer.frame)")
}
// MARK: - Private Helpers
private func command(_ cmd: String, args: [String] = []) {
guard mpv != nil else { return }
// Build array of C strings for mpv_command
var cargs: [UnsafeMutablePointer<CChar>?] = ([cmd] + args).map { strdup($0) }
cargs.append(nil) // null-terminate
defer {
for ptr in cargs {
free(ptr)
}
}
// mpv_command expects UnsafePointer, use withUnsafeBufferPointer for the conversion
cargs.withUnsafeBufferPointer { buffer in
var constPtrs = buffer.map { UnsafePointer($0) }
_ = mpv_command(mpv, &constPtrs)
}
}
private func readEvents() {
queue.async { [weak self] in
guard let self = self, let mpv = self.mpv else { return }
while true {
let event = mpv_wait_event(mpv, 0)
guard let eventPtr = event else { break }
if eventPtr.pointee.event_id == MPV_EVENT_NONE {
break
}
self.handleEvent(eventPtr.pointee)
}
}
}
private func handleEvent(_ event: mpv_event) {
switch event.event_id {
case MPV_EVENT_PROPERTY_CHANGE:
guard let data = event.data else { break }
let property = data.assumingMemoryBound(to: mpv_event_property.self).pointee
let name = String(cString: property.name)
handlePropertyChange(name: name, property: property)
case MPV_EVENT_FILE_LOADED:
DispatchQueue.main.async {
self.delegate?.onEvent(name: "file-loaded", data: nil)
}
case MPV_EVENT_END_FILE:
DispatchQueue.main.async {
self.delegate?.onEvent(name: "end-file", data: nil)
}
case MPV_EVENT_SHUTDOWN:
print("[MpvPlayerCore] MPV shutdown event")
case MPV_EVENT_LOG_MESSAGE:
if let msgPtr = event.data?.assumingMemoryBound(to: mpv_event_log_message.self) {
let msg = msgPtr.pointee
if let prefix = msg.prefix, let level = msg.level, let text = msg.text {
print("[MPV:\(String(cString: prefix))] \(String(cString: level)): \(String(cString: text))", terminator: "")
}
}
default:
break
}
}
private func handlePropertyChange(name: String, property: mpv_event_property) {
var value: Any?
switch property.format {
case MPV_FORMAT_DOUBLE:
if let ptr = property.data {
value = ptr.assumingMemoryBound(to: Double.self).pointee
}
case MPV_FORMAT_FLAG:
if let ptr = property.data {
value = ptr.assumingMemoryBound(to: Int32.self).pointee != 0
}
case MPV_FORMAT_NODE:
if let ptr = property.data {
let node = ptr.assumingMemoryBound(to: mpv_node.self).pointee
value = convertNode(node)
}
case MPV_FORMAT_STRING:
if let ptr = property.data {
let cstr = ptr.assumingMemoryBound(to: UnsafePointer<CChar>?.self).pointee
value = cstr.map { String(cString: $0) }
}
default:
break
}
DispatchQueue.main.async {
self.delegate?.onPropertyChange(name: name, value: value)
}
}
private func convertNode(_ node: mpv_node) -> Any? {
switch node.format {
case MPV_FORMAT_STRING:
return node.u.string.map { String(cString: $0) }
case MPV_FORMAT_FLAG:
return node.u.flag != 0
case MPV_FORMAT_INT64:
return node.u.int64
case MPV_FORMAT_DOUBLE:
return node.u.double_
case MPV_FORMAT_NODE_ARRAY:
guard let list = node.u.list?.pointee else { return nil }
var array = [Any]()
for i in 0..<Int(list.num) {
if let item = convertNode(list.values[i]) {
array.append(item)
}
}
return array
case MPV_FORMAT_NODE_MAP:
guard let list = node.u.list?.pointee else { return nil }
var dict = [String: Any]()
for i in 0..<Int(list.num) {
if let key = list.keys?[i].map({ String(cString: $0) }),
let val = convertNode(list.values[i]) {
dict[key] = val
}
}
return dict
default:
return nil
}
}
private func checkError(_ status: CInt) {
if status < 0 {
print("[MpvPlayerCore] MPV error: \(String(cString: mpv_error_string(status)))")
}
}
// MARK: - Cleanup
func dispose() {
NotificationCenter.default.removeObserver(self)
if let mpv = mpv {
mpv_terminate_destroy(mpv)
self.mpv = nil
}
metalLayer?.removeFromSuperlayer()
metalLayer = nil
isInitialized = false
print("[MpvPlayerCore] Disposed")
}
deinit {
dispose()
}
}
+235
View File
@@ -0,0 +1,235 @@
import UIKit
import Flutter
/// Flutter plugin that bridges MPV player to Dart via method and event channels
class MpvPlayerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, MpvPlayerDelegate {
// MARK: - Properties
private var playerCore: MpvPlayerCore?
private var eventSink: FlutterEventSink?
private weak var registrar: FlutterPluginRegistrar?
// MARK: - FlutterPlugin Registration
static func register(with registrar: FlutterPluginRegistrar) {
// Method channel for commands
let methodChannel = FlutterMethodChannel(
name: "com.plezy/mpv_player",
binaryMessenger: registrar.messenger()
)
// Event channel for state updates
let eventChannel = FlutterEventChannel(
name: "com.plezy/mpv_player/events",
binaryMessenger: registrar.messenger()
)
let instance = MpvPlayerPlugin()
instance.registrar = registrar
registrar.addMethodCallDelegate(instance, channel: methodChannel)
eventChannel.setStreamHandler(instance)
print("[MpvPlayerPlugin] Registered with Flutter")
}
// MARK: - FlutterStreamHandler
func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
self.eventSink = events
print("[MpvPlayerPlugin] Event stream connected")
return nil
}
func onCancel(withArguments arguments: Any?) -> FlutterError? {
self.eventSink = nil
print("[MpvPlayerPlugin] Event stream disconnected")
return nil
}
// MARK: - FlutterPlugin Method Handler
func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
switch call.method {
case "initialize":
handleInitialize(result: result)
case "dispose":
handleDispose(result: result)
case "setProperty":
handleSetProperty(call: call, result: result)
case "getProperty":
handleGetProperty(call: call, result: result)
case "observeProperty":
handleObserveProperty(call: call, result: result)
case "command":
handleCommand(call: call, result: result)
case "setVisible":
handleSetVisible(call: call, result: result)
case "isInitialized":
result(playerCore?.isInitialized ?? false)
default:
result(FlutterMethodNotImplemented)
}
}
// MARK: - Method Handlers
private func handleInitialize(result: @escaping FlutterResult) {
DispatchQueue.main.async { [weak self] in
guard let self = self else {
result(FlutterError(code: "ERROR", message: "Plugin deallocated", details: nil))
return
}
// Check if already initialized
if self.playerCore?.isInitialized == true {
print("[MpvPlayerPlugin] Already initialized")
result(true)
return
}
// Find the key window
guard let window = self.findKeyWindow() else {
print("[MpvPlayerPlugin] Failed to find key window")
result(FlutterError(code: "NO_WINDOW", message: "Could not find key window", details: nil))
return
}
// Create and initialize player core
let core = MpvPlayerCore()
core.delegate = self
guard core.initialize(in: window) else {
print("[MpvPlayerPlugin] Failed to initialize MPV")
result(FlutterError(code: "MPV_INIT_FAILED", message: "Failed to initialize MPV", details: nil))
return
}
self.playerCore = core
// Start hidden
core.setVisible(false)
print("[MpvPlayerPlugin] Initialized successfully")
result(true)
}
}
private func handleDispose(result: @escaping FlutterResult) {
DispatchQueue.main.async { [weak self] in
self?.playerCore?.dispose()
self?.playerCore = nil
print("[MpvPlayerPlugin] Disposed")
result(nil)
}
}
private 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
}
playerCore?.setProperty(name, value: value)
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 else {
result(FlutterError(code: "INVALID_ARGS", message: "Missing 'name' or 'format' argument", details: nil))
return
}
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?.command(commandArgs)
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)
}
}
// MARK: - MpvPlayerDelegate
func onPropertyChange(name: String, value: Any?) {
guard let eventSink = eventSink else { return }
var event: [String: Any] = ["type": "property", "name": name]
if let value = value {
event["value"] = value
}
eventSink(event)
}
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
private func findKeyWindow() -> UIWindow? {
guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let window = windowScene.windows.first(where: { $0.isKeyWindow }) else {
return nil
}
return window
}
}