@livedesk/hub 0.1.59 → 0.1.61
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/control-presentation-borrow-contract.test.mjs +999 -0
- package/src/live-stream-monitor-contract.js +195 -3
- package/src/remote-hub.js +108 -56
- package/src/server.js +183 -68
- package/src/settings/settings-schema.js +25 -6
- package/src/settings/settings-store.js +9 -8
- package/src/wall-source-restart-contract.test.mjs +48 -0
- package/src/wall-source-restart-runtime.test.mjs +146 -0
|
@@ -10,7 +10,7 @@ export function parseExactLiveStreamMonitorIndex(value) {
|
|
|
10
10
|
: null;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
export function isReusedLiveStreamFrameReady(result, activeStream, expectedBinding) {
|
|
13
|
+
export function isReusedLiveStreamFrameReady(result, activeStream, expectedBinding) {
|
|
14
14
|
const expectedMonitorIndex = parseExactLiveStreamMonitorIndex(expectedBinding?.monitorIndex);
|
|
15
15
|
const activeMonitorIndex = parseExactLiveStreamMonitorIndex(activeStream?.monitorIndex);
|
|
16
16
|
const latestFrame = activeStream?.latestFrame;
|
|
@@ -34,5 +34,197 @@ export function isReusedLiveStreamFrameReady(result, activeStream, expectedBindi
|
|
|
34
34
|
&& Number(latestFrame?.captureGeneration || 0) === expectedCaptureGeneration
|
|
35
35
|
&& expectedMonitorIndex !== null
|
|
36
36
|
&& activeMonitorIndex === expectedMonitorIndex
|
|
37
|
-
&& frameMonitorIndex === expectedMonitorIndex;
|
|
38
|
-
}
|
|
37
|
+
&& frameMonitorIndex === expectedMonitorIndex;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const READ_ONLY_CONTROL_PRESENTATION_MAX_IDLE_MS = 3_000;
|
|
41
|
+
export const READ_ONLY_CONTROL_STOP_RECONCILE_GRACE_MS = 250;
|
|
42
|
+
|
|
43
|
+
function normalizedPurpose(value) {
|
|
44
|
+
return String(value || '').trim().toLowerCase();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function positiveInteger(value) {
|
|
48
|
+
const parsed = Number(value);
|
|
49
|
+
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : 0;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Own exactly one short stop-reconcile timer per device. A replacement Control
|
|
54
|
+
* transition cancels that timer before a PWA Wall can claim the released
|
|
55
|
+
* native capture; its Ready event then rebinds observers immediately.
|
|
56
|
+
*/
|
|
57
|
+
export function createReadOnlyControlPresentationReconcileCoordinator({
|
|
58
|
+
onReconcile,
|
|
59
|
+
graceMs = READ_ONLY_CONTROL_STOP_RECONCILE_GRACE_MS,
|
|
60
|
+
setTimeoutFn = setTimeout,
|
|
61
|
+
clearTimeoutFn = clearTimeout
|
|
62
|
+
} = {}) {
|
|
63
|
+
if (typeof onReconcile !== 'function') {
|
|
64
|
+
throw new TypeError('onReconcile must be a function');
|
|
65
|
+
}
|
|
66
|
+
if (typeof setTimeoutFn !== 'function' || typeof clearTimeoutFn !== 'function') {
|
|
67
|
+
throw new TypeError('timer functions must be callable');
|
|
68
|
+
}
|
|
69
|
+
const normalizedGraceMs = Number(graceMs);
|
|
70
|
+
if (!Number.isFinite(normalizedGraceMs)
|
|
71
|
+
|| normalizedGraceMs < 1
|
|
72
|
+
|| normalizedGraceMs > 5_000) {
|
|
73
|
+
throw new RangeError('graceMs must be between 1 and 5000 milliseconds');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const pendingByDeviceId = new Map();
|
|
77
|
+
let closed = false;
|
|
78
|
+
|
|
79
|
+
function normalizeDeviceId(deviceId) {
|
|
80
|
+
return String(deviceId || '').trim();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function cancelForControlTransition(deviceId) {
|
|
84
|
+
const normalizedDeviceId = normalizeDeviceId(deviceId);
|
|
85
|
+
const owner = normalizedDeviceId ? pendingByDeviceId.get(normalizedDeviceId) : null;
|
|
86
|
+
if (!owner) return false;
|
|
87
|
+
pendingByDeviceId.delete(normalizedDeviceId);
|
|
88
|
+
clearTimeoutFn(owner.timer);
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function scheduleAfterConfirmedStop(deviceId) {
|
|
93
|
+
const normalizedDeviceId = normalizeDeviceId(deviceId);
|
|
94
|
+
if (closed || !normalizedDeviceId || pendingByDeviceId.has(normalizedDeviceId)) {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
const owner = { timer: null };
|
|
98
|
+
pendingByDeviceId.set(normalizedDeviceId, owner);
|
|
99
|
+
try {
|
|
100
|
+
owner.timer = setTimeoutFn(() => {
|
|
101
|
+
if (closed || pendingByDeviceId.get(normalizedDeviceId) !== owner) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
pendingByDeviceId.delete(normalizedDeviceId);
|
|
105
|
+
onReconcile(normalizedDeviceId);
|
|
106
|
+
}, normalizedGraceMs);
|
|
107
|
+
owner.timer?.unref?.();
|
|
108
|
+
} catch (error) {
|
|
109
|
+
if (pendingByDeviceId.get(normalizedDeviceId) === owner) {
|
|
110
|
+
pendingByDeviceId.delete(normalizedDeviceId);
|
|
111
|
+
}
|
|
112
|
+
throw error;
|
|
113
|
+
}
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function reconcileReadyControl(deviceId) {
|
|
118
|
+
const normalizedDeviceId = normalizeDeviceId(deviceId);
|
|
119
|
+
if (closed || !normalizedDeviceId) return false;
|
|
120
|
+
cancelForControlTransition(normalizedDeviceId);
|
|
121
|
+
onReconcile(normalizedDeviceId);
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function close() {
|
|
126
|
+
if (closed) return;
|
|
127
|
+
closed = true;
|
|
128
|
+
for (const owner of pendingByDeviceId.values()) {
|
|
129
|
+
clearTimeoutFn(owner.timer);
|
|
130
|
+
}
|
|
131
|
+
pendingByDeviceId.clear();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
scheduleAfterConfirmedStop,
|
|
136
|
+
cancelForControlTransition,
|
|
137
|
+
reconcileReadyControl,
|
|
138
|
+
close,
|
|
139
|
+
getPendingCount: () => pendingByDeviceId.size
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Bind a Wall presentation observer to an already healthy Control encoder.
|
|
145
|
+
* This returns metadata only: it never starts, stops, or replaces native work.
|
|
146
|
+
*/
|
|
147
|
+
export function resolveReadOnlyControlPresentationBorrow({
|
|
148
|
+
device,
|
|
149
|
+
liveOptions,
|
|
150
|
+
monitorIndex,
|
|
151
|
+
nowEpochMs = Date.now()
|
|
152
|
+
} = {}) {
|
|
153
|
+
if (liveOptions?.allowReadOnlyControlBorrow !== true
|
|
154
|
+
|| normalizedPurpose(liveOptions?.streamPurpose) !== 'wall') {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const active = device?.activeLiveStream;
|
|
159
|
+
const latest = active?.latestFrame;
|
|
160
|
+
const sessionId = String(device?.sessionId || '').trim();
|
|
161
|
+
const streamId = String(active?.streamId || '').trim();
|
|
162
|
+
const commandId = String(active?.commandId || '').trim();
|
|
163
|
+
const captureGeneration = positiveInteger(active?.captureGeneration);
|
|
164
|
+
const activeMonitorIndex = parseExactLiveStreamMonitorIndex(active?.monitorIndex);
|
|
165
|
+
const requestedMonitorIndex = parseExactLiveStreamMonitorIndex(monitorIndex);
|
|
166
|
+
const frameMode = String(active?.frameMode || active?.mode || '').trim().toLowerCase();
|
|
167
|
+
const requestedFrameMode = String(liveOptions?.frameMode || liveOptions?.mode || '').trim().toLowerCase();
|
|
168
|
+
const lastFrameAtEpochMs = Date.parse(String(active?.lastFrameAt || ''));
|
|
169
|
+
const currentEpochMs = Number(nowEpochMs);
|
|
170
|
+
const frameIdleMs = Number.isFinite(lastFrameAtEpochMs) && Number.isFinite(currentEpochMs)
|
|
171
|
+
? Math.max(0, currentEpochMs - lastFrameAtEpochMs)
|
|
172
|
+
: Number.POSITIVE_INFINITY;
|
|
173
|
+
const latestMonitorIndex = parseExactLiveStreamMonitorIndex(latest?.monitorIndex);
|
|
174
|
+
|
|
175
|
+
if (active?.active !== true
|
|
176
|
+
|| active?.open !== true
|
|
177
|
+
|| active?.stopPending === true
|
|
178
|
+
|| !!String(active?.stopCommandId || '').trim()
|
|
179
|
+
|| !!String(active?.pendingDescriptor?.commandId || '').trim()
|
|
180
|
+
|| normalizedPurpose(active?.streamPurpose) !== 'control'
|
|
181
|
+
|| frameMode !== 'mode3-h264-hw'
|
|
182
|
+
|| requestedFrameMode !== 'mode3-h264-hw'
|
|
183
|
+
|| !sessionId
|
|
184
|
+
|| !streamId
|
|
185
|
+
|| !commandId
|
|
186
|
+
|| captureGeneration <= 0
|
|
187
|
+
|| requestedMonitorIndex === null
|
|
188
|
+
|| activeMonitorIndex !== requestedMonitorIndex
|
|
189
|
+
|| active?.readyFrameReceived !== true
|
|
190
|
+
|| Number(active?.framesReceived || 0) <= 0
|
|
191
|
+
|| frameIdleMs > READ_ONLY_CONTROL_PRESENTATION_MAX_IDLE_MS
|
|
192
|
+
|| latest?.currentGenerationVerified !== true
|
|
193
|
+
|| String(latest?.sessionId || '') !== sessionId
|
|
194
|
+
|| String(latest?.streamId || '') !== streamId
|
|
195
|
+
|| String(latest?.commandId || '') !== commandId
|
|
196
|
+
|| Number(latest?.captureGeneration || 0) !== captureGeneration
|
|
197
|
+
|| latestMonitorIndex !== activeMonitorIndex
|
|
198
|
+
|| normalizedPurpose(latest?.streamPurpose) !== 'control') {
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
ok: true,
|
|
204
|
+
commandId,
|
|
205
|
+
sessionId,
|
|
206
|
+
streamId,
|
|
207
|
+
streamPurpose: 'control',
|
|
208
|
+
fps: positiveInteger(active?.fps) || 1,
|
|
209
|
+
mode: String(active?.mode || active?.frameMode || 'mode3-h264-hw'),
|
|
210
|
+
frameMode: 'mode3-h264-hw',
|
|
211
|
+
monitorIndex: activeMonitorIndex,
|
|
212
|
+
captureGeneration,
|
|
213
|
+
ready: true,
|
|
214
|
+
reused: true,
|
|
215
|
+
readOnlyControlBorrow: true,
|
|
216
|
+
presentationPurpose: 'wall',
|
|
217
|
+
requestedProfile: {
|
|
218
|
+
fps: Number(liveOptions?.fps || 0),
|
|
219
|
+
maxWidth: Number(liveOptions?.maxWidth || 0),
|
|
220
|
+
maxHeight: Number(liveOptions?.maxHeight || 0),
|
|
221
|
+
quality: Number(liveOptions?.quality || 0)
|
|
222
|
+
},
|
|
223
|
+
effectiveProfile: {
|
|
224
|
+
fps: Number(active?.fps || 0),
|
|
225
|
+
maxWidth: Number(active?.maxWidth || 0),
|
|
226
|
+
maxHeight: Number(active?.maxHeight || 0),
|
|
227
|
+
quality: Number(active?.quality || 0)
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
}
|
package/src/remote-hub.js
CHANGED
|
@@ -9,7 +9,10 @@ import {
|
|
|
9
9
|
isSecureDirectHandshakeStart
|
|
10
10
|
} from '../../runtime-core/src/direct-secure-transport.js';
|
|
11
11
|
import { createHubRelayControl } from './transport/relay-hub-control.js';
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
parseExactLiveStreamMonitorIndex,
|
|
14
|
+
resolveReadOnlyControlPresentationBorrow
|
|
15
|
+
} from './live-stream-monitor-contract.js';
|
|
13
16
|
import {
|
|
14
17
|
BoundedSegmentedBuffer,
|
|
15
18
|
createBoundedAgentBinaryIngressLane
|
|
@@ -63,9 +66,9 @@ const RECENT_THUMBNAIL_FRAME_CACHE_MAX_BYTES = 2 * 1024 * 1024;
|
|
|
63
66
|
const RECENT_LIVE_FRAME_CACHE_MAX_BYTES = 8 * 1024 * 1024;
|
|
64
67
|
const LIVE_STREAM_PENDING_REUSE_MS = 5000;
|
|
65
68
|
const LIVE_STREAM_REPLACEMENT_PENDING_MS = 7000;
|
|
66
|
-
const LIVE_STREAM_MIN_FRESH_MS = 3000;
|
|
67
|
-
const LIVE_STREAM_MAX_FRESH_MS = 12000;
|
|
68
|
-
const DEFAULT_REMOTE_INPUT_ACK_TIMEOUT_MS = 5000;
|
|
69
|
+
const LIVE_STREAM_MIN_FRESH_MS = 3000;
|
|
70
|
+
const LIVE_STREAM_MAX_FRESH_MS = 12000;
|
|
71
|
+
const DEFAULT_REMOTE_INPUT_ACK_TIMEOUT_MS = 5000;
|
|
69
72
|
// RemoteFast owns a shared 7s native stop deadline. Keep four seconds for a
|
|
70
73
|
// priority command.result to cross either Direct or encrypted Relay transport.
|
|
71
74
|
const DEFAULT_LIVE_CAPTURE_STOP_ACK_TIMEOUT_MS = 11000;
|
|
@@ -2303,7 +2306,7 @@ export class RemoteHubWebSocketAgentSocket {
|
|
|
2303
2306
|
}
|
|
2304
2307
|
}
|
|
2305
2308
|
|
|
2306
|
-
export function createRemoteHub(options = {}) {
|
|
2309
|
+
export function createRemoteHub(options = {}) {
|
|
2307
2310
|
const env = options.env || process.env;
|
|
2308
2311
|
const transportDiagnosticCommandTimeoutMs = clampNumber(
|
|
2309
2312
|
options.transportDiagnosticCommandTimeoutMs,
|
|
@@ -10638,7 +10641,7 @@ export function createRemoteHub(options = {}) {
|
|
|
10638
10641
|
=== normalized.streamPurpose;
|
|
10639
10642
|
}
|
|
10640
10643
|
|
|
10641
|
-
function sharedLiveProfileResult(device, activeLiveStream, normalized, extra = {}) {
|
|
10644
|
+
function sharedLiveProfileResult(device, activeLiveStream, normalized, extra = {}) {
|
|
10642
10645
|
return {
|
|
10643
10646
|
ok: true,
|
|
10644
10647
|
commandId: activeLiveStream.commandId,
|
|
@@ -10667,9 +10670,40 @@ export function createRemoteHub(options = {}) {
|
|
|
10667
10670
|
},
|
|
10668
10671
|
...extra
|
|
10669
10672
|
};
|
|
10670
|
-
}
|
|
10671
|
-
|
|
10672
|
-
function
|
|
10673
|
+
}
|
|
10674
|
+
|
|
10675
|
+
function readOnlyControlPresentationBorrowResult(device, normalized, options = {}) {
|
|
10676
|
+
const borrowed = resolveReadOnlyControlPresentationBorrow({
|
|
10677
|
+
device,
|
|
10678
|
+
liveOptions: {
|
|
10679
|
+
allowReadOnlyControlBorrow: options.allowReadOnlyControlBorrow === true,
|
|
10680
|
+
streamPurpose: normalized.streamPurpose,
|
|
10681
|
+
mode: normalized.transfer.mode,
|
|
10682
|
+
frameMode: normalized.transfer.frameMode,
|
|
10683
|
+
fps: normalized.fps,
|
|
10684
|
+
maxWidth: normalized.maxWidth,
|
|
10685
|
+
maxHeight: normalized.maxHeight,
|
|
10686
|
+
quality: normalized.quality
|
|
10687
|
+
},
|
|
10688
|
+
monitorIndex: normalized.monitorIndex
|
|
10689
|
+
});
|
|
10690
|
+
if (!borrowed) {
|
|
10691
|
+
return null;
|
|
10692
|
+
}
|
|
10693
|
+
emitRemoteEvent('RemoteLiveStreamControlPresentationBorrowed', device, {
|
|
10694
|
+
streamId: borrowed.streamId,
|
|
10695
|
+
commandId: borrowed.commandId,
|
|
10696
|
+
captureGeneration: borrowed.captureGeneration,
|
|
10697
|
+
streamPurpose: borrowed.streamPurpose,
|
|
10698
|
+
presentationPurpose: borrowed.presentationPurpose,
|
|
10699
|
+
monitorIndex: borrowed.monitorIndex,
|
|
10700
|
+
requestedProfile: borrowed.requestedProfile,
|
|
10701
|
+
effectiveProfile: borrowed.effectiveProfile
|
|
10702
|
+
});
|
|
10703
|
+
return borrowed;
|
|
10704
|
+
}
|
|
10705
|
+
|
|
10706
|
+
function startLiveStream(deviceId, options = {}) {
|
|
10673
10707
|
const device = devices.get(String(deviceId || ''));
|
|
10674
10708
|
const denied = policyError(device);
|
|
10675
10709
|
if (denied) return { ok: false, error: denied };
|
|
@@ -10693,11 +10727,13 @@ export function createRemoteHub(options = {}) {
|
|
|
10693
10727
|
}
|
|
10694
10728
|
|
|
10695
10729
|
const now = new Date().toISOString();
|
|
10696
|
-
const normalized = normalizeLiveStreamStartOptions({ fps: 2, ...options, platform: device.platform });
|
|
10697
|
-
const modePolicyError = liveStreamModePolicyError(normalized);
|
|
10698
|
-
if (modePolicyError) return { ok: false, error: modePolicyError };
|
|
10699
|
-
const { fps, maxWidth, maxHeight, quality, monitorIndex, transfer, streamPurpose } = normalized;
|
|
10700
|
-
const
|
|
10730
|
+
const normalized = normalizeLiveStreamStartOptions({ fps: 2, ...options, platform: device.platform });
|
|
10731
|
+
const modePolicyError = liveStreamModePolicyError(normalized);
|
|
10732
|
+
if (modePolicyError) return { ok: false, error: modePolicyError };
|
|
10733
|
+
const { fps, maxWidth, maxHeight, quality, monitorIndex, transfer, streamPurpose } = normalized;
|
|
10734
|
+
const borrowedControl = readOnlyControlPresentationBorrowResult(device, normalized, options);
|
|
10735
|
+
if (borrowedControl) return { ...borrowedControl, synthetic: true };
|
|
10736
|
+
const streamId = safeString(options.streamId, 128) || makeStableLiveStreamId(deviceId, streamPurpose);
|
|
10701
10737
|
if (!reserveDeviceLiveStreamDescriptor(device, streamId)) {
|
|
10702
10738
|
return {
|
|
10703
10739
|
ok: false,
|
|
@@ -10776,12 +10812,13 @@ export function createRemoteHub(options = {}) {
|
|
|
10776
10812
|
monitorIndex,
|
|
10777
10813
|
fps
|
|
10778
10814
|
});
|
|
10779
|
-
emitRemoteEvent('RemoteLiveStreamStarted', device, {
|
|
10780
|
-
streamId,
|
|
10781
|
-
commandId,
|
|
10782
|
-
|
|
10783
|
-
|
|
10784
|
-
|
|
10815
|
+
emitRemoteEvent('RemoteLiveStreamStarted', device, {
|
|
10816
|
+
streamId,
|
|
10817
|
+
commandId,
|
|
10818
|
+
streamPurpose,
|
|
10819
|
+
fps,
|
|
10820
|
+
mode: transfer.mode,
|
|
10821
|
+
synthetic: true
|
|
10785
10822
|
});
|
|
10786
10823
|
return {
|
|
10787
10824
|
ok: true,
|
|
@@ -10811,14 +10848,16 @@ export function createRemoteHub(options = {}) {
|
|
|
10811
10848
|
const modePolicyError = liveStreamModePolicyError(normalized);
|
|
10812
10849
|
if (modePolicyError) return { ok: false, error: modePolicyError };
|
|
10813
10850
|
const { fps, maxWidth, maxHeight, quality, monitorIndex, transfer, streamPurpose } = normalized;
|
|
10814
|
-
if (deviceExplicitlyRejectsFrameMode(device, transfer.frameMode)) {
|
|
10815
|
-
return {
|
|
10816
|
-
ok: false,
|
|
10817
|
-
error: 'device-frame-mode-unavailable',
|
|
10818
|
-
frameMode: transfer.frameMode
|
|
10819
|
-
};
|
|
10820
|
-
}
|
|
10821
|
-
const
|
|
10851
|
+
if (deviceExplicitlyRejectsFrameMode(device, transfer.frameMode)) {
|
|
10852
|
+
return {
|
|
10853
|
+
ok: false,
|
|
10854
|
+
error: 'device-frame-mode-unavailable',
|
|
10855
|
+
frameMode: transfer.frameMode
|
|
10856
|
+
};
|
|
10857
|
+
}
|
|
10858
|
+
const borrowedControl = readOnlyControlPresentationBorrowResult(device, normalized, options);
|
|
10859
|
+
if (borrowedControl) return borrowedControl;
|
|
10860
|
+
const streamId = safeString(options.streamId, 128) || makeStableLiveStreamId(deviceId, streamPurpose);
|
|
10822
10861
|
if (!reserveDeviceLiveStreamDescriptor(device, streamId)) {
|
|
10823
10862
|
return {
|
|
10824
10863
|
ok: false,
|
|
@@ -10875,9 +10914,9 @@ export function createRemoteHub(options = {}) {
|
|
|
10875
10914
|
}
|
|
10876
10915
|
const commandId = safeString(options.commandId, 128) || crypto.randomUUID();
|
|
10877
10916
|
const restartToken = safeString(options.restartToken, 128);
|
|
10878
|
-
if (activeLiveStream
|
|
10879
|
-
&& options.forceRestart === true
|
|
10880
|
-
&& restartToken
|
|
10917
|
+
if (activeLiveStream
|
|
10918
|
+
&& options.forceRestart === true
|
|
10919
|
+
&& restartToken
|
|
10881
10920
|
&& safeString(activeLiveStream.restartToken, 128) === restartToken
|
|
10882
10921
|
&& liveStreamMatchesOptions(activeLiveStream, normalized)
|
|
10883
10922
|
&& liveStreamIsReusable(activeLiveStream)) {
|
|
@@ -10900,11 +10939,11 @@ export function createRemoteHub(options = {}) {
|
|
|
10900
10939
|
captureGeneration: Number(activeLiveStream.captureGeneration || 0),
|
|
10901
10940
|
ready: liveStreamHasCurrentFrame(activeLiveStream),
|
|
10902
10941
|
pending: activeLiveStream.open !== true,
|
|
10903
|
-
reused: true
|
|
10904
|
-
};
|
|
10905
|
-
}
|
|
10906
|
-
if (activeLiveStream
|
|
10907
|
-
&& options.forceRestart !== true
|
|
10942
|
+
reused: true
|
|
10943
|
+
};
|
|
10944
|
+
}
|
|
10945
|
+
if (activeLiveStream
|
|
10946
|
+
&& options.forceRestart !== true
|
|
10908
10947
|
&& options.reuseExisting === true
|
|
10909
10948
|
&& liveStreamMatchesOptions(activeLiveStream, normalized)
|
|
10910
10949
|
&& liveStreamIsReusable(activeLiveStream)) {
|
|
@@ -11084,11 +11123,12 @@ export function createRemoteHub(options = {}) {
|
|
|
11084
11123
|
setDeviceLiveStream(device, nextStreamState);
|
|
11085
11124
|
}
|
|
11086
11125
|
device.counters.liveStreamsStarted += 1;
|
|
11087
|
-
emitRemoteEvent('RemoteLiveStreamStarted', device, {
|
|
11088
|
-
streamId,
|
|
11089
|
-
commandId,
|
|
11090
|
-
|
|
11091
|
-
|
|
11126
|
+
emitRemoteEvent('RemoteLiveStreamStarted', device, {
|
|
11127
|
+
streamId,
|
|
11128
|
+
commandId,
|
|
11129
|
+
streamPurpose,
|
|
11130
|
+
previousCommandId,
|
|
11131
|
+
pending: replacingActiveStream,
|
|
11092
11132
|
fps,
|
|
11093
11133
|
mode: transfer.mode,
|
|
11094
11134
|
frameMode: transfer.frameMode,
|
|
@@ -11277,11 +11317,13 @@ export function createRemoteHub(options = {}) {
|
|
|
11277
11317
|
captureLiveStreamStopOwner(device, streamState, commandId));
|
|
11278
11318
|
}
|
|
11279
11319
|
device.counters.liveStreamsStopped += 1;
|
|
11280
|
-
emitRemoteEvent('RemoteLiveStreamStopped', device, {
|
|
11281
|
-
streamId,
|
|
11282
|
-
commandId,
|
|
11283
|
-
|
|
11284
|
-
|
|
11320
|
+
emitRemoteEvent('RemoteLiveStreamStopped', device, {
|
|
11321
|
+
streamId,
|
|
11322
|
+
commandId,
|
|
11323
|
+
streamPurpose: safeString(streamState?.streamPurpose || streamPurpose, 24).toLowerCase() || 'wall',
|
|
11324
|
+
captureStopConfirmed: true,
|
|
11325
|
+
synthetic: true
|
|
11326
|
+
});
|
|
11285
11327
|
return { ok: true, commandId, streamId, synthetic: true };
|
|
11286
11328
|
}
|
|
11287
11329
|
|
|
@@ -11316,7 +11358,7 @@ export function createRemoteHub(options = {}) {
|
|
|
11316
11358
|
: (streamState ? [streamState] : []);
|
|
11317
11359
|
const stopRequestedAt = new Date().toISOString();
|
|
11318
11360
|
const targetOwners = [];
|
|
11319
|
-
for (const targetStream of targetStreams) {
|
|
11361
|
+
for (const targetStream of targetStreams) {
|
|
11320
11362
|
const pendingTarget = getPendingLiveStreamDescriptor(targetStream);
|
|
11321
11363
|
const targetDescriptor = pendingTarget || targetStream;
|
|
11322
11364
|
const targetOwnerKind = pendingTarget ? 'pending' : 'active';
|
|
@@ -11340,10 +11382,19 @@ export function createRemoteHub(options = {}) {
|
|
|
11340
11382
|
targetOwners.push({
|
|
11341
11383
|
owner: targetOwner,
|
|
11342
11384
|
ownerKind: targetOwnerKind
|
|
11343
|
-
});
|
|
11344
|
-
}
|
|
11345
|
-
}
|
|
11346
|
-
const
|
|
11385
|
+
});
|
|
11386
|
+
}
|
|
11387
|
+
}
|
|
11388
|
+
const stoppedStreamPurposes = new Set(
|
|
11389
|
+
targetOwners
|
|
11390
|
+
.map(targetRecord => safeString(targetRecord.owner?.streamPurpose, 24).toLowerCase())
|
|
11391
|
+
.filter(Boolean));
|
|
11392
|
+
const stoppedStreamPurpose = stoppedStreamPurposes.size === 1
|
|
11393
|
+
? [...stoppedStreamPurposes][0]
|
|
11394
|
+
: targetOwners.length === 0
|
|
11395
|
+
? safeString(streamState?.streamPurpose || streamPurpose, 24).toLowerCase()
|
|
11396
|
+
: '';
|
|
11397
|
+
const stopPromise = acknowledgementPromise.then(acknowledgement => {
|
|
11347
11398
|
const currentDevice = devices.get(String(deviceId || ''));
|
|
11348
11399
|
const stopReported = acknowledgement.result?.stream === true
|
|
11349
11400
|
&& acknowledgement.result?.stopped === true;
|
|
@@ -11411,11 +11462,12 @@ export function createRemoteHub(options = {}) {
|
|
|
11411
11462
|
if (captureStopConfirmed
|
|
11412
11463
|
&& (targetOwners.length === 0 || appliedOwnerCount > 0)) {
|
|
11413
11464
|
currentDevice.counters.liveStreamsStopped += 1;
|
|
11414
|
-
emitRemoteEvent('RemoteLiveStreamStopped', currentDevice, {
|
|
11415
|
-
streamId,
|
|
11416
|
-
commandId,
|
|
11417
|
-
|
|
11418
|
-
|
|
11465
|
+
emitRemoteEvent('RemoteLiveStreamStopped', currentDevice, {
|
|
11466
|
+
streamId,
|
|
11467
|
+
commandId,
|
|
11468
|
+
streamPurpose: stoppedStreamPurpose,
|
|
11469
|
+
appliedOwnerCount,
|
|
11470
|
+
captureStopConfirmed: true
|
|
11419
11471
|
});
|
|
11420
11472
|
} else if (!captureStopConfirmed && appliedOwnerCount > 0) {
|
|
11421
11473
|
emitRemoteEvent('RemoteLiveStreamStopUnconfirmed', currentDevice, {
|