@3dsource/angular-unreal-module 0.0.146-dev.6 → 0.0.146-dev.8

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,127 @@ 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 the phases are positioned at their real (anchor-relative) spans — shifted
6127
+ * LEFT to overlap the HTML-loading / route-idle rows — rather than sequentially
6128
+ * after them. That's the whole point of the prefetch: the handshake already
6129
+ * happened while the page was loading. Each phase ships:
6130
+ * - `duration` — the real sub-step time the prefetch spent.
6131
+ * - `spanStartMs` — anchor-relative start, so the viewer left-shifts the bar.
6132
+ * - `adoptionAtMs` — where Angular took over (the next phase, Unreal
6133
+ * communication, resumes here on the sequential timeline).
6134
+ * - savings params: `prefetchConnectMs` (full handshake), `prefetchSavedMs`
6135
+ * (done before Angular was ready), `adoptionWaitMs` (real connect→adopted
6136
+ * wait), `adoptionOverheadMs` (prefetch-ready → adopted gap).
6137
+ */
6138
+ emitAdoptedConnection(timings) {
6139
+ const s = this.session;
6140
+ if (!s)
6141
+ return;
6142
+ const t = timings ?? {};
6143
+ const now = performance.now();
6144
+ // Everything in anchor-relative ms — the same scale the viewer renders on
6145
+ // (and the prefetch marks share the page `performance` clock).
6146
+ const toMs = (perf) => Math.round(perf - s.anchorPerf);
6147
+ const startMs = toMs(t['prefetch_start'] ?? s.startedAtPerf);
6148
+ const wsOpenMs = Math.max(startMs, toMs(t['ws_open'] ?? t['prefetch_start'] ?? s.startedAtPerf));
6149
+ const readyMs = Math.max(wsOpenMs, toMs(t['ready'] ?? t['dc_open'] ?? t['ws_open'] ?? s.startedAtPerf));
6150
+ const adoptionAtMs = toMs(now);
6151
+ const sessionStartMs = toMs(s.startedAtPerf);
6152
+ const sigDur = wsOpenMs - startMs;
6153
+ const webDur = readyMs - wsOpenMs;
6154
+ const connectMs = readyMs - startMs;
6155
+ // Savings, anchored at the moment Angular reached the connect call.
6156
+ const savings = {
6157
+ prefetched: true,
6158
+ prefetchConnectMs: connectMs,
6159
+ // How much of the handshake finished before Angular was ready (the win).
6160
+ prefetchSavedMs: Math.max(0, Math.min(connectMs, sessionStartMs - startMs)),
6161
+ // The real wait Angular experienced (connect call → adopted).
6162
+ adoptionWaitMs: Math.max(0, adoptionAtMs - sessionStartMs),
6163
+ // Gap between the prefetch being ready and Angular adopting it.
6164
+ adoptionOverheadMs: Math.max(0, adoptionAtMs - readyMs),
6165
+ };
6166
+ // Region (phaseNo 1) — served from the ping cache during prefetch (~0),
6167
+ // positioned at the prefetch start; ship the samples.
6168
+ const region = this.readRegionCache();
6169
+ this.emitAdoptedPhase(0, startMs, 0, adoptionAtMs, {
6170
+ region: region?.region_code ?? '',
6171
+ pingsMs: region?.result.length ? `${region.result.join(', ')} ms` : null,
6172
+ });
6173
+ // Signaling (phaseNo 2) — [prefetch_start → ws_open]; streamer identity +
6174
+ // savings; ws-connected marker sits at the phase's own end.
6175
+ this.emitAdoptedPhase(1, startMs, sigDur, adoptionAtMs, {
6176
+ ...this.adoptedSignalingParams(),
6177
+ ...savings,
6178
+ prefetchSignalingMs: sigDur,
6179
+ }, [{ id: 'signaling-ws-connected', time: wsOpenMs }]);
6180
+ // WebRTC negotiation (phaseNo 3) — [ws_open → ready].
6181
+ this.emitAdoptedPhase(2, wsOpenMs, webDur, adoptionAtMs, {
6182
+ iceCandidateCount: 0,
6183
+ prefetchWebrtcMs: webDur,
6184
+ });
6185
+ // Unreal communication chains from the adoption moment (real wall-clock).
6186
+ s.lastMilestonePerf = now;
6187
+ }
6188
+ /**
6189
+ * Emit one adopted phase positioned at its real `spanStartMs` (left-shifted
6190
+ * to overlap page load) with its real prefetch `duration`. `prefetched`,
6191
+ * `spanStartMs` and `adoptionAtMs` ride as top-level wire fields the viewer
6192
+ * uses to lay the bar out; the rest become display params.
6193
+ */
6194
+ emitAdoptedPhase(bucketIdx, spanStartMs, durationMs, adoptionAtMs, extras, milestones = []) {
6195
+ const s = this.session;
6196
+ if (!s)
6197
+ return;
6198
+ const bucket = this.phaseCatalog[bucketIdx];
6199
+ this.track('stream.phase_ended', {
6200
+ sessionId: s.id,
6201
+ phaseNo: bucketIdx + 1,
6202
+ id: bucket.id,
6203
+ color: bucket.color,
6204
+ duration: durationMs,
6205
+ prefetched: true,
6206
+ spanStartMs,
6207
+ adoptionAtMs,
6208
+ milestones,
6209
+ params: toPhaseParams({ ...extras, prefetched: true }),
6210
+ overlays: [],
6211
+ });
6212
+ }
6213
+ /** Streamer identity params for the adopted signaling phase — mirrors the
6214
+ * `ssinfo-received` closer's shape (client / instanceName / …ssData). */
6215
+ adoptedSignalingParams() {
6216
+ const aws = this.awsInstance$();
6217
+ const ssData = this.ssData$();
6218
+ return {
6219
+ client: aws?.wsUrl ?? null,
6220
+ instanceName: aws?.instanceName ?? null,
6221
+ ...(ssData ?? {}),
6222
+ };
6223
+ }
6224
+ /** Read the region-ping cache the prefetch reused (localStorage), so the
6225
+ * adopted region phase can still report the winning region + samples. */
6226
+ readRegionCache() {
6227
+ try {
6228
+ const raw = localStorage.getItem(REGION_PING_CACHE_KEY);
6229
+ if (!raw)
6230
+ return null;
6231
+ const parsed = JSON.parse(raw);
6232
+ return {
6233
+ region_code: typeof parsed.region_code === 'string' ? parsed.region_code : '',
6234
+ result: Array.isArray(parsed.result)
6235
+ ? parsed.result.filter((n) => typeof n === 'number')
6236
+ : [],
6237
+ };
6238
+ }
6239
+ catch {
6240
+ return null;
6241
+ }
6242
+ }
6095
6243
  handleWindow(samples) {
6096
6244
  if (!this.session)
6097
6245
  return;