@3dsource/angular-unreal-module 0.0.162 → 0.0.164
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.
|
@@ -157,7 +157,6 @@ export interface RequestReservation extends InstanceMessageBase {
|
|
|
157
157
|
}
|
|
158
158
|
*/
|
|
159
159
|
const OrchestrationMessageTypes = {
|
|
160
|
-
ping: 'ping',
|
|
161
160
|
error: 'error',
|
|
162
161
|
offer: 'offer',
|
|
163
162
|
answer: 'answer',
|
|
@@ -171,8 +170,6 @@ const OrchestrationMessageTypes = {
|
|
|
171
170
|
instanceReady: 'instanceReady',
|
|
172
171
|
p2pEstablished: 'p2pEstablished',
|
|
173
172
|
instanceReserved: 'instanceReserved',
|
|
174
|
-
playerDisconnected: 'playerDisconnected',
|
|
175
|
-
streamerDisconnected: 'streamerDisconnected',
|
|
176
173
|
interruptClientStream: 'interruptClientStream',
|
|
177
174
|
};
|
|
178
175
|
|
|
@@ -225,6 +222,14 @@ const OUTER_RETRY_DELAY = POLLING_TIME * 2;
|
|
|
225
222
|
// blip, brief unavailability) without burning a second reservation.
|
|
226
223
|
// Total WS attempts before URL rotation = 1 + WS_RETRY_PER_INSTANCE.
|
|
227
224
|
const WS_RETRY_PER_INSTANCE = 1;
|
|
225
|
+
// Bound on `requestCirrusViaSocket`: how long the orchestration control
|
|
226
|
+
// socket may stay silent between `requestStream` and `instanceReady`.
|
|
227
|
+
// The orchestration itself reports an `eta` (default 150 s in
|
|
228
|
+
// `InstanceReservedHandler`) for a cold machine, so `WS_TIMEOUT` (6 s) is
|
|
229
|
+
// far too short here — this is a provisioning wait, not a handshake.
|
|
230
|
+
// On expiry the request fails like any other, so `connectAttempt`'s
|
|
231
|
+
// URL-rotating tier asks a different orchestration URL for a new machine.
|
|
232
|
+
const INSTANCE_READY_TIMEOUT = 180_000;
|
|
228
233
|
const WS_OPEN_STATE = 1;
|
|
229
234
|
const DEFAULT_AFK_TIMEOUT_PERIOD = 15;
|
|
230
235
|
const DEFAULT_AFK_TIMEOUT = 300; // 5 minutes
|
|
@@ -479,7 +484,7 @@ const initialState = {
|
|
|
479
484
|
};
|
|
480
485
|
const unrealReducer = createReducer(initialState, on(changeStatusMainVideoOnScene, (state, { isVideoPlaying }) => {
|
|
481
486
|
return { ...state, isVideoPlaying };
|
|
482
|
-
}), on(setAwsInstance, (state, { instanceName, wsUrl, pollingUrl }) => {
|
|
487
|
+
}), on(setAwsInstance, (state, { instanceName, wsUrl, pollingUrl, streamRequestId, kind }) => {
|
|
483
488
|
return {
|
|
484
489
|
...state,
|
|
485
490
|
awsInstance: {
|
|
@@ -487,7 +492,12 @@ const unrealReducer = createReducer(initialState, on(changeStatusMainVideoOnScen
|
|
|
487
492
|
instanceName,
|
|
488
493
|
wsUrl,
|
|
489
494
|
pollingUrl,
|
|
490
|
-
|
|
495
|
+
streamRequestId,
|
|
496
|
+
kind: kind ?? state.awsInstance.kind,
|
|
497
|
+
// In the socket flow this action lands only once `instanceReady`
|
|
498
|
+
// arrived, i.e. the machine IS provisioned — resetting to 0 here would
|
|
499
|
+
// throw the bar back to the start right at the end of that phase.
|
|
500
|
+
progressComplete: kind === 'socket' ? 1 : 0,
|
|
491
501
|
},
|
|
492
502
|
};
|
|
493
503
|
}), on(setViewportReady, (state) => {
|
|
@@ -593,6 +603,10 @@ const unrealReducer = createReducer(initialState, on(changeStatusMainVideoOnScen
|
|
|
593
603
|
instanceName,
|
|
594
604
|
streamRequestId,
|
|
595
605
|
eta,
|
|
606
|
+
// `instanceReserved` exists only in the socket flow, and it arrives
|
|
607
|
+
// BEFORE `setAwsInstance` — so this is where the flow becomes known,
|
|
608
|
+
// early enough for `selectTotalProgress` to use the eta-driven bar.
|
|
609
|
+
kind: 'socket',
|
|
596
610
|
},
|
|
597
611
|
};
|
|
598
612
|
}), on(setOrchestrationProgress, (state, { progressComplete }) => {
|
|
@@ -642,7 +656,7 @@ const unrealReducer = createReducer(initialState, on(changeStatusMainVideoOnScen
|
|
|
642
656
|
return { ...state, analyticsEvent: event };
|
|
643
657
|
}), on(setCirrusConnected, (state) => {
|
|
644
658
|
return { ...state, cirrusConnected: true };
|
|
645
|
-
}), on(streamAdopted, (state, { machineId, ssData }) => {
|
|
659
|
+
}), on(streamAdopted, (state, { machineId, ssData, streamRequestId }) => {
|
|
646
660
|
return {
|
|
647
661
|
...state,
|
|
648
662
|
cirrusConnected: true,
|
|
@@ -653,6 +667,7 @@ const unrealReducer = createReducer(initialState, on(changeStatusMainVideoOnScen
|
|
|
653
667
|
wsUrl: machineId,
|
|
654
668
|
pollingUrl: null,
|
|
655
669
|
instanceName: 'Adopted Connection',
|
|
670
|
+
streamRequestId: streamRequestId ?? state.awsInstance.streamRequestId,
|
|
656
671
|
progressComplete: 0,
|
|
657
672
|
},
|
|
658
673
|
};
|
|
@@ -778,12 +793,16 @@ function InstanceReadyHandler(msg) {
|
|
|
778
793
|
Logger.warn('instanceReady received without payload — ignoring');
|
|
779
794
|
return;
|
|
780
795
|
}
|
|
796
|
+
if (!payload.originInstanceUri) {
|
|
797
|
+
Logger.warn('instanceReady without originInstanceUri — ignoring');
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
781
800
|
// Emit for observability (telemetry) and store the stream-machine coords.
|
|
782
801
|
this.store.dispatch(instanceReady(payload));
|
|
783
|
-
//
|
|
784
|
-
//
|
|
785
|
-
//
|
|
786
|
-
this.
|
|
802
|
+
// This is the answer `requestCirrusViaSocket` is waiting for: the Cirrus
|
|
803
|
+
// address (`originInstanceUri`) plus the connection id to hand it. The socket
|
|
804
|
+
// itself is opened by the shared `connectToCirrus`, not here.
|
|
805
|
+
this.onInstanceReady$.next(payload);
|
|
787
806
|
}
|
|
788
807
|
|
|
789
808
|
function InstanceReservedHandler(msg) {
|
|
@@ -794,15 +813,6 @@ function InstanceReservedHandler(msg) {
|
|
|
794
813
|
}));
|
|
795
814
|
}
|
|
796
815
|
|
|
797
|
-
function PingHandler() {
|
|
798
|
-
// Reply pong on the active signalling socket (stream socket in the new flow,
|
|
799
|
-
// relay socket in the old polling flow).
|
|
800
|
-
this.sendPlayerSignal(JSON.stringify({
|
|
801
|
-
type: 'pong',
|
|
802
|
-
source: 'front',
|
|
803
|
-
}));
|
|
804
|
-
}
|
|
805
|
-
|
|
806
816
|
function PlayerCountHandler(msg) {
|
|
807
817
|
Logger.log(msg);
|
|
808
818
|
}
|
|
@@ -858,9 +868,16 @@ function OnMessageHandler(e) {
|
|
|
858
868
|
}
|
|
859
869
|
}
|
|
860
870
|
|
|
871
|
+
/**
|
|
872
|
+
* Open handler of the CIRRUS socket only (`addWsHandlers`), so
|
|
873
|
+
* `setCirrusConnected` now means what it says. It used to also send
|
|
874
|
+
* `requestStream` here, which made the flag fire on the orchestration control
|
|
875
|
+
* socket — a whole provisioning wait (`eta`, ~150 s) before any Cirrus existed.
|
|
876
|
+
* `requestStream` belongs to the control socket and is sent by
|
|
877
|
+
* `requestCirrusViaSocket`.
|
|
878
|
+
*/
|
|
861
879
|
function OnOpenHandler() {
|
|
862
880
|
this.store.dispatch(setCirrusConnected());
|
|
863
|
-
this.sendRequestStream();
|
|
864
881
|
}
|
|
865
882
|
|
|
866
883
|
/**
|
|
@@ -1180,7 +1197,9 @@ const selectSignalingParameters = createSelector(unrealFeature.selectSsData, unr
|
|
|
1180
1197
|
info.push(`Unreal App Version: ${data.streamerVersion}`);
|
|
1181
1198
|
info.push(`APP: ${data.streamPath}${data.streamApp}`);
|
|
1182
1199
|
}
|
|
1183
|
-
|
|
1200
|
+
// The endpoint that handed us the Cirrus address: an HTTP polling URL in one
|
|
1201
|
+
// flow, a `wss://` orchestration URL in the other.
|
|
1202
|
+
info.push(`Orchestration: ${instance.pollingUrl}`);
|
|
1184
1203
|
info.push(`WS: ${instance.wsUrl}`);
|
|
1185
1204
|
return info.join('<br>');
|
|
1186
1205
|
});
|
|
@@ -1200,13 +1219,15 @@ const selectIsLoaderScreenVisible = createSelector(selectIsFullyConnected, (full
|
|
|
1200
1219
|
*/
|
|
1201
1220
|
const selectTotalProgress = createSelector(unrealFeature.selectStatusPercentSignallingServer, selectCommandProgress, unrealFeature.selectAwsInstance, unrealFeature.selectDataChannelConnected, (statusPercentSignallingServer, commandProgress, awsInstance, dataChannelConnected) => {
|
|
1202
1221
|
const SIGNALLING_MAX = SIGNALLING_PERCENT_VALUE / 100;
|
|
1203
|
-
|
|
1204
|
-
|
|
1222
|
+
// Stated by the service (`AwsInstance.kind`), not sniffed from the hostname.
|
|
1223
|
+
// In the socket flow it is set by `instanceReserved` — i.e. before
|
|
1224
|
+
// `setAwsInstance` — so the eta-driven bar is picked up from the start of
|
|
1225
|
+
// the provisioning wait.
|
|
1226
|
+
const isNewOrchestration = awsInstance?.kind === 'socket';
|
|
1205
1227
|
// Once the data channel is connected, signaling is fully complete (1.0) —
|
|
1206
1228
|
// regardless of orchestration type. This is what makes an ADOPTED stream
|
|
1207
|
-
// (
|
|
1208
|
-
//
|
|
1209
|
-
// whole first phase done instead of stalling near the command-only value.
|
|
1229
|
+
// (which never set `statusPercentSignallingServer`) correctly show the whole
|
|
1230
|
+
// first phase done instead of stalling near the command-only value.
|
|
1210
1231
|
// Otherwise: new orchestration reports 0–1 progress; legacy uses the
|
|
1211
1232
|
// polling percent.
|
|
1212
1233
|
const signalingProgress = dataChannelConnected
|
|
@@ -1223,8 +1244,19 @@ const selectTotalProgress = createSelector(unrealFeature.selectStatusPercentSign
|
|
|
1223
1244
|
const progressRest = lerp(0, 1 - splitPoint, commandProgress);
|
|
1224
1245
|
return clampf(0, 1, progressSignaling + progressRest);
|
|
1225
1246
|
});
|
|
1226
|
-
/**
|
|
1227
|
-
|
|
1247
|
+
/**
|
|
1248
|
+
* The connection id of the current stream session.
|
|
1249
|
+
*
|
|
1250
|
+
* Primary source is the orchestration, which issues it as `streamRequestId` —
|
|
1251
|
+
* in the polling response (legacy flow) or in `instanceReserved`/`instanceReady`
|
|
1252
|
+
* (new orchestration) — and the browser hands it to Cirrus on the WS upgrade
|
|
1253
|
+
* URL. `ssData.connectionId` (the `ssInfo` message) is the fallback for
|
|
1254
|
+
* backends that don't issue one yet: Cirrus then generates it itself and
|
|
1255
|
+
* reports it back here.
|
|
1256
|
+
*
|
|
1257
|
+
* Used by external consumers (e.g., metabox). Do not remove.
|
|
1258
|
+
*/
|
|
1259
|
+
const selectCirrusConnectionId = createSelector(unrealFeature.selectAwsInstance, unrealFeature.selectSsData, (awsInstance, ssData) => awsInstance?.streamRequestId ?? ssData?.connectionId);
|
|
1228
1260
|
/** Used by external consumers (e.g., metabox). Do not remove. */
|
|
1229
1261
|
const selectUserInactivityDetected = createSelector(unrealFeature.selectAfkCountdown, unrealFeature.selectIsAfkTimerVisible, (remainingSecondsToDisconnect, countdownVisible) => ({
|
|
1230
1262
|
remainingSecondsToDisconnect,
|
|
@@ -1818,10 +1850,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImpor
|
|
|
1818
1850
|
}], ctorParameters: () => [] });
|
|
1819
1851
|
|
|
1820
1852
|
function StreamerListHandler(msg) {
|
|
1821
|
-
// `subscribe` is player signalling
|
|
1822
|
-
|
|
1823
|
-
// matching the socket the `streamerList` arrived on.
|
|
1824
|
-
this.sendPlayerSignal(JSON.stringify({
|
|
1853
|
+
// `subscribe` is player signalling → the Cirrus socket.
|
|
1854
|
+
this.send(JSON.stringify({
|
|
1825
1855
|
type: 'subscribe',
|
|
1826
1856
|
streamerId: msg.ids[0],
|
|
1827
1857
|
}));
|
|
@@ -1835,6 +1865,27 @@ function httpUrlToWs(url) {
|
|
|
1835
1865
|
return url.replace('http://', 'ws://').replace('https://', 'wss://');
|
|
1836
1866
|
}
|
|
1837
1867
|
|
|
1868
|
+
/**
|
|
1869
|
+
* Append the orchestration-issued connection id to a signalling WS URL as
|
|
1870
|
+
* `?streamRequestId=…`.
|
|
1871
|
+
*
|
|
1872
|
+
* Cirrus reads it in its `onConnection` upgrade handler and uses it as the
|
|
1873
|
+
* player's `connectionId` instead of generating one (both the legacy Cirrus and
|
|
1874
|
+
* the new Cirrus-WS keep their `guid()` generator as a fallback). A missing id
|
|
1875
|
+
* is a no-op, so the URL stays byte-identical for backends that don't issue one.
|
|
1876
|
+
*
|
|
1877
|
+
* Only used at the moment of connecting — the URL kept in the store stays clean
|
|
1878
|
+
* (it is rendered in the debug overlay and pattern-matched by
|
|
1879
|
+
* `selectTotalProgress`).
|
|
1880
|
+
*/
|
|
1881
|
+
function withStreamRequestId(wsUrl, streamRequestId) {
|
|
1882
|
+
if (!wsUrl || !streamRequestId) {
|
|
1883
|
+
return wsUrl;
|
|
1884
|
+
}
|
|
1885
|
+
const separator = wsUrl.includes('?') ? '&' : '?';
|
|
1886
|
+
return `${wsUrl}${separator}streamRequestId=${encodeURIComponent(streamRequestId)}`;
|
|
1887
|
+
}
|
|
1888
|
+
|
|
1838
1889
|
/**
|
|
1839
1890
|
* Factory that creates a WebSocket connection and wraps it in an RxJS Observable.
|
|
1840
1891
|
*
|
|
@@ -2007,19 +2058,6 @@ function createWebSocket(wsUrl, timeoutMs = WS_TIMEOUT) {
|
|
|
2007
2058
|
}));
|
|
2008
2059
|
}
|
|
2009
2060
|
|
|
2010
|
-
function PlayerDisconnectHandler() {
|
|
2011
|
-
this.store.dispatch(disconnectStream({
|
|
2012
|
-
reason: DisconnectReason.OrchestrationPlayerDisconnected,
|
|
2013
|
-
message: 'Orchestration: PlayerDisconnectHandler',
|
|
2014
|
-
}));
|
|
2015
|
-
}
|
|
2016
|
-
function StreamerDisconnectHandler() {
|
|
2017
|
-
this.store.dispatch(disconnectStream({
|
|
2018
|
-
reason: DisconnectReason.OrchestrationStreamerDisconnected,
|
|
2019
|
-
message: 'Orchestration: StreamerDisconnectHandler',
|
|
2020
|
-
}));
|
|
2021
|
-
}
|
|
2022
|
-
|
|
2023
2061
|
// Structured constants holding code + description
|
|
2024
2062
|
const ORCHESTRATION_ERROR_CODES = {
|
|
2025
2063
|
NO_AVAILABLE_INSTANCES: {
|
|
@@ -2044,6 +2082,16 @@ function OrchestrationErrorHandler(msg) {
|
|
|
2044
2082
|
if (RETRY_CODES.has(msg.payload.code)) {
|
|
2045
2083
|
const errorInfo = Object.values(ORCHESTRATION_ERROR_CODES).find(({ code }) => code === msg.payload.code);
|
|
2046
2084
|
this.store.dispatch(setOrchestrationMessage({ message: errorInfo?.description }));
|
|
2085
|
+
// Same telemetry channel as the WS/URL/region retry tiers — this branch used
|
|
2086
|
+
// to resend silently, so `kind: 'orchestration'` was declared but never seen.
|
|
2087
|
+
// No count cap here on purpose: the wait is bounded by
|
|
2088
|
+
// `INSTANCE_READY_TIMEOUT` in `requestCirrusViaSocket`, and on expiry the
|
|
2089
|
+
// URL-rotating tier takes over.
|
|
2090
|
+
this.store.dispatch(signalingRetryAttempted({
|
|
2091
|
+
attempt: ++this.orchestrationRetryCount,
|
|
2092
|
+
reason: errorInfo?.description ?? `code ${msg.payload.code}`,
|
|
2093
|
+
kind: 'orchestration',
|
|
2094
|
+
}));
|
|
2047
2095
|
this.orchestrationRetrySub?.unsubscribe();
|
|
2048
2096
|
this.orchestrationRetrySub = timer(POLLING_TIME)
|
|
2049
2097
|
.pipe(takeUntil(this.destroy$))
|
|
@@ -2067,21 +2115,27 @@ class SignallingService extends SubService {
|
|
|
2067
2115
|
this.onConfig$ = new Subject();
|
|
2068
2116
|
this.onWebRtcIce$ = new Subject();
|
|
2069
2117
|
this.onWebRtcAnswer$ = new Subject();
|
|
2118
|
+
// Fed by `InstanceReadyHandler`. `requestCirrusViaSocket` awaits it the same
|
|
2119
|
+
// way `requestCirrusViaPolling` awaits a ready polling response — it is the
|
|
2120
|
+
// socket flow's "here is your Cirrus URL" answer.
|
|
2121
|
+
this.onInstanceReady$ = new Subject();
|
|
2070
2122
|
// Pure signal — no side-effects. The `close()` is wired ONCE in init()
|
|
2071
2123
|
// below so a single abort event triggers exactly one close, regardless
|
|
2072
2124
|
// of how many pipelines have `takeUntil(this.abort$)` attached.
|
|
2073
2125
|
this.abort$ = this.actions$.pipe(ofType(abortEstablishingConnection));
|
|
2126
|
+
// The CIRRUS signalling socket — the only socket that speaks WebRTC
|
|
2127
|
+
// (config/offer/answer/iceCandidate/ping). Same field in all three flows:
|
|
2128
|
+
// the polled `signallingServer`, the stream machine (`originInstanceUri`), or
|
|
2129
|
+
// a direct dev URL.
|
|
2074
2130
|
this.ws = null;
|
|
2075
|
-
//
|
|
2076
|
-
//
|
|
2077
|
-
//
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
//
|
|
2081
|
-
//
|
|
2082
|
-
this.
|
|
2083
|
-
// Bounded retry counter for the stream-machine connection (new-flow only).
|
|
2084
|
-
this.streamMachineRetryCount = 0;
|
|
2131
|
+
// The orchestration CONTROL socket (socket flow only). Opened by
|
|
2132
|
+
// `requestCirrusViaSocket` to ask for a machine and closed the moment
|
|
2133
|
+
// `instanceReady` arrives — per the backend contract nothing uses it after the
|
|
2134
|
+
// handover, so it is never left open with dangling handlers.
|
|
2135
|
+
this.controlWs = null;
|
|
2136
|
+
// `requestStream` resends driven by `OrchestrationErrorHandler` within one
|
|
2137
|
+
// `requestCirrusViaSocket` wait. Reported as `kind: 'orchestration'` retries.
|
|
2138
|
+
this.orchestrationRetryCount = 0;
|
|
2085
2139
|
this.wsMsgHandlers = {};
|
|
2086
2140
|
this.init();
|
|
2087
2141
|
}
|
|
@@ -2089,7 +2143,7 @@ class SignallingService extends SubService {
|
|
|
2089
2143
|
if (this.unrealConfig?.playwright) {
|
|
2090
2144
|
return;
|
|
2091
2145
|
}
|
|
2092
|
-
this.
|
|
2146
|
+
this.setMessageHandlers();
|
|
2093
2147
|
// Single owner of the "close on abort" side-effect. Previously this
|
|
2094
2148
|
// lived as a `tap` inside `abort$` itself, which made every
|
|
2095
2149
|
// `takeUntil(this.abort$)` subscriber fire its own close().
|
|
@@ -2104,13 +2158,12 @@ class SignallingService extends SubService {
|
|
|
2104
2158
|
// and `init()` runs once in the constructor. If these subs were torn
|
|
2105
2159
|
// down by abort$, a second in-injector session (e.g. navigating between
|
|
2106
2160
|
// two routes that both host <app-unreal-scene>) would never send
|
|
2107
|
-
// `p2pEstablished`. `
|
|
2161
|
+
// `p2pEstablished`. `send()` is already a no-op on a closed WS,
|
|
2108
2162
|
// so there's no risk of stray messages.
|
|
2109
2163
|
this.actions$
|
|
2110
2164
|
.pipe(ofType(dataChannelReady), takeUntil(this.destroy$))
|
|
2111
2165
|
.subscribe(() => {
|
|
2112
|
-
|
|
2113
|
-
this.sendPlayerSignal({
|
|
2166
|
+
this.send({
|
|
2114
2167
|
type: 'p2pEstablished',
|
|
2115
2168
|
source: 'front',
|
|
2116
2169
|
sessionId: generateUuid(),
|
|
@@ -2161,7 +2214,7 @@ class SignallingService extends SubService {
|
|
|
2161
2214
|
}
|
|
2162
2215
|
/**
|
|
2163
2216
|
* One inner cycle: rotate `urlGen` over the pool, resolve via
|
|
2164
|
-
* {@link
|
|
2217
|
+
* {@link requestCirrus}, open WS via {@link connectToCirrus},
|
|
2165
2218
|
* retry up to {@link INNER_RETRY_LIMIT} on any hard failure. The
|
|
2166
2219
|
* urlGen lives in this closure so retries advance through the
|
|
2167
2220
|
* pool. Exhaustion bubbles to {@link connectToSignaling}'s outer
|
|
@@ -2169,12 +2222,20 @@ class SignallingService extends SubService {
|
|
|
2169
2222
|
*/
|
|
2170
2223
|
connectAttempt(urls) {
|
|
2171
2224
|
const urlGen = getActiveUrl(urls);
|
|
2172
|
-
return defer(() =>
|
|
2225
|
+
return defer(() => {
|
|
2226
|
+
// One control socket per ATTEMPT, whatever transport this attempt picks —
|
|
2227
|
+
// a rotation from a `wss://` orchestration URL to an `http://` one would
|
|
2228
|
+
// otherwise leave the previous control socket behind. Deliberately NOT
|
|
2229
|
+
// inside the WS-only tier below: that tier reuses the same resolved
|
|
2230
|
+
// machine, so its control socket must survive.
|
|
2231
|
+
this.teardownControlWs();
|
|
2232
|
+
return this.requestCirrus(`${urlGen.next().value}`);
|
|
2233
|
+
}).pipe(tap((awsInstance) => this.store.dispatch(setAwsInstance(awsInstance))), switchMap(({ wsUrl, streamRequestId }) =>
|
|
2173
2234
|
// ── WS-only retry tier ─────────────────────────────────────────────
|
|
2174
2235
|
// Tries the SAME `wsUrl` up to `WS_RETRY_PER_INSTANCE` extra times
|
|
2175
2236
|
// before letting the error bubble up to the outer (URL-rotating)
|
|
2176
2237
|
// retry. This is important because the outer retry calls
|
|
2177
|
-
// `
|
|
2238
|
+
// `requestCirrus(url)` again, which asks orchestration again,
|
|
2178
2239
|
// which causes orchestrator to RESERVE A NEW instance — the
|
|
2179
2240
|
// previous reservation is orphaned (server-side leak).
|
|
2180
2241
|
//
|
|
@@ -2183,7 +2244,11 @@ class SignallingService extends SubService {
|
|
|
2183
2244
|
// burning a second reservation. If WS still fails after these
|
|
2184
2245
|
// extra attempts, the instance is genuinely unusable and we
|
|
2185
2246
|
// accept the cost of rotating.
|
|
2186
|
-
|
|
2247
|
+
// `wsUrl` is always a CIRRUS address here, whichever request method
|
|
2248
|
+
// produced it, so the orchestration-issued connection id always rides
|
|
2249
|
+
// on this upgrade URL and Cirrus adopts it instead of generating its
|
|
2250
|
+
// own. Absent id (older backend, direct dev URL) → URL unchanged.
|
|
2251
|
+
this.connectToCirrus(withStreamRequestId(wsUrl ?? '', streamRequestId)).pipe(filter(Truthy), take(1), retry({
|
|
2187
2252
|
count: WS_RETRY_PER_INSTANCE,
|
|
2188
2253
|
delay: (err, attempt) => {
|
|
2189
2254
|
this.store.dispatch(signalingRetryAttempted({
|
|
@@ -2236,22 +2301,52 @@ class SignallingService extends SubService {
|
|
|
2236
2301
|
});
|
|
2237
2302
|
}
|
|
2238
2303
|
/**
|
|
2239
|
-
*
|
|
2240
|
-
*
|
|
2241
|
-
*
|
|
2304
|
+
* Ask the orchestration for a Cirrus to connect to. All three
|
|
2305
|
+
* implementations answer the same question — "which Cirrus, and with which
|
|
2306
|
+
* `streamRequestId`?" — and emit ONE `AwsInstance` whose `wsUrl` is a Cirrus
|
|
2307
|
+
* address. What differs is only how the answer is obtained and therefore how
|
|
2308
|
+
* it can fail:
|
|
2309
|
+
*
|
|
2310
|
+
* | Method | Transport | Fails on |
|
|
2311
|
+
* | -------------------------- | -------------------- | ------------------------------ |
|
|
2312
|
+
* | {@link requestCirrusViaPolling} | HTTP poll loop | HTTP 4xx (dead URL) |
|
|
2313
|
+
* | {@link requestCirrusViaSocket} | control WS | socket drop / no `instanceReady` in {@link INSTANCE_READY_TIMEOUT} |
|
|
2314
|
+
* | {@link requestCirrusDirect} | none (dev) | never — nothing is requested |
|
|
2315
|
+
*
|
|
2316
|
+
* Whatever they return goes through the SAME downstream in
|
|
2317
|
+
* {@link connectAttempt}: `setAwsInstance` → `connectToCirrus` → the WS-only
|
|
2318
|
+
* and URL-rotating retry tiers. A failure inside any of them is just an
|
|
2319
|
+
* error on this observable, so those tiers handle it uniformly.
|
|
2320
|
+
*/
|
|
2321
|
+
requestCirrus(url) {
|
|
2322
|
+
if (!url.match(/^ws(s?):\/\//i)) {
|
|
2323
|
+
return this.requestCirrusViaPolling(url);
|
|
2324
|
+
}
|
|
2325
|
+
// A `ws://` URL is the orchestration control endpoint when the app supplied
|
|
2326
|
+
// a stream-request context (the same precondition `sendRequestStream` needs);
|
|
2327
|
+
// without it there is nothing to negotiate and the URL is Cirrus itself.
|
|
2328
|
+
return this.hasOrchestrationContext()
|
|
2329
|
+
? this.requestCirrusViaSocket(url)
|
|
2330
|
+
: this.requestCirrusDirect(url);
|
|
2331
|
+
}
|
|
2332
|
+
/** True when `requestStream` can be composed → new orchestration. */
|
|
2333
|
+
hasOrchestrationContext() {
|
|
2334
|
+
return !!this.streamRequestContext() && !!this.environmentId();
|
|
2335
|
+
}
|
|
2336
|
+
/**
|
|
2337
|
+
* Wait until `clientId` and `viewId` are known — every request method needs
|
|
2338
|
+
* them before it may talk to the backend.
|
|
2242
2339
|
*/
|
|
2243
|
-
|
|
2244
|
-
return
|
|
2245
|
-
? this.directWsInstance(url)
|
|
2246
|
-
: this.pollOrchestration(url);
|
|
2340
|
+
waitForClientAndViewIds() {
|
|
2341
|
+
return this.store.select(selectClientAndViewIds).pipe(filter(({ clientId, viewId }) => !!clientId && !!viewId), take(1));
|
|
2247
2342
|
}
|
|
2248
2343
|
/**
|
|
2249
|
-
* Direct `ws://` / `wss://` short-circuit — no
|
|
2250
|
-
*
|
|
2251
|
-
*
|
|
2344
|
+
* Direct `ws://` / `wss://` short-circuit — no orchestration at all, the URL
|
|
2345
|
+
* already points at Cirrus. Dev/playwright convenience, hence no
|
|
2346
|
+
* `kind`: nothing was requested.
|
|
2252
2347
|
*/
|
|
2253
|
-
|
|
2254
|
-
return this.
|
|
2348
|
+
requestCirrusDirect(url) {
|
|
2349
|
+
return this.waitForClientAndViewIds().pipe(map$1(() => ({
|
|
2255
2350
|
wsUrl: url,
|
|
2256
2351
|
pollingUrl: null,
|
|
2257
2352
|
instanceName: 'Direct Connection',
|
|
@@ -2264,7 +2359,7 @@ class SignallingService extends SubService {
|
|
|
2264
2359
|
* last response captured in the closure). Maps the final ready
|
|
2265
2360
|
* response to an `AwsInstance`.
|
|
2266
2361
|
*/
|
|
2267
|
-
|
|
2362
|
+
requestCirrusViaPolling(url) {
|
|
2268
2363
|
return defer(() => {
|
|
2269
2364
|
let lastResp;
|
|
2270
2365
|
this.store.dispatch(pollingStarted());
|
|
@@ -2279,12 +2374,87 @@ class SignallingService extends SubService {
|
|
|
2279
2374
|
wsUrl: httpUrlToWs(`${location.protocol}//${data.signallingServer}`),
|
|
2280
2375
|
pollingUrl: url,
|
|
2281
2376
|
instanceName: data.signallingServer ?? '',
|
|
2377
|
+
// The orchestration issues the connection id here; it is handed to
|
|
2378
|
+
// Cirrus on the WS upgrade URL (see `connectAttempt`).
|
|
2379
|
+
streamRequestId: data.streamRequestId,
|
|
2380
|
+
kind: 'polling',
|
|
2282
2381
|
})), finalize(() => {
|
|
2283
2382
|
this.commandTelemetryService.trackStopCommand('EXT-getSignaling', true, { ...(lastResp ?? {}), multi: true });
|
|
2284
2383
|
this.store.dispatch(pollingEnded());
|
|
2285
2384
|
}));
|
|
2286
2385
|
});
|
|
2287
2386
|
}
|
|
2387
|
+
/**
|
|
2388
|
+
* WS-orchestration path — the socket-transport twin of
|
|
2389
|
+
* {@link requestCirrusViaPolling}. Opens the control socket, sends
|
|
2390
|
+
* `requestStream` and waits for `instanceReady`, whose `originInstanceUri`
|
|
2391
|
+
* IS the Cirrus address.
|
|
2392
|
+
*
|
|
2393
|
+
* Failure modes are handled here so they surface as a plain error and reach
|
|
2394
|
+
* `connectAttempt`'s retry tiers (previously this branch lived outside them
|
|
2395
|
+
* and a dead control socket silently hung the session):
|
|
2396
|
+
* - control socket closes/errors before `instanceReady` → error
|
|
2397
|
+
* - no `instanceReady` within {@link INSTANCE_READY_TIMEOUT} → error
|
|
2398
|
+
*
|
|
2399
|
+
* The control socket lives exactly as long as this wait — see
|
|
2400
|
+
* {@link awaitInstanceReady}.
|
|
2401
|
+
*/
|
|
2402
|
+
requestCirrusViaSocket(url) {
|
|
2403
|
+
return defer(() => {
|
|
2404
|
+
let resolved;
|
|
2405
|
+
this.orchestrationRetryCount = 0;
|
|
2406
|
+
this.commandTelemetryService.trackStartCommand('EXT-getSignaling');
|
|
2407
|
+
return this.waitForClientAndViewIds().pipe(switchMap(() => createWebSocket(url)), switchMap((control) => this.awaitInstanceReady(control, url)), tap((instance) => {
|
|
2408
|
+
resolved = instance;
|
|
2409
|
+
}), finalize(() => this.commandTelemetryService.trackStopCommand('EXT-getSignaling', true, { ...(resolved ?? {}), multi: true })));
|
|
2410
|
+
});
|
|
2411
|
+
}
|
|
2412
|
+
/**
|
|
2413
|
+
* Send `requestStream` on the freshly opened control socket and resolve with
|
|
2414
|
+
* the machine coordinates from `instanceReady`.
|
|
2415
|
+
*
|
|
2416
|
+
* The socket lives exactly as long as this wait: its close/error are HARD
|
|
2417
|
+
* failures of the request, and on resolution it is closed with every handler
|
|
2418
|
+
* detached (the backend does not use it after the handover). So the control
|
|
2419
|
+
* socket has no post-handover state at all — nothing that could report a stale
|
|
2420
|
+
* "disconnect" against a live stream.
|
|
2421
|
+
*/
|
|
2422
|
+
awaitInstanceReady(control, orchestrationUrl) {
|
|
2423
|
+
return new Observable((observer) => {
|
|
2424
|
+
control.onmessage = OnMessageHandler.bind(this);
|
|
2425
|
+
control.onclose = (e) => observer.error(new Error(`Orchestration socket closed before instanceReady (code ${e.code})`));
|
|
2426
|
+
control.onerror = () => observer.error(new Error('Orchestration socket error before instanceReady'));
|
|
2427
|
+
this.controlWs = control;
|
|
2428
|
+
Logger.log('ORCHESTRATION CONTROL WS OPEN');
|
|
2429
|
+
this.sendRequestStream();
|
|
2430
|
+
const sub = this.onInstanceReady$.pipe(take(1)).subscribe((payload) => {
|
|
2431
|
+
// The control socket has done its only job. Per the backend contract it
|
|
2432
|
+
// is not used after the handover, so it is closed here — with all its
|
|
2433
|
+
// handlers detached by `teardownSocket`, which also means the close
|
|
2434
|
+
// itself cannot come back as an error above.
|
|
2435
|
+
this.teardownControlWs();
|
|
2436
|
+
observer.next({
|
|
2437
|
+
wsUrl: payload.originInstanceUri,
|
|
2438
|
+
// The endpoint that answered — the orchestration URL here, the HTTP
|
|
2439
|
+
// polling URL in the other flow. Shown as `Orchestration:` in the overlay.
|
|
2440
|
+
pollingUrl: orchestrationUrl,
|
|
2441
|
+
instanceName: payload.originInstanceId,
|
|
2442
|
+
streamRequestId: payload.streamRequestId,
|
|
2443
|
+
kind: 'socket',
|
|
2444
|
+
});
|
|
2445
|
+
observer.complete();
|
|
2446
|
+
});
|
|
2447
|
+
// Errored, timed out or aborted before `instanceReady` — same teardown,
|
|
2448
|
+
// the socket must not be left behind either way.
|
|
2449
|
+
return () => {
|
|
2450
|
+
sub.unsubscribe();
|
|
2451
|
+
this.teardownControlWs();
|
|
2452
|
+
};
|
|
2453
|
+
}).pipe(timeout$1({
|
|
2454
|
+
first: INSTANCE_READY_TIMEOUT,
|
|
2455
|
+
with: () => throwError(() => new Error(`No instanceReady within ${INSTANCE_READY_TIMEOUT}ms — requesting another machine`)),
|
|
2456
|
+
}), takeUntil(this.abort$));
|
|
2457
|
+
}
|
|
2288
2458
|
/**
|
|
2289
2459
|
* Poll the SAME `url` until ready, or until HTTP 4xx says the URL
|
|
2290
2460
|
* is dead.
|
|
@@ -2341,37 +2511,31 @@ class SignallingService extends SubService {
|
|
|
2341
2511
|
// still create a new socket, but in tests would re-emit the same
|
|
2342
2512
|
// (already-errored) mock observable.
|
|
2343
2513
|
return defer(() => {
|
|
2344
|
-
|
|
2514
|
+
// Only the Cirrus socket — the control socket belongs to
|
|
2515
|
+
// `requestCirrusViaSocket`, which already opened it to GET this `wsUrl`.
|
|
2516
|
+
this.closeCirrus({
|
|
2517
|
+
code: 1000,
|
|
2518
|
+
reason: DisconnectReason.NewConnectionAttempt,
|
|
2519
|
+
});
|
|
2345
2520
|
return createWebSocket(wsUrl);
|
|
2346
2521
|
}).pipe(takeUntil(this.abort$), tapLog('WS CREATED'), tap((ws) => {
|
|
2347
2522
|
this.commandTelemetryService.trackStartCommand('EXT-iceCandidate');
|
|
2348
2523
|
this.addWsHandlers(ws);
|
|
2349
2524
|
}));
|
|
2350
2525
|
}
|
|
2526
|
+
/**
|
|
2527
|
+
* Handlers for the CIRRUS socket — the only long-lived socket. Its close/error
|
|
2528
|
+
* are real stream events (`setCirrusDisconnected` / `dropConnection`), unlike
|
|
2529
|
+
* the control socket's, which never outlives {@link awaitInstanceReady}.
|
|
2530
|
+
*/
|
|
2351
2531
|
addWsHandlers(ws) {
|
|
2352
2532
|
ws.onmessage = OnMessageHandler.bind(this);
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
ws.onclose = (e) => this.onOrchestrationClose(e);
|
|
2356
|
-
ws.onerror = (e) => this.onOrchestrationError(e);
|
|
2533
|
+
ws.onclose = OnCloseHandler.bind(this);
|
|
2534
|
+
ws.onerror = OnErrorHandler.bind(this);
|
|
2357
2535
|
this.ws = ws;
|
|
2358
2536
|
Logger.log('WS HANDLERS ADDED');
|
|
2359
2537
|
OnOpenHandler.call(this);
|
|
2360
2538
|
}
|
|
2361
|
-
onOrchestrationClose(e) {
|
|
2362
|
-
if (this.streamMachineConnected) {
|
|
2363
|
-
Logger.log(`Orchestration socket closed after occupancy (benign): ${e.code}`);
|
|
2364
|
-
return;
|
|
2365
|
-
}
|
|
2366
|
-
OnCloseHandler.call(this, e);
|
|
2367
|
-
}
|
|
2368
|
-
onOrchestrationError(e) {
|
|
2369
|
-
if (this.streamMachineConnected) {
|
|
2370
|
-
Logger.log('Orchestration socket error after occupancy (benign)');
|
|
2371
|
-
return;
|
|
2372
|
-
}
|
|
2373
|
-
OnErrorHandler.call(this, e);
|
|
2374
|
-
}
|
|
2375
2539
|
/**
|
|
2376
2540
|
* Adopt a live signalling socket opened by the `stream-prefetch.js`
|
|
2377
2541
|
* head-script. The socket is already OPEN and `requestStream` was already
|
|
@@ -2388,97 +2552,32 @@ class SignallingService extends SubService {
|
|
|
2388
2552
|
this.ws = ws;
|
|
2389
2553
|
Logger.log('WS ADOPTED (handshake already done, requestStream already sent)');
|
|
2390
2554
|
}
|
|
2391
|
-
/**
|
|
2392
|
-
* Open the SECOND socket, directly to the stream machine's Cirrus
|
|
2393
|
-
* (`originInstanceUri` from `instanceReady`). The whole WebRTC signalling
|
|
2394
|
-
* (config/offer/answer/iceCandidate/ping) flows over this socket; the
|
|
2395
|
-
* orchestration socket (`this.ws`) stays open for control/lifecycle.
|
|
2396
|
-
*
|
|
2397
|
-
* "Just connect" — nothing is sent on open (the machine is already reserved
|
|
2398
|
-
* for this session): Cirrus pushes `config` and Unreal sends the `offer`.
|
|
2399
|
-
*/
|
|
2400
|
-
connectToStreamMachine(originInstanceUri) {
|
|
2401
|
-
if (!originInstanceUri) {
|
|
2402
|
-
Logger.warn('instanceReady without originInstanceUri — stream socket not opened');
|
|
2403
|
-
return;
|
|
2404
|
-
}
|
|
2405
|
-
// Guard: a stream socket is already live. A duplicate / re-sent
|
|
2406
|
-
// `instanceReady` must NOT tear down the active stream.
|
|
2407
|
-
if (this.streamMachineConnected) {
|
|
2408
|
-
Logger.warn('Stream machine already connected — ignoring duplicate instanceReady');
|
|
2409
|
-
return;
|
|
2410
|
-
}
|
|
2411
|
-
// Drop any prior (half-open) stream socket AND cancel any in-flight connect
|
|
2412
|
-
// before a fresh attempt — e.g. a retry that resolved to a different machine
|
|
2413
|
-
// or two instanceReady messages before the first connect resolves.
|
|
2414
|
-
this.teardownStreamWs();
|
|
2415
|
-
this.streamWsSub = createWebSocket(originInstanceUri)
|
|
2416
|
-
.pipe(takeUntil(this.abort$), takeUntil(this.destroy$), take(1))
|
|
2417
|
-
.subscribe({
|
|
2418
|
-
next: (ws) => {
|
|
2419
|
-
this.streamMachineRetryCount = 0;
|
|
2420
|
-
this.addStreamWsHandlers(ws);
|
|
2421
|
-
},
|
|
2422
|
-
error: (err) => {
|
|
2423
|
-
Logger.error('Stream-machine WS failed to connect', err?.message);
|
|
2424
|
-
this.retryStreamMachine();
|
|
2425
|
-
},
|
|
2426
|
-
});
|
|
2427
|
-
}
|
|
2428
|
-
/**
|
|
2429
|
-
* The stream-machine (Cirrus) connection failed. Re-request a machine over
|
|
2430
|
-
* the still-open orchestration socket: orchestration issues a new
|
|
2431
|
-
* streamRequestId and a fresh `instanceReady(originInstanceUri)`, which calls
|
|
2432
|
-
* {@link connectToStreamMachine} again on a different machine. Bounded by
|
|
2433
|
-
* {@link INNER_RETRY_LIMIT}; on exhaustion the stream is dropped.
|
|
2434
|
-
*/
|
|
2435
|
-
retryStreamMachine() {
|
|
2436
|
-
if (this.streamMachineRetryCount >= INNER_RETRY_LIMIT) {
|
|
2437
|
-
Logger.error('Stream-machine connection exhausted retries — dropping');
|
|
2438
|
-
this.store.dispatch(disconnectStream({
|
|
2439
|
-
reason: DisconnectReason.ReconnectFailed,
|
|
2440
|
-
message: 'Could not connect to the stream machine',
|
|
2441
|
-
}));
|
|
2442
|
-
return;
|
|
2443
|
-
}
|
|
2444
|
-
this.streamMachineRetryCount++;
|
|
2445
|
-
this.orchestrationRetrySub?.unsubscribe();
|
|
2446
|
-
this.orchestrationRetrySub = timer(POLLING_TIME)
|
|
2447
|
-
.pipe(takeUntil(this.abort$))
|
|
2448
|
-
.subscribe(() => this.sendRequestStream());
|
|
2449
|
-
}
|
|
2450
|
-
addStreamWsHandlers(ws) {
|
|
2451
|
-
ws.onmessage = OnMessageHandler.bind(this);
|
|
2452
|
-
ws.onclose = OnCloseHandler.bind(this);
|
|
2453
|
-
ws.onerror = OnErrorHandler.bind(this);
|
|
2454
|
-
this.streamWs = ws;
|
|
2455
|
-
this.streamMachineConnected = true;
|
|
2456
|
-
Logger.log('STREAM-MACHINE WS CONNECTED');
|
|
2457
|
-
}
|
|
2458
|
-
teardownStreamWs() {
|
|
2459
|
-
this.streamWsSub?.unsubscribe();
|
|
2460
|
-
this.teardownSocket(this.streamWs, 1000, DisconnectReason.NewConnectionAttempt);
|
|
2461
|
-
this.streamWs = null;
|
|
2462
|
-
this.streamMachineConnected = false;
|
|
2463
|
-
}
|
|
2464
2555
|
showStatusMessage(statusMessage) {
|
|
2465
2556
|
this.store.dispatch(setStatusMessage({ statusMessage }));
|
|
2466
2557
|
}
|
|
2467
2558
|
/**
|
|
2468
|
-
* Close the
|
|
2469
|
-
*
|
|
2559
|
+
* Close everything: the Cirrus socket AND the orchestration control socket.
|
|
2560
|
+
* Used on abort/teardown. A new connection attempt inside a live cycle uses
|
|
2561
|
+
* {@link closeCirrus} instead, so it does not kill the control socket that
|
|
2562
|
+
* produced the address it is about to connect to.
|
|
2470
2563
|
*/
|
|
2471
2564
|
close(data) {
|
|
2472
2565
|
const { code, reason } = data;
|
|
2473
|
-
this.onWebRtcAnswer$.next(null);
|
|
2474
2566
|
this.orchestrationRetrySub?.unsubscribe();
|
|
2475
|
-
this.
|
|
2567
|
+
this.closeCirrus(data);
|
|
2568
|
+
this.teardownControlWs(code, reason);
|
|
2569
|
+
}
|
|
2570
|
+
/** Close only the Cirrus signalling socket. */
|
|
2571
|
+
closeCirrus(data) {
|
|
2572
|
+
const { code, reason } = data;
|
|
2573
|
+
this.onWebRtcAnswer$.next(null);
|
|
2476
2574
|
this.teardownSocket(this.ws, code, reason);
|
|
2477
2575
|
this.ws = null;
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
this.
|
|
2481
|
-
this.
|
|
2576
|
+
}
|
|
2577
|
+
teardownControlWs(code = 1000, reason = DisconnectReason.NewConnectionAttempt) {
|
|
2578
|
+
this.teardownSocket(this.controlWs, code, reason);
|
|
2579
|
+
this.controlWs = null;
|
|
2580
|
+
this.orchestrationRetryCount = 0;
|
|
2482
2581
|
}
|
|
2483
2582
|
teardownSocket(ws, code, reason) {
|
|
2484
2583
|
if (!ws) {
|
|
@@ -2495,17 +2594,16 @@ class SignallingService extends SubService {
|
|
|
2495
2594
|
ws.close(code, reason);
|
|
2496
2595
|
}
|
|
2497
2596
|
}
|
|
2597
|
+
/**
|
|
2598
|
+
* Player signalling (offer/answer/iceCandidate/pong/p2pEstablished/subscribe)
|
|
2599
|
+
* — always the Cirrus socket, in every flow.
|
|
2600
|
+
*/
|
|
2498
2601
|
send(data) {
|
|
2499
2602
|
this.sendTo(this.ws, data);
|
|
2500
2603
|
}
|
|
2501
|
-
/**
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
* the new flow (once connected), or the orchestration/relay socket in the old
|
|
2505
|
-
* polling flow (where there is no separate stream socket).
|
|
2506
|
-
*/
|
|
2507
|
-
sendPlayerSignal(data) {
|
|
2508
|
-
this.sendTo(this.streamMachineConnected ? this.streamWs : this.ws, data);
|
|
2604
|
+
/** Orchestration control messages (`requestStream`) — the control socket. */
|
|
2605
|
+
sendToOrchestration(data) {
|
|
2606
|
+
this.sendTo(this.controlWs, data);
|
|
2509
2607
|
}
|
|
2510
2608
|
sendTo(ws, data) {
|
|
2511
2609
|
let dataString = data;
|
|
@@ -2535,21 +2633,45 @@ class SignallingService extends SubService {
|
|
|
2535
2633
|
Logger.colored(...COLOR_CODES.FROM_CIRRUS_ERROR, `Invalid CIRRUS message type: ${msg.type}`);
|
|
2536
2634
|
}
|
|
2537
2635
|
}
|
|
2538
|
-
|
|
2539
|
-
|
|
2636
|
+
/**
|
|
2637
|
+
* One handler map shared by both sockets — `handleMessage` dispatches purely by
|
|
2638
|
+
* message type. Which socket a type can arrive on is fixed, so registration is
|
|
2639
|
+
* split by origin: {@link setOrchestrationHandlers} (control socket) and
|
|
2640
|
+
* {@link setStreamHandlers} (Cirrus).
|
|
2641
|
+
*
|
|
2642
|
+
* Keepalive is deliberately absent: both servers ping at the WS **protocol**
|
|
2643
|
+
* level (`ws.ping()`), which the browser answers on its own — the page never
|
|
2644
|
+
* sees a ping and must never reply with an application-level `pong`. An
|
|
2645
|
+
* unregistered type is dropped silently by `handleMessage`, so a `ping` from a
|
|
2646
|
+
* not-yet-updated backend is simply ignored.
|
|
2647
|
+
*/
|
|
2648
|
+
setMessageHandlers() {
|
|
2649
|
+
this.setOrchestrationHandlers();
|
|
2650
|
+
this.setStreamHandlers();
|
|
2651
|
+
}
|
|
2652
|
+
/**
|
|
2653
|
+
* Messages from the ORCHESTRATION control socket. Only reachable during the
|
|
2654
|
+
* provisioning wait — `requestCirrusViaSocket` closes that socket the moment
|
|
2655
|
+
* `instanceReady` lands.
|
|
2656
|
+
*/
|
|
2657
|
+
setOrchestrationHandlers() {
|
|
2658
|
+
// `instanceReserved` (eta/progress) and `instanceReady` (the Cirrus address)
|
|
2659
|
+
// ARE the request/response pair of `requestCirrusViaSocket`.
|
|
2660
|
+
this.wsMsgHandlers.instanceReserved = InstanceReservedHandler.bind(this);
|
|
2661
|
+
this.wsMsgHandlers.instanceReady = InstanceReadyHandler.bind(this);
|
|
2662
|
+
// Orchestration-side refusals (4001 no instances / 4002 limit reached) →
|
|
2663
|
+
// resend `requestStream`, bounded by `INSTANCE_READY_TIMEOUT`.
|
|
2540
2664
|
this.wsMsgHandlers.error = OrchestrationErrorHandler.bind(this);
|
|
2665
|
+
}
|
|
2666
|
+
/** Messages from the CIRRUS socket — the actual stream signalling. */
|
|
2667
|
+
setStreamHandlers() {
|
|
2541
2668
|
this.wsMsgHandlers.config = ConfigHandler.bind(this);
|
|
2669
|
+
this.wsMsgHandlers.offer = OfferHandler.bind(this);
|
|
2542
2670
|
this.wsMsgHandlers.answer = AnswerHandler.bind(this);
|
|
2671
|
+
this.wsMsgHandlers.iceCandidate = IceCandidateHandler.bind(this);
|
|
2543
2672
|
this.wsMsgHandlers.ssInfo = SSInfoHandler.bind(this);
|
|
2544
2673
|
this.wsMsgHandlers.playerCount = PlayerCountHandler.bind(this);
|
|
2545
2674
|
this.wsMsgHandlers.streamerList = StreamerListHandler.bind(this);
|
|
2546
|
-
this.wsMsgHandlers.iceCandidate = IceCandidateHandler.bind(this);
|
|
2547
|
-
this.wsMsgHandlers.instanceReady = InstanceReadyHandler.bind(this);
|
|
2548
|
-
this.wsMsgHandlers.instanceReserved = InstanceReservedHandler.bind(this);
|
|
2549
|
-
this.wsMsgHandlers.playerDisconnected = PlayerDisconnectHandler.bind(this);
|
|
2550
|
-
this.wsMsgHandlers.streamerDisconnected =
|
|
2551
|
-
StreamerDisconnectHandler.bind(this);
|
|
2552
|
-
this.wsMsgHandlers.offer = OfferHandler.bind(this);
|
|
2553
2675
|
}
|
|
2554
2676
|
sendRequestStream() {
|
|
2555
2677
|
const context = this.streamRequestContext();
|
|
@@ -2557,7 +2679,7 @@ class SignallingService extends SubService {
|
|
|
2557
2679
|
if (!context || !environmentId) {
|
|
2558
2680
|
return;
|
|
2559
2681
|
}
|
|
2560
|
-
this.
|
|
2682
|
+
this.sendToOrchestration({
|
|
2561
2683
|
type: OrchestrationMessageTypes.requestStream,
|
|
2562
2684
|
correlationId: this.correlationId,
|
|
2563
2685
|
payload: {
|
|
@@ -2568,18 +2690,12 @@ class SignallingService extends SubService {
|
|
|
2568
2690
|
});
|
|
2569
2691
|
}
|
|
2570
2692
|
/**
|
|
2571
|
-
*
|
|
2572
|
-
*
|
|
2573
|
-
*
|
|
2574
|
-
* - NEW flow (`streamMachineConnected`): the stream-machine socket
|
|
2575
|
-
* (`streamWs`). The orchestration socket (`this.ws`) is intentionally
|
|
2576
|
-
* closed after occupancy, so checking it would wrongly report "not
|
|
2577
|
-
* connected" and block reconnect.
|
|
2578
|
-
* - OLD polling flow: the single relay socket (`this.ws`) — unchanged.
|
|
2693
|
+
* Is the player-signalling socket open? Always the Cirrus socket — the
|
|
2694
|
+
* control socket's state is irrelevant to the stream (orchestration closes it
|
|
2695
|
+
* once the instance is occupied), so it must not be part of this gate.
|
|
2579
2696
|
*/
|
|
2580
2697
|
isConnected() {
|
|
2581
|
-
|
|
2582
|
-
return !!(activeWs && activeWs.readyState === WS_OPEN_STATE);
|
|
2698
|
+
return !!(this.ws && this.ws.readyState === WS_OPEN_STATE);
|
|
2583
2699
|
}
|
|
2584
2700
|
getRegion() {
|
|
2585
2701
|
Logger.log('Start Getting Region');
|
|
@@ -3498,7 +3614,7 @@ class WebRtcPlayerService extends SubService {
|
|
|
3498
3614
|
});
|
|
3499
3615
|
fromEvent(pcClient, 'icecandidate')
|
|
3500
3616
|
.pipe(map$1((e) => e.candidate), filter(Truthy), tap$1((candidate) => Logger.info('[Browser ICE candidate]', '| Type=', candidate.type, '| Protocol=', candidate.protocol, '| Address=', candidate.address, '| Port=', candidate.port, '|')), takeUntil(merge(this.disconnect$, this.destroy$)))
|
|
3501
|
-
.subscribe((candidate) => this.signallingSrv.
|
|
3617
|
+
.subscribe((candidate) => this.signallingSrv.send({
|
|
3502
3618
|
type: 'iceCandidate',
|
|
3503
3619
|
candidate,
|
|
3504
3620
|
}));
|
|
@@ -3648,7 +3764,7 @@ class WebRtcPlayerService extends SubService {
|
|
|
3648
3764
|
const offer = await pcClient.createOffer();
|
|
3649
3765
|
offer.sdp = this.mungeSDP(offer.sdp ?? '');
|
|
3650
3766
|
await pcClient.setLocalDescription(offer);
|
|
3651
|
-
this.signallingSrv.
|
|
3767
|
+
this.signallingSrv.send(offer);
|
|
3652
3768
|
}
|
|
3653
3769
|
catch {
|
|
3654
3770
|
Logger.error("Couldn't create offer");
|
|
@@ -3663,7 +3779,7 @@ class WebRtcPlayerService extends SubService {
|
|
|
3663
3779
|
const answer = await pcClient.createAnswer();
|
|
3664
3780
|
answer.sdp = this.mungeSDP(answer.sdp ?? '');
|
|
3665
3781
|
await pcClient.setLocalDescription(answer);
|
|
3666
|
-
this.signallingSrv.
|
|
3782
|
+
this.signallingSrv.send(answer);
|
|
3667
3783
|
// Observability signal for the signaling waterfall — the handshake
|
|
3668
3784
|
// went "answer sent" between `offer received` and `datachannel_ready`.
|
|
3669
3785
|
this.store.dispatch(answerSent());
|
|
@@ -3788,6 +3904,7 @@ class StreamAdoptionService {
|
|
|
3788
3904
|
this.store.dispatch(streamAdopted({
|
|
3789
3905
|
machineId: parked.machineId ?? null,
|
|
3790
3906
|
ssData: parked.ssData ?? null,
|
|
3907
|
+
streamRequestId: parked.ids?.streamRequestId || null,
|
|
3791
3908
|
timings: parked.timings ?? null,
|
|
3792
3909
|
}));
|
|
3793
3910
|
// Hand the live socket over without re-sending `requestStream`, reusing the
|
|
@@ -4089,9 +4206,12 @@ class UnrealEffects {
|
|
|
4089
4206
|
return this.actions$.pipe(ofType(setAwsInstance),
|
|
4090
4207
|
// require wsUrl present on the action payload
|
|
4091
4208
|
filter((action) => !!action.wsUrl),
|
|
4092
|
-
// for each awsInstance action capture one
|
|
4209
|
+
// for each awsInstance action capture one connectionId snapshot, then wait
|
|
4210
|
+
// for dataChannelConnected. The gate stays on `ssInfo` (unchanged timing);
|
|
4211
|
+
// the id itself now comes from the orchestration when it issued one, and
|
|
4212
|
+
// falls back to the `ssInfo` value otherwise.
|
|
4093
4213
|
switchMap((awsInstance) => {
|
|
4094
|
-
return this.actions$.pipe(ofType(updateCirrusInfo), take(1), switchMap((
|
|
4214
|
+
return this.actions$.pipe(ofType(updateCirrusInfo), take(1), withLatestFrom(this.store.select(selectCirrusConnectionId)), switchMap(([, connectionId]) => {
|
|
4095
4215
|
const abort$ = this.actions$.pipe(ofType(setCirrusDisconnected, // ← "Cirrus closed" action — direct,
|
|
4096
4216
|
// no fan-in tick delay
|
|
4097
4217
|
abortEstablishingConnection), take(1));
|
|
@@ -4108,12 +4228,20 @@ class UnrealEffects {
|
|
|
4108
4228
|
Logger.error(error);
|
|
4109
4229
|
const body = {
|
|
4110
4230
|
awsInstance,
|
|
4111
|
-
connectionId
|
|
4231
|
+
connectionId,
|
|
4112
4232
|
href: location.href,
|
|
4113
4233
|
userAgent: navigator.userAgent,
|
|
4114
4234
|
error,
|
|
4115
4235
|
};
|
|
4116
|
-
|
|
4236
|
+
// Only the polling flow has an HTTP endpoint to report to;
|
|
4237
|
+
// `pollingUrl` is a `wss://` orchestration address otherwise.
|
|
4238
|
+
const reportUrl = awsInstance.pollingUrl?.startsWith('http')
|
|
4239
|
+
? awsInstance.pollingUrl
|
|
4240
|
+
: null;
|
|
4241
|
+
const report$ = reportUrl
|
|
4242
|
+
? this.http.post(reportUrl, body)
|
|
4243
|
+
: of(null);
|
|
4244
|
+
return report$.pipe(catchError((postError) => {
|
|
4117
4245
|
Logger.error('Error sending timeout info:', postError);
|
|
4118
4246
|
return of(null);
|
|
4119
4247
|
}),
|
|
@@ -8044,5 +8172,5 @@ function provideUnrealModule() {
|
|
|
8044
8172
|
* Generated bundle index. Do not edit.
|
|
8045
8173
|
*/
|
|
8046
8174
|
|
|
8047
|
-
export { AFKService, AUTO_HIGH_RESOLUTION_IDLE_MS, AfkTimeoutModalComponent, AggregatorService, AnalyticsService, AnswerHandler, CommandTelemetryService, ConfigHandler, ConsoleExtensionsService, ControlScheme, DATA_CHANNEL_CONNECTION_TIMEOUT, DEBOUNCE_TO_MANY_RESIZE_CALLS, DEFAULT_AFK_TIMEOUT, DEFAULT_AFK_TIMEOUT_PERIOD, DESTROY_DISCONNECT_REASONS, DevModeService, DisconnectReason, FULL_HD_HEIGHT, FULL_HD_WIDTH, FileHandlerService, FileReceiverService, FpsMonitorService, FreezeFrameComponent, FreezeFrameService, INNER_RETRY_LIMIT, IceCandidateHandler, ImageLoadingSrcComponent, InputOptions, InputService, InstanceReadyHandler, InstanceReservedHandler, IntroSrcComponent, LatencyTimings, LoadingProgressService, MINIMAL_FPS, MIN_CONTAINER_SIZE, MessageType, MouseButton, MouseButtonsMask, OUTER_RETRY_DELAY, OnCloseHandler, OnErrorHandler, OnMessageHandler, OnOpenHandler, OrchestrationMessageTypes, POLLING_TIME,
|
|
8175
|
+
export { AFKService, AUTO_HIGH_RESOLUTION_IDLE_MS, AfkTimeoutModalComponent, AggregatorService, AnalyticsService, AnswerHandler, CommandTelemetryService, ConfigHandler, ConsoleExtensionsService, ControlScheme, DATA_CHANNEL_CONNECTION_TIMEOUT, DEBOUNCE_TO_MANY_RESIZE_CALLS, DEFAULT_AFK_TIMEOUT, DEFAULT_AFK_TIMEOUT_PERIOD, DESTROY_DISCONNECT_REASONS, DevModeService, DisconnectReason, FULL_HD_HEIGHT, FULL_HD_WIDTH, FileHandlerService, FileReceiverService, FpsMonitorService, FreezeFrameComponent, FreezeFrameService, INNER_RETRY_LIMIT, INSTANCE_READY_TIMEOUT, IceCandidateHandler, ImageLoadingSrcComponent, InputOptions, InputService, InstanceReadyHandler, InstanceReservedHandler, IntroSrcComponent, LatencyTimings, LoadingProgressService, MINIMAL_FPS, MIN_CONTAINER_SIZE, MessageType, MouseButton, MouseButtonsMask, OUTER_RETRY_DELAY, OnCloseHandler, OnErrorHandler, OnMessageHandler, OnOpenHandler, OrchestrationMessageTypes, POLLING_TIME, PlayerCountHandler, RECONNECT_CONFIG, REGION_PING_ACCURATE_SAMPLES, REGION_PING_BACKGROUND_REFRESH_DELAY_MS, REGION_PING_CACHE_KEY, REGION_PING_CACHE_TTL_MS, REGION_PING_WARMUP_DEADLINE_MS, RegionsPingService, SAME_SIZE_THRESHOLD, SCREEN_LOCKER_CONTAINER_ID, SIGNALLING_PERCENT_VALUE, SSInfoHandler, STREAMING_AUDIO_ID, STREAMING_VIDEO_ID, SafePipe, SignallingService, SpecialKeyCodes, StatGraphComponent, StreamAdoptionService, StreamStatusTelemetryService, StreamStatusTelemetryService2, SubService, ToClientMessage, UNREAL_CONFIG, UnrealCallbackService, UnrealCommunicatorService, UnrealEffects, UnrealSceneComponent, UnrealStatusMessage, VideoService, VideoStatsComponent, WSCloseCode_CIRRUS_ABNORMAL_CLOSURE, WSCloseCode_CIRRUS_MAX_PLAYERS_ERROR, WSCloseCode_CIRRUS_PLAYER_DISCONNECTED, WSCloseCode_CIRRUS_STREAMER_KIKED_PLAYER, WSCloseCode_FORCE_CIRRUS_CLOSE, WSCloseCode_NORMAL_AFK_TIMEOUT, WSCloseCode_NORMAL_CLOSURE, WSCloseCode_NORMAL_MANUAL_DISCONNECT, WSCloseCode_UNKNOWN, WSCloseCodes, WS_OPEN_STATE, WS_RETRY_PER_INSTANCE, WS_TIMEOUT, WebRtcPlayerService, WebrtcErrorModalComponent, abortEstablishingConnection, answerSent, changeStatusMainVideoOnScene, changeStreamResolutionAction, commandCompleted, commandStarted, dataChannelConnected, dataChannelReady, decodeData, destroyRemoteConnections, destroyUnrealScene, disconnectStream, dispatchResize, dropConnection, dropDC, dropPC, floatToPercents, floatToSmoothPercents, forceResizeViewport, fromResizeObserver, getActiveUrl, getImageFromVideoStream, getResizeStudioValues, getResizeValuesUniversal, getRtcErrorMessage, iceConnectionFailed, initSignalling, instanceReady, keepMaxUntilReset, makeEven, pollingEnded, pollingStarted, provideUnrealModule, provideUnrealState, qpToQuality, reconnectPeer, reconnectPeerFailed, reconnectPeerSuccess, regionPingFetchEnd, regionPingFetchStart, regionResolved, resetAfk, resetConfig, resetDataChannelForReconnect, resetIntroSrc, resetUserInactivityTimeout, resetWarnTimeout, saveAnalyticsEvent, selectCirrusConnectionId, selectClientAndViewIds, selectCommandProgress, selectFreezeFrameCombinedDataUrl, selectFreezeFrameDataUrl, selectFreezeFrameDataUrlFromVideo, selectFreezeFrameProgressMessageFromVideo, selectIsAutostart, selectIsExistMatchUrls, selectIsFreezeFrameLoading, selectIsFullyConnected, selectIsLoaderScreenVisible, selectIsVideoPlayingAndDataChannelConnected, selectShowLoader, selectShowReconnectPopup, selectSignalingParameters, selectStreamConfig, selectTotalProgress, selectUserInactivityDetected, selectWarnTimeout, setAfkTimerCountdown, setAfkTimerHide, setAfkTimerVisible, setAwsInstance, setCirrusConnected, setCirrusDisconnected, setConfig, setEstablishingConnection, setFreezeFrame, setFreezeFrameFromVideo, setIntroImageSrc, setIntroVideoSrc, setLoadingImageSrc, setLoopBackCommandIsCompleted, setMaxFps, setOrchestrationContext, setOrchestrationMessage, setOrchestrationParameters, setOrchestrationProgress, setSignalingName, setStatusMessage, setStatusPercentSignallingServer, setStreamClientCompanyId, setStreamViewId, setUnrealPlaywrightConfig, setUserInactivityTimeout, setViewportNotReady, setViewportReady, showLoadingProgress, showResumeStreamPopup, showUnrealErrorMessage, showUserInactivityTimeoutPopup, signalingRetryAttempted, smoothTransition, startStream, streamAdopted, trackMixpanelEvent, trackTelemetryData, uint8ToBase64, unrealCommandCallback, unrealFeature, unrealReducer, updateCirrusInfo, videoAdaptedToContainer, videoStreamInitialized };
|
|
8048
8176
|
//# sourceMappingURL=3dsource-angular-unreal-module.mjs.map
|