多媒体隧道

多媒体隧道使压缩视频数据能够通过硬件视频解码器直接传输到显示器,而无需经过应用代码或 Android 框架代码处理。 Android 堆栈下方的设备特定代码通过将视频帧呈现时间戳与以下类型的内部时钟之一进行比较来确定将哪些视频帧发送到显示器以及何时发送它们:

  • 对于 Android 5 或更高版本中的点播视频播放, AudioTrack时钟与应用程序传入的音频演示时间戳同步

  • 对于 Android 11 或更高版本中的直播播放,由调谐器驱动的节目参考时钟 (PCR) 或系统时间时钟 (STC)

背景

Android 上的传统视频播放会在压缩视频帧解码后通知应用程序。然后,应用程序将解码后的视频帧释放到显示器,以便在与相应音频帧相同的系统时钟时间进行渲染,并检索历史AudioTimestamps实例以计算正确的计时。

由于隧道视频播放绕过应用程序代码并减少了作用于视频的进程数量,因此它可以根据 OEM 实现提供更高效的视频渲染。它还可以提供更准确的视频节奏和与所选时钟(PRC、STC 或音频)的同步,避免因 Android 渲染视频请求的时序与真实硬件垂直同步的时序之间潜在偏差而引入的时序问题。但是,隧道也会减少对 GPU 效果的支持,例如画中画 (PiP) 窗口中的模糊或圆角,因为缓冲区会绕过 Android 图形堆栈。

下图显示了隧道如何简化视频播放过程。

传统模式与隧道模式对比

图 1.传统视频播放流程与隧道视频播放流程的比较

对于应用程序开发人员

由于大多数应用程序开发人员都集成了用于播放实现的库,因此在大多数情况下,实现只需重新配置该库即可进行隧道播放。对于隧道视频播放器的低级实现,请使用以下说明。

对于 Android 5 或更高版本中的点播视频播放:

  1. 创建一个SurfaceView实例。

  2. 创建一个audioSessionId实例。

  3. 使用步骤 2 中创建的audioSessionId实例创建AudioTrackMediaCodec实例。

  4. 使用音频数据中第一个音频帧的呈现时间戳将音频数据排队到AudioTrack

对于Android 11或更高版本的直播播放:

  1. 创建一个SurfaceView实例。

  2. Tuner获取avSyncHwId实例。

  3. 使用步骤 2 中创建的avSyncHwId实例创建AudioTrackMediaCodec实例。

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文件中声明它能够进行隧道播放:

<Feature name="tunneled-playback" required="true"/>

当隧道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 的输出流。如果尚未创建,则HW_AV_SYNC ID 将在AudioTrack创建期间传递到输出流。这是由播放线程完成的:

mOutput->stream->common.set_parameters(&mOutput->stream->common, AUDIO_PARAMETER_STREAM_HW_AV_SYNC, hwAVSyncId);

HW_AV_SYNC ID,无论它对应于音频输出流还是Tuner配置,都会被传递到 OMX 或 Codec2 组件中,以便 OEM 代码可以将编解码器与相应的音频输出流或调谐器流相关联。

在组件配置期间,OMX 或 Codec2 组件应返回一个边带句柄,可用于将编解码器与 Hardware Composer (HWC) 层关联起来。当应用程序将 Surface 与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实现通过OMXNodeInstanceACodec中配置隧道模式,如以下代码片段所示:

OMX_INDEXTYPE index;
OMX_STRING name = const_cast<OMX_STRING>(
        "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

编解码器2

在 Android 11 或更高版本中, Codec2支持隧道播放。解码器组件应支持以下内容:

  • 配置C2PortTunneledModeTuning ,它配置隧道模式并传入从音频输出设备或调谐器配置检索的HW_AV_SYNC

  • 查询C2_PARAMKEY_OUTPUT_TUNNEL_HANDLE ,以分配和检索 HWC 的边带句柄。

  • 连接到C2Work时处理C2_PARAMKEY_TUNNEL_HOLD_RENDER ,指示编解码器解码并发出工作完成信号,但不渲染输出缓冲区,直到 1) 稍后指示编解码器渲染它或 2) 音频播放开始。

  • 处理C2_PARAMKEY_TUNNEL_START_RENDER ,指示编解码器立即渲染标有C2_PARAMKEY_TUNNEL_HOLD_RENDER的帧,即使音频播放尚未开始也是如此。

  • 保留debug.stagefright.ccodec_delayed_params未配置(推荐)。如果您配置了它,请设置为false

AOSP 实现通过C2PortTunnelModeTuningCCodec中配置隧道模式,如以下代码片段所示:

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<std::unique_ptr<C2Param>> params;
c2err = comp->query({}, {C2PortTunnelHandleTuning::output::PARAM_TYPE},
        C2_DONT_BLOCK, &params);
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_SYNCAUDIO_OUTPUT_FLAG_DIRECT标志。这些标志用于根据音频时钟设置系统时钟。

奥米克斯

设备制造商应该有一个单独的 OMX 组件用于隧道视频播放(制造商可以有额外的 OMX 组件用于其他类型的音频和视频播放,例如安全播放)。隧道组件应该:

  • 在其输出端口上指定 0 个缓冲区( nBufferCountMinnBufferCountActual )。

  • 实现OMX.google.android.index.prepareForAdaptivePlayback setParameter扩展。

  • media_codecs.xml文件中指定其功能并声明隧道播放功能。它还应澄清对帧大小、对齐或比特率的任何限制。一个例子如下所示:

    <MediaCodec name="OMX.OEM_NAME.VIDEO.DECODER.AVC.tunneled"
    type="video/avc" >
        <Feature name="adaptive-playback" />
        <Feature name="tunneled-playback" required=”true” />
        <Limit name="size" min="32x32" max="3840x2160" />
        <Limit name="alignment" value="2x2" />
        <Limit name="bitrate" range="1-20000000" />
            ...
    </MediaCodec>
    

如果使用相同的 OMX 组件来支持隧道和非隧道解码,则应该不需要隧道播放功能。隧道解码器和非隧道解码器都具有相同的能力限制。一个例子如下所示:

<MediaCodec name="OMX._OEM\_NAME_.VIDEO.DECODER.AVC" type="video/avc" >
    <Feature name="adaptive-playback" />
    <Feature name="tunneled-playback" />
    <Limit name="size" min="32x32" max="3840x2160" />
    <Limit name="alignment" value="2x2" />
    <Limit name="bitrate" range="1-20000000" />
        ...
</MediaCodec>

硬件作曲家 (HWC)

当显示器上有隧道层(具有HWC_SIDEBAND compositionType层)时,该层的sidebandStream是 OMX 视频组件分配的边带句柄。

HWC 将解码的视频帧(来自隧道 OMX 组件)同步到关联的音轨(带有audio-hw-sync ID)。当新的视频帧成为当前视频帧时,HWC 将其与上次准备或设置调用期间接收到的所有层的当前内容进行合成,并显示结果图像。仅当其他层更改或边带层的属性(例如位置或大小)更改时,才会发生准备或设置调用。

下图表示 HWC 与硬件(或内核或驱动程序)同步器一起工作,将视频帧 (7b) 与最新的合成 (7a) 结合起来,以便基于音频 (7c) 在正确的时间显示。

HWC基于音频组合视频帧

图 2. HWC 硬件(或内核或驱动程序)同步器