diff --git a/materialious/package-lock.json b/materialious/package-lock.json index aa8475f5..6e9be708 100644 --- a/materialious/package-lock.json +++ b/materialious/package-lock.json @@ -25,6 +25,7 @@ "capacitor-nodejs": "https://github.com/EdenwareApps/Capacitor-NodeJS/releases/download/v1.0.0-beta.7/capacitor6-nodejs.tgz", "dashjs": "^4.7.4", "fuse.js": "^7.0.0", + "googlevideo": "^2.0.0", "he": "^1.2.0", "human-number": "^2.0.4", "iso-3166": "^4.3.0", @@ -7602,6 +7603,18 @@ "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", "license": "MIT" }, + "node_modules/googlevideo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/googlevideo/-/googlevideo-2.0.0.tgz", + "integrity": "sha512-OVlNWZ07TPIelaEII6mH9od+Cxljl7P4AzhEYVNN5d4FhFT9L5otpcLtgvraTE9u69KfVVw+L4pVeczArcD33w==", + "funding": [ + "https://github.com/sponsors/LuanRT" + ], + "license": "MIT", + "dependencies": { + "@bufbuild/protobuf": "^2.0.0" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", diff --git a/materialious/package.json b/materialious/package.json index 5f20f9e4..468d3a2a 100644 --- a/materialious/package.json +++ b/materialious/package.json @@ -58,6 +58,7 @@ "capacitor-nodejs": "https://github.com/EdenwareApps/Capacitor-NodeJS/releases/download/v1.0.0-beta.7/capacitor6-nodejs.tgz", "dashjs": "^4.7.4", "fuse.js": "^7.0.0", + "googlevideo": "^2.0.0", "he": "^1.2.0", "human-number": "^2.0.4", "iso-3166": "^4.3.0", @@ -75,4 +76,4 @@ "terser": "^5.34.1", "youtubei.js": "^12.2.0" } -} \ No newline at end of file +} diff --git a/materialious/src/lib/api/model.ts b/materialious/src/lib/api/model.ts index 22053502..a6c58151 100644 --- a/materialious/src/lib/api/model.ts +++ b/materialious/src/lib/api/model.ts @@ -1,3 +1,5 @@ +import type { YT } from "youtubei.js"; + export interface Image { url: string; width: number; @@ -99,6 +101,7 @@ export interface VideoPlay extends Video { captions: Captions[]; storyboards?: StoryBoard[]; fallbackPatch?: 'youtubejs' | 'piped'; + youtubeJsPatchInfo?: YT.VideoInfo; } export interface StoryBoard { diff --git a/materialious/src/lib/components/Player.svelte b/materialious/src/lib/components/Player.svelte index cdc7648c..e27e119a 100644 --- a/materialious/src/lib/components/Player.svelte +++ b/materialious/src/lib/components/Player.svelte @@ -7,7 +7,8 @@ import { ScreenOrientation, type ScreenOrientationResult } from '@capacitor/screen-orientation'; import { StatusBar, Style } from '@capacitor/status-bar'; import { NavigationBar } from '@hugotomazi/capacitor-navigation-bar'; - import { error, type Page } from '@sveltejs/kit'; + import { type Page } from '@sveltejs/kit'; + import GoogleVideo, { Protos } from 'googlevideo'; import 'shaka-player/dist/controls.css'; import shaka from 'shaka-player/dist/shaka-player.ui'; import { SponsorBlock, type Category, type Segment } from 'sponsorblock-api'; @@ -73,7 +74,7 @@ onMount(async () => { shaka.polyfill.installAll(); if (!shaka.Player.isBrowserSupported()) { - error(400, 'Shaka not supported on your browser'); + return; } player = new shaka.Player(); @@ -81,6 +82,142 @@ await player.attach(playerElement); + if (data.video.youtubeJsPatchInfo) { + player.configure({ + streaming: { + bufferingGoal: + (data.video.youtubeJsPatchInfo.page[0].player_config?.media_common_config + .dynamic_readahead_config.max_read_ahead_media_time_ms || 0) / 1000, + rebufferingGoal: + (data.video.youtubeJsPatchInfo.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( + data.video.youtubeJsPatchInfo.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; + + 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', get(poTokenCacheStore).toString()); + } + } + + request.method = 'POST'; + request.body = new Uint8Array([120, 0]); + } + + if (Capacitor.getPlatform() === 'android' && data.video.fallbackPatch === 'youtubejs') { + request.uris[0] = localProxy + url.toString(); + } else { + 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 20: { + const mediaHeader = Protos.MediaHeader.decode(data); + console.info('[MediaHeader]:', mediaHeader); + break; + } + case 21: { + handleMediaData(part.data.split(1).remainingBuffer.chunks[0]); + break; + } + case 43: { + redirect = Protos.SabrRedirect.decode(data); + console.info('[SABRRedirect]:', redirect); + break; + } + case 58: { + 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; + } + }); + } + const shakaUi = new shaka.ui.Overlay( player, document.getElementById('shaka-container') as HTMLElement, @@ -88,56 +225,26 @@ ); shakaUi.configure({ - chapter: true, - play_pause: true, - time_and_duration: true, - overflowMenuButtons: ['chapter', 'cast', 'airplay', 'captions', 'quality', 'loop', 'language'] - }); - - const playerConfig = player.getConfiguration(); - const instanceDefaultBitrate = import.meta.env.VITE_DEFAULT_DASH_BITRATE - ? Number(import.meta.env.VITE_DEFAULT_DASH_BITRATE) - : -1; - playerConfig.abr.defaultBandwidthEstimate = instanceDefaultBitrate; - - player.configure(playerConfig); - - 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; - - let proxiedUrl = url.toString(); - - if ( - Capacitor.getPlatform() === 'android' && - (url.host.endsWith('.googlevideo.com') || url.href.includes('drm')) - ) { - proxiedUrl = localProxy + proxiedUrl; - } - - 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', get(poTokenCacheStore)); - } - } - - request.method = 'POST'; - request.body = new Uint8Array([120, 0]); - } - - request.uris[0] = proxiedUrl; + controlPanelElements: [ + 'play_pause', + 'spacer', + 'volume', + 'chapter', + 'time_and_duration', + 'overflow_menu' + ], + overflowMenuButtons: ['cast', 'airplay', 'captions', 'quality', 'loop', 'language'] }); if (!data.video.hlsUrl) { + let dashUrl = data.video.dashUrl; + + if (!data.video.fallbackPatch && (!Capacitor.isNativePlatform() || proxyVideos)) { + dashUrl += '?local=true'; + } + + await player.load(dashUrl); + if (data.video.captions) { data.video.captions.forEach(async (caption) => { player.addTextTrackAsync( @@ -214,18 +321,6 @@ } } - let dashUrl = data.video.dashUrl; - - if (!data.video.fallbackPatch) { - if (!Capacitor.isNativePlatform() || proxyVideos) { - dashUrl += '?local=true'; - } - } - - console.log(dashUrl); - - await player.load(dashUrl); - await loadPlayerPos(); const defaultLanguage = get(playerDefaultLanguage); diff --git a/materialious/src/lib/patches/youtubejs.ts b/materialious/src/lib/patches/youtubejs.ts index ebf3b829..83a7b6b2 100644 --- a/materialious/src/lib/patches/youtubejs.ts +++ b/materialious/src/lib/patches/youtubejs.ts @@ -4,6 +4,7 @@ import { numberWithCommas } from '$lib/time'; import { Capacitor } from '@capacitor/core'; import type { WebPoSignalOutput } from 'bgutils-js'; import { BG, buildURL, GOOG_API_KEY } from 'bgutils-js'; +import { Buffer } from 'buffer'; import { get } from 'svelte/store'; import { Innertube, UniversalCache, YT, YTNodes } from 'youtubei.js'; @@ -141,8 +142,8 @@ export async function patchYoutubeJs(videoId: string): Promise { let dashUri: string = ''; if (!video.basic_info.is_live) { - let manifest = await video.toDash(); - dashUri = URL.createObjectURL(new Blob([manifest], { type: 'application/dash+xml;charset=utf8' })); + const manifest = await video.toDash(); + dashUri = `data:application/dash+xml;charset=utf-8;base64,${Buffer.from(manifest).toString('base64')}`; } const descString = video.secondary_info.description?.toString() || ''; @@ -250,6 +251,7 @@ export async function patchYoutubeJs(videoId: string): Promise { subCountText: '', keywords: video.basic_info.keywords || [], allowedRegions: [], - fallbackPatch: 'youtubejs' + fallbackPatch: 'youtubejs', + youtubeJsPatchInfo: video }; } diff --git a/materialious/src/lib/style.css b/materialious/src/lib/style.css index 4c1efbce..1e16e039 100644 --- a/materialious/src/lib/style.css +++ b/materialious/src/lib/style.css @@ -2,48 +2,6 @@ padding: 0.1em 0.4em; } -.vds-chapter-radio-label { - word-wrap: break-word !important; - overflow-wrap: break-word !important; - white-space: normal !important; -} - -.vds-chapter-radio-content { - max-width: 200px !important; -} - -.vds-kb-action { - position: absolute !important; - top: 50% !important; - left: 50% !important; -} - -.vds-chapter-radio { - align-items: start !important; -} - -.vds-time-divider { - font-size: 0 !important; - margin-right: 5px !important; -} - -.sponsorskip-bar { - position: absolute; - margin-top: 2px; - height: 5px; - top: -1; - border-radius: 1em; -} - -.vds-time-slider { - position: relative !important; - width: 100% !important; -} - -.vds-poster :where(img) { - object-fit: cover !important; -} - .infinite-status-prompt { display: none !important; }