SDL2文章列表git
SDL2入门github
SDL2事件处理ide
SDL2纹理渲染函数
SDL2音频播放学习
前两篇文章分别作了音频和视频的播放,要实现一个完整的简易播放器就必需要作到音视频同步播放了,而音视频同步在音视频开发中又是很是重要的知识点,因此在这里记录下音视频同步相关知识的理解。code
从前面的学习能够知道,在一个视频文件中,音频和视频都是单独以一条流的形式存在,互不干扰。那么在播放时根据视频的帧率(Frame Rate)和音频的采样率(Sample Rate)经过简单的计算获得其在某一Frame(Sample)的播放时间分别播放,**理论**上应该是同步的。可是因为机器运行速度,解码效率等等因素影响,颇有可能出现音频和视频不一样步,例如出现视频中人在说话,却只能看到人物嘴动却没有声音,很是影响用户观看体验。orm
如何作到音视频同步?要知道音视频同步是一个动态的过程,同步是暂时的,不一样步才是常态,须要一种随着时间会线性增加的量,视频和音频的播放速度都以该量为标准,播放快了就减慢播放速度;播放慢了就加快播放的速度,在你追我赶中达到同步的状态。目前主要有三种方式实现同步:视频
比较主流的是第三种,将视频同步到音频上。至于为何不使用前两种,由于通常来讲,人对于声音的敏感度更高,若是频繁地去调整音频会产生杂音让人感受到刺耳不舒服,而人对图像的敏感度就低不少了,因此通常都会采用第三种方式。
在音频中PTS和DTS通常相同。可是在视频中,因为B帧的存在,PTS和DTS可能会不一样。
实际帧顺序:I B B P
存放帧顺序:I P B B
解码时间戳:1 4 2 3
展现时间戳:1 2 3 4
/** * This is the fundamental unit of time (in seconds) in terms * of which frame timestamps are represented. * 这是表示帧时间戳的基本时间单位(以秒为单位)。 **/
typedef struct AVRational{
int num; ///< Numerator 分子
int den; ///< Denominator 分母
} AVRational;
复制代码
时间基是一个分数,以秒为单位,好比1/50秒,那它到底表示的是什么意思呢?以帧率为例,若是它的时间基是1/50秒,那么就表示每隔1/50秒显示一帧数据,也就是每1秒显示50帧,帧率为50FPS。
每一帧数据都有对应的PTS,在播放视频或音频的时候咱们须要将PTS时间戳转化为以秒为单位的时间,用来最后的展现。那如何计算一桢在整个视频中的时间位置?
static inline double av_q2d(AVRational a){
return a.num / (double) a.den;
}
//计算一桢在整个视频中的时间位置
timestamp(秒) = pts * av_q2d(st->time_base);
复制代码
Audio_Clock,也就是Audio的播放时长,从开始到当前的时间。获取Audio_Clock:
if (pkt->pts != AV_NOPTS_VALUE) {
state->audio_clock = av_q2d(state->audio_st->time_base) * pkt->pts;
}
复制代码
尚未结束,因为一个packet中能够包含多个Frame帧,packet中的PTS比真正的播放的PTS可能会早不少,能够根据Sample Rate 和 Sample Format来计算出该packet中的数据能够播放的时长,再次更新Audio_Clock。
// 每秒钟音频播放的字节数 采样率 * 通道数 * 采样位数 (一个sample占用的字节数)
n = 2 * state->audio_ctx->channels;
state->audio_clock += (double) data_size /
(double) (n * state->audio_ctx->sample_rate);
复制代码
最后还有一步,在咱们获取这个Audio_Clock时,颇有可能音频缓冲区还有没有播放结束的数据,也就是有一部分数据实际尚未播放,因此就要在Audio_Clock上减去这部分数据的播放时间,才是真正的Audio_Clock。
double get_audio_clock(VideoState *state) {
double pts;
int buf_size, bytes_per_sec;
//上一步获取的PTS
pts = state->audio_clock;
// 音频缓冲区尚未播放的数据
buf_size = state->audio_buf_size - state->audio_buf_index;
// 每秒钟音频播放的字节数
bytes_per_sec = state->audio_ctx->sample_rate * state->audio_ctx->channels * 2;
pts -= (double) buf_size / bytes_per_sec;
return pts;
}
复制代码
get_audio_clock
中返回的才是咱们最终须要的Audio_Clock,当前的音频的播放时长。
Video_Clock,视频播放到当前帧时的已播放的时间长度。
avcodec_send_packet(state->video_ctx, packet);
while (avcodec_receive_frame(state->video_ctx, pFrame) == 0) {
if ((pts = pFrame->best_effort_timestamp) != AV_NOPTS_VALUE) {
} else {
pts = 0;
}
pts *= av_q2d(state->video_st->time_base); // 时间基换算,单位为秒
pts = synchronize_video(state, pFrame, pts);
av_packet_unref(packet);
}
复制代码
旧版的FFmpeg使用av_frame_get_best_effort_timestamp
函数获取视频的最合适PTS,新版本的则在解码时生成了best_effort_timestamp
。可是依然可能会获取不到正确的PTS,因此在synchronize_video
中进行处理。
double synchronize_video(VideoState *state, AVFrame *src_frame, double pts) {
double frame_delay;
if (pts != 0) {
state->video_clock = pts;
} else {
pts = state->video_clock;// PTS错误,使用上一次的PTS值
}
//根据时间基,计算每一帧的间隔时间
frame_delay = av_q2d(state->video_ctx->time_base);
//解码后的帧要延时的时间
frame_delay += src_frame->repeat_pict * (frame_delay * 0.5);
state->video_clock += frame_delay;//获得video_clock,实际上也是预测的下一帧视频的时间
return pts;
}
复制代码
上面两步得到了Audio_Clock和Video_Clock,这样咱们就有了视频流中Frame的显示时间,而且获得了做为基准时间的音频播放时长Audio clock ,能够将视频同步到音频了。
#define AV_SYNC_THRESHOLD 0.01 // 同步最小阈值
#define AV_NOSYNC_THRESHOLD 10.0 // 不一样步阈值
double actual_delay, delay, sync_threshold, ref_clock, diff;
// 当前Frame时间减去上一帧的时间,获取两帧间的延时
delay = vp->pts - is->frame_last_pts;
if (delay <= 0 || delay >= 1.0) {
// 延时小于0或大于1秒(太长)都是错误的,将延时时间设置为上一次的延时时间
delay = is->frame_last_delay;
}
// 获取音频Audio_Clock
ref_clock = get_audio_clock(is);
// 获得当前PTS和Audio_Clock的差值
diff = vp->pts - ref_clock;
sync_threshold = (delay > AV_SYNC_THRESHOLD) ? delay : AV_SYNC_THRESHOLD;
// 调整播放下一帧的延迟时间,以实现同步
if (fabs(diff) < AV_NOSYNC_THRESHOLD) {
if (diff <= -sync_threshold) { // 慢了,delay设为0
delay = 0;
} else if (diff >= sync_threshold) { // 快了,加倍delay
delay = 2 * delay;
}
}
is->frame_timer += delay;
// 最终真正要延时的时间
actual_delay = is->frame_timer - (av_gettime() / 1000000.0);
if (actual_delay < 0.010) {
// 延时时间太小就设置个最小值
actual_delay = 0.010;
}
// 根据延时时间刷新视频
schedule_refresh(is, (int) (actual_delay * 1000 + 0.5));
复制代码
将视频同步到音频上实现音视频同步基本完成,整体就是动态的过程快了就等待,慢了就加速,在一个你追我赶的状态下实现同步播放。
后面的博客会真正实现一个音视频同步的播放器。