Release 2.6.7
This commit is contained in:
+238
-58
@@ -44,6 +44,55 @@ function formatPlayerConnectionLabel(deviceId, remoteAddress) {
|
||||
return normalizedRemoteAddress ? `${normalizedDeviceId} (ip ${normalizedRemoteAddress})` : normalizedDeviceId;
|
||||
}
|
||||
|
||||
function normalizeProxyBaseUrl(value) {
|
||||
const normalized = String(value || '').trim().replace(/\/$/, '');
|
||||
if (!normalized) {
|
||||
return '';
|
||||
}
|
||||
|
||||
try {
|
||||
const url = new URL(normalized);
|
||||
if (url.hostname === 'localhost' || url.hostname === '127.0.0.1' || url.hostname === '::1') {
|
||||
url.hostname = 'host.docker.internal';
|
||||
}
|
||||
return url.toString().replace(/\/$/, '');
|
||||
} catch (_error) {
|
||||
return normalized;
|
||||
}
|
||||
}
|
||||
|
||||
function isLocalLikeBaseUrl(value) {
|
||||
let host = '';
|
||||
try {
|
||||
host = new URL(String(value || '').trim().replace(/\/$/, '')).hostname.toLowerCase();
|
||||
} catch (_error) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return host === 'localhost'
|
||||
|| host === '127.0.0.1'
|
||||
|| host === '::1'
|
||||
|| host === 'host.docker.internal'
|
||||
|| host === 'player'
|
||||
|| host === 'player-dev'
|
||||
|| host === 'player-local'
|
||||
|| host === 'player-bridge-dev'
|
||||
|| host === 'web'
|
||||
|| host === 'player-bridge'
|
||||
|| host.endsWith('.local')
|
||||
|| host.endsWith('.internal')
|
||||
|| host.endsWith('.docker.internal');
|
||||
}
|
||||
|
||||
function resolveSnapshotUpstreamBaseUrl(player) {
|
||||
const internalBaseUrl = normalizeProxyBaseUrl(player && player.internal_base_url);
|
||||
if (internalBaseUrl && isLocalLikeBaseUrl(internalBaseUrl)) {
|
||||
return internalBaseUrl;
|
||||
}
|
||||
|
||||
return normalizeProxyBaseUrl(player && player.public_base_url) || null;
|
||||
}
|
||||
|
||||
function resolveScreenCommandTargets(slug, playerSockets, screenPlayerDeviceIds) {
|
||||
const key = String(slug || '').trim();
|
||||
if (!key || !screenPlayerDeviceIds || typeof screenPlayerDeviceIds.get !== 'function' || !playerSockets || typeof playerSockets.get !== 'function') {
|
||||
@@ -51,24 +100,38 @@ function resolveScreenCommandTargets(slug, playerSockets, screenPlayerDeviceIds)
|
||||
}
|
||||
|
||||
const deviceIds = screenPlayerDeviceIds.get(key);
|
||||
if (!Array.isArray(deviceIds) || !deviceIds.length) {
|
||||
return [];
|
||||
const targets = Array.isArray(deviceIds)
|
||||
? Array.from(new Set(deviceIds.map(function (value) {
|
||||
return normalizeDeviceId(value);
|
||||
}).filter(Boolean))).map(function (deviceId) {
|
||||
const socket = playerSockets.get(deviceId);
|
||||
if (!socket || socket.readyState !== WebSocket.OPEN) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return { deviceId: deviceId, socket: socket };
|
||||
}).filter(Boolean)
|
||||
: [];
|
||||
if (targets.length) {
|
||||
return targets;
|
||||
}
|
||||
|
||||
return Array.from(new Set(deviceIds.map(function (value) {
|
||||
return normalizeDeviceId(value);
|
||||
}).filter(Boolean))).map(function (deviceId) {
|
||||
const socket = playerSockets.get(deviceId);
|
||||
if (!socket || socket.readyState !== WebSocket.OPEN) {
|
||||
return null;
|
||||
}
|
||||
const fallbackTargets = Array.from(playerSockets.values()).filter(function (socket) {
|
||||
return socket && socket.readyState === WebSocket.OPEN;
|
||||
}).map(function (socket) {
|
||||
return {
|
||||
deviceId: String(socket.playerDeviceId || '').trim(),
|
||||
socket: socket
|
||||
};
|
||||
}).filter(function (target) {
|
||||
return Boolean(target.deviceId);
|
||||
});
|
||||
|
||||
return { deviceId: deviceId, socket: socket };
|
||||
}).filter(Boolean);
|
||||
return fallbackTargets.length === 1 ? fallbackTargets : [];
|
||||
}
|
||||
|
||||
function resolveWebBaseUrl(req) {
|
||||
const configuredWebBaseUrl = String(process.env.WEB_BASE_URL || '').trim().replace(/\/$/, '');
|
||||
const configuredWebBaseUrl = String(process.env.WEB_INTERNAL_URL || '').trim().replace(/\/$/, '');
|
||||
if (configuredWebBaseUrl) {
|
||||
return configuredWebBaseUrl;
|
||||
}
|
||||
@@ -118,24 +181,130 @@ async function start() {
|
||||
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 pendingPlayerCommands = new Map();
|
||||
|
||||
function normalizeProxyBaseUrl(value) {
|
||||
const normalized = String(value || '').trim().replace(/\/$/, '');
|
||||
if (!normalized) {
|
||||
return '';
|
||||
function getScreenSnapshotSourceBucket(slug) {
|
||||
const key = String(slug || '').trim();
|
||||
if (!key) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const url = new URL(normalized);
|
||||
if (url.hostname === 'localhost' || url.hostname === '127.0.0.1' || url.hostname === '::1') {
|
||||
url.hostname = 'host.docker.internal';
|
||||
}
|
||||
return url.toString().replace(/\/$/, '');
|
||||
} catch (_error) {
|
||||
return normalized;
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
async function fetchPlayerSnapshotRegistrations() {
|
||||
@@ -641,6 +810,16 @@ async function start() {
|
||||
|
||||
socket.playerDeviceId = deviceId;
|
||||
|
||||
if (messageType === 'snapshot') {
|
||||
const slug = String(payload.slug || '').trim();
|
||||
if (!slug) {
|
||||
return;
|
||||
}
|
||||
|
||||
setScreenSnapshotSource(slug, deviceId, Array.isArray(payload.connections) ? payload.connections : []);
|
||||
return;
|
||||
}
|
||||
|
||||
if (messageType === 'register') {
|
||||
const player = await upsertPlayerRegistration(pool, {
|
||||
deviceId: deviceId,
|
||||
@@ -719,12 +898,14 @@ async function start() {
|
||||
});
|
||||
|
||||
socket.on('close', function () {
|
||||
clearPlayerSnapshotSources(socket.playerDeviceId);
|
||||
if (removeConnectedPlayerSocket(socket)) {
|
||||
logPlayerDisconnect(socket);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('error', function () {
|
||||
clearPlayerSnapshotSources(socket.playerDeviceId);
|
||||
if (removeConnectedPlayerSocket(socket)) {
|
||||
logPlayerDisconnect(socket);
|
||||
}
|
||||
@@ -734,33 +915,18 @@ async function start() {
|
||||
screenSnapshotsWs.on('connection', function (socket, request, slug) {
|
||||
const normalizedSlug = String(slug || '').trim();
|
||||
const upstreamSockets = new Map();
|
||||
const upstreamSnapshots = new Map();
|
||||
const upstreamDeviceIds = new Set();
|
||||
let refreshTimer = null;
|
||||
let closed = false;
|
||||
|
||||
function sendMergedSnapshot() {
|
||||
if (!normalizedSlug || socket.readyState !== WebSocket.OPEN) {
|
||||
return;
|
||||
}
|
||||
|
||||
const connections = [];
|
||||
upstreamSnapshots.forEach(function (payload) {
|
||||
if (payload && Array.isArray(payload.connections)) {
|
||||
connections.push.apply(connections, payload.connections);
|
||||
}
|
||||
});
|
||||
|
||||
storeScreenSnapshot(normalizedSlug, connections, Array.from(upstreamDeviceIds));
|
||||
|
||||
socket.send(JSON.stringify({
|
||||
type: 'snapshot',
|
||||
slug: normalizedSlug,
|
||||
connections: connections,
|
||||
sentAt: new Date().toISOString()
|
||||
}));
|
||||
const subscriberBucket = getScreenSnapshotSubscriberBucket(normalizedSlug);
|
||||
if (!subscriberBucket) {
|
||||
socket.close();
|
||||
return;
|
||||
}
|
||||
|
||||
subscriberBucket.add(socket);
|
||||
|
||||
function closeUpstreamSockets() {
|
||||
upstreamSockets.forEach(function (upstreamSocket) {
|
||||
try {
|
||||
@@ -769,7 +935,6 @@ async function start() {
|
||||
}
|
||||
});
|
||||
upstreamSockets.clear();
|
||||
upstreamSnapshots.clear();
|
||||
}
|
||||
|
||||
async function refreshUpstreams() {
|
||||
@@ -786,12 +951,19 @@ async function start() {
|
||||
|
||||
const seenKeys = new Set();
|
||||
players.forEach(function (player) {
|
||||
const baseUrl = normalizeProxyBaseUrl(player && player.public_base_url);
|
||||
const sourceKey = String(player && player.identifier || player && player.id || '').trim();
|
||||
const connectedSocket = sourceKey ? playerSockets.get(sourceKey) : null;
|
||||
if (connectedSocket && connectedSocket.readyState === WebSocket.OPEN) {
|
||||
seenKeys.add(sourceKey);
|
||||
upstreamDeviceIds.add(sourceKey);
|
||||
return;
|
||||
}
|
||||
|
||||
const baseUrl = resolveSnapshotUpstreamBaseUrl(player);
|
||||
if (!baseUrl) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sourceKey = String(player && player.identifier || player && player.id || baseUrl);
|
||||
seenKeys.add(sourceKey);
|
||||
upstreamDeviceIds.add(sourceKey);
|
||||
if (upstreamSockets.has(sourceKey)) {
|
||||
@@ -814,20 +986,15 @@ async function start() {
|
||||
if (!payload || payload.type !== 'snapshot' || String(payload.slug || '').trim() !== normalizedSlug) {
|
||||
return;
|
||||
}
|
||||
upstreamSnapshots.set(sourceKey, {
|
||||
slug: normalizedSlug,
|
||||
connections: Array.isArray(payload.connections) ? payload.connections : []
|
||||
});
|
||||
sendMergedSnapshot();
|
||||
setScreenSnapshotSource(normalizedSlug, sourceKey, Array.isArray(payload.connections) ? payload.connections : []);
|
||||
} catch (_error) {
|
||||
}
|
||||
};
|
||||
|
||||
upstreamSocket.onclose = function () {
|
||||
upstreamSockets.delete(sourceKey);
|
||||
upstreamSnapshots.delete(sourceKey);
|
||||
if (!closed) {
|
||||
sendMergedSnapshot();
|
||||
if (!closed && !(playerSockets.get(sourceKey) && playerSockets.get(sourceKey).readyState === WebSocket.OPEN)) {
|
||||
clearScreenSnapshotSource(normalizedSlug, sourceKey);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -852,7 +1019,7 @@ async function start() {
|
||||
}
|
||||
});
|
||||
|
||||
sendMergedSnapshot();
|
||||
broadcastScreenSnapshot(normalizedSlug);
|
||||
}
|
||||
|
||||
refreshUpstreams();
|
||||
@@ -863,6 +1030,10 @@ async function start() {
|
||||
|
||||
socket.on('close', function () {
|
||||
closed = true;
|
||||
subscriberBucket.delete(socket);
|
||||
if (!subscriberBucket.size) {
|
||||
screenSnapshotSubscribersBySlug.delete(normalizedSlug);
|
||||
}
|
||||
if (refreshTimer) {
|
||||
clearInterval(refreshTimer);
|
||||
refreshTimer = null;
|
||||
@@ -872,6 +1043,10 @@ async function start() {
|
||||
|
||||
socket.on('error', function () {
|
||||
closed = true;
|
||||
subscriberBucket.delete(socket);
|
||||
if (!subscriberBucket.size) {
|
||||
screenSnapshotSubscribersBySlug.delete(normalizedSlug);
|
||||
}
|
||||
if (refreshTimer) {
|
||||
clearInterval(refreshTimer);
|
||||
refreshTimer = null;
|
||||
@@ -915,7 +1090,12 @@ async function start() {
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { start: start, resolveWebBaseUrl: resolveWebBaseUrl, resolveScreenCommandTargets: resolveScreenCommandTargets };
|
||||
module.exports = {
|
||||
start: start,
|
||||
resolveWebBaseUrl: resolveWebBaseUrl,
|
||||
resolveScreenCommandTargets: resolveScreenCommandTargets,
|
||||
resolveSnapshotUpstreamBaseUrl: resolveSnapshotUpstreamBaseUrl
|
||||
};
|
||||
|
||||
if (require.main === module) {
|
||||
start().catch(function (error) {
|
||||
|
||||
Reference in New Issue
Block a user