Release 2.6.7

This commit is contained in:
2026-08-08 13:02:31 +01:00
parent b20beb250b
commit 7e46fffc34
41 changed files with 2489 additions and 1290 deletions
+238 -58
View File
@@ -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) {