Dreamio Turn Document ยท May 25, 2026

Preserve external subtitle language names

Dreamio now keeps known OpenSubtitles language metadata attached to generic VLC subtitle tracks, so the native captions menu can show useful names instead of a stack of indistinguishable Track N entries.

Issuedreamio-ejh
ValidationTests and simulator build passed
ScopeNative VLC subtitles

Summary

Fixed the subtitle menu naming path for externally attached VLC subtitle tracks. Generic VLC names like Track 2 now receive Dreamio's preserved OpenSubtitles display name when available.

Changes Made

Context

The debug log showed OpenSubtitles candidates arriving with usable language metadata, then VLC eventually surfacing external subtitle tracks as Track 2, Track 3, and similar generic names. Dreamio already had a preservation queue, but it could spend a queued display name on a VLC track that already had a meaningful title, which left later generic tracks unnamed.

Important Implementation Details

The key behavioral change is deliberately narrow: reconciliation now skips non-generic VLC subtitle names. If VLC gives a real name such as English (SDH) - [English], Dreamio keeps it. If VLC gives Track 4, Dreamio can replace it with the next preserved external subtitle display name.

The alias table turns common three-letter subtitle codes into two-letter language codes before asking Locale for localized names. This makes labels such as nld and dan become Dutch and Danish instead of raw codes.

Relevant Diff Snippets

Dreamio/VLCNativePlaybackBackend.swift

Dreamio/VLCNativePlaybackBackend.swift
+1
328 unmodified lines
329
330
331
332
333
334
328 unmodified lines
.filter { $0.id >= 0 }
.filter { !externalSubtitleBaselineTrackIDs.contains($0.id) }
.filter { externalSubtitleDisplayNamesByTrackID[$0.id] == nil }
.sorted { $0.id < $1.id }
.forEach { track in
guard !pendingExternalSubtitleDisplayNames.isEmpty else {
328 unmodified lines
329
330
331
332
333
334
335
328 unmodified lines
.filter { $0.id >= 0 }
.filter { !externalSubtitleBaselineTrackIDs.contains($0.id) }
.filter { externalSubtitleDisplayNamesByTrackID[$0.id] == nil }
.filter { SubtitleDisplayName.isGenericLabel($0.name) }
.sorted { $0.id < $1.id }
.forEach { track in
guard !pendingExternalSubtitleDisplayNames.isEmpty else {

Dreamio/StreamCandidate.swift

Dreamio/StreamCandidate.swift
-1+36
47 unmodified lines
48
49
50
51
52
53
54
55
56
57
58
59
60
47 unmodified lines
]
private static let languageCodeAliases = [
"eng": "en",
"en": "en",
"spa": "es",
"es": "es",
"fre": "fr",
"fra": "fr",
"fr": "fr"
]
static func displayName(for candidate: SubtitleCandidate) -> String {
47 unmodified lines
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
47 unmodified lines
]
private static let languageCodeAliases = [
"ara": "ar",
"ar": "ar",
"cze": "cs",
"ces": "cs",
"cs": "cs",
"dan": "da",
"da": "da",
"de": "de",
"deu": "de",
"ell": "el",
"el": "el",
"eng": "en",
"en": "en",
"fin": "fi",
"fi": "fi",
"spa": "es",
"es": "es",
"ger": "de",
"gre": "el",
"heb": "he",
"he": "he",
"hun": "hu",
"hu": "hu",
"fre": "fr",
"fra": "fr",
"fr": "fr",
"dut": "nl",
"nld": "nl",
"nl": "nl",
"per": "fa",
"fas": "fa",
"fa": "fa",
"pob": "pt",
"por": "pt",
"pt": "pt",
"ron": "ro",
"rum": "ro",
"ro": "ro",
"srp": "sr",
"sr": "sr",
"tur": "tr",
"tr": "tr"
]
static func displayName(for candidate: SubtitleCandidate) -> String {

Tests/StreamResolverTests.swift

Tests/StreamResolverTests.swift
+16
471 unmodified lines
472
473
474
475
476
477
471 unmodified lines
)),
"movie.es"
)
}
private static func testSubtitleDisplayNameUsesPreservedNamesForGenericVLCTracks() {
471 unmodified lines
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
471 unmodified lines
)),
"movie.es"
)
assertEqual(
SubtitleDisplayName.displayName(for: SubtitleCandidate(
url: URL(string: "https://opensubtitles.example.test/download/subtitle.srt")!,
label: "Track 3",
language: "nld"
)),
"Dutch"
)
assertEqual(
SubtitleDisplayName.displayName(for: SubtitleCandidate(
url: URL(string: "https://opensubtitles.example.test/download/subtitle.srt")!,
label: "Track 4",
language: "dan"
)),
"Danish"
)
}
private static func testSubtitleDisplayNameUsesPreservedNamesForGenericVLCTracks() {

Expected Impact for End-Users

When subtitles load through the native VLC player, the captions menu should be easier to scan. Users should see language names for generic external subtitle tracks instead of guessing which Track N corresponds to the language they want.

Validation

Issues, Limitations, and Mitigations

This still depends on VLC eventually surfacing external subtitle tracks after addPlaybackSlave. The change improves the names once tracks appear; it does not change VLC's asynchronous attachment behavior or guarantee that every remote subtitle file becomes visible instantly.

Follow-up Work

No required follow-up remains for this fix. A useful later improvement would be deeper URL-to-track correlation if MobileVLCKit exposes enough metadata to map each external subtitle slave to an exact surfaced track.