Google 致力于为黑人社区推动种族平等。查看具体举措

多媒体隧道

您可以在 Android 框架 5.0 及更高版本中实现多媒体隧道。虽然对于 Android TV 来说,该隧道并不是必需的,但在播放超高清 (4K) 内容时,它可以给用户带来最佳体验。

对于 Android 11 或更高版本,您可以实现音频和视频内容直接来自调谐器的多媒体隧道。Codec2AudioTrack 可以使用调谐器中的硬件同步 ID,该 ID 可能对应程序时钟参考 (PCR) 或系统时钟 (STC) 通道。

背景

Android 媒体框架能用以下几种方式处理音频/视频内容:

  • 纯软件(本地解码):应用处理器 (AP) 会在本地解码音频以进行脉冲编码调制 (PCM),而不进行特殊加速。对于 Ogg Vorbis,会始终使用此方式;对于 MP3 和 AAC,在系统不支持压缩分流时,会使用此方式。
  • 压缩音频分流:将经过压缩的音频数据直接发送到数字信号处理器 (DSP),并尽可能使 AP 处于关闭状态。此方式用于在屏幕处于关闭状态时播放音乐文件。
  • 压缩音频直通:通过 HDMI 将经过压缩的音频(具体来说就是 AC3 和 E-AC3)直接发送到外部电视或音频接收器,而不在 Android TV 设备上对其进行解码。系统会单独处理视频部分。
  • 多媒体隧道:同时发送经过压缩的音频和视频数据。视频和音频解码器接收编码流后,编码流不会返回到框架。理想情况下,编码流不会干扰 AP。
  • 多媒体直通:将经过压缩的音频和视频数据同时从调谐器发送到视频和音频解码器,过程中不涉及框架。
多媒体隧道流程图
图 1. 多媒体隧道流程

方式比较

  纯软件 压缩音频分流 压缩音频直通 多媒体隧道 多媒体直通
解码位置 AP DSP 电视或音频/视频接收器 (AVR) 电视或 AVR 电视或 AVR
是否处理音频
是否处理视频

针对应用开发者的说明

需要创建 SurfaceView 实例,获取音频会话 ID,然后创建 AudioTrackMediaCodec 实例,以便为播放和视频帧解码提供必要的时间信息和配置。

对于 Android 11 或更高版本,作为音频会话 ID 的替代项,应用可以从调谐器获取硬件同步 ID,并将其提供给 AudioTrackMediaCodec 实例,以进行 A/V 同步。

A/V 同步

在多媒体隧道模式下,音频和视频会按照主时钟同步。

  • 对于 Android 11 或更高版本,调谐器中的 PCR 或 STC 可能是 A/V 同步的主时钟。
  • 对于 Android 10 或更低版本,音频时钟是进行 A/V 播放时使用的主时钟。

如果隧道式视频 MediaCodec 实例和 AudioTrack 实例已关联到 AudioTrack 中的 HW_AV_SYNC 实例,系统便会根据实际音频或视频帧的显示时间戳 (PTS),利用从 HW_AV_SYNC 推断出的隐式时钟来限制每个视频帧和音频样本的呈现时间。

API 调用流程

对于 Android 11 或更高版本,客户端可以使用调谐器中的硬件同步 ID。

  1. 创建 SurfaceView 实例。
    SurfaceView sv = new SurfaceView(mContext);
  2. 获取音频会话 ID。在创建音轨 (AudioTrack) 时,需要用到这个唯一 ID。该 ID 会传递到媒体编解码器 (MediaCodec),并供媒体框架用于关联音频和视频路径。
    AudioManager am = mContext.getSystemService(AUDIO_SERVICE);
    int audioSessionId = am.generateAudioSessionId()
    // or, for Android 11 or higher
    int avSyncId = tuner.getAvSyncHwId();
  3. 创建具有硬件 A/V 同步 AudioAttributesAudioTrack

    音频政策管理器会询问硬件抽象层 (HAL) 哪个设备输出支持 FLAG_HW_AV_SYNC,并创建直接连接到该输出的音轨(不使用中间混音器)。

    AudioAttributes.Builder aab = new AudioAttributes.Builder();
    aab.setUsage(AudioAttributes.USAGE_MEDIA);
    aab.setContentType(AudioAttributes.CONTENT_TYPE_MOVIE);
    aab.setFlag(AudioAttributes.FLAG_HW_AV_SYNC);
    
    // or, for Android 11 or higher
    new tunerConfig = TunerConfiguration(0, avSyncId);
    aab.setTunerConfiguration(tunerConfig);
    
    AudioAttributes aa = aab.build();
    AudioTrack at = new AudioTrack(aa);
    
  4. 创建视频 MediaCodec 实例,并对其进行配置,以实现隧道式视频播放。
    // retrieve codec with tunneled video playback feature
    MediaFormat mf = MediaFormat.createVideoFormat(“video/hevc”, 3840, 2160);
    mf.setFeatureEnabled(CodecCapabilities.FEATURE_TunneledPlayback, true);
    MediaCodecList mcl = new MediaCodecList(MediaCodecList.ALL_CODECS);
    String codecName = mcl.findDecoderForFormat(mf);
    if (codecName == null) {
      return FAILURE;
    }
    // create codec and configure it
    mf.setInteger(MediaFormat.KEY_AUDIO_SESSION_ID, audioSessionId);
    
    // or, for Android 11 or higher
    mf.setInteger(MediaFormat.KEY_HARDWARE_AV_SYNC_ID, avSyncId);
    
    MediaCodec mc = MediaCodec.createCodecByName(codecName);
    mc.configure(mf, sv.getSurfaceHolder().getSurface(), null, 0);
    
  5. 对视频帧进行解码。
    mc.start();
     for (;;) {
       int ibi = mc.dequeueInputBuffer(timeoutUs);
       if (ibi >= 0) {
         ByteBuffer ib = mc.getInputBuffer(ibi);
         // fill input buffer (ib) with valid data
         ...
         mc.queueInputBuffer(ibi, ...);
       }
       // no need to dequeue explicitly output buffers. The codec
       // does this directly to the sideband layer.
     }
     mc.stop();
     mc.release();
     mc = null;

注意:您可以在此过程中颠倒第 3 步和第 4 步的顺序,如以下两个图所示。

在配置编解码器之前创建的音轨的示意图
图 2. 在配置编解码器之前创建的音轨
在配置编解码器之后创建的音轨的示意图
图 3. 在配置编解码器之后创建的音轨

针对设备制造商的说明

原始设备制造商 (OEM) 应创建单独的视频解码器 OpenMAX IL (OMX) 组件,以支持隧道式视频播放。该 OMX 组件必须公告其能够进行隧道式播放(在 media_codecs.xml 中)。

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

该组件还必须支持采用 ConfigureVideoTunnelModeParams 结构的 OMX 扩展参数 OMX.google.android.index.configureVideoTunnelMode

struct ConfigureVideoTunnelModeParams {
    OMX_U32 nSize;              // IN
    OMX_VERSIONTYPE nVersion;   // IN
    OMX_U32 nPortIndex;         // IN
    OMX_BOOL bTunneled;         // IN/OUT
    OMX_U32 nAudioHwSync;       // IN
    OMX_PTR pSidebandWindow;    // OUT
};

创建隧道式 MediaCodec 的请求一经提出,框架便会将该 OMX 组件配置为隧道模式(通过将 bTunneled 设置为 OMX_TRUE),并会将关联的音频输出设备(使用 AUDIO_HW_AV_SYNC 标志创建)传递到该 OMX 组件(在 nAudioHwSync 中)。

如果该组件支持此配置,则应该为这个编解码器分配一个边带句柄,并通过 pSidebandWindow 成员将其传回。边带句柄是隧道式层的 ID 标记,硬件混合渲染器可通过该标记识别隧道式层。如果该组件不支持此配置,应将 bTunneled 设置为 OMX_FALSE

框架会检索该 OMX 组件分配的隧道式层(边带句柄),并将其传递给硬件混合渲染器。该层的 compositionType 会设置为 HWC_SIDEBAND。(请参阅 hardware/libhardware/include/hardware/hwcomposer.h。)

硬件混合渲染器负责在适当的时间(例如,同步到关联的音频输出设备时)从数据流接收新的图像缓冲区、将它们与其他层的当前内容进行合成,并展示所生成的图像。这是独立于正常的准备/设置周期而发生的。仅在其他层发生变化或边带层的属性(例如位置或大小)变化时,准备/设置调用才会发生。

配置

frameworks/av/services/audioflinger/AudioFlinger.cpp

HAL 返回的 HW_AV_SYNC ID 为某个 64 位整数的字符串十进制表示。(请参阅 frameworks/av/services/audioflinger/AudioFlinger.cpp。)

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);

frameworks/av/services/audioflinger/Threads.cpp

音频框架必须查找与此会话 ID 相对应的 HAL 输出流,并通过 set_parameters 向 HAL 查询 hwAVSyncId

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

OMX 解码器配置

MediaCodec.java

音频框架会查找与此会话 ID 相对应的 HAL 输出流,并会通过 get_parameters 向 HAL 查询 AUDIO_PARAMETER_STREAM_HW_AV_SYNC 标志,以便获得 audio-hw-sync ID。

// Retrieve HW AV sync audio output device from Audio Service
// in MediaCodec.configure()
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);
}

// ...

系统会使用自定义参数 OMX.google.android.index.configureVideoTunnelMode 将该硬件同步 ID 传递到 OMX 隧道式视频解码器。

ACodec.cpp

在您获得音频硬件同步 ID 后,ACodec 会使用该 ID 来配置隧道式视频解码器,以便让隧道式视频解码器知道要同步哪个音轨。

// Assume you're going to use tunneled video rendering.
// Configure OMX component in tunneled mode and grab sideband handle (sidebandHandle) from OMX
// component.

native_handle_t* sidebandHandle;

// Configure OMX component in tunneled mode
status_t err = mOMX->configureVideoTunnelMode(mNode, kPortIndexOutput,
        OMX_TRUE, audioHwSync, &sidebandHandle);

OMXNodeInstance.cpp

该 OMX 组件由上述 configureVideoTunnelMode 方法配置。

// paraphrased

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;

ACodec.cpp

在隧道模式下配置该 OMX 组件后,边带句柄会与呈现表面相关联。

  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; }

然后,系统会将最高分辨率提示(如果存在)发送到该组件。

// Configure max adaptive playback resolution - as for any other video decoder
int32_t maxWidth = 0, maxHeight = 0;
if (msg->findInt32("max-width", &maxWidth) &&
    msg->findInt32("max-height", &maxHeight)) {
    err = mOMX->prepareForAdaptivePlayback(
              mNode, kPortIndexOutput, OMX_TRUE, maxWidth, maxHeight);
}

对暂停功能的支持

Android 5.0 及更低版本不支持暂停功能。您只能通过 A/V 进程饥饿来暂停隧道式播放,但如果视频的内部缓冲区很大(例如,OMX 组件中有 1 秒钟的数据),则会导致暂停操作看起来没有响应。

在 Android 5.1 及更高版本中,AudioFlinger 支持暂停和恢复直接(隧道式)音频输出。如果 HAL 实现了暂停/恢复功能,系统会将轨道暂停/恢复请求转发到 HAL。

系统会在播放线程中执行 HAL 调用,以便遵循暂停、刷新、恢复调用序列(与分流相同)。

Codec2 支持

对于 Android 11 或更高版本,Codec2 支持隧道式播放。

CCodec.cpp

为了支持隧道式播放,Codec2 采用与 OMX 类似的工作方式。为了支持调谐器中的硬件同步 ID,Codec2 会从以下来源查找同步 ID。

sp<ANativeWindow> nativeWindow = static_cast<ANativeWindow *>(surface.get());
int32_t audioHwSync = 0;
if (!msg->findInt32("hw-av-sync-id", &audioHwSync)) {
       if (!msg->findInt32("audio-hw-sync", &audioHwSync)) {
       }
}
err = configureTunneledVideoPlayback(comp, audioHwSync, nativeWindow);

实现方面的建议

音频 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_SIDEBAND compositionType 的层)时,该层的 sidebandStream 就是 OMX 视频组件所分配的边带句柄。

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

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

硬件混合渲染器根据音频合并视频帧的示意图
图 4. 硬件混合渲染器与硬件(或内核/驱动程序)同步管理程序合作