From 967df992f663102f25830d22f44a2222459ed7ce Mon Sep 17 00:00:00 2001 From: dirtydishes Date: Tue, 26 May 2026 01:19:20 -0400 Subject: [PATCH] recreate opensubtitles turn doc --- ...subtitles-languages-in-caption-tracks.html | 1038 +++++++++++------ 1 file changed, 650 insertions(+), 388 deletions(-) diff --git a/docs/turns/2026-05-25-show-opensubtitles-languages-in-caption-tracks.html b/docs/turns/2026-05-25-show-opensubtitles-languages-in-caption-tracks.html index 2dfb66f..5354d29 100644 --- a/docs/turns/2026-05-25-show-opensubtitles-languages-in-caption-tracks.html +++ b/docs/turns/2026-05-25-show-opensubtitles-languages-in-caption-tracks.html @@ -4,428 +4,690 @@ Show OpenSubtitles Languages in Caption Tracks + + + -
-
-

Dreamio Turn Document · May 25, 2026

-

Show OpenSubtitles Languages in Caption Tracks

-

External subtitles attached through VLC now keep Dreamio's parsed language metadata, so the captions menu can show useful names like English, Spanish, or English SDH instead of generic VLC labels such as Track 1.

-
- Issue: dreamio-2ju - Scope: subtitle display names - Validation: tests and simulator build passed +
+
+

Dreamio turn document

+

Show OpenSubtitles Languages in Caption Tracks

+

External subtitles attached through VLC now keep Dreamio's parsed language metadata, so the captions menu can show useful names like English, Spanish, or English SDH instead of generic VLC labels such as Track 1.

+
+ May 25, 2026 + Beads issue dreamio-2ju + Subtitle display names + Tests and simulator build passed +
+
+ +
+
+

Summary

+

Dreamio already parsed OpenSubtitles labels and language codes, but VLC exposed attached external subtitle tracks later with generic names. This change preserves each external candidate's best display name, maps it onto the new VLC track ID once it appears, and returns the preserved name whenever VLC's reported name is generic or empty.

+
+ +
+

Changes Made

+
    +
  • Added SubtitleDisplayName, an internal helper for turning subtitle labels and language metadata into menu-ready names.
  • +
  • Added VLC backend state for pending external subtitle display names and track-ID-to-display-name mappings.
  • +
  • Updated subtitleTracks so generic VLC names are replaced only when Dreamio has a preserved external subtitle name for that specific track ID.
  • +
  • Added unit coverage for language-code normalization, meaningful labels, filename fallback, and menu option names.
  • +
+
+ +
+

Context

+

OpenSubtitles and Stremio payloads often carry useful metadata such as eng, Spanish, or English SDH. The parser kept that metadata on SubtitleCandidate, but once addPlaybackSlave handed a URL to VLC, the eventual track list could contain only Track 1, Track 2, or an empty string. The captions menu reads from backend.subtitleTracks, so the safest fix was to improve that backend output.

+
+ +
+

Important Implementation Details

+
    +
  • Labels are trusted when they are meaningful. English SDH stays English SDH.
  • +
  • Generic labels such as Track 1, External Subtitle, empty strings, and numeric-only labels fall back to language metadata.
  • +
  • Common language codes are normalized explicitly, including eng/en, spa/es, and fre/fra/fr. The helper also uses Locale for broader short-code support.
  • +
  • The VLC backend captures baseline subtitle track IDs before attachment, then assigns preserved names to newly visible external track IDs in attachment order.
  • +
  • Embedded tracks are left alone unless they match a newly mapped external ID and VLC reports a generic or empty name.
  • +
+
+ +
+

Relevant Diff Snippets

+
+
+

Dreamio/StreamCandidate.swift · subtitle display-name helper

+
+
Dreamio/StreamCandidate.swift
+32
28 unmodified lines
29
30
31
32
28 unmodified lines
let name: String
}
+
typealias AudioTrack = SubtitleTrack
28 unmodified lines
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
28 unmodified lines
let name: String
}
+
enum SubtitleDisplayName {
private static let genericLabels = [
"external subtitle",
"subtitle",
"unknown"
]
+
private static let languageCodeAliases = [
"eng": "en", "en": "en",
"spa": "es", "es": "es",
"fre": "fr", "fra": "fr", "fr": "fr",
"nld": "nl", "dan": "da"
]
+
static func displayName(for candidate: SubtitleCandidate) -> String {
if let label = meaningfulDisplayText(candidate.label) {
return label
}
if let languageName = languageName(for: candidate.language) {
return languageName
}
return fallbackName(from: candidate)
}
+
static func name(forVLCTrackName trackName: String, preservedName: String?) -> String {
guard isGenericLabel(trackName), let preservedName else {
return trackName
}
return preservedName
}
}
+
typealias AudioTrack = SubtitleTrack
+
+
diff --git a/Dreamio/StreamCandidate.swift b/Dreamio/StreamCandidate.swift
+index 2ad4c5e..7ec90c1 100644
+--- a/Dreamio/StreamCandidate.swift
++++ b/Dreamio/StreamCandidate.swift
+@@ -29,6 +29,58 @@ struct SubtitleTrack: Equatable {
+     let name: String
+ }
++
++enum SubtitleDisplayName {
++    private static let genericLabels = [
++        "external subtitle",
++        "subtitle",
++        "unknown"
++    ]
++
++    private static let languageCodeAliases = [
++        "eng": "en", "en": "en",
++        "spa": "es", "es": "es",
++        "fre": "fr", "fra": "fr", "fr": "fr",
++        "nld": "nl", "dan": "da"
++    ]
++
++    static func displayName(for candidate: SubtitleCandidate) -> String {
++        if let label = meaningfulDisplayText(candidate.label) {
++            return label
++        }
++        if let languageName = languageName(for: candidate.language) {
++            return languageName
++        }
++        return fallbackName(from: candidate)
++    }
++
++    static func name(forVLCTrackName trackName: String, preservedName: String?) -> String {
++        guard isGenericLabel(trackName), let preservedName else {
++            return trackName
++        }
++        return preservedName
++    }
++}
+
+ typealias AudioTrack = SubtitleTrack
+
+
+ +
+

Dreamio/VLCNativePlaybackBackend.swift · preserve names for new VLC tracks

+
+
Dreamio/VLCNativePlaybackBackend.swift
-1+28
25 unmodified lines
26
27
28
204 unmodified lines
236
237
238
239
240
241
35 unmodified lines
278
279
280
281
282
38 unmodified lines
322
323
324
25 unmodified lines
private var didUserSelectSubtitleTrack = false
private var autoSelectedSubtitleTrackID: Int32?
private var externalSubtitleBaselineTrackIDs = Set<Int32>()
204 unmodified lines
var subtitleTracks: [SubtitleTrack] {
#if canImport(MobileVLCKit)
return rawSubtitleTracks()
#else
[]
#endif
35 unmodified lines
attachedSubtitleURLs.insert(candidate.url)
externalSubtitleBaselineTrackIDs.formUnion(baselineTrackIDs)
hasPendingExternalSubtitleSelection = true
mediaPlayer.addPlaybackSlave(candidate.url, type: .subtitle, enforce: false)
attachedCount += 1
38 unmodified lines
SubtitleTrack(id: index.int32Value, name: name)
}
}
25 unmodified lines
26
27
28
29
30
31
204 unmodified lines
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
35 unmodified lines
288
289
290
291
292
293
38 unmodified lines
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
25 unmodified lines
private var didUserSelectSubtitleTrack = false
private var autoSelectedSubtitleTrackID: Int32?
private var externalSubtitleBaselineTrackIDs = Set<Int32>()
private var hasPendingExternalSubtitleSelection = false
private var pendingExternalSubtitleDisplayNames: [String] = []
private var externalSubtitleDisplayNamesByTrackID: [Int32: String] = [:]
204 unmodified lines
var subtitleTracks: [SubtitleTrack] {
#if canImport(MobileVLCKit)
reconcileExternalSubtitleDisplayNames()
return rawSubtitleTracks().map { track in
SubtitleTrack(
id: track.id,
name: SubtitleDisplayName.name(
forVLCTrackName: track.name,
preservedName: externalSubtitleDisplayNamesByTrackID[track.id]
)
)
}
#else
[]
#endif
35 unmodified lines
attachedSubtitleURLs.insert(candidate.url)
externalSubtitleBaselineTrackIDs.formUnion(baselineTrackIDs)
hasPendingExternalSubtitleSelection = true
pendingExternalSubtitleDisplayNames.append(SubtitleDisplayName.displayName(for: candidate))
mediaPlayer.addPlaybackSlave(candidate.url, type: .subtitle, enforce: false)
attachedCount += 1
38 unmodified lines
SubtitleTrack(id: index.int32Value, name: name)
}
}
+
private func reconcileExternalSubtitleDisplayNames() {
guard !pendingExternalSubtitleDisplayNames.isEmpty else {
return
}
+
rawSubtitleTracks()
.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 { externalSubtitleDisplayNamesByTrackID[$0.id] = pendingExternalSubtitleDisplayNames.removeFirst() }
}
+
+
diff --git a/Dreamio/VLCNativePlaybackBackend.swift b/Dreamio/VLCNativePlaybackBackend.swift
+index 8582f62..d7f3c44 100644
+--- a/Dreamio/VLCNativePlaybackBackend.swift
++++ b/Dreamio/VLCNativePlaybackBackend.swift
+@@ -26,6 +26,9 @@ final class VLCNativePlaybackBackend: NSObject, NativePlaybackBackend {
+     private var didUserSelectSubtitleTrack = false
+     private var autoSelectedSubtitleTrackID: Int32?
+     private var externalSubtitleBaselineTrackIDs = Set<Int32>()
++    private var hasPendingExternalSubtitleSelection = false
++    private var pendingExternalSubtitleDisplayNames: [String] = []
++    private var externalSubtitleDisplayNamesByTrackID: [Int32: String] = [:]
+@@ -236,7 +239,14 @@ final class VLCNativePlaybackBackend: NSObject, NativePlaybackBackend {
+     var subtitleTracks: [SubtitleTrack] {
+ #if canImport(MobileVLCKit)
+-        return rawSubtitleTracks()
++        reconcileExternalSubtitleDisplayNames()
++        return rawSubtitleTracks().map { track in
++            SubtitleTrack(
++                id: track.id,
++                name: SubtitleDisplayName.name(
++                    forVLCTrackName: track.name,
++                    preservedName: externalSubtitleDisplayNamesByTrackID[track.id]
++                )
++            )
++        }
+ #else
+         []
+ #endif
+@@ -278,6 +288,7 @@ final class VLCNativePlaybackBackend: NSObject, NativePlaybackBackend {
+             attachedSubtitleURLs.insert(candidate.url)
+             externalSubtitleBaselineTrackIDs.formUnion(baselineTrackIDs)
+             hasPendingExternalSubtitleSelection = true
++            pendingExternalSubtitleDisplayNames.append(SubtitleDisplayName.displayName(for: candidate))
+             mediaPlayer.addPlaybackSlave(candidate.url, type: .subtitle, enforce: false)
+             attachedCount += 1
+@@ -322,6 +333,20 @@ final class VLCNativePlaybackBackend: NSObject, NativePlaybackBackend {
+             SubtitleTrack(id: index.int32Value, name: name)
+         }
+     }
++
++    private func reconcileExternalSubtitleDisplayNames() {
++        guard !pendingExternalSubtitleDisplayNames.isEmpty else {
++            return
++        }
++
++        rawSubtitleTracks()
++            .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 { externalSubtitleDisplayNamesByTrackID[$0.id] = pendingExternalSubtitleDisplayNames.removeFirst() }
++    }
+
+
+ +
+

Tests/StreamResolverTests.swift · display-name coverage

+
+
Tests/StreamResolverTests.swift
+38
18 unmodified lines
19
20
21
22
23
413 unmodified lines
438
439
440
441
442
18 unmodified lines
testSubtitleCandidateDeduplicationPreservesLabels()
testSubtitleCandidateDeduplicationUpgradesLabels()
testSubtitleOptionMappingIncludesNone()
print("StreamResolverTests passed")
}
413 unmodified lines
assertEqual(options.first?.id, -1)
}
+
private static func assertEqual<T: Equatable>(_ actual: T?, _ expected: T, file: StaticString = #file, line: UInt = #line) {
assert(actual == expected, "Expected \(String(describing: expected)), got \(String(describing: actual))", file: file, line: line)
18 unmodified lines
19
20
21
22
23
24
25
413 unmodified lines
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
18 unmodified lines
testSubtitleCandidateDeduplicationPreservesLabels()
testSubtitleCandidateDeduplicationUpgradesLabels()
testSubtitleDisplayNameNormalization()
testSubtitleDisplayNameUsesPreservedNamesForGenericVLCTracks()
testSubtitleOptionMappingIncludesNone()
print("StreamResolverTests passed")
}
413 unmodified lines
assertEqual(options.first?.id, -1)
}
+
private static func testSubtitleDisplayNameNormalization() {
assertEqual(
SubtitleDisplayName.displayName(for: SubtitleCandidate(
url: URL(string: "https://opensubtitles.example.test/download/subtitle.srt")!,
label: "Track 1",
language: "eng"
)),
"English"
)
assertEqual(
SubtitleDisplayName.displayName(for: SubtitleCandidate(
url: URL(string: "https://opensubtitles.example.test/download/subtitle.srt")!,
label: "English SDH",
language: "eng"
)),
"English SDH"
)
assertEqual(
SubtitleDisplayName.displayName(for: SubtitleCandidate(
url: URL(string: "https://cdn.example.test/subtitles/movie.es.srt")!,
label: "External Subtitle",
language: nil
)),
"movie.es"
)
}
+
private static func testSubtitleDisplayNameUsesPreservedNamesForGenericVLCTracks() {
let options = SubtitleOptionMapper.options(from: [
SubtitleTrack(id: 3, name: SubtitleDisplayName.name(forVLCTrackName: "Track 1", preservedName: "English")),
SubtitleTrack(id: 4, name: SubtitleDisplayName.name(forVLCTrackName: "Commentary", preservedName: "Spanish"))
])
+
assertEqual(options.map(\.name), ["None", "English", "Commentary"])
}
+
private static func assertEqual<T: Equatable>(_ actual: T?, _ expected: T, file: StaticString = #file, line: UInt = #line) {
assert(actual == expected, "Expected \(String(describing: expected)), got \(String(describing: actual))", file: file, line: line)
+
+
diff --git a/Tests/StreamResolverTests.swift b/Tests/StreamResolverTests.swift
+index 4a4d03e..2bd7932 100644
+--- a/Tests/StreamResolverTests.swift
++++ b/Tests/StreamResolverTests.swift
+@@ -19,6 +19,8 @@ struct StreamResolverTests {
+         testSubtitleCandidateDeduplicationPreservesLabels()
+         testSubtitleCandidateDeduplicationUpgradesLabels()
++        testSubtitleDisplayNameNormalization()
++        testSubtitleDisplayNameUsesPreservedNamesForGenericVLCTracks()
+         testSubtitleOptionMappingIncludesNone()
+         print("StreamResolverTests passed")
+     }
+@@ -438,6 +440,50 @@ struct StreamResolverTests {
+         assertEqual(options.first?.id, -1)
+     }
++
++    private static func testSubtitleDisplayNameNormalization() {
++        assertEqual(
++            SubtitleDisplayName.displayName(for: SubtitleCandidate(
++                url: URL(string: "https://opensubtitles.example.test/download/subtitle.srt")!,
++                label: "Track 1",
++                language: "eng"
++            )),
++            "English"
++        )
++        assertEqual(
++            SubtitleDisplayName.displayName(for: SubtitleCandidate(
++                url: URL(string: "https://opensubtitles.example.test/download/subtitle.srt")!,
++                label: "English SDH",
++                language: "eng"
++            )),
++            "English SDH"
++        )
++        assertEqual(
++            SubtitleDisplayName.displayName(for: SubtitleCandidate(
++                url: URL(string: "https://cdn.example.test/subtitles/movie.es.srt")!,
++                label: "External Subtitle",
++                language: nil
++            )),
++            "movie.es"
++        )
++    }
++
++    private static func testSubtitleDisplayNameUsesPreservedNamesForGenericVLCTracks() {
++        let options = SubtitleOptionMapper.options(from: [
++            SubtitleTrack(id: 3, name: SubtitleDisplayName.name(forVLCTrackName: "Track 1", preservedName: "English")),
++            SubtitleTrack(id: 4, name: SubtitleDisplayName.name(forVLCTrackName: "Commentary", preservedName: "Spanish"))
++        ])
++
++        assertEqual(options.map(\.name), ["None", "English", "Commentary"])
++    }
+
+     private static func assertEqual<T: Equatable>(_ actual: T?, _ expected: T, file: StaticString = #file, line: UInt = #line) {
+         assert(actual == expected, "Expected \(String(describing: expected)), got \(String(describing: actual))", file: file, line: line)
+
+
+
+

Diffs are generated with @pierre/diffs/ssr at documentation time. Each snippet is intentionally scoped and contained inside its own diff shell so the page remains readable as a static HTML artifact.

+
+ +
+

Expected Impact for End-Users

+

Users should see clearer captions menu choices after external OpenSubtitles tracks attach. Instead of guessing between Track 1 and Track 2, they should see language-aware labels that make track selection understandable at a glance.

+
+ +
+

Validation

+
    +
  • swiftc Dreamio/StreamCandidate.swift Dreamio/StreamResolver.swift Tests/StreamResolverTests.swift -o /tmp/dreamio-stream-tests && /tmp/dreamio-stream-tests passed.
  • +
  • xcodebuild -workspace Dreamio.xcworkspace -scheme Dreamio -sdk iphonesimulator -configuration Debug build CODE_SIGNING_ALLOWED=NO passed.
  • +
  • Manual app verification with a live OpenSubtitles playback source was not performed in that turn.
  • +
+
+ +
+

Issues, Limitations, and Mitigations

+
    +
  • The external ID mapping assumes VLC exposes newly attached subtitle tracks in the same order Dreamio attached them. The mapping is constrained to tracks outside the captured baseline.
  • +
  • If VLC later returns a meaningful track name for an external subtitle, Dreamio keeps VLC's name instead of overriding it.
  • +
  • The language helper covers common OpenSubtitles/Stremio codes directly and delegates broader short-code support to Locale.
  • +
+
+ +
+

Follow-up Work

+
    +
  • No required follow-up was filed for dreamio-2ju.
  • +
  • A useful future improvement would be a lightweight integration test seam around VLC subtitle-track reconciliation, if the backend gets a mockable media-player adapter later.
  • +
+
-
- -
-

Summary

-

Dreamio already parsed OpenSubtitles labels and language codes, but VLC exposed attached external subtitle tracks later with generic names. This change preserves each external candidate's best display name, maps it onto the new VLC track ID once it appears, and returns the preserved name whenever VLC's reported name is generic or empty.

-
- -
-

Changes Made

-
    -
  • Added SubtitleDisplayName, an internal helper for turning subtitle labels and language metadata into menu-ready names.
  • -
  • Added VLC backend state for pending external subtitle display names and track-ID-to-display-name mappings.
  • -
  • Updated subtitleTracks so generic VLC names are replaced only when Dreamio has a preserved external subtitle name for that specific track ID.
  • -
  • Added unit coverage for language-code normalization, meaningful labels, filename fallback, and menu option names.
  • -
-
- -
-

Context

-

OpenSubtitles and Stremio payloads often carry useful metadata such as eng, Spanish, or English SDH. The parser kept that metadata on SubtitleCandidate, but once addPlaybackSlave handed a URL to VLC, the eventual track list could contain only Track 1, Track 2, or an empty string. The captions menu reads from backend.subtitleTracks, so the safest fix is to improve that backend output.

-
- -
-

Important Implementation Details

-
    -
  • Labels are trusted when they are meaningful. English SDH stays English SDH.
  • -
  • Generic labels such as Track 1, External Subtitle, empty strings, and numeric-only labels fall back to language metadata.
  • -
  • Common language codes are normalized explicitly, including eng/en, spa/es, and fre/fra/fr. The helper also uses Locale for broader short-code support.
  • -
  • The VLC backend captures baseline subtitle track IDs before attachment, then assigns preserved names to newly visible external track IDs in attachment order.
  • -
  • Embedded tracks are left alone unless they match a newly mapped external ID and VLC reports a generic or empty name.
  • -
-
- -
-

Relevant Diff Snippets

-

Dreamio/StreamCandidate.swift

Dreamio/StreamCandidate.swift
+102
39 unmodified lines
40
41
42
43
44
45
39 unmodified lines
let name: String
}
-
#if DEBUG
enum SubtitleDebugFormatter {
static func candidateSummary(_ candidates: [SubtitleCandidate]) -> String {
39 unmodified lines
40
41
42
43
44
45
46
47
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
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
39 unmodified lines
let name: String
}
-
enum SubtitleDisplayName {
private static let genericLabels = [
"external subtitle",
"subtitle",
"unknown"
]
-
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 {
if let label = meaningfulDisplayText(candidate.label) {
return label
}
-
if let languageName = languageName(for: candidate.language) {
return languageName
}
-
return fallbackName(from: candidate)
}
-
static func name(forVLCTrackName trackName: String, preservedName: String?) -> String {
guard isGenericLabel(trackName), let preservedName else {
return trackName
}
return preservedName
}
-
static func isGenericLabel(_ value: String) -> Bool {
let normalized = value.trimmingCharacters(in: .whitespacesAndNewlines)
guard !normalized.isEmpty else {
return true
}
-
let lowercased = normalized.lowercased()
if genericLabels.contains(lowercased) {
return true
}
if Int(normalized) != nil {
return true
}
if lowercased.range(of: #"^track\s*\d+$"#, options: .regularExpression) != nil {
return true
}
return false
}
-
private static func meaningfulDisplayText(_ value: String?) -> String? {
guard let value else {
return nil
}
let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
guard !isGenericLabel(trimmed) else {
return nil
}
guard trimmed.count <= 3 else {
return trimmed
}
return languageName(for: trimmed) ?? trimmed
}
-
private static func languageName(for value: String?) -> String? {
guard let value else {
return nil
}
-
let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty, Int(trimmed) == nil else {
return nil
}
-
if trimmed.count > 3 {
return trimmed.capitalized
}
-
let lowercased = trimmed.lowercased()
let languageCode = languageCodeAliases[lowercased] ?? lowercased
guard let name = Locale.current.localizedString(forLanguageCode: languageCode) else {
return nil
}
return name.capitalized
}
-
private static func fallbackName(from candidate: SubtitleCandidate) -> String {
let trimmedLabel = candidate.label.trimmingCharacters(in: .whitespacesAndNewlines)
if !isGenericLabel(trimmedLabel) {
return trimmedLabel
}
-
let fileName = candidate.url.deletingPathExtension().lastPathComponent
return fileName.isEmpty ? "External Subtitle" : fileName
}
}
-
#if DEBUG
enum SubtitleDebugFormatter {
static func candidateSummary(_ candidates: [SubtitleCandidate]) -> String {
-

Dreamio/VLCNativePlaybackBackend.swift

Dreamio/VLCNativePlaybackBackend.swift
-5+41
27 unmodified lines
28
29
30
31
32
33
17 unmodified lines
51
52
53
54
55
56
142 unmodified lines
199
200
201
202
203
204
205
206
207
208
20 unmodified lines
229
230
231
232
233
234
235
2 unmodified lines
238
239
240
241
242
243
24 unmodified lines
268
269
270
271
272
273
27 unmodified lines
private var autoSelectedSubtitleTrackID: Int32?
private var externalSubtitleBaselineTrackIDs = Set<Int32>()
private var hasPendingExternalSubtitleSelection = false
-
override init() {
super.init()
17 unmodified lines
autoSelectedSubtitleTrackID = nil
externalSubtitleBaselineTrackIDs.removeAll()
hasPendingExternalSubtitleSelection = false
let media = VLCMedia(url: request.playbackURL)
let headerValue = request.headers
.map { "\($0.key): \($0.value)" }
142 unmodified lines
-
var subtitleTracks: [SubtitleTrack] {
#if canImport(MobileVLCKit)
let names = mediaPlayer.videoSubTitlesNames as? [String] ?? []
let indexes = mediaPlayer.videoSubTitlesIndexes as? [NSNumber] ?? []
return zip(indexes, names).map { index, name in
SubtitleTrack(id: index.int32Value, name: name)
}
#else
[]
20 unmodified lines
private func attachSubtitles(_ candidates: [SubtitleCandidate]) -> Int {
var attachedCount = 0
var duplicateCount = 0
let baselineTrackIDs = Set(subtitleTracks.filter { $0.id >= 0 }.map(\.id))
candidates.forEach { candidate in
guard !attachedSubtitleURLs.contains(candidate.url) else {
duplicateCount += 1
2 unmodified lines
attachedSubtitleURLs.insert(candidate.url)
externalSubtitleBaselineTrackIDs.formUnion(baselineTrackIDs)
hasPendingExternalSubtitleSelection = true
mediaPlayer.addPlaybackSlave(candidate.url, type: .subtitle, enforce: false)
attachedCount += 1
#if DEBUG
24 unmodified lines
return attachedCount
}
-
#if DEBUG
private func logSubtitleTracks(reason: String) {
let names = mediaPlayer.videoSubTitlesNames as? [String] ?? []
27 unmodified lines
28
29
30
31
32
33
34
35
17 unmodified lines
53
54
55
56
57
58
59
60
142 unmodified lines
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
20 unmodified lines
238
239
240
241
242
243
244
2 unmodified lines
247
248
249
250
251
252
253
24 unmodified lines
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
27 unmodified lines
private var autoSelectedSubtitleTrackID: Int32?
private var externalSubtitleBaselineTrackIDs = Set<Int32>()
private var hasPendingExternalSubtitleSelection = false
private var pendingExternalSubtitleDisplayNames: [String] = []
private var externalSubtitleDisplayNamesByTrackID: [Int32: String] = [:]
-
override init() {
super.init()
17 unmodified lines
autoSelectedSubtitleTrackID = nil
externalSubtitleBaselineTrackIDs.removeAll()
hasPendingExternalSubtitleSelection = false
pendingExternalSubtitleDisplayNames.removeAll()
externalSubtitleDisplayNamesByTrackID.removeAll()
let media = VLCMedia(url: request.playbackURL)
let headerValue = request.headers
.map { "\($0.key): \($0.value)" }
142 unmodified lines
-
var subtitleTracks: [SubtitleTrack] {
#if canImport(MobileVLCKit)
reconcileExternalSubtitleDisplayNames()
return rawSubtitleTracks().map { track in
SubtitleTrack(
id: track.id,
name: SubtitleDisplayName.name(
forVLCTrackName: track.name,
preservedName: externalSubtitleDisplayNamesByTrackID[track.id]
)
)
}
#else
[]
20 unmodified lines
private func attachSubtitles(_ candidates: [SubtitleCandidate]) -> Int {
var attachedCount = 0
var duplicateCount = 0
let baselineTrackIDs = Set(rawSubtitleTracks().filter { $0.id >= 0 }.map(\.id))
candidates.forEach { candidate in
guard !attachedSubtitleURLs.contains(candidate.url) else {
duplicateCount += 1
2 unmodified lines
attachedSubtitleURLs.insert(candidate.url)
externalSubtitleBaselineTrackIDs.formUnion(baselineTrackIDs)
hasPendingExternalSubtitleSelection = true
pendingExternalSubtitleDisplayNames.append(SubtitleDisplayName.displayName(for: candidate))
mediaPlayer.addPlaybackSlave(candidate.url, type: .subtitle, enforce: false)
attachedCount += 1
#if DEBUG
24 unmodified lines
return attachedCount
}
-
private func rawSubtitleTracks() -> [SubtitleTrack] {
let names = mediaPlayer.videoSubTitlesNames as? [String] ?? []
let indexes = mediaPlayer.videoSubTitlesIndexes as? [NSNumber] ?? []
return zip(indexes, names).map { index, name in
SubtitleTrack(id: index.int32Value, name: name)
}
}
-
private func reconcileExternalSubtitleDisplayNames() {
guard !pendingExternalSubtitleDisplayNames.isEmpty else {
return
}
-
rawSubtitleTracks()
.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 {
return
}
externalSubtitleDisplayNamesByTrackID[track.id] = pendingExternalSubtitleDisplayNames.removeFirst()
}
}
-
#if DEBUG
private func logSubtitleTracks(reason: String) {
let names = mediaPlayer.videoSubTitlesNames as? [String] ?? []
-

Tests/StreamResolverTests.swift

Tests/StreamResolverTests.swift
+52
20 unmodified lines
21
22
23
24
25
26
410 unmodified lines
437
438
439
440
441
442
20 unmodified lines
await testSubtitleResolverRejectsNonSubtitleAPIResponse()
testSubtitleCandidateDeduplicationPreservesLabels()
testSubtitleCandidateDeduplicationUpgradesLabels()
testSubtitleOptionMappingIncludesNone()
print("StreamResolverTests passed")
}
410 unmodified lines
assertEqual(options.first?.id, -1)
}
-
private static func assertEqual<T: Equatable>(_ actual: T?, _ expected: T, file: StaticString = #file, line: UInt = #line) {
assert(actual == expected, "Expected \(String(describing: expected)), got \(String(describing: actual))", file: file, line: line)
}
20 unmodified lines
21
22
23
24
25
26
27
28
410 unmodified lines
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
20 unmodified lines
await testSubtitleResolverRejectsNonSubtitleAPIResponse()
testSubtitleCandidateDeduplicationPreservesLabels()
testSubtitleCandidateDeduplicationUpgradesLabels()
testSubtitleDisplayNameNormalization()
testSubtitleDisplayNameUsesPreservedNamesForGenericVLCTracks()
testSubtitleOptionMappingIncludesNone()
print("StreamResolverTests passed")
}
410 unmodified lines
assertEqual(options.first?.id, -1)
}
-
private static func testSubtitleDisplayNameNormalization() {
assertEqual(
SubtitleDisplayName.displayName(for: SubtitleCandidate(
url: URL(string: "https://opensubtitles.example.test/download/subtitle.srt")!,
label: "Track 1",
language: "eng"
)),
"English"
)
assertEqual(
SubtitleDisplayName.displayName(for: SubtitleCandidate(
url: URL(string: "https://opensubtitles.example.test/download/subtitle.srt")!,
label: "Track 2",
language: "Spanish"
)),
"Spanish"
)
assertEqual(
SubtitleDisplayName.displayName(for: SubtitleCandidate(
url: URL(string: "https://opensubtitles.example.test/download/subtitle.srt")!,
label: "English SDH",
language: "eng"
)),
"English SDH"
)
assertEqual(
SubtitleDisplayName.displayName(for: SubtitleCandidate(
url: URL(string: "https://cdn.example.test/subtitles/movie.es.srt")!,
label: "External Subtitle",
language: nil
)),
"movie.es"
)
}
-
private static func testSubtitleDisplayNameUsesPreservedNamesForGenericVLCTracks() {
let options = SubtitleOptionMapper.options(from: [
SubtitleTrack(
id: 3,
name: SubtitleDisplayName.name(forVLCTrackName: "Track 1", preservedName: "English")
),
SubtitleTrack(
id: 4,
name: SubtitleDisplayName.name(forVLCTrackName: "Commentary", preservedName: "Spanish")
)
])
-
assertEqual(options.map(\.name), ["None", "English", "Commentary"])
}
-
private static func assertEqual<T: Equatable>(_ actual: T?, _ expected: T, file: StaticString = #file, line: UInt = #line) {
assert(actual == expected, "Expected \(String(describing: expected)), got \(String(describing: actual))", file: file, line: line)
}
-
- -
-

Expected Impact for End-Users

-

Users should see clearer captions menu choices after external OpenSubtitles tracks attach. Instead of guessing between Track 1 and Track 2, they should see language-aware labels that make track selection understandable at a glance.

-
- -
-

Validation

-
    -
  • Passed: swiftc Dreamio/StreamCandidate.swift Dreamio/StreamResolver.swift Tests/StreamResolverTests.swift -o /tmp/dreamio-stream-tests && /tmp/dreamio-stream-tests
  • -
  • Passed: xcodebuild -workspace Dreamio.xcworkspace -scheme Dreamio -sdk iphonesimulator -configuration Debug build CODE_SIGNING_ALLOWED=NO
  • -
-

Manual app verification with a live OpenSubtitles playback source was not performed in this turn.

-
- -
-

Issues, Limitations, and Mitigations

-
    -
  • The external ID mapping assumes VLC exposes newly attached subtitle tracks in the same order Dreamio attached them. This matches the intended flow and is constrained to tracks outside the captured baseline.
  • -
  • If VLC later returns a meaningful track name for an external subtitle, Dreamio keeps VLC's name instead of overriding it.
  • -
  • The language helper covers common OpenSubtitles/Stremio codes directly and delegates broader short-code support to Locale.
  • -
-
- -
-

Follow-up Work

-

No required follow-up was filed. A useful future improvement would be a lightweight integration test seam around VLC subtitle-track reconciliation, if the backend gets a mockable media-player adapter later.

-
- - -
+ - \ No newline at end of file +