Socket
Socket
Sign inDemoInstall

litfass

Package Overview
Dependencies
Maintainers
1
Versions
17
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

litfass - npm Package Compare versions

Comparing version 1.4.2 to 1.5.0

358

litfass.js
"use strict";
const { EventEmitter } = require('events');
const puppeteer = require('puppeteer');

@@ -57,171 +59,215 @@ const getDisplays = require('displays');

exports.start = async (app) => {
// read the configuration and create a deep copy
const config = merge.all([require('config').get('litfass') || {}]);
module.exports = new (class Litfass extends EventEmitter {
displays = [] // an array of displays after litfass was started
// normalize the configuration
config.launchTimeout = ((config.launchTimeout | 0) || DEFAULT_LAUNCH_TIMEOUT) * 1e3; // normalize to milliseconds
config.preparePages = 'preparePages' in config ? config.preparePages : true; // by default prepare one page in advance
config.preparationTime = Math.max(( config.preparationTime | 0 ) || DEFAULT_PREPARATION_TIME, 0) * 1e3; // normalize to milliseconds
config.watchNumberOfDisplays = 'watchNumberOfDisplays' in config ? !!config.watchNumberOfDisplays : true; // detect if the number of displays has changed
// check if there is at least one display defined
if (!Array.isArray(config.displays)) {
throw new Error(`Litfaß needs a 'displays' array`);
} else if(!config.displays.length) {
throw new Error(`Litfaß needs at least one display configured`);
}
// create a scheduler, which can be closed when litfaß exists
const scheduler = new Scheduler(), sleep = scheduler.sleep.bind(scheduler);
// create browsers for each display
const displays = getDisplays().sort((displayA, displayB) =>
// sort all displays from top to bottom, left to right
displayA.top - displayB.top || displayA.left - displayB.left);
await Promise.all(displays.map(async (display, index) => {
// normalize the configuration for this display
Object.assign(display, config.displays[index] || config.displays[0], {
launch: true, currentPage: -1, currentTab: -1 });
// check if this display should be ignored by litfaß
if(display.ignore) {
return;
async start(settings) {
const displays = this.displays;
if (displays.length) {
throw new Error(`Litfaß is already running`);
} else if (!(this.settings = settings).ignoreConfiguration) {
process.env.SUPPRESS_NO_CONFIG_WARNING = true;
const config = require('config-uncached')(true);
// read & resolve the configuration and create a deep copy
config.util.setModuleDefaults('litfass', settings);
settings = merge.all([(await (require('config/async')
.resolveAsyncConfigs(config))).get('litfass') || {}]);
}
// check if the display has a pages array
if (!Array.isArray(display.pages)) {
throw new Error(`Display ${index} needs a 'pages' array`);
} else if(!display.pages.length) {
throw new Error(`Display ${index} needs at least one page to display`);
// normalize the settings
settings.launchTimeout = ((settings.launchTimeout | 0) || DEFAULT_LAUNCH_TIMEOUT) * 1e3; // normalize to milliseconds
settings.preparePages = 'preparePages' in settings ? settings.preparePages : true; // by default prepare one page in advance
settings.preparationTime = Math.max(( settings.preparationTime | 0 ) || DEFAULT_PREPARATION_TIME, 0) * 1e3; // normalize to milliseconds
settings.watchDisplays = 'watchDisplays' in settings ? !!settings.watchDisplays : true; // detect if the number of displays has changes (both physical, or if a browser was closed)
// check if there is at least one display defined
if (!Array.isArray(settings.displays)) {
throw new Error(`Litfaß needs a 'displays' array`);
} else if(!settings.displays.length) {
throw new Error(`Litfaß needs at least one display configured`);
}
// create a scheduler, which can be closed when litfaß exists
const scheduler = this.scheduler = new Scheduler(), sleep = scheduler.sleep.bind(scheduler);
// get all displays and sort them from top to bottom, left to right
displays.splice(0, Number.MAX_SAFE_INTEGER, ...getDisplays().sort((displayA, displayB) =>
displayA.top - displayB.top || displayA.left - displayB.left));
// normalize the rotationSpeed and URL into an array of page objects
display.pages = display.pages.map((url, index) => ({
url, airTime: ((Array.isArray(display.rotationSpeed) ?
(display.rotationSpeed[index] | 0) : (display.rotationSpeed | 0))
|| DEFAULT_AIR_TIME) * 1e3 // normalize to milliseconds
}));
// normalize the transitionAnimation to an object
if (!display.transitionAnimation || typeof display.transitionAnimation !== 'object') {
display.transitionAnimation = { name: display.transitionAnimation };
}
const transitionAnimation = display.transitionAnimation = {
name: display.transitionAnimation.name || DEFAULT_TRANSITION_ANIMATION.name,
duration: (display.transitionAnimation.duration | 0) || DEFAULT_TRANSITION_ANIMATION.duration
};
// check if a valid transition was specified and replace the durations
if (!(transitionAnimation.name in TRANSITION_ANIMATIONS)) {
throw new Error(`Display ${index} has an unknown transition animation '${transitionAnimation.name}'`);
} else if(transitionAnimation.name !== 'none') {
transitionAnimation.halfDuration = (transitionAnimation.duration / 2) | 0;
for (const key of ['in', 'after', 'out']) {
transitionAnimation[key] = `html { ${ TRANSITION_ANIMATIONS[transitionAnimation.name][key]
.replace('@duration', transitionAnimation.halfDuration) } }`
// create browsers for each display
await Promise.all(displays.map(async (display, index) => {
// normalize the settings for this display
Object.assign(display, settings.displays[index] || settings.displays[0], {
launch: true, currentPage: -1, currentTab: -1 });
// check if this display should be ignored by litfaß
if(display.ignore) {
return;
}
} else {
// no animation takes no time to animate
transitionAnimation.halfDuration = 0;
}
// launch one browser per display
const browser = display.browser = await puppeteer.launch(merge.all([config.browserOptions || {}, display.browserOptions || {}, {
headless: false,
defaultViewport: null, /* do not set any viewport => full screen */
ignoreDefaultArgs: ['--enable-automation'],
args: [
'--kiosk' /* launch in full-screen */,
`--window-position=${display.left},${display.top}`
]
}]));
// if all displays have been closed, exit litfaß
async function closeDisplay() {
// if the browser is still connected, close it (as only one page was closed)
if (browser.isConnected()) {
await browser.close();
return; // we'll be called again by the disconnected event
// check if the display has a pages array
if (!Array.isArray(display.pages)) {
throw new Error(`Display ${index} needs a 'pages' array`);
} else if(!display.pages.length) {
throw new Error(`Display ${index} needs at least one page to display`);
}
// as soon as all displays are closed, stop any open schedules / sleep timers
// this causes the original start promise to resolve if we are not restarting
display.closed = true;
if(!displays.some(display => !display.closed)) {
scheduler.close();
// normalize the rotationSpeed and URL into an array of page objects
display.pages = display.pages.map((url, index) => ({
url, airTime: ((Array.isArray(display.rotationSpeed) ?
(display.rotationSpeed[index] | 0) : (display.rotationSpeed | 0))
|| DEFAULT_AIR_TIME) * 1e3 // normalize to milliseconds
}));
// normalize the transitionAnimation to an object
if (!display.transitionAnimation || typeof display.transitionAnimation !== 'object') {
display.transitionAnimation = { name: display.transitionAnimation };
}
} browser.on('disconnected', closeDisplay);
// the browser will open with one page pre-loaded
const firstTab = (await browser.pages())[0];
await firstTab.goto(app); // passed in by ./bin/www
const transitionAnimation = display.transitionAnimation = {
name: display.transitionAnimation.name || DEFAULT_TRANSITION_ANIMATION.name,
duration: (display.transitionAnimation.duration | 0) || DEFAULT_TRANSITION_ANIMATION.duration
};
// check if a valid transition was specified and replace the durations
if (!(transitionAnimation.name in TRANSITION_ANIMATIONS)) {
throw new Error(`Display ${index} has an unknown transition animation '${transitionAnimation.name}'`);
} else if(transitionAnimation.name !== 'none') {
transitionAnimation.halfDuration = (transitionAnimation.duration / 2) | 0;
for (const key of ['in', 'after', 'out']) {
transitionAnimation[key] = `html { ${ TRANSITION_ANIMATIONS[transitionAnimation.name][key]
.replace('@duration', transitionAnimation.halfDuration) } }`
}
} else {
// no animation takes no time to animate
transitionAnimation.halfDuration = 0;
}
// launch one browser per display
const browser = display.browser = await puppeteer.launch(merge.all([settings.browserOptions || {}, display.browserOptions || {}, {
headless: false,
defaultViewport: null, /* do not set any viewport => full screen */
ignoreDefaultArgs: ['--enable-automation'],
args: [
'--kiosk' /* launch in full-screen */,
`--window-position=${display.left},${display.top}`
]
}]));
// if all displays have been closed, exit litfaß
async function closeDisplay() {
// if the browser is still connected, close it (as only one page was closed)
if (browser.isConnected()) {
await browser.close();
return; // we'll be called again by the disconnected event
}
// if a browser was closed remove the display from the displays array
displays.splice(displays.indexOf(display), 1);
// open more pages (tabs) for preparing pages before rotation
const additionalTabs = await Promise.all(Array.from({
length: config.preparePages | 0 // will work with positive numbers and booleans where true maps to 1
}, async () => {
// open the page and jump back to the launch screen immediately
let page = await browser.newPage();
await firstTab.bringToFront();
return page;
// as soon as all displays have been closed, stop any open schedules / sleep timers
// this causes the original start promise to resolve if we are not restarting. in case
// of a restart, the restart scheduler is currently not in a "sleep", so the interrupt
// will not cause the restart loop to exit and the start function will be triggered
if(!displays.length) {
scheduler.close();
delete this.scheduler;
}
} browser.on('disconnected', closeDisplay);
// the browser will open with one page pre-loaded
const firstTab = (await browser.pages())[0];
await firstTab.goto(settings.launchUrl || 'about:blank');
// open more pages (tabs) for preparing pages before rotation
const additionalTabs = await Promise.all(Array.from({
length: settings.preparePages | 0 // will work with positive numbers and booleans where true maps to 1
}, async () => {
// open the page and jump back to the launch screen immediately
let page = await browser.newPage();
await firstTab.bringToFront();
return page;
}));
// if any page was closed, close the browser for this display
(display.tabs = [firstTab, ...additionalTabs])
.forEach(page => page.on('close', closeDisplay));
}));
// emit a start event, after all browsers have been launched
this.emit('browsersLaunch', displays);
// if any page was closed, close the browser for this display
(display.tabs = [firstTab, ...additionalTabs])
.forEach(page => page.on('close', closeDisplay));
}));
// start the rotation for each display
const rotations = displays.map(async (display, index) => {
for(const transitionAnimation = display.transitionAnimation;;) {
// check if the browser for this display is still connected
if (display.ignore || !display.browser.isConnected()) {
break; // if not exit the rotation for this display
// start the rotation for each display
const rotations = displays.map(async (display, index) => {
for(const transitionAnimation = display.transitionAnimation;;) {
// check if the browser for this display is still connected
if (display.ignore || !display.browser.isConnected()) {
break; // if not exit the rotation for this display
}
// increment the current page count and get the current / next page object to display
const page = display.launch ? { airTime: settings.launchTimeout } :
display.pages[display.currentPage = ++display.currentPage % display.pages.length],
nextPage = display.pages[(display.currentPage + 1) % display.pages.length];
delete display.launch; // on the first iteration displaying the launch page will NOT increment the page count
// also get the currently active and next in line browser tab to use
const tab = display.tabs[display.currentTab = ++display.currentTab % display.tabs.length],
nextTab = display.tabs[(display.currentTab + 1) % display.tabs.length];
const loadNextTab = wait => animate(nextTab, wait ? () => sleep(wait) : null, async () => {
await nextTab.goto(nextPage.url, { waitUntil: 'domcontentloaded' });
this.emit('pageLoad', nextTab, display);
}, transitionAnimation.after);
// if pages should be prepared, load the next tab already shortly before we switch to it
settings.preparePages && loadNextTab(page.airTime - settings.preparationTime);
// all displays that are scheduled to show a page at the same time, should be doing so as synchronized as possible, so use a flaky scheduler here
await scheduler.scheduleIn(page.airTime, async () => {
animate(tab, transitionAnimation.out);
await sleep(transitionAnimation.halfDuration); // wait here, as the offset was subtracted from the schedule time before
animate(nextTab, async () => {
await (settings.preparePages ? nextTab.bringToFront() : loadNextTab());
this.emit('pageShow', nextTab, display);
}, transitionAnimation.in, settings.preparePages ? () => tab.goto('about:blank') : null);
}, -transitionAnimation.halfDuration /* offset, so we stay as close to the air time as possible */);
}
}).map(promise => promise.catch(() => undefined)); // do not consider it a problem if any single display fails, just resolve the promise!
// regularly check if the number of displays has changed, this is especially helpful, e.g.
// in case some displays are turned off in the afternoon and turned on again in the morning
await Promise.all([...rotations, settings.watchDisplays ? (async () => {
for(;;) {
try {
await sleep(WATCH_DISPLAYS_TIMEOUT);
} catch(interrupt) {
return; // in case the sleep was interrupted, the scheduler was closed!
}
// in case displays have been attached / detached, exit the loop, we'll start a new one soon!
if (this.watchRestart || displays.length !== getDisplays().length) {
delete this.watchRestart;
break;
}
}
// in case a change in the number of displays was detected, restart litfaß either because
// a physical display was connected / disconnected, or a browser window was closed
await this.restart();
})() : null]);
}
// increment the current page count and get the current / next page object to display
const page = display.launch ? { airTime: config.launchTimeout } :
display.pages[display.currentPage = ++display.currentPage % display.pages.length],
nextPage = display.pages[(display.currentPage + 1) % display.pages.length];
delete display.launch; // on the first iteration displaying the launch page will NOT increment the page count
// also get the currently active and next in line browser tab to use
const tab = display.tabs[display.currentTab = ++display.currentTab % display.tabs.length],
nextTab = display.tabs[(display.currentTab + 1) % display.tabs.length];
const loadNextTab = wait => animate(nextTab, wait ? () => sleep(wait) : null, () =>
nextTab.goto(nextPage.url, { waitUntil: 'domcontentloaded' }), transitionAnimation.after);
// if pages should be prepared, load the next tab already shortly before we switch to it
config.preparePages && loadNextTab(page.airTime - config.preparationTime);
// all displays that are scheduled to show a page at the same time, should be doing so as synchronized as possible, so use a flaky scheduler here
await scheduler.scheduleIn(page.airTime, async () => {
animate(tab, transitionAnimation.out);
await sleep(transitionAnimation.halfDuration); // wait here, as the offset was subtracted from the schedule time before
animate(nextTab, () => config.preparePages ? nextTab.bringToFront() : loadNextTab(), transitionAnimation.in, config.preparePages ? () => tab.goto('about:blank') : null);
}, -transitionAnimation.halfDuration /* offset, so we stay as close to the air time as possible */);
async restart(inWatcher) {
if (inWatcher) {
// this will cause litfaß to restart the next time it'll check for display changes
// with the added benefit of keeping the original promise chain going
this.watchRestart = true;
} else {
// restart litfaß by exiting and adding another "start" promise to the chain
await this.exit(); await this.start(this.settings);
}
}).map(promise => promise.catch(() => undefined)); // do not consider it a problem if any single display fails, just resolve the promise!
}
// regularly check if the number of displays has changed, this is especially helpful, e.g.
// in case some displays are turned off in the afternoon and turned on again in the morning
await Promise.all([...rotations, config.watchNumberOfDisplays ? (async () => {
for(;;) {
try {
await sleep(WATCH_DISPLAYS_TIMEOUT);
} catch(interrupt) {
return; // in case the sleep was interrupted, the scheduler was closed!
}
// in case displays have been attached / detached, exit the loop, we'll start a new one soon!
if (displays.length !== getDisplays().length) {
break;
}
}
// in case a change in the number of displays was detected, close the current displays and restart litfaß
await Promise.all(displays.map(display => display.browser.close()));
await exports.start(app); // adds to the promise chain
})() : null]);
};
async exit() {
await Promise.all(this.displays.map(display => display.browser.close()));
}
})();
{
"name": "litfass",
"version": "1.4.2",
"version": "1.5.0",
"description": "A library for advertising displays",

@@ -42,3 +42,4 @@ "keywords": [

"dependencies": {
"config": "^3.3.0",
"config": "^3.3.1",
"config-uncached": "^1.0.2",
"deepmerge": "^4.2.2",

@@ -51,3 +52,6 @@ "displays": "^1.1.2",

"puppeteer": "^2.1.1"
},
"resolutions": {
"config": "3.3.1"
}
}

@@ -41,3 +41,3 @@ [<img width="300" alt="Litfaß Logo" src="https://raw.githubusercontent.com/kristian/litfass/master/public/img/litfass.svg?sanitize=true">](https://github.com/kristian/litfass#readme)

- **preparePages**: Litfaß will automatically attempt to prepare any page, before it is displayed. This setting can be used to specify if Litfaß should prepare the pages (`true` / `false`) and / or the number of pages to prepare in advance. The setting **preparationTime** will determine how much in advance Litfaß is going to attempt to load the next tab (default to 5 seconds).
- **watchNumberOfDisplays**: Litfaß will automatically watch for the number of displays connected. In case the number of display changes, Litfaß will restart and show browsers on the newly connected displays. Can be turned off by setting this property to `false`.
- **watchDisplays**: Litfaß will automatically watch for the number of displays connected and compare it to the number of still open Litfaß displays. In case either a physical change of displays connected / disconnected, or in case a Litfaß browser window was closed, Litfaß will automatically restart and show browsers on all connected displays again. Can be turned off by setting this property to `false`.

@@ -44,0 +44,0 @@ By default the `displays` configuration section contains only one element. This causes the same rotation of pages to be displayed on all connected displays. Define multiple entries in the `displays` section, where each entry corresponds to one display connected. To ignore a display use `ignore: true`.

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc