@3dsource/angular-unreal-module 0.0.146-dev.6 → 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,
|
|
@@ -3668,6 +3675,7 @@ class StreamAdoptionService {
|
|
|
3668
3675
|
this.store.dispatch(streamAdopted({
|
|
3669
3676
|
machineId: parked.machineId ?? null,
|
|
3670
3677
|
ssData: parked.ssData ?? null,
|
|
3678
|
+
timings: parked.timings ?? null,
|
|
3671
3679
|
}));
|
|
3672
3680
|
// Hand the live socket over without re-sending `requestStream`, reusing the
|
|
3673
3681
|
// script's correlationId (the one `requestStream` was already sent with).
|
|
@@ -4224,24 +4232,6 @@ class UnrealEffects {
|
|
|
4224
4232
|
// defers `connectToSignaling`, which is what normally sets this.
|
|
4225
4233
|
ignoreElements(), startWith(setEstablishingConnection({ value: true })))));
|
|
4226
4234
|
});
|
|
4227
|
-
// Adoption skips `connectToSignaling → getRegion`, so the region-resolving
|
|
4228
|
-
// telemetry phase never fires. Replay the region waterfall (start → end →
|
|
4229
|
-
// resolved) from the cached winner the prefetch reused — a near-zero-length
|
|
4230
|
-
// phase that still carries the ping samples. No live ping sweep; if there is
|
|
4231
|
-
// no cache the resolved event just carries empty values.
|
|
4232
|
-
this.regionTelemetryOnAdopt$ = createEffect(() => {
|
|
4233
|
-
return this.actions$.pipe(ofType(streamAdopted), mergeMap(() => {
|
|
4234
|
-
const cached = this.regionsPingService.readCachedWinner();
|
|
4235
|
-
return [
|
|
4236
|
-
regionPingFetchStart(),
|
|
4237
|
-
regionPingFetchEnd(),
|
|
4238
|
-
regionResolved({
|
|
4239
|
-
region: cached?.region_code ?? '',
|
|
4240
|
-
pingsMs: cached?.result ?? [],
|
|
4241
|
-
}),
|
|
4242
|
-
];
|
|
4243
|
-
}));
|
|
4244
|
-
});
|
|
4245
4235
|
}
|
|
4246
4236
|
canPeerReconnect() {
|
|
4247
4237
|
return RECONNECT_CONFIG.enabled && this.signallingService.isConnected();
|
|
@@ -5543,6 +5533,19 @@ const QP_RED_THRESHOLD = 35;
|
|
|
5543
5533
|
const FPS_LOW_RATIO = 0.7; // p50 fps below 70% of target
|
|
5544
5534
|
const UNLIMITED_REFERENCE_FPS = 60;
|
|
5545
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
|
+
]);
|
|
5546
5549
|
// Persistent per-browser identifier — survives page reloads. Used to group
|
|
5547
5550
|
// multiple sessions of the same anonymous user for funnel / retention analysis.
|
|
5548
5551
|
const USER_ID_STORAGE_KEY = 'source3d.streamTelemetry.userId';
|
|
@@ -5573,6 +5576,17 @@ class StreamStatusTelemetryService2 {
|
|
|
5573
5576
|
this.disconnectReason$ = this.store.selectSignal(unrealFeature.selectDisconnectReason);
|
|
5574
5577
|
this.errorMessage$ = this.store.selectSignal(unrealFeature.selectErrorMessage);
|
|
5575
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);
|
|
5576
5590
|
this.session = null;
|
|
5577
5591
|
/**
|
|
5578
5592
|
* Whether a session has already started during the current page lifetime.
|
|
@@ -5965,6 +5979,13 @@ class StreamStatusTelemetryService2 {
|
|
|
5965
5979
|
if (!this.session)
|
|
5966
5980
|
return;
|
|
5967
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;
|
|
5968
5989
|
this.emitPhaseEnded(bucket, bucketIdx, value);
|
|
5969
5990
|
return;
|
|
5970
5991
|
}
|
|
@@ -5975,6 +5996,12 @@ class StreamStatusTelemetryService2 {
|
|
|
5975
5996
|
}));
|
|
5976
5997
|
});
|
|
5977
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)));
|
|
5978
6005
|
const bootstrapInfo = this.collectBootstrapInfo();
|
|
5979
6006
|
this.emitLifecycle('stream.session_started', {
|
|
5980
6007
|
sessionId: id,
|
|
@@ -6092,6 +6119,130 @@ class StreamStatusTelemetryService2 {
|
|
|
6092
6119
|
overlays,
|
|
6093
6120
|
});
|
|
6094
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
|
+
}
|
|
6095
6246
|
handleWindow(samples) {
|
|
6096
6247
|
if (!this.session)
|
|
6097
6248
|
return;
|