AVFoundation框架解析(二)—— 实现视频预览录制保存到相册

版本记录

版本号 时间
V1.0 2017.08.08

前言

AVFoundation框架是ios中很重要的框架,所有与视频音频相关的软硬件控制都在这个框架里面,接下来这几篇就主要对这个框架进行介绍和讲解。感兴趣的可以看我上几篇。
1. AVFoundation框架解析(一)—— 基本概览

功能要求

实现视频的预览功能,并可录制保存相册,并支持保存以后重新播放刚才录制内容。


功能实现

1. 几个相关的类

  • AVCaptureSession
    媒体(音、视频)捕获会话,负责把捕获的音视频数据输出到输出设备中。一个AVCaptureSession可以有多个输入输出。 AVCaptureSessionAVFoundation捕捉类的中心枢纽,在视频捕获时,客户端可以实例化AVCaptureSession并添加适当的AVCaptureInputsAVCaptureDeviceInput和输出,比如AVCaptureMovieFileOutput。通过[AVCaptureSession startRunning]开始数据流从输入到输出,和[AVCaptureSession stopRunning]停止输出输入的流动。客户端可以通过设置sessionPreset属性定制录制质量水平或输出的比特率。
  • AVCaptureDevice
    输入设备,包括麦克风、摄像头,通过该对象可以设置物理设备的一些属性(例如相机聚焦、白平衡等)。
  • AVCaptureDeviceInput
    设备输入数据管理对象,可以根据AVCaptureDevice创建对应AVCaptureDeviceInput对象,该对象将会被添加到AVCaptureSession中管理。
  • AVCaptureOutput
    输出数据管理对象,用于接收各类输出数据,通常使用对应的子类AVCaptureAudioDataOutputAVCaptureStillImageOutputAVCaptureVideoDataOutputAVCaptureFileOutput,该对象将会被添加到AVCaptureSession中管理。注意:前面几个对象的输出数据都是NSData类型,而AVCaptureFileOutput代表数据以文件形式输出,类似的,AVCcaptureFileOutput也不会直接创建使用,通常会使用其子类:AVCaptureAudioFileOutput、AVCaptureMovieFileOutput。当把一个输入或者输出添加到AVCaptureSession之后AVCaptureSession就会在所有相符的输入、输出设备之间建立连接(AVCaptionConnection)`。
  • AVCaptureVideoPreviewLayer
    相机拍摄预览图层,是CALayer的子类,使用该对象可以实时查看拍照或视频录制效果,创建该对象需要指定对应的AVCaptureSession对象。

2. 视频录制的步骤

  • 创建AVCaptureSession对象。
  • 使用AVCaptureDevice的静态方法获得需要使用的设备,例如拍照和录像就需要获得摄像头设备,录音就要获得麦克风设备。
  • 利用输入设备AVCaptureDevice初始化AVCaptureDeviceInput对象。
  • 初始化输出数据管理对象,如果要拍照就初始化AVCaptureStillImageOutput对象;如果拍摄视频就初始化AVCaptureMovieFileOutput对象。
  • 将数据输入对象AVCaptureDeviceInput、数据输出对象AVCaptureOutput添加到媒体会话管理对象AVCaptureSession中。
  • 创建视频预览图层AVCaptureVideoPreviewLayer并指定媒体会话,添加图层到显示容器中,调用AVCaptureSession的startRuning方法开始捕获。
  • 将捕获的音频或视频数据输出到指定文件。

3. 代码实现

下面还是直接看代码。

1. JJMoviePreviewVC.m
#import "JJMoviePreviewVC.h"
#import <AVFoundation/AVFoundation.h>
#import "Masonry.h"
#import <AssetsLibrary/AssetsLibrary.h>

@interface JJMoviePreviewVC () <AVCaptureFileOutputRecordingDelegate>

@property (nonatomic, strong) AVCaptureSession *captureSession;
@property (nonatomic, strong) AVCaptureVideoPreviewLayer *previewLayer;
@property (nonatomic, strong) AVCaptureMovieFileOutput *captureMovieFileOutput;
@property (nonatomic, strong) AVCaptureConnection *captureConnection;
@property (nonatomic, strong) AVCaptureDevice *captureVideoDevice;
@property (nonatomic, strong) AVCaptureDeviceInput *captureVideoDeviceInput;
@property (nonatomic, strong) AVCaptureDevice *captureAudioDevice;
@property (nonatomic, strong) AVCaptureDeviceInput *captureAudioDeviceInput;
@property (nonatomic, strong) UIButton *beginButton;
@property (nonatomic, strong) UILabel *timeLabel;
@property (nonatomic, strong) UIButton *replayButton;
@property (nonatomic, strong) UIButton *saveButton;
@property (nonatomic, strong) NSTimer *timer;
@property (nonatomic, assign) NSInteger timerInteger;
@property (nonatomic, strong) NSURL *videoUrl;
@property (nonatomic, assign) BOOL canSave;
@property (nonatomic, strong) AVPlayer *player;
@property (nonatomic, strong) AVPlayerItem *playItem;// 一个媒体资源管理对象,管理者视频的一些基本信息和状态,一个AVPlayerItem对应着一个视频资源
@property (nonatomic, strong) AVPlayerLayer *playerLayer;
@property (nonatomic, assign) BOOL isPlaying;

@end

@implementation JJMoviePreviewVC

- (void)viewDidLoad
{
    [super viewDidLoad];
   
    self.timerInteger = 0;
    self.view.backgroundColor = [UIColor whiteColor];
    
    //获取授权状态
    [self getAuthorizeStatus];
    
    [self setupUI];
}

- (void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];
    
    self.navigationController.navigationBarHidden = YES;
    [self.captureSession startRunning];
}

- (void)viewWillDisappear:(BOOL)animated
{
    [super viewWillDisappear:animated];
    
    self.navigationController.navigationBarHidden = NO;
    
    if ([self.captureSession isRunning]) {
        [self.captureSession stopRunning];
    }
    
    if ([self.timer isValid]) {
        [self.timer invalidate];
        self.timer = nil;
    }
}

#pragma mark - Object Private Function

- (void)beginVideoConfiguration
{
    //开启上下文
    [self addSession];
    
    [self.captureSession beginConfiguration];
    
    //开启视频配置
    [self addVideo];
    
    //开始配置音频
    [self addAudio];
    
    //开始设置预览图层
    [self addPreviewLayer];

    [self.captureSession commitConfiguration];
    
    //开始回话,不等于录制
    [self.captureSession startRunning];
}

- (void)addSession
{
   self.captureSession = [[AVCaptureSession alloc] init];

    if ([self.captureSession canSetSessionPreset:AVCaptureSessionPresetHigh]) {
        self.captureSession.sessionPreset = AVCaptureSessionPresetHigh;
    }
    else {
        self.captureSession.sessionPreset = AVCaptureSessionPreset1280x720;
    }
}

- (void)addVideo
{
    for (AVCaptureDevice *device in [AVCaptureDevice devices]) {
        if ([device hasMediaType:AVMediaTypeVideo]) {
            if (device.position == AVCaptureDevicePositionFront) {
                self.captureVideoDevice = device;
            }
        }
    }
    
    //添加输入设备
    [self addVideoInput];
    
    //添加输出设备
    [self addVideoOutput];
}

- (void)addAudio
{
    NSError *error;
    
    self.captureAudioDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio];
    self.captureAudioDeviceInput = [AVCaptureDeviceInput deviceInputWithDevice:self.captureAudioDevice error:&error];
    if (error) {
        return;
    }
    
    if ([self.captureSession canAddInput:self.captureAudioDeviceInput]) {
        [self.captureSession addInput:self.captureAudioDeviceInput];
    }
}

- (void)addPreviewLayer
{
    self.previewLayer = [AVCaptureVideoPreviewLayer layerWithSession:self.captureSession];

    [self.previewLayer setVideoGravity:AVLayerVideoGravityResizeAspect];
    self.previewLayer.frame = self.view.frame;
    [self.view.layer addSublayer:self.previewLayer];
}

- (void)addVideoInput
{
    NSError *error;
    self.captureVideoDeviceInput = [[AVCaptureDeviceInput alloc] initWithDevice:self.captureVideoDevice error:&error];
    if (error) {
        return;
    }
    
    if ([self.captureSession canAddInput:self.captureVideoDeviceInput]) {
        [self.captureSession addInput:self.captureVideoDeviceInput];
    }
}

- (void)addVideoOutput
{
    self.captureMovieFileOutput = [[AVCaptureMovieFileOutput alloc] init];
    if ([self.captureSession canAddOutput:self.captureMovieFileOutput]) {
        [self.captureSession addOutput:self.captureMovieFileOutput];
    }
    
    //设置链接管理对象
    AVCaptureConnection *captureConnection = [self.captureMovieFileOutput connectionWithMediaType:AVMediaTypeVideo];
    self.captureConnection = captureConnection;
    //视频旋转方向设置
    captureConnection.videoScaleAndCropFactor = captureConnection.videoMaxScaleAndCropFactor;;
    //视频稳定设置
    if ([captureConnection isVideoStabilizationSupported]) {
        captureConnection.preferredVideoStabilizationMode = AVCaptureVideoStabilizationModeAuto;
    }
}

//UI界面初始化

- (void)setupUI
{
    //开始录制按钮
    UIButton *beginButton = [UIButton buttonWithType:UIButtonTypeCustom];
    beginButton.backgroundColor = [UIColor clearColor];
    [beginButton setTitle:@"开始录制" forState:UIControlStateNormal];
    beginButton.layer.borderColor = [UIColor blueColor].CGColor;
    beginButton.layer.borderWidth = 1.0;
    [beginButton setTitleColor:[UIColor yellowColor] forState:UIControlStateNormal];
    [beginButton addTarget:self action:@selector(beginButtonDidClick:) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:beginButton];
    self.beginButton = beginButton;
    
    [beginButton sizeToFit];
    [beginButton mas_makeConstraints:^(MASConstraintMaker *make) {
        make.bottom.equalTo(self.view).offset(-30.0);
        make.centerX.equalTo(self.view);
    }];
    
    //计时标志
    UILabel *timeLabel = [[UILabel alloc] init];
    timeLabel.text = @"0";
    timeLabel.textAlignment = NSTextAlignmentCenter;
    timeLabel.backgroundColor = [UIColor clearColor];
    timeLabel.textColor = [UIColor redColor];
    timeLabel.font = [UIFont boldSystemFontOfSize:20.0];
    [self.view addSubview:timeLabel];
    self.timeLabel = timeLabel;
    
    [self.timeLabel mas_makeConstraints:^(MASConstraintMaker *make) {
        make.centerX.equalTo(self.view);
        make.bottom.equalTo(self.beginButton.mas_top).offset(-15.0);
        make.height.equalTo(@25);
        make.width.equalTo(@120);
    }];
    
    //重播按钮
    UIButton *replayButton = [UIButton buttonWithType:UIButtonTypeCustom];
    replayButton.backgroundColor = [UIColor clearColor];
    [replayButton setTitle:@"预览播放" forState:UIControlStateNormal];
    replayButton.layer.borderColor = [UIColor blueColor].CGColor;
    replayButton.layer.borderWidth = 1.0;
    [replayButton setTitleColor:[UIColor yellowColor] forState:UIControlStateNormal];
    [replayButton addTarget:self action:@selector(replayButtonDidClick) forControlEvents:UIControlEventTouchUpInside];
    replayButton.hidden = YES;
    [self.view addSubview:replayButton];
    self.replayButton = replayButton;
    
    [replayButton sizeToFit];
    [replayButton mas_makeConstraints:^(MASConstraintMaker *make) {
        make.right.equalTo(self.beginButton.mas_left).offset(-30.0);
        make.centerY.equalTo(self.beginButton);
    }];
    
    //保存按钮
    UIButton *saveButton = [UIButton buttonWithType:UIButtonTypeCustom];
    saveButton.backgroundColor = [UIColor clearColor];
    [saveButton setTitle:@"保存" forState:UIControlStateNormal];
    saveButton.layer.borderColor = [UIColor blueColor].CGColor;
    saveButton.layer.borderWidth = 1.0;
    [saveButton setTitleColor:[UIColor yellowColor] forState:UIControlStateNormal];
    [saveButton addTarget:self action:@selector(saveButtonDidClick) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:saveButton];
    self.saveButton = saveButton;
    
    [saveButton sizeToFit];
    [saveButton mas_makeConstraints:^(MASConstraintMaker *make) {
        make.left.equalTo(self.beginButton.mas_right).offset(30.0);
        make.centerY.equalTo(self.beginButton);
    }];
}

//用户权限

- (void)getAuthorizeStatus
{
    //判断照相机和,麦克风权限
    NSString *mediaType = AVMediaTypeVideo;//读取媒体类型
    AVAuthorizationStatus authStatus = [AVCaptureDevice authorizationStatusForMediaType:mediaType];//读取设备授权状态
    if(authStatus == AVAuthorizationStatusRestricted || authStatus == AVAuthorizationStatusDenied){
        NSString *errorStr = @"应用相机权限受限,请在设置中启用";
        NSLog(@"%@", errorStr);
        [self showAlertViewWithMessage:errorStr];
        return;
    }
    
    mediaType = AVMediaTypeAudio;//读取媒体类型
    authStatus = [AVCaptureDevice authorizationStatusForMediaType:mediaType];//读取设备授权状态
    if(authStatus == AVAuthorizationStatusRestricted || authStatus == AVAuthorizationStatusDenied){
        NSString *errorStr = @"麦克风权限受限,请在设置中启用";
        NSLog(@"%@", errorStr);
        [self showAlertViewWithMessage:errorStr];
        return;
    }
    
    [self beginVideoConfiguration];
}

- (void)showAlertViewWithMessage:(NSString *)message
{
    UIAlertController *alertVC = [UIAlertController alertControllerWithTitle:@"提示" message:message preferredStyle:UIAlertControllerStyleAlert];
    UIAlertAction *ensureAction = [UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {

    }];
    
    UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:@"取消" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {

    }];
    
    [alertVC addAction:ensureAction];
    [alertVC addAction:cancelAction];
    
    [self presentViewController:alertVC animated:YES completion:nil];
}

- (void)loadTimer
{
    NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerRun) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
    self.timer = timer;
}

//视频保存

- (void)saveVideo:(NSURL *)outputFileURL
{
    
    //判断是否有相册权限
    ALAuthorizationStatus authorStatus = [ALAssetsLibrary authorizationStatus];
    if (authorStatus == ALAuthorizationStatusRestricted || authorStatus == ALAuthorizationStatusDenied) {
        NSString *errorStr = @"没有使用相册权限,请设置info.plist文件";
        [self showAlertViewWithMessage:errorStr];
        return;
    }

    ALAssetsLibrary *library = [[ALAssetsLibrary alloc] init];
    [library writeVideoAtPathToSavedPhotosAlbum:outputFileURL
                                completionBlock:^(NSURL *assetURL, NSError *error) {
                                    if (error) {
                                        NSLog(@"保存视频失败:%@",error);
                                        [self.saveButton setTitle:@"保存失败" forState: UIControlStateNormal];
                                        [self.saveButton setTitleColor:[UIColor redColor] forState:UIControlStateNormal];
                                        self.replayButton.hidden = YES;
                                    }
                                    else {
                                        NSLog(@"保存视频到相册成功");
                                        [self.saveButton setTitle:@"保存成功" forState: UIControlStateNormal];
                                        [self.saveButton setTitleColor:[UIColor greenColor] forState:UIControlStateNormal];
                                        self.replayButton.hidden = NO;
                                    }
                                }];
}

- (NSURL *)outPutFileURL
{
    return [NSURL fileURLWithPath:[NSString stringWithFormat:@"%@%@", NSTemporaryDirectory(), @"outPut.mov"]];
}

//创建预览视图

- (void)creatPlayView
{
    NSLog(@"%@",self.videoUrl);
    [self.previewLayer removeFromSuperlayer];
    self.playItem = [AVPlayerItem playerItemWithURL:self.videoUrl];
    self.player = [AVPlayer playerWithPlayerItem:self.playItem];
    self.playerLayer = [AVPlayerLayer playerLayerWithPlayer:self.player];
    self.playerLayer.frame = self.view.frame;
    self.playerLayer.videoGravity = AVLayerVideoGravityResizeAspectFill;//视频填充模式
    CALayer *layer = self.view.layer;
    layer.masksToBounds = true;
    
    [layer addSublayer:_playerLayer];
}

#pragma mark - Action && Notification

//开始录制按钮

- (void)beginButtonDidClick:(UIButton *)button
{
    button.enabled = NO;
    
    NSLog(@"开始录制按钮");
    [self loadTimer];

    [self.captureMovieFileOutput startRecordingToOutputFileURL:[self outPutFileURL] recordingDelegate:self];
}

//重播按钮

- (void)replayButtonDidClick
{
    NSLog(@"重新播放按钮");
    
    self.replayButton.enabled = NO;
    
    [self creatPlayView];
    
    [self.view bringSubviewToFront:self.saveButton];
    [self.view bringSubviewToFront:self.replayButton];
    [self.view bringSubviewToFront:self.beginButton];
    
    [self.player play];
}

//保存按钮

- (void)saveButtonDidClick
{
    NSLog(@"保存按钮");
    
    self.saveButton.enabled = NO;
    
    if (self.timer) {
        [self.timer invalidate];
    }
    
    self.canSave = YES;
    [self.captureSession stopRunning];
    [self.captureMovieFileOutput stopRecording];
}

//定时器

- (void)timerRun
{
    NSInteger seconds = self.timerInteger % 60;
    NSInteger minutes = (self.timerInteger / 60) % 60;
    NSInteger hours = self.timerInteger / 3600;
    
    self.timeLabel.text = [NSString stringWithFormat:@"%02ld:%02ld:%02ld",hours, minutes, seconds];
    self.timerInteger ++;
}

#pragma mark - AVCaptureFileOutputRecordingDelegate

//开始录制调用的代理方法

- (void)captureOutput:(AVCaptureFileOutput *)captureOutput didStartRecordingToOutputFileAtURL:(NSURL *)fileURL fromConnections:(NSArray *)connections
{
    NSLog(@"---- 开始录制 ----");
}

//录制结束调用的代理方法

- (void)captureOutput:(AVCaptureFileOutput *)captureOutput didFinishRecordingToOutputFileAtURL:(NSURL *)outputFileURL fromConnections:(NSArray *)connections error:(NSError *)error
{
    NSLog(@"---- 录制结束 ---%@-%@ ",outputFileURL,captureOutput.outputFileURL);
    
    if (outputFileURL.absoluteString.length == 0 && captureOutput.outputFileURL.absoluteString.length == 0 ) {
        return;
    }
    
    if (self.canSave) {
        self.videoUrl = outputFileURL;
        self.canSave = NO;
        [self saveVideo:self.videoUrl];
    }
}

@end

4. 几点说明

第一:权限问题

对于视频录制类的应用都需要相机和麦克风权限,这里还设计到相册,所以这里还多加了一个相册权限,所以工程配置首先需要在info.plist中进行设置。

权限配置

在代码层面还需要加上

    //判断照相机和,麦克风权限
    NSString *mediaType = AVMediaTypeVideo;//读取媒体类型
    AVAuthorizationStatus authStatus = [AVCaptureDevice authorizationStatusForMediaType:mediaType];//读取设备授权状态
    if(authStatus == AVAuthorizationStatusRestricted || authStatus == AVAuthorizationStatusDenied){
        NSString *errorStr = @"应用相机权限受限,请在设置中启用";
        NSLog(@"%@", errorStr);
        [self showAlertViewWithMessage:errorStr];
        return;
    }
    
    mediaType = AVMediaTypeAudio;//读取媒体类型
    authStatus = [AVCaptureDevice authorizationStatusForMediaType:mediaType];//读取设备授权状态
    if(authStatus == AVAuthorizationStatusRestricted || authStatus == AVAuthorizationStatusDenied){
        NSString *errorStr = @"麦克风权限受限,请在设置中启用";
        NSLog(@"%@", errorStr);
        [self showAlertViewWithMessage:errorStr];
        return;
    }
    //判断是否有相册权限
    ALAuthorizationStatus authorStatus = [ALAssetsLibrary authorizationStatus];
    if (authorStatus == ALAuthorizationStatusRestricted || authorStatus == ALAuthorizationStatusDenied) {
        NSString *errorStr = @"没有使用相册权限,请设置info.plist文件";
        [self showAlertViewWithMessage:errorStr];
        return;
    }

第二:保存涉及到的框架

在保存的时候还设计到一个框架#import <AssetsLibrary/AssetsLibrary.h>

具体如下:

#import <AssetsLibrary/ALAsset.h>
#import <AssetsLibrary/ALAssetsFilter.h>
#import <AssetsLibrary/ALAssetsGroup.h>
#import <AssetsLibrary/ALAssetsLibrary.h>
#import <AssetsLibrary/ALAssetRepresentation.h>

第三:配置涉及到的几个枚举

下面看一下配置时设计到的几个枚举。

  • captureSessionsessionPreset
    /**
     *  AVCaptureSessionPresetPhoto
     *  AVCaptureSessionPresetHigh
     *  AVCaptureSessionPresetMedium
     *  AVCaptureSessionPresetLow
     *  AVCaptureSessionPreset320x240
     *  AVCaptureSessionPreset352x288
     *  AVCaptureSessionPreset640x480
     *  AVCaptureSessionPreset960x540
     *  AVCaptureSessionPreset1280x720
     *  AVCaptureSessionPreset1920x1080
     *  AVCaptureSessionPreset3840x2160
     *  AVCaptureSessionPresetiFrame960x540
     *  AVCaptureSessionPresetiFrame1280x720
     */
  • AVCaptureDeviceMediaType
    /**
     AVF_EXPORT NSString *const AVMediaTypeVideo                 NS_AVAILABLE(10_7, 4_0);
     AVF_EXPORT NSString *const AVMediaTypeAudio                 NS_AVAILABLE(10_7, 4_0);
     AVF_EXPORT NSString *const AVMediaTypeText                  NS_AVAILABLE(10_7, 4_0);
     AVF_EXPORT NSString *const AVMediaTypeClosedCaption         NS_AVAILABLE(10_7, 4_0);
     AVF_EXPORT NSString *const AVMediaTypeSubtitle              NS_AVAILABLE(10_7, 4_0);
     AVF_EXPORT NSString *const AVMediaTypeTimecode              NS_AVAILABLE(10_7, 4_0);
     AVF_EXPORT NSString *const AVMediaTypeMetadata              NS_AVAILABLE(10_8, 6_0);
     AVF_EXPORT NSString *const AVMediaTypeMuxed                 NS_AVAILABLE(10_7, 4_0);
     */
  • 相册授权的ALAuthorizationStatus
typedef enum {
    kCLAuthorizationStatusNotDetermined = 0, // 用户尚未做出选择这个应用程序的问候
    kCLAuthorizationStatusRestricted,        // 此应用程序没有被授权访问的照片数据。可能是家长控制权限
    kCLAuthorizationStatusDenied,            // 用户已经明确否认了这一照片数据的应用程序访问
    kCLAuthorizationStatusAuthorized         // 用户已经授权应用访问照片数据
} CLAuthorizationStatus;
  • AVCaptureVideoPreviewLayersetVideoGravity
    /**
     AVLayerVideoGravityResizeAspect
     AVLayerVideoGravityResizeAspectFill
     AVLayerVideoGravityResize
     */

功能效果

下面我们就看一下效果验证。

预览中
开始录制
保存成功
相册查看

可以看见,实现了预览录制保存等功能,点击预览播放还可以播放刚才录制的视频内容。

参考文章

1. 调用系统相机录像,压缩保存到相册(附仿微信视频录制demo)

后记

未完,待续~~~

秋殇

推荐阅读更多精彩内容