天天看點

FFmpeg簡易播放器的實作2-視訊播放

作者: 葉餘 來源: https://www.cnblogs.com/leisure_chn/p/10047035.html

基于 FFmpeg 和 SDL 實作的簡易視訊播放器,主要分為讀取視訊檔案解碼和調用 SDL 播放兩大部分。本實驗僅研究視訊播放的實作方式。

FFmpeg 簡易播放器系列文章如下:

[1].

FFmpeg簡易播放器的實作1-最簡版 [2]. FFmpeg簡易播放器的實作2-視訊播放 [3]. FFmpeg簡易播放器的實作3-音頻播放 [4]. FFmpeg簡易播放器的實作4-音視訊播放 [5]. FFmpeg簡易播放器的實作5-音視訊同步

1. 視訊播放器基本原理

下圖引用自 “

雷霄骅,視音頻編解碼技術零基礎學習方法

”,因原圖太小,看不太清楚,故重新制作了一張圖檔。

FFmpeg簡易播放器的實作2-視訊播放

如下内容引用自 “

”:

解協定

将流媒體協定的資料,解析為标準的相應的封裝格式資料。視音頻在網絡上傳播的時候,常常采用各種流媒體協定,例如 HTTP,RTMP,或是 MMS 等等。這些協定在傳輸視音頻資料的同時,也會傳輸一些信令資料。這些信令資料包括對播放的控制(播放,暫停,停止),或者對網絡狀态的描述等。解協定的過程中會去除掉信令資料而隻保留視音頻資料。例如,采用 RTMP 協定傳輸的資料,經過解協定操作後,輸出 FLV 格式的資料。

解封裝

将輸入的封裝格式的資料,分離成為音頻流壓縮編碼資料和視訊流壓縮編碼資料。封裝格式種類很多,例如 MP4,MKV,RMVB,TS,FLV,AVI 等等,它的作用就是将已經壓縮編碼的視訊資料和音頻資料按照一定的格式放到一起。例如,FLV 格式的資料,經過解封裝操作後,輸出 H.264 編碼的視訊碼流和 AAC 編碼的音頻碼流。

解碼

将視訊/音頻壓縮編碼資料,解碼成為非壓縮的視訊/音頻原始資料。音頻的壓縮編碼标準包含 AAC,MP3,AC-3 等等,視訊的壓縮編碼标準則包含 H.264,MPEG2,VC-1 等等。解碼是整個系統中最重要也是最複雜的一個環節。通過解碼,壓縮編碼的視訊資料輸出成為非壓縮的顔色資料,例如 YUV420P,RGB 等等;壓縮編碼的音頻資料輸出成為非壓縮的音頻抽樣資料,例如 PCM 資料。

音視訊同步

根據解封裝子產品處理過程中擷取到的參數資訊,同步解碼出來的視訊和音頻資料,并将視訊音頻資料送至系統的顯示卡和聲霸卡播放出來。

2. 簡易播放器的實作-視訊播放

2.1 實驗平台

實驗平台:  openSUSE Leap 42.3  
FFmpeg版本:4.1  
SDL版本:   2.0.9      

FFmpeg 開發環境搭建可參考 “

FFmpeg開發環境建構

2.2 源碼清單

使用如下指令下載下傳源碼:

svn checkout https://github.com/leichn/exercises/trunk/source/ffmpeg/player_video      

2.3 源碼流程簡述

流程比較簡單,不畫流程圖了,簡述如下:

media file --[decode]--> raw frame --[scale]--> yuv frame --[SDL]--> display  
media file ------------> p_frm_raw -----------> p_frm_yuv ---------> sdl_renderer      

加上相關關鍵函數後,流程如下:

media_file ---[av_read_frame()]----------->  
p_packet   ---[avcodec_send_packet()]----->  
decoder    ---[avcodec_receive_frame()]--->  
p_frm_raw  ---[sws_scale()]--------------->  
p_frm_yuv  ---[SDL_UpdateYUVTexture()]---->  
display      

2.4 解碼及顯示過程

2.4.1 讀取視訊資料

調用 av_read_frame() 從輸入檔案中讀取視訊資料包

// A8. 從視訊檔案中讀取一個packet
//     packet可能是視訊幀、音頻幀或其他資料,解碼器隻會解碼視訊幀或音頻幀,非音視訊資料并不會被
//     扔掉、進而能向解碼器提供盡可能多的資訊
//     對于視訊來說,一個packet隻包含一個frame
//     對于音頻來說,若是幀長固定的格式則一個packet可包含整數個frame,
//                   若是幀長可變的格式則一個packet隻包含一個frame
while (av_read_frame(p_fmt_ctx, p_packet) == 0)
{
    if (p_packet->stream_index == v_idx)  // 取到一幀視訊幀,則退出
    {
        break;
    }
}      

2.4.2 視訊資料解碼

調用 avcodec_send_packet() 和 avcodec_receive_frame() 對視訊資料解碼

// A9. 視訊解碼:packet ==> frame
// A9.1 向解碼器喂資料,一個packet可能是一個視訊幀或多個音頻幀,此處音頻幀已被上一句濾掉
ret = avcodec_send_packet(p_codec_ctx, p_packet);
if (ret != 0)
{
    printf("avcodec_send_packet() failed %d\n", ret);
    res = -1;
    goto exit8;
}
// A9.2 接收解碼器輸出的資料,此處隻處理視訊幀,每次接收一個packet,将之解碼得到一個frame
ret = avcodec_receive_frame(p_codec_ctx, p_frm_raw);
if (ret != 0)
{
    if (ret == AVERROR_EOF)
    {
        printf("avcodec_receive_frame(): the decoder has been fully flushed\n");
    }
    else if (ret == AVERROR(EAGAIN))
    {
        printf("avcodec_receive_frame(): output is not available in this state - "
                "user must try to send new input\n");
        continue;
    }
    else if (ret == AVERROR(EINVAL))
    {
        printf("avcodec_receive_frame(): codec not opened, or it is an encoder\n");
    }
    else
    {
        printf("avcodec_receive_frame(): legitimate decoding errors\n");
    }
    res = -1;
    goto exit8;
}      

2.4.3 圖像格式轉換

圖像格式轉換的目的,是為了解碼後的視訊幀能被 SDL 正常顯示。因為 FFmpeg 解碼後得到的圖像格式不一定就能被 SDL 支援,這種情況下不作圖像轉換是無法正常顯示的。

圖像轉換初始化相關:

// A7. 初始化SWS context,用于後續圖像轉換
//     此處第6個參數使用的是FFmpeg中的像素格式,對比參考注釋B4
//     FFmpeg中的像素格式AV_PIX_FMT_YUV420P對應SDL中的像素格式SDL_PIXELFORMAT_IYUV
//     如果解碼後得到圖像的不被SDL支援,不進行圖像轉換的話,SDL是無法正常顯示圖像的
//     如果解碼後得到圖像的能被SDL支援,則不必進行圖像轉換
//     這裡為了編碼簡便,統一轉換為SDL支援的格式AV_PIX_FMT_YUV420P==>SDL_PIXELFORMAT_IYUV
sws_ctx = sws_getContext(p_codec_ctx->width,    // src width
                         p_codec_ctx->height,   // src height
                         p_codec_ctx->pix_fmt,  // src format
                         p_codec_ctx->width,    // dst width
                         p_codec_ctx->height,   // dst height
                         AV_PIX_FMT_YUV420P,    // dst format
                         SWS_BICUBIC,           // flags
                         NULL,                  // src filter
                         NULL,                  // dst filter
                         NULL                   // param
                         );
// B4. 建立SDL_Texture
//     一個SDL_Texture對應一幀YUV資料,同SDL 1.x中的SDL_Overlay
//     此處第2個參數使用的是SDL中的像素格式,對比參考注釋A7
//     FFmpeg中的像素格式AV_PIX_FMT_YUV420P對應SDL中的像素格式SDL_PIXELFORMAT_IYUV
sdl_texture = SDL_CreateTexture(sdl_renderer, 
                                SDL_PIXELFORMAT_IYUV, 
                                SDL_TEXTUREACCESS_STREAMING,
                                p_codec_ctx->width,
                                p_codec_ctx->height
                                );      

圖像格式轉換過程調用 sws_scale() 實作:

// A10. 圖像轉換:p_frm_raw->data ==> p_frm_yuv->data
// 将源圖像中一片連續的區域經過處理後更新到目标圖像對應區域,處理的圖像區域必須逐行連續
// plane: 如YUV有Y、U、V三個plane,RGB有R、G、B三個plane
// slice: 圖像中一片連續的行,必須是連續的,順序由頂部到底部或由底部到頂部
// stride/pitch: 一行圖像所占的位元組數,Stride=BytesPerPixel*Width+Padding,注意對齊
// AVFrame.*data[]: 每個數組元素指向對應plane
// AVFrame.linesize[]: 每個數組元素表示對應plane中一行圖像所占的位元組數
sws_scale(sws_ctx,                                  // sws context
          (const uint8_t *const *)p_frm_raw->data,  // src slice
          p_frm_raw->linesize,                      // src stride
          0,                                        // src slice y
          p_codec_ctx->height,                      // src slice height
          p_frm_yuv->data,                          // dst planes
          p_frm_yuv->linesize                       // dst strides
          );      

2.4.4 顯示

調用 SDL 相關函數将圖像在螢幕上顯示:

// B7. 使用新的YUV像素資料更新SDL_Rect
SDL_UpdateYUVTexture(sdl_texture,                   // sdl texture
                     &sdl_rect,                     // sdl rect
                     p_frm_yuv->data[0],            // y plane
                     p_frm_yuv->linesize[0],        // y pitch
                     p_frm_yuv->data[1],            // u plane
                     p_frm_yuv->linesize[1],        // u pitch
                     p_frm_yuv->data[2],            // v plane
                     p_frm_yuv->linesize[2]         // v pitch
                     );
// B8. 使用特定顔色清空目前渲染目标
SDL_RenderClear(sdl_renderer);
// B9. 使用部分圖像資料(texture)更新目前渲染目标
SDL_RenderCopy(sdl_renderer,                        // sdl renderer
               sdl_texture,                         // sdl texture
               NULL,                                // src rect, if NULL copy texture
               &sdl_rect                            // dst rect
               );
// B10. 執行渲染,更新螢幕顯示
SDL_RenderPresent(sdl_renderer);      

2.5 幀率控制-定時重新整理機制

上一版源碼存在的兩個問題:

[1]. 以固定 25 FPS 的幀率播放視訊檔案,對于幀率不是 25 FPS 的視訊檔案,播放是不正常的

[2]. 即使對于幀率是 25 FPS 的檔案來說,幀率控制仍然較不準确,因為未考慮解碼視訊幀消耗的時間

本版源碼針對上述問題作了改善,将上一版代碼拆分為兩個線程:定時重新整理線程 + 解碼主線程,定時重新整理線程按計算出的幀率發送自定義 SDL 事件,通知解碼主線程解碼主線程收到 SDL 事件後,擷取一個視訊幀解碼并顯示。

3. 編譯與驗證

3.1 編譯

在源碼目錄運作:

./compiler.sh      

3.2 驗證

選用 clock.avi 測試檔案,測試檔案下載下傳(右鍵另存為):

clock.avi

檢視視訊檔案格式資訊:

ffprobe clock.avi      

列印視訊檔案資訊如下:

[avi @ 0x9286c0] non-interleaved AVI
Input #0, avi, from 'clock.avi':
  Duration: 00:00:12.00, start: 0.000000, bitrate: 42 kb/s
    Stream #0:0: Video: msrle ([1][0][0][0] / 0x0001), pal8, 320x320, 1 fps, 1 tbr, 1 tbn, 1 tbc
    Stream #0:1: Audio: truespeech ([34][0][0][0] / 0x0022), 8000 Hz, mono, s16, 8 kb/s      

運作測試指令:

./ffplayer clock.avi      

可以聽到每隔 1 秒時鐘指針跳動一格,跳動 12 次後播放結束。播放過程隻有圖像,沒有聲音。播放正常。

4. 參考資料

[1] 雷霄骅,

視音頻編解碼技術零基礎學習方法

[2] 雷霄骅,

FFmpeg源代碼簡單分析:常見結構體的初始化和銷毀(AVFormatContext,AVFrame等)

[3] 雷霄骅,

最簡單的基于FFMPEG+SDL的視訊播放器ver2(采用SDL2.0)

[4] 雷霄骅,

最簡單的視音頻播放示例7:SDL2播放RGB/YUV

[5]

使用SDL2.0進行YUV顯示

[6] Martin Bohme,

An ffmpeg and SDL Tutorial, Tutorial 01: Making Screencaps

[7] Martin Bohme,

An ffmpeg and SDL Tutorial, Tutorial 02: Outputting to the Screen

[8]

YUV圖像裡的stride和plane的解釋

[9]

圖文詳解YUV420資料格式

[10]

YUV

https://zh.wikipedia.org/wiki/YUV

5. 修改記錄

2018-11-23 V1.0 初稿

2018-11-29 V1.1 增加定時重新整理線程,使解碼幀率更加準确

2019-01-12 V1.2 增加解碼及顯示過程說明

「視訊雲技術」你最值得關注的音視訊技術公衆号,每周推送來自阿裡雲一線的實踐技術文章,在這裡與音視訊領域一流工程師交流切磋。
FFmpeg簡易播放器的實作2-視訊播放

繼續閱讀