Skip to content

Commit e91b123

Browse files
fix(youtube): SABR keeps first field-3 (single-track buffer fix)
1 parent 9e7ddcb commit e91b123

2 files changed

Lines changed: 168 additions & 92 deletions

File tree

src/config.rs

Lines changed: 23 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -210,29 +210,31 @@ pub struct Config {
210210
#[serde(default)]
211211
pub relay_url_patterns: Vec<String>,
212212

213-
/// Strip SABR quality-track entries (top-level field-3 of the
214-
/// segment-fetch protobuf) from `/videoplayback` POST bodies on
215-
/// `*.googlevideo.com` / `*.youtube.com`. Default `true` — this is
216-
/// the upstream-parity behaviour and the fix for "Response too
217-
/// large" 502s on multi-track segment fetches that exceed Apps
218-
/// Script `UrlFetchApp`'s ~10 MB cap (commits 9b6d03e + 33db28a
219-
/// from upstream Python).
213+
/// Strip surplus SABR quality-track entries (top-level field-3 of
214+
/// the segment-fetch protobuf) from `/videoplayback` POST bodies on
215+
/// `*.googlevideo.com` / `*.youtube.com`. Default `true` — fixes
216+
/// "Response too large" 502s on multi-track segment fetches that
217+
/// exceed Apps Script `UrlFetchApp`'s ~10 MB cap (commits 9b6d03e
218+
/// + 33db28a from upstream Python).
220219
///
221-
/// **Why this kill-switch exists** (#977 testing report from
222-
/// `unacoder`, May 2026): under `apps_script` mode with
223-
/// `youtube_via_relay = false`, forcing single-quality-track
224-
/// responses can interact poorly with playback at faster-than-1×
225-
/// speeds (1.7×–2×). Each chunk represents less buffer-ahead
226-
/// duration than a multi-track bundle would, and at speed-up the
227-
/// player drains the buffer faster than the next chunk arrives —
228-
/// reported as "only one videoplayback request is buffered."
220+
/// **Heuristic** (diverges from upstream's "strip all field-3"):
221+
/// the first field-3 entry is always kept; only the 2nd and
222+
/// subsequent ones are stripped, and only when at least one
223+
/// field-2 byte-range entry is present (segment-fetch shape, not
224+
/// session-init). Single-track requests pass through unchanged so
225+
/// googlevideo always has a track selected. This was added in
226+
/// response to #977 testing (unacoder, May 2026): the original
227+
/// strip-all rule turned single-track requests into "zero tracks
228+
/// selected" requests, which googlevideo answered with empty
229+
/// bodies — buffer never advanced, player retried with `rn=`
230+
/// incrementing.
229231
///
230-
/// Flip to `false` if you hit that regression. The trade-off is
231-
/// that long-form videos may then 502 on segments where Google
232-
/// would have bundled multiple quality tracks; the player handles
233-
/// that by falling back to a lower quality. The pre-port behaviour
234-
/// (no SABR strip) was tolerable for most users — the strip is a
235-
/// quality-of-life fix for the specific bundling-blowup case.
232+
/// **When to flip to `false`**: if you still see buffering issues
233+
/// on long-form video playback after the keep-first refinement,
234+
/// turn the strip off entirely to revert to the pre-port behaviour
235+
/// (occasional 502s on multi-track segments → player falls back
236+
/// to a lower quality). The pre-port behaviour was tolerable for
237+
/// most users; the strip is an opt-in quality-of-life fix.
236238
#[serde(default = "default_sabr_strip")]
237239
pub sabr_strip: bool,
238240

src/domain_fronter.rs

Lines changed: 145 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -3627,40 +3627,58 @@ fn url_host_is_youtube_video_endpoint(url: &str) -> bool {
36273627
.any(|s| h == *s || h.ends_with(&format!(".{}", s)))
36283628
}
36293629

3630-
/// Strip top-level field-3 (quality-track selection) entries from a SABR
3631-
/// segment-fetch protobuf body.
3630+
/// Strip surplus field-3 (quality-track selection) entries from a SABR
3631+
/// segment-fetch protobuf body, keeping the first one intact.
36323632
///
36333633
/// YouTube's SABR (Server-Adaptive Bitrate) `videoplayback` POST bodies
36343634
/// come in two distinct message shapes:
36353635
///
36363636
/// * **Segment-fetch** — carries field-2 (tag `0x12`) byte-range entries
36373637
/// for video/audio segments. Field-3 (tag `0x1a`) entries here are
3638-
/// quality-track selectors that ask googlevideo to bundle multiple
3639-
/// simultaneous quality tracks into one response, easily exceeding
3640-
/// Apps Script `UrlFetchApp`'s ~10 MB response buffer → 502. Stripping
3641-
/// them forces a single-track response that fits.
3638+
/// quality-track selectors that ask googlevideo to return a particular
3639+
/// quality track. When the player asks for *multiple* tracks at once
3640+
/// (multi-track bundling), googlevideo concatenates them into a single
3641+
/// response — easily exceeding Apps Script `UrlFetchApp`'s ~10 MB cap
3642+
/// → 502.
36423643
///
36433644
/// * **Session-init** — carries field-5 (tag `0x2a`) entries and *no*
36443645
/// field-2 entries. Field-3 here is essential session metadata
36453646
/// (language, viewer state, ...). Stripping it corrupts the init
36463647
/// handshake → CDN returns 403.
36473648
///
3648-
/// Heuristic: only strip field-3 when at least one field-2 entry is also
3649-
/// present at the top level (segment-fetch shape). Otherwise return the
3650-
/// body unchanged.
3649+
/// **Heuristic**:
3650+
///
3651+
/// 1. Body must be segment-fetch shape (≥ 1 field-2 entry). Otherwise
3652+
/// no-op — session-init bodies must not be touched.
3653+
/// 2. Body must carry **2 or more** field-3 entries before stripping
3654+
/// fires. The first field-3 is always kept; only the 2nd, 3rd, ...
3655+
/// are removed.
3656+
///
3657+
/// **Why keep the first field-3** (#977, unacoder testing, May 2026):
3658+
/// the original "strip all field-3" rule produced empty googlevideo
3659+
/// responses on single-track requests — the player asks for ONE track
3660+
/// via a sole field-3, we strip it, and the CDN answers a request with
3661+
/// zero tracks selected by sending nothing. The player retries
3662+
/// indefinitely with the `rn=` retry counter incrementing, never
3663+
/// advancing its buffer. Keeping the first field-3 means single-track
3664+
/// requests pass through unchanged (no regression at low quality)
3665+
/// while multi-track requests still get capped to one track (the
3666+
/// 10 MB-blowup fix is preserved).
36513667
///
36523668
/// Only top-level fields are inspected; nested messages are left intact.
36533669
/// On a malformed body (truncated tag, unknown wire type) the unparsed
36543670
/// tail is appended verbatim so a corrupt request is never silently
3655-
/// truncated. Ported from upstream `_strip_sabr_quality_tracks`
3656-
/// (commits 9b6d03e + 33db28a).
3671+
/// truncated. Originally ported from upstream
3672+
/// `_strip_sabr_quality_tracks` (commits 9b6d03e + 33db28a); the
3673+
/// keep-first refinement diverges from upstream based on local testing.
36573674
pub(crate) fn strip_sabr_quality_tracks(body: &[u8]) -> Vec<u8> {
36583675
// Phase 1: single pass — collect (field_number, start, end) for every
3659-
// top-level field. We need to know whether field 2 exists before deciding
3660-
// to strip, but a two-pass walk would be wasteful.
3676+
// top-level field. We need both the segment-fetch detection (field-2
3677+
// present) AND the field-3 count (≥ 2 to fire) before deciding,
3678+
// and a two-pass walk would be wasteful.
36613679
let mut segments: Vec<(u32, usize, usize)> = Vec::new();
36623680
let mut has_field2 = false;
3663-
let mut has_field3 = false;
3681+
let mut field3_count: usize = 0;
36643682
let mut i = 0usize;
36653683
let n = body.len();
36663684
let mut tail_start = n;
@@ -3778,20 +3796,32 @@ pub(crate) fn strip_sabr_quality_tracks(body: &[u8]) -> Vec<u8> {
37783796
if field_number == 2 {
37793797
has_field2 = true;
37803798
} else if field_number == 3 {
3781-
has_field3 = true;
3799+
field3_count += 1;
37823800
}
37833801
segments.push((field_number, seg_start, i));
37843802
}
37853803

3786-
// Phase 2: only strip when this is a segment-fetch body (has field 2)
3787-
// AND there's at least one field-3 entry to strip.
3788-
if !has_field2 || !has_field3 {
3804+
// Phase 2: only strip when this is a segment-fetch body (has field
3805+
// 2) AND there are at least 2 field-3 entries — i.e. real multi-
3806+
// track bundling. Single-track requests (one field-3) flow through
3807+
// unchanged so googlevideo still has a track selected.
3808+
if !has_field2 || field3_count < 2 {
37893809
return body.to_vec();
37903810
}
37913811

3812+
// Keep the first field-3 entry, strip the rest. `field3_kept`
3813+
// flips to `true` after the first encounter so subsequent ones
3814+
// fall through the strip branch.
37923815
let mut out = Vec::with_capacity(body.len());
3816+
let mut field3_kept = false;
37933817
for (field_number, seg_start, seg_end) in segments {
3794-
if field_number != 3 {
3818+
if field_number == 3 {
3819+
if !field3_kept {
3820+
field3_kept = true;
3821+
out.extend_from_slice(&body[seg_start..seg_end]);
3822+
}
3823+
// else: strip
3824+
} else {
37953825
out.extend_from_slice(&body[seg_start..seg_end]);
37963826
}
37973827
}
@@ -6091,10 +6121,32 @@ hello";
60916121
}
60926122

60936123
#[test]
6094-
fn sabr_strip_segment_fetch_drops_field3() {
6095-
// Segment-fetch shape: has both field-2 (range descriptor) and
6096-
// field-3 (quality-track selector). Strip should remove only the
6097-
// field-3 entries, leaving everything else intact.
6124+
fn sabr_strip_keeps_sole_field3_unchanged() {
6125+
// The #977 regression case from unacoder's testing: a
6126+
// segment-fetch body with exactly ONE field-3 entry (the
6127+
// single-track request the player sends at low/medium quality).
6128+
// The original "strip all field-3" rule turned this into a
6129+
// request with zero tracks selected, which googlevideo answered
6130+
// with an empty body — buffer never advanced, player retried
6131+
// 11+ times with `rn=` incrementing. Keep-first heuristic
6132+
// returns the body unchanged so the player gets a valid
6133+
// single-track response.
6134+
let mut body: Vec<u8> = Vec::new();
6135+
enc_length_delim(&mut body, 2, b"range-descriptor");
6136+
enc_length_delim(&mut body, 3, b"sole-quality-track");
6137+
enc_varint_field(&mut body, 4, 12345);
6138+
6139+
// No transform: 1 field-3 < 2-entry threshold.
6140+
assert_eq!(strip_sabr_quality_tracks(&body), body);
6141+
}
6142+
6143+
#[test]
6144+
fn sabr_strip_segment_fetch_keeps_first_field3_strips_rest() {
6145+
// Segment-fetch shape with TWO field-3 entries (multi-track
6146+
// bundling). The first field-3 is kept (preserves a single
6147+
// track on the wire so googlevideo has something to send); the
6148+
// second is stripped (caps the response under 10 MB). Other
6149+
// fields pass through unchanged.
60986150
let mut body: Vec<u8> = Vec::new();
60996151
enc_length_delim(&mut body, 2, b"range-descriptor-1");
61006152
enc_length_delim(&mut body, 3, b"quality-track-selector-1");
@@ -6104,7 +6156,9 @@ hello";
61046156

61056157
let mut expected: Vec<u8> = Vec::new();
61066158
enc_length_delim(&mut expected, 2, b"range-descriptor-1");
6159+
enc_length_delim(&mut expected, 3, b"quality-track-selector-1"); // KEPT
61076160
enc_length_delim(&mut expected, 2, b"range-descriptor-2");
6161+
// quality-track-selector-2: STRIPPED
61086162
enc_varint_field(&mut expected, 4, 12345);
61096163

61106164
assert_eq!(strip_sabr_quality_tracks(&body), expected);
@@ -6159,21 +6213,20 @@ hello";
61596213
}
61606214

61616215
#[test]
6162-
fn sabr_strip_truncated_tag_after_field3_preserves_tail() {
6163-
// field-2, field-3, then truncated. Strip should remove the
6164-
// field-3 entry (segment-fetch shape) and copy the truncated
6165-
// tail verbatim.
6216+
fn sabr_strip_truncated_tag_after_single_field3_is_noop() {
6217+
// field-2 + ONE field-3 (single-track request) + truncated tag.
6218+
// Under the keep-first heuristic, single field-3 is preserved
6219+
// → strip is a no-op → body returned verbatim (truncated tail
6220+
// included). The original behaviour was to strip the sole
6221+
// field-3 here, but that's exactly the regression #977
6222+
// identified.
61666223
let mut body: Vec<u8> = Vec::new();
61676224
enc_length_delim(&mut body, 2, b"range-desc");
61686225
enc_length_delim(&mut body, 3, b"quality-track");
6169-
// truncated tag
6170-
body.push(0x80);
6171-
6172-
let mut expected: Vec<u8> = Vec::new();
6173-
enc_length_delim(&mut expected, 2, b"range-desc");
6174-
expected.push(0x80);
6226+
body.push(0x80); // truncated tag
61756227

6176-
assert_eq!(strip_sabr_quality_tracks(&body), expected);
6228+
// No transform — single field-3 < 2-entry threshold.
6229+
assert_eq!(strip_sabr_quality_tracks(&body), body);
61776230
}
61786231

61796232
#[test]
@@ -6236,23 +6289,36 @@ hello";
62366289
}
62376290

62386291
#[test]
6239-
fn sabr_strip_truncated_fixed_width_preserves_segment_verbatim() {
6240-
// 64-bit fixed (wire type 1) — only 3 of 8 bytes present.
6241-
// Without a length-check this used to clamp via .min(n) and
6242-
// declare the field "complete." Now bails at the segment.
6292+
fn sabr_strip_truncated_fixed_width_with_single_field3_is_noop() {
6293+
// 64-bit fixed (wire type 1) — only 3 of 8 bytes present. The
6294+
// bail-on-truncated-payload behaviour is unchanged; what's
6295+
// different from the older "strip all field-3" version is
6296+
// that a SOLE field-3 is now kept (keep-first heuristic),
6297+
// so the body comes back unchanged.
62436298
let mut body: Vec<u8> = Vec::new();
62446299
enc_length_delim(&mut body, 2, b"r");
6245-
enc_length_delim(&mut body, 3, b"q");
6246-
// Tag = field 4, wire 1 = 0x21, then only 3 bytes follow.
6300+
enc_length_delim(&mut body, 3, b"q"); // sole field-3 → kept
6301+
body.push(0x21);
6302+
body.extend_from_slice(b"\x01\x02\x03");
6303+
6304+
assert_eq!(strip_sabr_quality_tracks(&body), body);
6305+
}
6306+
6307+
#[test]
6308+
fn sabr_strip_truncated_fixed_width_with_two_field3_strips_extras() {
6309+
// Same fixed-width-truncation shape, but with TWO field-3
6310+
// entries. Keep-first rule fires: first field-3 kept, second
6311+
// stripped, malformed tail verbatim.
6312+
let mut body: Vec<u8> = Vec::new();
6313+
enc_length_delim(&mut body, 2, b"r");
6314+
enc_length_delim(&mut body, 3, b"q1");
6315+
enc_length_delim(&mut body, 3, b"q2"); // stripped
62476316
body.push(0x21);
62486317
body.extend_from_slice(b"\x01\x02\x03");
62496318

6250-
// The malformed tail starts at the truncated fixed-width tag,
6251-
// so the field-2 / field-3 we did parse get emitted (segment-
6252-
// fetch shape, field-3 stripped), then the tail verbatim.
62536319
let mut expected: Vec<u8> = Vec::new();
62546320
enc_length_delim(&mut expected, 2, b"r");
6255-
// field-3 stripped here
6321+
enc_length_delim(&mut expected, 3, b"q1"); // kept
62566322
expected.push(0x21);
62576323
expected.extend_from_slice(b"\x01\x02\x03");
62586324
assert_eq!(strip_sabr_quality_tracks(&body), expected);
@@ -6262,44 +6328,56 @@ hello";
62626328

62636329
// ── SABR kill-switch runtime gate (#977) ─────────────────────────────
62646330

6265-
/// Build a known segment-fetch body (field-2 + field-3) that the
6266-
/// strip would actually shrink — used to prove the gate at runtime
6267-
/// rather than just the config-default round-trip.
6331+
/// Build a known segment-fetch body that the strip would actually
6332+
/// shrink — multi-track shape (field-2 + 2× field-3) so the
6333+
/// keep-first heuristic fires and removes the second field-3.
6334+
/// Used to prove the gate at runtime rather than just the
6335+
/// config-default round-trip.
62686336
fn segment_fetch_body() -> Vec<u8> {
62696337
let mut body: Vec<u8> = Vec::new();
62706338
enc_length_delim(&mut body, 2, b"range-descriptor");
6271-
enc_length_delim(&mut body, 3, b"quality-track-selector");
6339+
enc_length_delim(&mut body, 3, b"quality-track-selector-1");
6340+
enc_length_delim(&mut body, 3, b"quality-track-selector-2");
62726341
body
62736342
}
62746343

62756344
#[test]
6276-
fn sabr_strip_on_strips_segment_fetch_body_via_relay_gate() {
6277-
// sabr_strip = true (default): segment-fetch POST to a real
6278-
// googlevideo URL is stripped. This protects the main behaviour
6279-
// the kill-switch gates — if a future refactor accidentally
6280-
// drops the `self.sabr_strip` check from `relay()`, the strip
6281-
// would still apply on `true` and the test would pass; if the
6282-
// refactor accidentally INVERTS the check, this test fails
6283-
// because no bytes are removed.
6345+
fn sabr_strip_on_strips_extra_field3_entries_via_relay_gate() {
6346+
// sabr_strip = true (default), multi-track segment-fetch body
6347+
// (the keep-first heuristic threshold). The first field-3
6348+
// entry must survive (so the player still has a track selected
6349+
// — the #977 lesson); subsequent field-3 entries must be gone
6350+
// (the 10 MB-blowup fix). Protects the main behaviour the
6351+
// kill-switch gates: if a future refactor drops the
6352+
// `self.sabr_strip` check, the strip still applies on `true`
6353+
// and the test passes; if the refactor inverts the check, this
6354+
// fails because no bytes are removed.
62846355
let fronter = fronter_for_test_with(false, true);
62856356
let body = segment_fetch_body();
62866357
let result = fronter.maybe_strip_sabr_body(
62876358
"POST",
62886359
"https://rrx---sn-xxx.googlevideo.com/videoplayback?id=42",
62896360
&body,
62906361
);
6291-
let stripped = result.expect("sabr_strip=true must strip a segment-fetch body");
6362+
let stripped = result.expect("sabr_strip=true must strip a multi-track body");
62926363
assert!(
62936364
stripped.len() < body.len(),
62946365
"strip must remove at least one byte ({} -> {})",
62956366
body.len(),
62966367
stripped.len(),
62976368
);
6298-
// And the field-3 entry specifically is what was removed.
6369+
// First field-3 kept (single-track preservation), second stripped.
6370+
assert!(
6371+
stripped
6372+
.windows(b"quality-track-selector-1".len())
6373+
.any(|w| w == b"quality-track-selector-1"),
6374+
"first field-3 payload (quality-track-selector-1) must SURVIVE the strip",
6375+
);
62996376
assert!(
6300-
!stripped.windows(b"quality-track-selector".len())
6301-
.any(|w| w == b"quality-track-selector"),
6302-
"field-3 payload must be gone from stripped body",
6377+
!stripped
6378+
.windows(b"quality-track-selector-2".len())
6379+
.any(|w| w == b"quality-track-selector-2"),
6380+
"subsequent field-3 payload (quality-track-selector-2) must be STRIPPED",
63036381
);
63046382
}
63056383

@@ -6422,21 +6500,17 @@ hello";
64226500
}
64236501

64246502
#[test]
6425-
fn sabr_strip_truncated_varint_payload_preserves_segment_verbatim() {
6426-
// Wire type 0 (varint) with a continuation byte and no terminator.
6503+
fn sabr_strip_truncated_varint_payload_with_single_field3_is_noop() {
6504+
// Wire type 0 (varint) with a continuation byte and no
6505+
// terminator. Sole field-3 → kept under keep-first heuristic
6506+
// → body returned verbatim (truncated tail included).
64276507
let mut body: Vec<u8> = Vec::new();
64286508
enc_length_delim(&mut body, 2, b"r");
6429-
enc_length_delim(&mut body, 3, b"q");
6430-
// Tag = field 5, wire 0 = 0x28, then a continuation byte that
6431-
// never terminates.
6509+
enc_length_delim(&mut body, 3, b"q"); // sole field-3 → kept
64326510
body.push(0x28);
64336511
body.push(0x80); // continuation, then EOF
64346512

6435-
let mut expected: Vec<u8> = Vec::new();
6436-
enc_length_delim(&mut expected, 2, b"r");
6437-
expected.push(0x28);
6438-
expected.push(0x80);
6439-
assert_eq!(strip_sabr_quality_tracks(&body), expected);
6513+
assert_eq!(strip_sabr_quality_tracks(&body), body);
64406514
}
64416515

64426516
// ── StatsSnapshot::fmt_line + to_json (forwarder fields) ────────────

0 commit comments

Comments
 (0)