Fixes to ytjs fallback
This commit is contained in:
Generated
+88
@@ -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>
|
||||
|
||||
Generated
+453
-946
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user