iOS完整文件拉流解析解碼同步渲染音視頻流

需求

解析文件中的音視頻流以解碼同步并將視頻渲染到屏幕上,音頻通過(guò)揚(yáng)聲器輸出.對(duì)于僅僅需要單純播放一個(gè)視頻文件可直接使用AVFoundation中上層播放器,這里是用最底層的方式實(shí)現(xiàn),可獲取原始音視頻幀數(shù)據(jù).


實(shí)現(xiàn)原理

本文主要分為三大塊,解析模塊使用FFmpeg parse文件中的音視頻流,解碼模塊使用FFmpeg或蘋(píng)果原生解碼器解碼音視頻,渲染模塊使用OpenGL將視頻流渲染到屏幕,使用Audio Queue Player將音頻以揚(yáng)聲器形式輸出.


閱讀前提

注意: 本文涉及到的所有模塊具體實(shí)現(xiàn)均在如下鏈接中,可根據(jù)需求自行查看講解部分.


代碼地址 : iOS File Player

掘金地址 : iOS File Player

簡(jiǎn)書(shū)地址 : iOS File Player

博客地址 : iOS File Player


總體架構(gòu)

1.overview

本文以解碼一個(gè).MOV媒體文件為例, 該文件中包含H.264編碼的視頻數(shù)據(jù), AAC編碼的音頻數(shù)據(jù),首先要通過(guò)FFmpeg去parse文件中的音視頻流信息,parse出來(lái)的結(jié)果保存在AVPacket結(jié)構(gòu)體中,然后分別提取音視頻幀數(shù)據(jù),音頻幀通過(guò)FFmpeg解碼器或蘋(píng)果原生框架中的Audio Converter進(jìn)行解碼,視頻通過(guò)FFmpeg或蘋(píng)果原生框架VideoToolbox中的解碼器可將數(shù)據(jù)解碼,解碼后的音頻數(shù)據(jù)格式為PCM,解碼后的視頻數(shù)據(jù)格式為YUV原始數(shù)據(jù),根據(jù)時(shí)間戳對(duì)音視頻數(shù)據(jù)進(jìn)行同步,最后將PCM數(shù)據(jù)音頻傳給Audio Queue以實(shí)現(xiàn)音頻的播放,將YUV視頻原始數(shù)據(jù)封裝為CMSampleBufferRef數(shù)據(jù)結(jié)構(gòu)并傳給OpenGL以將視頻渲染到屏幕上,至此一個(gè)完整拉取文件視頻流的操作完成.

注意: 通過(guò)網(wǎng)址拉取一個(gè)RTMP流進(jìn)行解碼播放的流程與拉取文件流基本相同, 只是需要通過(guò)socket接收音視頻數(shù)據(jù)后再完成解碼及后續(xù)流程.

簡(jiǎn)易流程

Parse
  • 創(chuàng)建AVFormatContext上下文對(duì)象: AVFormatContext *avformat_alloc_context(void);
  • 從文件中獲取上下文對(duì)象并賦值給指定對(duì)象: int avformat_open_input(AVFormatContext **ps, const char *url, AVInputFormat *fmt, AVDictionary **options)
  • 讀取文件中的流信息: int avformat_find_stream_info(AVFormatContext *ic, AVDictionary **options);
  • 獲取文件中音視頻流: m_formatContext->streams[audio/video index]e
  • 開(kāi)始parse以獲取文件中視頻幀幀: int av_read_frame(AVFormatContext *s, AVPacket *pkt);
  • 如果是視頻幀通過(guò)av_bitstream_filter_filter生成sps,pps等關(guān)鍵信息.
  • 讀取到的AVPacket即包含文件中所有的音視頻壓縮數(shù)據(jù).
解碼

通過(guò)FFmpeg解碼

  • 獲取文件流的解碼器上下文: formatContext->streams[a/v index]->codec;
  • 通過(guò)解碼器上下文找到解碼器: AVCodec *avcodec_find_decoder(enum AVCodecID id);
  • 打開(kāi)解碼器: int avcodec_open2(AVCodecContext *avctx, const AVCodec *codec, AVDictionary **options);
  • 將文件中音視頻數(shù)據(jù)發(fā)送給解碼器: int avcodec_send_packet(AVCodecContext *avctx, const AVPacket *avpkt);
  • 循環(huán)接收解碼后的音視頻數(shù)據(jù): int avcodec_receive_frame(AVCodecContext *avctx, AVFrame *frame);
  • 如果是音頻數(shù)據(jù)可能需要重新采樣以便轉(zhuǎn)成設(shè)備支持的格式播放.(借助SwrContext)

通過(guò)VideoToolbox解碼視頻

  • 將從FFmpeg中parse到的extra data中分離提取中NALU頭關(guān)鍵信息sps,pps等
  • 通過(guò)上面提取的關(guān)鍵信息創(chuàng)建視頻描述信息:CMVideoFormatDescriptionRef, CMVideoFormatDescriptionCreateFromH264ParameterSets / CMVideoFormatDescriptionCreateFromHEVCParameterSets
  • 創(chuàng)建解碼器:VTDecompressionSessionCreate,并指定一系列相關(guān)參數(shù).
  • 將壓縮數(shù)據(jù)放入CMBlockBufferRef中:CMBlockBufferCreateWithMemoryBlock
  • 開(kāi)始解碼: VTDecompressionSessionDecodeFrame
  • 在回調(diào)中接收解碼后的視頻數(shù)據(jù)

通過(guò)AudioConvert解碼音頻

  • 通過(guò)原始數(shù)據(jù)與解碼后數(shù)據(jù)格式的ASBD結(jié)構(gòu)體創(chuàng)建解碼器: AudioConverterNewSpecific
  • 指定解碼器類(lèi)型AudioClassDescription
  • 開(kāi)始解碼: AudioConverterFillComplexBuffer
  • 注意: 解碼的前提是每次需要有1024個(gè)采樣點(diǎn)才能完成一次解碼操作.
同步

因?yàn)檫@里解碼的是本地文件中的音視頻, 也就是說(shuō)只要本地文件中音視頻的時(shí)間戳打的完全正確,我們解碼出來(lái)的數(shù)據(jù)是可以直接播放以實(shí)現(xiàn)同步的效果.而我們要做的僅僅是保證音視頻解碼后同時(shí)渲染.

注意: 比如通過(guò)一個(gè)RTMP地址拉取的流因?yàn)榇嬖诰W(wǎng)絡(luò)原因可能造成某個(gè)時(shí)間段數(shù)據(jù)丟失,造成音視頻不同步,所以需要有一套機(jī)制來(lái)糾正時(shí)間戳.大體機(jī)制即為視頻追趕音頻,后面會(huì)有文件專門(mén)介紹,這里不作過(guò)多說(shuō)明.

渲染

通過(guò)上面的步驟獲取到的視頻原始數(shù)據(jù)即可通過(guò)封裝好的OpenGL ES直接渲染到屏幕上,蘋(píng)果原生框架中也有GLKViewController可以完成屏幕渲染.音頻這里通過(guò)Audio Queue接收音頻幀數(shù)據(jù)以完成播放.

文件結(jié)構(gòu)

2.file_structure

快速使用

使用FFmpeg解碼

首先根據(jù)文件地址初始化FFmpeg以實(shí)現(xiàn)parse音視頻流.然后利用FFmpeg中的解碼器解碼音視頻數(shù)據(jù),這里需要注意的是,我們將從讀取到的第一個(gè)I幀開(kāi)始作為起點(diǎn),以實(shí)現(xiàn)音視頻同步.解碼后的音頻要先裝入傳輸隊(duì)列中,因?yàn)閍udio queue player設(shè)計(jì)模式是不斷從傳輸隊(duì)列中取數(shù)據(jù)以實(shí)現(xiàn)播放.視頻數(shù)據(jù)即可直接進(jìn)行渲染.

- (void)startRenderAVByFFmpegWithFileName:(NSString *)fileName {
    NSString *path = [[NSBundle mainBundle] pathForResource:fileName ofType:@"MOV"];
    
    XDXAVParseHandler *parseHandler = [[XDXAVParseHandler alloc] initWithPath:path];
    
    XDXFFmpegVideoDecoder *videoDecoder = [[XDXFFmpegVideoDecoder alloc] initWithFormatContext:[parseHandler getFormatContext] videoStreamIndex:[parseHandler getVideoStreamIndex]];
    videoDecoder.delegate = self;
    
    XDXFFmpegAudioDecoder *audioDecoder = [[XDXFFmpegAudioDecoder alloc] initWithFormatContext:[parseHandler getFormatContext] audioStreamIndex:[parseHandler getAudioStreamIndex]];
    audioDecoder.delegate = self;
    
    static BOOL isFindIDR = NO;
    
    [parseHandler startParseGetAVPackeWithCompletionHandler:^(BOOL isVideoFrame, BOOL isFinish, AVPacket packet) {
        if (isFinish) {
            isFindIDR = NO;
            [videoDecoder stopDecoder];
            [audioDecoder stopDecoder];
            dispatch_async(dispatch_get_main_queue(), ^{
                self.startWorkBtn.hidden = NO;
            });
            return;
        }
        
        if (isVideoFrame) { // Video
            if (packet.flags == 1 && isFindIDR == NO) {
                isFindIDR = YES;
            }
            
            if (!isFindIDR) {
                return;
            }
            
            [videoDecoder startDecodeVideoDataWithAVPacket:packet];
        }else {             // Audio
            [audioDecoder startDecodeAudioDataWithAVPacket:packet];
        }
    }];
}

-(void)getDecodeVideoDataByFFmpeg:(CMSampleBufferRef)sampleBuffer {
    CVPixelBufferRef pix = CMSampleBufferGetImageBuffer(sampleBuffer);
    [self.previewView displayPixelBuffer:pix];
}

- (void)getDecodeAudioDataByFFmpeg:(void *)data size:(int)size pts:(int64_t)pts isFirstFrame:(BOOL)isFirstFrame {
//    NSLog(@"demon test - %d",size);
    // Put audio data from audio file into audio data queue
    [self addBufferToWorkQueueWithAudioData:data size:size pts:pts];

    // control rate
    usleep(14.5*1000);
}
使用原生框架解碼

首先根據(jù)文件地址初始化FFmpeg以實(shí)現(xiàn)parse音視頻流.這里首先根據(jù)文件中實(shí)際的音頻流數(shù)據(jù)構(gòu)造ASBD結(jié)構(gòu)體以初始化音頻解碼器,然后將解碼后的音視頻數(shù)據(jù)分別渲染即可.這里需要注意的是,如果要拉取的文件視頻是H.265編碼格式的,解碼出來(lái)的數(shù)據(jù)的因?yàn)楹蠦幀所以時(shí)間戳是亂序的,我們需要借助一個(gè)鏈表對(duì)其排序,然后再將排序后的數(shù)據(jù)渲染到屏幕上.

- (void)startRenderAVByOriginWithFileName:(NSString *)fileName {
    NSString *path = [[NSBundle mainBundle] pathForResource:fileName ofType:@"MOV"];
    XDXAVParseHandler *parseHandler = [[XDXAVParseHandler alloc] initWithPath:path];
    
    XDXVideoDecoder *videoDecoder = [[XDXVideoDecoder alloc] init];
    videoDecoder.delegate = self;

    // Origin file aac format
    AudioStreamBasicDescription audioFormat = {
        .mSampleRate         = 48000,
        .mFormatID           = kAudioFormatMPEG4AAC,
        .mChannelsPerFrame   = 2,
        .mFramesPerPacket    = 1024,
    };
    
    XDXAduioDecoder *audioDecoder = [[XDXAduioDecoder alloc] initWithSourceFormat:audioFormat
                                                                     destFormatID:kAudioFormatLinearPCM
                                                                       sampleRate:48000
                                                              isUseHardwareDecode:YES];
    
    [parseHandler startParseWithCompletionHandler:^(BOOL isVideoFrame, BOOL isFinish, struct XDXParseVideoDataInfo *videoInfo, struct XDXParseAudioDataInfo *audioInfo) {
        if (isFinish) {
            [videoDecoder stopDecoder];
            [audioDecoder freeDecoder];
            
            dispatch_async(dispatch_get_main_queue(), ^{
                self.startWorkBtn.hidden = NO;
            });
            return;
        }
        
        if (isVideoFrame) {
            [videoDecoder startDecodeVideoData:videoInfo];
        }else {
            [audioDecoder decodeAudioWithSourceBuffer:audioInfo->data
                                     sourceBufferSize:audioInfo->dataSize
                                      completeHandler:^(AudioBufferList * _Nonnull destBufferList, UInt32 outputPackets, AudioStreamPacketDescription * _Nonnull outputPacketDescriptions) {
                                          // Put audio data from audio file into audio data queue
                                          [self addBufferToWorkQueueWithAudioData:destBufferList->mBuffers->mData size:destBufferList->mBuffers->mDataByteSize pts:audioInfo->pts];

                                          // control rate
                                          usleep(16.8*1000);
                                      }];
        }
    }];
}

- (void)getVideoDecodeDataCallback:(CMSampleBufferRef)sampleBuffer isFirstFrame:(BOOL)isFirstFrame {
    if (self.hasBFrame) {
        // Note : the first frame not need to sort.
        if (isFirstFrame) {
            CVPixelBufferRef pix = CMSampleBufferGetImageBuffer(sampleBuffer);
            [self.previewView displayPixelBuffer:pix];
            return;
        }
        
        [self.sortHandler addDataToLinkList:sampleBuffer];
    }else {
        CVPixelBufferRef pix = CMSampleBufferGetImageBuffer(sampleBuffer);
        [self.previewView displayPixelBuffer:pix];
    }
}

#pragma mark - Sort Callback
- (void)getSortedVideoNode:(CMSampleBufferRef)sampleBuffer {
    int64_t pts = (int64_t)(CMTimeGetSeconds(CMSampleBufferGetPresentationTimeStamp(sampleBuffer)) * 1000);
    static int64_t lastpts = 0;
//    NSLog(@"Test marigin - %lld",pts - lastpts);
    lastpts = pts;
    
    [self.previewView displayPixelBuffer:CMSampleBufferGetImageBuffer(sampleBuffer)];
}


具體實(shí)現(xiàn)

本文中每一部分的具體實(shí)現(xiàn)均有詳細(xì)介紹, 如需幫助請(qǐng)參考閱讀前提中附帶的鏈接地址.

注意

因?yàn)椴煌募袎嚎s的音視頻數(shù)據(jù)格式不同,這里僅僅兼容部分格式,可自定義進(jìn)行擴(kuò)展.

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末慎冤,一起剝皮案震驚了整個(gè)濱河市庇麦,隨后出現(xiàn)的幾起案子身害,更是在濱河造成了極大的恐慌纹蝴,老刑警劉巖评腺,帶你破解...
    沈念sama閱讀 206,378評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異参歹,居然都是意外死亡鸽素,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén)举哟,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)思劳,“玉大人,你說(shuō)我怎么就攤上這事妨猩∏迸眩” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,702評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵壶硅,是天一觀的道長(zhǎng)威兜。 經(jīng)常有香客問(wèn)我,道長(zhǎng)庐椒,這世上最難降的妖魔是什么椒舵? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,259評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮约谈,結(jié)果婚禮上笔宿,老公的妹妹穿的比我還像新娘。我一直安慰自己棱诱,他們只是感情好泼橘,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,263評(píng)論 5 371
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著军俊,像睡著了一般侥加。 火紅的嫁衣襯著肌膚如雪捧存。 梳的紋絲不亂的頭發(fā)上粪躬,一...
    開(kāi)封第一講書(shū)人閱讀 49,036評(píng)論 1 285
  • 那天,我揣著相機(jī)與錄音昔穴,去河邊找鬼镰官。 笑死,一個(gè)胖子當(dāng)著我的面吹牛吗货,可吹牛的內(nèi)容都是我干的泳唠。 我是一名探鬼主播,決...
    沈念sama閱讀 38,349評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼宙搬,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼笨腥!你這毒婦竟也來(lái)了拓哺?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 36,979評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤脖母,失蹤者是張志新(化名)和其女友劉穎士鸥,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體谆级,經(jīng)...
    沈念sama閱讀 43,469評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡烤礁,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,938評(píng)論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了肥照。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片脚仔。...
    茶點(diǎn)故事閱讀 38,059評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖舆绎,靈堂內(nèi)的尸體忽然破棺而出鲤脏,到底是詐尸還是另有隱情,我是刑警寧澤吕朵,帶...
    沈念sama閱讀 33,703評(píng)論 4 323
  • 正文 年R本政府宣布凑兰,位于F島的核電站,受9級(jí)特大地震影響边锁,放射性物質(zhì)發(fā)生泄漏姑食。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,257評(píng)論 3 307
  • 文/蒙蒙 一茅坛、第九天 我趴在偏房一處隱蔽的房頂上張望音半。 院中可真熱鬧,春花似錦贡蓖、人聲如沸曹鸠。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,262評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)彻桃。三九已至,卻和暖如春晾蜘,著一層夾襖步出監(jiān)牢的瞬間邻眷,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工剔交, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留肆饶,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,501評(píng)論 2 354
  • 正文 我出身青樓岖常,卻偏偏與公主長(zhǎng)得像驯镊,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,792評(píng)論 2 345

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