@3dsource/angular-unreal-module 0.0.117 → 0.0.119

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,7 +1,7 @@
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, from, fromEvent, throwError, defer, retry, tap as tap$1, NEVER, throttleTime, 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, 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 { Actions, createEffect, ofType, provideEffects } from '@ngrx/effects';
7
7
  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';
@@ -313,7 +313,7 @@ const setAfkTimerVisible = createAction(scoped `set afk timer visible`);
313
313
  const setAfkTimerHide = createAction(scoped `set afk timer hide`);
314
314
  const setAfkTimerCountdown = createAction(scoped `set afk timer count down`, props());
315
315
  const showUnrealErrorMessage = createAction(scoped `show unreal error message`, props());
316
- const initSignalling = createAction(scoped `init signalling`, (resetDisconnectionReason = true) => ({ resetDisconnectionReason }));
316
+ const initSignalling = createAction(scoped `init signalling`);
317
317
  const startStream = createAction(scoped `startStream`, props());
318
318
  const resetConfig = createAction(scoped `reset config`);
319
319
  const resetAfk = createAction(scoped `reset afk action`);
@@ -419,14 +419,15 @@ const unrealReducer = createReducer(initialState$1, on(changeStatusMainVideoOnSc
419
419
  freezeFrame.progress === null
420
420
  ? freezeFrame.dataUrl
421
421
  : state.freezeFrame.dataUrl,
422
- progress: freezeFrame.progress || null,
422
+ progress: freezeFrame.progress ?? null,
423
423
  },
424
424
  };
425
425
  }), on(disconnectStream, (state, errorMessage) => {
426
- const alreadyHandled = state.dataChannelConnected ||
427
- state.disconnectReason === DisconnectReason.Destroy;
428
- if (alreadyHandled)
426
+ // Skip user-explicit destroy: the user already initiated cleanup, no
427
+ // disconnect message should overwrite that intent.
428
+ if (state.disconnectReason === DisconnectReason.Destroy) {
429
429
  return state;
430
+ }
430
431
  return { ...state, errorMessage, statusMessage: null };
431
432
  }), on(setFreezeFrameFromVideo, (state, freezeFrameFromVideo) => {
432
433
  return {
@@ -467,13 +468,8 @@ const unrealReducer = createReducer(initialState$1, on(changeStatusMainVideoOnSc
467
468
  warnTimeout: state.userInactivityTimeout,
468
469
  },
469
470
  };
470
- }), on(initSignalling, (state, { resetDisconnectionReason = true }) => {
471
- return {
472
- ...state,
473
- disconnectReason: resetDisconnectionReason
474
- ? DisconnectReason.None
475
- : state.disconnectReason,
476
- };
471
+ }), on(initSignalling, (state) => {
472
+ return { ...state, disconnectReason: DisconnectReason.None };
477
473
  }), on(destroyRemoteConnections, (state, { reason }) => {
478
474
  if (state.disconnectReason === DisconnectReason.Destroy) {
479
475
  return state;
@@ -558,6 +554,10 @@ const unrealReducer = createReducer(initialState$1, on(changeStatusMainVideoOnSc
558
554
  ...initialState$1,
559
555
  establishingConnection: state.establishingConnection,
560
556
  disconnectReason: state.disconnectReason,
557
+ // Preserve the disconnect error so the UI can render the reason
558
+ // after the connection drops. Reset back to null on the next
559
+ // successful initSignalling (initialState default applies again).
560
+ errorMessage: state.errorMessage,
561
561
  isFirstSuccessLoad: state.isFirstSuccessLoad,
562
562
  matchUrls: state.matchUrls,
563
563
  streamClientCompanyId: state.streamClientCompanyId,
@@ -678,10 +678,18 @@ function OnMessageHandler(e) {
678
678
  this.handleMessage(msg);
679
679
  };
680
680
  if (typeof e.data === 'string') {
681
- handleData(e.data);
681
+ try {
682
+ handleData(e.data);
683
+ }
684
+ catch (err) {
685
+ Logger.warn('OnMessageHandler: failed to parse string message', err);
686
+ }
682
687
  }
683
688
  else if (e.data instanceof Blob) {
684
- e.data.text().then(handleData);
689
+ e.data
690
+ .text()
691
+ .then(handleData)
692
+ .catch((err) => Logger.warn('OnMessageHandler: failed to parse Blob message', err));
685
693
  }
686
694
  }
687
695
 
@@ -853,12 +861,15 @@ const getImageFromVideoStream = (takeSizeFrom = 'video', imageOutput, sizes) =>
853
861
  };
854
862
 
855
863
  /**
856
- * Generator for loop through urls
857
- * Each time it will return the next url, based on an index which MUST NOT be re-initialized inside function
858
- * @param urls
864
+ * Generator that cycles through `urls` indefinitely. Each call to
865
+ * `getActiveUrl(urls)` returns a fresh generator with its own counter,
866
+ * so concurrent generators (e.g., overlapping reconnect attempts) do
867
+ * not share state. The counter is initialized once per generator
868
+ * instance — before any `yield` — so each `.next()` call advances
869
+ * within that single generator.
859
870
  */
860
- let index = 0;
861
871
  function* getActiveUrl(urls) {
872
+ let index = 0;
862
873
  while (true) {
863
874
  yield urls[index++ % urls.length];
864
875
  }
@@ -1080,15 +1091,14 @@ function getResizeValuesUniversal(params) {
1080
1091
  return { w: makeEven(w), h: makeEven(h), x: 0, y: 0, scale: w / maxSpace.w };
1081
1092
  }
1082
1093
 
1083
- const removeExileCommands = (state, idToRemove) => {
1094
+ const removeExileCommands = (state, idToRemove, now) => {
1084
1095
  const exileTimeout = 10000;
1085
1096
  const out = { ...state };
1086
1097
  out.commandsInProgress = out.commandsInProgress.filter(whereNot({ id: idToRemove }));
1087
1098
  out.totalCommandsCompleted++;
1088
- const time = new Date().getTime();
1089
1099
  const markForDelete = [];
1090
1100
  for (const { id, timeStamp } of out.commandsInProgress) {
1091
- if (time - timeStamp > exileTimeout) {
1101
+ if (now - timeStamp > exileTimeout) {
1092
1102
  markForDelete.push(id);
1093
1103
  }
1094
1104
  }
@@ -1101,19 +1111,19 @@ const initialState = {
1101
1111
  totalCommandsStarted: 0,
1102
1112
  totalCommandsCompleted: 0,
1103
1113
  };
1104
- const commandsReducer = createReducer(initialState, on(commandStarted, (state, { id, command }) => {
1114
+ const commandsReducer = createReducer(initialState, on(commandStarted, (state, { id, command, timeStamp }) => {
1105
1115
  return {
1106
1116
  ...state,
1107
1117
  totalCommandsStarted: state.totalCommandsStarted + 1,
1108
1118
  commandsInProgress: [
1109
1119
  ...state.commandsInProgress,
1110
- { id, command, timeStamp: new Date().getTime() },
1120
+ { id, command, timeStamp },
1111
1121
  ],
1112
1122
  };
1113
- }), on(commandCompleted, (state, { id }) => {
1123
+ }), on(commandCompleted, (state, { id, now }) => {
1114
1124
  return {
1115
1125
  ...state,
1116
- ...removeExileCommands(state, id),
1126
+ ...removeExileCommands(state, id, now),
1117
1127
  };
1118
1128
  }), on(destroyUnrealScene, setCirrusDisconnected, () => {
1119
1129
  return initialState;
@@ -1242,11 +1252,14 @@ class UnrealEffects {
1242
1252
  onDataChannelClose: this.unrealInitialConfig?.reconnect?.onDataChannelClose ??
1243
1253
  DEFAULT_RECONNECT_ON_DATACHANNEL_CLOSE,
1244
1254
  };
1245
- this.destroyConnections$ = createEffect(() => {
1255
+ this.destroyConnectionsCleanup$ = createEffect(() => {
1246
1256
  return this.actions$.pipe(ofType(destroyUnrealScene), tap(() => {
1247
1257
  this.commandsSender.destroy();
1248
1258
  this.videoService.setContainer(null);
1249
- }), map(() => destroyRemoteConnections({ reason: DisconnectReason.Destroy })));
1259
+ }));
1260
+ }, { dispatch: false });
1261
+ this.destroyConnections$ = createEffect(() => {
1262
+ return this.actions$.pipe(ofType(destroyUnrealScene), map(() => destroyRemoteConnections({ reason: DisconnectReason.Destroy })));
1250
1263
  });
1251
1264
  this.changeStreamResolution$ = createEffect(() => {
1252
1265
  return this.actions$.pipe(ofType(changeStreamResolutionAction), filter((size) => !!(size?.width && size?.height)), concatLatestFrom(() => this.store.select(unrealFeature.selectStreamResolution)), filter(([newSizes, savedSizes]) => !(newSizes.width === savedSizes.width &&
@@ -1380,8 +1393,8 @@ class UnrealEffects {
1380
1393
  }), switchMap(() =>
1381
1394
  // Wait for initial commands to be sent before sending the loopback.
1382
1395
  // commandStarted fires for each sent command; debounce ensures
1383
- // we proceed only after no more commands are started for 100 ms.
1384
- this.actions$.pipe(ofType(commandStarted), debounceTime(100), take(1), switchMap(() => this.unrealCallbackService.observeCommandResponse({ command: MetaBoxCommand.FLoopBackCommand }, (data) => this.commandsSender.sendCommandToUnreal(data))), switchMap(() => this.videoService.videoStats$), filter(({ aggregatedStats: s }) => !!s.framesPerSecond &&
1396
+ // we proceed only after no more commands are started for 50 ms.
1397
+ this.actions$.pipe(ofType(commandStarted), debounceTime(50), take(1), switchMap(() => this.unrealCallbackService.observeCommandResponse({ command: MetaBoxCommand.FLoopBackCommand }, (data) => this.commandsSender.sendCommandToUnreal(data))), switchMap(() => this.videoService.videoStats$), filter(({ aggregatedStats: s }) => !!s.framesPerSecond &&
1385
1398
  s.framesPerSecond > MINIMAL_FPS &&
1386
1399
  (s.framesDecoded ?? 0) > 15), take(1), map(() => setLoopBackCommandIsCompleted()))));
1387
1400
  });
@@ -1391,8 +1404,12 @@ class UnrealEffects {
1391
1404
  this.startStream$ = createEffect(() => {
1392
1405
  return this.actions$.pipe(ofType(startStream), map(() => initSignalling()));
1393
1406
  });
1394
- this.resetAfkOnVideoStart$ = createEffect(() => {
1395
- return this.actions$.pipe(ofType(videoStreamInitialized), filter(() => !!this.videoService.video?.paused), tap(() => Logger.info('Simulate Press Play')), map(() => resetAfk()));
1407
+ // When the video stream is initialized but autoplay didn't kick in
1408
+ // (browser blocked it; user gesture pending), the user may need extra
1409
+ // time before interacting. Refresh the AFK timer so it doesn't tick
1410
+ // toward an inactivity warning while we're waiting for permission.
1411
+ this.resetAfkOnPausedVideoInit$ = createEffect(() => {
1412
+ return this.actions$.pipe(ofType(videoStreamInitialized), filter(() => !!this.videoService.video?.paused), map(() => resetAfk()));
1396
1413
  });
1397
1414
  /**
1398
1415
  * Effect: `handleDataChannelClosed$`
@@ -1557,7 +1574,7 @@ class CommandsEffects {
1557
1574
  this.listenUnrealCallbackByInitSignalling$ = createEffect(() => {
1558
1575
  return this.actions$.pipe(ofType(initSignalling), filter(() => !this.unrealInitialConfig?.playwright), switchMap(() => this.actions$.pipe(ofType(unrealCommandCallback), map(({ json }) => 'commandCallback' in json
1559
1576
  ? json.commandCallback?.correlationId
1560
- : json?.correlationId), filter(Truthy), map((id) => commandCompleted({ id })))));
1577
+ : json?.correlationId), filter(Truthy), map((id) => commandCompleted({ id, now: Date.now() })))));
1561
1578
  });
1562
1579
  }
1563
1580
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: CommandsEffects, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
@@ -1594,7 +1611,6 @@ class AFKService extends SubService {
1594
1611
  this.unrealConfig = inject(UNREAL_CONFIG, { optional: true });
1595
1612
  /** Ticks every time warnTimeout changes or resetAfk action fires */
1596
1613
  this.afkResetTrigger = toSignal(merge(this.store.select(selectWarnTimeout), this.actions$.pipe(ofType(resetAfk))));
1597
- this.disconnected = toSignal(this.disconnect$);
1598
1614
  this.destroyRef.onDestroy(() => this.clearTimers());
1599
1615
  if (this.unrealConfig?.playwright) {
1600
1616
  return;
@@ -1615,12 +1631,12 @@ class AFKService extends SubService {
1615
1631
  untracked(() => this.startAfkWarningTimer());
1616
1632
  }
1617
1633
  });
1618
- effect(() => {
1619
- const disconnected = this.disconnected();
1620
- if (disconnected !== undefined) {
1621
- untracked(() => this.stop());
1622
- }
1623
- });
1634
+ // Stop the AFK timers when Cirrus drops the connection. `disconnect$`
1635
+ // emits void; the previous toSignal(disconnect$) approach never fired
1636
+ // (void → undefined → effect's `!== undefined` guard always false).
1637
+ this.disconnect$
1638
+ .pipe(takeUntil$1(this.destroy$))
1639
+ .subscribe(() => this.stop());
1624
1640
  this.actions$
1625
1641
  .pipe(ofType(resetUserInactivityTimeout), takeUntil$1(this.destroy$))
1626
1642
  .subscribe(() => this.reset());
@@ -1708,9 +1724,24 @@ class LoadingProgressService {
1708
1724
  constructor() {
1709
1725
  this.#store = inject(Store);
1710
1726
  this.loaderVisible = this.#store.selectSignal(selectShowLoader);
1711
- this.#boundedProgress$ = this.#store
1712
- .select(selectTotalProgress)
1713
- .pipe(keepMaxUntilReset(toObservable(this.loaderVisible).pipe(filter(Falsy))));
1727
+ // Visibility-gated progress source.
1728
+ //
1729
+ // When the loader is visible, subscribe to `selectTotalProgress` and run
1730
+ // a `scan` that keeps the running max — progress should only advance
1731
+ // forward within a single loading session, never regress visually.
1732
+ //
1733
+ // When the loader is hidden, switch to a constant `of(0)`. This is what
1734
+ // makes percents drop to 0 on hide / destroy. A previous attempt used
1735
+ // `keepMaxUntilReset` here; that operator could not work because
1736
+ // `selectTotalProgress` is non-completing — it keeps re-emitting the
1737
+ // last (high) value after a reset, which immediately re-fills the max.
1738
+ // `switchMap` cancels the inner subscription on every visibility change
1739
+ // so stale emissions can't leak through.
1740
+ this.#boundedProgress$ = toObservable(this.loaderVisible).pipe(switchMap((visible) => visible
1741
+ ? this.#store
1742
+ .select(selectTotalProgress)
1743
+ .pipe(scan((max, val) => Math.max(max, val), 0))
1744
+ : of(0)), shareReplay({ bufferSize: 1, refCount: true }));
1714
1745
  this.smoothPercents = toSignal(this.#boundedProgress$.pipe(floatToSmoothPercents(), distinctUntilChanged()));
1715
1746
  this.percents = toSignal(this.#boundedProgress$.pipe(floatToPercents(), distinctUntilChanged()));
1716
1747
  this.loadingStatusChanged = computed(() => ({
@@ -1719,6 +1750,19 @@ class LoadingProgressService {
1719
1750
  }), ...(ngDevMode ? [{ debugName: "loadingStatusChanged" }] : /* istanbul ignore next */ []));
1720
1751
  }
1721
1752
  #store;
1753
+ // Visibility-gated progress source.
1754
+ //
1755
+ // When the loader is visible, subscribe to `selectTotalProgress` and run
1756
+ // a `scan` that keeps the running max — progress should only advance
1757
+ // forward within a single loading session, never regress visually.
1758
+ //
1759
+ // When the loader is hidden, switch to a constant `of(0)`. This is what
1760
+ // makes percents drop to 0 on hide / destroy. A previous attempt used
1761
+ // `keepMaxUntilReset` here; that operator could not work because
1762
+ // `selectTotalProgress` is non-completing — it keeps re-emitting the
1763
+ // last (high) value after a reset, which immediately re-fills the max.
1764
+ // `switchMap` cancels the inner subscription on every visibility change
1765
+ // so stale emissions can't leak through.
1722
1766
  #boundedProgress$;
1723
1767
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: LoadingProgressService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
1724
1768
  static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: LoadingProgressService }); }
@@ -1741,12 +1785,8 @@ class FreezeFrameService extends SubService {
1741
1785
  if (this.unrealConfig?.playwright) {
1742
1786
  return;
1743
1787
  }
1744
- this.store
1745
- .select(unrealFeature.selectViewportReady)
1746
- .pipe(filter(Truthy), takeUntil(this.destroy$))
1747
- .subscribe(() => this.invalidate());
1748
- this.actions$
1749
- .pipe(ofType(videoStreamInitialized), takeUntil(this.destroy$))
1788
+ merge(this.store.select(unrealFeature.selectViewportReady).pipe(filter(Truthy)), this.actions$.pipe(ofType(videoStreamInitialized)))
1789
+ .pipe(takeUntil(this.destroy$))
1750
1790
  .subscribe(() => this.invalidate());
1751
1791
  }
1752
1792
  setData(view) {
@@ -2100,7 +2140,9 @@ class VideoService extends SubService {
2100
2140
  video.disablePictureInPicture = true;
2101
2141
  this.video = video;
2102
2142
  this.container?.appendChild(this.video);
2103
- this.video.addEventListener('play', () => this.changeStatusMainVideoOnScene(true));
2143
+ fromEvent(this.video, 'play')
2144
+ .pipe(takeUntil$1(merge(this.disconnect$, this.destroy$)))
2145
+ .subscribe(() => this.changeStatusMainVideoOnScene(true));
2104
2146
  fromEvent(this.video, 'loadedmetadata')
2105
2147
  .pipe(take(1), filter((data) => this.video === data.target), tapLog('VideoService loadedmetadata:'), filter(() => !!this.video))
2106
2148
  .subscribe(() => this.store.dispatch(videoStreamInitialized()));
@@ -2128,6 +2170,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImpo
2128
2170
  class CommandTelemetryService {
2129
2171
  constructor() {
2130
2172
  this.actions$ = inject(Actions);
2173
+ this.destroyRef = inject(DestroyRef);
2174
+ this.destroy$ = new Subject();
2131
2175
  this.appId = 'metabox-telemetry';
2132
2176
  this.commandsSent = {};
2133
2177
  this.exileTimeout = 60000;
@@ -2138,6 +2182,10 @@ class CommandTelemetryService {
2138
2182
  this.unrealConfig = inject(UNREAL_CONFIG, {
2139
2183
  optional: true,
2140
2184
  });
2185
+ this.destroyRef.onDestroy(() => {
2186
+ this.destroy$.next();
2187
+ this.destroy$.complete();
2188
+ });
2141
2189
  this.init();
2142
2190
  }
2143
2191
  init() {
@@ -2150,7 +2198,9 @@ class CommandTelemetryService {
2150
2198
  this.userId = localStorage.getItem(this.appId) || this.uuid;
2151
2199
  sessionStorage.setItem(this.appId, this.sessionId);
2152
2200
  localStorage.setItem(this.appId, this.userId);
2153
- timer(0, this.pollingTime).subscribe(() => {
2201
+ timer(0, this.pollingTime)
2202
+ .pipe(takeUntil(this.destroy$))
2203
+ .subscribe(() => {
2154
2204
  this.send(this.payloads);
2155
2205
  });
2156
2206
  }
@@ -2272,7 +2322,7 @@ class CommandTelemetryService {
2272
2322
  .pipe(ofType(unrealCommandCallback), filter(({ json }) => {
2273
2323
  const callback = 'commandCallback' in json ? json.commandCallback : json;
2274
2324
  return Truthy(callback?.trackingId);
2275
- }))
2325
+ }), takeUntil(this.destroy$))
2276
2326
  .subscribe(({ json }) => {
2277
2327
  const callback = 'commandCallback' in json ? json.commandCallback : json;
2278
2328
  const timeStampsRaw = 'timeStamp' in json ? json?.timeStamp : undefined;
@@ -2312,7 +2362,7 @@ class CommandTelemetryService {
2312
2362
  * Starts tracking the time for the command.
2313
2363
  * Finally, calls the provided function with the modified data object.
2314
2364
  *
2315
- * @param {IToBeDecorated} funcToDecorate - The function to be decorated with telemetry tracking.
2365
+ * @param {ToBeDecorated} funcToDecorate - The function to be decorated with telemetry tracking.
2316
2366
  * @param {MetaBoxCommandPacket} data - The data object to be passed to the function. It will be augmented with a unique tracking ID.
2317
2367
  */
2318
2368
  wrapper(funcToDecorate, data) {
@@ -2548,14 +2598,15 @@ function OrchestrationErrorHandler(msg) {
2548
2598
  const errorInfo = Object.values(ORCHESTRATION_ERROR_CODES).find(({ code }) => code === msg.payload.code);
2549
2599
  this.store.dispatch(setOrchestrationMessage({ message: errorInfo?.description }));
2550
2600
  this.orchestrationRetrySub?.unsubscribe();
2551
- this.orchestrationRetrySub = timer(POLLING_TIME).subscribe(() => this.sendRequestStream());
2601
+ this.orchestrationRetrySub = timer(POLLING_TIME)
2602
+ .pipe(takeUntil(this.destroy$))
2603
+ .subscribe(() => this.sendRequestStream());
2552
2604
  }
2553
2605
  }
2554
2606
 
2555
2607
  class SignallingService extends SubService {
2556
2608
  constructor() {
2557
2609
  super();
2558
- this.action$ = inject(Actions);
2559
2610
  this.httpClient = inject(HttpClient);
2560
2611
  this.regionsPingService = inject(RegionsPingService);
2561
2612
  this.commandTelemetryService = inject(CommandTelemetryService);
@@ -2568,7 +2619,7 @@ class SignallingService extends SubService {
2568
2619
  this.onConfig$ = new Subject();
2569
2620
  this.onWebRtcIce$ = new Subject();
2570
2621
  this.onWebRtcAnswer$ = new Subject();
2571
- this.abort$ = this.action$.pipe(ofType(abortEstablishingConnection), tap(() => this.close({
2622
+ this.abort$ = this.actions$.pipe(ofType(abortEstablishingConnection), tap(() => this.close({
2572
2623
  code: 1000,
2573
2624
  reason: DisconnectReason.AbortEstablishingConnection,
2574
2625
  })));
@@ -2580,8 +2631,9 @@ class SignallingService extends SubService {
2580
2631
  return;
2581
2632
  }
2582
2633
  this.setHandlersFromStream();
2583
- this.action$
2584
- .pipe(ofType(dataChannelReady), takeUntil(this.abort$))
2634
+ const stop$ = merge(this.abort$, this.destroy$);
2635
+ this.actions$
2636
+ .pipe(ofType(dataChannelReady), takeUntil(stop$))
2585
2637
  .subscribe(() => {
2586
2638
  this.send({
2587
2639
  type: 'p2pEstablished',
@@ -2589,10 +2641,10 @@ class SignallingService extends SubService {
2589
2641
  sessionId: generateUuid(),
2590
2642
  });
2591
2643
  });
2592
- this.action$
2644
+ this.actions$
2593
2645
  .pipe(ofType(dataChannelReady), switchMap(() => this.store
2594
2646
  .select(unrealFeature.selectViewportReady)
2595
- .pipe(filter(Truthy), take(1))), takeUntil(this.abort$))
2647
+ .pipe(filter(Truthy), take(1))), takeUntil(stop$))
2596
2648
  .subscribe(() => {
2597
2649
  this.send({
2598
2650
  type: 'streamPreparedForUserInteraction',
@@ -2609,11 +2661,12 @@ class SignallingService extends SubService {
2609
2661
  return EMPTY;
2610
2662
  }
2611
2663
  Logger.log('CONNECT TO SIGNALLING');
2612
- // Preparation for WebSocket Orchestration
2613
- this.correlationId = generateUuid();
2614
- this.commandTelemetryService.reset();
2615
- this.startEstablishingConnection();
2616
2664
  return defer(() => {
2665
+ // Preparation for WebSocket Orchestration. Inside defer so these
2666
+ // side effects only fire once the caller actually subscribes.
2667
+ this.correlationId = generateUuid();
2668
+ this.commandTelemetryService.reset();
2669
+ this.startEstablishingConnection();
2617
2670
  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$));
2618
2671
  }).pipe(
2619
2672
  // Retry the whole attempt on error, advancing to the next URL each time.
@@ -3634,7 +3687,12 @@ class FileReceiverService extends SubService {
3634
3687
  this.reset();
3635
3688
  }
3636
3689
  }
3690
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: FileReceiverService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
3691
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: FileReceiverService }); }
3637
3692
  }
3693
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: FileReceiverService, decorators: [{
3694
+ type: Injectable
3695
+ }], ctorParameters: () => [] });
3638
3696
 
3639
3697
  const filteredLogs = [
3640
3698
  {
@@ -3642,9 +3700,6 @@ const filteredLogs = [
3642
3700
  },
3643
3701
  ];
3644
3702
  class AggregatorService extends SubService {
3645
- /**
3646
- * Never called for now
3647
- */
3648
3703
  constructor() {
3649
3704
  super();
3650
3705
  this.selectStreamConfig = this.store.selectSignal(selectStreamConfig);
@@ -3683,11 +3738,9 @@ class AggregatorService extends SubService {
3683
3738
  .pipe(filter(Truthy));
3684
3739
  merge(unlockViewport$, this.disconnect$)
3685
3740
  .pipe(takeUntil(this.destroy$))
3686
- .subscribe(() => {
3687
- this.removeLoadScreen();
3688
- });
3741
+ .subscribe(() => this.removeLoadScreen());
3689
3742
  if (!this.selectStreamConfig()?.autoStart ||
3690
- location.href.match(/^http:/gi)) {
3743
+ location.protocol === 'http:') {
3691
3744
  this.removeLoadScreen();
3692
3745
  }
3693
3746
  InputOptions.controlScheme = ControlScheme.HoveringMouse;
@@ -3934,14 +3987,27 @@ class UnrealCommunicatorService {
3934
3987
  this.commandTelemetryService = inject(CommandTelemetryService);
3935
3988
  this.webRtcPlayerService = inject(WebRtcPlayerService);
3936
3989
  this.unrealConfig = inject(UNREAL_CONFIG, { optional: true });
3990
+ this.destroyRef = inject(DestroyRef);
3937
3991
  this.destroy$ = new Subject();
3938
3992
  this.cirrusConnected = this.store.selectSignal(unrealFeature.selectCirrusConnected);
3939
3993
  this.afkIgnoredCommands = new Set(['executeConsoleCommand']);
3940
3994
  this.skipAfkReset = false;
3995
+ // Throttle: input events fire 60-200x/sec, but AFK timeouts are measured in
3996
+ // seconds. Leading-edge throttle dispatches once, then suppresses for 1s.
3997
+ this.resetAfk$ = new Subject();
3998
+ this.destroyRef.onDestroy(() => {
3999
+ this.destroy$.next();
4000
+ this.destroy$.complete();
4001
+ });
4002
+ this.resetAfk$
4003
+ .pipe(throttleTime(1000), takeUntil$1(this.destroy$))
4004
+ .subscribe(() => this.store.dispatch(resetAfk()));
3941
4005
  this.init();
3942
4006
  }
3943
4007
  init() {
3944
- this.isPlaywrightMode();
4008
+ if (this.isPlaywrightMode()) {
4009
+ return;
4010
+ }
3945
4011
  this.emitUIInteraction = this.commandTelemetryService.decorate(this.emitUIInteraction.bind(this));
3946
4012
  // Listen to the dataChannelConnected ACTION (point-in-time event) instead
3947
4013
  // of the store selector. The selector reads the current state and can fire on
@@ -3961,7 +4027,9 @@ class UnrealCommunicatorService {
3961
4027
  this.destroy$ = new Subject();
3962
4028
  }
3963
4029
  sendCommandToUnreal(data) {
3964
- this.isPlaywrightMode();
4030
+ if (this.isPlaywrightMode()) {
4031
+ return;
4032
+ }
3965
4033
  if (this.webRtcPlayerService.canSend()) {
3966
4034
  const uniqId = data?.correlationId ?? generateUuid();
3967
4035
  const sendData = { ...data, correlationId: uniqId };
@@ -3971,7 +4039,11 @@ class UnrealCommunicatorService {
3971
4039
  }
3972
4040
  this.emitUIInteraction(sendData);
3973
4041
  this.skipAfkReset = false;
3974
- this.store.dispatch(commandStarted({ id: uniqId, command: data.command }));
4042
+ this.store.dispatch(commandStarted({
4043
+ id: uniqId,
4044
+ command: data.command,
4045
+ timeStamp: Date.now(),
4046
+ }));
3975
4047
  }
3976
4048
  }
3977
4049
  emitUIInteraction(descriptor) {
@@ -3992,10 +4064,12 @@ class UnrealCommunicatorService {
3992
4064
  this.emitDescriptor(MessageType.Command, descriptor);
3993
4065
  }
3994
4066
  sendInputData(data) {
3995
- this.isPlaywrightMode();
4067
+ if (this.isPlaywrightMode()) {
4068
+ return;
4069
+ }
3996
4070
  try {
3997
4071
  if (!this.skipAfkReset) {
3998
- this.store.dispatch(resetAfk());
4072
+ this.resetAfk$.next();
3999
4073
  }
4000
4074
  this.webRtcPlayerService.send(data);
4001
4075
  }
@@ -4006,9 +4080,7 @@ class UnrealCommunicatorService {
4006
4080
  }
4007
4081
  }
4008
4082
  isPlaywrightMode() {
4009
- if (this.unrealConfig?.playwright) {
4010
- return;
4011
- }
4083
+ return !!this.unrealConfig?.playwright;
4012
4084
  }
4013
4085
  emitDescriptor(messageType, descriptor) {
4014
4086
  // Convert the descriptor object into a JSON string.
@@ -4094,7 +4166,7 @@ class InputService extends SubService {
4094
4166
  }
4095
4167
  const visibilityHiddenTrigger$ = fromEvent(document, 'visibilitychange').pipe(map$1(() => document.visibilityState === 'hidden'), filter(Truthy));
4096
4168
  const visibilityVisibleTrigger$ = fromEvent(document, 'visibilitychange').pipe(map$1(() => document.visibilityState === 'visible'), filter(Truthy), startWith(true));
4097
- this.deactivatedVideoTrigger$ = merge(this.disconnect$, this.reInit$, visibilityHiddenTrigger$);
4169
+ this.deactivatedVideoTrigger$ = merge(this.disconnect$, this.reInit$, visibilityHiddenTrigger$, this.destroy$);
4098
4170
  merge(visibilityVisibleTrigger$, this.store.select(unrealFeature.selectViewportReady).pipe(filter(Truthy)))
4099
4171
  .pipe(filter(() => !!this.video), takeUntil$1(this.destroy$))
4100
4172
  .subscribe(() => this.setup());
@@ -4527,7 +4599,12 @@ class InputService extends SubService {
4527
4599
  this.sendInputData(data.buffer);
4528
4600
  });
4529
4601
  }
4602
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: InputService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
4603
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: InputService }); }
4530
4604
  }
4605
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: InputService, decorators: [{
4606
+ type: Injectable
4607
+ }], ctorParameters: () => [] });
4531
4608
 
4532
4609
  class RegionsPingService {
4533
4610
  constructor() {
@@ -4546,56 +4623,50 @@ class RegionsPingService {
4546
4623
  // flow (start / providers-fetched / resolved). The final
4547
4624
  // `regionResolved` forwards the winner's full ping-sample array
4548
4625
  // straight from `PingResult.result` — no aggregation.
4549
- this.store.dispatch(regionPingFetchStart());
4550
- return this.getProviders(regionListUrl).pipe(tap(() => this.store.dispatch(regionPingFetchEnd())), switchMap((providers) => this.getPingResult(providers)), catchError(() => of(null)), tapLog('PingResult'), tap((data) => this.store.dispatch(regionResolved({
4551
- region: data?.region_code ?? '',
4552
- pingsMs: data?.result ?? [],
4553
- }))), map$1((data) => data?.region_code));
4626
+ return defer(() => {
4627
+ this.store.dispatch(regionPingFetchStart());
4628
+ return this.getProviders(regionListUrl).pipe(tap(() => this.store.dispatch(regionPingFetchEnd())), switchMap((providers) => this.getPingResult(providers)), catchError(() => of(null)), tapLog('PingResult'), tap((data) => this.store.dispatch(regionResolved({
4629
+ region: data?.region_code ?? '',
4630
+ pingsMs: data?.result ?? [],
4631
+ }))), map$1((data) => data?.region_code));
4632
+ });
4554
4633
  }
4555
4634
  getPingResult(providers) {
4556
- const regions = providers.regions;
4557
- this.config.ping_timeout = providers.timeout || this.config.ping_timeout;
4558
- return from(this.startPinging(regions));
4559
- }
4560
- getProviders(regionList = this.unrealInitialConfig?.regionsPingUrl || '') {
4561
- return this.httpClient.get(regionList);
4562
- }
4563
- startPinging(endpoints) {
4564
- // eslint-disable-next-line no-async-promise-executor
4565
- return new Promise(async (resolve) => {
4566
- const pingResults = [];
4567
- const semaphore = new Semaphore(10);
4568
- const tasks = endpoints.map(async ({ region_code, url, timeout }) => {
4569
- await semaphore.acquire();
4570
- try {
4571
- const result = await this.checkLatency(url, timeout);
4572
- const data = { region_code, result };
4573
- pingResults.push({ region_code, result });
4574
- resolve(data);
4575
- }
4576
- catch {
4577
- pingResults.push({ region_code: undefined, result: [] });
4578
- }
4579
- finally {
4580
- semaphore.release();
4581
- }
4582
- const okValues = pingResults.filter((pingResult) => pingResult.result.length > 1);
4583
- if (okValues.length) {
4584
- return okValues[0];
4585
- }
4586
- return okValues;
4587
- });
4588
- await Promise.all(tasks);
4589
- resolve(null);
4635
+ const defaultTimeout = providers.timeout ?? this.config.ping_timeout;
4636
+ return from(this.startPinging(providers.regions, defaultTimeout));
4637
+ }
4638
+ getProviders(regionListUrl = this.unrealInitialConfig?.regionsPingUrl || '') {
4639
+ return this.httpClient.get(regionListUrl);
4640
+ }
4641
+ async startPinging(endpoints, defaultTimeout) {
4642
+ const semaphore = new Semaphore(this.config.max_parallel_requests);
4643
+ const tasks = endpoints.map(async ({ region_code, url, timeout }) => {
4644
+ await semaphore.acquire();
4645
+ try {
4646
+ const result = await this.checkLatency(url, timeout ?? defaultTimeout);
4647
+ if (result.length < 2)
4648
+ throw new Error('insufficient pings');
4649
+ return { region_code, result };
4650
+ }
4651
+ finally {
4652
+ semaphore.release();
4653
+ }
4590
4654
  });
4655
+ try {
4656
+ return await Promise.any(tasks);
4657
+ }
4658
+ catch {
4659
+ return null;
4660
+ }
4591
4661
  }
4592
- async checkLatency(endpoint, timeout = this.config.ping_timeout) {
4662
+ async checkLatency(endpoint, timeout) {
4593
4663
  const pingResults = [];
4664
+ const sep = endpoint.includes('?') ? '&' : '?';
4594
4665
  for (let i = 0; i < this.config.ping_count; i++) {
4595
4666
  const startTime = performance.now();
4596
4667
  try {
4597
4668
  await this.fetchWithTimeout(this.config.bust_cache
4598
- ? `${endpoint}?&cache_buster=${performance.now()}`
4669
+ ? `${endpoint}${sep}cache_buster=${performance.now()}`
4599
4670
  : endpoint, {
4600
4671
  method: 'GET',
4601
4672
  mode: 'no-cors',
@@ -4604,11 +4675,11 @@ class RegionsPingService {
4604
4675
  Pragma: 'no-cache',
4605
4676
  Expires: '0',
4606
4677
  },
4607
- credentials: 'omit', // Optional: prevents sending or receiving cookies, which can affect caching.
4678
+ credentials: 'omit',
4608
4679
  }, timeout);
4609
4680
  }
4610
4681
  catch {
4611
- //Do not remove this block, else finally will not work!
4682
+ // intentional: finally pushes the elapsed time as the latency sample
4612
4683
  }
4613
4684
  finally {
4614
4685
  const latency = Math.round(performance.now() - startTime);
@@ -4638,10 +4709,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImpo
4638
4709
  type: Injectable
4639
4710
  }] });
4640
4711
 
4641
- class StreamStatusTelemetryService {
4712
+ class StreamStatusTelemetryService extends SubService {
4642
4713
  constructor() {
4643
- this.store = inject(Store);
4644
- this.actions$ = inject(Actions);
4714
+ super();
4645
4715
  this.videoService = inject(VideoService);
4646
4716
  this.unrealConfig = inject(UNREAL_CONFIG, { optional: true });
4647
4717
  this.init();
@@ -4671,8 +4741,8 @@ class StreamStatusTelemetryService {
4671
4741
  signalingReadyTime,
4672
4742
  viewPortReadyTime,
4673
4743
  })));
4674
- }))
4675
- .subscribe((data) => this.trackEventToMixPanel(data));
4744
+ }), takeUntil$1(this.destroy$))
4745
+ .subscribe((data) => this.store.dispatch(trackMixpanelEvent({ event: 'streamStatus', data })));
4676
4746
  }
4677
4747
  /**
4678
4748
  * Emits `undefined` until `readyState$` first emits a truthy value,
@@ -4703,9 +4773,6 @@ class StreamStatusTelemetryService {
4703
4773
  VideoEncoderQP,
4704
4774
  };
4705
4775
  }
4706
- trackEventToMixPanel(data) {
4707
- this.store.dispatch(trackMixpanelEvent({ event: 'streamStatus', data }));
4708
- }
4709
4776
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: StreamStatusTelemetryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
4710
4777
  static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: StreamStatusTelemetryService }); }
4711
4778
  }
@@ -4727,6 +4794,8 @@ class StreamStatusTelemetryService2 {
4727
4794
  this.videoService = inject(VideoService);
4728
4795
  this.signallingService = inject(SignallingService);
4729
4796
  this.unrealConfig = inject(UNREAL_CONFIG, { optional: true });
4797
+ this.destroyRef = inject(DestroyRef);
4798
+ this.destroy$ = new Subject();
4730
4799
  /**
4731
4800
  * Per-browser anonymous user id. Read from `localStorage` on first access;
4732
4801
  * if missing, a new UUID is generated and persisted. Included as
@@ -4942,6 +5011,13 @@ class StreamStatusTelemetryService2 {
4942
5011
  ],
4943
5012
  },
4944
5013
  ];
5014
+ this.destroyRef.onDestroy(() => {
5015
+ // End any active session BEFORE completing destroy$, so the
5016
+ // session-scoped subscriptions in `this.session.sub` are released.
5017
+ this.endSession('destroy');
5018
+ this.destroy$.next();
5019
+ this.destroy$.complete();
5020
+ });
4945
5021
  if (this.unrealConfig?.playwright ||
4946
5022
  !this.unrealConfig?.streamTelemetryV2) {
4947
5023
  return;
@@ -4966,16 +5042,18 @@ class StreamStatusTelemetryService2 {
4966
5042
  init() {
4967
5043
  // Start a new session on every initSignalling dispatch.
4968
5044
  this.actions$
4969
- .pipe(ofType(initSignalling))
5045
+ .pipe(ofType(initSignalling), takeUntil$1(this.destroy$))
4970
5046
  .subscribe(() => this.startSession());
4971
5047
  // End the current session when Cirrus drops (true → false transition).
4972
5048
  this.store
4973
5049
  .select(unrealFeature.selectCirrusConnected)
4974
- .pipe(pairwise(), filter$1(([prev, curr]) => prev && !curr))
5050
+ .pipe(pairwise(), filter$1(([prev, curr]) => prev && !curr), takeUntil$1(this.destroy$))
4975
5051
  .subscribe(() => this.endSession('disconnected'));
4976
5052
  // Close the session when the tab is hidden / being evicted from cache.
4977
5053
  // `pagehide` fires in more cases than `beforeunload` (covers iOS Safari, bfcache).
4978
- fromEvent(window, 'pagehide').subscribe(() => this.endSession('pagehide'));
5054
+ fromEvent(window, 'pagehide')
5055
+ .pipe(takeUntil$1(this.destroy$))
5056
+ .subscribe(() => this.endSession('pagehide'));
4979
5057
  }
4980
5058
  startSession() {
4981
5059
  // If a previous session is still active, close it first.
@@ -5435,17 +5513,19 @@ class FileHandlerService {
5435
5513
  observeFileResponse(mimetype, data, sender, timeOut = 60000) {
5436
5514
  const correlationId = generateUuid();
5437
5515
  const out = { ...data, correlationId };
5438
- const observable = this.fileService.fileComplete$.pipe(switchMap((data) => {
5439
- if (data.valid && this.fileHandlers[mimetype]) {
5440
- return this.fileHandlers[mimetype](data, correlationId).pipe(filter(Truthy));
5441
- }
5442
- return of(null);
5443
- }), timeout(timeOut), catchError((e) => {
5444
- Logger.error(e);
5445
- return of(null);
5446
- }), take(1));
5447
- setTimeout(() => sender(out), 0);
5448
- return observable;
5516
+ return defer(() => {
5517
+ const observable = this.fileService.fileComplete$.pipe(switchMap((data) => {
5518
+ if (data.valid && this.fileHandlers[mimetype]) {
5519
+ return this.fileHandlers[mimetype](data, correlationId).pipe(filter(Truthy));
5520
+ }
5521
+ return of(null);
5522
+ }), timeout(timeOut), catchError((e) => {
5523
+ Logger.error(e);
5524
+ return of(null);
5525
+ }), take(1));
5526
+ setTimeout(() => sender(out), 0);
5527
+ return observable;
5528
+ });
5449
5529
  }
5450
5530
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: FileHandlerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
5451
5531
  static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: FileHandlerService }); }
@@ -5455,25 +5535,21 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImpo
5455
5535
  }] });
5456
5536
 
5457
5537
  class AnalyticsService extends SubService {
5458
- /** Signals monitorMouse inner subscriptions to stop (viewport lost / new session). */
5459
5538
  #stopMouse$ = new Subject();
5460
- sendEventToMixPanel(name, params = {}) {
5461
- Logger.log('Analytics Event:', name, params);
5462
- this.store.dispatch(saveAnalyticsEvent({ event: { name, params } }));
5463
- }
5464
5539
  constructor() {
5465
5540
  super();
5466
- const abortSignal$ = this.store
5467
- .select(unrealFeature.selectViewportReady)
5468
- .pipe(filter$1(Falsy), map(() => void 0));
5469
5541
  this.store
5470
5542
  .select(unrealFeature.selectViewportReady)
5471
- .pipe(distinctUntilChanged(), filter$1(Truthy), takeUntil$1(this.destroy$))
5472
- .subscribe(() => this.monitorMouse(abortSignal$));
5543
+ .pipe(distinctUntilChanged(), takeUntil$1(this.destroy$))
5544
+ .subscribe((ready) => {
5545
+ this.#stopMouse$.next();
5546
+ if (ready) {
5547
+ this.monitorMouse();
5548
+ }
5549
+ });
5473
5550
  }
5474
- monitorMouse(abortSignal$) {
5475
- this.#stopMouse$.next();
5476
- const stop$ = merge(this.#stopMouse$.asObservable(), abortSignal$);
5551
+ monitorMouse() {
5552
+ const stop$ = this.#stopMouse$.asObservable();
5477
5553
  const element = document.getElementById('streamingVideo');
5478
5554
  if (!element) {
5479
5555
  return;
@@ -5538,6 +5614,10 @@ class AnalyticsService extends SubService {
5538
5614
  });
5539
5615
  });
5540
5616
  }
5617
+ sendEventToMixPanel(name, params = {}) {
5618
+ Logger.log('Analytics Event:', name, params);
5619
+ this.store.dispatch(saveAnalyticsEvent({ event: { name, params } }));
5620
+ }
5541
5621
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: AnalyticsService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
5542
5622
  static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: AnalyticsService }); }
5543
5623
  }
@@ -5618,10 +5698,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImpo
5618
5698
  * the existing `CommandsEffects.setMaxFps$` effect that sends the
5619
5699
  * `t.MaxFPS <value>` console command to Unreal Engine.
5620
5700
  */
5621
- class FpsMonitorService {
5701
+ class FpsMonitorService extends SubService {
5622
5702
  constructor() {
5703
+ super();
5623
5704
  this.videoService = inject(VideoService);
5624
- this.store = inject(Store);
5625
5705
  this.unrealConfig = inject(UNREAL_CONFIG, { optional: true });
5626
5706
  /** Discrete FPS targets the controller can switch between (ascending order). Last 0 is an unlimited framerate indicator*/
5627
5707
  this.FPS_STEPS = [30, 40, 50, 0];
@@ -5660,9 +5740,6 @@ class FpsMonitorService {
5660
5740
  this.samples = [];
5661
5741
  this.lastDecisionTime = Date.now();
5662
5742
  this.lastUpgrade = 'hold';
5663
- this.disconnect$ = this.store
5664
- .select(unrealFeature.selectCirrusConnected)
5665
- .pipe(skip(1), filter(Falsy));
5666
5743
  this.init();
5667
5744
  }
5668
5745
  init() {
@@ -5672,17 +5749,19 @@ class FpsMonitorService {
5672
5749
  // Reset mutable state and re-subscribe when a new session starts
5673
5750
  this.store
5674
5751
  .select(unrealFeature.selectViewportReady)
5675
- .pipe(filter(Boolean))
5752
+ .pipe(filter(Boolean), takeUntil(this.destroy$))
5676
5753
  .subscribe(() => this.startSession());
5677
5754
  // Reset mutable state when session ends
5678
- this.disconnect$.subscribe(() => this.resetSession());
5755
+ this.disconnect$
5756
+ .pipe(takeUntil(this.destroy$))
5757
+ .subscribe(() => this.resetSession());
5679
5758
  }
5680
5759
  startSession() {
5681
5760
  // Tear down previous stats subscription if any
5682
5761
  this.session$.next();
5683
5762
  this.resetMutableState();
5684
5763
  this.videoService.videoStats$
5685
- .pipe(withLatestFrom(this.store.select(unrealFeature.selectViewportReady)), filter(([, viewportReady]) => viewportReady), takeUntil(this.session$))
5764
+ .pipe(withLatestFrom(this.store.select(unrealFeature.selectViewportReady)), filter(([, viewportReady]) => viewportReady), takeUntil(merge(this.session$, this.destroy$)))
5686
5765
  .subscribe(([stats]) => this.processTick(stats));
5687
5766
  }
5688
5767
  resetSession() {
@@ -5968,14 +6047,16 @@ class UnrealCallbackService {
5968
6047
  */
5969
6048
  observeCommandResponse(data, sender, timeOut = 60000, dispatchOnTimeout = true) {
5970
6049
  const correlationId = generateUuid();
5971
- const observable = this.actions$.pipe(ofType(unrealCommandCallback), map$1(({ json }) => 'commandCallback' in json ? json.commandCallback : json), filter((json) => json.correlationId === correlationId), timeout(timeOut), catchError(() => dispatchOnTimeout
5972
- ? of({ command: 'timeout', correlationId: 'timeout' })
5973
- : of(null)), take(1), tapLog('Command Response Observer'), filter(Truthy), map$1((data) => ({
5974
- command: data.command,
5975
- payload: 'payload' in data ? data.payload : null,
5976
- })));
5977
- setTimeout(() => sender({ ...data, correlationId }), 0);
5978
- return observable;
6050
+ return defer(() => {
6051
+ const observable = this.actions$.pipe(ofType(unrealCommandCallback), map$1(({ json }) => 'commandCallback' in json ? json.commandCallback : json), filter((json) => json.correlationId === correlationId), timeout(timeOut), catchError(() => dispatchOnTimeout
6052
+ ? of({ command: 'timeout', correlationId: 'timeout' })
6053
+ : of(null)), take(1), tapLog('Command Response Observer'), filter(Truthy), map$1((data) => ({
6054
+ command: data.command,
6055
+ payload: 'payload' in data ? data.payload : null,
6056
+ })));
6057
+ setTimeout(() => sender({ ...data, correlationId }), 0);
6058
+ return observable;
6059
+ });
5979
6060
  }
5980
6061
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: UnrealCallbackService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
5981
6062
  static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: UnrealCallbackService }); }
@@ -6291,7 +6372,7 @@ class ResumeStreamButtonComponent {
6291
6372
  constructor() {
6292
6373
  this.#store = inject(Store);
6293
6374
  this.streamConfig = this.#store.selectSignal(selectStreamConfig);
6294
- this.isSecondStart = signal(!!(this.streamConfig()?.autoStart && location.href.match(/^https/gi)), ...(ngDevMode ? [{ debugName: "isSecondStart" }] : /* istanbul ignore next */ []));
6375
+ this.isSecondStart = signal(this.streamConfig()?.autoStart && location.protocol === 'https:', ...(ngDevMode ? [{ debugName: "isSecondStart" }] : /* istanbul ignore next */ []));
6295
6376
  this.isLoaderScreenVisible = this.#store.selectSignal(selectIsLoaderScreenVisible);
6296
6377
  this.showReconnectPopup = this.#store.selectSignal(selectShowReconnectPopup);
6297
6378
  this.isExistMatchUrls = this.#store.selectSignal(selectIsExistMatchUrls);
@@ -6651,6 +6732,14 @@ class LowBandwidthDetectorComponent {
6651
6732
  this.videoService = inject(VideoService);
6652
6733
  this.isDevMode = inject(DevModeService).isDevMode;
6653
6734
  this.viewportReady$ = this.#store.select(unrealFeature.selectViewportReady);
6735
+ // TODO: `isLowBandwidth$` is currently stubbed to `of(true)`, which
6736
+ // makes the entire monitoring chain in `ngOnInit` unreachable
6737
+ // (`monitoringTrigger$` always emits `false`, `canSwitchToLowBand$`
6738
+ // never passes its `filter(Truthy)`, and `trySetLowBandwidth` is
6739
+ // never invoked). Only the initial `toggleIndicator(true)` fires
6740
+ // once on mount. Wire this to a real reactive source (e.g. derived
6741
+ // from bitrate/FPS thresholds in `aggregatedStats$`) before
6742
+ // re-enabling the LBM feature, and remove this comment.
6654
6743
  this.isLowBandwidth$ = of(true);
6655
6744
  this.lowBandwidthTrigger$ = this.isLowBandwidth$.pipe(distinctUntilChanged$1(), filter(Truthy));
6656
6745
  this.highBandwidthTrigger$ = this.isLowBandwidth$.pipe(distinctUntilChanged$1(), filter(Falsy));