Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .dockerignore
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
.git
.git
assets.json
node_modules
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
# Changelog

## 1.0.0-beta.11 [UNRELEASED]
### Added
- An AI agent
- A `bridge.time.now()` api for getting the current server time
### Fixed
- An issue where context menues wouldn't be properly placed when running in a web browser
- An issue where time displays in the rundown didn't sync with the server time
- The clipboard isn't available in insecure contexts
- Updated dependencies

## 1.0.0-beta.10
### Added
- Human/AI readable descriptors for types
Expand Down
10 changes: 8 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
# The first stage will
# build the app into /app
FROM node:14.16.0-alpine3.10
FROM node:24-trixie

# RUN apk add --update --no-cache python3 make g++

WORKDIR /app

COPY package*.json ./
COPY plugins/* ./plugins/
COPY scripts/* ./scripts/

RUN npm ci

COPY . ./

RUN npm run build

CMD ["npm", "start"]
Expand All @@ -16,7 +22,7 @@ CMD ["npm", "start"]
# to force-squash the history
# and prevent any tokens
# from leaking out
FROM node:14.16.0-alpine3.10
FROM node:24-trixie
WORKDIR /app

COPY --from=0 /app /app
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
[![JavaScript Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://standardjs.com)
[![REUSE status](https://api.reuse.software/badge/github.com/svt/bridge)](https://api.reuse.software/info/github.com/svt/bridge)

Playout control software that can be customized to fit your needs.
AI-powered playout control software that can be customized to fit your needs.
Developed for CasparCG but can control anything that supports OSC.

![Screenshot](/media/screenshot.png)
Expand Down Expand Up @@ -46,6 +46,7 @@ The roadmap is available on Notion
- CasparCG library, playout and templates
- LTC timecode triggers
- Keyboard triggers
- Built in AI-agent

## Community plugins
- [CRON - triggers based on the time of day](https://github.com/axelboberg/bridge-plugin-cron)
Expand Down
19 changes: 17 additions & 2 deletions api/browser/clipboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ const DIController = require('../../shared/DIController')
const InvalidArgumentError = require('../error/InvalidArgumentError')

class Clipboard {
#copiedContent

/**
* Write a string into the clipboard
* @param { String } str A string to write
Expand All @@ -17,7 +19,17 @@ class Clipboard {
throw new InvalidArgumentError('Provided text is not a string and cannot be written to the clipboard')
}

return navigator.clipboard.writeText(str)
if (navigator.clipboard) {
return navigator.clipboard.writeText(str)
}

/*
Fall back to using an internal
property as store if navigator.clipboard
is not available (it isn't in insecure contexts)
*/
this.#copiedContent = str
return Promise.resolve(true)
}

/**
Expand All @@ -27,7 +39,10 @@ class Clipboard {
* @returns { Promise.<string> }
*/
readText () {
return navigator.clipboard.readText()
if (navigator.clipboard) {
return navigator.clipboard.readText()
}
return Promise.resolve(this.#copiedContent)
}

/**
Expand Down
29 changes: 29 additions & 0 deletions api/browser/ui/contextMenu.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,35 @@ class UIContextMenu {
this.#openedAt = Date.now()
this.#props.Events.emitLocally('ui.contextMenu.open', spec, opts)
}

/**
* Get the event position for use
* when opening a context menu
* @param { PointerEvent } e
* @returns
*/
getPositionFromEvent (e) {
if (Object.prototype.hasOwnProperty.call(e.nativeEvent, 'pointerType')) {
throw new InvalidArgumentError('Provided event is not of type PointerEvent')
}

/*
Find the root element, either an encapsulating
iframe or the body and use its position
as offset for the event
*/
let rootEl = e?.target?.ownerDocument?.defaultView?.frameElement
if (!rootEl) {
rootEl = document.body
}

const bounds = rootEl.getBoundingClientRect()

return {
x: e.clientX + bounds.x,
y: e.clientY + bounds.y
}
}
}

DIController.main.register('UIContextMenu', UIContextMenu, [
Expand Down
65 changes: 63 additions & 2 deletions api/time.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,15 @@

const DIController = require('../shared/DIController')

const SERVER_TIME_TTL_MS = 10000

class Time {
#props

#serverTime
#serverTimeUpdatedAt
#serverTimePromise

constructor (props) {
this.#props = props
}
Expand All @@ -30,10 +36,65 @@ class Time {
submitFrame (id, frame) {
return this.#props.Commands.executeRawCommand('time.submitFrame', id, frame)
}

/**
* Update the record of
* the current server time
*
* This will set the #serverTime and #serverTimeUpdatedAt
* properties, allowing the client to return the correct time
* accordingly
*
* @returns { Promise.<void> }
*/
async #updateServerTime () {
/*
Skip updating if it's already trying
to update to avoid multiple requests
*/
if (this.#serverTimePromise) {
return this.#serverTimePromise
}

/*
Resolve immediately if the local
time hasn't become outdated
*/
if (this.#serverTimeUpdatedAt && (Date.now() - this.#serverTimeUpdatedAt) < SERVER_TIME_TTL_MS) {
return Promise.resolve()
}

this.#serverTimePromise = new Promise((resolve, reject) => {
const start = Date.now()
this.#props.Commands.executeCommand('time.getServerTime')
.then(now => {
const roundtripDur = Date.now() - start
this.#serverTime = now + Math.round(roundtripDur / 2)
this.#serverTimeUpdatedAt = Date.now()
resolve()
})
.catch(err => {
this.#serverTimeUpdatedAt = Date.now()
reject(err)
})
.finally(() => {
this.#serverTimePromise = undefined
})
})
return this.#serverTimePromise
}

/**
* Get the current time according to the server,
* compensating for latency and clock drift
* @returns { Promise.<number> }
*/
async now (id) {
await this.#updateServerTime()
return this.#serverTime + (Date.now() - this.#serverTimeUpdatedAt)
}
}

DIController.main.register('Time', Time, [
'State',
'Events',
'Commands'
])
24 changes: 24 additions & 0 deletions api/time.unit.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
require('./time')

const DIController = require('../shared/DIController')

let time
beforeAll(() => {
time = DIController.main.instantiate('Time', {
Commands: {
executeCommand: command => {
if (command === 'time.getServerTime') {
return Promise.resolve(Date.now())
}
},
executeRawCommand: () => {}
}
})
})

test('get the server time', async () => {
const value = await time.now()
const now = Date.now()
expect(value).toBeGreaterThanOrEqual(now - 100)
expect(value).toBeLessThan(now + 100)
})
1 change: 1 addition & 0 deletions app/bridge.css
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ h3, .u-heading--3 {
}

h4, .u-heading--4 {
font-size: 1em;
margin: 0.2em 0;
}

Expand Down
21 changes: 2 additions & 19 deletions app/components/ContextMenuBoundary/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,20 +22,6 @@ function isNumber (x) {
return typeof x === 'number' && !Number.isNaN(x)
}

function getScreenCoordinates () {
return {
x: window.screenLeft,
y: window.screenTop
}
}

function convertToPageCoordinates (ctxX, ctxY, screenX, screenY) {
return {
x: ctxX - screenX,
y: ctxY - screenY
}
}

function sanitizeItemSpec (spec) {
const out = {}
for (const property of ALLOWED_SPEC_PROPERTIES) {
Expand Down Expand Up @@ -90,12 +76,9 @@ export function ContextMenuBoundary ({ children }) {
return
}

const screenCoords = getScreenCoordinates()
const pageCoords = convertToPageCoordinates(opts.x, opts.y, screenCoords.x, screenCoords.y)

setContextPos({
x: Math.max(pageCoords.x, 0),
y: Math.max(pageCoords.y, 0)
x: Math.max(opts.x, 0),
y: Math.max(opts.y, 0)
})

setOpts(opts)
Expand Down
3 changes: 2 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ services:
- .:/app
environment:
- NODE_ENV=development
- PORT=3000
- HTTP_PORT=3000
- HTTP_ADDR=0.0.0.0
- APP_DATA_BASE_PATH=../data
ports:
- 3000:3000
Expand Down
24 changes: 21 additions & 3 deletions docs/api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ Bridge provides a JavaScript api for use in plugins and their widgets.
- [Messages](#messages)
- [UI](#ui)
- [Context menus](#context-menus)
- [Time](#time)

## Getting started
The api is available for plugins and widgets running in either the main process or browser processes of Bridge and can be included as follows. The module will be provided by Bridge at runtime.
Expand Down Expand Up @@ -634,9 +635,15 @@ const spec = [

window.addEventListener('contextmenu', e => {
bridge.ui.contextMenu.open(spec, {
x: e.screenX, // Required
y: e.screenY, // Required
searchable: true // Optional, defaults to false, whether or not to show a search field and allow the user to search for any items in the menu
searchable: true // Optional, defaults to false, whether or not to show a search field and allow the user to search for any items in the menu,

// Coordinates must be required either by
// 1. Auto populating coordinates (recommended)
...bridge.ui.contextMenu.getPositionFromEvent(e),

// 2. Manually defining coordinates
y: 10,
x: 10
})
})
```
Expand All @@ -662,4 +669,15 @@ Close any opened context menus, this does not need to be called as a response to
```javascript
import bridge from 'bridge'
bridge.ui.contextMenu.close()
```

## Time

### `bridge.time.now(): Promise.<number>`
Get the current server time, consider this a replacement for Date.now().
This function compensates for roundtrip latency and local clock drift.

```javascript
import bridge from 'bridge'
const now = await bridge.time.now()
```
13 changes: 13 additions & 0 deletions lib/api/STime.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ class STime extends DIBase {
}

#setup () {
this.props.SCommands.registerAsyncCommand('time.getServerTime', this.getServerTime.bind(this))
this.props.SCommands.registerAsyncCommand('time.registerClock', this.registerClock.bind(this))
this.props.SCommands.registerAsyncCommand('time.getAllClocks', this.getAllClocks.bind(this))
this.props.SCommands.registerAsyncCommand('time.removeClock', this.removeClock.bind(this))
Expand Down Expand Up @@ -126,6 +127,18 @@ class STime extends DIBase {
this.#emitClocksChangedEvent()
}

/**
* Get the current
* server time
*
* @returns {
* now: number
* }
*/
getServerTime () {
return Date.now()
}

/**
* Submit a clock frame
* for a specific clock
Expand Down
3 changes: 2 additions & 1 deletion lib/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@
// SPDX-License-Identifier: MIT

exports.defaults = {
HTTP_PORT: 5544
HTTP_PORT: process.env.HTTP_PORT || 5544,
HTTP_ADDR: process.env.HTTP_ADDR || 'localhost'
}
2 changes: 1 addition & 1 deletion lib/init-common.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ const DEFAULT_HTTP_PORT = config.defaults.HTTP_PORT
logger.warn('Failed to restore user defaults, maybe it\'s the first time the application is running', err)
} finally {
UserDefaults.apply({
httpPort: process.env.PORT || json?.httpPort || DEFAULT_HTTP_PORT
httpPort: process.env.HTTP_PORT || json?.httpPort || DEFAULT_HTTP_PORT
})
}
})()
Loading
Loading