How the Read Frog `interceptor.content` Script Modifies YouTube Video Player Behavior
The interceptor.content script injects a thin RPC layer into YouTube pages that monkey-patches XMLHttpRequest to capture subtitle URLs, exposes internal player methods via window.postMessage, and can forcibly enable subtitles to support Read Frog's real-time translation features.
The Read Frog browser extension (mengxi-ream/read-frog) enhances language learning on YouTube by intercepting and augmenting the platform's native video player. At the core of this capability lies the interceptor.content script, a content script that silently installs observation hooks and exposes a controlled API for background and UI scripts to extract playback data and manipulate subtitle states.
Content Script Registration and Injection
The interceptor declares itself in src/entrypoints/interceptor.content/index.ts, targeting *.youtube.com/* pages with a document_start timing that ensures execution before the page finishes loading (lines 4-10). Upon initialization, it immediately invokes injectPlayerApi() to begin the setup process.
This early injection is critical because it allows the script to wrap native browser APIs before the YouTube player initializes its own network stack.
Intercepting Timed-Text Requests via XHR Monkey-Patching
To capture subtitle data, the script modifies the browser's network layer. In src/entrypoints/interceptor.content/timedtext-observer.ts, the setupTimedtextObserver() function overrides XMLHttpRequest.prototype.open and send to monitor every outgoing request (lines 45-60).
When a request URL matches the /api/timedtext/ pattern, the interceptor extracts the video ID (v) and the timed-text token (pot), then stores the full URL in a timedtextUrlCache Map. This cache enables asynchronous waiting via waitForTimedtextUrl(), which resolves pending promises once the specific video's subtitle URL is captured.
Message-Based Player Control Interface
The core communication mechanism resides in src/entrypoints/interceptor.content/inject-player-api.ts (lines 43-57), where a window.addEventListener('message') handler exposes three distinct RPC operations to other extension contexts. The script listens for specific message types and posts responses back to the origin, creating a bidirectional bridge between the isolated extension background scripts and the privileged page context.
Extracting Comprehensive Player Data
When receiving a PLAYER_DATA_REQUEST, the script invokes getPlayerData() (lines 79-110). This function locates the YouTube player element using selectors like .html5-video-player, then accesses undocumented internal methods including getPlayerResponse(), getAudioTrack(), and getAudioTracks() to extract:
- Current video ID and playback status
- Available caption tracks (normalized via
normalizeTracks()fromutils.ts) - Active audio track information (parsed via
parseAudioTracks()) - Device and player metadata
- The cached timed-text URL from the XHR observer
The aggregated data is returned via PLAYER_DATA_RESPONSE postMessage. Error states are handled using the errorResponse() helper.
Asynchronous Subtitle URL Resolution
For components that need to fetch raw subtitle XML, the WAIT_TIMEDTEXT_REQUEST message triggers waitForTimedtextUrl(videoId, timeout) (lines 58-66). This returns a promise that resolves once the timedtextUrlCache captures the requested video's URL, enabling reliable subtitle fetching without race conditions. Upon resolution, the script posts a WAIT_TIMEDTEXT_RESPONSE containing the captured URL.
Forcing Subtitle Activation
The ENSURE_SUBTITLES_REQUEST message addresses scenarios where captions are disabled. The ensureSubtitlesEnabled() function (lines 17-33 and 68-76) attempts to activate subtitles by either:
- Calling the player's internal
toggleSubtitles()method directly, or - Programmatically clicking the visible subtitle toggle button in the player UI
This guarantees that caption tracks are available for downstream processing, regardless of the user's initial player state.
Implementation Examples
Requesting player data from a background script:
// Send request
window.postMessage(
{ type: 'PLAYER_DATA_REQUEST', requestId: 'req-123', expectedVideoId: 'abc123' },
window.location.origin,
);
// Handle response
window.addEventListener('message', (e) => {
if (e.data?.type === 'PLAYER_DATA_RESPONSE' && e.data.requestId === 'req-123') {
console.log('Player data:', e.data.data);
}
});
Waiting for timed-text URL availability:
window.postMessage(
{ type: 'WAIT_TIMEDTEXT_REQUEST', requestId: 'tt-789', videoId: 'abc123' },
window.location.origin,
);
window.addEventListener('message', (e) => {
if (e.data?.type === 'WAIT_TIMEDTEXT_RESPONSE' && e.data.requestId === 'tt-789') {
const subtitleUrl = e.data.url; // https://www.youtube.com/api/timedtext?...
// Fetch subtitle content
}
});
Ensuring subtitles are enabled before processing:
window.postMessage(
{ type: 'ENSURE_SUBTITLES_REQUEST', requestId: 'sub-456' },
window.location.origin,
);
// The interceptor enables subtitles and acknowledges via postMessage
Summary
- The
interceptor.contentscript runs atdocument_starton all YouTube domains to establish early hooks. - It monkey-patches
XMLHttpRequestintimedtext-observer.tsto cache timed-text URLs and video tokens. - The script exposes a
postMessageAPI ininject-player-api.tsallowing other extension contexts to request player data, wait for subtitle URLs, and force-enable captions. - Internal player methods like
getPlayerResponse()andtoggleSubtitles()are accessed directly to extract state and modify behavior. - This architecture enables Read Frog's real-time translation, audio-track caption extraction, and synchronized playback control features.
Frequently Asked Questions
How does the interceptor script communicate with other Read Frog extension components?
The script establishes a message-passing bridge using window.postMessage. Background and UI scripts send typed messages (e.g., PLAYER_DATA_REQUEST) to the page context, and the interceptor responds with corresponding response types (e.g., PLAYER_DATA_RESPONSE), enabling cross-context RPC without direct function calls.
Is it safe to monkey-patch XMLHttpRequest in a browser extension?
While the technique modifies native browser APIs, the implementation in timedtext-observer.ts preserves the original request flow and only inspects URLs for the /api/timedtext/ pattern. The patch is applied immediately at document_start and is scoped to the YouTube page context, minimizing interference with other extensions or standard web functionality.
What happens if the YouTube player hasn't loaded timed-text data yet?
The waitForTimedtextUrl() function returns a promise that resolves once the XHR observer captures the subtitle URL for the specific video ID. If the request hasn't occurred, the interceptor waits until the player initiates the network call, then immediately resolves pending waiters and caches the result for subsequent requests.
Can the script enable subtitles on videos that don't have caption tracks?
No. The ensureSubtitlesEnabled() function only toggles the visibility of existing caption tracks or clicks the subtitle button. If the video lacks timed-text data entirely, the YouTube player will not generate subtitle URLs, and the timedtextUrlCache will not receive entries for that video ID. The interceptor cannot create captions where none exist in the source material.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →