diff --git a/.beads/interactions.jsonl b/.beads/interactions.jsonl index e392a3d..fea86c5 100644 --- a/.beads/interactions.jsonl +++ b/.beads/interactions.jsonl @@ -17,3 +17,4 @@ {"id":"int-ddab585f","kind":"field_change","created_at":"2026-05-25T11:07:34.849628Z","actor":"dirtydishes","issue_id":"dreamio-8cz","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Hardened subtitle bridge network observers so non-text Stremio subtitle loads are not touched, and made parser traversal deterministic for metadata preservation."}} {"id":"int-e07aeefe","kind":"field_change","created_at":"2026-05-25T13:50:43.373777Z","actor":"dirtydishes","issue_id":"dreamio-h5q","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Resolved OpenSubtitles V3 API-style subtitle download URLs to direct subtitle files before VLC attachment; added parser/resolver coverage and simulator build validation."}} {"id":"int-c7246990","kind":"field_change","created_at":"2026-05-25T14:07:13.774172Z","actor":"dirtydishes","issue_id":"dreamio-e9p","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Added DEBUG-only subtitle pipeline proof logging and documented validation."}} +{"id":"int-45781aa3","kind":"field_change","created_at":"2026-05-25T14:19:19.141163Z","actor":"dirtydishes","issue_id":"dreamio-c1m","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Added DEBUG-only logs for captions menu actions and VLC subtitle selection results."}} diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 3675ba6..a41aa86 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -11,6 +11,7 @@ {"_type":"issue","id":"dreamio-l68","title":"Add native playback for direct debrid streams","description":"Implement a WKWebView JavaScript bridge that detects direct-file debrid media URLs and routes unsupported containers to a native player backend, initially MobileVLCKit, while preserving normal Stremio Web playback for compatible streams.","status":"closed","priority":1,"issue_type":"feature","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-25T03:13:19Z","created_by":"dirtydishes","updated_at":"2026-05-25T03:20:17Z","started_at":"2026-05-25T03:13:28Z","closed_at":"2026-05-25T03:20:17Z","close_reason":"Implemented native direct-stream bridge, classification, MobileVLCKit backend wiring, CocoaPods workflow docs, and turn documentation. Full iOS build is blocked locally by missing CocoaPods and iPhoneOS SDK.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"dreamio-tnv","title":"Fix iOS bundle identifier install failure","description":"Xcode built Dreamio.app without a valid CFBundleIdentifier, causing device install to fail with CoreDeviceError 3000/3002. Investigate project bundle settings, fix the source configuration, validate the app bundle Info.plist, and document the change.","status":"closed","priority":1,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-25T01:23:00Z","created_by":"dirtydishes","updated_at":"2026-05-25T01:25:36Z","started_at":"2026-05-25T01:23:07Z","closed_at":"2026-05-25T01:25:36Z","close_reason":"Added bundle metadata to Info.plist and validated processed app bundle identifier.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"dreamio-4yn","title":"Build WKWebView MVP shell","description":"Create the first Dreamio MVP implementation: a minimal iOS WKWebView wrapper around hosted Stremio Web, with configuration, launch behavior, diagnostics, and documentation for real-device viability testing.","acceptance_criteria":"App project exists; WKWebView loads hosted Stremio Web; external/new-window navigation is handled; basic diagnostics and manual test documentation exist; quality gates are run or documented.","status":"closed","priority":1,"issue_type":"feature","owner":"dishes@dpdrm.com","created_at":"2026-05-24T14:55:12Z","created_by":"dirtydishes","updated_at":"2026-05-24T14:59:44Z","closed_at":"2026-05-24T14:59:44Z","close_reason":"Implemented the MVP WKWebView iOS shell, added run and validation documentation, and recorded current validation limits.","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"dreamio-c1m","title":"Add captions selection proof logging","description":"Add DEBUG-only logs around the native captions menu and VLC subtitle selection path so subtitle tap actions prove whether the UI fires and whether VLC accepts the selected embedded track index.","status":"closed","priority":2,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-25T14:18:06Z","created_by":"dirtydishes","updated_at":"2026-05-25T14:19:19Z","started_at":"2026-05-25T14:18:11Z","closed_at":"2026-05-25T14:19:19Z","close_reason":"Added DEBUG-only logs for captions menu actions and VLC subtitle selection results.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"dreamio-e9p","title":"Add native subtitle pipeline proof logging","description":"Add DEBUG-only logs across the web bridge, native player, subtitle resolution, and VLC attachment points so the next Xcode run can identify where external subtitles disappear without changing playback behavior.","status":"closed","priority":2,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-25T14:03:18Z","created_by":"dirtydishes","updated_at":"2026-05-25T14:07:14Z","started_at":"2026-05-25T14:03:22Z","closed_at":"2026-05-25T14:07:14Z","close_reason":"Added DEBUG-only subtitle pipeline proof logging and documented validation.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"dreamio-88m","title":"Make caption selection states clearer","description":"The native player caption menu should behave like a simple single-choice menu with None and loaded caption tracks, making the current caption state visually obvious.","status":"closed","priority":2,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-25T10:22:12Z","created_by":"dirtydishes","updated_at":"2026-05-25T10:25:23Z","started_at":"2026-05-25T10:22:48Z","closed_at":"2026-05-25T10:25:23Z","close_reason":"Implemented captions as a single-choice menu with None and selected loaded tracks, updated tests and turn documentation.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"dreamio-7w6","title":"Streamline native player controls","description":"Make the native playback controls take up less screen space while preserving play, seek, jump, captions, and close actions.","status":"closed","priority":2,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-25T10:15:49Z","created_by":"dirtydishes","updated_at":"2026-05-25T10:18:31Z","started_at":"2026-05-25T10:15:59Z","closed_at":"2026-05-25T10:18:31Z","close_reason":"Streamlined native player controls into a compact bottom overlay and validated the simulator build.","dependency_count":0,"dependent_count":0,"comment_count":0} diff --git a/Dreamio/NativePlayerViewController.swift b/Dreamio/NativePlayerViewController.swift index 3a94caf..ccd0c16 100644 --- a/Dreamio/NativePlayerViewController.swift +++ b/Dreamio/NativePlayerViewController.swift @@ -380,13 +380,27 @@ final class NativePlayerViewController: UIViewController { private func captionsMenu() -> UIMenu { let selectedTrackID = backend.selectedSubtitleTrackID - let trackActions = SubtitleOptionMapper.options(from: backend.subtitleTracks).map { track in + let tracks = backend.subtitleTracks + let options = SubtitleOptionMapper.options(from: tracks) +#if DEBUG + print("[DreamioCaptions] build-menu tracks=\(SubtitleDebugFormatter.trackSummary(tracks)) options=\(SubtitleDebugFormatter.trackSummary(options)) selected=\(selectedTrackID)") +#endif + let trackActions = options.map { track in UIAction( title: track.name, state: track.id == selectedTrackID ? .on : .off ) { [weak self] _ in - self?.backend.selectSubtitleTrack(id: track.id) - self?.refreshControls() + guard let self else { + return + } +#if DEBUG + print("[DreamioCaptions] select-request id=\(track.id) name=\(track.name) before=\(self.backend.selectedSubtitleTrackID)") +#endif + self.backend.selectSubtitleTrack(id: track.id) +#if DEBUG + print("[DreamioCaptions] select-result id=\(track.id) after=\(self.backend.selectedSubtitleTrackID) tracks=\(SubtitleDebugFormatter.trackSummary(self.backend.subtitleTracks))") +#endif + self.refreshControls() } } @@ -420,12 +434,17 @@ final class NativePlayerViewController: UIViewController { } private func refreshControls() { + let subtitleTracks = backend.subtitleTracks + let subtitleOptions = SubtitleOptionMapper.options(from: subtitleTracks) playPauseButton.setImage(UIImage(systemName: backend.isPlaying ? "pause.fill" : "play.fill"), for: .normal) scrubber.isEnabled = backend.isSeekable jumpBackButton.isEnabled = backend.isSeekable jumpForwardButton.isEnabled = backend.isSeekable - captionsButton.isEnabled = !SubtitleOptionMapper.options(from: backend.subtitleTracks).isEmpty + captionsButton.isEnabled = !subtitleOptions.isEmpty captionsButton.menu = captionsMenu() +#if DEBUG + print("[DreamioCaptions] refresh enabled=\(captionsButton.isEnabled) tracks=\(SubtitleDebugFormatter.trackSummary(subtitleTracks)) selected=\(backend.selectedSubtitleTrackID)") +#endif elapsedLabel.text = PlaybackTimeFormatter.label(for: backend.currentTime) remainingLabel.text = "-\(PlaybackTimeFormatter.label(for: backend.remainingTime))" if !isScrubbing { diff --git a/Dreamio/VLCNativePlaybackBackend.swift b/Dreamio/VLCNativePlaybackBackend.swift index 84ef193..b1fa064 100644 --- a/Dreamio/VLCNativePlaybackBackend.swift +++ b/Dreamio/VLCNativePlaybackBackend.swift @@ -100,14 +100,26 @@ final class VLCNativePlaybackBackend: NSObject, NativePlaybackBackend { func selectSubtitleTrack(id: Int32) { #if canImport(MobileVLCKit) +#if DEBUG + logSubtitleTracks(reason: "before-select-\(id)") +#endif mediaPlayer.currentVideoSubTitleIndex = id +#if DEBUG + logSubtitleTracks(reason: "after-select-\(id)") +#endif onSubtitleTracksChange?() #endif } func adjustSubtitleDelay(by seconds: TimeInterval) { #if canImport(MobileVLCKit) +#if DEBUG + print("[DreamioVLC] subtitle delay before=\(subtitleDelay) delta=\(seconds)") +#endif mediaPlayer.currentVideoSubTitleDelay += Int(seconds * 1_000_000) +#if DEBUG + print("[DreamioVLC] subtitle delay after=\(subtitleDelay)") +#endif onSubtitleTracksChange?() #endif } diff --git a/docs/turns/2026-05-25-prove-native-subtitle-pipeline.html b/docs/turns/2026-05-25-prove-native-subtitle-pipeline.html index fd877b3..a6e2700 100644 --- a/docs/turns/2026-05-25-prove-native-subtitle-pipeline.html +++ b/docs/turns/2026-05-25-prove-native-subtitle-pipeline.html @@ -383,6 +383,153 @@
Added DEBUG-only proof logs for the captions menu selection path after the latest Xcode run showed VLC exposing an embedded subtitle track but no new logs during selection.
+The previous diagnostics proved external subtitles were absent and VLC exposed an embedded English (SDH) track. The missing proof was whether tapping the captions menu fires a native action and whether VLC accepts the selected track id.
379 unmodified lines38038138238338438538638738838939039139227 unmodified lines420421422423424425426427428429430431379 unmodified lines+private func captionsMenu() -> UIMenu {let selectedTrackID = backend.selectedSubtitleTrackIDlet trackActions = SubtitleOptionMapper.options(from: backend.subtitleTracks).map { track inUIAction(title: track.name,state: track.id == selectedTrackID ? .on : .off) { [weak self] _ inself?.backend.selectSubtitleTrack(id: track.id)self?.refreshControls()}}+27 unmodified lines}+private func refreshControls() {playPauseButton.setImage(UIImage(systemName: backend.isPlaying ? "pause.fill" : "play.fill"), for: .normal)scrubber.isEnabled = backend.isSeekablejumpBackButton.isEnabled = backend.isSeekablejumpForwardButton.isEnabled = backend.isSeekablecaptionsButton.isEnabled = !SubtitleOptionMapper.options(from: backend.subtitleTracks).isEmptycaptionsButton.menu = captionsMenu()elapsedLabel.text = PlaybackTimeFormatter.label(for: backend.currentTime)remainingLabel.text = "-\(PlaybackTimeFormatter.label(for: backend.remainingTime))"if !isScrubbing {379 unmodified lines38038138238338438538638738838939039139239339439539639739839940040140240340440540627 unmodified lines434435436437438439440441442443444445446447448449450379 unmodified lines+private func captionsMenu() -> UIMenu {let selectedTrackID = backend.selectedSubtitleTrackIDlet tracks = backend.subtitleTrackslet options = SubtitleOptionMapper.options(from: tracks)#if DEBUGprint("[DreamioCaptions] build-menu tracks=\(SubtitleDebugFormatter.trackSummary(tracks)) options=\(SubtitleDebugFormatter.trackSummary(options)) selected=\(selectedTrackID)")#endiflet trackActions = options.map { track inUIAction(title: track.name,state: track.id == selectedTrackID ? .on : .off) { [weak self] _ inguard let self else {return}#if DEBUGprint("[DreamioCaptions] select-request id=\(track.id) name=\(track.name) before=\(self.backend.selectedSubtitleTrackID)")#endifself.backend.selectSubtitleTrack(id: track.id)#if DEBUGprint("[DreamioCaptions] select-result id=\(track.id) after=\(self.backend.selectedSubtitleTrackID) tracks=\(SubtitleDebugFormatter.trackSummary(self.backend.subtitleTracks))")#endifself.refreshControls()}}+27 unmodified lines}+private func refreshControls() {let subtitleTracks = backend.subtitleTrackslet subtitleOptions = SubtitleOptionMapper.options(from: subtitleTracks)playPauseButton.setImage(UIImage(systemName: backend.isPlaying ? "pause.fill" : "play.fill"), for: .normal)scrubber.isEnabled = backend.isSeekablejumpBackButton.isEnabled = backend.isSeekablejumpForwardButton.isEnabled = backend.isSeekablecaptionsButton.isEnabled = !subtitleOptions.isEmptycaptionsButton.menu = captionsMenu()#if DEBUGprint("[DreamioCaptions] refresh enabled=\(captionsButton.isEnabled) tracks=\(SubtitleDebugFormatter.trackSummary(subtitleTracks)) selected=\(backend.selectedSubtitleTrackID)")#endifelapsedLabel.text = PlaybackTimeFormatter.label(for: backend.currentTime)remainingLabel.text = "-\(PlaybackTimeFormatter.label(for: backend.remainingTime))"if !isScrubbing {
99 unmodified lines10010110210310410510610710810911011111211399 unmodified lines+func selectSubtitleTrack(id: Int32) {#if canImport(MobileVLCKit)mediaPlayer.currentVideoSubTitleIndex = idonSubtitleTracksChange?()#endif}+func adjustSubtitleDelay(by seconds: TimeInterval) {#if canImport(MobileVLCKit)mediaPlayer.currentVideoSubTitleDelay += Int(seconds * 1_000_000)onSubtitleTracksChange?()#endif}99 unmodified lines10010110210310410510610710810911011111211311411511611711811912012112212312412599 unmodified lines+func selectSubtitleTrack(id: Int32) {#if canImport(MobileVLCKit)#if DEBUGlogSubtitleTracks(reason: "before-select-\(id)")#endifmediaPlayer.currentVideoSubTitleIndex = id#if DEBUGlogSubtitleTracks(reason: "after-select-\(id)")#endifonSubtitleTracksChange?()#endif}+func adjustSubtitleDelay(by seconds: TimeInterval) {#if canImport(MobileVLCKit)#if DEBUGprint("[DreamioVLC] subtitle delay before=\(subtitleDelay) delta=\(seconds)")#endifmediaPlayer.currentVideoSubTitleDelay += Int(seconds * 1_000_000)#if DEBUGprint("[DreamioVLC] subtitle delay after=\(subtitleDelay)")#endifonSubtitleTracksChange?()#endif}
Related Beads issue: dreamio-c1m.