@3dsource/angular-unreal-module 0.0.156 → 0.0.158

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,9 +1,9 @@
1
1
  import * as i0 from '@angular/core';
2
- import { InjectionToken, inject, DestroyRef, ChangeDetectionStrategy, Component, Injectable, signal, effect, untracked, computed, SecurityContext, Pipe, input, ElementRef, viewChild, afterNextRender, HostListener, output, makeEnvironmentProviders, provideEnvironmentInitializer } from '@angular/core';
2
+ import { InjectionToken, inject, DestroyRef, ChangeDetectionStrategy, Component, Injectable, signal, effect, untracked, computed, input, ElementRef, viewChild, afterNextRender, HostListener, SecurityContext, Pipe, output, makeEnvironmentProviders, provideEnvironmentInitializer } from '@angular/core';
3
3
  import { SourceButtonComponent, SourceLoadingComponent } from '@3dsource/source-ui-native';
4
- import { Subject, skip, share, merge, map, scan, timestamp, startWith, pairwise, concatMap, of, animationFrameScheduler, interval, Observable, timer, take, defer, switchMap, from, throwError, EMPTY, retry, timeout as timeout$1, repeat, BehaviorSubject, takeUntil as takeUntil$1, fromEvent, tap as tap$1, throttleTime, delay, concat, debounceTime, mergeMap, ignoreElements, shareReplay, NEVER, asyncScheduler, filter as filter$1, withLatestFrom as withLatestFrom$1, Subscription, bufferTime, combineLatestWith, distinctUntilChanged as distinctUntilChanged$1 } from 'rxjs';
4
+ import { Subject, skip, share, merge, map, scan, timestamp, startWith, pairwise, concatMap, of, animationFrameScheduler, interval, Observable, timer, take, defer, switchMap, from, throwError, EMPTY, retry, timeout as timeout$1, repeat, BehaviorSubject, takeUntil as takeUntil$1, fromEvent, tap as tap$1, throttleTime, delay, concat, debounceTime, mergeMap, NEVER, ignoreElements, shareReplay, asyncScheduler, filter as filter$1, withLatestFrom as withLatestFrom$1, Subscription, bufferTime, combineLatestWith } from 'rxjs';
5
5
  import { createAction, props, createReducer, on, createFeature, Store, createSelector, provideState } from '@ngrx/store';
6
- import { whereNot, Falsy, Logger, calculateMedian, clampf, lerp, getCanvasCached, getSnapshot, fitIntoRectangle, generateUuid, Truthy, tapLog, Semaphore, COLOR_CODES, where, KeyboardNumericCode, InvertedKeyMap, HEXtoRGB, RGBtoHSV, inverseLerp, HSVtoRGB, RGBtoHEX, fpIsASameAsB } from '@3dsource/utils';
6
+ import { whereNot, Falsy, Logger, clampf, lerp, getCanvasCached, getSnapshot, fitIntoRectangle, generateUuid, Truthy, tapLog, Semaphore, COLOR_CODES, where, KeyboardNumericCode, InvertedKeyMap, HEXtoRGB, RGBtoHSV, inverseLerp, HSVtoRGB, RGBtoHEX, fpIsASameAsB } from '@3dsource/utils';
7
7
  import { filter, map as map$1, distinctUntilChanged, takeWhile, takeUntil, catchError, tap, timeout, finalize, first, switchMap as switchMap$1, withLatestFrom, exhaustMap, debounceTime as debounceTime$1, take as take$1, startWith as startWith$1 } from 'rxjs/operators';
8
8
  import { Actions, ofType, createEffect, provideEffects } from '@ngrx/effects';
9
9
  import { DialogRef, DIALOG_DATA, Dialog } from '@angular/cdk/dialog';
@@ -11,11 +11,8 @@ import { HttpClient } from '@angular/common/http';
11
11
  import { ScrollStrategyOptions } from '@angular/cdk/overlay';
12
12
  import { MetaBoxCommand, EPayloadVersion } from '@3dsource/types-unreal';
13
13
  import { toSignal, toObservable, takeUntilDestroyed } from '@angular/core/rxjs-interop';
14
- import { v4 } from 'uuid';
15
- import { DomSanitizer } from '@angular/platform-browser';
16
14
  import { NgOptimizedImage, JsonPipe } from '@angular/common';
17
- import * as i1 from '@angular/forms';
18
- import { FormsModule } from '@angular/forms';
15
+ import { DomSanitizer } from '@angular/platform-browser';
19
16
 
20
17
  const SpecialKeyCodes = {
21
18
  BackSpace: 8,
@@ -872,85 +869,6 @@ function decodeData(anyData) {
872
869
  }
873
870
  }
874
871
 
875
- class DataFlowMonitor {
876
- /**
877
- * Initializes the DataFlowMonitor monitor.
878
- * @param yellowFlagThresholdPercentage - The percentage drop to trigger a YELLOW warning (default: 15%).
879
- * @param redFlagThresholdPercentage - The percentage drop to trigger a RED warning (default: 30%).
880
- * @param historyBufferLength - buffer length (default: 100).
881
- * @param splitPoint - The point at which to split the history buffer into two halves (default: 0.5).
882
- */
883
- constructor(yellowFlagThresholdPercentage = 15, redFlagThresholdPercentage = 30, historyBufferLength = 100, splitPoint = 0.5) {
884
- this.yellowFlagThresholdPercentage = yellowFlagThresholdPercentage;
885
- this.redFlagThresholdPercentage = redFlagThresholdPercentage;
886
- this.historyBufferLength = historyBufferLength;
887
- this.splitPoint = splitPoint;
888
- this.dataHistory = [];
889
- }
890
- reset() {
891
- this.dataHistory.length = 0;
892
- }
893
- config(data) {
894
- this.reset();
895
- this.yellowFlagThresholdPercentage = data.yellowFlag;
896
- this.redFlagThresholdPercentage = data.redFlag;
897
- }
898
- /**
899
- * Adds a new bitrate measurement and checks for significant drops.
900
- * @param currentValue - The current bitrate in kbps.
901
- * @returns BitrateCheckResult indicating if a drop was detected.
902
- */
903
- addValue(currentValue) {
904
- if (isNaN(currentValue) || currentValue < 1) {
905
- return {};
906
- }
907
- const historyLength = this.dataHistory.length;
908
- if (historyLength > this.historyBufferLength) {
909
- this.dataHistory.shift();
910
- }
911
- this.dataHistory.push(currentValue);
912
- if (historyLength < this.historyBufferLength) {
913
- this.dataHistory.length = this.historyBufferLength;
914
- this.dataHistory.fill(currentValue, 0, this.historyBufferLength);
915
- }
916
- const splitIndex = Math.floor(historyLength * this.splitPoint);
917
- const firstHalf = this.dataHistory.slice(0, splitIndex);
918
- const secondHalf = this.dataHistory.slice(splitIndex);
919
- const firstHalfMedian = calculateMedian(firstHalf);
920
- const secondHalfMedian = calculateMedian(secondHalf);
921
- const dropPercentage = clampf(0, 100, ((firstHalfMedian - secondHalfMedian) / firstHalfMedian) * 100);
922
- const isRedFlag = dropPercentage >= this.redFlagThresholdPercentage;
923
- if (dropPercentage >= this.yellowFlagThresholdPercentage) {
924
- return {
925
- config: {
926
- yellowFlagThresholdPercentage: this.yellowFlagThresholdPercentage,
927
- redFlagThresholdPercentage: this.redFlagThresholdPercentage,
928
- historyBufferLength: this.historyBufferLength,
929
- splitPoint: this.splitPoint,
930
- },
931
- isDropDetected: true,
932
- dropPercentage,
933
- activeMedian: secondHalfMedian,
934
- quality: isRedFlag ? 'red' : 'orange',
935
- message: `Significant flow drop detected: ${dropPercentage.toFixed(2)}% (from ${firstHalfMedian} to ${secondHalfMedian})`,
936
- };
937
- }
938
- return {
939
- config: {
940
- yellowFlagThresholdPercentage: this.yellowFlagThresholdPercentage,
941
- redFlagThresholdPercentage: this.redFlagThresholdPercentage,
942
- historyBufferLength: this.historyBufferLength,
943
- splitPoint: this.splitPoint,
944
- },
945
- isDropDetected: false,
946
- dropPercentage,
947
- activeMedian: secondHalfMedian,
948
- quality: 'lime',
949
- message: 'Stable flow',
950
- };
951
- }
952
- }
953
-
954
872
  const dispatchResize = () => {
955
873
  window.dispatchEvent(new Event('resize'));
956
874
  };
@@ -1033,51 +951,6 @@ function* getActiveUrl(urls) {
1033
951
  }
1034
952
  }
1035
953
 
1036
- class KalmanFilter1D {
1037
- /**
1038
- * @param initialEstimate Initial guess of the state.
1039
- * @param initialUncertainty Initial uncertainty in the guess.
1040
- * @param processNoise Process variance (Q) representing model uncertainty.
1041
- * @param measurementNoise Measurement variance (R) representing noise in measurements.
1042
- */
1043
- constructor(initialEstimate, initialUncertainty, processNoise, measurementNoise) {
1044
- this.estimate = initialEstimate;
1045
- this.uncertainty = initialUncertainty;
1046
- this.processNoise = processNoise;
1047
- this.measurementNoise = measurementNoise;
1048
- }
1049
- /**
1050
- * Incorporates a new measurement and returns the updated estimate.
1051
- * @param measurement The new measurement value.
1052
- */
1053
- update(measurement) {
1054
- // === Prediction Step ===
1055
- // For a constant model, the predicted state is the previous estimate.
1056
- const predictedEstimate = this.estimate;
1057
- // Increase the uncertainty by the process noise.
1058
- const predictedUncertainty = this.uncertainty + this.processNoise;
1059
- // === Update Step ===
1060
- // Compute the innovation or measurement residual.
1061
- const innovation = measurement - predictedEstimate;
1062
- // Compute the innovation covariance.
1063
- const innovationCovariance = predictedUncertainty + this.measurementNoise;
1064
- // Compute the Kalman Gain.
1065
- const kalmanGain = predictedUncertainty / innovationCovariance;
1066
- // Update the estimate with the measurement, weighted by the Kalman Gain.
1067
- this.estimate = predictedEstimate + kalmanGain * innovation;
1068
- // Update the uncertainty.
1069
- this.uncertainty = (1 - kalmanGain) * predictedUncertainty;
1070
- return this.estimate;
1071
- }
1072
- config(data) {
1073
- const { initialBitrateEstimate, initialErrorCovariance, processNoise, measurementNoise, } = data;
1074
- this.estimate = initialBitrateEstimate;
1075
- this.uncertainty = initialErrorCovariance;
1076
- this.processNoise = processNoise;
1077
- this.measurementNoise = measurementNoise;
1078
- }
1079
- }
1080
-
1081
954
  class LatencyTimings {
1082
955
  constructor() {
1083
956
  this.TestStartTimeMs = null;
@@ -2565,30 +2438,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImpor
2565
2438
  args: [{ providedIn: 'root' }]
2566
2439
  }], ctorParameters: () => [] });
2567
2440
 
2568
- /**
2569
- * Default LBM Filter Parameters
2570
- */
2571
- const DefaultFilterModel = {
2572
- minimumBitrate: 1600,
2573
- yellowFlag: 30,
2574
- redFlag: 50,
2575
- minimumFps: 5,
2576
- monitoringDelayTime: 5000,
2577
- initialBitrateEstimate: 1600,
2578
- initialErrorCovariance: 1,
2579
- processNoise: 3,
2580
- measurementNoise: 500,
2581
- panelOpen: false,
2582
- };
2583
- /**
2584
- * Global LBM Filter Parameters
2585
- */
2586
- const FilterModel = { ...DefaultFilterModel };
2587
- /**
2588
- * Bitrate Monitor Static Class
2589
- */
2590
- const BITRATE_MONITOR = new DataFlowMonitor(FilterModel.yellowFlag, FilterModel.redFlag, 20);
2591
-
2592
2441
  class VideoService extends SubService {
2593
2442
  constructor() {
2594
2443
  super();
@@ -2606,15 +2455,13 @@ class VideoService extends SubService {
2606
2455
  this.videoTrack$ = new BehaviorSubject(null);
2607
2456
  this.VideoEncoderQP = 0;
2608
2457
  this.aggregatedStats = {};
2609
- this.kalmanFilter1D = new KalmanFilter1D(FilterModel.initialBitrateEstimate, FilterModel.initialErrorCovariance, FilterModel.processNoise, FilterModel.measurementNoise);
2610
2458
  /**
2611
2459
  * Aggregate video stats and emit it as videoStats$
2612
2460
  */
2613
2461
  this.videoStats$ = this.videoTrack$.pipe(
2614
2462
  // If videoTrack$ emits null, switch to EMPTY → the current interval is canceled.
2615
2463
  switchMap$1((track) => track
2616
- ? // IMPORTANT! DO NOT CHANGE THOSE NUMBERS, LBM Stats are based on those values
2617
- interval(250).pipe(map(() => track.pcClient), switchMap$1((pcClient) => from(this.getStats(pcClient))), filter(Truthy))
2464
+ ? interval(250).pipe(map(() => track.pcClient), switchMap$1((pcClient) => from(this.getStats(pcClient))), filter(Truthy))
2618
2465
  : of(null)), filter(Truthy), share());
2619
2466
  this.destroyAbort = new AbortController();
2620
2467
  this.destroyRef.onDestroy(() => this.destroy());
@@ -2843,9 +2690,6 @@ class VideoService extends SubService {
2843
2690
  }
2844
2691
  // Calculate duration of run
2845
2692
  newStat.runTime = Math.floor((newStat.timestamp - newStat.timestampStart) / 1000);
2846
- newStat.kalmanBitrate = Math.floor(this.kalmanFilter1D.update(newStat.bitrate || FilterModel.initialBitrateEstimate));
2847
- newStat.dataFlowCheckResult = BITRATE_MONITOR.addValue(newStat.kalmanBitrate);
2848
- newStat.bitrateDrop = Math.floor(newStat.dataFlowCheckResult.dropPercentage);
2849
2693
  this.aggregatedStats = newStat;
2850
2694
  return this.onAggregatedStats(newStat);
2851
2695
  }
@@ -2882,8 +2726,6 @@ class VideoService extends SubService {
2882
2726
  // Reset stat state to prevent stale deltas on next session
2883
2727
  this.aggregatedStats = {};
2884
2728
  this.VideoEncoderQP = 0;
2885
- this.kalmanFilter1D = new KalmanFilter1D(FilterModel.initialBitrateEstimate, FilterModel.initialErrorCovariance, FilterModel.processNoise, FilterModel.measurementNoise);
2886
- BITRATE_MONITOR.reset();
2887
2729
  while (this.container?.firstChild) {
2888
2730
  this.container.removeChild(this.container.firstChild);
2889
2731
  }
@@ -3354,9 +3196,7 @@ class WebRtcPlayerService extends SubService {
3354
3196
  dc.onmessage = null;
3355
3197
  dc.onopen = null;
3356
3198
  dc.onclose = null;
3357
- // Adoption announces the already-open channel manually below. Do not
3358
- // subscribe to a native `open` event that may still be queued.
3359
- this.dcClient = this.setupDataChannelCallbacks(dc, false);
3199
+ this.dcClient = this.setupDataChannelCallbacks(dc);
3360
3200
  for (const buffered of dcBuffer) {
3361
3201
  this.onDataChannelMessage$.next(buffered);
3362
3202
  }
@@ -3500,7 +3340,7 @@ class WebRtcPlayerService extends SubService {
3500
3340
  }
3501
3341
  }
3502
3342
  }
3503
- setupDataChannelCallbacks(datachannel, subscribeToOpen = true) {
3343
+ setupDataChannelCallbacks(datachannel) {
3504
3344
  try {
3505
3345
  Logger.log(`[DATACHANNEL] Setting up datachannel "${datachannel.label}" callbacks...`);
3506
3346
  this.commandTelemetryService.trackStartCommand('EXT-DataChannel Connection');
@@ -3523,16 +3363,14 @@ class WebRtcPlayerService extends SubService {
3523
3363
  const message = `[DATACHANNEL] Data channel "${datachannel.label}" error. id:${datachannel.id}`;
3524
3364
  Logger.error(message, e);
3525
3365
  });
3526
- if (subscribeToOpen) {
3527
- fromEvent(datachannel, 'open')
3528
- .pipe(tap$1(() => {
3529
- Logger.info(`[DATACHANNEL] Data channel "${datachannel.label}" connected. id:${datachannel.id}`);
3530
- }), takeUntil(merge(this.disconnect$, this.destroy$)))
3531
- .subscribe(() => {
3532
- this.commandTelemetryService.trackStopCommand('EXT-DataChannel Connection');
3533
- this.onDataChannelConnected();
3534
- });
3535
- }
3366
+ fromEvent(datachannel, 'open')
3367
+ .pipe(tap$1(() => {
3368
+ Logger.info(`[DATACHANNEL] Data channel "${datachannel.label}" connected. id:${datachannel.id}`);
3369
+ }), takeUntil(merge(this.disconnect$, this.destroy$)))
3370
+ .subscribe(() => {
3371
+ this.commandTelemetryService.trackStopCommand('EXT-DataChannel Connection');
3372
+ this.onDataChannelConnected();
3373
+ });
3536
3374
  fromEvent(datachannel, 'message')
3537
3375
  .pipe(
3538
3376
  /* tap((e: MessageEvent) => {
@@ -4081,8 +3919,25 @@ class UnrealEffects {
4081
3919
  data: { content },
4082
3920
  }).closed), map(() => showUnrealErrorMessage({ code: null })));
4083
3921
  });
3922
+ /**
3923
+ * `dataChannelReady` is a ONCE-PER-SESSION signal: it starts app init
3924
+ * (console commands → loopback → viewport) and makes `SignallingService`
3925
+ * send `p2pEstablished`. Neither is idempotent, so a second
3926
+ * `dataChannelConnected` inside the same session must not re-emit it.
3927
+ *
3928
+ * That second announcement is real in the prefetch flow: `adopt()` declares
3929
+ * the already-open channel manually, while the transport it adopted can
3930
+ * still produce its own `dataChannelConnected` afterwards — the effect used
3931
+ * to turn both into `dataChannelReady`.
3932
+ *
3933
+ * `exhaustMap` over an inner stream that emits once and then idles keeps the
3934
+ * gate shut until the session actually ends; `takeUntil` re-opens it on a
3935
+ * cirrus drop, a scene destroy, or the start of a peer reconnect (which is
3936
+ * where the pre-existing `isReconnecting` filter hands over).
3937
+ */
4084
3938
  this.unrealReceiverReady$ = createEffect(() => {
4085
- return this.actions$.pipe(ofType(dataChannelConnected), withLatestFrom(this.store.select(unrealFeature.selectIsReconnecting)), filter(([, isReconnecting]) => !isReconnecting), map(() => dataChannelReady()));
3939
+ const sessionEnded$ = this.actions$.pipe(ofType(setCirrusDisconnected, destroyUnrealScene, resetDataChannelForReconnect));
3940
+ return this.actions$.pipe(ofType(dataChannelConnected), withLatestFrom(this.store.select(unrealFeature.selectIsReconnecting)), filter(([, isReconnecting]) => !isReconnecting), exhaustMap(() => concat(of(dataChannelReady()), NEVER).pipe(takeUntil(sessionEnded$))));
4086
3941
  });
4087
3942
  this.initAppByDataChannelReady$ = createEffect(() => {
4088
3943
  return this.actions$.pipe(ofType(dataChannelReady), tap(() => {
@@ -5558,6 +5413,33 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImpor
5558
5413
  args: [{ providedIn: 'root' }]
5559
5414
  }], ctorParameters: () => [] });
5560
5415
 
5416
+ /**
5417
+ * Pro-grade telemetry for the WebRTC stream lifecycle.
5418
+ *
5419
+ * Wire-level event taxonomy (intentionally small — all phase-specific
5420
+ * details ride inside `stream.phase_ended`'s payload):
5421
+ * - `stream.session_started` — once per session, when a real connection
5422
+ * attempt begins (`setEstablishingConnection(true)`)
5423
+ * - `stream.phase_ended` — once per closer in {@link phaseCatalog};
5424
+ * carries phaseNo / id / color /
5425
+ * milestones / params / overlays
5426
+ * - `stream.sample` — every 5s, window-aggregated (p. 50/p. 95/min/max)
5427
+ * - `stream.quality_degraded` — on QP entering `red` (>35)
5428
+ * - `stream.quality_recovered` — on QP leaving `red`
5429
+ * - `stream.fps_low` — when the window FPS p50 < 70% of the target
5430
+ * - `stream.session_ended` — once per session, on disconnect/pagehide/restart
5431
+ *
5432
+ * Every event carries `sessionId` + `userId`. Samples carry a monotonic
5433
+ * `seq` for server-side ordering & dedup. Durations use `performance.now()`
5434
+ * — immune to wall-clock jumps.
5435
+ *
5436
+ * NOTE: For `stream.session_ended` on the tab close to survive teardown,
5437
+ * the Mixpanel client must be configured with `api_transport: 'sendBeacon'`.
5438
+ *
5439
+ * Every wire shape lives in `interfaces/stream-telemetry-events.interface.ts`
5440
+ * — `track()` / `emitLifecycle()` are typed against it, so a payload that
5441
+ * drifts from the contract is a compile error.
5442
+ */
5561
5443
  /** Trim a resource URL to its path: drop origin and query string so the
5562
5444
  * emitted name is short, stable, and not a PII-bearing query. */
5563
5445
  function shortResourceName(url) {
@@ -5860,14 +5742,14 @@ class StreamStatusTelemetryService2 {
5860
5742
  const existing = localStorage.getItem(USER_ID_STORAGE_KEY);
5861
5743
  if (existing && existing.length > 0)
5862
5744
  return existing;
5863
- const fresh = v4();
5745
+ const fresh = generateUuid();
5864
5746
  localStorage.setItem(USER_ID_STORAGE_KEY, fresh);
5865
5747
  return fresh;
5866
5748
  }
5867
5749
  catch {
5868
5750
  // localStorage disabled (private-mode restrictions, quota, etc.)
5869
5751
  // Fall back to a per-tab ephemeral id so telemetry keeps working.
5870
- return v4();
5752
+ return generateUuid();
5871
5753
  }
5872
5754
  }
5873
5755
  init() {
@@ -5920,7 +5802,7 @@ class StreamStatusTelemetryService2 {
5920
5802
  startSession() {
5921
5803
  // If a previous session is still active, close it first.
5922
5804
  this.endSession('restart');
5923
- const id = v4();
5805
+ const id = generateUuid();
5924
5806
  const startedAt = Date.now();
5925
5807
  const startedAtPerf = performance.now();
5926
5808
  const sub = new Subscription();
@@ -6129,6 +6011,8 @@ class StreamStatusTelemetryService2 {
6129
6011
  // anchors were already captured at the top of `startSession()`.
6130
6012
  const now = atPerf ?? performance.now();
6131
6013
  const duration = Math.round(now - this.session.lastMilestonePerf);
6014
+ // Re-adding the omitted `duration` reconstructs the full payload, but TS
6015
+ // can't verify that through the generic key — assert instead.
6132
6016
  this.track(event, { duration, ...extra });
6133
6017
  this.session.lastMilestonePerf = now;
6134
6018
  }
@@ -7124,11 +7008,11 @@ class AfkTimeoutModalComponent {
7124
7008
  this.#afkService.reset();
7125
7009
  }
7126
7010
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AfkTimeoutModalComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
7127
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.8", type: AfkTimeoutModalComponent, isStandalone: true, selector: "app-afk-timeout-modal", ngImport: i0, template: "@if (isShowAfkPopup()) {\n <div\n (click)=\"resetAfk()\"\n (keydown.enter)=\"resetAfk()\"\n class=\"clickableState\"\n id=\"videoPlayOverlay\"\n role=\"button\"\n tabindex=\"0\"\n >\n <div class=\"resume-box\">\n <div aria-hidden=\"true\" class=\"resume-box__pic\" role=\"presentation\">\n <div [innerHTML]=\"afkCountdown()\" class=\"text-number\"></div>\n </div>\n <div class=\"resume-box__text\">\n <h3 class=\"resume-box__heading\">Session will time out soon</h3>\n <p>\n No activity detected. Press 'Continue' if you wish to keep your\n session active\n </p>\n </div>\n <src-button\n [isFullWidth]=\"true\"\n [data-testid]=\"'continue-session'\"\n weight=\"primary\"\n size=\"lg\"\n >\n Continue\n </src-button>\n </div>\n </div>\n}\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 }); }
7011
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.8", type: AfkTimeoutModalComponent, isStandalone: true, selector: "app-afk-timeout-modal", ngImport: i0, template: "@if (isShowAfkPopup()) {\n <div (click)=\"resetAfk()\" class=\"clickableState\" id=\"videoPlayOverlay\">\n <div class=\"resume-box\">\n <div aria-hidden=\"true\" class=\"resume-box__pic\" role=\"presentation\">\n <div [innerHTML]=\"afkCountdown()\" class=\"text-number\"></div>\n </div>\n <div class=\"resume-box__text\">\n <h3 class=\"resume-box__heading\">Session will time out soon</h3>\n <p>\n No activity detected. Press 'Continue' if you wish to keep your\n session active\n </p>\n </div>\n <src-button\n [isFullWidth]=\"true\"\n [data-testid]=\"'continue-session'\"\n weight=\"primary\"\n size=\"lg\"\n >\n Continue\n </src-button>\n </div>\n </div>\n}\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 }); }
7128
7012
  }
7129
7013
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AfkTimeoutModalComponent, decorators: [{
7130
7014
  type: Component,
7131
- args: [{ selector: 'app-afk-timeout-modal', changeDetection: ChangeDetectionStrategy.OnPush, imports: [SourceButtonComponent], template: "@if (isShowAfkPopup()) {\n <div\n (click)=\"resetAfk()\"\n (keydown.enter)=\"resetAfk()\"\n class=\"clickableState\"\n id=\"videoPlayOverlay\"\n role=\"button\"\n tabindex=\"0\"\n >\n <div class=\"resume-box\">\n <div aria-hidden=\"true\" class=\"resume-box__pic\" role=\"presentation\">\n <div [innerHTML]=\"afkCountdown()\" class=\"text-number\"></div>\n </div>\n <div class=\"resume-box__text\">\n <h3 class=\"resume-box__heading\">Session will time out soon</h3>\n <p>\n No activity detected. Press 'Continue' if you wish to keep your\n session active\n </p>\n </div>\n <src-button\n [isFullWidth]=\"true\"\n [data-testid]=\"'continue-session'\"\n weight=\"primary\"\n size=\"lg\"\n >\n Continue\n </src-button>\n </div>\n </div>\n}\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"] }]
7015
+ args: [{ selector: 'app-afk-timeout-modal', changeDetection: ChangeDetectionStrategy.OnPush, imports: [SourceButtonComponent], template: "@if (isShowAfkPopup()) {\n <div (click)=\"resetAfk()\" class=\"clickableState\" id=\"videoPlayOverlay\">\n <div class=\"resume-box\">\n <div aria-hidden=\"true\" class=\"resume-box__pic\" role=\"presentation\">\n <div [innerHTML]=\"afkCountdown()\" class=\"text-number\"></div>\n </div>\n <div class=\"resume-box__text\">\n <h3 class=\"resume-box__heading\">Session will time out soon</h3>\n <p>\n No activity detected. Press 'Continue' if you wish to keep your\n session active\n </p>\n </div>\n <src-button\n [isFullWidth]=\"true\"\n [data-testid]=\"'continue-session'\"\n weight=\"primary\"\n size=\"lg\"\n >\n Continue\n </src-button>\n </div>\n </div>\n}\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"] }]
7132
7016
  }] });
7133
7017
 
7134
7018
  class FreezeFrameComponent {
@@ -7146,57 +7030,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImpor
7146
7030
  args: [{ selector: 'app-freeze-frame', changeDetection: ChangeDetectionStrategy.OnPush, template: "@let image = combinedFreeze();\n@let progress = freezeFrameProgressMessageFromVideo();\n\n<div class=\"freeze-images\">\n @if (image) {\n <img [src]=\"image\" class=\"videoStillImage\" alt=\"freezeFrameImage\" />\n }\n\n @if (progress) {\n <div class=\"progress-status\">\n {{ progress }}\n </div>\n }\n</div>\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"] }]
7147
7031
  }] });
7148
7032
 
7149
- class SafePipe {
7150
- constructor() {
7151
- this.sanitizer = inject(DomSanitizer);
7152
- }
7153
- transform(value, type) {
7154
- if (!value) {
7155
- return value;
7156
- }
7157
- const context = (() => {
7158
- switch (type) {
7159
- case 'html':
7160
- return SecurityContext.HTML;
7161
- case 'style':
7162
- return SecurityContext.STYLE;
7163
- case 'script':
7164
- return SecurityContext.SCRIPT;
7165
- case 'url':
7166
- return SecurityContext.URL;
7167
- case 'resourceUrl':
7168
- return SecurityContext.RESOURCE_URL;
7169
- default:
7170
- throw new Error(`Invalid safe type specified: ${type}`);
7171
- }
7172
- })();
7173
- // Preserve the published return type while returning Angular-sanitized
7174
- // content instead of disabling framework security checks.
7175
- return (this.sanitizer.sanitize(context, value) ?? '');
7176
- }
7177
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: SafePipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
7178
- static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "22.0.8", ngImport: i0, type: SafePipe, isStandalone: true, name: "safe" }); }
7179
- }
7180
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: SafePipe, decorators: [{
7181
- type: Pipe,
7182
- args: [{
7183
- name: 'safe',
7184
- }]
7185
- }] });
7186
-
7187
- class LowBandwidthModalComponent {
7188
- #dialogRef = inject(DialogRef);
7189
- close(value) {
7190
- this.#dialogRef.close(value || false);
7191
- }
7192
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: LowBandwidthModalComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
7193
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "22.0.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\">\n <header class=\"src-modal__header\">\n <h6 [attr.data-testid]=\"'title'\" class=\"src-modal__title\">\n Unstable Connection\n </h6>\n </header>\n <section class=\"src-modal__body\">\n <div\n [innerHtml]=\"\n 'Fluid Interactivity Modes were disabled due to an unstable connection. Showcase Gallery Mode is enabled. To regain full functionality, switch to Interactive Mode.'\n | safe: 'html'\n \"\n class=\"src-modal__scroll-box\"\n ></div>\n </section>\n <footer class=\"src-modal__footer\">\n <div class=\"src-modal__buttons\">\n <src-button\n (onClick)=\"close(true)\"\n [data-testid]=\"'switch-to-interactive-mode'\"\n >\n Switch\n </src-button>\n\n <src-button\n (onClick)=\"close()\"\n [data-testid]=\"'close-lbm-modal'\"\n weight=\"primary\"\n >\n Ok\n </src-button>\n </div>\n </footer>\n</div>\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 }); }
7194
- }
7195
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: LowBandwidthModalComponent, decorators: [{
7196
- type: Component,
7197
- 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\">\n <header class=\"src-modal__header\">\n <h6 [attr.data-testid]=\"'title'\" class=\"src-modal__title\">\n Unstable Connection\n </h6>\n </header>\n <section class=\"src-modal__body\">\n <div\n [innerHtml]=\"\n 'Fluid Interactivity Modes were disabled due to an unstable connection. Showcase Gallery Mode is enabled. To regain full functionality, switch to Interactive Mode.'\n | safe: 'html'\n \"\n class=\"src-modal__scroll-box\"\n ></div>\n </section>\n <footer class=\"src-modal__footer\">\n <div class=\"src-modal__buttons\">\n <src-button\n (onClick)=\"close(true)\"\n [data-testid]=\"'switch-to-interactive-mode'\"\n >\n Switch\n </src-button>\n\n <src-button\n (onClick)=\"close()\"\n [data-testid]=\"'close-lbm-modal'\"\n weight=\"primary\"\n >\n Ok\n </src-button>\n </div>\n </footer>\n</div>\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"] }]
7198
- }] });
7199
-
7200
7033
  class ImageLoadingSrcComponent {
7201
7034
  constructor() {
7202
7035
  this.#store = inject(Store);
@@ -7325,6 +7158,44 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImpor
7325
7158
  args: [{ selector: 'app-unreal-status', changeDetection: ChangeDetectionStrategy.OnPush, imports: [SourceLoadingComponent], template: "@if (isShowLoadingPercentsPopup()) {\n @let _awsInstance = awsInstance();\n <div class=\"status-box\">\n @if (_awsInstance?.message) {\n <p>{{ _awsInstance?.message }}</p>\n } @else {\n <div class=\"status-box__message\">\n <src-loading\n [size]=\"30\"\n [backgroundStrokeColor]=\"'#E5E7EB'\"\n [progressStrokeColor]=\"'#1F2937'\"\n />\n {{ smoothPercents() }}%\n </div>\n }\n </div>\n}\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"] }]
7326
7159
  }] });
7327
7160
 
7161
+ class SafePipe {
7162
+ constructor() {
7163
+ this.sanitizer = inject(DomSanitizer);
7164
+ }
7165
+ transform(value, type) {
7166
+ if (!value) {
7167
+ return value;
7168
+ }
7169
+ const context = (() => {
7170
+ switch (type) {
7171
+ case 'html':
7172
+ return SecurityContext.HTML;
7173
+ case 'style':
7174
+ return SecurityContext.STYLE;
7175
+ case 'script':
7176
+ return SecurityContext.SCRIPT;
7177
+ case 'url':
7178
+ return SecurityContext.URL;
7179
+ case 'resourceUrl':
7180
+ return SecurityContext.RESOURCE_URL;
7181
+ default:
7182
+ throw new Error(`Invalid safe type specified: ${type}`);
7183
+ }
7184
+ })();
7185
+ // Preserve the published return type while returning Angular-sanitized
7186
+ // content instead of disabling framework security checks.
7187
+ return (this.sanitizer.sanitize(context, value) ?? '');
7188
+ }
7189
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: SafePipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
7190
+ static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "22.0.8", ngImport: i0, type: SafePipe, isStandalone: true, name: "safe" }); }
7191
+ }
7192
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: SafePipe, decorators: [{
7193
+ type: Pipe,
7194
+ args: [{
7195
+ name: 'safe',
7196
+ }]
7197
+ }] });
7198
+
7328
7199
  class VideoStatsComponent {
7329
7200
  constructor() {
7330
7201
  this.collapsed = signal(false, /* @ts-ignore */
@@ -7362,8 +7233,6 @@ class VideoStatsComponent {
7362
7233
  }, /* @ts-ignore */
7363
7234
  ...(ngDevMode ? [{ debugName: "videoStatus" }] : /* istanbul ignore next */ []));
7364
7235
  this.fields = [
7365
- 'kalmanBitrate',
7366
- 'bitrateDrop',
7367
7236
  'runTime',
7368
7237
  'pixelRatio',
7369
7238
  'frameHeight',
@@ -7434,11 +7303,11 @@ class VideoStatsComponent {
7434
7303
  }));
7435
7304
  }
7436
7305
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: VideoStatsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
7437
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.8", type: VideoStatsComponent, isStandalone: true, selector: "app-video-stats", ngImport: i0, template: "@if (videoStatus() && viewportReady()) {\n <div class=\"settings-container\">\n <button (click)=\"toggle()\" class=\"gear-button\">Stats</button>\n <div\n (click)=\"toggle()\"\n (keydown.enter)=\"toggle()\"\n [class.min]=\"collapsed()\"\n id=\"aggregatedStats\"\n role=\"button\"\n tabindex=\"0\"\n >\n <div class=\"forNerds\">\n <div\n (click)=\"$event.stopPropagation()\"\n (keydown)=\"$event.stopPropagation()\"\n class=\"static\"\n role=\"group\"\n tabindex=\"0\"\n >\n <li>\n <app-stat-graph\n [stat]=\"fpsTick()!\"\n label=\"FPS (higher is better)\"\n />\n </li>\n <li>\n <app-stat-graph\n [stat]=\"videoQP()!\"\n label=\"QP (lower is better)\"\n color=\"#D5ff07\"\n />\n </li>\n <li>\n <app-stat-graph\n [stat]=\"bitrateTick()!\"\n label=\"Bitrate\"\n color=\"#D57F07\"\n />\n </li>\n </div>\n <div>\n @for (graph of graphList(); track graph.key) {\n <li\n (click)=\"toggleGraph($event, graph.key, 0)\"\n (keydown.enter)=\"toggleGraph($event, graph.key, 0)\"\n class=\"graph\"\n role=\"button\"\n tabindex=\"0\"\n >\n <app-stat-graph\n [label]=\"graph.key\"\n [stat]=\"graph.stat()\"\n [color]=\"graph.color\"\n />\n </li>\n }\n </div>\n </div>\n\n @for (el of videoStatus(); track el.key + $index) {\n <div\n (click)=\"toggleGraph($event, el.key, el.value)\"\n (keydown.enter)=\"toggleGraph($event, el.key, el.value)\"\n class=\"stat\"\n role=\"button\"\n tabindex=\"0\"\n >\n <span>{{ el.key }}: </span>{{ el.value | json }}\n </div>\n }\n </div>\n </div>\n @if (!collapsed()) {\n <div [innerHTML]=\"ssData() | safe: 'html'\" class=\"ssInfo\"></div>\n }\n}\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: JsonPipe, name: "json" }, { kind: "pipe", type: SafePipe, name: "safe" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
7306
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.8", type: VideoStatsComponent, isStandalone: true, selector: "app-video-stats", ngImport: i0, template: "@if (videoStatus() && viewportReady()) {\n <div class=\"settings-container\">\n <button (click)=\"toggle()\" class=\"gear-button\">Stats</button>\n <div (click)=\"toggle()\" [class.min]=\"collapsed()\" id=\"aggregatedStats\">\n <div class=\"forNerds\">\n <div (click)=\"$event.stopPropagation()\" class=\"static\">\n <li>\n <app-stat-graph\n [stat]=\"fpsTick()!\"\n label=\"FPS (higher is better)\"\n />\n </li>\n <li>\n <app-stat-graph\n [stat]=\"videoQP()!\"\n label=\"QP (lower is better)\"\n color=\"#D5ff07\"\n />\n </li>\n <li>\n <app-stat-graph\n [stat]=\"bitrateTick()!\"\n label=\"Bitrate\"\n color=\"#D57F07\"\n />\n </li>\n </div>\n <div>\n @for (graph of graphList(); track graph.key) {\n <li (click)=\"toggleGraph($event, graph.key, 0)\" class=\"graph\">\n <app-stat-graph\n [label]=\"graph.key\"\n [stat]=\"graph.stat()\"\n [color]=\"graph.color\"\n />\n </li>\n }\n </div>\n </div>\n\n @for (el of videoStatus(); track el.key + $index) {\n <div (click)=\"toggleGraph($event, el.key, el.value)\" class=\"stat\">\n <span>{{ el.key }}: </span>{{ el.value | json }}\n </div>\n }\n </div>\n </div>\n @if (!collapsed()) {\n <div [innerHTML]=\"ssData() | safe: 'html'\" class=\"ssInfo\"></div>\n }\n}\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: JsonPipe, name: "json" }, { kind: "pipe", type: SafePipe, name: "safe" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
7438
7307
  }
7439
7308
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: VideoStatsComponent, decorators: [{
7440
7309
  type: Component,
7441
- args: [{ selector: 'app-video-stats', changeDetection: ChangeDetectionStrategy.OnPush, imports: [StatGraphComponent, JsonPipe, SafePipe], template: "@if (videoStatus() && viewportReady()) {\n <div class=\"settings-container\">\n <button (click)=\"toggle()\" class=\"gear-button\">Stats</button>\n <div\n (click)=\"toggle()\"\n (keydown.enter)=\"toggle()\"\n [class.min]=\"collapsed()\"\n id=\"aggregatedStats\"\n role=\"button\"\n tabindex=\"0\"\n >\n <div class=\"forNerds\">\n <div\n (click)=\"$event.stopPropagation()\"\n (keydown)=\"$event.stopPropagation()\"\n class=\"static\"\n role=\"group\"\n tabindex=\"0\"\n >\n <li>\n <app-stat-graph\n [stat]=\"fpsTick()!\"\n label=\"FPS (higher is better)\"\n />\n </li>\n <li>\n <app-stat-graph\n [stat]=\"videoQP()!\"\n label=\"QP (lower is better)\"\n color=\"#D5ff07\"\n />\n </li>\n <li>\n <app-stat-graph\n [stat]=\"bitrateTick()!\"\n label=\"Bitrate\"\n color=\"#D57F07\"\n />\n </li>\n </div>\n <div>\n @for (graph of graphList(); track graph.key) {\n <li\n (click)=\"toggleGraph($event, graph.key, 0)\"\n (keydown.enter)=\"toggleGraph($event, graph.key, 0)\"\n class=\"graph\"\n role=\"button\"\n tabindex=\"0\"\n >\n <app-stat-graph\n [label]=\"graph.key\"\n [stat]=\"graph.stat()\"\n [color]=\"graph.color\"\n />\n </li>\n }\n </div>\n </div>\n\n @for (el of videoStatus(); track el.key + $index) {\n <div\n (click)=\"toggleGraph($event, el.key, el.value)\"\n (keydown.enter)=\"toggleGraph($event, el.key, el.value)\"\n class=\"stat\"\n role=\"button\"\n tabindex=\"0\"\n >\n <span>{{ el.key }}: </span>{{ el.value | json }}\n </div>\n }\n </div>\n </div>\n @if (!collapsed()) {\n <div [innerHTML]=\"ssData() | safe: 'html'\" class=\"ssInfo\"></div>\n }\n}\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"] }]
7310
+ args: [{ selector: 'app-video-stats', changeDetection: ChangeDetectionStrategy.OnPush, imports: [StatGraphComponent, JsonPipe, SafePipe], template: "@if (videoStatus() && viewportReady()) {\n <div class=\"settings-container\">\n <button (click)=\"toggle()\" class=\"gear-button\">Stats</button>\n <div (click)=\"toggle()\" [class.min]=\"collapsed()\" id=\"aggregatedStats\">\n <div class=\"forNerds\">\n <div (click)=\"$event.stopPropagation()\" class=\"static\">\n <li>\n <app-stat-graph\n [stat]=\"fpsTick()!\"\n label=\"FPS (higher is better)\"\n />\n </li>\n <li>\n <app-stat-graph\n [stat]=\"videoQP()!\"\n label=\"QP (lower is better)\"\n color=\"#D5ff07\"\n />\n </li>\n <li>\n <app-stat-graph\n [stat]=\"bitrateTick()!\"\n label=\"Bitrate\"\n color=\"#D57F07\"\n />\n </li>\n </div>\n <div>\n @for (graph of graphList(); track graph.key) {\n <li (click)=\"toggleGraph($event, graph.key, 0)\" class=\"graph\">\n <app-stat-graph\n [label]=\"graph.key\"\n [stat]=\"graph.stat()\"\n [color]=\"graph.color\"\n />\n </li>\n }\n </div>\n </div>\n\n @for (el of videoStatus(); track el.key + $index) {\n <div (click)=\"toggleGraph($event, el.key, el.value)\" class=\"stat\">\n <span>{{ el.key }}: </span>{{ el.value | json }}\n </div>\n }\n </div>\n </div>\n @if (!collapsed()) {\n <div [innerHTML]=\"ssData() | safe: 'html'\" class=\"ssInfo\"></div>\n }\n}\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"] }]
7442
7311
  }], ctorParameters: () => [] });
7443
7312
 
7444
7313
  class ResumeStreamButtonComponent {
@@ -7793,185 +7662,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImpor
7793
7662
  args: ['contextmenu', ['$event']]
7794
7663
  }] } });
7795
7664
 
7796
- class FilterSettingsComponent {
7797
- constructor() {
7798
- this.storageKey = 'LBMFilterSettings';
7799
- this.defaultFilterModel = { ...DefaultFilterModel };
7800
- this.settings = { ...DefaultFilterModel };
7801
- }
7802
- ngOnInit() {
7803
- const savedSettings = localStorage.getItem(this.storageKey);
7804
- if (savedSettings) {
7805
- this.dispatch(JSON.parse(savedSettings));
7806
- }
7807
- }
7808
- togglePanel() {
7809
- this.settings.panelOpen = !this.settings.panelOpen;
7810
- this.saveSettings();
7811
- }
7812
- saveSettings() {
7813
- const valid = Object.entries(this.settings).reduce((acc, [key, value]) => {
7814
- return Truthy(value) ? { ...acc, [key]: value } : acc;
7815
- }, {});
7816
- this.dispatch({ ...DefaultFilterModel, ...valid });
7817
- }
7818
- dispatch(settings) {
7819
- try {
7820
- localStorage.setItem(this.storageKey, JSON.stringify(settings));
7821
- }
7822
- catch (e) {
7823
- Logger.error('Storage not available, Error saving filter settings:', e);
7824
- }
7825
- Object.assign(this.settings, settings);
7826
- Object.assign(FilterModel, settings);
7827
- }
7828
- reset() {
7829
- this.dispatch({
7830
- ...DefaultFilterModel,
7831
- panelOpen: this.settings.panelOpen,
7832
- });
7833
- }
7834
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: FilterSettingsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
7835
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.8", type: FilterSettingsComponent, isStandalone: true, selector: "app-filter-settings", ngImport: i0, template: "<div class=\"settings-container\">\n <button (click)=\"togglePanel()\" class=\"gear-button\">LBM</button>\n @if (settings.panelOpen) {\n <div class=\"settings-panel\">\n <h3>LBM Trigger Settings</h3>\n <h5>Data Flow Monitor</h5>\n <form>\n <label>\n Monitoring delay:\n <input\n [(ngModel)]=\"settings.monitoringDelayTime\"\n [placeholder]=\"defaultFilterModel.monitoringDelayTime\"\n type=\"number\"\n name=\"monitoringDelayTime\"\n />\n </label>\n\n <label>\n Minimum Bitrate (kbit/s):\n <input\n [(ngModel)]=\"settings.minimumBitrate\"\n [placeholder]=\"defaultFilterModel.minimumBitrate\"\n type=\"number\"\n name=\"minimumBitrate\"\n />\n </label>\n <label>\n Yellow Flag (%):\n <input\n [(ngModel)]=\"settings.yellowFlag\"\n [placeholder]=\"defaultFilterModel.yellowFlag\"\n type=\"number\"\n name=\"yellowFlag\"\n />\n </label>\n <label>\n Red Flag (%):\n <input\n [(ngModel)]=\"settings.redFlag\"\n [placeholder]=\"defaultFilterModel.redFlag\"\n type=\"number\"\n name=\"redFlag\"\n />\n </label>\n <label>\n Minimum FPS:\n <input\n [(ngModel)]=\"settings.minimumFps\"\n [placeholder]=\"defaultFilterModel.minimumFps\"\n type=\"number\"\n name=\"minimumFps\"\n />\n </label>\n <hr />\n <h5>Kalman Filter</h5>\n <label>\n Initial Bitrate Estimate (kbit/s):\n <input\n [(ngModel)]=\"settings.initialBitrateEstimate\"\n [placeholder]=\"defaultFilterModel.initialBitrateEstimate\"\n type=\"number\"\n name=\"initialBitrateEstimate\"\n />\n </label>\n <label>\n Initial Error Covariance:\n <input\n [(ngModel)]=\"settings.initialErrorCovariance\"\n [placeholder]=\"defaultFilterModel.initialErrorCovariance\"\n type=\"number\"\n name=\"initialErrorCovariance\"\n />\n </label>\n <label>\n Process Noise (Q):\n <input\n [(ngModel)]=\"settings.processNoise\"\n [placeholder]=\"defaultFilterModel.processNoise\"\n type=\"number\"\n name=\"processNoise\"\n />\n </label>\n <label>\n Measurement Noise (R):\n <input\n [(ngModel)]=\"settings.measurementNoise\"\n [placeholder]=\"defaultFilterModel.measurementNoise\"\n type=\"number\"\n name=\"measurementNoise\"\n />\n </label>\n </form>\n <br />\n <br />\n <div class=\"apply-button-container\">\n <button (click)=\"reset()\">Reset</button>\n <button (click)=\"saveSettings()\">Apply</button>\n </div>\n </div>\n }\n</div>\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]):not([ngNoCva])[formControlName],textarea:not([ngNoCva])[formControlName],input:not([type=checkbox]):not([ngNoCva])[formControl],textarea:not([ngNoCva])[formControl],input:not([type=checkbox]):not([ngNoCva])[ngModel],textarea:not([ngNoCva])[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NumberValueAccessor, selector: "input[type=number]:not([ngNoCva])[formControlName],input[type=number]:not([ngNoCva])[formControl],input[type=number]:not([ngNoCva])[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 }); }
7836
- }
7837
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: FilterSettingsComponent, decorators: [{
7838
- type: Component,
7839
- args: [{ selector: 'app-filter-settings', imports: [FormsModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"settings-container\">\n <button (click)=\"togglePanel()\" class=\"gear-button\">LBM</button>\n @if (settings.panelOpen) {\n <div class=\"settings-panel\">\n <h3>LBM Trigger Settings</h3>\n <h5>Data Flow Monitor</h5>\n <form>\n <label>\n Monitoring delay:\n <input\n [(ngModel)]=\"settings.monitoringDelayTime\"\n [placeholder]=\"defaultFilterModel.monitoringDelayTime\"\n type=\"number\"\n name=\"monitoringDelayTime\"\n />\n </label>\n\n <label>\n Minimum Bitrate (kbit/s):\n <input\n [(ngModel)]=\"settings.minimumBitrate\"\n [placeholder]=\"defaultFilterModel.minimumBitrate\"\n type=\"number\"\n name=\"minimumBitrate\"\n />\n </label>\n <label>\n Yellow Flag (%):\n <input\n [(ngModel)]=\"settings.yellowFlag\"\n [placeholder]=\"defaultFilterModel.yellowFlag\"\n type=\"number\"\n name=\"yellowFlag\"\n />\n </label>\n <label>\n Red Flag (%):\n <input\n [(ngModel)]=\"settings.redFlag\"\n [placeholder]=\"defaultFilterModel.redFlag\"\n type=\"number\"\n name=\"redFlag\"\n />\n </label>\n <label>\n Minimum FPS:\n <input\n [(ngModel)]=\"settings.minimumFps\"\n [placeholder]=\"defaultFilterModel.minimumFps\"\n type=\"number\"\n name=\"minimumFps\"\n />\n </label>\n <hr />\n <h5>Kalman Filter</h5>\n <label>\n Initial Bitrate Estimate (kbit/s):\n <input\n [(ngModel)]=\"settings.initialBitrateEstimate\"\n [placeholder]=\"defaultFilterModel.initialBitrateEstimate\"\n type=\"number\"\n name=\"initialBitrateEstimate\"\n />\n </label>\n <label>\n Initial Error Covariance:\n <input\n [(ngModel)]=\"settings.initialErrorCovariance\"\n [placeholder]=\"defaultFilterModel.initialErrorCovariance\"\n type=\"number\"\n name=\"initialErrorCovariance\"\n />\n </label>\n <label>\n Process Noise (Q):\n <input\n [(ngModel)]=\"settings.processNoise\"\n [placeholder]=\"defaultFilterModel.processNoise\"\n type=\"number\"\n name=\"processNoise\"\n />\n </label>\n <label>\n Measurement Noise (R):\n <input\n [(ngModel)]=\"settings.measurementNoise\"\n [placeholder]=\"defaultFilterModel.measurementNoise\"\n type=\"number\"\n name=\"measurementNoise\"\n />\n </label>\n </form>\n <br />\n <br />\n <div class=\"apply-button-container\">\n <button (click)=\"reset()\">Reset</button>\n <button (click)=\"saveSettings()\">Apply</button>\n </div>\n </div>\n }\n</div>\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"] }]
7840
- }] });
7841
-
7842
- class LowBandwidthStateService {
7843
- constructor() {
7844
- this.#isLowBandwidth = signal(false, /* @ts-ignore */
7845
- ...(ngDevMode ? [{ debugName: "#isLowBandwidth" }] : /* istanbul ignore next */ []));
7846
- this.isLowBandwidth = this.#isLowBandwidth.asReadonly();
7847
- this.isLowBandwidth$ = toObservable(this.isLowBandwidth);
7848
- }
7849
- #isLowBandwidth;
7850
- enterLowBandwidthMode() {
7851
- this.#isLowBandwidth.set(true);
7852
- }
7853
- exitLowBandwidthMode() {
7854
- this.#isLowBandwidth.set(false);
7855
- }
7856
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: LowBandwidthStateService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
7857
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: LowBandwidthStateService, providedIn: 'root' }); }
7858
- }
7859
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: LowBandwidthStateService, decorators: [{
7860
- type: Injectable,
7861
- args: [{ providedIn: 'root' }]
7862
- }] });
7863
-
7864
- class LowBandwidthDetectorComponent {
7865
- constructor() {
7866
- this.#store = inject(Store);
7867
- this.dialog = inject(Dialog);
7868
- this.destroyRef = inject(DestroyRef);
7869
- this.videoService = inject(VideoService);
7870
- this.lowBandwidthState = inject(LowBandwidthStateService);
7871
- this.isDevMode = inject(DevModeService).isDevMode;
7872
- this.viewportReady$ = this.#store.select(unrealFeature.selectViewportReady);
7873
- this.isLowBandwidth$ = this.lowBandwidthState.isLowBandwidth$;
7874
- this.lowBandwidthTrigger$ = this.isLowBandwidth$.pipe(distinctUntilChanged$1(), filter(Truthy));
7875
- this.highBandwidthTrigger$ = this.isLowBandwidth$.pipe(distinctUntilChanged$1(), filter(Falsy));
7876
- this.viewPortDestroyedTrigger$ = this.viewportReady$.pipe(distinctUntilChanged$1(), filter(Falsy));
7877
- this.aggregatedStats$ = this.videoService.videoStats$.pipe(map((data) => data.aggregatedStats), share());
7878
- this.nativeQuality$ = this.videoService.videoStats$.pipe(map((data) => data.quality));
7879
- this.monitoringTrigger$ = this.viewportReady$
7880
- .pipe(combineLatestWith(this.isLowBandwidth$), map(([viewPortReady, isLowBandwidth]) => viewPortReady && !isLowBandwidth))
7881
- .pipe(share());
7882
- this.reTrigger$ = this.monitoringTrigger$.pipe(map(() => ({ quality: 'lime' })));
7883
- this.canSwitchToLowBand$ = this.monitoringTrigger$.pipe(debounceTime(100), switchMap((canSwitch) => concat(of(false), timer(FilterModel.monitoringDelayTime).pipe(map(() => canSwitch))).pipe(take(2), takeUntil$1(this.monitoringTrigger$))), distinctUntilChanged$1(), share());
7884
- this.videoQuality$ = merge(this.nativeQuality$, this.reTrigger$).pipe(share(), takeUntil$1(this.viewPortDestroyedTrigger$));
7885
- this.fps$ = this.aggregatedStats$.pipe(map((data) => data.framesPerSecond || 0));
7886
- this.bitrateDrop$ = this.aggregatedStats$.pipe(map((data) => data.dataFlowCheckResult), filter((br) => br.isDropDetected), takeUntil$1(merge(this.viewPortDestroyedTrigger$, this.reTrigger$)));
7887
- this.fps = toSignal(this.fps$, { initialValue: 0 });
7888
- this.nativeQuality = toSignal(this.nativeQuality$);
7889
- this.isLowBandwidth = this.lowBandwidthState.isLowBandwidth;
7890
- this.aggregatedStats = toSignal(this.aggregatedStats$);
7891
- this.isReducedQuality = signal(false, /* @ts-ignore */
7892
- ...(ngDevMode ? [{ debugName: "isReducedQuality" }] : /* istanbul ignore next */ []));
7893
- this.isIndicatorExpanded = signal(false, /* @ts-ignore */
7894
- ...(ngDevMode ? [{ debugName: "isIndicatorExpanded" }] : /* istanbul ignore next */ []));
7895
- this.isIndicatorExpandedTrigger$ = toObservable(this.isIndicatorExpanded).pipe(skip(1), distinctUntilChanged$1(), filter(Falsy));
7896
- }
7897
- #store;
7898
- ngOnInit() {
7899
- this.lowBandwidthTrigger$
7900
- .pipe(takeUntilDestroyed(this.destroyRef))
7901
- .subscribe(() => this.toggleIndicator(true));
7902
- merge(this.viewPortDestroyedTrigger$, this.highBandwidthTrigger$)
7903
- .pipe(takeUntilDestroyed(this.destroyRef))
7904
- .subscribe(() => this.isReducedQuality.set(false));
7905
- this.canSwitchToLowBand$
7906
- .pipe(filter(Truthy), tap(() => {
7907
- BITRATE_MONITOR.reset();
7908
- this.isReducedQuality.set(false);
7909
- }), switchMap(() => this.bitrateDrop$), tap(this.trySetLowBandwidth.bind(this)), map((dataFlowResult) => dataFlowResult?.isDropDetected), distinctUntilChanged$1(), tap(() => {
7910
- this.isReducedQuality.set(true);
7911
- }), switchMap(() => this.videoQuality$), filter((quality) => quality === 'lime'), tap(() => this.isReducedQuality.set(false)), takeUntilDestroyed(this.destroyRef))
7912
- .subscribe();
7913
- }
7914
- trySetLowBandwidth(dataFlowResult) {
7915
- // Evaluate FPS and native quality
7916
- const isFpsLow = this.fps() < FilterModel.minimumFps;
7917
- const isNativeQualityRed = this.nativeQuality() === 'red';
7918
- const shouldSwitch = isNativeQualityRed || isFpsLow;
7919
- // Exit if conditions are not met or already in low bandwidth
7920
- if (!shouldSwitch || this.isLowBandwidth())
7921
- return;
7922
- const { quality, activeMedian } = dataFlowResult;
7923
- const isQualityRed = quality === 'red';
7924
- const isLowBitrate = activeMedian < FilterModel.minimumBitrate;
7925
- // Final check for a red drop or low bitrate, or low FPS
7926
- if (isQualityRed || isLowBitrate || isFpsLow) {
7927
- let reason;
7928
- if (isFpsLow) {
7929
- reason = `FPS < ${FilterModel.minimumFps}`;
7930
- }
7931
- else if (quality === 'red') {
7932
- reason = `Drop > ${FilterModel.redFlag}%`;
7933
- }
7934
- else {
7935
- reason = `Speed < ${FilterModel.minimumBitrate}`;
7936
- }
7937
- Logger.info(`LBM DETECTED: ${reason}, switching to low bandwidth; `, dataFlowResult.message, {
7938
- stats: {
7939
- dataFlowResult,
7940
- reason,
7941
- triggerTime: this.aggregatedStats()?.runTime ?? 0,
7942
- },
7943
- });
7944
- this.lowBandwidthState.enterLowBandwidthMode();
7945
- }
7946
- }
7947
- toggleIndicator(value) {
7948
- this.isIndicatorExpanded.set(value);
7949
- if (value) {
7950
- this.hideIndicatorAfterDelay();
7951
- }
7952
- }
7953
- hideIndicatorAfterDelay() {
7954
- timer(3000)
7955
- .pipe(take(1), takeUntil$1(this.isIndicatorExpandedTrigger$))
7956
- .subscribe(() => this.toggleIndicator(false));
7957
- }
7958
- openLBMDialog() {
7959
- this.dialog
7960
- .open(LowBandwidthModalComponent)
7961
- .closed.pipe(filter(Truthy))
7962
- .subscribe(() => {
7963
- this.lowBandwidthState.exitLowBandwidthMode();
7964
- this.toggleIndicator(false);
7965
- });
7966
- }
7967
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: LowBandwidthDetectorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
7968
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.8", type: LowBandwidthDetectorComponent, isStandalone: true, selector: "app-low-bandwidth-detector", ngImport: i0, template: "@if (isReducedQuality() || isLowBandwidth()) {\n <div\n [class.expanded]=\"isIndicatorExpanded() && isLowBandwidth()\"\n class=\"lbm-indicator freeze-loader\"\n >\n <button (click)=\"toggleIndicator(true)\" class=\"lbm-icon\" type=\"button\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 20 20\">\n <path\n fill=\"#85888E\"\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\"\n />\n <path\n fill=\"#fff\"\n fill-rule=\"evenodd\"\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\"\n clip-rule=\"evenodd\"\n />\n </svg>\n </button>\n\n <div\n [class.lbm-message--open]=\"isLowBandwidth() && isIndicatorExpanded()\"\n class=\"lbm-message\"\n >\n <p class=\"lbm-description\">\n Fluid Interactivity Modes were disabled due to an unstable connection.\n\n <button\n (click)=\"openLBMDialog()\"\n [attr.data-testid]=\"'learn-more-lbm'\"\n type=\"button\"\n class=\"lbm-learn-more\"\n >\n Learn more\n </button>\n </p>\n <button\n (click)=\"toggleIndicator(false)\"\n [attr.data-testid]=\"'close-lbm-indicator'\"\n type=\"button\"\n class=\"lbm-close\"\n >\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"20\"\n height=\"20\"\n viewBox=\"0 0 20 20\"\n fill=\"none\"\n >\n <path\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\"\n fill=\"white\"\n />\n </svg>\n </button>\n </div>\n </div>\n}\n\n@if (isDevMode) {\n <app-filter-settings />\n}\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 }); }
7969
- }
7970
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: LowBandwidthDetectorComponent, decorators: [{
7971
- type: Component,
7972
- args: [{ selector: 'app-low-bandwidth-detector', changeDetection: ChangeDetectionStrategy.OnPush, imports: [FilterSettingsComponent], template: "@if (isReducedQuality() || isLowBandwidth()) {\n <div\n [class.expanded]=\"isIndicatorExpanded() && isLowBandwidth()\"\n class=\"lbm-indicator freeze-loader\"\n >\n <button (click)=\"toggleIndicator(true)\" class=\"lbm-icon\" type=\"button\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 20 20\">\n <path\n fill=\"#85888E\"\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\"\n />\n <path\n fill=\"#fff\"\n fill-rule=\"evenodd\"\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\"\n clip-rule=\"evenodd\"\n />\n </svg>\n </button>\n\n <div\n [class.lbm-message--open]=\"isLowBandwidth() && isIndicatorExpanded()\"\n class=\"lbm-message\"\n >\n <p class=\"lbm-description\">\n Fluid Interactivity Modes were disabled due to an unstable connection.\n\n <button\n (click)=\"openLBMDialog()\"\n [attr.data-testid]=\"'learn-more-lbm'\"\n type=\"button\"\n class=\"lbm-learn-more\"\n >\n Learn more\n </button>\n </p>\n <button\n (click)=\"toggleIndicator(false)\"\n [attr.data-testid]=\"'close-lbm-indicator'\"\n type=\"button\"\n class=\"lbm-close\"\n >\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"20\"\n height=\"20\"\n viewBox=\"0 0 20 20\"\n fill=\"none\"\n >\n <path\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\"\n fill=\"white\"\n />\n </svg>\n </button>\n </div>\n </div>\n}\n\n@if (isDevMode) {\n <app-filter-settings />\n}\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"] }]
7973
- }] });
7974
-
7975
7665
  /**
7976
7666
  * Registers the Unreal NgRx feature state. Call this ONCE at the application
7977
7667
  * root.
@@ -8018,5 +7708,5 @@ function provideUnrealModule() {
8018
7708
  * Generated bundle index. Do not edit.
8019
7709
  */
8020
7710
 
8021
- export { AFKService, AUTO_HIGH_RESOLUTION_IDLE_MS, AfkTimeoutModalComponent, AggregatorService, AnalyticsService, AnswerHandler, CommandTelemetryService, ConfigHandler, ConsoleExtensionsService, ControlScheme, DATA_CHANNEL_CONNECTION_TIMEOUT, DEBOUNCE_TO_MANY_RESIZE_CALLS, DEFAULT_AFK_TIMEOUT, DEFAULT_AFK_TIMEOUT_PERIOD, DESTROY_DISCONNECT_REASONS, DataFlowMonitor, DevModeService, DisconnectReason, FULL_HD_HEIGHT, FULL_HD_WIDTH, FileHandlerService, FileReceiverService, FilterSettingsComponent, FpsMonitorService, FreezeFrameComponent, FreezeFrameService, INNER_RETRY_LIMIT, IceCandidateHandler, ImageLoadingSrcComponent, InputOptions, InputService, InstanceReadyHandler, InstanceReservedHandler, IntroSrcComponent, KalmanFilter1D, LatencyTimings, LoadingProgressService, LowBandwidthDetectorComponent, LowBandwidthModalComponent, LowBandwidthStateService, MINIMAL_FPS, MIN_CONTAINER_SIZE, MessageType, MouseButton, MouseButtonsMask, OUTER_RETRY_DELAY, OnCloseHandler, OnErrorHandler, OnMessageHandler, OnOpenHandler, OrchestrationMessageTypes, POLLING_TIME, PingHandler, PlayerCountHandler, RECONNECT_CONFIG, REGION_PING_ACCURATE_SAMPLES, REGION_PING_BACKGROUND_REFRESH_DELAY_MS, REGION_PING_CACHE_KEY, REGION_PING_CACHE_TTL_MS, REGION_PING_WARMUP_DEADLINE_MS, RegionsPingService, SAME_SIZE_THRESHOLD, SCREEN_LOCKER_CONTAINER_ID, SIGNALLING_PERCENT_VALUE, SSInfoHandler, STREAMING_AUDIO_ID, STREAMING_VIDEO_ID, SafePipe, SignallingService, SpecialKeyCodes, StatGraphComponent, StreamAdoptionService, StreamStatusTelemetryService, StreamStatusTelemetryService2, SubService, ToClientMessage, UNREAL_CONFIG, UnrealCallbackService, UnrealCommunicatorService, UnrealEffects, UnrealSceneComponent, UnrealStatusMessage, VideoService, VideoStatsComponent, WSCloseCode_CIRRUS_ABNORMAL_CLOSURE, WSCloseCode_CIRRUS_MAX_PLAYERS_ERROR, WSCloseCode_CIRRUS_PLAYER_DISCONNECTED, WSCloseCode_CIRRUS_STREAMER_KIKED_PLAYER, WSCloseCode_FORCE_CIRRUS_CLOSE, WSCloseCode_NORMAL_AFK_TIMEOUT, WSCloseCode_NORMAL_CLOSURE, WSCloseCode_NORMAL_MANUAL_DISCONNECT, WSCloseCode_UNKNOWN, WSCloseCodes, WS_OPEN_STATE, WS_RETRY_PER_INSTANCE, WS_TIMEOUT, WebRtcPlayerService, WebrtcErrorModalComponent, abortEstablishingConnection, answerSent, changeStatusMainVideoOnScene, changeStreamResolutionAction, commandCompleted, commandStarted, dataChannelConnected, dataChannelReady, decodeData, destroyRemoteConnections, destroyUnrealScene, disconnectStream, dispatchResize, dropConnection, dropDC, dropPC, floatToPercents, floatToSmoothPercents, forceResizeViewport, fromResizeObserver, getActiveUrl, getImageFromVideoStream, getResizeStudioValues, getResizeValuesUniversal, getRtcErrorMessage, iceConnectionFailed, initSignalling, instanceReady, keepMaxUntilReset, makeEven, pollingEnded, pollingStarted, provideUnrealModule, provideUnrealState, qpToQuality, reconnectPeer, reconnectPeerFailed, reconnectPeerSuccess, regionPingFetchEnd, regionPingFetchStart, regionResolved, resetAfk, resetConfig, resetDataChannelForReconnect, resetIntroSrc, resetUserInactivityTimeout, resetWarnTimeout, saveAnalyticsEvent, selectCirrusConnectionId, selectClientAndViewIds, selectCommandProgress, selectFreezeFrameCombinedDataUrl, selectFreezeFrameDataUrl, selectFreezeFrameDataUrlFromVideo, selectFreezeFrameProgressMessageFromVideo, selectIsAutostart, selectIsExistMatchUrls, selectIsFreezeFrameLoading, selectIsFullyConnected, selectIsLoaderScreenVisible, selectIsVideoPlayingAndDataChannelConnected, selectShowLoader, selectShowReconnectPopup, selectSignalingParameters, selectStreamConfig, selectTotalProgress, selectUserInactivityDetected, selectWarnTimeout, setAfkTimerCountdown, setAfkTimerHide, setAfkTimerVisible, setAwsInstance, setCirrusConnected, setCirrusDisconnected, setConfig, setEstablishingConnection, setFreezeFrame, setFreezeFrameFromVideo, setIntroImageSrc, setIntroVideoSrc, setLoadingImageSrc, setLoopBackCommandIsCompleted, setMaxFps, setOrchestrationContext, setOrchestrationMessage, setOrchestrationParameters, setOrchestrationProgress, setSignalingName, setStatusMessage, setStatusPercentSignallingServer, setStreamClientCompanyId, setStreamViewId, setUnrealPlaywrightConfig, setUserInactivityTimeout, setViewportNotReady, setViewportReady, showLoadingProgress, showResumeStreamPopup, showUnrealErrorMessage, showUserInactivityTimeoutPopup, signalingRetryAttempted, smoothTransition, startStream, streamAdopted, trackMixpanelEvent, trackTelemetryData, uint8ToBase64, unrealCommandCallback, unrealFeature, unrealReducer, updateCirrusInfo, videoAdaptedToContainer, videoStreamInitialized };
7711
+ export { AFKService, AUTO_HIGH_RESOLUTION_IDLE_MS, AfkTimeoutModalComponent, AggregatorService, AnalyticsService, AnswerHandler, CommandTelemetryService, ConfigHandler, ConsoleExtensionsService, ControlScheme, DATA_CHANNEL_CONNECTION_TIMEOUT, DEBOUNCE_TO_MANY_RESIZE_CALLS, DEFAULT_AFK_TIMEOUT, DEFAULT_AFK_TIMEOUT_PERIOD, DESTROY_DISCONNECT_REASONS, DevModeService, DisconnectReason, FULL_HD_HEIGHT, FULL_HD_WIDTH, FileHandlerService, FileReceiverService, FpsMonitorService, FreezeFrameComponent, FreezeFrameService, INNER_RETRY_LIMIT, IceCandidateHandler, ImageLoadingSrcComponent, InputOptions, InputService, InstanceReadyHandler, InstanceReservedHandler, IntroSrcComponent, LatencyTimings, LoadingProgressService, MINIMAL_FPS, MIN_CONTAINER_SIZE, MessageType, MouseButton, MouseButtonsMask, OUTER_RETRY_DELAY, OnCloseHandler, OnErrorHandler, OnMessageHandler, OnOpenHandler, OrchestrationMessageTypes, POLLING_TIME, PingHandler, PlayerCountHandler, RECONNECT_CONFIG, REGION_PING_ACCURATE_SAMPLES, REGION_PING_BACKGROUND_REFRESH_DELAY_MS, REGION_PING_CACHE_KEY, REGION_PING_CACHE_TTL_MS, REGION_PING_WARMUP_DEADLINE_MS, RegionsPingService, SAME_SIZE_THRESHOLD, SCREEN_LOCKER_CONTAINER_ID, SIGNALLING_PERCENT_VALUE, SSInfoHandler, STREAMING_AUDIO_ID, STREAMING_VIDEO_ID, SafePipe, SignallingService, SpecialKeyCodes, StatGraphComponent, StreamAdoptionService, StreamStatusTelemetryService, StreamStatusTelemetryService2, SubService, ToClientMessage, UNREAL_CONFIG, UnrealCallbackService, UnrealCommunicatorService, UnrealEffects, UnrealSceneComponent, UnrealStatusMessage, VideoService, VideoStatsComponent, WSCloseCode_CIRRUS_ABNORMAL_CLOSURE, WSCloseCode_CIRRUS_MAX_PLAYERS_ERROR, WSCloseCode_CIRRUS_PLAYER_DISCONNECTED, WSCloseCode_CIRRUS_STREAMER_KIKED_PLAYER, WSCloseCode_FORCE_CIRRUS_CLOSE, WSCloseCode_NORMAL_AFK_TIMEOUT, WSCloseCode_NORMAL_CLOSURE, WSCloseCode_NORMAL_MANUAL_DISCONNECT, WSCloseCode_UNKNOWN, WSCloseCodes, WS_OPEN_STATE, WS_RETRY_PER_INSTANCE, WS_TIMEOUT, WebRtcPlayerService, WebrtcErrorModalComponent, abortEstablishingConnection, answerSent, changeStatusMainVideoOnScene, changeStreamResolutionAction, commandCompleted, commandStarted, dataChannelConnected, dataChannelReady, decodeData, destroyRemoteConnections, destroyUnrealScene, disconnectStream, dispatchResize, dropConnection, dropDC, dropPC, floatToPercents, floatToSmoothPercents, forceResizeViewport, fromResizeObserver, getActiveUrl, getImageFromVideoStream, getResizeStudioValues, getResizeValuesUniversal, getRtcErrorMessage, iceConnectionFailed, initSignalling, instanceReady, keepMaxUntilReset, makeEven, pollingEnded, pollingStarted, provideUnrealModule, provideUnrealState, qpToQuality, reconnectPeer, reconnectPeerFailed, reconnectPeerSuccess, regionPingFetchEnd, regionPingFetchStart, regionResolved, resetAfk, resetConfig, resetDataChannelForReconnect, resetIntroSrc, resetUserInactivityTimeout, resetWarnTimeout, saveAnalyticsEvent, selectCirrusConnectionId, selectClientAndViewIds, selectCommandProgress, selectFreezeFrameCombinedDataUrl, selectFreezeFrameDataUrl, selectFreezeFrameDataUrlFromVideo, selectFreezeFrameProgressMessageFromVideo, selectIsAutostart, selectIsExistMatchUrls, selectIsFreezeFrameLoading, selectIsFullyConnected, selectIsLoaderScreenVisible, selectIsVideoPlayingAndDataChannelConnected, selectShowLoader, selectShowReconnectPopup, selectSignalingParameters, selectStreamConfig, selectTotalProgress, selectUserInactivityDetected, selectWarnTimeout, setAfkTimerCountdown, setAfkTimerHide, setAfkTimerVisible, setAwsInstance, setCirrusConnected, setCirrusDisconnected, setConfig, setEstablishingConnection, setFreezeFrame, setFreezeFrameFromVideo, setIntroImageSrc, setIntroVideoSrc, setLoadingImageSrc, setLoopBackCommandIsCompleted, setMaxFps, setOrchestrationContext, setOrchestrationMessage, setOrchestrationParameters, setOrchestrationProgress, setSignalingName, setStatusMessage, setStatusPercentSignallingServer, setStreamClientCompanyId, setStreamViewId, setUnrealPlaywrightConfig, setUserInactivityTimeout, setViewportNotReady, setViewportReady, showLoadingProgress, showResumeStreamPopup, showUnrealErrorMessage, showUserInactivityTimeoutPopup, signalingRetryAttempted, smoothTransition, startStream, streamAdopted, trackMixpanelEvent, trackTelemetryData, uint8ToBase64, unrealCommandCallback, unrealFeature, unrealReducer, updateCirrusInfo, videoAdaptedToContainer, videoStreamInitialized };
8022
7712
  //# sourceMappingURL=3dsource-angular-unreal-module.mjs.map