fix: move video capture off main thread + filter stale end-of-playback notifications
Two fixes for video freeze during recording: 1. Video capture loop (30fps) moved from main-thread Timer to a dedicated DispatchSourceTimer on 'miniplayer.videoCapture' queue. copyPixelBuffer() on the main thread was competing with AVPlayer's rendering pipeline for CPU time, causing frame stalls. 2. AVPlayerItemDidPlayToEndTime observer now checks notification.object against player.currentItem. Previously object:nil caught stale notifications from replaced items, causing spurious playNext() calls that interrupted playback (visible as duplicate playIndex in logs).
This commit is contained in:
parent
9160b3ce2b
commit
f68476d57f
@ -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() }
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user