From 1c34000027644af8c6b7dd3f6c6061e4d2db22d1 Mon Sep 17 00:00:00 2001 From: dirtydishes Date: Mon, 25 May 2026 19:05:44 -0400 Subject: [PATCH] add queued subtitles before vlc playback --- Dreamio/VLCNativePlaybackBackend.swift | 36 ++++++- ...queue-vlc-subtitles-until-media-start.html | 96 ++++++++++++++++++- 2 files changed, 123 insertions(+), 9 deletions(-) diff --git a/Dreamio/VLCNativePlaybackBackend.swift b/Dreamio/VLCNativePlaybackBackend.swift index b65bc8f..4dcb69a 100644 --- a/Dreamio/VLCNativePlaybackBackend.swift +++ b/Dreamio/VLCNativePlaybackBackend.swift @@ -355,6 +355,7 @@ final class VLCNativePlaybackBackend: NSObject, NativePlaybackBackend { media.addOption(":http-reconnect") addRemoteHeaders(to: media, request: request) } + let queuedSubtitleCount = addQueuedSubtitleSlaves(to: media) mediaPlayer.media = media #if DEBUG @@ -362,7 +363,9 @@ final class VLCNativePlaybackBackend: NSObject, NativePlaybackBackend { #endif mediaPlayer.play() hasStartedMedia = true - flushPendingSubtitleCandidates() + if queuedSubtitleCount > 0 { + scheduleSubtitleTrackRefreshes(attachedCount: queuedSubtitleCount) + } } private func addRemoteHeaders(to media: VLCMedia, request: NativePlaybackRequest) { @@ -416,6 +419,14 @@ final class VLCNativePlaybackBackend: NSObject, NativePlaybackBackend { guard attachedCount > 0 else { return attachedCount } + scheduleSubtitleTrackRefreshes(attachedCount: attachedCount) + return attachedCount + } + + private func scheduleSubtitleTrackRefreshes(attachedCount: Int) { + guard attachedCount > 0 else { + return + } [0.2, 0.6, 1.0, 2.0, 4.0].forEach { delay in DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in self?.selectPreferredSubtitleTrackIfNeeded(reason: "delayed-refresh-\(String(format: "%.1f", delay))") @@ -428,7 +439,6 @@ final class VLCNativePlaybackBackend: NSObject, NativePlaybackBackend { self?.onSubtitleTracksChange?() } } - return attachedCount } private func queuePendingSubtitleCandidates(_ candidates: [SubtitleCandidate]) -> Int { @@ -447,18 +457,34 @@ final class VLCNativePlaybackBackend: NSObject, NativePlaybackBackend { return queuedCount } - private func flushPendingSubtitleCandidates() { + private func addQueuedSubtitleSlaves(to media: VLCMedia) -> Int { guard !pendingSubtitleCandidates.isEmpty else { - return + return 0 } let candidates = pendingSubtitleCandidates pendingSubtitleCandidates.removeAll() pendingSubtitleURLs.removeAll() + var addedCount = 0 + let baselineTrackIDs = Set(rawSubtitleTracks().filter { $0.id >= 0 }.map(\.id)) #if DEBUG print("[DreamioVLC] flushing queued subtitles count=\(candidates.count)") #endif - _ = attachSubtitles(candidates) + candidates.forEach { candidate in + guard !attachedSubtitleURLs.contains(candidate.url) else { + return + } + attachedSubtitleURLs.insert(candidate.url) + externalSubtitleBaselineTrackIDs.formUnion(baselineTrackIDs) + hasPendingExternalSubtitleSelection = true + pendingExternalSubtitleDisplayNames.append(SubtitleDisplayName.displayName(for: candidate)) + media.addOption(":input-slave=\(candidate.url.absoluteString)") + addedCount += 1 +#if DEBUG + print("[DreamioVLC] queued subtitle slave subtitle=\(URLRedactor.redactedURLString(candidate.url.absoluteString)) label=\(candidate.label) language=\(candidate.language ?? "unknown") ext=\(candidate.url.pathExtension.lowercased())") +#endif + } + return addedCount } private func rawSubtitleTracks() -> [SubtitleTrack] { 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 index 9132542..f3f3688 100644 --- 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 @@ -503,9 +503,97 @@

Related Beads issue: dreamio-8oe.

+ +
+

New Changes as of May 25, 2026 at 7:05 PM EDT

+

Summary of changes

+

Device logs showed local cached subtitle files reaching MobileVLCKit, but addPlaybackSlave still did not expose any subtitle tracks. The VLC backend now adds queued subtitle files to the VLCMedia with :input-slave=... before playback starts, and keeps addPlaybackSlave only for subtitles that arrive after media startup.

+

Why this change was made

+

The bundled libVLC headers note that media slaves should be added before parsing or playback. The previous flow waited until after mediaPlayer.play(), which MobileVLCKit accepted without turning into visible tracks.

+

Code diffs

+
Dreamio/VLCNativePlaybackBackend.swift
-5+31
354 unmodified lines
355
356
357
358
359
360
1 unmodified line
362
363
364
365
366
367
368
47 unmodified lines
416
417
418
419
420
421
6 unmodified lines
428
429
430
431
432
433
434
12 unmodified lines
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
354 unmodified lines
media.addOption(":http-reconnect")
addRemoteHeaders(to: media, request: request)
}
+
mediaPlayer.media = media
#if DEBUG
1 unmodified line
#endif
mediaPlayer.play()
hasStartedMedia = true
flushPendingSubtitleCandidates()
}
+
private func addRemoteHeaders(to media: VLCMedia, request: NativePlaybackRequest) {
47 unmodified lines
guard attachedCount > 0 else {
return attachedCount
}
[0.2, 0.6, 1.0, 2.0, 4.0].forEach { delay in
DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in
self?.selectPreferredSubtitleTrackIfNeeded(reason: "delayed-refresh-\(String(format: "%.1f", delay))")
6 unmodified lines
self?.onSubtitleTracksChange?()
}
}
return attachedCount
}
+
private func queuePendingSubtitleCandidates(_ candidates: [SubtitleCandidate]) -> Int {
12 unmodified lines
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] {
354 unmodified lines
355
356
357
358
359
360
361
1 unmodified line
363
364
365
366
367
368
369
370
371
47 unmodified lines
419
420
421
422
423
424
425
426
427
428
429
430
431
432
6 unmodified lines
439
440
441
442
443
444
12 unmodified lines
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
354 unmodified lines
media.addOption(":http-reconnect")
addRemoteHeaders(to: media, request: request)
}
let queuedSubtitleCount = addQueuedSubtitleSlaves(to: media)
+
mediaPlayer.media = media
#if DEBUG
1 unmodified line
#endif
mediaPlayer.play()
hasStartedMedia = true
if queuedSubtitleCount > 0 {
scheduleSubtitleTrackRefreshes(attachedCount: queuedSubtitleCount)
}
}
+
private func addRemoteHeaders(to media: VLCMedia, request: NativePlaybackRequest) {
47 unmodified lines
guard attachedCount > 0 else {
return attachedCount
}
scheduleSubtitleTrackRefreshes(attachedCount: attachedCount)
return attachedCount
}
+
private func scheduleSubtitleTrackRefreshes(attachedCount: Int) {
guard attachedCount > 0 else {
return
}
[0.2, 0.6, 1.0, 2.0, 4.0].forEach { delay in
DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in
self?.selectPreferredSubtitleTrackIfNeeded(reason: "delayed-refresh-\(String(format: "%.1f", delay))")
6 unmodified lines
self?.onSubtitleTracksChange?()
}
}
}
+
private func queuePendingSubtitleCandidates(_ candidates: [SubtitleCandidate]) -> Int {
12 unmodified lines
return queuedCount
}
+
private func addQueuedSubtitleSlaves(to media: VLCMedia) -> Int {
guard !pendingSubtitleCandidates.isEmpty else {
return 0
}
+
let candidates = pendingSubtitleCandidates
pendingSubtitleCandidates.removeAll()
pendingSubtitleURLs.removeAll()
var addedCount = 0
let baselineTrackIDs = Set(rawSubtitleTracks().filter { $0.id >= 0 }.map(\.id))
#if DEBUG
print("[DreamioVLC] flushing queued subtitles count=\(candidates.count)")
#endif
candidates.forEach { candidate in
guard !attachedSubtitleURLs.contains(candidate.url) else {
return
}
attachedSubtitleURLs.insert(candidate.url)
externalSubtitleBaselineTrackIDs.formUnion(baselineTrackIDs)
hasPendingExternalSubtitleSelection = true
pendingExternalSubtitleDisplayNames.append(SubtitleDisplayName.displayName(for: candidate))
media.addOption(":input-slave=\(candidate.url.absoluteString)")
addedCount += 1
#if DEBUG
print("[DreamioVLC] queued subtitle slave subtitle=\(URLRedactor.redactedURLString(candidate.url.absoluteString)) label=\(candidate.label) language=\(candidate.language ?? "unknown") ext=\(candidate.url.pathExtension.lowercased())")
#endif
}
return addedCount
}
+
private func rawSubtitleTracks() -> [SubtitleTrack] {
+

Related issues or PRs

+

Related Beads issue: dreamio-3aq.

+
+

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.

+

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. Queued subtitles are now registered with VLC before playback starts.

@@ -513,7 +601,7 @@ @@ -521,14 +609,14 @@

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.

+

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. If MobileVLCKit still refuses :input-slave subtitle files on iOS, the next mitigation would be a different subtitle rendering path outside VLC track import.

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.
  • -
  • Re-test on device and look for [DreamioSubtitles] cached subtitle followed by VLC attachment logs with ext=srt, ext=vtt, or ext=ass. If rejection still occurs, inspect the new preview= field in the rejection log.
  • +
  • Re-test on device and look for [DreamioSubtitles] cached subtitle followed by VLC attachment logs with ext=srt, ext=vtt, or ext=ass. If rejection still occurs, inspect the new preview= field in the rejection log. Also verify the next VLC log shows queued subtitle slave before opening mode=local-cache.
  • If tracks still do not appear after local caching, capture whether VLC receives file URLs and whether MobileVLCKit reports any subtitle import errors.