基於FFMPEG+SDL的簡單的視訊播放器分析

2023-07-04 18:01:13

基於FFMPEG+SDL的簡單的視訊播放器分析

前言

最近看了雷霄驊前輩的部落格《最簡單的基於FFMPEG+SDL的視訊播放器 ver2 (採用SDL2.0)》,參照他的程式碼,在windows端實現了一個簡單的視訊播放器,程式碼的有部分改動,但是整體的思路和實現的功能是一樣的。下面將對實現的原始碼進行分析,並對其中的一些細節進行記錄。

原始碼分析

引入標頭檔案

引入標頭檔案。

#include <iostream>
#include <windows.h>
extern "C"
{
#include "libavcodec/avcodec.h"
#include "libavformat/avformat.h"
#include "libswscale/swscale.h"
#include "libavutil/imgutils.h"
#include "SDL2/SDL.h"
}
.......

由於ffmpeg和SDL的原始碼都是C,所以在引入標頭檔案時,可以用extern "C"用於宣告 C 函數,以便使其在 C++ 程式碼中按照 C 語言的函數命名和呼叫規則處理。

命令列引數解析

這部分不是重點,可跳過直接看媒體檔案處理的部分

新增了啟動引數解析的程式碼,以便自定義播放的視訊。由於是在windows端實現和編譯的,所以使用了int WINAPI WinMain作為程式的入口。

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
    ......
    return 0;
}

因此,想要獲取啟動引數,需要對資料進行處理。

    .......
    // 獲取命令列引數字串
    LPWSTR lpWideCmdLine = GetCommandLineW(); 
    int argc;
    char *filepath;
    // 將命令列字串分割為一個字串陣列,其中每個元素表示一個命令列引數
    LPWSTR *argv = CommandLineToArgvW(lpWideCmdLine, &argc); 
    int bufferSize = WideCharToMultiByte(CP_UTF8, 0, argv[1], -1, NULL, 0, NULL, NULL);
    char *buffer = new char[bufferSize];
    // 判斷是否指定了播放檔案
    if (argc > 1)
    {
        // 將引數從寬字元轉換為多位元組字元
        WideCharToMultiByte(CP_UTF8, 0, argv[1], -1, buffer, bufferSize, NULL, NULL); 
        filepath = buffer;
        std::cout << "argv[1]: " << buffer << std::endl;
    }
    else
    {
        cout << "Please add the path to the video file that requires the part.\n" << endl;
        return -1;
    }
    ......

媒體檔案處理

讀取媒體檔案

讀取媒體檔案並獲取媒體流的相關資訊。程式碼如下:

    .......
    // av_register_all();
    // avformat_network_init();
    // 建立一個 AVFormatContext 結構體並進行初始化
    pFormatCtx = avformat_alloc_context();
    // 開啟媒體檔案並初始化 AVFormatContext 結構體
    if (avformat_open_input(&pFormatCtx, filepath, NULL, NULL) != 0)
        {
            cout << "Could not open input stream: " << filepath << "\n" << endl;
            return -1;
        }
    // 讀取媒體檔案並獲取媒體流的相關資訊
    if (avformat_find_stream_info(pFormatCtx, NULL) < 0)
    {
        cout << "Could not find stream information.\n" << endl;
        return -1;
    }
    .......

舊版本的ffmpeg程式, 程式開頭處, 一般總是av_register_all,4.x之後,該函數已經廢棄,不需要呼叫了。更多細節可以參考《ffmpeg4.x為什麼不再需要呼叫av_register_all呢》

獲取視訊流的下標。

在一個完整的媒體檔案中,一般會包含視訊流和音訊流。AVFormatContext結構體裡會儲存關於流的各種資訊,本例子是對視訊流進行處理,所以可以從AVFormatContext結構體中,獲得視訊流的下標資訊。程式碼如下:

    ......
    videoindex = -1;
    // nb_streams 是一個整數型別的欄位,表示 AVFormatContext 中包含的流的數量。
    for (i = 0; i < pFormatCtx->nb_streams; i++)
    {
        // 根據codec_type資訊,判斷資料流的型別
        if (pFormatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO)
        {
            // 如果 codec_type == AVMEDIA_TYPE_VIDEO,則資料流型別為視訊流,記錄視訊流下標
            videoindex = i;
            break;
        }
    }
    // 如果 videoindex == -1,則說明媒體檔案中未找到視訊流資料,檢查媒體檔案
    if (videoindex == -1)
    {
        cout << "Did not find a video stream.\n" << endl;
        return -1;
    }
    ......

codec_type代表編碼器的型別,常見的型別有

  • AVMEDIA_TYPE_VIDEO: 視訊編解碼器
  • AVMEDIA_TYPE_AUDIO: 音訊編解碼器
  • AVMEDIA_TYPE_SUBTITLE: 字幕編解碼器
  • AVMEDIA_TYPE_DATA: 資料編解碼器
  • AVMEDIA_TYPE_ATTACHMENT: 附件編解碼器
  • AVMEDIA_TYPE_UNKNOWN: 未知型別

視訊解碼器

根據視訊流資訊,查詢並開啟解碼器。程式碼如下:

    ......
    // 存取視訊流的編碼引數
    pCodecCtx = pFormatCtx->streams[videoindex]->codec;
    // 根據編碼器 ID(codec_id)查詢解碼器
    pCodec = avcodec_find_decoder(pCodecCtx->codec_id);
    if (pCodec == NULL)
    {
        cout << "Could not found.\n" << endl;
        return -1;
    }
    // 開啟編解碼器
    if (avcodec_open2(pCodecCtx, pCodec, NULL) < 0)
    {
        cout << "Could not open codec.\n" << endl;
        return -1;
    }
    ......

影象格式轉換

    ......
    // 分配 AVFrame 結構體的記憶體空間
    pFrame = av_frame_alloc();
    pFrameYUV = av_frame_alloc();
    // 分配用於儲存影象資料的緩衝區
    out_buffer = (unsigned char *)av_malloc(av_image_get_buffer_size(AV_PIX_FMT_YUV420P, pCodecCtx->width, pCodecCtx->height, 1));
    // 填充pFrameYUV->data,以便後續進行影象處理
    av_image_fill_arrays(pFrameYUV->data, pFrameYUV->linesize, out_buffer,
                         AV_PIX_FMT_YUV420P, pCodecCtx->width, pCodecCtx->height, 1);
    cout << "------------------File Information-----------------\n" << endl;
    // 輸出資料流資訊
    av_dump_format(pFormatCtx, 0, filepath, 0);
    cout << "--------------------------------------------------\n" << endl;
    // 建立影象轉換上下文,將原始的畫素格式轉化為 YUV420P
    img_convert_ctx = sws_getContext(pCodecCtx->width, pCodecCtx->height, pCodecCtx->pix_fmt,
                                     pCodecCtx->width, pCodecCtx->height, AV_PIX_FMT_YUV420P, SWS_BICUBIC, NULL, NULL, NULL);
    ......

av_image_get_buffer_size函數,用於計算給定影象引數下所需的緩衝區大小。根據畫素格式和影象的寬度和高度,計算出所需的緩衝區大小。此處所用的影象畫素格式為YUV420P。YUV420P 是最常用的畫素格式之一,特別在視訊編解碼領域廣泛應用。
av_image_fill_arrays用於向pFrameYUV->data中填充資料。不過此時原始的影象資料還並未填充進去,只是先分配記憶體,為後面儲存經過格式轉換的影象做準備。

播放實現

SDL初始化及設定

建立和設定SDL視窗,renderer,texture和rect。程式碼如下:

    ......
    // 初始化 SDL 庫
    if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER))
    {
        cout << "Could not initialize SDL - " << SDL_GetError() << "\n" << endl;
        return -1;
    }
    // 初始化視窗大小為視訊大小
    screen_w = pCodecCtx->width;
    screen_h = pCodecCtx->height;
    // 建立SDL視窗
    screen = SDL_CreateWindow("video Player", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, screen_w, screen_h, SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE);
    if (!screen)
    {
        cout << "SDL: could not creeate window - exiting:\n" << SDL_GetError() << "\n" << endl;
        return -1;
    }
    // 建立renderer
    sdlRenderer = SDL_CreateRenderer(screen, -1, 0);
    // 建立texture
    sdlTexture = SDL_CreateTexture(sdlRenderer, SDL_PIXELFORMAT_IYUV, SDL_TEXTUREACCESS_STREAMING, pCodecCtx->width, pCodecCtx->height);
    // 初始化rect
    sdlRect.x = 0;
    sdlRect.y = 0;
    sdlRect.w = screen_w;
    sdlRect.h = screen_h;
    // 分配一個 AVPacket 結構體的記憶體空間給packet
    packet = (AVPacket *)av_malloc(sizeof(AVPacket));
    ......

建立一個新的執行緒,用於檢測和處理SDL視窗的活動,程式碼如下:

int sfp_refresh_thread(void *opaque)
{
    // 初始化執行緒狀態
    thread_exit = 0;
    thread_pause = 0;
    while (!thread_exit)
    {
        if (!thread_pause)
        {
            SDL_Event event;
            // 設定event狀態為SFM_REFRESH_EVENT
            event.type = SFM_REFRESH_EVENT;
            // 向事件佇列中新增事件
            SDL_PushEvent(&event);
        }
        SDL_Delay(40);
    }
    thread_exit = 0;
    thread_pause = 0;
    SDL_Event event;
    // 設定event狀態為SFM_BREAK_EVENT
    event.type = SFM_BREAK_EVENT;
    SDL_PushEvent(&event);
    return 0;
}  
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
    ......
    // 建立執行緒,呼叫sfp_refresh_thread自定義函數
    video_tid = SDL_CreateThread(sfp_refresh_thread, NULL, NULL);
    ......
    return 0;
}

視訊播放

獲取視訊流資料,經過處理和影象轉換,將影象填充到SDL視窗,實現視訊的播放。程式碼如下:

    ......
    for (;;)
    {   // 獲取視窗活動狀態
        SDL_WaitEvent(&event);
        // 播放視訊
        if (event.type == SFM_REFRESH_EVENT)
        {
            while (1)
            {
                // 如果沒有讀取到packet,設定thread_exit為1,結束播放
                if (av_read_frame(pFormatCtx, packet) < 0)
                    thread_exit = 1;
                // 判斷packet是否為視訊流
                if (packet->stream_index == videoindex)
                    break;
            }
            // 解碼視訊幀
            ret = avcodec_decode_video2(pCodecCtx, pFrame, &got_picture, packet);
            if (ret < 0)
            {
                cout << "Decode Error.\n" << endl;
                return -1;
            }
            if (got_picture)
            {
                // 將pFrame中的原始影象資料,根據img_convert_ctx轉化後,儲存到pFrameYUV中,用於在SDL中顯示
                sws_scale(img_convert_ctx, (const unsigned char *const *)pFrame->data, pFrame->linesize, 0, pCodecCtx->height, pFrameYUV->data, pFrameYUV->linesize);
                // 更新texture,更新資料為pFrameYUV->data[0]
                SDL_UpdateTexture(sdlTexture, NULL, pFrameYUV->data[0], pFrameYUV->linesize[0]);
                // 清空渲染目標
                SDL_RenderClear(sdlRenderer);
                // 將紋理渲染到sdlRect
                SDL_RenderCopy(sdlRenderer, sdlTexture, NULL, &sdlRect);
                // 更新視窗顯示
                SDL_RenderPresent(sdlRenderer);
            }
            // 釋放 AVPacket 結構體記憶體
            av_packet_unref(packet);
        }
        // 暫停
        else if (event.type == SDL_KEYDOWN)
        {
            // 如果點選空格鍵,暫停
            if (event.key.keysym.sym == SDLK_SPACE)
                thread_pause = !thread_pause;
        }
        // 視窗退出
        else if (event.type == SDL_QUIT)
        {
            thread_exit = 1;
        }
        // 播放結束
        else if (event.type == SFM_BREAK_EVENT)
        {
            break;
        }
    }
    ......

記憶體釋放

在所有的操作完成後,最後是記憶體的釋放。程式碼如下:

{
    ......
    sws_freeContext(img_convert_ctx);
    SDL_Quit();
    av_frame_free(&pFrameYUV);
    av_frame_free(&pFrame);
    avcodec_close(pCodecCtx);
    avformat_close_input(&pFormatCtx);
    LocalFree(argv);
    delete[] buffer;
    return 0;
}

完整程式碼

simple_video_player.cpp

#include <iostream>
#include <windows.h>
extern "C"
{
#include "libavcodec/avcodec.h"
#include "libavformat/avformat.h"
#include "libswscale/swscale.h"
#include "libavutil/imgutils.h"
#include "SDL2/SDL.h"
}

using namespace std;

#define SFM_REFRESH_EVENT (SDL_USEREVENT + 1)
#define SFM_BREAK_EVENT (SDL_USEREVENT + 2)

int thread_exit = 0;
int thread_pause = 0;

int sfp_refresh_thread(void *opaque)
{
    thread_exit = 0;
    thread_pause = 0;

    while (!thread_exit)
    {
        if (!thread_pause)
        {
            SDL_Event event;
            event.type = SFM_REFRESH_EVENT;
            SDL_PushEvent(&event);
        }
        SDL_Delay(40);
    }
    thread_exit = 0;
    thread_pause = 0;

    SDL_Event event;
    event.type = SFM_BREAK_EVENT;
    SDL_PushEvent(&event);

    return 0;
}

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
    
    AVFormatContext *pFormatCtx;
    int i, videoindex;
    AVCodecContext *pCodecCtx;
    AVCodec *pCodec;
    AVFrame *pFrame, *pFrameYUV;
    unsigned char *out_buffer;
    AVPacket *packet;
    int ret, got_picture;

    int screen_w, screen_h;
    SDL_Window *screen;
    SDL_Renderer *sdlRenderer;
    SDL_Texture *sdlTexture;
    SDL_Rect sdlRect;
    SDL_Thread *video_tid;
    SDL_Event event;

    struct SwsContext *img_convert_ctx;

    LPWSTR lpWideCmdLine = GetCommandLineW(); 
    int argc;
    char *filepath;
    LPWSTR *argv = CommandLineToArgvW(lpWideCmdLine, &argc); 

    int bufferSize = WideCharToMultiByte(CP_UTF8, 0, argv[1], -1, NULL, 0, NULL, NULL);
    char *buffer = new char[bufferSize];
    
    if (argc > 1)
    {
        WideCharToMultiByte(CP_UTF8, 0, argv[1], -1, buffer, bufferSize, NULL, NULL);
        filepath = buffer;
        std::cout << "argv[1]: " << buffer << std::endl;
    }
    else
    {
        cout << "Please add the path to the video file that requires the part.\n" << endl;
        return -1;
    }

    // av_register_all();
    // avformat_network_init();
    pFormatCtx = avformat_alloc_context();

    if (avformat_open_input(&pFormatCtx, filepath, NULL, NULL) != 0)
    {
        cout << "Could not open input stream: " << filepath << "\n"
             << endl;
        return -1;
    }
    if (avformat_find_stream_info(pFormatCtx, NULL) < 0)
    {
        cout << "Could not find stream information.\n"
             << endl;
        return -1;
    }
    videoindex = -1;
    for (i = 0; i < pFormatCtx->nb_streams; i++)
    {
        if (pFormatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO)
        {
            videoindex = i;
            break;
        }
    }
    if (videoindex == -1)
    {
        cout << "Did not find a video stream.\n"
             << endl;
        return -1;
    }
    pCodecCtx = pFormatCtx->streams[videoindex]->codec;
    pCodec = avcodec_find_decoder(pCodecCtx->codec_id);
    if (pCodec == NULL)
    {
        cout << "Could not found.\n"
             << endl;
        return -1;
    }
    if (avcodec_open2(pCodecCtx, pCodec, NULL) < 0)
    {
        cout << "Could not open codec.\n"
             << endl;
        return -1;
    }
    pFrame = av_frame_alloc();
    pFrameYUV = av_frame_alloc();

    out_buffer = (unsigned char *)av_malloc(av_image_get_buffer_size(AV_PIX_FMT_YUV420P, pCodecCtx->width, pCodecCtx->height, 1));
    av_image_fill_arrays(pFrameYUV->data, pFrameYUV->linesize, out_buffer,
                         AV_PIX_FMT_YUV420P, pCodecCtx->width, pCodecCtx->height, 1);

    cout << "------------------File Information-----------------\n"
         << endl;
    av_dump_format(pFormatCtx, 0, filepath, 0);
    cout << "--------------------------------------------------\n"
         << endl;

    img_convert_ctx = sws_getContext(pCodecCtx->width, pCodecCtx->height, pCodecCtx->pix_fmt,
                                     pCodecCtx->width, pCodecCtx->height, AV_PIX_FMT_YUV420P, SWS_BICUBIC, NULL, NULL, NULL);
    if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER))
    {
        cout << "Could not initialize SDL - " << SDL_GetError() << "\n"
             << endl;
        return -1;
    }
    screen_w = pCodecCtx->width;
    screen_h = pCodecCtx->height;
    screen = SDL_CreateWindow("video Player", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, screen_w, screen_h, SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE);
    if (!screen)
    {
        cout << "SDL: could not creeate window - exiting:\n"
             << SDL_GetError() << "\n"
             << endl;
        return -1;
    }
    sdlRenderer = SDL_CreateRenderer(screen, -1, 0);
    sdlTexture = SDL_CreateTexture(sdlRenderer, SDL_PIXELFORMAT_IYUV, SDL_TEXTUREACCESS_STREAMING, pCodecCtx->width, pCodecCtx->height);

    sdlRect.x = 0;
    sdlRect.y = 0;
    sdlRect.w = screen_w;
    sdlRect.h = screen_h;
    packet = (AVPacket *)av_malloc(sizeof(AVPacket));

    video_tid = SDL_CreateThread(sfp_refresh_thread, NULL, NULL);

    for (;;)
    {
        SDL_WaitEvent(&event);
        if (event.type == SFM_REFRESH_EVENT)
        {
            while (1)
            {
                if (av_read_frame(pFormatCtx, packet) < 0)
                    thread_exit = 1;

                if (packet->stream_index == videoindex)
                    break;
            }
            ret = avcodec_decode_video2(pCodecCtx, pFrame, &got_picture, packet);
            if (ret < 0)
            {
                cout << "Decode Error.\n"
                     << endl;
                return -1;
            }
            if (got_picture)
            {
                sws_scale(img_convert_ctx, (const unsigned char *const *)pFrame->data, pFrame->linesize, 0, pCodecCtx->height, pFrameYUV->data, pFrameYUV->linesize);
                SDL_UpdateTexture(sdlTexture, NULL, pFrameYUV->data[0], pFrameYUV->linesize[0]);
                SDL_RenderClear(sdlRenderer);
                SDL_RenderCopy(sdlRenderer, sdlTexture, NULL, &sdlRect);
                SDL_RenderPresent(sdlRenderer);
            }
            av_packet_unref(packet);
        }
        else if (event.type == SDL_KEYDOWN)
        {
            if (event.key.keysym.sym == SDLK_SPACE)
                thread_pause = !thread_pause;
        }
        else if (event.type == SDL_QUIT)
        {
            thread_exit = 1;
        }
        else if (event.type == SFM_BREAK_EVENT)
        {
            break;
        }
    }
    sws_freeContext(img_convert_ctx);

    SDL_Quit();
    av_frame_free(&pFrameYUV);
    av_frame_free(&pFrame);
    avcodec_close(pCodecCtx);
    avformat_close_input(&pFormatCtx);

    LocalFree(argv);
    delete[] buffer;

    return 0;
}