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中的代碼充分調用了ffmpeg中的函數庫,因此,想學習ffmpeg的使用,或基於ffmpeg開發一個自己的播放器,ffplay都是一個很好的切入點。
公眾號:斷點實驗室 音視頻開發系列文章
ffmpeg源碼編譯環境搭建
ffplay源碼編譯
ffmpeg播放器實現詳解 - 框架搭建
ffmpeg播放器實現詳解 - 視頻顯示
ffmpeg播放器實現詳解 - 音頻播放
ffmpeg播放器實現詳解 - 創建線程
由於ffmpeg本身的開發文檔比較少,且ffplay播放器源碼的實現相對復雜,除了基礎的ffmpeg組件調用外,還包含視頻幀的渲染、音頻幀的播放、音視頻同步策略及線程調度等問題。
因此,這里我們以ffmpeg官網推薦的一個ffplay播放器簡化版本的開發例程為基礎,在此基礎上循序漸進由淺入深,最終探討實現一個視頻播放器的完整邏輯。
ffplay播放器簡化版本開發例程可在ffmpeg官網[documentation]頁面的右下角找到,點擊An FFmpeg and SDL Tutorial即可打開找到對應的源碼。

這里對其中部分難以理解的代碼進行了修改,並對幾乎所有代碼逐行注釋,方便大家理解,大家也可以通過下面的鏈接取得源碼,或者關注[公眾號:斷點實驗室],留下郵箱。
1、項目編譯環境搭建
這里仍以Ubuntu 16.04 LTS為基礎進行講述,由於ffmpeg支持多個主流平台,且api接口在各個平台是一致的,因此其他平台也可參照本文內容,后續會將代碼移植到windows等其他平台,方便大家調試。
源碼的編譯除了ffmpeg環境外,還需要SDL-1.x版本的支持,用於提供視頻幀的渲染及音頻幀的播放。
ffmpeg源碼編譯見[公眾號:斷點實驗室] 的前述文章 [ffmpeg源碼編譯安裝]
1.1 sdl庫編譯
SDL(Simple DirectMedia Layer)是一個跨平台的多媒體和游戲開發包,提供2D,音頻,事件驅動,多線程和定時器等服務,它使用C語言寫成,提供了多種控制圖像、聲音、輸出的函數,讓開發者只要用相同或是相似的代碼就可以開發出跨多個平台(Linux、Windows、Mac OS X等)的應用軟件。
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
下載完成后解壓進入sdl源碼目錄,可通過下面的配置方法生成Makefile文件
./configure --prefix=/usr/local/3rdparty/sdl
生成Makefile文件后,輸入make命令即可開始編譯過程,編譯完成后,執行make install命令進行安裝
make
make install
安裝完成后,會在configure指定的目錄下找到sdl的目錄,由於sdl以庫文件的方式提供支持,因此在sdl/bin目錄下沒有對應的可執行文件。
1.2 sdl環境變量配置
sdl編譯完成后,還需要讓系統能夠找到對應的安裝位置。打開/etc/profile配置文件,在該文件底部添加sdl的環境變量
#SDL ENVIRONMENT
export C_INCLUDE_PATH=/usr/local/3rdparty/sdl/include/SDL:$C_INCLUDE_PATH
export LD_LIBRARY_PATH=/usr/local/3rdparty/sdl/lib:$LD_LIBRARY_PATH
export PKG_CONFIG_PATH=/usr/local/3rdparty/sdl/lib/pkgconfig:$PKG_CONFIG_PATH
1.3 項目源碼編譯
項目源碼可采用如下Makefile腳本進行編譯
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
執行make命令開始編譯,編譯完成后,可在源碼目錄生成名為[tutorial01]的可執行文件。
1.4 驗證
與ffplay的使用方法類似,執行[tutorial01 url]命令,可以看到在源碼目錄生成的后綴名為.ppm的圖像
./tutorial01 rtmp://58.200.131.2:1935/livetv/hunantv
ppm圖像在linux平台下可直接打開,看到有ppm圖像生成,即可確定項目能夠正常工作,輸入Ctrl+C結束程序運行。

ppm格式的圖像平時不太常用,大家沒有必要做深入研究,這里僅用於對編譯結果的驗證。
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.
2 源碼分析
上述例程除了生成幾張圖片外,好像什么也做不了,似乎離一個功能完整的視頻播放器還有很遠的距離。
盡管如此,例程依然包含了ffmpeg視頻開發用到的幾乎所有關鍵的api與數據結構。后面的內容會在此基礎上不斷的完善,直至實現一個完整的視頻播放器。
2.1 流程
下面給出例程的流程圖,流程非常簡單,所有代碼都運行在主線程中,流程涉及api及數據結構的含義都在例程源碼中有詳細的注釋。

2.2 源碼中涉及的api及組件
由於篇幅的限制,這里先簡要介紹每個組件及api的含義,后續文章中會深入介紹每個組件及api的使用方法
組件:
- AVFormatContext 保存文件容器封裝信息及碼流參數的結構體
- AVCodecContext 解碼器上下文對象,解碼器依賴的相關環境、狀態、資源以及參數集的接口指針
- AVCodec 保存編解碼器信息的結構體,提供編碼與解碼的公共接口
- AVPacket 負責保存壓縮編碼數據相關信息的結構體,每幀圖像由一到多個packet包組成
- AVFrame 保存音視頻解碼后的數據,如狀態信息、編解碼器信息、宏塊類型表,QP表,運動矢量表等數據
- SwsContext 描述轉換器參數的結構體
api :
- av_register_all 注冊所有ffmpeg支持的多媒體格式及編解碼器
- avformat_open_input 打開視頻文件,讀文件頭內容,取得文件容器的封裝信息及碼流參數並存儲在pFormatCtx中
- avformat_find_stream_info 取得文件中保存的碼流信息,並填充到pFormatCtx->stream 字段
- avcodec_find_decoder 根據視頻流對應的解碼器上下文查找對應的解碼器,返回對應的解碼器
- avcodec_alloc_context3 復制編解碼器上下文對象,用於保存從視頻流中抽取的幀
- avcodec_open2 打開解碼器
- av_frame_alloc 為解碼后的視頻信息結構體分配空間並完成初始化操作
- av_read_frame 從文件中依次讀取每個圖像編碼數據包,並存儲在AVPacket數據結構中
- avcodec_decode_video2 解碼完整的一幀數據,若一個packet無法解碼一個完整的視頻幀,則在ffmpeg后台維護的緩存隊列會持續等待多個packet,直到能夠解碼出一個完整的視頻幀為止
3 ffmpeg能幫我們做什么
視頻開發涉及到多種視頻格式的編解碼,多種文件格式及傳輸協議的解封裝等操作,很難一下子全部掌握。
ffmpeg通過其封裝的api及組件,為我們屏蔽了不同視頻封裝格式及編碼格式的差異,以統一的api接口提供給開發者使用,開發者不需要了解每種編碼方式及封裝方式具體的技術細節,只需要調用ffmpeg提供的api就可以完成解封裝和解碼的操作了。
至於視頻幀的渲染及音頻幀的播放,ffmpeg就無能為力了,因此需要借助類似sdl庫等其他組件完成,后面的章節會為大家介紹繼續介紹。
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 <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libswscale/swscale.h>
#include <libavutil/imgutils.h>
#include <stdio.h>
// compatibility with newer API
#if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(55,28,1)
#define av_frame_alloc avcodec_alloc_frame
#define av_frame_free avcodec_free_frame
#endif
//保存PPM文件
void SaveFrame(AVFrame *pFrame, int width, int height, int iFrame) {
FILE *pFile;//定義文件對象
char szFilename[32];//定義輸出文件名
// Open file,打開文件
sprintf(szFilename, "frame%d.ppm", iFrame);//格式化輸出文件名
pFile = fopen(szFilename, "wb");//打開輸出文件
if (pFile == NULL) {//檢查輸出文件是否打開成功
return;
}
// Write header indicated how wide & tall the image is,向輸出文件中寫入文件頭
fprintf(pFile, "P6\n%d %d\n255\n", width, height);
// Write pixel data,write the file one line a time,一次一行循環寫入RGB24像素值
int y;
for (y = 0; y < height; y++) {
fwrite(pFrame->data[0]+y*pFrame->linesize[0], 1, width*3, pFile);
}
// Close file,關閉文件
fclose(pFile);
}
int main(int argc, char *argv[]) {
/*--------------參數定義-------------*/
// Initalizing these to NULL prevents segfaults!
AVFormatContext *pFormatCtx = NULL;//保存文件容器封裝信息及碼流參數的結構體
AVCodecContext *pCodecCtxOrig = NULL;//解碼器上下文對象,解碼器依賴的相關環境、狀態、資源以及參數集的接口指針
AVCodecContext *pCodecCtx = NULL;//編碼器上下文對象,用於PPM文件輸出
AVCodec *pCodec = NULL;//保存編解碼器信息的結構體,提供編碼與解碼的公共接口,可以看作是編碼器與解碼器的一個全局變量
AVPacket packet;//負責保存壓縮編碼數據相關信息的結構體,每幀圖像由一到多個packet包組成
AVFrame *pFrame = NULL;//保存音視頻解碼后的數據,如狀態信息、編解碼器信息、宏塊類型表,QP表,運動矢量表等數據
AVFrame *pFrameRGB = NULL;//保存輸出24-bit RGB的PPM文件數據
struct SwsContext *sws_ctx = NULL;//描述轉換器參數的結構體
int numBytes;//RGB24格式數據長度
uint8_t *buffer = NULL;//解碼數據輸出緩存指針
int i,videoStream;//循環變量,視頻流類型標號
int frameFinished;//解碼操作是否成功標識
/*-------------參數初始化------------*/
if (argc<2) {//檢查輸入參數個數是否正確
printf("Please provide a movie file\n");
return -1;
}
// Register all available formats and codecs,注冊所有ffmpeg支持的多媒體格式及編解碼器
av_register_all();
/*-----------------------
* Open video file,打開視頻文件,讀文件頭內容,取得文件容器的封裝信息及碼流參數並存儲在pFormatCtx中
* read the file header and stores information about the file format in the AVFormatContext structure
* The last three arguments are used to specify the file format, buffer size, and format options
* but by setting this to NULL or 0, libavformat will auto-detect these
-----------------------*/
if (avformat_open_input(&pFormatCtx, argv[1], NULL, NULL) != 0) {
return -1; // Couldn't open file.
}
/*-----------------------
* 取得文件中保存的碼流信息,並填充到pFormatCtx->stream 字段
* 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) < 0) {
return -1; // Couldn't find stream information.
}
// Dump information about file onto standard error,打印pFormatCtx中的碼流信息
av_dump_format(pFormatCtx, 0, argv[1], 0);
// Find the first video stream.
videoStream=-1;//視頻流類型標號初始化為-1
for (i=0;i<pFormatCtx->nb_streams;i++) {//遍歷文件中包含的所有流媒體類型(視頻流、音頻流、字幕流等)
if(pFormatCtx->streams[i]->codec->codec_type==AVMEDIA_TYPE_VIDEO) {//若文件中包含有視頻流
videoStream = i;//用視頻流類型的標號修改標識,使之不為-1
break;//退出循環
}
}
if (videoStream==-1) {//檢查文件中是否存在視頻流
return -1; // Didn't find a video stream.
}
// Get a pointer to the codec context for the video stream,根據流類型標號從pFormatCtx->streams中取得視頻流對應的解碼器上下文
pCodecCtxOrig = pFormatCtx->streams[videoStream]->codec;
/*-----------------------
* Find the decoder for the video stream,根據視頻流對應的解碼器上下文查找對應的解碼器,返回對應的解碼器(信息結構體)
* 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!\n");
return -1; // Codec not found.
}
// Copy context,復制編解碼器上下文對象,用於保存從視頻流中抽取的幀
pCodecCtx = avcodec_alloc_context3(pCodec);//創建AVCodecContext結構體對象pCodecCtx
if (avcodec_copy_context(pCodecCtx, pCodecCtxOrig) != 0) {//復制編解碼器上下文對象
fprintf(stderr, "Couldn't copy codec context");
return -1; // Error copying codec context.
}
// Open codec,打開解碼器
if (avcodec_open2(pCodecCtx, pCodec, NULL) < 0) {
return -1; // Could not open codec.
}
// Allocate video frame,為解碼后的視頻信息結構體分配空間並完成初始化操作(結構體中的圖像緩存按照下面兩步手動安裝)
pFrame = av_frame_alloc();
// Allocate an AVFrame structure,為轉換PPM文件的結構體分配空間並完成初始化操作
pFrameRGB = av_frame_alloc();
if (pFrameRGB == NULL) {//檢查初始化操作是否成功
return -1;
}
// Determine required buffer size and allocate buffer,根據像素格式及圖像尺寸計算內存大小
numBytes = av_image_get_buffer_size(AV_PIX_FMT_RGB24, pCodecCtx->width, pCodecCtx->height, 1);
buffer = (uint8_t *) av_malloc(numBytes * sizeof(uint8_t));//為轉換后的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指針結構上
av_image_fill_arrays(pFrameRGB->data, pFrameRGB->linesize, buffer, AV_PIX_FMT_RGB24, pCodecCtx->width, pCodecCtx->height, 1);
// Initialize SWS context for software scaling,設置圖像轉換像素格式為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);
/*--------------循環解碼-------------*/
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) {//從視頻文件或網絡流媒體中依次讀取每個圖像編碼數據包,並存儲在AVPacket數據結構中
// Is this a packet from the video stream,檢查數據包類型
if (packet.stream_index == videoStream) {
/*-----------------------
* Decode video frame,解碼完整的一幀數據,並將frameFinished設置為true
* 可能無法通過只解碼一個packet就獲得一個完整的視頻幀frame,可能需要讀取多個packet才行
* avcodec_decode_video2()會在解碼到完整的一幀時設置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,//將解碼后的圖像轉換為RGB24格式
sws_scale(sws_ctx, (uint8_t const * const *) pFrame->data, pFrame->linesize, 0, pCodecCtx->height, pFrameRGB->data, pFrameRGB->linesize);
if (++i <= 5) {// Save the frame to disk,將前5幀圖像存儲到磁盤上
SaveFrame(pFrameRGB, pCodecCtx->width, pCodecCtx->height, i);
}
}
}
// Free the packet that was allocated by av_read_frame,釋放AVPacket數據結構中編碼數據指針
av_packet_unref(&packet);
}
/*--------------參數撤銷-------------*/
// 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;
}
// 著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。
// 公眾號:斷點實驗室
// 掃描二維碼,關注更多優質原創,內容包括:音視頻開發、圖像處理、網絡、
// Linux,Windows、Android、嵌入式開發等
