YouTube media 403 on Windows despite deno + curl_cffi present: only player_client=mweb works, but mweb alone kills subtitles
Summary
On Windows with a fully-provisioned environment (deno present, curl_cffi impersonation targets available), every YouTube media download returns HTTP Error 403: Forbidden while captions download fine in the same run. The only player_client that returns usable media URLs is mweb.
This is not #93 (brew yt-dlp with zero impersonation targets) or #67 (missing deno/curl_cffi) — both of those root causes are absent here, and --impersonate does not help. Filing separately since the remedy is different.
Environment
- Windows 11, Python 3.14.5
- yt-dlp
[email protected](stable2026.07.04behaves identically) - ffmpeg
N-125875-g5d4d3bdc61-20260731 yt-dlp --list-impersonate-targets→ Chrome-133/136, Safari-17.2/18.0/18.4/26.0, etc. (curl_cffi bundled in the standalone exe)denoinstalled and on PATH
What fails
yt-dlp -F lists formats fine, so the failure is specific to fetching media, not to extraction:
ERROR: unable to download video data: HTTP Error 403: ForbiddenReproduces on any video, including a 19-second unrelated one, so it is not video-specific or rate limiting.
Tested per client on -f "worst[height<=360]/worst":
| player_client | media download |
|---|---|
default |
403 |
web |
403 |
tv |
403 |
web_safari |
403 |
ios |
403 |
mweb |
OK |
--impersonate chrome and --impersonate safari also 403 with the default client.
The part that makes the obvious fix wrong
Setting player_client=mweb globally fixes media but silently breaks subtitles — mweb requires a PO token for them:
WARNING: [youtube] There are missing subtitles languages because a PO token was
not provided. Subtitles for these languages are missing: en, ...So /watch then returns 100 frames and Transcript: none available, which reads like a captions bug rather than a side effect.
Listing both clients does not resolve it either — it just flips which half breaks:
| extractor-args | subtitles | media |
|---|---|---|
player_client=mweb |
missing | OK |
player_client=default,mweb |
OK | 403 |
player_client=default |
OK | 403 |
Fix that worked
Because download.py already makes two separate yt-dlp calls, the two passes can use different clients. In fetch_captions():
cmd = [
"yt-dlp",
"--extractor-args", "youtube:player_client=default",
"--skip-download",
...and in the media download:
cmd = [
"yt-dlp",
"--extractor-args", "youtube:player_client=mweb",
"-N", "8",
...With that split, captions and frames both work on the same run. Happy to open a PR if the approach looks right — though a maintainer may well know a cleaner client than mweb to pin the media pass to.
Aside
--cookies-from-browser chrome is not a usable fallback here: it fails with Could not copy Chrome cookie database whenever Chrome is running, which for most people is always.
Source: bradautomates/claude-video