Files
materialious/materialious/src/lib/components/Player.svelte
T
2025-05-30 23:22:12 +12:00

1012 lines
31 KiB
Svelte

<script lang="ts">
import { page } from '$app/stores';
import '$lib/css/shaka-player-theme.css';
import { getBestThumbnail } from '$lib/images';
import { padTime, videoLength } from '$lib/numbers';
import { type PhasedDescription } from '$lib/timestamps';
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 { base64ToU8, Protos } from 'googlevideo';
import ui from 'beercss';
import ISO6391 from 'iso-639-1';
import Mousetrap from 'mousetrap';
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 {
authStore,
instanceStore,
playerAndroidLockOrientation,
playerAutoPlayStore,
playerDefaultLanguage,
playerProxyVideosStore,
playerSavePlaybackPositionStore,
poTokenCacheStore,
sponsorBlockCategoriesStore,
sponsorBlockDisplayToastStore,
sponsorBlockStore,
sponsorBlockUrlStore,
synciousInstanceStore,
synciousStore
} from '../store';
import { getDynamicTheme, setStatusBarColor } from '../theme';
import { HttpFetchPlugin, type SabrStreamingContext } from '$lib/sabr/shakaHttpPlugin';
import { Constants, type Misc } from 'youtubei.js';
import {
fromFormat,
fromFormatInitializationMetadata,
fromMediaHeader,
getUniqueFormatId
} from '$lib/sabr/formatKeyUtils';
interface Props {
data: { video: VideoPlay; content: PhasedDescription; playlistId: string | null };
isSyncing?: boolean;
isEmbed?: boolean;
segments?: Segment[];
playerElement: HTMLMediaElement | undefined;
}
let {
data,
isEmbed = false,
segments = $bindable([]),
playerElement = $bindable()
}: Props = $props();
let snackBarAlert = $state('');
let playerPosSet = false;
let originalOrigination: ScreenOrientationResult | undefined;
let watchProgressTimeout: NodeJS.Timeout;
let userWantsFullscreen = false;
let player: shaka.Player;
let shakaUi: shaka.ui.Overlay;
// Shaka player required variables for SABR based off LuanRT's example
const sessionId = Array.from(Array(16), () => Math.floor(Math.random() * 36).toString(36)).join(
''
);
let isLive = false;
let videoPlaybackUstreamerConfig: string | undefined;
let serverAbrStreamingUrl: URL | undefined = undefined;
let drmParams: string | undefined;
let lastRequestMs = 0;
let lastSeekMs = 0;
let lastManualFormatSelectionMs = 0;
let lastActionMs = 0;
let playerElementResizeObserver: ResizeObserver | undefined;
let clientViewportHeight = playerElement?.clientHeight || 0;
let clientViewportWidth = playerElement?.clientWidth || 0;
let lastPlaybackCookie: Protos.PlaybackCookie | undefined;
const initializedFormats = new Map<
string,
{
lastSegmentMetadata?: {
formatId: Protos.FormatId;
startTimeMs: number;
startSequenceNumber: number;
endSequenceNumber: number;
durationMs: number;
};
formatInitializationMetadata: Protos.FormatInitializationMetadata;
}
>();
let formatList: Misc.Format[] = [];
shaka.net.NetworkingEngine.registerScheme(
'http',
HttpFetchPlugin.parse,
shaka.net.NetworkingEngine.PluginPriority.PREFERRED
);
shaka.net.NetworkingEngine.registerScheme(
'https',
HttpFetchPlugin.parse,
shaka.net.NetworkingEngine.PluginPriority.PREFERRED
);
// Shaka player SABR var end
const STORAGE_KEY_QUALITY = 'shaka-preferred-quality';
const STORAGE_KEY_VOLUME = 'shaka-preferred-volume';
function saveQualityPreference() {
const tracks = player.getVariantTracks();
const selectedTrack = tracks.find((track) => track.active);
if (selectedTrack) {
localStorage.setItem(STORAGE_KEY_QUALITY, selectedTrack.bandwidth.toString());
}
}
function saveVolumePreference() {
if (!playerElement) return;
localStorage.setItem(STORAGE_KEY_VOLUME, playerElement.volume.toString());
}
function restoreQualityPreference() {
const savedQuality =
localStorage.getItem(STORAGE_KEY_QUALITY) ??
(import.meta.env.VITE_DEFAULT_DASH_BITRATE as string | undefined);
if (savedQuality) {
const qualityBandwidth = parseInt(savedQuality);
const tracks = player.getVariantTracks();
let preferredTrack = tracks.find((track) => track.bandwidth === qualityBandwidth);
if (!preferredTrack) {
preferredTrack = tracks.reduce((prev, curr) =>
Math.abs(curr.bandwidth - qualityBandwidth) < Math.abs(prev.bandwidth - qualityBandwidth)
? curr
: prev
);
}
if (preferredTrack) {
player.selectVariantTrack(preferredTrack, true);
}
}
}
function loadTimeFromUrl(page: Page): boolean {
if (player) {
const timeGivenUrl = page.url.searchParams.get('time');
if (timeGivenUrl && !isNaN(parseFloat(timeGivenUrl))) {
if (!playerElement) return false;
playerElement.currentTime = Number(timeGivenUrl);
return true;
}
}
return false;
}
page.subscribe((pageUpdate) => loadTimeFromUrl(pageUpdate));
const proxyVideos = get(playerProxyVideosStore);
onMount(async () => {
shaka.polyfill.installAll();
if (!shaka.Player.isBrowserSupported()) {
return;
}
HttpFetchPlugin.cacheManager.clearCache();
player = new shaka.Player();
player.configure({
abr: {
enabled: true,
restrictions: {
maxWidth: 1920,
maxHeight: 1080
}
},
streaming: {
bufferingGoal: 120,
rebufferingGoal: 0.01,
bufferBehind: 300,
retryParameters: {
maxAttempts: 30,
baseDelay: 1500,
backoffFactor: 2.5,
fuzzFactor: 0.7,
timeout: 120000
},
stallThreshold: 2,
stallSkip: 0.5
}
});
playerElement = document.getElementById('player') as HTMLMediaElement;
playerElementResizeObserver = new ResizeObserver(() => {
if (playerElement) {
clientViewportHeight = playerElement.clientHeight;
clientViewportWidth = playerElement.clientWidth;
}
});
playerElementResizeObserver.observe(playerElement);
// Change instantly to stop video from being loud for a second
const savedVolume = localStorage.getItem(STORAGE_KEY_VOLUME);
if (savedVolume) {
playerElement.volume = parseFloat(savedVolume);
}
shakaUi = new shaka.ui.Overlay(
player,
document.getElementById('shaka-container') as HTMLElement,
playerElement
);
shakaUi.configure({
controlPanelElements: [
'play_pause',
Capacitor.getPlatform() === 'android' ? '' : 'volume',
'spacer',
'chapter',
'time_and_duration',
'captions',
'overflow_menu',
'fullscreen'
],
overflowMenuButtons: [
'cast',
'airplay',
'captions',
'quality',
'playback_rate',
'loop',
'language',
'statistics'
],
enableTooltips: true,
seekBarColors: {
played: (await getDynamicTheme())['--primary']
}
});
if (data.video.fallbackPatch === 'youtubejs' && data.video.ytjs) {
isLive = !!data.video.ytjs.video.basic_info.is_live;
if (
data.video.ytjs.rawApiResponse.data.streamingData &&
(data.video.ytjs.rawApiResponse.data.streamingData as any).drmParams
) {
drmParams = (data.video.ytjs.rawApiResponse.data.streamingData as any).drmParams;
player.configure({
drm: {
servers: {
'com.widevine.alpha':
'https://www.youtube.com/youtubei/v1/player/get_drm_license?alt=json'
}
}
});
}
videoPlaybackUstreamerConfig =
data.video.ytjs.video.page[0].player_config?.media_common_config
.media_ustreamer_request_config?.video_playback_ustreamer_config;
if (data.video.ytjs.video.streaming_data) {
formatList = data.video.ytjs.video.streaming_data.adaptive_formats;
}
if (data.video.ytjs.video.streaming_data?.server_abr_streaming_url)
serverAbrStreamingUrl = new URL(
data.video.ytjs.innertube.session.player!.decipher(
data.video.ytjs.video.streaming_data.server_abr_streaming_url
)
);
playerElement.addEventListener('seeked', () => (lastSeekMs = Date.now()));
player.addEventListener('variantchanged', (event) => {
// Technically, all variant changes here are manual, given we don't handle ABR updates from the server.
if (event.type !== 'variant') {
lastManualFormatSelectionMs = Date.now();
}
lastActionMs = Date.now();
});
player.addEventListener('error', (event) => {
const error = (event as CustomEvent).detail as shaka.util.Error;
console.error('Player error:', error);
});
const networkingEngine = player.getNetworkingEngine();
if (!networkingEngine) return;
// Required to stop buttons from being still selected when fullscreening
document.addEventListener('fullscreenchange', async () => {
userWantsFullscreen = document.fullscreenElement !== null;
const buttons = document.querySelectorAll('.shaka-controls-button-panel button');
buttons.forEach((button) => {
// Reset the button's focus and active states
(button as HTMLElement).blur(); // Remove focus from the button
button.removeAttribute('aria-pressed'); // Reset any ARIA attributes that might indicate selection
});
});
// Based off the following
// https://github.com/FreeTubeApp/FreeTube/blob/d270c9e251a433f1e4246a3f6a37acef707d22aa/src/renderer/components/ft-shaka-video-player/ft-shaka-video-player.js#L1206
// https://github.com/LuanRT/BgUtils/blob/6b121166be1ccb0b952dee1bdac488808365ae6b/examples/browser/web/src/main.ts#L293
// https://github.com/LuanRT/yt-sabr-shaka-demo/blob/main/src/components/VideoPlayer.vue
networkingEngine.registerRequestFilter(async (type, request, context) => {
if (!player) return;
const originalUrl = new URL(request.uris[0]);
let url = originalUrl.hostname === 'sabr' ? serverAbrStreamingUrl : originalUrl;
const headers = request.headers;
if (!url) return;
if (
type === shaka.net.NetworkingEngine.RequestType.SEGMENT &&
url.pathname.includes('videoplayback')
) {
if (!isLive) {
const currentFormat = formatList.find(
(format) =>
fromFormat(format) === (new URL(request.uris[0]).searchParams.get('___key') || '')
);
if (!videoPlaybackUstreamerConfig) throw new Error('Ustreamer config not found.');
if (!currentFormat) throw new Error('No format found for SABR request.');
if (!playerElement) throw new Error('No video element found.');
const activeVariant = player
.getVariantTracks()
.find(
(track) =>
getUniqueFormatId(currentFormat) ===
(currentFormat.has_video ? track.originalVideoId : track.originalAudioId)
);
let videoFormat: Misc.Format | undefined;
let audioFormat: Misc.Format | undefined;
let videoFormatId: Protos.FormatId | undefined;
let audioFormatId: Protos.FormatId | undefined;
if (activeVariant) {
for (const fmt of formatList) {
const uniqueFormatId = getUniqueFormatId(fmt);
if (uniqueFormatId === activeVariant.originalVideoId) {
videoFormat = fmt;
} else if (uniqueFormatId === activeVariant.originalAudioId) {
audioFormat = fmt;
}
}
}
if (videoFormat) {
videoFormatId = {
itag: videoFormat.itag,
lastModified: parseInt(videoFormat.last_modified_ms),
xtags: videoFormat.xtags
};
}
if (audioFormat) {
audioFormatId = {
itag: audioFormat.itag,
lastModified: parseInt(audioFormat.last_modified_ms),
xtags: audioFormat.xtags
};
}
const isInit = context ? !context.segment : true;
const videoPlaybackAbrRequest: Protos.VideoPlaybackAbrRequest = {
clientAbrState: {
playbackRate: player.getPlaybackRate(),
playerTimeMs: Math.round(
(context?.segment?.getStartTime() ?? playerElement.currentTime) * 1000
),
elapsedWallTimeMs: Date.now() - lastRequestMs,
timeSinceLastSeek: lastSeekMs === 0 ? 0 : Date.now() - lastSeekMs,
timeSinceLastActionMs: lastActionMs === 0 ? 0 : Date.now() - lastActionMs,
timeSinceLastManualFormatSelectionMs:
lastManualFormatSelectionMs === 0 ? 0 : Date.now() - lastManualFormatSelectionMs,
clientViewportIsFlexible: false,
bandwidthEstimate: Math.round(player.getStats().estimatedBandwidth),
drcEnabled: currentFormat.is_drc === true,
enabledTrackTypesBitfield: currentFormat.has_audio ? 1 : 2,
clientViewportHeight,
clientViewportWidth
},
bufferedRanges: [],
selectedFormatIds: [],
selectedAudioFormatIds: [audioFormatId || {}],
selectedVideoFormatIds: [videoFormatId || {}],
videoPlaybackUstreamerConfig: base64ToU8(videoPlaybackUstreamerConfig),
streamerContext: {
poToken: base64ToU8(get(poTokenCacheStore) ?? ''),
playbackCookie: lastPlaybackCookie
? Protos.PlaybackCookie.encode(lastPlaybackCookie).finish()
: undefined,
clientInfo: {
clientName: parseInt(Constants.CLIENT_NAME_IDS.WEB),
clientVersion: data.video.ytjs?.innertube.session.context.client.clientVersion,
osName: 'Windows',
osVersion: '10.0'
},
field5: [],
field6: []
},
field1000: []
};
// Normalize the resolution.
if (currentFormat.width && currentFormat.height) {
let resolution = currentFormat.height;
const aspectRatio = currentFormat.height / currentFormat.width;
if (aspectRatio > 16 / 9) {
resolution = Math.round((currentFormat.width * 9) / 16);
}
if (resolution && videoPlaybackAbrRequest.clientAbrState) {
videoPlaybackAbrRequest.clientAbrState.stickyResolution = resolution;
videoPlaybackAbrRequest.clientAbrState.lastManualSelectedResolution = resolution;
}
}
if (!isInit) {
// Add the currently/previously active formats to the list of buffered ranges and selected formats
// so that the server doesn't send its init data again.
const initializedFormatsArray = Array.from(initializedFormats.values());
for (const initializedFormat of initializedFormatsArray) {
if (initializedFormat.lastSegmentMetadata) {
videoPlaybackAbrRequest.bufferedRanges.push({
formatId: initializedFormat.lastSegmentMetadata.formatId,
startSegmentIndex: initializedFormat.lastSegmentMetadata.startSequenceNumber,
durationMs: initializedFormat.lastSegmentMetadata.durationMs,
startTimeMs: 0,
endSegmentIndex: initializedFormat.lastSegmentMetadata.endSequenceNumber
});
}
}
if (audioFormatId) videoPlaybackAbrRequest.selectedFormatIds.push(audioFormatId);
if (videoFormatId) videoPlaybackAbrRequest.selectedFormatIds.push(videoFormatId);
}
request.body = Protos.VideoPlaybackAbrRequest.encode(videoPlaybackAbrRequest).finish();
const byteRange = headers.Range
? {
start: Number(headers.Range.split('=')[1].split('-')[0]),
end: Number(headers.Range.split('=')[1].split('-')[1])
}
: null;
const sabrStreamingContext = {
byteRange,
format: currentFormat,
isInit,
isUMP: true,
isSABR: true,
playerTimeMs: videoPlaybackAbrRequest.clientAbrState?.playerTimeMs
};
// @NOTE: Not a real header. See the http plugin code for more info.
request.headers['X-Streaming-Context'] = btoa(JSON.stringify(sabrStreamingContext));
delete headers.Range;
} else {
url.pathname += '/ump/1';
url.pathname += '/srfvp/1';
request.headers['X-Streaming-Context'] = btoa(
JSON.stringify({
isUMP: true,
isSABR: false
})
);
url.pathname += '/pot/' + get(poTokenCacheStore);
}
request.method = 'POST';
} else if (type == shaka.net.NetworkingEngine.RequestType.LICENSE) {
const wrapped = {} as Record<string, any>;
wrapped.context = data.video.ytjs?.innertube.session.context;
wrapped.cpn = data.video.ytjs?.clientPlaybackNonce;
wrapped.drmParams = decodeURIComponent(drmParams || '');
wrapped.drmSystem = 'DRM_SYSTEM_WIDEVINE';
wrapped.drmVideoFeature = 'DRM_VIDEO_FEATURE_SDR';
wrapped.licenseRequest = shaka.util.Uint8ArrayUtils.toBase64(
request.body as ArrayBuffer | ArrayBufferView
);
wrapped.sessionId = sessionId;
wrapped.videoId = data.video.videoId;
request.body = shaka.util.StringUtils.toUTF8(JSON.stringify(wrapped));
}
request.uris[0] = url.toString();
});
networkingEngine.registerResponseFilter(async (type, response, context) => {
if (type === shaka.net.NetworkingEngine.RequestType.SEGMENT) {
const url = new URL(response.uri);
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, '');
/* @ts-ignore */
response.data = new TextEncoder().encode(cleaned).buffer;
} else {
const sabrStreamingContext = response.headers['X-Streaming-Context'];
if (sabrStreamingContext) {
const { streamInfo, format, byteRange } = JSON.parse(
atob(sabrStreamingContext)
) as SabrStreamingContext;
if (streamInfo) {
const sabrRedirect = streamInfo.redirect;
const playbackCookie = streamInfo.playbackCookie;
const streamProtectionStatus = streamInfo.streamProtectionStatus;
const formatInitMetadata = streamInfo.formatInitMetadata || [];
const mainSegmentMediaHeader = streamInfo.mediaHeader;
// If we have a redirect, follow it.
if (sabrRedirect?.url && !response.data.byteLength) {
let redirectUrl = new URL(sabrRedirect.url);
serverAbrStreamingUrl = redirectUrl;
redirectUrl = new URL(`https://sabr?___key=${fromFormat(format) || ''}`);
const retryParameters = player!.getConfiguration().streaming.retryParameters;
const redirectRequest = shaka.net.NetworkingEngine.makeRequest(
[redirectUrl.toString()],
retryParameters
);
// Keep range so we can slice the response (only if it's the init segment).
if (typeof byteRange !== 'undefined') {
redirectRequest.headers['Range'] = `bytes=${byteRange.start}-${byteRange.end}`;
}
const requestOperation = player!
.getNetworkingEngine()
?.request(type, redirectRequest, context);
const redirectResponse = await requestOperation!.promise;
// Modify the original response to contain the results of the redirect
// response.
Object.assign(response, redirectResponse);
return;
}
if (playbackCookie) lastPlaybackCookie = streamInfo.playbackCookie;
if (streamProtectionStatus && streamProtectionStatus.status === 3) {
console.warn('[UMP] Attestation required.');
}
for (const metadata of formatInitMetadata) {
const key = fromFormatInitializationMetadata(metadata);
if (!initializedFormats.has(key)) {
initializedFormats.set(key, {
formatInitializationMetadata: metadata
});
console.log(`[SABR] Initialized format ${key}`);
}
}
if (mainSegmentMediaHeader) {
const formatKey = fromMediaHeader(mainSegmentMediaHeader);
const initializedFormat = initializedFormats.get(formatKey);
if (initializedFormat) {
initializedFormat.lastSegmentMetadata = {
formatId: mainSegmentMediaHeader.formatId!,
startTimeMs: mainSegmentMediaHeader.startMs || 0,
startSequenceNumber: mainSegmentMediaHeader.sequenceNumber || 1,
endSequenceNumber: mainSegmentMediaHeader.sequenceNumber || 1,
durationMs: mainSegmentMediaHeader.durationMs || 0
};
}
}
}
}
}
} else if (type == shaka.net.NetworkingEngine.RequestType.LICENSE) {
const wrappedString = shaka.util.StringUtils.fromUTF8(response.data);
const wrapped = JSON.parse(wrappedString);
const rawLicenseBase64 = wrapped.license;
response.data = shaka.util.Uint8ArrayUtils.fromBase64(rawLicenseBase64);
}
});
}
await player.attach(playerElement);
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(
caption.url.startsWith('http') ? caption.url : `${get(instanceStore)}${caption.url}`,
caption.language_code,
'captions',
undefined,
undefined,
caption.label
);
});
}
if (data.content.timestamps) {
let chapterWebVTT = 'WEBVTT\n\n';
let timestampIndex = 0;
data.content.timestamps.forEach((timestamp) => {
let endTime: string;
if (timestampIndex === data.content.timestamps.length - 1) {
endTime = videoLength(data.video.lengthSeconds);
} else {
endTime = data.content.timestamps[timestampIndex + 1].timePretty;
}
chapterWebVTT += `${padTime(timestamp.timePretty)} --> ${padTime(endTime)}\n${timestamp.title.replaceAll('-', '').trim()}\n\n`;
timestampIndex += 1;
});
if (timestampIndex > 0) {
player.addChaptersTrack(
`data:text/vtt;base64,${btoa(chapterWebVTT)}`,
get(playerDefaultLanguage)
);
}
}
// Auto save watch progress every minute.
watchProgressTimeout = setInterval(() => savePlayerPos(), 60000);
if (get(sponsorBlockStore) && get(sponsorBlockCategoriesStore)) {
const currentCategories = get(sponsorBlockCategoriesStore);
const sponsorBlockUrl = get(sponsorBlockUrlStore);
if (currentCategories.length > 0 && sponsorBlockUrl && sponsorBlockUrl !== '') {
const sponsorBlock = new SponsorBlock('', { baseURL: sponsorBlockUrl });
try {
segments = await sponsorBlock.getSegments(
data.video.videoId,
get(sponsorBlockCategoriesStore) as Category[]
);
playerElement.addEventListener('timeupdate', () => {
segments.forEach((segment) => {
if (!playerElement) return;
if (
playerElement.currentTime >= segment.startTime &&
playerElement.currentTime <= segment.endTime
) {
if (Math.round(playerElement.currentTime) >= Math.round(playerElement.duration)) {
return;
}
playerElement.currentTime = segment.endTime + 1;
if (!get(sponsorBlockDisplayToastStore)) {
snackBarAlert = `${get(_)('skipping')} ${segment.category}`;
ui('#snackbar-alert');
}
}
});
});
} catch {}
}
}
await loadPlayerPos();
if (Capacitor.getPlatform() === 'android' && data.video.adaptiveFormats.length > 0) {
const videoFormats = data.video.adaptiveFormats.filter((format) =>
format.type.startsWith('video/')
);
originalOrigination = await ScreenOrientation.orientation();
document.addEventListener('fullscreenchange', async () => {
const isFullScreen = !!document.fullscreenElement;
if (isFullScreen) {
// Ensure bar color is black while in fullscreen
await StatusBar.setBackgroundColor({ color: '#000000' });
await NavigationBar.setColor({
color: '#000000'
});
await StatusBar.setStyle({ style: Style.Dark });
} else {
await setStatusBarColor();
}
if (!get(playerAndroidLockOrientation)) return;
if (isFullScreen && videoFormats[0].resolution) {
const widthHeight = videoFormats[0].resolution.split('x');
if (widthHeight.length !== 2) return;
if (Number(widthHeight[0]) > Number(widthHeight[1])) {
await StatusBar.setOverlaysWebView({ overlay: true });
await StatusBar.hide();
await NavigationBar.hide();
await ScreenOrientation.lock({ orientation: 'landscape' });
} else {
await ScreenOrientation.lock({ orientation: 'portrait' });
}
} else {
await StatusBar.setOverlaysWebView({ overlay: false });
await StatusBar.show();
await NavigationBar.show();
await ScreenOrientation.lock({
orientation: (originalOrigination as ScreenOrientationResult).type
});
}
});
}
} else {
if (data.video.fallbackPatch === 'youtubejs') {
await player.load(data.video.dashUrl);
} else {
await player.load(data.video.hlsUrl + '?local=true');
}
}
if (data.video.fallbackPatch === 'youtubejs') {
snackBarAlert = get(_)('player.youtubeJsFallBack');
ui('#snackbar-alert');
}
player.addEventListener('buffering', saveQualityPreference);
playerElement.addEventListener('volumechange', saveVolumePreference);
player.addEventListener('loaded', () => {
restoreQualityPreference();
const defaultLanguage = get(playerDefaultLanguage);
if (defaultLanguage) {
const audioLanguages = player.getAudioLanguages();
const langCode = ISO6391.getCode(defaultLanguage);
for (const audioLanguage of audioLanguages) {
if (audioLanguage.startsWith(langCode)) {
player.selectAudioLanguage(audioLanguage);
break;
}
}
}
});
const overflowMenuButton = document.querySelector('.shaka-overflow-menu-button');
if (overflowMenuButton) {
overflowMenuButton.innerHTML = 'settings';
}
const backToOverflowButton = document.querySelector('.shaka-back-to-overflow-button');
if (backToOverflowButton) {
backToOverflowButton.innerHTML = 'arrow_back_ios_new';
}
Mousetrap.bind('space', () => {
if (!playerElement) return;
if (playerElement.paused) {
playerElement.play();
} else {
playerElement.pause();
}
if (!document.fullscreenElement && userWantsFullscreen) {
shakaUi.getControls()?.toggleFullScreen();
}
return false;
});
Mousetrap.bind('right', () => {
if (!playerElement) return;
playerElement.currentTime = playerElement.currentTime + 10;
if (!document.fullscreenElement && userWantsFullscreen) {
shakaUi.getControls()?.toggleFullScreen();
}
return false;
});
Mousetrap.bind('left', () => {
if (!playerElement) return;
playerElement.currentTime = playerElement.currentTime - 10;
if (!document.fullscreenElement && userWantsFullscreen) {
shakaUi.getControls()?.toggleFullScreen();
}
return false;
});
Mousetrap.bind('c', () => {
const isVisible = player.isTextTrackVisible();
if (isVisible) {
player.setTextTrackVisibility(false);
} else {
const defaultLanguage = get(playerDefaultLanguage);
const langCode = ISO6391.getCode(defaultLanguage);
const tracks = player.getTextTracks();
const subtitleTrack = tracks.find((track) => track.language === langCode);
if (subtitleTrack) {
player.selectTextTrack(subtitleTrack);
player.setTextTrackVisibility(true);
}
}
if (!document.fullscreenElement && userWantsFullscreen) {
shakaUi.getControls()?.toggleFullScreen();
}
return false;
});
Mousetrap.bind('f', () => {
if (document.fullscreenElement) {
document.exitFullscreen();
} else {
shakaUi.getControls()?.toggleFullScreen();
}
return false;
});
Mousetrap.bind('shift+left', () => {
if (!playerElement) return;
playerElement.playbackRate = playerElement.playbackRate - 0.25;
if (!document.fullscreenElement && userWantsFullscreen) {
shakaUi.getControls()?.toggleFullScreen();
}
});
Mousetrap.bind('shift+right', () => {
if (!playerElement) return;
playerElement.playbackRate = playerElement.playbackRate + 0.25;
if (!document.fullscreenElement && userWantsFullscreen) {
shakaUi.getControls()?.toggleFullScreen();
}
});
});
async function loadPlayerPos() {
if (playerPosSet) return;
playerPosSet = true;
if (loadTimeFromUrl($page)) return;
let toSetTime = 0;
if (get(synciousStore) && get(synciousInstanceStore) && get(authStore)) {
try {
toSetTime = (await getVideoProgress(data.video.videoId))[0].time;
} catch {}
} else {
if (get(playerSavePlaybackPositionStore)) {
try {
const playerPos = localStorage.getItem(`v_${data.video.videoId}`);
if (playerPos && Number(playerPos) > toSetTime) {
toSetTime = Number(playerPos);
}
} catch {}
}
}
if (toSetTime > 0 && playerElement) playerElement.currentTime = toSetTime;
}
function savePlayerPos() {
if (data.video.hlsUrl) return;
if (
get(playerSavePlaybackPositionStore) &&
player &&
playerElement &&
playerElement.currentTime
) {
if (
playerElement.currentTime < playerElement.duration - 10 &&
playerElement.currentTime > 10
) {
try {
localStorage.setItem(`v_${data.video.videoId}`, playerElement.currentTime.toString());
} catch {}
if (get(synciousStore) && get(synciousInstanceStore) && get(authStore)) {
saveVideoProgress(data.video.videoId, playerElement.currentTime);
}
} else {
try {
localStorage.removeItem(`v_${data.video.videoId}`);
} catch {}
if (get(synciousStore) && get(synciousInstanceStore) && get(authStore)) {
deleteVideoProgress(data.video.videoId);
}
}
}
}
onDestroy(async () => {
if (Capacitor.getPlatform() === 'android') {
if (originalOrigination) {
await StatusBar.setOverlaysWebView({ overlay: false });
await StatusBar.show();
await ScreenOrientation.lock({
orientation: originalOrigination.type
});
}
}
if (watchProgressTimeout) {
clearTimeout(watchProgressTimeout);
}
try {
savePlayerPos();
} catch (error) {}
playerPosSet = false;
HttpFetchPlugin.cacheManager.clearCache();
if (playerElementResizeObserver) {
playerElementResizeObserver.disconnect();
}
if (playerElement) {
playerElement.src = '';
playerElement.load();
}
if (player) {
player.destroy();
}
if (shakaUi) {
shakaUi.destroy();
}
});
</script>
<div
id="shaka-container"
class="youtube-theme"
style="max-height: 80vh; max-width: calc(80vh * 16 / 9); overflow: hidden; position: relative; flex: 1; background-color: black;"
data-shaka-player-container
>
<video
controls={false}
autoplay={$playerAutoPlayStore}
id="player"
style="width: 100%; height: 100%; object-fit: contain;"
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>
</div>
{/if}