diff --git a/app.json b/app.json index 9eb5aff0..6c4b8c90 100644 --- a/app.json +++ b/app.json @@ -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" } ] ], diff --git a/modules/mpv-player/ios/MPVSoftwareRenderer.swift b/modules/mpv-player/ios/MPVSoftwareRenderer.swift index df19c10e..6fb8619b 100644 --- a/modules/mpv-player/ios/MPVSoftwareRenderer.swift +++ b/modules/mpv-player/ios/MPVSoftwareRenderer.swift @@ -1,5 +1,5 @@ import UIKit -import Libmpv +import MPVKit import CoreMedia import CoreVideo import AVFoundation @@ -12,34 +12,19 @@ protocol MPVSoftwareRendererDelegate: AnyObject { func renderer(_ renderer: MPVSoftwareRenderer, didBecomeTracksReady: Bool) } +/// MPV player using vo_avfoundation for video output. +/// This renders video directly to AVSampleBufferDisplayLayer for PiP support. final class MPVSoftwareRenderer { enum RendererError: Error { case mpvCreationFailed case mpvInitialization(Int32) - case renderContextCreation(Int32) } private let displayLayer: AVSampleBufferDisplayLayer - private let renderQueue = DispatchQueue(label: "mpv.software.render", qos: .userInitiated) - private let eventQueue = DispatchQueue(label: "mpv.software.events", qos: .utility) - private let stateQueue = DispatchQueue(label: "mpv.software.state", attributes: .concurrent) - private let eventQueueGroup = DispatchGroup() - private let renderQueueKey = DispatchSpecificKey() - - private var dimensionsArray = [Int32](repeating: 0, count: 2) - private var renderParams = [mpv_render_param](repeating: mpv_render_param(type: MPV_RENDER_PARAM_INVALID, data: nil), count: 5) + private let queue = DispatchQueue(label: "mpv.avfoundation", qos: .userInitiated) + private let stateQueue = DispatchQueue(label: "mpv.avfoundation.state", attributes: .concurrent) private var mpv: OpaquePointer? - private var renderContext: OpaquePointer? - private var videoSize: CGSize = .zero - private var pixelBufferPool: CVPixelBufferPool? - private var pixelBufferPoolAuxAttributes: CFDictionary? - private var formatDescription: CMVideoFormatDescription? - private var didFlushForFormatChange = false - private var poolWidth: Int = 0 - private var poolHeight: Int = 0 - private var preAllocatedBuffers: [CVPixelBuffer] = [] - private let maxPreAllocatedBuffers = 12 private var currentPreset: PlayerPreset? private var currentURL: URL? @@ -48,26 +33,18 @@ final class MPVSoftwareRenderer { private var initialSubtitleId: Int? private var initialAudioId: Int? - private var disposeBag: [() -> Void] = [] - private var isRunning = false private var isStopping = false - private var shouldClearPixelBuffer = false - private let bgraFormatCString: [CChar] = Array("bgra\0".utf8CString) - private let maxInFlightBuffers = 3 - private var inFlightBufferCount = 0 - private let inFlightLock = NSLock() weak var delegate: MPVSoftwareRendererDelegate? - // Thread-safe state for playback (uses existing stateQueue to prevent races causing stutter) + // Thread-safe state for playback private var _cachedDuration: Double = 0 private var _cachedPosition: Double = 0 private var _isPaused: Bool = true private var _playbackSpeed: Double = 1.0 - private var _isSeeking: Bool = false - private var _positionUpdateTime: CFTimeInterval = 0 // Host time when position was last updated - private var _lastPTS: Double = 0 // Last presentation timestamp (ensures monotonic increase) + private var _isLoading: Bool = false + private var _isReadyToSeek: Bool = false // Thread-safe accessors private var cachedDuration: Double { @@ -86,70 +63,21 @@ final class MPVSoftwareRenderer { get { stateQueue.sync { _playbackSpeed } } set { stateQueue.async(flags: .barrier) { self._playbackSpeed = newValue } } } - private var isSeeking: Bool { - get { stateQueue.sync { _isSeeking } } - set { stateQueue.async(flags: .barrier) { self._isSeeking = newValue } } + private var isLoading: Bool { + get { stateQueue.sync { _isLoading } } + set { stateQueue.async(flags: .barrier) { self._isLoading = newValue } } } - private var positionUpdateTime: CFTimeInterval { - get { stateQueue.sync { _positionUpdateTime } } - set { stateQueue.async(flags: .barrier) { self._positionUpdateTime = newValue } } + private var isReadyToSeek: Bool { + get { stateQueue.sync { _isReadyToSeek } } + set { stateQueue.async(flags: .barrier) { self._isReadyToSeek = newValue } } } - private var lastPTS: Double { - get { stateQueue.sync { _lastPTS } } - set { stateQueue.async(flags: .barrier) { self._lastPTS = newValue } } - } - - /// Get next monotonically increasing PTS based on video position - /// This ensures frames always have increasing timestamps (prevents stutter from drops) - private func nextMonotonicPTS() -> Double { - let currentPos = interpolatedPosition() - let last = lastPTS - - // Ensure PTS always increases (by at least 1ms) to prevent frame drops - let pts = max(currentPos, last + 0.001) - lastPTS = pts - return pts - } - - /// Calculate smooth interpolated position based on last known position + elapsed time - private func interpolatedPosition() -> Double { - let basePosition = cachedPosition - let lastUpdate = positionUpdateTime - let paused = isPaused - let speed = playbackSpeed - - guard !paused, lastUpdate > 0 else { - return basePosition - } - - let elapsed = CACurrentMediaTime() - lastUpdate - return basePosition + (elapsed * speed) - } - - private var isLoading: Bool = false - private var isRenderScheduled = false - private var lastRenderTime: CFTimeInterval = 0 - private var minRenderInterval: CFTimeInterval - private var isReadyToSeek: Bool = false - private var lastRenderDimensions: CGSize = .zero var isPausedState: Bool { return isPaused } init(displayLayer: AVSampleBufferDisplayLayer) { - guard - let screen = UIApplication.shared.connectedScenes - .compactMap({ ($0 as? UIWindowScene)?.screen }) - .first - else { - fatalError("⚠️ No active screen found — app may not have a visible window yet.") - } self.displayLayer = displayLayer - let maxFPS = screen.maximumFramesPerSecond - let cappedFPS = min(maxFPS, 60) - self.minRenderInterval = 1.0 / CFTimeInterval(cappedFPS) - renderQueue.setSpecific(key: renderQueueKey, value: ()) } deinit { @@ -162,37 +90,52 @@ final class MPVSoftwareRenderer { throw RendererError.mpvCreationFailed } mpv = handle - setOption(name: "terminal", value: "yes") - setOption(name: "msg-level", value: "status") - setOption(name: "keep-open", value: "yes") - setOption(name: "idle", value: "yes") - setOption(name: "vo", value: "libmpv") - setOption(name: "hwdec", value: "videotoolbox-copy") - setOption(name: "gpu-api", value: "metal") - setOption(name: "gpu-context", value: "metal") - setOption(name: "demuxer-thread", value: "yes") - setOption(name: "ytdl", value: "yes") - setOption(name: "profile", value: "fast") - setOption(name: "vd-lavc-threads", value: "8") - setOption(name: "cache", value: "yes") - setOption(name: "demuxer-max-bytes", value: "150M") - setOption(name: "demuxer-readahead-secs", value: "20") - // Subtitle options - use vf=sub to burn subtitles into video frames - // This happens at the filter level, BEFORE the software renderer - setOption(name: "vf", value: "sub") - setOption(name: "sub-visibility", value: "yes") - + // Logging + #if DEBUG + checkError(mpv_request_log_messages(handle, "warn")) + #else + checkError(mpv_request_log_messages(handle, "no")) + #endif + + // Pass the AVSampleBufferDisplayLayer to mpv via --wid + // The vo_avfoundation driver expects this + var displayLayerPtr = Int64(Int(bitPattern: Unmanaged.passUnretained(displayLayer).toOpaque())) + checkError(mpv_set_option(handle, "wid", MPV_FORMAT_INT64, &displayLayerPtr)) + + // Use AVFoundation video output - required for PiP support + checkError(mpv_set_option_string(handle, "vo", "avfoundation")) + + // Enable composite OSD mode - renders subtitles directly onto video frames using GPU + // This is better for PiP as subtitles are baked into the video + checkError(mpv_set_option_string(handle, "avfoundation-composite-osd", "yes")) + + // Hardware decoding with VideoToolbox - REQUIRED for vo_avfoundation + // vo_avfoundation ONLY accepts IMGFMT_VIDEOTOOLBOX frames + checkError(mpv_set_option_string(handle, "hwdec", "videotoolbox")) + checkError(mpv_set_option_string(handle, "hwdec-codecs", "all")) + checkError(mpv_set_option_string(handle, "hwdec-software-fallback", "no")) + + // Subtitle and audio settings + checkError(mpv_set_option_string(mpv, "subs-match-os-language", "yes")) + checkError(mpv_set_option_string(mpv, "subs-fallback", "yes")) + + // Initialize mpv let initStatus = mpv_initialize(handle) guard initStatus >= 0 else { throw RendererError.mpvInitialization(initStatus) } - mpv_request_log_messages(handle, "warn") - - try createRenderContext() + // Observe properties observeProperties() - installWakeupHandler() + + // Setup wakeup callback + mpv_set_wakeup_callback(handle, { ctx in + guard let ctx = ctx else { return } + let instance = Unmanaged.fromOpaque(ctx).takeUnretainedValue() + instance.processEvents() + }, Unmanaged.passUnretained(self).toOpaque()) + isRunning = true } @@ -202,52 +145,12 @@ final class MPVSoftwareRenderer { isRunning = false isStopping = true - var handleForShutdown: OpaquePointer? - - renderQueue.sync { [weak self] in - guard let self else { return } + queue.sync { [weak self] in + guard let self, let handle = self.mpv else { return } - if let ctx = self.renderContext { - mpv_render_context_set_update_callback(ctx, nil, nil) - mpv_render_context_free(ctx) - self.renderContext = nil - } - - handleForShutdown = self.mpv - if let handle = handleForShutdown { - mpv_set_wakeup_callback(handle, nil, nil) - self.command(handle, ["quit"]) - mpv_wakeup(handle) - } - - self.formatDescription = nil - self.preAllocatedBuffers.removeAll() - self.pixelBufferPool = nil - self.poolWidth = 0 - self.poolHeight = 0 - self.lastRenderDimensions = .zero - } - - eventQueueGroup.wait() - - renderQueue.sync { [weak self] in - guard let self else { return } - - if let handle = handleForShutdown { - mpv_destroy(handle) - } + mpv_set_wakeup_callback(handle, nil, nil) + mpv_terminate_destroy(handle) self.mpv = nil - - self.preAllocatedBuffers.removeAll() - self.pixelBufferPool = nil - self.pixelBufferPoolAuxAttributes = nil - self.formatDescription = nil - self.poolWidth = 0 - self.poolHeight = 0 - self.lastRenderDimensions = .zero - - self.disposeBag.forEach { $0() } - self.disposeBag.removeAll() } DispatchQueue.main.async { [weak self] in @@ -278,7 +181,7 @@ final class MPVSoftwareRenderer { self.initialSubtitleId = initialSubtitleId self.initialAudioId = initialAudioId - renderQueue.async { [weak self] in + queue.async { [weak self] in guard let self else { return } self.isLoading = true self.isReadyToSeek = false @@ -290,11 +193,11 @@ final class MPVSoftwareRenderer { guard let handle = self.mpv else { return } self.apply(commands: preset.commands, on: handle) - // Sync stop to ensure previous playback is stopped before loading new file - self.commandSync(handle, ["stop"]) + // Stop previous playback before loading new file + self.command(handle, ["stop"]) self.updateHTTPHeaders(headers) - // Set start position using property (setOption only works before mpv_initialize) + // Set start position if let startPos = startPosition, startPos > 0 { self.setProperty(name: "start", value: String(format: "%.2f", startPos)) } else { @@ -306,7 +209,7 @@ final class MPVSoftwareRenderer { self.setAudioTrack(audioId) } - // Set initial subtitle track if no external subs (external subs change track IDs) + // Set initial subtitle track if no external subs if self.pendingExternalSubtitles.isEmpty { if let subId = self.initialSubtitleId { self.setSubtitleTrack(subId) @@ -314,16 +217,10 @@ final class MPVSoftwareRenderer { self.disableSubtitles() } } else { - // External subs will be added after file loads, set sid then self.disableSubtitles() } - var finalURL = url - if !url.isFileURL { - finalURL = url - } - - let target = finalURL.isFileURL ? finalURL.path : finalURL.absoluteString + let target = url.isFileURL ? url.path : url.absoluteString self.command(handle, ["loadfile", target, "replace"]) } } @@ -336,28 +233,22 @@ final class MPVSoftwareRenderer { func applyPreset(_ preset: PlayerPreset) { currentPreset = preset guard let handle = mpv else { return } - renderQueue.async { [weak self] in + queue.async { [weak self] in guard let self else { return } self.apply(commands: preset.commands, on: handle) } } + // MARK: - Property Helpers + private func setOption(name: String, value: String) { guard let handle = mpv else { return } - _ = value.withCString { valuePointer in - name.withCString { namePointer in - mpv_set_option_string(handle, namePointer, valuePointer) - } - } + checkError(mpv_set_option_string(handle, name, value)) } private func setProperty(name: String, value: String) { guard let handle = mpv else { return } - let status = value.withCString { valuePointer in - name.withCString { namePointer in - mpv_set_property_string(handle, namePointer, valuePointer) - } - } + let status = mpv_set_property_string(handle, name, value) if status < 0 { Logger.shared.log("Failed to set property \(name)=\(value) (\(status))", type: "Warn") } @@ -365,9 +256,7 @@ final class MPVSoftwareRenderer { private func clearProperty(name: String) { guard let handle = mpv else { return } - let status = name.withCString { namePointer in - mpv_set_property(handle, namePointer, MPV_FORMAT_NONE, nil) - } + let status = mpv_set_property(handle, name, MPV_FORMAT_NONE, nil) if status < 0 { Logger.shared.log("Failed to clear property \(name) (\(status))", type: "Warn") } @@ -380,519 +269,23 @@ final class MPVSoftwareRenderer { } let headerString = headers - .map { key, value in - "\(key): \(value)" - } + .map { key, value in "\(key): \(value)" } .joined(separator: "\r\n") setProperty(name: "http-header-fields", value: headerString) } - private func createRenderContext() throws { - guard let handle = mpv else { return } - - var apiType = MPV_RENDER_API_TYPE_SW - let status = withUnsafePointer(to: &apiType) { apiTypePtr in - var params = [ - mpv_render_param(type: MPV_RENDER_PARAM_API_TYPE, data: UnsafeMutableRawPointer(mutating: apiTypePtr)), - mpv_render_param(type: MPV_RENDER_PARAM_INVALID, data: nil) - ] - - return params.withUnsafeMutableBufferPointer { pointer -> Int32 in - pointer.baseAddress?.withMemoryRebound(to: mpv_render_param.self, capacity: pointer.count) { parameters in - return mpv_render_context_create(&renderContext, handle, parameters) - } ?? -1 - } - } - - guard status >= 0, renderContext != nil else { - throw RendererError.renderContextCreation(status) - } - - mpv_render_context_set_update_callback(renderContext, { context in - guard let context = context else { return } - let instance = Unmanaged.fromOpaque(context).takeUnretainedValue() - instance.scheduleRender() - }, Unmanaged.passUnretained(self).toOpaque()) - } - private func observeProperties() { guard let handle = mpv else { return } let properties: [(String, mpv_format)] = [ - ("dwidth", MPV_FORMAT_INT64), - ("dheight", MPV_FORMAT_INT64), ("duration", MPV_FORMAT_DOUBLE), ("time-pos", MPV_FORMAT_DOUBLE), ("pause", MPV_FORMAT_FLAG), - ("track-list/count", MPV_FORMAT_INT64) // Notify when tracks are available + ("track-list/count", MPV_FORMAT_INT64), + ("paused-for-cache", MPV_FORMAT_FLAG) ] for (name, format) in properties { - _ = name.withCString { pointer in - mpv_observe_property(handle, 0, pointer, format) - } - } - } - - private func installWakeupHandler() { - guard let handle = mpv else { return } - mpv_set_wakeup_callback(handle, { userdata in - guard let userdata else { return } - let instance = Unmanaged.fromOpaque(userdata).takeUnretainedValue() - instance.processEvents() - }, Unmanaged.passUnretained(self).toOpaque()) - renderQueue.async { [weak self] in - guard let self else { return } - self.disposeBag.append { [weak self] in - guard let self, let handle = self.mpv else { return } - mpv_set_wakeup_callback(handle, nil, nil) - } - } - } - - private func scheduleRender() { - renderQueue.async { [weak self] in - guard let self, self.isRunning, !self.isStopping else { return } - - let currentTime = CACurrentMediaTime() - let timeSinceLastRender = currentTime - self.lastRenderTime - if timeSinceLastRender < self.minRenderInterval { - let remaining = self.minRenderInterval - timeSinceLastRender - if self.isRenderScheduled { return } - self.isRenderScheduled = true - - self.renderQueue.asyncAfter(deadline: .now() + remaining) { [weak self] in - guard let self else { return } - self.lastRenderTime = CACurrentMediaTime() - self.performRenderUpdate() - self.isRenderScheduled = false - } - return - } - - self.isRenderScheduled = true - self.lastRenderTime = currentTime - self.performRenderUpdate() - self.isRenderScheduled = false - } - } - - private func performRenderUpdate() { - guard let context = renderContext else { return } - let status = mpv_render_context_update(context) - - let updateFlags = UInt32(status) - - if updateFlags & MPV_RENDER_UPDATE_FRAME.rawValue != 0 { - renderFrame() - } - - if status > 0 { - scheduleRender() - } - } - - private func renderFrame() { - guard let context = renderContext else { return } - let videoSize = currentVideoSize() - guard videoSize.width > 0, videoSize.height > 0 else { return } - - let targetSize = targetRenderSize(for: videoSize) - let width = Int(targetSize.width) - let height = Int(targetSize.height) - guard width > 0, height > 0 else { return } - if lastRenderDimensions != targetSize { - lastRenderDimensions = targetSize - if targetSize != videoSize { - Logger.shared.log("Rendering scaled output at \(width)x\(height) (source \(Int(videoSize.width))x\(Int(videoSize.height)))", type: "Info") - } else { - Logger.shared.log("Rendering output at native size \(width)x\(height)", type: "Info") - } - } - - if poolWidth != width || poolHeight != height { - recreatePixelBufferPool(width: width, height: height) - } - - var pixelBuffer: CVPixelBuffer? - var status: CVReturn = kCVReturnError - - if !preAllocatedBuffers.isEmpty { - pixelBuffer = preAllocatedBuffers.removeFirst() - status = kCVReturnSuccess - } else if let pool = pixelBufferPool { - status = CVPixelBufferPoolCreatePixelBufferWithAuxAttributes(kCFAllocatorDefault, pool, pixelBufferPoolAuxAttributes, &pixelBuffer) - } - - if status != kCVReturnSuccess || pixelBuffer == nil { - let attrs: [CFString: Any] = [ - kCVPixelBufferIOSurfacePropertiesKey: [:] as CFDictionary, - kCVPixelBufferCGImageCompatibilityKey: kCFBooleanTrue!, - kCVPixelBufferCGBitmapContextCompatibilityKey: kCFBooleanTrue!, - kCVPixelBufferMetalCompatibilityKey: kCFBooleanTrue!, - kCVPixelBufferWidthKey: width, - kCVPixelBufferHeightKey: height, - kCVPixelBufferPixelFormatTypeKey: kCVPixelFormatType_32BGRA - ] - status = CVPixelBufferCreate(kCFAllocatorDefault, width, height, kCVPixelFormatType_32BGRA, attrs as CFDictionary, &pixelBuffer) - } - - guard status == kCVReturnSuccess, let buffer = pixelBuffer else { - Logger.shared.log("Failed to create pixel buffer for rendering (status: \(status))", type: "Error") - return - } - - let actualFormat = CVPixelBufferGetPixelFormatType(buffer) - if actualFormat != kCVPixelFormatType_32BGRA { - Logger.shared.log("Pixel buffer format mismatch: expected BGRA (0x42475241), got \(actualFormat)", type: "Error") - } - - CVPixelBufferLockBaseAddress(buffer, []) - guard let baseAddress = CVPixelBufferGetBaseAddress(buffer) else { - CVPixelBufferUnlockBaseAddress(buffer, []) - return - } - - if shouldClearPixelBuffer { - let bufferDataSize = CVPixelBufferGetDataSize(buffer) - memset(baseAddress, 0, bufferDataSize) - shouldClearPixelBuffer = false - } - - dimensionsArray[0] = Int32(width) - dimensionsArray[1] = Int32(height) - let stride = Int32(CVPixelBufferGetBytesPerRow(buffer)) - let expectedMinStride = Int32(width * 4) - if stride < expectedMinStride { - Logger.shared.log("Unexpected pixel buffer stride \(stride) < expected \(expectedMinStride) — skipping render to avoid memory corruption", type: "Error") - CVPixelBufferUnlockBaseAddress(buffer, []) - return - } - - let pointerValue = baseAddress - dimensionsArray.withUnsafeMutableBufferPointer { dimsPointer in - bgraFormatCString.withUnsafeBufferPointer { formatPointer in - withUnsafePointer(to: stride) { stridePointer in - renderParams[0] = mpv_render_param(type: MPV_RENDER_PARAM_SW_SIZE, data: UnsafeMutableRawPointer(dimsPointer.baseAddress)) - renderParams[1] = mpv_render_param(type: MPV_RENDER_PARAM_SW_FORMAT, data: UnsafeMutableRawPointer(mutating: formatPointer.baseAddress)) - renderParams[2] = mpv_render_param(type: MPV_RENDER_PARAM_SW_STRIDE, data: UnsafeMutableRawPointer(mutating: stridePointer)) - renderParams[3] = mpv_render_param(type: MPV_RENDER_PARAM_SW_POINTER, data: pointerValue) - renderParams[4] = mpv_render_param(type: MPV_RENDER_PARAM_INVALID, data: nil) - - let rc = mpv_render_context_render(context, &renderParams) - if rc < 0 { - Logger.shared.log("mpv_render_context_render returned error \(rc)", type: "Error") - } - } - } - } - - CVPixelBufferUnlockBaseAddress(buffer, []) - - enqueue(buffer: buffer) - - if preAllocatedBuffers.count < 4 { - renderQueue.async { [weak self] in - self?.preAllocateBuffers() - } - } - } - - private func targetRenderSize(for videoSize: CGSize) -> CGSize { - guard videoSize.width > 0, videoSize.height > 0 else { return videoSize } - guard - let screen = UIApplication.shared.connectedScenes - .compactMap({ ($0 as? UIWindowScene)?.screen }) - .first - else { - fatalError("⚠️ No active screen found — app may not have a visible window yet.") - } - var scale = screen.scale - if scale <= 0 { scale = 1 } - let maxWidth = max(screen.bounds.width * scale, 1.0) - let maxHeight = max(screen.bounds.height * scale, 1.0) - if maxWidth <= 0 || maxHeight <= 0 { - return videoSize - } - let widthRatio = videoSize.width / maxWidth - let heightRatio = videoSize.height / maxHeight - let ratio = max(widthRatio, heightRatio, 1) - let targetWidth = max(1, Int(videoSize.width / ratio)) - let targetHeight = max(1, Int(videoSize.height / ratio)) - return CGSize(width: CGFloat(targetWidth), height: CGFloat(targetHeight)) - } - - private func createPixelBufferPool(width: Int, height: Int) { - guard width > 0, height > 0 else { return } - - let pixelFormat = kCVPixelFormatType_32BGRA - - let attrs: [CFString: Any] = [ - kCVPixelBufferPixelFormatTypeKey: pixelFormat, - kCVPixelBufferWidthKey: width, - kCVPixelBufferHeightKey: height, - kCVPixelBufferIOSurfacePropertiesKey: [:] as CFDictionary, - kCVPixelBufferMetalCompatibilityKey: kCFBooleanTrue!, - kCVPixelBufferCGImageCompatibilityKey: kCFBooleanTrue!, - kCVPixelBufferCGBitmapContextCompatibilityKey: kCFBooleanTrue! - ] - - let poolAttrs: [CFString: Any] = [ - kCVPixelBufferPoolMinimumBufferCountKey: maxPreAllocatedBuffers, - kCVPixelBufferPoolMaximumBufferAgeKey: 0 - ] - - let auxAttrs: [CFString: Any] = [ - kCVPixelBufferPoolAllocationThresholdKey: 8 - ] - - var pool: CVPixelBufferPool? - let status = CVPixelBufferPoolCreate(kCFAllocatorDefault, poolAttrs as CFDictionary, attrs as CFDictionary, &pool) - if status == kCVReturnSuccess, let pool { - renderQueueSync { - self.pixelBufferPool = pool - self.pixelBufferPoolAuxAttributes = auxAttrs as CFDictionary - self.poolWidth = width - self.poolHeight = height - } - - renderQueue.async { [weak self] in - self?.preAllocateBuffers() - } - } else { - Logger.shared.log("Failed to create CVPixelBufferPool (status: \(status))", type: "Error") - } - } - - private func recreatePixelBufferPool(width: Int, height: Int) { - renderQueueSync { - self.preAllocatedBuffers.removeAll() - self.pixelBufferPool = nil - self.formatDescription = nil - self.poolWidth = 0 - self.poolHeight = 0 - } - - createPixelBufferPool(width: width, height: height) - } - - private func preAllocateBuffers() { - guard DispatchQueue.getSpecific(key: renderQueueKey) != nil else { - renderQueue.async { [weak self] in - self?.preAllocateBuffers() - } - return - } - - guard let pool = pixelBufferPool else { return } - - let targetCount = min(maxPreAllocatedBuffers, 8) - let currentCount = preAllocatedBuffers.count - - guard currentCount < targetCount else { return } - - let bufferCount = targetCount - currentCount - - for _ in 0.. Bool { - var didChange = false - let width = Int32(CVPixelBufferGetWidth(buffer)) - let height = Int32(CVPixelBufferGetHeight(buffer)) - let pixelFormat = CVPixelBufferGetPixelFormatType(buffer) - - renderQueueSync { - var needsRecreate = false - - if let description = formatDescription { - let currentDimensions = CMVideoFormatDescriptionGetDimensions(description) - let currentPixelFormat = CMFormatDescriptionGetMediaSubType(description) - - if currentDimensions.width != width || - currentDimensions.height != height || - currentPixelFormat != pixelFormat { - needsRecreate = true - } - } else { - needsRecreate = true - } - - if needsRecreate { - var newDescription: CMVideoFormatDescription? - - let status = CMVideoFormatDescriptionCreateForImageBuffer( - allocator: kCFAllocatorDefault, - imageBuffer: buffer, - formatDescriptionOut: &newDescription - ) - - if status == noErr, let newDescription = newDescription { - formatDescription = newDescription - didChange = true - Logger.shared.log("Created new format description: \(width)x\(height), format: \(pixelFormat)", type: "Info") - } else { - Logger.shared.log("Failed to create format description (status: \(status))", type: "Error") - } - } - } - return didChange - } - - private func renderQueueSync(_ block: () -> Void) { - if DispatchQueue.getSpecific(key: renderQueueKey) != nil { - block() - } else { - renderQueue.sync(execute: block) - } - } - - private func currentVideoSize() -> CGSize { - stateQueue.sync { - videoSize - } - } - - private func updateVideoSize(width: Int, height: Int) { - let size = CGSize(width: max(width, 0), height: max(height, 0)) - stateQueue.async(flags: .barrier) { - self.videoSize = size - } - renderQueue.async { [weak self] in - guard let self else { return } - - if self.poolWidth != width || self.poolHeight != height { - self.recreatePixelBufferPool(width: max(width, 0), height: max(height, 0)) - } + mpv_observe_property(handle, 0, name, format) } } @@ -903,7 +296,6 @@ final class MPVSoftwareRenderer { } } - /// Async command - returns immediately, mpv processes later private func command(_ handle: OpaquePointer, _ args: [String]) { guard !args.isEmpty else { return } _ = withCStringArray(args) { pointer in @@ -911,7 +303,6 @@ final class MPVSoftwareRenderer { } } - /// Sync command - waits for mpv to process before returning private func commandSync(_ handle: OpaquePointer, _ args: [String]) -> Int32 { guard !args.isEmpty else { return -1 } return withCStringArray(args) { pointer in @@ -919,17 +310,23 @@ final class MPVSoftwareRenderer { } } + private func checkError(_ status: CInt) { + if status < 0 { + Logger.shared.log("MPV API error: \(String(cString: mpv_error_string(status)))", type: "Error") + } + } + + // MARK: - Event Handling + private func processEvents() { - eventQueueGroup.enter() - let group = eventQueueGroup - eventQueue.async { [weak self] in - defer { group.leave() } + queue.async { [weak self] in guard let self else { return } - while !self.isStopping { - guard let handle = self.mpv else { return } - guard let eventPointer = mpv_wait_event(handle, 0) else { return } + + while self.mpv != nil && !self.isStopping { + guard let handle = self.mpv, + let eventPointer = mpv_wait_event(handle, 0) else { return } let event = eventPointer.pointee - if event.event_id == MPV_EVENT_NONE { continue } + if event.event_id == MPV_EVENT_NONE { break } self.handleEvent(event) if event.event_id == MPV_EVENT_SHUTDOWN { break } } @@ -938,8 +335,6 @@ final class MPVSoftwareRenderer { private func handleEvent(_ event: mpv_event) { switch event.event_id { - case MPV_EVENT_VIDEO_RECONFIG: - refreshVideoState() case MPV_EVENT_FILE_LOADED: // Add external subtitles now that the file is loaded let hadExternalSubs = !pendingExternalSubtitles.isEmpty @@ -949,7 +344,7 @@ final class MPVSoftwareRenderer { } pendingExternalSubtitles = [] - // Set subtitle after external subs are added (track IDs have changed) + // Set subtitle after external subs are added if let subId = initialSubtitleId { setSubtitleTrack(subId) } else { @@ -964,13 +359,35 @@ final class MPVSoftwareRenderer { self.delegate?.renderer(self, didBecomeReadyToSeek: true) } } + + // Notify loading ended + if isLoading { + isLoading = false + DispatchQueue.main.async { [weak self] in + guard let self else { return } + self.delegate?.renderer(self, didChangeLoading: false) + } + } + + case MPV_EVENT_PLAYBACK_RESTART: + // Video playback has started/restarted + if isLoading { + isLoading = false + DispatchQueue.main.async { [weak self] in + guard let self else { return } + self.delegate?.renderer(self, didChangeLoading: false) + } + } + case MPV_EVENT_PROPERTY_CHANGE: if let property = event.data?.assumingMemoryBound(to: mpv_event_property.self).pointee.name { let name = String(cString: property) - refreshProperty(named: name) + refreshProperty(named: name, event: event) } + case MPV_EVENT_SHUTDOWN: Logger.shared.log("mpv shutdown", type: "Warn") + case MPV_EVENT_LOG_MESSAGE: if let logMessagePointer = event.data?.assumingMemoryBound(to: mpv_event_log_message.self) { let component = String(cString: logMessagePointer.pointee.prefix) @@ -978,44 +395,42 @@ final class MPVSoftwareRenderer { let lower = text.lowercased() if lower.contains("error") { Logger.shared.log("mpv[\(component)] \(text)", type: "Error") - } else if lower.contains("warn") || lower.contains("warning") || lower.contains("deprecated") { + } else if lower.contains("warn") || lower.contains("warning") { Logger.shared.log("mpv[\(component)] \(text)", type: "Warn") } } + default: break } } - private func refreshVideoState() { - guard let handle = mpv else { return } - var width: Int64 = 0 - var height: Int64 = 0 - getProperty(handle: handle, name: "dwidth", format: MPV_FORMAT_INT64, value: &width) - getProperty(handle: handle, name: "dheight", format: MPV_FORMAT_INT64, value: &height) - updateVideoSize(width: Int(width), height: Int(height)) - } - - private func refreshProperty(named name: String) { + private func refreshProperty(named name: String, event: mpv_event) { guard let handle = mpv else { return } + switch name { case "duration": var value = Double(0) let status = getProperty(handle: handle, name: name, format: MPV_FORMAT_DOUBLE, value: &value) if status >= 0 { cachedDuration = value - delegate?.renderer(self, didUpdatePosition: cachedPosition, duration: cachedDuration) + DispatchQueue.main.async { [weak self] in + guard let self else { return } + self.delegate?.renderer(self, didUpdatePosition: self.cachedPosition, duration: self.cachedDuration) + } } + case "time-pos": - // Skip updates while seeking to prevent race condition - guard !isSeeking else { return } var value = Double(0) let status = getProperty(handle: handle, name: name, format: MPV_FORMAT_DOUBLE, value: &value) if status >= 0 { cachedPosition = value - positionUpdateTime = CACurrentMediaTime() // Record when we got this update - delegate?.renderer(self, didUpdatePosition: cachedPosition, duration: cachedDuration) + DispatchQueue.main.async { [weak self] in + guard let self else { return } + self.delegate?.renderer(self, didUpdatePosition: self.cachedPosition, duration: self.cachedDuration) + } } + case "pause": var flag: Int32 = 0 let status = getProperty(handle: handle, name: name, format: MPV_FORMAT_FLAG, value: &flag) @@ -1023,26 +438,38 @@ final class MPVSoftwareRenderer { let newPaused = flag != 0 if newPaused != isPaused { isPaused = newPaused - // Update timebase rate - use playbackSpeed when playing, 0 when paused - let speed = self.playbackSpeed DispatchQueue.main.async { [weak self] in - if let timebase = self?.displayLayer.controlTimebase { - CMTimebaseSetRate(timebase, rate: newPaused ? 0 : speed) - } + guard let self else { return } + self.delegate?.renderer(self, didChangePause: self.isPaused) } - delegate?.renderer(self, didChangePause: isPaused) } } + + case "paused-for-cache": + var flag: Int32 = 0 + let status = getProperty(handle: handle, name: name, format: MPV_FORMAT_FLAG, value: &flag) + if status >= 0 { + let buffering = flag != 0 + if buffering != isLoading { + isLoading = buffering + DispatchQueue.main.async { [weak self] in + guard let self else { return } + self.delegate?.renderer(self, didChangeLoading: buffering) + } + } + } + case "track-list/count": var trackCount: Int64 = 0 let status = getProperty(handle: handle, name: name, format: MPV_FORMAT_INT64, value: &trackCount) if status >= 0 && trackCount > 0 { Logger.shared.log("Track list updated: \(trackCount) tracks available", type: "Info") DispatchQueue.main.async { [weak self] in - guard let self = self else { return } + guard let self else { return } self.delegate?.renderer(self, didBecomeTracksReady: true) } } + default: break } @@ -1050,21 +477,17 @@ final class MPVSoftwareRenderer { private func getStringProperty(handle: OpaquePointer, name: String) -> String? { var result: String? - name.withCString { pointer in - if let cString = mpv_get_property_string(handle, pointer) { - result = String(cString: cString) - mpv_free(cString) - } + if let cString = mpv_get_property_string(handle, name) { + result = String(cString: cString) + mpv_free(cString) } return result } @discardableResult private func getProperty(handle: OpaquePointer, name: String, format: mpv_format, value: inout T) -> Int32 { - return name.withCString { pointer in - return withUnsafeMutablePointer(to: &value) { mutablePointer in - return mpv_get_property(handle, pointer, format, mutablePointer) - } + return withUnsafeMutablePointer(to: &value) { mutablePointer in + return mpv_get_property(handle, name, format, mutablePointer) } } @@ -1090,6 +513,7 @@ final class MPVSoftwareRenderer { } // MARK: - Playback Controls + func play() { setProperty(name: "pause", value: "no") } @@ -1105,101 +529,25 @@ final class MPVSoftwareRenderer { func seek(to seconds: Double) { guard let handle = mpv else { return } let clamped = max(0, seconds) - let wasPaused = isPaused - // Prevent time-pos updates from overwriting during seek - isSeeking = true - // Update cached position BEFORE seek so new frames get correct timestamp cachedPosition = clamped - positionUpdateTime = CACurrentMediaTime() // Reset interpolation base - lastPTS = clamped // Reset monotonic PTS to new position - // Update timebase to match new position (sets rate to 1 for frame display) - syncTimebase(to: clamped) - // Sync seek for accurate positioning commandSync(handle, ["seek", String(clamped), "absolute"]) - isSeeking = false - // Restore paused rate after seek completes - if wasPaused { - restoreTimebaseRate() - } } func seek(by seconds: Double) { guard let handle = mpv else { return } - let wasPaused = isPaused - // Prevent time-pos updates from overwriting during seek - isSeeking = true - // Update cached position BEFORE seek let newPosition = max(0, cachedPosition + seconds) cachedPosition = newPosition - positionUpdateTime = CACurrentMediaTime() // Reset interpolation base - lastPTS = newPosition // Reset monotonic PTS to new position - // Update timebase to match new position (sets rate to 1 for frame display) - syncTimebase(to: newPosition) - // Sync seek for accurate positioning commandSync(handle, ["seek", String(seconds), "relative"]) - isSeeking = false - // Restore paused rate after seek completes - if wasPaused { - restoreTimebaseRate() - } } - private func restoreTimebaseRate() { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) { [weak self] in - guard let self = self, self.isPaused else { return } - if let timebase = self.displayLayer.controlTimebase { - CMTimebaseSetRate(timebase, rate: 0) - } - } - } - - private func syncTimebase(to position: Double) { - let speed = playbackSpeed - let doWork = { [weak self] in - guard let self = self else { return } - // Flush old frames to avoid "old frames with new clock" mismatches - if #available(iOS 17.0, *) { - self.displayLayer.sampleBufferRenderer.flush(removingDisplayedImage: false, completionHandler: nil) - } else { - self.displayLayer.flush() - } - if let timebase = self.displayLayer.controlTimebase { - // Update timebase to new position - CMTimebaseSetTime(timebase, time: CMTime(seconds: position, preferredTimescale: 1000)) - // Set rate to playback speed during seek to ensure frame displays - // restoreTimebaseRate() will set it back to 0 if paused - CMTimebaseSetRate(timebase, rate: speed) - } - } - - if Thread.isMainThread { - doWork() - } else { - DispatchQueue.main.sync { doWork() } - } - } - - /// Sync timebase with current position without flushing (for smooth PiP transitions) + /// Sync timebase - no-op for vo_avfoundation (mpv handles timing) func syncTimebase() { - DispatchQueue.main.async { [weak self] in - guard let self = self else { return } - if let timebase = self.displayLayer.controlTimebase { - CMTimebaseSetTime(timebase, time: CMTime(seconds: self.cachedPosition, preferredTimescale: 1000)) - CMTimebaseSetRate(timebase, rate: self.isPaused ? 0 : self.playbackSpeed) - } - } + // vo_avfoundation manages its own timebase } func setSpeed(_ speed: Double) { playbackSpeed = speed setProperty(name: "speed", value: String(speed)) - // Sync timebase rate with playback speed - DispatchQueue.main.async { [weak self] in - guard let self = self, - let timebase = self.displayLayer.controlTimebase else { return } - let rate = self.isPaused ? 0.0 : speed - CMTimebaseSetRate(timebase, rate: rate) - } } func getSpeed() -> Double { @@ -1212,9 +560,9 @@ final class MPVSoftwareRenderer { // MARK: - Subtitle Controls func getSubtitleTracks() -> [[String: Any]] { - guard let handle = mpv else { + guard let handle = mpv else { Logger.shared.log("getSubtitleTracks: mpv handle is nil", type: "Warn") - return [] + return [] } var tracks: [[String: Any]] = [] @@ -1222,12 +570,8 @@ final class MPVSoftwareRenderer { getProperty(handle: handle, name: "track-list/count", format: MPV_FORMAT_INT64, value: &trackCount) for i in 0.. [[String: Any]] { - guard let handle = mpv else { + guard let handle = mpv else { Logger.shared.log("getAudioTracks: mpv handle is nil", type: "Warn") - return [] + return [] } var tracks: [[String: Any]] = [] @@ -1327,12 +668,8 @@ final class MPVSoftwareRenderer { getProperty(handle: handle, name: "track-list/count", format: MPV_FORMAT_INT64, value: &trackCount) for i in 0.. 0.40.0' + s.dependency 'MPVKit-GPL' # Swift/Objective-C compatibility s.pod_target_xcconfig = { diff --git a/modules/mpv-player/ios/MpvPlayerView.swift b/modules/mpv-player/ios/MpvPlayerView.swift index a6348a7b..1f6fd456 100644 --- a/modules/mpv-player/ios/MpvPlayerView.swift +++ b/modules/mpv-player/ios/MpvPlayerView.swift @@ -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() + } } diff --git a/modules/mpv-player/ios/PiPController.swift b/modules/mpv-player/ios/PiPController.swift index 80680896..1d20899d 100644 --- a/modules/mpv-player/ios/PiPController.swift +++ b/modules/mpv-player/ios/PiPController.swift @@ -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 diff --git a/plugins/addSPMDependenciesToMainTarget/app.plugin.js b/plugins/addSPMDependenciesToMainTarget/app.plugin.js deleted file mode 100644 index 4c53a161..00000000 --- a/plugins/addSPMDependenciesToMainTarget/app.plugin.js +++ /dev/null @@ -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; diff --git a/plugins/addSPMDependenciesToMainTarget/package.json b/plugins/addSPMDependenciesToMainTarget/package.json deleted file mode 100644 index 3008e7e7..00000000 --- a/plugins/addSPMDependenciesToMainTarget/package.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "addSPMDependenciesToMainTarget", - "version": "1.0.0", - "main": "app.plugin.js" -} diff --git a/plugins/withGitPod.js b/plugins/withGitPod.js new file mode 100644 index 00000000..dc046e8a --- /dev/null +++ b/plugins/withGitPod.js @@ -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;