const express = require('express'); const fs = require('fs'); const http = require('http'); const path = require('path'); const common = require('./common'); const { createPlayerRuntime } = require('./player/runtime'); const { createPlayerPlaylistService } = require('./player/playlist'); const { createRtmpStreamService } = require('./player/modules/rtmp-streams'); const { normalizeDeviceId, registerPlayerOnboardingRoutes, commitDeviceBinding } = require('./player/onboarding'); const { createOnboardingStore } = require('./player/onboarding/store'); const { registerPlayerRoutes } = require('./player/routes'); const { ensureFontLibrary } = require('#src/web/lib/media/font-library'); // Player runtime, media API, and websocket wiring. async function start() { const app = express(); const pool = common.createPool(); const PORT = Number(process.env.PLAYER_PORT || 8081); const PLAYER_PUBLIC_BASE_URL = String(process.env.PLAYER_PUBLIC_BASE_URL || process.env.PLAYER_BASE_URL || '').trim().replace(/\/$/, ''); const PLAYER_INTERNAL_BASE_URL = String(process.env.PLAYER_INTERNAL_BASE_URL || process.env.PLAYER_BASE_URL || '').trim().replace(/\/$/, ''); const PLAYER_IDENTIFIER = '1'; const ASSET_DIR = path.join(__dirname, 'player', 'public'); const MEDIA_DIR = path.join(__dirname, '..', 'media'); const ONBOARDING_QUEUE_FILE = path.join(MEDIA_DIR, 'player-onboarding-queue.json'); const DB_SYNC_INTERVAL_MS = Number(process.env.PLAYER_DB_SYNC_INTERVAL_MS || 15000); const onboardingStore = createOnboardingStore(ONBOARDING_QUEUE_FILE); const playerRuntime = createPlayerRuntime({ pool: pool, normalizeDeviceId: normalizeDeviceId }); const playerPlaylistService = createPlayerPlaylistService({ pool: pool, common: common, snapshotDir: path.join(MEDIA_DIR, 'player-cache', 'screen-playlists') }); const rtmpStreamService = createRtmpStreamService({ mediaDir: MEDIA_DIR }); const server = http.createServer(app); playerRuntime.installWebsocket(server); app.use(express.json()); registerPlayerOnboardingRoutes(app, { pool: pool, common: common, playerRuntime: playerRuntime, onboardingStore: onboardingStore, playerPublicBaseUrl: PLAYER_PUBLIC_BASE_URL, playerInternalBaseUrl: PLAYER_INTERNAL_BASE_URL, QRCode: require('qrcode') }); registerPlayerRoutes(app, { pool: pool, common: common, mediaDir: MEDIA_DIR, assetDir: ASSET_DIR, playerRuntime: playerRuntime, playerPlaylistService: playerPlaylistService, rtmpStreamService: rtmpStreamService, playerPublicBaseUrl: PLAYER_PUBLIC_BASE_URL, playerInternalBaseUrl: PLAYER_INTERNAL_BASE_URL, playerIdentifier: PLAYER_IDENTIFIER }); app.use(function (error, _req, res, _next) { console.error(error); res.status(error.statusCode || 500).send(error.statusCode ? error.message : 'Internal server error'); }); fs.mkdirSync(MEDIA_DIR, { recursive: true }); await ensureFontLibrary(MEDIA_DIR); server.listen(PORT, function () { console.log(`Pulse Signage app listening on port ${PORT}`); }); async function syncDatabaseState() { try { await common.ensureSchema(pool, { mediaDir: MEDIA_DIR }); await common.bootstrapDatabase(pool); if (playerRuntime.snapshotAllConnections().length > 0) { await common.pruneStaleOnboardingDevices(pool); } await onboardingStore.flushBindings(function (entry) { return commitDeviceBinding( pool, entry.deviceId, entry.clientName, entry.screenSlug, playerRuntime.isClientNameAvailableOnScreen, playerRuntime.snapshotAllConnections() ); }); } catch (error) { console.error(error); } } await syncDatabaseState(); if (PLAYER_IDENTIFIER) { const { upsertPlayerRegistration } = require('./player/onboarding'); await upsertPlayerRegistration(pool, PLAYER_IDENTIFIER, PLAYER_PUBLIC_BASE_URL, PLAYER_INTERNAL_BASE_URL, null); } setInterval(function () { syncDatabaseState().catch(function (error) { console.error(error); }); }, DB_SYNC_INTERVAL_MS); } module.exports = { start }; if (require.main === module) { start().catch(function (error) { console.error(error); process.exit(1); }); }