More shaka player progress

This commit is contained in:
WardPearce
2025-02-23 07:48:04 +13:00
parent 44db93b0ad
commit dc4d823766
9 changed files with 735 additions and 539 deletions
+520 -319
View File
File diff suppressed because it is too large Load Diff
+4 -2
View File
@@ -9,7 +9,8 @@
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"lint": "prettier --check . && eslint .",
"format": "prettier --write ."
"format": "prettier --write .",
"postinstall": "patch-package"
},
"devDependencies": {
"@capacitor/assets": "^3.0.5",
@@ -30,6 +31,7 @@
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.45.1",
"jsdom": "^25.0.1",
"patch-package": "^8.0.0",
"prettier": "^3.3.3",
"prettier-plugin-svelte": "^3.2.7",
"svelte": "^5.19.7",
@@ -73,4 +75,4 @@
"terser": "^5.34.1",
"youtubei.js": "^12.2.0"
}
}
}
@@ -0,0 +1,38 @@
diff --git a/node_modules/shaka-player/dist/shaka-player.compiled.d.ts b/node_modules/shaka-player/dist/shaka-player.compiled.d.ts
index 0971e58..9d5c515 100644
--- a/node_modules/shaka-player/dist/shaka-player.compiled.d.ts
+++ b/node_modules/shaka-player/dist/shaka-player.compiled.d.ts
@@ -5266,3 +5266,5 @@ declare namespace shaka.extern {
declare namespace shaka.extern {
type TransmuxerPlugin = ( ) => shaka.extern.Transmuxer ;
}
+
+export default shaka;
diff --git a/node_modules/shaka-player/dist/shaka-player.ui.d.ts b/node_modules/shaka-player/dist/shaka-player.ui.d.ts
index 460e009..41bab00 100644
--- a/node_modules/shaka-player/dist/shaka-player.ui.d.ts
+++ b/node_modules/shaka-player/dist/shaka-player.ui.d.ts
@@ -6011,3 +6011,5 @@ declare namespace shaka.extern {
declare namespace shaka.ui.Watermark {
type Options = { alpha : number , color : string , displayDuration : number , interval : number , position : string , size : number , skip : number , text : string , transitionDuration : number , type : string } ;
}
+
+export default shaka;
diff --git a/node_modules/shaka-player/index.d.ts b/node_modules/shaka-player/index.d.ts
new file mode 100644
index 0000000..79f4829
--- /dev/null
+++ b/node_modules/shaka-player/index.d.ts
@@ -0,0 +1,2 @@
+<reference path="./dist/shaka-player.compiled.d.ts" />
+<reference path="./dist/shaka-player.ui.d.ts" />
\ No newline at end of file
diff --git a/node_modules/shaka-player/ui.d.ts b/node_modules/shaka-player/ui.d.ts
new file mode 100644
index 0000000..84a3be0
--- /dev/null
+++ b/node_modules/shaka-player/ui.d.ts
@@ -0,0 +1,3 @@
+import shaka from 'shaka-player/dist/shaka-player.ui'
+export * from 'shaka-player/dist/shaka-player.ui'
+export default shaka;
+1 -1
View File
@@ -1 +1 @@
declare module 'virtual:pwa-info';
declare module 'virtual:pwa-info';
@@ -1,55 +1,20 @@
import { goto } from "$app/navigation";
import { Capacitor } from "@capacitor/core";
import { capacitorFetch } from "./capacitorFetch";
export const localProxy = 'http://localhost:3000/';
if (Capacitor.getPlatform() === 'android') {
const originalFetch = window.fetch;
const corsProxyUrl: string = 'http://localhost:3000/';
window.fetch = async (requestInput: string | URL | Request, requestOptions?: RequestInit): Promise<Response> => {
let requestUrl: string;
if (typeof requestInput === 'string') {
requestUrl = requestInput;
} else if ('url' in requestInput) {
requestUrl = requestInput.url;
requestOptions = {
body: requestInput.body,
cache: requestInput.cache,
credentials: requestInput.credentials,
headers: requestInput.headers,
method: requestInput.method,
mode: requestInput.mode,
redirect: requestInput.redirect,
referrer: requestInput.referrer,
signal: requestInput.signal,
};
if (requestOptions.body && requestOptions.body instanceof ReadableStream) {
(requestOptions as RequestInit & { duplex?: 'half' | 'full'; }).duplex = 'half';
}
} else {
requestUrl = requestInput.toString();
window.fetch = async (
requestInput: string | URL | Request, requestOptions?: RequestInit
): Promise<Response> => {
if (requestInput.toString().startsWith(localProxy)) {
return originalFetch(requestInput, requestOptions);
}
if (!requestUrl.startsWith(corsProxyUrl) && !requestUrl.startsWith('/') && !requestUrl.startsWith('blob:')) {
requestInput = corsProxyUrl + requestUrl;
}
// Use the original fetch with the proxied URL and options
return originalFetch(requestInput, requestOptions);
};
const currentOrigin: string = window.location.protocol + '//' + window.location.host;
const originalXhrOpen = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function (...args: any[]): void {
const targetOriginMatch = /^https?:\/\/([^\/]+)/i.exec(args[1]);
if (targetOriginMatch && targetOriginMatch[0].toLowerCase() !== currentOrigin) {
args[1] = corsProxyUrl + args[1];
}
/* @ts-ignore */
return originalXhrOpen.apply(this, args);
return capacitorFetch(requestInput, requestOptions);
};
setTimeout(() => goto('/', { replaceState: true }), 10);
+131 -137
View File
@@ -1,18 +1,22 @@
<script lang="ts">
import { page } from '$app/stores';
import { localProxy } from '$lib/android/http/androidRequests';
import { getBestThumbnail } from '$lib/images';
import { padTime, videoLength } from '$lib/time';
import { Capacitor } from '@capacitor/core';
import { ScreenOrientation, type ScreenOrientationResult } from '@capacitor/screen-orientation';
import { StatusBar, Style } from '@capacitor/status-bar';
import { NavigationBar } from '@hugotomazi/capacitor-navigation-bar';
import type { Page } from '@sveltejs/kit';
import { error, type Page } from '@sveltejs/kit';
import 'shaka-player/dist/controls.css';
import shaka from 'shaka-player/dist/shaka-player.ui';
import { SponsorBlock, type Category, type Segment } from 'sponsorblock-api';
import { onDestroy, onMount } from 'svelte';
import { _ } from 'svelte-i18n';
import { get } from 'svelte/store';
import { deleteVideoProgress, getVideoProgress, saveVideoProgress } from '../api';
import type { VideoPlay } from '../api/model';
import { pullBitratePreference, type PhasedDescription } from '../misc';
import { type PhasedDescription } from '../misc';
import {
authStore,
instanceStore,
@@ -21,10 +25,10 @@
playerDefaultLanguage,
playerProxyVideosStore,
playerSavePlaybackPositionStore,
poTokenCacheStore,
sponsorBlockCategoriesStore,
sponsorBlockDisplayToastStore,
sponsorBlockStore,
sponsorBlockTimelineStore,
sponsorBlockUrlStore,
synciousInstanceStore,
synciousStore
@@ -42,59 +46,19 @@
let { data, audioMode = false, isEmbed = false, segments = $bindable([]) }: Props = $props();
let snackBarAlert = $state('');
let playerIsLive = $state(false);
let playerPosSet = false;
let originalOrigination: ScreenOrientationResult | undefined;
let sponsorBlockElements: Element[] = [];
let watchProgressTimeout: NodeJS.Timeout;
function setSponsorTimeline() {
if (get(sponsorBlockTimelineStore)) return;
if (segments.length === 0) return;
const timeline = document.getElementsByClassName('vds-time-slider')[0];
if (sponsorBlockElements.length > 0) {
sponsorBlockElements.forEach((barDiv) => {
if (timeline.contains(barDiv)) {
timeline.removeChild(barDiv);
}
});
}
const segmentColors = {
sponsor: '00d400',
selfpromo: 'ffff00',
interaction: 'cc00ff',
intro: '00ffff',
outro: '0202ed',
preview: '008fd6',
music_offtopic: 'ff9900',
filler: '7300FF'
};
segments.forEach((segment) => {
const startPercent = (segment.startTime / data.video.lengthSeconds) * 100;
const endPercent = (segment.endTime / data.video.lengthSeconds) * 100;
const widthPercent = endPercent - startPercent;
const barDiv = document.createElement('div');
barDiv.classList.add('sponsorskip-bar');
barDiv.style.left = `${startPercent}%`;
barDiv.style.width = `${widthPercent}%`;
barDiv.style.backgroundColor =
segment.category in segmentColors ? `#${segmentColors[segment.category]}` : 'grey';
timeline.appendChild(barDiv);
sponsorBlockElements.push(barDiv);
});
}
let player: shaka.Player;
let playerElement: HTMLMediaElement;
function loadTimeFromUrl(page: Page): boolean {
if (player) {
const timeGivenUrl = page.url.searchParams.get('time');
if (timeGivenUrl && !isNaN(parseFloat(timeGivenUrl))) {
player.currentTime = Number(timeGivenUrl);
playerElement.currentTime = Number(timeGivenUrl);
return true;
}
}
@@ -107,19 +71,83 @@
const proxyVideos = get(playerProxyVideosStore);
onMount(async () => {
shaka.polyfill.installAll();
if (!shaka.Player.isBrowserSupported()) {
error(400, 'Shaka not supported on your browser');
}
player = new shaka.Player();
playerElement = document.getElementById('player') as HTMLMediaElement;
await player.attach(playerElement);
const shakaUi = new shaka.ui.Overlay(
player,
document.getElementById('shaka-container') as HTMLElement,
playerElement
);
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;
});
if (!data.video.hlsUrl) {
playerIsLive = false;
if (data.video.captions) {
data.video.captions.forEach(async (caption) => {
player.textTracks.add({
label: caption.label,
kind: 'captions',
language: caption.language_code,
// Need if captions are generated when youtube.js is being used.
src: caption.url.startsWith('http')
? caption.url
: `${get(instanceStore)}${caption.url}`
});
player.addTextTrackAsync(
caption.url.startsWith('http') ? caption.url : `${get(instanceStore)}${caption.url}`,
caption.language_code,
'captions',
undefined,
undefined,
caption.label
);
});
}
@@ -141,44 +169,16 @@
});
if (timestampIndex > 0) {
player.textTracks.add({
kind: 'chapters',
src: URL.createObjectURL(new Blob([chapterWebVTT])),
default: true
});
player.addChaptersTrack(
URL.createObjectURL(new Blob([chapterWebVTT])),
get(playerDefaultLanguage)
);
}
}
// Auto save watch progress every minute.
watchProgressTimeout = setInterval(() => savePlayerPos(), 60000);
player.addEventListener('provider-change', (event) => {
const provider = event.detail as any;
if (provider?.type === 'dash') {
const bitrate = pullBitratePreference();
const instanceDefaultBitrate = import.meta.env.VITE_DEFAULT_DASH_BITRATE
? Number(import.meta.env.VITE_DEFAULT_DASH_BITRATE)
: -1;
provider.library = () => import('dashjs');
provider.config = {
streaming: {
abr: {
ABRStrategy: 'abrBola',
movingAverageMethod: 'ewma',
autoSwitchBitrate: {
video: false,
audio: false
},
initialBitrate: {
video: bitrate === -1 ? instanceDefaultBitrate : bitrate,
audio: bitrate === -1 ? instanceDefaultBitrate : bitrate
}
}
}
};
}
});
if (get(sponsorBlockStore) && get(sponsorBlockCategoriesStore)) {
const currentCategories = get(sponsorBlockCategoriesStore);
@@ -193,22 +193,16 @@
get(sponsorBlockCategoriesStore) as Category[]
);
setSponsorTimeline();
addEventListener('resize', () => {
setSponsorTimeline();
});
player.addEventListener('time-update', (event: MediaTimeUpdateEvent) => {
player.addEventListener('time-update', () => {
segments.forEach((segment) => {
if (
event.detail.currentTime >= segment.startTime &&
event.detail.currentTime <= segment.endTime
playerElement.currentTime >= segment.startTime &&
playerElement.currentTime <= segment.endTime
) {
if (Math.round(player.currentTime) >= Math.round(player.duration)) {
if (Math.round(playerElement.currentTime) >= Math.round(playerElement.duration)) {
return;
}
player.currentTime = segment.endTime + 1;
playerElement.currentTime = segment.endTime + 1;
if (!get(sponsorBlockDisplayToastStore)) {
snackBarAlert = `${get(_)('skipping')} ${segment.category}`;
ui('#snackbar-alert');
@@ -220,35 +214,27 @@
}
}
src = [{ src: data.video.dashUrl, type: 'application/dash+xml' }];
let dashUrl = data.video.dashUrl;
if (!data.video.fallbackPatch) {
if (!Capacitor.isNativePlatform() || proxyVideos) {
(src[0] as { src: string }).src += '?local=true';
dashUrl += '?local=true';
}
}
player.addEventListener('dash-can-play', async () => {
await loadPlayerPos();
console.log(dashUrl);
const defaultLanguage = get(playerDefaultLanguage);
if (defaultLanguage) {
let trackIndex = 0;
for (const track of player.audioTracks) {
console.log(player.audioTracks);
if (track.label.toLowerCase().includes(defaultLanguage)) {
player.remoteControl.changeAudioTrack(trackIndex);
break;
}
trackIndex++;
}
}
await player.load(dashUrl);
if (get(playerAutoPlayStore)) {
player.play();
player.exitFullscreen();
await loadPlayerPos();
const defaultLanguage = get(playerDefaultLanguage);
if (defaultLanguage) {
const audioLanguages = player.getAudioLanguages();
if (audioLanguages.includes(defaultLanguage)) {
player.selectAudioLanguage(defaultLanguage);
}
});
}
if (Capacitor.getPlatform() === 'android' && data.video.adaptiveFormats.length > 0) {
const videoFormats = data.video.adaptiveFormats.filter((format) =>
@@ -257,8 +243,10 @@
originalOrigination = await ScreenOrientation.orientation();
player.addEventListener('fullscreen-change', async (event: FullscreenChangeEvent) => {
if (event.detail) {
player.addEventListener('fullscreen-change', async () => {
const isFullScreen = document.fullscreenElement;
if (isFullScreen) {
// Ensure bar color is black while in fullscreen
await StatusBar.setBackgroundColor({ color: '#000000' });
await NavigationBar.setColor({
@@ -271,7 +259,7 @@
if (!get(playerAndroidLockOrientation)) return;
if (event.detail && videoFormats[0].resolution) {
if (isFullScreen && videoFormats[0].resolution) {
const widthHeight = videoFormats[0].resolution.split('x');
if (widthHeight.length !== 2) return;
@@ -296,17 +284,9 @@
});
}
} else {
playerIsLive = true;
src = [
{
src: data.video.hlsUrl + '?local=true',
type: 'application/x-mpegurl'
}
];
await player.load(data.video.hlsUrl + '?local=true');
}
player.storage = 'video-player';
const currentTheme = await getDynamicTheme();
if (data.video.fallbackPatch === 'youtubejs') {
@@ -365,20 +345,23 @@
}
}
if (toSetTime > 0) player.currentTime = toSetTime;
if (toSetTime > 0) playerElement.currentTime = toSetTime;
}
function savePlayerPos() {
if (data.video.hlsUrl) return;
if (get(playerSavePlaybackPositionStore) && player && player.currentTime) {
if (player.currentTime < player.duration - 10 && player.currentTime > 10) {
if (get(playerSavePlaybackPositionStore) && player && playerElement.currentTime) {
if (
playerElement.currentTime < playerElement.duration - 10 &&
playerElement.currentTime > 10
) {
try {
localStorage.setItem(`v_${data.video.videoId}`, player.currentTime.toString());
localStorage.setItem(`v_${data.video.videoId}`, playerElement.currentTime.toString());
} catch {}
if (get(synciousStore) && get(synciousInstanceStore) && get(authStore)) {
saveVideoProgress(data.video.videoId, player.currentTime);
saveVideoProgress(data.video.videoId, playerElement.currentTime);
}
} else {
try {
@@ -408,8 +391,9 @@
try {
savePlayerPos();
} catch (error) {}
await player.pause();
await playerElement.pause();
player.destroy();
playerElement.remove();
playerPosSet = false;
});
</script>
@@ -418,6 +402,16 @@
<div style="margin-top: 40vh;"></div>
{/if}
<div id="shaka-container" data-shaka-player-container>
<video
controls={false}
autoplay={$playerAutoPlayStore}
id="player"
width="100%"
poster={getBestThumbnail(data.video.videoThumbnails, 1251, 781)}
></video>
</div>
{#if !isEmbed}
<div class="snackbar" id="snackbar-alert">
<span class="bold" style="text-transform: capitalize;">{snackBarAlert}</span>
-14
View File
@@ -29,20 +29,6 @@ export function proxyVideoUrl(source: string): string {
return rawSrc.toString();
}
export function pullBitratePreference(): number {
const vidstack = localStorage.getItem('video-player');
if (vidstack) {
const vidstackSettings = JSON.parse(vidstack);
if (vidstackSettings.quality && vidstackSettings.quality.bitrate) {
return vidstackSettings.quality.bitrate;
}
}
return -1;
}
export function unsafeRandomItem(array: any[]): any {
return array[Math.floor(Math.random() * array.length)];
}
+31 -21
View File
@@ -5,8 +5,7 @@ import { Capacitor } from '@capacitor/core';
import type { WebPoSignalOutput } from 'bgutils-js';
import { BG, buildURL, GOOG_API_KEY } from 'bgutils-js';
import { get } from 'svelte/store';
import { Innertube, UniversalCache } from 'youtubei.js';
import { capacitorFetch } from '../android/http/capacitorFetch';
import { Innertube, UniversalCache, YT, YTNodes } from 'youtubei.js';
type WebPoMinter = {
integrityTokenBasedMinter?: BG.WebPoMinter;
@@ -88,7 +87,7 @@ export async function patchYoutubeJs(videoId: string): Promise<VideoPlay> {
}
const youtube = await Innertube.create({
fetch: Capacitor.getPlatform() === 'android' ? capacitorFetch : fetch,
fetch: fetch,
generate_session_locally: true,
cache: new UniversalCache(false),
location: get(interfaceRegionStore)
@@ -105,7 +104,35 @@ export async function patchYoutubeJs(videoId: string): Promise<VideoPlay> {
sessionWebPo = poTokensCached;
}
const video = await youtube.getInfo(videoId);
const extraArgs: Record<string, any> = {
playbackContext: {
contentPlaybackContext: {
vis: 0,
splay: false,
lactMilliseconds: '-1',
signatureTimestamp: youtube.session.player?.sts
}
},
contentCheckOk: true,
racyCheckOk: true
};
// Generate content WebPO token.
if (integrityTokenBasedMinter) {
extraArgs.serviceIntegrityDimensions = {
poToken: await integrityTokenBasedMinter.mintAsWebsafeString(videoId)
};
}
const watchEndpoint = new YTNodes.NavigationEndpoint({ watchEndpoint: { videoId } });
const rawPlayerResponse = await watchEndpoint.call(youtube.actions, extraArgs);
const rawNextResponse = await watchEndpoint.call(youtube.actions, {
override_endpoint: '/next',
racyCheckOk: true,
contentCheckOk: true
});
const video = new YT.VideoInfo([rawPlayerResponse, rawNextResponse], youtube!.actions, '');
if (!video.primary_info || !video.secondary_info) {
throw new Error('Unable to pull video info from youtube.js');
@@ -115,21 +142,6 @@ export async function patchYoutubeJs(videoId: string): Promise<VideoPlay> {
if (!video.basic_info.is_live) {
let manifest = await video.toDash();
let parser = new DOMParser();
let xmlDoc = parser.parseFromString(manifest, 'application/xml');
let baseURLs = xmlDoc.getElementsByTagName('BaseURL');
Array.from(baseURLs).forEach((baseURL) => {
let url = new URL(baseURL.textContent as string);
url.searchParams.set('pot', (sessionWebPo ?? BG.PoToken.generateColdStartToken(youtube.session.context.client.visitorData ?? '')));
baseURL.textContent = url.toString();
});
const serializer = new XMLSerializer();
manifest = serializer.serializeToString(xmlDoc);
dashUri = URL.createObjectURL(new Blob([manifest], { type: 'application/dash+xml;charset=utf8' }));
}
@@ -199,8 +211,6 @@ export async function patchYoutubeJs(videoId: string): Promise<VideoPlay> {
});
}
console.log(video.primary_info);
return {
type: 'video',
title: video.primary_info.title?.toString() || '',
+1 -1
View File
@@ -11,7 +11,7 @@
"strict": true,
"moduleResolution": "bundler",
"types": [
"vidstack/svelte"
"shaka-player"
]
},
"include": [