livedesk 0.1.431 → 0.1.432

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@livedesk/client",
3
- "version": "0.1.189",
3
+ "version": "0.1.190",
4
4
  "description": "LiveDesk local remote client",
5
5
  "type": "module",
6
6
  "bin": {
@@ -41,10 +41,10 @@
41
41
  "ws": "^8.18.3"
42
42
  },
43
43
  "optionalDependencies": {
44
- "@livedesk/fast-linux-x64": "0.1.396",
45
- "@livedesk/fast-osx-arm64": "0.1.396",
46
- "@livedesk/fast-osx-x64": "0.1.396",
47
- "@livedesk/fast-win-x64": "0.1.396"
44
+ "@livedesk/fast-linux-x64": "0.1.397",
45
+ "@livedesk/fast-osx-arm64": "0.1.397",
46
+ "@livedesk/fast-osx-x64": "0.1.397",
47
+ "@livedesk/fast-win-x64": "0.1.397"
48
48
  },
49
49
  "publishConfig": {
50
50
  "access": "public"
@@ -49,7 +49,20 @@ function normalizeAccountAvatarUrl(value) {
49
49
  }
50
50
  }
51
51
 
52
+ function readAccessTokenProfile(accessToken) {
53
+ const token = normalizeString(accessToken, 8192);
54
+ const payloadSegment = token.split('.')[1] || '';
55
+ if (!payloadSegment) return {};
56
+ try {
57
+ const payload = JSON.parse(Buffer.from(payloadSegment, 'base64url').toString('utf8'));
58
+ return payload && typeof payload === 'object' ? payload : {};
59
+ } catch {
60
+ return {};
61
+ }
62
+ }
63
+
52
64
  function readClientAccountProfile(sessionOrUser) {
65
+ const tokenProfile = readAccessTokenProfile(sessionOrUser?.access_token);
53
66
  const user = sessionOrUser?.user && typeof sessionOrUser.user === 'object'
54
67
  ? sessionOrUser.user
55
68
  : sessionOrUser && typeof sessionOrUser === 'object'
@@ -58,12 +71,19 @@ function readClientAccountProfile(sessionOrUser) {
58
71
  const metadata = user.user_metadata && typeof user.user_metadata === 'object'
59
72
  ? user.user_metadata
60
73
  : {};
61
- const email = normalizeString(user.email, 320);
74
+ const tokenMetadata = tokenProfile.user_metadata && typeof tokenProfile.user_metadata === 'object'
75
+ ? tokenProfile.user_metadata
76
+ : {};
77
+ const email = normalizeString(user.email || tokenProfile.email, 320);
62
78
  const name = normalizeString(
63
79
  metadata.full_name
64
80
  || metadata.name
65
81
  || metadata.preferred_username
82
+ || tokenMetadata.full_name
83
+ || tokenMetadata.name
84
+ || tokenMetadata.preferred_username
66
85
  || user.name
86
+ || tokenProfile.name
67
87
  || email
68
88
  || user.id,
69
89
  160
@@ -71,6 +91,8 @@ function readClientAccountProfile(sessionOrUser) {
71
91
  const avatarUrl = normalizeAccountAvatarUrl(
72
92
  metadata.avatar_url
73
93
  || metadata.picture
94
+ || tokenMetadata.avatar_url
95
+ || tokenMetadata.picture
74
96
  || user.avatar_url
75
97
  || user.picture
76
98
  );
@@ -79,7 +101,11 @@ function readClientAccountProfile(sessionOrUser) {
79
101
 
80
102
  function mergeClientAccountProfile(session, sourceUser) {
81
103
  const profile = readClientAccountProfile(sourceUser);
104
+ const existingUserMetadata = session?.user?.user_metadata && typeof session.user.user_metadata === 'object'
105
+ ? session.user.user_metadata
106
+ : {};
82
107
  const userMetadata = {
108
+ ...existingUserMetadata,
83
109
  ...(profile.name ? { full_name: profile.name } : {}),
84
110
  ...(profile.avatarUrl ? { avatar_url: profile.avatarUrl } : {})
85
111
  };
@@ -1275,7 +1301,11 @@ export function createClientRuntimeServer(options = {}) {
1275
1301
  setImmediate(() => {
1276
1302
  void (async () => {
1277
1303
  try {
1278
- const normalizedSession = mergeClientAccountProfile(saved.session, savedSession?.user);
1304
+ // Older persisted sessions kept only id/email on `user`, while
1305
+ // Supabase still carries Google name/avatar claims in the
1306
+ // access-token payload. Recover those display-only claims
1307
+ // locally before any best-effort network hydration.
1308
+ const normalizedSession = mergeClientAccountProfile(saved.session, savedSession);
1279
1309
  const hydratedSession = await hydrateClientAccountProfile(normalizedSession);
1280
1310
  if (!writeSavedSession(hydratedSession)) throw new Error('refresh-token-required');
1281
1311
  state.auth.persisted = true;
@@ -685,7 +685,9 @@ function normalizeRemoteInputEvent(value = {}) {
685
685
  const captureGeneration = Number(input.captureGeneration ?? input.CaptureGeneration);
686
686
  return {
687
687
  type,
688
- monitorIndex: Number.isFinite(monitorIndex) ? normalizeMonitorIndex(monitorIndex) : 0,
688
+ // A missing monitor is not equivalent to the primary display. Control
689
+ // input must prove the exact monitor owned by its capture generation.
690
+ monitorIndex: Number.isFinite(monitorIndex) ? normalizeMonitorIndex(monitorIndex) : null,
689
691
  normalizedX: Number.isFinite(normalizedX) ? Math.max(0, Math.min(1, normalizedX)) : undefined,
690
692
  normalizedY: Number.isFinite(normalizedY) ? Math.max(0, Math.min(1, normalizedY)) : undefined,
691
693
  button: safeString(input.button || input.Button, 24),
@@ -2144,9 +2146,12 @@ export function createRemoteHub(options = {}) {
2144
2146
  byteLength: frame.byteLength
2145
2147
  });
2146
2148
  streamState.latestFrame = frame;
2147
- streamState.lastFrameAt = frame.receivedAt;
2148
- streamState.lastFrameSeq = frame.frameSeq;
2149
- streamState.framesReceived = (streamState.framesReceived || 0) + 1;
2149
+ streamState.lastFrameAt = frame.receivedAt;
2150
+ streamState.lastFrameSeq = frame.frameSeq;
2151
+ streamState.framesReceived = (streamState.framesReceived || 0) + 1;
2152
+ // A synthetic PNG is a complete independently decodable surface and
2153
+ // therefore key-frame equivalent for Control readiness.
2154
+ streamState.readyFrameReceived = true;
2150
2155
  device.lastSeenAt = frame.receivedAt;
2151
2156
  device.counters.liveFramesReceived += 1;
2152
2157
  emitRemoteEvent('RemoteFrameReceived', device, {
@@ -3337,6 +3342,13 @@ export function createRemoteHub(options = {}) {
3337
3342
  || result.InputType
3338
3343
  || pending?.inputType,
3339
3344
  48),
3345
+ monitorIndex: Number(
3346
+ message.monitorIndex
3347
+ ?? message.MonitorIndex
3348
+ ?? result.monitorIndex
3349
+ ?? result.MonitorIndex
3350
+ ?? pending?.monitorIndex
3351
+ ?? 0) || 0,
3340
3352
  issuedAtEpochMs: Number(
3341
3353
  message.issuedAtEpochMs
3342
3354
  ?? message.IssuedAtEpochMs
@@ -3602,13 +3614,35 @@ export function createRemoteHub(options = {}) {
3602
3614
  : 0;
3603
3615
  }
3604
3616
 
3605
- function readFrameStageMs(message, property) {
3606
- const value = Number(message?.[property]);
3607
- return Number.isFinite(value) && value >= 0
3608
- ? Math.round(value)
3609
- : 0;
3610
- }
3611
-
3617
+ function readFrameStageMs(message, property) {
3618
+ const value = Number(message?.[property]);
3619
+ return Number.isFinite(value) && value >= 0
3620
+ ? Math.round(value)
3621
+ : 0;
3622
+ }
3623
+
3624
+ function readOptionalFrameStageMs(message, property) {
3625
+ const rawValue = message?.[property];
3626
+ if (rawValue === undefined || rawValue === null || rawValue === '') {
3627
+ return undefined;
3628
+ }
3629
+ const value = Number(rawValue);
3630
+ return Number.isFinite(value) && value >= 0
3631
+ ? Math.round(value)
3632
+ : undefined;
3633
+ }
3634
+
3635
+ function hasFrameCaptureTiming(message) {
3636
+ if (message?.captureTimingAvailable === false) {
3637
+ return false;
3638
+ }
3639
+ if (message?.captureTimingAvailable === true) {
3640
+ return true;
3641
+ }
3642
+ return ['captureP95Ms', 'captureAverageMs', 'captureStageMs']
3643
+ .some(property => readOptionalFrameStageMs(message, property) !== undefined);
3644
+ }
3645
+
3612
3646
  function computeSameContentStreak(previousFrame, contentHash) {
3613
3647
  if (!contentHash || !previousFrame?.contentHash || previousFrame.contentHash !== contentHash) {
3614
3648
  return 0;
@@ -3682,7 +3716,7 @@ export function createRemoteHub(options = {}) {
3682
3716
  byteLength,
3683
3717
  transport,
3684
3718
  contentHash,
3685
- captureMs: readFrameCaptureMs(message),
3719
+ captureMs: readFrameCaptureMs(message),
3686
3720
  captureStageMs: readFrameStageMs(message, 'captureStageMs'),
3687
3721
  convertMs: readFrameStageMs(message, 'convertMs'),
3688
3722
  compressMs: readFrameStageMs(message, 'compressMs'),
@@ -3774,6 +3808,7 @@ export function createRemoteHub(options = {}) {
3774
3808
  lastFrameAt: '',
3775
3809
  lastFrameSeq: 0,
3776
3810
  framesReceived: 0,
3811
+ readyFrameReceived: false,
3777
3812
  latestFrame: null,
3778
3813
  stoppedAt: '',
3779
3814
  stopReason: ''
@@ -3991,6 +4026,8 @@ export function createRemoteHub(options = {}) {
3991
4026
  && (activeCaptureGeneration <= 0
3992
4027
  || (frameCaptureGeneration > 0
3993
4028
  && frameCaptureGeneration === activeCaptureGeneration));
4029
+ const readyFrameReceivedBeforeFrame = streamState.readyFrameReceived === true;
4030
+ const captureTimingAvailable = hasFrameCaptureTiming(message);
3994
4031
  device.latestLiveFrame = {
3995
4032
  streamId,
3996
4033
  frameSeq,
@@ -4037,10 +4074,11 @@ export function createRemoteHub(options = {}) {
4037
4074
  transferProtocolVersion: transfer.transferProtocolVersion,
4038
4075
  handshake: transfer.handshake,
4039
4076
  fps: Number.isFinite(Number(message.fps)) ? Number(message.fps) : streamState.fps,
4040
- requestedFps: Number.isFinite(Number(message.requestedFps)) ? Number(message.requestedFps) : Number(streamState.fps || 0),
4041
- effectiveFps: Number.isFinite(Number(message.effectiveFps)) ? Number(message.effectiveFps) : Number(message.fps || streamState.fps || 0),
4042
- captureAverageMs: readFrameStageMs(message, 'captureAverageMs'),
4043
- captureP95Ms: readFrameStageMs(message, 'captureP95Ms'),
4077
+ requestedFps: Number.isFinite(Number(message.requestedFps)) ? Number(message.requestedFps) : Number(streamState.fps || 0),
4078
+ effectiveFps: Number.isFinite(Number(message.effectiveFps)) ? Number(message.effectiveFps) : Number(message.fps || streamState.fps || 0),
4079
+ captureTimingAvailable,
4080
+ captureAverageMs: captureTimingAvailable ? readOptionalFrameStageMs(message, 'captureAverageMs') : undefined,
4081
+ captureP95Ms: captureTimingAvailable ? readOptionalFrameStageMs(message, 'captureP95Ms') : undefined,
4044
4082
  slowFrameCount: Number.isFinite(Number(message.slowFrameCount)) ? Number(message.slowFrameCount) : 0,
4045
4083
  captureHelperRestarts: Number.isFinite(Number(message.captureHelperRestarts)) ? Number(message.captureHelperRestarts) : 0,
4046
4084
  capturedAt,
@@ -4048,8 +4086,8 @@ export function createRemoteHub(options = {}) {
4048
4086
  byteLength,
4049
4087
  transport,
4050
4088
  contentHash,
4051
- captureMs: readFrameCaptureMs(message),
4052
- captureStageMs: readFrameStageMs(message, 'captureStageMs'),
4089
+ captureMs: captureTimingAvailable ? readOptionalFrameStageMs(message, 'captureMs') : undefined,
4090
+ captureStageMs: captureTimingAvailable ? readOptionalFrameStageMs(message, 'captureStageMs') : undefined,
4053
4091
  convertMs: readFrameStageMs(message, 'convertMs'),
4054
4092
  compressMs: readFrameStageMs(message, 'compressMs'),
4055
4093
  captureSourceSerial: Number.isFinite(Number(message.captureSourceSerial)) ? Number(message.captureSourceSerial) : 0,
@@ -4073,9 +4111,12 @@ export function createRemoteHub(options = {}) {
4073
4111
  mimeType: device.latestLiveFrame.mimeType,
4074
4112
  byteLength: device.latestLiveFrame.byteLength
4075
4113
  });
4076
- streamState.lastFrameAt = device.lastSeenAt;
4077
- streamState.lastFrameSeq = frameSeq;
4078
- streamState.framesReceived = (streamState.framesReceived || 0) + 1;
4114
+ streamState.lastFrameAt = device.lastSeenAt;
4115
+ streamState.lastFrameSeq = frameSeq;
4116
+ streamState.framesReceived = (streamState.framesReceived || 0) + 1;
4117
+ streamState.readyFrameReceived = readyFrameReceivedBeforeFrame
4118
+ || (currentGenerationVerified
4119
+ && liveFrameSatisfiesReadiness(streamState, device.latestLiveFrame));
4079
4120
  streamState.mode = transfer.mode;
4080
4121
  streamState.frameMode = transfer.frameMode;
4081
4122
  streamState.frameProfile = transfer.frameProfile;
@@ -4093,7 +4134,9 @@ export function createRemoteHub(options = {}) {
4093
4134
  streamState.monitorCount = device.latestLiveFrame.monitorCount;
4094
4135
  ensureDeviceLiveStreams(device).set(streamId, streamState);
4095
4136
  device.counters.liveFramesReceived += 1;
4096
- if (streamState.framesReceived === 1 && liveStreamHasCurrentFrame(streamState)) {
4137
+ if (!readyFrameReceivedBeforeFrame
4138
+ && streamState.readyFrameReceived === true
4139
+ && liveStreamHasCurrentFrame(streamState)) {
4097
4140
  emitRemoteEvent('RemoteLiveStreamReady', device, {
4098
4141
  streamId,
4099
4142
  commandId: streamState.commandId,
@@ -5292,6 +5335,15 @@ export function createRemoteHub(options = {}) {
5292
5335
  activeCaptureGeneration: Number(controlStream.captureGeneration || 0)
5293
5336
  };
5294
5337
  }
5338
+ const activeMonitorIndex = normalizeMonitorIndex(controlStream.monitorIndex);
5339
+ if (!Number.isInteger(normalized.monitorIndex)
5340
+ || normalized.monitorIndex !== activeMonitorIndex) {
5341
+ return {
5342
+ ok: false,
5343
+ error: 'STALE_CONTROL_MONITOR',
5344
+ activeMonitorIndex
5345
+ };
5346
+ }
5295
5347
 
5296
5348
  const inputSocket = device.inputSocket;
5297
5349
  if (inputSocket && !inputSocket.destroyed) {
@@ -5311,7 +5363,8 @@ export function createRemoteHub(options = {}) {
5311
5363
  inputSocket: true,
5312
5364
  sessionId: device.sessionId,
5313
5365
  commandId: controlStream.commandId,
5314
- captureGeneration: controlStream.captureGeneration
5366
+ captureGeneration: controlStream.captureGeneration,
5367
+ monitorIndex: activeMonitorIndex
5315
5368
  };
5316
5369
  }
5317
5370
 
@@ -5332,6 +5385,7 @@ export function createRemoteHub(options = {}) {
5332
5385
  commandId: fallback.commandId,
5333
5386
  inputSeq: normalized.inputSeq,
5334
5387
  inputType: normalized.type,
5388
+ monitorIndex: normalized.monitorIndex,
5335
5389
  issuedAtEpochMs: normalized.issuedAtEpochMs,
5336
5390
  hubReceivedAtEpochMs: normalized.hubReceivedAtEpochMs,
5337
5391
  hubForwardedAtEpochMs,
@@ -5348,6 +5402,7 @@ export function createRemoteHub(options = {}) {
5348
5402
  deliveryCommandId: fallback.commandId,
5349
5403
  commandId: controlStream.commandId,
5350
5404
  captureGeneration: controlStream.captureGeneration,
5405
+ monitorIndex: activeMonitorIndex,
5351
5406
  inputSeq: normalized.inputSeq
5352
5407
  }
5353
5408
  : {
@@ -5855,12 +5910,31 @@ export function createRemoteHub(options = {}) {
5855
5910
  return Number.isFinite(startedAt) && Date.now() - startedAt < LIVE_STREAM_REPLACEMENT_PENDING_MS;
5856
5911
  }
5857
5912
 
5913
+ function liveStreamRequiresReadyKeyFrame(activeLiveStream) {
5914
+ if (safeString(activeLiveStream?.streamPurpose, 24).toLowerCase() !== 'control') {
5915
+ return false;
5916
+ }
5917
+ const codec = safeString(activeLiveStream?.codec, 80).toLowerCase();
5918
+ const mode = safeString(activeLiveStream?.frameMode || activeLiveStream?.mode, 80).toLowerCase();
5919
+ return codec.includes('h264') || mode === 'mode3-h264-hw';
5920
+ }
5921
+
5922
+ function liveFrameSatisfiesReadiness(activeLiveStream, frame) {
5923
+ return !liveStreamRequiresReadyKeyFrame(activeLiveStream)
5924
+ || frame?.isKeyFrame === true
5925
+ || safeString(frame?.chunkType, 20).toLowerCase() === 'key';
5926
+ }
5927
+
5858
5928
  function liveStreamHasCurrentFrame(activeLiveStream) {
5859
5929
  if (!activeLiveStream?.active
5860
5930
  || activeLiveStream.open !== true
5861
5931
  || Number(activeLiveStream.framesReceived || 0) < 1) {
5862
5932
  return false;
5863
5933
  }
5934
+ if (liveStreamRequiresReadyKeyFrame(activeLiveStream)
5935
+ && activeLiveStream.readyFrameReceived !== true) {
5936
+ return false;
5937
+ }
5864
5938
  const latestFrame = activeLiveStream.latestFrame;
5865
5939
  if (!latestFrame || latestFrame.currentGenerationVerified !== true) {
5866
5940
  return false;
@@ -5984,6 +6058,7 @@ export function createRemoteHub(options = {}) {
5984
6058
  lastFrameAt: '',
5985
6059
  lastFrameSeq: 0,
5986
6060
  framesReceived: 0,
6061
+ readyFrameReceived: false,
5987
6062
  streamPurpose,
5988
6063
  captureGeneration
5989
6064
  });
@@ -6201,8 +6276,9 @@ export function createRemoteHub(options = {}) {
6201
6276
  stoppedAt: '',
6202
6277
  stopReason: '',
6203
6278
  lastFrameAt: '',
6204
- lastFrameSeq: 0,
6279
+ lastFrameSeq: 0,
6205
6280
  framesReceived: 0,
6281
+ readyFrameReceived: false,
6206
6282
  streamPurpose,
6207
6283
  captureGeneration,
6208
6284
  latestFrame: null,
package/hub/src/server.js CHANGED
@@ -2045,8 +2045,13 @@ function buildRemoteFrameBinaryPacket(frameEvent, diagnostics = {}) {
2045
2045
  fps: Number(frame.fps || 0) || 0,
2046
2046
  requestedFps: Number(frame.requestedFps || 0) || 0,
2047
2047
  effectiveFps: Number(frame.effectiveFps || 0) || 0,
2048
- captureAverageMs: Number(frame.captureAverageMs || 0) || 0,
2049
- captureP95Ms: Number(frame.captureP95Ms || 0) || 0,
2048
+ captureTimingAvailable: frame.captureTimingAvailable === true,
2049
+ captureAverageMs: frame.captureTimingAvailable === true && Number.isFinite(Number(frame.captureAverageMs))
2050
+ ? Number(frame.captureAverageMs)
2051
+ : null,
2052
+ captureP95Ms: frame.captureTimingAvailable === true && Number.isFinite(Number(frame.captureP95Ms))
2053
+ ? Number(frame.captureP95Ms)
2054
+ : null,
2050
2055
  slowFrameCount: Number(frame.slowFrameCount || 0) || 0,
2051
2056
  captureHelperRestarts: Number(frame.captureHelperRestarts || 0) || 0,
2052
2057
  capturedAt: frame.capturedAt || '',
@@ -2055,8 +2060,12 @@ function buildRemoteFrameBinaryPacket(frameEvent, diagnostics = {}) {
2055
2060
  hubPacketEpochMs,
2056
2061
  byteLength: Number(frameEvent.byteLength || payload.length) || payload.length,
2057
2062
  contentHash: frame.contentHash || '',
2058
- captureMs: Number(frame.captureMs || 0) || 0,
2059
- captureStageMs: Number(frame.captureStageMs || 0) || 0,
2063
+ captureMs: frame.captureTimingAvailable === true && Number.isFinite(Number(frame.captureMs))
2064
+ ? Number(frame.captureMs)
2065
+ : null,
2066
+ captureStageMs: frame.captureTimingAvailable === true && Number.isFinite(Number(frame.captureStageMs))
2067
+ ? Number(frame.captureStageMs)
2068
+ : null,
2060
2069
  convertMs: Number(frame.convertMs || 0) || 0,
2061
2070
  compressMs: Number(frame.compressMs || 0) || 0,
2062
2071
  captureSourceSerial: Number(frame.captureSourceSerial || 0) || 0,
@@ -2157,7 +2166,8 @@ function broadcastRemoteBinaryFrame(frameEvent) {
2157
2166
  || Number(frame.monitorIndex || 0) !== expectedBinding.monitorIndex) {
2158
2167
  continue;
2159
2168
  }
2160
- if (!expectedBinding.readySent) {
2169
+ const requiresReadyKeyFrame = String(expectedBinding.streamPurpose || '').toLowerCase() === 'control' && isH264;
2170
+ if (!expectedBinding.readySent && (!requiresReadyKeyFrame || isKeyFrame)) {
2161
2171
  expectedBinding.readySent = sendJson(client, {
2162
2172
  type: 'RemoteFrameStreamReady',
2163
2173
  deviceId,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "livedesk",
3
- "version": "0.1.431",
4
- "livedeskClientVersion": "0.1.189",
3
+ "version": "0.1.432",
4
+ "livedeskClientVersion": "0.1.190",
5
5
  "buildFlavor": "production",
6
6
  "description": "LiveDesk Hub and client launcher",
7
7
  "type": "module",
@@ -50,10 +50,10 @@
50
50
  "ws": "^8.18.3"
51
51
  },
52
52
  "optionalDependencies": {
53
- "@livedesk/fast-linux-x64": "0.1.396",
54
- "@livedesk/fast-osx-arm64": "0.1.396",
55
- "@livedesk/fast-osx-x64": "0.1.396",
56
- "@livedesk/fast-win-x64": "0.1.396"
53
+ "@livedesk/fast-linux-x64": "0.1.397",
54
+ "@livedesk/fast-osx-arm64": "0.1.397",
55
+ "@livedesk/fast-osx-x64": "0.1.397",
56
+ "@livedesk/fast-win-x64": "0.1.397"
57
57
  },
58
58
  "publishConfig": {
59
59
  "access": "public"