@3dsource/angular-unreal-module 0.0.122 → 0.0.123-dev.0
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.
|
@@ -6,7 +6,7 @@ import { createAction, props, createReducer, on, createFeature, createSelector,
|
|
|
6
6
|
import { concatLatestFrom } from '@ngrx/operators';
|
|
7
7
|
import { Actions, createEffect, ofType, provideEffects } from '@ngrx/effects';
|
|
8
8
|
import { whereNot, Logger, calculateMedian, clampf, lerp, getCanvasCached, getSnapshot, fitIntoRectangle, Truthy, tapLog, Falsy, generateUuid, COLOR_CODES, where, KeyboardNumericCode, InvertedKeyMap, Semaphore, HEXtoRGB, RGBtoHSV, inverseLerp, HSVtoRGB, RGBtoHEX, fpIsASameAsB } from '@3dsource/utils';
|
|
9
|
-
import { map as map$1, distinctUntilChanged, takeWhile, filter, tap, takeUntil, catchError, withLatestFrom, exhaustMap, switchMap as switchMap$1, timeout as timeout$1, debounceTime as debounceTime$1, take as take$1, startWith as startWith$1 } from 'rxjs/operators';
|
|
9
|
+
import { map as map$1, distinctUntilChanged, takeWhile, filter, tap, takeUntil, catchError, withLatestFrom, exhaustMap, switchMap as switchMap$1, timeout as timeout$1, share as share$1, debounceTime as debounceTime$1, take as take$1, startWith as startWith$1 } from 'rxjs/operators';
|
|
10
10
|
import { DialogRef, DIALOG_DATA, Dialog } from '@angular/cdk/dialog';
|
|
11
11
|
import { HttpClient } from '@angular/common/http';
|
|
12
12
|
import { ScrollStrategyOptions } from '@angular/cdk/overlay';
|
|
@@ -1276,12 +1276,6 @@ class UnrealEffects {
|
|
|
1276
1276
|
onDataChannelClose: this.unrealInitialConfig?.reconnect?.onDataChannelClose ??
|
|
1277
1277
|
DEFAULT_RECONNECT_ON_DATACHANNEL_CLOSE,
|
|
1278
1278
|
};
|
|
1279
|
-
this.destroyConnectionsCleanup$ = createEffect(() => {
|
|
1280
|
-
return this.actions$.pipe(ofType(destroyUnrealScene), tap(() => {
|
|
1281
|
-
this.commandsSender.destroy();
|
|
1282
|
-
this.videoService.setContainer(null);
|
|
1283
|
-
}));
|
|
1284
|
-
}, { dispatch: false });
|
|
1285
1279
|
this.destroyConnections$ = createEffect(() => {
|
|
1286
1280
|
return this.actions$.pipe(ofType(destroyUnrealScene), map(() => destroyRemoteConnections({ reason: DisconnectReason.Destroy })));
|
|
1287
1281
|
});
|
|
@@ -1903,7 +1897,7 @@ const BITRATE_MONITOR = new DataFlowMonitor(FilterModel.yellowFlag, FilterModel.
|
|
|
1903
1897
|
|
|
1904
1898
|
class VideoService extends SubService {
|
|
1905
1899
|
constructor() {
|
|
1906
|
-
super(
|
|
1900
|
+
super();
|
|
1907
1901
|
this.isVideoPlaying = this.store.selectSignal(unrealFeature.selectIsVideoPlaying);
|
|
1908
1902
|
this.video = null;
|
|
1909
1903
|
this.audio = null;
|
|
@@ -1922,6 +1916,11 @@ class VideoService extends SubService {
|
|
|
1922
1916
|
? // IMPORTANT! DO NOT CHANGE THOSE NUMBERS, LBM Stats are based on those values
|
|
1923
1917
|
interval(250).pipe(map(() => track.pcClient), switchMap$1((pcClient) => from(this.getStats(pcClient))), filter(Truthy))
|
|
1924
1918
|
: of(null)), filter(Truthy), share());
|
|
1919
|
+
this.destroyAbort = new AbortController();
|
|
1920
|
+
this.destroyRef.onDestroy(() => {
|
|
1921
|
+
this.destroyAbort.abort();
|
|
1922
|
+
this.destroy();
|
|
1923
|
+
});
|
|
1925
1924
|
}
|
|
1926
1925
|
setContainer(container = null) {
|
|
1927
1926
|
this.container = container;
|
|
@@ -1933,7 +1932,9 @@ class VideoService extends SubService {
|
|
|
1933
1932
|
this.VideoEncoderQP = Number(new TextDecoder('utf-16').decode(data));
|
|
1934
1933
|
}
|
|
1935
1934
|
create() {
|
|
1936
|
-
this.disconnect
|
|
1935
|
+
this.disconnect$
|
|
1936
|
+
.pipe(take(1), takeUntil$1(this.destroy$))
|
|
1937
|
+
.subscribe(() => this.destroy());
|
|
1937
1938
|
this.destroy();
|
|
1938
1939
|
this.createWebRtcVideo();
|
|
1939
1940
|
this.createWebRtcAudio();
|
|
@@ -2006,8 +2007,13 @@ class VideoService extends SubService {
|
|
|
2006
2007
|
window.addEventListener('pointerdown', resume, {
|
|
2007
2008
|
once: true,
|
|
2008
2009
|
capture: true,
|
|
2010
|
+
signal: this.destroyAbort.signal,
|
|
2011
|
+
});
|
|
2012
|
+
window.addEventListener('keydown', resume, {
|
|
2013
|
+
once: true,
|
|
2014
|
+
capture: true,
|
|
2015
|
+
signal: this.destroyAbort.signal,
|
|
2009
2016
|
});
|
|
2010
|
-
window.addEventListener('keydown', resume, { once: true, capture: true });
|
|
2011
2017
|
}
|
|
2012
2018
|
}
|
|
2013
2019
|
async getStats(pcClient) {
|
|
@@ -2173,7 +2179,7 @@ class VideoService extends SubService {
|
|
|
2173
2179
|
.pipe(takeUntil$1(merge(this.disconnect$, this.destroy$)))
|
|
2174
2180
|
.subscribe(() => this.changeStatusMainVideoOnScene(true));
|
|
2175
2181
|
fromEvent(this.video, 'loadedmetadata')
|
|
2176
|
-
.pipe(take(1), filter((data) => this.video === data.target), tapLog('VideoService loadedmetadata:'), filter(() => !!this.video))
|
|
2182
|
+
.pipe(take(1), filter((data) => this.video === data.target), tapLog('VideoService loadedmetadata:'), filter(() => !!this.video), takeUntil$1(this.destroy$))
|
|
2177
2183
|
.subscribe(() => this.store.dispatch(videoStreamInitialized()));
|
|
2178
2184
|
}
|
|
2179
2185
|
createWebRtcAudio() {
|
|
@@ -2189,12 +2195,12 @@ class VideoService extends SubService {
|
|
|
2189
2195
|
error: () => ({}),
|
|
2190
2196
|
});
|
|
2191
2197
|
}
|
|
2192
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: VideoService, deps:
|
|
2198
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: VideoService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
2193
2199
|
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: VideoService }); }
|
|
2194
2200
|
}
|
|
2195
2201
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: VideoService, decorators: [{
|
|
2196
2202
|
type: Injectable
|
|
2197
|
-
}] });
|
|
2203
|
+
}], ctorParameters: () => [] });
|
|
2198
2204
|
|
|
2199
2205
|
class CommandTelemetryService {
|
|
2200
2206
|
constructor() {
|
|
@@ -2650,21 +2656,32 @@ class SignallingService extends SubService {
|
|
|
2650
2656
|
this.onWebRtcAnswer$ = new Subject();
|
|
2651
2657
|
// Fan-in produced by abortEstablishingConnection$ in unreal.effects.ts
|
|
2652
2658
|
// whenever cirrus drops, dropConnection fires, or the scene is destroyed.
|
|
2659
|
+
// `share()` deduplicates the tap side effect across all takeUntil(abort$)
|
|
2660
|
+
// subscribers — close() runs once per action, not once per subscriber.
|
|
2653
2661
|
this.abort$ = this.actions$.pipe(ofType(abortEstablishingConnection), tap(() => this.close({
|
|
2654
2662
|
code: 1000,
|
|
2655
2663
|
reason: DisconnectReason.AbortEstablishingConnection,
|
|
2656
|
-
})));
|
|
2664
|
+
})), share$1());
|
|
2665
|
+
this.ws = null;
|
|
2657
2666
|
this.wsMsgHandlers = {};
|
|
2658
2667
|
this.init();
|
|
2668
|
+
this.destroyRef.onDestroy(() => this.close({
|
|
2669
|
+
code: 1000,
|
|
2670
|
+
reason: DisconnectReason.AbortEstablishingConnection,
|
|
2671
|
+
}));
|
|
2659
2672
|
}
|
|
2660
2673
|
init() {
|
|
2661
2674
|
if (this.unrealConfig?.playwright) {
|
|
2662
2675
|
return;
|
|
2663
2676
|
}
|
|
2664
2677
|
this.setHandlersFromStream();
|
|
2665
|
-
|
|
2678
|
+
// Subscriptions persist across sessions (use destroy$ only, not abort$).
|
|
2679
|
+
// dataChannelReady is a per-session event; abort$ fires on every
|
|
2680
|
+
// destroyUnrealScene, so binding to it kills the subs after the first
|
|
2681
|
+
// session within a persistent injector. send() is already a no-op when
|
|
2682
|
+
// the WS is closed, so there's no risk of leaking messages during teardown.
|
|
2666
2683
|
this.actions$
|
|
2667
|
-
.pipe(ofType(dataChannelReady), takeUntil(
|
|
2684
|
+
.pipe(ofType(dataChannelReady), takeUntil(this.destroy$))
|
|
2668
2685
|
.subscribe(() => {
|
|
2669
2686
|
this.send({
|
|
2670
2687
|
type: 'p2pEstablished',
|
|
@@ -2675,7 +2692,7 @@ class SignallingService extends SubService {
|
|
|
2675
2692
|
this.actions$
|
|
2676
2693
|
.pipe(ofType(dataChannelReady), switchMap(() => this.store
|
|
2677
2694
|
.select(unrealFeature.selectViewportReady)
|
|
2678
|
-
.pipe(filter(Truthy), take(1))), takeUntil(
|
|
2695
|
+
.pipe(filter(Truthy), take(1))), takeUntil(this.destroy$))
|
|
2679
2696
|
.subscribe(() => {
|
|
2680
2697
|
this.send({
|
|
2681
2698
|
type: 'streamPreparedForUserInteraction',
|
|
@@ -2698,7 +2715,7 @@ class SignallingService extends SubService {
|
|
|
2698
2715
|
this.correlationId = generateUuid();
|
|
2699
2716
|
this.commandTelemetryService.reset();
|
|
2700
2717
|
this.startEstablishingConnection();
|
|
2701
|
-
return this.getRegion().pipe(tap((region) => this.region.set(region ?? '')), map$1((region) => this.adaptUrlsToRegion(urlList, region)), switchMap((urls) => this.getAwsInstance(urls).pipe(take(1))), tap((awsInstance) => this.store.dispatch(setAwsInstance(awsInstance))), switchMap(({ wsUrl }) => this.connectToCirrus(wsUrl ?? '').pipe(filter(Truthy), take(1))), takeUntil(this.abort$));
|
|
2718
|
+
return this.getRegion().pipe(tap((region) => this.region.set(region ?? '')), map$1((region) => this.adaptUrlsToRegion(urlList, region)), switchMap((urls) => this.getAwsInstance(urls).pipe(take(1))), tap((awsInstance) => this.store.dispatch(setAwsInstance(awsInstance))), switchMap(({ wsUrl }) => this.connectToCirrus(wsUrl ?? '').pipe(filter(Truthy), take(1))), takeUntil(merge(this.abort$, this.destroy$)));
|
|
2702
2719
|
}).pipe(
|
|
2703
2720
|
// Retry the whole attempt on error, advancing to the next URL each time.
|
|
2704
2721
|
// Note: this currently re-runs `getRegion()` on every retry — see
|
|
@@ -2718,10 +2735,10 @@ class SignallingService extends SubService {
|
|
|
2718
2735
|
// count: 10, // Optional: cap the number of attempts.
|
|
2719
2736
|
}),
|
|
2720
2737
|
// abort$ sits OUTSIDE retry, so it kills the entire retry loop,
|
|
2721
|
-
// not just the current attempt.
|
|
2722
|
-
//
|
|
2723
|
-
//
|
|
2724
|
-
takeUntil(this.abort$));
|
|
2738
|
+
// not just the current attempt. destroy$ is the defense-in-depth
|
|
2739
|
+
// signal for injector teardown — if the abort action chain doesn't
|
|
2740
|
+
// run for any reason, destroy$ still stops the loop.
|
|
2741
|
+
takeUntil(merge(this.abort$, this.destroy$)));
|
|
2725
2742
|
}
|
|
2726
2743
|
startEstablishingConnection() {
|
|
2727
2744
|
this.showStatusMessage(UnrealStatusMessage.STARTING_YOUR_SESSION);
|
|
@@ -2903,10 +2920,10 @@ class SignallingService extends SubService {
|
|
|
2903
2920
|
}
|
|
2904
2921
|
connectToCirrus(wsUrl) {
|
|
2905
2922
|
this.close({ code: 1000, reason: DisconnectReason.NewConnectionAttempt });
|
|
2906
|
-
return createWebSocket(wsUrl).pipe(
|
|
2923
|
+
return createWebSocket(wsUrl).pipe(tapLog('WS CREATED'), tap((ws) => {
|
|
2907
2924
|
this.commandTelemetryService.trackStartCommand('EXT-iceCandidate');
|
|
2908
2925
|
this.addWsHandlers(ws);
|
|
2909
|
-
}));
|
|
2926
|
+
}), takeUntil(merge(this.abort$, this.destroy$)));
|
|
2910
2927
|
}
|
|
2911
2928
|
addWsHandlers(ws) {
|
|
2912
2929
|
ws.onmessage = OnMessageHandler.bind(this);
|
|
@@ -2925,6 +2942,9 @@ class SignallingService extends SubService {
|
|
|
2925
2942
|
*/
|
|
2926
2943
|
close(data) {
|
|
2927
2944
|
const { code, reason } = data;
|
|
2945
|
+
// Signal "no answer in flight anymore" to any subscriber that observes
|
|
2946
|
+
// raw emissions (today's subscribers filter Truthy, but the Subject's
|
|
2947
|
+
// `| null` type is the contract — keep it honored).
|
|
2928
2948
|
this.onWebRtcAnswer$.next(null);
|
|
2929
2949
|
this.orchestrationRetrySub?.unsubscribe();
|
|
2930
2950
|
if (this.ws) {
|
|
@@ -3004,7 +3024,7 @@ class SignallingService extends SubService {
|
|
|
3004
3024
|
* Check if the WebSocket connection is open and ready.
|
|
3005
3025
|
*/
|
|
3006
3026
|
isConnected() {
|
|
3007
|
-
return this.ws && this.ws.readyState === WS_OPEN_STATE;
|
|
3027
|
+
return !!(this.ws && this.ws.readyState === WS_OPEN_STATE);
|
|
3008
3028
|
}
|
|
3009
3029
|
getRegion() {
|
|
3010
3030
|
Logger.log('Start Getting Region');
|
|
@@ -3042,6 +3062,7 @@ class WebRtcPlayerService extends SubService {
|
|
|
3042
3062
|
this.actions$
|
|
3043
3063
|
.pipe(ofType(dropPC), takeUntil(this.destroy$))
|
|
3044
3064
|
.subscribe(() => this.pcClient?.close());
|
|
3065
|
+
this.destroyRef.onDestroy(() => this.closePC());
|
|
3045
3066
|
}
|
|
3046
3067
|
init() {
|
|
3047
3068
|
this.signallingSrv.onConfig$
|
|
@@ -3176,7 +3197,7 @@ class WebRtcPlayerService extends SubService {
|
|
|
3176
3197
|
this.commandTelemetryService.trackStartCommand('EXT-Browser ICE connection');
|
|
3177
3198
|
this.commandTelemetryService.trackStartCommand('EXT-Signaling state change');
|
|
3178
3199
|
this.handlePeerConnectionEvents(this.pcClient);
|
|
3179
|
-
this.disconnect$.pipe(take(1)).subscribe(() => {
|
|
3200
|
+
this.disconnect$.pipe(take(1), takeUntil(this.destroy$)).subscribe(() => {
|
|
3180
3201
|
this.closePC();
|
|
3181
3202
|
});
|
|
3182
3203
|
}
|
|
@@ -3383,39 +3404,39 @@ class WebRtcPlayerService extends SubService {
|
|
|
3383
3404
|
}
|
|
3384
3405
|
handlePeerConnectionEvents(pcClient) {
|
|
3385
3406
|
fromEvent(pcClient, 'signalingstatechange')
|
|
3386
|
-
.pipe(map$1(() => this.pcClient?.signalingState), takeUntil(this.disconnect$))
|
|
3407
|
+
.pipe(map$1(() => this.pcClient?.signalingState), takeUntil(merge(this.disconnect$, this.destroy$)))
|
|
3387
3408
|
.subscribe((state) => {
|
|
3388
3409
|
Logger.info('[Signaling state change] |', state, '|');
|
|
3389
3410
|
this.commandTelemetryService.trackStopCommand('EXT-Signaling state change', true, { state, multi: true });
|
|
3390
3411
|
});
|
|
3391
3412
|
fromEvent(pcClient, 'iceconnectionstatechange')
|
|
3392
|
-
.pipe(map$1(() => this.pcClient?.iceConnectionState), takeUntil(this.disconnect$))
|
|
3413
|
+
.pipe(map$1(() => this.pcClient?.iceConnectionState), takeUntil(merge(this.disconnect$, this.destroy$)))
|
|
3393
3414
|
.subscribe((state) => {
|
|
3394
3415
|
Logger.info('[Browser ICE connection] |', state, '|');
|
|
3395
3416
|
this.commandTelemetryService.trackStopCommand('EXT-Browser ICE connection', true, { state, multi: true });
|
|
3396
3417
|
});
|
|
3397
3418
|
fromEvent(pcClient, 'icegatheringstatechange')
|
|
3398
|
-
.pipe(map$1(() => this.pcClient?.iceGatheringState), takeUntil(this.disconnect$))
|
|
3419
|
+
.pipe(map$1(() => this.pcClient?.iceGatheringState), takeUntil(merge(this.disconnect$, this.destroy$)))
|
|
3399
3420
|
.subscribe((state) => {
|
|
3400
3421
|
Logger.info('[Browser ICE gathering] |', state, '|');
|
|
3401
3422
|
this.commandTelemetryService.trackStopCommand('EXT-Browser ICE gathering', true, { state, multi: true });
|
|
3402
3423
|
});
|
|
3403
3424
|
fromEvent(pcClient, 'connectionstatechange')
|
|
3404
|
-
.pipe(map$1(() => this.pcClient?.connectionState), takeUntil(this.disconnect$))
|
|
3425
|
+
.pipe(map$1(() => this.pcClient?.connectionState), takeUntil(merge(this.disconnect$, this.destroy$)))
|
|
3405
3426
|
.subscribe((state) => {
|
|
3406
3427
|
Logger.info('[RTCPeerConnection state] |', state, '|');
|
|
3407
3428
|
});
|
|
3408
3429
|
fromEvent(pcClient, 'icecandidate')
|
|
3409
|
-
.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(this.disconnect$))
|
|
3430
|
+
.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$)))
|
|
3410
3431
|
.subscribe((candidate) => this.signallingSrv.send({ type: 'iceCandidate', candidate }));
|
|
3411
3432
|
// When the server creates the DataChannel (server-offer flow),
|
|
3412
3433
|
// we receive it here via the 'datachannel' event
|
|
3413
3434
|
fromEvent(pcClient, 'datachannel')
|
|
3414
|
-
.pipe(tap$1((e) => Logger.info(`[DATACHANNEL] Received remote DC: label="${e.channel.label}" id=${e.channel.id} protocol="${e.channel.protocol}" ordered=${e.channel.ordered}`)), map$1((e) => e.channel), takeUntil(this.disconnect$))
|
|
3435
|
+
.pipe(tap$1((e) => Logger.info(`[DATACHANNEL] Received remote DC: label="${e.channel.label}" id=${e.channel.id} protocol="${e.channel.protocol}" ordered=${e.channel.ordered}`)), map$1((e) => e.channel), takeUntil(merge(this.disconnect$, this.destroy$)))
|
|
3415
3436
|
.subscribe((datachannel) => {
|
|
3416
3437
|
this.dcClient = this.setupDataChannelCallbacks(datachannel);
|
|
3417
3438
|
});
|
|
3418
|
-
const onTrack$ = fromEvent(pcClient, 'track').pipe(takeUntil(this.disconnect$), filter((e) => !!e.track), tap$1((e) => Logger.info('Got track - ' +
|
|
3439
|
+
const onTrack$ = fromEvent(pcClient, 'track').pipe(takeUntil(merge(this.disconnect$, this.destroy$)), filter((e) => !!e.track), tap$1((e) => Logger.info('Got track - ' +
|
|
3419
3440
|
e.track.kind +
|
|
3420
3441
|
' id=' +
|
|
3421
3442
|
e.track.id +
|
|
@@ -3496,7 +3517,7 @@ class WebRtcPlayerService extends SubService {
|
|
|
3496
3517
|
fromEvent(datachannel, 'close')
|
|
3497
3518
|
.pipe(
|
|
3498
3519
|
// Only dispatch disconnect if not already reconnecting to avoid infinite loops
|
|
3499
|
-
filter(() => !this.isReconnecting()), take(1))
|
|
3520
|
+
filter(() => !this.isReconnecting()), take(1), takeUntil(this.destroy$))
|
|
3500
3521
|
.subscribe((e) => {
|
|
3501
3522
|
Logger.warn(`[DATACHANNEL] Data channel disconnected: ${datachannel.label}(${datachannel.id})`, e);
|
|
3502
3523
|
this.store.dispatch(disconnectStream({
|
|
@@ -3505,7 +3526,7 @@ class WebRtcPlayerService extends SubService {
|
|
|
3505
3526
|
}));
|
|
3506
3527
|
});
|
|
3507
3528
|
fromEvent(datachannel, 'error')
|
|
3508
|
-
.pipe(takeUntil(this.disconnect$))
|
|
3529
|
+
.pipe(takeUntil(merge(this.disconnect$, this.destroy$)))
|
|
3509
3530
|
.subscribe((e) => {
|
|
3510
3531
|
const message = `[DATACHANNEL] Data channel "${datachannel.label}" error. id:${datachannel.id}`;
|
|
3511
3532
|
Logger.error(message, e);
|
|
@@ -3513,7 +3534,7 @@ class WebRtcPlayerService extends SubService {
|
|
|
3513
3534
|
fromEvent(datachannel, 'open')
|
|
3514
3535
|
.pipe(tap$1(() => {
|
|
3515
3536
|
Logger.info(`[DATACHANNEL] Data channel "${datachannel.label}" connected. id:${datachannel.id}`);
|
|
3516
|
-
}), takeUntil(this.disconnect$))
|
|
3537
|
+
}), takeUntil(merge(this.disconnect$, this.destroy$)))
|
|
3517
3538
|
.subscribe(() => {
|
|
3518
3539
|
this.commandTelemetryService.trackStopCommand('EXT-DataChannel Connection');
|
|
3519
3540
|
this.onDataChannelConnected();
|
|
@@ -3526,7 +3547,7 @@ class WebRtcPlayerService extends SubService {
|
|
|
3526
3547
|
`[DATACHANNEL] Message received on "${datachannel.label}" id=${datachannel.id}, byteCode=${view[0]}, size=${view.byteLength}`,
|
|
3527
3548
|
);
|
|
3528
3549
|
}),*/
|
|
3529
|
-
map$1((e) => e.data), takeUntil(this.disconnect$))
|
|
3550
|
+
map$1((e) => e.data), takeUntil(merge(this.disconnect$, this.destroy$)))
|
|
3530
3551
|
.subscribe((data) => this.onDataChannelMessage$.next(data));
|
|
3531
3552
|
return datachannel;
|
|
3532
3553
|
}
|
|
@@ -4065,12 +4086,6 @@ class UnrealCommunicatorService {
|
|
|
4065
4086
|
this.requestQualityControl();
|
|
4066
4087
|
});
|
|
4067
4088
|
}
|
|
4068
|
-
destroy() {
|
|
4069
|
-
this.destroy$.next();
|
|
4070
|
-
// Recreate the Subject so it remains usable for the next session.
|
|
4071
|
-
// A completed Subject silently ignores all future next/complete calls.
|
|
4072
|
-
this.destroy$ = new Subject();
|
|
4073
|
-
}
|
|
4074
4089
|
sendCommandToUnreal(data) {
|
|
4075
4090
|
if (this.isPlaywrightMode()) {
|
|
4076
4091
|
return;
|
|
@@ -4656,12 +4671,17 @@ class RegionsPingService {
|
|
|
4656
4671
|
this.httpClient = inject(HttpClient);
|
|
4657
4672
|
this.store = inject(Store);
|
|
4658
4673
|
this.unrealInitialConfig = inject(UNREAL_CONFIG, { optional: true });
|
|
4674
|
+
this.destroyRef = inject(DestroyRef);
|
|
4675
|
+
// Aborted on injector destruction. Cascades through every in-flight
|
|
4676
|
+
// fetch so background pings stop the moment the service dies.
|
|
4677
|
+
this.destroyAbort = new AbortController();
|
|
4659
4678
|
this.config = {
|
|
4660
4679
|
ping_count: 2,
|
|
4661
4680
|
ping_timeout: 2000,
|
|
4662
4681
|
max_parallel_requests: 10,
|
|
4663
4682
|
bust_cache: true,
|
|
4664
4683
|
};
|
|
4684
|
+
this.destroyRef.onDestroy(() => this.destroyAbort.abort());
|
|
4665
4685
|
}
|
|
4666
4686
|
getFastest(regionListUrl = this.unrealInitialConfig?.regionsPingUrl || '') {
|
|
4667
4687
|
// Self-announce every lifecycle waypoint of the region-discovery
|
|
@@ -4684,13 +4704,27 @@ class RegionsPingService {
|
|
|
4684
4704
|
return this.httpClient.get(regionListUrl);
|
|
4685
4705
|
}
|
|
4686
4706
|
async startPinging(endpoints, defaultTimeout) {
|
|
4707
|
+
// Two-tier cancellation:
|
|
4708
|
+
// - destroyAbort: service teardown (injector destroy)
|
|
4709
|
+
// - winnerAbort: first successful region resolves Promise.any
|
|
4710
|
+
// Both aborts cascade into every in-flight fetch so losing pings stop
|
|
4711
|
+
// immediately instead of running their full ping_count × timeout budget.
|
|
4712
|
+
const winnerAbort = new AbortController();
|
|
4713
|
+
const stopSignal = AbortSignal.any([
|
|
4714
|
+
winnerAbort.signal,
|
|
4715
|
+
this.destroyAbort.signal,
|
|
4716
|
+
]);
|
|
4687
4717
|
const semaphore = new Semaphore(this.config.max_parallel_requests);
|
|
4688
4718
|
const tasks = endpoints.map(async ({ region_code, url, timeout }) => {
|
|
4689
4719
|
await semaphore.acquire();
|
|
4690
4720
|
try {
|
|
4691
|
-
|
|
4692
|
-
|
|
4721
|
+
if (stopSignal.aborted) {
|
|
4722
|
+
throw new Error('aborted');
|
|
4723
|
+
}
|
|
4724
|
+
const result = await this.checkLatency(url, timeout ?? defaultTimeout, stopSignal);
|
|
4725
|
+
if (result.length < 2) {
|
|
4693
4726
|
throw new Error('insufficient pings');
|
|
4727
|
+
}
|
|
4694
4728
|
return { region_code, result };
|
|
4695
4729
|
}
|
|
4696
4730
|
finally {
|
|
@@ -4698,13 +4732,15 @@ class RegionsPingService {
|
|
|
4698
4732
|
}
|
|
4699
4733
|
});
|
|
4700
4734
|
try {
|
|
4701
|
-
|
|
4735
|
+
const winner = await Promise.any(tasks);
|
|
4736
|
+
winnerAbort.abort();
|
|
4737
|
+
return winner;
|
|
4702
4738
|
}
|
|
4703
4739
|
catch {
|
|
4704
4740
|
return null;
|
|
4705
4741
|
}
|
|
4706
4742
|
}
|
|
4707
|
-
async checkLatency(endpoint, timeout) {
|
|
4743
|
+
async checkLatency(endpoint, timeout, externalSignal) {
|
|
4708
4744
|
const pingResults = [];
|
|
4709
4745
|
const sep = endpoint.includes('?') ? '&' : '?';
|
|
4710
4746
|
for (let i = 0; i < this.config.ping_count; i++) {
|
|
@@ -4721,7 +4757,7 @@ class RegionsPingService {
|
|
|
4721
4757
|
Expires: '0',
|
|
4722
4758
|
},
|
|
4723
4759
|
credentials: 'omit',
|
|
4724
|
-
}, timeout);
|
|
4760
|
+
}, timeout, externalSignal);
|
|
4725
4761
|
}
|
|
4726
4762
|
catch {
|
|
4727
4763
|
// intentional: finally pushes the elapsed time as the latency sample
|
|
@@ -4733,9 +4769,11 @@ class RegionsPingService {
|
|
|
4733
4769
|
}
|
|
4734
4770
|
return pingResults;
|
|
4735
4771
|
}
|
|
4736
|
-
async fetchWithTimeout(url, options = {}, timeout) {
|
|
4772
|
+
async fetchWithTimeout(url, options = {}, timeout, externalSignal) {
|
|
4737
4773
|
const controller = new AbortController();
|
|
4738
|
-
options.signal =
|
|
4774
|
+
options.signal = externalSignal
|
|
4775
|
+
? AbortSignal.any([controller.signal, externalSignal])
|
|
4776
|
+
: controller.signal;
|
|
4739
4777
|
const timeoutId = setTimeout(() => controller.abort(new DOMException('Request timed out', 'TimeoutError')), timeout);
|
|
4740
4778
|
try {
|
|
4741
4779
|
const response = await fetch(url, options);
|
|
@@ -4752,7 +4790,7 @@ class RegionsPingService {
|
|
|
4752
4790
|
}
|
|
4753
4791
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: RegionsPingService, decorators: [{
|
|
4754
4792
|
type: Injectable
|
|
4755
|
-
}] });
|
|
4793
|
+
}], ctorParameters: () => [] });
|
|
4756
4794
|
|
|
4757
4795
|
class StreamStatusTelemetryService extends SubService {
|
|
4758
4796
|
constructor() {
|
|
@@ -5711,7 +5749,7 @@ class AnalyticsService extends SubService {
|
|
|
5711
5749
|
});
|
|
5712
5750
|
}
|
|
5713
5751
|
monitorMouse() {
|
|
5714
|
-
const stop$ = this.#stopMouse
|
|
5752
|
+
const stop$ = merge(this.#stopMouse$, this.destroy$);
|
|
5715
5753
|
const element = document.getElementById('streamingVideo');
|
|
5716
5754
|
if (!element) {
|
|
5717
5755
|
return;
|