@3dsource/angular-unreal-module 0.0.187 → 0.0.189

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.
package/LICENSE CHANGED
File without changes
@@ -3,10 +3,10 @@ import { input, output, ChangeDetectionStrategy, Component, inject, DestroyRef,
3
3
  import { NgTemplateOutlet, NgOptimizedImage, JsonPipe } from '@angular/common';
4
4
  import { createAction, props, createReducer, on, createFeature, createSelector, Store, provideState } from '@ngrx/store';
5
5
  import { Actions, ofType, createEffect, provideEffects } from '@ngrx/effects';
6
- import { Subject, skip, share, timer, of, take, defer, switchMap, from, throwError, Observable, EMPTY, retry, merge, timeout as timeout$1, repeat, BehaviorSubject, interval, map as map$1, takeUntil as takeUntil$1, fromEvent, tap as tap$1, throttleTime, delay, concat, debounceTime, mergeMap, NEVER, ignoreElements, startWith, scan, timestamp, pairwise, concatMap, animationFrameScheduler, shareReplay, firstValueFrom, asyncScheduler, filter as filter$1, withLatestFrom as withLatestFrom$1, Subscription, bufferTime, combineLatestWith } from 'rxjs';
6
+ import { Subject, skip, share, timer, of, take, defer, switchMap, from, throwError, Observable, EMPTY, retry, merge, timeout as timeout$1, repeat, BehaviorSubject, interval, takeUntil as takeUntil$1, fromEvent, tap as tap$1, throttleTime, map as map$1, delay, concat, debounceTime, mergeMap, startWith, takeWhile, NEVER, ignoreElements, scan, timestamp, pairwise, concatMap, animationFrameScheduler, shareReplay, firstValueFrom, asyncScheduler, filter as filter$1, withLatestFrom as withLatestFrom$1, Subscription, bufferTime, combineLatestWith } from 'rxjs';
7
7
  import { UNREAL_CONFIG } from '@3dsource/angular-unreal-module/config';
8
8
  export { UNREAL_CONFIG } from '@3dsource/angular-unreal-module/config';
9
- import { tap, filter, takeUntil, catchError, map, timeout, finalize, first, switchMap as switchMap$1, distinctUntilChanged, withLatestFrom, exhaustMap, takeWhile, debounceTime as debounceTime$1, take as take$1, startWith as startWith$1 } from 'rxjs/operators';
9
+ import { tap, filter, takeUntil, catchError, map, timeout, finalize, first, switchMap as switchMap$1, distinctUntilChanged, withLatestFrom, exhaustMap, takeWhile as takeWhile$1, debounceTime as debounceTime$1, take as take$1, startWith as startWith$1 } from 'rxjs/operators';
10
10
  import { v4 } from 'uuid';
11
11
  import { DialogRef, DIALOG_DATA, Dialog } from '@angular/cdk/dialog';
12
12
  import { HttpClient } from '@angular/common/http';
@@ -673,6 +673,13 @@ const RECONNECT_CONFIG = {
673
673
  maxAttempts: 3,
674
674
  delayMs: 1000,
675
675
  };
676
+ // Full-signalling restarts after consecutive DataChannelTimeouts. The limit is
677
+ // read at every timeout: a hidden tab gives up sooner, since nobody watches it
678
+ // and each attempt occupies a streaming instance.
679
+ const DATA_CHANNEL_TIMEOUT_RESTARTS = {
680
+ hidden: 3,
681
+ visible: 10,
682
+ };
676
683
  const UnrealStatusMessage = {
677
684
  CONNECTING_TO_SESSION: 'Connecting to session.',
678
685
  STARTING_YOUR_SESSION: 'Starting your session',
@@ -2984,6 +2991,9 @@ class VideoService extends SubService {
2984
2991
  // would drop that emission, so the stats interval (and `setViewportReady`,
2985
2992
  // gated on `framesPerSecond`) would never start.
2986
2993
  this.videoTrack$ = new BehaviorSubject(null);
2994
+ /** Bumped per attached peer connection: candidate-pair ids are only unique
2995
+ * inside one connection, so the epoch scopes the telemetry route ref. */
2996
+ this.pcEpoch = 0;
2987
2997
  this.VideoEncoderQP = 0;
2988
2998
  this.aggregatedStats = {};
2989
2999
  /**
@@ -2992,7 +3002,7 @@ class VideoService extends SubService {
2992
3002
  this.videoStats$ = this.videoTrack$.pipe(
2993
3003
  // If videoTrack$ emits null, switch to EMPTY → the current interval is canceled.
2994
3004
  switchMap$1((track) => track
2995
- ? interval(250).pipe(map$1(() => track.pcClient), switchMap$1((pcClient) => from(this.getStats(pcClient))), filter(Truthy))
3005
+ ? interval(250).pipe(switchMap$1(() => from(this.getStats(track.pcClient, track.pcEpoch))), filter(Truthy))
2996
3006
  : of(null)), filter(Truthy), share());
2997
3007
  this.destroyAbort = new AbortController();
2998
3008
  // Teardown is wired ONCE here instead of per `create()`. The per-create
@@ -3075,7 +3085,7 @@ class VideoService extends SubService {
3075
3085
  this.audio.srcObject = new MediaStream(audioTracks);
3076
3086
  this.playAudio(this.audio);
3077
3087
  }
3078
- this.videoTrack$.next({ stream, pcClient });
3088
+ this.videoTrack$.next({ stream, pcClient, pcEpoch: ++this.pcEpoch });
3079
3089
  // Safari does not reliably honor the autoplay attribute when srcObject
3080
3090
  // is bound post-creation for a WebRTC MediaStream; trigger play()
3081
3091
  // explicitly and let safePlay() fall back if Safari refuses.
@@ -3158,7 +3168,7 @@ class VideoService extends SubService {
3158
3168
  signal: this.destroyAbort.signal,
3159
3169
  });
3160
3170
  }
3161
- async getStats(pcClient) {
3171
+ async getStats(pcClient, pcEpoch = 0) {
3162
3172
  if (!pcClient || pcClient.connectionState === 'closed') {
3163
3173
  return null;
3164
3174
  }
@@ -3169,7 +3179,7 @@ class VideoService extends SubService {
3169
3179
  if (!this.video) {
3170
3180
  return null;
3171
3181
  }
3172
- return this.generateAggregatedStatsFunction(stats);
3182
+ return this.generateAggregatedStatsFunction(stats, pcEpoch);
3173
3183
  }
3174
3184
  catch (err) {
3175
3185
  // Handle InvalidStateError or any transient WebRTC errors gracefully
@@ -3180,8 +3190,10 @@ class VideoService extends SubService {
3180
3190
  throw err;
3181
3191
  }
3182
3192
  }
3183
- generateAggregatedStatsFunction(stats) {
3193
+ generateAggregatedStatsFunction(stats, pcEpoch = 0) {
3184
3194
  const newStat = {};
3195
+ let selectedPairId;
3196
+ let flaggedPairId;
3185
3197
  // store each type of codec we can get stats on
3186
3198
  newStat.codecs = {};
3187
3199
  newStat.currentRoundTripTime = -1;
@@ -3190,7 +3202,9 @@ class VideoService extends SubService {
3190
3202
  if (report.type === 'inbound-rtp' && report.kind === 'video') {
3191
3203
  this.applyInboundVideoReport(newStat, report);
3192
3204
  }
3193
- // Only read RTT from the *nominated* ICE candidate pair. The
3205
+ // Fallback RTT for when no selected pair resolves —
3206
+ // `applySelectedPair` overrides it with the selected pair's RTT.
3207
+ // Only read it from the *nominated* ICE candidate pair. The
3194
3208
  // stats report contains multiple `candidate-pair` entries (one
3195
3209
  // per host/srflx/relay combination tried during ICE); only the
3196
3210
  // nominated pair carries a meaningful `currentRoundTripTime`,
@@ -3205,9 +3219,13 @@ class VideoService extends SubService {
3205
3219
  Object.hasOwn(report, 'currentRoundTripTime')) {
3206
3220
  newStat.currentRoundTripTime = report.currentRoundTripTime ?? 0;
3207
3221
  }
3208
- if (report.type === 'transport' &&
3209
- Object.hasOwn(report, 'selectedCandidatePairId')) {
3210
- this.applySelectedPair(newStat, stats, report.selectedCandidatePairId);
3222
+ if (report.type === 'transport' && report.selectedCandidatePairId) {
3223
+ selectedPairId = report.selectedCandidatePairId;
3224
+ }
3225
+ // Firefox reports no `transport` stats; its pre-standard `selected`
3226
+ // flag marks the same pair and is used only when the transport is absent.
3227
+ if (report.type === 'candidate-pair' && report.selected === true) {
3228
+ flaggedPairId = report.id;
3211
3229
  }
3212
3230
  // Store mimetype of each codec
3213
3231
  if (Object.hasOwn(newStat, 'codecs') &&
@@ -3220,6 +3238,7 @@ class VideoService extends SubService {
3220
3238
  .replace('audio/', '');
3221
3239
  }
3222
3240
  });
3241
+ this.applySelectedPair(newStat, stats, selectedPairId ?? flaggedPairId, pcEpoch);
3223
3242
  this.applySessionTotals(newStat);
3224
3243
  this.aggregatedStats = newStat;
3225
3244
  return this.onAggregatedStats(newStat);
@@ -3261,24 +3280,37 @@ class VideoService extends SubService {
3261
3280
  }
3262
3281
  }
3263
3282
  /**
3264
- * Resolves the transport's selected candidate pair into the local/remote
3265
- * descriptors shown in the stats overlay. Silently skips when the pair (or
3266
- * either of its candidates) is missing from this report.
3283
+ * Resolves the selected candidate pair into its route description, byte
3284
+ * counters and RTT. Anything missing from this report stays unknown, and a
3285
+ * malformed report never throws into the stats loop — the route is optional,
3286
+ * playback is not.
3267
3287
  */
3268
- applySelectedPair(newStat, stats, selectedCandidatePairId) {
3269
- if (!selectedCandidatePairId) {
3288
+ applySelectedPair(newStat, stats, pairId, pcEpoch) {
3289
+ newStat.selectedPair = null;
3290
+ if (!pairId) {
3270
3291
  return;
3271
3292
  }
3272
- const selectedPair = stats.get(selectedCandidatePairId);
3273
- if (!selectedPair) {
3274
- return;
3293
+ try {
3294
+ const pair = stats.get(pairId);
3295
+ if (!pair) {
3296
+ return;
3297
+ }
3298
+ newStat.selectedPair = {
3299
+ ref: `${pcEpoch}:${pairId}`,
3300
+ timestamp: finite(pair.timestamp),
3301
+ bytesReceived: finite(pair.bytesReceived),
3302
+ bytesSent: finite(pair.bytesSent),
3303
+ local: candidateInfo(stats.get(pair.localCandidateId), true),
3304
+ remote: candidateInfo(stats.get(pair.remoteCandidateId), false),
3305
+ };
3306
+ const rtt = finite(pair.currentRoundTripTime);
3307
+ if (rtt !== undefined) {
3308
+ newStat.currentRoundTripTime = rtt;
3309
+ }
3310
+ }
3311
+ catch {
3312
+ newStat.selectedPair = null;
3275
3313
  }
3276
- const local = stats.get(selectedPair.localCandidateId);
3277
- const remote = stats.get(selectedPair.remoteCandidateId);
3278
- newStat.selectedPair = {
3279
- local: { type: local?.candidateType, url: local?.url },
3280
- remote: { type: remote?.candidateType, url: remote?.url },
3281
- };
3282
3314
  }
3283
3315
  /** Per-session values that do not come from a single report. */
3284
3316
  applySessionTotals(newStat) {
@@ -3394,6 +3426,35 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
3394
3426
  function pinBaseline(pinned, current) {
3395
3427
  return (pinned || current);
3396
3428
  }
3429
+ function finite(value) {
3430
+ return typeof value === 'number' && Number.isFinite(value)
3431
+ ? value
3432
+ : undefined;
3433
+ }
3434
+ /**
3435
+ * The route-relevant fields of one candidate. The address is uploaded for a
3436
+ * relay candidate only — host / srflx addresses identify the user, a relay
3437
+ * address identifies the TURN provider's region.
3438
+ */
3439
+ function candidateInfo(candidate, isLocal) {
3440
+ if (!candidate) {
3441
+ return {};
3442
+ }
3443
+ const isRelay = candidate.candidateType === 'relay';
3444
+ return {
3445
+ type: candidate.candidateType,
3446
+ protocol: candidate.protocol,
3447
+ ...(isRelay
3448
+ ? {
3449
+ address: candidate.address ?? undefined,
3450
+ port: finite(candidate.port),
3451
+ }
3452
+ : {}),
3453
+ ...(isLocal
3454
+ ? { url: candidate.url, relayProtocol: candidate.relayProtocol }
3455
+ : {}),
3456
+ };
3457
+ }
3397
3458
  /**
3398
3459
  * Whether a rejected `play()` was blocked by the browser autoplay policy.
3399
3460
  * Narrows an `unknown` rejection value without assuming it is an Error — a
@@ -3708,6 +3769,12 @@ class WebRtcPlayerService extends SubService {
3708
3769
  canSend() {
3709
3770
  return this.dcClient && this.dcClient.readyState === 'open';
3710
3771
  }
3772
+ describeConnectionState() {
3773
+ const pc = this.pcClient;
3774
+ return pc
3775
+ ? `signaling=${pc.signalingState}, ice=${pc.iceConnectionState}, connection=${pc.connectionState}`
3776
+ : 'no peer connection';
3777
+ }
3711
3778
  send(data) {
3712
3779
  if (this.canSend()) {
3713
3780
  this.dcClient?.send(data);
@@ -4529,7 +4596,8 @@ class UnrealEffects {
4529
4596
  mergeMap(() => EMPTY),
4530
4597
  // ⏱️ Only actual timeout (or real upstream error) ends up here.
4531
4598
  catchError(() => {
4532
- const error = `⏱️ DataChannel connection timeout ${this.dataChannelConnectionTimeout}ms, requesting new signaling.`;
4599
+ const details = `${document.visibilityState}; ${this.webRtcPlayerService.describeConnectionState()}`;
4600
+ const error = `⏱️ DataChannel connection timeout ${this.dataChannelConnectionTimeout}ms (${details}).`;
4533
4601
  Logger.error(error);
4534
4602
  const body = {
4535
4603
  awsInstance,
@@ -4553,7 +4621,7 @@ class UnrealEffects {
4553
4621
  // map response to the destroy action that ngrx will dispatch
4554
4622
  map$1(() => disconnectStream({
4555
4623
  reason: DisconnectReason.DataChannelTimeout,
4556
- message: 'Data Channel Timeout',
4624
+ message: `Data Channel Timeout (${details})`,
4557
4625
  })));
4558
4626
  }));
4559
4627
  }));
@@ -4585,8 +4653,28 @@ class UnrealEffects {
4585
4653
  this.webRtcPlayerService.releaseRemoteConnections(reason);
4586
4654
  }), filter(({ reason }) => reason === DisconnectReason.DropConnection), map$1(() => dropConnection()));
4587
4655
  });
4588
- this.destroyConnectionAndRestart$ = createEffect(() => {
4589
- return this.actions$.pipe(ofType(setCirrusDisconnected), withLatestFrom(this.store.select(unrealFeature.selectDisconnectReason)), filter(([, reason]) => reason === DisconnectReason.DataChannelTimeout), map$1(() => initSignalling()));
4656
+ /**
4657
+ * Restarts signalling after a DataChannelTimeout, a limited number of times.
4658
+ *
4659
+ * The counter is the index inside one restart window. A window opens on load,
4660
+ * on a successful data channel, on the user's `startStream` and on a scene
4661
+ * destroy, so only consecutive failures count. Once the limit for the current
4662
+ * tab visibility is reached, `establishingConnection` is cleared and the
4663
+ * resume popup shows.
4664
+ */
4665
+ this.restartAfterDataChannelTimeout$ = createEffect(() => {
4666
+ const timeoutDrops$ = this.actions$.pipe(ofType(setCirrusDisconnected), withLatestFrom(this.store.select(unrealFeature.selectDisconnectReason)), filter(([, reason]) => reason === DisconnectReason.DataChannelTimeout));
4667
+ return this.actions$.pipe(ofType(dataChannelConnected, startStream, destroyUnrealScene), startWith(null), switchMap(() => timeoutDrops$.pipe(map$1((_, attempt) => {
4668
+ const limit = document.hidden
4669
+ ? DATA_CHANNEL_TIMEOUT_RESTARTS.hidden
4670
+ : DATA_CHANNEL_TIMEOUT_RESTARTS.visible;
4671
+ if (attempt < limit) {
4672
+ Logger.warn(`[Restart] DataChannelTimeout, restart ${attempt + 1}/${limit}`);
4673
+ return initSignalling();
4674
+ }
4675
+ Logger.error(`[Restart] DataChannelTimeout, giving up after ${attempt} restarts`);
4676
+ return setEstablishingConnection({ value: false });
4677
+ }), takeWhile((action) => action.type === initSignalling.type, true))));
4590
4678
  });
4591
4679
  this.showUnrealError$ = createEffect(() => {
4592
4680
  return this.actions$.pipe(ofType(showUnrealErrorMessage), map$1(({ code }) => getRtcErrorMessage(code)), distinctUntilChanged(), filter(Truthy), exhaustMap((content) => this.dialog.open(UnrealErrorModalComponent, {
@@ -4989,7 +5077,7 @@ const floatToSmoothPercents = () => (source) => source.pipe(timestamp(), startWi
4989
5077
  */
4990
5078
  const smoothTransition = (start, end, duration) => {
4991
5079
  const startTime = animationFrameScheduler.now();
4992
- return interval(0, animationFrameScheduler).pipe(map(() => clampf(0, 1, (animationFrameScheduler.now() - startTime) / duration)), distinctUntilChanged(), takeWhile((fraction) => fraction < 1, true), map((fraction) => lerp(start, end, fraction)));
5080
+ return interval(0, animationFrameScheduler).pipe(map(() => clampf(0, 1, (animationFrameScheduler.now() - startTime) / duration)), distinctUntilChanged(), takeWhile$1((fraction) => fraction < 1, true), map((fraction) => lerp(start, end, fraction)));
4993
5081
  };
4994
5082
 
4995
5083
  const getImageFromVideoStream = (takeSizeFrom = 'video', imageOutput, sizes) => {
@@ -6765,6 +6853,56 @@ class CounterWindow {
6765
6853
  }
6766
6854
  }
6767
6855
 
6856
+ /**
6857
+ * Turns one `stream.sample` window of 250 ms stats reads into its route
6858
+ * fields: the runs of reads that shared a selected pair, and the description
6859
+ * of every pair this session has not described yet.
6860
+ *
6861
+ * One per session, because the described refs are that session's: the
6862
+ * collector files each session separately, so every file must describe the
6863
+ * routes it references.
6864
+ *
6865
+ * Only the reads that are there are reported. A read without a selected pair
6866
+ * closes the run, and nothing is interpolated across it — the server sees the
6867
+ * gap between two segments and keeps it unattributed.
6868
+ */
6869
+ class RouteLog {
6870
+ constructor() {
6871
+ this.described = new Set();
6872
+ }
6873
+ take(samples) {
6874
+ const routeSegments = [];
6875
+ const routes = {};
6876
+ let open = null;
6877
+ for (const { aggregatedStats } of samples) {
6878
+ const pair = aggregatedStats.selectedPair;
6879
+ if (!pair) {
6880
+ open = null;
6881
+ continue;
6882
+ }
6883
+ const ts = pair.timestamp === undefined ? null : Math.round(pair.timestamp);
6884
+ const rx = pair.bytesReceived ?? null;
6885
+ const tx = pair.bytesSent ?? null;
6886
+ if (open?.ref === pair.ref) {
6887
+ open.ts[1] = ts;
6888
+ open.rx[1] = rx;
6889
+ open.tx[1] = tx;
6890
+ continue;
6891
+ }
6892
+ open = { ref: pair.ref, ts: [ts, ts], rx: [rx, rx], tx: [tx, tx] };
6893
+ routeSegments.push(open);
6894
+ if (!this.described.has(pair.ref)) {
6895
+ this.described.add(pair.ref);
6896
+ routes[pair.ref] = { local: pair.local, remote: pair.remote };
6897
+ }
6898
+ }
6899
+ return {
6900
+ ...(routeSegments.length ? { routeSegments } : {}),
6901
+ ...(Object.keys(routes).length ? { routes } : {}),
6902
+ };
6903
+ }
6904
+ }
6905
+
6768
6906
  /**
6769
6907
  * Telemetry for the WebRTC stream lifecycle.
6770
6908
  *
@@ -7183,6 +7321,7 @@ class StreamStatusTelemetryService2 {
7183
7321
  seq: 0,
7184
7322
  sub,
7185
7323
  bytes: new CounterWindow(),
7324
+ routes: new RouteLog(),
7186
7325
  qpBad: false,
7187
7326
  fpsLow: false,
7188
7327
  maxFpsTarget: UNLIMITED_REFERENCE_FPS,
@@ -7190,6 +7329,7 @@ class StreamStatusTelemetryService2 {
7190
7329
  handshakeFlow: null,
7191
7330
  commandCount: 0,
7192
7331
  times,
7332
+ openPhaseIdx: 0,
7193
7333
  };
7194
7334
  // Resolve the wire session id from the stream's connection id, then flush
7195
7335
  // everything queued so far. Subscribed FIRST so the id is in place before
@@ -7345,6 +7485,10 @@ class StreamStatusTelemetryService2 {
7345
7485
  ? errorMessage.message
7346
7486
  : undefined;
7347
7487
  const includeGranular = granularReason && granularReason !== DisconnectReason.None;
7488
+ const openPhase = this.phaseCatalog[session.openPhaseIdx];
7489
+ if (openPhase) {
7490
+ this.emitPhaseEnded(openPhase, session.openPhaseIdx, null, true);
7491
+ }
7348
7492
  this.emitLifecycle('stream.session_ended', {
7349
7493
  sessionId: id,
7350
7494
  reason,
@@ -7408,9 +7552,10 @@ class StreamStatusTelemetryService2 {
7408
7552
  * phaseNo (derived from the catalog index), id, color, milestones,
7409
7553
  * params, overlays. No display text on the wire: the viewer's
7410
7554
  * vocabulary maps every id back to a human label, and boundary labels
7411
- * are derived from neighbor phases.
7555
+ * are derived from neighbor phases. `incomplete` marks the phase the
7556
+ * session ended in, sent from `endSession` instead of from its closer.
7412
7557
  */
7413
- emitPhaseEnded(bucket, bucketIdx, extrasValue) {
7558
+ emitPhaseEnded(bucket, bucketIdx, extrasValue, incomplete = false) {
7414
7559
  if (!this.session)
7415
7560
  return;
7416
7561
  const now = performance.now();
@@ -7427,7 +7572,15 @@ class StreamStatusTelemetryService2 {
7427
7572
  milestones,
7428
7573
  params,
7429
7574
  overlays,
7575
+ ...(incomplete && { incomplete: true }),
7430
7576
  });
7577
+ this.closePhase(bucketIdx);
7578
+ }
7579
+ /** Move the open phase past `bucketIdx` — phases close in catalog order. */
7580
+ closePhase(bucketIdx) {
7581
+ if (!this.session)
7582
+ return;
7583
+ this.session.openPhaseIdx = Math.max(this.session.openPhaseIdx, bucketIdx + 1);
7431
7584
  }
7432
7585
  /**
7433
7586
  * Emit the region / signaling / webrtc phases for an ADOPTED connection.
@@ -7518,6 +7671,7 @@ class StreamStatusTelemetryService2 {
7518
7671
  params: toPhaseParams({ ...extras, prefetched: true }),
7519
7672
  overlays: [],
7520
7673
  });
7674
+ this.closePhase(bucketIdx);
7521
7675
  }
7522
7676
  /** Streamer identity params for the adopted signaling phase — mirrors the
7523
7677
  * `ssinfo-received` closer's shape (client / instanceName / …ssData /
@@ -7612,6 +7766,7 @@ class StreamStatusTelemetryService2 {
7612
7766
  width: latest?.frameWidth,
7613
7767
  height: latest?.frameHeight,
7614
7768
  },
7769
+ ...s.routes.take(samples),
7615
7770
  });
7616
7771
  this.evaluateAnomalies(fps, qp);
7617
7772
  }
@@ -7873,7 +8028,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
7873
8028
  /** Compact one side of an ICE candidate pair to `"type (url)"` or just
7874
8029
  * `type` (host candidates have no URL). */
7875
8030
  function fmtCandidate(c) {
7876
- if (!c) {
8031
+ if (!c?.type) {
7877
8032
  return null;
7878
8033
  }
7879
8034
  return c.url ? `${c.type} (${c.url})` : c.type;
@@ -8646,17 +8801,42 @@ class VideoStatsComponent {
8646
8801
  value: this.stats()?.aggregatedStats.VideoEncoderQP ?? 0,
8647
8802
  }), /* @ts-ignore */
8648
8803
  ...(ngDevMode ? [{ debugName: "videoQP" }] : /* istanbul ignore next */ []));
8649
- this.videoStatus = computed(() => {
8804
+ /** What the window shows: the raw stats plus the few display-only values
8805
+ * derived here. Shared by the text rows and the graphs. */
8806
+ this.view = computed(() => {
8650
8807
  const event = this.stats();
8651
- if (!this.isDevMode || !event) {
8808
+ if (!event) {
8652
8809
  return undefined;
8653
8810
  }
8654
- const data = {
8655
- ...event.aggregatedStats,
8811
+ const stats = event.aggregatedStats;
8812
+ const pair = stats.selectedPair;
8813
+ const view = {
8814
+ ...stats,
8656
8815
  quality: event.quality,
8816
+ MBytesReceived: typeof stats.bytesReceived === 'number'
8817
+ ? +(stats.bytesReceived / 1e6).toFixed(2)
8818
+ : undefined,
8819
+ // Only the stable part of the route: counters, stats timestamp and ref
8820
+ // change on every tick and would make the row jump.
8821
+ selectedPair: pair && {
8822
+ local: {
8823
+ type: pair.local.type,
8824
+ protocol: pair.local.protocol,
8825
+ url: pair.local.url,
8826
+ },
8827
+ remote: { type: pair.remote.type, protocol: pair.remote.protocol },
8828
+ },
8657
8829
  };
8830
+ return view;
8831
+ }, /* @ts-ignore */
8832
+ ...(ngDevMode ? [{ debugName: "view" }] : /* istanbul ignore next */ []));
8833
+ this.videoStatus = computed(() => {
8834
+ const view = this.view();
8835
+ if (!this.isDevMode || !view) {
8836
+ return undefined;
8837
+ }
8658
8838
  return this.fields
8659
- .map((key) => ({ key, value: data[key] }))
8839
+ .map((key) => ({ key, value: view[key] }))
8660
8840
  .filter((r) => !this.elementsToShow().includes(r.key));
8661
8841
  }, /* @ts-ignore */
8662
8842
  ...(ngDevMode ? [{ debugName: "videoStatus" }] : /* istanbul ignore next */ []));
@@ -8691,9 +8871,7 @@ class VideoStatsComponent {
8691
8871
  Math.floor(Math.random() * 16777216)
8692
8872
  .toString(16)
8693
8873
  .padStart(6, '0'),
8694
- stat: computed(() => ({
8695
- value: this.stats()?.aggregatedStats[key],
8696
- })),
8874
+ stat: computed(() => ({ value: this.view()?.[key] })),
8697
8875
  }));
8698
8876
  this.graphList = computed(() => this.graphListItems.filter((r) => this.elementsToShow().includes(r.key)), /* @ts-ignore */
8699
8877
  ...(ngDevMode ? [{ debugName: "graphList" }] : /* istanbul ignore next */ []));
@@ -9204,5 +9382,5 @@ function provideUnrealModule() {
9204
9382
  * Generated bundle index. Do not edit.
9205
9383
  */
9206
9384
 
9207
- export { AFKService, ALL_KEYBOARD_KEYS, AUTO_HIGH_RESOLUTION_IDLE_MS, AfkTimeoutModalComponent, AggregatorService, AnalyticsService, AnswerHandler, COLOR_CODES, 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, Falsy, FileHandlerService, FileReceiverService, FpsMonitorService, FreezeFrameComponent, FreezeFrameService, HEXtoRGB, HSVtoRGB, IME_PROCESS_KEY, INNER_RETRY_LIMIT, INSTANCE_READY_TIMEOUT, IceCandidateHandler, ImageLoadingSrcComponent, InputOptions, InputService, InstanceReadyHandler, InstanceReservedHandler, IntroSrcComponent, LatencyTimings, LoadingProgressService, Logger, MINIMAL_FPS, MODIFIER_KEY_CODES, MessageType, MouseButton, MouseButtonsMask, OUTER_RETRY_DELAY, OnCloseHandler, OnErrorHandler, OnMessageHandler, OnOpenHandler, OrchestrationMessageTypes, POLLING_TIME, 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, RGBtoHEX, RGBtoHSV, RegionsPingService, SAME_SIZE_THRESHOLD, SCREEN_LOCKER_CONTAINER_ID, SIGNALLING_PERCENT_VALUE, SSInfoHandler, STREAMING_AUDIO_ID, STREAMING_VIDEO_ID, STREAM_RESOLUTION_LIMITS, SafePipe, Semaphore, SignallingService, StatGraphComponent, StreamAdoptionService, StreamStatusTelemetryService, StreamStatusTelemetryService2, SubService, ToClientMessage, Truthy, UnrealAfkSlotDirective, UnrealCallbackService, UnrealCommunicatorService, UnrealEffects, UnrealKeyCode, UnrealResumeSlotDirective, UnrealSceneComponent, UnrealStatusMessage, UnrealStatusSlotDirective, 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, clampStreamResolution, clampf, commandCompleted, commandStarted, dataChannelConnected, dataChannelReady, decodeData, destroyRemoteConnections, destroyUnrealScene, disconnectStream, dispatchResize, dropConnection, dropDC, dropPC, fitIntoRectangle, floatToPercents, floatToSmoothPercents, forceResizeViewport, fpIsASameAsB, fromResizeObserver, generateUuid, getActiveUrl, getCanvasCached, getImageFromVideoStream, getResizeStudioValues, getResizeValuesUniversal, getRtcErrorMessage, getSnapshot, iceConnectionFailed, initSignalling, instanceReady, inverseLerp, isAppOwned, isMetaboxMode, keepMaxUntilReset, lerp, makeEven, pollingEnded, pollingStarted, provideUnrealModule, provideUnrealState, qpToQuality, reconnectPeer, reconnectPeerFailed, reconnectPeerSuccess, regionPingFetchEnd, regionPingFetchStart, regionResolved, resetAfk, resetConfig, resetDataChannelForReconnect, resetIntroSrc, resetUserInactivityTimeout, resetWarnTimeout, resolveSkin, 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, startStream, streamAdopted, tapLog, trackMixpanelEvent, trackTelemetryData, uint8ToBase64, unrealCommandCallback, unrealFeature, unrealReducer, updateCirrusInfo, videoAdaptedToContainer, videoStreamInitialized };
9385
+ export { AFKService, ALL_KEYBOARD_KEYS, AUTO_HIGH_RESOLUTION_IDLE_MS, AfkTimeoutModalComponent, AggregatorService, AnalyticsService, AnswerHandler, COLOR_CODES, CommandTelemetryService, ConfigHandler, ConsoleExtensionsService, ControlScheme, DATA_CHANNEL_CONNECTION_TIMEOUT, DATA_CHANNEL_TIMEOUT_RESTARTS, DEBOUNCE_TO_MANY_RESIZE_CALLS, DEFAULT_AFK_TIMEOUT, DEFAULT_AFK_TIMEOUT_PERIOD, DESTROY_DISCONNECT_REASONS, DevModeService, DisconnectReason, FULL_HD_HEIGHT, FULL_HD_WIDTH, Falsy, FileHandlerService, FileReceiverService, FpsMonitorService, FreezeFrameComponent, FreezeFrameService, HEXtoRGB, HSVtoRGB, IME_PROCESS_KEY, INNER_RETRY_LIMIT, INSTANCE_READY_TIMEOUT, IceCandidateHandler, ImageLoadingSrcComponent, InputOptions, InputService, InstanceReadyHandler, InstanceReservedHandler, IntroSrcComponent, LatencyTimings, LoadingProgressService, Logger, MINIMAL_FPS, MODIFIER_KEY_CODES, MessageType, MouseButton, MouseButtonsMask, OUTER_RETRY_DELAY, OnCloseHandler, OnErrorHandler, OnMessageHandler, OnOpenHandler, OrchestrationMessageTypes, POLLING_TIME, 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, RGBtoHEX, RGBtoHSV, RegionsPingService, SAME_SIZE_THRESHOLD, SCREEN_LOCKER_CONTAINER_ID, SIGNALLING_PERCENT_VALUE, SSInfoHandler, STREAMING_AUDIO_ID, STREAMING_VIDEO_ID, STREAM_RESOLUTION_LIMITS, SafePipe, Semaphore, SignallingService, StatGraphComponent, StreamAdoptionService, StreamStatusTelemetryService, StreamStatusTelemetryService2, SubService, ToClientMessage, Truthy, UnrealAfkSlotDirective, UnrealCallbackService, UnrealCommunicatorService, UnrealEffects, UnrealKeyCode, UnrealResumeSlotDirective, UnrealSceneComponent, UnrealStatusMessage, UnrealStatusSlotDirective, 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, clampStreamResolution, clampf, commandCompleted, commandStarted, dataChannelConnected, dataChannelReady, decodeData, destroyRemoteConnections, destroyUnrealScene, disconnectStream, dispatchResize, dropConnection, dropDC, dropPC, fitIntoRectangle, floatToPercents, floatToSmoothPercents, forceResizeViewport, fpIsASameAsB, fromResizeObserver, generateUuid, getActiveUrl, getCanvasCached, getImageFromVideoStream, getResizeStudioValues, getResizeValuesUniversal, getRtcErrorMessage, getSnapshot, iceConnectionFailed, initSignalling, instanceReady, inverseLerp, isAppOwned, isMetaboxMode, keepMaxUntilReset, lerp, makeEven, pollingEnded, pollingStarted, provideUnrealModule, provideUnrealState, qpToQuality, reconnectPeer, reconnectPeerFailed, reconnectPeerSuccess, regionPingFetchEnd, regionPingFetchStart, regionResolved, resetAfk, resetConfig, resetDataChannelForReconnect, resetIntroSrc, resetUserInactivityTimeout, resetWarnTimeout, resolveSkin, 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, startStream, streamAdopted, tapLog, trackMixpanelEvent, trackTelemetryData, uint8ToBase64, unrealCommandCallback, unrealFeature, unrealReducer, updateCirrusInfo, videoAdaptedToContainer, videoStreamInitialized };
9208
9386
  //# sourceMappingURL=3dsource-angular-unreal-module.mjs.map