@3dsource/angular-unreal-module 0.0.199 → 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
  }
@@ -2908,6 +2928,13 @@ var StreamAdoptionService = class StreamAdoptionService {
2908
2928
  this.store = inject(Store);
2909
2929
  this.signalling = inject(SignallingService);
2910
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;
2911
2938
  }
2912
2939
  awaitAndAdopt$() {
2913
2940
  return defer(() => {
@@ -2917,32 +2944,47 @@ var StreamAdoptionService = class StreamAdoptionService {
2917
2944
  return of(false);
2918
2945
  }
2919
2946
  if (isAppOwned(slot)) return of(false);
2920
- if (slot.state !== "connecting") return of(this.tryAdoptOrTakeSlot());
2947
+ if (slot.state !== "connecting") return of(this.settle(slot, null));
2921
2948
  Logger.info("[Adoption] Prefetch in progress — waiting before connecting");
2949
+ this.waitStarted$.next();
2922
2950
  return this.awaitSettled$(slot);
2923
2951
  });
2924
2952
  }
2925
2953
  awaitSettled$(parked) {
2926
2954
  return new Observable((subscriber) => {
2927
2955
  let done = false;
2956
+ const sincePerf = performance.now();
2957
+ this.waiting = {
2958
+ parked,
2959
+ sincePerf
2960
+ };
2928
2961
  parked.onSettled(() => {
2929
2962
  if (done) return;
2930
2963
  done = true;
2931
- let adopted = false;
2932
- try {
2933
- adopted = this.tryAdoptOrTakeSlot();
2934
- } catch (e) {
2935
- Logger.error("[Adoption] Adoption threw — falling back", e);
2936
- this.takeSlot();
2937
- }
2938
- subscriber.next(adopted);
2964
+ this.waiting = null;
2965
+ subscriber.next(this.settle(parked, sincePerf));
2939
2966
  subscriber.complete();
2940
2967
  });
2941
2968
  return () => {
2969
+ if (done) return;
2942
2970
  done = true;
2971
+ this.waiting = null;
2972
+ this.report$.next(reportOf(parked, "abandoned", sincePerf));
2943
2973
  };
2944
2974
  });
2945
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
+ }
2946
2988
  tryAdoptOrTakeSlot() {
2947
2989
  const adopted = this.tryAdopt();
2948
2990
  if (!adopted) this.takeSlot();
@@ -2969,7 +3011,7 @@ var StreamAdoptionService = class StreamAdoptionService {
2969
3011
  if (!slot || isAppOwned(slot)) return false;
2970
3012
  const parked = slot;
2971
3013
  if (parked.state !== "ready") return false;
2972
- 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) {
2973
3015
  Logger.warn("[Adoption] Parked stream is ready but transport is not live");
2974
3016
  return false;
2975
3017
  }
@@ -2980,6 +3022,7 @@ var StreamAdoptionService = class StreamAdoptionService {
2980
3022
  pollingUrl: parked.pollingUrl ?? null,
2981
3023
  ssData: parked.ssData ?? null,
2982
3024
  streamRequestId: parked.ids?.streamRequestId || null,
3025
+ correlationId: parked.ids?.correlationId || null,
2983
3026
  kind: parked.kind ?? null,
2984
3027
  instanceName: parked.instanceName ?? null,
2985
3028
  eta: parked.eta ?? null,
@@ -3020,6 +3063,20 @@ i0.ɵɵngDeclareClassMetadata({
3020
3063
  args: [{ providedIn: "root" }]
3021
3064
  }]
3022
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
+ }
3023
3080
  var UnrealCallbackService = class UnrealCallbackService {
3024
3081
  constructor() {
3025
3082
  this.actions$ = inject(Actions);
@@ -3542,6 +3599,10 @@ const dropCodes = [
3542
3599
  function OnCloseHandler(e) {
3543
3600
  const message = `WebSocket closed: ${JSON.stringify(e.code)} - ${e.reason}`;
3544
3601
  Logger.warn(message);
3602
+ this.onCirrusClose$.next({
3603
+ code: e.code,
3604
+ reason: e.reason
3605
+ });
3545
3606
  this.store.dispatch(setCirrusDisconnected());
3546
3607
  if (dropCodes.includes(e.code)) this.store.dispatch(dropConnection());
3547
3608
  }
@@ -5155,6 +5216,7 @@ var StreamStatusTelemetryService2 = class StreamStatusTelemetryService2 {
5155
5216
  this.videoService = inject(VideoService);
5156
5217
  this.signallingService = inject(SignallingService);
5157
5218
  this.webRtcPlayer = inject(WebRtcPlayerService);
5219
+ this.adoption = inject(StreamAdoptionService);
5158
5220
  this.unrealConfig = inject(UNREAL_CONFIG, { optional: true });
5159
5221
  this.destroyRef = inject(DestroyRef);
5160
5222
  this.destroy$ = new Subject();
@@ -5203,6 +5265,10 @@ var StreamStatusTelemetryService2 = class StreamStatusTelemetryService2 {
5203
5265
  id: "region-resolving",
5204
5266
  color: "#61afef",
5205
5267
  milestones: [
5268
+ {
5269
+ id: "prefetch-wait-start",
5270
+ event: this.adoption.waitStarted$
5271
+ },
5206
5272
  {
5207
5273
  id: "region-ping-fetch-start",
5208
5274
  event: this.actions$.pipe(ofType(regionPingFetchStart))
@@ -5235,6 +5301,10 @@ var StreamStatusTelemetryService2 = class StreamStatusTelemetryService2 {
5235
5301
  id: "polling-ended",
5236
5302
  event: this.actions$.pipe(ofType(pollingEnded))
5237
5303
  },
5304
+ {
5305
+ id: "request-stream-sent",
5306
+ event: this.signallingService.onRequestStreamSent$
5307
+ },
5238
5308
  this.awsInstanceResolvedEntry,
5239
5309
  this.signalingServerConnectedEntry,
5240
5310
  {
@@ -5248,12 +5318,7 @@ var StreamStatusTelemetryService2 = class StreamStatusTelemetryService2 {
5248
5318
  this.configReceivedEntry,
5249
5319
  {
5250
5320
  id: "ssinfo-received",
5251
- 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]) => ({
5252
- client: aws.wsUrl,
5253
- instanceName: aws.instanceName,
5254
- ...ssData,
5255
- connectionId
5256
- })))
5321
+ event: this.store.select(unrealFeature.selectSsData).pipe(filter(Boolean), take(1), map(() => this.signalingParams()))
5257
5322
  }
5258
5323
  ]
5259
5324
  },
@@ -5396,12 +5461,48 @@ var StreamStatusTelemetryService2 = class StreamStatusTelemetryService2 {
5396
5461
  handshakeFlow: null,
5397
5462
  commandCount: 0,
5398
5463
  times,
5399
- 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
+ }
5400
5476
  };
5401
5477
  sub.add(this.store.select(selectCirrusConnectionId).pipe(filter(Boolean), take(1)).subscribe((connectionId) => this.resolveWireId(connectionId)));
5402
5478
  sub.add(this.signallingService.onWebRtcIce$.pipe(filter(Boolean)).subscribe(() => {
5403
5479
  if (this.session) this.session.iceCandidateCount++;
5404
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)));
5405
5506
  sub.add(this.actions$.pipe(ofType(answerSent), take(1)).subscribe(() => {
5406
5507
  if (this.session) this.session.handshakeFlow = "server-offer";
5407
5508
  }));
@@ -5447,7 +5548,10 @@ var StreamStatusTelemetryService2 = class StreamStatusTelemetryService2 {
5447
5548
  }));
5448
5549
  });
5449
5550
  });
5450
- 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
+ }));
5451
5555
  const bootstrapInfo = this.collectBootstrapInfo();
5452
5556
  const embedderUrl = this.collectEmbedderUrl();
5453
5557
  this.emitLifecycle("stream.session_started", {
@@ -5475,14 +5579,19 @@ var StreamStatusTelemetryService2 = class StreamStatusTelemetryService2 {
5475
5579
  const errorMessage = this.errorMessage$();
5476
5580
  const granularMessage = errorMessage?.reason === granularReason ? errorMessage.message : void 0;
5477
5581
  const includeGranular = granularReason && granularReason !== DisconnectReason.None;
5582
+ const waiting = this.adoption.waitingReport();
5583
+ if (waiting) this.trackPrefetch(waiting);
5478
5584
  const openPhase = this.phaseCatalog[session.openPhaseIdx];
5479
- 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);
5480
5586
  this.flushPaks(true);
5587
+ const close = session.cirrusClose;
5481
5588
  this.emitLifecycle("stream.session_ended", {
5482
5589
  sessionId: id,
5483
5590
  reason,
5484
5591
  ...includeGranular ? { disconnectReason: granularReason } : {},
5485
5592
  ...includeGranular && granularMessage ? { disconnectMessage: granularMessage } : {},
5593
+ ...close && { closeCode: close.code },
5594
+ ...close?.reason && { closeReason: close.reason },
5486
5595
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
5487
5596
  });
5488
5597
  this.flushOutbox(session, id);
@@ -5561,7 +5670,7 @@ var StreamStatusTelemetryService2 = class StreamStatusTelemetryService2 {
5561
5670
  const region = parkedRegion ?? NO_REGION;
5562
5671
  this.emitAdoptedPhase(0, startMs, 0, adoptionAtMs, regionParams(region));
5563
5672
  this.emitAdoptedPhase(1, startMs, sigDur, adoptionAtMs, {
5564
- ...this.adoptedSignalingParams(),
5673
+ ...this.signalingParams(),
5565
5674
  ...savings,
5566
5675
  prefetchSignalingMs: sigDur
5567
5676
  }, [{
@@ -5596,13 +5705,22 @@ var StreamStatusTelemetryService2 = class StreamStatusTelemetryService2 {
5596
5705
  });
5597
5706
  this.closePhase(bucketIdx);
5598
5707
  }
5599
- adoptedSignalingParams() {
5600
- const aws = this.awsInstance$();
5601
- const ssData = this.ssData$();
5708
+ signalingParams() {
5709
+ const s = this.session;
5710
+ const aws = s?.aws ?? this.awsInstance$();
5602
5711
  return {
5603
5712
  client: aws?.wsUrl ?? null,
5604
5713
  instanceName: aws?.instanceName ?? null,
5605
- ...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$() ?? {},
5606
5724
  connectionId: this.connectionId$()
5607
5725
  };
5608
5726
  }
@@ -5845,6 +5963,26 @@ var StreamStatusTelemetryService2 = class StreamStatusTelemetryService2 {
5845
5963
  ...session?.wireId ? { sessionId: session.wireId } : {}
5846
5964
  });
5847
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
+ }
5848
5986
  emit(event, data) {
5849
5987
  this.store.dispatch(trackTelemetryData({
5850
5988
  event,
@@ -5915,6 +6053,26 @@ function iceParams(ice) {
5915
6053
  function fmtCounts(counts) {
5916
6054
  return Object.entries(counts).map(([k, n]) => `${k} ${n}`).join(" · ") || null;
5917
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
+ }
5918
6076
  function toPhaseParams(value) {
5919
6077
  if (!value || typeof value !== "object") return [];
5920
6078
  const out = [];