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

最近在學(xué)習(xí)音視頻的相關(guān)知識(shí),在接觸到ffmpeg庫(kù)后嘗試著使用其編寫(xiě)了一個(gè)視頻播放器


demo截圖

音視頻解碼

視頻播放器播放一個(gè)互聯(lián)網(wǎng)上的視頻文件,需要經(jīng)過(guò)以下幾個(gè)步驟:解協(xié)議,解封裝,解碼視音頻,視音頻同步。如果播放本地文件則不需要解協(xié)議,為以下幾個(gè)步驟:解封裝,解碼視音頻,視音頻同步。他們的過(guò)程如圖所示。


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

解碼后得到的音頻數(shù)據(jù)采用AudioQueue進(jìn)行播放,視頻數(shù)據(jù)使用OpenGL ES來(lái)進(jìn)行展示,具體可以參照文章末尾處的demo

關(guān)于音視頻的同步,有三種方式:

  • 參考一個(gè)外部時(shí)鐘,將音頻與視頻同步至此時(shí)間
  • 以視頻為基準(zhǔn),音頻去同步視頻的時(shí)間
  • 以音頻為基準(zhǔn),視頻去同步音頻的時(shí)間

由于某些生物學(xué)的原理,人對(duì)聲音的變化比較敏感,但是對(duì)視覺(jué)變化不太敏感。所以頻繁的去調(diào)整聲音的播放會(huì)有些刺耳或者雜音吧影響用戶體驗(yàn),所以普遍使用第三種方式來(lái)做音視頻同步

音視頻編碼

音頻的錄制采用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(@"轉(zhuǎn)換出錯(cuò)");
}
//        TMSCheckStatusUnReturn(status, @"轉(zhuǎn)換出錯(cuò)");

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

//輸出數(shù)據(jù)到下一個(gè)環(huán)節(jié)
//        NSLog(@"output buffer size:%d",outputBuffers.mBuffers[0].mDataByteSize);
self.bufferData->bufferList = &outputBuffers;
self.bufferData->inNumberFrames = packetPerConvert*_outputDesc.mFramesPerPacket;  //包數(shù) * 每個(gè)包的幀數(shù)(幀數(shù)+采樣率計(jì)算時(shí)長(zhǎng))
[self transportAudioBuffersToNext];

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

- (void)encoderToH264:(CMSampleBufferRef)sampleBuffer
{
    // 1.通過(guò)CMSampleBufferRef對(duì)象獲取CVPixelBufferRef對(duì)象
    CVPixelBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
    
    // 2.鎖定imageBuffer內(nèi)存地址開(kāi)始進(jìn)行編碼
    if (CVPixelBufferLockBaseAddress(imageBuffer, 0) == kCVReturnSuccess) {
        // 3.從CVPixelBufferRef讀取YUV的值
        // NV12和NV21屬于YUV格式,是一種two-plane模式,即Y和UV分為兩個(gè)Plane,但是UV(CbCr)為交錯(cuò)存儲(chǔ),而不是分為三個(gè)plane
        // 3.1.獲取Y分量的地址
        UInt8 *bufferPtr = (UInt8 *)CVPixelBufferGetBaseAddressOfPlane(imageBuffer,0);
        // 3.2.獲取UV分量的地址
        UInt8 *bufferPtr1 = (UInt8 *)CVPixelBufferGetBaseAddressOfPlane(imageBuffer,1);
        
        // 3.3.根據(jù)像素獲取圖片的真實(shí)寬度&高度
        size_t width = CVPixelBufferGetWidth(imageBuffer);
        size_t height = CVPixelBufferGetHeight(imageBuffer);
        // 獲取Y分量長(zhǎng)度
        size_t bytesrow0 = CVPixelBufferGetBytesPerRowOfPlane(imageBuffer,0);
        size_t bytesrow1  = CVPixelBufferGetBytesPerRowOfPlane(imageBuffer,1);
        UInt8 *yuv420_data = (UInt8 *)malloc(width * height * 3 / 2);
        
        // 3.4.將NV12數(shù)據(jù)轉(zhuǎn)成YUV420P(I420)數(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.設(shè)置當(dāng)前幀
        pFrame->pts = framecnt;
        
        // 4.設(shè)置寬度高度以及YUV格式
        pFrame->width = encoder_h264_frame_width;
        pFrame->height = encoder_h264_frame_height;
        pFrame->format = AV_PIX_FMT_YUV420P;
        
        // 5.對(duì)編碼前的原始數(shù)據(jù)(AVFormat)利用編碼器進(jìn)行編碼,將 pFrame 編碼后的數(shù)據(jù)傳入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語(yǔ)言函數(shù):fwrite()、fflush()寫(xiě)文件和清空文件寫(xiě)入緩沖區(qū)。
//            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數(shù)據(jù)
        free(yuv420_data);
    }
    
    CVPixelBufferUnlockBaseAddress(imageBuffer, 0);
}

編碼后得到的h264文件通過(guò)H264BSAnalyzer解析發(fā)現(xiàn),每個(gè)IDR幀之前都含有SPS和PPS,說(shuō)明此種方式進(jìn)行的編碼可用于網(wǎng)絡(luò)流的傳輸

視頻封裝

本文示例將H264和AAC封裝成FLV,封裝流程示意圖如下,具體代碼實(shí)現(xiàn)請(qǐng)參照文章末尾處demo


直播推流

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

由于ffmpeg庫(kù)占用空間過(guò)大,需自行引入方可運(yùn)行
demo下載

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容