diff --git a/.beads/interactions.jsonl b/.beads/interactions.jsonl index 654e9c5..b5a0784 100644 --- a/.beads/interactions.jsonl +++ b/.beads/interactions.jsonl @@ -22,3 +22,4 @@ {"id":"int-26b872a1","kind":"field_change","created_at":"2026-05-25T14:31:46.83464Z","actor":"dirtydishes","issue_id":"dreamio-ese","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Added subtitle-shaped fetch/XHR inspection diagnostics and validated the build."}} {"id":"int-4e095d3f","kind":"field_change","created_at":"2026-05-25T14:38:21.968713Z","actor":"dirtydishes","issue_id":"dreamio-djc","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Auto-select the first discovered VLC subtitle track when playback is still disabled, while preserving manual caption choices."}} {"id":"int-96629c65","kind":"field_change","created_at":"2026-05-25T14:45:38.521113Z","actor":"dirtydishes","issue_id":"dreamio-ppj","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Re-applied the auto-selected VLC subtitle track after stream discovery and playback state changes to harden rendering timing."}} +{"id":"int-027cec57","kind":"field_change","created_at":"2026-05-25T14:51:44.599319Z","actor":"dirtydishes","issue_id":"dreamio-3xi","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Captured OpenSubtitles V3 subtitle URLs from browser track elements and textTracks so they can be forwarded to native playback."}} diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 4986ec7..04ca83c 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -1,4 +1,5 @@ {"_type":"issue","id":"dreamio-8cz","title":"fix stremio external subtitle loading regression","description":"After adding late subtitle forwarding for native playback, Stremio external subtitle loading is failing. Investigate the injected bridge and native subtitle forwarding path, then adjust behavior so Stremio can still load external subtitles while native playback receives late candidates.","status":"closed","priority":0,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-25T11:05:42Z","created_by":"dirtydishes","updated_at":"2026-05-25T11:07:35Z","started_at":"2026-05-25T11:05:55Z","closed_at":"2026-05-25T11:07:35Z","close_reason":"Hardened subtitle bridge network observers so non-text Stremio subtitle loads are not touched, and made parser traversal deterministic for metadata preservation.","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"dreamio-3xi","title":"Capture browser text tracks for OpenSubtitles V3","description":"OpenSubtitles V3 subtitles can be attached to the Stremio web player as HTML track/textTrack entries rather than appearing in the initial stream candidate. Extend the web bridge to inspect track elements and textTracks so external subtitles can be forwarded to native playback.","status":"closed","priority":1,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-25T14:49:50Z","created_by":"dirtydishes","updated_at":"2026-05-25T14:51:45Z","started_at":"2026-05-25T14:49:52Z","closed_at":"2026-05-25T14:51:45Z","close_reason":"Captured OpenSubtitles V3 subtitle URLs from browser track elements and textTracks so they can be forwarded to native playback.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"dreamio-ppj","title":"Reapply VLC embedded subtitle selection after track discovery","description":"Device logs show VLC eventually exposes and selects the embedded English SDH subtitle track, but subtitles still do not render. Investigate and harden the VLC selection timing so embedded tracks are selected after discovery is stable.","status":"closed","priority":1,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-25T14:44:08Z","created_by":"dirtydishes","updated_at":"2026-05-25T14:45:38Z","started_at":"2026-05-25T14:44:18Z","closed_at":"2026-05-25T14:45:38Z","close_reason":"Re-applied the auto-selected VLC subtitle track after stream discovery and playback state changes to harden rendering timing.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"dreamio-djc","title":"Auto-select embedded VLC subtitle tracks","description":"VLC discovers embedded MKV subtitle tracks after playback starts, but Dreamio leaves subtitles disabled when no external candidates were provided. Add automatic selection for the first selectable VLC subtitle track while preserving manual caption choices.","status":"closed","priority":1,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-25T14:36:11Z","created_by":"dirtydishes","updated_at":"2026-05-25T14:38:22Z","started_at":"2026-05-25T14:36:17Z","closed_at":"2026-05-25T14:38:22Z","close_reason":"Auto-select the first discovered VLC subtitle track when playback is still disabled, while preserving manual caption choices.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"dreamio-ese","title":"Discover Stremio external subtitle payloads","description":"Extend and instrument the injected web subtitle discovery path so Stremio/OpenSubtitles addon responses can be captured when native playback only sees embedded VLC subtitle tracks.","status":"closed","priority":1,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-25T14:29:57Z","created_by":"dirtydishes","updated_at":"2026-05-25T14:31:47Z","started_at":"2026-05-25T14:30:03Z","closed_at":"2026-05-25T14:31:47Z","close_reason":"Added subtitle-shaped fetch/XHR inspection diagnostics and validated the build.","dependency_count":0,"dependent_count":0,"comment_count":0} diff --git a/Dreamio/DreamioWebViewController.swift b/Dreamio/DreamioWebViewController.swift index c38ae55..53d417f 100644 --- a/Dreamio/DreamioWebViewController.swift +++ b/Dreamio/DreamioWebViewController.swift @@ -199,6 +199,37 @@ final class DreamioWebViewController: UIViewController { postSubtitleCandidates([candidate]); }; + const inspectTrack = (track) => { + if (!track) { + return; + } + if (track instanceof HTMLTrackElement) { + addSubtitleCandidate({ + url: track.src || track.getAttribute("src") || "", + label: track.label || track.srclang || "External Subtitle", + language: track.srclang || "" + }); + return; + } + const source = track.src || track.url || ""; + if (source) { + addSubtitleCandidate({ + url: source, + label: track.label || track.language || track.kind || "External Subtitle", + language: track.language || "" + }); + } + }; + + const inspectTextTracks = (media) => { + try { + Array.from(media.textTracks || []).forEach(inspectTrack); + } catch (_) {} + try { + media.querySelectorAll("track").forEach(inspectTrack); + } catch (_) {} + }; + const postSubtitleInspection = (source, url, beforeCount, afterCount, payloadLength) => { if (afterCount > beforeCount) { return; @@ -307,11 +338,17 @@ final class DreamioWebViewController: UIViewController { if (!node) { return; } + if (node instanceof HTMLTrackElement) { + inspectTrack(node); + } if (node instanceof HTMLVideoElement || node instanceof HTMLSourceElement) { postCandidate(node.currentSrc || node.src || node.getAttribute("src"), node); } if (node.querySelectorAll) { - node.querySelectorAll("video, source").forEach(inspectMedia); + node.querySelectorAll("video, source, track").forEach(inspectMedia); + } + if (node instanceof HTMLVideoElement) { + inspectTextTracks(node); } }; @@ -337,10 +374,34 @@ final class DreamioWebViewController: UIViewController { }); } + const trackSrcDescriptor = Object.getOwnPropertyDescriptor(HTMLTrackElement.prototype, "src"); + if (trackSrcDescriptor && trackSrcDescriptor.set) { + Object.defineProperty(HTMLTrackElement.prototype, "src", { + get: trackSrcDescriptor.get, + set(value) { + addSubtitleCandidate({ + url: value, + label: this.label || this.srclang || "External Subtitle", + language: this.srclang || "" + }); + return trackSrcDescriptor.set.call(this, value); + } + }); + } + const originalSetAttribute = Element.prototype.setAttribute; Element.prototype.setAttribute = function(name, value) { - if (String(name).toLowerCase() === "src" && (this instanceof HTMLVideoElement || this instanceof HTMLSourceElement)) { - postCandidate(value, this); + if (String(name).toLowerCase() === "src") { + if (this instanceof HTMLVideoElement || this instanceof HTMLSourceElement) { + postCandidate(value, this); + } + if (this instanceof HTMLTrackElement) { + addSubtitleCandidate({ + url: value, + label: this.label || this.srclang || "External Subtitle", + language: this.srclang || "" + }); + } } return originalSetAttribute.call(this, name, value); }; @@ -349,9 +410,13 @@ final class DreamioWebViewController: UIViewController { HTMLMediaElement.prototype.load = function() { inspectMedia(this); this.querySelectorAll("source").forEach(inspectMedia); + inspectTextTracks(this); return originalLoad.call(this); }; + document.addEventListener("addtrack", (event) => { + inspectTrack(event.track || event.target); + }, true); document.addEventListener("loadedmetadata", (event) => inspectMedia(event.target), true); document.addEventListener("error", (event) => inspectMedia(event.target), true); new MutationObserver((mutations) => { @@ -365,7 +430,7 @@ final class DreamioWebViewController: UIViewController { childList: true, subtree: true, attributes: true, - attributeFilter: ["src"] + attributeFilter: ["src", "label", "srclang"] }); inspectMedia(document); diff --git a/docs/turns/2026-05-25-auto-select-vlc-subtitle-tracks.html b/docs/turns/2026-05-25-auto-select-vlc-subtitle-tracks.html index 033eb0d..2abc22c 100644 --- a/docs/turns/2026-05-25-auto-select-vlc-subtitle-tracks.html +++ b/docs/turns/2026-05-25-auto-select-vlc-subtitle-tracks.html @@ -306,6 +306,90 @@
Related issues or PRs: Beads issue dreamio-ppj.
Summary of changes: Added browser text-track capture for OpenSubtitles V3 subtitles. The web bridge now inspects <track> elements, HTMLTrackElement.src assignments, setAttribute("src", ...) calls, video.textTracks, and late addtrack events.
Why this change was made: The device logs showed embedded subtitles were visible, but OpenSubtitles V3 options still never reached native playback. That means the external subtitle candidates were being missed before VLC, likely because Stremio attached them as browser text tracks rather than including them in the original stream candidate.
+Code diffs:
+198 unmodified lines199200201202203204102 unmodified lines30730830931031131231331431531631719 unmodified lines3373383393403413423433443453462 unmodified lines3493503513523533543553563577 unmodified lines365366367368369370371198 unmodified linespostSubtitleCandidates([candidate]);};+const postSubtitleInspection = (source, url, beforeCount, afterCount, payloadLength) => {if (afterCount > beforeCount) {return;102 unmodified linesif (!node) {return;}if (node instanceof HTMLVideoElement || node instanceof HTMLSourceElement) {postCandidate(node.currentSrc || node.src || node.getAttribute("src"), node);}if (node.querySelectorAll) {node.querySelectorAll("video, source").forEach(inspectMedia);}};+19 unmodified lines});}+const originalSetAttribute = Element.prototype.setAttribute;Element.prototype.setAttribute = function(name, value) {if (String(name).toLowerCase() === "src" && (this instanceof HTMLVideoElement || this instanceof HTMLSourceElement)) {postCandidate(value, this);}return originalSetAttribute.call(this, name, value);};2 unmodified linesHTMLMediaElement.prototype.load = function() {inspectMedia(this);this.querySelectorAll("source").forEach(inspectMedia);return originalLoad.call(this);};+document.addEventListener("loadedmetadata", (event) => inspectMedia(event.target), true);document.addEventListener("error", (event) => inspectMedia(event.target), true);new MutationObserver((mutations) => {7 unmodified lineschildList: true,subtree: true,attributes: true,attributeFilter: ["src"]});+inspectMedia(document);198 unmodified lines199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235102 unmodified lines33833934034134234334434534634734834935035135235335419 unmodified lines3743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064072 unmodified lines4104114124134144154164174184194204214227 unmodified lines430431432433434435436198 unmodified linespostSubtitleCandidates([candidate]);};+const inspectTrack = (track) => {if (!track) {return;}if (track instanceof HTMLTrackElement) {addSubtitleCandidate({url: track.src || track.getAttribute("src") || "",label: track.label || track.srclang || "External Subtitle",language: track.srclang || ""});return;}const source = track.src || track.url || "";if (source) {addSubtitleCandidate({url: source,label: track.label || track.language || track.kind || "External Subtitle",language: track.language || ""});}};+const inspectTextTracks = (media) => {try {Array.from(media.textTracks || []).forEach(inspectTrack);} catch (_) {}try {media.querySelectorAll("track").forEach(inspectTrack);} catch (_) {}};+const postSubtitleInspection = (source, url, beforeCount, afterCount, payloadLength) => {if (afterCount > beforeCount) {return;102 unmodified linesif (!node) {return;}if (node instanceof HTMLTrackElement) {inspectTrack(node);}if (node instanceof HTMLVideoElement || node instanceof HTMLSourceElement) {postCandidate(node.currentSrc || node.src || node.getAttribute("src"), node);}if (node.querySelectorAll) {node.querySelectorAll("video, source, track").forEach(inspectMedia);}if (node instanceof HTMLVideoElement) {inspectTextTracks(node);}};+19 unmodified lines});}+const trackSrcDescriptor = Object.getOwnPropertyDescriptor(HTMLTrackElement.prototype, "src");if (trackSrcDescriptor && trackSrcDescriptor.set) {Object.defineProperty(HTMLTrackElement.prototype, "src", {get: trackSrcDescriptor.get,set(value) {addSubtitleCandidate({url: value,label: this.label || this.srclang || "External Subtitle",language: this.srclang || ""});return trackSrcDescriptor.set.call(this, value);}});}+const originalSetAttribute = Element.prototype.setAttribute;Element.prototype.setAttribute = function(name, value) {if (String(name).toLowerCase() === "src") {if (this instanceof HTMLVideoElement || this instanceof HTMLSourceElement) {postCandidate(value, this);}if (this instanceof HTMLTrackElement) {addSubtitleCandidate({url: value,label: this.label || this.srclang || "External Subtitle",language: this.srclang || ""});}}return originalSetAttribute.call(this, name, value);};2 unmodified linesHTMLMediaElement.prototype.load = function() {inspectMedia(this);this.querySelectorAll("source").forEach(inspectMedia);inspectTextTracks(this);return originalLoad.call(this);};+document.addEventListener("addtrack", (event) => {inspectTrack(event.track || event.target);}, true);document.addEventListener("loadedmetadata", (event) => inspectMedia(event.target), true);document.addEventListener("error", (event) => inspectMedia(event.target), true);new MutationObserver((mutations) => {7 unmodified lineschildList: true,subtree: true,attributes: true,attributeFilter: ["src", "label", "srclang"]});+inspectMedia(document);
Related issues or PRs: Beads issue dreamio-3xi.