Новости о продуктах

Улучшение воспроизведения медиаконтента: подробный обзор PreloadManager в Media3. Часть 2

Время на чтение: 9 минут
Посмотреть профиль Mayuri Khinvasara Khabya
Mayuri Khinvasara Khabya Developer Relations Engineer

Представляем вторую часть серии из трех статей о предварительной загрузке медиаконтента с помощью Media3. В этой серии видео мы расскажем, как создавать приложения для Android, которые быстро реагируют на действия пользователя и воспроизводят медиаконтент с низкой задержкой.

  • В первой части мы рассмотрели основы. Мы рассмотрели разницу между PreloadConfiguration для простых плейлистов и более мощным DefaultPreloadManager для динамических пользовательских интерфейсов. Вы узнали, как реализовать базовый жизненный цикл API: добавлять медиафайлы с помощью add(), получать подготовленный MediaSource с помощью getMediaSource(), управлять приоритетами с помощью setCurrentPlayingIndex() и invalidate(), а также освобождать ресурсы с помощью remove() и release().
  • Часть 2 (эта статья). В этой статье мы рассмотрим расширенные возможности DefaultPreloadManager. Вы узнаете, как получать статистику с помощью PreloadManagerListener, применять рекомендации по подготовке к публикации, например использовать основные компоненты совместно с ExoPlayer, и эффективно управлять памятью, используя скользящее окно.
  • Часть 3. В заключительной части этой серии мы рассмотрим интеграцию PreloadManager с постоянным кешем диска, что позволит вам сократить потребление данных за счет управления ресурсами и обеспечить бесперебойную работу.

Если вы ещё не знакомы с предварительной загрузкой в Media3, настоятельно рекомендуем сначала прочитать первую часть. Если вы готовы выйти за рамки базовых функций, давайте рассмотрим, как улучшить реализацию воспроизведения медиаконтента.

Как получать аналитику с помощью PreloadManagerListener

Когда вы запускаете функцию в рабочей среде, вам как разработчику приложений также нужно понимать и собирать аналитику по ней. Как убедиться, что стратегия предзагрузки эффективна в реальных условиях? Чтобы ответить на этот вопрос, нужны данные о доле успешных запросов, сбоях и производительности. Основной механизм сбора этих данных – интерфейс PreloadManagerListener.

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

  • onCompleted(MediaItem mediaItem) – этот обратный вызов выполняется после успешного завершения запроса на предзагрузку, как определено в TargetPreloadStatusControl.
  • onError(PreloadException error) – этот обратный вызов может быть полезен для отладки и мониторинга. Он вызывается, когда предварительная загрузка завершается ошибкой, и предоставляет связанное исключение.

Вы можете зарегистрировать прослушиватель с помощью одного вызова метода, как показано в примере кода ниже.

val preloadManagerListener = object : PreloadManagerListener {
    override fun onCompleted(mediaItem: MediaItem) {
        // Log success for analytics. 
        Log.d("PreloadAnalytics", "Preload completed for $mediaItem")
    }

    override fun onError( preloadError: PreloadException) {
        // Log the specific error for debugging and monitoring.
        Log.e("PreloadAnalytics", "Preload error ", preloadError)
    }
}

preloadManager.addListener(preloadManagerListener)

Извлечение информации из слушателя

Эти обратные вызовы можно подключить к конвейеру аналитики. Передавая эти события в систему аналитики, вы сможете получить ответы на следующие вопросы:

  • Какова доля успешных предварительных загрузок? (отношение количества событий onCompleted к общему числу попыток предзагрузки).
  • Какие сети доставки контента или форматы видео имеют самые высокие показатели ошибок? (путем анализа исключений из onError)
  • Какова доля ошибок при предварительной загрузке? (отношение количества событий onError к общему числу попыток предзагрузки)

Эти данные могут дать вам количественную обратную связь о вашей стратегии предзагрузки, позволяя проводить A/B-тестирование и улучшать удобство использования на основе данных. Эти данные помогут вам оптимизировать длительность предзагрузки, количество предзагружаемых видео и размер буфера.

Не только отладка: использование onError для резервного интерфейса

Неудачная предзагрузка – это явный признак того, что у пользователя возникнут проблемы с буферизацией. Функция обратного вызова onError позволяет реагировать на ошибки. Вместо того чтобы просто регистрировать ошибку, вы можете адаптировать интерфейс. Например, если следующее видео не загружается заранее, приложение может отключить автовоспроизведение для следующего пролистывания, чтобы пользователь мог начать воспроизведение нажатием.

Кроме того, вы можете определить более эффективную стратегию повторных попыток, проверив тип PreloadException. Приложение может сразу удалить неработающий источник из менеджера на основе сообщения об ошибке или кода статуса HTTP. Объект нужно удалить из потока пользовательского интерфейса, чтобы проблемы с загрузкой не повлияли на удобство использования. Вы также можете получить более подробные данные из PreloadException, например HttpDataSourceException, чтобы глубже изучить ошибки. Подробнее об устранении неполадок с ExoPlayer…

Зачем ExoPlayer нужны компоненты из Media3

DefaultPreloadManager и ExoPlayer предназначены для совместной работы. Чтобы обеспечить стабильность и эффективность, они должны использовать несколько основных компонентов. Если они работают с отдельными, несогласованными компонентами, это может повлиять на безопасность потоков и удобство использования предварительно загруженных треков в проигрывателе, поскольку нам необходимо убедиться, что предварительно загруженные треки должны воспроизводиться в правильном проигрывателе. Кроме того, отдельные компоненты могут конкурировать за ограниченные ресурсы, такие как пропускная способность сети и память, что может привести к снижению производительности. Важная часть жизненного цикла – правильное удаление. Рекомендуемый порядок удаления: сначала PreloadManager, затем ExoPlayer.

DefaultPreloadManager.Builder упрощает этот процесс и содержит API для создания экземпляров PreloadManager и связанного проигрывателя. Давайте разберемся, почему такие компоненты, как BandwidthMeter, LoadControl, TrackSelector и Looper, должны быть общими. Посмотрите визуальное представление того, как эти компоненты взаимодействуют с ExoPlayer Playback.

preloadManager2.png

Как предотвратить конфликты пропускной способности с помощью общего BandwidthMeter

BandwidthMeter оценивает доступную пропускную способность сети на основе статистики скорости передачи данных. Если PreloadManager и проигрыватель используют разные экземпляры, они не знают о сетевой активности друг друга, что может привести к сбоям. Например, пользователь смотрит видео, и в этот момент качество сетевого подключения ухудшается, а MediaSource начинает активно скачивать следующее видео. Активность MediaSource, выполняющего предварительную загрузку, будет потреблять пропускную способность, необходимую активному проигрывателю, что приведет к остановке текущего видео. Остановка воспроизведения – это серьезная проблема, которая может оттолкнуть пользователей.

Благодаря общему BandwidthMeter TrackSelector может выбирать дорожки наивысшего качества с учетом текущих условий сети и состояния буфера во время предзагрузки или воспроизведения. Затем он может принимать разумные решения, чтобы защитить активный сеанс воспроизведения и обеспечить бесперебойную работу.

preloadManagerBuilder.setBandwidthMeter(customBandwidthMeter)

Обеспечение согласованности с общими компонентами LoadControl, TrackSelector и Renderer в ExoPlayer

  • LoadControl. Этот компонент определяет правила буферизации, например объем данных, которые нужно буферизовать перед началом воспроизведения, и время начала или остановки загрузки дополнительных данных. Общий LoadControl гарантирует, что потребление памяти проигрывателем и PreloadManager будет регулироваться единой скоординированной стратегией буферизации как для предварительно загруженных, так и для активно воспроизводимых медиафайлов, что предотвратит конкуренцию за ресурсы. Вам нужно будет грамотно распределить размер буфера, согласовав его с количеством предварительно загружаемых элементов и их продолжительностью, чтобы обеспечить согласованность. В случае конфликта проигрыватель будет отдавать приоритет воспроизведению текущего элемента, показанного на экране. При использовании общего LoadControl менеджер предварительной загрузки будет продолжать предварительную загрузку до тех пор, пока целевые байты буфера, выделенные для предварительной загрузки, не достигнут верхнего предела, он не будет ждать завершения загрузки для воспроизведения.

Примечание. Общий доступ к LoadControl в последней версии Media3 (1.8) гарантирует, что его Allocator может быть правильно предоставлен PreloadManager и проигрывателю. Использование LoadControl для эффективного управления предварительной загрузкой – это функция, которая будет доступна в предстоящем выпуске Media3 1.9.

preloadManagerBuilder.setLoadControl(customLoadControl)

  • TrackSelector. Этот компонент отвечает за выбор дорожек (например, видео определенного разрешения или аудио на определенном языке) для загрузки и воспроизведения. Это гарантирует, что проигрыватель будет использовать те же дорожки, которые были выбраны во время предварительной загрузки. Это позволяет избежать ситуации, когда предварительно загружается видеодорожка 480p, а проигрыватель сразу же отбрасывает ее и запрашивает дорожку 720p при воспроизведении.< br /> Менеджер предварительной загрузки НЕ должен использовать тот же экземпляр TrackSelector, что и проигрыватель. Вместо этого они должны использовать другой экземпляр TrackSelector, но с той же реализацией. Поэтому в DefaultPreloadManager.Builder мы задаем TrackSelectorFactory, а не TrackSelector.

preloadManagerBuilder.setTrackSelectorFactory(customTrackSelectorFactory)

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

preloadManagerBuilder.setRenderersFactory(customRenderersFactory)

Подробнее о компонентах Exoplayer…

Золотое правило: один Playback Looper для всех

Поток, в котором можно получить доступ к экземпляру ExoPlayer, можно явно указать, передав Looper при создании проигрывателя. Цикл Looper потока, из которого осуществляется доступ к проигрывателю, можно запросить с помощью Player.getApplicationLooper. Благодаря общему Looper между проигрывателем и PreloadManager все операции с общими медиаобъектами сериализуются в очередь сообщений одного потока. Это позволяет снизить количество ошибок параллелизма.

Все взаимодействия между PreloadManager и проигрывателем с источниками мультимедиа, которые нужно загрузить или предварительно загрузить, должны происходить в одном потоке воспроизведения. Для обеспечения потокобезопасности необходимо использовать общий Looper, поэтому мы должны использовать общий PlaybackLooper между PreloadManager и проигрывателем.

PreloadManager подготавливает объект MediaSource с отслеживанием состояния в фоновом режиме. Когда код пользовательского интерфейса вызывает player.setMediaSource(mediaSource), вы передаете этот сложный объект с отслеживанием состояния от предварительно загруженного MediaSource проигрывателю. В этом случае весь объект PreloadMediaSource перемещается из менеджера в проигрыватель. Все эти взаимодействия и передачи должны происходить в одном и том же PlaybackLooper.

Если бы PreloadManager и ExoPlayer работали в разных потоках, могло бы возникнуть состояние гонки. Поток PreloadManager может изменять внутреннее состояние MediaSource (например, записывать новые данные в буфер) в тот самый момент, когда поток проигрывателя пытается выполнить чтение из него. Это приводит к непредсказуемому поведению и ошибке IllegalStateException, которую трудно отладить.

preloadManagerBuilder.setPreloadLooper(playbackLooper)

Давайте посмотрим, как можно использовать все перечисленные выше компоненты в ExoPlayer и DefaultPreloadManager.

val preloadManagerBuilder =
DefaultPreloadManager.Builder(context, targetPreloadStatusControl)

// Optional - Share components between ExoPlayer and DefaultPreloadManager
preloadManagerBuilder
     .setBandwidthMeter(customBandwidthMeter)
     .setLoadControl(customLoadControl)
     .setMediaSourceFactory(customMediaSourceFactory)
     .setTrackSelectorFactory(customTrackSelectorFactory)
     .setRenderersFactory(customRenderersFactory)
     .setPreloadLooper(playbackLooper)

val preloadManager = val preloadManagerBuilder.build()

Совет. Если вы используете в ExoPlayer компоненты по умолчанию, например DefaultLoadControl, вам не нужно явно передавать их в DefaultPreloadManager. Если вы создаете экземпляр ExoPlayer с помощью buildExoPlayer из DefaultPreloadManager.Builder, эти компоненты автоматически ссылаются друг на друга, если вы используете реализации по умолчанию с конфигурациями по умолчанию. Если же вы используете собственные компоненты или конфигурации, вам нужно явно сообщить о них DefaultPreloadManager с помощью указанных выше API.

Предварительная загрузка, готовая к использованию: шаблон скользящего окна

В динамическом фиде пользователь может прокручивать практически бесконечное количество контента. Если вы будете постоянно добавлять видео в DefaultPreloadManager, не удаляя их, то неизбежно столкнетесь с ошибкой OutOfMemoryError. Каждый предварительно загруженный MediaSource содержит SampleQueue, который выделяет буферы памяти. По мере накопления они могут исчерпать пространство кучи приложения. Решением является алгоритм, с которым вы, возможно, уже знакомы, называемый скользящим окном. Шаблон скользящего окна поддерживает в памяти небольшой управляемый набор элементов, которые логически примыкают к текущей позиции пользователя в фиде. По мере прокрутки это "окно" управляемых элементов перемещается вместе с пользователем, добавляя новые элементы, которые попадают в поле зрения, а также удаляя элементы, которые теперь находятся далеко.

slidingwindow.png

Как реализовать шаблон скользящего окна

Важно понимать, что в PreloadManager нет встроенного метода setWindowSize(). Скользящее окно – это шаблон проектирования, который вы, как разработчик, должны реализовать с помощью примитивных методов add() и remove(). Логика приложения должна связывать события пользовательского интерфейса, такие как прокрутка или смена страницы, с этими вызовами API. Если вам нужен пример кода, посмотрите шаблон скользящего окна, реализованный в примере socialite, который также включает PreloadManagerWrapper, имитирующий скользящее окно.

Не забудьте добавить в реализацию preloadManager.remove(mediaItem), чтобы удалять объекты, которые вряд ли будут воспроизведены в ближайшее время. Если не удалять объекты, которые больше не находятся рядом с пользователем, это может привести к проблемам с памятью при предзагрузке. Вызов remove() гарантирует, что ресурсы будут освобождены, что поможет вам поддерживать использование памяти вашим приложением на стабильном уровне.

Тонкая настройка стратегии категоризированной предзагрузки с помощью TargetPreloadStatusControl

Теперь, когда мы определили, что нужно предварительно загружать (элементы в нашем окне), мы можем применить четко определенную стратегию относительно того, сколько нужно предварительно загружать для каждого элемента. В первой части мы уже рассказывали, как добиться такой детализации с помощью настройки TargetPreloadStatusControl.

Напомним, что у объекта на позиции +/- 1 вероятность воспроизведения может быть выше, чем у объекта на позиции +/- 4. Вы можете выделить больше ресурсов (сеть, ЦП, память) для элементов, которые пользователь, скорее всего, просмотрит следующими. Это создает стратегию "предварительной загрузки" на основе близости, которая является ключом к балансировке немедленного воспроизведения с эффективным использованием ресурсов.

Чтобы определить стратегию длительности предзагрузки, вы можете использовать данные аналитики, полученные через PreloadManagerListener, как описано в предыдущих разделах.

Заключение и дальнейшие действия

Теперь вы знаете, как создавать быстрые, стабильные и эффективные ленты медиаконтента с помощью DefaultPreloadManager в Media3.

Основные выводы

  • Используйте PreloadManagerListener, чтобы собирать аналитические данные и реализовать надежную обработку ошибок.
  • Всегда используйте один DefaultPreloadManager.Builder для создания экземпляров менеджера и проигрывателя, чтобы обеспечить совместное использование важных компонентов.
  • Реализуйте шаблон скользящего окна, активно управляя вызовами add() и remove(), чтобы предотвратить OutOfMemoryError.
  • Используйте TargetPreloadStatusControl, чтобы создать умную многоуровневую стратегию предварительной загрузки, которая балансирует производительность и потребление ресурсов.

Что дальше. Часть 3. Кеширование с предварительно загруженными медиафайлами

Предварительная загрузка данных в память обеспечивает немедленный прирост производительности, но может иметь и отрицательные последствия. После закрытия приложения или удаления предварительно загруженного медиаконтента из менеджера данные удаляются. Чтобы добиться более стабильного уровня оптимизации, мы можем объединить предзагрузку с кешем диска. Эта функция находится в активной разработке и станет доступна в ближайшие месяцы.

Хотите поделиться отзывом? Мы будем рады услышать ваше мнение.

Следите за новостями и попробуйте ускорить воспроизведение видео. 🚀

Автор:
Продолжить чтение