iOS直播技術學習筆記-硬編碼&軟編碼實現(xiàn)(五)

iOS硬編碼實現(xiàn)

前言

  • 在上一篇中,我們已經(jīng)知道iOS編碼的一些概念知識,從現(xiàn)在開始,我們可以正式對采集到的視頻進行編碼
  • 這里我們重點介紹硬編碼的使用方式,也就是VideoToolBox框架的使用
  • 編碼的流程:采集--> 獲取到視頻幀--> 對視頻幀進行編碼 --> 獲取到視頻幀信息 --> 將編碼后的數(shù)據(jù)以NALU方式寫入到文件

視頻采集

  • 視頻采集我們已經(jīng)在前面進行了介紹和學習,所有這里就直接貼代碼,只是我對采集過程進行了一些簡單的封裝
視頻采集.png

視頻硬件編碼

  • 初始化壓縮編碼會話(VTCompressionSessionRef)
    • 在VideoToolbox框架的使用過程中,基本都是C語言函數(shù)
  • 初始化后通過VTSessionSetProperty設置對象屬性
    • 編碼方式:H.264編碼
    • 幀率:每秒鐘多少幀畫面
    • 碼率:單位時間內保存的數(shù)據(jù)量
    • 關鍵幀(GOPsize)間隔:多少幀為一個GOP
  • 準備編碼
  • 代碼如下:
- (void)setupVideoSession {
    // 1.用于記錄當前是第幾幀數(shù)據(jù)(畫面幀數(shù)非常多)
    self.frameID = 0;

    // 2.錄制視頻的寬度&高度
    int width = [UIScreen mainScreen].bounds.size.width;
    int height = [UIScreen mainScreen].bounds.size.height;

    // 3.創(chuàng)建CompressionSession對象,該對象用于對畫面進行編碼
    // kCMVideoCodecType_H264 : 表示使用h.264進行編碼
    // didCompressH264 : 當一次編碼結束會在該函數(shù)進行回調,可以在該函數(shù)中將數(shù)據(jù),寫入文件中
    VTCompressionSessionCreate(NULL, width, height, kCMVideoCodecType_H264, NULL, NULL, NULL, didCompressH264, (__bridge void *)(self),  &_compressionSession);

    // 4.設置實時編碼輸出(直播必然是實時輸出,否則會有延遲)
    VTSessionSetProperty(self.compressionSession, kVTCompressionPropertyKey_RealTime, kCFBooleanTrue);

    // 5.設置期望幀率(每秒多少幀,如果幀率過低,會造成畫面卡頓)
    int fps = 30;
    CFNumberRef  fpsRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &fps);
    VTSessionSetProperty(self.compressionSession, kVTCompressionPropertyKey_ExpectedFrameRate, fpsRef);

    // 6.設置碼率(碼率: 編碼效率, 碼率越高,則畫面越清晰, 如果碼率較低會引起馬賽克 --> 碼率高有利于還原原始畫面,但是也不利于傳輸)
    int bitRate = 800*1024;
    CFNumberRef bitRateRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &bitRate);
    VTSessionSetProperty(self.compressionSession, kVTCompressionPropertyKey_AverageBitRate, bitRateRef);
    NSArray *limit = @[@(bitRate * 1.5/8), @(1)];
    VTSessionSetProperty(self.compressionSession, kVTCompressionPropertyKey_DataRateLimits, (__bridge CFArrayRef)limit);

    // 7.設置關鍵幀(GOPsize)間隔
    int frameInterval = 30;
    CFNumberRef  frameIntervalRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &frameInterval);
    VTSessionSetProperty(self.compressionSession, kVTCompressionPropertyKey_MaxKeyFrameInterval, frameIntervalRef);

    // 8.基本設置結束, 準備進行編碼
    VTCompressionSessionPrepareToEncodeFrames(self.compressionSession);
}

  • 將輸入的幀進行編碼
    • 將CMSampleBufferRef轉成CVImageBufferRef
    • 開始對CVImageBufferRef進行編碼
- (void)encodeSampleBuffer:(CMSampleBufferRef)sampleBuffer {
    // 1.將sampleBuffer轉成imageBuffer
    CVImageBufferRef imageBuffer = (CVImageBufferRef)CMSampleBufferGetImageBuffer(sampleBuffer);

    // 2.根據(jù)當前的幀數(shù),創(chuàng)建CMTime的時間
    CMTime presentationTimeStamp = CMTimeMake(self.frameID++, 1000);
    VTEncodeInfoFlags flags;

    // 3.開始編碼該幀數(shù)據(jù)
    OSStatus statusCode = VTCompressionSessionEncodeFrame(self.compressionSession,
                                                          imageBuffer,
                                                          presentationTimeStamp,
                                                          kCMTimeInvalid,
                                                          NULL, (__bridge void * _Nullable)(self), &flags);
    if (statusCode == noErr) {
        NSLog(@"H264: VTCompressionSessionEncodeFrame Success");
    }
}

  • 當編碼成功后,將編碼后的碼流寫入文件
    • 編碼成功后會回調之前輸入的函數(shù)
    • 1> 先判斷是否是關鍵幀:
      • 如果是關鍵幀,則需要在寫入關鍵幀之前,先寫入PPS、SPS的NALU
      • 取出PPS、SPS數(shù)據(jù),并且封裝成NALU單元,寫入文件
    • 2> 將I幀、P幀、B幀分別封裝成NALU單元寫入文件
    • 寫入后,數(shù)據(jù)存儲方式:
數(shù)據(jù)存儲方式.png
  • 代碼如下:
// 編碼完成回調
void didCompressH264(void *outputCallbackRefCon, void *sourceFrameRefCon, OSStatus status, VTEncodeInfoFlags infoFlags, CMSampleBufferRef sampleBuffer) {

    // 1.判斷狀態(tài)是否等于沒有錯誤
    if (status != noErr) {
        return;
    }

    // 2.根據(jù)傳入的參數(shù)獲取對象
    VideoEncoder* encoder = (__bridge VideoEncoder*)outputCallbackRefCon;

    // 3.判斷是否是關鍵幀
    bool isKeyframe = !CFDictionaryContainsKey( (CFArrayGetValueAtIndex(CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, true), 0)), kCMSampleAttachmentKey_NotSync);
    // 判斷當前幀是否為關鍵幀
    // 獲取sps & pps數(shù)據(jù)
    if (isKeyframe)
    {
        // 獲取編碼后的信息(存儲于CMFormatDescriptionRef中)
        CMFormatDescriptionRef format = CMSampleBufferGetFormatDescription(sampleBuffer);

        // 獲取SPS信息
        size_t sparameterSetSize, sparameterSetCount;
        const uint8_t *sparameterSet;
        CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 0, &sparameterSet, &sparameterSetSize, &sparameterSetCount, 0 );

        // 獲取PPS信息
        size_t pparameterSetSize, pparameterSetCount;
        const uint8_t *pparameterSet;
        CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 1, &pparameterSet, &pparameterSetSize, &pparameterSetCount, 0 );

        // 裝sps/pps轉成NSData,以方便寫入文件
        NSData *sps = [NSData dataWithBytes:sparameterSet length:sparameterSetSize];
        NSData *pps = [NSData dataWithBytes:pparameterSet length:pparameterSetSize];

        // 寫入文件
        [encoder gotSpsPps:sps pps:pps];
    }

    // 獲取數(shù)據(jù)塊
    CMBlockBufferRef dataBuffer = CMSampleBufferGetDataBuffer(sampleBuffer);
    size_t length, totalLength;
    char *dataPointer;
    OSStatus statusCodeRet = CMBlockBufferGetDataPointer(dataBuffer, 0, &length, &totalLength, &dataPointer);
    if (statusCodeRet == noErr) {
        size_t bufferOffset = 0;
        static const int AVCCHeaderLength = 4; // 返回的nalu數(shù)據(jù)前四個字節(jié)不是0001的startcode,而是大端模式的幀長度length

        // 循環(huán)獲取nalu數(shù)據(jù)
        while (bufferOffset < totalLength - AVCCHeaderLength) {
            uint32_t NALUnitLength = 0;
            // Read the NAL unit length
            memcpy(&NALUnitLength, dataPointer + bufferOffset, AVCCHeaderLength);

            // 從大端轉系統(tǒng)端
            NALUnitLength = CFSwapInt32BigToHost(NALUnitLength);

            NSData* data = [[NSData alloc] initWithBytes:(dataPointer + bufferOffset + AVCCHeaderLength) length:NALUnitLength];
            [encoder gotEncodedData:data isKeyFrame:isKeyframe];

            // 移動到寫一個塊,轉成NALU單元
            // Move to the next NAL unit in the block buffer
            bufferOffset += AVCCHeaderLength + NALUnitLength;
        }
    }
}

- (void)gotSpsPps:(NSData*)sps pps:(NSData*)pps
{
    // 1.拼接NALU的header
    const char bytes[] = "\x00\x00\x00\x01";
    size_t length = (sizeof bytes) - 1;
    NSData *ByteHeader = [NSData dataWithBytes:bytes length:length];

    // 2.將NALU的頭&NALU的體寫入文件
    [self.fileHandle writeData:ByteHeader];
    [self.fileHandle writeData:sps];
    [self.fileHandle writeData:ByteHeader];
    [self.fileHandle writeData:pps];

}
- (void)gotEncodedData:(NSData*)data isKeyFrame:(BOOL)isKeyFrame
{
    NSLog(@"gotEncodedData %d", (int)[data length]);
    if (self.fileHandle != NULL)
    {
        const char bytes[] = "\x00\x00\x00\x01";
        size_t length = (sizeof bytes) - 1; //string literals have implicit trailing '\0'
        NSData *ByteHeader = [NSData dataWithBytes:bytes length:length];
        [self.fileHandle writeData:ByteHeader];
        [self.fileHandle writeData:data];
    }
}

iOS視頻軟編碼

軟編碼介紹

  • 軟編碼主要是利用CPU進行編碼的過程, 具體的編碼通常會用FFmpeg+x264
  • FFmpeg
    • FFmpeg是一個非常強大的音視頻處理庫,包括視頻采集功能、視頻格式轉換、視頻抓圖、給視頻加水印等。
    • FFmpeg在Linux平臺下開發(fā),但它同樣也可以在其它操作系統(tǒng)環(huán)境中編譯運行,包括Windows、Mac OS X等。
  • X264
    • H.264是ITU制定的視頻編碼標準
    • 而x264是一個開源的H.264/MPEG-4 AVC視頻編碼函數(shù)庫[1] ,是最好的有損視頻編碼器,里面集成了非常多優(yōu)秀的算法用于視頻編碼.
  • 關于軟編碼推薦博客(雷霄驊)

Mac安裝/使用FFmpeg

  • 安裝
    • ruby -e "$(curl -fsSkL raw.github.com/mxcl/homebrew/go)"
    • brew install ffmpeg
  • 簡單使用
    • 轉化格式: ffmpeg -i story.webm story.mp4
    • 分離視頻: ffmpeg -i story.mp4 -vcodec copy -an demo.mp4
    • 分離音頻: ffmpeg -i story.mp4 -acodec copy -vn demo.aac

編譯FFmpeg(iOS)

編譯X264

  • 下載x264
  • 下載gas-preprocessor(FFmpeg編譯時已經(jīng)下載過)
  • 下載x264 build shell
  • 修改權限/執(zhí)行腳本
    • sudo chmod u+x build-x264.sh
    • sudo ./build-x264.sh

iOS項目中集成FFmpeg

  • 將編譯好的文件夾拖入到工程中
  • 添加依賴庫: libiconv.dylib/libz.dylib/libbz2.dylib/CoreMedia.framework/AVFoundation.framework
  • 初始化編碼器
/*
 *  設置X264
 */
- (int)setX264ResourceWithVideoWidth:(int)width height:(int)height bitrate:(int)bitrate
{
    // 1.默認從第0幀開始(記錄當前的幀數(shù))
    framecnt = 0;

    // 2.記錄傳入的寬度&高度
    encoder_h264_frame_width = width;
    encoder_h264_frame_height = height;

    // 3.注冊FFmpeg所有編解碼器(無論編碼還是解碼都需要該步驟)
    av_register_all();

    // 4.初始化AVFormatContext: 用作之后寫入視頻幀并編碼成 h264,貫穿整個工程當中(釋放資源時需要銷毀)
    pFormatCtx = avformat_alloc_context();

    // 5.設置輸出文件的路徑
    fmt = av_guess_format(NULL, out_file, NULL);
    pFormatCtx->oformat = fmt;

    // 6.打開文件的緩沖區(qū)輸入輸出,flags 標識為  AVIO_FLAG_READ_WRITE ,可讀寫
    if (avio_open(&pFormatCtx->pb, out_file, AVIO_FLAG_READ_WRITE) < 0){
        printf("Failed to open output file! \n");
        return -1;
    }

    // 7.創(chuàng)建新的輸出流, 用于寫入文件
    video_st = avformat_new_stream(pFormatCtx, 0);

    // 8.設置 20 幀每秒 ,也就是 fps 為 20
    video_st->time_base.num = 1;
    video_st->time_base.den = 25;

    if (video_st==NULL){
        return -1;
    }

    // 9.pCodecCtx 用戶存儲編碼所需的參數(shù)格式等等
    // 9.1.從媒體流中獲取到編碼結構體,他們是一一對應的關系,一個 AVStream 對應一個  AVCodecContext
    pCodecCtx = video_st->codec;

    // 9.2.設置編碼器的編碼格式(是一個id),每一個編碼器都對應著自己的 id,例如 h264 的編碼 id 就是 AV_CODEC_ID_H264
    pCodecCtx->codec_id = fmt->video_codec;

    // 9.3.設置編碼類型為 視頻編碼
    pCodecCtx->codec_type = AVMEDIA_TYPE_VIDEO;

    // 9.4.設置像素格式為 yuv 格式
    pCodecCtx->pix_fmt = PIX_FMT_YUV420P;

    // 9.5.設置視頻的寬高
    pCodecCtx->width = encoder_h264_frame_width;
    pCodecCtx->height = encoder_h264_frame_height;

    // 9.6.設置幀率
    pCodecCtx->time_base.num = 1;
    pCodecCtx->time_base.den = 15;

    // 9.7.設置碼率(比特率)
    pCodecCtx->bit_rate = bitrate;

    // 9.8.視頻質量度量標準(常見qmin=10, qmax=51)
    pCodecCtx->qmin = 10;
    pCodecCtx->qmax = 51;

    // 9.9.設置圖像組層的大小(GOP-->兩個I幀之間的間隔)
    pCodecCtx->gop_size = 250;

    // 9.10.設置 B 幀最大的數(shù)量,B幀為視頻圖片空間的前后預測幀, B 幀相對于 I、P 幀來說,壓縮率比較大,也就是說相同碼率的情況下,
    // 越多 B 幀的視頻,越清晰,現(xiàn)在很多打視頻網(wǎng)站的高清視頻,就是采用多編碼 B 幀去提高清晰度,
    // 但同時對于編解碼的復雜度比較高,比較消耗性能與時間
    pCodecCtx->max_b_frames = 5;

    // 10.可選設置
    AVDictionary *param = 0;
    // H.264
    if(pCodecCtx->codec_id == AV_CODEC_ID_H264) {
        // 通過--preset的參數(shù)調節(jié)編碼速度和質量的平衡。
        av_dict_set(&param, "preset", "slow", 0);

        // 通過--tune的參數(shù)值指定片子的類型,是和視覺優(yōu)化的參數(shù),或有特別的情況。
        // zerolatency: 零延遲,用在需要非常低的延遲的情況下,比如視頻直播的編碼
        av_dict_set(&param, "tune", "zerolatency", 0);
    }

    // 11.輸出打印信息,內部是通過printf函數(shù)輸出(不需要輸出可以注釋掉該局)
    av_dump_format(pFormatCtx, 0, out_file, 1);

    // 12.通過 codec_id 找到對應的編碼器
    pCodec = avcodec_find_encoder(pCodecCtx->codec_id);
    if (!pCodec) {
        printf("Can not find encoder! \n");
        return -1;
    }

    // 13.打開編碼器,并設置參數(shù) param
    if (avcodec_open2(pCodecCtx, pCodec,&param) < 0) {
        printf("Failed to open encoder! \n");
        return -1;
    }

    // 13.初始化原始數(shù)據(jù)對象: AVFrame
    pFrame = av_frame_alloc();

    // 14.通過像素格式(這里為 YUV)獲取圖片的真實大小,例如將 480 * 720 轉換成 int 類型
    avpicture_fill((AVPicture *)pFrame, picture_buf, pCodecCtx->pix_fmt, pCodecCtx->width, pCodecCtx->height);

    // 15.h264 封裝格式的文件頭部,基本上每種編碼都有著自己的格式的頭部,想看具體實現(xiàn)的同學可以看看 h264 的具體實現(xiàn)
    avformat_write_header(pFormatCtx, NULL);

    // 16.創(chuàng)建編碼后的數(shù)據(jù) AVPacket 結構體來存儲 AVFrame 編碼后生成的數(shù)據(jù)
    av_new_packet(&pkt, picture_size);

    // 17.設置 yuv 數(shù)據(jù)中 y 圖的寬高
    y_size = pCodecCtx->width * pCodecCtx->height;

    return 0;
}

  • 編碼每一幀數(shù)據(jù)
/*
 * 將CMSampleBufferRef格式的數(shù)據(jù)編碼成h264并寫入文件
 *
 */
- (void)encoderToH264:(CMSampleBufferRef)sampleBuffer
{
    // 1.通過CMSampleBufferRef對象獲取CVPixelBufferRef對象
    CVPixelBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);

    // 2.鎖定imageBuffer內存地址開始進行編碼
    if (CVPixelBufferLockBaseAddress(imageBuffer, 0) == kCVReturnSuccess) {
        // 3.從CVPixelBufferRef讀取YUV的值
        // NV12和NV21屬于YUV格式,是一種two-plane模式,即Y和UV分為兩個Plane,但是UV(CbCr)為交錯存儲,而不是分為三個plane
        // 3.1.獲取Y分量的地址
        UInt8 *bufferPtr = (UInt8 *)CVPixelBufferGetBaseAddressOfPlane(imageBuffer,0);
        // 3.2.獲取UV分量的地址
        UInt8 *bufferPtr1 = (UInt8 *)CVPixelBufferGetBaseAddressOfPlane(imageBuffer,1);

        // 3.3.根據(jù)像素獲取圖片的真實寬度&高度
        size_t width = CVPixelBufferGetWidth(imageBuffer);
        size_t height = CVPixelBufferGetHeight(imageBuffer);
        // 獲取Y分量長度
        size_t bytesrow0 = CVPixelBufferGetBytesPerRowOfPlane(imageBuffer,0);
        size_t bytesrow1  = CVPixelBufferGetBytesPerRowOfPlane(imageBuffer,1);
        UInt8 *yuv420_data = (UInt8 *)malloc(width * height *3/2);

        /* convert NV12 data to YUV420*/
        // 3.4.將NV12數(shù)據(jù)轉成YUV420數(shù)據(jù)
        UInt8 *pY = bufferPtr ;
        UInt8 *pUV = bufferPtr1;
        UInt8 *pU = yuv420_data + width*height;
        UInt8 *pV = pU + width*height/4;
        for(int i =0;i<height;i++)
        {
            memcpy(yuv420_data+i*width,pY+i*bytesrow0,width);
        }
        for(int j = 0;j<height/2;j++)
        {
            for(int i =0;i<width/2;i++)
            {
                *(pU++) = pUV[i<<1];
                *(pV++) = pUV[(i<<1) + 1];
            }
            pUV+=bytesrow1;
        }

        // 3.5.分別讀取YUV的數(shù)據(jù)
        picture_buf = yuv420_data;
        pFrame->data[0] = picture_buf;              // Y
        pFrame->data[1] = picture_buf+ y_size;      // U
        pFrame->data[2] = picture_buf+ y_size*5/4;  // V

        // 4.設置當前幀
        pFrame->pts = framecnt;
        int got_picture = 0;

        // 4.設置寬度高度以及YUV各式
        pFrame->width = encoder_h264_frame_width;
        pFrame->height = encoder_h264_frame_height;
        pFrame->format = PIX_FMT_YUV420P;

        // 5.對編碼前的原始數(shù)據(jù)(AVFormat)利用編碼器進行編碼,將 pFrame 編碼后的數(shù)據(jù)傳入pkt 中
        int ret = avcodec_encode_video2(pCodecCtx, &pkt, pFrame, &got_picture);
        if(ret < 0) {
            printf("Failed to encode! \n");

        }

        // 6.編碼成功后寫入 AVPacket 到 輸入輸出數(shù)據(jù)操作著 pFormatCtx 中,當然,記得釋放內存
        if (got_picture==1) {
            framecnt++;
            pkt.stream_index = video_st->index;
            ret = av_write_frame(pFormatCtx, &pkt);
            av_free_packet(&pkt);
        }

        // 7.釋放yuv數(shù)據(jù)
        free(yuv420_data);
    }

    CVPixelBufferUnlockBaseAddress(imageBuffer, 0);
}

  • 釋放資源
/*
 * 釋放資源
 */
- (void)freeX264Resource
{
    // 1.釋放AVFormatContext
    int ret = flush_encoder(pFormatCtx,0);
    if (ret < 0) {
        printf("Flushing encoder failed\n");
    }

    // 2.將還未輸出的AVPacket輸出出來
    av_write_trailer(pFormatCtx);

    // 3.關閉資源
    if (video_st){
        avcodec_close(video_st->codec);
        av_free(pFrame);
    }
    avio_close(pFormatCtx->pb);
    avformat_free_context(pFormatCtx);
}

int flush_encoder(AVFormatContext *fmt_ctx,unsigned int stream_index)
{
    int ret;
    int got_frame;
    AVPacket enc_pkt;
    if (!(fmt_ctx->streams[stream_index]->codec->codec->capabilities &
          CODEC_CAP_DELAY))
        return 0;

    while (1) {
        enc_pkt.data = NULL;
        enc_pkt.size = 0;
        av_init_packet(&enc_pkt);
        ret = avcodec_encode_video2 (fmt_ctx->streams[stream_index]->codec, &enc_pkt,
                                     NULL, &got_frame);
        av_frame_free(NULL);
        if (ret < 0)
            break;
        if (!got_frame){
            ret=0;
            break;
        }
        ret = av_write_frame(fmt_ctx, &enc_pkt);
        if (ret < 0)
            break;
    }
    return ret;
}

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容