Улучшение воспроизведения медиаконтента: подробное погружение в тему PreloadManager в Media3 (часть 2)
Время на чтение: 9 минут
Представляем вторую статью из серии, посвященной предварительной загрузке медиаконтента с помощью Media3. В этой серии статей рассказывается, как создавать в приложениях для Android медиаконтент с высокой скоростью отклика и низкой задержкой.
- Часть 1. Предварительная загрузка с помощью Media3 посвящена основам. Мы рассмотрели различия между PreloadConfiguration для простых плейлистов и более мощным DefaultPreloadManager для динамических пользовательских интерфейсов. Вы узнали, как реализовать базовый жизненный цикл API: добавлять медиаконтент с помощью add(), получать подготовленный MediaSource с помощью getMediaSource(), управлять приоритетами с помощью setCurrentPlayingIndex() и invalidate(), а также освобождать ресурсы с помощью remove() и release().
- Часть 2 (эта статья). В этом блоге мы рассмотрим расширенные возможности DefaultPreloadManager. Мы расскажем, как получать статистику с помощью PreloadManagerListener, применять готовые рекомендации, например использовать основные компоненты с ExoPlayer, и эффективно управлять памятью с помощью шаблона скользящего окна.
- Часть 3. В заключительной части этой серии мы расскажем, как интегрировать PreloadManager с постоянным дисковым кешем, чтобы вы могли сократить потребление данных с помощью управления ресурсами и обеспечить бесперебойную работу.
Если вы ещё не работали с предварительной загрузкой в Media3, рекомендуем сначала прочитать часть 1. Если вы хотите узнать больше, мы расскажем, как улучшить реализацию воспроизведения медиаконтента.
Прослушивание: получение данных аналитики с помощью 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 к общему количеству попыток предварительной загрузки)
- Какие CDN или форматы видео показывают наибольшее количество ошибок? (путем анализа исключений из onError)
- Какова доля ошибок при предварительной загрузке? (соотношение событий onError к общему количеству попыток предзагрузки)
Эти данные помогут вам получить количественную обратную связь о вашей стратегии предварительной загрузки, проводить A/B-тестирование и улучшать удобство использования приложения на основе данных. Эти данные помогут вам оптимизировать продолжительность предзагрузки, количество предзагружаемых видео и выделяемые буферы.
Помимо отладки: использование onError для корректного перехода на резервный интерфейс
Неудачная предварительная загрузка – это явный признак того, что пользователь скоро столкнется с буферизацией. Функция обратного вызова onError позволяет реагировать на ошибки. Вместо того чтобы просто регистрировать ошибку, вы можете адаптировать интерфейс. Например, если следующее видео не загрузилось заранее, приложение может отключить автовоспроизведение для следующего пролистывания и потребовать от пользователя нажать на экран, чтобы начать воспроизведение.
Кроме того, изучив тип PreloadException, вы можете разработать более эффективную стратегию повторных попыток. Приложение может сразу удалить источник из менеджера на основе сообщения об ошибке или кода статуса HTTP. Чтобы проблемы с загрузкой не влияли на удобство использования, элемент нужно удалить из потока интерфейса. Вы также можете получить более подробные данные из PreloadException, например HttpDataSourceException, чтобы глубже изучить ошибки. Подробнее об устранении неполадок с ExoPlayer…
Система обмена данными: почему необходимо передавать компоненты ExoPlayer?
DefaultPreloadManager и ExoPlayer предназначены для совместной работы. Чтобы обеспечить стабильность и эффективность, они должны использовать несколько основных компонентов. Если они работают с отдельными, не скоординированными компонентами, это может повлиять на безопасность потока и удобство использования предварительно загруженных треков на проигрывателе, поскольку нам нужно убедиться, что предварительно загруженные треки воспроизводятся на правильном проигрывателе. Отдельные компоненты могут конкурировать за ограниченные ресурсы, такие как пропускная способность сети и память, что может привести к снижению производительности. Важная часть жизненного цикла – правильная утилизация. Рекомендуется сначала освободить PreloadManager, а затем ExoPlayer.
Класс DefaultPreloadManager.Builder предназначен для упрощения этой задачи и содержит API для создания экземпляров как PreloadManager, так и связанного с ним проигрывателя. Давайте разберемся, почему такие компоненты, как BandwidthMeter, LoadControl, TrackSelector и Looper, должны быть общими. Посмотрите визуальное представление того, как эти компоненты взаимодействуют с воспроизведением ExoPlayer.
Как избежать конфликтов пропускной способности при использовании общего BandwidthMeter
BandwidthMeter оценивает доступную пропускную способность сети на основе истории скорости передачи данных. Если PreloadManager и проигрыватель используют разные экземпляры, они не знают о сетевой активности друг друга, что может привести к сбоям. Например, пользователь смотрит видео, но качество связи ухудшается, и MediaSource начинает агрессивно скачивать следующее видео. Активность MediaSource, выполняющего предварительную загрузку, будет потреблять пропускную способность, необходимую активному проигрывателю, что приведет к остановке текущего видео. Задержки при воспроизведении видео значительно ухудшают качество просмотра.
Используя один и тот же объект BandwidthMeter, TrackSelector может выбирать треки самого высокого качества с учетом текущих условий сети и состояния буфера во время предварительной загрузки или воспроизведения. После этого система может принимать решения, чтобы защитить активный сеанс воспроизведения и обеспечить его бесперебойность.
preloadManagerBuilder.setBandwidthMeter(customBandwidthMeter)
Обеспечение согласованности с общими компонентами LoadControl, TrackSelector и Renderer ExoPlayer
- LoadControl. Этот компонент определяет правила буферизации, например сколько данных нужно буферизовать перед началом воспроизведения и когда начинать или прекращать загрузку дополнительных данных. Общий LoadControl гарантирует, что потребление памяти проигрывателем и PreloadManager регулируется единой скоординированной стратегией буферизации как для предварительно загруженного, так и для воспроизводимого в данный момент медиаконтента, предотвращая конфликты ресурсов. Чтобы обеспечить стабильность, вам нужно правильно распределить размер буфера, учитывая количество предварительно загружаемых объектов и их продолжительность. В случае конфликта проигрыватель будет отдавать приоритет воспроизведению текущего элемента, отображаемого на экране. При использовании общего объекта LoadControl менеджер предварительной загрузки продолжает загружать контент, пока не будет достигнут верхний предел целевого буфера, выделенного для предварительной загрузки. Он не ждет, пока завершится загрузка для воспроизведения.
Примечание. В последней версии Media3 (1.8) реализована возможность совместного использования LoadControl, благодаря чему его 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…
Золотое правило: используйте один цикл воспроизведения
Поток, в котором можно получить доступ к экземпляру ExoPlayer, можно явно указать, передав Looper при создании проигрывателя. Объект Looper потока, из которого должен быть получен доступ к проигрывателю, можно запросить с помощью метода Player.getApplicationLooper. Если проигрыватель и PreloadManager используют один и тот же объект Looper, все операции с общими медиаобъектами будут выполняться последовательно в очереди сообщений одного потока. Это может уменьшить количество ошибок параллелизма.
Все взаимодействия между PreloadManager и проигрывателем с источниками медиаконтента, которые нужно загрузить или предварительно загрузить, должны происходить в одном и том же потоке воспроизведения. Looper необходимо передавать для обеспечения безопасности потока, поэтому мы должны передавать PlaybackLooper между PreloadManager и проигрывателем.
PreloadManager подготавливает объект MediaSource с состоянием в фоновом режиме. Когда код вашего интерфейса вызывает player.setMediaSource(mediaSource), вы передаете этот сложный объект с состоянием из предварительно загруженного MediaSource в проигрыватель. В этом случае весь объект PreloadMediaSource перемещается из менеджера в проигрыватель. Все эти взаимодействия и передачи должны происходить в одном и том же цикле воспроизведения.
Если 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, который выделяет буферы памяти. По мере накопления они могут исчерпать пространство кучи приложения. Для решения этой задачи можно использовать алгоритм, который вам, возможно, уже знаком, – скользящее окно. При использовании шаблона скользящего окна в памяти хранится небольшой набор элементов, логически связанных с текущей позицией пользователя в фиде. По мере прокрутки экрана пользователем это "окно" управляемых объектов перемещается вместе с ним, добавляя новые объекты, которые появляются в поле зрения, и удаляя те, которые теперь находятся далеко.
Реализация шаблона скользящего окна
Важно понимать, что в PreloadManager нет встроенного метода setWindowSize(). Скользящее окно – это шаблон проектирования, который разработчик должен реализовать самостоятельно, используя примитивные методы add() и remove(). Логика приложения должна связывать события интерфейса, например прокрутку или смену страницы, с этими вызовами API. Если вам нужен пример кода, посмотрите реализацию скользящего окна в образце socialite. В нем также есть PreloadManagerWrapper, который имитирует скользящее окно.
Не забудьте добавить в реализацию preloadManager.remove(mediaItem), когда элемент вряд ли будет воспроизведен в ближайшее время. Основная причина проблем с памятью при реализации предзагрузки – это неспособность удалять объекты, которые больше не находятся рядом с пользователем. Вызов remove() гарантирует, что ресурсы будут освобождены, что поможет вам поддерживать стабильное использование памяти приложения.
Как настроить стратегию предварительной загрузки с помощью TargetPreloadStatusControl
Теперь, когда мы определили, что нужно предварительно загрузить (элементы в окне), мы можем применить четко определенную стратегию того, сколько предварительно загружать для каждого элемента. Мы уже видели, как этого можно добиться с помощью настройки TargetPreloadStatusControl в части 1.
Напомним, что вероятность воспроизведения элемента на позиции +/- 1 выше, чем на позиции +/- 4. Вы можете выделить больше ресурсов (сеть, процессор, память) для объектов, которые пользователь, скорее всего, посмотрит следующими. Это позволяет создать стратегию предварительной загрузки на основе близости, которая помогает сбалансировать немедленное воспроизведение с эффективным использованием ресурсов.
Вы можете использовать данные аналитики через PreloadManagerListener, как описано в предыдущих разделах, чтобы определить стратегию продолжительности предварительной загрузки.
Заключение и дальнейшие действия
Теперь вы знаете, как создавать быстрые, стабильные и эффективные медиафиды с помощью DefaultPreloadManager из Media3.
Основные выводы
- Используйте PreloadManagerListener, чтобы собирать данные аналитики и реализовать надежную обработку ошибок.
- Всегда используйте один и тот же объект DefaultPreloadManager.Builder для создания экземпляров менеджера и проигрывателя, чтобы важные компоненты были общими.
- Реализуйте шаблон скользящего окна, активно управляя вызовами add() и remove(), чтобы предотвратить ошибку OutOfMemoryError.
- Используйте TargetPreloadStatusControl, чтобы создать многоуровневую стратегию предварительной загрузки, которая обеспечит баланс между производительностью и потреблением ресурсов.
Что дальше в части 3: кеширование с предварительно загруженными медиафайлами
Предварительная загрузка данных в память позволяет сразу повысить производительность, но может иметь и недостатки. После закрытия приложения или удаления предварительно загруженного контента из менеджера данные исчезают. Чтобы оптимизация была более устойчивой, можно сочетать предварительную загрузку с кешированием на диске. Эта функция находится на стадии активной разработки и станет доступна в ближайшие месяцы.
Хотите поделиться отзывом? Мы ждем вашего ответа.
Следите за новостями и ускоряйте воспроизведение видео! 🚀
-
Новости продуктовВ современных приложениях, ориентированных на медиаконтент, плавное воспроизведение без прерываний – это ключевой фактор удобства для пользователей. Пользователи ожидают, что видео будут запускаться мгновенно и воспроизводиться без пауз.
Mayuri Khinvasara Khabya • Время на чтение: 8 минут -
Новости продуктовУ разработчиков Android есть множество вариантов выбора агентов, больших языковых моделей (LLM), инструментов и интерфейсов командной строки (CLI), которые можно использовать для разработки приложений. Наша цель – помочь вам создавать красивые и качественные приложения для Android, независимо от того, как вы это делаете.
Simona Milanovic • Время на чтение: 4 минуты -
Новости продуктовМы постоянно расширяем возможности платформы подписок в Google Play, чтобы помочь вам развивать бизнес, адаптироваться к новым бизнес-моделям и привлекать пользователей.
Sheenam Mittal • Время на чтение: 4 минуты
Получайте свежие новости о разработке приложений для Android на свою электронную почту каждую неделю.