Всем привет.

Пару дней назад появилась задача переключать 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.
В реальности этим же способом можно легко работать и с другими типами субтитров — достаточно параметризировать методы типом. Таким же образом не составит труда работа и с аудио дорожками. Конечно, именно в таком виде использовать решение довольно сложно и требуется приводить к более удобному виду.

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