@3dsource/angular-unreal-module 0.0.146-dev.0 → 0.0.146-dev.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
2
|
import { InjectionToken, inject, DestroyRef, ChangeDetectionStrategy, Component, Injectable, signal, effect, untracked, computed, Pipe, input, ElementRef, viewChild, afterNextRender, HostListener, output, makeEnvironmentProviders, provideEnvironmentInitializer } from '@angular/core';
|
|
3
3
|
import { SourceButtonComponent, SourceLoadingComponent } from '@3dsource/source-ui-native';
|
|
4
|
-
import { Subject, skip, share, merge, map, scan, timestamp, startWith, pairwise, concatMap, of, animationFrameScheduler, interval, Observable, timer, take, defer, switchMap, from, throwError, EMPTY, retry, timeout as timeout$1, repeat, takeUntil as takeUntil$1, fromEvent, tap as tap$1, throttleTime, delay, concat, debounceTime, mergeMap, shareReplay, NEVER, asyncScheduler, filter as filter$1, withLatestFrom as withLatestFrom$1, Subscription, bufferTime, combineLatestWith, distinctUntilChanged as distinctUntilChanged$1 } from 'rxjs';
|
|
4
|
+
import { Subject, skip, share, merge, map, scan, timestamp, startWith, pairwise, concatMap, of, animationFrameScheduler, interval, Observable, timer, take, defer, switchMap, from, throwError, EMPTY, retry, timeout as timeout$1, repeat, BehaviorSubject, takeUntil as takeUntil$1, fromEvent, tap as tap$1, throttleTime, delay, concat, debounceTime, mergeMap, ignoreElements, shareReplay, NEVER, asyncScheduler, filter as filter$1, withLatestFrom as withLatestFrom$1, Subscription, bufferTime, combineLatestWith, distinctUntilChanged as distinctUntilChanged$1 } from 'rxjs';
|
|
5
5
|
import { createAction, props, createReducer, on, createFeature, Store, createSelector, provideState } from '@ngrx/store';
|
|
6
6
|
import { whereNot, Falsy, Logger, calculateMedian, clampf, lerp, getCanvasCached, getSnapshot, fitIntoRectangle, generateUuid, Truthy, tapLog, Semaphore, COLOR_CODES, where, KeyboardNumericCode, InvertedKeyMap, HEXtoRGB, RGBtoHSV, inverseLerp, HSVtoRGB, RGBtoHEX, fpIsASameAsB } from '@3dsource/utils';
|
|
7
7
|
import { filter, map as map$1, distinctUntilChanged, takeWhile, takeUntil, catchError, tap, timeout, finalize, first, switchMap as switchMap$1, withLatestFrom, exhaustMap, debounceTime as debounceTime$1, take as take$1, startWith as startWith$1 } from 'rxjs/operators';
|
|
@@ -444,6 +444,7 @@ const initialState = {
|
|
|
444
444
|
isAfkTimerVisible: false,
|
|
445
445
|
afkCountdown: 0,
|
|
446
446
|
cirrusConnected: false,
|
|
447
|
+
adopted: false,
|
|
447
448
|
establishingConnection: false,
|
|
448
449
|
isReconnecting: false,
|
|
449
450
|
viewportReady: false,
|
|
@@ -580,7 +581,11 @@ const unrealReducer = createReducer(initialState, on(changeStatusMainVideoOnScen
|
|
|
580
581
|
},
|
|
581
582
|
};
|
|
582
583
|
}), on(initSignalling, (state) => {
|
|
583
|
-
return {
|
|
584
|
+
return {
|
|
585
|
+
...state,
|
|
586
|
+
adopted: false,
|
|
587
|
+
disconnectReason: DisconnectReason.None,
|
|
588
|
+
};
|
|
584
589
|
}), on(reconnectPeerSuccess, (state) => {
|
|
585
590
|
return { ...state, isReconnecting: false };
|
|
586
591
|
}), on(setSignalingName, (state, { instanceName }) => {
|
|
@@ -643,10 +648,12 @@ const unrealReducer = createReducer(initialState, on(changeStatusMainVideoOnScen
|
|
|
643
648
|
return { ...state, analyticsEvent: event };
|
|
644
649
|
}), on(setCirrusConnected, (state) => {
|
|
645
650
|
return { ...state, cirrusConnected: true };
|
|
646
|
-
}), on(streamAdopted, (state, { machineId }) => {
|
|
651
|
+
}), on(streamAdopted, (state, { machineId, ssData }) => {
|
|
647
652
|
return {
|
|
648
653
|
...state,
|
|
649
654
|
cirrusConnected: true,
|
|
655
|
+
adopted: true,
|
|
656
|
+
ssData: ssData ?? state.ssData,
|
|
650
657
|
awsInstance: {
|
|
651
658
|
...state.awsInstance,
|
|
652
659
|
wsUrl: machineId,
|
|
@@ -676,6 +683,7 @@ const unrealReducer = createReducer(initialState, on(changeStatusMainVideoOnScen
|
|
|
676
683
|
return {
|
|
677
684
|
...state,
|
|
678
685
|
cirrusConnected: false,
|
|
686
|
+
adopted: false,
|
|
679
687
|
dataChannelConnected: false,
|
|
680
688
|
viewportReady: false,
|
|
681
689
|
isVideoPlaying: false,
|
|
@@ -1314,13 +1322,18 @@ const selectTotalProgress = createSelector(unrealFeature.selectStatusPercentSign
|
|
|
1314
1322
|
const SIGNALLING_MAX = SIGNALLING_PERCENT_VALUE / 100;
|
|
1315
1323
|
const ORCHESTRATION_WS_PATTERN = /^wss:\/\/orchestration-websocket/i;
|
|
1316
1324
|
const isNewOrchestration = ORCHESTRATION_WS_PATTERN.test(awsInstance?.wsUrl ?? '');
|
|
1317
|
-
//
|
|
1318
|
-
//
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1325
|
+
// Once the data channel is connected, signaling is fully complete (1.0) —
|
|
1326
|
+
// regardless of orchestration type. This is what makes an ADOPTED stream
|
|
1327
|
+
// (whose wsUrl is the machine host, not the orchestration-websocket URL,
|
|
1328
|
+
// and which never set `statusPercentSignallingServer`) correctly show the
|
|
1329
|
+
// whole first phase done instead of stalling near the command-only value.
|
|
1330
|
+
// Otherwise: new orchestration reports 0–1 progress; legacy uses the
|
|
1331
|
+
// polling percent.
|
|
1332
|
+
const signalingProgress = dataChannelConnected
|
|
1333
|
+
? 1
|
|
1334
|
+
: isNewOrchestration
|
|
1335
|
+
? (awsInstance?.progressComplete ?? 0)
|
|
1336
|
+
: (statusPercentSignallingServer ?? 0) / 100;
|
|
1324
1337
|
// +1% bump once WS URL is known (visual step for WS handshake)
|
|
1325
1338
|
const splitPoint = awsInstance?.wsUrl
|
|
1326
1339
|
? SIGNALLING_MAX + 0.01
|
|
@@ -1664,6 +1677,10 @@ class RegionsPingService {
|
|
|
1664
1677
|
* entry is well-formed and still within {@link REGION_PING_CACHE_TTL_MS}.
|
|
1665
1678
|
* All access is guarded — `localStorage` can throw (private mode, disabled
|
|
1666
1679
|
* storage) or be absent (non-browser).
|
|
1680
|
+
*
|
|
1681
|
+
* Public so adopted connections (which skip `getFastest`) can replay the
|
|
1682
|
+
* region-resolving telemetry phase from the same cached winner — see
|
|
1683
|
+
* `UnrealEffects.regionTelemetryOnAdopt$`.
|
|
1667
1684
|
*/
|
|
1668
1685
|
readCachedWinner() {
|
|
1669
1686
|
try {
|
|
@@ -2573,7 +2590,13 @@ class VideoService extends SubService {
|
|
|
2573
2590
|
this.audio = null;
|
|
2574
2591
|
this.container = null;
|
|
2575
2592
|
this.latencyTestTimings = new LatencyTimings();
|
|
2576
|
-
|
|
2593
|
+
// BehaviorSubject (not Subject) so a late subscriber to `videoStats$` still
|
|
2594
|
+
// gets the current track. Adoption (`WebRtcPlayerService.adopt`) calls
|
|
2595
|
+
// `attachVideoStream` synchronously/early — before the resize and app-init
|
|
2596
|
+
// (`initAppByDataChannelReady$`) subscribers attach — and a plain Subject
|
|
2597
|
+
// would drop that emission, so the stats interval (and `setViewportReady`,
|
|
2598
|
+
// gated on `framesPerSecond`) would never start.
|
|
2599
|
+
this.videoTrack$ = new BehaviorSubject(null);
|
|
2577
2600
|
this.VideoEncoderQP = 0;
|
|
2578
2601
|
this.aggregatedStats = {};
|
|
2579
2602
|
this.kalmanFilter1D = new KalmanFilter1D(FilterModel.initialBitrateEstimate, FilterModel.initialErrorCovariance, FilterModel.processNoise, FilterModel.measurementNoise);
|
|
@@ -3579,6 +3602,70 @@ class StreamAdoptionService {
|
|
|
3579
3602
|
this.signalling = inject(SignallingService);
|
|
3580
3603
|
this.webRtc = inject(WebRtcPlayerService);
|
|
3581
3604
|
}
|
|
3605
|
+
/**
|
|
3606
|
+
* Decide adoption, WAITING for an in-flight prefetch instead of racing it.
|
|
3607
|
+
*
|
|
3608
|
+
* - nothing parked → emit `false` at once (caller connects normally).
|
|
3609
|
+
* - already settled (`ready`/`failed`/`adopted`) → adopt-if-ready at once.
|
|
3610
|
+
* - still `connecting` → wait for the script's `onSettled` callback and
|
|
3611
|
+
* adopt-if-ready the instant it fires (event-driven, zero polling latency).
|
|
3612
|
+
*
|
|
3613
|
+
* Deliberately NO time budget: for DIRECT orchestration the prefetch holds
|
|
3614
|
+
* the socket open while the orchestrator provisions a machine, which can take
|
|
3615
|
+
* minutes (150–200s+). A fixed deadline would make Angular give up and open a
|
|
3616
|
+
* second connection mid-provisioning — exactly the race/orphan this avoids.
|
|
3617
|
+
* The wait is bounded by liveness instead: the script flips the parked state
|
|
3618
|
+
* to `failed` on a dead socket (`ws.onclose`/`onerror`), and the caller wraps
|
|
3619
|
+
* this in `takeUntil(abortEstablishingConnection)`, so a route change / teardown
|
|
3620
|
+
* cancels it. Emits exactly once. Cancelling runs no adoption — which is what
|
|
3621
|
+
* stops a fast route-switch from starting overlapping connections.
|
|
3622
|
+
*
|
|
3623
|
+
* Wrapped in `defer` so the `tryAdopt()` side effect (which dispatches
|
|
3624
|
+
* `streamAdopted`) runs at SUBSCRIPTION time, not when the observable is
|
|
3625
|
+
* built. The effect prepends `startWith(setEstablishingConnection(true))`,
|
|
3626
|
+
* which starts the telemetry session and subscribes its phase catalog; that
|
|
3627
|
+
* must happen BEFORE `streamAdopted` → `regionTelemetryOnAdopt$` replays
|
|
3628
|
+
* `regionResolved`, or the region phase fires into a session that doesn't
|
|
3629
|
+
* exist yet and is lost. `defer` guarantees that ordering.
|
|
3630
|
+
*/
|
|
3631
|
+
awaitAndAdopt$() {
|
|
3632
|
+
return defer(() => {
|
|
3633
|
+
const parked = window.__stream;
|
|
3634
|
+
if (!parked) {
|
|
3635
|
+
return of(false);
|
|
3636
|
+
}
|
|
3637
|
+
if (parked.state !== 'connecting') {
|
|
3638
|
+
return of(this.tryAdopt());
|
|
3639
|
+
}
|
|
3640
|
+
Logger.info('[Adoption] Prefetch in progress — waiting before connecting');
|
|
3641
|
+
return this.awaitSettled$(parked);
|
|
3642
|
+
});
|
|
3643
|
+
}
|
|
3644
|
+
/**
|
|
3645
|
+
* Event-driven wait: the script fires `onSettled` the instant it parks
|
|
3646
|
+
* `ready`/`failed`, so we adopt with zero polling lag. Emits exactly once.
|
|
3647
|
+
*
|
|
3648
|
+
* No timer/budget by design: an indefinitely `connecting` stream is valid
|
|
3649
|
+
* (DIRECT orchestration holds the socket open for minutes while it
|
|
3650
|
+
* provisions), and every terminal state — including a dead socket — flips to
|
|
3651
|
+
* `ready`/`failed` and fires the hook. Teardown (route change → unsubscribe)
|
|
3652
|
+
* sets `done`, so a late callback after cancel can't trigger a stray adoption.
|
|
3653
|
+
*/
|
|
3654
|
+
awaitSettled$(parked) {
|
|
3655
|
+
return new Observable((subscriber) => {
|
|
3656
|
+
let done = false;
|
|
3657
|
+
parked.onSettled(() => {
|
|
3658
|
+
if (done)
|
|
3659
|
+
return;
|
|
3660
|
+
done = true;
|
|
3661
|
+
subscriber.next(this.tryAdopt());
|
|
3662
|
+
subscriber.complete();
|
|
3663
|
+
});
|
|
3664
|
+
return () => {
|
|
3665
|
+
done = true;
|
|
3666
|
+
};
|
|
3667
|
+
});
|
|
3668
|
+
}
|
|
3582
3669
|
/**
|
|
3583
3670
|
* @returns `true` if a ready parked stream was adopted (caller must NOT
|
|
3584
3671
|
* start a fresh connection), `false` otherwise.
|
|
@@ -3598,14 +3685,19 @@ class StreamAdoptionService {
|
|
|
3598
3685
|
return false;
|
|
3599
3686
|
}
|
|
3600
3687
|
Logger.info('[Adoption] Adopting prefetched stream', parked.timings);
|
|
3601
|
-
//
|
|
3688
|
+
// Clear the script's timers and mark the handoff complete.
|
|
3602
3689
|
parked.adopt();
|
|
3603
3690
|
// Flag cirrus connected (so a later `initSignalling` won't open a 2nd
|
|
3604
|
-
// connection)
|
|
3605
|
-
//
|
|
3606
|
-
//
|
|
3607
|
-
//
|
|
3608
|
-
|
|
3691
|
+
// connection), record the adopted machine, and hydrate the streamer info
|
|
3692
|
+
// (`ssData`) the prefetch captured from the `ssInfo` message. NOTE:
|
|
3693
|
+
// clientId/viewId are left untouched — the host app (metabox) already set
|
|
3694
|
+
// them authoritatively from its own data before `initSignalling`; the
|
|
3695
|
+
// script's ids are only used internally to establish the connection.
|
|
3696
|
+
this.store.dispatch(streamAdopted({
|
|
3697
|
+
machineId: parked.machineId ?? null,
|
|
3698
|
+
ssData: parked.ssData ?? null,
|
|
3699
|
+
timings: parked.timings ?? null,
|
|
3700
|
+
}));
|
|
3609
3701
|
// Hand the live socket over without re-sending `requestStream`, reusing the
|
|
3610
3702
|
// script's correlationId (the one `requestStream` was already sent with).
|
|
3611
3703
|
this.signalling.adoptSocket(parked.ws, parked.ids.correlationId);
|
|
@@ -4148,12 +4240,19 @@ class UnrealEffects {
|
|
|
4148
4240
|
}), map(([, matchMakerUrls]) => [...matchMakerUrls].filter(Truthy)), filter((urls) => urls.length > 0),
|
|
4149
4241
|
// Ensure a single in-flight request per attempt.
|
|
4150
4242
|
exhaustMap((urls) =>
|
|
4151
|
-
//
|
|
4152
|
-
//
|
|
4153
|
-
|
|
4154
|
-
|
|
4155
|
-
|
|
4156
|
-
|
|
4243
|
+
// If a prefetched `window.__stream` is ready — or still connecting —
|
|
4244
|
+
// adopt it (waiting for it to finish) instead of racing a second
|
|
4245
|
+
// connection; otherwise fall back to a fresh signalling connection.
|
|
4246
|
+
// `takeUntil(abort)` cancels the wait on teardown / route change so a
|
|
4247
|
+
// fast switch can't leave overlapping connections.
|
|
4248
|
+
this.streamAdoptionService.awaitAndAdopt$().pipe(switchMap((adopted) => adopted ? EMPTY : this.signallingService.connectToSignaling(urls)), takeUntil(this.actions$.pipe(ofType(abortEstablishingConnection))),
|
|
4249
|
+
// The adopt/connect stream is side-effect-only (its values aren't
|
|
4250
|
+
// actions) — swallow them, and emit `setEstablishingConnection`
|
|
4251
|
+
// first so the reconnect/resume popup stays hidden during the
|
|
4252
|
+
// (possibly deferred) prefetch-adoption wait. Adoption otherwise
|
|
4253
|
+
// defers `connectToSignaling`, which is what normally sets this.
|
|
4254
|
+
ignoreElements(), startWith(setEstablishingConnection({ value: true })))));
|
|
4255
|
+
});
|
|
4157
4256
|
}
|
|
4158
4257
|
canPeerReconnect() {
|
|
4159
4258
|
return RECONNECT_CONFIG.enabled && this.signallingService.isConnected();
|
|
@@ -5455,6 +5554,19 @@ const QP_RED_THRESHOLD = 35;
|
|
|
5455
5554
|
const FPS_LOW_RATIO = 0.7; // p50 fps below 70% of target
|
|
5456
5555
|
const UNLIMITED_REFERENCE_FPS = 60;
|
|
5457
5556
|
const SAMPLE_WINDOW_MS = 5000;
|
|
5557
|
+
/**
|
|
5558
|
+
* Catalog phase ids the prefetch establishes invisibly. For an adopted
|
|
5559
|
+
* connection their live closers are suppressed and the phases are emitted by
|
|
5560
|
+
* {@link StreamStatusTelemetryService2.emitAdoptedConnection} instead, timed
|
|
5561
|
+
* from the prefetch's own marks and anchored at the moment Angular reached the
|
|
5562
|
+
* connect call (session start) — so the waterfall shows the time the prefetch
|
|
5563
|
+
* actually SAVED, not the prefetch's page-load-parallel raw timeline.
|
|
5564
|
+
*/
|
|
5565
|
+
const PREFETCHED_PHASE_IDS = new Set([
|
|
5566
|
+
'region-resolving',
|
|
5567
|
+
'signaling-connection',
|
|
5568
|
+
'webrtc-negotiation',
|
|
5569
|
+
]);
|
|
5458
5570
|
// Persistent per-browser identifier — survives page reloads. Used to group
|
|
5459
5571
|
// multiple sessions of the same anonymous user for funnel / retention analysis.
|
|
5460
5572
|
const USER_ID_STORAGE_KEY = 'source3d.streamTelemetry.userId';
|
|
@@ -5485,6 +5597,17 @@ class StreamStatusTelemetryService2 {
|
|
|
5485
5597
|
this.disconnectReason$ = this.store.selectSignal(unrealFeature.selectDisconnectReason);
|
|
5486
5598
|
this.errorMessage$ = this.store.selectSignal(unrealFeature.selectErrorMessage);
|
|
5487
5599
|
this.viewPortReady$ = this.store.selectSignal(unrealFeature.selectViewportReady);
|
|
5600
|
+
/**
|
|
5601
|
+
* Synchronous adoption flag. The `streamAdopted` reducer sets it BEFORE any
|
|
5602
|
+
* action subscriber runs, so the connection-phase closers (which fire inside
|
|
5603
|
+
* the same dispatch, e.g. `ssinfo-received` off the `ssData` state change)
|
|
5604
|
+
* can read it and defer to the prefetch-timed attribution emitted by
|
|
5605
|
+
* {@link emitAdoptedConnection} instead of recording the live wall-clock.
|
|
5606
|
+
*/
|
|
5607
|
+
this.adopted$ = this.store.selectSignal(unrealFeature.selectAdopted);
|
|
5608
|
+
/** Streamer identity for the adopted signaling phase params. */
|
|
5609
|
+
this.ssData$ = this.store.selectSignal(unrealFeature.selectSsData);
|
|
5610
|
+
this.awsInstance$ = this.store.selectSignal(unrealFeature.selectAwsInstance);
|
|
5488
5611
|
this.session = null;
|
|
5489
5612
|
/**
|
|
5490
5613
|
* Whether a session has already started during the current page lifetime.
|
|
@@ -5562,7 +5685,10 @@ class StreamStatusTelemetryService2 {
|
|
|
5562
5685
|
// CLOSER — the `regionResolved` action carries the raw ping
|
|
5563
5686
|
// samples (one per probe) from `RegionsPingService`; the
|
|
5564
5687
|
// catalog flattens them to a comma-separated string since
|
|
5565
|
-
// `toPhaseParams` only accepts primitives.
|
|
5688
|
+
// `toPhaseParams` only accepts primitives. Adopted connections never
|
|
5689
|
+
// run `getRegion`, but `UnrealEffects.regionTelemetryOnAdopt$` replays
|
|
5690
|
+
// the region waterfall (start → end → resolved) from the cached winner,
|
|
5691
|
+
// so this phase still appears with its ping samples.
|
|
5566
5692
|
{
|
|
5567
5693
|
id: 'region-resolved',
|
|
5568
5694
|
event: this.actions$.pipe(ofType(regionResolved), take(1), map(({ region, pingsMs }) => ({
|
|
@@ -5874,6 +6000,13 @@ class StreamStatusTelemetryService2 {
|
|
|
5874
6000
|
if (!this.session)
|
|
5875
6001
|
return;
|
|
5876
6002
|
if (isCloser) {
|
|
6003
|
+
// Adopted connections receive the region / signaling / webrtc
|
|
6004
|
+
// phases from `emitAdoptedConnection` (timed from the prefetch's
|
|
6005
|
+
// own marks). Skip the live closer for those, so the whole
|
|
6006
|
+
// pre-adoption wait isn't dumped onto whichever connection closer
|
|
6007
|
+
// happens to fire first at the adoption instant.
|
|
6008
|
+
if (this.adopted$() && PREFETCHED_PHASE_IDS.has(bucket.id))
|
|
6009
|
+
return;
|
|
5877
6010
|
this.emitPhaseEnded(bucket, bucketIdx, value);
|
|
5878
6011
|
return;
|
|
5879
6012
|
}
|
|
@@ -5884,6 +6017,12 @@ class StreamStatusTelemetryService2 {
|
|
|
5884
6017
|
}));
|
|
5885
6018
|
});
|
|
5886
6019
|
});
|
|
6020
|
+
// Adopted connection: emit the region / signaling / webrtc phases timed
|
|
6021
|
+
// from the prefetch marks, anchored at session start (see method doc). The
|
|
6022
|
+
// live closers for these phases are suppressed (PREFETCHED_PHASE_IDS).
|
|
6023
|
+
sub.add(this.actions$
|
|
6024
|
+
.pipe(ofType(streamAdopted))
|
|
6025
|
+
.subscribe(({ timings }) => this.emitAdoptedConnection(timings)));
|
|
5887
6026
|
const bootstrapInfo = this.collectBootstrapInfo();
|
|
5888
6027
|
this.emitLifecycle('stream.session_started', {
|
|
5889
6028
|
sessionId: id,
|
|
@@ -6001,6 +6140,127 @@ class StreamStatusTelemetryService2 {
|
|
|
6001
6140
|
overlays,
|
|
6002
6141
|
});
|
|
6003
6142
|
}
|
|
6143
|
+
/**
|
|
6144
|
+
* Emit the region / signaling / webrtc phases for an ADOPTED connection.
|
|
6145
|
+
*
|
|
6146
|
+
* The prefetch script established the connection in PARALLEL with page load,
|
|
6147
|
+
* so the phases are positioned at their real (anchor-relative) spans — shifted
|
|
6148
|
+
* LEFT to overlap the HTML-loading / route-idle rows — rather than sequentially
|
|
6149
|
+
* after them. That's the whole point of the prefetch: the handshake already
|
|
6150
|
+
* happened while the page was loading. Each phase ships:
|
|
6151
|
+
* - `duration` — the real sub-step time the prefetch spent.
|
|
6152
|
+
* - `spanStartMs` — anchor-relative start, so the viewer left-shifts the bar.
|
|
6153
|
+
* - `adoptionAtMs` — where Angular took over (the next phase, Unreal
|
|
6154
|
+
* communication, resumes here on the sequential timeline).
|
|
6155
|
+
* - savings params: `prefetchConnectMs` (full handshake), `prefetchSavedMs`
|
|
6156
|
+
* (done before Angular was ready), `adoptionWaitMs` (real connect→adopted
|
|
6157
|
+
* wait), `adoptionOverheadMs` (prefetch-ready → adopted gap).
|
|
6158
|
+
*/
|
|
6159
|
+
emitAdoptedConnection(timings) {
|
|
6160
|
+
const s = this.session;
|
|
6161
|
+
if (!s)
|
|
6162
|
+
return;
|
|
6163
|
+
const t = timings ?? {};
|
|
6164
|
+
const now = performance.now();
|
|
6165
|
+
// Everything in anchor-relative ms — the same scale the viewer renders on
|
|
6166
|
+
// (and the prefetch marks share the page `performance` clock).
|
|
6167
|
+
const toMs = (perf) => Math.round(perf - s.anchorPerf);
|
|
6168
|
+
const startMs = toMs(t['prefetch_start'] ?? s.startedAtPerf);
|
|
6169
|
+
const wsOpenMs = Math.max(startMs, toMs(t['ws_open'] ?? t['prefetch_start'] ?? s.startedAtPerf));
|
|
6170
|
+
const readyMs = Math.max(wsOpenMs, toMs(t['ready'] ?? t['dc_open'] ?? t['ws_open'] ?? s.startedAtPerf));
|
|
6171
|
+
const adoptionAtMs = toMs(now);
|
|
6172
|
+
const sessionStartMs = toMs(s.startedAtPerf);
|
|
6173
|
+
const sigDur = wsOpenMs - startMs;
|
|
6174
|
+
const webDur = readyMs - wsOpenMs;
|
|
6175
|
+
const connectMs = readyMs - startMs;
|
|
6176
|
+
// Savings, anchored at the moment Angular reached the connect call.
|
|
6177
|
+
const savings = {
|
|
6178
|
+
prefetched: true,
|
|
6179
|
+
prefetchConnectMs: connectMs,
|
|
6180
|
+
// How much of the handshake finished before Angular was ready (the win).
|
|
6181
|
+
prefetchSavedMs: Math.max(0, Math.min(connectMs, sessionStartMs - startMs)),
|
|
6182
|
+
// The real wait Angular experienced (connect call → adopted).
|
|
6183
|
+
adoptionWaitMs: Math.max(0, adoptionAtMs - sessionStartMs),
|
|
6184
|
+
// Gap between the prefetch being ready and Angular adopting it.
|
|
6185
|
+
adoptionOverheadMs: Math.max(0, adoptionAtMs - readyMs),
|
|
6186
|
+
};
|
|
6187
|
+
// Region (phaseNo 1) — served from the ping cache during prefetch (~0),
|
|
6188
|
+
// positioned at the prefetch start; ship the samples.
|
|
6189
|
+
const region = this.readRegionCache();
|
|
6190
|
+
this.emitAdoptedPhase(0, startMs, 0, adoptionAtMs, {
|
|
6191
|
+
region: region?.region_code ?? '',
|
|
6192
|
+
pingsMs: region?.result.length ? `${region.result.join(', ')} ms` : null,
|
|
6193
|
+
});
|
|
6194
|
+
// Signaling (phaseNo 2) — [prefetch_start → ws_open]; streamer identity +
|
|
6195
|
+
// savings; ws-connected marker sits at the phase's own end.
|
|
6196
|
+
this.emitAdoptedPhase(1, startMs, sigDur, adoptionAtMs, {
|
|
6197
|
+
...this.adoptedSignalingParams(),
|
|
6198
|
+
...savings,
|
|
6199
|
+
prefetchSignalingMs: sigDur,
|
|
6200
|
+
}, [{ id: 'signaling-ws-connected', time: wsOpenMs }]);
|
|
6201
|
+
// WebRTC negotiation (phaseNo 3) — [ws_open → ready].
|
|
6202
|
+
this.emitAdoptedPhase(2, wsOpenMs, webDur, adoptionAtMs, {
|
|
6203
|
+
iceCandidateCount: 0,
|
|
6204
|
+
prefetchWebrtcMs: webDur,
|
|
6205
|
+
});
|
|
6206
|
+
// Unreal communication chains from the adoption moment (real wall-clock).
|
|
6207
|
+
s.lastMilestonePerf = now;
|
|
6208
|
+
}
|
|
6209
|
+
/**
|
|
6210
|
+
* Emit one adopted phase positioned at its real `spanStartMs` (left-shifted
|
|
6211
|
+
* to overlap page load) with its real prefetch `duration`. `prefetched`,
|
|
6212
|
+
* `spanStartMs` and `adoptionAtMs` ride as top-level wire fields the viewer
|
|
6213
|
+
* uses to lay the bar out; the rest become display params.
|
|
6214
|
+
*/
|
|
6215
|
+
emitAdoptedPhase(bucketIdx, spanStartMs, durationMs, adoptionAtMs, extras, milestones = []) {
|
|
6216
|
+
const s = this.session;
|
|
6217
|
+
if (!s)
|
|
6218
|
+
return;
|
|
6219
|
+
const bucket = this.phaseCatalog[bucketIdx];
|
|
6220
|
+
this.track('stream.phase_ended', {
|
|
6221
|
+
sessionId: s.id,
|
|
6222
|
+
phaseNo: bucketIdx + 1,
|
|
6223
|
+
id: bucket.id,
|
|
6224
|
+
color: bucket.color,
|
|
6225
|
+
duration: durationMs,
|
|
6226
|
+
prefetched: true,
|
|
6227
|
+
spanStartMs,
|
|
6228
|
+
adoptionAtMs,
|
|
6229
|
+
milestones,
|
|
6230
|
+
params: toPhaseParams({ ...extras, prefetched: true }),
|
|
6231
|
+
overlays: [],
|
|
6232
|
+
});
|
|
6233
|
+
}
|
|
6234
|
+
/** Streamer identity params for the adopted signaling phase — mirrors the
|
|
6235
|
+
* `ssinfo-received` closer's shape (client / instanceName / …ssData). */
|
|
6236
|
+
adoptedSignalingParams() {
|
|
6237
|
+
const aws = this.awsInstance$();
|
|
6238
|
+
const ssData = this.ssData$();
|
|
6239
|
+
return {
|
|
6240
|
+
client: aws?.wsUrl ?? null,
|
|
6241
|
+
instanceName: aws?.instanceName ?? null,
|
|
6242
|
+
...(ssData ?? {}),
|
|
6243
|
+
};
|
|
6244
|
+
}
|
|
6245
|
+
/** Read the region-ping cache the prefetch reused (localStorage), so the
|
|
6246
|
+
* adopted region phase can still report the winning region + samples. */
|
|
6247
|
+
readRegionCache() {
|
|
6248
|
+
try {
|
|
6249
|
+
const raw = localStorage.getItem(REGION_PING_CACHE_KEY);
|
|
6250
|
+
if (!raw)
|
|
6251
|
+
return null;
|
|
6252
|
+
const parsed = JSON.parse(raw);
|
|
6253
|
+
return {
|
|
6254
|
+
region_code: typeof parsed.region_code === 'string' ? parsed.region_code : '',
|
|
6255
|
+
result: Array.isArray(parsed.result)
|
|
6256
|
+
? parsed.result.filter((n) => typeof n === 'number')
|
|
6257
|
+
: [],
|
|
6258
|
+
};
|
|
6259
|
+
}
|
|
6260
|
+
catch {
|
|
6261
|
+
return null;
|
|
6262
|
+
}
|
|
6263
|
+
}
|
|
6004
6264
|
handleWindow(samples) {
|
|
6005
6265
|
if (!this.session)
|
|
6006
6266
|
return;
|