A practical guide for developers setting up and working with the Xibo Electron Player and its companion layout renderer library.
- Prerequisites
- Repository Setup
- Running in Development
- Building & Packaging
- Architecture Overview
- Process Deep-Dives
- Key Data Flows
- Configuration & Environment
- Logging & Debugging
- Common Tasks
| Tool | Version | Notes |
|---|---|---|
| Node.js | 22 or newer | Electron and better-sqlite3 both require Node >= 22 |
| npm | bundled with Node | No Yarn/pnpm |
| Git | any recent | SSH key required (XLR is a git dependency) |
| Electron | auto-installed | Version pinned in package.json devDependencies |
| Python | 3.6 or newer | Required to compile better-sqlite3. See below |
better-sqlite3 has no prebuilt binaries for the Electron versions this project targets, so it is
compiled from source during npm run rebuild. That needs both a C++ compiler and Python, on every
platform.
All platforms: Install Python 3. On Windows, use the installer from
python.org with "Add python.exe to PATH" ticked; the Microsoft
Store version does not work with node-gyp. After installing, restart your terminal or editor so it
picks up the new PATH.
Windows only: Install Visual Studio Build Tools (C++ workload), required for better-sqlite3 native bindings.
Linux only: snapcraft is required if packaging Snap.
This player depends on two repositories. Clone both into the same parent directory so the dev alias works correctly.
# From your preferred workspace root (e.g. ~/dev/xibo/)
git clone git@github.com:xibosignage/electron-player.git
git clone git@github.com:xibosignage/xibo-layout-renderer.gitYour directory structure should look like:
xibo/
├── electron-player/ ← this app
└── xibo-layout-renderer/ ← layout renderer library
cd electron-player
npm install
npm run rebuildBoth steps are required. npm install also downloads the Electron binary via a postinstall hook,
since Electron no longer fetches it during a plain install.
npm run rebuild compiles better-sqlite3 against the current Electron version. It takes a few
minutes and prints compiler output, which is expected. Skipping it lets the app start and then fail
when it first opens the database, so run it after every npm install and after every Electron or
Node version change.
If it fails with Could not find any Python installation to use, check
Prerequisites. If Python is installed and works in a fresh terminal but not in
your editor, fully quit and reopen the editor: it passes its own copy of PATH to integrated
terminals, and that copy is captured at launch.
The renderer library must be built before the player can use it in dev mode:
cd ../xibo-layout-renderer
npm install
npm run build # generates dist/You only need to redo this when you pull changes to xibo-layout-renderer.
cd electron-player
npm run devThis starts electron-vite dev with:
- HMR on the renderer process
- Sourcemaps enabled
- Remote debugging on port
9222(attach Chrome DevTools atchrome://inspect) - Node inspector on the main process (
--inspect src/main/index.ts)
In dev mode, the alias @xibosignage/xibo-layout-renderer resolves to ../xibo-layout-renderer (your local clone), so changes to the renderer library are picked up immediately after a rebuild.
On first run the player has no configuration. It will show the activation screen where you enter:
- A CMS URL
- A CMS security key
Or you can enter an activation code generated by the CMS.
| Command | What it does |
|---|---|
npm run build |
Compiles all processes to dist/ via electron-vite |
npm run start |
Previews the built output (no HMR) |
npm run package |
Build + Electron Forge package (creates unpacked app) |
npm run make |
Build + Electron Forge make (creates installer) |
npm run make:snap |
Builds a Snap package (Linux, requires snapcraft) |
Electron Forge is configured in forge.config.cjs with makers for:
- Windows — Squirrel installer (
.exe/.msi) - Linux — Debian package (
.deb) + Snap - macOS — ZIP archive
In production builds, xibo-layout-renderer is bundled into the app (not aliased to the local clone).
The application is a standard three-process Electron app augmented by a separate renderer library.
┌─────────────────────────────────────────────────────────────┐
│ electron-player │
│ │
│ ┌──────────────────┐ ┌─────────────────────────┐ │
│ │ Main Process │ IPC │ Renderer Process │ │
│ │ (Node.js) │◄──────►│ (Chromium / browser) │ │
│ │ │ │ │ │
│ │ - XMDS client │ │ - XiboLayoutRenderer │ │
│ │ - File manager │ │ (XLR library) │ │
│ │ - Schedule mgr │ │ - ConfigHandler UI │ │
│ │ - Express server│ │ - Stats / Faults UI │ │
│ │ - SQLite stats │ │ │ │
│ └──────────────────┘ └─────────────────────────┘ │
│ ▲ ▲ │
│ │ │ HTTP │
│ ┌─────┴──────┐ ┌──────┴──────┐ │
│ │ Preload │ │ Express │ │
│ │ (bridge) │ │ port 9696 │ │
│ └────────────┘ └─────────────┘ │
│ ▲ │
│ │ serves │
│ ~/Documents/xibo_library/ │
└─────────────────────────────────────────────────────────────┘
▲
│ SOAP (XMDS)
Xibo CMS
| Directory | Electron process | Runtime |
|---|---|---|
src/main/ |
Main | Node.js |
src/preload/ |
Preload | Sandboxed Node subset |
src/renderer/ |
Renderer | Chromium (browser APIs) |
src/shared/ |
— | Used by both main and renderer |
The main process is the backend of the application. Its responsibilities:
- Creates the
BrowserWindowand loads the renderer - Initialises
Config,ConsoleDB,Faults, andStatesingletons - Manages the XMDS polling loop (register → get required files → schedule → collect stats)
- Listens for IPC events from the renderer and forwards commands back
- Reads/writes
config.jsonandcms_config.jsonfrom Electron'suserDatadirectory - Generates a stable
hardwareKeyfrom the machine ID (max 40 chars) - Assigns a random UUID as the
xmrChannelon first run
- Polls the CMS schedule
- Determines which layout(s) to play, including overlay layouts
- Sends
update-loopandupdate-overlaysIPC events to the renderer
- Downloads required media files from the CMS
- Stores them in
~/Documents/xibo_library/ - Tracks download state to avoid re-fetching unchanged files
StatsDB.tsusesbetter-sqlite3to persist play statistics locallyPoPStats.tsbatches and submits stats to the CMS via XMDS
- Runs an Express HTTP server on port
9696 - Serves media files from the library directory to the renderer via
http://localhost:9696/files/ - The renderer cannot access the local filesystem directly, so this bridge is necessary
- SOAP client for the XMDS protocol (Xibo's proprietary CMS API)
- Handles:
RegisterDisplay,RequiredFiles,Schedule,SubmitLog,SubmitStats
The preload runs in a privileged context with access to both Node.js APIs and the DOM. It exposes a safe, typed API to the renderer via contextBridge.
Two objects are exposed on window:
window.apiHandler // renderer → main (config, XMDS triggers)
window.playerAPI // main ↔ renderer (layout lifecycle, state updates)The preload also installs an extended console that pipes console.log/warn/error calls over IPC so they are captured by the main process logger.
The renderer is a plain HTML/JS page running inside the Electron BrowserWindow. No framework (React, Vue, etc.) — it uses the XLR library directly.
- Initialises
XiboLayoutRendererwith current schedule and overlay data - Calls
xlr.bootstrap()to insert the XLR container into the DOM - Registers event handlers for layout lifecycle events:
layoutStart/layoutEnd→ report stats via BroadcastChannelcommandCodeReceived→ forward command to main processoverlayEnd→ request next overlay from main process
- Subscribes to
playerAPIevents pushed from the main process:onUpdateLoop→ callsxlr.updateLoop(newLayouts)onUpdateOverlays→ callsxlr.updateOverlays(overlays)onStateChange→ updates displayed state info
- Shown when the player is not yet configured
- Supports both activation-code flow and manual CMS URL + key entry
- Calls
window.apiHandler.xmdsTryRegister()to register with the CMS
This is a separate library consumed by the renderer process. It handles all the visual playback logic — the electron-player is responsible for delivering schedules and assets; XLR renders them.
XiboLayoutRenderer(layouts, overlays, options)
│
▼
xlr.bootstrap() ← inserts DOM skeleton, shows splash screen
│
xlr.init() ← parses layout XLF XML, creates Layout objects
│
xlr.playSchedules()← makes the first layout visible
│
┌────▼──────┐
│ Layout │ ← manages a single layout's DOM and timing
│ ├─Region │ ← a rectangle on screen, plays a media playlist
│ │ └─Media ← image / video / audio / html widget
│ └─Region │
└───────────┘
Layout lifecycle states: IDLE → RUNNING → PLAYED → CANCELLED | ERROR
Key events emitted by XLR (subscribe via xlr.on(event, handler)):
| Event | When |
|---|---|
layoutStart |
A layout begins playing |
layoutEnd |
A layout finishes all regions |
widgetStart / widgetEnd |
An individual media item starts/stops |
updateLoop |
XLR requests a refreshed schedule |
overlayEnd |
An overlay layout has finished |
commandCodeReceived |
A command widget fired |
Platform mode: the player sets platform: ConsumerPlatform.ELECTRON in the XLR options. This enables electron-specific behaviour (fault reporting, local file URLs, etc.).
Main: index.ts
→ loads config.json
→ if not configured → sends 'configure' IPC → renderer shows ConfigHandler
→ if configured → starts XMDS registration loop
→ xmds.RegisterDisplay()
→ xmds.RequiredFiles() → fileManager downloads assets
→ xmds.Schedule() → scheduleManager parses schedule
→ sends 'update-loop' IPC to renderer
Main: scheduleManager
→ parses schedule XML into layout objects
→ sends IPC 'update-loop' with array of { layoutId, file, ... }
Renderer: renderer.ts
→ receives onUpdateLoop(layouts)
→ calls xlr.updateLoop(layouts)
XLR: xibo-layout-renderer.ts
→ fetches layout XLF from http://localhost:9696/files/<layoutId>.xlf
→ parses XML → creates Layout → Region → Media objects
→ plays current layout, queues next
→ emits 'layoutStart', 'layoutEnd' events back to renderer
XLR emits layoutStart / widgetStart
→ renderer.ts catches event
→ sends stats message via BroadcastChannel ('xibo-stats')
Main process stats listener
→ receives BroadcastChannel message via IPC
→ writes to SQLite via StatsDB
→ batches and submits to CMS via xmds.SubmitStats()
package.json is the single source of truth for the app version. Two fields are defined there:
{
"version": "4.0.3",
"versionCode": 403
}electron.vite.config.js reads these at build time via readFileSync and injects them as compile-time constants available to all processes:
| Constant | Type | Value example |
|---|---|---|
__APP_VERSION__ |
string |
"4.0.3" |
__APP_VERSION_CODE__ |
number |
403 |
These constants replace any runtime variable lookups — no .env files are needed for versioning.
snap/snapcraft.yaml is also kept in sync: scripts/set-snap-version.cjs runs automatically as part of make:snap and writes the version field from package.json into the YAML before snapcraft pack is invoked.
To bump the version, edit only package.json.
| File | Contents |
|---|---|
config.json |
Player settings: CMS URL, key, hardware ID, display name, paths |
cms_config.json |
CMS-pushed display settings (resolution, timezone, etc.) |
Media assets are downloaded to:
~/Documents/xibo_library/
The Express server at http://localhost:9696/files/ serves everything in this directory.
| Alias | Resolves to |
|---|---|
@renderer/* |
src/renderer/src/* |
@shared/* |
src/shared/* |
Attach Chrome DevTools to the renderer process:
- Open
chrome://inspectin any Chromium browser - Click Configure and add
localhost:9222 - The player's renderer window will appear under "Remote Target"
The main process Node inspector is also available on the default port — attach with VS Code's Node debugger or chrome://inspect.
The preload overrides console.* methods to forward all log output over IPC to the main process. Logs are persisted to ConsoleDB (SQLite) and can be viewed via the player's status window.
Faults.ts tracks and persists faults. The renderer initiates fault collection via playerAPI.initFaults() and reports faults back via BroadcastChannel (xibo-faults).
| Channel | Direction | Meaning |
|---|---|---|
configure |
main → renderer | Show configuration screen |
state-change |
main → renderer | Player state updated |
update-loop |
main → renderer | New layout schedule |
update-overlays |
main → renderer | New overlay list |
showStatusWindow |
main → renderer | Show status overlay |
open-child-window |
renderer → main | Open a secondary window |
report-fault |
renderer → main | Fault detected |
Edit files in ../xibo-layout-renderer/src/, then:
cd ../xibo-layout-renderer && npm run buildThe dev server will pick up the rebuilt output automatically (the alias points to dist/). Restart npm run dev if HMR doesn't catch the change.
- Define the handler type in
src/shared/types.ts(add toPlayerAPIorApiHandler) - Register the
ipcMain.handleoripcMain.oninsrc/main/index.ts - Expose it via
contextBridge.exposeInMainWorldinsrc/preload/index.ts - Consume
window.playerAPI.*orwindow.apiHandler.*insrc/renderer/
- Create a response parser in
src/main/xmds/response/ - Add the call in
src/main/xmds/xmds.ts - Wire it into the polling loop in
src/main/index.ts
The Express server port is hardcoded to 9696. To change it, update the port constant in src/main/express.ts and the appHost option passed to XiboLayoutRenderer in src/renderer/src/renderer.ts.
npm run build # compiles to dist/
npm run start # launches the compiled appnpm run make # produces installer in out/make/Windows builds must be made on Windows; Linux builds on Linux.
Electron only supports the latest three major versions, so this needs doing periodically. Check the release schedule for what is still supported.
npm install electron@<major> # bump the version
npm install # required: see below
npm run rebuild # recompile better-sqlite3 for the new version
npm run buildThe bare npm install on the second line is not redundant. npm skips the project's postinstall
hook when you install a named package, so npm install electron@<major> swaps the package without
downloading the new binary. The build then fails with [vite:bytecode] Electron uninstall, which
gives no hint about the cause. A plain npm install afterwards triggers the hook. Running
npx install-electron --no directly has the same effect.
Two other things may need attention when moving to a new major:
node-abi, overridden inpackage.json, maps Electron versions to build identifiers. Ifnpm run rebuildfails withCould not detect abi for version <x> and runtime electron, raise that override to a version that lists the new release.better-sqlite3must support the V8 version Electron ships. If the rebuild fails with C++ errors referencingv8::, the library needs upgrading rather than the build fixing.