livedesk 0.1.583 → 0.1.585

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,104 +1,104 @@
1
- import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
2
- import { dirname } from 'node:path';
3
-
4
- let temporaryFileSequence = 0;
5
-
6
- function writeAtomicPrivateFile(path, contents) {
7
- mkdirSync(dirname(path), { recursive: true });
8
- temporaryFileSequence += 1;
9
- const temporaryPath = `${path}.${process.pid}.${Date.now()}.${temporaryFileSequence}.tmp`;
10
- try {
11
- writeFileSync(temporaryPath, contents, {
12
- encoding: 'utf8',
13
- mode: 0o600,
14
- flush: true
15
- });
16
- renameSync(temporaryPath, path);
17
- } finally {
18
- try { rmSync(temporaryPath, { force: true }); } catch { /* best effort */ }
19
- }
20
- }
21
-
22
- export function writeDurableAuthSessionRecord({
23
- primaryPath,
24
- backupPath = `${primaryPath}.backup`,
25
- record
26
- }) {
27
- if (!primaryPath || !record || typeof record !== 'object') {
28
- throw new Error('durable-auth-session-write-parameters-required');
29
- }
30
- const contents = JSON.stringify(record, null, 2);
31
- let backupError = null;
32
- try {
33
- writeAtomicPrivateFile(backupPath, contents);
34
- } catch (error) {
35
- // A mirror failure is reported to the caller, but it must not prevent the
36
- // primary from preserving the successfully refreshed encrypted login.
37
- backupError = error;
38
- }
39
- writeAtomicPrivateFile(primaryPath, contents);
40
- return { backupWritten: !backupError, backupError };
41
- }
42
-
43
- export function readDurableAuthSessionRecord({
44
- primaryPath,
45
- backupPath = `${primaryPath}.backup`,
46
- decode
47
- }) {
48
- if (!primaryPath || typeof decode !== 'function') {
49
- throw new Error('durable-auth-session-read-parameters-required');
50
- }
51
- const failures = [];
52
- const candidates = [];
53
- for (const candidatePath of [primaryPath, backupPath]) {
54
- if (!existsSync(candidatePath)) continue;
55
- try {
56
- const record = JSON.parse(readFileSync(candidatePath, 'utf8'));
57
- const value = decode(record);
58
- if (!value) throw new Error('durable-auth-session-record-invalid');
59
- const savedAt = Date.parse(String(record?.savedAt || ''));
60
- candidates.push({
61
- path: candidatePath,
62
- record,
63
- value,
64
- savedAt: Number.isFinite(savedAt) ? savedAt : 0
65
- });
66
- } catch (error) {
67
- failures.push({ path: candidatePath, error: String(error?.message || error) });
68
- }
69
- }
70
- if (candidates.length === 0) {
71
- return { value: null, recoveredFromBackup: false, primaryRepaired: false, failures };
72
- }
73
-
74
- // A refresh may finish writing the backup immediately before an interrupted
75
- // primary replacement. Prefer the newest valid encrypted record so the app
76
- // does not fall back to an older, already-rotated refresh token.
77
- candidates.sort((left, right) => {
78
- if (right.savedAt !== left.savedAt) return right.savedAt - left.savedAt;
79
- if (left.path === primaryPath) return -1;
80
- if (right.path === primaryPath) return 1;
81
- return 0;
82
- });
83
- const selected = candidates[0];
84
- const recoveredFromBackup = selected.path === backupPath;
85
- let primaryRepaired = false;
86
- if (recoveredFromBackup) {
87
- try {
88
- writeAtomicPrivateFile(primaryPath, JSON.stringify(selected.record, null, 2));
89
- primaryRepaired = true;
90
- } catch (error) {
91
- failures.push({ path: primaryPath, error: String(error?.message || error) });
92
- }
93
- }
94
- return { value: selected.value, recoveredFromBackup, primaryRepaired, failures };
95
- }
96
-
97
- export function clearDurableAuthSessionRecord({
98
- primaryPath,
99
- backupPath = `${primaryPath}.backup`
100
- }) {
101
- for (const path of [primaryPath, backupPath]) {
102
- try { rmSync(path, { force: true }); } catch { /* best effort */ }
103
- }
104
- }
1
+ import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
2
+ import { dirname } from 'node:path';
3
+
4
+ let temporaryFileSequence = 0;
5
+
6
+ function writeAtomicPrivateFile(path, contents) {
7
+ mkdirSync(dirname(path), { recursive: true });
8
+ temporaryFileSequence += 1;
9
+ const temporaryPath = `${path}.${process.pid}.${Date.now()}.${temporaryFileSequence}.tmp`;
10
+ try {
11
+ writeFileSync(temporaryPath, contents, {
12
+ encoding: 'utf8',
13
+ mode: 0o600,
14
+ flush: true
15
+ });
16
+ renameSync(temporaryPath, path);
17
+ } finally {
18
+ try { rmSync(temporaryPath, { force: true }); } catch { /* best effort */ }
19
+ }
20
+ }
21
+
22
+ export function writeDurableAuthSessionRecord({
23
+ primaryPath,
24
+ backupPath = `${primaryPath}.backup`,
25
+ record
26
+ }) {
27
+ if (!primaryPath || !record || typeof record !== 'object') {
28
+ throw new Error('durable-auth-session-write-parameters-required');
29
+ }
30
+ const contents = JSON.stringify(record, null, 2);
31
+ let backupError = null;
32
+ try {
33
+ writeAtomicPrivateFile(backupPath, contents);
34
+ } catch (error) {
35
+ // A mirror failure is reported to the caller, but it must not prevent the
36
+ // primary from preserving the successfully refreshed encrypted login.
37
+ backupError = error;
38
+ }
39
+ writeAtomicPrivateFile(primaryPath, contents);
40
+ return { backupWritten: !backupError, backupError };
41
+ }
42
+
43
+ export function readDurableAuthSessionRecord({
44
+ primaryPath,
45
+ backupPath = `${primaryPath}.backup`,
46
+ decode
47
+ }) {
48
+ if (!primaryPath || typeof decode !== 'function') {
49
+ throw new Error('durable-auth-session-read-parameters-required');
50
+ }
51
+ const failures = [];
52
+ const candidates = [];
53
+ for (const candidatePath of [primaryPath, backupPath]) {
54
+ if (!existsSync(candidatePath)) continue;
55
+ try {
56
+ const record = JSON.parse(readFileSync(candidatePath, 'utf8'));
57
+ const value = decode(record);
58
+ if (!value) throw new Error('durable-auth-session-record-invalid');
59
+ const savedAt = Date.parse(String(record?.savedAt || ''));
60
+ candidates.push({
61
+ path: candidatePath,
62
+ record,
63
+ value,
64
+ savedAt: Number.isFinite(savedAt) ? savedAt : 0
65
+ });
66
+ } catch (error) {
67
+ failures.push({ path: candidatePath, error: String(error?.message || error) });
68
+ }
69
+ }
70
+ if (candidates.length === 0) {
71
+ return { value: null, recoveredFromBackup: false, primaryRepaired: false, failures };
72
+ }
73
+
74
+ // A refresh may finish writing the backup immediately before an interrupted
75
+ // primary replacement. Prefer the newest valid encrypted record so the app
76
+ // does not fall back to an older, already-rotated refresh token.
77
+ candidates.sort((left, right) => {
78
+ if (right.savedAt !== left.savedAt) return right.savedAt - left.savedAt;
79
+ if (left.path === primaryPath) return -1;
80
+ if (right.path === primaryPath) return 1;
81
+ return 0;
82
+ });
83
+ const selected = candidates[0];
84
+ const recoveredFromBackup = selected.path === backupPath;
85
+ let primaryRepaired = false;
86
+ if (recoveredFromBackup) {
87
+ try {
88
+ writeAtomicPrivateFile(primaryPath, JSON.stringify(selected.record, null, 2));
89
+ primaryRepaired = true;
90
+ } catch (error) {
91
+ failures.push({ path: primaryPath, error: String(error?.message || error) });
92
+ }
93
+ }
94
+ return { value: selected.value, recoveredFromBackup, primaryRepaired, failures };
95
+ }
96
+
97
+ export function clearDurableAuthSessionRecord({
98
+ primaryPath,
99
+ backupPath = `${primaryPath}.backup`
100
+ }) {
101
+ for (const path of [primaryPath, backupPath]) {
102
+ try { rmSync(path, { force: true }); } catch { /* best effort */ }
103
+ }
104
+ }
package/electron/main.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { app, BrowserWindow, clipboard, dialog, ipcMain, Menu, nativeImage, powerMonitor, safeStorage, shell, Tray } from 'electron';
2
- import { existsSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
2
+ import { existsSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
3
3
  import { homedir } from 'node:os';
4
4
  import { join, resolve } from 'node:path';
5
5
  import { spawn } from 'node:child_process';
@@ -7,12 +7,12 @@ import { createProductUpdateManager } from './update-manager.mjs';
7
7
  import { createRecurringProductUpdateOwner } from './recurring-update-owner.mjs';
8
8
  import { createPkceAuthorizationUrl, exchangePkceCode, fetchSupabaseUser, parsePkceCallbackUrl, refreshSupabaseSession } from './oauth-pkce.mjs';
9
9
  import { getRuntimeAuthEnvironment, resolveDesktopAuthConfig, resolveDesktopBuildFlavor } from './auth-config.mjs';
10
- import { createBoundedDesktopLogger } from './bounded-desktop-log.mjs';
11
- import {
12
- clearDurableAuthSessionRecord,
13
- readDurableAuthSessionRecord,
14
- writeDurableAuthSessionRecord
15
- } from './durable-auth-session-file.mjs';
10
+ import { createBoundedDesktopLogger } from './bounded-desktop-log.mjs';
11
+ import {
12
+ clearDurableAuthSessionRecord,
13
+ readDurableAuthSessionRecord,
14
+ writeDurableAuthSessionRecord
15
+ } from './durable-auth-session-file.mjs';
16
16
  import {
17
17
  DESKTOP_AUTH_RETRY_MS,
18
18
  createDesktopAuthInvalidationGuard,
@@ -47,8 +47,8 @@ import {
47
47
  // warm-idle process baseline instead of retaining a hidden utility process.
48
48
  app.commandLine.appendSwitch('disable-features', 'AudioServiceOutOfProcess');
49
49
 
50
- const APP_NAME = 'LiveDesk Desktop';
51
- const appWindowTitle = () => `${APP_NAME} · ${app.getVersion()}`;
50
+ const APP_NAME = 'LiveDesk Desktop';
51
+ const appWindowTitle = () => `${APP_NAME} · ${app.getVersion()}`;
52
52
  const LOCAL_RUNTIME_PORT = Number(process.env.LIVEDESK_LOCAL_RUNTIME_PORT || 5179);
53
53
  const LOCAL_RUNTIME_URL = `http://127.0.0.1:${Number.isInteger(LOCAL_RUNTIME_PORT) && LOCAL_RUNTIME_PORT > 0 ? LOCAL_RUNTIME_PORT : 5179}/`;
54
54
  function readDesktopPackageMetadata() {
@@ -78,8 +78,8 @@ const stateRoot = configuredStateRoot
78
78
  : join(process.env.LOCALAPPDATA || join(homedir(), 'AppData', 'Local'), 'LiveDesk');
79
79
  const logDir = join(stateRoot, 'logs');
80
80
  const logPath = join(logDir, 'desktop.log');
81
- const authSessionPath = join(stateRoot, 'auth-session.json');
82
- const authSessionBackupPath = join(stateRoot, 'auth-session.backup.json');
81
+ const authSessionPath = join(stateRoot, 'auth-session.json');
82
+ const authSessionBackupPath = join(stateRoot, 'auth-session.backup.json');
83
83
  const runtimeStateDir = process.env.LIVEDESK_STATE_DIR || join(homedir(), '.livedesk');
84
84
  try { app.setPath('userData', stateRoot); } catch { /* Electron may reject late path changes in embedded test hosts. */ }
85
85
  const appPath = app.getAppPath();
@@ -120,8 +120,8 @@ let runtimeLogoutPromise = null;
120
120
  let desktopAuthRefreshTimer = null;
121
121
  let desktopSessionRestorePromise = null;
122
122
  let lastDesktopResumeRecoveryAt = 0;
123
- let desktopSessionEpoch = 0;
124
- let lastKnownDesktopSession = null;
123
+ let desktopSessionEpoch = 0;
124
+ let lastKnownDesktopSession = null;
125
125
  const desktopAuthInvalidationGuard = createDesktopAuthInvalidationGuard();
126
126
  const FALLBACK_ICON_DATA_URL = `data:image/svg+xml;charset=utf-8,${encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32" fill="none"><rect x="1" y="1" width="30" height="30" rx="7" fill="#111827"/><g transform="translate(5 5) scale(.785714)"><rect x="2.5" y="4" width="14" height="9" rx="1.5" stroke="#F8FAFC" stroke-width="1.7" opacity=".42"/><path d="M7 16v1.5h5" stroke="#F8FAFC" stroke-width="1.7" stroke-linecap="round" opacity=".42"/><rect x="10.5" y="2.5" width="14" height="9" rx="1.5" stroke="#F8FAFC" stroke-width="1.7" opacity=".62"/><path d="M15 14.5V16h5" stroke="#F8FAFC" stroke-width="1.7" stroke-linecap="round" opacity=".62"/><rect x="6.5" y="8.5" width="15" height="10" rx="1.7" fill="#F8FAFC" fill-opacity=".12" stroke="#F8FAFC" stroke-width="1.9"/><path d="M12 21v1.5H9.5M16 21v1.5h2.5M9.5 22.5h9" stroke="#F8FAFC" stroke-width="1.9" stroke-linecap="round"/></g></svg>')}`;
127
127
  const desktopLogger = createBoundedDesktopLogger({ logPath });
@@ -185,55 +185,55 @@ function normalizeSession(session) {
185
185
  };
186
186
  }
187
187
 
188
- function saveEncryptedSession(session) {
189
- const normalized = normalizeSession(session);
190
- if (!normalized) throw new Error('oauth-session-invalid');
191
- if (!safeStorage.isEncryptionAvailable()) throw new Error('secure-storage-unavailable');
192
- const encryptedSession = safeStorage.encryptString(JSON.stringify(normalized)).toString('base64');
193
- const persisted = writeDurableAuthSessionRecord({
194
- primaryPath: authSessionPath,
195
- backupPath: authSessionBackupPath,
196
- record: { version: 2, savedAt: new Date().toISOString(), encryptedSession }
197
- });
198
- if (!persisted.backupWritten) {
199
- log(`secure auth session backup write failed: ${persisted.backupError?.message || persisted.backupError}`);
200
- }
201
- lastKnownDesktopSession = normalized;
202
- return normalized;
203
- }
204
-
205
- function readEncryptedSession() {
206
- if (!safeStorage.isEncryptionAvailable()) return lastKnownDesktopSession;
207
- const restored = readDurableAuthSessionRecord({
208
- primaryPath: authSessionPath,
209
- backupPath: authSessionBackupPath,
210
- decode: saved => {
211
- if (!saved?.encryptedSession) return null;
212
- const session = JSON.parse(safeStorage.decryptString(Buffer.from(saved.encryptedSession, 'base64')));
213
- return normalizeSession(session);
214
- }
215
- });
216
- if (restored.failures.length > 0) {
217
- log(`secure auth session read recovered=${restored.recoveredFromBackup} failures=${restored.failures.length}`);
218
- }
219
- if (restored.recoveredFromBackup) log('secure auth session primary restored from encrypted backup');
220
- if (!restored.value && restored.failures.length > 0) return lastKnownDesktopSession;
221
- if (restored.value && !existsSync(authSessionBackupPath)) {
222
- try {
223
- saveEncryptedSession(restored.value);
224
- log('secure auth session upgraded to atomic mirrored storage');
225
- } catch (error) {
226
- log(`secure auth session mirror upgrade deferred: ${error?.message || error}`);
227
- }
228
- }
229
- lastKnownDesktopSession = restored.value;
230
- return restored.value;
231
- }
232
-
233
- function clearEncryptedSession() {
234
- clearDesktopAuthRefreshTimer();
235
- lastKnownDesktopSession = null;
236
- clearDurableAuthSessionRecord({ primaryPath: authSessionPath, backupPath: authSessionBackupPath });
188
+ function saveEncryptedSession(session) {
189
+ const normalized = normalizeSession(session);
190
+ if (!normalized) throw new Error('oauth-session-invalid');
191
+ if (!safeStorage.isEncryptionAvailable()) throw new Error('secure-storage-unavailable');
192
+ const encryptedSession = safeStorage.encryptString(JSON.stringify(normalized)).toString('base64');
193
+ const persisted = writeDurableAuthSessionRecord({
194
+ primaryPath: authSessionPath,
195
+ backupPath: authSessionBackupPath,
196
+ record: { version: 2, savedAt: new Date().toISOString(), encryptedSession }
197
+ });
198
+ if (!persisted.backupWritten) {
199
+ log(`secure auth session backup write failed: ${persisted.backupError?.message || persisted.backupError}`);
200
+ }
201
+ lastKnownDesktopSession = normalized;
202
+ return normalized;
203
+ }
204
+
205
+ function readEncryptedSession() {
206
+ if (!safeStorage.isEncryptionAvailable()) return lastKnownDesktopSession;
207
+ const restored = readDurableAuthSessionRecord({
208
+ primaryPath: authSessionPath,
209
+ backupPath: authSessionBackupPath,
210
+ decode: saved => {
211
+ if (!saved?.encryptedSession) return null;
212
+ const session = JSON.parse(safeStorage.decryptString(Buffer.from(saved.encryptedSession, 'base64')));
213
+ return normalizeSession(session);
214
+ }
215
+ });
216
+ if (restored.failures.length > 0) {
217
+ log(`secure auth session read recovered=${restored.recoveredFromBackup} failures=${restored.failures.length}`);
218
+ }
219
+ if (restored.recoveredFromBackup) log('secure auth session primary restored from encrypted backup');
220
+ if (!restored.value && restored.failures.length > 0) return lastKnownDesktopSession;
221
+ if (restored.value && !existsSync(authSessionBackupPath)) {
222
+ try {
223
+ saveEncryptedSession(restored.value);
224
+ log('secure auth session upgraded to atomic mirrored storage');
225
+ } catch (error) {
226
+ log(`secure auth session mirror upgrade deferred: ${error?.message || error}`);
227
+ }
228
+ }
229
+ lastKnownDesktopSession = restored.value;
230
+ return restored.value;
231
+ }
232
+
233
+ function clearEncryptedSession() {
234
+ clearDesktopAuthRefreshTimer();
235
+ lastKnownDesktopSession = null;
236
+ clearDurableAuthSessionRecord({ primaryPath: authSessionPath, backupPath: authSessionBackupPath });
237
237
  }
238
238
 
239
239
  function migrateLegacyPlaintextSession() {
@@ -904,14 +904,14 @@ function updateTray(status = 'Starting') {
904
904
  tray.setToolTip(`${APP_NAME} — ${status}`);
905
905
  }
906
906
 
907
- function showWindow() {
908
- if (!mainWindow) return;
909
- if (mainWindow.isMinimized()) mainWindow.restore();
910
- mainWindow.show();
911
- mainWindow.focus();
912
- if (!mainWindow.webContents.isDestroyed()) mainWindow.webContents.invalidate();
913
- requestDesktopResumeAuthRecovery('window-show');
914
- }
907
+ function showWindow() {
908
+ if (!mainWindow) return;
909
+ if (mainWindow.isMinimized()) mainWindow.restore();
910
+ mainWindow.show();
911
+ mainWindow.focus();
912
+ if (!mainWindow.webContents.isDestroyed()) mainWindow.webContents.invalidate();
913
+ requestDesktopResumeAuthRecovery('window-show');
914
+ }
915
915
 
916
916
  function createMainWindow() {
917
917
  const iconPath = appResource('electron', 'icon.png');
@@ -923,7 +923,7 @@ function createMainWindow() {
923
923
  minHeight: 720,
924
924
  show: false,
925
925
  autoHideMenuBar: true,
926
- title: appWindowTitle(),
926
+ title: appWindowTitle(),
927
927
  icon: existsSync(iconPath) ? iconPath : nativeImage.createFromDataURL(FALLBACK_ICON_DATA_URL),
928
928
  webPreferences: {
929
929
  preload: appResource('electron', 'preload.cjs'),
@@ -932,12 +932,12 @@ function createMainWindow() {
932
932
  sandbox: true,
933
933
  spellcheck: false
934
934
  }
935
- });
936
- mainWindow.on('page-title-updated', event => {
937
- event.preventDefault();
938
- mainWindow?.setTitle(appWindowTitle());
939
- });
940
- mainWindow.on('focus', () => requestDesktopResumeAuthRecovery('window-focus'));
935
+ });
936
+ mainWindow.on('page-title-updated', event => {
937
+ event.preventDefault();
938
+ mainWindow?.setTitle(appWindowTitle());
939
+ });
940
+ mainWindow.on('focus', () => requestDesktopResumeAuthRecovery('window-focus'));
941
941
  mainWindow.webContents.on('will-navigate', (event, url) => {
942
942
  if (isAllowedRuntimeUrl(url)) return;
943
943
  event.preventDefault();
@@ -9533,7 +9533,7 @@ export function createRemoteHub(options = {}) {
9533
9533
  return getLiveStreamIdleMs(activeLiveStream) <= getLiveStreamFreshWindowMs(activeLiveStream);
9534
9534
  }
9535
9535
 
9536
- function liveStreamMatchesOptions(activeLiveStream, normalized) {
9536
+ function liveStreamMatchesOptions(activeLiveStream, normalized) {
9537
9537
  if (!activeLiveStream?.active) {
9538
9538
  return false;
9539
9539
  }
@@ -9544,49 +9544,49 @@ export function createRemoteHub(options = {}) {
9544
9544
  && Number(activeLiveStream.quality || 0) === normalized.quality
9545
9545
  && Number(activeLiveStream.monitorIndex || 0) === normalized.monitorIndex
9546
9546
  && (safeString(activeLiveStream.streamPurpose, 24).toLowerCase() || 'wall')
9547
- === normalized.streamPurpose;
9548
- }
9549
-
9550
- function liveStreamMatchesOwnerIdentity(activeLiveStream, normalized) {
9551
- if (!activeLiveStream) {
9552
- return false;
9553
- }
9554
- return activeLiveStream.frameMode === normalized.transfer.frameMode
9555
- && Number(activeLiveStream.monitorIndex || 0) === normalized.monitorIndex
9556
- && (safeString(activeLiveStream.streamPurpose, 24).toLowerCase() || 'wall')
9557
- === normalized.streamPurpose;
9558
- }
9559
-
9560
- function sharedLiveProfileResult(device, activeLiveStream, normalized, extra = {}) {
9561
- return {
9562
- ok: true,
9563
- commandId: activeLiveStream.commandId,
9564
- sessionId: device.sessionId,
9565
- streamId: activeLiveStream.streamId,
9566
- streamPurpose: safeString(activeLiveStream.streamPurpose, 24) || normalized.streamPurpose,
9567
- fps: Number(activeLiveStream.fps || normalized.fps),
9568
- mode: activeLiveStream.mode || normalized.transfer.mode,
9569
- frameMode: activeLiveStream.frameMode || normalized.transfer.frameMode,
9570
- monitorIndex: Number(activeLiveStream.monitorIndex ?? normalized.monitorIndex),
9571
- captureGeneration: Number(activeLiveStream.captureGeneration || 0),
9572
- ready: liveStreamHasCurrentFrame(activeLiveStream),
9573
- reused: true,
9574
- sharedProfileReused: true,
9575
- requestedProfile: {
9576
- fps: normalized.fps,
9577
- maxWidth: normalized.maxWidth,
9578
- maxHeight: normalized.maxHeight,
9579
- quality: normalized.quality
9580
- },
9581
- effectiveProfile: {
9582
- fps: Number(activeLiveStream.fps || normalized.fps),
9583
- maxWidth: Number(activeLiveStream.maxWidth || normalized.maxWidth),
9584
- maxHeight: Number(activeLiveStream.maxHeight || normalized.maxHeight),
9585
- quality: Number(activeLiveStream.quality || normalized.quality)
9586
- },
9587
- ...extra
9588
- };
9589
- }
9547
+ === normalized.streamPurpose;
9548
+ }
9549
+
9550
+ function liveStreamMatchesOwnerIdentity(activeLiveStream, normalized) {
9551
+ if (!activeLiveStream) {
9552
+ return false;
9553
+ }
9554
+ return activeLiveStream.frameMode === normalized.transfer.frameMode
9555
+ && Number(activeLiveStream.monitorIndex || 0) === normalized.monitorIndex
9556
+ && (safeString(activeLiveStream.streamPurpose, 24).toLowerCase() || 'wall')
9557
+ === normalized.streamPurpose;
9558
+ }
9559
+
9560
+ function sharedLiveProfileResult(device, activeLiveStream, normalized, extra = {}) {
9561
+ return {
9562
+ ok: true,
9563
+ commandId: activeLiveStream.commandId,
9564
+ sessionId: device.sessionId,
9565
+ streamId: activeLiveStream.streamId,
9566
+ streamPurpose: safeString(activeLiveStream.streamPurpose, 24) || normalized.streamPurpose,
9567
+ fps: Number(activeLiveStream.fps || normalized.fps),
9568
+ mode: activeLiveStream.mode || normalized.transfer.mode,
9569
+ frameMode: activeLiveStream.frameMode || normalized.transfer.frameMode,
9570
+ monitorIndex: Number(activeLiveStream.monitorIndex ?? normalized.monitorIndex),
9571
+ captureGeneration: Number(activeLiveStream.captureGeneration || 0),
9572
+ ready: liveStreamHasCurrentFrame(activeLiveStream),
9573
+ reused: true,
9574
+ sharedProfileReused: true,
9575
+ requestedProfile: {
9576
+ fps: normalized.fps,
9577
+ maxWidth: normalized.maxWidth,
9578
+ maxHeight: normalized.maxHeight,
9579
+ quality: normalized.quality
9580
+ },
9581
+ effectiveProfile: {
9582
+ fps: Number(activeLiveStream.fps || normalized.fps),
9583
+ maxWidth: Number(activeLiveStream.maxWidth || normalized.maxWidth),
9584
+ maxHeight: Number(activeLiveStream.maxHeight || normalized.maxHeight),
9585
+ quality: Number(activeLiveStream.quality || normalized.quality)
9586
+ },
9587
+ ...extra
9588
+ };
9589
+ }
9590
9590
 
9591
9591
  function startLiveStream(deviceId, options = {}) {
9592
9592
  const device = devices.get(String(deviceId || ''));
@@ -9742,28 +9742,28 @@ export function createRemoteHub(options = {}) {
9742
9742
  const captureClaim = claimSingleLiveCapture(deviceId, device, streamId, streamPurpose);
9743
9743
  if (!captureClaim.ok) return captureClaim;
9744
9744
  const activeLiveStream = getDeviceLiveStream(device, streamId);
9745
- const pendingDescriptor = getPendingLiveStreamDescriptor(activeLiveStream);
9746
- if (pendingDescriptor?.commandId) {
9747
- const pendingMatchesRequest = liveStreamMatchesOptions(pendingDescriptor, normalized);
9748
- const pendingMatchesSharedOwner = options.forceRestart !== true
9749
- && options.reuseExisting === true
9750
- && options.reuseSharedExisting === true
9751
- && liveStreamMatchesOwnerIdentity(pendingDescriptor, normalized);
9752
- if ((pendingMatchesRequest || pendingMatchesSharedOwner)
9753
- && liveStreamReplacementStillPending(activeLiveStream)) {
9754
- emitRemoteEvent('RemoteLiveStreamRestartPending', device, {
9745
+ const pendingDescriptor = getPendingLiveStreamDescriptor(activeLiveStream);
9746
+ if (pendingDescriptor?.commandId) {
9747
+ const pendingMatchesRequest = liveStreamMatchesOptions(pendingDescriptor, normalized);
9748
+ const pendingMatchesSharedOwner = options.forceRestart !== true
9749
+ && options.reuseExisting === true
9750
+ && options.reuseSharedExisting === true
9751
+ && liveStreamMatchesOwnerIdentity(pendingDescriptor, normalized);
9752
+ if ((pendingMatchesRequest || pendingMatchesSharedOwner)
9753
+ && liveStreamReplacementStillPending(activeLiveStream)) {
9754
+ emitRemoteEvent('RemoteLiveStreamRestartPending', device, {
9755
9755
  streamId,
9756
9756
  commandId: pendingDescriptor.commandId,
9757
9757
  activeCommandId: activeLiveStream.commandId,
9758
9758
  reason: safeString(options.restartReason || 'duplicate-restart', 128)
9759
9759
  });
9760
- if (pendingMatchesSharedOwner && !pendingMatchesRequest) {
9761
- return sharedLiveProfileResult(device, pendingDescriptor, normalized, {
9762
- ready: false,
9763
- pending: true
9764
- });
9765
- }
9766
- return {
9760
+ if (pendingMatchesSharedOwner && !pendingMatchesRequest) {
9761
+ return sharedLiveProfileResult(device, pendingDescriptor, normalized, {
9762
+ ready: false,
9763
+ pending: true
9764
+ });
9765
+ }
9766
+ return {
9767
9767
  ok: true,
9768
9768
  commandId: pendingDescriptor.commandId,
9769
9769
  sessionId: device.sessionId,
@@ -9816,9 +9816,9 @@ export function createRemoteHub(options = {}) {
9816
9816
  reused: true
9817
9817
  };
9818
9818
  }
9819
- if (activeLiveStream
9820
- && options.forceRestart !== true
9821
- && options.reuseExisting === true
9819
+ if (activeLiveStream
9820
+ && options.forceRestart !== true
9821
+ && options.reuseExisting === true
9822
9822
  && liveStreamMatchesOptions(activeLiveStream, normalized)
9823
9823
  && liveStreamIsReusable(activeLiveStream)) {
9824
9824
  return {
@@ -9833,28 +9833,28 @@ export function createRemoteHub(options = {}) {
9833
9833
  monitorIndex: Number(activeLiveStream.monitorIndex || monitorIndex),
9834
9834
  captureGeneration: Number(activeLiveStream.captureGeneration || 0),
9835
9835
  ready: liveStreamHasCurrentFrame(activeLiveStream),
9836
- reused: true
9837
- };
9838
- }
9839
- if (activeLiveStream
9840
- && options.forceRestart !== true
9841
- && options.reuseExisting === true
9842
- && options.reuseSharedExisting === true
9843
- && liveStreamMatchesOwnerIdentity(activeLiveStream, normalized)
9844
- && liveStreamIsReusable(activeLiveStream)) {
9845
- emitRemoteEvent('RemoteLiveStreamSharedProfileReused', device, {
9846
- streamId,
9847
- commandId: activeLiveStream.commandId,
9848
- captureGeneration: Number(activeLiveStream.captureGeneration || 0),
9849
- requestedFps: normalized.fps,
9850
- effectiveFps: Number(activeLiveStream.fps || normalized.fps),
9851
- requestedMaxWidth: normalized.maxWidth,
9852
- requestedMaxHeight: normalized.maxHeight,
9853
- effectiveMaxWidth: Number(activeLiveStream.maxWidth || normalized.maxWidth),
9854
- effectiveMaxHeight: Number(activeLiveStream.maxHeight || normalized.maxHeight)
9855
- });
9856
- return sharedLiveProfileResult(device, activeLiveStream, normalized);
9857
- }
9836
+ reused: true
9837
+ };
9838
+ }
9839
+ if (activeLiveStream
9840
+ && options.forceRestart !== true
9841
+ && options.reuseExisting === true
9842
+ && options.reuseSharedExisting === true
9843
+ && liveStreamMatchesOwnerIdentity(activeLiveStream, normalized)
9844
+ && liveStreamIsReusable(activeLiveStream)) {
9845
+ emitRemoteEvent('RemoteLiveStreamSharedProfileReused', device, {
9846
+ streamId,
9847
+ commandId: activeLiveStream.commandId,
9848
+ captureGeneration: Number(activeLiveStream.captureGeneration || 0),
9849
+ requestedFps: normalized.fps,
9850
+ effectiveFps: Number(activeLiveStream.fps || normalized.fps),
9851
+ requestedMaxWidth: normalized.maxWidth,
9852
+ requestedMaxHeight: normalized.maxHeight,
9853
+ effectiveMaxWidth: Number(activeLiveStream.maxWidth || normalized.maxWidth),
9854
+ effectiveMaxHeight: Number(activeLiveStream.maxHeight || normalized.maxHeight)
9855
+ });
9856
+ return sharedLiveProfileResult(device, activeLiveStream, normalized);
9857
+ }
9858
9858
  if (activeLiveStream
9859
9859
  && options.forceRestart !== true
9860
9860
  && liveStreamMatchesOptions(activeLiveStream, normalized)