Manifest V3 Chrome extension for finding and saving media loaded by the current page. Version 1.5.2 (1.5.0 plus download-stall fixes and a YouTube converter repair) adds a streaming & CDN engine: automatic discovery (network, DOM, embedded players, redirects, signed URLs), full HLS and MPEG-DASH downloading with separate audio/video merging, DRM/encryption diagnostics, a resumable disk-backed download manager with pause/cancel/speed/ETA, and a repaired YouTube detector with configuration diagnostics in the full build.
Only save content you own, that is public domain, or that you have the right to save.
Requires Chrome 116 or newer. Clone the repository, open chrome://extensions, enable Developer mode, and choose Load unpacked with this folder. No build step is required. After updating, reload the extension. Refresh can reinject the scanner into already-open pages; reload pages to update other content scripts.
Tagged archives are available from GitHub Releases; unreleased changes require installation from source.
Open a page, play its media, then select Media Scanner in the popup. The list updates automatically while the popup is open (the service worker pushes changes over a port; polling remains as a fallback). All / Audio / Video / Playlists filters; each row shows:
- filename, media type and MIME type, size (from headers or a 1-byte range probe), CDN host and provider (CloudFront, Akamai, Fastly, Cloudflare, Google Video, …), the originating website, and via redirect when the media was reached through redirects;
- for HLS/DASH: available resolutions, codecs, audio tracks (name/language), duration, live/VOD, segment container and segment CDN hosts, plus quality and audio track selectors;
- signed-URL provider and expiry, and a clear status: No encryption detected, DRM-protected (Widevine/PlayReady/FairPlay), Encrypted HLS (AES-128 / SAMPLE-AES), Link expired, Access denied/login page, Not found or Unsupported format (Smooth Streaming, HDS);
- Download / Retry / Download again, Copy URL, and for running jobs Pause/Resume, Cancel, progress, speed, ETA, bytes and a separate conversion phase.
Download all uses the current filter and selected qualities. Parallel sets the number of concurrent segment requests per download (1–8, default 4). Up to three downloads run at once; the rest queue.
| Source | What is found |
|---|---|
webRequest responses |
MP4, WebM, MOV/MKV, MP3, AAC, M4A, OGG, Opus, FLAC, WAV, HLS (.m3u8, application/x-mpegURL, Azure format=m3u8-aapl), DASH (.mpd, application/dash+xml, format=mpd-time-csf), Smooth/HDS (reported as unsupported). Segment requests (.ts, .m4s, CMAF) are counted per CDN host instead of listed. |
| Redirects | onBeforeRedirect chains: the row is keyed by the stable original URL, downloads use the final CDN URL, and an expired signed URL falls back to the original URL so the CDN can issue a fresh one. |
| DOM | <video>/<audio>/<source>, media links, og:video/twitter:player:stream meta, preload links, data-* player attributes (e.g. video.js data-setup), inline scripts and JSON-LD (contentUrl) including \/-escaped JSON. |
| JavaScript players (Refresh) | Sources already exposed by JW Player, video.js and common player/hls/dash/shaka globals, read in the page's main world without changing player state. |
| Resource Timing | Requests made before the popup opened or before the service worker woke. |
| EME | encrypted events and the key system named in their init data (pssh) — detection only. |
Rows are deduplicated by URL identity: signature/expiry/range parameters (CloudFront, AWS SigV4, Google Cloud, Akamai tokens, Azure SAS, googlevideo, Meta CDN, Wowza, secure-link) are ignored for identity, so a re-signed URL refreshes the existing row; unrelated query strings (for example ?token=one vs ?token=two) stay separate. Detections are per tab, stored in chrome.storage.session, and reset on navigation.
| Stream | Behavior |
|---|---|
| HLS master playlists | All variants with resolution, bandwidth, codecs and frame rate; audio renditions (EXT-X-MEDIA) with name/language/channels; best quality by default or the selected one. |
| HLS MPEG-TS | Segments downloaded in order, remuxed to MP4 locally with mux.js (H.264/AAC). Separate TS/AAC audio renditions are remuxed separately and merged with synchronized timestamps. Unsupported codecs fall back to the original TS. |
| HLS fragmented MP4 / CMAF | EXT-X-MAP init segments, byte ranges, separate audio renditions merged into one MP4. |
| DASH | SegmentTemplate ($Number$, $Time$, $RepresentationID$, $Bandwidth$, %0Nd widths), SegmentTimeline (including r=-1), SegmentList (URLs and mediaRange), SegmentBase/single files, BaseURL inheritance and multi-period concatenation when init segments match. Video and audio representations are merged into MP4. WebM (VP9/Opus) representations are merged losslessly with the bundled FFmpeg in the full build and saved as separate playable files in the store build. |
| Live HLS/DASH | The segments listed at download time are saved (with a note). Number-based live DASH templates are reported as unsupported. |
| Encrypted / DRM | Protected – not downloadable: HLS METHOD=AES-128/SAMPLE-AES/SAMPLE-AES-CTR, FairPlay (skd://, com.apple.streamingkeydelivery), Widevine/PlayReady key formats, DASH ContentProtection (Widevine, PlayReady, FairPlay, CENC), encrypted MP4 init segments (encv/enca/sinf/tenc/pssh) and SAMPLE-AES elementary streams inside TS. Detection stops processing before any key, license or encrypted media segment request. |
| Smooth Streaming, HDS | Reported as unsupported formats. |
Separate audio and video are merged by a built-in fragmented-MP4 muxer (shared/mp4.js): it rebuilds the moov with both tracks, rescales movie-timescale fields, rewrites track IDs, sequence numbers and absolute base_data_offsets, normalizes start times while keeping relative A/V offsets and interleaves fragments by decode time. Every output is validated (container structure, expected audio/video tracks, not HTML/empty) before it is saved, and the saved file is checked through chrome.downloads (exists, size matches) before a job is marked complete.
- Concurrent segment downloads with a configurable limit and a bounded reorder window: segments are written strictly in order while at most concurrency + 2 segments are held in memory.
- Exponential backoff with jitter (and
Retry-After) for timeouts, network errors, 408/425/429/5xx and incomplete ranges; 401/403/404/410 and HTML responses fail immediately with a specific explanation (expired signed URL, authentication required, not found, login page). - Pause/Resume and Cancel from the popup or the download tab. Cancel aborts in-flight requests, releases temporary storage and never saves a file.
- Progress, speed and ETA for segment jobs (download tab) and direct jobs (
chrome.downloads). - Disk-backed temporary storage: segment data is written to the extension's Origin Private File System in 32 MB committed chunks and the final file is assembled from those files, so large downloads do not need to fit in memory (in the Node test a 400 MB download peaked at roughly 130–155 MB of extra buffer memory across runs; by design the window is bounded by concurrency + 2 segments, see
test-large.cjs). Falls back to browser memory when OPFS is unavailable. - Partial-download recovery: committed chunks and a checkpoint survive a failed or closed download tab; Retry resumes after the last committed segment when the playlist is unchanged (a changed playlist restarts cleanly). Direct downloads interrupted by network errors are resumed with
chrome.downloads.resumewhen Chrome allows it. - Referrer/CORS-restricted media can use the original page/frame as a fetch fallback (browser-approved Referer and cookies; 32 MB per request; forbidden headers are never overridden).
- Detections, queue and job progress live in
chrome.storage.session; the worker reconciles running jobs on every start (closed download tabs → failed with resume hint, finishedchrome.downloadsitems → verified). - Download tabs must claim their job from the worker; restored or duplicated tabs (for example after session restore) cannot re-run a job. One active job per resource and quality.
- Every handled message replies, unhandled commands do not keep channels open, badge updates on closed tabs are caught, maps are bounded and ports are removed on disconnect.
- Success is only reported after the output passed validation and the saved file was verified.
Blob URLs are player-local and skipped. Pages such as YouTube can show This site streams in segments – use the Video Grabber tab. Browser-protected pages cannot be scanned. Set DEBUG: true in config.js for scanner console logs.
Video Grabber retains previews, direct downloads, quality detection, HLS-to-MP4 conversion and existing Instagram lookup. If its TS conversion fails, it saves the original TS with an explanation.
In the full build, open a YouTube video and let it play until Video Grabber detects the existing video/audio URLs. Select YouTube, Refresh tracks, and choose:
- MP4 (video+audio) and a detected quality, such as 1080p, 720p or 480p.
- MP3 (audio only) at 128, 192 or 320 kbps.
Download progress is followed by Converting… and saving through chrome.downloads.download. Filenames use the sanitized video title. Cancel stops downloading/conversion or cancels saving. Keep the conversion tab open.
This feature reuses existing detected googlevideo tracks. It does not introduce signature-cipher extraction or protection-circumvention code. Refresh recovers direct tracks from the page's request history and current player metadata, including playback started before opening the popup, and updates automatically while the tab is open. Player metadata is read from the already-loaded page; ciphered/protected entries and stale responses from a previous video are skipped. Diagnostics distinguish no observed requests from unsupported UMP/SABR playback and count failed responses, total resources, players and direct metadata URLs. A blob player alone does not block usable HTTP tracks. Refresh expired URLs by replaying the source video. One YouTube conversion runs at a time. There are no fixed input-size caps; conversion still depends on available browser/WebAssembly memory.
1.5.2: pipeline stages and diagnostics. The YouTube tab shows the current stage (1 detecting media, 2 retrieving metadata, 3 discovering tracks, 4 connecting to the media server, 5 downloading video, 6 downloading audio, 7 merging/converting, 8 saving) and a Diagnostics panel with Copy diagnostics (no cookies, signatures, tokens or page URL). H.264, VP9 and AV1 video is now copied into MP4 instead of being re-encoded, which previously made VP9-only qualities take several times the video's length. The whole pipeline was verified in real Chromium against a local YouTube simulation, not against live YouTube. See YouTube diagnostics and root-cause analysis for what each stage means and which playback configurations cannot be supported.
1.5.0 repair. Code review of the 1.4.3 detector found these causes of missing tracks (verified with unit tests against recorded URL/metadata shapes, not against live YouTube, which this development environment cannot reach): unknown/new itags (for example 599/600/774 audio, AV1 694–702, HDR 330–337) were classified as video whenever the response carried no content type, so audio could be missing and "itag N" qualities appeared; multi-language and DRC audio renditions share an itag and overwrote each other; only movie_player and the possibly stale ytInitialPlayerResponse were read; and /embed/ and /live/ URLs yielded no video id. Now the URL's mime/xtags parameters decide track type and audio language, distinct renditions are kept (original, non-DRC audio preferred), player metadata is read from movie_player, shorts-player, ytd-watch-flexy, ytd-player, ytplayer and ytInitialPlayerResponse (only when it matches the current video id), and expired URLs are skipped.
The most common remaining reason for "no tracks" is not a bug: when YouTube plays a video through UMP/SABR (server-driven POST requests) or lists only signature-ciphered formats, there are no directly fetchable track URLs. The tab now names the configuration — direct tracks, UMP/SABR, ciphered (no deciphering is performed), DRM, sign-in/age restriction, unplayable, upcoming, expired, or live (whose ordinary HLS manifest is handed to the Media Scanner) — instead of a generic empty state. Outputs are validated before saving, the saved size is verified, and HTTP 403 track URLs fail immediately with an explanation. YouTube conversion still holds both tracks in memory, because FFmpeg WebAssembly needs its inputs in its in-memory file system.
FFmpeg WebAssembly is bundled and runs in a separate worker: H.264, VP9 and AV1 video is copied into the MP4 without re-encoding, AAC audio is copied and Opus/Vorbis audio is encoded to AAC; only other video codecs are transcoded to H.264. MP3 uses libmp3lame and the output bitrate is verified. No remote scripts or conversion servers are used.
| Permission | Purpose |
|---|---|
webRequest |
Observe media response types/sizes and permitted Referer headers without blocking or modifying requests. |
downloads |
Save files, track progress and cancel downloads. |
storage |
Per-tab detections and job state in temporary session storage; the parallel-download setting and resumable checkpoints in local storage. Temporary media chunks use the extension's Origin Private File System (no extra permission). |
scripting |
Refresh already-open pages/frames and retain existing site lookup. |
host_permissions: ["<all_urls>"] |
Observe pages and fetch media from arbitrary CDNs; Chrome still restricts protected browser pages. |
Declarative content scripts match <all_urls> at document_idle in all frames. The full build's wasm-unsafe-eval CSP permits the bundled WebAssembly core; scripts and workers remain restricted to the extension itself. No offscreen permission is needed because conversion uses a worker in a dedicated extension tab.
No accounts, telemetry or external processing servers. Requests go directly to original media hosts. URLs, referrers, titles and job status are handled locally. See PRIVACY.md.
ENABLE_YOUTUBE in config.js controls the feature. Build scripts create both packages in dist/:
| Build | YouTube support |
|---|---|
full |
Enabled; intended for GitHub/unpacked installation. |
store |
Disabled; YouTube tab, conversion code and FFmpeg assets omitted, and WebAssembly CSP permission removed. |
Use the store package for Chrome Web Store submission; YouTube download functionality is excluded. Exclusion does not guarantee store approval.
powershell -ExecutionPolicy Bypass -File scripts\build.ps1# macOS/Linux: Python 3 is required by this build script.
bash scripts/build.shVersion tags run the existing release workflow. See CHANGELOG.md.
Node.js 20+ and native FFmpeg/ffprobe are required (FFmpeg generates local fixtures and independently validates outputs; extension users need no native FFmpeg).
node scripts/run-tests.cjs # everything below, with a summary
node scripts/run-tests.cjs dash # only matching test files
VERBOSE=1 node scripts/run-tests.cjs # full output| Area | Test |
|---|---|
| Direct media detection, JS players, per-tab association | test-discovery.cjs |
| HLS master/media playlists, fMP4/TS separate audio, quality selection | test-hls-engine.cjs |
| DASH parsing (Template/Timeline/List/Base, multi-period) and downloads, WebM fallback | test-dash.cjs |
| Separate audio/video merging (offsets, timescales, normalization) | test-merge.cjs |
| CDN redirects, signed/expired URLs, auth/HTML/404 errors | test-cdn.cjs |
| Segment retries, backoff, ordering, pause, cancellation | test-engine.cjs |
| 400 MB disk-backed download, checkpoint resume, cancel cleanup | test-large.cjs (run with --expose-gc) |
| Manifest V3 worker restarts, claims, duplicates, verified completion | test-worker-restart.cjs |
| DRM detection and rejection (HLS, DASH, MP4 init, TS, EME) | test-drm.cjs |
| YouTube classification and diagnostics | test-youtube-engine.cjs |
| YouTube pipeline in real Chromium against a local YouTube simulation (optional, Playwright) | test-youtube-e2e.cjs |
| 1.4.x regressions (scanner, conversion, popup race, YouTube WASM, messages) | the other test-*.cjs files |
Optional suites (reported as SKIP when unavailable):
test-browser-e2e.cjs [dist/full|dist/store]loads the built extension into Chromium through Playwright (PLAYWRIGHT_MODULE,CHROME_PATH) and drives the real worker, webRequest, content scripts, downloads, download tabs, OPFS and the popup.test-public-streams.cjsdownloads the first segments of public authorized test streams (Apple, Mux, DASH-IF/Akamai, Shaka demo assets) and verifies them with ffprobe.
After building, node scripts/verify-build.cjs checks both packages (manifests, referenced files, syntax, store exclusions, zip contents).
node scripts/manual-fixtures.cjs serves generated fixtures at http://127.0.0.1:8765/ (separate-audio HLS, DASH variants, WebM DASH, DRM samples, redirects, expired/login URLs, a 1 GB resumable file and a player-config page), the popup preview with mock APIs at /preview.html, and /wasm-preview.html for the bundled FFmpeg worker. Follow the manual checklist for Google Chrome and Brave.
Scanner modules: background/scanner.js, content/dom-scan.js, ui/media-tab.js. Streaming engine: shared/hls.js, shared/dash.js (with shared/xml.js), shared/mp4.js, shared/download-engine.js, shared/drm.js, shared/cdn.js, downloader.js, ts-converter.js. YouTube modules: background/youtube.js, shared/youtube.js, ui/youtube-tab.js, and youtube/. See the 1.5.0 implementation report for architecture, limitations and verification.
Project code is MIT. Bundled mux.js is Apache-2.0. The full build bundles FFmpeg core under GPL-2.0-or-later; see its notices and source references. Third-party license terms apply to their respective components.