@3dsource/angular-unreal-module 0.0.146-dev.5 → 0.0.146-dev.7
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.
|
@@ -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 }) => {
|
|
@@ -647,6 +652,7 @@ const unrealReducer = createReducer(initialState, on(changeStatusMainVideoOnScen
|
|
|
647
652
|
return {
|
|
648
653
|
...state,
|
|
649
654
|
cirrusConnected: true,
|
|
655
|
+
adopted: true,
|
|
650
656
|
ssData: ssData ?? state.ssData,
|
|
651
657
|
awsInstance: {
|
|
652
658
|
...state.awsInstance,
|
|
@@ -677,6 +683,7 @@ const unrealReducer = createReducer(initialState, on(changeStatusMainVideoOnScen
|
|
|
677
683
|
return {
|
|
678
684
|
...state,
|
|
679
685
|
cirrusConnected: false,
|
|
686
|
+
adopted: false,
|
|
680
687
|
dataChannelConnected: false,
|
|
681
688
|
viewportReady: false,
|
|
682
689
|
isVideoPlaying: false,
|
|
@@ -3613,20 +3620,30 @@ class StreamAdoptionService {
|
|
|
3613
3620
|
* this in `takeUntil(abortEstablishingConnection)`, so a route change / teardown
|
|
3614
3621
|
* cancels it. Emits exactly once. Cancelling runs no adoption — which is what
|
|
3615
3622
|
* stops a fast route-switch from starting overlapping connections.
|
|
3623
|
+
*
|
|
3624
|
+
* Wrapped in `defer` so the `tryAdopt()` side effect (which dispatches
|
|
3625
|
+
* `streamAdopted`) runs at SUBSCRIPTION time, not when the observable is
|
|
3626
|
+
* built. The effect prepends `startWith(setEstablishingConnection(true))`,
|
|
3627
|
+
* which starts the telemetry session and subscribes its phase catalog; that
|
|
3628
|
+
* must happen BEFORE `streamAdopted` → `regionTelemetryOnAdopt$` replays
|
|
3629
|
+
* `regionResolved`, or the region phase fires into a session that doesn't
|
|
3630
|
+
* exist yet and is lost. `defer` guarantees that ordering.
|
|
3616
3631
|
*/
|
|
3617
3632
|
awaitAndAdopt$() {
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
|
|
3627
|
-
|
|
3628
|
-
|
|
3629
|
-
|
|
3633
|
+
return defer(() => {
|
|
3634
|
+
const parked = window.__stream;
|
|
3635
|
+
if (!parked) {
|
|
3636
|
+
return of(false);
|
|
3637
|
+
}
|
|
3638
|
+
if (parked.state !== 'connecting') {
|
|
3639
|
+
return of(this.tryAdopt());
|
|
3640
|
+
}
|
|
3641
|
+
Logger.info('[Adoption] Prefetch in progress — waiting before connecting');
|
|
3642
|
+
return timer(0, ADOPT_POLL_MS).pipe(map$1(() => window.__stream?.state ?? 'failed'),
|
|
3643
|
+
// Keep polling while still connecting; the `true` (inclusive) emits the
|
|
3644
|
+
// settling value (`ready`/`failed`) that ends the wait.
|
|
3645
|
+
takeWhile((state) => state === 'connecting', true), last(), map$1(() => this.tryAdopt()));
|
|
3646
|
+
});
|
|
3630
3647
|
}
|
|
3631
3648
|
/**
|
|
3632
3649
|
* @returns `true` if a ready parked stream was adopted (caller must NOT
|
|
@@ -3647,7 +3664,7 @@ class StreamAdoptionService {
|
|
|
3647
3664
|
return false;
|
|
3648
3665
|
}
|
|
3649
3666
|
Logger.info('[Adoption] Adopting prefetched stream', parked.timings);
|
|
3650
|
-
//
|
|
3667
|
+
// Clear the script's timers and mark the handoff complete.
|
|
3651
3668
|
parked.adopt();
|
|
3652
3669
|
// Flag cirrus connected (so a later `initSignalling` won't open a 2nd
|
|
3653
3670
|
// connection), record the adopted machine, and hydrate the streamer info
|
|
@@ -3658,6 +3675,7 @@ class StreamAdoptionService {
|
|
|
3658
3675
|
this.store.dispatch(streamAdopted({
|
|
3659
3676
|
machineId: parked.machineId ?? null,
|
|
3660
3677
|
ssData: parked.ssData ?? null,
|
|
3678
|
+
timings: parked.timings ?? null,
|
|
3661
3679
|
}));
|
|
3662
3680
|
// Hand the live socket over without re-sending `requestStream`, reusing the
|
|
3663
3681
|
// script's correlationId (the one `requestStream` was already sent with).
|
|
@@ -4214,24 +4232,6 @@ class UnrealEffects {
|
|
|
4214
4232
|
// defers `connectToSignaling`, which is what normally sets this.
|
|
4215
4233
|
ignoreElements(), startWith(setEstablishingConnection({ value: true })))));
|
|
4216
4234
|
});
|
|
4217
|
-
// Adoption skips `connectToSignaling → getRegion`, so the region-resolving
|
|
4218
|
-
// telemetry phase never fires. Replay the region waterfall (start → end →
|
|
4219
|
-
// resolved) from the cached winner the prefetch reused — a near-zero-length
|
|
4220
|
-
// phase that still carries the ping samples. No live ping sweep; if there is
|
|
4221
|
-
// no cache the resolved event just carries empty values.
|
|
4222
|
-
this.regionTelemetryOnAdopt$ = createEffect(() => {
|
|
4223
|
-
return this.actions$.pipe(ofType(streamAdopted), mergeMap(() => {
|
|
4224
|
-
const cached = this.regionsPingService.readCachedWinner();
|
|
4225
|
-
return [
|
|
4226
|
-
regionPingFetchStart(),
|
|
4227
|
-
regionPingFetchEnd(),
|
|
4228
|
-
regionResolved({
|
|
4229
|
-
region: cached?.region_code ?? '',
|
|
4230
|
-
pingsMs: cached?.result ?? [],
|
|
4231
|
-
}),
|
|
4232
|
-
];
|
|
4233
|
-
}));
|
|
4234
|
-
});
|
|
4235
4235
|
}
|
|
4236
4236
|
canPeerReconnect() {
|
|
4237
4237
|
return RECONNECT_CONFIG.enabled && this.signallingService.isConnected();
|
|
@@ -5533,6 +5533,19 @@ const QP_RED_THRESHOLD = 35;
|
|
|
5533
5533
|
const FPS_LOW_RATIO = 0.7; // p50 fps below 70% of target
|
|
5534
5534
|
const UNLIMITED_REFERENCE_FPS = 60;
|
|
5535
5535
|
const SAMPLE_WINDOW_MS = 5000;
|
|
5536
|
+
/**
|
|
5537
|
+
* Catalog phase ids the prefetch establishes invisibly. For an adopted
|
|
5538
|
+
* connection their live closers are suppressed and the phases are emitted by
|
|
5539
|
+
* {@link StreamStatusTelemetryService2.emitAdoptedConnection} instead, timed
|
|
5540
|
+
* from the prefetch's own marks and anchored at the moment Angular reached the
|
|
5541
|
+
* connect call (session start) — so the waterfall shows the time the prefetch
|
|
5542
|
+
* actually SAVED, not the prefetch's page-load-parallel raw timeline.
|
|
5543
|
+
*/
|
|
5544
|
+
const PREFETCHED_PHASE_IDS = new Set([
|
|
5545
|
+
'region-resolving',
|
|
5546
|
+
'signaling-connection',
|
|
5547
|
+
'webrtc-negotiation',
|
|
5548
|
+
]);
|
|
5536
5549
|
// Persistent per-browser identifier — survives page reloads. Used to group
|
|
5537
5550
|
// multiple sessions of the same anonymous user for funnel / retention analysis.
|
|
5538
5551
|
const USER_ID_STORAGE_KEY = 'source3d.streamTelemetry.userId';
|
|
@@ -5563,6 +5576,17 @@ class StreamStatusTelemetryService2 {
|
|
|
5563
5576
|
this.disconnectReason$ = this.store.selectSignal(unrealFeature.selectDisconnectReason);
|
|
5564
5577
|
this.errorMessage$ = this.store.selectSignal(unrealFeature.selectErrorMessage);
|
|
5565
5578
|
this.viewPortReady$ = this.store.selectSignal(unrealFeature.selectViewportReady);
|
|
5579
|
+
/**
|
|
5580
|
+
* Synchronous adoption flag. The `streamAdopted` reducer sets it BEFORE any
|
|
5581
|
+
* action subscriber runs, so the connection-phase closers (which fire inside
|
|
5582
|
+
* the same dispatch, e.g. `ssinfo-received` off the `ssData` state change)
|
|
5583
|
+
* can read it and defer to the prefetch-timed attribution emitted by
|
|
5584
|
+
* {@link emitAdoptedConnection} instead of recording the live wall-clock.
|
|
5585
|
+
*/
|
|
5586
|
+
this.adopted$ = this.store.selectSignal(unrealFeature.selectAdopted);
|
|
5587
|
+
/** Streamer identity for the adopted signaling phase params. */
|
|
5588
|
+
this.ssData$ = this.store.selectSignal(unrealFeature.selectSsData);
|
|
5589
|
+
this.awsInstance$ = this.store.selectSignal(unrealFeature.selectAwsInstance);
|
|
5566
5590
|
this.session = null;
|
|
5567
5591
|
/**
|
|
5568
5592
|
* Whether a session has already started during the current page lifetime.
|
|
@@ -5955,6 +5979,13 @@ class StreamStatusTelemetryService2 {
|
|
|
5955
5979
|
if (!this.session)
|
|
5956
5980
|
return;
|
|
5957
5981
|
if (isCloser) {
|
|
5982
|
+
// Adopted connections receive the region / signaling / webrtc
|
|
5983
|
+
// phases from `emitAdoptedConnection` (timed from the prefetch's
|
|
5984
|
+
// own marks). Skip the live closer for those, so the whole
|
|
5985
|
+
// pre-adoption wait isn't dumped onto whichever connection closer
|
|
5986
|
+
// happens to fire first at the adoption instant.
|
|
5987
|
+
if (this.adopted$() && PREFETCHED_PHASE_IDS.has(bucket.id))
|
|
5988
|
+
return;
|
|
5958
5989
|
this.emitPhaseEnded(bucket, bucketIdx, value);
|
|
5959
5990
|
return;
|
|
5960
5991
|
}
|
|
@@ -5965,6 +5996,12 @@ class StreamStatusTelemetryService2 {
|
|
|
5965
5996
|
}));
|
|
5966
5997
|
});
|
|
5967
5998
|
});
|
|
5999
|
+
// Adopted connection: emit the region / signaling / webrtc phases timed
|
|
6000
|
+
// from the prefetch marks, anchored at session start (see method doc). The
|
|
6001
|
+
// live closers for these phases are suppressed (PREFETCHED_PHASE_IDS).
|
|
6002
|
+
sub.add(this.actions$
|
|
6003
|
+
.pipe(ofType(streamAdopted))
|
|
6004
|
+
.subscribe(({ timings }) => this.emitAdoptedConnection(timings)));
|
|
5968
6005
|
const bootstrapInfo = this.collectBootstrapInfo();
|
|
5969
6006
|
this.emitLifecycle('stream.session_started', {
|
|
5970
6007
|
sessionId: id,
|
|
@@ -6082,6 +6119,130 @@ class StreamStatusTelemetryService2 {
|
|
|
6082
6119
|
overlays,
|
|
6083
6120
|
});
|
|
6084
6121
|
}
|
|
6122
|
+
/**
|
|
6123
|
+
* Emit the region / signaling / webrtc phases for an ADOPTED connection.
|
|
6124
|
+
*
|
|
6125
|
+
* The prefetch script established the connection in parallel with page load,
|
|
6126
|
+
* so its raw timeline (prefetch_start … ready) mostly predates this session —
|
|
6127
|
+
* showing it verbatim would look identical to a non-prefetched connect. What
|
|
6128
|
+
* matters is measured from the moment Angular reached the connect call
|
|
6129
|
+
* (session start, == `lastMilestonePerf` here): the REAL wait until adoption,
|
|
6130
|
+
* and how much the prefetch SAVED by finishing the handshake during page load.
|
|
6131
|
+
*
|
|
6132
|
+
* So the three phases fill exactly the `[session start → adopted]` window
|
|
6133
|
+
* (split across signaling / webrtc by the prefetch's own sub-step ratio; the
|
|
6134
|
+
* region was served from cache → ~0), and the savings figures ride in params:
|
|
6135
|
+
* - `prefetchConnectMs` — full handshake the prefetch measured (the cost a
|
|
6136
|
+
* cold, non-prefetched connect would pay here).
|
|
6137
|
+
* - `prefetchSavedMs` — how much of it completed before Angular was ready.
|
|
6138
|
+
* - `adoptionWaitMs` — the actual wait Angular experienced (= window).
|
|
6139
|
+
* - `adoptionOverheadMs` — gap between prefetch-ready and adoption.
|
|
6140
|
+
*/
|
|
6141
|
+
emitAdoptedConnection(timings) {
|
|
6142
|
+
const s = this.session;
|
|
6143
|
+
if (!s)
|
|
6144
|
+
return;
|
|
6145
|
+
const t = timings ?? {};
|
|
6146
|
+
const start = t['prefetch_start'] ?? s.startedAtPerf;
|
|
6147
|
+
const wsOpen = t['ws_open'] ?? start;
|
|
6148
|
+
const ready = t['ready'] ?? t['dc_open'] ?? wsOpen;
|
|
6149
|
+
// Real prefetch sub-durations — how the handshake actually broke down.
|
|
6150
|
+
const sigRaw = Math.max(0, wsOpen - start);
|
|
6151
|
+
const webRaw = Math.max(0, ready - wsOpen);
|
|
6152
|
+
const connectMs = Math.round(sigRaw + webRaw);
|
|
6153
|
+
// `lastMilestonePerf` is still pinned at session start (the connection
|
|
6154
|
+
// closers are suppressed for adopted sessions), so this is the genuine
|
|
6155
|
+
// connect → adopted wait the user lived through.
|
|
6156
|
+
const now = performance.now();
|
|
6157
|
+
const waitMs = Math.max(0, Math.round(now - s.lastMilestonePerf));
|
|
6158
|
+
// What running the handshake during page load bought us: the part already
|
|
6159
|
+
// done by the time Angular reached the connect call.
|
|
6160
|
+
const savedMs = Math.max(0, Math.min(connectMs, Math.round(s.startedAtPerf - start)));
|
|
6161
|
+
const adoptionOverheadMs = Math.max(0, Math.round(now - ready));
|
|
6162
|
+
const savings = {
|
|
6163
|
+
prefetched: true,
|
|
6164
|
+
prefetchConnectMs: connectMs,
|
|
6165
|
+
prefetchSavedMs: savedMs,
|
|
6166
|
+
adoptionWaitMs: waitMs,
|
|
6167
|
+
adoptionOverheadMs,
|
|
6168
|
+
};
|
|
6169
|
+
// Split the real wait across signaling / webrtc by the prefetch's own
|
|
6170
|
+
// signaling:webrtc proportion.
|
|
6171
|
+
const denom = sigRaw + webRaw;
|
|
6172
|
+
const sigMs = denom > 0 ? Math.round((waitMs * sigRaw) / denom) : 0;
|
|
6173
|
+
const webMs = Math.max(0, waitMs - sigMs);
|
|
6174
|
+
// Region (phaseNo 1) — served from the ping cache during prefetch (~0),
|
|
6175
|
+
// but ship the samples so the phase still carries its data.
|
|
6176
|
+
const region = this.readRegionCache();
|
|
6177
|
+
this.emitAdoptedPhase(0, 0, {
|
|
6178
|
+
region: region?.region_code ?? '',
|
|
6179
|
+
pingsMs: region?.result.length
|
|
6180
|
+
? `${region.result.join(', ')} ms`
|
|
6181
|
+
: null,
|
|
6182
|
+
prefetched: true,
|
|
6183
|
+
}, []);
|
|
6184
|
+
// Signaling (phaseNo 2) — carries the streamer identity + savings summary.
|
|
6185
|
+
this.emitAdoptedPhase(1, sigMs, {
|
|
6186
|
+
...this.adoptedSignalingParams(),
|
|
6187
|
+
...savings,
|
|
6188
|
+
prefetchSignalingMs: Math.round(sigRaw),
|
|
6189
|
+
}, [{ id: 'signaling-ws-connected', time: Math.round(wsOpen) }]);
|
|
6190
|
+
// WebRTC negotiation (phaseNo 3).
|
|
6191
|
+
this.emitAdoptedPhase(2, webMs, {
|
|
6192
|
+
iceCandidateCount: 0,
|
|
6193
|
+
prefetched: true,
|
|
6194
|
+
prefetchWebrtcMs: Math.round(webRaw),
|
|
6195
|
+
}, []);
|
|
6196
|
+
}
|
|
6197
|
+
/** Emit one adopted phase with an explicit (prefetch-derived) duration and
|
|
6198
|
+
* advance `lastMilestonePerf` synthetically so the next phase chains right. */
|
|
6199
|
+
emitAdoptedPhase(bucketIdx, durationMs, extras, milestones) {
|
|
6200
|
+
const s = this.session;
|
|
6201
|
+
if (!s)
|
|
6202
|
+
return;
|
|
6203
|
+
const bucket = this.phaseCatalog[bucketIdx];
|
|
6204
|
+
this.track('stream.phase_ended', {
|
|
6205
|
+
sessionId: s.id,
|
|
6206
|
+
phaseNo: bucketIdx + 1,
|
|
6207
|
+
id: bucket.id,
|
|
6208
|
+
color: bucket.color,
|
|
6209
|
+
duration: durationMs,
|
|
6210
|
+
milestones,
|
|
6211
|
+
params: toPhaseParams(extras),
|
|
6212
|
+
overlays: [],
|
|
6213
|
+
});
|
|
6214
|
+
s.lastMilestonePerf += durationMs;
|
|
6215
|
+
}
|
|
6216
|
+
/** Streamer identity params for the adopted signaling phase — mirrors the
|
|
6217
|
+
* `ssinfo-received` closer's shape (client / instanceName / …ssData). */
|
|
6218
|
+
adoptedSignalingParams() {
|
|
6219
|
+
const aws = this.awsInstance$();
|
|
6220
|
+
const ssData = this.ssData$();
|
|
6221
|
+
return {
|
|
6222
|
+
client: aws?.wsUrl ?? null,
|
|
6223
|
+
instanceName: aws?.instanceName ?? null,
|
|
6224
|
+
...(ssData ?? {}),
|
|
6225
|
+
};
|
|
6226
|
+
}
|
|
6227
|
+
/** Read the region-ping cache the prefetch reused (localStorage), so the
|
|
6228
|
+
* adopted region phase can still report the winning region + samples. */
|
|
6229
|
+
readRegionCache() {
|
|
6230
|
+
try {
|
|
6231
|
+
const raw = localStorage.getItem(REGION_PING_CACHE_KEY);
|
|
6232
|
+
if (!raw)
|
|
6233
|
+
return null;
|
|
6234
|
+
const parsed = JSON.parse(raw);
|
|
6235
|
+
return {
|
|
6236
|
+
region_code: typeof parsed.region_code === 'string' ? parsed.region_code : '',
|
|
6237
|
+
result: Array.isArray(parsed.result)
|
|
6238
|
+
? parsed.result.filter((n) => typeof n === 'number')
|
|
6239
|
+
: [],
|
|
6240
|
+
};
|
|
6241
|
+
}
|
|
6242
|
+
catch {
|
|
6243
|
+
return null;
|
|
6244
|
+
}
|
|
6245
|
+
}
|
|
6085
6246
|
handleWindow(samples) {
|
|
6086
6247
|
if (!this.session)
|
|
6087
6248
|
return;
|