首先让我说我已经搜索了这个问题的答案很长时间并且已经阅读了许多 SO 帖子,但没有一个提供我需要的答案
我正在尝试使用多个 AVAssetWriters/AVAssetWriterInputs 录制多个视频片段,我将其实例化并立即排队。
我面临的问题是,虽然视频被正确录制,但当它们进入 AVQueuePlayer 以按顺序播放时,会随机发生其中一个视频将冻结但继续播放音频的情况。
准备 AVAssetWriters/VideoInputs/AudioInputs:
private func setupAssetWriters() {
guard assetWriterOne == nil else { return }
// This just returns a URL with ".mov" as the file suffix in the absoluteString.
outputFileURLOne = createRandomizedURL(withMediaFileType: .mov)
(assetWriterOne, videoAssetWriterInputOne, audioAssetWriterInputOne) = configureAssetWriter(withInput: outputFileURLOne)
guard assetWriterTwo == nil else { return }
outputFileURLTwo = createRandomizedURL(withMediaFileType: .mov)
(assetWriterTwo, videoAssetWriterInputTwo, audioAssetWriterInputTwo) = configureAssetWriter(withInput: outputFileURLTwo)
guard assetWriterThree == nil else { return }
outputFileURLThree = createRandomizedURL(withMediaFileType: .mov)
(assetWriterThree, videoAssetWriterInputThree, audioAssetWriterInputThree) = configureAssetWriter(withInput: outputFileURLThree)
guard assetWriterFour == nil else { return }
outputFileURLFour = createRandomizedURL(withMediaFileType: .mov)
(assetWriterFour, videoAssetWriterInputFour, audioAssetWriterInputFour) = configureAssetWriter(withInput: outputFileURLFour)
}
private func configureAssetWriter(withInput url: URL) -> (assetWriter: AVAssetWriter?, videoAssetWriterInput: AVAssetWriterInput?, audioAssetWriterInput: AVAssetWriterInput?){
do {
let recommendedVideoSettings: [String: Any]? = videoDataOutput?.recommendedVideoSettingsForAssetWriter(writingTo: AVFileType.mov)
let assetWriter = try VideoAssetWriter(url: url, fileType: AVFileType.mov)
guard
assetWriter.canApply(outputSettings: recommendedVideoSettings, forMediaType: AVMediaType.video),
assetWriter.canApply(outputSettings: audioSettings, forMediaType: AVMediaType.audio)
else { return (nil, nil, nil) }
let videoAssetWriterInput = AVAssetWriterInput(mediaType: AVMediaType.video, outputSettings: recommendedVideoSettings, sourceFormatHint: videoFormatDescription)
let audioAssetWriterInput = AVAssetWriterInput(mediaType: AVMediaType.audio, outputSettings: audioSettings)
guard
assetWriter.canAdd(videoAssetWriterInput),
assetWriter.canAdd(audioAssetWriterInput)
else { return (nil, nil, nil) }
assetWriter.add(videoAssetWriterInput)
assetWriter.add(audioAssetWriterInput)
videoAssetWriterInput.expectsMediaDataInRealTime = true
audioAssetWriterInput.expectsMediaDataInRealTime = true
return (assetWriter, videoAssetWriterInput, audioAssetWriterInput)
} catch let error {
print("error getting AVAssetWriter: \(error.localizedDescription)")
return (nil, nil, nil)
}
}
处理 CMSampleBuffers
现在 AVAssetWriters 及其输入已初始化,我按下一个记录按钮,该按钮更改 Bool 值,让AVCaptureVideoDataOutputSampleBufferDelegate
知道开始捕获 CMSampleBuffers:
func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
if isRecording {
processVideoSamples(withOutput: output, sampleBuffer: sampleBuffer)
}
}
private func processVideoSamples(withOutput output: AVCaptureOutput, sampleBuffer: CMSampleBuffer) {
switch currentVideoInProcess {
case 1:
processSamples(usingAssetWriter: assetWriterOne, videoAssetWriterInput: videoAssetWriterInputOne, audioAssetWriterInput: audioAssetWriterInputOne, withOutput: output, sampleBuffer: sampleBuffer)
break
case 2:
processSamples(usingAssetWriter: assetWriterTwo, videoAssetWriterInput: videoAssetWriterInputTwo, audioAssetWriterInput: audioAssetWriterInputTwo, withOutput: output, sampleBuffer: sampleBuffer)
break
case 3:
processSamples(usingAssetWriter: assetWriterThree, videoAssetWriterInput: videoAssetWriterInputThree, audioAssetWriterInput: audioAssetWriterInputThree, withOutput: output, sampleBuffer: sampleBuffer)
break
case 4:
processSamples(usingAssetWriter: assetWriterFour, videoAssetWriterInput: videoAssetWriterInputFour, audioAssetWriterInput: audioAssetWriterInputFour, withOutput: output, sampleBuffer: sampleBuffer)
break
default:
break
}
}
private func processSamples(usingAssetWriter assetWriter: VideoAssetWriter?, videoAssetWriterInput: AVAssetWriterInput?, audioAssetWriterInput: AVAssetWriterInput?, withOutput output: AVCaptureOutput, sampleBuffer: CMSampleBuffer) {
guard CMSampleBufferDataIsReady(sampleBuffer) else { return }
guard
assetWriter != nil,
videoAssetWriterInput != nil,
audioAssetWriterInput != nil
else { return }
if assetWriter?.status == .unknown {
if let _ = output as? AVCaptureVideoDataOutput {
print("\n STARTED RECORDING")
assetWriter?.startWriting()
closingTime = sampleTime
let startRecordingTime = CMSampleBufferGetPresentationTimeStamp(sampleBuffer)
assetWriter?.startSession(atSourceTime: startRecordingTime)
} else {
print("output type unknown")
return
}
}
if assetWriter?.status == .failed { return }
guard assetWriter?.status == .writing else { return }
if let _ = output as? AVCaptureVideoDataOutput {
if (videoAssetWriterInput?.isReadyForMoreMediaData)! {
videoAssetWriterInput?.append(sampleBuffer)
if assetWriter?.hasWrittenFirstVideoSample == false {
print("added 1st video frame")
assetWriter?.hasWrittenFirstVideoSample = true
}
} else {
print("video writer not ready")
}
} else if let _ = output as? AVCaptureAudioDataOutput {
if (audioAssetWriterInput?.isReadyForMoreMediaData)! && assetWriter?.hasWrittenFirstVideoSample == true {
audioAssetWriterInput?.append(sampleBuffer)
} else {
print("audio writer not ready OR video not written yet")
}
}
}
从每个 AVAssetWriter 获取视频片段
至于获取视频片段,我endRecording
以指定的时间间隔调用此函数,以便从每个 AVAssetWriter 的 outputURL 中获取视频:
private func endRecordingOfCurrentWriter(completion: @escaping (_ fileURL: URL?, _ error: Error?) -> ()) {
// currentVideoInProcess is just an Int to keep track of which AVAssetWriter is current processing CMSampleBuffers
switch currentVideoInProcess {
case 1:
endRecording(atOutputURL: outputFileURLOne, usingAssetWriter: assetWriterOne, videoAssetWriterInput: videoAssetWriterInputOne, audioAssetWriterInput: audioAssetWriterInputOne) { (fileURL, error) in
completion(fileURL, error)
}
break
case 2:
endRecording(atOutputURL: outputFileURLTwo, usingAssetWriter: assetWriterTwo, videoAssetWriterInput: videoAssetWriterInputTwo, audioAssetWriterInput: audioAssetWriterInputTwo) { (fileURL, error) in
completion(fileURL, error)
}
break
case 3:
endRecording(atOutputURL: outputFileURLThree, usingAssetWriter: assetWriterThree, videoAssetWriterInput: videoAssetWriterInputThree, audioAssetWriterInput: audioAssetWriterInputThree) { (fileURL, error) in
completion(fileURL, error)
}
break
case 4:
endRecording(atOutputURL: outputFileURLFour, usingAssetWriter: assetWriterFour, videoAssetWriterInput: videoAssetWriterInputFour, audioAssetWriterInput: audioAssetWriterInputFour) { (fileURL, error) in
completion(fileURL, error)
}
break
default: break
}
currentVideoInProcess += 1
}
private func endRecording(atOutputURL outputURL: URL, usingAssetWriter assetWriter: VideoAssetWriter?, videoAssetWriterInput: AVAssetWriterInput?, audioAssetWriterInput: AVAssetWriterInput?, completion: @escaping (_ fileURL: URL?, _ error: Error?) -> ()) {
if assetWriter?.status.rawValue == 1 {
videoAssetWriterInput?.markAsFinished()
audioAssetWriterInput?.markAsFinished()
}
assetWriter?.finishWriting() {
let status = assetWriter?.status
guard assetWriter?.error == nil else {
if let error = assetWriter?.error {
completion(nil, assetWriter?.error)
}
return
}
switch status {
case .completed?:
completion(assetWriter?.outputURL, nil)
case .failed?:
completion(nil, assetWriter?.error)
case .cancelled?:
completion(nil, assetWriter?.error)
default:
completion(nil, assetWriter?.error)
}
}
}
问题
为了重申这个问题,这个实现创建了四个视频,每个视频几乎没有问题,但是,当我尝试使用AVQueuePlayer
. 其中一个视频仅在开始时随机冻结,同时继续播放音频直到结束。我可以根据自己的喜好多次执行此记录并获得不同的结果。实际上,在某些情况下根本不会发生冻结并且可以毫无问题地播放所有视频。
但通常情况下,冻结以完全随机的间隔发生。这种冻结不会解冻。应用程序的其余部分仍然响应(我可以按一个按钮删除所有视频并让我从头开始重新录制)。但是再次执行记录会导致在某个其他随机时间间隔冻结。
我尝试将视频合并为单个视频并在照片应用程序中播放,随机冻结也发生在那里。但是,我也将视频单独下载到照片应用程序(四个短视频和一个长视频下载到照片),如果单独播放,每个视频都可以正常播放。