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. 視頻播放器基本原理

下圖引用自 “雷霄驊,視音頻編解碼技術零基礎學習方法”,因原圖太小,看不太清楚,故重新制作了一張圖片。
播放器基本原理示意圖
如下內容引用自 “雷霄驊,視音頻編解碼技術零基礎學習方法”:

解協議
將流媒體協議的數據,解析為標准的相應的封裝格式數據。視音頻在網絡上傳播的時候,常常采用各種流媒體協議,例如 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] YUVhttps://zh.wikipedia.org/wiki/YUV

5. 修改記錄

2018-11-23 V1.0 初稿
2018-11-29 V1.1 增加定時刷新線程,使解碼幀率更加准確
2019-01-12 V1.2 增加解碼及顯示過程說明


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM