From 2cbe982a478d44b57efac932999692d9d3bf4364 Mon Sep 17 00:00:00 2001 From: dirtydishes Date: Mon, 25 May 2026 10:19:28 -0400 Subject: [PATCH] add captions selection proof logging --- .beads/interactions.jsonl | 1 + .beads/issues.jsonl | 1 + Dreamio/NativePlayerViewController.swift | 27 +++- Dreamio/VLCNativePlaybackBackend.swift | 12 ++ ...-05-25-prove-native-subtitle-pipeline.html | 147 ++++++++++++++++++ 5 files changed, 184 insertions(+), 4 deletions(-) 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 @@
  • If logs show VLC attachment succeeds but track arrays stay empty, test VLC subtitle slave behavior for the resolved file type and timing.
  • +
    +

    New Changes as of 2026-05-25 10:19 AM EDT

    +

    Summary of changes

    +

    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.

    +

    Why this change was made

    +

    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.

    +

    Code diffs

    +

    NativePlayerViewController.swift

    Dreamio/NativePlayerViewController.swift
    -4+23
    379 unmodified lines
    380
    381
    382
    383
    384
    385
    386
    387
    388
    389
    390
    391
    392
    27 unmodified lines
    420
    421
    422
    423
    424
    425
    426
    427
    428
    429
    430
    431
    379 unmodified lines
    +
    private func captionsMenu() -> UIMenu {
    let selectedTrackID = backend.selectedSubtitleTrackID
    let trackActions = SubtitleOptionMapper.options(from: backend.subtitleTracks).map { track in
    UIAction(
    title: track.name,
    state: track.id == selectedTrackID ? .on : .off
    ) { [weak self] _ in
    self?.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.isSeekable
    jumpBackButton.isEnabled = backend.isSeekable
    jumpForwardButton.isEnabled = backend.isSeekable
    captionsButton.isEnabled = !SubtitleOptionMapper.options(from: backend.subtitleTracks).isEmpty
    captionsButton.menu = captionsMenu()
    elapsedLabel.text = PlaybackTimeFormatter.label(for: backend.currentTime)
    remainingLabel.text = "-\(PlaybackTimeFormatter.label(for: backend.remainingTime))"
    if !isScrubbing {
    379 unmodified lines
    380
    381
    382
    383
    384
    385
    386
    387
    388
    389
    390
    391
    392
    393
    394
    395
    396
    397
    398
    399
    400
    401
    402
    403
    404
    405
    406
    27 unmodified lines
    434
    435
    436
    437
    438
    439
    440
    441
    442
    443
    444
    445
    446
    447
    448
    449
    450
    379 unmodified lines
    +
    private func captionsMenu() -> UIMenu {
    let selectedTrackID = backend.selectedSubtitleTrackID
    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
    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()
    }
    }
    +
    27 unmodified lines
    }
    +
    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 = !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 {
    +

    VLCNativePlaybackBackend.swift

    Dreamio/VLCNativePlaybackBackend.swift
    +12
    99 unmodified lines
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    99 unmodified lines
    +
    func selectSubtitleTrack(id: Int32) {
    #if canImport(MobileVLCKit)
    mediaPlayer.currentVideoSubTitleIndex = id
    onSubtitleTracksChange?()
    #endif
    }
    +
    func adjustSubtitleDelay(by seconds: TimeInterval) {
    #if canImport(MobileVLCKit)
    mediaPlayer.currentVideoSubTitleDelay += Int(seconds * 1_000_000)
    onSubtitleTracksChange?()
    #endif
    }
    99 unmodified lines
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    99 unmodified lines
    +
    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
    }
    +

    Related issues or PRs

    +

    Related Beads issue: dreamio-c1m.

    +
    +