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

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

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