Optymalizowanie skuteczności dźwięku za pomocą dekoderów w trakcie przetwarzania

W Androidzie 17 (API na poziomie 37) i nowszych wersjach aktualizowanych za pomocą aktualizacji systemu Google Play (moduły APEX Mainline) wprowadzono wewnętrzne dekodery audio dla skompresowanych formatów audio, w tym Opus i AAC.

Dzięki uruchamianiu dekoderów audio bezpośrednio w procesie aplikacji, a nie w osobnym procesie systemowym w piaskownicy, aplikacje mogą zmniejszyć opóźnienie dekodowania dźwięku o około 40%, zmniejszyć wykorzystanie procesora i wydłużyć czas pracy na baterii podczas ciągłego odtwarzania multimediów, przesyłania wiadomości głosowych i grania.

Dekodowanie poza procesem a w procesie

W przeszłości Android uruchamiał wszystkie dekodery multimediów w ramach oprogramowania platformy w dedykowanym, odizolowanym procesie systemowym (mediaswcodec), aby chronić aplikacje i system operacyjny przed nieprawidłowymi strumieniami bitów multimediów. Jednak piaskownica poza procesem ma zauważalny wpływ na wydajność:

  • Opóźnienie komunikacji międzyprocesowej (IPC): każda ramka wejściowa audio umieszczona w kolejce dekodera i każdy zdekodowany bufor audio PCM wymaga serializacji IPC Binder i przełączania kontekstu między procesami.
  • Narzucone obciążenie pamięci współdzielonej: przekazywanie buforów między procesami wymaga synchronizacji pamięci współdzielonej (C2Buffer) i zarządzania pamięcią podręczną.
  • Konflikt planowania: przy dużym obciążeniu procesora konflikt planowania wątków między procesem aplikacji a mediaswcodec może powodować brak bufora i zacinanie się dźwięku w potokach audio o niskim opóźnieniu (takich jak cyfrowe stacje robocze, komunikacja VoIP i gry interaktywne).

Uruchamianie dekoderów multimediów w procesie bezpośrednio rozwiązuje te problemy z wydajnością:

  • Eliminuje opóźnienie IPC: pomija transakcje IPC w Binderze i przełączanie kontekstu, co zmniejsza opóźnienie dekodowania end-to-end o około 40%.
  • Minimalizuje narzut bufora: przekazuje bufory audio bezpośrednio w przestrzeni pamięci lokalnej aplikacji bez konieczności mapowania pamięci współdzielonej między procesami.
  • Zapobiega zakłóceniom w harmonogramie: wykonuje dekodowanie na własnych wątkach priorytetowych aplikacji (np. wątkach AAudio lub Oboe w czasie rzeczywistym), zapobiegając przerwom w dźwięku i niedoborom bufora przy dużym obciążeniu systemu.

Bezpieczeństwo pamięci w Rust

W przeszłości Android izolował dekodery oprogramowania w osobnym procesie (mediaswcodec), ponieważ dekodery przetwarzają niezaufane strumienie bitów z zewnętrznych plików multimedialnych, co sprawia, że wykorzystywanie luk w zabezpieczeniach pamięci (takich jak przepełnienie bufora) stanowi poważne zagrożenie dla bezpieczeństwa.

Android może bezpiecznie przenosić dekodery oprogramowania bezpośrednio do procesu aplikacji wywołującej, implementując je w językach bezpiecznych pod względem pamięci, takich jak Rust, lub izolując je za pomocą lekkiej izolacji błędów (LFI).

Aby bezpiecznie wyeliminować narzut związany z piaskownicą poza procesem w przypadku AAC (c2.android.inproc.aac.decoder), Android udostępnia dekoder zaimplementowany w czystym języku Rust lub chroniony przez bezpieczne interfejsy FFI w języku Rust.

Rust jest uważany za bezpieczny w przypadku dekodowania w procesie z tych powodów:

  • Bezpieczeństwo pamięci w czasie kompilacji: ścisłe zasady własności, pożyczania i sprawdzania czasu życia w Rust zapewniają, że nie można uzyskać dostępu do pamięci po jej zwolnieniu ani zmodyfikować jej, gdy jest udostępniana.
  • Eliminacja typowych luk w zabezpieczeniach: Rust całkowicie zapobiega przepełnieniom bufora sterty, przepełnieniom stosu, odczytom i zapisom tablic poza zakresem, błędom użycia po zwolnieniu i lukom w zabezpieczeniach związanym z podwójnym zwolnieniem pamięci w czasie kompilacji.
  • Brak podatku od piaskownicy w czasie działania: ponieważ bezpieczeństwo pamięci jest matematycznie udowodnione w czasie kompilacji i weryfikowane przez sprawdzanie granic, dekodery Rusta działają z natywną szybkością w procesie aplikacji bez konieczności przełączania tabeli stron ani kontekstów piaskownicy sprzętowej.

Bezpieczeństwo pamięci dzięki lekkiej izolacji błędów (LFI)

W przypadku złożonych starszych dekoderów audio C/C++, które nie zostały jeszcze przepisane w języku Rust, a w szczególności dekodera Opus (c2.android.inproc.opus.decoder zasilanego przez libopus), Android zapewnia bezpieczeństwo w procesie za pomocą lekkiej izolacji błędów (LFI).

LFI jest uważany za bezpieczny w przypadku dekoderów C/C++ z tych powodów:

  • Piaskownica pamięci liniowej chroniona sprzętowo: LFI kompiluje kodek C/C++ do postaci liniowego kodu maszynowego pochodzącego z WebAssembly, który jest ograniczony do chronionego sprzętowo 4-gigabajtowego gniazda pamięci liniowej.
  • Klucze ochrony pamięci w systemie Linux (pku): LFI używa kluczy ochrony pamięci w systemie Linux (pku / pkey_mprotect) do dzielenia uprawnień przestrzeni adresowej. Odizolowana biblioteka nie może odczytywać ani zapisywać pamięci poza przypisanym jej 4-gigabajtowym miejscem w piaskownicy.
  • Przechwytywanie wywołań systemowych: w piaskownicy nie są dozwolone żadne wywołania systemowe systemu operacyjnego hosta (takie jak operacje wejścia/wyjścia plików, dostęp do sieci czy kontrola procesów). Wszelkie nieobsługiwane wywołania biblioteki są kierowane do minimalnych atrap (lfi_stubs.c).
  • Bezpieczne odzyskiwanie po błędach: jeśli nieprawidłowy lub złośliwy strumień bitów Opus próbuje odczytać lub zapisać dane poza zakresem w liniowej piaskownicy, środowisko wykonawcze LFI bezpiecznie przechwytuje błąd w przestrzeni użytkownika i zwraca czysty błąd dekodowania C2_CORRUPTED bez powodowania awarii aplikacji hosta.

Obsługiwane architektury i urządzenia w przypadku LFI

LFI nie wymaga niestandardowych instrukcji sprzętowych procesora i jest obsługiwane w przypadku architektur procesora ARM64 (aarch64) i x86_64:

  • Urządzenia ARM64 (aarch64): większość urządzeń mobilnych dla konsumentów, w tym telefony komórkowe (np. urządzenia Pixel z układami SoC Google Tensor, Qualcomm Snapdragon i MediaTek), urządzenia składane, tablety i samochodowe systemy infotainment, działa na procesorach ARM64 i obsługuje dekodery LFI w procesie.
  • Urządzenia x86_64: emulatory Androida działające na stacjach roboczych deweloperów z procesorami Intel lub AMD, a także Chromebooki z ChromeOS, na których działają aplikacje na Androida korzystające ze środowiska wykonawczego Androida dla ChromeOS (ARC), działają na architekturach x86_64 i obsługują piaskownicę LFI.

Dostępne dekodery audio w procesie

Format dźwięku Typ MIME Nazwa komponentu w procesie Implementacja
Opus audio/opus c2.android.inproc.opus.decoder C/C++ libopus z LFI
AAC audio/mp4a-latm c2.android.inproc.aac.decoder Rust z bezpiecznym zarządzaniem pamięcią (C2ApexAacDec)
  • AAC (c2.android.inproc.aac.decoder): obsługuje strumienie ADTS (z dynamiczną analizą nagłówka ADTS) i surowe jednostki dostępu (AU) z dokładnym śledzeniem sygnatury czasowej prezentacji (w przypadku AAC_PACKAGING_RAW przetwarzanie tylko jednostek AU z jedną klatką).
  • Opus (c2.android.inproc.opus.decoder): dekoduje standardowe pakiety kontenera Ogg Opus lub Matroska z wewnętrznym próbkowaniem do 48 kHz PCM.

Włączanie dekoderów w procesie

Obecnie dekodery w trakcie przetwarzania nie są domyślnym systemem podczas dzwonienia na numery MediaCodec.createDecoderByType(). Podczas weryfikacji ekosystemu platforma zachowuje wyższy priorytet wyboru kodeka 2.0 w przypadku starszych dekoderów działających poza procesem.

Aby włączyć dekoder w procesie na potrzeby odtwarzania z niskimi opóźnieniami lub testowania, utwórz instancję kodeka w sposób jawny według nazwy komponentu za pomocą MediaCodec.createByCodecName().

Tworzenie instancji według nazwy komponentu

Poniższy przykład pokazuje, jak jawnie utworzyć instancję dekodera AAC w procesie, a w razie niedostępności na urządzeniu komponentu w procesie użyć domyślnego dekodera:

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

Konfigurowanie Jetpack Media3 lub ExoPlayer

Jeśli Twoja aplikacja korzysta z biblioteki Jetpack Media3 lub ExoPlayer, możesz utworzyć niestandardowy element MediaCodecSelector, aby w przypadku dostępności na urządzeniu priorytetowo traktować dekodery w procesie:

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

Plan działania dotyczący domyślnych dekoderów systemowych

Android aktywnie przygotowuje się do przejścia na te bezpieczne pod względem pamięci dekodery w procesie, aby stały się domyślnymi dekoderami systemowymi dla odpowiednich typów MIME audio w nadchodzących wersjach platformy (począwszy od Opus LFI i AAC Rust w Androidzie 18 lub nadchodzących aktualizacjach modułu Mainline).

Gdy dekoder w procesie stanie się domyślnym komponentem Codec 2.0 dla swojego typu MIME, standardowe wywołania MediaCodec.createDecoderByType(...) będą automatycznie kierowane przez implementację w procesie, co zapewni o około 40% niższe opóźnienie dekodowania bez konieczności wprowadzania zmian w kodzie aplikacji.