ijkplayer音视频同步策略分析

2024-06-15 10:38

本文主要是介绍ijkplayer音视频同步策略分析,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

音视频同步是播放器的一道必选题,也是面试官常问的面试题。大家应该都知道音视频同步时钟有三种,默认使用音频时钟作为主时钟。但是面试官会有其他变种问法:如果直播流的音频落后或者中断怎么办?如果没有音频流,以什么时钟作为主时钟?如果有两个音频流(原声和伴奏的播放场景)怎么办?如果视频时间戳落后或者超前怎么办,不同落后程度怎么处理?如果设置倍速播放有没影响?

总结一下音视频同步问题:

1、视频时间戳落后或超前的处理;

2、倍速播放的处理;

3、双轨音频播放的处理;

4、没有音频流的情况处理;

5、音频流落后或中断的处理;

我们从ijkplayer的ff_ffplay.c进行分析,基本方法有get_clock()、set_clock()、set_clock_at()、set_clock_speed(),具体代码如下:

static double get_clock(Clock *c)
{if (*c->queue_serial != c->serial)return NAN;if (c->paused) {return c->pts;} else {double time = av_gettime_relative() / 1000000.0;return c->pts_drift + time - (time - c->last_updated) * (1.0 - c->speed);}
}static void set_clock_at(Clock *c, double pts, int serial, double time)
{c->pts = pts;c->last_updated = time;c->pts_drift = c->pts - time;c->serial = serial;
}static void set_clock(Clock *c, double pts, int serial)
{double time = av_gettime_relative() / 1000000.0;set_clock_at(c, pts, serial, time);
}

然后是获取主时钟类型与主时钟。首先说明一下,应该大家都知道的,人们对声音比画面更敏感,由听觉与视觉决定,所以一般默认音频时钟作为主时钟。如果默认用视频时钟作为主时钟,有视频就用视频时钟,否则用音频时钟;如果默认用音频时钟作为主时钟,有音频就用音频时钟,否则用外部时钟;其他情况使用外部时钟。代码如下:

static int get_master_sync_type(VideoState *is) {if (is->av_sync_type == AV_SYNC_VIDEO_MASTER) {if (is->video_st)return AV_SYNC_VIDEO_MASTER;elsereturn AV_SYNC_AUDIO_MASTER;} else if (is->av_sync_type == AV_SYNC_AUDIO_MASTER) {if (is->audio_st)return AV_SYNC_AUDIO_MASTER;elsereturn AV_SYNC_EXTERNAL_CLOCK;} else {return AV_SYNC_EXTERNAL_CLOCK;}
}static double get_master_clock(VideoState *is)
{double val;switch (get_master_sync_type(is)) {case AV_SYNC_VIDEO_MASTER:val = get_clock(&is->vidclk);break;case AV_SYNC_AUDIO_MASTER:val = get_clock(&is->audclk);break;default:val = get_clock(&is->extclk);break;}return val;
}

接着是设置与检查时钟速度,在倍速播放时需要用到设置时钟速度。代码如下:

static void set_clock_speed(Clock *c, double speed)
{set_clock(c, get_clock(c), c->serial);c->speed = speed;
}static void check_external_clock_speed(VideoState *is) {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));} else if ((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;// if isn't normal speed, need to set clock speedif (speed != 1.0)set_clock_speed(&is->extclk, speed + EXTERNAL_CLOCK_SPEED_STEP * (1.0 - speed) / fabs(1.0 - speed));}
}

在音频播放时,如果音频时钟落后或者发生异常,需要把外部时钟同步给音频时钟。具体代码在sdl_audio_callback()方法中:

static void sdl_audio_callback(void *opaque, Uint8 *stream, int len)
{......is->audio_write_buf_size = is->audio_buf_size - is->audio_buf_index;/* Let's assume the audio driver that is used by SDL has two periods. */if (!isnan(is->audio_clock)) {set_clock_at(&is->audclk, is->audio_clock - (double)(is->audio_write_buf_size) / is->audio_tgt.bytes_per_sec - SDL_AoutGetLatencySeconds(ffp->aout), is->audio_clock_serial, ffp->audio_callback_time / 1000000.0);// sync external clock to audio clocksync_clock_to_slave(&is->extclk, &is->audclk);}if (!ffp->first_audio_frame_rendered) {ffp->first_audio_frame_rendered = 1;ffp_notify_msg1(ffp, FFP_MSG_AUDIO_RENDERING_START);}
}

在视频播放时,会检查外部时钟速度、计算目标延时时间、更新视频的pts显示时间戳。

计算延时的时候,如果主时钟不是视频时钟,会计算视频时钟与主时钟的差值diff,然后用diff与sync_threshold比较,最终更新delay延时时间:

static double compute_target_delay(FFPlayer *ffp, double delay, VideoState *is)
{double sync_threshold, diff = 0;/* update delay to follow master synchronisation source */if (get_master_sync_type(is) != AV_SYNC_VIDEO_MASTER) {/* if video is slave, we try to correct big delays byduplicating or deleting a frame */diff = get_clock(&is->vidclk) - get_master_clock(is);/* skip or repeat frame. We take into account thedelay to compute the threshold. I still don't knowif it is the best guess */sync_threshold = FFMAX(AV_SYNC_THRESHOLD_MIN, FFMIN(AV_SYNC_THRESHOLD_MAX, delay));/* -- by bbcallen: replace is->max_frame_duration with AV_NOSYNC_THRESHOLD */if (!isnan(diff) && fabs(diff) < AV_NOSYNC_THRESHOLD) {if (diff <= -sync_threshold)delay = FFMAX(0, delay + diff);else if (diff >= sync_threshold && delay > AV_SYNC_FRAMEDUP_THRESHOLD)delay = delay + diff;else if (diff >= sync_threshold)delay = 2 * delay;}}if (ffp) {ffp->stat.avdelay = delay;ffp->stat.avdiff  = diff;}return delay;
}

更新视频pts的方法,主要是重新设置时钟、同步从时钟:

static void update_video_pts(VideoState *is, double pts, int64_t pos, int serial) {/* update current video pts */set_clock(&is->vidclk, pts, serial);sync_clock_to_slave(&is->extclk, &is->vidclk);
}

视频播放核心代码在video_refresh()方法。如果视频时间戳落后小于delay,直接去渲染;如果主时钟不是视频时钟,并且视频时间戳落后大于duration,丢弃当前视频帧取下一帧:

static void video_refresh(FFPlayer *opaque, double *remaining_time)
{// 检查外部时钟if (!is->paused && get_master_sync_type(is) == AV_SYNC_EXTERNAL_CLOCK && is->realtime)check_external_clock_speed(is);......if (is->video_st) {
retry:if (frame_queue_nb_remaining(&is->pictq) == 0) {// nothing to do, no picture to display in the queue} else {/* compute nominal last_duration */last_duration = vp_duration(is, lastvp, vp);// 计算延时时间delay = compute_target_delay(ffp, last_duration, is);time= av_gettime_relative()/1000000.0;if (isnan(is->frame_timer) || time < is->frame_timer)is->frame_timer = time;// 视频时间戳落后小于delay,直接去渲染if (time < is->frame_timer + delay) {*remaining_time = FFMIN(is->frame_timer + delay - time, *remaining_time);goto display;}is->frame_timer += delay;if (delay > 0 && time - is->frame_timer > AV_SYNC_THRESHOLD_MAX)is->frame_timer = time;// 更新视频ptsSDL_LockMutex(is->pictq.mutex);if (!isnan(vp->pts))update_video_pts(is, vp->pts, vp->pos, 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);// 如果主时钟不是视频时钟,并且视频时间戳落后大于duration,丢弃当前视频帧取下一帧if(!is->step && (ffp->framedrop > 0 || (ffp->framedrop && get_master_sync_type(is) != AV_SYNC_VIDEO_MASTER)) && time > is->frame_timer + duration) {frame_queue_next(&is->pictq);goto retry;}}frame_queue_next(&is->pictq);is->force_refresh = 1;}
display:/* display picture */if (!ffp->display_disable && is->force_refresh && is->show_mode == SHOW_MODE_VIDEO && is->pictq.rindex_shown)video_display2(ffp);}is->force_refresh = 0;
}

如果有双轨音频播放,需要选择其中一个音频时钟作为主时钟。比如原声与伴奏同时播放的场景,选择原声的音频时钟作为主时钟,伴奏同步于原声。

这篇关于ijkplayer音视频同步策略分析的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1063218

相关文章

redis过期key的删除策略介绍

《redis过期key的删除策略介绍》:本文主要介绍redis过期key的删除策略,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录第一种策略:被动删除第二种策略:定期删除第三种策略:强制删除关于big key的清理UNLINK命令FLUSHALL/FLUSHDB命

Java程序进程起来了但是不打印日志的原因分析

《Java程序进程起来了但是不打印日志的原因分析》:本文主要介绍Java程序进程起来了但是不打印日志的原因分析,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录Java程序进程起来了但是不打印日志的原因1、日志配置问题2、日志文件权限问题3、日志文件路径问题4、程序

Java字符串操作技巧之语法、示例与应用场景分析

《Java字符串操作技巧之语法、示例与应用场景分析》在Java算法题和日常开发中,字符串处理是必备的核心技能,本文全面梳理Java中字符串的常用操作语法,结合代码示例、应用场景和避坑指南,可快速掌握字... 目录引言1. 基础操作1.1 创建字符串1.2 获取长度1.3 访问字符2. 字符串处理2.1 子字

SpringRetry重试机制之@Retryable注解与重试策略详解

《SpringRetry重试机制之@Retryable注解与重试策略详解》本文将详细介绍SpringRetry的重试机制,特别是@Retryable注解的使用及各种重试策略的配置,帮助开发者构建更加健... 目录引言一、SpringRetry基础知识二、启用SpringRetry三、@Retryable注解

MySQL 分区与分库分表策略应用小结

《MySQL分区与分库分表策略应用小结》在大数据量、复杂查询和高并发的应用场景下,单一数据库往往难以满足性能和扩展性的要求,本文将详细介绍这两种策略的基本概念、实现方法及优缺点,并通过实际案例展示如... 目录mysql 分区与分库分表策略1. 数据库水平拆分的背景2. MySQL 分区策略2.1 分区概念

Python 迭代器和生成器概念及场景分析

《Python迭代器和生成器概念及场景分析》yield是Python中实现惰性计算和协程的核心工具,结合send()、throw()、close()等方法,能够构建高效、灵活的数据流和控制流模型,这... 目录迭代器的介绍自定义迭代器省略的迭代器生产器的介绍yield的普通用法yield的高级用法yidle

C++ Sort函数使用场景分析

《C++Sort函数使用场景分析》sort函数是algorithm库下的一个函数,sort函数是不稳定的,即大小相同的元素在排序后相对顺序可能发生改变,如果某些场景需要保持相同元素间的相对顺序,可使... 目录C++ Sort函数详解一、sort函数调用的两种方式二、sort函数使用场景三、sort函数排序

kotlin中const 和val的区别及使用场景分析

《kotlin中const和val的区别及使用场景分析》在Kotlin中,const和val都是用来声明常量的,但它们的使用场景和功能有所不同,下面给大家介绍kotlin中const和val的区别,... 目录kotlin中const 和val的区别1. val:2. const:二 代码示例1 Java

Go标准库常见错误分析和解决办法

《Go标准库常见错误分析和解决办法》Go语言的标准库为开发者提供了丰富且高效的工具,涵盖了从网络编程到文件操作等各个方面,然而,标准库虽好,使用不当却可能适得其反,正所谓工欲善其事,必先利其器,本文将... 目录1. 使用了错误的time.Duration2. time.After导致的内存泄漏3. jsO

Spring事务中@Transactional注解不生效的原因分析与解决

《Spring事务中@Transactional注解不生效的原因分析与解决》在Spring框架中,@Transactional注解是管理数据库事务的核心方式,本文将深入分析事务自调用的底层原理,解释为... 目录1. 引言2. 事务自调用问题重现2.1 示例代码2.2 问题现象3. 为什么事务自调用会失效3