由于我们公司不是专门做直播的, 所以研究直播开发完全处于兴趣爱好,可能很多地方用处理的不是很周到, 所以, 希望大家多提提意见, 互相学习一下哈!
这里附上我写的第一篇直播开发的文章传送门
iOS-直播开发(开发从底层做起)
好啦, 废话不多说, 直奔主题! 本篇文章是针对直播开发中的第一部分, 音视频采集! 用的是iOS 原生的AVFoundation框架!
Demo传送门GitHub
实现的效果图
iOS-直播开发(开发从底层做起)之音视频采集
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框架中部分类的使用和见解,拿出来跟大家分享探讨一下, 希望能对大家有所帮助, 有不完善的地方, 希望大家能多提提, 我这边也学习改正一下!
由于工作比较忙, 可能后边的技术文正会更的比较慢, 见谅!
网友评论
采集回来的视频对象如何才能够先矫正视频的方向再推流到服务器呢?
The queue on which callbacks should be invoked. You must use a serial dispatch queue, to guarantee that video frames will be delivered in order.
好像要求使用串行队列!
欢迎订阅《iOS开发以及其它》https://toutiao.io/subjects/84364