<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          跨平臺(tái)播放器開(kāi)發(fā) (七) ffplay 解封裝、解碼、音視頻同步原理分析

          共 4508字,需瀏覽 10分鐘

           ·

          2022-04-01 09:27

          簡(jiǎn)介

          該篇主要介紹 「ffplay」 如何實(shí)現(xiàn)的解封裝,解碼和音視頻同步的能力,下一篇文章會(huì)根據(jù) 「ffplay」 已提供的能力,會(huì)把一些基礎(chǔ)能力給獨(dú)立出來(lái),以便于后續(xù)的擴(kuò)展。

          解封裝

          解封裝,顧名思義其實(shí)就是將封裝的包裹進(jìn)行拆解,拿到對(duì)應(yīng)音頻、視頻的壓縮數(shù)據(jù)。下面我們來(lái)看下解封裝的主要架構(gòu)圖

          下面借用「雷神」繪制的 ffplay 架構(gòu)圖,我提取了對(duì)應(yīng)的解封裝:

          0509dc0c24fdf83865f057db2a8543cc.webp

          「av_register_all」

          如下代碼所示,其實(shí)在高版本中,可以不需要調(diào)用 av_register_all.

          /*?register?all?codecs,?demux?and?protocols?*/
          #if?CONFIG_AVDEVICE
          ????avdevice_register_all();
          #endif

          「avformat_open_input」

          int?avformat_open_input(AVFormatContext?**ps,?const?char?*url,?ff_const59?AVInputFormat?*fmt,?AVDictionary?**options);

          avformat_open_input 的含義就是打開(kāi)一個(gè)輸入流并讀取封裝 header。最后必須使用 avformat_close_input() 關(guān)閉流。

          「avformat_find_stream_info」

          int?avformat_find_stream_info(AVFormatContext?*ic,?AVDictionary?**options);

          avformat_find_stream_info 函數(shù),主要是探測(cè)音頻,視頻流的基本信息,內(nèi)部會(huì)打開(kāi)解碼,進(jìn)行解碼探測(cè)。

          「av_read_frame」

          int?av_read_frame(AVFormatContext?*s,?AVPacket?*pkt);

          av_read_frame 該函數(shù)主要是將解封裝后的音頻、視頻、字幕數(shù)據(jù)讀取出來(lái)。在 ffplay 源碼中有一處需要注意,代碼如下:

          c6e359c9dbf1775a8076e920e7e9a0a4.webp4bf8c40609d382c9f9c03c3977f63440.webp

          根據(jù)源碼中的提示,ffplay 將 discard 設(shè)置為了禁止「所有幀的讀取」 ?。因?yàn)槲以诮怦?ffplay 代碼的時(shí)候,沒(méi)有注意該函數(shù),導(dǎo)致在測(cè)試解封裝,讀取數(shù)據(jù)失敗。

          ffplay 中的 av_read_fream 函數(shù),會(huì)一直在 read_thread線層中進(jìn)行死循環(huán)讀取。

          解碼

          ffplay 中的音頻、視頻、字幕解碼分別創(chuàng)建了獨(dú)立的線程,在 stream_component_open函數(shù)中, 主要代碼如下:

          static?int?stream_component_open(VideoState?*is,?int?stream_index)?{
          ??
          ???...
          ????
          ????/*?根據(jù)codec_id查找解碼器?*/
          ????codec?=?avcodec_find_decoder(avctx->codec_id);????
          ??
          ???...
          ????//打開(kāi)解碼器
          ?????if?((ret?=?avcodec_open2(avctx,?codec,?&opts))?0)?{
          ????????goto?fail;
          ????}?????
          ???
          ???//在這里對(duì)流信息設(shè)置了默認(rèn),默認(rèn)我的理解就是可以讀取所有的幀
          ????ic->streams[stream_index]->discard?=?AVDISCARD_DEFAULT;
          ????switch?(avctx->codec_type)?{
          ????????case?AVMEDIA_TYPE_AUDIO:

          ?????????...

          ????????????/*?prepare?audio?output?準(zhǔn)備音頻輸出*/
          ????????????//調(diào)用audio_open打開(kāi)sdl音頻輸出,實(shí)際打開(kāi)的設(shè)備參數(shù)保存在audio_tgt,返回值表示輸出設(shè)備的緩沖區(qū)大小
          ????????????if?((ret?=?audio_open(is,?channel_layout,?nb_channels,?sample_rate,?&is->audio_tgt))?0)
          ????????????????goto?fail;
          ?????...
          ????????????
          ????????????//?初始化ffplay封裝的音頻解碼器
          ????????????decoder_init(&is->auddec,?avctx,?&is->audioq,?is->continue_read_thread);
          ?????...
          ????????????//?啟動(dòng)音頻解碼線程
          ????????????if?((ret?=?decoder_start(&is->auddec,?audio_thread,?"audio_decoder",?is))?0)
          ????????????????goto?out;
          ????????????SDL_PauseAudioDevice(audio_dev,?0);
          ????????????break;
          ????????case?AVMEDIA_TYPE_VIDEO:
          ?????...
          ????????????//?初始化ffplay封裝的視頻解碼器
          ????????????decoder_init(&is->viddec,?avctx,?&is->videoq,?is->continue_read_thread);
          ????????????//?啟動(dòng)視頻頻解碼線程
          ????????????if?((ret?=?decoder_start(&is->viddec,?video_thread,?"video_decoder",?is))?0)
          ????????????????goto?out;
          ????????????is->queue_attachments_req?=?1;?//?使能請(qǐng)求mp3、aac等音頻文件的封面
          ????????????break;
          ????????case?AVMEDIA_TYPE_SUBTITLE:?//?視頻是類(lèi)似邏輯處理
          ??????...
          ????????????decoder_init(&is->subdec,?avctx,?&is->subtitleq,?is->continue_read_thread);
          ????????????if?((ret?=?decoder_start(&is->subdec,?subtitle_thread,?"subtitle_decoder",?is))?0)
          ????????????????goto?out;
          ????????????break;
          ????????default:
          ????????????break;
          ????}
          }

          根據(jù)上面簡(jiǎn)略代碼,可以看出,音頻、視頻、字幕,分別在 audio_thread、video_threadsubtitle_thread 線程中進(jìn)行解碼。

          解碼的核心 api 如下:

          讀取解碼后的 AVFrame 原始數(shù)據(jù)

          avcodec_receive_frame(d->avctx,?frame);

          將壓縮數(shù)據(jù)發(fā)送到解碼緩沖隊(duì)列中

          avcodec_send_packet(d->avctx,?&pkt);

          自此,再配合 packet queue, 只要隊(duì)列中有數(shù)據(jù),那么就會(huì)調(diào)用解碼函數(shù),解碼成功就會(huì)存入對(duì)應(yīng)的 frame queue 中。

          音視頻同步

          何為音視頻同步,其實(shí)它的作用就是保證音頻播放的時(shí)間與視頻播放的時(shí)間要在一條時(shí)間線上。所以這里就產(chǎn)生出了,如何保證在一條時(shí)間線上,這個(gè)時(shí)候就需要一個(gè)參考,在 ffplay 中默認(rèn)使用了視頻參考音頻的方式,

          其一,視頻以音頻為基準(zhǔn)(推薦,用的最多):

          • 視頻播放時(shí)間以音頻播放時(shí)間為準(zhǔn),如果視頻比音頻快,那么將等待其實(shí)就是定幀,如果視頻比音頻慢,就需要進(jìn)行丟幀處理了。

          其二,音頻以視頻 pts 為基準(zhǔn)

          其三,以外部時(shí)鐘為基準(zhǔn)

          如果視頻向音頻同步,那么首先是需要音頻播放的時(shí)候,將音頻播放的 pts 給存儲(chǔ)起來(lái),在 ffplay 源碼中,是這樣進(jìn)行操作的,主要是在 sdl_audio_callback 代碼中,我這里為了便于理解,直接上偽代碼吧。

          「音頻播放 pts 設(shè)置」

          static?void?sdl_audio_callback()?{
          ????//拿到回調(diào)開(kāi)始時(shí)間
          ????long?audio_callback_time?=?av_gettime_relative();?//?while?可能產(chǎn)生延遲,最后時(shí)間需要算上中間處理的時(shí)間
          ????//PCM?音頻采樣?1024??*?2?通道?*?16/2?byte?*?緩存?2?幀?=?8192;
          ????int?sdl_audio_buf_size?=?8192;

          ????int?audio_pts?=?0;
          ????int?ret?=?sdl_audio_buf_size;

          ????while?(ret?>?0)//循環(huán)讀取,直到讀取到足夠的數(shù)據(jù)
          ????{
          ????????//1、拿到解碼的數(shù)據(jù)
          ????????uint8_t?*data_?=?NULL;
          ????????//2.?拿到解碼后的?PCM?數(shù)據(jù)
          ????????int?size?=?get_audio_frame(&data_);
          ????????//3、將?pcm?數(shù)據(jù),copy?到?sdl?緩沖區(qū)中
          ????????copy_to_sdl(data_);
          ????????ret?-=?size;
          ????????audio_pts+=size/audio_sample_rate;
          ????}

          ????//4、設(shè)置音頻時(shí)鐘
          ????//這里的含義就是為了更加準(zhǔn)確的計(jì)算出當(dāng)前實(shí)際播放了多少音頻數(shù)據(jù)
          ????set_audio_clock(audio_pts-(實(shí)際寫(xiě)入到緩存區(qū)的音頻數(shù)據(jù)/(sample_rate*2*2)),audio_callback_time);
          }

          ffplay 主要設(shè)置時(shí)鐘的代碼,我給簡(jiǎn)化成了偽代碼,這樣看起來(lái)或許會(huì)更好理解一點(diǎn)。

          「視頻播放同步設(shè)置」

          這里我還是以偽代碼進(jìn)行同步

          static?void?video_refresh()
          {
          ????//拿到當(dāng)前時(shí)間
          ????int?time?=?av_gettime_relative()?/?1000000.0;
          ????//1、計(jì)算當(dāng)前幀顯示的持續(xù)時(shí)長(zhǎng)
          ????int?last_duration?=?當(dāng)前視頻幀pts?-?上一幀pts;
          ????double?delay?=?last_duration;
          ????//2、拿到真正需要?delay?時(shí)間
          ????double?diff?=?當(dāng)前視頻幀pts?-?獲取音頻播放的pts;
          ???//拿到最大的一個(gè)同步范圍,ffplay?最小是0.04s?,最大是?0.1s
          ????double?sync_threshold?=?FFMAX(0.04,
          ???????????????????????????FFMIN(0.1,?delay));

          ????if?(diff?<=?-sync_threshold)?{??????//?視頻已經(jīng)落后了
          ????????delay?=?FFMAX(0,?delay?+?diff);?//?上一幀持續(xù)的時(shí)間往小的方向去調(diào)整
          ????}?else?if?(diff?>=?sync_threshold?&&?delay?>?AV_SYNC_FRAMEDUP_THRESHOLD)?{//?視頻超前
          ????????delay?=?delay?+?diff;?//?上一幀持續(xù)時(shí)間往大的方向去調(diào)整
          ????}?else?if?(diff?>=?sync_threshold)?{
          ????????//?上一幀持續(xù)時(shí)間往大的方向去調(diào)整
          ????????delay?=?2?*?delay;
          ????}
          ????//設(shè)置最后一幀顯示的時(shí)間
          ????frame_timer??+=?delay;
          ????if?(frame_timer?<=?0)//如果是第一幀,那么直接設(shè)置當(dāng)前系統(tǒng)時(shí)間
          ????????frame_timer?=?time;

          ????//判斷是否丟幀,如果已經(jīng)大于顯示的一幀數(shù)據(jù),那么進(jìn)行丟幀
          ????double?next_duration?=?get_next_frame().pts?-?get_last_frame().pts;
          ????if?(time?>?frame_timer?+?next_duration){
          ????????drop_frame();
          ????????return;
          ????}

          ????//顯示視頻幀
          ????display();

          ????//其實(shí)就是顯示多久
          ????sleep(delay);

          }

          上面?zhèn)未a的注釋都比較清晰,相信大家能看得懂,或者你可以直接看 ffplay 源碼部分

          SDL 預(yù)覽

          SDL預(yù)覽部分,我這里就不必在分析了,因?yàn)榇a都是一樣的。

          總結(jié)

          該篇簡(jiǎn)略的分析了 ffplay 實(shí)現(xiàn)原理,個(gè)人覺(jué)得 ffplay 內(nèi)部最不容易理解的其實(shí)是音視頻同步,其它都還是比較容易看的懂。建議還是自己讀一下 ffplay 源碼,這樣才會(huì)對(duì) ffplay 的理解更加深入。

          參考

          • ffplay4.2.1


          瀏覽 134
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  日本欧美在线视频播放 | 国产婷婷五月综合亚洲 | 日韩黄色一级网站 | 超碰在线人人妻 | 欧美a在线观看 |