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

2019-04-24  本文已影响0人  Seacen_Liu
Action

在前面【iOS】自定义相机(六)拍照录像中,我们介绍了如何使用AVCaptureMovieFileOutput进行视频的录制。AVCaptureMovieFileOutput虽然十分方便,但是我们无法使用AVCaptureVideoDataOutput获取帧图片进行处理。因此这一篇文章就介绍一下直接使用AVCaptureVideoDataOutputAVCaptureAudioDataOutput进行视频的录制的姿势。

对应的代码在SCCamera中的SCMovieManager.mSCCameraController.m中。

AVCaptureVideoDataOutputSampleBufferDelegateAVCaptureAudioDataOutputSampleBufferDelegatecaptureOutput:didOutputSampleBuffer:fromConnection:中我们可以获取到音视频数据CMSampleBufferRef。通过使用AVAssetWriter我们就能达到,将每一帧的视频和音频写入文件的目的。从而模拟出AVCaptureMovieFileOutput的效果。

AVAssetWriter 使用

AVFoundation 中提供了直接读写视频媒体样本的类,分别为AVAssetReaderAVAssetWriterAVAssetReader 用于将资源从 AVAsset 实例中读取媒体样本,AVAssetWriter 用于对媒体资源进行编码并将其写入到容器文件中。

AVAssetWriter是一个用于写入资源的类,他由一个或多个AVAssetWriterInput组成,用于添加要写入容器的特定的媒体样本的CMSampleBufferAVAssetWriterInput需要配置指定的媒体类型,比如音频或视频。对于自定义相机的视频录制,AVAssetWriter的使用如下:

AVAssetWriter 写入过程

AVAssetWriter的创建十分简单,只需要指定目标容器的NSURL即可。重点需要注意的地方是AVAssetWriterInput

outputSettings

outputSettings是一个NSDictionary<NSString *, id>,用键值对的方式表示音视频编码与压缩相关信息,例子如下:

NSDictionary *outSettings = @{
    AVVideoCodecKey: AVVideoCodecH264,
    AVVideoWidthKey: @1280,
    AVVideoHeightKey: @720,
    AVVideoCompressionPropertiesKey: @{
        AVVideoMaxKeyFrameIntervalKey: @1,
        AVVideoAverageBitRateKey: @105000000,
        AVVideoProfileLevelKey: AVVideoProfileLevelH264Main31
    }
}

逐个键值对进行设置可控性比较强,但容易写错。方便起见,我们可以使用AVCaptureVideoDataOutputAVCaptureAudioDataOutputrecommendedVideoSettingsForAssetWriterWithOutputFileType:方法。只需要传入我们需要导出的文件类型,即可获取一个当前系统可以使用的一个settings字典。

音视频样本写入模式

我们都知道,在使用AVCaptureVideoOutputAVCaotureAudioOutput进行视频录制中,音频数据和视频数据是分开获取的。因此我们在使用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.");
}

录制过程

使用AVCaptureVideoDataOutputAVCaptureAudioDataOutput进行获取数据的时候,我们都需要实现下面的代理方法并进行样本数据写入:

- (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.movieWriterfinishWritingWithCompletionHandler:方式,并根据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这个串行队列异步执行

上一篇 下一篇

猜你喜欢

热点阅读