多媒体隧道使压缩的视频数据能够通过硬件视频解码器直接传送到显示器,而无需通过应用程序代码或 Android 框架代码进行处理。 Android 堆栈下方的设备特定代码通过将视频帧呈现时间戳与以下类型的内部时钟之一进行比较来确定将哪些视频帧发送到显示器以及何时发送它们:
对于 Android 5 或更高版本中的点播视频播放, AudioTrack时钟与应用传入的音频演示时间戳同步
对于 Android 11 或更高版本的直播播放,由调谐器驱动的节目参考时钟 (PCR) 或系统时钟 (STC)
Android 上的传统视频播放会在压缩视频帧被解码时通知应用程序。然后,应用程序将解码的视频帧发布到显示器,以与相应的音频帧在相同的系统时钟时间进行渲染,检索历史AudioTimestamps实例以计算正确的时间。
由于隧道式视频播放绕过了应用程序代码并减少了作用于视频的进程数量,因此它可以根据 OEM 实现提供更高效的视频渲染。它还可以通过避免由 Android 请求渲染视频的时间和真正的硬件 vsync 的时间之间的潜在偏差引入的时间问题,为所选时钟(PRC、STC 或音频)提供更准确的视频节奏和同步。但是,隧道也可以减少对 GPU 效果的支持,例如画中画 (PiP) 窗口中的模糊或圆角,因为缓冲区绕过 Android 图形堆栈。
下图显示了隧道如何简化视频播放过程。
图 1.传统和隧道视频播放过程的比较
由于大多数应用程序开发人员都集成了用于播放实现的库,因此在大多数情况下,实施只需要重新配置该库以进行隧道播放。对于隧道视频播放器的低级实现,请使用以下说明。
对于 Android 5 或更高版本中的点播视频播放:
创建一个SurfaceView
实例。
创建一个audioSessionId
实例。
使用步骤 2 中创建的audioSessionId
实例创建AudioTrack
和MediaCodec
实例。
使用音频数据中第一个音频帧的呈现时间戳将音频数据排队到AudioTrack
。
对于 Android 11 或更高版本的直播播放:
创建一个SurfaceView
实例。
从Tuner
获取一个avSyncHwId
实例。
使用在步骤 2 中创建的avSyncHwId
实例创建AudioTrack
和MediaCodec
实例。
API 调用流程显示在以下代码片段中:
aab.setContentType(AudioAttributes.CONTENT_TYPE_MOVIE);
// configure for audio clock sync
aab.setFlag(AudioAttributes.FLAG_HW_AV_SYNC);
// or, for tuner clock sync (Android 11 or higher)
new tunerConfig = TunerConfiguration(0, avSyncId);
aab.setTunerConfiguration(tunerConfig);
if (codecName == null) {
return FAILURE;
}
// configure for audio clock sync
mf.setInteger(MediaFormat.KEY_AUDIO_SESSION_ID, audioSessionId);
// or, for tuner clock sync (Android 11 or higher)
mf.setInteger(MediaFormat.KEY_HARDWARE_AV_SYNC_ID, avSyncId);
因为隧道式点播视频播放隐式绑定到AudioTrack
播放,所以隧道式视频播放的行为可能取决于音频播放的行为。
在大多数设备上,默认情况下,在音频播放开始之前不会渲染视频帧。但是,应用程序可能需要在开始音频播放之前渲染视频帧,例如,在搜索时向用户显示当前视频位置。
要表示第一个排队的视频帧应在解码后立即渲染,请将PARAMETER_KEY_TUNNEL_PEEK参数设置为1
。当压缩视频帧在队列中重新排序时(例如存在B 帧时),这意味着第一个显示的视频帧应该始终是 I 帧。
如果您不希望在音频播放开始之前渲染第一个排队的视频帧,请将此参数设置为0
。
如果未设置此参数,OEM 将确定设备的行为。
当音频数据未提供给AudioTrack
并且缓冲区为空(音频欠载)时,视频播放会停止,直到写入更多音频数据,因为音频时钟不再前进。
在播放期间,应用程序无法纠正的不连续性可能会出现在音频演示时间戳中。发生这种情况时,OEM 会通过停止当前视频帧来纠正负间隙,并通过丢弃视频帧或插入静音音频帧(取决于 OEM 实施)来纠正正间隙。对于插入的无声音频帧, AudioTimestamp
帧位置不会增加。
OEM 应该创建一个单独的视频解码器来支持隧道视频播放。此解码器应在media_codecs.xml
文件中宣传它能够进行隧道播放:
当隧道MediaCodec
实例配置了音频会话 ID 时,它会向AudioFlinger
查询此HW_AV_SYNC
ID:
if (entry.getKey().equals(MediaFormat.KEY_AUDIO_SESSION_ID)) {
int sessionId = 0;
try {
sessionId = (Integer)entry.getValue();
}
catch (Exception e) {
throw new IllegalArgumentException("Wrong Session ID Parameter!");
}
keys[i] = "audio-hw-sync";
values[i] = AudioSystem.getAudioHwSyncForSession(sessionId);
}
在此查询期间, AudioFlinger从主音频设备检索HW_AV_SYNC
ID,并在内部将其与音频会话 ID 关联:
audio_hw_device_t *dev = mPrimaryHardwareDev->hwDevice();
char *reply = dev->get_parameters(dev, AUDIO_PARAMETER_HW_AV_SYNC);
AudioParameter param = AudioParameter(String8(reply));
int hwAVSyncId;
param.getInt(String8(AUDIO_PARAMETER_HW_AV_SYNC), hwAVSyncId);
如果已经创建了AudioTrack
实例,则会将HW_AV_SYNC
ID 传递给具有相同音频会话 ID 的输出流。如果尚未创建,则在AudioTrack
创建期间将HW_AV_SYNC
ID 传递给输出流。这是由播放线程完成的:
mOutput->stream->common.set_parameters(&mOutput->stream->common, AUDIO_PARAMETER_STREAM_HW_AV_SYNC, hwAVSyncId);
HW_AV_SYNC
ID,无论是对应于音频输出流还是Tuner
配置,都被传递到 OMX 或 Codec2 组件中,以便 OEM 代码可以将编解码器与相应的音频输出流或 Tuner 流相关联。
在组件配置期间,OMX 或 Codec2 组件应返回一个边带句柄,该句柄可用于将编解码器与 Hardware Composer (HWC) 层相关联。当应用程序将表面与MediaCodec
相关联时,此边带句柄通过SurfaceFlinger
向下传递给 HWC,它将层配置为边带层。
err = native_window_set_sideband_stream(nativeWindow.get(), sidebandHandle);
if (err != OK) {
ALOGE("native_window_set_sideband_stream(%p) failed! (err %d).", sidebandHandle, err);
return err;
}
HWC 负责在适当的时间从编解码器输出接收新的图像缓冲区,同步到相关的音频输出流或调谐器程序参考时钟,将缓冲区与其他层的当前内容合成,并显示结果图像。这与正常的准备和设置周期无关。只有当其他层发生变化或边带层的属性(例如位置或大小)发生变化时,才会调用准备和设置。
隧道解码器组件应支持以下内容:
设置OMX.google.android.index.configureVideoTunnelMode
扩展参数,该参数使用ConfigureVideoTunnelModeParams
结构传入与音频输出设备关联的HW_AV_SYNC
ID。
配置OMX_IndexConfigAndroidTunnelPeek
参数,该参数告诉编解码器渲染或不渲染第一个解码的视频帧,无论音频播放是否已开始。
当第一个隧道视频帧已解码并准备好呈现时,发送OMX_EventOnFirstTunnelFrameReady
事件。
AOSP 实现通过OMXNodeInstance在ACodec中配置隧道模式,如下代码片段所示:
OMX_INDEXTYPE index;
OMX_STRING name = const_cast(
"OMX.google.android.index.configureVideoTunnelMode");
OMX_ERRORTYPE err = OMX_GetExtensionIndex(mHandle, name, &index);
ConfigureVideoTunnelModeParams tunnelParams;
InitOMXParams(&tunnelParams);
tunnelParams.nPortIndex = portIndex;
tunnelParams.bTunneled = tunneled;
tunnelParams.nAudioHwSync = audioHwSync;
err = OMX_SetParameter(mHandle, index, &tunnelParams);
err = OMX_GetParameter(mHandle, index, &tunnelParams);
sidebandHandle = (native_handle_t*)tunnelParams.pSidebandWindow;
如果组件支持这种配置,它应该为这个编解码器分配一个边带句柄,并通过pSidebandWindow
成员将它传回,以便 HWC 可以识别关联的编解码器。如果组件不支持此配置,则应将bTunneled
设置为OMX_FALSE
。
在 Android 11 或更高版本中, Codec2
支持隧道播放。解码器组件应支持以下内容:
配置C2PortTunneledModeTuning
,它配置隧道模式并传入从音频输出设备或调谐器配置检索到的HW_AV_SYNC
。
查询C2_PARAMKEY_OUTPUT_TUNNEL_HANDLE
,为 HWC 分配和检索边带句柄。
在连接到C2_PARAMKEY_TUNNEL_HOLD_RENDER
时处理C2Work
,它指示编解码器解码并发出工作完成信号,但在 1) 编解码器稍后被指示渲染它或 2) 音频播放开始之前不渲染输出缓冲区。
处理C2_PARAMKEY_TUNNEL_START_RENDER
,它指示编解码器立即渲染标有C2_PARAMKEY_TUNNEL_HOLD_RENDER
的帧,即使音频播放尚未开始。
保留debug.stagefright.ccodec_delayed_params
未配置(推荐)。如果您确实配置它,请设置为false
。
注意:不要将debug.stagefright.ccodec_delayed_params
设置为true
,因为这会导致延迟将C2_PARAMKEY_TUNNEL_START_RENDER
发送到编解码器。
AOSP 实现通过C2PortTunnelModeTuning
在CCodec中配置隧道模式,如下代码片段所示:
if (msg->findInt32("audio-hw-sync", &tunneledPlayback->m.syncId[0])) {
tunneledPlayback->m.syncType =
C2PortTunneledModeTuning::Struct::sync_type_t::AUDIO_HW_SYNC;
} else if (msg->findInt32("hw-av-sync-id", &tunneledPlayback->m.syncId[0])) {
tunneledPlayback->m.syncType =
C2PortTunneledModeTuning::Struct::sync_type_t::HW_AV_SYNC;
} else {
tunneledPlayback->m.syncType =
C2PortTunneledModeTuning::Struct::sync_type_t::REALTIME;
tunneledPlayback->setFlexCount(0);
}
c2_status_t c2err = comp->config({ tunneledPlayback.get() }, C2_MAY_BLOCK,
failures);
std::vector> params;
c2err = comp->query({}, {C2PortTunnelHandleTuning::output::PARAM_TYPE},
C2_DONT_BLOCK, ¶ms);
if (c2err == C2_OK && params.size() == 1u) {
C2PortTunnelHandleTuning::output *videoTunnelSideband =
C2PortTunnelHandleTuning::output::From(params[0].get());
return OK;
}
如果组件支持这种配置,它应该为这个编解码器分配一个边带句柄,并通过C2PortTunnelHandlingTuning
将其传回,以便 HWC 可以识别关联的编解码器。
对于点播视频播放,音频 HAL 接收与大端格式的音频数据内联的音频演示时间戳,该时间戳位于应用程序写入的每个音频数据块开头的标头内:
struct TunnelModeSyncHeader {
// The 32-bit data to identify the sync header (0x55550002)
int32 syncWord;
// The size of the audio data following the sync header before the next sync
// header might be found.
int32 sizeInBytes;
// The presentation timestamp of the first audio sample following the sync
// header.
int64 presentationTimestamp;
// The number of bytes to skip after the beginning of the sync header to find the
// first audio sample (20 bytes for compressed audio, or larger for PCM, aligned
// to the channel count and sample size).
int32 offset;
}
为了让 HWC 与相应的音频帧同步渲染视频帧,音频 HAL 应解析同步标头并使用演示时间戳将播放时钟与音频渲染重新同步。要在播放压缩音频时重新同步,音频 HAL 可能需要解析压缩音频数据中的元数据以确定其播放持续时间。
Android 5 或更低版本不包含暂停支持。您只能通过 A/V 饥饿来暂停隧道播放,但如果视频的内部缓冲区很大(例如,OMX 组件中有一秒钟的数据),它会使暂停看起来没有响应。
在 Android 5.1 或更高版本中, AudioFlinger
支持直接(隧道)音频输出的暂停和恢复。如果 HAL 实现了暂停和恢复,则将跟踪暂停和恢复转发给 HAL。
通过在播放线程中执行 HAL 调用(与卸载相同)来遵守暂停、刷新、恢复调用顺序。
对于 Android 11,来自 PCR 或 STC 的 HW 同步 ID 可用于 A/V 同步,因此支持仅视频流。
对于 Android 10 或更低版本,支持隧道视频播放的设备应在其audio_policy.conf
文件中至少具有一个带有FLAG_HW_AV_SYNC
和AUDIO_OUTPUT_FLAG_DIRECT
标志的音频输出流配置文件。这些标志用于从音频时钟设置系统时钟。
设备制造商应该有一个单独的 OMX 组件用于隧道视频播放(制造商可以有额外的 OMX 组件用于其他类型的音频和视频播放,例如安全播放)。隧道组件应该:
在其输出端口上指定 0 个缓冲区( nBufferCountMin
、 nBufferCountActual
)。
实现OMX.google.android.index.prepareForAdaptivePlayback setParameter
扩展。
在media_codecs.xml
文件中指定其功能并声明隧道播放功能。它还应阐明对帧大小、对齐方式或比特率的任何限制。一个例子如下所示:
...
如果使用相同的 OMX 组件来支持隧道和非隧道解码,它应该将隧道播放功能保留为非必需。隧道和非隧道解码器都具有相同的能力限制。一个例子如下所示:
...
当显示器上有一个隧道层(具有HWC_SIDEBAND
compositionType
的层)时,该层的sidebandStream
是 OMX 视频组件分配的边带句柄。
HWC 将解码的视频帧(来自隧道 OMX 组件)同步到关联的音轨(使用audio-hw-sync
ID)。当一个新的视频帧变为当前时,HWC 将它与在最后一次准备或设置调用期间接收到的所有层的当前内容合成,并显示结果图像。只有当其他层发生变化或边带层的属性(例如位置或大小)发生变化时,才会调用准备或设置。
下图表示 HWC 与硬件(或内核或驱动程序)同步器一起工作,以基于音频 (7c) 将视频帧 (7b) 与最新组合 (7a) 组合以在正确的时间显示。
图 2. HWC 硬件(或内核或驱动程序)同步器