FFmpeg簡易播放器的實現1-最簡版


本文為作者原創:https://www.cnblogs.com/leisure_chn/p/10040202.html,轉載請注明出處

基於 FFmpeg 和 SDL 實現的簡易視頻播放器,主要分為讀取視頻文件解碼和調用 SDL 播放兩大部分。

本實驗僅實現最簡單的視頻播放流程,不考慮細節,不考慮音頻。本實驗主要參考如下兩篇文章:
[1]. 最簡單的基於FFMPEG+SDL的視頻播放器ver2(采用SDL2.0)
[2]. An ffmpeg and SDL Tutorial

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 源碼清單

/*****************************************************************
 * ffplayer.c
 *
 * history:
 *   2018-11-27 - [lei]     created file
 *
 * details:
 *   A simple ffmpeg player.
 *
 * refrence:
 *   1. https://blog.csdn.net/leixiaohua1020/article/details/38868499
 *   2. http://dranger.com/ffmpeg/ffmpegtutorial_all.html#tutorial01.html
 *   3. http://dranger.com/ffmpeg/ffmpegtutorial_all.html#tutorial02.html
******************************************************************/

#include <stdio.h>
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libswscale/swscale.h>
#include <SDL2/SDL.h>
#include <SDL2/SDL_video.h>
#include <SDL2/SDL_render.h>
#include <SDL2/SDL_rect.h>

int main(int argc, char *argv[])
{
    // Initalizing these to NULL prevents segfaults!
    AVFormatContext*    p_fmt_ctx = NULL;
    AVCodecContext*     p_codec_ctx = NULL;
    AVCodecParameters*  p_codec_par = NULL;
    AVCodec*            p_codec = NULL;
    AVFrame*            p_frm_raw = NULL;        // 幀,由包解碼得到原始幀
    AVFrame*            p_frm_yuv = NULL;        // 幀,由原始幀色彩轉換得到
    AVPacket*           p_packet = NULL;         // 包,從流中讀出的一段數據
    struct SwsContext*  sws_ctx = NULL;
    int                 buf_size;
    uint8_t*            buffer = NULL;
    int                 i;
    int                 v_idx;
    int                 ret;
    SDL_Window*         screen; 
    SDL_Renderer*       sdl_renderer;
    SDL_Texture*        sdl_texture;
    SDL_Rect            sdl_rect;

    if (argc < 2)
    {
        printf("Please provide a movie file\n");
        return -1;
    }

    // 初始化libavformat(所有格式),注冊所有復用器/解復用器
    // av_register_all();   // 已被申明為過時的,直接不再使用即可

    // A1. 打開視頻文件:讀取文件頭,將文件格式信息存儲在"fmt context"中
    ret = avformat_open_input(&p_fmt_ctx, argv[1], NULL, NULL);
    if (ret != 0)
    {
        printf("avformat_open_input() failed\n");
        return -1;
    }

    // A2. 搜索流信息:讀取一段視頻文件數據,嘗試解碼,將取到的流信息填入pFormatCtx->streams
    //     p_fmt_ctx->streams是一個指針數組,數組大小是pFormatCtx->nb_streams
    ret = avformat_find_stream_info(p_fmt_ctx, NULL);
    if (ret < 0)
    {
        printf("avformat_find_stream_info() failed\n");
        return -1;
    }

    // 將文件相關信息打印在標准錯誤設備上
    av_dump_format(p_fmt_ctx, 0, argv[1], 0);

    // A3. 查找第一個視頻流
    v_idx = -1;
    for (i=0; i<p_fmt_ctx->nb_streams; i++)
    {
        if (p_fmt_ctx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO)
        {
            v_idx = i;
            printf("Find a video stream, index %d\n", v_idx);
            break;
        }
    }
    if (v_idx == -1)
    {
        printf("Cann't find a video stream\n");
        return -1;
    }

    // A5. 為視頻流構建解碼器AVCodecContext

    // A5.1 獲取解碼器參數AVCodecParameters
    p_codec_par = p_fmt_ctx->streams[v_idx]->codecpar;
    // A5.2 獲取解碼器
    p_codec = avcodec_find_decoder(p_codec_par->codec_id);
    if (p_codec == NULL)
    {
        printf("Cann't find codec!\n");
        return -1;
    }
    // A5.3 構建解碼器AVCodecContext
    // A5.3.1 p_codec_ctx初始化:分配結構體,使用p_codec初始化相應成員為默認值
    p_codec_ctx = avcodec_alloc_context3(p_codec);

    // A5.3.2 p_codec_ctx初始化:p_codec_par ==> p_codec_ctx,初始化相應成員
    ret = avcodec_parameters_to_context(p_codec_ctx, p_codec_par);
    if (ret < 0)
    {
        printf("avcodec_parameters_to_context() failed %d\n", ret);
        return -1;
    }

    // A5.3.3 p_codec_ctx初始化:使用p_codec初始化p_codec_ctx,初始化完成
    ret = avcodec_open2(p_codec_ctx, p_codec, NULL);
    if (ret < 0)
    {
        printf("avcodec_open2() failed %d\n", ret);
        return -1;
    }

    // A6. 分配AVFrame
    // A6.1 分配AVFrame結構,注意並不分配data buffer(即AVFrame.*data[])
    p_frm_raw = av_frame_alloc();
    p_frm_yuv = av_frame_alloc();

    // A6.2 為AVFrame.*data[]手工分配緩沖區,用於存儲sws_scale()中目的幀視頻數據
    //     p_frm_raw的data_buffer由av_read_frame()分配,因此不需手工分配
    //     p_frm_yuv的data_buffer無處分配,因此在此處手工分配
    buf_size = av_image_get_buffer_size(AV_PIX_FMT_YUV420P, 
                                        p_codec_ctx->width, 
                                        p_codec_ctx->height, 
                                        1
                                       );
    // buffer將作為p_frm_yuv的視頻數據緩沖區
    buffer = (uint8_t *)av_malloc(buf_size);
    // 使用給定參數設定p_frm_yuv->data和p_frm_yuv->linesize
    av_image_fill_arrays(p_frm_yuv->data,           // dst data[]
                         p_frm_yuv->linesize,       // dst linesize[]
                         buffer,                    // src buffer
                         AV_PIX_FMT_YUV420P,        // pixel format
                         p_codec_ctx->width,        // width
                         p_codec_ctx->height,       // height
                         1                          // align
                        );

    // 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
                            );                 


    // B1. 初始化SDL子系統:缺省(事件處理、文件IO、線程)、視頻、音頻、定時器
    if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER))
    {  
        printf("SDL_Init() failed: %s\n", SDL_GetError()); 
        return -1;
    }
    
    // B2. 創建SDL窗口,SDL 2.0支持多窗口
    //     SDL_Window即運行程序后彈出的視頻窗口,同SDL 1.x中的SDL_Surface
    screen = SDL_CreateWindow("Simplest ffmpeg player's Window", 
                              SDL_WINDOWPOS_UNDEFINED,// 不關心窗口X坐標
                              SDL_WINDOWPOS_UNDEFINED,// 不關心窗口Y坐標
                              p_codec_ctx->width, 
                              p_codec_ctx->height,
                              SDL_WINDOW_OPENGL
                             );

    if (screen == NULL)
    {  
        printf("SDL_CreateWindow() failed: %s\n", SDL_GetError());  
        return -1;
    }

    // B3. 創建SDL_Renderer
    //     SDL_Renderer:渲染器
    sdl_renderer = SDL_CreateRenderer(screen, -1, 0);

    // 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);  

    sdl_rect.x = 0;
    sdl_rect.y = 0;
    sdl_rect.w = p_codec_ctx->width;
    sdl_rect.h = p_codec_ctx->height;

    p_packet = (AVPacket *)av_malloc(sizeof(AVPacket));
    // 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)  // 僅處理視頻幀
        {
            // 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);
                return -1;
            }
            // A9.2 接收解碼器輸出的數據,此處只處理視頻幀,每次接收一個packet,將之解碼得到一個frame
            ret = avcodec_receive_frame(p_codec_ctx, p_frm_raw);
            if (ret != 0)
            {
                printf("avcodec_receive_frame() failed %d\n", ret);
                return -1;
            }

            // 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
                     );
            
            // B5. 使用新的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
                                 );
            
            // B6. 使用特定顏色清空當前渲染目標
            SDL_RenderClear(sdl_renderer);
            // B7. 使用部分圖像數據(texture)更新當前渲染目標
            SDL_RenderCopy(sdl_renderer,                        // sdl renderer
                           sdl_texture,                         // sdl texture
                           NULL,                                // src rect, if NULL copy texture
                           &sdl_rect                            // dst rect
                          );
            // B8. 執行渲染,更新屏幕顯示
            SDL_RenderPresent(sdl_renderer);  

            // B9. 控制幀率為25FPS,此處不夠准確,未考慮解碼消耗的時間
            SDL_Delay(40);
        }
        av_packet_unref(p_packet);
    }

    SDL_Quit();
    sws_freeContext(sws_ctx);
    av_free(buffer);
    av_frame_free(&p_frm_yuv);
    av_frame_free(&p_frm_raw);
    avcodec_close(p_codec_ctx);
    avformat_close_input(&p_fmt_ctx);

    return 0;
}

源碼清單中涉及的一些概念簡述如下:
container:
容器,也稱封裝器,對應數據結構 AVFormatContext。封裝是指將流數據組裝為指定格式的文件。封裝格式有 AVI、MP4 等。FFmpeg 可識別五種流類型:視頻 video(v)、音頻 audio(a)、attachment(t)、數據 data(d)、字幕 subtitle。

codec:
編解碼器,對應數據結構 AVCodec。編碼器將未壓縮的原始圖像或音頻數據編碼為壓縮數據。解碼器與之相反。

codec context:
編解碼器上下文,對應數據結構 AVCodecContext。此為非常重要的一個數據結構,后文分析。各API大量使用 AVCodecContext 來引用編解碼器。

codec par:
編解碼器參數,對應數據結構 AVCodecParameters。新版本增加的字段。新版本建議使用 AVStream->codepar 替代 AVStream->codec。

packet:
經過編碼的數據包,對應數據結構 AVPacket。通過 av_read_frame() 從媒體文件中獲取得到的一個 packet 可能包含多個(整數個)音頻幀或單個視頻幀,或者其他類型的流數據。

frame:
未編碼的原始數據幀,對應數據結構 AVFrame。解碼器將 packet 解碼后生成 frame。

plane:
如 YUV 有 Y、U、V 三個 plane,RGB 有 R、G、B 三個 plane。

slice:
圖像中一片連續的行,必須是連續的,順序由頂部到底部或由底部到頂部

stride/pitch:
一行圖像所占的字節數,Stride = BytesPerPixel × Width,按 x 字節對齊[待確認]

sdl window:
播放視頻時彈出的窗口,對應數據結構SDL_Window。在 SDL1.x 版本中,只可以創建一個窗口。在 SDL2.0 版本中,可以創建多個窗口。

sdl texture:
對應數據結構 SDL_Texture。一個SDL_Texture對應一幀解碼后的圖像數據。

sdl renderer:
渲染器,對應數據結構SDL_Renderer。將 SDL_Texture 渲染至 SDL_Window。

sdl rect:
對應數據結構 SDL_Rect,SDL_Rect 用於確定 SDL_Texture 顯示的位置。一個 SDL_Window 上可以顯示多個 SDL_Rect。這樣可以實現同一窗口的分屏顯示。

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.3.1 初始化

初始化解碼及顯示環境。

2.3.2 讀取視頻數據

調用 av_read_frame() 從輸入文件中讀取視頻數據包。

2.3.3 視頻數據解碼

調用 avcodec_send_packet() 和 avcodec_receive_frame() 對視頻數據解碼。

2.3.4 圖像格式轉換

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

2.3.5 顯示

調用 SDL 相關函數將圖像在屏幕上顯示。

3. 編譯與驗證

3.1 編譯

gcc -o ffplayer ffplayer.c -lavutil -lavformat -lavcodec -lavutil -lswscale -lSDL2

3.2 驗證

選用 bigbuckbunny_480x272.h265 測試文件,測試文件下載(右鍵另存為):bigbuckbunny_480x272.h265
運行測試命令:

./ffplayer bigbuckbunny_480x272.h265 

4. 參考資料

[1] 雷霄驊,視音頻編解碼技術零基礎學習方法
[2] 雷霄驊,FFmpeg源代碼簡單分析:常見結構體的初始化和銷毀(AVFormatContext,AVFrame等)
[3] 雷霄驊,最簡單的基於FFMPEG+SDL的視頻播放器ver2(采用SDL2.0)
[4] Martin Bohme, An ffmpeg and SDL Tutorial, Tutorial 01: Making Screencaps
[5] Martin Bohme, An ffmpeg and SDL Tutorial, Tutorial 02: Outputting to the Screen
[6] YUV圖像里的stride和plane的解釋
[7] 圖文詳解YUV420數據格式
[8] YUVhttps://zh.wikipedia.org/wiki/YUV

5. 修改記錄

2018-11-23 V1.0 初稿
2018-11-29 V1.1 增加定時刷新線程,使解碼幀率更加准確


免責聲明!

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



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