@3dsource/angular-unreal-module 0.0.117 → 0.0.119
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,7 +1,7 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
2
|
import { InjectionToken, inject, ChangeDetectionStrategy, Component, Injectable, DestroyRef, effect, untracked, computed, signal, Pipe, input, ElementRef, viewChild, afterNextRender, HostListener, output, makeEnvironmentProviders, provideEnvironmentInitializer } from '@angular/core';
|
|
3
3
|
import { SourceButtonComponent, SourceLoadingComponent } from '@3dsource/source-ui-native';
|
|
4
|
-
import { merge, map, scan, timestamp, startWith, pairwise, concatMap, of, animationFrameScheduler, interval, Observable, skip, switchMap, concat, delay, debounceTime, timer, take, timeout, mergeMap, EMPTY, Subject, share, takeUntil as takeUntil$1, from, fromEvent, throwError, defer, retry, tap as tap$1,
|
|
4
|
+
import { merge, map, scan, timestamp, startWith, pairwise, concatMap, of, animationFrameScheduler, interval, Observable, skip, switchMap, concat, delay, debounceTime, timer, take, timeout, mergeMap, EMPTY, Subject, share, takeUntil as takeUntil$1, shareReplay, from, fromEvent, throwError, defer, retry, tap as tap$1, throttleTime, 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, createSelector, Store, provideState } from '@ngrx/store';
|
|
6
6
|
import { Actions, createEffect, ofType, provideEffects } from '@ngrx/effects';
|
|
7
7
|
import { map as map$1, distinctUntilChanged, takeWhile, filter, tap, takeUntil, catchError, withLatestFrom, exhaustMap, switchMap as switchMap$1, timeout as timeout$1, debounceTime as debounceTime$1, take as take$1, startWith as startWith$1 } from 'rxjs/operators';
|
|
@@ -313,7 +313,7 @@ const setAfkTimerVisible = createAction(scoped `set afk timer visible`);
|
|
|
313
313
|
const setAfkTimerHide = createAction(scoped `set afk timer hide`);
|
|
314
314
|
const setAfkTimerCountdown = createAction(scoped `set afk timer count down`, props());
|
|
315
315
|
const showUnrealErrorMessage = createAction(scoped `show unreal error message`, props());
|
|
316
|
-
const initSignalling = createAction(scoped `init signalling
|
|
316
|
+
const initSignalling = createAction(scoped `init signalling`);
|
|
317
317
|
const startStream = createAction(scoped `startStream`, props());
|
|
318
318
|
const resetConfig = createAction(scoped `reset config`);
|
|
319
319
|
const resetAfk = createAction(scoped `reset afk action`);
|
|
@@ -419,14 +419,15 @@ const unrealReducer = createReducer(initialState$1, on(changeStatusMainVideoOnSc
|
|
|
419
419
|
freezeFrame.progress === null
|
|
420
420
|
? freezeFrame.dataUrl
|
|
421
421
|
: state.freezeFrame.dataUrl,
|
|
422
|
-
progress: freezeFrame.progress
|
|
422
|
+
progress: freezeFrame.progress ?? null,
|
|
423
423
|
},
|
|
424
424
|
};
|
|
425
425
|
}), on(disconnectStream, (state, errorMessage) => {
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
if (
|
|
426
|
+
// Skip user-explicit destroy: the user already initiated cleanup, no
|
|
427
|
+
// disconnect message should overwrite that intent.
|
|
428
|
+
if (state.disconnectReason === DisconnectReason.Destroy) {
|
|
429
429
|
return state;
|
|
430
|
+
}
|
|
430
431
|
return { ...state, errorMessage, statusMessage: null };
|
|
431
432
|
}), on(setFreezeFrameFromVideo, (state, freezeFrameFromVideo) => {
|
|
432
433
|
return {
|
|
@@ -467,13 +468,8 @@ const unrealReducer = createReducer(initialState$1, on(changeStatusMainVideoOnSc
|
|
|
467
468
|
warnTimeout: state.userInactivityTimeout,
|
|
468
469
|
},
|
|
469
470
|
};
|
|
470
|
-
}), on(initSignalling, (state
|
|
471
|
-
return {
|
|
472
|
-
...state,
|
|
473
|
-
disconnectReason: resetDisconnectionReason
|
|
474
|
-
? DisconnectReason.None
|
|
475
|
-
: state.disconnectReason,
|
|
476
|
-
};
|
|
471
|
+
}), on(initSignalling, (state) => {
|
|
472
|
+
return { ...state, disconnectReason: DisconnectReason.None };
|
|
477
473
|
}), on(destroyRemoteConnections, (state, { reason }) => {
|
|
478
474
|
if (state.disconnectReason === DisconnectReason.Destroy) {
|
|
479
475
|
return state;
|
|
@@ -558,6 +554,10 @@ const unrealReducer = createReducer(initialState$1, on(changeStatusMainVideoOnSc
|
|
|
558
554
|
...initialState$1,
|
|
559
555
|
establishingConnection: state.establishingConnection,
|
|
560
556
|
disconnectReason: state.disconnectReason,
|
|
557
|
+
// Preserve the disconnect error so the UI can render the reason
|
|
558
|
+
// after the connection drops. Reset back to null on the next
|
|
559
|
+
// successful initSignalling (initialState default applies again).
|
|
560
|
+
errorMessage: state.errorMessage,
|
|
561
561
|
isFirstSuccessLoad: state.isFirstSuccessLoad,
|
|
562
562
|
matchUrls: state.matchUrls,
|
|
563
563
|
streamClientCompanyId: state.streamClientCompanyId,
|
|
@@ -678,10 +678,18 @@ function OnMessageHandler(e) {
|
|
|
678
678
|
this.handleMessage(msg);
|
|
679
679
|
};
|
|
680
680
|
if (typeof e.data === 'string') {
|
|
681
|
-
|
|
681
|
+
try {
|
|
682
|
+
handleData(e.data);
|
|
683
|
+
}
|
|
684
|
+
catch (err) {
|
|
685
|
+
Logger.warn('OnMessageHandler: failed to parse string message', err);
|
|
686
|
+
}
|
|
682
687
|
}
|
|
683
688
|
else if (e.data instanceof Blob) {
|
|
684
|
-
e.data
|
|
689
|
+
e.data
|
|
690
|
+
.text()
|
|
691
|
+
.then(handleData)
|
|
692
|
+
.catch((err) => Logger.warn('OnMessageHandler: failed to parse Blob message', err));
|
|
685
693
|
}
|
|
686
694
|
}
|
|
687
695
|
|
|
@@ -853,12 +861,15 @@ const getImageFromVideoStream = (takeSizeFrom = 'video', imageOutput, sizes) =>
|
|
|
853
861
|
};
|
|
854
862
|
|
|
855
863
|
/**
|
|
856
|
-
* Generator
|
|
857
|
-
*
|
|
858
|
-
*
|
|
864
|
+
* Generator that cycles through `urls` indefinitely. Each call to
|
|
865
|
+
* `getActiveUrl(urls)` returns a fresh generator with its own counter,
|
|
866
|
+
* so concurrent generators (e.g., overlapping reconnect attempts) do
|
|
867
|
+
* not share state. The counter is initialized once per generator
|
|
868
|
+
* instance — before any `yield` — so each `.next()` call advances
|
|
869
|
+
* within that single generator.
|
|
859
870
|
*/
|
|
860
|
-
let index = 0;
|
|
861
871
|
function* getActiveUrl(urls) {
|
|
872
|
+
let index = 0;
|
|
862
873
|
while (true) {
|
|
863
874
|
yield urls[index++ % urls.length];
|
|
864
875
|
}
|
|
@@ -1080,15 +1091,14 @@ function getResizeValuesUniversal(params) {
|
|
|
1080
1091
|
return { w: makeEven(w), h: makeEven(h), x: 0, y: 0, scale: w / maxSpace.w };
|
|
1081
1092
|
}
|
|
1082
1093
|
|
|
1083
|
-
const removeExileCommands = (state, idToRemove) => {
|
|
1094
|
+
const removeExileCommands = (state, idToRemove, now) => {
|
|
1084
1095
|
const exileTimeout = 10000;
|
|
1085
1096
|
const out = { ...state };
|
|
1086
1097
|
out.commandsInProgress = out.commandsInProgress.filter(whereNot({ id: idToRemove }));
|
|
1087
1098
|
out.totalCommandsCompleted++;
|
|
1088
|
-
const time = new Date().getTime();
|
|
1089
1099
|
const markForDelete = [];
|
|
1090
1100
|
for (const { id, timeStamp } of out.commandsInProgress) {
|
|
1091
|
-
if (
|
|
1101
|
+
if (now - timeStamp > exileTimeout) {
|
|
1092
1102
|
markForDelete.push(id);
|
|
1093
1103
|
}
|
|
1094
1104
|
}
|
|
@@ -1101,19 +1111,19 @@ const initialState = {
|
|
|
1101
1111
|
totalCommandsStarted: 0,
|
|
1102
1112
|
totalCommandsCompleted: 0,
|
|
1103
1113
|
};
|
|
1104
|
-
const commandsReducer = createReducer(initialState, on(commandStarted, (state, { id, command }) => {
|
|
1114
|
+
const commandsReducer = createReducer(initialState, on(commandStarted, (state, { id, command, timeStamp }) => {
|
|
1105
1115
|
return {
|
|
1106
1116
|
...state,
|
|
1107
1117
|
totalCommandsStarted: state.totalCommandsStarted + 1,
|
|
1108
1118
|
commandsInProgress: [
|
|
1109
1119
|
...state.commandsInProgress,
|
|
1110
|
-
{ id, command, timeStamp
|
|
1120
|
+
{ id, command, timeStamp },
|
|
1111
1121
|
],
|
|
1112
1122
|
};
|
|
1113
|
-
}), on(commandCompleted, (state, { id }) => {
|
|
1123
|
+
}), on(commandCompleted, (state, { id, now }) => {
|
|
1114
1124
|
return {
|
|
1115
1125
|
...state,
|
|
1116
|
-
...removeExileCommands(state, id),
|
|
1126
|
+
...removeExileCommands(state, id, now),
|
|
1117
1127
|
};
|
|
1118
1128
|
}), on(destroyUnrealScene, setCirrusDisconnected, () => {
|
|
1119
1129
|
return initialState;
|
|
@@ -1242,11 +1252,14 @@ class UnrealEffects {
|
|
|
1242
1252
|
onDataChannelClose: this.unrealInitialConfig?.reconnect?.onDataChannelClose ??
|
|
1243
1253
|
DEFAULT_RECONNECT_ON_DATACHANNEL_CLOSE,
|
|
1244
1254
|
};
|
|
1245
|
-
this.
|
|
1255
|
+
this.destroyConnectionsCleanup$ = createEffect(() => {
|
|
1246
1256
|
return this.actions$.pipe(ofType(destroyUnrealScene), tap(() => {
|
|
1247
1257
|
this.commandsSender.destroy();
|
|
1248
1258
|
this.videoService.setContainer(null);
|
|
1249
|
-
})
|
|
1259
|
+
}));
|
|
1260
|
+
}, { dispatch: false });
|
|
1261
|
+
this.destroyConnections$ = createEffect(() => {
|
|
1262
|
+
return this.actions$.pipe(ofType(destroyUnrealScene), map(() => destroyRemoteConnections({ reason: DisconnectReason.Destroy })));
|
|
1250
1263
|
});
|
|
1251
1264
|
this.changeStreamResolution$ = createEffect(() => {
|
|
1252
1265
|
return this.actions$.pipe(ofType(changeStreamResolutionAction), filter((size) => !!(size?.width && size?.height)), concatLatestFrom(() => this.store.select(unrealFeature.selectStreamResolution)), filter(([newSizes, savedSizes]) => !(newSizes.width === savedSizes.width &&
|
|
@@ -1380,8 +1393,8 @@ class UnrealEffects {
|
|
|
1380
1393
|
}), switchMap(() =>
|
|
1381
1394
|
// Wait for initial commands to be sent before sending the loopback.
|
|
1382
1395
|
// commandStarted fires for each sent command; debounce ensures
|
|
1383
|
-
// we proceed only after no more commands are started for
|
|
1384
|
-
this.actions$.pipe(ofType(commandStarted), debounceTime(
|
|
1396
|
+
// we proceed only after no more commands are started for 50 ms.
|
|
1397
|
+
this.actions$.pipe(ofType(commandStarted), debounceTime(50), take(1), switchMap(() => this.unrealCallbackService.observeCommandResponse({ command: MetaBoxCommand.FLoopBackCommand }, (data) => this.commandsSender.sendCommandToUnreal(data))), switchMap(() => this.videoService.videoStats$), filter(({ aggregatedStats: s }) => !!s.framesPerSecond &&
|
|
1385
1398
|
s.framesPerSecond > MINIMAL_FPS &&
|
|
1386
1399
|
(s.framesDecoded ?? 0) > 15), take(1), map(() => setLoopBackCommandIsCompleted()))));
|
|
1387
1400
|
});
|
|
@@ -1391,8 +1404,12 @@ class UnrealEffects {
|
|
|
1391
1404
|
this.startStream$ = createEffect(() => {
|
|
1392
1405
|
return this.actions$.pipe(ofType(startStream), map(() => initSignalling()));
|
|
1393
1406
|
});
|
|
1394
|
-
|
|
1395
|
-
|
|
1407
|
+
// When the video stream is initialized but autoplay didn't kick in
|
|
1408
|
+
// (browser blocked it; user gesture pending), the user may need extra
|
|
1409
|
+
// time before interacting. Refresh the AFK timer so it doesn't tick
|
|
1410
|
+
// toward an inactivity warning while we're waiting for permission.
|
|
1411
|
+
this.resetAfkOnPausedVideoInit$ = createEffect(() => {
|
|
1412
|
+
return this.actions$.pipe(ofType(videoStreamInitialized), filter(() => !!this.videoService.video?.paused), map(() => resetAfk()));
|
|
1396
1413
|
});
|
|
1397
1414
|
/**
|
|
1398
1415
|
* Effect: `handleDataChannelClosed$`
|
|
@@ -1557,7 +1574,7 @@ class CommandsEffects {
|
|
|
1557
1574
|
this.listenUnrealCallbackByInitSignalling$ = createEffect(() => {
|
|
1558
1575
|
return this.actions$.pipe(ofType(initSignalling), filter(() => !this.unrealInitialConfig?.playwright), switchMap(() => this.actions$.pipe(ofType(unrealCommandCallback), map(({ json }) => 'commandCallback' in json
|
|
1559
1576
|
? json.commandCallback?.correlationId
|
|
1560
|
-
: json?.correlationId), filter(Truthy), map((id) => commandCompleted({ id })))));
|
|
1577
|
+
: json?.correlationId), filter(Truthy), map((id) => commandCompleted({ id, now: Date.now() })))));
|
|
1561
1578
|
});
|
|
1562
1579
|
}
|
|
1563
1580
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: CommandsEffects, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
@@ -1594,7 +1611,6 @@ class AFKService extends SubService {
|
|
|
1594
1611
|
this.unrealConfig = inject(UNREAL_CONFIG, { optional: true });
|
|
1595
1612
|
/** Ticks every time warnTimeout changes or resetAfk action fires */
|
|
1596
1613
|
this.afkResetTrigger = toSignal(merge(this.store.select(selectWarnTimeout), this.actions$.pipe(ofType(resetAfk))));
|
|
1597
|
-
this.disconnected = toSignal(this.disconnect$);
|
|
1598
1614
|
this.destroyRef.onDestroy(() => this.clearTimers());
|
|
1599
1615
|
if (this.unrealConfig?.playwright) {
|
|
1600
1616
|
return;
|
|
@@ -1615,12 +1631,12 @@ class AFKService extends SubService {
|
|
|
1615
1631
|
untracked(() => this.startAfkWarningTimer());
|
|
1616
1632
|
}
|
|
1617
1633
|
});
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1634
|
+
// Stop the AFK timers when Cirrus drops the connection. `disconnect$`
|
|
1635
|
+
// emits void; the previous toSignal(disconnect$) approach never fired
|
|
1636
|
+
// (void → undefined → effect's `!== undefined` guard always false).
|
|
1637
|
+
this.disconnect$
|
|
1638
|
+
.pipe(takeUntil$1(this.destroy$))
|
|
1639
|
+
.subscribe(() => this.stop());
|
|
1624
1640
|
this.actions$
|
|
1625
1641
|
.pipe(ofType(resetUserInactivityTimeout), takeUntil$1(this.destroy$))
|
|
1626
1642
|
.subscribe(() => this.reset());
|
|
@@ -1708,9 +1724,24 @@ class LoadingProgressService {
|
|
|
1708
1724
|
constructor() {
|
|
1709
1725
|
this.#store = inject(Store);
|
|
1710
1726
|
this.loaderVisible = this.#store.selectSignal(selectShowLoader);
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1727
|
+
// Visibility-gated progress source.
|
|
1728
|
+
//
|
|
1729
|
+
// When the loader is visible, subscribe to `selectTotalProgress` and run
|
|
1730
|
+
// a `scan` that keeps the running max — progress should only advance
|
|
1731
|
+
// forward within a single loading session, never regress visually.
|
|
1732
|
+
//
|
|
1733
|
+
// When the loader is hidden, switch to a constant `of(0)`. This is what
|
|
1734
|
+
// makes percents drop to 0 on hide / destroy. A previous attempt used
|
|
1735
|
+
// `keepMaxUntilReset` here; that operator could not work because
|
|
1736
|
+
// `selectTotalProgress` is non-completing — it keeps re-emitting the
|
|
1737
|
+
// last (high) value after a reset, which immediately re-fills the max.
|
|
1738
|
+
// `switchMap` cancels the inner subscription on every visibility change
|
|
1739
|
+
// so stale emissions can't leak through.
|
|
1740
|
+
this.#boundedProgress$ = toObservable(this.loaderVisible).pipe(switchMap((visible) => visible
|
|
1741
|
+
? this.#store
|
|
1742
|
+
.select(selectTotalProgress)
|
|
1743
|
+
.pipe(scan((max, val) => Math.max(max, val), 0))
|
|
1744
|
+
: of(0)), shareReplay({ bufferSize: 1, refCount: true }));
|
|
1714
1745
|
this.smoothPercents = toSignal(this.#boundedProgress$.pipe(floatToSmoothPercents(), distinctUntilChanged()));
|
|
1715
1746
|
this.percents = toSignal(this.#boundedProgress$.pipe(floatToPercents(), distinctUntilChanged()));
|
|
1716
1747
|
this.loadingStatusChanged = computed(() => ({
|
|
@@ -1719,6 +1750,19 @@ class LoadingProgressService {
|
|
|
1719
1750
|
}), ...(ngDevMode ? [{ debugName: "loadingStatusChanged" }] : /* istanbul ignore next */ []));
|
|
1720
1751
|
}
|
|
1721
1752
|
#store;
|
|
1753
|
+
// Visibility-gated progress source.
|
|
1754
|
+
//
|
|
1755
|
+
// When the loader is visible, subscribe to `selectTotalProgress` and run
|
|
1756
|
+
// a `scan` that keeps the running max — progress should only advance
|
|
1757
|
+
// forward within a single loading session, never regress visually.
|
|
1758
|
+
//
|
|
1759
|
+
// When the loader is hidden, switch to a constant `of(0)`. This is what
|
|
1760
|
+
// makes percents drop to 0 on hide / destroy. A previous attempt used
|
|
1761
|
+
// `keepMaxUntilReset` here; that operator could not work because
|
|
1762
|
+
// `selectTotalProgress` is non-completing — it keeps re-emitting the
|
|
1763
|
+
// last (high) value after a reset, which immediately re-fills the max.
|
|
1764
|
+
// `switchMap` cancels the inner subscription on every visibility change
|
|
1765
|
+
// so stale emissions can't leak through.
|
|
1722
1766
|
#boundedProgress$;
|
|
1723
1767
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: LoadingProgressService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
1724
1768
|
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: LoadingProgressService }); }
|
|
@@ -1741,12 +1785,8 @@ class FreezeFrameService extends SubService {
|
|
|
1741
1785
|
if (this.unrealConfig?.playwright) {
|
|
1742
1786
|
return;
|
|
1743
1787
|
}
|
|
1744
|
-
this.store
|
|
1745
|
-
.
|
|
1746
|
-
.pipe(filter(Truthy), takeUntil(this.destroy$))
|
|
1747
|
-
.subscribe(() => this.invalidate());
|
|
1748
|
-
this.actions$
|
|
1749
|
-
.pipe(ofType(videoStreamInitialized), takeUntil(this.destroy$))
|
|
1788
|
+
merge(this.store.select(unrealFeature.selectViewportReady).pipe(filter(Truthy)), this.actions$.pipe(ofType(videoStreamInitialized)))
|
|
1789
|
+
.pipe(takeUntil(this.destroy$))
|
|
1750
1790
|
.subscribe(() => this.invalidate());
|
|
1751
1791
|
}
|
|
1752
1792
|
setData(view) {
|
|
@@ -2100,7 +2140,9 @@ class VideoService extends SubService {
|
|
|
2100
2140
|
video.disablePictureInPicture = true;
|
|
2101
2141
|
this.video = video;
|
|
2102
2142
|
this.container?.appendChild(this.video);
|
|
2103
|
-
this.video
|
|
2143
|
+
fromEvent(this.video, 'play')
|
|
2144
|
+
.pipe(takeUntil$1(merge(this.disconnect$, this.destroy$)))
|
|
2145
|
+
.subscribe(() => this.changeStatusMainVideoOnScene(true));
|
|
2104
2146
|
fromEvent(this.video, 'loadedmetadata')
|
|
2105
2147
|
.pipe(take(1), filter((data) => this.video === data.target), tapLog('VideoService loadedmetadata:'), filter(() => !!this.video))
|
|
2106
2148
|
.subscribe(() => this.store.dispatch(videoStreamInitialized()));
|
|
@@ -2128,6 +2170,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImpo
|
|
|
2128
2170
|
class CommandTelemetryService {
|
|
2129
2171
|
constructor() {
|
|
2130
2172
|
this.actions$ = inject(Actions);
|
|
2173
|
+
this.destroyRef = inject(DestroyRef);
|
|
2174
|
+
this.destroy$ = new Subject();
|
|
2131
2175
|
this.appId = 'metabox-telemetry';
|
|
2132
2176
|
this.commandsSent = {};
|
|
2133
2177
|
this.exileTimeout = 60000;
|
|
@@ -2138,6 +2182,10 @@ class CommandTelemetryService {
|
|
|
2138
2182
|
this.unrealConfig = inject(UNREAL_CONFIG, {
|
|
2139
2183
|
optional: true,
|
|
2140
2184
|
});
|
|
2185
|
+
this.destroyRef.onDestroy(() => {
|
|
2186
|
+
this.destroy$.next();
|
|
2187
|
+
this.destroy$.complete();
|
|
2188
|
+
});
|
|
2141
2189
|
this.init();
|
|
2142
2190
|
}
|
|
2143
2191
|
init() {
|
|
@@ -2150,7 +2198,9 @@ class CommandTelemetryService {
|
|
|
2150
2198
|
this.userId = localStorage.getItem(this.appId) || this.uuid;
|
|
2151
2199
|
sessionStorage.setItem(this.appId, this.sessionId);
|
|
2152
2200
|
localStorage.setItem(this.appId, this.userId);
|
|
2153
|
-
timer(0, this.pollingTime)
|
|
2201
|
+
timer(0, this.pollingTime)
|
|
2202
|
+
.pipe(takeUntil(this.destroy$))
|
|
2203
|
+
.subscribe(() => {
|
|
2154
2204
|
this.send(this.payloads);
|
|
2155
2205
|
});
|
|
2156
2206
|
}
|
|
@@ -2272,7 +2322,7 @@ class CommandTelemetryService {
|
|
|
2272
2322
|
.pipe(ofType(unrealCommandCallback), filter(({ json }) => {
|
|
2273
2323
|
const callback = 'commandCallback' in json ? json.commandCallback : json;
|
|
2274
2324
|
return Truthy(callback?.trackingId);
|
|
2275
|
-
}))
|
|
2325
|
+
}), takeUntil(this.destroy$))
|
|
2276
2326
|
.subscribe(({ json }) => {
|
|
2277
2327
|
const callback = 'commandCallback' in json ? json.commandCallback : json;
|
|
2278
2328
|
const timeStampsRaw = 'timeStamp' in json ? json?.timeStamp : undefined;
|
|
@@ -2312,7 +2362,7 @@ class CommandTelemetryService {
|
|
|
2312
2362
|
* Starts tracking the time for the command.
|
|
2313
2363
|
* Finally, calls the provided function with the modified data object.
|
|
2314
2364
|
*
|
|
2315
|
-
* @param {
|
|
2365
|
+
* @param {ToBeDecorated} funcToDecorate - The function to be decorated with telemetry tracking.
|
|
2316
2366
|
* @param {MetaBoxCommandPacket} data - The data object to be passed to the function. It will be augmented with a unique tracking ID.
|
|
2317
2367
|
*/
|
|
2318
2368
|
wrapper(funcToDecorate, data) {
|
|
@@ -2548,14 +2598,15 @@ function OrchestrationErrorHandler(msg) {
|
|
|
2548
2598
|
const errorInfo = Object.values(ORCHESTRATION_ERROR_CODES).find(({ code }) => code === msg.payload.code);
|
|
2549
2599
|
this.store.dispatch(setOrchestrationMessage({ message: errorInfo?.description }));
|
|
2550
2600
|
this.orchestrationRetrySub?.unsubscribe();
|
|
2551
|
-
this.orchestrationRetrySub = timer(POLLING_TIME)
|
|
2601
|
+
this.orchestrationRetrySub = timer(POLLING_TIME)
|
|
2602
|
+
.pipe(takeUntil(this.destroy$))
|
|
2603
|
+
.subscribe(() => this.sendRequestStream());
|
|
2552
2604
|
}
|
|
2553
2605
|
}
|
|
2554
2606
|
|
|
2555
2607
|
class SignallingService extends SubService {
|
|
2556
2608
|
constructor() {
|
|
2557
2609
|
super();
|
|
2558
|
-
this.action$ = inject(Actions);
|
|
2559
2610
|
this.httpClient = inject(HttpClient);
|
|
2560
2611
|
this.regionsPingService = inject(RegionsPingService);
|
|
2561
2612
|
this.commandTelemetryService = inject(CommandTelemetryService);
|
|
@@ -2568,7 +2619,7 @@ class SignallingService extends SubService {
|
|
|
2568
2619
|
this.onConfig$ = new Subject();
|
|
2569
2620
|
this.onWebRtcIce$ = new Subject();
|
|
2570
2621
|
this.onWebRtcAnswer$ = new Subject();
|
|
2571
|
-
this.abort$ = this.
|
|
2622
|
+
this.abort$ = this.actions$.pipe(ofType(abortEstablishingConnection), tap(() => this.close({
|
|
2572
2623
|
code: 1000,
|
|
2573
2624
|
reason: DisconnectReason.AbortEstablishingConnection,
|
|
2574
2625
|
})));
|
|
@@ -2580,8 +2631,9 @@ class SignallingService extends SubService {
|
|
|
2580
2631
|
return;
|
|
2581
2632
|
}
|
|
2582
2633
|
this.setHandlersFromStream();
|
|
2583
|
-
this.
|
|
2584
|
-
|
|
2634
|
+
const stop$ = merge(this.abort$, this.destroy$);
|
|
2635
|
+
this.actions$
|
|
2636
|
+
.pipe(ofType(dataChannelReady), takeUntil(stop$))
|
|
2585
2637
|
.subscribe(() => {
|
|
2586
2638
|
this.send({
|
|
2587
2639
|
type: 'p2pEstablished',
|
|
@@ -2589,10 +2641,10 @@ class SignallingService extends SubService {
|
|
|
2589
2641
|
sessionId: generateUuid(),
|
|
2590
2642
|
});
|
|
2591
2643
|
});
|
|
2592
|
-
this.
|
|
2644
|
+
this.actions$
|
|
2593
2645
|
.pipe(ofType(dataChannelReady), switchMap(() => this.store
|
|
2594
2646
|
.select(unrealFeature.selectViewportReady)
|
|
2595
|
-
.pipe(filter(Truthy), take(1))), takeUntil(
|
|
2647
|
+
.pipe(filter(Truthy), take(1))), takeUntil(stop$))
|
|
2596
2648
|
.subscribe(() => {
|
|
2597
2649
|
this.send({
|
|
2598
2650
|
type: 'streamPreparedForUserInteraction',
|
|
@@ -2609,11 +2661,12 @@ class SignallingService extends SubService {
|
|
|
2609
2661
|
return EMPTY;
|
|
2610
2662
|
}
|
|
2611
2663
|
Logger.log('CONNECT TO SIGNALLING');
|
|
2612
|
-
// Preparation for WebSocket Orchestration
|
|
2613
|
-
this.correlationId = generateUuid();
|
|
2614
|
-
this.commandTelemetryService.reset();
|
|
2615
|
-
this.startEstablishingConnection();
|
|
2616
2664
|
return defer(() => {
|
|
2665
|
+
// Preparation for WebSocket Orchestration. Inside defer so these
|
|
2666
|
+
// side effects only fire once the caller actually subscribes.
|
|
2667
|
+
this.correlationId = generateUuid();
|
|
2668
|
+
this.commandTelemetryService.reset();
|
|
2669
|
+
this.startEstablishingConnection();
|
|
2617
2670
|
return this.getRegion().pipe(tap((region) => this.region.set(region ?? '')), map$1((region) => this.adaptUrlsToRegion(urlList, region)), switchMap((urls) => this.getAwsInstance(urls).pipe(take(1))), tap((awsInstance) => this.store.dispatch(setAwsInstance(awsInstance))), switchMap(({ wsUrl }) => this.connectToCirrus(wsUrl ?? '').pipe(filter(Truthy), take(1))), takeUntil(this.abort$));
|
|
2618
2671
|
}).pipe(
|
|
2619
2672
|
// Retry the whole attempt on error, advancing to the next URL each time.
|
|
@@ -3634,7 +3687,12 @@ class FileReceiverService extends SubService {
|
|
|
3634
3687
|
this.reset();
|
|
3635
3688
|
}
|
|
3636
3689
|
}
|
|
3690
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: FileReceiverService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
3691
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: FileReceiverService }); }
|
|
3637
3692
|
}
|
|
3693
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: FileReceiverService, decorators: [{
|
|
3694
|
+
type: Injectable
|
|
3695
|
+
}], ctorParameters: () => [] });
|
|
3638
3696
|
|
|
3639
3697
|
const filteredLogs = [
|
|
3640
3698
|
{
|
|
@@ -3642,9 +3700,6 @@ const filteredLogs = [
|
|
|
3642
3700
|
},
|
|
3643
3701
|
];
|
|
3644
3702
|
class AggregatorService extends SubService {
|
|
3645
|
-
/**
|
|
3646
|
-
* Never called for now
|
|
3647
|
-
*/
|
|
3648
3703
|
constructor() {
|
|
3649
3704
|
super();
|
|
3650
3705
|
this.selectStreamConfig = this.store.selectSignal(selectStreamConfig);
|
|
@@ -3683,11 +3738,9 @@ class AggregatorService extends SubService {
|
|
|
3683
3738
|
.pipe(filter(Truthy));
|
|
3684
3739
|
merge(unlockViewport$, this.disconnect$)
|
|
3685
3740
|
.pipe(takeUntil(this.destroy$))
|
|
3686
|
-
.subscribe(() =>
|
|
3687
|
-
this.removeLoadScreen();
|
|
3688
|
-
});
|
|
3741
|
+
.subscribe(() => this.removeLoadScreen());
|
|
3689
3742
|
if (!this.selectStreamConfig()?.autoStart ||
|
|
3690
|
-
location.
|
|
3743
|
+
location.protocol === 'http:') {
|
|
3691
3744
|
this.removeLoadScreen();
|
|
3692
3745
|
}
|
|
3693
3746
|
InputOptions.controlScheme = ControlScheme.HoveringMouse;
|
|
@@ -3934,14 +3987,27 @@ class UnrealCommunicatorService {
|
|
|
3934
3987
|
this.commandTelemetryService = inject(CommandTelemetryService);
|
|
3935
3988
|
this.webRtcPlayerService = inject(WebRtcPlayerService);
|
|
3936
3989
|
this.unrealConfig = inject(UNREAL_CONFIG, { optional: true });
|
|
3990
|
+
this.destroyRef = inject(DestroyRef);
|
|
3937
3991
|
this.destroy$ = new Subject();
|
|
3938
3992
|
this.cirrusConnected = this.store.selectSignal(unrealFeature.selectCirrusConnected);
|
|
3939
3993
|
this.afkIgnoredCommands = new Set(['executeConsoleCommand']);
|
|
3940
3994
|
this.skipAfkReset = false;
|
|
3995
|
+
// Throttle: input events fire 60-200x/sec, but AFK timeouts are measured in
|
|
3996
|
+
// seconds. Leading-edge throttle dispatches once, then suppresses for 1s.
|
|
3997
|
+
this.resetAfk$ = new Subject();
|
|
3998
|
+
this.destroyRef.onDestroy(() => {
|
|
3999
|
+
this.destroy$.next();
|
|
4000
|
+
this.destroy$.complete();
|
|
4001
|
+
});
|
|
4002
|
+
this.resetAfk$
|
|
4003
|
+
.pipe(throttleTime(1000), takeUntil$1(this.destroy$))
|
|
4004
|
+
.subscribe(() => this.store.dispatch(resetAfk()));
|
|
3941
4005
|
this.init();
|
|
3942
4006
|
}
|
|
3943
4007
|
init() {
|
|
3944
|
-
this.isPlaywrightMode()
|
|
4008
|
+
if (this.isPlaywrightMode()) {
|
|
4009
|
+
return;
|
|
4010
|
+
}
|
|
3945
4011
|
this.emitUIInteraction = this.commandTelemetryService.decorate(this.emitUIInteraction.bind(this));
|
|
3946
4012
|
// Listen to the dataChannelConnected ACTION (point-in-time event) instead
|
|
3947
4013
|
// of the store selector. The selector reads the current state and can fire on
|
|
@@ -3961,7 +4027,9 @@ class UnrealCommunicatorService {
|
|
|
3961
4027
|
this.destroy$ = new Subject();
|
|
3962
4028
|
}
|
|
3963
4029
|
sendCommandToUnreal(data) {
|
|
3964
|
-
this.isPlaywrightMode()
|
|
4030
|
+
if (this.isPlaywrightMode()) {
|
|
4031
|
+
return;
|
|
4032
|
+
}
|
|
3965
4033
|
if (this.webRtcPlayerService.canSend()) {
|
|
3966
4034
|
const uniqId = data?.correlationId ?? generateUuid();
|
|
3967
4035
|
const sendData = { ...data, correlationId: uniqId };
|
|
@@ -3971,7 +4039,11 @@ class UnrealCommunicatorService {
|
|
|
3971
4039
|
}
|
|
3972
4040
|
this.emitUIInteraction(sendData);
|
|
3973
4041
|
this.skipAfkReset = false;
|
|
3974
|
-
this.store.dispatch(commandStarted({
|
|
4042
|
+
this.store.dispatch(commandStarted({
|
|
4043
|
+
id: uniqId,
|
|
4044
|
+
command: data.command,
|
|
4045
|
+
timeStamp: Date.now(),
|
|
4046
|
+
}));
|
|
3975
4047
|
}
|
|
3976
4048
|
}
|
|
3977
4049
|
emitUIInteraction(descriptor) {
|
|
@@ -3992,10 +4064,12 @@ class UnrealCommunicatorService {
|
|
|
3992
4064
|
this.emitDescriptor(MessageType.Command, descriptor);
|
|
3993
4065
|
}
|
|
3994
4066
|
sendInputData(data) {
|
|
3995
|
-
this.isPlaywrightMode()
|
|
4067
|
+
if (this.isPlaywrightMode()) {
|
|
4068
|
+
return;
|
|
4069
|
+
}
|
|
3996
4070
|
try {
|
|
3997
4071
|
if (!this.skipAfkReset) {
|
|
3998
|
-
this.
|
|
4072
|
+
this.resetAfk$.next();
|
|
3999
4073
|
}
|
|
4000
4074
|
this.webRtcPlayerService.send(data);
|
|
4001
4075
|
}
|
|
@@ -4006,9 +4080,7 @@ class UnrealCommunicatorService {
|
|
|
4006
4080
|
}
|
|
4007
4081
|
}
|
|
4008
4082
|
isPlaywrightMode() {
|
|
4009
|
-
|
|
4010
|
-
return;
|
|
4011
|
-
}
|
|
4083
|
+
return !!this.unrealConfig?.playwright;
|
|
4012
4084
|
}
|
|
4013
4085
|
emitDescriptor(messageType, descriptor) {
|
|
4014
4086
|
// Convert the descriptor object into a JSON string.
|
|
@@ -4094,7 +4166,7 @@ class InputService extends SubService {
|
|
|
4094
4166
|
}
|
|
4095
4167
|
const visibilityHiddenTrigger$ = fromEvent(document, 'visibilitychange').pipe(map$1(() => document.visibilityState === 'hidden'), filter(Truthy));
|
|
4096
4168
|
const visibilityVisibleTrigger$ = fromEvent(document, 'visibilitychange').pipe(map$1(() => document.visibilityState === 'visible'), filter(Truthy), startWith(true));
|
|
4097
|
-
this.deactivatedVideoTrigger$ = merge(this.disconnect$, this.reInit$, visibilityHiddenTrigger$);
|
|
4169
|
+
this.deactivatedVideoTrigger$ = merge(this.disconnect$, this.reInit$, visibilityHiddenTrigger$, this.destroy$);
|
|
4098
4170
|
merge(visibilityVisibleTrigger$, this.store.select(unrealFeature.selectViewportReady).pipe(filter(Truthy)))
|
|
4099
4171
|
.pipe(filter(() => !!this.video), takeUntil$1(this.destroy$))
|
|
4100
4172
|
.subscribe(() => this.setup());
|
|
@@ -4527,7 +4599,12 @@ class InputService extends SubService {
|
|
|
4527
4599
|
this.sendInputData(data.buffer);
|
|
4528
4600
|
});
|
|
4529
4601
|
}
|
|
4602
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: InputService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
4603
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: InputService }); }
|
|
4530
4604
|
}
|
|
4605
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: InputService, decorators: [{
|
|
4606
|
+
type: Injectable
|
|
4607
|
+
}], ctorParameters: () => [] });
|
|
4531
4608
|
|
|
4532
4609
|
class RegionsPingService {
|
|
4533
4610
|
constructor() {
|
|
@@ -4546,56 +4623,50 @@ class RegionsPingService {
|
|
|
4546
4623
|
// flow (start / providers-fetched / resolved). The final
|
|
4547
4624
|
// `regionResolved` forwards the winner's full ping-sample array
|
|
4548
4625
|
// straight from `PingResult.result` — no aggregation.
|
|
4549
|
-
|
|
4550
|
-
|
|
4551
|
-
|
|
4552
|
-
|
|
4553
|
-
|
|
4626
|
+
return defer(() => {
|
|
4627
|
+
this.store.dispatch(regionPingFetchStart());
|
|
4628
|
+
return this.getProviders(regionListUrl).pipe(tap(() => this.store.dispatch(regionPingFetchEnd())), switchMap((providers) => this.getPingResult(providers)), catchError(() => of(null)), tapLog('PingResult'), tap((data) => this.store.dispatch(regionResolved({
|
|
4629
|
+
region: data?.region_code ?? '',
|
|
4630
|
+
pingsMs: data?.result ?? [],
|
|
4631
|
+
}))), map$1((data) => data?.region_code));
|
|
4632
|
+
});
|
|
4554
4633
|
}
|
|
4555
4634
|
getPingResult(providers) {
|
|
4556
|
-
const
|
|
4557
|
-
this.
|
|
4558
|
-
|
|
4559
|
-
|
|
4560
|
-
|
|
4561
|
-
|
|
4562
|
-
|
|
4563
|
-
|
|
4564
|
-
|
|
4565
|
-
|
|
4566
|
-
|
|
4567
|
-
|
|
4568
|
-
|
|
4569
|
-
|
|
4570
|
-
|
|
4571
|
-
|
|
4572
|
-
|
|
4573
|
-
|
|
4574
|
-
|
|
4575
|
-
}
|
|
4576
|
-
catch {
|
|
4577
|
-
pingResults.push({ region_code: undefined, result: [] });
|
|
4578
|
-
}
|
|
4579
|
-
finally {
|
|
4580
|
-
semaphore.release();
|
|
4581
|
-
}
|
|
4582
|
-
const okValues = pingResults.filter((pingResult) => pingResult.result.length > 1);
|
|
4583
|
-
if (okValues.length) {
|
|
4584
|
-
return okValues[0];
|
|
4585
|
-
}
|
|
4586
|
-
return okValues;
|
|
4587
|
-
});
|
|
4588
|
-
await Promise.all(tasks);
|
|
4589
|
-
resolve(null);
|
|
4635
|
+
const defaultTimeout = providers.timeout ?? this.config.ping_timeout;
|
|
4636
|
+
return from(this.startPinging(providers.regions, defaultTimeout));
|
|
4637
|
+
}
|
|
4638
|
+
getProviders(regionListUrl = this.unrealInitialConfig?.regionsPingUrl || '') {
|
|
4639
|
+
return this.httpClient.get(regionListUrl);
|
|
4640
|
+
}
|
|
4641
|
+
async startPinging(endpoints, defaultTimeout) {
|
|
4642
|
+
const semaphore = new Semaphore(this.config.max_parallel_requests);
|
|
4643
|
+
const tasks = endpoints.map(async ({ region_code, url, timeout }) => {
|
|
4644
|
+
await semaphore.acquire();
|
|
4645
|
+
try {
|
|
4646
|
+
const result = await this.checkLatency(url, timeout ?? defaultTimeout);
|
|
4647
|
+
if (result.length < 2)
|
|
4648
|
+
throw new Error('insufficient pings');
|
|
4649
|
+
return { region_code, result };
|
|
4650
|
+
}
|
|
4651
|
+
finally {
|
|
4652
|
+
semaphore.release();
|
|
4653
|
+
}
|
|
4590
4654
|
});
|
|
4655
|
+
try {
|
|
4656
|
+
return await Promise.any(tasks);
|
|
4657
|
+
}
|
|
4658
|
+
catch {
|
|
4659
|
+
return null;
|
|
4660
|
+
}
|
|
4591
4661
|
}
|
|
4592
|
-
async checkLatency(endpoint, timeout
|
|
4662
|
+
async checkLatency(endpoint, timeout) {
|
|
4593
4663
|
const pingResults = [];
|
|
4664
|
+
const sep = endpoint.includes('?') ? '&' : '?';
|
|
4594
4665
|
for (let i = 0; i < this.config.ping_count; i++) {
|
|
4595
4666
|
const startTime = performance.now();
|
|
4596
4667
|
try {
|
|
4597
4668
|
await this.fetchWithTimeout(this.config.bust_cache
|
|
4598
|
-
? `${endpoint}
|
|
4669
|
+
? `${endpoint}${sep}cache_buster=${performance.now()}`
|
|
4599
4670
|
: endpoint, {
|
|
4600
4671
|
method: 'GET',
|
|
4601
4672
|
mode: 'no-cors',
|
|
@@ -4604,11 +4675,11 @@ class RegionsPingService {
|
|
|
4604
4675
|
Pragma: 'no-cache',
|
|
4605
4676
|
Expires: '0',
|
|
4606
4677
|
},
|
|
4607
|
-
credentials: 'omit',
|
|
4678
|
+
credentials: 'omit',
|
|
4608
4679
|
}, timeout);
|
|
4609
4680
|
}
|
|
4610
4681
|
catch {
|
|
4611
|
-
//
|
|
4682
|
+
// intentional: finally pushes the elapsed time as the latency sample
|
|
4612
4683
|
}
|
|
4613
4684
|
finally {
|
|
4614
4685
|
const latency = Math.round(performance.now() - startTime);
|
|
@@ -4638,10 +4709,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImpo
|
|
|
4638
4709
|
type: Injectable
|
|
4639
4710
|
}] });
|
|
4640
4711
|
|
|
4641
|
-
class StreamStatusTelemetryService {
|
|
4712
|
+
class StreamStatusTelemetryService extends SubService {
|
|
4642
4713
|
constructor() {
|
|
4643
|
-
|
|
4644
|
-
this.actions$ = inject(Actions);
|
|
4714
|
+
super();
|
|
4645
4715
|
this.videoService = inject(VideoService);
|
|
4646
4716
|
this.unrealConfig = inject(UNREAL_CONFIG, { optional: true });
|
|
4647
4717
|
this.init();
|
|
@@ -4671,8 +4741,8 @@ class StreamStatusTelemetryService {
|
|
|
4671
4741
|
signalingReadyTime,
|
|
4672
4742
|
viewPortReadyTime,
|
|
4673
4743
|
})));
|
|
4674
|
-
}))
|
|
4675
|
-
.subscribe((data) => this.
|
|
4744
|
+
}), takeUntil$1(this.destroy$))
|
|
4745
|
+
.subscribe((data) => this.store.dispatch(trackMixpanelEvent({ event: 'streamStatus', data })));
|
|
4676
4746
|
}
|
|
4677
4747
|
/**
|
|
4678
4748
|
* Emits `undefined` until `readyState$` first emits a truthy value,
|
|
@@ -4703,9 +4773,6 @@ class StreamStatusTelemetryService {
|
|
|
4703
4773
|
VideoEncoderQP,
|
|
4704
4774
|
};
|
|
4705
4775
|
}
|
|
4706
|
-
trackEventToMixPanel(data) {
|
|
4707
|
-
this.store.dispatch(trackMixpanelEvent({ event: 'streamStatus', data }));
|
|
4708
|
-
}
|
|
4709
4776
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: StreamStatusTelemetryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
4710
4777
|
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: StreamStatusTelemetryService }); }
|
|
4711
4778
|
}
|
|
@@ -4727,6 +4794,8 @@ class StreamStatusTelemetryService2 {
|
|
|
4727
4794
|
this.videoService = inject(VideoService);
|
|
4728
4795
|
this.signallingService = inject(SignallingService);
|
|
4729
4796
|
this.unrealConfig = inject(UNREAL_CONFIG, { optional: true });
|
|
4797
|
+
this.destroyRef = inject(DestroyRef);
|
|
4798
|
+
this.destroy$ = new Subject();
|
|
4730
4799
|
/**
|
|
4731
4800
|
* Per-browser anonymous user id. Read from `localStorage` on first access;
|
|
4732
4801
|
* if missing, a new UUID is generated and persisted. Included as
|
|
@@ -4942,6 +5011,13 @@ class StreamStatusTelemetryService2 {
|
|
|
4942
5011
|
],
|
|
4943
5012
|
},
|
|
4944
5013
|
];
|
|
5014
|
+
this.destroyRef.onDestroy(() => {
|
|
5015
|
+
// End any active session BEFORE completing destroy$, so the
|
|
5016
|
+
// session-scoped subscriptions in `this.session.sub` are released.
|
|
5017
|
+
this.endSession('destroy');
|
|
5018
|
+
this.destroy$.next();
|
|
5019
|
+
this.destroy$.complete();
|
|
5020
|
+
});
|
|
4945
5021
|
if (this.unrealConfig?.playwright ||
|
|
4946
5022
|
!this.unrealConfig?.streamTelemetryV2) {
|
|
4947
5023
|
return;
|
|
@@ -4966,16 +5042,18 @@ class StreamStatusTelemetryService2 {
|
|
|
4966
5042
|
init() {
|
|
4967
5043
|
// Start a new session on every initSignalling dispatch.
|
|
4968
5044
|
this.actions$
|
|
4969
|
-
.pipe(ofType(initSignalling))
|
|
5045
|
+
.pipe(ofType(initSignalling), takeUntil$1(this.destroy$))
|
|
4970
5046
|
.subscribe(() => this.startSession());
|
|
4971
5047
|
// End the current session when Cirrus drops (true → false transition).
|
|
4972
5048
|
this.store
|
|
4973
5049
|
.select(unrealFeature.selectCirrusConnected)
|
|
4974
|
-
.pipe(pairwise(), filter$1(([prev, curr]) => prev && !curr))
|
|
5050
|
+
.pipe(pairwise(), filter$1(([prev, curr]) => prev && !curr), takeUntil$1(this.destroy$))
|
|
4975
5051
|
.subscribe(() => this.endSession('disconnected'));
|
|
4976
5052
|
// Close the session when the tab is hidden / being evicted from cache.
|
|
4977
5053
|
// `pagehide` fires in more cases than `beforeunload` (covers iOS Safari, bfcache).
|
|
4978
|
-
fromEvent(window, 'pagehide')
|
|
5054
|
+
fromEvent(window, 'pagehide')
|
|
5055
|
+
.pipe(takeUntil$1(this.destroy$))
|
|
5056
|
+
.subscribe(() => this.endSession('pagehide'));
|
|
4979
5057
|
}
|
|
4980
5058
|
startSession() {
|
|
4981
5059
|
// If a previous session is still active, close it first.
|
|
@@ -5435,17 +5513,19 @@ class FileHandlerService {
|
|
|
5435
5513
|
observeFileResponse(mimetype, data, sender, timeOut = 60000) {
|
|
5436
5514
|
const correlationId = generateUuid();
|
|
5437
5515
|
const out = { ...data, correlationId };
|
|
5438
|
-
|
|
5439
|
-
|
|
5440
|
-
|
|
5441
|
-
|
|
5442
|
-
|
|
5443
|
-
|
|
5444
|
-
|
|
5445
|
-
|
|
5446
|
-
|
|
5447
|
-
|
|
5448
|
-
|
|
5516
|
+
return defer(() => {
|
|
5517
|
+
const observable = this.fileService.fileComplete$.pipe(switchMap((data) => {
|
|
5518
|
+
if (data.valid && this.fileHandlers[mimetype]) {
|
|
5519
|
+
return this.fileHandlers[mimetype](data, correlationId).pipe(filter(Truthy));
|
|
5520
|
+
}
|
|
5521
|
+
return of(null);
|
|
5522
|
+
}), timeout(timeOut), catchError((e) => {
|
|
5523
|
+
Logger.error(e);
|
|
5524
|
+
return of(null);
|
|
5525
|
+
}), take(1));
|
|
5526
|
+
setTimeout(() => sender(out), 0);
|
|
5527
|
+
return observable;
|
|
5528
|
+
});
|
|
5449
5529
|
}
|
|
5450
5530
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: FileHandlerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
5451
5531
|
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: FileHandlerService }); }
|
|
@@ -5455,25 +5535,21 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImpo
|
|
|
5455
5535
|
}] });
|
|
5456
5536
|
|
|
5457
5537
|
class AnalyticsService extends SubService {
|
|
5458
|
-
/** Signals monitorMouse inner subscriptions to stop (viewport lost / new session). */
|
|
5459
5538
|
#stopMouse$ = new Subject();
|
|
5460
|
-
sendEventToMixPanel(name, params = {}) {
|
|
5461
|
-
Logger.log('Analytics Event:', name, params);
|
|
5462
|
-
this.store.dispatch(saveAnalyticsEvent({ event: { name, params } }));
|
|
5463
|
-
}
|
|
5464
5539
|
constructor() {
|
|
5465
5540
|
super();
|
|
5466
|
-
const abortSignal$ = this.store
|
|
5467
|
-
.select(unrealFeature.selectViewportReady)
|
|
5468
|
-
.pipe(filter$1(Falsy), map(() => void 0));
|
|
5469
5541
|
this.store
|
|
5470
5542
|
.select(unrealFeature.selectViewportReady)
|
|
5471
|
-
.pipe(distinctUntilChanged(),
|
|
5472
|
-
.subscribe(() =>
|
|
5543
|
+
.pipe(distinctUntilChanged(), takeUntil$1(this.destroy$))
|
|
5544
|
+
.subscribe((ready) => {
|
|
5545
|
+
this.#stopMouse$.next();
|
|
5546
|
+
if (ready) {
|
|
5547
|
+
this.monitorMouse();
|
|
5548
|
+
}
|
|
5549
|
+
});
|
|
5473
5550
|
}
|
|
5474
|
-
monitorMouse(
|
|
5475
|
-
this.#stopMouse$.
|
|
5476
|
-
const stop$ = merge(this.#stopMouse$.asObservable(), abortSignal$);
|
|
5551
|
+
monitorMouse() {
|
|
5552
|
+
const stop$ = this.#stopMouse$.asObservable();
|
|
5477
5553
|
const element = document.getElementById('streamingVideo');
|
|
5478
5554
|
if (!element) {
|
|
5479
5555
|
return;
|
|
@@ -5538,6 +5614,10 @@ class AnalyticsService extends SubService {
|
|
|
5538
5614
|
});
|
|
5539
5615
|
});
|
|
5540
5616
|
}
|
|
5617
|
+
sendEventToMixPanel(name, params = {}) {
|
|
5618
|
+
Logger.log('Analytics Event:', name, params);
|
|
5619
|
+
this.store.dispatch(saveAnalyticsEvent({ event: { name, params } }));
|
|
5620
|
+
}
|
|
5541
5621
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: AnalyticsService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
5542
5622
|
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: AnalyticsService }); }
|
|
5543
5623
|
}
|
|
@@ -5618,10 +5698,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImpo
|
|
|
5618
5698
|
* the existing `CommandsEffects.setMaxFps$` effect that sends the
|
|
5619
5699
|
* `t.MaxFPS <value>` console command to Unreal Engine.
|
|
5620
5700
|
*/
|
|
5621
|
-
class FpsMonitorService {
|
|
5701
|
+
class FpsMonitorService extends SubService {
|
|
5622
5702
|
constructor() {
|
|
5703
|
+
super();
|
|
5623
5704
|
this.videoService = inject(VideoService);
|
|
5624
|
-
this.store = inject(Store);
|
|
5625
5705
|
this.unrealConfig = inject(UNREAL_CONFIG, { optional: true });
|
|
5626
5706
|
/** Discrete FPS targets the controller can switch between (ascending order). Last 0 is an unlimited framerate indicator*/
|
|
5627
5707
|
this.FPS_STEPS = [30, 40, 50, 0];
|
|
@@ -5660,9 +5740,6 @@ class FpsMonitorService {
|
|
|
5660
5740
|
this.samples = [];
|
|
5661
5741
|
this.lastDecisionTime = Date.now();
|
|
5662
5742
|
this.lastUpgrade = 'hold';
|
|
5663
|
-
this.disconnect$ = this.store
|
|
5664
|
-
.select(unrealFeature.selectCirrusConnected)
|
|
5665
|
-
.pipe(skip(1), filter(Falsy));
|
|
5666
5743
|
this.init();
|
|
5667
5744
|
}
|
|
5668
5745
|
init() {
|
|
@@ -5672,17 +5749,19 @@ class FpsMonitorService {
|
|
|
5672
5749
|
// Reset mutable state and re-subscribe when a new session starts
|
|
5673
5750
|
this.store
|
|
5674
5751
|
.select(unrealFeature.selectViewportReady)
|
|
5675
|
-
.pipe(filter(Boolean))
|
|
5752
|
+
.pipe(filter(Boolean), takeUntil(this.destroy$))
|
|
5676
5753
|
.subscribe(() => this.startSession());
|
|
5677
5754
|
// Reset mutable state when session ends
|
|
5678
|
-
this.disconnect
|
|
5755
|
+
this.disconnect$
|
|
5756
|
+
.pipe(takeUntil(this.destroy$))
|
|
5757
|
+
.subscribe(() => this.resetSession());
|
|
5679
5758
|
}
|
|
5680
5759
|
startSession() {
|
|
5681
5760
|
// Tear down previous stats subscription if any
|
|
5682
5761
|
this.session$.next();
|
|
5683
5762
|
this.resetMutableState();
|
|
5684
5763
|
this.videoService.videoStats$
|
|
5685
|
-
.pipe(withLatestFrom(this.store.select(unrealFeature.selectViewportReady)), filter(([, viewportReady]) => viewportReady), takeUntil(this.session$))
|
|
5764
|
+
.pipe(withLatestFrom(this.store.select(unrealFeature.selectViewportReady)), filter(([, viewportReady]) => viewportReady), takeUntil(merge(this.session$, this.destroy$)))
|
|
5686
5765
|
.subscribe(([stats]) => this.processTick(stats));
|
|
5687
5766
|
}
|
|
5688
5767
|
resetSession() {
|
|
@@ -5968,14 +6047,16 @@ class UnrealCallbackService {
|
|
|
5968
6047
|
*/
|
|
5969
6048
|
observeCommandResponse(data, sender, timeOut = 60000, dispatchOnTimeout = true) {
|
|
5970
6049
|
const correlationId = generateUuid();
|
|
5971
|
-
|
|
5972
|
-
|
|
5973
|
-
|
|
5974
|
-
|
|
5975
|
-
|
|
5976
|
-
|
|
5977
|
-
|
|
5978
|
-
|
|
6050
|
+
return defer(() => {
|
|
6051
|
+
const observable = this.actions$.pipe(ofType(unrealCommandCallback), map$1(({ json }) => 'commandCallback' in json ? json.commandCallback : json), filter((json) => json.correlationId === correlationId), timeout(timeOut), catchError(() => dispatchOnTimeout
|
|
6052
|
+
? of({ command: 'timeout', correlationId: 'timeout' })
|
|
6053
|
+
: of(null)), take(1), tapLog('Command Response Observer'), filter(Truthy), map$1((data) => ({
|
|
6054
|
+
command: data.command,
|
|
6055
|
+
payload: 'payload' in data ? data.payload : null,
|
|
6056
|
+
})));
|
|
6057
|
+
setTimeout(() => sender({ ...data, correlationId }), 0);
|
|
6058
|
+
return observable;
|
|
6059
|
+
});
|
|
5979
6060
|
}
|
|
5980
6061
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: UnrealCallbackService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
5981
6062
|
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: UnrealCallbackService }); }
|
|
@@ -6291,7 +6372,7 @@ class ResumeStreamButtonComponent {
|
|
|
6291
6372
|
constructor() {
|
|
6292
6373
|
this.#store = inject(Store);
|
|
6293
6374
|
this.streamConfig = this.#store.selectSignal(selectStreamConfig);
|
|
6294
|
-
this.isSecondStart = signal(
|
|
6375
|
+
this.isSecondStart = signal(this.streamConfig()?.autoStart && location.protocol === 'https:', ...(ngDevMode ? [{ debugName: "isSecondStart" }] : /* istanbul ignore next */ []));
|
|
6295
6376
|
this.isLoaderScreenVisible = this.#store.selectSignal(selectIsLoaderScreenVisible);
|
|
6296
6377
|
this.showReconnectPopup = this.#store.selectSignal(selectShowReconnectPopup);
|
|
6297
6378
|
this.isExistMatchUrls = this.#store.selectSignal(selectIsExistMatchUrls);
|
|
@@ -6651,6 +6732,14 @@ class LowBandwidthDetectorComponent {
|
|
|
6651
6732
|
this.videoService = inject(VideoService);
|
|
6652
6733
|
this.isDevMode = inject(DevModeService).isDevMode;
|
|
6653
6734
|
this.viewportReady$ = this.#store.select(unrealFeature.selectViewportReady);
|
|
6735
|
+
// TODO: `isLowBandwidth$` is currently stubbed to `of(true)`, which
|
|
6736
|
+
// makes the entire monitoring chain in `ngOnInit` unreachable
|
|
6737
|
+
// (`monitoringTrigger$` always emits `false`, `canSwitchToLowBand$`
|
|
6738
|
+
// never passes its `filter(Truthy)`, and `trySetLowBandwidth` is
|
|
6739
|
+
// never invoked). Only the initial `toggleIndicator(true)` fires
|
|
6740
|
+
// once on mount. Wire this to a real reactive source (e.g. derived
|
|
6741
|
+
// from bitrate/FPS thresholds in `aggregatedStats$`) before
|
|
6742
|
+
// re-enabling the LBM feature, and remove this comment.
|
|
6654
6743
|
this.isLowBandwidth$ = of(true);
|
|
6655
6744
|
this.lowBandwidthTrigger$ = this.isLowBandwidth$.pipe(distinctUntilChanged$1(), filter(Truthy));
|
|
6656
6745
|
this.highBandwidthTrigger$ = this.isLowBandwidth$.pipe(distinctUntilChanged$1(), filter(Falsy));
|