流媒体开发(四)音视频的录制

在前面几篇文章中,我们介绍了在iOS中如何实现音视频的播放,在本文中,我们将介绍一下在iOS中如何实现音视频的录制功能。

1. 音频录制

在AVFoundation框架中还要一个AVAudioRecorder类专门处理录音操作,它同样支持多种音频格式。与AVAudioPlayer类似,你完全可以将它看成是一个录音机控制类,

下面是AVAudioRecorder常用的属性和方法:

属性 说明
@property(readonly, getter=isRecording) BOOL recording; 是否正在录音,只读
@property(readonly) NSURL *url 录音文件地址,只读
@property(readonly) NSDictionary *settings 录音文件设置,只读
@property(readonly) NSTimeInterval currentTime 录音时长,只读,注意仅仅在录音状态可用
@property(readonly) NSTimeInterval deviceCurrentTime 输入设置的时间长度,只读,注意此属性一直可访问
@property(getter=isMeteringEnabled) BOOL meteringEnabled; 是否启用录音测量,如果启用录音测量可以获得录音分贝等数据信息
@property(nonatomic, copy) NSArray *channelAssignments 当前录音的通道
对象方法 说明
-(instancetype)initWithURL:(NSURL )url settings:(NSDictionary )settings error:(NSError **)outError 录音机对象初始化方法,注意其中的url必须是本地文件url,settings是录音格式、编码等设置
-(BOOL)prepareToRecord 准备录音,主要用于创建缓冲区,如果不手动调用,在调用record录音时也会自动调用
-(BOOL)record 开始录音
-(BOOL)recordAtTime:(NSTimeInterval)time 在指定的时间开始录音,一般用于录音暂停再恢复录音
-(BOOL)recordForDuration:(NSTimeInterval) duration 按指定的时长开始录音
-(BOOL)recordAtTime:(NSTimeInterval)time forDuration:(NSTimeInterval) duration 在指定的时间开始录音,并指定录音时长
-(void)pause; 暂停录音
-(void)stop; 停止录音
-(BOOL)deleteRecording; 删除录音,注意要删除录音此时录音机必须处于停止状态
-(void)updateMeters; 更新测量数据,注意只有meteringEnabled为YES此方法才可用
-(float)peakPowerForChannel:(NSUInteger)channelNumber; 指定通道的测量峰值,注意只有调用完updateMeters才有值
-(float)averagePowerForChannel:(NSUInteger)channelNumber 指定通道的测量平均值,注意只有调用完updateMeters才有值
代理方法 说明
-(void)audioRecorderDidFinishRecording:(AVAudioRecorder *)recorder successfully:(BOOL)flag 完成录音
-(void)audioRecorderEncodeErrorDidOccur:(AVAudioRecorder )recorder error:(NSError )error 录音编码发生错误
AVAudioRecorder很多属性和方法跟AVAudioPlayer都是类似的,但是它的创建有所不同,在创建录音机时除了指定路径外还必须指定录音设置信息,因为录音机必须知道录音文件的格式、采样率、通道数、每个采样点的位数等信息,但是也并不是所有的信息都必须设置,通常只需要几个常用设置。关于录音设置详见帮助文档中的“AV Foundation Audio Settings Constants”。

下面就使用AVAudioRecorder创建一个录音机,实现了录音、暂停、停止、播放等功能,在这个示例中将实行一个完整的录音控制,包括录音、暂停、恢复、停止,同时还会实时展示用户录音的声音波动,当用户点击完停止按钮还会自动播放录音文件。

程序的构建主要分为以下几步:

  • 设置音频会话类型为AVAudioSessionCategoryPlayAndRecord,因为程序中牵扯到录音和播放操作。
  • 创建录音机AVAudioRecorder,指定录音保存的路径并且设置录音属性,注意对于一般的录音文件要求的采样率、位数并不高,需要适当设置以保证录音文件的大小和效果。
  • 设置录音机代理以便在录音完成后播放录音,打开录音测量保证能够实时获得录音时的声音强度。(注意声音强度范围-160到0,0代表最大输入)
  • 创建音频播放器AVAudioPlayer,用于在录音完成之后播放录音。
  • 创建一个定时器以便实时刷新录音测量值并更新录音强度到UIProgressView中显示。
  • 添加录音、暂停、恢复、停止操作,需要注意录音的恢复操作其实是有音频会话管理的,恢复时只要再次调用record方法即可,无需手动管理恢复时间等。

示例代码:

#import "ViewController.h"
#import <AVFoundation/AVFoundation.h>

#define kRecordAudioFile @"myRecord.wav"

@interface ViewController ()<AVAudioRecorderDelegate>

@property (nonatomic,strong) AVAudioRecorder *audioRecorder;//音频录音机
@property (nonatomic,strong) AVAudioPlayer *audioPlayer;//音频播放器,用于播放录音文件
@property (nonatomic,strong) NSTimer *timer;//录音声波监控(注意这里暂时不对播放进行监控)

@property (weak, nonatomic) IBOutlet UIProgressView *audioPower;//音频波动
@property (weak, nonatomic) IBOutlet UIButton *startButton;//开始
@property (weak, nonatomic) IBOutlet UIButton *pauseButton;//暂停
@property (weak, nonatomic) IBOutlet UIButton *resumeButton;//回复
@property (weak, nonatomic) IBOutlet UIButton *stopButton;//停止

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    // 设置音频会话类型为AVAudioSessionCategoryPlayAndRecord,因为程序中牵扯到录音和播放操作。
    [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayAndRecord error:nil];
    [[AVAudioSession sharedInstance] setActive:YES error:nil];

}

- (AVAudioRecorder *)audioRecorder {

    if (!_audioRecorder) {

        // 2. 创建录音机AVAudioRecorder,指定录音保存的路径并且设置录音属性,
        NSURL *fileURL = [self setFilePathForRecorder];// 录音文件保存路径
        NSDictionary *attrbutes = [self setAttributesForRecorder];// 录音属性
        NSError *error = nil;
        _audioRecorder = [[AVAudioRecorder alloc] initWithURL:fileURL settings:attrbutes error:&error];
        if (error) {
            NSLog(@"创建录音机对象时发生错误,错误信息:%@",error.localizedDescription);
            return nil;
        }

        // 设置录音机代理以便在录音完成后播放录音
        _audioRecorder.delegate = self;
        // 打开录音测量保证能够实时获得录音时的声音强度。
        //(注意声音强度范围-160到0,0代表最大输入)
        _audioRecorder.meteringEnabled = YES;// 如果要监控声波则必须设置为YES
    }
    return _audioRecorder;
}

// 设置录音文件的保存路径
- (NSURL *)setFilePathForRecorder {
    /*
     FOUNDATION_EXPORT NSArray<NSString *> *NSSearchPathForDirectoriesInDomains(NSSearchPathDirectory directory, NSSearchPathDomainMask domainMask, BOOL expandTilde);
     该方法用于返回指定范围内的指定名称的目录的路径集合。有三个参数:
     NSSearchPathDirectory类型的enum值,表明我们要搜索的目录名称,比如这里用NSDocumentDirectory表明我们要搜索的是Documents目录。如果我们将其换成NSCachesDirectory就表示我们搜索的是Library/Caches目录。
     NSSearchPathDomainMask类型的enum值,指定搜索范围,这里的NSUserDomainMask表示搜索的范围限制于当前应用的沙盒目录。还可以写成NSLocalDomainMask(表示/Library)、NSNetworkDomainMask(表示/Network)等。
     expandTilde BOOL值,表示是否展开波浪线~。我们知道在iOS中~的全写形式是/User/userName,该值为YES即表示写成全写形式,为NO就表示直接写成“~”。
     */
    NSString *filePath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
    // 构造一个路径,相当于两个字符串拼接起来
    filePath = [filePath stringByAppendingPathComponent:kRecordAudioFile];
    NSURL *fileURL = [NSURL fileURLWithPath:filePath];

    NSLog(@"%@",filePath);

    return fileURL;
}

// 设置录音属性
- (NSDictionary *)setAttributesForRecorder{
    //注意对于一般的录音文件要求的采样率、位数并不高,需要适当设置以保证录音文件的大小和效果。

    NSMutableDictionary *attributes = [[NSMutableDictionary alloc] init];

    //设置录音格式
    [attributes setObject:@(kAudioFormatLinearPCM) forKey:AVFormatIDKey];
    //设置录音采样率,8000是电话采样率,对于一般录音已经够了
    [attributes setObject:@8000 forKey:AVSampleRateKey];
    //设置通道,这里采用单声道
    [attributes setObject:@1 forKey:AVNumberOfChannelsKey];
    //每个采样点位数,分为8、16、24、32
    [attributes setObject:@32 forKey:AVLinearPCMBitDepthKey];
    //是否使用浮点数采样
    [attributes setObject:@(YES) forKey:AVLinearPCMIsFloatKey];

    return attributes;
}

//开始按钮
- (IBAction)start:(UIButton *)sender {
    //判断如果是处于非录音状态,开始录音
    if (![self.audioRecorder isRecording]) {
        [self.audioRecorder record];//首次使用应用时如果调用record方法会询问用户是否允许使用麦克风
#pragma mark - 计时器开启
        self.timer.fireDate = [NSDate distantPast];//past 开启
    }
}

- (NSTimer *)timer {

    if (!_timer) {

        // 创建一个定时器以便实时刷新录音测量值并更新录音强度到UIProgressView中显示。
        _timer = [NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:@selector(timerAction) userInfo:nil repeats:YES];
    }
    return _timer;
}
- (void)timerAction {

    //实时更新测量值
    [self.audioRecorder updateMeters];
    // power   -160 ~ 0
    float power = [self.audioRecorder averagePowerForChannel:0];
    NSLog(@"%f",power);

    //重新设置progressView的值
    CGFloat progress = -power/160.0;
//    NSLog(@"%f",progress);
    [self.audioPower setProgress:1-progress];
}

//暂停按钮
- (IBAction)pause:(UIButton *)sender {
    //判断如果处于录音状态,暂停录音,计时器暂停
    if ([self.audioRecorder isRecording]) {
        [self.audioRecorder pause];
#pragma mark - 计时器暂停
        self.timer.fireDate = [NSDate distantFuture];
    }
}

//恢复录音
- (IBAction)resume:(UIButton *)sender {
    [self start:sender];
}

//停止录音,计时器停止
- (IBAction)stop:(UIButton *)sender {
    [self.audioRecorder stop];
#pragma mark - 计时器停止,progressView值为0
    self.timer.fireDate = [NSDate distantFuture];
//    [self.timer invalidate];// 停止后无法重启
}

#pragma mark - AVAudioRecorderDelegate
/**
 *  录音完成,录音完成后播放录音
 *
 *  @param recorder 录音机对象
 *  @param flag     是否成功
 */
-(void)audioRecorderDidFinishRecording:(AVAudioRecorder *)recorder successfully:(BOOL)flag{
    NSLog(@"Recorder Did Finished");
    if (![self.audioPlayer isPlaying]) {
        [self.audioPlayer play];
    }
}

// 懒加载AVAudioPlayer,即使播放录制的音频
- (AVAudioPlayer *)audioPlayer {

    if (!_audioPlayer) {

        // 创建音频播放器AVAudioPlayer,用于在录音完成之后播放录音。
        _audioPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:[self setFilePathForRecorder] error:nil];
        //循环次数
        _audioPlayer.numberOfLoops=0;
        //准备播放
        [_audioPlayer prepareToPlay];
    }
    return _audioPlayer;
}

@end

2. 视频录制

在上面,我们实现了音频的录制功能,那么怎么实现视频的录制呢?很明显想要实现视频录制,我们需要调用系统硬件的摄像头。当然,调用摄像头不仅仅可以实现视频录制,同样可以实现拍照功能。下面看一下在iOS如何调用设备的摄像头实现视频录制,另外值得强调的是,在iOS中,照片库并不只是照片的集合,同时也包含了视频,录制的视频都可以保存到照片库中。

2.1 利用UIImagePickerController调用摄像头

UIImagePickerController 继承于UINavigationController,是系统支持的用于获取设备上图片和视频的用户界面,用于管理或自定义相册,同时可以用于在App中选择存储的图片和视频。一个UIImagePickerController管理用户交互并且将这些交互结果传递给一个代理对象。该类不能被继承和修改,除了自定义cameraOverlayView外。
其实UIImagePickerController的功能不仅如此,它还可以用来实现拍照和录制视频的功能。在iOS中要拍照和录制视频最简单的方法就是使用UIImagePickerController。

首先看一下这个类常用的属性和方法:

属性 说明
@property(nonatomic)UIImagePickerControllerSourceType sourceType 拾取源类型,sourceType是枚举类型:UIImagePickerControllerSourceTypePhotoLibrary:照片库,默认值UIImagePickerControllerSourceTypeCamera:摄像头UIImagePickerControllerSourceTypeSavedPhotosAlbum:相簿
@property(nonatomic,copy)NSArray *mediaTypes 媒体类型,设置相机支持的类型,拍照和录像.默认情况下此数组包含kUTTypeImage,所以拍照时可以不用设置;但是当要录像的时候必须设置,可以设置为kUTTypeVideo(视频,但不带声音)或者kUTTypeMovie(视频并带有声音)allowsEditing设置当拍照完或在相册选完照片后,是否跳到编辑模式进行图片剪裁。只有当showsCameraControls属性为true时才有效果allowsImageEditing允许用户编辑图片,已弃用
@property(nonatomic)NSTimeInterval videoMaximumDuration 视频最大录制时长,默认为10 s
@property(nonatomic) UIImagePickerControllerQualityType videoQuality 视频质量,枚举类型:UIImagePickerControllerQualityTypeHigh:高清质量UIImagePickerControllerQualityTypeMedium:中等质量,适合WiFi传输UIImagePickerControllerQualityTypeLow:低质量,适合蜂窝网传输UIImagePickerControllerQualityType640x480:640*480UIImagePickerControllerQualityTypeIFrame1280x720:1280*720UIImagePickerControllerQualityTypeIFrame960x540:960*540
@property(nonatomic) BOOL showsCameraControls 是否显示摄像头控制面板,默认为YES,设置拍照时的下方的工具栏是否显示,如果需要自定义拍摄界面,则可把该工具栏隐藏
@property(nonatomic,retain) UIView *cameraOverlayView 摄像头上覆盖的视图,可用通过这个视频来自定义拍照或录像界面
@property(nonatomic) CGAffineTransform cameraViewTransform 摄像头形变设置拍摄时屏幕的view的transform属性,可以实现旋转,缩放功能
@property(nonatomic) UIImagePickerControllerCameraCaptureMode cameraCaptureMode 摄像头捕获模式,捕获模式是枚举类型:UIImagePickerControllerCameraCaptureModePhoto:拍照模式UIImagePickerControllerCameraCaptureModeVideo:视频录制模式
@property(nonatomic) UIImagePickerControllerCameraDevice cameraDevice 设置使用后置摄像头,可以使用前置摄像头摄像头设备,cameraDevice是枚举类型:UIImagePickerControllerCameraDeviceRear:前置摄像头UIImagePickerControllerCameraDeviceFront:后置摄像头
@property(nonatomic) UIImagePickerControllerCameraFlashMode cameraFlashMode 闪光灯模式,枚举类型:UIImagePickerControllerCameraFlashModeOff:关闭闪光灯UIImagePickerControllerCameraFlashModeAuto:闪光灯自动UIImagePickerControllerCameraFlashModeOn:打开闪光灯
类方法 说明
+(BOOL)isSourceTypeAvailable:(UIImagePickerControllerSourceType)sourceType 指定的源类型是否可用,sourceType是枚举类型:UIImagePickerControllerSourceTypePhotoLibrary:照片库UIImagePickerControllerSourceTypeCamera:摄像头UIImagePickerControllerSourceTypeSavedPhotosAlbum:相簿
+(NSArray *)availableMediaTypesForSourceType:(UIImagePickerControllerSourceType)sourceType 指定的源设备上可用的媒体类型,一般就是图片和视频
+(BOOL)isCameraDeviceAvailable:(UIImagePickerControllerCameraDevice)cameraDevice 指定的摄像头是否可用,cameraDevice是枚举类型:UIImagePickerControllerCameraDeviceRear:前置摄像头UIImagePickerControllerCameraDeviceFront:后置摄像头
+(BOOL)isFlashAvailableForCameraDevice:(UIImagePickerControllerCameraDevice)cameraDevice 指定摄像头的闪光灯是否可用
+(NSArray *)availableCaptureModesForCameraDevice:(UIImagePickerControllerCameraDevice)cameraDevice
获得指定摄像头上的可用捕获模式,捕获模式是枚举类型:UIImagePickerControllerCameraCaptureModePhoto:拍照模式UIImagePickerControllerCameraCaptureModeVideo:视频录制模式
对象方法 说明
-(void)takePicture 编程方式拍照
-(BOOL)startVideoCapture 编程方式录制视频
-(void)stopVideoCapture 编程方式停止录制视频
代理方法 说明
-(void)imagePickerController:(UIImagePickerController )picker didFinishPickingMediaWithInfo:(NSDictionary )info 媒体拾取完成
-(void)imagePickerControllerDidCancel:(UIImagePickerController *)picker 取消拾取
扩展方法(主要用于保存照片、视频到相簿) 说明
UIImageWriteToSavedPhotosAlbum(UIImage *image, id completionTarget, SEL completionSelector, void *contextInfo) 保存照片到相簿
UIVideoAtPathIsCompatibleWithSavedPhotosAlbum(NSString *videoPath) 能否将视频保存到相簿
void UISaveVideoAtPathToSavedPhotosAlbum(NSString *videoPath, id completionTarget, SEL completionSelector, void *contextInfo) 保存视频到相簿

注意:

所有含有cameraXXX的属性都必须要sourceType是UIImagePickerControllerSourceTypeCamera时设置才有效果,否则会有异常。

下面我们来看看如何使用UIImagePickerController来实现录制视频的功能。要用UIImagePickerController录制视频通常可以分为如下步骤:

  • 创建UIImagePickerController对象。
  • 指定拾取源,平时选择照片时使用的拾取源是照片库或者相簿,此刻需要指定为摄像头类型。
  • 指定摄像头,前置摄像头或者后置摄像头。
  • 设置媒体类型mediaType,注意如果是录像必须设置,如果是拍照此步骤可以省略,因为mediaType默认包含kUTTypeImage(注意媒体类型定义在MobileCoreServices.framework中)
  • 指定捕获模式,拍照或者录制视频。(视频录制时必须先设置媒体类型再设置捕获模式)
  • 展示UIImagePickerController(通常以模态窗口形式打开)。
  • 拍照和录制视频结束后在代理方法中展示/保存照片或视频。

当然这个过程中有很多细节可以设置,例如是否显示拍照控制面板,拍照后是否允许编辑等等,通过上面的属性/方法列表相信并不难理解。下面就以一个示例展示如何使用UIImagePickerController来拍照和录制视频,下面的程序中只要将_isVideo设置为YES就是视频录制模式,录制完后在主视图控制器中自动播放;如果将_isVideo设置为NO则为拍照模式,拍照完成之后在主视图控制器中显示拍摄的照片:

示例代码:

#import "ViewController.h"
#import <AVFoundation/AVFoundation.h>
#import <MobileCoreServices/MobileCoreServices.h>

@interface ViewController ()<UIImagePickerControllerDelegate,UINavigationControllerDelegate>

@property (nonatomic, strong)UIImagePickerController *imagePicker;// 实现录制视频功能
@property (nonatomic, strong)AVPlayer *avPlayer;// 录制视频后完成播放

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
}

- (IBAction)takeCamera:(id)sender {

    // 展示UIImagePickerController,完成视频录制
    [self presentViewController:self.imagePicker animated:YES completion:nil];
}
- (IBAction)video:(UISwitch *)sender {

    NSLog(@"开启视频录制%d",sender.on);
    self.isVideo = sender.on;//

    self.imagePicker = nil;//重新初始化UIImagePickerController
}

// 懒加载UIImagePickerController
- (UIImagePickerController *)imagePicker {

    if (!_imagePicker) {

        // 1.创建UIImagePickerController对象。
        _imagePicker = [[UIImagePickerController alloc] init];

        // 2.指定拾取源,此刻需要指定为摄像头类型。
        _imagePicker.sourceType = UIImagePickerControllerSourceTypeCamera;

        // 3.指定摄像头,前置摄像头或者后置摄像头。
        _imagePicker.cameraDevice = UIImagePickerControllerCameraDeviceRear;
        //UIImagePickerControllerCameraDeviceFront,// 前置摄像头

        // 4.设置媒体类型mediaType,录像必须设置(注意媒体类型定义在MobileCoreServices.framework中)
        _imagePicker.mediaTypes = @[(NSString *)kUTTypeMovie];
        // 5.指定捕获模式,UIImagePickerControllerCameraCaptureModeVideo(视频录制时必须先设置媒体类型再设置捕获模式)
        _imagePicker.cameraCaptureMode = UIImagePickerControllerCameraCaptureModeVideo;

        // 视频录制的质量
        _imagePicker.videoQuality = UIImagePickerControllerQualityTypeIFrame1280x720;

        _imagePicker.allowsEditing = YES;// 允许编辑
        _imagePicker.delegate = self;// 设置代理,检测操作
    }

    return _imagePicker;
    // 6.展示UIImagePickerController(通常以模态窗口形式打开)。
    // 7.拍照和录制视频结束后在代理方法中展示/保存照片或视频。
}

#pragma mark - UIImagePickerControllerDelegate
// 编辑状态下,如不复写该方法,仍会走下面的代理方法
//- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingImage:(UIImage *)image editingInfo:(nullable NSDictionary<NSString *,id> *)editingInfo NS_DEPRECATED_IOS(2_0, 3_0) {}

- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary<NSString *,id> *)info {

    NSString *mediaType=[info objectForKey:UIImagePickerControllerMediaType];

    if([mediaType isEqualToString:(NSString *)kUTTypeMovie]){//获得的媒体类型为视频,说明为录制视频
        NSLog(@"video...");
        // 获取拍摄的视频路径
        NSURL *url=[info objectForKey:UIImagePickerControllerMediaURL];
        NSString *urlStr=[url path];
        if (UIVideoAtPathIsCompatibleWithSavedPhotosAlbum(urlStr)) {
            //保存视频到相簿,并设置回调
            UISaveVideoAtPathToSavedPhotosAlbum(urlStr, self, @selector(video:didFinishSavingWithError:contextInfo:), nil);

        }
    }
    [self dismissViewControllerAnimated:YES completion:nil];

}

- (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker {

    NSLog(@"cancel");
}

#pragma mark - 文件保存的方法回调
// 视频文件保存
- (void)video:(NSString *)videoPath didFinishSavingWithError:(NSError *)error contextInfo:(void *)contextInfo {

    if (error) {
        NSLog(@"保存视频过程中发生错误,错误信息:%@",error.localizedDescription);
    }else{

        NSLog(@"视频保存成功.");
        //录制完之后自动播放
        NSURL *url=[NSURL fileURLWithPath:videoPath];
        _avPlayer =[AVPlayer playerWithURL:url];
        AVPlayerLayer *playerLayer=[AVPlayerLayer playerLayerWithPlayer:_avPlayer];
        playerLayer.frame=self.photoView.frame;
        [self.photoView.layer addSublayer:playerLayer];
        [_avPlayer play];

    }
}

@end

2.2 通过AVFoundation框架调用摄像头

如果你想要更多关于处理捕获视频的方法,而这些方法是 UIImagePickerController 所不能提供的,那么你需要使用 AVFoundation。
AVFoundation中提供了很多现成的播放器和录音机,但是事实上它还有更加底层的内容可以供开发者使用。因为AVFoundation中抽了很多和底层输入、输出设备打交道的类,依靠这些类开发人员面对的不再是封装好的音频播放器AVAudioPlayer、录音机(AVAudioRecorder)、视频(包括音频)播放器AVPlayer,而是输入设备(例如麦克风、摄像头)、输出设备(图片、视频)等。首先了解一下使用AVFoundation做拍照和视频录制开发用到的相关类:

  • AVCaptureSession:媒体(音、视频)捕获会话,负责把捕获的音视频数据输出到输出设备中。一个AVCaptureSession可以有多个输入输出,它负责调配影音输入与输出之间的数据流:
    这里写图片描述
  • AVCaptureDevice:输入设备,包括麦克风、摄像头,通过该对象可以设置物理设备的一些属性(例如相机聚焦、白平衡等)。

  • AVCaptureDeviceInput:设备输入数据管理对象,可以根据AVCaptureDevice创建对应的AVCaptureDeviceInput对象,该对象将会被添加到AVCaptureSession中管理。

  • AVCaptureOutput:输出数据管理对象,用于接收各类输出数据,通常使用对应的子类AVCaptureAudioDataOutput、AVCaptureStillImageOutput、AVCaptureVideoDataOutput、AVCaptureFileOutput,该对象将会被添加到AVCaptureSession中管理。注意:前面几个对象的输出数据都是NSData类型,而AVCaptureFileOutput代表数据以文件形式输出,类似的,AVCcaptureFileOutput也不会直接创建使用,通常会使用其子类:AVCaptureAudioFileOutput、AVCaptureMovieFileOutput。当把一个输入或者输出添加到AVCaptureSession之后AVCaptureSession就会在所有相符的输入、输出设备之间建立连接(AVCaptionConnection):
    这里写图片描述

  • AVCaptureVideoPreviewLayer:相机拍摄预览图层,是CALayer的子类,使用该对象可以实时查看拍照或视频录制效果,创建该对象需要指定对应的AVCaptureSession对象。

使用AVFoundation录制视频的一般步骤如下:

  1. 创建AVCaptureSession对象。
  2. 使用AVCaptureDevice的静态方法获得需要使用的设备,例如拍照和录像就需要获得摄像头设备,录音就要获得麦克风设备。
  3. 利用输入设备AVCaptureDevice初始化AVCaptureDeviceInput对象。
  4. 初始化输出数据管理对象,创建一个视频频播放文件输出对象AVCaptureMovieFileOutput
  5. 将数据输入对象AVCaptureDeviceInput、数据输出对象AVCaptureOutput添加到媒体会话管理对象AVCaptureSession中。同时添加一个音频输入到会话(使用[[AVCaptureDevice devicesWithMediaType:AVMediaTypeAudio] firstObject]获得输入设备,然后根据此输入设备创建一个设备输入对象),
  6. 创建视频预览图层AVCaptureVideoPreviewLayer并指定媒体会话,添加图层到显示容器中,调用AVCaptureSession的startRuning方法开始捕获。
  7. 将捕获到的视频数据写入到临时文件并在停止录制之后保存到相簿(通过AVCaptureMovieFileOutput的代理方法)。

当然为了让程序更加完善在下面的视频录制程序中加入了屏幕旋转视频、自动布局和后台保存任务等细节。

示例代码:

#import "ViewController.h"
#import <AVFoundation/AVFoundation.h>
#import <AssetsLibrary/AssetsLibrary.h>
typedef void(^PropertyChangeBlock)(AVCaptureDevice *captureDevice);

@interface ViewController ()<AVCaptureFileOutputRecordingDelegate>//视频文件输出代理

@property (strong,nonatomic) AVCaptureSession *captureSession;// 负责输入和输出设置之间的数据传递
@property (strong,nonatomic) AVCaptureDeviceInput *captureDeviceInput;// 负责从AVCaptureDevice获得输入数据
@property (strong,nonatomic) AVCaptureMovieFileOutput *captureMovieFileOutput;// 视频输出流,录制视频需要使用视频输出流而不是图片
@property (strong,nonatomic) AVCaptureVideoPreviewLayer *captureVideoPreviewLayer;// 相机拍摄预览图层

@property (assign,nonatomic) BOOL enableRotation;// 是否允许旋转(注意在视频录制过程中禁止屏幕旋转)
@property (assign,nonatomic) CGRect *lastBounds;// 旋转的前大小
@property (assign,nonatomic) UIBackgroundTaskIdentifier backgroundTaskIdentifier;// 后台任务标识

@property (weak, nonatomic) IBOutlet UIView *viewContainer;// 视图容器
@property (weak, nonatomic) IBOutlet UIButton *takeButton;// 视频录制按钮
@property (weak, nonatomic) IBOutlet UIImageView *focusCursor;// 聚焦光标

@end

@implementation ViewController

#pragma mark - 控制器视图方法
- (void)viewDidLoad {
    [super viewDidLoad];
}

-(void)viewWillAppear:(BOOL)animated{
    [super viewWillAppear:animated];

    // 1.初始化会话
    _captureSession = [[AVCaptureSession alloc]init];
    if ([_captureSession canSetSessionPreset:AVCaptureSessionPreset1280x720]) {//设置分辨率
        _captureSession.sessionPreset = AVCaptureSessionPreset1280x720;
    }

    // 2.获得输入设备
    AVCaptureDevice *captureDevice;
    NSArray *cameras = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo];
    for (AVCaptureDevice *camera in cameras) {
        if ([camera position] == AVCaptureDevicePositionBack) {
            captureDevice = camera;// 取得后置摄像头
        }
    }
    if (!captureDevice) {
        NSLog(@"取得后置摄像头时出现问题.");
        return;
    }



    // 3.根据输入设备初始化设备输入对象,用于获得输入数据
    NSError *error = nil;
    _captureDeviceInput = [[AVCaptureDeviceInput alloc]initWithDevice:captureDevice error:&error];
    if (error) {
        NSLog(@"取得设备输入对象时出错,错误原因:%@",error.localizedDescription);
        return;
    }

    // 额外需要添加一个音频输入设备
    AVCaptureDevice *audioCaptureDevice = [[AVCaptureDevice devicesWithMediaType:AVMediaTypeAudio] firstObject];
    AVCaptureDeviceInput *audioCaptureDeviceInput = [[AVCaptureDeviceInput alloc] initWithDevice:audioCaptureDevice error:&error];
    if (error) {
        NSLog(@"取得设备输入对象时出错,错误原因:%@",error.localizedDescription);
        return;
    }

    // 4.初始化设备输出对象,用于获得输出数据
    _captureMovieFileOutput = [[AVCaptureMovieFileOutput alloc]init];

    // 5.将数据输入对象AVCaptureDeviceInput、数据输出对象AVCaptureOutput添加到媒体会话管理对象AVCaptureSession中。
    // 将设备输入添加到会话中
    if ([_captureSession canAddInput:_captureDeviceInput]) {

        [_captureSession addInput:_captureDeviceInput];
        [_captureSession addInput:audioCaptureDeviceInput];

        // 获取设备连接
        AVCaptureConnection *captureConnection=[_captureMovieFileOutput connectionWithMediaType:AVMediaTypeVideo];
        // 设备连接是否支持防抖动模式
        if ([captureConnection isVideoStabilizationSupported]) {
            // 设置防抖动模式
            captureConnection.preferredVideoStabilizationMode = AVCaptureVideoStabilizationModeAuto;
        }
    }
    // 将设备输出添加到会话中
    if ([_captureSession canAddOutput:_captureMovieFileOutput]) {
        [_captureSession addOutput:_captureMovieFileOutput];
    }

    // 6.创建视频预览层,用于实时展示摄像头状态
    _captureVideoPreviewLayer=[[AVCaptureVideoPreviewLayer alloc]initWithSession:self.captureSession];
    CALayer *layer=self.viewContainer.layer;
    layer.masksToBounds=YES;
    _captureVideoPreviewLayer.frame = layer.bounds;
    _captureVideoPreviewLayer.videoGravity =AVLayerVideoGravityResize;//填充模式
    //将视频预览层添加到界面中,在取聚焦光标
    [layer insertSublayer:_captureVideoPreviewLayer below:self.focusCursor.layer];



    _enableRotation = YES;// 允许旋转
    [self addGenstureRecognizer];// 添加手势做聚焦设置
    //添加通知,监听输入设备状态
    [self addNotificationToCaptureDevice:captureDevice];

}

//在控制器视图展示和视图离开界面时启动、停止会话。
-(void)viewDidAppear:(BOOL)animated{
    [super viewDidAppear:animated];
    [self.captureSession startRunning];
}
-(void)viewDidDisappear:(BOOL)animated{
    [super viewDidDisappear:animated];
    [self.captureSession stopRunning];
}
#pragma mark - 手势
/**
 *  添加点按手势,点按时聚焦
 */
-(void)addGenstureRecognizer{
    UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc]initWithTarget:self action:@selector(tapScreen:)];
    [self.viewContainer addGestureRecognizer:tapGesture];
}
-(void)tapScreen:(UITapGestureRecognizer *)tapGesture{
    CGPoint point= [tapGesture locationInView:self.viewContainer];
    //将UI坐标转化为摄像头坐标
    CGPoint cameraPoint= [self.captureVideoPreviewLayer captureDevicePointOfInterestForPoint:point];
    [self setFocusCursorWithPoint:point];
    [self focusWithMode:AVCaptureFocusModeAutoFocus exposureMode:AVCaptureExposureModeAutoExpose atPoint:cameraPoint];
}
/**
 *  设置聚焦光标位置
 *
 *  @param point 光标位置
 */
-(void)setFocusCursorWithPoint:(CGPoint)point{
    self.focusCursor.center=point;
    self.focusCursor.transform=CGAffineTransformMakeScale(1.5, 1.5);
    self.focusCursor.alpha=1.0;
    [UIView animateWithDuration:1.0 animations:^{
        self.focusCursor.transform=CGAffineTransformIdentity;
    } completion:^(BOOL finished) {
        self.focusCursor.alpha=0;

    }];
}
/**
 *  设置聚焦点
 *
 *  @param point 聚焦点
 */
-(void)focusWithMode:(AVCaptureFocusMode)focusMode exposureMode:(AVCaptureExposureMode)exposureMode atPoint:(CGPoint)point{
    [self changeDeviceProperty:^(AVCaptureDevice *captureDevice) {
        if ([captureDevice isFocusModeSupported:focusMode]) {
            [captureDevice setFocusMode:AVCaptureFocusModeAutoFocus];
        }
        if ([captureDevice isFocusPointOfInterestSupported]) {
            [captureDevice setFocusPointOfInterest:point];
        }
        if ([captureDevice isExposureModeSupported:exposureMode]) {
            [captureDevice setExposureMode:AVCaptureExposureModeAutoExpose];
        }
        if ([captureDevice isExposurePointOfInterestSupported]) {
            [captureDevice setExposurePointOfInterest:point];
        }
    }];
}
#pragma mark - 通知
/**
 *  给输入设备添加通知
 */
-(void)addNotificationToCaptureDevice:(AVCaptureDevice *)captureDevice{
    //注意添加区域改变捕获通知必须首先设置设备允许捕获
    [self changeDeviceProperty:^(AVCaptureDevice *captureDevice) {
        captureDevice.subjectAreaChangeMonitoringEnabled=YES;
    }];
    NSNotificationCenter *notificationCenter= [NSNotificationCenter defaultCenter];
    //捕获区域发生改变
    [notificationCenter addObserver:self selector:@selector(areaChange:) name:AVCaptureDeviceSubjectAreaDidChangeNotification object:captureDevice];
}
-(void)removeNotificationFromCaptureDevice:(AVCaptureDevice *)captureDevice{
    NSNotificationCenter *notificationCenter= [NSNotificationCenter defaultCenter];
    [notificationCenter removeObserver:self name:AVCaptureDeviceSubjectAreaDidChangeNotification object:captureDevice];
}
// 捕获区域改变
-(void)areaChange:(NSNotification *)notification{
    NSLog(@"捕获区域改变...");
}

-(void)dealloc{
    NSNotificationCenter *notificationCenter= [NSNotificationCenter defaultCenter];
    [notificationCenter removeObserver:self];
}

#pragma mark - 屏幕旋转
//屏幕旋转时调整视频预览图层的方向
-(void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration {
    AVCaptureConnection *captureConnection=[self.captureVideoPreviewLayer connection];
    captureConnection.videoOrientation=(AVCaptureVideoOrientation)toInterfaceOrientation;
}
//旋转后重新设置大小
-(void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation{
    _captureVideoPreviewLayer.frame=self.viewContainer.bounds;
}

#pragma mark - UI方法
#pragma mark 视频录制
- (IBAction)takeButtonClick:(UIButton *)sender {
    // 根据设备输出获得连接
    AVCaptureConnection *captureConnection=[self.captureMovieFileOutput connectionWithMediaType:AVMediaTypeVideo];

    // 根据连接取得设备输出的数据
    // 判断视频的输出源是否处在录制状态,是的话停止录制
    if (![self.captureMovieFileOutput isRecording]) {
        self.enableRotation=NO;// 禁止旋转
        // 如果支持多任务则则开始多任务
        if ([[UIDevice currentDevice] isMultitaskingSupported]) {
            self.backgroundTaskIdentifier=[[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:nil];
        }
        // 设置预览图层和视频方向保持一致
        captureConnection.videoOrientation = [self.captureVideoPreviewLayer connection].videoOrientation;
        // 设置视频文件的存储路径
        NSString *outputFielPath=[NSTemporaryDirectory() stringByAppendingString:@"myMovie.mov"];
        NSLog(@"save path is :%@",outputFielPath);
        NSURL *fileUrl=[NSURL fileURLWithPath:outputFielPath];
        NSLog(@"fileUrl:%@",fileUrl);
        // 开始录制视频并设置录制代理
        [self.captureMovieFileOutput startRecordingToOutputFileURL:fileUrl recordingDelegate:self];
    }
    else{
        [self.captureMovieFileOutput stopRecording];//停止录制
    }
}
#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(@"视频录制完成.");
    //视频录入完成之后在后台将视频存储到相簿
    self.enableRotation=YES;
    UIBackgroundTaskIdentifier lastBackgroundTaskIdentifier = self.backgroundTaskIdentifier;// 获取最后的后台任务标识
    self.backgroundTaskIdentifier = UIBackgroundTaskInvalid;// 设置之前的后台任务无效

    ALAssetsLibrary *assetsLibrary=[[ALAssetsLibrary alloc]init];
    [assetsLibrary writeVideoAtPathToSavedPhotosAlbum:outputFileURL completionBlock:^(NSURL *assetURL, NSError *error) {
        if (error) {
            NSLog(@"保存视频到相簿过程中发生错误,错误信息:%@",error.localizedDescription);
        }
        NSLog(@"outputUrl:%@",outputFileURL);
        [[NSFileManager defaultManager] removeItemAtURL:outputFileURL error:nil];
        if (lastBackgroundTaskIdentifier!=UIBackgroundTaskInvalid) {
            [[UIApplication sharedApplication] endBackgroundTask:lastBackgroundTaskIdentifier];
        }
        NSLog(@"成功保存视频到相簿.");
    }];

}

#pragma mark 切换前后摄像头
- (IBAction)toggleButtonClick:(UIButton *)sender {

    // 获取输入设备
    AVCaptureDevice *currentDevice=[self.captureDeviceInput device];
    // 获取原有输入设备位置
    AVCaptureDevicePosition currentPosition=[currentDevice position];
    // 移除对原设备的通知
    [self removeNotificationFromCaptureDevice:currentDevice];

    // 获取新的设备
    AVCaptureDevice *toChangeDevice;
    AVCaptureDevicePosition toChangePosition=AVCaptureDevicePositionFront;
    if (currentPosition==AVCaptureDevicePositionUnspecified||currentPosition==AVCaptureDevicePositionFront) {
        toChangePosition=AVCaptureDevicePositionBack;
    }
    NSArray *cameras= [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo];
    for (AVCaptureDevice *camera in cameras) {
        if ([camera position]==toChangePosition) {
            toChangeDevice = camera;
        }
    }
    [self addNotificationToCaptureDevice:toChangeDevice];

    //获得要调整的设备输入对象
    AVCaptureDeviceInput *toChangeDeviceInput=[[AVCaptureDeviceInput alloc]initWithDevice:toChangeDevice error:nil];

    //改变会话的配置前一定要先开启配置,配置完成后提交配置改变
    [self.captureSession beginConfiguration];
    //移除原有输入对象
    [self.captureSession removeInput:self.captureDeviceInput];
    //添加新的输入对象
    if ([self.captureSession canAddInput:toChangeDeviceInput]) {
        [self.captureSession addInput:toChangeDeviceInput];
        self.captureDeviceInput=toChangeDeviceInput;
    }
    //提交会话配置
    [self.captureSession commitConfiguration];

}

#pragma mark - 私有方法
/**
 *  改变设备属性的统一操作方法
 *
 *  @param propertyChange 属性改变操作
 */
-(void)changeDeviceProperty:(PropertyChangeBlock)propertyChange{
    AVCaptureDevice *captureDevice= [self.captureDeviceInput device];
    NSError *error;
    //注意改变设备属性前一定要首先调用lockForConfiguration:调用完之后使用unlockForConfiguration方法解锁
    if ([captureDevice lockForConfiguration:&error]) {
        propertyChange(captureDevice);
        [captureDevice unlockForConfiguration];
    }else{
        NSLog(@"设置设备属性过程发生错误,错误信息:%@",error.localizedDescription);
    }
}

@end

猜你喜欢

转载自blog.csdn.net/qq_32510689/article/details/51352256