Skip to content
Open
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
6 changes: 6 additions & 0 deletions apps/electron/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
"electron-builder": "26.9.1",
"wait-on": "^7.2.0"
},
"dependencies": {
"@stagetimerio/grandiose": "0.2.0"
},
"scripts": {
"dev:electron": "wait-on http://localhost:3000 && cross-env NODE_ENV=development electron .",
"lint": "oxlint --quiet --type-aware",
Expand All @@ -28,6 +31,9 @@
"productName": "ontime",
"appId": "no.lightdev.ontime",
"asar": true,
"asarUnpack": [
"node_modules/@stagetimerio/grandiose/dist/**"
],
"dmg": {
"artifactName": "ontime-macOS-${arch}.dmg"
},
Expand Down
28 changes: 24 additions & 4 deletions apps/electron/src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ const path = require('path');

const { getApplicationMenu } = require('./menu/applicationMenu.js');
const { getTrayMenu } = require('./menu/trayMenu.js');
const { NdiOutputManager } = require('./ndi/NdiOutputManager.js');

const electronConfig = require('./electron.config.js');
const {
Expand Down Expand Up @@ -35,6 +36,8 @@ let isQuitting = false;
let win;
let splash;
let tray = null;
let ndiOutputManager;
let refreshApplicationMenu = () => {};

/**
* Coordinates the node process startup
Expand Down Expand Up @@ -76,6 +79,8 @@ function showNotification(title, body) {
* Terminate node service and close electron app
*/
function appShutdown() {
ndiOutputManager?.stopAll();

// terminate node service
(async () => {
const ontimeServer = require(nodePath);
Expand Down Expand Up @@ -199,14 +204,29 @@ app.whenReady().then(() => {
}

createWindow();
ndiOutputManager = new NdiOutputManager({
icon: appIcon,
onError: (title, message) => dialog.showErrorBox(title, message),
});

startBackend()
.then((port) => {
const clientUrl = getClientUrl(port);
const serverUrl = getServerUrl(port);
const menu = getApplicationMenu(askToQuit, clientUrl, serverUrl, redirectWindow, showDialog, (url) =>
win.webContents.downloadURL(url),
);
Menu.setApplicationMenu(menu);
refreshApplicationMenu = () => {
const menu = getApplicationMenu(
askToQuit,
clientUrl,
serverUrl,
redirectWindow,
showDialog,
(url) => win.webContents.downloadURL(url),
ndiOutputManager,
refreshApplicationMenu,
);
Menu.setApplicationMenu(menu);
};
refreshApplicationMenu();

win
.loadURL(`${clientUrl}/editor`)
Expand Down
99 changes: 98 additions & 1 deletion apps/electron/src/menu/applicationMenu.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,26 @@ const {
* @param {function} redirectWindow - function to redirect main window content
* @param {function} showDialog - asks the react app to show a user dialog
* @param {function} download - function to download a resource from url
* @param {import('../ndi/NdiOutputManager.js').NdiOutputManager} ndiOutputManager - NDI output manager
* @param {function} refreshMenu - rebuilds the application menu
* @returns {Menu} - application menu
*/
function getApplicationMenu(askToQuit, clientUrl, serverUrl, redirectWindow, showDialog, download) {
function getApplicationMenu(
askToQuit,
clientUrl,
serverUrl,
redirectWindow,
showDialog,
download,
ndiOutputManager,
refreshMenu,
) {
const template = [
...(isMac ? [makeMacMenu(askToQuit)] : []),
makeFileMenu(askToQuit, serverUrl, redirectWindow, showDialog, download),
makeEditMenu(),
makeViewMenu(clientUrl),
makeNdiMenu(clientUrl, ndiOutputManager, refreshMenu),
makeSettingsMenu(redirectWindow),
makeHelpMenu(redirectWindow),
...(isProduction ? [] : [{ label: 'Dev', submenu: [{ role: 'toggleDevTools' }] }]),
Expand Down Expand Up @@ -173,6 +185,91 @@ function makeViewMenu(clientUrl) {
};
}

const ndiViews = [
{ id: 'timer', label: 'Timer', path: '/timer' },
{ id: 'backstage', label: 'Backstage', path: '/backstage' },
{ id: 'studio', label: 'Studio Clock', path: '/studio' },
{ id: 'countdown', label: 'Countdown', path: '/countdown' },
{ id: 'info', label: 'Project info', path: '/info' },
];

const ndiResolutions = [
{ label: '720p', width: 1280, height: 720 },
{ label: '1080p', width: 1920, height: 1080 },
{ label: '1440p', width: 2560, height: 1440 },
{ label: '2160p', width: 3840, height: 2160 },
];

const ndiFrameRates = [25, 30, 50, 60];

/**
* Utility function generates the NDI menu
* @param {string} clientUrl - base url for the application
* @param {import('../ndi/NdiOutputManager.js').NdiOutputManager} ndiOutputManager - NDI output manager
* @param {function} refreshMenu - rebuilds the application menu
* @returns {Object}
*/
function makeNdiMenu(clientUrl, ndiOutputManager, refreshMenu) {
const format = ndiOutputManager.getFormat();
const hasActiveOutputs = ndiViews.some((view) => ndiOutputManager.isActive(view.id));

return {
label: 'NDI',
submenu: [
{
label: `Resolution: ${format.width}x${format.height}`,
submenu: ndiResolutions.map((resolution) => ({
label: `${resolution.label} (${resolution.width}x${resolution.height})`,
type: 'radio',
checked: format.width === resolution.width && format.height === resolution.height,
click: async () => {
await ndiOutputManager.setFormat(resolution);
refreshMenu();
},
})),
},
{
label: `Frame rate: ${format.fps}fps`,
submenu: ndiFrameRates.map((fps) => ({
label: `${fps}fps`,
type: 'radio',
checked: format.fps === fps,
click: async () => {
await ndiOutputManager.setFormat({ fps });
refreshMenu();
},
})),
},
{ type: 'separator' },
...ndiViews.map((view) => ({
label: `Output ${view.label}`,
type: 'checkbox',
checked: ndiOutputManager.isActive(view.id),
click: async () => {
if (ndiOutputManager.isActive(view.id)) {
ndiOutputManager.stopOutput(view.id);
} else {
await ndiOutputManager.startOutput(view.id, {
name: `Ontime ${view.label}`,
url: `${clientUrl}${view.path}?n=1`,
});
}
refreshMenu();
},
})),
{ type: 'separator' },
{
label: 'Stop all NDI outputs',
enabled: hasActiveOutputs,
click: () => {
ndiOutputManager.stopAll();
refreshMenu();
},
},
],
};
}

/**
* Utility function generates the settings menu
* @param {function} redirectWindow - function to redirect main window content
Expand Down
Loading