iOS 基于ffmpeg的音視頻編、解碼以及播放器的制作

最近在學習音視頻的相關知識,在接觸到ffmpeg庫后嘗試著使用其編寫了一個視頻播放器


demo截圖

音視頻解碼

視頻播放器播放一個互聯網上的視頻文件,需要經過以下幾個步驟:解協議,解封裝,解碼視音頻,視音頻同步。如果播放本地文件則不需要解協議,為以下幾個步驟:解封裝,解碼視音頻,視音頻同步。他們的過程如圖所示。


本文示例使用的是本地視頻文件,對解碼過程中使用到的api不做過多講解,具體的api介紹可以參考雷神的博客,或者閱讀demo中“FFMpeg解碼中”解碼音頻、解碼視頻的文件。解碼的步驟如下圖所示,新版的ffmpeg已經不需要使用av_register_all(),圖片來源于網絡

解碼后得到的音頻數據采用AudioQueue進行播放,視頻數據使用OpenGL ES來進行展示,具體可以參照文章末尾處的demo

關于音視頻的同步,有三種方式:

  • 參考一個外部時鐘,將音頻與視頻同步至此時間
  • 以視頻為基準,音頻去同步視頻的時間
  • 以音頻為基準,視頻去同步音頻的時間

由于某些生物學的原理,人對聲音的變化比較敏感,但是對視覺變化不太敏感。所以頻繁的去調整聲音的播放會有些刺耳或者雜音吧影響用戶體驗,所以普遍使用第三種方式來做音視頻同步

音視頻編碼

音頻的錄制采用AudioUnit,音頻的編碼使用AudioConverterRef

//輸入
AudioBuffer encodeBuffer;
encodeBuffer.mNumberChannels = inBuffer->mNumberChannels;
encodeBuffer.mDataByteSize = (UInt32)bufferLengthPerConvert;
encodeBuffer.mData = current;


UInt32 packetPerConvert = PACKET_PER_CONVERT;

//輸出
AudioBufferList outputBuffers;
outputBuffers.mNumberBuffers = 1;
outputBuffers.mBuffers[0].mNumberChannels =inBuffer->mNumberChannels;
outputBuffers.mBuffers[0].mDataByteSize = outPacketLength*packetPerConvert;
outputBuffers.mBuffers[0].mData = _convertedDataBuf;
memset(_convertedDataBuf, 0, bufferLengthPerConvert);

OSStatus status = AudioConverterFillComplexBuffer(_audioConverter, convertDataProc, &encodeBuffer, &packetPerConvert, &outputBuffers, NULL);
if (status != noErr) {
    NSLog(@"轉換出錯");
}
//        TMSCheckStatusUnReturn(status, @"轉換出錯");

if (current == leftBuf) {
    current = inBuffer->mData + bufferLengthPerConvert - lastLeftLength;
}else{
    current += bufferLengthPerConvert;
}
leftLength -= bufferLengthPerConvert;

//輸出數據到下一個環節
//        NSLog(@"output buffer size:%d",outputBuffers.mBuffers[0].mDataByteSize);
self.bufferData->bufferList = &outputBuffers;
self.bufferData->inNumberFrames = packetPerConvert*_outputDesc.mFramesPerPacket;  //包數 * 每個包的幀數(幀數+采樣率計算時長)
[self transportAudioBuffersToNext];

視頻的錄制采用AVCaptureSession,視頻的編碼使用ffmpeg

- (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.根據像素獲取圖片的真實寬度&高度
        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);
        
        // 3.4.將NV12數據轉成YUV420P(I420)數據
        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的數據
        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;
        
        // 4.設置寬度高度以及YUV格式
        pFrame->width = encoder_h264_frame_width;
        pFrame->height = encoder_h264_frame_height;
        pFrame->format = AV_PIX_FMT_YUV420P;
        
        // 5.對編碼前的原始數據(AVFormat)利用編碼器進行編碼,將 pFrame 編碼后的數據傳入pkt 中
        int ret = avcodec_send_frame(pCodecCtx, pFrame);
        if (ret != 0) {
            printf("Failed to encode! \n");
            return;
        }
        
        while (avcodec_receive_packet(pCodecCtx, &pkt) == 0) {
            framecnt++;
            pkt.stream_index = video_st->index;
            //也可以使用C語言函數:fwrite()、fflush()寫文件和清空文件寫入緩沖區。
//            ret = av_write_frame(pFormatCtx, &pkt);
            fwrite(pkt.data, 1, pkt.size, file);
            if (ret < 0) {
                printf("Failed write to file!\n");
            }
            //釋放packet
            av_packet_unref(&pkt);
        }
        
        // 7.釋放yuv數據
        free(yuv420_data);
    }
    
    CVPixelBufferUnlockBaseAddress(imageBuffer, 0);
}

編碼后得到的h264文件通過H264BSAnalyzer解析發現,每個IDR幀之前都含有SPS和PPS,說明此種方式進行的編碼可用于網絡流的傳輸

視頻封裝

本文示例將H264和AAC封裝成FLV,封裝流程示意圖如下,具體代碼實現請參照文章末尾處demo


直播推流

推流:使用的是LFLiveKit三方庫
拉流:可以使用ijkplayer,也可以使用mac端的VLC播放器
服務器:nginx
具體的配置及使用可以參考這里

由于ffmpeg庫占用空間過大,需自行引入方可運行
demo下載

參考文章:
雷神博客
https://github.com/czqasngit/ffmpeg-player
http://www.lxweimin.com/p/ba5045da282c

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容