@3dsource/angular-unreal-module 0.0.110 → 0.0.112
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,10 +1,10 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
2
|
import { InjectionToken, inject, ChangeDetectionStrategy, Component, Injectable, DestroyRef, effect, untracked, signal, Pipe, input, ElementRef, viewChild, afterNextRender, HostListener, computed, output, makeEnvironmentProviders, provideEnvironmentInitializer } from '@angular/core';
|
|
3
3
|
import { SourceButtonComponent, SourceLoadingComponent } from '@3dsource/source-ui-native';
|
|
4
|
-
import { merge, map, scan, concatMap, of, animationFrameScheduler, interval, Observable, debounceTime, switchMap, take, fromEvent, EMPTY, timer, timeout, mergeMap, Subject, skip, share, from, throwError, defer, retry, tap as tap$1, startWith, takeUntil as takeUntil$1,
|
|
4
|
+
import { merge, map, scan, concatMap, of, animationFrameScheduler, interval, Observable, debounceTime, switchMap, take, fromEvent, EMPTY, timer, timeout, mergeMap, Subject, skip, share, from, throwError, defer, retry, tap as tap$1, startWith, takeUntil as takeUntil$1, throttleTime, asyncScheduler, filter as filter$1, withLatestFrom as withLatestFrom$1, pairwise, Subscription, bufferTime, BehaviorSubject, combineLatestWith, distinctUntilChanged as distinctUntilChanged$1, concat } from 'rxjs';
|
|
5
5
|
import { createAction, props, createReducer, on, createFeature, createSelector, Store, provideState } from '@ngrx/store';
|
|
6
6
|
import { Actions, createEffect, ofType, provideEffects } from '@ngrx/effects';
|
|
7
|
-
import { map as map$1, distinctUntilChanged, takeWhile, filter, tap, withLatestFrom, exhaustMap, catchError, takeUntil, switchMap as switchMap$1, timeout as timeout$1, debounceTime as debounceTime$1, delay, skip as skip$1 } from 'rxjs/operators';
|
|
7
|
+
import { map as map$1, distinctUntilChanged, takeWhile, filter, tap, withLatestFrom, exhaustMap, catchError, takeUntil, switchMap as switchMap$1, timeout as timeout$1, debounceTime as debounceTime$1, take as take$1, startWith as startWith$1, delay, skip as skip$1 } from 'rxjs/operators';
|
|
8
8
|
import { Logger, calculateMedian, clampf, lerp, getCanvasCached, getSnapshot, fitIntoRectangle, whereNot, Truthy, Falsy, tapLog, generateUuid, COLOR_CODES, where, KeyboardNumericCode, InvertedKeyMap, Semaphore, HEXtoRGB, RGBtoHSV, inverseLerp, HSVtoRGB, RGBtoHEX, fpIsASameAsB } from '@3dsource/utils';
|
|
9
9
|
import { concatLatestFrom } from '@ngrx/operators';
|
|
10
10
|
import { DialogRef, DIALOG_DATA, Dialog } from '@angular/cdk/dialog';
|
|
@@ -12,6 +12,7 @@ import { HttpClient } from '@angular/common/http';
|
|
|
12
12
|
import { ScrollStrategyOptions } from '@angular/cdk/overlay';
|
|
13
13
|
import { MetaBoxCommand, EPayloadVersion } from '@3dsource/types-unreal';
|
|
14
14
|
import { toSignal, takeUntilDestroyed, toObservable } from '@angular/core/rxjs-interop';
|
|
15
|
+
import { v4 } from 'uuid';
|
|
15
16
|
import { DomSanitizer } from '@angular/platform-browser';
|
|
16
17
|
import { NgOptimizedImage, AsyncPipe, JsonPipe } from '@angular/common';
|
|
17
18
|
import * as i1 from '@angular/forms';
|
|
@@ -150,6 +151,7 @@ const commandCompleted = createAction(scoped$1 `command completed`, props());
|
|
|
150
151
|
|
|
151
152
|
const scoped = (templateString) => `[UNREAL] ${templateString[0]}`;
|
|
152
153
|
const trackMixpanelEvent = createAction(scoped `track mixpanel event`, props());
|
|
154
|
+
const trackTelemetryData = createAction(scoped `track telemetry data`, props());
|
|
153
155
|
const changeLowBandwidth = createAction(scoped `change low bandwidth`, props());
|
|
154
156
|
const setMaxFps = createAction(scoped `change fps`, props());
|
|
155
157
|
const destroyRemoteConnections = createAction(scoped `destroyRemoteConnections`, props());
|
|
@@ -184,6 +186,47 @@ const changeStreamResolutionAction = createAction(scoped `change stream resoluti
|
|
|
184
186
|
const changeStreamResolutionSuccessAction = createAction(scoped `change stream resolution success action`, props());
|
|
185
187
|
const setSignalingName = createAction(scoped `set aws instanceName`, props());
|
|
186
188
|
const setOrchestrationParameters = createAction(scoped `set orchestration parameters`, props());
|
|
189
|
+
/**
|
|
190
|
+
* Dispatched when the orchestration server signals that the reserved
|
|
191
|
+
* instance is ready to serve the stream (`instanceReady` WS message).
|
|
192
|
+
* Emitted for observability only — no reducer state change.
|
|
193
|
+
*/
|
|
194
|
+
const instanceReady = createAction(scoped `instance ready`);
|
|
195
|
+
/**
|
|
196
|
+
* Dispatched when `RegionsPingService.getFastest()` begins — right
|
|
197
|
+
* before the region-list HTTP request goes out. Bookends the entire
|
|
198
|
+
* region-discovery flow together with {@link regionResolved}.
|
|
199
|
+
* Observability-only — no state change.
|
|
200
|
+
*/
|
|
201
|
+
const regionPingFetchStart = createAction(scoped `region ping fetch start`);
|
|
202
|
+
/**
|
|
203
|
+
* Dispatched once `RegionsPingService.getProviders()` returns the region
|
|
204
|
+
* list — i.e. the fetch step is done and the actual per-region ping
|
|
205
|
+
* sweep is about to start. Splits the region-discovery waterfall into
|
|
206
|
+
* two legs: config fetch vs ping sweep. Observability-only.
|
|
207
|
+
*/
|
|
208
|
+
const regionPingFetchEnd = createAction(scoped `region ping fetch end`);
|
|
209
|
+
/**
|
|
210
|
+
* Dispatched once the closest region has been resolved by the
|
|
211
|
+
* regions-ping service. Carries every sampled ping RTT to the winning
|
|
212
|
+
* region (one entry per probe — `checkLatency` issues `ping_count`
|
|
213
|
+
* sequential pings per region) so consumers can inspect variance /
|
|
214
|
+
* jitter, not just the mean. Observability-only — no state change.
|
|
215
|
+
*/
|
|
216
|
+
const regionResolved = createAction(scoped `region resolved`, props());
|
|
217
|
+
/**
|
|
218
|
+
* Dispatched around the HTTP polling roundtrip inside `getAwsInstance`
|
|
219
|
+
* (old orchestration flow). Only fires when polling is actually used
|
|
220
|
+
* (i.e. signaling URL is not a direct `ws://` / `wss://` endpoint).
|
|
221
|
+
* Observability-only — no state change.
|
|
222
|
+
*/
|
|
223
|
+
const pollingStarted = createAction(scoped `polling started`);
|
|
224
|
+
const pollingEnded = createAction(scoped `polling ended`);
|
|
225
|
+
/**
|
|
226
|
+
* Dispatched when the client has just sent an SDP answer back to Cirrus
|
|
227
|
+
* after receiving the server-initiated offer. Observability-only.
|
|
228
|
+
*/
|
|
229
|
+
const answerSent = createAction(scoped `answer sent`);
|
|
187
230
|
const setOrchestrationMessage = createAction(scoped `set orchestration message`, props());
|
|
188
231
|
const setOrchestrationProgress = createAction(scoped `set orchestration progress`, props());
|
|
189
232
|
const setOrchestrationContext = createAction(scoped `set orchestration params`, props());
|
|
@@ -508,7 +551,6 @@ const unrealReducer = createReducer(initialState$1, on(changeLowBandwidth, (stat
|
|
|
508
551
|
}), on(setUnrealPlaywrightConfig, (state) => {
|
|
509
552
|
return {
|
|
510
553
|
...state,
|
|
511
|
-
isFirstSuccessLoad: true,
|
|
512
554
|
cirrusConnected: true,
|
|
513
555
|
viewportReady: true,
|
|
514
556
|
dataChannelConnected: true,
|
|
@@ -541,7 +583,9 @@ function IceCandidateHandler(msg) {
|
|
|
541
583
|
}
|
|
542
584
|
|
|
543
585
|
function InstanceReadyHandler() {
|
|
544
|
-
//
|
|
586
|
+
// No state change — emit the action purely so services that want to
|
|
587
|
+
// observe orchestration progress (e.g. telemetry) have a hook.
|
|
588
|
+
this.store.dispatch(instanceReady());
|
|
545
589
|
}
|
|
546
590
|
|
|
547
591
|
function InstanceReservedHandler(msg) {
|
|
@@ -1169,6 +1213,7 @@ const selectTotalProgress = createSelector(unrealFeature.selectStatusPercentSign
|
|
|
1169
1213
|
});
|
|
1170
1214
|
const selectClientAndViewIds = createSelector(unrealFeature.selectStreamClientCompanyId, unrealFeature.selectStreamViewId, (clientId, viewId) => ({ clientId, viewId }));
|
|
1171
1215
|
const selectIsLoaderScreenVisible = createSelector(unrealFeature.selectViewportReady, unrealFeature.selectDataChannelConnected, unrealFeature.selectCirrusConnected, (viewReady, cirrusConnected, dataChannelConnected) => !viewReady || !cirrusConnected || !dataChannelConnected);
|
|
1216
|
+
const selectCirrusConnectionId = createSelector(unrealFeature.selectSsData, (data) => data?.connectionId);
|
|
1172
1217
|
|
|
1173
1218
|
class UnrealErrorModalComponent {
|
|
1174
1219
|
constructor() {
|
|
@@ -2580,7 +2625,9 @@ class SignallingService extends SubService {
|
|
|
2580
2625
|
this.correlationId = generateUuid();
|
|
2581
2626
|
this.commandTelemetryService.reset();
|
|
2582
2627
|
this.startEstablishingConnection();
|
|
2583
|
-
return defer(() =>
|
|
2628
|
+
return defer(() => {
|
|
2629
|
+
return this.getRegion().pipe(tap((region) => this.region.set(region ?? '')), map$1((region) => this.adaptUrlsToRegion(urlList, region)), switchMap((urls) => this.getAwsInstance(urls).pipe(take(1))), tap((awsInstance) => this.store.dispatch(setAwsInstance(awsInstance))), switchMap(({ wsUrl }) => this.connectToCirrus(wsUrl ?? '').pipe(filter(Truthy), take(1))), takeUntil(this.abort$));
|
|
2630
|
+
}).pipe(
|
|
2584
2631
|
// Retry the whole attempt on error, advancing to the next URL each time.
|
|
2585
2632
|
retry({
|
|
2586
2633
|
delay: (err, attempt) => {
|
|
@@ -2714,6 +2761,9 @@ class SignallingService extends SubService {
|
|
|
2714
2761
|
};
|
|
2715
2762
|
}));
|
|
2716
2763
|
}
|
|
2764
|
+
// Old (HTTP-polling) orchestration flow — mark the roundtrip so
|
|
2765
|
+
// the signaling-phase timeline can show it as a distinct segment.
|
|
2766
|
+
this.store.dispatch(pollingStarted());
|
|
2717
2767
|
return this.store.select(selectClientAndViewIds).pipe(
|
|
2718
2768
|
// Log missing IDs without altering the stream.
|
|
2719
2769
|
tap(({ clientId, viewId }) => {
|
|
@@ -2746,6 +2796,7 @@ class SignallingService extends SubService {
|
|
|
2746
2796
|
}),
|
|
2747
2797
|
// Map a valid orchestration payload into the final AwsInstance.
|
|
2748
2798
|
map$1((data) => {
|
|
2799
|
+
this.store.dispatch(pollingEnded());
|
|
2749
2800
|
const wsUrl = httpUrlToWs(`${location.protocol}//${data.signallingServer}`);
|
|
2750
2801
|
return {
|
|
2751
2802
|
wsUrl,
|
|
@@ -3413,6 +3464,9 @@ class WebRtcPlayerService extends SubService {
|
|
|
3413
3464
|
answer.sdp = this.mungeSDP(answer.sdp ?? '');
|
|
3414
3465
|
await pcClient.setLocalDescription(answer);
|
|
3415
3466
|
this.signallingSrv.send(answer);
|
|
3467
|
+
// Observability signal for the signaling waterfall — the handshake
|
|
3468
|
+
// went "answer sent" between `offer received` and `datachannel_ready`.
|
|
3469
|
+
this.store.dispatch(answerSent());
|
|
3416
3470
|
}
|
|
3417
3471
|
catch (e) {
|
|
3418
3472
|
Logger.error("Couldn't create answer", e);
|
|
@@ -4485,6 +4539,7 @@ class InputService extends SubService {
|
|
|
4485
4539
|
class RegionsPingService {
|
|
4486
4540
|
constructor() {
|
|
4487
4541
|
this.httpClient = inject(HttpClient);
|
|
4542
|
+
this.store = inject(Store);
|
|
4488
4543
|
this.unrealInitialConfig = inject(UNREAL_CONFIG, { optional: true });
|
|
4489
4544
|
this.config = {
|
|
4490
4545
|
ping_count: 2,
|
|
@@ -4494,7 +4549,15 @@ class RegionsPingService {
|
|
|
4494
4549
|
};
|
|
4495
4550
|
}
|
|
4496
4551
|
getFastest(regionListUrl = this.unrealInitialConfig?.regionsPingUrl || '') {
|
|
4497
|
-
|
|
4552
|
+
// Self-announce every lifecycle waypoint of the region-discovery
|
|
4553
|
+
// flow (start / providers-fetched / resolved). The final
|
|
4554
|
+
// `regionResolved` forwards the winner's full ping-sample array
|
|
4555
|
+
// straight from `PingResult.result` — no aggregation.
|
|
4556
|
+
this.store.dispatch(regionPingFetchStart());
|
|
4557
|
+
return this.getProviders(regionListUrl).pipe(tap(() => this.store.dispatch(regionPingFetchEnd())), switchMap((providers) => this.getPingResult(providers)), catchError(() => of(null)), tapLog('PingResult'), tap((data) => this.store.dispatch(regionResolved({
|
|
4558
|
+
region: data?.region_code ?? '',
|
|
4559
|
+
pingsMs: data?.result ?? [],
|
|
4560
|
+
}))), map$1((data) => data?.region_code));
|
|
4498
4561
|
}
|
|
4499
4562
|
getPingResult(providers) {
|
|
4500
4563
|
const regions = providers.regions;
|
|
@@ -4583,11 +4646,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.10", ngImpo
|
|
|
4583
4646
|
}] });
|
|
4584
4647
|
|
|
4585
4648
|
class StreamStatusTelemetryService {
|
|
4586
|
-
#store;
|
|
4587
|
-
#videoService;
|
|
4588
4649
|
constructor() {
|
|
4589
|
-
this
|
|
4590
|
-
this
|
|
4650
|
+
this.store = inject(Store);
|
|
4651
|
+
this.actions$ = inject(Actions);
|
|
4652
|
+
this.videoService = inject(VideoService);
|
|
4591
4653
|
this.unrealConfig = inject(UNREAL_CONFIG, { optional: true });
|
|
4592
4654
|
this.init();
|
|
4593
4655
|
}
|
|
@@ -4595,13 +4657,47 @@ class StreamStatusTelemetryService {
|
|
|
4595
4657
|
if (this.unrealConfig?.playwright) {
|
|
4596
4658
|
return;
|
|
4597
4659
|
}
|
|
4598
|
-
|
|
4599
|
-
|
|
4660
|
+
// Start a telemetry session on every `initSignalling` dispatch.
|
|
4661
|
+
// switchMap cancels the previous session's inner pipeline on re-init.
|
|
4662
|
+
// throttleTime leading+trailing emits the first event immediately and
|
|
4663
|
+
// at most one event every 5s afterward.
|
|
4664
|
+
this.actions$
|
|
4665
|
+
.pipe(ofType(initSignalling), switchMap$1(() => {
|
|
4666
|
+
// Captured once per session — marks the UTC moment signaling started.
|
|
4667
|
+
const startedAt = Date.now();
|
|
4668
|
+
const startTimeUtc = new Date(startedAt).toISOString();
|
|
4669
|
+
const signalingReadyTime$ = this.readyTimeSince(this.store.select(unrealFeature.selectCirrusConnected), startedAt);
|
|
4670
|
+
const viewPortReadyTime$ = this.readyTimeSince(this.store.select(unrealFeature.selectViewportReady), startedAt);
|
|
4671
|
+
return this.videoService.videoStats$.pipe(throttleTime(5000, asyncScheduler, {
|
|
4672
|
+
leading: true,
|
|
4673
|
+
trailing: true,
|
|
4674
|
+
}), withLatestFrom(this.store.select(unrealFeature.selectAwsInstance), this.store.select(unrealFeature.selectLowBandwidth), this.store.select(unrealFeature.selectLowBandwidthStats), signalingReadyTime$, viewPortReadyTime$), map$1(([data, signalingServer, isLowBandwidth, lbmStats, signalingReadyTime, viewPortReadyTime,]) => this.mapEventData({
|
|
4675
|
+
data,
|
|
4676
|
+
signalingServer,
|
|
4677
|
+
isLowBandwidth,
|
|
4678
|
+
lbmStats,
|
|
4679
|
+
startTimeUtc,
|
|
4680
|
+
signalingReadyTime,
|
|
4681
|
+
viewPortReadyTime,
|
|
4682
|
+
})));
|
|
4683
|
+
}))
|
|
4600
4684
|
.subscribe((data) => this.trackEventToMixPanel(data));
|
|
4601
4685
|
}
|
|
4602
|
-
|
|
4686
|
+
/**
|
|
4687
|
+
* Emits `undefined` until `readyState$` first emits a truthy value,
|
|
4688
|
+
* then emits the ms elapsed since `startedAt` and completes.
|
|
4689
|
+
* Used with `withLatestFrom` so events carry a stable duration for the whole session.
|
|
4690
|
+
*/
|
|
4691
|
+
readyTimeSince(readyState$, startedAt) {
|
|
4692
|
+
return readyState$.pipe(filter(Boolean), take$1(1), map$1(() => Date.now() - startedAt), startWith$1(undefined));
|
|
4693
|
+
}
|
|
4694
|
+
mapEventData(params) {
|
|
4695
|
+
const { data, signalingServer, isLowBandwidth, lbmStats, startTimeUtc, signalingReadyTime, viewPortReadyTime, } = params;
|
|
4603
4696
|
const { bitrate, VideoEncoderQP, framesPerSecond, frameWidth, frameHeight, jitter, packetsLost, currentRoundTripTime, runTime, } = data.aggregatedStats;
|
|
4604
4697
|
return {
|
|
4698
|
+
startTimeUtc,
|
|
4699
|
+
signalingReadyTime,
|
|
4700
|
+
viewPortReadyTime,
|
|
4605
4701
|
runTime,
|
|
4606
4702
|
frameWidth,
|
|
4607
4703
|
frameHeight,
|
|
@@ -4619,7 +4715,7 @@ class StreamStatusTelemetryService {
|
|
|
4619
4715
|
};
|
|
4620
4716
|
}
|
|
4621
4717
|
trackEventToMixPanel(data) {
|
|
4622
|
-
this
|
|
4718
|
+
this.store.dispatch(trackMixpanelEvent({ event: 'streamStatus', data }));
|
|
4623
4719
|
}
|
|
4624
4720
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.10", ngImport: i0, type: StreamStatusTelemetryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
4625
4721
|
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.10", ngImport: i0, type: StreamStatusTelemetryService }); }
|
|
@@ -4628,6 +4724,707 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.10", ngImpo
|
|
|
4628
4724
|
type: Injectable
|
|
4629
4725
|
}], ctorParameters: () => [] });
|
|
4630
4726
|
|
|
4727
|
+
const QP_RED_THRESHOLD = 35;
|
|
4728
|
+
const FPS_LOW_RATIO = 0.7; // p50 fps below 70% of target
|
|
4729
|
+
const UNLIMITED_REFERENCE_FPS = 60;
|
|
4730
|
+
const SAMPLE_WINDOW_MS = 5000;
|
|
4731
|
+
// Persistent per-browser identifier — survives page reloads. Used to group
|
|
4732
|
+
// multiple sessions of the same anonymous user for funnel / retention analysis.
|
|
4733
|
+
const USER_ID_STORAGE_KEY = 'sourceui.streamTelemetry.userId';
|
|
4734
|
+
class StreamStatusTelemetryService2 {
|
|
4735
|
+
constructor() {
|
|
4736
|
+
this.store = inject(Store);
|
|
4737
|
+
this.actions$ = inject(Actions);
|
|
4738
|
+
this.videoService = inject(VideoService);
|
|
4739
|
+
this.signallingService = inject(SignallingService);
|
|
4740
|
+
this.unrealConfig = inject(UNREAL_CONFIG, { optional: true });
|
|
4741
|
+
/**
|
|
4742
|
+
* Per-browser anonymous user id. Read from `localStorage` on first access;
|
|
4743
|
+
* if missing, a new UUID is generated and persisted. Included as
|
|
4744
|
+
* `data.userId` on every emitted event — the server uses it to bucket
|
|
4745
|
+
* per-session files under `data/<userId>/<sessionId>.jsonl`.
|
|
4746
|
+
*/
|
|
4747
|
+
this.userId = this.getOrCreateUserId();
|
|
4748
|
+
this.session = null;
|
|
4749
|
+
/**
|
|
4750
|
+
* Whether a session has already started during the current page lifetime.
|
|
4751
|
+
* Used to distinguish a fresh page load (bootstrap measurement is
|
|
4752
|
+
* meaningful) from a Resume/restart (the document is already loaded,
|
|
4753
|
+
* so "bootstrap" is a no-op). Only the first session gets the bootstrap
|
|
4754
|
+
* phase anchored at navigation start (perf.now() === 0).
|
|
4755
|
+
*/
|
|
4756
|
+
this.hasStartedBefore = false;
|
|
4757
|
+
/**
|
|
4758
|
+
* Rolling snapshot of the currently selected WebRTC ICE candidate pair
|
|
4759
|
+
* (local/remote type + url). Scanned over `videoStats$` so the stream
|
|
4760
|
+
* *keeps* the first non-null pair even if later stats emit null — WebRTC
|
|
4761
|
+
* doesn't un-select a pair mid-session, but the source is nullable. The
|
|
4762
|
+
* `startWith(null)` seed makes `withLatestFrom` on this stream safe even
|
|
4763
|
+
* before the first video stat arrives (common at data-channel-open).
|
|
4764
|
+
*/
|
|
4765
|
+
this.selectedPair$ = this.videoService.videoStats$.pipe(map((s) => s.aggregatedStats.selectedPair ?? null), scan((acc, curr) => curr ?? acc, null), startWith(null));
|
|
4766
|
+
/**
|
|
4767
|
+
* Correlation-matched Observable: emits once when the first `loopBack`
|
|
4768
|
+
* command's corresponding `command completed` action fires. Built from
|
|
4769
|
+
* the two related actions — the command-started captures the correlation
|
|
4770
|
+
* id, the command-completed with that id emits.
|
|
4771
|
+
*/
|
|
4772
|
+
this.loopbackReceived$ = this.actions$.pipe(ofType(commandStarted), filter$1(({ command }) => command === 'loopBack'), take(1), switchMap(({ id }) => this.actions$.pipe(ofType(commandCompleted), filter$1((b) => b.id === id), take(1))));
|
|
4773
|
+
// Overlay-referenced milestones — hoisted as fields so both
|
|
4774
|
+
// `milestones: [...]` and `overlays: [...]` share the same object
|
|
4775
|
+
// identity (the in-session Map keys milestones by that identity).
|
|
4776
|
+
this.pollingStartedEntry = {
|
|
4777
|
+
label: 'Polling started (HTTP orchestration)',
|
|
4778
|
+
event: this.actions$.pipe(ofType(pollingStarted)),
|
|
4779
|
+
};
|
|
4780
|
+
this.awsInstanceResolvedEntry = {
|
|
4781
|
+
label: 'Instance WS URL resolved',
|
|
4782
|
+
event: this.actions$.pipe(ofType(setAwsInstance)),
|
|
4783
|
+
};
|
|
4784
|
+
this.loopbackReceivedEntry = {
|
|
4785
|
+
label: 'LoopBack received',
|
|
4786
|
+
event: this.loopbackReceived$,
|
|
4787
|
+
};
|
|
4788
|
+
this.signalingServerConnectedEntry = {
|
|
4789
|
+
label: 'Websocket connected',
|
|
4790
|
+
event: this.store
|
|
4791
|
+
.select(unrealFeature.selectCirrusConnected)
|
|
4792
|
+
.pipe(filter$1(Boolean), take(1)),
|
|
4793
|
+
};
|
|
4794
|
+
this.configReceivedEntry = {
|
|
4795
|
+
label: 'Config received',
|
|
4796
|
+
event: this.signallingService.onConfig$.pipe(filter$1(Boolean), take(1)),
|
|
4797
|
+
};
|
|
4798
|
+
/**
|
|
4799
|
+
* Unified phase catalog — the single source of truth for everything
|
|
4800
|
+
* this service tracks after `session_started`. Each entry is a phase;
|
|
4801
|
+
* its `milestones` array is ordered chronologically and the *last*
|
|
4802
|
+
* entry is the phase closer. When that closer's observable fires, the
|
|
4803
|
+
* service emits one generic `stream.phase_ended` wire event with every
|
|
4804
|
+
* display field the viewer needs.
|
|
4805
|
+
*
|
|
4806
|
+
* `phaseNo` is the array index; `startPhase` chains from the previous
|
|
4807
|
+
* phase's `endPhase` — neither needs to be specified here.
|
|
4808
|
+
*/
|
|
4809
|
+
this.phaseCatalog = [
|
|
4810
|
+
{
|
|
4811
|
+
label: 'Region resolving',
|
|
4812
|
+
color: '#61afef',
|
|
4813
|
+
milestones: [
|
|
4814
|
+
{
|
|
4815
|
+
label: 'Region ping fetch start',
|
|
4816
|
+
event: this.actions$.pipe(ofType(regionPingFetchStart)),
|
|
4817
|
+
},
|
|
4818
|
+
{
|
|
4819
|
+
label: 'Region ping fetch end',
|
|
4820
|
+
event: this.actions$.pipe(ofType(regionPingFetchEnd)),
|
|
4821
|
+
},
|
|
4822
|
+
// CLOSER — the `regionResolved` action carries the raw ping
|
|
4823
|
+
// samples (one per probe) from `RegionsPingService`; the
|
|
4824
|
+
// catalog flattens them to a comma-separated string since
|
|
4825
|
+
// `toPhaseParams` only accepts primitives.
|
|
4826
|
+
{
|
|
4827
|
+
label: 'Region resolved',
|
|
4828
|
+
event: this.actions$.pipe(ofType(regionResolved), take(1), map(({ region, pingsMs }) => ({
|
|
4829
|
+
region,
|
|
4830
|
+
pingsMs: pingsMs.length ? `${pingsMs.join(', ')} ms` : null,
|
|
4831
|
+
}))),
|
|
4832
|
+
},
|
|
4833
|
+
],
|
|
4834
|
+
},
|
|
4835
|
+
{
|
|
4836
|
+
label: 'Signaling connection',
|
|
4837
|
+
color: '#c678dd',
|
|
4838
|
+
overlays: [
|
|
4839
|
+
{
|
|
4840
|
+
from: this.pollingStartedEntry,
|
|
4841
|
+
to: this.awsInstanceResolvedEntry,
|
|
4842
|
+
title: 'Polling → instance resolved (HTTP polling + AWS instance handle)',
|
|
4843
|
+
},
|
|
4844
|
+
{
|
|
4845
|
+
from: this.signalingServerConnectedEntry,
|
|
4846
|
+
to: this.configReceivedEntry,
|
|
4847
|
+
title: 'Signaling handshake (WS open → Cirrus config received)',
|
|
4848
|
+
},
|
|
4849
|
+
],
|
|
4850
|
+
milestones: [
|
|
4851
|
+
this.pollingStartedEntry,
|
|
4852
|
+
{
|
|
4853
|
+
label: 'Polling ended (HTTP orchestration)',
|
|
4854
|
+
event: this.actions$.pipe(ofType(pollingEnded)),
|
|
4855
|
+
},
|
|
4856
|
+
this.awsInstanceResolvedEntry,
|
|
4857
|
+
this.signalingServerConnectedEntry,
|
|
4858
|
+
{
|
|
4859
|
+
label: 'Instance reserved (WS orchestration)',
|
|
4860
|
+
event: this.actions$.pipe(ofType(setOrchestrationParameters)),
|
|
4861
|
+
},
|
|
4862
|
+
{
|
|
4863
|
+
label: 'Instance ready (WS orchestration)',
|
|
4864
|
+
event: this.actions$.pipe(ofType(instanceReady)),
|
|
4865
|
+
},
|
|
4866
|
+
this.configReceivedEntry,
|
|
4867
|
+
{
|
|
4868
|
+
label: 'SSInfo received',
|
|
4869
|
+
event: this.store.select(unrealFeature.selectSsData).pipe(filter$1(Boolean), take(1), withLatestFrom$1(this.store.select(unrealFeature.selectAwsInstance)), map(([ssData, aws]) => ({
|
|
4870
|
+
client: aws.wsUrl,
|
|
4871
|
+
instanceName: aws.instanceName,
|
|
4872
|
+
...ssData,
|
|
4873
|
+
}))),
|
|
4874
|
+
},
|
|
4875
|
+
],
|
|
4876
|
+
},
|
|
4877
|
+
{
|
|
4878
|
+
label: 'WebRTC negotiation',
|
|
4879
|
+
color: '#e5c07b',
|
|
4880
|
+
milestones: [
|
|
4881
|
+
{
|
|
4882
|
+
label: 'Offer received',
|
|
4883
|
+
event: this.signallingService.onOffer$.pipe(filter$1(Boolean)),
|
|
4884
|
+
},
|
|
4885
|
+
{
|
|
4886
|
+
label: 'Answer sent (server-offer flow)',
|
|
4887
|
+
event: this.actions$.pipe(ofType(answerSent)),
|
|
4888
|
+
},
|
|
4889
|
+
{
|
|
4890
|
+
label: 'Answer received (client-offer flow)',
|
|
4891
|
+
event: this.signallingService.onWebRtcAnswer$.pipe(filter$1(Boolean)),
|
|
4892
|
+
},
|
|
4893
|
+
{
|
|
4894
|
+
label: 'First ICE candidate from server',
|
|
4895
|
+
event: this.signallingService.onWebRtcIce$.pipe(filter$1(Boolean)),
|
|
4896
|
+
},
|
|
4897
|
+
{
|
|
4898
|
+
label: 'Last ICE candidate from server',
|
|
4899
|
+
event: this.signallingService.onWebRtcIce$.pipe(filter$1(Boolean)),
|
|
4900
|
+
mode: 'last',
|
|
4901
|
+
},
|
|
4902
|
+
// CLOSER — snaps negotiation diagnostics and the Cirrus
|
|
4903
|
+
// streamer identity (`ssData`, reliably populated by DC-open
|
|
4904
|
+
// time) into phase-3 params:
|
|
4905
|
+
// - `iceCandidateCount` (total ICE candidates from server — too
|
|
4906
|
+
// few hints at a restrictive network)
|
|
4907
|
+
// - `handshakeFlow` (server-offer vs client-offer; we only use
|
|
4908
|
+
// server-offer today, but the detector stays in place)
|
|
4909
|
+
// - `instanceType` / `branchInfo` / version / path / app /
|
|
4910
|
+
// `connectionId` (streamer identity, from `ssInfo` message)
|
|
4911
|
+
{
|
|
4912
|
+
label: 'Data channel connected',
|
|
4913
|
+
event: this.store
|
|
4914
|
+
.select(unrealFeature.selectDataChannelConnected)
|
|
4915
|
+
.pipe(filter$1(Boolean), take(1), map(() => ({
|
|
4916
|
+
iceCandidateCount: this.session?.iceCandidateCount ?? 0,
|
|
4917
|
+
handshakeFlow: this.session?.handshakeFlow ?? null,
|
|
4918
|
+
}))),
|
|
4919
|
+
},
|
|
4920
|
+
],
|
|
4921
|
+
},
|
|
4922
|
+
{
|
|
4923
|
+
label: 'Unreal communication',
|
|
4924
|
+
color: '#98c379',
|
|
4925
|
+
overlays: [
|
|
4926
|
+
{
|
|
4927
|
+
from: this.loopbackReceivedEntry,
|
|
4928
|
+
title: 'Stabilization (warm up)',
|
|
4929
|
+
},
|
|
4930
|
+
],
|
|
4931
|
+
milestones: [
|
|
4932
|
+
{
|
|
4933
|
+
label: 'First command started',
|
|
4934
|
+
event: this.actions$.pipe(ofType(commandStarted)),
|
|
4935
|
+
},
|
|
4936
|
+
{
|
|
4937
|
+
label: 'LoopBack sent',
|
|
4938
|
+
event: this.actions$.pipe(ofType(commandStarted), filter$1(({ command }) => command === 'loopBack')),
|
|
4939
|
+
},
|
|
4940
|
+
this.loopbackReceivedEntry,
|
|
4941
|
+
// CLOSER — flattens the selected ICE candidate pair into two
|
|
4942
|
+
// string params (local / remote), and snapshots the running
|
|
4943
|
+
// command counter so phase 5 shows how many commands the client
|
|
4944
|
+
// fired at Unreal before the viewport was ready.
|
|
4945
|
+
{
|
|
4946
|
+
label: 'Viewport ready',
|
|
4947
|
+
event: this.store.select(unrealFeature.selectViewportReady).pipe(filter$1(Boolean), take(1), withLatestFrom$1(this.selectedPair$), map(([, selectedPair]) => ({
|
|
4948
|
+
localCandidate: fmtCandidate(selectedPair?.local),
|
|
4949
|
+
remoteCandidate: fmtCandidate(selectedPair?.remote),
|
|
4950
|
+
commandCount: this.session?.commandCount ?? 0,
|
|
4951
|
+
}))),
|
|
4952
|
+
},
|
|
4953
|
+
],
|
|
4954
|
+
},
|
|
4955
|
+
];
|
|
4956
|
+
if (this.unrealConfig?.playwright)
|
|
4957
|
+
return;
|
|
4958
|
+
if (!this.unrealConfig?.streamTelemetryV2)
|
|
4959
|
+
return;
|
|
4960
|
+
this.init();
|
|
4961
|
+
}
|
|
4962
|
+
getOrCreateUserId() {
|
|
4963
|
+
try {
|
|
4964
|
+
const existing = localStorage.getItem(USER_ID_STORAGE_KEY);
|
|
4965
|
+
if (existing && existing.length > 0)
|
|
4966
|
+
return existing;
|
|
4967
|
+
const fresh = v4();
|
|
4968
|
+
localStorage.setItem(USER_ID_STORAGE_KEY, fresh);
|
|
4969
|
+
return fresh;
|
|
4970
|
+
}
|
|
4971
|
+
catch {
|
|
4972
|
+
// localStorage disabled (private-mode restrictions, quota, etc.)
|
|
4973
|
+
// Fall back to a per-tab ephemeral id so telemetry keeps working.
|
|
4974
|
+
return v4();
|
|
4975
|
+
}
|
|
4976
|
+
}
|
|
4977
|
+
init() {
|
|
4978
|
+
// Start a new session on every initSignalling dispatch.
|
|
4979
|
+
this.actions$
|
|
4980
|
+
.pipe(ofType(initSignalling))
|
|
4981
|
+
.subscribe(() => this.startSession());
|
|
4982
|
+
// End the current session when Cirrus drops (true → false transition).
|
|
4983
|
+
this.store
|
|
4984
|
+
.select(unrealFeature.selectCirrusConnected)
|
|
4985
|
+
.pipe(pairwise(), filter$1(([prev, curr]) => prev && !curr))
|
|
4986
|
+
.subscribe(() => this.endSession('disconnected'));
|
|
4987
|
+
// Close the session when the tab is hidden / being evicted from bfcache.
|
|
4988
|
+
// `pagehide` fires in more cases than `beforeunload` (covers iOS Safari, bfcache).
|
|
4989
|
+
fromEvent(window, 'pagehide').subscribe(() => this.endSession('pagehide'));
|
|
4990
|
+
}
|
|
4991
|
+
startSession() {
|
|
4992
|
+
// If a previous session is still active, close it first.
|
|
4993
|
+
this.endSession('restart');
|
|
4994
|
+
const id = v4();
|
|
4995
|
+
const startedAt = Date.now();
|
|
4996
|
+
const startedAtPerf = performance.now();
|
|
4997
|
+
const sub = new Subscription();
|
|
4998
|
+
// Anchor selection:
|
|
4999
|
+
// - First session on this page → anchor at navigation start (0).
|
|
5000
|
+
// `performance.now()` is already "ms since navigation start", so
|
|
5001
|
+
// `session_started.duration` becomes the full page-load + idle-time
|
|
5002
|
+
// leading up to the first stream start (i.e. the real bootstrap).
|
|
5003
|
+
// - Resume/restart sessions → anchor at the session's own start.
|
|
5004
|
+
// The document is already loaded — a "bootstrap" phase would either
|
|
5005
|
+
// balloon (to include all prior idle time) or be misleading, so we
|
|
5006
|
+
// suppress it by making the anchor == session_started.
|
|
5007
|
+
const isFirstSession = !this.hasStartedBefore;
|
|
5008
|
+
this.hasStartedBefore = true;
|
|
5009
|
+
const anchorPerf = isFirstSession ? 0 : startedAtPerf;
|
|
5010
|
+
const times = new Map();
|
|
5011
|
+
this.session = {
|
|
5012
|
+
id,
|
|
5013
|
+
startedAt,
|
|
5014
|
+
startedAtPerf,
|
|
5015
|
+
anchorPerf,
|
|
5016
|
+
// Initial milestone anchor = anchorPerf. For the first session this is
|
|
5017
|
+
// navigation start (0), so `session_started.duration` becomes the full
|
|
5018
|
+
// page-load elapsed. For Resume sessions anchor == startedAtPerf, so
|
|
5019
|
+
// `session_started.duration` is 0 (no bootstrap phase).
|
|
5020
|
+
lastMilestonePerf: anchorPerf,
|
|
5021
|
+
seq: 0,
|
|
5022
|
+
sub,
|
|
5023
|
+
qpBad: false,
|
|
5024
|
+
fpsLow: false,
|
|
5025
|
+
maxFpsTarget: UNLIMITED_REFERENCE_FPS,
|
|
5026
|
+
iceCandidateCount: 0,
|
|
5027
|
+
handshakeFlow: null,
|
|
5028
|
+
commandCount: 0,
|
|
5029
|
+
times,
|
|
5030
|
+
};
|
|
5031
|
+
// Side-channel counters for WebRTC negotiation diagnostics — shown
|
|
5032
|
+
// as phase-3 params on the `datachannel_connected` closer. Kept
|
|
5033
|
+
// imperative (session-scoped) so no RxJS hot-stream plumbing is
|
|
5034
|
+
// needed to survive the subscribe-on-close timing.
|
|
5035
|
+
sub.add(this.signallingService.onWebRtcIce$
|
|
5036
|
+
.pipe(filter$1(Boolean))
|
|
5037
|
+
.subscribe(() => {
|
|
5038
|
+
if (this.session)
|
|
5039
|
+
this.session.iceCandidateCount++;
|
|
5040
|
+
}));
|
|
5041
|
+
sub.add(this.actions$.pipe(ofType(answerSent), take(1)).subscribe(() => {
|
|
5042
|
+
if (this.session)
|
|
5043
|
+
this.session.handshakeFlow = 'server-offer';
|
|
5044
|
+
}));
|
|
5045
|
+
sub.add(this.signallingService.onWebRtcAnswer$
|
|
5046
|
+
.pipe(filter$1(Boolean), take(1))
|
|
5047
|
+
.subscribe(() => {
|
|
5048
|
+
if (this.session)
|
|
5049
|
+
this.session.handshakeFlow = 'client-offer';
|
|
5050
|
+
}));
|
|
5051
|
+
// Unreal-command counter — increments on every `commandStarted`
|
|
5052
|
+
// action. Snapshotted into the `viewport_ready` closer payload as
|
|
5053
|
+
// phase-5's `commandCount`.
|
|
5054
|
+
sub.add(this.actions$.pipe(ofType(commandStarted)).subscribe(() => {
|
|
5055
|
+
if (this.session)
|
|
5056
|
+
this.session.commandCount++;
|
|
5057
|
+
}));
|
|
5058
|
+
// Unified catalog loop — one subscription per entry. Non-closers
|
|
5059
|
+
// record their emission time into the session map. The *last* entry
|
|
5060
|
+
// of each phase is the closer: its emission fires a single generic
|
|
5061
|
+
// `stream.phase_ended` event carrying every display field the viewer
|
|
5062
|
+
// needs. The viewer never inspects milestone names or phase kinds;
|
|
5063
|
+
// adding a new phase to `phaseCatalog` is enough — zero viewer work.
|
|
5064
|
+
this.phaseCatalog.forEach((bucket, bucketIdx) => {
|
|
5065
|
+
bucket.milestones.forEach((entry, idx) => {
|
|
5066
|
+
const isCloser = idx === bucket.milestones.length - 1;
|
|
5067
|
+
sub.add(entry.event.subscribe((value) => {
|
|
5068
|
+
if (!this.session)
|
|
5069
|
+
return;
|
|
5070
|
+
if (isCloser) {
|
|
5071
|
+
this.emitPhaseEnded(bucket, bucketIdx, value);
|
|
5072
|
+
return;
|
|
5073
|
+
}
|
|
5074
|
+
const time = Math.round(performance.now() - this.session.anchorPerf);
|
|
5075
|
+
if (entry.mode !== 'last' && this.session.times.has(entry))
|
|
5076
|
+
return;
|
|
5077
|
+
this.session.times.set(entry, time);
|
|
5078
|
+
}));
|
|
5079
|
+
});
|
|
5080
|
+
});
|
|
5081
|
+
const bootstrapInfo = this.collectBootstrapInfo();
|
|
5082
|
+
this.emitLifecycle('stream.session_started', {
|
|
5083
|
+
sessionId: id,
|
|
5084
|
+
startTimeUtc: new Date(startedAt).toISOString(),
|
|
5085
|
+
pixelRatio: window.devicePixelRatio,
|
|
5086
|
+
// Browser viewport size at session start (`window.innerWidth` /
|
|
5087
|
+
// `innerHeight`). Named `browserInnerSize` to distinguish from
|
|
5088
|
+
// the physical screen — what gates the rendered video resolution
|
|
5089
|
+
// is the container we started from, not the device hardware.
|
|
5090
|
+
browserInnerSize: {
|
|
5091
|
+
width: window.innerWidth,
|
|
5092
|
+
height: window.innerHeight,
|
|
5093
|
+
},
|
|
5094
|
+
userAgent: navigator.userAgent,
|
|
5095
|
+
sampleWindowMs: SAMPLE_WINDOW_MS,
|
|
5096
|
+
// Full page URL — `location.href` is the only reliable way to get this.
|
|
5097
|
+
// The server-observed `Referer` header is truncated to origin for
|
|
5098
|
+
// cross-origin requests by default Referrer-Policy.
|
|
5099
|
+
pageUrl: window.location.href,
|
|
5100
|
+
// Navigation / page-load context for the "app bootstrap → session_started"
|
|
5101
|
+
// waterfall phase. Only useful for the first session (`bootstrap` phase
|
|
5102
|
+
// is suppressed on Resume), but `bootstrapInfo` describes the document,
|
|
5103
|
+
// not the session — harmless and cheap to send either way.
|
|
5104
|
+
...(bootstrapInfo && { bootstrapInfo }),
|
|
5105
|
+
},
|
|
5106
|
+
// Pin the milestone to the captured `performance.now()` so `duration`
|
|
5107
|
+
// equals "ms since navigation start" precisely (for first sessions),
|
|
5108
|
+
// rather than being inflated by any synchronous setup done in between.
|
|
5109
|
+
startedAtPerf);
|
|
5110
|
+
// Track current maxFps target to evaluate FPS-ratio anomalies.
|
|
5111
|
+
sub.add(this.actions$
|
|
5112
|
+
.pipe(ofType(setMaxFps))
|
|
5113
|
+
.subscribe(({ maxFps }) => this.updateMaxFpsTarget(maxFps)));
|
|
5114
|
+
// Periodic aggregated sample — every 5s window.
|
|
5115
|
+
sub.add(this.videoService.videoStats$
|
|
5116
|
+
.pipe(bufferTime(SAMPLE_WINDOW_MS), filter$1((samples) => samples.length > 0))
|
|
5117
|
+
.subscribe((samples) => this.handleWindow(samples)));
|
|
5118
|
+
}
|
|
5119
|
+
endSession(reason) {
|
|
5120
|
+
if (!this.session)
|
|
5121
|
+
return;
|
|
5122
|
+
const { id, sub } = this.session;
|
|
5123
|
+
this.emitLifecycle('stream.session_ended', {
|
|
5124
|
+
sessionId: id,
|
|
5125
|
+
reason,
|
|
5126
|
+
timestamp: new Date().toISOString(),
|
|
5127
|
+
});
|
|
5128
|
+
sub.unsubscribe();
|
|
5129
|
+
this.session = null;
|
|
5130
|
+
}
|
|
5131
|
+
/**
|
|
5132
|
+
* Emit a lifecycle event with a `duration` field — ms between the
|
|
5133
|
+
* previous lifecycle event and now (phase-local, NOT anchor-relative).
|
|
5134
|
+
* For `session_started` this equals the full page-load elapsed on the
|
|
5135
|
+
* first session, or 0 on Resume. Advances `lastMilestonePerf` so the
|
|
5136
|
+
* next event's duration is correctly rooted.
|
|
5137
|
+
*/
|
|
5138
|
+
emitLifecycle(event, extra, atPerf) {
|
|
5139
|
+
if (!this.session)
|
|
5140
|
+
return;
|
|
5141
|
+
// `atPerf` lets callers pin the milestone to a specific `performance.now()`
|
|
5142
|
+
// value — useful for `session_started` where the wall-clock and perf-clock
|
|
5143
|
+
// anchors were already captured at the top of `startSession()`.
|
|
5144
|
+
const now = atPerf ?? performance.now();
|
|
5145
|
+
const duration = Math.round(now - this.session.lastMilestonePerf);
|
|
5146
|
+
this.track(event, { duration, ...extra });
|
|
5147
|
+
this.session.lastMilestonePerf = now;
|
|
5148
|
+
}
|
|
5149
|
+
/**
|
|
5150
|
+
* Emit the unified `stream.phase_ended` wire event for a closer. Packs
|
|
5151
|
+
* every display field the viewer consumes — phaseNo (derived from the
|
|
5152
|
+
* catalog index), label, color, milestones, params, overlays. Boundary
|
|
5153
|
+
* labels aren't on the wire — the viewer derives them from neighbour
|
|
5154
|
+
* phases' `label` fields.
|
|
5155
|
+
*/
|
|
5156
|
+
emitPhaseEnded(bucket, bucketIdx, extrasValue) {
|
|
5157
|
+
if (!this.session)
|
|
5158
|
+
return;
|
|
5159
|
+
const now = performance.now();
|
|
5160
|
+
const phaseStartMs = Math.round(this.session.lastMilestonePerf - this.session.anchorPerf);
|
|
5161
|
+
const phaseEndMs = Math.round(now - this.session.anchorPerf);
|
|
5162
|
+
const milestones = this.milestonesOfBucket(bucket);
|
|
5163
|
+
const params = toPhaseParams(extrasValue);
|
|
5164
|
+
const overlays = resolveOverlays(bucket.overlays, this.session.times, phaseStartMs, phaseEndMs);
|
|
5165
|
+
this.emitLifecycle('stream.phase_ended', {
|
|
5166
|
+
sessionId: this.session.id,
|
|
5167
|
+
phaseNo: bucketIdx + 1,
|
|
5168
|
+
label: bucket.label,
|
|
5169
|
+
color: bucket.color,
|
|
5170
|
+
milestones,
|
|
5171
|
+
params,
|
|
5172
|
+
overlays,
|
|
5173
|
+
});
|
|
5174
|
+
}
|
|
5175
|
+
handleWindow(samples) {
|
|
5176
|
+
if (!this.session)
|
|
5177
|
+
return;
|
|
5178
|
+
const s = this.session;
|
|
5179
|
+
const bitrate = this.aggregate(samples.map((x) => x.aggregatedStats.bitrate ?? 0));
|
|
5180
|
+
const fps = this.aggregate(samples.map((x) => x.aggregatedStats.framesPerSecond ?? 0));
|
|
5181
|
+
const qp = this.aggregate(samples.map((x) => x.aggregatedStats.VideoEncoderQP ?? 0));
|
|
5182
|
+
const jitter = this.aggregate(samples.map((x) => x.aggregatedStats.jitter ?? 0));
|
|
5183
|
+
const rtt = this.aggregate(samples.map((x) => x.aggregatedStats.currentRoundTripTime ?? 0));
|
|
5184
|
+
const latest = samples[samples.length - 1]?.aggregatedStats;
|
|
5185
|
+
// Periodic sample event.
|
|
5186
|
+
this.track('stream.sample', {
|
|
5187
|
+
sessionId: s.id,
|
|
5188
|
+
seq: s.seq++,
|
|
5189
|
+
timestamp: new Date().toISOString(),
|
|
5190
|
+
sampleCount: samples.length,
|
|
5191
|
+
// Tab visibility state at emission time. Chrome does NOT throttle timers
|
|
5192
|
+
// for WebRTC-active tabs, so the handler runs on time even when hidden —
|
|
5193
|
+
// the point-in-time read is reliable in this specific context.
|
|
5194
|
+
visibilityState: document.visibilityState,
|
|
5195
|
+
bitrate,
|
|
5196
|
+
fps,
|
|
5197
|
+
qp,
|
|
5198
|
+
jitter,
|
|
5199
|
+
rtt,
|
|
5200
|
+
packetsLost: latest?.packetsLost,
|
|
5201
|
+
viewPort: {
|
|
5202
|
+
width: latest?.frameWidth,
|
|
5203
|
+
height: latest?.frameHeight,
|
|
5204
|
+
},
|
|
5205
|
+
});
|
|
5206
|
+
this.evaluateAnomalies(fps, qp);
|
|
5207
|
+
}
|
|
5208
|
+
evaluateAnomalies(fps, qp) {
|
|
5209
|
+
if (!this.session)
|
|
5210
|
+
return;
|
|
5211
|
+
const s = this.session;
|
|
5212
|
+
// Quality (QP red) entry/exit.
|
|
5213
|
+
const qpBadNow = qp.p50 > QP_RED_THRESHOLD;
|
|
5214
|
+
if (qpBadNow !== s.qpBad) {
|
|
5215
|
+
s.qpBad = qpBadNow;
|
|
5216
|
+
this.track(qpBadNow ? 'stream.quality_degraded' : 'stream.quality_recovered', {
|
|
5217
|
+
sessionId: s.id,
|
|
5218
|
+
seq: s.seq,
|
|
5219
|
+
timestamp: new Date().toISOString(),
|
|
5220
|
+
qpP50: qp.p50,
|
|
5221
|
+
qpP95: qp.p95,
|
|
5222
|
+
});
|
|
5223
|
+
}
|
|
5224
|
+
// FPS low vs current target.
|
|
5225
|
+
const ref = s.maxFpsTarget > 0 ? s.maxFpsTarget : UNLIMITED_REFERENCE_FPS;
|
|
5226
|
+
const fpsLowNow = fps.p50 < ref * FPS_LOW_RATIO;
|
|
5227
|
+
if (fpsLowNow && !s.fpsLow) {
|
|
5228
|
+
this.track('stream.fps_low', {
|
|
5229
|
+
sessionId: s.id,
|
|
5230
|
+
seq: s.seq,
|
|
5231
|
+
timestamp: new Date().toISOString(),
|
|
5232
|
+
target: s.maxFpsTarget,
|
|
5233
|
+
referenceTarget: ref,
|
|
5234
|
+
fpsP50: fps.p50,
|
|
5235
|
+
fpsP95: fps.p95,
|
|
5236
|
+
});
|
|
5237
|
+
}
|
|
5238
|
+
s.fpsLow = fpsLowNow;
|
|
5239
|
+
}
|
|
5240
|
+
updateMaxFpsTarget(maxFps) {
|
|
5241
|
+
if (this.session)
|
|
5242
|
+
this.session.maxFpsTarget = maxFps;
|
|
5243
|
+
}
|
|
5244
|
+
/**
|
|
5245
|
+
* Snapshot every milestone recorded in this session that belongs to
|
|
5246
|
+
* {@link phase}. Used to attach a phase's milestone array to the
|
|
5247
|
+
* lifecycle event that closes that phase.
|
|
5248
|
+
*/
|
|
5249
|
+
/**
|
|
5250
|
+
* Collect wire-shape milestones that fired within a bucket. Walks the
|
|
5251
|
+
* catalog's own milestone list (minus the closer) and pulls each
|
|
5252
|
+
* entry's time from the in-session map via object identity.
|
|
5253
|
+
*/
|
|
5254
|
+
milestonesOfBucket(bucket) {
|
|
5255
|
+
if (!this.session)
|
|
5256
|
+
return [];
|
|
5257
|
+
const out = [];
|
|
5258
|
+
const closerIdx = bucket.milestones.length - 1;
|
|
5259
|
+
for (let i = 0; i < closerIdx; i++) {
|
|
5260
|
+
const entry = bucket.milestones[i];
|
|
5261
|
+
const time = this.session.times.get(entry);
|
|
5262
|
+
if (time !== undefined)
|
|
5263
|
+
out.push({ label: entry.label, time });
|
|
5264
|
+
}
|
|
5265
|
+
return out;
|
|
5266
|
+
}
|
|
5267
|
+
/**
|
|
5268
|
+
* Capture browser navigation timings — useful to explain the
|
|
5269
|
+
* "application bootstrap → session_started" waterfall phase.
|
|
5270
|
+
* Values are ms from navigation start; null if the entry is unavailable
|
|
5271
|
+
* (old browsers, cross-origin iframes with limited perf access, etc.).
|
|
5272
|
+
*/
|
|
5273
|
+
collectBootstrapInfo() {
|
|
5274
|
+
try {
|
|
5275
|
+
const nav = performance.getEntriesByType('navigation')[0];
|
|
5276
|
+
if (!nav)
|
|
5277
|
+
return null;
|
|
5278
|
+
return {
|
|
5279
|
+
type: nav.type, // 'navigate' | 'reload' | 'back_forward' | 'prerender'
|
|
5280
|
+
protocol: nav.nextHopProtocol || null, // 'h2' / 'h3' / 'http/1.1'
|
|
5281
|
+
firstByteMs: Math.round(nav.responseStart),
|
|
5282
|
+
domInteractiveMs: Math.round(nav.domInteractive),
|
|
5283
|
+
domContentLoadedMs: Math.round(nav.domContentLoadedEventEnd),
|
|
5284
|
+
loadEventMs: Math.round(nav.loadEventEnd),
|
|
5285
|
+
// Navigation-only size — the HTML document itself. Usually small;
|
|
5286
|
+
// the interesting number is `resources.transferSize` below.
|
|
5287
|
+
navigationTransferSize: nav.transferSize || null,
|
|
5288
|
+
navigationEncodedBodySize: nav.encodedBodySize || null,
|
|
5289
|
+
navigationDecodedBodySize: nav.decodedBodySize || null,
|
|
5290
|
+
resources: this.collectResourceTotals(),
|
|
5291
|
+
};
|
|
5292
|
+
}
|
|
5293
|
+
catch {
|
|
5294
|
+
return null;
|
|
5295
|
+
}
|
|
5296
|
+
}
|
|
5297
|
+
/**
|
|
5298
|
+
* Aggregate all resources the browser has loaded so far
|
|
5299
|
+
* (`performance.getEntriesByType('resource')`), grouped by `initiatorType`.
|
|
5300
|
+
* Gives a real "bytes downloaded before session start" figure — unlike the
|
|
5301
|
+
* navigation entry which only covers the HTML document.
|
|
5302
|
+
*
|
|
5303
|
+
* `transferSize === 0 && decodedBodySize > 0` means the resource came from
|
|
5304
|
+
* the HTTP cache — counted separately so cached hits don't inflate
|
|
5305
|
+
* the "bytes on the wire" metric.
|
|
5306
|
+
*/
|
|
5307
|
+
collectResourceTotals() {
|
|
5308
|
+
try {
|
|
5309
|
+
const list = performance.getEntriesByType('resource');
|
|
5310
|
+
if (!list.length)
|
|
5311
|
+
return null;
|
|
5312
|
+
const empty = () => ({
|
|
5313
|
+
count: 0,
|
|
5314
|
+
cachedCount: 0,
|
|
5315
|
+
transferSize: 0,
|
|
5316
|
+
encodedBodySize: 0,
|
|
5317
|
+
decodedBodySize: 0,
|
|
5318
|
+
});
|
|
5319
|
+
const totals = empty();
|
|
5320
|
+
const byType = {};
|
|
5321
|
+
for (const r of list) {
|
|
5322
|
+
const type = r.initiatorType || 'other';
|
|
5323
|
+
const b = (byType[type] ||= empty());
|
|
5324
|
+
const fromCache = r.transferSize === 0 && r.decodedBodySize > 0;
|
|
5325
|
+
const t = r.transferSize || 0;
|
|
5326
|
+
const enc = r.encodedBodySize || 0;
|
|
5327
|
+
const dec = r.decodedBodySize || 0;
|
|
5328
|
+
b.count++;
|
|
5329
|
+
if (fromCache)
|
|
5330
|
+
b.cachedCount++;
|
|
5331
|
+
b.transferSize += t;
|
|
5332
|
+
b.encodedBodySize += enc;
|
|
5333
|
+
b.decodedBodySize += dec;
|
|
5334
|
+
totals.count++;
|
|
5335
|
+
if (fromCache)
|
|
5336
|
+
totals.cachedCount++;
|
|
5337
|
+
totals.transferSize += t;
|
|
5338
|
+
totals.encodedBodySize += enc;
|
|
5339
|
+
totals.decodedBodySize += dec;
|
|
5340
|
+
}
|
|
5341
|
+
return { ...totals, byType };
|
|
5342
|
+
}
|
|
5343
|
+
catch {
|
|
5344
|
+
return null;
|
|
5345
|
+
}
|
|
5346
|
+
}
|
|
5347
|
+
aggregate(values) {
|
|
5348
|
+
if (values.length === 0)
|
|
5349
|
+
return { p50: 0, p95: 0, min: 0, max: 0 };
|
|
5350
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
5351
|
+
// Nearest-rank method: p-th percentile ≡ sorted[ceil(p/100 * n) - 1].
|
|
5352
|
+
// Using `floor` would make p95 collapse to `max` for n=20
|
|
5353
|
+
// (floor(0.95*20) = 19 = max index), hiding the max line behind p95
|
|
5354
|
+
// in charts. `ceil - 1` keeps them distinct: p95 = sorted[18], max = sorted[19].
|
|
5355
|
+
const pick = (p) => {
|
|
5356
|
+
const idx = Math.ceil((p / 100) * sorted.length) - 1;
|
|
5357
|
+
return sorted[Math.max(0, Math.min(sorted.length - 1, idx))];
|
|
5358
|
+
};
|
|
5359
|
+
return {
|
|
5360
|
+
p50: pick(50),
|
|
5361
|
+
p95: pick(95),
|
|
5362
|
+
min: sorted[0],
|
|
5363
|
+
max: sorted[sorted.length - 1],
|
|
5364
|
+
};
|
|
5365
|
+
}
|
|
5366
|
+
track(event, data) {
|
|
5367
|
+
// `userId` is attached to every event so the server can bucket files
|
|
5368
|
+
// under `data/<userId>/<sessionId>.jsonl` without maintaining a
|
|
5369
|
+
// session→user index.
|
|
5370
|
+
this.store.dispatch(trackTelemetryData({
|
|
5371
|
+
event,
|
|
5372
|
+
data: { userId: this.userId, ...data },
|
|
5373
|
+
}));
|
|
5374
|
+
}
|
|
5375
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.10", ngImport: i0, type: StreamStatusTelemetryService2, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
5376
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.10", ngImport: i0, type: StreamStatusTelemetryService2 }); }
|
|
5377
|
+
}
|
|
5378
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.10", ngImport: i0, type: StreamStatusTelemetryService2, decorators: [{
|
|
5379
|
+
type: Injectable
|
|
5380
|
+
}], ctorParameters: () => [] });
|
|
5381
|
+
// ──────────────────────────────────────────────────────────────────
|
|
5382
|
+
// Module-scope helpers — keep the class body focused on RxJS wiring.
|
|
5383
|
+
// ──────────────────────────────────────────────────────────────────
|
|
5384
|
+
/** Compact one side of an ICE candidate pair to `"type (url)"` or just
|
|
5385
|
+
* `type` (host candidates have no URL). */
|
|
5386
|
+
function fmtCandidate(c) {
|
|
5387
|
+
if (!c)
|
|
5388
|
+
return null;
|
|
5389
|
+
return c.url ? `${c.type} (${c.url})` : c.type;
|
|
5390
|
+
}
|
|
5391
|
+
/**
|
|
5392
|
+
* Flatten the closer's emitted extras object into display key/value
|
|
5393
|
+
* pairs. Null/undefined/empty are dropped; objects and arrays are
|
|
5394
|
+
* skipped — they should be pre-formatted by the closer's `map(...)`.
|
|
5395
|
+
*/
|
|
5396
|
+
function toPhaseParams(value) {
|
|
5397
|
+
if (!value || typeof value !== 'object')
|
|
5398
|
+
return [];
|
|
5399
|
+
const out = [];
|
|
5400
|
+
for (const [key, v] of Object.entries(value)) {
|
|
5401
|
+
if (v === null || v === undefined || v === '')
|
|
5402
|
+
continue;
|
|
5403
|
+
if (typeof v === 'object')
|
|
5404
|
+
continue;
|
|
5405
|
+
out.push({ key, value: String(v) });
|
|
5406
|
+
}
|
|
5407
|
+
return out;
|
|
5408
|
+
}
|
|
5409
|
+
/**
|
|
5410
|
+
* Resolve declarative overlay recipes to absolute ms spans. Endpoints
|
|
5411
|
+
* fall back to the phase's own start/end when unspecified; recipes
|
|
5412
|
+
* whose referenced entry never fired are quietly skipped.
|
|
5413
|
+
*/
|
|
5414
|
+
function resolveOverlays(recipes, times, phaseStartMs, phaseEndMs) {
|
|
5415
|
+
if (!recipes?.length)
|
|
5416
|
+
return [];
|
|
5417
|
+
const out = [];
|
|
5418
|
+
for (const r of recipes) {
|
|
5419
|
+
const fromMs = r.from ? (times.get(r.from) ?? null) : phaseStartMs;
|
|
5420
|
+
const toMs = r.to ? (times.get(r.to) ?? null) : phaseEndMs;
|
|
5421
|
+
if (fromMs === null || toMs === null || toMs <= fromMs)
|
|
5422
|
+
continue;
|
|
5423
|
+
out.push({ fromMs, toMs, title: r.title });
|
|
5424
|
+
}
|
|
5425
|
+
return out;
|
|
5426
|
+
}
|
|
5427
|
+
|
|
4631
5428
|
const ReceivedMimeTypes = {
|
|
4632
5429
|
ApplicationJson: 'application/json',
|
|
4633
5430
|
};
|
|
@@ -6089,12 +6886,14 @@ function provideUnrealModule() {
|
|
|
6089
6886
|
AFKService,
|
|
6090
6887
|
SignallingService,
|
|
6091
6888
|
StreamStatusTelemetryService,
|
|
6889
|
+
StreamStatusTelemetryService2,
|
|
6092
6890
|
FpsMonitorService,
|
|
6093
6891
|
WebRtcPlayerService,
|
|
6094
6892
|
provideEnvironmentInitializer(() => {
|
|
6095
6893
|
inject(AggregatorService);
|
|
6096
6894
|
inject(InputService);
|
|
6097
6895
|
inject(StreamStatusTelemetryService);
|
|
6896
|
+
inject(StreamStatusTelemetryService2);
|
|
6098
6897
|
inject(ConsoleExtensionsService);
|
|
6099
6898
|
inject(AFKService);
|
|
6100
6899
|
inject(FreezeFrameService);
|
|
@@ -6108,5 +6907,5 @@ function provideUnrealModule() {
|
|
|
6108
6907
|
* Generated bundle index. Do not edit.
|
|
6109
6908
|
*/
|
|
6110
6909
|
|
|
6111
|
-
export { AFKService, AfkTimeoutModalComponent, AggregatorService, AnalyticsService, AnswerHandler, CONSOLE_COMMAND_DISABLE_MESSAGES, CONSOLE_COMMAND_ENABLE_MESSAGES, CONSOLE_COMMAND_PIXEL_QUALITY, CommandTelemetryService, CommandsEffects, ConfigHandler, ConsoleExtensionsService, ControlScheme, DATA_CHANNEL_CONNECTION_TIMEOUT, DEBOUNCE_TO_MANY_RESIZE_CALLS, DEFAULT_AFK_TIMEOUT, DEFAULT_AFK_TIMEOUT_PERIOD, DEFAULT_RECONNECT_DELAY_MS, DEFAULT_RECONNECT_ENABLED, DEFAULT_RECONNECT_MAX_ATTEMPTS, DEFAULT_RECONNECT_ON_DATACHANNEL_CLOSE, DEFAULT_RECONNECT_ON_ICE_FAILURE, DataFlowMonitor, DevModeService, DisconnectReason, FULL_HD_HEIGHT, FULL_HD_WIDTH, FileHandlerService, FileReceiverService, FilterSettingsComponent, FpsMonitorService, FreezeFrameComponent, FreezeFrameService, IceCandidateHandler, ImageLoadingSrcComponent, InputOptions, InputService, InstanceReadyHandler, InstanceReservedHandler, IntroSrcComponent, KalmanFilter1D, LatencyTimings, LowBandwidthDetectorComponent, LowBandwidthModalComponent, MINIMAL_FPS, MessageType, MouseButton, MouseButtonsMask, OnCloseHandler, OnErrorHandler, OnMessageHandler, OnOpenHandler, OrchestrationMessageTypes, POLLING_TIME, PingHandler, PlayerCountHandler, RegionsPingService, SAME_SIZE_THRESHOLD, SCREEN_LOCKER_CONTAINER_ID, SIGNALLING_PERCENT_VALUE, SSInfoHandler, STREAMING_VIDEO_ID, SafePipe, SignallingService, SpecialKeyCodes, StatGraphComponent, StreamStatusTelemetryService, SubService, ToClientMessage, UNREAL_CONFIG, UnrealCallbackService, UnrealCommunicatorService, UnrealEffects, UnrealSceneComponent, UnrealStatusMessage, VideoService, VideoStatsComponent, WSCloseCode_CIRRUS_ABNORMAL_CLOSURE, WSCloseCode_CIRRUS_MAX_PLAYERS_ERROR, WSCloseCode_CIRRUS_PLAYER_DISCONNECTED, WSCloseCode_CIRRUS_STREAMER_KIKED_PLAYER, WSCloseCode_FORCE_CIRRUS_CLOSE, WSCloseCode_NORMAL_AFK_TIMEOUT, WSCloseCode_NORMAL_CLOSURE, WSCloseCode_NORMAL_MANUAL_DISCONNECT, WSCloseCode_UNKNOWN, WSCloseCodes, WS_OPEN_STATE, WS_TIMEOUT, WebRtcPlayerService, WebrtcErrorModalComponent, abortEstablishingConnection, changeLowBandwidth, changeStatusMainVideoOnScene, changeStreamResolutionAction, changeStreamResolutionSuccessAction, commandCompleted, commandStarted, commandsFeature, commandsReducer, dataChannelConnected, dataChannelReady, decodeData, destroyRemoteConnections, destroyUnrealScene, disconnectStream, dispatchResize, dropConnection, dropDC, dropPC, floatToSmoothPercents, forceResizeViewport, fromResizeObserver, getActiveUrl, getImageFromVideoStream, getResizeStudioValues, getResizeStudioValuesOld, getResizeValuesUniversal, getResizeValuesUniversalOld, getRtcErrorMessage, iceConnectionFailed, initSignalling, keepMaxUntilReset, makeEven, provideUnrealModule, qpToQuality, reconnectPeer, reconnectPeerFailed, reconnectPeerSuccess, resetAfk, resetConfig, resetDataChannelForReconnect, resetIntroSrc, resetWarnTimeout, saveAnalyticsEvent, selectClientAndViewIds, selectCommandProgress, selectCommandsInProgress, selectFreezeFrameCombinedDataUrl, selectFreezeFrameDataUrl, selectFreezeFrameDataUrlFromVideo, selectFreezeFrameProgressMessageFromVideo, selectIsAutostart, selectIsExistMatchUrls, selectIsFreezeFrameLoading, selectIsLoaderScreenVisible, selectIsVideoPlayingAndDataChannelConnected, selectShowLoader, selectShowReconnectPopup, selectSignalingParameters, selectStreamConfig, selectTotalProgress, selectWarnTimeout, setAfkTimerCountdown, setAfkTimerHide, setAfkTimerVisible, setAwsInstance, setCirrusConnected, setCirrusDisconnected, setConfig, setDataChannelConnected, setEstablishingConnection, setFreezeFrame, setFreezeFrameFromVideo, setIntroImageSrc, setIntroVideoSrc, setKalmanSettings, setLoadingImageSrc, setLoopBackCommandIsCompleted, setMaxFps, setOrchestrationContext, setOrchestrationMessage, setOrchestrationParameters, setOrchestrationProgress, setSignalingName, setStatusMessage, setStatusPercentSignallingServer, setStreamClientCompanyId, setStreamViewId, setUnrealPlaywrightConfig, setViewportNotReady, setViewportReady, showUnrealErrorMessage, smoothTransition, startStream, trackMixpanelEvent, unrealCommandCallback, unrealFeature, unrealReducer, updateCirrusInfo, videoAdaptedToContainer, videoStreamInitialized };
|
|
6910
|
+
export { AFKService, AfkTimeoutModalComponent, AggregatorService, AnalyticsService, AnswerHandler, CONSOLE_COMMAND_DISABLE_MESSAGES, CONSOLE_COMMAND_ENABLE_MESSAGES, CONSOLE_COMMAND_PIXEL_QUALITY, CommandTelemetryService, CommandsEffects, ConfigHandler, ConsoleExtensionsService, ControlScheme, DATA_CHANNEL_CONNECTION_TIMEOUT, DEBOUNCE_TO_MANY_RESIZE_CALLS, DEFAULT_AFK_TIMEOUT, DEFAULT_AFK_TIMEOUT_PERIOD, DEFAULT_RECONNECT_DELAY_MS, DEFAULT_RECONNECT_ENABLED, DEFAULT_RECONNECT_MAX_ATTEMPTS, DEFAULT_RECONNECT_ON_DATACHANNEL_CLOSE, DEFAULT_RECONNECT_ON_ICE_FAILURE, DataFlowMonitor, DevModeService, DisconnectReason, FULL_HD_HEIGHT, FULL_HD_WIDTH, FileHandlerService, FileReceiverService, FilterSettingsComponent, FpsMonitorService, FreezeFrameComponent, FreezeFrameService, IceCandidateHandler, ImageLoadingSrcComponent, InputOptions, InputService, InstanceReadyHandler, InstanceReservedHandler, IntroSrcComponent, KalmanFilter1D, LatencyTimings, LowBandwidthDetectorComponent, LowBandwidthModalComponent, MINIMAL_FPS, MessageType, MouseButton, MouseButtonsMask, OnCloseHandler, OnErrorHandler, OnMessageHandler, OnOpenHandler, OrchestrationMessageTypes, POLLING_TIME, PingHandler, PlayerCountHandler, RegionsPingService, SAME_SIZE_THRESHOLD, SCREEN_LOCKER_CONTAINER_ID, SIGNALLING_PERCENT_VALUE, SSInfoHandler, STREAMING_VIDEO_ID, SafePipe, SignallingService, SpecialKeyCodes, StatGraphComponent, StreamStatusTelemetryService, StreamStatusTelemetryService2, SubService, ToClientMessage, UNREAL_CONFIG, UnrealCallbackService, UnrealCommunicatorService, UnrealEffects, UnrealSceneComponent, UnrealStatusMessage, VideoService, VideoStatsComponent, WSCloseCode_CIRRUS_ABNORMAL_CLOSURE, WSCloseCode_CIRRUS_MAX_PLAYERS_ERROR, WSCloseCode_CIRRUS_PLAYER_DISCONNECTED, WSCloseCode_CIRRUS_STREAMER_KIKED_PLAYER, WSCloseCode_FORCE_CIRRUS_CLOSE, WSCloseCode_NORMAL_AFK_TIMEOUT, WSCloseCode_NORMAL_CLOSURE, WSCloseCode_NORMAL_MANUAL_DISCONNECT, WSCloseCode_UNKNOWN, WSCloseCodes, WS_OPEN_STATE, WS_TIMEOUT, WebRtcPlayerService, WebrtcErrorModalComponent, abortEstablishingConnection, answerSent, changeLowBandwidth, changeStatusMainVideoOnScene, changeStreamResolutionAction, changeStreamResolutionSuccessAction, commandCompleted, commandStarted, commandsFeature, commandsReducer, dataChannelConnected, dataChannelReady, decodeData, destroyRemoteConnections, destroyUnrealScene, disconnectStream, dispatchResize, dropConnection, dropDC, dropPC, floatToSmoothPercents, forceResizeViewport, fromResizeObserver, getActiveUrl, getImageFromVideoStream, getResizeStudioValues, getResizeStudioValuesOld, getResizeValuesUniversal, getResizeValuesUniversalOld, getRtcErrorMessage, iceConnectionFailed, initSignalling, instanceReady, keepMaxUntilReset, makeEven, pollingEnded, pollingStarted, provideUnrealModule, qpToQuality, reconnectPeer, reconnectPeerFailed, reconnectPeerSuccess, regionPingFetchEnd, regionPingFetchStart, regionResolved, resetAfk, resetConfig, resetDataChannelForReconnect, resetIntroSrc, resetWarnTimeout, saveAnalyticsEvent, selectCirrusConnectionId, selectClientAndViewIds, selectCommandProgress, selectCommandsInProgress, selectFreezeFrameCombinedDataUrl, selectFreezeFrameDataUrl, selectFreezeFrameDataUrlFromVideo, selectFreezeFrameProgressMessageFromVideo, selectIsAutostart, selectIsExistMatchUrls, selectIsFreezeFrameLoading, selectIsLoaderScreenVisible, selectIsVideoPlayingAndDataChannelConnected, selectShowLoader, selectShowReconnectPopup, selectSignalingParameters, selectStreamConfig, selectTotalProgress, selectWarnTimeout, setAfkTimerCountdown, setAfkTimerHide, setAfkTimerVisible, setAwsInstance, setCirrusConnected, setCirrusDisconnected, setConfig, setDataChannelConnected, setEstablishingConnection, setFreezeFrame, setFreezeFrameFromVideo, setIntroImageSrc, setIntroVideoSrc, setKalmanSettings, setLoadingImageSrc, setLoopBackCommandIsCompleted, setMaxFps, setOrchestrationContext, setOrchestrationMessage, setOrchestrationParameters, setOrchestrationProgress, setSignalingName, setStatusMessage, setStatusPercentSignallingServer, setStreamClientCompanyId, setStreamViewId, setUnrealPlaywrightConfig, setViewportNotReady, setViewportReady, showUnrealErrorMessage, smoothTransition, startStream, trackMixpanelEvent, trackTelemetryData, unrealCommandCallback, unrealFeature, unrealReducer, updateCirrusInfo, videoAdaptedToContainer, videoStreamInitialized };
|
|
6112
6911
|
//# sourceMappingURL=3dsource-angular-unreal-module.mjs.map
|