@3dsource/angular-unreal-module 0.0.197 → 0.0.200-dev.0

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.
package/LICENSE CHANGED
File without changes
@@ -3,7 +3,7 @@ import { ChangeDetectionStrategy, Component, DestroyRef, Directive, ElementRef,
3
3
  import { JsonPipe, NgOptimizedImage, NgTemplateOutlet } from "@angular/common";
4
4
  import { Store, createAction, createFeature, createReducer, createSelector, on, props, provideState } from "@ngrx/store";
5
5
  import { Actions, createEffect, ofType, provideEffects } from "@ngrx/effects";
6
- import { BehaviorSubject, EMPTY, NEVER, Observable, Subject, Subscription, animationFrameScheduler, asyncScheduler, bufferTime, combineLatestWith, concat, concatMap, debounceTime, defer, delay, filter, firstValueFrom, from, fromEvent, ignoreElements, interval, map, merge, mergeMap, of, pairwise, repeat, retry, scan, share, shareReplay, skip, startWith, switchMap, take, takeUntil, takeWhile, tap, throttleTime, throwError, timeout, timer, timestamp, withLatestFrom } from "rxjs";
6
+ import { BehaviorSubject, EMPTY, NEVER, Observable, Subject, Subscription, TimeoutError, animationFrameScheduler, asyncScheduler, bufferTime, combineLatestWith, concat, concatMap, debounceTime, defer, delay, filter, firstValueFrom, from, fromEvent, ignoreElements, interval, map, merge, mergeMap, of, pairwise, repeat, retry, scan, share, shareReplay, skip, startWith, switchMap, take, takeUntil, takeWhile, tap, throttleTime, throwError, timeout, timer, timestamp, withLatestFrom } from "rxjs";
7
7
  import { UNREAL_CONFIG } from "@3dsource/angular-unreal-module/config";
8
8
  import { catchError, debounceTime as debounceTime$1, distinctUntilChanged, exhaustMap, filter as filter$1, finalize, first, map as map$1, startWith as startWith$1, switchMap as switchMap$1, take as take$1, takeUntil as takeUntil$1, takeWhile as takeWhile$1, tap as tap$1, timeout as timeout$1, withLatestFrom as withLatestFrom$1 } from "rxjs/operators";
9
9
  import { v4 } from "uuid";
@@ -1804,6 +1804,7 @@ const ORCHESTRATION_ERROR_CODES = {
1804
1804
  };
1805
1805
  const RETRY_CODES = /* @__PURE__ */ new Set([ORCHESTRATION_ERROR_CODES.NO_AVAILABLE_INSTANCES.code, ORCHESTRATION_ERROR_CODES.LIMIT_REACHED.code]);
1806
1806
  function OrchestrationErrorHandler(msg) {
1807
+ this.onOrchestrationError$.next(msg.payload);
1807
1808
  if (RETRY_CODES.has(msg.payload.code)) {
1808
1809
  const errorInfo = Object.values(ORCHESTRATION_ERROR_CODES).find(({ code }) => code === msg.payload.code);
1809
1810
  this.store.dispatch(setOrchestrationMessage({ message: errorInfo?.description }));
@@ -1831,6 +1832,10 @@ var SignallingService = class SignallingService extends SubService {
1831
1832
  this.onWebRtcIce$ = new Subject();
1832
1833
  this.onWebRtcAnswer$ = new Subject();
1833
1834
  this.onInstanceReady$ = new Subject();
1835
+ this.onRequestStreamSent$ = new Subject();
1836
+ this.onOrchestrationError$ = new Subject();
1837
+ this.onCirrusClose$ = new Subject();
1838
+ this.onPollResponse$ = new Subject();
1834
1839
  this.abort$ = this.actions$.pipe(ofType(abortEstablishingConnection));
1835
1840
  this.ws = null;
1836
1841
  this.controlWs = null;
@@ -1978,8 +1983,13 @@ var SignallingService = class SignallingService extends SubService {
1978
1983
  }));
1979
1984
  }
1980
1985
  pollLoop(url) {
1981
- return defer(() => this.httpClient.get(url)).pipe(timeout(POLLING_TIME), catchError((err) => {
1986
+ let sentAt = 0;
1987
+ return defer(() => {
1988
+ sentAt = performance.now();
1989
+ return this.httpClient.get(url);
1990
+ }).pipe(timeout(POLLING_TIME), tap$1((data) => this.onPollResponse$.next(pollResponse(200, data, sentAt))), catchError((err) => {
1982
1991
  const status = err?.status;
1992
+ this.onPollResponse$.next(pollResponse(typeof status === "number" && status > 0 ? status : err instanceof TimeoutError ? "timeout" : "network", null, sentAt));
1983
1993
  if (typeof status === "number" && status >= 400 && status < 500 && status !== 429) {
1984
1994
  Logger.warn(`Polling HARD error on ${url} (HTTP ${status}); rotating URL`);
1985
1995
  return throwError(() => err);
@@ -2122,6 +2132,7 @@ var SignallingService = class SignallingService extends SubService {
2122
2132
  context
2123
2133
  }
2124
2134
  });
2135
+ this.onRequestStreamSent$.next(this.correlationId);
2125
2136
  }
2126
2137
  isConnected() {
2127
2138
  return !!(this.ws && this.ws.readyState === 1);
@@ -2161,6 +2172,15 @@ i0.ɵɵngDeclareClassMetadata({
2161
2172
  }],
2162
2173
  ctorParameters: () => []
2163
2174
  });
2175
+ function pollResponse(status, data, sentAt) {
2176
+ return {
2177
+ status,
2178
+ ms: Math.round(performance.now() - sentAt),
2179
+ ready: !!data?.signallingServer,
2180
+ percent: data?.info?.percent ?? null,
2181
+ message: data?.mm_message || data?.error || null
2182
+ };
2183
+ }
2164
2184
  function finite(value) {
2165
2185
  return typeof value === "number" && Number.isFinite(value) ? value : void 0;
2166
2186
  }
@@ -2214,10 +2234,16 @@ function iceSnapshot(stats) {
2214
2234
  };
2215
2235
  }
2216
2236
  var VideoService = class VideoService extends SubService {
2237
+ get video() {
2238
+ return this.video$.value;
2239
+ }
2240
+ set video(video) {
2241
+ this.video$.next(video);
2242
+ }
2217
2243
  constructor() {
2218
2244
  super();
2219
2245
  this.isVideoPlaying = this.store.selectSignal(unrealFeature.selectIsVideoPlaying);
2220
- this.video = null;
2246
+ this.video$ = new BehaviorSubject(null);
2221
2247
  this.audio = null;
2222
2248
  this.container = null;
2223
2249
  this.latencyTestTimings = new LatencyTimings();
@@ -2568,7 +2594,7 @@ var WebRtcPlayerService = class WebRtcPlayerService extends SubService {
2568
2594
  const pc = this.pcClient;
2569
2595
  if (!pc) return;
2570
2596
  await this.setupTransceiversAsync(pc);
2571
- this.dcClient = this.createDataChannel(pc, "UnrealDcConnection", { ordered: true });
2597
+ this.createDataChannel(pc, "UnrealDcConnection", { ordered: true });
2572
2598
  await this.createOffer(pc);
2573
2599
  } catch (e) {
2574
2600
  Logger.error("Failed to create WebRTC offer", e);
@@ -2581,15 +2607,17 @@ var WebRtcPlayerService = class WebRtcPlayerService extends SubService {
2581
2607
  setupPeerConnection(config) {
2582
2608
  this.closePC();
2583
2609
  Logger.log("Creating new RTCPeerConnection with config");
2584
- this.pcClient = new RTCPeerConnection(config);
2585
- this.handlePeerConnectionEvents(this.pcClient);
2610
+ this.ownPeerConnection(new RTCPeerConnection(config));
2611
+ }
2612
+ ownPeerConnection(pc) {
2613
+ this.pcClient = pc;
2614
+ this.handlePeerConnectionEvents(pc);
2586
2615
  this.disconnect$.pipe(take(1), takeUntil$1(this.destroy$)).subscribe(() => {
2587
2616
  this.closePC();
2588
2617
  });
2589
2618
  }
2590
2619
  createDataChannel(pc, label, options) {
2591
- const datachannel = pc.createDataChannel(label, options);
2592
- return this.setupDataChannelCallbacks(datachannel);
2620
+ this.ownDataChannel(pc.createDataChannel(label, options));
2593
2621
  }
2594
2622
  handleCandidateFromServer(iceCandidate) {
2595
2623
  try {
@@ -2709,23 +2737,19 @@ var WebRtcPlayerService = class WebRtcPlayerService extends SubService {
2709
2737
  return true;
2710
2738
  }
2711
2739
  adopt(pc, dc, stream, config, dcBuffer) {
2712
- this.closePC();
2713
- this.pcClient = pc;
2714
2740
  this.cfg = config ?? {};
2715
- this.remoteDescriptionSet = true;
2741
+ this.videoService.create();
2716
2742
  pc.onicecandidate = null;
2717
2743
  pc.ondatachannel = null;
2718
2744
  pc.ontrack = null;
2719
- this.handlePeerConnectionEvents(pc);
2720
- this.disconnect$.pipe(take(1), takeUntil$1(this.destroy$)).subscribe(() => this.closePC());
2745
+ this.closePC();
2746
+ this.ownPeerConnection(pc);
2747
+ this.remoteDescriptionSet = true;
2748
+ this.videoService.attachVideoStream(stream, pc);
2721
2749
  dc.onmessage = null;
2722
2750
  dc.onopen = null;
2723
2751
  dc.onclose = null;
2724
- this.dcClient = this.setupDataChannelCallbacks(dc);
2725
- for (const buffered of dcBuffer) this.onDataChannelMessage$.next(buffered);
2726
- this.onDataChannelConnected();
2727
- this.videoService.create();
2728
- this.videoService.attachVideoStream(stream, pc);
2752
+ this.ownDataChannel(dc, dcBuffer);
2729
2753
  }
2730
2754
  setConfig(parOptions = {}) {
2731
2755
  const urlParams = new URLSearchParams(window.location.search);
@@ -2778,9 +2802,7 @@ var WebRtcPlayerService = class WebRtcPlayerService extends SubService {
2778
2802
  type: "iceCandidate",
2779
2803
  candidate
2780
2804
  }));
2781
- fromEvent(pcClient, "datachannel").pipe(tap((e) => Logger.info(`[DATACHANNEL] Received remote DC: label="${e.channel.label}" id=${e.channel.id} protocol="${e.channel.protocol}" ordered=${e.channel.ordered}`)), map$1((e) => e.channel), takeUntil$1(merge(this.disconnect$, this.destroy$))).subscribe((datachannel) => {
2782
- this.dcClient = this.setupDataChannelCallbacks(datachannel);
2783
- });
2805
+ fromEvent(pcClient, "datachannel").pipe(tap((e) => Logger.info(`[DATACHANNEL] Received remote DC: label="${e.channel.label}" id=${e.channel.id} protocol="${e.channel.protocol}" ordered=${e.channel.ordered}`)), map$1((e) => e.channel), takeUntil$1(merge(this.disconnect$, this.destroy$))).subscribe((datachannel) => this.ownDataChannel(datachannel));
2784
2806
  const onTrack$ = fromEvent(pcClient, "track").pipe(takeUntil$1(merge(this.disconnect$, this.destroy$)), filter$1((e) => !!e.track), tap((e) => Logger.info("Got track - " + e.track.kind + " id=" + e.track.id + " readyState=" + e.track.readyState)), share());
2785
2807
  onTrack$.pipe(filter$1((e) => e.track.kind === "audio"), map$1((e) => e.streams[0])).subscribe((stream) => this.videoService.attachAudioStream(stream));
2786
2808
  onTrack$.pipe(filter$1((e) => e.track.kind === "video"), map$1((e) => e.streams[0]), tap(() => Logger.info("Set video source from video track ontrack."))).subscribe((stream) => {
@@ -2818,7 +2840,8 @@ var WebRtcPlayerService = class WebRtcPlayerService extends SubService {
2818
2840
  } else pc.addTransceiver("audio", { direction: "recvonly" });
2819
2841
  }
2820
2842
  }
2821
- setupDataChannelCallbacks(datachannel) {
2843
+ ownDataChannel(datachannel, replay = []) {
2844
+ this.dcClient = datachannel;
2822
2845
  try {
2823
2846
  Logger.log(`[DATACHANNEL] Setting up datachannel "${datachannel.label}" callbacks...`);
2824
2847
  datachannel.binaryType = "arraybuffer";
@@ -2833,14 +2856,14 @@ var WebRtcPlayerService = class WebRtcPlayerService extends SubService {
2833
2856
  const message = `[DATACHANNEL] Data channel "${datachannel.label}" error. id:${datachannel.id}`;
2834
2857
  Logger.error(message, e);
2835
2858
  });
2836
- fromEvent(datachannel, "open").pipe(tap(() => {
2837
- Logger.info(`[DATACHANNEL] Data channel "${datachannel.label}" connected. id:${datachannel.id}`);
2838
- }), takeUntil$1(merge(this.disconnect$, this.destroy$))).subscribe(() => this.onDataChannelConnected());
2839
2859
  fromEvent(datachannel, "message").pipe(map$1((e) => e.data), takeUntil$1(merge(this.disconnect$, this.destroy$))).subscribe((data) => this.onDataChannelMessage$.next(data));
2840
- return datachannel;
2860
+ for (const buffered of replay) this.onDataChannelMessage$.next(buffered);
2861
+ (datachannel.readyState === "open" ? of(null) : fromEvent(datachannel, "open").pipe(take(1))).pipe(tap(() => {
2862
+ Logger.info(`[DATACHANNEL] Data channel "${datachannel.label}" connected. id:${datachannel.id}`);
2863
+ }), switchMap$1(() => this.videoService.video$.pipe(filter$1(Truthy), take(1))), takeUntil$1(merge(this.disconnect$, this.destroy$))).subscribe(() => this.onDataChannelConnected());
2841
2864
  } catch (e) {
2842
2865
  Logger.warn("[DATACHANNEL] Datachannel setup caused an exception: ", e);
2843
- return null;
2866
+ this.dcClient = null;
2844
2867
  }
2845
2868
  }
2846
2869
  onDataChannelConnected() {
@@ -2905,6 +2928,13 @@ var StreamAdoptionService = class StreamAdoptionService {
2905
2928
  this.store = inject(Store);
2906
2929
  this.signalling = inject(SignallingService);
2907
2930
  this.webRtc = inject(WebRtcPlayerService);
2931
+ this.report$ = new Subject();
2932
+ this.waitStarted$ = new Subject();
2933
+ this.waiting = null;
2934
+ }
2935
+ waitingReport() {
2936
+ const w = this.waiting;
2937
+ return w ? reportOf(w.parked, "waiting", w.sincePerf) : null;
2908
2938
  }
2909
2939
  awaitAndAdopt$() {
2910
2940
  return defer(() => {
@@ -2914,32 +2944,47 @@ var StreamAdoptionService = class StreamAdoptionService {
2914
2944
  return of(false);
2915
2945
  }
2916
2946
  if (isAppOwned(slot)) return of(false);
2917
- if (slot.state !== "connecting") return of(this.tryAdoptOrTakeSlot());
2947
+ if (slot.state !== "connecting") return of(this.settle(slot, null));
2918
2948
  Logger.info("[Adoption] Prefetch in progress — waiting before connecting");
2949
+ this.waitStarted$.next();
2919
2950
  return this.awaitSettled$(slot);
2920
2951
  });
2921
2952
  }
2922
2953
  awaitSettled$(parked) {
2923
2954
  return new Observable((subscriber) => {
2924
2955
  let done = false;
2956
+ const sincePerf = performance.now();
2957
+ this.waiting = {
2958
+ parked,
2959
+ sincePerf
2960
+ };
2925
2961
  parked.onSettled(() => {
2926
2962
  if (done) return;
2927
2963
  done = true;
2928
- let adopted = false;
2929
- try {
2930
- adopted = this.tryAdoptOrTakeSlot();
2931
- } catch (e) {
2932
- Logger.error("[Adoption] Adoption threw — falling back", e);
2933
- this.takeSlot();
2934
- }
2935
- subscriber.next(adopted);
2964
+ this.waiting = null;
2965
+ subscriber.next(this.settle(parked, sincePerf));
2936
2966
  subscriber.complete();
2937
2967
  });
2938
2968
  return () => {
2969
+ if (done) return;
2939
2970
  done = true;
2971
+ this.waiting = null;
2972
+ this.report$.next(reportOf(parked, "abandoned", sincePerf));
2940
2973
  };
2941
2974
  });
2942
2975
  }
2976
+ settle(parked, sincePerf) {
2977
+ let adopted = false;
2978
+ try {
2979
+ adopted = this.tryAdoptOrTakeSlot();
2980
+ } catch (e) {
2981
+ Logger.error("[Adoption] Adoption threw — falling back", e);
2982
+ this.takeSlot();
2983
+ }
2984
+ const outcome = adopted ? "adopted" : parked.state === "failed" ? "failed" : parked.state === "ready" ? "dead" : null;
2985
+ if (outcome) this.report$.next(reportOf(parked, outcome, sincePerf));
2986
+ return adopted;
2987
+ }
2943
2988
  tryAdoptOrTakeSlot() {
2944
2989
  const adopted = this.tryAdopt();
2945
2990
  if (!adopted) this.takeSlot();
@@ -2966,7 +3011,7 @@ var StreamAdoptionService = class StreamAdoptionService {
2966
3011
  if (!slot || isAppOwned(slot)) return false;
2967
3012
  const parked = slot;
2968
3013
  if (parked.state !== "ready") return false;
2969
- if (!parked.ws || parked.ws.readyState !== 1 || !parked.pc || !parked.dc || !parked.stream) {
3014
+ if (parked.deadReason || !parked.ws || parked.ws.readyState !== 1 || !parked.pc || DEAD_PC_STATES.has(parked.pc.connectionState) || !parked.dc || DEAD_DC_STATES.has(parked.dc.readyState) || !parked.stream) {
2970
3015
  Logger.warn("[Adoption] Parked stream is ready but transport is not live");
2971
3016
  return false;
2972
3017
  }
@@ -2977,6 +3022,7 @@ var StreamAdoptionService = class StreamAdoptionService {
2977
3022
  pollingUrl: parked.pollingUrl ?? null,
2978
3023
  ssData: parked.ssData ?? null,
2979
3024
  streamRequestId: parked.ids?.streamRequestId || null,
3025
+ correlationId: parked.ids?.correlationId || null,
2980
3026
  kind: parked.kind ?? null,
2981
3027
  instanceName: parked.instanceName ?? null,
2982
3028
  eta: parked.eta ?? null,
@@ -3017,6 +3063,20 @@ i0.ɵɵngDeclareClassMetadata({
3017
3063
  args: [{ providedIn: "root" }]
3018
3064
  }]
3019
3065
  });
3066
+ const DEAD_PC_STATES = /* @__PURE__ */ new Set(["failed", "closed"]);
3067
+ const DEAD_DC_STATES = /* @__PURE__ */ new Set(["closing", "closed"]);
3068
+ function reportOf(parked, outcome, sincePerf) {
3069
+ return {
3070
+ outcome,
3071
+ failReason: outcome === "dead" ? parked.deadReason ?? "transport-not-live" : parked.failReason ?? null,
3072
+ failError: parked.failError ?? null,
3073
+ waitMs: sincePerf === null ? null : Math.round(performance.now() - sincePerf),
3074
+ kind: parked.kind ?? null,
3075
+ instanceName: parked.instanceName ?? null,
3076
+ streamRequestId: parked.ids?.streamRequestId || null,
3077
+ trail: [...parked.trail ?? []]
3078
+ };
3079
+ }
3020
3080
  var UnrealCallbackService = class UnrealCallbackService {
3021
3081
  constructor() {
3022
3082
  this.actions$ = inject(Actions);
@@ -3539,6 +3599,10 @@ const dropCodes = [
3539
3599
  function OnCloseHandler(e) {
3540
3600
  const message = `WebSocket closed: ${JSON.stringify(e.code)} - ${e.reason}`;
3541
3601
  Logger.warn(message);
3602
+ this.onCirrusClose$.next({
3603
+ code: e.code,
3604
+ reason: e.reason
3605
+ });
3542
3606
  this.store.dispatch(setCirrusDisconnected());
3543
3607
  if (dropCodes.includes(e.code)) this.store.dispatch(dropConnection());
3544
3608
  }
@@ -5152,6 +5216,7 @@ var StreamStatusTelemetryService2 = class StreamStatusTelemetryService2 {
5152
5216
  this.videoService = inject(VideoService);
5153
5217
  this.signallingService = inject(SignallingService);
5154
5218
  this.webRtcPlayer = inject(WebRtcPlayerService);
5219
+ this.adoption = inject(StreamAdoptionService);
5155
5220
  this.unrealConfig = inject(UNREAL_CONFIG, { optional: true });
5156
5221
  this.destroyRef = inject(DestroyRef);
5157
5222
  this.destroy$ = new Subject();
@@ -5200,6 +5265,10 @@ var StreamStatusTelemetryService2 = class StreamStatusTelemetryService2 {
5200
5265
  id: "region-resolving",
5201
5266
  color: "#61afef",
5202
5267
  milestones: [
5268
+ {
5269
+ id: "prefetch-wait-start",
5270
+ event: this.adoption.waitStarted$
5271
+ },
5203
5272
  {
5204
5273
  id: "region-ping-fetch-start",
5205
5274
  event: this.actions$.pipe(ofType(regionPingFetchStart))
@@ -5232,6 +5301,10 @@ var StreamStatusTelemetryService2 = class StreamStatusTelemetryService2 {
5232
5301
  id: "polling-ended",
5233
5302
  event: this.actions$.pipe(ofType(pollingEnded))
5234
5303
  },
5304
+ {
5305
+ id: "request-stream-sent",
5306
+ event: this.signallingService.onRequestStreamSent$
5307
+ },
5235
5308
  this.awsInstanceResolvedEntry,
5236
5309
  this.signalingServerConnectedEntry,
5237
5310
  {
@@ -5245,12 +5318,7 @@ var StreamStatusTelemetryService2 = class StreamStatusTelemetryService2 {
5245
5318
  this.configReceivedEntry,
5246
5319
  {
5247
5320
  id: "ssinfo-received",
5248
- event: this.store.select(unrealFeature.selectSsData).pipe(filter(Boolean), take(1), withLatestFrom(this.store.select(unrealFeature.selectAwsInstance), this.store.select(selectCirrusConnectionId)), map(([ssData, aws, connectionId]) => ({
5249
- client: aws.wsUrl,
5250
- instanceName: aws.instanceName,
5251
- ...ssData,
5252
- connectionId
5253
- })))
5321
+ event: this.store.select(unrealFeature.selectSsData).pipe(filter(Boolean), take(1), map(() => this.signalingParams()))
5254
5322
  }
5255
5323
  ]
5256
5324
  },
@@ -5393,12 +5461,48 @@ var StreamStatusTelemetryService2 = class StreamStatusTelemetryService2 {
5393
5461
  handshakeFlow: null,
5394
5462
  commandCount: 0,
5395
5463
  times,
5396
- openPhaseIdx: 0
5464
+ openPhaseIdx: 0,
5465
+ connectCorrelationId: null,
5466
+ orchestrationError: null,
5467
+ iceServers: null,
5468
+ cirrusClose: null,
5469
+ aws: null,
5470
+ prefetchReported: false,
5471
+ polls: {
5472
+ count: 0,
5473
+ errors: 0,
5474
+ last: null
5475
+ }
5397
5476
  };
5398
5477
  sub.add(this.store.select(selectCirrusConnectionId).pipe(filter(Boolean), take(1)).subscribe((connectionId) => this.resolveWireId(connectionId)));
5399
5478
  sub.add(this.signallingService.onWebRtcIce$.pipe(filter(Boolean)).subscribe(() => {
5400
5479
  if (this.session) this.session.iceCandidateCount++;
5401
5480
  }));
5481
+ sub.add(this.signallingService.onRequestStreamSent$.subscribe((id) => {
5482
+ if (!this.session) return;
5483
+ this.session.connectCorrelationId = id;
5484
+ this.session.aws = null;
5485
+ }));
5486
+ sub.add(this.signallingService.onOrchestrationError$.subscribe((e) => {
5487
+ if (this.session) this.session.orchestrationError = `${e?.code}: ${e?.message ?? ""}`;
5488
+ }));
5489
+ sub.add(this.signallingService.onConfig$.pipe(take(1)).subscribe((msg) => {
5490
+ if (this.session) this.session.iceServers = iceServerHosts(msg?.peerConnectionOptions?.iceServers);
5491
+ }));
5492
+ sub.add(this.actions$.pipe(ofType(setAwsInstance)).subscribe(() => {
5493
+ if (this.session) this.session.aws = this.awsInstance$();
5494
+ }));
5495
+ sub.add(this.signallingService.onCirrusClose$.subscribe((close) => {
5496
+ if (this.session) this.session.cirrusClose = close;
5497
+ }));
5498
+ sub.add(this.signallingService.onPollResponse$.subscribe((poll) => {
5499
+ const polls = this.session?.polls;
5500
+ if (!polls) return;
5501
+ polls.count++;
5502
+ if (poll.status !== 200) polls.errors++;
5503
+ polls.last = poll;
5504
+ }));
5505
+ sub.add(this.adoption.report$.subscribe((report) => this.trackPrefetch(report)));
5402
5506
  sub.add(this.actions$.pipe(ofType(answerSent), take(1)).subscribe(() => {
5403
5507
  if (this.session) this.session.handshakeFlow = "server-offer";
5404
5508
  }));
@@ -5444,7 +5548,10 @@ var StreamStatusTelemetryService2 = class StreamStatusTelemetryService2 {
5444
5548
  }));
5445
5549
  });
5446
5550
  });
5447
- sub.add(this.actions$.pipe(ofType(streamAdopted)).subscribe(({ timings, region }) => this.emitAdoptedConnection(timings, region)));
5551
+ sub.add(this.actions$.pipe(ofType(streamAdopted)).subscribe(({ timings, region, correlationId }) => {
5552
+ if (this.session && correlationId) this.session.connectCorrelationId = correlationId;
5553
+ this.emitAdoptedConnection(timings, region);
5554
+ }));
5448
5555
  const bootstrapInfo = this.collectBootstrapInfo();
5449
5556
  const embedderUrl = this.collectEmbedderUrl();
5450
5557
  this.emitLifecycle("stream.session_started", {
@@ -5472,14 +5579,19 @@ var StreamStatusTelemetryService2 = class StreamStatusTelemetryService2 {
5472
5579
  const errorMessage = this.errorMessage$();
5473
5580
  const granularMessage = errorMessage?.reason === granularReason ? errorMessage.message : void 0;
5474
5581
  const includeGranular = granularReason && granularReason !== DisconnectReason.None;
5582
+ const waiting = this.adoption.waitingReport();
5583
+ if (waiting) this.trackPrefetch(waiting);
5475
5584
  const openPhase = this.phaseCatalog[session.openPhaseIdx];
5476
- if (openPhase) this.emitPhaseEnded(openPhase, session.openPhaseIdx, null, true);
5585
+ if (openPhase) this.emitPhaseEnded(openPhase, session.openPhaseIdx, openPhase.id === "signaling-connection" ? this.signalingParams() : null, true);
5477
5586
  this.flushPaks(true);
5587
+ const close = session.cirrusClose;
5478
5588
  this.emitLifecycle("stream.session_ended", {
5479
5589
  sessionId: id,
5480
5590
  reason,
5481
5591
  ...includeGranular ? { disconnectReason: granularReason } : {},
5482
5592
  ...includeGranular && granularMessage ? { disconnectMessage: granularMessage } : {},
5593
+ ...close && { closeCode: close.code },
5594
+ ...close?.reason && { closeReason: close.reason },
5483
5595
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
5484
5596
  });
5485
5597
  this.flushOutbox(session, id);
@@ -5558,7 +5670,7 @@ var StreamStatusTelemetryService2 = class StreamStatusTelemetryService2 {
5558
5670
  const region = parkedRegion ?? NO_REGION;
5559
5671
  this.emitAdoptedPhase(0, startMs, 0, adoptionAtMs, regionParams(region));
5560
5672
  this.emitAdoptedPhase(1, startMs, sigDur, adoptionAtMs, {
5561
- ...this.adoptedSignalingParams(),
5673
+ ...this.signalingParams(),
5562
5674
  ...savings,
5563
5675
  prefetchSignalingMs: sigDur
5564
5676
  }, [{
@@ -5593,13 +5705,22 @@ var StreamStatusTelemetryService2 = class StreamStatusTelemetryService2 {
5593
5705
  });
5594
5706
  this.closePhase(bucketIdx);
5595
5707
  }
5596
- adoptedSignalingParams() {
5597
- const aws = this.awsInstance$();
5598
- const ssData = this.ssData$();
5708
+ signalingParams() {
5709
+ const s = this.session;
5710
+ const aws = s?.aws ?? this.awsInstance$();
5599
5711
  return {
5600
5712
  client: aws?.wsUrl ?? null,
5601
5713
  instanceName: aws?.instanceName ?? null,
5602
- ...ssData ?? {},
5714
+ transport: aws ? aws.kind ?? "direct" : null,
5715
+ orchestrationUrl: aws?.kind === "socket" ? aws.pollingUrl : null,
5716
+ streamRequestId: aws?.streamRequestId ?? null,
5717
+ connectCorrelationId: s?.connectCorrelationId ?? null,
5718
+ orchestrationError: s?.orchestrationError ?? null,
5719
+ iceServers: s?.iceServers ?? null,
5720
+ orchPolls: s?.polls.count || null,
5721
+ orchPollErrors: s?.polls.errors || null,
5722
+ orchLastPoll: s?.polls.last ? pollText(s.polls.last) : null,
5723
+ ...this.ssData$() ?? {},
5603
5724
  connectionId: this.connectionId$()
5604
5725
  };
5605
5726
  }
@@ -5842,6 +5963,26 @@ var StreamStatusTelemetryService2 = class StreamStatusTelemetryService2 {
5842
5963
  ...session?.wireId ? { sessionId: session.wireId } : {}
5843
5964
  });
5844
5965
  }
5966
+ trackPrefetch(report) {
5967
+ const session = this.session;
5968
+ if (!session || session.prefetchReported) return;
5969
+ session.prefetchReported = true;
5970
+ this.track("stream.prefetch", {
5971
+ sessionId: session.id,
5972
+ outcome: report.outcome,
5973
+ ...report.failReason && { failReason: report.failReason },
5974
+ ...report.failError && { failError: report.failError },
5975
+ ...report.waitMs !== null && { waitMs: report.waitMs },
5976
+ ...report.kind && { kind: report.kind },
5977
+ ...report.instanceName && { instanceName: report.instanceName },
5978
+ ...report.streamRequestId && { streamRequestId: report.streamRequestId },
5979
+ trail: report.trail.map((entry) => ({
5980
+ ...entry,
5981
+ t: Math.round(entry.t - session.anchorPerf)
5982
+ })),
5983
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
5984
+ });
5985
+ }
5845
5986
  emit(event, data) {
5846
5987
  this.store.dispatch(trackTelemetryData({
5847
5988
  event,
@@ -5912,6 +6053,26 @@ function iceParams(ice) {
5912
6053
  function fmtCounts(counts) {
5913
6054
  return Object.entries(counts).map(([k, n]) => `${k} ${n}`).join(" · ") || null;
5914
6055
  }
6056
+ function pollText(poll) {
6057
+ return [
6058
+ poll.status,
6059
+ poll.status === 200 ? poll.ready ? "ready" : "waiting" : null,
6060
+ poll.percent !== null ? `${poll.percent}%` : null,
6061
+ poll.message,
6062
+ `${poll.ms}ms`
6063
+ ].filter((part) => part !== null && part !== "").join(" ");
6064
+ }
6065
+ function iceServerHosts(servers) {
6066
+ const hosts = /* @__PURE__ */ new Set();
6067
+ for (const server of servers ?? []) {
6068
+ const urls = Array.isArray(server.urls) ? server.urls : [server.urls];
6069
+ for (const url of urls) {
6070
+ const m = /^(stuns?|turns?):([^:?/]+)/i.exec(url ?? "");
6071
+ if (m) hosts.add(`${m[1].toLowerCase()}:${m[2].toLowerCase()}`);
6072
+ }
6073
+ }
6074
+ return hosts.size ? [...hosts].join(", ") : null;
6075
+ }
5915
6076
  function toPhaseParams(value) {
5916
6077
  if (!value || typeof value !== "object") return [];
5917
6078
  const out = [];