一:簡單的播放器V1(只播放視頻)
(一)回顧
FFmpeg編程(二)FFmpeg中級開發
FFmpeg編程(三)SDL開發
(二)FFmpeg與SDL的簡單結合

#include <stdio.h> #include <SDL.h> #include <libavutil/log.h> #include <libavformat/avformat.h> #include <libavcodec/avcodec.h> #include <libswscale/swscale.h> int main(int argc,char* argv[]){ char* in_file = NULL; int ret = -1; //返回值 int len; //存放讀取的數據大小 int stream_idx; //存放視頻流的索引 int frameFin; //標志packet中數據幀是否讀取完成 AVFormatContext* fmt_cxt = NULL; //格式上下文 AVCodecContext* codec_cxt = NULL; //編解碼器上下文 struct SwsContext* sws_cxt = NULL; //圖像處理上下文 AVCodec* codec = NULL; //編解碼器 AVFrame* frame = NULL; //幀 AVPicture* pict = NULL; //YUV圖像存放 AVPacket packet; //包 SDL_Window* win = NULL; //SDL窗口指針 SDL_Renderer* rend = NULL; //SDL渲染器指針 SDL_Texture* text = NULL; //紋理 SDL_Rect rect; //用於顯示數據的矩陣區域 SDL_Event event; //SDL事件 int w_width = 640; //設置的默認窗口大小,后面會進行調整 int w_height = 480; if(argc<2){ //檢查是否設置了播放的文件名稱 SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,"Usage: command <file>"); return ret; } in_file = argv[1]; if(SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER)){ SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,"Failed to initialize SDL - %s!",SDL_GetError()); return ret; } //------------------------配置所有的FFmpeg信息 av_register_all(); //注冊所有協議 //獲取格式上下文 if(avformat_open_input(&fmt_cxt,in_file,NULL,NULL)<0){ SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,"Failed to open video!"); goto __INIT; } //獲取流的信息,這里換一種方式獲取,不直接使用av_find_best_stream獲取 stream_idx = -1; for(int i=0;i<fmt_cxt->nb_streams;i++){ if(fmt_cxt->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO){ stream_idx = i; break; } } if(stream_idx==-1){ SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,"Failed to get video stream!"); goto __FORMAT; } //打印輸入視頻流的詳細信息 if(avformat_find_stream_info(fmt_cxt,0)<0){ SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,"Failed to get video stream information!"); goto __FORMAT; } av_dump_format(fmt_cxt,stream_idx,in_file,0); //打印信息 //開始獲取解碼器,對視頻流進行解碼獲取YUV數據 //根據輸入流的參數獲取對於的解碼器 codec = avcodec_find_decoder(fmt_cxt->streams[stream_idx]->codec->codec_id); if(codec==NULL){ SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,"Failed to find video decoder!"); goto __FORMAT; } //獲取對應上下文 codec_cxt = avcodec_alloc_context3(codec); if(!codec_cxt){ SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,"Failed to find video decoder context!"); goto __FORMAT; } //開始拷貝輸入流參數到解碼器中 if(avcodec_copy_context(codec_cxt,fmt_cxt->streams[stream_idx]->codec)<0){ SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,"Failed to copy video decoder context!"); goto __CODEC; } //打開編解碼器 if(avcodec_open2(codec_cxt,codec,NULL)<0){ SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,"Failed to opn video decoder!"); goto __CODEC; } frame = av_frame_alloc(); //分配幀 if(!frame){ SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,"Failed to alloc video frame!"); goto __CODEC; } //創建圖片轉換上下文(在上面參數拷貝后面)----------------- //源圖像寬、高、像素格式;目標圖像寬、高、像素格式;以及圖像拉伸使用的算法 sws_cxt = sws_getContext(codec_cxt->width,codec_cxt->height,codec_cxt->pix_fmt, codec_cxt->width,codec_cxt->height,AV_PIX_FMT_YUV420P, //輸出像素格式YUV SWS_BILINEAR,NULL,NULL,NULL); //獲取圖像處理上下文 pict = (AVPicture*)malloc(sizeof(AVPicture)); if(!pict){ SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,"Failed to alloc video picture frame!"); goto __FRAME; } avpicture_alloc(pict, AV_PIX_FMT_YUV420P, codec_cxt->width, codec_cxt->height); //為圖像存放分配空間 //------------------------開始設置SDL數據 w_width = codec_cxt->width; w_height = codec_cxt->height; //創建窗口 win = SDL_CreateWindow("Media Player", SDL_WINDOWPOS_UNDEFINED,SDL_WINDOWPOS_UNDEFINED, //不指定左上位置 w_width,w_height, //設置窗口寬高,與視頻一般或者大些都可以 SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE); //窗口可用於OpenGL上下文 窗口可以調整大小 if(!win){ SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,"Failed to create window to SDL"); goto __PICT; } //創建渲染器 rend = SDL_CreateRenderer(win,-1,0); if(!rend){ SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,"Failed to create renderer to SDL"); goto __WIN; } //創建紋理 text = SDL_CreateTexture(rend,SDL_PIXELFORMAT_IYUV, SDL_TEXTUREACCESS_STREAMING, //視頻流,連續的 w_width,w_height); if(!text){ SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,"Failed to create texture to SDL"); goto __RENDER; } rect.x = 0; //顯示矩陣的左上點 rect.y = 0; //------------------------開始讀取數據,結合SDL與FFmpeg av_init_packet(&packet); //初始化數據包 while(av_read_frame(fmt_cxt,&packet)>=0){ if(packet.stream_index==stream_idx){ //屬於視頻流 len = avcodec_decode_video2(codec_cxt,frame,&frameFin,&packet); //作用是解碼一幀視頻數據。輸入一個壓縮編碼的結構體AVPacket,輸出一個解碼后的結構體AVFrame。 if(len<0){ SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,"Fail to decode video frame"); goto __INIT; } if(frameFin){ //表示讀取完成了一幀,開始進行轉換,解碼數據已經放入了frame中 sws_scale(sws_cxt,(uint8_t const * const *)frame->data,frame->linesize, //當前處理區域的每個通道數據指針,每個通道行字節數 0,codec_cxt->height, //參數int srcSliceY, int srcSliceH,定義在輸入圖像上處理區域,srcSliceY是起始位置,srcSliceH是處理多少行。如果srcSliceY=0,srcSliceH=height,表示一次性處理完整個圖像。 pict->data,pict->linesize); //定義輸出圖像信息(輸出的每個通道數據指針,每個通道行字節數) //可以渲染YUV數據 SDL_UpdateYUVTexture(text,NULL, pict->data[0],pict->linesize[0], pict->data[1],pict->linesize[1], pict->data[2],pict->linesize[2]); //更新矩陣的信息 rect.w = codec_cxt->width; rect.h = codec_cxt->height; SDL_RenderClear(rend); //清空渲染器緩沖區 SDL_RenderCopy(rend,text,NULL,&rect); //拷貝紋理到顯卡,選擇渲染目標的一塊矩形區域作為輸出 SDL_RenderPresent(rend); //將結果顯示到窗口 } } av_packet_unref(&packet); //注意:減少引用不會釋放空間,因為本函數還在引用這個packet結構體; //所以我們在解碼函數中剩余的其他幀數據,還是保留在packet中的,后面根據av_read_frame繼續向內部添加數據 //設置事件輪詢 SDL_PollEvent(&event); switch(event.type){ case SDL_QUIT: goto __QUIT; break; default: break; } } __QUIT: ret = 0; av_packet_unref(&packet); //減少引用,使得自己釋放空間 SDL_DestroyTexture(text); __RENDER: SDL_DestroyRenderer(rend); __WIN: SDL_DestroyWindow(win); __PICT: avpicture_free(pict); free(pict); __FRAME: av_frame_free(&frame); __CODEC: avcodec_close(codec_cxt); __FORMAT: avformat_close_input(&fmt_cxt); __INIT: SDL_Quit(); return ret; }
gcc playerV1.c -o play_1 -I /usr/local/include/ -I /usr/local/include/SDL2/ -L /usr/local/lib/ -lSDL2 -lavformat -lavcodec -lswscale -lavutil
./play_1 gfxm.mp4
二:簡單的播放器V2(播放視頻和音頻,未同步)
(一)回顧鎖與條件變量
SDL中的互斥量和條件變量
(二)基於隊列實現音頻數據的音視頻播放器

#include <stdio.h> #include <assert.h> #include <SDL.h> #include <libavutil/log.h> #include <libavformat/avformat.h> #include <libavcodec/avcodec.h> #include <libswscale/swscale.h> #include <libswresample/swresample.h> #define SDL_AUDIO_BUFFER_SIZE 1024 //對於AAC,一幀數據中單通道獲取 1024個 sample采樣點 #define MAX_AUDIO_FRAME_SIZE 192000 //是指雙通道下,采用48k采樣率,位深為16位,采樣時間為1s //定義隊列 typedef struct PacketQueue{ AVPacketList* first_pkt,* last_pkt; int nb_packets; //數據包個數 int size; //隊列全部數據量大小 SDL_mutex* mutex; //隊列中的鎖 SDL_cond* cond; //條件變量 }PacketQueue; struct SwrContext* adswr_cxt = NULL; //全局音頻重采樣上下文 PacketQueue audioq; //定義全局音頻數據包隊列 int quit = 0; //標志音頻數據是否全部讀取完成 //隊列初始化數據成員 void packet_queue_init(PacketQueue* q){ memset(q,0,sizeof(PacketQueue)); q->mutex = SDL_CreateMutex(); //創建互斥鎖 q->cond = SDL_CreateCond(); //創建條件變量 } //向隊列中添加數據包 int packet_queue_put(PacketQueue* q,AVPacket* pkt){ AVPacketList* pktl; //先對數據包加引用,使得不被提前釋放 /* 原本數據采訪在AVPacket成員---->AVBufferRef *buf中,由av_malloc申請的獨立的buffer(unshared buffer); 使用av_dup_packet后,拷貝了一份數據到data成員中,重新設置pkt->data終於有自己的獨立內存了,不用共享別的AVPacket的內存 https://blog.csdn.net/weixin_34416649/article/details/92075939 https://blog.csdn.net/wangshilin/article/details/8186608 當某個AVPacket結構的數據緩沖區不再被使用時,要需要通過調用 av_free_packet 釋放。av_free_packet調用的是結構體本身的destruct函數 安全起見,如果用戶希望 自由地使用一個FFMPEG內部創建的AVPacket結構,最好調用av_dup_packet進行緩沖區的克隆, 將其轉化為緩沖區能夠被釋放的 AVPacket,以免對緩沖區的不當占用造成異常錯誤。 av_dup_packet會為destruct指針為 av_destruct_packet_nofree的AVPacket新建一個緩沖區,然后將原緩沖區的數據拷貝至新緩沖區,置data的值為新緩沖區 的地址, 同時設destruct指針為av_destruct_packet。 */ if(av_dup_packet(pkt)){ //使得更加安全,保證了數據不被別人共享,但是不能保證不被av_packet_unref刪除,所以我們在主函數中處理音頻數據包的時候,不要調用unref刪除 return -1; //原本數據在buffer中,但是put操作將內容放入了data中,使得packet使用=賦值可以獲取到我們想要的數據起始地址!!!! } pktl = av_malloc(sizeof(AVPacketList)); if(!pktl){ return -1; } pktl->pkt = *pkt; pktl->next = NULL; SDL_LockMutex(q->mutex); //加鎖,開始添加 if(!q->last_pkt){ q->first_pkt = pktl; }else{ q->last_pkt->next = pktl; } q->last_pkt = pktl; q->nb_packets++; q->size += pktl->pkt.size; SDL_CondSignal(q->cond); //重點:對於條件變量,發送信號前會進行解鎖,發送后進行加鎖------------ SDL_UnlockMutex(q->mutex); return 0; } int packet_queue_get(PacketQueue* q,AVPacket* pkt,int block){ AVPacketList* pktl; int ret; SDL_LockMutex(q->mutex); //讀取前加鎖 for(;;){ //如果沒有數據則進行等待(當然不是一直加鎖狀態下死等,后面會使用條件變量,使得先解鎖,等待信號量到達,再加鎖處理) if(quit){ //音頻數據全部讀取完成,退出 ret = -1; break; } pktl = q->first_pkt; if(pktl){ //隊列中存在數據,直接讀取 q->first_pkt = pktl->next; if(!q->first_pkt) //原本只有一個數據時候,尾指針也指向這個數據,所以也要處理,置為空 q->last_pkt = NULL; q->nb_packets--; q->size -= pktl->pkt.size; *pkt = pktl->pkt; av_free(pktl); //只是釋放了AVPakcetList,並沒有釋放內部的packet數據 ret = 1; break; //數據已經讀取到pkt中了 }else if(!block){ //隊列中不存在數據,並且設置了block,不進行阻塞等待條件變量 ret = 0; //這時並沒有返回如何數據,我們對應處理設置靜默音即可 break; }else{ //需要阻塞等待獲取到數據,需要設置條件變量 SDL_CondWait(q->cond,q->mutex); } } SDL_UnlockMutex(q->mutex); return ret; } //對數據包進行解碼返回 int audio_decode_frame(AVCodecContext* adcodec_cxt,uint8_t* buf,int size){ //下面是靜態存儲區全局變量 static AVPacket pkt; static uint8_t* audio_pkt_data = NULL; static int audio_pkt_size = 0; //用來存放從隊列中獲取的packet大小 static AVFrame frame; int len1,data_size=0; for(;;){ while(audio_pkt_size>0){ //pakcet中還有數據沒有處理完成 int got_frame = 0; len1 = avcodec_decode_audio4(adcodec_cxt,&frame,&got_frame,&pkt); //從pkt中解碼packet數據為一幀一幀的frame數據,然后由swr進行重采樣處理 if(len1<0){ //解碼出錯,跳過該packet的數據 audio_pkt_size = 0; break; } audio_pkt_data += len1; audio_pkt_size -= len1; data_size = 0; if(got_frame){ //獲取了一幀數據 //dst_size = av_samples_get_buffer_size(NULL,out_channels,out_nb_samples,sample_fmt,1); //這是每次采樣的數據 = 通道數×每個通道采樣(每幀)×格式 data_size = 2*2*frame.nb_samples; //雙通道,16位深 assert(data_size<=data_size); swr_convert(adswr_cxt,&buf, MAX_AUDIO_FRAME_SIZE*3/2, (const uint8_t**)frame.data, frame.nb_samples); //將重采樣結果放入buf中去 } if(data_size<=0){ //==0,表示我們的packet中的剩余數據還沒有達到獲取一幀的數據大小,繼續去獲取 continue; } return data_size; //已經獲取了一幀數據,返回 } //不在上面while循環,說明需要去獲取隊列中數據 if(pkt.data) //先判斷pkt是否清空 av_free_packet(&pkt); if(quit) return -1; if(packet_queue_get(&audioq,&pkt,1)<0){ return -1; } audio_pkt_data = pkt.data; //賦予初始值--------重點:原本數據在buffer中,但是前面的put操作將內容放入了data中 audio_pkt_size = pkt.size; } } //設置實現音頻聲卡回調函數,用於聲卡主動獲取數據 void audio_callback(void* userdata,uint8_t* stream,int len){ //先獲取編解碼上下文 AVCodecContext* adcodec_cxt = (AVCodecContext*)userdata; int audio_size,len1; //每次讀取的數據包的大小,和,每次可以讀取的數據大小 //設置全局靜態存儲區變量 static uint8_t audio_buf[MAX_AUDIO_FRAME_SIZE*3/2]; static unsigned int audio_buf_size = 0; //音頻數據大小 static unsigned int audio_buf_index = 0; //音頻數據起始位置,當當前數據處理完成,即index到達最后size,就開始解碼下一個音頻數據包 while(len>0){ //聲卡需要的數據長度 if(audio_buf_index>=audio_buf_size){ //開始去獲取下一個數據包 audio_size = audio_decode_frame(adcodec_cxt,audio_buf,sizeof(audio_buf)); if(audio_size<0){ //設置靜默聲音 audio_buf_size = 1024; memset(audio_buf,0,audio_buf_size); }else{ audio_buf_size = audio_size; //設置為獲取的數據包大小 } audio_buf_index = 0; //設置起始位置為首部 } //有數據了,開始讀取數據到聲卡 len1 = audio_buf_size - audio_buf_index; if(len1 > len){ //可以直接讀取 len1 = len; } printf("index=%d,len1=%d,len=%d\n",audio_buf_index,len1,len); memcpy(stream,(uint8_t*)audio_buf+audio_buf_index,len1); len -= len1; //開始改變數據位置 stream += len1; audio_buf_index += len1; } } int main(int argc,char* argv[]){ char* in_file = NULL; int ret = -1; //返回值 int len; //存放讀取的數據大小 int vdstream_idx; //存放視頻流的索引 int adstream_idx; //存放視頻流的索引 int frameFin; //標志packet中數據幀是否讀取完成 AVFormatContext* fmt_cxt = NULL; //格式上下文 struct SwsContext* sws_cxt = NULL; //圖像處理上下文 //----------視頻編解碼器---------- AVCodec* vdcodec = NULL; //編解碼器 AVCodecContext* vdcodec_cxt = NULL; //編解碼器上下文 //----------音頻編解碼器---------- AVCodec* adcodec = NULL; AVCodecContext* adcodec_cxt = NULL; AVFrame* frame = NULL; //幀 AVPicture* pict = NULL; //YUV圖像存放 AVPacket packet; //包 SDL_Window* win = NULL; //SDL窗口指針 SDL_Renderer* rend = NULL; //SDL渲染器指針 SDL_Texture* text = NULL; //紋理 SDL_Rect rect; //用於顯示數據的矩陣區域 int w_width = 640; //設置的默認窗口大小,后面會進行調整 int w_height = 480; SDL_Event event; //SDL事件 SDL_AudioSpec new_spec,old_spec; //用來設置音頻參數,分別表示原始數據和設置的新參數 int64_t in_channel_layout; //音頻的布局 int64_t out_channel_layout; if(argc<2){ //檢查是否設置了播放的文件名稱 SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,"Usage: command <file>"); return ret; } in_file = argv[1]; if(SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER)){ SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,"Failed to initialize SDL - %s!",SDL_GetError()); return ret; } //------------------------配置所有的FFmpeg信息 av_register_all(); //注冊所有協議 //獲取格式上下文 if(avformat_open_input(&fmt_cxt,in_file,NULL,NULL)<0){ SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,"Failed to open video!"); goto __FAIL; } //獲取流的信息,這里換一種方式獲取,不直接使用av_find_best_stream獲取 vdstream_idx = -1; adstream_idx = -1; for(int i=0;i<fmt_cxt->nb_streams;i++){ if(fmt_cxt->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO && vdstream_idx<0){ vdstream_idx = i; } if(fmt_cxt->streams[i]->codec->codec_type == AVMEDIA_TYPE_AUDIO && adstream_idx<0){ adstream_idx = i; } } if(vdstream_idx==-1 || adstream_idx==-1){ SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,"Failed to get video stream or audio stream!"); goto __FAIL; } //打印輸入視頻流的詳細信息 if(avformat_find_stream_info(fmt_cxt,0)<0){ SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,"Failed to get video stream information!"); goto __FAIL; } av_dump_format(fmt_cxt,vdstream_idx,in_file,0); //打印信息 av_dump_format(fmt_cxt,adstream_idx,in_file,0); //打印信息 //-----------------處理音頻編解碼器--------------- adcodec = avcodec_find_decoder(fmt_cxt->streams[adstream_idx]->codec->codec_id); if(adcodec==NULL){ SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,"Failed to find audio decoder!"); goto __FAIL; } adcodec_cxt = avcodec_alloc_context3(adcodec); if(!adcodec_cxt){ SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,"Failed to find adio decoder context!"); goto __FAIL; } if(avcodec_copy_context(adcodec_cxt,fmt_cxt->streams[adstream_idx]->codec)<0){ SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,"Failed to copy audio decoder context!"); goto __FAIL; } //打開編解碼器 if(avcodec_open2(adcodec_cxt,adcodec,NULL)<0){ SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,"Failed to opn audio decoder!"); goto __FAIL; } //-----------------處理音頻數據--------------- new_spec.freq = adcodec_cxt->sample_rate; new_spec.format = AUDIO_S16SYS; new_spec.channels = adcodec_cxt->channels; new_spec.silence = 0; new_spec.samples = SDL_AUDIO_BUFFER_SIZE; new_spec.callback = audio_callback; new_spec.userdata = adcodec_cxt; if(SDL_OpenAudio(&new_spec,&old_spec)<0){ SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,"Failed to opn audio device - %s!",SDL_GetError()); goto __FAIL; } packet_queue_init(&audioq); //初始化音頻數據包隊列 in_channel_layout = av_get_default_channel_layout(adcodec_cxt->channels); //獲取輸入流的通道布局 out_channel_layout = in_channel_layout; //輸出流通道布局不變 printf("in layout:%ld, out layout:%ld\n",in_channel_layout,out_channel_layout); adswr_cxt = swr_alloc(); //分配音頻重采樣上下文 if(!adswr_cxt){ SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,"Failed to alloc audio resample context!"); goto __FAIL; } //----------創建重采樣的上下文 swr_alloc_set_opts(adswr_cxt, //設置已經創建好的上下文,如果沒有,則為NULL out_channel_layout, //設置輸出目標的通道布局(雙聲道,立體聲,...,方位增寬) AV_SAMPLE_FMT_S16, //設置輸出目標的采樣格式,與上面的AUDIO_S16SYS保持一致 adcodec_cxt->sample_rate, //設置輸出目標的采樣率 in_channel_layout, //輸入數據的通道布局,是雙聲道 adcodec_cxt->sample_fmt, //輸入數據的采樣格式為s16le adcodec_cxt->sample_rate, //輸入的采樣率 0, //日志級別 NULL); //日志上下文 swr_init(adswr_cxt); SDL_PauseAudio(0); //開始播放音頻 //-----------------處理視頻編解碼器--------------- //開始獲取解碼器,對視頻流進行解碼獲取YUV數據 //根據輸入流的參數獲取對於的解碼器 vdcodec = avcodec_find_decoder(fmt_cxt->streams[vdstream_idx]->codec->codec_id); if(vdcodec==NULL){ SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,"Failed to find video decoder!"); goto __FAIL; } //獲取對應上下文 vdcodec_cxt = avcodec_alloc_context3(vdcodec); if(!vdcodec_cxt){ SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,"Failed to find video decoder context!"); goto __FAIL; } //開始拷貝輸入流參數到解碼器中 if(avcodec_copy_context(vdcodec_cxt,fmt_cxt->streams[vdstream_idx]->codec)<0){ SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,"Failed to copy video decoder context!"); goto __FAIL; } //打開編解碼器 if(avcodec_open2(vdcodec_cxt,vdcodec,NULL)<0){ SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,"Failed to opn video decoder!"); goto __FAIL; } //-----------------處理視頻數據--------------- frame = av_frame_alloc(); //分配幀 if(!frame){ SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,"Failed to alloc video frame!"); goto __FAIL; } //創建圖片轉換上下文(在上面參數拷貝后面)----------------- //源圖像寬、高、像素格式;目標圖像寬、高、像素格式;以及圖像拉伸使用的算法 sws_cxt = sws_getContext(vdcodec_cxt->width,vdcodec_cxt->height,vdcodec_cxt->pix_fmt, vdcodec_cxt->width,vdcodec_cxt->height,AV_PIX_FMT_YUV420P, //輸出像素格式YUV SWS_BILINEAR,NULL,NULL,NULL); //獲取圖像處理上下文 pict = (AVPicture*)malloc(sizeof(AVPicture)); if(!pict){ SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,"Failed to alloc video picture frame!"); goto __FAIL; } avpicture_alloc(pict, AV_PIX_FMT_YUV420P, vdcodec_cxt->width, vdcodec_cxt->height); //為圖像存放分配空間 //------------------------開始設置SDL數據 w_width = vdcodec_cxt->width; w_height = vdcodec_cxt->height; //創建窗口 win = SDL_CreateWindow("Media Player", SDL_WINDOWPOS_UNDEFINED,SDL_WINDOWPOS_UNDEFINED, //不指定左上位置 w_width,w_height, //設置窗口寬高,與視頻一般或者大些都可以 SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE); //窗口可用於OpenGL上下文 窗口可以調整大小 if(!win){ SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,"Failed to create window to SDL"); goto __FAIL; } //創建渲染器 rend = SDL_CreateRenderer(win,-1,0); if(!rend){ SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,"Failed to create renderer to SDL"); goto __FAIL; } //創建紋理 text = SDL_CreateTexture(rend,SDL_PIXELFORMAT_IYUV, SDL_TEXTUREACCESS_STREAMING, //視頻流,連續的 w_width,w_height); if(!text){ SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,"Failed to create texture to SDL"); goto __FAIL; } rect.x = 0; //顯示矩陣的左上點 rect.y = 0; //------------------------開始讀取數據,結合SDL與FFmpeg av_init_packet(&packet); //初始化數據包 while(av_read_frame(fmt_cxt,&packet)>=0){ if(packet.stream_index == vdstream_idx){ //屬於視頻流 len = avcodec_decode_video2(vdcodec_cxt,frame,&frameFin,&packet); //作用是解碼一幀視頻數據。輸入一個壓縮編碼的結構體AVPacket,輸出一個解碼后的結構體AVFrame。 if(len<0){ SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,"Fail to decode video frame"); goto __FAIL; } if(frameFin){ //表示讀取完成了一幀,開始進行轉換,解碼數據已經放入了frame中 sws_scale(sws_cxt,(uint8_t const * const *)frame->data,frame->linesize, //當前處理區域的每個通道數據指針,每個通道行字節數 0,vdcodec_cxt->height, //參數int srcSliceY, int srcSliceH,定義在輸入圖像上處理區域,srcSliceY是起始位置,srcSliceH是處理多少行。如果srcSliceY=0,srcSliceH=height,表示一次性處理完整個圖像。 pict->data,pict->linesize); //定義輸出圖像信息(輸出的每個通道數據指針,每個通道行字節數) //可以渲染YUV數據 SDL_UpdateYUVTexture(text,NULL, pict->data[0],pict->linesize[0], pict->data[1],pict->linesize[1], pict->data[2],pict->linesize[2]); //更新矩陣的信息 rect.w = vdcodec_cxt->width; rect.h = vdcodec_cxt->height; SDL_RenderClear(rend); //清空渲染器緩沖區 SDL_RenderCopy(rend,text,NULL,&rect); //拷貝紋理到顯卡,選擇渲染目標的一塊矩形區域作為輸出 SDL_RenderPresent(rend); //將結果顯示到窗口 } av_packet_unref(&packet); //注意:減少引用不會釋放空間,因為本函數還在引用這個packet結構體; //所以我們在解碼函數中剩余的其他幀數據,還是保留在packet中的,后面根據av_read_frame繼續向內部添加數據 }else if(packet.stream_index == adstream_idx){ //處理音頻流 packet_queue_put(&audioq,&packet); //重點:對於音頻數據包我們是暫存放在隊列中,所以不能馬上刪除!!!! }else{ av_packet_unref(&packet); //注意:減少引用不會釋放空間,因為本函數還在引用這個packet結構體; //所以我們在解碼函數中剩余的其他幀數據,還是保留在packet中的,后面根據av_read_frame繼續向內部添加數據 } //設置事件輪詢 SDL_PollEvent(&event); switch(event.type){ case SDL_QUIT: goto __QUIT; break; default: break; } } __QUIT: ret = 0; __FAIL: av_packet_unref(&packet); //減少引用,使得自己釋放空間 if(frame){ av_frame_free(&frame); } if(text){ SDL_DestroyTexture(text); } if(rend){ SDL_DestroyRenderer(rend); } if(win){ SDL_DestroyWindow(win); } if(pict){ avpicture_free(pict); free(pict); } if(adcodec_cxt){ avcodec_close(adcodec_cxt); } if(vdcodec_cxt){ avcodec_close(vdcodec_cxt); } if(fmt_cxt){ avformat_close_input(&fmt_cxt); } SDL_Quit(); return ret; }
gcc playerV2.c -o play_2 -I /usr/local/include/ -I /usr/local/include/SDL2/ -L /usr/local/lib/ -lSDL2 -lavformat -lavcodec -lswscale -lavutil -lswresample
三:簡單的播放器V3(線程實現播放視頻和音頻,未同步)
(一)線程模型
1.主線程:事件處理、視頻渲染等簡單事件,不做過多事情,使得響應更快。將其它復雜的邏輯放入子線程當中
2.解復用線程:對文件數據進行解復用,將視頻流放入視頻流隊列,音頻流放入音頻流隊列當中,然后創建子線程進行處理(解碼處理占CPU)
3.視頻解碼線程:解碼視頻流隊列中的數據,將結果放入解碼后的視頻隊列中去
4.SDL創建音頻渲染線程,去音頻流隊列中取數據,進行解碼,輸出到聲卡
5.視頻渲染,由主線程實現,通過從解碼后的視頻流隊列中取得數據,進行渲染
以上使得音視頻的渲染都是通過回調的方式處理,使得更好的進行音視頻同步處理!!!
(二)基於多線程實現音頻數據的音視頻播放器

#include <assert.h> #include <stdio.h> #include <SDL.h> #include <libavutil/log.h> #include <libavformat/avformat.h> #include <libavcodec/avcodec.h> #include <libswscale/swscale.h> #include <libswresample/swresample.h> #define SDL_AUDIO_BUFFER_SIZE 1024 //對於AAC,一幀數據中單通道獲取 1024個 sample采樣點 #define MAX_AUDIO_FRAME_SIZE 192000 //是指雙通道下,采用48k采樣率,位深為16位,采樣時間為1s #define MAX_AUDIOQ_SIZE (5*6*1024) //音頻隊列中數據字節數 界限,在MAX_AUDIO_FRAME_SIZE內部,即可。 采用最小8k采樣率,2通道,16位深 = 32k,這里使用(5*6*1024)小於這個最小值即可 #define MAX_VEDIOQ_SIZE (5*256*1024) //視頻隊列中數據字節數 界限,需要處理1280x1024的圖像,這里使用這個數值作為界限,實際上對於yuv420p數據,可以在×1.5,但是沒必要的。 #define FF_REFRESH_EVENT (SDL_USEREVENT) //使用自定義事件 #define FF_QUIT_EVENT (SDL_USEREVENT + 1) //使用自定義事件 #define VIDEO_PICTURE_QUEUE_SIZE 1 //定義視頻圖像裁剪后幀的最大個數 typedef struct PacketQueue { AVPacketList* first_pkt,* last_pkt; int nb_packets; int size; SDL_mutex* mutex; SDL_cond* cond; }PacketQueue; typedef struct VideoPicture { AVPicture* pict; //存放yuv數據 int width,height; int allocated; //用來標記圖像是否分配了空間 }VideoPicture; typedef struct VideoState { /*------------多媒體文件的基礎信息-----------*/ char filename[1024]; //文件名 AVFormatContext* pFormatCtx; //文件格式上下文 int videoStream,audioStream; //視頻流、音頻流索引 /*----------------音頻信息----------------*/ AVStream *audio_st; //流 AVCodecContext *audio_ctx; //解碼上下文 struct SwrContext* audio_swr_cxt; //音頻重采樣上下文 uint8_t audio_buf[(MAX_AUDIO_FRAME_SIZE*3)/2]; //重采樣后的數據最后存放位置 unsigned int audio_buf_size; //寫入的數據大小 unsigned int audio_buf_index; //讀取到的數據的索引 PacketQueue audioq; //音頻隊列 AVFrame audio_frame; //音頻幀(解碼后的)--- 從隊列中取出來的packet,進行解碼出來的一幀數據 AVPacket audio_pkt; //音頻包(隊列中讀取的),這里是提前初始化了一個包,避免了后面每次獲取數據時都設置一個,類似於全局變量了。或者我們像前一個版本那樣使用靜態局部變量也可以的 uint8_t *audio_pkt_data; //指向packet中的data數據要讀取的位置 int audio_pkt_size; //讀取的packet中的數據大小 /*----------------視頻信息----------------*/ AVStream *video_st; //流 AVCodecContext *video_ctx; //圖像解碼上下文 struct SwsContext *sws_ctx; //圖像裁剪上下文 PacketQueue videoq; //隊列中保存着所有讀取的關於視頻的packet VideoPicture pictq[VIDEO_PICTURE_QUEUE_SIZE]; //最后要存放的數據位置---隊列中取出來的一個packet進行裁剪處理后的數據 int pictq_size; //pictq_size表示pictq數組的大小, int pictq_rindex; //索引pictq_rindex表示要訪問的數據在pictq數組中的位置 int pictq_windex; //索引pictq_windex表示要插入的數據在pictq數組中的位置 /*----------------線程信息----------------*/ //只有視頻是我們要處理的數據,所以包含獨立的鎖,音頻是被聲卡主動讀取的 SDL_mutex *pictq_mutex; //視頻數據的鎖,因為數據可以同時讀取和寫入 SDL_cond *pictq_cond; //條件變量 SDL_Thread *parse_tid; //解復用線程id SDL_Thread *video_tid; //視頻解碼線程id /*--------------全局退出信號---------------*/ int quit; //0正常,1退出 }VideoState; /*-------------------SDL相關-----------------*/ SDL_mutex *texture_mutex; //是對渲染器的鎖,因為主線程和視頻解碼線程都會用到 SDL_Window *win; //窗口指針 SDL_Renderer *renderer; //渲染器指針 SDL_Texture *texture; //紋理指針 /*--------------只有一個解復用線程,只在這里線程中被修改!!!---------------*/ VideoState *global_video_state; /*-----------------隊列操作方法---------------*/ void packet_queue_init(PacketQueue *q){ memset(q,0,sizeof(PacketQueue)); q->mutex = SDL_CreateMutex(); q->cond = SDL_CreateCond(); } int packet_queue_get(PacketQueue *q,AVPacket *pkt,int block){ AVPacketList *pktl; int ret; SDL_LockMutex(q->mutex); for(;;){ if(global_video_state->quit){ //對於每個死循環,我們都要進行退出信號判斷 fprintf(stderr,"quit from queue_get!\n"); ret = -1; break; } pktl = q->first_pkt; if(pktl){ q->first_pkt = pktl->next; if(!q->first_pkt) q->last_pkt = NULL; q->nb_packets--; q->size -= pktl->pkt.size; *pkt = pktl->pkt; av_free(pktl); ret = 1; break; }else if(!block){ ret = 0; //表示沒有獲取到數據,接着進入輪詢 break; }else{ //類似阻塞等待 fprintf(stderr,"queue is empty, so wait a moment and wait a cond signal!\n"); SDL_CondWait(q->cond,q->mutex); } } SDL_UnlockMutex(q->mutex); return ret; } int packet_queue_put(PacketQueue *q,AVPacket *pkt){ AVPacketList* pktl; if(av_dup_packet(pkt) < 0){ return -1; } pktl = av_malloc(sizeof(AVPacketList)); if(!pktl){ return -1; } pktl->pkt = *pkt; //data賦值引用即可 pktl->next = NULL; SDL_LockMutex(q->mutex); if(!q->last_pkt) q->first_pkt = pktl; else q->last_pkt->next = pktl; q->last_pkt = pktl; q->nb_packets++; q->size += pktl->pkt.size; SDL_CondSignal(q->cond); //發送信號給get消費者 SDL_UnlockMutex(q->mutex); return 0; } /*-----------------音頻操作方法---------------*/ int audio_decode_frame(VideoState *is, uint8_t *audio_buf, int buf_size){ int len1, data_size = 0; AVPacket *pkt = &is->audio_pkt; for(;;){ while(is->audio_pkt_size > 0){ //還有數據在packet中 int got_frame = 0; len1 = avcodec_decode_audio4(is->audio_ctx,&is->audio_frame,&got_frame,pkt); //讀取pkt中的數據到frame if(len1 < 0){ printf("Failed to decode audio...\n"); is->audio_pkt_size = 0; break; } data_size = 0; if(got_frame){ //解碼幀成功 data_size = 2*2*is->audio_frame.nb_samples; //雙通道,16位深 assert(data_size <= buf_size); //開始進行重采樣 swr_convert(is->audio_swr_cxt, &audio_buf, //輸出空間 MAX_AUDIO_FRAME_SIZE*3/2, //空間大小 (const uint8_t **)is->audio_frame.data, //幀數據位置 is->audio_frame.nb_samples); //幀通道采樣率 //處理數據 is->audio_pkt_size -= len1; is->audio_pkt_data += len1; } if(data_size == 0) //沒有讀取到數據 continue; return data_size; //讀取到了數據 } //如果沒有進入上面的while循環,則表示開始沒有數據存在pkt中,我們需要去隊列中獲取 if(pkt->data) av_free_packet(pkt); if(is->quit){ //對於循環,一定退出判斷 printf("Will quit program...\n"); return -1; } if(packet_queue_get(&is->audioq,pkt,0) <= 0){ return -1; } is->audio_pkt_data = pkt->data; is->audio_pkt_size = pkt->size; } } //聲卡回調讀取數據 void audio_callback(void *userdata,uint8_t *stream,int len){ VideoState *is = (VideoState *)userdata; int len1,audio_size; SDL_memset(stream,0,len); //初始化緩沖區 while(len > 0){ //這個循環不用擔心,因為會產生靜默聲音,最后跳出while if(is->audio_buf_index >= is->audio_buf_size){ //原有的數據已經讀完了,下面開始獲取新的數據 audio_size = audio_decode_frame(is,is->audio_buf,sizeof(is->audio_buf)); if(audio_size < 0){ //讀取出錯 設置靜默聲音,當其中audio_decode_frame去隊列取數據block為0時,會進入 is->audio_buf_size = 1024*2*2; printf("--------------no audio packet-------------------\n"); memset(is->audio_buf,0,is->audio_buf_size); }else{ is->audio_buf_size = audio_size; } is->audio_buf_index = 0; //新讀取的數據的起始索引位置 } len1 = is->audio_buf_size - is->audio_buf_index; //數據有效大小 if(len1 > len) len1 = len; //memcpy(stream,(uint8_t*)is->audio_buf+is->audio_buf_index,len1); //使用這個可以代替SDL_memset和 SDL_MixAudio(stream,(uint8_t*)is->audio_buf+is->audio_buf_index,len1,SDL_MIX_MAXVOLUME); len -= len1; stream += len1; is->audio_buf_index += len1; } } /*-----------------視頻操作方法---------------*/ //設置定時器 static uint32_t sdl_refresh_timer_cb(uint32_t interval, void *dat){ SDL_Event event; event.type = FF_REFRESH_EVENT; event.user.data1 = dat; SDL_PushEvent(&event); return 0; //0意味着停止計時 } //定時刷新 static void schedule_refresh(VideoState *is,int delay){ SDL_AddTimer(delay,sdl_refresh_timer_cb,is); } //顯示圖像 void video_display(VideoState *is){ SDL_Rect rect; VideoPicture *vp; vp = &is->pictq[is->pictq_rindex]; //獲取要顯示的數據 if(vp->pict){ //有數據,開始顯示 SDL_UpdateYUVTexture(texture,NULL, vp->pict->data[0],vp->pict->linesize[0], vp->pict->data[1],vp->pict->linesize[1], vp->pict->data[2],vp->pict->linesize[2]); rect.x = 0; rect.y = 0; rect.w = is->video_ctx->width; rect.h = is->video_ctx->height; SDL_LockMutex(texture_mutex); //分配數據和渲染數據加鎖處理 SDL_RenderClear(renderer); SDL_RenderCopy(renderer,texture,NULL,&rect); SDL_RenderPresent(renderer); SDL_UnlockMutex(texture_mutex); } } //開始根據事件處理顯示圖像 void video_refresh_timer(void *userdata){ VideoState *is = (VideoState*)userdata; VideoPicture *vp; if(is->video_st){ //如果視頻流存在,則進行處理 if(is->pictq_size == 0){ //隊列中沒有數據,則反復去輪詢是否有數據 schedule_refresh(is,1); //輪詢時間為1 }else{ vp = &is->pictq[is->pictq_rindex]; //獲取要顯示的數據 schedule_refresh(is,40); //顯示圖像之間間隔40ms video_display(is); //顯示圖像 if(++is->pictq_rindex == VIDEO_PICTURE_QUEUE_SIZE){ is->pictq_rindex = 0; //修改要顯示的索引位置 } SDL_LockMutex(is->pictq_mutex); is->pictq_size--; //顯示后,去通知可以解析下一次圖像 SDL_CondSignal(is->pictq_cond); //通知queue_picture去分配空間 SDL_UnlockMutex(is->pictq_mutex); } }else{ schedule_refresh(is,100); //如果不存在視頻流,就再延長等待一段時間 } } //為pictq數組中新插入的數據分配空間 void alloc_picture(void *userdata){ VideoState *is = (VideoState *)userdata; VideoPicture *vp = &is->pictq[is->pictq_windex]; if(vp->pict){ avpicture_free(vp->pict); //釋放yuv空間 free(vp->pict); //釋放pict } SDL_LockMutex(texture_mutex); //因為分配和渲染都要使用到這個數據,所以要進行加鎖 vp->pict = (AVPicture*)malloc(sizeof(AVPicture)); //分配空間 if(vp->pict){ avpicture_alloc(vp->pict, AV_PIX_FMT_YUV420P, is->video_ctx->width, is->video_ctx->height); } SDL_UnlockMutex(texture_mutex); vp->width = is->video_ctx->width; vp->height = is->video_ctx->height; vp->allocated = 1; } //分配空間,將視頻幀放入pictq數組中去 int queue_picture(VideoState *is,AVFrame *pFrame){ VideoPicture *vp; int dst_pix_fmt; AVPicture pict; //yuv數據 SDL_LockMutex(is->pictq_mutex); //------------??應該是if while(is->pictq_size >= VIDEO_PICTURE_QUEUE_SIZE && !is->quit){ //表示隊列的大小已經到達最大界限(使用==判斷也可以) SDL_CondWait(is->pictq_cond,is->pictq_mutex); //現在數組中已經放不下裁剪后的圖像了,只有主線程消費了圖像以后,發生信號過來,才進行解碼下一幀圖像 } SDL_UnlockMutex(is->pictq_mutex); //上面的條件等待完成,表示前一個圖像顯示完成,現在需要裁剪新的圖像 if(is->quit){ printf("quit from queue picture...\n"); return -1; } vp = &is->pictq[is->pictq_windex]; //這個是要插入的位置數據 if(!vp->pict || vp->width != is->video_ctx->width || vp->height != is->video_ctx->height){ //開始分配空間 vp->allocated = 0; //還沒有開始分配空間 alloc_picture(is); //開始分配空間,內部會修改標識符 if(is->quit){ printf("quit from queue picture 2...\n"); return -1; } } if(vp->pict){ //分配成功,開始進行裁剪處理,處理后的數據放入這里面 sws_scale(is->sws_ctx,(uint8_t const* const*)pFrame->data, pFrame->linesize,0,is->video_ctx->height, vp->pict->data,vp->pict->linesize); if(++is->pictq_windex == VIDEO_PICTURE_QUEUE_SIZE){ is->pictq_windex = 0; } SDL_LockMutex(is->pictq_mutex); is->pictq_size++; //多線程會操作這個 SDL_UnlockMutex(is->pictq_mutex); } return 0; } //解碼視頻幀 int video_thread(void *arg){ VideoState *is = (VideoState *)arg; AVPacket packet,*pkt = &packet; int frameFinished; AVFrame *pFrame; pFrame = av_frame_alloc(); for(;;){ if(packet_queue_get(&is->videoq,pkt,1) < 0){ break; //沒有數據,退出 } avcodec_decode_video2(is->video_ctx,pFrame,&frameFinished,pkt); //解碼數據幀 if(frameFinished){ //去裁剪處理 if(queue_picture(is,pFrame) < 0){ break; } } av_free_packet(pkt); } av_frame_free(&pFrame); return 0; } /*-----------------解復用線程方法,主要用於初始化---------------*/ //初始化音視頻流的相關數據,(只被音頻、視頻流各自調用一次) int stream_component_open(VideoState *is, int stream_index){ int64_t in_channel_layout,out_channel_layout; AVFormatContext *pFormatCtx = is->pFormatCtx; AVCodecContext *codecCtx = NULL; AVCodec *codec = NULL; SDL_AudioSpec new_spec,old_spec; codec = avcodec_find_decoder(pFormatCtx->streams[stream_index]->codec->codec_id); //獲取解碼器信息 if(!codec){ printf("UnSupport codec!\n"); return -1; } codecCtx = avcodec_alloc_context3(codec); if(!codecCtx){ printf("Failed to alloc codec context\n"); return -1; } if(avcodec_copy_context(codecCtx,pFormatCtx->streams[stream_index]->codec) < 0){ printf("Failed to copy codec context\n"); return -1; } if(codecCtx->codec_type == AVMEDIA_TYPE_AUDIO){ //設置音頻信息 new_spec.freq = codecCtx->sample_rate; new_spec.format = AUDIO_S16SYS; new_spec.channels = codecCtx->channels; new_spec.silence = 0; new_spec.samples = SDL_AUDIO_BUFFER_SIZE; //AAC單通道采樣率就是1024 new_spec.callback = audio_callback; new_spec.userdata = is; if(SDL_OpenAudio(&new_spec,&old_spec)<0){ printf("SDL_OpenAudio: %s\n", SDL_GetError()); return -1; } } if(avcodec_open2(codecCtx,codec,NULL) < 0){ printf("Failed to open codec\n"); return -1; } switch(codecCtx->codec_type){ case AVMEDIA_TYPE_AUDIO: //處理音頻 is->audioStream = stream_index; is->audio_st = pFormatCtx->streams[stream_index]; is->audio_ctx = codecCtx; is->audio_buf_size = 0; is->audio_buf_index = 0; memset(&is->audio_pkt,0,sizeof(is->audio_pkt)); //取代了前一個版本的靜態局部變量而已,這里不使用memset也可以 packet_queue_init(&is->audioq); SDL_PauseAudio(0); in_channel_layout = av_get_default_channel_layout(is->audio_ctx->channels); out_channel_layout = in_channel_layout; is->audio_swr_cxt = swr_alloc(); //開始分配重采樣的數據空間 swr_alloc_set_opts(is->audio_swr_cxt, out_channel_layout, AV_SAMPLE_FMT_S16, is->audio_ctx->sample_rate, in_channel_layout, is->audio_ctx->sample_fmt, is->audio_ctx->sample_rate, 0, NULL); swr_init(is->audio_swr_cxt); break; case AVMEDIA_TYPE_VIDEO: //處理視頻 is->videoStream = stream_index; is->video_st = pFormatCtx->streams[stream_index]; is->video_ctx = codecCtx; packet_queue_init(&is->videoq); is->sws_ctx = sws_getContext(is->video_ctx->width, is->video_ctx->height, is->video_ctx->pix_fmt, is->video_ctx->width, is->video_ctx->height, AV_PIX_FMT_YUV420P, SWS_BILINEAR, NULL,NULL,NULL); is->video_tid = SDL_CreateThread(video_thread,"video_thread",is); //開始視頻解碼線程 break; default: break; } return 0; } //解復用,會在后面一直讀取視頻數據包 int decode_thread(void *arg){ VideoState *is = (VideoState *)arg; AVFormatContext *pFormatCtx = NULL; AVPacket packet, *ptk = &packet; SDL_Event event; int i,ret=0; int video_index = -1; int audio_index = -1; //-------初始化操作 is->videoStream = -1; is->audioStream = -1; global_video_state = is; //ffmpeg操作,獲取文件信息 if(avformat_open_input(&pFormatCtx,is->filename,NULL,NULL)<0){ printf("Failed to open file[%s] context\n", is->filename); return -1; } is->pFormatCtx = pFormatCtx; if(avformat_find_stream_info(pFormatCtx,NULL)<0){ printf("Failed to get detail stream infomation\n"); return -1; } for(i=0;i<pFormatCtx->nb_streams;i++){ if(pFormatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO && video_index < 0) video_index = i; if(pFormatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_AUDIO && audio_index < 0) audio_index = i; } if(video_index < 0 || audio_index < 0){ printf("Failed to find stream index for audio/video\n"); return -1; } av_dump_format(pFormatCtx,audio_index,is->filename,0); //打印信息 av_dump_format(pFormatCtx,video_index,is->filename,0); //打印信息 //初始化音頻流信息 ret = stream_component_open(is,audio_index); if(ret < 0){ printf("Failed to initialize audio data\n"); goto fail; } //初始化視頻流信息 ret = stream_component_open(is,video_index); if(ret < 0){ printf("Failed to initialize video data\n"); goto fail; } //初始化SDL相關數據 win = SDL_CreateWindow("Media Player", SDL_WINDOWPOS_UNDEFINED,SDL_WINDOWPOS_UNDEFINED, is->video_ctx->width,is->video_ctx->height, SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE ); renderer = SDL_CreateRenderer(win,-1,0); texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_IYUV, SDL_TEXTUREACCESS_STREAMING, is->video_ctx->width, is->video_ctx->height); //開始獲取包 for(;;){ if(is->quit){ //對於死循環,需要一直去判斷退出信號 SDL_CondSignal(is->videoq.cond); //既然要退出了,就要讓所有在循環中的程序退出,發送所有可能阻塞的信號量,防止因為缺少信號量而導致的阻塞 SDL_CondSignal(is->audioq.cond); break; } if(is->audioq.size > MAX_AUDIOQ_SIZE || is->videoq.size > MAX_VEDIOQ_SIZE){ //對於數據量大於界限值的時候,我們直接去獲取這些值,不去獲取下一個packet SDL_Delay(10); //等待10ms continue; } if(av_read_frame(is->pFormatCtx,ptk) < 0){ //context對象中有一個pb,這是內部用到的IO上下文,通過pb->error,就能獲取到IO上發生的錯誤。 if(is->pFormatCtx->pb->error == 0){ SDL_Delay(100); //IO錯誤,等待一段時間用戶輸入 continue; }else{ //讀取完畢,退出 break; } } //開始處理讀取的數據包 if(ptk->stream_index == is->videoStream){ //視頻包 packet_queue_put(&is->videoq,ptk); //當我們讀取的視頻也是25fps的話,播放聲音不會卡頓,當是30fps,音頻會出現卡頓,因為隊列中數據可能不存在 printf("put video queue, size:%d\n", is->videoq.nb_packets); }else if(ptk->stream_index == is->audioStream){ //音頻包 packet_queue_put(&is->audioq,ptk); printf("put audio queue, size:%d\n", is->audioq.nb_packets); }else{ av_free_packet(ptk); } } while(!is->quit){ //出現數據包讀取完畢,放入了隊列中,但是還沒有從隊列中讀取完成去解碼播放,所以我們需要等待一段時間 SDL_Delay(100); //播放完畢以后,等待用戶主動關閉 } fail: event.type = FF_QUIT_EVENT; event.user.data1 = is; SDL_PushEvent(&event); return 0; } /*-----------------主(函數)線程方法---------------*/ int main(int argc,char *argv[]){ int ret = -1; SDL_Event event; //主函數線程主要處理事件和視頻渲染問題 VideoState *is; if(argc<2){ fprintf(stderr,"Usage: test <file>\n"); return ret; } //------進行初始化操作(簡單的初始化,初始化鎖、條件變量等信息),開啟解復用線程 is = av_malloc(sizeof(VideoState)); av_register_all(); if(SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER)){ printf("Failed to initialize SDL - %s\n", SDL_GetError()); return ret; } texture_mutex = SDL_CreateMutex(); av_strlcpy(is->filename,argv[1],sizeof(is->filename)); is->pictq_mutex = SDL_CreateMutex(); is->pictq_cond = SDL_CreateCond(); schedule_refresh(is,40); //設置40ms發送刷新信號,這個間隔內去實現解復用,獲取數據。如果這個時間不夠,后面獲取數據為空,則還會再延遲的 //開啟解復用線程,內部也實現了很多初始化操作 is->parse_tid = SDL_CreateThread(decode_thread,"decode_thread",is); if(!is->parse_tid){ av_free(is); //解復用線程開啟失敗 goto __FAIL; } for(;;){ SDL_WaitEvent(&event); //阻塞式等待信號 switch(event.type){ case FF_QUIT_EVENT: //出錯導致出現這個事件 case SDL_QUIT: //用戶點擊了關閉 printf("receive a QUIT event:%d\n", event.type); is->quit = 1; //其他線程推出 goto __QUIT; break; case FF_REFRESH_EVENT: video_refresh_timer(event.user.data1); //傳遞的數據就是VideoState break; default: break; } } __QUIT: ret = 0; __FAIL: SDL_Quit(); return ret; }
gcc playerV3.c -o play_3 -I /usr/local/include/ -I /usr/local/include/SDL2/ -L /usr/local/lib/ -lSDL2 -lavformat -lavcodec -lswscale -lavutil -lswresample
因為音頻的頻率是25fps,視頻這里是30fps;所以是不同步的。而我們代碼中實現的是40ms每張圖片(25fps顯示),會導致視頻隊列中的數據包個數不斷累加!!!
四:簡單的播放器V4(線程實現播放視頻和音頻,實現音視頻同步)
(一)時間同步
回顧:FFmpeg學習(四)視頻基礎
補充1:PTS的獲取
一般使用解碼后的AVFrame的pts,但是pts出錯的話,可以通過函數獲取進行推算!!!
補充2:如何獲取下一幀的PTS
video_clock = 上一幀的video_clock + frame_delay幀間隔時間
音視頻同步就是這兩種clock相互追趕實現的!!
補充3:音視頻同步方式
1.視頻同步到音頻:視頻展示的時候進行延遲即可 2.音頻同步到視頻:丟幀或者添加靜默聲 3.都同步到系統時鍾
補充4:視頻播放的基本思路
(二)基於時間同步的音視頻播放器
以audio為參考時鍾,video同步到音頻的示例代碼:
1.獲取當前要顯示的video PTS,減去上一幀視頻PTS,則得出上一幀視頻應該顯示的時長delay; 2.當前video PTS與參考時鍾當前audio PTS比較,得出音視頻差距diff; 3.獲取同步閾值sync_threshold,為一幀視頻差距,范圍為10ms-100ms; 4.diff小於sync_threshold,則認為不需要同步;否則delay+diff值,則是正確糾正delay; 5.如果超過sync_threshold,且視頻落后於音頻,那么需要減小delay(FFMAX(0, delay + diff)),讓當前幀盡快顯示。 6.如果視頻落后超過1秒,且之前10次都快速輸出視頻幀,那么需要反饋給音頻源減慢,同時反饋視頻源進行丟幀處理,讓視頻盡快追上音頻。因為這很可能是視頻解碼跟不上了,再怎么調整delay也沒用。 7.如果超過sync_threshold,且視頻快於音頻,那么需要加大delay,讓當前幀延遲顯示。 8.將delay*2慢慢調整差距,這是為了平緩調整差距,因為直接delay+diff,會讓畫面畫面遲滯。 9.如果視頻前一幀本身顯示時間很長,那么直接delay+diff一步調整到位,因為這種情況再慢慢調整也沒太大意義。 10.考慮到渲染的耗時,還需進行調整。frame_timer為一幀顯示的系統時間,frame_timer+delay- curr_time,則得出正在需要延遲顯示當前幀的時間。

#include <assert.h> #include <stdio.h> #include <math.h> #include <SDL.h> #include <libavutil/log.h> #include <libavformat/avformat.h> #include <libavcodec/avcodec.h> #include <libswscale/swscale.h> #include <libswresample/swresample.h> #define SDL_AUDIO_BUFFER_SIZE 1024 //對於AAC,一幀數據中單通道獲取 1024個 sample采樣點 #define MAX_AUDIO_FRAME_SIZE 192000 //是指雙通道下,采用48k采樣率,位深為16位,采樣時間為1s #define AV_SYNC_THRESHOLD 0.01 //10ms --- 同步閾值 #define AV_NOSYNC_THRESHOLD 10.0 //10s --- 非同步閾值 #define MAX_AUDIOQ_SIZE (5*6*1024) //音頻隊列中數據字節數 界限,在MAX_AUDIO_FRAME_SIZE內部,即可。 采用最小8k采樣率,2通道,16位深 = 32k,這里使用(5*6*1024)小於這個最小值即可 #define MAX_VEDIOQ_SIZE (5*256*1024) //視頻隊列中數據字節數 界限,需要處理1280x1024的圖像,這里使用這個數值作為界限,實際上對於yuv420p數據,可以在×1.5,但是沒必要的。 #define FF_REFRESH_EVENT (SDL_USEREVENT) //使用自定義事件 #define FF_QUIT_EVENT (SDL_USEREVENT + 1) //使用自定義事件 #define VIDEO_PICTURE_QUEUE_SIZE 1 //定義視頻圖像裁剪后幀的最大個數 typedef struct PacketQueue { AVPacketList* first_pkt,* last_pkt; int nb_packets; int size; SDL_mutex* mutex; SDL_cond* cond; }PacketQueue; typedef struct VideoPicture { AVPicture* pict; //存放yuv數據 int width,height; int allocated; //用來標記圖像是否分配了空間 double pts; }VideoPicture; typedef struct VideoState { /*------------多媒體文件的基礎信息-----------*/ char filename[1024]; //文件名 AVFormatContext* pFormatCtx; //文件格式上下文 int videoStream,audioStream; //視頻流、音頻流索引 /*----------------音頻信息----------------*/ AVStream *audio_st; //流 AVCodecContext *audio_ctx; //解碼上下文 struct SwrContext* audio_swr_cxt; //音頻重采樣上下文 uint8_t audio_buf[(MAX_AUDIO_FRAME_SIZE*3)/2]; //重采樣后的數據最后存放位置 unsigned int audio_buf_size; //寫入的數據大小 unsigned int audio_buf_index; //讀取到的數據的索引 PacketQueue audioq; //音頻隊列 AVFrame audio_frame; //音頻幀(解碼后的)--- 從隊列中取出來的packet,進行解碼出來的一幀數據 AVPacket audio_pkt; //音頻包(隊列中讀取的),這里是提前初始化了一個包,避免了后面每次獲取數據時都設置一個,類似於全局變量了。或者我們像前一個版本那樣使用靜態局部變量也可以的 uint8_t *audio_pkt_data; //指向packet中的data數據要讀取的位置 int audio_pkt_size; //讀取的packet中的數據大小 /*----------------視頻信息----------------*/ AVStream *video_st; //流 AVCodecContext *video_ctx; //圖像解碼上下文 struct SwsContext *sws_ctx; //圖像裁剪上下文 PacketQueue videoq; //隊列中保存着所有讀取的關於視頻的packet VideoPicture pictq[VIDEO_PICTURE_QUEUE_SIZE]; //最后要存放的數據位置---隊列中取出來的一個packet進行裁剪處理后的數據 int pictq_size; //pictq_size表示pictq數組的大小, int pictq_rindex; //索引pictq_rindex表示要訪問的數據在pictq數組中的位置 int pictq_windex; //索引pictq_windex表示要插入的數據在pictq數組中的位置 /*----------------線程信息----------------*/ //只有視頻是我們要處理的數據,所以包含獨立的鎖,音頻是被聲卡主動讀取的 SDL_mutex *pictq_mutex; //視頻數據的鎖,因為數據可以同時讀取和寫入 SDL_cond *pictq_cond; //條件變量 SDL_Thread *parse_tid; //解復用線程id SDL_Thread *video_tid; //視頻解碼線程id /*--------------音視頻同步信息-------------*/ double audio_clock; //音頻(參考)時鍾當前audio PTS,時間戳 double video_clock; //視頻當前要顯示的video PTS double frame_timer; //記錄下次回調的時間(是av_gettime獲取的系統時間,不是時間間隔) double frame_last_pts; //上一次播放視頻幀的pts double frame_last_delay; //上一次播放視頻幀的delay(也可以認為是上一個圖片顯示的時間) /*--------------全局退出信號---------------*/ int quit; //0正常,1退出 }VideoState; /*-------------------SDL相關-----------------*/ SDL_mutex *texture_mutex; //是對渲染器的鎖,因為主線程和視頻解碼線程都會用到 SDL_Window *win; //窗口指針 SDL_Renderer *renderer; //渲染器指針 SDL_Texture *texture; //紋理指針 /*--------------只有一個解復用線程,只在這里線程中被修改!!!---------------*/ VideoState *global_video_state; /*-----------------隊列操作方法---------------*/ void packet_queue_init(PacketQueue *q){ memset(q,0,sizeof(PacketQueue)); q->mutex = SDL_CreateMutex(); q->cond = SDL_CreateCond(); } int packet_queue_get(PacketQueue *q,AVPacket *pkt,int block){ AVPacketList *pktl; int ret; SDL_LockMutex(q->mutex); for(;;){ if(global_video_state->quit){ //對於每個死循環,我們都要進行退出信號判斷 fprintf(stderr,"quit from queue_get!\n"); ret = -1; break; } pktl = q->first_pkt; if(pktl){ q->first_pkt = pktl->next; if(!q->first_pkt) q->last_pkt = NULL; q->nb_packets--; q->size -= pktl->pkt.size; *pkt = pktl->pkt; av_free(pktl); ret = 1; break; }else if(!block){ ret = 0; //表示沒有獲取到數據,接着進入輪詢 break; }else{ //類似阻塞等待 fprintf(stderr,"queue is empty, so wait a moment and wait a cond signal!\n"); SDL_CondWait(q->cond,q->mutex); } } SDL_UnlockMutex(q->mutex); return ret; } int packet_queue_put(PacketQueue *q,AVPacket *pkt){ AVPacketList* pktl; if(av_dup_packet(pkt) < 0){ return -1; } pktl = av_malloc(sizeof(AVPacketList)); if(!pktl){ return -1; } pktl->pkt = *pkt; //data賦值引用即可 pktl->next = NULL; SDL_LockMutex(q->mutex); if(!q->last_pkt) q->first_pkt = pktl; else q->last_pkt->next = pktl; q->last_pkt = pktl; q->nb_packets++; q->size += pktl->pkt.size; SDL_CondSignal(q->cond); //發送信號給get消費者 SDL_UnlockMutex(q->mutex); return 0; } /*-----------------音頻操作方法---------------*/ //獲取音頻的時間戳---獲取方法和函數audio_decode_frame中的pts獲取相對 double get_audio_clock(VideoState *is){ double pts; int hw_buf_size,bytes_per_sec,n; pts = is->audio_clock; //獲取預測的下一個音頻包開始播放時間(或者當前音頻包的結束時間) hw_buf_size = is->audio_buf_size - is->audio_buf_index; //這個就是當前播放的音頻大小 bytes_per_sec = 0; n = is->audio_ctx->channels*2; //通道數×2字節(16位深) if(is->audio_st){ bytes_per_sec = is->audio_ctx->sample_rate*n; //每秒所處理音頻的字節數量 } if(bytes_per_sec){ pts -= (double)hw_buf_size/bytes_per_sec; //獲取當前音頻幀播放的時間!!! } return pts; } //音頻解碼 int audio_decode_frame(VideoState *is, uint8_t *audio_buf, int buf_size, double *pts_ptr){ int len1, data_size = 0,n; AVPacket *pkt = &is->audio_pkt; double pts; for(;;){ while(is->audio_pkt_size > 0){ //還有數據在packet中 int got_frame = 0; len1 = avcodec_decode_audio4(is->audio_ctx,&is->audio_frame,&got_frame,pkt); //讀取pkt中的數據到frame if(len1 < 0){ printf("Failed to decode audio...\n"); is->audio_pkt_size = 0; break; } data_size = 0; if(got_frame){ //解碼幀成功 data_size = 2*2*is->audio_frame.nb_samples; //雙通道,16位深 assert(data_size <= buf_size); //開始進行重采樣 swr_convert(is->audio_swr_cxt, &audio_buf, //輸出空間 MAX_AUDIO_FRAME_SIZE*3/2, //空間大小 (const uint8_t **)is->audio_frame.data, //幀數據位置 is->audio_frame.nb_samples); //幀通道采樣率 //處理數據 is->audio_pkt_size -= len1; is->audio_pkt_data += len1; } if(data_size == 0) //沒有讀取到數據 continue; //---------------------------------------重點:更新音頻包的時間戳!!------------------------------------ pts = is->audio_clock; //獲取時間戳 *pts_ptr = pts; //賦值 n = 2*is->audio_ctx->channels; //通道×2字節(重采樣后的16位深) //(n*is->audio_ctx->sample_rate)是1s內采樣的數據量,所以 //(double)data_size / (double)(n*is->audio_ctx->sample_rate)就是這些數據處理(播放)會花的時間 is->audio_clock += (double)data_size / (double)(n*is->audio_ctx->sample_rate); //就相當於下一次的音頻pts,因為部分packet不會包含pts,這樣推斷更好 //這個is->audio_clock是預測的下一次時間(或者本次音頻包播放完畢的時間) return data_size; //讀取到了數據 } //如果沒有進入上面的while循環,則表示開始沒有數據存在pkt中,我們需要去隊列中獲取 if(pkt->data) av_free_packet(pkt); if(is->quit){ //對於循環,一定退出判斷 printf("Will quit program...\n"); return -1; } if(packet_queue_get(&is->audioq,pkt,0) <= 0){ return -1; } is->audio_pkt_data = pkt->data; is->audio_pkt_size = pkt->size; //---------------------------------------重點:獲取音頻包的時間戳!!------------------------------------ if(pkt->pts != AV_NOPTS_VALUE){ //如果這個數據包有PTS值,就開始更新 is->audio_clock = av_q2d(is->audio_st->time_base)*pkt->pts; //獲取時間戳!! timestamp(秒) = pts * av_q2d(time_base) } } } //聲卡回調讀取數據 void audio_callback(void *userdata,uint8_t *stream,int len){ VideoState *is = (VideoState *)userdata; int len1,audio_size; double pts; //返回的當前的音頻幀的pts!!! SDL_memset(stream,0,len); //初始化緩沖區 while(len > 0){ //這個循環不用擔心,因為會產生靜默聲音,最后跳出while if(is->audio_buf_index >= is->audio_buf_size){ //原有的數據已經讀完了,下面開始獲取新的數據 audio_size = audio_decode_frame(is,is->audio_buf,sizeof(is->audio_buf),&pts); if(audio_size < 0){ //讀取出錯 設置靜默聲音,當其中audio_decode_frame去隊列取數據block為0時,會進入 is->audio_buf_size = 1024*2*2; printf("--------------no audio packet-------------------\n"); memset(is->audio_buf,0,is->audio_buf_size); }else{ is->audio_buf_size = audio_size; } is->audio_buf_index = 0; //新讀取的數據的起始索引位置 } len1 = is->audio_buf_size - is->audio_buf_index; //數據有效大小 if(len1 > len) len1 = len; //memcpy(stream,(uint8_t*)is->audio_buf+is->audio_buf_index,len1); //使用這個可以代替SDL_memset和 SDL_MixAudio(stream,(uint8_t*)is->audio_buf+is->audio_buf_index,len1,SDL_MIX_MAXVOLUME); len -= len1; stream += len1; is->audio_buf_index += len1; } } /*-----------------視頻操作方法---------------*/ //設置定時器 static uint32_t sdl_refresh_timer_cb(uint32_t interval, void *dat){ SDL_Event event; event.type = FF_REFRESH_EVENT; event.user.data1 = dat; SDL_PushEvent(&event); return 0; //0意味着停止計時 } //定時刷新 static void schedule_refresh(VideoState *is,int delay){ SDL_AddTimer(delay,sdl_refresh_timer_cb,is); } //顯示圖像 void video_display(VideoState *is){ SDL_Rect rect; VideoPicture *vp; vp = &is->pictq[is->pictq_rindex]; //獲取要顯示的數據 if(vp->pict){ //有數據,開始顯示 SDL_UpdateYUVTexture(texture,NULL, vp->pict->data[0],vp->pict->linesize[0], vp->pict->data[1],vp->pict->linesize[1], vp->pict->data[2],vp->pict->linesize[2]); rect.x = 0; rect.y = 0; rect.w = is->video_ctx->width; rect.h = is->video_ctx->height; SDL_LockMutex(texture_mutex); //分配數據和渲染數據加鎖處理 SDL_RenderClear(renderer); SDL_RenderCopy(renderer,texture,NULL,&rect); SDL_RenderPresent(renderer); SDL_UnlockMutex(texture_mutex); } } //開始根據事件處理顯示圖像 void video_refresh_timer(void *userdata){ VideoState *is = (VideoState*)userdata; VideoPicture *vp; /* actual_delay: 實際時延間隔 delay: 上一次的時間 sync_threshold 同步閾值 ref_clock: 參考時間(音頻的時間) diff: 差值=當前視頻幀pts - 音頻的參考pts */ double actual_delay,delay,sync_threshold,ref_clock,diff; if(is->video_st){ //如果視頻流存在,則進行處理 if(is->pictq_size == 0){ //隊列中沒有數據,則反復去輪詢是否有數據 schedule_refresh(is,1); //輪詢時間為1 }else{ vp = &is->pictq[is->pictq_rindex]; //獲取要顯示的數據 delay = vp->pts - is->frame_last_pts; //當前視頻幀的pts - 上一幀的pts if(delay <= 0 || delay >= 1.0){ //不合理時間 0或者1s delay = is->frame_last_delay; //上一次的時延(展示時間) } is->frame_last_delay = delay; //更新本次時延 is->frame_last_pts = vp->pts; //上一幀的pts = 當前的pts ref_clock = get_audio_clock(is); //獲取參考幀的數據 diff = vp->pts - ref_clock; sync_threshold = (delay > AV_SYNC_THRESHOLD) ? delay : AV_SYNC_THRESHOLD; //delay時間是否超過閾值,進行更新(取大) if(fabs(diff) < AV_NOSYNC_THRESHOLD){ //非同步閾值10s if(diff <= -sync_threshold){ //視頻比音頻慢(diff為負號),並且超過允許的時延閾值(<=-sync_threshold),加快---- delay = 0; //快點播放 }else if(diff >= sync_threshold){ //視頻比音頻快(diff為正號),並且超過允許的時延閾值 delay *= 2; //視頻延遲展示 } } is->frame_timer += delay; //下次回調時間(時間戳) actual_delay = is->frame_timer - (av_gettime()/1000000.0); //去計算時延間隔(delay)---秒 if(actual_delay < AV_SYNC_THRESHOLD){ //在允許的閾值范圍內 actual_delay = AV_SYNC_THRESHOLD; //至少10ms刷新一次 } schedule_refresh(is,(int)(actual_delay*1000+0.5)); //下一次幀渲染時間,將上面獲取的s轉換為ms,+0.5的差值 video_display(is); //顯示圖像 if(++is->pictq_rindex == VIDEO_PICTURE_QUEUE_SIZE){ is->pictq_rindex = 0; //修改要顯示的索引位置 } SDL_LockMutex(is->pictq_mutex); is->pictq_size--; //顯示后,去通知可以解析下一次圖像 SDL_CondSignal(is->pictq_cond); //通知queue_picture去分配空間 SDL_UnlockMutex(is->pictq_mutex); } }else{ schedule_refresh(is,100); //如果不存在視頻流,就再延長等待一段時間 } } //為pictq數組中新插入的數據分配空間 void alloc_picture(void *userdata){ VideoState *is = (VideoState *)userdata; VideoPicture *vp = &is->pictq[is->pictq_windex]; if(vp->pict){ avpicture_free(vp->pict); //釋放yuv空間 free(vp->pict); //釋放pict } SDL_LockMutex(texture_mutex); //因為分配和渲染都要使用到這個數據,所以要進行加鎖 vp->pict = (AVPicture*)malloc(sizeof(AVPicture)); //分配空間 if(vp->pict){ avpicture_alloc(vp->pict, AV_PIX_FMT_YUV420P, is->video_ctx->width, is->video_ctx->height); } SDL_UnlockMutex(texture_mutex); vp->width = is->video_ctx->width; vp->height = is->video_ctx->height; vp->allocated = 1; } //分配空間,將視頻幀放入pictq數組中去 int queue_picture(VideoState *is,AVFrame *pFrame,double pts){ VideoPicture *vp; int dst_pix_fmt; AVPicture pict; //yuv數據 SDL_LockMutex(is->pictq_mutex); //------------??應該是if while(is->pictq_size >= VIDEO_PICTURE_QUEUE_SIZE && !is->quit){ //表示隊列的大小已經到達最大界限(使用==判斷也可以) SDL_CondWait(is->pictq_cond,is->pictq_mutex); //現在數組中已經放不下裁剪后的圖像了,只有主線程消費了圖像以后,發生信號過來,才進行解碼下一幀圖像 } SDL_UnlockMutex(is->pictq_mutex); //上面的條件等待完成,表示前一個圖像顯示完成,現在需要裁剪新的圖像 if(is->quit){ printf("quit from queue picture...\n"); return -1; } vp = &is->pictq[is->pictq_windex]; //這個是要插入的位置數據 if(!vp->pict || vp->width != is->video_ctx->width || vp->height != is->video_ctx->height){ //開始分配空間 vp->allocated = 0; //還沒有開始分配空間 alloc_picture(is); //開始分配空間,內部會修改標識符 if(is->quit){ printf("quit from queue picture 2...\n"); return -1; } } if(vp->pict){ //分配成功,開始進行裁剪處理,處理后的數據放入這里面 //---------------------修改pts--------------------- vp->pts = pts; //更新pts sws_scale(is->sws_ctx,(uint8_t const* const*)pFrame->data, pFrame->linesize,0,is->video_ctx->height, vp->pict->data,vp->pict->linesize); if(++is->pictq_windex == VIDEO_PICTURE_QUEUE_SIZE){ is->pictq_windex = 0; } SDL_LockMutex(is->pictq_mutex); is->pictq_size++; //多線程會操作這個 SDL_UnlockMutex(is->pictq_mutex); } return 0; } double synchronize_video(VideoState* is,AVFrame *src_frame,double pts){ double frame_delay; if(pts != 0){ is->video_clock = pts; //直接更新video_clock }else{ pts = is->video_clock; //否則使用上一次的video_clock作為pts值 } frame_delay = av_q2d(is->video_ctx->time_base); //通過時間基獲取幀間隔 //糾正play (播放時間)的方法 repeat_pict / (2 * fps) 是ffmpeg注釋里教的 frame_delay += src_frame->repeat_pict * (frame_delay*0.5); //這個幀有一個字段repeat_pict,是要這個幀重復的播放 is->video_clock += frame_delay; //保存了下一幀的pts return pts; } //解碼視頻幀 int video_thread(void *arg){ VideoState *is = (VideoState *)arg; AVPacket packet,*pkt = &packet; int frameFinished; AVFrame *pFrame; double pts; pFrame = av_frame_alloc(); for(;;){ if(packet_queue_get(&is->videoq,pkt,1) < 0){ break; //沒有數據,退出 } avcodec_decode_video2(is->video_ctx,pFrame,&frameFinished,pkt); //解碼數據幀 //------------------------獲取視頻幀的pts信息------------------------ if((pts = av_frame_get_best_effort_timestamp(pFrame)) == AV_NOPTS_VALUE){ //av_frame_get_best_effort_timestamp計算最合適的pts時間 pts = 0; } pts *= av_q2d(is->video_st->time_base); //獲取時間戳信息---轉換為秒 if(frameFinished){ //去裁剪處理 pts = synchronize_video(is,pFrame,pts); //計算pts,防止前面的pts為0,然后順便獲取下一幀的pts if(queue_picture(is,pFrame,pts) < 0){ //內部修改視頻的pts信息 break; } } av_free_packet(pkt); } av_frame_free(&pFrame); return 0; } /*-----------------解復用線程方法,主要用於初始化---------------*/ //初始化音視頻流的相關數據,(只被音頻、視頻流各自調用一次) int stream_component_open(VideoState *is, int stream_index){ int64_t in_channel_layout,out_channel_layout; AVFormatContext *pFormatCtx = is->pFormatCtx; AVCodecContext *codecCtx = NULL; AVCodec *codec = NULL; SDL_AudioSpec new_spec,old_spec; codec = avcodec_find_decoder(pFormatCtx->streams[stream_index]->codec->codec_id); //獲取解碼器信息 if(!codec){ printf("UnSupport codec!\n"); return -1; } codecCtx = avcodec_alloc_context3(codec); if(!codecCtx){ printf("Failed to alloc codec context\n"); return -1; } if(avcodec_copy_context(codecCtx,pFormatCtx->streams[stream_index]->codec) < 0){ printf("Failed to copy codec context\n"); return -1; } if(codecCtx->codec_type == AVMEDIA_TYPE_AUDIO){ //設置音頻信息 new_spec.freq = codecCtx->sample_rate; new_spec.format = AUDIO_S16SYS; new_spec.channels = codecCtx->channels; new_spec.silence = 0; new_spec.samples = SDL_AUDIO_BUFFER_SIZE; //AAC單通道采樣率就是1024 new_spec.callback = audio_callback; new_spec.userdata = is; if(SDL_OpenAudio(&new_spec,&old_spec)<0){ printf("SDL_OpenAudio: %s\n", SDL_GetError()); return -1; } } if(avcodec_open2(codecCtx,codec,NULL) < 0){ printf("Failed to open codec\n"); return -1; } switch(codecCtx->codec_type){ case AVMEDIA_TYPE_AUDIO: //處理音頻 is->audioStream = stream_index; is->audio_st = pFormatCtx->streams[stream_index]; is->audio_ctx = codecCtx; is->audio_buf_size = 0; is->audio_buf_index = 0; memset(&is->audio_pkt,0,sizeof(is->audio_pkt)); //取代了前一個版本的靜態局部變量而已,這里不使用memset也可以 packet_queue_init(&is->audioq); SDL_PauseAudio(0); in_channel_layout = av_get_default_channel_layout(is->audio_ctx->channels); out_channel_layout = in_channel_layout; is->audio_swr_cxt = swr_alloc(); //開始分配重采樣的數據空間 swr_alloc_set_opts(is->audio_swr_cxt, out_channel_layout, AV_SAMPLE_FMT_S16, is->audio_ctx->sample_rate, in_channel_layout, is->audio_ctx->sample_fmt, is->audio_ctx->sample_rate, 0, NULL); swr_init(is->audio_swr_cxt); break; case AVMEDIA_TYPE_VIDEO: //處理視頻 is->videoStream = stream_index; is->video_st = pFormatCtx->streams[stream_index]; is->video_ctx = codecCtx; //------------------------時間初始化------------------------ is->frame_timer = (double)av_gettime()/1000000.0; is->frame_last_delay = 40e-3; packet_queue_init(&is->videoq); is->sws_ctx = sws_getContext(is->video_ctx->width, is->video_ctx->height, is->video_ctx->pix_fmt, is->video_ctx->width, is->video_ctx->height, AV_PIX_FMT_YUV420P, SWS_BILINEAR, NULL,NULL,NULL); is->video_tid = SDL_CreateThread(video_thread,"video_thread",is); //開始視頻解碼線程 break; default: break; } return 0; } //解復用,會在后面一直讀取視頻數據包 int decode_thread(void *arg){ VideoState *is = (VideoState *)arg; AVFormatContext *pFormatCtx = NULL; AVPacket packet, *ptk = &packet; SDL_Event event; int i,ret=0; int video_index = -1; int audio_index = -1; //-------初始化操作 is->videoStream = -1; is->audioStream = -1; global_video_state = is; //ffmpeg操作,獲取文件信息 if(avformat_open_input(&pFormatCtx,is->filename,NULL,NULL)<0){ printf("Failed to open file[%s] context\n", is->filename); return -1; } is->pFormatCtx = pFormatCtx; if(avformat_find_stream_info(pFormatCtx,NULL)<0){ printf("Failed to get detail stream infomation\n"); return -1; } for(i=0;i<pFormatCtx->nb_streams;i++){ if(pFormatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO && video_index < 0) video_index = i; if(pFormatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_AUDIO && audio_index < 0) audio_index = i; } if(video_index < 0 || audio_index < 0){ printf("Failed to find stream index for audio/video\n"); return -1; } av_dump_format(pFormatCtx,audio_index,is->filename,0); //打印信息 av_dump_format(pFormatCtx,video_index,is->filename,0); //打印信息 //初始化音頻流信息 ret = stream_component_open(is,audio_index); if(ret < 0){ printf("Failed to initialize audio data\n"); goto fail; } //初始化視頻流信息 ret = stream_component_open(is,video_index); if(ret < 0){ printf("Failed to initialize video data\n"); goto fail; } //初始化SDL相關數據 win = SDL_CreateWindow("Media Player", SDL_WINDOWPOS_UNDEFINED,SDL_WINDOWPOS_UNDEFINED, is->video_ctx->width,is->video_ctx->height, SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE ); renderer = SDL_CreateRenderer(win,-1,0); texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_IYUV, SDL_TEXTUREACCESS_STREAMING, is->video_ctx->width, is->video_ctx->height); //開始獲取包 for(;;){ if(is->quit){ //對於死循環,需要一直去判斷退出信號 SDL_CondSignal(is->videoq.cond); //既然要退出了,就要讓所有在循環中的程序退出,發送所有可能阻塞的信號量,防止因為缺少信號量而導致的阻塞 SDL_CondSignal(is->audioq.cond); break; } if(is->audioq.size > MAX_AUDIOQ_SIZE || is->videoq.size > MAX_VEDIOQ_SIZE){ //對於數據量大於界限值的時候,我們直接去獲取這些值,不去獲取下一個packet SDL_Delay(10); //等待10ms continue; } if(av_read_frame(is->pFormatCtx,ptk) < 0){ //context對象中有一個pb,這是內部用到的IO上下文,通過pb->error,就能獲取到IO上發生的錯誤。 if(is->pFormatCtx->pb->error == 0){ SDL_Delay(100); //IO錯誤,等待一段時間用戶輸入 continue; }else{ //讀取完畢,退出 break; } } //開始處理讀取的數據包 if(ptk->stream_index == is->videoStream){ //視頻包 packet_queue_put(&is->videoq,ptk); //當我們讀取的視頻也是25fps的話,播放聲音不會卡頓,當是30fps,音頻會出現卡頓,因為隊列中數據可能不存在 printf("put video queue, size:%d\n", is->videoq.nb_packets); }else if(ptk->stream_index == is->audioStream){ //音頻包 packet_queue_put(&is->audioq,ptk); printf("put audio queue, size:%d\n", is->audioq.nb_packets); }else{ av_free_packet(ptk); } } while(!is->quit){ //出現數據包讀取完畢,放入了隊列中,但是還沒有從隊列中讀取完成去解碼播放,所以我們需要等待一段時間 SDL_Delay(100); //播放完畢以后,等待用戶主動關閉 } fail: event.type = FF_QUIT_EVENT; event.user.data1 = is; SDL_PushEvent(&event); return 0; } /*-----------------主(函數)線程方法---------------*/ int main(int argc,char *argv[]){ int ret = -1; SDL_Event event; //主函數線程主要處理事件和視頻渲染問題 VideoState *is; if(argc<2){ fprintf(stderr,"Usage: test <file>\n"); return ret; } //------進行初始化操作(簡單的初始化,初始化鎖、條件變量等信息),開啟解復用線程 is = av_malloc(sizeof(VideoState)); av_register_all(); if(SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER)){ printf("Failed to initialize SDL - %s\n", SDL_GetError()); return ret; } texture_mutex = SDL_CreateMutex(); av_strlcpy(is->filename,argv[1],sizeof(is->filename)); is->pictq_mutex = SDL_CreateMutex(); is->pictq_cond = SDL_CreateCond(); schedule_refresh(is,40); //設置40ms發送刷新信號,這個間隔內去實現解復用,獲取數據。如果這個時間不夠,后面獲取數據為空,則還會再延遲的 //開啟解復用線程,內部也實現了很多初始化操作 is->parse_tid = SDL_CreateThread(decode_thread,"decode_thread",is); if(!is->parse_tid){ av_free(is); //解復用線程開啟失敗 goto __FAIL; } for(;;){ SDL_WaitEvent(&event); //阻塞式等待信號 switch(event.type){ case FF_QUIT_EVENT: //出錯導致出現這個事件 case SDL_QUIT: //用戶點擊了關閉 printf("receive a QUIT event:%d\n", event.type); is->quit = 1; //其他線程推出 goto __QUIT; break; case FF_REFRESH_EVENT: video_refresh_timer(event.user.data1); //傳遞的數據就是VideoState break; default: break; } } __QUIT: ret = 0; __FAIL: SDL_Quit(); return ret; }
因為進行了音視頻同步,所以導致音頻、視頻隊列中的數據包個數幾乎保持不變,為12和37!!!