Files
pulse-signage/src/player/regions/rtmp.js
T

826 lines
24 KiB
JavaScript

var registry = window.pulsePlayerRegionTypes;
function renderRtmpRegion(region, regionContent) {
var url = String(regionContent.value || '').trim();
var disableAudio = regionContent.disable_audio === undefined ? true : Boolean(regionContent.disable_audio);
var skipUnavailable = Boolean(currentPlaylistSkipUnavailableRtmp);
if (!url) {
return '';
}
return '<div class="template-region rtmp" style="' + region.baseStyle + '"><video class="template-region-rtmp-video" data-rtmp-source="' + escapeHtml(url) + '" data-rtmp-disable-audio="' + (disableAudio ? '1' : '0') + '" data-rtmp-skip-unavailable="' + (skipUnavailable ? '1' : '0') + '" autoplay playsinline preload="auto" tabindex="-1" disablepictureinpicture' + (disableAudio ? ' muted' : '') + '></video><div class="template-region-placeholder template-region-rtmp-placeholder">Loading RTMP stream...</div></div>';
}
function getRtmpSessionUrl(sourceUrl, disableAudio) {
return '/api/rtmp/session?source=' + encodeURIComponent(sourceUrl) + '&disableAudio=' + (disableAudio ? '1' : '0');
}
var rtmpAvailabilityCache = Object.create(null);
var rtmpAvailabilityRetryMs = 5000;
var rtmpWarmupTimers = Object.create(null);
var rtmpBrowserReadyCache = Object.create(null);
var rtmpBrowserWarmupTimers = Object.create(null);
var rtmpBrowserWarmupContainer = null;
var rtmpBrowserWarmupStates = Object.create(null);
function getRtmpAvailabilityKey(sourceUrl, disableAudio) {
return String(sourceUrl || '').trim() + '\n' + (disableAudio ? '1' : '0');
}
function setRtmpAvailability(sourceUrl, disableAudio, available) {
var key = getRtmpAvailabilityKey(sourceUrl, disableAudio);
rtmpAvailabilityCache[key] = {
available: available === null ? null : Boolean(available),
pending: available === null,
checkedAt: Date.now()
};
}
function getRtmpAvailability(sourceUrl, disableAudio) {
return rtmpAvailabilityCache[getRtmpAvailabilityKey(sourceUrl, disableAudio)] || null;
}
function isRtmpAvailabilityStale(status) {
if (!status || !status.checkedAt) {
return true;
}
return Date.now() - Number(status.checkedAt || 0) >= rtmpAvailabilityRetryMs;
}
function isRtmpSlideUnavailable(slide) {
if (!slide || !slide.template || !Array.isArray(slide.template.regions)) {
return false;
}
var content = slide.content || {};
return slide.template.regions.some(function (region) {
if (region.region_type !== 'rtmp') {
return false;
}
var regionContent = content[region.region_key] || {};
var sourceUrl = String(regionContent.value || '').trim();
if (!sourceUrl) {
return false;
}
var disableAudio = regionContent.disable_audio === undefined ? true : Boolean(regionContent.disable_audio);
var serverStatus = getRtmpAvailability(sourceUrl, disableAudio);
return Boolean(!serverStatus || isRtmpAvailabilityStale(serverStatus) || serverStatus.available !== true);
});
}
function clearRtmpAvailabilityForSlide(slide) {
if (!slide || !slide.template || !Array.isArray(slide.template.regions)) {
return;
}
var content = slide.content || {};
slide.template.regions.forEach(function (region) {
if (region.region_type !== 'rtmp') {
return;
}
var regionContent = content[region.region_key] || {};
var sourceUrl = String(regionContent.value || '').trim();
if (!sourceUrl) {
return;
}
var disableAudio = regionContent.disable_audio === undefined ? true : Boolean(regionContent.disable_audio);
setRtmpAvailability(sourceUrl, disableAudio, null);
});
}
function setRtmpBrowserReady(sourceUrl, disableAudio, ready) {
var key = getRtmpAvailabilityKey(sourceUrl, disableAudio);
rtmpBrowserReadyCache[key] = {
ready: ready === null ? null : Boolean(ready),
checkedAt: Date.now()
};
}
function getRtmpBrowserReady(sourceUrl, disableAudio) {
return rtmpBrowserReadyCache[getRtmpAvailabilityKey(sourceUrl, disableAudio)] || null;
}
function isRtmpBrowserReadyStale(status) {
if (!status || !status.checkedAt) {
return true;
}
return Date.now() - Number(status.checkedAt || 0) >= rtmpAvailabilityRetryMs;
}
function ensureRtmpBrowserWarmupContainer() {
if (rtmpBrowserWarmupContainer) {
return rtmpBrowserWarmupContainer;
}
rtmpBrowserWarmupContainer = document.createElement('div');
rtmpBrowserWarmupContainer.className = 'rtmp-browser-warmups';
rtmpBrowserWarmupContainer.setAttribute('aria-hidden', 'true');
document.body.appendChild(rtmpBrowserWarmupContainer);
return rtmpBrowserWarmupContainer;
}
function getRtmpBrowserWarmupState(sourceUrl, disableAudio) {
return rtmpBrowserWarmupStates[getRtmpAvailabilityKey(sourceUrl, disableAudio)] || null;
}
function clearRtmpBrowserWarmupState(sourceUrl, disableAudio) {
var key = getRtmpAvailabilityKey(sourceUrl, disableAudio);
var state = rtmpBrowserWarmupStates[key];
if (!state) {
return;
}
if (state.video && state.video.__rtmpBrowserWarmupTimer) {
window.clearTimeout(state.video.__rtmpBrowserWarmupTimer);
state.video.__rtmpBrowserWarmupTimer = null;
}
if (state.video && state.video.__rtmpHls) {
try {
state.video.__rtmpHls.destroy();
} catch (_error) {
// ignore cleanup errors
}
state.video.__rtmpHls = null;
}
if (state.wrapper && state.wrapper.parentNode) {
state.wrapper.parentNode.removeChild(state.wrapper);
}
delete rtmpBrowserWarmupStates[key];
}
function warmupRtmpBrowser(sourceUrl, disableAudio) {
var key = getRtmpAvailabilityKey(sourceUrl, disableAudio);
var existing = rtmpBrowserWarmupStates[key];
if (existing) {
return existing;
}
var wrapper = document.createElement('div');
wrapper.className = 'rtmp-browser-warmup-entry';
wrapper.setAttribute('aria-hidden', 'true');
var video = document.createElement('video');
video.className = 'template-region-rtmp-video';
video.dataset.rtmpSource = sourceUrl;
video.dataset.rtmpDisableAudio = disableAudio ? '1' : '0';
video.autoplay = true;
video.playsInline = true;
video.preload = 'auto';
video.tabIndex = -1;
video.disablePictureInPicture = true;
video.muted = disableAudio;
wrapper.appendChild(video);
ensureRtmpBrowserWarmupContainer().appendChild(wrapper);
var state = {
wrapper: wrapper,
video: video,
sourceUrl: sourceUrl,
disableAudio: disableAudio
};
rtmpBrowserWarmupStates[key] = state;
setRtmpBrowserReady(sourceUrl, disableAudio, null);
var markReady = function () {
setRtmpBrowserReady(sourceUrl, disableAudio, true);
clearRtmpBrowserWarmupState(sourceUrl, disableAudio);
};
var markNotReady = function () {
setRtmpBrowserReady(sourceUrl, disableAudio, false);
};
video.addEventListener('canplay', markReady, { once: true });
video.addEventListener('playing', markReady, { once: true });
video.addEventListener('error', markNotReady, { once: true });
if (window.Hls && window.Hls.isSupported && window.Hls.isSupported()) {
var hls = new window.Hls({
enableWorker: true,
lowLatencyMode: true,
liveSyncDurationCount: 4,
liveMaxLatencyDurationCount: 8,
maxBufferLength: 10,
maxLiveSyncPlaybackRate: 1,
backBufferLength: 10
});
video.__rtmpHls = hls;
hls.attachMedia(video);
hls.on(window.Hls.Events.MEDIA_ATTACHED, function () {
requestWarmupPlaylist(sourceUrl, disableAudio, video, markNotReady);
});
hls.on(window.Hls.Events.ERROR, function (_event, data) {
if (data && data.fatal) {
markNotReady();
clearRtmpBrowserWarmupState(sourceUrl, disableAudio);
}
});
return state;
}
if (video.canPlayType && video.canPlayType('application/vnd.apple.mpegurl')) {
requestWarmupPlaylist(sourceUrl, disableAudio, video, markNotReady);
return state;
}
markNotReady();
return state;
}
function requestWarmupPlaylist(sourceUrl, disableAudio, video, onFailure) {
fetch(getRtmpSessionUrl(sourceUrl, disableAudio), {
credentials: 'same-origin'
}).then(function (response) {
return response.json().catch(function () {
return null;
}).then(function (payload) {
return {
response: response,
payload: payload
};
});
}).then(function (result) {
if (!result || !result.response) {
throw new Error('Unable to initialize RTMP stream.');
}
var response = result.response;
var payload = result.payload || null;
var playlistUrl = payload && payload.playlistUrl ? String(payload.playlistUrl).trim() : '';
if (!response.ok) {
if (response.status === 503 && payload && payload.probing) {
setRtmpBrowserReady(sourceUrl, disableAudio, null);
if (video && !video.__rtmpBrowserWarmupTimer) {
video.__rtmpBrowserWarmupTimer = window.setTimeout(function () {
video.__rtmpBrowserWarmupTimer = null;
requestWarmupPlaylist(sourceUrl, disableAudio, video, onFailure);
}, 500);
}
return;
}
throw new Error('Unable to initialize RTMP stream.');
}
if (!playlistUrl) {
throw new Error('RTMP playlist URL was not returned.');
}
setRtmpBrowserReady(sourceUrl, disableAudio, false);
if (video.__rtmpHls && window.Hls && window.Hls.isSupported && window.Hls.isSupported()) {
video.__rtmpHls.loadSource(playlistUrl);
return;
}
if (video) {
video.src = playlistUrl;
video.play().catch(function () {
return null;
});
}
}).catch(function () {
setRtmpBrowserReady(sourceUrl, disableAudio, false);
if (typeof onFailure === 'function') {
onFailure();
}
});
}
function getRtmpWarmupSlides(sourceSlides, targetIndex) {
var availableSlides = Array.isArray(sourceSlides) ? sourceSlides : [];
if (!availableSlides.length) {
return [];
}
var normalizedIndex = Number(targetIndex || 0);
if (!Number.isFinite(normalizedIndex) || normalizedIndex < 0 || normalizedIndex >= availableSlides.length) {
normalizedIndex = 0;
}
var warmupSlides = [];
var nextSlide = availableSlides[normalizedIndex + 1];
if (nextSlide) {
warmupSlides.push(nextSlide);
}
return warmupSlides;
}
function getRtmpWarmupEntries(sourceSlides) {
var entries = [];
var seen = Object.create(null);
(Array.isArray(sourceSlides) ? sourceSlides : []).forEach(function (slide) {
var content = slide && slide.content ? slide.content : {};
var regions = slide && slide.template && Array.isArray(slide.template.regions) ? slide.template.regions : [];
regions.forEach(function (region) {
if (region.region_type !== 'rtmp') {
return;
}
var regionContent = content[region.region_key] || {};
var url = String(regionContent.value || '').trim();
if (!url) {
return;
}
var disableAudio = regionContent.disable_audio === undefined ? true : Boolean(regionContent.disable_audio);
var key = getRtmpAvailabilityKey(url, disableAudio);
if (seen[key]) {
return;
}
seen[key] = true;
entries.push({
url: url,
disableAudio: disableAudio,
key: key
});
});
});
return entries;
}
function syncRtmpWarmups(sourceSlides, targetIndex) {
var entries = getRtmpWarmupEntries(getRtmpWarmupSlides(sourceSlides, targetIndex));
entries.forEach(function (entry) {
var status = getRtmpAvailability(entry.url, entry.disableAudio);
if (status && !isRtmpAvailabilityStale(status) && status.available === true) {
return;
}
if (rtmpWarmupTimers[entry.key]) {
return;
}
function requestWarmup() {
fetch(getRtmpSessionUrl(entry.url, entry.disableAudio), {
credentials: 'same-origin'
}).then(function (response) {
return response.json().catch(function () {
return null;
}).then(function (payload) {
return {
response: response,
payload: payload
};
});
}).then(function (result) {
rtmpWarmupTimers[entry.key] = null;
if (!result || !result.response) {
setRtmpAvailability(entry.url, entry.disableAudio, false);
return;
}
if (!result.response.ok) {
if (result.response.status === 503 && result.payload && result.payload.probing) {
setRtmpAvailability(entry.url, entry.disableAudio, null);
rtmpWarmupTimers[entry.key] = window.setTimeout(function () {
rtmpWarmupTimers[entry.key] = null;
requestWarmup();
}, 500);
return;
}
setRtmpAvailability(entry.url, entry.disableAudio, false);
return;
}
setRtmpAvailability(entry.url, entry.disableAudio, true);
}).catch(function () {
rtmpWarmupTimers[entry.key] = null;
setRtmpAvailability(entry.url, entry.disableAudio, false);
});
}
setRtmpAvailability(entry.url, entry.disableAudio, null);
requestWarmup();
});
}
async function probeRtmpSlideAvailability(slide) {
var entries = getRtmpWarmupEntries([slide]);
if (!entries.length) {
return true;
}
var results = await Promise.all(entries.map(function (entry) {
setRtmpAvailability(entry.url, entry.disableAudio, null);
return fetch(getRtmpSessionUrl(entry.url, entry.disableAudio), {
credentials: 'same-origin'
}).then(function (response) {
return response.json().catch(function () {
return null;
}).then(function (payload) {
return {
response: response,
payload: payload
};
});
}).then(function (result) {
if (!result || !result.response) {
setRtmpAvailability(entry.url, entry.disableAudio, false);
return false;
}
if (!result.response.ok) {
if (result.response.status === 503 && result.payload && result.payload.probing) {
setRtmpAvailability(entry.url, entry.disableAudio, null);
return false;
}
setRtmpAvailability(entry.url, entry.disableAudio, false);
return false;
}
setRtmpAvailability(entry.url, entry.disableAudio, true);
return true;
}).catch(function () {
setRtmpAvailability(entry.url, entry.disableAudio, false);
return false;
});
}));
return results.every(function (value) {
return Boolean(value);
});
}
function syncRtmpBrowserWarmups(sourceSlides, targetIndex) {
return;
}
function isRtmpBrowserUnavailable(slide) {
if (!slide || !slide.template || !Array.isArray(slide.template.regions)) {
return false;
}
var content = slide.content || {};
return slide.template.regions.some(function (region) {
if (region.region_type !== 'rtmp') {
return false;
}
var regionContent = content[region.region_key] || {};
var sourceUrl = String(regionContent.value || '').trim();
if (!sourceUrl) {
return false;
}
var disableAudio = regionContent.disable_audio === undefined ? true : Boolean(regionContent.disable_audio);
var status = getRtmpBrowserReady(sourceUrl, disableAudio);
return Boolean(!status || isRtmpBrowserReadyStale(status) || status.ready !== true);
});
}
function bindRtmpVideoPlaceholder(video, placeholder) {
if (!video) {
return;
}
var markReady = function () {
if (placeholder) {
placeholder.style.display = 'none';
}
};
video.addEventListener('canplay', markReady, { once: true });
video.addEventListener('playing', markReady, { once: true });
if (video.readyState >= 2) {
markReady();
}
}
function startRtmpPlayback(video, sourceUrl, disableAudio, skipUnavailable, placeholder, isPreload, onFailure) {
var startupTimeoutMs = skipUnavailable ? 6000 : 0;
var startupTimer = null;
var probeRetryTimer = null;
var webReceiveTimer = null;
var webReceiveRetryUsed = false;
function failPlayback(message, silent) {
if (video.dataset.rtmpFailureHandled === '1') {
return;
}
video.dataset.rtmpFailureHandled = '1';
if (typeof onFailure === 'function') {
onFailure(message, silent);
return;
}
if (!isPreload && skipUnavailable && typeof handleRtmpPlaybackFailure === 'function' && handleRtmpPlaybackFailure(message, { silent: Boolean(silent) })) {
return;
}
if (placeholder) {
placeholder.style.display = 'flex';
placeholder.textContent = message;
}
}
function clearStartupTimer() {
if (startupTimer) {
window.clearTimeout(startupTimer);
startupTimer = null;
}
if (video.__rtmpStartupTimer) {
video.__rtmpStartupTimer = null;
}
if (probeRetryTimer) {
window.clearTimeout(probeRetryTimer);
probeRetryTimer = null;
}
if (video.__rtmpProbeRetryTimer) {
video.__rtmpProbeRetryTimer = null;
}
if (webReceiveTimer) {
window.clearTimeout(webReceiveTimer);
webReceiveTimer = null;
}
if (video.__rtmpWebReceiveTimer) {
video.__rtmpWebReceiveTimer = null;
}
}
function tryPlay() {
if (video && typeof video.play === 'function') {
video.play().catch(function () {
return null;
});
}
}
function markReceiving() {
if (webReceiveTimer) {
window.clearTimeout(webReceiveTimer);
webReceiveTimer = null;
}
if (video.__rtmpWebReceiveTimer) {
video.__rtmpWebReceiveTimer = null;
}
}
function scheduleReceiveRetry() {
if (isPreload || webReceiveRetryUsed || video.dataset.rtmpFailureHandled === '1') {
return;
}
if (webReceiveTimer) {
return;
}
webReceiveTimer = window.setTimeout(function () {
webReceiveTimer = null;
video.__rtmpWebReceiveTimer = null;
if (video.dataset.rtmpFailureHandled === '1' || webReceiveRetryUsed) {
return;
}
if (video.readyState >= 2) {
return;
}
webReceiveRetryUsed = true;
clearStartupTimer();
try {
if (video.__rtmpHls) {
video.__rtmpHls.destroy();
}
} catch (_error) {
// ignore cleanup errors
}
video.__rtmpHls = null;
requestSession();
}, 1200);
video.__rtmpWebReceiveTimer = webReceiveTimer;
}
function retryWebReceiveOrFail(message, silent) {
if (isPreload || video.dataset.rtmpFailureHandled === '1') {
return;
}
if (!webReceiveRetryUsed) {
webReceiveRetryUsed = true;
clearStartupTimer();
if (webReceiveTimer) {
window.clearTimeout(webReceiveTimer);
webReceiveTimer = null;
}
if (video.__rtmpWebReceiveTimer) {
video.__rtmpWebReceiveTimer = null;
}
window.setTimeout(function () {
requestSession();
}, 500);
return;
}
failPlayback(message, silent);
}
video.dataset.rtmpInitialized = '1';
video.muted = disableAudio;
video.controls = false;
video.playsInline = true;
video.autoplay = true;
bindRtmpVideoPlaceholder(video, placeholder);
video.addEventListener('canplay', markReceiving, { once: true });
video.addEventListener('playing', markReceiving, { once: true });
if (startupTimeoutMs > 0) {
startupTimer = window.setTimeout(function () {
startupTimer = null;
video.__rtmpStartupTimer = null;
failPlayback('Unable to load RTMP stream.', true);
}, startupTimeoutMs);
video.__rtmpStartupTimer = startupTimer;
}
function retryWhileProbing() {
if (probeRetryTimer || video.dataset.rtmpFailureHandled === '1') {
return;
}
probeRetryTimer = window.setTimeout(function () {
probeRetryTimer = null;
video.__rtmpProbeRetryTimer = null;
requestSession();
}, 500);
video.__rtmpProbeRetryTimer = probeRetryTimer;
}
function requestSession() {
fetch(getRtmpSessionUrl(sourceUrl, disableAudio), {
credentials: 'same-origin'
}).then(function (response) {
return response.json().catch(function () {
return null;
}).then(function (payload) {
return {
response: response,
payload: payload
};
});
}).then(function (result) {
if (!result || !result.response) {
throw new Error('Unable to initialize RTMP stream.');
}
var response = result.response;
var payload = result.payload || null;
var playlistUrl = payload && payload.playlistUrl ? String(payload.playlistUrl).trim() : '';
if (!response.ok) {
if (response.status === 503 && payload && payload.probing) {
setRtmpAvailability(sourceUrl, disableAudio, null);
retryWhileProbing();
return null;
}
setRtmpAvailability(sourceUrl, disableAudio, false);
retryWebReceiveOrFail('Unable to load RTMP stream.', true);
return null;
}
setRtmpAvailability(sourceUrl, disableAudio, true);
if (!playlistUrl) {
throw new Error('RTMP playlist URL was not returned.');
}
if (window.Hls && window.Hls.isSupported && window.Hls.isSupported()) {
var hls = new window.Hls({
enableWorker: true,
liveSyncDurationCount: 6,
liveMaxLatencyDurationCount: 12,
maxBufferLength: 30,
maxLiveSyncPlaybackRate: 1,
backBufferLength: 60
});
video.__rtmpHls = hls;
hls.attachMedia(video);
hls.on(window.Hls.Events.MEDIA_ATTACHED, function () {
hls.loadSource(playlistUrl);
scheduleReceiveRetry();
});
hls.on(window.Hls.Events.MANIFEST_PARSED, function () {
tryPlay();
});
hls.on(window.Hls.Events.ERROR, function (_event, data) {
if (data && data.fatal) {
clearStartupTimer();
setRtmpAvailability(sourceUrl, disableAudio, false);
try {
hls.destroy();
} catch (_error) {
// ignore cleanup errors
}
video.__rtmpHls = null;
retryWebReceiveOrFail('RTMP playback failed.', true);
}
});
return;
}
if (video.canPlayType && video.canPlayType('application/vnd.apple.mpegurl')) {
video.src = playlistUrl;
tryPlay();
scheduleReceiveRetry();
return;
}
failPlayback('RTMP playback is not supported in this browser.', true);
return null;
}).catch(function () {
clearStartupTimer();
setRtmpAvailability(sourceUrl, disableAudio, false);
retryWebReceiveOrFail('Unable to load RTMP stream.', true);
});
}
requestSession();
}
function syncRtmpRegions(root) {
if (!root) {
return;
}
var videos = root.querySelectorAll('video[data-rtmp-source]');
Array.prototype.forEach.call(videos, function (video) {
if (!video || video.dataset.rtmpInitialized === '1') {
return;
}
var sourceUrl = String(video.dataset.rtmpSource || '').trim();
var disableAudio = String(video.dataset.rtmpDisableAudio || '1') !== '0';
var region = video.parentNode;
var placeholder = region ? region.querySelector('.template-region-rtmp-placeholder') : null;
if (!sourceUrl) {
if (placeholder) {
placeholder.textContent = 'RTMP stream';
}
return;
}
var skipUnavailable = String(video.dataset.rtmpSkipUnavailable || '0') === '1';
var startupTimer = null;
if (skipUnavailable) {
var preflightStatus = getRtmpAvailability(sourceUrl, disableAudio);
if (preflightStatus && !isRtmpAvailabilityStale(preflightStatus) && preflightStatus.available === false) {
failPlayback('Unable to load RTMP stream.', true);
return;
}
}
function failPlayback(message, silent) {
if (video.dataset.rtmpFailureHandled === '1') {
return;
}
video.dataset.rtmpFailureHandled = '1';
if (skipUnavailable && typeof handleRtmpPlaybackFailure === 'function' && handleRtmpPlaybackFailure(message, { silent: Boolean(silent) })) {
return;
}
if (placeholder) {
placeholder.style.display = 'flex';
placeholder.textContent = message;
}
}
startRtmpPlayback(video, sourceUrl, disableAudio, skipUnavailable, placeholder, false);
});
}
function destroyRtmpRegions(root) {
if (!root) {
return;
}
var videos = root.querySelectorAll('video[data-rtmp-source]');
Array.prototype.forEach.call(videos, function (video) {
if (video.__rtmpStartupTimer) {
window.clearTimeout(video.__rtmpStartupTimer);
video.__rtmpStartupTimer = null;
}
if (video.__rtmpProbeRetryTimer) {
window.clearTimeout(video.__rtmpProbeRetryTimer);
video.__rtmpProbeRetryTimer = null;
}
if (video.__rtmpWebReceiveTimer) {
window.clearTimeout(video.__rtmpWebReceiveTimer);
video.__rtmpWebReceiveTimer = null;
}
if (video.__rtmpHls) {
try {
video.__rtmpHls.destroy();
} catch (_error) {
// ignore cleanup errors
}
video.__rtmpHls = null;
}
});
}
registry.register('rtmp', {
renderRegion: renderRtmpRegion
});