@ytdlm/ytdlm
v1.1.0
Published
Real-time SABR UMP WebM/Opus audio streaming & seekable downloading library, CLI, and HTTP gateway for Discord music bots (Node.js, Python, Java, Rust).
Maintainers
Readme
ytdlm — Real-Time SABR UMP WebM/Opus Audio Streamer & Downloader
ytdlm is a zero-dependency core engine, yt-dlp-compatible CLI, and multi-language HTTP Streaming Gateway designed for real-time on-the-fly WebM/Opus audio streaming (zero-wait live pipe) and 100% seekable .opus file downloading over the SABR UMP (Server-side ABR Unified Media Protocol) transport.
Built specifically for multi-guild Discord music bots, 24/7 Lo-Fi / Radio streams, and cross-language media pipelines (Node.js, Python, Java, Rust, Go, C#).
Table of Contents
- Why
ytdlm? - Deep-Dive Technical Architecture
- Project Structure
- Installation & CLI Usage (
bin/ytdlm.js) - Node.js API Reference (
src/index.js) - HTTP Streaming & Metadata Gateway (
ytdlm serve) - Multi-Language Discord Bot Integration Guides
- Testing & Verification
- Contributing
- License & Legal Disclaimer
1. Why ytdlm?
| Feature | Traditional Downloaders | ytdlm (SABR UMP Live Engine) |
| :--- | :--- | :--- |
| Time-to-First-Byte (Startup Latency) | Waits for heavy process spawn or full file download (1.5s – 5s+) | ~270ms – 350ms (emits patched WebM Init + first Opus Cluster immediately) |
| Discord Bot CPU Overhead | Decodes and re-encodes audio via FFmpeg | 0% Transcode CPU (native 48kHz Stereo WebM/Opus frames piped directly to Discord) |
| Memory Usage (Multi-Stream) | Grows linearly with track length | Constant $O(1)$ (~512 KB / stream) via retainClusters: false + stream backpressure |
| Account Safety (Anti-Ban) | Risk of leaking cookies/tokens on standard 251 streams | 100% Isolated Anonymous Guest TVHTML5 with a rotating 16-session visitorData pool |
| 24/7 Continuous Streaming (10h+ mixes) | Drops connection when CDN URL expires (HTTP 403) | Auto-Healing SABR Session Refresh (refreshSabrSession) resumes from exact millisecond |
| Downloaded .opus Seekability | Raw UMP dumps often show 00:00 or fail to seek | Full binary patching of Duration + 56-byte SeekHead + convergent multi-pass Cues |
2. Deep-Dive Technical Architecture
2.1. SABR UMP Processing Pipeline
flowchart LR
subgraph Client["Discord Bot / Application (JS, Py, Java, Rust)"]
A["Request: URL / VideoID / Search Query"]
end
subgraph Innertube["1. InnerTube & Guest Pool (src/innertube.js)"]
B["Guest VisitorData Pool (16 Rotating Anonymous Sessions)"]
C["POST /youtubei/v1/player (TVHTML5)"]
D["Parallel oEmbed Metadata"]
end
subgraph Decipher["2. Node VM Decipher (src/decipher.js)"]
E["Disk-Cached base.js"]
F["n-Challenge Decipher (< 5ms)"]
end
subgraph SABR["3. SABR UMP Transport (src/protobuf.js & src/sabr-stream.js)"]
G["Protobuf VideoPlaybackAbrRequest"]
H["POST googlevideo.com/videoplayback?alr=yes&rn=N"]
I["UMP Demuxer (Part 20 Header, Part 21 Media, Part 35 Cookie, Part 43 Redirect)"]
end
subgraph WebM["4. EBML/WebM Assembler (src/webm.js)"]
J["Live Stream: Indeterminate Segment 0x01FFFFFFFFFFFFFF + Immediate Clusters"]
K["File Download: Patched Duration + 56B SeekHead + Multi-Pass Cues"]
end
A --> B & D
B --> C
C --> F
E --> F
F --> G --> H --> I
I --> J & K2.2. Strict Guest Session Isolation (TV_GUEST_SABR_251) vs. TV Premium (TV_PREMIUM_SABR_774)
In src/innertube.js:
- Standard
itag: 251(Default) orguest: true:- Fetches an anonymous Guest
visitorDatatoken fromhttps://www.youtube.com/sw.js_data(/(Cg[st][a-zA-Z0-9_-]+)/). - Calls
/youtubei/v1/playerasTVHTML5with zero cookies and zeroAuthorizationheaders. - Selects the clean original audio track (excluding DRC dynamic range compression) at ~135–160kbps Opus (
48000 Hz, 2 channels).
- Fetches an anonymous Guest
- Optional TV OAuth Token (
ytdlm auth login) foritag: 774(~256–300kbps Opus):- Checks whether the requested video offers a high-bitrate
itag 774stream. - Strict Fallback Rule: If a video does not have
itag 774,ytdlmdiscards the authenticated context and resolvesitag 251using an Anonymous Guest session (guest: true). Main account tokens are never used to stream or downloaditag 251.
- Checks whether the requested video offers a high-bitrate
2.3. Binary EBML/WebM Patching: Live Streaming vs. Seekable Download
Raw UMP media payloads (Part 21) contain an unpatched WebM Initialization Segment (Duration = 0 and a fixed Segment size) followed by fragmented Cluster chunks. src/webm.js handles two distinct modes:
- Live Streaming Mode (
buildStreamingWebmInit):- Patches
Segment -> Info -> Duration(0x4489, 64-bit float) with the track's actual duration in milliseconds. - Rewrites the 12-byte
Segmentheader (0x18538067) to the standard 8-byte EBML unknown/indeterminate streaming size:18 53 80 67 01 FF FF FF FF FF FF FF - Downstream decoders (
@discordjs/voice,FFmpeg,Lavaplayer,GStreamer,MPV) immediately recognize the stream as live WebM/Opus and decode incomingCluster(0x1F43B675) frames on the fly.
- Patches
- Seekable File Download Mode (
buildSeekableWebmFile):- Computes the exact total duration from the final assembled
Cluster. - Injects a 56-byte
SeekHead(0x114D9B74) pointing toInfo,Tracks, andCues. - Runs a convergent multi-pass loop (up to 5 passes) to build the
Cuesindex table (0x1C53BB6B) mapping everyClustertimecode to its exact byte offset, producing a 100% seekable.opusfile.
- Computes the exact total duration from the final assembled
2.4. Unlimited Multi-Stream Concurrency ($O(1)$ RAM) & 24/7 Continuous Playback
- Constant $O(1)$ Memory per Active Stream (
retainClusters: false):- In
createSabrAudioStream,WebmClusterAssemblerruns withretainClusters: false. - As soon as a WebM
Clusteris pushed to the Node.jsReadablestream, itsUint8Arraypayload is immediately eligible for garbage collection. - Combined with native stream backpressure (
highWaterMark: 512 KB),ytdlmpauses SABR network polling whenever the consumer buffer is full and resumes only when Discord Voice reads more frames (~16–20 KB/s). - Result: Streaming to 100 concurrent Discord voice channels consumes only ~50 MB of RAM in total.
- In
- 16-Session Rotating Guest Pool:
src/innertube.jsmaintains a rotating pool of up to 16 independent anonymousvisitorDatatokens (GUEST_POOL_MAX = 16), distributing concurrent stream initialization across separate guest sessions.
- Auto-Healing SABR Session Refresh (
refreshSabrSession):- During multi-hour streams or 24/7 radio playback, if a
googlevideo.comURL expires (HTTP 403) or encounters transient network errors,refreshSabrSession()transparently resolves a freshserverAbrStreamingUrl, deciphers the newnchallenge, and resumes fetching clusters fromlastEmittedTcwithout interrupting the output stream.
- During multi-hour streams or 24/7 radio playback, if a
- Infinite 24/7 Loop (
loop: true) & Continuous Multi-Track Queue (streamContinuous):- Pass
{ loop: true }tostreamAudio()(or--loopin CLI /?loop=1in HTTP) to loop a single stream 24/7. - Or use
streamContinuous(playlistOrAsyncFn, options)to stream an infinite sequence of tracks back-to-back.
- Pass
3. Project Structure
ytdlm/
├── package.json # Node.js package config (CommonJS + ESM + CLI binary)
├── LICENSE # Standard SPDX MIT License
├── DISCLAIMER.md # Legal Disclaimer, Copyright Notice & Contributor Protection
├── README.md # Architecture & Multi-Language Integration Documentation
├── bin/
│ └── ytdlm.js # yt-dlp compatible CLI (stream, download, info, search, serve, auth)
├── src/
│ ├── index.js # CommonJS entrypoint
│ ├── index.mjs # ES Module entrypoint
│ ├── protobuf.js # Pure JS Protobuf wire encoder/decoder & SABR UMP demuxer
│ ├── webm.js # EBML/WebM parser, SeekHead/Cues builder & O(1) Cluster Assembler
│ ├── decipher.js # Standalone Node.js VM base.js n-challenge decipher & disk cache
│ ├── innertube.js # TVHTML5 Guest Pool (251), TV OAuth (774), Search & oEmbed
│ ├── sabr-stream.js # Backpressure-aware Readable stream, auto-heal & file downloader
│ └── server.js # HTTP Streaming & Metadata Gateway Server (/stream, /info, /search, /health)
├── sdks/
│ ├── javascript/
│ │ └── discord-js-bot.js # Discord.js v14 + @discordjs/voice Multi-Guild & 24/7 Manager
│ ├── python/
│ │ ├── pyproject.toml # PyPI package configuration
│ │ ├── ytdlm.py # Python SDK (Subprocess Pipe + HTTP Gateway)
│ │ └── discord_py_bot.py # Discord.py bot example using FFmpegOpusAudio(codec="copy")
│ ├── java/
│ │ ├── pom.xml # Maven / JitPack build configuration
│ │ └── YtdlmClient.java # Java 11+ SDK for JDA, Lavaplayer & Lavalink
│ └── rust/
│ ├── Cargo.toml # Crates.io package configuration (ytdlm-rs)
│ └── src/lib.rs # Rust SDK for Serenity / Songbird / Twilight
└── test/
└── verify.js # Automated verification suite (Metadata, Time-to-First-Byte, HTTP Server)4. Installation & CLI Usage (bin/ytdlm.js)
Requirements: Node.js >= 18.0.0 (zero external npm dependencies).
npm install -g ytdlm
# Or from a local clone:
npm linkCLI Command & Flag Reference
| Command / Flag | Description |
| :--- | :--- |
| ytdlm stream <url_or_query> | Stream live WebM/Opus audio to stdout (or to a file with -o). |
| ytdlm <url_or_query> -o - | yt-dlp-compatible drop-in syntax for piping live audio to stdout. |
| ytdlm download <url> [-o path] | Download a 100% seekable .opus file with patched Duration, SeekHead & Cues. |
| ytdlm info <url> [-j] | Print track and SABR format metadata as JSON (yt-dlp -j compatible). |
| ytdlm search <query> [-n 5] [-j] | Search videos in anonymous Guest mode. |
| ytdlm serve [-p 8787] [--host 127.0.0.1] | Start the HTTP Streaming & Metadata Gateway Server. |
| ytdlm auth <login\|status\|logout> | Manage optional TV OAuth device token (for itag 774 HQ audio). |
| -f, --format <251\|774\|bestaudio> | Select format (251: Guest ~160kbps Opus; 774: TV Premium ~256kbps Opus). |
| --guest | Force anonymous Guest TVHTML5 mode (enabled by default for 251). |
| --start <ms> | Start streaming immediately from the given millisecond offset (instant seek). |
| --loop | Loop the audio stream infinitely (24/7 mode). |
| -q, --quiet | Suppress progress/status logs on stderr. |
CLI Examples (Using Creative Commons 3.0 Benchmark: Blender Foundation)
# 1. Pipe live WebM/Opus audio directly to ffplay or mpv (~300ms startup):
ytdlm stream "https://www.youtube.com/watch?v=aqz-KE-bpKQ" | ffplay -i pipe:0 -nodisp
# 2. Search and stream starting at 1:00 (--start 60000):
ytdlm stream "ytsearch:Big Buck Bunny Blender Foundation" -f 251 --start 60000 -q | mpv -
# 3. Continuous 24/7 infinite loop (--loop):
ytdlm stream "https://www.youtube.com/watch?v=aqz-KE-bpKQ" --guest --loop -q | ffmpeg -i pipe:0 -c:a copy -f ogg -
# 4. Download a 100% seekable .opus file into ./music:
ytdlm download "https://www.youtube.com/watch?v=aqz-KE-bpKQ" -o ./music/5. Node.js API Reference (src/index.js)
// CommonJS
const {
Ytdlm,
streamAudio,
streamContinuous,
downloadAudio,
getInfo,
searchVideos,
startServer,
} = require('ytdlm');
// ES Modules (ESM)
import {
streamAudio,
streamContinuous,
downloadAudio,
getInfo,
searchVideos,
startServer,
} from 'ytdlm';5.1. streamAudio(videoIdOrQuery, options)
Returns a backpressure-aware Node.js Readable stream emitting patched WebM Init + Opus Cluster chunks in real time.
options:itag(number | string, default:251):251(Guest Opus) or774(TV Premium HQ Opus).guest(boolean, default:truefor251): Force anonymous Guest session.startTimeMs(number, default:0): Instant seek offset in milliseconds.loop(boolean, default:false): Infinite 24/7 single-track loop.highWaterMark(number, default:524288/ 512 KB): Stream buffer threshold.onProgress(function({ bufferedMs, durationMs, percent, bytes, itag })): Progress callback.
- Methods & Events:
await stream.getInfo(): Resolves with the track metadata object before the first media cluster is emitted.'info': Emits(ctx)metadata.'loop': Emits(ctx)whenever the stream loops inloop: truemode.
5.2. streamContinuous(tracksOrProvider, options)
Creates a 24/7 continuous stream across an array of tracks or an async (index) => nextUrlOrQuery provider function. Emits 'trackStart', 'trackEnd', and 'trackError'.
5.3. downloadAudio(videoIdOrQuery, outputPath, options)
Downloads and assembles a seekable .opus file with patched Duration, SeekHead, and Cues. Returns { info, buffer, outputPath, durationMs, clustersCount, sizeBytes }.
6. HTTP Streaming & Metadata Gateway (ytdlm serve)
For Java (Lavaplayer / Lavalink), Python, Rust (Songbird), Go, or C#, start the built-in HTTP gateway server (src/server.js):
ytdlm serve --port 8787 --host 127.0.0.1Endpoints
GET /stream?v=<id_or_url>&itag=251&guest=1&start=0&loop=0- Streams live
audio/webm; codecs="opus"(Transfer-Encoding: chunked). - Response headers include
X-Video-Id,X-Audio-Itag,X-Audio-Mode,X-Duration-Ms,X-Audio-Bitrate. - Example:
http://127.0.0.1:8787/stream?v=aqz-KE-bpKQ&itag=251&guest=1&start=0
- Streams live
GET /info?v=<id_or_url>&itag=251— Returns JSON metadata +streamUrl.GET /search?q=<query>&limit=5— Returns JSON search results.GET /health— Health check endpoint.
[!TIP] Set
YTDLM_API_KEY=your_secretto requireX-API-KeyorAuthorization: Bearerauthentication on all HTTP endpoints.
7. Multi-Language Discord Bot Integration Guides
7.1. JavaScript / TypeScript (discord.js v14 + @discordjs/voice) — Zero-FFmpeg
See sdks/javascript/discord-js-bot.js.
Because ytdlm emits native 48kHz Stereo WebM/Opus with a patched indeterminate Segment header, @discordjs/voice can demux the stream directly via StreamType.WebmOpus without spawning FFmpeg!
const { Client, GatewayIntentBits } = require('discord.js');
const { joinVoiceChannel } = require('@discordjs/voice');
const { DiscordMusicManager } = require('./sdks/javascript/discord-js-bot');
const client = new Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildVoiceStates,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.MessageContent,
],
});
// Multi-guild concurrent manager (isolated O(1) memory per guild, Guest mode 251)
const music = new DiscordMusicManager({ itag: 251, guest: true });
client.on('messageCreate', async message => {
if (message.author.bot || !message.guild) return;
if (message.content.startsWith('!play ')) {
const query = message.content.slice(6).trim();
const voiceChannel = message.member?.voice?.channel;
if (!voiceChannel) return message.reply('Join a voice channel first!');
const connection = joinVoiceChannel({
channelId: voiceChannel.id,
guildId: message.guild.id,
adapterCreator: message.guild.voiceAdapterCreator,
});
const track = await music.enqueue(message.guild.id, connection, query);
return message.reply(
`Playing [${track.mode} | ${Math.round(track.bitrate / 1000)}kbps]: **${track.title}** (${track.durationSec}s)`
);
}
if (message.content.startsWith('!seek ')) {
const sec = Number(message.content.slice(6).trim()) || 0;
const track = await music.seek(message.guild.id, sec * 1000);
if (track) return message.reply(`Seeked to **${sec}s**!`);
}
if (message.content.startsWith('!loop ')) {
const mode = message.content.slice(6).trim().toLowerCase(); // 'off' | 'track' | 'queue'
music.setLoopMode(message.guild.id, mode);
return message.reply(`Loop mode set to: **${mode}**`);
}
if (message.content === '!skip') {
music.skip(message.guild.id);
return message.reply('Skipped!');
}
if (message.content === '!stop') {
music.destroyGuild(message.guild.id);
return message.reply('Stopped and disconnected.');
}
});
client.login(process.env.DISCORD_TOKEN);7.2. Python (discord.py / py-cord / nextcord)
See sdks/python/ytdlm.py and sdks/python/discord_py_bot.py.
Use discord.FFmpegOpusAudio(proc.stdout, pipe=True, codec="copy") so FFmpeg copies the pre-encoded 48kHz Opus frames directly to Discord Voice with 0% transcoding CPU usage:
import discord
from discord.ext import commands
from ytdlm import YtdlmClient
intents = discord.Intents.default()
intents.message_content = True
bot = commands.Bot(command_prefix="!", intents=intents)
ytdlm = YtdlmClient(default_itag=251, force_guest=True)
guild_processes = {}
def cleanup_guild_proc(guild_id: int):
proc = guild_processes.pop(guild_id, None)
if proc:
try:
proc.terminate()
except Exception:
pass
@bot.command(name="play")
async def play(ctx: commands.Context, *, query: str):
if not ctx.author.voice:
return await ctx.send("Please join a voice channel first!")
vc: discord.VoiceClient = ctx.voice_client or await ctx.author.voice.channel.connect()
if vc.is_playing():
vc.stop()
cleanup_guild_proc(ctx.guild.id)
track = ytdlm.get_info(query)
proc = ytdlm.open_stream_pipe(track.video_id, itag=track.itag, start_ms=0, loop=False)
guild_processes[ctx.guild.id] = proc
source = discord.FFmpegOpusAudio(proc.stdout, pipe=True, codec="copy")
vc.play(source, after=lambda err: cleanup_guild_proc(ctx.guild.id))
await ctx.send(
f"▶️ Streaming [**{track.mode}** | `{track.abr_kbps}kbps` Opus]: **{track.title}** — *{track.author}*"
)7.3. Java (JDA + Lavaplayer / Lavalink)
See sdks/java/YtdlmClient.java and sdks/java/pom.xml.
Register HttpAudioSourceManager in Lavaplayer and pass ytdlm.getStreamUrl(...) to playerManager.loadItem(...) — Lavaplayer's built-in MatroskaAudioTrack demuxer streams the WebM/Opus chunks directly over HTTP:
import com.sedmelluq.discord.lavaplayer.player.AudioPlayerManager;
import com.sedmelluq.discord.lavaplayer.player.DefaultAudioPlayerManager;
import com.sedmelluq.discord.lavaplayer.source.http.HttpAudioSourceManager;
import dev.ytdlm.YtdlmClient;
public class JdaLavaplayerIntegration {
private final AudioPlayerManager playerManager = new DefaultAudioPlayerManager();
private final YtdlmClient ytdlm = new YtdlmClient("ytdlm", "http://127.0.0.1:8787", 251, true);
public JdaLavaplayerIntegration() {
this.playerManager.registerSourceManager(new HttpAudioSourceManager());
}
public String resolveStreamUrl(String queryOrUrl, long startTimeMs) {
return ytdlm.getStreamUrl(queryOrUrl, 251, startTimeMs);
}
}7.4. Rust (Serenity + Songbird / Twilight)
See sdks/rust/Cargo.toml and sdks/rust/src/lib.rs.
use songbird::input::HttpRequest;
use ytdlm_rs::YtdlmClient;
pub async fn play_on_songbird(
call_lock: std::sync::Arc<tokio::sync::Mutex<songbird::Call>>,
http_client: reqwest::Client,
query_or_url: &str,
start_ms: u64,
) -> Result<ytdlm_rs::TrackInfo, Box<dyn std::error::Error + Send + Sync>> {
let ytdlm = YtdlmClient::default().with_server_url("http://127.0.0.1:8787");
let info = ytdlm.get_info(query_or_url).await?;
let stream_url = ytdlm.get_stream_url(&info.video_id, start_ms);
let input = HttpRequest::new(http_client, stream_url);
let mut handler = call_lock.lock().await;
handler.play_only_input(input.into());
Ok(info)
}7.5. Go (DiscordGo) & C# (Discord.Net / DSharpPlus)
Go (exec.Command Live Pipe):
cmd := exec.Command("ytdlm", "stream", videoURL, "-f", "251", "--guest", "-o", "-", "-q")
stdoutPipe, err := cmd.StdoutPipe()
if err != nil {
return err
}
if err := cmd.Start(); err != nil {
return err
}
// Pipe stdoutPipe directly into dca / ffmpeg -c:a copyC# (Lavalink4NET / Victoria / HttpClient):
string streamUrl = $"http://127.0.0.1:8787/stream?v={Uri.EscapeDataString(videoId)}&itag=251&guest=1";8. Testing & Verification
Run the automated verification suite (test/verify.js), which tests against the Blender Foundation's Creative Commons Attribution 3.0 film (Big Buck Bunny, aqz-KE-bpKQ):
npm testSample output:
=== [1/3] Testing getInfo() (Guest TVHTML5 SABR 251) ===
Resolved in 572ms: {
videoId: 'aqz-KE-bpKQ',
title: 'Big Buck Bunny 60fps 4K - Official Blender Foundation Short Film',
durationSec: 635,
itag: 251,
mode: 'TV_GUEST_SABR_251',
isGuest: true,
bitrate: 128612
}
=== [2/3] Testing streamAudio() Real-Time Live Pipe ===
First WebM Init chunk emitted in 342ms (195 bytes)
Streamed 3 chunks (320.0 KB) before early stop.
=== [3/3] Testing HTTP Server (/info & /stream) ===
Server started at http://127.0.0.1:51881
HTTP /info OK: Big Buck Bunny 60fps 4K - Official Blender Foundation Short Film (TV_GUEST_SABR_251)
HTTP /stream first chunk received: 65049 bytes
ALL YTDLM VERIFICATION TESTS PASSED!9. Contributing
Contributions of all kinds—protocol updates, bug fixes, performance optimizations, and new language SDKs—are warmly welcomed!
Please read our Contributing Guidelines (CONTRIBUTING.md) before opening an Issue or Pull Request. Following the yt-dlp standard, never include URLs or titles of copyrighted commercial media in tests, code, or pull requests—always use Creative Commons / Open Source benchmark videos (such as Blender Foundation's aqz-KE-bpKQ).
10. License & Legal Disclaimer
Licensed under the standard MIT License. See DISCLAIMER.md for the full Legal Disclaimer, Copyright Notice, and Contributor Protection terms.
[!CAUTION] LEGAL DISCLAIMER & LIMITATION OF LIABILITY
- Educational & Research Purpose ("AS IS"):
ytdlmis provided free of charge on an "AS IS" basis strictly for educational and interoperability research into media streaming protocols. This project does not host, store, or distribute any copyrighted media content and is not affiliated with Google LLC, YouTube, or Discord Inc.- Complete Exemption from Liability, Fines & Penalties: In no event shall the authors or contributors be held liable for any claims, damages, administrative or civil fines, statutory penalties, DMCA claims, or account/IP suspensions arising from the use, modification, or deployment of this software.
- User Assumption of Risk: By downloading, installing, or integrating this software, you agree that you are solely responsible for complying with all applicable laws and third-party Terms of Service.
