ffplay是ffmpeg源碼中一個自帶的開源播放器實例,同時支持本地視頻文件的播放以及在線流媒體播放,功能非常強大。
FFplay: FFplay is a very simple and portable media player using the FFmpeg libraries and the SDL library. It is mostly used as a testbed for the various FFmpeg APIs
ffplay中的代碼充分調(diào)用了ffmpeg中的函數(shù)庫,因此,想學(xué)習(xí)ffmpeg的使用,或基于ffmpeg開發(fā)一個自己的播放器,ffplay都是一個很好的切入點。 ffplay源碼編譯見[公眾號:斷點實驗室]的前述文章 [ffplay源碼編譯]。 由于ffmpeg本身的開發(fā)文檔比較少,且ffplay播放器源碼的實現(xiàn)相對復(fù)雜,除了基礎(chǔ)的ffmpeg組件調(diào)用外,還包含視頻幀的渲染、音頻幀的播放、音視頻同步策略及線程調(diào)度等問題。
因此,這里我們以ffmpeg官網(wǎng)推薦的一個ffplay播放器簡化版本的開發(fā)例程為基礎(chǔ),在此基礎(chǔ)上循序漸進(jìn)由淺入深,最終探討實現(xiàn)一個視頻播放器的完整邏輯。
ffplay播放器簡化版本開發(fā)例程可在ffmpeg官網(wǎng)[documentation]頁面的右下角找到,點擊An FFmpeg and SDL Tutorial即可打開找到對應(yīng)的源碼。
這里對其中部分難以理解的代碼進(jìn)行了修改,并對幾乎所有代碼逐行注釋,方便大家理解
1、項目編譯環(huán)境搭建
這里仍以Ubuntu 16.04 LTS為基礎(chǔ)進(jìn)行講述,由于ffmpeg支持多個主流平臺,且api接口在各個平臺是一致的,因此其他平臺也可參照本文內(nèi)容,后續(xù)會將代碼移植到windows等其他平臺,方便大家調(diào)試。
源碼的編譯除了ffmpeg環(huán)境外,還需要SDL-1.x版本的支持,用于提供視頻幀的渲染及音頻幀的播放。
1.1 sdl庫編譯
SDL(Simple DirectMedia Layer)是一個跨平臺的多媒體和游戲開發(fā)包,提供2D,音頻,事件驅(qū)動,多線程和定時器等服務(wù),它使用C語言寫成,提供了多種控制圖像、聲音、輸出的函數(shù),讓開發(fā)者只要用相同或是相似的代碼就可以開發(fā)出跨多個平臺(Linux、Windows、Mac OS X等)的應(yīng)用軟件。
SDL: Simple DirectMedia Layer is a cross-platform development library designed to provide low level access to audio, keyboard, mouse, joystick, and graphics hardware via OpenGL and Direct3D. It is used by video playback software, emulators, and popular games including Valve’s award winning catalog and many Humble Bundle games.
可通過下面的鏈接下載SDL-1.2.15源碼,注意,例程中依賴的SDL版本與ffplay中有所不同 https://www.libsdl.org/download-1.2.php
下載完成后解壓進(jìn)入sdl源碼目錄,可通過下面的配置方法生成Makefile文件
./configure –prefix=/usr/local/3rdparty/sdl
生成Makefile文件后,輸入make命令即可開始編譯過程,編譯完成后,執(zhí)行make install命令進(jìn)行安裝
make make install
安裝完成后,會在configure指定的目錄下找到sdl的目錄,由于sdl以庫文件的方式提供支持,因此在sdl/bin目錄下沒有對應(yīng)的可執(zhí)行文件。
1.2 sdl環(huán)境變量配置
sdl編譯完成后,還需要讓系統(tǒng)能夠找到對應(yīng)的安裝位置。打開/etc/profile配置文件,在該文件底部添加sdl的環(huán)境變量
#SDL ENVIRONMENTexport C_INCLUDE_PATH=/usr/local/3rdparty/sdl/include/SDL:$C_INCLUDE_PATHexport LD_LIBRARY_PATH=/usr/local/3rdparty/sdl/lib:$LD_LIBRARY_PATHexport PKG_CONFIG_PATH=/usr/local/3rdparty/sdl/lib/pkgconfig:$PKG_CONFIG_PATH
1.3 項目源碼編譯
項目源碼可采用如下Makefile腳本進(jìn)行編譯
tutorial01: tutorial01.c gcc -o tutorial01 -g3 tutorial01.c -I${FFMPEG_INCLUDE} -I${SDL_INCLUDE} -L${FFMPEG_LIB} -lavutil -lavformat -lavcodec -lswscale -lswresample -lz -lm `sdl-config –cflags –libs`clean: rm -rf tutorial01 rm -rf *.ppm
執(zhí)行make命令開始編譯,編譯完成后,可在源碼目錄生成名為[tutorial01]的可執(zhí)行文件。
1.4 驗證
與ffplay的使用方法類似,執(zhí)行[tutorial01 url]命令,可以看到在源碼目錄生成的后綴名為.ppm的圖像
./tutorial01 rtmp://58.200.131.2:1935/livetv/hunantv
ppm圖像在linux平臺下可直接打開,看到有ppm圖像生成,即可確定項目能夠正常工作,輸入Ctrl+C結(jié)束程序運行。
ppm格式的圖像平時不太常用,大家沒有必要做深入研究,這里僅用于對編譯結(jié)果的驗證。
PPM: A PPM file is a 24-bit color image formatted using a text format. It stores each pixel with a number from 0 to 65536, which specifies the color of the pixel. PPM files also store the image height and width, whitespace data, and the maximum color value. The portable pixmap format (PPM), the portable graymap format (PGM) and the portable bitmap format (PBM) are image file formats designed to be easily exchanged between platforms.
領(lǐng)取C++音視頻開發(fā)學(xué)習(xí)資料:點擊 音視頻開發(fā)(資料文檔+視頻教程+面試題)(FFmpeg+WebRTC+RTMP+RTSP+HLS+RTP)
2 源碼分析
上述例程除了生成幾張圖片外,好像什么也做不了,似乎離一個功能完整的視頻播放器還有很遠(yuǎn)的距離。 盡管如此,例程依然包含了ffmpeg視頻開發(fā)用到的幾乎所有關(guān)鍵的api與數(shù)據(jù)結(jié)構(gòu)。后面的內(nèi)容會在此基礎(chǔ)上不斷的完善,直至實現(xiàn)一個完整的視頻播放器。
2.1 流程
下面給出例程的流程圖,流程非常簡單,所有代碼都運行在主線程中,流程涉及api及數(shù)據(jù)結(jié)構(gòu)的含義都在例程源碼中有詳細(xì)的注釋。
2.2 源碼中涉及的api及組件
由于篇幅的限制,這里先簡要介紹每個組件及api的含義,后續(xù)文章中會深入介紹每個組件及api的使用方法
組件:
- AVFormatContext 保存文件容器封裝信息及碼流參數(shù)的結(jié)構(gòu)體
- AVCodecContext 解碼器上下文對象,解碼器依賴的相關(guān)環(huán)境、狀態(tài)、資源以及參數(shù)集的接口指針
- AVCodec 保存編解碼器信息的結(jié)構(gòu)體,提供編碼與解碼的公共接口
- AVPacket 負(fù)責(zé)保存壓縮編碼數(shù)據(jù)相關(guān)信息的結(jié)構(gòu)體,每幀圖像由一到多個packet包組成
- AVFrame 保存音視頻解碼后的數(shù)據(jù),如狀態(tài)信息、編解碼器信息、宏塊類型表,QP表,運動矢量表等數(shù)據(jù)
- SwsContext 描述轉(zhuǎn)換器參數(shù)的結(jié)構(gòu)體
api :
- av_register_all 注冊所有ffmpeg支持的多媒體格式及編解碼器
- avformat_open_input 打開視頻文件,讀文件頭內(nèi)容,取得文件容器的封裝信息及碼流參數(shù)并存儲在pFormatCtx中
- avformat_find_stream_info 取得文件中保存的碼流信息,并填充到pFormatCtx->stream 字段
- avcodec_find_decoder 根據(jù)視頻流對應(yīng)的解碼器上下文查找對應(yīng)的解碼器,返回對應(yīng)的解碼器
- avcodec_alloc_context3 復(fù)制編解碼器上下文對象,用于保存從視頻流中抽取的幀
- avcodec_open2 打開解碼器
- av_frame_alloc 為解碼后的視頻信息結(jié)構(gòu)體分配空間并完成初始化操作
- av_read_frame 從文件中依次讀取每個圖像編碼數(shù)據(jù)包,并存儲在AVPacket數(shù)據(jù)結(jié)構(gòu)中
- avcodec_decode_video2 解碼完整的一幀數(shù)據(jù),若一個packet無法解碼一個完整的視頻幀,則在ffmpeg后臺維護的緩存隊列會持續(xù)等待多個packet,直到能夠解碼出一個完整的視頻幀為止
3 ffmpeg能幫我們做什么
視頻開發(fā)涉及到多種視頻格式的編解碼,多種文件格式及傳輸協(xié)議的解封裝等操作,很難一下子全部掌握。 ffmpeg通過其封裝的api及組件,為我們屏蔽了不同視頻封裝格式及編碼格式的差異,以統(tǒng)一的api接口提供給開發(fā)者使用,開發(fā)者不需要了解每種編碼方式及封裝方式具體的技術(shù)細(xì)節(jié),只需要調(diào)用ffmpeg提供的api就可以完成解封裝和解碼的操作了。 至于視頻幀的渲染及音頻幀的播放,ffmpeg就無能為力了,因此需要借助類似sdl庫等其他組件完成,后面的章節(jié)會為大家介紹繼續(xù)介紹。
4 源碼清單
// tutorial01.c// Code based on a tutorial by Martin Bohme (boehme@inb.uni-luebeckREMOVETHIS.de)// Tested on Gentoo, CVS version 5/01/07 compiled with GCC 4.1.1// With updates from https://github.com/chelyaev/ffmpeg-tutorial// Updates tested on:// LAVC 54.59.100, LAVF 54.29.104, LSWS 2.1.101 // on GCC 4.7.2 in Debian February 2015//// Updates tested on:// Mac OS X 10.11.6// Apple LLVM version 8.0.0 (clang-800.0.38)//// A small sample program that shows how to use libavformat and libavcodec to read video from a file.//// Use//// $ gcc -o tutorial01 tutorial01.c -lavutil -lavformat -lavcodec -lswscale -lz -lm//// to build (assuming libavutil/libavformat/libavcodec/libswscale are correctly installed your system).//// Run using//// $ tutorial01 myvideofile.mpg//// to write the first five frames from “myvideofile.mpg” to disk in PPM format.// comment by breakpointlab@outlook.com#include #include #include #include #include // compatibility with newer API#if LIBAVCODEC_VERSION_INT linesize[0], 1, width*3, pFile); } // Close file,關(guān)閉文件 fclose(pFile);}int main(int argc, char *argv[]) {/*————–參數(shù)定義————-*/ // Initalizing these to NULL prevents segfaults! AVFormatContext *pFormatCtx = NULL;//保存文件容器封裝信息及碼流參數(shù)的結(jié)構(gòu)體 AVCodecContext *pCodecCtxOrig = NULL;//解碼器上下文對象,解碼器依賴的相關(guān)環(huán)境、狀態(tài)、資源以及參數(shù)集的接口指針 AVCodecContext *pCodecCtx = NULL;//編碼器上下文對象,用于PPM文件輸出 AVCodec *pCodec = NULL;//保存編解碼器信息的結(jié)構(gòu)體,提供編碼與解碼的公共接口,可以看作是編碼器與解碼器的一個全局變量 AVPacket packet;//負(fù)責(zé)保存壓縮編碼數(shù)據(jù)相關(guān)信息的結(jié)構(gòu)體,每幀圖像由一到多個packet包組成 AVFrame *pFrame = NULL;//保存音視頻解碼后的數(shù)據(jù),如狀態(tài)信息、編解碼器信息、宏塊類型表,QP表,運動矢量表等數(shù)據(jù) AVFrame *pFrameRGB = NULL;//保存輸出24-bit RGB的PPM文件數(shù)據(jù) struct SwsContext *sws_ctx = NULL;//描述轉(zhuǎn)換器參數(shù)的結(jié)構(gòu)體 int numBytes;//RGB24格式數(shù)據(jù)長度 uint8_t *buffer = NULL;//解碼數(shù)據(jù)輸出緩存指針 int i,videoStream;//循環(huán)變量,視頻流類型標(biāo)號 int frameFinished;//解碼操作是否成功標(biāo)識/*————-參數(shù)初始化————*/ if (argcstream 字段 * check out & Retrieve the stream information in the file * then populate pFormatCtx->stream with the proper information * pFormatCtx->streams is just an array of pointers, of size pFormatCtx->nb_streams ———————–*/ if (avformat_find_stream_info(pFormatCtx, NULL) streams[i]->codec->codec_type==AVMEDIA_TYPE_VIDEO) {//若文件中包含有視頻流 videoStream = i;//用視頻流類型的標(biāo)號修改標(biāo)識,使之不為-1 break;//退出循環(huán) } } if (videoStream==-1) {//檢查文件中是否存在視頻流 return -1; // Didn’t find a video stream. } // Get a pointer to the codec context for the video stream,根據(jù)流類型標(biāo)號從pFormatCtx->streams中取得視頻流對應(yīng)的解碼器上下文 pCodecCtxOrig = pFormatCtx->streams[videoStream]->codec; /*———————– * Find the decoder for the video stream,根據(jù)視頻流對應(yīng)的解碼器上下文查找對應(yīng)的解碼器,返回對應(yīng)的解碼器(信息結(jié)構(gòu)體) * The stream’s information about the codec is in what we call the “codec context. * This contains all the information about the codec that the stream is using ———————–*/ pCodec = avcodec_find_decoder(pCodecCtxOrig->codec_id); if (pCodec == NULL) {//檢查解碼器是否匹配 fprintf(stderr, “Unsupported codec!”); return -1; // Codec not found. } // Copy context,復(fù)制編解碼器上下文對象,用于保存從視頻流中抽取的幀 pCodecCtx = avcodec_alloc_context3(pCodec);//創(chuàng)建AVCodecContext結(jié)構(gòu)體對象pCodecCtx if (avcodec_copy_context(pCodecCtx, pCodecCtxOrig) != 0) {//復(fù)制編解碼器上下文對象 fprintf(stderr, “Couldn’t copy codec context”); return -1; // Error copying codec context. } // Open codec,打開解碼器 if (avcodec_open2(pCodecCtx, pCodec, NULL) width, pCodecCtx->height, 1); buffer = (uint8_t *) av_malloc(numBytes * sizeof(uint8_t));//為轉(zhuǎn)換后的RGB24圖像配置緩存空間 // Assign appropriate parts of buffer to image planes in pFrameRGB Note that pFrameRGB is an AVFrame, but AVFrame is a superset of AVPicture // 為AVFrame對象安裝圖像緩存,將out_buffer緩存掛到pFrameYUV->data指針結(jié)構(gòu)上 av_image_fill_arrays(pFrameRGB->data, pFrameRGB->linesize, buffer, AV_PIX_FMT_RGB24, pCodecCtx->width, pCodecCtx->height, 1); // Initialize SWS context for software scaling,設(shè)置圖像轉(zhuǎn)換像素格式為AV_PIX_FMT_RGB24 sws_ctx = sws_getContext(pCodecCtx->width, pCodecCtx->height, pCodecCtx->pix_fmt, pCodecCtx->width, pCodecCtx->height, AV_PIX_FMT_RGB24, SWS_BILINEAR, NULL, NULL, NULL);/*————–循環(huán)解碼————-*/ i = 0;// Read frames(2 packet) and save first five frames to disk, /*———————– * read in a packet and store it in the AVPacket struct * ffmpeg allocates the internal data for us,which is pointed to by packet.data * this is freed by the av_free_packet() ———————–*/ while (av_read_frame(pFormatCtx, &packet) >= 0) {//從視頻文件或網(wǎng)絡(luò)流媒體中依次讀取每個圖像編碼數(shù)據(jù)包,并存儲在AVPacket數(shù)據(jù)結(jié)構(gòu)中 // Is this a packet from the video stream,檢查數(shù)據(jù)包類型 if (packet.stream_index == videoStream) { /*———————– * Decode video frame,解碼完整的一幀數(shù)據(jù),并將frameFinished設(shè)置為true * 可能無法通過只解碼一個packet就獲得一個完整的視頻幀frame,可能需要讀取多個packet才行 * avcodec_decode_video2()會在解碼到完整的一幀時設(shè)置frameFinished為真 * Technically a packet can contain partial frames or other bits of data * ffmpeg’s parser ensures that the packets we get contain either complete or multiple frames * convert the packet to a frame for us and set frameFinisned for us when we have the next frame ———————–*/ avcodec_decode_video2(pCodecCtx, pFrame, &frameFinished, &packet); // Did we get a video frame,檢查是否解碼出完整一幀圖像 if (frameFinished) { // Convert the image from its native format to RGB,//將解碼后的圖像轉(zhuǎn)換為RGB24格式 sws_scale(sws_ctx, (uint8_t const * const *) pFrame->data, pFrame->linesize, 0, pCodecCtx->height, pFrameRGB->data, pFrameRGB->linesize); if (++i width, pCodecCtx->height, i); } } } // Free the packet that was allocated by av_read_frame,釋放AVPacket數(shù)據(jù)結(jié)構(gòu)中編碼數(shù)據(jù)指針 av_packet_unref(&packet); }/*————–參數(shù)撤銷————-*/ // Free the RGB image buffer av_free(buffer); av_frame_free(&pFrameRGB); // Free the YUV frame. av_frame_free(&pFrame); // Close the codecs. avcodec_close(pCodecCtx); avcodec_close(pCodecCtxOrig); // Close the video file. avformat_close_input(&pFormatCtx); return 0;}