作者: 葉餘 來源: 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 簡易播放器系列文章如下: FFmpeg簡易播放器的實作1-最簡版 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] 雷霄骅,
[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]
YUV,
https://zh.wikipedia.org/wiki/YUV5. 修改記錄
2018-11-23 V1.0 初稿
2018-11-29 V1.1 增加定時重新整理線程,使解碼幀率更加準确
「視訊雲技術」你最值得關注的音視訊技術公衆号,每周推送來自阿裡雲一線的實踐技術文章,在這裡與音視訊領域一流工程師交流切磋。