feat: MPV player for both Android and iOS with added HW decoding PiP (with subtitles) (#1332)

Co-authored-by: Alex Kim <alexkim@Alexs-MacBook-Pro.local>
Co-authored-by: Alex <111128610+Alexk2309@users.noreply.github.com>
Co-authored-by: Simon-Eklundh <simon.eklundh@proton.me>
This commit is contained in:
Fredrik Burmester
2026-01-10 19:35:27 +01:00
committed by GitHub
parent df2f44e086
commit f1575ca48b
98 changed files with 3257 additions and 7448 deletions

View File

@@ -12,6 +12,7 @@ protocol PiPControllerDelegate: AnyObject {
func pipController(_ controller: PiPController, skipByInterval interval: CMTime)
func pipControllerIsPlaying(_ controller: PiPController) -> Bool
func pipControllerDuration(_ controller: PiPController) -> Double
func pipControllerCurrentPosition(_ controller: PiPController) -> Double
}
final class PiPController: NSObject {
@@ -20,6 +21,13 @@ final class PiPController: NSObject {
weak var delegate: PiPControllerDelegate?
// Timebase for PiP progress tracking
private var timebase: CMTimebase?
// Track current time for PiP progress
private var currentTime: CMTime = .zero
private var currentDuration: Double = 0
var isPictureInPictureSupported: Bool {
return AVPictureInPictureController.isPictureInPictureSupported()
}
@@ -35,9 +43,29 @@ final class PiPController: NSObject {
init(sampleBufferDisplayLayer: AVSampleBufferDisplayLayer) {
self.sampleBufferDisplayLayer = sampleBufferDisplayLayer
super.init()
setupTimebase()
setupPictureInPicture()
}
private func setupTimebase() {
// Create a timebase for tracking playback time
var newTimebase: CMTimebase?
let status = CMTimebaseCreateWithSourceClock(
allocator: kCFAllocatorDefault,
sourceClock: CMClockGetHostTimeClock(),
timebaseOut: &newTimebase
)
if status == noErr, let tb = newTimebase {
timebase = tb
CMTimebaseSetTime(tb, time: .zero)
CMTimebaseSetRate(tb, rate: 0) // Start paused
// Set the control timebase on the display layer
sampleBufferDisplayLayer?.controlTimebase = tb
}
}
private func setupPictureInPicture() {
guard isPictureInPictureSupported,
let displayLayer = sampleBufferDisplayLayer else {
@@ -81,6 +109,9 @@ final class PiPController: NSObject {
}
func updatePlaybackState() {
// Only invalidate when PiP is active to avoid "no context menu visible" warnings
guard isPictureInPictureActive else { return }
if Thread.isMainThread {
pipController?.invalidatePlaybackState()
} else {
@@ -89,6 +120,36 @@ final class PiPController: NSObject {
}
}
}
/// Updates the current playback time for PiP progress display
func setCurrentTime(_ time: CMTime) {
currentTime = time
// Update the timebase to reflect current position
if let tb = timebase {
CMTimebaseSetTime(tb, time: time)
}
// Only invalidate when PiP is active to avoid unnecessary updates
if isPictureInPictureActive {
updatePlaybackState()
}
}
/// Updates the current playback time from seconds
func setCurrentTimeFromSeconds(_ seconds: Double, duration: Double) {
guard seconds >= 0 else { return }
currentDuration = duration
let time = CMTime(seconds: seconds, preferredTimescale: 1000)
setCurrentTime(time)
}
/// Updates the playback rate on the timebase (1.0 = playing, 0.0 = paused)
func setPlaybackRate(_ rate: Float) {
if let tb = timebase {
CMTimebaseSetRate(tb, rate: Float64(rate))
}
}
}
// MARK: - AVPictureInPictureControllerDelegate