使用程序內解碼器盡可能提升音訊效能

Android 17 (API 級別 37) 以上版本會透過 Google Play 系統更新 (Mainline APEX 模組) 推出程序內軟體音訊解碼器,支援 Opus 和 AAC 等壓縮音訊格式。

應用程式直接在自己的程序中執行音訊解碼器,而非在獨立的沙箱系統程序中執行,因此可將音訊解碼延遲時間縮短約 40%,並降低 CPU 使用率,延長連續播放媒體、語音訊息和遊戲時的電池續航力。

程序外解碼與程序內解碼

在過去,Android 會在專屬的沙箱系統 Daemon (mediaswcodec) 中執行所有平台軟體媒體解碼器,以保護應用程式和 OS 免於媒體位元串流格式錯誤的影響。不過,程序外沙箱會造成可測量的效能負擔:

  • 處理序間通訊 (IPC) 延遲:每個排入解碼器佇列的音訊輸入影格,以及每個傳回的解碼 PCM 音訊緩衝區,都需要跨處理序的繫結器 IPC 序列化和內容切換。
  • 共用記憶體負擔:程序間緩衝區交接需要共用記憶體 (C2Buffer) 同步處理和快取管理。
  • 排程爭用:在 CPU 負載過重的情況下,應用程式程序與 mediaswcodec 之間的執行緒排程爭用可能會導致緩衝區資源不足,並在低延遲音訊管道 (例如 DAW、VoIP 通訊和互動式遊戲) 中造成音訊斷續。

在程序內執行媒體解碼器可直接解決這些效能瓶頸:

  • 消除 IPC 延遲:略過 Binder IPC 交易和內容切換,將端對端解碼延遲時間縮短約 40%。
  • 減少緩衝區負荷:直接在本機應用程式記憶體空間中傳遞音訊緩衝區,不必進行跨程序共用記憶體對應。
  • 避免排程時基誤差:在應用程式本身的優先順序執行緒 (例如即時 AAudio 或 Oboe 執行緒) 中執行解碼,避免音訊中斷,以及在系統負載過重時發生緩衝區溢位。

Rust 的記憶體安全

過去,Android 會在獨立程序 (mediaswcodec) 中隔離軟體解碼器,因為解碼器會處理來自外部媒體檔案的不受信任位元串流,因此記憶體損毀漏洞 (例如緩衝區溢位) 是主要的安全性問題。

Android 可以使用 Rust 等可保護記憶體的語言實作軟體解碼器,或使用輕量型錯誤隔離 (LFI) 技術隔離解碼器,安全地將軟體解碼器直接移至呼叫應用程式程序。

為安全地消除 AAC (c2.android.inproc.aac.decoder) 的程序外沙箱管理負擔,Android 提供以純 Rust 實作的解碼器,或受安全 Rust 外部函式介面 (FFI) 輔助程式保護的解碼器。

Rust 適用於程序內解碼,原因如下:

  • 編譯階段的記憶體安全:Rust 嚴格的擁有權、借用和生命週期檢查機制,可確保記憶體在釋放後或共用時發生變異後,不會遭到存取。
  • 消除常見安全漏洞:Rust 會在編譯時完全防止堆積緩衝區溢位、堆疊損毀、陣列讀取和寫入越界、釋放後使用錯誤,以及重複釋放安全漏洞。
  • 零執行階段沙箱稅:由於記憶體安全是在編譯時間經過數學驗證,並透過界限檢查進行驗證,因此 Rust 解碼器會在應用程式程序中以原生速度執行,不需要頁面表格轉換或硬體沙箱環境。

透過輕量型錯誤隔離 (LFI) 確保記憶體安全

對於尚未以 Rust 重寫的複雜舊版 C/C++ 音訊解碼器 (特別是 Opus 解碼器,由 c2.android.inproc.opus.decoder 支援,並由 libopus 提供技術),Android 會使用輕量型錯誤隔離 (LFI) 提供程序內安全防護。

基於下列原因,LFI 對於 C/C++ 解碼器來說是安全的:

  • 受硬體保護的線性記憶體沙箱:LFI 會將 C/C++ 編碼器/解碼器程式庫編譯成 WebAssembly 衍生線性機器碼,並限制在受硬體保護的 4 GB 線性記憶體插槽中。
  • Linux 記憶體保護金鑰 (pku):LFI 會使用 Linux 記憶體保護金鑰 (pku / pkey_mprotect) 分割位址空間權限。獨立程式庫無法讀取或寫入指派的 4 GB 沙箱插槽以外的記憶體。
  • 系統呼叫攔截:沙箱內不得有任何主機 OS 系統呼叫 (例如檔案 I/O、網路存取或程序控制)。任何不受支援的程式庫呼叫都會轉送至最少的虛擬存根 (lfi_stubs.c)。
  • 安全錯誤復原:如果格式錯誤或惡意的 Opus 位元串嘗試在線性沙箱內進行超出範圍的讀取或寫入作業,LFI 執行階段會在使用者空間中安全地捕捉錯誤,並傳回乾淨的 C2_CORRUPTED 解碼錯誤,不會導致主機應用程式當機。

LFI 支援的架構和裝置

LFI 不需要自訂 CPU 硬體指令,並支援 ARM64 (aarch64) 和 x86_64 處理器架構:

  • ARM64 (aarch64) 裝置:大多數消費者行動裝置 (包括手機,例如搭載 Google Tensor SoC、Qualcomm Snapdragon 和 MediaTek SoC 的 Pixel 裝置)、摺疊式裝置、平板電腦和車輛資訊娛樂單元,都採用 ARM64 處理器,並支援 LFI 處理程序內解碼器。
  • x86_64 裝置:在搭載 Intel 或 AMD CPU 的開發人員工作站上執行的 Android 模擬器,以及使用 ARC (Android Runtime for ChromeOS) 執行 Android 應用程式的 ChromeOS Chromebook,都會在 x86_64 架構上執行,並支援 LFI 沙箱。

可用的處理中音訊解碼器

音訊格式 MIME 類型 程序內元件名稱 導入作業
Opus audio/opus c2.android.inproc.opus.decoder C/C++ libopus 與 LFI
AAC audio/mp4a-latm c2.android.inproc.aac.decoder 記憶體安全 Rust (C2ApexAacDec)
  • AAC (c2.android.inproc.aac.decoder):處理 ADTS 串流 (含動態 ADTS 標頭剖析) 和原始存取單元 (AU),並準確追蹤呈現時間戳記 (如果是 AAC_PACKAGING_RAW,則只處理單一影格 AU)。
  • Opus (c2.android.inproc.opus.decoder):解碼標準 Ogg Opus 或 Matroska 容器封包,並將內部重新取樣至 48 kHz PCM。

選擇啟用程序內解碼器

目前呼叫 MediaCodec.createDecoderByType() 時,系統預設不會使用處理中解碼器。在生態系統驗證期間,平台會保留較高的 Codec 2.0 選取優先順序,以供舊版程序外解碼器使用。

如要立即選擇使用程序內解碼器,以進行低延遲播放或測試,請使用 MediaCodec.createByCodecName(),依元件名稱明確例項化轉碼器。

依元件名稱例項化

以下範例示範如何明確例項化程序內 AAC 解碼器,並在裝置上無法使用程序內元件時,回復為預設解碼器:

Kotlin

val INPROC_AAC_CODEC = "c2.android.inproc.aac.decoder"

fun createAudioDecoder(): MediaCodec {
    return try {
        // Attempt to opt in to the low-latency in-process AAC decoder
        MediaCodec.createByCodecName(INPROC_AAC_CODEC)
    } catch (e: IllegalArgumentException) {
        // Fall back to the default platform AAC decoder
        MediaCodec.createDecoderByType(MediaFormat.MIMETYPE_AUDIO_AAC)
    }
}

Java

private static final String INPROC_AAC_CODEC = "c2.android.inproc.aac.decoder";

public MediaCodec createAudioDecoder() throws IOException {
    try {
        // Attempt to opt in to the low-latency in-process AAC decoder
        return MediaCodec.createByCodecName(INPROC_AAC_CODEC);
    } catch (IllegalArgumentException e) {
        // Fall back to the default platform AAC decoder
        return MediaCodec.createDecoderByType(MediaFormat.MIMETYPE_AUDIO_AAC);
    }
}

設定 Jetpack Media3 或 ExoPlayer

如果應用程式使用 Jetpack Media3 或 ExoPlayer,您可以建立自訂 MediaCodecSelector,在裝置上提供程序內解碼器時優先使用:

Kotlin

class InprocPreferredMediaCodecSelector : MediaCodecSelector {
    override fun getDecoderInfos(
        mimeType: String,
        requiresSecureDecoder: Boolean,
        requiresTunnelingDecoder: Boolean
    ): List<MediaCodecInfo> {
        val defaultInfos = MediaCodecUtil.getDecoderInfos(
            mimeType,
            requiresSecureDecoder,
            requiresTunnelingDecoder
        )
        val inprocNames = setOf(
            "c2.android.inproc.opus.decoder",
            "c2.android.inproc.aac.decoder"
        )
        // Sort in-process decoders to the top of the selection list
        return defaultInfos.sortedByDescending { it.name in inprocNames }
    }
}

Java

public class InprocPreferredMediaCodecSelector implements MediaCodecSelector {
    private static final Set<String> INPROC_NAMES = new HashSet<>(Arrays.asList(
        "c2.android.inproc.opus.decoder",
        "c2.android.inproc.aac.decoder"
    ));

    @Override
    public List<MediaCodecInfo> getDecoderInfos(
            String mimeType,
            boolean requiresSecureDecoder,
            boolean requiresTunnelingDecoder) throws MediaCodecUtil.DecoderQueryException {
        List<MediaCodecInfo> defaultInfos = new ArrayList<>(
            MediaCodecUtil.getDecoderInfos(mimeType, requiresSecureDecoder, requiresTunnelingDecoder)
        );
        // Sort in-process decoders to the top of the selection list
        defaultInfos.sort((a, b) -> {
            boolean aInproc = INPROC_NAMES.contains(a.name);
            boolean bInproc = INPROC_NAMES.contains(b.name);
            return Boolean.compare(bInproc, aInproc);
        });
        return defaultInfos;
    }
}

預設系統解碼器的路線圖

Android 正在積極準備,將這些記憶體安全程序內解碼器,轉換為即將發布的平台版本中,各自音訊 MIME 類型的系統預設解碼器 (從 Android 18 或即將推出的 Mainline 模組更新開始,將 Opus LFI 和 AAC Rust 設為預設解碼器)。

當程序內解碼器成為 MIME 類型的預設 Codec 2.0 元件後,對 MediaCodec.createDecoderByType(...) 的標準呼叫會自動透過程序內實作項目傳送,解碼延遲時間可減少約 40%,且無須變更任何應用程式程式碼。