mirror of
https://github.com/screentinker/screentinker.git
synced 2026-08-13 13:53:12 -06:00
Transitions have never run on BrightSign, and it was never a GPU problem.
`transitionRuntimeReady()` is a presence check on three globals and touches no
WebGL at all. A BrightSign roHtmlWidget is created with `nodejs_enabled: true`,
which puts Node's `module` into classic-script scope — so every shared module
that exported with an `else` took the CommonJS branch and never assigned its
browser global. The runtime was absent before WebGL was ever asked a question.
This is deducible from the fleet without touching the hardware: the player
pushes system.reboot / display.power / display.resolution / system.self_update
only behind BS.hasHost(), which needs require('@brightsign/messageport') to
resolve. Our XT245's stored capability row carries all four, so Node
integration was live in that page, so the CommonJS branch was taken.
Transitions are the least of it. schedule-eval.js had the same shape, and the
player falls back to "always active" when ScheduleEval is missing — so per-item
DAYPARTING silently stopped applying on that platform and scheduled content
played outside its window with nothing in any log. player-media-health.js the
same. Four files, all fixed by exporting to BOTH targets rather than either/or.
media-mute.js, orientation-style.js and wall-geometry.js already assigned their
globals in a separate unconditional block and were never affected; the audit
that reached me claimed all seven, and reading them is what separated the four
from the three.
THE GUARD, WITHOUT WHICH THE ABOVE IS A REGRESSION.
Restore the globals alone and BrightSign starts attempting video wipes it
cannot supply. On a hardware video plane drawImage(video) succeeds, throws
nothing, and paints a fully TRANSPARENT frame — so the wipe fades from nothing,
behind a video plane that is still lit. Worse than the hard cut it replaces.
The discriminator already existed: videoFrameIsCapturable() probes ALPHA, so a
genuine fade-to-black still reads as captured. It was wired into the screenshot
path and not this one, which asked isMediaReadable() — a CORS question, "am I
allowed to read this", not "did any pixels arrive". Both the outgoing frame and
the incoming warm-play snapshot now consult it, cached per platform, defaulting
to available while undetermined so a cold start is not crippled.
Net effect on BrightSign: image-to-image transitions light up, anything
involving video hard-cuts honestly, and dayparting starts working.
Full video transitions are reachable later — BrightSign documents that video
"captured as a canvas for WebGL processing must be routed to the GPU" via a
per-element hwz="off", which keeps hardware decode at an 8-bit/1080p ceiling.
That needs the hardware to validate and is not in this change.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014skWYXJUWhF73EvNPgB2AS
106 lines
5 KiB
JavaScript
106 lines
5 KiB
JavaScript
// Canonical per-playlist-item schedule evaluator (#74 dayparting + #75 expiry).
|
|
//
|
|
// CONTRACT: shared/schedule-vectors.json. The JS server, the web player, and the
|
|
// Tizen player all consume this exact module; the Android (Kotlin) port must agree
|
|
// with the same vectors. If an implementation disagrees with a vector, the
|
|
// implementation is wrong.
|
|
//
|
|
// Time model: instants are UTC; schedule blocks are LOCAL wall-clock rules. We take
|
|
// utc_now, convert to device-local wall-clock via the device's IANA timezone (DST
|
|
// handled by Intl), then test the block(s). Blocks are never stored/transmitted in
|
|
// UTC - that would break across DST and zone changes.
|
|
//
|
|
// Block = { days:[0-6 (0=Sun)], start:"HH:MM", end:"HH:MM"|"24:00",
|
|
// start_date:"YYYY-MM-DD"|null, end_date:"YYYY-MM-DD"|null }
|
|
// - within a block: day AND date AND time must all pass
|
|
// - blocks OR together; >=1 match = active
|
|
// - zero blocks = always active (this is the "no schedule = always plays" fallback)
|
|
// - time window is [start, end): start inclusive, end exclusive ("24:00" = end of day)
|
|
// - start > end means the window crosses midnight; the day/date test anchors to the
|
|
// day the window STARTED (a Fri 22:00-02:00 block is active Sat 01:00).
|
|
//
|
|
// Dependency-free UMD: Node (require) + browser/Tizen (window.ScheduleEval).
|
|
|
|
(function (root, factory) {
|
|
// BOTH, not either/or: a BrightSign widget runs with Node integration, so `module` exists in
|
|
// page scope and an `else` left root.ScheduleEval undefined there. The player falls back to
|
|
// "always active" when it is missing — i.e. per-item DAYPARTING silently stopped applying on
|
|
// that platform, and scheduled content played outside its window with nothing in any log.
|
|
var api = factory();
|
|
if (typeof module === 'object' && module.exports) module.exports = api;
|
|
if (root) root.ScheduleEval = api;
|
|
})(typeof self !== 'undefined' ? self : this, function () {
|
|
'use strict';
|
|
|
|
var DOW = { Sun: 0, Mon: 1, Tue: 2, Wed: 3, Thu: 4, Fri: 5, Sat: 6 };
|
|
|
|
// UTC instant -> device-local {y, mo(1-12), day, dow(0-6), min(0-1439)}.
|
|
// ianaTz falsy -> trust the runtime's own local clock as-is (the device's OS time).
|
|
function localParts(utcNow, ianaTz) {
|
|
var d = (utcNow instanceof Date) ? utcNow : new Date(utcNow);
|
|
if (!ianaTz) {
|
|
return { y: d.getFullYear(), mo: d.getMonth() + 1, day: d.getDate(), dow: d.getDay(), min: d.getHours() * 60 + d.getMinutes() };
|
|
}
|
|
var fmt = new Intl.DateTimeFormat('en-US', {
|
|
timeZone: ianaTz, year: 'numeric', month: '2-digit', day: '2-digit',
|
|
hour: '2-digit', minute: '2-digit', hourCycle: 'h23', weekday: 'short'
|
|
});
|
|
var p = {}, parts = fmt.formatToParts(d);
|
|
for (var i = 0; i < parts.length; i++) p[parts[i].type] = parts[i].value;
|
|
var hh = parseInt(p.hour, 10) % 24; // h23 yields 00-23; guard against env quirks
|
|
return { y: +p.year, mo: +p.month, day: +p.day, dow: DOW[p.weekday], min: hh * 60 + (+p.minute) };
|
|
}
|
|
|
|
function hm(s) { var a = String(s).split(':'); return (+a[0]) * 60 + (+a[1]); } // "24:00" -> 1440
|
|
|
|
function ymd(y, mo, day) { function p2(n) { return (n < 10 ? '0' : '') + n; } return y + '-' + p2(mo) + '-' + p2(day); }
|
|
|
|
// Pure calendar arithmetic (UTC Date used only for date math, never time/DST).
|
|
function addDays(y, mo, day, delta) {
|
|
var d = new Date(Date.UTC(y, mo - 1, day));
|
|
d.setUTCDate(d.getUTCDate() + delta);
|
|
return { y: d.getUTCFullYear(), mo: d.getUTCMonth() + 1, day: d.getUTCDate() };
|
|
}
|
|
|
|
function dayOk(dow, days) {
|
|
if (!days || !days.length) return false;
|
|
for (var i = 0; i < days.length; i++) if (days[i] === dow) return true;
|
|
return false;
|
|
}
|
|
|
|
function dateOk(dateStr, startDate, endDate) {
|
|
if (startDate && dateStr < startDate) return false; // ISO YYYY-MM-DD sorts lexicographically
|
|
if (endDate && dateStr > endDate) return false; // inclusive on both ends
|
|
return true;
|
|
}
|
|
|
|
function blockMatches(b, L) {
|
|
var s = hm(b.start), e = hm(b.end), now = L.min;
|
|
if (s <= e) {
|
|
// same-day window [s, e), anchored to today
|
|
if (now < s || now >= e) return false;
|
|
return dayOk(L.dow, b.days) && dateOk(ymd(L.y, L.mo, L.day), b.start_date, b.end_date);
|
|
}
|
|
// overnight wrap
|
|
if (now >= s) {
|
|
// before-midnight portion: anchor = today
|
|
return dayOk(L.dow, b.days) && dateOk(ymd(L.y, L.mo, L.day), b.start_date, b.end_date);
|
|
}
|
|
if (now < e) {
|
|
// after-midnight portion: anchor = the day it started = yesterday (device-local)
|
|
var y = addDays(L.y, L.mo, L.day, -1);
|
|
return dayOk((L.dow + 6) % 7, b.days) && dateOk(ymd(y.y, y.mo, y.day), b.start_date, b.end_date);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function isItemActiveNow(blocks, utcNow, ianaTz) {
|
|
if (!blocks || blocks.length === 0) return true; // no schedule = always on
|
|
var L = localParts(utcNow, ianaTz);
|
|
for (var i = 0; i < blocks.length; i++) if (blockMatches(blocks[i], L)) return true;
|
|
return false;
|
|
}
|
|
|
|
return { isItemActiveNow: isItemActiveNow, _localParts: localParts, _blockMatches: blockMatches };
|
|
});
|