@3dsource/angular-unreal-module 0.0.123-dev.0 → 0.0.123

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.
@@ -1,12 +1,12 @@
1
1
  import * as i0 from '@angular/core';
2
2
  import { InjectionToken, inject, ChangeDetectionStrategy, Component, Injectable, DestroyRef, effect, untracked, computed, signal, Pipe, input, ElementRef, viewChild, afterNextRender, HostListener, output, makeEnvironmentProviders, provideEnvironmentInitializer } from '@angular/core';
3
3
  import { SourceButtonComponent, SourceLoadingComponent } from '@3dsource/source-ui-native';
4
- import { merge, map, scan, timestamp, startWith, pairwise, concatMap, of, animationFrameScheduler, interval, Observable, skip, switchMap, concat, delay, debounceTime, timer, take, timeout, mergeMap, EMPTY, Subject, share, takeUntil as takeUntil$1, shareReplay, from, fromEvent, throwError, defer, retry, tap as tap$1, throttleTime, NEVER, asyncScheduler, filter as filter$1, withLatestFrom as withLatestFrom$1, Subscription, bufferTime, combineLatestWith, distinctUntilChanged as distinctUntilChanged$1 } from 'rxjs';
4
+ import { merge, map, scan, timestamp, startWith, pairwise, concatMap, of, animationFrameScheduler, interval, Observable, skip, switchMap, concat, delay, debounceTime, timer, take, timeout, mergeMap, EMPTY, Subject, share, takeUntil as takeUntil$1, shareReplay, from, fromEvent, throwError, defer, retry, repeat, tap as tap$1, throttleTime, NEVER, asyncScheduler, filter as filter$1, withLatestFrom as withLatestFrom$1, Subscription, bufferTime, combineLatestWith, distinctUntilChanged as distinctUntilChanged$1 } from 'rxjs';
5
5
  import { createAction, props, createReducer, on, createFeature, createSelector, Store, provideState } from '@ngrx/store';
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, share as share$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, finalize, first, 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';
@@ -207,8 +207,31 @@ const CONSOLE_COMMAND_DISABLE_MESSAGES = 'DisableAllScreenMessages';
207
207
  const CONSOLE_COMMAND_PIXEL_QUALITY = 'PixelStreaming.FreezeFrameQuality 95';
208
208
  const FULL_HD_WIDTH = 1920;
209
209
  const FULL_HD_HEIGHT = 1080;
210
- const WS_TIMEOUT = 2000;
210
+ // Bound on a single WS upgrade attempt (`createWebSocket`). On cold
211
+ // connections the TCP+TLS+WS handshake can legitimately take 3-5 s
212
+ // (cold DNS, TLS session miss). 2 s was too aggressive — first
213
+ // attempts kept timing out and the second attempt (with warm caches)
214
+ // would connect in <500 ms. 10 s gives slow networks room without
215
+ // letting a truly hung handshake hold up `connectToCirrus` forever.
216
+ const WS_TIMEOUT = 6000;
211
217
  const POLLING_TIME = 4000;
218
+ // Staged retry policy for `SignallingService.connectToSignaling`:
219
+ // INNER_RETRY_LIMIT = max number of WS+orchestration attempts before
220
+ // the pipeline gives up on the current region and bubbles the error
221
+ // to the outer retry. URL rotation (`urlGen`) advances across these.
222
+ // OUTER_RETRY_DELAY = backoff between region re-picks. Intentionally
223
+ // longer than POLLING_TIME so region discovery is rare relative to
224
+ // WS retries.
225
+ const INNER_RETRY_LIMIT = 3;
226
+ const OUTER_RETRY_DELAY = POLLING_TIME * 2;
227
+ // Extra WS-only retries on the SAME wsUrl (cached AwsInstance) BEFORE
228
+ // falling through to a URL-rotating retry that re-polls orchestration.
229
+ // Why: a fresh `default` poll asks orchestrator to RESERVE a new
230
+ // instance — the previous reservation is orphaned (server-side leak).
231
+ // One quick same-wsUrl retry catches transient WS failures (network
232
+ // blip, brief unavailability) without burning a second reservation.
233
+ // Total WS attempts before URL rotation = 1 + WS_RETRY_PER_INSTANCE.
234
+ const WS_RETRY_PER_INSTANCE = 1;
212
235
  const WS_OPEN_STATE = 1;
213
236
  const DEFAULT_AFK_TIMEOUT_PERIOD = 15;
214
237
  const DEFAULT_AFK_TIMEOUT = 600; // 10 minutes
@@ -1672,9 +1695,9 @@ class AFKService extends SubService {
1672
1695
  resetAfkWarningTimer() {
1673
1696
  if (this.active) {
1674
1697
  this.clearTimers();
1675
- this.warnTimer = setTimeout(() => {
1676
- this.showAfkOverlay();
1677
- }, (this.warnTimeout() || DEFAULT_AFK_TIMEOUT) * 1000);
1698
+ // `selectWarnTimeout` already falls back to DEFAULT_AFK_TIMEOUT when
1699
+ // config is missing, so warnTimeout() always returns a positive number.
1700
+ this.warnTimer = setTimeout(() => this.showAfkOverlay(), this.warnTimeout() * 1000);
1678
1701
  }
1679
1702
  }
1680
1703
  /**
@@ -1694,7 +1717,8 @@ class AFKService extends SubService {
1694
1717
  }
1695
1718
  reset() {
1696
1719
  this.hideOverlay();
1697
- clearInterval(this.countdownTimer);
1720
+ // startAfkWarningTimer → resetAfkWarningTimer → clearTimers() already
1721
+ // clears the countdown interval, no separate clearInterval needed.
1698
1722
  this.startAfkWarningTimer();
1699
1723
  }
1700
1724
  showAfkOverlay() {
@@ -2202,11 +2226,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
2202
2226
  type: Injectable
2203
2227
  }], ctorParameters: () => [] });
2204
2228
 
2205
- class CommandTelemetryService {
2229
+ class CommandTelemetryService extends SubService {
2206
2230
  constructor() {
2207
- this.actions$ = inject(Actions);
2208
- this.destroyRef = inject(DestroyRef);
2209
- this.destroy$ = new Subject();
2231
+ super();
2210
2232
  this.appId = 'metabox-telemetry';
2211
2233
  this.commandsSent = {};
2212
2234
  this.exileTimeout = 60000;
@@ -2214,13 +2236,7 @@ class CommandTelemetryService {
2214
2236
  this.commandSequenceNumber = 0;
2215
2237
  this.payloads = [];
2216
2238
  this.httpClient = inject(HttpClient);
2217
- this.unrealConfig = inject(UNREAL_CONFIG, {
2218
- optional: true,
2219
- });
2220
- this.destroyRef.onDestroy(() => {
2221
- this.destroy$.next();
2222
- this.destroy$.complete();
2223
- });
2239
+ this.unrealConfig = inject(UNREAL_CONFIG, { optional: true });
2224
2240
  this.init();
2225
2241
  }
2226
2242
  init() {
@@ -2654,32 +2670,35 @@ class SignallingService extends SubService {
2654
2670
  this.onConfig$ = new Subject();
2655
2671
  this.onWebRtcIce$ = new Subject();
2656
2672
  this.onWebRtcAnswer$ = new Subject();
2657
- // Fan-in produced by abortEstablishingConnection$ in unreal.effects.ts
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.
2661
- this.abort$ = this.actions$.pipe(ofType(abortEstablishingConnection), tap(() => this.close({
2662
- code: 1000,
2663
- reason: DisconnectReason.AbortEstablishingConnection,
2664
- })), share$1());
2673
+ // Pure signal — no side-effects. The `close()` is wired ONCE in init()
2674
+ // below so a single abort event triggers exactly one close, regardless
2675
+ // of how many pipelines have `takeUntil(this.abort$)` attached.
2676
+ this.abort$ = this.actions$.pipe(ofType(abortEstablishingConnection));
2665
2677
  this.ws = null;
2666
2678
  this.wsMsgHandlers = {};
2667
2679
  this.init();
2668
- this.destroyRef.onDestroy(() => this.close({
2669
- code: 1000,
2670
- reason: DisconnectReason.AbortEstablishingConnection,
2671
- }));
2672
2680
  }
2673
2681
  init() {
2674
2682
  if (this.unrealConfig?.playwright) {
2675
2683
  return;
2676
2684
  }
2677
2685
  this.setHandlersFromStream();
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.
2686
+ // Single owner of the "close on abort" side-effect. Previously this
2687
+ // lived as a `tap` inside `abort$` itself, which made every
2688
+ // `takeUntil(this.abort$)` subscriber fire its own close().
2689
+ this.abort$.pipe(takeUntil(this.destroy$)).subscribe(() => {
2690
+ this.close({
2691
+ code: 1000,
2692
+ reason: DisconnectReason.AbortEstablishingConnection,
2693
+ });
2694
+ });
2695
+ // `dataChannelReady` is a per-session event. Bind ONLY to destroy$, not
2696
+ // to abort$ — `abort$` fires on every destroyUnrealScene (route swap),
2697
+ // and `init()` runs once in the constructor. If these subs were torn
2698
+ // down by abort$, a second in-injector session (e.g. navigating between
2699
+ // two routes that both host <app-unreal-scene>) would never send
2700
+ // `p2pEstablished` / `streamPreparedForUserInteraction`. `send()` is
2701
+ // already a no-op on a closed WS, so there's no risk of stray messages.
2683
2702
  this.actions$
2684
2703
  .pipe(ofType(dataChannelReady), takeUntil(this.destroy$))
2685
2704
  .subscribe(() => {
@@ -2709,36 +2728,93 @@ class SignallingService extends SubService {
2709
2728
  return EMPTY;
2710
2729
  }
2711
2730
  Logger.log('CONNECT TO SIGNALLING');
2731
+ // Side-effects that belong to one logical `connectToSignaling`
2732
+ // invocation (NOT one retry attempt). Wrapping them in a `defer`
2733
+ // makes them fire on subscribe (lazy), but only ONCE — the retry
2734
+ // below sits INSIDE this defer's projection so re-subscribing the
2735
+ // inner pipeline does not re-trigger these.
2712
2736
  return defer(() => {
2713
- // Preparation for WebSocket Orchestration. Inside defer so these
2714
- // side effects only fire once the caller actually subscribes.
2715
2737
  this.correlationId = generateUuid();
2716
2738
  this.commandTelemetryService.reset();
2717
2739
  this.startEstablishingConnection();
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$)));
2719
- }).pipe(
2720
- // Retry the whole attempt on error, advancing to the next URL each time.
2721
- // Note: this currently re-runs `getRegion()` on every retry — see
2722
- // `SIGNALLING.md` for the leak description and proposed fix. The
2723
- // dispatch below is observability-only so the viewer can show the
2724
- // retry storm in the Anomalies table without the fix being applied.
2740
+ // Staged retry:
2741
+ // * `getRegion()` is pulled in front of the retry — region
2742
+ // discovery happens once per outer cycle.
2743
+ // * `connectAttempt(urls)` owns inner retry + URL rotation +
2744
+ // WS connect. WS/orch failures stay inside it (capped at
2745
+ // `INNER_RETRY_LIMIT`).
2746
+ // * If the inner retry exhausts, the error propagates to the
2747
+ // OUTER retry below which re-runs `getRegion()` (rare, real
2748
+ // "region down" case). Outer delay > inner so escalation is
2749
+ // bounded.
2750
+ const connectCycle$ = this.getRegion().pipe(tap((region) => this.region.set(region ?? '')), map$1((region) => this.adaptUrlsToRegion(urlList, region)), switchMap((urls) => this.connectAttempt(urls)), takeUntil(this.abort$));
2751
+ return connectCycle$.pipe(retry({
2752
+ delay: (err, attempt) => {
2753
+ this.store.dispatch(signalingRetryAttempted({
2754
+ attempt,
2755
+ reason: err?.message ?? 'unknown',
2756
+ kind: 'region-cycle',
2757
+ }));
2758
+ Logger.info(`Outer retry (region re-pick) in ${OUTER_RETRY_DELAY}ms. Attempt# ${attempt}.`, err?.message);
2759
+ return timer(OUTER_RETRY_DELAY);
2760
+ },
2761
+ }));
2762
+ }).pipe(takeUntil(this.abort$));
2763
+ }
2764
+ /**
2765
+ * One inner cycle: rotate `urlGen` over the pool, resolve via
2766
+ * {@link getAwsInstance}, open WS via {@link connectToCirrus},
2767
+ * retry up to {@link INNER_RETRY_LIMIT} on any hard failure. The
2768
+ * urlGen lives in this closure so retries advance through the
2769
+ * pool. Exhaustion bubbles to {@link connectToSignaling}'s outer
2770
+ * retry which re-pings the region.
2771
+ */
2772
+ connectAttempt(urls) {
2773
+ const urlGen = getActiveUrl(urls);
2774
+ return defer(() => this.getAwsInstance(`${urlGen.next().value}`)).pipe(tap((awsInstance) => this.store.dispatch(setAwsInstance(awsInstance))), switchMap(({ wsUrl }) =>
2775
+ // ── WS-only retry tier ─────────────────────────────────────────────
2776
+ // Tries the SAME `wsUrl` up to `WS_RETRY_PER_INSTANCE` extra times
2777
+ // before letting the error bubble up to the outer (URL-rotating)
2778
+ // retry. This is important because the outer retry calls
2779
+ // `getAwsInstance(url)` again, which polls orchestration again,
2780
+ // which causes orchestrator to RESERVE A NEW instance — the
2781
+ // previous reservation is orphaned (server-side leak).
2782
+ //
2783
+ // The same-wsUrl retry catches transient WS failures (network
2784
+ // blip, brief region unavailability, slow cold handshake) without
2785
+ // burning a second reservation. If WS still fails after these
2786
+ // extra attempts, the instance is genuinely unusable and we
2787
+ // accept the cost of rotating.
2788
+ this.connectToCirrus(wsUrl ?? '').pipe(filter(Truthy), take(1), retry({
2789
+ count: WS_RETRY_PER_INSTANCE,
2790
+ delay: (err, attempt) => {
2791
+ this.store.dispatch(signalingRetryAttempted({
2792
+ attempt,
2793
+ reason: err?.message ?? 'unknown',
2794
+ kind: 'ws-retry',
2795
+ }));
2796
+ this.showStatusMessage(UnrealStatusMessage.CONNECTING_TO_SESSION);
2797
+ Logger.info(`WS-only retry on SAME url in ${POLLING_TIME}ms. Attempt# ${attempt}.`, err?.message);
2798
+ return timer(POLLING_TIME);
2799
+ },
2800
+ }))),
2801
+ // ── URL-rotating retry tier ─────────────────────────────────────────
2802
+ // Triggers only when the WS-only retries above are exhausted. Each
2803
+ // attempt advances `urlGen` → new `getAwsInstance(url)` → new
2804
+ // orchestration poll → new AwsInstance reservation.
2725
2805
  retry({
2806
+ count: INNER_RETRY_LIMIT,
2726
2807
  delay: (err, attempt) => {
2727
2808
  this.store.dispatch(signalingRetryAttempted({
2728
2809
  attempt,
2729
2810
  reason: err?.message ?? 'unknown',
2730
2811
  kind: 'connect',
2731
2812
  }));
2732
- Logger.info(`Retrying Signalling Connection in ${POLLING_TIME}ms. Attempt# ${attempt}.`, err?.message);
2813
+ this.showStatusMessage(UnrealStatusMessage.CONNECTING_TO_SESSION);
2814
+ Logger.info(`Inner retry (WS+orch) in ${POLLING_TIME}ms. Attempt# ${attempt}.`, err?.message);
2733
2815
  return timer(POLLING_TIME);
2734
2816
  },
2735
- // count: 10, // Optional: cap the number of attempts.
2736
- }),
2737
- // abort$ sits OUTSIDE retry, so it kills the entire retry loop,
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$)));
2817
+ }));
2742
2818
  }
2743
2819
  startEstablishingConnection() {
2744
2820
  this.showStatusMessage(UnrealStatusMessage.STARTING_YOUR_SESSION);
@@ -2753,177 +2829,117 @@ class SignallingService extends SubService {
2753
2829
  });
2754
2830
  }
2755
2831
  /**
2756
- * Resolves an active AWS *signaling* instance and returns a WebSocket endpoint.
2757
- *
2758
- * The observable produced by this method is **cold** and executes inside a `defer`,
2759
- * so each subscription (including those triggered by `retry`) pulls the **next**
2760
- * URL candidate from a generator created by `getActiveUrl(urlsPool)`.
2761
- *
2762
- * ### Execution flow
2763
- *
2764
- * 1. **Telemetry** — Starts a `getSignaling` span for end-to-end timing.
2765
- * 2. **Candidate URL selection** — A stateful generator (`urlGen`) is created
2766
- * from `urlsPool`. Each resubscription (e.g., via `retry`) advances to the next URL.
2767
- * 3. **Short-circuit for WS URLs** — If the picked candidate already matches
2768
- * `ws://` or `wss://`, emit `{ wsUrl, pollingUrl: null, instanceName: null }`
2769
- * immediately and complete (no HTTP request).
2770
- * 4. **Await client/view identifiers** — `this.store.select(selectClientAndViewIds)`
2771
- * is tapped for logging (missing IDs) and then filtered to require both `clientId`
2772
- * and `viewId` to be truthy.
2773
- * 5. **Single in-flight HTTP orchestration call** — Uses `exhaustMap` to issue
2774
- * `GET {signalingUrl}{clientId}/{viewId}`. New `{clientId, viewId}` emissions
2775
- * while the request is in flight are **ignored** until completion, preventing overlap.
2776
- * 6. **Per-attempt timeout** — `timeout(WS_TIMEOUT)` caps how long we wait for a response.
2777
- * On timeout, the attempt errors and the outer `retry` schedules the next URL.
2778
- * 7. **Response validation and progress reporting** — A `filter`:
2779
- * - Calls `TelemetryStop('getSignaling', { ...data, multi: true })` on receipt.
2780
- * - If `data.signallingServer === ''` or `data.error`:
2781
- * shows a status message, optionally dispatches `setStatusPercentSignallingServer`
2782
- * with `data.info.percent`, and **throws** to fail the attempt (triggers `retry`).
2783
- * 8. **Mapping to `AwsInstance`** — Converts the validated orchestration payload into:
2784
- * - `wsUrl`: via `httpUrlToWs(\`\${location.protocol}//\${data.signallingServer}\`)`
2785
- * - `pollingUrl`: the base `signalingUrl` that succeeded
2786
- * - `instanceName`: the (validated) `data.signallingServer`
2787
- * 9. **Retry policy** — On *any* upstream error (timeout, HTTP error, invalid payload),
2788
- * `retry({ delay })`:
2789
- * - Shows a “connecting” status,
2790
- * - Logs the attempt number,
2791
- * - Waits `WS_TIMEOUT` ms (via `timer`) and **resubscribes**, advancing `urlGen`.
2792
- *
2793
- * ### Concurrency semantics
2794
- *
2795
- * - `exhaustMap` guarantees a **single** HTTP request per attempt; later
2796
- * `{clientId, viewId}` emissions are ignored until the current request completes.
2797
- *
2798
- * ### Telemetry semantics
2799
- *
2800
- * - `trackStartCommand('EXT-getSignaling')` begins before work.
2801
- * - `trackStopCommand('getSignaling', {...})` runs on receipt of an orchestration response.
2802
- * If an attempt fails (e.g., timeout), stopping the span is the responsibility of
2803
- * your telemetry layer or a `finalize` elsewhere if desired.
2804
- *
2805
- * ### Error & retry semantics
2806
- *
2807
- * - Missing/never-emitted IDs → timeout → retry with next URL.
2808
- * - HTTP/network error → retry with next URL.
2809
- * - Invalid orchestration payload (`error` set or empty `signallingServer`) → throw in
2810
- * validation filter → retry with next URL.
2811
- * - Short-circuit WS path **does not** retry (emits once, completes).
2812
- *
2813
- * @param urlsPool - Ordered list of base URLs to probe. Retries advance through this list
2814
- * until a working signaling server is found. A candidate may also be a direct `ws://`/`wss://`
2815
- * URL to short-circuit HTTP orchestration.
2816
- *
2817
- * @returns Observable that emits exactly one {@link AwsInstance} on success and then completes.
2818
- * On failure, the stream errors; the built-in `retry` operator re-subscribes after `WS_TIMEOUT`
2819
- * and advances to the next URL candidate.
2820
- *
2821
- * @throws Emits an error within the observable chain (not a thrown synchronous exception) when:
2822
- * - The HTTP request fails or times out.
2823
- * - The orchestration response indicates an error or an empty `signallingServer`.
2824
- * - Any other operator in the chain surfaces an error.
2825
- *
2826
- * @remarks
2827
- * - The URL generator (`urlGen`) is created **outside** `defer`, so resubscriptions
2828
- * advance the pool. Creating it inside `defer` would restart from the first URL each retry.
2829
- * - `location.protocol` is used to derive `ws` vs. `wss`. If this may run in non-browser
2830
- * contexts (e.g., SSR), guard or abstract this logic.
2831
- * - Consider capping retries with `count` in `retry({ count, delay })` when appropriate.
2832
- *
2833
- * @example
2834
- * ```ts
2835
- * getAwsInstance(['https://signaling.example.com/', 'wss://direct.example.com'])
2836
- * .subscribe({
2837
- * next: ({ wsUrl }) => connect(wsUrl),
2838
- * error: (e) => console.error('Unable to establish signaling:', e),
2839
- * });
2840
- * ```
2841
- *
2842
- * @see httpUrlToWs
2843
- * @see selectClientAndViewIds
2844
- * @see setStatusPercentSignallingServer
2832
+ * Resolve one URL to an `AwsInstance`. Dispatches to
2833
+ * {@link directWsInstance} for `ws://` / `wss://` URLs, otherwise
2834
+ * to {@link pollOrchestration}.
2835
+ */
2836
+ getAwsInstance(url) {
2837
+ return url.match(/^ws(s?):\/\//i)
2838
+ ? this.directWsInstance(url)
2839
+ : this.pollOrchestration(url);
2840
+ }
2841
+ /**
2842
+ * Direct `ws://` / `wss://` short-circuit — no HTTP orchestration.
2843
+ * Waits for `clientId` and `viewId` to be truthy in the store, then
2844
+ * emits a `Direct Connection` `AwsInstance` and completes.
2845
+ */
2846
+ directWsInstance(url) {
2847
+ return this.store.select(selectClientAndViewIds).pipe(filter(({ clientId, viewId }) => !!clientId && !!viewId), take(1), map$1(() => ({
2848
+ wsUrl: url,
2849
+ pollingUrl: null,
2850
+ instanceName: 'Direct Connection',
2851
+ })));
2852
+ }
2853
+ /**
2854
+ * HTTP-orchestration path. Brackets {@link pollLoop} with the
2855
+ * `pollingStarted`/`pollingEnded` actions and the `EXT-getSignaling`
2856
+ * telemetry span (start in `defer`, stop in `finalize` with the
2857
+ * last response captured in the closure). Maps the final ready
2858
+ * response to an `AwsInstance`.
2845
2859
  */
2846
- getAwsInstance(urlsPool) {
2847
- this.commandTelemetryService.trackStartCommand('EXT-getSignaling');
2848
- // Stateful iterator over candidate URLs; retries advance this sequence.
2849
- const urlGen = getActiveUrl(urlsPool);
2860
+ pollOrchestration(url) {
2850
2861
  return defer(() => {
2851
- const signalingUrl = `${urlGen.next().value}`;
2852
- // Short-circuit if the candidate is already a WS endpoint.
2853
- if (signalingUrl.match(/^ws(s?):\/\//i)) {
2854
- return this.store.select(selectClientAndViewIds).pipe(filter(({ clientId, viewId }) => !!clientId && !!viewId), map$1(() => {
2855
- return {
2856
- wsUrl: signalingUrl,
2857
- pollingUrl: null,
2858
- instanceName: 'Direct Connection',
2859
- };
2860
- }));
2861
- }
2862
- // Old (HTTP-polling) orchestration flow — mark the round trip so
2863
- // the signaling-phase timeline can show it as a distinct segment.
2862
+ let lastResp;
2864
2863
  this.store.dispatch(pollingStarted());
2865
- return this.store.select(selectClientAndViewIds).pipe(
2866
- // Log missing IDs without altering the stream.
2867
- tap(({ clientId, viewId }) => {
2864
+ this.commandTelemetryService.trackStartCommand('EXT-getSignaling');
2865
+ return this.store.select(selectClientAndViewIds).pipe(tap(({ clientId, viewId }) => {
2868
2866
  if (!clientId || !viewId) {
2869
2867
  Logger.error('Client ID or View ID is not set');
2870
2868
  }
2871
- }),
2872
- // Only proceed when both identifiers are available.
2873
- filter(({ clientId, viewId }) => !!clientId && !!viewId),
2874
- // Take first value
2875
- take(1),
2876
- // Ensure a single in-flight HTTP request per attempt.
2877
- exhaustMap(({ clientId, viewId }) => this.httpClient.get(`${signalingUrl}${clientId}/${viewId}`)),
2878
- // Bound the time spent waiting for the orchestration response.
2879
- timeout(POLLING_TIME),
2880
- // Validate response; report progress; fail fast on invalid payloads.
2881
- filter((data) => {
2882
- this.commandTelemetryService.trackStopCommand('EXT-getSignaling', true, { ...data, multi: true });
2883
- if (data.signallingServer === '' || data.error) {
2884
- this.showStatusMessage(data?.mm_message || 'Server not found');
2885
- if (data?.info) {
2886
- this.store.dispatch(setStatusPercentSignallingServer({
2887
- percent: data.info.percent,
2888
- }));
2889
- }
2890
- // Throwing here fails the attempt and activates the retry strategy.
2891
- throw new Error(`Orchestration message => ${data?.mm_message || 'Polling retry'}`);
2892
- }
2893
- return true;
2894
- }),
2895
- // Map a valid orchestration payload into the final AwsInstance.
2896
- map$1((data) => {
2869
+ }), filter(({ clientId, viewId }) => !!clientId && !!viewId), take(1), switchMap(({ clientId, viewId }) => this.pollLoop(`${url}${clientId}/${viewId}`).pipe(tap((data) => {
2870
+ lastResp = data;
2871
+ }))), map$1((data) => ({
2872
+ wsUrl: httpUrlToWs(`${location.protocol}//${data.signallingServer}`),
2873
+ pollingUrl: url,
2874
+ instanceName: data.signallingServer ?? '',
2875
+ })), finalize(() => {
2876
+ this.commandTelemetryService.trackStopCommand('EXT-getSignaling', true, { ...(lastResp ?? {}), multi: true });
2897
2877
  this.store.dispatch(pollingEnded());
2898
- const wsUrl = httpUrlToWs(`${location.protocol}//${data.signallingServer}`);
2899
- return {
2900
- wsUrl,
2901
- pollingUrl: signalingUrl,
2902
- instanceName: data.signallingServer ?? '',
2903
- };
2904
2878
  }));
2905
- }).pipe(
2906
- // Retry the whole attempt on error, advancing to the next URL each time.
2907
- retry({
2908
- delay: (err, attempt) => {
2909
- this.store.dispatch(signalingRetryAttempted({
2910
- attempt,
2911
- reason: err?.message ?? 'unknown',
2912
- kind: 'orchestration',
2879
+ });
2880
+ }
2881
+ /**
2882
+ * Poll the SAME `url` until ready, or until HTTP 4xx says the URL
2883
+ * is dead.
2884
+ *
2885
+ * | Response | Class | Action |
2886
+ * | ---------------------------------------- | ---------- | ---------------------------- |
2887
+ * | 200 `{ signallingServer: 'cirrus', ... }`| ✅ Ready | `first()` emits, loop ends |
2888
+ * | 200 empty `signallingServer` (any error/info/mm_message) | ⏳ Progress | Dispatch status+percent, re-poll same URL |
2889
+ * | HTTP 4xx (except 429) | ❌ Hard | Re-throw → caller rotates URL |
2890
+ * | HTTP 5xx / 429 / timeout / network | ⚠️ Soft | `EMPTY` → `repeat` same URL |
2891
+ *
2892
+ * `data.error` is NOT treated as fatal — the backend reuses it for
2893
+ * progress strings like `"Ready in ~2m 23s"`. The only fatal exits
2894
+ * are HTTP 4xx and `abort$` upstream.
2895
+ */
2896
+ pollLoop(url) {
2897
+ return defer(() => this.httpClient.get(url)).pipe(timeout(POLLING_TIME), catchError((err) => {
2898
+ // HTTP 4xx (except 429) = the URL itself is dead → rotate.
2899
+ const status = err?.status;
2900
+ if (typeof status === 'number' &&
2901
+ status >= 400 &&
2902
+ status < 500 &&
2903
+ status !== 429) {
2904
+ Logger.warn(`Polling HARD error on ${url} (HTTP ${status}); rotating URL`);
2905
+ return throwError(() => err);
2906
+ }
2907
+ // Everything else = soft → re-poll same URL.
2908
+ Logger.warn(`Polling soft error on ${url} (${err?.message ?? err}); re-polling same URL`);
2909
+ return EMPTY;
2910
+ }), tap((data) => {
2911
+ // Ready response is handled by `first()` below — nothing to
2912
+ // dispatch here. Empty `signallingServer` = progress.
2913
+ if (data.signallingServer)
2914
+ return;
2915
+ this.showStatusMessage(data?.mm_message || 'Server not found');
2916
+ if (data?.info) {
2917
+ this.store.dispatch(setStatusPercentSignallingServer({
2918
+ percent: data.info.percent,
2913
2919
  }));
2914
- this.showStatusMessage(UnrealStatusMessage.CONNECTING_TO_SESSION);
2915
- Logger.log(`Retrying with new URL in ${POLLING_TIME}ms. Attempt# ${attempt}`, err?.message);
2916
- return timer(POLLING_TIME);
2917
- },
2918
- // count: 10, // Optional: cap the number of attempts.
2919
- }));
2920
+ }
2921
+ }),
2922
+ // Re-subscribe (= poll again) after each completed response OR
2923
+ // after `catchError` swallowed a transient failure.
2924
+ repeat({ delay: () => timer(POLLING_TIME) }),
2925
+ // Stop the poll loop as soon as we get a ready response.
2926
+ first((data) => !!data.signallingServer));
2920
2927
  }
2921
2928
  connectToCirrus(wsUrl) {
2922
- this.close({ code: 1000, reason: DisconnectReason.NewConnectionAttempt });
2923
- return createWebSocket(wsUrl).pipe(tapLog('WS CREATED'), tap((ws) => {
2929
+ // Wrapped in `defer` so each subscription (including ws-retry
2930
+ // re-subscriptions from `connectAttempt`) starts a FRESH WS attempt:
2931
+ // the previous socket is closed and a new one is created. Without
2932
+ // this, the WS-only retry would just re-subscribe the SAME WS
2933
+ // observable, which in production-defer'd `createWebSocket` would
2934
+ // still create a new socket, but in tests would re-emit the same
2935
+ // (already-errored) mock observable.
2936
+ return defer(() => {
2937
+ this.close({ code: 1000, reason: DisconnectReason.NewConnectionAttempt });
2938
+ return createWebSocket(wsUrl);
2939
+ }).pipe(takeUntil(this.abort$), tapLog('WS CREATED'), tap((ws) => {
2924
2940
  this.commandTelemetryService.trackStartCommand('EXT-iceCandidate');
2925
2941
  this.addWsHandlers(ws);
2926
- }), takeUntil(merge(this.abort$, this.destroy$)));
2942
+ }));
2927
2943
  }
2928
2944
  addWsHandlers(ws) {
2929
2945
  ws.onmessage = OnMessageHandler.bind(this);
@@ -2942,9 +2958,6 @@ class SignallingService extends SubService {
2942
2958
  */
2943
2959
  close(data) {
2944
2960
  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).
2948
2961
  this.onWebRtcAnswer$.next(null);
2949
2962
  this.orchestrationRetrySub?.unsubscribe();
2950
2963
  if (this.ws) {
@@ -4046,25 +4059,18 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
4046
4059
  }]
4047
4060
  }] });
4048
4061
 
4049
- class UnrealCommunicatorService {
4062
+ class UnrealCommunicatorService extends SubService {
4050
4063
  constructor() {
4051
- this.store = inject(Store);
4052
- this.actions$ = inject(Actions);
4064
+ super();
4053
4065
  this.commandTelemetryService = inject(CommandTelemetryService);
4054
4066
  this.webRtcPlayerService = inject(WebRtcPlayerService);
4055
4067
  this.unrealConfig = inject(UNREAL_CONFIG, { optional: true });
4056
- this.destroyRef = inject(DestroyRef);
4057
- this.destroy$ = new Subject();
4058
4068
  this.cirrusConnected = this.store.selectSignal(unrealFeature.selectCirrusConnected);
4059
4069
  this.afkIgnoredCommands = new Set(['executeConsoleCommand']);
4060
4070
  this.skipAfkReset = false;
4061
4071
  // Throttle: input events fire 60-200x/sec, but AFK timeouts are measured in
4062
4072
  // seconds. Leading-edge throttle dispatches once, then suppresses for 1s.
4063
4073
  this.resetAfk$ = new Subject();
4064
- this.destroyRef.onDestroy(() => {
4065
- this.destroy$.next();
4066
- this.destroy$.complete();
4067
- });
4068
4074
  this.resetAfk$
4069
4075
  .pipe(throttleTime(1000), takeUntil$1(this.destroy$))
4070
4076
  .subscribe(() => this.store.dispatch(resetAfk()));
@@ -4670,20 +4676,21 @@ class RegionsPingService {
4670
4676
  constructor() {
4671
4677
  this.httpClient = inject(HttpClient);
4672
4678
  this.store = inject(Store);
4673
- this.unrealInitialConfig = inject(UNREAL_CONFIG, { optional: true });
4679
+ this.unrealConfig = inject(UNREAL_CONFIG, { optional: true });
4674
4680
  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();
4681
+ // Aborted when the injector is destroyed. Forwarded into every
4682
+ // `startPinging` call's winnerController so in-flight fetches cancel
4683
+ // immediately on route exit instead of running to natural completion.
4684
+ this.destroyController = new AbortController();
4678
4685
  this.config = {
4679
4686
  ping_count: 2,
4680
4687
  ping_timeout: 2000,
4681
4688
  max_parallel_requests: 10,
4682
4689
  bust_cache: true,
4683
4690
  };
4684
- this.destroyRef.onDestroy(() => this.destroyAbort.abort());
4691
+ this.destroyRef.onDestroy(() => this.destroyController.abort());
4685
4692
  }
4686
- getFastest(regionListUrl = this.unrealInitialConfig?.regionsPingUrl || '') {
4693
+ getFastest(regionListUrl = this.unrealConfig?.regionsPingUrl || '') {
4687
4694
  // Self-announce every lifecycle waypoint of the region-discovery
4688
4695
  // flow (start / providers-fetched / resolved). The final
4689
4696
  // `regionResolved` forwards the winner's full ping-sample array
@@ -4700,31 +4707,34 @@ class RegionsPingService {
4700
4707
  const defaultTimeout = providers.timeout ?? this.config.ping_timeout;
4701
4708
  return from(this.startPinging(providers.regions, defaultTimeout));
4702
4709
  }
4703
- getProviders(regionListUrl = this.unrealInitialConfig?.regionsPingUrl || '') {
4710
+ getProviders(regionListUrl = this.unrealConfig?.regionsPingUrl || '') {
4704
4711
  return this.httpClient.get(regionListUrl);
4705
4712
  }
4706
4713
  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
- ]);
4717
4714
  const semaphore = new Semaphore(this.config.max_parallel_requests);
4715
+ // Shared "stop" signal. Aborted when EITHER:
4716
+ // - one region finishes successfully (winner found → cancel losers); OR
4717
+ // - the service's injector is destroyed (route exit → cancel everything).
4718
+ // Without this, losing regions keep pinging to their own `ping_timeout`,
4719
+ // generating up to `(endpoints.length - 1) × ping_count` wasted requests
4720
+ // every time `getFastest` runs, and in-flight fetches outlive the route.
4721
+ const winnerController = new AbortController();
4722
+ const destroySignal = this.destroyController.signal;
4723
+ let onDestroyAbort;
4724
+ if (destroySignal.aborted) {
4725
+ // Service already destroyed — bail every task synchronously.
4726
+ winnerController.abort(destroySignal.reason);
4727
+ }
4728
+ else {
4729
+ onDestroyAbort = () => winnerController.abort(destroySignal.reason);
4730
+ destroySignal.addEventListener('abort', onDestroyAbort, { once: true });
4731
+ }
4718
4732
  const tasks = endpoints.map(async ({ region_code, url, timeout }) => {
4719
4733
  await semaphore.acquire();
4720
4734
  try {
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) {
4735
+ const result = await this.checkLatency(url, timeout ?? defaultTimeout, winnerController.signal);
4736
+ if (result.length < 2)
4726
4737
  throw new Error('insufficient pings');
4727
- }
4728
4738
  return { region_code, result };
4729
4739
  }
4730
4740
  finally {
@@ -4732,18 +4742,31 @@ class RegionsPingService {
4732
4742
  }
4733
4743
  });
4734
4744
  try {
4735
- const winner = await Promise.any(tasks);
4736
- winnerAbort.abort();
4737
- return winner;
4745
+ return await Promise.any(tasks);
4738
4746
  }
4739
4747
  catch {
4740
4748
  return null;
4741
4749
  }
4750
+ finally {
4751
+ // Cancel losing in-flight pings. Safe to call on success (we already
4752
+ // captured the winner) AND on all-failed (no-op, all tasks settled).
4753
+ winnerController.abort();
4754
+ // Detach the destroy listener so it doesn't linger on the long-lived
4755
+ // destroyController.signal across multiple `getFastest` invocations.
4756
+ if (onDestroyAbort) {
4757
+ destroySignal.removeEventListener('abort', onDestroyAbort);
4758
+ }
4759
+ }
4742
4760
  }
4743
- async checkLatency(endpoint, timeout, externalSignal) {
4761
+ async checkLatency(endpoint, timeout, abortSignal) {
4744
4762
  const pingResults = [];
4745
4763
  const sep = endpoint.includes('?') ? '&' : '?';
4746
4764
  for (let i = 0; i < this.config.ping_count; i++) {
4765
+ // Bail out as soon as another region's task has won — no point
4766
+ // burning further `ping_timeout` budget here.
4767
+ if (abortSignal?.aborted) {
4768
+ break;
4769
+ }
4747
4770
  const startTime = performance.now();
4748
4771
  try {
4749
4772
  await this.fetchWithTimeout(this.config.bust_cache
@@ -4757,7 +4780,7 @@ class RegionsPingService {
4757
4780
  Expires: '0',
4758
4781
  },
4759
4782
  credentials: 'omit',
4760
- }, timeout, externalSignal);
4783
+ }, timeout, abortSignal);
4761
4784
  }
4762
4785
  catch {
4763
4786
  // intentional: finally pushes the elapsed time as the latency sample
@@ -4769,20 +4792,31 @@ class RegionsPingService {
4769
4792
  }
4770
4793
  return pingResults;
4771
4794
  }
4772
- async fetchWithTimeout(url, options = {}, timeout, externalSignal) {
4795
+ async fetchWithTimeout(url, options = {}, timeout, abortSignal) {
4773
4796
  const controller = new AbortController();
4774
- options.signal = externalSignal
4775
- ? AbortSignal.any([controller.signal, externalSignal])
4776
- : controller.signal;
4797
+ options.signal = controller.signal;
4798
+ // Forward an external abort into our local controller so a winner-found signal cancels the in-flight `fetch` immediately. If the
4799
+ // external signal is already aborted, abort synchronously before
4800
+ // even starting the fetch.
4801
+ let onExternalAbort;
4802
+ if (abortSignal) {
4803
+ if (abortSignal.aborted) {
4804
+ controller.abort(abortSignal.reason);
4805
+ }
4806
+ else {
4807
+ onExternalAbort = () => controller.abort(abortSignal.reason);
4808
+ abortSignal.addEventListener('abort', onExternalAbort, { once: true });
4809
+ }
4810
+ }
4777
4811
  const timeoutId = setTimeout(() => controller.abort(new DOMException('Request timed out', 'TimeoutError')), timeout);
4778
4812
  try {
4779
- const response = await fetch(url, options);
4780
- clearTimeout(timeoutId);
4781
- return response;
4813
+ return await fetch(url, options);
4782
4814
  }
4783
- catch (error) {
4815
+ finally {
4784
4816
  clearTimeout(timeoutId);
4785
- throw error;
4817
+ if (abortSignal && onExternalAbort) {
4818
+ abortSignal.removeEventListener('abort', onExternalAbort);
4819
+ }
4786
4820
  }
4787
4821
  }
4788
4822
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: RegionsPingService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
@@ -7085,5 +7119,5 @@ function provideUnrealModule() {
7085
7119
  * Generated bundle index. Do not edit.
7086
7120
  */
7087
7121
 
7088
- export { AFKService, AfkTimeoutModalComponent, AggregatorService, AnalyticsService, AnswerHandler, CONSOLE_COMMAND_DISABLE_MESSAGES, CONSOLE_COMMAND_ENABLE_MESSAGES, CONSOLE_COMMAND_PIXEL_QUALITY, CommandTelemetryService, ConfigHandler, ConsoleExtensionsService, ControlScheme, DATA_CHANNEL_CONNECTION_TIMEOUT, DEBOUNCE_TO_MANY_RESIZE_CALLS, DEFAULT_AFK_TIMEOUT, DEFAULT_AFK_TIMEOUT_PERIOD, DEFAULT_RECONNECT_DELAY_MS, DEFAULT_RECONNECT_ENABLED, DEFAULT_RECONNECT_MAX_ATTEMPTS, DEFAULT_RECONNECT_ON_DATACHANNEL_CLOSE, DEFAULT_RECONNECT_ON_ICE_FAILURE, DESTROY_DISCONNECT_REASONS, DataFlowMonitor, DevModeService, DisconnectReason, FULL_HD_HEIGHT, FULL_HD_WIDTH, FileHandlerService, FileReceiverService, FilterSettingsComponent, FpsMonitorService, FreezeFrameComponent, FreezeFrameService, IceCandidateHandler, ImageLoadingSrcComponent, InputOptions, InputService, InstanceReadyHandler, InstanceReservedHandler, IntroSrcComponent, KalmanFilter1D, LatencyTimings, LoadingProgressService, LowBandwidthDetectorComponent, LowBandwidthModalComponent, MINIMAL_FPS, MIN_CONTAINER_SIZE, MessageType, MouseButton, MouseButtonsMask, OnCloseHandler, OnErrorHandler, OnMessageHandler, OnOpenHandler, OrchestrationMessageTypes, POLLING_TIME, PingHandler, PlayerCountHandler, RegionsPingService, SAME_SIZE_THRESHOLD, SCREEN_LOCKER_CONTAINER_ID, SIGNALLING_PERCENT_VALUE, SSInfoHandler, STREAMING_AUDIO_ID, STREAMING_VIDEO_ID, SafePipe, SignallingService, SpecialKeyCodes, StatGraphComponent, 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_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, 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, trackMixpanelEvent, trackTelemetryData, unrealCommandCallback, unrealFeature, unrealReducer, updateCirrusInfo, videoAdaptedToContainer, videoStreamInitialized };
7122
+ export { AFKService, AfkTimeoutModalComponent, AggregatorService, AnalyticsService, AnswerHandler, CONSOLE_COMMAND_DISABLE_MESSAGES, CONSOLE_COMMAND_ENABLE_MESSAGES, CONSOLE_COMMAND_PIXEL_QUALITY, CommandTelemetryService, ConfigHandler, ConsoleExtensionsService, ControlScheme, DATA_CHANNEL_CONNECTION_TIMEOUT, DEBOUNCE_TO_MANY_RESIZE_CALLS, DEFAULT_AFK_TIMEOUT, DEFAULT_AFK_TIMEOUT_PERIOD, DEFAULT_RECONNECT_DELAY_MS, DEFAULT_RECONNECT_ENABLED, DEFAULT_RECONNECT_MAX_ATTEMPTS, DEFAULT_RECONNECT_ON_DATACHANNEL_CLOSE, DEFAULT_RECONNECT_ON_ICE_FAILURE, DESTROY_DISCONNECT_REASONS, DataFlowMonitor, DevModeService, DisconnectReason, FULL_HD_HEIGHT, FULL_HD_WIDTH, FileHandlerService, FileReceiverService, FilterSettingsComponent, FpsMonitorService, FreezeFrameComponent, FreezeFrameService, INNER_RETRY_LIMIT, IceCandidateHandler, ImageLoadingSrcComponent, InputOptions, InputService, InstanceReadyHandler, InstanceReservedHandler, IntroSrcComponent, KalmanFilter1D, LatencyTimings, LoadingProgressService, LowBandwidthDetectorComponent, LowBandwidthModalComponent, MINIMAL_FPS, MIN_CONTAINER_SIZE, MessageType, MouseButton, MouseButtonsMask, OUTER_RETRY_DELAY, OnCloseHandler, OnErrorHandler, OnMessageHandler, OnOpenHandler, OrchestrationMessageTypes, POLLING_TIME, PingHandler, PlayerCountHandler, RegionsPingService, SAME_SIZE_THRESHOLD, SCREEN_LOCKER_CONTAINER_ID, SIGNALLING_PERCENT_VALUE, SSInfoHandler, STREAMING_AUDIO_ID, STREAMING_VIDEO_ID, SafePipe, SignallingService, SpecialKeyCodes, StatGraphComponent, 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, 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, trackMixpanelEvent, trackTelemetryData, unrealCommandCallback, unrealFeature, unrealReducer, updateCirrusInfo, videoAdaptedToContainer, videoStreamInitialized };
7089
7123
  //# sourceMappingURL=3dsource-angular-unreal-module.mjs.map