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 '
Loading RTMP stream...
'; } 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 });