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.0.3 to 1.1.0

208

litfass.js

@@ -0,1 +1,3 @@

"use strict";
const puppeteer = require('puppeteer');

@@ -5,9 +7,24 @@

const Display = nbind.init(__dirname).lib.Display;
const sleep = time => new Promise(resolve => setTimeout(resolve, time));
process.env["NODE_CONFIG_DIR"] = __dirname + "/config/";
const config = require('config').get('litfass');
const { scheduleIn } = require('./schedule');
const { sleep, scheduleIn } = require('./schedule');
const LAUNCH_TIMEOUT = 10e3, DEFAULT_ROTATION_SPEED = 5e3, DEFAULT_TRANSITION_ANIMATION = {
const merge = require('deepmerge')
const silentImmediate = (asyncCallback, errorCallback) => {
setImmediate(async () => {
try { await asyncCallback(); }
catch(error) {
// catch any errors (silently)
if (errorCallback) {
errorCallback(error);
}
}
})
};
const DEFAULT_LAUNCH_TIME = 10, DEFAULT_PAGE_PRELOAD = {
numberOfPages: 2,
preloadTime: 5,
}, DEFAULT_AIR_TIME = 5, DEFAULT_TRANSITION_ANIMATION = {
name: 'fade',

@@ -34,44 +51,51 @@ duration: 400

const animate = (page, ...animations) => {
return new Promise((resolve, reject) => {
silentImmediate(async () => {
for(const animation of animations) {
if (typeof animation === "string") {
await page.addStyleTag({ content: animation });
} else if (animation) {
await animation(page);
}
} resolve();
}, reject);
}).catch(error => { /* do nothing */ });
};
exports.start = async (app) => {
const displays = Display.getDisplays(), pages = [];
// read the configuration and create a deep copy
const config = merge.all([require('config').get('litfass') || {}]);
// launch one browser per display
for (const display of displays) {
const browser = await puppeteer.launch({
headless: false,
defaultViewport: null, /* do not set any viewport => full screen */
args: [
'--kiosk' /* launch in full-screen */,
`--app=${app}`, /* needed to get rid of "automation" banner */
`--window-position=${display.left},${display.top}`
]
});
// remember the page objects, we'll need them later on
let page; pages.push(page = (await browser.pages())[0]);
// normalize the configuration
config.launch = config.launch || {};
config.launch.time = ((config.launch.time | 0) || DEFAULT_LAUNCH_TIME) * 1e3; // normalize to milliseconds
config.launch.page = { url: app, airTime: config.launch.time }; // build an artificial launch page configuration
config.pagePreload = config.pagePreload || {};
config.pagePreload.numberOfPages = Math.max(( config.pagePreload.numberOfPages | 0 ) || DEFAULT_PAGE_PRELOAD.numberOfPages, 1);
config.pagePreload.preloadTime = Math.max(( config.pagePreload.preloadTime | 0 ) || DEFAULT_PAGE_PRELOAD.preloadTime, 0) * 1e3; // normalize to milliseconds
// if all pages / browser have been closed, exit litfaß
function closeDisplay(display) {
display.closed = true;
if(!displays.some(display => !display.closed)) {
// all displays have been closed, exit the process
process.exit(0);
}
}
browser.on('disconnected', () => closeDisplay(display));
page.on('close', () => closeDisplay(display));
// page preloading can be turned off, by setting the number of preload pages to one. this is helpful for
// low-resource machines, but we'll need to treat the preloading logic differently for this corner case
const shouldPreload = config.pagePreload.numberOfPages > 1;
// check if there is at least one display defined
if (!Array.isArray(config.displays)) {
throw new Error(`Litfaß configuration needs a 'displays' array`);
} else if(!config.displays.length) {
throw new Error(`Litfaß configuration needs at least one display`);
}
// wait on the launch screen until the launch timeout has passed
await sleep(LAUNCH_TIMEOUT);
// for each display start the rotation
await Promise.all(displays.map(async (nothing, index) => {
const page = pages[index], display = Object.assign({}, config[index] || config[0], { currentPage: -1 });
// create browsers for each display
const displays = Display.getDisplays();
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 the display has a pages array
if (!Array.isArray(display.pages)) {
throw new Error(`Display ${index} needs a 'pages' array`);
throw new Error(`Display configuration ${index} needs a 'pages' array`);
} else if(!display.pages.length) {
throw new Error(`Display ${index} needs at least one page to display`);
throw new Error(`Display configuration ${index} needs at least one page to display`);
} else if(display.ignore) {

@@ -82,10 +106,8 @@ // ignore this display for the litfaß display

// normalize the rotationSpeed to an array of integer numbers
if (!display.rotationSpeed || !Array.isArray(display.rotationSpeed)) {
display.rotationSpeed = Array(display.pages.length).fill(
(display.rotationSpeed | 0) || DEFAULT_ROTATION_SPEED);
} else {
display.rotationSpeed = Array.from(Array(display.pages.length), (nothing, page) =>
(display.rotationSpeed[page] | 0) || DEFAULT_ROTATION_SPEED);
}
// 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
}));

@@ -110,37 +132,79 @@ // normalize the transitionAnimation to an object

}
} else {
// no animation takes no time to animate
transitionAnimation.halfDuration = 0;
}
// function to advance to the next page
async function advancePage() {
display.currentPage = ++display.currentPage % display.pages.length;
// launch one browser per display
const browser = display.browser = await puppeteer.launch(merge(config.launch.options, {
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 (!page.isClosed() && display.pages.length > 1) {
// we have a very flaky schedule in place, thus this call will always try to merge
// multiple events into one. this way all the displays will stay nicely in sync!
scheduleIn(display.rotationSpeed[display.currentPage], advancePage);
// 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();
}
try {
if (transitionAnimation.name !== 'none') {
// in case a transition should be made fade out first
await page.addStyleTag({ content: transitionAnimation.out });
await sleep(transitionAnimation.halfDuration);
display.closed = true;
if(!displays.some(display => !display.closed)) {
// all displays have been closed, exit the process
process.exit(0);
}
} browser.on('disconnected', closeDisplay);
// the browser will open with one page pre-loaded
const firstTab = (await browser.pages())[0];
await firstTab.goto(config.launch.page.url);
// do NOT await the page to be advanced (maybe it takes longer than the next timeout)
await page.goto(display.pages[display.currentPage], { waitUntil: 'domcontentloaded' });
await page.addStyleTag({ content: transitionAnimation.after });
await page.addStyleTag({ content: transitionAnimation.in });
} else {
// with no transition, navigate immediately
await page.goto(display.pages[display.currentPage]);
}
} catch(e) {
// nothing to do here, likely the page has been closed!
console.warn(`Display ${index} navigation failed`);
}
// open more pages (tabs) for preloading the page rotation
const additionalTabs = await Promise.all(Array.from({
length: config.pagePreload.numberOfPages - 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));
}));
// start the rotation for each display
await Promise.all(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
}
// advance to the first page and schedule the rotation
advancePage();
}));
// increment the current page count and get the current / next page object to display
const page = display.launch ? config.launch.page : 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 preloadNextTab = wait => animate(nextTab, wait ? () => sleep(wait) : null, () =>
nextTab.goto(nextPage.url, { waitUntil: 'domcontentloaded' }), transitionAnimation.after);
// if pages should be preloaded, prepare the next tab shortly before we switch to it
shouldPreload && preloadNextTab(page.airTime - config.pagePreload.preloadTime);
// 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 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, () => shouldPreload ? nextTab.bringToFront() : preloadNextTab(), transitionAnimation.in, shouldPreload ? () => tab.goto('about:blank') : null);
}, -transitionAnimation.halfDuration /* offset, so we stay as close to the air time as possible */);
}}));
};
{
"name": "litfass",
"version": "1.0.3",
"version": "1.1.0",
"description": "A library for advertising displays",

@@ -46,2 +46,3 @@ "keywords": [

"config": "^3.3.0",
"deepmerge": "^4.2.2",
"express": "~4.16.1",

@@ -48,0 +49,0 @@ "http-errors": "^1.7.3",

@@ -36,7 +36,13 @@ [<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)

- **pages**: An array of URLs to rotate the display between (e.g. https://example.com and https://kra.lc/ will rotate between those two pages in the set `rotationSpeed`)
- **rotationSpeed**: The number of seconds each page should be displayed. In case some pages should be displayed longer than others, you can also specify an array. By default a page is displayed for 10 seconds before being rotated.
- **transitionAnimation**: Litfaß supports transitioning between multiple pages when rotating. The following animations are supported `none`, `fade`, `slideUp`, `slideLeft`, defaults to `fade`. You can also use an object `{ name: ..., duration: ... }` to specify the speed of the transition in milliseconds.
- **launch**: An object to specify different launch options for Litfaß:
- *time*: The number of seconds Litfaß will display its slash / launch screen
- *options*: Options merged into the launch options for Puppeteer / Chrome, e.g. handy in case you would like to specify an own *executablePath* for Chrome.
- **displays**: An array of displays, to specify different rotations on different displays connected. In case more displays are connected than specified here, the first display configuration will be used.
- *pages*: An array of URLs to rotate the display between (e.g. https://example.com and https://kra.lc/ will rotate between those two pages in the set `rotationSpeed`)
- *rotationSpeed*: The number of seconds each page should be displayed. In case some pages should be displayed longer than others, you can also specify an array. By default a page is displayed for 10 seconds before being rotated.
- *transitionAnimation*: Litfaß supports transitioning between multiple pages when rotating. The following animations are supported `none`, `fade`, `slideUp`, `slideLeft`, defaults to `fade`. You can also use an object `{ name: ..., duration: ... }` to specify the speed of the transition in milliseconds.
- *ignore*: If set Litfaß will ignore this display and will not start a browser on the specified display.
- **pagePreload**: Litfaß will automatically attempt to preload any page, before it is displayed. This object can be used to specify the number of buffered pages to use (*numberOfPages*) and the time the page should be started loading in advance (*preloadTime*).
By default the `litfass` 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 `litfass` section, where each entry corresponds to one display connected. To ignore a display use `ignore: true`.
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`.

@@ -43,0 +49,0 @@ ## Author

// this is a VERY "flaky" schedule, by design! this means close events (by 2 seconds) will be merged
// together into one event, this way the litfaß will not "stutter" between multiple displays even
// if it runs for a very long time, due to un-synchronized setTimestamp calls!
const flakySchedule = new Map();
const queue = new Map();
exports.scheduleIn = (seconds, task) => {
const sleep = exports.sleep = (time, callback) => new Promise(resolve => time > 0 ? setTimeout(resolve, time) : setImmediate(resolve)).then(callback);
exports.scheduleIn = (ms, task, offset) => {
// calculate the future time slot, when the event should be triggered
const time = (Date.now() / 1000) + seconds, timeSlot = Math.round(time);
const time = Math.ceil((Date.now() + ms) / 1e3), timeSlot = time + ((offset | 0) / 1e3);
// check if there is a schedule for this / or the upcoming time slot already
let promise = flakySchedule.get(timeSlot) || flakySchedule.get(Math.ceil(time));
// check if there is a schedule for this time slot already
let schedule = queue.get(timeSlot);
// if there is no schedule yet, create one as close to the full second calculated as possible
if (!promise) {
flakySchedule.set(timeSlot, promise = new Promise((resolve) => {
setTimeout(resolve, (timeSlot * 1000) - Date.now());
}));
if (!schedule) {
const tasks = [];
queue.set(timeSlot, schedule = {
tasks, promise: sleep((timeSlot * 1e3) - Date.now()).then(async () => {
await Promise.all(tasks.map(task => task())); // execute all tasks in parallel
})
});
}
// in case a task function as specified, chain it to the promise
task && promise.then(task);
// append the task to the schedules tasks list
schedule.tasks.push(task);
// return the promise, which allows more chaining
return promise;
// return the promise this schedule is waiting for
return schedule.promise;
};

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