@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 { ...state, disconnectReason: DisconnectReason.None };
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;