Fix Safari SABR and WebABR

This commit is contained in:
Acobat12 2026-10-06 11:59:48 +02:00
parent 16348de4ac
commit 7fe59bc833
6 changed files with 1823 additions and 124 deletions

View file

@ -761,10 +761,96 @@ async function handleTopRequest(
audioDownloadType,
host: targetWindow.location.hostname,
});
// Run WebABR/SABR directly in the real current YouTube page context.
// The hidden /embed player is retained only for the legacy MSE fallback.
if (audioDownloadType === AudioDownloadType.WEB_ABR) {
const controller = new AbortController();
let settled = false;
const abort = (abortEvent: MessageEvent<MseMessage>) => {
const data = abortEvent.data;
if (
abortEvent.source === source &&
abortEvent.origin === event.origin &&
data.messageId === message.messageId &&
data.messageType === MESSAGE_TYPE &&
data.messageDirection === "request" &&
data.isAborted
) {
controller.abort(data.payload);
}
};
targetWindow.addEventListener("message", abort);
const postProgress = () => {
if (settled) return;
postResponse(source, event.origin, {
...message,
messageDirection: "response",
payload: undefined,
isProgress: true,
});
};
const heartbeat = setInterval(postProgress, 30_000);
postProgress();
debug.log("Audio downloader. direct WebABR/SABR request started", {
videoId,
messageId: message.messageId,
host: targetWindow.location.hostname,
});
try {
const chunks = getWebAbrAudioChunks(
targetWindow,
videoId,
controller.signal,
getSourceLanguage(message),
);
for await (const chunk of chunks) {
postResponse(source, event.origin, {
...message,
messageDirection: "response",
payload: chunk,
});
}
settled = true;
debug.log("Audio downloader. direct WebABR/SABR stream finished", {
videoId,
messageId: message.messageId,
});
postResponse(source, event.origin, {
...message,
messageDirection: "response",
payload: undefined,
isStreamFinished: true,
});
} catch (error) {
settled = true;
debug.error("Audio downloader. direct WebABR/SABR request failed", {
videoId,
messageId: message.messageId,
error: error instanceof Error ? error.message : String(error),
});
postResponse(source, event.origin, {
...message,
messageDirection: "response",
payload: undefined,
error: error instanceof Error ? error.message : String(error),
isAborted: controller.signal.aborted || isAbortError(error),
});
} finally {
clearInterval(heartbeat);
targetWindow.removeEventListener("message", abort);
}
return;
}
// MSE fallback keeps the original auxiliary /embed player path.
const iframe = targetWindow.document.createElement("iframe");
// display:none iframes have no layout box, and YouTube defers media
// loading for non-rendered players (no src is ever assigned). Keep the
// frame rendered but invisible: 2x2px, off the visual path.
iframe.style.cssText =
"position:fixed;right:0;bottom:0;width:2px;height:2px;border:0;" +
"padding:0;margin:0;opacity:0;visibility:hidden;pointer-events:none;";

View file

@ -0,0 +1,293 @@
import debug from "../../utils/debug";
const REQUEST_EVENT = "vot:safari-page-po-token:request";
const RESPONSE_EVENT = "vot:safari-page-po-token:response";
const BRIDGE_MARKER = "data-vot-safari-po-token-bridge";
const TOP_REQUEST_MESSAGE = "vot:safari-page-po-token:top-request";
const TOP_RESPONSE_MESSAGE = "vot:safari-page-po-token:top-response";
let topBrokerInstalled = false;
type TopPoTokenMessage = {
type?: string;
requestId?: string;
binding?: string;
token?: string;
error?: string;
};
type PoTokenResponse = {
requestId?: string;
token?: string;
error?: string;
};
function isSafariBrowser(): boolean {
const ua = navigator.userAgent;
return (
/Safari\//.test(ua) &&
!/(?:Chrome|Chromium|CriOS|Edg|OPR|Firefox|FxiOS)\//.test(ua)
);
}
function installSafariPageBridge(): boolean {
if (!isSafariBrowser()) return false;
if (document.documentElement.hasAttribute(BRIDGE_MARKER)) return true;
const script = document.createElement("script");
const nonceSource =
document.querySelector<HTMLScriptElement>("script[nonce]");
if (nonceSource?.nonce) script.nonce = nonceSource.nonce;
// This source is intentionally self-contained. The injected script executes
// in YouTube's real page realm, where bevasrsg/havuokmhhs-* is available.
script.textContent = `(() => {
const REQUEST_EVENT = ${JSON.stringify(REQUEST_EVENT)};
const RESPONSE_EVENT = ${JSON.stringify(RESPONSE_EVENT)};
const MARKER = ${JSON.stringify(BRIDGE_MARKER)};
if (document.documentElement.hasAttribute(MARKER)) return;
document.documentElement.setAttribute(MARKER, "1");
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
async function mint(binding) {
const keys = Object.getOwnPropertyNames(globalThis).filter(
(key) => key === "bevasrsg" || key.startsWith("havuokmhhs-"),
);
if (!keys.length) {
throw new Error("YouTube PO token provider is unavailable in page realm");
}
for (const key of keys) {
let bevasrs;
try {
bevasrs = globalThis[key]?.bevasrs;
} catch {
continue;
}
const wpc = bevasrs?.wpc;
if (typeof wpc !== "function") continue;
for (let attempt = 0; attempt < 10; attempt++) {
try {
const minter = await wpc.call(bevasrs);
const token = await minter?.mws?.({
c: binding,
mc: false,
me: false,
});
if (typeof token === "string" && token) return token;
} catch (error) {
const message =
error instanceof Error ? error.message : String(error);
if (!message.includes("SDF:notready")) break;
}
await delay(500);
}
}
throw new Error("YouTube PO token mint failed in page realm");
}
addEventListener(REQUEST_EVENT, async (event) => {
const detail = event?.detail;
const requestId = detail?.requestId;
const binding = detail?.binding;
if (typeof requestId !== "string" || typeof binding !== "string" || !binding) {
return;
}
try {
const token = await mint(binding);
dispatchEvent(new CustomEvent(RESPONSE_EVENT, {
detail: { requestId, token },
}));
} catch (error) {
dispatchEvent(new CustomEvent(RESPONSE_EVENT, {
detail: {
requestId,
error: error instanceof Error ? error.message : String(error),
},
}));
}
});
})();`;
(document.documentElement || document.head).append(script);
script.remove();
return document.documentElement.hasAttribute(BRIDGE_MARKER);
}
async function requestSafariPagePoTokenLocal(
binding: string,
signal: AbortSignal,
): Promise<string | undefined> {
if (!isSafariBrowser()) return undefined;
signal.throwIfAborted();
if (!installSafariPageBridge()) {
debug.error("[VOT][PO_TOKEN] Safari page bridge injection failed");
return undefined;
}
const requestId = crypto.randomUUID();
debug.log("[VOT][PO_TOKEN] trying Safari page-realm bridge", {
bindingLength: binding.length,
});
return await new Promise<string | undefined>((resolve, reject) => {
let settled = false;
const timeout = setTimeout(() => finish(undefined), 8000);
const cleanup = () => {
clearTimeout(timeout);
removeEventListener(RESPONSE_EVENT, onResponse as EventListener);
signal.removeEventListener("abort", onAbort);
};
const finish = (value: string | undefined) => {
if (settled) return;
settled = true;
cleanup();
resolve(value);
};
const onAbort = () => {
if (settled) return;
settled = true;
cleanup();
reject(signal.reason);
};
const onResponse = (event: Event) => {
const detail = (event as CustomEvent<PoTokenResponse>).detail;
if (detail?.requestId !== requestId) return;
if (typeof detail.token === "string" && detail.token) {
finish(detail.token);
return;
}
if (detail.error) {
debug.error("[VOT][PO_TOKEN] Safari page-realm mint failed", {
message: detail.error,
});
}
finish(undefined);
};
addEventListener(RESPONSE_EVENT, onResponse as EventListener);
signal.addEventListener("abort", onAbort, { once: true });
dispatchEvent(
new CustomEvent(REQUEST_EVENT, {
detail: { requestId, binding },
}),
);
});
}
/** Install a same-origin token broker in the top YouTube document.
* WebABR may keep running in its legacy /embed player; only PO-token minting
* is delegated to the top player document where Safari exposes the provider.
*/
export function initSafariPagePoTokenBroker(): void {
if (!isSafariBrowser() || window !== window.top || topBrokerInstalled) return;
topBrokerInstalled = true;
addEventListener("message", (event: MessageEvent<TopPoTokenMessage>) => {
const data = event.data;
if (event.origin !== location.origin) return;
if (data?.type !== TOP_REQUEST_MESSAGE) return;
if (
typeof data.requestId !== "string" ||
typeof data.binding !== "string" ||
!data.binding
)
return;
const source = event.source as Window | null;
if (!source) return;
const controller = new AbortController();
void requestSafariPagePoTokenLocal(data.binding, controller.signal)
.then((token) => {
source.postMessage(
{ type: TOP_RESPONSE_MESSAGE, requestId: data.requestId, token },
event.origin,
);
})
.catch((error) => {
source.postMessage(
{
type: TOP_RESPONSE_MESSAGE,
requestId: data.requestId,
error: error instanceof Error ? error.message : String(error),
},
event.origin,
);
});
});
debug.log("[VOT][PO_TOKEN] top player token broker installed");
}
async function requestTopSafariPagePoToken(
binding: string,
signal: AbortSignal,
): Promise<string | undefined> {
const requestId = crypto.randomUUID();
debug.log("[VOT][PO_TOKEN] requesting token from top YouTube player", {
bindingLength: binding.length,
});
return await new Promise<string | undefined>((resolve, reject) => {
let settled = false;
const timeout = setTimeout(() => finish(undefined), 8000);
const cleanup = () => {
clearTimeout(timeout);
removeEventListener("message", onMessage);
signal.removeEventListener("abort", onAbort);
};
const finish = (value: string | undefined) => {
if (settled) return;
settled = true;
cleanup();
resolve(value);
};
const onAbort = () => {
if (settled) return;
settled = true;
cleanup();
reject(signal.reason);
};
const onMessage = (event: MessageEvent<TopPoTokenMessage>) => {
if (event.source !== window.top || event.origin !== location.origin)
return;
const data = event.data;
if (data?.type !== TOP_RESPONSE_MESSAGE || data.requestId !== requestId)
return;
if (typeof data.token === "string" && data.token) {
debug.log("[VOT][PO_TOKEN] token received from top YouTube player", {
tokenLength: data.token.length,
});
finish(data.token);
} else {
if (data.error)
debug.error("[VOT][PO_TOKEN] top player token mint failed", {
message: data.error,
});
finish(undefined);
}
};
addEventListener("message", onMessage);
signal.addEventListener("abort", onAbort, { once: true });
window.top?.postMessage(
{ type: TOP_REQUEST_MESSAGE, requestId, binding },
location.origin,
);
});
}
export async function requestSafariPagePoToken(
binding: string,
signal: AbortSignal,
): Promise<string | undefined> {
if (!isSafariBrowser()) return undefined;
signal.throwIfAborted();
if (window !== window.top) {
return requestTopSafariPagePoToken(binding, signal);
}
return requestSafariPagePoTokenLocal(binding, signal);
}

View file

@ -167,83 +167,14 @@ export function buildMediaRanges(
return ranges;
}
export async function mintPagePoToken(
pageWindow: WebAbrWindow,
binding: string,
signal: AbortSignal,
): Promise<string | undefined> {
const realms = new Set<WebAbrWindow>([pageWindow]);
try {
realms.add(pageWindow.parent as WebAbrWindow);
realms.add(pageWindow.top as WebAbrWindow);
} catch {
// Cross-origin access is denied.
}
for (const realm of realms) {
let keys: string[];
try {
keys = Object.getOwnPropertyNames(realm).filter(
(key) => key === "bevasrsg" || key.startsWith("havuokmhhs-"),
);
} catch {
continue;
}
for (const key of keys) {
let bevasrs: { wpc?: unknown } | undefined;
try {
bevasrs = (
(realm as unknown as Record<string, unknown>)[key] as {
bevasrs?: { wpc?: unknown };
}
)?.bevasrs;
} catch {
continue;
}
const wpc = bevasrs?.wpc;
if (typeof wpc !== "function") continue;
for (let attempt = 0; attempt < 10; attempt++) {
if (signal.aborted) throw signal.reason;
try {
const minter = await wpc.call(bevasrs);
const token = await minter?.mws?.({
c: binding,
mc: false,
me: false,
});
if (typeof token === "string" && token) return token;
} catch (error) {
if (!String(error).includes("SDF:notready")) break;
}
await createAbortableDelay(500, signal);
}
}
}
}
// PO-token handling belongs to the dedicated YouTube player layer.
// Re-export it here to keep existing SABR imports/API compatible.
export {
mintPagePoToken,
selectGvsPoTokenBinding,
} from "./youtubePlayer";
export function selectGvsPoTokenBinding(
videoId: string,
options: {
loggedIn: boolean;
dataSyncId: unknown;
visitorData: unknown;
experimentFlags: string[];
},
): { kind: "video" | "datasync" | "visitor"; value: string } | undefined {
if (
options.experimentFlags.some(
(flags) =>
new URLSearchParams(flags)
.getAll("html5_generate_content_po_token")
.at(-1) === "true",
)
) {
return { kind: "video", value: videoId };
}
// Authenticated GVS uses the full datasync ID, including the || separator.
const value = options.loggedIn ? options.dataSyncId : options.visitorData;
if (typeof value !== "string" || !value) return;
return { kind: options.loggedIn ? "datasync" : "visitor", value };
}
import { mintPagePoToken, selectGvsPoTokenBinding } from "./youtubePlayer";
export function getConfigValue(config: YouTubeConfig, key: string): unknown {
return config.get?.(key) ?? config.data_?.[key];

File diff suppressed because it is too large Load diff

View file

@ -1,54 +1,15 @@
import type { SabrFormat } from "googlevideo/shared-types";
import { createAbortableDelay } from "../../utils/abort";
import debug from "../../utils/debug";
import {
getYoutubeAudioFormatLanguage as getAudioFormatLanguage,
normalizeAudioLanguageTag as normalizeAudioLanguage,
} from "../utils";
import {
audioLanguageMatches,
getAudioFormatLanguage,
getTopPageWindow,
isDrcAudioFormat,
normalizeAudioLanguage,
type WebAbrWindow,
type WebEmbeddedFormat,
} from "./webAbr";
import { getTopPageWindow } from "./youtubeSabrPlayer";
export function splitTopLevelProto(
bytes: Uint8Array,
): Array<{ field: number; start: number; end: number }> {
const out: Array<{ field: number; start: number; end: number }> = [];
const readVarint = (offset: number) => {
let value = 0;
let shift = 0;
let index = offset;
while (index < bytes.length && shift <= 35) {
const byte = bytes[index++];
value += (byte & 0x7f) * 2 ** shift;
if ((byte & 0x80) === 0) return { value, next: index };
shift += 7;
}
throw new Error("invalid protobuf varint");
};
let offset = 0;
while (offset < bytes.length) {
const start = offset;
const tag = readVarint(offset);
offset = tag.next;
const field = Math.floor(tag.value / 8);
const wire = tag.value & 7;
if (wire === 0) offset = readVarint(offset).next;
else if (wire === 1) offset += 8;
else if (wire === 2) {
const length = readVarint(offset);
offset = length.next + length.value;
} else if (wire === 5) offset += 4;
else throw new Error(`unsupported protobuf wire ${wire}`);
if (offset > bytes.length) throw new Error("truncated protobuf field");
out.push({ field, start, end: offset });
}
return out;
}
} from "./youtubePlayer";
export function toSabrFormat(
format: WebEmbeddedFormat,
@ -172,7 +133,7 @@ export function setGeneratedSabrAudioTrackId(
const payloadChunks = fields
.filter((field) => field.field !== 69)
.map((field) => payload.slice(field.start, field.end));
payloadChunks.push(new Uint8Array(trackField));
payloadChunks.push(trackField);
const patchedPayload = concatBytes(payloadChunks);
chunks.push(

View file

@ -181,6 +181,8 @@
"cloudflare-dns.com",
"porntn.com",
"youtube.com",
"googlevideo.com"
"googlevideo.com",
"vtrans.eu.cc",
"vot-worker.vtrans.eu.cc"
]
}