多媒体隧道

在多媒体隧道的加持下,经过压缩的视频数据可以通过硬件视频解码器建立隧道,直接发送到显示屏,无需经由应用代码或 Android 框架代码加以处理。Android 堆栈下方的设备专属代码会判断何时将哪些视频帧发送到显示屏,具体方法为将视频帧呈现时间戳与以下某种内置时钟进行比较:

  • 如果是 Android 5 或更高版本中的点播视频播放,则相应内置时钟为同步到由应用传入的音频呈现时间戳的 AudioTrack 时钟

  • 如果是 Android 11 或更高版本中的直播播放,则相应内置时钟为由调谐器驱动的程序参考时钟 (PCR) 或系统时钟 (STC)

背景

Android 上的传统视频播放功能会通知应用经过压缩的视频帧已完成解码。然后,应用会将解码后的视频帧释放到显示屏,并根据与相应音频帧相同的系统时钟,检索历史 AudioTimestamps 实例,计算出正确的时间。

由于隧道式视频播放会绕过应用代码,针对视频执行操作的进程数也有所减少,因此它能根据 OEM 实现情况,更高效地渲染视频。此外,它还可以避免因 Android 请求渲染视频的时间与硬件 Vsync 实际时间之间的潜在偏差而引起的时间问题,从而提供更准确的视频节奏,并精准同步到选定的时钟(PRC、STC 或音频)。不过,隧道技术还可以降低对 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-frame。

    • 如果您希望在音频开始播放之后,再渲染第一个排入队列的视频帧,请将此参数设置为 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 组件中有 1 秒钟的数据),则会导致暂停操作看起来没有响应。

在 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 视频组件所分配的边带句柄。

硬件混合渲染器会将解码的视频帧(来自隧道式 OMX 组件)同步到关联的音轨(包含 audio-hw-sync ID)。当某个新的视频帧成为当前帧时,硬件混合渲染器便会将其与在上一个准备或设置调用过程中接收到的所有层的当前内容进行合成,然后显示所生成的图像。仅在其他层发生变化或边带层的属性(例如位置或大小)变化时,准备或设置调用才会发生。

下图展示了 HWC 如何与硬件(或内核或驱动程序)同步管理程序合作,以便将视频帧 (7b) 与最新的合成内容 (7a) 进行合并,进而根据音频 (7c) 在正确的时间显示合并后的内容。

HWC 根据音频合并视频帧

图 2. HWC 与硬件(或内核/驱动程序)同步管理程序合作