版本记录
版本号 | 时间 |
---|---|
V1.0 | 2017.08.08 |
前言
AVFoundation
框架是ios中很重要的框架,所有与视频音频相关的软硬件控制都在这个框架里面,接下来这几篇就主要对这个框架进行介绍和讲解。感兴趣的可以看我上几篇。
1. AVFoundation框架解析(一)—— 基本概览
功能要求
实现视频的预览功能,并可录制保存相册,并支持保存以后重新播放刚才录制内容。
功能实现
1. 几个相关的类
-
AVCaptureSession
媒体(音、视频)捕获会话,负责把捕获的音视频数据输出到输出设备中。一个AVCaptureSession
可以有多个输入输出。AVCaptureSession
是AVFoundation
捕捉类的中心枢纽,在视频捕获时,客户端可以实例化AVCaptureSession
并添加适当的AVCaptureInputs
、AVCaptureDeviceInput
和输出,比如AVCaptureMovieFileOutput
。通过[AVCaptureSession startRunning]
开始数据流从输入到输出,和[AVCaptureSession stopRunning]
停止输出输入的流动。客户端可以通过设置sessionPreset
属性定制录制质量水平或输出的比特率。 -
AVCaptureDevice
输入设备,包括麦克风、摄像头,通过该对象可以设置物理设备的一些属性(例如相机聚焦、白平衡等)。 -
AVCaptureDeviceInput
设备输入数据管理对象,可以根据AVCaptureDevice创建对应AVCaptureDeviceInput对象,该对象将会被添加到AVCaptureSession中管理。 -
AVCaptureOutput
输出数据管理对象,用于接收各类输出数据,通常使用对应的子类AVCaptureAudioDataOutput
、AVCaptureStillImageOutput
、AVCaptureVideoDataOutput
、AVCaptureFileOutput
,该对象将会被添加到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>
第三:配置涉及到的几个枚举
下面看一下配置时设计到的几个枚举。
-
captureSession
的sessionPreset
/**
* AVCaptureSessionPresetPhoto
* AVCaptureSessionPresetHigh
* AVCaptureSessionPresetMedium
* AVCaptureSessionPresetLow
* AVCaptureSessionPreset320x240
* AVCaptureSessionPreset352x288
* AVCaptureSessionPreset640x480
* AVCaptureSessionPreset960x540
* AVCaptureSessionPreset1280x720
* AVCaptureSessionPreset1920x1080
* AVCaptureSessionPreset3840x2160
* AVCaptureSessionPresetiFrame960x540
* AVCaptureSessionPresetiFrame1280x720
*/
-
AVCaptureDevice
的MediaType
/**
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;
-
AVCaptureVideoPreviewLayer
的setVideoGravity
/**
AVLayerVideoGravityResizeAspect
AVLayerVideoGravityResizeAspectFill
AVLayerVideoGravityResize
*/
功能效果
下面我们就看一下效果验证。
可以看见,实现了预览录制保存等功能,点击预览播放还可以播放刚才录制的视频内容。
参考文章
1. 调用系统相机录像,压缩保存到相册(附仿微信视频录制demo)
后记
未完,待续~~~