Всем привет.
Пару дней назад появилась задача переключать WebVTT субтитры в HLS потоке.
Играем видео мы с помощью ExoPlayer и по началу казалось, что Гугл и Ко должны были бы предоставить решение из коробки вида «взял и сделал». Но реальность не совпала с ожиданием:)
Гуглинг и Хабринг не привели к результату и всё сводилось к тому, чтобы ковырять официальное демо приложение ExoPlayer.
Т.к. статья предполагает некое знакомство со структурой HLS и наличие какого-то опыта в ExoPlayer, то перейдём сразу к делу.
Вот что говорит нам документация о переключении потоков(видео, аудио, субтитры). А говорит она нам почти ничего — инициализируйте плеер с помощью DefaultTrackSelector и используйте его. Всё, ну ок :)
Все субтитры любого вида(CEA-608, WebVtt) как и любые другие дорожки хранятся внутри DefaultTrackSelector и нужно уметь до них добраться. Всё делится на группы и подгруппы, и если кратко, то внутренняя структура выглядит примерно следующим образом:

Попробуем теперь получить субтитры типа WebVTT, они должны хранится внутри Renderer c типом «3»(C.TRACK_TYPE_TEXT — константа в Exo):
Как можно заметить каких-то удобных способов итерирования и поиска не предоставляется и приходится ручками делать все циклы.
Но нам же надо знать не только о самом списке, но ещё и о выбранных вариантах(которые мы будем выбирать в будущем). По какой-то причине получить выбранные треки из DefaultTrackSelector нельзя, зато можно у самого ExoPlayer. Схема получения примерно такая же, идём вглубь, но тут мы пропускаем проход по рендерерам:
Отлично, имеем общий список и список выбранных. Как объединить и отображать на UI — не цель этой статьи. Способов много. Нам же осталось научиться устанавливать дорожку.
Для наглядности сделаем это способом в лоб, чтобы были очевидны все циклы и индексы.
Установка дорожку по её языковому коду(«ru», «en», ...):
Мы рассмотрели основы того, как получать и переключать WebVTT субтитры в ExoPlayer'e.
В реальности этим же способом можно легко работать и с другими типами субтитров — достаточно параметризировать методы типом. Таким же образом не составит труда работа и с аудио дорожками. Конечно, именно в таком виде использовать решение довольно сложно и требуется приводить к более удобному виду.
Спасибо за внимание.
Пару дней назад появилась задача переключать WebVTT субтитры в HLS потоке.
Играем видео мы с помощью ExoPlayer и по началу казалось, что Гугл и Ко должны были бы предоставить решение из коробки вида «взял и сделал». Но реальность не совпала с ожиданием:)
Гуглинг и Хабринг не привели к результату и всё сводилось к тому, чтобы ковырять официальное демо приложение ExoPlayer.
Т.к. статья предполагает некое знакомство со структурой HLS и наличие какого-то опыта в ExoPlayer, то перейдём сразу к делу.
Вот что говорит нам документация о переключении потоков(видео, аудио, субтитры). А говорит она нам почти ничего — инициализируйте плеер с помощью DefaultTrackSelector и используйте его. Всё, ну ок :)
Все субтитры любого вида(CEA-608, WebVtt) как и любые другие дорожки хранятся внутри DefaultTrackSelector и нужно уметь до них добраться. Всё делится на группы и подгруппы, и если кратко, то внутренняя структура выглядит примерно следующим образом:

Попробуем теперь получить субтитры типа WebVTT, они должны хранится внутри Renderer c типом «3»(C.TRACK_TYPE_TEXT — константа в Exo):
fun getVttSubtitles(): List<String> { val tracks = mutableListOf<String>() // берём MappedTrackInfo defaultTrackSelector.currentMappedTrackInfo?.let { mappedTrackInfo -> val renderCount = mappedTrackInfo.rendererCount for (renderIndex in 0 until renderCount) { // проходим по всем renderer'ам и ищем тип TEXT val renderType = mappedTrackInfo.getRendererType(renderIndex) if (renderType == C.TRACK_TYPE_TEXT) { val trackGroupArray = mappedTrackInfo.getTrackGroups(renderIndex) // проходим по всем подгруппам for (trackGroupArrayIndex in 0 until trackGroupArray.length) { val trackGroup = trackGroupArray[trackGroupArrayIndex] for (trackGroupIndex in 0 until trackGroup.length) { // проходим по всем трекам с форматом TEXT_VTT val format = trackGroup.getFormat(trackGroupIndex) if (format.sampleMimeType == MimeTypes.TEXT_VTT) { tracks += format.language.orEmpty() } } } } } } return tracks }
Как можно заметить каких-то удобных способов итерирования и поиска не предоставляется и приходится ручками делать все циклы.
Но нам же надо знать не только о самом списке, но ещё и о выбранных вариантах(которые мы будем выбирать в будущем). По какой-то причине получить выбранные треки из DefaultTrackSelector нельзя, зато можно у самого ExoPlayer. Схема получения примерно такая же, идём вглубь, но тут мы пропускаем проход по рендерерам:
fun getSelectedVttSubtitles(): List<String> { val selectedLangs = mutableListOf<String>() val currentTrackSelections = exoPlayer.currentTrackSelections for (selectionIndex in 0 until currentTrackSelections.length) { val trackSelection = currentTrackSelections[selectionIndex] if (trackSelection != null) { // проходим по все выбранным группам val length = trackSelection.length() for (trackIndex in 0 until length) { // ищем все выбранные треки нужного формата val format = trackSelection.getFormat(trackIndex) if (format.sampleMimeType == MimeTypes.TEXT_VTT) { selectedLangs += format.language.orEmpty() } } } } return selectedLangs }
Отлично, имеем общий список и список выбранных. Как объединить и отображать на UI — не цель этой статьи. Способов много. Нам же осталось научиться устанавливать дорожку.
Для наглядности сделаем это способом в лоб, чтобы были очевидны все циклы и индексы.
Установка дорожку по её языковому коду(«ru», «en», ...):
fun selectTrackByIsoCodeAndType(langCode: String) { defaultTrackSelector.currentMappedTrackInfo?.let { mappedTrackInfo -> // начало такое же как и в получении списка субтитров val renderCount = mappedTrackInfo.rendererCount for (renderIndex in 0 until renderCount) { // проходим по всем renderer'ам и ищем тип TEXT val renderType = mappedTrackInfo.getRendererType(renderIndex) if (renderType == C.TRACK_TYPE_TEXT) { val trackGroupArray = mappedTrackInfo.getTrackGroups(renderIndex) // проходим по всем подгруппам for (trackGroupArrayIndex in 0 until trackGroupArray.length) { val trackGroup = trackGroupArray[trackGroupArrayIndex] for (trackGroupIndex in 0 until trackGroup.length) { val format = trackGroup.getFormat(trackGroupIndex) // находим наш формат с нужным языковым кодом if (format.sampleMimeType == MimeTypes.TEXT_VTT && format.language == langCode ) { // копируем текущее состояние треков val currentParams = defaultTrackSelector.buildUponParameters() // очищаем предыдущие установки(если были) // в рендерере currentParams.clearSelectionOverride( renderIndex, trackGroupArray ) // сообщаем, что именно хотим выбрать // самое сложное здесь не запутаться в индексах // указываем какой трек в какой группе выбираем val override = DefaultTrackSelector.SelectionOverride( trackGroupArrayIndex, trackGroupIndex ) // указываем в каком рендерере установка currentParams.setSelectionOverride( renderIndex, trackGroupArray, override ) // устанавливаем новые параметры в селектор defaultTrackSelector.setParameters(currentParams) return } } } } } } }
Заключение
Мы рассмотрели основы того, как получать и переключать WebVTT субтитры в ExoPlayer'e.
В реальности этим же способом можно легко работать и с другими типами субтитров — достаточно параметризировать методы типом. Таким же образом не составит труда работа и с аудио дорожками. Конечно, именно в таком виде использовать решение довольно сложно и требуется приводить к более удобному виду.
Спасибо за внимание.