Clients
YouTube answers each of its apps differently: one plays age-restricted videos, another plays live streams, another needs a PO token. Ytdl asks the clients in clients in order, and the first one that plays wins.
import { Client, Ytdl } from "@distube/ytdl-core";
const yt = new Ytdl({ clients: [Client.WebEmbedded, Client.Tv] });Client lists only clients that played a video in the live suite when each was the only client. DEFAULT_CLIENTS is all five in this order: visionos, web_embedded, tv, mweb and web_creator. An unknown name, such as a 4.x client like "android", throws INVALID_ARGUMENT.
- A client that needs a PO token is skipped without
getPoToken: mweb and web_creator. - With a cookie, a client that accepts one is asked again signed in, right after its signed-out request. web_creator is only asked signed in.
- A client whose media URLs keep failing counts as blocked for ten minutes and moves to the end of the list.
What each client plays
Section titled “What each client plays”The signed-out table comes from a run of one client at a time on 2026-10-21, over one to three videos per class. Cells are ✅ when getInfo returned a playable format for every video, ❌ when it did for none, and “some” when the result depended on the video. The signed-in table comes from a cookie run on 2026-10-22 and is narrower, see below.
Without a cookie:
| Client | Normal | Age-restricted | Made for kids | Embedding disabled | Live HLS | Live DASH |
|---|---|---|---|---|---|---|
| visionos | ✅ | ❌ | ❌ | ✅ | ✅ | ✅ |
| web_embedded | ✅ | some | ✅ | ❌ | ✅ | ✅ |
| tv | ✅ | ❌ | ✅ | ✅ | ❌ | ✅ |
| mweb | ✅ | ❌ | ✅ | ✅ | ✅ | ✅ |
- web_embedded played one of two age-restricted videos and reported
AGE_RESTRICTEDfor the other. Pass a cookie when age-restricted videos matter. - visionos answers a made-for-kids video with no formats, so the ladder moves on to web_embedded.
- tv lists live formats without an HLS manifest. For an upcoming live event, tv reports
NOT_STARTEDlike the others, without a start time when YouTube gives none. - visionos lists a live stream’s video variants with no audio. The audio comes as a separate HLS rendition, which
getInfolists as an audio-only format withisHLS. Choose it withfilter: { hasAudio: true, hasVideo: false }, or use a variant that reportshasAudio.
With a cookie, the table lists what each signed-in rung returned. Error codes are YtdlErrorCode values. A YouTube status is marked as such. visionos has no signed-in rung, so it plays signed out even when you pass a cookie. “Not measured” means no run asked that client signed in for that class.
| Client | Normal | Age-restricted | Made for kids | Live | Region-blocked |
|---|---|---|---|---|---|
| web_embedded | not measured | YouTube status ERROR (1 of 2 videos asked) |
not measured | not measured | NOT_FOUND |
| tv | not measured | played (1 of 1 asked) | not measured | not measured | REGION_BLOCKED |
| mweb | not measured | not measured | not measured | not measured | REGION_BLOCKED |
| web_creator | ✅ | ✅ | ✅ | ✅ | REGION_BLOCKED |
- web_creator was the only client in the run, with a cookie and a PO token provider, over seven videos. It played every video except the region-blocked one. The sweep did not separate HLS from DASH live streams.
- web_embedded played the age-restricted video
HtVdAasjOgUsigned out, so its signed-in rung was never asked. ForTq92D6wQ1mgit returnedUNPLAYABLEsigned out andERRORsigned in, and tv played it signed in afterLOGIN_REQUIREDsigned out. - On the region-blocked video the signed-in web_embedded rung reported
NOT_FOUNDand the other rungsREGION_BLOCKED.NOT_FOUNDranks afterREGION_BLOCKED, so the error isREGION_BLOCKED.