基于FFmpeg和Android的音视频同步播放实现
本文将实现音视频的同步播放。
实现需求
基于FFmpeg实现视频解码,并通过OpenGL ES进行渲染;
基于OpenSL ES进行PCM注入播放;
播放时进行音视频同步;
关于音视频同步原理
本文不打算详细介绍音视频同步的基本原理,网上关于这部分的资源很多。简单的来说,是音视频在编码时,在音频和视频PES包中,打入时间戳信息(PTS),那么在终端解码时,由于音频解码和视频解码的速度可能不一致,如果不进行同步操作,可能声音和画面就不同步了,造成画面超前或者滞后于声音。
一般来说,声音的播放时固定采样率的,所以声音的播放本身是平滑的,因此我们往往基于音频播放的基准(PTS)来进行视频同步,让视频画面的播放速度来匹配音频的解码速度。
音频同步实现
由于参考了前述博文视频和音频播放,所以原理部分不再阐述,重点描述同步相关的代码实现。关注如下两个函数:
double get_audio_clock() {
double pts;
int hw_buf_size, bytes_per_sec, n;
pts = audio_clock;
bytes_per_sec = 0;
n = global_context.acodec_ctx->channels * 2;
bytes_per_sec = global_context.acodec_ctx->sample_rate * n;
hw_buf_size = last_enqueue_buffer_size
- (double(av_gettime() - last_enqueue_buffer_time) / 1000000.0)
* bytes_per_sec;
if (bytes_per_sec) {
pts -= (double) hw_buf_size / bytes_per_sec;
}
//LOGV2("get_audio_clock:pts is %ld", pts);
return pts;
}
// this callback ha
ndler is called every time a buffer finishes playing
void bqPlayerCallback(SLAndroidSimpleBufferQueueItf bq, void *context) {
SLresult result;
//LOGV2("bqPlayerCallback...");
if (bq != bqPlayerBufferQueue) {
LOGV2("bqPlayerCallback : not the same player object.");
return;
}
int decoded_size = audio_decode_frame(decoded_audio_buf,
sizeof(decoded_audio_buf));
if (decoded_size > 0) {
result = (*bqPlayerBufferQueue)->Enqueue(bqPlayerBufferQueue,
decoded_audio_buf, decoded_size);
last_enqueue_buffer_time = av_gettime();
last_enqueue_buffer_size = decoded_size;
// the most likely other result is SL_RESULT_BUFFER_INSUFFICIENT,
// which for this code example would indicate a programming error
if (SL_RESULT_SUCCESS != result) {
LOGV2("bqPlayerCallback : bqPlayerBufferQueue Enqueue failure.");
}
}
}
本文福利, 免费领取C++音视频学习资料包、技术视频,内容包括(音视频开发,面试题,FFmpeg ,webRTC ,rtmp ,hls ,rtsp ,ffplay ,编解码,推拉流,srs)↓↓↓↓↓↓见下面↓↓文章底部点击免费领取↓↓
我们知道bqPlayerCallback函数是注册给OpenSL ES的接口函数,当解码芯片音频数据消耗完毕时,会调用此函数,我们在这个函数里存储了两个变量,
last_enqueue_buffer_time = av_gettime();
last_enqueue_buffer_size = decoded_size;
其中,last_enqueue_buffer_time保存当前的系统时间,last_enqueue_buffer_size存储注入的数据大小。
接下来,函数get_audio_clock(),是获取音频时钟信息的,返回单位是秒,一般是浮点数,这里重点注意如下代码段,
pts = audio_clock;
bytes_per_sec = 0;
n = global_context.acodec_ctx->channels * 2;
bytes_per_sec = global_context.acodec_ctx->sample_rate * n;
hw_buf_size = last_enqueue_buffer_size
- (double(av_gettime() - last_enqueue_buffer_time) / 1000000.0)
* bytes_per_sec;
if (bytes_per_sec) {
pts -= (double) hw_buf_size / bytes_per_sec;
}
return pts;
全局变量audio_clock保存的是最后一次解码音频帧的时钟(时间戳)信息,n表示每声道存储的字节数,这里是16bit格式,所以乘以2。bytes_per_sec表示每秒钟消耗的音频字节数,根据采样率和n计算得到。hw_buf_size表示芯片音频缓存区里剩余未解码的音频数据大小,这里依赖保存的变量last_enqueue_buffer_size和last_enqueue_buffer_time计算得到,很好理解,最后就可以计算得到当前的音频时钟pts并返回。
视频同步实现
视频的同步相对复杂一点,首先建立了两个线程,一个负责解码即video_thread线程,一个负责视频渲染即picture_thread线程,解码后的视频帧(即picture)存储在一个队列中,我们定义成,
VideoPicture pictq[VIDEO_PICTURE_QUEUE_SIZE];
放在GlobalContext全局上下文中。
视频渲染线程picture_thread中,每一帧视频渲染的时间点,是由timer_delay_ms延时时间决定的,而每一帧视频的timer_delay_ms延时时间的计算主要由如下函数计算得到,
void video_refresh_timer() {
VideoPicture *vp;
double actual_delay, delay, sync_threshold, ref_clock, diff;
if (global_context.pictq_size == 0) {
schedule_refresh(1);
} else {
vp = &global_context.pictq[global_context.pictq_rindex];
video_current_pts = vp->pts;
global_context.video_current_pts_time = av_gettime();
delay = vp->pts - global_context.frame_last_pts;
if (delay <= 0 || delay >= 1.0) { // 非法值判断
delay = global_context.frame_last_delay;
}
global_context.frame_last_delay = delay;
global_context.frame_last_pts = vp->pts;
ref_clock = get_master_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) {
//av_log(NULL, AV_LOG_ERROR, "video_refresh_timer : skip. \n");
LOGV("video_refresh_timer : skip. \n");
delay = 0;
} else if (diff >= sync_threshold) {
//av_log(NULL, AV_LOG_ERROR, "video_refresh_timer : repeat. \n");
LOGV("video_refresh_timer : repeat. \n");
delay = 2 * delay;
}
} else {
//av_log(NULL, AV_LOG_ERROR,
// " video_refresh_timer : diff > 10 , diff = %f, vp->pts = %f , ref_clock = %f\n",
// diff, vp->pts, ref_clock);
LOGV(
" video_refresh_timer : diff > 10 , diff = %f, vp->pts = %f , ref_clock = %f\n",
diff, vp->pts, ref_clock);
}
global_context.frame_timer += delay;
actual_delay = global_context.frame_timer - (av_gettime() / 1000000.0);
if (actual_delay < 0.010) { //每秒100帧的刷新率不存在
actual_delay = 0.010;
}
schedule_refresh((int) (actual_delay * 1000 + 0.5)); //add 0.5 for 进位
if (vp->pFrame)
video_display(vp->pFrame);
if (++global_context.pictq_rindex >= VIDEO_PICTURE_QUEUE_SIZE) {
global_context.pictq_rindex = 0;
}
pthread_mutex_lock(&global_context.pictq_mutex);
global_context.pictq_size--;
pthread_cond_signal(&global_context.pictq_cond);
pthread_mutex_unlock(&global_context.pictq_mutex);
}
}
其中,get_master_clock()函数获取系统时钟,我们这里一般配置成音频的解码时间戳(PTS),然后计算视频时间戳和参考时间的差值,
ref_clock = get_master_clock();
diff = vp->pts - ref_clock;
以下代码判断差值的大小,如果小于门限值,则不延时,也就是说要迅速播放下一帧,类似于快进,如果大于门限值,则延时加倍,也就是说要慢点播放下一帧,当然可能一帧并不能马上达到音视频之间的同步,但是可以通过多帧的累积,最终使两者同步。
if (diff <= -sync_threshold) {
//av_log(NULL, AV_LOG_ERROR, "video_refresh_timer : skip. \n");
LOGV("video_refresh_timer : skip. \n");
delay = 0;
} else if (diff >= sync_threshold) {
//av_log(NULL, AV_LOG_ERROR, "video_refresh_timer : repeat. \n");
LOGV("video_refresh_timer : repeat. \n");
delay = 2 * delay;
}
注意video_refresh_timer()函数中,还有一个actual_delay变量,奇怪的是,delay变量就行了,为何还有个actual_delay变量呢?原来,我们的程序在处理视频数据或者执行时,总要消耗时间,因此实际的delay时间总是要小于上述计算得到的delay值,actual_delay的计算如下,frame_timer是记录的上一次帧显示的系统时间加上了delay的值,av_gettime()是当前的系统时间,两者的差值刚好是下一帧图像显示的延时时间。
actual_delay = global_context.frame_timer - (av_gettime() / 1000000.0);
下面函数完成一帧图像的显示,这和以前博文介绍的实现方法一致。
video_display(vp->pFrame);
几个时间变量
关于代码中出现的几个关于时间的变量,分别解释如下:
pFrame->pkt_pts
这个是码流里存储的PTS值,就是一个计数器,相对于时基的一个计数值
av_q2d(global_context.vstream->time_base)
把分数的时基,转成浮点数(double)型的时间基准,就是最小分辨率时间段吧
pFrame->pkt_pts*av_q2d(global_context.vstream->time_base)
这是把码流里的PTS计数值,转换成时间戳的形式,单位是秒(如0.0001秒)
av_gettime
获取当前系统时间,单位微秒us
vp->pts = pFrame->pkt_pts*av_q2d(global_context.vstream->time_base)
当前要显示的帧的时间戳,单位是秒(如0.0001秒),是 倍数*(1/时基)
video_current_pts
保留最后刷新的帧的vp->pts值
global_context.video_current_pts_time
当前显示帧时的系统时间,单位微秒
global_context.frame_last_pts
看起来和video_current_pts一个意思?最后刷新的帧的vp->pts值
global_context.frame_last_delay
记录最后的延时时间,用于下次PTS非法或者跳变时,采用上一次的值
global_context.frame_timer
用于帧的定时器时间计算,单位微秒
delay = vp->pts - global_context.frame_last_pts
get_master_clock()
返回系统的pts时间戳,是 倍数*(1/时基)
delay = vp->pts - global_context.frame_last_pts
两帧之间的时间戳差值
diff = vp->pts - ref_clock
当前帧和系统时钟之间的差值
global_context.frame_timer
应该是指每一帧视频的显示时刻,单位是秒,记住程序open媒体的时候有一个初始值
本文福利, 免费领取C++音视频学习资料包、技术视频,内容包括(音视频开发,面试题,FFmpeg ,webRTC ,rtmp ,hls ,rtsp ,ffplay ,编解码,推拉流,srs)↓↓↓↓↓↓见下面↓↓文章底部点击免费领取↓↓
相关文章
- android系统开机画面_Android开机画面
- android 的hook技术,Android Native Hook技术(一)
- Android 屏幕适配
- android进阶之了解Android系统与开机过程
- android activity singletask,Android Activity启动模式之singleTask实例详解
- android vlc 中文字幕,解决Android版vlc中文乱码问题
- android sdk下载安装教程_android studio安装sdk
- android activitymanager 系统api_Android view
- Android resource linking failed_android sdk location should not
- android studio输出文字_androiditem长按删除
- 【Android应用开发】 Android 崩溃日志 本地存储 与 远程保存
- 【Android 高性能音频】Oboe 开发流程 ( Oboe 音频帧简介 | AudioStreamCallback 中的数据帧说明 )
- 【Android 进程保活】应用进程拉活 ( 账户同步拉活 | 账号添加 | 源码资源 )
- 【错误记录】编译 Android 版本的 ijkplayer 报错 ( ./init-android.sh: 第 37 行: cd: android/contrib/: 没有那个文件或目录 )
- 【ijkplayer】编译 Android 版本的 ijkplayer ② ( 切换到 k0.8.8 分支 | 执行 init-android.sh 脚本进行初始化操作 )
- 【Android Gradle 插件】组件化中的 Gradle 构建脚本实现 ⑤ ( 优化 Gradle 构建脚本 | 构建脚本结构 | 闭包定义及用法 | 依赖配置 | android 块配置 )
- [android] 看博客学习Android常见的几种RuntimeException详解手机开发
- 如何在 Android 上借助 Wine 来运行 Windows Apps
- 安卓编年史(8):Android 1.5 Cupcake——虚拟键盘打开设备设计的大门
- Android系统基于Linux内核,实现移动设备突破极限。(android linux内核)
- Android打开GPS导航并获取位置信息返回null解决方案
- Android实现歌曲播放时歌词同步显示具体思路
- android使用handlerui线程和子线程通讯更新ui示例
- 在Android开发中替换资源图片不起作用的解决方法