系列文章目錄
- 基于 FFmpeg 的跨平臺視頻播放器簡明教程(一):FFMPEG + Conan 環(huán)境集成
- 基于 FFmpeg 的跨平臺視頻播放器簡明教程(二):基礎知識和解封裝(demux)
- 基于 FFmpeg 的跨平臺視頻播放器簡明教程(三):視頻解碼
- 基于 FFmpeg 的跨平臺視頻播放器簡明教程(四):像素格式與格式轉換
前言
經(jīng)過前面四章的學習,現(xiàn)在我們已經(jīng)掌握了如何使用 FFmpeg 進行視頻解碼,中間穿插了很多音視頻相關的知識點,例如容器、編解碼器、解封裝、像素格式、格式轉換等等?,F(xiàn)在回看,音視頻的入門門檻還是比較高的,一個最簡單的任務就已經(jīng)涉及到大量的知識點。但問題不大,本人希望通過一系列的文章來帶你入門,通過完成一個播放器項目來不斷地學習音視頻內(nèi)容。
在開始新的旅程前,重新審視下現(xiàn)有代碼,發(fā)現(xiàn)有些模塊可以被封裝成更為內(nèi)聚的類,具體的包括:
- FFmpegDemuxer,用于解封裝相關的任務
- FFmpegCodec,用于解碼相關的任務
- FFmpegImageConverter,用于 AVFrame 格式轉換
這些類的使用方式,你可以在單元測試中找到示例,此處不再贅述。
抽象封裝成一些類的好處主要有幾點:
- 資源管理。FFmpeg 是 C 接口,很多資源需要手動的管理,這樣的代碼寫多了難免會出現(xiàn)內(nèi)存泄漏的問題。因此用 C++ 的 ”資源獲取即初始化“ 理念來管理這些資源。
- 減少代碼冗余。將某些任務封裝成更為簡便的接口,減少代碼冗余。
好的,準備就緒,讓我們進入今天的主題:播放視頻。本文參考文章來自 An ffmpeg and SDL Tutorial - Tutorial 02: Outputting to the Screen。這個系列對新手較為友好,但 2015 后就不再更新了,以至于文章中的 ffmpeg api 已經(jīng)被棄用了。幸運的是,有人對該教程的代碼進行重寫,使用了較新的 api,你可以在 rambodrahmani/ffmpeg-video-player 找到這些代碼。
本文的代碼在 ffmpeg_video_player_tutorial-my_tutorial02。
SDL2,跨平臺多媒體開發(fā)庫
SDL2 是 Simple DirectMedia Layer(簡單直接媒體層)的縮寫,它是一個跨平臺的 C/C++ 庫,專為游戲和多媒體應用程序提供硬件抽象層的支持。SDL2 提供了對音頻、鍵盤、鼠標、操縱桿和圖形硬件的底層訪問能力,它在 Windows、macOS、Linux、iOS 和 Android 等平臺上都有廣泛的應用。
FFmpeg 提供的命令行工具 FFplay 中使用了 SDL2 作為渲染圖像的依賴庫。FFplay 是 FFmpeg 項目中的一個簡單的媒體播放器,它依賴于 FFmpeg 庫來解碼、解復用和處理媒體數(shù)據(jù)。FFmpeg 本身專注于視頻和音頻的編解碼,以及其他媒體處理功能,但不涉及與硬件交互的部分,例如音視頻的渲染和播放。
FFplay 使用 SDL 主要是因為 SDL 提供了易于使用的跨平臺 API,用于訪問音頻、視頻、鍵盤、鼠標和操縱桿等硬件設備。通過使用 SDL,F(xiàn)Fplay 能在各種操作環(huán)境下實現(xiàn)音視頻的同步播放,以及用戶交互(如鍵盤和鼠標操作)。
SDL 提供了以下特性,使其成為 FFplay 使用的理想選擇:
- 跨平臺支持:SDL 可在多個平臺(如 Windows、macOS、Linux 等)上運行,這意味著基于 SDL 的 FFplay 可以很容易地移植到其他環(huán)境。
- 音頻和視頻渲染:SDL 提供了對音頻和視頻的播放支持,使得 FFplay 可以正確地渲染音頻和視頻數(shù)據(jù)。
- 事件處理:SDL 提供了對各類輸入設備事件(如鍵盤、鼠標等)的處理,這使得 FFplay 可以對用戶的操作做出響應,例如暫停、快進等。
關于 SDL 的使用,我之前寫過一些文章,供大家參考,此處不再贅述:
- SDL2 簡明教程(一):使用 Cmake 和 Conan 構建 SDL2 編程環(huán)境
- SDL2 簡明教程(二):創(chuàng)建一個空的窗口
- SDL2 簡明教程(三):顯示圖片
- SDL2 簡明教程(四):用 SDL_IMAGE 庫導入圖片
- SDL2 簡明教程(五):OpenGL 繪制
SDL 顯示圖像
首先讓我們快速過一下 SDL 顯示圖片的過程。使用SDL來顯示一幀圖像的步驟如下:
- 初始化SDL:調(diào)用SDL_Init函數(shù)來初始化SDL庫。
- 創(chuàng)建一個窗口和渲染器:調(diào)用SDL_CreateWindow和SDL_CreateRenderer函數(shù)來創(chuàng)建一個窗口和渲染器。
- 加載圖像:使用SDL_image庫中的函數(shù)(如IMG_Load)加載需要顯示的圖像。
- 創(chuàng)建一個紋理:使用加載的圖像創(chuàng)建一個紋理,使用SDL_CreateTextureFromSurface函數(shù)。
- 將紋理渲染到屏幕:使用SDL_RenderCopy函數(shù)將紋理渲染到屏幕上。
- 刷新屏幕:使用SDL_RenderPresent函數(shù)來刷新屏幕。
- 釋放資源:使用SDL_DestroyTexture、SDL_DestroyRenderer、SDL_DestroyWindow等函數(shù)釋放分配的資源。
下面是C++代碼示例,顯示一張圖像:
#include <SDL.h>
#include <SDL_image.h>
int main(int argc, char const *argv[])
{
SDL_Init(SDL_INIT_VIDEO);
SDL_Window* window = SDL_CreateWindow("SDL Example", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 640, 480, 0);
SDL_Renderer* renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED);
SDL_Surface* image = IMG_Load("example.png");
SDL_Texture* texture = SDL_CreateTextureFromSurface(renderer, image);
SDL_RenderCopy(renderer, texture, NULL, NULL);
SDL_RenderPresent(renderer);
SDL_Delay(5000); //等待5秒
SDL_DestroyTexture(texture);
SDL_FreeSurface(image);
SDL_DestroyRenderer(renderer);
SDL_DestroyWindow(window);
IMG_Quit();
SDL_Quit();
return 0;
}
在SDL中,window、renderer、surface和texture是四個關鍵的圖形和圖像處理對象,它們之間有一定的聯(lián)系。讓我們一一分析它們的作用和關系。
-
SDL_Window: 窗口對象。用于在屏幕上創(chuàng)建、管理和顯示一個簡單的矩形窗口。窗口可以由操作系統(tǒng)進行管理,提供了標題欄、邊框和其他窗口功能。SDL_Window代表了應用程序中使用的這個窗口實例。
-
SDL_Renderer: 渲染器對象。負責將多個圖像(通常由SDL_Texture表示)繪制到屏幕上。渲染器可以使用不同的后端實現(xiàn)(如OpenGL,Direct3D等),并將其集成到SDL_Window中。與SDL_Window對象關聯(lián)的渲染器用于將圖像和圖形繪制到窗口中。
-
SDL_Surface: 表面對象。表示原始的位圖圖像,每個像素由色彩值定義。表面可以包括一個或多個圖層,用于繪制2D圖像。然而,它們在處理上較慢,因為渲染操作通常在CPU端執(zhí)行。表面通常用于加載、處理和創(chuàng)建圖像資源,然后將它們轉換成紋理用于高效渲染。
-
SDL_Texture: 紋理對象。代表了可以被硬件加速的圖形。紋理被GPU管理,可以更高效地使用SDL_Renderer進行渲染。它們通常由SDL_Surface轉換而來,在紋理上傳到GPU之后,關聯(lián)的表面對象可以被釋放以節(jié)省內(nèi)存。
這四個對象之間的關系如下:
- 一個SDL_Window與一個SDL_Renderer關聯(lián),將圖形和圖像繪制到窗口上。
- SDL_Renderer負責處理和繪制SDL_Texture對象。
- SDL_Surface用于創(chuàng)建、處理和存儲原始的位圖圖像,然后將它們轉換成硬件加速的SDL_Texture以供渲染器繪制。
簡而言之,SDL_Window負責顯示,SDL_Renderer負責繪制,SDL_Surface負責處理原始位圖,SDL_Texture則可以高效地被渲染器繪制到窗口。在實際應用中,通常需要處理并將多個表面轉換成紋理,然后使用渲染器將它們繪制到窗口中。
SDL 顯示 YUV 圖像
視頻多數(shù)情況下使用 YUV 格式來存放像素數(shù)據(jù),主要原因是壓縮效率更高,同時還能保持較好的圖像質(zhì)量。具體來說,有以下幾點原因:
- 符合人眼特性: YUV格式將圖像的亮度信息(Y分量)和色度信息(U和V分量)分開存儲。而人眼對亮度信息(黑白圖像)的敏感度要高于色度信息(彩色信息)。在進行壓縮時,可以減少色度信息的分辨率,從而降低數(shù)據(jù)量,這符合人類視覺系統(tǒng)的特性,不會明顯降低觀感質(zhì)量。這種削減色度分辨率的方式叫色度子采樣(如4:2:0、4:2:2、4:4:4)。
- 節(jié)省存儲空間和帶寬: 相比于像RGB這種直接存儲色彩值的格式,YUV格式可以利用色度子采樣來有效地減少數(shù)據(jù)量,在同樣的圖像質(zhì)量下,YUV格式需要的存儲空間和傳輸帶寬更小。這在視頻傳輸、壓縮、存儲等場景中十分重要。
- 兼容性和廣泛應用: YUV格式已經(jīng)被廣泛應用于許多視頻壓縮標準,如H.264(AVC)、H.265(HEVC)等,以及各種視頻設備和傳輸系統(tǒng)。這意味著使用YUV格式可以顯著提高視頻系統(tǒng)的通用性和兼容性。
- 易于處理: YUV格式把亮度信息與色度信息分開存儲,方便視頻圖像處理和編輯,如調(diào)整亮度、對比度、色彩平衡等。
在SDL中顯示一張YUV圖像,可以使用 SDL_Texture的SDL_PIXELFORMAT_YV12 或SDL_PIXELFORMAT_IYUV像素格式。首先,需要創(chuàng)建一個相應格式的紋理,然后通過SDL_UpdateYUVTexture()函數(shù)更新紋理數(shù)據(jù)。下面是一個簡單的示例:
#include <SDL.h>
int main(int argc, char *argv[])
{
// 初始化SDL
if (SDL_Init(SDL_INIT_VIDEO) != 0)
{
SDL_Log("Unable to initialize SDL: %s", SDL_GetError());
return -1;
}
// 創(chuàng)建窗口和渲染器
SDL_Window* window = SDL_CreateWindow("YUV Example", SDL_WINDOWPOS_CENTERED,
SDL_WINDOWPOS_CENTERED, 640, 480, 0);
SDL_Renderer* renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED);
// 創(chuàng)建紋理
SDL_Texture* texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_YV12,
SDL_TEXTUREACCESS_STREAMING, 640, 480);
// 加載YUV數(shù)據(jù)
Uint8* yuvData = loadYUVData();
// 假設 yPlane, uPlane 和 vPlane 分別是Y,U和V分量的指針
Uint8 *yPlane;
Uint8 *uPlane;
Uint8 *vPlane;
int dataSize = width * height;
// 設置三個分量數(shù)據(jù)的跨度
int yPitch = width;
int uPitch = width / 2;
int vPitch = width / 2;
// 更新紋理數(shù)據(jù)
SDL_UpdateYUVTexture(texture,
nullptr, // 更新整個紋理
yPlane, yPitch,
uPlane, uPitch,
vPlane, vPitch);
// 渲染紋理
SDL_RenderClear(renderer);
SDL_RenderCopy(renderer, texture, NULL, NULL);
SDL_RenderPresent(renderer);
SDL_Delay(5000);
// 釋放資源
SDL_DestroyTexture(texture);
SDL_DestroyRenderer(renderer);
SDL_DestroyWindow(window);
SDL_Quit();
return 0;
}
在文章 YUV 文件讀取、顯示、縮放、裁剪等操作教程
有更詳細的說明,包括 SDL YUV 格式與 ffmpeg 像素格式之間的對應,Chroma subsampling 是什么,YUV Packed 與 Planar 的區(qū)別,如何正確讀取一張 YUV 圖片等等,強烈推薦先看這篇文章后再繼續(xù)我們的教程。此外,你也可以直接看項目 simple_yuv_viewer 的 源碼來學習 。
SDL 播放視頻
現(xiàn)在,我們的目標是取代上一個教程中的 saveFrame
函數(shù),而直接將視頻幀輸出到屏幕。為了讓視頻解碼與視頻播放的邏輯獨立開,這里引入一個類叫 SDLApp
,它負責 SDL 環(huán)境的創(chuàng)建、將 AVFrame 更新至紋理、處理事件等等。具體實現(xiàn)你可以在 ffmpeg_video_player_tutorial-my_tutorial02 中看到。
這里先簡單介紹下 SDLApp 的主要方法:
-
SDLApp::onInit(int video_width, int video_height)
,負責 SDL 資源的初始化,包括窗口、紋理以及 Render。注意,在創(chuàng)建紋理時使用的格式 SDL_PIXELFORMAT_IYUV,對應 FFmpeg 中的 yuv420p 格式。這是最為常用的格式。 -
SDLApp::onLoop(AVFrame *pict)
,負責將 YUV 數(shù)據(jù)更新至紋理。 -
SDLApp::onRender(double sleep_time_s)
,負責渲染紋理,即將紋理顯示在窗口中。其中sleep_time_s
是一個等待時間,用于控制播放的速率。 -
void onEvent(const SDL_Event &event)
負責對 SDL 窗口事件進行響應。在這里,這個函數(shù)其實沒有起任何作用??梢院雎?。 -
SDLApp::onCleanup
,負責釋放 SDL 資源。
SDLApp 負責渲染圖像,F(xiàn)Fmpeg 負責解碼視頻,整體流程大致是這樣的:
// 使用 demuxer 打開文件
FFmpegDmuxer demuxer;
demuxer.openFile(infile);
// 創(chuàng)建視頻解碼器
AVStream *video_stream = demuxer.getStream(video_stream_index);
FFmpegCodec video_codec;
auto codec_id = video_stream->codecpar->codec_id;
auto par = video_stream->codecpar;
video_codec.prepare(codec_id, par);
// 創(chuàng)建 ImageConverter,負責將解碼后的圖像轉換為 yuv420p 格式。與 SDL 中的紋理格式匹配
auto dst_format = AVPixelFormat::AV_PIX_FMT_YUV420P;
auto codec_context = video_codec.getCodecContext();
FFMPEGImageConverter img_conv;
img_conv.prepare(codec_context->width, codec_context->height,
codec_context->pix_fmt, codec_context->width,
codec_context->height, dst_format, SWS_BILINEAR, nullptr,
nullptr, nullptr);
// sdl 環(huán)境初始化
auto video_width = video_codec.getCodecContext()->width;
auto video_height = video_codec.getCodecContext()->height;
SDLApp app;
app.onInit(video_width, video_height);
// 獲取視頻的 fps
double fps = av_q2d(video_stream->r_frame_rate);
double sleep_time = 1.0 / fps;
// 一直解碼,直到滿足某種條件退出
for(;!finished;)
{
AVFrame* frame = decodeNextFrame();
AVFRame* yuv_frame = convertToYUV(frame);
// 顯示圖片
app.onLoop(pict);
app.onRender(sleep_time);
}
app.onCleanup();
好的,以上就是使用 SDL 播放視頻的所有邏輯。詳細代碼請參考 ffmpeg_video_player_tutorial-my_tutorial02。文章來源:http://www.zghlxwxcb.cn/news/detail-532722.html
總結
本文介紹了 SDL 框架,使用 SDL 框架顯示圖片的流程,以及如何結合 FFmpeg 的解碼能力使用 SDL 來播放視頻。在我們的實現(xiàn)中,只顯示顯示了畫面,但沒有聲音,這是沒有靈魂的。在下一章中,我們將介紹如何使用 SDL 同時播放視頻與音頻。文章來源地址http://www.zghlxwxcb.cn/news/detail-532722.html
參考
- SDL - github
- SDL2 簡明教程(一):使用 Cmake 和 Conan 構建 SDL2 編程環(huán)境
- SDL2 簡明教程(二):創(chuàng)建一個空的窗口
- SDL2 簡明教程(三):顯示圖片
- SDL2 簡明教程(四):用 SDL_IMAGE 庫導入圖片
- SDL2 簡明教程(五):OpenGL 繪制
- YUV 文件讀取、顯示、縮放、裁剪等操作教程
- simple_yuv_viewer
- ffmpeg_video_player_tutorial-my_tutorial02
到了這里,關于基于 FFmpeg 的跨平臺視頻播放器簡明教程(五):使用 SDL 播放視頻的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網(wǎng)!