Queue VLC Subtitles Until Media Start
Fixed a startup race introduced by the real local range buffer: subtitle candidates can now arrive during the cache probe and wait safely until VLC has an active media item.
Summary
The native VLC backend now queues external subtitle candidates received before media startup completes. Once the local-cache or direct VLC media item is created and playback begins, the queued candidates are flushed through the existing subtitle attachment path.
Changes Made
- Added a pending subtitle queue and URL set to
VLCNativePlaybackBackend. - Reset the queue and startup flag for each new playback request.
- Marked media as started inside
startVLCMedia, then flushed queued subtitles. - Kept the existing attachment, dedupe, delayed refresh, display-name preservation, and auto-selection behavior intact.
Context
The logs showed subtitle attachments happening after cache-probe but before opening mode=local-cache. With the real range cache, VLC media creation is delayed by the async probe and local server setup. The native player still forwarded buffered subtitles immediately, so addPlaybackSlave was called while VLC had no active media item.
Important Implementation Details
The fix treats pre-media subtitle candidates as valid work, not duplicates. Candidates queued during startup are not inserted into attachedSubtitleURLs; they are held separately so the later flush can attach them normally once VLC is ready.
When subtitles arrive after media startup, behavior is unchanged. They still attach immediately and use the existing delayed refresh timers to wait for VLC to expose external subtitle tracks.
Relevant Diff Snippets
Rendered with @pierre/diffs/ssr from the one-file patch for Dreamio/VLCNativePlaybackBackend.swift.
27 unmodified lines28293031323320 unmodified lines545556575859295 unmodified lines35535635735835936010 unmodified lines37137237337437537636 unmodified lines41341441541641741827 unmodified linesprivate var lastLoggedState: String?private var lastBufferingLogTime: Date?private var attachedSubtitleURLs = Set<URL>()private var didAutoSelectSubtitleTrack = falseprivate var didUserSelectSubtitleTrack = falseprivate var autoSelectedSubtitleTrackID: Int32?20 unmodified lines#if canImport(MobileVLCKit)playbackStartupTask?.cancel()attachedSubtitleURLs.removeAll()didAutoSelectSubtitleTrack = falsedidUserSelectSubtitleTrack = falseautoSelectedSubtitleTrackID = nil295 unmodified linesprint("[DreamioVLC] opening mode=\(playbackMode) cachingMs=\(cachingMilliseconds) url=\(URLRedactor.redactedURLString(url.absoluteString))")#endifmediaPlayer.play()}private func addRemoteHeaders(to media: VLCMedia, request: NativePlaybackRequest) {10 unmodified lines}private func attachSubtitles(_ candidates: [SubtitleCandidate]) -> Int {var attachedCount = 0var duplicateCount = 0let baselineTrackIDs = Set(rawSubtitleTracks().filter { $0.id >= 0 }.map(\.id))36 unmodified linesreturn attachedCount}private func rawSubtitleTracks() -> [SubtitleTrack] {let names = mediaPlayer.videoSubTitlesNames as? [String] ?? []let indexes = mediaPlayer.videoSubTitlesIndexes as? [NSNumber] ?? []27 unmodified lines28293031323334353620 unmodified lines575859606162636465295 unmodified lines36136236336436536636736810 unmodified lines37938038138238338438538638738838939039139239339436 unmodified lines43143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546627 unmodified linesprivate var lastLoggedState: String?private var lastBufferingLogTime: Date?private var attachedSubtitleURLs = Set<URL>()private var pendingSubtitleCandidates: [SubtitleCandidate] = []private var pendingSubtitleURLs = Set<URL>()private var hasStartedMedia = falseprivate var didAutoSelectSubtitleTrack = falseprivate var didUserSelectSubtitleTrack = falseprivate var autoSelectedSubtitleTrackID: Int32?20 unmodified lines#if canImport(MobileVLCKit)playbackStartupTask?.cancel()attachedSubtitleURLs.removeAll()pendingSubtitleCandidates.removeAll()pendingSubtitleURLs.removeAll()hasStartedMedia = falsedidAutoSelectSubtitleTrack = falsedidUserSelectSubtitleTrack = falseautoSelectedSubtitleTrackID = nil295 unmodified linesprint("[DreamioVLC] opening mode=\(playbackMode) cachingMs=\(cachingMilliseconds) url=\(URLRedactor.redactedURLString(url.absoluteString))")#endifmediaPlayer.play()hasStartedMedia = trueflushPendingSubtitleCandidates()}private func addRemoteHeaders(to media: VLCMedia, request: NativePlaybackRequest) {10 unmodified lines}private func attachSubtitles(_ candidates: [SubtitleCandidate]) -> Int {guard hasStartedMedia else {let queued = queuePendingSubtitleCandidates(candidates)#if DEBUGif !candidates.isEmpty {print("[DreamioVLC] subtitle candidates=\(candidates.count) queued=\(queued) reason=media-not-started")}#endifreturn queued}var attachedCount = 0var duplicateCount = 0let baselineTrackIDs = Set(rawSubtitleTracks().filter { $0.id >= 0 }.map(\.id))36 unmodified linesreturn attachedCount}private func queuePendingSubtitleCandidates(_ candidates: [SubtitleCandidate]) -> Int {var queuedCount = 0candidates.forEach { candidate inguard !attachedSubtitleURLs.contains(candidate.url),!pendingSubtitleURLs.contains(candidate.url)else {return}pendingSubtitleURLs.insert(candidate.url)pendingSubtitleCandidates.append(candidate)queuedCount += 1}return queuedCount}private func flushPendingSubtitleCandidates() {guard !pendingSubtitleCandidates.isEmpty else {return}let candidates = pendingSubtitleCandidatespendingSubtitleCandidates.removeAll()pendingSubtitleURLs.removeAll()#if DEBUGprint("[DreamioVLC] flushing queued subtitles count=\(candidates.count)")#endif_ = attachSubtitles(candidates)}private func rawSubtitleTracks() -> [SubtitleTrack] {let names = mediaPlayer.videoSubTitlesNames as? [String] ?? []let indexes = mediaPlayer.videoSubTitlesIndexes as? [NSNumber] ?? []
New Changes as of May 25, 2026 at 6:55 PM EDT
Summary of changes
After the queue fix landed, device logs showed the queued subtitles flushing after VLC media startup, but VLC still exposed no subtitle tracks. The remaining problem was the subtitle URL shape: Stremio provider download URLs are extensionless and VLC accepts them without creating visible external subtitle tracks.
The subtitle resolver now downloads resolvable subtitle bodies and caches them as local files with real subtitle extensions before handing them to VLC. Stremio download URLs are still parsed as candidates, but they are no longer treated as direct VLC-ready subtitle files.
Why this change was made
The native backend debug output showed ext=none for every attached Stremio subtitle and visible=0 after attachment. Caching the subtitle payload gives VLC a local .srt, .vtt, or .ass file, which is a more reliable attachment target than a provider download endpoint.
Code diffs
34 unmodified lines35363738394041424325 unmodified lines69707172737412 unmodified lines87888990919238 unmodified lines13113213313413513613716 unmodified lines15415515615715815934 unmodified linesfinal class SubtitleResolver: SubtitleResolving {private let session: URLSessioninit(session: URLSession = .shared) {self.session = session}func resolve(_ candidate: SubtitleCandidate) async -> SubtitleCandidate? {25 unmodified linesreturn SubtitleCandidate(url: finalURL, label: candidate.label, language: candidate.language)}return Self.bestPlayableCandidate(from: data,responseURL: response.url,12 unmodified lines}}static func bestPlayableCandidate(from data: Data,responseURL: URL?,38 unmodified lineslet lowercased = url.absoluteString.lowercased()return ["srt", "vtt", "ass", "ssa", "sub"].contains(url.pathExtension.lowercased())|| [".srt?", ".vtt?", ".ass?", ".ssa?", ".sub?", ".srt&", ".vtt&", ".ass&", ".ssa&", ".sub&"].contains(where: lowercased.contains)|| isStremioSubtitleDownloadURL(url)}private static func shouldResolve(_ url: URL) -> Bool {16 unmodified lines|| path.range(of: #"(^|/)download(/|$)"#, options: .regularExpression) != nil}private static func logRejected(_ candidate: SubtitleCandidate, responseURL: URL?, data: Data) -> SubtitleCandidate? {#if DEBUGlet responseDescription = responseURL.map { URLRedactor.redactedURLString($0.absoluteString) } ?? "none"34 unmodified lines353637383940414243444546474825 unmodified lines7475767778798081828312 unmodified lines9697989910010110210310410510610710810911011111211311411511611711811912012112212312412512638 unmodified lines16516616716816917016 unmodified lines18718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523634 unmodified linesfinal class SubtitleResolver: SubtitleResolving {private let session: URLSessionprivate let cacheDirectory: URLinit(session: URLSession = .shared,cacheDirectory: URL = FileManager.default.temporaryDirectory.appendingPathComponent("DreamioSubtitles", isDirectory: true)) {self.session = sessionself.cacheDirectory = cacheDirectory}func resolve(_ candidate: SubtitleCandidate) async -> SubtitleCandidate? {25 unmodified linesreturn SubtitleCandidate(url: finalURL, label: candidate.label, language: candidate.language)}if let cachedCandidate = cacheSubtitleDataIfNeeded(data, original: candidate) {return cachedCandidate}return Self.bestPlayableCandidate(from: data,responseURL: response.url,12 unmodified lines}}private func cacheSubtitleDataIfNeeded(_ data: Data, original: SubtitleCandidate) -> SubtitleCandidate? {guard let subtitleType = Self.subtitleType(in: data) else {return nil}do {try FileManager.default.createDirectory(at: cacheDirectory,withIntermediateDirectories: true)let filename = "\(UUID().uuidString).\(subtitleType.fileExtension)"let fileURL = cacheDirectory.appendingPathComponent(filename)try data.write(to: fileURL, options: .atomic)#if DEBUGprint("[DreamioSubtitles] cached subtitle url=\(URLRedactor.redactedURLString(original.url.absoluteString)) file=\(fileURL.lastPathComponent)")#endifreturn SubtitleCandidate(url: fileURL, label: original.label, language: original.language)} catch {#if DEBUGprint("[DreamioSubtitles] cache failure=\(error.localizedDescription) url=\(URLRedactor.redactedURLString(original.url.absoluteString))")#endifreturn nil}}static func bestPlayableCandidate(from data: Data,responseURL: URL?,38 unmodified lineslet lowercased = url.absoluteString.lowercased()return ["srt", "vtt", "ass", "ssa", "sub"].contains(url.pathExtension.lowercased())|| [".srt?", ".vtt?", ".ass?", ".ssa?", ".sub?", ".srt&", ".vtt&", ".ass&", ".ssa&", ".sub&"].contains(where: lowercased.contains)}private static func shouldResolve(_ url: URL) -> Bool {16 unmodified lines|| path.range(of: #"(^|/)download(/|$)"#, options: .regularExpression) != nil}private enum SubtitlePayloadType {case srtcase vttcase assvar fileExtension: String {switch self {case .srt:return "srt"case .vtt:return "vtt"case .ass:return "ass"}}}private static func subtitleType(in data: Data) -> SubtitlePayloadType? {guard !data.isEmpty,let text = String(data: data.prefix(4096), encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines),!text.isEmptyelse {return nil}let lowercased = text.lowercased()if lowercased.hasPrefix("webvtt") {return .vtt}if lowercased.hasPrefix("[script info]")|| lowercased.contains("\n[events]")|| lowercased.contains("\r\n[events]") {return .ass}if lowercased.range(of: #"(?m)^\d+\s*[\r\n]+(?:\d{1,2}:)?\d{2}:\d{2}[,.]\d{3}\s*-->\s*(?:\d{1,2}:)?\d{2}:\d{2}[,.]\d{3}"#,options: .regularExpression) != nil {return .srt}return nil}private static func logRejected(_ candidate: SubtitleCandidate, responseURL: URL?, data: Data) -> SubtitleCandidate? {#if DEBUGlet responseDescription = responseURL.map { URLRedactor.redactedURLString($0.absoluteString) } ?? "none"
15 unmodified lines161718192021248 unmodified lines270271272273274275276160 unmodified lines43743843944044144244314 unmodified lines45845946046146246312 unmodified lines47647747847948048115 unmodified linestestStremioSubtitleDownloadURLParsing()testOpenSubtitlesV3DownloadResponseResolution()testOpenSubtitlesNestedDownloadResponseResolution()await testSubtitleResolverDownloadJSONReturningLink()await testSubtitleResolverRedirectToDirectSubtitle()await testSubtitleResolverRejectsNonSubtitleAPIResponse()248 unmodified linesassertEqual(candidates[0].url.absoluteString, "https://subs5.strem.io/en/download/subencoding-stremio-utf8/src-api/file/1952341941")assertEqual(candidates[0].label, "English")assertEqual(candidates[0].language, "eng")assert(SubtitleResolver.isDirectSubtitleFile(candidates[0].url), "Expected Stremio subtitle downloads to be attachable without another resolver hop")}private static func testContentRangeParsing() {160 unmodified linesassertEqual(candidate?.language, "eng")}private static func testSubtitleResolverDownloadJSONReturningLink() async {MockURLProtocol.handlers = ["https://api.opensubtitles.com/api/v1/download/123": (200,14 unmodified lines}private static func testSubtitleResolverRedirectToDirectSubtitle() async {MockURLProtocol.handlers = ["https://api.opensubtitles.com/api/v1/download/redirect": (200,12 unmodified lines}private static func testSubtitleResolverRejectsNonSubtitleAPIResponse() async {MockURLProtocol.handlers = ["https://api.opensubtitles.com/api/v1/download/not-found": (200,15 unmodified lines16171819202122248 unmodified lines271272273274275276277160 unmodified lines43843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248314 unmodified lines49849950050150250350412 unmodified lines51751851952052152252315 unmodified linestestStremioSubtitleDownloadURLParsing()testOpenSubtitlesV3DownloadResponseResolution()testOpenSubtitlesNestedDownloadResponseResolution()await testSubtitleResolverCachesStremioDownloadBody()await testSubtitleResolverDownloadJSONReturningLink()await testSubtitleResolverRedirectToDirectSubtitle()await testSubtitleResolverRejectsNonSubtitleAPIResponse()248 unmodified linesassertEqual(candidates[0].url.absoluteString, "https://subs5.strem.io/en/download/subencoding-stremio-utf8/src-api/file/1952341941")assertEqual(candidates[0].label, "English")assertEqual(candidates[0].language, "eng")assert(!SubtitleResolver.isDirectSubtitleFile(candidates[0].url), "Expected Stremio subtitle downloads to be resolved before VLC attachment")}private static func testContentRangeParsing() {160 unmodified linesassertEqual(candidate?.language, "eng")}private static func testSubtitleResolverCachesStremioDownloadBody() async {let sourceURL = "https://subs5.strem.io/en/download/subencoding-stremio-utf8/src-api/file/1952341941"let subtitleBody = """100:00:01,000 --> 00:00:02,000Hello from Stremio"""MockURLProtocol.handler = nilMockURLProtocol.handlers = [sourceURL: (200,URL(string: sourceURL)!,subtitleBody.data(using: .utf8)!)]let cacheDirectory = FileManager.default.temporaryDirectory.appendingPathComponent("DreamioSubtitleResolverTests-\(UUID().uuidString)", isDirectory: true)defer {try? FileManager.default.removeItem(at: cacheDirectory)}let resolver = SubtitleResolver(session: mockSession(), cacheDirectory: cacheDirectory)let candidate = await resolver.resolve(SubtitleCandidate(url: URL(string: sourceURL)!,label: "English",language: "eng"))assertEqual(candidate?.url.isFileURL, true)assertEqual(candidate?.url.pathExtension, "srt")assertEqual(candidate?.label, "English")assertEqual(candidate?.language, "eng")let cachedBody = try? String(contentsOf: candidate!.url, encoding: .utf8)assertEqual(cachedBody, subtitleBody)}private static func testSubtitleResolverDownloadJSONReturningLink() async {MockURLProtocol.handler = nilMockURLProtocol.handlers = ["https://api.opensubtitles.com/api/v1/download/123": (200,14 unmodified lines}private static func testSubtitleResolverRedirectToDirectSubtitle() async {MockURLProtocol.handler = nilMockURLProtocol.handlers = ["https://api.opensubtitles.com/api/v1/download/redirect": (200,12 unmodified lines}private static func testSubtitleResolverRejectsNonSubtitleAPIResponse() async {MockURLProtocol.handler = nilMockURLProtocol.handlers = ["https://api.opensubtitles.com/api/v1/download/not-found": (200,
Related issues or PRs
Related Beads issue: dreamio-771.
New Changes as of May 25, 2026 at 7:00 PM EDT
Summary of changes
Device logs showed Stremio subtitle downloads returning text, but the strict resolver detector rejected every body as text-without-direct-subtitle. The resolver now accepts plausible plain subtitle text from Stremio download endpoints and caches it as a local .srt file.
Why this change was made
The previous cache step only recognized classic indexed SRT, WebVTT, and ASS signatures. The live Stremio payloads appear to be subtitle-like text without matching that narrow signature, so they never reached VLC. This update keeps strict handling for general URLs while applying a provider-specific fallback for Stremio subtitle downloads.
Code diffs
96 unmodified lines979899100101102103100 unmodified lines20420520620720820921017 unmodified lines2282292302312322332342352367 unmodified lines24424524624724824925025125225396 unmodified lines}private func cacheSubtitleDataIfNeeded(_ data: Data, original: SubtitleCandidate) -> SubtitleCandidate? {guard let subtitleType = Self.subtitleType(in: data) else {return nil}100 unmodified lines}}private static func subtitleType(in data: Data) -> SubtitlePayloadType? {guard !data.isEmpty,let text = String(data: data.prefix(4096), encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines),17 unmodified lines) != nil {return .srt}return nil}private static func logRejected(_ candidate: SubtitleCandidate, responseURL: URL?, data: Data) -> SubtitleCandidate? {#if DEBUGlet responseDescription = responseURL.map { URLRedactor.redactedURLString($0.absoluteString) } ?? "none"7 unmodified lines} else {bodyKind = "unreadable"}print("[DreamioSubtitles] rejected candidate reason=\(bodyKind) url=\(URLRedactor.redactedURLString(candidate.url.absoluteString)) responseURL=\(responseDescription)")#endifreturn nil}}protocol StreamResolving {96 unmodified lines979899100101102103100 unmodified lines20420520620720820921017 unmodified lines2282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582597 unmodified lines26726826927027127227327427527627727827928028128228328428528628796 unmodified lines}private func cacheSubtitleDataIfNeeded(_ data: Data, original: SubtitleCandidate) -> SubtitleCandidate? {guard let subtitleType = Self.subtitleType(in: data, sourceURL: original.url) else {return nil}100 unmodified lines}}private static func subtitleType(in data: Data, sourceURL: URL? = nil) -> SubtitlePayloadType? {guard !data.isEmpty,let text = String(data: data.prefix(4096), encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines),17 unmodified lines) != nil {return .srt}if let sourceURL,isStremioSubtitleDownloadURL(sourceURL),isPlausiblePlainSubtitleText(text) {return .srt}return nil}private static func isPlausiblePlainSubtitleText(_ text: String) -> Bool {let lowercased = text.lowercased()guard !lowercased.hasPrefix("{"),!lowercased.hasPrefix("["),!lowercased.hasPrefix("<!doctype"),!lowercased.hasPrefix("<html"),!lowercased.hasPrefix("<?xml")else {return false}return lowercased.contains("-->")|| lowercased.contains("<font")|| lowercased.contains("{\\")|| lowercased.contains("\\n")|| lowercased.split(whereSeparator: \.isNewline).count > 1}private static func logRejected(_ candidate: SubtitleCandidate, responseURL: URL?, data: Data) -> SubtitleCandidate? {#if DEBUGlet responseDescription = responseURL.map { URLRedactor.redactedURLString($0.absoluteString) } ?? "none"7 unmodified lines} else {bodyKind = "unreadable"}print("[DreamioSubtitles] rejected candidate reason=\(bodyKind) url=\(URLRedactor.redactedURLString(candidate.url.absoluteString)) responseURL=\(responseDescription) preview=\(rejectionPreview(data))")#endifreturn nil}#if DEBUGprivate static func rejectionPreview(_ data: Data) -> String {guard let text = String(data: data.prefix(180), encoding: .utf8) else {return "unreadable"}return text.replacingOccurrences(of: "\n", with: "\\n").replacingOccurrences(of: "\r", with: "\\r")}#endif}protocol StreamResolving {
16 unmodified lines171819202122453 unmodified lines47647747847948048116 unmodified linestestOpenSubtitlesV3DownloadResponseResolution()testOpenSubtitlesNestedDownloadResponseResolution()await testSubtitleResolverCachesStremioDownloadBody()await testSubtitleResolverDownloadJSONReturningLink()await testSubtitleResolverRedirectToDirectSubtitle()await testSubtitleResolverRejectsNonSubtitleAPIResponse()453 unmodified linesassertEqual(cachedBody, subtitleBody)}private static func testSubtitleResolverDownloadJSONReturningLink() async {MockURLProtocol.handler = nilMockURLProtocol.handlers = [16 unmodified lines17181920212223453 unmodified lines47747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651716 unmodified linestestOpenSubtitlesV3DownloadResponseResolution()testOpenSubtitlesNestedDownloadResponseResolution()await testSubtitleResolverCachesStremioDownloadBody()await testSubtitleResolverCachesPlainStremioDownloadBody()await testSubtitleResolverDownloadJSONReturningLink()await testSubtitleResolverRedirectToDirectSubtitle()await testSubtitleResolverRejectsNonSubtitleAPIResponse()453 unmodified linesassertEqual(cachedBody, subtitleBody)}private static func testSubtitleResolverCachesPlainStremioDownloadBody() async {let sourceURL = "https://subs5.strem.io/en/download/subencoding-stremio-utf8/src-api/file/1952341942"let subtitleBody = """00:01.000 --> 00:02.000Plain cue text without an index"""MockURLProtocol.handler = nilMockURLProtocol.handlers = [sourceURL: (200,URL(string: sourceURL)!,subtitleBody.data(using: .utf8)!)]let cacheDirectory = FileManager.default.temporaryDirectory.appendingPathComponent("DreamioSubtitleResolverTests-\(UUID().uuidString)", isDirectory: true)defer {try? FileManager.default.removeItem(at: cacheDirectory)}let resolver = SubtitleResolver(session: mockSession(), cacheDirectory: cacheDirectory)let candidate = await resolver.resolve(SubtitleCandidate(url: URL(string: sourceURL)!,label: "English",language: "eng"))assertEqual(candidate?.url.isFileURL, true)assertEqual(candidate?.url.pathExtension, "srt")let cachedBody = try? String(contentsOf: candidate!.url, encoding: .utf8)assertEqual(cachedBody, subtitleBody)}private static func testSubtitleResolverDownloadJSONReturningLink() async {MockURLProtocol.handler = nilMockURLProtocol.handlers = [
Related issues or PRs
Related Beads issue: dreamio-8oe.
Expected Impact for End-Users
When an MKV stream opens through the real local range buffer, subtitles discovered before playback should still appear in the captions menu and be eligible for auto-selection once VLC exposes the track list. Stremio subtitle downloads should now reach VLC as local subtitle files rather than extensionless provider URLs, including plain cue-text payloads that do not match classic indexed SRT.
Validation
- Ran
git diff --check: passed. - Ran
pod installto restore missing local CocoaPods support files in this worktree. - Ran
xcodebuild -workspace Dreamio.xcworkspace -scheme Dreamio -destination 'generic/platform=iOS Simulator' -quiet build: passed. - Ran
swiftc -parse-as-library -D DEBUG Dreamio/StreamCandidate.swift Dreamio/StreamResolver.swift Dreamio/ProgressiveHTTPRangeCache.swift Tests/StreamResolverTests.swift -o /tmp/StreamResolverTests && /tmp/StreamResolverTests: passed, including the Stremio subtitle cache tests for strict SRT and plain cue text. - The build still reports existing MobileVLCKit warnings about simulator deployment target and a run-script phase, but compilation succeeded.
Issues, Limitations, and Mitigations
This addresses the pre-media attachment race shown in the logs and the follow-on issue where extensionless Stremio subtitle download URLs were accepted by VLC without visible tracks. If a provider returns a compressed archive, a non-UTF-8 payload, HTML/XML, JSON without a direct subtitle link, or a format VLC cannot parse after local caching, that would still need a separate resolver enhancement.
Follow-up Work
- Re-test on device with the South Park stream and confirm the log order changes to queued subtitles followed by
flushing queued subtitlesafteropening mode=local-cache. - Re-test on device and look for
[DreamioSubtitles] cached subtitlefollowed by VLC attachment logs withext=srt,ext=vtt, orext=ass. If rejection still occurs, inspect the newpreview=field in the rejection log. - If tracks still do not appear after local caching, capture whether VLC receives file URLs and whether MobileVLCKit reports any subtitle import errors.