使用AVFoundation实现视频的录制(仿微信视频录制)

前言:项目中有发送视频和图片的需求,产品想要微信小视频录制的功能,即单击拍照,长按录制小视频,参考很多大神的demo,以及翻阅AVFoundation相关文献,自己用AVFoundation的API封装了一个小视频录制,后面会加上闪光灯,聚焦,滤镜...等功能。

AVFoundation官方文档介绍:

The AVFoundation framework combines four major technology areas that together encompass a wide range of tasks for capturing, processing, synthesizing, controlling, importing and exporting audiovisual media on Apple platforms.

解释为:
AVFoundation框架结合了四个主要的技术领域,它们共同包含了在苹果平台上捕获、处理、合成、控制、导入和导出视听媒体的广泛任务。

AVFoundation在相关框架栈中的位置图如:

AVFoundation框架图

我们本文章只讨论关于AVFoundation关于音视频录制的功能,AVFoundation包含很多头文件,其中涉及音视频的头文件主要有以下几个:

//AVCaptureDevice提供实时输入媒体数据(如视频和音频)的物理设备
#import <AVFoundation/AVCaptureDevice.h>
//AVCaptureInput是一个抽象类,它提供了一个接口,用于将捕获输入源连接到AVCaptureSession
#import <AVFoundation/AVCaptureInput.h> 
//AVCaptureOutput用于处理未压缩或压缩的音视频样本被捕获,一般用AVCaptureAudioDataOutput和AVCaptureVideoDataOutput子类
#import <AVFoundation/AVCaptureOutput.h> 
//AVCaptureSession是AVFoundation捕获类的中心枢纽
#import <AVFoundation/AVCaptureSession.h>
//用于预览AVCaptureSession的可视输出的CoreAnimation层的子类
#import <AVFoundation/AVCaptureVideoPreviewLayer.h>
//AVAssetWriter提供将媒体数据写入新文件的服务
#import <AVFoundation/AVAssetWriter.h>
//用于将新媒体样本或对打包为CMSampleBuffer对象的现有媒体样本的引用附加到AVAssetWriter输出文件的单个轨迹中
#import <AVFoundation/AVAssetWriterInput.h>
//系统提供的处理视频的类(压缩)
#import <AVFoundation/AVAssetExportSession.h>

可以用如下一幅图来概述:


AVFoundation功能概述图

从图上可以清晰的看出各个模块的功能,接下来详细介绍一下每个模块如何使用的。

1. AVCaptureSession

AVCaptureSession是AVFoundation捕获类的中心枢纽,用法:

- (AVCaptureSession *)session{
    if (_session == nil){
        _session = [[AVCaptureSession alloc] init];
        //高质量采集率
        [_session setSessionPreset:AVCaptureSessionPresetHigh];
        if([_session canAddInput:self.videoInput]) [_session addInput:self.videoInput]; //添加视频输入流
        if([_session canAddInput:self.audioInput])  [_session addInput:self.audioInput];  //添加音频输入流
        if([_session canAddOutput:self.videoDataOutput]) [_session addOutput:self.videoDataOutput];  //视频数据输出流 纯画面
        if([_session canAddOutput:self.audioDataOutput]) [_session addOutput:self.audioDataOutput];  //音频数据输出流
        
        AVCaptureConnection * captureVideoConnection = [self.videoDataOutput connectionWithMediaType:AVMediaTypeVideo];
        // 设置是否为镜像,前置摄像头采集到的数据本来就是翻转的,这里设置为镜像把画面转回来
        if (self.devicePosition == AVCaptureDevicePositionFront && captureVideoConnection.supportsVideoMirroring) {
            captureVideoConnection.videoMirrored = YES;
        }
        captureVideoConnection.videoOrientation = AVCaptureVideoOrientationPortrait;
    }
    return _session;
}

AVCaptureSessionPreset此属性的值是AVCaptureSession预设值,表示接收方正在使用的当前会话预设值。可以在接收器运行时设置session预设属性,有一下几个值:

  • AVCaptureSessionPresetPhoto:适用于高分辨率照片质量输出
  • AVCaptureSessionPresetHigh:适用于高质量视频和音频输出
  • AVCaptureSessionPresetMedium:适合中等质量输出
  • AVCaptureSessionPresetLow:适用于低质量输出
  • AVCaptureSessionPreset320x240:适合320x240视频输出
  • AVCaptureSessionPreset352x288:CIF质量
  • AVCaptureSessionPreset640x480:VGA质量
  • AVCaptureSessionPreset960x540:HD质量
  • AVCaptureSessionPreset1280x720:720p
  • AVCaptureSessionPreset1920x1080:1080P
    -AVCaptureSessionPreset3840x2160:UHD or 4K
  • AVCaptureSessionPresetiFrame960x540:实现960x540质量的iFrame H.264视频在~30兆/秒AAC音频
  • AVCaptureSessionPresetiFrame1280x720:实现1280x720质量的iFrame H.264视频,在~ 40mbits /秒的AAC音频
  • AVCaptureSessionPresetInputPriority:当客户端在设备上设置活动格式时,相关会话的- session预设属性自动更改为AVCaptureSessionPresetInputPriority

AVCaptureSession需要添加相应的视频/音频的输入/输出流才能捕获音视频的样本。

AVCaptureSession可以调用startRunning来启动捕获和stopRunning来停止捕获。

2. AVCaptureDeviceInput

AVCaptureDeviceInputAVCaptureInput的子类,提供了一个接口,用于将捕获输入源链接到AVCaptureSession,用法:

① 视频输入源:

- (AVCaptureDeviceInput *)videoInput {
    if (_videoInput == nil) {
        //添加一个视频输入设备  默认是后置摄像头
        AVCaptureDevice *videoCaptureDevice =  [self getCameraDeviceWithPosition:AVCaptureDevicePositionBack];
        //创建视频输入流
        _videoInput = [AVCaptureDeviceInput deviceInputWithDevice:videoCaptureDevice error:nil];
        if (!_videoInput){
            NSLog(@"获得摄像头失败");
            return nil;
        }
    }
    return _videoInput;
}

② 音频输入源:

- (AVCaptureDeviceInput *)audioInput {
    if (_audioInput == nil) {
        NSError * error = nil;
        //添加一个音频输入/捕获设备
        AVCaptureDevice * audioCaptureDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio];
        _audioInput = [[AVCaptureDeviceInput alloc] initWithDevice:audioCaptureDevice error:&error];
        if (error) {
            NSLog(@"获得音频输入设备失败:%@",error.localizedDescription);
        }
    }
    return _audioInput;
}

音视频输入源都用到了AVCaptureDevice,AVCaptureDevice表示提供实时输入媒体数据(如视频和音频)的物理设备,AVCaptureDevice通过AVCaptureDevicePosition参数获取,AVCaptureDevicePosition有一下几个参数:

-AVCaptureDevicePositionUnspecified:默认(后置)
-AVCaptureDevicePositionBack:后置
-AVCaptureDevicePositionFront:前置

获取视频的AVCaptureDevice的代码如下:

//获取指定位置的摄像头
- (AVCaptureDevice *)getCameraDeviceWithPosition:(AVCaptureDevicePosition)positon {
    if (@available(iOS 10.2, *)) {
        AVCaptureDeviceDiscoverySession *dissession = [AVCaptureDeviceDiscoverySession discoverySessionWithDeviceTypes:@[AVCaptureDeviceTypeBuiltInDualCamera,AVCaptureDeviceTypeBuiltInTelephotoCamera,AVCaptureDeviceTypeBuiltInWideAngleCamera] mediaType:AVMediaTypeVideo position:positon];
        for (AVCaptureDevice *device in dissession.devices) {
            if ([device position] == positon) {
                return device;
            }
        }
    } else {
        NSArray *devices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo];
        for (AVCaptureDevice *device in devices) {
            if ([device position] == positon) {
                return device;
            }
        }
    }
    return nil;
}

注:切换前后置摄像头需要调用beginConfigurationcommitConfiguration来进行摄像头设备的切换,移除之前的设备输入源,添加新的设备输入源,代码如下:

//切换前/后置摄像头
- (void)switchsCamera:(AVCaptureDevicePosition)devicePosition {
    //当前设备方向
    if (self.devicePosition == devicePosition) {
        return;
    }
    AVCaptureDeviceInput *videoInput = [AVCaptureDeviceInput deviceInputWithDevice:[self getCameraDeviceWithPosition:devicePosition] error:nil];
    //先开启配置,配置完成后提交配置改变
    [self.session beginConfiguration];
    //移除原有输入对象
    [self.session removeInput:self.videoInput];
    //添加新的输入对象
    if ([self.session canAddInput:videoInput]) {
        [self.session addInput:videoInput];
        self.videoInput = videoInput;
    }
    
    //视频输入对象发生了改变  视频输出的链接也要重新初始化
    AVCaptureConnection * captureConnection = [self.videoDataOutput connectionWithMediaType:AVMediaTypeVideo];
    if (self.devicePosition == AVCaptureDevicePositionFront && captureConnection.supportsVideoMirroring) {
        captureConnection.videoMirrored = YES;
    }
    captureConnection.videoOrientation = AVCaptureVideoOrientationPortrait;
    
    //提交新的输入对象
    [self.session commitConfiguration];
}

获取音频的AVCaptureDevice则是通过defaultDeviceWithMediaType:方法的,需要的参数是AVMediaType-媒体类型,常用的有视频的:AVMediaTypeVideo和音频的:AVMediaTypeAudio

3. AVCaptureVideoDataOutput和AVCaptureAudioDataOutput

AVCaptureVideoDataOutputAVCaptureAudioDataOutputAVCaptureOutput的子类,用于捕获未压缩或压缩的音视频样本,使用代码如下:

//视频输入源
- (AVCaptureVideoDataOutput *)videoDataOutput {
    if (_videoDataOutput == nil) {
        _videoDataOutput = [[AVCaptureVideoDataOutput alloc] init];
        [_videoDataOutput setSampleBufferDelegate:self queue:dispatch_get_global_queue(0, 0)];
    }
    return _videoDataOutput;
}
//音频输入源
- (AVCaptureAudioDataOutput *)audioDataOutput {
    if (_audioDataOutput == nil) {
        _audioDataOutput = [[AVCaptureAudioDataOutput alloc] init];
        [_audioDataOutput setSampleBufferDelegate:self queue:dispatch_get_global_queue(0, 0)];
    }
    return _audioDataOutput;
}

需要设置AVCaptureVideoDataOutputSampleBufferDelegate和AVCaptureAudioDataOutputSampleBufferDelegate代理,有两个捕获音视频数据的代理方法:

#pragma mark -  AVCaptureVideoDataOutputSampleBufferDelegate AVCaptureAudioDataOutputSampleBufferDelegate 实时输出音视频
/// 实时输出采集到的音视频帧内容
- (void)captureOutput:(AVCaptureOutput *)output didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection {
    if (!sampleBuffer) {
        return;
    }
    //提供对外接口,方便自定义处理
    if (output == self.videoDataOutput) {
        if([self.delegate respondsToSelector:@selector(captureSession:didOutputVideoSampleBuffer:fromConnection:)]) {
            [self.delegate captureSession:self didOutputVideoSampleBuffer:sampleBuffer fromConnection:connection];
        }
    }
    if (output == self.audioDataOutput) {
        if([self.delegate respondsToSelector:@selector(captureSession:didOutputAudioSampleBuffer:fromConnection:)]) {
            [self.delegate captureSession:self didOutputAudioSampleBuffer:sampleBuffer fromConnection:connection];
        }
    }
}
/// 实时输出丢弃的音视频帧内容
- (void)captureOutput:(AVCaptureOutput *)output didDropSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection API_AVAILABLE(ios(6.0)) {
    
}

捕获到音视频之后,用户就可以用于压缩和本地保存了,后面会说一下音视频的压缩和本地保存。

4. AVCaptureVideoPreviewLayer

AVCaptureVideoPreviewLayer用于预览AVCaptureSession的可视输出的CoreAnimation层的子类,简单点说就是实时预览摄像头捕获到的视图。

- (AVCaptureVideoPreviewLayer *)previewLayer {
    if (_previewLayer == nil) {
        _previewLayer = [AVCaptureVideoPreviewLayer layerWithSession:self.session];
        _previewLayer.videoGravity = AVLayerVideoGravityResizeAspect;
    }
    return _previewLayer;
}

需要AVCaptureSession参数来初始化,在AVCaptureSession对象startRunning(启动运行)时,显示出摄像头捕获到的视图。
videoGravity参数是视频如何在AVCaptureVideoPreviewLayer边界矩形内显示,有三种样式:

-AVLayerVideoGravityResizeAspect:在视图内保持长宽比,可能预览的视图不是全屏的。
-AVLayerVideoGravityResizeAspectFill:在视图内保持长宽比的情况下填充满。
-AVLayerVideoGravityResize:拉伸填充层边界。

默认是AVLayerVideoGravityResizeAspect

5. CMMotionManager

CMMotionManager是运动传感器,用来监测设备方向的,初始化如下,

- (CMMotionManager *)motionManager {
    if (!_motionManager) {
        _motionManager = [[CMMotionManager alloc] init];
    }
    return _motionManager;
}

当用户startRunning开启时,则需要监测设备方向了,当用户stopRunning停止时,则停止监测设备方向。在代码中调用startUpdateDeviceDirectionstopUpdateDeviceDirection来开启监测和停止监测,这里就不贴代码了。

6. AVAssetWriter

AVAssetWriter提供将媒体数据写入新文件的服务,通过assetWriterWithURL:fileType:error:方法来初始化,需要AVAssetWriterInput将新媒体样本或打包为CMSampleBuffer对象的现有媒体样本引用附加到AVAssetWriter输出文件中,有几种方法:

startWriting:为接收输入和将输出写入输出文件做好准备。
startSessionAtSourceTime::为接收方启动一个示例编写会话。
finishWritingWithCompletionHandler::将所有未完成的输入标记为完成,并完成输出文件的写入。

7.AVAssetWriterInput

AVAssetWriterInput用于将新媒体样本或对打包为CMSampleBuffer对象的现有媒体样本的引用附加到AVAssetWriter输出文件的单个轨迹中。

视频写入文件初始化:

- (AVAssetWriterInput *)assetWriterVideoInput {
    if (!_assetWriterVideoInput) {
        //写入视频大小
        NSInteger numPixels = self.videoSize.width * [UIScreen mainScreen].scale  * self.videoSize.height * [UIScreen mainScreen].scale;
        //每像素比特
        CGFloat bitsPerPixel = 24.0;
        NSInteger bitsPerSecond = numPixels * bitsPerPixel;
        // 码率和帧率设置
        NSDictionary *compressionProperties = @{ AVVideoAverageBitRateKey : @(bitsPerSecond),
                                                 AVVideoExpectedSourceFrameRateKey : @(30),
                                                 AVVideoMaxKeyFrameIntervalKey : @(30),
                                                 AVVideoProfileLevelKey : AVVideoProfileLevelH264BaselineAutoLevel };
        CGFloat width = self.videoSize.width * [UIScreen mainScreen].scale;
        CGFloat height = self.videoSize.height * [UIScreen mainScreen].scale;
        //视频属性
        self.videoCompressionSettings = @{ AVVideoCodecKey : AVVideoCodecH264,
                                           AVVideoWidthKey : @(width),
                                           AVVideoHeightKey : @(height),
                                           AVVideoScalingModeKey : AVVideoScalingModeResizeAspectFill,
                                           AVVideoCompressionPropertiesKey : compressionProperties };
        
        _assetWriterVideoInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo outputSettings:self.videoCompressionSettings];
        //expectsMediaDataInRealTime 必须设为yes,需要从capture session 实时获取数据
        _assetWriterVideoInput.expectsMediaDataInRealTime = YES;
    }
    return _assetWriterVideoInput;
}

音频写入文件初始化:

- (AVAssetWriterInput *)assetWriterAudioInput {
    if (_assetWriterAudioInput == nil) {
        /* 注:
         <1>AVNumberOfChannelsKey 通道数  1为单通道 2为立体通道
         <2>AVSampleRateKey 采样率 取值为 8000/44100/96000 影响音频采集的质量
         <3>d 比特率(音频码率) 取值为 8 16 24 32
         <4>AVEncoderAudioQualityKey 质量  (需要iphone8以上手机)
         <5>AVEncoderBitRateKey 比特采样率 一般是128000
         */
        
        /*另注:aac的音频采样率不支持96000,当我设置成8000时,assetWriter也是报错*/
        // 音频设置
        _audioCompressionSettings = @{ AVEncoderBitRatePerChannelKey : @(28000),
                                       AVFormatIDKey : @(kAudioFormatMPEG4AAC),
                                       AVNumberOfChannelsKey : @(1),
                                       AVSampleRateKey : @(22050) };
        
        _assetWriterAudioInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeAudio outputSettings:self.audioCompressionSettings];
        _assetWriterAudioInput.expectsMediaDataInRealTime = YES;
    }
    return _assetWriterAudioInput;
}

8. AVAssetExportSession

AVAssetExportSession是系统自带的压缩
主要有几个参数:

-outputURL:输出URL
-shouldOptimizeForNetworkUse:优化网络
-outputFileType:转换后的格式

设置完上面的参数之后,就可以调用下面的方法来压缩视频了,压缩之后就可以根据视频地址保存被压缩之后的视频了

//异步导出
[videoExportSession exportAsynchronouslyWithCompletionHandler:^(NSError * _Nonnull error) {
            if (error) {
                NSLog(@"%@",error.localizedDescription);
            } else {
                //获取第一帧
                UIImage *cover = [UIImage dx_videoFirstFrameWithURL:url];
                //保存到相册,没有权限走出错处理
                [TMCaptureTool saveVideoToPhotoLibrary:url completion:^(PHAsset * _Nonnull asset, NSString * _Nonnull errorMessage) {
                    if (errorMessage) {  //保存失败
                        NSLog(@"%@",errorMessage);
                        [weakSelf finishWithImage:cover asset:nil videoPath:outputVideoFielPath];
                    } else {
                        [weakSelf finishWithImage:cover asset:asset videoPath:outputVideoFielPath];
                    }
                }];
            }
        } progress:^(float progress) {
            //NSLog(@"视频导出进度 %f",progress);
        }];

这里保存视频的方法为:

        [[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{
           PHAssetChangeRequest *request = [PHAssetChangeRequest creationRequestForAssetFromVideoAtFileURL:url];
            localIdentifier = request.placeholderForCreatedAsset.localIdentifier;
            request.creationDate = [NSDate date];
        } completionHandler:^(BOOL success, NSError * _Nullable error) {
            TM_DISPATCH_ON_MAIN_THREAD(^{
                if (success) {
                    PHAsset *asset = [[PHAsset fetchAssetsWithLocalIdentifiers:@[localIdentifier] options:nil] firstObject];
                    if (completion) completion(asset,nil);
                } else if (error) {
                    NSLog(@"保存视频失败 %@",error.localizedDescription);
                    if (completion) completion(nil,[NSString stringWithFormat:@"保存视频失败 %@",error.localizedDescription]);
                }
            });
        }];

到此,视频就录制和保存完了在介绍中只贴出来了部分代码,还有很多是自己封装起来的,想要查看完整项目的,可以查看TMCaptureVideo,是上传到github上的完整项目,仅供大家参考,欢迎大家指导!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 207,248评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,681评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,443评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,475评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,458评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,185评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,451评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,112评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,609评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,083评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,163评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,803评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,357评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,357评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,590评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,636评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,925评论 2 344