Files
pulse-signage/src/player/public/js/player-page-rendering.js
T
lzstealth 7295f83f04
Publish Docker Image / build-and-push-existing-registry (./build/Dockerfile, web, pulse-signage-web) (push) Successful in 4m34s
Publish Docker Image / build-and-push-existing-registry (./build/Dockerfile.player, player, pulse-signage-player) (push) Successful in 57s
Release v2.15.2
2026-10-15 01:50:39 +01:00

502 lines
19 KiB
JavaScript

// 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 ? '<img class="template-background" src="' + escapeHtml(selectPlayerMediaSource(template.background_image_path, template.background_image_variants, Math.max(templateCanvas.width, templateCanvas.height))) + '" alt="" />' : '',
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', '<div class="template-stage" style="' + stageStyle + '">' + (layout ? layout.background : '') + regions + '</div>');
}
// Media rendering helpers.
// Build the direct media element for a slide.
function renderMediaSlideContent(slide) {
if (slide.kind === 'image') {
return '<img src="' + escapeHtml(slide.media_url) + '" alt="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 ? '<div class="body">' + escapeHtml(slide.body) + '</div>' : '';
const className = canvasClass ? 'slide-canvas ' + canvasClass : 'slide-canvas';
const style = 'width:' + canvasWidth + ';height:' + canvasHeight + ';' + (canvasStyle || '');
return '<div class="slide"><div class="' + className + '" style="' + style + '">' + innerHtml + body + '</div></div>';
}
// 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);
}