1. 程式人生 > >最簡單的基於FFMPEG+SDL的視訊播放器 ver2 (採用SDL2.0

最簡單的基於FFMPEG+SDL的視訊播放器 ver2 (採用SDL2.0

               

=====================================================

最簡單的基於FFmpeg的視訊播放器系列文章列表:

=====================================================

簡介

之前做過一個FFMPEG+SDL的簡單播放器:《100行程式碼實現最簡單的基於FFMPEG+SDL的視訊播放器》。該播放器採用SDL1.2顯示視訊。最近有不少人反映SDL已經升級到2.0版本了,甚至官網的Wiki上都只有SDL2.0的文件了,因此下載了SDL 2.0 並且進行了簡單的研究。隨後對此前的播放器進行了修改,將SDL1.2換成了SDL2.0。

注:《100行程式碼實現最簡單的基於FFMPEG+SDL的視訊播放器》文章中提到的很多知識這裡不再重複記錄。本文重點記錄SDL1.2與SDL2.0的不同。

平臺使用了VC2010,FFmpeg類庫使用了最近的版本,SDL使用2.0版本。

Simplest FFmpeg Player 2

專案主頁

流程圖

FFmpeg解碼一個視訊流程如下圖所示:

SDL2.0顯示YUV的流程圖:

對比SDL1.2的流程圖,發現變化還是很大的。幾乎所有的API都發生了變化。但是函式和變數有一定的對應關係:

SDL_SetVideoMode()————SDL_CreateWindow()

SDL_Surface————SDL_Window

SDL_CreateYUVOverlay()————SDL_CreateTexture()

SDL_Overlay————SDL_Texture

不再一一例舉。

下圖為SDL1.x顯示YUV的流程圖。

簡單解釋各個變數的作用:

SDL_Window就是使用SDL的時候彈出的那個視窗。在SDL1.x版本中,只可以建立一個一個視窗。在SDL2.0版本中,可以建立多個視窗。SDL_Texture用於顯示YUV資料。一個SDL_Texture對應一幀YUV資料。SDL_Renderer用於渲染SDL_Texture至SDL_Window。SDL_Rect用於確定SDL_Texture顯示的位置。注意:一個SDL_Texture可以指定多個不同的SDL_Rect,這樣就可以在SDL_Window不同位置顯示相同的內容(使用SDL_RenderCopy()函式)。它們的關係如下圖所示:

下圖舉了個例子,指定了4個SDL_Rect,可以實現4分屏的顯示。

simplest_ffmpeg_player(標準版)程式碼

最基礎的版本,學習的開始。

/** * 最簡單的基於FFmpeg的視訊播放器 2 * Simplest FFmpeg Player 2 * * 雷霄驊 Lei Xiaohua * [email protected] * 中國傳媒大學/數字電視技術 * Communication University of China / Digital TV Technology * http://blog.csdn.net/leixiaohua1020 * * 第2版使用SDL2.0取代了第一版中的SDL1.2 * Version 2 use SDL 2.0 instead of SDL 1.2 in version 1. * * 本程式實現了視訊檔案的解碼和顯示(支援HEVC,H.264,MPEG2等)。 * 是最簡單的FFmpeg視訊解碼方面的教程。 * 通過學習本例子可以瞭解FFmpeg的解碼流程。 * This software is a simplest video player based on FFmpeg. * Suitable for beginner of FFmpeg. * */#include <stdio.h>#define __STDC_CONSTANT_MACROS#ifdef _WIN32//Windowsextern "C"{#include "libavcodec/avcodec.h"#include "libavformat/avformat.h"#include "libswscale/swscale.h"#include "libavutil/imgutils.h"#include "SDL2/SDL.h"};#else//Linux...#ifdef __cplusplusextern "C"{#endif#include <libavcodec/avcodec.h>#include <libavformat/avformat.h>#include <libswscale/swscale.h>#include <SDL2/SDL.h>#include <libavutil/imgutils.h>#ifdef __cplusplus};#endif#endif//Output YUV420P data as a file #define OUTPUT_YUV420P 0int main(int argc, char* argv[]){ AVFormatContext *pFormatCtx; int    i, videoindex; AVCodecContext *pCodecCtx; AVCodec   *pCodec; AVFrame *pFrame,*pFrameYUV; unsigned char *out_buffer; AVPacket *packet; int y_size; int ret, got_picture; struct SwsContext *img_convert_ctx; char filepath[]="bigbuckbunny_480x272.h265"//SDL--------------------------- int screen_w=0,screen_h=0; SDL_Window *screen;  SDL_Renderer* sdlRenderer; SDL_Texture* sdlTexture; SDL_Rect sdlRect; FILE *fp_yuv; av_register_all(); avformat_network_init(); pFormatCtx = avformat_alloc_context(); if(avformat_open_input(&pFormatCtx,filepath,NULL,NULL)!=0){  printf("Couldn't open input stream.\n");  return -1; } if(avformat_find_stream_info(pFormatCtx,NULL)<0){  printf("Couldn't find stream information.\n");  return -1; } videoindex=-1for(i=0; i<pFormatCtx->nb_streams; i++)   if(pFormatCtx->streams[i]->codec->codec_type==AVMEDIA_TYPE_VIDEO){   videoindex=i;   break;  } if(videoindex==-1){  printf("Didn't find a video stream.\n");  return -1; } pCodecCtx=pFormatCtx->streams[videoindex]->codec; pCodec=avcodec_find_decoder(pCodecCtx->codec_id); if(pCodec==NULL){  printf("Codec not found.\n");  return -1; } if(avcodec_open2(pCodecCtx, pCodec,NULL)<0){  printf("Could not open codec.\n");  return -1; }  pFrame=av_frame_alloc(); pFrameYUV=av_frame_alloc(); out_buffer=(unsigned char *)av_malloc(av_image_get_buffer_size(AV_PIX_FMT_YUV420P,  pCodecCtx->width, pCodecCtx->height,1)); av_image_fill_arrays(pFrameYUV->data, pFrameYUV->linesize,out_buffer,  AV_PIX_FMT_YUV420P,pCodecCtx->width, pCodecCtx->height,1);  packet=(AVPacket *)av_malloc(sizeof(AVPacket)); //Output Info----------------------------- printf("--------------- File Information ----------------\n"); av_dump_format(pFormatCtx,0,filepath,0); printf("-------------------------------------------------\n"); img_convert_ctx = sws_getContext(pCodecCtx->width, pCodecCtx->height, pCodecCtx->pix_fmt,   pCodecCtx->width, pCodecCtx->height, AV_PIX_FMT_YUV420P, SWS_BICUBIC, NULL, NULL, NULL); #if OUTPUT_YUV420P     fp_yuv=fopen("output.yuv","wb+");  #endif    if(SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER)) {    printf( "Could not initialize SDL - %s\n", SDL_GetError());   return -1; }  screen_w = pCodecCtx->width; screen_h = pCodecCtx->height; //SDL 2.0 Support for multiple windows screen = SDL_CreateWindow("Simplest ffmpeg player's Window", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,  screen_w, screen_h,  SDL_WINDOW_OPENGL); if(!screen) {    printf("SDL: could not create window - exiting:%s\n",SDL_GetError());    return -1; } sdlRenderer = SDL_CreateRenderer(screen, -1, 0);   //IYUV: Y + U + V  (3 planes) //YV12: Y + V + U  (3 planes) sdlTexture = SDL_CreateTexture(sdlRenderer, SDL_PIXELFORMAT_IYUV, SDL_TEXTUREACCESS_STREAMING,pCodecCtx->width,pCodecCtx->height);   sdlRect.x=0; sdlRect.y=0; sdlRect.w=screen_w; sdlRect.h=screen_h; //SDL End---------------------- while(av_read_frame(pFormatCtx, packet)>=0){  if(packet->stream_index==videoindex){   ret = avcodec_decode_video2(pCodecCtx, pFrame, &got_picture, packet);   if(ret < 0){    printf("Decode Error.\n");    return -1;   }   if(got_picture){    sws_scale(img_convert_ctx, (const unsigned char* const*)pFrame->data, pFrame->linesize, 0, pCodecCtx->height,      pFrameYUV->data, pFrameYUV->linesize);    #if OUTPUT_YUV420P    y_size=pCodecCtx->width*pCodecCtx->height;      fwrite(pFrameYUV->data[0],1,y_size,fp_yuv);    //Y     fwrite(pFrameYUV->data[1],1,y_size/4,fp_yuv);  //U    fwrite(pFrameYUV->data[2],1,y_size/4,fp_yuv);  //V#endif    //SDL---------------------------#if 0    SDL_UpdateTexture( sdlTexture, NULL, pFrameYUV->data[0], pFrameYUV->linesize[0] );  #else    SDL_UpdateYUVTexture(sdlTexture, &sdlRect,    pFrameYUV->data[0], pFrameYUV->linesize[0],    pFrameYUV->data[1], pFrameYUV->linesize[1],    pFrameYUV->data[2], pFrameYUV->linesize[2]);#endif         SDL_RenderClear( sdlRenderer );      SDL_RenderCopy( sdlRenderer, sdlTexture,  NULL, &sdlRect);      SDL_RenderPresent( sdlRenderer );      //SDL End-----------------------    //Delay 40ms    SDL_Delay(40);   }  }  av_free_packet(packet); } //flush decoder //FIX: Flush Frames remained in Codec while (1) {  ret = avcodec_decode_video2(pCodecCtx, pFrame, &got_picture, packet);  if (ret < 0)   break;  if (!got_picture)   break;  sws_scale(img_convert_ctx, (const unsigned char* const*)pFrame->data, pFrame->linesize, 0, pCodecCtx->height,    pFrameYUV->data, pFrameYUV->linesize);#if OUTPUT_YUV420P  int y_size=pCodecCtx->width*pCodecCtx->height;    fwrite(pFrameYUV->data[0],1,y_size,fp_yuv);    //Y   fwrite(pFrameYUV->data[1],1,y_size/4,fp_yuv);  //U  fwrite(pFrameYUV->data[2],1,y_size/4,fp_yuv);  //V#endif  //SDL---------------------------  SDL_UpdateTexture( sdlTexture, &sdlRect, pFrameYUV->data[0], pFrameYUV->linesize[0] );    SDL_RenderClear( sdlRenderer );    SDL_RenderCopy( sdlRenderer, sdlTexture,  NULL, &sdlRect);    SDL_RenderPresent( sdlRenderer );    //SDL End-----------------------  //Delay 40ms  SDL_Delay(40); } sws_freeContext(img_convert_ctx);#if OUTPUT_YUV420P     fclose(fp_yuv);#endif  SDL_Quit(); av_frame_free(&pFrameYUV); av_frame_free(&pFrame); avcodec_close(pCodecCtx); avformat_close_input(&pFormatCtx); return 0;}

simplest_ffmpeg_player_su(SU版)程式碼

標準版的基礎之上引入了SDL的Event。

效果如下:

(1)SDL彈出的視窗可以移動了(2)畫面顯示是嚴格的40ms一幀

/** * 最簡單的基於FFmpeg的視訊播放器2(SDL升級版) * Simplest FFmpeg Player 2(SDL Update) * * 雷霄驊 Lei Xiaohua * [email protected] * 中國傳媒大學/數字電視技術 * Communication University of China / Digital TV Technology * http://blog.csdn.net/leixiaohua1020 * * 第2版使用SDL2.0取代了第一版中的SDL1.2 * Version 2 use SDL 2.0 instead of SDL 1.2 in version 1. * * 本程式實現了視訊檔案的解碼和顯示(支援HEVC,H.264,MPEG2等)。 * 是最簡單的FFmpeg視訊解碼方面的教程。 * 通過學習本例子可以瞭解FFmpeg的解碼流程。 * 本版本中使用SDL訊息機制重新整理視訊畫面。 * This software is a simplest video player based on FFmpeg. * Suitable for beginner of FFmpeg. * * 備註: * 標準版在播放視訊的時候,畫面顯示使用延時40ms的方式。這麼做有兩個後果: * (1)SDL彈出的視窗無法移動,一直顯示是忙碌狀態 * (2)畫面顯示並不是嚴格的40ms一幀,因為還沒有考慮解碼的時間。 * SU(SDL Update)版在視訊解碼的過程中,不再使用延時40ms的方式,而是建立了 * 一個執行緒,每隔40ms傳送一個自定義的訊息,告知主函式進行解碼顯示。這樣做之後: * (1)SDL彈出的視窗可以移動了 * (2)畫面顯示是嚴格的40ms一幀 * Remark: * Standard Version use's SDL_Delay() to control video's frame rate, it has 2 * disadvantages: * (1)SDL's Screen can't be moved and always "Busy". * (2)Frame rate can't be accurate because it doesn't consider the time consumed  * by avcodec_decode_video2() * SU(SDL Update)Version solved 2 problems above. It create a thread to send SDL  * Event every 40ms to tell the main loop to decode and show video frames. */#include <stdio.h>#define __STDC_CONSTANT_MACROS#ifdef _WIN32//Windowsextern "C"{#include "libavcodec/avcodec.h"#include "libavformat/avformat.h"#include "libswscale/swscale.h"#include "libavutil/imgutils.h"#include "SDL2/SDL.h"};#else//Linux...#ifdef __cplusplusextern "C"{#endif#include <libavcodec/avcodec.h>#include <libavformat/avformat.h>#include <libswscale/swscale.h>#include <libavutil/imgutils.h>#include <SDL2/SDL.h>#ifdef __cplusplus};#endif#endif//Refresh Event#define SFM_REFRESH_EVENT  (SDL_USEREVENT + 1)#define SFM_BREAK_EVENT  (SDL_USEREVENT + 2)int thread_exit=0;int thread_pause=0;int sfp_refresh_thread(void *opaque){ thread_exit=0; thread_pause=0while (!thread_exit) {  if(!thread_pause){   SDL_Event event;   event.type = SFM_REFRESH_EVENT;   SDL_PushEvent(&event);  }  SDL_Delay(40); } thread_exit=0; thread_pause=0//Break SDL_Event event; event.type = SFM_BREAK_EVENT; SDL_PushEvent(&event); return 0;}int main(int argc, char* argv[]){ AVFormatContext *pFormatCtx; int    i, videoindex; AVCodecContext *pCodecCtx; AVCodec   *pCodec; AVFrame *pFrame,*pFrameYUV; unsigned char *out_buffer; AVPacket *packet; int ret, got_picture; //------------SDL---------------- int screen_w,screen_h; SDL_Window *screen;  SDL_Renderer* sdlRenderer; SDL_Texture* sdlTexture; SDL_Rect sdlRect; SDL_Thread *video_tid; SDL_Event event; struct SwsContext *img_convert_ctx; //char filepath[]="bigbuckbunny_480x272.h265"; char filepath[]="Titanic.ts"; av_register_all(); avformat_network_init(); pFormatCtx = avformat_alloc_context(); if(avformat_open_input(&pFormatCtx,filepath,NULL,NULL)!=0){  printf("Couldn't open input stream.\n");  return -1; } if(avformat_find_stream_info(pFormatCtx,NULL)<0){  printf("Couldn't find stream information.\n");  return -1; } videoindex=-1for(i=0; i<pFormatCtx->nb_streams; i++)   if(pFormatCtx->streams[i]->codec->codec_type==AVMEDIA_TYPE_VIDEO){   videoindex=i;   break;  } if(videoindex==-1){  printf("Didn't find a video stream.\n");  return -1; } pCodecCtx=pFormatCtx->streams[videoindex]->codec; pCodec=avcodec_find_decoder(pCodecCtx->codec_id); if(pCodec==NULL){  printf("Codec not found.\n");  return -1; } if(avcodec_open2(pCodecCtx, pCodec,NULL)<0){  printf("Could not open codec.\n");  return -1; } pFrame=av_frame_alloc(); pFrameYUV=av_frame_alloc(); out_buffer=(unsigned char *)av_malloc(av_image_get_buffer_size(AV_PIX_FMT_YUV420P,  pCodecCtx->width, pCodecCtx->height,1)); av_image_fill_arrays(pFrameYUV->data, pFrameYUV->linesize,out_buffer,  AV_PIX_FMT_YUV420P,pCodecCtx->width, pCodecCtx->height,1); //Output Info----------------------------- printf("---------------- File Information ---------------\n"); av_dump_format(pFormatCtx,0,filepath,0); printf("-------------------------------------------------\n");  img_convert_ctx = sws_getContext(pCodecCtx->width, pCodecCtx->height, pCodecCtx->pix_fmt,   pCodecCtx->width, pCodecCtx->height, AV_PIX_FMT_YUV420P, SWS_BICUBIC, NULL, NULL, NULL);   if(SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER)) {    printf( "Could not initialize SDL - %s\n", SDL_GetError());   return -1; }  //SDL 2.0 Support for multiple windows screen_w = pCodecCtx->width; screen_h = pCodecCtx->height; screen = SDL_CreateWindow("Simplest ffmpeg player's Window", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,  screen_w, screen_h,SDL_WINDOW_OPENGL); if(!screen) {    printf("SDL: could not create window - exiting:%s\n",SDL_GetError());    return -1; } sdlRenderer = SDL_CreateRenderer(screen, -1, 0);   //IYUV: Y + U + V  (3 planes) //YV12: Y + V + U  (3 planes) sdlTexture = SDL_CreateTexture(sdlRenderer, SDL_PIXELFORMAT_IYUV, SDL_TEXTUREACCESS_STREAMING,pCodecCtx->width,pCodecCtx->height);   sdlRect.x=0; sdlRect.y=0; sdlRect.w=screen_w; sdlRect.h=screen_h; packet=(AVPacket *)av_malloc(sizeof(AVPacket)); video_tid = SDL_CreateThread(sfp_refresh_thread,NULL,NULL); //------------SDL End------------ //Event Loop  for (;;) {  //Wait  SDL_WaitEvent(&event);  if(event.type==SFM_REFRESH_EVENT){   while(1){    if(av_read_frame(pFormatCtx, packet)<0)     thread_exit=1;    if(packet->stream_index==videoindex)     break;   }   ret = avcodec_decode_video2(pCodecCtx, pFrame, &got_picture, packet);   if(ret < 0){    printf("Decode Error.\n");    return -1;   }   if(got_picture){    sws_scale(img_convert_ctx, (const unsigned char* const*)pFrame->data, pFrame->linesize, 0, pCodecCtx->height, pFrameYUV->data, pFrameYUV->linesize);    //SDL---------------------------    SDL_UpdateTexture( sdlTexture, NULL, pFrameYUV->data[0], pFrameYUV->linesize[0] );      SDL_RenderClear( sdlRenderer );      //SDL_RenderCopy( sdlRenderer, sdlTexture, &sdlRect, &sdlRect );      SDL_RenderCopy( sdlRenderer, sdlTexture, NULL, NULL);      SDL_RenderPresent( sdlRenderer );      //SDL End-----------------------   }   av_free_packet(packet);  }else if(event.type==SDL_KEYDOWN){   //Pause   if(event.key.keysym.sym==SDLK_SPACE)    thread_pause=!thread_pause;  }else if(event.type==SDL_QUIT){   thread_exit=1;  }else if(event.type==SFM_BREAK_EVENT){   break;  } } sws_freeContext(img_convert_ctx); SDL_Quit(); //-------------- av_frame_free(&pFrameYUV); av_frame_free(&pFrame); avcodec_close(pCodecCtx); avformat_close_input(&pFormatCtx); return 0;}

執行結果

程式執行後,會在命令列視窗列印一些視訊資訊,同時會彈出一個視窗播放視訊內容。

下載

CSDN完整工程下載地址:

更新(2014.10.5)==============================

版本升級至2.2。

1.新版本在原版本的基礎上增加了“flush_decoder”功能。當av_read_frame()迴圈退出的時候,實際上解碼器中可能還包含剩餘的幾幀資料。因此需要通過“flush_decoder”將這幾幀資料輸出。“flush_decoder”功能簡而言之即直接呼叫avcodec_decode_video2()獲得AVFrame,而不再向解碼器傳遞AVPacket。參考程式碼如下:

 //FIX: Flush Frames remained in Codec while (1) {  ret = avcodec_decode_video2(pCodecCtx, pFrame, &got_picture, packet);  if (ret < 0)   break;  if (!got_picture)   break;  sws_scale(img_convert_ctx, (const uint8_t* const*)pFrame->data, pFrame->linesize, 0, pCodecCtx->height, pFrameYUV->data, pFrameYUV->linesize);  //處理... }

2.為了更好地適應Linux等其他作業系統,做到可以跨平臺,去除掉了VC特有的一些函式。比如“#include "stdafx.h"”,“_tmain()”等等。

2.2版下載地址:http://download.csdn.net/detail/leixiaohua1020/8002337

更新-2.3(2015.1.03)==============================

2.3版CSDN下載地址:http://download.csdn.net/detail/leixiaohua1020/8322307

更新-2.4(2015.2.13)==============================

這次考慮到了跨平臺的要求,調整了原始碼。經過這次調整之後,原始碼可以在以下平臺編譯通過:

VC++:開啟sln檔案即可編譯,無需配置。

cl.exe:開啟compile_cl.bat即可命令列下使用cl.exe進行編譯,注意可能需要按照VC的安裝路徑調整腳本里面的引數。編譯命令如下。

::VS2010 Environmentcall "D:\Program Files\Microsoft Visual Studio 10.0\VC\vcvarsall.bat"::[email protected] INCLUDE=include;%INCLUDE%::[email protected] LIB=lib;%LIB%::compile and linkcl simplest_ffmpeg_player.cpp /MD /link SDL2.lib SDL2main.lib avcodec.lib ^avformat.lib avutil.lib avdevice.lib avfilter.lib postproc.lib swresample.lib swscale.lib ^/SUBSYSTEM:WINDOWS /OPT:NOREF

MinGW:MinGW命令列下執行compile_mingw.sh即可使用MinGW的g++進行編譯。編譯命令如下。

g++ simplest_ffmpeg_player.cpp -g -o simplest_ffmpeg_player.exe \-I /usr/local/include -L /usr/local/lib \-lmingw32 -lSDL2main -lSDL2 -lavformat -lavcodec -lavutil -lswscale

GCC:Linux或者MacOS命令列下執行compile_gcc.sh即可使用GCC進行編譯。編譯命令如下。

gcc simplest_ffmpeg_player.cpp -g -o simplest_ffmpeg_player.out \-I /usr/local/include -L /usr/local/lib -lSDL2main -lSDL2 -lavformat -lavcodec -lavutil -lswscale
PS:相關的編譯命令已經儲存到了工程資料夾中SourceForge、Github等上面已經更新。更新-2.5(2015.7.17)==============================增加了下列工程:
simplest_ffmpeg_decoder:一個包含了封裝格式處理功能的解碼器。使用了libavcodec和libavformat。simplest_video_play_sdl2:使用SDL2播放YUV的例子。simplest_ffmpeg_helloworld:輸出FFmpeg類庫的資訊。
SourceForge、Github等上面已經更新。