Get and switch WebVTT subtitles in ExoPlayer

Hello everyone.

A couple of days ago there was a task to switch WebVTT subtitles in the HLS stream.
We play the video using ExoPlayer, and at first it seemed that Google and Co. would have to provide a solution from the โ€œtook and doneโ€ box. But the reality did not coincide with the expectation :)

Googling and Habring did not lead to a result and it all came down to picking the official demo application ExoPlayer.

Because Since the article assumes some familiarity with the structure of HLS and the presence of some experience in ExoPlayer, we get right to the point.

This is what the documentation about switching streams (video, audio, subtitles) tells us . And she tells us almost nothing - initialize the player using DefaultTrackSelector and use it. That's it, ok :)

All subtitles of any kind (CEA-608, WebVtt) like any other tracks are stored inside DefaultTrackSelector and you need to be able to get to them. Everything is divided into groups and subgroups, and in short, the internal structure looks something like this:


Now let's try to get subtitles of type WebVTT, they should be stored inside Renderer with type โ€œ3โ€ (C.TRACK_TYPE_TEXT - constant in 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
    }

As you can see, some convenient ways to iterate and search are not provided and you have to do all the loops with pens.

But we need to know not only about the list itself, but also about the selected options (which we will choose in the future). For some reason, you canโ€™t get the selected tracks from DefaultTrackSelector, but you can use ExoPlayer itself. The scheme for getting is approximately the same, we go deeper, but here we skip the passage through the renderers:

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
    }

Well, we have a common list and a list of selected ones. How to combine and display on the UI is not the purpose of this article. There are many ways. We just have to learn how to set the track.

For clarity, we will do this in a forehead way, so that all cycles and indices are obvious.

Installation of a track by its language code ("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
                            }
                        }
                    }
                }
            }
        }
    }

Conclusion


We've covered the basics of how to get and switch WebVTT subtitles in ExoPlayer.
In reality, the same method can easily work with other types of subtitles - it is enough to parameterize the methods with the type. In the same way, it will not be difficult to work with audio tracks. Of course, it is quite difficult to use the solution in this form and it is required to bring it to a more convenient form.

Thank you for the attention.

All Articles