diff --git a/Dreamio/VLCNativePlaybackBackend.swift b/Dreamio/VLCNativePlaybackBackend.swift index 1760bb8..b65bc8f 100644 --- a/Dreamio/VLCNativePlaybackBackend.swift +++ b/Dreamio/VLCNativePlaybackBackend.swift @@ -28,6 +28,9 @@ final class VLCNativePlaybackBackend: NSObject, NativePlaybackBackend { private var lastLoggedState: String? private var lastBufferingLogTime: Date? private var attachedSubtitleURLs = Set() + private var pendingSubtitleCandidates: [SubtitleCandidate] = [] + private var pendingSubtitleURLs = Set() + private var hasStartedMedia = false private var didAutoSelectSubtitleTrack = false private var didUserSelectSubtitleTrack = false private var autoSelectedSubtitleTrackID: Int32? @@ -54,6 +57,9 @@ final class VLCNativePlaybackBackend: NSObject, NativePlaybackBackend { #if canImport(MobileVLCKit) playbackStartupTask?.cancel() attachedSubtitleURLs.removeAll() + pendingSubtitleCandidates.removeAll() + pendingSubtitleURLs.removeAll() + hasStartedMedia = false didAutoSelectSubtitleTrack = false didUserSelectSubtitleTrack = false autoSelectedSubtitleTrackID = nil @@ -355,6 +361,8 @@ final class VLCNativePlaybackBackend: NSObject, NativePlaybackBackend { print("[DreamioVLC] opening mode=\(playbackMode) cachingMs=\(cachingMilliseconds) url=\(URLRedactor.redactedURLString(url.absoluteString))") #endif mediaPlayer.play() + hasStartedMedia = true + flushPendingSubtitleCandidates() } private func addRemoteHeaders(to media: VLCMedia, request: NativePlaybackRequest) { @@ -371,6 +379,16 @@ final class VLCNativePlaybackBackend: NSObject, NativePlaybackBackend { } private func attachSubtitles(_ candidates: [SubtitleCandidate]) -> Int { + guard hasStartedMedia else { + let queued = queuePendingSubtitleCandidates(candidates) +#if DEBUG + if !candidates.isEmpty { + print("[DreamioVLC] subtitle candidates=\(candidates.count) queued=\(queued) reason=media-not-started") + } +#endif + return queued + } + var attachedCount = 0 var duplicateCount = 0 let baselineTrackIDs = Set(rawSubtitleTracks().filter { $0.id >= 0 }.map(\.id)) @@ -413,6 +431,36 @@ final class VLCNativePlaybackBackend: NSObject, NativePlaybackBackend { return attachedCount } + private func queuePendingSubtitleCandidates(_ candidates: [SubtitleCandidate]) -> Int { + var queuedCount = 0 + candidates.forEach { candidate in + guard !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 = pendingSubtitleCandidates + pendingSubtitleCandidates.removeAll() + pendingSubtitleURLs.removeAll() +#if DEBUG + print("[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] ?? [] diff --git a/docs/turns/2026-05-25-queue-vlc-subtitles-until-media-start.html b/docs/turns/2026-05-25-queue-vlc-subtitles-until-media-start.html new file mode 100644 index 0000000..024f87c --- /dev/null +++ b/docs/turns/2026-05-25-queue-vlc-subtitles-until-media-start.html @@ -0,0 +1,206 @@ + + + + + + Queue VLC Subtitles Until Media Start + + + +
+
+
Dreamio turn record
+

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.

+
+ Issue dreamio-qyh + May 25, 2026 + Native playback +
+
+ +
+

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.

+
Dreamio/VLCNativePlaybackBackend.swift
+48
27 unmodified lines
28
29
30
31
32
33
20 unmodified lines
54
55
56
57
58
59
295 unmodified lines
355
356
357
358
359
360
10 unmodified lines
371
372
373
374
375
376
36 unmodified lines
413
414
415
416
417
418
27 unmodified lines
private var lastLoggedState: String?
private var lastBufferingLogTime: Date?
private var attachedSubtitleURLs = Set<URL>()
private var didAutoSelectSubtitleTrack = false
private var didUserSelectSubtitleTrack = false
private var autoSelectedSubtitleTrackID: Int32?
20 unmodified lines
#if canImport(MobileVLCKit)
playbackStartupTask?.cancel()
attachedSubtitleURLs.removeAll()
didAutoSelectSubtitleTrack = false
didUserSelectSubtitleTrack = false
autoSelectedSubtitleTrackID = nil
295 unmodified lines
print("[DreamioVLC] opening mode=\(playbackMode) cachingMs=\(cachingMilliseconds) url=\(URLRedactor.redactedURLString(url.absoluteString))")
#endif
mediaPlayer.play()
}
+
private func addRemoteHeaders(to media: VLCMedia, request: NativePlaybackRequest) {
10 unmodified lines
}
+
private func attachSubtitles(_ candidates: [SubtitleCandidate]) -> Int {
var attachedCount = 0
var duplicateCount = 0
let baselineTrackIDs = Set(rawSubtitleTracks().filter { $0.id >= 0 }.map(\.id))
36 unmodified lines
return attachedCount
}
+
private func rawSubtitleTracks() -> [SubtitleTrack] {
let names = mediaPlayer.videoSubTitlesNames as? [String] ?? []
let indexes = mediaPlayer.videoSubTitlesIndexes as? [NSNumber] ?? []
27 unmodified lines
28
29
30
31
32
33
34
35
36
20 unmodified lines
57
58
59
60
61
62
63
64
65
295 unmodified lines
361
362
363
364
365
366
367
368
10 unmodified lines
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
36 unmodified lines
431
432
433
434
435
436
437
438
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
27 unmodified lines
private var lastLoggedState: String?
private var lastBufferingLogTime: Date?
private var attachedSubtitleURLs = Set<URL>()
private var pendingSubtitleCandidates: [SubtitleCandidate] = []
private var pendingSubtitleURLs = Set<URL>()
private var hasStartedMedia = false
private var didAutoSelectSubtitleTrack = false
private var didUserSelectSubtitleTrack = false
private var autoSelectedSubtitleTrackID: Int32?
20 unmodified lines
#if canImport(MobileVLCKit)
playbackStartupTask?.cancel()
attachedSubtitleURLs.removeAll()
pendingSubtitleCandidates.removeAll()
pendingSubtitleURLs.removeAll()
hasStartedMedia = false
didAutoSelectSubtitleTrack = false
didUserSelectSubtitleTrack = false
autoSelectedSubtitleTrackID = nil
295 unmodified lines
print("[DreamioVLC] opening mode=\(playbackMode) cachingMs=\(cachingMilliseconds) url=\(URLRedactor.redactedURLString(url.absoluteString))")
#endif
mediaPlayer.play()
hasStartedMedia = true
flushPendingSubtitleCandidates()
}
+
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 DEBUG
if !candidates.isEmpty {
print("[DreamioVLC] subtitle candidates=\(candidates.count) queued=\(queued) reason=media-not-started")
}
#endif
return queued
}
+
var attachedCount = 0
var duplicateCount = 0
let baselineTrackIDs = Set(rawSubtitleTracks().filter { $0.id >= 0 }.map(\.id))
36 unmodified lines
return attachedCount
}
+
private func queuePendingSubtitleCandidates(_ candidates: [SubtitleCandidate]) -> Int {
var queuedCount = 0
candidates.forEach { candidate in
guard !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 = pendingSubtitleCandidates
pendingSubtitleCandidates.removeAll()
pendingSubtitleURLs.removeAll()
#if DEBUG
print("[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] ?? []
+
+ +
+

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.

+
+ +
+

Validation

+
    +
  • Ran git diff --check: passed.
  • +
  • Ran pod install to restore missing local CocoaPods support files in this worktree.
  • +
  • Ran xcodebuild -workspace Dreamio.xcworkspace -scheme Dreamio -destination 'generic/platform=iOS Simulator' -quiet build: passed.
  • +
  • The build still reports the existing MobileVLCKit run-script output warning, but compilation succeeded.
  • +
+
+ +
+

Issues, Limitations, and Mitigations

+

This addresses the pre-media attachment race shown in the logs. It does not prove every remote subtitle download URL will be accepted by VLC after attachment; if a provider returns an unsupported payload or requires extra headers, that would need a separate resolver-level fix.

+
+ +
+

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 subtitles after opening mode=local-cache.
  • +
  • If tracks still do not appear, capture the post-flush VLC logs to distinguish a timing issue from a subtitle URL/content issue.
  • +
+
+
+ +