WebRTC на Android: как включить аппаратное кодирование на множестве устройств

Для видеозвонков в Badoo мы используем стандарт WebRTC и кодек H.264. Если верить документации, этот кодек должен без проблем работать на любых устройствах Android начиная с Android 5.0. Но на практике всё оказалось не совсем так. В этой статье я расскажу про особенности реализации аппаратного кодирования для кодека H.264 в WebRTC и о том, как заставить его работать на большем количестве устройств.

tultw1tnly1q-hovglpxdkzaax8.jpeg

Почему именно H.264?


При соединении по WebRTC все устройства, участвующие в сеансе, передают различные параметры связи, в том числе видео- и аудиокодеки. Если устройства поддерживают несколько кодеков (например, VP8 и H.264), приоритетные для платформы кодеки указываются первыми. Эти данные используются на этапе согласования в WebRTC, после которого остаются только кодеки, поддерживаемые всеми устройствами. Пример таких данных с расшифровкой можно увидеть в этом документе.

В случае с видеозвонками при отсутствии на одном из устройств поддержки кодека H.264 оба устройства могут перейти, например, на кодек VP8, который не зависит от аппаратной реализации на устройстве. Но наше приложение доступно на самых разных гаджетах, в том числе на смартфонах предыдущих поколений. Поэтому для видеосвязи мы хотели по возможности использовать аппаратное кодирование: оно снижает нагрузку на процессор и не так сильно ест батарею, что критично для устаревших гаджетов. Поддержка аппаратного кодирования H.264 реализована на большом количестве устройств, в отличие от того же VP8.

Поддержка H.264 на Android


Если верить описанию поддержки форматов мультимедиа, декодирование H.264 Baseline Profile должно работать на всех Android-устройствах, а кодирование — начиная с Android 3.0. В Badoo мы поддерживаем устройства начиная с Android 5.0, так что у нас не должно было возникнуть проблем. Но всё оказалось не так просто: даже в гаджетах с пятой версией мы обнаружили большое количество особенностей.

С чем это может быть связано?

Как известно, при разработке нового устройства на Android любому производителю необходимо пройти набор тестов Compatibility Test Suite. Он запускается на подключённом к устройству ПК, а его результаты необходимо отправить в Google для подтверждения того, что устройство соответствует требованиям ОС Android указанной версии. Только после этого гаджет можно выпустить на рынок.

Нас в этом наборе тестов интересуют мультимедиа-тесты, а конкретнее — тесты на кодирование и декодирование видео. Я решил остановиться на тестах EncodeDecodeTest, MediaCodecTest, DecoderTest и EncoderTest, так как они присутствуют на всех версиях Android начиная с 4.3. График количества строк кода в этих тестах выглядит так:

tdg_4prrdcocytay8z7df_igw0o.png

До версии 4.3 большинства из этих тестов просто не существовало, и значительный их прирост пришёлся на версии 5 и 7. Поэтому можно говорить о том, что до версии Android 4.3 Google никак не проверяла соответствие устройств своей спецификации по кодированию и декодированию видео, а в версии 5.0 значительно улучшила эту проверку.

Казалось бы, это указывает на то, что начиная с версии 5.0 с кодированием всё должно быть в порядке. Но, учитывая предыдущий мой опыт работы с декодированием потокового видео на Android, я был уверен, что это не так. Достаточно было посмотреть на количество топиков про кодирование в Google-группе discuss-webrtc.

Искать подводные камни нам помогали исходные файлы WebRTC, которые находятся в свободном доступе. Рассмотрим их подробнее.

Поддержка H.264 в WebRTC


Начнём с HardwareVideoEncoderFactory.

Тут есть метод с говорящим названием isHardwareSupportedInCurrentSdkH264:

private boolean isHardwareSupportedInCurrentSdkH264(MediaCodecInfo info) {
	// First, H264 hardware might perform poorly on this model.
	if (H264_HW_EXCEPTION_MODELS.contains(Build.MODEL)) {
  	return false;
	}
	String name = info.getName();
	// QCOM H264 encoder is supported in KITKAT or later.
	return (name.startsWith(QCOM_PREFIX) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT)
    	// Exynos H264 encoder is supported in LOLLIPOP or later.
    	|| (name.startsWith(EXYNOS_PREFIX)
           	&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP);
 }


Как мы видим, поддержка аппаратного кодирования на Android реализована только для чипсетов Qualcomm и Exynos. Почему же в стандартной реализации WebRTC нет поддержки других чипсетов? Вероятнее всего, это связано с особенностями реализации аппаратных кодеков производителей. И выявить эти особенности часто можно только на продакшене, поскольку найти те или иные устройства не всегда представляется возможным.

Все описания кодеков на устройстве хранятся в файле media_codecs.xml. Вот, например, этот файл для Pixel XL и для HUAWEI P8 lite. При получении списка кодеков с помощью метода getCodecInfos () объекта MediaCodecList этот файл парсится — и возвращаются кодеки, хранящиеся в нём. Эта операция и правильность заполнения этого файла производителем покрываются в CTS тестом MediaCodecListTest, который также увеличился со 160 строк кода в Android 4.3 до 740 строк в Android 10.

В Badoo мы поменяли код метода isHardwareSupportedInCurrentSdkH264, отказавшись от «белого» списка кодеков и заменив его «чёрным» списком префиксов программных кодеков, которые перечислены в WebRTC:

static final String[] SOFTWARE_IMPLEMENTATION_PREFIXES = {"OMX.google.", "OMX.SEC."};


Но нельзя просто так взять и реализовать поддержку всех кодеков, не обращая внимания на особенности производителей. Из названий топиков, посвящённых аппаратному кодированию на Android в группе discuss-webrtc, можно понять, что в этом случае у нас точно возникнут ошибки. В основном они появляются на этапе конфигурации кодека.

Параметры конфигурации кодека


Инициализация кодека для кодирования выглядит так:

MediaCodec mediaCodec = createByCodecName(codecName);
MediaFormat format = MediaFormat.createVideoFormat(mime, width, height);
format.setInteger(MediaFormat.KEY_BIT_RATE, targetBitrateBps);
format.setInteger(MediaFormat.KEY_BITRATE_MODE, VIDEO_ControlRateConstant);
format.setInteger(MediaFormat.KEY_COLOR_FORMAT, colorFormat);
format.setInteger(MediaFormat.KEY_FRAME_RATE, targetFps);
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, keyFrameIntervalSec);
mediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);


В некоторых из этих параметров легко допустить ошибку, что вызовет исключение при конфигурации кодека и нарушит работу приложения. Также при работе с кодеком может понадобиться регулировать его битрейт в зависимости от различных факторов, так как сам кодек делает это неправильно. За это в WebRTC отвечает класс BaseBitrateAdjuster, у которого есть два наследника:

  • DynamicBitrateAdjuster — регулирует битрейт в зависимости от объёма данных,
  • FramerateBitrateAdjuster — регулирует битрейт в зависимости от частоты кадров.


Соответственно, для каждого кодека необходимо выбрать свой механизм регулировки битрейта. Рассмотрим подробнее особенности установки параметров инициализации для аппаратных кодеков.

Разрешение потока


После получения для кодека объекта MediaCodecInfo можно изучить кодек подробнее, получив его возможности в классе CodecCapabilities. Из них можно узнать, поддерживает ли кодек выбранные разрешение и частоту кадров. Если он поддерживает эти параметры, их можно устанавливать безопасно.

Однако иногда это правило не работает. Мы столкнулись с тем, что кодеки с префиксом «OMX.MARVELL.» кодировали неправильно, показывая зелёные полосы по краям экрана, если разрешение потока отличалось от 4:3. При этом сам кодек утверждал, что выбранные разрешение и частота кадров поддерживаются.

Режим битрейта


Стандартный режим для всех видеокодеков — постоянный битрейт. Однако однажды нам пришлось использовать переменный битрейт:

format.setInteger(MediaFormat.KEY_BITRATE_MODE, VIDEO_ControlRateVariable);


Произошло это на устройстве Lenovo A1000 с чипсетом компании Spreadtrum (теперь Unisoc), начинающимся с префикса «OMX.sprd.». Поиск в Интернете привёл нас к посту шестилетней давности о Firefox OS, описывающему эту проблему и способ её решения.

Цветовой формат


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

@Nullable
static Integer selectColorFormat(
  int[] supportedColorFormats, CodecCapabilities capabilities) {
    for (int supportedColorFormat : supportedColorFormats) {
  	for (int codecColorFormat : capabilities.colorFormats) {
    	    if (codecColorFormat == supportedColorFormat) {
      		return codecColorFormat;
    	    }
 	}
    }
    return null;
}


Грубо говоря, всегда выбирается первый из поддерживаемых цветовых форматов.

Однако в случае с кодеками HUAWEI, начинающимися с префиксов «OMX.IMG.TOPAZ.», «OMX.hisi.» и «OMX.k3.», это не работало, и после долгих поисков мы нашли решение: вне зависимости от того, какой формат возвращают эти кодеки, необходимо использовать формат COLOR_FormatYUV420SemiPlanar. Разобраться в этом нам помог тред на одном китайском форуме.

Регулировка битрейта


Стандартный код WebRTC содержит следующее:

private BitrateAdjuster createBitrateAdjuster(VideoCodecMimeType type, String codecName) {
  if (codecName.startsWith(EXYNOS_PREFIX)) {
	if (type == VideoCodecMimeType.VP8) {
  	// Exynos VP8 encoders need dynamic bitrate adjustment.
  	return new DynamicBitrateAdjuster();
	} else {
  	// Exynos VP9 and H264 encoders need framerate-based bitrate adjustment.
  	return new FramerateBitrateAdjuster();
	}
  }
  // Other codecs don't need bitrate adjustment.
  return new BaseBitrateAdjuster();
}


Как видно из этого кода, для всех чипсетов, кроме Exynos, регулировка битрейта выключена. Но это относится только к Qualcomm, так как в стандартном коде поддерживаются только Exynos и Qualcomm. Поэкспериментировав с различными значениями этой настройки, а также поискав в Интернете, мы выяснили, что для кодеков с префиксами «OMX.MTK.» её тоже нужно включить. Также необходимо сделать это для кодеков HUAWEI, начинающихся с префикса «OMX.IMG.TOPAZ.», «OMX.hisi.» или «OMX.k3.». Это связано с тем, что эти кодеки не используют временные метки кадров для регулировки битрейта, считая, что все кадры приходят с одинаковой частотой, установленной при конфигурации кодека.

В завершение приведу список кодеков, которые мы получили для устройств на Android 5.0 и 5.1. Они были нам интересны в первую очередь потому, что на более новых версиях Android ситуация улучшается и нестандартных кодеков становится всё меньше.

Это видно на графике ниже. Шкала логарифмическая, чтобы лучше показать редкие случаи.

lengwsark1w8mgc0jmg95pptv_k.png
Как мы видим, у большинства устройств были чипсеты Spreadtrum, MediaTek, HUAWEI и MARVELL — поэтому наши изменения помогли включить аппаратное кодирование на этих гаджетах.

Результат


Хотя мы и предполагали, что на некоторых устройствах при работе с H.264 будут возникать проблемы, Android опять смог нас удивить. Как мы видим из статистики пользователей Badoo, на руках у пользователей ещё достаточно много устройств 2014–2016 года выпуска, которые они не хотят или не могут обновлять. И хотя ситуация с выходом обновлений Android для новых устройств уже гораздо лучше, чем несколько лет назад, доля гаджетов предыдущего поколения сокращается довольно медленно и поддерживать их придётся ещё достаточно долго.

Сейчас WebRTC активно развивается Google из-за его использования в проекте Stadia (вот видео с подробностями на эту тему), поэтому он будет становиться всё лучше и лучше и, скорее всего, станет стандартом для реализации видеосвязи. Надеюсь, что эта статья поможет вам понять особенности работы с H.264 в WebRTC и использовать это в своих проектах.

© Habrahabr.ru