ffmpeg播放器實(shí)現(xiàn)詳解之視頻顯示(推薦)
FFmpeg是一套可以用來記錄、轉(zhuǎn)換數(shù)字音頻、視頻,并能將其轉(zhuǎn)化為流的開源計(jì)算機(jī)程序。它包括了目前領(lǐng)先的音/視頻編碼庫(kù)libavcodec。 FFmpeg是在 Linux 下開發(fā)出來的,但它可以在包括 Windows 在內(nèi)的大多數(shù)操作系統(tǒng)中編譯。這個(gè)項(xiàng)目是由 Fabrice Bellard 發(fā)起的,現(xiàn)在由 Michael Niedermayer 主持??梢暂p易地實(shí)現(xiàn)多種視頻格式之間的相互轉(zhuǎn)換,例如可以將攝錄下的視頻avi等轉(zhuǎn)成現(xiàn)在視頻網(wǎng)站所采用的flv格式。
ffplay是ffmpeg源碼中一個(gè)自帶的開源播放器實(shí)例,同時(shí)支持本地視頻文件的播放以及在線流媒體播放,功能非常強(qiáng)大。
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ù)庫(kù),因此,想學(xué)習(xí)ffmpeg的使用,或基于ffmpeg開發(fā)一個(gè)自己的播放器,ffplay都是一個(gè)很好的切入點(diǎn)。
由于ffmpeg本身的開發(fā)文檔比較少,且ffplay播放器源碼的實(shí)現(xiàn)相對(duì)復(fù)雜,除了基礎(chǔ)的ffmpeg組件調(diào)用外,還包含視頻幀的渲染、音頻幀的播放、音視頻同步策略及線程調(diào)度等問題。
因此,這里我們以ffmpeg官網(wǎng)推薦的一個(gè)ffplay播放器簡(jiǎn)化版本的開發(fā)例程為基礎(chǔ),在此基礎(chǔ)上循序漸進(jìn)由淺入深,最終探討實(shí)現(xiàn)一個(gè)視頻播放器的完整邏輯。
在上篇文章中介紹了如果搭建一個(gè)基于ffmpeg的播放器框架
本文在上篇文章的基礎(chǔ)上,繼續(xù)討論如何將ffmpeg解碼出的視頻幀進(jìn)行渲染顯示
1、視頻幀渲染
上篇文章中介紹了如何基于ffmpeg搭建一個(gè)視頻播放器框架,運(yùn)行程序后可以看到,除了生成幾張圖片外,程序好像什么也做不了。
這是因?yàn)閒fmpeg通過其封裝的api及組件,為我們屏蔽了不同視頻封裝格式及編碼格式的差異,以統(tǒng)一的api接口提供給開發(fā)者使用,開發(fā)者不需要了解每種編碼方式及封裝方式具體的技術(shù)細(xì)節(jié),只需要調(diào)用ffmpeg提供的api就可以完成解封裝和解碼的操作了。
至于視頻幀的渲染及音頻幀的播放,ffmpeg就無(wú)能為力了,因此需要借助類似sdl庫(kù)等其他第三方組件來完成。
這里講述如何使用sdl庫(kù)完成視頻幀的渲染,sdl在底層封裝了opengl圖形庫(kù),sdl提供的api簡(jiǎn)化了opengl的繪圖操作,為開發(fā)者提供了很多便利的操作,當(dāng)然,你也可以采用其他系統(tǒng)支持的圖形庫(kù)來繪制視頻幀。
sdl庫(kù)的編譯安裝詳見[公眾號(hào):斷點(diǎn)實(shí)驗(yàn)室]的前述文章 [ffmpeg播放器實(shí)現(xiàn)詳解 - 框架搭建]。
1.1 渲染環(huán)境搭建
一個(gè)視頻幀在顯示前,需要準(zhǔn)備一個(gè)用于顯示視頻的窗口對(duì)象,以及附著在窗口上的畫布對(duì)象
創(chuàng)建SDL窗口,并指定圖像尺寸及像素個(gè)數(shù)
// 創(chuàng)建SDL窗口,并指定圖像尺寸 screen = SDL_SetVideoMode(pCodecCtx->width, pCodecCtx->height, 24, 0);
創(chuàng)建畫布對(duì)象
// 創(chuàng)建畫布對(duì)象 bmp = SDL_CreateYUVOverlay(pCodecCtx->width, pCodecCtx->height, SDL_YV12_OVERLAY, screen);
1.2 視頻幀渲染
在窗口和畫布對(duì)象創(chuàng)建完成后,就可以開始視頻幀的渲染顯示了。
在對(duì)畫布對(duì)象操作前,需要對(duì)其加線程鎖保護(hù),避免其他線程對(duì)畫布中的內(nèi)容進(jìn)行競(jìng)爭(zhēng)性訪問(后面的內(nèi)容很快會(huì)涉及到多線程環(huán)境的開發(fā))。對(duì)線程操作不熟悉的同學(xué)可以了解一下在多線程環(huán)境下,多個(gè)線程對(duì)臨界區(qū)資源的競(jìng)爭(zhēng)性訪問與線程同步操作。
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);
在畫布對(duì)象的視頻幀填充操作完成后,釋放sdl線程鎖。
//Unlocks a previously locked overlay. An overlay must be unlocked before it can be displayed SDL_UnlockYUVOverlay(bmp);
對(duì)視頻幀的渲染
SDL_DisplayYUVOverlay(bmp, &rect);//圖像渲染
可以看到,由于借助了sdl封裝的api繪圖接口,視頻幀的渲染還是非常容易的,如果直接采用opengl繪圖,繪制過程會(huì)相對(duì)復(fù)雜些,例程主要的目的是為了介紹ffmpeg的使用,因此,這里采用sdl簡(jiǎn)化了渲染流程。
1.3 項(xiàng)目源碼編譯
本例程和上篇文章中用到的編譯方法完全一樣
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
執(zhí)行make命令開始編譯,編譯完成后,可在源碼目錄生成名為[tutorial02]的可執(zhí)行文件。
可通過ldd命令查詢當(dāng)前可執(zhí)行文件所有依賴的動(dòng)態(tài)庫(kù)。
1.4 驗(yàn)證
執(zhí)行[tutorial02 url]命令,可以看到有畫面輸出了。
./tutorial02 rtmp://58.200.131.2:1935/livetv/hunantv
雖然畫面已經(jīng)有了,但還缺少聲音,下篇文章會(huì)繼續(xù)完善我們的播放器開發(fā),討論如何播放聲音。
2、視頻播放中可能出現(xiàn)的問題
視頻播放中可能會(huì)出現(xiàn)以下兩個(gè)問題
sdl找不到音頻設(shè)備 SDL_OpenAudio no such audio device
sdl無(wú)法初始化 Could not initialize SDL, no available video device
解決方法見[公眾號(hào):斷點(diǎn)實(shí)驗(yàn)室]的前述文章 [ffplay源碼編譯]。
3、源碼清單
源碼非常的簡(jiǎn)單,僅在上篇的內(nèi)容基礎(chǔ)上,增加了sdl渲染環(huán)境的搭建,整個(gè)源碼仍然運(yùn)行在main的主線程中,后面的內(nèi)容會(huì)涉及多個(gè)線程的調(diào)度及同步的場(chǎng)景。
// 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[]) { /*--------------參數(shù)定義-------------*/ AVFormatContext *pFormatCtx = NULL;//保存文件容器封裝信息及碼流參數(shù)的結(jié)構(gòu)體 AVCodecContext *pCodecCtx = NULL;//解碼器上下文對(duì)象,解碼器依賴的相關(guān)環(huán)境、狀態(tài)、資源以及參數(shù)集的接口指針 AVCodec *pCodec = NULL;//保存編解碼器信息的結(jié)構(gòu)體,提供編碼與解碼的公共接口,可以看作是編碼器與解碼器的一個(gè)全局變量 AVPacket packet;//負(fù)責(zé)保存壓縮編碼數(shù)據(jù)相關(guān)信息的結(jié)構(gòu)體,每幀圖像由一到多個(gè)packet包組成 AVFrame *pFrame = NULL;//保存音視頻解碼后的數(shù)據(jù),如狀態(tài)信息、編解碼器信息、宏塊類型表,QP表,運(yùn)動(dòng)矢量表等數(shù)據(jù) struct SwsContext *sws_ctx = NULL;//描述轉(zhuǎn)換器參數(shù)的結(jié)構(gòu)體 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矩形對(duì)象 SDL_Event event;//SDL事件對(duì)象 int i, videoStream;//循環(huán)變量,視頻流類型標(biāo)號(hào) int frameFinished;//解碼操作是否成功標(biāo)識(shí) /*-------------參數(shù)初始化------------*/ if (argc<2) {//檢查輸入?yún)?shù)個(gè)數(shù)是否正確 fprintf(stderr, "Usage: test <file>\n"); exit(1); } // Register all available formats and codecs,注冊(cè)所有ffmpeg支持的多媒體格式及編解碼器 av_register_all(); /*----------------------- * Open video file,打開視頻文件,讀文件頭內(nèi)容,取得文件容器的封裝信息及碼流參數(shù)并存儲(chǔ)在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;//視頻流類型標(biāo)號(hào)初始化為-1 for(i = 0; i < pFormatCtx->nb_streams; i++) {//遍歷文件中包含的所有流媒體類型(視頻流、音頻流、字幕流等) if (pFormatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO) {//若文件中包含有視頻流 videoStream = i;//用視頻流類型的標(biāo)號(hào)修改標(biāo)識(shí),使之不為-1 break; } } 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)號(hào)從pFormatCtx->streams中取得視頻流對(duì)應(yīng)的解碼器上下文 pCodecCtx = pFormatCtx->streams[videoStream]->codec; /*----------------------- * Find the decoder for the video stream,根據(jù)視頻流對(duì)應(yīng)的解碼器上下文查找對(duì)應(yīng)的解碼器,返回對(duì)應(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(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,為解碼后的視頻信息結(jié)構(gòu)體分配空間并完成初始化操作(結(jié)構(gòu)體中的圖像緩存按照下面兩步手動(dòng)安裝) pFrame = av_frame_alloc(); // Initialize SWS context for software scaling,設(shè)置圖像轉(zhuǎn)換像素格式為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已經(jīng)棄用,改為SDL_CreateWindow及SDL_CreateRenderer創(chuàng)建窗口及著色器 #ifndef __DARWIN__ screen = SDL_SetVideoMode(pCodecCtx->width, pCodecCtx->height, 24, 0);//創(chuàng)建SDL窗口,并指定圖像尺寸 #else screen = SDL_SetVideoMode(pCodecCtx->width, pCodecCtx->height, 24, 0);//創(chuàng)建SDL窗口,并指定圖像尺寸 #endif if (!screen) {//檢查SDL窗口是否創(chuàng)建成功 fprintf(stderr, "SDL: could not set video mode - exiting\n"); exit(1); } SDL_WM_SetCaption(argv[1],0);//用輸入文件名設(shè)置SDL窗口標(biāo)題 // Allocate a place to put our YUV image on that screen,創(chuàng)建畫布對(duì)象 bmp = SDL_CreateYUVOverlay(pCodecCtx->width, pCodecCtx->height, SDL_YV12_OVERLAY, screen); /*--------------循環(huán)解碼-------------*/ 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) {//從文件中依次讀取每個(gè)圖像編碼數(shù)據(jù)包,并存儲(chǔ)在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 * 可能無(wú)法通過只解碼一個(gè)packet就獲得一個(gè)完整的視頻幀frame,可能需要讀取多個(gè)packet才行 * avcodec_decode_video2()會(huì)在解碼到完整的一幀時(shí)設(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) { SDL_LockYUVOverlay(bmp);//locks the overlay for direct access to pixel data,原子操作,保護(hù)像素緩沖區(qū)臨界資源 AVFrame pict;//保存轉(zhuǎn)換為AV_PIX_FMT_YUV420P格式的視頻幀 pict.data[0] = bmp->pixels[0];//將轉(zhuǎn)碼后的圖像與畫布的像素緩沖器關(guān)聯(lián) pict.data[1] = bmp->pixels[2]; pict.data[2] = bmp->pixels[1]; pict.linesize[0] = bmp->pitches[0];//將轉(zhuǎn)碼后的圖像掃描行長(zhǎng)度與畫布像素緩沖區(qū)的掃描行長(zhǎng)度相關(guān)聯(lián) 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,將解碼后的圖像轉(zhuǎn)換為AV_PIX_FMT_YUV420P格式,并賦值到pict對(duì)象 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 //設(shè)置矩形顯示區(qū)域 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數(shù)據(jù)結(jié)構(gòu)中編碼數(shù)據(jù)指針 av_packet_unref(&packet); /*------------------------- * 在每次循環(huán)中從SDL后臺(tái)隊(duì)列取事件并填充到SDL_Event對(duì)象中 * SDL的事件系統(tǒng)使得你可以接收用戶的輸入,從而完成一些控制操作 * 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事件對(duì)象 case SDL_QUIT://退出事件 printf("SDL_QUIT\n"); SDL_Quit();//退出操作 exit(0);//結(jié)束進(jìn)程 break; default: break; }//end for switch }//end for while /*--------------參數(shù)撤銷-------------*/ // Free the YUV frame. av_free(pFrame); // Close the codec. avcodec_close(pCodecCtx); // Close the video file. avformat_close_input(&pFormatCtx); return 0; }
到此這篇關(guān)于ffmpeg播放器實(shí)現(xiàn)詳解 - 視頻顯示的文章就介紹到這了,更多相關(guān)ffmpeg播放器實(shí)現(xiàn)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
nasm實(shí)現(xiàn)的用vmware運(yùn)行自做的linux啟動(dòng)盤的引導(dǎo)代碼
這個(gè)小的代碼的編寫和運(yùn)行還是能讓自己對(duì)系統(tǒng)啟動(dòng)有一個(gè)更深的認(rèn)識(shí),不過有個(gè)不懂的就是怎么用ISO鏡像文件啟動(dòng),怎么將引導(dǎo)代碼寫入ISO鏡像文件,依然沒有找到很好的方法解決2013-04-04HTTP中ETag語(yǔ)法及使用實(shí)戰(zhàn)詳解
這篇文章主要為大家介紹了HTTP中ETag語(yǔ)法及使用實(shí)戰(zhàn)詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-03-03淺談測(cè)試驅(qū)動(dòng)開發(fā)TDD之爭(zhēng)
在軟件行業(yè)中,神仙打架的名場(chǎng)面,那就不得不提的是2014年的那場(chǎng)——測(cè)試驅(qū)動(dòng)開發(fā)(TDD)之爭(zhēng)。2021-05-05計(jì)算機(jī)網(wǎng)絡(luò)編程MQTT協(xié)議基礎(chǔ)原理詳解
這篇文章主要為大家介紹了計(jì)算機(jī)編程MQTT協(xié)議的基礎(chǔ)原理詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步早日升職加薪2021-11-11Cordova插件實(shí)現(xiàn)JavaScript與Java的通信的詳細(xì)過程
本文將結(jié)合最常用的華為推送服務(wù)Cordova插件,介紹HMS Core用到的JS-Java消息交互方式,講解在JS側(cè)如何調(diào)用Java側(cè)接口,最終實(shí)現(xiàn)HMS Core能力,感興趣的朋友一起學(xué)習(xí)下吧2021-06-06一文讀懂modbus slave和modbus poll使用說明
modbus poll和modbus slave是一款實(shí)用的modbus開發(fā)和調(diào)試工具,可以非常方便的進(jìn)行modbus調(diào)試,是非常有用的Modbus主機(jī)/從機(jī)模擬程序,這篇文章給大家介紹modbus slave和modbus poll使用說明,感興趣的朋友一起看看吧2021-04-04