基于FFmpeg和Android的音视频同步播放实现

发布时间:2018-06-23 22:16,

浏览次数:575

, 标签:

FFmpeg

Android

前言

在以前的博文中,我们通过FFmpeg解码,并基于OpenGL ES完成了视频的渲染,也完成了基于OpenSL ES实现的native音频注入播放。

本文将这两部分代码进行合并,并实现音视频的同步播放。

实现需求

* 基于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."); } } }

我们知道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.pict

q[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媒体的时候有一个初始值

遗留问题

* 本程序在解码avi等视频格式时,无法播出图像,应该是avcodec_decode_video2解码后的数据是分段的,并不能一次完整解出;

*

在我的测试机器上,宏定义VIDEO_PICTURE_QUEUE_SIZE取值不同时,视频可能会出现跳帧卡顿的现象,调整到30帧时,表现最好,具体原因没有查到。

GitHub源码

请参考完整的源码路径:

https://github.com/ericbars/FFmpegAVSync

android 声音同步 测试,基于FFmpeg和Android的音视频同步播放实现相关推荐

  1. Android WebView加载H5音视频自动播放、关闭Activity停止播放

    在Android加载H5,实现H5中的音视频自动播放  在Activity中添加代码: if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELL ...

  2. WebRTC 音视频同步分析

    文中提到的代码引用自 libwebrtc M96 版本 https://github.com/aggresss/libwebrtc/tree/M96 0x00 前言 WebRTC 音频和视频分别通过不 ...

  3. 用Excel分析音视频同步

    声明:     这里主要介绍如何运用Excel来分析音视频是否同步,希望可以对大家有所帮助. 介绍:     学习音视频就一定要知道做音视频同步,而现在我们来分析音视频同步的工具也是有的,比如easy ...

  4. WebRTC音视频同步详解

    WebRTC音视频同步详解 1 WebRTC版本 2 时间戳 2.1 视频时间戳 2.2 音频时间戳 2.3 NTP时间戳 2 延迟 3 同步 3.1 一张图看懂音视频同步 3.2 音视频相对延迟 3 ...

  5. ffmpeg播放器 android,Android使用FFmpeg(六)--ffmpeg实现音视频同步播放

    关于 准备工作 正文 依旧依照流程图来逐步实现同步播放: 从流程图可以看出,实现同步播放需要三个线程,一个开启解码的装置得到packet线程,然后分别是播放音频和视频的线程.这篇简书是以音频播放为基准 ...

  6. ffmpeg录制桌面视频和系统内部声音(音视频同步)

    本文抓取的是电脑内部声音,需要先安装软件screen capture recorder,这个软件大小有50M,太大,安装后,里面有一个脚本文件,如下所示: 打开这个文件,可以看到如下内容: 这个文件比 ...

  7. 深入理解Android音视频同步机制(二)ExoPlayer的avsync逻辑

    深入理解Android音视频同步机制(一)概述 深入理解Android音视频同步机制(二)ExoPlayer的avsync逻辑 深入理解Android音视频同步机制(三)NuPlayer的avsync ...

  8. Android基于腾讯云实时音视频实现类似微信视频通话最小化悬浮

    最近项目中有需要语音.视频通话需求,看到这个像环信.融云等SDK都有具体Demo实现,但咋的领导对腾讯情有独钟啊,IM要用腾讯云IM,不妙的是腾讯云IM并不包含有音视频通话都要自己实现,没办法深入了解 ...

  9. Android 短视频 SDK 转场特效的音视频同步分析

    在短视频的应用场景中,经常存在用户拍摄的两个或者多个视频生成一个视频的需求,为了达到两个视频平滑过渡,就需要在两个视频中间添加转场效果. 由于导入视频的帧率.码率等参数都不一致,如何保证在添加完转场效 ...

  10. android 音视频同步_如何轻松地将音乐,视频和照片与Android同步

    android 音视频同步 Apple users have iTunes to synchronize their media libraries back and forth, but what ...

最新文章

  1. 虚拟机网络设置方法——转载
  2. vue循环出来的数据,通过点击事件改变了数据,但是视图却没有更新
  3. clob和blob是不是可以进行模糊查询_为省几十元买假内存条?金士顿内存条真伪查询与辨别方法...
  4. 使用SVN提示“工作副本已经锁定”的解决办法
  5. 李宏毅机器学习(七)Bert and its family
  6. 甲流疫情死亡率(信息学奥赛一本通-T1011)
  7. HDU Problem 1285 确定比赛名次【拓扑排序】
  8. SUM OF SUB RECTANGLE AREAS(打表+oeis+c++大数类板子)
  9. 用ZK UI解决storm 读取Kafka时的Fetch offset *** is out of range for topic , resetting offset
  10. 路由器关闭DHCP之后连接不到路由器设置界面?
  11. 短视频优质作者必备|配音神器分享|那些你刷视频时肯定听过的声音
  12. 如何用Java读取单元格的数据_Java读取Excel中的单元格数据
  13. 中国广电剑未出鞘,但中国联通和中国电信已吓得瑟瑟发抖
  14. 计算机命令窗口怎么打开,如何打开命令行窗口_教你在win7上直接打开命令行窗口 - 驱动管家...
  15. 区块链教程(1)——区块链原理
  16. Unix/Linux中rc代表什么意思
  17. esxi-linux-lvm磁盘扩容
  18. 简单几行命令让pip升级
  19. java设置北京时间的时区
  20. 快手如何直播引流?快手直播推广方法分享

热门文章

  1. 一文说明白ECDSA spec256k1 spec256r1 EdDSA ed25519千丝万缕的关系
  2. 初探强化学习(2)rollout算法
  3. 饥饿游戏3:嘲笑鸟(下)[The Hunger Games: Mockingjay - Part 2]
  4. mysql ndb 安装_mysql NDB的安装配置使用示例
  5. AXI_DMAC的寄存器说明
  6. matplotlib 3D绘图警告;MatplotlibDeprecationWarning: Axes3D(fig) adding itself to the figure is deprecate
  7. linux内存占用率高怎么办,Linux下如何解决高内存使用率问题?
  8. ManiGAN Text-Guided Image Manipulation
  9. 数字签名和电子签名有什么不一样?
  10. 什么是全栈工程师,如何成为全栈工程师