fix(ios): keep PiP timebase renderer-owned

This commit is contained in:
edde746
2026-05-19 20:09:23 +02:00
parent 7d60875e19
commit d58007a9e2
6 changed files with 27 additions and 36 deletions
+1 -1
View File
@@ -789,7 +789,7 @@
repositoryURL = "https://github.com/edde746/MPVKit"; repositoryURL = "https://github.com/edde746/MPVKit";
requirement = { requirement = {
kind = revision; kind = revision;
revision = 32bfe07056049bad711ec2b5a8976ab203863c8f; revision = 42dcc3123000361d0be54fbaec981043b77e1e05;
}; };
}; };
/* End XCRemoteSwiftPackageReference section */ /* End XCRemoteSwiftPackageReference section */
@@ -40,7 +40,7 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/edde746/MPVKit", "location" : "https://github.com/edde746/MPVKit",
"state" : { "state" : {
"revision" : "32bfe07056049bad711ec2b5a8976ab203863c8f" "revision" : "42dcc3123000361d0be54fbaec981043b77e1e05"
} }
}, },
{ {
@@ -40,7 +40,7 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/edde746/MPVKit", "location" : "https://github.com/edde746/MPVKit",
"state" : { "state" : {
"revision" : "32bfe07056049bad711ec2b5a8976ab203863c8f" "revision" : "42dcc3123000361d0be54fbaec981043b77e1e05"
} }
}, },
{ {
+13 -26
View File
@@ -13,7 +13,7 @@ import UIKit
func pipDidStop(restored: Bool) func pipDidStop(restored: Bool)
func pipDidFailToStart(error: Error?) func pipDidFailToStart(error: Error?)
func pipSetPlaying(_ playing: Bool) func pipSetPlaying(_ playing: Bool)
func pipSkip(byInterval seconds: Double) func pipSkip(byInterval seconds: Double, completion: @escaping () -> Void)
var isPipPlaying: Bool { get } var isPipPlaying: Bool { get }
var pipDuration: Double { get } var pipDuration: Double { get }
} }
@@ -48,7 +48,7 @@ import UIKit
/// Forward play/pause commands from PiP overlay to mpv /// Forward play/pause commands from PiP overlay to mpv
func pipSetPlaying(_ playing: Bool) func pipSetPlaying(_ playing: Bool)
/// Forward skip forward/backward commands from PiP overlay to mpv /// 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 /// Query whether mpv is currently playing
var isPipPlaying: Bool { get } var isPipPlaying: Bool { get }
/// Get total duration in seconds /// Get total duration in seconds
@@ -99,9 +99,7 @@ import UIKit
self.delegateHelper = helper self.delegateHelper = helper
pipController = AVPictureInPictureController(contentSource: contentSource) pipController = AVPictureInPictureController(contentSource: contentSource)
pipController?.delegate = helper 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) /// Enable/disable system auto-PiP (starts PiP automatically on background transition)
@@ -110,30 +108,15 @@ import UIKit
pipController?.canStartPictureInPictureAutomaticallyFromInline = enabled pipController?.canStartPictureInPictureAutomaticallyFromInline = enabled
} }
/// Sync the layer's controlTimebase with the actual playback position. /// MPVKit owns the sample-buffer layer timebase. PiP only reads it.
/// This makes the PiP progress bar show the correct time.
func syncTimebase(currentTime: Double, isPlaying: Bool) { 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) { func warmLayer(currentTime: Double, isPlaying: Bool) {
guard let sampleBufferLayer else { return } if sampleBufferLayer?.controlTimebase == nil {
if sampleBufferLayer.controlTimebase == nil { print("[MpvPipController] Waiting for MPVKit renderer timebase before PiP")
var timebase: CMTimebase?
CMTimebaseCreateWithSourceClock(
allocator: kCFAllocatorDefault,
sourceClock: CMClockGetHostTimeClock(),
timebaseOut: &timebase
)
if let tb = timebase {
sampleBufferLayer.controlTimebase = tb
}
} }
syncTimebase(currentTime: currentTime, isPlaying: isPlaying)
} }
// MARK: - Public API // MARK: - Public API
@@ -188,6 +171,7 @@ import UIKit
/// Invalidate the playback state so PiP updates its UI (play/pause button) /// Invalidate the playback state so PiP updates its UI (play/pause button)
func invalidatePlaybackState() { func invalidatePlaybackState() {
guard #available(iOS 15.0, *) else { return }
pipController?.invalidatePlaybackState() pipController?.invalidatePlaybackState()
} }
@@ -310,8 +294,11 @@ import UIKit
) { ) {
let seconds = CMTimeGetSeconds(skipInterval) let seconds = CMTimeGetSeconds(skipInterval)
print("[MpvPipController] PiP skip by \(seconds)s") print("[MpvPipController] PiP skip by \(seconds)s")
controller?.delegate?.pipSkip(byInterval: seconds) guard let delegate = controller?.delegate else {
completionHandler() completionHandler()
return
}
delegate.pipSkip(byInterval: seconds, completion: completionHandler)
} }
} }
+10 -6
View File
@@ -504,13 +504,17 @@ extension MpvPlayerPlugin: MpvPipDelegate {
} }
} }
func pipSkip(byInterval seconds: Double) { func pipSkip(byInterval seconds: Double, completion: @escaping () -> Void) {
guard let playerCore = playerCore else { return } guard let playerCore = playerCore else {
completion()
return
}
let newTime = max(0, playerCore.timePos + seconds) let newTime = max(0, playerCore.timePos + seconds)
playerCore.command(["seek", String(newTime), "absolute"]) playerCore.commandAsync(["seek", String(newTime), "absolute"]) { [weak self] _ in
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self?.syncPipTimebase() self?.pipController?.invalidatePlaybackState()
self?.pipController?.invalidatePlaybackState() completion()
}
} }
} }
+1 -1
View File
@@ -1,7 +1,7 @@
name: plezy name: plezy
description: "A beautiful Plex and Jellyfin client for Flutter" description: "A beautiful Plex and Jellyfin client for Flutter"
publish_to: "none" publish_to: "none"
version: 2.1.1+98 version: 2.1.2+99
environment: environment:
sdk: ">=3.12.0 <4.0.0" sdk: ">=3.12.0 <4.0.0"