Add external subtitles in Native Android SDK after load Source

Hello guys, I’m implementing the player on native Android (Java), but I need to include external subtitles with WebVTT format.

I do this to include the caption, after loading the Source:

SubtitleTrack subtitleTrack = new SubtitleTrack(url, label, subtitleId, true, language);
Source playerSource = player.getSource();
playerSource.getConfig().addSubtitleTrack(subtitleTrack);

This doesn’t return any errors.

Then I do this to fetch the available subtitles:

List<SubtitleTrack> sutitleTracks = player.getSource().getAvailableSubtitleTracks();
log(sutitleTracks.toString());
log(sutitleTracks.get(0).getId());

And I have this return, the caption that I included does not appear:

D/BitmovinPlayer: [com.bitmovin.player.api.media.subtitle.SubtitleTrack@c82ec76f]
D/BitmovinPlayer: 16727/8219

At one point I managed to include the caption if I insert it before loading the Source. Using the SourceConfig function.

The question would be, how do I include the caption after loading?

Thanks

Hi Nicolas
modifying the source configurations after loading it into the player is unfortunately not supported.

We will further improve our documentation/feedback in such cases in the future to make this easier to spot .

So in short: Captions have to be added before loading. They can be activated/deactivated using setSubtitleTrack if needed.
I hope this helps, feel free to reach out if there are any other questions.

Hi @Lukas, it worked out. I can insert the subtitles before loading the font and then selecting it.

I’m having problem now on iOS (Swift), can you help me with this too? I need to display only the subtitles, not the controls screen. I used playerConfig.styleConfig.userInterfaceType = .subtitle but it doesn’t display anything, neither controls nor subtitles. The caption is only showing when the controls appear.

Here’s my code:

//
// Bitmovin Player iOS SDK
// Copyright (C) 2021, Bitmovin GmbH, All Rights Reserved
//
// This source code and its use and distribution, is subject to the terms
// and conditions of the applicable license agreement.
//

import UIKit
import BitmovinPlayer

final class ViewController: UIViewController {
    var player: Player!

    deinit {
        player?.destroy()
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        self.view.backgroundColor = .black

        // Define needed resources
        guard let streamUrl = URL(string: "https://dacastmmod-mmd-cust.lldns.net/e2/7b8f4b53-23aa-13b9-7254-752ccde4bb7c/stream.ismd/manifest.m3u8?stream=e9000dd2-ceb5-075e-96ea-6238b7314688_rendition%3Be69d9e1d-9187-10a0-d367-c605bb7cd7b5_rendition%3B11b9c069-db9d-fa16-8c3b-877e33c98db0_rendition%3B50f76e84-cb48-9f1b-1db2-f01309c08032_rendition&p=90&h=568e5c8a24b6695d1e4a47be280a57f0") else {
            return
        }

        // Create player configuration
        let playerConfig = PlayerConfig()
        playerConfig.styleConfig.userInterfaceType = .subtitle

        // Create player based on player config
        player = PlayerFactory.create(playerConfig: playerConfig)

        // Create player view and pass the player instance to it
        let playerView = PlayerView(player: player, frame: .zero)

        // Listen to player events
        player.add(listener: self)
        

        playerView.autoresizingMask = [.flexibleHeight, .flexibleWidth]
        playerView.frame = view.bounds

        view.addSubview(playerView)
        view.bringSubviewToFront(playerView)

        // Create source config
        let sourceConfig = SourceConfig(url: streamUrl, type: .hls)
        sourceConfig.add(subtitleTrack: SubtitleTrack(url: URL(string: "https://www.nicolasalves.dev.br/legenda.vtt"), label: "Portugues BR", identifier: "pt-br", isDefaultTrack: true, language: "Portuguese"))
        
        
        // Set a poster image
//        sourceConfig.posterSource = posterUrl
        player.load(sourceConfig: sourceConfig)
        player.setSubtitle(trackIdentifier: "pt-br")
        player.play()
    }
}

extension ViewController: PlayerListener {
    func onEvent(_ event: Event, player: Player) {
        dump(event, name: "[Player Event]", maxDepth: 1)
    }
}

Hi @nicolas.alves , great to know that it worked out for Android.

For iOS, side-loaded subtitles are not supported with userInterfaceType = .subtitle. Only in-manifest subtitles are rendered in this mode.

When not using Bitmovin UI, player still downloads and parses the side-loaded subtitles but does not render these. To render side-loaded subtitles, you will need to use default Bitmovin UI OR alternatively, you can render the subtitles using a custom view in your app. For this, application can listen to onCueEnter and onCueExit events. These events carry subtitle text, styling and position information which can be used to render the subtitles.

Additionally, best practise to call player.setSubtitle is after player has reached Loaded state. This can be checked by waiting for Loaded state. Additionally you can also check the available subtitles using player.availableSubtitles API and show these in a custom subtitle selector UI.

Hope above information helps to implement your desired.

Hi @lucky.goyal! Thanks for your help! Do you have any example code using the subtitles with custom ui? It’s just that I need to implement external subtitles but without the standard Bitmovin controls (the controls will be in another part of the app).

Thanks!

Hi @nicolas.alves , unfortunately, we do not have a sample application for custom subtitle rendering.

@lucky.goyal, i managed to do it by projecting a UILabel above the playerView and changing its text according to the onCueEnter, is it like this? But onCueExit is being called several times right after onCueEnter, so it quickly removes the caption from the screen, not waiting for the right time.

What could be happening?

extension BPlayerVideoView: PlayerListener {
    public func onCueEnter(_ event: CueEnterEvent, player: Player) {
        debugPrint("onCueEnter")
        self.subtitleText.text = event.text ?? ""
    }
    
    public func onCueExit(_ event: CueExitEvent, player: Player) {
        debugPrint("onCueExit")
        self.subtitleText.text = nil
    }
}

Apparently onCueExit is doing random loops and returns all subtitles constantly.

public func onCueExit(_ event: CueExitEvent, player: Player) {
        debugPrint(event.text)
    }

Returns:

Optional("<i>Comecei pedindo empréstimos</i>\r\n<i>que não pretendia pagar,</i>")
Optional("<i>de uma forma que os bancos</i>\r\n<i>não podiam detectar</i>")
Optional("<i>que eu colecionava dívidas.</i>")
Optional("<i>Eu queria criar alternativas</i>\r\n<i>para os movimentos sociais,</i>")
Optional("<i>para o sistema político</i>\r\n<i>e econômico</i>")
Optional("<i>que leva à destruição do planeta</i>")
Optional("<i>e às desigualdades sociais.</i>")
Optional("ANNA: VOCÊ ESTÁ ESCONDIDO HÁ 9 ANOS.\r\nFARIA ISSO DE NOVO?")
Optional("ENRIC: EU FARIA ALGO AINDA MAIOR!")
Optional("ENRIC: ENCONTRE-ME EM ****,\r\nMAS NÃO LEVE O CELULAR.")
Optional("ROBIN HOOD DOS BANCOS")
Optional("UM DOCUMENTÁRIO\r\nDE ANNA GIRALT GRIS")

Hi @nicolas.alves , can you please print startTime, endTime and text fields in both onCueEnter and onCueExit? Additionally please share the subtitle URL so that we can reproduce the same behaviour on our side.

Hi @lucky.goyal, apparently the onCueExit is in cluttered loops as the first onCueEnter appears after several onCueExit :frowning:

Subtitle URL: https://www.nicolasalves.dev.br/legenda.vtt

extension BPlayerVideoView: PlayerListener {
    public func onCueEnter(_ event: CueEnterEvent, player: Player) {
        debugPrint("onCueEnter: startTime \(event.startTime) | endTime: \(event.endTime) | Text: \(event.text!)")
        self.subtitleText.text = event.text ?? ""
    }
    
    public func onCueExit(_ event: CueExitEvent, player: Player) {
        debugPrint("onCueExit: startTime \(event.startTime) | endTime: \(event.endTime) | Text: \(event.text!)")
    }
}

Returns:

"onCueExit: startTime 89.006 | endTime: 91.049 | Text: ENRIC: EU FARIA ALGO AINDA MAIOR!"
"onCueExit: startTime 92.217 | endTime: 95.804 | Text: ENRIC: ENCONTRE-ME EM ****,\r\nMAS NÃO LEVE O CELULAR."
"onCueExit: startTime 102.747 | endTime: 104.608 | Text: ROBIN HOOD DOS BANCOS"
"onCueExit: startTime 104.69200000000001 | endTime: 106.193 | Text: UM DOCUMENTÁRIO\r\nDE ANNA GIRALT GRIS"
"onCueEnter: startTime 4.92 | endTime: 7.418 | Text: ENRIC ENVIOU\r\nUMA MENSAGEM SECRETA A VOCÊ"
"onCueExit: startTime 8.393 | endTime: 9.461 | Text: ACEITAR"
"onCueExit: startTime 10.185 | endTime: 11.89 | Text: ESTA MENSAGEM SE AUTODESTRUIRÁ\r\nAPÓS SER LIDA"
"onCueExit: startTime 12.905 | endTime: 14.905 | Text: ENRIC: POSSO CONVERSAR.\r\nO QUE VOCÊ QUER SABER?"
"onCueExit: startTime 15.098 | endTime: 17.031 | Text: ANNA: COMO TUDO COMEÇOU?"
"onCueExit: startTime 19.495 | endTime: 21.104 | Text: DINHEIRO GRÁTIS"
"onCueExit: startTime 33.408 | endTime: 34.788 | Text: ROUBO AQUI"
"onCueExit: startTime 34.872 | endTime: 36.703 | Text: Ele é chamado\r\nde \"Robin Hood dos bancos\"."
"onCueExit: startTime 36.828 | endTime: 38.722 | Text: <i>Ele é chamado</i>\r\n<i>de \"Robin Hood dos bancos\".</i>"
"onCueExit: startTime 38.856 | endTime: 40.284 | Text: <i>Ele cometeu fraude</i>"
"onCueExit: startTime 40.377 | endTime: 43.07 | Text: <i>por meio de empresas fictícias</i>\r\n<i>e todo tipo de contratos.</i>"
"onCueExit: startTime 43.168 | endTime: 47.172 | Text: <i>Promotores pediram</i>\r\n<i>um mandado de busca e apreensão.</i>"
"onCueExit: startTime 47.256 | endTime: 50.551 | Text: Enric livre!"
"onCueExit: startTime 51.301 | endTime: 55.222 | Text: <i>Se a vida é tão difícil</i>\r\n<i>para a sociedade,</i>"
"onCueExit: startTime 55.305 | endTime: 59.446 | Text: <i>por que ninguém arrisca</i>\r\n<i>denunciar a situação verdadeira?</i>"
"onCueExit: startTime 60.185 | endTime: 64.189 | Text: <i>Comecei pedindo empréstimos</i>\r\n<i>que não pretendia pagar,</i>"
"onCueExit: startTime 64.606 | endTime: 66.788 | Text: <i>de uma forma que os bancos</i>\r\n<i>não podiam detectar</i>"
"onCueExit: startTime 67.006 | endTime: 68.639 | Text: <i>que eu colecionava dívidas.</i>"

@nicolas.alves , thanks for sharing the logs. I tried the provided subtitle URL using latest Bitmovin player version and can see the events firing in correct order and time. Which version of player are you testing with? Can you please try latest version 3.27.0?

I found that the behaviour that you are observing was fixed in player release 3.25.0. As best practice I will advise to upgrade to latest player version 3.27.0.

@lucky.goyal, that was it! I was on version 3.23.0, I upgraded to 3.27.0 and it worked perfectly.

Thank you so much Lucky!! :grinning: