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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,16 +1,17 @@
1
1
  import * as i0 from '@angular/core';
2
2
  import { InjectionToken, inject, ChangeDetectionStrategy, Component, Injectable, DestroyRef, signal, Pipe, input, ElementRef, viewChild, effect, untracked, afterNextRender, HostListener, computed, output, makeEnvironmentProviders, provideEnvironmentInitializer } from '@angular/core';
3
3
  import { SourceButtonComponent, SourceLoadingComponent } from '@3dsource/source-ui-native';
4
- import { map as map$1, distinctUntilChanged, takeWhile, filter, tap, withLatestFrom, exhaustMap, catchError, takeUntil, switchMap as switchMap$1, timeout as timeout$1, debounceTime as debounceTime$1, delay, skip as skip$1 } from 'rxjs/operators';
4
+ import { map as map$1, distinctUntilChanged, takeWhile, filter, tap, withLatestFrom, exhaustMap, catchError, takeUntil, switchMap as switchMap$1, timeout as timeout$1, debounceTime as debounceTime$1, take as take$1, startWith as startWith$1, delay, skip as skip$1 } from 'rxjs/operators';
5
5
  import { Logger, calculateMedian, clampf, lerp, getCanvasCached, getSnapshot, fitIntoRectangle, whereNot, Truthy, Falsy, tapLog, generateUuid, COLOR_CODES, where, KeyboardNumericCode, InvertedKeyMap, Semaphore, HEXtoRGB, RGBtoHSV, inverseLerp, HSVtoRGB, RGBtoHEX, fpIsASameAsB } from '@3dsource/utils';
6
- import { merge, map, scan, concatMap, of, animationFrameScheduler, interval, Observable, debounceTime, switchMap, take, fromEvent, EMPTY, timer, timeout, mergeMap, Subject, skip, share, from, throwError, defer, retry, tap as tap$1, startWith, takeUntil as takeUntil$1, auditTime, filter as filter$1, throttleTime, BehaviorSubject, combineLatestWith, distinctUntilChanged as distinctUntilChanged$1, concat } from 'rxjs';
6
+ import { merge, map, scan, concatMap, of, animationFrameScheduler, interval, Observable, debounceTime, switchMap, take, fromEvent, EMPTY, timer, timeout, mergeMap, Subject, skip, share, from, throwError, defer, retry, tap as tap$1, startWith, takeUntil as takeUntil$1, throttleTime, asyncScheduler, filter as filter$1, withLatestFrom as withLatestFrom$1, pairwise, Subscription, bufferTime, BehaviorSubject, combineLatestWith, distinctUntilChanged as distinctUntilChanged$1, concat } from 'rxjs';
7
7
  import { createAction, props, createReducer, on, createFeature, createSelector, Store, provideState } from '@ngrx/store';
8
8
  import { Actions, createEffect, ofType, provideEffects } from '@ngrx/effects';
9
9
  import { concatLatestFrom } from '@ngrx/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';
13
- import { MetaBoxCommand } from '@3dsource/types-unreal';
13
+ import { MetaBoxCommand, EPayloadVersion } from '@3dsource/types-unreal';
14
+ import { v4 } from 'uuid';
14
15
  import { DomSanitizer } from '@angular/platform-browser';
15
16
  import { NgOptimizedImage, AsyncPipe, JsonPipe } from '@angular/common';
16
17
  import { toSignal, takeUntilDestroyed, toObservable } from '@angular/core/rxjs-interop';
@@ -150,6 +151,7 @@ const commandCompleted = createAction(scoped$1 `command completed`, props());
150
151
 
151
152
  const scoped = (templateString) => `[UNREAL] ${templateString[0]}`;
152
153
  const trackMixpanelEvent = createAction(scoped `track mixpanel event`, props());
154
+ const trackTelemetryData = createAction(scoped `track telemetry data`, props());
153
155
  const changeLowBandwidth = createAction(scoped `change low bandwidth`, props());
154
156
  const setMaxFps = createAction(scoped `change fps`, props());
155
157
  const destroyRemoteConnections = createAction(scoped `destroyRemoteConnections`, props());
@@ -184,6 +186,47 @@ const changeStreamResolutionAction = createAction(scoped `change stream resoluti
184
186
  const changeStreamResolutionSuccessAction = createAction(scoped `change stream resolution success action`, props());
185
187
  const setSignalingName = createAction(scoped `set aws instanceName`, props());
186
188
  const setOrchestrationParameters = createAction(scoped `set orchestration parameters`, props());
189
+ /**
190
+ * Dispatched when the orchestration server signals that the reserved
191
+ * instance is ready to serve the stream (`instanceReady` WS message).
192
+ * Emitted for observability only — no reducer state change.
193
+ */
194
+ const instanceReady = createAction(scoped `instance ready`);
195
+ /**
196
+ * Dispatched when `RegionsPingService.getFastest()` begins — right
197
+ * before the region-list HTTP request goes out. Bookends the entire
198
+ * region-discovery flow together with {@link regionResolved}.
199
+ * Observability-only — no state change.
200
+ */
201
+ const regionPingFetchStart = createAction(scoped `region ping fetch start`);
202
+ /**
203
+ * Dispatched once `RegionsPingService.getProviders()` returns the region
204
+ * list — i.e. the fetch step is done and the actual per-region ping
205
+ * sweep is about to start. Splits the region-discovery waterfall into
206
+ * two legs: config fetch vs ping sweep. Observability-only.
207
+ */
208
+ const regionPingFetchEnd = createAction(scoped `region ping fetch end`);
209
+ /**
210
+ * Dispatched once the closest region has been resolved by the
211
+ * regions-ping service. Carries every sampled ping RTT to the winning
212
+ * region (one entry per probe — `checkLatency` issues `ping_count`
213
+ * sequential pings per region) so consumers can inspect variance /
214
+ * jitter, not just the mean. Observability-only — no state change.
215
+ */
216
+ const regionResolved = createAction(scoped `region resolved`, props());
217
+ /**
218
+ * Dispatched around the HTTP polling roundtrip inside `getAwsInstance`
219
+ * (old orchestration flow). Only fires when polling is actually used
220
+ * (i.e. signaling URL is not a direct `ws://` / `wss://` endpoint).
221
+ * Observability-only — no state change.
222
+ */
223
+ const pollingStarted = createAction(scoped `polling started`);
224
+ const pollingEnded = createAction(scoped `polling ended`);
225
+ /**
226
+ * Dispatched when the client has just sent an SDP answer back to Cirrus
227
+ * after receiving the server-initiated offer. Observability-only.
228
+ */
229
+ const answerSent = createAction(scoped `answer sent`);
187
230
  const setOrchestrationMessage = createAction(scoped `set orchestration message`, props());
188
231
  const setOrchestrationProgress = createAction(scoped `set orchestration progress`, props());
189
232
  const setOrchestrationContext = createAction(scoped `set orchestration params`, props());
@@ -541,7 +584,9 @@ function IceCandidateHandler(msg) {
541
584
  }
542
585
 
543
586
  function InstanceReadyHandler() {
544
- //Do nothing here
587
+ // No state change — emit the action purely so services that want to
588
+ // observe orchestration progress (e.g. telemetry) have a hook.
589
+ this.store.dispatch(instanceReady());
545
590
  }
546
591
 
547
592
  function InstanceReservedHandler(msg) {
@@ -1169,6 +1214,7 @@ const selectTotalProgress = createSelector(unrealFeature.selectStatusPercentSign
1169
1214
  });
1170
1215
  const selectClientAndViewIds = createSelector(unrealFeature.selectStreamClientCompanyId, unrealFeature.selectStreamViewId, (clientId, viewId) => ({ clientId, viewId }));
1171
1216
  const selectIsLoaderScreenVisible = createSelector(unrealFeature.selectViewportReady, unrealFeature.selectDataChannelConnected, unrealFeature.selectCirrusConnected, (viewReady, cirrusConnected, dataChannelConnected) => !viewReady || !cirrusConnected || !dataChannelConnected);
1217
+ const selectCirrusConnectionId = createSelector(unrealFeature.selectSsData, (data) => data?.connectionId);
1172
1218
 
1173
1219
  class UnrealErrorModalComponent {
1174
1220
  constructor() {
@@ -1179,10 +1225,10 @@ class UnrealErrorModalComponent {
1179
1225
  close() {
1180
1226
  this.#dialogRef.close();
1181
1227
  }
1182
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: UnrealErrorModalComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
1183
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.8", type: UnrealErrorModalComponent, isStandalone: true, selector: "app-unreal-error-modal", ngImport: i0, template: "<div class=\"src-modal src-modal--small\">\r\n <header class=\"src-modal__header\">\r\n <div\r\n [attr.data-testid]=\"'unreal-error-header-title'\"\r\n class=\"src-modal__title\"\r\n >\r\n Warning\r\n </div>\r\n </header>\r\n <div class=\"src-modal__body\">\r\n <div>{{ dialogData.content }}</div>\r\n </div>\r\n <div class=\"src-modal__footer\">\r\n <src-button (onClick)=\"close()\">Ok</src-button>\r\n </div>\r\n</div>\r\n", styles: [".src-modal__header{padding:16px 24px 0}\n"], dependencies: [{ kind: "component", type: SourceButtonComponent, selector: "src-button", inputs: ["type", "appearance", "weight", "size", "context", "customClass", "isFullWidth", "isPressed", "isDisabled", "isLoading", "isInverted", "srcButtonConfig", "formID", "data-testid", "ariaLabel"], outputs: ["onClick", "onSubmit"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
1228
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: UnrealErrorModalComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
1229
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.9", type: UnrealErrorModalComponent, isStandalone: true, selector: "app-unreal-error-modal", ngImport: i0, template: "<div class=\"src-modal src-modal--small\">\r\n <header class=\"src-modal__header\">\r\n <div\r\n [attr.data-testid]=\"'unreal-error-header-title'\"\r\n class=\"src-modal__title\"\r\n >\r\n Warning\r\n </div>\r\n </header>\r\n <div class=\"src-modal__body\">\r\n <div>{{ dialogData.content }}</div>\r\n </div>\r\n <div class=\"src-modal__footer\">\r\n <src-button (onClick)=\"close()\">Ok</src-button>\r\n </div>\r\n</div>\r\n", styles: [".src-modal__header{padding:16px 24px 0}\n"], dependencies: [{ kind: "component", type: SourceButtonComponent, selector: "src-button", inputs: ["type", "appearance", "weight", "size", "context", "customClass", "isFullWidth", "isPressed", "isDisabled", "isLoading", "isInverted", "srcButtonConfig", "formID", "data-testid", "ariaLabel"], outputs: ["onClick", "onSubmit"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
1184
1230
  }
1185
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: UnrealErrorModalComponent, decorators: [{
1231
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: UnrealErrorModalComponent, decorators: [{
1186
1232
  type: Component,
1187
1233
  args: [{ selector: 'app-unreal-error-modal', changeDetection: ChangeDetectionStrategy.OnPush, imports: [SourceButtonComponent], template: "<div class=\"src-modal src-modal--small\">\r\n <header class=\"src-modal__header\">\r\n <div\r\n [attr.data-testid]=\"'unreal-error-header-title'\"\r\n class=\"src-modal__title\"\r\n >\r\n Warning\r\n </div>\r\n </header>\r\n <div class=\"src-modal__body\">\r\n <div>{{ dialogData.content }}</div>\r\n </div>\r\n <div class=\"src-modal__footer\">\r\n <src-button (onClick)=\"close()\">Ok</src-button>\r\n </div>\r\n</div>\r\n", styles: [".src-modal__header{padding:16px 24px 0}\n"] }]
1188
1234
  }] });
@@ -1568,10 +1614,10 @@ class UnrealEffects {
1568
1614
  }));
1569
1615
  }, { dispatch: false });
1570
1616
  }
1571
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: UnrealEffects, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
1572
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: UnrealEffects }); }
1617
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: UnrealEffects, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
1618
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: UnrealEffects }); }
1573
1619
  }
1574
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: UnrealEffects, decorators: [{
1620
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: UnrealEffects, decorators: [{
1575
1621
  type: Injectable
1576
1622
  }] });
1577
1623
 
@@ -1585,10 +1631,10 @@ class CommandsEffects {
1585
1631
  : json?.correlationId), filter(Truthy), map((id) => commandCompleted({ id })))));
1586
1632
  });
1587
1633
  }
1588
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: CommandsEffects, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
1589
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: CommandsEffects }); }
1634
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: CommandsEffects, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
1635
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: CommandsEffects }); }
1590
1636
  }
1591
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: CommandsEffects, decorators: [{
1637
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: CommandsEffects, decorators: [{
1592
1638
  type: Injectable
1593
1639
  }] });
1594
1640
 
@@ -1704,10 +1750,10 @@ class AFKService extends SubService {
1704
1750
  }
1705
1751
  }, 1000);
1706
1752
  }
1707
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: AFKService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
1708
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: AFKService }); }
1753
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AFKService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
1754
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AFKService }); }
1709
1755
  }
1710
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: AFKService, decorators: [{
1756
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AFKService, decorators: [{
1711
1757
  type: Injectable
1712
1758
  }], ctorParameters: () => [] });
1713
1759
 
@@ -1795,10 +1841,10 @@ class FreezeFrameService extends SubService {
1795
1841
  progress: 1,
1796
1842
  }));
1797
1843
  }
1798
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: FreezeFrameService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
1799
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: FreezeFrameService }); }
1844
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: FreezeFrameService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
1845
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: FreezeFrameService }); }
1800
1846
  }
1801
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: FreezeFrameService, decorators: [{
1847
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: FreezeFrameService, decorators: [{
1802
1848
  type: Injectable
1803
1849
  }], ctorParameters: () => [] });
1804
1850
 
@@ -2074,10 +2120,10 @@ class VideoService extends SubService {
2074
2120
  error: () => ({}),
2075
2121
  });
2076
2122
  }
2077
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: VideoService, deps: null, target: i0.ɵɵFactoryTarget.Injectable }); }
2078
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: VideoService }); }
2123
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: VideoService, deps: null, target: i0.ɵɵFactoryTarget.Injectable }); }
2124
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: VideoService }); }
2079
2125
  }
2080
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: VideoService, decorators: [{
2126
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: VideoService, decorators: [{
2081
2127
  type: Injectable
2082
2128
  }] });
2083
2129
 
@@ -2283,10 +2329,10 @@ class CommandTelemetryService {
2283
2329
  this.trackTime(out);
2284
2330
  funcToDecorate(out);
2285
2331
  }
2286
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: CommandTelemetryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
2287
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: CommandTelemetryService }); }
2332
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: CommandTelemetryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
2333
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: CommandTelemetryService }); }
2288
2334
  }
2289
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: CommandTelemetryService, decorators: [{
2335
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: CommandTelemetryService, decorators: [{
2290
2336
  type: Injectable
2291
2337
  }], ctorParameters: () => [] });
2292
2338
 
@@ -2567,7 +2613,9 @@ class SignallingService extends SubService {
2567
2613
  this.correlationId = generateUuid();
2568
2614
  this.commandTelemetryService.reset();
2569
2615
  this.startEstablishingConnection();
2570
- return defer(() => 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$))).pipe(
2616
+ return defer(() => {
2617
+ 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
+ }).pipe(
2571
2619
  // Retry the whole attempt on error, advancing to the next URL each time.
2572
2620
  retry({
2573
2621
  delay: (err, attempt) => {
@@ -2701,6 +2749,9 @@ class SignallingService extends SubService {
2701
2749
  };
2702
2750
  }));
2703
2751
  }
2752
+ // Old (HTTP-polling) orchestration flow — mark the roundtrip so
2753
+ // the signaling-phase timeline can show it as a distinct segment.
2754
+ this.store.dispatch(pollingStarted());
2704
2755
  return this.store.select(selectClientAndViewIds).pipe(
2705
2756
  // Log missing IDs without altering the stream.
2706
2757
  tap(({ clientId, viewId }) => {
@@ -2733,6 +2784,7 @@ class SignallingService extends SubService {
2733
2784
  }),
2734
2785
  // Map a valid orchestration payload into the final AwsInstance.
2735
2786
  map$1((data) => {
2787
+ this.store.dispatch(pollingEnded());
2736
2788
  const wsUrl = httpUrlToWs(`${location.protocol}//${data.signallingServer}`);
2737
2789
  return {
2738
2790
  wsUrl,
@@ -2860,10 +2912,10 @@ class SignallingService extends SubService {
2860
2912
  Logger.log('Start Getting Region');
2861
2913
  return this.regionsPingService.getFastest().pipe(take(1));
2862
2914
  }
2863
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: SignallingService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
2864
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: SignallingService }); }
2915
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: SignallingService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
2916
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: SignallingService }); }
2865
2917
  }
2866
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: SignallingService, decorators: [{
2918
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: SignallingService, decorators: [{
2867
2919
  type: Injectable
2868
2920
  }], ctorParameters: () => [] });
2869
2921
 
@@ -2878,103 +2930,243 @@ class WebRtcPlayerService extends SubService {
2878
2930
  this.useMic = false;
2879
2931
  this.forceTURN = false;
2880
2932
  this.forceMonoAudio = false;
2933
+ /** Whether the remote SDP has been applied to the current peer connection */
2934
+ this.remoteDescriptionSet = false;
2935
+ /** ICE candidates received before the remote description was set */
2936
+ this.iceCandidateBuffer = [];
2881
2937
  this.isReconnecting = this.store.selectSignal(unrealFeature.selectIsReconnecting);
2882
2938
  this.init();
2883
2939
  this.actions$.pipe(ofType(dropDC)).subscribe(() => this.dcClient?.close());
2884
2940
  this.actions$.pipe(ofType(dropPC)).subscribe(() => this.pcClient?.close());
2885
2941
  }
2886
2942
  init() {
2887
- const iceCandidateBuffer = [];
2888
- const processBuffer = () => {
2889
- iceCandidateBuffer.forEach((cnd) => {
2890
- this.handleCandidateFromServer(cnd);
2891
- });
2892
- iceCandidateBuffer.length = 0;
2893
- };
2894
2943
  this.signallingSrv.onConfig$.subscribe((config) => {
2895
2944
  this.videoService.create();
2896
2945
  this.setConfig(config);
2897
2946
  });
2947
+ // Server-initiated offer: set remote description and respond with answer
2948
+ this.signallingSrv.onOffer$.pipe(filter(Truthy)).subscribe((offer) => {
2949
+ void this.handleServerOffer(offer);
2950
+ });
2951
+ // Answer to a client-initiated offer
2898
2952
  this.signallingSrv.onWebRtcAnswer$
2899
2953
  .pipe(filter(Truthy))
2900
2954
  .subscribe((answer) => {
2901
- this.receiveAnswer(answer);
2902
- processBuffer();
2903
- });
2904
- this.signallingSrv.onOffer$.pipe(filter(Truthy)).subscribe(() => {
2905
- void this.createWebRtcOffer();
2955
+ void this.handleServerAnswer(answer);
2906
2956
  });
2907
- this.signallingSrv.onWebRtcIce$
2908
- .pipe(withLatestFrom(this.signallingSrv.onWebRtcAnswer$))
2909
- .subscribe(([candidate, answerEstablished]) => {
2910
- this.commandTelemetryService.trackStopCommand('EXT-iceCandidate', true, { multi: true });
2911
- iceCandidateBuffer.push(candidate);
2912
- if (answerEstablished) {
2913
- processBuffer();
2957
+ // ICE candidates — buffered until remote description is set
2958
+ this.signallingSrv.onWebRtcIce$.subscribe((candidate) => {
2959
+ this.commandTelemetryService.trackStopCommand('EXT-iceCandidate', true, {
2960
+ multi: true,
2961
+ });
2962
+ if (this.remoteDescriptionSet) {
2963
+ this.handleCandidateFromServer(candidate);
2964
+ }
2965
+ else {
2966
+ this.iceCandidateBuffer.push(candidate);
2914
2967
  }
2915
2968
  });
2916
2969
  }
2970
+ /**
2971
+ * Handles an SDP offer received from the server.
2972
+ * Creates a new peer connection, applies the remote offer,
2973
+ * then responds with an SDP answer.
2974
+ *
2975
+ * The DataChannel is NOT created here — the server creates it,
2976
+ * and we receive it via the `datachannel` event on the peer connection.
2977
+ */
2978
+ async handleServerOffer(offer) {
2979
+ Logger.info('Received offer from server, creating answer');
2980
+ try {
2981
+ this.remoteDescriptionSet = false;
2982
+ this.iceCandidateBuffer.length = 0;
2983
+ if (!this.pcClient) {
2984
+ this.setupPeerConnection(this.cfg);
2985
+ }
2986
+ // Offer already defines media transceivers — don't add extras
2987
+ await this.pcClient?.setRemoteDescription(offer);
2988
+ this.remoteDescriptionSet = true;
2989
+ this.flushIceCandidateBuffer();
2990
+ await this.createAnswer(this.pcClient);
2991
+ }
2992
+ catch (e) {
2993
+ Logger.error('Failed to handle server offer', e);
2994
+ this.store.dispatch(disconnectStream({
2995
+ reason: DisconnectReason.WebRTCError,
2996
+ message: 'WebRTCError: Failed to handle server offer',
2997
+ }));
2998
+ }
2999
+ }
3000
+ /**
3001
+ * Handles an SDP answer received from the server in response to our offer.
3002
+ * Applies the remote description and flushes any buffered ICE candidates.
3003
+ */
3004
+ async handleServerAnswer(answer) {
3005
+ Logger.info('Received answer from server');
3006
+ try {
3007
+ await this.pcClient?.setRemoteDescription(answer);
3008
+ this.remoteDescriptionSet = true;
3009
+ this.flushIceCandidateBuffer();
3010
+ }
3011
+ catch (e) {
3012
+ Logger.error('Failed to set remote description from answer', e);
3013
+ }
3014
+ }
3015
+ /** Drains buffered ICE candidates after the remote description has been set */
3016
+ flushIceCandidateBuffer() {
3017
+ for (const candidate of this.iceCandidateBuffer) {
3018
+ this.handleCandidateFromServer(candidate);
3019
+ }
3020
+ this.iceCandidateBuffer.length = 0;
3021
+ }
3022
+ /**
3023
+ * Creates a client-initiated WebRTC offer and sends it to the server.
3024
+ * When the client offers, it also creates the DataChannel.
3025
+ * Used for peer reconnection through the existing signaling connection.
3026
+ */
2917
3027
  async createWebRtcOffer() {
2918
- Logger.info('Creating offer');
2919
- let offer;
3028
+ Logger.info('Creating client-initiated offer');
2920
3029
  try {
2921
- offer = await this.createPeerConnection(this.cfg);
3030
+ this.remoteDescriptionSet = false;
3031
+ this.iceCandidateBuffer.length = 0;
3032
+ if (!this.pcClient) {
3033
+ this.setupPeerConnection(this.cfg);
3034
+ }
3035
+ // Client offers: transceivers first, then DC, then offer
3036
+ await this.setupTransceiversAsync(this.pcClient);
3037
+ this.dcClient = this.createDataChannel(this.pcClient, 'UnrealDcConnection', { ordered: true });
3038
+ await this.createOffer(this.pcClient);
2922
3039
  }
2923
- catch {
3040
+ catch (e) {
3041
+ Logger.error('Failed to create WebRTC offer', e);
2924
3042
  this.store.dispatch(disconnectStream({
2925
3043
  reason: DisconnectReason.WebRTCError,
2926
3044
  message: 'WebRTCError: Offer Error',
2927
3045
  }));
2928
3046
  }
2929
- return offer;
3047
+ }
3048
+ /**
3049
+ * Initializes a new RTCPeerConnection with event handlers.
3050
+ * Transceivers are NOT added here — the caller is responsible for adding
3051
+ * them at the correct point depending on the negotiation flow.
3052
+ */
3053
+ setupPeerConnection(config) {
3054
+ this.closePC();
3055
+ Logger.log('Creating new RTCPeerConnection with config');
3056
+ this.pcClient = new RTCPeerConnection(config);
3057
+ this.commandTelemetryService.trackStartCommand('EXT-Browser ICE gathering');
3058
+ this.commandTelemetryService.trackStartCommand('EXT-Browser ICE connection');
3059
+ this.commandTelemetryService.trackStartCommand('EXT-Signaling state change');
3060
+ this.handlePeerConnectionEvents(this.pcClient);
3061
+ this.disconnect$.pipe(take(1)).subscribe(() => {
3062
+ this.closePC();
3063
+ });
2930
3064
  }
2931
3065
  createDataChannel(pc, label, options) {
2932
- // This is the primary data channel code path when we are "offering"
2933
3066
  const datachannel = pc.createDataChannel(label, options);
2934
3067
  return this.setupDataChannelCallbacks(datachannel);
2935
3068
  }
2936
- receiveAnswer(answer) {
2937
- this.pcClient?.setRemoteDescription(answer);
2938
- }
2939
3069
  handleCandidateFromServer(iceCandidate) {
2940
3070
  try {
2941
3071
  const candidate = new RTCIceCandidate(iceCandidate);
2942
3072
  Logger.info('[Unreal ICE candidate]', '| Type=', candidate.type, '| Protocol=', candidate.protocol, '| Address=', candidate.address, '| Port=', candidate.port, '|');
2943
- // if forcing TURN, reject any candidates not relay
3073
+ // If forcing TURN, reject any candidates not relay
2944
3074
  if (this.forceTURN) {
2945
- // check if no relay address is found, if so, we are assuming it means no TURN server
2946
3075
  if (candidate.candidate.indexOf('relay') < 0) {
2947
3076
  Logger.warn('Dropping candidate because it was not TURN relay.', '| Type=', candidate.type, '| Protocol=', candidate.protocol, '| Address=', candidate.address, '| Port=', candidate.port, '|');
2948
3077
  return;
2949
3078
  }
2950
3079
  }
2951
- this.pcClient?.addIceCandidate(candidate).catch(function () {
2952
- // Logger.error('Failed to add ICE candidate', e);
3080
+ this.pcClient?.addIceCandidate(candidate).catch((e) => {
3081
+ Logger.warn('Failed to add ICE candidate', e);
2953
3082
  });
2954
3083
  }
2955
3084
  catch (e) {
2956
3085
  Logger.error('Error handleCandidateFromServer', e);
2957
3086
  }
2958
3087
  }
2959
- mungeSDP(offer) {
2960
- let audioSDP = '';
2961
- // set max bitrate to the highest bitrate Opus supports
2962
- audioSDP += 'maxaveragebitrate=510000;';
3088
+ /**
3089
+ * Reorders the `m=video` line in the SDP so that payload types matching
3090
+ * the given H.264 `profile-level-id` appear first, making the browser
3091
+ * prefer that profile during codec negotiation.
3092
+ *
3093
+ * A `profile-level-id` is a 3-byte hex string (`XXYYZZ`) defined in
3094
+ * RFC 6184 that encodes three H.264 parameters:
3095
+ * - **Profile IDC** (`XX`) — the H.264 profile.
3096
+ * - **Profile IOP** (`YY`) — constraint flags that further narrow the profile.
3097
+ * - **Level IDC** (`ZZ`) — the level, which caps resolution/framerate/bitrate.
3098
+ *
3099
+ * ### H.264 profiles relevant to Unreal Engine Pixel Streaming
3100
+ *
3101
+ * | profile-level-id | Profile | Level | Notes |
3102
+ * |------------------|----------------------------|-------|----------------------------------------------|
3103
+ * | `42001f` | Baseline | 3.1 | No B-frames, low latency; legacy devices |
3104
+ * | `42e01f` | Constrained Baseline | 3.1 | Widest HW decode support; **recommended** |
3105
+ * | `4d001f` | Main | 3.1 | B-frames + CABAC; better compression |
3106
+ * | `4d4028` | Main | 4.0 | Up to 1080p60; common desktop target |
3107
+ * | `640028` | High | 4.0 | Best quality/compression; most GPUs support |
3108
+ * | `64002a` | High | 4.2 | Up to 1080p120 or 4K30 |
3109
+ * | `640033` | High | 5.1 | Up to 4K60; requires modern HW |
3110
+ * | `640034` | High | 5.2 | Up to 4K120; high-end HW only |
3111
+ * | `6e001f` | High 10 | 3.1 | 10-bit color; limited browser support |
3112
+ * | `f4001f` | High 4:4:4 Predictive | 3.1 | Lossless capable; rare in browsers |
3113
+ *
3114
+ * UE Pixel Streaming defaults to Constrained Baseline (`42e01f`) or High (`6400xx`)
3115
+ * depending on engine version and `WebRTC.H264Profile` CVar.
3116
+ *
3117
+ * We prioritize `42e01f` because it guarantees hardware decoding on
3118
+ * virtually all client devices (desktop, mobile, embedded), avoiding
3119
+ * fallback to software decoding or negotiation of a higher profile
3120
+ * that the client GPU may not accelerate.
3121
+ */
3122
+ prioritizeH264Profile(sdp, profileLevelId) {
3123
+ const lines = sdp.split('\r\n');
3124
+ const targetPts = [];
3125
+ for (const line of lines) {
3126
+ if (line.startsWith('a=fmtp:') &&
3127
+ line.toLowerCase().includes(`profile-level-id=${profileLevelId}`)) {
3128
+ const pt = line.split(':')[1]?.split(' ')[0];
3129
+ if (pt)
3130
+ targetPts.push(pt);
3131
+ }
3132
+ }
3133
+ if (targetPts.length === 0)
3134
+ return sdp;
3135
+ return lines
3136
+ .map((line) => {
3137
+ if (!line.startsWith('m=video'))
3138
+ return line;
3139
+ const parts = line.split(' ');
3140
+ // parts: m=video <port> <proto> <pt1> <pt2> ...
3141
+ const header = parts.slice(0, 3);
3142
+ const pts = parts.slice(3);
3143
+ const prioritized = [
3144
+ ...targetPts.filter((pt) => pts.includes(pt)),
3145
+ ...pts.filter((pt) => !targetPts.includes(pt)),
3146
+ ];
3147
+ return [...header, ...prioritized].join(' ');
3148
+ })
3149
+ .join('\r\n');
3150
+ }
3151
+ mungeSDP(sdp) {
3152
+ let mungedSDP = this.prioritizeH264Profile(sdp, '42e01f');
3153
+ mungedSDP = mungedSDP.replace(/(a=fmtp:\d+ .*level-asymmetry-allowed=.*)\r\n/gm, '$1;x-google-start-bitrate=10000;x-google-max-bitrate=100000\r\n');
3154
+ // Set max bitrate to highest bitrate Opus supports
3155
+ let audioSDP = 'maxaveragebitrate=510000;';
2963
3156
  if (this.useMic) {
2964
- // set the max capture rate to 48khz (so we can send high-quality audio from mic)
3157
+ // Set the max capture rate to 48khz (so we can send high-quality audio from mic)
2965
3158
  audioSDP += 'sprop-maxcapturerate=48000;';
2966
3159
  }
2967
3160
  // Force mono or stereo based on whether ?forceMono was passed or not
2968
- audioSDP += this.forceMonoAudio
2969
- ? 'sprop-stereo=0;stereo=0;'
2970
- : 'sprop-stereo=1;stereo=1;';
2971
- // enable in-band forward error correction for opus audio
3161
+ audioSDP += this.forceMonoAudio ? 'stereo=0;' : 'stereo=1;';
3162
+ // Enable in-band forward error correction for opus audio
2972
3163
  audioSDP += 'useinbandfec=1';
2973
3164
  // We use the line 'useinbandfec=1' (which Opus uses) to set our Opus specific audio parameters.
2974
- offer.sdp = offer.sdp?.replace('useinbandfec=1', audioSDP);
3165
+ mungedSDP = mungedSDP?.replace('useinbandfec=1', audioSDP);
3166
+ return mungedSDP;
2975
3167
  }
2976
3168
  /**
2977
- * Closes existing PeerConnection
3169
+ * Closes existing PeerConnection and DataChannel, resets negotiation state.
2978
3170
  */
2979
3171
  closePC() {
2980
3172
  if (this.pcClient) {
@@ -2987,6 +3179,8 @@ class WebRtcPlayerService extends SubService {
2987
3179
  }
2988
3180
  this.dcClient?.close();
2989
3181
  this.dcClient = null;
3182
+ this.remoteDescriptionSet = false;
3183
+ this.iceCandidateBuffer.length = 0;
2990
3184
  }
2991
3185
  canSend() {
2992
3186
  return this.dcClient && this.dcClient.readyState === 'open';
@@ -3023,9 +3217,9 @@ class WebRtcPlayerService extends SubService {
3023
3217
  }
3024
3218
  }
3025
3219
  /**
3026
- * Reconnect only the WebRTC peer connection without re-establishing signaling.
3027
- * This closes the current PeerConnection and creates a new offer through
3028
- * the existing WebSocket connection.
3220
+ * Reconnects only the WebRTC peer connection without re-establishing signaling.
3221
+ * Closes the current PeerConnection and creates a new client-initiated offer
3222
+ * through the existing WebSocket connection.
3029
3223
  * @returns true if reconnection was initiated, false if signaling is not connected
3030
3224
  */
3031
3225
  reconnectPeer() {
@@ -3058,11 +3252,7 @@ class WebRtcPlayerService extends SubService {
3058
3252
  this.cfg.bundlePolicy = urlParams.has('ForceMaxBundle')
3059
3253
  ? 'max-bundle'
3060
3254
  : 'balanced';
3061
- //**********************
3062
- //Variables
3063
- //**********************
3064
- this.pcClient = null;
3065
- this.dcClient = null;
3255
+ this.cfg.iceCandidatePoolSize = 1;
3066
3256
  // When ?useMic check for SSL or localhost
3067
3257
  const isLocalhostConnection = location.hostname === 'localhost' || location.hostname === '127.0.0.1';
3068
3258
  const isHttpsConnection = location.protocol === 'https:';
@@ -3071,23 +3261,7 @@ class WebRtcPlayerService extends SubService {
3071
3261
  Logger.error('Microphone access in the browser will not work if you are not on HTTPS or localhost. Disabling mic access.');
3072
3262
  Logger.error("For testing you can enable HTTP microphone access Chrome by visiting chrome://flags/ and enabling 'unsafely-treat-insecure-origin-as-secure'");
3073
3263
  }
3074
- }
3075
- async createPeerConnection(config) {
3076
- this.closePC();
3077
- this.pcClient = new RTCPeerConnection(config);
3078
- this.commandTelemetryService.trackStartCommand('EXT-Browser ICE gathering');
3079
- this.commandTelemetryService.trackStartCommand('EXT-Browser ICE connection');
3080
- this.commandTelemetryService.trackStartCommand('EXT-Signaling state change');
3081
- this.handlePeerConnectionEvents(this.pcClient);
3082
- await this.setupTransceiversAsync(this.pcClient);
3083
- this.dcClient = this.createDataChannel(this.pcClient, 'UnrealDcConnection', {
3084
- // See https://www.w3.org/TR/webrtc/#dom-rtcdatachannelinit for values (this is needed for Firefox to be consistent with Chrome.)
3085
- ordered: true,
3086
- });
3087
- this.disconnect$.pipe(take(1)).subscribe(() => {
3088
- this.closePC();
3089
- });
3090
- await this.createOffer(this.pcClient);
3264
+ this.setupPeerConnection(this.cfg);
3091
3265
  }
3092
3266
  handlePeerConnectionEvents(pcClient) {
3093
3267
  fromEvent(pcClient, 'signalingstatechange')
@@ -3116,8 +3290,10 @@ class WebRtcPlayerService extends SubService {
3116
3290
  fromEvent(pcClient, 'icecandidate')
3117
3291
  .pipe(map$1((e) => e.candidate), filter(Truthy), tap$1((candidate) => Logger.info('[Browser ICE candidate]', '| Type=', candidate.type, '| Protocol=', candidate.protocol, '| Address=', candidate.address, '| Port=', candidate.port, '|')), takeUntil(this.disconnect$))
3118
3292
  .subscribe((candidate) => this.signallingSrv.send({ type: 'iceCandidate', candidate }));
3293
+ // When the server creates the DataChannel (server-offer flow),
3294
+ // we receive it here via the 'datachannel' event
3119
3295
  fromEvent(pcClient, 'datachannel')
3120
- .pipe(tap$1(() => Logger.info('Data channel created for us by browser as we are a receiving peer.')), map$1((e) => e.channel), takeUntil(this.disconnect$))
3296
+ .pipe(tap$1((e) => Logger.info(`[DATACHANNEL] Received remote DC: label="${e.channel.label}" id=${e.channel.id} protocol="${e.channel.protocol}" ordered=${e.channel.ordered}`)), map$1((e) => e.channel), takeUntil(this.disconnect$))
3121
3297
  .subscribe((datachannel) => {
3122
3298
  this.dcClient = this.setupDataChannelCallbacks(datachannel);
3123
3299
  });
@@ -3225,7 +3401,14 @@ class WebRtcPlayerService extends SubService {
3225
3401
  this.onDataChannelConnected();
3226
3402
  });
3227
3403
  fromEvent(datachannel, 'message')
3228
- .pipe(map$1((e) => e.data), takeUntil(this.disconnect$))
3404
+ .pipe(
3405
+ /* tap((e: MessageEvent) => {
3406
+ const view = new Uint8Array(e.data);
3407
+ Logger.info(
3408
+ `[DATACHANNEL] Message received on "${datachannel.label}" id=${datachannel.id}, byteCode=${view[0]}, size=${view.byteLength}`,
3409
+ );
3410
+ }),*/
3411
+ map$1((e) => e.data), takeUntil(this.disconnect$))
3229
3412
  .subscribe((data) => this.onDataChannelMessage$.next(data));
3230
3413
  return datachannel;
3231
3414
  }
@@ -3242,34 +3425,55 @@ class WebRtcPlayerService extends SubService {
3242
3425
  }
3243
3426
  this.store.dispatch(dataChannelConnected({ statusMessage }));
3244
3427
  }
3428
+ /**
3429
+ * Creates an SDP offer, applies SDP munging, sets it as a local description,
3430
+ * and sends it to the server via signaling.
3431
+ * Transceivers already configure media directions, so deprecated
3432
+ * offerToReceive* options are not used.
3433
+ */
3245
3434
  async createOffer(pcClient) {
3246
3435
  try {
3247
- const offer = await pcClient.createOffer({
3248
- offerToReceiveAudio: false, //Note: if you don't need audio, you can get improved latency by turning this off.
3249
- offerToReceiveVideo: true,
3250
- });
3251
- this.mungeSDP(offer);
3252
- pcClient?.setLocalDescription(offer);
3436
+ const offer = await pcClient.createOffer();
3437
+ offer.sdp = this.mungeSDP(offer.sdp ?? '');
3438
+ await pcClient.setLocalDescription(offer);
3253
3439
  this.signallingSrv.send(offer);
3254
3440
  }
3255
3441
  catch {
3256
3442
  Logger.error("Couldn't create offer");
3257
3443
  }
3258
3444
  }
3259
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: WebRtcPlayerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
3260
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: WebRtcPlayerService }); }
3261
- }
3262
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: WebRtcPlayerService, decorators: [{
3263
- type: Injectable
3264
- }], ctorParameters: () => [] });
3265
-
3266
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
3267
- const deepEqual = (a, b) => {
3268
- if (a === b)
3269
- return true;
3270
- // must both be objects (and not null)?
3271
- if (typeof a === 'object' &&
3272
- a !== null &&
3445
+ /**
3446
+ * Creates an SDP answer, applies SDP munging, sets it as a local description,
3447
+ * and sends it to the server via signaling.
3448
+ */
3449
+ async createAnswer(pcClient) {
3450
+ try {
3451
+ const answer = await pcClient.createAnswer();
3452
+ answer.sdp = this.mungeSDP(answer.sdp ?? '');
3453
+ await pcClient.setLocalDescription(answer);
3454
+ this.signallingSrv.send(answer);
3455
+ // Observability signal for the signaling waterfall — the handshake
3456
+ // went "answer sent" between `offer received` and `datachannel_ready`.
3457
+ this.store.dispatch(answerSent());
3458
+ }
3459
+ catch (e) {
3460
+ Logger.error("Couldn't create answer", e);
3461
+ }
3462
+ }
3463
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: WebRtcPlayerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
3464
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: WebRtcPlayerService }); }
3465
+ }
3466
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: WebRtcPlayerService, decorators: [{
3467
+ type: Injectable
3468
+ }], ctorParameters: () => [] });
3469
+
3470
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
3471
+ const deepEqual = (a, b) => {
3472
+ if (a === b)
3473
+ return true;
3474
+ // must both be objects (and not null)?
3475
+ if (typeof a === 'object' &&
3476
+ a !== null &&
3273
3477
  typeof b === 'object' &&
3274
3478
  b !== null) {
3275
3479
  // arrays?
@@ -3603,10 +3807,10 @@ class AggregatorService extends SubService {
3603
3807
  Logger.warn(`unrecognized data received, packet ID ${view[0]}`);
3604
3808
  }
3605
3809
  }
3606
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: AggregatorService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
3607
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: AggregatorService }); }
3810
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AggregatorService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
3811
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AggregatorService }); }
3608
3812
  }
3609
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: AggregatorService, decorators: [{
3813
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AggregatorService, decorators: [{
3610
3814
  type: Injectable
3611
3815
  }], ctorParameters: () => [] });
3612
3816
 
@@ -3694,10 +3898,10 @@ class ConsoleExtensionsService extends SubService {
3694
3898
  Logger.info('setWarnTime() => set time to appear the AFK window.');
3695
3899
  Logger.info('emitCommand(command) => send command to Unreal Engine.');
3696
3900
  }
3697
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: ConsoleExtensionsService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
3698
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: ConsoleExtensionsService }); }
3901
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: ConsoleExtensionsService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
3902
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: ConsoleExtensionsService }); }
3699
3903
  }
3700
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: ConsoleExtensionsService, decorators: [{
3904
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: ConsoleExtensionsService, decorators: [{
3701
3905
  type: Injectable
3702
3906
  }], ctorParameters: () => [] });
3703
3907
 
@@ -3713,10 +3917,10 @@ class DevModeService {
3713
3917
  localStorage.removeItem('devMode');
3714
3918
  }
3715
3919
  }
3716
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: DevModeService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
3717
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: DevModeService, providedIn: 'root' }); }
3920
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: DevModeService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
3921
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: DevModeService, providedIn: 'root' }); }
3718
3922
  }
3719
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: DevModeService, decorators: [{
3923
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: DevModeService, decorators: [{
3720
3924
  type: Injectable,
3721
3925
  args: [{
3722
3926
  providedIn: 'root',
@@ -3819,10 +4023,10 @@ class UnrealCommunicatorService {
3819
4023
  requestQualityControl() {
3820
4024
  this.sendInputData(new Uint8Array([MessageType.RequestQualityControl]).buffer);
3821
4025
  }
3822
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: UnrealCommunicatorService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
3823
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: UnrealCommunicatorService }); }
4026
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: UnrealCommunicatorService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
4027
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: UnrealCommunicatorService }); }
3824
4028
  }
3825
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: UnrealCommunicatorService, decorators: [{
4029
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: UnrealCommunicatorService, decorators: [{
3826
4030
  type: Injectable
3827
4031
  }], ctorParameters: () => [] });
3828
4032
 
@@ -4315,6 +4519,7 @@ class InputService extends SubService {
4315
4519
  class RegionsPingService {
4316
4520
  constructor() {
4317
4521
  this.httpClient = inject(HttpClient);
4522
+ this.store = inject(Store);
4318
4523
  this.unrealInitialConfig = inject(UNREAL_CONFIG, { optional: true });
4319
4524
  this.config = {
4320
4525
  ping_count: 2,
@@ -4324,7 +4529,15 @@ class RegionsPingService {
4324
4529
  };
4325
4530
  }
4326
4531
  getFastest(regionListUrl = this.unrealInitialConfig?.regionsPingUrl || '') {
4327
- return this.getProviders(regionListUrl).pipe(switchMap((providers) => this.getPingResult(providers)), catchError(() => of(null)), tapLog('PingResult'), map$1((data) => data?.region_code));
4532
+ // Self-announce every lifecycle waypoint of the region-discovery
4533
+ // flow (start / providers-fetched / resolved). The final
4534
+ // `regionResolved` forwards the winner's full ping-sample array
4535
+ // straight from `PingResult.result` — no aggregation.
4536
+ this.store.dispatch(regionPingFetchStart());
4537
+ 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({
4538
+ region: data?.region_code ?? '',
4539
+ pingsMs: data?.result ?? [],
4540
+ }))), map$1((data) => data?.region_code));
4328
4541
  }
4329
4542
  getPingResult(providers) {
4330
4543
  const regions = providers.regions;
@@ -4405,19 +4618,18 @@ class RegionsPingService {
4405
4618
  throw error;
4406
4619
  }
4407
4620
  }
4408
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: RegionsPingService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
4409
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: RegionsPingService }); }
4621
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RegionsPingService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
4622
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RegionsPingService }); }
4410
4623
  }
4411
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: RegionsPingService, decorators: [{
4624
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RegionsPingService, decorators: [{
4412
4625
  type: Injectable
4413
4626
  }] });
4414
4627
 
4415
4628
  class StreamStatusTelemetryService {
4416
- #store;
4417
- #videoService;
4418
4629
  constructor() {
4419
- this.#store = inject(Store);
4420
- this.#videoService = inject(VideoService);
4630
+ this.store = inject(Store);
4631
+ this.actions$ = inject(Actions);
4632
+ this.videoService = inject(VideoService);
4421
4633
  this.unrealConfig = inject(UNREAL_CONFIG, { optional: true });
4422
4634
  this.init();
4423
4635
  }
@@ -4425,13 +4637,47 @@ class StreamStatusTelemetryService {
4425
4637
  if (this.unrealConfig?.playwright) {
4426
4638
  return;
4427
4639
  }
4428
- this.#videoService.videoStats$
4429
- .pipe(withLatestFrom(this.#store.select(unrealFeature.selectAwsInstance), this.#store.select(unrealFeature.selectLowBandwidth), this.#store.select(unrealFeature.selectLowBandwidthStats), this.#store.select(unrealFeature.selectCirrusConnected)), auditTime(5000), filter(([, , , , cirrusConnected]) => cirrusConnected), map$1(([data, signalingServer, isLowBandwidth, lbmStats]) => this.mapEventData(data, signalingServer, isLowBandwidth, lbmStats)))
4640
+ // Start a telemetry session on every `initSignalling` dispatch.
4641
+ // switchMap cancels the previous session's inner pipeline on re-init.
4642
+ // throttleTime leading+trailing emits the first event immediately and
4643
+ // at most one event every 5s afterwards.
4644
+ this.actions$
4645
+ .pipe(ofType(initSignalling), switchMap$1(() => {
4646
+ // Captured once per session — marks the UTC moment signalling started.
4647
+ const startedAt = Date.now();
4648
+ const startTimeUtc = new Date(startedAt).toISOString();
4649
+ const signalingReadyTime$ = this.readyTimeSince(this.store.select(unrealFeature.selectCirrusConnected), startedAt);
4650
+ const viewPortReadyTime$ = this.readyTimeSince(this.store.select(unrealFeature.selectViewportReady), startedAt);
4651
+ return this.videoService.videoStats$.pipe(throttleTime(5000, asyncScheduler, {
4652
+ leading: true,
4653
+ trailing: true,
4654
+ }), withLatestFrom(this.store.select(unrealFeature.selectAwsInstance), this.store.select(unrealFeature.selectLowBandwidth), this.store.select(unrealFeature.selectLowBandwidthStats), signalingReadyTime$, viewPortReadyTime$), map$1(([data, signalingServer, isLowBandwidth, lbmStats, signalingReadyTime, viewPortReadyTime,]) => this.mapEventData({
4655
+ data,
4656
+ signalingServer,
4657
+ isLowBandwidth,
4658
+ lbmStats,
4659
+ startTimeUtc,
4660
+ signalingReadyTime,
4661
+ viewPortReadyTime,
4662
+ })));
4663
+ }))
4430
4664
  .subscribe((data) => this.trackEventToMixPanel(data));
4431
4665
  }
4432
- mapEventData(data, signalingServer, isLowBandwidth, lbmStats) {
4666
+ /**
4667
+ * Emits `undefined` until `readyState$` first emits a truthy value,
4668
+ * then emits the ms elapsed since `startedAt` and completes.
4669
+ * Used with `withLatestFrom` so events carry a stable duration for the whole session.
4670
+ */
4671
+ readyTimeSince(readyState$, startedAt) {
4672
+ return readyState$.pipe(filter(Boolean), take$1(1), map$1(() => Date.now() - startedAt), startWith$1(undefined));
4673
+ }
4674
+ mapEventData(params) {
4675
+ const { data, signalingServer, isLowBandwidth, lbmStats, startTimeUtc, signalingReadyTime, viewPortReadyTime, } = params;
4433
4676
  const { bitrate, VideoEncoderQP, framesPerSecond, frameWidth, frameHeight, jitter, packetsLost, currentRoundTripTime, runTime, } = data.aggregatedStats;
4434
4677
  return {
4678
+ startTimeUtc,
4679
+ signalingReadyTime,
4680
+ viewPortReadyTime,
4435
4681
  runTime,
4436
4682
  frameWidth,
4437
4683
  frameHeight,
@@ -4449,559 +4695,715 @@ class StreamStatusTelemetryService {
4449
4695
  };
4450
4696
  }
4451
4697
  trackEventToMixPanel(data) {
4452
- this.#store.dispatch(trackMixpanelEvent({ event: 'streamStatus', data }));
4698
+ this.store.dispatch(trackMixpanelEvent({ event: 'streamStatus', data }));
4453
4699
  }
4454
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: StreamStatusTelemetryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
4455
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: StreamStatusTelemetryService }); }
4700
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: StreamStatusTelemetryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
4701
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: StreamStatusTelemetryService }); }
4456
4702
  }
4457
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: StreamStatusTelemetryService, decorators: [{
4703
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: StreamStatusTelemetryService, decorators: [{
4458
4704
  type: Injectable
4459
4705
  }], ctorParameters: () => [] });
4460
4706
 
4461
- class WebRtcPlayerService2 extends SubService {
4707
+ const QP_RED_THRESHOLD = 35;
4708
+ const FPS_LOW_RATIO = 0.7; // p50 fps below 70% of target
4709
+ const UNLIMITED_REFERENCE_FPS = 60;
4710
+ const SAMPLE_WINDOW_MS = 5000;
4711
+ // Persistent per-browser identifier — survives page reloads. Used to group
4712
+ // multiple sessions of the same anonymous user for funnel / retention analysis.
4713
+ const USER_ID_STORAGE_KEY = 'sourceui.streamTelemetry.userId';
4714
+ class StreamStatusTelemetryService2 {
4462
4715
  constructor() {
4463
- super();
4464
- this.onDataChannelMessage$ = new Subject();
4465
- this.signallingSrv = inject(SignallingService);
4716
+ this.store = inject(Store);
4717
+ this.actions$ = inject(Actions);
4466
4718
  this.videoService = inject(VideoService);
4467
- this.commandTelemetryService = inject(CommandTelemetryService);
4468
- this.cfg = {};
4469
- this.useMic = false;
4470
- this.forceTURN = false;
4471
- this.forceMonoAudio = false;
4472
- /** Whether the remote SDP has been applied to the current peer connection */
4473
- this.remoteDescriptionSet = false;
4474
- /** ICE candidates received before the remote description was set */
4475
- this.iceCandidateBuffer = [];
4476
- this.isReconnecting = this.store.selectSignal(unrealFeature.selectIsReconnecting);
4477
- this.init();
4478
- this.actions$.pipe(ofType(dropDC)).subscribe(() => this.dcClient?.close());
4479
- this.actions$.pipe(ofType(dropPC)).subscribe(() => this.pcClient?.close());
4480
- }
4481
- init() {
4482
- this.signallingSrv.onConfig$.subscribe((config) => {
4483
- this.videoService.create();
4484
- this.setConfig(config);
4485
- });
4486
- // Server-initiated offer: set remote description and respond with answer
4487
- this.signallingSrv.onOffer$.pipe(filter(Truthy)).subscribe((offer) => {
4488
- void this.handleServerOffer(offer);
4489
- });
4490
- // Answer to a client-initiated offer
4491
- this.signallingSrv.onWebRtcAnswer$
4492
- .pipe(filter(Truthy))
4493
- .subscribe((answer) => {
4494
- void this.handleServerAnswer(answer);
4495
- });
4496
- // ICE candidates — buffered until remote description is set
4497
- this.signallingSrv.onWebRtcIce$.subscribe((candidate) => {
4498
- this.commandTelemetryService.trackStopCommand('EXT-iceCandidate', true, {
4499
- multi: true,
4500
- });
4501
- if (this.remoteDescriptionSet) {
4502
- this.handleCandidateFromServer(candidate);
4503
- }
4504
- else {
4505
- this.iceCandidateBuffer.push(candidate);
4506
- }
4507
- });
4508
- }
4509
- /**
4510
- * Handles an SDP offer received from the server.
4511
- * Creates a new peer connection, applies the remote offer,
4512
- * then responds with an SDP answer.
4513
- *
4514
- * The DataChannel is NOT created here — the server creates it,
4515
- * and we receive it via the `datachannel` event on the peer connection.
4516
- */
4517
- async handleServerOffer(offer) {
4518
- Logger.info('Received offer from server, creating answer');
4519
- try {
4520
- this.remoteDescriptionSet = false;
4521
- this.iceCandidateBuffer.length = 0;
4522
- if (!this.pcClient) {
4523
- this.setupPeerConnection(this.cfg);
4524
- }
4525
- // Offer already defines media transceivers — don't add extras
4526
- await this.pcClient?.setRemoteDescription(offer);
4527
- this.remoteDescriptionSet = true;
4528
- this.flushIceCandidateBuffer();
4529
- await this.createAnswer(this.pcClient);
4530
- }
4531
- catch (e) {
4532
- Logger.error('Failed to handle server offer', e);
4533
- this.store.dispatch(disconnectStream({
4534
- reason: DisconnectReason.WebRTCError,
4535
- message: 'WebRTCError: Failed to handle server offer',
4536
- }));
4537
- }
4538
- }
4539
- /**
4540
- * Handles an SDP answer received from the server in response to our offer.
4541
- * Applies the remote description and flushes any buffered ICE candidates.
4542
- */
4543
- async handleServerAnswer(answer) {
4544
- Logger.info('Received answer from server');
4545
- try {
4546
- await this.pcClient?.setRemoteDescription(answer);
4547
- this.remoteDescriptionSet = true;
4548
- this.flushIceCandidateBuffer();
4549
- }
4550
- catch (e) {
4551
- Logger.error('Failed to set remote description from answer', e);
4552
- }
4553
- }
4554
- /** Drains buffered ICE candidates after the remote description has been set */
4555
- flushIceCandidateBuffer() {
4556
- for (const candidate of this.iceCandidateBuffer) {
4557
- this.handleCandidateFromServer(candidate);
4558
- }
4559
- this.iceCandidateBuffer.length = 0;
4560
- }
4561
- /**
4562
- * Creates a client-initiated WebRTC offer and sends it to the server.
4563
- * When the client offers, it also creates the DataChannel.
4564
- * Used for peer reconnection through the existing signaling connection.
4565
- */
4566
- async createWebRtcOffer() {
4567
- Logger.info('Creating client-initiated offer');
4568
- try {
4569
- this.remoteDescriptionSet = false;
4570
- this.iceCandidateBuffer.length = 0;
4571
- if (!this.pcClient) {
4572
- this.setupPeerConnection(this.cfg);
4573
- }
4574
- // Client offers: transceivers first, then DC, then offer
4575
- await this.setupTransceiversAsync(this.pcClient);
4576
- this.dcClient = this.createDataChannel(this.pcClient, 'UnrealDcConnection', { ordered: true });
4577
- await this.createOffer(this.pcClient);
4578
- }
4579
- catch (e) {
4580
- Logger.error('Failed to create WebRTC offer', e);
4581
- this.store.dispatch(disconnectStream({
4582
- reason: DisconnectReason.WebRTCError,
4583
- message: 'WebRTCError: Offer Error',
4584
- }));
4585
- }
4586
- }
4587
- /**
4588
- * Initializes a new RTCPeerConnection with event handlers.
4589
- * Transceivers are NOT added here — the caller is responsible for adding
4590
- * them at the correct point depending on the negotiation flow.
4591
- */
4592
- setupPeerConnection(config) {
4593
- this.closePC();
4594
- Logger.log('Creating new RTCPeerConnection with config');
4595
- this.pcClient = new RTCPeerConnection(config);
4596
- this.commandTelemetryService.trackStartCommand('EXT-Browser ICE gathering');
4597
- this.commandTelemetryService.trackStartCommand('EXT-Browser ICE connection');
4598
- this.commandTelemetryService.trackStartCommand('EXT-Signaling state change');
4599
- this.handlePeerConnectionEvents(this.pcClient);
4600
- this.disconnect$.pipe(take(1)).subscribe(() => {
4601
- this.closePC();
4602
- });
4603
- }
4604
- createDataChannel(pc, label, options) {
4605
- const datachannel = pc.createDataChannel(label, options);
4606
- return this.setupDataChannelCallbacks(datachannel);
4607
- }
4608
- handleCandidateFromServer(iceCandidate) {
4609
- try {
4610
- const candidate = new RTCIceCandidate(iceCandidate);
4611
- Logger.info('[Unreal ICE candidate]', '| Type=', candidate.type, '| Protocol=', candidate.protocol, '| Address=', candidate.address, '| Port=', candidate.port, '|');
4612
- // If forcing TURN, reject any candidates not relay
4613
- if (this.forceTURN) {
4614
- if (candidate.candidate.indexOf('relay') < 0) {
4615
- Logger.warn('Dropping candidate because it was not TURN relay.', '| Type=', candidate.type, '| Protocol=', candidate.protocol, '| Address=', candidate.address, '| Port=', candidate.port, '|');
4616
- return;
4617
- }
4618
- }
4619
- this.pcClient?.addIceCandidate(candidate).catch((e) => {
4620
- Logger.warn('Failed to add ICE candidate', e);
4621
- });
4622
- }
4623
- catch (e) {
4624
- Logger.error('Error handleCandidateFromServer', e);
4625
- }
4626
- }
4627
- /**
4628
- * Reorders the `m=video` line in the SDP so that payload types matching
4629
- * the given H.264 `profile-level-id` appear first, making the browser
4630
- * prefer that profile during codec negotiation.
4631
- *
4632
- * A `profile-level-id` is a 3-byte hex string (`XXYYZZ`) defined in
4633
- * RFC 6184 that encodes three H.264 parameters:
4634
- * - **Profile IDC** (`XX`) — the H.264 profile.
4635
- * - **Profile IOP** (`YY`) — constraint flags that further narrow the profile.
4636
- * - **Level IDC** (`ZZ`) — the level, which caps resolution/framerate/bitrate.
4637
- *
4638
- * ### H.264 profiles relevant to Unreal Engine Pixel Streaming
4639
- *
4640
- * | profile-level-id | Profile | Level | Notes |
4641
- * |------------------|----------------------------|-------|----------------------------------------------|
4642
- * | `42001f` | Baseline | 3.1 | No B-frames, low latency; legacy devices |
4643
- * | `42e01f` | Constrained Baseline | 3.1 | Widest HW decode support; **recommended** |
4644
- * | `4d001f` | Main | 3.1 | B-frames + CABAC; better compression |
4645
- * | `4d4028` | Main | 4.0 | Up to 1080p60; common desktop target |
4646
- * | `640028` | High | 4.0 | Best quality/compression; most GPUs support |
4647
- * | `64002a` | High | 4.2 | Up to 1080p120 or 4K30 |
4648
- * | `640033` | High | 5.1 | Up to 4K60; requires modern HW |
4649
- * | `640034` | High | 5.2 | Up to 4K120; high-end HW only |
4650
- * | `6e001f` | High 10 | 3.1 | 10-bit color; limited browser support |
4651
- * | `f4001f` | High 4:4:4 Predictive | 3.1 | Lossless capable; rare in browsers |
4652
- *
4653
- * UE Pixel Streaming defaults to Constrained Baseline (`42e01f`) or High (`6400xx`)
4654
- * depending on engine version and `WebRTC.H264Profile` CVar.
4655
- *
4656
- * We prioritize `42e01f` because it guarantees hardware decoding on
4657
- * virtually all client devices (desktop, mobile, embedded), avoiding
4658
- * fallback to software decoding or negotiation of a higher profile
4659
- * that the client GPU may not accelerate.
4660
- */
4661
- prioritizeH264Profile(sdp, profileLevelId) {
4662
- const lines = sdp.split('\r\n');
4663
- const targetPts = [];
4664
- for (const line of lines) {
4665
- if (line.startsWith('a=fmtp:') &&
4666
- line.toLowerCase().includes(`profile-level-id=${profileLevelId}`)) {
4667
- const pt = line.split(':')[1]?.split(' ')[0];
4668
- if (pt)
4669
- targetPts.push(pt);
4670
- }
4671
- }
4672
- if (targetPts.length === 0)
4673
- return sdp;
4674
- return lines
4675
- .map((line) => {
4676
- if (!line.startsWith('m=video'))
4677
- return line;
4678
- const parts = line.split(' ');
4679
- // parts: m=video <port> <proto> <pt1> <pt2> ...
4680
- const header = parts.slice(0, 3);
4681
- const pts = parts.slice(3);
4682
- const prioritized = [
4683
- ...targetPts.filter((pt) => pts.includes(pt)),
4684
- ...pts.filter((pt) => !targetPts.includes(pt)),
4685
- ];
4686
- return [...header, ...prioritized].join(' ');
4687
- })
4688
- .join('\r\n');
4689
- }
4690
- mungeSDP(sdp) {
4691
- let mungedSDP = this.prioritizeH264Profile(sdp, '42e01f');
4692
- mungedSDP = mungedSDP.replace(/(a=fmtp:\d+ .*level-asymmetry-allowed=.*)\r\n/gm, '$1;x-google-start-bitrate=10000;x-google-max-bitrate=100000\r\n');
4693
- // Set max bitrate to highest bitrate Opus supports
4694
- let audioSDP = 'maxaveragebitrate=510000;';
4695
- if (this.useMic) {
4696
- // Set the max capture rate to 48khz (so we can send high-quality audio from mic)
4697
- audioSDP += 'sprop-maxcapturerate=48000;';
4698
- }
4699
- // Force mono or stereo based on whether ?forceMono was passed or not
4700
- audioSDP += this.forceMonoAudio ? 'stereo=0;' : 'stereo=1;';
4701
- // Enable in-band forward error correction for opus audio
4702
- audioSDP += 'useinbandfec=1';
4703
- // We use the line 'useinbandfec=1' (which Opus uses) to set our Opus specific audio parameters.
4704
- mungedSDP = mungedSDP?.replace('useinbandfec=1', audioSDP);
4705
- return mungedSDP;
4706
- }
4707
- /**
4708
- * Closes existing PeerConnection and DataChannel, resets negotiation state.
4709
- */
4710
- closePC() {
4711
- if (this.pcClient) {
4712
- Logger.warn('Closing existing PeerConnection');
4713
- }
4714
- this.pcClient?.close();
4715
- this.pcClient = null;
4716
- if (this.dcClient) {
4717
- Logger.warn('Closing existing DataChannel');
4718
- }
4719
- this.dcClient?.close();
4720
- this.dcClient = null;
4721
- this.remoteDescriptionSet = false;
4722
- this.iceCandidateBuffer.length = 0;
4723
- }
4724
- canSend() {
4725
- return this.dcClient && this.dcClient.readyState === 'open';
4726
- }
4727
- send(data) {
4728
- if (this.canSend()) {
4729
- this.dcClient?.send(data);
4730
- }
4731
- else {
4732
- Logger.colored('black', 'yellow', `DataChannel connection not prepared or in state: ${this.dcClient?.readyState}`);
4733
- // Trigger peer reconnection if DC is not open and we're not yet reconnecting
4734
- this.tryReconnectOnSendFailure();
4735
- }
4736
- }
4737
- /**
4738
- * Attempts to reconnect the peer when send fails due to DataChannel not being open.
4739
- * This handles cases where the DC connection is broken, but no error events are fired.
4740
- */
4741
- tryReconnectOnSendFailure() {
4742
- // Skip if already reconnecting or signaling is not connected
4743
- if (this.isReconnecting()) {
4744
- Logger.info('[Reconnect] Already reconnecting, skipping duplicate attempt');
4745
- return;
4746
- }
4747
- if (!this.signallingSrv.isConnected()) {
4748
- Logger.warn('[Reconnect] Cannot reconnect: Signaling not connected');
4749
- return;
4750
- }
4751
- // Check if DC exists but is in a non-open state (closing, closed, or connecting stuck)
4752
- const dcState = this.dcClient?.readyState;
4753
- if (dcState && dcState !== 'open') {
4754
- Logger.warn(`[Reconnect] DataChannel in "${dcState}" state during send, initiating peer reconnection`);
4755
- this.store.dispatch(reconnectPeer({ attempt: 1, maxAttempts: 3, delay: 0 }));
4756
- }
4757
- }
4758
- /**
4759
- * Reconnects only the WebRTC peer connection without re-establishing signaling.
4760
- * Closes the current PeerConnection and creates a new client-initiated offer
4761
- * through the existing WebSocket connection.
4762
- * @returns true if reconnection was initiated, false if signaling is not connected
4763
- */
4764
- reconnectPeer() {
4765
- if (!this.signallingSrv.isConnected()) {
4766
- Logger.error('[Reconnect] Cannot reconnect peer: Signaling not connected');
4767
- return false;
4768
- }
4769
- if (!this.cfg || Object.keys(this.cfg).length === 0) {
4770
- Logger.error('[Reconnect] Cannot reconnect peer: No config available');
4771
- return false;
4772
- }
4773
- Logger.info('[Reconnect] Initiating peer reconnection');
4774
- // Close only the PeerConnection, keep WebSocket alive
4775
- this.closePC();
4776
- // Create a new WebRTC offer using the stored config
4777
- void this.createWebRtcOffer();
4778
- return true;
4779
- }
4780
- setConfig(parOptions = {}) {
4781
- const urlParams = new URLSearchParams(window.location.search);
4782
- //**********************
4783
- //Config setup
4784
- //**********************
4785
- this.cfg = { ...parOptions.peerConnectionOptions };
4786
- this.forceTURN = urlParams.has('ForceTURN');
4787
- if (this.forceTURN) {
4788
- Logger.info('Forcing TURN usage by setting ICE Transport Policy in peer connection config.');
4789
- this.cfg.iceTransportPolicy = 'relay';
4790
- }
4791
- this.cfg.bundlePolicy = urlParams.has('ForceMaxBundle')
4792
- ? 'max-bundle'
4793
- : 'balanced';
4794
- this.cfg.iceCandidatePoolSize = 1;
4795
- // When ?useMic check for SSL or localhost
4796
- const isLocalhostConnection = location.hostname === 'localhost' || location.hostname === '127.0.0.1';
4797
- const isHttpsConnection = location.protocol === 'https:';
4798
- if (this.useMic && !isLocalhostConnection && !isHttpsConnection) {
4799
- this.useMic = false;
4800
- Logger.error('Microphone access in the browser will not work if you are not on HTTPS or localhost. Disabling mic access.');
4801
- Logger.error("For testing you can enable HTTP microphone access Chrome by visiting chrome://flags/ and enabling 'unsafely-treat-insecure-origin-as-secure'");
4719
+ this.signallingService = inject(SignallingService);
4720
+ this.unrealConfig = inject(UNREAL_CONFIG, { optional: true });
4721
+ /**
4722
+ * Per-browser anonymous user id. Read from `localStorage` on first access;
4723
+ * if missing, a new UUID is generated and persisted. Included as
4724
+ * `data.userId` on every emitted event — the server uses it to bucket
4725
+ * per-session files under `data/<userId>/<sessionId>.jsonl`.
4726
+ */
4727
+ this.userId = this.getOrCreateUserId();
4728
+ this.session = null;
4729
+ /**
4730
+ * Whether a session has already started during the current page lifetime.
4731
+ * Used to distinguish a fresh page load (bootstrap measurement is
4732
+ * meaningful) from a Resume/restart (the document is already loaded,
4733
+ * so "bootstrap" is a no-op). Only the first session gets the bootstrap
4734
+ * phase anchored at navigation start (perf.now() === 0).
4735
+ */
4736
+ this.hasStartedBefore = false;
4737
+ /**
4738
+ * Rolling snapshot of the currently selected WebRTC ICE candidate pair
4739
+ * (local/remote type + url). Scanned over `videoStats$` so the stream
4740
+ * *keeps* the first non-null pair even if later stats emit null — WebRTC
4741
+ * doesn't un-select a pair mid-session, but the source is nullable. The
4742
+ * `startWith(null)` seed makes `withLatestFrom` on this stream safe even
4743
+ * before the first video stat arrives (common at data-channel-open).
4744
+ */
4745
+ this.selectedPair$ = this.videoService.videoStats$.pipe(map((s) => s.aggregatedStats.selectedPair ?? null), scan((acc, curr) => curr ?? acc, null), startWith(null));
4746
+ /**
4747
+ * Correlation-matched Observable: emits once when the first `loopBack`
4748
+ * command's corresponding `command completed` action fires. Built from
4749
+ * the two related actions — the command-started captures the correlation
4750
+ * id, the command-completed with that id emits.
4751
+ */
4752
+ this.loopbackReceived$ = this.actions$.pipe(ofType(commandStarted), filter$1(({ command }) => command === 'loopBack'), take(1), switchMap(({ id }) => this.actions$.pipe(ofType(commandCompleted), filter$1((b) => b.id === id), take(1))));
4753
+ // Overlay-referenced milestones — hoisted as fields so both
4754
+ // `milestones: [...]` and `overlays: [...]` share the same object
4755
+ // identity (the in-session Map keys milestones by that identity).
4756
+ this.pollingStartedEntry = {
4757
+ label: 'Polling started (HTTP orchestration)',
4758
+ event: this.actions$.pipe(ofType(pollingStarted)),
4759
+ };
4760
+ this.awsInstanceResolvedEntry = {
4761
+ label: 'Instance WS URL resolved',
4762
+ event: this.actions$.pipe(ofType(setAwsInstance)),
4763
+ };
4764
+ this.loopbackReceivedEntry = {
4765
+ label: 'LoopBack received',
4766
+ event: this.loopbackReceived$,
4767
+ };
4768
+ this.signalingServerConnectedEntry = {
4769
+ label: 'Websocket connected',
4770
+ event: this.store
4771
+ .select(unrealFeature.selectCirrusConnected)
4772
+ .pipe(filter$1(Boolean), take(1)),
4773
+ };
4774
+ this.configReceivedEntry = {
4775
+ label: 'Config received',
4776
+ event: this.signallingService.onConfig$.pipe(filter$1(Boolean), take(1)),
4777
+ };
4778
+ /**
4779
+ * Unified phase catalog — the single source of truth for everything
4780
+ * this service tracks after `session_started`. Each entry is a phase;
4781
+ * its `milestones` array is ordered chronologically and the *last*
4782
+ * entry is the phase closer. When that closer's observable fires, the
4783
+ * service emits one generic `stream.phase_ended` wire event with every
4784
+ * display field the viewer needs.
4785
+ *
4786
+ * `phaseNo` is the array index; `startPhase` chains from the previous
4787
+ * phase's `endPhase` — neither needs to be specified here.
4788
+ */
4789
+ this.phaseCatalog = [
4790
+ {
4791
+ label: 'Region resolving',
4792
+ color: '#61afef',
4793
+ milestones: [
4794
+ {
4795
+ label: 'Region ping fetch start',
4796
+ event: this.actions$.pipe(ofType(regionPingFetchStart)),
4797
+ },
4798
+ {
4799
+ label: 'Region ping fetch end',
4800
+ event: this.actions$.pipe(ofType(regionPingFetchEnd)),
4801
+ },
4802
+ // CLOSER — the `regionResolved` action carries the raw ping
4803
+ // samples (one per probe) from `RegionsPingService`; the
4804
+ // catalog flattens them to a comma-separated string since
4805
+ // `toPhaseParams` only accepts primitives.
4806
+ {
4807
+ label: 'Region resolved',
4808
+ event: this.actions$.pipe(ofType(regionResolved), take(1), map(({ region, pingsMs }) => ({
4809
+ region,
4810
+ pingsMs: pingsMs.length ? `${pingsMs.join(', ')} ms` : null,
4811
+ }))),
4812
+ },
4813
+ ],
4814
+ },
4815
+ {
4816
+ label: 'Signaling connection',
4817
+ color: '#c678dd',
4818
+ overlays: [
4819
+ {
4820
+ from: this.pollingStartedEntry,
4821
+ to: this.awsInstanceResolvedEntry,
4822
+ title: 'Polling → instance resolved (HTTP polling + AWS instance handle)',
4823
+ },
4824
+ {
4825
+ from: this.signalingServerConnectedEntry,
4826
+ to: this.configReceivedEntry,
4827
+ title: 'Signaling handshake (WS open → Cirrus config received)',
4828
+ },
4829
+ ],
4830
+ milestones: [
4831
+ this.pollingStartedEntry,
4832
+ {
4833
+ label: 'Polling ended (HTTP orchestration)',
4834
+ event: this.actions$.pipe(ofType(pollingEnded)),
4835
+ },
4836
+ this.awsInstanceResolvedEntry,
4837
+ this.signalingServerConnectedEntry,
4838
+ {
4839
+ label: 'Instance reserved (WS orchestration)',
4840
+ event: this.actions$.pipe(ofType(setOrchestrationParameters)),
4841
+ },
4842
+ {
4843
+ label: 'Instance ready (WS orchestration)',
4844
+ event: this.actions$.pipe(ofType(instanceReady)),
4845
+ },
4846
+ this.configReceivedEntry,
4847
+ {
4848
+ label: 'SSInfo received',
4849
+ event: this.store.select(unrealFeature.selectSsData).pipe(filter$1(Boolean), take(1), withLatestFrom$1(this.store.select(unrealFeature.selectAwsInstance)), map(([ssData, aws]) => ({
4850
+ client: aws.wsUrl,
4851
+ instanceName: aws.instanceName,
4852
+ ...ssData,
4853
+ }))),
4854
+ },
4855
+ ],
4856
+ },
4857
+ {
4858
+ label: 'WebRTC negotiation',
4859
+ color: '#e5c07b',
4860
+ milestones: [
4861
+ {
4862
+ label: 'Offer received',
4863
+ event: this.signallingService.onOffer$.pipe(filter$1(Boolean)),
4864
+ },
4865
+ {
4866
+ label: 'Answer sent (server-offer flow)',
4867
+ event: this.actions$.pipe(ofType(answerSent)),
4868
+ },
4869
+ {
4870
+ label: 'Answer received (client-offer flow)',
4871
+ event: this.signallingService.onWebRtcAnswer$.pipe(filter$1(Boolean)),
4872
+ },
4873
+ {
4874
+ label: 'First ICE candidate from server',
4875
+ event: this.signallingService.onWebRtcIce$.pipe(filter$1(Boolean)),
4876
+ },
4877
+ {
4878
+ label: 'Last ICE candidate from server',
4879
+ event: this.signallingService.onWebRtcIce$.pipe(filter$1(Boolean)),
4880
+ mode: 'last',
4881
+ },
4882
+ // CLOSER — snaps negotiation diagnostics and the Cirrus
4883
+ // streamer identity (`ssData`, reliably populated by DC-open
4884
+ // time) into phase-3 params:
4885
+ // - `iceCandidateCount` (total ICE candidates from server — too
4886
+ // few hints at a restrictive network)
4887
+ // - `handshakeFlow` (server-offer vs client-offer; we only use
4888
+ // server-offer today, but the detector stays in place)
4889
+ // - `instanceType` / `branchInfo` / version / path / app /
4890
+ // `connectionId` (streamer identity, from `ssInfo` message)
4891
+ {
4892
+ label: 'Data channel connected',
4893
+ event: this.store
4894
+ .select(unrealFeature.selectDataChannelConnected)
4895
+ .pipe(filter$1(Boolean), take(1), map(() => ({
4896
+ iceCandidateCount: this.session?.iceCandidateCount ?? 0,
4897
+ handshakeFlow: this.session?.handshakeFlow ?? null,
4898
+ }))),
4899
+ },
4900
+ ],
4901
+ },
4902
+ {
4903
+ label: 'Unreal communication',
4904
+ color: '#98c379',
4905
+ overlays: [
4906
+ {
4907
+ from: this.loopbackReceivedEntry,
4908
+ title: 'Stabilization (warm up)',
4909
+ },
4910
+ ],
4911
+ milestones: [
4912
+ {
4913
+ label: 'First command started',
4914
+ event: this.actions$.pipe(ofType(commandStarted)),
4915
+ },
4916
+ {
4917
+ label: 'LoopBack sent',
4918
+ event: this.actions$.pipe(ofType(commandStarted), filter$1(({ command }) => command === 'loopBack')),
4919
+ },
4920
+ this.loopbackReceivedEntry,
4921
+ // CLOSER — flattens the selected ICE candidate pair into two
4922
+ // string params (local / remote), and snapshots the running
4923
+ // command counter so phase 5 shows how many commands the client
4924
+ // fired at Unreal before the viewport was ready.
4925
+ {
4926
+ label: 'Viewport ready',
4927
+ event: this.store.select(unrealFeature.selectViewportReady).pipe(filter$1(Boolean), take(1), withLatestFrom$1(this.selectedPair$), map(([, selectedPair]) => ({
4928
+ localCandidate: fmtCandidate(selectedPair?.local),
4929
+ remoteCandidate: fmtCandidate(selectedPair?.remote),
4930
+ commandCount: this.session?.commandCount ?? 0,
4931
+ }))),
4932
+ },
4933
+ ],
4934
+ },
4935
+ ];
4936
+ if (this.unrealConfig?.playwright)
4937
+ return;
4938
+ if (!this.unrealConfig?.streamTelemetryV2)
4939
+ return;
4940
+ this.init();
4941
+ }
4942
+ getOrCreateUserId() {
4943
+ try {
4944
+ const existing = localStorage.getItem(USER_ID_STORAGE_KEY);
4945
+ if (existing && existing.length > 0)
4946
+ return existing;
4947
+ const fresh = v4();
4948
+ localStorage.setItem(USER_ID_STORAGE_KEY, fresh);
4949
+ return fresh;
4950
+ }
4951
+ catch {
4952
+ // localStorage disabled (private-mode restrictions, quota, etc.)
4953
+ // Fall back to a per-tab ephemeral id so telemetry keeps working.
4954
+ return v4();
4802
4955
  }
4803
- this.setupPeerConnection(this.cfg);
4804
4956
  }
4805
- handlePeerConnectionEvents(pcClient) {
4806
- fromEvent(pcClient, 'signalingstatechange')
4807
- .pipe(map$1(() => this.pcClient?.signalingState), takeUntil(this.disconnect$))
4808
- .subscribe((state) => {
4809
- Logger.info('[Signaling state change] |', state, '|');
4810
- this.commandTelemetryService.trackStopCommand('EXT-Signaling state change', true, { state, multi: true });
4811
- });
4812
- fromEvent(pcClient, 'iceconnectionstatechange')
4813
- .pipe(map$1(() => this.pcClient?.iceConnectionState), takeUntil(this.disconnect$))
4814
- .subscribe((state) => {
4815
- Logger.info('[Browser ICE connection] |', state, '|');
4816
- this.commandTelemetryService.trackStopCommand('EXT-Browser ICE connection', true, { state, multi: true });
4817
- });
4818
- fromEvent(pcClient, 'icegatheringstatechange')
4819
- .pipe(map$1(() => this.pcClient?.iceGatheringState), takeUntil(this.disconnect$))
4820
- .subscribe((state) => {
4821
- Logger.info('[Browser ICE gathering] |', state, '|');
4822
- this.commandTelemetryService.trackStopCommand('EXT-Browser ICE gathering', true, { state, multi: true });
4957
+ init() {
4958
+ // Start a new session on every initSignalling dispatch.
4959
+ this.actions$
4960
+ .pipe(ofType(initSignalling))
4961
+ .subscribe(() => this.startSession());
4962
+ // End the current session when Cirrus drops (true → false transition).
4963
+ this.store
4964
+ .select(unrealFeature.selectCirrusConnected)
4965
+ .pipe(pairwise(), filter$1(([prev, curr]) => prev && !curr))
4966
+ .subscribe(() => this.endSession('disconnected'));
4967
+ // Close the session when the tab is hidden / being evicted from bfcache.
4968
+ // `pagehide` fires in more cases than `beforeunload` (covers iOS Safari, bfcache).
4969
+ fromEvent(window, 'pagehide').subscribe(() => this.endSession('pagehide'));
4970
+ }
4971
+ startSession() {
4972
+ // If a previous session is still active, close it first.
4973
+ this.endSession('restart');
4974
+ const id = v4();
4975
+ const startedAt = Date.now();
4976
+ const startedAtPerf = performance.now();
4977
+ const sub = new Subscription();
4978
+ // Anchor selection:
4979
+ // - First session on this page → anchor at navigation start (0).
4980
+ // `performance.now()` is already "ms since navigation start", so
4981
+ // `session_started.duration` becomes the full page-load + idle-time
4982
+ // leading up to the first stream start (i.e. the real bootstrap).
4983
+ // - Resume/restart sessions → anchor at the session's own start.
4984
+ // The document is already loaded — a "bootstrap" phase would either
4985
+ // balloon (to include all prior idle time) or be misleading, so we
4986
+ // suppress it by making the anchor == session_started.
4987
+ const isFirstSession = !this.hasStartedBefore;
4988
+ this.hasStartedBefore = true;
4989
+ const anchorPerf = isFirstSession ? 0 : startedAtPerf;
4990
+ const times = new Map();
4991
+ this.session = {
4992
+ id,
4993
+ startedAt,
4994
+ startedAtPerf,
4995
+ anchorPerf,
4996
+ // Initial milestone anchor = anchorPerf. For the first session this is
4997
+ // navigation start (0), so `session_started.duration` becomes the full
4998
+ // page-load elapsed. For Resume sessions anchor == startedAtPerf, so
4999
+ // `session_started.duration` is 0 (no bootstrap phase).
5000
+ lastMilestonePerf: anchorPerf,
5001
+ seq: 0,
5002
+ sub,
5003
+ qpBad: false,
5004
+ fpsLow: false,
5005
+ maxFpsTarget: UNLIMITED_REFERENCE_FPS,
5006
+ iceCandidateCount: 0,
5007
+ handshakeFlow: null,
5008
+ commandCount: 0,
5009
+ times,
5010
+ };
5011
+ // Side-channel counters for WebRTC negotiation diagnostics — shown
5012
+ // as phase-3 params on the `datachannel_connected` closer. Kept
5013
+ // imperative (session-scoped) so no RxJS hot-stream plumbing is
5014
+ // needed to survive the subscribe-on-close timing.
5015
+ sub.add(this.signallingService.onWebRtcIce$
5016
+ .pipe(filter$1(Boolean))
5017
+ .subscribe(() => {
5018
+ if (this.session)
5019
+ this.session.iceCandidateCount++;
5020
+ }));
5021
+ sub.add(this.actions$.pipe(ofType(answerSent), take(1)).subscribe(() => {
5022
+ if (this.session)
5023
+ this.session.handshakeFlow = 'server-offer';
5024
+ }));
5025
+ sub.add(this.signallingService.onWebRtcAnswer$
5026
+ .pipe(filter$1(Boolean), take(1))
5027
+ .subscribe(() => {
5028
+ if (this.session)
5029
+ this.session.handshakeFlow = 'client-offer';
5030
+ }));
5031
+ // Unreal-command counter — increments on every `commandStarted`
5032
+ // action. Snapshotted into the `viewport_ready` closer payload as
5033
+ // phase-5's `commandCount`.
5034
+ sub.add(this.actions$.pipe(ofType(commandStarted)).subscribe(() => {
5035
+ if (this.session)
5036
+ this.session.commandCount++;
5037
+ }));
5038
+ // Unified catalog loop — one subscription per entry. Non-closers
5039
+ // record their emission time into the session map. The *last* entry
5040
+ // of each phase is the closer: its emission fires a single generic
5041
+ // `stream.phase_ended` event carrying every display field the viewer
5042
+ // needs. The viewer never inspects milestone names or phase kinds;
5043
+ // adding a new phase to `phaseCatalog` is enough — zero viewer work.
5044
+ this.phaseCatalog.forEach((bucket, bucketIdx) => {
5045
+ bucket.milestones.forEach((entry, idx) => {
5046
+ const isCloser = idx === bucket.milestones.length - 1;
5047
+ sub.add(entry.event.subscribe((value) => {
5048
+ if (!this.session)
5049
+ return;
5050
+ if (isCloser) {
5051
+ this.emitPhaseEnded(bucket, bucketIdx, value);
5052
+ return;
5053
+ }
5054
+ const time = Math.round(performance.now() - this.session.anchorPerf);
5055
+ if (entry.mode !== 'last' && this.session.times.has(entry))
5056
+ return;
5057
+ this.session.times.set(entry, time);
5058
+ }));
5059
+ });
4823
5060
  });
4824
- fromEvent(pcClient, 'connectionstatechange')
4825
- .pipe(map$1(() => this.pcClient?.connectionState), takeUntil(this.disconnect$))
4826
- .subscribe((state) => {
4827
- Logger.info('[RTCPeerConnection state] |', state, '|');
5061
+ const bootstrapInfo = this.collectBootstrapInfo();
5062
+ this.emitLifecycle('stream.session_started', {
5063
+ sessionId: id,
5064
+ startTimeUtc: new Date(startedAt).toISOString(),
5065
+ pixelRatio: window.devicePixelRatio,
5066
+ // Browser viewport size at session start (`window.innerWidth` /
5067
+ // `innerHeight`). Named `browserInnerSize` to distinguish from
5068
+ // the physical screen — what gates the rendered video resolution
5069
+ // is the container we started from, not the device hardware.
5070
+ browserInnerSize: {
5071
+ width: window.innerWidth,
5072
+ height: window.innerHeight,
5073
+ },
5074
+ userAgent: navigator.userAgent,
5075
+ sampleWindowMs: SAMPLE_WINDOW_MS,
5076
+ // Full page URL — `location.href` is the only reliable way to get this.
5077
+ // The server-observed `Referer` header is truncated to origin for
5078
+ // cross-origin requests by default Referrer-Policy.
5079
+ pageUrl: window.location.href,
5080
+ // Navigation / page-load context for the "app bootstrap → session_started"
5081
+ // waterfall phase. Only useful for the first session (`bootstrap` phase
5082
+ // is suppressed on Resume), but `bootstrapInfo` describes the document,
5083
+ // not the session — harmless and cheap to send either way.
5084
+ ...(bootstrapInfo && { bootstrapInfo }),
5085
+ },
5086
+ // Pin the milestone to the captured `performance.now()` so `duration`
5087
+ // equals "ms since navigation start" precisely (for first sessions),
5088
+ // rather than being inflated by any synchronous setup done in between.
5089
+ startedAtPerf);
5090
+ // Track current maxFps target to evaluate FPS-ratio anomalies.
5091
+ sub.add(this.actions$
5092
+ .pipe(ofType(setMaxFps))
5093
+ .subscribe(({ maxFps }) => this.updateMaxFpsTarget(maxFps)));
5094
+ // Periodic aggregated sample — every 5s window.
5095
+ sub.add(this.videoService.videoStats$
5096
+ .pipe(bufferTime(SAMPLE_WINDOW_MS), filter$1((samples) => samples.length > 0))
5097
+ .subscribe((samples) => this.handleWindow(samples)));
5098
+ }
5099
+ endSession(reason) {
5100
+ if (!this.session)
5101
+ return;
5102
+ const { id, sub } = this.session;
5103
+ this.emitLifecycle('stream.session_ended', {
5104
+ sessionId: id,
5105
+ reason,
5106
+ timestamp: new Date().toISOString(),
4828
5107
  });
4829
- fromEvent(pcClient, 'icecandidate')
4830
- .pipe(map$1((e) => e.candidate), filter(Truthy), tap$1((candidate) => Logger.info('[Browser ICE candidate]', '| Type=', candidate.type, '| Protocol=', candidate.protocol, '| Address=', candidate.address, '| Port=', candidate.port, '|')), takeUntil(this.disconnect$))
4831
- .subscribe((candidate) => this.signallingSrv.send({ type: 'iceCandidate', candidate }));
4832
- // When the server creates the DataChannel (server-offer flow),
4833
- // we receive it here via the 'datachannel' event
4834
- fromEvent(pcClient, 'datachannel')
4835
- .pipe(tap$1((e) => Logger.info(`[DATACHANNEL] Received remote DC: label="${e.channel.label}" id=${e.channel.id} protocol="${e.channel.protocol}" ordered=${e.channel.ordered}`)), map$1((e) => e.channel), takeUntil(this.disconnect$))
4836
- .subscribe((datachannel) => {
4837
- this.dcClient = this.setupDataChannelCallbacks(datachannel);
5108
+ sub.unsubscribe();
5109
+ this.session = null;
5110
+ }
5111
+ /**
5112
+ * Emit a lifecycle event with a `duration` field — ms between the
5113
+ * previous lifecycle event and now (phase-local, NOT anchor-relative).
5114
+ * For `session_started` this equals the full page-load elapsed on the
5115
+ * first session, or 0 on Resume. Advances `lastMilestonePerf` so the
5116
+ * next event's duration is correctly rooted.
5117
+ */
5118
+ emitLifecycle(event, extra, atPerf) {
5119
+ if (!this.session)
5120
+ return;
5121
+ // `atPerf` lets callers pin the milestone to a specific `performance.now()`
5122
+ // value — useful for `session_started` where the wall-clock and perf-clock
5123
+ // anchors were already captured at the top of `startSession()`.
5124
+ const now = atPerf ?? performance.now();
5125
+ const duration = Math.round(now - this.session.lastMilestonePerf);
5126
+ this.track(event, { duration, ...extra });
5127
+ this.session.lastMilestonePerf = now;
5128
+ }
5129
+ /**
5130
+ * Emit the unified `stream.phase_ended` wire event for a closer. Packs
5131
+ * every display field the viewer consumes — phaseNo (derived from the
5132
+ * catalog index), label, color, milestones, params, overlays. Boundary
5133
+ * labels aren't on the wire — the viewer derives them from neighbour
5134
+ * phases' `label` fields.
5135
+ */
5136
+ emitPhaseEnded(bucket, bucketIdx, extrasValue) {
5137
+ if (!this.session)
5138
+ return;
5139
+ const now = performance.now();
5140
+ const phaseStartMs = Math.round(this.session.lastMilestonePerf - this.session.anchorPerf);
5141
+ const phaseEndMs = Math.round(now - this.session.anchorPerf);
5142
+ const milestones = this.milestonesOfBucket(bucket);
5143
+ const params = toPhaseParams(extrasValue);
5144
+ const overlays = resolveOverlays(bucket.overlays, this.session.times, phaseStartMs, phaseEndMs);
5145
+ this.emitLifecycle('stream.phase_ended', {
5146
+ sessionId: this.session.id,
5147
+ phaseNo: bucketIdx + 1,
5148
+ label: bucket.label,
5149
+ color: bucket.color,
5150
+ milestones,
5151
+ params,
5152
+ overlays,
4838
5153
  });
4839
- const onTrack$ = fromEvent(pcClient, 'track').pipe(takeUntil(this.disconnect$), filter((e) => !!e.track), tap$1((e) => Logger.info('Got track - ' +
4840
- e.track.kind +
4841
- ' id=' +
4842
- e.track.id +
4843
- ' readyState=' +
4844
- e.track.readyState)), share());
4845
- onTrack$
4846
- .pipe(filter((e) => e.track.kind === 'audio'), map$1((e) => e.streams[0]))
4847
- .subscribe((stream) => this.videoService.attachAudioStream(stream));
4848
- onTrack$
4849
- .pipe(filter((e) => e.track.kind === 'video'), map$1((e) => e.streams[0]), tap$1(() => Logger.info('Set video source from video track ontrack.')))
4850
- .subscribe((stream) => {
4851
- if (this.pcClient) {
4852
- this.videoService.attachVideoStream(stream, this.pcClient);
4853
- }
5154
+ }
5155
+ handleWindow(samples) {
5156
+ if (!this.session)
5157
+ return;
5158
+ const s = this.session;
5159
+ const bitrate = this.aggregate(samples.map((x) => x.aggregatedStats.bitrate ?? 0));
5160
+ const fps = this.aggregate(samples.map((x) => x.aggregatedStats.framesPerSecond ?? 0));
5161
+ const qp = this.aggregate(samples.map((x) => x.aggregatedStats.VideoEncoderQP ?? 0));
5162
+ const jitter = this.aggregate(samples.map((x) => x.aggregatedStats.jitter ?? 0));
5163
+ const rtt = this.aggregate(samples.map((x) => x.aggregatedStats.currentRoundTripTime ?? 0));
5164
+ const latest = samples[samples.length - 1]?.aggregatedStats;
5165
+ // Periodic sample event.
5166
+ this.track('stream.sample', {
5167
+ sessionId: s.id,
5168
+ seq: s.seq++,
5169
+ timestamp: new Date().toISOString(),
5170
+ sampleCount: samples.length,
5171
+ // Tab visibility state at emission time. Chrome does NOT throttle timers
5172
+ // for WebRTC-active tabs, so the handler runs on time even when hidden —
5173
+ // the point-in-time read is reliable in this specific context.
5174
+ visibilityState: document.visibilityState,
5175
+ bitrate,
5176
+ fps,
5177
+ qp,
5178
+ jitter,
5179
+ rtt,
5180
+ packetsLost: latest?.packetsLost,
5181
+ viewPort: {
5182
+ width: latest?.frameWidth,
5183
+ height: latest?.frameHeight,
5184
+ },
4854
5185
  });
5186
+ this.evaluateAnomalies(fps, qp);
4855
5187
  }
4856
- async setupTransceiversAsync(pc) {
4857
- const hasTransceivers = pc.getTransceivers().length > 0;
4858
- // Set up a transceiver for getting UE video
4859
- pc.addTransceiver('video', { direction: 'recvonly' });
4860
- // Set up a transceiver for sending mic audio to UE and receiving audio from UE
4861
- if (!this.useMic) {
4862
- pc.addTransceiver('audio', { direction: 'recvonly' });
4863
- }
4864
- else {
4865
- const audioSendOptions = this.useMic
4866
- ? {
4867
- autoGainControl: false,
4868
- channelCount: 1,
4869
- echoCancellation: false,
4870
- latency: 0,
4871
- noiseSuppression: false,
4872
- sampleRate: 48000,
4873
- sampleSize: 16,
4874
- volume: 1.0,
4875
- }
4876
- : false;
4877
- // Note using mic on android chrome requires SSL or chrome://flags/ "unsafely-treat-insecure-origin-as-secure"
4878
- const stream = await navigator.mediaDevices.getUserMedia({
4879
- video: false,
4880
- audio: audioSendOptions,
5188
+ evaluateAnomalies(fps, qp) {
5189
+ if (!this.session)
5190
+ return;
5191
+ const s = this.session;
5192
+ // Quality (QP red) entry/exit.
5193
+ const qpBadNow = qp.p50 > QP_RED_THRESHOLD;
5194
+ if (qpBadNow !== s.qpBad) {
5195
+ s.qpBad = qpBadNow;
5196
+ this.track(qpBadNow ? 'stream.quality_degraded' : 'stream.quality_recovered', {
5197
+ sessionId: s.id,
5198
+ seq: s.seq,
5199
+ timestamp: new Date().toISOString(),
5200
+ qpP50: qp.p50,
5201
+ qpP95: qp.p95,
4881
5202
  });
4882
- if (stream) {
4883
- if (hasTransceivers) {
4884
- for (const transceiver of pc.getTransceivers()) {
4885
- if (transceiver &&
4886
- transceiver.receiver &&
4887
- transceiver.receiver.track &&
4888
- transceiver.receiver.track.kind === 'audio') {
4889
- for (const track of stream.getTracks()) {
4890
- if (track.kind && track.kind === 'audio') {
4891
- await transceiver.sender.replaceTrack(track);
4892
- transceiver.direction = 'sendrecv';
4893
- }
4894
- }
4895
- }
4896
- }
4897
- }
4898
- else {
4899
- for (const track of stream.getTracks()) {
4900
- if (track.kind && track.kind === 'audio') {
4901
- pc.addTransceiver(track, { direction: 'sendrecv' });
4902
- }
4903
- }
4904
- }
4905
- }
4906
- else {
4907
- pc.addTransceiver('audio', { direction: 'recvonly' });
4908
- }
4909
5203
  }
4910
- }
4911
- setupDataChannelCallbacks(datachannel) {
4912
- try {
4913
- Logger.log(`[DATACHANNEL] Setting up datachannel "${datachannel.label}" callbacks...`);
4914
- this.commandTelemetryService.trackStartCommand('EXT-DataChannel Connection');
4915
- // Inform browser we would like binary data as an ArrayBuffer (FF chooses Blob by default!)
4916
- datachannel.binaryType = 'arraybuffer';
4917
- fromEvent(datachannel, 'close')
4918
- .pipe(
4919
- // Only dispatch disconnect if not already reconnecting to avoid infinite loops
4920
- filter(() => !this.isReconnecting()), take(1))
4921
- .subscribe((e) => {
4922
- Logger.warn(`[DATACHANNEL] Data channel disconnected: ${datachannel.label}(${datachannel.id})`, e);
4923
- this.store.dispatch(disconnectStream({
4924
- reason: DisconnectReason.DataChannelClosed,
4925
- message: 'DataChannelClosed',
4926
- }));
4927
- });
4928
- fromEvent(datachannel, 'error')
4929
- .pipe(takeUntil(this.disconnect$))
4930
- .subscribe((e) => {
4931
- const message = `[DATACHANNEL] Data channel "${datachannel.label}" error. id:${datachannel.id}`;
4932
- Logger.error(message, e);
4933
- });
4934
- fromEvent(datachannel, 'open')
4935
- .pipe(tap$1(() => {
4936
- Logger.info(`[DATACHANNEL] Data channel "${datachannel.label}" connected. id:${datachannel.id}`);
4937
- }), takeUntil(this.disconnect$))
4938
- .subscribe(() => {
4939
- this.commandTelemetryService.trackStopCommand('EXT-DataChannel Connection');
4940
- this.onDataChannelConnected();
5204
+ // FPS low vs current target.
5205
+ const ref = s.maxFpsTarget > 0 ? s.maxFpsTarget : UNLIMITED_REFERENCE_FPS;
5206
+ const fpsLowNow = fps.p50 < ref * FPS_LOW_RATIO;
5207
+ if (fpsLowNow && !s.fpsLow) {
5208
+ this.track('stream.fps_low', {
5209
+ sessionId: s.id,
5210
+ seq: s.seq,
5211
+ timestamp: new Date().toISOString(),
5212
+ target: s.maxFpsTarget,
5213
+ referenceTarget: ref,
5214
+ fpsP50: fps.p50,
5215
+ fpsP95: fps.p95,
4941
5216
  });
4942
- fromEvent(datachannel, 'message')
4943
- .pipe(
4944
- /* tap((e: MessageEvent) => {
4945
- const view = new Uint8Array(e.data);
4946
- Logger.info(
4947
- `[DATACHANNEL] Message received on "${datachannel.label}" id=${datachannel.id}, byteCode=${view[0]}, size=${view.byteLength}`,
4948
- );
4949
- }),*/
4950
- map$1((e) => e.data), takeUntil(this.disconnect$))
4951
- .subscribe((data) => this.onDataChannelMessage$.next(data));
4952
- return datachannel;
4953
- }
4954
- catch (e) {
4955
- Logger.warn('[DATACHANNEL] Datachannel setup caused an exception: ', e);
4956
- return null;
4957
5217
  }
5218
+ s.fpsLow = fpsLowNow;
4958
5219
  }
4959
- onDataChannelConnected() {
4960
- let statusMessage = 'Session connected, video stream starting.';
4961
- if (navigator.userAgent.includes('Firefox')) {
4962
- statusMessage +=
4963
- '\n If you are experiencing connection problems please try Google Chrome';
4964
- }
4965
- this.store.dispatch(dataChannelConnected({ statusMessage }));
5220
+ updateMaxFpsTarget(maxFps) {
5221
+ if (this.session)
5222
+ this.session.maxFpsTarget = maxFps;
4966
5223
  }
4967
5224
  /**
4968
- * Creates an SDP offer, applies SDP munging, sets it as a local description,
4969
- * and sends it to the server via signaling.
4970
- * Transceivers already configure media directions, so deprecated
4971
- * offerToReceive* options are not used.
5225
+ * Snapshot every milestone recorded in this session that belongs to
5226
+ * {@link phase}. Used to attach a phase's milestone array to the
5227
+ * lifecycle event that closes that phase.
4972
5228
  */
4973
- async createOffer(pcClient) {
5229
+ /**
5230
+ * Collect wire-shape milestones that fired within a bucket. Walks the
5231
+ * catalog's own milestone list (minus the closer) and pulls each
5232
+ * entry's time from the in-session map via object identity.
5233
+ */
5234
+ milestonesOfBucket(bucket) {
5235
+ if (!this.session)
5236
+ return [];
5237
+ const out = [];
5238
+ const closerIdx = bucket.milestones.length - 1;
5239
+ for (let i = 0; i < closerIdx; i++) {
5240
+ const entry = bucket.milestones[i];
5241
+ const time = this.session.times.get(entry);
5242
+ if (time !== undefined)
5243
+ out.push({ label: entry.label, time });
5244
+ }
5245
+ return out;
5246
+ }
5247
+ /**
5248
+ * Capture browser navigation timings — useful to explain the
5249
+ * "application bootstrap → session_started" waterfall phase.
5250
+ * Values are ms from navigation start; null if the entry is unavailable
5251
+ * (old browsers, cross-origin iframes with limited perf access, etc.).
5252
+ */
5253
+ collectBootstrapInfo() {
4974
5254
  try {
4975
- const offer = await pcClient.createOffer();
4976
- offer.sdp = this.mungeSDP(offer.sdp ?? '');
4977
- await pcClient.setLocalDescription(offer);
4978
- this.signallingSrv.send(offer);
5255
+ const nav = performance.getEntriesByType('navigation')[0];
5256
+ if (!nav)
5257
+ return null;
5258
+ return {
5259
+ type: nav.type, // 'navigate' | 'reload' | 'back_forward' | 'prerender'
5260
+ protocol: nav.nextHopProtocol || null, // 'h2' / 'h3' / 'http/1.1'
5261
+ firstByteMs: Math.round(nav.responseStart),
5262
+ domInteractiveMs: Math.round(nav.domInteractive),
5263
+ domContentLoadedMs: Math.round(nav.domContentLoadedEventEnd),
5264
+ loadEventMs: Math.round(nav.loadEventEnd),
5265
+ // Navigation-only size — the HTML document itself. Usually small;
5266
+ // the interesting number is `resources.transferSize` below.
5267
+ navigationTransferSize: nav.transferSize || null,
5268
+ navigationEncodedBodySize: nav.encodedBodySize || null,
5269
+ navigationDecodedBodySize: nav.decodedBodySize || null,
5270
+ resources: this.collectResourceTotals(),
5271
+ };
4979
5272
  }
4980
5273
  catch {
4981
- Logger.error("Couldn't create offer");
5274
+ return null;
4982
5275
  }
4983
5276
  }
4984
5277
  /**
4985
- * Creates an SDP answer, applies SDP munging, sets it as a local description,
4986
- * and sends it to the server via signaling.
5278
+ * Aggregate all resources the browser has loaded so far
5279
+ * (`performance.getEntriesByType('resource')`), grouped by `initiatorType`.
5280
+ * Gives a real "bytes downloaded before session start" figure — unlike the
5281
+ * navigation entry which only covers the HTML document.
5282
+ *
5283
+ * `transferSize === 0 && decodedBodySize > 0` means the resource came from
5284
+ * the HTTP cache — counted separately so cached hits don't inflate
5285
+ * the "bytes on the wire" metric.
4987
5286
  */
4988
- async createAnswer(pcClient) {
5287
+ collectResourceTotals() {
4989
5288
  try {
4990
- const answer = await pcClient.createAnswer();
4991
- answer.sdp = this.mungeSDP(answer.sdp ?? '');
4992
- await pcClient.setLocalDescription(answer);
4993
- this.signallingSrv.send(answer);
5289
+ const list = performance.getEntriesByType('resource');
5290
+ if (!list.length)
5291
+ return null;
5292
+ const empty = () => ({
5293
+ count: 0,
5294
+ cachedCount: 0,
5295
+ transferSize: 0,
5296
+ encodedBodySize: 0,
5297
+ decodedBodySize: 0,
5298
+ });
5299
+ const totals = empty();
5300
+ const byType = {};
5301
+ for (const r of list) {
5302
+ const type = r.initiatorType || 'other';
5303
+ const b = (byType[type] ||= empty());
5304
+ const fromCache = r.transferSize === 0 && r.decodedBodySize > 0;
5305
+ const t = r.transferSize || 0;
5306
+ const enc = r.encodedBodySize || 0;
5307
+ const dec = r.decodedBodySize || 0;
5308
+ b.count++;
5309
+ if (fromCache)
5310
+ b.cachedCount++;
5311
+ b.transferSize += t;
5312
+ b.encodedBodySize += enc;
5313
+ b.decodedBodySize += dec;
5314
+ totals.count++;
5315
+ if (fromCache)
5316
+ totals.cachedCount++;
5317
+ totals.transferSize += t;
5318
+ totals.encodedBodySize += enc;
5319
+ totals.decodedBodySize += dec;
5320
+ }
5321
+ return { ...totals, byType };
4994
5322
  }
4995
- catch (e) {
4996
- Logger.error("Couldn't create answer", e);
5323
+ catch {
5324
+ return null;
4997
5325
  }
4998
5326
  }
4999
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: WebRtcPlayerService2, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
5000
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: WebRtcPlayerService2 }); }
5327
+ aggregate(values) {
5328
+ if (values.length === 0)
5329
+ return { p50: 0, p95: 0, min: 0, max: 0 };
5330
+ const sorted = [...values].sort((a, b) => a - b);
5331
+ // Nearest-rank method: p-th percentile ≡ sorted[ceil(p/100 * n) - 1].
5332
+ // Using `floor` would make p95 collapse to `max` for n=20
5333
+ // (floor(0.95*20) = 19 = max index), hiding the max line behind p95
5334
+ // in charts. `ceil - 1` keeps them distinct: p95 = sorted[18], max = sorted[19].
5335
+ const pick = (p) => {
5336
+ const idx = Math.ceil((p / 100) * sorted.length) - 1;
5337
+ return sorted[Math.max(0, Math.min(sorted.length - 1, idx))];
5338
+ };
5339
+ return {
5340
+ p50: pick(50),
5341
+ p95: pick(95),
5342
+ min: sorted[0],
5343
+ max: sorted[sorted.length - 1],
5344
+ };
5345
+ }
5346
+ track(event, data) {
5347
+ // `userId` is attached to every event so the server can bucket files
5348
+ // under `data/<userId>/<sessionId>.jsonl` without maintaining a
5349
+ // session→user index.
5350
+ this.store.dispatch(trackTelemetryData({
5351
+ event,
5352
+ data: { userId: this.userId, ...data },
5353
+ }));
5354
+ }
5355
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: StreamStatusTelemetryService2, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
5356
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: StreamStatusTelemetryService2 }); }
5001
5357
  }
5002
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: WebRtcPlayerService2, decorators: [{
5358
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: StreamStatusTelemetryService2, decorators: [{
5003
5359
  type: Injectable
5004
5360
  }], ctorParameters: () => [] });
5361
+ // ──────────────────────────────────────────────────────────────────
5362
+ // Module-scope helpers — keep the class body focused on RxJS wiring.
5363
+ // ──────────────────────────────────────────────────────────────────
5364
+ /** Compact one side of an ICE candidate pair to `"type (url)"` or just
5365
+ * `type` (host candidates have no URL). */
5366
+ function fmtCandidate(c) {
5367
+ if (!c)
5368
+ return null;
5369
+ return c.url ? `${c.type} (${c.url})` : c.type;
5370
+ }
5371
+ /**
5372
+ * Flatten the closer's emitted extras object into display key/value
5373
+ * pairs. Null/undefined/empty are dropped; objects and arrays are
5374
+ * skipped — they should be pre-formatted by the closer's `map(...)`.
5375
+ */
5376
+ function toPhaseParams(value) {
5377
+ if (!value || typeof value !== 'object')
5378
+ return [];
5379
+ const out = [];
5380
+ for (const [key, v] of Object.entries(value)) {
5381
+ if (v === null || v === undefined || v === '')
5382
+ continue;
5383
+ if (typeof v === 'object')
5384
+ continue;
5385
+ out.push({ key, value: String(v) });
5386
+ }
5387
+ return out;
5388
+ }
5389
+ /**
5390
+ * Resolve declarative overlay recipes to absolute ms spans. Endpoints
5391
+ * fall back to the phase's own start/end when unspecified; recipes
5392
+ * whose referenced entry never fired are quietly skipped.
5393
+ */
5394
+ function resolveOverlays(recipes, times, phaseStartMs, phaseEndMs) {
5395
+ if (!recipes?.length)
5396
+ return [];
5397
+ const out = [];
5398
+ for (const r of recipes) {
5399
+ const fromMs = r.from ? (times.get(r.from) ?? null) : phaseStartMs;
5400
+ const toMs = r.to ? (times.get(r.to) ?? null) : phaseEndMs;
5401
+ if (fromMs === null || toMs === null || toMs <= fromMs)
5402
+ continue;
5403
+ out.push({ fromMs, toMs, title: r.title });
5404
+ }
5405
+ return out;
5406
+ }
5005
5407
 
5006
5408
  const ReceivedMimeTypes = {
5007
5409
  ApplicationJson: 'application/json',
@@ -5035,10 +5437,10 @@ class FileHandlerService {
5035
5437
  setTimeout(() => sender(out), 0);
5036
5438
  return observable;
5037
5439
  }
5038
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: FileHandlerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
5039
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: FileHandlerService }); }
5440
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: FileHandlerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
5441
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: FileHandlerService }); }
5040
5442
  }
5041
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: FileHandlerService, decorators: [{
5443
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: FileHandlerService, decorators: [{
5042
5444
  type: Injectable
5043
5445
  }] });
5044
5446
 
@@ -5127,10 +5529,10 @@ class AnalyticsService extends SubService {
5127
5529
  });
5128
5530
  });
5129
5531
  }
5130
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: AnalyticsService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
5131
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: AnalyticsService }); }
5532
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AnalyticsService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
5533
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AnalyticsService }); }
5132
5534
  }
5133
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: AnalyticsService, decorators: [{
5535
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AnalyticsService, decorators: [{
5134
5536
  type: Injectable
5135
5537
  }], ctorParameters: () => [] });
5136
5538
 
@@ -5497,10 +5899,10 @@ class FpsMonitorService {
5497
5899
  }
5498
5900
  return { fps: fps / len, bitrate: bitrate / len, qp: qp / len };
5499
5901
  }
5500
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: FpsMonitorService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
5501
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: FpsMonitorService }); }
5902
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: FpsMonitorService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
5903
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: FpsMonitorService }); }
5502
5904
  }
5503
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: FpsMonitorService, decorators: [{
5905
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: FpsMonitorService, decorators: [{
5504
5906
  type: Injectable
5505
5907
  }], ctorParameters: () => [] });
5506
5908
 
@@ -5571,10 +5973,10 @@ class UnrealCallbackService {
5571
5973
  setTimeout(() => sender({ ...data, correlationId }), 0);
5572
5974
  return observable;
5573
5975
  }
5574
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: UnrealCallbackService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
5575
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: UnrealCallbackService }); }
5976
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: UnrealCallbackService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
5977
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: UnrealCallbackService }); }
5576
5978
  }
5577
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: UnrealCallbackService, decorators: [{
5979
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: UnrealCallbackService, decorators: [{
5578
5980
  type: Injectable
5579
5981
  }] });
5580
5982
 
@@ -5590,10 +5992,10 @@ class AfkTimeoutModalComponent {
5590
5992
  resetAfk() {
5591
5993
  this.#afkService.reset();
5592
5994
  }
5593
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: AfkTimeoutModalComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
5594
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.8", type: AfkTimeoutModalComponent, isStandalone: true, selector: "app-afk-timeout-modal", ngImport: i0, template: "@if (isAfkTimerVisible()) {\r\n <div (click)=\"resetAfk()\" class=\"clickableState\" id=\"videoPlayOverlay\">\r\n <div class=\"resume-box\">\r\n <div aria-hidden=\"true\" class=\"resume-box__pic\" role=\"presentation\">\r\n <div [innerHTML]=\"afkCountdown()\" class=\"text-number\"></div>\r\n </div>\r\n <div class=\"resume-box__text\">\r\n <h3 class=\"resume-box__heading\">Session will time out soon</h3>\r\n <p>\r\n No activity detected. Press 'Continue' if you wish to keep your\r\n session active\r\n </p>\r\n </div>\r\n <src-button\r\n [isFullWidth]=\"true\"\r\n [data-testid]=\"'continue-session'\"\r\n weight=\"primary\"\r\n size=\"lg\"\r\n >\r\n Continue\r\n </src-button>\r\n </div>\r\n </div>\r\n}\r\n", styles: ["#videoPlayOverlay{position:absolute;z-index:30;top:0;width:100%;height:100%;font-size:1.8em;font-family:var(--src-font-family-body);background-color:#646464b3}.clickableState{display:flex;justify-content:center;align-items:center;cursor:pointer}.textDisplayState{display:flex}.hiddenState{display:none}.resume-box{width:340px;padding:32px 20px 20px;flex-direction:column;align-items:center;border-radius:var(--src-border-rounded-parent, 8px);background:var(--src-surface-container-main, #fff);box-shadow:0 26px 80px #0003,0 0 1px #0003}.resume-box .resume-box__pic{width:72px;height:72px;margin:0 auto 22px;border-radius:48px;background:#ecf0f2;padding:12px;display:flex;align-items:center;justify-content:center}.resume-box .resume-box__pic .text-number{color:var(--src-colors-text-default, #1f2937);text-align:center;font-family:var(--src-font-family-body);font-size:30px;font-style:normal;font-weight:400;line-height:24px}.resume-box__text{margin-bottom:18px}.resume-box__text p{text-align:center;font-family:var(--src-font-family-body);font-size:var(--src-font-size-sm, 14px);font-style:normal;font-weight:400;line-height:24px;color:var(--src-text-body-secondary, #6b7280)}.resume-box__text .resume-box__heading{color:var(--src-text-body-main, #1f2937);text-align:center;font-family:var(--src-font-family-body);font-size:18px;font-style:normal;font-weight:500;line-height:26px;margin-bottom:8px}\n"], dependencies: [{ kind: "component", type: SourceButtonComponent, selector: "src-button", inputs: ["type", "appearance", "weight", "size", "context", "customClass", "isFullWidth", "isPressed", "isDisabled", "isLoading", "isInverted", "srcButtonConfig", "formID", "data-testid", "ariaLabel"], outputs: ["onClick", "onSubmit"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
5995
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AfkTimeoutModalComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
5996
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: AfkTimeoutModalComponent, isStandalone: true, selector: "app-afk-timeout-modal", ngImport: i0, template: "@if (isAfkTimerVisible()) {\r\n <div (click)=\"resetAfk()\" class=\"clickableState\" id=\"videoPlayOverlay\">\r\n <div class=\"resume-box\">\r\n <div aria-hidden=\"true\" class=\"resume-box__pic\" role=\"presentation\">\r\n <div [innerHTML]=\"afkCountdown()\" class=\"text-number\"></div>\r\n </div>\r\n <div class=\"resume-box__text\">\r\n <h3 class=\"resume-box__heading\">Session will time out soon</h3>\r\n <p>\r\n No activity detected. Press 'Continue' if you wish to keep your\r\n session active\r\n </p>\r\n </div>\r\n <src-button\r\n [isFullWidth]=\"true\"\r\n [data-testid]=\"'continue-session'\"\r\n weight=\"primary\"\r\n size=\"lg\"\r\n >\r\n Continue\r\n </src-button>\r\n </div>\r\n </div>\r\n}\r\n", styles: ["#videoPlayOverlay{position:absolute;z-index:30;top:0;width:100%;height:100%;font-size:1.8em;font-family:var(--src-font-family-body);background-color:#646464b3}.clickableState{display:flex;justify-content:center;align-items:center;cursor:pointer}.textDisplayState{display:flex}.hiddenState{display:none}.resume-box{width:340px;padding:32px 20px 20px;flex-direction:column;align-items:center;border-radius:var(--src-border-rounded-parent, 8px);background:var(--src-surface-container-main, #fff);box-shadow:0 26px 80px #0003,0 0 1px #0003}.resume-box .resume-box__pic{width:72px;height:72px;margin:0 auto 22px;border-radius:48px;background:#ecf0f2;padding:12px;display:flex;align-items:center;justify-content:center}.resume-box .resume-box__pic .text-number{color:var(--src-colors-text-default, #1f2937);text-align:center;font-family:var(--src-font-family-body);font-size:30px;font-style:normal;font-weight:400;line-height:24px}.resume-box__text{margin-bottom:18px}.resume-box__text p{text-align:center;font-family:var(--src-font-family-body);font-size:var(--src-font-size-sm, 14px);font-style:normal;font-weight:400;line-height:24px;color:var(--src-text-body-secondary, #6b7280)}.resume-box__text .resume-box__heading{color:var(--src-text-body-main, #1f2937);text-align:center;font-family:var(--src-font-family-body);font-size:18px;font-style:normal;font-weight:500;line-height:26px;margin-bottom:8px}\n"], dependencies: [{ kind: "component", type: SourceButtonComponent, selector: "src-button", inputs: ["type", "appearance", "weight", "size", "context", "customClass", "isFullWidth", "isPressed", "isDisabled", "isLoading", "isInverted", "srcButtonConfig", "formID", "data-testid", "ariaLabel"], outputs: ["onClick", "onSubmit"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
5595
5997
  }
5596
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: AfkTimeoutModalComponent, decorators: [{
5998
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AfkTimeoutModalComponent, decorators: [{
5597
5999
  type: Component,
5598
6000
  args: [{ selector: 'app-afk-timeout-modal', changeDetection: ChangeDetectionStrategy.OnPush, imports: [SourceButtonComponent], template: "@if (isAfkTimerVisible()) {\r\n <div (click)=\"resetAfk()\" class=\"clickableState\" id=\"videoPlayOverlay\">\r\n <div class=\"resume-box\">\r\n <div aria-hidden=\"true\" class=\"resume-box__pic\" role=\"presentation\">\r\n <div [innerHTML]=\"afkCountdown()\" class=\"text-number\"></div>\r\n </div>\r\n <div class=\"resume-box__text\">\r\n <h3 class=\"resume-box__heading\">Session will time out soon</h3>\r\n <p>\r\n No activity detected. Press 'Continue' if you wish to keep your\r\n session active\r\n </p>\r\n </div>\r\n <src-button\r\n [isFullWidth]=\"true\"\r\n [data-testid]=\"'continue-session'\"\r\n weight=\"primary\"\r\n size=\"lg\"\r\n >\r\n Continue\r\n </src-button>\r\n </div>\r\n </div>\r\n}\r\n", styles: ["#videoPlayOverlay{position:absolute;z-index:30;top:0;width:100%;height:100%;font-size:1.8em;font-family:var(--src-font-family-body);background-color:#646464b3}.clickableState{display:flex;justify-content:center;align-items:center;cursor:pointer}.textDisplayState{display:flex}.hiddenState{display:none}.resume-box{width:340px;padding:32px 20px 20px;flex-direction:column;align-items:center;border-radius:var(--src-border-rounded-parent, 8px);background:var(--src-surface-container-main, #fff);box-shadow:0 26px 80px #0003,0 0 1px #0003}.resume-box .resume-box__pic{width:72px;height:72px;margin:0 auto 22px;border-radius:48px;background:#ecf0f2;padding:12px;display:flex;align-items:center;justify-content:center}.resume-box .resume-box__pic .text-number{color:var(--src-colors-text-default, #1f2937);text-align:center;font-family:var(--src-font-family-body);font-size:30px;font-style:normal;font-weight:400;line-height:24px}.resume-box__text{margin-bottom:18px}.resume-box__text p{text-align:center;font-family:var(--src-font-family-body);font-size:var(--src-font-size-sm, 14px);font-style:normal;font-weight:400;line-height:24px;color:var(--src-text-body-secondary, #6b7280)}.resume-box__text .resume-box__heading{color:var(--src-text-body-main, #1f2937);text-align:center;font-family:var(--src-font-family-body);font-size:18px;font-style:normal;font-weight:500;line-height:26px;margin-bottom:8px}\n"] }]
5599
6001
  }] });
@@ -5605,10 +6007,10 @@ class FreezeFrameComponent {
5605
6007
  this.combinedFreeze = this.#store.selectSignal(selectFreezeFrameCombinedDataUrl);
5606
6008
  }
5607
6009
  #store;
5608
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: FreezeFrameComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
5609
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.8", type: FreezeFrameComponent, isStandalone: true, selector: "app-freeze-frame", ngImport: i0, template: "@let image = combinedFreeze();\r\n@let progress = freezeFrameProgressMessageFromVideo();\r\n\r\n<div class=\"freeze-images\">\r\n @if (image) {\r\n <img [src]=\"image\" class=\"videoStillImage\" alt=\"freezeFrameImage\" />\r\n }\r\n\r\n @if (progress) {\r\n <div class=\"progress-status\">\r\n {{ progress }}\r\n </div>\r\n }\r\n</div>\r\n", styles: [".freeze-images{pointer-events:none;position:absolute;left:0;top:0;width:100%;height:100%;z-index:1}.freeze-images .freezeFrameOverlay{top:0;left:0;width:100%;height:100%;position:absolute;object-fit:cover;object-position:center}.freeze-images .videoStillImage{position:absolute;z-index:1;top:0;left:0;width:100%;height:100%;object-fit:cover}.freeze-images .progress-status{position:absolute;top:50%;left:50%;z-index:1;display:flex;flex-direction:column;justify-content:center;align-items:center;width:auto;height:auto;margin:0;padding:10px;background-color:#ffffffb3;border:1px solid grey;transform:translate(-50%,-50%)}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
6010
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: FreezeFrameComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
6011
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: FreezeFrameComponent, isStandalone: true, selector: "app-freeze-frame", ngImport: i0, template: "@let image = combinedFreeze();\r\n@let progress = freezeFrameProgressMessageFromVideo();\r\n\r\n<div class=\"freeze-images\">\r\n @if (image) {\r\n <img [src]=\"image\" class=\"videoStillImage\" alt=\"freezeFrameImage\" />\r\n }\r\n\r\n @if (progress) {\r\n <div class=\"progress-status\">\r\n {{ progress }}\r\n </div>\r\n }\r\n</div>\r\n", styles: [".freeze-images{pointer-events:none;position:absolute;left:0;top:0;width:100%;height:100%;z-index:1}.freeze-images .freezeFrameOverlay{top:0;left:0;width:100%;height:100%;position:absolute;object-fit:cover;object-position:center}.freeze-images .videoStillImage{position:absolute;z-index:1;top:0;left:0;width:100%;height:100%;object-fit:cover}.freeze-images .progress-status{position:absolute;top:50%;left:50%;z-index:1;display:flex;flex-direction:column;justify-content:center;align-items:center;width:auto;height:auto;margin:0;padding:10px;background-color:#ffffffb3;border:1px solid grey;transform:translate(-50%,-50%)}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
5610
6012
  }
5611
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: FreezeFrameComponent, decorators: [{
6013
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: FreezeFrameComponent, decorators: [{
5612
6014
  type: Component,
5613
6015
  args: [{ selector: 'app-freeze-frame', changeDetection: ChangeDetectionStrategy.OnPush, template: "@let image = combinedFreeze();\r\n@let progress = freezeFrameProgressMessageFromVideo();\r\n\r\n<div class=\"freeze-images\">\r\n @if (image) {\r\n <img [src]=\"image\" class=\"videoStillImage\" alt=\"freezeFrameImage\" />\r\n }\r\n\r\n @if (progress) {\r\n <div class=\"progress-status\">\r\n {{ progress }}\r\n </div>\r\n }\r\n</div>\r\n", styles: [".freeze-images{pointer-events:none;position:absolute;left:0;top:0;width:100%;height:100%;z-index:1}.freeze-images .freezeFrameOverlay{top:0;left:0;width:100%;height:100%;position:absolute;object-fit:cover;object-position:center}.freeze-images .videoStillImage{position:absolute;z-index:1;top:0;left:0;width:100%;height:100%;object-fit:cover}.freeze-images .progress-status{position:absolute;top:50%;left:50%;z-index:1;display:flex;flex-direction:column;justify-content:center;align-items:center;width:auto;height:auto;margin:0;padding:10px;background-color:#ffffffb3;border:1px solid grey;transform:translate(-50%,-50%)}\n"] }]
5614
6016
  }] });
@@ -5636,10 +6038,10 @@ class SafePipe {
5636
6038
  throw new Error(`Invalid safe type specified: ${type}`);
5637
6039
  }
5638
6040
  }
5639
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: SafePipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
5640
- static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.2.8", ngImport: i0, type: SafePipe, isStandalone: true, name: "safe" }); }
6041
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: SafePipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
6042
+ static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.2.9", ngImport: i0, type: SafePipe, isStandalone: true, name: "safe" }); }
5641
6043
  }
5642
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: SafePipe, decorators: [{
6044
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: SafePipe, decorators: [{
5643
6045
  type: Pipe,
5644
6046
  args: [{
5645
6047
  name: 'safe',
@@ -5651,10 +6053,10 @@ class LowBandwidthModalComponent {
5651
6053
  close(value) {
5652
6054
  this.#dialogRef.close(value || false);
5653
6055
  }
5654
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: LowBandwidthModalComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
5655
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.8", type: LowBandwidthModalComponent, isStandalone: true, selector: "app-low-bandwidth-modal", ngImport: i0, template: "<div [attr.data-testid]=\"'low-bandwidth'\" class=\"src-modal src-modal--lbm\">\r\n <header class=\"src-modal__header\">\r\n <h6 [attr.data-testid]=\"'title'\" class=\"src-modal__title\">\r\n Unstable Connection\r\n </h6>\r\n </header>\r\n <section class=\"src-modal__body\">\r\n <div\r\n [innerHtml]=\"\r\n 'Fluid Interactivity Modes were disabled due to an unstable connection. Showcase Gallery Mode is enabled. To regain full functionality, switch to Interactive Mode.'\r\n | safe: 'html'\r\n \"\r\n class=\"src-modal__scroll-box\"\r\n ></div>\r\n </section>\r\n <footer class=\"src-modal__footer\">\r\n <div class=\"src-modal__buttons\">\r\n <src-button\r\n (onClick)=\"close(true)\"\r\n [data-testid]=\"'switch-to-interactive-mode'\"\r\n >\r\n Switch\r\n </src-button>\r\n\r\n <src-button\r\n (onClick)=\"close()\"\r\n [data-testid]=\"'close-lbm-modal'\"\r\n weight=\"primary\"\r\n >\r\n Ok\r\n </src-button>\r\n </div>\r\n </footer>\r\n</div>\r\n", styles: [".src-modal--lbm{width:360px}.src-modal--lbm .src-modal__body{font-size:14px;line-height:24px}.src-modal--lbm .src-modal__buttons{display:flex;gap:8px}\n"], dependencies: [{ kind: "component", type: SourceButtonComponent, selector: "src-button", inputs: ["type", "appearance", "weight", "size", "context", "customClass", "isFullWidth", "isPressed", "isDisabled", "isLoading", "isInverted", "srcButtonConfig", "formID", "data-testid", "ariaLabel"], outputs: ["onClick", "onSubmit"] }, { kind: "pipe", type: SafePipe, name: "safe" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
6056
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: LowBandwidthModalComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
6057
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.9", type: LowBandwidthModalComponent, isStandalone: true, selector: "app-low-bandwidth-modal", ngImport: i0, template: "<div [attr.data-testid]=\"'low-bandwidth'\" class=\"src-modal src-modal--lbm\">\r\n <header class=\"src-modal__header\">\r\n <h6 [attr.data-testid]=\"'title'\" class=\"src-modal__title\">\r\n Unstable Connection\r\n </h6>\r\n </header>\r\n <section class=\"src-modal__body\">\r\n <div\r\n [innerHtml]=\"\r\n 'Fluid Interactivity Modes were disabled due to an unstable connection. Showcase Gallery Mode is enabled. To regain full functionality, switch to Interactive Mode.'\r\n | safe: 'html'\r\n \"\r\n class=\"src-modal__scroll-box\"\r\n ></div>\r\n </section>\r\n <footer class=\"src-modal__footer\">\r\n <div class=\"src-modal__buttons\">\r\n <src-button\r\n (onClick)=\"close(true)\"\r\n [data-testid]=\"'switch-to-interactive-mode'\"\r\n >\r\n Switch\r\n </src-button>\r\n\r\n <src-button\r\n (onClick)=\"close()\"\r\n [data-testid]=\"'close-lbm-modal'\"\r\n weight=\"primary\"\r\n >\r\n Ok\r\n </src-button>\r\n </div>\r\n </footer>\r\n</div>\r\n", styles: [".src-modal--lbm{width:360px}.src-modal--lbm .src-modal__body{font-size:14px;line-height:24px}.src-modal--lbm .src-modal__buttons{display:flex;gap:8px}\n"], dependencies: [{ kind: "component", type: SourceButtonComponent, selector: "src-button", inputs: ["type", "appearance", "weight", "size", "context", "customClass", "isFullWidth", "isPressed", "isDisabled", "isLoading", "isInverted", "srcButtonConfig", "formID", "data-testid", "ariaLabel"], outputs: ["onClick", "onSubmit"] }, { kind: "pipe", type: SafePipe, name: "safe" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
5656
6058
  }
5657
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: LowBandwidthModalComponent, decorators: [{
6059
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: LowBandwidthModalComponent, decorators: [{
5658
6060
  type: Component,
5659
6061
  args: [{ selector: 'app-low-bandwidth-modal', changeDetection: ChangeDetectionStrategy.OnPush, imports: [SafePipe, SourceButtonComponent], template: "<div [attr.data-testid]=\"'low-bandwidth'\" class=\"src-modal src-modal--lbm\">\r\n <header class=\"src-modal__header\">\r\n <h6 [attr.data-testid]=\"'title'\" class=\"src-modal__title\">\r\n Unstable Connection\r\n </h6>\r\n </header>\r\n <section class=\"src-modal__body\">\r\n <div\r\n [innerHtml]=\"\r\n 'Fluid Interactivity Modes were disabled due to an unstable connection. Showcase Gallery Mode is enabled. To regain full functionality, switch to Interactive Mode.'\r\n | safe: 'html'\r\n \"\r\n class=\"src-modal__scroll-box\"\r\n ></div>\r\n </section>\r\n <footer class=\"src-modal__footer\">\r\n <div class=\"src-modal__buttons\">\r\n <src-button\r\n (onClick)=\"close(true)\"\r\n [data-testid]=\"'switch-to-interactive-mode'\"\r\n >\r\n Switch\r\n </src-button>\r\n\r\n <src-button\r\n (onClick)=\"close()\"\r\n [data-testid]=\"'close-lbm-modal'\"\r\n weight=\"primary\"\r\n >\r\n Ok\r\n </src-button>\r\n </div>\r\n </footer>\r\n</div>\r\n", styles: [".src-modal--lbm{width:360px}.src-modal--lbm .src-modal__body{font-size:14px;line-height:24px}.src-modal--lbm .src-modal__buttons{display:flex;gap:8px}\n"] }]
5660
6062
  }] });
@@ -5666,10 +6068,10 @@ class ImageLoadingSrcComponent {
5666
6068
  this.imageLoadingSrc = this.#store.selectSignal(unrealFeature.selectImageLoadingSrc);
5667
6069
  }
5668
6070
  #store;
5669
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: ImageLoadingSrcComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
5670
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.8", type: ImageLoadingSrcComponent, isStandalone: true, selector: "app-image-loading-src", ngImport: i0, template: "@let image = imageLoadingSrc();\r\n@if (isLoaderScreenVisible()) {\r\n <div class=\"preload\">\r\n <div class=\"back\">\r\n @if (image) {\r\n <img [ngSrc]=\"image\" fill loading=\"lazy\" alt=\"image loading src\" />\r\n }\r\n </div>\r\n </div>\r\n}\r\n", styles: [".preload{position:absolute;top:0;left:0;width:100%;height:100%;text-align:center}.preload .back{position:absolute;top:0;left:0;display:flex;justify-content:center;align-items:center;width:100%;height:100%}.preload .back img{width:100%;height:100%;object-fit:cover}.preload .back{background-color:#fff}\n"], dependencies: [{ kind: "directive", type: NgOptimizedImage, selector: "img[ngSrc]", inputs: ["ngSrc", "ngSrcset", "sizes", "width", "height", "decoding", "loading", "priority", "loaderParams", "disableOptimizedSrcset", "fill", "placeholder", "placeholderConfig", "src", "srcset"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
6071
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: ImageLoadingSrcComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
6072
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: ImageLoadingSrcComponent, isStandalone: true, selector: "app-image-loading-src", ngImport: i0, template: "@let image = imageLoadingSrc();\r\n@if (isLoaderScreenVisible()) {\r\n <div class=\"preload\">\r\n <div class=\"back\">\r\n @if (image) {\r\n <img [ngSrc]=\"image\" fill loading=\"lazy\" alt=\"image loading src\" />\r\n }\r\n </div>\r\n </div>\r\n}\r\n", styles: [".preload{position:absolute;top:0;left:0;width:100%;height:100%;text-align:center}.preload .back{position:absolute;top:0;left:0;display:flex;justify-content:center;align-items:center;width:100%;height:100%}.preload .back img{width:100%;height:100%;object-fit:cover}.preload .back{background-color:#fff}\n"], dependencies: [{ kind: "directive", type: NgOptimizedImage, selector: "img[ngSrc]", inputs: ["ngSrc", "ngSrcset", "sizes", "width", "height", "decoding", "loading", "priority", "loaderParams", "disableOptimizedSrcset", "fill", "placeholder", "placeholderConfig", "src", "srcset"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
5671
6073
  }
5672
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: ImageLoadingSrcComponent, decorators: [{
6074
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: ImageLoadingSrcComponent, decorators: [{
5673
6075
  type: Component,
5674
6076
  args: [{ selector: 'app-image-loading-src', changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgOptimizedImage], template: "@let image = imageLoadingSrc();\r\n@if (isLoaderScreenVisible()) {\r\n <div class=\"preload\">\r\n <div class=\"back\">\r\n @if (image) {\r\n <img [ngSrc]=\"image\" fill loading=\"lazy\" alt=\"image loading src\" />\r\n }\r\n </div>\r\n </div>\r\n}\r\n", styles: [".preload{position:absolute;top:0;left:0;width:100%;height:100%;text-align:center}.preload .back{position:absolute;top:0;left:0;display:flex;justify-content:center;align-items:center;width:100%;height:100%}.preload .back img{width:100%;height:100%;object-fit:cover}.preload .back{background-color:#fff}\n"] }]
5675
6077
  }] });
@@ -5745,10 +6147,10 @@ class StatGraphComponent {
5745
6147
  }
5746
6148
  this.draw();
5747
6149
  }
5748
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: StatGraphComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
5749
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "21.2.8", type: StatGraphComponent, isStandalone: true, selector: "app-stat-graph", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, stat: { classPropertyName: "stat", publicName: "stat", isSignal: true, isRequired: false, transformFunction: null }, color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: false, transformFunction: null }, tickStep: { classPropertyName: "tickStep", publicName: "tickStep", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "window:resize": "resize()" } }, viewQueries: [{ propertyName: "graph", first: true, predicate: ["graph"], descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"content\">\r\n <p>{{ label() }}: {{ current() }} | Min:{{ min() }} | Max:{{ max() }}</p>\r\n <canvas #graph class=\"graph\"></canvas>\r\n</div>\r\n", styles: [".content{width:100%;height:100%}.content p{color:#fff;margin:5px 0 0;height:25px}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
6150
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: StatGraphComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
6151
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "21.2.9", type: StatGraphComponent, isStandalone: true, selector: "app-stat-graph", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, stat: { classPropertyName: "stat", publicName: "stat", isSignal: true, isRequired: false, transformFunction: null }, color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: false, transformFunction: null }, tickStep: { classPropertyName: "tickStep", publicName: "tickStep", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "window:resize": "resize()" } }, viewQueries: [{ propertyName: "graph", first: true, predicate: ["graph"], descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"content\">\r\n <p>{{ label() }}: {{ current() }} | Min:{{ min() }} | Max:{{ max() }}</p>\r\n <canvas #graph class=\"graph\"></canvas>\r\n</div>\r\n", styles: [".content{width:100%;height:100%}.content p{color:#fff;margin:5px 0 0;height:25px}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
5750
6152
  }
5751
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: StatGraphComponent, decorators: [{
6153
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: StatGraphComponent, decorators: [{
5752
6154
  type: Component,
5753
6155
  args: [{ selector: 'app-stat-graph', changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"content\">\r\n <p>{{ label() }}: {{ current() }} | Min:{{ min() }} | Max:{{ max() }}</p>\r\n <canvas #graph class=\"graph\"></canvas>\r\n</div>\r\n", styles: [".content{width:100%;height:100%}.content p{color:#fff;margin:5px 0 0;height:25px}\n"] }]
5754
6156
  }], ctorParameters: () => [], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], stat: [{ type: i0.Input, args: [{ isSignal: true, alias: "stat", required: false }] }], color: [{ type: i0.Input, args: [{ isSignal: true, alias: "color", required: false }] }], tickStep: [{ type: i0.Input, args: [{ isSignal: true, alias: "tickStep", required: false }] }], graph: [{ type: i0.ViewChild, args: ['graph', { isSignal: true }] }], resize: [{
@@ -5829,10 +6231,10 @@ class UnrealStatusComponent {
5829
6231
  // Fails silently if the iframe's content window cannot be accessed
5830
6232
  }
5831
6233
  }
5832
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: UnrealStatusComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
5833
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.8", type: UnrealStatusComponent, isStandalone: true, selector: "app-unreal-status", ngImport: i0, template: "@if (showLoader()) {\r\n <div class=\"status-box\">\r\n @if (awsInstance()?.message) {\r\n <p>{{ awsInstance()?.message }}</p>\r\n } @else {\r\n <div class=\"status-box__message\">\r\n <src-loading\r\n [size]=\"30\"\r\n [backgroundStrokeColor]=\"'#E5E7EB'\"\r\n [progressStrokeColor]=\"'#1F2937'\"\r\n />\r\n {{ messagePercents() }}%\r\n </div>\r\n }\r\n </div>\r\n}\r\n", styles: ["app-stat-graph{height:30px;width:100%}.status-box{position:absolute;top:80%;left:50%;transform:translate(-50%,-50%);z-index:5;display:flex;justify-content:center;align-items:center;width:auto;max-width:500px;margin:0;background:var(--src-surface-container-main, #fff);border:none;border-radius:var(--src-border-rounded-full, 9999px);box-shadow:0 8px 20px #1718181f,0 3px 6px #17181814;pointer-events:none}.status-box p{padding:10px 15px;margin:0;text-align:center}.status-box__message{display:flex;align-items:center;padding:12px 20px 12px 16px;color:var(--src-text-body-main, #1f2937);font-size:16px;font-weight:400;line-height:24px}.status-box__message .status-box__message_text{display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden}.status-box__message src-loading{margin-right:12px}\n"], dependencies: [{ kind: "component", type: SourceLoadingComponent, selector: "src-loading", inputs: ["size", "progress", "lineCap", "backgroundStrokeColor", "progressStrokeColor", "strokeWidth", "data-testid", "role", "ariaLabel", "ariaValueNow", "ariaValueMin", "ariaValueMax"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
6234
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: UnrealStatusComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
6235
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: UnrealStatusComponent, isStandalone: true, selector: "app-unreal-status", ngImport: i0, template: "@if (showLoader()) {\r\n <div class=\"status-box\">\r\n @if (awsInstance()?.message) {\r\n <p>{{ awsInstance()?.message }}</p>\r\n } @else {\r\n <div class=\"status-box__message\">\r\n <src-loading\r\n [size]=\"30\"\r\n [backgroundStrokeColor]=\"'#E5E7EB'\"\r\n [progressStrokeColor]=\"'#1F2937'\"\r\n />\r\n {{ messagePercents() }}%\r\n </div>\r\n }\r\n </div>\r\n}\r\n", styles: ["app-stat-graph{height:30px;width:100%}.status-box{position:absolute;top:80%;left:50%;transform:translate(-50%,-50%);z-index:5;display:flex;justify-content:center;align-items:center;width:auto;max-width:500px;margin:0;background:var(--src-surface-container-main, #fff);border:none;border-radius:var(--src-border-rounded-full, 9999px);box-shadow:0 8px 20px #1718181f,0 3px 6px #17181814;pointer-events:none}.status-box p{padding:10px 15px;margin:0;text-align:center}.status-box__message{display:flex;align-items:center;padding:12px 20px 12px 16px;color:var(--src-text-body-main, #1f2937);font-size:16px;font-weight:400;line-height:24px}.status-box__message .status-box__message_text{display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden}.status-box__message src-loading{margin-right:12px}\n"], dependencies: [{ kind: "component", type: SourceLoadingComponent, selector: "src-loading", inputs: ["size", "progress", "lineCap", "backgroundStrokeColor", "progressStrokeColor", "strokeWidth", "data-testid", "role", "ariaLabel", "ariaValueNow", "ariaValueMin", "ariaValueMax"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
5834
6236
  }
5835
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: UnrealStatusComponent, decorators: [{
6237
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: UnrealStatusComponent, decorators: [{
5836
6238
  type: Component,
5837
6239
  args: [{ selector: 'app-unreal-status', changeDetection: ChangeDetectionStrategy.OnPush, imports: [SourceLoadingComponent], template: "@if (showLoader()) {\r\n <div class=\"status-box\">\r\n @if (awsInstance()?.message) {\r\n <p>{{ awsInstance()?.message }}</p>\r\n } @else {\r\n <div class=\"status-box__message\">\r\n <src-loading\r\n [size]=\"30\"\r\n [backgroundStrokeColor]=\"'#E5E7EB'\"\r\n [progressStrokeColor]=\"'#1F2937'\"\r\n />\r\n {{ messagePercents() }}%\r\n </div>\r\n }\r\n </div>\r\n}\r\n", styles: ["app-stat-graph{height:30px;width:100%}.status-box{position:absolute;top:80%;left:50%;transform:translate(-50%,-50%);z-index:5;display:flex;justify-content:center;align-items:center;width:auto;max-width:500px;margin:0;background:var(--src-surface-container-main, #fff);border:none;border-radius:var(--src-border-rounded-full, 9999px);box-shadow:0 8px 20px #1718181f,0 3px 6px #17181814;pointer-events:none}.status-box p{padding:10px 15px;margin:0;text-align:center}.status-box__message{display:flex;align-items:center;padding:12px 20px 12px 16px;color:var(--src-text-body-main, #1f2937);font-size:16px;font-weight:400;line-height:24px}.status-box__message .status-box__message_text{display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden}.status-box__message src-loading{margin-right:12px}\n"] }]
5838
6240
  }] });
@@ -5961,10 +6363,10 @@ class VideoStatsComponent {
5961
6363
  elements: this.elementsToShow,
5962
6364
  }));
5963
6365
  }
5964
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: VideoStatsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
5965
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.8", type: VideoStatsComponent, isStandalone: true, selector: "app-video-stats", ngImport: i0, template: "@if (videoStatus() && viewportReady()) {\r\n <div class=\"settings-container\">\r\n <button (click)=\"toggle()\" class=\"gear-button\">Stats</button>\r\n <div (click)=\"toggle()\" [class.min]=\"collapsed()\" id=\"aggregatedStats\">\r\n <div class=\"forNerds\">\r\n <div (click)=\"$event.stopPropagation()\" class=\"static\">\r\n <li>\r\n <app-stat-graph\r\n [stat]=\"fpsTick()!\"\r\n label=\"FPS (higher is better)\"\r\n />\r\n </li>\r\n <li>\r\n <app-stat-graph\r\n [stat]=\"videoQP()!\"\r\n label=\"QP (lower is better)\"\r\n color=\"#D5ff07\"\r\n />\r\n </li>\r\n <li>\r\n <app-stat-graph\r\n [stat]=\"bitrateTick()!\"\r\n label=\"Bitrate\"\r\n color=\"#D57F07\"\r\n />\r\n </li>\r\n </div>\r\n <div>\r\n @for (graph of graphList$ | async; track graph) {\r\n <li (click)=\"toggleGraph($event, graph.key, 0)\" class=\"graph\">\r\n <app-stat-graph\r\n [label]=\"graph.key\"\r\n [stat]=\"(graph.stat | async)!\"\r\n [color]=\"graph.color\"\r\n />\r\n </li>\r\n }\r\n </div>\r\n </div>\r\n\r\n @for (el of videoStatus(); track el.key + $index) {\r\n <div (click)=\"toggleGraph($event, el.key, el.value)\" class=\"stat\">\r\n <span>{{ el.key }}: </span>{{ el.value | json }}\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n @if (!collapsed()) {\r\n <div [innerHTML]=\"ssInfo() | safe: 'html'\" class=\"ssInfo\"></div>\r\n }\r\n}\r\n", styles: [".settings-container{position:absolute;top:65px;left:10px;z-index:1000;bottom:10px}.gear-button{left:0;width:50px;position:absolute;background:none;cursor:pointer;font-size:10px;font-weight:500;border:1px solid;border-radius:9px;background:#0162cc;color:#fff;padding:4px}.gear-button>*{color:#fff;filter:drop-shadow(1px 1px 1px rgb(0,0,0))}#aggregatedStats{max-height:calc(100% - 25px);overflow-y:auto;margin-top:25px;left:20px;z-index:31;max-width:400px;padding:10px;background-color:#fff3;border:1px solid grey}#aggregatedStats>*{color:#fff;text-shadow:-1px -1px 0 #000,1px -1px 0 #000,-1px 1px 0 #000,1px 1px 0 #000}#aggregatedStats:empty{display:none}#aggregatedStats button{position:absolute;right:5px;top:5px}#aggregatedStats.min{display:none}#aggregatedStats .forNerds{width:100%}#aggregatedStats .forNerds li{list-style-type:none;height:60px}#aggregatedStats .forNerds li app-stat-graph{display:block;width:100%;height:100%}#aggregatedStats .stat span{font-weight:700}#aggregatedStats .stat,#aggregatedStats .graph{cursor:pointer}#aggregatedStats .static{pointer-events:all}.ssInfo{position:absolute;top:0;left:50%;transform:translate(-50%);color:#fff;text-shadow:-1px -1px 0 #000,1px -1px 0 #000,-1px 1px 0 #000,1px 1px 0 #000;text-align:center;width:80%;pointer-events:none}\n"], dependencies: [{ kind: "component", type: StatGraphComponent, selector: "app-stat-graph", inputs: ["label", "stat", "color", "tickStep"] }, { kind: "pipe", type: AsyncPipe, name: "async" }, { kind: "pipe", type: JsonPipe, name: "json" }, { kind: "pipe", type: SafePipe, name: "safe" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
6366
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: VideoStatsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
6367
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: VideoStatsComponent, isStandalone: true, selector: "app-video-stats", ngImport: i0, template: "@if (videoStatus() && viewportReady()) {\r\n <div class=\"settings-container\">\r\n <button (click)=\"toggle()\" class=\"gear-button\">Stats</button>\r\n <div (click)=\"toggle()\" [class.min]=\"collapsed()\" id=\"aggregatedStats\">\r\n <div class=\"forNerds\">\r\n <div (click)=\"$event.stopPropagation()\" class=\"static\">\r\n <li>\r\n <app-stat-graph\r\n [stat]=\"fpsTick()!\"\r\n label=\"FPS (higher is better)\"\r\n />\r\n </li>\r\n <li>\r\n <app-stat-graph\r\n [stat]=\"videoQP()!\"\r\n label=\"QP (lower is better)\"\r\n color=\"#D5ff07\"\r\n />\r\n </li>\r\n <li>\r\n <app-stat-graph\r\n [stat]=\"bitrateTick()!\"\r\n label=\"Bitrate\"\r\n color=\"#D57F07\"\r\n />\r\n </li>\r\n </div>\r\n <div>\r\n @for (graph of graphList$ | async; track graph) {\r\n <li (click)=\"toggleGraph($event, graph.key, 0)\" class=\"graph\">\r\n <app-stat-graph\r\n [label]=\"graph.key\"\r\n [stat]=\"(graph.stat | async)!\"\r\n [color]=\"graph.color\"\r\n />\r\n </li>\r\n }\r\n </div>\r\n </div>\r\n\r\n @for (el of videoStatus(); track el.key + $index) {\r\n <div (click)=\"toggleGraph($event, el.key, el.value)\" class=\"stat\">\r\n <span>{{ el.key }}: </span>{{ el.value | json }}\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n @if (!collapsed()) {\r\n <div [innerHTML]=\"ssInfo() | safe: 'html'\" class=\"ssInfo\"></div>\r\n }\r\n}\r\n", styles: [".settings-container{position:absolute;top:65px;left:10px;z-index:1000;bottom:10px}.gear-button{left:0;width:50px;position:absolute;background:none;cursor:pointer;font-size:10px;font-weight:500;border:1px solid;border-radius:9px;background:#0162cc;color:#fff;padding:4px}.gear-button>*{color:#fff;filter:drop-shadow(1px 1px 1px rgb(0,0,0))}#aggregatedStats{max-height:calc(100% - 25px);overflow-y:auto;margin-top:25px;left:20px;z-index:31;max-width:400px;padding:10px;background-color:#fff3;border:1px solid grey}#aggregatedStats>*{color:#fff;text-shadow:-1px -1px 0 #000,1px -1px 0 #000,-1px 1px 0 #000,1px 1px 0 #000}#aggregatedStats:empty{display:none}#aggregatedStats button{position:absolute;right:5px;top:5px}#aggregatedStats.min{display:none}#aggregatedStats .forNerds{width:100%}#aggregatedStats .forNerds li{list-style-type:none;height:60px}#aggregatedStats .forNerds li app-stat-graph{display:block;width:100%;height:100%}#aggregatedStats .stat span{font-weight:700}#aggregatedStats .stat,#aggregatedStats .graph{cursor:pointer}#aggregatedStats .static{pointer-events:all}.ssInfo{position:absolute;top:0;left:50%;transform:translate(-50%);color:#fff;text-shadow:-1px -1px 0 #000,1px -1px 0 #000,-1px 1px 0 #000,1px 1px 0 #000;text-align:center;width:80%;pointer-events:none}\n"], dependencies: [{ kind: "component", type: StatGraphComponent, selector: "app-stat-graph", inputs: ["label", "stat", "color", "tickStep"] }, { kind: "pipe", type: AsyncPipe, name: "async" }, { kind: "pipe", type: JsonPipe, name: "json" }, { kind: "pipe", type: SafePipe, name: "safe" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
5966
6368
  }
5967
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: VideoStatsComponent, decorators: [{
6369
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: VideoStatsComponent, decorators: [{
5968
6370
  type: Component,
5969
6371
  args: [{ selector: 'app-video-stats', changeDetection: ChangeDetectionStrategy.OnPush, imports: [StatGraphComponent, AsyncPipe, JsonPipe, SafePipe], template: "@if (videoStatus() && viewportReady()) {\r\n <div class=\"settings-container\">\r\n <button (click)=\"toggle()\" class=\"gear-button\">Stats</button>\r\n <div (click)=\"toggle()\" [class.min]=\"collapsed()\" id=\"aggregatedStats\">\r\n <div class=\"forNerds\">\r\n <div (click)=\"$event.stopPropagation()\" class=\"static\">\r\n <li>\r\n <app-stat-graph\r\n [stat]=\"fpsTick()!\"\r\n label=\"FPS (higher is better)\"\r\n />\r\n </li>\r\n <li>\r\n <app-stat-graph\r\n [stat]=\"videoQP()!\"\r\n label=\"QP (lower is better)\"\r\n color=\"#D5ff07\"\r\n />\r\n </li>\r\n <li>\r\n <app-stat-graph\r\n [stat]=\"bitrateTick()!\"\r\n label=\"Bitrate\"\r\n color=\"#D57F07\"\r\n />\r\n </li>\r\n </div>\r\n <div>\r\n @for (graph of graphList$ | async; track graph) {\r\n <li (click)=\"toggleGraph($event, graph.key, 0)\" class=\"graph\">\r\n <app-stat-graph\r\n [label]=\"graph.key\"\r\n [stat]=\"(graph.stat | async)!\"\r\n [color]=\"graph.color\"\r\n />\r\n </li>\r\n }\r\n </div>\r\n </div>\r\n\r\n @for (el of videoStatus(); track el.key + $index) {\r\n <div (click)=\"toggleGraph($event, el.key, el.value)\" class=\"stat\">\r\n <span>{{ el.key }}: </span>{{ el.value | json }}\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n @if (!collapsed()) {\r\n <div [innerHTML]=\"ssInfo() | safe: 'html'\" class=\"ssInfo\"></div>\r\n }\r\n}\r\n", styles: [".settings-container{position:absolute;top:65px;left:10px;z-index:1000;bottom:10px}.gear-button{left:0;width:50px;position:absolute;background:none;cursor:pointer;font-size:10px;font-weight:500;border:1px solid;border-radius:9px;background:#0162cc;color:#fff;padding:4px}.gear-button>*{color:#fff;filter:drop-shadow(1px 1px 1px rgb(0,0,0))}#aggregatedStats{max-height:calc(100% - 25px);overflow-y:auto;margin-top:25px;left:20px;z-index:31;max-width:400px;padding:10px;background-color:#fff3;border:1px solid grey}#aggregatedStats>*{color:#fff;text-shadow:-1px -1px 0 #000,1px -1px 0 #000,-1px 1px 0 #000,1px 1px 0 #000}#aggregatedStats:empty{display:none}#aggregatedStats button{position:absolute;right:5px;top:5px}#aggregatedStats.min{display:none}#aggregatedStats .forNerds{width:100%}#aggregatedStats .forNerds li{list-style-type:none;height:60px}#aggregatedStats .forNerds li app-stat-graph{display:block;width:100%;height:100%}#aggregatedStats .stat span{font-weight:700}#aggregatedStats .stat,#aggregatedStats .graph{cursor:pointer}#aggregatedStats .static{pointer-events:all}.ssInfo{position:absolute;top:0;left:50%;transform:translate(-50%);color:#fff;text-shadow:-1px -1px 0 #000,1px -1px 0 #000,-1px 1px 0 #000,1px 1px 0 #000;text-align:center;width:80%;pointer-events:none}\n"] }]
5970
6372
  }], ctorParameters: () => [] });
@@ -6008,10 +6410,10 @@ class ResumeStreamButtonComponent {
6008
6410
  onDisconnect() {
6009
6411
  this.isSecondStart.set(true);
6010
6412
  }
6011
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: ResumeStreamButtonComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
6012
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.8", type: ResumeStreamButtonComponent, isStandalone: true, selector: "app-resume-stream-button", ngImport: i0, template: "@if (isShowResumeButton()) {\r\n <div class=\"resume-box\">\r\n @if (isSecondStart()) {\r\n <div class=\"resume-box__text\">\r\n Your stream has been paused due to inactivity\r\n </div>\r\n }\r\n\r\n <src-button\r\n (onClick)=\"connect()\"\r\n [isFullWidth]=\"true\"\r\n [data-testid]=\"'connect-button'\"\r\n size=\"lg\"\r\n weight=\"primary\"\r\n class=\"connect-button\"\r\n >\r\n {{ isSecondStart() ? 'Resume' : 'Start' }}\r\n </src-button>\r\n </div>\r\n}\r\n", styles: [".resume-box{padding:16px;position:absolute;bottom:0;left:50%;gap:12px;z-index:6;display:flex;transform:translate(-50%,-50%);flex-direction:column;align-items:center;border-radius:var(--src-border-rounded-parent, 8px);background:var(--src-surface-container-main, #fff);box-shadow:0 26px 80px #0003,0 0 1px #0003}.resume-box .connect-button{width:100%}.resume-box .resume-box__text{color:var(--src-color-grey-500, #6b7280);text-align:center;font-family:var(--src-font-family-body);font-size:14px;font-style:normal;font-weight:400;line-height:24px}\n"], dependencies: [{ kind: "component", type: SourceButtonComponent, selector: "src-button", inputs: ["type", "appearance", "weight", "size", "context", "customClass", "isFullWidth", "isPressed", "isDisabled", "isLoading", "isInverted", "srcButtonConfig", "formID", "data-testid", "ariaLabel"], outputs: ["onClick", "onSubmit"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
6413
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: ResumeStreamButtonComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
6414
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: ResumeStreamButtonComponent, isStandalone: true, selector: "app-resume-stream-button", ngImport: i0, template: "@if (isShowResumeButton()) {\r\n <div class=\"resume-box\">\r\n @if (isSecondStart()) {\r\n <div class=\"resume-box__text\">\r\n Your stream has been paused due to inactivity\r\n </div>\r\n }\r\n\r\n <src-button\r\n (onClick)=\"connect()\"\r\n [isFullWidth]=\"true\"\r\n [data-testid]=\"'connect-button'\"\r\n size=\"lg\"\r\n weight=\"primary\"\r\n class=\"connect-button\"\r\n >\r\n {{ isSecondStart() ? 'Resume' : 'Start' }}\r\n </src-button>\r\n </div>\r\n}\r\n", styles: [".resume-box{padding:16px;position:absolute;bottom:0;left:50%;gap:12px;z-index:6;display:flex;transform:translate(-50%,-50%);flex-direction:column;align-items:center;border-radius:var(--src-border-rounded-parent, 8px);background:var(--src-surface-container-main, #fff);box-shadow:0 26px 80px #0003,0 0 1px #0003}.resume-box .connect-button{width:100%}.resume-box .resume-box__text{color:var(--src-color-grey-500, #6b7280);text-align:center;font-family:var(--src-font-family-body);font-size:14px;font-style:normal;font-weight:400;line-height:24px}\n"], dependencies: [{ kind: "component", type: SourceButtonComponent, selector: "src-button", inputs: ["type", "appearance", "weight", "size", "context", "customClass", "isFullWidth", "isPressed", "isDisabled", "isLoading", "isInverted", "srcButtonConfig", "formID", "data-testid", "ariaLabel"], outputs: ["onClick", "onSubmit"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
6013
6415
  }
6014
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: ResumeStreamButtonComponent, decorators: [{
6416
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: ResumeStreamButtonComponent, decorators: [{
6015
6417
  type: Component,
6016
6418
  args: [{ selector: 'app-resume-stream-button', changeDetection: ChangeDetectionStrategy.OnPush, imports: [SourceButtonComponent], template: "@if (isShowResumeButton()) {\r\n <div class=\"resume-box\">\r\n @if (isSecondStart()) {\r\n <div class=\"resume-box__text\">\r\n Your stream has been paused due to inactivity\r\n </div>\r\n }\r\n\r\n <src-button\r\n (onClick)=\"connect()\"\r\n [isFullWidth]=\"true\"\r\n [data-testid]=\"'connect-button'\"\r\n size=\"lg\"\r\n weight=\"primary\"\r\n class=\"connect-button\"\r\n >\r\n {{ isSecondStart() ? 'Resume' : 'Start' }}\r\n </src-button>\r\n </div>\r\n}\r\n", styles: [".resume-box{padding:16px;position:absolute;bottom:0;left:50%;gap:12px;z-index:6;display:flex;transform:translate(-50%,-50%);flex-direction:column;align-items:center;border-radius:var(--src-border-rounded-parent, 8px);background:var(--src-surface-container-main, #fff);box-shadow:0 26px 80px #0003,0 0 1px #0003}.resume-box .connect-button{width:100%}.resume-box .resume-box__text{color:var(--src-color-grey-500, #6b7280);text-align:center;font-family:var(--src-font-family-body);font-size:14px;font-style:normal;font-weight:400;line-height:24px}\n"] }]
6017
6419
  }] });
@@ -6070,10 +6472,10 @@ class IntroSrcComponent {
6070
6472
  videoElement.innerHTML += 'Browser not supported';
6071
6473
  videoContainer.appendChild(videoElement);
6072
6474
  }
6073
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: IntroSrcComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
6074
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.8", type: IntroSrcComponent, isStandalone: true, selector: "app-intro-src", viewQueries: [{ propertyName: "videoElement", first: true, predicate: ["videoPlayerCover"], descendants: true, isSignal: true }], ngImport: i0, template: "@let image = imageIntroSrc();\r\n@if (image) {\r\n <div class=\"backImage\">\r\n <img [ngSrc]=\"image\" fill alt=\"imageSrc\" />\r\n </div>\r\n} @else if (videoIntroSrc()) {\r\n <div #videoPlayerCover class=\"backVideo\"></div>\r\n}\r\n", styles: [".backVideo{position:absolute;top:0;left:0;display:flex;justify-content:center;align-items:center;width:100%;height:100%;z-index:1}.backVideo video{position:absolute;width:100%;height:100%;object-fit:cover}.backImage{position:absolute;top:0;left:0;display:flex;justify-content:center;align-items:center;width:100%;height:100%;z-index:1}.backImage img{position:absolute;width:100%;height:100%;object-fit:cover}\n"], dependencies: [{ kind: "directive", type: NgOptimizedImage, selector: "img[ngSrc]", inputs: ["ngSrc", "ngSrcset", "sizes", "width", "height", "decoding", "loading", "priority", "loaderParams", "disableOptimizedSrcset", "fill", "placeholder", "placeholderConfig", "src", "srcset"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
6475
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: IntroSrcComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
6476
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: IntroSrcComponent, isStandalone: true, selector: "app-intro-src", viewQueries: [{ propertyName: "videoElement", first: true, predicate: ["videoPlayerCover"], descendants: true, isSignal: true }], ngImport: i0, template: "@let image = imageIntroSrc();\r\n@if (image) {\r\n <div class=\"backImage\">\r\n <img [ngSrc]=\"image\" fill alt=\"imageSrc\" />\r\n </div>\r\n} @else if (videoIntroSrc()) {\r\n <div #videoPlayerCover class=\"backVideo\"></div>\r\n}\r\n", styles: [".backVideo{position:absolute;top:0;left:0;display:flex;justify-content:center;align-items:center;width:100%;height:100%;z-index:1}.backVideo video{position:absolute;width:100%;height:100%;object-fit:cover}.backImage{position:absolute;top:0;left:0;display:flex;justify-content:center;align-items:center;width:100%;height:100%;z-index:1}.backImage img{position:absolute;width:100%;height:100%;object-fit:cover}\n"], dependencies: [{ kind: "directive", type: NgOptimizedImage, selector: "img[ngSrc]", inputs: ["ngSrc", "ngSrcset", "sizes", "width", "height", "decoding", "loading", "priority", "loaderParams", "disableOptimizedSrcset", "fill", "placeholder", "placeholderConfig", "src", "srcset"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
6075
6477
  }
6076
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: IntroSrcComponent, decorators: [{
6478
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: IntroSrcComponent, decorators: [{
6077
6479
  type: Component,
6078
6480
  args: [{ selector: 'app-intro-src', imports: [NgOptimizedImage], changeDetection: ChangeDetectionStrategy.OnPush, template: "@let image = imageIntroSrc();\r\n@if (image) {\r\n <div class=\"backImage\">\r\n <img [ngSrc]=\"image\" fill alt=\"imageSrc\" />\r\n </div>\r\n} @else if (videoIntroSrc()) {\r\n <div #videoPlayerCover class=\"backVideo\"></div>\r\n}\r\n", styles: [".backVideo{position:absolute;top:0;left:0;display:flex;justify-content:center;align-items:center;width:100%;height:100%;z-index:1}.backVideo video{position:absolute;width:100%;height:100%;object-fit:cover}.backImage{position:absolute;top:0;left:0;display:flex;justify-content:center;align-items:center;width:100%;height:100%;z-index:1}.backImage img{position:absolute;width:100%;height:100%;object-fit:cover}\n"] }]
6079
6481
  }], ctorParameters: () => [], propDecorators: { videoElement: [{ type: i0.ViewChild, args: ['videoPlayerCover', { isSignal: true }] }] } });
@@ -6096,7 +6498,6 @@ class UnrealSceneComponent {
6096
6498
  this.changeMouseOverScene.emit(false);
6097
6499
  }
6098
6500
  constructor() {
6099
- this.unrealConfig = inject(UNREAL_CONFIG);
6100
6501
  this.isStudio = input(false, ...(ngDevMode ? [{ debugName: "isStudio" }] : /* istanbul ignore next */ []));
6101
6502
  this.useContainerAsSizeProvider = input(true, ...(ngDevMode ? [{ debugName: "useContainerAsSizeProvider" }] : /* istanbul ignore next */ []));
6102
6503
  this.studioResolutionSize = input({
@@ -6199,16 +6600,7 @@ class UnrealSceneComponent {
6199
6600
  containerHeight = this.videoElement()?.nativeElement.offsetHeight || 2;
6200
6601
  }
6201
6602
  const pixelRatio = this.pixelRatio;
6202
- if (this.unrealConfig.optimizedResize) {
6203
- //New Approach
6204
- return getResizeValuesUniversal({
6205
- pixelRatio,
6206
- containerWidth,
6207
- containerHeight,
6208
- maxResolution: this.maxResolution,
6209
- });
6210
- }
6211
- return getResizeValuesUniversalOld({
6603
+ return getResizeValuesUniversal({
6212
6604
  pixelRatio,
6213
6605
  containerWidth,
6214
6606
  containerHeight,
@@ -6216,20 +6608,11 @@ class UnrealSceneComponent {
6216
6608
  });
6217
6609
  }
6218
6610
  getResizeStudioValues() {
6219
- if (this.unrealConfig.optimizedResize) {
6220
- return getResizeStudioValues({
6221
- pixelRatio: this.pixelRatio,
6222
- containerWidth: this.videoElement()?.nativeElement.offsetWidth || 2,
6223
- containerHeight: this.videoElement()?.nativeElement.offsetHeight || 2,
6224
- studioWidth: this.width,
6225
- studioHeight: this.height,
6226
- maxResolution: this.maxResolution,
6227
- });
6228
- }
6229
- return getResizeStudioValuesOld({
6611
+ const videoElement = this.videoElement()?.nativeElement;
6612
+ return getResizeStudioValues({
6230
6613
  pixelRatio: this.pixelRatio,
6231
- containerWidth: this.videoElement()?.nativeElement.offsetWidth || 2,
6232
- containerHeight: this.videoElement()?.nativeElement.offsetHeight || 2,
6614
+ containerWidth: videoElement?.offsetWidth || 2,
6615
+ containerHeight: videoElement?.offsetHeight || 2,
6233
6616
  studioWidth: this.width,
6234
6617
  studioHeight: this.height,
6235
6618
  maxResolution: this.maxResolution,
@@ -6238,13 +6621,18 @@ class UnrealSceneComponent {
6238
6621
  sendResize(size) {
6239
6622
  this.commandsSender.sendCommandToUnreal({
6240
6623
  command: MetaBoxCommand.FChangeResolutionCommand,
6241
- payload: { resolution: { x: size.w, y: size.h } },
6624
+ payload: {
6625
+ resolution: { x: size.w, y: size.h },
6626
+ payloadVersion: this.isStudio()
6627
+ ? EPayloadVersion.disableFovRecalculation
6628
+ : undefined,
6629
+ },
6242
6630
  });
6243
6631
  }
6244
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: UnrealSceneComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
6245
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.8", type: UnrealSceneComponent, isStandalone: true, selector: "app-unreal-scene", inputs: { isStudio: { classPropertyName: "isStudio", publicName: "isStudio", isSignal: true, isRequired: false, transformFunction: null }, useContainerAsSizeProvider: { classPropertyName: "useContainerAsSizeProvider", publicName: "useContainerAsSizeProvider", isSignal: true, isRequired: false, transformFunction: null }, studioResolutionSize: { classPropertyName: "studioResolutionSize", publicName: "studioResolutionSize", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { changeMouseOverScene: "changeMouseOverScene" }, host: { listeners: { "mouseover": "onMouseOver()", "mouseout": "onMouseOut()" } }, viewQueries: [{ propertyName: "videoElement", first: true, predicate: ["videoContainer"], descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"frame unreal-container\">\r\n <div #videoContainer [class.lightModeCursor]=\"lightMode()\" id=\"player\"></div>\r\n\r\n <app-intro-src />\r\n <app-image-loading-src />\r\n <app-resume-stream-button />\r\n <app-unreal-status />\r\n <app-freeze-frame />\r\n <app-afk-timeout-modal />\r\n\r\n <ng-content select=\"app-pdf\"></ng-content>\r\n <ng-content select=\"app-show-case\"></ng-content>\r\n <ng-content select=\"app-low-bandwidth-detector\"></ng-content>\r\n\r\n @if (isDevMode) {\r\n <app-video-stats />\r\n }\r\n</div>\r\n", styles: [".unreal-container{position:absolute;top:0;left:0;width:100%;height:100%;background-color:#fff}.unreal-container #player{position:absolute;width:100%;height:100%;display:flex;justify-content:center;align-items:center;overflow:hidden}.unreal-container.select{cursor:url('data:image/svg+xml,<svg width=\"24\" height=\"24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M15.15 21.375a1.423 1.423 0 0 1-1.15.063 1.487 1.487 0 0 1-.85-.788l-3-6.45-2.325 3.25c-.283.4-.658.525-1.125.375-.467-.15-.7-.466-.7-.95V4.05c0-.417.188-.717.563-.9.375-.183.729-.142 1.062.125l10.1 7.95c.383.284.496.65.337 1.1-.158.45-.479.675-.962.675h-4.2l2.975 6.375c.183.384.204.767.063 1.15a1.487 1.487 0 0 1-.788.85Z\" fill=\"%23000\"/><path d=\"m12.697 20.861.002.005c.237.495.617.852 1.128 1.04.515.191 1.039.16 1.539-.08a1.987 1.987 0 0 0 1.04-1.128 1.922 1.922 0 0 0-.079-1.536v-.003L13.684 13.5H17.1c.32 0 .628-.075.89-.26.263-.184.438-.447.544-.749.106-.3.135-.617.04-.925a1.458 1.458 0 0 0-.545-.738L7.936 2.883a1.51 1.51 0 0 0-.768-.336 1.471 1.471 0 0 0-.825.154 1.495 1.495 0 0 0-.626.547 1.492 1.492 0 0 0-.217.802v12.825c0 .324.08.634.272.897.193.261.467.43.775.53.31.099.632.12.942.016.31-.103.555-.313.743-.578h.001l1.825-2.552 2.639 5.673Z\" stroke=\"%23fff\" stroke-opacity=\".7\"/></svg>') 6 2,auto}.unreal-container.orbit{cursor:url('data:image/svg+xml,<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" fill=\"none\"><path fill=\"%23000\" d=\"M11.998 21.6c-1.966 0-3.737-.53-5.312-1.588-1.575-1.058-2.738-2.437-3.488-4.137a.852.852 0 0 1 .013-.713.867.867 0 0 1 .512-.487.86.86 0 0 1 .688.037c.225.109.387.28.487.513.617 1.35 1.559 2.45 2.825 3.3 1.267.85 2.692 1.275 4.275 1.275 1.384 0 2.646-.33 3.788-.988a7.985 7.985 0 0 0 2.762-2.612h-.85a.873.873 0 0 1-.64-.257.863.863 0 0 1-.26-.638.88.88 0 0 1 .26-.643.864.864 0 0 1 .64-.262h3c.255 0 .47.086.642.259a.87.87 0 0 1 .258.64v3a.874.874 0 0 1-.257.642.863.863 0 0 1-.637.259.88.88 0 0 1-.643-.259.865.865 0 0 1-.263-.641v-.725a9.95 9.95 0 0 1-3.35 2.925c-1.35.733-2.833 1.1-4.45 1.1Zm0-17.4c-1.383 0-2.645.33-3.787.987A7.985 7.985 0 0 0 5.448 7.8h.85c.255 0 .47.086.642.257a.863.863 0 0 1 .258.638.88.88 0 0 1-.258.642.864.864 0 0 1-.642.263h-3a.87.87 0 0 1-.64-.259.87.87 0 0 1-.26-.641v-3c0-.255.086-.469.258-.641a.863.863 0 0 1 .637-.26.88.88 0 0 1 .643.26.864.864 0 0 1 .262.64v.726A9.95 9.95 0 0 1 7.548 3.5c1.35-.733 2.834-1.1 4.45-1.1 2 0 3.788.542 5.363 1.625 1.575 1.083 2.736 2.49 3.482 4.22a.724.724 0 0 1-.045.646.926.926 0 0 1-.525.437.87.87 0 0 1-.687-.04.977.977 0 0 1-.488-.513c-.616-1.35-1.558-2.45-2.825-3.3-1.266-.85-2.691-1.275-4.275-1.275Zm0 10.8a2.893 2.893 0 0 1-2.125-.875A2.893 2.893 0 0 1 8.998 12c0-.833.292-1.542.875-2.125A2.893 2.893 0 0 1 11.998 9c.834 0 1.542.292 2.125.875.584.583.875 1.292.875 2.125s-.291 1.542-.875 2.125a2.893 2.893 0 0 1-2.125.875Z\"/><path stroke=\"%23fff\" stroke-opacity=\".7\" d=\"m2.739 16.072.002.005c.789 1.788 2.014 3.24 3.666 4.35 1.66 1.116 3.53 1.673 5.591 1.673 1.696 0 3.262-.386 4.69-1.16a10.54 10.54 0 0 0 2.714-2.097c.069.167.171.32.308.454.272.268.613.403.994.403.38 0 .722-.135.992-.406s.402-.614.402-.994v-3c0-.381-.134-.724-.405-.995a1.37 1.37 0 0 0-.995-.405h-3c-.383 0-.727.137-.997.411a1.38 1.38 0 0 0-.403.994c0 .38.135.723.407.992.238.237.532.369.858.397a7.552 7.552 0 0 1-2.027 1.685c-1.062.613-2.237.92-3.538.92-1.488 0-2.815-.397-3.996-1.19-1.19-.798-2.07-1.826-2.647-3.087-.147-.34-.393-.6-.727-.76a1.36 1.36 0 0 0-1.073-.058c-.37.133-.64.397-.8.754a1.351 1.351 0 0 0-.016 1.114Zm4.553-8.37a1.36 1.36 0 0 0-.858-.396A7.552 7.552 0 0 1 8.46 5.62c1.062-.613 2.237-.921 3.537-.921 1.489 0 2.816.398 3.997 1.19 1.19.8 2.07 1.827 2.646 3.088.148.34.394.6.728.76a1.37 1.37 0 0 0 1.068.063c.347-.12.62-.346.799-.668.187-.338.215-.71.072-1.072l-.003-.007-.003-.007c-.784-1.818-2.006-3.298-3.658-4.434C15.982 2.47 14.094 1.9 11.998 1.9c-1.695 0-3.261.385-4.688 1.16a10.536 10.536 0 0 0-2.715 2.096 1.364 1.364 0 0 0-.308-.453 1.38 1.38 0 0 0-.994-.403c-.38 0-.722.135-.992.406s-.403.614-.403.994v3c0 .38.135.724.406.995.27.27.614.405.994.405h3c.384 0 .727-.137.998-.412a1.38 1.38 0 0 0 .402-.993c0-.38-.135-.723-.406-.992ZM9.52 14.48a3.393 3.393 0 0 0 2.478 1.02c.965 0 1.8-.342 2.479-1.02a3.392 3.392 0 0 0 1.021-2.48c0-.963-.343-1.8-1.021-2.478A3.392 3.392 0 0 0 11.998 8.5c-.964 0-1.8.343-2.478 1.021A3.392 3.392 0 0 0 8.498 12c0 .964.344 1.8 1.022 2.479Z\"/></svg>') 12 12,auto}.unreal-container.pan{cursor:url('data:image/svg+xml,<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" fill=\"none\"><path fill=\"%23000\" d=\"M11.997 10.075a1.01 1.01 0 0 1-.738-.313 1.008 1.008 0 0 1-.312-.737V6.15l-.95.95a.998.998 0 0 1-.763.312 1.09 1.09 0 0 1-.763-.337 1.06 1.06 0 0 1-.324-.775c0-.3.108-.558.324-.775l2.776-2.775c.117-.116.237-.2.362-.25a1.04 1.04 0 0 1 .775 0c.125.05.246.134.363.25l2.8 2.8c.216.217.32.47.313.762a1.096 1.096 0 0 1-.338.763 1.058 1.058 0 0 1-.775.325c-.3 0-.558-.108-.775-.325l-.925-.925v2.875c0 .283-.104.53-.312.737a1.01 1.01 0 0 1-.738.313Zm0 11.5a1.04 1.04 0 0 1-.388-.075 1.103 1.103 0 0 1-.362-.25l-2.8-2.8a1.002 1.002 0 0 1-.313-.762c.009-.292.121-.546.337-.763.217-.217.475-.325.775-.325.3 0 .559.108.775.325l.926.925v-2.875c0-.283.104-.529.312-.737a1.01 1.01 0 0 1 .738-.313c.283 0 .529.104.738.313.208.208.312.454.312.737v2.875l.95-.95a.998.998 0 0 1 .762-.312c.292.008.546.12.763.337.216.217.324.475.324.775 0 .3-.108.559-.324.775l-2.775 2.775c-.117.116-.238.2-.363.25a1.04 1.04 0 0 1-.387.075Zm4.925-6.05a1.058 1.058 0 0 1-.326-.774c0-.3.109-.559.326-.776l.925-.925H14.97c-.283 0-.528-.104-.736-.312a1.01 1.01 0 0 1-.313-.738c0-.283.104-.53.313-.738.208-.208.453-.312.737-.312h2.875l-.95-.95a.998.998 0 0 1-.313-.762 1.09 1.09 0 0 1 .338-.764 1.06 1.06 0 0 1 .775-.324c.3 0 .558.108.775.324l2.774 2.776c.117.117.2.238.251.363a1.04 1.04 0 0 1 0 .775c-.05.124-.134.245-.25.362l-2.8 2.8c-.217.216-.471.32-.762.313a1.096 1.096 0 0 1-.763-.337Zm-11.4 0L2.746 12.75a1.103 1.103 0 0 1-.25-.362 1.04 1.04 0 0 1 0-.775c.05-.125.133-.246.25-.363l2.8-2.8c.216-.216.47-.32.762-.313.292.01.546.121.763.337.217.217.325.476.325.776 0 .3-.108.558-.325.775l-.925.925h2.875c.283 0 .529.104.737.312.208.209.313.455.313.738s-.105.53-.313.738a1.008 1.008 0 0 1-.737.312H6.147l.95.95c.217.217.32.471.312.762a1.09 1.09 0 0 1-.337.764 1.06 1.06 0 0 1-.775.324c-.3 0-.559-.108-.776-.324Z\"/><path stroke=\"%23fff\" stroke-opacity=\".7\" d=\"M9.22 7.912c.436.013.821-.15 1.13-.458l.097-.097v1.668c0 .422.16.793.458 1.09.298.299.67.46 1.092.46.422 0 .793-.161 1.091-.46.298-.297.459-.668.459-1.09V7.357l.071.072c.309.308.693.471 1.13.471.435 0 .82-.163 1.128-.471.301-.303.47-.676.483-1.102v-.001a1.501 1.501 0 0 0-.46-1.13l-2.799-2.8a1.6 1.6 0 0 0-.528-.36l-.003-.001a1.54 1.54 0 0 0-1.145 0l-.002.001a1.6 1.6 0 0 0-.529.36L8.118 5.172l-.001.001a1.56 1.56 0 0 0-.47 1.128c0 .435.163.82.47 1.128.303.303.676.472 1.103.484Zm0 0 .014-.5-.015.5Zm-1.311 6.865a1.497 1.497 0 0 0-.458-1.13l-.097-.097h1.668c.421 0 .793-.16 1.09-.458.298-.299.46-.67.46-1.092a1.51 1.51 0 0 0-.46-1.091 1.507 1.507 0 0 0-1.09-.459H7.354l.071-.072c.309-.308.472-.692.472-1.128 0-.436-.163-.82-.472-1.13a1.596 1.596 0 0 0-1.101-.482h-.002a1.501 1.501 0 0 0-1.129.459l-2.8 2.8a1.6 1.6 0 0 0-.36.528l-.001.003a1.54 1.54 0 0 0 0 1.144v.003c.081.2.208.375.362.529l2.774 2.775c.309.308.693.47 1.129.47.435 0 .82-.162 1.128-.47.303-.302.472-.676.484-1.102Zm0 0-.5-.014.5.015v-.001Zm3.038-8.627-.5.5.5.207V6.15Zm.475 15.814.002.001a1.54 1.54 0 0 0 1.145 0h.002c.2-.081.375-.208.53-.362l2.775-2.774a1.56 1.56 0 0 0 .47-1.129c0-.435-.162-.82-.47-1.128a1.589 1.589 0 0 0-1.103-.484 1.497 1.497 0 0 0-1.13.458l-.096.097v-1.668c0-.421-.161-.793-.459-1.09a1.509 1.509 0 0 0-1.091-.46c-.422 0-.794.161-1.092.46a1.507 1.507 0 0 0-.458 1.09v1.668l-.072-.072a1.558 1.558 0 0 0-1.129-.471c-.436 0-.82.163-1.128.471l-.001.001c-.301.302-.47.675-.483 1.101v.001c-.012.437.151.821.46 1.13l2.8 2.8c.153.153.328.28.528.36Zm5.146-6.085c.303.302.676.47 1.102.484a1.5 1.5 0 0 0 1.13-.46l2.8-2.8a1.6 1.6 0 0 0 .36-.528l.002-.003a1.54 1.54 0 0 0 0-1.144l-.001-.003a1.6 1.6 0 0 0-.361-.529l-2.774-2.775h-.001a1.56 1.56 0 0 0-1.128-.47c-.436 0-.82.162-1.128.47h-.001a1.59 1.59 0 0 0-.484 1.102c-.012.437.15.822.459 1.13l.097.097h-1.668c-.422 0-.793.16-1.09.459-.299.298-.46.67-.46 1.091 0 .422.161.793.46 1.092.297.297.668.458 1.09.458h1.668l-.072.072a1.558 1.558 0 0 0-.472 1.129c0 .435.164.82.472 1.128ZM6.147 13.05l.5.5.207-.5h-.707Z\"/></svg>') 12 12,auto}.unreal-container.dolly{cursor:url('data:image/svg+xml,<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" fill=\"none\"><mask id=\"a\" width=\"24\" height=\"24\" x=\"0\" y=\"0\" maskUnits=\"userSpaceOnUse\" style=\"mask-type:alpha\"><path fill=\"%23127392\" d=\"M0 0h24v24H0z\"/></mask><g mask=\"url(%23a)\"><path fill=\"%23000\" d=\"M12.002 20.575a1.026 1.026 0 0 1-.75-.325l-2.55-2.55a.933.933 0 0 1-.287-.713c.008-.275.113-.513.313-.713.2-.2.441-.3.725-.3.283 0 .524.1.724.3l.776.776V6.95l-.8.8c-.2.2-.438.296-.712.288a1.018 1.018 0 0 1-.713-.312c-.2-.2-.3-.442-.3-.726s.1-.525.3-.725l2.524-2.525a1.026 1.026 0 0 1 1.5 0l2.55 2.55c.2.2.296.438.289.713a1.023 1.023 0 0 1-.314.713c-.2.2-.441.3-.724.3a.988.988 0 0 1-.725-.3l-.775-.776v10.1l.8-.8c.2-.2.437-.296.712-.288.275.008.512.112.712.312.2.2.3.442.3.726s-.1.525-.3.725l-2.524 2.525a1.026 1.026 0 0 1-.75.325Z\"/><path stroke=\"%23fff\" stroke-opacity=\".7\" d=\"M13.553 8.153c.28.245.62.373 1 .373.415 0 .784-.153 1.078-.447.287-.287.446-.644.46-1.05v-.002a1.432 1.432 0 0 0-.435-1.08l-2.55-2.55-.354.353.354-.354a1.526 1.526 0 0 0-2.207 0L8.374 5.921A1.487 1.487 0 0 0 7.928 7c0 .416.152.785.446 1.08.288.287.645.446 1.052.457l4.127-.384Zm0 0v7.693m0-7.693a1.667 1.667 0 0 1-.079-.074l-3.021.075v7.693a1.482 1.482 0 0 0-1-.373c-.416 0-.785.153-1.079.447a1.522 1.522 0 0 0-.46 1.05v.002c-.011.417.14.786.435 1.08l2.55 2.55a1.527 1.527 0 0 0 2.207 0l2.525-2.524c.294-.294.446-.663.446-1.079 0-.416-.152-.785-.446-1.08a1.518 1.518 0 0 0-1.052-.457m0 0-.014.5.014-.5Zm0 0a1.424 1.424 0 0 0-1.027.383m0 0a1.583 1.583 0 0 0-.053.05l.354.354m-.3-.404v.104l.3.3m0 0-.3-.3v.6l.3-.3Z\"/></g></svg>%0D%0A') 12 12,auto}.unreal-container.disabled:before{content:\"\";position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px)}#playerUI{position:absolute;z-index:10;width:100%}#statsContainer{display:none;text-align:left;background-color:#000c}#stats{padding:6px;font-weight:700;font-size:14px;color:#0f0}canvas{position:absolute;image-rendering:crisp-edges}#overlay{position:absolute;top:0;right:2%;z-index:100;padding:4px;border-top-width:0;-webkit-border-bottom-right-radius:5px;-moz-border-radius-bottomright:5px;border-bottom-right-radius:5px;-webkit-border-bottom-left-radius:5px;-moz-border-radius-bottomleft:5px;border-bottom-left-radius:5px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-touch-callout:none}.overlay{font-weight:lighter;font-family:var(--src-font-family-body)}#overlayButton:hover{cursor:pointer}#overlayButton{font-size:40px;text-align:right}#overlaySettings{display:none;width:300px;padding:4px}#videoMessageOverlay{position:absolute;z-index:20;width:100%;margin:auto;font-size:1.8em;font-family:var(--src-font-family-body)}#playButton{display:inline-block;height:auto}img#playButton{width:10%;max-width:241px}#UIInteraction{position:fixed}#UIInteractionButtonBoundary{padding:2px}#UIInteractionButton{cursor:pointer}#hiddenInput{position:absolute;left:-10%;width:0;opacity:0}#editTextButton{position:absolute;width:40px;height:40px}\n"], dependencies: [{ kind: "component", type: AfkTimeoutModalComponent, selector: "app-afk-timeout-modal" }, { kind: "component", type: UnrealStatusComponent, selector: "app-unreal-status" }, { kind: "component", type: FreezeFrameComponent, selector: "app-freeze-frame" }, { kind: "component", type: VideoStatsComponent, selector: "app-video-stats" }, { kind: "component", type: ResumeStreamButtonComponent, selector: "app-resume-stream-button" }, { kind: "component", type: ImageLoadingSrcComponent, selector: "app-image-loading-src" }, { kind: "component", type: IntroSrcComponent, selector: "app-intro-src" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
6632
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: UnrealSceneComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
6633
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: UnrealSceneComponent, isStandalone: true, selector: "app-unreal-scene", inputs: { isStudio: { classPropertyName: "isStudio", publicName: "isStudio", isSignal: true, isRequired: false, transformFunction: null }, useContainerAsSizeProvider: { classPropertyName: "useContainerAsSizeProvider", publicName: "useContainerAsSizeProvider", isSignal: true, isRequired: false, transformFunction: null }, studioResolutionSize: { classPropertyName: "studioResolutionSize", publicName: "studioResolutionSize", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { changeMouseOverScene: "changeMouseOverScene" }, host: { listeners: { "mouseover": "onMouseOver()", "mouseout": "onMouseOut()" } }, viewQueries: [{ propertyName: "videoElement", first: true, predicate: ["videoContainer"], descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"frame unreal-container\">\r\n <div #videoContainer [class.lightModeCursor]=\"lightMode()\" id=\"player\"></div>\r\n\r\n <app-intro-src />\r\n <app-image-loading-src />\r\n <app-resume-stream-button />\r\n <app-unreal-status />\r\n <app-freeze-frame />\r\n <app-afk-timeout-modal />\r\n\r\n <ng-content select=\"app-pdf\"></ng-content>\r\n <ng-content select=\"app-show-case\"></ng-content>\r\n <ng-content select=\"app-low-bandwidth-detector\"></ng-content>\r\n\r\n @if (isDevMode) {\r\n <app-video-stats />\r\n }\r\n</div>\r\n", styles: [".unreal-container{position:absolute;top:0;left:0;width:100%;height:100%;background-color:#fff}.unreal-container #player{position:absolute;width:100%;height:100%;display:flex;justify-content:center;align-items:center;overflow:hidden}.unreal-container.select{cursor:url('data:image/svg+xml,<svg width=\"24\" height=\"24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M15.15 21.375a1.423 1.423 0 0 1-1.15.063 1.487 1.487 0 0 1-.85-.788l-3-6.45-2.325 3.25c-.283.4-.658.525-1.125.375-.467-.15-.7-.466-.7-.95V4.05c0-.417.188-.717.563-.9.375-.183.729-.142 1.062.125l10.1 7.95c.383.284.496.65.337 1.1-.158.45-.479.675-.962.675h-4.2l2.975 6.375c.183.384.204.767.063 1.15a1.487 1.487 0 0 1-.788.85Z\" fill=\"%23000\"/><path d=\"m12.697 20.861.002.005c.237.495.617.852 1.128 1.04.515.191 1.039.16 1.539-.08a1.987 1.987 0 0 0 1.04-1.128 1.922 1.922 0 0 0-.079-1.536v-.003L13.684 13.5H17.1c.32 0 .628-.075.89-.26.263-.184.438-.447.544-.749.106-.3.135-.617.04-.925a1.458 1.458 0 0 0-.545-.738L7.936 2.883a1.51 1.51 0 0 0-.768-.336 1.471 1.471 0 0 0-.825.154 1.495 1.495 0 0 0-.626.547 1.492 1.492 0 0 0-.217.802v12.825c0 .324.08.634.272.897.193.261.467.43.775.53.31.099.632.12.942.016.31-.103.555-.313.743-.578h.001l1.825-2.552 2.639 5.673Z\" stroke=\"%23fff\" stroke-opacity=\".7\"/></svg>') 6 2,auto}.unreal-container.orbit{cursor:url('data:image/svg+xml,<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" fill=\"none\"><path fill=\"%23000\" d=\"M11.998 21.6c-1.966 0-3.737-.53-5.312-1.588-1.575-1.058-2.738-2.437-3.488-4.137a.852.852 0 0 1 .013-.713.867.867 0 0 1 .512-.487.86.86 0 0 1 .688.037c.225.109.387.28.487.513.617 1.35 1.559 2.45 2.825 3.3 1.267.85 2.692 1.275 4.275 1.275 1.384 0 2.646-.33 3.788-.988a7.985 7.985 0 0 0 2.762-2.612h-.85a.873.873 0 0 1-.64-.257.863.863 0 0 1-.26-.638.88.88 0 0 1 .26-.643.864.864 0 0 1 .64-.262h3c.255 0 .47.086.642.259a.87.87 0 0 1 .258.64v3a.874.874 0 0 1-.257.642.863.863 0 0 1-.637.259.88.88 0 0 1-.643-.259.865.865 0 0 1-.263-.641v-.725a9.95 9.95 0 0 1-3.35 2.925c-1.35.733-2.833 1.1-4.45 1.1Zm0-17.4c-1.383 0-2.645.33-3.787.987A7.985 7.985 0 0 0 5.448 7.8h.85c.255 0 .47.086.642.257a.863.863 0 0 1 .258.638.88.88 0 0 1-.258.642.864.864 0 0 1-.642.263h-3a.87.87 0 0 1-.64-.259.87.87 0 0 1-.26-.641v-3c0-.255.086-.469.258-.641a.863.863 0 0 1 .637-.26.88.88 0 0 1 .643.26.864.864 0 0 1 .262.64v.726A9.95 9.95 0 0 1 7.548 3.5c1.35-.733 2.834-1.1 4.45-1.1 2 0 3.788.542 5.363 1.625 1.575 1.083 2.736 2.49 3.482 4.22a.724.724 0 0 1-.045.646.926.926 0 0 1-.525.437.87.87 0 0 1-.687-.04.977.977 0 0 1-.488-.513c-.616-1.35-1.558-2.45-2.825-3.3-1.266-.85-2.691-1.275-4.275-1.275Zm0 10.8a2.893 2.893 0 0 1-2.125-.875A2.893 2.893 0 0 1 8.998 12c0-.833.292-1.542.875-2.125A2.893 2.893 0 0 1 11.998 9c.834 0 1.542.292 2.125.875.584.583.875 1.292.875 2.125s-.291 1.542-.875 2.125a2.893 2.893 0 0 1-2.125.875Z\"/><path stroke=\"%23fff\" stroke-opacity=\".7\" d=\"m2.739 16.072.002.005c.789 1.788 2.014 3.24 3.666 4.35 1.66 1.116 3.53 1.673 5.591 1.673 1.696 0 3.262-.386 4.69-1.16a10.54 10.54 0 0 0 2.714-2.097c.069.167.171.32.308.454.272.268.613.403.994.403.38 0 .722-.135.992-.406s.402-.614.402-.994v-3c0-.381-.134-.724-.405-.995a1.37 1.37 0 0 0-.995-.405h-3c-.383 0-.727.137-.997.411a1.38 1.38 0 0 0-.403.994c0 .38.135.723.407.992.238.237.532.369.858.397a7.552 7.552 0 0 1-2.027 1.685c-1.062.613-2.237.92-3.538.92-1.488 0-2.815-.397-3.996-1.19-1.19-.798-2.07-1.826-2.647-3.087-.147-.34-.393-.6-.727-.76a1.36 1.36 0 0 0-1.073-.058c-.37.133-.64.397-.8.754a1.351 1.351 0 0 0-.016 1.114Zm4.553-8.37a1.36 1.36 0 0 0-.858-.396A7.552 7.552 0 0 1 8.46 5.62c1.062-.613 2.237-.921 3.537-.921 1.489 0 2.816.398 3.997 1.19 1.19.8 2.07 1.827 2.646 3.088.148.34.394.6.728.76a1.37 1.37 0 0 0 1.068.063c.347-.12.62-.346.799-.668.187-.338.215-.71.072-1.072l-.003-.007-.003-.007c-.784-1.818-2.006-3.298-3.658-4.434C15.982 2.47 14.094 1.9 11.998 1.9c-1.695 0-3.261.385-4.688 1.16a10.536 10.536 0 0 0-2.715 2.096 1.364 1.364 0 0 0-.308-.453 1.38 1.38 0 0 0-.994-.403c-.38 0-.722.135-.992.406s-.403.614-.403.994v3c0 .38.135.724.406.995.27.27.614.405.994.405h3c.384 0 .727-.137.998-.412a1.38 1.38 0 0 0 .402-.993c0-.38-.135-.723-.406-.992ZM9.52 14.48a3.393 3.393 0 0 0 2.478 1.02c.965 0 1.8-.342 2.479-1.02a3.392 3.392 0 0 0 1.021-2.48c0-.963-.343-1.8-1.021-2.478A3.392 3.392 0 0 0 11.998 8.5c-.964 0-1.8.343-2.478 1.021A3.392 3.392 0 0 0 8.498 12c0 .964.344 1.8 1.022 2.479Z\"/></svg>') 12 12,auto}.unreal-container.pan{cursor:url('data:image/svg+xml,<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" fill=\"none\"><path fill=\"%23000\" d=\"M11.997 10.075a1.01 1.01 0 0 1-.738-.313 1.008 1.008 0 0 1-.312-.737V6.15l-.95.95a.998.998 0 0 1-.763.312 1.09 1.09 0 0 1-.763-.337 1.06 1.06 0 0 1-.324-.775c0-.3.108-.558.324-.775l2.776-2.775c.117-.116.237-.2.362-.25a1.04 1.04 0 0 1 .775 0c.125.05.246.134.363.25l2.8 2.8c.216.217.32.47.313.762a1.096 1.096 0 0 1-.338.763 1.058 1.058 0 0 1-.775.325c-.3 0-.558-.108-.775-.325l-.925-.925v2.875c0 .283-.104.53-.312.737a1.01 1.01 0 0 1-.738.313Zm0 11.5a1.04 1.04 0 0 1-.388-.075 1.103 1.103 0 0 1-.362-.25l-2.8-2.8a1.002 1.002 0 0 1-.313-.762c.009-.292.121-.546.337-.763.217-.217.475-.325.775-.325.3 0 .559.108.775.325l.926.925v-2.875c0-.283.104-.529.312-.737a1.01 1.01 0 0 1 .738-.313c.283 0 .529.104.738.313.208.208.312.454.312.737v2.875l.95-.95a.998.998 0 0 1 .762-.312c.292.008.546.12.763.337.216.217.324.475.324.775 0 .3-.108.559-.324.775l-2.775 2.775c-.117.116-.238.2-.363.25a1.04 1.04 0 0 1-.387.075Zm4.925-6.05a1.058 1.058 0 0 1-.326-.774c0-.3.109-.559.326-.776l.925-.925H14.97c-.283 0-.528-.104-.736-.312a1.01 1.01 0 0 1-.313-.738c0-.283.104-.53.313-.738.208-.208.453-.312.737-.312h2.875l-.95-.95a.998.998 0 0 1-.313-.762 1.09 1.09 0 0 1 .338-.764 1.06 1.06 0 0 1 .775-.324c.3 0 .558.108.775.324l2.774 2.776c.117.117.2.238.251.363a1.04 1.04 0 0 1 0 .775c-.05.124-.134.245-.25.362l-2.8 2.8c-.217.216-.471.32-.762.313a1.096 1.096 0 0 1-.763-.337Zm-11.4 0L2.746 12.75a1.103 1.103 0 0 1-.25-.362 1.04 1.04 0 0 1 0-.775c.05-.125.133-.246.25-.363l2.8-2.8c.216-.216.47-.32.762-.313.292.01.546.121.763.337.217.217.325.476.325.776 0 .3-.108.558-.325.775l-.925.925h2.875c.283 0 .529.104.737.312.208.209.313.455.313.738s-.105.53-.313.738a1.008 1.008 0 0 1-.737.312H6.147l.95.95c.217.217.32.471.312.762a1.09 1.09 0 0 1-.337.764 1.06 1.06 0 0 1-.775.324c-.3 0-.559-.108-.776-.324Z\"/><path stroke=\"%23fff\" stroke-opacity=\".7\" d=\"M9.22 7.912c.436.013.821-.15 1.13-.458l.097-.097v1.668c0 .422.16.793.458 1.09.298.299.67.46 1.092.46.422 0 .793-.161 1.091-.46.298-.297.459-.668.459-1.09V7.357l.071.072c.309.308.693.471 1.13.471.435 0 .82-.163 1.128-.471.301-.303.47-.676.483-1.102v-.001a1.501 1.501 0 0 0-.46-1.13l-2.799-2.8a1.6 1.6 0 0 0-.528-.36l-.003-.001a1.54 1.54 0 0 0-1.145 0l-.002.001a1.6 1.6 0 0 0-.529.36L8.118 5.172l-.001.001a1.56 1.56 0 0 0-.47 1.128c0 .435.163.82.47 1.128.303.303.676.472 1.103.484Zm0 0 .014-.5-.015.5Zm-1.311 6.865a1.497 1.497 0 0 0-.458-1.13l-.097-.097h1.668c.421 0 .793-.16 1.09-.458.298-.299.46-.67.46-1.092a1.51 1.51 0 0 0-.46-1.091 1.507 1.507 0 0 0-1.09-.459H7.354l.071-.072c.309-.308.472-.692.472-1.128 0-.436-.163-.82-.472-1.13a1.596 1.596 0 0 0-1.101-.482h-.002a1.501 1.501 0 0 0-1.129.459l-2.8 2.8a1.6 1.6 0 0 0-.36.528l-.001.003a1.54 1.54 0 0 0 0 1.144v.003c.081.2.208.375.362.529l2.774 2.775c.309.308.693.47 1.129.47.435 0 .82-.162 1.128-.47.303-.302.472-.676.484-1.102Zm0 0-.5-.014.5.015v-.001Zm3.038-8.627-.5.5.5.207V6.15Zm.475 15.814.002.001a1.54 1.54 0 0 0 1.145 0h.002c.2-.081.375-.208.53-.362l2.775-2.774a1.56 1.56 0 0 0 .47-1.129c0-.435-.162-.82-.47-1.128a1.589 1.589 0 0 0-1.103-.484 1.497 1.497 0 0 0-1.13.458l-.096.097v-1.668c0-.421-.161-.793-.459-1.09a1.509 1.509 0 0 0-1.091-.46c-.422 0-.794.161-1.092.46a1.507 1.507 0 0 0-.458 1.09v1.668l-.072-.072a1.558 1.558 0 0 0-1.129-.471c-.436 0-.82.163-1.128.471l-.001.001c-.301.302-.47.675-.483 1.101v.001c-.012.437.151.821.46 1.13l2.8 2.8c.153.153.328.28.528.36Zm5.146-6.085c.303.302.676.47 1.102.484a1.5 1.5 0 0 0 1.13-.46l2.8-2.8a1.6 1.6 0 0 0 .36-.528l.002-.003a1.54 1.54 0 0 0 0-1.144l-.001-.003a1.6 1.6 0 0 0-.361-.529l-2.774-2.775h-.001a1.56 1.56 0 0 0-1.128-.47c-.436 0-.82.162-1.128.47h-.001a1.59 1.59 0 0 0-.484 1.102c-.012.437.15.822.459 1.13l.097.097h-1.668c-.422 0-.793.16-1.09.459-.299.298-.46.67-.46 1.091 0 .422.161.793.46 1.092.297.297.668.458 1.09.458h1.668l-.072.072a1.558 1.558 0 0 0-.472 1.129c0 .435.164.82.472 1.128ZM6.147 13.05l.5.5.207-.5h-.707Z\"/></svg>') 12 12,auto}.unreal-container.dolly{cursor:url('data:image/svg+xml,<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" fill=\"none\"><mask id=\"a\" width=\"24\" height=\"24\" x=\"0\" y=\"0\" maskUnits=\"userSpaceOnUse\" style=\"mask-type:alpha\"><path fill=\"%23127392\" d=\"M0 0h24v24H0z\"/></mask><g mask=\"url(%23a)\"><path fill=\"%23000\" d=\"M12.002 20.575a1.026 1.026 0 0 1-.75-.325l-2.55-2.55a.933.933 0 0 1-.287-.713c.008-.275.113-.513.313-.713.2-.2.441-.3.725-.3.283 0 .524.1.724.3l.776.776V6.95l-.8.8c-.2.2-.438.296-.712.288a1.018 1.018 0 0 1-.713-.312c-.2-.2-.3-.442-.3-.726s.1-.525.3-.725l2.524-2.525a1.026 1.026 0 0 1 1.5 0l2.55 2.55c.2.2.296.438.289.713a1.023 1.023 0 0 1-.314.713c-.2.2-.441.3-.724.3a.988.988 0 0 1-.725-.3l-.775-.776v10.1l.8-.8c.2-.2.437-.296.712-.288.275.008.512.112.712.312.2.2.3.442.3.726s-.1.525-.3.725l-2.524 2.525a1.026 1.026 0 0 1-.75.325Z\"/><path stroke=\"%23fff\" stroke-opacity=\".7\" d=\"M13.553 8.153c.28.245.62.373 1 .373.415 0 .784-.153 1.078-.447.287-.287.446-.644.46-1.05v-.002a1.432 1.432 0 0 0-.435-1.08l-2.55-2.55-.354.353.354-.354a1.526 1.526 0 0 0-2.207 0L8.374 5.921A1.487 1.487 0 0 0 7.928 7c0 .416.152.785.446 1.08.288.287.645.446 1.052.457l4.127-.384Zm0 0v7.693m0-7.693a1.667 1.667 0 0 1-.079-.074l-3.021.075v7.693a1.482 1.482 0 0 0-1-.373c-.416 0-.785.153-1.079.447a1.522 1.522 0 0 0-.46 1.05v.002c-.011.417.14.786.435 1.08l2.55 2.55a1.527 1.527 0 0 0 2.207 0l2.525-2.524c.294-.294.446-.663.446-1.079 0-.416-.152-.785-.446-1.08a1.518 1.518 0 0 0-1.052-.457m0 0-.014.5.014-.5Zm0 0a1.424 1.424 0 0 0-1.027.383m0 0a1.583 1.583 0 0 0-.053.05l.354.354m-.3-.404v.104l.3.3m0 0-.3-.3v.6l.3-.3Z\"/></g></svg>%0D%0A') 12 12,auto}.unreal-container.disabled:before{content:\"\";position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px)}#playerUI{position:absolute;z-index:10;width:100%}#statsContainer{display:none;text-align:left;background-color:#000c}#stats{padding:6px;font-weight:700;font-size:14px;color:#0f0}canvas{position:absolute;image-rendering:crisp-edges}#overlay{position:absolute;top:0;right:2%;z-index:100;padding:4px;border-top-width:0;-webkit-border-bottom-right-radius:5px;-moz-border-radius-bottomright:5px;border-bottom-right-radius:5px;-webkit-border-bottom-left-radius:5px;-moz-border-radius-bottomleft:5px;border-bottom-left-radius:5px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-touch-callout:none}.overlay{font-weight:lighter;font-family:var(--src-font-family-body)}#overlayButton:hover{cursor:pointer}#overlayButton{font-size:40px;text-align:right}#overlaySettings{display:none;width:300px;padding:4px}#videoMessageOverlay{position:absolute;z-index:20;width:100%;margin:auto;font-size:1.8em;font-family:var(--src-font-family-body)}#playButton{display:inline-block;height:auto}img#playButton{width:10%;max-width:241px}#UIInteraction{position:fixed}#UIInteractionButtonBoundary{padding:2px}#UIInteractionButton{cursor:pointer}#hiddenInput{position:absolute;left:-10%;width:0;opacity:0}#editTextButton{position:absolute;width:40px;height:40px}\n"], dependencies: [{ kind: "component", type: AfkTimeoutModalComponent, selector: "app-afk-timeout-modal" }, { kind: "component", type: UnrealStatusComponent, selector: "app-unreal-status" }, { kind: "component", type: FreezeFrameComponent, selector: "app-freeze-frame" }, { kind: "component", type: VideoStatsComponent, selector: "app-video-stats" }, { kind: "component", type: ResumeStreamButtonComponent, selector: "app-resume-stream-button" }, { kind: "component", type: ImageLoadingSrcComponent, selector: "app-image-loading-src" }, { kind: "component", type: IntroSrcComponent, selector: "app-intro-src" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
6246
6634
  }
6247
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: UnrealSceneComponent, decorators: [{
6635
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: UnrealSceneComponent, decorators: [{
6248
6636
  type: Component,
6249
6637
  args: [{ selector: 'app-unreal-scene', changeDetection: ChangeDetectionStrategy.OnPush, imports: [
6250
6638
  AfkTimeoutModalComponent,
@@ -6283,10 +6671,10 @@ class WebrtcErrorModalComponent {
6283
6671
  }
6284
6672
  this.close();
6285
6673
  }
6286
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: WebrtcErrorModalComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
6287
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.8", type: WebrtcErrorModalComponent, isStandalone: true, selector: "app-webrtc-error-modal", ngImport: i0, template: "<div [attr.data-testid]=\"'webrtc-error-modal'\" class=\"src-modal\">\r\n <header class=\"src-modal__header\"></header>\r\n <section class=\"src-modal__body\">\r\n <div [attr.data-testid]=\"'title'\" class=\"src-modal__title\">\r\n WebRTC error\r\n </div>\r\n <div style=\"text-align: left\">\r\n An internet connection type (WebRTC) appears to be blocked either by your\r\n browser settings or your current network. If WebRTC is blocked on your\r\n browser you may be able to adjust these settings yourself based on\r\n instructions here:\r\n <a href=\"https://myownconference.com/blog/en/webrtc/\" target=\"blank\"\r\n >https://myownconference.com/blog/en/webrtc/</a\r\n >\r\n <br /><br />\r\n Trying a different web browser may help confirm this as well. If WebRTC is\r\n blocked by your network, try switching to a different network if possible\r\n or contact your network administrator.<br /><br />\r\n WebRTC is common, safe and increasingly utilised method for streaming real\r\n time 3D experiences via a web browser. It typically consumes no more\r\n bandwidth than streaming an HD video.\r\n </div>\r\n </section>\r\n <footer class=\"src-modal__footer\">\r\n <src-button\r\n (onClick)=\"closeModalWithCirrusDisconnect()\"\r\n [data-testid]=\"'close-webrtc-error-modal'\"\r\n weight=\"primary\"\r\n >\r\n Ok\r\n </src-button>\r\n </footer>\r\n</div>\r\n", styles: [".src-modal{--modalBodyPadding: 20px 8px 20px 20px;display:grid;grid-template-columns:minmax(0,1fr);grid-template-rows:auto minmax(0,1fr) auto}.src-modal__body{width:100%}.src-modal__body p{color:var(--src-text-body-main, #1f2937);font-size:14px;font-style:normal;font-weight:400;line-height:24px;margin-top:0}.src-modal__body a{word-wrap:break-word;white-space:normal}\n"], dependencies: [{ kind: "component", type: SourceButtonComponent, selector: "src-button", inputs: ["type", "appearance", "weight", "size", "context", "customClass", "isFullWidth", "isPressed", "isDisabled", "isLoading", "isInverted", "srcButtonConfig", "formID", "data-testid", "ariaLabel"], outputs: ["onClick", "onSubmit"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
6674
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: WebrtcErrorModalComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
6675
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.9", type: WebrtcErrorModalComponent, isStandalone: true, selector: "app-webrtc-error-modal", ngImport: i0, template: "<div [attr.data-testid]=\"'webrtc-error-modal'\" class=\"src-modal\">\r\n <header class=\"src-modal__header\"></header>\r\n <section class=\"src-modal__body\">\r\n <div [attr.data-testid]=\"'title'\" class=\"src-modal__title\">\r\n WebRTC error\r\n </div>\r\n <div style=\"text-align: left\">\r\n An internet connection type (WebRTC) appears to be blocked either by your\r\n browser settings or your current network. If WebRTC is blocked on your\r\n browser you may be able to adjust these settings yourself based on\r\n instructions here:\r\n <a href=\"https://myownconference.com/blog/en/webrtc/\" target=\"blank\"\r\n >https://myownconference.com/blog/en/webrtc/</a\r\n >\r\n <br /><br />\r\n Trying a different web browser may help confirm this as well. If WebRTC is\r\n blocked by your network, try switching to a different network if possible\r\n or contact your network administrator.<br /><br />\r\n WebRTC is common, safe and increasingly utilised method for streaming real\r\n time 3D experiences via a web browser. It typically consumes no more\r\n bandwidth than streaming an HD video.\r\n </div>\r\n </section>\r\n <footer class=\"src-modal__footer\">\r\n <src-button\r\n (onClick)=\"closeModalWithCirrusDisconnect()\"\r\n [data-testid]=\"'close-webrtc-error-modal'\"\r\n weight=\"primary\"\r\n >\r\n Ok\r\n </src-button>\r\n </footer>\r\n</div>\r\n", styles: [".src-modal{--modalBodyPadding: 20px 8px 20px 20px;display:grid;grid-template-columns:minmax(0,1fr);grid-template-rows:auto minmax(0,1fr) auto}.src-modal__body{width:100%}.src-modal__body p{color:var(--src-text-body-main, #1f2937);font-size:14px;font-style:normal;font-weight:400;line-height:24px;margin-top:0}.src-modal__body a{word-wrap:break-word;white-space:normal}\n"], dependencies: [{ kind: "component", type: SourceButtonComponent, selector: "src-button", inputs: ["type", "appearance", "weight", "size", "context", "customClass", "isFullWidth", "isPressed", "isDisabled", "isLoading", "isInverted", "srcButtonConfig", "formID", "data-testid", "ariaLabel"], outputs: ["onClick", "onSubmit"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
6288
6676
  }
6289
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: WebrtcErrorModalComponent, decorators: [{
6677
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: WebrtcErrorModalComponent, decorators: [{
6290
6678
  type: Component,
6291
6679
  args: [{ selector: 'app-webrtc-error-modal', changeDetection: ChangeDetectionStrategy.OnPush, imports: [SourceButtonComponent], template: "<div [attr.data-testid]=\"'webrtc-error-modal'\" class=\"src-modal\">\r\n <header class=\"src-modal__header\"></header>\r\n <section class=\"src-modal__body\">\r\n <div [attr.data-testid]=\"'title'\" class=\"src-modal__title\">\r\n WebRTC error\r\n </div>\r\n <div style=\"text-align: left\">\r\n An internet connection type (WebRTC) appears to be blocked either by your\r\n browser settings or your current network. If WebRTC is blocked on your\r\n browser you may be able to adjust these settings yourself based on\r\n instructions here:\r\n <a href=\"https://myownconference.com/blog/en/webrtc/\" target=\"blank\"\r\n >https://myownconference.com/blog/en/webrtc/</a\r\n >\r\n <br /><br />\r\n Trying a different web browser may help confirm this as well. If WebRTC is\r\n blocked by your network, try switching to a different network if possible\r\n or contact your network administrator.<br /><br />\r\n WebRTC is common, safe and increasingly utilised method for streaming real\r\n time 3D experiences via a web browser. It typically consumes no more\r\n bandwidth than streaming an HD video.\r\n </div>\r\n </section>\r\n <footer class=\"src-modal__footer\">\r\n <src-button\r\n (onClick)=\"closeModalWithCirrusDisconnect()\"\r\n [data-testid]=\"'close-webrtc-error-modal'\"\r\n weight=\"primary\"\r\n >\r\n Ok\r\n </src-button>\r\n </footer>\r\n</div>\r\n", styles: [".src-modal{--modalBodyPadding: 20px 8px 20px 20px;display:grid;grid-template-columns:minmax(0,1fr);grid-template-rows:auto minmax(0,1fr) auto}.src-modal__body{width:100%}.src-modal__body p{color:var(--src-text-body-main, #1f2937);font-size:14px;font-style:normal;font-weight:400;line-height:24px;margin-top:0}.src-modal__body a{word-wrap:break-word;white-space:normal}\n"] }]
6292
6680
  }] });
@@ -6332,10 +6720,10 @@ class FilterSettingsComponent {
6332
6720
  panelOpen: this.settings.panelOpen,
6333
6721
  });
6334
6722
  }
6335
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: FilterSettingsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
6336
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.8", type: FilterSettingsComponent, isStandalone: true, selector: "app-filter-settings", ngImport: i0, template: "<div class=\"settings-container\">\r\n <button (click)=\"togglePanel()\" class=\"gear-button\">LBM</button>\r\n @if (settings.panelOpen) {\r\n <div class=\"settings-panel\">\r\n <h3>LBM Trigger Settings</h3>\r\n <h5>Data Flow Monitor</h5>\r\n <form>\r\n <label>\r\n Monitoring delay:\r\n <input\r\n [(ngModel)]=\"settings.monitoringDelayTime\"\r\n [placeholder]=\"defaultFilterModel.monitoringDelayTime\"\r\n type=\"number\"\r\n name=\"monitoringDelayTime\"\r\n />\r\n </label>\r\n\r\n <label>\r\n Minimum Bitrate (kbit/s):\r\n <input\r\n [(ngModel)]=\"settings.minimumBitrate\"\r\n [placeholder]=\"defaultFilterModel.minimumBitrate\"\r\n type=\"number\"\r\n name=\"minimumBitrate\"\r\n />\r\n </label>\r\n <label>\r\n Yellow Flag (%):\r\n <input\r\n [(ngModel)]=\"settings.yellowFlag\"\r\n [placeholder]=\"defaultFilterModel.yellowFlag\"\r\n type=\"number\"\r\n name=\"yellowFlag\"\r\n />\r\n </label>\r\n <label>\r\n Red Flag (%):\r\n <input\r\n [(ngModel)]=\"settings.redFlag\"\r\n [placeholder]=\"defaultFilterModel.redFlag\"\r\n type=\"number\"\r\n name=\"redFlag\"\r\n />\r\n </label>\r\n <label>\r\n Minimum FPS:\r\n <input\r\n [(ngModel)]=\"settings.minimumFps\"\r\n [placeholder]=\"defaultFilterModel.minimumFps\"\r\n type=\"number\"\r\n name=\"minimumFps\"\r\n />\r\n </label>\r\n <hr />\r\n <h5>Kalman Filter</h5>\r\n <label>\r\n Initial Bitrate Estimate (kbit/s):\r\n <input\r\n [(ngModel)]=\"settings.initialBitrateEstimate\"\r\n [placeholder]=\"defaultFilterModel.initialBitrateEstimate\"\r\n type=\"number\"\r\n name=\"initialBitrateEstimate\"\r\n />\r\n </label>\r\n <label>\r\n Initial Error Covariance:\r\n <input\r\n [(ngModel)]=\"settings.initialErrorCovariance\"\r\n [placeholder]=\"defaultFilterModel.initialErrorCovariance\"\r\n type=\"number\"\r\n name=\"initialErrorCovariance\"\r\n />\r\n </label>\r\n <label>\r\n Process Noise (Q):\r\n <input\r\n [(ngModel)]=\"settings.processNoise\"\r\n [placeholder]=\"defaultFilterModel.processNoise\"\r\n type=\"number\"\r\n name=\"processNoise\"\r\n />\r\n </label>\r\n <label>\r\n Measurement Noise (R):\r\n <input\r\n [(ngModel)]=\"settings.measurementNoise\"\r\n [placeholder]=\"defaultFilterModel.measurementNoise\"\r\n type=\"number\"\r\n name=\"measurementNoise\"\r\n />\r\n </label>\r\n </form>\r\n <br />\r\n <br />\r\n <div class=\"apply-button-container\">\r\n <button (click)=\"reset()\">Reset</button>\r\n <button (click)=\"saveSettings()\">Apply</button>\r\n </div>\r\n </div>\r\n }\r\n</div>\r\n", styles: [".settings-container{position:absolute;top:65px;right:10px;z-index:1000}.gear-button{right:0;width:50px;position:absolute;cursor:pointer;font-size:10px;background:none;font-weight:500;border:1px solid;border-radius:9px;background:#0162cc;color:#fff;padding:4px}.gear-button>*{color:#e6f419;filter:drop-shadow(1px 1px 1px rgb(0,0,0))}.settings-panel{font-size:13px;background:#fff;border:1px solid #ccc;padding:15px;margin-top:25px;border-radius:4px;box-shadow:0 2px 8px #0003}.settings-panel h3{margin-top:0}.settings-panel form{display:flex;flex-direction:column}.settings-panel label{margin-bottom:10px;display:flex;flex-direction:column}.settings-panel input{padding:4px;margin-top:4px;border:1px solid #ccc;border-radius:2px}.apply-button-container{position:absolute;bottom:15px;right:15px}.apply-button-container button{padding:6px 12px;border:none;background-color:#1976d2;color:#fff;border-radius:4px;cursor:pointer;margin:2px}.apply-button-container button:hover{background-color:#1565c0}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i1.NgForm, selector: "form:not([ngNoForm]):not([formGroup]):not([formArray]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
6723
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: FilterSettingsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
6724
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: FilterSettingsComponent, isStandalone: true, selector: "app-filter-settings", ngImport: i0, template: "<div class=\"settings-container\">\r\n <button (click)=\"togglePanel()\" class=\"gear-button\">LBM</button>\r\n @if (settings.panelOpen) {\r\n <div class=\"settings-panel\">\r\n <h3>LBM Trigger Settings</h3>\r\n <h5>Data Flow Monitor</h5>\r\n <form>\r\n <label>\r\n Monitoring delay:\r\n <input\r\n [(ngModel)]=\"settings.monitoringDelayTime\"\r\n [placeholder]=\"defaultFilterModel.monitoringDelayTime\"\r\n type=\"number\"\r\n name=\"monitoringDelayTime\"\r\n />\r\n </label>\r\n\r\n <label>\r\n Minimum Bitrate (kbit/s):\r\n <input\r\n [(ngModel)]=\"settings.minimumBitrate\"\r\n [placeholder]=\"defaultFilterModel.minimumBitrate\"\r\n type=\"number\"\r\n name=\"minimumBitrate\"\r\n />\r\n </label>\r\n <label>\r\n Yellow Flag (%):\r\n <input\r\n [(ngModel)]=\"settings.yellowFlag\"\r\n [placeholder]=\"defaultFilterModel.yellowFlag\"\r\n type=\"number\"\r\n name=\"yellowFlag\"\r\n />\r\n </label>\r\n <label>\r\n Red Flag (%):\r\n <input\r\n [(ngModel)]=\"settings.redFlag\"\r\n [placeholder]=\"defaultFilterModel.redFlag\"\r\n type=\"number\"\r\n name=\"redFlag\"\r\n />\r\n </label>\r\n <label>\r\n Minimum FPS:\r\n <input\r\n [(ngModel)]=\"settings.minimumFps\"\r\n [placeholder]=\"defaultFilterModel.minimumFps\"\r\n type=\"number\"\r\n name=\"minimumFps\"\r\n />\r\n </label>\r\n <hr />\r\n <h5>Kalman Filter</h5>\r\n <label>\r\n Initial Bitrate Estimate (kbit/s):\r\n <input\r\n [(ngModel)]=\"settings.initialBitrateEstimate\"\r\n [placeholder]=\"defaultFilterModel.initialBitrateEstimate\"\r\n type=\"number\"\r\n name=\"initialBitrateEstimate\"\r\n />\r\n </label>\r\n <label>\r\n Initial Error Covariance:\r\n <input\r\n [(ngModel)]=\"settings.initialErrorCovariance\"\r\n [placeholder]=\"defaultFilterModel.initialErrorCovariance\"\r\n type=\"number\"\r\n name=\"initialErrorCovariance\"\r\n />\r\n </label>\r\n <label>\r\n Process Noise (Q):\r\n <input\r\n [(ngModel)]=\"settings.processNoise\"\r\n [placeholder]=\"defaultFilterModel.processNoise\"\r\n type=\"number\"\r\n name=\"processNoise\"\r\n />\r\n </label>\r\n <label>\r\n Measurement Noise (R):\r\n <input\r\n [(ngModel)]=\"settings.measurementNoise\"\r\n [placeholder]=\"defaultFilterModel.measurementNoise\"\r\n type=\"number\"\r\n name=\"measurementNoise\"\r\n />\r\n </label>\r\n </form>\r\n <br />\r\n <br />\r\n <div class=\"apply-button-container\">\r\n <button (click)=\"reset()\">Reset</button>\r\n <button (click)=\"saveSettings()\">Apply</button>\r\n </div>\r\n </div>\r\n }\r\n</div>\r\n", styles: [".settings-container{position:absolute;top:65px;right:10px;z-index:1000}.gear-button{right:0;width:50px;position:absolute;cursor:pointer;font-size:10px;background:none;font-weight:500;border:1px solid;border-radius:9px;background:#0162cc;color:#fff;padding:4px}.gear-button>*{color:#e6f419;filter:drop-shadow(1px 1px 1px rgb(0,0,0))}.settings-panel{font-size:13px;background:#fff;border:1px solid #ccc;padding:15px;margin-top:25px;border-radius:4px;box-shadow:0 2px 8px #0003}.settings-panel h3{margin-top:0}.settings-panel form{display:flex;flex-direction:column}.settings-panel label{margin-bottom:10px;display:flex;flex-direction:column}.settings-panel input{padding:4px;margin-top:4px;border:1px solid #ccc;border-radius:2px}.apply-button-container{position:absolute;bottom:15px;right:15px}.apply-button-container button{padding:6px 12px;border:none;background-color:#1976d2;color:#fff;border-radius:4px;cursor:pointer;margin:2px}.apply-button-container button:hover{background-color:#1565c0}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i1.NgForm, selector: "form:not([ngNoForm]):not([formGroup]):not([formArray]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
6337
6725
  }
6338
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: FilterSettingsComponent, decorators: [{
6726
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: FilterSettingsComponent, decorators: [{
6339
6727
  type: Component,
6340
6728
  args: [{ selector: 'app-filter-settings', imports: [FormsModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"settings-container\">\r\n <button (click)=\"togglePanel()\" class=\"gear-button\">LBM</button>\r\n @if (settings.panelOpen) {\r\n <div class=\"settings-panel\">\r\n <h3>LBM Trigger Settings</h3>\r\n <h5>Data Flow Monitor</h5>\r\n <form>\r\n <label>\r\n Monitoring delay:\r\n <input\r\n [(ngModel)]=\"settings.monitoringDelayTime\"\r\n [placeholder]=\"defaultFilterModel.monitoringDelayTime\"\r\n type=\"number\"\r\n name=\"monitoringDelayTime\"\r\n />\r\n </label>\r\n\r\n <label>\r\n Minimum Bitrate (kbit/s):\r\n <input\r\n [(ngModel)]=\"settings.minimumBitrate\"\r\n [placeholder]=\"defaultFilterModel.minimumBitrate\"\r\n type=\"number\"\r\n name=\"minimumBitrate\"\r\n />\r\n </label>\r\n <label>\r\n Yellow Flag (%):\r\n <input\r\n [(ngModel)]=\"settings.yellowFlag\"\r\n [placeholder]=\"defaultFilterModel.yellowFlag\"\r\n type=\"number\"\r\n name=\"yellowFlag\"\r\n />\r\n </label>\r\n <label>\r\n Red Flag (%):\r\n <input\r\n [(ngModel)]=\"settings.redFlag\"\r\n [placeholder]=\"defaultFilterModel.redFlag\"\r\n type=\"number\"\r\n name=\"redFlag\"\r\n />\r\n </label>\r\n <label>\r\n Minimum FPS:\r\n <input\r\n [(ngModel)]=\"settings.minimumFps\"\r\n [placeholder]=\"defaultFilterModel.minimumFps\"\r\n type=\"number\"\r\n name=\"minimumFps\"\r\n />\r\n </label>\r\n <hr />\r\n <h5>Kalman Filter</h5>\r\n <label>\r\n Initial Bitrate Estimate (kbit/s):\r\n <input\r\n [(ngModel)]=\"settings.initialBitrateEstimate\"\r\n [placeholder]=\"defaultFilterModel.initialBitrateEstimate\"\r\n type=\"number\"\r\n name=\"initialBitrateEstimate\"\r\n />\r\n </label>\r\n <label>\r\n Initial Error Covariance:\r\n <input\r\n [(ngModel)]=\"settings.initialErrorCovariance\"\r\n [placeholder]=\"defaultFilterModel.initialErrorCovariance\"\r\n type=\"number\"\r\n name=\"initialErrorCovariance\"\r\n />\r\n </label>\r\n <label>\r\n Process Noise (Q):\r\n <input\r\n [(ngModel)]=\"settings.processNoise\"\r\n [placeholder]=\"defaultFilterModel.processNoise\"\r\n type=\"number\"\r\n name=\"processNoise\"\r\n />\r\n </label>\r\n <label>\r\n Measurement Noise (R):\r\n <input\r\n [(ngModel)]=\"settings.measurementNoise\"\r\n [placeholder]=\"defaultFilterModel.measurementNoise\"\r\n type=\"number\"\r\n name=\"measurementNoise\"\r\n />\r\n </label>\r\n </form>\r\n <br />\r\n <br />\r\n <div class=\"apply-button-container\">\r\n <button (click)=\"reset()\">Reset</button>\r\n <button (click)=\"saveSettings()\">Apply</button>\r\n </div>\r\n </div>\r\n }\r\n</div>\r\n", styles: [".settings-container{position:absolute;top:65px;right:10px;z-index:1000}.gear-button{right:0;width:50px;position:absolute;cursor:pointer;font-size:10px;background:none;font-weight:500;border:1px solid;border-radius:9px;background:#0162cc;color:#fff;padding:4px}.gear-button>*{color:#e6f419;filter:drop-shadow(1px 1px 1px rgb(0,0,0))}.settings-panel{font-size:13px;background:#fff;border:1px solid #ccc;padding:15px;margin-top:25px;border-radius:4px;box-shadow:0 2px 8px #0003}.settings-panel h3{margin-top:0}.settings-panel form{display:flex;flex-direction:column}.settings-panel label{margin-bottom:10px;display:flex;flex-direction:column}.settings-panel input{padding:4px;margin-top:4px;border:1px solid #ccc;border-radius:2px}.apply-button-container{position:absolute;bottom:15px;right:15px}.apply-button-container button{padding:6px 12px;border:none;background-color:#1976d2;color:#fff;border-radius:4px;cursor:pointer;margin:2px}.apply-button-container button:hover{background-color:#1565c0}\n"] }]
6341
6729
  }] });
@@ -6440,10 +6828,10 @@ class LowBandwidthDetectorComponent {
6440
6828
  this.store.dispatch(changeLowBandwidth({ lowBandwidth: false }));
6441
6829
  });
6442
6830
  }
6443
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: LowBandwidthDetectorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
6444
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.8", type: LowBandwidthDetectorComponent, isStandalone: true, selector: "app-low-bandwidth-detector", ngImport: i0, template: "@if (isReducedQuality() || isLowBandwidth()) {\r\n <div\r\n [class.expanded]=\"isIndicatorExpanded() && isLowBandwidth()\"\r\n class=\"lbm-indicator freeze-loader\"\r\n >\r\n <div (click)=\"toggleIndicator(true)\" class=\"lbm-icon\">\r\n <svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 20 20\">\r\n <path\r\n fill=\"#85888E\"\r\n d=\"M7.189 3.605c-.73.145-1.438.35-2.126.614A14.412 14.412 0 0 0 .896 6.666a1.08 1.08 0 0 0-.375.844c0 .34.118.629.354.865s.524.36.865.375c.34.014.65-.09.927-.313a11.89 11.89 0 0 1 3.385-1.916 10.94 10.94 0 0 1 1.235-.375l-.098-2.541ZM7.385 8.708a9.107 9.107 0 0 0-2.906 1.48c-.264.194-.402.464-.416.812-.014.347.104.646.354.896.236.236.524.364.864.385.34.02.664-.073.97-.281.422-.29.878-.53 1.368-.721a2.545 2.545 0 0 1-.166-.814l-.068-1.757ZM12.386 11.267c.094-.25.15-.52.161-.802l.068-1.755a9.019 9.019 0 0 1 2.927 1.52c.264.193.4.46.406.801.007.34-.114.635-.364.885a1.213 1.213 0 0 1-.865.365A1.614 1.614 0 0 1 13.77 12a6.574 6.574 0 0 0-1.385-.733ZM12.713 6.146l.098-2.542c.73.146 1.438.351 2.127.615 1.541.59 2.93 1.406 4.166 2.447.25.223.379.5.386.834.007.333-.115.625-.365.875a1.253 1.253 0 0 1-.864.375c-.34.014-.65-.09-.927-.313a11.892 11.892 0 0 0-3.386-1.916 10.94 10.94 0 0 0-1.235-.375ZM8.813 16.187c.32.32.715.48 1.187.48.472 0 .868-.16 1.188-.48.32-.32.479-.715.479-1.187 0-.473-.16-.868-.48-1.188-.319-.32-.715-.479-1.187-.479-.472 0-.868.16-1.187.48-.32.319-.48.714-.48 1.187 0 .472.16.868.48 1.187Z\"\r\n />\r\n <path\r\n fill=\"#fff\"\r\n fill-rule=\"evenodd\"\r\n d=\"M10 .833c-.91 0-1.637.756-1.602 1.665l.304 7.92a1.299 1.299 0 0 0 2.596 0l.305-7.92A1.604 1.604 0 0 0 10 .833ZM8.813 16.187c.32.32.715.48 1.187.48.472 0 .868-.16 1.188-.48.32-.32.479-.715.479-1.187 0-.473-.16-.868-.48-1.188-.319-.32-.715-.479-1.187-.479-.472 0-.868.16-1.187.48-.32.319-.48.714-.48 1.187 0 .472.16.868.48 1.187Z\"\r\n clip-rule=\"evenodd\"\r\n />\r\n </svg>\r\n </div>\r\n\r\n <div\r\n [class.lbm-message--open]=\"isLowBandwidth() && isIndicatorExpanded()\"\r\n class=\"lbm-message\"\r\n >\r\n <p class=\"lbm-description\">\r\n Fluid Interactivity Modes were disabled due to an unstable connection.\r\n\r\n <button\r\n (click)=\"openLBMDialog()\"\r\n [attr.data-testid]=\"'learn-more-lbm'\"\r\n type=\"button\"\r\n class=\"lbm-learn-more\"\r\n >\r\n Learn more\r\n </button>\r\n </p>\r\n <button\r\n (click)=\"toggleIndicator(false)\"\r\n [attr.data-testid]=\"'close-lbm-indicator'\"\r\n type=\"button\"\r\n class=\"lbm-close\"\r\n >\r\n <svg\r\n xmlns=\"http://www.w3.org/2000/svg\"\r\n width=\"20\"\r\n height=\"20\"\r\n viewBox=\"0 0 20 20\"\r\n fill=\"none\"\r\n >\r\n <path\r\n d=\"M10.0001 11.2289L6.68618 14.5428C6.51951 14.7095 6.31818 14.7895 6.08218 14.7828C5.84618 14.7755 5.64485 14.6885 5.47818 14.5218C5.31151 14.3552 5.22818 14.1505 5.22818 13.9078C5.22818 13.6645 5.31151 13.4595 5.47818 13.2928L8.77107 9.99992L5.45718 6.68603C5.29051 6.51936 5.21051 6.3147 5.21718 6.07203C5.22451 5.8287 5.31151 5.6237 5.47818 5.45703C5.64485 5.29036 5.84951 5.20703 6.09218 5.20703C6.33551 5.20703 6.54051 5.29036 6.70718 5.45703L10.0001 8.77092L13.314 5.45703C13.4806 5.29036 13.6853 5.20703 13.928 5.20703C14.1713 5.20703 14.3763 5.29036 14.543 5.45703C14.7096 5.6237 14.793 5.8287 14.793 6.07203C14.793 6.3147 14.7096 6.51936 14.543 6.68603L11.2291 9.99992L14.543 13.3138C14.7096 13.4805 14.793 13.6818 14.793 13.9178C14.793 14.1538 14.7096 14.3552 14.543 14.5218C14.3763 14.6885 14.1713 14.7718 13.928 14.7718C13.6853 14.7718 13.4806 14.6885 13.314 14.5218L10.0001 11.2289Z\"\r\n fill=\"white\"\r\n />\r\n </svg>\r\n </button>\r\n </div>\r\n </div>\r\n}\r\n\r\n@if (isDevMode) {\r\n <app-filter-settings />\r\n}\r\n", styles: [".freeze-loader,.lbm-indicator{--lbmPositionTop: 18px;--lbmPositionLeft: 18px;--lbmPositionRight: 18px;--lbmIndicatorSize: 36px;--lbmIndicatorBorderRadius: 8px;--lbmIndicatorCloseSize: 20px;--lbmPadding: 10px;--lbmTextSize: 13px;--lbmLineHeight: 20px;--lbmTextColor: #ffffff;--lbmBackgroundColor: rgba(17, 24, 39, .7);position:absolute;top:var(--lbmPositionTop);left:var(--lbmPositionLeft);z-index:10;display:flex;justify-content:center;align-items:flex-start;min-width:var(--lbmIndicatorSize);min-height:var(--lbmIndicatorSize);overflow:hidden}.freeze-loader.expanded,.lbm-indicator.expanded{--lbmPositionTop: 8px;--lbmPositionLeft: 8px;--lbmPositionRight: 8px;width:auto;max-width:calc(100% - var(--lbmPositionLeft) - var(--lbmPositionRight));padding:var(--lbmPadding);background:var(--lbmBackgroundColor);-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);border-radius:var(--lbmIndicatorBorderRadius);gap:12px}.freeze-loader .lbm-icon,.lbm-indicator .lbm-icon{display:flex;width:var(--lbmIndicatorSize);height:var(--lbmIndicatorSize);padding:8px;color:#fff;background-color:var(--lbmBackgroundColor);border-radius:50%;flex-shrink:0;cursor:pointer}.freeze-loader .lbm-icon svg,.freeze-loader .lbm-icon img,.lbm-indicator .lbm-icon svg,.lbm-indicator .lbm-icon img{width:100%;height:100%}.freeze-loader .lbm-close,.lbm-indicator .lbm-close{position:absolute;top:0;right:0;width:var(--lbmIndicatorCloseSize);height:var(--lbmIndicatorCloseSize);background-color:transparent;border:none;padding:0;cursor:pointer}.freeze-loader .lbm-message,.lbm-indicator .lbm-message{position:relative;opacity:0;height:0;max-width:0;padding-right:24px;pointer-events:none;visibility:hidden;animation:closeMessage .3s forwards}.freeze-loader .lbm-message--open,.lbm-indicator .lbm-message--open{position:relative;visibility:visible;display:flex;align-items:center;align-self:center;pointer-events:all;gap:12px;max-width:fit-content;height:auto;opacity:1;animation:openMessage .3s forwards}.freeze-loader .lbm-description,.lbm-indicator .lbm-description{margin:0;font-size:var(--lbmTextSize);font-weight:400;line-height:var(--lbmLineHeight);color:var(--lbmTextColor)}.freeze-loader .lbm-learn-more,.lbm-indicator .lbm-learn-more{display:inline-flex;padding:0;font-size:var(--lbmTextSize);font-weight:400;line-height:var(--lbmLineHeight);color:var(--lbmTextColor);text-decoration-line:underline;background-color:transparent;border:none;cursor:pointer}.freeze-loader .lbm-learn-more:hover,.lbm-indicator .lbm-learn-more:hover{text-decoration:none}@keyframes openMessage{0%{opacity:0;max-width:0;height:0}10%{height:auto;max-width:fit-content}to{opacity:1;height:auto;max-width:fit-content}}@keyframes closeMessage{0%{opacity:1;max-width:fit-content}to{opacity:0;max-width:0}}\n"], dependencies: [{ kind: "component", type: FilterSettingsComponent, selector: "app-filter-settings" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
6831
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: LowBandwidthDetectorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
6832
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: LowBandwidthDetectorComponent, isStandalone: true, selector: "app-low-bandwidth-detector", ngImport: i0, template: "@if (isReducedQuality() || isLowBandwidth()) {\r\n <div\r\n [class.expanded]=\"isIndicatorExpanded() && isLowBandwidth()\"\r\n class=\"lbm-indicator freeze-loader\"\r\n >\r\n <div (click)=\"toggleIndicator(true)\" class=\"lbm-icon\">\r\n <svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 20 20\">\r\n <path\r\n fill=\"#85888E\"\r\n d=\"M7.189 3.605c-.73.145-1.438.35-2.126.614A14.412 14.412 0 0 0 .896 6.666a1.08 1.08 0 0 0-.375.844c0 .34.118.629.354.865s.524.36.865.375c.34.014.65-.09.927-.313a11.89 11.89 0 0 1 3.385-1.916 10.94 10.94 0 0 1 1.235-.375l-.098-2.541ZM7.385 8.708a9.107 9.107 0 0 0-2.906 1.48c-.264.194-.402.464-.416.812-.014.347.104.646.354.896.236.236.524.364.864.385.34.02.664-.073.97-.281.422-.29.878-.53 1.368-.721a2.545 2.545 0 0 1-.166-.814l-.068-1.757ZM12.386 11.267c.094-.25.15-.52.161-.802l.068-1.755a9.019 9.019 0 0 1 2.927 1.52c.264.193.4.46.406.801.007.34-.114.635-.364.885a1.213 1.213 0 0 1-.865.365A1.614 1.614 0 0 1 13.77 12a6.574 6.574 0 0 0-1.385-.733ZM12.713 6.146l.098-2.542c.73.146 1.438.351 2.127.615 1.541.59 2.93 1.406 4.166 2.447.25.223.379.5.386.834.007.333-.115.625-.365.875a1.253 1.253 0 0 1-.864.375c-.34.014-.65-.09-.927-.313a11.892 11.892 0 0 0-3.386-1.916 10.94 10.94 0 0 0-1.235-.375ZM8.813 16.187c.32.32.715.48 1.187.48.472 0 .868-.16 1.188-.48.32-.32.479-.715.479-1.187 0-.473-.16-.868-.48-1.188-.319-.32-.715-.479-1.187-.479-.472 0-.868.16-1.187.48-.32.319-.48.714-.48 1.187 0 .472.16.868.48 1.187Z\"\r\n />\r\n <path\r\n fill=\"#fff\"\r\n fill-rule=\"evenodd\"\r\n d=\"M10 .833c-.91 0-1.637.756-1.602 1.665l.304 7.92a1.299 1.299 0 0 0 2.596 0l.305-7.92A1.604 1.604 0 0 0 10 .833ZM8.813 16.187c.32.32.715.48 1.187.48.472 0 .868-.16 1.188-.48.32-.32.479-.715.479-1.187 0-.473-.16-.868-.48-1.188-.319-.32-.715-.479-1.187-.479-.472 0-.868.16-1.187.48-.32.319-.48.714-.48 1.187 0 .472.16.868.48 1.187Z\"\r\n clip-rule=\"evenodd\"\r\n />\r\n </svg>\r\n </div>\r\n\r\n <div\r\n [class.lbm-message--open]=\"isLowBandwidth() && isIndicatorExpanded()\"\r\n class=\"lbm-message\"\r\n >\r\n <p class=\"lbm-description\">\r\n Fluid Interactivity Modes were disabled due to an unstable connection.\r\n\r\n <button\r\n (click)=\"openLBMDialog()\"\r\n [attr.data-testid]=\"'learn-more-lbm'\"\r\n type=\"button\"\r\n class=\"lbm-learn-more\"\r\n >\r\n Learn more\r\n </button>\r\n </p>\r\n <button\r\n (click)=\"toggleIndicator(false)\"\r\n [attr.data-testid]=\"'close-lbm-indicator'\"\r\n type=\"button\"\r\n class=\"lbm-close\"\r\n >\r\n <svg\r\n xmlns=\"http://www.w3.org/2000/svg\"\r\n width=\"20\"\r\n height=\"20\"\r\n viewBox=\"0 0 20 20\"\r\n fill=\"none\"\r\n >\r\n <path\r\n d=\"M10.0001 11.2289L6.68618 14.5428C6.51951 14.7095 6.31818 14.7895 6.08218 14.7828C5.84618 14.7755 5.64485 14.6885 5.47818 14.5218C5.31151 14.3552 5.22818 14.1505 5.22818 13.9078C5.22818 13.6645 5.31151 13.4595 5.47818 13.2928L8.77107 9.99992L5.45718 6.68603C5.29051 6.51936 5.21051 6.3147 5.21718 6.07203C5.22451 5.8287 5.31151 5.6237 5.47818 5.45703C5.64485 5.29036 5.84951 5.20703 6.09218 5.20703C6.33551 5.20703 6.54051 5.29036 6.70718 5.45703L10.0001 8.77092L13.314 5.45703C13.4806 5.29036 13.6853 5.20703 13.928 5.20703C14.1713 5.20703 14.3763 5.29036 14.543 5.45703C14.7096 5.6237 14.793 5.8287 14.793 6.07203C14.793 6.3147 14.7096 6.51936 14.543 6.68603L11.2291 9.99992L14.543 13.3138C14.7096 13.4805 14.793 13.6818 14.793 13.9178C14.793 14.1538 14.7096 14.3552 14.543 14.5218C14.3763 14.6885 14.1713 14.7718 13.928 14.7718C13.6853 14.7718 13.4806 14.6885 13.314 14.5218L10.0001 11.2289Z\"\r\n fill=\"white\"\r\n />\r\n </svg>\r\n </button>\r\n </div>\r\n </div>\r\n}\r\n\r\n@if (isDevMode) {\r\n <app-filter-settings />\r\n}\r\n", styles: [".freeze-loader,.lbm-indicator{--lbmPositionTop: 18px;--lbmPositionLeft: 18px;--lbmPositionRight: 18px;--lbmIndicatorSize: 36px;--lbmIndicatorBorderRadius: 8px;--lbmIndicatorCloseSize: 20px;--lbmPadding: 10px;--lbmTextSize: 13px;--lbmLineHeight: 20px;--lbmTextColor: #ffffff;--lbmBackgroundColor: rgba(17, 24, 39, .7);position:absolute;top:var(--lbmPositionTop);left:var(--lbmPositionLeft);z-index:10;display:flex;justify-content:center;align-items:flex-start;min-width:var(--lbmIndicatorSize);min-height:var(--lbmIndicatorSize);overflow:hidden}.freeze-loader.expanded,.lbm-indicator.expanded{--lbmPositionTop: 8px;--lbmPositionLeft: 8px;--lbmPositionRight: 8px;width:auto;max-width:calc(100% - var(--lbmPositionLeft) - var(--lbmPositionRight));padding:var(--lbmPadding);background:var(--lbmBackgroundColor);-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);border-radius:var(--lbmIndicatorBorderRadius);gap:12px}.freeze-loader .lbm-icon,.lbm-indicator .lbm-icon{display:flex;width:var(--lbmIndicatorSize);height:var(--lbmIndicatorSize);padding:8px;color:#fff;background-color:var(--lbmBackgroundColor);border-radius:50%;flex-shrink:0;cursor:pointer}.freeze-loader .lbm-icon svg,.freeze-loader .lbm-icon img,.lbm-indicator .lbm-icon svg,.lbm-indicator .lbm-icon img{width:100%;height:100%}.freeze-loader .lbm-close,.lbm-indicator .lbm-close{position:absolute;top:0;right:0;width:var(--lbmIndicatorCloseSize);height:var(--lbmIndicatorCloseSize);background-color:transparent;border:none;padding:0;cursor:pointer}.freeze-loader .lbm-message,.lbm-indicator .lbm-message{position:relative;opacity:0;height:0;max-width:0;padding-right:24px;pointer-events:none;visibility:hidden;animation:closeMessage .3s forwards}.freeze-loader .lbm-message--open,.lbm-indicator .lbm-message--open{position:relative;visibility:visible;display:flex;align-items:center;align-self:center;pointer-events:all;gap:12px;max-width:fit-content;height:auto;opacity:1;animation:openMessage .3s forwards}.freeze-loader .lbm-description,.lbm-indicator .lbm-description{margin:0;font-size:var(--lbmTextSize);font-weight:400;line-height:var(--lbmLineHeight);color:var(--lbmTextColor)}.freeze-loader .lbm-learn-more,.lbm-indicator .lbm-learn-more{display:inline-flex;padding:0;font-size:var(--lbmTextSize);font-weight:400;line-height:var(--lbmLineHeight);color:var(--lbmTextColor);text-decoration-line:underline;background-color:transparent;border:none;cursor:pointer}.freeze-loader .lbm-learn-more:hover,.lbm-indicator .lbm-learn-more:hover{text-decoration:none}@keyframes openMessage{0%{opacity:0;max-width:0;height:0}10%{height:auto;max-width:fit-content}to{opacity:1;height:auto;max-width:fit-content}}@keyframes closeMessage{0%{opacity:1;max-width:fit-content}to{opacity:0;max-width:0}}\n"], dependencies: [{ kind: "component", type: FilterSettingsComponent, selector: "app-filter-settings" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
6445
6833
  }
6446
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: LowBandwidthDetectorComponent, decorators: [{
6834
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: LowBandwidthDetectorComponent, decorators: [{
6447
6835
  type: Component,
6448
6836
  args: [{ selector: 'app-low-bandwidth-detector', changeDetection: ChangeDetectionStrategy.OnPush, imports: [FilterSettingsComponent], template: "@if (isReducedQuality() || isLowBandwidth()) {\r\n <div\r\n [class.expanded]=\"isIndicatorExpanded() && isLowBandwidth()\"\r\n class=\"lbm-indicator freeze-loader\"\r\n >\r\n <div (click)=\"toggleIndicator(true)\" class=\"lbm-icon\">\r\n <svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 20 20\">\r\n <path\r\n fill=\"#85888E\"\r\n d=\"M7.189 3.605c-.73.145-1.438.35-2.126.614A14.412 14.412 0 0 0 .896 6.666a1.08 1.08 0 0 0-.375.844c0 .34.118.629.354.865s.524.36.865.375c.34.014.65-.09.927-.313a11.89 11.89 0 0 1 3.385-1.916 10.94 10.94 0 0 1 1.235-.375l-.098-2.541ZM7.385 8.708a9.107 9.107 0 0 0-2.906 1.48c-.264.194-.402.464-.416.812-.014.347.104.646.354.896.236.236.524.364.864.385.34.02.664-.073.97-.281.422-.29.878-.53 1.368-.721a2.545 2.545 0 0 1-.166-.814l-.068-1.757ZM12.386 11.267c.094-.25.15-.52.161-.802l.068-1.755a9.019 9.019 0 0 1 2.927 1.52c.264.193.4.46.406.801.007.34-.114.635-.364.885a1.213 1.213 0 0 1-.865.365A1.614 1.614 0 0 1 13.77 12a6.574 6.574 0 0 0-1.385-.733ZM12.713 6.146l.098-2.542c.73.146 1.438.351 2.127.615 1.541.59 2.93 1.406 4.166 2.447.25.223.379.5.386.834.007.333-.115.625-.365.875a1.253 1.253 0 0 1-.864.375c-.34.014-.65-.09-.927-.313a11.892 11.892 0 0 0-3.386-1.916 10.94 10.94 0 0 0-1.235-.375ZM8.813 16.187c.32.32.715.48 1.187.48.472 0 .868-.16 1.188-.48.32-.32.479-.715.479-1.187 0-.473-.16-.868-.48-1.188-.319-.32-.715-.479-1.187-.479-.472 0-.868.16-1.187.48-.32.319-.48.714-.48 1.187 0 .472.16.868.48 1.187Z\"\r\n />\r\n <path\r\n fill=\"#fff\"\r\n fill-rule=\"evenodd\"\r\n d=\"M10 .833c-.91 0-1.637.756-1.602 1.665l.304 7.92a1.299 1.299 0 0 0 2.596 0l.305-7.92A1.604 1.604 0 0 0 10 .833ZM8.813 16.187c.32.32.715.48 1.187.48.472 0 .868-.16 1.188-.48.32-.32.479-.715.479-1.187 0-.473-.16-.868-.48-1.188-.319-.32-.715-.479-1.187-.479-.472 0-.868.16-1.187.48-.32.319-.48.714-.48 1.187 0 .472.16.868.48 1.187Z\"\r\n clip-rule=\"evenodd\"\r\n />\r\n </svg>\r\n </div>\r\n\r\n <div\r\n [class.lbm-message--open]=\"isLowBandwidth() && isIndicatorExpanded()\"\r\n class=\"lbm-message\"\r\n >\r\n <p class=\"lbm-description\">\r\n Fluid Interactivity Modes were disabled due to an unstable connection.\r\n\r\n <button\r\n (click)=\"openLBMDialog()\"\r\n [attr.data-testid]=\"'learn-more-lbm'\"\r\n type=\"button\"\r\n class=\"lbm-learn-more\"\r\n >\r\n Learn more\r\n </button>\r\n </p>\r\n <button\r\n (click)=\"toggleIndicator(false)\"\r\n [attr.data-testid]=\"'close-lbm-indicator'\"\r\n type=\"button\"\r\n class=\"lbm-close\"\r\n >\r\n <svg\r\n xmlns=\"http://www.w3.org/2000/svg\"\r\n width=\"20\"\r\n height=\"20\"\r\n viewBox=\"0 0 20 20\"\r\n fill=\"none\"\r\n >\r\n <path\r\n d=\"M10.0001 11.2289L6.68618 14.5428C6.51951 14.7095 6.31818 14.7895 6.08218 14.7828C5.84618 14.7755 5.64485 14.6885 5.47818 14.5218C5.31151 14.3552 5.22818 14.1505 5.22818 13.9078C5.22818 13.6645 5.31151 13.4595 5.47818 13.2928L8.77107 9.99992L5.45718 6.68603C5.29051 6.51936 5.21051 6.3147 5.21718 6.07203C5.22451 5.8287 5.31151 5.6237 5.47818 5.45703C5.64485 5.29036 5.84951 5.20703 6.09218 5.20703C6.33551 5.20703 6.54051 5.29036 6.70718 5.45703L10.0001 8.77092L13.314 5.45703C13.4806 5.29036 13.6853 5.20703 13.928 5.20703C14.1713 5.20703 14.3763 5.29036 14.543 5.45703C14.7096 5.6237 14.793 5.8287 14.793 6.07203C14.793 6.3147 14.7096 6.51936 14.543 6.68603L11.2291 9.99992L14.543 13.3138C14.7096 13.4805 14.793 13.6818 14.793 13.9178C14.793 14.1538 14.7096 14.3552 14.543 14.5218C14.3763 14.6885 14.1713 14.7718 13.928 14.7718C13.6853 14.7718 13.4806 14.6885 13.314 14.5218L10.0001 11.2289Z\"\r\n fill=\"white\"\r\n />\r\n </svg>\r\n </button>\r\n </div>\r\n </div>\r\n}\r\n\r\n@if (isDevMode) {\r\n <app-filter-settings />\r\n}\r\n", styles: [".freeze-loader,.lbm-indicator{--lbmPositionTop: 18px;--lbmPositionLeft: 18px;--lbmPositionRight: 18px;--lbmIndicatorSize: 36px;--lbmIndicatorBorderRadius: 8px;--lbmIndicatorCloseSize: 20px;--lbmPadding: 10px;--lbmTextSize: 13px;--lbmLineHeight: 20px;--lbmTextColor: #ffffff;--lbmBackgroundColor: rgba(17, 24, 39, .7);position:absolute;top:var(--lbmPositionTop);left:var(--lbmPositionLeft);z-index:10;display:flex;justify-content:center;align-items:flex-start;min-width:var(--lbmIndicatorSize);min-height:var(--lbmIndicatorSize);overflow:hidden}.freeze-loader.expanded,.lbm-indicator.expanded{--lbmPositionTop: 8px;--lbmPositionLeft: 8px;--lbmPositionRight: 8px;width:auto;max-width:calc(100% - var(--lbmPositionLeft) - var(--lbmPositionRight));padding:var(--lbmPadding);background:var(--lbmBackgroundColor);-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);border-radius:var(--lbmIndicatorBorderRadius);gap:12px}.freeze-loader .lbm-icon,.lbm-indicator .lbm-icon{display:flex;width:var(--lbmIndicatorSize);height:var(--lbmIndicatorSize);padding:8px;color:#fff;background-color:var(--lbmBackgroundColor);border-radius:50%;flex-shrink:0;cursor:pointer}.freeze-loader .lbm-icon svg,.freeze-loader .lbm-icon img,.lbm-indicator .lbm-icon svg,.lbm-indicator .lbm-icon img{width:100%;height:100%}.freeze-loader .lbm-close,.lbm-indicator .lbm-close{position:absolute;top:0;right:0;width:var(--lbmIndicatorCloseSize);height:var(--lbmIndicatorCloseSize);background-color:transparent;border:none;padding:0;cursor:pointer}.freeze-loader .lbm-message,.lbm-indicator .lbm-message{position:relative;opacity:0;height:0;max-width:0;padding-right:24px;pointer-events:none;visibility:hidden;animation:closeMessage .3s forwards}.freeze-loader .lbm-message--open,.lbm-indicator .lbm-message--open{position:relative;visibility:visible;display:flex;align-items:center;align-self:center;pointer-events:all;gap:12px;max-width:fit-content;height:auto;opacity:1;animation:openMessage .3s forwards}.freeze-loader .lbm-description,.lbm-indicator .lbm-description{margin:0;font-size:var(--lbmTextSize);font-weight:400;line-height:var(--lbmLineHeight);color:var(--lbmTextColor)}.freeze-loader .lbm-learn-more,.lbm-indicator .lbm-learn-more{display:inline-flex;padding:0;font-size:var(--lbmTextSize);font-weight:400;line-height:var(--lbmLineHeight);color:var(--lbmTextColor);text-decoration-line:underline;background-color:transparent;border:none;cursor:pointer}.freeze-loader .lbm-learn-more:hover,.lbm-indicator .lbm-learn-more:hover{text-decoration:none}@keyframes openMessage{0%{opacity:0;max-width:0;height:0}10%{height:auto;max-width:fit-content}to{opacity:1;height:auto;max-width:fit-content}}@keyframes closeMessage{0%{opacity:1;max-width:fit-content}to{opacity:0;max-width:0}}\n"] }]
6449
6837
  }] });
@@ -6468,20 +6856,14 @@ function provideUnrealModule() {
6468
6856
  AFKService,
6469
6857
  SignallingService,
6470
6858
  StreamStatusTelemetryService,
6859
+ StreamStatusTelemetryService2,
6471
6860
  FpsMonitorService,
6472
- {
6473
- provide: WebRtcPlayerService,
6474
- useFactory: () => {
6475
- const config = inject(UNREAL_CONFIG, { optional: true });
6476
- return config?.newWebRtcConnector
6477
- ? new WebRtcPlayerService2()
6478
- : new WebRtcPlayerService();
6479
- },
6480
- },
6861
+ WebRtcPlayerService,
6481
6862
  provideEnvironmentInitializer(() => {
6482
6863
  inject(AggregatorService);
6483
6864
  inject(InputService);
6484
6865
  inject(StreamStatusTelemetryService);
6866
+ inject(StreamStatusTelemetryService2);
6485
6867
  inject(ConsoleExtensionsService);
6486
6868
  inject(AFKService);
6487
6869
  inject(FreezeFrameService);
@@ -6495,5 +6877,5 @@ function provideUnrealModule() {
6495
6877
  * Generated bundle index. Do not edit.
6496
6878
  */
6497
6879
 
6498
- export { AFKService, AfkTimeoutModalComponent, AggregatorService, AnalyticsService, AnswerHandler, CONSOLE_COMMAND_DISABLE_MESSAGES, CONSOLE_COMMAND_ENABLE_MESSAGES, CONSOLE_COMMAND_PIXEL_QUALITY, CommandTelemetryService, CommandsEffects, 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, DataFlowMonitor, DevModeService, DisconnectReason, FULL_HD_HEIGHT, FULL_HD_WIDTH, FileHandlerService, FileReceiverService, FilterSettingsComponent, FpsMonitorService, FreezeFrameComponent, FreezeFrameService, IceCandidateHandler, ImageLoadingSrcComponent, InputOptions, InputService, InstanceReadyHandler, InstanceReservedHandler, IntroSrcComponent, KalmanFilter1D, LatencyTimings, LowBandwidthDetectorComponent, LowBandwidthModalComponent, MINIMAL_FPS, 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_VIDEO_ID, SafePipe, SignallingService, SpecialKeyCodes, StatGraphComponent, StreamStatusTelemetryService, 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, WebRtcPlayerService2, WebrtcErrorModalComponent, abortEstablishingConnection, changeLowBandwidth, changeStatusMainVideoOnScene, changeStreamResolutionAction, changeStreamResolutionSuccessAction, commandCompleted, commandStarted, commandsFeature, commandsReducer, dataChannelConnected, dataChannelReady, decodeData, destroyRemoteConnections, destroyUnrealScene, disconnectStream, dispatchResize, dropConnection, dropDC, dropPC, floatToSmoothPercents, forceResizeViewport, fromResizeObserver, getActiveUrl, getImageFromVideoStream, getResizeStudioValues, getResizeStudioValuesOld, getResizeValuesUniversal, getResizeValuesUniversalOld, getRtcErrorMessage, iceConnectionFailed, initSignalling, keepMaxUntilReset, makeEven, provideUnrealModule, qpToQuality, reconnectPeer, reconnectPeerFailed, reconnectPeerSuccess, resetAfk, resetConfig, resetDataChannelForReconnect, resetIntroSrc, resetWarnTimeout, saveAnalyticsEvent, selectClientAndViewIds, selectCommandProgress, selectCommandsInProgress, selectFreezeFrameCombinedDataUrl, selectFreezeFrameDataUrl, selectFreezeFrameDataUrlFromVideo, selectFreezeFrameProgressMessageFromVideo, selectIsAutostart, selectIsExistMatchUrls, selectIsFreezeFrameLoading, selectIsLoaderScreenVisible, selectIsVideoPlayingAndDataChannelConnected, selectShowLoader, selectShowReconnectPopup, selectSignalingParameters, selectStreamConfig, selectTotalProgress, selectWarnTimeout, setAfkTimerCountdown, setAfkTimerHide, setAfkTimerVisible, setAwsInstance, setCirrusConnected, setCirrusDisconnected, setConfig, setDataChannelConnected, setEstablishingConnection, setFreezeFrame, setFreezeFrameFromVideo, setIntroImageSrc, setIntroVideoSrc, setKalmanSettings, setLoadingImageSrc, setLoopBackCommandIsCompleted, setMaxFps, setOrchestrationContext, setOrchestrationMessage, setOrchestrationParameters, setOrchestrationProgress, setSignalingName, setStatusMessage, setStatusPercentSignallingServer, setStreamClientCompanyId, setStreamViewId, setUnrealPlaywrightConfig, setViewportNotReady, setViewportReady, showUnrealErrorMessage, smoothTransition, startStream, trackMixpanelEvent, unrealCommandCallback, unrealFeature, unrealReducer, updateCirrusInfo, videoAdaptedToContainer, videoStreamInitialized };
6880
+ export { AFKService, AfkTimeoutModalComponent, AggregatorService, AnalyticsService, AnswerHandler, CONSOLE_COMMAND_DISABLE_MESSAGES, CONSOLE_COMMAND_ENABLE_MESSAGES, CONSOLE_COMMAND_PIXEL_QUALITY, CommandTelemetryService, CommandsEffects, 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, DataFlowMonitor, DevModeService, DisconnectReason, FULL_HD_HEIGHT, FULL_HD_WIDTH, FileHandlerService, FileReceiverService, FilterSettingsComponent, FpsMonitorService, FreezeFrameComponent, FreezeFrameService, IceCandidateHandler, ImageLoadingSrcComponent, InputOptions, InputService, InstanceReadyHandler, InstanceReservedHandler, IntroSrcComponent, KalmanFilter1D, LatencyTimings, LowBandwidthDetectorComponent, LowBandwidthModalComponent, MINIMAL_FPS, 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_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, changeLowBandwidth, changeStatusMainVideoOnScene, changeStreamResolutionAction, changeStreamResolutionSuccessAction, commandCompleted, commandStarted, commandsFeature, commandsReducer, dataChannelConnected, dataChannelReady, decodeData, destroyRemoteConnections, destroyUnrealScene, disconnectStream, dispatchResize, dropConnection, dropDC, dropPC, floatToSmoothPercents, forceResizeViewport, fromResizeObserver, getActiveUrl, getImageFromVideoStream, getResizeStudioValues, getResizeStudioValuesOld, getResizeValuesUniversal, getResizeValuesUniversalOld, getRtcErrorMessage, iceConnectionFailed, initSignalling, instanceReady, keepMaxUntilReset, makeEven, pollingEnded, pollingStarted, provideUnrealModule, qpToQuality, reconnectPeer, reconnectPeerFailed, reconnectPeerSuccess, regionPingFetchEnd, regionPingFetchStart, regionResolved, resetAfk, resetConfig, resetDataChannelForReconnect, resetIntroSrc, resetWarnTimeout, saveAnalyticsEvent, selectCirrusConnectionId, selectClientAndViewIds, selectCommandProgress, selectCommandsInProgress, selectFreezeFrameCombinedDataUrl, selectFreezeFrameDataUrl, selectFreezeFrameDataUrlFromVideo, selectFreezeFrameProgressMessageFromVideo, selectIsAutostart, selectIsExistMatchUrls, selectIsFreezeFrameLoading, selectIsLoaderScreenVisible, selectIsVideoPlayingAndDataChannelConnected, selectShowLoader, selectShowReconnectPopup, selectSignalingParameters, selectStreamConfig, selectTotalProgress, selectWarnTimeout, setAfkTimerCountdown, setAfkTimerHide, setAfkTimerVisible, setAwsInstance, setCirrusConnected, setCirrusDisconnected, setConfig, setDataChannelConnected, setEstablishingConnection, setFreezeFrame, setFreezeFrameFromVideo, setIntroImageSrc, setIntroVideoSrc, setKalmanSettings, setLoadingImageSrc, setLoopBackCommandIsCompleted, setMaxFps, setOrchestrationContext, setOrchestrationMessage, setOrchestrationParameters, setOrchestrationProgress, setSignalingName, setStatusMessage, setStatusPercentSignallingServer, setStreamClientCompanyId, setStreamViewId, setUnrealPlaywrightConfig, setViewportNotReady, setViewportReady, showUnrealErrorMessage, smoothTransition, startStream, trackMixpanelEvent, trackTelemetryData, unrealCommandCallback, unrealFeature, unrealReducer, updateCirrusInfo, videoAdaptedToContainer, videoStreamInitialized };
6499
6881
  //# sourceMappingURL=3dsource-angular-unreal-module.mjs.map