本篇来聊聊视频的同步显示。
video_refresh
函数名虽然叫视频刷新,但其实干的却是时钟同步的勾当。真正的视频渲染是在video_display中完成的。
外部时钟
函数第一处逻辑是更新外部时钟:
// 条件一:正常播放状态
// 条件二:外部时钟为主时钟
// 条件三:播放的是实时流
if (!is->paused && get_master_sync_type(is) == AV_SYNC_EXTERNAL_CLOCK && is->realtime)
check_external_clock_speed(is);我们看下这个外部时钟函数都干了点啥:
staticvoidcheck_external_clock_speed(VideoState *is) {
// 当音频流和视频流的包都小于最小门限(EXTERNAL_CLOCK_MIN_FRAMES)时,也就是当前缓冲不足了
// 此时要调慢时钟,让播放变慢些以保持播放流畅性。
if (is->video_stream >= 0 && is->videoq.nb_packets <= EXTERNAL_CLOCK_MIN_FRAMES ||
is->audio_stream >= 0 && is->audioq.nb_packets <= EXTERNAL_CLOCK_MIN_FRAMES) {
set_clock_speed(&is->extclk, FFMAX(EXTERNAL_CLOCK_SPEED_MIN, is->extclk.speed - EXTERNAL_CLOCK_SPEED_STEP));
// 当前缓冲很充足,此时要调快时钟,播放速度加快,加快缓冲包的消耗防止堆积。
} elseif ((is->video_stream < 0 || is->videoq.nb_packets > EXTERNAL_CLOCK_MAX_FRAMES) &&
(is->audio_stream < 0 || is->audioq.nb_packets > EXTERNAL_CLOCK_MAX_FRAMES)) {
set_clock_speed(&is->extclk, FFMIN(EXTERNAL_CLOCK_SPEED_MAX, is->extclk.speed + EXTERNAL_CLOCK_SPEED_STEP));
} else {
double speed = is->extclk.speed;
// 在正常区间内时,让时钟回归到1倍速中。
if (speed != 1.0)
set_clock_speed(&is->extclk, speed + EXTERNAL_CLOCK_SPEED_STEP * (1.0 - speed) / fabs(1.0 - speed));
}
}非视频模式
没有视频流,仅有音频流时,显示音频频谱/波形
// 仅播放音频的情况,显示音频频谱
if (!display_disable && is->show_mode != SHOW_MODE_VIDEO && is->audio_st) {
time = av_gettime_relative() / 1000000.0;
// 显示频谱间隔 rdftspeed(默认20ms)
if (is->force_refresh || is->last_vis_time + rdftspeed < time) {
video_display(is);
is->last_vis_time = time;
}
// 计算事件循环中应等待的时间
*remaining_time = FFMIN(*remaining_time, is->last_vis_time + rdftspeed - time);
}视频同步
首先要判断有无可播放的视频帧,没有则无事发生。
if (frame_queue_nb_remaining(&is->pictq) == 0) {
// nothing to do, no picture to display in the queue
} else {计算帧显示时间间隔
// 拿上一帧和当前帧
// lastvp上一帧是正在显示的帧;vp当前帧是待显示的帧。
lastvp = frame_queue_peek_last(&is->pictq);
vp = frame_queue_peek(&is->pictq);
// 序列换了:要么是视频源换了,要么被快进等操作导致的非连续播放。
if (vp->serial != is->videoq.serial) {
frame_queue_next(&is->pictq);
goto retry;
}
// 重置当前帧的时间戳
if (lastvp->serial != vp->serial)
is->frame_timer = av_gettime_relative() / 1000000.0;
// 如果被暂停,则重复显示同一帧画面
if (is->paused)
goto display;
// 上一帧理论上应该显示的时长
last_duration = vp_duration(is, lastvp, vp);
// 上一帧还应显示多长时间(同步的核心点)
delay = compute_target_delay(last_duration, is);帧的理论显示时长计算:
staticdoublevp_duration(VideoState *is, Frame *vp, Frame *nextvp) {
if (vp->serial == nextvp->serial) {
// 帧间隔就是上一帧的理论显示时长
double duration = nextvp->pts - vp->pts;
// 帧间隔小于0或者大于最大帧间隔,则使用上一帧的显示时长
if (isnan(duration) || duration <= 0 || duration > is->max_frame_duration)
return vp->duration;
else
return duration;
} else {
// 不是同序列的直接返回0
return0.0;
}
}delay计算,同步的关键,这里很重要:
staticdoublecompute_target_delay(double delay, VideoState *is)
{
double sync_threshold, diff = 0;
// 主时钟不能是视频时钟,如果是视频时钟,就没必要算延迟了
if (get_master_sync_type(is) != AV_SYNC_VIDEO_MASTER) {
//计算视频时钟和主时钟的差值
//diff < 0 视频时钟比主时钟慢,表示视频播放落后
//diff = 0 视频时钟和主时钟完美同步
//diff > 0 视频时钟比主时钟快,表示视频播放超前
diff = get_clock(&is->vidclk) - get_master_clock(is);
// 同步的门限,上限100ms 下限40ms
sync_threshold = FFMAX(AV_SYNC_THRESHOLD_MIN, FFMIN(AV_SYNC_THRESHOLD_MAX, delay));
// 差值小于最大帧间隔
if (!isnan(diff) && fabs(diff) < is->max_frame_duration) {
// 视频时钟比主时钟慢,需要追赶, diff为负数,所以delay是减小的。
if (diff <= -sync_threshold)
delay = FFMAX(0, delay + diff);
// 视频时钟比主时钟快,且当前帧比较长,长到超过了门限。
elseif (diff >= sync_threshold && delay > AV_SYNC_FRAMEDUP_THRESHOLD)
delay = delay + diff;
// 视频时钟比主时钟快,翻倍等待。相当于再播放一次当前帧
elseif (diff >= sync_threshold)
delay = 2 * delay;
}
}
av_log(NULL, AV_LOG_TRACE, "video: delay=%0.3f A-V=%f\n",
delay, -diff);
return delay;
}看完了延迟计算,接着往下看代码:
time= av_gettime_relative()/1000000.0;
// 判断当前帧是否已播完
if (time < is->frame_timer + delay) {
// 没有播放完成,计算等待时间
*remaining_time = FFMIN(is->frame_timer + delay - time, *remaining_time);
goto display;
}这里的display其实就是结果函数的意思,此时跳到到display标记处也不会去刷新视频。
准备播放下一帧
能执行到这里,说明当前帧已播完,可以播放下一帧了。
// 更新当前播放的时间戳
is->frame_timer += delay;
// 太离谱就需要纠正错误
if (delay > 0 && time - is->frame_timer > AV_SYNC_THRESHOLD_MAX)
is->frame_timer = time;
// 更新视频时钟
SDL_LockMutex(is->pictq.mutex);
if (!isnan(vp->pts))
update_video_pts(is, vp->pts, vp->serial);
SDL_UnlockMutex(is->pictq.mutex);
// 视频帧队列有多个帧待播放
if (frame_queue_nb_remaining(&is->pictq) > 1) {
Frame *nextvp = frame_queue_peek_next(&is->pictq);
duration = vp_duration(is, vp, nextvp);
// 帧没播放,但是已经过期了,则需要跳过
if(!is->step && (framedrop>0 || (framedrop && get_master_sync_type(is) != AV_SYNC_VIDEO_MASTER)) && time > is->frame_timer + duration){
is->frame_drops_late++;
frame_queue_next(&is->pictq);
goto retry;
}
}更新字幕
ffplay中的字幕它只接受图片类型的字幕,而现在的字幕大多都是文本类字幕,所以这个地方的逻辑一般很难进入的,但我们还是大致看一眼:
if (is->subtitle_st) {
while (frame_queue_nb_remaining(&is->subpq) > 0) {
// 获取一帧
sp = frame_queue_peek(&is->subpq);
// 获取下一帧
if (frame_queue_nb_remaining(&is->subpq) > 1)
sp2 = frame_queue_peek_next(&is->subpq);
else
sp2 = NULL;
// 防止前后帧的字幕重叠,所以做了两个判断:
// 1、sp字幕是否已经自然结束
// 2、sp2字幕是否已经自然开始
// 以上两个条件不论哪个条件成立,都需要当前字幕退场
if (sp->serial != is->subtitleq.serial
|| (is->vidclk.pts > (sp->pts + ((float) sp->sub.end_display_time / 1000)))
|| (sp2 && is->vidclk.pts > (sp2->pts + ((float) sp2->sub.start_display_time / 1000))))
{
if (sp->uploaded) {
int i;
for (i = 0; i < sp->sub.num_rects; i++) {
AVSubtitleRect *sub_rect = sp->sub.rects[i];
uint8_t *pixels;
int pitch, j;
if (!SDL_LockTexture(is->sub_texture, (SDL_Rect *)sub_rect, (void **)&pixels, &pitch)) {
for (j = 0; j < sub_rect->h; j++, pixels += pitch)
// 纹理清除
memset(pixels, 0, sub_rect->w << 2);
SDL_UnlockTexture(is->sub_texture);
}
}
}
frame_queue_next(&is->subpq);
} else {
break;
}
}
}显示视频帧
// 切下一帧
frame_queue_next(&is->pictq);
// 关键的显示标志
is->force_refresh = 1;
// 逐帧播放时,播一帧时就需要暂停
if (is->step && !is->paused)
stream_toggle_pause(is);
}
display:
// 视频显示
if (!display_disable && is->force_refresh && is->show_mode == SHOW_MODE_VIDEO && is->pictq.rindex_shown)
video_display(is);
}
// 重置标志
is->force_refresh = 0;代码再往下是状态打印,咱就不看了,大家感兴趣的话可以瞅瞅。
夜雨聆风