Dreamio turn document

# Throttle VLC subtitle reapply

Reduced noisy VLC subtitle reapply behavior so repeated buffering notifications no longer keep writing the same already-selected subtitle track.

May 25, 2026 Issue dreamio-h5n Native VLC playback

## Summary

Dreamio was auto-selecting embedded VLC subtitle tracks correctly, but VLC buffering callbacks repeatedly reapplied the same track while it was already selected. The change keeps recovery behavior for real subtitle-selection drift and startup timing, while suppressing repeated no-op reapply writes from player state changes.

## Changes Made

- Added a selected-track guard inside `reapplyAutoSelectedSubtitleTrackIfNeeded`.
- Kept delayed startup reapply attempts visible by passing `shouldLogNoop: true` for the 0.3, 1.0, 2.0, and 4.0 second retries.
- Changed debug logging to label reapply events as `action=confirm` for delayed no-op confirmations or `action=recover` when VLC had drifted away from the intended subtitle.

## Context

The diagnostic log showed VLC auto-selecting `English (SDH)`, then repeatedly logging `reapply subtitle` during buffering even though `selected=3` never changed. The external OpenSubtitles load failure was non-critical in that trace, and the working subtitle came from the MKV itself.

## Important Implementation Details

The VLC state delegate still calls the reapply helper during `.buffering` and `.playing`. The helper now checks `mediaPlayer.currentVideoSubTitleIndex` before writing. If the intended auto-selected track is already active, state-driven calls return without touching VLC or logging. Delayed startup retries intentionally keep their confirmation logging because those are bounded and useful for diagnosing timing-sensitive subtitle attachment.

## Relevant Diff Snippets

Dreamio/VLCNativePlaybackBackend.swift
-3+12

    283 unmodified lines284285286287288289290291292293294295296297298299300301302303304283 unmodified lines    private func scheduleAutoSubtitleSelectionReapply(trackID: Int32) {        [0.3, 1.0, 2.0, 4.0].forEach { delay in            DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in                self?.reapplyAutoSelectedSubtitleTrackIfNeeded(reason: "delayed-\(String(format: "%.1f", delay))")            }        }    }
        private func reapplyAutoSelectedSubtitleTrackIfNeeded(reason: String) {        guard !didUserSelectSubtitleTrack,              let trackID = autoSelectedSubtitleTrackID,              subtitleTracks.contains(where: { $0.id == trackID }) else {            return        }
            mediaPlayer.currentVideoSubTitleIndex = trackID#if DEBUG        print("[DreamioVLC] reapply subtitle id=\(trackID) reason=\(reason) selected=\(mediaPlayer.currentVideoSubTitleIndex)")#endif    }#endif283 unmodified lines284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313283 unmodified lines    private func scheduleAutoSubtitleSelectionReapply(trackID: Int32) {        [0.3, 1.0, 2.0, 4.0].forEach { delay in            DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in                self?.reapplyAutoSelectedSubtitleTrackIfNeeded(                    reason: "delayed-\(String(format: "%.1f", delay))",                    shouldLogNoop: true                )            }        }    }
        private func reapplyAutoSelectedSubtitleTrackIfNeeded(reason: String, shouldLogNoop: Bool = false) {        guard !didUserSelectSubtitleTrack,              let trackID = autoSelectedSubtitleTrackID,              subtitleTracks.contains(where: { $0.id == trackID }) else {            return        }
            let selectedTrackID = mediaPlayer.currentVideoSubTitleIndex        guard selectedTrackID != trackID || shouldLogNoop else {            return        }
            mediaPlayer.currentVideoSubTitleIndex = trackID#if DEBUG        let action = selectedTrackID == trackID ? "confirm" : "recover"        print("[DreamioVLC] reapply subtitle id=\(trackID) reason=\(reason) action=\(action) selected=\(mediaPlayer.currentVideoSubTitleIndex)")#endif    }#endif

## Expected Impact for End-Users

Playback should behave the same when the embedded subtitle is successfully auto-selected. Debug logs should become much quieter during buffering, making real subtitle failures easier to spot. If VLC drops the subtitle selection, Dreamio will still reapply the intended auto-selected track.

## Validation

- Ran `xcodebuild -workspace Dreamio.xcworkspace -scheme Dreamio -destination 'generic/platform=iOS Simulator' build`.
- The build succeeded. Xcode emitted the existing MobileVLCKit run-script output warning and AppIntents metadata skip warning, neither related to this change.

## Issues, Limitations, and Mitigations

- This was validated with a simulator build, not a live Stremio playback session against the exact South Park stream from the provided log.
- The OpenSubtitles external subtitle load failure is separate. This change addresses VLC reapply spam after embedded subtitle auto-selection, not remote subtitle download reliability.
- Delayed retry logs remain by design, but they are bounded to four scheduled checks.

## Follow-up Work

No follow-up issue is required for this specific buffering log noise. A separate issue would be appropriate if external OpenSubtitles subtitle downloads still fail for streams without embedded subtitles.