由于我们公司不是专门做直播的, 所以研究直播开发完全处于兴趣爱好,可能很多地方用处理的不是很周到, 所以, 希望大家多提提意见, 互相学习一下哈!
这里附上我写的第一篇直播开发的文章传送门
iOS-直播开发(开发从底层做起)
好啦, 废话不多说, 直奔主题! 本篇文章是针对直播开发中的第一部分, 音视频采集! 用的是iOS 原生的AVFoundation框架!
Demo传送门GitHub
实现的效果图
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 |
摄像头和麦克风的权限检验
// 检查是否授权摄像头的使用权限
- (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下载地址
5.结尾
本文是用的AVFoundation 框架实现的音视频数据采集, 系统的原生框架进行视频采集, 如果进行美颜的话, 工作量和难度会增加很多很多, 不过如果需要进行美颜, 我们可以使用GPUImage 开源框架的美颜相机GPUImageVideoCamera来进行视频数据采集! 后边有时间我会专门写篇文章, 来跟大家谈论一下GPUImageVideoCamera 的视频数据采集等!
音视频的数据采集, 相对来说不是很难, AVFoundation 中的很多类我们都比较陌生, 很少使用到, 所以很感觉相对难一点! 这篇文章只是分享了一下我个人对AVFoundation框架中部分类的使用和见解,拿出来跟大家分享探讨一下, 希望能对大家有所帮助, 有不完善的地方, 希望大家能多提提, 我这边也学习改正一下!
由于工作比较忙, 可能后边的技术文正会更的比较慢, 见谅!