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 {
|
.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 */
|
/* Buttons - match video.js colors */
|
||||||
@ -249,8 +256,40 @@
|
|||||||
display: none !important;
|
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 */
|
/* Poster/thumbnail styling */
|
||||||
.shaka-video-container .shaka-poster {
|
.shaka-video-container .shaka-poster {
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
object-fit: 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++) {
|
for (var j = 0; j < storyboard.storyboardCount; j++) {
|
||||||
var startTime = j * segmentDuration;
|
var startTime = j * segmentDuration;
|
||||||
var endTime = Math.min(startTime + segmentDuration, duration);
|
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(
|
var segmentReference = new shaka.media.SegmentReference(
|
||||||
startTime, endTime,
|
startTime, endTime,
|
||||||
function () { return urls; },
|
function () { return urls; },
|
||||||
|
|||||||
@ -60,6 +60,7 @@ var SABRPlayer = (function () {
|
|||||||
// per reload (loadVideo re-runs on every SABR reload).
|
// per reload (loadVideo re-runs on every SABR reload).
|
||||||
var requestFiltersInstalled = false;
|
var requestFiltersInstalled = false;
|
||||||
var loadedListenerInstalled = false;
|
var loadedListenerInstalled = false;
|
||||||
|
var keydownInstalled = false;
|
||||||
var sabrManifest = null; // captured from player.getManifest() on 'loaded'
|
var sabrManifest = null; // captured from player.getManifest() on 'loaded'
|
||||||
var currentLoadOptions = null; // for onReloadOnce -> re-run loadVideo
|
var currentLoadOptions = null; // for onReloadOnce -> re-run loadVideo
|
||||||
var reloadCount = 0; // cap reloads to prevent infinite loop on blocked/throttled videos
|
var reloadCount = 0; // cap reloads to prevent infinite loop on blocked/throttled videos
|
||||||
@ -162,8 +163,9 @@ var SABRPlayer = (function () {
|
|||||||
|
|
||||||
async function fetchOnesieConfig() {
|
async function fetchOnesieConfig() {
|
||||||
if (clientConfig && SABRHelpers.isConfigValid(clientConfig)) return clientConfig;
|
if (clientConfig && SABRHelpers.isConfigValid(clientConfig)) return clientConfig;
|
||||||
var cached = SABRHelpers.loadCachedClientConfig();
|
// Do NOT read a persisted (localStorage) config: its baseUrl embeds the egress IP
|
||||||
if (cached) { clientConfig = cached; return clientConfig; }
|
// 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 {
|
try {
|
||||||
var tvConfigResponse = await SABRHelpers.fetchWithProxy(
|
var tvConfigResponse = await SABRHelpers.fetchWithProxy(
|
||||||
'https://www.youtube.com/tv_config?action_get_config=true&client=lb4&theme=cl',
|
'https://www.youtube.com/tv_config?action_get_config=true&client=lb4&theme=cl',
|
||||||
@ -185,7 +187,7 @@ var SABRPlayer = (function () {
|
|||||||
baseUrl: onesieHotConfig.baseUrl,
|
baseUrl: onesieHotConfig.baseUrl,
|
||||||
timestamp: Date.now()
|
timestamp: Date.now()
|
||||||
};
|
};
|
||||||
SABRHelpers.saveCachedClientConfig(clientConfig);
|
// Not persisted to localStorage on purpose (baseUrl embeds a now-stale-on-proxy-change ip).
|
||||||
return clientConfig;
|
return clientConfig;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[SABRPlayer]', 'Failed to fetch Onesie client config', error);
|
console.error('[SABRPlayer]', 'Failed to fetch Onesie client config', error);
|
||||||
@ -294,7 +296,8 @@ var SABRPlayer = (function () {
|
|||||||
shaka.polyfill.installAll();
|
shaka.polyfill.installAll();
|
||||||
|
|
||||||
shakaContainer = document.createElement('div');
|
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.width = '100%';
|
||||||
shakaContainer.style.height = '100%';
|
shakaContainer.style.height = '100%';
|
||||||
|
|
||||||
@ -305,6 +308,14 @@ var SABRPlayer = (function () {
|
|||||||
videoElement.style.backgroundColor = '#000';
|
videoElement.style.backgroundColor = '#000';
|
||||||
|
|
||||||
shakaContainer.appendChild(videoElement);
|
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);
|
containerElement.appendChild(shakaContainer);
|
||||||
|
|
||||||
player = new shaka.Player();
|
player = new shaka.Player();
|
||||||
@ -329,11 +340,20 @@ var SABRPlayer = (function () {
|
|||||||
videoElement.addEventListener('volumechange', function () { saveVolume(videoElement.volume); });
|
videoElement.addEventListener('volumechange', function () { saveVolume(videoElement.volume); });
|
||||||
videoElement.addEventListener('playing', function () {
|
videoElement.addEventListener('playing', function () {
|
||||||
player.configure('abr.restrictions.maxHeight', Infinity);
|
player.configure('abr.restrictions.maxHeight', Infinity);
|
||||||
|
hideSpinner();
|
||||||
});
|
});
|
||||||
|
videoElement.addEventListener('waiting', function () { showSpinner(); });
|
||||||
|
videoElement.addEventListener('canplay', function () { hideSpinner(); });
|
||||||
videoElement.addEventListener('pause', function () {
|
videoElement.addEventListener('pause', function () {
|
||||||
if (currentVideoId) savePlaybackPosition(currentVideoId, videoElement.currentTime);
|
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);
|
await player.attach(videoElement);
|
||||||
|
|
||||||
if (shaka.ui && shaka.ui.Overlay) {
|
if (shaka.ui && shaka.ui.Overlay) {
|
||||||
@ -432,6 +452,28 @@ var SABRPlayer = (function () {
|
|||||||
return out;
|
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) {
|
function buildStoryboards(videoInfo) {
|
||||||
var out = [];
|
var out = [];
|
||||||
var sb = videoInfo.storyboards;
|
var sb = videoInfo.storyboards;
|
||||||
@ -441,7 +483,7 @@ var SABRPlayer = (function () {
|
|||||||
var board = boards.length ? boards[boards.length - 1] : null;
|
var board = boards.length ? boards[boards.length - 1] : null;
|
||||||
if (!board) return out;
|
if (!board) return out;
|
||||||
out.push({
|
out.push({
|
||||||
templateUrl: board.template_url,
|
templateUrl: proxyStoryboardUrl(board.template_url),
|
||||||
mimeType: 'image/webp',
|
mimeType: 'image/webp',
|
||||||
columns: board.columns,
|
columns: board.columns,
|
||||||
rows: board.rows,
|
rows: board.rows,
|
||||||
@ -528,6 +570,61 @@ var SABRPlayer = (function () {
|
|||||||
function getPlayer() { return player; }
|
function getPlayer() { return player; }
|
||||||
function getManifest() { return sabrManifest; }
|
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) {
|
function wireSabrStream(loadFn) {
|
||||||
if (!sabrStream) return;
|
if (!sabrStream) return;
|
||||||
sabrStream.onBackoffRequested(function (info) {
|
sabrStream.onBackoffRequested(function (info) {
|
||||||
@ -566,6 +663,7 @@ var SABRPlayer = (function () {
|
|||||||
// session identity. The 60s SABR wall is tied to the session/streaming
|
// session identity. The 60s SABR wall is tied to the session/streaming
|
||||||
// context, so a fresh session is what actually lets the reload recover.
|
// context, so a fresh session is what actually lets the reload recover.
|
||||||
var resumeAt = (videoElement && isFinite(videoElement.currentTime)) ? videoElement.currentTime : undefined;
|
var resumeAt = (videoElement && isFinite(videoElement.currentTime)) ? videoElement.currentTime : undefined;
|
||||||
|
var wasPaused = !!(videoElement && videoElement.paused);
|
||||||
innertube = null;
|
innertube = null;
|
||||||
clientConfig = null;
|
clientConfig = null;
|
||||||
playbackWebPoToken = null;
|
playbackWebPoToken = null;
|
||||||
@ -573,6 +671,9 @@ var SABRPlayer = (function () {
|
|||||||
try { BotguardService.dispose(); } catch (e) {}
|
try { BotguardService.dispose(); } catch (e) {}
|
||||||
var reloadOptions = Object.assign({}, currentLoadOptions || {});
|
var reloadOptions = Object.assign({}, currentLoadOptions || {});
|
||||||
if (resumeAt !== undefined && resumeAt > 1) reloadOptions.startTime = resumeAt;
|
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);
|
if (currentVideoId && loadFn) loadFn(currentVideoId, shakaContainer, reloadOptions);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -584,6 +685,7 @@ var SABRPlayer = (function () {
|
|||||||
currentLoadOptions = options;
|
currentLoadOptions = options;
|
||||||
playbackWebPoToken = null;
|
playbackWebPoToken = null;
|
||||||
playbackWebPoTokenContentBinding = videoId;
|
playbackWebPoTokenContentBinding = videoId;
|
||||||
|
showSpinner(); // re-show on reloads (container already exists)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Start Shaka player init (DOM + polyfills) in parallel with network init.
|
// Start Shaka player init (DOM + polyfills) in parallel with network init.
|
||||||
@ -738,9 +840,13 @@ var SABRPlayer = (function () {
|
|||||||
: undefined;
|
: undefined;
|
||||||
await player.load(manifestUri, isLive ? undefined : startTime, mimeType);
|
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) {
|
videoElement.play().catch(function (err) {
|
||||||
if (err.name === 'NotAllowedError') console.warn('[SABRPlayer]', 'Autoplay was prevented by the browser');
|
if (err.name === 'NotAllowedError') console.warn('[SABRPlayer]', 'Autoplay was prevented by the browser');
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (savePositionInterval) clearInterval(savePositionInterval);
|
if (savePositionInterval) clearInterval(savePositionInterval);
|
||||||
savePositionInterval = setInterval(function () {
|
savePositionInterval = setInterval(function () {
|
||||||
@ -752,12 +858,14 @@ var SABRPlayer = (function () {
|
|||||||
return { player: player, videoElement: videoElement, videoInfo: videoInfo };
|
return { player: player, videoElement: videoElement, videoInfo: videoInfo };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[SABRPlayer]', 'Error loading video:', error);
|
console.error('[SABRPlayer]', 'Error loading video:', error);
|
||||||
|
hideSpinner();
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function dispose() {
|
async function dispose() {
|
||||||
if (savePositionInterval) { clearInterval(savePositionInterval); savePositionInterval = null; }
|
if (savePositionInterval) { clearInterval(savePositionInterval); savePositionInterval = null; }
|
||||||
|
if (keydownInstalled) { document.removeEventListener('keydown', handleKeydown); keydownInstalled = false; }
|
||||||
if (videoElement && currentVideoId) savePlaybackPosition(currentVideoId, videoElement.currentTime);
|
if (videoElement && currentVideoId) savePlaybackPosition(currentVideoId, videoElement.currentTime);
|
||||||
|
|
||||||
if (sabrStream) { try { sabrStream.cleanup(); } catch (e) {} sabrStream = null; }
|
if (sabrStream) { try { sabrStream.cleanup(); } catch (e) {} sabrStream = null; }
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user