141 lines
4.5 KiB
JavaScript
141 lines
4.5 KiB
JavaScript
// This script ensures that a given video or animated image is in the viewport before autoplaying it,
|
||
// then plays it. If the image is scrolled out of view, the playback stops. This applies to video
|
||
// and image types including mp4, m4v, wepb, gif, apng, etc.
|
||
|
||
import { setting_VideoPlaybackOptions } from "./SettingsManager.js";
|
||
|
||
import { utilitiesInstance } from "./Utilities.js";
|
||
|
||
// Note: On some browsers the user may have to explicitly allow autoplay in order for playback to work correctly
|
||
// Otherwise, you end up with a bunch of errors while scrolling informing us that the user didn't 'interact' with the page first (even if they are clearly scrolling)
|
||
|
||
export class ImageAndVideoObserverOptions {
|
||
playbackThreshold = 0.01; // Percentage of video that must be visible in order to load. Anything less and it will be unloaded.
|
||
}
|
||
|
||
let observerOptions = new ImageAndVideoObserverOptions();
|
||
|
||
const observedElements = new Set();
|
||
|
||
const bIsUnloadEnabled = true;
|
||
|
||
function tryPlayVideo(element) {
|
||
|
||
// Helper anon function
|
||
const start = () => {
|
||
|
||
// Pause all other videos if Solo mode
|
||
if (setting_VideoPlaybackOptions.value.solo) {
|
||
for (const video of utilitiesInstance.getVideoElements()) {
|
||
tryStopVideo(video);
|
||
}
|
||
}
|
||
|
||
if (element.lastSeekTime != null) {
|
||
element.currentTime = element.lastSeekTime;
|
||
}
|
||
|
||
// Try to play the video but don't error if not possible
|
||
element.play().catch(() => {});
|
||
};
|
||
|
||
// Start if ready
|
||
if (element.readyState >= 1) {
|
||
start();
|
||
} else {
|
||
// Otherwise wait for metadata to load to avoid stuttering start
|
||
element.addEventListener('loadedmetadata', start, { once: true });
|
||
}
|
||
}
|
||
|
||
|
||
function tryStopVideo(element) {
|
||
if (element.readyState >= element.HAVE_ENOUGH_DATA && !element.paused) {
|
||
try {
|
||
element.pause();
|
||
} catch { }
|
||
}
|
||
}
|
||
|
||
export function observeVisualElement(element) {
|
||
if (!element) {
|
||
return;
|
||
}
|
||
// Check if the element is already being observed
|
||
if (!observedElements.has(element)) {
|
||
// If not observed, add it to the set of observed elements
|
||
observedElements.add(element);
|
||
// Start observing the element
|
||
imageAndVideoObserver.observe(element);
|
||
}
|
||
}
|
||
|
||
export function unobserveVisualElement(element) {
|
||
if (!element) {
|
||
return;
|
||
}
|
||
observedElements.delete(element);
|
||
imageAndVideoObserver.unobserve(element);
|
||
}
|
||
|
||
const UNLOAD_DELAY_MS = 500; // Wait 0.5s before unloading, avoids flashing when DOM changes
|
||
const unloadTimeouts = new WeakMap();
|
||
|
||
const imageAndVideoObserver = new IntersectionObserver((entries) => {
|
||
entries.forEach(async entry => {
|
||
const element = entry.target;
|
||
if (!element) {
|
||
unobserveVisualElement(element);
|
||
return;
|
||
}
|
||
|
||
// If it’s visible
|
||
if (entry.isIntersecting) {
|
||
// Cancel any pending unload
|
||
const timeout = unloadTimeouts.get(element);
|
||
if (timeout) {
|
||
clearTimeout(timeout);
|
||
unloadTimeouts.delete(element);
|
||
}
|
||
|
||
// Handle intersecting
|
||
if (element.onObserverIntersect) {
|
||
element.onObserverIntersect();
|
||
} else if (!element.src || element.src === '') {
|
||
if (element.forceLoad) {
|
||
element.forceLoad();
|
||
}
|
||
}
|
||
|
||
if (element.tagName === 'VIDEO' && setting_VideoPlaybackOptions.value.autoplay) {
|
||
tryPlayVideo(element);
|
||
}
|
||
|
||
} else {
|
||
|
||
if (!bIsUnloadEnabled) {return;}
|
||
// Not visible — schedule unload
|
||
if (element.onObserverUnintersect) {
|
||
element.onObserverUnintersect();
|
||
} else if (element.tagName === 'VIDEO') {
|
||
// Debounce unloading to prevent flicker when DOM changes
|
||
const timeout = setTimeout(() => {
|
||
tryStopVideo(element);
|
||
if (element.currentTime) {
|
||
element.lastSeekTime = element.currentTime;
|
||
}
|
||
|
||
if ('src' in element) {
|
||
element.removeAttribute('src'); // unload
|
||
try {
|
||
if (element.load) { element.load(); } // release memory
|
||
} catch { }
|
||
}
|
||
unloadTimeouts.delete(element);
|
||
}, UNLOAD_DELAY_MS);
|
||
|
||
unloadTimeouts.set(element, timeout);
|
||
}
|
||
}
|
||
});
|
||
}, { threshold: observerOptions.playbackThreshold }); |