Files
YouTube.js/examples/browser/web/src/main.ts
2025-01-16 18:08:04 -03:00

371 lines
12 KiB
TypeScript

import { BG } from 'bgutils-js';
import GoogleVideo, { PART, Protos } from 'googlevideo';
import { Innertube, ProtoUtils, UniversalCache, Utils } from '../../../..';
import shaka from 'shaka-player/dist/shaka-player.ui';
import 'shaka-player/dist/controls.css';
function fetchFn(input: RequestInfo | URL, init?: RequestInit) {
const url = typeof input === 'string'
? new URL(input)
: input instanceof URL
? input
: new URL(input.url);
// Transform the url for use with our proxy.
url.searchParams.set('__host', url.host);
url.host = 'localhost:8080';
url.protocol = 'http';
const headers = init?.headers
? new Headers(init.headers)
: input instanceof Request
? input.headers
: new Headers();
// Now serialize the headers.
url.searchParams.set('__headers', JSON.stringify([ ...headers ]));
// Copy over the request.
const request = new Request(
url,
input instanceof Request ? input : undefined
);
headers.delete('user-agent');
return fetch(request, init ? {
...init,
headers
} : {
headers
});
}
const title = document.getElementById('title') as HTMLHeadingElement;
const description = document.getElementById('description') as HTMLDivElement;
const metadata = document.getElementById('metadata') as HTMLDivElement;
const loader = document.getElementById('loader') as HTMLDivElement;
const form = document.querySelector('form') as HTMLFormElement;
async function getPo(identifier: string): Promise<string | undefined> {
const requestKey = 'O43z0dpjhgX20SCx4KAo';
const bgConfig = {
fetch: (input: string | URL | globalThis.Request, init?: RequestInit) => fetch(input, init),
globalObj: window,
requestKey,
identifier
};
const bgChallenge = await BG.Challenge.create(bgConfig);
if (!bgChallenge)
throw new Error('Could not get challenge');
const interpreterJavascript = bgChallenge.interpreterJavascript.privateDoNotAccessOrElseSafeScriptWrappedValue;
if (interpreterJavascript) {
new Function(interpreterJavascript)();
} else throw new Error('Could not load VM');
const poTokenResult = await BG.PoToken.generate({
program: bgChallenge.program,
globalName: bgChallenge.globalName,
bgConfig
});
return poTokenResult.poToken;
}
async function main() {
let poToken: string | undefined;
const visitorData = ProtoUtils.encodeVisitorData(Utils.generateRandomString(11), Math.floor(Date.now() / 1000));
// Immediately mint a cold start token so we can start playback without delays.
const coldStartToken = BG.PoToken.generatePlaceholder(visitorData);
getPo(visitorData).then((webPo) => poToken = webPo);
const yt = await Innertube.create({
po_token: poToken || coldStartToken,
visitor_data: visitorData,
fetch: fetchFn,
generate_session_locally: true,
cache: new UniversalCache(false)
});
form.animate({ opacity: [ 0, 1 ] }, { duration: 300, easing: 'ease-in-out' });
form.style.display = 'block';
showUI({ hidePlayer: true });
let player: shaka.Player | undefined;
let ui: shaka.ui.Overlay | undefined;
form.addEventListener('submit', async (e) => {
e.preventDefault();
if (player) {
await player.destroy();
}
hideUI();
let videoId;
const videoIdOrURL = document.querySelector<HTMLInputElement>('input[type=text]')?.value;
if (!videoIdOrURL) {
title.textContent = 'No video id or URL provided';
showUI({ hidePlayer: true });
return;
}
try {
if (videoIdOrURL.match(/(http|https):\/\/([\w_-]+(?:\.[\w_-]+)+)([\w.,@?^=%&:/~+#-]*[\w@?^=%&/~+#-])/)) {
const endpoint = await yt.resolveURL(videoIdOrURL);
if (!endpoint.payload.videoId) {
title.textContent = 'Could not resolve URL';
showUI({ hidePlayer: true });
return;
}
videoId = endpoint.payload.videoId;
} else {
videoId = videoIdOrURL;
}
const info = await yt.getInfo(videoId);
title.textContent = info.basic_info.title || null;
description.innerHTML = info.secondary_info?.description.toHTML() || '';
title.textContent = info.basic_info.title || null;
document.title = info.basic_info.title || '';
metadata.innerHTML = '';
metadata.innerHTML += `<div id="metadata-item">${info.primary_info?.published.toHTML()}</div>`;
metadata.innerHTML += `<div id="metadata-item">${info.primary_info?.view_count?.short_view_count?.toHTML()}</div>`;
metadata.innerHTML += `<div id="metadata-item">${info.basic_info.like_count} likes</div>`;
showUI({ hidePlayer: false });
const dash = await info.toDash();
const uri = `data:application/dash+xml;charset=utf-8;base64,${btoa(dash)}`;
if (player) {
await player.destroy();
player = undefined;
}
if (ui) {
await ui.destroy();
ui = undefined;
}
const videoEl = document.getElementById('videoel') as HTMLVideoElement;
const shakaContainer = document.getElementById('shaka-container') as HTMLDivElement;
shakaContainer
.querySelectorAll('div')
.forEach((node) => node.remove());
shaka.polyfill.installAll();
if (shaka.Player.isBrowserSupported()) {
videoEl.poster = info.basic_info.thumbnail![0].url;
player = new shaka.Player();
await player.attach(videoEl);
ui = new shaka.ui.Overlay(player, shakaContainer, videoEl);
const config = {
seekBarColors: {
base: 'rgba(255,255,255,.2)',
buffered: 'rgba(255,255,255,.4)',
played: 'rgb(255,0,0)'
},
fadeDelay: 0
};
ui.configure(config);
const overflowMenuButton = document.querySelector('.shaka-overflow-menu-button');
if (overflowMenuButton) {
overflowMenuButton.innerHTML = 'settings';
}
const backToOverflowButton = document.querySelector('.shaka-back-to-overflow-button .material-icons-round');
if (backToOverflowButton) {
backToOverflowButton.innerHTML = 'arrow_back_ios_new';
}
player.configure({
streaming: {
bufferingGoal: (info.page[0].player_config?.media_common_config.dynamic_readahead_config.max_read_ahead_media_time_ms || 0) / 1000,
rebufferingGoal: (info.page[0].player_config?.media_common_config.dynamic_readahead_config.read_ahead_growth_rate_ms || 0) / 1000,
bufferBehind: 300,
autoLowLatencyMode: true
},
abr: {
enabled: true,
restrictions: {
maxBandwidth: Number(info.page[0].player_config?.stream_selection_config.max_bitrate)
}
}
});
const networkingEngine = player.getNetworkingEngine();
if (!networkingEngine) return;
networkingEngine.registerRequestFilter(async (type, request) => {
const uri = request.uris[0];
const url = new URL(uri);
const headers = request.headers;
// For local development.
if ((url.host.endsWith('.googlevideo.com') || url.href.includes('drm'))) {
url.searchParams.set('__host', url.host);
url.host = 'localhost';
url.port = '8080';
url.protocol = 'http';
}
if (type === shaka.net.NetworkingEngine.RequestType.SEGMENT) {
if (url.pathname.includes('videoplayback')) {
if (headers.Range) {
url.searchParams.set('range', headers.Range.split('=')[1]);
url.searchParams.set('ump', '1');
url.searchParams.set('srfvp', '1');
url.searchParams.set('pot', (poToken ?? coldStartToken) ?? '');
request.headers = {};
delete headers.Range;
}
}
request.method = 'POST';
request.body = new Uint8Array([ 120, 0 ]);
}
request.uris[0] = url.toString();
});
const RequestType = shaka.net.NetworkingEngine.RequestType;
networkingEngine.registerResponseFilter(async (type, response) => {
let mediaData = new Uint8Array(0);
const handleRedirect = async (redirectData: Protos.SabrRedirect) => {
const redirectRequest = shaka.net.NetworkingEngine.makeRequest([ redirectData.url! ], player!.getConfiguration().streaming.retryParameters);
const requestOperation = player!.getNetworkingEngine()!.request(type, redirectRequest);
const redirectResponse = await requestOperation.promise;
response.data = redirectResponse.data;
response.headers = redirectResponse.headers;
response.uri = redirectResponse.uri;
};
const handleMediaData = async (data: Uint8Array) => {
const combinedLength = mediaData.length + data.length;
const tempMediaData = new Uint8Array(combinedLength);
tempMediaData.set(mediaData);
tempMediaData.set(data, mediaData.length);
mediaData = tempMediaData;
};
if (type == RequestType.SEGMENT) {
const googUmp = new GoogleVideo.UMP(new GoogleVideo.ChunkedDataBuffer([ new Uint8Array(response.data as ArrayBuffer) ]));
let redirect: Protos.SabrRedirect | undefined;
googUmp.parse((part) => {
try {
const data = part.data.chunks[0];
switch (part.type) {
case PART.MEDIA_HEADER: {
const mediaHeader = Protos.MediaHeader.decode(data);
console.info('[MediaHeader]:', mediaHeader);
break;
}
case PART.MEDIA: {
handleMediaData(part.data.split(1).remainingBuffer.chunks[0]);
break;
}
case PART.SABR_REDIRECT: {
redirect = Protos.SabrRedirect.decode(data);
console.info('[SABRRedirect]:', redirect);
break;
}
case PART.STREAM_PROTECTION_STATUS: {
const streamProtectionStatus = Protos.StreamProtectionStatus.decode(data);
switch (streamProtectionStatus.status) {
case 1:
console.info('[StreamProtectionStatus]: Ok');
break;
case 2:
console.error('[StreamProtectionStatus]: Attestation pending');
break;
case 3:
console.error('[StreamProtectionStatus]: Attestation required');
break;
default:
break;
}
break;
}
}
} catch (error) {
console.error('An error occurred while processing the part:', error);
}
});
if (redirect)
return handleRedirect(redirect);
if (mediaData.length)
response.data = mediaData;
}
});
try {
await player.load(uri);
} catch (e) {
console.error('Could not load manifest', e);
}
} else {
console.error('Browser not supported!');
}
} catch (error) {
title.textContent = 'An error occurred (see console)';
showUI({ hidePlayer: true });
console.error(error);
}
});
}
function showUI(args: { hidePlayer?: boolean } = {
hidePlayer: true
}) {
const ytplayer = document.getElementById('shaka-container') as HTMLDivElement;
ytplayer.style.display = args.hidePlayer ? 'none' : 'block';
const video_container = document.getElementById('video-container') as HTMLDivElement;
video_container.animate({ opacity: [ 0, 1 ] }, { duration: 300, easing: 'ease-in-out' });
video_container.style.display = 'block';
loader.style.display = 'none';
}
function hideUI() {
const video_container = document.getElementById('video-container') as HTMLDivElement;
video_container.style.display = 'none';
loader.style.display = 'block';
}
main().catch(console.error);