iOS-直播开发(开发从底层做起)之音视频采集

timg.jpeg

由于我们公司不是专门做直播的, 所以研究直播开发完全处于兴趣爱好,可能很多地方用处理的不是很周到, 所以, 希望大家多提提意见, 互相学习一下哈!

这里附上我写的第一篇直播开发的文章传送门
iOS-直播开发(开发从底层做起)

好啦, 废话不多说, 直奔主题! 本篇文章是针对直播开发中的第一部分, 音视频采集! 用的是iOS 原生的AVFoundation框架!
Demo传送门GitHub

实现的效果图


图片发自简书App

1. 所使用的系统类

AVCaptureSession *session;    // 音视频管理对象
AVCaptureDevice *videoDevice; // 视频设备对象 (用来操作闪光灯, 聚焦, 摄像头切换等)
AVCaptureDevice *audioDevice; // 音频设备对象
AVCaptureDeviceInput *videoInput;         // 视频输入对象
AVCaptureDeviceInput *audioInput;         // 音频输入对象
AVCaptureVideoDataOutput *videoOutput;    // 视频输出对象
AVCaptureAudioDataOutput *audioOutput;    // 音频输出对象
AVCaptureVideoPreviewLayer *preViewLayer; // 用来展示视频的layer对象

2. 封装音视频采集类

为了方便后边的使用, 我们把音视频采集这个功能单独封装成一个类, 这里封装成 JFCaptureSession


JFCaptureSession.h
typedef NS_ENUM(NSUInteger, JFCaptureSessionPreset){
    /// 低分辨率
    JFCaptureSessionPreset368x640 = 0,
    /// 中分辨率
    JFCaptureSessionPreset540x960 = 1,
    /// 高分辨率
    JFCaptureSessionPreset720x1280 = 2
};

这个枚举是来初始化JFCaptureSession 该类对象的时候需要传的一个枚举值, 来制定视频采集的分辨率, 有三个枚举值

JFCaptureSessionPreset368x640  //该枚举值是分辨率最低的, 基本上所有的机型都支持该分辨率
JFCaptureSessionPreset720x1280 //而这个枚举值分辨率比较高, 可能有些机型不支持该分辨率, .m中的实现有判断, 如果不支持该分辨率, 则会降一级

.h中的另一个枚举 该枚举用来操控前后摄像头的

// 摄像头方向
typedef NS_ENUM(NSInteger, JFCaptureDevicePosition) {
    JFCaptureDevicePositionFront = 0,  // 前置摄像头
    JFCaptureDevicePositionBack        // 后置摄像头
};

然后就是JFCaptureSession 的代理 JFCaptureSessionDelegate, 用来回调采集的音视频帧数据 CMSampleBufferRef

/** 视频取样数据回调 */
- (void)videoCaptureOutputWithSampleBuffer:(CMSampleBufferRef)sampleBuffer;
/** 音频取样数据回调 */
- (void)audioCaptureOutputWithSampleBuffer:(CMSampleBufferRef)sampleBuffer;

JFCaptureSession 该类的初始化方法, 初始化的时候需要传一分辨率的枚举值, 来设置要采集视频的分辨率

- (instancetype)defaultJFCaptureSessionWithSessionPreset:(JFCaptureSessionPreset)sessionPreset;
@property (nonatomic, strong) UIView *preView;      // 用来展示视频图像
@property (nonatomic, assign) JFCaptureDevicePosition videoDevicePosition;    // 先后摄像头切换
@property (nonatomic, assign) id <JFCaptureSessionDelegate> delegate;  // 代理

开始采集, 暂停采集

/**
 开始
 */
- (void)startRunning;
/**
 暂停
 */
- (void)stopRunning;

JFCaptureSession.m 集体实现音视频采集的方法
// 初始化方法
- (instancetype)defaultJFCaptureSessionWithSessionPreset:(JFCaptureSessionPreset)sessionPreset {
    if ([super init]) {
        self.sessionPreset = sessionPreset;
        // 初始化Session
        [self initAVCaptureSession];
    }
    return self;
}
- (void)initAVCaptureSession {
    // 初始化
    self.session = [[AVCaptureSession alloc] init];
    // 设置录像的分辨率
    [self.session canSetSessionPreset:[self supportSessionPreset]];
/** 注意: 配置AVCaptureSession 的时候, 必须先开始配置, beginConfiguration, 配置完成, 必须提交配置 commitConfiguration, 否则配置无效  **/
    // 开始配置
    [self.session beginConfiguration];
    // 设置视频 I/O 对象 并添加到session
    [self videoInputAndOutput];
    // 设置音频 I/O 对象 并添加到session
    [self audioInputAndOutput];
    // 提交配置
    [self.session commitConfiguration];
}
// 设置视频 I/O 对象
- (void)videoInputAndOutput {
     NSError *error;
    // 初始化视频设备对象
    self.videoDevice = nil;
    // 创建摄像头类型数组 (前置, 和后置摄像头之分)
    NSArray *devices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo];
    // 便利获取的所有支持的摄像头类型
    for (AVCaptureDevice *devcie in devices) {
        // 默然先开启前置摄像头
        if (devcie.position == AVCaptureDevicePositionFront) {
            self.videoDevice = devcie;
        }
    }
    // 视频输入
    // 根据视频设备来初始化输入对象
    self.videoInput = [[AVCaptureDeviceInput alloc] initWithDevice:self.videoDevice error:&error];
    if (error) {
        NSLog(@"== 摄像头错误 ==");
        return;
    }
    // 将输入对象添加到管理者 AVCaptureSession 中
    // 需要先判断是否能够添加输入对象
    if ([self.session canAddInput:self.videoInput]) {
        // 可以添加, 才能添加
        [self.session addInput:self.videoInput];
    }
    // 视频输出对象
    self.videoOutput = [[AVCaptureVideoDataOutput alloc] init];
    // 是否允许卡顿时丢帧
    self.videoOutput.alwaysDiscardsLateVideoFrames = NO;
    if ([self supportsFastTextureUpload]) {
        // 是否支持全频色彩编码 YUV 一种色彩编码方式, 即YCbCr, 现在视频一般采用该颜色空间, 可以分离亮度跟色彩, 在不影响清晰度的情况下来压缩视频
        BOOL supportFullYUVRange = NO;
        // 获取输出对象所支持的像素格式
        NSArray *supportedPixelFormats = self.videoOutput.availableVideoCVPixelFormatTypes;
        for (NSNumber *currentPixelFormat in supportedPixelFormats) {
            if ([currentPixelFormat integerValue] == kCVPixelFormatType_420YpCbCr8BiPlanarFullRange) {
                supportFullYUVRange = YES;
            }
        }
        // 根据是否支持全频色彩编码 YUV 来设置输出对象的视频像素压缩格式
        if (supportFullYUVRange) {
            [self.videoOutput setVideoSettings:[NSDictionary dictionaryWithObject:[NSNumber numberWithInt:kCVPixelFormatType_420YpCbCr8BiPlanarFullRange] forKey:(id)kCVPixelBufferPixelFormatTypeKey]];
        } else {
            [self.videoOutput setVideoSettings:[NSDictionary dictionaryWithObject:[NSNumber numberWithInt:kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange] forKey:(id)kCVPixelBufferPixelFormatTypeKey]];
        }
    } else {
        [self.videoOutput setVideoSettings:[NSDictionary dictionaryWithObject:[NSNumber numberWithInt:kCVPixelFormatType_32BGRA] forKey:(id)kCVPixelBufferPixelFormatTypeKey]];
    }
    // 创建设置代理是所需要的线程队列 优先级设为高
    dispatch_queue_t videoQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
    // 设置代理
    [self.videoOutput setSampleBufferDelegate:self queue:videoQueue];
    // 判断session 是否可添加视频输出对象
    if ([self.session canAddOutput:self.videoOutput]) {
        [self.session addOutput:self.videoOutput];
        // 链接视频 I/O 对象
        [self connectionVideoInputVideoOutput];
    }
}
// 设置音频I/O 对象
- (void)audioInputAndOutput {
    NSError *error;
    // 初始音频设备对象
    self.audioDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio];
    // 音频输入对象
    self.audioInput = [[AVCaptureDeviceInput alloc] initWithDevice:self.audioDevice error:&error];
    if (error) {
        NSLog(@"== 录音设备出错");
    }
    // 判断session 是否可以添加 音频输入对象
    if ([self.session canAddInput:self.audioInput]) {
        [self.session addInput:self.audioInput];
    }
    // 音频输出对象
    self.audioOutput = [[AVCaptureAudioDataOutput alloc] init];
    // 判断是否可以添加音频输出对象
    if ([self.session canAddOutput:self.audioOutput]) {
        [self.session addOutput:self.audioOutput];
    }
    // 创建设置音频输出代理所需要的线程队列
    dispatch_queue_t audioQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0);
    [self.audioOutput setSampleBufferDelegate:self queue:audioQueue];
}
// 链接 视频 I/O 对象
- (void)connectionVideoInputVideoOutput {
    // AVCaptureConnection是一个类,用来在AVCaptureInput和AVCaptureOutput之间建立连接。AVCaptureSession必须从AVCaptureConnection中获取实际数据。
    AVCaptureConnection *connection = [self.videoOutput connectionWithMediaType:AVMediaTypeVideo];
    // 设置视频的方向, 如果不设置的话, 视频默认是旋转 90°的
    connection.videoOrientation = AVCaptureVideoOrientationPortrait;
    // 设置视频的稳定性, 先判断connection 连接对象是否支持 视频稳定
    if ([connection isVideoStabilizationSupported]) {
        connection.preferredVideoStabilizationMode = AVCaptureVideoStabilizationModeAuto;
    }
    // 缩放裁剪系数, 设为最大
    connection.videoScaleAndCropFactor = connection.videoMaxScaleAndCropFactor;
}
// 判断是否支持设置的分辨率, 如果不支持, 默认降一级, 还不支持, 设为默认
- (NSString *)supportSessionPreset {
    if (![self.session canSetSessionPreset:self.avPreset]) {
        self.sessionPreset = JFCaptureSessionPreset540x960;
        if (![self.session canSetSessionPreset:self.avPreset]) {
            self.sessionPreset = JFCaptureSessionPreset368x640;
        }
    } else {
        self.sessionPreset = JFCaptureSessionPreset368x640;
    }
    return self.avPreset;
}
#pragma mark - Setter
- (void)setSessionPreset:(JFCaptureSessionPreset)sessionPreset {
    _sessionPreset = sessionPreset;
}
// 根据视频分辨率, 设置具体对应的类型
- (NSString *)avPreset {
    switch (self.sessionPreset) {
        case JFCaptureSessionPreset368x640:
            _avPreset = AVCaptureSessionPreset640x480;
            break;
        case JFCaptureSessionPreset540x960:
            _avPreset = AVCaptureSessionPresetiFrame960x540;
            break;
        case JFCaptureSessionPreset720x1280:
            _avPreset = AVCaptureSessionPreset1280x720;
            break;
        default:
            _avPreset = AVCaptureSessionPreset640x480;
            break;
    }
    return _avPreset;
}
// 摄像头切换
- (void)setVideoDevicePosition:(JFCaptureDevicePosition)videoDevicePosition {
    if (_videoDevicePosition != videoDevicePosition) {
        _videoDevicePosition = videoDevicePosition;
        if (_videoDevicePosition == JFCaptureDevicePositionFront) {
            self.videoDevice = [self deviceWithMediaType:AVMediaTypeVideo preferringPosition:AVCaptureDevicePositionFront];
        } else {
            self.videoDevice = [self deviceWithMediaType:AVMediaTypeVideo preferringPosition:AVCaptureDevicePositionBack];
        }
        [self changeDevicePropertySafety:^(AVCaptureDevice *captureDevice) {
            NSError *error;
            AVCaptureDeviceInput *newVideoInput = [[AVCaptureDeviceInput alloc] initWithDevice:_videoDevice error:&error];
            if (newVideoInput != nil) {
                //必选先 remove 才能询问 canAdd
                [self.session removeInput:_videoInput];
                if ([self.session canAddInput:newVideoInput]) {
                    [self.session addInput:newVideoInput];
                    _videoInput = newVideoInput;
                }else{
                    [self.session addInput:_videoInput];
                }
            } else if (error) {
                NSLog(@"切换前/后摄像头失败, error = %@", error);
            }
        }];   
    }
}
// 获取需要的设备对象
- (AVCaptureDevice *)deviceWithMediaType:(NSString *)mediaType preferringPosition:(AVCaptureDevicePosition)position {
    // 获取所有类型的摄像头设备
    NSArray *devices = [AVCaptureDevice devicesWithMediaType:mediaType];
    AVCaptureDevice *captureDevice = devices.firstObject;   // 先初始化一个设备对象并赋初值
    // 便利获取需要的设备
    for (AVCaptureDevice *device in devices) {
        if (device.position == position) {
           captureDevice = device;
            break;
        }
    }
    return captureDevice;
}
#pragma mark  更改设备属性前一定要锁上
-(void)changeDevicePropertySafety:(void (^)(AVCaptureDevice *captureDevice))propertyChange{
    //也可以直接用_videoDevice,但是下面这种更好
    AVCaptureDevice *captureDevice= [_videoInput device];
    NSError *error;
    //注意改变设备属性前一定要首先调用lockForConfiguration:调用完之后使用unlockForConfiguration方法解锁,意义是---进行修改期间,先锁定,防止多处同时修改
    BOOL lockAcquired = [captureDevice lockForConfiguration:&error];
    if (!lockAcquired) {
        NSLog(@"锁定设备过程error,错误信息:%@",error.localizedDescription);
    }else{
        //调整设备前后要调用beginConfiguration/commitConfiguration
        [self.session beginConfiguration];
        propertyChange(captureDevice);
        [captureDevice unlockForConfiguration];
        [self.session commitConfiguration];
    }
}
// 展示视频的试图
- (void)setPreView:(UIView *)preView {
    _preView = preView;
    if (_preView && !self.preViewLayer) {
        self.preViewLayer = [[AVCaptureVideoPreviewLayer alloc] initWithSession:self.session];
        self.preViewLayer.frame = _preView.layer.bounds;
        // 设置layer展示视频的方向
        self.preViewLayer.connection.videoOrientation = [self.videoOutput connectionWithMediaType:AVMediaTypeVideo].videoOrientation;
        self.preViewLayer.videoGravity = AVLayerVideoGravityResizeAspectFill;
        self.preViewLayer.position = CGPointMake(_preView.frame.size.width * 0.5, _preView.frame.size.height * 0.5);
        CALayer *layer = _preView.layer;
        layer.masksToBounds = YES;
        [layer addSublayer:self.preViewLayer];
    }
}

开始和暂停音视频数据的方法实现

#pragma mark - Method
- (void)startRunning {
    [self.session startRunning];
}
- (void)stopRunning {
    if ([self.session isRunning]) {
        [self.session stopRunning];
    }
}

视频输出对象和音频输出对象的代理方法是同一个

#pragma mark - AVCaptureVideoDataAndAudioDataOutputSampleBufferDelegate
// 实现视频输出对象和音频输出对象的代理方法, 在该方法中获取音视频采集的数据, 或者叫做帧数据
- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection {
    // 判断 captureOutput 多媒体输出对象的类型
    if (captureOutput == self.audioOutput) {    // 音频输出对象
        if (self.delegate && [self.delegate respondsToSelector:@selector(audioCaptureOutputWithSampleBuffer:)]) {
            [self.delegate audioCaptureOutputWithSampleBuffer:sampleBuffer];
        }
    } else {                                    // 视频输出对象
        if (self.delegate && [self.delegate respondsToSelector:@selector(videoCaptureOutputWithSampleBuffer:)]) {
            [self.delegate videoCaptureOutputWithSampleBuffer:sampleBuffer];
        }
    }
}
// 是否支持快速纹理更新
- (BOOL)supportsFastTextureUpload;
{
#if TARGET_IPHONE_SIMULATOR
    return NO;
#else
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wtautological-pointer-compare"
    return (CVOpenGLESTextureCacheCreate != NULL);
#pragma clang diagnostic pop
#endif
}
- (void)dealloc {
    [self stopRunning];
    // 取消代理, 回到主线程
    [self.videoOutput setSampleBufferDelegate:nil queue:dispatch_get_main_queue()];
    [self.audioOutput setSampleBufferDelegate:nil queue:dispatch_get_main_queue()];
}

到此, 音视频采集的类已经封装完成!

3.JFCaptureSession的使用

用的时候需要先检验设备是否授权摄像头或麦克风的使用权限!

注意Xcode8.0以后, 使用麦克风, 摄像头, 相册等需要在info.plist文件中添加开启权限的Key 和 value

key value
Privacy - Camera Usage Description cameraDescription
Privacy - Photo Library Usage Description photoLibraryDescription
Privacy - Microphone Usage Description microphoneDescription
infoFile.jpg

摄像头和麦克风的权限检验

// 检查是否授权摄像头的使用权限
- (void)checkVideoDeviceAuth {
    switch ([AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo]) {
        case AVAuthorizationStatusAuthorized:    // 已授权
            self.authRemember += 1;
            break;
        case AVAuthorizationStatusNotDetermined: // 未授权, 进行允许和拒绝授权
        {
            [AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo completionHandler:^(BOOL granted) {
                if (granted) {
                    NSLog(@"已开启摄像头权限");
                } else {
                    NSLog(@"拒绝授权");
                }
            }];
        }
            break;
        default:
            NSLog(@"用户尚未授权摄像头的使用权");
            break;
    }
}
// 检查是否授权麦克风的shiyongquan
- (void)checkAudioDeviceAuth {
    AVAuthorizationStatus status = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeAudio];
    switch (status) {
        case AVAuthorizationStatusNotDetermined:{
            [AVCaptureDevice requestAccessForMediaType:AVMediaTypeAudio completionHandler:^(BOOL granted) {
                if (granted) {
                    self.authRemember += 1;
                } else {
                    NSLog(@"拒绝授权");
                }
            }];
        }
            break;
        case AVAuthorizationStatusAuthorized:
            NSLog(@"已开启麦克风权限");
            break;
        case AVAuthorizationStatusDenied:
        case AVAuthorizationStatusRestricted:
            break;
        default:
            break;
    }
}

本文中, 设置的是只有摄像头和麦克风同事已授权的时候才初始化的JFCaptureSession的实例对象

self.session = [[JFCaptureSession alloc] defaultJFCaptureSessionWithSessionPreset:JFCaptureSessionPreset540x960];
        _session.preView = self.view;
        _session.delegate = self;  // 记得实现代理方法, 不然获取不到采集的数据
[self.session startRunning];
/** 在需要暂停的时候 调用
[self.session stopRunning];
*/ 就可以啦

4.Demo下载地址

Demo传送门GitHub

5.结尾

本文是用的AVFoundation 框架实现的音视频数据采集, 系统的原生框架进行视频采集, 如果进行美颜的话, 工作量和难度会增加很多很多, 不过如果需要进行美颜, 我们可以使用GPUImage 开源框架的美颜相机GPUImageVideoCamera来进行视频数据采集! 后边有时间我会专门写篇文章, 来跟大家谈论一下GPUImageVideoCamera 的视频数据采集等!
音视频的数据采集, 相对来说不是很难, AVFoundation 中的很多类我们都比较陌生, 很少使用到, 所以很感觉相对难一点! 这篇文章只是分享了一下我个人对AVFoundation框架中部分类的使用和见解,拿出来跟大家分享探讨一下, 希望能对大家有所帮助, 有不完善的地方, 希望大家能多提提, 我这边也学习改正一下!
由于工作比较忙, 可能后边的技术文正会更的比较慢, 见谅!

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

推荐阅读更多精彩内容