Skip to content

feat(subtitles): add streaming subtitle adapter framework - #1757

Closed
semantic-craft wants to merge 3 commits into
mengxi-ream:mainfrom
semantic-craft:feat/streaming-adapters-netflix
Closed

feat(subtitles): add streaming subtitle adapter framework#1757
semantic-craft wants to merge 3 commits into
mengxi-ream:mainfrom
semantic-craft:feat/streaming-adapters-netflix

Conversation

@semantic-craft

@semantic-craft semantic-craft commented Jun 28, 2026

Copy link
Copy Markdown

Summary

This supersedes #1751 with the systematic adapter shape requested there: streaming subtitle support now goes through one shared pipeline plus a site adapter/registry entry, with Netflix as the first adapter.

Netflix has two paths:

  • when Netflix exposes subtitle download tracks in page JSON, Read Frog parses and aligns those official tracks;
  • on the current cadmium playback path, where subtitle assets are not directly replayable from the content script, Read Frog falls back to a MAIN-world player/DOM live capture bridge and then uses the existing translation pipeline.

The MAIN-world bridge matters because Netflix's player API is not visible from the extension isolated world. The content script now coordinates through postMessage; the interceptor does the player track switch and live cue capture where window.netflix actually exists.

Size / Review

Final diff: 991 insertions / 27 deletions, kept under the 1000-line threshold. HBO stays out of this PR.

Real Browser Evidence

Verified in the user's Chrome profile on https://www.netflix.com/watch/81613638 after reloading the unpacked extension:

  • Read Frog switch stays ON;
  • Netflix player track changes from zh-Hant to English CC (bcp47: en, rawTrackType: CLOSEDCAPTIONS);
  • Netflix native timed-text DOM is hidden by Read Frog but continues updating with English cue text;
  • Read Frog shadow DOM receives source + translated cue text, e.g. Probably around the time Emily and I set our boat out on the lake. plus the Chinese translation.

Tests

  • pnpm type-check
  • pnpm exec eslint ... on touched subtitle/interceptor files
  • WXT_SKIP_ENV_VALIDATION=true pnpm build
  • SKIP_FREE_API=true pnpm exec vitest run targeted subtitle tests: 5 files / 85 tests
  • pre-push full extension checks: lint, type-check, vitest 163 files / 1400 tests

Closes #1751.

Introduce a systematic adapter framework for official bilingual subtitles on
streaming sites, with Netflix as the first adapter:

- StreamingSiteAdapter + StreamingSubtitlesFetcher: shared source/target track
  selection and official-to-official bilingual alignment, in one place.
- MAIN-world interceptor captures Netflix's own subtitle download URLs from its
  manifest JSON (ttDownloadables), ignoring non-subtitle media; replays missed
  manifests after a late extension inject.
- One streaming-site registry; new platforms plug in by adding an adapter.

Directly addresses the maintainer request on mengxi-ream#1751 for a systematic, non
one-off design. Kept under the new-contributor changed-line threshold.
@changeset-bot

changeset-bot Bot commented Jun 28, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 99712c7

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@read-frog/extension Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions github-actions Bot added feat contrib-trust:new PR author trust score is 0-29. needs-maintainer-review Contributor trust automation recommends maintainer review. labels Jun 28, 2026
@github-actions

github-actions Bot commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

Contributor trust score

16/100 — New contributor

This score estimates contributor familiarity with mengxi-ream/read-frog using public GitHub signals. It is advisory only and does not block merges automatically.

Outcome

Score breakdown

Dimension Score Signals
Repo familiarity 0/35 commits in repo, merged PRs, reviews
Community standing 6/25 account age, followers, repo role
OSS influence 8/20 stars on owned non-fork repositories
PR track record 2/20 merge rate across resolved PRs in this repo

Signals used

  • Repo commits: 0 (author commits reachable from the repository default branch)
  • Repo PR history: merged 0, open 1, closed-unmerged 2
  • Repo reviews: 0
  • PR counted changed lines: 1018 (+991 / -27)
  • Repo permission: read
  • Followers: 2
  • Account age: 129 months
  • Owned non-fork repos considered: max 102, total 208 (semantic-craft/mac-tmux-kit (102), semantic-craft/iOS-vibebuddy (100), semantic-craft/raycast-doubao-tts (3), semantic-craft/vscode-ai-voice-studio (1), semantic-craft/Raycast-Gemini-TTS (1), semantic-craft/snapocr-via-paddle (1), semantic-craft/famo-releases (0), semantic-craft/famoime-mac (0), semantic-craft/responsay-legal-skills (0), semantic-craft/responsay-releases (0), semantic-craft/tmux-cheatsheet (0), semantic-craft/raycast-responsay (0), semantic-craft/parlance (0), semantic-craft/vscode-english-coach (0), semantic-craft/raycast-skill-manager (0), semantic-craft/Raycast-Mimo-TTS (0), semantic-craft/raycast-tmux (0), semantic-craft/raycast-ai-voice-studio (0), semantic-craft/english-speaking-training-vscode (0), semantic-craft/raycast-ai-translate (0))

Policy

  • Low-score review threshold: < 30
  • Auto-close: score < 20 and counted changed lines > 1000
  • Migration-related files are excluded from the auto-close line count
  • Policy version: v1.2

Updated automatically when the PR changes or when a maintainer reruns the workflow.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0ab6962fde

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +89 to +90
if (++attempts >= WATCH_WAIT_MAX_ATTEMPTS) {
stop()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Do not stop waiting for Netflix watch navigation

When the content script starts on a Netflix browse page, this timeout removes the only listener that can call bootstrapRuntime() after about 20 seconds. Netflix navigation to /watch/... is SPA-only, so the content script will not run again; users who spend more than 20 seconds choosing a title get no subtitle UI until a full page reload.

Useful? React with 👍 / 👎.

measureHeight: () => 0,
checkVisibility: () => true,
},
getVideoId: () => window.location.pathname,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid treating Netflix browse paths as video ids

Returning every pathname as a video id makes non-player routes participate in the generic navigation reset. After a user leaves /watch/... for a browse/details page, resetForNavigation() clears the session id; the next SPA navigation back to /watch/... is then ignored by videoIdChanged because there is no previous session id, leaving the Netflix adapter without a fresh scheduler for the new title.

Useful? React with 👍 / 👎.

- index.tsx: keep listening for SPA navigation to /watch/ instead of removing the
  listener after ~20s, so users who browse a while before picking a title still
  get the subtitle UI without a reload.
- netflix/config.ts: scope getVideoId to /watch/<id> (null on browse pages) so
  leaving a title and returning isn't dropped by the navigation-reset logic.
@semantic-craft

Copy link
Copy Markdown
Author

Thanks @codex — both points addressed in 6609c86d:

  1. Netflix watch navigation (index.tsx): removed the ~20s timeout that detached the URL-change listener. The content script now keeps listening for the SPA navigation to /watch/… for as long as the page is open, so users who browse a while before picking a title still get the subtitle UI without a reload. (For Netflix, readiness only changes via navigation, so a bounded poll was unnecessary — the listener alone is correct and simpler.)

  2. getVideoId on browse paths (netflix/config.ts): getVideoId now returns the /watch/<id> id and null on browse/details pages. Browse navigations no longer trigger a reset that nulls the session id, so videoIdChanged correctly fires when the user opens the next title.

@chatgpt-codex-connector

Copy link
Copy Markdown

To use Codex here, create an environment for this repo.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6609c86d41

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +35 to +39
if (event.data?.type === STREAMING_SUBTITLE_TRACKS_TYPE && Array.isArray(event.data.tracks)) {
event.data.tracks.forEach((track: StreamingTrack) => {
const key = getTrackKey(track)
if (key)
tracksByKey.set(key, { ...track, pagePath: track.pagePath ?? getCurrentPagePath() })

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Reject page-supplied subtitle URLs before fetching

This accepts any window.postMessage from the Netflix page as trusted subtitle tracks. In a Netflix page context (or an XSS/third-party script running there), a script can post READ_FROG_STREAMING_SUBTITLE_TRACKS with an arbitrary url; when auto-start is enabled or the user turns subtitles on, resolveTrackText later sends that URL through backgroundFetch(..., { credentials: "include" }) using the extension's host permissions, so untrusted page data can drive privileged credentialed cross-origin requests and render subtitle-like responses back into the page UI. Please authenticate the capture path or at least restrict accepted track URLs to known Netflix subtitle hosts before storing them.

Useful? React with 👍 / 👎.

window.fetch = async function (input: RequestInfo | URL, init?: RequestInit) {
const response = await originalFetch.call(this, input, init)
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url
void response.clone().text().then(text => inspectJson(url, text, response.headers.get("content-type"))).catch(() => {})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid reading every fetch response as text

When Netflix uses fetch for non-JSON or large resources, this clone is fully consumed as text before inspectJson checks the content type or URL. That means media chunks or other large responses fetched through this patched API can be duplicated and decoded just to be ignored, which can add avoidable memory/CPU pressure during playback; gate on response.headers.get("content-type")/the request URL before calling clone().text().

Useful? React with 👍 / 👎.

Comment thread .changeset/streaming-netflix-adapter.md Outdated
"@read-frog/extension": patch
---

Add a streaming subtitle adapter framework with Netflix as the first adapter. On Netflix it pairs the platform's own official source-language captions with its official target-language subtitles into bilingual cues — an official-to-official comparison rather than machine translation. Other streaming sites plug into the same shared capture + alignment pipeline through one adapter registry.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Use conventional commit text in the changeset

The root AGENTS.md PR Notes say to “use conventional commit format for the changeset content,” but this release note is plain prose. This user-facing feature changes @read-frog/extension, so the changeset should start with conventional-commit wording such as feat(subtitles): ... to satisfy the repository release-note convention.

Useful? React with 👍 / 👎.

@github-actions github-actions Bot closed this Jun 28, 2026
@semantic-craft semantic-craft changed the title feat(subtitles): systematic streaming subtitle adapters — official Netflix bilingual captions feat(subtitles): add streaming subtitle adapter framework Jun 28, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 99712c71c3

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

else {
await this.processTranslatedSubtitles()
}
this.startLiveCapture()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Only start Netflix live capture as a fallback

When official Netflix source/target tracks are available, the branch above renders pre-aligned bilingual cues and does not create a TranslationCoordinator; this unconditional call still starts DOM live capture afterward. Those live cues are source-only and use current playback time as their start, so titles whose manifest exposes tracks can get untranslated duplicate/extended cues merged into the scheduler. Start live capture only when the static/downloadable subtitle fetch actually fell back to no fragments.

Useful? React with 👍 / 👎.

Comment on lines +218 to +219
if (previous && !previous.isNoneTrack)
player.setTimedTextTrack(previous)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Restore Netflix's no-subtitle selection on stop

When the user had Netflix captions off, previous is the none track, but this stop callback skips restoring it because previous.isNoneTrack is true. Live capture switches the player to the source track before reading DOM captions, so disabling Read Frog then removes the hiding CSS while Netflix native captions remain enabled. Restore the saved none track, or explicitly switch back to none, as part of stopping capture.

Useful? React with 👍 / 👎.

},
// Only /watch/<id> pages carry a video id; browse/details pages return null so
// they don't trigger a navigation reset that would drop the next title's session.
getVideoId: () => window.location.pathname.match(/^\/watch\/([^/?#]+)/)?.[1] ?? null,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Clear Netflix overlay state when leaving watch pages

Returning null on browse/details URLs means a /watch/... -> non-watch SPA navigation is ignored by UniversalVideoAdapter.videoIdChanged, which requires a truthy current video id before it resets. If subtitles are active when the user exits playback, the scheduler/live capture and current subtitle state can remain mounted on body, leaving stale overlay/capture state over Netflix browsing until another watch navigation occurs. Add a separate leave-watch cleanup instead of suppressing all handling for null ids.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

contrib-trust:new PR author trust score is 0-29. feat needs-maintainer-review Contributor trust automation recommends maintainer review.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant