livedesk 0.1.224 → 0.1.226
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/hub/src/mode4-atlas.js +7 -2
- package/hub/src/remote-hub.js +96 -12
- package/hub/src/server.js +151 -50
- package/package.json +2 -2
- package/web/dist/assets/index-CkNEuxQr.js +15 -0
- package/web/dist/index.html +1 -1
- package/web/dist/assets/index-DnQqFU1G.js +0 -15
package/hub/src/mode4-atlas.js
CHANGED
|
@@ -10,6 +10,8 @@ export class Mode4AtlasSession {
|
|
|
10
10
|
this.onStatus = typeof options.onStatus === 'function' ? options.onStatus : () => {};
|
|
11
11
|
this.deviceIds = [];
|
|
12
12
|
this.deviceSet = new Set();
|
|
13
|
+
this.inputDeviceIds = [];
|
|
14
|
+
this.inputDeviceSet = new Set();
|
|
13
15
|
this.config = null;
|
|
14
16
|
this.closed = false;
|
|
15
17
|
this.worker = null;
|
|
@@ -65,7 +67,10 @@ export class Mode4AtlasSession {
|
|
|
65
67
|
this.deviceIds = [...new Set((Array.isArray(options.deviceIds) ? options.deviceIds : [])
|
|
66
68
|
.map(value => String(value || '').trim()).filter(Boolean))].slice(0, 100);
|
|
67
69
|
this.deviceSet = new Set(this.deviceIds);
|
|
68
|
-
this.
|
|
70
|
+
this.inputDeviceIds = [...new Set((Array.isArray(options.inputDeviceIds) ? options.inputDeviceIds : this.deviceIds)
|
|
71
|
+
.map(value => String(value || '').trim()).filter(value => this.deviceSet.has(value)))].slice(0, 100);
|
|
72
|
+
this.inputDeviceSet = new Set(this.inputDeviceIds);
|
|
73
|
+
this.pendingFrames = new Map([...this.pendingFrames].filter(([deviceId]) => this.inputDeviceSet.has(deviceId)));
|
|
69
74
|
this.config = {
|
|
70
75
|
deviceIds: this.deviceIds,
|
|
71
76
|
width: Number(options.width || 1920),
|
|
@@ -102,7 +107,7 @@ export class Mode4AtlasSession {
|
|
|
102
107
|
const frame = frameEvent?.frame || {};
|
|
103
108
|
const deviceId = String(frameEvent?.deviceId || frame.deviceId || '').trim();
|
|
104
109
|
const mode = String(frame.frameMode || frame.mode || '').toLowerCase();
|
|
105
|
-
if (!this.
|
|
110
|
+
if (!this.inputDeviceSet.has(deviceId) || mode !== 'mode2-lzo' || !Buffer.isBuffer(frameEvent?.payload)) return;
|
|
106
111
|
this.pendingFrames.set(deviceId, {
|
|
107
112
|
type: 'frame',
|
|
108
113
|
deviceId,
|
package/hub/src/remote-hub.js
CHANGED
|
@@ -672,6 +672,7 @@ function createDeviceCounters(overrides = {}) {
|
|
|
672
672
|
audioStreamsStopped: 0,
|
|
673
673
|
audioFramesReceived: 0,
|
|
674
674
|
audioFramesDropped: 0,
|
|
675
|
+
audioStatusReceived: 0,
|
|
675
676
|
tasksQueued: 0,
|
|
676
677
|
taskResultsReceived: 0,
|
|
677
678
|
taskResultsFailed: 0,
|
|
@@ -958,6 +959,7 @@ function serializeDevice(device, options = {}) {
|
|
|
958
959
|
byteLength: device.latestAudioFrame.byteLength
|
|
959
960
|
}
|
|
960
961
|
: null,
|
|
962
|
+
latestAudioStatus: device.latestAudioStatus ? { ...device.latestAudioStatus } : null,
|
|
961
963
|
latestTask: device.latestTask ? { ...device.latestTask } : null,
|
|
962
964
|
synthetic: device.synthetic === true,
|
|
963
965
|
recentTasks: Array.isArray(device.recentTasks)
|
|
@@ -2456,6 +2458,7 @@ export function createRemoteHub(options = {}) {
|
|
|
2456
2458
|
activeLiveStream: null,
|
|
2457
2459
|
activeAudioStream: null,
|
|
2458
2460
|
latestAudioFrame: null,
|
|
2461
|
+
latestAudioStatus: null,
|
|
2459
2462
|
latestTask,
|
|
2460
2463
|
recentTasks: latestTask ? [latestTask] : [],
|
|
2461
2464
|
pendingTaskCommands: new Map(),
|
|
@@ -2621,6 +2624,7 @@ export function createRemoteHub(options = {}) {
|
|
|
2621
2624
|
activeLiveStream: null,
|
|
2622
2625
|
activeAudioStream: null,
|
|
2623
2626
|
latestAudioFrame: null,
|
|
2627
|
+
latestAudioStatus: null,
|
|
2624
2628
|
latestTask: null,
|
|
2625
2629
|
recentTasks: [],
|
|
2626
2630
|
pendingTaskCommands: new Map(),
|
|
@@ -3470,6 +3474,12 @@ export function createRemoteHub(options = {}) {
|
|
|
3470
3474
|
transferProtocolVersion: transfer.transferProtocolVersion,
|
|
3471
3475
|
handshake: transfer.handshake,
|
|
3472
3476
|
fps: Number.isFinite(Number(message.fps)) ? Number(message.fps) : streamState.fps,
|
|
3477
|
+
requestedFps: Number.isFinite(Number(message.requestedFps)) ? Number(message.requestedFps) : Number(streamState.fps || 0),
|
|
3478
|
+
effectiveFps: Number.isFinite(Number(message.effectiveFps)) ? Number(message.effectiveFps) : Number(message.fps || streamState.fps || 0),
|
|
3479
|
+
captureAverageMs: readFrameStageMs(message, 'captureAverageMs'),
|
|
3480
|
+
captureP95Ms: readFrameStageMs(message, 'captureP95Ms'),
|
|
3481
|
+
slowFrameCount: Number.isFinite(Number(message.slowFrameCount)) ? Number(message.slowFrameCount) : 0,
|
|
3482
|
+
captureHelperRestarts: Number.isFinite(Number(message.captureHelperRestarts)) ? Number(message.captureHelperRestarts) : 0,
|
|
3473
3483
|
capturedAt,
|
|
3474
3484
|
receivedAt: device.lastSeenAt,
|
|
3475
3485
|
byteLength,
|
|
@@ -3590,6 +3600,50 @@ export function createRemoteHub(options = {}) {
|
|
|
3590
3600
|
return true;
|
|
3591
3601
|
}
|
|
3592
3602
|
|
|
3603
|
+
function applyAudioStatus(device, message, framePayload, transport = 'binary') {
|
|
3604
|
+
if (!Buffer.isBuffer(framePayload) || framePayload.length < 1) {
|
|
3605
|
+
device.counters.audioFramesDropped += 1;
|
|
3606
|
+
return false;
|
|
3607
|
+
}
|
|
3608
|
+
|
|
3609
|
+
const streamId = safeString(message.streamId, 128) || device.activeAudioStream?.streamId || 'audio';
|
|
3610
|
+
const status = {
|
|
3611
|
+
streamId,
|
|
3612
|
+
commandId: safeString(message.commandId, 128),
|
|
3613
|
+
captureAlive: message.captureAlive === true,
|
|
3614
|
+
helperAlive: message.helperAlive === true,
|
|
3615
|
+
helperPid: Number(message.helperPid || 0) || 0,
|
|
3616
|
+
captureGeneration: Number(message.captureGeneration || 0) || 0,
|
|
3617
|
+
lastPcmAtEpochMs: Number(message.lastPcmAtEpochMs || 0) || 0,
|
|
3618
|
+
silentForMs: Math.max(0, Number(message.silentForMs || 0) || 0),
|
|
3619
|
+
restartCount: Math.max(0, Number(message.restartCount || 0) || 0),
|
|
3620
|
+
error: safeString(message.error, 500),
|
|
3621
|
+
statusAtEpochMs: Number(message.statusAtEpochMs || 0) || Date.now(),
|
|
3622
|
+
receivedAt: new Date().toISOString(),
|
|
3623
|
+
transport
|
|
3624
|
+
};
|
|
3625
|
+
device.latestAudioStatus = status;
|
|
3626
|
+
if (device.activeAudioStream && device.activeAudioStream.streamId === streamId) {
|
|
3627
|
+
device.activeAudioStream.lastStatusAt = status.receivedAt;
|
|
3628
|
+
device.activeAudioStream.captureAlive = status.captureAlive;
|
|
3629
|
+
device.activeAudioStream.helperAlive = status.helperAlive;
|
|
3630
|
+
device.activeAudioStream.captureGeneration = status.captureGeneration;
|
|
3631
|
+
device.activeAudioStream.lastPcmAtEpochMs = status.lastPcmAtEpochMs;
|
|
3632
|
+
device.activeAudioStream.silentForMs = status.silentForMs;
|
|
3633
|
+
device.activeAudioStream.restartCount = status.restartCount;
|
|
3634
|
+
device.activeAudioStream.audioError = status.error;
|
|
3635
|
+
}
|
|
3636
|
+
device.counters.audioStatusReceived += 1;
|
|
3637
|
+
emitAudio({
|
|
3638
|
+
deviceId: device.deviceId,
|
|
3639
|
+
audioStatus: true,
|
|
3640
|
+
frame: status,
|
|
3641
|
+
payload: framePayload,
|
|
3642
|
+
byteLength: framePayload.length
|
|
3643
|
+
});
|
|
3644
|
+
return true;
|
|
3645
|
+
}
|
|
3646
|
+
|
|
3593
3647
|
function handleAgentBinaryFrame(socket, state, header, framePayload) {
|
|
3594
3648
|
if (!state.authenticated || !state.device) {
|
|
3595
3649
|
writeJsonLine(socket, { type: 'error', error: 'hello-required' });
|
|
@@ -3632,6 +3686,11 @@ export function createRemoteHub(options = {}) {
|
|
|
3632
3686
|
return;
|
|
3633
3687
|
}
|
|
3634
3688
|
|
|
3689
|
+
if (frameKind === 'audio-status' || header.type === 'audio.status.binary') {
|
|
3690
|
+
applyAudioStatus(device, header, framePayload, 'binary');
|
|
3691
|
+
return;
|
|
3692
|
+
}
|
|
3693
|
+
|
|
3635
3694
|
emitRemoteEvent('RemoteAgentMessageIgnored', device, {
|
|
3636
3695
|
messageType: safeString(header.type, 80),
|
|
3637
3696
|
frameKind
|
|
@@ -3650,7 +3709,12 @@ export function createRemoteHub(options = {}) {
|
|
|
3650
3709
|
&& header.isKeyFrame !== true
|
|
3651
3710
|
&& chunkType !== 'key';
|
|
3652
3711
|
});
|
|
3653
|
-
const
|
|
3712
|
+
const nonStatusIndex = state.binaryQueue.findIndex(item => {
|
|
3713
|
+
const header = item?.header || {};
|
|
3714
|
+
const frameKind = safeString(header.frameKind || header.kind || header.frameType || '', 40).toLowerCase();
|
|
3715
|
+
return frameKind !== 'audio-status' && header.type !== 'audio.status.binary';
|
|
3716
|
+
});
|
|
3717
|
+
const index = deltaIndex >= 0 ? deltaIndex : nonStatusIndex >= 0 ? nonStatusIndex : 0;
|
|
3654
3718
|
state.binaryQueue.splice(index, 1);
|
|
3655
3719
|
state.binaryQueueDrops = Number(state.binaryQueueDrops || 0) + 1;
|
|
3656
3720
|
}
|
|
@@ -4751,16 +4815,27 @@ export function createRemoteHub(options = {}) {
|
|
|
4751
4815
|
return `${role}-${hash}`;
|
|
4752
4816
|
}
|
|
4753
4817
|
|
|
4818
|
+
function normalizeLivePlatform(value) {
|
|
4819
|
+
const normalized = safeString(value, 32).toLowerCase();
|
|
4820
|
+
if (['macos', 'darwin', 'mac', 'macosx'].includes(normalized)) return 'macos';
|
|
4821
|
+
if (['windows', 'win32', 'win32nt'].includes(normalized)) return 'windows';
|
|
4822
|
+
if (['linux', 'unix'].includes(normalized)) return 'linux';
|
|
4823
|
+
return normalized || 'unknown';
|
|
4824
|
+
}
|
|
4825
|
+
|
|
4754
4826
|
function normalizeLiveStreamStartOptions(options = {}) {
|
|
4755
4827
|
const transfer = buildRemoteFrameTransferDescriptor(options.mode || options.frameMode || DEFAULT_REMOTE_FRAME_MODE);
|
|
4756
4828
|
const streamPurpose = safeString(options.streamPurpose || options.purpose || 'wall', 24) || 'wall';
|
|
4757
|
-
const
|
|
4758
|
-
const
|
|
4759
|
-
const
|
|
4760
|
-
const
|
|
4829
|
+
const platform = normalizeLivePlatform(options.platform);
|
|
4830
|
+
const macWallProfile = platform === 'macos' && streamPurpose !== 'control';
|
|
4831
|
+
const maxFps = streamPurpose === 'control' ? 60 : macWallProfile ? 8 : 30;
|
|
4832
|
+
const defaultFps = macWallProfile && options.fps === undefined ? 4 : 8;
|
|
4833
|
+
const maxWidth = clampNumber(options.maxWidth, 320, macWallProfile ? 432 : 3840, macWallProfile ? 432 : 640);
|
|
4834
|
+
const maxHeight = clampNumber(options.maxHeight, 180, macWallProfile ? 243 : 2160, macWallProfile ? 243 : 360);
|
|
4835
|
+
const fps = clampNumber(options.fps, 1, maxFps, defaultFps);
|
|
4761
4836
|
const quality = clampNumber(options.quality, 20, 95, 45);
|
|
4762
4837
|
const monitorIndex = normalizeMonitorIndex(options.monitorIndex ?? options.screenIndex ?? options.displayIndex);
|
|
4763
|
-
return { fps, maxWidth, maxHeight, quality, monitorIndex, transfer, streamPurpose };
|
|
4838
|
+
return { fps, maxWidth, maxHeight, quality, monitorIndex, transfer, streamPurpose, platform, macWallProfile };
|
|
4764
4839
|
}
|
|
4765
4840
|
|
|
4766
4841
|
function liveStreamStartStillPending(activeLiveStream) {
|
|
@@ -4821,7 +4896,7 @@ export function createRemoteHub(options = {}) {
|
|
|
4821
4896
|
}
|
|
4822
4897
|
|
|
4823
4898
|
const now = new Date().toISOString();
|
|
4824
|
-
const normalized = normalizeLiveStreamStartOptions({ fps: 2, ...options });
|
|
4899
|
+
const normalized = normalizeLiveStreamStartOptions({ fps: 2, ...options, platform: device.platform });
|
|
4825
4900
|
const { fps, maxWidth, maxHeight, quality, monitorIndex, transfer, streamPurpose } = normalized;
|
|
4826
4901
|
const streamId = safeString(options.streamId, 128) || makeStableLiveStreamId(deviceId, streamPurpose);
|
|
4827
4902
|
const activeLiveStream = getDeviceLiveStream(device, streamId);
|
|
@@ -4898,7 +4973,7 @@ export function createRemoteHub(options = {}) {
|
|
|
4898
4973
|
}
|
|
4899
4974
|
|
|
4900
4975
|
const now = new Date().toISOString();
|
|
4901
|
-
const normalized = normalizeLiveStreamStartOptions(options);
|
|
4976
|
+
const normalized = normalizeLiveStreamStartOptions({ ...options, platform: device.platform });
|
|
4902
4977
|
const { fps, maxWidth, maxHeight, quality, monitorIndex, transfer, streamPurpose } = normalized;
|
|
4903
4978
|
const streamId = safeString(options.streamId, 128) || makeStableLiveStreamId(deviceId, streamPurpose);
|
|
4904
4979
|
const activeLiveStream = getDeviceLiveStream(device, streamId);
|
|
@@ -5041,11 +5116,22 @@ export function createRemoteHub(options = {}) {
|
|
|
5041
5116
|
|
|
5042
5117
|
function stopLiveStream(deviceId, options = {}) {
|
|
5043
5118
|
const device = devices.get(String(deviceId || ''));
|
|
5119
|
+
const streamPurpose = safeString(options.streamPurpose || options.purpose, 24);
|
|
5120
|
+
const requestedStreamId = safeString(options.streamId, 128);
|
|
5121
|
+
const purposeStreamId = streamPurpose ? makeStableLiveStreamId(deviceId, streamPurpose) : '';
|
|
5122
|
+
const streamId = requestedStreamId || purposeStreamId || device?.activeLiveStream?.streamId || '';
|
|
5123
|
+
const streamState = getDeviceLiveStream(device, streamId);
|
|
5124
|
+
if (streamState && streamState.active !== true) {
|
|
5125
|
+
return {
|
|
5126
|
+
ok: true,
|
|
5127
|
+
commandId: safeString(options.commandId, 128),
|
|
5128
|
+
streamId,
|
|
5129
|
+
alreadyStopped: true
|
|
5130
|
+
};
|
|
5131
|
+
}
|
|
5044
5132
|
if (device?.synthetic === true && device.connected) {
|
|
5045
|
-
const streamId = safeString(options.streamId, 128) || device.activeLiveStream?.streamId || '';
|
|
5046
5133
|
const commandId = safeString(options.commandId, 128) || crypto.randomUUID();
|
|
5047
5134
|
device.counters.commandsSent += 1;
|
|
5048
|
-
const streamState = getDeviceLiveStream(device, streamId);
|
|
5049
5135
|
if (streamState) {
|
|
5050
5136
|
streamState.active = false;
|
|
5051
5137
|
streamState.stoppedAt = new Date().toISOString();
|
|
@@ -5064,7 +5150,6 @@ export function createRemoteHub(options = {}) {
|
|
|
5064
5150
|
return { ok: false, error: 'device-not-connected' };
|
|
5065
5151
|
}
|
|
5066
5152
|
|
|
5067
|
-
const streamId = safeString(options.streamId, 128) || device.activeLiveStream?.streamId || '';
|
|
5068
5153
|
const commandId = safeString(options.commandId, 128) || crypto.randomUUID();
|
|
5069
5154
|
const result = sendCommand(deviceId, {
|
|
5070
5155
|
command: 'stream.stop',
|
|
@@ -5079,7 +5164,6 @@ export function createRemoteHub(options = {}) {
|
|
|
5079
5164
|
return result;
|
|
5080
5165
|
}
|
|
5081
5166
|
|
|
5082
|
-
const streamState = getDeviceLiveStream(device, streamId);
|
|
5083
5167
|
if (streamState) {
|
|
5084
5168
|
streamState.active = false;
|
|
5085
5169
|
streamState.stoppedAt = new Date().toISOString();
|
package/hub/src/server.js
CHANGED
|
@@ -101,11 +101,11 @@ function handleRemoteHubEvent(type, event) {
|
|
|
101
101
|
if (!deviceId) {
|
|
102
102
|
return;
|
|
103
103
|
}
|
|
104
|
-
for (const ws of atlasClients.keys()) {
|
|
105
|
-
if (ws.readyState === 1 && ws.
|
|
106
|
-
startMode4AtlasInput(ws, deviceId, 'device-connected');
|
|
107
|
-
}
|
|
108
|
-
}
|
|
104
|
+
for (const ws of atlasClients.keys()) {
|
|
105
|
+
if (ws.readyState === 1 && ws.liveDeskAtlasInputDeviceIds?.has(deviceId)) {
|
|
106
|
+
startMode4AtlasInput(ws, deviceId, 'device-connected');
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
109
|
const clients = new Set([
|
|
110
110
|
...(frameClientsByDeviceId.get(deviceId) || []),
|
|
111
111
|
...frameWildcardClients
|
|
@@ -976,11 +976,12 @@ function safeWebSocketSend(ws, payload, options = undefined) {
|
|
|
976
976
|
}
|
|
977
977
|
}
|
|
978
978
|
|
|
979
|
-
function unregisterFrameClient(ws) {
|
|
980
|
-
if (!ws) {
|
|
981
|
-
return;
|
|
982
|
-
}
|
|
983
|
-
|
|
979
|
+
function unregisterFrameClient(ws) {
|
|
980
|
+
if (!ws) {
|
|
981
|
+
return;
|
|
982
|
+
}
|
|
983
|
+
stopFrameClientStreams(ws);
|
|
984
|
+
const ids = ws.liveDeskDeviceIds instanceof Set ? ws.liveDeskDeviceIds : new Set();
|
|
984
985
|
for (const deviceId of ids) {
|
|
985
986
|
const clients = frameClientsByDeviceId.get(deviceId);
|
|
986
987
|
if (!clients) {
|
|
@@ -991,9 +992,49 @@ function unregisterFrameClient(ws) {
|
|
|
991
992
|
frameClientsByDeviceId.delete(deviceId);
|
|
992
993
|
}
|
|
993
994
|
}
|
|
994
|
-
frameWildcardClients.delete(ws);
|
|
995
|
-
ws.liveDeskFrameSendLane?.queue?.splice?.(0);
|
|
996
|
-
}
|
|
995
|
+
frameWildcardClients.delete(ws);
|
|
996
|
+
ws.liveDeskFrameSendLane?.queue?.splice?.(0);
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
function hasOtherFrameStreamOwner(ws, deviceId, streamId) {
|
|
1000
|
+
if (!streamId) {
|
|
1001
|
+
return false;
|
|
1002
|
+
}
|
|
1003
|
+
for (const candidate of frameClients) {
|
|
1004
|
+
if (candidate === ws || candidate.readyState !== candidate.OPEN) {
|
|
1005
|
+
continue;
|
|
1006
|
+
}
|
|
1007
|
+
if (candidate.liveDeskStreamIdsByDeviceId instanceof Map
|
|
1008
|
+
&& candidate.liveDeskStreamIdsByDeviceId.get(deviceId) === streamId) {
|
|
1009
|
+
return true;
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
return false;
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
function stopFrameClientStreams(ws) {
|
|
1016
|
+
const streams = ws.liveDeskStreamIdsByDeviceId instanceof Map
|
|
1017
|
+
? ws.liveDeskStreamIdsByDeviceId
|
|
1018
|
+
: null;
|
|
1019
|
+
if (!streams) {
|
|
1020
|
+
return;
|
|
1021
|
+
}
|
|
1022
|
+
const streamPurpose = String(ws.liveDeskLiveOptions?.streamPurpose || 'wall');
|
|
1023
|
+
for (const [deviceId, streamId] of streams.entries()) {
|
|
1024
|
+
if (!streamId || hasOtherFrameStreamOwner(ws, deviceId, streamId)) {
|
|
1025
|
+
continue;
|
|
1026
|
+
}
|
|
1027
|
+
const result = remoteHub.stopLiveStream(deviceId, {
|
|
1028
|
+
streamId,
|
|
1029
|
+
streamPurpose,
|
|
1030
|
+
reason: 'frame-client-closed'
|
|
1031
|
+
});
|
|
1032
|
+
if (result?.ok && !result.alreadyStopped) {
|
|
1033
|
+
console.log(`[LiveDesk Hub] stopped ${streamPurpose} stream after frame client closed device=${deviceId} stream=${streamId}`);
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
streams.clear();
|
|
1037
|
+
}
|
|
997
1038
|
|
|
998
1039
|
function registerFrameClient(ws) {
|
|
999
1040
|
if (!ws) {
|
|
@@ -1322,9 +1363,15 @@ function buildRemoteFrameBinaryPacket(frameEvent, diagnostics = {}) {
|
|
|
1322
1363
|
hardwareEncoder: frame.hardwareEncoder || '',
|
|
1323
1364
|
platformProfile: frame.platformProfile || '',
|
|
1324
1365
|
monitorIndex: Number.isFinite(Number(frame.monitorIndex)) ? Number(frame.monitorIndex) : 0,
|
|
1325
|
-
monitorCount: Number.isFinite(Number(frame.monitorCount)) ? Number(frame.monitorCount) : 1,
|
|
1326
|
-
fps: Number(frame.fps || 0) || 0,
|
|
1327
|
-
|
|
1366
|
+
monitorCount: Number.isFinite(Number(frame.monitorCount)) ? Number(frame.monitorCount) : 1,
|
|
1367
|
+
fps: Number(frame.fps || 0) || 0,
|
|
1368
|
+
requestedFps: Number(frame.requestedFps || 0) || 0,
|
|
1369
|
+
effectiveFps: Number(frame.effectiveFps || 0) || 0,
|
|
1370
|
+
captureAverageMs: Number(frame.captureAverageMs || 0) || 0,
|
|
1371
|
+
captureP95Ms: Number(frame.captureP95Ms || 0) || 0,
|
|
1372
|
+
slowFrameCount: Number(frame.slowFrameCount || 0) || 0,
|
|
1373
|
+
captureHelperRestarts: Number(frame.captureHelperRestarts || 0) || 0,
|
|
1374
|
+
capturedAt: frame.capturedAt || '',
|
|
1328
1375
|
receivedAt: frame.receivedAt || '',
|
|
1329
1376
|
hubPacketAt,
|
|
1330
1377
|
hubPacketEpochMs,
|
|
@@ -1354,24 +1401,38 @@ function buildRemoteFrameBinaryPacket(frameEvent, diagnostics = {}) {
|
|
|
1354
1401
|
return Buffer.concat([header, metaBuffer, payload], 4 + metaBuffer.length + payload.length);
|
|
1355
1402
|
}
|
|
1356
1403
|
|
|
1357
|
-
function buildRemoteAudioBinaryPacket(audioEvent) {
|
|
1358
|
-
const payload = audioEvent?.payload;
|
|
1359
|
-
if (!Buffer.isBuffer(payload) || payload.length === 0) {
|
|
1360
|
-
return null;
|
|
1361
|
-
}
|
|
1362
|
-
const frame = audioEvent.frame || {};
|
|
1363
|
-
const
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1404
|
+
function buildRemoteAudioBinaryPacket(audioEvent) {
|
|
1405
|
+
const payload = audioEvent?.payload;
|
|
1406
|
+
if (!Buffer.isBuffer(payload) || payload.length === 0) {
|
|
1407
|
+
return null;
|
|
1408
|
+
}
|
|
1409
|
+
const frame = audioEvent.frame || {};
|
|
1410
|
+
const audioStatus = audioEvent.audioStatus === true;
|
|
1411
|
+
const metadata = {
|
|
1412
|
+
type: audioStatus ? 'remote.audio.status' : 'remote.audio.binary',
|
|
1413
|
+
frameKind: audioStatus ? 'audio-status' : 'audio',
|
|
1414
|
+
deviceId: audioEvent.deviceId || frame.deviceId || '',
|
|
1415
|
+
frameSeq: Number(frame.frameSeq || 0) || 0,
|
|
1416
|
+
streamId: frame.streamId || '',
|
|
1368
1417
|
mimeType: audioEvent.mimeType || frame.mimeType || frame.format || 'audio/webm;codecs=opus',
|
|
1369
1418
|
sampleRate: Number(frame.sampleRate || 0) || 48000,
|
|
1370
1419
|
channels: Number(frame.channels || 0) || 2,
|
|
1371
1420
|
capturedAt: frame.capturedAt || '',
|
|
1372
1421
|
receivedAt: frame.receivedAt || '',
|
|
1373
|
-
byteLength: Number(audioEvent.byteLength || payload.length) || payload.length
|
|
1374
|
-
|
|
1422
|
+
byteLength: Number(audioEvent.byteLength || payload.length) || payload.length,
|
|
1423
|
+
...(audioStatus ? {
|
|
1424
|
+
commandId: frame.commandId || '',
|
|
1425
|
+
captureAlive: frame.captureAlive === true,
|
|
1426
|
+
helperAlive: frame.helperAlive === true,
|
|
1427
|
+
helperPid: Number(frame.helperPid || 0) || 0,
|
|
1428
|
+
captureGeneration: Number(frame.captureGeneration || 0) || 0,
|
|
1429
|
+
lastPcmAtEpochMs: Number(frame.lastPcmAtEpochMs || 0) || 0,
|
|
1430
|
+
silentForMs: Math.max(0, Number(frame.silentForMs || 0) || 0),
|
|
1431
|
+
restartCount: Math.max(0, Number(frame.restartCount || 0) || 0),
|
|
1432
|
+
error: String(frame.error || ''),
|
|
1433
|
+
statusAtEpochMs: Number(frame.statusAtEpochMs || 0) || Date.now()
|
|
1434
|
+
} : {})
|
|
1435
|
+
};
|
|
1375
1436
|
const metaBuffer = Buffer.from(JSON.stringify(metadata), 'utf8');
|
|
1376
1437
|
const header = Buffer.allocUnsafe(4);
|
|
1377
1438
|
header.writeUInt32BE(metaBuffer.length, 0);
|
|
@@ -1523,28 +1584,42 @@ function sendMode4AtlasFrame(ws, output) {
|
|
|
1523
1584
|
}
|
|
1524
1585
|
}
|
|
1525
1586
|
|
|
1526
|
-
function configureMode4Atlas(ws, payload = {}) {
|
|
1527
|
-
const deviceIds = normalizeDeviceIds(payload.deviceIds ?? payload.devices ?? payload.deviceId).slice(0, 100);
|
|
1528
|
-
const
|
|
1587
|
+
function configureMode4Atlas(ws, payload = {}) {
|
|
1588
|
+
const deviceIds = normalizeDeviceIds(payload.deviceIds ?? payload.devices ?? payload.deviceId).slice(0, 100);
|
|
1589
|
+
const captureDeviceIds = normalizeDeviceIds(payload.captureDeviceIds ?? payload.inputDeviceIds ?? deviceIds)
|
|
1590
|
+
.filter(deviceId => deviceIds.includes(deviceId));
|
|
1591
|
+
const monitorSelections = normalizeMonitorSelections(payload.monitorSelections);
|
|
1529
1592
|
const sharpTiles = Number(payload.tileWidth) >= 480 || Number(payload.tileHeight) >= 270;
|
|
1530
1593
|
const tileWidth = sharpTiles ? 480 : 320;
|
|
1531
1594
|
const tileHeight = sharpTiles ? 270 : 180;
|
|
1532
|
-
ws.
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1595
|
+
const previousInputDeviceIds = ws.liveDeskAtlasInputDeviceIds instanceof Set
|
|
1596
|
+
? new Set(ws.liveDeskAtlasInputDeviceIds)
|
|
1597
|
+
: new Set(deviceIds);
|
|
1598
|
+
ws.liveDeskAtlasDeviceIds = new Set(deviceIds);
|
|
1599
|
+
ws.liveDeskAtlasInputDeviceIds = new Set(captureDeviceIds);
|
|
1600
|
+
ws.liveDeskAtlasInputOptions = { tileWidth, tileHeight, monitorSelections };
|
|
1601
|
+
ws.liveDeskAtlasSession.configure({
|
|
1602
|
+
deviceIds,
|
|
1603
|
+
inputDeviceIds: captureDeviceIds,
|
|
1536
1604
|
width: clampNumber(payload.width, 640, 3840, 1920),
|
|
1537
1605
|
height: clampNumber(payload.height, 360, 2160, 1080),
|
|
1538
1606
|
tileWidth,
|
|
1539
1607
|
tileHeight,
|
|
1540
1608
|
fps: clampNumber(payload.fps, 1, 30, 20)
|
|
1541
1609
|
});
|
|
1542
|
-
for (const deviceId of
|
|
1543
|
-
|
|
1544
|
-
|
|
1610
|
+
for (const deviceId of previousInputDeviceIds) {
|
|
1611
|
+
if (ws.liveDeskAtlasInputDeviceIds.has(deviceId)) {
|
|
1612
|
+
continue;
|
|
1613
|
+
}
|
|
1614
|
+
stopMode4AtlasInput(ws, deviceId, 'atlas-input-paused');
|
|
1615
|
+
}
|
|
1616
|
+
for (const deviceId of captureDeviceIds) {
|
|
1617
|
+
startMode4AtlasInput(ws, deviceId, 'atlas-configure');
|
|
1618
|
+
}
|
|
1545
1619
|
sendJson(ws, {
|
|
1546
|
-
type: 'Mode4AtlasSubscription',
|
|
1547
|
-
deviceIds,
|
|
1620
|
+
type: 'Mode4AtlasSubscription',
|
|
1621
|
+
deviceIds,
|
|
1622
|
+
captureDeviceIds,
|
|
1548
1623
|
inputMode: 'mode2-lzo',
|
|
1549
1624
|
outputMode: 'mode4-h264-atlas',
|
|
1550
1625
|
tileWidth,
|
|
@@ -1553,10 +1628,10 @@ function configureMode4Atlas(ws, payload = {}) {
|
|
|
1553
1628
|
});
|
|
1554
1629
|
}
|
|
1555
1630
|
|
|
1556
|
-
function startMode4AtlasInput(ws, deviceId, reason = 'atlas-configure') {
|
|
1557
|
-
const options = ws?.liveDeskAtlasInputOptions;
|
|
1558
|
-
if (!options || !ws.
|
|
1559
|
-
return remoteHub.startLiveStream(deviceId, {
|
|
1631
|
+
function startMode4AtlasInput(ws, deviceId, reason = 'atlas-configure') {
|
|
1632
|
+
const options = ws?.liveDeskAtlasInputOptions;
|
|
1633
|
+
if (!options || !ws.liveDeskAtlasInputDeviceIds?.has(deviceId)) return { ok: false, error: 'atlas-device-not-configured' };
|
|
1634
|
+
return remoteHub.startLiveStream(deviceId, {
|
|
1560
1635
|
fps: 8,
|
|
1561
1636
|
maxWidth: options.tileWidth,
|
|
1562
1637
|
maxHeight: options.tileHeight,
|
|
@@ -1568,8 +1643,30 @@ function startMode4AtlasInput(ws, deviceId, reason = 'atlas-configure') {
|
|
|
1568
1643
|
reuseExisting: false,
|
|
1569
1644
|
silentReuse: true,
|
|
1570
1645
|
restartReason: reason
|
|
1571
|
-
});
|
|
1572
|
-
}
|
|
1646
|
+
});
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
function hasOtherAtlasInputOwner(ws, deviceId) {
|
|
1650
|
+
for (const candidate of atlasClients.keys()) {
|
|
1651
|
+
if (candidate === ws || candidate.readyState !== candidate.OPEN) {
|
|
1652
|
+
continue;
|
|
1653
|
+
}
|
|
1654
|
+
if (candidate.liveDeskAtlasInputDeviceIds?.has(deviceId)) {
|
|
1655
|
+
return true;
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
return false;
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
function stopMode4AtlasInput(ws, deviceId, reason = 'atlas-input-stopped') {
|
|
1662
|
+
if (!deviceId || hasOtherAtlasInputOwner(ws, deviceId)) {
|
|
1663
|
+
return { ok: true, alreadyOwned: true };
|
|
1664
|
+
}
|
|
1665
|
+
return remoteHub.stopLiveStream(deviceId, {
|
|
1666
|
+
streamPurpose: 'atlas',
|
|
1667
|
+
reason
|
|
1668
|
+
});
|
|
1669
|
+
}
|
|
1573
1670
|
|
|
1574
1671
|
function wantsBinaryFrame(req) {
|
|
1575
1672
|
return /^(1|true|yes|on|binary|raw)$/i.test(String(req.query?.binary ?? req.query?.raw ?? ''))
|
|
@@ -2443,9 +2540,13 @@ atlasWss.on('connection', ws => {
|
|
|
2443
2540
|
});
|
|
2444
2541
|
ws.liveDeskAtlasSession = session;
|
|
2445
2542
|
atlasClients.set(ws, session);
|
|
2446
|
-
const cleanup = () => {
|
|
2447
|
-
atlasClients.delete(ws);
|
|
2448
|
-
|
|
2543
|
+
const cleanup = () => {
|
|
2544
|
+
atlasClients.delete(ws);
|
|
2545
|
+
for (const deviceId of ws.liveDeskAtlasInputDeviceIds || []) {
|
|
2546
|
+
stopMode4AtlasInput(ws, deviceId, 'atlas-client-closed');
|
|
2547
|
+
}
|
|
2548
|
+
ws.liveDeskAtlasInputDeviceIds = new Set();
|
|
2549
|
+
session.close();
|
|
2449
2550
|
const lane = ws.liveDeskFrameSendLane;
|
|
2450
2551
|
if (lane) {
|
|
2451
2552
|
lane.queue.length = 0;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "livedesk",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.226",
|
|
4
4
|
"description": "LiveDesk Hub and client launcher",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"@ffmpeg-installer/ffmpeg": "^1.1.0",
|
|
34
|
-
"@livedesk/client": "0.1.
|
|
34
|
+
"@livedesk/client": "0.1.129",
|
|
35
35
|
"@openai/codex-sdk": "0.144.5",
|
|
36
36
|
"cors": "^2.8.5",
|
|
37
37
|
"express": "^4.21.2",
|