Видео стало важной частью нашей жизни и теперь часто интегрируется в различные мобильные приложения.
Reddit не является исключением. Там есть более 10 различных видеоповерхностей:
В этой статье я поделюсь практическими советами, подкрепленными производственными данными, о том, как улучшить воспроизведение видео с разных точек зрения и эффективно использовать ExoPlayer
в приложении для Android.
Эта статья будет полезна, если вы являетесь Android-инженером и знакомы с основами ExoPlayer и Media 3.
Доставка контента
Существует несколько популярных способов доставки видео по запросу (VOD) от сервера к клиенту.
Бинарный файл
Самый простой способ — получить бинарный файл, например mp4
, и воспроизвести его на клиенте. Он отлично работает и подходит для всех устройств.
Однако у этого метода есть недостатки. Так, он не позволяет адаптироваться автоматически к изменениям в сети и обеспечивает только один битрейт и разрешение. Это может оказаться проблемным для длинных видео, так как пропускной способности сети может не хватить для быстрой загрузки видео.
Адаптивный подход
Чтобы устранить недостаток, связанный с пропускной способностью при бинарной доставке, нашли другой способ — адаптивные протоколы, такие как HLS
, разработанный Apple, и DASH
от MPEG.
Вместо того чтобы напрямую получать видео- и аудиосегменты, эти протоколы сначала получают файл манифеста (manifest). В этом файле содержатся различные сегменты для каждого битрейта, а также отдельные дорожки для аудио и видео.
После загрузки файла manifest реализация протокола выберет наилучшее качество видео в зависимости от доступной пропускной способности устройства. Этот процесс достаточно умен, чтобы адаптировать качество видео «на лету» в зависимости от состояния сети. Особенно полезен такой подход для длинных видео.
Однако он не идеален. Так, чтобы начать воспроизведение видео в DASH
, может потребоваться не менее 3 циклов, которые включают получение manifest, аудио- и видеосегментов. Это может увеличить вероятность сетевой ошибки.
С другой стороны, в HLS
может потребоваться 4 цикла, включая получение главного манифеста, манифеста, аудио- и видеосегментов.
Опыт Reddit
Традиционно мы использовали DASH
для видеоконтента для Android и HLS
для видеоконтента для Web и iOS. Однако около 75 % нашего видеоконтента имеет длину менее 45 секунд.
Мы предполагаем, что для коротких видео нет необходимости переключать битрейт во время воспроизведения. Чтобы проверить эту теорию, мы провели эксперимент, в котором подавали некоторые видео в формате MP4
вместо DASH
с различными ограничениями по длительности.
Мы заметили, что ограничение 45-second
показало наиболее прагматичный результат:
- Количество ошибок при воспроизведении уменьшилось на 5,5%.
- Случаи, когда пользователи переставали смотреть видео до начала воспроизведения (функция «Выход до начала видео» предполагается в будущем), сократились на 2,5 %.
- Общее количество просмотров видео увеличилось на 1,7 %.
Основываясь на этих данных, мы приняли решение предоставлять все видео длительностью менее 45 секунд в чистом формате MP4
. Что касается более длинных видео, мы продолжим размещать их в адаптивном потоковом формате.
Кэширование и предварительное получение контента
Концепция предварительного получения подразумевает получение контента до его отображения и показ его из кэша, когда пользователь его находит. Однако сначала нам нужно реализовать кэширование, что может быть не так просто.
Рассмотрим потенциальные проблемы, с которыми можно столкнуться.
ExternalDir доступен не везде
В Android есть два варианта кэширования: внутренний и внешний кэш. Для большинства приложений использование internalDir
является практичным выбором, если только не нужно кэшировать огромные видеофайлы. В этом случае externalDir
может оказаться лучшим вариантом.
Важно отметить, что система может очистить internalDir
, если приложение достигнет определенной квоты, в то время как внешний кэш очищается только в случае удаления приложения (при условии хранения в папке app).
В Reddit мы изначально пытались кэшировать видео в externalDir
, но позже перешли на internalDir
, чтобы избежать проблем с совместимостью на устройствах, которые его не имеют, например OPPO.
SimpleCache может очищать другие файлы
Если вы посмотрите на реализацию SimpleCache, то заметите, что она не так проста, как следует из ее названия.
Таким образом, SimpleCache
потенциально может удалять другие файлы кэша, если только нет специальной выделенной папки, которая способна повлиять на другую логику приложения. Будьте осторожны.
SimpleCache повреждает диск в конструкторе
Во время создания SimpleCache мы столкнулись с частой ошибкой ANR (Application Not Responding). Погрузившись в реализацию, я понял, что он наносит вред диску в конструкторе:
Чтобы избежать такой ситуации, создавайте этот экземпляр в фоновом потоке.
URL используется в качестве кэш-ключа
Это предусмотрено по умолчанию. Однако если ваш URL отличается сигнатурой подписи или дополнительными параметрами, вам нужно предоставить фабрику кэш-ключей для источника данных. Это поможет увеличить количество кэш-хитов и оптимизировать производительность.
Вытеснение должно быть явно включено
Вытеснение — довольно эффективная стратегия, позволяющая предотвратить накопление кэшированных данных и возникновение проблем. Многие библиотеки, например Glide
, используют ее «под капотом». Если видеоконтент не находится в фокусе вашего приложения, SimpleCache
позволяет легко реализовать эту стратегию всего в одной строке:
Параметры предварительного получения контента
У нас есть 5 вариантов предварительного получения: DownloadManager, DownloadHelper
, DashUtil
, DashDownloader
и HlsDownloader
. На мой взгляд, проще всего работать с DownloadManager
. Можете интегрировать его с ExoPlayer
. Для работы он использует тот же экземпляр SimpleCache
:
Кроме того, DownloadManager располагает широкими возможностями кастомизации: так, он позволяет приостанавливать, возобновлять и удалять загрузки, что может быть очень удобно, когда пользователи прокручивают страницу слишком быстро и текущие процессы загрузки больше им не нужны. Кроме того, в DownloadManager есть множество опций для работы с потоками и распараллеливанием.
Для предварительного получения адаптивных потоков вы также можете использовать DownloadManager
в сочетании с DownloadHelper, который упрощает эту работу.
К сожалению, одним из недостатков этого способа является отсутствие возможности предварительной загрузки определенного объема видеоконтента (например, около 500 кб), о чем говорилось в этом обсуждении.
Опыт Reddit
Мы опробовали различные варианты, включая предварительное получение только следующего видео, либо двух следующих видео параллельно или друг за другом, а также только короткого видеоконтента (mp4).
Оценив эти подходы к предварительному получению контента, мы обнаружили, что реализация функции предварительного получения только следующего видео дала наиболее практичный результат:
- Время загрузки видео < 250 мс: не изменилось.
- Время загрузки видео < 500 мс: увеличилось на 1,9 %.
- Время загрузки видео > 1000 мс: уменьшилось на 0,872 %.
- Выход перед началом видео: без изменений.
Чтобы внести улучшения в наш эксперимент, мы решили учесть мощность интернет-соединения пользователя в качестве фактора для предварительного получения. Мы провели многовариантный эксперимент с различными вариантами пропускной способности, начиная с 2 Мбит/с и заканчивая 20 Мбит/с.
К сожалению, эксперимент не увенчался успехом. Например, при скорости 2 мбит/с:
- Время загрузки видео < 250 мс: уменьшилось на 0,9 %.
- Время загрузки видео < 500 мс: уменьшилось на 1,1 %.
- Время загрузки видео > 1000 мс: увеличилось на 3 %.
В будущем мы планируем продолжить эксперименты и определить, будет ли более выгодным частичное предварительное получение энного количества видео параллельно.
LoadControl
LoadControl — это механизм, позволяющий управлять загрузкой. Говоря простым языком, он позволяет ответить на следующие вопросы:
- Достаточно ли у нас данных для начала воспроизведения?
- Нужно ли продолжать загрузку данных?
А самое интересное в том, что мы можем настроить это поведение!
bufferForPlaybackMs, default: 2500
Это объем видеоконтента, который должен быть загружен до того, как будет отрисован первый кадр или воспроизведение будет прервано пользователем (например, функция pause/seek).
bufferForPlaybackAfterRebufferMs, default: 5000
Это объем данных, которые должны быть загружены после того, как воспроизведение прерывается из-за изменений в сети или переключения битрейта.
minBuffer & maxBuffer, default: 50000
Во время воспроизведения ExoPlayer
буферизирует медиаданные, пока не достигнет maxBufferMs
. Затем он приостанавливает загрузку, пока буфер не уменьшится до minBufferMs
, после чего загрузка возобновляется.
Вы можете заметить, что по умолчанию эти значения имеют одинаковое значение. Однако в ранних версиях ExoPlayer
они были разными. Различное значение конфигурации буфера могло привести к учащению ребуферинга при нестабильной работе сети.
При установке одинаковых значений буфер постоянно заполняется (эта техника называется Drip-Feeding).
Опыт Reddit
Поскольку большинство наших видеороликов короткие, мы заметили, что значения буфера по умолчанию были слишком длинными. Поэтому было решено попробовать разные значения и посмотреть, как они работают.
Мы обнаружили, что установка значений bufferForPlaybackAfterRebufferMs = 1 000
и minBuffer and maxBuffer = 20,000
дала наиболее прагматичные результаты:
- Время загрузки видео < 250 мс: увеличилось на 2,7 %.
- Время загрузки видео < 500 мс: увеличилось на 4,4 %.
- Время загрузки видео > 1000 мс: уменьшилось на 11,9%.
- Время загрузки видео > 2000 мс: уменьшилось на 17,7%.
- Ребуферинг сократился на 4,8%.
- Общее количество просмотров видео увеличилось на 1,5%.
На данный момент этот эксперимент с видео является одним из самых впечатляющих.
Улучшение адаптивного битрейта с помощью BandwidthMeter
Улучшение качества видео может оказаться непростой задачей, поскольку более высокое качество часто приводит к снижению скорости загрузки, поэтому важно найти правильный баланс для оптимизации впечатлений от просмотра.
Чтобы выбрать подходящий битрейт видео и обеспечить оптимальное качество видео в зависимости от работы сети, ExoPlayer
использует BandwidthMeter
.
Он рассчитывает пропускную способность сети, необходимую для загрузки сегментов, и на основе этого выбирает подходящие аудио- и видеодорожки для последующих видео.
Опыт Reddit
В какой-то момент мы заметили, что, хотя у пользователей наблюдается хорошая пропускная способность сети, мы не всегда предоставляем им видео наилучшего качества.
Первая проблема, которую мы выявили, заключалась в том, что предварительное получение контента не влияет на общую пропускную способность сети в BandwidthMeter, поскольку DataSource
в DownloadManager
ничего об этом не знает. Чтобы исправить ситуацию, нужно учитывать предварительное получение контента при работе с общей пропускной способностью.
Мы провели эксперимент для подтверждения этой теории на практике и получили следующий результат:
- На 1,4% улучшилось разрешение видео.
- Общее количество просмотров видео в цепочке увеличилось на 0,5 %.
- Изменение битрейта во время воспроизведения сократилось на 0,5%.
- Время загрузки видео > 1000 мс увеличилось на 0,3% (что является компромиссным решением).
Стоит отметить, что текущий инструмент BandwidthMeter все еще не идеален в плане расчета правильного битрейта видео. В версии 1.0.1 был добавлен экспериментальный BandwidthMeter, который со временем заменит старый, что должно улучшить положение дел.
Кроме того, по умолчанию BandwidthMeter
использует жестко запрограммированные значения, которые отличаются в зависимости от типа сети и страны. Они могут быть не актуальны для текущей сети и в целом могут оказаться неточными. Например, в Великобритании 3G считается быстрее, чем 4G.
Мы еще не проводили соответствующих экспериментов, но одним из способов решения этой проблемы было бы запоминание последнего значения пропускной способности сети и установка его при запуске приложения:
В AdaptiveTrackSelection.Factory
также есть несколько настроек для управления переключением между лучшим и худшим качеством: minDurationForQualityIncreaseMs
(значение по умолчанию: 15 000) и minDurationForQualityDecreaseMs
(значение по умолчанию: 25000).
Выбор битрейта для контента в формате MP4
Если видеоконтент не находится в фокусе вашего приложения и вы используете его, например, только для демонстрации обновлений или обзоров года, практичнее всего придерживаться среднего битрейта.
Когда мы впервые перевели короткие видео в формат mp4 в компании Reddit, мы начали передавать текущую полосу пропускания, чтобы получить следующую партию видео.
Однако это решение не очень точное, поскольку пропускная способность может меняться чаще. Мы решили внести следующее улучшение:
Основное отличие этой реализации (вторая диаграмма) от адаптивного битрейта (DASH/HLS) заключается в том, что нам не нужно было предварительно получать манифест (так как мы получаем его при получении партии видео), что снизило вероятность сетевых ошибок. Кроме того, битрейт остается постоянным во время воспроизведения.
Когда мы экспериментировали с этим подходом, мы изначально ориентировались на приблизительный битрейт для каждого видео и аудио, поэтому он не имел точных значений. В результате метрики не менялись в нужном направлении:
- Качество видео: улучшилось на 9,70 %.
- Время загрузки видео > 1000 мс: увеличилось на 12,9 %.
- Общее количество просмотров видео уменьшилось на 2 %.
В будущем мы будем экспериментировать, используя точные битрейты видео и аудио, а также пороговые значения, чтобы достичь оптимального баланса между временем загрузки и качеством.
Декодеры и экземпляры проигрывателей
В какой-то момент мы заметили всплеск ошибки воспроизведения 4001, которая говорила о том, что декодер недоступен. Эта проблема возникала почти для всех поставщиков Android.
Каждое устройство имеет ограничения по количеству доступных декодеров. Описанная проблема может возникнуть, например, когда приложение не выпустило декодер должным образом.
Несмотря на то, что мы не можем решить проблему с декодером на 100%, ExoPlayer
предоставляет возможность переключиться на программный декодер, если основной недоступен:
Это решение не идеально, поскольку возврат к программному декодеру может стать причиной более медленной работы по сравнению с аппаратным декодером, но это все же лучше, чем невозможность воспроизвести видео. Включение опции fallback во время экспериментов привело к снижению количества ошибок воспроизведения на 0,9 %.
Чтобы уменьшить количество таких случаев, ExoPlayer
использует аудиоменеджер и может запрашивать фокус от вашего имени. Однако для этого необходимо сделать явный запрос:
Еще один вариант — использование только одного экземпляра ExoPlayer
для каждого приложения. Изначально это может показаться простым решением. Однако, если у вас есть видео в лентах, управление миниатюрами и последними кадрами вручную может оказаться сложной задачей. Кроме того, если вы хотите повторно использовать уже инициализированные декодеры, нужно избегать вызова stop()
и вызывать prepare()
с новым видео поверх текущего воспроизведения.
С другой стороны, синхронизация нескольких экземпляров ExoPlayer
также является сложной задачей и может привести к проблемам с размытием звука.
В Reddit мы повторно используем видеоплееры при навигации между поверхностями. Однако при прокрутке мы сейчас создаем новый экземпляр для каждого воспроизведения видео, что приводит к лишним накладным расходам.
В настоящее время мы рассматриваем два варианта: использование фиксированного пула игроков на основе доступности декодеров или задействование одного экземпляра. Как только мы проведем соответствующий эксперимент, мы напишем новую статью, чтобы поделиться результатами.
Рендеринг
Имеем два варианта: TextureView
или SurfaceView
. Если TextureView
— это обычное представление, интегрированное в иерархию представлений, то SurfaceView
располагает другим механизмом рендеринга. Он отрисовывает контент в отдельном окне непосредственно на GPU, в то время как TextureView
рендерит в окне приложения и нуждается в синхронизации с GPU, что может создавать накладные расходы в плане производительности и расхода батареи.
Однако если у вас много анимаций с видео, имейте в виду, что до Android N у SurfaceView
были проблемы с синхронизацией анимаций.
ExoPlayer
также предоставляет стандартные элементы управления (play/pause/seekbar) и позволяет выбрать место рендеринга видео.
Опыт Reddit
Исторически сложилось так, что для рендеринга видео мы использовали TextureView
. Однако мы планируем перейти на SurfaceView
для повышения эффективности.
В настоящее время мы переносим наши функции на Jetpack Compose
и создали композиционные обертки для видео. Одна из проблем, с которой мы столкнулись, заключается в том, что, поскольку большинство наших основных лент уже работают в Compose, нам приходится постоянно повторно «раздувать» видео, что может занимать до 30ms
, а это приводит к сбросу кадров.
Для решения этой проблемы в Jetpack Compose 1.4
появился ViewPool
, в котором необходимо переопределять обратные вызовы:
Тем не менее мы решили реализовать собственный ViewPool
, чтобы иметь возможность повторно использовать переполненные представления на разных экранах и получить больше контроля в будущем, например предварительно инициализировать их перед показом первого видео:
Эта реализация дала следующие преимущества:
- Время загрузки видео < 250 мс: увеличилось на 1,7 %.
- Время загрузки видео < 500 мс: увеличилось на 0,3%.
- Количество просмотренных минут видео увеличилось на 1,4%.
- Создание P50:
1ms
, улучшено в 30 раз. - Создание P90:
24ms
, улучшено в 1,5 раза.
Кроме того, поскольку стандартные элементы управления ExoPlayer
реализованы с помощью устаревших представлений, я бы рекомендовал всегда реализовывать собственные элементы управления, чтобы избежать ненужного «раздувания».
Обертки для SurfaceView
уже есть в Jetpack Compose 1.6: AndroidExternalSurface и AndroidEmbeddedExternalSurface.
Заключение
Один из ключевых моментов, о которых следует помнить при работе с видео, — это важность аналитики и регулярного проведения A/B-тестирования с внесением различных улучшений. Это помогает выявить не только положительные изменения, но и проблемы с регрессией.
Если вы только начали работать с видеороликами, вы должны знать о следующих событиях:
- отрисовка первого кадра (время);
- ребуферинг;
- запуск/остановка воспроизведения;
- ошибка воспроизведения.
ExoPlayer
также предоставляет AnalyticsListener, который поможет в этом.
Кроме того, должен сказать, что работа с видео была для меня довольно сложным опытом. Но не волнуйтесь. Если и у вас все пойдет не по плану — это совершенно нормально. На самом деле так и должно быть. А если представить процесс работы с видео в виде песни, то на ум приходит «Trouble» группы Cage the Elephant.
Читайте также:
- Динамическое извлечение видеокадров в Android
- Миграция UI-ориентированной библиотеки Android на Compose Multiplatform (Android/iOS)
- Изучаем AndroidManifest.xml: <service> как подэлемент <application>
Читайте нас в Telegram, VK и Дзен
Перевод статьи Alexey Bykov: Improving video playback with ExoPlayer