diff --git a/modules/mpv-player/ios/MpvPlayer.podspec b/modules/mpv-player/ios/MpvPlayer.podspec index 75411574..c64f146b 100644 --- a/modules/mpv-player/ios/MpvPlayer.podspec +++ b/modules/mpv-player/ios/MpvPlayer.podspec @@ -7,14 +7,10 @@ Pod::Spec.new do |s| s.source = { git: '' } s.homepage = 'https://github.com/mpvkit/MPVKit' s.platforms = { :ios => '13.4', :tvos => '13.4' } + s.static_framework = true s.dependency 'ExpoModulesCore' - - spm_dependency(s, - url: 'https://github.com/mpvkit/MPVKit.git', - requirement: {kind: 'upToNextMajorVersion', minimumVersion: '0.40.0'}, - products: ['MPVKit'] - ) + s.dependency 'MPVKit', '~> 0.40.6' # Swift/Objective-C compatibility s.pod_target_xcconfig = { diff --git a/modules/mpv-player/ios/MpvPlayerView.swift b/modules/mpv-player/ios/MpvPlayerView.swift index 9683d38f..c7857a8b 100644 --- a/modules/mpv-player/ios/MpvPlayerView.swift +++ b/modules/mpv-player/ios/MpvPlayerView.swift @@ -5,9 +5,7 @@ import UIKit // MARK: - Metal Layer class MetalLayer: CAMetalLayer { - // workaround for a MoltenVK that sets the drawableSize to 1x1 to forcefully complete - // the presentation, this causes flicker and the drawableSize possibly staying at 1x1 - // https://github.com/mpv-player/mpv/pull/13651 + // Workaround for MoltenVK issue that sets drawableSize to 1x1 override var drawableSize: CGSize { get { return super.drawableSize } set { @@ -17,13 +15,10 @@ class MetalLayer: CAMetalLayer { } } - // Hack for fix [target-colorspace-hint] option: - // Update wantsExtendedDynamicRangeContent only available in iOS 16.0+ + // Handle extended dynamic range content on iOS 16+ @available(iOS 16.0, *) override var wantsExtendedDynamicRangeContent: Bool { - get { - return super.wantsExtendedDynamicRangeContent - } + get { return super.wantsExtendedDynamicRangeContent } set { if Thread.isMainThread { super.wantsExtendedDynamicRangeContent = newValue @@ -35,7 +30,7 @@ class MetalLayer: CAMetalLayer { } } - // Helper function to conditionally set HDR content + // Helper to set HDR content safely func setHDRContent(_ enabled: Bool) { if #available(iOS 16.0, *) { if Thread.isMainThread { @@ -49,14 +44,31 @@ class MetalLayer: CAMetalLayer { } } +// MARK: - MPV Properties +enum MpvProperty { + static let timePosition = "time-pos" + static let duration = "duration" + static let pause = "pause" + static let pausedForCache = "paused-for-cache" + static let videoParamsSigPeak = "video-params/sig-peak" +} + +// MARK: - Protocol +protocol MpvPlayerDelegate: AnyObject { + func propertyChanged(mpv: OpaquePointer, propertyName: String, value: Any?) +} + // MARK: - MPV Player View class MpvPlayerView: ExpoView { - private var mpvViewController: MpvMetalViewController? - private var coordinator: MpvMetalPlayerView.Coordinator? + // MARK: - Properties + private var playerController: MpvMetalViewController? + private var coordinator: MpvMetalPlayerView.Coordinator? + private var hostingController: UIHostingController? private var source: [String: Any]? - // Event emitters + // MARK: - Event Emitters + @objc var onVideoStateChange: RCTDirectEventBlock? @objc var onVideoLoadStart: RCTDirectEventBlock? @objc var onVideoLoadEnd: RCTDirectEventBlock? @@ -65,42 +77,48 @@ class MpvPlayerView: ExpoView { @objc var onPlaybackStateChanged: RCTDirectEventBlock? @objc var onPipStarted: RCTDirectEventBlock? + // MARK: - Initialization + required init(appContext: AppContext? = nil) { super.init(appContext: appContext) setupView() } + // MARK: - Setup + private func setupView() { backgroundColor = .black - // Create coordinator + // Create coordinator and configure property change handling let coordinator = MpvMetalPlayerView.Coordinator() - coordinator.onPropertyChange = { [weak self] _, propertyName, data in - self?.handlePropertyChange(propertyName: propertyName, data: data) + coordinator.onPropertyChange = { [weak self] _, propertyName, value in + DispatchQueue.main.async { + self?.handlePropertyChange(propertyName: propertyName, value: value) + } } self.coordinator = coordinator - // Create MPV controller - let mpvController = MpvMetalViewController() - mpvController.playDelegate = coordinator - coordinator.player = mpvController + // Create player controller + let controller = MpvMetalViewController() + controller.delegate = coordinator + coordinator.player = controller + playerController = controller - mpvViewController = mpvController - - // Add to view hierarchy - let hostingController = UIHostingController( + // Create and add SwiftUI hosting controller + let hostController = UIHostingController( rootView: MpvMetalPlayerView(coordinator: coordinator) ) + self.hostingController = hostController - hostingController.view.translatesAutoresizingMaskIntoConstraints = false - hostingController.view.backgroundColor = .clear + hostController.view.translatesAutoresizingMaskIntoConstraints = false + hostController.view.backgroundColor = .clear - addSubview(hostingController.view) + addSubview(hostController.view) NSLayoutConstraint.activate([ - hostingController.view.leadingAnchor.constraint(equalTo: leadingAnchor), - hostingController.view.trailingAnchor.constraint(equalTo: trailingAnchor), - hostingController.view.topAnchor.constraint(equalTo: topAnchor), - hostingController.view.bottomAnchor.constraint(equalTo: bottomAnchor), + hostController.view.leadingAnchor.constraint(equalTo: leadingAnchor), + hostController.view.trailingAnchor.constraint(equalTo: trailingAnchor), + hostController.view.topAnchor.constraint(equalTo: topAnchor), + hostController.view.bottomAnchor.constraint(equalTo: bottomAnchor), ]) } @@ -125,78 +143,96 @@ class MpvPlayerView: ExpoView { } func startPictureInPicture() { - self.onPipStarted?(["pipStarted": false, "target": self.reactTag as Any]) + DispatchQueue.main.async { [weak self] in + guard let self = self else { return } + self.onPipStarted?(["pipStarted": false, "target": self.reactTag as Any]) + } } func play() { - mpvViewController?.play() + playerController?.play() } func pause() { - mpvViewController?.pause() + playerController?.pause() } func stop() { - mpvViewController?.command("stop", args: []) + playerController?.command("stop", args: []) } func seekTo(_ time: Int32) { let seconds = Double(time) / 1000.0 - mpvViewController?.command("seek", args: ["\(seconds)"]) + playerController?.command("seek", args: ["\(seconds)"]) } func setAudioTrack(_ trackIndex: Int) { - mpvViewController?.command("set", args: ["aid", "\(trackIndex)"]) + playerController?.command("set", args: ["aid", "\(trackIndex)"]) } - func getAudioTracks() -> [[String: Any]]? { + func getAudioTracks() -> [[String: Any]] { + // Implementation would go here return [] } func setSubtitleTrack(_ trackIndex: Int) { - mpvViewController?.command("set", args: ["sid", "\(trackIndex)"]) + playerController?.command("set", args: ["sid", "\(trackIndex)"]) } - func getSubtitleTracks() -> [[String: Any]]? { + func getSubtitleTracks() -> [[String: Any]] { + // Implementation would go here return [] } func setSubtitleURL(_ subtitleURL: String, name: String) { guard let url = URL(string: subtitleURL) else { return } - mpvViewController?.command("sub-add", args: [url.absoluteString]) + playerController?.command("sub-add", args: [url.absoluteString]) } // MARK: - Private Methods - private func handlePropertyChange(propertyName: String, data: Any?) { + private func handlePropertyChange(propertyName: String, value: Any?) { + guard let playerController = playerController else { return } + switch propertyName { case MpvProperty.pausedForCache: - let isBuffering = data as? Bool ?? false - onVideoStateChange?(["isBuffering": isBuffering, "target": reactTag as Any]) - - case MpvProperty.timePosition: - if let position = data as? Double { - let timeMs = position * 1000 - onVideoProgress?([ - "currentTime": timeMs, - "duration": getVideoDuration() * 1000, - "isPlaying": !isPaused(), - "isBuffering": isBuffering(), - "target": reactTag as Any, + let isBuffering = value as? Bool ?? false + DispatchQueue.main.async { [weak self] in + guard let self = self else { return } + self.onVideoStateChange?([ + "isBuffering": isBuffering, "target": self.reactTag as Any, ]) } + case MpvProperty.timePosition: + if let position = value as? Double { + let timeMs = position * 1000 + DispatchQueue.main.async { [weak self] in + guard let self = self else { return } + self.onVideoProgress?([ + "currentTime": timeMs, + "duration": self.getVideoDuration() * 1000, + "isPlaying": !self.isPaused(), + "isBuffering": self.isBuffering(), + "target": self.reactTag as Any, + ]) + } + } + case MpvProperty.pause: - if let isPaused = data as? Bool { + if let isPaused = value as? Bool { let state = isPaused ? "Paused" : "Playing" - onPlaybackStateChanged?([ - "state": state, - "isPlaying": !isPaused, - "isBuffering": isBuffering(), - "currentTime": getCurrentTime() * 1000, - "duration": getVideoDuration() * 1000, - "target": reactTag as Any, - ]) + DispatchQueue.main.async { [weak self] in + guard let self = self else { return } + self.onPlaybackStateChanged?([ + "state": state, + "isPlaying": !isPaused, + "isBuffering": self.isBuffering(), + "currentTime": self.getCurrentTime() * 1000, + "duration": self.getVideoDuration() * 1000, + "target": self.reactTag as Any, + ]) + } } default: @@ -205,60 +241,80 @@ class MpvPlayerView: ExpoView { } private func isPaused() -> Bool { - return mpvViewController?.getFlag(MpvProperty.pause) ?? true + return playerController?.getFlag(MpvProperty.pause) ?? true } private func isBuffering() -> Bool { - return mpvViewController?.getFlag(MpvProperty.pausedForCache) ?? false + return playerController?.getFlag(MpvProperty.pausedForCache) ?? false } private func getCurrentTime() -> Double { - return mpvViewController?.getDouble(MpvProperty.timePosition) ?? 0 + return playerController?.getDouble(MpvProperty.timePosition) ?? 0 } private func getVideoDuration() -> Double { - return mpvViewController?.getDouble(MpvProperty.duration) ?? 0 + return playerController?.getDouble(MpvProperty.duration) ?? 0 } -} -// MARK: - MPV Properties and Protocol -enum MpvProperty { - static let timePosition = "time-pos" - static let duration = "duration" - static let pause = "pause" - static let pausedForCache = "paused-for-cache" - static let videoParamsSigPeak = "video-params/sig-peak" -} + // MARK: - Cleanup -protocol MpvPlayerDelegate: AnyObject { - func propertyChange(mpv: OpaquePointer, propertyName: String, data: Any?) + override func removeFromSuperview() { + cleanup() + super.removeFromSuperview() + } + + private func cleanup() { + // Ensure everything completes before cleanup + playerController?.mpvQueue.sync {} + + // Stop playback + stop() + + // Break reference cycles + coordinator?.player = nil + coordinator?.onPropertyChange = nil + playerController?.delegate = nil + + // Remove from view hierarchy + hostingController?.view.removeFromSuperview() + hostingController = nil + coordinator = nil + playerController = nil + } + + deinit { + cleanup() + } } // MARK: - SwiftUI Wrapper struct MpvMetalPlayerView: UIViewControllerRepresentable { @ObservedObject var coordinator: Coordinator - func makeUIViewController(context: Context) -> some UIViewController { - let mpv = MpvMetalViewController() - mpv.playDelegate = coordinator - mpv.playUrl = coordinator.playUrl + func makeUIViewController(context: Context) -> UIViewController { + let controller = MpvMetalViewController() + controller.delegate = coordinator + controller.playUrl = coordinator.playUrl - context.coordinator.player = mpv - return mpv + coordinator.player = controller + return controller } - func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) { + func updateUIViewController(_ uiViewController: UIViewController, context: Context) { + // Updates if needed } - public func makeCoordinator() -> Coordinator { + func makeCoordinator() -> Coordinator { coordinator } + // Method for playing media func play(_ url: URL) -> Self { coordinator.playUrl = url return self } + // Method for handling property changes func onPropertyChange(_ handler: @escaping (MpvMetalViewController, String, Any?) -> Void) -> Self { @@ -267,9 +323,8 @@ struct MpvMetalPlayerView: UIViewControllerRepresentable { } @MainActor - public final class Coordinator: MpvPlayerDelegate, ObservableObject { + final class Coordinator: MpvPlayerDelegate, ObservableObject { weak var player: MpvMetalViewController? - var playUrl: URL? var onPropertyChange: ((MpvMetalViewController, String, Any?) -> Void)? @@ -277,58 +332,62 @@ struct MpvMetalPlayerView: UIViewControllerRepresentable { player?.loadFile(url) } - func propertyChange(mpv: OpaquePointer, propertyName: String, data: Any?) { + func propertyChanged(mpv: OpaquePointer, propertyName: String, value: Any?) { guard let player = player else { return } - - self.onPropertyChange?(player, propertyName, data) + onPropertyChange?(player, propertyName, value) } } } -// MARK: - MPV Metal View Controller +// MARK: - Player Controller final class MpvMetalViewController: UIViewController { + // MARK: - Properties + var metalLayer = MetalLayer() - var mpv: OpaquePointer! - weak var playDelegate: MpvPlayerDelegate? - lazy var queue = DispatchQueue(label: "mpv", qos: .userInitiated) + var mpv: OpaquePointer? + weak var delegate: MpvPlayerDelegate? + let mpvQueue = DispatchQueue(label: "mpv.queue", qos: .userInitiated) + + private var isBeingDeallocated = false + private var contextPointer: UnsafeMutableRawPointer? var playUrl: URL? + var hdrAvailable: Bool { if #available(iOS 16.0, *) { let maxEDRRange = view.window?.screen.potentialEDRHeadroom ?? 1.0 let sigPeak = getDouble(MpvProperty.videoParamsSigPeak) - // display screen support HDR and current playing HDR video return maxEDRRange > 1.0 && sigPeak > 1.0 } else { - return false // HDR not available on iOS < 16.0 + return false } } + var hdrEnabled = false { didSet { - // FIXME: target-colorspace-hint does not support being changed at runtime. - // this option should be set as early as possible otherwise can cause issues - // not recommended to use this way. + guard let mpv = mpv else { return } + if hdrEnabled { - checkError(mpv_set_option_string(mpv, "target-colorspace-hint", "yes")) + mpv_set_option_string(mpv, "target-colorspace-hint", "yes") metalLayer.setHDRContent(true) } else { - checkError(mpv_set_option_string(mpv, "target-colorspace-hint", "no")) + mpv_set_option_string(mpv, "target-colorspace-hint", "no") metalLayer.setHDRContent(false) } } } + // Add a new property to track shutdown state + private var isShuttingDown = false + private let syncQueue = DispatchQueue(label: "com.mpv.sync", qos: .userInitiated) + + // MARK: - Lifecycle + override func viewDidLoad() { super.viewDidLoad() - metalLayer.frame = view.frame - metalLayer.contentsScale = UIScreen.main.nativeScale - metalLayer.framebufferOnly = true - metalLayer.backgroundColor = UIColor.black.cgColor - - view.layer.addSublayer(metalLayer) - - setupMpv() + setupMetalLayer() + setupMPV() if let url = playUrl { loadFile(url) @@ -337,53 +396,128 @@ final class MpvMetalViewController: UIViewController { override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() - - metalLayer.frame = view.frame + metalLayer.frame = view.bounds } - func setupMpv() { - mpv = mpv_create() - if mpv == nil { - print("failed creating context\n") - exit(1) + deinit { + // Use syncQueue to ensure thread safety during shutdown + syncQueue.sync { + // Mark as shutting down to prevent new callbacks from running + isShuttingDown = true + isBeingDeallocated = true + + // Make sure to handle this on the mpv queue + mpvQueue.sync { + // First remove the wakeup callback to prevent any new callbacks + if let mpv = self.mpv { + mpv_set_wakeup_callback(mpv, nil, nil) + } + + // Release the container + if let contextPtr = contextPointer { + let container = Unmanaged>.fromOpaque( + contextPtr + ).takeUnretainedValue() + container.invalidate() + Unmanaged>.fromOpaque(contextPtr) + .release() + contextPointer = nil + } + + // Terminate and destroy mpv as the final step + if let mpv = self.mpv { + mpv_terminate_destroy(mpv) + self.mpv = nil + } + } + } + } + + // MARK: - Setup + + private func setupMetalLayer() { + metalLayer.frame = view.bounds + metalLayer.contentsScale = UIScreen.main.nativeScale + metalLayer.framebufferOnly = true + metalLayer.backgroundColor = UIColor.black.cgColor + + view.layer.addSublayer(metalLayer) + } + + private func setupMPV() { + guard let mpvHandle = mpv_create() else { + print("Failed to create MPV instance") + return } - // https://mpv.io/manual/stable/#options + mpv = mpvHandle + + // Configure mpv options #if DEBUG - checkError(mpv_request_log_messages(mpv, "debug")) + mpv_request_log_messages(mpvHandle, "debug") #else - checkError(mpv_request_log_messages(mpv, "no")) + mpv_request_log_messages(mpvHandle, "no") #endif - checkError(mpv_set_option(mpv, "wid", MPV_FORMAT_INT64, &metalLayer)) - checkError(mpv_set_option_string(mpv, "subs-match-os-language", "yes")) - checkError(mpv_set_option_string(mpv, "subs-fallback", "yes")) - checkError(mpv_set_option_string(mpv, "vo", "gpu-next")) - checkError(mpv_set_option_string(mpv, "gpu-api", "vulkan")) - checkError(mpv_set_option_string(mpv, "hwdec", "videotoolbox")) - checkError(mpv_set_option_string(mpv, "video-rotate", "no")) - checkError(mpv_set_option_string(mpv, "ytdl", "no")) + mpv_set_option(mpvHandle, "wid", MPV_FORMAT_INT64, &metalLayer) + mpv_set_option_string(mpvHandle, "subs-match-os-language", "yes") + mpv_set_option_string(mpvHandle, "subs-fallback", "yes") + mpv_set_option_string(mpvHandle, "vo", "gpu-next") + mpv_set_option_string(mpvHandle, "gpu-api", "vulkan") + mpv_set_option_string(mpvHandle, "hwdec", "videotoolbox") + mpv_set_option_string(mpvHandle, "video-rotate", "no") + mpv_set_option_string(mpvHandle, "ytdl", "no") - checkError(mpv_initialize(mpv)) + // Initialize mpv + let status = mpv_initialize(mpvHandle) + if status < 0 { + print("Failed to initialize MPV: \(String(cString: mpv_error_string(status)))") + mpv_terminate_destroy(mpvHandle) + mpv = nil + return + } - mpv_observe_property(mpv, 0, MpvProperty.videoParamsSigPeak, MPV_FORMAT_DOUBLE) - mpv_observe_property(mpv, 0, MpvProperty.pausedForCache, MPV_FORMAT_FLAG) - mpv_observe_property(mpv, 0, MpvProperty.timePosition, MPV_FORMAT_DOUBLE) - mpv_observe_property(mpv, 0, MpvProperty.duration, MPV_FORMAT_DOUBLE) - mpv_observe_property(mpv, 0, MpvProperty.pause, MPV_FORMAT_FLAG) + // Observe properties + mpv_observe_property(mpvHandle, 0, MpvProperty.videoParamsSigPeak, MPV_FORMAT_DOUBLE) + mpv_observe_property(mpvHandle, 0, MpvProperty.pausedForCache, MPV_FORMAT_FLAG) + mpv_observe_property(mpvHandle, 0, MpvProperty.timePosition, MPV_FORMAT_DOUBLE) + mpv_observe_property(mpvHandle, 0, MpvProperty.duration, MPV_FORMAT_DOUBLE) + mpv_observe_property(mpvHandle, 0, MpvProperty.pause, MPV_FORMAT_FLAG) + // Set up weak reference for callback with improved safety + let container = WeakContainer(value: self) + contextPointer = Unmanaged.passRetained(container).toOpaque() + + // Set wakeup callback with safer checking mpv_set_wakeup_callback( - self.mpv, - { (ctx) in - let client = unsafeBitCast(ctx, to: MpvMetalViewController.self) - client.readEvents() - }, UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque())) + mpvHandle, + { pointer in + guard let ptr = pointer else { return } + + // Get the container safely + let container = Unmanaged>.fromOpaque(ptr) + .takeUnretainedValue() + + // Access the value with additional safety checks + DispatchQueue.main.async { + if let controller = container.value { + if !controller.isBeingDeallocated { + controller.processEvents() + } else { + // If the controller is being deallocated, invalidate the container + container.invalidate() + } + } + } + }, contextPointer) } - func loadFile(_ url: URL) { - var args = [url.absoluteString] - args.append("replace") + // MARK: - MPV Methods + func loadFile(_ url: URL) { + guard let mpv = mpv else { return } + + var args = [url.absoluteString, "replace"] command("loadfile", args: args) } @@ -400,152 +534,181 @@ final class MpvMetalViewController: UIViewController { } func getDouble(_ name: String) -> Double { - guard mpv != nil else { return 0.0 } - var data = Double() + guard let mpv = mpv else { return 0.0 } + + var data = 0.0 mpv_get_property(mpv, name, MPV_FORMAT_DOUBLE, &data) return data } func getString(_ name: String) -> String? { - guard mpv != nil else { return nil } - let cstr = mpv_get_property_string(mpv, name) - let str: String? = cstr == nil ? nil : String(cString: cstr!) - mpv_free(cstr) - return str + guard let mpv = mpv else { return nil } + + guard let cString = mpv_get_property_string(mpv, name) else { return nil } + let string = String(cString: cString) + mpv_free(UnsafeMutableRawPointer(mutating: cString)) + return string } func getFlag(_ name: String) -> Bool { - var data = Int64() + guard let mpv = mpv else { return false } + + var data: Int64 = 0 mpv_get_property(mpv, name, MPV_FORMAT_FLAG, &data) return data > 0 } - func setFlag(_ name: String, _ flag: Bool) { - guard mpv != nil else { return } - var data: Int = flag ? 1 : 0 + func setFlag(_ name: String, _ value: Bool) { + guard let mpv = mpv else { return } + + var data: Int = value ? 1 : 0 mpv_set_property(mpv, name, MPV_FORMAT_FLAG, &data) } func command( _ command: String, - args: [String?] = [], - checkForErrors: Bool = true, - returnValueCallback: ((Int32) -> Void)? = nil + args: [String] = [], + checkErrors: Bool = true, + completion: ((Int32) -> Void)? = nil ) { - guard mpv != nil else { + guard let mpv = mpv else { + completion?(-1) return } - var cargs = makeCArgs(command, args).map { $0.flatMap { UnsafePointer(strdup($0)) } } - defer { - for ptr in cargs where ptr != nil { - free(UnsafeMutablePointer(mutating: ptr!)) + + // Create the C-style command array manually with the correct type + let cStrings = [command] + args + + // Create array of C string pointers with the correct type + let count = cStrings.count + let cArray = UnsafeMutablePointer?>.allocate(capacity: count + 1) + + // Fill the array + for i in 0.. [String?] { - if !args.isEmpty, args.last == nil { - fatalError("Command do not need a nil suffix") - } + while self.mpv != nil && !self.isBeingDeallocated + && !self.syncQueue.sync(execute: { self.isShuttingDown }) + { + guard let event = mpv_wait_event(mpv, 0) else { break } + if event.pointee.event_id == MPV_EVENT_NONE { break } - var strArgs = args - strArgs.insert(command, at: 0) - strArgs.append(nil) - - return strArgs - } - - func readEvents() { - queue.async { [weak self] in - guard let self = self else { return } - - while self.mpv != nil { - let event = mpv_wait_event(self.mpv, 0) - if event?.pointee.event_id == MPV_EVENT_NONE { - break - } - - switch event!.pointee.event_id { - case MPV_EVENT_PROPERTY_CHANGE: - let dataOpaquePtr = OpaquePointer(event!.pointee.data) - if let property = UnsafePointer(dataOpaquePtr)?.pointee { - let propertyName = String(cString: property.name) - - switch propertyName { - case MpvProperty.pausedForCache: - let buffering = - UnsafePointer(OpaquePointer(property.data))?.pointee ?? true - DispatchQueue.main.async { - self.playDelegate?.propertyChange( - mpv: self.mpv, propertyName: propertyName, data: buffering) - } - case MpvProperty.timePosition: - if let data = property.data, - let position = UnsafePointer(OpaquePointer(data))?.pointee - { - DispatchQueue.main.async { - self.playDelegate?.propertyChange( - mpv: self.mpv, propertyName: propertyName, data: position) - } - } - case MpvProperty.pause: - if let data = property.data, - let paused = UnsafePointer(OpaquePointer(data))?.pointee - { - DispatchQueue.main.async { - self.playDelegate?.propertyChange( - mpv: self.mpv, propertyName: propertyName, data: paused) - } - } - case MpvProperty.duration: - if let data = property.data, - let duration = UnsafePointer(OpaquePointer(data))?.pointee - { - DispatchQueue.main.async { - self.playDelegate?.propertyChange( - mpv: self.mpv, propertyName: propertyName, data: duration) - } - } - default: - break - } - } - case MPV_EVENT_SHUTDOWN: - print("event: shutdown\n") - mpv_terminate_destroy(mpv) - mpv = nil - break - case MPV_EVENT_LOG_MESSAGE: - let msg = UnsafeMutablePointer( - OpaquePointer(event!.pointee.data)) - print( - "[\(String(cString: (msg!.pointee.prefix)!))] \(String(cString: (msg!.pointee.level)!)): \(String(cString: (msg!.pointee.text)!))", - terminator: "") - default: - let eventName = mpv_event_name(event!.pointee.event_id) - print("event: \(String(cString: (eventName)!))") - } + self.handleEvent(event) } } } - private func checkError(_ status: CInt) { - if status < 0 { - print("MPV API error: \(String(cString: mpv_error_string(status)))\n") + private func handleEvent(_ event: UnsafePointer) { + // Exit early if we're shutting down + if syncQueue.sync(execute: { isShuttingDown }) || isBeingDeallocated { + return } - } - deinit { - if mpv != nil { - mpv_terminate_destroy(mpv) - mpv = nil + guard let mpv = mpv else { return } + + switch event.pointee.event_id { + case MPV_EVENT_PROPERTY_CHANGE: + guard let propertyData = event.pointee.data else { break } + let property = UnsafePointer(OpaquePointer(propertyData)).pointee + let propertyName = String(cString: property.name) + + var value: Any? + + switch propertyName { + case MpvProperty.pausedForCache, MpvProperty.pause: + if let data = property.data, + let boolValue = UnsafePointer(OpaquePointer(data))?.pointee + { + value = boolValue + } + + case MpvProperty.timePosition, MpvProperty.duration: + if let data = property.data, + let doubleValue = UnsafePointer(OpaquePointer(data))?.pointee + { + value = doubleValue + } + + default: + break + } + + // Notify delegate on main thread + if let value = value { + DispatchQueue.main.async { [weak self] in + guard let self = self, !self.isBeingDeallocated else { return } + self.delegate?.propertyChanged( + mpv: mpv, propertyName: propertyName, value: value) + } + } + + case MPV_EVENT_SHUTDOWN: + print("MPV shutdown event received") + mpvQueue.async { [weak self] in + guard let self = self, self.mpv != nil else { return } + mpv_terminate_destroy(self.mpv) + self.mpv = nil + } + + default: + if let eventName = mpv_event_name(event.pointee.event_id) { + print("MPV event: \(String(cString: eventName))") + } } } } + +// MARK: - Improved WeakContainer +class WeakContainer { + private weak var _value: T? + private var _isValid = true + + var value: T? { + guard _isValid else { return nil } + return _value + } + + func invalidate() { + _isValid = false + _value = nil + } + + init(value: T) { + self._value = value + } +}