npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@irithell-js/yt-play

v0.4.6

Published

YouTube search + download engine (audio/video) with optional caching.

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

  • 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 Readable streams 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.conf without 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-play

yt-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:

  1. Install "Get cookies.txt LOCALLY" in your browser
  2. Log into youtube.com
  3. 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 only

For 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 downloaded

If 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.streamStreamEngine

The StreamEngine instance is exposed as engine.stream. See StreamEngine.


engine.cacheCacheStore

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_status

client.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 voice

stream.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.mjs

Cache corruption

rm -rf ./cache  # or your configured cacheDir

Live stream chunks not arriving

  • Ensure ffmpeg is installed and available in $PATH, or set ffmpegPath in options.
  • Listen to the "error" event on the session for the underlying FFmpeg message.
  • For was_live streams, try setting seekToSeconds: 0 explicitly.

Requirements

  • Node.js ≥ 18.0.0
  • ~150 MB disk space for bundled binaries
  • ffmpeg in $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 jsRuntimeOverride option — 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 install alongside yt-dlp and aria2c
  • Added jsRuntime option ("deno" | "node" | "auto") on both PlayEngine and YtDlpClient — controls which runtime solves YouTube's JS challenges
  • Added JS runtime auto-detection: tries bundled deno → system deno → node fallback; passed via --js-runtimes on every yt-dlp call
  • Fixed setupYtDlpConfig no longer overwrites existing yt-dlp.conf content — only manages cookie lines, preserving user-set flags like --js-runtimes, --remote-components, or custom extractor args
  • Fixed playerClient default changed from "android" to "default" — Android client was returning restricted format lists after a YouTube-side change
  • Fixed ffmpegArgs type mismatch in getVideo — was comparing string[] ?? string, now correctly uses .join(" ") ?? defaultArgs
  • Fixed duplicate --postprocessor-args when both getVideo and _execInternal added the flag — _execInternal now skips it if already present in args
  • Fixed __dirname shadowing 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: chunkCount not being reset between reconnect cycles, causing wrong sequence numbers and mismatched ghost-file deletion after autoReconnect
  • Fixed: waitCache() replaced inefficient 500ms setTimeout polling with an EventEmitter-based signal — resolves immediately when the file is ready
  • Fixed: ChannelTabNotFoundError now properly thrown instead of silently returning an empty playlist object
  • Fixed: Binary path resolution now uses Promise.any for parallel existence checks instead of sequential fs.access calls
  • Fixed: buildOptimizationArgs and binary resolution promises are now cached — no repeated I/O across multiple downloads in the same client instance
  • Fixed: getAudio and getVideo now run getInfo and buildOptimizationArgs in parallel when metadata is not already available
  • Fixed: stdout and stderr collected as Buffer[] and concatenated once — avoids GC pressure from repeated string concatenation in long-running processes
  • Changed: preload() accepts a third targetType: "audio" | "video" | "both" parameter (default: "both")
  • Changed: getOrDownload() accepts an optional third fallbackMetadata: PlayMetadata parameter to create a cache entry on the fly
  • Changed: waitCache() intervalMs parameter removed — method is now event-driven
  • Added: playerClient option ("android" | "ios" | "web" | "default") on both PlayEngine and YtDlpClient
  • Added: maxPreloadBufferSize option — files larger than this threshold are not buffered in RAM (default: 50 MB)
  • Added: chunkVod option on LiveStreamOptions to force VOD URLs into chunk-segmentation mode
  • Added: seekToSeconds on LiveStreamOptions for seeking into finished live recordings
  • Added: YtDlpPlaylistOptions extended with startItem, endItem, playlistItems, reverse, maxDurationSeconds, minDurationSeconds
  • Added: passThrough.on("close") destroys the upstream HTTPS response to prevent socket leaks on early stream termination
  • Added: stderrBuffer length guard in LiveStreamSession to prevent unbounded growth during long live sessions
  • Added: sanitizeFilename call removed from generated temp filenames (already safe strings); filenames simplified to <type>_<requestId>.<ext>
  • Added: setupYtDlpConfig now diffs existing config before writing — no unnecessary disk writes on repeated PlayEngine instantiation

0.3.3

  • Added StalkerEngine for 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 StreamEngine for raw Readable streams without disk storage

0.3.2

  • Added full YouTube Playlist support via searchPlaylistBest and getPlaylistInfo
  • Added fast batch URL resolution for playlists with resolveLinks and configurable batchSize
  • 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 .part and .ytdl temporary 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.txt support

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.