More progress with porting to shaka player

This commit is contained in:
WardPearce
2025-02-23 08:35:42 +13:00
parent dc4d823766
commit eec1bfaed7
6 changed files with 179 additions and 107 deletions
+13
View File
@@ -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",
+2 -1
View File
@@ -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"
}
}
}
+3
View File
@@ -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 {
+156 -61
View File
@@ -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);
+5 -3
View File
@@ -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<VideoPlay> {
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<VideoPlay> {
subCountText: '',
keywords: video.basic_info.keywords || [],
allowedRegions: [],
fallbackPatch: 'youtubejs'
fallbackPatch: 'youtubejs',
youtubeJsPatchInfo: video
};
}
-42
View File
@@ -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;
}