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

相关文章

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

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

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

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

Python 中的异步与同步深度解析(实践记录)

《Python中的异步与同步深度解析(实践记录)》在Python编程世界里,异步和同步的概念是理解程序执行流程和性能优化的关键,这篇文章将带你深入了解它们的差异,以及阻塞和非阻塞的特性,同时通过实际... 目录python中的异步与同步:深度解析与实践异步与同步的定义异步同步阻塞与非阻塞的概念阻塞非阻塞同步

找不到Anaconda prompt终端的原因分析及解决方案

《找不到Anacondaprompt终端的原因分析及解决方案》因为anaconda还没有初始化,在安装anaconda的过程中,有一行是否要添加anaconda到菜单目录中,由于没有勾选,导致没有菜... 目录问题原因问http://www.chinasem.cn题解决安装了 Anaconda 却找不到 An

Spring定时任务只执行一次的原因分析与解决方案

《Spring定时任务只执行一次的原因分析与解决方案》在使用Spring的@Scheduled定时任务时,你是否遇到过任务只执行一次,后续不再触发的情况?这种情况可能由多种原因导致,如未启用调度、线程... 目录1. 问题背景2. Spring定时任务的基本用法3. 为什么定时任务只执行一次?3.1 未启用

SpringBoot如何通过Map实现策略模式

《SpringBoot如何通过Map实现策略模式》策略模式是一种行为设计模式,它允许在运行时选择算法的行为,在Spring框架中,我们可以利用@Resource注解和Map集合来优雅地实现策略模式,这... 目录前言底层机制解析Spring的集合类型自动装配@Resource注解的行为实现原理使用直接使用M

C++ 各种map特点对比分析

《C++各种map特点对比分析》文章比较了C++中不同类型的map(如std::map,std::unordered_map,std::multimap,std::unordered_multima... 目录特点比较C++ 示例代码 ​​​​​​代码解释特点比较1. std::map底层实现:基于红黑

Spring、Spring Boot、Spring Cloud 的区别与联系分析

《Spring、SpringBoot、SpringCloud的区别与联系分析》Spring、SpringBoot和SpringCloud是Java开发中常用的框架,分别针对企业级应用开发、快速开... 目录1. Spring 框架2. Spring Boot3. Spring Cloud总结1. Sprin

Spring 中 BeanFactoryPostProcessor 的作用和示例源码分析

《Spring中BeanFactoryPostProcessor的作用和示例源码分析》Spring的BeanFactoryPostProcessor是容器初始化的扩展接口,允许在Bean实例化前... 目录一、概览1. 核心定位2. 核心功能详解3. 关键特性二、Spring 内置的 BeanFactory

MyBatis-Plus中Service接口的lambdaUpdate用法及实例分析

《MyBatis-Plus中Service接口的lambdaUpdate用法及实例分析》本文将详细讲解MyBatis-Plus中的lambdaUpdate用法,并提供丰富的案例来帮助读者更好地理解和应... 目录深入探索MyBATis-Plus中Service接口的lambdaUpdate用法及示例案例背景