由于我們公司不是專門做直播的, 所以研究直播開發(fā)完全處于興趣愛好,可能很多地方用處理的不是很周到, 所以, 希望大家多提提意見, 互相學習一下哈!
這里附上我寫的第一篇直播開發(fā)的文章傳送門
iOS-直播開發(fā)(開發(fā)從底層做起)
好啦, 廢話不多說, 直奔主題! 本篇文章是針對直播開發(fā)中的第一部分, 音視頻采集! 用的是iOS 原生的AVFoundation框架!
Demo傳送門GitHub
實現(xiàn)的效果圖
1. 所使用的系統(tǒng)類
AVCaptureSession *session; // 音視頻管理對象
AVCaptureDevice *videoDevice; // 視頻設(shè)備對象 (用來操作閃光燈, 聚焦, 攝像頭切換等)
AVCaptureDevice *audioDevice; // 音頻設(shè)備對象
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中的實現(xiàn)有判斷, 如果不支持該分辨率, 則會降一級
.h中的另一個枚舉 該枚舉用來操控前后攝像頭的
// 攝像頭方向
typedef NS_ENUM(NSInteger, JFCaptureDevicePosition) {
JFCaptureDevicePositionFront = 0, // 前置攝像頭
JFCaptureDevicePositionBack // 后置攝像頭
};
然后就是JFCaptureSession 的代理 JFCaptureSessionDelegate, 用來回調(diào)采集的音視頻幀數(shù)據(jù) CMSampleBufferRef
/** 視頻取樣數(shù)據(jù)回調(diào) */
- (void)videoCaptureOutputWithSampleBuffer:(CMSampleBufferRef)sampleBuffer;
/** 音頻取樣數(shù)據(jù)回調(diào) */
- (void)audioCaptureOutputWithSampleBuffer:(CMSampleBufferRef)sampleBuffer;
JFCaptureSession 該類的初始化方法, 初始化的時候需要傳一分辨率的枚舉值, 來設(shè)置要采集視頻的分辨率
- (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 集體實現(xiàn)音視頻采集的方法
// 初始化方法
- (instancetype)defaultJFCaptureSessionWithSessionPreset:(JFCaptureSessionPreset)sessionPreset {
if ([super init]) {
self.sessionPreset = sessionPreset;
// 初始化Session
[self initAVCaptureSession];
}
return self;
}
- (void)initAVCaptureSession {
// 初始化
self.session = [[AVCaptureSession alloc] init];
// 設(shè)置錄像的分辨率
[self.session canSetSessionPreset:[self supportSessionPreset]];
/** 注意: 配置AVCaptureSession 的時候, 必須先開始配置, beginConfiguration, 配置完成, 必須提交配置 commitConfiguration, 否則配置無效 **/
// 開始配置
[self.session beginConfiguration];
// 設(shè)置視頻 I/O 對象 并添加到session
[self videoInputAndOutput];
// 設(shè)置音頻 I/O 對象 并添加到session
[self audioInputAndOutput];
// 提交配置
[self.session commitConfiguration];
}
// 設(shè)置視頻 I/O 對象
- (void)videoInputAndOutput {
NSError *error;
// 初始化視頻設(shè)備對象
self.videoDevice = nil;
// 創(chuàng)建攝像頭類型數(shù)組 (前置, 和后置攝像頭之分)
NSArray *devices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo];
// 便利獲取的所有支持的攝像頭類型
for (AVCaptureDevice *devcie in devices) {
// 默然先開啟前置攝像頭
if (devcie.position == AVCaptureDevicePositionFront) {
self.videoDevice = devcie;
}
}
// 視頻輸入
// 根據(jù)視頻設(shè)備來初始化輸入對象
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, 現(xiàn)在視頻一般采用該顏色空間, 可以分離亮度跟色彩, 在不影響清晰度的情況下來壓縮視頻
BOOL supportFullYUVRange = NO;
// 獲取輸出對象所支持的像素格式
NSArray *supportedPixelFormats = self.videoOutput.availableVideoCVPixelFormatTypes;
for (NSNumber *currentPixelFormat in supportedPixelFormats) {
if ([currentPixelFormat integerValue] == kCVPixelFormatType_420YpCbCr8BiPlanarFullRange) {
supportFullYUVRange = YES;
}
}
// 根據(jù)是否支持全頻色彩編碼 YUV 來設(shè)置輸出對象的視頻像素壓縮格式
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]];
}
// 創(chuàng)建設(shè)置代理是所需要的線程隊列 優(yōu)先級設(shè)為高
dispatch_queue_t videoQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
// 設(shè)置代理
[self.videoOutput setSampleBufferDelegate:self queue:videoQueue];
// 判斷session 是否可添加視頻輸出對象
if ([self.session canAddOutput:self.videoOutput]) {
[self.session addOutput:self.videoOutput];
// 鏈接視頻 I/O 對象
[self connectionVideoInputVideoOutput];
}
}
// 設(shè)置音頻I/O 對象
- (void)audioInputAndOutput {
NSError *error;
// 初始音頻設(shè)備對象
self.audioDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio];
// 音頻輸入對象
self.audioInput = [[AVCaptureDeviceInput alloc] initWithDevice:self.audioDevice error:&error];
if (error) {
NSLog(@"== 錄音設(shè)備出錯");
}
// 判斷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];
}
// 創(chuàng)建設(shè)置音頻輸出代理所需要的線程隊列
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中獲取實際數(shù)據(jù)。
AVCaptureConnection *connection = [self.videoOutput connectionWithMediaType:AVMediaTypeVideo];
// 設(shè)置視頻的方向, 如果不設(shè)置的話, 視頻默認是旋轉(zhuǎn) 90°的
connection.videoOrientation = AVCaptureVideoOrientationPortrait;
// 設(shè)置視頻的穩(wěn)定性, 先判斷connection 連接對象是否支持 視頻穩(wěn)定
if ([connection isVideoStabilizationSupported]) {
connection.preferredVideoStabilizationMode = AVCaptureVideoStabilizationModeAuto;
}
// 縮放裁剪系數(shù), 設(shè)為最大
connection.videoScaleAndCropFactor = connection.videoMaxScaleAndCropFactor;
}
// 判斷是否支持設(shè)置的分辨率, 如果不支持, 默認降一級, 還不支持, 設(shè)為默認
- (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;
}
// 根據(jù)視頻分辨率, 設(shè)置具體對應(yīng)的類型
- (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);
}
}];
}
}
// 獲取需要的設(shè)備對象
- (AVCaptureDevice *)deviceWithMediaType:(NSString *)mediaType preferringPosition:(AVCaptureDevicePosition)position {
// 獲取所有類型的攝像頭設(shè)備
NSArray *devices = [AVCaptureDevice devicesWithMediaType:mediaType];
AVCaptureDevice *captureDevice = devices.firstObject; // 先初始化一個設(shè)備對象并賦初值
// 便利獲取需要的設(shè)備
for (AVCaptureDevice *device in devices) {
if (device.position == position) {
captureDevice = device;
break;
}
}
return captureDevice;
}
#pragma mark 更改設(shè)備屬性前一定要鎖上
-(void)changeDevicePropertySafety:(void (^)(AVCaptureDevice *captureDevice))propertyChange{
//也可以直接用_videoDevice,但是下面這種更好
AVCaptureDevice *captureDevice= [_videoInput device];
NSError *error;
//注意改變設(shè)備屬性前一定要首先調(diào)用lockForConfiguration:調(diào)用完之后使用unlockForConfiguration方法解鎖,意義是---進行修改期間,先鎖定,防止多處同時修改
BOOL lockAcquired = [captureDevice lockForConfiguration:&error];
if (!lockAcquired) {
NSLog(@"鎖定設(shè)備過程error,錯誤信息:%@",error.localizedDescription);
}else{
//調(diào)整設(shè)備前后要調(diào)用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;
// 設(shè)置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];
}
}
開始和暫停音視頻數(shù)據(jù)的方法實現(xiàn)
#pragma mark - Method
- (void)startRunning {
[self.session startRunning];
}
- (void)stopRunning {
if ([self.session isRunning]) {
[self.session stopRunning];
}
}
視頻輸出對象和音頻輸出對象的代理方法是同一個
#pragma mark - AVCaptureVideoDataAndAudioDataOutputSampleBufferDelegate
// 實現(xiàn)視頻輸出對象和音頻輸出對象的代理方法, 在該方法中獲取音視頻采集的數(shù)據(jù), 或者叫做幀數(shù)據(jù)
- (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()];
}
到此, 音視頻采集的類已經(jīng)封裝完成!
3.JFCaptureSession的使用
用的時候需要先檢驗設(shè)備是否授權(quán)攝像頭或麥克風的使用權(quán)限!
注意Xcode8.0以后, 使用麥克風, 攝像頭, 相冊等需要在info.plist文件中添加開啟權(quán)限的Key 和 value
key | value |
---|---|
Privacy - Camera Usage Description | cameraDescription |
Privacy - Photo Library Usage Description | photoLibraryDescription |
Privacy - Microphone Usage Description | microphoneDescription |
攝像頭和麥克風的權(quán)限檢驗
// 檢查是否授權(quán)攝像頭的使用權(quán)限
- (void)checkVideoDeviceAuth {
switch ([AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo]) {
case AVAuthorizationStatusAuthorized: // 已授權(quán)
self.authRemember += 1;
break;
case AVAuthorizationStatusNotDetermined: // 未授權(quán), 進行允許和拒絕授權(quán)
{
[AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo completionHandler:^(BOOL granted) {
if (granted) {
NSLog(@"已開啟攝像頭權(quán)限");
} else {
NSLog(@"拒絕授權(quán)");
}
}];
}
break;
default:
NSLog(@"用戶尚未授權(quán)攝像頭的使用權(quán)");
break;
}
}
// 檢查是否授權(quán)麥克風的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(@"拒絕授權(quán)");
}
}];
}
break;
case AVAuthorizationStatusAuthorized:
NSLog(@"已開啟麥克風權(quán)限");
break;
case AVAuthorizationStatusDenied:
case AVAuthorizationStatusRestricted:
break;
default:
break;
}
}
本文中, 設(shè)置的是只有攝像頭和麥克風同事已授權(quán)的時候才初始化的JFCaptureSession的實例對象
self.session = [[JFCaptureSession alloc] defaultJFCaptureSessionWithSessionPreset:JFCaptureSessionPreset540x960];
_session.preView = self.view;
_session.delegate = self; // 記得實現(xiàn)代理方法, 不然獲取不到采集的數(shù)據(jù)
[self.session startRunning];
/** 在需要暫停的時候 調(diào)用
[self.session stopRunning];
*/ 就可以啦
4.Demo下載地址
5.結(jié)尾
本文是用的AVFoundation 框架實現(xiàn)的音視頻數(shù)據(jù)采集, 系統(tǒng)的原生框架進行視頻采集, 如果進行美顏的話, 工作量和難度會增加很多很多, 不過如果需要進行美顏, 我們可以使用GPUImage 開源框架的美顏相機GPUImageVideoCamera來進行視頻數(shù)據(jù)采集! 后邊有時間我會專門寫篇文章, 來跟大家談?wù)撘幌翯PUImageVideoCamera 的視頻數(shù)據(jù)采集等!
音視頻的數(shù)據(jù)采集, 相對來說不是很難, AVFoundation 中的很多類我們都比較陌生, 很少使用到, 所以很感覺相對難一點! 這篇文章只是分享了一下我個人對AVFoundation框架中部分類的使用和見解,拿出來跟大家分享探討一下, 希望能對大家有所幫助, 有不完善的地方, 希望大家能多提提, 我這邊也學習改正一下!
由于工作比較忙, 可能后邊的技術(shù)文正會更的比較慢, 見諒!