// General sanitization and sizing helpers. // Strip unsupported characters from a font family string. var playerRenderingShared = window.pulsePlayerRenderingShared; var escapeHtml = playerRenderingShared.escapeHtml; var normalizeStyleAttributeValue = playerRenderingShared.normalizeStyleAttributeValue; var sanitizeFontFamily = playerRenderingShared.sanitizeFontFamily; var sanitizeFontSize = playerRenderingShared.sanitizeFontSize; var sanitizeTextColor = playerRenderingShared.sanitizeTextColor; var sanitizeRichTextAttributes = playerRenderingShared.sanitizeRichTextAttributes; var sanitizeRichText = playerRenderingShared.sanitizeRichText; var renderEditorJsBlock = playerRenderingShared.renderEditorJsBlock; var renderEditorJsListItem = playerRenderingShared.renderEditorJsListItem; var renderEditorJsTable = playerRenderingShared.renderEditorJsTable; var renderEditorJsContent = playerRenderingShared.renderEditorJsContent; var parseMaybeJson = playerRenderingShared.parseMaybeJson; var normalizeContentValue = playerRenderingShared.normalizeContentValue; var fitCanvasSize = playerRenderingShared.fitCanvasSize; var decorateRegionMarkup = playerRenderingShared.decorateRegionMarkup; function getTemplateCanvasSize(template) { return { width: Math.max(1, Number(template.canvas_size_width || 1920)), height: Math.max(1, Number(template.canvas_size_height || 1080)) }; } function selectPlayerMediaSource(originalSource, variants, targetDimension) { var original = String(originalSource || '').trim(); var target = Math.max(1, Number(targetDimension || 0) || 1); var candidates = (Array.isArray(variants) ? variants : []).filter(function (variant) { return variant && String(variant.url || '').trim() && Number(variant.width) >= target; }).sort(function (left, right) { return Number(left.width) - Number(right.width); }); return candidates.length ? String(candidates[0].url) : original; } function getPlaylistRevision(data) { if (data && data.revision) { return String(data.revision); } if (data && data.playlist && data.playlist.revision) { return String(data.playlist.revision); } if (currentPlaylistEtag) { return String(currentPlaylistEtag).replace(/^"|"$/g, ''); } return String(Date.now()); } function setPlayerCanvasDimensions(canvasWidth, canvasHeight) { if (!document || !document.documentElement) { return; } var width = Math.max(1, Math.round(Number(canvasWidth) || 0) || 0); var height = Math.max(1, Math.round(Number(canvasHeight) || 0) || 0); document.documentElement.style.setProperty('--player-canvas-width', width + 'px'); document.documentElement.style.setProperty('--player-canvas-height', height + 'px'); } // Normalize a slide and its nested region content. function normalizeSlide(slide) { var normalized = playerRenderingShared.normalizeSlide(slide); if (normalized.use_video_duration && normalized.template && Array.isArray(normalized.template.regions)) { normalized.template.regions.forEach(function (region) { if (region && region.region_type === 'video' && normalized.content[region.region_key]) { normalized.content[region.region_key].loop = false; } }); } return normalized; } // Parse the stored schedule-day list into numbers. function parseScheduleDays(value) { if (!value) { return []; } if (Array.isArray(value)) { return value.map(function (item) { return Number(item); }).filter(function (item) { return !Number.isNaN(item); }); } try { const parsed = JSON.parse(value); return Array.isArray(parsed) ? parsed.map(function (item) { return Number(item); }).filter(function (item) { return !Number.isNaN(item); }) : []; } catch (_error) { return []; } } // Convert a HH:MM time string to minutes since midnight. function parseTimeToMinutes(value) { const raw = String(value || '').trim(); if (!raw) { return null; } const match = raw.match(/^(\d{2}):(\d{2})/); if (!match) { return null; } return Number(match[1]) * 60 + Number(match[2]); } function normalizeScheduleRule(rule) { const input = rule && typeof rule === 'object' ? rule : {}; const startDatetime = String(input.start_datetime || input.startDateTime || '').trim(); const endDatetime = String(input.end_datetime || input.endDateTime || '').trim(); const startTime = String(input.start_time || input.startTime || '').trim(); const endTime = String(input.end_time || input.endTime || '').trim(); const days = parseScheduleDays(input.days); if (startDatetime || endDatetime) { if (!startDatetime || !endDatetime) { return null; } } if (startTime || endTime) { if (!startTime || !endTime) { return null; } } if (!startDatetime && !endDatetime && !startTime && !endTime && !days.length) { return null; } return { start_datetime: startDatetime || null, end_datetime: endDatetime || null, start_time: startTime || null, end_time: endTime || null, days: days }; } function parseScheduleRules(value) { let rawRules = []; if (Array.isArray(value)) { rawRules = value; } else { const raw = String(value || '').trim(); if (!raw) { return []; } try { const parsed = JSON.parse(raw); rawRules = Array.isArray(parsed) ? parsed : []; } catch (_error) { return []; } } return rawRules.map(normalizeScheduleRule).filter(Boolean); } function matchesScheduleRule(rule, now) { const normalized = normalizeScheduleRule(rule); if (!normalized) { return false; } if (normalized.start_datetime && normalized.end_datetime) { const start = new Date(normalized.start_datetime); const end = new Date(normalized.end_datetime); if (Number.isNaN(start.getTime()) || Number.isNaN(end.getTime())) { return false; } if (now < start || now > end) { return false; } } if (normalized.days.length && normalized.days.indexOf(now.getDay()) === -1) { return false; } if (normalized.start_time && normalized.end_time) { const startMinutes = parseTimeToMinutes(normalized.start_time); const endMinutes = parseTimeToMinutes(normalized.end_time); if (startMinutes === null || endMinutes === null) { return false; } const nowMinutes = now.getHours() * 60 + now.getMinutes(); if (startMinutes <= endMinutes) { return nowMinutes >= startMinutes && nowMinutes <= endMinutes; } return nowMinutes >= startMinutes || nowMinutes <= endMinutes; } return true; } // Determine whether a slide should be shown at the current time. function isSlideActive(slide, now) { const rules = Array.isArray(slide.scheduleRules) ? slide.scheduleRules : []; if (rules.length && !rules.some(function (rule) { return matchesScheduleRule(rule, now); })) { return false; } const groupRules = parseScheduleRules(slide.groupScheduleRules || slide.group_schedule_rules_json); if (slide.groupKey && groupRules.length && !groupRules.some(function (rule) { return matchesScheduleRule(rule, now); })) { return false; } return true; } // Build the cache key for a template layout. function getTemplateLayoutCacheKey(template) { var viewportKey = window.innerWidth + 'x' + window.innerHeight; return [currentPlaylistSignature || '', template && template.id ? template.id : '', viewportKey].join('|'); } // Build or reuse layout metadata for a template. function getTemplateLayout(template) { if (!template || !template.id) { return null; } syncRenderCacheViewport(); var cacheKey = getTemplateLayoutCacheKey(template); if (Object.prototype.hasOwnProperty.call(templateLayoutCache, cacheKey)) { return templateLayoutCache[cacheKey]; } var templateCanvas = getTemplateCanvasSize(template); var canvasSize = fitCanvasSize(templateCanvas.width, templateCanvas.height, window.innerWidth, window.innerHeight); var canvasScale = canvasSize.width / templateCanvas.width; var regions = (template.regions || []).map(function (region) { var left = (Number(region.x) / templateCanvas.width) * 100; var top = (Number(region.y) / templateCanvas.height) * 100; var width = (Number(region.width) / templateCanvas.width) * 100; var height = (Number(region.height) / templateCanvas.height) * 100; var baseStyle = 'left:' + left + '%;top:' + top + '%;width:' + width + '%;height:' + height + '%;z-index:' + Number(region.z_index || 0) + ';'; var pixelWidth = Math.max(1, Math.round(Number(region.width || 0) || 1)); var pixelHeight = Math.max(1, Math.round(Number(region.height || 0) || 1)); return { regionKey: region.region_key, regionType: region.region_type, label: region.label, animationJson: region.animation_json || null, baseStyle: baseStyle, pixelWidth: pixelWidth, pixelHeight: pixelHeight, canvasWidth: templateCanvas.width, canvasHeight: templateCanvas.height, fontFamily: region.font_family || null, fontSize: region.font_size || null, fontColor: region.font_color || null, canvasScale: canvasScale }; }); var layout = { canvasWidth: canvasSize.width, canvasHeight: canvasSize.height, background: template.background_image_path ? '' : '', backgroundColor: template.background_color || '#111111', backgroundGradient: template.background_gradient || '', regions: regions }; templateLayoutCache[cacheKey] = layout; return layout; } // Build a dark backdrop style for template and media canvases. function buildBackdropStyle(backgroundColor, backgroundImagePath, backgroundGradient) { var color = String(backgroundColor || '#111111').trim() || '#111111'; var style = 'background-color:' + escapeHtml(color) + ';'; var gradient = ''; try { var gradientData = typeof backgroundGradient === 'string' ? JSON.parse(backgroundGradient) : backgroundGradient; if (gradientData && gradientData.type === 'linear' && ((Array.isArray(gradientData.stops) && gradientData.stops.length >= 2) || (Array.isArray(gradientData.colors) && gradientData.colors.length >= 2))) { var stops = Array.isArray(gradientData.stops) ? gradientData.stops : (gradientData.colors || []).map(function (color, index, colors) { return { color: color, position: colors.length > 1 ? Math.round(index * 100 / (colors.length - 1)) : 0 }; }); stops = stops.filter(function (stop) { return stop && /^#[0-9a-fA-F]{3,8}$/.test(String(stop.color || '').trim()); }).slice(0, 12); if (stops.length >= 2) { var angle = Number(gradientData.angle); gradient = 'linear-gradient(' + (Number.isFinite(angle) ? Math.max(0, Math.min(360, angle)) : 90) + 'deg,' + stops.map(function (stop) { return stop.color + ' ' + Math.max(0, Math.min(100, Number(stop.position) || 0)) + '%'; }).join(',') + ')'; } } } catch (_error) { gradient = ''; } if (backgroundImagePath) { style += 'background-image:url("' + escapeHtml(backgroundImagePath) + '")' + (gradient ? ',' + gradient : '') + ';'; style += 'background-position:center,center;background-size:contain,cover;background-repeat:no-repeat,no-repeat;'; return style; } style += 'background-image:' + (gradient || 'none') + ';background-position:center;background-size:cover;background-repeat:no-repeat;'; return style; } // Build the cache key for a template render plan. function getTemplateRenderPlanCacheKey(template) { return getTemplateLayoutCacheKey(template); } // Build or reuse the render plan for a template. function getTemplateRenderPlan(template) { if (!template || !template.id) { return null; } syncRenderCacheViewport(); var cacheKey = getTemplateRenderPlanCacheKey(template); if (Object.prototype.hasOwnProperty.call(templateRenderPlanCache, cacheKey)) { return templateRenderPlanCache[cacheKey]; } var layout = getTemplateLayout(template); function getPlayerRegionModule(regionType) { return window.pulsePlayerRegionTypes && typeof window.pulsePlayerRegionTypes.get === 'function' ? window.pulsePlayerRegionTypes.get(regionType) : null; } var plan = { layout: layout, renderRegion: function (region, regionContent) { var regionModule = getPlayerRegionModule(region.regionType); if (regionModule && typeof regionModule.renderRegion === 'function') { return regionModule.renderRegion(region, regionContent); } if (region.regionType === 'image') { return renderImageRegion(region, regionContent); } if (region.regionType === 'video') { return renderVideoRegion(region, regionContent); } if (region.regionType === 'webpage') { return renderWebpageRegion(region, regionContent); } if (region.regionType === 'rtmp') { return renderRtmpRegion(region, regionContent); } if (region.regionType === 'rss') { return renderRssRegion(region, regionContent); } if (region.regionType === 'api') { return renderApiRegion(region, regionContent); } if (region.regionType === 'html') { return renderHtmlRegion(region, regionContent); } return renderTextRegion(region, regionContent); } }; templateRenderPlanCache[cacheKey] = plan; return plan; } // Render a template-based slide using the cached layout. function renderTemplateSlideMarkup(slide) { const template = slide.template; const content = slide.content || {}; const plan = getTemplateRenderPlan(template); const layout = plan ? plan.layout : null; const regions = layout ? layout.regions.map(function (region) { const regionContent = content[region.regionKey] || {}; const markup = plan.renderRegion(region, regionContent); return decorateRegionMarkup(markup, { animationConfig: normalizePlayerAnimationConfig(region.animationJson) }); }).join('') : ''; const backgroundPath = layout ? selectPlayerMediaSource(template.background_image_path, template.background_image_variants, Math.max(layout.canvasWidth, layout.canvasHeight)) : ''; const stageStyle = layout ? buildBackdropStyle(layout.backgroundColor || '#111111', backgroundPath, layout.backgroundGradient) : ''; if (layout) { setPlayerCanvasDimensions(layout.canvasWidth, layout.canvasHeight); } return renderSlideShell(slide, '', (layout ? layout.canvasWidth : 0) + 'px', (layout ? layout.canvasHeight : 0) + 'px', '
' + (layout ? layout.background : '') + regions + '
'); } // Media rendering helpers. // Build the direct media element for a slide. function renderMediaSlideContent(slide) { if (slide.kind === 'image') { return 'slide'; } return ''; } // Render a slide that contains direct media content. function renderMediaSlideMarkup(slide) { const canvasSize = fitCanvasSize(16, 9, window.innerWidth, window.innerHeight); const media = renderMediaSlideContent(slide); const canvasStyle = slide.kind === 'image' ? buildBackdropStyle('#111111', slide.media_url) : ''; setPlayerCanvasDimensions(canvasSize.width, canvasSize.height); return renderSlideShell(slide, 'slide-media', canvasSize.width + 'px', canvasSize.height + 'px', media, canvasStyle); } // Render the shared slide shell around slide-specific inner content. function renderSlideShell(slide, canvasClass, canvasWidth, canvasHeight, innerHtml, canvasStyle) { const body = slide.body ? '
' + escapeHtml(slide.body) + '
' : ''; const className = canvasClass ? 'slide-canvas ' + canvasClass : 'slide-canvas'; const style = 'width:' + canvasWidth + ';height:' + canvasHeight + ';' + (canvasStyle || ''); return '
' + innerHtml + body + '
'; } // Build the cache key for rendered slide markup. function getSlideMarkupCacheKey(slide) { var viewportKey = window.innerWidth + 'x' + window.innerHeight; return [currentPlaylistSignature || '', slide && slide.id ? slide.id : '', slide && slide.template_id ? slide.template_id : '', slide && slide.modified_at ? slide.modified_at : '', viewportKey, videoRegionRenderVersion || 0].join('|'); } function restorePlayerCanvasDimensions(width, height) { if (!document || !document.documentElement) { return; } var style = document.documentElement.style; if (width) { style.setProperty('--player-canvas-width', width); } else { style.removeProperty('--player-canvas-width'); } if (height) { style.setProperty('--player-canvas-height', height); } else { style.removeProperty('--player-canvas-height'); } } function primeSlideMarkup(slide) { if (!slide) { return ''; } var cachedMarkup = getCachedSlideMarkup(slide); if (cachedMarkup) { return cachedMarkup; } var previousWidth = document && document.documentElement && document.documentElement.style ? String(document.documentElement.style.getPropertyValue('--player-canvas-width') || '') : ''; var previousHeight = document && document.documentElement && document.documentElement.style ? String(document.documentElement.style.getPropertyValue('--player-canvas-height') || '') : ''; try { return buildSlideMarkupForState(slide, false); } finally { restorePlayerCanvasDimensions(previousWidth.trim(), previousHeight.trim()); } } function notifyVideoRegionSourceReady() { if (typeof videoRegionRenderVersion === 'number') { videoRegionRenderVersion += 1; } slideMarkupCache = Object.create(null); if (typeof showCurrent === 'function' && slides && slides.length) { showCurrent({ skipFade: true }); } } window.notifyVideoRegionSourceReady = notifyVideoRegionSourceReady; // Look up a previously rendered slide in the cache. function getCachedSlideMarkup(slide) { var cacheKey = getSlideMarkupCacheKey(slide); return Object.prototype.hasOwnProperty.call(slideMarkupCache, cacheKey) ? slideMarkupCache[cacheKey] : null; } // Store rendered slide markup in the cache. function setCachedSlideMarkup(slide, markup) { syncRenderCacheViewport(); slideMarkupCache[getSlideMarkupCacheKey(slide)] = markup; } // Slide rendering and markup cache helpers. // Choose the right slide renderer and cache the result. function buildSlideMarkupForState(slide, updateCurrentState) { if (updateCurrentState) { lastRenderedSlide = slide || null; syncBlackoutState(); } var cachedMarkup = getCachedSlideMarkup(slide); if (cachedMarkup) { return cachedMarkup; } var markup = ''; if (slide.template_id && slide.template) { markup = renderTemplateSlideMarkup(slide); setCachedSlideMarkup(slide, markup); return markup; } markup = renderMediaSlideMarkup(slide); setCachedSlideMarkup(slide, markup); return markup; } function buildSlideMarkup(slide) { return buildSlideMarkupForState(slide, true); }