Всем привет.
Пару дней назад появилась задача переключать 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.
В реальности этим же способом можно легко работать и с другими типами субтитров — достаточно параметризировать методы типом. Таким же образом не составит труда работа и с аудио дорожками. Конечно, именно в таком виде использовать решение довольно сложно и требуется приводить к более удобному виду.
Спасибо за внимание.
Fessmax
Не сталкивались с таким, что если выбирать по типу TRACK_TYPE_TEXT, то получаются 2 группы треков с разными mime-type: application/CEA-608 и text/vtt? Демо плеера воспринимает их как 2 разные дорожки и предлагает между переключаться. Я вижу, что в коде есть условие
но это просто обход «фичи» плеера или под этим есть какое-то объяснение? (Я в своем проекте делал так же, т.к. в свое время не нашел другого решения)agent10 Автор
Как раз такая сейчас ситуация и есть у меня.
Как в примерах выше и указано я фильтрую ещё и по mimeType:
А демо плеера этого не делает, мало того он будет показывать там любые текстовые дорожки(не только субтитры)
agent10 Автор
Как раз наоборот это и фича наверное:)
Как вы их сгруппируете/отфильтруете — на ваше усмотрение под вашу задачу.
Например, у нас бывает задача — показывать из всего списка только русские субтитры vtt, игнорируя остальные..
Fessmax
Да, я понимаю. Но, например, в моем плейлисте только одна дорожка субтитров, а TrackSelector показывает их 2. Я не смог докопаться в какой момент добавляется еще одна «текстовая» дорожка. Просто предполагаю, что это некая информация о всем плейлисте, а не субтитры.
agent10 Автор
У вас HLS? Тогда это странно, с таким не сталкивался. Может быть вам "подебажить" сам HLS плейлист на предмет двух дорожек? Если есть плейлист можете прислать в личку — гляну..