This commit is contained in:
Alex Kim
2026-01-10 02:31:21 +11:00
parent fd2d420320
commit d1fdea76e8
8 changed files with 270 additions and 952 deletions

View File

@@ -135,12 +135,10 @@
["./plugins/withTrustLocalCerts.js"],
["./plugins/withGradleProperties.js"],
[
"./plugins/addSPMDependenciesToMainTarget",
"./plugins/withGitPod.js",
{
"version": "0.40.0",
"repositoryUrl": "https://github.com/Alexk2309/mpvkit-private",
"repoName": "mpvkit-private",
"productName": "MPVKit"
"podName": "MPVKit-GPL",
"podspecUrl": "https://raw.githubusercontent.com/Alexk2309/MPVKit/0.40.0-av/MPVKit-GPL.podspec"
}
]
],

File diff suppressed because it is too large Load Diff

View File

@@ -13,7 +13,7 @@ Pod::Spec.new do |s|
s.static_framework = true
s.dependency 'ExpoModulesCore'
s.dependency 'MPVKit', '~> 0.40.0'
s.dependency 'MPVKit-GPL'
# Swift/Objective-C compatibility
s.pod_target_xcconfig = {

View File

@@ -148,12 +148,14 @@ class MpvPlayerView: ExpoView {
func play() {
intendedPlayState = true
renderer?.play()
pipController?.setPlaybackRate(1.0)
pipController?.updatePlaybackState()
}
func pause() {
intendedPlayState = false
renderer?.pausePlayback()
pipController?.setPlaybackRate(0.0)
pipController?.updatePlaybackState()
}
@@ -283,9 +285,9 @@ extension MpvPlayerView: MPVSoftwareRendererDelegate {
DispatchQueue.main.async { [weak self] in
guard let self else { return }
// Only update PiP state when PiP is active
// Update PiP current time for progress bar
if self.pipController?.isPictureInPictureActive == true {
self.pipController?.updatePlaybackState()
self.pipController?.setCurrentTimeFromSeconds(position, duration: duration)
}
self.onProgress([
@@ -301,12 +303,14 @@ extension MpvPlayerView: MPVSoftwareRendererDelegate {
guard let self else { return }
// Don't update intendedPlayState here - it's only set by user actions (play/pause)
// This prevents PiP UI flicker during seeking
// Sync timebase rate with actual playback state
self.pipController?.setPlaybackRate(isPaused ? 0.0 : 1.0)
self.onPlaybackStateChange([
"isPaused": isPaused,
"isPlaying": !isPaused,
])
// Note: Don't call updatePlaybackState() here to avoid flicker
// PiP queries pipControllerIsPlaying when it needs the state
}
}
@@ -343,12 +347,14 @@ extension MpvPlayerView: PiPControllerDelegate {
print("PiP will start")
// Sync timebase before PiP starts for smooth transition
renderer?.syncTimebase()
pipController?.updatePlaybackState()
// Set current time for PiP progress bar
pipController?.setCurrentTimeFromSeconds(cachedPosition, duration: cachedDuration)
}
func pipController(_ controller: PiPController, didStartPictureInPicture: Bool) {
print("PiP did start: \(didStartPictureInPicture)")
pipController?.updatePlaybackState()
// Ensure current time is synced when PiP starts
pipController?.setCurrentTimeFromSeconds(cachedPosition, duration: cachedDuration)
}
func pipController(_ controller: PiPController, willStopPictureInPicture: Bool) {
@@ -371,12 +377,16 @@ extension MpvPlayerView: PiPControllerDelegate {
func pipControllerPlay(_ controller: PiPController) {
print("PiP play requested")
play()
intendedPlayState = true
renderer?.play()
pipController?.setPlaybackRate(1.0)
}
func pipControllerPause(_ controller: PiPController) {
print("PiP pause requested")
pause()
intendedPlayState = false
renderer?.pausePlayback()
pipController?.setPlaybackRate(0.0)
}
func pipController(_ controller: PiPController, skipByInterval interval: CMTime) {
@@ -394,4 +404,8 @@ extension MpvPlayerView: PiPControllerDelegate {
func pipControllerDuration(_ controller: PiPController) -> Double {
return getDuration()
}
func pipControllerCurrentPosition(_ controller: PiPController) -> Double {
return getCurrentPosition()
}
}

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 {
@@ -89,6 +117,34 @@ 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)
}
// Always invalidate to refresh the PiP UI
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

View File

@@ -1,104 +0,0 @@
const { withXcodeProject } = require("@expo/config-plugins");
const addSPMDependenciesToMainTarget = (config, options) =>
withXcodeProject(config, (config) => {
const { version, repositoryUrl, repoName, productName } = options;
const xcodeProject = config.modResults;
// update XCRemoteSwiftPackageReference
const spmReferences =
xcodeProject.hash.project.objects.XCRemoteSwiftPackageReference;
if (!spmReferences) {
xcodeProject.hash.project.objects.XCRemoteSwiftPackageReference = {};
}
const packageReferenceUUID = xcodeProject.generateUuid();
xcodeProject.hash.project.objects.XCRemoteSwiftPackageReference[
`${packageReferenceUUID} /* XCRemoteSwiftPackageReference "${repoName}" */`
] = {
isa: "XCRemoteSwiftPackageReference",
repositoryURL: repositoryUrl,
requirement: {
kind: "upToNextMajorVersion",
minimumVersion: version,
},
};
// update XCSwiftPackageProductDependency
const spmProducts =
xcodeProject.hash.project.objects.XCSwiftPackageProductDependency;
if (!spmProducts) {
xcodeProject.hash.project.objects.XCSwiftPackageProductDependency = {};
}
const packageUUID = xcodeProject.generateUuid();
xcodeProject.hash.project.objects.XCSwiftPackageProductDependency[
`${packageUUID} /* ${productName} */`
] = {
isa: "XCSwiftPackageProductDependency",
// from step before
package: `${packageReferenceUUID} /* XCRemoteSwiftPackageReference "${repoName}" */`,
productName: productName,
};
// update PBXProject
const projectId = Object.keys(
xcodeProject.hash.project.objects.PBXProject,
).at(0);
if (
!xcodeProject.hash.project.objects.PBXProject[projectId].packageReferences
) {
xcodeProject.hash.project.objects.PBXProject[
projectId
].packageReferences = [];
}
xcodeProject.hash.project.objects.PBXProject[projectId].packageReferences =
[
...xcodeProject.hash.project.objects.PBXProject[projectId]
.packageReferences,
`${packageReferenceUUID} /* XCRemoteSwiftPackageReference "${repoName}" */`,
];
// update PBXBuildFile
const frameworkUUID = xcodeProject.generateUuid();
xcodeProject.hash.project.objects.PBXBuildFile[`${frameworkUUID}_comment`] =
`${productName} in Frameworks`;
xcodeProject.hash.project.objects.PBXBuildFile[frameworkUUID] = {
isa: "PBXBuildFile",
productRef: packageUUID,
productRef_comment: productName,
};
// update PBXFrameworksBuildPhase
const buildPhaseId = Object.keys(
xcodeProject.hash.project.objects.PBXFrameworksBuildPhase,
).at(0);
if (
!xcodeProject.hash.project.objects.PBXFrameworksBuildPhase[buildPhaseId]
.files
) {
xcodeProject.hash.project.objects.PBXFrameworksBuildPhase[
buildPhaseId
].files = [];
}
xcodeProject.hash.project.objects.PBXFrameworksBuildPhase[
buildPhaseId
].files = [
...xcodeProject.hash.project.objects.PBXFrameworksBuildPhase[buildPhaseId]
.files,
`${frameworkUUID} /* ${productName} in Frameworks */`,
];
return config;
});
module.exports = addSPMDependenciesToMainTarget;

View File

@@ -1,5 +0,0 @@
{
"name": "addSPMDependenciesToMainTarget",
"version": "1.0.0",
"main": "app.plugin.js"
}

24
plugins/withGitPod.js Normal file
View File

@@ -0,0 +1,24 @@
const { withPodfile } = require("@expo/config-plugins");
const withGitPod = (config, { podName, podspecUrl }) => {
return withPodfile(config, (config) => {
const podfile = config.modResults.contents;
const podLine = ` pod '${podName}', :podspec => '${podspecUrl}'`;
// Check if already added
if (podfile.includes(podLine)) {
return config;
}
// Insert after "use_expo_modules!"
config.modResults.contents = podfile.replace(
"use_expo_modules!",
`use_expo_modules!\n${podLine}`,
);
return config;
});
};
module.exports = withGitPod;