mirror of
https://github.com/iv-org/invidious.git
synced 2026-08-17 05:20:51 -05:00
chore: fix some usability
This commit is contained in:
parent
240c8f0f22
commit
ce6d2d6d7e
@ -72,7 +72,14 @@
|
||||
}
|
||||
|
||||
.shaka-bottom-controls {
|
||||
background: linear-gradient(to top, rgba(0, 0, 0, 0.6), rgba(0, 0, 0, 0));
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* Control-contrast gradient goes on the SCRIM, which Shaka fades in/out together with the
|
||||
controls. (Putting it on .shaka-bottom-controls left a permanent black band at the bottom
|
||||
because that element stays visible when the controls auto-hide.) */
|
||||
.shaka-scrim-container {
|
||||
background: linear-gradient(to top, rgba(0, 0, 0, 0.55), rgba(0, 0, 0, 0));
|
||||
}
|
||||
|
||||
/* Buttons - match video.js colors */
|
||||
@ -249,8 +256,40 @@
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Hide Shaka's built-in buffering spinner — we drive our own (.sabr-loading-spinner),
|
||||
which also covers the pre-load SABR init. Otherwise both show at once. */
|
||||
.shaka-spinner-container {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Poster/thumbnail styling */
|
||||
.shaka-video-container .shaka-poster {
|
||||
background-size: cover;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
/* Loading spinner — shown while the container has .is-loading (SABR init + buffering).
|
||||
Driven by sabr_player.js (shaka's own spinner only covers its post-load buffering). */
|
||||
.sabr-loading-spinner {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
margin: -28px 0 0 -28px;
|
||||
border: 4px solid rgba(255, 255, 255, 0.25);
|
||||
border-top-color: rgba(0, 182, 240, 1);
|
||||
border-radius: 50%;
|
||||
animation: sabr-spin 0.9s linear infinite;
|
||||
z-index: 20;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.sabr-player-container.is-loading .sabr-loading-spinner {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@keyframes sabr-spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
@ -326,7 +326,10 @@
|
||||
for (var j = 0; j < storyboard.storyboardCount; j++) {
|
||||
var startTime = j * segmentDuration;
|
||||
var endTime = Math.min(startTime + segmentDuration, duration);
|
||||
var urls = [storyboard.templateUrl.replace('$M', String(j))];
|
||||
// NOTE: must be `let` (block-scoped). With `var` the closure below captures the
|
||||
// function-scoped binding, so every SegmentReference returns the LAST storyboard
|
||||
// image -> the same (mismatched, distorted) thumbnail for the whole video.
|
||||
let urls = [storyboard.templateUrl.replace('$M', String(j))];
|
||||
var segmentReference = new shaka.media.SegmentReference(
|
||||
startTime, endTime,
|
||||
function () { return urls; },
|
||||
|
||||
@ -60,6 +60,7 @@ var SABRPlayer = (function () {
|
||||
// per reload (loadVideo re-runs on every SABR reload).
|
||||
var requestFiltersInstalled = false;
|
||||
var loadedListenerInstalled = false;
|
||||
var keydownInstalled = false;
|
||||
var sabrManifest = null; // captured from player.getManifest() on 'loaded'
|
||||
var currentLoadOptions = null; // for onReloadOnce -> re-run loadVideo
|
||||
var reloadCount = 0; // cap reloads to prevent infinite loop on blocked/throttled videos
|
||||
@ -162,8 +163,9 @@ var SABRPlayer = (function () {
|
||||
|
||||
async function fetchOnesieConfig() {
|
||||
if (clientConfig && SABRHelpers.isConfigValid(clientConfig)) return clientConfig;
|
||||
var cached = SABRHelpers.loadCachedClientConfig();
|
||||
if (cached) { clientConfig = cached; return clientConfig; }
|
||||
// Do NOT read a persisted (localStorage) config: its baseUrl embeds the egress IP
|
||||
// that YouTube saw at fetch time, so a cached config goes stale when the proxy/egress
|
||||
// IP changes and the CDN rejects the mismatched IP. Always refetch fresh.
|
||||
try {
|
||||
var tvConfigResponse = await SABRHelpers.fetchWithProxy(
|
||||
'https://www.youtube.com/tv_config?action_get_config=true&client=lb4&theme=cl',
|
||||
@ -185,7 +187,7 @@ var SABRPlayer = (function () {
|
||||
baseUrl: onesieHotConfig.baseUrl,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
SABRHelpers.saveCachedClientConfig(clientConfig);
|
||||
// Not persisted to localStorage on purpose (baseUrl embeds a now-stale-on-proxy-change ip).
|
||||
return clientConfig;
|
||||
} catch (error) {
|
||||
console.error('[SABRPlayer]', 'Failed to fetch Onesie client config', error);
|
||||
@ -294,7 +296,8 @@ var SABRPlayer = (function () {
|
||||
shaka.polyfill.installAll();
|
||||
|
||||
shakaContainer = document.createElement('div');
|
||||
shakaContainer.className = 'sabr-player-container';
|
||||
// Start in the loading state so the spinner is visible during the initial SABR init.
|
||||
shakaContainer.className = 'sabr-player-container is-loading';
|
||||
shakaContainer.style.width = '100%';
|
||||
shakaContainer.style.height = '100%';
|
||||
|
||||
@ -305,6 +308,14 @@ var SABRPlayer = (function () {
|
||||
videoElement.style.backgroundColor = '#000';
|
||||
|
||||
shakaContainer.appendChild(videoElement);
|
||||
|
||||
// Loading spinner shown during our custom SABR init (Innertube/Onesie/PO token/load)
|
||||
// and whenever the video is buffering. Shaka's own spinner only covers its own
|
||||
// buffering phase (after player.load), not the pre-load init, so we drive our own.
|
||||
var spinnerEl = document.createElement('div');
|
||||
spinnerEl.className = 'sabr-loading-spinner';
|
||||
shakaContainer.appendChild(spinnerEl);
|
||||
|
||||
containerElement.appendChild(shakaContainer);
|
||||
|
||||
player = new shaka.Player();
|
||||
@ -329,11 +340,20 @@ var SABRPlayer = (function () {
|
||||
videoElement.addEventListener('volumechange', function () { saveVolume(videoElement.volume); });
|
||||
videoElement.addEventListener('playing', function () {
|
||||
player.configure('abr.restrictions.maxHeight', Infinity);
|
||||
hideSpinner();
|
||||
});
|
||||
videoElement.addEventListener('waiting', function () { showSpinner(); });
|
||||
videoElement.addEventListener('canplay', function () { hideSpinner(); });
|
||||
videoElement.addEventListener('pause', function () {
|
||||
if (currentVideoId) savePlaybackPosition(currentVideoId, videoElement.currentTime);
|
||||
});
|
||||
|
||||
// Page-level keyboard shortcuts (installed once per SABRPlayer instance).
|
||||
if (!keydownInstalled) {
|
||||
document.addEventListener('keydown', handleKeydown);
|
||||
keydownInstalled = true;
|
||||
}
|
||||
|
||||
await player.attach(videoElement);
|
||||
|
||||
if (shaka.ui && shaka.ui.Overlay) {
|
||||
@ -432,6 +452,28 @@ var SABRPlayer = (function () {
|
||||
return out;
|
||||
}
|
||||
|
||||
// Rewrite a storyboard template URL to a same-origin URL so the seek-bar thumbnail tiles
|
||||
// load under CSP img-src 'self'. Reuses Invidious's EXISTING storyboard proxy route (the
|
||||
// one the classic video.js player uses), matching src/invidious/videos/storyboard.cr:
|
||||
// https://<authority>.ytimg.com/sb/<rest> -> /sb/<authority>/<rest>
|
||||
// A raw string swap (not URL parsing) preserves the '$M' tile placeholder and the literal
|
||||
// '$' inside the signature (sigh=rs$AOn...).
|
||||
function proxyStoryboardUrl(templateUrl) {
|
||||
var m = templateUrl.match(/^https?:\/\/(i\d?)\.ytimg\.com\/sb\/(.*)$/);
|
||||
if (m) {
|
||||
return '/sb/' + m[1] + '/' + m[2];
|
||||
}
|
||||
// Fallback for non-ytimg hosts: generic SABR /proxy (same-origin for CSP).
|
||||
try {
|
||||
var u = new URL(templateUrl);
|
||||
return '/proxy' + u.pathname +
|
||||
(u.search ? u.search + '&' : '?') +
|
||||
'__host=' + encodeURIComponent(u.host) + '&__headers=%5B%5D';
|
||||
} catch (e) {
|
||||
return templateUrl;
|
||||
}
|
||||
}
|
||||
|
||||
function buildStoryboards(videoInfo) {
|
||||
var out = [];
|
||||
var sb = videoInfo.storyboards;
|
||||
@ -441,7 +483,7 @@ var SABRPlayer = (function () {
|
||||
var board = boards.length ? boards[boards.length - 1] : null;
|
||||
if (!board) return out;
|
||||
out.push({
|
||||
templateUrl: board.template_url,
|
||||
templateUrl: proxyStoryboardUrl(board.template_url),
|
||||
mimeType: 'image/webp',
|
||||
columns: board.columns,
|
||||
rows: board.rows,
|
||||
@ -528,6 +570,61 @@ var SABRPlayer = (function () {
|
||||
function getPlayer() { return player; }
|
||||
function getManifest() { return sabrManifest; }
|
||||
|
||||
// Loading spinner: toggled via an 'is-loading' class on the player container.
|
||||
function showSpinner() { if (shakaContainer) shakaContainer.classList.add('is-loading'); }
|
||||
function hideSpinner() { if (shakaContainer) shakaContainer.classList.remove('is-loading'); }
|
||||
|
||||
// Global keyboard shortcuts (YouTube-style). Shaka's built-in shortcuts only fire when a
|
||||
// control is focused, so we add a page-level handler. Ignored while typing in a field.
|
||||
function handleKeydown(e) {
|
||||
if (!player || !videoElement) return;
|
||||
if (e.ctrlKey || e.altKey || e.metaKey) return;
|
||||
var t = e.target;
|
||||
var tag = (t && t.tagName) || '';
|
||||
if (/^(INPUT|TEXTAREA|SELECT)$/.test(tag) || (t && t.isContentEditable)) return;
|
||||
var dur = isFinite(videoElement.duration) ? videoElement.duration : Infinity;
|
||||
var handled = true;
|
||||
switch (e.key) {
|
||||
case ' ':
|
||||
case 'k':
|
||||
if (videoElement.paused) videoElement.play().catch(function () {}); else videoElement.pause();
|
||||
break;
|
||||
case 'ArrowLeft':
|
||||
videoElement.currentTime = Math.max(0, videoElement.currentTime - 5);
|
||||
break;
|
||||
case 'ArrowRight':
|
||||
videoElement.currentTime = Math.min(dur, videoElement.currentTime + 5);
|
||||
break;
|
||||
case 'j':
|
||||
videoElement.currentTime = Math.max(0, videoElement.currentTime - 10);
|
||||
break;
|
||||
case 'l':
|
||||
videoElement.currentTime = Math.min(dur, videoElement.currentTime + 10);
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
videoElement.volume = Math.min(1, videoElement.volume + 0.05);
|
||||
break;
|
||||
case 'ArrowDown':
|
||||
videoElement.volume = Math.max(0, videoElement.volume - 0.05);
|
||||
break;
|
||||
case 'm':
|
||||
videoElement.muted = !videoElement.muted;
|
||||
break;
|
||||
case 'f':
|
||||
if (document.fullscreenElement) { document.exitFullscreen(); }
|
||||
else if (shakaContainer && shakaContainer.requestFullscreen) { shakaContainer.requestFullscreen(); }
|
||||
break;
|
||||
default:
|
||||
// number keys 0-9 -> seek to that fraction of the video
|
||||
if (e.key >= '0' && e.key <= '9' && isFinite(dur)) {
|
||||
videoElement.currentTime = dur * (parseInt(e.key, 10) / 10);
|
||||
} else {
|
||||
handled = false;
|
||||
}
|
||||
}
|
||||
if (handled) e.preventDefault();
|
||||
}
|
||||
|
||||
function wireSabrStream(loadFn) {
|
||||
if (!sabrStream) return;
|
||||
sabrStream.onBackoffRequested(function (info) {
|
||||
@ -566,6 +663,7 @@ var SABRPlayer = (function () {
|
||||
// session identity. The 60s SABR wall is tied to the session/streaming
|
||||
// context, so a fresh session is what actually lets the reload recover.
|
||||
var resumeAt = (videoElement && isFinite(videoElement.currentTime)) ? videoElement.currentTime : undefined;
|
||||
var wasPaused = !!(videoElement && videoElement.paused);
|
||||
innertube = null;
|
||||
clientConfig = null;
|
||||
playbackWebPoToken = null;
|
||||
@ -573,6 +671,9 @@ var SABRPlayer = (function () {
|
||||
try { BotguardService.dispose(); } catch (e) {}
|
||||
var reloadOptions = Object.assign({}, currentLoadOptions || {});
|
||||
if (resumeAt !== undefined && resumeAt > 1) reloadOptions.startTime = resumeAt;
|
||||
// Don't force playback if the user had paused: a server-requested reload should
|
||||
// resume at the same position AND keep the paused state (not "play again").
|
||||
reloadOptions.startPaused = wasPaused;
|
||||
if (currentVideoId && loadFn) loadFn(currentVideoId, shakaContainer, reloadOptions);
|
||||
});
|
||||
}
|
||||
@ -584,6 +685,7 @@ var SABRPlayer = (function () {
|
||||
currentLoadOptions = options;
|
||||
playbackWebPoToken = null;
|
||||
playbackWebPoTokenContentBinding = videoId;
|
||||
showSpinner(); // re-show on reloads (container already exists)
|
||||
|
||||
try {
|
||||
// Start Shaka player init (DOM + polyfills) in parallel with network init.
|
||||
@ -738,9 +840,13 @@ var SABRPlayer = (function () {
|
||||
: undefined;
|
||||
await player.load(manifestUri, isLive ? undefined : startTime, mimeType);
|
||||
|
||||
// Don't auto-play on a reload that happened while the user had the video paused
|
||||
// (options.startPaused set by the SABR reload handler).
|
||||
if (!options.startPaused) {
|
||||
videoElement.play().catch(function (err) {
|
||||
if (err.name === 'NotAllowedError') console.warn('[SABRPlayer]', 'Autoplay was prevented by the browser');
|
||||
});
|
||||
}
|
||||
|
||||
if (savePositionInterval) clearInterval(savePositionInterval);
|
||||
savePositionInterval = setInterval(function () {
|
||||
@ -752,12 +858,14 @@ var SABRPlayer = (function () {
|
||||
return { player: player, videoElement: videoElement, videoInfo: videoInfo };
|
||||
} catch (error) {
|
||||
console.error('[SABRPlayer]', 'Error loading video:', error);
|
||||
hideSpinner();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function dispose() {
|
||||
if (savePositionInterval) { clearInterval(savePositionInterval); savePositionInterval = null; }
|
||||
if (keydownInstalled) { document.removeEventListener('keydown', handleKeydown); keydownInstalled = false; }
|
||||
if (videoElement && currentVideoId) savePlaybackPosition(currentVideoId, videoElement.currentTime);
|
||||
|
||||
if (sabrStream) { try { sabrStream.cleanup(); } catch (e) {} sabrStream = null; }
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user