@irithell-js/yt-play
v0.4.6
Published
YouTube search + download engine (audio/video) with optional caching.
Maintainers
Readme
@irithell-js/yt-play
High-performance YouTube audio/video download engine with intelligent caching, live stream chunking, metadata extraction, and bundled yt-dlp + aria2c binaries for blazing fast downloads. Zero system dependencies.
Table of Contents
- Features
- Installation
- Quick Start
- Configuration
- API Reference
- Type Reference
- Auto-Update System
- File Formats
- Performance
- Troubleshooting
- Requirements
- Binaries
- Changelog
Features
- Bundled Binaries — yt-dlp, aria2c, and deno included, no system dependencies required
- Auto-Update System — yt-dlp updates automatically in background or on-demand when a download fails
- Ultra Fast Downloads — aria2c acceleration with up to 16 parallel connections (up to 5x faster)
- Live Stream Chunking — Segment active or finished live streams into timed chunks via FFmpeg, with auto-reconnect
- Playlist Support — Extract and resolve direct media URLs from entire playlists with configurable concurrency and duration filters
- Native Streaming — Get raw
Readablestreams directly from YouTube CDN without saving to disk - Metadata Stalking — Deep extraction of videos, channels, and live streams (real-time viewers, tags, subscriber counts, etc.)
- Intelligent Caching — TTL-based in-memory cache with automatic cleanup, size limits, and event-driven readiness
- Smart Quality — Automatically reduces quality for long videos (>1h), configurable quality presets
- JS Runtime Auto-Detection — Automatically detects bundled deno, system deno, or node to solve YouTube's JS challenges
- Player Client Selection — Switch between Android, iOS, or web YouTube clients to bypass restrictions
- Container Ready — Works in Docker/isolated environments; respects existing
yt-dlp.confwithout overwriting user settings - Cross-Platform — Linux (x64/arm64), macOS (x64/arm64), Windows x64
- TypeScript — Full type definitions included, ships as ESM and CommonJS
Installation
npm install @irithell-js/yt-playyt-dlp, aria2c, and deno binaries are automatically downloaded for your platform during npm install.
Quick Start
ESM
import { PlayEngine } from "@irithell-js/yt-play";
const engine = new PlayEngine();
const metadata = await engine.search("linkin park numb");
if (!metadata) throw new Error("Not found");
const requestId = engine.generateRequestId();
await engine.preload(metadata, requestId);
const { file: audio } = await engine.getOrDownload(requestId, "audio");
const { file: video } = await engine.getOrDownload(requestId, "video");
console.log("Audio:", audio.path);
console.log("Video:", video.path);
engine.cleanup(requestId);CommonJS
const { PlayEngine } = require("@irithell-js/yt-play");
const engine = new PlayEngine();
async function run() {
const metadata = await engine.search("song name");
const requestId = engine.generateRequestId();
await engine.preload(metadata, requestId);
const { file } = await engine.getOrDownload(requestId, "audio");
console.log("Downloaded:", file.path);
engine.cleanup(requestId);
}
run();Configuration
PlayEngineOptions
Pass options to the PlayEngine constructor. All fields are optional.
const engine = new PlayEngine({
// ── Cache ─────────────────────────────────────────────────────────────────
cacheDir: "./cache", // Cache directory (default: OS temp dir)
ttlMs: 5 * 60_000, // Cache entry TTL in ms (default: 3 min)
cleanupIntervalMs: 30_000, // Cleanup interval in ms (default: 30s)
preloadBuffer: true, // Load files into RAM after download (default: true)
maxPreloadBufferSize: 50 * 1024 * 1024, // Max file size to buffer in RAM (default: 50 MB)
// ── Quality ───────────────────────────────────────────────────────────────
preferredAudioKbps: 128, // 320 | 256 | 192 | 128 | 96 | 64 (default: 128)
preferredVideoP: 720, // 1080 | 720 | 480 | 360 (default: 720)
maxPreloadDurationSeconds: 1200, // Max video duration for preload (default: 20 min)
// ── Performance ───────────────────────────────────────────────────────────
useAria2c: true, // Use aria2c downloader (default: auto-detected)
concurrentFragments: 8, // Parallel download fragments (default: 5)
ytdlpTimeoutMs: 300_000, // yt-dlp process timeout in ms (default: 5 min)
// ── Player Client ─────────────────────────────────────────────────────────
// Controls which YouTube client yt-dlp impersonates.
// "default" lets yt-dlp choose automatically (recommended).
// Use "ios" or "web" if you encounter bot-detection or format errors.
playerClient: "default", // "android" | "ios" | "web" | "default" (default: "default")
// ── JS Runtime ────────────────────────────────────────────────────────────
// Runtime used to solve YouTube's JS challenges (required for full format access).
// "auto" tries bundled deno → system deno → node (default).
// "deno" forces deno only. "node" forces node only.
jsRuntime: "auto", // "deno" | "node" | "auto" (default: "auto")
// Use this to pass any runtime string directly, bypassing auto-detection.
// Supports any yt-dlp-compatible runtime or a comma-separated list.
jsRuntimeOverride:
"deno:/home/container/.deno/bin/deno,node:/usr/local/bin/node",
// ── Binaries (auto-detected if omitted) ───────────────────────────────────
ytdlpBinaryPath: "./bin/yt-dlp",
aria2cPath: "./bin/aria2c",
ffmpegPath: "/usr/bin/ffmpeg",
ffmpegArgs: ["-threads", "4"], // Extra FFmpeg post-processor arguments
// ── Authentication ────────────────────────────────────────────────────────
cookiesPath: "./cookies.txt", // Netscape-format cookies file
cookiesFromBrowser: "firefox", // chrome | firefox | edge | safari
// ── Daemon Mode ───────────────────────────────────────────────────────────
// Limits concurrent yt-dlp processes to prevent resource exhaustion.
daemon: {
enabled: true,
maxWorkers: 4, // Max concurrent yt-dlp processes (default: 4)
},
// ── Logging ───────────────────────────────────────────────────────────────
logger: console,
});Cookie Configuration (VPS / Docker)
In headless environments, YouTube often requires authentication to avoid bot-detection.
Option 1 — Cookies file:
- Install "Get cookies.txt LOCALLY" in your browser
- Log into youtube.com
- Export cookies as
cookies.txt(Netscape format)
const engine = new PlayEngine({ cookiesPath: "/path/to/cookies.txt" });Option 2 — Extract from installed browser:
const engine = new PlayEngine({
cookiesFromBrowser: "firefox", // chrome | firefox | edge | safari
});The engine automatically manages cookie settings in yt-dlp.conf on startup. Any existing user configuration in the file (such as --js-runtimes, --remote-components, or custom flags) is preserved — only the cookie lines are managed by the engine.
API Reference
Standalone Functions
These are exported directly from the package root and work without any class instantiation.
searchBest(query: string): Promise<PlayMetadata | null>
Searches YouTube for a video by name or resolves a direct YouTube URL. Returns null if no result is found.
searchPlaylistBest(query: string): Promise<PlayMetadata | null>
Searches YouTube for a playlist by name or resolves a direct playlist URL. Returns the playlist's base metadata including its listId. Returns null if no result is found.
normalizeYoutubeUrl(input: string): string | null
Normalizes any YouTube URL variant (shorts, embeds, youtu.be, markdown-wrapped links, angle-bracket-wrapped links) to a canonical https://www.youtube.com/watch?v=VIDEO_ID form. Returns null if no valid video ID is found.
getYouTubeVideoId(input: string): string | null
Extracts the 11-character video ID from any YouTube URL variant. Returns null if the input is not a recognizable YouTube URL.
PlayEngine
The main high-level engine. Handles searching, preloading, caching, and downloading.
new PlayEngine(options?: PlayEngineOptions)
See Configuration for all options.
engine.search(query): Promise<PlayMetadata | null>
Searches for a video by name or resolves a direct URL.
const meta = await engine.search("never gonna give you up");
const meta2 = await engine.search(
"https://www.youtube.com/watch?v=dQw4w9WgXcQ",
);engine.generateRequestId(prefix?): string
Generates a unique, cryptographically safe request ID for use as a cache key.
const requestId = engine.generateRequestId("player"); // "player_1234567890_a1b2c3d4"engine.preload(metadata, requestId, targetType?): Promise<void>
Pre-downloads audio and/or video in parallel and stores the result in cache.
await engine.preload(metadata, requestId); // both audio and video
await engine.preload(metadata, requestId, "audio"); // audio only
await engine.preload(metadata, requestId, "video"); // video onlyFor videos longer than 1 hour, audio is automatically reduced to 96 kbps and video download is skipped, regardless of targetType.
| Parameter | Type | Default | Description |
| ------------ | ------------------------------ | -------- | ------------------------------------ |
| metadata | PlayMetadata | required | Video metadata from search() |
| requestId | string | required | Cache key from generateRequestId() |
| targetType | "audio" \| "video" \| "both" | "both" | Which media types to preload |
engine.getOrDownload(requestId, type, fallbackMetadata?): Promise<{ metadata, file, direct }>
Returns a cached file if available, otherwise downloads directly.
const { metadata, file, direct } = await engine.getOrDownload(
requestId,
"audio",
);
console.log(file.path); // "/tmp/yt-play/cache/audio_xxx.m4a"
console.log(file.size); // 8457234 (bytes)
console.log(file.info.quality); // "128kbps m4a"
console.log(file.buffer); // Buffer | undefined
console.log(direct); // false = from cache, true = just downloadedIf requestId is not in cache, pass fallbackMetadata to initiate a direct download without a prior preload() call:
const { file } = await engine.getOrDownload(requestId, "audio", metadata);| Parameter | Type | Description |
| ------------------ | ------------------------- | -------------------------------------------------------- |
| requestId | string | Cache key |
| type | "audio" \| "video" | Media type to retrieve |
| fallbackMetadata | PlayMetadata (optional) | Used to create a cache entry if requestId is not found |
engine.waitCache(requestId, type, timeoutMs?): Promise<CachedFile | null>
Event-driven wait for a preloaded file to become available in cache. Returns null on timeout. Resolves immediately if the file is already cached.
const cached = await engine.waitCache(requestId, "audio", 8_000);
if (cached) {
console.log("Ready:", cached.path);
} else {
const { file } = await engine.getOrDownload(requestId, "audio");
}| Parameter | Type | Default | Description |
| ----------- | -------------------- | -------- | ---------------------- |
| requestId | string | required | Cache key |
| type | "audio" \| "video" | required | Media type to wait for |
| timeoutMs | number | 8000 | Max wait time in ms |
engine.getFromCache(requestId): CacheEntry | undefined
Reads the raw cache entry without triggering any download.
const entry = engine.getFromCache(requestId);
if (entry?.loading) console.log("Still preloading...");
if (entry?.audio?.path) console.log("Audio ready:", entry.audio.path);engine.cleanup(requestId): void
Removes the cache entry and deletes associated files from disk.
engine.cleanup(requestId);engine.stream — StreamEngine
The StreamEngine instance is exposed as engine.stream. See StreamEngine.
engine.cache — CacheStore
The internal CacheStore instance, exposed for advanced use.
YtDlpClient
A lower-level client that wraps yt-dlp directly. Use this when you need raw playlist resolution, direct URLs, or full metadata without the caching layer.
// ESM
import { YtDlpClient } from "@irithell-js/yt-play";
// CJS
const { YtDlpClient } = require("@irithell-js/yt-play");
const client = new YtDlpClient({
playerClient: "android",
cookiesFromBrowser: "firefox",
daemon: { enabled: true, maxWorkers: 4 },
});client.getInfo(url): Promise<YtDlpVideoInfo>
Fetches basic metadata for a single video or live stream.
const info = await client.getInfo("https://youtube.com/watch?v=VIDEO_ID");
// info.id, info.title, info.uploader, info.duration, info.is_live, info.live_statusclient.getDirectUrl(url, type?): Promise<string>
Resolves the direct CDN streaming URL for a video. The URL expires after a short period (typically a few hours).
const url = await client.getDirectUrl(
"https://youtube.com/watch?v=VIDEO_ID",
"audio",
);
// Returns a direct CDN URL (e.g. https://rr1---sn-...googlevideo.com/...)| Parameter | Type | Default | Description |
| --------- | ------------------------------ | --------- | ----------------- |
| url | string | required | YouTube video URL |
| type | "audio" \| "video" \| "both" | "audio" | Format preference |
client.getPlaylistInfo(url, opts?): Promise<YtDlpPlaylistInfo>
Fetches playlist metadata and optionally resolves direct streaming URLs for each entry.
const playlist = await client.getPlaylistInfo(meta.url, {
limit: 10,
resolveLinks: true,
type: "audio",
batchSize: 15,
startItem: 1,
endItem: 50,
maxDurationSeconds: 600,
minDurationSeconds: 30,
reverse: false,
});See YtDlpPlaylistOptions for all fields.
client.resolvePlaylistItems(entries, limit, type?, concurrencyLimit?): Promise<YtDlpResolvedItem[]>
Resolves direct CDN URLs for a list of playlist items using a worker-pool approach.
const resolved = await client.resolvePlaylistItems(
playlist.entries,
10,
"audio",
15,
);client.exec(args): Promise<string>
Runs yt-dlp with arbitrary arguments and returns stdout as a string. Respects the daemon task queue if enabled.
const stdout = await client.exec(["-J", "--no-warnings", url]);
const info = JSON.parse(stdout);StreamEngine
Bypasses disk storage and returns Readable streams, or starts live stream sessions with chunk-based delivery.
// Via PlayEngine
engine.stream.getAudioStream(url);
// Standalone — ESM
import { StreamEngine, YtDlpClient } from "@irithell-js/yt-play";
const stream = new StreamEngine(new YtDlpClient());
// Standalone — CJS
const { StreamEngine, YtDlpClient } = require("@irithell-js/yt-play");
const stream = new StreamEngine(new YtDlpClient());stream.getAudioStream(url, qualityKbps?): Promise<StreamInfo>
Returns a Readable M4A audio stream directly from YouTube's CDN. No disk I/O.
import fs from "node:fs";
const { stream, quality, format } = await engine.stream.getAudioStream(
"https://youtube.com/watch?v=VIDEO_ID",
128,
);
stream.pipe(fs.createWriteStream("output.m4a")); // to file
stream.pipe(res); // to HTTP response
voiceConnection.play(stream); // to Discord voicestream.getVideoStream(url, qualityP?): Promise<StreamInfo>
Returns a Readable MP4 video stream directly from YouTube's CDN.
const { stream } = await engine.stream.getVideoStream(
"https://youtube.com/watch?v=VIDEO_ID",
720,
);
stream.pipe(fs.createWriteStream("output.mp4"));stream.startLive(url, options): Promise<LiveStreamSessionContract | StreamInfo>
Starts a live stream ingestion session that segments the stream into local chunk files via FFmpeg.
Return type decision:
| Condition | Returns |
| ---------------------------------------------- | ------------------------------------- |
| URL is an active live stream (is_live) | LiveStreamSessionContract |
| URL is a finished live recording (was_live) | LiveStreamSessionContract |
| URL is a regular VOD and chunkVod: true | LiveStreamSessionContract |
| URL is a regular VOD and chunkVod is not set | StreamInfo (direct stream fallback) |
const result = await engine.stream.startLive(url, {
type: "audio",
format: "mp3",
chunkDurationSeconds: 10,
});
if ("stop" in result) {
// LiveStreamSessionContract
result.on("chunk_ready", (chunk) => console.log("Chunk:", chunk.path));
result.on("end", (reason) => console.log("Done:", reason));
} else {
// StreamInfo — VOD fallback
result.stream.pipe(fs.createWriteStream("output.mp3"));
}StalkerEngine
Deep metadata extraction for videos, live streams, and channels. Requires a YtDlpClient instance.
// ESM
import {
StalkerEngine,
YtDlpClient,
ChannelTabNotFoundError,
} from "@irithell-js/yt-play";
// CJS
const {
StalkerEngine,
YtDlpClient,
ChannelTabNotFoundError,
} = require("@irithell-js/yt-play");
const client = new YtDlpClient({ cookiesFromBrowser: "firefox" });
const stalker = new StalkerEngine(client);stalker.stalkVideoOrLive(url): Promise<YtDlpVideoMetadata>
Returns full metadata for a single video or live stream. See YtDlpVideoMetadata for all available fields.
const info = await stalker.stalkVideoOrLive(
"https://youtube.com/watch?v=LIVE_ID",
);
console.log(info.title);
console.log(info.view_count);
console.log(info.like_count);
console.log(info.tags);
if (info.is_live) {
console.log(`Live viewers: ${info.concurrent_view_count}`);
}stalker.stalkChannel(url, opts?): Promise<YtDlpChannelMetadata>
Scrapes channel data. Supports tab targeting and item ranges. See StalkChannelOptions.
// Most recent 5 videos (fast, basic info only)
const channel = await stalker.stalkChannel("https://youtube.com/@MrBeast", {
flat: true,
tab: "videos",
endItem: 5,
});
// Items 500–510
const older = await stalker.stalkChannel("https://youtube.com/@MrBeast", {
flat: true,
tab: "videos",
startItem: 500,
endItem: 510,
});
// Specific items using yt-dlp syntax
const specific = await stalker.stalkChannel("https://youtube.com/@MrBeast", {
flat: true,
tab: "videos",
playlistItems: "1:3,10,25",
});If the requested tab does not exist, a ChannelTabNotFoundError is thrown:
try {
await stalker.stalkChannel(url, { tab: "shorts" });
} catch (err) {
if (err instanceof ChannelTabNotFoundError) {
console.log("Tab not found:", err.tabUrl);
} else {
throw err;
}
}Live Stream Guide
Full Example
import { StreamEngine, YtDlpClient } from "@irithell-js/yt-play";
const client = new YtDlpClient({ cookiesFromBrowser: "firefox" });
const stream = new StreamEngine(client);
const session = await stream.startLive("https://www.youtube.com/live/LIVE_ID", {
type: "audio",
format: "mp3",
quality: 128,
chunkDurationSeconds: 15,
outputDir: "./chunks",
autoReconnect: true,
maxRetries: 10,
retryDelayMs: 5000,
});
if (!("stop" in session)) {
// VOD fallback — direct stream
session.stream.pipe(fs.createWriteStream("output.mp3"));
return;
}
session.on("start", ({ pid }) => console.log(`FFmpeg PID: ${pid}`));
session.on("chunk_ready", (chunk) => {
console.log(`Chunk ${chunk.sequence}: ${chunk.path} (${chunk.size} bytes)`);
processChunk(chunk);
});
session.on("reconnecting", (attempt) => {
console.warn(`Reconnecting (attempt ${attempt})...`);
});
session.on("error", (err) => console.error("Fatal:", err.message));
session.on("end", (reason) => console.log("Ended:", reason));
// Stop manually
setTimeout(() => session.stop(), 60_000);Segmenting a Finished Live Stream (was_live)
const session = await stream.startLive(
"https://www.youtube.com/live/FINISHED_LIVE_ID",
{
type: "audio",
format: "mp3",
chunkDurationSeconds: 30,
seekToSeconds: 3600, // Start from the 1h mark
},
);Force Chunking a Regular VOD
const session = await stream.startLive(
"https://www.youtube.com/watch?v=VOD_ID",
{
type: "video",
format: "mp4",
chunkDurationSeconds: 10,
chunkVod: true,
},
);Type Reference
A complete reference for all public interfaces and types exported from this package.
Core Types
MediaType
type MediaType = "audio" | "video";PlayMetadata
Returned by search(), searchBest(), and searchPlaylistBest(). Used as input to preload() and getOrDownload().
| Field | Type | Description |
| ----------------- | --------------------- | --------------------------------------------------------------------- |
| title | string | Video or playlist title |
| author | string \| undefined | Channel or uploader name |
| duration | string \| undefined | Formatted duration string (e.g. "3:45") |
| durationSeconds | number | Duration in seconds (0 for playlists) |
| thumb | string \| undefined | Thumbnail URL |
| videoId | string | 11-character YouTube video ID, or playlist listId |
| url | string | Normalized https://www.youtube.com/watch?v=... URL, or playlist URL |
CachedFile
Represents a downloaded media file in the cache.
| Field | Type | Description |
| -------------- | --------------------- | ----------------------------------------------------------------------------------------------- |
| path | string | Absolute path to the file on disk |
| size | number | File size in bytes |
| info.quality | string | Quality label (e.g. "128kbps m4a", "720p H.264") |
| buffer | Buffer \| undefined | In-memory file content. Present only if preloadBuffer: true and size ≤ maxPreloadBufferSize |
CacheEntry
The full entry stored in the CacheStore for a given requestId.
| Field | Type | Description |
| ----------- | -------------------- | ------------------------------------------------ |
| metadata | PlayMetadata | Video metadata |
| audio | CachedFile \| null | Cached audio file (null if not yet downloaded) |
| video | CachedFile \| null | Cached video file (null if not yet downloaded) |
| expiresAt | number | Unix timestamp (ms) when this entry expires |
| loading | boolean | true while preload() is still in progress |
DownloadInfo
Returned by YtDlpClient.getAudio() and YtDlpClient.getVideo().
| Field | Type | Description |
| ------------- | --------------------- | -------------------------------------------- |
| title | string \| undefined | Video title |
| author | string \| undefined | Channel or uploader name |
| duration | string \| undefined | Formatted duration string |
| filename | string | Base filename without directory path |
| quality | string | Quality description (e.g. "128kbps m4a") |
| downloadUrl | string | Absolute path to the downloaded file on disk |
StreamInfo
Returned by getAudioStream(), getVideoStream(), and startLive() when the URL is a VOD without chunkVod.
| Field | Type | Description |
| --------- | ---------- | ------------------------------------------ |
| stream | Readable | Node.js readable stream from YouTube CDN |
| quality | string | Quality label (e.g. "128kbps", "720p") |
| format | string | Container format (e.g. "m4a", "mp4") |
Configuration Interfaces
PlayEngineOptions
All fields are optional.
| Field | Type | Default | Description |
| --------------------------- | -------------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| cacheDir | string | OS temp dir | Directory where downloaded files are stored |
| ttlMs | number | 180000 (3 min) | Cache entry lifetime in ms |
| cleanupIntervalMs | number | 30000 (30s) | How often expired entries are evicted |
| preloadBuffer | boolean | true | Load files into RAM after download |
| maxPreloadBufferSize | number | 52428800 (50 MB) | Max file size (bytes) to buffer in RAM |
| preferredAudioKbps | number | 128 | Preferred audio bitrate: 320 \| 256 \| 192 \| 128 \| 96 \| 64 |
| preferredVideoP | number | 720 | Preferred video height: 1080 \| 720 \| 480 \| 360 |
| maxPreloadDurationSeconds | number | 1200 (20 min) | Videos longer than this use reduced quality settings |
| playerClient | string | "default" | YouTube client: "android" \| "ios" \| "web" \| "default" |
| jsRuntime | string | "auto" | JS challenge solver: "deno" \| "node" \| "auto" |
| jsRuntimeOverride | string | — | Raw value passed directly to --js-runtimes, bypassing auto-detection. Supports any runtime or multiple separated by comma (e.g. "deno:/path/deno,node:/usr/bin/node") |
| useAria2c | boolean | auto | Force enable or disable aria2c downloader |
| concurrentFragments | number | 5 | Parallel download fragments per file |
| ytdlpTimeoutMs | number | 300000 (5 min) | yt-dlp process timeout in ms |
| ytdlpBinaryPath | string | auto | Custom path to yt-dlp binary |
| aria2cPath | string | auto | Custom path to aria2c binary |
| ffmpegPath | string | — | Custom path to ffmpeg binary |
| ffmpegArgs | string[] | — | Additional FFmpeg post-processor arguments |
| cookiesPath | string | — | Path to a Netscape-format cookies file |
| cookiesFromBrowser | string | — | Browser to extract cookies from (chrome \| firefox \| edge \| safari) |
| daemon | YtDlpDaemonOptions | — | Task queue configuration for concurrent process limiting |
| logger | object | — | Logger with optional debug, info, warn, error methods |
YtDlpClientOptions
Options for new YtDlpClient().
| Field | Type | Default | Description |
| --------------------- | -------------------- | ----------- | ----------------------------------------------------------------------------------------------------------- |
| binaryPath | string | auto | Custom path to yt-dlp binary |
| ffmpegPath | string | — | Custom path to ffmpeg binary |
| ffmpegArgs | string[] | — | Additional FFmpeg post-processor arguments |
| aria2cPath | string | auto | Custom path to aria2c binary |
| timeoutMs | number | 300000 | yt-dlp process timeout in ms |
| useAria2c | boolean | auto | Force enable or disable aria2c |
| playerClient | string | "default" | YouTube client: "android" \| "ios" \| "web" \| "default" |
| jsRuntime | string | "auto" | JS challenge solver: "deno" \| "node" \| "auto" |
| jsRuntimeOverride | string | — | Raw --js-runtimes value, bypassing auto-detection (e.g. "node:/path/node" or "deno:/path,node:/path") |
| concurrentFragments | number | 5 | Parallel download fragments |
| cookiesPath | string | — | Path to Netscape-format cookies file |
| cookiesFromBrowser | string | — | Browser to extract cookies from |
| daemon | YtDlpDaemonOptions | — | Task queue configuration |
YtDlpDaemonOptions
Controls the internal task queue that limits concurrent yt-dlp processes.
| Field | Type | Default | Description |
| ------------------- | --------- | -------- | --------------------------------------------- |
| enabled | boolean | required | Enable the task queue |
| maxWorkers | number | 4 | Maximum number of concurrent yt-dlp processes |
| maxTasksPerWorker | number | — | Reserved for future use |
YouTube Data Interfaces
YtDlpVideoInfo
Lightweight video metadata returned by client.getInfo().
| Field | Type | Description |
| ------------- | ---------------------- | -------------------------------------------------------- |
| id | string | 11-character YouTube video ID |
| title | string | Video title |
| uploader | string \| undefined | Channel or uploader name |
| duration | number | Duration in seconds |
| thumbnail | string \| undefined | Best thumbnail URL |
| is_live | boolean \| undefined | Whether the video is currently live |
| live_status | string \| undefined | "is_live" \| "was_live" \| "is_upcoming" \| "not_live" |
YtDlpVideoMetadata
Full metadata object returned by stalker.stalkVideoOrLive().
| Field | Type | Description |
| ------------------------ | ------------------------------- | -------------------------------------------------------- |
| _type | "video" | Discriminant field |
| id | string | YouTube video ID |
| title | string | Video title |
| description | string \| undefined | Full description |
| channel | string | Channel name |
| channel_id | string | Unique channel ID |
| channel_follower_count | number \| undefined | Subscriber count |
| duration | number \| undefined | Duration in seconds |
| view_count | number \| undefined | Total view count |
| like_count | number \| undefined | Like count |
| comment_count | number \| undefined | Comment count |
| tags | string[] \| undefined | Video tags |
| categories | string[] \| undefined | YouTube categories |
| upload_date | string \| undefined | Upload date as "YYYYMMDD" |
| timestamp | number \| undefined | Upload Unix timestamp |
| thumbnails | YtDlpThumbnail[] \| undefined | All available thumbnails |
| webpage_url | string | Canonical YouTube URL |
| is_live | boolean \| undefined | Whether the video is currently live |
| was_live | boolean \| undefined | Whether the video was a live stream |
| concurrent_view_count | number \| undefined | Real-time viewer count (live streams only) |
| live_status | string \| undefined | "is_live" \| "was_live" \| "is_upcoming" \| "not_live" |
| release_timestamp | number \| undefined | Scheduled premiere Unix timestamp |
YtDlpThumbnail
A single thumbnail variant from YtDlpVideoMetadata.thumbnails or YtDlpChannelMetadata.thumbnails.
| Field | Type | Description |
| -------- | --------------------- | ------------------- |
| url | string | Thumbnail URL |
| width | number \| undefined | Width in pixels |
| height | number \| undefined | Height in pixels |
| id | string \| undefined | yt-dlp thumbnail ID |
YtDlpChannelMetadata
Full channel metadata returned by stalker.stalkChannel().
| Field | Type | Description |
| ------------------------ | ------------------------------------------------------- | ------------------------------------------------------------------------ |
| _type | "playlist" | Discriminant field |
| id | string | Channel or playlist ID |
| title | string | Channel name or playlist title |
| channel | string | Channel display name |
| channel_id | string | Unique channel ID |
| description | string \| undefined | Channel description |
| channel_follower_count | number \| undefined | Subscriber count |
| tags | string[] \| undefined | Channel tags |
| thumbnails | YtDlpThumbnail[] \| undefined | Channel artwork/thumbnails |
| playlist_count | number \| undefined | Total number of items in the playlist |
| entries | (YtDlpFlatEntry \| YtDlpVideoMetadata)[] \| undefined | Scraped entries. Flat entries when flat: true; full metadata otherwise |
YtDlpFlatEntry
A lightweight entry returned when flat: true is passed to stalkChannel().
| Field | Type | Description |
| ------------ | ---------------------------- | ---------------------- |
| _type | "url" \| "url_transparent" | yt-dlp entry type |
| id | string | YouTube video ID |
| title | string | Video title |
| url | string | Full YouTube watch URL |
| duration | number \| undefined | Duration in seconds |
| view_count | number \| undefined | View count |
| channel | string \| undefined | Channel name |
Playlist Interfaces
YtDlpPlaylistInfo
Returned by client.getPlaylistInfo().
| Field | Type | Description |
| ---------- | -------------------------------------------- | ------------------------------------------------------------------------- |
| id | string | Playlist ID |
| title | string | Playlist title |
| uploader | string \| undefined | Playlist owner name |
| entries | YtDlpPlaylistItem[] \| YtDlpResolvedItem[] | Playlist entries. Items are YtDlpResolvedItem when resolveLinks: true |
YtDlpPlaylistItem
A single entry from a playlist, before link resolution.
| Field | Type | Description |
| ----------- | --------------------- | ---------------------------------------------------------------- |
| id | string | YouTube video ID |
| title | string | Video title |
| url | string | Full https://www.youtube.com/watch?v=... URL |
| duration | number \| undefined | Duration in seconds |
| uploader | string \| undefined | Channel name (falls back to playlist uploader) |
| directUrl | string \| undefined | Resolved CDN URL (only set when returned as YtDlpResolvedItem) |
YtDlpResolvedItem
Extends YtDlpPlaylistItem with a guaranteed direct CDN URL.
| Field | Type | Description |
| ---------------------------------- | -------- | ---------------------------------------------------------------------------- |
| (all YtDlpPlaylistItem fields) | | |
| directUrl | string | Direct CDN streaming URL (always present, may be "" on resolution failure) |
YtDlpPlaylistOptions
Options for client.getPlaylistInfo().
| Field | Type | Default | Description |
| -------------------- | ------------------------------ | --------- | -------------------------------------------------------------------------- |
| limit | number | — | Maximum number of entries to return |
| resolveLinks | boolean | false | Resolve direct CDN URLs for each entry |
| type | "audio" \| "video" \| "both" | "audio" | Format preference for link resolution |
| batchSize | number | 15 | Concurrent resolution workers |
| startItem | number | — | 1-based playlist start index |
| endItem | number | — | 1-based playlist end index |
| playlistItems | string | — | yt-dlp item selector (e.g. "1:5,10,15"). Overrides startItem/endItem |
| reverse | boolean | false | Reverse playlist order before slicing |
| maxDurationSeconds | number | — | Exclude entries longer than this value |
| minDurationSeconds | number | — | Exclude entries shorter than this value |
StalkChannelOptions
Options for stalker.stalkChannel().
| Field | Type | Default | Description |
| --------------- | --------- | ------- | ------------------------------------------------------------------------------------- |
| flat | boolean | false | Use --flat-playlist for faster extraction with basic info only |
| tab | string | — | Channel tab to scrape: "videos" \| "shorts" \| "streams" \| "popular" \| "featured" |
| startItem | number | — | 1-based start index |
| endItem | number | — | 1-based end index |
| playlistItems | string | — | yt-dlp item selector (e.g. "1:3,10,25"). Overrides startItem/endItem |
Live Stream Interfaces
LiveStreamOptions
Options for stream.startLive().
| Field | Type | Default | Description |
| ---------------------- | ------------------------- | ---------------------- | -------------------------------------------------------------------- |
| type | "audio" \| "video" | required | Stream media type |
| format | "mp3" \| "m4a" \| "mp4" | "mp3" / "mp4" | Output container format |
| quality | number | 128 | Audio bitrate in kbps (audio only) |
| chunkDurationSeconds | number | 10 | Duration of each segment in seconds |
| outDir | string | tmp_live/<sessionId> | Directory where chunk files are written |
| sessionId | string | random hex | Custom session identifier |
| seekToSeconds | number | — | Start position in seconds (useful for was_live recordings) |
| chunkVod | boolean | false | Force chunk-mode on regular VODs instead of falling back to a stream |
| autoReconnect | boolean | true | Automatically restart FFmpeg on unexpected exit |
| maxRetries | number | 5 | Maximum number of reconnect attempts |
| retryDelayMs | number | 3000 | Delay between reconnect attempts in ms |
LiveStreamChunk
Emitted via the "chunk_ready" event on a LiveStreamSessionContract.
| Field | Type | Description |
| ------------------ | -------- | ---------------------------------------------------------------- |
| sessionId | string | Session identifier |
| chunkId | string | Unique hex ID for this chunk |
| sequence | number | 0-based chunk index within this session |
| startTimeSeconds | number | Byte offset from stream start: sequence × chunkDurationSeconds |
| path | string | Absolute path to the chunk file on disk |
| size | number | File size in bytes |
| createdAt | number | Unix timestamp in ms when the chunk was emitted |
LiveStreamSessionContract
The interface implemented by LiveStreamSession, returned by startLive() for live or chunked sources.
Methods:
| Method | Description |
| -------------- | ------------------------------------------------------------------------- |
| stop(): void | Terminates FFmpeg, cleans up any incomplete chunk file, and emits "end" |
Events:
| Event | Listener signature | Description |
| ---------------- | ----------------------------------------- | ------------------------------------------------------------------- |
| "start" | (info: { sessionId, url, pid }) => void | FFmpeg process has started |
| "chunk_ready" | (chunk: LiveStreamChunk) => void | A complete segment file is ready |
| "reconnecting" | (attempt: number) => void | FFmpeg exited unexpectedly; reconnect in progress |
| "error" | (error: Error) => void | Fatal error (max retries exceeded, or unrecoverable FFmpeg failure) |
| "end" | (reason: string) => void | Session finished (EOF, stop() called, or persistent failure) |
off method:
| Method | Description |
| ---------------------------- | -------------------------------- |
| off(event, listener): this | Remove a specific event listener |
ChannelTabNotFoundError
Thrown by stalker.stalkChannel() when the requested channel tab does not exist.
| Property | Type | Description |
| --------- | -------- | ------------------------------------------------------ |
| message | string | Human-readable error: "Channel tab not found: <url>" |
| name | string | "ChannelTabNotFoundError" |
| tabUrl | string | The full URL that was attempted |
import { ChannelTabNotFoundError } from "@irithell-js/yt-play";
try {
await stalker.stalkChannel(url, { tab: "shorts" });
} catch (err) {
if (err instanceof ChannelTabNotFoundError) {
console.log("Tab not found:", err.tabUrl);
} else {
throw err;
}
}Auto-Update System
yt-dlp is updated automatically to stay compatible with YouTube's frequently changing API.
Background checks — On every PlayEngine instantiation, the engine checks GitHub for a new yt-dlp release (at most once per hour per process).
On-demand updates — If any download fails, the engine immediately forces an update check and retries the download after updating.
<!> Download failed. Forcing yt-dlp update check...
✓ yt-dlp updated successfully
>> Retrying download after update...| Trigger | Frequency | | ------------------- | --------------------------------- | | Background check | At most once per hour per process | | On-demand (failure) | Immediately on any download error | | Source | GitHub Releases API |
File Formats
| Media | Container | Codec | Notes | | ----- | --------- | ----------- | --------------------------------------- | | Audio | M4A | AAC | Native format, no re-encoding — fastest | | Video | MP4 | H.264 + AAC | Audio merged in a single pass |
M4A provides a better quality-to-size ratio and downloads 2–5x faster than MP3 because no transcoding is needed.
Performance
Measured on a local machine with aria2c enabled. Results vary by network speed and YouTube throttling.
| Video Length | Audio Download | Video Download | | ------------ | -------------- | -------------- | | 5 min | ~3–5 s | ~6–8 s | | 1 h | ~15–20 s | Audio only | | 2 h | ~25–30 s | Audio only |
For long videos, direct download (skipping preload()) is recommended.
Troubleshooting
Slow downloads
const engine = new PlayEngine({
useAria2c: true,
concurrentFragments: 10,
});Bot detection / "Sign in to confirm" / missing formats
// Switch to a different YouTube client
const engine = new PlayEngine({ playerClient: "ios" });
// Force a specific JS runtime for challenge solving
const engine = new PlayEngine({ jsRuntime: "node" });
// Or provide cookies
const engine = new PlayEngine({ cookiesFromBrowser: "chrome" });Age-restricted or member-only content
Export cookies while logged into an account with access, then:
const engine = new PlayEngine({ cookiesPath: "/path/to/cookies.txt" });Binary not found
npm rebuild @irithell-js/yt-play
# or
node node_modules/@irithell-js/yt-play/scripts/setup-binaries.mjsCache corruption
rm -rf ./cache # or your configured cacheDirLive stream chunks not arriving
- Ensure
ffmpegis installed and available in$PATH, or setffmpegPathin options. - Listen to the
"error"event on the session for the underlying FFmpeg message. - For
was_livestreams, try settingseekToSeconds: 0explicitly.
Requirements
- Node.js ≥ 18.0.0
- ~150 MB disk space for bundled binaries
ffmpegin$PATH(required for live stream chunking and video merging)
Binaries
The package downloads and manages the following binaries automatically during npm install:
| Binary | Version | Size | Purpose | | ------ | --------------------- | ------ | ---------------------------------------- | | yt-dlp | latest (auto-updated) | ~35 MB | YouTube extraction engine | | aria2c | v1.37.0 | ~12 MB | Parallel multi-connection downloader | | deno | latest | ~95 MB | JS runtime for YouTube challenge solving |
Supported Platforms
| Platform | Architecture | | -------- | -------------------------- | | Linux | x64, arm64 | | macOS | x64, arm64 (Apple Silicon) | | Windows | x64 |
Custom Binary Paths
const engine = new PlayEngine({
ytdlpBinaryPath: "/custom/path/yt-dlp",
aria2cPath: "/custom/path/aria2c",
ffmpegPath: "/custom/path/ffmpeg",
});Changelog
0.4.6
- Added
jsRuntimeOverrideoption — passes a raw string directly to--js-runtimes, bypassing auto-detection; supports any yt-dlp-compatible runtime or comma-separated list - Added bundled deno binary — downloaded automatically during
npm installalongside yt-dlp and aria2c - Added
jsRuntimeoption ("deno" | "node" | "auto") on bothPlayEngineandYtDlpClient— controls which runtime solves YouTube's JS challenges - Added JS runtime auto-detection: tries bundled deno → system deno → node fallback; passed via
--js-runtimeson every yt-dlp call - Fixed
setupYtDlpConfigno longer overwrites existingyt-dlp.confcontent — only manages cookie lines, preserving user-set flags like--js-runtimes,--remote-components, or custom extractor args - Fixed
playerClientdefault changed from"android"to"default"— Android client was returning restricted format lists after a YouTube-side change - Fixed
ffmpegArgstype mismatch ingetVideo— was comparingstring[] ?? string, now correctly uses.join(" ") ?? defaultArgs - Fixed duplicate
--postprocessor-argswhen bothgetVideoand_execInternaladded the flag —_execInternalnow skips it if already present in args - Fixed
__dirnameshadowing in CJS bundles — renamed internal module dir constant to avoid conflicting with the CJS global, fixing dynamic import path resolution for the auto-update script
0.4.4
- Added ffmpeg default args ("-vcodec libx264 -acodec aac -movflags +faststart") on ytdlp args build (you an use custom args with ffmpegArgs in YtDlpClient build)
0.4.2
- Fixed Client issues on dockered instances
- Fixed Construction of improved download arguments
- Fixed Improved quality fallbacks
0.4.1
- Fixed: Live stream session generating a spurious empty third chunk file when
stop()is called — off-by-one error in the ghost-file cleanup index calculation - Fixed:
chunkCountnot being reset between reconnect cycles, causing wrongsequencenumbers and mismatched ghost-file deletion afterautoReconnect - Fixed:
waitCache()replaced inefficient 500ms setTimeout polling with anEventEmitter-based signal — resolves immediately when the file is ready - Fixed:
ChannelTabNotFoundErrornow properly thrown instead of silently returning an empty playlist object - Fixed: Binary path resolution now uses
Promise.anyfor parallel existence checks instead of sequentialfs.accesscalls - Fixed:
buildOptimizationArgsand binary resolution promises are now cached — no repeated I/O across multiple downloads in the same client instance - Fixed:
getAudioandgetVideonow rungetInfoandbuildOptimizationArgsin parallel when metadata is not already available - Fixed:
stdoutandstderrcollected asBuffer[]and concatenated once — avoids GC pressure from repeated string concatenation in long-running processes - Changed:
preload()accepts a thirdtargetType: "audio" | "video" | "both"parameter (default:"both") - Changed:
getOrDownload()accepts an optional thirdfallbackMetadata: PlayMetadataparameter to create a cache entry on the fly - Changed:
waitCache()intervalMsparameter removed — method is now event-driven - Added:
playerClientoption ("android" | "ios" | "web" | "default") on bothPlayEngineandYtDlpClient - Added:
maxPreloadBufferSizeoption — files larger than this threshold are not buffered in RAM (default: 50 MB) - Added:
chunkVodoption onLiveStreamOptionsto force VOD URLs into chunk-segmentation mode - Added:
seekToSecondsonLiveStreamOptionsfor seeking into finished live recordings - Added:
YtDlpPlaylistOptionsextended withstartItem,endItem,playlistItems,reverse,maxDurationSeconds,minDurationSeconds - Added:
passThrough.on("close")destroys the upstream HTTPS response to prevent socket leaks on early stream termination - Added:
stderrBufferlength guard inLiveStreamSessionto prevent unbounded growth during long live sessions - Added:
sanitizeFilenamecall removed from generated temp filenames (already safe strings); filenames simplified to<type>_<requestId>.<ext> - Added:
setupYtDlpConfignow diffs existing config before writing — no unnecessary disk writes on repeatedPlayEngineinstantiation
0.3.3
- Added
StalkerEnginefor deep metadata extraction from videos, live streams, and entire channels - Added advanced channel scraping with tab support (
videos,shorts,streams,popular) and item ranges - Added real-time live stream metadata (
is_live,concurrent_view_count) - Added
StreamEnginefor rawReadablestreams without disk storage
0.3.2
- Added full YouTube Playlist support via
searchPlaylistBestandgetPlaylistInfo - Added fast batch URL resolution for playlists with
resolveLinksand configurablebatchSize - Fixed race conditions in the auto-update system during concurrent downloads
- Fixed binary setup to use atomic downloads, preventing corrupted binaries on connection drops
- Added automatic cleanup of
.partand.ytdltemporary files on failure
0.2.8
- Fixed codec selection for Shorts across all platforms
0.2.7
- Fixed URL parsing for Shorts
0.2.6
- Added auto-update system for yt-dlp
- Added direct YouTube URL support in
search() - Added auto-configuration system (
yt-dlp.conf) - Added cookie authentication support
- Fixed URL search returning wrong video
- Improved error handling with automatic retry after update
0.2.5
- Added cookie extraction from installed browsers
0.2.4
- Added
cookies.txtsupport
0.2.3
- Updated documentation; improved error messages
0.2.2
- Various syntax fixes
0.2.1
- Added auto-detection for yt-dlp and aria2c binaries
- Fixed CommonJS compatibility
- Improved error handling for long videos
0.2.0
- Initial release with bundled binaries, aria2c acceleration, and intelligent caching
License
MIT
Contributing
Issues and pull requests are welcome.
