Як зробити список із відео-view і навіщо він взагалі потрібен

Усі статті, обговорення, новини про Mobile — в одному місці. Підписуйтеся на телеграм-канал!

Привіт, я Нік, Android-інженер в Uptech Product Studio. Нещодавно моя команда працювала над тим, щоб показувати відео, список відео, вкладені відео та інші подібні речі в нашому поточному проєкті. Показ списку відео виявився найскладнішим. Я витратив багато часу, намагаючись оптимізувати системні ресурси й правильно організувати відтворення. На жаль, пошук рішень в інтернеті не приніс задовільних результатів. Через брак інформації в інтернеті я подумав, що мій досвід пошуку рішення буде цікавим для багатьох Android-інженерів.

У цій статті я покажу, як відтворювати кілька відео за допомогою RecyclerView і ExoPlayer. Ми розглянемо такі основні моменти:

  • управління відтворенням під час прокрутки;
  • управління відтворенням, коли застосунок у фоновому режимі;
  • оптимізація системних ресурсів.

Чому список відео краще, ніж одне відео за раз

Це цілком резонне питання. Дійсно, в більшості випадків користувач звертає увагу на одне відео за раз. Однак одночасне відтворення декількох відео у порівнянні з відтворенням одного за іншим забезпечує кращий UX.

Користувач прокручує стрічку і миттєво бачить весь контент замість того, щоб переходити від одного відео до іншого і чекати, поки почнеться відтворення наступного.

Ми показали коротке циклічне відео, що демонструє одяг на маркетплейсі. Відео забезпечує кращий UX, ніж просто зображення, тому що дає більше інформації про кожен окремий продукт. Крім того, відео демонструє кращу продуктивність у порівнянні з .GIF.

Але не все так просто

Перш за все, створення екземпляра плеєра вимагає часу і ресурсів пристрою. Це може сповільнити роботу нашого списку відео та призвести до менш гнучкого UX. Недбале створення декількох екземплярів плеєра значно вплине на UX і витратить пам’ять пристрою і процесорний час. Крім того, кожен пристрій має обмежену кількість медіакодеків, які відповідають за декодування відео. Після того як усі доступні медіакодеки вичерпаються, застосунок завершить роботу.

Щоб уникнути всіх згаданих проблем, ми використаємо кілька хитрощів:

  • Пул об’єктів.
  • Управління життєвим циклом Activity.
  • Управління прокруткою RecyclerView.

Нижче загальна схема того, як це працює:

Пул плеєрів

Ми будемо повторно використовувати екземпляри плеєрів, щоб уникнути створення нових екземплярів щоразу. Це означає кращу продуктивність і плавніший UX для RecyclerView. Нам допоможе патерн пул об’єктів.

Як тільки view приєднано і відео готове до відтворення, його viewHolder отримує плеєр із пулу. І навпаки, якщо view від’єднано, то viewHolder звільняє плеєр.

VideoAdapter.kt

override fun onViewAttachedToWindow(holder: VideoViewHolder) {
 super.onViewAttachedToWindow(holder)
 holder.attach()
}

override fun onViewDetachedFromWindow(holder: VideoViewHolder) {
 holder.detach()
 super.onViewDetachedFromWindow(holder)
}

Кількість плеєрів, які ми можемо ініціалізувати, обмежена. Якщо кількість перевищити, то програма припинить роботу. Це пов’язано з обмеженим числом відеокодеків з апаратним прискоренням (яке залежить від пристрою). Ми впровадимо обмеження кількості екземплярів плеєра на рівні пулу. Якщо всі плеєри в пулі використовуються, то кожен наступний viewHolder перебуватиме в стані очікування плеєра. ViewHolder, що очікує, додається в чергу пулу плеєрів. Він отримає плеєр, як тільки в пулі звільниться плеєр, який уже використовується. Тоді ViewHolder може прив’язати плеєр до View і негайно почати відтворення.

Звільнення невикористаних ресурсів

Існує два випадки, коли плеєр може бути звільнений з метою економії ресурсів:

  • відео прокручується з RecyclerView і більше не показується;
  • Activity-контейнер більше не знаходиться на передньому плані.

У цих випадках користувач не бачить view з відео, тому відповідні плеєри можуть бути зупинені або звільнені. Тим самим ми даємо можливість іншому застосунку використовувати наявні медіакодеки.

Перший випадок я описав вище.

Тепер розгляньмо, як звільнити ресурси плеєра, коли застосунок згорнуто.

Примітка: враховуйте підтримку багатовіконності, починаючи з 24 версії API. У режимі розділеного вікна застосунок може бути видимим, але не активним, тому ініціалізувати/звільняти плеєр ми маємо в onStart/onStop відповідно.

override fun onPause() {
 if (Util.SDK_INT <= 23)
   viewModel.releasePlayers()
 super.onPause()
}

override fun onStop() {
 if (Util.SDK_INT > 23)
   viewModel.releasePlayers()
 super.onStop()
}

Після звільнення плеєра функцією player.release() його вже не можна використати знову. Тому немає сенсу тримати вільні плеєри у пулі.

Після того як користувач повернувся у застосунок, ми повинні повідомити про це ViewHolder-и і перевести їх в режим програвання.

MainActivity.kt

override fun onStart() {
 super.onStart()
 if (Util.SDK_INT > 23)
   viewModel.restartPlayers()
}

override fun onResume() {
 super.onResume()
 if (Util.SDK_INT <= 23)
   viewModel.restartPlayers()
}

restartPlayers() змушує ViewHolders отримати плеєри і запустити їх.

VideoAdapter.VideoViewHolder.kt

restartJob = playersActions
 .onEach { action ->
   when(action) {
     RELEASE -> with(binding.playerView) {
       player?.run {
         release()
         playersPool.release(this)
       }
       player = null
     }
     RESTART -> {
       bindPlayer(
         videoUrls[absoluteAdapterPosition],
         playbackPositions[absoluteAdapterPosition]
       )
     }
   }
 }.launchIn(lifecycleScope)

Ви також можете зберегти стан відтворення безпосередньо перед тим, як звільнити плеєр. Таким чином, користувач може продовжити з того ж місця, коли відео або знову приєднається до RecyclerView, або Activity-контейнер вийде на передній план.

Обробка подій життєвого циклу

Я використав корутіни для спрощення обробки подій у ViewHolder. Існує два види подій, що вимагають вивільнення ресурсів плеєра:

  • згортання застосунку (вихід з Activity);
  • прокрутка view за межі вікна.

Щоб все працювало коректно і не ставалося витоків пам’яті, ці події мають оброблятися у спеціальних CoroutineScopes:

  • LifecycleScope використовується для звільнення/перезапуску всіх плеєрів. Він прив’язаний до методів життєвого циклу activities’ onCreate()/onDestroy(), що означає, що всі події, які ми видаємо в onStart(), onResume(), onPause(), onStop(), будуть оброблятися ViewHolder’ом.
  • VideoScope специфічний для кожного окремого ViewHolder. Він запускається одразу після того, як конкретний View стає видимим (onViewAttachedToWindow()) і скасовується після того, як View проскролюється за межі екрана (onViewDetachedFromScreen()). VideoScope може бути інстанційований та скасований декілька разів для того самого ViewHolder протягом життєвого циклу Activity (залежить від того, наскільки активно користувач прокручує RecyclerView). Цей CoroutineScope відповідає за отримання плеєрів із пулу та запуск відтворення відео.

Структура

ULM-діаграма нижче демонструє структуру проєкту:

Бонус

Для кожного формату відео на пристрої є обмежена кількість кодеків. Вони бувають апаратно прискорені та програмні. Рекомендується використовувати апаратно прискорені кодеки. На сьогодні у більшості пристроїв доступно 16+ екземплярів кодеків для більшості форматів. Але зазвичай програмі не потрібно стільки кодеків одночасно. Тому ви можете задати розмір пулу плеєрів залежно від ваших потреб. (Наприклад, у прикладі програми я використовую 4 екземпляри плеєра). Ось фрагмент, щоб дізнатися кількість кодеків для .mp4.

fun availableCodecsNum(): Int =
 listOf(MimeTypes.VIDEO_MP4, MimeTypes.VIDEO_H264, MimeTypes.VIDEO_MP4V).map { mimeType ->
   MediaCodecUtil.getDecoderInfos(mimeType, false, false)
 }.flatten()
   .filter { codecInfo -> codecInfo.hardwareAccelerated }
   .map { it.maxSupportedInstances }
   .reduce { acc, maxInstances -> acc + maxInstances }

Висновок

Зрештою, реалізація списку відео-view більше не здається такою складною. Усе працюватиме як годинник, якщо оптимізувати використання системних ресурсів, обробляючи події життєвого циклу Activity та врахувати системні обмеження. Повний код проєкту доступний на нашому GitHub.

👍ПодобаєтьсяСподобалось10
До обраногоВ обраному3
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Підписатись на коментарі