@hoangquyet/ytdown
v1.0.2
Published
Zero-dependency YouTube downloader built on a reverse-engineered InnerTube WEB client. Download by quality or itag, read metadata as JSON.
Maintainers
Readme
@hoangquyet/ytdown
A YouTube downloader built on a reverse-engineered InnerTube WEB client.
Zero runtime dependencies — no ytdl-core, no yt-dlp, no youtubei.js, no
protobuf library. The protocol work is done in this package: the protobuf codec,
the UMP demuxer, the SABR request loop, and the player-script analysis are all
part of the source tree.
import YTdownload from '@hoangquyet/ytdown';
const video = await YTdownload.info('https://www.youtube.com/watch?v=dQw4w9WgXcQ');
console.log(video.title);
const result = await YTdownload.down('dQw4w9WgXcQ', { quality: '1080' });
console.log(result.path); // → /full/path/to/video.mp4Install
npm install @hoangquyet/ytdownRequires Node.js 18 or newer. ffmpeg on PATH is needed only to mux
separate video and audio tracks together — short videos that offer a muxed
format, and audio-only downloads, need nothing else.
Quick start
import YTdownload from '@hoangquyet/ytdown';
// Metadata
const info = await YTdownload.info('https://youtu.be/dQw4w9WgXcQ');
// Best quality, muxed into one file
const { path } = await YTdownload.down('https://youtu.be/dQw4w9WgXcQ');
// Cap the resolution
await YTdownload.down('dQw4w9WgXcQ', { quality: '720' });
// Audio only
await YTdownload.down('dQw4w9WgXcQ', { audioOnly: true });
// Audio only, transcoded to mp3
await YTdownload.down('dQw4w9WgXcQ', { audioOnly: true, mp3: true });
// A specific format, by itag
await YTdownload.down('dQw4w9WgXcQ', { itag: 137 });
// Everything as JSON, with n-transformed media URLs
const json = await YTdownload.json('dQw4w9WgXcQ');API
The default export is an object:
import YTdownload from '@hoangquyet/ytdown';
// YTdownload.down · YTdownload.info · YTdownload.formats · YTdownload.json ·
// YTdownload.describe · YTdownload.sessionNamed exports are available too:
import { down, info, formats, json, describe, session } from '@hoangquyet/ytdown';Every method accepts anything you are likely to paste: a watch URL, a
youtu.be link, a shorts link, an embed, or a bare eleven-character id.
YTdownload.down(input, options?)
Download a video. Resolves to { path, info, tracks }, where path is the
absolute path of the saved file. When ffmpeg is unavailable and the tracks
cannot be muxed, path is null and videoPath/audioPath are set instead.
| option | type | default | meaning |
|---|---|---|---|
| quality | 'best' \| 'audio' \| string | 'best' | 'best', 'audio', or a max height such as '1080' or '720' |
| itag | number | — | download a specific format by itag, overriding quality |
| audioOnly | boolean | false | only the audio track |
| videoOnly | boolean | false | only the video track |
| mp3 | boolean | false | with audioOnly, transcode to mp3 |
| outputDir | string | process.cwd() | output directory (output is accepted as an alias) |
| outputPath | string | — | exact output file path (file is accepted as an alias) |
| container | 'mp4' \| 'webm' \| 'mkv' | auto | force the output container |
| keepTracks | boolean | false | also keep the separate video/audio files |
| transport | 'auto' \| 'progressive' \| 'direct' \| 'session' \| 'playback' | 'auto' | how to fetch the media |
| rate | number | 16 | playback-capture speed multiplier |
| headless | boolean | false | run the session browser without a window |
| attest | boolean | true | mint a PO token |
| ffmpegPath | string | — | path to the ffmpeg binary |
| onProgress | function | — | ({ bytes, bufferedMs, durationMs }) => void |
YTdownload.info(input)
Resolves to { videoId, title, author, channelId, durationSeconds, viewCount,
publishDate, isLive, thumbnails, formats, bestVideo, bestAudio }.
YTdownload.formats(input)
Resolves to the normalised list of formats. Each entry carries itag, kind
('video' | 'audio'), muxed, height, qualityLabel, codecs, bitrate,
contentLength, and more — everything you need to pick an itag for down().
YTdownload.json(input) / YTdownload.describe(input)
The full resolved payload: metadata plus every media URL, already
n-transformed so each is directly usable by curl, ffmpeg, or a player. Each
format carries a reach field:
| reach | meaning |
|---|---|
| whole | the URL streams the entire file — muxed formats only |
| partial | adaptive; the URL stops short and cannot be resumed |
| none | no direct URL, so only SABR can fetch it |
recommended.muxed is the one to reach for when you just want a working link.
YTdownload.session(input, options?)
Exports a reusable session as JSON. Downloading against an exported session
(YTW_SESSION) needs no browser at all, so a credential minted on one machine
can be used on a headless one:
node -e "import('@hoangquyet/ytdown').then(m => m.session('dQw4w9WgXcQ').then(s => console.log(JSON.stringify(s))))" > session.json
YTW_SESSION=session.json node src/cli.js download dQw4w9WgXcQCLI
The package also ships a ytw command:
npx ytw <url|id> # best quality
npx ytw info <url|id> # metadata
npx ytw formats <url|id> # every offered format
npx ytw json <url|id> # metadata + media urls as JSON
npx ytw download <url|id> -q 1080 # cap the resolution
npx ytw download <url|id> --audio-onlyOptions
-o, --output <dir> output directory (default: current directory)
-f, --file <path> exact output file path
-q, --quality <spec> best | audio | a max height such as 1080
--audio-only download only the audio track
--mp3 with --audio-only: transcode to mp3
--video-only download only the video track
--container <ext> force the output container (mp4 | webm | mkv)
--keep-tracks also keep the separate video and audio files
--transport <mode> auto | progressive | direct | session | playback
--rate <n> playback capture speed multiplier (default: 16)
--headless run the session browser without a window
--urls-only with json: emit only formats that carry a url
--compact with json: no indentation
--no-attest skip PO token minting
--ffmpeg <path> path to the ffmpeg binary
-v, --verbose verbose logging
--quiet errors only
-h, --help show this helpTransports
| transport | behaviour |
|---|---|
| progressive | plain media URLs, no browser at any point |
| direct | SABR over plain HTTP, also browserless; a cold session is capped at roughly a minute |
| session | downloads in Node against a trusted session (YTW_SESSION, cache, or a browser opened once) |
| playback | plays the video in a browser and keeps what its player fetches — a last resort |
| auto | progressive when plain URLs are available, direct for short videos, session for anything longer |
Only browser surfaces are impersonated — the desktop client and, when it helps, the mobile web client. No ANDROID / iOS / TV / visionOS client is used anywhere.
Requirements
- Node.js 18 or newer
- ffmpeg on
PATH(or--ffmpeg <path>) for muxing video and audio together - Google Chrome or Microsoft Edge — only as a fallback, for when the mobile surface declines and the video is longer than the plain-SABR limit
How it works
- Bootstrap. Load the watch page for cookies,
INNERTUBE_API_KEY, the client version,visitorData, and the player'sbase.jsURL. - Player analysis. Every media URL carries a throttling-deterrent
nparameter, and SABR endpoints reject an untransformed one with a bare 403 — so this step is mandatory. The transform is located structurally and driven inside a locked-downnode:vm. - Player response.
POST /youtubei/v1/playerwith the WEB context yields the format ladder andserverAbrStreamingUrl. - SABR loop. Build a
VideoPlaybackAbrRequest, POST it, demux the UMP part stream, reassemble segments, and report progress so the server's serving window slides forward. - Mux. Concatenate each track's segments and stream-copy them into one container with ffmpeg. No re-encoding. The container is chosen by codec, not mime label.
Tests
npm test60 tests covering the protobuf codec, UMP framing, format selection, URL
parsing, SABR message encoding, and the n transform against a value captured
from a real session.
