From cedd448ef267957c50b46e4279dcc3556295c845 Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Fri, 8 May 2026 02:39:42 +0200 Subject: [PATCH] feat(apple): use AVFoundation mpv output --- ios/Runner.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/swiftpm/Package.resolved | 2 +- .../xcshareddata/swiftpm/Package.resolved | 2 +- ios/Runner/MpvPlayer/MpvPipController.swift | 120 +++--------------- ios/Runner/MpvPlayer/MpvPlayerCore.swift | 74 +++-------- ios/Runner/MpvPlayer/MpvPlayerPlugin.swift | 36 +++--- .../settings/keyboard_shortcuts_screen.dart | 5 +- lib/screens/video_player/parts/build.dart | 4 +- .../video_player/parts/pip_shader.dart | 5 +- lib/services/ambient_lighting_service.dart | 2 +- lib/services/shader_service.dart | 6 +- lib/services/video_pip_manager.dart | 6 +- macos/Runner.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/swiftpm/Package.resolved | 2 +- .../xcshareddata/swiftpm/Package.resolved | 2 +- macos/Runner/MpvPlayer/MpvPipController.swift | 27 ++-- macos/Runner/MpvPlayer/MpvPlayerCore.swift | 98 +++++++------- macos/Runner/MpvPlayer/MpvPlayerPlugin.swift | 14 +- pubspec.yaml | 2 +- .../apple/MpvPlayer/MpvPlayerCoreBase.swift | 117 ++++++++--------- tvos/Runner.xcodeproj/project.pbxproj | 4 +- .../xcshareddata/swiftpm/Package.resolved | 3 +- .../xcshareddata/swiftpm/Package.resolved | 3 +- tvos/scripts/wire_mpv.rb | 6 +- 24 files changed, 216 insertions(+), 328 deletions(-) diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 70befcb9..517f6349 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -781,7 +781,7 @@ repositoryURL = "https://github.com/edde746/MPVKit"; requirement = { kind = revision; - revision = 29803ccbfa631997d496f34c2c1ed191cc8fd2c3; + revision = 8aa5e1abb68fffe1d29511da9e02feca575c9311; }; }; /* End XCRemoteSwiftPackageReference section */ diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 5833e1ac..94b854be 100644 --- a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -6,7 +6,7 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/edde746/MPVKit", "state" : { - "revision" : "29803ccbfa631997d496f34c2c1ed191cc8fd2c3" + "revision" : "8aa5e1abb68fffe1d29511da9e02feca575c9311" } } ], diff --git a/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved index 5833e1ac..94b854be 100644 --- a/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -6,7 +6,7 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/edde746/MPVKit", "state" : { - "revision" : "29803ccbfa631997d496f34c2c1ed191cc8fd2c3" + "revision" : "8aa5e1abb68fffe1d29511da9e02feca575c9311" } } ], diff --git a/ios/Runner/MpvPlayer/MpvPipController.swift b/ios/Runner/MpvPlayer/MpvPipController.swift index 377cdcaf..e4655ba2 100644 --- a/ios/Runner/MpvPlayer/MpvPipController.swift +++ b/ios/Runner/MpvPlayer/MpvPipController.swift @@ -1,3 +1,4 @@ +import AVFoundation import AVKit import UIKit @@ -22,11 +23,7 @@ import UIKit weak var delegate: MpvPipDelegate? var isPipActive: Bool { false } var autoStartEnabled: Bool { false } - var layerPointer: UnsafeMutableRawPointer { - // Return a dummy non-null pointer — layerPointer is handed to mpv for - // rendering into PiP, which never activates on tvOS. - UnsafeMutableRawPointer(bitPattern: 0x1)! - } + init(sampleBufferDisplayLayer: AVSampleBufferDisplayLayer) { super.init() } func setup(with layer: CALayer, containerView: UIView) {} func setAutoStart(_ enabled: Bool) {} func warmLayer(currentTime: Double, isPlaying: Bool) {} @@ -67,18 +64,13 @@ import UIKit // MARK: - Properties private var pipController: AVPictureInPictureController? - private let sampleBufferLayer = AVSampleBufferDisplayLayer() - private var containerView: UIView? + private weak var sampleBufferLayer: AVSampleBufferDisplayLayer? weak var delegate: MpvPipDelegate? - /// Pointer to the sample buffer layer for passing to mpv as `wid` - var layerPointer: UnsafeMutableRawPointer { - Unmanaged.passUnretained(sampleBufferLayer).toOpaque() - } - // MARK: - Initialization - override init() { + init(sampleBufferDisplayLayer: AVSampleBufferDisplayLayer) { + self.sampleBufferLayer = sampleBufferDisplayLayer super.init() setup() } @@ -93,18 +85,6 @@ import UIKit print("[MpvPipController] Failed to configure audio session: \(error)") } - // The sample buffer layer must be in a visible view hierarchy for - // isPictureInPicturePossible to become true. - if let window = ExternalDisplayManager.mainApplicationWindow() { - let view = UIView(frame: window.bounds) - view.clipsToBounds = true - view.isUserInteractionEnabled = false - sampleBufferLayer.frame = view.bounds - view.layer.addSublayer(sampleBufferLayer) - window.addSubview(view) - containerView = view - } - createPipController() } @@ -112,7 +92,7 @@ import UIKit private var delegateHelper: AnyObject? private func createPipController() { - guard #available(iOS 15.0, *) else { return } + guard #available(iOS 15.0, *), let sampleBufferLayer else { return } let helper = PipDelegateHelper(controller: self) let contentSource = AVPictureInPictureController.ContentSource( sampleBufferDisplayLayer: sampleBufferLayer, @@ -132,82 +112,23 @@ import UIKit pipController?.canStartPictureInPictureAutomaticallyFromInline = enabled } - /// Push a black frame to the sample buffer layer so PiP has content - /// to display immediately (before vo_pip decodes the first real frame). + /// vo_avfoundation renders into the inline display layer, so PiP reuses + /// the current video frame instead of pushing synthetic buffers here. func pushBlankFrame(width: Int32 = 1920, height: Int32 = 1080) { - var pixelBuffer: CVPixelBuffer? - let attrs: [String: Any] = [ - kCVPixelBufferIOSurfacePropertiesKey as String: [:] - ] - let status = CVPixelBufferCreate( - kCFAllocatorDefault, Int(width), Int(height), - kCVPixelFormatType_32BGRA, attrs as CFDictionary, &pixelBuffer - ) - guard status == kCVReturnSuccess, let pb = pixelBuffer else { return } - - // Fill with black - CVPixelBufferLockBaseAddress(pb, []) - if let base = CVPixelBufferGetBaseAddress(pb) { - memset(base, 0, CVPixelBufferGetDataSize(pb)) - } - CVPixelBufferUnlockBaseAddress(pb, []) - - // Use current timebase time for PTS (if available) so the frame isn't stale - let pts: CMTime - if let tb = sampleBufferLayer.controlTimebase { - pts = CMTimebaseGetTime(tb) - } else { - pts = CMTime(value: 0, timescale: 30) - } - - var timing = CMSampleTimingInfo( - duration: CMTime(value: 1, timescale: 30), - presentationTimeStamp: pts, - decodeTimeStamp: .invalid - ) - - // Create format description and sample buffer - var formatDesc: CMVideoFormatDescription? - CMVideoFormatDescriptionCreateForImageBuffer( - allocator: kCFAllocatorDefault, - imageBuffer: pb, - formatDescriptionOut: &formatDesc - ) - guard let fmt = formatDesc else { return } - - var sampleBuffer: CMSampleBuffer? - CMSampleBufferCreateReadyWithImageBuffer( - allocator: kCFAllocatorDefault, - imageBuffer: pb, - formatDescription: fmt, - sampleTiming: &timing, - sampleBufferOut: &sampleBuffer - ) - guard let sb = sampleBuffer else { return } - - // Set DisplayImmediately so it shows regardless of timebase timing - if let attachments = CMSampleBufferGetSampleAttachmentsArray( - sb, createIfNecessary: true) as? [NSMutableDictionary], - let dict = attachments.first - { - dict[kCMSampleAttachmentKey_DisplayImmediately] = true - } - - sampleBufferLayer.enqueue(sb) } /// Sync the layer's controlTimebase with the actual playback position. /// This makes the PiP progress bar show the correct time. func syncTimebase(currentTime: Double, isPlaying: Bool) { - guard let timebase = sampleBufferLayer.controlTimebase else { return } + guard let timebase = sampleBufferLayer?.controlTimebase else { return } let cmTime = CMTime(seconds: currentTime, preferredTimescale: 1000) CMTimebaseSetTime(timebase, time: cmTime) CMTimebaseSetRate(timebase, rate: isPlaying ? 1.0 : 0.0) } - /// Ensure the layer has a timebase and blank frame so the system considers - /// PiP possible (required for canStartPictureInPictureAutomaticallyFromInline). + /// Ensure the layer has a timebase so the PiP progress UI can follow mpv. func warmLayer(currentTime: Double, isPlaying: Bool) { + guard let sampleBufferLayer else { return } if sampleBufferLayer.controlTimebase == nil { var timebase: CMTimebase? CMTimebaseCreateWithSourceClock( @@ -220,7 +141,6 @@ import UIKit } } syncTimebase(currentTime: currentTime, isPlaying: isPlaying) - pushBlankFrame() } // MARK: - Public API @@ -241,19 +161,19 @@ import UIKit var attempts = 0 func tryStart() { let possible = pipController.isPictureInPicturePossible - let hasTimebase = sampleBufferLayer.controlTimebase != nil + let hasTimebase = self.sampleBufferLayer?.controlTimebase != nil let hasFrame: Bool if !waitForFrame { hasFrame = true // Skip frame check for auto-PiP } else if #available(iOS 17.4, *) { - hasFrame = sampleBufferLayer.isReadyForDisplay + hasFrame = self.sampleBufferLayer?.isReadyForDisplay ?? false } else { hasFrame = true } if possible && hasTimebase && hasFrame { - print("[MpvPipController] vo_pip ready after \(attempts) retries, starting PiP") + print("[MpvPipController] vo_avfoundation ready after \(attempts) retries, starting PiP") pipController.startPictureInPicture() completion(true) } else if attempts < 40 { @@ -278,9 +198,7 @@ import UIKit pipController?.invalidatePlaybackState() } - /// Fully tear down PiP — removes the container view from the window and - /// destroys the AVPictureInPictureController so the system can no longer - /// trigger auto-PiP after the player is disposed. + /// Fully tear down PiP without touching the shared inline display layer. func teardown() { pipController?.stopPictureInPicture() if #available(iOS 14.2, *) { @@ -288,16 +206,10 @@ import UIKit } pipController = nil delegateHelper = nil - sampleBufferLayer.flushAndRemoveImage() - sampleBufferLayer.controlTimebase = nil - sampleBufferLayer.removeFromSuperlayer() - containerView?.removeFromSuperview() - containerView = nil } - /// Flush enqueued sample buffers from the layer to free video frame memory + /// PiP shares the inline display layer, so cleanup must not flush it. func flushLayer() { - sampleBufferLayer.flushAndRemoveImage() } } diff --git a/ios/Runner/MpvPlayer/MpvPlayerCore.swift b/ios/Runner/MpvPlayer/MpvPlayerCore.swift index a99655ec..177472c6 100644 --- a/ios/Runner/MpvPlayer/MpvPlayerCore.swift +++ b/ios/Runner/MpvPlayer/MpvPlayerCore.swift @@ -1,7 +1,7 @@ -import Libmpv +import AVFoundation import UIKit -/// Core MPV player using Metal rendering for iOS. +/// Core MPV player using AVFoundation sample-buffer rendering for iOS/tvOS. class MpvPlayerCore: MpvPlayerCoreBase { private var containerView: UIView? @@ -23,15 +23,15 @@ class MpvPlayerCore: MpvPlayerCoreBase { container.backgroundColor = .clear container.isUserInteractionEnabled = false - let layer = MpvMetalLayer() + let layer = MpvVideoLayer() layer.frame = container.bounds layer.contentsScale = window.screen.nativeScale - layer.framebufferOnly = true layer.backgroundColor = UIColor.black.cgColor + layer.videoGravity = .resizeAspect container.layer.addSublayer(layer) containerView = container - metalLayer = layer + videoLayer = layer window.insertSubview(container, at: 0) @@ -39,7 +39,7 @@ class MpvPlayerCore: MpvPlayerCoreBase { print("[MpvPlayerCore] Failed to setup MPV") layer.removeFromSuperlayer() container.removeFromSuperview() - metalLayer = nil + videoLayer = nil containerView = nil return false } @@ -54,40 +54,7 @@ class MpvPlayerCore: MpvPlayerCoreBase { return true } - func switchToPipVO(layerPtr: UnsafeMutableRawPointer) -> Bool { - guard mpv != nil else { return false } - - print("[MpvPlayerCore] Switching to pip VO for PiP") - - metalLayer?.removeFromSuperlayer() - - setProperty("vid", value: "no") - setInt64PropertyAsync("wid", value: Int64(Int(bitPattern: layerPtr))) { _ in } - setProperty("vo", value: "pip") - setProperty("vid", value: "auto") - - print("[MpvPlayerCore] Switched to pip VO successfully") - return true - } - - func switchToGpuNextVO() -> Bool { - guard mpv != nil, let metalLayer else { return false } - - print("[MpvPlayerCore] Switching back to gpu-next VO") - - setProperty("vid", value: "no") - setInt64PropertyAsync("wid", value: Int64(Int(bitPattern: Unmanaged.passUnretained(metalLayer).toOpaque()))) { _ in - } - applyGpuNextOptions() - setProperty("vid", value: "auto") - - if metalLayer.superlayer == nil, let containerView { - containerView.layer.addSublayer(metalLayer) - } - - print("[MpvPlayerCore] Switched back to gpu-next VO successfully") - return true - } + var sampleBufferDisplayLayer: MpvVideoLayer? { videoLayer } func setVisible(_ visible: Bool) { guard let containerView else { return } @@ -99,28 +66,24 @@ class MpvPlayerCore: MpvPlayerCoreBase { } func updateFrame(_ frame: CGRect? = nil) { - guard let metalLayer, let containerView else { return } + guard let videoLayer, let containerView else { return } if let frame { containerView.frame = frame - metalLayer.frame = containerView.bounds + videoLayer.frame = containerView.bounds } else if let superview = containerView.superview { containerView.frame = superview.bounds - metalLayer.frame = containerView.bounds + videoLayer.frame = containerView.bounds } else if let window { containerView.frame = window.bounds - metalLayer.frame = containerView.bounds + videoLayer.frame = containerView.bounds } mainBlankView?.frame = window?.bounds ?? .zero let screen = containerView.window?.screen ?? window?.screen ?? UIScreen.main let scale = screen.nativeScale > 0 ? screen.nativeScale : screen.scale - metalLayer.contentsScale = scale - metalLayer.drawableSize = CGSize( - width: metalLayer.frame.width * scale, - height: metalLayer.frame.height * scale - ) + videoLayer.contentsScale = scale } func externalDisplayDidChange() { @@ -191,20 +154,19 @@ class MpvPlayerCore: MpvPlayerCoreBase { mainBlankView = blankView } - /// Nudge mpv to present the current paused frame after switching back from PiP. + /// Nudge mpv to present the current paused frame after leaving PiP. func forceDraw() { command(["seek", "0", "relative+exact"]) } override func updateEDRMode(sigPeak: Double) { - guard let metalLayer else { return } + guard let videoLayer else { return } var edrHeadroom: CGFloat = 1.0 #if os(iOS) - if #available(iOS 16.0, *) { + if #available(iOS 17.0, *) { edrHeadroom = containerView?.window?.screen.potentialEDRHeadroom ?? 1.0 - metalLayer.wantsExtendedDynamicRangeContent = - hdrEnabled && sigPeak > 1.0 && edrHeadroom > 1.0 + videoLayer.wantsExtendedDynamicRangeContent = hdrEnabled && sigPeak > 1.0 && edrHeadroom > 1.0 } #endif @@ -221,8 +183,8 @@ class MpvPlayerCore: MpvPlayerCoreBase { #endif disposeSharedState(destroySynchronously: false) - metalLayer?.removeFromSuperlayer() - metalLayer = nil + videoLayer?.removeFromSuperlayer() + videoLayer = nil containerView?.removeFromSuperview() containerView = nil mainBlankView?.removeFromSuperview() diff --git a/ios/Runner/MpvPlayer/MpvPlayerPlugin.swift b/ios/Runner/MpvPlayer/MpvPlayerPlugin.swift index 128c1712..1019667b 100644 --- a/ios/Runner/MpvPlayer/MpvPlayerPlugin.swift +++ b/ios/Runner/MpvPlayer/MpvPlayerPlugin.swift @@ -96,9 +96,10 @@ class MpvPlayerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, MpvPluginS // MARK: - PiP - private func ensurePipController() -> MpvPipController { + private func ensurePipController() -> MpvPipController? { if let existing = pipController { return existing } - let controller = MpvPipController() + guard let layer = playerCore?.sampleBufferDisplayLayer else { return nil } + let controller = MpvPipController(sampleBufferDisplayLayer: layer) controller.delegate = self pipController = controller return controller @@ -158,7 +159,10 @@ class MpvPlayerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, MpvPluginS if let args = call.arguments as? [String: Any], let ready = args["ready"] as? Bool { self.autoPipEnabled = ready if ready { - let pip = self.ensurePipController() + guard let pip = self.ensurePipController() else { + result(nil) + return + } pip.setAutoStart(true) // Warm the layer so the system considers PiP possible if let pc = self.playerCore { @@ -175,16 +179,14 @@ class MpvPlayerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, MpvPluginS } } - /// Switch to PiP VO and prepare the sample buffer layer for PiP display. + /// Prepare the shared AVFoundation display layer for PiP display. /// Returns the MpvPipController on success, nil on failure. @discardableResult - private func switchToPipAndPrepare() -> MpvPipController? { + private func preparePip() -> MpvPipController? { guard let playerCore = playerCore else { return nil } - let pip = ensurePipController() - guard playerCore.switchToPipVO(layerPtr: pip.layerPointer) else { return nil } + guard let pip = ensurePipController() else { return nil } pendingInlineRestoreAfterPip = false playerCore.isPipStarting = true - pip.pushBlankFrame() pip.syncTimebase(currentTime: playerCore.timePos, isPlaying: !playerCore.isPaused) pip.invalidatePlaybackState() return pip @@ -205,10 +207,10 @@ class MpvPlayerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, MpvPluginS ]) return } - guard let pip = switchToPipAndPrepare() else { + guard let pip = preparePip() else { result?([ - "success": false, "errorCode": "vo_switch_failed", - "errorMessage": "Failed to switch VO", + "success": false, "errorCode": "pip_prepare_failed", + "errorMessage": "Failed to prepare PiP", ]) return } @@ -232,15 +234,13 @@ class MpvPlayerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, MpvPluginS playerCore?.isPipActive = false isManualPipRequest = false stopPipTimebaseSync() - pipController?.flushLayer() - let restoredInlineVO = playerCore?.switchToGpuNextVO() ?? false if pause { playerCore?.setPropertyAsync("pause", value: "yes") { [weak self] _ in self?.pipController?.invalidatePlaybackState() self?.syncPipTimebase() } } - pendingInlineRestoreAfterPip = restoredInlineVO + pendingInlineRestoreAfterPip = true if pendingInlineRestoreAfterPip { if isSceneActive { restoreInlinePlayerAfterPip() @@ -393,11 +393,11 @@ class MpvPlayerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, MpvPluginS extension MpvPlayerPlugin: MpvPipDelegate { func pipWillStart() { - // If PiP was system-initiated (not via our enterPip), switch VO now + // If PiP was system-initiated (not via our enterPip), prepare the shared layer now. guard let playerCore = playerCore, !playerCore.isPipStarting else { return } - print("[MpvPlayerPlugin] System-initiated PiP detected, switching VO") - if switchToPipAndPrepare() == nil { - print("[MpvPlayerPlugin] VO switch failed for system-initiated PiP") + print("[MpvPlayerPlugin] System-initiated PiP detected, preparing shared layer") + if preparePip() == nil { + print("[MpvPlayerPlugin] PiP preparation failed for system-initiated PiP") pipController?.stopPip() } } diff --git a/lib/screens/settings/keyboard_shortcuts_screen.dart b/lib/screens/settings/keyboard_shortcuts_screen.dart index 66605241..1026a6d4 100644 --- a/lib/screens/settings/keyboard_shortcuts_screen.dart +++ b/lib/screens/settings/keyboard_shortcuts_screen.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import '../../i18n/strings.g.dart'; import '../../models/hotkey_model.dart'; import '../../services/keyboard_shortcuts_service.dart'; +import '../../services/shader_service.dart'; import '../../utils/snackbar_helper.dart'; import '../../focus/focusable_button.dart'; import '../../widgets/focused_scroll_scaffold.dart'; @@ -19,7 +20,9 @@ class KeyboardShortcutsScreen extends StatelessWidget { listenable: keyboardService, builder: (context, _) { final hotkeys = keyboardService.hotkeys; - final actions = hotkeys.keys.toList(); + final actions = hotkeys.keys + .where((action) => action != 'shader_toggle' || ShaderService.isPlatformSupported) + .toList(); return FocusedScrollScaffold( title: Text(t.settings.keyboardShortcuts), slivers: [ diff --git a/lib/screens/video_player/parts/build.dart b/lib/screens/video_player/parts/build.dart index 8b14a222..bbe73104 100644 --- a/lib/screens/video_player/parts/build.dart +++ b/lib/screens/video_player/parts/build.dart @@ -212,7 +212,9 @@ extension _VideoPlayerBuildMethods on VideoPlayerScreenState { onLiveSeek: _captureBuffer != null ? _seekLivePosition : null, onJumpToLive: _captureBuffer != null && !_isAtLiveEdge ? _jumpToLiveEdge : null, isAmbientLightingEnabled: _ambientLightingService?.isEnabled ?? false, - onToggleAmbientLighting: _toggleAmbientLighting, + onToggleAmbientLighting: _ambientLightingService?.isSupported == true + ? _toggleAmbientLighting + : null, toastController: _toastController, ), ); diff --git a/lib/screens/video_player/parts/pip_shader.dart b/lib/screens/video_player/parts/pip_shader.dart index 7e1be461..228eb471 100644 --- a/lib/screens/video_player/parts/pip_shader.dart +++ b/lib/screens/video_player/parts/pip_shader.dart @@ -37,8 +37,9 @@ extension _VideoPlayerPipShaderMethods on VideoPlayerScreenState { if (_videoPIPManager == null || _videoFilterManager == null) return; - // Only handle exit - entry is handled by onBeforeEnterPip callback - if (!isInPip) { + if (isInPip) { + _videoFilterManager!.enterPipMode(); + } else { final restoreAmbient = _videoFilterManager!.hadAmbientLightingBeforePip; _videoFilterManager!.exitPipMode(); // Restore ambient lighting if it was active before PiP diff --git a/lib/services/ambient_lighting_service.dart b/lib/services/ambient_lighting_service.dart index c63f09d1..827b9efc 100644 --- a/lib/services/ambient_lighting_service.dart +++ b/lib/services/ambient_lighting_service.dart @@ -27,7 +27,7 @@ class AmbientLightingService { AmbientLightingService(this._player); bool get isEnabled => _enabled; - bool get isSupported => _player.playerType == 'mpv'; + bool get isSupported => _player.playerType == 'mpv' && !Platform.isIOS && !Platform.isMacOS; /// Enable ambient lighting effect. /// diff --git a/lib/services/shader_service.dart b/lib/services/shader_service.dart index 0f2b3736..5d49a59f 100644 --- a/lib/services/shader_service.dart +++ b/lib/services/shader_service.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import '../models/shader_preset.dart'; import '../mpv/player/player.dart'; import '../utils/app_logger.dart'; @@ -20,8 +22,10 @@ class ShaderService { ShaderPreset get currentPreset => _currentPreset; + static bool get isPlatformSupported => !Platform.isIOS && !Platform.isMacOS; + /// Check if the player is MPV (shaders are MPV-only) - bool get isSupported => _player.playerType == 'mpv'; + bool get isSupported => _player.playerType == 'mpv' && isPlatformSupported; /// Apply a shader preset to the video player. /// diff --git a/lib/services/video_pip_manager.dart b/lib/services/video_pip_manager.dart index 538a289c..c15e0bf0 100644 --- a/lib/services/video_pip_manager.dart +++ b/lib/services/video_pip_manager.dart @@ -68,9 +68,9 @@ class VideoPIPManager { return (true, null); } - // Reset video filter to contain mode BEFORE entering PiP (Android only — - // iOS switches VO entirely so the filter is irrelevant) - if (Platform.isAndroid) { + // Reset video filter to contain mode before entering PiP. Android, iOS, + // and macOS all reuse the inline video surface/layer for PiP. + if (Platform.isAndroid || Platform.isIOS || Platform.isMacOS) { onBeforeEnterPip?.call(); // Wait a frame for the filter change to take effect await Future.delayed(const Duration(milliseconds: 50)); diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj index cef3da2b..9513b74c 100644 --- a/macos/Runner.xcodeproj/project.pbxproj +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -875,7 +875,7 @@ repositoryURL = "https://github.com/edde746/MPVKit"; requirement = { kind = revision; - revision = 29803ccbfa631997d496f34c2c1ed191cc8fd2c3; + revision = 8aa5e1abb68fffe1d29511da9e02feca575c9311; }; }; /* End XCRemoteSwiftPackageReference section */ diff --git a/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 5833e1ac..94b854be 100644 --- a/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -6,7 +6,7 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/edde746/MPVKit", "state" : { - "revision" : "29803ccbfa631997d496f34c2c1ed191cc8fd2c3" + "revision" : "8aa5e1abb68fffe1d29511da9e02feca575c9311" } } ], diff --git a/macos/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved b/macos/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved index 5833e1ac..94b854be 100644 --- a/macos/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/macos/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -6,7 +6,7 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/edde746/MPVKit", "state" : { - "revision" : "29803ccbfa631997d496f34c2c1ed191cc8fd2c3" + "revision" : "8aa5e1abb68fffe1d29511da9e02feca575c9311" } } ], diff --git a/macos/Runner/MpvPlayer/MpvPipController.swift b/macos/Runner/MpvPlayer/MpvPipController.swift index d6be9386..9b0934d3 100644 --- a/macos/Runner/MpvPlayer/MpvPipController.swift +++ b/macos/Runner/MpvPlayer/MpvPipController.swift @@ -13,8 +13,8 @@ protocol MpvPipDelegate: AnyObject { } /// Encapsulates macOS Picture-in-Picture using the private PIP.framework (PIPViewController). -/// This approach wraps the existing Metal rendering view in PiP — no VO switching needed. -/// mpv continues rendering to its CAMetalLayer throughout PiP. +/// This approach wraps the existing video layer in PiP — no VO switching needed. +/// mpv continues rendering to its AVFoundation display layer throughout PiP. class MpvPipController: NSObject, PIPViewControllerDelegate { // MARK: - Properties @@ -39,23 +39,18 @@ class MpvPipController: NSObject, PIPViewControllerDelegate { static var isSupported: Bool { true } - /// Enter PiP by wrapping the given Metal layer in a view and presenting it. + /// Enter PiP by wrapping the given video layer in a view and presenting it. /// The layer continues receiving mpv frames — no VO switch needed. - func startPip(metalLayer: CAMetalLayer, window: NSWindow, aspectRatio: NSSize) { + func startPip(videoLayer: CALayer, window: NSWindow, aspectRatio: NSSize) { guard !isActive else { return } sourceWindow = window - // Create a layer-hosting wrapper view for the Metal layer. + // Create a layer-hosting wrapper view for the video layer. // PIPViewController resizes the view (and its root layer) as the PiP window resizes. let videoView = NSView(frame: NSRect(origin: .zero, size: aspectRatio)) videoView.wantsLayer = true - videoView.layer = metalLayer - - // Reset drawableSize to zero so it auto-derives from the layer's bounds. - // Without this, the explicit drawableSize set by updateFrame() (main window size) - // persists and causes mpv/MoltenVK to render at the wrong resolution in PiP. - metalLayer.drawableSize = .zero + videoView.layer = videoLayer // Create a view controller for PIPViewController let vc = NSViewController() @@ -97,16 +92,16 @@ class MpvPipController: NSObject, PIPViewControllerDelegate { autoPipEnabled = enabled } - /// Clean up after PiP closes — detaches the Metal layer from the wrapper view + /// Clean up after PiP closes — detaches the video layer from the wrapper view /// so MpvPlayerCore can re-add it to the main window. - /// Returns the Metal layer that was hosted in PiP. + /// Returns the layer that was hosted in PiP. @discardableResult - func detachLayer() -> CAMetalLayer? { - let metalLayer = pipVideoView?.layer as? CAMetalLayer + func detachLayer() -> CALayer? { + let videoLayer = pipVideoView?.layer pipVideoView?.layer = CALayer() // detach before removing pipVideoView = nil pipVideoVC = nil - return metalLayer + return videoLayer } // MARK: - PIPViewControllerDelegate diff --git a/macos/Runner/MpvPlayer/MpvPlayerCore.swift b/macos/Runner/MpvPlayer/MpvPlayerCore.swift index d9faa47b..49bb70bd 100644 --- a/macos/Runner/MpvPlayer/MpvPlayerCore.swift +++ b/macos/Runner/MpvPlayer/MpvPlayerCore.swift @@ -1,7 +1,8 @@ +import AVFoundation import Cocoa import Libmpv -/// Core MPV player using Metal rendering. +/// Core MPV player using AVFoundation sample-buffer rendering. class MpvPlayerCore: MpvPlayerCoreBase { private weak var window: NSWindow? @@ -22,27 +23,28 @@ class MpvPlayerCore: MpvPlayerCoreBase { self.window = window - let layer = MpvMetalLayer() + let layer = MpvVideoLayer() layer.frame = contentView.bounds if let screen = window.screen ?? NSScreen.main { layer.contentsScale = screen.backingScaleFactor } - layer.framebufferOnly = true layer.isOpaque = true layer.backgroundColor = NSColor.black.cgColor layer.autoresizingMask = [.layerWidthSizable, .layerHeightSizable] + layer.videoGravity = .resizeAspect - metalLayer = layer + videoLayer = layer + updateEDRMode(sigPeak: lastSigPeak) contentView.wantsLayer = true contentView.layer?.addSublayer(layer) - print("[MpvPlayerCore] Metal layer added, frame: \(layer.frame)") + print("[MpvPlayerCore] Video layer added, frame: \(layer.frame)") guard setupMpv() else { print("[MpvPlayerCore] Failed to setup MPV") layer.removeFromSuperlayer() - metalLayer = nil + videoLayer = nil return false } @@ -73,29 +75,23 @@ class MpvPlayerCore: MpvPlayerCoreBase { override func configurePlatformMpvOptions() { guard let mpv else { return } + checkError(mpv_set_option_string(mpv, "avfoundation-composite-osd", "no")) checkError(mpv_set_option_string(mpv, "ao", "avfoundation,coreaudio")) - // Default fifo (vsync) mode — mailbox was causing continuous GPU rendering even when paused } - var videoLayer: CAMetalLayer? { metalLayer } + func reattachVideoLayer() { + guard let videoLayer, let contentView = window?.contentView else { return } - func reattachMetalLayer() { - guard let metalLayer, let contentView = window?.contentView else { return } - - if metalLayer.superlayer == nil { + if videoLayer.superlayer == nil { contentView.wantsLayer = true - contentView.layer?.insertSublayer(metalLayer, at: 0) - metalLayer.frame = contentView.bounds + contentView.layer?.insertSublayer(videoLayer, at: 0) + videoLayer.frame = contentView.bounds if let screen = window?.screen ?? NSScreen.main { - metalLayer.contentsScale = screen.backingScaleFactor - metalLayer.drawableSize = CGSize( - width: contentView.bounds.width * screen.backingScaleFactor, - height: contentView.bounds.height * screen.backingScaleFactor - ) + videoLayer.contentsScale = screen.backingScaleFactor } } - print("[MpvPlayerCore] Metal layer reattached to window") + print("[MpvPlayerCore] Video layer reattached to window") } func forceDraw() { @@ -106,22 +102,22 @@ class MpvPlayerCore: MpvPlayerCoreBase { private var pausedState = true func setVisible(_ visible: Bool) { - guard let metalLayer, !isPipActive else { return } + guard let videoLayer, !isPipActive else { return } isVisible = visible isBackgrounded = !visible if visible { - metalLayer.removeFromSuperlayer() + videoLayer.removeFromSuperlayer() if let superlayer = window?.contentView?.layer { - superlayer.insertSublayer(metalLayer, at: 0) + superlayer.insertSublayer(videoLayer, at: 0) } beginPlaybackActivity() } else { endPlaybackActivity() } - metalLayer.isHidden = !visible + videoLayer.isHidden = !visible print("[MpvPlayerCore] setVisible(\(visible))") } @@ -135,38 +131,48 @@ class MpvPlayerCore: MpvPlayerCoreBase { } func updateFrame(_ frame: CGRect? = nil) { - guard let metalLayer, !isPipActive else { return } + guard let videoLayer, !isPipActive else { return } if let frame { - metalLayer.frame = frame + videoLayer.frame = frame } else if let contentView = window?.contentView { - metalLayer.frame = contentView.bounds + videoLayer.frame = contentView.bounds } if let screen = window?.screen ?? NSScreen.main { - let scale = screen.backingScaleFactor - metalLayer.drawableSize = CGSize( - width: metalLayer.frame.width * scale, - height: metalLayer.frame.height * scale - ) + videoLayer.contentsScale = screen.backingScaleFactor } + updateEDRMode(sigPeak: lastSigPeak) - print("[MpvPlayerCore] updateFrame: \(metalLayer.frame)") + print("[MpvPlayerCore] updateFrame: \(videoLayer.frame)") } override func updateEDRMode(sigPeak: Double) { - guard let metalLayer else { return } + guard let videoLayer else { return } - var edrHeadroom: CGFloat = 1.0 + var currentHeadroom: CGFloat = 1.0 + var potentialHeadroom: CGFloat = 1.0 if let screen = window?.screen ?? NSScreen.main { - edrHeadroom = screen.maximumExtendedDynamicRangeColorComponentValue + currentHeadroom = screen.maximumExtendedDynamicRangeColorComponentValue + potentialHeadroom = screen.maximumPotentialExtendedDynamicRangeColorComponentValue } - let shouldEnableEDR = hdrEnabled && sigPeak > 1.0 && edrHeadroom > 1.0 - metalLayer.wantsExtendedDynamicRangeContent = shouldEnableEDR + let signalHeadroom = CGFloat(max(sigPeak, 1.0)) + let contentHeadroom = min(signalHeadroom, potentialHeadroom) + let shouldEnableEDR = hdrEnabled && sigPeak > 1.0 && potentialHeadroom > 1.0 + if #available(macOS 26.0, *) { + videoLayer.preferredDynamicRange = shouldEnableEDR ? .high : .standard + videoLayer.contentsHeadroom = shouldEnableEDR ? contentHeadroom : 0 + } + if #available(macOS 15.0, *) { + videoLayer.toneMapMode = shouldEnableEDR ? .ifSupported : .automatic + } + if #available(macOS 14.0, *) { + videoLayer.wantsExtendedDynamicRangeContent = shouldEnableEDR + } print( - "[MpvPlayerCore] EDR mode: \(shouldEnableEDR) (hdrEnabled: \(hdrEnabled), sigPeak: \(sigPeak), headroom: \(edrHeadroom))" + "[MpvPlayerCore] EDR mode: \(shouldEnableEDR) (hdrEnabled: \(hdrEnabled), sigPeak: \(sigPeak), currentHeadroom: \(currentHeadroom), potentialHeadroom: \(potentialHeadroom), contentHeadroom: \(shouldEnableEDR ? contentHeadroom : 0))" ) } @@ -178,8 +184,8 @@ class MpvPlayerCore: MpvPlayerCoreBase { NotificationCenter.default.removeObserver(self) disposeSharedState(destroySynchronously: false) - metalLayer?.removeFromSuperlayer() - metalLayer = nil + videoLayer?.removeFromSuperlayer() + videoLayer = nil isInitialized = false print("[MpvPlayerCore] Disposed") } @@ -199,19 +205,19 @@ class MpvPlayerCore: MpvPlayerCoreBase { } @objc private func windowOcclusionDidChange(_ notification: Notification) { - guard let metalLayer, mpv != nil, !isPipActive else { return } + guard let videoLayer, mpv != nil, !isPipActive else { return } let windowVisible = window?.occlusionState.contains(.visible) ?? true if !windowVisible && !layerHiddenForOcclusion { - print("[MpvPlayerCore] Window occluded - hiding Metal layer") - metalLayer.isHidden = true + print("[MpvPlayerCore] Window occluded - hiding video layer") + videoLayer.isHidden = true layerHiddenForOcclusion = true isBackgrounded = true endPlaybackActivity() } else if windowVisible && layerHiddenForOcclusion { - print("[MpvPlayerCore] Window visible - showing Metal layer") + print("[MpvPlayerCore] Window visible - showing video layer") layerHiddenForOcclusion = false - metalLayer.isHidden = false + videoLayer.isHidden = false isBackgrounded = false if !pausedState { beginPlaybackActivity() diff --git a/macos/Runner/MpvPlayer/MpvPlayerPlugin.swift b/macos/Runner/MpvPlayer/MpvPlayerPlugin.swift index 76668435..189d1c1e 100644 --- a/macos/Runner/MpvPlayer/MpvPlayerPlugin.swift +++ b/macos/Runner/MpvPlayer/MpvPlayerPlugin.swift @@ -158,8 +158,8 @@ class MpvPlayerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, MpvPluginS } } - /// Enter PiP by moving the Metal rendering layer to a PiP window. - /// No VO switching — mpv keeps rendering to the same Metal layer. + /// Enter PiP by moving the AVFoundation video layer to a PiP window. + /// No VO switching — mpv keeps rendering to the same layer. private func enterPip(manual: Bool, result: FlutterResult? = nil) { guard let playerCore = playerCore else { result?([ @@ -167,7 +167,7 @@ class MpvPlayerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, MpvPluginS ]) return } - guard let metalLayer = playerCore.videoLayer else { + guard let videoLayer = playerCore.videoLayer else { result?(["success": false, "errorCode": "failed", "errorMessage": "No video layer"]) return } @@ -191,7 +191,7 @@ class MpvPlayerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, MpvPluginS enteredPipViaAuto = !manual playerCore.isPipActive = true - pip.startPip(metalLayer: metalLayer, window: window, aspectRatio: aspectRatio) + pip.startPip(videoLayer: videoLayer, window: window, aspectRatio: aspectRatio) pipChannel?.invokeMethod("onPipChanged", arguments: true) result?(["success": true]) } @@ -353,11 +353,11 @@ extension MpvPlayerPlugin: MpvPipDelegate { playerCore?.isPipActive = false enteredPipViaAuto = false - // Detach the Metal layer from the PiP wrapper view + // Detach the video layer from the PiP wrapper view pipController?.detachLayer() - // Re-attach the Metal layer to the main window - playerCore?.reattachMetalLayer() + // Re-attach the video layer to the main window + playerCore?.reattachVideoLayer() // Force a redraw if paused (prevents black frame after PiP exit) if playerCore?.isPaused == true { diff --git a/pubspec.yaml b/pubspec.yaml index 215d2870..753f177b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: plezy description: "A beautiful Plex client for Flutter" publish_to: "none" -version: 2.0.0+82 +version: 2.0.0+84 environment: sdk: ">=3.10.7 <4.0.0" diff --git a/shared/apple/MpvPlayer/MpvPlayerCoreBase.swift b/shared/apple/MpvPlayer/MpvPlayerCoreBase.swift index 12fb2d13..cd0ae924 100644 --- a/shared/apple/MpvPlayer/MpvPlayerCoreBase.swift +++ b/shared/apple/MpvPlayer/MpvPlayerCoreBase.swift @@ -1,3 +1,4 @@ +import AVFoundation import Foundation import Libmpv import QuartzCore @@ -13,49 +14,7 @@ protocol MpvPlayerDelegate: AnyObject { func onEvent(name: String, data: [String: Any]?) } -// Workaround for MoltenVK problems that cause flicker. -// https://github.com/mpv-player/mpv/pull/13651 -class MpvMetalLayer: CAMetalLayer { - override var drawableSize: CGSize { - get { super.drawableSize } - set { - if newValue == .zero || (Int(newValue.width) > 1 && Int(newValue.height) > 1) { - super.drawableSize = newValue - } - } - } - - #if os(iOS) - // wantsExtendedDynamicRangeContent is unavailable on tvOS as of SDK 26.4, - // so this override only applies to iOS / macOS. - @available(iOS 16.0, *) - override var wantsExtendedDynamicRangeContent: Bool { - get { super.wantsExtendedDynamicRangeContent } - set { - if Thread.isMainThread { - super.wantsExtendedDynamicRangeContent = newValue - } else { - DispatchQueue.main.sync { - super.wantsExtendedDynamicRangeContent = newValue - } - } - } - } - #elseif os(macOS) - override var wantsExtendedDynamicRangeContent: Bool { - get { super.wantsExtendedDynamicRangeContent } - set { - if Thread.isMainThread { - super.wantsExtendedDynamicRangeContent = newValue - } else { - DispatchQueue.main.async { - super.wantsExtendedDynamicRangeContent = newValue - } - } - } - } - #endif -} +class MpvVideoLayer: AVSampleBufferDisplayLayer {} /// Safely convert a C string to Swift String with UTF-8 validation. /// Falls back to Latin-1 decoding if the bytes are not valid UTF-8. @@ -77,7 +36,7 @@ func safeString(_ cstr: UnsafePointer) -> String { class MpvPlayerCoreBase: NSObject { weak var delegate: MpvPlayerDelegate? - var metalLayer: MpvMetalLayer? + var videoLayer: MpvVideoLayer? var mpv: OpaquePointer? var isInitialized = false var isDisposing = false @@ -118,6 +77,8 @@ class MpvPlayerCoreBase: NSObject { private var cachedTimePos = 0.0 private var cachedWidth = 0.0 private var cachedHeight = 0.0 + private var currentPanscan = 0.0 + private var aspectOverrideActive = false override init() { super.init() @@ -129,7 +90,7 @@ class MpvPlayerCoreBase: NSObject { func updateEDRMode(sigPeak: Double) {} func setupMpv() -> Bool { - guard let metalLayer else { return false } + guard let videoLayer else { return false } mpv = mpv_create() guard let mpv else { @@ -143,7 +104,7 @@ class MpvPlayerCoreBase: NSObject { checkError(mpv_request_log_messages(mpv, "warn")) #endif - var layer = Int64(Int(bitPattern: Unmanaged.passUnretained(metalLayer).toOpaque())) + var layer = Int64(Int(bitPattern: Unmanaged.passUnretained(videoLayer).toOpaque())) checkError(mpv_set_option(mpv, "wid", MPV_FORMAT_INT64, &layer)) applySharedMpvOptions() configurePlatformMpvOptions() @@ -186,6 +147,21 @@ class MpvPlayerCoreBase: NSObject { value: String, completion: @escaping (Result) -> Void ) { + #if targetEnvironment(simulator) + if name == "hwdec" { + completion(.success(())) + return + } + #endif + + if isAvFoundationManagedProperty(name) { + print("[MpvPlayerCore] Ignoring managed AVFoundation property: \(name)=\(value)") + completion(.success(())) + return + } + + updateVideoGravityIfNeeded(name: name, value: value) + if name == "pause" { setCachedPaused(value == "yes" || value == "true" || value == "1") } @@ -365,22 +341,49 @@ class MpvPlayerCoreBase: NSObject { } } - func applyGpuNextOptions() { - guard mpv != nil else { return } - setProperty("gpu-api", value: "vulkan") - setProperty("gpu-context", value: "moltenvk") - setProperty("vo", value: "gpu-next") - } - private func applySharedMpvOptions() { guard let mpv else { return } - 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")) + checkError(mpv_set_option_string(mpv, "vo", "avfoundation")) + #if targetEnvironment(simulator) + checkError(mpv_set_option_string(mpv, "avfoundation-composite-osd", "no")) + checkError(mpv_set_option_string(mpv, "hwdec", "no")) + #else + checkError(mpv_set_option_string(mpv, "avfoundation-composite-osd", "yes")) + checkError(mpv_set_option_string(mpv, "hwdec", "videotoolbox")) + #endif + checkError(mpv_set_option_string(mpv, "hwdec-codecs", "all")) + checkError(mpv_set_option_string(mpv, "hwdec-software-fallback", "yes")) checkError(mpv_set_option_string(mpv, "target-colorspace-hint", "yes")) } + private func isAvFoundationManagedProperty(_ name: String) -> Bool { + name == "vo" || name == "wid" || name == "gpu-api" || name == "gpu-context" + } + + private func updateVideoGravityIfNeeded(name: String, value: String) { + switch name { + case "panscan": + currentPanscan = Double(value) ?? 0 + case "video-aspect-override": + aspectOverrideActive = value != "no" && value != "-1" && value != "0" + default: + return + } + + let gravity: AVLayerVideoGravity + if aspectOverrideActive { + gravity = .resize + } else if currentPanscan > 0 { + gravity = .resizeAspectFill + } else { + gravity = .resizeAspect + } + + DispatchQueue.main.async { [weak self] in + self?.videoLayer?.videoGravity = gravity + } + } + private func cancelPendingRequests() { pendingRequestsLock.lock() let pending = pendingRequests diff --git a/tvos/Runner.xcodeproj/project.pbxproj b/tvos/Runner.xcodeproj/project.pbxproj index 91a6d44f..78e8f12a 100644 --- a/tvos/Runner.xcodeproj/project.pbxproj +++ b/tvos/Runner.xcodeproj/project.pbxproj @@ -711,8 +711,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/edde746/MPVKit"; requirement = { - branch = main; - kind = branch; + kind = revision; + revision = 8aa5e1abb68fffe1d29511da9e02feca575c9311; }; }; /* End XCRemoteSwiftPackageReference section */ diff --git a/tvos/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/tvos/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 8597c517..94b854be 100644 --- a/tvos/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/tvos/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -6,8 +6,7 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/edde746/MPVKit", "state" : { - "branch" : "main", - "revision" : "29803ccbfa631997d496f34c2c1ed191cc8fd2c3" + "revision" : "8aa5e1abb68fffe1d29511da9e02feca575c9311" } } ], diff --git a/tvos/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved b/tvos/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved index 8597c517..94b854be 100644 --- a/tvos/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/tvos/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -6,8 +6,7 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/edde746/MPVKit", "state" : { - "branch" : "main", - "revision" : "29803ccbfa631997d496f34c2c1ed191cc8fd2c3" + "revision" : "8aa5e1abb68fffe1d29511da9e02feca575c9311" } } ], diff --git a/tvos/scripts/wire_mpv.rb b/tvos/scripts/wire_mpv.rb index d5f1eba3..a7a85974 100755 --- a/tvos/scripts/wire_mpv.rb +++ b/tvos/scripts/wire_mpv.rb @@ -42,16 +42,18 @@ end # Swift Package: MPVKit. pkg_url = 'https://github.com/edde746/MPVKit' +pkg_revision = '8aa5e1abb68fffe1d29511da9e02feca575c9311' existing_pkg = project.root_object.package_references.find do |p| p.repositoryURL == pkg_url rescue false end if existing_pkg - puts "[skip] MPVKit SPM package already present" + existing_pkg.requirement = { 'kind' => 'revision', 'revision' => pkg_revision } + puts "[set ] MPVKit SPM package revision" else pkg = project.new(Xcodeproj::Project::Object::XCRemoteSwiftPackageReference) pkg.repositoryURL = pkg_url - pkg.requirement = { 'kind' => 'branch', 'branch' => 'main' } + pkg.requirement = { 'kind' => 'revision', 'revision' => pkg_revision } project.root_object.package_references << pkg product = project.new(Xcodeproj::Project::Object::XCSwiftPackageProductDependency)