当前位置: 首页 > news >正文

ffplay6 播放器关键技术点分析 1/2


author: hjjdebug
date: 2025年 07月 05日 星期六 20:20:37 CST
descrip: ffplay6 播放器关键技术点分析 1/2


文章目录

  • 一: 视频是如何显示的.
    • 1: refresh_loop_wait_event 代码注释
    • 2: video_refresh 代码注释
    • 3. 分析调用到video_display的条件
  • 二: 如何控制的视频播放速率?
    • 1. 分析 queue_picture 函数,
    • 2. video_thread 是一个线程,负责视频解码,转换,并推入帧队列
    • 3: 审查video_display
    • 4: 审查read_thread.
    • 5.分析read_thread 调用wait 的条件:
    • 6. 如果f_pictq 不消耗, 会发生什么?
    • 7. 如果f_pictq 消耗光了会怎样?
  • 三. 音频是如何播放的?
    • 1. read_thread 会把读取的音频包送入音频queue
    • 2. 分析audio_thread 代码
    • 3. push 进的is->f_sampq 如何被消费?
  • 四. 音视频是如何同步的?
    • 1.补充计算delay时间的代码

描述中对原代码中的一些引用有少许改变,例如 is->f_pictq, 原代码用的是is->pictq.
由于它是FrameQueue 类型而不是PacketQueue 类型, 所以我把源码统一改成了f_pictq,
为了读代码时一眼就知道它的类型.

一: 视频是如何显示的.

主函数main,或者说主线程调用了video_refresh 函数
main->event_loop->refresh_loop_wait_event->video_refresh
循环中等待时间是0.01s, 理论值能达到100hz/s 的扫描频率,即1秒钟调用100次video_refresh

1: refresh_loop_wait_event 代码注释

//获取事件并刷新显示

static void refresh_loop_wait_event(VideoState* is, SDL_Event* event)
{double remaining_time = 0.0;SDL_PumpEvents();while (!SDL_PeepEvents(event, 1, SDL_GETEVENT, SDL_FIRSTEVENT, SDL_LASTEVENT)){   //没有事件发生,就执行视频刷新if (!cursor_hidden && av_gettime_relative() - cursor_last_shown > CURSOR_HIDE_DELAY){ //CURSOR_HIDE_DELAY 刷新时间 1000000usSDL_ShowCursor(0);cursor_hidden = 1;}if (remaining_time > 0.0)av_usleep((int64_t)(remaining_time * 1000000.0));  //这里会等待remaining_time = REFRESH_RATE; //0.01s ***** 这里是关键,控制了刷新频率if (is->v_show_mode != SHOW_MODE_NONE && (!is->paused || is->force_refresh))video_refresh(is, &remaining_time);SDL_PumpEvents();}
}

2: video_refresh 代码注释

有删减! 考虑的视频同步到音频情况. 即AV_SYNC_AUDIO_MASTER

void video_refresh(void* opaque, double* remaining_time)
{VideoState* is = opaque;double time;if (is->video_st) //没有视频流,肯定就不用更新画面了.{retry:if (frame_queue_nb_remaining(&is->f_pictq) == 0) //没有frame要处理了,什么都不做{// nothing to do, no picture to display in the queue}else{  //还有事要做double last_duration, duration, delay;Frame *vp, *lastvp;/* dequeue the picture */lastvp = frame_queue_peek_last(&is->f_pictq); // 获取上一帧:上次已显示的帧vp = frame_queue_peek(&is->f_pictq);   // 获取当前帧:当前待显示的帧if (vp->serial != is->p_videoq.serial) //如果序列号不对,要继续取下一个.{frame_queue_next(&is->f_pictq);goto retry;}// 一个seek会开始一个新播放序列,lastvp和vp不是同一播放序列,将frame_timer更新为当前时间if (lastvp->serial != vp->serial)is->frame_timer = av_gettime_relative() / 1000000.0;// 暂停处理:暂停就是跳走, 不会更新显示,因为条件force_refresh 不满足,名字起得不好.// 改为 goto exit_display 意义较好, 在is->force_refresh = 0; 处加一个exit_display标号if (is->paused)goto display; //实际意义是跳出,不显示last_duration = vp_duration(is, lastvp, vp); // 上一帧播放时长:vp->pts - lastvp->pts,一般是0.04s// 根据视频时钟和同步时钟(音频时钟)的差值,修正last_duration为delay值// 后面有该函数的分析delay = compute_target_delay(last_duration, is);  time = av_gettime_relative() / 1000000.0; //当前时间if (time < is->frame_timer + delay) //is->frame_timer+delay 是当前帧期望的播放时间{  //时间未到,更新remaining_time, 跳走*remaining_time = FFMIN(is->frame_timer + delay - time, *remaining_time);goto display; //跳走,不显示}is->frame_timer += delay;             // 更新frame_timer值//若frame_timer落后于当前系统时间太久(超过最大同步域值),则更新为当前系统时间if (delay > 0 && time - is->frame_timer > AV_SYNC_THRESHOLD_MAX)is->frame_timer = time;SDL_LockMutex(is->f_pictq.mutex);if (!isnan(vp->pts))update_video_pts(is, vp->pts, vp->serial); // 更新视频时钟:时间戳、时钟序列号SDL_UnlockMutex(is->f_pictq.mutex);if (frame_queue_nb_remaining(&is->f_pictq) > 1) // 队列中未显示帧数>1(只有一帧则不考虑丢帧,不过最多也就3帧){Frame* nextvp = frame_queue_peek_next(&is->f_pictq); //获取下一待显示帧duration = vp_duration(is, vp, nextvp); //得到当前时长if (!is->step  //非步进模式&& (framedrop > 0  //framedrop 模式生效, 默认-1,不生效|| (framedrop //虽然它是-1, 但master_sync_type 不是视频是音频,条件满足&& get_master_sync_type(is) != AV_SYNC_VIDEO_MASTER)) && time > is->frame_timer + duration) //当前时间已经超过下一帧期望的播放时间{is->frame_drops_late++;   //丢帧个数加1frame_queue_next(&is->f_pictq); //指针挪向下一个,跳过了该帧,重新retrygoto retry;}}if (is->subtitle_st){ 字幕处理 }frame_queue_next(&is->f_pictq); //指针前移,fq->size--is->force_refresh = 1; //强制刷新if (is->step && !is->paused) //如果是单步模式并且在播放,toggle_pause;stream_toggle_pause(is);}display:/* display picture */if (!display_disable && is->force_refresh && is->v_show_mode == SHOW_MODE_VIDEO && is->f_pictq.rindex_shown)video_display(is); //条件满足时,获取的时上一帧数据来显示,调用video_image_display更新texture,renderer}is->force_refresh = 0;if (show_status){在控制台打印信息 }
}

当is->video_st 还没有建立时,肯定更新不了画面.
is->video_st 是视频流,它是read_thread 调用stream_component_open(is, st_index[AVMEDIA_TYPE_VIDEO) 来建立的
stream_component_open还会启动video_decoder 线程.

3. 分析调用到video_display的条件

满足若干个条件后, video_refresh才能调用到video_display,代码如下:

if (!display_disable
&& is->force_refresh
&& is->v_show_mode == SHOW_MODE_VIDEO
&& is->f_pictq.rindex_shown)video_display(is);
  1. display_disable: 是个全局变量,默认是0,通过命令行选项可以改变该初始值. 条件满足

  2. is->force_refresh
    这是VideoState is实例的一个成员变量, 在stream_open(char *filename) 时, 返回来一个VideoState 实例 is,
    它默认未明确赋值的变量都是0.
    is = av_mallocz(sizeof(VideoState));
    is->force_refresh何时变成真的?
    a:控制台命令可以改变它. 全屏及恢复,显示窗口移动会把is->force_refresh 置1, show_mode改变也会置1
    b: 在video_refresh中
    当frame_queue中添加好了数据,当把指针向下移动之后,设置了force_refresh=1
    frame_queue_next(&is->f_pictq);
    is->force_refresh = 1;
    is->force_refresh 何时变为假?
    video_refresh 每执行一次,总是把 is->force_refresh置为0
    is->force_refresh = 0;

  3. is->v_show_mode 分析
    在read_thread 中初始化部分,会用show_mode 初始化is->v_show_mode, 全局变量show_mode默认是SHOW_MODE_NONE
    is->v_show_mode = show_mode;
    继续初始化,当打开视频流后,如果is->v_show_mode == SHOW_MODE_NONE, 会改为SHOW_MODE_VIDEO,否则SHOW_MODE_RDFT
    if (is->v_show_mode == SHOW_MODE_NONE)
    is->v_show_mode = ret >= 0 ? SHOW_MODE_VIDEO : SHOW_MODE_RDFT;

  4. is->f_pictq.rindex_shown 分析
    rindex_shown 是FrameQueue 的一个成员变量, 其初始化值: 靠frame_queue_init 来确定.
    frame_queue_init(&is->f_pictq, &is->p_videoq, VIDEO_PICTURE_QUEUE_SIZE, 1);
    未明确赋值的成员默认都是0
    memset(fq, 0, sizeof(FrameQueue));
    何时变成真?
    当调用frame_queue_next 时, 只要fq->keep_last, fq->rindex_shown如果为0,就变成1.
    if (fq->keep_last && !fq->rindex_shown)
    {
    fq->rindex_shown = 1;
    return;
    }
    视频fq 初始化时keep_last 是1, 所以一旦有了frame, 就会调用frame_queue_next, 就会置位该变量. 不会再变为0了.

所以4个条件正常运行时实际起作用是一个条件控制 force_refresh, 当有了frame,调用frame_queue_next 就会置位force_refresh

二: 如何控制的视频播放速率?

因为video_refresh每秒调用100次,看起来一旦得到了帧数据,就会置位force_refresh,就会被刷新.
那何时得到的帧数据?

1. 分析 queue_picture 函数,

它负责不断向FrameQueue中填充帧
vp 指向framequeue 中的一个Frame
构建出一个Frame, 调用push 使f_pictq 的size++, 其中一个重要参数frame_out 是被解出的AVFrame
av_frame_move_ref(vp->frame, frame_out); //把从过滤器拉出的AVFrame(frame_out) 挪到 vp->frame下
frame_queue_push(&is->f_pictq);

frame_out是从filter 中拉出的. 名称被我从frame改成了frame_out,使得意义明确.
ret = av_buffersink_get_frame_flags(filt_out, frame_out, 0)

2. video_thread 是一个线程,负责视频解码,转换,并推入帧队列

在video_thread 的循环中,不断的获取frame, filter_in, filter_out,queue_picture过程,把packet 变成frame,变换并推入帧队列.

  for (;;){ret = get_video_frame(is, frame);ret = av_buffersrc_add_frame(filt_in, frame);ret = av_buffersink_get_frame_flags(filt_out, frame_out, 0);ret = queue_picture(is, frame_out, pts, duration, fd ? fd->pkt_pos : -1, is->viddec.pkt_serial);}

其中get_video_frame 会调用decoder_decode_frame->packet_queue_get 获取包
然后调用avcodec_send_packet,avcodec_receive_frame获取frame

据此,也可以看出,解码,变化,保存是没有延时的.

3: 审查video_display

由于 is->v_show_mode == SHOW_MODE_VIDEO 会调用
video_image_display(is);
video_image_display 直接向texture 提交…
ret = SDL_UpdateYUVTexture(*tex, …
SDL_RenderCopyEx(renderer, is->vid_texture,
SDL_RenderPresent(renderer);

4: 审查read_thread.

它有一个循环for(;;) 核心是
	for(;;){//当queue 已经满了的时候,就不要再读了, 这是关键!!! 整个运行节奏控制全在这了.if (infinite_buffer < 1&& (is->p_audioq.size + is->p_videoq.size + is->p_subtitleq.size > MAX_QUEUE_SIZE|| (stream_has_enough_packets(is->audio_st, is->audio_stream, &is->p_audioq)&& stream_has_enough_packets(is->video_st, is->video_stream, &is->p_videoq)&& stream_has_enough_packets(is->subtitle_st, is->subtitle_stream, &is->p_subtitleq)))){/* wait 10 ms */SDL_LockMutex(wait_mutex);SDL_CondWaitTimeout(is->continue_read_cond, wait_mutex, 10);SDL_UnlockMutex(wait_mutex);continue;}ret = av_read_frame(ic, pkt);switch(pkt->stream_index){case video_index:packet_queue_put(&is->p_videoq, pkt);break;case audio_index:packet_queue_put(&is->p_audioq, pkt);break;case substitle_index:packet_queue_put(&is->p_subtitleq, pkt);break;default:break;}
}

5.分析read_thread 调用wait 的条件:

SDL_CondWaitTimeout(is->continue_read_cond, wait_mutex, 10);
a. infinite_buffer < 1
infinite_buffer 是一个全局变量,初始化为-1,可以通过命令行设置为0或1.
所以默认是-1,条件满足. 无穷大缓存不是真.
b. 一堆条件的复合语句
音频包+视频包+字幕包大小已经大于最大队列尺寸(15M)
或者
有了足够多的packet(音频,视频,字幕) 超过25个packet 为真, 没有流也算真等等
就是说读的足够多了会停下来!

所以从以上分析并没有得出帧率是如何被控制的,只知道如果显示帧不被消耗,则read_thread 会停下来.
SDL_CondWaitTimeout(is->continue_read_cond, wait_mutex, 10);

6. 如果f_pictq 不消耗, 会发生什么?

此时,
frame_queue_peek_writable(f_pictq) 会被阻塞.
while (fq->size >= fq->max_size && !fq->pktq->abort_request)
{
SDL_CondWait(fq->cond, fq->mutex); // 阻塞在fq->cond 上,
}
视频队列的max_size是 3, 音频队列max_size 是9, 都是很小的值
被阻塞的是video_thread 线程, video_thread->queue_picture->frame_queue_peek_writable
同理 audio_thread 也会调用自己的frame_queue_peek_writable, 满了会被阻塞.
audio,video thread 阻塞以后, packet 就不能被消耗,导致read_thread因存了足够多包以后会停下来.

7. 如果f_pictq 消耗光了会怎样?

例如主线程中调用的video_refresh, 没有视频帧什么都不做
if (frame_queue_nb_remaining(&is->f_pictq) == 0)
{
// nothing to do, no picture to display in the queue
}

三. 音频是如何播放的?

1. read_thread 会把读取的音频包送入音频queue

packet_queue_put(&is->p_audioq, pkt);

谁会调用packet_queue_get 呢?
decoder 会调用
packet_queue_get(d->pkt_queue, d->pkt, 1, &d->pkt_serial);
1代表拿不到pkt就阻塞
这是在decoder_decode_frame 函数中
int decoder_decode_frame(Decoder* d, AVFrame* frame, AVSubtitle* sub)
audio_thread, video_thread 都会调用该函数, 他们的任务就是要解出frame来并推入frame队列

2. 分析audio_thread 代码

表示可以看懂.
调用decoder_decode_frame 解出AVFrame frame,
用过滤器过滤进行转换,再拉出frame,
查看音频FrameQueue 状态:
if (!(af = frame_queue_peek_writable(&is->f_sampq)))
goto the_end;
没有空间,不可写会等待, 如果已放弃, 则返回空,
if (fq->pktq->abort_request)
return NULL;
af==NULL 会跳到结尾.

有空间了返回地址指针Frame *af, 组织数据后,保存到队列
av_frame_move_ref(af->frame, frame);
frame_queue_push(&is->f_sampq);
为啥push函数仅一个参数,对象自己, af起什么作用? 原来FrameQueue 就是一个固定数组,af就是它的一个地址
数据都操作好了,push 只是把size++

3. push 进的is->f_sampq 如何被消费?

最多只有9个sample.
原来是在audio_decode_frame函数中被消费, 名字起的不好,改为audio_resample_frame更好!
前面已经有一个decoder_decode_frame名字了.
a. audio_resample_frame 函数分析
首先它要从is->f_sampq 中取到数据. 如果数据与sdl wanted 格式不一样,会在这里发生重采样.
如果不重采样,那就直接使用frame 的数据了.
is->audio_buf = af->frame->data[0];
resampled_data_size = data_size;
return resampled_data_size;

b. 谁调用audio_resammple_frame?
sdl_audio_callback();
该函数是sdl 音频的回调函数, sdl 按它自己的节奏播放音频数据,当需要新数据时,调用该回调函数.
要求填充固定长度的数据.
该函数首先看看缓冲中有没有数据,没有就要解出来一个frame数据来使用,调用的就是audio_resammple_frame
设定buffer的地址和大小指向该frame, 用 SDL_MixAudioForma 把数据copy走指定的len, 调整数据指针
及剩余大小供下次调用使用.

四. 音视频是如何同步的?

sdl 的音频是按自己的节奏播放的.(当然是初始化时调用audio_open来设置的wanted_spec)
它控制了数据消费的时间. 这样音频队列充满了就会等待.阻塞了音频解码线程并进一步阻塞read_thread
音视频包是交错的. 音频包不会太多,则视频包也不会太多,节奏得以控制.

精确控制:
精准的音视频同步需要时钟的参与, 请看前面强力标注video_refresh() 代码

1.补充计算delay时间的代码

static double compute_target_delay(double delay, VideoState* is)
{double sync_threshold, diff = 0;if (get_master_sync_type(is) != AV_SYNC_VIDEO_MASTER)  //默认AV_SYNC_AUDIO_MASTER{diff = get_clock(&is->vidclk) - get_master_clock(is);  //关键代码!! 计算音频时钟和视频时钟的偏差. 时钟的概念请参考另一篇博客//AV_SYNC_THRESHOLD_MIN=0.04, AV_SYNC_THRESHOLD_MAX=0.1 delay一般是0.04, 所以sync_threshold一般是0.04sync_threshold = FFMAX(AV_SYNC_THRESHOLD_MIN, FFMIN(AV_SYNC_THRESHOLD_MAX, delay));if (!isnan(diff) && fabs(diff) < is->max_frame_duration) //时钟偏差不大时{  //修正delay 算法if (diff <= -sync_threshold) //diff很小delay = FFMAX(0, delay + diff); //delay 加diff负数但不能小于0else if (diff >= sync_threshold && delay > AV_SYNC_FRAMEDUP_THRESHOLD)delay = delay + diff; //diff 很大,delay+diffelse if (diff >= sync_threshold) //diff较大, 把delay加倍delay = 2 * delay;}}return delay;  // 返回delay, diff 很小时delay不会改变
}
http://www.lryc.cn/news/581278.html

相关文章:

  • Windows内核并发优化
  • rk3128 emmc显示剩余容量为0
  • 深度学习5(深层神经网络 + 参数和超参数)
  • 力扣网编程55题:跳跃游戏之逆向思维
  • 前端相关性能优化笔记
  • Python数据容器-list和tuple
  • 四、jenkins自动构建和设置邮箱
  • PHP语法基础篇(九):正则表达式
  • CppCon 2018 学习:Smart References
  • 有限状态机(Finite State Machine)
  • 相机位姿估计
  • 2 大模型高效参数微调;prompt tunning
  • 【Linux】自旋锁和读写锁
  • 全素山药开发指南:从防痒处理到高可用食谱架构
  • DeepSeek扫雷游戏网页版HTML5(附源码)
  • C#指针:解锁内存操作的底层密码
  • 机械时代的计算
  • 【Linux】常用基本指令
  • 爬虫工程师Chrome开发者工具简单介绍
  • 推荐算法系统系列五>推荐算法CF协同过滤用户行为挖掘(itembase+userbase)
  • Python实例题:基于 Python 的简单电子词典
  • 洛谷刷题9
  • Django中关于templates目录和static目录存放位置的总结
  • Django跨域
  • python使用fastmcp包编写mcp服务端(mcp_server)和mcp客户端(mcp_client)
  • jxWebUI--用数据表输入输出数据
  • 前端进阶之路-从传统前端到VUE-JS(第三期-VUE-JS配套UI组件的选择)(Element Plus的构建)
  • SQL 表结构转 Go、Java、TS 自定义实体类,支持自编模板
  • 学习日志04 python
  • 解决kali Linux在VMware中的全局缩放问题