@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(...arguments);
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$.pipe(take(1)).subscribe(() => this.destroy());
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: null, target: i0.ɵɵFactoryTarget.Injectable }); }
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
- const stop$ = merge(this.abort$, this.destroy$);
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(stop$))
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(stop$))
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. Each inner createWebSocket also
2722
- // receives abort$ independently, so mid-connection attempts are
2723
- // canceled too.
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(takeUntil(this.abort$), tapLog('WS CREATED'), tap((ws) => {
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
- const result = await this.checkLatency(url, timeout ?? defaultTimeout);
4692
- if (result.length < 2)
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
- return await Promise.any(tasks);
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 = controller.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$.asObservable();
5752
+ const stop$ = merge(this.#stopMouse$, this.destroy$);
5715
5753
  const element = document.getElementById('streamingVideo');
5716
5754
  if (!element) {
5717
5755
  return;