1. 程式人生 > >AVFounction學習筆記之--VideoToolbox視訊硬編碼

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獲取對應應用下面編碼的視訊檔案即可。
在這裡插入圖片描述