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官網推薦的一個ffplay播放器簡化版本的開發例程為基礎,在此基礎上循序漸進由淺入深,最終探討實現一個視訊播放器的完整邏輯。
在上篇文章中介紹瞭如果搭建一個基於ffmpeg的播放器框架
本文在上篇文章的基礎上,繼續討論如何將ffmpeg解碼出的視訊幀進行渲染顯示
1、視訊幀渲染
上篇文章中介紹瞭如何基於ffmpeg搭建一個視訊播放器框架,執行程式後可以看到,除了生成幾張圖片外,程式好像什麼也做不了。
這是因為ffmpeg通過其封裝的api及元件,為我們遮蔽了不同視訊封裝格式及編碼格式的差異,以統一的api介面提供給開發者使用,開發者不需要了解每種編碼方式及封裝方式具體的技術細節,只需要呼叫ffmpeg提供的api就可以完成解封裝和解碼的操作了。
至於視訊幀的渲染及音訊幀的播放,ffmpeg就無能為力了,因此需要藉助類似sdl庫等其他第三方元件來完成。
這裡講述如何使用sdl庫完成視訊幀的渲染,sdl在底層封裝了opengl圖形庫,sdl提供的api簡化了opengl的繪圖操作,為開發者提供了很多便利的操作,當然,你也可以採用其他系統支援的圖形庫來繪製視訊幀。
sdl庫的編譯安裝詳見[公眾號:斷點實驗室]的前述文章 [ffmpeg播放器實現詳解 - 框架搭建]。
1.1 渲染環境搭建
一個視訊幀在顯示前,需要準備一個用於顯示視訊的視窗物件,以及附著在視窗上的畫布物件
建立SDL視窗,並指定影像尺寸及畫素個數
// 建立SDL視窗,並指定影像尺寸
screen = SDL_SetVideoMode(pCodecCtx->width, pCodecCtx->height, 24, 0);
建立畫布物件
// 建立畫布物件
bmp = SDL_CreateYUVOverlay(pCodecCtx->width, pCodecCtx->height, SDL_YV12_OVERLAY, screen);
1.2 視訊幀渲染
在視窗和畫布物件建立完成後,就可以開始視訊幀的渲染顯示了。
在對畫布物件操作前,需要對其加執行緒鎖保護,避免其他執行緒對畫布中的內容進行競爭性訪問(後面的內容很快會涉及到多執行緒環境的開發)。對執行緒操作不熟悉的同學可以瞭解一下在多執行緒環境下,多個執行緒對臨界區資源的競爭性訪問與執行緒同步操作。
SDL_LockYUVOverlay(bmp);//locks the overlay for direct access to pixel data
向畫布注入解碼後的視訊幀
sws_scale(sws_ctx, (uint8_t const * const *)pFrame->data, pFrame->linesize, 0, pCodecCtx->height, pict.data, pict.linesize);
在畫布物件的視訊幀填充操作完成後,釋放sdl執行緒鎖。
//Unlocks a previously locked overlay. An overlay must be unlocked before it can be displayed
SDL_UnlockYUVOverlay(bmp);
對視訊幀的渲染
SDL_DisplayYUVOverlay(bmp, &rect);//影像渲染
可以看到,由於藉助了sdl封裝的api繪圖介面,視訊幀的渲染還是非常容易的,如果直接採用opengl繪圖,繪製過程會相對複雜些,例程主要的目的是為了介紹ffmpeg的使用,因此,這裡採用sdl簡化了渲染流程。
1.3 專案原始碼編譯
本例程和上篇文章中用到的編譯方法完全一樣
tutorial02: tutorial02.c
gcc -o tutorial02 -g3 tutorial02.c -I${FFMPEG_INCLUDE} -I${SDL_INCLUDE} \
-L${FFMPEG_LIB} -lavutil -lavformat -lavcodec -lswscale -lswresample -lz -lm \
`sdl-config --cflags --libs`
clean:
rm -rf tutorial02
執行make命令開始編譯,編譯完成後,可在原始碼目錄生成名為[tutorial02]的可執行檔案。
可通過ldd命令查詢當前可執行檔案所有依賴的動態庫。
1.4 驗證
執行[tutorial02 url]命令,可以看到有畫面輸出了。
./tutorial02 rtmp://58.200.131.2:1935/livetv/hunantv
雖然畫面已經有了,但還缺少聲音,下篇文章會繼續完善我們的播放器開發,討論如何播放聲音。
2、視訊播放中可能出現的問題
視訊播放中可能會出現以下兩個問題
sdl找不到音訊裝置 SDL_OpenAudio no such audio device
sdl無法初始化 Could not initialize SDL, no available video device
解決方法見[公眾號:斷點實驗室]的前述文章 [ffplay原始碼編譯]。
3、原始碼清單
原始碼非常的簡單,僅在上篇的內容基礎上,增加了sdl渲染環境的搭建,整個原始碼仍然執行在main的主執行緒中,後面的內容會涉及多個執行緒的排程及同步的場景。
// tutorial02.c
// A pedagogical video player that will stream through every video frame as fast as it can.
//
// This tutorial was written by Stephen Dranger (dranger@gmail.com).
//
// Code based on FFplay, Copyright (c) 2003 Fabrice Bellard,
// and a tutorial by Martin Bohme (boehme@inb.uni-luebeckREMOVETHIS.de)
// Tested on Gentoo, CVS version 5/01/07 compiled with GCC 4.1.1
//
// Updates tested on:
// Mac OS X 10.11.6
// Apple LLVM version 8.0.0 (clang-800.0.38)
//
// Use
//
// $ gcc -o tutorial02 tutorial02.c -lavutil -lavformat -lavcodec -lswscale -lz -lm `sdl-config --cflags --libs`
//
// to build (assuming libavutil/libavformat/libavcodec/libswscale are correctly installed your system).
//
// Run using
//
// $ tutorial02 myvideofile.mpg
//
// to play the video stream on your screen.
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libswscale/swscale.h>
#include <SDL.h>
#include <SDL_thread.h>
#ifdef __MINGW32__
#undef main // Prevents SDL from overriding main().
#endif
#include <stdio.h>
int main(int argc, char *argv[]) {
/*--------------引數定義-------------*/
AVFormatContext *pFormatCtx = NULL;//儲存檔案容器封裝資訊及碼流引數的結構體
AVCodecContext *pCodecCtx = NULL;//解碼器上下文物件,解碼器依賴的相關環境、狀態、資源以及引數集的介面指標
AVCodec *pCodec = NULL;//儲存編解碼器資訊的結構體,提供編碼與解碼的公共介面,可以看作是編碼器與解碼器的一個全域性變數
AVPacket packet;//負責儲存壓縮編碼資料相關資訊的結構體,每幀影像由一到多個packet包組成
AVFrame *pFrame = NULL;//儲存音視訊解碼後的資料,如狀態資訊、編解碼器資訊、巨集塊型別表,QP表,運動向量表等資料
struct SwsContext *sws_ctx = NULL;//描述轉換器引數的結構體
AVDictionary *optionsDict = NULL;
SDL_Surface *screen = NULL;//SDL繪圖視窗,A structure that contains a collection of pixels used in software blitting
SDL_Overlay *bmp = NULL;//SDL畫布
SDL_Rect rect;//SDL矩形物件
SDL_Event event;//SDL事件物件
int i, videoStream;//迴圈變數,視訊流型別標號
int frameFinished;//解碼操作是否成功標識
/*-------------引數初始化------------*/
if (argc<2) {//檢查輸入引數個數是否正確
fprintf(stderr, "Usage: test <file>\n");
exit(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中取得視訊流對應的解碼器上下文
pCodecCtx = 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(pCodecCtx->codec_id);
if (pCodec == NULL) {//檢查解碼器是否匹配
fprintf(stderr, "Unsupported codec!\n");
return -1; // Codec not found.
}
// Open codec,開啟解碼器
if (avcodec_open2(pCodecCtx, pCodec, &optionsDict) < 0) {
return -1; // Could not open codec.
}
// Allocate video frame,為解碼後的視訊資訊結構體分配空間並完成初始化操作(結構體中的影像快取按照下面兩步手動安裝)
pFrame = av_frame_alloc();
// Initialize SWS context for software scaling,設定影像轉換畫素格式為AV_PIX_FMT_YUV420P
sws_ctx = sws_getContext(pCodecCtx->width, pCodecCtx->height, pCodecCtx->pix_fmt, pCodecCtx->width, pCodecCtx->height, AV_PIX_FMT_YUV420P, SWS_BILINEAR, NULL, NULL, NULL);
//SDL_Init initialize the Event Handling, File I/O, and Threading subsystems,初始化SDL
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER)) {//initialize the video audio & timer subsystem
fprintf(stderr, "Could not initialize SDL - %s\n", SDL_GetError());//tell the library what features we're going to use
exit(1);
}
// Make a screen to put our video,在SDL2.0中SDL_SetVideoMode及SDL_Overlay已經棄用,改為SDL_CreateWindow及SDL_CreateRenderer建立視窗及著色器
#ifndef __DARWIN__
screen = SDL_SetVideoMode(pCodecCtx->width, pCodecCtx->height, 24, 0);//建立SDL視窗,並指定影像尺寸
#else
screen = SDL_SetVideoMode(pCodecCtx->width, pCodecCtx->height, 24, 0);//建立SDL視窗,並指定影像尺寸
#endif
if (!screen) {//檢查SDL視窗是否建立成功
fprintf(stderr, "SDL: could not set video mode - exiting\n");
exit(1);
}
SDL_WM_SetCaption(argv[1],0);//用輸入檔名設定SDL視窗標題
// Allocate a place to put our YUV image on that screen,建立畫布物件
bmp = SDL_CreateYUVOverlay(pCodecCtx->width, pCodecCtx->height, SDL_YV12_OVERLAY, screen);
/*--------------迴圈解碼-------------*/
i = 0;// Read frames 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) {
SDL_LockYUVOverlay(bmp);//locks the overlay for direct access to pixel data,原子操作,保護畫素緩衝區臨界資源
AVFrame pict;//儲存轉換為AV_PIX_FMT_YUV420P格式的視訊幀
pict.data[0] = bmp->pixels[0];//將轉碼後的影像與畫布的畫素緩衝器關聯
pict.data[1] = bmp->pixels[2];
pict.data[2] = bmp->pixels[1];
pict.linesize[0] = bmp->pitches[0];//將轉碼後的影像掃描行長度與畫布畫素緩衝區的掃描行長度相關聯
pict.linesize[1] = bmp->pitches[2];//linesize-Size, in bytes, of the data for each picture/channel plane
pict.linesize[2] = bmp->pitches[1];//For audio, only linesize[0] may be set
// Convert the image into YUV format that SDL uses,將解碼後的影像轉換為AV_PIX_FMT_YUV420P格式,並賦值到pict物件
sws_scale(sws_ctx, (uint8_t const * const *)pFrame->data, pFrame->linesize, 0, pCodecCtx->height, pict.data, pict.linesize);
SDL_UnlockYUVOverlay(bmp);//Unlocks a previously locked overlay. An overlay must be unlocked before it can be displayed
//設定矩形顯示區域
rect.x = 0;
rect.y = 0;
rect.w = pCodecCtx->width;
rect.h = pCodecCtx->height;
SDL_DisplayYUVOverlay(bmp, &rect);//影像渲染
}
}
// Free the packet that was allocated by av_read_frame,釋放AVPacket資料結構中編碼資料指標
av_packet_unref(&packet);
/*-------------------------
* 在每次迴圈中從SDL後臺佇列取事件並填充到SDL_Event物件中
* SDL的事件系統使得你可以接收使用者的輸入,從而完成一些控制操作
* SDL_PollEvent() is the favored way of receiving system events
* since it can be done from the main loop and does not suspend the main loop
* while waiting on an event to be posted
* poll for events right after we finish processing a packet
------------------------*/
SDL_PollEvent(&event);
switch (event.type) {//檢查SDL事件物件
case SDL_QUIT://退出事件
printf("SDL_QUIT\n");
SDL_Quit();//退出操作
exit(0);//結束程式
break;
default:
break;
}//end for switch
}//end for while
/*--------------引數撤銷-------------*/
// Free the YUV frame.
av_free(pFrame);
// Close the codec.
avcodec_close(pCodecCtx);
// Close the video file.
avformat_close_input(&pFormatCtx);
return 0;
}
// 著作權歸作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。
// 公眾號:斷點實驗室
// 掃描二維碼,關注更多優質原創,內容包括:音視訊開發、影像處理、網路、
// Linux,Windows、Android、嵌入式開發等