Android应用中的AV同步
精确的音频和视频同步,是媒体播放的关键性能衡量标准之一。通常情况下,在录制设备上同时录制的音频和视频,需在播放设备(例如,电视和显示屏)上同时播放。按照以下指南操作,确保运行Android API(19级)设备上的音频-视频正常同步。
- 音频视频同步理论
- 在应用中保持音频视频同步
- 1.使用ExoPlayer
- 2.若使用定制媒体播放器
- 3.使用标准Android媒体播放器
- 4.使用Android NDK的OpenSL ES框架
- 其他资源
音频视频同步理论
一起处理的最小数据单位称为帧。 音频和视频流均切成帧,所有帧均作标记,显示时间戳。音频和视频可独立下载和解码,但时间戳匹配的音频和视频帧应一起呈现。理论上,若需匹配音频和视频处理,有三种AV同步解决方案:
- 连续播放音频帧: 使用音频播放位置作为主要时间参考点,并将视频播放位置与该参考点匹配。
- 使用系统时间作为参考点: 将音频和视频播放与系统时间匹配。
- 使用视频播放作为参考: 将音频匹配视频。
第一个选项是唯一具有持续音频数据流的选项,无需调整播放时间、播放速度、音频帧的持续时间。除非对音频重新采样,否则对上述参数进行调整后,很容易被人听出异样,而且可能会出现音频故障,导致干扰用户体验。因此,一般多媒体应用应将音频播放位置作为主要时间参考点。下文对该解决方案进行讨论。(另外两个选项不在本文档的讨论范围之内。)
在应用中保持音频视频同步
音频和视频的管道必须同时采用相同的时间戳对帧进行渲染。音频播放位置用作主要时间参考点,而视频管道只输出与最新渲染的音频帧匹配的视频帧。对于可能采用的所有实现方式,准确计算上次渲染的音频时间戳都是必不可少的。Android通过多个API查询音频时间戳,以及音频管道各个阶段的延迟。以下指南介绍了最佳实践。
1.使用ExoPlayer
强烈建议在Fire OS上使用ExoPlayer进行媒体播放。Amazon ExoPlayer端口与各代Fire TV设备均兼容,作了很多其他修复,避免了在非亚马逊平台上更改原始ExoPlayer。
在AV同步方面,针对“API 21级”之前的亚马逊设备,ExoPlayer亚马逊端口利用下节所述方法,进行音频延迟计算。当此端口用作媒体播放器时,自动执行同步。您无需针对延迟手动调整时间戳。
2.如果您使用的是定制媒体播放器
在定制媒体播放器中,应用可完全控制音频和视频数据流,知道需要多久来解码音频和视频数据包。应用还可随意增加或减少缓冲的视频数据量,以保持连续播放。需要根据音频管道所示的时间戳,调整视频管道。应采用以下两个API:
2.1 AudioTrack.getTimestamp() (API Level 19+)
如果此音频管道支持查询上次渲染的时间戳,则getTimestamp()
方法提供了一种非常简单的方式来确定要查找的值。如果时间戳可用,AudioTimestamp
实例中将填入一个位置(以帧为单位),以及呈现该帧的估计时间。此信息可用于控制视频管道,以将视频帧与音频帧匹配。
请注意以下事项:
- 建议的时间戳查询频率为每10秒一次。可能出现轻微扭曲,但不会发生突然变化,因此无需更频繁地查询时间戳。提高查询频率,会增加CPU和电池使用量,可能对电池供电设备不利。
- 若时间戳返回正常,应用应信任这些值,无需采用额外的硬编码偏移量。不鼓励添加实验值。它们并非独立于平台,管道可能随时更新(例如,连接蓝牙接收器时),导致此前准确的数值不再准确。
- AudioTrack.getTimestamp() API返回0,在音频管道的初始预热期间,可能无法连续更新时间戳。此短暂时间段可能持续几秒钟,因此,为了避免在播放开始时出现任何音频视频同步问题,您需要回退到下一节中介绍的AudioTrack.getPlaybackHeadPosition() API。
有关包括可用参数和返回值在内的详细信息,请参阅Android文档中的getTimestamp()
方法。
2.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的一部分),因此需要进行反射才能访问它:
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类中可以看到示例实现。
3.使用标准Android媒体播放器
Fire OS支持用于处理音频和视频播放的标准Android MediaPlayer
类。这些媒体类可以根据AV同步要求处理基本媒体播放。但是,它们的功能在多个方面受到限制。强烈建议使用Amazon ExoPlayer端口(或付费媒体播放器选项之一)。
MediaPlayer
类时,应用应该依赖于Android媒体播放器和Fire OS返回的函数和值。不鼓励手动调整时间戳、音频延迟、缓冲等,因为当音频管道更新时,任何硬编码值都可能变得不准确。4.使用Android NDK的OpenSL ES框架
当OpenSL ES通过其标准API查询音频延迟时,它只获取Audio Flinger报告的音频硬件延迟,不包括软件引入的任何延迟(主要是音轨缓冲)。为了获得包含了硬件和软件音频延迟这两者的准确音频延迟值,在Fire OS 6和7中,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", ¶mSize, &audioLatency);
if (result == SL_RESULT_SUCCESS) {
// 硬件 + 软件音频延迟填充在SLuint32类型的变量audioLatency中。
} else {
//调用您当前的get_audio_latency API。只能获得硬件音频延迟值。
}
其他资源
有关AV同步的更多信息,参见以下外部资料。
- 音频到视频同步(维基百科)
- 音频和视频同步: 界定问题和实现解决方案(Telos联盟)