目前安卓系统手机播放器在输出设备为蓝牙耳机时会出现严重音视频不同步现象,严重影响用户体验。本文旨在指导第三方视频播放应用正确获取音频相关信息并使用来保证音视频同步。
精确的音视频同步是媒体播放的关键性能指标之一。一般来说,在录音设备上同时录制的音频和视频需要在播放设备(例如手机,电视,媒体播放器)上同时播放。为了实现设备上的音视频同步可以按如下指南操作。
无
| Abbreviations缩略语 | Full spelling 英文全名 | Chinese explanation 中文解释 |
| PTS | Presentation Time Stamp | 送显时间戳 |
| DTS | Decoding Time Stamp | 编码时间戳 |
音视频数据的最小处理单元称为帧。音频流和视频流都被分割成帧,所有帧都被标记为需要按特定的时间戳显示。音频和视频可以独立下载和解码,但就具有匹配时间戳的音频和视频帧应同时呈现,达到A/V同步的效果。

理论上,因为音频通路存在时延,匹配音频和视频处理,有三种A/V同步解决方案可用:
(1)连续播放音频帧:使用音频播放位置作为主时间参考,并将视频播放位置与其匹配。
(2)使用系统时间作为参考:将音频和视频播放与系统时间匹配。
(3)使用视频播放作为参考:让音频匹配视频。
第一个选项是唯一一个具有连续音频数据流的选项,没有对音频帧的显示时间、播放速度或持续时间进行任何调整。这些参数的任何调整都很容易被人的耳朵注意到,并导致干扰的音频故障,除非音频被重新采样;但是,重新采样也会改变音调。因此,一般的多媒体应用应使用音频播放位置作为主时间参考。以下段落将讨论此解决方案。(其他两个选项不在本文档的范围内)
音频和视频的管道必须同时以相同的时间戳呈现每帧数据。音频播放位置用作主时间参考,而视频管道只输出与最新渲染音频帧匹配的视频帧。对于所有可能的实现,精确计算最后一次呈现的音频时间戳是至关重要的。Android提供了几个API来查询音频管道各个阶段的音频时间戳和延迟。
下面按第三方播放器采用的播放器框架区分,分别描述如何获取到音轨的时间戳和时延信息。
在定制的媒体播放器中,应用程序完全控制音频和视频数据流,并知道解码音频和视频数据包需要多长时间。应用程序还可以自由地增加或减少缓冲视频数据量,以保持连续播放。视频管道需要调整为音频管道呈现的时间戳。应该使用以下两个API。
音频管道支持查询最新呈现的时间戳,getTimeStamp() 方法提供了一种简单的方法来确定我们要查找的值。如果时间戳可用,则audioTimestamp实例将填充以帧单位表示的位置,以及显示该帧时的估计时间。此信息可用于控制视频管道,使视频帧与音频帧匹配。
注意以下事项:
(1)建议查询时间戳的频率从每10秒一次到每分钟一次。有可能存在轻微误差,但不会突然跳变,因此不需要频繁的查询时间戳。
(2)正确返回时间戳时,应用程序应信任这些值,而不使用任何其他硬编码偏移量。强烈反对增加实验值。它们不是平台独立的,管道可能随时更新(例如,连接蓝牙接收器时),从而使以前正确的值不准确。
- int latestAudioFrameTimestamp = audioTrack.getTimeStamp();
有关可用参数和返回值的详细信息,请参阅Android文档中的getTimeStamp() 方法。
如果音频管道不支持查询上一节所述的最新呈现音频时间戳,则需要另一种方法。
这个解决方案由两部分组成,使用audioTrack类与AudioSystem类的独立函数。第一个根据audioTrack.getPosition() 方法返回的当前缓存区头部位置计算最新的帧的音频时间戳;第二个根据AudioSystem.getOutputLatency()方法返回硬件延时。
- private long framesToDurationUs(long frameCount) {
- return (frameCount * C.MICROS_PER_SECOND) / sampleRate;
- }
- long timestamp = framesToDurationUs(audioTrack.getPosition());
- int latestAudioFrameTimestamp = framesToDurationUs(audioTrack.getPosition()– AudioSystem.getOutputLatency();
该播放器已经实现了3.1 第三方定制媒体播放器。
Android的标准播放器已经实现了3.1 第三方定制媒体播放器。
当前的OpenSLES框架无法通过已有接口获取当前音轨的最新帧时间戳信息。只能根据延时信息及播放器写入数据时间戳来计算音轨的最新帧时间戳信息。目前OpenSLES调用流程如下:

针对使用OpenSL ES框架播放器,Player(Native)无法通过OpenSles调用getposition(), gettimestamp(), latency()。建议通过如下方式获取音轨时延信息。
播放器可同过Framework调用AudioSystem接口获取OutPutLatency信息。

应用通过AudioSystem接口获取到硬件时延信息,并通过自身计算获得软件延时信息。
音频时延 = 硬件时延 + 软件时延;
软件时延计算方式如下:
- long bufferSizeUs = isOutputPcm ? framesToDurationUs(bufferSize / outputPcmFrameSize) : C.TIME_UNSET;
播放器可同过加载libaudioclient.so的方式调用AudioSystem接口获取OutPutLatency信息。具体步骤参照上图。
需要注意:Android8.0.0_r4以下版本,AudioSystem对应库文件为libmedia.so。

第三方播放器直接调用OpenSlES接口获取当前音轨时延。目前第三方应用使用的头文件为OpenSLES.h。扩展接口OpenSLES_Android.h中已经新增接口,可以使上层直接获取到当前音轨时延。
1、音频播放器对象为caudioplayer_类型
- // Sample code for getting Fire OS software+hardware audio latency when using OpenSL framework.
- SLuint32 audioLatency = 0;
- SLuint32 valueSize = 0;
- // variable ap is the audio player object of type CAudioPlayer.
- if (android_audioPlayer_getConfig((CAudioPlayer * ) & ap, (const SLchar * )
- "androidGetAudioLatency",
- (SLuint32 * ) & valueSize, (void * ) & audioLatency) == SL_RESULT_SUCCESS) {
- // The hardware + software audio latency is filled into variable audioLatency of type `SLuint32`.
- } else {
- // Call your current get_audio_latency API. You will only query hardware audio latency value.
- }
2、 音频播放器对象是由slenginetf接口api createaudioplayer()创建
目前已知华为播放器是由以下方式创建的音频播放器。
播放器由如下方式创建:
- result = ( * engine) - > CreateAudioPlayer(engine, & playerObject, & audioSrc, & audioSink, NUM_INTERFACES, ids, req);
- }
可参照如下代码获取音轨时延信息:
- // Sample code for getting Fire OS software+hardware audio latency when using OpenSL framework.
- // Create playerObject with latency query interface support
- // SL_IID_ANDROIDCONFIGURATION is requested to be included on CreateAudioPlayer
- const SLInterfaceID ids[] = {
- SL_IID_ANDROIDCONFIGURATION
- };
- const SLboolean req[] = {
- SL_BOOLEAN_TRUE
- };
- SLint32 result = 0;
- result = ( * engine) - > CreateAudioPlayer(engine, & playerObject, &
- audioSrc, & audioSink, 1 /* size of ids & req array */ , ids, req);
- if (result != SL_RESULT_SUCCESS) {
- ALOGE("CreateAudioPlayer failed with result %d", result);
- return;
- }
- SLAndroidConfigurationItf playerConfig;
- SLuint32 audioLatency = 0;
- SLuint32 paramSize = sizeof(`SLuint32`);
- // Realizing playerObject is required before latency query
- result = ( * playerObject) - > Realize(playerObject, SL_BOOLEAN_FALSE);
- if (result != SL_RESULT_SUCCESS) {
- ALOGE("playerObject realize failed with result %d", result);
- return;
- }
- // Get the audio player's interface
- result = ( * playerObject) - > GetInterface(playerObject,
- SL_IID_ANDROIDCONFIGURATION, &
- playerConfig);
- if (result != SL_RESULT_SUCCESS) {
- ALOGE("config GetInterface failed with result %d", result);
- return;
- }
- // Get the audio player's latency
- result = ( * playerConfig) - > GetConfiguration(playerConfig,
- (const SLchar * )
- "androidGetAudioLatency", & paramSize, & audioLatency);
- if (result == SL_RESULT_SUCCESS) {
- // The hardware+software audio latency is filled in SLuint32 type of variable audioLatency.
- } else {
- // Call your current get_audio_latency API. You will only get hardware audio latency value.
- }
华为手机平台建议如下音视频接口调用,各第三方应用根据应用实现框架决定调用如下接口之一。推荐使用“第三方定制媒体播放器”。
| 方式 | 获取音频信息 | 实现步骤 | 优点 | 缺点 |
| 第三方定制媒体播放器 | 时间戳 | audiotrack.getTimeStamp() | 可以精准获取端侧播放音频的时间戳 | 无 |
| 音频缓存区头部位置+输出设备时延 | audiotrack.getPosition()+AudioSystem.getOutputLatency() | 可以相对精准获取端侧播放音频的时间戳 | 无 | |
| EXOPlayer | 音频时间戳 | audiotrack.getTimeStamp() | 第三方播放器,已经实现AV同步,相对较精准。 | 扩展性差 |
| 音频缓存区头部位置+输出设备时延 | audiotrack.getPosition()+AudioSystem.getOutputLatency() | 第三方播放器,已经实现AV同步,相对较精准。 | 扩展性差 | |
| 标准Android NuPlayer | 音频时间戳 | audiotrack.getTimeStamp() | Android播放器,已经实现AV同步,相对较精准。 | 扩展性差 |
| 音频缓存区头部位置+输出设备时延 | audiotrack.getPosition()+AudioSystem.getOutputLatency() | Android播放器,已经实现AV同步,相对较精准。 | 扩展性差 | |
| Android NDK OpenSL ES框架 | 音频通路时延 | AudioSystem.getOutputLatency()获取latency(JAVA) | NA | 需要进行JAVA-Native侧交互 |
| 音频通路时延 | AudioSystem.getOutputLatency()获取latency(Native) | NA | 需要加载libaudioclient.so才可以调用(不同Android版本,名字有变化。) | |
| 音频通路时延 | OpenSLES.getconfig()获取latency(Native) | 第三方播放器修改最少;Native侧交互,高效。 | 目前OpenSLES没有实现扩展,需实现扩展。 |
华为手机EMUI,音频Framework通过日志打印的方式,验证音频时间信息是否被第三方应用正确获取和调用。
测量手机播放器音频数据下发到耳机发声音的端到端时延。
手机与耳机建立蓝牙连接,手机上加载音视频时延测试APK,产生视频和声音数据时,同步触发拉LED闪光,耳机出声后通过声控LED发光。摄像机同步拍摄。
拍摄后通过数帧来判断声音和视频的同步时延差。

耳机出声音到声控LED灯点亮的时延,理想情况下,声音340m/s的传输速度,当两者距离在34cm范围内,时延为1ms。此外,还需要考虑器件的实际因素。
高速相机的时延误差,取决于拍摄帧率,以帧率240fps计算,1帧误差范围为4.2ms。
手机与耳机建立蓝牙连接,手机播放视频同时声音从耳机出声。将音量调整到最大。摄像机靠近耳机同步拍摄。
拍摄后通过数帧来判断声音和视频的同步时延差。

Email:lichaoone.li@huawei.com