
音视频同步的目的是为了使播放的声音和显示的画面保持一致。
视频按帧播放,图像显示设备每次显示一帧画面,视频播放速度由帧率确定,帧率指示每秒显示多少帧;
音频按采样点播放,声音播放设备每次播放一个采样点,声音播放速度由采样率确定,采样率指示每秒播放多少个采样点。
如果仅仅是视频按帧率播放,音频按采样率播放,二者没有同步机制,即使最初音视频是基本同步的,随着时间的流逝,音视频会逐渐失去同步,并且不同步的现象会越来越严重。
这是因为:一、播放时间难以精确控制,二、异常及误差会随时间累积。所以,必须要采用一定的同步策略,不断对音视频的时间差作校正,使图像显示与声音播放总体保持一致。
音视频同步的方式基本是确定一个时钟(音频时钟、视频时钟、外部时钟)作为主时钟,非主时钟的音频或视频时钟为从时钟。在播放过程中,主时钟作为同步基准,不断判断从时钟与主时钟的差异,调节从时钟,使从时钟追赶(落后时)或等待(超前时)主时钟。
按照主时钟的不同种类,可以将音视频同步模式分为如下三种:
ffplay默认的同步方式:视频同步到音频。
I帧:I帧(Intra-codedpicture,帧内编码帧,常称为关键帧)包含一幅完整的图像信息,属于帧内编码图像,不含运动矢量,在解码时不需要参考其他帧图像。因此在I帧图像处可以切换频道,而不会导致图像丢失或无法解码。I帧图像用于阻止误差的累积和扩散。在闭合式GOP中,每个GOP的第一个帧一定是I帧,且当前GOP的数据不会参考前后GOP的数据。
IDR帧:IDR帧(InstantaneousDecodingRefreshpicture,即时解码刷新帧)是一种特殊的I帧。当解码器解码到IDR帧时,会将DPB(DecodedPictureBuffer,指前后向参考帧列表)清空,将已解码的数据全部输出或抛弃,然后开始一次全新的解码序列。IDR帧之后的图像不会参考IDR帧之前的图像,因此IDR帧可以阻止视频流中的错误传播,同时IDR帧也是解码器、播放器的一个安全访问点。
P帧:P帧(Predictive-codedpicture,预测编码图像帧)是帧间编码帧,利用之前的I帧或P帧进行预测编码。
B帧:B帧(Bi-directionallypredictedpicture,双向预测编码图像帧)是帧间编码帧,利用之前和(或)之后的I帧或P帧进行双向预测编码。B帧不可以作为参考帧。B帧具有更高的压缩率,但需要更多的缓冲时间以及更高的CPU占用率,因此B帧适合本地存储以及视频点播,而不适用对实时性要求较高的直播系统。
GOP(Group Of Pictures,图像组)是一组连续的图像,由一个I帧和多个B/P帧组成,是编解码器存取的基本单位。GOP结构常用的两个参数M和N,M指定GOP中两个anchor frame(anchor frame指可被其他帧参考的帧,即I帧或P帧)之间的距离,N指定一个GOP的大小。例如M=3,N=15,GOP结构为:IBBPBBPBBPBBPBB
GOP有两种:闭合式GOP和开放式GOP。
在开放式GOP中,普通I帧和IDR帧功能是有差别的,需要明确区分两种帧类型。在闭合式GOP中,普通I帧和IDR帧功能没有差别,可以不作区分。
开放式GOP和闭合式GOP中I帧、P帧、B帧的依赖关系如下图所示:

音频中DTS和PTS是相同的。视频中由于B帧需要双向预测,B帧依赖于其前和其后的帧,因此含B帧的视频解码顺序与显示顺序不同,即DTS与PTS不同。当然,不含B帧的视频,其DTS和PTS是相同的。下图以一个开放式GOP示意图为例,说明视频流的解码顺序和显示顺序:

以图中“B[1]”帧为例进行说明,“B[1]”帧解码时需要参考“I[0]”帧和“P[3]”帧,因此“P[3]”帧必须比“B[1]”帧先解码。这就导致了解码顺序和显示顺序的不一致,后显示的帧需要先解码。
video_decode_frame() 函数:
本函数实现如下功能:
注意如下几点:
如何确定解码器的输出 frame 与输入 packet 的对应关系呢?可以对比 frame->pkt_pos 和 pkt.pos 的值,这两个值表示 packet 在视频文件中的偏移地址,如果这两个变量值相等,表示此 frame 来自此 packet。调试跟踪这两个变量值,即能发现解码器输入帧与输出帧的关系。
视频同步到音频是 ffplay 的默认同步方式,在视频播放线程中实现。其中,video_refresh()函数实现了视频播放(包含同步控制)核心步骤。
相关函数关系如下:
main() -->
player_running() -->
open_video() -->
open_video_playing() -->
SDL_CreateThread(video_playing_thread, ...) 创建视频播放线程
video_playing_thread() -->
video_refresh()
视频播放线程源码如下:
static int video_playing_thread(void *arg)
{
player_stat_t *is = (player_stat_t *)arg;
double remaining_time = 0.0;
while (1)
{
if (remaining_time > 0.0)
{
av_usleep((unsigned)(remaining_time * 1000000.0));
}
remaining_time = REFRESH_RATE;
// 立即显示当前帧,或延时remaining_time后再显示
video_refresh(is, &remaining_time);
}
return 0;
}
video_refresh()函数源码如下:
/* called to display each frame */
static void video_refresh(void *opaque, double *remaining_time)
{
player_stat_t *is = (player_stat_t *)opaque;
double time;
static bool first_frame = true;
retry:
if (frame_queue_nb_remaining(&is->video_frm_queue) == 0) // 所有帧已显示
{
// nothing to do, no picture to display in the queue
return;
}
double last_duration, duration, delay;
frame_t *vp, *lastvp;
/* dequeue the picture */
lastvp = frame_queue_peek_last(&is->video_frm_queue); // 上一帧:上次已显示的帧
vp = frame_queue_peek(&is->video_frm_queue); // 当前帧:当前待显示的帧
// lastvp和vp不是同一播放序列(一个seek会开始一个新播放序列),将frame_timer更新为当前时间
if (first_frame)
{
is->frame_timer = av_gettime_relative() / 1000000.0;
first_frame = false;
}
// 暂停处理:不停播放上一帧图像
if (is->paused)
goto display;
/* compute nominal last_duration */
last_duration = vp_duration(is, lastvp, vp); // 上一帧播放时长:vp->pts - lastvp->pts
delay = compute_target_delay(last_duration, is); // 根据视频时钟和同步时钟的差值,计算delay值
time = av_gettime_relative()/1000000.0;
// 当前帧播放时刻(is->frame_timer+delay)大于当前时刻(time),表示播放时刻未到
if (time < is->frame_timer + delay) {
// 播放时刻未到,则更新刷新时间remaining_time为当前时刻到下一播放时刻的时间差
*remaining_time = FFMIN(is->frame_timer + delay - time, *remaining_time);
// 播放时刻未到,则不播放,直接返回
return;
}
// 更新frame_timer值
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->video_frm_queue.mutex);
if (!isnan(vp->pts))
{
update_video_pts(is, vp->pts, vp->pos, vp->serial); // 更新视频时钟:时间戳、时钟时间
}
SDL_UnlockMutex(is->video_frm_queue.mutex);
// 是否要丢弃未能及时播放的视频帧
if (frame_queue_nb_remaining(&is->video_frm_queue) > 1) // 队列中未显示帧数>1(只有一帧则不考虑丢帧)
{
frame_t *nextvp = frame_queue_peek_next(&is->video_frm_queue); // 下一帧:下一待显示的帧
duration = vp_duration(is, vp, nextvp); // 当前帧vp播放时长 = nextvp->pts - vp->pts
// 当前帧vp未能及时播放,即下一帧播放时刻(is->frame_timer+duration)小于当前系统时刻(time)
if (time > is->frame_timer + duration)
{
frame_queue_next(&is->video_frm_queue); // 删除上一帧已显示帧,即删除lastvp,读指针加1(从lastvp更新到vp)
goto retry;
}
}
// 删除当前读指针元素,读指针+1。若未丢帧,读指针从lastvp更新到vp;若有丢帧,读指针从vp更新到nextvp
frame_queue_next(&is->video_frm_queue);
display:
video_display(is); // 取出当前帧vp(若有丢帧是nextvp)进行播放
}
视频同步到音频的基本方法是:**如果视频超前音频,则不进行播放,等待音频;如果视频落后音频,则丢弃当前帧直接播放下一帧,追赶音频。**此函数执行流程参考如下流程图:

步骤如下:
在 video_refresh() 函数中,调用了 compute_target_delay() 来根据视频时钟与主时钟的差异来调节 delay 值,从而调节视频帧播放的时刻:
// 根据视频时钟与同步时钟(如音频时钟)的差值,校正delay值,使视频时钟追赶或等待同步时钟
// 输入参数delay是上一帧播放时长,即上一帧播放后应延时多长时间后再播放当前帧,通过调节此值来调节当前帧播放快慢
// 返回值delay是将输入参数delay经校正后得到的值
static double compute_target_delay(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) {
// 视频时钟与同步时钟(如音频时钟)的差异,时钟值是上一帧pts值(实为:上一帧pts + 上一帧至今流逝的时间差)
diff = get_clock(&is->vidclk) - get_master_clock(is);
// delay是上一帧播放时长:当前帧(待播放的帧)播放时间与上一帧播放时间差理论值
// diff是视频时钟与同步时钟的差值
// 若delay < AV_SYNC_THRESHOLD_MIN,则同步域值为AV_SYNC_THRESHOLD_MIN
// 若delay > AV_SYNC_THRESHOLD_MAX,则同步域值为AV_SYNC_THRESHOLD_MAX
// 若AV_SYNC_THRESHOLD_MIN < delay < AV_SYNC_THRESHOLD_MAX,则同步域值为delay
sync_threshold = FFMAX(AV_SYNC_THRESHOLD_MIN, FFMIN(AV_SYNC_THRESHOLD_MAX, delay));
if (!isnan(diff) && fabs(diff) < is->max_frame_duration) {
if (diff <= -sync_threshold) // 视频时钟落后于同步时钟,且超过同步域值
delay = FFMAX(0, delay + diff); // 当前帧播放时刻落后于同步时钟(delay+diff<0)则delay=0(视频追赶,立即播放),否则delay=delay+diff
else if (diff >= sync_threshold && delay > AV_SYNC_FRAMEDUP_THRESHOLD) // 视频时钟超前于同步时钟,且超过同步域值,但上一帧播放时长超长
delay = delay + diff; // 仅仅校正为delay=delay+diff,主要是AV_SYNC_FRAMEDUP_THRESHOLD参数的作用,不作同步补偿
else if (diff >= sync_threshold) // 视频时钟超前于同步时钟,且超过同步域值
delay = 2 * delay; // 视频播放要放慢脚步,delay扩大至2倍
}
}
av_log(NULL, AV_LOG_TRACE, "video: delay=%0.3f A-V=%f\n", delay, -diff);
return delay;
}
本函数实现功能如下:
对上述视频同步到音频的过程作一个总结,参考下图:

图中,小黑圆圈是代表帧的实际播放时刻,小红圆圈代表帧的理论播放时刻,小绿方块表示当前系统时间(当前时刻),小红方块表示位于不同区间的时间点,则当前时刻处于不同区间时,视频同步策略为:
上述内容是为了方便理解进行的简单而形象的描述。实际过程要计算相关值,根据 compute_target_delay() 和 video_refresh() 中的策略来控制播放过程。
音频时钟是同步主时钟,音频按照自己的节奏进行播放即可,视频播放时则要参考音频时钟。音频播放函数由 SDL 音频播放线程回调,回调函数实现如下:
// 音频处理回调函数。读队列获取音频包,解码,播放
// 此函数被SDL按需调用,此函数不在用户主线程中,因此数据需要保护
// \param[in] opaque 用户在注册回调函数时指定的参数
// \param[out] stream 音频数据缓冲区地址,将解码后的音频数据填入此缓冲区
// \param[out] len 音频数据缓冲区大小,单位字节
// 回调函数返回后,stream指向的音频缓冲区将变为无效
// 双声道采样点的顺序为LRLRLR
static void sdl_audio_callback(void *opaque, Uint8 *stream, int len)
{
player_stat_t *is = (player_stat_t *)opaque;
int audio_size, len1;
int64_t audio_callback_time = av_gettime_relative();
while (len > 0) // 输入参数len等于is->audio_hw_buf_size,是audio_open()中申请到的SDL音频缓冲区大小
{
if (is->audio_cp_index >= (int)is->audio_frm_size)
{
// 1. 从音频frame队列中取出一个frame,转换为音频设备支持的格式,返回值是重采样音频帧的大小
audio_size = audio_resample(is, audio_callback_time);
if (audio_size < 0)
{
/* if error, just output silence */
is->p_audio_frm = NULL;
is->audio_frm_size = SDL_AUDIO_MIN_BUFFER_SIZE / is->audio_param_tgt.frame_size * is->audio_param_tgt.frame_size;
}
else
{
is->audio_frm_size = audio_size;
}
is->audio_cp_index = 0;
}
// 引入is->audio_cp_index的作用:防止一帧音频数据大小超过SDL音频缓冲区大小,这样一帧数据需要经过多次拷贝
// 用is->audio_cp_index标识重采样帧中已拷入SDL音频缓冲区的数据位置索引,len1表示本次拷贝的数据量
len1 = is->audio_frm_size - is->audio_cp_index;
if (len1 > len)
{
len1 = len;
}
// 2. 将转换后的音频数据拷贝到音频缓冲区stream中,之后的播放就是音频设备驱动程序的工作了
if (is->p_audio_frm != NULL)
{
memcpy(stream, (uint8_t *)is->p_audio_frm + is->audio_cp_index, len1);
}
else
{
memset(stream, 0, len1);
}
len -= len1;
stream += len1;
is->audio_cp_index += len1;
}
// is->audio_write_buf_size是本帧中尚未拷入SDL音频缓冲区的数据量
is->audio_write_buf_size = is->audio_frm_size - is->audio_cp_index;
/* Let's assume the audio driver that is used by SDL has two periods. */
// 3. 更新时钟
if (!isnan(is->audio_clock))
{
// 更新音频时钟,更新时刻:每次往声卡缓冲区拷入数据后
// 前面audio_decode_frame中更新的is->audio_clock是以音频帧为单位,所以此处第二个参数要减去未拷贝数据量占用的时间
set_clock_at(&is->audio_clk,
is->audio_clock - (double)(2 * is->audio_hw_buf_size + is->audio_write_buf_size) / is->audio_param_tgt.bytes_per_sec,
is->audio_clock_serial,
audio_callback_time / 1000000.0);
}
}
Clock结构体:
// 时钟/同步时钟
typedef struct Clock {
double pts; // 当前正在播放的帧的pts /* clock base */
double pts_drift; // 当前的pts与系统时间的差值 保持设置pts时候的差值,后面就可以利用这个差值推算下一个pts播放的时间点
double last_updated; // 最后一次更新时钟的时间
double speed; // 播放速度控制
int serial; // 播放序列
int paused; // 是否暂停
int *queue_serial; // 队列的播放序列 PacketQueue中的serial
} Clock;
再结合关于时钟的几个函数看看:
// 主要由set_clock调用
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 double get_clock(Clock *c)
{
// 如果时钟的播放序列与待解码包队列的序列不一致,返回NAN,肯定就是不同步或者需要丢帧了
if (*c->queue_serial != c->serial)
return NAN;
if (c->paused) {
// 暂停状态则返回原来的pts
return c->pts;
} else {
double time = av_gettime_relative() / 1000000.0;
// speed可以先忽略播放速度控制
// 如果是1倍播放速度,c->pts_drift + time
return c->pts_drift + time - (time - c->last_updated) * (1.0 - c->speed);
}
}
音频和视频每次在播放新的一帧数据时都会调用函数set_clock更新音频时钟或视频时钟。通过函数set_clock_at我们发现,就是更新了 Clock 结构体的四个变量。其中pts_drift是当前帧的pts与系统时间的差值,有了这个差值在未来的某一刻就能够很方便地算出当前帧对于系统时刻的时钟点。
声卡虽然是以音频采样点为播放单位,但每次往声卡缓冲区送一个音频frame,每送一个音频frame更新一下音频的播放时刻,即每隔一个音频frame时长更新一下音频时钟。
在audio_decode_frame函数中,更新音频时钟audio_clock:
/* update the audio clock with the pts */
if (!isnan(af->pts))
is->audio_clock = af->pts + (double) af->frame->nb_samples / af->frame->sample_rate;
else
is->audio_clock = NAN;
is->audio_clock_serial = af->serial;
在sdl_audio_callback函数中,设置音频时钟:
if (!isnan(is->audio_clock))
{
// 更新音频时钟,更新时刻:每次往声卡缓冲区拷入数据后
// 前面audio_decode_frame中更新的is->audio_clock是以音频帧为单位,所以此处第二个参数要减去未拷贝数据量占用的时间
set_clock_at(&is->audio_clk,
is->audio_clock - (double)(2 * is->audio_hw_buf_size + is->audio_write_buf_size) / is->audio_param_tgt.bytes_per_sec,
is->audio_clock_serial,
audio_callback_time / 1000000.0);
}
在video_refresh函数更新视频时钟:
SDL_LockMutex(is->video_frm_queue.mutex);
if (!isnan(vp->pts))
{
update_video_pts(is, vp->pts, vp->pos, vp->serial); // 更新视频时钟:时间戳、时钟时间
}
SDL_UnlockMutex(is->video_frm_queue.mutex);
暂停/继续状态的切换是由用户按空格键实现的,每按一次空格键,暂停/继续的状态翻转一次。
函数调用关系如下:
main() -->
event_loop() -->
toggle_pause() -->
stream_toggle_pause()
stream_toggle_pause()实现状态翻转:
/* pause or resume the video */
static void stream_toggle_pause(VideoState *is)
{
if (is->paused) {
// 这里表示当前是暂停状态,将切换到继续播放状态。在继续播放之前,先将暂停期间流逝的时间加到frame_timer中
is->frame_timer += av_gettime_relative() / 1000000.0 - is->vidclk.last_updated;
if (is->read_pause_return != AVERROR(ENOSYS)) {
is->vidclk.paused = 0;
}
set_clock(&is->vidclk, get_clock(&is->vidclk), is->vidclk.serial);
}
set_clock(&is->extclk, get_clock(&is->extclk), is->extclk.serial);
is->paused = is->audclk.paused = is->vidclk.paused = is->extclk.paused = !is->paused;
}
在video_refresh()函数中有如下代码:
/* called to display each frame */
static void video_refresh(void *opaque, double *remaining_time)
{
......
// 视频播放
if (is->video_st) {
......
// 暂停处理:不停播放上一帧图像
if (is->paused)
goto display;
......
}
......
}
在暂停状态下,实际就是不停播放上一帧(最后一帧)图像,画面不更新。
逐帧播放是用户每按一次s键,播放器播放一帧画现。逐帧播放实现的方法是:每次按了s键,就将状态切换为播放,播放一帧画面后,将状态切换为暂停。
函数调用关系如下:
main() -->
event_loop() -->
step_to_next_frame() -->
stream_toggle_pause()
实现代码比较简单,如下:
static void step_to_next_frame(VideoState *is)
{
/* if the stream is paused unpause it, then step */
if (is->paused)
stream_toggle_pause(is); // 确保切换到播放状态,播放一帧画面
is->step = 1;
}
/* called to display each frame */
static void video_refresh(void *opaque, double *remaining_time)
{
......
// 视频播放
if (is->video_st) {
......
if (is->step && !is->paused)
stream_toggle_pause(is); // 逐帧播放模式下,播放一帧画面后暂停
......
}
......
}
变速变调可分为:变速不变调和变调不变速。
语音变速不变调是指保持音调和语义保持不变,语速变快或变慢。该过程表现为语谱图在时间轴上如手风琴般压缩或者扩展。那也就是说,基频值几乎不变,对应于音调不变;整个时间过程被压缩或者扩展,声门周期的数目减小或者增加,即声道运动速率发生改变,语速也随之变化。对应于语音产生模型,激励和系统经历与原始发音情况几乎相同的状态,但持续时间相比原来或长或短。
严格地讲,基频和音调是两个不同的概念,基频是指声带振动的频率,音调是指人类对基频的主观感知,但是两者变化基本一致,即基频越高,音调越高,基频越低,音调越低,音调是由基频决定的。因此,语音变调不变速就是指改变说话人基频的大小,同时保持语速和语义不变,即保持短时频谱包络(共振峰的位置和带宽)和时间过程基本不变。对应于语音产生模型,变调改变了激励源;声道模型的共振峰参数几乎不变,保证了语义和语速不变。
综上所述,变速改变声道运动速率,力求保持激励源不变;变调改变激励源,力求保持声道的共振峰信息不变。但是声源和声道不是相互独立的,在改变声源时,必然也会非线性的影响声道,同样地,改变声道时也会或多或少的影响声源,两者之间相互影响,相互作用。
语音变调在变声软件中较常用。而语音变速在播放器中常用,比如倍速播放(快播、慢播)。相对于视频基于帧的变速原理,跳帧或者插帧,音频的变速原理并不是如此简单,因为简单的抽采样点会引起声音的不连续、噪声或爆破音,主观体验较差。
目前较为常用的音频变速解决方案有两个:soundtouch和Sonic。ijkplayer使用的是soundtouch,EXOPlayer使用的是Sonic。在Android上还有一种实现方式,基于AudioTrack的变速播放。
Sonic和Soundtouch用法类似,都是提供封装好的库,将原音频的PCM数据通过接口函数处理为目标格式,比如二倍速,可能PCM采样点就减半。Soundtouch提供的接口如下所示:
参数设置类接口:
PCM处理类接口:
从上述接口来看,类似于常规的解码器或者解复用器的调用逻辑。
SEEK操作就是由用户干预而改变播放进度的实现方式,比如鼠标拖动播放进度条。
相关数据变量定义如下:
typedef struct VideoState {
......
int seek_req; // 标识一次SEEK请求
int seek_flags; // SEEK标志,诸如AVSEEK_FLAG_BYTE等
int64_t seek_pos; // SEEK的目标位置(当前位置+增量)
int64_t seek_rel; // 本次SEEK的位置增量
......
} VideoState;
VideoState.seek_flags表示SEEK标志。SEEK标志的类型定义如下:
#define AVSEEK_FLAG_BACKWARD 1 ///< seek backward
#define AVSEEK_FLAG_BYTE 2 ///< seeking based on position in bytes
#define AVSEEK_FLAG_ANY 4 ///< seek to any frame, even non-keyframes
#define AVSEEK_FLAG_FRAME 8 ///< seeking based on frame number
SEEK目标播放点(后文简称SEEK点)的确定,根据SEEK标志的不同,分为如下几种情况:
AVSEEK_FLAG_BYTE:SEEK点对应文件中的位置(字节表示)。有些解复用器可能不支持这种情况。
AVSEEK_FLAG_FRAME:SEEK点对应stream中frame序号,stream由stream_index指定。有些解复用器可能不支持这种情况。
如果不含上述两种标志且stream_index有效:SEEK点对应时间戳,单位是stream中的timebase,stream由stream_index指定。SEEK点的值由“目标frame中的pts(秒) × stream中的timebase”得到。
如果不含上述两种标志且stream_index是-1:SEEK点对应时间戳,单位是AV_TIME_BASE。SEEK点的值由“目标frame中的pts(秒) × AV_TIME_BASE”得到。
AVSEEK_FLAG_ANY:SEEK点对应帧序号,播放点可停留在任意帧(包括非关键帧)。有些解复用器可能不支持这种情况。
AVSEEK_FLAG_BACKWARD:忽略。
其中AV_TIME_BASE是FFmpeg内部使用的时间基,定义如下:
/**
* Internal time base represented as integer
*/
#define AV_TIME_BASE 1000000
AV_TIME_BASE表示1000000us。
通过异步事件机制实现[快进]/[快退]控制的。首先通过上下左右4个方向键触发[快进]/[快退]事件,其中,左右键设定[快进]/[快退]10s,上下键设定[快进]/[快退]60s。然后在main函数的事件循环处理逻辑中,通过sdl来监听捕获每个按键对应的消息,接着通过goto跳转到do_seek执行具体的事件处理逻辑。
在event_loop()函数进行的SDL消息处理中有如下代码片段:
//事件到来后唤醒主线程后,检查事件类型,执行相应操作
case SDLK_LEFT: //左键
incr = seek_interval ? -seek_interval : -10.0; //后退10s
goto do_seek;
case SDLK_RIGHT: //右键
incr = seek_interval ? seek_interval : 10.0; //前进10s
goto do_seek;
case SDLK_UP: //上键
incr = 60.0; //前进60s
goto do_seek;
case SDLK_DOWN: //下键
incr = -60.0; //后退60s
do_seek://处理请求
if (seek_by_bytes) {//通过字节方式seek
pos = -1;
//从frame(解码后)队列中获取当前播放到什么位置
if (pos < 0 && cur_stream->video_stream >= 0)
pos = frame_queue_last_pos(&cur_stream->pictq);
if (pos < 0 && cur_stream->audio_stream >= 0)
pos = frame_queue_last_pos(&cur_stream->sampq);
if (pos < 0)
pos = avio_tell(cur_stream->ic->pb);
//根据封装格式的比特率计算一秒有多少个字节
//计算seek的秒数相应有多少个字节
if (cur_stream->ic->bit_rate)
incr *= cur_stream->ic->bit_rate / 8.0; //比特除以8得到字节
else
incr *= 180000.0;
//当前位置加上偏移量得到seek的位置
pos += incr;
//进行seek参数设置,实际seek调用是在数据读取线程read_thread()里进行
stream_seek(cur_stream, pos, incr, 1);
} else {//通过时间方式seek
//获取播放当前时间(s)
pos = get_master_clock(cur_stream);
//get_master_clock(cur_stream)返回失败值,再用这个方法获取
if (isnan(pos))
pos = (double)cur_stream->seek_pos / AV_TIME_BASE;
//当前位置加上偏移量得到seek的位置
pos += incr;
//cur_stream->ic->start_time为封装文件第一帧的时间
//如果seek值比这个小就充值为cur_stream->ic->start_time
if (cur_stream->ic->start_time != AV_NOPTS_VALUE && pos < cur_stream->ic->start_time / (double)AV_TIME_BASE)
pos = cur_stream->ic->start_time / (double)AV_TIME_BASE;
//进行seek参数设置,实际seek调用是在数据读取线程read_thread()里进行
//pos和incr均转为微秒
stream_seek(cur_stream, (int64_t)(pos * AV_TIME_BASE), (int64_t)(incr * AV_TIME_BASE), 0);
}
break;
seek_by_bytes生效(对应AVSEEK_FLAG_BYTE标志)时,SEEK点对应文件中的位置,上述代码中设置了对应1秒数据量的播放增量;不生效时,SEEK点对应于播放时刻。
此函数实现如下功能:
stream_seek(cur_stream, (int64_t)(pos * AV_TIME_BASE), (int64_t)(incr * AV_TIME_BASE), 0);就是记录目标播放点和播放进度增量两个参数的,精确到微秒。再看一下stream_seak()函数的实现,仅仅是变量赋值:
/* seek in the stream */
//进行seek参数设置,实际seek调用是在数据读取线程read_thread()里进行
static void stream_seek(VideoState *is, int64_t pos, int64_t rel, int seek_by_bytes)
{
//当前没有seek请求才进行参数设置
if (!is->seek_req) {
is->seek_pos = pos; //seek到的位置(字节/微秒)
is->seek_rel = rel; //seek的偏移(字节/微秒)
is->seek_flags &= ~AVSEEK_FLAG_BYTE;
if (seek_by_bytes)
is->seek_flags |= AVSEEK_FLAG_BYTE;
is->seek_req = 1;
//如果数据读取线程read_thread()睡眠则唤醒
SDL_CondSignal(is->continue_read_thread);
}
}
在解复用线程主循环中处理了SEEK操作。
static int read_thread(void *arg)
{
......
for (;;) {
//判断是否有seek请求
if (is->seek_req) {
//seek_target的位置不一定对应能够播放的位置,如不是I帧,则会偏移到合适的位置
int64_t seek_target = is->seek_pos;
//可以接受seek_target的最小值
int64_t seek_min = is->seek_rel > 0 ? seek_target - is->seek_rel + 2: INT64_MIN;
//可以接受seek_target的最大值
int64_t seek_max = is->seek_rel < 0 ? seek_target - is->seek_rel - 2: INT64_MAX;
//调用avformat_seek_file()进行真正的seek操作
//阻塞函数,等待seek完成才返回
ret = avformat_seek_file(is->ic, -1, seek_min, seek_target, seek_max, is->seek_flags);
if (ret < 0) {
av_log(NULL, AV_LOG_ERROR,
"%s: error while seeking\n", is->ic->url);
} else {
//seek时要把原来的数据清空,重置解码器
if (is->audio_stream >= 0) {
//清空Packet(解码前)队列的数据
packet_queue_flush(&is->audioq);
//放入flush_pkt,重新开始一个播放序列(serial)
//解码器读到flush_pkt会清空解码器内缓存的Packet数据,serial++
packet_queue_put(&is->audioq, &flush_pkt);
}
if (is->subtitle_stream >= 0) {
packet_queue_flush(&is->subtitleq);
packet_queue_put(&is->subtitleq, &flush_pkt);
}
if (is->video_stream >= 0) {
packet_queue_flush(&is->videoq);
packet_queue_put(&is->videoq, &flush_pkt);
}
if (is->seek_flags & AVSEEK_FLAG_BYTE) {
set_clock(&is->extclk, NAN, 0);
} else {
set_clock(&is->extclk, seek_target / (double)AV_TIME_BASE, 0);
}
}
is->seek_req = 0;
is->queue_attachments_req = 1;
is->eof = 0;
//如果是暂停状态,显示下一帧就暂停
if (is->paused)
step_to_next_frame(is);
}
}
......
}
上述代码中的SEEK操作执行如下步骤:
avformat_seek_file()完成解复用器中的SEEK点切换操作;// 函数原型
int avformat_seek_file(AVFormatContext *s, int stream_index, int64_t min_ts, int64_t ts, int64_t max_ts, int flags);
// 调用代码
ret = avformat_seek_file(is->ic, -1, seek_min, seek_target, seek_max, is->seek_flags);
这个函数会等待SEEK操作完成才返回。实际的播放点力求最接近参数ts,并确保在[min_ts, max_ts]区间内,之所以播放点不一定在ts位置,是因为ts位置未必能正常播放。函数与SEEK点相关的三个参数(实参“seek_min”,“seek_target”,“seek_max”)取值方式与SEEK标志有关(实参“is->seek_flags”),此处“is->seek_flags”值为0。
if (is->video_stream >= 0) {
packet_queue_flush(&is->videoq);
packet_queue_put(&is->videoq, &flush_pkt);
}
is->seek_req = 0;文章目录一、概述简介原理模块二、配置Mysql使用版本环境要求1.操作系统2.mysql要求三、配置canal-server离线下载在线下载上传解压修改配置单机配置集群配置分库分表配置1.修改全局配置2.实例配置垂直分库水平分库3.修改group-instance.xml4.启动监听四、配置canal-adapter1修改启动配置2配置映射文件3启动ES数据同步查询所有订阅同步数据同步开关启动4.验证五、配置canal-admin一、概述简介canal是Alibaba旗下的一款开源项目,Java开发。基于数据库增量日志解析,提供增量数据订阅&消费。Git地址:https://github.co
动漫制作技巧是很多新人想了解的问题,今天小编就来解答与大家分享一下动漫制作流程,为了帮助有兴趣的同学理解,大多数人会选择动漫培训机构,那么今天小编就带大家来看看动漫制作要掌握哪些技巧?一、动漫作品首先完成草图设计和原型制作。设计草图要有目的、有对象、有步骤、要形象、要简单、符合实际。设计图要一致性,以保证制作的顺利进行。二、原型制作是根据设计图纸和制作材料,可以是手绘也可以是3d软件创建。在此步骤中,要注意的问题是色彩和平面布局。三、动漫制作制作完成后,加工成型。完成不同的表现形式后,就要对设计稿进行加工处理,使加工的难易度降低,并得到一些基本准确的概念,以便于后续的大样、准确的尺寸制定。四、
2022/8/4更新支持加入水印水印必须包含透明图像,并且水印图像大小要等于原图像的大小pythonconvert_image_to_video.py-f30-mwatermark.pngim_dirout.mkv2022/6/21更新让命令行参数更加易用新的命令行使用方法pythonconvert_image_to_video.py-f30im_dirout.mkvFFMPEG命令行转换一组JPG图像到视频时,是将这组图像视为MJPG流。我需要转换一组PNG图像到视频,FFMPEG就不认了。pyav内置了ffmpeg库,不需要系统带有ffmpeg工具因此我使用ffmpeg的python包装p
Transformers开始在视频识别领域的“猪突猛进”,各种改进和魔改层出不穷。由此作者将开启VideoTransformer系列的讲解,本篇主要介绍了FBAI团队的TimeSformer,这也是第一篇使用纯Transformer结构在视频识别上的文章。如果觉得有用,就请点赞、收藏、关注!paper:https://arxiv.org/abs/2102.05095code(offical):https://github.com/facebookresearch/TimeSformeraccept:ICML2021author:FacebookAI一、前言Transformers(VIT)在图
目前我正在使用这个正则表达式从YoutubeURL中提取视频ID:url.match(/v=([^&]*)/)[1]我怎样才能改变它,以便它也可以从这个没有v参数的YoutubeURL获取视频ID:http://www.youtube.com/user/SHAYTARDS#p/u/9/Xc81AajGUMU感谢阅读。编辑:我正在使用ruby1.8.7 最佳答案 对于Ruby1.8.7,这就可以了。url_1='http://www.youtube.com/watch?v=8WVTOUh53QY&feature=feedf'url
类似的问题,但对于java,Keepingi18nresourcessynced如何保持i18nyamllocals的key同步?即,当将key添加到en.yml时,如何将它们添加到nb.yml或ru.yml?如果我在my_title:"atitle"旁边添加键my_label:"sometextinenglish"我想把它给我的其他本地人我指定,因为我不能做所有的翻译,它应该回到其他语言的英语例如en.ymlsomegroup:my_tile:"atitleinenglish"my_label:"sometextinenglish"othergroup:...我想发出命令,将整个键和
一、什么是web项目ui自动化测试?通过测试工具模拟人为操控浏览器,使软件按照测试人员的预定计划自动执行测试的一种方式,可以完成许多手工测试无法完成或者不易实现的繁琐工作。正确使用自动化测试,可以更全面的对软件进行测试,从而提高软件质量进而缩短迭代周期。二、构建测试用例的“九部曲”(一)创建流程包划分功能模块日常测试活动中,都会根据功能模块进行拆分,所以在设计器中我们可以通过创建流程包的方式来拆分需要测试的功能模块,如下图中操作创建一个电脑流程包并且取名为对应的功能模块名称,如果有多个功能模块就创建多个对应的流程包,实在RPA设计器有易用的图形可视化界面,方便管理较多的功能模块。(二)在流程包
目录FIFO一.自定义同步FIFO1.1代码设计1.2Testbech1.3行为仿真***学习位宽计算函数$clog2()***$clog2()系统函数使用,可以不关注***分布式资源或者BLOCKBRAM二.异步FIFO2.1在FIFO判满的时候有两种方式:2.2异步FIFO为什么要使用格雷码2.2.1介绍格雷码2.2.2格雷码在异步FIFO中的应用2.2.2格雷码判满2.4二进制与格雷码之间的转换2.4.1二进制码转换为格雷码的方法2.4.2格雷码转换为二进制码的方法2.3实现框图2.5实现及仿真代码2.6仿真图验证2.7结论FIFO 这篇更多的是记录FIFO学习,参考了众多优秀的文章,
目录需求基于JavaCV跨平台执行ffmpeg命令[^1]坑一内存不足坑二多个ffmpeg进程并行导致IO负载大,进而导致ioerror?坑三使用Java操作ffmpeg时,有时会卡死坑四Process的waitFor死锁问题及解决办法需求给透明背景的视频自动叠加一张背景图片基于JavaCV跨平台执行ffmpeg命令1我测试发现的本需求的最小依赖:dependency>groupId>org.bytedecogroupId>artifactId>ffmpeg-platform-gplartifactId>version>5.0-1.5.7version>dependency>核心代码:Stri
摘要本论文主要论述了如何使用Python技术开发一个短视频智能推荐,本系统将严格按照软件开发流程进行各个阶段的工作,采用B/S架构,面向对象编程思想进行项目开发。在引言中,作者将论述短视频智能推荐的当前背景以及系统开发的目的,后续章节将严格按照软件开发流程,对系统进行各个阶段分析设计。 短视频智能推荐的主要使用者分为管理员和用户,实现功能包括管理员:首页、个人中心、用户管理、热门视频管理、用户上传管理、系统管理,用户:首页、个人中心、用户上传管理、我的收藏管理,前台首页;首页、热门视频、用户上传、公告信息、个人中心、后台管理等功能。由于本网站的功能模块设计比较全面,所以使得整个短视频智能推荐信