fix: 音视频同步——音频从 firstFrameTime 开始读取
之前音频用 player.currentTime() 作为起始时间,视频用 AVPlayerItemVideoOutput 的 firstFrameTime,两者可能不一致 导致音视频不同步。 改为: - prepareAudioCapture: 只预加载音频轨道,不立即开始读取 - captureVideoFrame: 第一帧时设置 firstFrameTime 后调用 beginAudioReading(startTime: firstFrameTime) - 音频和视频共用同一个时间基准,确保精确同步 - 移除 startRecording 的 startTime 参数
This commit is contained in:
parent
60a3369d82
commit
42f3cf2b84
@ -122,8 +122,7 @@ final class PlayerBridge: ObservableObject {
|
|||||||
}
|
}
|
||||||
guard !queue.isEmpty else { return }
|
guard !queue.isEmpty else { return }
|
||||||
let sourceURL = queue[currentIndex].url
|
let sourceURL = queue[currentIndex].url
|
||||||
let startTime = player.currentTime()
|
screenRecorder.startRecording(from: output, sourceURL: sourceURL)
|
||||||
screenRecorder.startRecording(from: output, sourceURL: sourceURL, startTime: startTime)
|
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,6 +22,10 @@ final class PlayerRecorder: NSObject {
|
|||||||
/// 音频线程读取 firstFrameTime 的镜像
|
/// 音频线程读取 firstFrameTime 的镜像
|
||||||
nonisolated(unsafe) private var audioFirstFrameTime: CMTime?
|
nonisolated(unsafe) private var audioFirstFrameTime: CMTime?
|
||||||
|
|
||||||
|
// 音频预加载状态
|
||||||
|
private var pendingAudioAsset: AVURLAsset?
|
||||||
|
private var pendingAudioTrack: AVAssetTrack?
|
||||||
|
|
||||||
@Published var isRecording = false
|
@Published var isRecording = false
|
||||||
@Published var durationText = "00:00"
|
@Published var durationText = "00:00"
|
||||||
|
|
||||||
@ -38,7 +42,7 @@ final class PlayerRecorder: NSObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 开始录制
|
/// 开始录制
|
||||||
func startRecording(from output: AVPlayerItemVideoOutput, sourceURL: URL, startTime: CMTime) {
|
func startRecording(from output: AVPlayerItemVideoOutput, sourceURL: URL) {
|
||||||
guard !isRecording else { return }
|
guard !isRecording else { return }
|
||||||
|
|
||||||
weakOutput = output
|
weakOutput = output
|
||||||
@ -97,8 +101,8 @@ final class PlayerRecorder: NSObject {
|
|||||||
startTimer()
|
startTimer()
|
||||||
startCaptureLoop()
|
startCaptureLoop()
|
||||||
|
|
||||||
// 启动音频读取(从源 URL)
|
// 准备音频读取(等待 firstFrameTime 后才真正开始)
|
||||||
startAudioCapture(sourceURL: sourceURL, startTime: startTime, audioWriterInput: aInput)
|
prepareAudioCapture(sourceURL: sourceURL)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 停止录制
|
/// 停止录制
|
||||||
@ -151,6 +155,8 @@ final class PlayerRecorder: NSObject {
|
|||||||
audioFirstFrameTime = itemTime // 同步给音频线程
|
audioFirstFrameTime = itemTime // 同步给音频线程
|
||||||
writer?.startSession(atSourceTime: itemTime)
|
writer?.startSession(atSourceTime: itemTime)
|
||||||
isRunning = true
|
isRunning = true
|
||||||
|
// 音频预加载完成后,用 firstFrameTime 作为起始时间开始读取
|
||||||
|
beginAudioReading(startTime: itemTime)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 相对时间戳
|
// 相对时间戳
|
||||||
@ -182,8 +188,12 @@ final class PlayerRecorder: NSObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - 音频读取(从源 URL)
|
// MARK: - 音频读取(从源 URL)
|
||||||
private func startAudioCapture(sourceURL: URL, startTime: CMTime, audioWriterInput: AVAssetWriterInput) {
|
|
||||||
|
/// 预加载音频轨道(异步),不立即开始读取
|
||||||
|
private func prepareAudioCapture(sourceURL: URL) {
|
||||||
let asset = AVURLAsset(url: sourceURL)
|
let asset = AVURLAsset(url: sourceURL)
|
||||||
|
pendingAudioAsset = asset
|
||||||
|
pendingAudioTrack = nil
|
||||||
|
|
||||||
Task {
|
Task {
|
||||||
do {
|
do {
|
||||||
@ -192,43 +202,66 @@ final class PlayerRecorder: NSObject {
|
|||||||
print("[Recorder] No audio track in source")
|
print("[Recorder] No audio track in source")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
self.pendingAudioTrack = audioTrack
|
||||||
let reader = try AVAssetReader(asset: asset)
|
// 如果 firstFrameTime 已经设置好了,立即开始读取
|
||||||
let output = AVAssetReaderTrackOutput(track: audioTrack, outputSettings: [
|
if let fft = self.firstFrameTime {
|
||||||
AVFormatIDKey: kAudioFormatLinearPCM,
|
self.beginAudioReading(startTime: fft)
|
||||||
AVSampleRateKey: 44100,
|
|
||||||
AVNumberOfChannelsKey: 2,
|
|
||||||
AVLinearPCMBitDepthKey: 16,
|
|
||||||
AVLinearPCMIsFloatKey: false,
|
|
||||||
AVLinearPCMIsBigEndianKey: false,
|
|
||||||
AVLinearPCMIsNonInterleaved: false
|
|
||||||
])
|
|
||||||
output.alwaysCopiesSampleData = false
|
|
||||||
|
|
||||||
// 从播放位置开始读取
|
|
||||||
reader.timeRange = CMTimeRange(start: startTime, duration: .positiveInfinity)
|
|
||||||
|
|
||||||
if reader.canAdd(output) {
|
|
||||||
reader.add(output)
|
|
||||||
reader.startReading()
|
|
||||||
|
|
||||||
self.audioReader = reader
|
|
||||||
self.audioReaderOutput = output
|
|
||||||
|
|
||||||
// 后台线程读取音频
|
|
||||||
let queue = DispatchQueue(label: "recorder.audio.capture")
|
|
||||||
self.audioCaptureQueue = queue
|
|
||||||
queue.async { [weak self] in
|
|
||||||
guard let self = self, let aInput = self.audioInput else { return }
|
|
||||||
self.readAudioLoop(reader: reader, output: output, audioInput: aInput)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
print("[Recorder] Audio capture setup failed: \(error)")
|
print("[Recorder] Audio track load failed: \(error)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 用 firstFrameTime 作为起始时间,创建 reader 并开始后台读取
|
||||||
|
private func beginAudioReading(startTime: CMTime) {
|
||||||
|
guard let asset = pendingAudioAsset, let audioTrack = pendingAudioTrack else {
|
||||||
|
return // 轨道还没加载好,prepareAudioCapture 的回调会再次调用
|
||||||
|
}
|
||||||
|
guard let aInput = audioInput else { return }
|
||||||
|
// 防止重复启动
|
||||||
|
guard audioReader == nil else { return }
|
||||||
|
|
||||||
|
do {
|
||||||
|
let reader = try AVAssetReader(asset: asset)
|
||||||
|
let output = AVAssetReaderTrackOutput(track: audioTrack, outputSettings: [
|
||||||
|
AVFormatIDKey: kAudioFormatLinearPCM,
|
||||||
|
AVSampleRateKey: 44100,
|
||||||
|
AVNumberOfChannelsKey: 2,
|
||||||
|
AVLinearPCMBitDepthKey: 16,
|
||||||
|
AVLinearPCMIsFloatKey: false,
|
||||||
|
AVLinearPCMIsBigEndianKey: false,
|
||||||
|
AVLinearPCMIsNonInterleaved: false
|
||||||
|
])
|
||||||
|
output.alwaysCopiesSampleData = false
|
||||||
|
|
||||||
|
// 从视频第一帧时间开始读取,确保音视频同步
|
||||||
|
reader.timeRange = CMTimeRange(start: startTime, duration: .positiveInfinity)
|
||||||
|
|
||||||
|
if reader.canAdd(output) {
|
||||||
|
reader.add(output)
|
||||||
|
reader.startReading()
|
||||||
|
|
||||||
|
self.audioReader = reader
|
||||||
|
self.audioReaderOutput = output
|
||||||
|
|
||||||
|
// 后台线程读取音频
|
||||||
|
let queue = DispatchQueue(label: "recorder.audio.capture")
|
||||||
|
self.audioCaptureQueue = queue
|
||||||
|
queue.async { [weak self] in
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.readAudioLoop(reader: reader, output: output, audioInput: aInput)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
print("[Recorder] Audio capture setup failed: \(error)")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理预加载状态
|
||||||
|
pendingAudioAsset = nil
|
||||||
|
pendingAudioTrack = nil
|
||||||
|
}
|
||||||
|
|
||||||
private nonisolated func readAudioLoop(reader: AVAssetReader, output: AVAssetReaderTrackOutput, audioInput: AVAssetWriterInput) {
|
private nonisolated func readAudioLoop(reader: AVAssetReader, output: AVAssetReaderTrackOutput, audioInput: AVAssetWriterInput) {
|
||||||
while !audioStopped && reader.status == .reading {
|
while !audioStopped && reader.status == .reading {
|
||||||
guard let sampleBuffer = output.copyNextSampleBuffer() else {
|
guard let sampleBuffer = output.copyNextSampleBuffer() else {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user