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.
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.
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.
354 unmodified lines3553563573583593601 unmodified line36236336436536636736847 unmodified lines4164174184194204216 unmodified lines42842943043143243343412 unmodified lines447448449450451452453454455456457458459460461462463464354 unmodified linesmedia.addOption(":http-reconnect")addRemoteHeaders(to: media, request: request)}+mediaPlayer.media = media#if DEBUG1 unmodified line#endifmediaPlayer.play()hasStartedMedia = trueflushPendingSubtitleCandidates()}+private func addRemoteHeaders(to media: VLCMedia, request: NativePlaybackRequest) {47 unmodified linesguard attachedCount > 0 else {return attachedCount}[0.2, 0.6, 1.0, 2.0, 4.0].forEach { delay inDispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] inself?.selectPreferredSubtitleTrackIfNeeded(reason: "delayed-refresh-\(String(format: "%.1f", delay))")6 unmodified linesself?.onSubtitleTracksChange?()}}return attachedCount}+private func queuePendingSubtitleCandidates(_ candidates: [SubtitleCandidate]) -> Int {12 unmodified linesreturn 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] {354 unmodified lines3553563573583593603611 unmodified line36336436536636736836937037147 unmodified lines4194204214224234244254264274284294304314326 unmodified lines43944044144244344412 unmodified lines457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490354 unmodified linesmedia.addOption(":http-reconnect")addRemoteHeaders(to: media, request: request)}let queuedSubtitleCount = addQueuedSubtitleSlaves(to: media)+mediaPlayer.media = media#if DEBUG1 unmodified line#endifmediaPlayer.play()hasStartedMedia = trueif queuedSubtitleCount > 0 {scheduleSubtitleTrackRefreshes(attachedCount: queuedSubtitleCount)}}+private func addRemoteHeaders(to media: VLCMedia, request: NativePlaybackRequest) {47 unmodified linesguard 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 inDispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] inself?.selectPreferredSubtitleTrackIfNeeded(reason: "delayed-refresh-\(String(format: "%.1f", delay))")6 unmodified linesself?.onSubtitleTracksChange?()}}}+private func queuePendingSubtitleCandidates(_ candidates: [SubtitleCandidate]) -> Int {12 unmodified linesreturn queuedCount}+private func addQueuedSubtitleSlaves(to media: VLCMedia) -> Int {guard !pendingSubtitleCandidates.isEmpty else {return 0}+let candidates = pendingSubtitleCandidatespendingSubtitleCandidates.removeAll()pendingSubtitleURLs.removeAll()var addedCount = 0let baselineTrackIDs = Set(rawSubtitleTracks().filter { $0.id >= 0 }.map(\.id))#if DEBUGprint("[DreamioVLC] flushing queued subtitles count=\(candidates.count)")#endifcandidates.forEach { candidate inguard !attachedSubtitleURLs.contains(candidate.url) else {return}attachedSubtitleURLs.insert(candidate.url)externalSubtitleBaselineTrackIDs.formUnion(baselineTrackIDs)hasPendingExternalSubtitleSelection = truependingExternalSubtitleDisplayNames.append(SubtitleDisplayName.displayName(for: candidate))media.addOption(":input-slave=\(candidate.url.absoluteString)")addedCount += 1#if DEBUGprint("[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 Beads issue: dreamio-3aq.
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.
git diff --check: passed.pod install to restore missing local CocoaPods support files in this worktree.xcodebuild -workspace Dreamio.xcworkspace -scheme Dreamio -destination 'generic/platform=iOS Simulator' -quiet build: passed.xcodebuild -workspace Dreamio.xcworkspace -scheme Dreamio -destination 'generic/platform=iOS Simulator' -quiet build: passed after moving queued subtitles to pre-playback media options.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.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.
flushing queued subtitles after opening mode=local-cache.[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.[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.