npm.io
0.4.6 • Published 3d ago

@irithell-js/yt-play

Licence
MIT
Version
0.4.6
Deps
3
Size
315 kB
Vulns
0
Weekly
298
Install scriptsThis package runs scripts during installation (preinstall/install/postinstall)

@irithell-js/yt-play

npm version License: MIT Node.js 18+
TypeScript ESM + CJS Platforms Bundled binaries

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,
});

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.

Keywords