Skip to content

fix(trailer): reject PO-token-gated stream URLs and resolve without the rate-limited watch page - #1831

Open
aneeshtigga wants to merge 1 commit into
NuvioMedia:cmp-rewritefrom
aneeshtigga:fix/trailer-resolution
Open

aneeshtigga wants to merge 1 commit into
NuvioMedia:cmp-rewritefrom
aneeshtigga:fix/trailer-resolution

Conversation

@aneeshtigga

@aneeshtigga aneeshtigga commented Aug 30, 2026

Copy link
Copy Markdown

Summary

Trailers stop failing with [ffmpeg] https: HTTP error 403 Forbidden, and resolution no longer depends on the watch page that YouTube rate limits first. Reachability probing now detects PO token gated URLs, and resolution falls back through HLS, separate audio/video and finally ANDROID progressive itag 18, so a trailer keeps playing at lower quality instead of failing.

PR type

  • Behavior bug/regression fix

Why

Two independent causes, both measured directly and documented in #1857.

1. isUrlReachable probes with range: bytes=0-0, which cannot see PO token gating.
ANDROID and IOS adaptive URLs answer the first range and then reject every later range. This is independent of User-Agent (tested Chrome, Safari, Lavf and each client's own UA) and independent of itag:

Client that minted the URL bytes=0-65535 tail range full GET
IOS (itag 137) 206 403 403
ANDROID (itag 137, 136, 134, 140, 251) 206 403 403
ANDROID (itag 18, progressive) 206 n/a 200, downloads fully
VISIONOS (itag 137) 206 206 200

The head range always succeeds, so a dead URL passed validation and mpv/ffmpeg then 403'd on the next range, which is the exact error users see.

2. Extraction required the watch page, which is the first thing YouTube rate limits.
extractPlaybackSourceInternal fetched the watch page (1.1-1.4 MB) before every player call, only to scrape INNERTUBE_API_KEY and VISITOR_DATA, and threw when that failed. Once the device is flagged the page returns 429 and then a 302 to a 407 byte error page, so extraction died before any player call. Measured at that same moment:

Video Player API (ANDROID) Progressive itag 18
MAbjq7ydKDI OK with and without a key full GET 200, 10,139,298 bytes
8ZYhuvIv1pA OK with and without a key full GET 200, 4,282,937 bytes

INNERTUBE_API_KEY is a public constant present in every YouTube page (the same value this code already scraped), so it is used directly and the watch page is only consulted if every client returned nothing.

Issue or approval

Fixes #1857. Same fix for desktop: NuvioMedia/NuvioDesktop#552 (issue NuvioMedia/NuvioDesktop#584).

(The first filing, #1829, was closed by the unlabeled-issue bot before triage because an outside contributor cannot set labels from the API. #1857 is the same report submitted through the bug template so it carries the bug label.)

UI / behavior impact

  • No UI change
  • Behavior changed only to fix a documented bug/regression

Policy check

  • I have read and understood CONTRIBUTING.md.
  • This PR is small, focused, and limited to one problem.
  • This PR is not cosmetic-only.
  • Any UI change fixes a linked glitch/bug and includes visual proof, or this PR has no UI change.
  • Any behavior change fixes a linked bug/regression or has explicit approval, or this PR has no behavior change.
  • This PR does not bundle unrelated refactors, cleanups, formatting, or drive-by changes.
  • This PR does not add dependencies, architecture changes, migrations, or product-direction changes without explicit approval.
  • I listed the testing performed below.

What changed

Three files: the shared extractor plus the iOS and Android TrailerExtractionPlatform actuals.

  • Reachability requires head and tail ranges (tail only when clen is present), so gated URLs are rejected instead of reaching the player. One rejection condemns the rest of that client's adaptive formats, so its siblings are not probed.
  • buildPlaybackSource now receives candidate lists rather than one pre-picked candidate per kind, and tries HLS, then separate streams, then progressive, taking the first that probes clean. A video only source is kept as a last resort so a silent trailer never wins over one with audio.
  • Watch page is off the hot path: the public constant key is used, and the page is scraped only when every client came back empty.
  • Clients whose URLs are not gated (visionos, android_vr) are preferred, and the remaining clients are skipped once one of them produced candidates. Typical extraction is one player call instead of three, plus no 1.4 MB page fetch.
  • Client versions refreshed: android 20.10.35 to 21.26.364, ios 20.10.1 to 21.26.4, and android_vr added. Stale versions attract bot challenges on their own.
  • UNPLAYABLE and ERROR are final for that video (geo blocks, removed videos) instead of being retried.
  • After an HTTP 429 the extractor backs off for 15 minutes instead of retrying per trailer, per hover and per hero. Request volume from a single residential IP is what gets a whole network flagged ([Feature]: Opt-in "Open Trailers Externally" fallback for YouTube-flagged IPs NuvioTV#2451 reports YouTube blocking every device behind the IP).

Scope boundaries

Intentionally not included:

  • No caching of resolved URLs. Desktop caches them; this repository does not, and I kept it that way to stay inside the scope of this bug. It is worth adding separately, since re-resolving on every hero, hover and popup is a large part of the request volume that triggers flagging.
  • No UI or copy changes, and no changes to hero trailer or hover preview call sites.
  • No PO token / BotGuard support and no cookie support. Both address the underlying bot check more completely and both deserve their own feature request.
  • No new diagnostics or logging, to keep the diff limited to behavior.

Testing

Manual, iOS, plus compile checks:

  1. Built an unsigned release ipa from this branch with scripts/build-ios-ipa.sh and installed it on an iPhone. Trailers play, including titles that previously failed with the ffmpeg 403.
  2. Reproduced the original failures first on the previous code: the ffmpeg 403 on adaptive URLs, and total failure once the IP was flagged.
  3. Verified a geo blocked trailer ("The uploader has not made this video available in your country") now fails on its own and does not affect the next trailer.
  4. Verified the gate behaviour outside the app by replaying the same requests and comparing head range, tail range and full GET per client and per itag (tables above).
  5. ./gradlew :composeApp:compileKotlinIosArm64 and ./gradlew :composeApp:compileAndroidMain both pass.

Android was validated by compilation only; I do not have an Android device to hand, and the Android platform change is the same probe and fallback logic as iOS.

Screenshots / Video (UI changes only)

Not a UI change.

Breaking changes

None.

Linked issues

Fixes #1857. Related: NuvioMedia/NuvioDesktop#584, NuvioMedia/NuvioDesktop#552, NuvioMedia/NuvioTV#2451.

…page

isUrlReachable probed with range bytes=0-0, which cannot detect PO token
gating: ANDROID and IOS adaptive URLs answer the first range and 403 the rest,
so dead URLs passed validation and ffmpeg failed with 403 during playback.
Probes now check head and tail, gated clients are dropped after one rejection,
and resolution falls back to ANDROID progressive itag 18.

Extraction also fetched the 1.1-1.4 MB watch page before every player call
just to scrape the public INNERTUBE_API_KEY, so a rate limited device failed
before reaching a player endpoint that still worked. The constant key is used
directly now and the page is a last resort.

Also prefers ungated clients, skips the rest once one resolves, refreshes
stale client versions, treats UNPLAYABLE as final, and backs off after a 429.

Details and measurements in NuvioMedia#1829.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Trailers 403 mid-open then stop resolving: bytes=0-0 probe misses PO-token gating and extraction needs the rate-limited watch page

1 participant