Современные мобильные приложения с пользовательским контентом играют огромную роль в нашей повседневной жизни. Сейчас почти не встретишь людей, особенно подростков, кто не пользовался бы такими сервисами, как TikTok, YouTube, ВКонтакте и соцсетями с картинками. На волне хайпа вертикальных коротких видео многие платформы и соцсети разработали собственные аналоги TikTok и YouTube, где стал популярен такой формат.

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

Меня зовут Рустам и я Android-разработчик в компании SimbirSoft. Рекомендую свою статью тем, кто сталкивается с видеоконтентом на проектах, и кому хотелось бы оптимизировать свою работу с ним. Я рассказал о таких методах на примере стандартных инструментов из библиотеки ExoPlayer, поэтому у middle-разработчиков и более опытных специалистов не должно возникнуть трудностей.

Итак, начнем. В контесте Android-приложений мы с вами рассмотрим способы оптимизации на примере ExoPlayer, как наиболее популярной библиотеки для воспроизведения видео. 

В стандартной ситуации мы создаем экземпляр ExoPlayer, передаем ему нужный MediaSourceFactory и загружаем само видео в качестве MediaItem. С точки зрения UI остается лишь PlayerView установить созданный нами плеер и видео будет готово к показу:

val dataFactory = DefaultDataSource.Factory(this)
        val mediaSourceFactory = HlsMediaSource.Factory(dataFactory)
        val player = ExoPlayer.Builder(this)
            .setMediaSourceFactory(mediaSourceFactory)
            .build()
        val mediaItem = MediaItem.fromUri(videoUrl)
        player.setMediaItem(mediaItem)
        player.prepare()
        binding.playerView.player = player
        player.play()

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

Данную проблему можно решить с использованием сразу нескольких плееров вместо одного:

  • Один для воспроизведения текущего видео.

  • Один для загрузки следующего в списке видео.

  • Опционально, при необходимости: еще один плеер для предыдущего видео.

Но лучше избегать использования чрезмерного количества неконтролируемых плееров, так как это может существенно повлиять на производительность. Число плееров может быть ограничено числом аппаратных декодеров на телефоне.

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

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

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

Для этого необходимо создать экземпляр SimpleCache, который занимается управлением кэша:

val cache = SimpleCache(
            File(this.cacheDir, "media"),
            LeastRecentlyUsedCacheEvictor(10 * 1024 * 1024),
            StandaloneDatabaseProvider(this)
        )

При создании экземпляра есть возможность указать расположение кэша. Также можно указать CacheEvictor с максимальным размером кэша в байтах — LeastRecentlyUsedCacheEvictor или NoOpCacheEvictor. Разница между ними в том, что NoOpCacheEvictor при переполнении кэша больше не кэширует данные, а сохраняет их в памяти, а LeastRecentlyUsedCacheEvictor при переполнении вытесняет наиболее старые данные и кэширует новые.

Для плеера при использовании кэша необходимо применять CacheDataSource: 

val cacheDataSourceFactory = CacheDataSource.Factory()
          .setCache(cache)
          .setUpstreamDataSourceFactory(DefaultHttpDataSource.Factory())

При создании экземпляра нужно передать созданный ранее экземпляр SimpleCache и при необходимости установить upstreamDataSourceFactory, который будет использоваться в случае, если видео отсутствует в кэше.

Остается лишь сделать MediaSoruceFactory и передать туда созданный ранее CacheDataSoruceFacory:

val mediaSourceFactory = HlsMediaSource.Factory(cacheDataSourceFactory)
        val player = ExoPlayer.Builder(this)
            .setMediaSourceFactory(mediaSourceFactory)
            .build()

Так при загрузке видео плеер сначала проверить кэш на наличие этого видео, и, если оно будет отсутствовать в кэше, то загрузить его из сети.

Для того чтобы принудительно закэшировать нужное видео, возьмем созданный ранее экземпляр CacheDataSorce:

suspend fun cacheMedia(mediaUrl: String) {
        val mediaItem = MediaItem.fromUri(mediaUrl)
        with(HlsDownloader(mediaItem, dataSource)) {
            download { contentLength, bytesDownloaded, percentDownloaded ->
                if (bytesDownloaded >= 1 * 1024 * 1024) {
                    cancel()
                }
            }
        }
    }

Так, передав в HlsDownloader MediaItem c нужным видео и CacheDataSoruceFactory, можно запустить кэширование видео с помощью метода download и по необходимости отменить. Например, в коде выше кэшируется 1 мегабайт от всего видео.

Давайте сделаем сравнение для наглядности. Для этого возьмем список из 10 видео и посмотрим время загрузки при использовании одного плеера, одного плеера с использованием кэша, а также кэша и трёх плееров:

По графику видно явное преимущество применения пула плееров. При использовании одного плеера среднее время загрузки составило 1297 миллисекунд, с плеером и кэшем время загрузки значительно меньше — 127 миллисекунд, а с применением трёх плееров и кэша — 104 миллисекунды. Несколько плееров хоть и не влияет на скорость загрузки, но обеспечивает плавность переключения между видео, поскольку фактически при свайпе на следующее видео плеер уже будет готов к воспроизведению.

Вывод

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

Спасибо за внимание!  

Больше полезных материалов для mobile-разработчиков мы также публикуем в наших соцсетях — ВК и Telegram.

Комментарии (0)