安卓应用中的 AV 同步


安卓应用中的 AV 同步

精确的音频和视频同步是媒体播放的关键性能衡量标准之一。通常情况下,在录制设备上同时录制的音频和视频需要在播放设备(例如,电视和显示器)上同时播放。遵循以下指南,确保运行安卓 API 级别 19+ 的设备正确进行音频视频同步。

音频视频同步理论

一起处理的最小数据单位称为。 音频和视频流都被切片成帧,所有帧都被标记为在特定时间戳显示。音频和视频可以独立下载和解码,但具有匹配时间戳的音频和视频帧应该一起显示。理论上,如果您想匹配音频和视频处理,有三种 AV 同步解决方案可用:

  • 连续播放音频帧: 使用音频播放位置作为主时间参考点,并将视频播放位置与该参考点匹配。
  • 使用系统时间作为参考点: 将音频和视频播放与系统时间匹配。
  • 使用视频播放作为参考点: 让音频匹配视频。

第一个选项是唯一一个具有持续音频数据流的选项,无需对呈现时间、播放速度或音频帧的持续时间进行任何调整。对这些参数的任何调整都很容易被人的耳朵听出来,并会导致令人不安的音频故障,除非对音频重新采样;但是,重新采样也会改变音高。因此,一般多媒体应用应使用音频播放位置作为主时间参考点。以下段落讨论了这一解决方案。(另外两个选项不在本文档的范围之内。)

在应用中维护音频视频同步

音频和视频的管道必须同时使用相同的时间戳渲染其帧。音频播放位置用作主时间参考点,而视频管道只输出与最新渲染的音频帧匹配的视频帧。对于所有可能的实现,准确计算上次渲染的音频时间戳都至关重要。安卓提供了多个 API 来查询音频时间戳和音频管道各个阶段的延迟。以下指南介绍了最佳实践。

1.如果您使用的是定制媒体播放器

在定制媒体播放器中,应用可完全控制音频和视频数据流,并且知道需要多长时间来解码音频和视频数据包。该应用还可以自由地增加或减少缓冲的视频数据量,以保持连续播放。视频管道需要根据音频管道呈现的时间戳进行调整。应使用以下两个 API:

1.1 AudioTrack.getTimestamp() (API level 19+)

如果这个音频管道支持查询最新渲染的时间戳,则 getTimestamp() 方法提供了一种简单的方式来确定我们要查找的值。如果时间戳可用,AudioTimestamp 实例将随呈现该帧的估计时间一起填入一个位置(以帧为单位)。此信息可用于控制视频管道,以将视频帧与音频帧匹配。

请注意以下事项:

  • 建议的时间戳查询频率为每 10 秒一次到每分钟一次。可能出现轻微的偏斜,但预计不会发生突然变化,因此无需更频繁地查询时间戳。
  • 当时间戳正确返回时,应用应该信任这些值,而无需使用任何额外的硬编码偏移量。非常不鼓励添加实验值。它们不是独立于平台的,并且管道可能随时更新(例如,连接蓝牙接收器时),从而导致任何以前正确的值不再准确。

有关如何包括可用参数和返回值的详细信息,请参阅安卓文档中的 getTimestamp() 方法。

1.2 getPlaybackHeadPosition() (API Level 3+)

如果音频管道不支持查询上一节中所述的最新渲染的音频时间戳,则需要另一种方法。

该解决方案由两部分组成,使用 AudioTrack 类的两个独立函数。第一部分根据方法 getPlaybackHeadPosition() 返回的当前头位置(以帧为单位)计算最新的音频时间戳:

private long framesToDurationUs(long frameCount) {
    return (frameCount * C.MICROS_PER_SECOND) / sampleRate;
}

long timestamp = framesToDurationUs(audioTrack.getPlaybackHeadPosition());

上面计算的 timestamp 值不考虑较低层引入的延迟;因此,需要进行一些调整。

该解决方案的第二部分使用 getLatency() 函数确定缺失的延迟值。由于 getLatency() 方法是 AudioTrack 类的隐藏成员(不是公共 SDK 的一部分),因此需要 reflection 才能访问它:

Method getLatencyMethod;
if (Util.SDK_INT >= 18) {
  try {
    getLatencyMethod =
     android.media.AudioTrack.class.getMethod("getLatency", (Class < ? > []) null);
   } catch (NoSuchMethodException e) {
      // 不能保证此方法存在。什么都不做。
   }
}`

返回的值包括混音器的延迟、音频硬件驱动程序,以及 AudioTrack 缓冲区引入的延迟。要仅获取 AudioTrack 下各层的延迟,必须减去缓冲区 (bufferSizeUs) 引入的延迟。

long bufferSizeUs = isOutputPcm ? framesToDurationUs(bufferSize / outputPcmFrameSize) : C.TIME_UNSET;
int audioLatencyUs = (Integer) getLatencyMethod.invoke(audioTrack, (Object[]) null) * 1000L - bufferSizeUs;

将这两个部分结合在一起,计算由音频管道渲染的上一时间戳最接近的近似值的完整解决方案如下所示:

int latestAudioFrameTimestamp = framesToDurationUs(audioTrack.getPlaybackHeadPosition() - audioLatencyUs;

Amazon ExoPlayer 端口AudioTrackPositionTracker 类中可以看到示例实现。

2.使用 ExoPlayer

强烈建议在 Fire OS 上使用 ExoPlayer 进行媒体播放。Amazon ExoPlayer 端口与各代 Fire TV 设备均兼容,提供了许多额外的修复,并且还避免了在非亚马逊平台上更改原始 ExoPlayer 行为。

在 AV 同步方面,Amazon ExoPlayer 端口使用前面章节中描述的方法,为“API 21 级”之前的亚马逊设备维持正确的音频延迟计算。当此端口用作媒体播放器时,它将自动执行同步。您无需针对延迟手动调整时间戳。

3.使用标准安卓媒体播放器

Fire OS 支持用于处理音频和视频播放的标准安卓 MediaPlayer 类。这些媒体类可以根据 AV 同步要求处理基本媒体播放。但是,它们的能力在多个方面受到限制。强烈建议使用 Amazon ExoPlayer 端口(或付费媒体播放器选项之一)。

4.使用安卓 NDK 的 OpenSL ES 框架

OpenSL ES 通过其标准 API 查询音频延迟时,它只获取 Audio Flinger 报告的音频硬件延迟。不包括软件引入的任何延迟(主要是音轨缓冲)。为了获得包括硬件和软件音频延迟的准确音频延迟值,在 Fire OS 6 中,OpenSL ES API android_audioPlayer_getConfig() 已更新,以报告完整的音频延迟。

以下代码示例演示了如何使用这些函数来计算软件层和硬件层引入的延迟值。

4.1 当音频播放器对象是 CaudioPlayer 类型时

// 使用 OpenSL 框架时用于获取 Fire OS 软件 + 硬件音频延迟的示例代码。
SLuint32 audioLatency = 0;
SLuint32 valueSize = 0;

// 变量 ap 是 CaudioPlayer 类型的音频播放器对象。
if (android_audioPlayer_getConfig((CAudioPlayer * ) & ap, (const SLchar * )
  "androidGetAudioLatency",
  (SLuint32 * ) &valueSize, (void *) &audioLatency) == SL_RESULT_SUCCESS) {
    // 硬件 + 软件音频延迟在“SLuint32”类型的变量 audioLatency 中填充。
} else {
    // 调用您当前的 get_audio_latency API。您将只查询硬件音频延迟值。
}

4.2 如果您的音频播放器对象由 SLEngineItf 接口 API CreateAudioPlayer() 创建

如果您的音频播放器是通过以下方式创建的:

result = (*engine)->CreateAudioPlayer(engine, &playerObject, &audioSrc, &audioSink, NUM_INTERFACES, ids, req);

您可以使用下面的示例代码来获取创建的音频播放器的总音频延迟。请注意,变量 playerObject 必须指向与调用时使用的方法 CreateAudioPlayer() 指向同一实例。

// 使用 OpenSL 框架时用于获取 Fire OS 软件 + 硬件音频延迟的示例代码。

// 创建支持延迟查询接口的 playerObject
// 请求在 CreateAudioPlayer 中包含 SL_IID_ANDROIDCONFIGURATION
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`);

// 必须在延迟查询之前实现 playerObject
result = (*playerObject)->Realize(playerObject, SL_BOOLEAN_FALSE);

if (result != SL_RESULT_SUCCESS) {
    ALOGE("playerObject realize failed with result %d", result);
    return;
}

// 获取音频播放器的界面
result = (*playerObject)->GetInterface(playerObject,
                                       SL_IID_ANDROIDCONFIGURATION,
                                       &playerConfig);
if (result != SL_RESULT_SUCCESS) {
    ALOGE("config GetInterface failed with result %d", result);
    return;
}

// 获取音频播放器的延迟
 result = (*playerConfig)->GetConfiguration(playerConfig,
           (const SLchar * )"androidGetAudioLatency", &paramSize, &audioLatency);

if (result == SL_RESULT_SUCCESS) {
    // 硬件 + 软件音频延迟在 SLuint32 类型的变量 audioLatency 中填充。
} else {
    // 调用您当前的 get_audio_latency API。您只会获得硬件音频延迟值。
}

其他资源

以下外部资源可能有助于了解有关 AV 同步的更多信息。