多媒體隧道

多媒體隧道使壓縮的視頻數據能夠通過硬件視頻解碼器直接傳送到顯示器,而無需通過應用程序代碼或 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 幀。

    • 如果您不希望在音頻播放開始之前渲染第一個排隊的視頻幀,請將此參數設置為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 的輸出流。如果尚未創建,則在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

隧道解碼器組件應支持以下內容:

  • 設置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 分配和檢索邊帶句柄。

  • 在連接到C2_PARAMKEY_TUNNEL_HOLD_RENDER時處理C2Work ,它指示編解碼器解碼並發出工作完成信號,但在 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 的 HW 同步 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 基於音頻組合視頻幀

圖 2. HWC 硬件(或內核或驅動程序)同步器