9.基於FFMPEG+SDL2播放視訊(解碼執行緒和播放執行緒分開)
參考資料:
1.雷博部落格
2. An ffmpeg and SDL Tutorial
繼續FFMPEG學習之路。。。
文章目錄
1 綜述
本文主要有兩份程式碼,第一份程式碼就是雷博的《100行實現最簡單的FFMPEG視訊播放器》,採用的方法就是建立一個執行緒,定時發出訊號,而在主執行緒中則等待訊號進行解碼然後顯示,整個流程非常清晰,很容易就能看到之間的關係。第二份程式碼則是參考前面《基於FFMPEG+SDL2播放音訊》以及FFMPEG教程《An ffmpeg and SDL Tutorial》將播放執行緒和解碼執行緒單獨分開,每個執行緒單獨做一件事,這樣可以更精確的控制播放,但是同時代碼複雜程度也同樣增大了。。。
2 程式碼1(基礎程式碼)
此部分程式碼是基於前面文章《基於FFMPEG將video解碼為YUV》以及《基於SDL2播放YUV視訊》基礎上綜合而來,基本思路如下:
初始化複用器和解複用器—>獲取輸入檔案的一些資訊—>查詢解碼器並開啟—>初始化SDL—>建立執行緒定時發出訊號—>主執行緒等待訊號解碼並顯示—>結束,退出。
程式碼如下:
#include <stdio.h> #define __STDC_CONSTANT_MACROS extern "C" { #include "libavcodec/avcodec.h" #include "libavformat/avformat.h" #include "libswscale/swscale.h" #include "sdl/SDL.h" }; //#define debug_msg(fmt, args ...) printf("--->[%s,%d] " fmt "\n\n", __FUNCTION__, __LINE__, ##args) #define REFRESH_EVENT (SDL_USEREVENT + 1) #define BREAK_EVENT (SDL_USEREVENT + 2) int thread_exit=0; int refresh_video(void *opaque) { thread_exit=0; SDL_Event event; while (thread_exit==0) { event.type = REFRESH_EVENT; SDL_PushEvent(&event); SDL_Delay(40); } thread_exit=0; //Break //SDL_Event event; event.type = BREAK_EVENT; SDL_PushEvent(&event); return 0; } int main(int argc, char* argv[]) { AVFormatContext *pFormatCtx; AVCodecContext *pCodecCtx; AVCodec *pCodec; AVPacket *packet; AVFrame *pFrame,*pFrameYUV; int screen_w,screen_h; SDL_Window *screen; SDL_Renderer* sdlRenderer; SDL_Texture* sdlTexture; SDL_Rect sdlRect; SDL_Thread *refresh_thread; SDL_Event event; struct SwsContext *img_convert_ctx; uint8_t *out_buffer; int ret, got_picture; int i = 0; int videoindex = -1; char filepath[]="Titanic.ts"; //char filepath[]="屌絲男士.mov"; av_register_all(); 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; } for(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; } packet=(AVPacket *)av_malloc(sizeof(AVPacket)); pFrame = av_frame_alloc(); pFrameYUV = av_frame_alloc(); out_buffer=(uint8_t *)av_malloc(avpicture_get_size(AV_PIX_FMT_YUV420P, pCodecCtx->width, pCodecCtx->height)); avpicture_fill((AVPicture *)pFrameYUV, out_buffer, AV_PIX_FMT_YUV420P, pCodecCtx->width, pCodecCtx->height); printf("----------->width is %d, height is %d, size is %d,\n", pCodecCtx->width, pCodecCtx->height, avpicture_get_size( pCodecCtx->pix_fmt, pCodecCtx->width, pCodecCtx->height)); //av_dump_format(pFormatCtx,0,filepath,0); 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_TIMER)) { printf( "Could not initialize SDL - %s\n", SDL_GetError()); return -1; } screen_w = pCodecCtx->width; screen_h = pCodecCtx->height; screen = SDL_CreateWindow("FFMPEG_SDL2_Play_Video", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 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); sdlTexture = SDL_CreateTexture(sdlRenderer,SDL_PIXELFORMAT_IYUV, SDL_TEXTUREACCESS_STREAMING,screen_w,screen_h); refresh_thread = SDL_CreateThread(refresh_video,NULL,NULL); sdlRect.x = 0; sdlRect.y = 0; sdlRect.w = screen_w; sdlRect.h = screen_h; while(1) { SDL_WaitEvent(&event); if(event.type==REFRESH_EVENT) { if (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 uint8_t* const*)pFrame->data, pFrame->linesize, 0, pCodecCtx->height, pFrameYUV->data, pFrameYUV->linesize); //fwrite(pFrameYUV->data[0],1, (pCodecCtx->width*pCodecCtx->height),fd_yuv); //fwrite(pFrameYUV->data[1],1, (pCodecCtx->width*pCodecCtx->height)/4,fd_yuv); //fwrite(pFrameYUV->data[2],1, (pCodecCtx->width*pCodecCtx->height)/4,fd_yuv); //fwrite(out_buffer,1, (pCodecCtx->width*pCodecCtx->height)*3/2,fd_yuv); SDL_UpdateTexture( sdlTexture, &sdlRect,out_buffer, pCodecCtx->width); SDL_RenderClear( sdlRenderer ); SDL_RenderCopy( sdlRenderer, sdlTexture, &sdlRect, &sdlRect ); //SDL_RenderCopy( sdlRenderer, sdlTexture, NULL, NULL); SDL_RenderPresent( sdlRenderer ); } } av_free_packet(packet); } else { thread_exit=1; } } else if(event.type==SDL_QUIT) { thread_exit=1; } else if(event.type==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; }
3 程式碼2(播放執行緒和解碼執行緒分開)
將解碼執行緒和播放執行緒分開,大致思路是:播放主執行緒通過事件來重新整理定時器並且從VideoPicture佇列中取出一幀解碼後的資料進行顯示,在解碼執行緒中從檔案中不斷讀取資料幀並放入AVPacket佇列中,然後再建立一個執行緒(解碼執行緒子執行緒)從AVPacket佇列中不斷讀出每一幀資料,並將其放入VideoPicture佇列中。這樣就會形成一個整體迴圈,一個執行緒不斷的將資料解碼並放入佇列中,另外一個執行緒則等待事件來顯示並重新重新整理事件。
3.1 幾個結構體
3.1.1 VideoState
typedef struct _VideoState_
{
AVFormatContext *pFormatCtx;
int videoStreamIndex;
AVStream* pVideoStream;
PacketQueue videoQueue;
VideoPicture pictQueue[VIDEO_PICTURE_QUEUE_SIZE];
int pictSize;
int pictRindex; //取出索引
int pictWindex; //寫入索引
SDL_mutex *pictQueueMutex;
SDL_cond *pictQueueCond;
SDL_Thread *videoThrd;
SDL_Thread *parseThrd;
char fileName[128];
int quit;
struct SwsContext *sws_ctx;
}VideoState;
上面的結構體儲存了視訊的必要資訊,videoStreamIndex 為video在檔案中的索引,videoQueue佇列則是 存放從檔案中讀取到的AVPacket資料,pictQueue則為存放解碼後的YUV資料,pictSize為pictQueue佇列中的YUV資料數量,pictRindex為顯示視訊時候的索引,pictWindex為將YUV資料放到pictQueue佇列中的索引,其他引數也是一些必要的資訊。
3.1.2 VideoPicture
typedef struct _VideoPicture_
{
AVFrame* pFrame;
int frameWidth;
int frameHeight;
int allocated;
}VideoPicture;
此結構體則為存放解碼後的一幀YUV資料,frameWidth和frameHeight則為YUV資料的長和寬,allocated則為一個標誌位,用於判斷是否需要重新申請AVFrame結構體。
3.2 PacketQueue佇列操作
將檔案中讀取到的每一幀資料,放入到PacketQueue佇列中,然後從PacketQueue佇列中取出每一幀資料,這一部分相關的操作主要函式有:
void packet_queue_init(PacketQueue *q);
static int packet_queue_put(PacketQueue *q, AVPacket *pkt) ;
static int packet_queue_get(PacketQueue *q, AVPacket *pkt, int block) ;
這一部分的內容在前面文章《基於FFMPEG+SDL2播放音訊》中已經做了相關的介紹,這裡就不在介紹了。
3.3 播放執行緒
在播放主執行緒中,我們主要是等待重新整理事件進行控制,程式碼如下:
case FF_REFRESH_EVENT:
if (NULL == pSdlScreen)
{
VideoState* pState = (VideoState *)event.user.data1;
if ((pState->pVideoStream) && (pState->pictSize != 0))
{
sdlWidth = pState->pictQueue[pState->pictRindex].frameWidth;
sdlHeight = pState->pictQueue[pState->pictRindex].frameHeight;
printf("------------>SDL width is %d, height is %d\n", sdlWidth, sdlHeight);
pSdlScreen = SDL_CreateWindow("FFMPEG_SDL2_Play_Video", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,sdlWidth, sdlHeight,SDL_WINDOW_OPENGL);
if(!pSdlScreen)
{
printf("SDL: could not create window - exiting:%s\n",SDL_GetError());
return -1;
}
pSdlRenderer = SDL_CreateRenderer(pSdlScreen, -1, 0);
pTexTure = SDL_CreateTexture(pSdlRenderer,SDL_PIXELFORMAT_IYUV, SDL_TEXTUREACCESS_STREAMING,sdlWidth,sdlHeight);
}
else
{
schduleRefresh(pState, 1);
break;
}
}
videoRefreshTimer(event.user.data1,pSdlRenderer,pTexTure);
break;
其中videoRefreshTimer函式便是我們的顯示函式,在此之前,我們根據YUV資料的長和寬建立了SDL顯示相關的視窗、渲染器、紋理。
在介紹videoRefreshTimer之前先介紹一下SDL中的定時器。
3.3.1 SDL中的定時器—SDL_AddTimer
函式原型如下:
SDL_TimerID SDLCALL SDL_AddTimer(Uint32 interval,SDL_TimerCallback callback, void *param);
此函式的作用新增一個新的定時資訊,interval單位為ms,新增定時器interval ms後呼叫回撥函式,callback為回撥函式,param則為呼叫回撥函式時候攜帶的引數。
回撥函式的原型為:
typedef Uint32 (SDLCALL * SDL_TimerCallback) (Uint32 interval, void *param);
注意,此函式的返回值為下次呼叫此回撥函式的間隔,如果此返回值為0,則不再呼叫此函式。
程式碼中的回撥相關函式如下:
//定時重新整理
void schduleRefresh(VideoState *pState, int delay)
{
SDL_AddTimer(delay, SDLRefreshCB, pState);
}
//定時重新整理回撥函式
static Uint32 SDLRefreshCB(Uint32 interval, void *opaque)
{
SDL_Event event;
event.type = FF_REFRESH_EVENT;
event.user.data1 = opaque;
SDL_PushEvent(&event);
return 0; /* 0 means stop timer */
}
可以看到,每次呼叫時候會發出一個重新整理事件(FF_REFRESH_EVENT),通知主執行緒進行解碼。同時看到SDLRefreshCB返回值為0,即只調用一次此定時器便失效,然後不再重新整理了?不,而是在播放執行緒中再次新增一個定時器。
3.3.2 播放執行緒—videoRefreshTimer
先上一下程式碼:
//重新整理定時器並將視訊顯示到SDL上
void videoRefreshTimer(void *userdata,SDL_Renderer* renderer,SDL_Texture* texture)
{
static int exitCnt = 0;
VideoState* pState = (VideoState *)userdata;
if (pState->pVideoStream)
{
if (pState->pictSize == 0) //當前視訊佇列中沒有視訊資料
{
schduleRefresh(pState, 1); // 1ms後再次重新整理
if (++exitCnt > 200)
pState->quit = 1;
}
else
{
schduleRefresh(pState, 40);
videoDisplay(pState, renderer, texture); //顯示
if(++pState->pictRindex >= VIDEO_PICTURE_QUEUE_SIZE)
{
pState->pictRindex = 0;
}
SDL_LockMutex(pState->pictQueueMutex);
pState->pictSize--;
SDL_CondSignal(pState->pictQueueCond);
SDL_UnlockMutex(pState->pictQueueMutex);
exitCnt = 0;
}
}
else //相關準備還沒有做好
{
schduleRefresh(pState, 100);
}
}
播放執行緒的主要作用便是重新整理定時器並且從VideoPicture佇列中取出一幀YUV影象進行顯示。
在前面我們說過,定時器每次只觸發一次,然後在播放執行緒中再次新增一個定時器,這樣做的好處是我們可以精確的控制下次呼叫播放執行緒的時間。
播放執行緒的主要功能便是將一幀YUV影象進行顯示。
3.3.2.1 顯示畫面—videoDisplay
程式碼如下:
//通過SDL顯示畫面
void videoDisplay(VideoState *pState,SDL_Renderer* renderer,SDL_Texture* texture)
{
VideoPicture* pVideoPic = &pState->pictQueue[pState->pictRindex];
SDL_Rect sdlRect;
if (pVideoPic->pFrame)
{
sdlRect.x = 0;
sdlRect.y = 0;
sdlRect.w = pVideoPic->frameWidth;
sdlRect.h = pVideoPic->frameHeight;
SDL_UpdateTexture( texture, &sdlRect,pVideoPic->pFrame->data[0], pVideoPic->pFrame->linesize[0]);
SDL_RenderClear( renderer );
//SDL_RenderCopy( sdlRenderer, sdlTexture, &sdlRect, &sdlRect );
SDL_RenderCopy( renderer, texture, &sdlRect , &sdlRect );
SDL_RenderPresent( renderer );
}
}
在上面可以看到,我們直接從佇列中取出一幀YUV影象,然後直接通過標準SDL顯示流程進行顯示。
3.4 解碼執行緒----decordThread
先上程式碼:
//解碼執行緒
int decordThread(void* arg)
{
VideoState *state = (VideoState *)arg;
AVFormatContext * pFormatCtx = avformat_alloc_context();
AVPacket* packet =(AVPacket *)av_malloc(sizeof(AVPacket));
int i= 0;
int videoIndex = -1;
state->videoStreamIndex = -1;
global_video_state = state;
if(avformat_open_input(&pFormatCtx,state->fileName,NULL,NULL) != 0)
{
printf("Couldn't open input stream.\n");
return -1;
}
state->pFormatCtx = pFormatCtx;
if(avformat_find_stream_info(pFormatCtx,NULL) < 0)
{
printf("Couldn't find stream information.\n");
return -1;
}
for(i = 0; i < pFormatCtx->nb_streams; i++)
{
if(pFormatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO)
{
videoIndex = i;
break;
}
}
if (videoIndex >= 0)
{
streamComponentOpen(state, videoIndex);
}
if (state->videoStreamIndex < 0)
{
printf("open codec error!\n");
goto fail;
}
while(1)
{
if (state->quit) break;
if (state->videoQueue.size > MAX_VIDEOQ_SIZE) //防止佇列過長
{
SDL_Delay(10);
continue;
}
if(av_read_frame(state->pFormatCtx, packet) < 0)
{
if(state->pFormatCtx->pb->error == 0)
{
SDL_Delay(100); /* no error; wait for user input */
continue;
}
else
break;
}
if (packet->stream_index == state->videoStreamIndex)
{
packet_queue_put(&state->videoQueue, packet);
}
else
{
av_free_packet(packet);
}
}
while(state->quit == 0) //等待退出訊號
{
SDL_Delay(100);
}
fail:
SDL_Event event;
event.type = FF_QUIT_EVENT;
event.user.data1 = state;
SDL_PushEvent(&event);
return 0;
}
解碼執行緒主要做了以下幾個工作:
初始化解碼器
從檔案中讀出每幀資料並放入佇列
從佇列中取出沒幀資料並解碼放入VideoPicture佇列中
3.4.1 初始化編解碼器
初始化編解碼器其實包含了FFMPEG一整套的流程,如下:
開啟輸入檔案—>查詢檔案的一些資訊—>查詢視訊index—>查詢解碼器—>開啟解碼器
上面這些流程對應的函式在以前已經說過,這裡不再多說。
在查詢解碼器並開啟解碼器的時候,另外封裝了一個函式streamComponentOpen,這樣做減少了程式碼的複雜性,並且以後音訊解碼器相關的操作也可以複用此函式。
在初始化解碼器後,又建立了一個函式videoThread,此函式的作用便是解碼每一幀資料並放入VideoPicture佇列中,這一點在下面會說明。
3.4.2 讀出每幀資料
從檔案中讀出每幀資料並將其放入PacketQueue佇列中,這部分的操作和前面的文章《基於FFMPEG+SDL2播放音訊》中的操作一致,這裡不再說明。
另外,由於前面文章《基於FFMPEG+SDL2播放音訊》中的音訊本身很小,所以會一直讀取資料將其放入佇列中,但是視訊可能很大,幾百兆很正常,所以如果由於某種原因導致取資料操作異常或者退出,而將資料放入佇列操作缺卻一直在進行,這樣就會導致記憶體申請過大,因此在將資料放入佇列前,可以做一個判斷,當佇列中的資料大於某個值的時候,就會暫停放入佇列操作,等待佇列中的資料被取出,如下:
if (state->videoQueue.size > MAX_VIDEOQ_SIZE) //防止佇列過長
{
SDL_Delay(10);
continue;
}
3.4.3 取出每幀資料並解碼
從PacketQueue佇列中取出每幀資料並解碼,這部分的操作和前面的文章《基於FFMPEG+SDL2播放音訊》中的操作一致,這裡不再說明。
再將解碼後的每一幀資料放入VideoPictue佇列中去的函式queuePicture中,需要首先判斷VideoPictue佇列中的AVFrame是否已經初始化,如果沒有初始化,則需要發出FF_ALLOC_EVENT事件,等待allocPicture函式初始化好VideoPictue。
最後附上在vs2010上建立好的工程(在vs2010上測試ok):