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 { pruneStaleOnboardingDevices } = require('./db'); // Player runtime, media API, and websocket wiring. async function start() { const app = express(); const pool = common.createPool(); const PORT = Number(process.env.PLAYER_PORT || 3001); 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, QRCode: require('qrcode') }); registerPlayerRoutes(app, { pool: pool, common: common, mediaDir: MEDIA_DIR, assetDir: ASSET_DIR, playerRuntime: playerRuntime, playerPlaylistService: playerPlaylistService, rtmpStreamService: rtmpStreamService }); 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 }); server.listen(PORT, function () { console.log(`Pulse Signage app listening on port ${PORT}`); }); async function syncDatabaseState() { try { await common.ensureSchema(pool, { mediaDir: MEDIA_DIR }); if (playerRuntime.snapshotAllConnections().length > 0) { await 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(); 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); }); }