Auto-select VLC subtitle tracks
Dreamio now asks MobileVLCKit to enable the first real subtitle track when VLC discovers embedded MKV subtitles and playback is still on “Disable.” This targets the log pattern where VLC sees English (SDH) but leaves selected=-1.
Summary
Fixed the remaining native playback subtitle issue for streams that provide no external subtitle candidates but do contain embedded subtitle tracks. VLC can discover those tracks after playback starts; Dreamio now auto-selects the first selectable one once it appears.
Changes Made
- Added per-playback state to track whether Dreamio has already auto-selected subtitles.
- Added per-playback state to detect when the user manually chooses a caption option, including disabling captions.
- Auto-selects the first subtitle track with a non-negative VLC track id when VLC reports
.esAddedor after an external subtitle attach refresh. - Added a debug log line for automatic subtitle selection so future device logs should show the selected track id and reason.
Context
The reported logs showed subtitle candidates=0 from the native player, followed by VLC reporting subtitle tracks named Disable and English (SDH) - [English] with selected track -1. That means stream parsing and VLC track discovery were both working; the remaining gap was that Dreamio never changed VLC away from the disabled subtitle track.
Important Implementation Details
- The selection guard only runs when
currentVideoSubTitleIndexis below zero, so it will not replace an already active subtitle track. - The auto-selection runs only once per playback item.
- User interaction wins: once
selectSubtitleTrack(id:)is called from the captions menu, Dreamio stops automatic caption selection for that playback item. - The state resets in
play(request:), alongside the existing attached subtitle URL reset.
Relevant Diff Snippets
Rendered with @pierre/diffs/ssr.
22 unmodified lines23242526272812 unmodified lines41424344454653 unmodified lines100101102103104105133 unmodified lines2392402412422432449 unmodified lines25425525625725825912 unmodified lines27227327427527627722 unmodified linesprivate let mediaPlayer = VLCMediaPlayer()#endifprivate var attachedSubtitleURLs = Set<URL>()override init() {super.init()12 unmodified linesfunc play(request: NativePlaybackRequest) {#if canImport(MobileVLCKit)attachedSubtitleURLs.removeAll()let media = VLCMedia(url: request.playbackURL)let headerValue = request.headers.map { "\($0.key): \($0.value)" }53 unmodified linesfunc selectSubtitleTrack(id: Int32) {#if canImport(MobileVLCKit)#if DEBUGlogSubtitleTracks(reason: "before-select-\(id)")#endif133 unmodified linesreturn attachedCount}DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak self] in#if DEBUGself?.logSubtitleTracks(reason: "delayed-refresh")#endif9 unmodified linesprint("[DreamioVLC] subtitle tracks reason=\(reason) names=\(names) indexes=\(indexes.map { $0.int32Value }) selected=\(mediaPlayer.currentVideoSubTitleIndex)")}#endif#endif}12 unmodified linescase .paused, .stopped, .ended:onStateChange?()case .esAdded:#if DEBUGlogSubtitleTracks(reason: "esAdded")#endif22 unmodified lines232425262728293012 unmodified lines434445464748495053 unmodified lines104105106107108109110133 unmodified lines2442452462472482492509 unmodified lines26026126226326426526626726826927027127227327427527627727827928012 unmodified lines29329429529629729829922 unmodified linesprivate let mediaPlayer = VLCMediaPlayer()#endifprivate var attachedSubtitleURLs = Set<URL>()private var didAutoSelectSubtitleTrack = falseprivate var didUserSelectSubtitleTrack = falseoverride init() {super.init()12 unmodified linesfunc play(request: NativePlaybackRequest) {#if canImport(MobileVLCKit)attachedSubtitleURLs.removeAll()didAutoSelectSubtitleTrack = falsedidUserSelectSubtitleTrack = falselet media = VLCMedia(url: request.playbackURL)let headerValue = request.headers.map { "\($0.key): \($0.value)" }53 unmodified linesfunc selectSubtitleTrack(id: Int32) {#if canImport(MobileVLCKit)didUserSelectSubtitleTrack = true#if DEBUGlogSubtitleTracks(reason: "before-select-\(id)")#endif133 unmodified linesreturn attachedCount}DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak self] inself?.selectInitialSubtitleTrackIfNeeded(reason: "delayed-refresh")#if DEBUGself?.logSubtitleTracks(reason: "delayed-refresh")#endif9 unmodified linesprint("[DreamioVLC] subtitle tracks reason=\(reason) names=\(names) indexes=\(indexes.map { $0.int32Value }) selected=\(mediaPlayer.currentVideoSubTitleIndex)")}#endifprivate func selectInitialSubtitleTrackIfNeeded(reason: String) {guard !didUserSelectSubtitleTrack,!didAutoSelectSubtitleTrack,mediaPlayer.currentVideoSubTitleIndex < 0,let track = subtitleTracks.first(where: { $0.id >= 0 }) else {return}didAutoSelectSubtitleTrack = true#if DEBUGprint("[DreamioVLC] auto-select subtitle id=\(track.id) name=\(track.name) reason=\(reason)")#endifmediaPlayer.currentVideoSubTitleIndex = track.id}#endif}12 unmodified linescase .paused, .stopped, .ended:onStateChange?()case .esAdded:selectInitialSubtitleTrackIfNeeded(reason: "esAdded")#if DEBUGlogSubtitleTracks(reason: "esAdded")#endif
Expected Impact for End-Users
MKV streams with embedded subtitles should show captions automatically instead of requiring the user to open the captions menu and pick the embedded track manually. Users can still disable captions or switch tracks afterward.
Validation
- Ran
xcodebuild -scheme Dreamio -project Dreamio.xcodeproj -destination 'generic/platform=iOS' build. - The build succeeded against MobileVLCKit.
Issues, Limitations, and Mitigations
- This was validated by build, not by replaying the exact Real-Debrid stream on device in this turn.
- If a file has multiple embedded subtitle tracks, Dreamio chooses the first selectable track VLC exposes. The captions menu remains available for manual switching.
- If a stream intentionally starts with subtitles disabled and the user never touches the captions menu, Dreamio will now enable the first discovered track by default.
Follow-up Work
- Test the same South Park MKV on device and confirm the logs include
[DreamioVLC] auto-select subtitlefollowed by a non-negative selected subtitle id. - Consider a future setting for “auto-enable embedded subtitles” if users want control over the default behavior.
New Changes as of May 25, 2026 at 10:45 AM EDT
Summary of changes: Added a timed re-apply loop for the automatically selected VLC subtitle track. Dreamio now remembers the auto-selected track id and re-sends it after short delays and when VLC reports buffering or playing.
Why this change was made: Follow-up device logs showed VLC selected track 3, but subtitles still did not render. That points to a MobileVLCKit timing issue after elementary stream discovery, so the selected embedded track is re-applied after playback settles.
Code diffs:
24 unmodified lines25262728293014 unmodified lines45464748495054 unmodified lines105106107108109110159 unmodified lines2702712722732742752762772782796 unmodified lines28628728828929029124 unmodified linesprivate var attachedSubtitleURLs = Set<URL>()private var didAutoSelectSubtitleTrack = falseprivate var didUserSelectSubtitleTrack = falseoverride init() {super.init()14 unmodified linesattachedSubtitleURLs.removeAll()didAutoSelectSubtitleTrack = falsedidUserSelectSubtitleTrack = falselet media = VLCMedia(url: request.playbackURL)let headerValue = request.headers.map { "\($0.key): \($0.value)" }54 unmodified linesfunc selectSubtitleTrack(id: Int32) {#if canImport(MobileVLCKit)didUserSelectSubtitleTrack = true#if DEBUGlogSubtitleTracks(reason: "before-select-\(id)")#endif159 unmodified lines}didAutoSelectSubtitleTrack = true#if DEBUGprint("[DreamioVLC] auto-select subtitle id=\(track.id) name=\(track.name) reason=\(reason)")#endifmediaPlayer.currentVideoSubTitleIndex = track.id}#endif}6 unmodified lines#endifswitch mediaPlayer.state {case .buffering, .playing:onReady?()onStateChange?()case .error:24 unmodified lines2526272829303114 unmodified lines4647484950515254 unmodified lines107108109110111112113159 unmodified lines2732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043056 unmodified lines31231331431531631731824 unmodified linesprivate var attachedSubtitleURLs = Set<URL>()private var didAutoSelectSubtitleTrack = falseprivate var didUserSelectSubtitleTrack = falseprivate var autoSelectedSubtitleTrackID: Int32?override init() {super.init()14 unmodified linesattachedSubtitleURLs.removeAll()didAutoSelectSubtitleTrack = falsedidUserSelectSubtitleTrack = falseautoSelectedSubtitleTrackID = nillet media = VLCMedia(url: request.playbackURL)let headerValue = request.headers.map { "\($0.key): \($0.value)" }54 unmodified linesfunc selectSubtitleTrack(id: Int32) {#if canImport(MobileVLCKit)didUserSelectSubtitleTrack = trueautoSelectedSubtitleTrackID = nil#if DEBUGlogSubtitleTracks(reason: "before-select-\(id)")#endif159 unmodified lines}didAutoSelectSubtitleTrack = trueautoSelectedSubtitleTrackID = track.id#if DEBUGprint("[DreamioVLC] auto-select subtitle id=\(track.id) name=\(track.name) reason=\(reason)")#endifmediaPlayer.currentVideoSubTitleIndex = track.idscheduleAutoSubtitleSelectionReapply(trackID: track.id)}private func scheduleAutoSubtitleSelectionReapply(trackID: Int32) {[0.3, 1.0, 2.0, 4.0].forEach { delay inDispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] inself?.reapplyAutoSelectedSubtitleTrackIfNeeded(reason: "delayed-\(String(format: "%.1f", delay))")}}}private func reapplyAutoSelectedSubtitleTrackIfNeeded(reason: String) {guard !didUserSelectSubtitleTrack,let trackID = autoSelectedSubtitleTrackID,subtitleTracks.contains(where: { $0.id == trackID }) else {return}mediaPlayer.currentVideoSubTitleIndex = trackID#if DEBUGprint("[DreamioVLC] reapply subtitle id=\(trackID) reason=\(reason) selected=\(mediaPlayer.currentVideoSubTitleIndex)")#endif}#endif}6 unmodified lines#endifswitch mediaPlayer.state {case .buffering, .playing:reapplyAutoSelectedSubtitleTrackIfNeeded(reason: stateName(mediaPlayer.state))onReady?()onStateChange?()case .error:
Related issues or PRs: Beads issue dreamio-ppj.