Fixes to ytjs fallback

This commit is contained in:
WardPearce
2025-04-01 02:23:24 +13:00
parent ac5f2ea7c9
commit 3370e7e1d8
10 changed files with 922 additions and 1328 deletions
+88
View File
@@ -14,6 +14,17 @@
<option name="screenX" value="720" />
<option name="screenY" value="1280" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="OnePlus" />
<option name="codename" value="OP5552L1" />
<option name="id" value="OP5552L1" />
<option name="manufacturer" value="OnePlus" />
<option name="name" value="CPH2415" />
<option name="screenDensity" value="480" />
<option name="screenX" value="1080" />
<option name="screenY" value="2412" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="OPPO" />
@@ -102,6 +113,17 @@
<option name="screenX" value="1080" />
<option name="screenY" value="1272" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="motorola" />
<option name="codename" value="austin" />
<option name="id" value="austin" />
<option name="manufacturer" value="Motorola" />
<option name="name" value="moto g 5G (2022)" />
<option name="screenDensity" value="280" />
<option name="screenX" value="720" />
<option name="screenY" value="1600" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="samsung" />
@@ -267,6 +289,28 @@
<option name="screenX" value="1080" />
<option name="screenY" value="2340" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="gta9pwifi" />
<option name="id" value="gta9pwifi" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="SM-X210" />
<option name="screenDensity" value="240" />
<option name="screenX" value="1200" />
<option name="screenY" value="1920" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="gts7xllite" />
<option name="id" value="gts7xllite" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="SM-T738U" />
<option name="screenDensity" value="340" />
<option name="screenX" value="1600" />
<option name="screenY" value="2560" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="samsung" />
@@ -278,6 +322,28 @@
<option name="screenX" value="1848" />
<option name="screenY" value="2960" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="gts8wifi" />
<option name="id" value="gts8wifi" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy Tab S8" />
<option name="screenDensity" value="274" />
<option name="screenX" value="1600" />
<option name="screenY" value="2560" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="gts9fe" />
<option name="id" value="gts9fe" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy Tab S9 FE 5G" />
<option name="screenDensity" value="280" />
<option name="screenX" value="1440" />
<option name="screenY" value="2304" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="google" />
@@ -333,6 +399,17 @@
<option name="screenX" value="720" />
<option name="screenY" value="1600" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="o1q" />
<option name="id" value="o1q" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy S21" />
<option name="screenDensity" value="421" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="31" />
<option name="brand" value="google" />
@@ -455,6 +532,17 @@
<option name="screenX" value="1080" />
<option name="screenY" value="2424" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="35" />
<option name="brand" value="google" />
<option name="codename" value="tokay" />
<option name="id" value="tokay" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 9" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2424" />
</PersistentDeviceSelectionData>
</list>
</option>
</component>
+453 -946
View File
File diff suppressed because it is too large Load Diff
+3 -4
View File
@@ -30,7 +30,6 @@
"eslint": "^8.56.0",
"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",
@@ -54,7 +53,7 @@
"@capgo/inappbrowser": "^6.6.8",
"@hugotomazi/capacitor-navigation-bar": "^4.0.1",
"beercss": "^3.8.0",
"bgutils-js": "^3.1.3",
"bgutils-js": "^3.2.0",
"capacitor-nodejs": "https://github.com/EdenwareApps/Capacitor-NodeJS/releases/download/v1.0.0-beta.7/capacitor6-nodejs.tgz",
"fuse.js": "^7.0.0",
"googlevideo": "^2.0.0",
@@ -67,12 +66,12 @@
"media-icons": "^0.10.0",
"mousetrap": "^1.6.5",
"peerjs": "^1.5.4",
"shaka-player": "^4.13.4",
"shaka-player": "^4.13.0",
"sponsorblock-api": "^0.2.4",
"svelte-i18n": "^4.0.1",
"svelte-infinite-loading": "^1.4.0",
"svelte-persisted-store": "^0.12.0",
"terser": "^5.34.1",
"youtubei.js": "^13.1.0"
"youtubei.js": "^13.3.0"
}
}
+3
View File
@@ -1,3 +1,5 @@
import type { YT } from 'youtubei.js';
export interface Image {
url: string;
@@ -99,6 +101,7 @@ export interface VideoPlay extends Video {
authorThumbnails: Image[];
captions: Captions[];
storyboards?: StoryBoard[];
ytJsVideoInfo?: YT.VideoInfo,
fallbackPatch?: 'youtubejs' | 'piped';
}
+113 -62
View File
@@ -7,6 +7,7 @@
import { StatusBar, Style } from '@capacitor/status-bar';
import { NavigationBar } from '@hugotomazi/capacitor-navigation-bar';
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';
@@ -40,19 +41,23 @@
isSyncing?: boolean;
isEmbed?: boolean;
segments?: Segment[];
playerElement: HTMLMediaElement;
}
let { data, audioMode = false, isEmbed = false, segments = $bindable([]) }: Props = $props();
let {
data,
audioMode = false,
isEmbed = false,
segments = $bindable([]),
playerElement = $bindable()
}: Props = $props();
let snackBarAlert = $state('');
let playerPosSet = false;
let originalOrigination: ScreenOrientationResult | undefined;
let watchProgressTimeout: NodeJS.Timeout;
const HTTP_IN_HEX = 0x68747470;
let player: shaka.Player;
let playerElement: HTMLMediaElement;
let shakaUi: shaka.ui.Overlay;
function loadTimeFromUrl(page: Page): boolean {
@@ -101,18 +106,30 @@
player.configure({
streaming: {
bufferingGoal: 180,
rebufferingGoal: 0.02,
bufferBehind: 300
},
manifest: {
// disableVideo: format === 'audio',
segmentRelativeVttTiming: true
bufferingGoal: data.video.ytJsVideoInfo
? (data.video.ytJsVideoInfo.page[0].player_config?.media_common_config
.dynamic_readahead_config.max_read_ahead_media_time_ms || 0) / 1000
: 180,
rebufferingGoal: data.video.ytJsVideoInfo
? (data.video.ytJsVideoInfo.page[0].player_config?.media_common_config
.dynamic_readahead_config.read_ahead_growth_rate_ms || 0) / 1000
: 0.02,
bufferBehind: 300,
autoLowLatencyMode: true
},
abr: {
enabled: false,
restrictToElementSize: true
enabled: true,
restrictToElementSize: true,
restrictions: {
maxBandwidth: data.video.ytJsVideoInfo
? Number(
data.video.ytJsVideoInfo.page[0].player_config?.stream_selection_config.max_bitrate
)
: null
}
},
preferredDecodingAttributes: !data.video.hlsUrl ? ['smooth', 'powerEfficient'] : [],
autoShowText: shaka.config.AutoShowText.NEVER
});
@@ -126,70 +143,104 @@
// https://github.com/LuanRT/BgUtils/blob/6b121166be1ccb0b952dee1bdac488808365ae6b/examples/browser/web/src/main.ts#L293
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]);
const url = new URL(request.uris[0]);
if (url.hostname.endsWith('.googlevideo.com') && url.pathname === '/videoplayback') {
console.log('Modifying request');
if (request.headers.Range) {
url.searchParams.set('range', request.headers.Range.split('=')[1]);
url.searchParams.set('ump', '1');
url.searchParams.set('srfvp', '1');
url.searchParams.set('mpd_version', '7');
url.searchParams.set('pot', get(poTokenCacheStore));
delete headers.Range;
url.searchParams.set('pot', get(poTokenCacheStore).poToken);
delete request.headers.Range;
}
request.method = 'POST';
request.body = new Uint8Array([120, 0]);
}
request.method = 'POST';
request.body = new Uint8Array([120, 0]);
request.uris[0] = url.toString();
}
request.uris[0] = url.toString();
});
networkingEngine.registerResponseFilter(async (type, response, context) => {
if (
response.data &&
response.data.byteLength > 4 &&
new DataView(
response.data as ArrayBufferLike & { BYTES_PER_ELEMENT?: undefined }
).getUint32(0) === HTTP_IN_HEX
) {
// Interpret the response data as a URL string.
const responseAsString = shaka.util.StringUtils.fromUTF8(response.data);
networkingEngine.registerResponseFilter(async (type, response) => {
let mediaData = new Uint8Array(0);
const retryParameters = player.getConfiguration().streaming.retryParameters;
// Make another request for the redirect URL.
const uris = [responseAsString];
const redirectRequest = shaka.net.NetworkingEngine.makeRequest(uris, retryParameters);
const requestOperation = networkingEngine.request(type, redirectRequest, context);
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;
// Modify the original response to contain the results of the redirect
// response.
response.data = redirectResponse.data;
response.headers = redirectResponse.headers;
response.uri = redirectResponse.uri;
} else {
const url = new URL(response.uri);
};
// Fix positioning for auto-generated subtitles
if (
url.hostname.endsWith('.youtube.com') &&
url.pathname === '/api/timedtext' &&
url.searchParams.get('caps') === 'asr' &&
url.searchParams.get('kind') === 'asr' &&
url.searchParams.get('fmt') === 'vtt'
) {
const stringBody = new TextDecoder().decode(response.data);
// position:0% for LTR text and position:100% for RTL text
const cleaned = stringBody.replaceAll(/ align:start position:(?:10)?0%$/gm, '');
const handleMediaData = async (data: Uint8Array) => {
const combinedLength = mediaData.length + data.length;
const tempMediaData = new Uint8Array(combinedLength);
response.data = new TextEncoder().encode(cleaned).buffer as ArrayBuffer;
}
tempMediaData.set(mediaData);
tempMediaData.set(data, mediaData.length);
mediaData = tempMediaData;
};
if (type == shaka.net.NetworkingEngine.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;
}
});
}
@@ -258,7 +309,7 @@
get(sponsorBlockCategoriesStore) as Category[]
);
player.addEventListener('time-update', () => {
playerElement.addEventListener('timeupdate', () => {
segments.forEach((segment) => {
if (
playerElement.currentTime >= segment.startTime &&
@@ -296,7 +347,7 @@
originalOrigination = await ScreenOrientation.orientation();
player.addEventListener('fullscreen-change', async () => {
playerElement.addEventListener('fullscreenchange', async () => {
const isFullScreen = document.fullscreenElement;
if (isFullScreen) {
@@ -100,7 +100,7 @@
</script>
<form onsubmit={preventDefault(handleSubmit)}>
<div class="field prefix round fill no-margin search">
<div class="field prefix fill no-margin search">
<i class="front">search</i>
<input
id="search-box"
@@ -151,7 +151,13 @@
{/if}
{#if !suggestionsForSearch.length && $interfaceSearchHistoryEnabled}
{#each $searchHistoryStore as history}
<a class="row" href={`/search/${encodeURIComponent(history)}`}>
<a
class="row"
onclick={() => {
search = history;
}}
href={`/search/${encodeURIComponent(history)}`}
>
<div>{history}</div>
</a>
{/each}
@@ -10,9 +10,10 @@
interface Props {
video: VideoPlay;
playerElement: HTMLMediaElement;
}
let { video }: Props = $props();
let { video, playerElement = $bindable() }: Props = $props();
let url: string | null = $state(null);
let autoScroll: boolean = $state(true);
@@ -23,21 +24,23 @@
let currentTime = $state(0);
let search: string = $state('');
// player.addEventListener('time-update', (event: MediaTimeUpdateEvent) => {
// currentTime = event.detail.currentTime;
console.log('playerElement', playerElement);
// if (autoScroll) {
// const currentTranscriptLine = document.querySelector(
// '.transcript-line.secondary-container'
// ) as HTMLElement;
// const transcriptScrollable = document.getElementById('transcript');
playerElement.addEventListener('timeupdate', () => {
currentTime = playerElement.currentTime;
// if (currentTranscriptLine && transcriptScrollable) {
// transcriptScrollable.scrollTop =
// currentTranscriptLine.offsetTop - transcriptScrollable.offsetTop - 300;
// }
// }
// });
if (autoScroll) {
const currentTranscriptLine = document.querySelector(
'.transcript-line.secondary-container'
) as HTMLElement;
const transcriptScrollable = document.getElementById('transcript');
if (currentTranscriptLine && transcriptScrollable) {
transcriptScrollable.scrollTop =
currentTranscriptLine.offsetTop - transcriptScrollable.offsetTop - 300;
}
}
});
async function loadTranscript() {
if (!url) {
@@ -112,7 +115,7 @@
<div
class="transcript-line"
id={`transcript-line-${cue.startTime}`}
onclick={() => (player.currentTime = cue.startTime)}
onclick={() => (playerElement.currentTime = cue.startTime)}
class:secondary-container={currentTime >= cue.startTime && currentTime <= cue.endTime}
>
<p class="chip no-margin">{videoLength(cue.startTime)}</p>
+44 -116
View File
@@ -3,140 +3,67 @@ import type { AdaptiveFormats, Captions, Image, StoryBoard, Thumbnail, VideoBase
import { interfaceRegionStore, poTokenCacheStore } from '$lib/store';
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 { BG, type BgConfig } from 'bgutils-js';
import { Buffer } from 'buffer';
import { get } from 'svelte/store';
import { Innertube, UniversalCache, YT, YTNodes } from 'youtubei.js';
type WebPoMinter = {
integrityTokenBasedMinter?: BG.WebPoMinter;
botguardClient?: BG.BotGuardClient;
};
import { Innertube, UniversalCache } from 'youtubei.js';
const fetchClient = Capacitor.getPlatform() === 'android' ? capacitorFetch : fetch;
async function getWebPoMinter(): Promise<WebPoMinter> {
const requestKey = 'O43z0dpjhgX20SCx4KAo';
const challengeResponse = await fetchClient(buildURL('Create', true), {
method: 'POST',
headers: {
'content-type': 'application/json+protobuf',
'x-goog-api-key': GOOG_API_KEY,
'x-user-agent': 'grpc-web-javascript/0.1'
},
body: JSON.stringify([requestKey])
});
const challengeResponseData = await challengeResponse.json();
const bgChallenge = BG.Challenge.parseChallengeData(challengeResponseData);
if (!bgChallenge)
throw new Error('Could not get challenge');
const interpreterJavascript = bgChallenge.interpreterJavascript.privateDoNotAccessOrElseSafeScriptWrappedValue;
if (!document.getElementById(bgChallenge.interpreterHash)) {
const script = document.createElement('script');
script.type = 'text/javascript';
script.id = bgChallenge.interpreterHash;
script.textContent = interpreterJavascript;
document.head.appendChild(script);
}
const botguardClient = await BG.BotGuardClient.create({
globalObj: globalThis,
globalName: bgChallenge.globalName,
program: bgChallenge.program
});
if (bgChallenge) {
const webPoSignalOutput: WebPoSignalOutput = [];
const botguardResponse = await botguardClient.snapshot({ webPoSignalOutput });
const integrityTokenResponse = await fetchClient(buildURL('GenerateIT', true), {
method: 'POST',
headers: {
'content-type': 'application/json+protobuf',
'x-goog-api-key': GOOG_API_KEY,
'x-user-agent': 'grpc-web-javacript/0.1'
},
body: JSON.stringify([requestKey, botguardResponse])
});
const integrityTokenResponseData = await integrityTokenResponse.json();
const integrityToken = integrityTokenResponseData[0] as string | undefined;
if (!integrityToken) {
console.error('Could not get integrity token. Interpreter Hash:', bgChallenge.interpreterHash);
return {};
}
const integrityTokenBasedMinter = await BG.WebPoMinter.create({ integrityToken }, webPoSignalOutput);
return {
integrityTokenBasedMinter,
botguardClient
};
}
return {};
}
export async function patchYoutubeJs(videoId: string): Promise<VideoPlay> {
if (!Capacitor.isNativePlatform()) {
throw new Error('Platform not supported');
}
const youtube = await Innertube.create({
fetch: fetchClient,
generate_session_locally: false,
cache: new UniversalCache(false),
location: get(interfaceRegionStore)
});
let youtube: Innertube;
const { integrityTokenBasedMinter } = await getWebPoMinter();
if (!get(poTokenCacheStore)) {
youtube = await Innertube.create({ retrieve_player: false, fetch: fetchClient });
let sessionWebPo: string | undefined;
const poTokensCached = get(poTokenCacheStore);
if (!poTokensCached && integrityTokenBasedMinter) {
sessionWebPo = await integrityTokenBasedMinter.mintAsWebsafeString(youtube.session.context.client.visitorData ?? '');
poTokenCacheStore.set(sessionWebPo);
} else {
sessionWebPo = poTokensCached;
}
const requestKey = 'O43z0dpjhgX20SCx4KAo';
const visitorData = youtube.session.context.client.visitorData as string;
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 bgConfig: BgConfig = {
fetch: fetchClient,
globalObj: globalThis,
identifier: visitorData,
requestKey
};
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
});
poTokenCacheStore.set({
poToken: poTokenResult.poToken,
visitorData: visitorData
});
}
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 cachedPoToken = get(poTokenCacheStore);
youtube = await Innertube.create({
fetch: fetchClient,
generate_session_locally: true,
cache: new UniversalCache(false),
location: get(interfaceRegionStore),
visitor_data: cachedPoToken.visitorData,
po_token: cachedPoToken.poToken
});
const video = new YT.VideoInfo([rawPlayerResponse, rawNextResponse], youtube!.actions, '');
const video = await youtube.getInfo(videoId);
if (!video.primary_info || !video.secondary_info) {
throw new Error('Unable to pull video info from youtube.js');
@@ -254,6 +181,7 @@ export async function patchYoutubeJs(videoId: string): Promise<VideoPlay> {
subCountText: '',
keywords: video.basic_info.keywords || [],
allowedRegions: [],
ytJsVideoInfo: video,
fallbackPatch: 'youtubejs',
};
}
+1 -1
View File
@@ -101,6 +101,6 @@ export const playlistSettingsStore: Writable<Record<string, { shuffle: boolean;
writable({});
export const poTokenCacheStore: Writable<string> = writable();
export const poTokenCacheStore: Writable<{ poToken: string, visitorData: string; }> = writable();
export const searchHistoryStore: Writable<string[]> = persisted('searchHistory', []);
@@ -1,4 +1,5 @@
<script lang="ts">
import { goto } from '$app/navigation';
import {
addPlaylistVideo,
deleteUnsubscribe,
@@ -16,27 +17,33 @@
import Transcript from '$lib/components/Transcript.svelte';
import { getBestThumbnail, proxyGoogleImage } from '$lib/images';
import { letterCase } from '$lib/letterCasing';
import { truncate } from '$lib/misc';
import { truncate, unsafeRandomItem } from '$lib/misc';
import type { PlayerEvents } from '$lib/player.js';
import {
activePageStore,
authStore,
interfaceAutoExpandComments,
interfaceAutoExpandDesc,
interfaceLowBandwidthMode,
playerAutoplayNextByDefaultStore,
playerListenByDefaultStore,
playerTheatreModeByDefaultStore,
playlistSettingsStore,
syncPartyConnectionsStore,
syncPartyPeerStore
} from '$lib/store';
import { cleanNumber, humanizeSeconds, numberWithCommas } from '$lib/time';
import ui from 'beercss';
import type { DataConnection } from 'peerjs';
import { type Segment } from 'sponsorblock-api';
import { onDestroy, tick } from 'svelte';
import { onDestroy, onMount, tick } from 'svelte';
import { _ } from 'svelte-i18n';
import { get } from 'svelte/store';
let { data = $bindable() } = $props();
let playerElement: HTMLMediaElement;
let comments: Comments | null = $state(null);
data.streamed.comments?.then((streamedComments) => {
comments = streamedComments;
@@ -78,209 +85,203 @@
}
});
// function playerSyncEvents(conn: DataConnection) {
// if (player) {
// conn.send({
// events: [{ type: 'seek', time: player.currentTime }]
// } as PlayerEvents);
// }
function playerSyncEvents(conn: DataConnection) {
if (playerElement) {
conn.send({
events: [{ type: 'seek', time: playerElement.currentTime }]
} as PlayerEvents);
}
// if (data.playlistId) {
// conn.send({
// events: [
// {
// type: 'playlist',
// playlistId: data.playlistId
// }
// ]
// } as PlayerEvents);
// }
if (data.playlistId) {
conn.send({
events: [
{
type: 'playlist',
playlistId: data.playlistId
}
]
} as PlayerEvents);
}
// conn.on('data', (rawData) => {
// const events = rawData as PlayerEvents;
conn.on('data', (rawData) => {
const events = rawData as PlayerEvents;
// events.events.forEach(async (event) => {
// if (!player) return;
events.events.forEach(async (event) => {
if (!playerElement) return;
// if (event.type === 'pause') {
// player.pause();
// } else if (event.type === 'play') {
// player.play();
// } else if (event.type === 'seek' && event.time) {
// const timeDiff = player.currentTime - event.time;
if (event.type === 'pause') {
playerElement.pause();
} else if (event.type === 'play') {
playerElement.play();
} else if (event.type === 'seek' && event.time) {
const timeDiff = playerElement.currentTime - event.time;
// if (timeDiff > 5 || timeDiff < -5) {
// player.currentTime = event.time;
// }
// } else if (
// event.type === 'playlist' &&
// event.playlistId &&
// event.playlistId !== data.playlistId
// ) {
// data.playlistId = event.playlistId;
// await loadPlaylist(event.playlistId);
// goToCurrentPlaylistItem();
// }
// });
// });
if (timeDiff > 5 || timeDiff < -5) {
playerElement.currentTime = event.time;
}
} else if (
event.type === 'playlist' &&
event.playlistId &&
event.playlistId !== data.playlistId
) {
data.playlistId = event.playlistId;
await loadPlaylist(event.playlistId);
goToCurrentPlaylistItem();
}
});
});
// if (!player) return;
if (!playerElement) return;
// player.addEventListener('auto-play-fail', () => {
// conn.send({
// events: [
// {
// type: 'pause'
// }
// ]
// } as PlayerEvents);
// });
playerElement.addEventListener('error', () => {
if (!playerElement) return;
conn.send({
events: [
{
type: 'seek',
time: playerElement.currentTime
},
{
type: 'play'
}
]
} as PlayerEvents);
});
// player.addEventListener('auto-play', () => {
// if (!player) return;
// conn.send({
// events: [
// {
// type: 'seek',
// time: player.currentTime
// },
// {
// type: 'play'
// }
// ]
// } as PlayerEvents);
// });
playerElement.addEventListener('pause', () => {
conn.send({
events: [
{
type: 'pause'
}
]
} as PlayerEvents);
});
// player.addEventListener('pause', () => {
// conn.send({
// events: [
// {
// type: 'pause'
// }
// ]
// } as PlayerEvents);
// });
playerElement.addEventListener('playing', () => {
if (!playerElement) return;
conn.send({
events: [
{
type: 'seek',
time: playerElement.currentTime
},
{
type: 'play'
}
]
} as PlayerEvents);
});
// player.addEventListener('playing', () => {
// if (!player) return;
// conn.send({
// events: [
// {
// type: 'seek',
// time: player.currentTime
// },
// {
// type: 'play'
// }
// ]
// } as PlayerEvents);
// });
playerElement.addEventListener('play', () => {
if (!playerElement) return;
conn.send({
events: [
{
type: 'seek',
time: playerElement.currentTime
},
{
type: 'play'
}
]
} as PlayerEvents);
});
// player.addEventListener('play', () => {
// if (!player) return;
// conn.send({
// events: [
// {
// type: 'seek',
// time: player.currentTime
// },
// {
// type: 'play'
// }
// ]
// } as PlayerEvents);
// });
playerElement.addEventListener('waiting', () => {
conn.send({
events: [
{
type: 'pause'
}
]
} as PlayerEvents);
});
// player.addEventListener('waiting', () => {
// conn.send({
// events: [
// {
// type: 'pause'
// }
// ]
// } as PlayerEvents);
// });
playerElement.addEventListener('seeked', () => {
if (!playerElement) return;
conn.send({
events: [
{
type: 'seek',
time: playerElement.currentTime
}
]
} as PlayerEvents);
});
}
// player.addEventListener('seeked', () => {
// if (!player) return;
// conn.send({
// events: [
// {
// type: 'seek',
// time: player.currentTime
// }
// ]
// } as PlayerEvents);
// });
// }
syncPartyConnectionsStore.subscribe((connections) => {
if (!connections || !playerElement) return;
playerSyncEvents(connections[connections.length - 1]);
});
// syncPartyConnectionsStore.subscribe((connections) => {
// if (!connections || !player) return;
// playerSyncEvents(connections[connections.length - 1]);
// });
onMount(async () => {
if ($syncPartyConnectionsStore) {
$syncPartyConnectionsStore.forEach((conn) => {
playerSyncEvents(conn);
});
}
// onMount(async () => {
// if ($syncPartyConnectionsStore) {
// $syncPartyConnectionsStore.forEach((conn) => {
// playerSyncEvents(conn);
// });
// }
if (playerElement) {
playerElement.addEventListener('timeupdate', () => {
playerCurrentTime = playerElement.currentTime;
});
// if (player) {
// player.addEventListener('end', async () => {
// if (playlistVideos.length === 0) {
// if ($playerAutoplayNextByDefaultStore) {
// goto(`/watch/${data.video.recommendedVideos[0].videoId}`);
// }
// return;
// }
playerElement.addEventListener('end', async () => {
if (playlistVideos.length === 0) {
if ($playerAutoplayNextByDefaultStore) {
goto(`/watch/${data.video.recommendedVideos[0].videoId}`);
}
return;
}
// await goToCurrentPlaylistItem();
await goToCurrentPlaylistItem();
// const playlistVideoIds = playlistVideos.map((value) => {
// return value.videoId;
// });
const playlistVideoIds = playlistVideos.map((value) => {
return value.videoId;
});
// let goToVideo: PlaylistPageVideo | undefined;
let goToVideo: PlaylistPageVideo | undefined;
// if (shufflePlaylist) {
// goToVideo = unsafeRandomItem(playlistVideos);
// } else {
// const currentVideoIndex = playlistVideoIds.indexOf(data.video.videoId);
// const newIndex = currentVideoIndex + 1;
// if (currentVideoIndex !== -1 && newIndex < playlistVideoIds.length) {
// goToVideo = playlistVideos[newIndex];
// } else if (loopPlaylist) {
// // Loop playlist on end
// goToVideo = playlistVideos[0];
// }
// }
if (shufflePlaylist) {
goToVideo = unsafeRandomItem(playlistVideos);
} else {
const currentVideoIndex = playlistVideoIds.indexOf(data.video.videoId);
const newIndex = currentVideoIndex + 1;
if (currentVideoIndex !== -1 && newIndex < playlistVideoIds.length) {
goToVideo = playlistVideos[newIndex];
} else if (loopPlaylist) {
// Loop playlist on end
goToVideo = playlistVideos[0];
}
}
// if (typeof goToVideo !== 'undefined') {
// if ($syncPartyConnectionsStore) {
// $syncPartyConnectionsStore.forEach((conn) => {
// if (typeof goToVideo === 'undefined') return;
if (typeof goToVideo !== 'undefined') {
if ($syncPartyConnectionsStore) {
$syncPartyConnectionsStore.forEach((conn) => {
if (typeof goToVideo === 'undefined') return;
// conn.send({
// events: [
// { type: 'change-video', videoId: goToVideo.videoId },
// { type: 'playlist', playlistId: data.playlistId }
// ]
// } as PlayerEvents);
// });
// }
conn.send({
events: [
{ type: 'change-video', videoId: goToVideo.videoId },
{ type: 'playlist', playlistId: data.playlistId }
]
} as PlayerEvents);
});
}
// goto(`/watch/${goToVideo.videoId}?playlist=${data.playlistId}`);
// }
// });
// }
goto(`/watch/${goToVideo.videoId}?playlist=${data.playlistId}`);
}
});
}
// if (!data.playlistId) return;
if (!data.playlistId) return;
// await loadPlaylist(data.playlistId);
await loadPlaylist(data.playlistId);
// await goToCurrentPlaylistItem();
// });
await goToCurrentPlaylistItem();
});
onDestroy(() => {
// Reset title when page left.
@@ -406,7 +407,13 @@
<div class={`s12 m12 l${theatreMode ? '12' : '9'}`}>
<div style="display: flex;justify-content: center;">
{#key data.video.videoId}
<Player bind:segments {data} {audioMode} isSyncing={$syncPartyPeerStore !== null} />
<Player
bind:playerElement
bind:segments
{data}
{audioMode}
isSyncing={$syncPartyPeerStore !== null}
/>
{/key}
</div>
@@ -586,7 +593,9 @@
{#if data.content.timestamps.length > 0}
<h6 style="margin-bottom: .3em;">Chapters</h6>
{#each data.content.timestamps as timestamp}
<button onclick={() => (player.currentTime = timestamp.time)} class="timestamps"
<button
onclick={() => (playerElement.currentTime = timestamp.time)}
class="timestamps"
>{timestamp.timePretty}
{#if !timestamp.title.startsWith('-')}
-
@@ -635,7 +644,7 @@
{#if !theatreMode}
<div class="s12 m12 l3">
{#if showTranscript}
<Transcript video={data.video} />
<Transcript video={data.video} bind:playerElement />
{/if}
{#if playlist}
<article