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

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


demo截圖

音視頻解碼

視頻播放器播放一個互聯(lián)網(wǎng)上的視頻文件借杰,需要經(jīng)過以下幾個步驟:解協(xié)議,解封裝进泼,解碼視音頻蔗衡,視音頻同步。如果播放本地文件則不需要解協(xié)議乳绕,為以下幾個步驟:解封裝绞惦,解碼視音頻,視音頻同步洋措。他們的過程如圖所示济蝉。


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

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

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

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

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

音視頻編碼

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

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

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

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

- (void)encoderToH264:(CMSampleBufferRef)sampleBuffer
{
    // 1.通過CMSampleBufferRef對象獲取CVPixelBufferRef對象
    CVPixelBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
    
    // 2.鎖定imageBuffer內(nèi)存地址開始進行編碼
    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);
        
        // 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.設置當前幀
        pFrame->pts = framecnt;
        
        // 4.設置寬度高度以及YUV格式
        pFrame->width = encoder_h264_frame_width;
        pFrame->height = encoder_h264_frame_height;
        pFrame->format = AV_PIX_FMT_YUV420P;
        
        // 5.對編碼前的原始數(shù)據(jù)(AVFormat)利用編碼器進行編碼澳腹,將 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語言函數(shù):fwrite()织盼、fflush()寫文件和清空文件寫入緩沖區(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文件通過H264BSAnalyzer解析發(fā)現(xiàn)沥邻,每個IDR幀之前都含有SPS和PPS,說明此種方式進行的編碼可用于網(wǎng)絡流的傳輸

視頻封裝

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


直播推流

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

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

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末邮利,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子垃帅,更是在濱河造成了極大的恐慌延届,老刑警劉巖,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件挺智,死亡現(xiàn)場離奇詭異祷愉,居然都是意外死亡窗宦,警方通過查閱死者的電腦和手機赦颇,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來赴涵,“玉大人媒怯,你說我怎么就攤上這事∷璐埽” “怎么了扇苞?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵欺殿,是天一觀的道長。 經(jīng)常有香客問我鳖敷,道長脖苏,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任定踱,我火速辦了婚禮棍潘,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘崖媚。我一直安慰自己亦歉,他們只是感情好,可當我...
    茶點故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布畅哑。 她就那樣靜靜地躺著肴楷,像睡著了一般。 火紅的嫁衣襯著肌膚如雪荠呐。 梳的紋絲不亂的頭發(fā)上赛蔫,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天,我揣著相機與錄音直秆,去河邊找鬼濒募。 笑死,一個胖子當著我的面吹牛圾结,可吹牛的內(nèi)容都是我干的瑰剃。 我是一名探鬼主播,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼筝野,長吁一口氣:“原來是場噩夢啊……” “哼晌姚!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起歇竟,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤挥唠,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后焕议,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體宝磨,經(jīng)...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年盅安,在試婚紗的時候發(fā)現(xiàn)自己被綠了唤锉。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,161評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡别瞭,死狀恐怖窿祥,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情蝙寨,我是刑警寧澤晒衩,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布嗤瞎,位于F島的核電站,受9級特大地震影響听系,放射性物質(zhì)發(fā)生泄漏贝奇。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一靠胜、第九天 我趴在偏房一處隱蔽的房頂上張望弃秆。 院中可真熱鬧,春花似錦髓帽、人聲如沸菠赚。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽衡查。三九已至,卻和暖如春必盖,著一層夾襖步出監(jiān)牢的瞬間拌牲,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工歌粥, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留塌忽,地道東北人。 一個月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓失驶,卻偏偏與公主長得像土居,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子嬉探,可洞房花燭夜當晚...
    茶點故事閱讀 42,916評論 2 344

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