AVFoundation拍照及录制到文件(四)

前言

从本文开始逐渐学习iOS自带的多媒体处理框架,例如AVFoundation,VideoToolbox,CoreMedia,CoreVideo实现多媒体的处理,并且将实现方式以及效果和ffmpeg的方式做对比

APP会有这样的需求,录制一段音频或者一段视频,或者拍摄一张照片等等,AVFoundation提供了为我们提供了实现这些需求的接口。通过这些接口我们可以从设备获取指定格式的未压缩的音视频数据,然后又可以压缩之后保存到文件里面存储在本地或者在网络上传输

本文的目的:
1、熟悉AVFoundation中关于拍照接口的使用
2、将采集到的音频或者视频压缩后用另外一种更加简单的方式保存在MP4文件中

采集相关流程

image.png

上图介绍了AVFoundation框架中关于采集相关的对象关系图,如下为具体对象的解释

要开启音视频采集,必须在项目配置文件Info.plist中添加NSMicrophoneUsageDescription音频使用权限和NSCameraUsageDescription相机使用权限

相关对象及函数介绍

  • 1、AVCaptureSession
    采集会话对象,用于管理采集的开始,结束等等操作,它一端连接着麦克风和摄像头等等输入设备对象接受他们提供的音视频数据,另一端连接着音频输出对象和视频输出对象通过他们向外界提供指定格式的音视频数据
  • 2、AVCaptureDevice
    代表着具体的采集音频或者视频的物理对象,例如麦克风,前后置摄像头等等

  • 3、AVCaptureDeviceInput
    采集输入对象,它是AVCaptureInput的实现子类,该对象被连接到AVCaptureSession之后就可以对其提供音频或者视频数据了,通过如下方法添加采集输入对象
    -(void)addInput:(AVCaptureInput *)input;

  • 4、AVCaptureVideoDataOutput
    视频输出对象,它被添加到AVCaptureSession之后就可以向外界提供采集好的视频数据了,同时通过该对象设置采集到的视频数据的格式(包括像素的格式,比如RGB的还是YUV的等等)

  • 5、AVCaptureAudioDataOutput
    音频输出对象,它被添加到AVCaptureSession之后就可以向外界提供采集好的音频数据了,该对象对于音频数据的格式设置比较少,如果想要更加详细的音频格式采集可以采用AudioUnit框架进行 参考我前面写的文章 AudioUnit录制音频+耳返(四)

  • 6、AVCaptureStillImageOutput
    原始照片输出对象,同样它也需要通过被添加到AVCaptureSession之后向外界提供采集好的原始照片

AVCaptureVideoDataOutput、AVCaptureAudioDataOutput、AVCaptureStillImageOutput、AVCaptureMovieFileOutput都是AVCaptureOutput的具体实现子类,
通过-(void)addOutput:(AVCaptureOutput *)output方法被添加AVCaptureSession中去,然后由他们向外界提供数据

  • 7、CMSampleBufferRef
    此对象代表了采集到的音视频数据的一个结构体,它包含了音频或者视频相关的参数,这些参数包括音频参数(编码方式,采样率,采样格式,声道类型,声道数等等),视频参数(编码方式,宽高,颜色标准bt601/bt709,像素格式yuv还是RGB)

8 、- (void)startRunning;和-(void)stopRunning;
分别对应着采集开始和结束采集,他们一般都成对调用。备注:startRunning方法可能会花费1秒左右时间,它会阻塞当前线程,所以使用是要注意不能阻塞主线程

实现代码

主要实现的功能就是采集音频和1280x720的视频,视频采用h264方式编码,音频采用aac方式编码,最后保存到MOV中

#import <UIKit/UIKit.h>


@interface AVCapturePriviewer : UIView

/** 实现AVFoundation采集的视频实时预览,同时能够拍摄高分辨率的照片
 *  将采集到的音视频保存到MOV中
 *
 *  备注:通过AVCaptureMovieFileOutput只能保存为MOV文件格式
 */
- (void)startCaptureMovieDst:(NSURL*)moveURL;
@end
#import "AVCapturePriviewer.h"
#import <AVFoundation/AVFoundation.h>
#import <CoreAudio/CoreAudioTypes.h>

@interface AVCapturePriviewer()<AVCaptureFileOutputRecordingDelegate,AVCapturePhotoCaptureDelegate>
{
    // 采集管理会话
    AVCaptureSession *captureSession;
    // 采集工作队列,由于采集,开始,更换摄像头等等需要一定的耗时,所以需要放在子线程做做这些事情
    dispatch_queue_t sessionQueue;
    AVCaptureDeviceInput *videoInput;
    AVCaptureDeviceInput *audioInput;
    AVCaptureMovieFileOutput *fileOutput;
    AVCapturePhotoOutput *stillOutput;
    
    UIButton *stillImage;
    UIButton *record;
    UIButton *change;
    
    NSURL *dstURL;
    
    
    UIBackgroundTaskIdentifier taskId;
}
@end

@implementation AVCapturePriviewer

- (void)startCaptureMovieDst:(NSURL*)moveURL
{
    taskId = UIBackgroundTaskInvalid;
    dstURL = moveURL;
    stillImage = [UIButton buttonWithType:UIButtonTypeSystem];
    stillImage.frame = CGRectMake(self.bounds.size.width - 80, 0, 80, 30);
    [stillImage setTitle:@"stillImage" forState:UIControlStateNormal];
    [stillImage addTarget:self action:@selector(onTapButton:) forControlEvents:UIControlEventTouchUpInside];
    [self addSubview:stillImage];


    record = [UIButton buttonWithType:UIButtonTypeSystem];
    record.frame = CGRectMake(self.bounds.size.width - 80, 40, 80, 30);
    [record setTitle:@"record" forState:UIControlStateNormal];
    [record addTarget:self action:@selector(onTapButton:) forControlEvents:UIControlEventTouchUpInside];
    [self addSubview:record];

    change = [UIButton buttonWithType:UIButtonTypeSystem];
    change.frame = CGRectMake(self.bounds.size.width - 80, 80, 80, 30);
    [change setTitle:@"change" forState:UIControlStateNormal];
    [change addTarget:self action:@selector(onTapButton:) forControlEvents:UIControlEventTouchUpInside];
    [self addSubview:change];
    
    /** 关于相机权限和麦克风权限的总结:
     *  1、要想使用相机和麦克风首先得在Info.plist文件中添加NSMicrophoneUsageDescription音频使用权限
     *  和NSCameraUsageDescription相机使用权限
     *  2、首次使用APP,当使用AVCaptureDeviceInput进行初始化的时候就会弹出使用相机权限的对话框,如果用户拒绝赋予权限
     *  或者先赋予了又在设置里面拒绝给权限,那么这个初始化方法将返回nil
     *  4、用户删掉APP后对应的权限等等也一并删除
     *  3、所以正确的做法应该是如下的代码,根据当前权限状态来作相应的初始化
     */
    switch ([AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo]) {
        case AVAuthorizationStatusAuthorized:
        {
            NSLog(@"有视频权限");
            [self requestMicroAuthorized];
        }
        break;
        case AVAuthorizationStatusNotDetermined:
        {
            NSLog(@"首次打开APP则需要请求相机权限");
            // 此方法为异步的,用户选择完毕后回调被执行。如果APP还没有请求过权限,那么调用此方法会弹出一个对话框提示用户给与
            // 权限。如果用户已经拒绝过或者给予了权限则调用该方法无效
            [AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo completionHandler:^(BOOL granted) {
                NSLog(@"相机的选择为:%d",granted);
                if (granted) {
                    [self requestMicroAuthorized];
                }
            }];
        }
            break;
        default:
        {
            NSLog(@"用户拒绝给予相机权限,这时候可以调用自定义权限请求对话框请求权限");
            // 此时调用此方法无效
//            [AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo completionHandler:^(BOOL granted) {
//                if (!granted) {
//                    NSLog(@"用户拒绝了给予相机权限");
//                }
//
//            }];
        }
            break;
    }
    
    NSLog(@"结束");
}

- (void)requestMicroAuthorized
{
    switch ([AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeAudio]) {
        case AVAuthorizationStatusAuthorized:
        {
            NSLog(@"有麦克风权限了");
            [self setupCaptureSession];
        }
            break;
        case AVAuthorizationStatusNotDetermined:
        {
            NSLog(@"首次打开APP,还未请求过麦克风权限");
            [AVCaptureDevice requestAccessForMediaType:AVMediaTypeAudio completionHandler:^(BOOL granted) {
                if (granted) {
                    [self setupCaptureSession];
                }
                NSLog(@"音频权限 %d",granted);
            }];
        }
        default:
        {
            NSLog(@"不具有麦克风权限");
        }
            break;
    }
}

- (void)setupCaptureSession
{
    // 创建工作队列
    sessionQueue = dispatch_queue_create("CaputureQueue", DISPATCH_QUEUE_SERIAL);
    
    // 创建会话
    captureSession = [[AVCaptureSession alloc] init];
    // 设置会话的sessionPreset,代表了采集视频的宽高
    captureSession.sessionPreset = AVCaptureSessionPreset640x480;
    AVCaptureVideoPreviewLayer *videoLayer = [[AVCaptureVideoPreviewLayer alloc] init];
    videoLayer.frame = self.layer.bounds;
    videoLayer.session = captureSession;
    [self.layer addSublayer:videoLayer];
    
    dispatch_async(sessionQueue, ^{
        
        AVCaptureDevice *videoDeivce = [AVCaptureDevice defaultDeviceWithDeviceType:AVCaptureDeviceTypeBuiltInWideAngleCamera mediaType:AVMediaTypeVideo position:AVCaptureDevicePositionBack];
        // 1、添加视频采集
        // 首次使用APP,调用该方法就会弹出使用相机权限的对话框,如果用户拒绝赋予权限
        // 或者先赋予了又在设置里面拒绝给权限,那么这个初始化方法将返回nil
        self->videoInput = [[AVCaptureDeviceInput alloc] initWithDevice:videoDeivce error:nil];
        [self->captureSession addInput:self->videoInput];
        
        
        /** AVCaptureConnection
         *  AVCaptureSession实际上是通过该对象构建起AVCaptureInput和AVCaptureOutput之间的连接
         *  当调用addInput:和addOut:时就会自动构建一个AVCaptureConnection对象。
         *  AVCaptureVideoPreviewLayer内部也包含一个AVCaptureOutput对象,当调用videoLayer.session
         *  时会自动调用addOut:
         *
         *  它代表了一个流对象,通过它可以设置输出视频的方向,可以设置输出视频是否增稳等等属性
         */
        videoLayer.connection.videoOrientation = AVCaptureVideoOrientationPortrait;
    //    NSLog(@"connections %@",captureSession.connections);
        
        
        // 2、添加音频采集
        AVCaptureDevice *audioDeivce = [AVCaptureDevice defaultDeviceWithDeviceType:AVCaptureDeviceTypeBuiltInMicrophone mediaType:AVMediaTypeAudio position:AVCaptureDevicePositionUnspecified];
        // 首次使用APP,调用该方法就会弹出使用相机权限的对话框,如果用户拒绝赋予权限
        // 或者先赋予了又在设置里面拒绝给权限,那么这个初始化方法将返回nil
        self->audioInput = [[AVCaptureDeviceInput alloc] initWithDevice:audioDeivce error:nil];
        [self->captureSession addInput:self->audioInput];
        
        
        /** AVCaptureMovieFileOutput
         *  用于将来自采集的音视频输出自动压缩然后保存到指定的文件中
         *  它的文件容器格式只能是MOV,编码方式颜色格式音频参数等等都采用默认值
         *
         *  AVCaptureAudioFileOutput也是一样的工作方式
         */
        // 3、添加将采集的音视频保存到文件中
        self->fileOutput = [[AVCaptureMovieFileOutput alloc] init];
        [self->captureSession addOutput:self->fileOutput];
        // AVCaptureConnection必须在addOutput:方法调用之后才会生成AVCaptureConnection
        AVCaptureConnection *videoConn = [self->fileOutput connectionWithMediaType:AVMediaTypeVideo];
        NSDictionary *videoSettings = @{
            AVVideoCodecKey:AVVideoCodecH264
        };
        // 设置视频编码方式;它一般会有个默认编码方式,不同设备可能不一样,比如IphoneX 就是HEVC
        [self->fileOutput setOutputSettings:videoSettings forConnection:videoConn];
        
        
        // 4、添加拍照输出对象
        // 备注:会话管理对象一次性可以添加多个输入输出对象
        AVCapturePhotoOutput *stillOut = [[AVCapturePhotoOutput alloc] init];
        [self->captureSession addOutput:stillOut];
        self->stillOutput = stillOut;
        
        // 采集会话
        [self->captureSession startRunning];
        
        
    });
}


- (void)onTapButton:(UIButton*)btn
{
    if (btn == change) {
        [self changeCamera];
    } else if(btn == record) {
        [self recordStartOrStop:!fileOutput.recording];
    } else if(btn == stillImage) {
        [self takeStillImage];
    }
}

// 更换摄像头
- (void)changeCamera
{
    dispatch_async(sessionQueue, ^{
        
        AVCaptureDevicePosition curPostion = [self->videoInput.device position];
        if (curPostion == AVCaptureDevicePositionBack) {
            curPostion = AVCaptureDevicePositionFront;
        } else {
            curPostion = AVCaptureDevicePositionBack;
        }
        
        // 更换摄像头,只需要将以前AVCaptureInput删除,重新添加新的AVCaputreDeviceInput即可
        // 这个过程只需要卸载 beginConfiguration和commitConfiguration中
        AVCaptureDevice *device = [AVCaptureDevice defaultDeviceWithDeviceType:AVCaptureDeviceTypeBuiltInWideAngleCamera mediaType:AVMediaTypeVideo position:curPostion];
        AVCaptureDeviceInput *newInput = [[AVCaptureDeviceInput alloc] initWithDevice:device error:nil];
        [self->captureSession beginConfiguration];
        
        // 先删掉的输入对象,然后在添加新的,如果新的添加失败,则将以前的还原
        [self->captureSession removeInput:self->videoInput];
        if ([self->captureSession canAddInput:newInput]) {
            [self->captureSession addInput:newInput];
            self->videoInput = newInput;
        } else {
            [self->captureSession addInput:self->videoInput];
        }
        
        [self->captureSession commitConfiguration];
    });
}

- (void)recordStartOrStop:(BOOL)start
{
    dispatch_async(sessionQueue, ^{
        if (start) {
            // 当应用即将处于后台或者处于后台时,didFinishRecordingToOutputFileAtURL是不会调用的,也就是有可能文件没有完整保存。
            // 所以这里需要给应用申请多执行一段时间大概为180秒 该方法可以在APP的启动后任何地方调用,它必须和endBackgroundTask
            // 成对调用,否则会崩溃
            if (self->taskId == UIBackgroundTaskInvalid) {
                self->taskId = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:nil];
            }
            
            // 将采集的音视频自动保存到文件中
            [self->fileOutput startRecordingToOutputFileURL:self->dstURL recordingDelegate:self];
        } else {
            // 调用停止保存到文件方法
            [self->fileOutput stopRecording];
            
            NSLog(@"结束保存到文件");
        }
    });
    
}

- (void)takeStillImage
{
    dispatch_async(sessionQueue, ^{
        /** AVCapturePhotoSettings定义输出照片的数据格式已经生成文件的格式,例如压缩方式(JPEG,PNG,RAW),文件格式(JPG,png,DNG)等等其他拍照参数
         *  也支持输出未压缩的原始数据CVPixelbufferRef,具体可以参考AVCapturePhotoSettings设置
         */
        // 代表默认输出JPG文件
        AVCapturePhotoSettings *defaultSettings = [AVCapturePhotoSettings photoSettings];
        defaultSettings.autoStillImageStabilizationEnabled = YES;
        // 是否输出原始拍照分辨率,默认NO(代表输出照片的分辨率和视频分辨率一样大,例如前面sessionPreset为640x480的,那么生成照片也是这样大的)
        // 备注:下面这两个必须同时打开,否则崩溃
        defaultSettings.highResolutionPhotoEnabled = YES;
        self->stillOutput.highResolutionCaptureEnabled = YES;
        /** AVCapturePhotoOutpt 是对AVCaptureStillImageOutput的升级,它支持Live Photo capture, preview-sized image delivery, wide color, RAW, RAW+JPG and RAW+DNG formats.
         *  照片
         */
        // 按照指定的输出格式执行拍照指令,具体的执行情况通过代理回调
        
        [self->stillOutput capturePhotoWithSettings:defaultSettings delegate:self];
    });
    
}

- (void)captureOutput:(AVCaptureFileOutput *)output didStartRecordingToOutputFileAtURL:(NSURL *)fileURL fromConnections:(NSArray<AVCaptureConnection *> *)connections
{
    [record setTitle:@"stop" forState:UIControlStateNormal];
    NSLog(@"record start url %@ thread %@",fileURL,[NSThread currentThread]);
}

- (void)captureOutput:(AVCaptureFileOutput *)output didFinishRecordingToOutputFileAtURL:(NSURL *)outputFileURL fromConnections:(NSArray<AVCaptureConnection *> *)connections error:(NSError *)error
{
    NSLog(@"record finish url %@ thread %@",outputFileURL,[NSThread currentThread]);
    // 结束之后调用此方法
    [[UIApplication sharedApplication] endBackgroundTask:self->taskId];
    
    [self->record setTitle:@"record" forState:UIControlStateNormal];
}



- (void)captureOutput:(AVCapturePhotoOutput *)output willCapturePhotoForResolvedSettings:(nonnull AVCaptureResolvedPhotoSettings *)resolvedSettings
{
    NSLog(@"即将开始拍照 %@",resolvedSettings);
}

- (void)captureOutput:(AVCapturePhotoOutput *)output didCapturePhotoForResolvedSettings:(AVCaptureResolvedPhotoSettings *)resolvedSettings
{
    NSLog(@"拍照完成 %@",resolvedSettings);
}

// ios 11+的获取照片方式
#if __IPHONE_11_0
- (void)captureOutput:(AVCapturePhotoOutput *)output didFinishProcessingPhoto:(AVCapturePhoto *)photo error:(NSError *)error
API_AVAILABLE(ios(11.0)){
    NSLog(@"didFinishProcessingPhoto ");
    NSString *dstPath = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory,NSUserDomainMask,YES)[0] stringByAppendingPathComponent:@"1-test_capture.JPG"];
    NSURL *dstURL = [NSURL fileURLWithPath:dstPath];
    NSData *jpg = [photo fileDataRepresentation];
    [jpg writeToURL:dstURL atomically:YES];
}
#else
// ios 10的获取照片方式
- (void)captureOutput:(AVCapturePhotoOutput *)output didFinishProcessingPhotoSampleBuffer:(CMSampleBufferRef)photoSampleBuffer previewPhotoSampleBuffer:(CMSampleBufferRef)previewPhotoSampleBuffer resolvedSettings:(AVCaptureResolvedPhotoSettings *)resolvedSettings bracketSettings:(AVCaptureBracketedStillImageSettings *)bracketSettings error:(NSError *)error
{
    NSData *jpgegData = [AVCapturePhotoOutput JPEGPhotoDataRepresentationForJPEGSampleBuffer:photoSampleBuffer previewPhotoSampleBuffer:previewPhotoSampleBuffer];
    NSString *dstPath = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory,NSUserDomainMask,YES)[0] stringByAppendingPathComponent:@"1-test_capture.JPG"];
    NSURL *dstURL = [NSURL fileURLWithPath:dstPath];
    [jpgegData writeToURL:dstURL atomically:YES];
}
#endif


- (void)captureOutput:(AVCapturePhotoOutput *)output didFinishCaptureForResolvedSettings:(nonnull AVCaptureResolvedPhotoSettings *)resolvedSettings error:(nullable NSError *)error
{
    NSLog(@"拍照完成 didFinishCaptureForResolvedSettings");
}
@end

tips:通过AVCaptureMovieFileOutput采集保存音视频只能保存为MOV格式的,编码方式可以选择H264或者H25等等

遇到问题

项目地址

https://github.com/nldzsz/ffmpeg-demo

位于AVFoundation目录下文件AVCapturePriviewer.h/AVCapturePriviewer.m中

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