【iOS】自定义相机(九)录像进阶

在前面【iOS】自定义相机(六)拍照录像中,我们介绍了如何使用AVCaptureMovieFileOutput
进行视频的录制。AVCaptureMovieFileOutput
虽然十分方便,但是我们无法使用AVCaptureVideoDataOutput
获取帧图片进行处理。因此这一篇文章就介绍一下直接使用AVCaptureVideoDataOutput
和AVCaptureAudioDataOutput
进行视频的录制的姿势。
对应的代码在SCCamera中的SCMovieManager.m和SCCameraController.m中。
在AVCaptureVideoDataOutputSampleBufferDelegate
和AVCaptureAudioDataOutputSampleBufferDelegate
的captureOutput:didOutputSampleBuffer:fromConnection:
中我们可以获取到音视频数据CMSampleBufferRef
。通过使用AVAssetWriter
我们就能达到,将每一帧的视频和音频写入文件的目的。从而模拟出AVCaptureMovieFileOutput
的效果。
AVAssetWriter 使用
AVFoundation 中提供了直接读写视频媒体样本的类,分别为AVAssetReader与AVAssetWriter。AVAssetReader 用于将资源从 AVAsset 实例中读取媒体样本,AVAssetWriter 用于对媒体资源进行编码并将其写入到容器文件中。
AVAssetWriter
是一个用于写入资源的类,他由一个或多个AVAssetWriterInput
组成,用于添加要写入容器的特定的媒体样本的CMSampleBuffer
。AVAssetWriterInput
需要配置指定的媒体类型,比如音频或视频。对于自定义相机的视频录制,AVAssetWriter
的使用如下:

AVAssetWriter
的创建十分简单,只需要指定目标容器的NSURL
即可。重点需要注意的地方是AVAssetWriterInput
:
- 创建
AVAssetWriterInput
需要的outputSettings
- 音视频样本写入模式
outputSettings
outputSettings
是一个NSDictionary<NSString *, id>
,用键值对的方式表示音视频编码与压缩相关信息,例子如下:
NSDictionary *outSettings = @{
AVVideoCodecKey: AVVideoCodecH264,
AVVideoWidthKey: @1280,
AVVideoHeightKey: @720,
AVVideoCompressionPropertiesKey: @{
AVVideoMaxKeyFrameIntervalKey: @1,
AVVideoAverageBitRateKey: @105000000,
AVVideoProfileLevelKey: AVVideoProfileLevelH264Main31
}
}
逐个键值对进行设置可控性比较强,但容易写错。方便起见,我们可以使用AVCaptureVideoDataOutput
和AVCaptureAudioDataOutput
的recommendedVideoSettingsForAssetWriterWithOutputFileType:
方法。只需要传入我们需要导出的文件类型,即可获取一个当前系统可以使用的一个settings
字典。
音视频样本写入模式
我们都知道,在使用AVCaptureVideoOutput
和AVCaotureAudioOutput
进行视频录制中,音频数据和视频数据是分开获取的。因此我们在使用AVAssetWriter
进行写入视频的时候需要注意数据的写入顺序。为了使数据有较高效率的排列,存储设备能更方便有效地读取数据,并在播放和寻找资源时提高性能,我们在音视频样本获取的时候需要采用交错模式。
交错模式:音频数据和视频数据是逐个交错排列的。
在AVAssetWriter
中我们并不能直接设置写入模式,但是在AVAssetWriterInput
中的readyForMoreMediaData
属性可用于判断该input
是否可以进行数据拼接。只需input
在使用appendSampleBuffer:
写入数据之前进行一层判断,即可控制最后的音视频样本排列方式为交错模式。
录制视频步骤
为了方便编写,我将关于AVAssetWriter
的代码都放进了SCMovieManager
中,完整代码点这里。录制视频分为三个步骤,分别是:
- 录制开始
- 录制过程
- 录制结束
录制开始
录制开始需要做的步骤有以下三步:
- (void)startRecordWithVideoSettings:(NSDictionary *)videoSettings
audioSettings:(NSDictionary *)audioSettings
handle:(void (^ _Nullable)(NSError * _Nonnull))handle {
dispatch_async(self.movieQueue, ^{
// 第一步:创建 AVAssetWriter
// 第二步:创建视频输入
// 第三步:创建音频输入
self.recording = YES;
self.firstSample = YES;
});
}
PS: self.firstSample
用于控制录制第一帧必须是视频帧。
第一步:创建 AVAssetWriter
NSError *error;
self.movieWriter = [AVAssetWriter assetWriterWithURL:self.movieURL fileType:AVFileTypeQuickTimeMovie error:&error];
if (!self.movieWriter || error) {
NSLog(@"movieWriter error.");
return;
}
PS:self.movieURL
需要开发者自己管理好,最好在每次录制之前保证该路径下并没有文件。
第二步:创建视频输入
self.movieVideoInput = [[AVAssetWriterInput alloc] initWithMediaType:AVMediaTypeVideo outputSettings:videoSettings];
// 针对实时性进行优化
self.movieVideoInput.expectsMediaDataInRealTime = YES;
if ([self.movieWriter canAddInput:self.movieVideoInput]) {
[self.movieWriter addInput:self.movieVideoInput];
} else {
NSLog(@"Unable to add video input.");
}
PS:如果应用程序只支持一个视频方向就需要根据当前设备方向做图像旋转转换,并设置self.movieVideoInput.transform
属性。
第三步:创建音频输入
self.movieAudioInput = [[AVAssetWriterInput alloc] initWithMediaType:AVMediaTypeAudio outputSettings:audioSettings];
// 针对实时性进行优化
self.movieAudioInput.expectsMediaDataInRealTime = YES;
if ([self.movieWriter canAddInput:self.movieAudioInput]) {
[self.movieWriter addInput:self.movieAudioInput];
} else {
NSLog(@"Unable to add audio input.");
}
录制过程
使用AVCaptureVideoDataOutput
和AVCaptureAudioDataOutput
进行获取数据的时候,我们都需要实现下面的代理方法并进行样本数据写入:
- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection{
if (self.movieManager.isRecording) {
[self.movieManager recordSampleBuffer:sampleBuffer];
}
}
recordSampleBuffer:
方法是录制的核心方法,主要是根据mediaType
区分音频和视频数据进行相应的写入。
- (void)recordSampleBuffer:(CMSampleBufferRef)sampleBuffer {
if (!self.isRecording) {
return;
}
CMFormatDescriptionRef formatDesc = CMSampleBufferGetFormatDescription(sampleBuffer);
CMMediaType mediaType = CMFormatDescriptionGetMediaType(formatDesc);
if (mediaType == kCMMediaType_Video) {
// 视频数据处理
CMTime timestamp = CMSampleBufferGetPresentationTimeStamp(sampleBuffer);
if (self.isFirstSample) {
if ([self.movieWriter startWriting]) {
[self.movieWriter startSessionAtSourceTime: timestamp];
} else {
NSLog(@"Failed to start writing.");
}
self.firstSample = NO;
}
if (self.movieVideoInput.readyForMoreMediaData) {
if (![self.movieVideoInput appendSampleBuffer:sampleBuffer]) {
NSLog(@"Error appending video sample buffer.");
}
}
} else if (!self.firstSample && mediaType == kCMMediaType_Audio) {
// 音频数据处理(已处理至少一个视频数据)
if (self.movieAudioInput.readyForMoreMediaData) {
if (![self.movieAudioInput appendSampleBuffer:sampleBuffer]) {
NSLog(@"Error appending audio sample buffer.");
}
}
}
}
录制结束
录制结束比较简单,主要是就是调用self.movieWriter
的finishWritingWithCompletionHandler:
方式,并根据self.movieWriter
的状态进行后续的处理。
- (void)stopRecordWithCompletion:(void (^)(BOOL, NSURL * _Nullable))completion {
self.recording = NO;
dispatch_async(self.movieQueue, ^{
[self.movieWriter finishWritingWithCompletionHandler:^{
switch (self.movieWriter.status) {
case AVAssetWriterStatusCompleted:{
self.firstSample = YES;
NSURL *fileURL = [self.movieWriter outputURL];
completion(YES, fileURL);
// FIXME: - 测试用保存
[self saveMovieToCameraRoll:fileURL authHandle:^(BOOL success, PHAuthorizationStatus status) {
NSLog(@"相册添加权限:%d, %ld", success, (long)status);
} completion:^(BOOL success, NSError * _Nullable error) {
NSLog(@"视频添加结果:%d, %@", success, error);
}];
break;
}
default:
NSLog(@"Failed to write movie: %@", self.movieWriter.error);
break;
}
}];
});
}
最后在fileURL
中的视频文件就是我们录制的成果了。
由于录制的全过程并不需要在主线程中进行操作,因此,我们都需要在
self.movieQueue
这个串行队列中异步执行。