@openbrf/display
v0.1.2
Published
The Open BRF screen content contract, the rotation builder, the renderer and the fetch loop that every screen showing an association's content is built from.
Maintainers
Readme
@openbrf/display
What every screen showing a housing cooperative's content is built from, in Open BRF: the screen content contract, the rotation builder, the renderer and the fetch loop. The core's display mode (a browser opened on a display link) and the digital signage module's player both use all four, so a screen shows the association's news, events, the notice of the next general meeting and free/busy availability the same way whichever of them it runs.
It uses no framework and no DOM library, and has one dependency,
@openbrf/tokens, for the
default theme's colours.
Installing
npm install @openbrf/displayThe four parts
- The screen content contract. The
ScreenContentdocument, version 1, as types (ScreenContent,Slide, the body kinds), its limits (SCREEN_CONTENT_LIMITS,SLIDE_ID_PATTERN), andparseScreenContent, which reads a document by the player's rules: unknown fields are ignored, a slide of an unknown or malformed kind is skipped, and an unknownversionis refused with aScreenContentError. - The rotation builder.
buildRotationturns the news items, occurrences, meeting notices, bookable resources and busy intervals a caller has read, and the moment of the fetch, into thenews,events,meetingandavailabilityslides. It reads nothing on its own.availabilityWindowgives the window an availability slide covers: from the minute of the fetch to the end of the next day in the association's time zone. - The renderer.
mountRenderershows a document on an element: the rotation with each slide'ssecondsanduntil, the schedule, the clock band, the orientation, the theme and every body kind, in Swedish or English. Nothing a slide holds is cut off: a slide the screen cannot hold whole is shown a page at a time, each page for the slide'sseconds. A list, such as eight events, goes as many items to a page as fit whole; a page still too tall is set smaller, and a text too tall even then, such as a news item of fifty paragraphs, continues on the next page, broken between two words, or a word longer than a page between two of its characters. It fetches nothing; a picture comes from a function you hand it. It adds its stylesheet (DISPLAY_STYLESHEET, every class prefixedobrf-display) to the document once, as a<style>element. A page whose content security policy has no'unsafe-inline'instyle-srcserves the stylesheet as a file instead: it links the file as<link rel="stylesheet" data-obrf-display href="...">and passesstylesheet: falsetomountRenderer. The renderer then adds no<style>element and sets nostyleattribute, only properties through the style object, which such a policy allows. A page that has alinkorstylewithdata-obrf-displayin its head gets no second copy either way. - The fetch loop.
startFetchLoopasks for screen content with a device token and acts on each answer: 200 and 304 show content, a 401 withrealm="device"anddevice-token-invalidforgets it and stops, 404 pauses, 429 waits, and anything else shows the last content for at most 24 hours. Where the content is cached is yours to choose.
Usage
import { mountRenderer, startFetchLoop } from "@openbrf/display";
const renderer = mountRenderer(document.getElementById("screen")!, {
// Fetch the picture with the token in a header, never in its URL.
picture: async (ref) => {
const response = await fetch(
new URL(`pictures/${encodeURIComponent(ref.mediaFileId)}`, contentUrl),
{ headers: { Authorization: `Bearer ${token}` }, credentials: "omit" },
);
return response.ok ? URL.createObjectURL(await response.blob()) : null;
},
releasePicture: (url) => URL.revokeObjectURL(url),
});
renderer.message("loading", "sv");
startFetchLoop({
url: contentUrl,
token: () => localStorage.getItem("token"),
cache: {
read: () => JSON.parse(localStorage.getItem("content") ?? "null"),
write: (entry) => localStorage.setItem("content", JSON.stringify(entry)),
clear: () => localStorage.removeItem("content"),
},
onContent: (content, status) => renderer.show(content, status),
onRevoked: () => {
localStorage.removeItem("token");
renderer.message("link-invalid", "sv");
},
onPaused: () => renderer.message("paused", "sv"),
onDisconnected: () => renderer.message("not-connected", "sv"),
});Give the element a size, such as the whole viewport: the canvas fills it and is turned by the document's orientation.
Browsers
The renderer and its stylesheet run in Chromium 85, the browser of Samsung's
2021 Tizen displays, without a shim: the package calls nothing newer and writes
no CSS newer. A test fails when one of the newer calls or CSS features it
lists, such as replaceChildren, Array.prototype.at or inset, is added;
anything it does not list is for review to catch. A word longer than a page
is broken between characters with Intl.Segmenter where the browser has it
(Chromium 87), and between code points where it does not.
Versions
The package carries version 1 of the screen content contract. A field or a body kind may be added within version 1, and an older renderer ignores what it does not know. A change an older renderer would misread raises the contract's version, which is served beside version 1, and the package then carries both.
Documentation
- The screen content contract
- ADR 0050: the display mode, and one renderer for every screen
- ADR 0034: digital signage on the public plugin API
Licence
AGPL-3.0-only, with the Open BRF Module Exception. A player or shell built on this package is a module under that exception and may be licensed on any terms. Both texts are in the package.
