创作不易,喜欢的话请点赞收藏转载,您的支持是我更新的最大动力!!!
相机,除了拍照功能,视频录制也是一个是十分受欢迎的功能。下面,笔者将同大家一起学习下 AV Foundation 中视频录制相关技术。
关于视频的采集,在 AV Foundation 相机之拍照 这一节中,这里就不再展开了,建议读者先阅读完再进入本节。
采集音频
在手机上,声音的采集依赖手机的麦克风,麦克风使用涉及到用户隐私,因此,在获取麦克风前,需要先申请麦克风使用权限。
权限申请
申请麦克风使用权限的前提,需要在 Info.plist 中加入 NSMicrophoneUsageDescription key,并提供申请用途,如下所示:
<dict>
...
<key>NSMicrophoneUsageDescription</key>
<string>This app requires access to your camera to take photos and videos.</string>
...
</dict>
随后,开发者便可以开始申请麦克风使用权限,如下代码所示:
private func requestAudioPermission(completion: @escaping (Bool)->Void) {
let status = AVCaptureDevice.authorizationStatus(for: .audio)
switch status {
case .notDetermined:
AVCaptureDevice.requestAccess(for: .audio) { (ret) in
DispatchQueue.main.async {
completion(ret)
}
}
case .authorized:
completion(true)
default:
completion(false)
}
}
添加 Input 和 Output
申请麦克风权限成功后,开发者便可以获取麦克风 audioDevice。在相机的 AVCaptureSession 实例 session 中添加 input 和 output。相关代码如下所示:
- (void)addMicrophoneWith:(void (^)(void))completion
{
__weak typeof(self)weakSelf = self;
dispatch_async(_sessionQueue, ^{
__strong typeof(weakSelf)strongSelf = weakSelf;
if (strongSelf->_audioInput == nil) {
// 1.获取音频设备
AVCaptureDevice* audioDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio];
NSError *error = nil;
// 2.根据 audioDevice,创建 audioDeviceInput
AVCaptureDeviceInput* audioDeviceInput = [AVCaptureDeviceInput deviceInputWithDevice:audioDevice error:&error];
if (error) {
SYLog(TAG, "addMicrophoneWith deviceInputWithDevice error = %@", error.description);
}
// 3.调用 session 的 canAddInput 方法判断是否可以加入 audioDeviceInput
if ([strongSelf->_session canAddInput:audioDeviceInput]) {
// 4. 调用 session 的 addInput 方法,添加 audioDeviceInput
[strongSelf->_session addInput:audioDeviceInput];
strongSelf->_audioInput = audioDeviceInput;
SYLog(TAG, "configureAudioDeviceInput addAudioInput");
} else {
SYLog(TAG, "addMicrophoneWith can not addAudioInput");
}
}
if (strongSelf->_audioOutput == nil) {
// 5. 创建 audioOutput
strongSelf->_audioOutput = [[AVCaptureAudioDataOutput alloc] init];
// 6.设置 audioOutput 数据回调的队列和 delegate
[strongSelf->_audioOutput setSampleBufferDelegate:self queue:strongSelf->_cameraProcessQueue];
// 7.调用 session 的 canAddOutput 方法判断是否可以加入 audioOutput
if ([strongSelf->_session canAddOutput:strongSelf->_audioOutput]) {
// 8. 调用 session 的 addOutput 方法,添加 audioOutput
[strongSelf->_session addOutput:strongSelf->_audioOutput];
} else {
SYLog(TAG, "addMicrophoneWith addAudioOutput");
}
}
completion();
});
}
- 麦克风获取的方式同摄像头类似,只不过调用 deviceInputWithDevice:error: 方法时,传入的类型是 AVMediaTypeAudio;
- AVCaptureAudioDataOutput 是负责记录音频数据和提供音频内容访问,在指定的队列中,通过 AVCaptureAudioDataOutputSampleBufferDelegate 的 captureOutput:didOutputSampleBuffer:fromConnection: 方法回调;
视频音频写入
Apple 提供了 AVAssetWriter 用于将视频和音频数据写入到指定的媒体容器中(如 MP4),并保存到本地磁盘。
AVAssetWriter 创建
AVAssetWriter 的创建相对简单,如下代码所示:
AVMediaType mediaType = AVFileTypeMPEG4;
NSError *error;
// 1. 指定文件路径和存储容器属性,这里用的是 MP4
self->_writer = [[AVAssetWriter alloc] initWithURL:[self getURL] fileType:mediaType error:&error];
// 2.写入方式是否更适合网络播放
self->_writer.shouldOptimizeForNetworkUse = YES;
AVAssetWriter 是通过配置不同 AVAssetWriterInput 写入不同类型的媒体数据,其映射一个或多个 input。对于视频录制而言,开发者需要配置一个音频写入的 input 和视频写入的 input。
用于写入视频的 AVAssetWriterInput,具体的创建代码如下所示:
- (void)addVideoWriterInput
{
if (!_videoWriterInput) {
// 1.AVAssetWriterInput 需要指定类型,且配置好视频写入的配置参数
_videoWriterInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo outputSettings:[self fetchVideoSetting]];
// 2.expectsMediaDataInRealTime 为YES,让 readyForMoreMediaData
// 计算更准确,写入更加有限,适合视频录制场景
_videoWriterInput.expectsMediaDataInRealTime = YES;
}
// 3.调用 writer 的 canAddInput 是否支持 input
if ([_writer canAddInput:_videoWriterInput]) {
// 4.调用 writer 的 addInput 添加 input
[_writer addInput:_videoWriterInput];
} else {
SYLog(TAG, "addVideoWriterInput addInput failure");
}
}
- (NSDictionary<NSString *, id> *)fetchVideoSetting
{
NSMutableDictionary *compressDict = [NSMutableDictionary dictionaryWithDictionary: @{
AVVideoAverageBitRateKey: @(_config.bitrate*1024),
AVVideoMaxKeyFrameIntervalKey: @(_config.gop),
AVVideoExpectedSourceFrameRateKey: @30,
AVVideoAllowFrameReorderingKey: @(NO),
AVVideoProfileLevelKey: AVVideoProfileLevelH264HighAutoLevel,
}];
return @{
AVVideoCodecKey: AVVideoCodecTypeH264,
AVVideoCompressionPropertiesKey: compressDict,
AVVideoWidthKey: @(_config.size.width),
AVVideoHeightKey: @(_config.size.height),
};
}
- AVAssetWriterInput 需要指定类型为 AVMediaTypeVideo,才能支持写入视频资源;
- 创建 AVAssetWriterInput,还需要指定视频的编码参数 setting,一般而言,可以直接通过 AVCaptureVideoDataOutput 的 recommendedVideoSettingsForAssetWriterWithOutputFileType: 方法获取 setting,也可以根据实际情况使用自定义的方式:
- AVVideoCodecKey:视频的编码方式,AVVideoCodecTypeH264 表示用 H264 编码;
- AVVideoCompressionPropertiesKey:视频的编码属性设置
- AVVideoAverageBitRateKey:视频的编码码率,码率越大,精度越高,画面更加还原;
- AVVideoMaxKeyFrameIntervalKey:关键帧(I 帧)之间相邻的最大间隔数,间隔数越大,体积越小
- AVVideoExpectedSourceFrameRateKey:视频预期帧率;
- AVVideoWidthKey 和 AVVideoHeightKey:用于指定视频的分辨率。
- AVVideoCodecKey:视频的编码方式,AVVideoCodecTypeH264 表示用 H264 编码;
- expectsMediaDataInRealTime: 表示是否要针对实时数据进行定制处理,如果为 YES,则音频和视频数据采用交错方式排列,提高写入效率,如下图所示:
如果单纯为了将视频写入到文件,创建 AVAssetWriterInput 就足够了,但如果需要对视频内容进行修饰再写入到文件,则需要引入 AVAssetWriterInputPixelBufferAdaptor,用于写入修改过的 CVPixelBuffer。创建 adapter 代码如下所示:
- (void)configureAdaptor
{
if (!_adaptor) {
// 1. 创建 sourcePixelBufferAttributes,设置 AVAssetWriterInputPixelBufferAdaptor 的基础配置参数
NSDictionary *sourcePixelBufferAttributes = @{
(id)kCVPixelBufferPixelFormatTypeKey: @(kCVPixelFormatType_32BGRA),
(id)kCVPixelBufferWidthKey: @(_config.size.width),
(id)kCVPixelBufferHeightKey: @(_config.size.height),
};
// 2.创建 AVAssetWriterInputPixelBufferAdaptor,并且将其与前面创建的 videoWriterInput 关联
_adaptor = [AVAssetWriterInputPixelBufferAdaptor assetWriterInputPixelBufferAdaptorWithAssetWriterInput:_videoWriterInput sourcePixelBufferAttributes:sourcePixelBufferAttributes];
}
}
同样,想要将音频数据写入到文件中,也需要创建一个用于写入音频的 AVAssetWriterInput,代码如下所示:
- (void)addAudioWriterInput
{
if (!_audioWriterInput) {
// 1.AVAssetWriterInput 需要将媒体类型指定为 AVMediaTypeAudio,且配置好视频写入的配置参数
_audioWriterInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeAudio outputSettings:[self fetchAudioSetting]];
// 2.将 expectsMediaDataInRealTime 同样配置成 YES;
_audioWriterInput.expectsMediaDataInRealTime = YES;
}
// 3.调用 writer 的 canAddInput 方法判断是否可以加入 audioWriterInput
if ([_writer canAddInput:_audioWriterInput]) {
// 4.调用 writer 的 addInput 添加 audioWriterInput
[_writer addInput:_audioWriterInput];
} else {
SYLog(TAG, "addAudioWriterInput addInput failure");
}
}
- (NSDictionary<NSString *, id> *)fetchAudioSetting
{
AudioChannelLayout acl;
bzero(&acl, sizeof(acl));
acl.mChannelLayoutTag = kAudioChannelLayoutTag_Mono;
NSUInteger sampleRate = [AVAudioSession sharedInstance].sampleRate;
if (sampleRate < 44100) {
sampleRate = 44100;
} else if (sampleRate > 48000) {
sampleRate = 48000;
}
return @{
AVFormatIDKey: @(kAudioFormatMPEG4AAC),
AVNumberOfChannelsKey: @(1),
AVSampleRateKey: @(sampleRate),
AVChannelLayoutKey: [NSData dataWithBytes:&acl length:sizeof(acl)],
AVEncoderBitRateKey: @(64000),
};
}
- 同样,创建 AudioWriterInput,也需要指定编码参数,也可以从 AVCaptureAudioDataOutput 的 recommendedAudioSettingsForAssetWriterWithOutputFileType: 方法获取 setting,也可以根据实际情况使用自定义的方式:
- AVFormatIDKey:音频的编码方式,kAudioFormatMPEG4AAC 表示用 AAC 格式编码;
- AVNumberOfChannelsKey:表示通道数
- AVSampleRateKey:音频的编码码率,码率越大,精度越高,声音还原度更高;
- AVChannelLayoutKey:是用来描述音频数据通道布局的数据结构;
- AVEncoderBitRateKey:音频的预期比特率。
开始录制
当开发者创建了 AVAssetWriter 实例 writer,并且为 writer 添加了用于写入视频的 AVAssetWriterInput 实例 videoWriterInput 和用于写入音频的 AVAssetWriterInput 实例 audioWriterInput 后,便可以开始录制,写入视频和音频等媒体资源。
视频写入如下代码所示:
- (void)appendVideo:(CMSampleBufferRef)sampleBuffer
{
CFRetain(sampleBuffer);
__weak typeof(self)weakSelf = self;
dispatch_async(_recordQueue, ^{
__strong typeof(weakSelf)strongSelf = weakSelf;
if (!strongSelf->_canWriting) {
CFRelease(sampleBuffer);
return;
}
// 1. 获取视频的时间戳
CMTime timestamp = CMSampleBufferGetPresentationTimeStamp(sampleBuffer);
// 2.判断当前 writer 的状态,如果是 unknow,则需要调用 startWriting 启动写入
if (strongSelf->_writer.status == AVAssetWriterStatusUnknown) {
[strongSelf->_writer startWriting];
// 3.启动写入后,需要重置下 writer 的初始化时间戳
[strongSelf->_writer startSessionAtSourceTime:timestamp];
CFRelease(sampleBuffer);
SYLog(TAG, "appendVideo writer status is AVAssetWriterStatusUnknown");
return;
}
// 4.每次写入的时候需要判断下 writer 的写入状态,
// 如果不为 AVAssetWriterStatusWriting,则表示 writer 不再具备写入条件;
if (strongSelf->_writer.status != AVAssetWriterStatusWriting) {
CFRelease(sampleBuffer);
SYLog(TAG, "appendVideo writer status is %ld, error = %@", (long)strongSelf->_writer.status, strongSelf->_writer.error.description);
return;
}
// 5.当 writer 处于可写入状态后,
// 需要再判断 videoWriterInput 是否准备好可写入的数据
if (!strongSelf->_videoWriterInput.isReadyForMoreMediaData) {
CFRelease(sampleBuffer);
SYLog(TAG, "appendVideo writer status is not readyForMoreMediaData");
return;
}
// 6.从 sampleBuffer 获取 pixelBuffer,可以修饰之后,
// 在给回 AVAssetWriterInputPixelBufferAdaptor 用以写入
CVPixelBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
// 7.写入视频
[self->_adaptor appendPixelBuffer:pixelBuffer withPresentationTime:timestamp];
CFRelease(sampleBuffer);
});
}
- writer 内部有个AVAssetWriterStatus 类型的属性 status,用于表示当前 writer 的状态,具体如下:
- AVAssetWriterStatusUnknown:未知状态,表示 writer 还没开始写入,需要调用 writer 的 startWriting 允许写入,改变状态;
- AVAssetWriterStatusWriting:写入中状态,表示 writer 处于可写入状态;
- AVAssetWriterStatusCompleted:当前 writer 已写入完毕,不可再写入;
- AVAssetWriterStatusFailed:writer 写入失败,不可再写入,可以通过 writer.error 查看具体错误原因;
- AVAssetWriterStatusCancelled:writer 取消了写入,不可再写入了。
- writer 的 startSessionAtSourceTime 用于将 writer 的媒体写入起始时间同首帧对齐;
- isReadyForMoreMediaData:表示当前 videoWriterInput 的数据是否准备好了;
音频写入如下代码所示:
- (void)appendAudio:(CMSampleBufferRef)sampleBuffer
{
CFRetain(sampleBuffer);
__weak typeof(self)weakSelf = self;
dispatch_async(_recordQueue, ^{
__strong typeof(weakSelf)strongSelf = weakSelf;
if (!strongSelf->_canWriting) {
CFRelease(sampleBuffer);
return;
}
// 1.每次写入的时候需要判断下 writer 的写入状态,
// 如果不为 AVAssetWriterStatusWriting,则表示 writer 不再具备写入条件;
if (strongSelf->_writer.status != AVAssetWriterStatusWriting) {
CFRelease(sampleBuffer);
SYLog(TAG, "appendAudio writer status is %ld", (long)strongSelf->_writer.status);
return;
}
// 2.当 writer 处于可写入状态后,
// 需要再判断 videoWriterInput 是否准备好可写入的数据
if (!strongSelf->_audioWriterInput.isReadyForMoreMediaData) {
CFRelease(sampleBuffer);
SYLog(TAG, "appendAudio writer status is not readyForMoreMediaData");
return;
}
// 3.写入音频
[strongSelf->_audioWriterInput appendSampleBuffer:sampleBuffer];
CFRelease(sampleBuffer);
});
}
结束录制
结束录制相对简单,具体代码如下图所示:
- (void)stopRecordWithCompletion:(void (^)(NSURL * _Nullable, BOOL))completion
{
__weak typeof(self)weakSelf = self;
dispatch_async(_recordQueue, ^{
__strong typeof(weakSelf)strongSelf = weakSelf;
strongSelf->_canWriting = NO;
SYLog(TAG, "stopRecordWithCompletion canWriting = %d", strongSelf->_canWriting);
// 1. 调用 writer 的 finishWritingWithCompletionHandler 结束录制
[strongSelf->_writer finishWritingWithCompletionHandler:^{
// 2. 若此时 writer 的状态是 AVAssetWriterStatusCompleted,
// 则表示此次录制成功,可以在 outputURL 处获取视频
if (strongSelf->_writer.status == AVAssetWriterStatusCompleted) {
completion(strongSelf->_writer.outputURL, YES);
} else {
// 3.若其他状态,则表明录制出错,具体可通过 writer.error 而知
SYLog(TAG, "stopRecordWithCompletion failure, status = %ld", (long)strongSelf->_writer.status);
completion(nil, NO);
}
}];
});
}
总结
本文主要是分享了 AV Foundation 中相机的录制。视频录制需要包含音频,因此首先需要请求麦克风权限,用户允许后,才可以调用麦克风采集相关的 API。
麦克风采集和摄像头采集类似,需要先获取对应的 AVCaptureDevice 实例 audioDevice,接着通过 AudioDevice 创建 AVCaptureDeviceInput 实例 audioInput,以及用于传输采集数据的 AVCaptureAudioDataOutput 实例 audioOutput。
由于摄像头和麦克风采集的数据都是裸数据,需要用 AVAssetWriter 将其编码并存储到文件。AVAssetWriter 编码和存储采用的是 input 方式来管理的,因此需要分别创建 videoWriterInput 和 audioWriterInput 协助 writer 写入音视频数据。
开始录制后,writer 在每次写入数据的时候,需要判断自身的状态,处于 AVAssetWriterStatusWriting 才可以写入。而当结束录制,writer 要处于 AVAssetWriterStatusCompleted 才是编码和保存成功。
本文涉及的相关代码可以参考 SwiftyCamera,SwiftyCamera 旨在打造一个易用强大的相机 SDK,目前尚处于摇篮阶段,有什么写的不对或不好的,欢迎大家提意见。