221 lines
7.2 KiB
JavaScript
221 lines
7.2 KiB
JavaScript
async function waitForSlideImages(markup, renderRequestId) {
|
|
if (typeof Image !== 'function' || typeof document === 'undefined') return markup;
|
|
var template = document.createElement('template');
|
|
template.innerHTML = markup;
|
|
var sources = Array.from(new Set(Array.from(template.content.querySelectorAll('img[src]')).map(function (image) { return image.getAttribute('src'); }).filter(Boolean)));
|
|
if (!sources.length) return markup;
|
|
if (!lastRenderedViewKey && !app.querySelector('.player-media-loading')) {
|
|
var loading = document.createElement('div');
|
|
loading.className = 'empty player-media-loading';
|
|
loading.setAttribute('role', 'status');
|
|
loading.setAttribute('aria-live', 'polite');
|
|
loading.innerHTML = '<span class="player-media-loading-spinner" aria-hidden="true"></span><span>Loading media...</span>';
|
|
app.appendChild(loading);
|
|
}
|
|
async function loadSource(source) {
|
|
var attempt = 0;
|
|
var loadedUrl = source;
|
|
while (renderRequestId === slideRenderRequestId) {
|
|
var ready = await new Promise(function (resolve) {
|
|
var image = new Image();
|
|
var settled = false;
|
|
var timeout;
|
|
function finish(value) {
|
|
if (settled) return;
|
|
settled = true;
|
|
window.clearTimeout(timeout);
|
|
image.onload = null;
|
|
image.onerror = null;
|
|
resolve(value);
|
|
}
|
|
image.onload = async function () {
|
|
try {
|
|
if (typeof image.decode === 'function') await image.decode();
|
|
loadedUrl = image.src;
|
|
finish(image.naturalWidth > 0);
|
|
} catch (_error) { finish(false); }
|
|
};
|
|
image.onerror = function () { finish(false); };
|
|
timeout = window.setTimeout(function () { finish(false); }, 10000);
|
|
var url = source;
|
|
if (attempt) {
|
|
try {
|
|
var retryUrl = new URL(source, window.location.href);
|
|
if (retryUrl.origin === window.location.origin && retryUrl.pathname.startsWith('/media/')) {
|
|
retryUrl.searchParams.set('_mediaReady', String(Date.now()));
|
|
url = retryUrl.toString();
|
|
}
|
|
} catch (_error) {}
|
|
}
|
|
image.src = url;
|
|
});
|
|
if (ready) {
|
|
if (loadedUrl !== source) template.content.querySelectorAll('img[src]').forEach(function (element) {
|
|
if (element.getAttribute('src') === source) element.setAttribute('src', loadedUrl);
|
|
});
|
|
return renderRequestId === slideRenderRequestId;
|
|
}
|
|
if (renderRequestId !== slideRenderRequestId) return false;
|
|
attempt += 1;
|
|
await new Promise(function (resolve) { window.setTimeout(resolve, Math.min(1000 * attempt, 5000)); });
|
|
}
|
|
return false;
|
|
}
|
|
var results = await Promise.all(sources.map(loadSource));
|
|
return renderRequestId === slideRenderRequestId && results.every(Boolean) ? template.innerHTML : false;
|
|
}
|
|
|
|
// Slide media startup and teardown helpers.
|
|
|
|
var renderedVideoPlaybackCleanup = new WeakMap();
|
|
|
|
function cancelRenderedVideoPlayback(video) {
|
|
var cleanup = renderedVideoPlaybackCleanup.get(video);
|
|
if (cleanup) {
|
|
cleanup();
|
|
renderedVideoPlaybackCleanup.delete(video);
|
|
}
|
|
}
|
|
|
|
function initializeRenderedVideoPlayback(root, delayMs) {
|
|
if (!root) {
|
|
return;
|
|
}
|
|
|
|
var startDelayMs = Math.max(0, Number(delayMs || 0));
|
|
var videos = root.querySelectorAll('.template-region.video video');
|
|
Array.prototype.forEach.call(videos, function (video) {
|
|
if (!video) {
|
|
return;
|
|
}
|
|
|
|
cancelRenderedVideoPlayback(video);
|
|
var playbackScheduled = false;
|
|
var playbackCancelled = false;
|
|
var playbackTimer = null;
|
|
|
|
function removeReadinessListeners() {
|
|
video.removeEventListener('canplay', schedulePlaybackStart);
|
|
video.removeEventListener('loadedmetadata', onLoadedMetadata);
|
|
}
|
|
|
|
// A cancelled callback can already be queued for dispatch, so it also checks this flag.
|
|
renderedVideoPlaybackCleanup.set(video, function () {
|
|
playbackCancelled = true;
|
|
if (playbackTimer !== null) {
|
|
window.clearTimeout(playbackTimer);
|
|
playbackTimer = null;
|
|
}
|
|
removeReadinessListeners();
|
|
});
|
|
|
|
// Script-controlled startup aligns playback with the incoming slide's fade timing.
|
|
video.autoplay = false;
|
|
video.loop = !(video.dataset && video.dataset.loop === '0');
|
|
video.muted = !(video.dataset && video.dataset.disableAudio === '0');
|
|
video.playsInline = true;
|
|
|
|
function startPlayback() {
|
|
playbackTimer = null;
|
|
if (playbackCancelled || video.isConnected === false || root.isConnected === false) {
|
|
return;
|
|
}
|
|
var playPromise = video.play && video.play();
|
|
if (playPromise && typeof playPromise.catch === 'function') {
|
|
playPromise.catch(function () {
|
|
return null;
|
|
});
|
|
}
|
|
}
|
|
|
|
function schedulePlaybackStart() {
|
|
// Metadata and canplay can both fire; schedule only one start for this video.
|
|
if (playbackCancelled || playbackScheduled) {
|
|
return;
|
|
}
|
|
playbackScheduled = true;
|
|
removeReadinessListeners();
|
|
if (startDelayMs > 0) {
|
|
playbackTimer = window.setTimeout(startPlayback, startDelayMs);
|
|
return;
|
|
}
|
|
startPlayback();
|
|
}
|
|
|
|
function onLoadedMetadata() {
|
|
if (video.readyState >= 2) {
|
|
schedulePlaybackStart();
|
|
}
|
|
}
|
|
|
|
// HAVE_CURRENT_DATA means a frame is available, not just the video's metadata.
|
|
if (video.readyState >= 2) {
|
|
schedulePlaybackStart();
|
|
return;
|
|
}
|
|
|
|
video.addEventListener('canplay', schedulePlaybackStart, { once: true });
|
|
video.addEventListener('loadedmetadata', onLoadedMetadata, { once: true });
|
|
});
|
|
}
|
|
|
|
function pauseRenderedVideoPlayback(root, delayMs) {
|
|
if (!root) {
|
|
return;
|
|
}
|
|
|
|
var pauseDelayMs = Math.max(0, Number(delayMs || 0));
|
|
var videos = root.querySelectorAll('.template-region.video video, .slide-media video');
|
|
Array.prototype.forEach.call(videos, function (video) {
|
|
if (!video || typeof video.pause !== 'function') {
|
|
return;
|
|
}
|
|
|
|
window.setTimeout(function () {
|
|
try {
|
|
video.pause();
|
|
} catch (_error) {
|
|
return null;
|
|
}
|
|
}, pauseDelayMs);
|
|
});
|
|
}
|
|
|
|
function initializeSlideMedia(root, delayMs) {
|
|
if (!root || root.isConnected === false) {
|
|
return;
|
|
}
|
|
|
|
if (typeof syncRtmpRegions === 'function') {
|
|
syncRtmpRegions(root);
|
|
}
|
|
initializeRenderedVideoPlayback(root, delayMs);
|
|
}
|
|
|
|
function destroySlideMedia(root, options) {
|
|
if (!root) {
|
|
return;
|
|
}
|
|
|
|
var preserveVideoFrame = Boolean(options && options.preserveVideoFrame);
|
|
var videos = root.querySelectorAll('.template-region.video video, .slide-media video');
|
|
Array.prototype.forEach.call(videos, function (video) {
|
|
cancelRenderedVideoPlayback(video);
|
|
if (typeof video.pause === 'function') {
|
|
video.pause();
|
|
}
|
|
// Keep the paused frame through a crossfade; release decoder/network resources at removal.
|
|
if (!preserveVideoFrame) {
|
|
video.removeAttribute('src');
|
|
if (typeof video.load === 'function') {
|
|
video.load();
|
|
}
|
|
}
|
|
});
|
|
|
|
// RTMP players hold independent HLS resources beyond the lifetime of their DOM nodes.
|
|
if (typeof destroyRtmpRegions === 'function') {
|
|
destroyRtmpRegions(root);
|
|
}
|
|
}
|