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] ?? []
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 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. - 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 subtitlesafteropening 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.