1小時學會:最簡單的iOS直播推流(七)h264/aac 硬編碼

hard_man發表於2018-01-17

最簡單的iOS 推流程式碼,視訊捕獲,軟編碼(faac,x264),硬編碼(aac,h264),美顏,flv編碼,rtmp協議,陸續更新程式碼解析,你想學的知識這裡都有,願意懂直播技術的同學快來看!!

原始碼:https://github.com/hardman/AWLive

前面已經介紹瞭如何從硬體裝置獲取到音視訊資料(pcm,NV12)。

但是我們需要的視訊格式是 aac和 h264。

現在就介紹一下如何將pcm編碼aac,將NV12資料編碼為h264。

編碼分為軟編碼和硬編碼。

硬編碼是系統提供的,由系統專門嵌入的硬體裝置處理音視訊編碼,主要計算操作在對應的硬體中。硬編碼的特點是,速度快,cpu佔用少,但是不夠靈活,只能使用一些特定的功能。

軟編碼是指,通過程式碼計算進行資料編碼,主要計算操作在cpu中。軟編碼的特點是,靈活,多樣,功能豐富可擴充套件,但是cpu佔用較多。

在程式碼中,編碼器是通過AWEncoderManager獲取的。

AWENcoderManager是一個工廠,通過audioEncoderType和videoEncoderType指定編碼器型別。

編碼器分為兩類,音訊編碼器(AWAudioEncoder),視訊編碼器(AWVideoEncoder)。

音視訊編碼器又分別分為硬編碼(在HW目錄中)和軟編碼(在SW目錄中)。

所以編碼部分主要有4個檔案:硬編碼H264(AWHWH264Encoder),硬編碼AAC(AWHWAACEncoder),軟編碼AAC(AWSWFaacEncoder),軟編碼H264(AWSWX264Encoder)

硬編碼H264

第一步,開啟硬編碼器

-(void)open{
    //建立 video encode session
    // 建立 video encode session
    // 傳入視訊寬高,編碼型別:kCMVideoCodecType_H264
    // 編碼回撥:vtCompressionSessionCallback,這個回撥函式為編碼結果回撥,編碼成功後,會將資料傳入此回撥中。
    // (__bridge void * _Nullable)(self):這個引數會被原封不動地傳入vtCompressionSessionCallback中,此引數為編碼回撥同外界通訊的唯一引數。
    // &_vEnSession,c語言可以給傳入引數賦值。在函式內部會分配記憶體並初始化_vEnSession。
    OSStatus status = VTCompressionSessionCreate(NULL, (int32_t)(self.videoConfig.pushStreamWidth), (int32_t)self.videoConfig.pushStreamHeight, kCMVideoCodecType_H264, NULL, NULL, NULL, vtCompressionSessionCallback, (__bridge void * _Nullable)(self), &_vEnSession);
    if (status == noErr) {
        // 設定引數
        // ProfileLevel,h264的協議等級,不同的清晰度使用不同的ProfileLevel。
        VTSessionSetProperty(_vEnSession, kVTCompressionPropertyKey_ProfileLevel, kVTProfileLevel_H264_Main_AutoLevel);
        // 設定位元速率
        VTSessionSetProperty(_vEnSession, kVTCompressionPropertyKey_AverageBitRate, (__bridge CFTypeRef)@(self.videoConfig.bitrate));
        // 設定實時編碼
        VTSessionSetProperty(_vEnSession, kVTCompressionPropertyKey_RealTime, kCFBooleanTrue);
        // 關閉重排Frame,因為有了B幀(雙向預測幀,根據前後的影像計算出本幀)後,編碼順序可能跟顯示順序不同。此引數可以關閉B幀。
        VTSessionSetProperty(_vEnSession, kVTCompressionPropertyKey_AllowFrameReordering, kCFBooleanFalse);
        // 關鍵幀最大間隔,關鍵幀也就是I幀。此處表示關鍵幀最大間隔為2s。
        VTSessionSetProperty(_vEnSession, kVTCompressionPropertyKey_MaxKeyFrameInterval, (__bridge CFTypeRef)@(self.videoConfig.fps * 2));
        // 關於B幀 P幀 和I幀,請參考:http://blog.csdn.net/abcjennifer/article/details/6577934
        
        //引數設定完畢,準備開始,至此初始化完成,隨時來資料,隨時編碼
        status = VTCompressionSessionPrepareToEncodeFrames(_vEnSession);
        if (status != noErr) {
            [self onErrorWithCode:AWEncoderErrorCodeVTSessionPrepareFailed des:@"硬編碼vtsession prepare失敗"];
        }
    }else{
        [self onErrorWithCode:AWEncoderErrorCodeVTSessionCreateFailed des:@"硬編碼vtsession建立失敗"];
    }
}
複製程式碼

第二步,向編碼器丟資料:

//這裡的引數yuvData就是從相機獲取的NV12資料。
-(aw_flv_video_tag *)encodeYUVDataToFlvTag:(NSData *)yuvData{
    if (!_vEnSession) {
        return NULL;
    }
    //yuv 變成 轉CVPixelBufferRef
    OSStatus status = noErr;
    
    //視訊寬度
    size_t pixelWidth = self.videoConfig.pushStreamWidth;
    //視訊高度
    size_t pixelHeight = self.videoConfig.pushStreamHeight;

    //現在要把NV12資料放入 CVPixelBufferRef中,因為 硬編碼主要呼叫VTCompressionSessionEncodeFrame函式,此函式不接受yuv資料,但是接受CVPixelBufferRef型別。
    CVPixelBufferRef pixelBuf = NULL;
    //初始化pixelBuf,資料型別是kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange,此型別資料格式同NV12格式相同。
    CVPixelBufferCreate(NULL, pixelWidth, pixelHeight, kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange, NULL, &pixelBuf);
    
    // Lock address,鎖定資料,應該是多執行緒防止重入操作。
    if(CVPixelBufferLockBaseAddress(pixelBuf, 0) != kCVReturnSuccess){
        [self onErrorWithCode:AWEncoderErrorCodeLockSampleBaseAddressFailed des:@"encode video lock base address failed"];
        return NULL;
    }
    
    //將yuv資料填充到CVPixelBufferRef中
    size_t y_size = pixelWidth * pixelHeight;
    size_t uv_size = y_size / 4;
    uint8_t *yuv_frame = (uint8_t *)yuvData.bytes;
    
    //處理y frame
    uint8_t *y_frame = CVPixelBufferGetBaseAddressOfPlane(pixelBuf, 0);
    memcpy(y_frame, yuv_frame, y_size);
    
    uint8_t *uv_frame = CVPixelBufferGetBaseAddressOfPlane(pixelBuf, 1);
    memcpy(uv_frame, yuv_frame + y_size, uv_size * 2);
    
    //硬編碼 CmSampleBufRef
    
    //時間戳
    uint32_t ptsMs = self.manager.timestamp + 1; //self.vFrameCount++ * 1000.f / self.videoConfig.fps;
    
    CMTime pts = CMTimeMake(ptsMs, 1000);
    
    //硬編碼主要其實就這一句。將攜帶NV12資料的PixelBuf送到硬編碼器中,進行編碼。
    status = VTCompressionSessionEncodeFrame(_vEnSession, pixelBuf, pts, kCMTimeInvalid, NULL, pixelBuf, NULL);

    ... ...
}
複製程式碼

第三步,通過硬編碼回撥獲取h264資料

static void vtCompressionSessionCallback (void * CM_NULLABLE outputCallbackRefCon,
                                          void * CM_NULLABLE sourceFrameRefCon,
                                          OSStatus status,
                                          VTEncodeInfoFlags infoFlags,
                                          CM_NULLABLE CMSampleBufferRef sampleBuffer ){
    //通過outputCallbackRefCon獲取AWHWH264Encoder的物件指標,將編碼好的h264資料傳出去。
    AWHWH264Encoder *encoder = (__bridge AWHWH264Encoder *)(outputCallbackRefCon);

    //判斷是否編碼成功
    if (status != noErr) {
        dispatch_semaphore_signal(encoder.vSemaphore);
        [encoder onErrorWithCode:AWEncoderErrorCodeEncodeVideoFrameFailed des:@"encode video frame error 1"];
        return;
    }
    
    //是否資料是完整的
    if (!CMSampleBufferDataIsReady(sampleBuffer)) {
        dispatch_semaphore_signal(encoder.vSemaphore);
        [encoder onErrorWithCode:AWEncoderErrorCodeEncodeVideoFrameFailed des:@"encode video frame error 2"];
        return;
    }
    
    //是否是關鍵幀,關鍵幀和非關鍵幀要區分清楚。推流時也要註明。 
    BOOL isKeyFrame = !CFDictionaryContainsKey( (CFArrayGetValueAtIndex(CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, true), 0)), kCMSampleAttachmentKey_NotSync);
    
    //首先獲取sps 和pps
    //sps pss 也是h264的一部分,可以認為它們是特別的h264視訊幀,儲存了h264視訊的一些必要資訊。
    //沒有這部分資料h264視訊很難解析出來。
    //資料處理時,sps pps 資料可以作為一個普通h264幀,放在h264視訊流的最前面。
    BOOL needSpsPps = NO;
    if (!encoder.spsPpsData) {
        if (isKeyFrame) {
            //獲取avcC,這就是我們想要的sps和pps資料。
            //如果儲存到檔案中,需要將此資料前加上 [0 0 0 1] 4個位元組,寫入到h264檔案的最前面。
            //如果推流,將此資料放入flv資料區即可。
            CMFormatDescriptionRef sampleBufFormat = CMSampleBufferGetFormatDescription(sampleBuffer);
            NSDictionary *dict = (__bridge NSDictionary *)CMFormatDescriptionGetExtensions(sampleBufFormat);
            encoder.spsPpsData = dict[@"SampleDescriptionExtensionAtoms"][@"avcC"];
        }
        needSpsPps = YES;
    }
    
    //獲取真正的視訊幀資料
    CMBlockBufferRef blockBuffer = CMSampleBufferGetDataBuffer(sampleBuffer);
    size_t blockDataLen;
    uint8_t *blockData;
    status = CMBlockBufferGetDataPointer(blockBuffer, 0, NULL, &blockDataLen, (char **)&blockData);
    if (status == noErr) {
        size_t currReadPos = 0;
        //一般情況下都是隻有1幀,在最開始編碼的時候有2幀,取最後一幀
        while (currReadPos < blockDataLen - 4) {
            uint32_t naluLen = 0;
            memcpy(&naluLen, blockData + currReadPos, 4);
            naluLen = CFSwapInt32BigToHost(naluLen);
            
            //naluData 即為一幀h264資料。
            //如果儲存到檔案中,需要將此資料前加上 [0 0 0 1] 4個位元組,按順序寫入到h264檔案中。
            //如果推流,需要將此資料前加上4個位元組表示資料長度的數字,此資料需轉為大端位元組序。
            //關於大端和小端模式,請參考此網址:http://blog.csdn.net/hackbuteer1/article/details/7722667
            encoder.naluData = [NSData dataWithBytes:blockData + currReadPos + 4 length:naluLen];
            
            currReadPos += 4 + naluLen;
            
            encoder.isKeyFrame = isKeyFrame;
        }
    }else{
        [encoder onErrorWithCode:AWEncoderErrorCodeEncodeGetH264DataFailed des:@"got h264 data failed"];
    }
    
    ... ...
}
複製程式碼

第四步,其實,此時硬編碼已結束,這一步跟編碼無關,將取得的h264資料,送到推流器中。

-(aw_flv_video_tag *)encodeYUVDataToFlvTag:(NSData *)yuvData{
    
    ... ...
    
    if (status == noErr) {
        dispatch_semaphore_wait(self.vSemaphore, DISPATCH_TIME_FOREVER);
        if (_naluData) {
            //此處 硬編碼成功,_naluData內的資料即為h264視訊幀。
            //我們是推流,所以獲取幀長度,轉成大端位元組序,放到資料的最前面
            uint32_t naluLen = (uint32_t)_naluData.length;
            //小端轉大端。計算機內一般都是小端,而網路和檔案中一般都是大端。大端轉小端和小端轉大端演算法一樣,就是位元組序反轉就行了。
            uint8_t naluLenArr[4] = {naluLen >> 24 & 0xff, naluLen >> 16 & 0xff, naluLen >> 8 & 0xff, naluLen & 0xff};
            //將資料拼在一起
            NSMutableData *mutableData = [NSMutableData dataWithBytes:naluLenArr length:4];
            [mutableData appendData:_naluData];

            //將h264資料合成flv tag,合成flvtag之後就可以直接傳送到服務端了。後續會介紹
            aw_flv_video_tag *video_tag = aw_encoder_create_video_tag((int8_t *)mutableData.bytes, mutableData.length, ptsMs, 0, self.isKeyFrame);
            
            //到此,編碼工作完成,清除狀態。
            _naluData = nil;
            _isKeyFrame = NO;
            
            CVPixelBufferUnlockBaseAddress(pixelBuf, 0);
            
            CFRelease(pixelBuf);
            
            return video_tag;
        }
    }else{
        [self onErrorWithCode:AWEncoderErrorCodeEncodeVideoFrameFailed des:@"encode video frame error"];
    }
    CVPixelBufferUnlockBaseAddress(pixelBuf, 0);
    
    CFRelease(pixelBuf);
    
    return NULL;
複製程式碼

第五步,關閉編碼器

//永遠不忘記關閉釋放資源。
-(void)close{
    dispatch_semaphore_signal(self.vSemaphore);
    
    VTCompressionSessionInvalidate(_vEnSession);
    _vEnSession = nil;
    
    self.naluData = nil;
    self.isKeyFrame = NO;
    self.spsPpsData = nil;
}
複製程式碼

硬編碼AAC

硬編碼AAC邏輯同H264差不多。

第一步,開啟編碼器

-(void)open{
    //建立audio encode converter也就是AAC編碼器
    //初始化一系列引數
    AudioStreamBasicDescription inputAudioDes = {
        .mFormatID = kAudioFormatLinearPCM,
        .mSampleRate = self.audioConfig.sampleRate,
        .mBitsPerChannel = (uint32_t)self.audioConfig.sampleSize,
        .mFramesPerPacket = 1,//每個包1幀
        .mBytesPerFrame = 2,//每幀2位元組
        .mBytesPerPacket = 2,//每個包1幀也是2位元組
        .mChannelsPerFrame = (uint32_t)self.audioConfig.channelCount,//聲道數,推流一般使用單聲道
        //下面這個flags的設定參照此文:http://www.mamicode.com/info-detail-986202.html
        .mFormatFlags = kLinearPCMFormatFlagIsPacked | kLinearPCMFormatFlagIsSignedInteger | kLinearPCMFormatFlagIsNonInterleaved,
        .mReserved = 0
    };
    
    //設定輸出格式,聲道數
    AudioStreamBasicDescription outputAudioDes = {
        .mChannelsPerFrame = (uint32_t)self.audioConfig.channelCount,
        .mFormatID = kAudioFormatMPEG4AAC,
        0
    };
    
    //初始化_aConverter
    uint32_t outDesSize = sizeof(outputAudioDes);
    AudioFormatGetProperty(kAudioFormatProperty_FormatInfo, 0, NULL, &outDesSize, &outputAudioDes);
    OSStatus status = AudioConverterNew(&inputAudioDes, &outputAudioDes, &_aConverter);
    if (status != noErr) {
        [self onErrorWithCode:AWEncoderErrorCodeCreateAudioConverterFailed des:@"硬編碼AAC建立失敗"];
    }
    
    //設定位元速率
    uint32_t aBitrate = (uint32_t)self.audioConfig.bitrate;
    uint32_t aBitrateSize = sizeof(aBitrate);
    status = AudioConverterSetProperty(_aConverter, kAudioConverterEncodeBitRate, aBitrateSize, &aBitrate);
    
    //查詢最大輸出
    uint32_t aMaxOutput = 0;
    uint32_t aMaxOutputSize = sizeof(aMaxOutput);
    AudioConverterGetProperty(_aConverter, kAudioConverterPropertyMaximumOutputPacketSize, &aMaxOutputSize, &aMaxOutput);
    self.aMaxOutputFrameSize = aMaxOutput;
    if (aMaxOutput == 0) {
        [self onErrorWithCode:AWEncoderErrorCodeAudioConverterGetMaxFrameSizeFailed des:@"AAC 獲取最大frame size失敗"];
    }
}
複製程式碼

第二步,獲取audio specific config,這是一個特別的flv tag,儲存了使用的aac的一些關鍵資料,作為解析音訊幀的基礎。 在rtmp中,必須將此幀在所有音訊幀之前傳送。

-(aw_flv_audio_tag *)createAudioSpecificConfigFlvTag{
    //profile,表示使用的協議
    uint8_t profile = kMPEG4Object_AAC_LC;
    //取樣率
    uint8_t sampleRate = 4;
    //channel資訊
    uint8_t chanCfg = 1;
    //將上面3個資訊拼在一起,成為2位元組
    uint8_t config1 = (profile << 3) | ((sampleRate & 0xe) >> 1);
    uint8_t config2 = ((sampleRate & 0x1) << 7) | (chanCfg << 3);
    
    //將資料轉成aw_data
    aw_data *config_data = NULL;
    data_writer.write_uint8(&config_data, config1);
    data_writer.write_uint8(&config_data, config2);
    
    //轉成flv tag
    aw_flv_audio_tag *audio_specific_config_tag = aw_encoder_create_audio_specific_config_tag(config_data, &_faacConfig);
    
    free_aw_data(&config_data);
    
    //返回給呼叫方,準備傳送
    return audio_specific_config_tag;
}
複製程式碼

第三步:當從麥克風獲取到音訊資料時,將資料交給AAC編碼器編碼。

-(aw_flv_audio_tag *)encodePCMDataToFlvTag:(NSData *)pcmData{
    self.curFramePcmData = pcmData;
    
    //構造輸出結構體,編碼器需要
    AudioBufferList outAudioBufferList = {0};
    outAudioBufferList.mNumberBuffers = 1;
    outAudioBufferList.mBuffers[0].mNumberChannels = (uint32_t)self.audioConfig.channelCount;
    outAudioBufferList.mBuffers[0].mDataByteSize = self.aMaxOutputFrameSize;
    outAudioBufferList.mBuffers[0].mData = malloc(self.aMaxOutputFrameSize);
    
    uint32_t outputDataPacketSize = 1;
    
    //執行編碼,此處需要傳一個回撥函式aacEncodeInputDataProc,以同步的方式,在回撥中填充pcm資料。
    OSStatus status = AudioConverterFillComplexBuffer(_aConverter, aacEncodeInputDataProc, (__bridge void * _Nullable)(self), &outputDataPacketSize, &outAudioBufferList, NULL);
    if (status == noErr) {
        //編碼成功,獲取資料
        NSData *rawAAC = [NSData dataWithBytes: outAudioBufferList.mBuffers[0].mData length:outAudioBufferList.mBuffers[0].mDataByteSize];
        //時間戳(ms) = 1000 * 每秒取樣數 / 取樣率;
        self.manager.timestamp += 1024 * 1000 / self.audioConfig.sampleRate;
        //獲取到aac資料,轉成flv audio tag,傳送給服務端。
        return aw_encoder_create_audio_tag((int8_t *)rawAAC.bytes, rawAAC.length, (uint32_t)self.manager.timestamp, &_faacConfig);
    }else{
        //編碼錯誤
        [self onErrorWithCode:AWEncoderErrorCodeAudioEncoderFailed des:@"aac 編碼錯誤"];
    }
    
    return NULL;
}

//回撥函式,系統指定格式
static OSStatus aacEncodeInputDataProc(AudioConverterRef inAudioConverter, UInt32 *ioNumberDataPackets, AudioBufferList *ioData, AudioStreamPacketDescription **outDataPacketDescription, void *inUserData){
    AWHWAACEncoder *hwAacEncoder = (__bridge AWHWAACEncoder *)inUserData;
    //將pcm資料交給編碼器
    if (hwAacEncoder.curFramePcmData) {
        ioData->mBuffers[0].mData = (void *)hwAacEncoder.curFramePcmData.bytes;
        ioData->mBuffers[0].mDataByteSize = (uint32_t)hwAacEncoder.curFramePcmData.length;
        ioData->mNumberBuffers = 1;
        ioData->mBuffers[0].mNumberChannels = (uint32_t)hwAacEncoder.audioConfig.channelCount;
        
        return noErr;
    }
    
    return -1;
}

複製程式碼

第四步:關閉編碼器釋放資源

-(void)close{
    AudioConverterDispose(_aConverter);
    _aConverter = nil;
    self.curFramePcmData = nil;
    self.aMaxOutputFrameSize = 0;
}
複製程式碼

文章列表

  1. 1小時學會:最簡單的iOS直播推流(一)專案介紹
  2. 1小時學會:最簡單的iOS直播推流(二)程式碼架構概述
  3. 1小時學會:最簡單的iOS直播推流(三)使用系統介面捕獲音視訊
  4. 1小時學會:最簡單的iOS直播推流(四)如何使用GPUImage,如何美顏
  5. 1小時學會:最簡單的iOS直播推流(五)yuv、pcm資料的介紹和獲取
  6. 1小時學會:最簡單的iOS直播推流(六)h264、aac、flv介紹
  7. 1小時學會:最簡單的iOS直播推流(七)h264/aac 硬編碼
  8. 1小時學會:最簡單的iOS直播推流(八)h264/aac 軟編碼
  9. 1小時學會:最簡單的iOS直播推流(九)flv 編碼與音視訊時間戳同步
  10. 1小時學會:最簡單的iOS直播推流(十)librtmp使用介紹
  11. 1小時學會:最簡單的iOS直播推流(十一)sps&pps和AudioSpecificConfig介紹(完結)

相關文章