AV Foundation 相机之录制

1,227 阅读9分钟

创作不易,喜欢的话请点赞收藏转载,您的支持是我更新的最大动力!!!

相机,除了拍照功能,视频录制也是一个是十分受欢迎的功能。下面,笔者将同大家一起学习下 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:用于指定视频的分辨率。
  • expectsMediaDataInRealTime: 表示是否要针对实时数据进行定制处理,如果为 YES,则音频和视频数据采用交错方式排列,提高写入效率,如下图所示:

未命名绘图 (2).png

如果单纯为了将视频写入到文件,创建 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,目前尚处于摇篮阶段,有什么写的不对或不好的,欢迎大家提意见。