iOS视频硬编码技术
iOS視頻硬編碼技術
一.iOS視頻采集硬編碼
基本原理
硬編碼 & 軟編碼
硬編碼:通過系統自帶的Camera錄制視頻,實際上調用的是底層的高清編碼硬件模塊,即顯卡,不使用CPU,速度快
軟編碼:使用CPU進行編碼,如常見C/C // [videoOutput setAlwaysDiscardsLateVideoFrames:NO];
// 3.2 設置輸出代理,捕獲視頻樣品數據
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
[videoOutput setSampleBufferDelegate:self queue:queue];
if ([captureSession canAddOutput:videoOutput]) {
[captureSession addOutput:videoOutput];
}
//3.3 設置視頻輸出方向
// 注意:設置方向,必須在videoOutput添加到captureSession之后,否則出錯
AVCaptureConnection *connection = [videoOutput connectionWithMediaType:AVMediaTypeVideo];
if (connection.isVideoOrientationSupported) {[connection setVideoOrientation:AVCaptureVideoOrientationPortrait];
}
- 添加視頻預覽層
AVCaptureVideoPreviewLayer *layer = [AVCaptureVideoPreviewLayer layerWithSession:captureSession];
self.preViewlayer = layer;
[layer setVideoGravity:AVLayerVideoGravityResizeAspect];
layer.frame = preview.bounds;
[preview.layer insertSublayer:layer atIndex:0];
5.開始采集
[captureSession startRunning];
? 獲取攝像頭方向
//指定攝像頭方向,獲取攝像頭
-
(AVCaptureDevice *)getVideoDevice:(AVCaptureDevicePosition)position{
NSArray *devices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo];
for (AVCaptureDevice *device in devices) {
if (device.position == position) {
return device;
}
}
return nil;
}
? 切換攝像頭
// 切換采集攝像頭 -
(void)switchScene:(UIView *)preview{
// 1.添加動畫
CATransition *rotaionAnim = [[CATransition alloc] init];
rotaionAnim.type = @“oglFlip”;
rotaionAnim.subtype = @“fromLeft”;
rotaionAnim.duration = 0.5;
[preview.layer addAnimation:rotaionAnim forKey:nil];// 2.獲取當前鏡頭
AVCaptureDevicePosition position = self.videoDeviceInput.device.position == AVCaptureDevicePositionBack ? AVCaptureDevicePositionFront : AVCaptureDevicePositionBack;// 3.創建新的input對象
AVCaptureDevice *newDevice = [self getVideoDevice:position];
AVCaptureDeviceInput *newDeviceInput = [AVCaptureDeviceInput deviceInputWithDevice:newDevice error:nil];// 4.移除舊輸入,添加新輸入
[self.captureSession beginConfiguration];
[self.captureSession removeInput:self.videoDeviceInput];
[self.captureSession addInput:newDeviceInput];
// 此處要重新設置視頻輸出方向,默認會旋轉90度
self.connection = [_videoOutput connectionWithMediaType:AVMediaTypeVideo];
if (_connection.isVideoOrientationSupported) {
[_connection setVideoOrientation:AVCaptureVideoOrientationPortrait];
}[self.captureSession commitConfiguration];
// 5.保存新輸入
self.videoDeviceInput = newDeviceInput;
}
? 實現代理 – AVCaptureVideoDataOutputSampleBufferDelegat
- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection{
NSLog(@“獲取到一幀數據”);
// 對獲取到的數據進行編碼,編碼部分在下一篇繼續講
dispatch_sync(mEncodeQueue, ^{
[self.encoder encodeFrame:sampleBuffer];
});
}
? 停止采集 - (void)stopCapturing{
// 停止掃描
[self.captureSession stopRunning];
// 移除預覽圖層
[self.preViewlayer removeFromSuperlayer];
// 將對象置為nil
self.captureSession = nil;
}
- iOS音視頻采集硬編碼
關于音視頻采集硬編碼,為方便項目的參考。用到AVCaptureSession來進行音視頻數據采集,AVCaptureSession是用來管理視頻與數據的捕獲,采集到音頻原始數據pcm(pcm是指未經過壓縮處理的)壓縮為aac格式,采集到yuv420格式的視頻幀壓縮成h.264格式。
demo:https://github.com/oopsr/AVDecode
二.iOS視頻開發:視頻H264硬編碼
已經介紹了如何采集iOS攝像頭的視頻數據,采集到的原始視頻數據量是比較大的,這么大的數據量不利于進行儲存或網絡傳輸。需要對視頻數據進行壓縮,就像你要向別人傳文件時覺得文件太大了,打個rar壓縮包再發給對方的道理一樣。視頻數據的壓縮也叫做編碼,H264是一種視頻編碼格式,iOS 8.0及以上蘋果開放了VideoToolbox框架來實現H264硬編碼,開發者可以利用VideoToolbox框架很方便地實現視頻的硬編碼。下面將分以下幾部分內容來講解H264硬編碼在iOS中的實現:
1、介紹視頻編碼的基本概念
2、VideoToolbox實現硬編碼原理及流程
3、代碼實現硬編碼
4、總結及Demo
基本概念
視頻數據為什么可以壓縮呢,因為視頻數據存在冗余。通俗地理解,例如一個視頻中,前一秒畫面跟當前的畫面內容相似度很高,那么這兩秒的數據是不是可以不用全部保存,只保留一個完整的畫面,下一個畫面看有哪些地方有變化了記錄下來,拿視頻去播放的時候就按這個完整的畫面和其他有變化的地方把其他畫面也恢復出來。記錄畫面不同然后保存下來這個過程就是數據編碼,根據不同的地方恢復畫面的過程就是數據解碼。
H264是一種視頻編碼標準,在H264協議里定義了三種幀:
? I幀:完整編碼的幀,也叫關鍵幀
? P幀:參考之前的I幀生成的只包含差異部分編碼的幀
? B幀:參考前后的幀編碼的幀叫B幀
H264采用的核心算法是幀內壓縮和幀間壓縮,幀內壓縮是生成I幀的算法,幀間壓縮是生成B幀和P幀的算法。
H264原始碼流是由一個接一個的NALU(Nal Unit)組成的,NALU = 開始碼 + NAL類型 + 視頻數據
開始碼用于標示這是一個NALU 單元的開始,必須是"00 00 00 01" 或"00 00 01"
NALU類型如下:
類型 說明
0 未規定
1 非IDR圖像中不采用數據劃分的片段
2 非IDR圖像中A類數據劃分片段
3 非IDR圖像中B類數據劃分片段
4 非IDR圖像中C類數據劃分片段
5 IDR圖像的片段
6 補充增強信息(SEI)
7 序列參數集(SPS)
8 圖像參數集(PPS)
9 分割符
10 序列結束符
11 流結束符
12 填充數據
13 序列參數集擴展
14 帶前綴的NAL單元
15 子序列參數集
16 – 18 保留
19 不采用數據劃分的輔助編碼圖像片段
20 編碼片段擴展
21 – 23 保留
24 – 31 未規定
一般只用到了1、5、7、8這4個類型就夠了。類型為5表示這是一個I幀,I幀前面必須有SPS和PPS數據,也就是類型為7和8,類型為1表示這是一個P幀或B幀。
幀率:單位為fps(frame pre second),視頻畫面每秒有多少幀畫面,數值越大畫面越流暢
碼率:單位為bps(bit pre second),視頻每秒輸出的數據量,數值越大畫面越清晰
分辨率:視頻畫面像素密度,例如常見的720P、1080P等
關鍵幀間隔:每隔多久編碼一個關鍵幀
軟編碼:使用CPU進行編碼。性能較差
硬編碼:不使用CPU進行編碼,使用顯卡GPU,專用的DSP、FPGA、ASIC芯片等硬件進行編碼。性能較好
VideoToolbox實現H264硬編碼
iOS8.0及以上可以通過VideoToolbox實現視頻數據的硬編解碼。VideoToolbox基本數據結構:
? CVPixelBufferRef/CVImageBufferRef:存放編碼前和解碼后的圖像數據,這倆貨其實是同一個東西
? CMTime:時間戳相關,時間以64-bit/32-bit的形式出現
? CMBlockBufferRef:編碼后輸出的數據
? CMFormatDescriptionRef/CMVideoFormatDescriptionRef:圖像存儲方式,編解碼器等格式描述。這倆貨也是同一個東西
? CMSampleBufferRef:存放編解碼前后的視頻圖像的容器數據
CMSampleBuffer編解碼前后數據結構
基本步驟
1、通過VTCompressionSessionCreate創建編碼器
2、通過VTSessionSetProperty設置編碼器屬性
3、設置完屬性調用VTCompressionSessionPrepareToEncodeFrames準備編碼
4、輸入采集到的視頻數據,調用VTCompressionSessionEncodeFrame進行編碼
5、獲取到編碼后的數據并進行處理
6、調用VTCompressionSessionCompleteFrames停止編碼器
7、調用VTCompressionSessionInvalidate銷毀編碼器
1、創建編碼器
VTCompressionSessionCreate用來創建視頻編碼會話,這個方法有10個參數,可以看一下蘋果對這個API的注釋
VTCompressionSessionCreate(
CM_NULLABLE CFAllocatorRef allocator,
int32_t width,
int32_t height,
CMVideoCodecType codecType,
CM_NULLABLE CFDictionaryRef encoderSpecification,
CM_NULLABLE CFDictionaryRef sourceImageBufferAttributes,
CM_NULLABLE CFAllocatorRef compressedDataAllocator,
CM_NULLABLE VTCompressionOutputCallback outputCallback,
void * CM_NULLABLE outputCallbackRefCon,
CM_RETURNS_RETAINED_PARAMETER CM_NULLABLE VTCompressionSessionRef * CM_NONNULL compressionSessionOut)
allocator:內存分配器,填NULL為默認分配器
width、height:視頻幀像素的寬高,如果編碼器不支持這個寬高的話可能會改變
codecType:編碼類型,枚舉
encoderSpecification:指定特定的編碼器,填NULL的話由VideoToolBox自動選擇
sourceImageBufferAttributes:源像素緩沖區的屬性,如果這個參數有值的話,VideoToolBox會創建一個緩沖池,不需要緩沖池可以設置為NULL
compressedDataAllocator:壓縮后數據的內存分配器,填NULL使用默認分配器
outputCallback:視頻編碼后輸出數據回調函數
outputCallbackRefCon:回調函數中的自定義指針,通常傳self,在回調函數中就可以拿到當前類的方法和屬性了
compressionSessionOut:編碼器句柄,傳入編碼器的指針
OSStatus status = VTCompressionSessionCreate(NULL, 180, 320, kCMVideoCodecType_H264, NULL, NULL, NULL, encodeOutputDataCallback, (__bridge void *)(self), &_compressionSessionRef);
2、設置編碼器屬性 & 準備編碼
編碼器創建完了,所有給編碼器設置屬性都是調用VTSessionSetProperty方法來實現。
kVTCompressionPropertyKey_AverageBitRate:設置編碼的平均碼率,單位是bps,這不是一個硬性指標,設置的碼率會上下浮動。VideoToolBox框架只支持ABR模式。H264有4種碼率控制方法:
? CBR(Constant Bit Rate)是以恒定比特率方式進行編碼,有Motion發生時,由于碼率恒定,只能通過增大QP來減少碼字大小,圖像質量變差,當場景靜止時,圖像質量又變好,因此圖像質量不穩定。這種算法優先考慮碼率(帶寬)。
? VBR(Variable Bit Rate)動態比特率,其碼率可以隨著圖像的復雜程度的不同而變化,因此其編碼效率比較高,Motion發生時,馬賽克很少。碼率控制算法根據圖像內容確定使用的比特率,圖像內容比較簡單則分配較少的碼率(似乎碼字更合適),圖像內容復雜則分配較多的碼字,這樣既保證了質量,又兼顧帶寬限制。這種算法優先考慮圖像質量。
*CVBR(Constrained VariableBit Rate),這樣翻譯成中文就比較難聽了,它是VBR的一種改進方法。但是Constrained又體現在什么地方呢?這種算法對應的Maximum bitRate恒定或者Average BitRate恒定。這種方法的兼顧了以上兩種方法的優點:在圖像內容靜止時,節省帶寬,有Motion發生時,利用前期節省的帶寬來盡可能的提高圖像質量,達到同時兼顧帶寬和圖像質量的目的。
? ABR (Average Bit Rate) 在一定的時間范圍內達到設定的碼率,但是局部碼率峰值可以超過設定的碼率,平均碼率恒定。可以作為VBR和CBR的一種折中選擇。
kVTCompressionPropertyKey_ProfileLevel:設置H264編碼的畫質,H264有4種Profile:BP、EP、MP、HP
BP(Baseline Profile):基本畫質。支持I/P 幀,只支持無交錯(Progressive)和CAVLC;主要應用:可視電話,會議電視,和無線通訊等實時視頻通訊領域
EP(Extended profile):進階畫質。支持I/P/B/SP/SI 幀,只支持無交錯(Progressive)和CAVLC;
MP(Main profile):主流畫質。提供I/P/B 幀,支持無交錯(Progressive)和交錯(Interlaced),也支持CAVLC 和CABAC 的支持;主要應用:數字廣播電視和數字視頻存儲
HP(High profile):高級畫質。在main Profile 的基礎上增加了8×8內部預測、自定義量化、 無損視頻編碼和更多的YUV 格式;應用于廣電和存儲領域
Level就多了,這里不一一列舉,可參考h264 profile & level,iPhone上常用的方案如下:
? 實時直播:
低清Baseline Level 1.3
標清Baseline Level 3
半高清Baseline Level 3.1
全高清Baseline Level 4.1
? 存儲媒體:
低清 Main Level 1.3
標清 Main Level 3
半高清 Main Level 3.1
全高清 Main Level 4.1
? 高清存儲:
半高清 High Level 3.1
全高清 High Level 4.1
kVTCompressionPropertyKey_RealTime:設置是否實時編碼輸出
kVTCompressionPropertyKey_AllowFrameReordering:配置是否產生B幀,High profile 支持 B 幀
kVTCompressionPropertyKey_MaxKeyFrameInterval、kVTCompressionPropertyKey_MaxKeyFrameIntervalDuration:配置I幀間隔
設置完編碼器的屬性后,調用VTCompressionSessionPrepareToEncodeFrames準備編碼
// 設置碼率 512kbps
OSStatus status = VTSessionSetProperty(_compressionSessionRef, kVTCompressionPropertyKey_AverageBitRate, (__bridge CFTypeRef)@(512 * 1024));
// 設置ProfileLevel為BP3.1
status = VTSessionSetProperty(_compressionSessionRef, kVTCompressionPropertyKey_ProfileLevel, kVTProfileLevel_H264_Baseline_3_1);
// 設置實時編碼輸出(避免延遲)
status = VTSessionSetProperty(_compressionSessionRef, kVTCompressionPropertyKey_RealTime, kCFBooleanTrue);
// 配置是否產生B幀
status = VTSessionSetProperty(_compressionSessionRef, kVTCompressionPropertyKey_AllowFrameReordering, self.videoEncodeParam.allowFrameReordering ? kCFBooleanTrue : kCFBooleanFalse);
// 配置最大I幀間隔 15幀 x 240秒 = 3600幀,也就是每隔3600幀編一個I幀
status = VTSessionSetProperty(_compressionSessionRef, kVTCompressionPropertyKey_MaxKeyFrameInterval, (__bridge CFTypeRef)@(self.videoEncodeParam.frameRate * self.videoEncodeParam.maxKeyFrameInterval));
// 配置I幀持續時間,240秒編一個I幀
status = VTSessionSetProperty(_compressionSessionRef, kVTCompressionPropertyKey_MaxKeyFrameIntervalDuration, (__bridge CFTypeRef)@(self.videoEncodeParam.maxKeyFrameInterval));
// 編碼器準備編碼
status = VTCompressionSessionPrepareToEncodeFrames(_compressionSessionRef);
3、輸入待編碼的視頻數據
向編碼器輸送待編碼的視頻數據,通過調用VTCompressionSessionEncodeFrame方法實現。
VTCompressionSessionEncodeFrame(
CM_NONNULL VTCompressionSessionRef session,
CM_NONNULL CVImageBufferRef imageBuffer,
CMTime presentationTimeStamp,
CMTime duration, // may be kCMTimeInvalid
CM_NULLABLE CFDictionaryRef frameProperties,
void * CM_NULLABLE sourceFrameRefCon,
VTEncodeInfoFlags * CM_NULLABLE infoFlagsOut )
session:創建編碼器時的句柄
imageBuffer:YUV數據,iOS通過攝像頭采集出來的視頻流數據類型是CMSampleBufferRef,要從里面拿到CVImageBufferRef來進行編碼。通過CMSampleBufferGetImageBuffer方法可以從sampleBuffer中獲得imageBuffer。
presentationTimeStamp:這一幀的時間戳,單位是毫秒
duration:這一幀的持續時間,如果沒有持續時間,填kCMTimeInvalid
frameProperties:指定這一幀的屬性,這里可以用來指定產生I幀
encodeParams:自定義指針
infoFlagsOut:用于接收編碼操作的信息,不需要就置為NULL
// 獲取CVImageBufferRef
CVImageBufferRef imageBuffer = (CVImageBufferRef)CMSampleBufferGetImageBuffer(sampleBuffer);
// 設置是否為I幀
NSDictionary *frameProperties = @{(__bridge NSString *)kVTEncodeFrameOptionKey_ForceKeyFrame: @(forceKeyFrame)};;
// 輸入待編碼數據
OSStatus status = VTCompressionSessionEncodeFrame(_compressionSessionRef, imageBuffer, kCMTimeInvalid, kCMTimeInvalid, (__bridge CFDictionaryRef)frameProperties, NULL, NULL);
4、獲取編碼后的數據并進行處理
編碼后的數據通過VTCompressionSessionCreate方法設置的回調函數返回。編碼后的數據以及這一幀的基本信息都在CMSampleBufferRef中。如果這一幀是個關鍵幀,那么需要獲取SPS和PPS數據,然后給這些數據加個開始碼返回出去。
VEVideoEncoder *encoder = (__bridge VEVideoEncoder *)outputCallbackRefCon;
// 開始碼
const char header[] = “\x00\x00\x00\x01”;
size_t headerLen = (sizeof header) - 1;
NSData *headerData = [NSData dataWithBytes:header length:headerLen];
// 判斷是否是關鍵幀
bool isKeyFrame = !CFDictionaryContainsKey((CFDictionaryRef)CFArrayGetValueAtIndex(CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, true), 0), (const void *)kCMSampleAttachmentKey_NotSync);
if (isKeyFrame)
{
NSLog(@“VEVideoEncoder::編碼了一個關鍵幀”);
CMFormatDescriptionRef formatDescriptionRef = CMSampleBufferGetFormatDescription(sampleBuffer);
// 關鍵幀需要加上SPS、PPS信息
size_t sParameterSetSize, sParameterSetCount;
const uint8_t *sParameterSet;
OSStatus spsStatus = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(formatDescriptionRef, 0, &sParameterSet, &sParameterSetSize, &sParameterSetCount, 0);size_t pParameterSetSize, pParameterSetCount;
const uint8_t *pParameterSet;
OSStatus ppsStatus = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(formatDescriptionRef, 1, &pParameterSet, &pParameterSetSize, &pParameterSetCount, 0);if (noErr == spsStatus && noErr == ppsStatus)
{// sps數據加上開始碼組成NALUNSData *sps = [NSData dataWithBytes:sParameterSet length:sParameterSetSize];NSMutableData *spsData = [NSMutableData data];[spsData appendData:headerData];[spsData appendData:sps];// 通過代理回調給上層if ([encoder.delegate respondsToSelector:@selector(videoEncodeOutputDataCallback:isKeyFrame:)]){[encoder.delegate videoEncodeOutputDataCallback:spsData isKeyFrame:isKeyFrame];}// pps數據加上開始碼組成NALUNSData *pps = [NSData dataWithBytes:pParameterSet length:pParameterSetSize];NSMutableData *ppsData = [NSMutableData data];[ppsData appendData:headerData];[ppsData appendData:pps];if ([encoder.delegate respondsToSelector:@selector(videoEncodeOutputDataCallback:isKeyFrame:)]){[encoder.delegate videoEncodeOutputDataCallback:ppsData isKeyFrame:isKeyFrame];}
}
}
// 獲取幀數據
CMBlockBufferRef blockBuffer = CMSampleBufferGetDataBuffer(sampleBuffer);
size_t length, totalLength;
char *dataPointer;
status = CMBlockBufferGetDataPointer(blockBuffer, 0, &length, &totalLength, &dataPointer);
if (noErr != status)
{
NSLog(@“VEVideoEncoder::CMBlockBufferGetDataPointer Error : %d!”, (int)status);
return;
}
size_t bufferOffset = 0;
static const int avcHeaderLength = 4;
while (bufferOffset < totalLength - avcHeaderLength)
{
// 讀取 NAL 單元長度
uint32_t nalUnitLength = 0;
memcpy(&nalUnitLength, dataPointer + bufferOffset, avcHeaderLength);
// 大端轉小端
nalUnitLength = CFSwapInt32BigToHost(nalUnitLength);NSData *frameData = [[NSData alloc] initWithBytes:(dataPointer + bufferOffset + avcHeaderLength) length:nalUnitLength];NSMutableData *outputFrameData = [NSMutableData data];
[outputFrameData appendData:headerData];
[outputFrameData appendData:frameData];bufferOffset += avcHeaderLength + nalUnitLength;if ([encoder.delegate respondsToSelector:@selector(videoEncodeOutputDataCallback:isKeyFrame:)])
{[encoder.delegate videoEncodeOutputDataCallback:outputFrameData isKeyFrame:isKeyFrame];
}
}
5、停止編碼
OSStatus status = VTCompressionSessionCompleteFrames(_compressionSessionRef, kCMTimeInvalid);
6、釋放編碼器
VTCompressionSessionInvalidate(_compressionSessionRef);
CFRelease(_compressionSessionRef);
_compressionSessionRef = NULL;
踩坑及總結
在弄視頻編解碼的時候,發現720P的分辨率,碼率1Mbps,在畫面晃動的時候馬賽克很嚴重,碼率設置的再低一點更嚴重。一開始以為是編碼器的某些屬性漏了設置了,或者是參數設置錯了。查閱了很多資料都找不到原因。后來懷疑是ABR模式當畫面從靜止到晃動碼率一下子上不去,導致馬賽克,這個假設似乎成立,結果去打印編碼出來的碼率,畫面晃動的時候碼率是有上去的,說明這個思路還是不對。后來,發現,攝像頭采集的數據是720P,也就是1280x720的分辨率,給編碼器設置編碼寬高的時候也是按1280x720的寬高設給編碼器的,但實際上解碼、播放是展示的畫面尺寸(像素)只有320x180,于是嘗試了一下把編碼的寬高設置為320x180,馬賽克問題解決了!
總結
以上是生活随笔為你收集整理的iOS视频硬编码技术的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 基于Jittor框架实现LSGAN图像生
- 下一篇: Linux实现ffmpeg H.265视