Module Reference
src/lib/schemas.ts — Zod Validation Schemas
Types
| Schema | Type | Description |
|---|---|---|
VariantSchema |
Variant |
HLS variant with bandwidth, resolution, framerate, codecs, URL |
GqlResponseSchema |
GqlResponse |
Validated Twitch GQL response |
VolumeSchema |
number |
0..1, finite |
VariantIdxSchema |
number |
Non-negative integer, validated on storage read |
SpeedIdxSchema |
number |
0..8 integer, validated on storage read |
ChatOverlayEnabledSchema |
"true" \| "false" |
String enum for localStorage |
VodPositionSchema |
number |
Positive finite number |
StoryboardInfoSchema |
StoryboardInfo |
Storyboard sprite sheet metadata |
StoryboardEntrySchema |
StoryboardEntry |
Single storyboard sprite sheet entry |
src/lib/utils.ts — Shared Utilities
Exported Functions
| Function | Signature | Description |
|---|---|---|
parseMasterManifest |
(m3u8: string) => Variant[] |
Parse HLS master manifest, validate with Zod, sort by resolution descending |
getChannelName |
(url: string) => string \| undefined |
Extract channel name from URL pathname |
isInChannel |
(url: string) => boolean |
Check if URL is a channel/VOD page for player installation |
formatTime |
(secs: number) => string |
Format seconds to HH:MM:SS or MM:SS |
getOauthToken |
(cookies: string[]) => string \| null |
Extract OAuth token from twilight-user cookie with Zod validation |
parseGqlResponse |
(raw: unknown) => GqlResponse |
Validate GQL response with Zod, redact tokens in debug log |
readSavedVolume |
() => number |
Read saved volume from localStorage (default: 0.5) |
buildChatReplayUrl |
(embedSrc: string, videoId: string \| null) => string |
Build chat replay iframe URL |
Constants
| Name | Value | Description |
|---|---|---|
HANDLE_RADIUS |
7.25 |
Radius of seek/volume slider handle in pixels |
Logger
dvrlog.info(...) // console.info with [TwitchDVR] [INFO] prefix
dvrlog.warn(...) // console.warn
dvrlog.error(...) // console.error
dvrlog.debug(...) // console.debug
src/lib/storage.ts — Centralized Storage
Constants
| Name | Description |
|---|---|
KEYS |
Object mapping every storage key name to its twitch-dvr:-prefixed string |
Functions
| Function | Description |
|---|---|
getItem(key) |
Async read from chrome.storage.local, falls back to localStorage |
setItem(key, value) |
Async write to chrome.storage.local, falls back to localStorage |
removeItem(key) |
Async remove from chrome.storage.local, falls back to localStorage |
getSync(key) |
Synchronous read from localStorage (for boot path) |
setSync(key, value) |
Synchronous write to localStorage |
removeSync(key) |
Synchronous remove from localStorage |
src/lib/theater.ts — Theater Mode Helpers
| Function | Description |
|---|---|
getTheaterIconActive(element) |
Returns true if element has .active class |
computeTheaterAction(current, target) |
Returns { needsToggle: boolean } |
applyTheaterState(element, target) |
Idempotent toggle: returns 0 if already in target state, 1 if toggled |
src/content/videojs.d.ts — video.js Runtime Global Types
Ambient type declarations for the videojs global + VHS, injected by manifest.json content_scripts (video.min.js, plugins/quality-levels.min.js, plugins/hls-quality-selector.min.js). VHS is bundled inside video.min.js.
Why a custom .d.ts? video.js ships its own types, but they assume an ESM runtime import (import videojs from 'video.js'). We CANNOT use a runtime import — build-content.mjs runs esbuild with bundle: true, which would pull the entire ~580KB library into static/index.js. Instead the script is loaded as a separate content_script, exposing a global. So this file:
- Uses
import type { Player } from "video.js"(type-only — esbuild strips it, zero bundle cost) for the real player class. - Declares the runtime
videojsglobal (function +VERSION+registerPlugin+Vhs.xhr). - Augments
Playerwith runtime-only members absent from video.js’s own types:liveTracker(attached whenliveui/liveTrackeropts are set),hlsQualitySelector(the@types/videojs-hls-quality-selectorpackage targets a non-existentVideoJsPlayerinterface — a known DefinitelyTyped quirk — so its augmentation is a no-op; we re-declare it on the realPlayer).
src/lib/videojs-install.ts — video.js Player Installation
Exports
| Function | Description |
|---|---|
getLiveHlsUrl(channel) |
Resolve signed live HLS master URL via GQL token + usher |
getVodHlsUrl(vodId) |
Resolve signed VOD HLS master URL via GQL token + usher |
installVODPlayer(hlsUrl, changelog) |
Create video.js player for VOD with timer elements |
installLivePlayer(hlsUrl, changelog) |
Create video.js player for live with custom seek bar |
wasMutedPlaylistDetected() |
Returns true if muted segment playlist was detected |
resetMutedSegmentToast() |
Reset the muted-toast-once-per-load flag |
getVodCdnBaseUrl() |
Captured VOD CDN base URL from VHS onResponse |
resetVodCdnBaseUrl() |
Reset the VOD CDN base URL (for channel switch) |
getLiveTotalElapsed() |
Latest totalElapsed from live playlist parse |
resetLiveTotalElapsed() |
Reset totalElapsed (for channel switch) |
Internals
_createBasePlayer(hlsUrl, changelog, onReady) — Shared player creation for both live and VOD. Creates #vjs-player-container, calls disableNativePlayer, wires VHS onRequest/onResponse hooks (includes muted-segment detection via -muted.ts / index-muted- URL check with toast), creates the video.js player with fill/liveui/overrideNative/liveTracker, enables hlsQualitySelector, arms 1s native-disable + 2s diagnostic intervals (cleared on dispose). Full debug logging.
installVODPlayer — Calls _createBasePlayer with an onReady callback that injects custom timer elements (#vjs-timer, #vjs-timer-divider, #vjs-timer-end) into the control bar before the progressControl. The built-in progressControl is left visible for VOD seeking.
installLivePlayer — Calls _createBasePlayer with an onReady callback that adds the vjs-live CSS class (hides the native progressControl), injects custom timer elements + a full-width custom seek bar (#vjs-custom-seek-outer), and starts the 1s seek bar update tick.
src/lib/player-main.ts — Main Loop
runMain(cb) — Starts a 1s setInterval that detects URL changes via handleUrlChange() and routes to install/switch/uninstall callbacks. No playerTick — video.js handles all playback internally.
src/lib/url-watcher.ts — URL Change Detection
handleUrlChange(newPath, oldPath, wasInChannel, cb) — Detects channel navigation. Routes to install (no player found), switch (existing player found), or uninstall (left channel page). Always returns newPath as currentUrl so the 1s tick never re-detects the same change.
src/lib/native-player.ts — Native Player Suppression
| Function | Description |
|---|---|
playNative() |
Play the native Twitch video (used when toggling to native player) |
disableNativePlayer(reason) |
Mute, pause, hide, strip controls, unfocus all native .video-player__container video elements (skipping ours) |
disableNativePlayer skip logic: checks v.id === "player" || v.id === "vjs-player" || v.closest("#vjs-player-container") || v.closest(".video-js"). This is critical — video.js renames the <video> element, so checking only id misses it.
src/lib/volume.ts — Volume Control
| Function | Description |
|---|---|
setVolume(vol, player, volumeEl, handleRadius) |
Clamp [0,1], set player.volume, update width on BOTH #volume-slider track AND .slider-filled child, persist |
toggleMute(player, volumeEl, handleRadius) |
Toggle mute, restore last known volume, fire toast |
applySavedVolume(player) |
Read saved volume from storage and apply to player |
src/lib/play-pause.ts — Play/Pause State
| Function | Description |
|---|---|
pause() |
Track behind-live offset, call player.pause() |
play() |
Play with optional ad-break skip for black-screen watchdog |
src/lib/keyboard-handler.ts — Keyboard Shortcuts
| Key | Action |
|---|---|
| Space / K | Toggle play/pause (uses player.paused — actual HTMLVideoElement state) |
| Left arrow / J | Seek back seekBackStep (10s) |
| Right arrow / L | Seek forward seekForwardStep (30s, VOD only) |
| Up arrow | Volume up 5% |
| Down arrow | Volume down 5% |
| M | Toggle mute |
| F | Toggle fullscreen |
| T | Toggle theater mode |
| , | Decrease playback speed |
| . | Increase playback speed |
| C | Toggle chat overlay (fullscreen only) |
Uses capture-phase (true) to intercept keys before native handlers. Form-field blacklist (INPUT / TEXTAREA / role="textbox" / contenteditable) instead of a whitelist. Null-checks e.target before reading properties.
src/lib/seek-bar.ts — Seek Bar DOM Helpers
| Function | Description |
|---|---|
getSeekWidth() |
Calculate seek bar pixel width (container width - 560px) |
getTimeAtOffset(offsetX, maxTime, handleRadius) |
Convert pixel offset to time value, clamped to [0, maxTime] |
updateSeekBar(offsetX, maxTime, handleRadius, seekSlider) |
Update seek slider element width |
src/lib/switch-mode.ts — Mode Switching
switchMode(mode, videoMode, setVideoMode, setChatReplay, updateChatOverlayChannel, channel, vodId) — Switches between “live” and “vod” mode, updating the live indicator button, chat replay, and chat overlay.
src/lib/fetch-storyboard.ts — Storyboard Sprite Sheet Fetching
fetchStoryboard(vodBaseUrl, vodId, state) — Fetches {vodBaseUrl}../storyboards/{vodId}-info.json (Zod-parsed via StoryboardInfoSchema), preloads high-quality sprite sheets. Returns StoryboardSprite or null. A live stream’s storyboard doesn’t exist until the stream ends, so 403/404 is retried rather than treated as a hard error.
src/lib/resolve-vod-url.ts — VOD URL Resolution
resolveVodForChannel(channel, totalElapsed, state) — Resolves the most recent VOD for a channel via GQL, validates freshness against totalElapsed. Returns { vodId, hlsUrl } or null.
src/lib/get-m3u8.ts — HLS Master URL Resolution
getM3U8(isLive, channelOrVodId, clientId, authState) — Fetches GQL PlaybackAccessToken, then fetches the usher master.m3u8, parses with parseMasterManifest, validates with Zod. Returns the highest-resolution variant URL.
src/lib/overlays.ts — Error/Ad Overlays
| Function | Description |
|---|---|
showErrorOverlay(message) |
Show error overlay with message (no external links) |
showAdOverlay() |
Show ad-break overlay |
hideAdOverlay() |
Hide ad-break overlay |
src/lib/vod-position.ts — VOD Position Save/Restore
| Function | Description |
|---|---|
getSavedVodPosition(vodId) |
Read saved position from chrome.storage.local |
saveVodPosition(vodId, time) |
Write position to chrome.storage.local |
clearVodPosition(vodId) |
Remove saved position from chrome.storage.local |
src/lib/dom-helpers.ts — DOM Helpers
| Function | Description |
|---|---|
addStyle(css) |
Inject <style> tag with given CSS content |
src/lib/toast.ts — Toast Notifications
showToast(message) — Self-contained toast notification with internal timer. Shows at bottom-center of player, auto-hides after 2s.
src/lib/chat-replay.ts — Chat Replay Sync
setChatReplay(channel, vodId) — Toggles chat between live and VOD replay modes. 1s interval syncs hidden native player’s currentTime to DVR player’s adjustedTime in VOD mode.
src/lib/chat-overlay.ts — Fullscreen Chat Overlay
| Function | Description |
|---|---|
createChatOverlay() |
Build DOM: #chat-overlay > #chat-overlay-header + #chat-overlay-iframe |
updateChatOverlayChannel(channel, vodId) |
Sync iframe between live chat and VOD chat replay |
toggleChatOverlay(enabled) |
Show/hide the chat overlay |
isChatOverlayEnabled() |
Read from localStorage |
setChatOverlayEnabled(v) |
Write to localStorage |
loadChatOverlayPos() |
Read saved position from localStorage |
saveChatOverlayPos(pos) |
Write position to localStorage |
src/lib/controls-toggle.ts — Control Bar Show/Hide
| Function | Description |
|---|---|
hideToggle() |
Hide control bar elements |
showToggle() |
Show control bar elements |
showToggleForAWhile() |
Show controls temporarily, then auto-hide |
src/lib/parse-vod-playlist.ts — VOD Variant Playlist Parsing
parseVodVariantPlaylist(playlistUrl, vodId, state) — Parse VOD variant playlist, detect .ts vs .mp4 extension, build vodSegmentNames map for muted segments. Returns VodPlaylistState.
src/lib/clip.ts — Clip Creation
createClip(channel, clientId, adjustedTime, videoMode, vodId) — Create a Twitch clip via GQL.
src/lib/uninstall-player.ts — Player Cleanup
uninstallPlayer(container, player, ...state) — Cleanup: dispose video.js, clear timers, remove DOM, reset state.
src/lib/picker.ts — Quality Picker
togglePicker() — Toggle quality picker dropdown.
src/lib/speed-popover.ts — Speed Popover
| Function | Description |
|---|---|
toggleSpeedPopover(player) |
Toggle speed popover visibility |
closeSpeedPopover() |
Close speed popover |
src/lib/settings.ts — Settings Menu
toggleSettings() — Toggle settings menu (chat overlay toggle, etc.).
src/lib/fullscreen.ts — Fullscreen
toggleFullscreen() — Enter/exit fullscreen, capture theater state for restore on exit.
src/lib/theater-mode.ts — Theater Mode State
| Function | Description |
|---|---|
setTheaterMode(target) |
Idempotent setter: toggle .video-player__container--theatre class |
restoreTheaterMode() |
Restore theater mode from saved preference |
toggleTheaterMode() |
Flip theater mode |
src/lib/controls-toggle.ts — Control Bar Toggle
| Function | Description |
|---|---|
hideToggle() |
Hide control bar |
showToggle() |
Show control bar |
showToggleForAWhile() |
Show controls temporarily |