@irithell-js/yt-play
@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.