@3dsource/angular-unreal-module 0.0.163 → 0.0.165
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, streamRequestId }) => {
|
|
487
|
+
}), on(setAwsInstance, (state, { instanceName, wsUrl, pollingUrl, streamRequestId, kind }) => {
|
|
483
488
|
return {
|
|
484
489
|
...state,
|
|
485
490
|
awsInstance: {
|
|
@@ -488,7 +493,11 @@ const unrealReducer = createReducer(initialState, on(changeStatusMainVideoOnScen
|
|
|
488
493
|
wsUrl,
|
|
489
494
|
pollingUrl,
|
|
490
495
|
streamRequestId,
|
|
491
|
-
|
|
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,
|
|
492
501
|
},
|
|
493
502
|
};
|
|
494
503
|
}), on(setViewportReady, (state) => {
|
|
@@ -594,6 +603,10 @@ const unrealReducer = createReducer(initialState, on(changeStatusMainVideoOnScen
|
|
|
594
603
|
instanceName,
|
|
595
604
|
streamRequestId,
|
|
596
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',
|
|
597
610
|
},
|
|
598
611
|
};
|
|
599
612
|
}), on(setOrchestrationProgress, (state, { progressComplete }) => {
|
|
@@ -643,7 +656,7 @@ const unrealReducer = createReducer(initialState, on(changeStatusMainVideoOnScen
|
|
|
643
656
|
return { ...state, analyticsEvent: event };
|
|
644
657
|
}), on(setCirrusConnected, (state) => {
|
|
645
658
|
return { ...state, cirrusConnected: true };
|
|
646
|
-
}), on(streamAdopted, (state, { machineId, ssData, streamRequestId }) => {
|
|
659
|
+
}), on(streamAdopted, (state, { machineId, pollingUrl, ssData, streamRequestId }) => {
|
|
647
660
|
return {
|
|
648
661
|
...state,
|
|
649
662
|
cirrusConnected: true,
|
|
@@ -652,7 +665,10 @@ const unrealReducer = createReducer(initialState, on(changeStatusMainVideoOnScen
|
|
|
652
665
|
awsInstance: {
|
|
653
666
|
...state.awsInstance,
|
|
654
667
|
wsUrl: machineId,
|
|
655
|
-
|
|
668
|
+
// The endpoint the prefetch polled. It is the only record of where the
|
|
669
|
+
// adopted stream came from — `connectToSignaling` never ran, so no
|
|
670
|
+
// `setAwsInstance` will fill this in later.
|
|
671
|
+
pollingUrl: pollingUrl ?? state.awsInstance.pollingUrl,
|
|
656
672
|
instanceName: 'Adopted Connection',
|
|
657
673
|
streamRequestId: streamRequestId ?? state.awsInstance.streamRequestId,
|
|
658
674
|
progressComplete: 0,
|
|
@@ -780,13 +796,16 @@ function InstanceReadyHandler(msg) {
|
|
|
780
796
|
Logger.warn('instanceReady received without payload — ignoring');
|
|
781
797
|
return;
|
|
782
798
|
}
|
|
799
|
+
if (!payload.originInstanceUri) {
|
|
800
|
+
Logger.warn('instanceReady without originInstanceUri — ignoring');
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
783
803
|
// Emit for observability (telemetry) and store the stream-machine coords.
|
|
784
804
|
this.store.dispatch(instanceReady(payload));
|
|
785
|
-
//
|
|
786
|
-
//
|
|
787
|
-
//
|
|
788
|
-
|
|
789
|
-
this.connectToStreamMachine(payload.originInstanceUri, payload.streamRequestId);
|
|
805
|
+
// This is the answer `requestCirrusViaSocket` is waiting for: the Cirrus
|
|
806
|
+
// address (`originInstanceUri`) plus the connection id to hand it. The socket
|
|
807
|
+
// itself is opened by the shared `connectToCirrus`, not here.
|
|
808
|
+
this.onInstanceReady$.next(payload);
|
|
790
809
|
}
|
|
791
810
|
|
|
792
811
|
function InstanceReservedHandler(msg) {
|
|
@@ -797,15 +816,6 @@ function InstanceReservedHandler(msg) {
|
|
|
797
816
|
}));
|
|
798
817
|
}
|
|
799
818
|
|
|
800
|
-
function PingHandler() {
|
|
801
|
-
// Reply pong on the active signalling socket (stream socket in the new flow,
|
|
802
|
-
// relay socket in the old polling flow).
|
|
803
|
-
this.sendPlayerSignal(JSON.stringify({
|
|
804
|
-
type: 'pong',
|
|
805
|
-
source: 'front',
|
|
806
|
-
}));
|
|
807
|
-
}
|
|
808
|
-
|
|
809
819
|
function PlayerCountHandler(msg) {
|
|
810
820
|
Logger.log(msg);
|
|
811
821
|
}
|
|
@@ -861,9 +871,16 @@ function OnMessageHandler(e) {
|
|
|
861
871
|
}
|
|
862
872
|
}
|
|
863
873
|
|
|
874
|
+
/**
|
|
875
|
+
* Open handler of the CIRRUS socket only (`addWsHandlers`), so
|
|
876
|
+
* `setCirrusConnected` now means what it says. It used to also send
|
|
877
|
+
* `requestStream` here, which made the flag fire on the orchestration control
|
|
878
|
+
* socket — a whole provisioning wait (`eta`, ~150 s) before any Cirrus existed.
|
|
879
|
+
* `requestStream` belongs to the control socket and is sent by
|
|
880
|
+
* `requestCirrusViaSocket`.
|
|
881
|
+
*/
|
|
864
882
|
function OnOpenHandler() {
|
|
865
883
|
this.store.dispatch(setCirrusConnected());
|
|
866
|
-
this.sendRequestStream();
|
|
867
884
|
}
|
|
868
885
|
|
|
869
886
|
/**
|
|
@@ -1183,7 +1200,9 @@ const selectSignalingParameters = createSelector(unrealFeature.selectSsData, unr
|
|
|
1183
1200
|
info.push(`Unreal App Version: ${data.streamerVersion}`);
|
|
1184
1201
|
info.push(`APP: ${data.streamPath}${data.streamApp}`);
|
|
1185
1202
|
}
|
|
1186
|
-
|
|
1203
|
+
// The endpoint that handed us the Cirrus address: an HTTP polling URL in one
|
|
1204
|
+
// flow, a `wss://` orchestration URL in the other.
|
|
1205
|
+
info.push(`Orchestration: ${instance.pollingUrl}`);
|
|
1187
1206
|
info.push(`WS: ${instance.wsUrl}`);
|
|
1188
1207
|
return info.join('<br>');
|
|
1189
1208
|
});
|
|
@@ -1203,13 +1222,15 @@ const selectIsLoaderScreenVisible = createSelector(selectIsFullyConnected, (full
|
|
|
1203
1222
|
*/
|
|
1204
1223
|
const selectTotalProgress = createSelector(unrealFeature.selectStatusPercentSignallingServer, selectCommandProgress, unrealFeature.selectAwsInstance, unrealFeature.selectDataChannelConnected, (statusPercentSignallingServer, commandProgress, awsInstance, dataChannelConnected) => {
|
|
1205
1224
|
const SIGNALLING_MAX = SIGNALLING_PERCENT_VALUE / 100;
|
|
1206
|
-
|
|
1207
|
-
|
|
1225
|
+
// Stated by the service (`AwsInstance.kind`), not sniffed from the hostname.
|
|
1226
|
+
// In the socket flow it is set by `instanceReserved` — i.e. before
|
|
1227
|
+
// `setAwsInstance` — so the eta-driven bar is picked up from the start of
|
|
1228
|
+
// the provisioning wait.
|
|
1229
|
+
const isNewOrchestration = awsInstance?.kind === 'socket';
|
|
1208
1230
|
// Once the data channel is connected, signaling is fully complete (1.0) —
|
|
1209
1231
|
// regardless of orchestration type. This is what makes an ADOPTED stream
|
|
1210
|
-
// (
|
|
1211
|
-
//
|
|
1212
|
-
// whole first phase done instead of stalling near the command-only value.
|
|
1232
|
+
// (which never set `statusPercentSignallingServer`) correctly show the whole
|
|
1233
|
+
// first phase done instead of stalling near the command-only value.
|
|
1213
1234
|
// Otherwise: new orchestration reports 0–1 progress; legacy uses the
|
|
1214
1235
|
// polling percent.
|
|
1215
1236
|
const signalingProgress = dataChannelConnected
|
|
@@ -1832,10 +1853,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImpor
|
|
|
1832
1853
|
}], ctorParameters: () => [] });
|
|
1833
1854
|
|
|
1834
1855
|
function StreamerListHandler(msg) {
|
|
1835
|
-
// `subscribe` is player signalling
|
|
1836
|
-
|
|
1837
|
-
// matching the socket the `streamerList` arrived on.
|
|
1838
|
-
this.sendPlayerSignal(JSON.stringify({
|
|
1856
|
+
// `subscribe` is player signalling → the Cirrus socket.
|
|
1857
|
+
this.send(JSON.stringify({
|
|
1839
1858
|
type: 'subscribe',
|
|
1840
1859
|
streamerId: msg.ids[0],
|
|
1841
1860
|
}));
|
|
@@ -2042,19 +2061,6 @@ function createWebSocket(wsUrl, timeoutMs = WS_TIMEOUT) {
|
|
|
2042
2061
|
}));
|
|
2043
2062
|
}
|
|
2044
2063
|
|
|
2045
|
-
function PlayerDisconnectHandler() {
|
|
2046
|
-
this.store.dispatch(disconnectStream({
|
|
2047
|
-
reason: DisconnectReason.OrchestrationPlayerDisconnected,
|
|
2048
|
-
message: 'Orchestration: PlayerDisconnectHandler',
|
|
2049
|
-
}));
|
|
2050
|
-
}
|
|
2051
|
-
function StreamerDisconnectHandler() {
|
|
2052
|
-
this.store.dispatch(disconnectStream({
|
|
2053
|
-
reason: DisconnectReason.OrchestrationStreamerDisconnected,
|
|
2054
|
-
message: 'Orchestration: StreamerDisconnectHandler',
|
|
2055
|
-
}));
|
|
2056
|
-
}
|
|
2057
|
-
|
|
2058
2064
|
// Structured constants holding code + description
|
|
2059
2065
|
const ORCHESTRATION_ERROR_CODES = {
|
|
2060
2066
|
NO_AVAILABLE_INSTANCES: {
|
|
@@ -2079,6 +2085,16 @@ function OrchestrationErrorHandler(msg) {
|
|
|
2079
2085
|
if (RETRY_CODES.has(msg.payload.code)) {
|
|
2080
2086
|
const errorInfo = Object.values(ORCHESTRATION_ERROR_CODES).find(({ code }) => code === msg.payload.code);
|
|
2081
2087
|
this.store.dispatch(setOrchestrationMessage({ message: errorInfo?.description }));
|
|
2088
|
+
// Same telemetry channel as the WS/URL/region retry tiers — this branch used
|
|
2089
|
+
// to resend silently, so `kind: 'orchestration'` was declared but never seen.
|
|
2090
|
+
// No count cap here on purpose: the wait is bounded by
|
|
2091
|
+
// `INSTANCE_READY_TIMEOUT` in `requestCirrusViaSocket`, and on expiry the
|
|
2092
|
+
// URL-rotating tier takes over.
|
|
2093
|
+
this.store.dispatch(signalingRetryAttempted({
|
|
2094
|
+
attempt: ++this.orchestrationRetryCount,
|
|
2095
|
+
reason: errorInfo?.description ?? `code ${msg.payload.code}`,
|
|
2096
|
+
kind: 'orchestration',
|
|
2097
|
+
}));
|
|
2082
2098
|
this.orchestrationRetrySub?.unsubscribe();
|
|
2083
2099
|
this.orchestrationRetrySub = timer(POLLING_TIME)
|
|
2084
2100
|
.pipe(takeUntil(this.destroy$))
|
|
@@ -2102,21 +2118,27 @@ class SignallingService extends SubService {
|
|
|
2102
2118
|
this.onConfig$ = new Subject();
|
|
2103
2119
|
this.onWebRtcIce$ = new Subject();
|
|
2104
2120
|
this.onWebRtcAnswer$ = new Subject();
|
|
2121
|
+
// Fed by `InstanceReadyHandler`. `requestCirrusViaSocket` awaits it the same
|
|
2122
|
+
// way `requestCirrusViaPolling` awaits a ready polling response — it is the
|
|
2123
|
+
// socket flow's "here is your Cirrus URL" answer.
|
|
2124
|
+
this.onInstanceReady$ = new Subject();
|
|
2105
2125
|
// Pure signal — no side-effects. The `close()` is wired ONCE in init()
|
|
2106
2126
|
// below so a single abort event triggers exactly one close, regardless
|
|
2107
2127
|
// of how many pipelines have `takeUntil(this.abort$)` attached.
|
|
2108
2128
|
this.abort$ = this.actions$.pipe(ofType(abortEstablishingConnection));
|
|
2129
|
+
// The CIRRUS signalling socket — the only socket that speaks WebRTC
|
|
2130
|
+
// (config/offer/answer/iceCandidate/ping). Same field in all three flows:
|
|
2131
|
+
// the polled `signallingServer`, the stream machine (`originInstanceUri`), or
|
|
2132
|
+
// a direct dev URL.
|
|
2109
2133
|
this.ws = null;
|
|
2110
|
-
//
|
|
2111
|
-
//
|
|
2112
|
-
//
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
//
|
|
2116
|
-
//
|
|
2117
|
-
this.
|
|
2118
|
-
// Bounded retry counter for the stream-machine connection (new-flow only).
|
|
2119
|
-
this.streamMachineRetryCount = 0;
|
|
2134
|
+
// The orchestration CONTROL socket (socket flow only). Opened by
|
|
2135
|
+
// `requestCirrusViaSocket` to ask for a machine and closed the moment
|
|
2136
|
+
// `instanceReady` arrives — per the backend contract nothing uses it after the
|
|
2137
|
+
// handover, so it is never left open with dangling handlers.
|
|
2138
|
+
this.controlWs = null;
|
|
2139
|
+
// `requestStream` resends driven by `OrchestrationErrorHandler` within one
|
|
2140
|
+
// `requestCirrusViaSocket` wait. Reported as `kind: 'orchestration'` retries.
|
|
2141
|
+
this.orchestrationRetryCount = 0;
|
|
2120
2142
|
this.wsMsgHandlers = {};
|
|
2121
2143
|
this.init();
|
|
2122
2144
|
}
|
|
@@ -2124,7 +2146,7 @@ class SignallingService extends SubService {
|
|
|
2124
2146
|
if (this.unrealConfig?.playwright) {
|
|
2125
2147
|
return;
|
|
2126
2148
|
}
|
|
2127
|
-
this.
|
|
2149
|
+
this.setMessageHandlers();
|
|
2128
2150
|
// Single owner of the "close on abort" side-effect. Previously this
|
|
2129
2151
|
// lived as a `tap` inside `abort$` itself, which made every
|
|
2130
2152
|
// `takeUntil(this.abort$)` subscriber fire its own close().
|
|
@@ -2139,13 +2161,12 @@ class SignallingService extends SubService {
|
|
|
2139
2161
|
// and `init()` runs once in the constructor. If these subs were torn
|
|
2140
2162
|
// down by abort$, a second in-injector session (e.g. navigating between
|
|
2141
2163
|
// two routes that both host <app-unreal-scene>) would never send
|
|
2142
|
-
// `p2pEstablished`. `
|
|
2164
|
+
// `p2pEstablished`. `send()` is already a no-op on a closed WS,
|
|
2143
2165
|
// so there's no risk of stray messages.
|
|
2144
2166
|
this.actions$
|
|
2145
2167
|
.pipe(ofType(dataChannelReady), takeUntil(this.destroy$))
|
|
2146
2168
|
.subscribe(() => {
|
|
2147
|
-
|
|
2148
|
-
this.sendPlayerSignal({
|
|
2169
|
+
this.send({
|
|
2149
2170
|
type: 'p2pEstablished',
|
|
2150
2171
|
source: 'front',
|
|
2151
2172
|
sessionId: generateUuid(),
|
|
@@ -2196,7 +2217,7 @@ class SignallingService extends SubService {
|
|
|
2196
2217
|
}
|
|
2197
2218
|
/**
|
|
2198
2219
|
* One inner cycle: rotate `urlGen` over the pool, resolve via
|
|
2199
|
-
* {@link
|
|
2220
|
+
* {@link requestCirrus}, open WS via {@link connectToCirrus},
|
|
2200
2221
|
* retry up to {@link INNER_RETRY_LIMIT} on any hard failure. The
|
|
2201
2222
|
* urlGen lives in this closure so retries advance through the
|
|
2202
2223
|
* pool. Exhaustion bubbles to {@link connectToSignaling}'s outer
|
|
@@ -2204,12 +2225,20 @@ class SignallingService extends SubService {
|
|
|
2204
2225
|
*/
|
|
2205
2226
|
connectAttempt(urls) {
|
|
2206
2227
|
const urlGen = getActiveUrl(urls);
|
|
2207
|
-
return defer(() =>
|
|
2228
|
+
return defer(() => {
|
|
2229
|
+
// One control socket per ATTEMPT, whatever transport this attempt picks —
|
|
2230
|
+
// a rotation from a `wss://` orchestration URL to an `http://` one would
|
|
2231
|
+
// otherwise leave the previous control socket behind. Deliberately NOT
|
|
2232
|
+
// inside the WS-only tier below: that tier reuses the same resolved
|
|
2233
|
+
// machine, so its control socket must survive.
|
|
2234
|
+
this.teardownControlWs();
|
|
2235
|
+
return this.requestCirrus(`${urlGen.next().value}`);
|
|
2236
|
+
}).pipe(tap((awsInstance) => this.store.dispatch(setAwsInstance(awsInstance))), switchMap(({ wsUrl, streamRequestId }) =>
|
|
2208
2237
|
// ── WS-only retry tier ─────────────────────────────────────────────
|
|
2209
2238
|
// Tries the SAME `wsUrl` up to `WS_RETRY_PER_INSTANCE` extra times
|
|
2210
2239
|
// before letting the error bubble up to the outer (URL-rotating)
|
|
2211
2240
|
// retry. This is important because the outer retry calls
|
|
2212
|
-
// `
|
|
2241
|
+
// `requestCirrus(url)` again, which asks orchestration again,
|
|
2213
2242
|
// which causes orchestrator to RESERVE A NEW instance — the
|
|
2214
2243
|
// previous reservation is orphaned (server-side leak).
|
|
2215
2244
|
//
|
|
@@ -2218,13 +2247,10 @@ class SignallingService extends SubService {
|
|
|
2218
2247
|
// burning a second reservation. If WS still fails after these
|
|
2219
2248
|
// extra attempts, the instance is genuinely unusable and we
|
|
2220
2249
|
// accept the cost of rotating.
|
|
2221
|
-
//
|
|
2222
|
-
//
|
|
2223
|
-
// upgrade URL
|
|
2224
|
-
//
|
|
2225
|
-
// in `instanceReserved`/`instanceReady`) → no-op, URL unchanged; the id
|
|
2226
|
-
// is attached to the machine address instead, see
|
|
2227
|
-
// `connectToStreamMachine`.
|
|
2250
|
+
// `wsUrl` is always a CIRRUS address here, whichever request method
|
|
2251
|
+
// produced it, so the orchestration-issued connection id always rides
|
|
2252
|
+
// on this upgrade URL and Cirrus adopts it instead of generating its
|
|
2253
|
+
// own. Absent id (older backend, direct dev URL) → URL unchanged.
|
|
2228
2254
|
this.connectToCirrus(withStreamRequestId(wsUrl ?? '', streamRequestId)).pipe(filter(Truthy), take(1), retry({
|
|
2229
2255
|
count: WS_RETRY_PER_INSTANCE,
|
|
2230
2256
|
delay: (err, attempt) => {
|
|
@@ -2278,22 +2304,52 @@ class SignallingService extends SubService {
|
|
|
2278
2304
|
});
|
|
2279
2305
|
}
|
|
2280
2306
|
/**
|
|
2281
|
-
*
|
|
2282
|
-
*
|
|
2283
|
-
*
|
|
2307
|
+
* Ask the orchestration for a Cirrus to connect to. All three
|
|
2308
|
+
* implementations answer the same question — "which Cirrus, and with which
|
|
2309
|
+
* `streamRequestId`?" — and emit ONE `AwsInstance` whose `wsUrl` is a Cirrus
|
|
2310
|
+
* address. What differs is only how the answer is obtained and therefore how
|
|
2311
|
+
* it can fail:
|
|
2312
|
+
*
|
|
2313
|
+
* | Method | Transport | Fails on |
|
|
2314
|
+
* | -------------------------- | -------------------- | ------------------------------ |
|
|
2315
|
+
* | {@link requestCirrusViaPolling} | HTTP poll loop | HTTP 4xx (dead URL) |
|
|
2316
|
+
* | {@link requestCirrusViaSocket} | control WS | socket drop / no `instanceReady` in {@link INSTANCE_READY_TIMEOUT} |
|
|
2317
|
+
* | {@link requestCirrusDirect} | none (dev) | never — nothing is requested |
|
|
2318
|
+
*
|
|
2319
|
+
* Whatever they return goes through the SAME downstream in
|
|
2320
|
+
* {@link connectAttempt}: `setAwsInstance` → `connectToCirrus` → the WS-only
|
|
2321
|
+
* and URL-rotating retry tiers. A failure inside any of them is just an
|
|
2322
|
+
* error on this observable, so those tiers handle it uniformly.
|
|
2284
2323
|
*/
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2324
|
+
requestCirrus(url) {
|
|
2325
|
+
if (!url.match(/^ws(s?):\/\//i)) {
|
|
2326
|
+
return this.requestCirrusViaPolling(url);
|
|
2327
|
+
}
|
|
2328
|
+
// A `ws://` URL is the orchestration control endpoint when the app supplied
|
|
2329
|
+
// a stream-request context (the same precondition `sendRequestStream` needs);
|
|
2330
|
+
// without it there is nothing to negotiate and the URL is Cirrus itself.
|
|
2331
|
+
return this.hasOrchestrationContext()
|
|
2332
|
+
? this.requestCirrusViaSocket(url)
|
|
2333
|
+
: this.requestCirrusDirect(url);
|
|
2334
|
+
}
|
|
2335
|
+
/** True when `requestStream` can be composed → new orchestration. */
|
|
2336
|
+
hasOrchestrationContext() {
|
|
2337
|
+
return !!this.streamRequestContext() && !!this.environmentId();
|
|
2338
|
+
}
|
|
2339
|
+
/**
|
|
2340
|
+
* Wait until `clientId` and `viewId` are known — every request method needs
|
|
2341
|
+
* them before it may talk to the backend.
|
|
2342
|
+
*/
|
|
2343
|
+
waitForClientAndViewIds() {
|
|
2344
|
+
return this.store.select(selectClientAndViewIds).pipe(filter(({ clientId, viewId }) => !!clientId && !!viewId), take(1));
|
|
2289
2345
|
}
|
|
2290
2346
|
/**
|
|
2291
|
-
* Direct `ws://` / `wss://` short-circuit — no
|
|
2292
|
-
*
|
|
2293
|
-
*
|
|
2347
|
+
* Direct `ws://` / `wss://` short-circuit — no orchestration at all, the URL
|
|
2348
|
+
* already points at Cirrus. Dev/playwright convenience, hence no
|
|
2349
|
+
* `kind`: nothing was requested.
|
|
2294
2350
|
*/
|
|
2295
|
-
|
|
2296
|
-
return this.
|
|
2351
|
+
requestCirrusDirect(url) {
|
|
2352
|
+
return this.waitForClientAndViewIds().pipe(map$1(() => ({
|
|
2297
2353
|
wsUrl: url,
|
|
2298
2354
|
pollingUrl: null,
|
|
2299
2355
|
instanceName: 'Direct Connection',
|
|
@@ -2306,7 +2362,7 @@ class SignallingService extends SubService {
|
|
|
2306
2362
|
* last response captured in the closure). Maps the final ready
|
|
2307
2363
|
* response to an `AwsInstance`.
|
|
2308
2364
|
*/
|
|
2309
|
-
|
|
2365
|
+
requestCirrusViaPolling(url) {
|
|
2310
2366
|
return defer(() => {
|
|
2311
2367
|
let lastResp;
|
|
2312
2368
|
this.store.dispatch(pollingStarted());
|
|
@@ -2324,12 +2380,84 @@ class SignallingService extends SubService {
|
|
|
2324
2380
|
// The orchestration issues the connection id here; it is handed to
|
|
2325
2381
|
// Cirrus on the WS upgrade URL (see `connectAttempt`).
|
|
2326
2382
|
streamRequestId: data.streamRequestId,
|
|
2383
|
+
kind: 'polling',
|
|
2327
2384
|
})), finalize(() => {
|
|
2328
2385
|
this.commandTelemetryService.trackStopCommand('EXT-getSignaling', true, { ...(lastResp ?? {}), multi: true });
|
|
2329
2386
|
this.store.dispatch(pollingEnded());
|
|
2330
2387
|
}));
|
|
2331
2388
|
});
|
|
2332
2389
|
}
|
|
2390
|
+
/**
|
|
2391
|
+
* WS-orchestration path — the socket-transport twin of
|
|
2392
|
+
* {@link requestCirrusViaPolling}. Opens the control socket, sends
|
|
2393
|
+
* `requestStream` and waits for `instanceReady`, whose `originInstanceUri`
|
|
2394
|
+
* IS the Cirrus address.
|
|
2395
|
+
*
|
|
2396
|
+
* Failure modes are handled here so they surface as a plain error and reach
|
|
2397
|
+
* `connectAttempt`'s retry tiers (previously this branch lived outside them
|
|
2398
|
+
* and a dead control socket silently hung the session):
|
|
2399
|
+
* - control socket closes/errors before `instanceReady` → error
|
|
2400
|
+
* - no `instanceReady` within {@link INSTANCE_READY_TIMEOUT} → error
|
|
2401
|
+
*
|
|
2402
|
+
* The control socket lives exactly as long as this wait — see
|
|
2403
|
+
* {@link awaitInstanceReady}.
|
|
2404
|
+
*/
|
|
2405
|
+
requestCirrusViaSocket(url) {
|
|
2406
|
+
return defer(() => {
|
|
2407
|
+
let resolved;
|
|
2408
|
+
this.orchestrationRetryCount = 0;
|
|
2409
|
+
this.commandTelemetryService.trackStartCommand('EXT-getSignaling');
|
|
2410
|
+
return this.waitForClientAndViewIds().pipe(switchMap(() => createWebSocket(url)), switchMap((control) => this.awaitInstanceReady(control, url)), tap((instance) => {
|
|
2411
|
+
resolved = instance;
|
|
2412
|
+
}), finalize(() => this.commandTelemetryService.trackStopCommand('EXT-getSignaling', true, { ...(resolved ?? {}), multi: true })));
|
|
2413
|
+
});
|
|
2414
|
+
}
|
|
2415
|
+
/**
|
|
2416
|
+
* Send `requestStream` on the freshly opened control socket and resolve with
|
|
2417
|
+
* the machine coordinates from `instanceReady`.
|
|
2418
|
+
*
|
|
2419
|
+
* The socket lives exactly as long as this wait: its close/error are HARD
|
|
2420
|
+
* failures of the request, and on resolution it is closed with every handler
|
|
2421
|
+
* detached (the backend does not use it after the handover). So the control
|
|
2422
|
+
* socket has no post-handover state at all — nothing that could report a stale
|
|
2423
|
+
* "disconnect" against a live stream.
|
|
2424
|
+
*/
|
|
2425
|
+
awaitInstanceReady(control, orchestrationUrl) {
|
|
2426
|
+
return new Observable((observer) => {
|
|
2427
|
+
control.onmessage = OnMessageHandler.bind(this);
|
|
2428
|
+
control.onclose = (e) => observer.error(new Error(`Orchestration socket closed before instanceReady (code ${e.code})`));
|
|
2429
|
+
control.onerror = () => observer.error(new Error('Orchestration socket error before instanceReady'));
|
|
2430
|
+
this.controlWs = control;
|
|
2431
|
+
Logger.log('ORCHESTRATION CONTROL WS OPEN');
|
|
2432
|
+
this.sendRequestStream();
|
|
2433
|
+
const sub = this.onInstanceReady$.pipe(take(1)).subscribe((payload) => {
|
|
2434
|
+
// The control socket has done its only job. Per the backend contract it
|
|
2435
|
+
// is not used after the handover, so it is closed here — with all its
|
|
2436
|
+
// handlers detached by `teardownSocket`, which also means the close
|
|
2437
|
+
// itself cannot come back as an error above.
|
|
2438
|
+
this.teardownControlWs();
|
|
2439
|
+
observer.next({
|
|
2440
|
+
wsUrl: payload.originInstanceUri,
|
|
2441
|
+
// The endpoint that answered — the orchestration URL here, the HTTP
|
|
2442
|
+
// polling URL in the other flow. Shown as `Orchestration:` in the overlay.
|
|
2443
|
+
pollingUrl: orchestrationUrl,
|
|
2444
|
+
instanceName: payload.originInstanceId,
|
|
2445
|
+
streamRequestId: payload.streamRequestId,
|
|
2446
|
+
kind: 'socket',
|
|
2447
|
+
});
|
|
2448
|
+
observer.complete();
|
|
2449
|
+
});
|
|
2450
|
+
// Errored, timed out or aborted before `instanceReady` — same teardown,
|
|
2451
|
+
// the socket must not be left behind either way.
|
|
2452
|
+
return () => {
|
|
2453
|
+
sub.unsubscribe();
|
|
2454
|
+
this.teardownControlWs();
|
|
2455
|
+
};
|
|
2456
|
+
}).pipe(timeout$1({
|
|
2457
|
+
first: INSTANCE_READY_TIMEOUT,
|
|
2458
|
+
with: () => throwError(() => new Error(`No instanceReady within ${INSTANCE_READY_TIMEOUT}ms — requesting another machine`)),
|
|
2459
|
+
}), takeUntil(this.abort$));
|
|
2460
|
+
}
|
|
2333
2461
|
/**
|
|
2334
2462
|
* Poll the SAME `url` until ready, or until HTTP 4xx says the URL
|
|
2335
2463
|
* is dead.
|
|
@@ -2386,37 +2514,31 @@ class SignallingService extends SubService {
|
|
|
2386
2514
|
// still create a new socket, but in tests would re-emit the same
|
|
2387
2515
|
// (already-errored) mock observable.
|
|
2388
2516
|
return defer(() => {
|
|
2389
|
-
|
|
2517
|
+
// Only the Cirrus socket — the control socket belongs to
|
|
2518
|
+
// `requestCirrusViaSocket`, which already opened it to GET this `wsUrl`.
|
|
2519
|
+
this.closeCirrus({
|
|
2520
|
+
code: 1000,
|
|
2521
|
+
reason: DisconnectReason.NewConnectionAttempt,
|
|
2522
|
+
});
|
|
2390
2523
|
return createWebSocket(wsUrl);
|
|
2391
2524
|
}).pipe(takeUntil(this.abort$), tapLog('WS CREATED'), tap((ws) => {
|
|
2392
2525
|
this.commandTelemetryService.trackStartCommand('EXT-iceCandidate');
|
|
2393
2526
|
this.addWsHandlers(ws);
|
|
2394
2527
|
}));
|
|
2395
2528
|
}
|
|
2529
|
+
/**
|
|
2530
|
+
* Handlers for the CIRRUS socket — the only long-lived socket. Its close/error
|
|
2531
|
+
* are real stream events (`setCirrusDisconnected` / `dropConnection`), unlike
|
|
2532
|
+
* the control socket's, which never outlives {@link awaitInstanceReady}.
|
|
2533
|
+
*/
|
|
2396
2534
|
addWsHandlers(ws) {
|
|
2397
2535
|
ws.onmessage = OnMessageHandler.bind(this);
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
ws.onclose = (e) => this.onOrchestrationClose(e);
|
|
2401
|
-
ws.onerror = (e) => this.onOrchestrationError(e);
|
|
2536
|
+
ws.onclose = OnCloseHandler.bind(this);
|
|
2537
|
+
ws.onerror = OnErrorHandler.bind(this);
|
|
2402
2538
|
this.ws = ws;
|
|
2403
2539
|
Logger.log('WS HANDLERS ADDED');
|
|
2404
2540
|
OnOpenHandler.call(this);
|
|
2405
2541
|
}
|
|
2406
|
-
onOrchestrationClose(e) {
|
|
2407
|
-
if (this.streamMachineConnected) {
|
|
2408
|
-
Logger.log(`Orchestration socket closed after occupancy (benign): ${e.code}`);
|
|
2409
|
-
return;
|
|
2410
|
-
}
|
|
2411
|
-
OnCloseHandler.call(this, e);
|
|
2412
|
-
}
|
|
2413
|
-
onOrchestrationError(e) {
|
|
2414
|
-
if (this.streamMachineConnected) {
|
|
2415
|
-
Logger.log('Orchestration socket error after occupancy (benign)');
|
|
2416
|
-
return;
|
|
2417
|
-
}
|
|
2418
|
-
OnErrorHandler.call(this, e);
|
|
2419
|
-
}
|
|
2420
2542
|
/**
|
|
2421
2543
|
* Adopt a live signalling socket opened by the `stream-prefetch.js`
|
|
2422
2544
|
* head-script. The socket is already OPEN and `requestStream` was already
|
|
@@ -2433,104 +2555,32 @@ class SignallingService extends SubService {
|
|
|
2433
2555
|
this.ws = ws;
|
|
2434
2556
|
Logger.log('WS ADOPTED (handshake already done, requestStream already sent)');
|
|
2435
2557
|
}
|
|
2436
|
-
/**
|
|
2437
|
-
* Open the SECOND socket, directly to the stream machine's Cirrus
|
|
2438
|
-
* (`originInstanceUri` from `instanceReady`). The whole WebRTC signalling
|
|
2439
|
-
* (config/offer/answer/iceCandidate/ping) flows over this socket; the
|
|
2440
|
-
* orchestration socket (`this.ws`) stays open for control/lifecycle.
|
|
2441
|
-
*
|
|
2442
|
-
* "Just connect" — nothing is sent on open (the machine is already reserved
|
|
2443
|
-
* for this session): Cirrus pushes `config` and Unreal sends the `offer`.
|
|
2444
|
-
*
|
|
2445
|
-
* The orchestration-issued `streamRequestId` (from the same `instanceReady`
|
|
2446
|
-
* payload) rides on this upgrade URL: in the new flow THIS is the Cirrus
|
|
2447
|
-
* socket, so it is the one that must carry the id (the first socket is
|
|
2448
|
-
* orchestration-only control and needs nothing). Without it the stream
|
|
2449
|
-
* machine's Cirrus falls back to its own `guid()` and the session can no
|
|
2450
|
-
* longer be correlated. Absent id → URL unchanged.
|
|
2451
|
-
*/
|
|
2452
|
-
connectToStreamMachine(originInstanceUri, streamRequestId) {
|
|
2453
|
-
if (!originInstanceUri) {
|
|
2454
|
-
Logger.warn('instanceReady without originInstanceUri — stream socket not opened');
|
|
2455
|
-
return;
|
|
2456
|
-
}
|
|
2457
|
-
// Guard: a stream socket is already live. A duplicate / re-sent
|
|
2458
|
-
// `instanceReady` must NOT tear down the active stream.
|
|
2459
|
-
if (this.streamMachineConnected) {
|
|
2460
|
-
Logger.warn('Stream machine already connected — ignoring duplicate instanceReady');
|
|
2461
|
-
return;
|
|
2462
|
-
}
|
|
2463
|
-
// Drop any prior (half-open) stream socket AND cancel any in-flight connect
|
|
2464
|
-
// before a fresh attempt — e.g. a retry that resolved to a different machine
|
|
2465
|
-
// or two instanceReady messages before the first connect resolves.
|
|
2466
|
-
this.teardownStreamWs();
|
|
2467
|
-
this.streamWsSub = createWebSocket(withStreamRequestId(originInstanceUri, streamRequestId))
|
|
2468
|
-
.pipe(takeUntil(this.abort$), takeUntil(this.destroy$), take(1))
|
|
2469
|
-
.subscribe({
|
|
2470
|
-
next: (ws) => {
|
|
2471
|
-
this.streamMachineRetryCount = 0;
|
|
2472
|
-
this.addStreamWsHandlers(ws);
|
|
2473
|
-
},
|
|
2474
|
-
error: (err) => {
|
|
2475
|
-
Logger.error('Stream-machine WS failed to connect', err?.message);
|
|
2476
|
-
this.retryStreamMachine();
|
|
2477
|
-
},
|
|
2478
|
-
});
|
|
2479
|
-
}
|
|
2480
|
-
/**
|
|
2481
|
-
* The stream-machine (Cirrus) connection failed. Re-request a machine over
|
|
2482
|
-
* the still-open orchestration socket: orchestration issues a new
|
|
2483
|
-
* streamRequestId and a fresh `instanceReady(originInstanceUri)`, which calls
|
|
2484
|
-
* {@link connectToStreamMachine} again on a different machine. Bounded by
|
|
2485
|
-
* {@link INNER_RETRY_LIMIT}; on exhaustion the stream is dropped.
|
|
2486
|
-
*/
|
|
2487
|
-
retryStreamMachine() {
|
|
2488
|
-
if (this.streamMachineRetryCount >= INNER_RETRY_LIMIT) {
|
|
2489
|
-
Logger.error('Stream-machine connection exhausted retries — dropping');
|
|
2490
|
-
this.store.dispatch(disconnectStream({
|
|
2491
|
-
reason: DisconnectReason.ReconnectFailed,
|
|
2492
|
-
message: 'Could not connect to the stream machine',
|
|
2493
|
-
}));
|
|
2494
|
-
return;
|
|
2495
|
-
}
|
|
2496
|
-
this.streamMachineRetryCount++;
|
|
2497
|
-
this.orchestrationRetrySub?.unsubscribe();
|
|
2498
|
-
this.orchestrationRetrySub = timer(POLLING_TIME)
|
|
2499
|
-
.pipe(takeUntil(this.abort$))
|
|
2500
|
-
.subscribe(() => this.sendRequestStream());
|
|
2501
|
-
}
|
|
2502
|
-
addStreamWsHandlers(ws) {
|
|
2503
|
-
ws.onmessage = OnMessageHandler.bind(this);
|
|
2504
|
-
ws.onclose = OnCloseHandler.bind(this);
|
|
2505
|
-
ws.onerror = OnErrorHandler.bind(this);
|
|
2506
|
-
this.streamWs = ws;
|
|
2507
|
-
this.streamMachineConnected = true;
|
|
2508
|
-
Logger.log('STREAM-MACHINE WS CONNECTED');
|
|
2509
|
-
}
|
|
2510
|
-
teardownStreamWs() {
|
|
2511
|
-
this.streamWsSub?.unsubscribe();
|
|
2512
|
-
this.teardownSocket(this.streamWs, 1000, DisconnectReason.NewConnectionAttempt);
|
|
2513
|
-
this.streamWs = null;
|
|
2514
|
-
this.streamMachineConnected = false;
|
|
2515
|
-
}
|
|
2516
2558
|
showStatusMessage(statusMessage) {
|
|
2517
2559
|
this.store.dispatch(setStatusMessage({ statusMessage }));
|
|
2518
2560
|
}
|
|
2519
2561
|
/**
|
|
2520
|
-
* Close the
|
|
2521
|
-
*
|
|
2562
|
+
* Close everything: the Cirrus socket AND the orchestration control socket.
|
|
2563
|
+
* Used on abort/teardown. A new connection attempt inside a live cycle uses
|
|
2564
|
+
* {@link closeCirrus} instead, so it does not kill the control socket that
|
|
2565
|
+
* produced the address it is about to connect to.
|
|
2522
2566
|
*/
|
|
2523
2567
|
close(data) {
|
|
2524
2568
|
const { code, reason } = data;
|
|
2525
|
-
this.onWebRtcAnswer$.next(null);
|
|
2526
2569
|
this.orchestrationRetrySub?.unsubscribe();
|
|
2527
|
-
this.
|
|
2570
|
+
this.closeCirrus(data);
|
|
2571
|
+
this.teardownControlWs(code, reason);
|
|
2572
|
+
}
|
|
2573
|
+
/** Close only the Cirrus signalling socket. */
|
|
2574
|
+
closeCirrus(data) {
|
|
2575
|
+
const { code, reason } = data;
|
|
2576
|
+
this.onWebRtcAnswer$.next(null);
|
|
2528
2577
|
this.teardownSocket(this.ws, code, reason);
|
|
2529
2578
|
this.ws = null;
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
this.
|
|
2533
|
-
this.
|
|
2579
|
+
}
|
|
2580
|
+
teardownControlWs(code = 1000, reason = DisconnectReason.NewConnectionAttempt) {
|
|
2581
|
+
this.teardownSocket(this.controlWs, code, reason);
|
|
2582
|
+
this.controlWs = null;
|
|
2583
|
+
this.orchestrationRetryCount = 0;
|
|
2534
2584
|
}
|
|
2535
2585
|
teardownSocket(ws, code, reason) {
|
|
2536
2586
|
if (!ws) {
|
|
@@ -2547,17 +2597,16 @@ class SignallingService extends SubService {
|
|
|
2547
2597
|
ws.close(code, reason);
|
|
2548
2598
|
}
|
|
2549
2599
|
}
|
|
2600
|
+
/**
|
|
2601
|
+
* Player signalling (offer/answer/iceCandidate/pong/p2pEstablished/subscribe)
|
|
2602
|
+
* — always the Cirrus socket, in every flow.
|
|
2603
|
+
*/
|
|
2550
2604
|
send(data) {
|
|
2551
2605
|
this.sendTo(this.ws, data);
|
|
2552
2606
|
}
|
|
2553
|
-
/**
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
* the new flow (once connected), or the orchestration/relay socket in the old
|
|
2557
|
-
* polling flow (where there is no separate stream socket).
|
|
2558
|
-
*/
|
|
2559
|
-
sendPlayerSignal(data) {
|
|
2560
|
-
this.sendTo(this.streamMachineConnected ? this.streamWs : this.ws, data);
|
|
2607
|
+
/** Orchestration control messages (`requestStream`) — the control socket. */
|
|
2608
|
+
sendToOrchestration(data) {
|
|
2609
|
+
this.sendTo(this.controlWs, data);
|
|
2561
2610
|
}
|
|
2562
2611
|
sendTo(ws, data) {
|
|
2563
2612
|
let dataString = data;
|
|
@@ -2587,21 +2636,45 @@ class SignallingService extends SubService {
|
|
|
2587
2636
|
Logger.colored(...COLOR_CODES.FROM_CIRRUS_ERROR, `Invalid CIRRUS message type: ${msg.type}`);
|
|
2588
2637
|
}
|
|
2589
2638
|
}
|
|
2590
|
-
|
|
2591
|
-
|
|
2639
|
+
/**
|
|
2640
|
+
* One handler map shared by both sockets — `handleMessage` dispatches purely by
|
|
2641
|
+
* message type. Which socket a type can arrive on is fixed, so registration is
|
|
2642
|
+
* split by origin: {@link setOrchestrationHandlers} (control socket) and
|
|
2643
|
+
* {@link setStreamHandlers} (Cirrus).
|
|
2644
|
+
*
|
|
2645
|
+
* Keepalive is deliberately absent: both servers ping at the WS **protocol**
|
|
2646
|
+
* level (`ws.ping()`), which the browser answers on its own — the page never
|
|
2647
|
+
* sees a ping and must never reply with an application-level `pong`. An
|
|
2648
|
+
* unregistered type is dropped silently by `handleMessage`, so a `ping` from a
|
|
2649
|
+
* not-yet-updated backend is simply ignored.
|
|
2650
|
+
*/
|
|
2651
|
+
setMessageHandlers() {
|
|
2652
|
+
this.setOrchestrationHandlers();
|
|
2653
|
+
this.setStreamHandlers();
|
|
2654
|
+
}
|
|
2655
|
+
/**
|
|
2656
|
+
* Messages from the ORCHESTRATION control socket. Only reachable during the
|
|
2657
|
+
* provisioning wait — `requestCirrusViaSocket` closes that socket the moment
|
|
2658
|
+
* `instanceReady` lands.
|
|
2659
|
+
*/
|
|
2660
|
+
setOrchestrationHandlers() {
|
|
2661
|
+
// `instanceReserved` (eta/progress) and `instanceReady` (the Cirrus address)
|
|
2662
|
+
// ARE the request/response pair of `requestCirrusViaSocket`.
|
|
2663
|
+
this.wsMsgHandlers.instanceReserved = InstanceReservedHandler.bind(this);
|
|
2664
|
+
this.wsMsgHandlers.instanceReady = InstanceReadyHandler.bind(this);
|
|
2665
|
+
// Orchestration-side refusals (4001 no instances / 4002 limit reached) →
|
|
2666
|
+
// resend `requestStream`, bounded by `INSTANCE_READY_TIMEOUT`.
|
|
2592
2667
|
this.wsMsgHandlers.error = OrchestrationErrorHandler.bind(this);
|
|
2668
|
+
}
|
|
2669
|
+
/** Messages from the CIRRUS socket — the actual stream signalling. */
|
|
2670
|
+
setStreamHandlers() {
|
|
2593
2671
|
this.wsMsgHandlers.config = ConfigHandler.bind(this);
|
|
2672
|
+
this.wsMsgHandlers.offer = OfferHandler.bind(this);
|
|
2594
2673
|
this.wsMsgHandlers.answer = AnswerHandler.bind(this);
|
|
2674
|
+
this.wsMsgHandlers.iceCandidate = IceCandidateHandler.bind(this);
|
|
2595
2675
|
this.wsMsgHandlers.ssInfo = SSInfoHandler.bind(this);
|
|
2596
2676
|
this.wsMsgHandlers.playerCount = PlayerCountHandler.bind(this);
|
|
2597
2677
|
this.wsMsgHandlers.streamerList = StreamerListHandler.bind(this);
|
|
2598
|
-
this.wsMsgHandlers.iceCandidate = IceCandidateHandler.bind(this);
|
|
2599
|
-
this.wsMsgHandlers.instanceReady = InstanceReadyHandler.bind(this);
|
|
2600
|
-
this.wsMsgHandlers.instanceReserved = InstanceReservedHandler.bind(this);
|
|
2601
|
-
this.wsMsgHandlers.playerDisconnected = PlayerDisconnectHandler.bind(this);
|
|
2602
|
-
this.wsMsgHandlers.streamerDisconnected =
|
|
2603
|
-
StreamerDisconnectHandler.bind(this);
|
|
2604
|
-
this.wsMsgHandlers.offer = OfferHandler.bind(this);
|
|
2605
2678
|
}
|
|
2606
2679
|
sendRequestStream() {
|
|
2607
2680
|
const context = this.streamRequestContext();
|
|
@@ -2609,7 +2682,7 @@ class SignallingService extends SubService {
|
|
|
2609
2682
|
if (!context || !environmentId) {
|
|
2610
2683
|
return;
|
|
2611
2684
|
}
|
|
2612
|
-
this.
|
|
2685
|
+
this.sendToOrchestration({
|
|
2613
2686
|
type: OrchestrationMessageTypes.requestStream,
|
|
2614
2687
|
correlationId: this.correlationId,
|
|
2615
2688
|
payload: {
|
|
@@ -2620,18 +2693,12 @@ class SignallingService extends SubService {
|
|
|
2620
2693
|
});
|
|
2621
2694
|
}
|
|
2622
2695
|
/**
|
|
2623
|
-
*
|
|
2624
|
-
*
|
|
2625
|
-
*
|
|
2626
|
-
* - NEW flow (`streamMachineConnected`): the stream-machine socket
|
|
2627
|
-
* (`streamWs`). The orchestration socket (`this.ws`) is intentionally
|
|
2628
|
-
* closed after occupancy, so checking it would wrongly report "not
|
|
2629
|
-
* connected" and block reconnect.
|
|
2630
|
-
* - OLD polling flow: the single relay socket (`this.ws`) — unchanged.
|
|
2696
|
+
* Is the player-signalling socket open? Always the Cirrus socket — the
|
|
2697
|
+
* control socket's state is irrelevant to the stream (orchestration closes it
|
|
2698
|
+
* once the instance is occupied), so it must not be part of this gate.
|
|
2631
2699
|
*/
|
|
2632
2700
|
isConnected() {
|
|
2633
|
-
|
|
2634
|
-
return !!(activeWs && activeWs.readyState === WS_OPEN_STATE);
|
|
2701
|
+
return !!(this.ws && this.ws.readyState === WS_OPEN_STATE);
|
|
2635
2702
|
}
|
|
2636
2703
|
getRegion() {
|
|
2637
2704
|
Logger.log('Start Getting Region');
|
|
@@ -3550,7 +3617,7 @@ class WebRtcPlayerService extends SubService {
|
|
|
3550
3617
|
});
|
|
3551
3618
|
fromEvent(pcClient, 'icecandidate')
|
|
3552
3619
|
.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$)))
|
|
3553
|
-
.subscribe((candidate) => this.signallingSrv.
|
|
3620
|
+
.subscribe((candidate) => this.signallingSrv.send({
|
|
3554
3621
|
type: 'iceCandidate',
|
|
3555
3622
|
candidate,
|
|
3556
3623
|
}));
|
|
@@ -3700,7 +3767,7 @@ class WebRtcPlayerService extends SubService {
|
|
|
3700
3767
|
const offer = await pcClient.createOffer();
|
|
3701
3768
|
offer.sdp = this.mungeSDP(offer.sdp ?? '');
|
|
3702
3769
|
await pcClient.setLocalDescription(offer);
|
|
3703
|
-
this.signallingSrv.
|
|
3770
|
+
this.signallingSrv.send(offer);
|
|
3704
3771
|
}
|
|
3705
3772
|
catch {
|
|
3706
3773
|
Logger.error("Couldn't create offer");
|
|
@@ -3715,7 +3782,7 @@ class WebRtcPlayerService extends SubService {
|
|
|
3715
3782
|
const answer = await pcClient.createAnswer();
|
|
3716
3783
|
answer.sdp = this.mungeSDP(answer.sdp ?? '');
|
|
3717
3784
|
await pcClient.setLocalDescription(answer);
|
|
3718
|
-
this.signallingSrv.
|
|
3785
|
+
this.signallingSrv.send(answer);
|
|
3719
3786
|
// Observability signal for the signaling waterfall — the handshake
|
|
3720
3787
|
// went "answer sent" between `offer received` and `datachannel_ready`.
|
|
3721
3788
|
this.store.dispatch(answerSent());
|
|
@@ -3839,6 +3906,7 @@ class StreamAdoptionService {
|
|
|
3839
3906
|
// script's ids are only used internally to establish the connection.
|
|
3840
3907
|
this.store.dispatch(streamAdopted({
|
|
3841
3908
|
machineId: parked.machineId ?? null,
|
|
3909
|
+
pollingUrl: parked.pollingUrl ?? null,
|
|
3842
3910
|
ssData: parked.ssData ?? null,
|
|
3843
3911
|
streamRequestId: parked.ids?.streamRequestId || null,
|
|
3844
3912
|
timings: parked.timings ?? null,
|
|
@@ -4169,7 +4237,15 @@ class UnrealEffects {
|
|
|
4169
4237
|
userAgent: navigator.userAgent,
|
|
4170
4238
|
error,
|
|
4171
4239
|
};
|
|
4172
|
-
|
|
4240
|
+
// Only the polling flow has an HTTP endpoint to report to;
|
|
4241
|
+
// `pollingUrl` is a `wss://` orchestration address otherwise.
|
|
4242
|
+
const reportUrl = awsInstance.pollingUrl?.startsWith('http')
|
|
4243
|
+
? awsInstance.pollingUrl
|
|
4244
|
+
: null;
|
|
4245
|
+
const report$ = reportUrl
|
|
4246
|
+
? this.http.post(reportUrl, body)
|
|
4247
|
+
: of(null);
|
|
4248
|
+
return report$.pipe(catchError((postError) => {
|
|
4173
4249
|
Logger.error('Error sending timeout info:', postError);
|
|
4174
4250
|
return of(null);
|
|
4175
4251
|
}),
|
|
@@ -8100,5 +8176,5 @@ function provideUnrealModule() {
|
|
|
8100
8176
|
* Generated bundle index. Do not edit.
|
|
8101
8177
|
*/
|
|
8102
8178
|
|
|
8103
|
-
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,
|
|
8179
|
+
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 };
|
|
8104
8180
|
//# sourceMappingURL=3dsource-angular-unreal-module.mjs.map
|