diff --git a/Sources/PlayerBridge.swift b/Sources/PlayerBridge.swift index 180982a..842a239 100644 --- a/Sources/PlayerBridge.swift +++ b/Sources/PlayerBridge.swift @@ -215,11 +215,13 @@ final class PlayerBridge: ObservableObject { // MARK: - 播放结束 private func setupEndObserver() { - // queue: .main → 回调已在主线程,直接调用,不创建 Task endObserverToken = NotificationCenter.default.addObserver( forName: .AVPlayerItemDidPlayToEndTime, object: nil, queue: .main - ) { [weak self] _ in + ) { [weak self] notification in guard let self = self else { return } + // 只处理当前 item 的结束通知,忽略旧 item 被替换时发出的残留通知 + guard let endedItem = notification.object as? AVPlayerItem, + endedItem === self.player.currentItem else { return } Task { @MainActor in self.onPlaybackEnded() } } } diff --git a/Sources/PlayerRecorder.swift b/Sources/PlayerRecorder.swift index 7733f04..fd29f13 100644 --- a/Sources/PlayerRecorder.swift +++ b/Sources/PlayerRecorder.swift @@ -284,16 +284,17 @@ struct AudioDiagStats { final class PlayerRecorder: NSObject { private var writer: AVAssetWriter? - private var videoInput: AVAssetWriterInput? + nonisolated(unsafe) private var videoInput: AVAssetWriterInput? private var audioInput: AVAssetWriterInput? - private var captureTimer: Timer? + private var captureTimer: DispatchSourceTimer? + private let captureQueue = DispatchQueue(label: "miniplayer.videoCapture", qos: .userInteractive) private var durationTimer: Timer? private var startDate: Date? - private weak var weakOutput: AVPlayerItemVideoOutput? + nonisolated(unsafe) private weak var weakOutput: AVPlayerItemVideoOutput? nonisolated(unsafe) private var isRunning = false - private var lastPixelBuffer: CVPixelBuffer? - private var captureFrameCount: Int = 0 + nonisolated(unsafe) private var lastPixelBuffer: CVPixelBuffer? + nonisolated(unsafe) private var captureFrameCount: Int = 0 // Audio Tap private var tapContext: AudioTapContext? @@ -556,7 +557,7 @@ final class PlayerRecorder: NSObject { isRecording = false isRunning = false RecorderState.isFinalizing = true - captureTimer?.invalidate() + captureTimer?.cancel() captureTimer = nil stopDurationTimer() @@ -667,17 +668,19 @@ final class PlayerRecorder: NSObject { lastPixelBuffer = nil } - // MARK: - 视频抓帧 (30fps Timer) + // MARK: - 视频抓帧 (30fps, 独立队列避免阻塞主线程渲染) private func startCaptureLoop() { - captureTimer = Timer.scheduledTimer(withTimeInterval: 1.0 / 30.0, repeats: true) { [weak self] _ in - Task { @MainActor in - self?.captureVideoFrame() - } + let timer = DispatchSource.makeTimerSource(queue: captureQueue) + timer.schedule(deadline: .now(), repeating: .milliseconds(33), leeway: .milliseconds(2)) + timer.setEventHandler { [weak self] in + self?.captureVideoFrame() } + timer.resume() + captureTimer = timer } - private func captureVideoFrame() { + nonisolated private func captureVideoFrame() { guard isRunning, let output = weakOutput, let input = videoInput, input.isReadyForMoreMediaData else { return } @@ -700,7 +703,7 @@ final class PlayerRecorder: NSObject { } } - private static func createSampleBuffer(from pixelBuffer: CVPixelBuffer, time: CMTime) -> CMSampleBuffer? { + nonisolated private static func createSampleBuffer(from pixelBuffer: CVPixelBuffer, time: CMTime) -> CMSampleBuffer? { var info = CMSampleTimingInfo() info.presentationTimeStamp = time info.duration = CMTime(value: 1, timescale: 30)