在上传一个开源播放器项目ffplay for mfc。它会ffmpeg工程ffplay媒体播放器(ffplay.c)移植到VC环境,而使用MFC做一套接口。它可以完成一个播放器播放的基本流程的视频:解决方案协议。解包,视频/音频解码,AV同步。视频和音频输出。
此外还包含一些控制功能:播放,暂停/继续,前进,后退,停止,逐帧播放,全屏等;以及一些码流分析功能:视频解码分析和音频解码分析。
具体的软件使用就不细致介绍了,本文简介当中比較重要的模块的流程。以防长时间不看的话忘了~
软件信息:
SourceForge项目主页:
1. 软件结构
软件结构如图1所看到的。包含例如以下模块:控制,视频播放。參数提取。码流分析。当中,视频播放模块用于视频的解码和播放;控制模块用于控制视频的播放。參数提取模块用于提取显示视频的各种參数;码流分析模块伴随着视频的播放分析视音频流中的參数。
图1.软件结构
2.模块说明
2.1. 视频播放模块
视频播放模块的作用就是将网络上(或者是本地)的视音频数据接收下来经过一系列处理后终于输出到视音频设备上。依据处理的顺序不同,它能够分为下面几个子模块:
1) 解协议模块
2) 解封装模块
3) 视频解码模块
4) 音频解码模块
5) 视音频同步模块
视频播放模块的流程图如图2所看到的。依照处理的顺序分为解协议,解封装,视频解码,音频解码,视音频同步。
具体的原理在文章 中有具体的说明,在这里不再反复,示意图例如以下所看到的。
图2.视频播放模块流程
这一模块主要是通过对ffplay.c改写而得到的。
改写完毕后为ffplaycore.cpp。
简单的代码方面的流程能够參考:
比較完整的代码方面的流程能够參考:
2.2. 控制模块
控制模块的作用就是控制视频的播放。包含下面几种功能:
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 Barvoid ffmfc_seek_bar(int pos){ SDL_Event event; event.type = FFMFC_SEEK_BAR_EVENT; seek_bar_pos=pos; SDL_PushEvent(&event);}MFC中按钮,进度条控件的消息响应函数仅仅要调用以上功能函数就能够实现相应的功能:
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();}
2.3.參数提取模块
參数提取模块的作用就是提取视频码流中的一部分參数。
依照參数种类的不同。分为封装格式參数。视频编码參数。音频编码參数。
(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;}
2.4. 码流分析模块
码流分析模块在视频播放过程中,伴随着视频的解码。分析当中的视音频參数。
能够分为视频码流分析模块和音频码流分析模块。
(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; }
版权声明:本文博主原创文章,博客,未经同意不得转载。