From 07741bae96545253224857b95394fa0a8f0b5a17 Mon Sep 17 00:00:00 2001 From: dirtydishes Date: Mon, 25 May 2026 12:05:12 -0400 Subject: [PATCH] capture opensubtitles candidates from stremio messages --- .beads/interactions.jsonl | 4 + .beads/issues.jsonl | 1 + Dreamio/DreamioWebViewController.swift | 82 ++++++++ ...s-discovery-after-native-interception.html | 195 ++++++++++++++++++ 4 files changed, 282 insertions(+) create mode 100644 docs/turns/2026-05-25-fix-opensubtitles-discovery-after-native-interception.html diff --git a/.beads/interactions.jsonl b/.beads/interactions.jsonl index c9116fb..63c41ef 100644 --- a/.beads/interactions.jsonl +++ b/.beads/interactions.jsonl @@ -26,3 +26,7 @@ {"id":"int-3acaadff","kind":"field_change","created_at":"2026-05-25T15:09:02.023077Z","actor":"dirtydishes","issue_id":"dreamio-h5n","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Limited VLC auto-subtitle reapply to real selection recovery while keeping bounded delayed startup confirmations."}} {"id":"int-c526b5ae","kind":"field_change","created_at":"2026-05-25T15:32:37.748454Z","actor":"dirtydishes","issue_id":"dreamio-dow","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Implemented stream-keyed subtitle buffering, OpenSubtitles parser/resolver hardening, VLC refresh behavior, and focused validation."}} {"id":"int-320e7321","kind":"field_change","created_at":"2026-05-25T15:53:52.866657Z","actor":"dirtydishes","issue_id":"dreamio-hzj","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Hardened OpenSubtitles candidate discovery, nested payload resolution, VLC external subtitle visibility selection, diagnostics, tests, and turn documentation."}} +{"id":"int-95ad98d5","kind":"field_change","created_at":"2026-05-25T16:00:18.70354Z","actor":"dirtydishes","issue_id":"dreamio-656","extra":{"field":"status","new_value":"in_progress","old_value":"open"}} +{"id":"int-323d3a68","kind":"field_change","created_at":"2026-05-25T16:02:09.791701Z","actor":"dirtydishes","issue_id":"dreamio-656","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"fixed"}} +{"id":"int-6e411a6a","kind":"field_change","created_at":"2026-05-25T16:03:23.023525Z","actor":"dirtydishes","issue_id":"dreamio-656","extra":{"field":"status","new_value":"in_progress","old_value":"open"}} +{"id":"int-fe1c7364","kind":"field_change","created_at":"2026-05-25T16:04:54.482803Z","actor":"dirtydishes","issue_id":"dreamio-656","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"fixed"}} diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index a13d943..8073c1f 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-656","title":"Capture OpenSubtitles candidates from Stremio app-state messages","description":"OpenSubtitlesV3 appears loaded in Stremio before native playback launches, but Dreamio forwards zero external subtitle candidates. The likely failure is not native-player timing; it is that the injected WebKit bridge does not extract Stremio's loaded subtitle metadata/state into URL candidates before opening VLC.","status":"closed","priority":1,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-25T16:00:09Z","created_by":"dirtydishes","updated_at":"2026-05-25T16:04:54Z","started_at":"2026-05-25T16:00:18Z","closed_at":"2026-05-25T16:04:54Z","close_reason":"fixed","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"dreamio-hzj","title":"OpenSubtitles tracks missing from native captions menu","description":"OpenSubtitles subtitle candidates can be discovered or resolved inconsistently, and external VLC subtitle slaves may not become visible quickly enough to show as selectable native caption tracks. Harden discovery, resolution, attachment, diagnostics, tests, and turn documentation for the native captions path.","status":"closed","priority":1,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-25T15:51:07Z","created_by":"dirtydishes","updated_at":"2026-05-25T15:53:53Z","started_at":"2026-05-25T15:51:13Z","closed_at":"2026-05-25T15:53:53Z","close_reason":"Hardened OpenSubtitles candidate discovery, nested payload resolution, VLC external subtitle visibility selection, diagnostics, tests, and turn documentation.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"dreamio-dow","title":"fix stremio external subtitle handoff to vlc","status":"closed","priority":1,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-25T15:17:16Z","created_by":"dirtydishes","updated_at":"2026-05-25T15:32:38Z","started_at":"2026-05-25T15:17:25Z","closed_at":"2026-05-25T15:32:38Z","close_reason":"Implemented stream-keyed subtitle buffering, OpenSubtitles parser/resolver hardening, VLC refresh behavior, and focused validation.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"dreamio-bao","title":"add native player audio track selection","description":"Add audio track discovery and selection to the native VLC-backed player so multi-language files can be filtered from the player controls.","status":"closed","priority":1,"issue_type":"feature","owner":"dishes@dpdrm.com","created_at":"2026-05-25T14:57:14Z","created_by":"dirtydishes","updated_at":"2026-05-25T15:01:36Z","closed_at":"2026-05-25T15:01:36Z","close_reason":"Implemented native audio track discovery and selection with a far-left audio menu in the VLC-backed player.","dependency_count":0,"dependent_count":0,"comment_count":0} diff --git a/Dreamio/DreamioWebViewController.swift b/Dreamio/DreamioWebViewController.swift index 95d5e26..78ffbcd 100644 --- a/Dreamio/DreamioWebViewController.swift +++ b/Dreamio/DreamioWebViewController.swift @@ -304,6 +304,12 @@ final class DreamioWebViewController: UIViewController { postSubtitleInspection(source, url, beforeCount, subtitleCandidates.length, text ? text.length : 0); }; + const inspectMessagePayload = (source, payload) => { + const beforeCount = subtitleCandidates.length; + inspectSubtitlePayload(payload); + postSubtitleInspection(source, "", beforeCount, subtitleCandidates.length, 0); + }; + const originalFetch = window.fetch; if (originalFetch) { window.fetch = async (...args) => { @@ -349,6 +355,82 @@ final class DreamioWebViewController: UIViewController { return originalXHRSend.apply(this, args); }; + const originalWindowPostMessage = window.postMessage; + if (originalWindowPostMessage) { + window.postMessage = function(message, targetOrigin, transfer) { + try { inspectMessagePayload("window.postMessage", message); } catch (_) {} + return originalWindowPostMessage.apply(this, arguments); + }; + } + window.addEventListener("message", (event) => { + try { inspectMessagePayload("window.message", event.data); } catch (_) {} + }, true); + + const OriginalWorker = window.Worker; + if (OriginalWorker) { + window.Worker = function(...args) { + const worker = new OriginalWorker(...args); + try { + const originalWorkerPostMessage = worker.postMessage; + worker.postMessage = function(message, transfer) { + try { inspectMessagePayload("worker.postMessage", message); } catch (_) {} + return originalWorkerPostMessage.apply(this, arguments); + }; + worker.addEventListener("message", (event) => { + try { inspectMessagePayload("worker.message", event.data); } catch (_) {} + }, true); + } catch (_) {} + return worker; + }; + try { + window.Worker.prototype = OriginalWorker.prototype; + } catch (_) {} + } + + if (window.MessagePort && window.MessagePort.prototype) { + const originalPortPostMessage = window.MessagePort.prototype.postMessage; + if (originalPortPostMessage) { + window.MessagePort.prototype.postMessage = function(message, transfer) { + try { inspectMessagePayload("message-port.postMessage", message); } catch (_) {} + return originalPortPostMessage.apply(this, arguments); + }; + } + const originalPortAddEventListener = window.MessagePort.prototype.addEventListener; + if (originalPortAddEventListener) { + window.MessagePort.prototype.addEventListener = function(type, listener, options) { + if (type === "message" && typeof listener === "function") { + const wrapped = function(event) { + try { inspectMessagePayload("message-port.message", event && event.data); } catch (_) {} + return listener.apply(this, arguments); + }; + return originalPortAddEventListener.call(this, type, wrapped, options); + } + return originalPortAddEventListener.apply(this, arguments); + }; + } + } + + const OriginalBroadcastChannel = window.BroadcastChannel; + if (OriginalBroadcastChannel) { + window.BroadcastChannel = function(...args) { + const channel = new OriginalBroadcastChannel(...args); + try { + const originalBroadcastPostMessage = channel.postMessage; + channel.postMessage = function(message) { + try { inspectMessagePayload("broadcast-channel.postMessage", message); } catch (_) {} + return originalBroadcastPostMessage.apply(this, arguments); + }; + channel.addEventListener("message", (event) => { + try { inspectMessagePayload("broadcast-channel.message", event.data); } catch (_) {} + }, true); + } catch (_) {} + return channel; + }; + try { + window.BroadcastChannel.prototype = OriginalBroadcastChannel.prototype; + } catch (_) {} + } + const stopNativeHandledMedia = (element) => { const media = element instanceof HTMLVideoElement ? element diff --git a/docs/turns/2026-05-25-fix-opensubtitles-discovery-after-native-interception.html b/docs/turns/2026-05-25-fix-opensubtitles-discovery-after-native-interception.html new file mode 100644 index 0000000..79cc517 --- /dev/null +++ b/docs/turns/2026-05-25-fix-opensubtitles-discovery-after-native-interception.html @@ -0,0 +1,195 @@ + + + + + + Capture OpenSubtitles Candidates from Stremio Messages + + + +
+
+
Dreamio turn document
+

Capture OpenSubtitles candidates from Stremio messages

+

Dreamio now inspects Stremio app-state and worker messages for subtitle objects, so OpenSubtitlesV3 entries that are already loaded in Stremio can become native subtitle candidates before VLC opens.

+
+ Date: 2026-05-25 + Issue: dreamio-656 + Scope: native subtitle handoff +
+
+ +
+

Summary

+

The failure is not embedded subtitle rendering and is probably not a native-player delay. Stremio can show OpenSubtitlesV3 as loaded before Dreamio launches VLC, while Dreamio still forwards zero external subtitle candidates. The bridge now watches message surfaces where Stremio is likely moving already-loaded subtitle state: window.postMessage, window message events, Worker messages, MessagePort messages, and BroadcastChannel messages.

+
+ +
+

Changes Made

+
    +
  • Added inspectMessagePayload, which sends arbitrary app-state/message payloads through the existing subtitle payload parser.
  • +
  • Wrapped window.postMessage and listened for window message events.
  • +
  • Wrapped constructed Worker instances so messages to and from Stremio workers are inspected.
  • +
  • Wrapped MessagePort.postMessage and message listeners for channel-based state transport.
  • +
  • Wrapped BroadcastChannel construction to inspect broadcasted state messages.
  • +
  • Removed the earlier delayed-cleanup hypothesis; native-handled media cleanup remains immediate.
  • +
+
+ +
+

Context

+

The observed embedded-subtitle logs showed VLC successfully listing and selecting an embedded MKV subtitle track. The OpenSubtitles path is separate: Dreamio’s native player had subtitle candidates=0, meaning no external subtitle candidate reached the Swift attachment/resolution layer.

+

Stremio showing OpenSubtitlesV3 as loaded means the data likely exists in the web app before native launch. If that data moves through a worker or message channel rather than main-window fetch or DOM tracks, the old bridge would never see it.

+
+ +
+

Important Implementation Details

+
    +
  • The new hooks reuse inspectSubtitlePayload, so they support the same URL fields, nested objects, and OpenSubtitles file_id handling as the fetch/XHR path.
  • +
  • The hooks inspect messages passively and then call the original browser APIs, preserving Stremio behavior.
  • +
  • Debug logs should now identify message-derived inspection via sources like worker.message, message-port.message, or broadcast-channel.message.
  • +
  • If candidates are discovered, the existing Swift path still resolves OpenSubtitles API/download URLs into direct subtitle files before attaching them to VLC.
  • +
+
+ +
+

Relevant Diff Snippets

+
Dreamio/DreamioWebViewController.swift
+82
303 unmodified lines
304
305
306
307
308
309
39 unmodified lines
349
350
351
352
353
354
303 unmodified lines
postSubtitleInspection(source, url, beforeCount, subtitleCandidates.length, text ? text.length : 0);
};
+
const originalFetch = window.fetch;
if (originalFetch) {
window.fetch = async (...args) => {
39 unmodified lines
return originalXHRSend.apply(this, args);
};
+
const stopNativeHandledMedia = (element) => {
const media = element instanceof HTMLVideoElement
? element
303 unmodified lines
304
305
306
307
308
309
310
311
312
313
314
315
39 unmodified lines
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
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
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
303 unmodified lines
postSubtitleInspection(source, url, beforeCount, subtitleCandidates.length, text ? text.length : 0);
};
+
const inspectMessagePayload = (source, payload) => {
const beforeCount = subtitleCandidates.length;
inspectSubtitlePayload(payload);
postSubtitleInspection(source, "", beforeCount, subtitleCandidates.length, 0);
};
+
const originalFetch = window.fetch;
if (originalFetch) {
window.fetch = async (...args) => {
39 unmodified lines
return originalXHRSend.apply(this, args);
};
+
const originalWindowPostMessage = window.postMessage;
if (originalWindowPostMessage) {
window.postMessage = function(message, targetOrigin, transfer) {
try { inspectMessagePayload("window.postMessage", message); } catch (_) {}
return originalWindowPostMessage.apply(this, arguments);
};
}
window.addEventListener("message", (event) => {
try { inspectMessagePayload("window.message", event.data); } catch (_) {}
}, true);
+
const OriginalWorker = window.Worker;
if (OriginalWorker) {
window.Worker = function(...args) {
const worker = new OriginalWorker(...args);
try {
const originalWorkerPostMessage = worker.postMessage;
worker.postMessage = function(message, transfer) {
try { inspectMessagePayload("worker.postMessage", message); } catch (_) {}
return originalWorkerPostMessage.apply(this, arguments);
};
worker.addEventListener("message", (event) => {
try { inspectMessagePayload("worker.message", event.data); } catch (_) {}
}, true);
} catch (_) {}
return worker;
};
try {
window.Worker.prototype = OriginalWorker.prototype;
} catch (_) {}
}
+
if (window.MessagePort && window.MessagePort.prototype) {
const originalPortPostMessage = window.MessagePort.prototype.postMessage;
if (originalPortPostMessage) {
window.MessagePort.prototype.postMessage = function(message, transfer) {
try { inspectMessagePayload("message-port.postMessage", message); } catch (_) {}
return originalPortPostMessage.apply(this, arguments);
};
}
const originalPortAddEventListener = window.MessagePort.prototype.addEventListener;
if (originalPortAddEventListener) {
window.MessagePort.prototype.addEventListener = function(type, listener, options) {
if (type === "message" && typeof listener === "function") {
const wrapped = function(event) {
try { inspectMessagePayload("message-port.message", event && event.data); } catch (_) {}
return listener.apply(this, arguments);
};
return originalPortAddEventListener.call(this, type, wrapped, options);
}
return originalPortAddEventListener.apply(this, arguments);
};
}
}
+
const OriginalBroadcastChannel = window.BroadcastChannel;
if (OriginalBroadcastChannel) {
window.BroadcastChannel = function(...args) {
const channel = new OriginalBroadcastChannel(...args);
try {
const originalBroadcastPostMessage = channel.postMessage;
channel.postMessage = function(message) {
try { inspectMessagePayload("broadcast-channel.postMessage", message); } catch (_) {}
return originalBroadcastPostMessage.apply(this, arguments);
};
channel.addEventListener("message", (event) => {
try { inspectMessagePayload("broadcast-channel.message", event.data); } catch (_) {}
}, true);
} catch (_) {}
return channel;
};
try {
window.BroadcastChannel.prototype = OriginalBroadcastChannel.prototype;
} catch (_) {}
}
+
const stopNativeHandledMedia = (element) => {
const media = element instanceof HTMLVideoElement
? element
+
+ +
+

Expected Impact for End-Users

+

Streams where Stremio has already loaded OpenSubtitlesV3 should have a better chance of handing those subtitles to native playback. The expected visible result is that the native captions menu gains external OpenSubtitles options instead of showing no external candidates.

+
+ +
+

Validation

+
    +
  • Passed: swiftc -parse-as-library Dreamio/StreamCandidate.swift Dreamio/StreamResolver.swift Tests/StreamResolverTests.swift -o /tmp/dreamio-stream-resolver-tests && /tmp/dreamio-stream-resolver-tests
  • +
  • Passed: xcodebuild -workspace Dreamio.xcworkspace -scheme Dreamio -destination 'generic/platform=iOS Simulator' build
  • +
  • Not manually confirmed: a real OpenSubtitlesV3 stream still needs to verify that bridge logs show nonzero candidates from one of the message sources.
  • +
+
+ +
+

Issues, Limitations, and Mitigations

+
+

This fix assumes Stremio exposes loaded subtitle objects through main-window or worker messaging. If OpenSubtitles still stays at zero candidates, the next likely gap is a storage-backed state path, such as IndexedDB or a framework store that never crosses an intercepted message boundary. The debug source labels should make that next step clearer.

+
+
+ +
+

Follow-up Work

+
    +
  • Run the exact OpenSubtitlesV3 scenario and look for [DreamioSubtitles] bridge source=worker.message, message-port.message, or broadcast-channel.message with parsed above zero.
  • +
  • If message hooks still do not see candidates, inspect Stremio storage/state immediately before native launch.
  • +
  • Add a debug-only dump of subtitle-looking message keys if the next real run still shows zero candidates.
  • +
+
+
+ +