mirror of
https://github.com/cpvalente/ontime.git
synced 2026-09-04 22:09:10 +00:00
docs: demo project is runtime data documentation
This commit is contained in:
committed by
Carlos Valente
parent
54f07c4330
commit
87df656470
+402
@@ -1,3 +1,405 @@
|
|||||||
## Demo
|
## Demo
|
||||||
|
|
||||||
This is a demo application which demonstrates how to create a custom view leveraging a websocket client to get data from Ontime.
|
This is a demo application which demonstrates how to create a custom view leveraging a websocket client to get data from Ontime.
|
||||||
|
|
||||||
|
Here, we subscribe to the websocket and display all the data received in a grid.
|
||||||
|
|
||||||
|
Please note this demo tries to be simple and clear. You would likely want to implement a more robust solution in a production environment.
|
||||||
|
|
||||||
|
### Getting the data
|
||||||
|
|
||||||
|
To subscribe to the websocket you will need:
|
||||||
|
|
||||||
|
- The address of the Ontime server (including the IP): eg, `cloud.getontime.no/stage-hash` or `192.168.1.1:4001`
|
||||||
|
- If the stage is password protected, you will also need to provide a token to access the data. You can get this token by generating a share link for Companion (Editor > Settings > Share link) and ensuring the "Authenticate Link" option is on.
|
||||||
|
|
||||||
|
#### Example
|
||||||
|
|
||||||
|
- Ontime URL: `https://cloud.getontime.no/stage-123`
|
||||||
|
- Ontime token: `token-from-share`
|
||||||
|
|
||||||
|
```js
|
||||||
|
// use wss since we are connecting to an https address
|
||||||
|
const socketUrl = `wss://cloud.getontime.no/stage-123/ws?token=token-from-share`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Connects to the websocket server
|
||||||
|
* NOTE: this demo does not handle reconnections or errors
|
||||||
|
* @param {string} socketUrl
|
||||||
|
*/
|
||||||
|
const connectSocket = (socketUrl) => {
|
||||||
|
const websocket = new WebSocket(socketUrl);
|
||||||
|
|
||||||
|
websocket.onmessage = (event) => {
|
||||||
|
// all objects from ontime are structured with tag and payload
|
||||||
|
const { tag, payload } = JSON.parse(event.data);
|
||||||
|
|
||||||
|
// runtime-data is sent on connect, with the full state
|
||||||
|
// runtime-patch is sent on every change to the state
|
||||||
|
if (tag === 'runtime-data') {
|
||||||
|
handleOntimePayload(payload);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Runtime data
|
||||||
|
|
||||||
|
`runtime-data` contains a patch of all the data in the server
|
||||||
|
you would need to create a function that parses the patch and extract the data you need
|
||||||
|
|
||||||
|
In our case, we simply map the data to a DOM element with the same ID as the field name.
|
||||||
|
|
||||||
|
[See the docs](https://docs.getontime.no/api/data/runtime-data/).
|
||||||
|
|
||||||
|
#### Example of handling the payload
|
||||||
|
|
||||||
|
```js
|
||||||
|
const handleOntimePayload = (payload) => {
|
||||||
|
// 1. apply the patch into your local copy of the data
|
||||||
|
localData = { ...localData, ...payload };
|
||||||
|
|
||||||
|
// 2. update the UI with the new data
|
||||||
|
// ... timer data
|
||||||
|
if ('clock' in payload) updateDOM('clock', formatTimer(payload.clock));
|
||||||
|
if ('timer' in payload) updateDOM('timer', formatObject(payload.timer));
|
||||||
|
// ... rundown data
|
||||||
|
if ('rundown' in payload) updateDOM('rundown', formatObject(payload.rundown));
|
||||||
|
// ... runtime
|
||||||
|
if ('offset' in payload) updateDOM('offset', formatObject(payload.offset));
|
||||||
|
// ... relevant entries
|
||||||
|
if ('eventNow' in payload) updateDOM('eventNow', formatObject(payload.eventNow));
|
||||||
|
if ('eventNext' in payload) updateDOM('eventNext', formatObject(payload.eventNext));
|
||||||
|
if ('eventFlag' in payload) updateDOM('eventFlag', formatObject(payload.eventFlag));
|
||||||
|
if ('groupNow' in payload) updateDOM('groupNow', formatObject(payload.groupNow));
|
||||||
|
// ... messages service
|
||||||
|
if ('message' in payload) updateDOM('message', formatObject(payload.message));
|
||||||
|
// ... extra timers
|
||||||
|
if ('auxtimer1' in payload) updateDOM('auxtimer1', formatObject(payload.auxtimer1));
|
||||||
|
if ('auxtimer2' in payload) updateDOM('auxtimer2', formatObject(payload.auxtimer2));
|
||||||
|
if ('auxtimer3' in payload) updateDOM('auxtimer3', formatObject(payload.auxtimer3));
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Payload example
|
||||||
|
|
||||||
|
See below what the payload looks like.
|
||||||
|
Note: all timer values are in milliseconds.
|
||||||
|
|
||||||
|
```jsonc
|
||||||
|
{
|
||||||
|
/** Current server clock value */
|
||||||
|
"clock": 37816011,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gathers the current running timer state
|
||||||
|
*/
|
||||||
|
"timer": {
|
||||||
|
/** Additional time added to the running timer, can be negative */
|
||||||
|
"addedTime": 0,
|
||||||
|
/** Current running timer countdown */
|
||||||
|
"current": 3574976,
|
||||||
|
/** Total duration of the running event */
|
||||||
|
"duration": 3600000,
|
||||||
|
/** Time elapsed since the timer started */
|
||||||
|
"elapsed": 25024,
|
||||||
|
/** Timestamp of the expected finish time */
|
||||||
|
"expectedFinish": 41391285,
|
||||||
|
/** Current phase of the running event */
|
||||||
|
"phase": "default",
|
||||||
|
/** Timer's playback state */
|
||||||
|
"playback": "play",
|
||||||
|
/** Secondary timer, used to count to an event start in roll mode */
|
||||||
|
"secondaryTimer": null,
|
||||||
|
/** Timestamp when the timer started */
|
||||||
|
"startedAt": 37791285,
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Offset represents our current position in relation to the planned time
|
||||||
|
* a positive value means that we have added extra time to the expected end
|
||||||
|
* aka behind schedule
|
||||||
|
*/
|
||||||
|
"offset": {
|
||||||
|
/** Current absolute offset: accounts for planned times */
|
||||||
|
"absolute": 40394840,
|
||||||
|
/** Current relative offset: only counts for generated offset since start */
|
||||||
|
"relative": -35997119,
|
||||||
|
/** Currently selected offset mode */
|
||||||
|
"mode": "absolute",
|
||||||
|
/** Timestamp of the expected start of the next flag */
|
||||||
|
"expectedFlagStart": 80594840,
|
||||||
|
/** Timestamp of the expected end of the current group */
|
||||||
|
"expectedGroupEnd": 83594840,
|
||||||
|
/** Timestamp of the expected end of the loaded rundown */
|
||||||
|
"expectedRundownEnd": 90794840,
|
||||||
|
},
|
||||||
|
|
||||||
|
/** Data object describes rundown schedule and the current progress */
|
||||||
|
"rundown": {
|
||||||
|
/** Index of the currently selected event */
|
||||||
|
"selectedEventIndex": 1,
|
||||||
|
/** Total number of events */
|
||||||
|
"numEvents": 7,
|
||||||
|
/** Timestamp of the rundown's planned start time */
|
||||||
|
"plannedStart": 0,
|
||||||
|
/** Timestamp of the rundown's planned end time */
|
||||||
|
"plannedEnd": 50400000,
|
||||||
|
/** Timestamp of when the rundown was actually started */
|
||||||
|
"actualStart": 76391959,
|
||||||
|
},
|
||||||
|
|
||||||
|
/** Data of currently loaded event */
|
||||||
|
"eventNow": {
|
||||||
|
/** Unique identifier for the event */
|
||||||
|
"id": "9bf60f",
|
||||||
|
/** Entry type */
|
||||||
|
"type": "event",
|
||||||
|
/** Whether the event is flagged */
|
||||||
|
"flag": false,
|
||||||
|
/** Title of the event */
|
||||||
|
"title": "Pre-show Countdown",
|
||||||
|
/** Timestamp of the planned start time */
|
||||||
|
"timeStart": 36000000,
|
||||||
|
/** Timestamp of the planned end time */
|
||||||
|
"timeEnd": 39600000,
|
||||||
|
/** Planned event duration */
|
||||||
|
"duration": 3600000,
|
||||||
|
/** Strategy for time management */
|
||||||
|
"timeStrategy": "lock-end",
|
||||||
|
/** Whether the event is linked to the start of the previous */
|
||||||
|
"linkStart": false,
|
||||||
|
/** Action to take at the end of the event */
|
||||||
|
"endAction": "none",
|
||||||
|
/** Type of timer used for the event */
|
||||||
|
"timerType": "count-down",
|
||||||
|
/** Whether the timer counts to the end */
|
||||||
|
"countToEnd": false,
|
||||||
|
/** Whether the event is skipped */
|
||||||
|
"skip": false,
|
||||||
|
/** Note associated with the event */
|
||||||
|
"note": "Music plays, holding slide on screens",
|
||||||
|
/** Colour code for the event */
|
||||||
|
"colour": "#77C785",
|
||||||
|
/** Current delay inherited from the rundown schedule */
|
||||||
|
"delay": 0,
|
||||||
|
/** Day offset for the event */
|
||||||
|
"dayOffset": 0,
|
||||||
|
/** Time gap between events */
|
||||||
|
"gap": 0,
|
||||||
|
/** Cue number for the event */
|
||||||
|
"cue": "1",
|
||||||
|
/** Parent group ID */
|
||||||
|
"parent": "7eaf99",
|
||||||
|
/** Revision number for the entry */
|
||||||
|
"revision": 0,
|
||||||
|
/** Warning time */
|
||||||
|
"timeWarning": 600000,
|
||||||
|
/** Danger time */
|
||||||
|
"timeDanger": 300000,
|
||||||
|
/** Custom fields for the event */
|
||||||
|
"custom": { "Custom_Field": "Put additional info here" },
|
||||||
|
/** Triggers associated with the event */
|
||||||
|
"triggers": [],
|
||||||
|
},
|
||||||
|
|
||||||
|
/** Upcoming event data */
|
||||||
|
"eventNext": {
|
||||||
|
/** Unique identifier for the event */
|
||||||
|
"id": "c2697f",
|
||||||
|
/** Entry type */
|
||||||
|
"type": "event",
|
||||||
|
/** Whether the event is flagged */
|
||||||
|
"flag": false,
|
||||||
|
/** Title of the event */
|
||||||
|
"title": "Welcome",
|
||||||
|
/** Timestamp of the planned start time */
|
||||||
|
"timeStart": 39600000,
|
||||||
|
/** Timestamp of the planned end time */
|
||||||
|
"timeEnd": 40200000,
|
||||||
|
/** Planned event duration */
|
||||||
|
"duration": 600000,
|
||||||
|
/** Strategy for time management */
|
||||||
|
"timeStrategy": "lock-duration",
|
||||||
|
/** Whether the event is linked to the start of the previous */
|
||||||
|
"linkStart": true,
|
||||||
|
/** Action to take at the end of the event */
|
||||||
|
"endAction": "none",
|
||||||
|
/** Type of timer used for the event */
|
||||||
|
"timerType": "count-down",
|
||||||
|
/** Whether the timer counts to the end */
|
||||||
|
"countToEnd": false,
|
||||||
|
/** Whether the event is skipped */
|
||||||
|
"skip": false,
|
||||||
|
/** Note associated with the event */
|
||||||
|
"note": "Emma Thompson",
|
||||||
|
/** Colour code for the event */
|
||||||
|
"colour": "#FFCC78",
|
||||||
|
/** Current delay inherited from the rundown schedule */
|
||||||
|
"delay": 0,
|
||||||
|
/** Day offset for the event */
|
||||||
|
"dayOffset": 0,
|
||||||
|
/** Time gap between events */
|
||||||
|
"gap": 0,
|
||||||
|
/** Cue number for the event */
|
||||||
|
"cue": "1.1",
|
||||||
|
/** Parent group ID */
|
||||||
|
"parent": "7eaf99",
|
||||||
|
/** Revision number for the entry */
|
||||||
|
"revision": 0,
|
||||||
|
/** Warning time */
|
||||||
|
"timeWarning": 120000,
|
||||||
|
/** Danger time */
|
||||||
|
"timeDanger": 60000,
|
||||||
|
/** Custom fields for the event */
|
||||||
|
"custom": {},
|
||||||
|
/** Triggers associated with the event */
|
||||||
|
"triggers": [],
|
||||||
|
},
|
||||||
|
|
||||||
|
/** Data of currently targetted flag event */
|
||||||
|
"eventFlag": {
|
||||||
|
/** Unique identifier for the event */
|
||||||
|
"id": "fa593e",
|
||||||
|
/** Entry type */
|
||||||
|
"type": "event",
|
||||||
|
/** Whether the event is flagged */
|
||||||
|
"flag": true,
|
||||||
|
/** Title of the event */
|
||||||
|
"title": "Session 1",
|
||||||
|
/** Timestamp of the planned start time */
|
||||||
|
"timeStart": 40200000,
|
||||||
|
/** Timestamp of the planned end time */
|
||||||
|
"timeEnd": 43200000,
|
||||||
|
/** Planned event duration */
|
||||||
|
"duration": 3000000,
|
||||||
|
/** Strategy for time management */
|
||||||
|
"timeStrategy": "lock-duration",
|
||||||
|
/** Whether the event is linked to the start of the previous */
|
||||||
|
"linkStart": true,
|
||||||
|
/** Action to take at the end of the event */
|
||||||
|
"endAction": "none",
|
||||||
|
/** Type of timer used for the event */
|
||||||
|
"timerType": "count-down",
|
||||||
|
/** Whether the timer counts to the end */
|
||||||
|
"countToEnd": false,
|
||||||
|
/** Whether the event is skipped */
|
||||||
|
"skip": false,
|
||||||
|
/** Note associated with the event */
|
||||||
|
"note": "Liam Carter, Sophia Patel + PowerPoint",
|
||||||
|
/** Colour code for the event */
|
||||||
|
"colour": "#77C785",
|
||||||
|
/** Current delay inherited from the rundown schedule */
|
||||||
|
"delay": 0,
|
||||||
|
/** Day offset for the event */
|
||||||
|
"dayOffset": 0,
|
||||||
|
/** Time gap between events */
|
||||||
|
"gap": 0,
|
||||||
|
/** Cue number for the event */
|
||||||
|
"cue": "1.2",
|
||||||
|
/** Parent group ID */
|
||||||
|
"parent": "7eaf99",
|
||||||
|
/** Revision number for the entry */
|
||||||
|
"revision": 0,
|
||||||
|
/** Warning time */
|
||||||
|
"timeWarning": 120000,
|
||||||
|
/** Danger time */
|
||||||
|
"timeDanger": 60000,
|
||||||
|
/** Custom fields for the event */
|
||||||
|
"custom": {},
|
||||||
|
/** Triggers associated with the event */
|
||||||
|
"triggers": [],
|
||||||
|
},
|
||||||
|
|
||||||
|
/** Current group data */
|
||||||
|
"groupNow": {
|
||||||
|
/** Unique identifier for the group */
|
||||||
|
"id": "7eaf99",
|
||||||
|
/** Entry type */
|
||||||
|
"type": "group",
|
||||||
|
/** Title of the group */
|
||||||
|
"title": "Morning Sessions",
|
||||||
|
/** Note associated with the group */
|
||||||
|
"note": "",
|
||||||
|
/** ID of entries nested in the group */
|
||||||
|
"entries": ["9bf60f", "bf71a2", "c2697f", "fa593e", "a8b0b3"],
|
||||||
|
/** Optional, user defined target duration */
|
||||||
|
"targetDuration": null,
|
||||||
|
/** Colour code for the group */
|
||||||
|
"colour": "#339E4E",
|
||||||
|
/** Custom fields for the group */
|
||||||
|
"custom": {},
|
||||||
|
/** Revision number for the entry */
|
||||||
|
"revision": 0,
|
||||||
|
/** Timestamp of the first event's planned start time */
|
||||||
|
"timeStart": 36000000,
|
||||||
|
/** Timestamp of the last event's planned end time */
|
||||||
|
"timeEnd": 43200000,
|
||||||
|
/** Accumulated events duration */
|
||||||
|
"duration": 7200000,
|
||||||
|
/** Whether the first event has its start time linked */
|
||||||
|
"isFirstLinked": false,
|
||||||
|
},
|
||||||
|
|
||||||
|
/** Message object with data */
|
||||||
|
"message": {
|
||||||
|
/** Timer view message data */
|
||||||
|
"timer": {
|
||||||
|
/** Text associated with the timer view */
|
||||||
|
"text": "",
|
||||||
|
/** Whether the message is visible */
|
||||||
|
"visible": false,
|
||||||
|
/** Whether the timer view is blinking */
|
||||||
|
"blink": false,
|
||||||
|
/** Whether the timer view is blacked out */
|
||||||
|
"blackout": false,
|
||||||
|
/** Secondary source for the view */
|
||||||
|
"secondarySource": null,
|
||||||
|
},
|
||||||
|
/** Secondary message text */
|
||||||
|
"secondary": "",
|
||||||
|
},
|
||||||
|
|
||||||
|
/** Auxiliary timer 1 */
|
||||||
|
"auxtimer1": {
|
||||||
|
/** Duration of the timer */
|
||||||
|
"duration": 300000,
|
||||||
|
/** Current timer value */
|
||||||
|
"current": 300000,
|
||||||
|
/** Playback state (e.g., play, pause, stop) */
|
||||||
|
"playback": "stop",
|
||||||
|
/** Direction of the timer */
|
||||||
|
"direction": "count-down",
|
||||||
|
},
|
||||||
|
|
||||||
|
/** Auxiliary timer 2 */
|
||||||
|
"auxtimer2": {
|
||||||
|
/** Duration of the timer */
|
||||||
|
"duration": 300000,
|
||||||
|
/** Current timer value */
|
||||||
|
"current": 300000,
|
||||||
|
/** Playback state (e.g., play, pause, stop) */
|
||||||
|
"playback": "stop",
|
||||||
|
/** Direction of the timer */
|
||||||
|
"direction": "count-down",
|
||||||
|
},
|
||||||
|
|
||||||
|
/** Auxiliary timer 3 */
|
||||||
|
"auxtimer3": {
|
||||||
|
/** Duration of the timer */
|
||||||
|
"duration": 300000,
|
||||||
|
/** Current timer value */
|
||||||
|
"current": 300000,
|
||||||
|
/** Playback state (e.g., play, pause, stop) */
|
||||||
|
"playback": "stop",
|
||||||
|
/** Direction of the timer */
|
||||||
|
"direction": "count-down",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Links
|
||||||
|
|
||||||
|
- [Ontime Documentation](https://docs.getontime.no)
|
||||||
|
- [GitHub Repository](https://github.com/getontime/ontime)
|
||||||
|
- [Runtime data reference](https://docs.getontime.no/api/data/runtime-data/)
|
||||||
|
|||||||
Vendored
+100
-45
@@ -4,37 +4,22 @@
|
|||||||
* You could use this as a starting point to creating your own interfaces
|
* You could use this as a starting point to creating your own interfaces
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const mts = 1000; // millis to seconds
|
// Data that the user needs to provide depending on the Ontime URL
|
||||||
const mtm = 1000 * 60; // millis to minutes
|
const isSecure = window.location.protocol === 'https:';
|
||||||
const mth = 1000 * 60 * 60; // millis to hours
|
const userProvidedSocketUrl = `${isSecure ? 'wss' : 'ws'}://${window.location.hostname}:${window.location.port}/ws`;
|
||||||
|
|
||||||
const leftPad = (number) => {
|
connectSocket();
|
||||||
return Math.floor(number).toString().padStart(2, '0');
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatTimer = (number) => {
|
|
||||||
const millis = Math.abs(number);
|
|
||||||
const isNegative = number < 0;
|
|
||||||
return `${isNegative ? '-' : ''}${leftPad(millis / mth)}:${leftPad((millis % mth) / mtm)}:${leftPad(
|
|
||||||
(millis % mtm) / mts,
|
|
||||||
)}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
function updateTimerElement(playback, timerValue) {
|
|
||||||
const timerElement = document.getElementById('timer');
|
|
||||||
if (playback === 'stop') {
|
|
||||||
timerElement.innerText = '--:--:--';
|
|
||||||
} else {
|
|
||||||
timerElement.innerText = formatTimer(timerValue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let reconnectTimeout;
|
let reconnectTimeout;
|
||||||
const reconnectInterval = 1000;
|
const reconnectInterval = 1000;
|
||||||
let reconnectAttempts = 0;
|
let reconnectAttempts = 0;
|
||||||
|
|
||||||
const connectSocket = () => {
|
/**
|
||||||
const websocket = new WebSocket(`ws://${window.location.hostname}:${window.location.port}/ws`);
|
* Connects to the websocket server
|
||||||
|
* @param {string} socketUrl
|
||||||
|
*/
|
||||||
|
function connectSocket(socketUrl = userProvidedSocketUrl) {
|
||||||
|
const websocket = new WebSocket(socketUrl);
|
||||||
|
|
||||||
websocket.onopen = () => {
|
websocket.onopen = () => {
|
||||||
clearTimeout(reconnectTimeout);
|
clearTimeout(reconnectTimeout);
|
||||||
@@ -57,27 +42,97 @@ const connectSocket = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
websocket.onmessage = (event) => {
|
websocket.onmessage = (event) => {
|
||||||
const data = JSON.parse(event.data);
|
// all objects from ontime are structured with tag and payload
|
||||||
|
const { tag, payload } = JSON.parse(event.data);
|
||||||
|
|
||||||
// all objects from ontime are structured with type and payload
|
/**
|
||||||
const { type, payload } = data;
|
* runtime-data is sent
|
||||||
|
* - on connect with the full state
|
||||||
// we only need to read message type of ontime
|
* - and then on every update with a patch
|
||||||
switch (type) {
|
*/
|
||||||
case 'ontime': {
|
if (tag === 'runtime-data') {
|
||||||
// destructure known data from ontime
|
handleOntimePayload(payload);
|
||||||
// see https://docs.getontime.no/api/data/runtime-data/
|
|
||||||
const { current, playback } = payload.timer;
|
|
||||||
updateTimerElement(playback, current);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'ontime-timer': {
|
|
||||||
const { current, playback } = payload;
|
|
||||||
updateTimerElement(playback, current);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
};
|
}
|
||||||
|
|
||||||
connectSocket();
|
let localData = {};
|
||||||
|
/**
|
||||||
|
* Handles the ontime payload updates
|
||||||
|
* @param {object} payload - The payload object containing the updates
|
||||||
|
*/
|
||||||
|
function handleOntimePayload(payload) {
|
||||||
|
// 1. apply the patch into your local copy of the data
|
||||||
|
localData = { ...localData, ...payload };
|
||||||
|
|
||||||
|
// 2. update the UI with the new data
|
||||||
|
// ... timer data
|
||||||
|
if ('clock' in payload) updateDOM('clock', formatTimer(payload.clock));
|
||||||
|
if ('timer' in payload) updateDOM('timer', formatObject(payload.timer));
|
||||||
|
// ... rundown data
|
||||||
|
if ('rundown' in payload) updateDOM('rundown', formatObject(payload.rundown));
|
||||||
|
// ... runtime
|
||||||
|
if ('offset' in payload) updateDOM('offset', formatObject(payload.offset));
|
||||||
|
// ... relevant entries
|
||||||
|
if ('eventNow' in payload) updateDOM('eventNow', formatObject(payload.eventNow));
|
||||||
|
if ('eventNext' in payload) updateDOM('eventNext', formatObject(payload.eventNext));
|
||||||
|
if ('eventFlag' in payload) updateDOM('eventFlag', formatObject(payload.eventFlag));
|
||||||
|
if ('groupNow' in payload) updateDOM('groupNow', formatObject(payload.groupNow));
|
||||||
|
// ... messages service
|
||||||
|
if ('message' in payload) updateDOM('message', formatObject(payload.message));
|
||||||
|
// ... extra timers
|
||||||
|
if ('auxtimer1' in payload) updateDOM('auxtimer1', formatObject(payload.auxtimer1));
|
||||||
|
if ('auxtimer2' in payload) updateDOM('auxtimer2', formatObject(payload.auxtimer2));
|
||||||
|
if ('auxtimer3' in payload) updateDOM('auxtimer3', formatObject(payload.auxtimer3));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the DOM with a given payload
|
||||||
|
* @param {string} field - The runtime data field
|
||||||
|
* @param {object} payload - The patch object for the field
|
||||||
|
*/
|
||||||
|
function updateDOM(field, payload) {
|
||||||
|
const domElement = document.getElementById(field);
|
||||||
|
if (domElement) {
|
||||||
|
domElement.innerText = payload;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Time constants used for calculating times
|
||||||
|
const millisToSeconds = 1000;
|
||||||
|
const millisToMinutes = 1000 * 60;
|
||||||
|
const millisToHours = 1000 * 60 * 60;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats a timer value into a human-readable string
|
||||||
|
* @param {number} number - The timer value in milliseconds
|
||||||
|
* @returns {string} The formatted timer string
|
||||||
|
*/
|
||||||
|
function formatTimer(number) {
|
||||||
|
if (number == null) {
|
||||||
|
return '--:--:--';
|
||||||
|
}
|
||||||
|
const millis = Math.abs(number);
|
||||||
|
const isNegative = number < 0;
|
||||||
|
return `${isNegative ? '-' : ''}${leftPad(millis / millisToHours)}:${leftPad(
|
||||||
|
(millis % millisToHours) / millisToMinutes,
|
||||||
|
)}:${leftPad((millis % millisToMinutes) / millisToSeconds)}`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pads a number with leading zeros
|
||||||
|
* @param {number} number - The number to pad
|
||||||
|
* @returns {string} The padded number string
|
||||||
|
*/
|
||||||
|
function leftPad(number) {
|
||||||
|
return Math.floor(number).toString().padStart(2, '0');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stringifies an object into a pretty string
|
||||||
|
* @param {object} data - The data object to format
|
||||||
|
* @returns {string} The formatted data string
|
||||||
|
*/
|
||||||
|
function formatObject(data) {
|
||||||
|
return JSON.stringify(data, null, 2);
|
||||||
|
}
|
||||||
|
|||||||
+108
-8
@@ -1,14 +1,114 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<!-- For detailed explanations and examples, refer to the README.md file in this directory -->
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta charset="UTF-8" />
|
||||||
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>ontime demo</title>
|
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
|
||||||
<link href="./styles.css" rel="stylesheet" />
|
<title>ontime demo</title>
|
||||||
|
<link href="./styles.css" rel="stylesheet" />
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div id="timer"></div>
|
<header class="title-card">
|
||||||
<script src="./app.js" type="text/javascript"></script>
|
<div class="logo-title">
|
||||||
</html>
|
<img src="https://www.getontime.no/images/icons/ontime-logo.png" alt="Ontime logo"
|
||||||
|
onerror="this.style.display='none'" />
|
||||||
|
<h1 class="title">Ontime demo</h1>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>Last message received at</span>
|
||||||
|
<span id="clock">-</span>
|
||||||
|
</div>
|
||||||
|
<nav>
|
||||||
|
<a href="https://docs.getontime.no/api/data/runtime-data" target="_blank">Help? See docs</a>
|
||||||
|
<div>See <a href="README.md">README.md</a> details.</div>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="container">
|
||||||
|
<section class="column">
|
||||||
|
<details class="card" open>
|
||||||
|
<summary class="title">Timer</summary>
|
||||||
|
<figure>
|
||||||
|
<figcaption class="description">Current timer values</figcaption>
|
||||||
|
<code id="timer">-</code>
|
||||||
|
</figure>
|
||||||
|
</details>
|
||||||
|
<details class="card" open>
|
||||||
|
<summary class="title">Rundown</summary>
|
||||||
|
<figure>
|
||||||
|
<figcaption class="description">Progress of the current rundown</figcaption>
|
||||||
|
<code id="rundown">-</code>
|
||||||
|
</figure>
|
||||||
|
</details>
|
||||||
|
<details class="card" open>
|
||||||
|
<summary class="title">Offset</summary>
|
||||||
|
<figure>
|
||||||
|
<figcaption class="description">Runtime offset and timings for upcoming targets</figcaption>
|
||||||
|
<code id="offset">-</code>
|
||||||
|
</figure>
|
||||||
|
</details>
|
||||||
|
</section>
|
||||||
|
<section class="column">
|
||||||
|
<details class="card" open>
|
||||||
|
<summary class="title">Event now</summary>
|
||||||
|
<figure>
|
||||||
|
<figcaption class="description">Currently loaded event</figcaption>
|
||||||
|
<code id="eventNow">-</code>
|
||||||
|
</figure>
|
||||||
|
</details>
|
||||||
|
<details class="card" open>
|
||||||
|
<summary class="title">Event next</summary>
|
||||||
|
<figure>
|
||||||
|
<figcaption class="description">Next scheduled event</figcaption>
|
||||||
|
<code id="eventNext">-</code>
|
||||||
|
</figure>
|
||||||
|
</details>
|
||||||
|
</section>
|
||||||
|
<section class="column">
|
||||||
|
<details class="card" open>
|
||||||
|
<summary class="title">Group now</summary>
|
||||||
|
<figure>
|
||||||
|
<figcaption class="description">Currently active group</figcaption>
|
||||||
|
<code id="groupNow">-</code>
|
||||||
|
</figure>
|
||||||
|
</details>
|
||||||
|
<details class="card" open>
|
||||||
|
<summary class="title">Event flag</summary>
|
||||||
|
<figure>
|
||||||
|
<figcaption class="description">Currently targeted flag</figcaption>
|
||||||
|
<code id="eventFlag">-</code>
|
||||||
|
</figure>
|
||||||
|
</details>
|
||||||
|
</section>
|
||||||
|
<section class="column">
|
||||||
|
<details class="card" open>
|
||||||
|
<summary class="title">Message</summary>
|
||||||
|
<figure>
|
||||||
|
<figcaption class="description">Messaging feature</figcaption>
|
||||||
|
<code id="message">-</code>
|
||||||
|
</figure>
|
||||||
|
</details>
|
||||||
|
<details class="card" open>
|
||||||
|
<summary class="title">Aux timers</summary>
|
||||||
|
<figure>
|
||||||
|
<figcaption class="description">Auxiliary Timer 1</figcaption>
|
||||||
|
<code id="auxtimer1">-</code>
|
||||||
|
</figure>
|
||||||
|
<figure>
|
||||||
|
<figcaption class="description">Auxiliary Timer 2</figcaption>
|
||||||
|
<code id="auxtimer2">-</code>
|
||||||
|
</figure>
|
||||||
|
<figure>
|
||||||
|
<figcaption class="description">Auxiliary Timer 3</figcaption>
|
||||||
|
<code id="auxtimer3">-</code>
|
||||||
|
</figure>
|
||||||
|
</details>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<script src="./app.js" type="text/javascript"></script>
|
||||||
|
|
||||||
|
</html>
|
||||||
+86
-11
@@ -1,16 +1,91 @@
|
|||||||
body {
|
body {
|
||||||
overflow: hidden;
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
max-width: 100vw;
|
||||||
|
overflow-x: hidden;
|
||||||
|
font-family: 'Inter', 'Segoe UI', 'Helvetica Neue', Arial, sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.4;
|
||||||
|
background: #f6f6f6;
|
||||||
|
color: #222;
|
||||||
}
|
}
|
||||||
div {
|
|
||||||
height: 100vh;
|
|
||||||
width: 100vw;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
color: azure;
|
.container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container .column {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-card {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 10px 12px;
|
||||||
|
background: #eaeaea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-title img {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
padding: 10px 12px;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.03);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card summary.title {
|
||||||
|
border-bottom: 1px solid #ccc;
|
||||||
|
padding-bottom: 2px;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1.title,
|
||||||
|
summary.title {
|
||||||
|
font-size: 0.95em;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 0;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
summary.title {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
font-size: 0.75em;
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
font-size: 20vw;
|
background: #f4f4f4;
|
||||||
background-color: black;
|
border-radius: 4px;
|
||||||
}
|
padding: 1.5px 3px;
|
||||||
|
display: inline-block;
|
||||||
|
white-space: pre;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
figure {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
figcaption.description {
|
||||||
|
color: #555;
|
||||||
|
font-size: 0.75em;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,11 +5,22 @@ export enum OffsetMode {
|
|||||||
Relative = 'relative',
|
Relative = 'relative',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Offset represents our current position in relation to the planned time
|
||||||
|
* a positive value means that we have added extra time to the expected end
|
||||||
|
* aka behind schedule
|
||||||
|
*/
|
||||||
export type Offset = {
|
export type Offset = {
|
||||||
absolute: number; // a positive value means that we are in over time aka behind schedule
|
/** Current absolute offset: accounts for planned times */
|
||||||
|
absolute: number;
|
||||||
|
/** Current relative offset: only counts for generated offset since start */
|
||||||
relative: number;
|
relative: number;
|
||||||
|
/** Currently selected offset mode */
|
||||||
mode: OffsetMode;
|
mode: OffsetMode;
|
||||||
|
/** Timestamp of the expected start of the next flag */
|
||||||
expectedGroupEnd: MaybeNumber;
|
expectedGroupEnd: MaybeNumber;
|
||||||
|
/** Timestamp of the expected end of the current group */
|
||||||
expectedRundownEnd: MaybeNumber;
|
expectedRundownEnd: MaybeNumber;
|
||||||
|
/** Timestamp of the expected end of the loaded rundown */
|
||||||
expectedFlagStart: MaybeNumber;
|
expectedFlagStart: MaybeNumber;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -11,23 +11,26 @@ export enum TimerPhase {
|
|||||||
Pending = 'pending',
|
Pending = 'pending',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gathers the current running timer state
|
||||||
|
*/
|
||||||
export type TimerState = {
|
export type TimerState = {
|
||||||
/** time added by user, can be negative */
|
/** Additional time added to the running timer, can be negative */
|
||||||
addedTime: number;
|
addedTime: number;
|
||||||
/** running countdown */
|
/** Current running timer countdown */
|
||||||
current: MaybeNumber;
|
current: MaybeNumber;
|
||||||
/** normalised duration of current event */
|
/** Total duration of the running event */
|
||||||
duration: MaybeNumber;
|
duration: MaybeNumber;
|
||||||
/** elapsed time in current timer */
|
/** Time elapsed since the timer started */
|
||||||
elapsed: MaybeNumber;
|
elapsed: MaybeNumber;
|
||||||
/** time we expect timer to finish */
|
/** Timestamp of the expected finish time */
|
||||||
expectedFinish: MaybeNumber;
|
expectedFinish: MaybeNumber;
|
||||||
/** phase of of the running event */
|
/** Current phase of the running event */
|
||||||
phase: TimerPhase;
|
phase: TimerPhase;
|
||||||
/** playback state of the event */
|
/** Timer's playback state */
|
||||||
playback: Playback;
|
playback: Playback;
|
||||||
/** used for roll mode */
|
/** Secondary timer, used to count to an event start in roll mode */
|
||||||
secondaryTimer: MaybeNumber;
|
secondaryTimer: MaybeNumber;
|
||||||
/** only if timer has already started */
|
/** Timestamp when the timer started */
|
||||||
startedAt: MaybeNumber;
|
startedAt: MaybeNumber;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user