ffplay是ffmpeg源碼中一個自帶的開源播放器實例,同時支持本地視頻文件的播放以及在線流媒體播放,功能很是強大。shell
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.api
ffplay中的代碼充分調用了ffmpeg中的函數庫,所以,想學習ffmpeg的使用,或基於ffmpeg開發一個本身的播放器,ffplay都是一個很好的切入點。緩存
因爲ffmpeg自己的開發文檔比較少,且ffplay播放器源碼的實現相對複雜,除了基礎的ffmpeg組件調用外,還包含視頻幀的渲染、音頻幀的播放、音視頻同步策略及線程調度等問題。網絡
所以,這裏咱們以ffmpeg官網推薦的一個ffplay播放器簡化版本的開發例程爲基礎,在此基礎上按部就班由淺入深,最終探討實現一個視頻播放器的完整邏輯。數據結構
在上篇文章中介紹了若是搭建一個基於ffmpeg的播放器框架
本文在上篇文章的基礎上,繼續討論如何將ffmpeg解碼出的視頻幀進行渲染顯示多線程
上篇文章中介紹瞭如何基於ffmpeg搭建一個視頻播放器框架,運行程序後能夠看到,除了生成幾張圖片外,程序好像什麼也作不了。框架
這是由於ffmpeg經過其封裝的api及組件,爲咱們屏蔽了不一樣視頻封裝格式及編碼格式的差別,以統一的api接口提供給開發者使用,開發者不須要了解每種編碼方式及封裝方式具體的技術細節,只須要調用ffmpeg提供的api就能夠完成解封裝和解碼的操做了。ide
至於視頻幀的渲染及音頻幀的播放,ffmpeg就無能爲力了,所以須要藉助相似sdl庫等其餘第三方組件來完成。函數
這裏講述如何使用sdl庫完成視頻幀的渲染,sdl在底層封裝了opengl圖形庫,sdl提供的api簡化了opengl的繪圖操做,爲開發者提供了不少便利的操做,固然,你也能夠採用其餘系統支持的圖形庫來繪製視頻幀。oop
sdl庫的編譯安裝詳見[公衆號:斷點實驗室]的前述文章 [ffmpeg播放器實現詳解 - 框架搭建]。
一個視頻幀在顯示前,須要準備一個用於顯示視頻的窗口對象,以及附着在窗口上的畫布對象
建立SDL窗口,並指定圖像尺寸及像素個數
// 建立SDL窗口,並指定圖像尺寸 screen = SDL_SetVideoMode(pCodecCtx->width, pCodecCtx->height, 24, 0);
建立畫布對象
// 建立畫布對象 bmp = SDL_CreateYUVOverlay(pCodecCtx->width, pCodecCtx->height, SDL_YV12_OVERLAY, screen);
在窗口和畫布對象建立完成後,就能夠開始視頻幀的渲染顯示了。
在對畫布對象操做前,須要對其加線程鎖保護,避免其餘線程對畫布中的內容進行競爭性訪問(後面的內容很快會涉及到多線程環境的開發)。對線程操做不熟悉的同窗能夠了解一下在多線程環境下,多個線程對臨界區資源的競爭性訪問與線程同步操做。
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簡化了渲染流程。
本例程和上篇文章中用到的編譯方法徹底同樣
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命令查詢當前可執行文件全部依賴的動態庫。
執行[tutorial02 url]命令,能夠看到有畫面輸出了。
./tutorial02 rtmp://58.200.131.2:1935/livetv/hunantv
雖然畫面已經有了,但還缺乏聲音,下篇文章會繼續完善咱們的播放器開發,討論如何播放聲音。
視頻播放中可能會出現如下兩個問題
sdl找不到音頻設備 SDL_OpenAudio no such audio device
sdl沒法初始化 Could not initialize SDL, no available video device
解決方法見[公衆號:斷點實驗室]的前述文章 [ffplay源碼編譯]。
源碼很是的簡單,僅在上篇的內容基礎上,增長了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、嵌入式開發等