@3dsource/angular-unreal-module 0.0.146-dev.0 → 0.0.146-dev.1

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.
@@ -1,10 +1,10 @@
1
1
  import * as i0 from '@angular/core';
2
2
  import { InjectionToken, inject, DestroyRef, ChangeDetectionStrategy, Component, Injectable, signal, effect, untracked, computed, Pipe, input, ElementRef, viewChild, afterNextRender, HostListener, output, makeEnvironmentProviders, provideEnvironmentInitializer } from '@angular/core';
3
3
  import { SourceButtonComponent, SourceLoadingComponent } from '@3dsource/source-ui-native';
4
- import { Subject, skip, share, merge, map, scan, timestamp, startWith, pairwise, concatMap, of, animationFrameScheduler, interval, Observable, timer, take, defer, switchMap, from, throwError, EMPTY, retry, timeout as timeout$1, repeat, takeUntil as takeUntil$1, fromEvent, tap as tap$1, throttleTime, delay, concat, debounceTime, mergeMap, shareReplay, NEVER, asyncScheduler, filter as filter$1, withLatestFrom as withLatestFrom$1, Subscription, bufferTime, combineLatestWith, distinctUntilChanged as distinctUntilChanged$1 } from 'rxjs';
4
+ import { Subject, skip, share, merge, map, scan, timestamp, startWith, pairwise, concatMap, of, animationFrameScheduler, interval, Observable, timer, take, defer, switchMap, from, throwError, EMPTY, retry, timeout as timeout$1, repeat, BehaviorSubject, takeUntil as takeUntil$1, fromEvent, tap as tap$1, throttleTime, delay, concat, debounceTime, mergeMap, ignoreElements, shareReplay, NEVER, asyncScheduler, filter as filter$1, withLatestFrom as withLatestFrom$1, Subscription, bufferTime, combineLatestWith, distinctUntilChanged as distinctUntilChanged$1 } from 'rxjs';
5
5
  import { createAction, props, createReducer, on, createFeature, Store, createSelector, provideState } from '@ngrx/store';
6
6
  import { whereNot, Falsy, Logger, calculateMedian, clampf, lerp, getCanvasCached, getSnapshot, fitIntoRectangle, generateUuid, Truthy, tapLog, Semaphore, COLOR_CODES, where, KeyboardNumericCode, InvertedKeyMap, HEXtoRGB, RGBtoHSV, inverseLerp, HSVtoRGB, RGBtoHEX, fpIsASameAsB } from '@3dsource/utils';
7
- import { filter, map as map$1, distinctUntilChanged, takeWhile, takeUntil, catchError, tap, timeout, finalize, first, switchMap as switchMap$1, withLatestFrom, exhaustMap, debounceTime as debounceTime$1, take as take$1, startWith as startWith$1 } from 'rxjs/operators';
7
+ import { filter, map as map$1, distinctUntilChanged, takeWhile, takeUntil, catchError, tap, timeout, finalize, first, switchMap as switchMap$1, last, withLatestFrom, exhaustMap, debounceTime as debounceTime$1, take as take$1, startWith as startWith$1 } from 'rxjs/operators';
8
8
  import { Actions, ofType, createEffect, provideEffects } from '@ngrx/effects';
9
9
  import { concatLatestFrom } from '@ngrx/operators';
10
10
  import { DialogRef, DIALOG_DATA, Dialog } from '@angular/cdk/dialog';
@@ -643,10 +643,11 @@ const unrealReducer = createReducer(initialState, on(changeStatusMainVideoOnScen
643
643
  return { ...state, analyticsEvent: event };
644
644
  }), on(setCirrusConnected, (state) => {
645
645
  return { ...state, cirrusConnected: true };
646
- }), on(streamAdopted, (state, { machineId }) => {
646
+ }), on(streamAdopted, (state, { machineId, ssData }) => {
647
647
  return {
648
648
  ...state,
649
649
  cirrusConnected: true,
650
+ ssData: ssData ?? state.ssData,
650
651
  awsInstance: {
651
652
  ...state.awsInstance,
652
653
  wsUrl: machineId,
@@ -1314,13 +1315,18 @@ const selectTotalProgress = createSelector(unrealFeature.selectStatusPercentSign
1314
1315
  const SIGNALLING_MAX = SIGNALLING_PERCENT_VALUE / 100;
1315
1316
  const ORCHESTRATION_WS_PATTERN = /^wss:\/\/orchestration-websocket/i;
1316
1317
  const isNewOrchestration = ORCHESTRATION_WS_PATTERN.test(awsInstance?.wsUrl ?? '');
1317
- // Orchestration progress is 0–1 from the backend; map to 0–1 directly.
1318
- // Once the data channel is connected, signaling is fully complete (1.0).
1319
- const signalingProgress = isNewOrchestration
1320
- ? dataChannelConnected
1321
- ? 1
1322
- : (awsInstance?.progressComplete ?? 0)
1323
- : (statusPercentSignallingServer ?? 0) / 100;
1318
+ // Once the data channel is connected, signaling is fully complete (1.0) —
1319
+ // regardless of orchestration type. This is what makes an ADOPTED stream
1320
+ // (whose wsUrl is the machine host, not the orchestration-websocket URL,
1321
+ // and which never set `statusPercentSignallingServer`) correctly show the
1322
+ // whole first phase done instead of stalling near the command-only value.
1323
+ // Otherwise: new orchestration reports 0–1 progress; legacy uses the
1324
+ // polling percent.
1325
+ const signalingProgress = dataChannelConnected
1326
+ ? 1
1327
+ : isNewOrchestration
1328
+ ? (awsInstance?.progressComplete ?? 0)
1329
+ : (statusPercentSignallingServer ?? 0) / 100;
1324
1330
  // +1% bump once WS URL is known (visual step for WS handshake)
1325
1331
  const splitPoint = awsInstance?.wsUrl
1326
1332
  ? SIGNALLING_MAX + 0.01
@@ -2573,7 +2579,13 @@ class VideoService extends SubService {
2573
2579
  this.audio = null;
2574
2580
  this.container = null;
2575
2581
  this.latencyTestTimings = new LatencyTimings();
2576
- this.videoTrack$ = new Subject();
2582
+ // BehaviorSubject (not Subject) so a late subscriber to `videoStats$` still
2583
+ // gets the current track. Adoption (`WebRtcPlayerService.adopt`) calls
2584
+ // `attachVideoStream` synchronously/early — before the resize and app-init
2585
+ // (`initAppByDataChannelReady$`) subscribers attach — and a plain Subject
2586
+ // would drop that emission, so the stats interval (and `setViewportReady`,
2587
+ // gated on `framesPerSecond`) would never start.
2588
+ this.videoTrack$ = new BehaviorSubject(null);
2577
2589
  this.VideoEncoderQP = 0;
2578
2590
  this.aggregatedStats = {};
2579
2591
  this.kalmanFilter1D = new KalmanFilter1D(FilterModel.initialBitrateEstimate, FilterModel.initialErrorCovariance, FilterModel.processNoise, FilterModel.measurementNoise);
@@ -3566,6 +3578,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
3566
3578
  args: [{ providedIn: 'root' }]
3567
3579
  }], ctorParameters: () => [] });
3568
3580
 
3581
+ // How often to re-check `window.__stream.state` while a prefetch is connecting.
3582
+ const ADOPT_POLL_MS = 100;
3569
3583
  /**
3570
3584
  * Adopts a WebRTC connection prefetched by the `stream-prefetch.js` head-script
3571
3585
  * and parked on `window.__stream`, instead of running `connectToSignaling` from
@@ -3579,6 +3593,32 @@ class StreamAdoptionService {
3579
3593
  this.signalling = inject(SignallingService);
3580
3594
  this.webRtc = inject(WebRtcPlayerService);
3581
3595
  }
3596
+ /**
3597
+ * Decide adoption, WAITING for an in-flight prefetch instead of racing it.
3598
+ *
3599
+ * - nothing parked → emit `false` at once (caller connects normally).
3600
+ * - already settled (`ready`/`failed`/`adopted`) → adopt-if-ready at once.
3601
+ * - still `connecting` → poll until it settles, then adopt-if-ready. Bounded
3602
+ * by `timeoutMs` so a stuck prefetch can't block the fallback forever.
3603
+ *
3604
+ * Emits exactly once. Cancel by unsubscribing (the effect does this on
3605
+ * teardown via `takeUntil`) — no adoption runs after cancellation, which is
3606
+ * what stops a fast route-switch from starting overlapping connections.
3607
+ */
3608
+ awaitAndAdopt$(timeoutMs) {
3609
+ const parked = window.__stream;
3610
+ if (!parked) {
3611
+ return of(false);
3612
+ }
3613
+ if (parked.state !== 'connecting') {
3614
+ return of(this.tryAdopt());
3615
+ }
3616
+ Logger.info('[Adoption] Prefetch in progress — waiting before connecting');
3617
+ return timer(0, ADOPT_POLL_MS).pipe(map$1(() => window.__stream?.state ?? 'failed'),
3618
+ // Keep polling while still connecting AND within budget; the `true`
3619
+ // (inclusive) emits the settling value (or the last one at the deadline).
3620
+ takeWhile((state, i) => state === 'connecting' && i * ADOPT_POLL_MS < timeoutMs, true), last(), map$1(() => this.tryAdopt()));
3621
+ }
3582
3622
  /**
3583
3623
  * @returns `true` if a ready parked stream was adopted (caller must NOT
3584
3624
  * start a fresh connection), `false` otherwise.
@@ -3601,11 +3641,15 @@ class StreamAdoptionService {
3601
3641
  // Stop the script's watchdog and mark the handoff complete.
3602
3642
  parked.adopt();
3603
3643
  // Flag cirrus connected (so a later `initSignalling` won't open a 2nd
3604
- // connection) and record the adopted machine. NOTE: clientId/viewId are
3605
- // left untouched — the host app (metabox) already set them authoritatively
3606
- // from its own data before `initSignalling`; the script's ids are only used
3607
- // internally to establish the prefetched connection.
3608
- this.store.dispatch(streamAdopted({ machineId: parked.machineId ?? null }));
3644
+ // connection), record the adopted machine, and hydrate the streamer info
3645
+ // (`ssData`) the prefetch captured from the `ssInfo` message. NOTE:
3646
+ // clientId/viewId are left untouched — the host app (metabox) already set
3647
+ // them authoritatively from its own data before `initSignalling`; the
3648
+ // script's ids are only used internally to establish the connection.
3649
+ this.store.dispatch(streamAdopted({
3650
+ machineId: parked.machineId ?? null,
3651
+ ssData: parked.ssData ?? null,
3652
+ }));
3609
3653
  // Hand the live socket over without re-sending `requestStream`, reusing the
3610
3654
  // script's correlationId (the one `requestStream` was already sent with).
3611
3655
  this.signalling.adoptSocket(parked.ws, parked.ids.correlationId);
@@ -4148,12 +4192,21 @@ class UnrealEffects {
4148
4192
  }), map(([, matchMakerUrls]) => [...matchMakerUrls].filter(Truthy)), filter((urls) => urls.length > 0),
4149
4193
  // Ensure a single in-flight request per attempt.
4150
4194
  exhaustMap((urls) =>
4151
- // Adopt a prefetched `window.__stream` if one is ready; otherwise
4152
- // fall back to opening a fresh signalling connection.
4153
- this.streamAdoptionService.tryAdopt()
4154
- ? EMPTY
4155
- : this.signallingService.connectToSignaling(urls)));
4156
- }, { dispatch: false });
4195
+ // If a prefetched `window.__stream` is ready — or still connecting —
4196
+ // adopt it (waiting for it to finish) instead of racing a second
4197
+ // connection; otherwise fall back to a fresh signalling connection.
4198
+ // `takeUntil(abort)` cancels the wait on teardown / route change so a
4199
+ // fast switch can't leave overlapping connections.
4200
+ this.streamAdoptionService
4201
+ .awaitAndAdopt$(this.dataChannelConnectionTimeout)
4202
+ .pipe(switchMap((adopted) => adopted ? EMPTY : this.signallingService.connectToSignaling(urls)), takeUntil(this.actions$.pipe(ofType(abortEstablishingConnection))),
4203
+ // The adopt/connect stream is side-effect-only (its values aren't
4204
+ // actions) — swallow them, and emit `setEstablishingConnection`
4205
+ // first so the reconnect/resume popup stays hidden during the
4206
+ // (possibly deferred) prefetch-adoption wait. Adoption otherwise
4207
+ // defers `connectToSignaling`, which is what normally sets this.
4208
+ ignoreElements(), startWith(setEstablishingConnection({ value: true })))));
4209
+ });
4157
4210
  }
4158
4211
  canPeerReconnect() {
4159
4212
  return RECONNECT_CONFIG.enabled && this.signallingService.isConnected();