多媒体隧道

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

  • 对于 Android 5 或更高版本中的按需视频播放,AudioTrack 时钟与应用 传入的音频呈现时间戳同步

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

背景

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

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

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

comparison of tradition and tunnel modes

图 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"/>

当使用音频会话 ID 配置隧道 MediaCodec 实例时,它会向 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 代码可以将编解码器与相应的音频输出流或调谐器流相关联。

在组件配置期间,OMX 或 Codec2 组件应返回一个边带句柄,该句柄可用于将编解码器与硬件合成器 (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

隧道解码器组件应支持以下功能

  • 设置 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

Codec2

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

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

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

  • 处理附加到 C2WorkC2_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

对于按需视频播放,音频 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 调用来遵守。

实现建议

音频 HAL

对于 Android 11,PCR 或 STC 中的硬件同步 ID 可用于 A/V 同步,因此支持纯视频流。

对于 Android 10 或更低版本,支持隧道视频播放的设备应在其 audio_policy.conf 文件中至少具有一个音频输出流配置文件,其中包含 FLAG_HW_AV_SYNCAUDIO_OUTPUT_FLAG_DIRECT 标志。这些标志用于从音频时钟设置系统时钟。

OMX

设备制造商应为隧道视频播放提供单独的 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 与硬件(或内核或驱动程序)同步器协同工作,以根据音频 (7c) 将视频帧 (7b) 与最新的合成 (7a) 组合,以便在正确的时间显示。

HWC combining video frames based on audio

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