Files
JaredTherriault-ComfyUI-JNodes/web/js/common/ImageAndVideoObserver.js
T

141 lines
4.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 });