Auto-select VLC subtitle tracks

Turn document for dreamio-djc, created May 25, 2026.

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

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

Relevant Diff Snippets

Rendered with @pierre/diffs/ssr.

Dreamio/VLCNativePlaybackBackend.swift
+22
22 unmodified lines
23
24
25
26
27
28
12 unmodified lines
41
42
43
44
45
46
53 unmodified lines
100
101
102
103
104
105
133 unmodified lines
239
240
241
242
243
244
9 unmodified lines
254
255
256
257
258
259
12 unmodified lines
272
273
274
275
276
277
22 unmodified lines
private let mediaPlayer = VLCMediaPlayer()
#endif
private var attachedSubtitleURLs = Set<URL>()
override init() {
super.init()
12 unmodified lines
func 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 lines
func selectSubtitleTrack(id: Int32) {
#if canImport(MobileVLCKit)
#if DEBUG
logSubtitleTracks(reason: "before-select-\(id)")
#endif
133 unmodified lines
return attachedCount
}
DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak self] in
#if DEBUG
self?.logSubtitleTracks(reason: "delayed-refresh")
#endif
9 unmodified lines
print("[DreamioVLC] subtitle tracks reason=\(reason) names=\(names) indexes=\(indexes.map { $0.int32Value }) selected=\(mediaPlayer.currentVideoSubTitleIndex)")
}
#endif
#endif
}
12 unmodified lines
case .paused, .stopped, .ended:
onStateChange?()
case .esAdded:
#if DEBUG
logSubtitleTracks(reason: "esAdded")
#endif
22 unmodified lines
23
24
25
26
27
28
29
30
12 unmodified lines
43
44
45
46
47
48
49
50
53 unmodified lines
104
105
106
107
108
109
110
133 unmodified lines
244
245
246
247
248
249
250
9 unmodified lines
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
12 unmodified lines
293
294
295
296
297
298
299
22 unmodified lines
private let mediaPlayer = VLCMediaPlayer()
#endif
private var attachedSubtitleURLs = Set<URL>()
private var didAutoSelectSubtitleTrack = false
private var didUserSelectSubtitleTrack = false
override init() {
super.init()
12 unmodified lines
func play(request: NativePlaybackRequest) {
#if canImport(MobileVLCKit)
attachedSubtitleURLs.removeAll()
didAutoSelectSubtitleTrack = false
didUserSelectSubtitleTrack = false
let media = VLCMedia(url: request.playbackURL)
let headerValue = request.headers
.map { "\($0.key): \($0.value)" }
53 unmodified lines
func selectSubtitleTrack(id: Int32) {
#if canImport(MobileVLCKit)
didUserSelectSubtitleTrack = true
#if DEBUG
logSubtitleTracks(reason: "before-select-\(id)")
#endif
133 unmodified lines
return attachedCount
}
DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak self] in
self?.selectInitialSubtitleTrackIfNeeded(reason: "delayed-refresh")
#if DEBUG
self?.logSubtitleTracks(reason: "delayed-refresh")
#endif
9 unmodified lines
print("[DreamioVLC] subtitle tracks reason=\(reason) names=\(names) indexes=\(indexes.map { $0.int32Value }) selected=\(mediaPlayer.currentVideoSubTitleIndex)")
}
#endif
private func selectInitialSubtitleTrackIfNeeded(reason: String) {
guard !didUserSelectSubtitleTrack,
!didAutoSelectSubtitleTrack,
mediaPlayer.currentVideoSubTitleIndex < 0,
let track = subtitleTracks.first(where: { $0.id >= 0 }) else {
return
}
didAutoSelectSubtitleTrack = true
#if DEBUG
print("[DreamioVLC] auto-select subtitle id=\(track.id) name=\(track.name) reason=\(reason)")
#endif
mediaPlayer.currentVideoSubTitleIndex = track.id
}
#endif
}
12 unmodified lines
case .paused, .stopped, .ended:
onStateChange?()
case .esAdded:
selectInitialSubtitleTrackIfNeeded(reason: "esAdded")
#if DEBUG
logSubtitleTracks(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

Issues, Limitations, and Mitigations

Follow-up Work