main()函数解析

FFplay的主要流程

调用了如下函数

  • av_register_all():注册所有编码器和解码器。

  • show_banner():打印输出FFmpeg版本信息(编译时间,编译选项,类库信息等)。

  • parse_options():解析输入的命令。

  •  SDL_Init():SDL初始化。

  •  stream_open ():打开输入媒体。

  •  event_loop():处理各种消息,不停地循环下去。

FFplay的代码总体结构

parse_options()

parse_options() 解析全部输入选项。

即将输入命令“ffplay -f h264 test.264”中的“-f”这样的命令解析出来。

需要注意的是,FFplay(ffplay.c)的 parse_options()和FFmpeg(ffmpeg.c)中的parse_options()实际上是一样的。

Ffmepg -i aa.mp4 -acodec aac -vcodec libx264 -ss 0 -t 20   -f flv

SDL_Init()

SDL_Init()用于初始化SDL。

FFplay中视频的显示和声音的播放都用到了SDL。

stream_open()

stream_open()的作用是打开输入的媒体。

这个函数还是比较复杂的,包含了FFplay中各种线程的创建。

stream_open()调用了如下函数:

  •  packet_queue_init():初始化各个PacketQueue(视频/音频/字幕)

  •  read_thread():读取媒体信息线程。

  • read_thread()

read_thread()调用了如下函数:

  • avformat_open_input():打开媒体。
  • avformat_find_stream_info():获得媒体信息。
  • av_dump_format():输出媒体信息到控制台。
  • stream_component_open():分别打开视频/音频/字幕解码线程。
  • refresh_thread():视频刷新线程。
  • av_read_frame():获取一帧压缩编码数据(即一个AVPacket)。
  • packet_queue_put():根据压缩编码数据类型的不同(视频/音频/字幕),放到不同的PacketQueue中。

refresh_thread()

refresh_thread()调用了如下函数:

  • SDL_PushEvent(FF_REFRESH_EVENT):发送FF_REFRESH_EVENT的SDL_Event
  • av_usleep():每两次发送之间,间隔一段时间。

stream_component_open()

stream_component_open()用于打开视频/音频/字幕解码的线程。

其函数调用关系如下所示。

stream_component_open()调用了如下函数:

  • avcodec_find_decoder():获得解码器。
  • avcodec_open2():打开解码器。
  • audio_open():打开音频解码。
  • SDL_PauseAudio(0):SDL中播放音频的函数。
  • video_thread():创建视频解码线程。
  • subtitle_thread():创建字幕解码线程。
  • packet_queue_start():初始化PacketQueue。

decoder_start()

开启解码器线程,非常重要的一个函数

audio_open()

audio_open()调用了如下函数

SDL_OpenAudio():SDL 中打开音频设备的函数。

注意它是根据SDL_AudioSpec参数打开音频设备。

SDL_AudioSpec中的callback字段指定了音频播放的 回调函数sdl_audio_callback()。当音频设备需要更多数据的时候,会调用该回调函数。因此该函数是会被反复调用的。

下面来看一下SDL_AudioSpec中指定的回调函数sdl_audio_callback()。

sdl_audio_callback()调用了如下函数

  • audio_decode_frame():解码音频数据
  • update_sample_display():当不显示视频图像,而是显示音频波形的时候,调用此函数。

audio_decode_frame()调用了如下函数

  • packet_queue_get():获取音频压缩编码数据(一个AVPacket)。
  • avcodec_decode_audio4():解码音频压缩编码数据(得到一个AVFrame)。
  • swr_init():初始化libswresample中的SwrContext。libswresample用于音频采样采样数据(PCM)的转换。
  • swr_convert():转换音频采样率到适合系统播放的格式。
  • swr_free():释放SwrContext。

video_thread()

video_thread()调用了如下函数

  • avcodec_alloc_frame():初始化一个AVFrame。
  • get_video_frame():获取一个存储解码后数据的AVFrame。
  • queue_picture()

get_video_frame()调用了如下函数

  • packet_queue_get():获取视频压缩编码数据(一个AVPacket)。
  • avcodec_decode_video2():解码视频压缩编码数据(得到一个AVFrame)。

queue_picture()调用了如下函数

  • SDL_LockYUVOverlay():锁定一个SDL_Overlay。
  • sws_getCachedContext(): 初始化libswscale中的SwsContext。Libswscale用于图像的Raw格式数据(YUV,RGB)之间的转换。注意 sws_getCachedContext()和sws_getContext()功能是一致的。
  • sws_scale():转换图像数据到适合系统播放的格式。
  • SDL_UnlockYUVOverlay():解锁一个SDL_Overlay。

subtitle_thread()调用了如下函数

  • packet_queue_get():获取字幕压缩编码数据(一个AVPacket)。
  • avcodec_decode_subtitle2():解码字幕压缩编码数据。

event_loop()

FFplay再打开媒体之后,便会进入event_loop()函数,永远不停的循环下去。

该函数用于接收并处理各种各样的消息。

有点像Windows的消息循环机制。

PS:该循环确实是无止尽的,其形式为如下

SDL_Event event;
for (;;) {  SDL_WaitEvent(&event);  switch (event.type) {  case SDLK_ESCAPE:  case SDLK_q:  do_exit(cur_stream);  break;  case SDLK_f:  …  …  }
}

event_loop()函数调用关系:

根据event_loop()中SDL_WaitEvent()接收到的SDL_Event类型的不同,会调用不同的函数进行处理(从编程的角度来说就是一个switch()语法)。

仅仅列举了几个例子:

SDLK_ESCAPE(按下“ESC”键):do_exit()。退出程序。

SDLK_f(按下“f”键):toggle_full_screen()。切换全屏显示。

SDLK_SPACE(按下“空格”键):toggle_pause()。切换“暂停”。

SDLK_DOWN(按下鼠标键):stream_seek()。跳转到指定的时间点播放。

SDL_VIDEORESIZE(窗口大小发生变化):SDL_SetVideoMode()。重新设置宽高。

FF_REFRESH_EVENT(视频刷新事件(自定义事件)):video_refresh()。刷新视频

do_exit()

下面分析一下do_exit()函数。该函数用于退出程序。

函数的调用关系如下所示。

  • do_exit()函数调用了以下函数
  • stream_close():关闭打开的媒体。
  • SDL_Quit():关闭SDL。

stream_close()

stream_close()函数调用了以下函数

  • packet_queue_destroy():释放PacketQueue。
  • SDL_FreeYUVOverlay():释放SDL_Overlay。
  • sws_freeContext():释放SwsContext。

video_refresh()

下面重点分析video_refresh()函数。

该函数用于将图像显示到显示器上。

函数的调用关系如下所示。

video_refresh()函数调用了以下函数

  • video_display():显示像素数据到屏幕上。
  • show_status:这算不上是一个函数,但是是一个独立的功能模块,因此列了出来。该部分打印输出播放的状态至屏幕上。如下图所示。

video_display()函数调用了以下函数

  • video_open():初始化的时候调用,打开播放窗口。
  • video_audio_display():显示音频波形图(或者频谱图)的时候调用。里面包含了不少画图操作。
  • video_image_display():显示视频画面的时候调用。

video_open()函数调用了以下函数

  • SDL_SetVideoMode():设置SDL_Surface(即SDL最基础的黑色的框)的大小
  • SDL_WM_SetCaption():设置SDL_Surface对应窗口的标题文字。

音视频同步

视频帧的播放时间其实依赖pts字段的,音频和视频都有自己单独的pts。

但pts究竟是如何生成的呢,假如音视频不同步时,pts是否需要动态调整,以保证音视频的同步?

下面先来分析,如何控制视频帧的显示时间的:

static void video_refresh(void *opaque){//根据索引获取当前需要显示的VideoPictureVideoPicture *vp = &is->pictq[is->pictq_rindex];if (is->paused)goto display; //只有在paused的情况下,才播放图像// 将当前帧的pts减去上一帧的pts,得到中间时间差last_duration = vp->pts - is->frame_last_pts;//检查差值是否在合理范围内,因为两个连续帧pts的时间差,不应该太大或太小if (last_duration > 0 && last_duration < 10.0) {/* if duration of the last frame was sane, update last_duration in video state */is->frame_last_duration = last_duration;}//既然要音视频同步,肯定要以视频或音频为参考标准,然后控制延时来保证音视频的同步,//这个函数就做这个事情了,下面会有分析,具体是如何做到的。delay = compute_target_delay(is->frame_last_duration, is);//获取当前时间time= av_gettime()/1000000.0;//假如当前时间小于frame_timer + delay,也就是这帧改显示的时间超前,还没到,就直接返回if (time < is->frame_timer + delay)return;//根据音频时钟,只要需要延时,即delay大于0,就需要更新累加到frame_timer当中。if (delay > 0)//更新frame_timer,frame_time是delay的累加值is->frame_timer += delay * FFMAX(1, floor((time-is->frame_timer) / delay));SDL_LockMutex(is->pictq_mutex);//更新is当中当前帧的pts,比如video_current_pts、video_current_pos 等变量update_video_pts(is, vp->pts, vp->pos);SDL_UnlockMutex(is->pictq_mutex);display:/* display picture */if (!display_disable)video_display(is);
}

函数compute_target_delay根据音频的时钟信号,重新计算了延时,从而达到了根据音频来调整视频的显示时间,从而实现音视频同步的效果。

static double compute_target_delay(double delay, VideoState *is)
{double sync_threshold, diff;//因为音频是采样数据,有固定的采用周期并且依赖于主系统时钟,要调整音频的延时播放较难控制。所以实际场合中视频同步音频相比音频同步视频实现起来更容易。if (((is->av_sync_type == AV_SYNC_AUDIO_MASTER && is->audio_st) ||is->av_sync_type == AV_SYNC_EXTERNAL_CLOCK)) {//获取当前视频帧播放的时间,与系统主时钟时间相减得到差值diff = get_video_clock(is) - get_master_clock(is);sync_threshold = FFMAX(AV_SYNC_THRESHOLD, delay);//假如当前帧的播放时间,也就是pts,滞后于主时钟if (fabs(diff) < AV_NOSYNC_THRESHOLD) {if (diff <= -sync_threshold)delay = 0;//假如当前帧的播放时间,也就是pts,超前于主时钟,那就需要加大延时else if (diff >= sync_threshold)delay = 2 * delay;}}return delay;
}

如何控制视频的播放和暂停?

static void stream_toggle_pause(VideoState *is)
{if (is->paused) {//由于frame_timer记下来视频从开始播放到当前帧播放的时间,所以暂停后,必须要将暂停的时间( is->video_current_pts_drift - is->video_current_pts)一起累加起来,并加上drift时间。is->frame_timer += av_gettime() / 1000000.0 + is->video_current_pts_drift - is->video_current_pts;if (is->read_pause_return != AVERROR(ENOSYS)) {//并更新video_current_ptsis->video_current_pts = is->video_current_pts_drift + av_gettime() / 1000000.0;}//drift其实就是当前帧的pts和当前时间的时间差is->video_current_pts_drift = is->video_current_pts - av_gettime() / 1000000.0;}//paused取反,paused标志位也会控制到图像帧的展示,按一次空格键实现暂停,再按一次就实现播放了。is->paused = !is->paused;
}

特别说明:paused标志位控制着视频是否播放,当需要继续播放的时候,一定要重新更新当前所需要播放帧的pts时间,因为这里面要加上已经暂停的时间。

逐帧播放是如何做的?

在视频解码线程中,不断通过stream_toggle_paused,控制对视频的暂停和显示,从而实现逐帧播放:

static void step_to_next_frame(VideoState *is)
{//逐帧播放时,一定要先继续播放,然后再设置step变量,控制逐帧播放if (is->paused)stream_toggle_pause(is);//会不断将paused进行取反is->step = 1;
}

其原理就是不断的播放,然后暂停,从而实现逐帧播放:

static int video_thread(void *arg)
{if (is->step)stream_toggle_pause(is);……………………if (is->paused)goto display;//显示视频}
}

FFmpeg入门详解之52:ffplay源码分析相关推荐

  1. Java源码详解六:ConcurrentHashMap源码分析--openjdk java 11源码

    文章目录 注释 类的继承与实现 数据的存储 构造函数 哈希 put get 扩容 本系列是Java详解,专栏地址:Java源码分析 ConcurrentHashMap 官方文档:ConcurrentH ...

  2. Java源码详解四:String源码分析--openjdk java 11源码

    文章目录 注释 类的继承 数据的存储 构造函数 charAt函数 equals函数 hashCode函数 indexOf函数 intern函数 本系列是Java详解,专栏地址:Java源码分析 Str ...

  3. Java源码详解五:ArrayList源码分析--openjdk java 11源码

    文章目录 注释 类的继承与实现 构造函数 add操作 扩容函数 remove函数 subList函数 总结 本系列是Java详解,专栏地址:Java源码分析 ArrayList 官方文档:ArrayL ...

  4. Java源码详解三:Hashtable源码分析--openjdk java 11源码

    文章目录 注释 哈希算法与映射 线程安全的实现方法 put 操作 get操作 本系列是Java详解,专栏地址:Java源码分析 Hashtable官方文档:Hashtable (Java Platfo ...

  5. Java源码详解二:HashMap源码分析--openjdk java 11源码

    文章目录 HashMap.java介绍 1.HashMap的get和put操作平均时间复杂度和最坏时间复杂度 2.为什么链表长度超过8才转换为红黑树 3.红黑树中的节点如何排序 本系列是Java详解, ...

  6. FFmpeg入门详解之104:Win10快速安装OpenSSL(不用编译源码)

    win10x64安装OpenSSL:不用编译源码 网上一堆的教程教你去下载opensll源代码并用vs进行编译,虽然我有安装vs2015,但是看到那么长的编译步骤,太心累了. 懒人只得找简单的方法,最 ...

  7. FFmpeg入门详解之83:流媒体与直播技术

    流媒体 流媒体又叫流式媒体,它是指商家用一个视频传送服务器(比如:vlc)把节目(比如:ande10.mp4)当成数据包发出,传送到网络上.用户通过解压设备对这些数据进行解压后,节目就会像发送前那样显 ...

  8. FFmpeg入门详解之117:视频监控的架构和流程

    几张架构图带您快速了解视频监控 图一 图二 图三 图四 视频监控系统的简介 视频监控 视频监控是安全防范系统的重要组成部分,英文Cameras and Surveillance.传统的监控系统包括前端 ...

  9. FFmpeg入门详解之122:Qt5 FFmpeg本地摄像头采集预览实战

    6.Qt5+FFmpeg本地摄像头采集预览实战 源码工程:S26_Test2 FFmpeg命令行处理摄像头 ffmpeg -list_devices true -f dshow -i dummy 命令 ...

  10. FFmpeg入门详解之119:FFmpeg的SDK编程回顾总结并操练

    3.FFmpeg的SDK编程回顾总结并操练 参考课程:"FFmpeg4.3--系列5--SDK二次开发详解与实战" FFmpeg主要框架 FFmpeg骨架:"八大金刚&q ...

最新文章

  1. 使用postman修改SAP Marketing Cloud contact主数据
  2. TensorFlow文本摘要生成 - 基于注意力的序列到序列模型
  3. OSPF分解试验部分-LAB3:OSPF各种网络类型试验
  4. 学习笔记Hadoop(十)—— Hadoop基础操作(2)—— HDFS常用Shell操作
  5. Swift游戏实战-跑酷熊猫 00 游戏预览
  6. Kubernetes客户端client-go简介
  7. 体育计算机培训心得,体育网络培训心得
  8. H3C OSPF基本配置命令
  9. 建筑工程计算机的应用,计算机对建筑工程的应用
  10. 实战ItemTouchHelper仿网易新闻客户端自定义栏目页面
  11. VScode+latex+Sumatra PDF环境配置(步步到位)
  12. 智源社区周刊:Gary Marcus谈大模型研究可借鉴的三个因素;OpenAI提出视频预训练模型VPT,可玩MC游戏...
  13. gorm中使用where in 条件
  14. 数据挖掘中的机器学习
  15. AirDisk存宝 【S3\S6简易使用说明】
  16. sql查询 (连表查询)
  17. 工作必备Git保姆级教学
  18. 基于Dragonboard410c的智能音箱(四)
  19. Gitkraken收费问题解决
  20. 索尼xz Android 内存,首发Android 9.0索尼新旗舰XZ2P曝光:搭载骁龙845处理器和6GB内存...

热门文章

  1. 简单数据查询——单表无条件和有条件查询
  2. 《MBA一日读2.0 读书笔记》
  3. 机器学习大牛李飞飞的电脑配置
  4. win10进不去计算机配置,Win10电脑系统设置打不开的解决方法
  5. 彩虹六号按键精灵挂机脚本制作教程
  6. 广州博森科技有限公司外汇自动交易软件,免费下载试用
  7. 二维码生成(js实现)
  8. SpringBoot 中如何使用JSP页面开发?
  9. mysql数据库技术与应用微课版 pdf_MySQL数据库原理与应用(微课版)
  10. 白帽子讲web安全笔记-xss总结