Release v2.14.1
This commit is contained in:
1 parent
635e1a9942
commit
c3299abaac
174 files changed
+4483
-1687
No files matched your search
@@ -0,0 +1,191 @@
|
||||
'use strict';
|
||||
|
||||
// This bridge state owns the in-memory socket and screen snapshot lifecycle so the
|
||||
// HTTP/WebSocket bootstrap stays focused on routing instead of mutable state.
|
||||
const { WebSocket } = require('ws');
|
||||
|
||||
function createPlayerBridgeState() {
|
||||
const playerSockets = new Map();
|
||||
const screenSnapshotCache = new Map();
|
||||
const screenSnapshotSourcesBySlug = new Map();
|
||||
const screenSnapshotSubscribersBySlug = new Map();
|
||||
const screenPlayerDeviceIds = new Map();
|
||||
|
||||
function getScreenSnapshotSourceBucket(slug) {
|
||||
const key = String(slug || '').trim();
|
||||
if (!key) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!screenSnapshotSourcesBySlug.has(key)) {
|
||||
screenSnapshotSourcesBySlug.set(key, new Map());
|
||||
}
|
||||
|
||||
return screenSnapshotSourcesBySlug.get(key);
|
||||
}
|
||||
|
||||
function getScreenSnapshotSubscriberBucket(slug) {
|
||||
const key = String(slug || '').trim();
|
||||
if (!key) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!screenSnapshotSubscribersBySlug.has(key)) {
|
||||
screenSnapshotSubscribersBySlug.set(key, new Set());
|
||||
}
|
||||
|
||||
return screenSnapshotSubscribersBySlug.get(key);
|
||||
}
|
||||
|
||||
function storeScreenSnapshot(slug, connections, deviceIds) {
|
||||
const key = String(slug || '').trim();
|
||||
const normalizedConnections = [];
|
||||
const connectionIndexes = new Map();
|
||||
|
||||
(Array.isArray(connections) ? connections : []).forEach(function (connection) {
|
||||
const identity = connection && typeof connection === 'object'
|
||||
? [String(connection.deviceId || '').trim(), String(connection.clientId || '').trim()].join('|')
|
||||
: '';
|
||||
if (!identity || !connectionIndexes.has(identity)) {
|
||||
if (identity) {
|
||||
connectionIndexes.set(identity, normalizedConnections.length);
|
||||
}
|
||||
normalizedConnections.push(connection);
|
||||
return;
|
||||
}
|
||||
normalizedConnections[connectionIndexes.get(identity)] = connection;
|
||||
});
|
||||
|
||||
const normalizedDeviceIds = Array.isArray(deviceIds) ? deviceIds.filter(Boolean).map(function (value) {
|
||||
return String(value || '').trim();
|
||||
}).filter(Boolean) : [];
|
||||
|
||||
screenSnapshotCache.set(key, {
|
||||
slug: key,
|
||||
count: normalizedConnections.length,
|
||||
connections: normalizedConnections
|
||||
});
|
||||
screenPlayerDeviceIds.set(key, Array.from(new Set(normalizedDeviceIds)));
|
||||
}
|
||||
|
||||
function buildMergedScreenSnapshot(slug) {
|
||||
const key = String(slug || '').trim();
|
||||
const sourceBucket = screenSnapshotSourcesBySlug.get(key);
|
||||
const connections = [];
|
||||
const deviceIds = [];
|
||||
|
||||
if (sourceBucket && typeof sourceBucket.forEach === 'function') {
|
||||
sourceBucket.forEach(function (payload) {
|
||||
if (payload && Array.isArray(payload.connections)) {
|
||||
connections.push.apply(connections, payload.connections);
|
||||
}
|
||||
});
|
||||
sourceBucket.forEach(function (_payload, sourceKey) {
|
||||
deviceIds.push(sourceKey);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
slug: key,
|
||||
count: connections.length,
|
||||
connections: connections,
|
||||
deviceIds: deviceIds
|
||||
};
|
||||
}
|
||||
|
||||
function broadcastScreenSnapshot(slug) {
|
||||
const key = String(slug || '').trim();
|
||||
const snapshot = buildMergedScreenSnapshot(key);
|
||||
storeScreenSnapshot(key, snapshot.connections, snapshot.deviceIds);
|
||||
|
||||
const bucket = screenSnapshotSubscribersBySlug.get(key);
|
||||
if (!bucket || !bucket.size) {
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = JSON.stringify({
|
||||
type: 'snapshot',
|
||||
slug: key,
|
||||
connections: snapshot.connections,
|
||||
sentAt: new Date().toISOString()
|
||||
});
|
||||
|
||||
bucket.forEach(function (socket) {
|
||||
if (socket && socket.readyState === WebSocket.OPEN) {
|
||||
socket.send(payload);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function setScreenSnapshotSource(slug, sourceKey, connections) {
|
||||
const key = String(slug || '').trim();
|
||||
const normalizedSourceKey = String(sourceKey || '').trim();
|
||||
if (!key || !normalizedSourceKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bucket = getScreenSnapshotSourceBucket(key);
|
||||
if (!bucket) {
|
||||
return;
|
||||
}
|
||||
|
||||
bucket.set(normalizedSourceKey, {
|
||||
slug: key,
|
||||
connections: Array.isArray(connections) ? connections : []
|
||||
});
|
||||
broadcastScreenSnapshot(key);
|
||||
}
|
||||
|
||||
function clearScreenSnapshotSource(slug, sourceKey) {
|
||||
const key = String(slug || '').trim();
|
||||
const normalizedSourceKey = String(sourceKey || '').trim();
|
||||
const bucket = screenSnapshotSourcesBySlug.get(key);
|
||||
if (!bucket || !normalizedSourceKey || !bucket.has(normalizedSourceKey)) {
|
||||
return;
|
||||
}
|
||||
|
||||
bucket.delete(normalizedSourceKey);
|
||||
if (!bucket.size) {
|
||||
screenSnapshotSourcesBySlug.delete(key);
|
||||
}
|
||||
|
||||
broadcastScreenSnapshot(key);
|
||||
}
|
||||
|
||||
function clearPlayerSnapshotSources(sourceKey) {
|
||||
const normalizedSourceKey = String(sourceKey || '').trim();
|
||||
if (!normalizedSourceKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
Array.from(screenSnapshotSourcesBySlug.keys()).forEach(function (slug) {
|
||||
clearScreenSnapshotSource(slug, normalizedSourceKey);
|
||||
});
|
||||
}
|
||||
|
||||
function getScreenSnapshotCount(slug) {
|
||||
const snapshot = screenSnapshotCache.get(String(slug || '').trim()) || { count: 0 };
|
||||
return Number(snapshot.count || 0);
|
||||
}
|
||||
|
||||
return {
|
||||
playerSockets: playerSockets,
|
||||
screenSnapshotCache: screenSnapshotCache,
|
||||
screenSnapshotSourcesBySlug: screenSnapshotSourcesBySlug,
|
||||
screenSnapshotSubscribersBySlug: screenSnapshotSubscribersBySlug,
|
||||
screenPlayerDeviceIds: screenPlayerDeviceIds,
|
||||
getScreenSnapshotSourceBucket: getScreenSnapshotSourceBucket,
|
||||
getScreenSnapshotSubscriberBucket: getScreenSnapshotSubscriberBucket,
|
||||
storeScreenSnapshot: storeScreenSnapshot,
|
||||
buildMergedScreenSnapshot: buildMergedScreenSnapshot,
|
||||
broadcastScreenSnapshot: broadcastScreenSnapshot,
|
||||
setScreenSnapshotSource: setScreenSnapshotSource,
|
||||
clearScreenSnapshotSource: clearScreenSnapshotSource,
|
||||
clearPlayerSnapshotSources: clearPlayerSnapshotSources,
|
||||
getScreenSnapshotCount: getScreenSnapshotCount
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
createPlayerBridgeState: createPlayerBridgeState
|
||||
};
|
||||
+21
-161
@@ -6,14 +6,14 @@ const fs = require('fs');
|
||||
const http = require('http');
|
||||
const path = require('path');
|
||||
const { WebSocketServer, WebSocket } = require('ws');
|
||||
const common = require('../common');
|
||||
const { verifyRequestAuth, createRequestAuthHeaders } = require('#src/request-auth');
|
||||
const common = require('#src/common');
|
||||
const { verifyRequestAuth, createRequestAuthHeaders, verifyPageAuthToken } = require('#src/request-auth');
|
||||
const { normalizeDeviceId, upsertPlayerRegistration, recordPlayerHeartbeat } = require('#src/data/player-registry');
|
||||
const { createPlayerPlaylistService } = require('../player/playlist');
|
||||
const { commitDeviceBinding, bindPlayerToScreen, getOnboardingStatus, getPlayerPublicBaseUrl } = require('../player/onboarding');
|
||||
const { createStyledQrCodeSvg } = require('../data/qr-code');
|
||||
const { verifyPageAuthToken } = require('#src/request-auth');
|
||||
const { collectLocalControlUsers } = require('../web/lib/local-control-users');
|
||||
const { createPlayerBridgeState } = require('#src/player-bridge/connection-state');
|
||||
const { createPlayerPlaylistService } = require('#src/player/playlist');
|
||||
const { commitDeviceBinding, bindPlayerToScreen, getOnboardingStatus, getPlayerPublicBaseUrl } = require('#src/player/onboarding/index');
|
||||
const { createStyledQrCodeSvg } = require('#src/data/qr-code');
|
||||
const { collectLocalControlUsers } = require('#src/web/lib/local-control-users');
|
||||
|
||||
|
||||
function createThinClientConfig() {
|
||||
@@ -187,6 +187,7 @@ async function start() {
|
||||
const server = http.createServer(app);
|
||||
const pool = common.createPool();
|
||||
const config = createThinClientConfig();
|
||||
const bridgeState = createPlayerBridgeState();
|
||||
const localPlayerInternalUrl = String(process.env.LOCAL_PLAYER_INTERNAL_URL || process.env.PLAYER_INTERNAL_URL || '').trim().replace(/\/$/, '');
|
||||
const playerPlaylistService = createPlayerPlaylistService({
|
||||
pool: pool,
|
||||
@@ -197,133 +198,21 @@ async function start() {
|
||||
app.use(express.json());
|
||||
const playersWs = new WebSocketServer({ noServer: true });
|
||||
const screenSnapshotsWs = new WebSocketServer({ noServer: true });
|
||||
const playerSockets = new Map();
|
||||
const screenSnapshotCache = new Map();
|
||||
const screenSnapshotSourcesBySlug = new Map();
|
||||
const screenSnapshotSubscribersBySlug = new Map();
|
||||
const screenPlayerDeviceIds = new Map();
|
||||
const playerSockets = bridgeState.playerSockets;
|
||||
const screenSnapshotCache = bridgeState.screenSnapshotCache;
|
||||
const screenSnapshotSourcesBySlug = bridgeState.screenSnapshotSourcesBySlug;
|
||||
const screenSnapshotSubscribersBySlug = bridgeState.screenSnapshotSubscribersBySlug;
|
||||
const screenPlayerDeviceIds = bridgeState.screenPlayerDeviceIds;
|
||||
const pendingPlayerCommands = new Map();
|
||||
|
||||
function getScreenSnapshotSourceBucket(slug) {
|
||||
const key = String(slug || '').trim();
|
||||
if (!key) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!screenSnapshotSourcesBySlug.has(key)) {
|
||||
screenSnapshotSourcesBySlug.set(key, new Map());
|
||||
}
|
||||
|
||||
return screenSnapshotSourcesBySlug.get(key);
|
||||
}
|
||||
|
||||
function getScreenSnapshotSubscriberBucket(slug) {
|
||||
const key = String(slug || '').trim();
|
||||
if (!key) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!screenSnapshotSubscribersBySlug.has(key)) {
|
||||
screenSnapshotSubscribersBySlug.set(key, new Set());
|
||||
}
|
||||
|
||||
return screenSnapshotSubscribersBySlug.get(key);
|
||||
}
|
||||
|
||||
function buildMergedScreenSnapshot(slug) {
|
||||
const key = String(slug || '').trim();
|
||||
const sourceBucket = screenSnapshotSourcesBySlug.get(key);
|
||||
const connections = [];
|
||||
const deviceIds = [];
|
||||
|
||||
if (sourceBucket && typeof sourceBucket.forEach === 'function') {
|
||||
sourceBucket.forEach(function (payload) {
|
||||
if (payload && Array.isArray(payload.connections)) {
|
||||
connections.push.apply(connections, payload.connections);
|
||||
}
|
||||
});
|
||||
sourceBucket.forEach(function (_payload, sourceKey) {
|
||||
deviceIds.push(sourceKey);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
slug: key,
|
||||
count: connections.length,
|
||||
connections: connections,
|
||||
deviceIds: deviceIds
|
||||
};
|
||||
}
|
||||
|
||||
function broadcastScreenSnapshot(slug) {
|
||||
const key = String(slug || '').trim();
|
||||
const snapshot = buildMergedScreenSnapshot(key);
|
||||
storeScreenSnapshot(key, snapshot.connections, snapshot.deviceIds);
|
||||
|
||||
const bucket = screenSnapshotSubscribersBySlug.get(key);
|
||||
if (!bucket || !bucket.size) {
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = JSON.stringify({
|
||||
type: 'snapshot',
|
||||
slug: key,
|
||||
connections: snapshot.connections,
|
||||
sentAt: new Date().toISOString()
|
||||
});
|
||||
|
||||
bucket.forEach(function (socket) {
|
||||
if (socket && socket.readyState === WebSocket.OPEN) {
|
||||
socket.send(payload);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function setScreenSnapshotSource(slug, sourceKey, connections) {
|
||||
const key = String(slug || '').trim();
|
||||
const normalizedSourceKey = String(sourceKey || '').trim();
|
||||
if (!key || !normalizedSourceKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bucket = getScreenSnapshotSourceBucket(key);
|
||||
if (!bucket) {
|
||||
return;
|
||||
}
|
||||
|
||||
bucket.set(normalizedSourceKey, {
|
||||
slug: key,
|
||||
connections: Array.isArray(connections) ? connections : []
|
||||
});
|
||||
broadcastScreenSnapshot(key);
|
||||
}
|
||||
|
||||
function clearScreenSnapshotSource(slug, sourceKey) {
|
||||
const key = String(slug || '').trim();
|
||||
const normalizedSourceKey = String(sourceKey || '').trim();
|
||||
const bucket = screenSnapshotSourcesBySlug.get(key);
|
||||
if (!bucket || !normalizedSourceKey || !bucket.has(normalizedSourceKey)) {
|
||||
return;
|
||||
}
|
||||
|
||||
bucket.delete(normalizedSourceKey);
|
||||
if (!bucket.size) {
|
||||
screenSnapshotSourcesBySlug.delete(key);
|
||||
}
|
||||
|
||||
broadcastScreenSnapshot(key);
|
||||
}
|
||||
|
||||
function clearPlayerSnapshotSources(sourceKey) {
|
||||
const normalizedSourceKey = String(sourceKey || '').trim();
|
||||
if (!normalizedSourceKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
Array.from(screenSnapshotSourcesBySlug.keys()).forEach(function (slug) {
|
||||
clearScreenSnapshotSource(slug, normalizedSourceKey);
|
||||
});
|
||||
}
|
||||
const getScreenSnapshotSourceBucket = bridgeState.getScreenSnapshotSourceBucket;
|
||||
const getScreenSnapshotSubscriberBucket = bridgeState.getScreenSnapshotSubscriberBucket;
|
||||
const buildMergedScreenSnapshot = bridgeState.buildMergedScreenSnapshot;
|
||||
const broadcastScreenSnapshot = bridgeState.broadcastScreenSnapshot;
|
||||
const setScreenSnapshotSource = bridgeState.setScreenSnapshotSource;
|
||||
const clearScreenSnapshotSource = bridgeState.clearScreenSnapshotSource;
|
||||
const clearPlayerSnapshotSources = bridgeState.clearPlayerSnapshotSources;
|
||||
const storeScreenSnapshot = bridgeState.storeScreenSnapshot;
|
||||
|
||||
async function fetchPlayerSnapshotRegistrations() {
|
||||
if (typeof common.fetchPlayerRegistrations === 'function') {
|
||||
@@ -471,35 +360,6 @@ async function start() {
|
||||
return Array.isArray(deviceIds) ? deviceIds.slice() : [];
|
||||
}
|
||||
|
||||
function storeScreenSnapshot(slug, connections, deviceIds) {
|
||||
const key = String(slug || '').trim();
|
||||
const normalizedConnections = [];
|
||||
const connectionIndexes = new Map();
|
||||
(Array.isArray(connections) ? connections : []).forEach(function (connection) {
|
||||
const identity = connection && typeof connection === 'object'
|
||||
? [String(connection.deviceId || '').trim(), String(connection.clientId || '').trim()].join('|')
|
||||
: '';
|
||||
if (!identity || !connectionIndexes.has(identity)) {
|
||||
if (identity) {
|
||||
connectionIndexes.set(identity, normalizedConnections.length);
|
||||
}
|
||||
normalizedConnections.push(connection);
|
||||
return;
|
||||
}
|
||||
normalizedConnections[connectionIndexes.get(identity)] = connection;
|
||||
});
|
||||
const normalizedDeviceIds = Array.isArray(deviceIds) ? deviceIds.map(function (value) {
|
||||
return normalizeDeviceId(value);
|
||||
}).filter(Boolean) : [];
|
||||
|
||||
screenSnapshotCache.set(key, {
|
||||
slug: key,
|
||||
count: normalizedConnections.length,
|
||||
connections: normalizedConnections
|
||||
});
|
||||
screenPlayerDeviceIds.set(key, Array.from(new Set(normalizedDeviceIds)));
|
||||
}
|
||||
|
||||
app.get('/health', function (_req, res) {
|
||||
res.json({ ok: true, service: 'player-bridge' });
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user