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);
-
display_disable: 是个全局变量,默认是0,通过命令行选项可以改变该初始值. 条件满足
-
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; -
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; -
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不会改变
}