diff --git a/src/hooks/json-stringify.ts b/src/hooks/json-stringify.ts index 1919aec5..bce3705a 100644 --- a/src/hooks/json-stringify.ts +++ b/src/hooks/json-stringify.ts @@ -18,10 +18,33 @@ function stringify( // TODO: add below to a dump-level logger // console.debug('JSON.stringify', value, replacer, space); - // @ts-expect-error TS doesn't allow optional chaining on `unknown`. See: https://github.com/microsoft/TypeScript/issues/37700 - const ctx = value?.playbackContext?.contentPlaybackContext as unknown; - if (!isPrimitive(ctx)) { - (ctx as Record).isInlinePlaybackNoAd = true; + const holder = value as Record; + const pbCtx = holder.playbackContext as Record | undefined; + const ctx = pbCtx?.contentPlaybackContext as + | Record + | undefined; + + if (!isPrimitive(ctx) && ctx!.isInlinePlaybackNoAd !== true) { + // Setting `isInlinePlaybackNoAd` tells InnerTube not to serve ads, which + // avoids the server-side SABR "backoff" that otherwise stalls playback + // after a few seconds. YouTube has shipped a "locker" script that defines + // this property as non-writable/non-configurable via Object.defineProperty, + // so a direct assignment (`ctx.isInlinePlaybackNoAd = true`) silently fails. + // + // Instead of mutating YouTube's object in place, rebuild the holder chain + // with fresh plain objects. `JSON.stringify` only serializes own enumerable + // properties, so spreading reproduces exactly what would be serialized while + // dropping any locked property descriptors, letting our flag stick. + value = { + ...holder, + playbackContext: { + ...pbCtx, + contentPlaybackContext: { + ...ctx, + isInlinePlaybackNoAd: true + } + } + }; console.info(`[JSON.stringify] Set isInlinePlaybackNoAd`); } }