@livedesk/hub 0.1.13 → 0.1.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/remote-hub.js +284 -88
- package/src/server.js +173 -28
- package/src/transport/relay-hub-control.js +1004 -0
- package/src/transport/udp-rendezvous.js +2 -0
package/src/server.js
CHANGED
|
@@ -1542,11 +1542,11 @@ function sendJson(ws, payload) {
|
|
|
1542
1542
|
return safeWebSocketSend(ws, JSON.stringify(payload));
|
|
1543
1543
|
}
|
|
1544
1544
|
|
|
1545
|
-
function forgetWebSocketClient(ws) {
|
|
1546
|
-
unregisterFrameClient(ws);
|
|
1547
|
-
frameClients.delete(ws);
|
|
1548
|
-
|
|
1549
|
-
inputClients.delete(ws);
|
|
1545
|
+
function forgetWebSocketClient(ws) {
|
|
1546
|
+
unregisterFrameClient(ws);
|
|
1547
|
+
frameClients.delete(ws);
|
|
1548
|
+
releaseAudioClient(ws, 'websocket-forgotten');
|
|
1549
|
+
inputClients.delete(ws);
|
|
1550
1550
|
try {
|
|
1551
1551
|
ws.terminate?.();
|
|
1552
1552
|
} catch {
|
|
@@ -1647,14 +1647,34 @@ function scheduleFrameStreamStop(deviceId, streamId, streamPurpose) {
|
|
|
1647
1647
|
if (hasOtherFrameStreamOwner(null, deviceId, streamId, streamPurpose)) {
|
|
1648
1648
|
return;
|
|
1649
1649
|
}
|
|
1650
|
-
const result = remoteHub.stopLiveStream(deviceId, {
|
|
1651
|
-
streamId,
|
|
1652
|
-
streamPurpose,
|
|
1653
|
-
reason: 'frame-client-closed'
|
|
1654
|
-
});
|
|
1655
|
-
if (result?.
|
|
1656
|
-
|
|
1657
|
-
|
|
1650
|
+
const result = remoteHub.stopLiveStream(deviceId, {
|
|
1651
|
+
streamId,
|
|
1652
|
+
streamPurpose,
|
|
1653
|
+
reason: 'frame-client-closed'
|
|
1654
|
+
});
|
|
1655
|
+
if (result?.stopPromise) {
|
|
1656
|
+
void result.stopPromise.then(confirmation => {
|
|
1657
|
+
if (confirmation?.captureStopConfirmed === true) {
|
|
1658
|
+
console.log(`[LiveDesk Hub] confirmed ${streamPurpose} capture stopped after frame client closed device=${deviceId} stream=${streamId}`);
|
|
1659
|
+
return;
|
|
1660
|
+
}
|
|
1661
|
+
if (!hasOtherFrameStreamOwner(null, deviceId, streamId, streamPurpose)) {
|
|
1662
|
+
console.warn(
|
|
1663
|
+
`[LiveDesk Hub] ${streamPurpose} capture stop was not confirmed device=${deviceId} `
|
|
1664
|
+
+ `stream=${streamId}: ${confirmation?.error || 'capture-stop-unconfirmed'}. `
|
|
1665
|
+
+ 'Disconnecting the Agent so its final capture drain runs before reconnect.'
|
|
1666
|
+
);
|
|
1667
|
+
remoteHub.disconnectDevice(deviceId, 'frame-capture-stop-unconfirmed');
|
|
1668
|
+
}
|
|
1669
|
+
}).catch(error => {
|
|
1670
|
+
if (!hasOtherFrameStreamOwner(null, deviceId, streamId, streamPurpose)) {
|
|
1671
|
+
console.warn(`[LiveDesk Hub] ${streamPurpose} capture stop confirmation failed device=${deviceId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1672
|
+
remoteHub.disconnectDevice(deviceId, 'frame-capture-stop-confirmation-failed');
|
|
1673
|
+
}
|
|
1674
|
+
});
|
|
1675
|
+
} else if (result?.ok && !result.alreadyStopped) {
|
|
1676
|
+
console.log(`[LiveDesk Hub] queued ${streamPurpose} capture stop after frame client closed device=${deviceId} stream=${streamId}`);
|
|
1677
|
+
}
|
|
1658
1678
|
}, frameStreamStopGraceMs);
|
|
1659
1679
|
pendingFrameStreamStops.set(key, { timer });
|
|
1660
1680
|
}
|
|
@@ -2003,10 +2023,19 @@ function restartFrameSubscriptionLive(ws, payload = {}) {
|
|
|
2003
2023
|
}
|
|
2004
2024
|
}
|
|
2005
2025
|
|
|
2006
|
-
function updateAudioSubscription(ws, payload = {}) {
|
|
2007
|
-
const
|
|
2008
|
-
|
|
2009
|
-
|
|
2026
|
+
function updateAudioSubscription(ws, payload = {}) {
|
|
2027
|
+
const previousDeviceIds = ws.liveDeskAudioDeviceIds instanceof Set
|
|
2028
|
+
? new Set(ws.liveDeskAudioDeviceIds)
|
|
2029
|
+
: null;
|
|
2030
|
+
const deviceIds = normalizeDeviceIds(payload.deviceIds ?? payload.devices ?? payload.deviceId);
|
|
2031
|
+
ws.liveDeskAudioDeviceIds = new Set(deviceIds);
|
|
2032
|
+
if (previousDeviceIds) {
|
|
2033
|
+
const releasedDeviceIds = previousDeviceIds.size === 0
|
|
2034
|
+
? activeRemoteAudioDeviceIds().filter(deviceId => !ws.liveDeskAudioDeviceIds.has(deviceId))
|
|
2035
|
+
: [...previousDeviceIds].filter(deviceId => !ws.liveDeskAudioDeviceIds.has(deviceId));
|
|
2036
|
+
stopUnsubscribedAudioDevices(releasedDeviceIds, 'audio-subscription-changed');
|
|
2037
|
+
}
|
|
2038
|
+
sendJson(ws, {
|
|
2010
2039
|
type: 'RemoteAudioSubscription',
|
|
2011
2040
|
timestamp: new Date().toISOString(),
|
|
2012
2041
|
deviceIds
|
|
@@ -2469,6 +2498,90 @@ function startMode4AtlasInput(ws, deviceId, reason = 'atlas-configure', { forceR
|
|
|
2469
2498
|
restartReason: reason
|
|
2470
2499
|
});
|
|
2471
2500
|
}
|
|
2501
|
+
|
|
2502
|
+
function activeRemoteAudioDeviceIds() {
|
|
2503
|
+
return remoteHub.listDevices({ includeDataUrl: false })
|
|
2504
|
+
.filter(device => device?.activeAudioStream?.active === true)
|
|
2505
|
+
.map(device => String(device.deviceId || '').trim())
|
|
2506
|
+
.filter(Boolean);
|
|
2507
|
+
}
|
|
2508
|
+
|
|
2509
|
+
function hasRemoteAudioSubscriber(deviceId) {
|
|
2510
|
+
for (const client of audioClients) {
|
|
2511
|
+
if (client.readyState !== 1) {
|
|
2512
|
+
continue;
|
|
2513
|
+
}
|
|
2514
|
+
const subscribedDeviceIds = client.liveDeskAudioDeviceIds;
|
|
2515
|
+
if (!(subscribedDeviceIds instanceof Set)
|
|
2516
|
+
|| subscribedDeviceIds.size === 0
|
|
2517
|
+
|| subscribedDeviceIds.has(deviceId)) {
|
|
2518
|
+
return true;
|
|
2519
|
+
}
|
|
2520
|
+
}
|
|
2521
|
+
return false;
|
|
2522
|
+
}
|
|
2523
|
+
|
|
2524
|
+
function stopUnsubscribedAudioDevices(deviceIds, reason) {
|
|
2525
|
+
const activeDevices = new Map(
|
|
2526
|
+
remoteHub.listDevices({ includeDataUrl: false })
|
|
2527
|
+
.filter(device => device?.activeAudioStream?.active === true)
|
|
2528
|
+
.map(device => [String(device.deviceId || '').trim(), device])
|
|
2529
|
+
);
|
|
2530
|
+
for (const deviceId of new Set(normalizeDeviceIds(deviceIds))) {
|
|
2531
|
+
if (!deviceId || hasRemoteAudioSubscriber(deviceId)) {
|
|
2532
|
+
continue;
|
|
2533
|
+
}
|
|
2534
|
+
const device = activeDevices.get(deviceId);
|
|
2535
|
+
if (!device) {
|
|
2536
|
+
continue;
|
|
2537
|
+
}
|
|
2538
|
+
const result = remoteHub.stopAudioStream(deviceId, {
|
|
2539
|
+
streamId: String(device.activeAudioStream?.streamId || ''),
|
|
2540
|
+
reason
|
|
2541
|
+
});
|
|
2542
|
+
if (result?.stopPromise) {
|
|
2543
|
+
void result.stopPromise.then(confirmation => {
|
|
2544
|
+
if (confirmation?.captureStopConfirmed === true) {
|
|
2545
|
+
return;
|
|
2546
|
+
}
|
|
2547
|
+
if (!hasRemoteAudioSubscriber(deviceId)) {
|
|
2548
|
+
console.warn(
|
|
2549
|
+
`[LiveDesk Hub] audio capture stop was not confirmed device=${deviceId} reason=${reason}: `
|
|
2550
|
+
+ `${confirmation?.error || 'audio-stop-unconfirmed'}. Disconnecting the Agent for a final drain.`
|
|
2551
|
+
);
|
|
2552
|
+
remoteHub.disconnectDevice(deviceId, 'audio-capture-stop-unconfirmed');
|
|
2553
|
+
}
|
|
2554
|
+
}).catch(error => {
|
|
2555
|
+
if (!hasRemoteAudioSubscriber(deviceId)) {
|
|
2556
|
+
console.warn(`[LiveDesk Hub] audio capture stop confirmation failed device=${deviceId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
2557
|
+
remoteHub.disconnectDevice(deviceId, 'audio-capture-stop-confirmation-failed');
|
|
2558
|
+
}
|
|
2559
|
+
});
|
|
2560
|
+
}
|
|
2561
|
+
if (result?.ok === false
|
|
2562
|
+
&& !['device-not-found', 'device-not-connected', 'device-audio-unavailable'].includes(String(result.error || ''))) {
|
|
2563
|
+
console.warn(`[LiveDesk Hub] Could not stop unused remote audio capture device=${deviceId} reason=${reason}: ${result.error || 'unknown-error'}`);
|
|
2564
|
+
}
|
|
2565
|
+
}
|
|
2566
|
+
}
|
|
2567
|
+
|
|
2568
|
+
function releaseAudioClient(ws, reason = 'audio-subscriber-closed') {
|
|
2569
|
+
if (!ws
|
|
2570
|
+
|| (!audioClients.has(ws) && !ws.liveDeskAudioClientId)
|
|
2571
|
+
|| ws.liveDeskAudioCleanupComplete === true) {
|
|
2572
|
+
return;
|
|
2573
|
+
}
|
|
2574
|
+
ws.liveDeskAudioCleanupComplete = true;
|
|
2575
|
+
const subscribedDeviceIds = ws.liveDeskAudioDeviceIds instanceof Set
|
|
2576
|
+
? ws.liveDeskAudioDeviceIds
|
|
2577
|
+
: new Set();
|
|
2578
|
+
const releasedDeviceIds = subscribedDeviceIds.size === 0
|
|
2579
|
+
? activeRemoteAudioDeviceIds()
|
|
2580
|
+
: [...subscribedDeviceIds];
|
|
2581
|
+
audioClients.delete(ws);
|
|
2582
|
+
ws.liveDeskAudioDeviceIds = new Set();
|
|
2583
|
+
stopUnsubscribedAudioDevices(releasedDeviceIds, reason);
|
|
2584
|
+
}
|
|
2472
2585
|
|
|
2473
2586
|
function hasOtherAtlasInputOwner(ws, deviceId) {
|
|
2474
2587
|
for (const candidate of atlasClients.keys()) {
|
|
@@ -4087,9 +4200,32 @@ app.post('/api/remote/devices/:deviceId/live/start', requireHubFeatureAccess, (r
|
|
|
4087
4200
|
res.json(remoteHub.startLiveStream(req.params.deviceId, req.body || {}));
|
|
4088
4201
|
});
|
|
4089
4202
|
|
|
4090
|
-
|
|
4203
|
+
async function resolveConfirmedMediaStop(result, kind) {
|
|
4204
|
+
if (!result?.stopPromise) {
|
|
4205
|
+
return result;
|
|
4206
|
+
}
|
|
4207
|
+
const confirmation = await result.stopPromise;
|
|
4208
|
+
const confirmed = confirmation?.captureStopConfirmed === true;
|
|
4209
|
+
return {
|
|
4210
|
+
...result,
|
|
4211
|
+
ok: confirmed,
|
|
4212
|
+
captureStopConfirmed: confirmed,
|
|
4213
|
+
stopPending: false,
|
|
4214
|
+
stopError: confirmed ? '' : String(confirmation?.error || `${kind}-capture-stop-unconfirmed`),
|
|
4215
|
+
error: confirmed ? undefined : `${kind.toUpperCase()}_CAPTURE_STOP_UNCONFIRMED`
|
|
4216
|
+
};
|
|
4217
|
+
}
|
|
4218
|
+
|
|
4219
|
+
app.post('/api/remote/devices/:deviceId/live/stop', async (req, res, next) => {
|
|
4091
4220
|
noStore(res);
|
|
4092
|
-
|
|
4221
|
+
try {
|
|
4222
|
+
res.json(await resolveConfirmedMediaStop(
|
|
4223
|
+
remoteHub.stopLiveStream(req.params.deviceId, req.body || {}),
|
|
4224
|
+
'video'
|
|
4225
|
+
));
|
|
4226
|
+
} catch (error) {
|
|
4227
|
+
next(error);
|
|
4228
|
+
}
|
|
4093
4229
|
});
|
|
4094
4230
|
|
|
4095
4231
|
app.post('/api/remote/devices/:deviceId/live/pause', async (req, res, next) => {
|
|
@@ -4111,10 +4247,17 @@ app.post('/api/remote/devices/:deviceId/audio/start', requireHubFeatureAccess, (
|
|
|
4111
4247
|
res.json(remoteHub.startAudioStream(req.params.deviceId, req.body || {}));
|
|
4112
4248
|
});
|
|
4113
4249
|
|
|
4114
|
-
app.post('/api/remote/devices/:deviceId/audio/stop', (req, res) => {
|
|
4115
|
-
noStore(res);
|
|
4116
|
-
|
|
4117
|
-
|
|
4250
|
+
app.post('/api/remote/devices/:deviceId/audio/stop', async (req, res, next) => {
|
|
4251
|
+
noStore(res);
|
|
4252
|
+
try {
|
|
4253
|
+
res.json(await resolveConfirmedMediaStop(
|
|
4254
|
+
remoteHub.stopAudioStream(req.params.deviceId, req.body || {}),
|
|
4255
|
+
'audio'
|
|
4256
|
+
));
|
|
4257
|
+
} catch (error) {
|
|
4258
|
+
next(error);
|
|
4259
|
+
}
|
|
4260
|
+
});
|
|
4118
4261
|
|
|
4119
4262
|
if (existsSync(webIndexPath)) {
|
|
4120
4263
|
app.use(express.static(webDistPath, {
|
|
@@ -4241,9 +4384,10 @@ atlasWss.on('connection', ws => {
|
|
|
4241
4384
|
});
|
|
4242
4385
|
});
|
|
4243
4386
|
|
|
4244
|
-
audioWss.on('connection', (ws, req) => {
|
|
4245
|
-
audioClients.add(ws);
|
|
4246
|
-
ws.liveDeskAudioClientId = `raws-${++audioClientSeq}`;
|
|
4387
|
+
audioWss.on('connection', (ws, req) => {
|
|
4388
|
+
audioClients.add(ws);
|
|
4389
|
+
ws.liveDeskAudioClientId = `raws-${++audioClientSeq}`;
|
|
4390
|
+
ws.liveDeskAudioCleanupComplete = false;
|
|
4247
4391
|
try {
|
|
4248
4392
|
const parsed = new URL(req.url || '/', `http://${req.headers.host || '127.0.0.1'}`);
|
|
4249
4393
|
updateAudioSubscription(ws, { deviceIds: parsed.searchParams.get('devices') || '' });
|
|
@@ -4260,8 +4404,9 @@ audioWss.on('connection', (ws, req) => {
|
|
|
4260
4404
|
sendJson(ws, { type: 'RemoteAudioSubscriptionError', error: 'invalid-message' });
|
|
4261
4405
|
}
|
|
4262
4406
|
});
|
|
4263
|
-
|
|
4264
|
-
ws.on('
|
|
4407
|
+
const cleanup = () => releaseAudioClient(ws);
|
|
4408
|
+
ws.on('close', cleanup);
|
|
4409
|
+
ws.on('error', cleanup);
|
|
4265
4410
|
sendJson(ws, {
|
|
4266
4411
|
type: 'RemoteAudioSocketReady',
|
|
4267
4412
|
protocol: 'livedesk.remote.audio.binary.v1',
|