YouTube (youtube.com) — the only one. Used for the watch page, caption
tracks, and the rendered transcript panel. No API key, no account, no quota.
The extension is an unauthenticated consumer of pages a browser would fetch
anyway.
There is no contract here. Every integration point below is an observed behaviour of a product that can change without notice.
| Assumption | Where it lives | If it breaks |
|---|---|---|
| Video ids are 11 URL-safe base64 characters | VIDEO_ID_RE in src/lib/youtube.js |
URLs stop parsing; clear error, no wrong output |
The watch page embeds ytInitialPlayerResponse as a JSON object literal |
parseWatchPage |
Falls back to a bare "captionTracks":[...] scan, then reports parsed: false |
Caption tracks carry baseUrl, languageCode, name, and kind: 'asr' for auto-generated |
parseWatchPage |
Track list empties or mislabels auto-generated tracks |
timedtext accepts &fmt=json3 and returns events[].segs[].utf8 |
parseJson3 |
Falls through to the XML parser, then to strategy 2 |
timedtext still serves the legacy <text start dur> XML |
parseTimedTextXml |
Falls through to strategy 2 |
| A “Show transcript” control exists, by text or aria-label | openPanel in the content script |
Strategy 2 fails; reported as “blocked” |
Transcript cues render as transcript-segment-view-model or ytd-transcript-segment-renderer |
SEGMENT_SELECTOR |
Strategy 2 returns nothing; see the runbook |
Caption text sits in a span whose class contains AttributedString |
readRow fallback |
Falls back to longest-leaf-text heuristic |
| The transcript panel renders every cue rather than virtualising | Verified live: 137 cues, stable across a 2s re-measure | Long videos would silently truncate — the one failure that could produce wrong output rather than none |
hl=en yields English track names and consent text |
loadWatchPage |
Language labels appear localised |
The virtualisation assumption is the one worth re-verifying after any YouTube redesign, because it is the only one whose failure is silent.
| Assumption | Detail |
|---|---|
| Manifest V3, Chrome 116+ | Declared as minimum_chrome_version |
| Service workers terminate when idle | ~30s. Nothing is stored in module scope. |
URL.createObjectURL is unavailable in a service worker |
Why the PDF is built in the popup |
| A blob URL dies with its creating document | Why the download uses saveAs: false |
chrome.storage.session is readable from extension pages |
Default TRUSTED_CONTEXTS access level |
User-Agent is a forbidden fetch header |
It is not set; Chrome would drop it |
The extension CSP blocks eval and inline scripts |
Relied on as a defence, not worked around |
| Standard PDF viewers ship Helvetica and Helvetica-Bold | Why no font is embedded |
There are none. The extension reads no configuration, has no build-time flags,
no .env, and no dev-versus-prod branching. What runs unpacked is what ships.
The only environmental variation is the browser: Chromium forks (Edge, Brave, Arc) work if they are recent enough, but only Chrome is tested.
| Value | Location | Rationale |
|---|---|---|
| 15000ms request timeout | REQUEST_TIMEOUT_MS |
Long enough for a 1 MB page on a slow link |
| 20000ms tab-load timeout | TAB_LOAD_TIMEOUT_MS |
YouTube page loads are slow |
| 150ms poll interval | waitFor in the content script |
Responsive without spinning |
| 420-character paragraph cap | groupIntoParagraphs |
Roughly a readable print paragraph |
| 2.5s speech-gap break | groupIntoParagraphs |
Long enough to mean a pause, not a breath |
| 54pt page margin, 11pt body | buildTranscriptPdf |
Standard document proportions |
| 120-character filename cap | toSafeFilename |
Under every filesystem limit |
None of these are user-configurable. Each is a judgement call that could reasonably be exposed as a setting if anyone asks.