在上傳一個開源播放器項目ffplay for mfc。它會ffmpeg工程ffplay媒體播放器(ffplay.c)移植到VC環境,而使用MFC作一套接口。它能夠完成一個播放器播放的基本流程的視頻:解決方案協議。解包,視頻/音頻解碼,AV同步。視頻和音頻輸出。網絡
此外還包含一些控制功能:播放,暫停/繼續,前進,後退,中止,逐幀播放,全屏等;以及一些碼流分析功能:視頻解碼分析和音頻解碼分析。數據結構
具體的軟件使用就不細緻介紹了,本文簡介當中比較重要的模塊的流程。以防長時間不看的話忘了~ide
軟件信息:函數
ffplay播放器移植VC的工程:ffplay for MFCoop
SourceForge項目主頁:
https://sourceforge.net/projects/ffplayformfc/源碼分析
軟件結構如圖1所看到的。包含例如如下模塊:控制,視頻播放。參數提取。碼流分析。當中,視頻播放模塊用於視頻的解碼和播放;控制模塊用於控制視頻的播放。參數提取模塊用於提取顯示視頻的各類參數;碼流分析模塊伴隨着視頻的播放分析視音頻流中的參數。學習
圖1.軟件結構ui
視頻播放模塊的做用就是將網絡上(或者是本地)的視音頻數據接收下來通過一系列處理後終於輸出到視音頻設備上。依據處理的順序不一樣,它可以分爲下面幾個子模塊:編碼
1) 解協議模塊.net
2) 解封裝模塊
3) 視頻解碼模塊
4) 音頻解碼模塊
5) 視音頻同步模塊
視頻播放模塊的流程圖如圖2所看到的。依照處理的順序分爲解協議,解封裝,視頻解碼,音頻解碼,視音頻同步。
具體的原理在文章 [總結]視音頻編解碼技術零基礎學習方法 中有具體的說明,在這裏再也不反覆,示意圖例如如下所看到的。
圖2.視頻播放模塊流程
這一模塊主要是經過對ffplay.c改寫而獲得的。
改寫完畢後爲ffplaycore.cpp。
簡單的代碼方面的流程可以參考:
100行代碼實現最簡單的基於FFMPEG+SDL的視頻播放器
比較完整的代碼方面的流程可以參考:
控制模塊的做用就是控制視頻的播放。包含下面幾種功能:
1) 開始
2) 暫停/繼續
3) 快進/快退
4) 逐幀播放
5) 調整窗體大小
6) 全屏
7) 調整播放進度
軟件開始解碼視頻數據以後,會進入一個函數event_loop()。該函數內部不停地循環,使用SDL_WaitEvent()等待着響應系統的消息。接收到消息以後,依據消息類型的不一樣,作出不一樣的響應。如圖3所看到的例舉了6種不一樣的消息相應的不一樣的響應:
1) SDLK_ESCAPE。相應鍵盤上「Esc」鍵的響應。功能是退出程序。
2) SDLK_SPACE。相應鍵盤上「空格」鍵的響應。功能是暫停播放。
3) SDL_MOUSEBUTTONDOWN。相應鼠標單擊的響應。功能是調整視頻播放進度。
4) SDL_VIDEORESIZE。
相應「VideoResize」消息的響應。功能是調整播放窗體的大小。
5) FF_REFRESH_EVENT。
相應本身定義消息「FF_REFRESH_EVENT」的響應。
功能是刷新視頻畫面。
6) FFMFC_SEEK_BAR_EVENT。相應本身定義消息「FFMFC_SEEK_BAR_EVENT」的響應。功能是調整視頻播放進度條。
圖3.控制模塊流程(消息循環)
event_loop()函數代碼例如如下:
/* handle an event sent by the GUI */ //處理各類鼠標鍵盤命令,包含各類事件 static void event_loop(VideoState *cur_stream) { SDL_Event event; double incr, pos, frac; for (;;) { double x; //推斷退出------- if(exit_remark==1) break; //--------------- if (cur_stream->abort_request) break; SDL_WaitEvent(&event); switch (event.type) { case SDL_KEYDOWN: if (exit_on_keydown) { do_exit(cur_stream); break; } switch (event.key.keysym.sym) { case SDLK_ESCAPE: case SDLK_q: do_exit(cur_stream); break; case SDLK_f: //全屏 toggle_full_screen(cur_stream); cur_stream->force_refresh = 1; break; case SDLK_p: //暫停 case SDLK_SPACE: toggle_pause(cur_stream); break; case SDLK_s: // S: Step to next frame step_to_next_frame(cur_stream); break; case SDLK_a: stream_cycle_channel(cur_stream, AVMEDIA_TYPE_AUDIO); break; case SDLK_v: stream_cycle_channel(cur_stream, AVMEDIA_TYPE_VIDEO); break; case SDLK_t: stream_cycle_channel(cur_stream, AVMEDIA_TYPE_SUBTITLE); break; //改動了一下,三中顯示模式分紅了三個鍵 case SDLK_w: toggle_audio_display(cur_stream,SHOW_MODE_VIDEO); cur_stream->force_refresh = 1; break; case SDLK_e: toggle_audio_display(cur_stream,SHOW_MODE_WAVES); cur_stream->force_refresh = 1; break; case SDLK_r: toggle_audio_display(cur_stream,SHOW_MODE_RDFT); cur_stream->force_refresh = 1; break; case SDLK_y: cur_stream->v_show_mode=SHOW_MODE_Y; break; case SDLK_PAGEUP: incr = 600.0; goto do_seek; case SDLK_PAGEDOWN: incr = -600.0; goto do_seek; //左方向鍵 case SDLK_LEFT: incr = -10.0; goto do_seek; case SDLK_RIGHT: incr = 10.0; goto do_seek; case SDLK_UP: incr = 60.0; goto do_seek; case SDLK_DOWN: incr = -60.0; do_seek: if (seek_by_bytes) { if (cur_stream->video_stream >= 0 && cur_stream->video_current_pos >= 0) { pos = cur_stream->video_current_pos; } else if (cur_stream->audio_stream >= 0 && cur_stream->audio_pkt.pos >= 0) { pos = cur_stream->audio_pkt.pos; } else pos = avio_tell(cur_stream->ic->pb); if (cur_stream->ic->bit_rate) incr *= cur_stream->ic->bit_rate / 8.0; else incr *= 180000.0; pos += incr; stream_seek(cur_stream, pos, incr, 1); } else { pos = get_master_clock(cur_stream); pos += incr; stream_seek(cur_stream, (int64_t)(pos * AV_TIME_BASE), (int64_t)(incr * AV_TIME_BASE), 0); } break; default: break; } break; case SDL_VIDEOEXPOSE: cur_stream->force_refresh = 1; break; //鼠標單擊 case SDL_MOUSEBUTTONDOWN: if (exit_on_mousedown) { do_exit(cur_stream); break; } case SDL_MOUSEMOTION: if (event.type == SDL_MOUSEBUTTONDOWN) { x = event.button.x; } else { if (event.motion.state != SDL_PRESSED) break; x = event.motion.x; } if (seek_by_bytes || cur_stream->ic->duration <= 0) { uint64_t size = avio_size(cur_stream->ic->pb); stream_seek(cur_stream, size*x/cur_stream->width, 0, 1); } else { int64_t ts; int ns, hh, mm, ss; int tns, thh, tmm, tss; tns = cur_stream->ic->duration / 1000000LL; thh = tns / 3600; tmm = (tns % 3600) / 60; tss = (tns % 60); frac = x / cur_stream->width; ns = frac * tns; hh = ns / 3600; mm = (ns % 3600) / 60; ss = (ns % 60); fprintf(stderr, "Seek to %2.0f%% (%2d:%02d:%02d) of total duration (%2d:%02d:%02d) \n", frac*100, hh, mm, ss, thh, tmm, tss); ts = frac * cur_stream->ic->duration; if (cur_stream->ic->start_time != AV_NOPTS_VALUE) ts += cur_stream->ic->start_time; stream_seek(cur_stream, ts, 0, 0); } break; case SDL_VIDEORESIZE: screen = SDL_SetVideoMode(event.resize.w, event.resize.h, 0, SDL_HWSURFACE|SDL_RESIZABLE|SDL_ASYNCBLIT|SDL_HWACCEL); screen_width = cur_stream->width = event.resize.w; screen_height = cur_stream->height = event.resize.h; cur_stream->force_refresh = 1; break; case SDL_QUIT: case FF_QUIT_EVENT: do_exit(cur_stream); break; case FF_ALLOC_EVENT: alloc_picture((VideoState *)(event.user.data1)); break; case FF_REFRESH_EVENT: video_refresh(event.user.data1); cur_stream->refresh = 0; break; case FFMFC_SEEK_BAR_EVENT:{ if (seek_by_bytes || cur_stream->ic->duration <= 0) { uint64_t size = avio_size(cur_stream->ic->pb); stream_seek(cur_stream, size*seek_bar_pos/1000, 0, 1); } else { int64_t ts; frac=(double)seek_bar_pos/1000; ts = frac * cur_stream->ic->duration; if (cur_stream->ic->start_time != AV_NOPTS_VALUE) ts += cur_stream->ic->start_time; stream_seek(cur_stream, ts, 0, 0); } break; } default: break; } } }
控制模塊的各個功能函數,僅僅需要設置必定內容的消息。再發送出去。就可以完畢相應的控制功能。
如圖4所看到的,分別例舉了3種控制功能的完畢方式。
1) 「暫停」功能,發送SDLK_SPACE消息。
2) 「調整窗體大小」功能。發送VIDEORESIZE消息,並附帶窗體的大小。
3) 「調整視頻播放進度條」功能。發送FFMFC_SEEK_BAR_EVENT消息。
圖4.控制模塊流程(發送消息)
各個功能函數的代碼例如如下:
//發送「全屏」命令 //Send Command "FullScreen" void ffmfc_play_fullcreen(){ SDL_Event event; event.type = SDL_KEYDOWN; event.key.keysym.sym=SDLK_f; SDL_PushEvent(&event); } //發送「暫停」命令 //Send Command "Pause" void ffmfc_play_pause(){ SDL_Event event; event.type = SDL_KEYDOWN; event.key.keysym.sym=SDLK_p; SDL_PushEvent(&event); } //發送「逐幀」命令 //Send Command "Step" void ffmfc_seek_step(){ SDL_Event event; event.type = SDL_KEYDOWN; event.key.keysym.sym=SDLK_s; SDL_PushEvent(&event); } //發送「寬高比」命令 //Send Command "AspectRatio" void ffmfc_aspectratio(int num,int den){ int w=g_is->width; int h=g_is->height; int w_re=h*num/den; SDL_Event event; event.type = SDL_VIDEORESIZE; event.resize.w=w_re; event.resize.h=h; SDL_PushEvent(&event); } //發送「大小」命令 //Send Command "WindowSize" void ffmfc_size(int percentage){ int w=g_is->ic->streams[g_is->video_stream]->codec->width; int h=g_is->ic->streams[g_is->video_stream]->codec->height; SDL_Event event; event.type = SDL_VIDEORESIZE; event.resize.w=w*percentage/100; event.resize.h=h*percentage/100; SDL_PushEvent(&event); } //發送「窗體畫面內容」命令 //Send Command "Audio Display Mode" void ffmfc_audio_display(int mode){ SDL_Event event; event.type = SDL_KEYDOWN; switch(mode){ case 0:event.key.keysym.sym=SDLK_w;break; case 1:event.key.keysym.sym=SDLK_e;break; case 2:event.key.keysym.sym=SDLK_r;break; } SDL_PushEvent(&event); } //發送「前進/後退」命令 //Send Command "Seek" void ffmfc_seek(int time){ SDL_Event event; event.type = SDL_KEYDOWN; switch (time){ case -10 :event.key.keysym.sym=SDLK_LEFT;break; case 10 :event.key.keysym.sym=SDLK_RIGHT;break; case -60 :event.key.keysym.sym=SDLK_DOWN;break; case 60 :event.key.keysym.sym=SDLK_UP;break; case -600 :event.key.keysym.sym=SDLK_PAGEDOWN;break; case 600 :event.key.keysym.sym=SDLK_PAGEUP;break; default :event.key.keysym.sym=SDLK_RIGHT;break; } SDL_PushEvent(&event); } //播放進度 //Seek Bar void ffmfc_seek_bar(int pos){ SDL_Event event; event.type = FFMFC_SEEK_BAR_EVENT; seek_bar_pos=pos; SDL_PushEvent(&event); }
void CffplaymfcDlg::OnBnClickedSeekB() { ffmfc_seek(-60); } void CffplaymfcDlg::OnBnClickedPause() { ffmfc_play_pause(); } void CffplaymfcDlg::OnBnClickedSeekF() { ffmfc_seek(60); } void CffplaymfcDlg::OnBnClickedStop() { ffmfc_quit(); SystemClear(); ResetBtn(); } void CffplaymfcDlg::OnBnClickedSeekStep() { ffmfc_seek_step(); } void CffplaymfcDlg::OnBnClickedFullscreen() { ffmfc_play_fullcreen(); }
參數提取模塊的做用就是提取視頻碼流中的一部分參數。
依照參數種類的不一樣。分爲封裝格式參數。視頻編碼參數。音頻編碼參數。
(1) 封裝格式參數
封裝格式參數指的是封裝格式中包含的參數。包含:
1) 輸入協議
2) 封裝格式
3) 比特率
4) 時長
5) 元數據
(2)視頻編碼參數
視頻編碼參數指的是視頻碼流中的參數。包含:
1) 輸出像素格式
2) 編碼方式
3) 幀率
4) 畫面大小
(3)音頻編碼參數
音頻編碼參數指的是音頻碼流中的參數。
包含:
1) 採樣率
2) 編碼方式
3) 聲道數
參數提取模塊的流程圖如圖5所看到的。參數提取的功能在函數ffmfc_param_global()中實現。系統經過調用av_register_all()、avformat_open_input()等一系列函數直到avcodec_open()函數完畢初始化工做。初始化完畢以後。系統調用ffmfc_param_global()完畢參數提取功能。參數提取功能完畢以後。系統循環調用函數av_read_frame()獲取每幀壓縮碼流數據。
圖5.參數提取模塊流程
參數提取函數ffmfc_param_global()代碼例如如下:
//全局的,僅僅設置一次 int ffmfc_param_global(VideoState *is){ //初始化 CString input_protocol,input_format,wxh,decoder_name, decoder_type,bitrate,extention,pix_fmt,framerate,timelong,decoder_name_au,sample_rate_au,channels_au; float framerate_temp,timelong_temp,bitrate_temp; //注意:把int等類型轉換成LPCTSTR //CString可以直接賦值給LPCTSTR AVFormatContext *pFormatCtx = is->ic; int video_stream=is->video_stream; int audio_stream=is->audio_stream; AVCodecContext *pCodecCtx = pFormatCtx->streams[video_stream]->codec; AVCodecContext *pCodecCtx_au = pFormatCtx->streams[audio_stream]->codec; URLContext *uc=(URLContext *)pFormatCtx->pb->opaque; URLProtocol *up=(URLProtocol *)uc->prot; //輸入文件的協議---------- input_protocol.Format("%s",up->name); dlg->m_formatprotocol.SetWindowText(input_protocol); //視頻解碼參數,有視頻的時候設置 if(video_stream!=-1){ wxh.Format("%d x %d",pCodecCtx->width,pCodecCtx->height); dlg->m_codecvresolution.SetWindowText(wxh); decoder_name.Format("%s",pCodecCtx->codec->long_name); dlg->m_codecvname.SetWindowText(decoder_name); //幀率顯示還有問題 framerate_temp=(pFormatCtx->streams[video_stream]->r_frame_rate.num)/(pFormatCtx->streams[video_stream]->r_frame_rate.den); framerate.Format("%5.2ffps",framerate_temp); dlg->m_codecvframerate.SetWindowText(framerate); switch(pCodecCtx->pix_fmt){ case 0: pix_fmt.Format("YUV420P");break; case 1: pix_fmt.Format("YUYV422");break; case 2: pix_fmt.Format("RGB24");break; case 3: pix_fmt.Format("BGR24");break; case 12: pix_fmt.Format("PIX_FMT_YUVJ420P");break; default: pix_fmt.Format("UNKNOWN"); } dlg->m_codecvpixfmt.SetWindowText(pix_fmt); } //音頻解碼參數。有音頻的時候設置 if(audio_stream!=-1){ decoder_name_au.Format("%s",pCodecCtx_au->codec->long_name); dlg->m_codecaname.SetWindowText(decoder_name_au); sample_rate_au.Format("%d",pCodecCtx_au->sample_rate); dlg->m_codecasamplerate.SetWindowText(sample_rate_au); channels_au.Format("%d",pCodecCtx_au->channels); dlg->m_codecachannels.SetWindowText(channels_au); } //顯示成以k爲單位 bitrate_temp=((float)(pFormatCtx->bit_rate))/1000; bitrate.Format("%5.2fkbps",bitrate_temp); dlg->m_formatbitrate.SetWindowText(bitrate); //duration是以微秒爲單位 timelong_temp=(pFormatCtx->duration)/1000000; //轉換成hh:mm:ss形式 int tns, thh, tmm, tss; tns = (pFormatCtx->duration)/1000000; thh = tns / 3600; tmm = (tns % 3600) / 60; tss = (tns % 60); timelong.Format("%02d:%02d:%02d",thh,tmm,tss); dlg->m_formatduration.SetWindowText(timelong); dlg->m_duration.SetWindowText(timelong); //輸入文件的封裝格式------ input_format.Format("%s",pFormatCtx->iformat->long_name); dlg->m_formatinputformat.SetWindowText(input_format); //------------------------ //bitrate.Format("%d",pCodecCtx->bit_rate); //dlg->m_bitrate.SetWindowText(bitrate); //MetaData------------------------------------------------------------ //從AVDictionary得到 //需要用到AVDictionaryEntry對象 //CString author,copyright,description; CString meta=NULL,key,value; AVDictionaryEntry *m = NULL; //不用一個一個找出來 /* m=av_dict_get(pFormatCtx->metadata,"author",m,0); author.Format("做者:%s",m->value); m=av_dict_get(pFormatCtx->metadata,"copyright",m,0); copyright.Format("版權:%s",m->value); m=av_dict_get(pFormatCtx->metadata,"description",m,0); description.Format("描寫敘述:%s",m->value); */ //使用循環讀出 //(需要讀取的數據,字段名稱。前一條字段(循環時使用),參數) while(m=av_dict_get(pFormatCtx->metadata,"",m,AV_DICT_IGNORE_SUFFIX)){ key.Format(m->key); value.Format(m->value); meta+=key+"\t:"+value+"\r\n" ; } //EditControl換行用\n不行。需要使用\r\n //除了要用\r\n外。還要都CEdit 的屬性進行設置: //Auto HScroll 設置爲 False //MultiLine 設置爲 True //dlg->m_metadata.SetWindowText(author+"\r\n"+copyright+"\r\n"+description); dlg->m_formatmetadata.SetWindowText(meta); //-------------------------------------------------------------------- return 0; }
碼流分析模塊在視頻播放過程當中,伴隨着視頻的解碼。分析當中的視音頻參數。
可以分爲視頻碼流分析模塊和音頻碼流分析模塊。
(1)視頻碼流分析模塊
視頻碼流分析模塊伴隨着視頻的解碼,分析每一個視頻幀的參數。包含:
1) 序號
2) 幀類型
3) 關鍵幀
4) 碼流序號
5) PTS
(2) 音頻碼流分析模塊
音頻碼流分析模塊伴隨着音頻的解碼,分析音頻幀的參數。包含:
1) 序號
2) 大小
3) PTS
碼流分析模塊的流程圖如圖6所看到的。視頻碼流分析功能在函數ffmfc_param_vframe()中實現。音頻碼流分析功能在函數ffmfc_param_aframe()中實現。
這兩個函數在系統一幀一幀解碼視頻/音頻的過程當中循環調用。系統在初始化完畢以後,調用av_read_frame()獲取一幀一幀的視頻/音頻壓縮編碼數據(存儲在結構體AVPacket中)。獲取一幀壓縮編碼數據以後,首先推斷它的類型。
假設該幀數據是視頻。則調用avcodec_decode_video2()對該幀視頻進行解碼,隨後調用ffmfc_param_vframe()分析該幀視頻的參數(主要存儲在結構體AVFrame中)。
假設該幀數據是音頻,則調用avcodec_decode_audio4()對該幀音頻進行解碼。隨後調用ffmfc_param_aframe()分析該幀音頻的參數(主要也是存儲在結構體AVFrame中)。
圖6.碼流分析模塊流程
視頻碼流分析的函數ffmfc_param_vframe()代碼例如如下:
//視頻幀參數提取 int ffmfc_param_vframe(VideoState *is,AVFrame *pFrame,AVPacket *packet){ //-------------------------------------------------------------------- CString key_frame,pict_type,reference,f_index,pts,dts,codednum; AVFormatContext *pFormatCtx = is->ic; int video_stream=is->video_stream; AVCodecContext *pCodecCtx = pFormatCtx->streams[video_stream]->codec; //避免數據太多。超過必定量以後,就會清零-------------------------- if(vframe_index>=MAX_FRAME_NUM){ dlg->SystemClear(); } //------------------------------ f_index.Format("%d",vframe_index); //獲取當前記錄條數 int nIndex=dlg->vddlg->m_videodecodelist.GetItemCount(); //「行」數據結構 LV_ITEM lvitem; lvitem.mask=LVIF_TEXT; lvitem.iItem=nIndex; lvitem.iSubItem=0; //注:vframe_index不可以直接賦值! //務必使用f_index運行Format!再賦值! lvitem.pszText=(char *)(LPCTSTR)f_index; //------------------------ switch(pFrame->key_frame){ case 0: key_frame.Format("No");break; case 1: key_frame.Format("Yes");break; default: key_frame.Format("Unknown"); } switch(pFrame->pict_type){ case 0: pict_type.Format("Unknown");break; case 1: pict_type.Format("I");break; case 2: pict_type.Format("P");break; case 3: pict_type.Format("B");break; case 4: pict_type.Format("S");break; case 5: pict_type.Format("SI");break; case 6: pict_type.Format("SP");break; case 7: pict_type.Format("BI");break; default: pict_type.Format("Unknown"); } reference.Format("%d",pFrame->reference); pts.Format("%d",pFrame->pkt_pts); dts.Format("%d",pFrame->pkt_dts); codednum.Format("%d",pFrame->coded_picture_number); //插入表格------------------------ dlg->vddlg->m_videodecodelist.InsertItem(&lvitem); dlg->vddlg->m_videodecodelist.SetItemText(nIndex,1,pict_type); dlg->vddlg->m_videodecodelist.SetItemText(nIndex,2,key_frame); dlg->vddlg->m_videodecodelist.SetItemText(nIndex,3,codednum); dlg->vddlg->m_videodecodelist.SetItemText(nIndex,4,pts); dlg->vddlg->m_videodecodelist.SendMessage(WM_VSCROLL, SB_BOTTOM, NULL); vframe_index++; return 0; }
音頻碼流分析的函數ffmfc_param_aframe()代碼例如如下:
//音頻幀參數提取 int ffmfc_param_aframe(VideoState *is,AVFrame *pFrame,AVPacket *packet){ //-------------------------------------------------------------------- AVFormatContext *pFormatCtx = is->ic; int audio_stream=is->audio_stream; AVCodecContext *pCodecCtx = pFormatCtx->streams[audio_stream]->codec; //避免數據太多,超過必定量以後,就會清零-------------------------- if(aframe_index>=MAX_FRAME_NUM){ dlg->SystemClear(); } //------------------------------ CString number,packet_size,dts,pts; //--------------- number.Format("%d",aframe_index); //獲取當前記錄條數 int nIndex=dlg->addlg->m_audiodecodelist.GetItemCount(); //「行」數據結構 LV_ITEM lvitem; lvitem.mask=LVIF_TEXT; lvitem.iItem=nIndex; lvitem.iSubItem=0; //注:frame_index不可以直接賦值!//務必使用f_index運行Format!再賦值! lvitem.pszText=(char *)(LPCTSTR)number; //------------------------ packet_size.Format("%d",packet->size); pts.Format("%d",packet->pts); dts.Format("%d",packet->dts); //--------------- dlg->addlg->m_audiodecodelist.InsertItem(&lvitem); dlg->addlg->m_audiodecodelist.SetItemText(nIndex,1,packet_size); dlg->addlg->m_audiodecodelist.SetItemText(nIndex,2,pts); dlg->addlg->m_audiodecodelist.SetItemText(nIndex,3,dts); dlg->addlg->m_audiodecodelist.SendMessage(WM_VSCROLL, SB_BOTTOM, NULL); aframe_index++; return 0; }