Оптимизация качества звука с помощью встроенных декодеров

Начиная с Android 17 (уровень API 37) и обновляясь через системные обновления Google Play (модули Mainline APEX), Android внедряет встроенные программные декодеры аудио для сжатых аудиоформатов, включая Opus и AAC .

Запуская аудиодекодеры непосредственно в процессе приложения, а не в отдельном изолированном системном процессе, приложения могут сократить задержку декодирования звука примерно на 40% , снизить загрузку процессора и продлить время работы батареи при непрерывном воспроизведении мультимедиа, голосовых сообщениях и играх.

Декодирование вне процесса и внутри процесса

Исторически сложилось так, что Android запускал все программные декодеры мультимедиа внутри выделенного изолированного системного демона ( mediaswcodec ), чтобы защитить приложения и ОС от некорректных потоков медиаданных. Однако использование изолированных процессов вне основного процесса приводит к ощутимым накладным расходам на производительность:

  • Задержка межпроцессного взаимодействия (IPC): каждый входной аудиокадр, поставленный в очередь декодеру, и каждый возвращенный декодированный аудиобуфер PCM требуют сериализации IPC между процессами Binder и переключения контекста.
  • Накладные расходы на общую память: Передача буферов между процессами требует синхронизации общей памяти ( C2Buffer ) и управления кэшем.
  • Конфликты при планировании: При высокой нагрузке на ЦП конфликты при планировании потоков между процессом приложения и mediaswcodec могут вызывать нехватку буфера и заикание звука в аудиосистемах с низкой задержкой (таких как DAW, VoIP-связь и интерактивные игры).

Запуск медиадекодеров в рамках одного процесса напрямую решает эти проблемы с производительностью:

  • Устраняет задержку межпроцессного взаимодействия: обходит транзакции межпроцессного взаимодействия Binder и переключения контекста, снижая задержку сквозного декодирования примерно на 40%.
  • Минимизирует накладные расходы на буферизацию: передает аудиобуферы непосредственно в локальное пространство памяти приложения, не требуя межпроцессного сопоставления общей памяти.
  • Предотвращает сбои в планировании: декодирование выполняется в собственных приоритетных потоках приложения (например, в потоках AAudio или Oboe в реальном времени), что предотвращает прерывания звука и переполнение буфера при высокой системной нагрузке.

Безопасность памяти в Rust

Исторически сложилось так, что Android изолировал программные декодеры в отдельном процессе ( mediaswcodec ), поскольку декодеры обрабатывают ненадежные битовые потоки из внешних медиафайлов, что делало уязвимости, связанные с повреждением памяти (например, переполнением буфера), серьезной проблемой безопасности.

Android может безопасно перемещать программные декодеры непосредственно в процесс вызывающего приложения, реализуя их на языках с безопасной памятью, таких как Rust, или изолируя их с помощью легковесной изоляции ошибок (LFI).

Для безопасного устранения накладных расходов на песочницу вне процесса для AAC ( c2.android.inproc.aac.decoder ) Android предоставляет декодер, реализованный на чистом Rust или защищенный безопасными интерфейсами внешних функций Rust (FFI).

Язык Rust считается безопасным для внутрипроцессного декодирования по следующим причинам:

  • Безопасность памяти на этапе компиляции: строгие правила владения, заимствования и проверки времени жизни памяти в Rust гарантируют, что доступ к памяти после ее освобождения или изменения в процессе совместного использования будет невозможен.
  • Устранение распространенных уязвимостей: Rust полностью предотвращает переполнение буфера в куче, сбой стека, чтение и запись массивов за пределы допустимого диапазона, ошибки использования памяти после освобождения и уязвимости двойного освобождения памяти на этапе компиляции.
  • Отсутствие затрат на изоляцию во время выполнения: поскольку безопасность памяти математически доказана во время компиляции и проверена с помощью проверки границ, декодеры Rust выполняются со скоростью нативного кода внутри процесса приложения, не требуя переходов между таблицами страниц или контекстов аппаратной песочницы.

Безопасность памяти с помощью облегченной изоляции ошибок (LFI)

Для сложных устаревших аудиодекодеров на C/C++, которые еще не были переписаны на Rust — в частности, для декодера Opus ( c2.android.inproc.opus.decoder на базе libopus ) — Android обеспечивает внутрипроцессную безопасность с помощью облегченной изоляции ошибок (LFI) .

Протокол LFI считается безопасным для декодеров на C/C++ по следующим причинам:

  • Аппаратная защита линейной памяти: LFI компилирует библиотеку кодеков C/C++ в линейный машинный код, производный от WebAssembly, ограниченный аппаратно защищенным слотом линейной памяти объемом 4 ГБ.
  • Ключи защиты памяти Linux ( pku ): LFI использует ключи защиты памяти Linux ( pku / pkey_mprotect ) для разделения разрешений адресного пространства. Изолированная библиотека не может читать или записывать данные в память за пределами выделенного ей 4 ГБ изолированного пространства.
  • Перехват системных вызовов: Внутри песочницы запрещены любые системные вызовы хост-системы (такие как ввод-вывод файлов, доступ к сети или управление процессами). Любые неподдерживаемые вызовы библиотек перенаправляются на минимальные заглушки ( lfi_stubs.c ).
  • Безопасное восстановление после сбоя: если некорректный или вредоносный битовый поток Opus пытается выполнить чтение или запись за пределами допустимого диапазона внутри линейной песочницы, среда выполнения LFI безопасно перехватывает ошибку в пользовательском пространстве и возвращает корректную ошибку декодирования C2_CORRUPTED без сбоя хост-приложения.

Поддерживаемые архитектуры и устройства для LFI

Для работы LFI не требуются специальные аппаратные инструкции ЦП, и эта функция поддерживается на процессорах архитектур ARM64 ( aarch64 ) и x86_64 :

  • Устройства на базе ARM64 ( aarch64 ): Большинство потребительских мобильных устройств, включая мобильные телефоны (такие как устройства Pixel с процессорами Google Tensor, Qualcomm Snapdragon и MediaTek), складные устройства, планшеты и автомобильные информационно-развлекательные системы, работают на процессорах ARM64 и поддерживают внутрипроцессные декодеры LFI.
  • Устройства x86_64: эмуляторы Android, работающие на рабочих станциях разработчиков с процессорами Intel или AMD, а также Chromebook на ChromeOS, запускающие приложения Android с использованием ARC (Android Runtime for ChromeOS), выполняющиеся на архитектурах x86_64 и поддерживающие песочницу LFI.

Доступные встроенные аудиодекодеры

Аудиоформат MIME-тип Название обрабатываемого компонента Выполнение
Опус audio/opus c2.android.inproc.opus.decoder libopus на C/C++ с LFI
ААК audio/mp4a-latm c2.android.inproc.aac.decoder Безопасный для памяти Rust ( C2ApexAacDec )
  • AAC ( c2.android.inproc.aac.decoder ): Обрабатывает потоки ADTS (с динамическим анализом заголовка ADTS) и необработанные блоки доступа (AU) с точным отслеживанием временной метки представления (обработка AU только для одного кадра в случае AAC_PACKAGING_RAW ).
  • Opus ( c2.android.inproc.opus.decoder ): Декодирует стандартные пакеты Ogg Opus или Matroska с внутренней передискретизацией в PCM 48 кГц.

Включите декодеры, работающие в процессе обработки.

В настоящее время декодеры, созданные непосредственно в процессе обработки, не являются системными декодерами по умолчанию при вызове MediaCodec.createDecoderByType() . Платформа сохраняет более высокий приоритет выбора кодека 2.0 для устаревших декодеров, созданных вне процесса обработки, во время проверки экосистемы.

Чтобы сегодня включить встроенный декодер для воспроизведения с низкой задержкой или тестирования, явно создайте экземпляр кодека по имени компонента, используя MediaCodec.createByCodecName() .

Создать экземпляр по имени компонента.

В следующем примере показано, как явно создать экземпляр декодера AAC в процессе выполнения, используя в качестве резервного варианта декодер по умолчанию, если компонент, созданный в процессе выполнения, недоступен на устройстве:

Котлин

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 , чтобы отдавать приоритет декодерам, находящимся в процессе обработки, когда они доступны на устройстве:

Котлин

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-типов (начиная с Opus LFI и AAC Rust в Android 18 или в будущих обновлениях модулей Mainline).

Как только встроенный декодер становится компонентом Codec 2.0 по умолчанию для своего MIME-типа, стандартные вызовы MediaCodec.createDecoderByType(...) будут автоматически перенаправляться через встроенную реализацию, обеспечивая примерно на 40% меньшую задержку декодирования без необходимости внесения каких-либо изменений в код приложения.