本文爲做者原創:http://www.javashuo.com/article/p-hydpotvi-gy.html,轉載請註明出處html
基於FFmpeg和SDL實現的簡易視頻播放器,主要分爲讀取視頻文件解碼和調用SDL播放兩大部分。
本實驗僅研究視頻播放的實現方式。git
FFmpeg簡易播放器系列文章以下:
[1]. FFmpeg簡易播放器的實現-最簡版
[2]. FFmpeg簡易播放器的實現-視頻播放
[3]. FFmpeg簡易播放器的實現-音頻播放
[4]. FFmpeg簡易播放器的實現-音視頻播放
[5]. FFmpeg簡易播放器的實現-音視頻同步github
下圖引用自「雷霄驊,視音頻編解碼技術零基礎學習方法」,因原圖過小,看不太清楚,故從新制做了一張圖片。
以下內容引用自「雷霄驊,視音頻編解碼技術零基礎學習方法」:shell
解協議
將流媒體協議的數據,解析爲標準的相應的封裝格式數據。視音頻在網絡上傳播的時候,經常採用各類流媒體協議,例如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數據。數據結構音視頻同步
根據解封裝模塊處理過程當中獲取到的參數信息,同步解碼出來的視頻和音頻數據,並將視頻音頻數據送至系統的顯卡和聲卡播放出來。ide
實驗平臺:openSUSE Leap 42.3
FFmpeg版本:4.1
SDL版本:2.0.9
FFmpeg開發環境搭建可參考「FFmpeg開發環境構建」函數
代碼已經變得挺長了,不貼完整源碼了,源碼參考:
https://github.com/leichn/exercises/blob/master/source/ffmpeg/player_video/ffplayer.c學習
源碼清單中涉及的一些概念簡述以下:
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。這樣能夠實現同一窗口的分屏顯示。
流程比較簡單,不畫流程圖了,簡述以下:
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
調用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; } }
調用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; }
圖像格式轉換的目的,是爲了解碼後的視頻幀能被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 );
調用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);
上一版源碼存在的兩個問題:
[1]. 以固定25FPS的幀率播放視頻文件,對於幀率不是25FPS的視頻文件,播放是不正常的
[2]. 即便對於幀率是25FPS的文件來講,幀率控制仍然較不許確,由於未考慮解碼視頻幀消耗的時間
本版源碼針對此問題做了改善,將上一版代碼拆分爲兩個線程:定時刷新線程 + 解碼主線程。
定時刷新線程按計算出的幀率發送自定義SDL事件,通知解碼主線程
解碼主線程收到SDL事件後,獲取一個視頻幀解碼並顯示
gcc -o ffplayer ffplayer.c -lavutil -lavformat -lavcodec -lavutil -lswscale -lSDL2
選用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次後播放結束。播放過程只有圖像,沒有聲音。播放正常。
[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] YUV,https://zh.wikipedia.org/wiki/YUV
2018-11-23 V1.0 初稿 2018-11-29 V1.1 增長定時刷新線程,使解碼幀率更加準確 2019-01-12 V1.2 增長解碼及顯示過程說明