AVFounction學習筆記之--VideoToolbox視訊硬編碼
AVFounction學習筆記之–VideoToolbox視訊硬編碼
- 視訊編碼相關知識概念
幀:每幀代表一張靜態的影象
GOP:GOP就是一組連續額畫面,每個畫面都是一幀,一個GOP就是很多幀的集合,GOP cache長度越長,畫面質量越好
位元速率:畫面進行壓縮後每秒顯示的資料量
幀率:每秒顯示圖片的數(人眼所看畫面在16幀以上,就會認為是連貫的)
解析度:圖片的長度 * 寬度,圖片的尺寸
壓縮前每秒資料:幀率 * 解析度
壓縮比:壓縮前的每秒鐘資料 / 位元速率(壓縮比越高,畫面質量越差)
視訊封裝格式:一種儲存視訊資訊的容器(流式封裝:TS\FLV;索引封裝:MP4\MOV\AVI)
主要作用:一個視訊檔案往往會包含影象和音訊,還有一些配置資訊,這些內容需要按照一定規則組織封裝起來
注意:封裝格式和檔案格式一樣,因為一般視訊檔案格式的字尾即採用相應的視訊封裝格式的名稱,所以視訊檔案格式就是視訊封裝格式
I幀(關鍵幀):幀內編碼幀,包含一幀畫面的完整幀,是P幀和B幀的參考幀,佔用資料的資訊量比較大,是GOP基礎幀的第一幀,一組GOP中只有一個I幀
P幀(差別幀):保留幀與前幀的區別(以I幀為參考幀),解碼需要快取畫面疊加本幀定義的差別,生成最終畫面,P幀只儲存差別資料,並不是完整幀,壓縮比比較高
B幀(雙向差別幀):記錄的是本幀與前後幀的差別。通過前面的I幀或P幀和後面的P幀來進行預測的
幀內壓縮(空間壓縮):只考慮本幀資料,不考慮相鄰幀之間的冗餘資訊。當壓縮⼀幀影象時,僅考慮本幀的資料⽽不考慮相鄰幀之間的冗餘資訊,這實際上與靜態影象壓縮類似。幀內⼀般採⽤用有失真壓縮演算法,由於幀內壓縮是編碼一個完整的影象,所以可以獨立的解碼、顯示。幀內壓縮一般達不不到很⾼高的壓縮,跟編碼jpeg差不多
幀間壓縮:通過比較時間軸上不同幀之間的資料進行壓縮。幀間壓縮一般是無損的。
位元速率計算公式
名稱 | 公式 | 192*144 | 320*240 | 480*360 | 640*480 | 1280*720 | 1920*1080 |
---|---|---|---|---|---|---|---|
極低位元速率 | 寬* 高 * 3/4 | 30kb/s | 60kb/s | 120kb/s | 250kb/s | 500kb/s | 1mbps |
低位元速率 | 寬* 高 * 3/2 | 60kb/s | 120kb/s | 250kb/s | 500kb/s | 1mbps | 2mbps |
中位元速率 | 寬* 高 * 3 | 120kb/s | 250kb/s | 500kb/s | 1mbps | 2mbps | 4mbps |
高位元速率 | 寬* 高 * 3 * 2 | 250kb/s | 500kb/s | 1mbps | 2mbps | 4mbps | 8mbps |
極高位元速率 | 寬* 高 * 3 * 4 | 500kb/s | 1mbps | 2mbps | 4mbps | 8mbps | 16mbps |
- H264視訊編碼
H264視訊編碼壓縮方法:
1、分組:把幾幀影象分為一組(GOP),為防止運動變化,幀數不宜去多
2、定義幀:將每組內各幀影象定義為三種類型:I幀、B幀、P幀
3、預測幀:以I幀作為基礎,以I幀預測P幀,再由I幀預測B幀
4、資料傳輸:最後將I幀資料與預測的差值資訊進行儲存和傳輸
H264 NAL頭解析
如果NALU對相應的Slice為一幀的開始,則用4位元組表示,即0x00000001;否則用3位元組表示,0x000001、
NAL Header: forbidden_bit, nal_reference_bit(優先順序)2bit,nal_unit_type(型別)5bit。標識NAL單位稱為VCL的NAL單元,其他型別的NAL單元為非VCL的NAL單元
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:保留
14-31:未規定
H.264的SPS和PPS串,包含了初始化H.264解析器所需要的資訊引數,包括編碼所用的profile,level,影象的寬高,deblock濾波器等
- 編碼資料格式
編碼前或者解碼後的資料格式 = CMSampleBuffer = CMTime + CMVideoFormatDesc + CVPixelBuffer
編碼後的資料格式 = CMSampleBuffer = CMTime + CMVideoFormat(影象儲存格式) + CMBlockBuffer
FFmpeg或者硬編碼編碼完成的H264資料格式
- VideoToolbox編碼流程
- VideoToolbox示例程式碼
#import "H264Encoder.h"
#import <VideoToolbox/VideoToolbox.h>
@interface H264Encoder()
@property(nonatomic, assign)int frameID;
@property(nonatomic, assign)VTCompressionSessionRef cEncodeingSession;
@property (nonatomic, strong) NSFileHandle * videoFileHandle;
@property (nonatomic, strong) dispatch_queue_t encodeQueue;
@end
@implementation H264Encoder
- (instancetype)init
{
self = [super init];
if (self) {
dispatch_sync(self.encodeQueue, ^{
[self initVideoToolbox];
});
}
return self;
}
- (void)stopEncode
{
VTCompressionSessionCompleteFrames(self.cEncodeingSession, kCMTimeInvalid);
VTCompressionSessionInvalidate(self.cEncodeingSession);
CFRelease(self.cEncodeingSession);
self.cEncodeingSession = NULL;
[self.videoFileHandle closeFile];
self.videoFileHandle = NULL;
}
- (void)encodeH264:(CMSampleBufferRef)sampleBuffer {
dispatch_sync(self.encodeQueue, ^{
NSLog(@"H264編碼中...");
// 拿到每一幀的未編碼的資料
CVImageBufferRef imageBuffer = (CVImageBufferRef)CMSampleBufferGetImageBuffer(sampleBuffer);
// 根據當前的幀數建立幀時間
CMTime ptime = CMTimeMake(self.frameID ++, 1000);
// 編碼準備
VTEncodeInfoFlags flags; // 0 同步編碼 1表示非同步編碼
OSStatus status = VTCompressionSessionEncodeFrame(self.cEncodeingSession, imageBuffer, ptime, kCMTimeInvalid, NULL, NULL, &flags);
if (status != noErr) {
VTCompressionSessionInvalidate(self.cEncodeingSession);
CFRelease(self.cEncodeingSession);
self.cEncodeingSession = NULL;
return;
} else {
NSLog(@"encode error status = %d", (int)status);
}
});
}
- (void)initVideoToolbox {
// 用於記錄是第幾幀資料
self.frameID = 0;
// 捕捉視訊的寬高
int width = [UIScreen mainScreen].bounds.size.width;
int height = [UIScreen mainScreen].bounds.size.height;
// 建立一個編碼器 didCompressH264編碼回撥函式
OSStatus status = VTCompressionSessionCreate(NULL, width, height, kCMVideoCodecType_H264,
NULL, NULL, NULL,
didCompressH264,
(__bridge void*)self, &_cEncodeingSession);
if (status != 0) {
NSLog(@"建立編碼器失敗 status = %d", (int)status);
return ;
}
// 設定實施編碼輸出
VTSessionSetProperty(self.cEncodeingSession, kVTCompressionPropertyKey_RealTime, kCFBooleanTrue);
VTSessionSetProperty(self.cEncodeingSession, kVTCompressionPropertyKey_ProfileLevel, kVTProfileLevel_H264_Baseline_AutoLevel);
// 設定關鍵幀(GOPsize)間隔
int frameInterval = 30;
CFNumberRef frameIntervalRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &frameInterval);
VTSessionSetProperty(self.cEncodeingSession, kVTCompressionPropertyKey_MaxKeyFrameInterval, frameIntervalRef);
// 設定期望幀率,不是實際幀率
int fps = 30;
CFNumberRef fpsRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &fps);
VTSessionSetProperty(self.cEncodeingSession, kVTCompressionPropertyKey_ExpectedFrameRate, fpsRef);
// 設定位元速率,單位是byte (編碼效率, 位元速率越高,則畫面越清晰, 如果位元速率較低會引起馬賽克 --> 位元速率高有利於還原原始畫面,但是也不利於傳輸)
int bigRate = width * height * 3 * 4 * 8;
CFNumberRef bigRateRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &bigRate);
VTSessionSetProperty(self.cEncodeingSession, kVTCompressionPropertyKey_AverageBitRate, bigRateRef);
int bigRateLimit = width * height * 3 * 4;
CFNumberRef bigRateLimitRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &bigRateLimit);
VTSessionSetProperty(self.cEncodeingSession, kVTCompressionPropertyKey_DataRateLimits, bigRateLimitRef);
// 開始準備編碼
VTCompressionSessionPrepareToEncodeFrames(self.cEncodeingSession);
}
#pragma mark - 編碼回撥
// 編碼完成回撥
void didCompressH264(void *outputCallbackRefCon, void *sourceFrameRefCon, OSStatus status, VTEncodeInfoFlags infoFlags, CMSampleBufferRef sampleBuffer)
{
// CMSampleBufferRef 包括 CMTime(時間戳) + CMVideoGormatDesc(影象儲存方式) + CMBlockBuffer(編碼後的資料)
// 獲取h264編碼的資料 sampleBuffer
NSLog(@"didCompressH264: status = %d infoFlags = %u", (int)status, (unsigned int)infoFlags);
// 狀態錯誤
if (status != 0) {
return;
}
// 沒準備好
if (!CMSampleBufferDataIsReady(sampleBuffer)) {
NSLog(@"didCompressH264 data is not ready");
return;
}
// 需要呼叫oc的方法
H264Encoder * self = (__bridge H264Encoder*)outputCallbackRefCon;
// 判斷當前幀是否為關鍵幀
bool keyFrame = !CFDictionaryContainsKey(CFArrayGetValueAtIndex(CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, true), 0), kCMSampleAttachmentKey_NotSync);
if (keyFrame) {
// sps 序列引數集 pps 影象引數集 h264
// 獲取影象編碼後的儲存資訊
CMFormatDescriptionRef format = CMSampleBufferGetFormatDescription(sampleBuffer);
// 獲取 sps 內容、大小、長度
size_t spsCount, spsLength;
const uint8_t *spsSet;
OSStatus spsStatus = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format,
0,
&spsSet,
&spsLength,
&spsCount,
0);
if (spsStatus == noErr) {
// 獲取pps資訊
size_t ppsCount, ppsLength;
const uint8_t *ppsSet;
OSStatus ppsStatus = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format,
1,
&ppsSet,
&ppsLength,
&ppsCount,
0);
if (ppsStatus == noErr) {
// 將sps pps轉成 NSData 寫入檔案
NSData * spsData = [NSData dataWithBytes:spsSet length:spsLength];
NSData * ppsData = [NSData dataWithBytes:ppsSet length:ppsLength];
if (self) {
[self gotSpsPps:spsData pps:ppsData];
}
}
}
}
// 獲取資料塊
CMBlockBufferRef dataBuffer = CMSampleBufferGetDataBuffer(sampleBuffer);
size_t length, totleLength;
char *dataPointer;
OSStatus blockStatus = CMBlockBufferGetDataPointer(dataBuffer,
0,
&length,
&totleLength,
&dataPointer);
if (blockStatus == noErr) {
size_t bufferOfSet = 0;
// 返回的nalu資料前四個位元組不是0001的startcode,而是大端模式的幀長度length
static const int AVCCHeaderLength = 4;
// 獲取nalu資料
while (bufferOfSet < totleLength - AVCCHeaderLength) {
UInt32 NALUnitLength = 0;
// Read the NAL unit length
memcpy(&NALUnitLength, dataPointer + bufferOfSet, AVCCHeaderLength);
// 大端模式 轉換為 系統端模式
NALUnitLength = CFSwapInt32BigToHost(NALUnitLength);
// 獲取nalu資料
NSData * data = [[NSData alloc] initWithBytes:(dataPointer + AVCCHeaderLength + bufferOfSet) length:NALUnitLength];
// 將 nalu資料 寫入檔案
[self gotEncodedData:data isKeyFrame:keyFrame];
// 移動偏移量
bufferOfSet += AVCCHeaderLength + NALUnitLength;
}
}
}
- (void)gotSpsPps:(NSData*)sps pps:(NSData*)pps
{
NSLog(@"gotSpsPps %lu - %lu", (unsigned long)sps.length, (unsigned long)pps.length);
const char bytres[] = "\x00\x00\x00\x01";
size_t length = (sizeof bytres) - 1;
NSData * byteHeader = [NSData dataWithBytes:bytres length:length];
[self.videoFileHandle writeData:byteHeader];
[self.videoFileHandle writeData:sps];
[self.videoFileHandle writeData:byteHeader];
[self.videoFileHandle writeData:pps];
}
- (void)gotEncodedData:(NSData*)data isKeyFrame:(BOOL)isKeyFrame
{
NSLog(@"gotEncodedData = %lu", (unsigned long)data.length);
if (self.videoFileHandle != NULL) {
const char bytres[] = "\x00\x00\x00\x01";
size_t length = (sizeof bytres) - 1;
NSData * byteHeader = [NSData dataWithBytes:bytres length:length];
[self.videoFileHandle writeData:byteHeader];
[self.videoFileHandle writeData:data];
}
}
#pragma mark - get
- (dispatch_queue_t)encodeQueue {
if (!_encodeQueue) {
_encodeQueue = dispatch_queue_create("encode_video_queue", DISPATCH_QUEUE_SERIAL);
}
return _encodeQueue;
}
- (NSFileHandle *)videoFileHandle {
if (!_videoFileHandle) {
NSString * filePath = [NSHomeDirectory() stringByAppendingPathComponent:@"/Documents/demo.h264"];
[[NSFileManager defaultManager] removeItemAtPath:filePath error:nil];
BOOL createFile = [[NSFileManager defaultManager] createFileAtPath:filePath contents:nil attributes:nil];
NSAssert(createFile, @"create video path error");
_videoFileHandle = [NSFileHandle fileHandleForWritingAtPath:filePath];
}
return _videoFileHandle;
}
- 編碼資料檢視
在Info.plist中新增Application supports iTunes file sharing = YES,然後再Mac上面通過ITunes獲取對應應用下面編碼的視訊檔案即可。