From d58007a9e250bf9839927d3263e958dc987efb7b Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Tue, 19 May 2026 19:59:48 +0200 Subject: [PATCH] fix(ios): keep PiP timebase renderer-owned --- ios/Runner.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/swiftpm/Package.resolved | 2 +- .../xcshareddata/swiftpm/Package.resolved | 2 +- ios/Runner/MpvPlayer/MpvPipController.swift | 39 +++++++------------ ios/Runner/MpvPlayer/MpvPlayerPlugin.swift | 16 +++++--- pubspec.yaml | 2 +- 6 files changed, 27 insertions(+), 36 deletions(-) diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index f88cb9c3..0cbf868a 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -789,7 +789,7 @@ repositoryURL = "https://github.com/edde746/MPVKit"; requirement = { kind = revision; - revision = 32bfe07056049bad711ec2b5a8976ab203863c8f; + revision = 42dcc3123000361d0be54fbaec981043b77e1e05; }; }; /* 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 a3904f2f..af20df59 100644 --- a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -40,7 +40,7 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/edde746/MPVKit", "state" : { - "revision" : "32bfe07056049bad711ec2b5a8976ab203863c8f" + "revision" : "42dcc3123000361d0be54fbaec981043b77e1e05" } }, { diff --git a/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved index a3904f2f..af20df59 100644 --- a/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -40,7 +40,7 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/edde746/MPVKit", "state" : { - "revision" : "32bfe07056049bad711ec2b5a8976ab203863c8f" + "revision" : "42dcc3123000361d0be54fbaec981043b77e1e05" } }, { diff --git a/ios/Runner/MpvPlayer/MpvPipController.swift b/ios/Runner/MpvPlayer/MpvPipController.swift index 7a6c9763..45d052e5 100644 --- a/ios/Runner/MpvPlayer/MpvPipController.swift +++ b/ios/Runner/MpvPlayer/MpvPipController.swift @@ -13,7 +13,7 @@ import UIKit func pipDidStop(restored: Bool) func pipDidFailToStart(error: Error?) func pipSetPlaying(_ playing: Bool) - func pipSkip(byInterval seconds: Double) + func pipSkip(byInterval seconds: Double, completion: @escaping () -> Void) var isPipPlaying: Bool { get } var pipDuration: Double { get } } @@ -48,7 +48,7 @@ import UIKit /// Forward play/pause commands from PiP overlay to mpv func pipSetPlaying(_ playing: Bool) /// Forward skip forward/backward commands from PiP overlay to mpv - func pipSkip(byInterval seconds: Double) + func pipSkip(byInterval seconds: Double, completion: @escaping () -> Void) /// Query whether mpv is currently playing var isPipPlaying: Bool { get } /// Get total duration in seconds @@ -99,9 +99,7 @@ import UIKit self.delegateHelper = helper pipController = AVPictureInPictureController(contentSource: contentSource) pipController?.delegate = helper - if #available(iOS 14.2, *) { - pipController?.canStartPictureInPictureAutomaticallyFromInline = false - } + pipController?.canStartPictureInPictureAutomaticallyFromInline = false } /// Enable/disable system auto-PiP (starts PiP automatically on background transition) @@ -110,30 +108,15 @@ import UIKit pipController?.canStartPictureInPictureAutomaticallyFromInline = enabled } - /// Sync the layer's controlTimebase with the actual playback position. - /// This makes the PiP progress bar show the correct time. + /// MPVKit owns the sample-buffer layer timebase. PiP only reads it. func syncTimebase(currentTime: Double, isPlaying: Bool) { - 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 so the PiP progress UI can follow mpv. + /// Ensure the layer has MPVKit's renderer-owned timebase before PiP starts. func warmLayer(currentTime: Double, isPlaying: Bool) { - guard let sampleBufferLayer else { return } - if sampleBufferLayer.controlTimebase == nil { - var timebase: CMTimebase? - CMTimebaseCreateWithSourceClock( - allocator: kCFAllocatorDefault, - sourceClock: CMClockGetHostTimeClock(), - timebaseOut: &timebase - ) - if let tb = timebase { - sampleBufferLayer.controlTimebase = tb - } + if sampleBufferLayer?.controlTimebase == nil { + print("[MpvPipController] Waiting for MPVKit renderer timebase before PiP") } - syncTimebase(currentTime: currentTime, isPlaying: isPlaying) } // MARK: - Public API @@ -188,6 +171,7 @@ import UIKit /// Invalidate the playback state so PiP updates its UI (play/pause button) func invalidatePlaybackState() { + guard #available(iOS 15.0, *) else { return } pipController?.invalidatePlaybackState() } @@ -310,8 +294,11 @@ import UIKit ) { let seconds = CMTimeGetSeconds(skipInterval) print("[MpvPipController] PiP skip by \(seconds)s") - controller?.delegate?.pipSkip(byInterval: seconds) - completionHandler() + guard let delegate = controller?.delegate else { + completionHandler() + return + } + delegate.pipSkip(byInterval: seconds, completion: completionHandler) } } diff --git a/ios/Runner/MpvPlayer/MpvPlayerPlugin.swift b/ios/Runner/MpvPlayer/MpvPlayerPlugin.swift index ae2f886c..0b0b1b47 100644 --- a/ios/Runner/MpvPlayer/MpvPlayerPlugin.swift +++ b/ios/Runner/MpvPlayer/MpvPlayerPlugin.swift @@ -504,13 +504,17 @@ extension MpvPlayerPlugin: MpvPipDelegate { } } - func pipSkip(byInterval seconds: Double) { - guard let playerCore = playerCore else { return } + func pipSkip(byInterval seconds: Double, completion: @escaping () -> Void) { + guard let playerCore = playerCore else { + completion() + return + } let newTime = max(0, playerCore.timePos + seconds) - playerCore.command(["seek", String(newTime), "absolute"]) - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in - self?.syncPipTimebase() - self?.pipController?.invalidatePlaybackState() + playerCore.commandAsync(["seek", String(newTime), "absolute"]) { [weak self] _ in + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + self?.pipController?.invalidatePlaybackState() + completion() + } } } diff --git a/pubspec.yaml b/pubspec.yaml index d5a927da..7d778787 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: plezy description: "A beautiful Plex and Jellyfin client for Flutter" publish_to: "none" -version: 2.1.1+98 +version: 2.1.2+99 environment: sdk: ">=3.12.0 <4.0.0"