Улучшение воспроизведения мультимедиа: подробный анализ PreloadManager от Media3 — Часть 2
9 минут чтения
Добро пожаловать во вторую часть нашей серии из трех статей о предварительной загрузке медиаконтента с помощью Media3. Эта серия призвана помочь вам в процессе создания высокоэффективных приложений для Android с низкой задержкой и быстрым откликом.
- Part 1: Introducing Preloading with Media3 covered the fundamentals. We explored the distinction between PreloadConfiguration for simple playlists and the more powerful DefaultPreloadManager for dynamic user interfaces. You learned how to implement the basic API lifecycle: adding media with add(), retrieving a prepared MediaSource with getMediaSource(), managing priorities with setCurrentPlayingIndex() and invalidate(), and releasing resources with remove() and release().
- Часть 2 (Эта статья): В этом блоге мы рассмотрим расширенные возможности DefaultPreloadManager. Мы расскажем, как получить ценную информацию с помощью PreloadManagerListener , внедрить лучшие практики, готовые к использованию в производственной среде, такие как совместное использование основных компонентов с ExoPlayer, и освоить шаблон скользящего окна для эффективного управления памятью.
- Часть 3: В заключительной части этой серии мы рассмотрим интеграцию PreloadManager с постоянным дисковым кэшем, что позволит вам сократить потребление данных за счет управления ресурсами и обеспечить бесперебойную работу.
Если вы новичок в предварительной загрузке в Media3, мы настоятельно рекомендуем прочитать Часть 1, прежде чем продолжить. А для тех, кто готов выйти за рамки основ, давайте рассмотрим, как улучшить реализацию воспроизведения мультимедиа.
Отслеживание: Получение аналитики с помощью PreloadManagerListener
When you want to launch a feature in production, as an app developer you also want to understand and capture the analytics behind it. How can you be certain that your preloading strategy is effective in a real-world environment? Answering this requires data on success rates, failures, and performance. The PreloadManagerListener interface is the primary mechanism for gathering this data.
Объект PreloadManagerListener предоставляет две важные функции обратного вызова, которые позволяют получить критически важную информацию о процессе и состоянии предварительной загрузки.
- onCompleted (MediaItem mediaItem) : Этот обратный вызов вызывается после успешного завершения запроса предварительной загрузки, как определено в вашем TargetPreloadStatusControl.
- onError (ошибка PreloadException) : Этот коллбэк может быть полезен для отладки и мониторинга. Он вызывается при сбое предварительной загрузки, предоставляя соответствующее исключение.
Зарегистрировать слушатель можно с помощью одного вызова метода, как показано в следующем примере кода:
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)Извлечение полезной информации из уст слушателя.
Эти обработчики событий можно подключить к вашему аналитическому конвейеру. Пересылая эти события в вашу аналитическую систему, вы можете ответить на ключевые вопросы, такие как:
- Каков наш показатель успешности предварительной загрузки? (отношение количества завершенных событий к общему числу попыток предварительной загрузки)
- Какие CDN или видеоформаты демонстрируют самый высокий уровень ошибок? (По результатам анализа исключений из функции onError)
- Каков наш показатель ошибок предварительной загрузки? (отношение числа событий onError к общему числу попыток предварительной загрузки)
Эти данные могут предоставить вам количественную обратную связь по вашей стратегии предварительной загрузки, что позволит проводить A/B-тестирование и вносить улучшения в пользовательский опыт на основе данных. Кроме того, эти данные помогут вам более точно настроить продолжительность предварительной загрузки , количество загружаемых видео и выделяемые буферы.
Помимо отладки: использование onError для корректного резервного варианта в пользовательском интерфейсе.
Неудачная предварительная загрузка — это явный признак предстоящей буферизации для пользователя. Коллбэк onError позволяет реагировать на это автоматически. Вместо простого логирования ошибки, вы можете адаптировать пользовательский интерфейс. Например, если предварительная загрузка предстоящего видео не удалась, ваше приложение может отключить автовоспроизведение для следующего свайпа, требуя от пользователя нажатия для начала воспроизведения.
Additionally, by inspecting the PreloadException type you can define a more intelligent retry strategy. An app can choose to immediately remove a failing source from the manager based on the error message or HTTP status code. The item would need to be removed from the UI stream accordingly to not make loading issues leak into the user experience. You could also get more granular data from PreloadException like the HttpDataSourceException to probe further into the errors. Read more about ExoPlayer troubleshooting .
Система взаимопомощи: зачем необходимо делиться компонентами с ExoPlayer?
The DefaultPreloadManager and ExoPlayer are designed to work together. To ensure stability and efficiency, they must share several core components . If they operate with separate, uncoordinated components, it could impact thread safety and usability of preloaded tracks on the player since we need to ensure that preloaded tracks should be played on the correct player. The separate components could also compete for limited resources like network bandwidth and memory, which could lead to performance degradation. An important part of the lifecycle is handling appropriate disposal, the recommended order of disposal is to release the PreloadManager first, followed by the ExoPlayer.
Компонент DefaultPreloadManager.Builder предназначен для упрощения такого совместного использования и имеет API для создания экземпляров как PreloadManager, так и связанного экземпляра проигрывателя. Давайте посмотрим, почему такие компоненты, как BandwidthMeter, LoadControl, TrackSelector, Looper, должны использоваться совместно. Ознакомьтесь с визуальным представлением того, как эти компоненты взаимодействуют с ExoPlayer Playback.

Предотвращение конфликтов полосы пропускания с помощью общего счетчика полосы пропускания.
The BandwidthMeter provides an estimate of available network bandwidth based on historical transfer rates. If the PreloadManager and the player use separate instances, they are unaware of each other's network activity, which can lead to failure scenarios. For example, consider the scenario where a user is watching a video, their network connection degrades, and the preloading MediaSource simultaneously initiates an aggressive download for a future video. The preloading MediaSource's activity would consume bandwidth needed by the active player, causing the current video to stall. A stall during playback is a significant user experience failure.
Благодаря использованию единого индикатора пропускной способности (BandwidthMeter), TrackSelector может выбирать треки наилучшего качества с учетом текущих сетевых условий и состояния буфера, как во время предварительной загрузки, так и во время воспроизведения. Затем он может принимать интеллектуальные решения для защиты активной сессии воспроизведения и обеспечения бесперебойной работы.
preloadManagerBuilder.setBandwidthMeter(customBandwidthMeter)
Обеспечение согласованности с общими компонентами LoadControl, TrackSelector и Renderer в ExoPlayer.
- LoadControl : This component dictates buffering policy, such as how much data to buffer before starting playback and when to start or stop loading more data. Sharing LoadControl ensures that the memory consumption of player and PreloadManager is guided by a single, coordinated buffering strategy across both preloaded and actively playing media, preventing resource contention. You will have to smartly allocate buffer size coordinating with how many items you are preloading and with what duration, to ensure consistency. In times of contention, the player will prioritize playback of the current item displayed on the screen. With a shared LoadControl, the preload manager will continue preloading as long as the target buffer bytes allocated for preloading hasn't reached the upper limit, it doesn't wait until the loading for playback is done.
Примечание: Совместное использование LoadControl в последней версии Media3 (1.8) гарантирует корректное совместное использование его Allocator с PreloadManager и плеером. Использование LoadControl для эффективного управления предварительной загрузкой — это функция, которая будет доступна в предстоящем релизе Media3 1.9.
preloadManagerBuilder.setLoadControl(customLoadControl)
- TrackSelector : This component is responsible for selecting which tracks (for example, video of a certain resolution, audio in a specific language) to load and play. Sharing ensures that the tracks selected during preloading are the same ones the player will use. This avoids a wasteful scenario where a 480p video track is preloaded, only for the player to immediately discard it and fetch a 720p track upon playback.< br /> The preload manager should NOT share the same instance of TrackSelector with the player. Instead, they should use the different TrackSelector instance but of the same implementation . That's why we set the TrackSelectorFactory rather than a TrackSelector in the DefaultPreloadManager.Builder.
preloadManagerBuilder.setTrackSelectorFactory(customTrackSelectorFactory)
- Рендерер : Этот компонент отвечает за понимание возможностей проигрывателя без создания полных рендереров. Он проверяет этот шаблон, чтобы определить, какие видео, аудио и текстовые форматы будет поддерживать конечный проигрыватель. Это позволяет ему интеллектуально выбирать и загружать только совместимые медиафайлы и предотвращает трату полосы пропускания на контент, который проигрыватель фактически не может воспроизвести.
preloadManagerBuilder.setRenderersFactory(customRenderersFactory)
Узнайте больше о компонентах Exoplayer .
Золотое правило: универсальный лупер воспроизведения , который подойдет для всех.
The thread on which an ExoPlayer instance can be accessed can be explicitly specified by passing a Looper when creating the player. The Looper of the thread from which the player must be accessed can be queried using Player.getApplicationLooper . By maintaining a shared Looper between the player and PreloadManager, it is guaranteed that all operations on these shared media objects are serialized onto a single thread's message queue. This can reduce the concurrency bugs.
Все взаимодействия между PreloadManager и плеером с медиаисточниками, которые необходимо загрузить или предварительно загрузить, должны происходить в одном и том же потоке воспроизведения. Совместное использование Looper является обязательным условием потокобезопасности, поэтому мы должны совместно использовать PlaybackLooper между PreloadManager и плеером.
The PreloadManager prepares a stateful MediaSource object in the background. When your UI code calls player.setMediaSource(mediaSource), you are performing a handoff of this complex, stateful object from the preloading MediaSource to the player. In this scenario, the entire PreloadMediaSource is moved from the manager to the player. All these interactions and handoffs should occur on the same 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()Tip: If you use the Default components in ExoPlayer like the DefaultLoadControl , etc, you don't need to explicitly share them with DefaultPreloadManager. When you build your ExoPlayer instance via the buildExoPlayer of the DefaultPreloadManager.Builder these components are automatically referenced with each other, if you use the default implementations with default configurations. But if you use custom components or custom configurations, you should explicitly notify the DefaultPreloadManager about them via the above APIs.
Предварительная загрузка, готовая к производству: схема скользящего окна.
In a dynamic feed, a user can scroll through a virtually infinite amount of content. If you continuously add videos to the DefaultPreloadManager without a corresponding removal strategy, you will inevitably cause an OutOfMemoryError. Each preloaded MediaSource holds onto a SampleQueue , which allocates memory buffers. As these accumulate, they can exhaust the application's heap space. The solution is an algorithm you may already be familiar with, called the sliding window. The sliding window pattern maintains a small, manageable set of items in memory that are logically adjacent to the user's current position in the feed. As the user scrolls, this "window" of managed items slides with them, adding new items that come into view, and also removing items that are now distant.

Реализация шаблона скользящего окна
It is essential to understand that PreloadManager does not provide a built-in setWindowSize() method. The sliding window is a design pattern that you, the developer, are responsible for implementing using the primitive add() and remove() methods. Your application logic must connect UI events, such as a scroll or page change, to these API calls. If you want a code reference for this, we have this sliding window pattern implemented in socialite sample which also includes a PreloadManagerWrapper which imitates a sliding window.
Don't forget to add preloadManager.remove(mediaItem) in your implementation when the item is no longer likely to come up soon in the user's viewing. Failing to remove items that are no longer proximate to the user is the primary cause of memory issues in preloading implementations. The remove() call ensures resources are released that help you keep your app's memory usage bound and stable.
Тонкая настройка стратегии предварительной загрузки по категориям с помощью TargetPreloadStatusControl
Теперь, когда мы определили, что именно нужно предварительно загрузить (элементы в нашем окне), мы можем применить четко определенную стратегию для определения объема предварительной загрузки каждого элемента. Мы уже видели, как добиться такой детализации с помощью настройки TargetPreloadStatusControl в части 1 .
Напомним, что элемент, находящийся на позиции +/- 1, может иметь более высокую вероятность быть воспроизведенным, чем элемент на позиции +/- 4. Вы можете выделить больше ресурсов (сети, процессора, памяти) для элементов, которые пользователь, скорее всего, увидит следующим. Это создает стратегию «предварительной загрузки», основанную на близости, что является ключом к балансу между немедленным воспроизведением и эффективным использованием ресурсов.
Как обсуждалось в предыдущих разделах, вы можете использовать аналитические данные через PreloadManagerListener для определения стратегии продолжительности предварительной загрузки.
Заключение и дальнейшие шаги
Теперь вы обладаете необходимыми знаниями для создания быстрых, стабильных и ресурсоэффективных медиапотоков с помощью DefaultPreloadManager от Media3.
Давайте подведем итоги основных выводов:
- Используйте PreloadManagerListener для сбора аналитических данных и реализации надежной обработки ошибок.
- Для создания экземпляров менеджера и игрока всегда используйте один и тот же DefaultPreloadManager.Builder, чтобы обеспечить совместное использование важных компонентов.
- Реализуйте шаблон скользящего окна, активно управляя вызовами add() и remove(), чтобы предотвратить ошибку OutOfMemoryError.
- Используйте TargetPreloadStatusControl для создания интеллектуальной многоуровневой стратегии предварительной загрузки, которая обеспечивает баланс между производительностью и потреблением ресурсов.
Что дальше в части 3: Кэширование с предварительно загруженными медиафайлами
Предварительная загрузка данных в память обеспечивает немедленный прирост производительности, но может сопровождаться компромиссами. После закрытия приложения или удаления предварительно загруженных медиафайлов из менеджера данные исчезают. Для достижения более стабильного уровня оптимизации можно комбинировать предварительную загрузку с дисковым кэшированием. Эта функция находится в активной разработке и появится в ближайшее время, через несколько месяцев.
У вас есть какие-либо отзывы, которыми вы хотели бы поделиться ? Мы будем рады их услышать.
Оставайтесь с нами и ускоряйте воспроизведение видео! 🚀
Новости о продуктахВ современных приложениях, ориентированных на мультимедиа, обеспечение плавного и бесперебойного воспроизведения является ключом к приятному пользовательскому опыту. Пользователи ожидают, что их видео начнут воспроизводиться мгновенно и без пауз.
Mayuri Khinvasara Khabya • Чтение: 8 мин.
Новости о продуктахОбеспечение безопасного взаимодействия в интернете и защита пользователей от вреда являются одними из главных приоритетов Google Play.
Paul Feng • 2 мин чтения
Новости о продуктахСегодня мы официально отмечаем пятилетие с момента выхода Jetpack Compose 1.0. Начиная с версии 1.0, анонсированной 28 июля 2021 года, и заканчивая нашей последней версией 1.11, мы стали свидетелями значительного развития API, и мы хотим отметить это событие.
Rebecca Franks , Nick Butcher , Loryn Hairston • 4 мин чтения
Получайте еженедельно самые свежие новости о разработке Android прямо на свою электронную почту.


