Skip to content

Add support for SABR (v2) - #5814

Draft
unixfox wants to merge 15 commits into
iv-org:masterfrom
unixfox:sabr-shakajs-2
Draft

Add support for SABR (v2)#5814
unixfox wants to merge 15 commits into
iv-org:masterfrom
unixfox:sabr-shakajs-2

Conversation

@unixfox

@unixfox unixfox commented Jul 20, 2026

Copy link
Copy Markdown
Member

Checklist

  • I have read the AI Policy and understand the disclosure requirements

AI Disclosure

  • AI was not used to create this pull request
  • AI was used to fully create this pull request
  • AI was used to partially create this pull request

Model(s) used (and thinking/reasoning level if relevant):
Claude Opus 4.8

Tool(s) used:
Claude Code

How was AI used?

Was used to replicate the code from https://github.com/LuanRT/kira and freetube's SABR code.


Pull request description

This pull request is for adding SABR support to invidious.

Close #5263

TODO

  • Add support for https://github.com/LuanRT/ytc-bridge
  • Remove the dependency on Invidious companion (reuse work from trying to only use next endpoint for the major data #5003)
  • Make loading the video player faster
  • Fix glitch with loading icon in Shakajs
  • Wire potoken generation to Invidious companion since you can't generate a valid potoken directly in the browser anymore.
  • Avoid using esm.sh for fetch the dependencies.
  • Support extensions like SponsorBlock and DeArrow

Worth knowing

  • Potoken generation needs to be done in an external program because YouTube requires the potoken generation to be done as if you were connected to https://www.youtube.com. But this is impossible to do inside a browser.

Notes

  • Change This is not used for loading the video streams from YouTube servers (circumvent YouTube restrictions) in config.yml because wouldn't be true anymore since proxy is in invidious.

@unixfox unixfox mentioned this pull request Jul 20, 2026
6 tasks
mdbraber added a commit to mdbraber/owntube that referenced this pull request Jul 30, 2026
docs/OWNTUBE-UPSTREAM-PLAN.md proposes replacing Invidious with a service we own,
built on youtubei.js. Two pressures point the same way: the fork is now 8 patches
rebased forever, and upstream is moving to SABR client-side only
(iv-org/invidious#5814), which is a viable answer for a web player but not
obviously one for a client set that includes expo-video on Android TV.

The plan records what was measured rather than assumed — youtubei.js' coverage
(FreeTube's 2,270-line local.js, shipped to 21.5k users), how much of Invidious we
actually use (8 endpoint families; ~40% of it is UI and accounts), and that
server-side SABR is feasible because googlevideo exports SabrStream for headless
use. It also records four corrections to my own earlier reasoning, including that
Android TV does *not* structurally force a server-side connector — Materialious
ships SABR on Android TV through a Capacitor WebView, so that constraint is our
choice of expo-video, not the platform.

The detector is the part worth having today. `sabrExposure` measures the share of
*non-live* videos that no longer carry init/index byte ranges — the thing that
would silently break /dash and /hls. Currently 0/12. Live streams are excluded
because they are legitimately segment-addressed; that exclusion leans on liveNow
being correct, which Phase 3.2 fixed and listLiveFlag guards.

It samples `popular` rather than trending, which was the first attempt and
SKIPped: trending is the livestreams feed now, so it yields no non-live sample at
all.

Canary is 8 PASS + 1 SKIP.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@TheFrenchGhosty
TheFrenchGhosty requested a review from Copilot August 2, 2026 08:18

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds frontend SABR (v2) playback support (Shaka + SABR scheme/manifest) and adjusts backend parsing/error-handling so watch/embed pages can still render using /next-only metadata when the /player endpoint is unavailable (e.g., no Invidious Companion).

Changes:

  • Add a SABR playback mode, including a new /proxy route and a new SABR player path in the watch/embed templates.
  • Extend user/video preferences to support quality=sabr and a SABR codec preference.
  • Make video parsing more resilient by falling back to /next data and surfacing improved unplayable error info.

Reviewed changes

Copilot reviewed 39 out of 46 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
src/invidious/yt_backend/youtube_api.cr Clarifies player returning nil without Companion to enable /next fallback.
src/invidious/yt_backend/extractors_utils.cr Adds helper to infer unlisted status from badges when videoDetails is missing.
src/invidious/views/watch.ecr Hides player when video.reason is present; surfaces reason/subreason UI.
src/invidious/views/user/preferences.ecr Adds SABR quality option and codec preference UI.
src/invidious/views/embed.ecr Shows unplayable reason/subreason instead of player when unavailable.
src/invidious/views/components/player.ecr Switches between video.js and SABR player, and loads SABR assets when enabled.
src/invidious/videos/video_preferences.cr Adds quality_sabr handling to processed per-video params.
src/invidious/videos/parser.cr Adds /next fallback parsing and enriches error handling (reason/subreason).
src/invidious/videos.cr Bumps schema version and adds subreason accessor + safer reason accessor.
src/invidious/user/preferences.cr Adds persisted preference for quality_sabr.
src/invidious/routing.cr Registers new proxy routes.
src/invidious/routes/watch.cr Avoids redirects/raw playback when the video is in an error state.
src/invidious/routes/proxy.cr Introduces SABR-oriented proxy endpoint for browser SABR traffic.
src/invidious/routes/preferences.cr Persists quality_sabr from preferences form submissions.
src/invidious/routes/before_all.cr Adjusts CSP to allow SABR/BotGuard dependencies and network calls.
src/invidious/config.cr Adds default config preference for quality_sabr.
spec/invidious/videos/regular_videos_extract_spec.cr Adds coverage for parsing without videoDetails and for subreason null behavior.
scripts/fetch-sabr-dependencies.cr Fetches SABR-related third-party browser artifacts into assets.
scripts/fetch-player-dependencies.cr Hooks SABR dependency fetch into existing player dependency fetch flow.
scripts/bundle-sabr-libs.js Bundles googlevideo/bgutils via esbuild for browser use.
package.json Adds Node dependencies and bundling script for SABR libraries.
package-lock.json Locks Node dependencies for bundling SABR libraries.
locales/en-US.json Adds UI strings for SABR preferences and unplayable error label.
assets/js/sabr/youtubei.js/versions.yml Records youtubei.js asset version.
assets/js/sabr/shaka-player/versions.yml Records shaka-player asset version.
assets/js/sabr/shaka-player/controls.css Adds Shaka UI stylesheet asset (with font adjustments).
assets/js/sabr/googlevideo/versions.yml Records googlevideo asset version.
assets/js/sabr/bgutils-js/versions.yml Records bgutils-js asset version.
assets/js/sabr/bgutils-js/bgutils.bundle.min.js Adds bundled bgutils-js browser artifact.
assets/js/sabr_ebml_parser.js Adds EBML parsing helper for SABR segment index parsing.
assets/js/sabr_mp4_index.js Adds MP4 segment index parser for SABR manifests.
assets/js/sabr_webm_index.js Adds WebM segment index parser for SABR manifests.
assets/js/sabr_manifest_parser.js Registers Shaka manifest parser for application/sabr+json.
assets/js/sabr_scheme_plugin.js Registers Shaka networking scheme sabr: and routes traffic through /proxy.
assets/js/sabr_helpers.js Adds shared helper utilities (proxy URL building, Onesie crypto helpers, caching).
assets/js/sabr_potoken.js Adds BotGuard/PoToken generation logic for SABR playback.
assets/js/sabr_onesie.js Adds Onesie player-response fetching for SABR URL portability.
assets/js/sabr_player.js Implements the SABR player orchestrator using Shaka + SABR scheme/manifest.
assets/js/sabr_loader.js Loads ES module SABR dependencies and exposes them on window.
assets/js/sabr_init.js Boots SABR player on page load once SABR libs are loaded.
assets/css/sabr_player.css Adds styling to align Shaka UI with existing player styling.
.gitignore Ignores node_modules/.
Files not reviewed (1)
  • assets/js/sabr/shaka-player/controls.css: Generated file
Suppressed comments (4)

src/invidious/routes/proxy.cr:213

  • The proxied response also reflects Origin and sets Access-Control-Allow-Credentials=true. Even if preflight is tightened, this still enables credentialed cross-origin reads for any allowed upstream host. If CORS is needed at all here, avoid Allow-Credentials and only set ACAO when an Origin is present (and add Vary: Origin).
    src/invidious/routes/proxy.cr:95
  • Custom headers from the __headers query param are currently accepted verbatim, but ALLOWED_HEADERS is not enforced. This allows clients to smuggle arbitrary headers upstream (e.g., Cookie, Host, X-Forwarded-*, etc.), which is a security risk even with an allowlisted host set. Filter __headers to the explicit allowlist (case-insensitive) before copying into request_headers.
    src/invidious/routes/proxy.cr:100
  • This route uses puts for warning output. Invidious routes elsewhere use LOGGER, which integrates with the app’s log level/formatting. Please use LOGGER.warn here so proxy warnings are captured consistently.
    src/invidious/routes/proxy.cr:193
  • For POST requests, the proxy reads the entire request body into memory via getb_to_end before forwarding. A malformed or unexpectedly large body can cause high memory usage per request. Consider streaming the request body to the upstream client (or enforcing a strict size limit / Content-Length cap) instead of buffering everything.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/invidious/routes/before_all.cr Outdated
Comment on lines +51 to +55
"script-src 'self' 'unsafe-eval' 'unsafe-inline'",
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data:",
"font-src 'self' data:",
"connect-src 'self' " + COMPANION_CSP.companion_urls,
"connect-src 'self' https://*.googleapis.com https://*.youtube.com " + COMPANION_CSP.companion_urls,
Comment on lines +85 to +89
<script>
document.getElementById('quality').addEventListener('change', function() {
var sabrGroup = document.getElementById('sabr-codec-group');
if (this.value === 'sabr') {
sabrGroup.style.display = '';
Comment on lines +49 to +61
# OPTIONS /proxy
def self.options(env)
origin = env.request.headers["Origin"]? || "*"

env.response.headers["Access-Control-Allow-Origin"] = origin
env.response.headers["Access-Control-Allow-Methods"] = "GET, POST, OPTIONS"
env.response.headers["Access-Control-Allow-Headers"] = ALLOWED_HEADERS.join(", ")
env.response.headers["Access-Control-Max-Age"] = "86400"
env.response.headers["Access-Control-Allow-Credentials"] = "true"

env.response.status_code = 200
""
end
Comment thread package.json
Comment on lines +8 to +12
"dependencies": {
"googlevideo": "^4.0.4",
"youtubei.js": "^17.0.1",
"bgutils-js": "^3.2.0"
},
Copilot AI review requested due to automatic review settings August 2, 2026 08:37
@unixfox
unixfox marked this pull request as draft August 2, 2026 08:38

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 39 out of 46 changed files in this pull request and generated 1 comment.

Files not reviewed (1)
  • assets/js/sabr/shaka-player/controls.css: Generated file
Suppressed comments (8)

src/invidious/routes/before_all.cr:53

  • The global CSP is being weakened for every response by adding both 'unsafe-eval' and 'unsafe-inline' to script-src. This substantially increases the blast radius of any XSS (inline script execution + eval) and also affects pages that never load the SABR player. Consider scoping these relaxations to SABR pages only (e.g., watch/embed when quality=sabr), or using nonces/hashes + avoiding eval where possible.
    package.json:12
  • package.json dependency versions don't match package-lock.json (e.g. package.json requests googlevideo ^4.1.1 / youtubei.js ^17.2.0 / bgutils-js ^4.0.2, but the lockfile pins 4.0.4 / 17.0.1 / 3.2.0). This will produce inconsistent installs depending on whether npm honors the lockfile, and can break bundling/reproducibility. Align the versions (or regenerate the lockfile).
  "dependencies": {
    "googlevideo": "^4.1.1",
    "youtubei.js": "^17.2.0",
    "bgutils-js": "^4.0.2"
  },

src/invidious/routes/proxy.cr:220

  • The proxy response sets Access-Control-Allow-Credentials: true. Removing this avoids credentialed cross-origin reads, and also avoids the invalid "" + credentials combination when Origin is missing (since this route currently falls back to "").
    src/invidious/routes/proxy.cr:59
  • CORS is currently reflecting the request Origin (or using "") while also advertising Access-Control-Allow-Credentials: true. Using "" with credentials is invalid per the CORS spec, and reflecting arbitrary origins with credentials can allow other sites to read proxied responses. Since /proxy is same-origin for the SABR player, it should not need credentialed CORS at all.
    src/invidious/routes/proxy.cr:104
  • This route logs parse failures with puts, which bypasses the app logger and can spam stdout in production. Use LOGGER.warn (or similar) for consistency with the rest of the routes.
    assets/js/sabr_player.js:42
  • BG_HELPER_URL is hardcoded to http://127.0.0.1:4416/generate, which is the end-user's localhost (browser-side), not the Invidious server. For remote instances this will always fail, and allowing/encouraging browser JS to talk to localhost can have security implications (probing local services). Consider making this a same-origin endpoint (e.g. /bg-helper/generate) and proxying server-side, or gating this feature behind an explicit opt-in.
  // bg-helper-server /generate endpoint. Mints an ATTESTED (StreamProtectionStatus=1)
  // PO token server-side, where jsdom presents a youtube.com origin. In-browser BotGuard
  // (Invidious origin) can only get status 2 (pending), so it's used only as a fallback.
  var BG_HELPER_URL = 'http://127.0.0.1:4416/generate';

assets/js/sabr_scheme_plugin.js:308

  • The SABR_REDIRECT handler assigns to currentState.sabrUrl, but the request loop uses currentState.sabrStreamState.sabrUrl. As written, the assignment has no effect and is misleading (and also contradicts the header comment claiming the redirect bug is fixed). If redirects are intentionally ignored for compatibility, remove this write entirely (or update the comments accordingly).
              // (sabrStreamState.sabrUrl). Earlier we "fixed" this to follow the redirect,
              // which pins the session to a specific CDN host (rrN---snXXX) that maintains a
              // sequential cursor and returns 0 media when the client jumps/seeks. Following
              // the redirect breaks seeking; ignoring it (as FreeTube does) keeps seeks working.
              currentState.sabrUrl = sabrRedirect.url;

scripts/fetch-sabr-dependencies.cr:5

  • require "digest/sha1" is unused in this script. Keeping unused requires increases load time and can trip linting/static checks. Remove it if not needed.
require "http"
require "yaml"
require "digest/sha1"
require "option_parser"
require "colorize"

Comment on lines +95 to +99
name = header_array[0]?.try &.as_s
value = header_array[1]?.try &.as_s
if name && value
custom_headers[name] = value
end
unixfox and others added 15 commits August 3, 2026 02:50
… 5.1.10), companion-optional watch page, no esm.sh
Segment index parsers (mp4/webm) declared the per-segment URI array with
`var` inside the loop, so every SegmentReference's getUris() closure
captured the same function-scoped binding and returned the LAST segment's
URL. YouTube received the final segment's startTimeMs/sq for every
segment, replied with policy-only UMP (no media), and the player looped
forever on a black screen ("SABR throttled by YouTube"). Use `let`
(block-scoped, fresh per iteration) to match FreeTube's `const`.

Also scope the big centered play button styling to
`.shaka-play-button-container`; the bare `.shaka-play-button` selector
also matched the control-bar play button (48px siblings), oversizing it
to ~54px and pushing it out of alignment.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the plain /youtubei/v1/player request (made through the proxy,
and therefore bound to the proxy's egress IP) with an encrypted Onesie
request using the WEB client. Onesie is proxied by YouTube's "trusted
bandaid", so the returned player response - and the server_abr_streaming_url
inside it - is not tied to our egress IP. Media is still pulled over SABR
(sabr_scheme_plugin.js); only how the player response is obtained changes.

- sabr_onesie.js: new module, window.fetchOnesiePlayerResponse(). Builds the
  WEB player request, encrypts it (OnesieInnertubeRequest + OnesieRequest),
  POSTs to the onesie endpoint through /proxy, parses ONESIE_HEADER/ONESIE_DATA
  UMP parts, gunzips/decrypts, returns the raw player response JSON. Adapted
  from googlevideo/examples/onesie-request and invidious-secret-companion's
  WEB-client variant.
- sabr_helpers.js: add decryptResponse() (AES-CTR + HMAC verify), companion to
  the existing encryptRequest().
- sabr_loader.js: expose window.YT so a VideoInfo can be built from the raw
  onesie player response.
- sabr_player.js: in loadVideo(), fetch the player response via Onesie and wrap
  it in new YT.VideoInfo(...); fall back to innertube.getInfo() on failure.
- player.ecr: load sabr_onesie.js before sabr_player.js.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.

[Enhancement] SABR support

2 participants