@3dsource/angular-unreal-module 0.0.118 → 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,11 +1,12 @@
|
|
|
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
|
-
import { map as map$1, distinctUntilChanged, takeWhile, filter, tap,
|
|
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';
|
|
8
8
|
import { Logger, calculateMedian, clampf, lerp, getCanvasCached, getSnapshot, fitIntoRectangle, whereNot, Truthy, tapLog, Falsy, generateUuid, COLOR_CODES, where, KeyboardNumericCode, InvertedKeyMap, Semaphore, HEXtoRGB, RGBtoHSV, inverseLerp, HSVtoRGB, RGBtoHEX, fpIsASameAsB } from '@3dsource/utils';
|
|
9
|
+
import { concatLatestFrom } from '@ngrx/operators';
|
|
9
10
|
import { DialogRef, DIALOG_DATA, Dialog } from '@angular/cdk/dialog';
|
|
10
11
|
import { HttpClient } from '@angular/common/http';
|
|
11
12
|
import { ScrollStrategyOptions } from '@angular/cdk/overlay';
|
|
@@ -312,7 +313,7 @@ const setAfkTimerVisible = createAction(scoped `set afk timer visible`);
|
|
|
312
313
|
const setAfkTimerHide = createAction(scoped `set afk timer hide`);
|
|
313
314
|
const setAfkTimerCountdown = createAction(scoped `set afk timer count down`, props());
|
|
314
315
|
const showUnrealErrorMessage = createAction(scoped `show unreal error message`, props());
|
|
315
|
-
const initSignalling = createAction(scoped `init signalling
|
|
316
|
+
const initSignalling = createAction(scoped `init signalling`);
|
|
316
317
|
const startStream = createAction(scoped `startStream`, props());
|
|
317
318
|
const resetConfig = createAction(scoped `reset config`);
|
|
318
319
|
const resetAfk = createAction(scoped `reset afk action`);
|
|
@@ -418,14 +419,15 @@ const unrealReducer = createReducer(initialState$1, on(changeStatusMainVideoOnSc
|
|
|
418
419
|
freezeFrame.progress === null
|
|
419
420
|
? freezeFrame.dataUrl
|
|
420
421
|
: state.freezeFrame.dataUrl,
|
|
421
|
-
progress: freezeFrame.progress
|
|
422
|
+
progress: freezeFrame.progress ?? null,
|
|
422
423
|
},
|
|
423
424
|
};
|
|
424
425
|
}), on(disconnectStream, (state, errorMessage) => {
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
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) {
|
|
428
429
|
return state;
|
|
430
|
+
}
|
|
429
431
|
return { ...state, errorMessage, statusMessage: null };
|
|
430
432
|
}), on(setFreezeFrameFromVideo, (state, freezeFrameFromVideo) => {
|
|
431
433
|
return {
|
|
@@ -466,13 +468,8 @@ const unrealReducer = createReducer(initialState$1, on(changeStatusMainVideoOnSc
|
|
|
466
468
|
warnTimeout: state.userInactivityTimeout,
|
|
467
469
|
},
|
|
468
470
|
};
|
|
469
|
-
}), on(initSignalling, (state
|
|
470
|
-
return {
|
|
471
|
-
...state,
|
|
472
|
-
disconnectReason: resetDisconnectionReason
|
|
473
|
-
? DisconnectReason.None
|
|
474
|
-
: state.disconnectReason,
|
|
475
|
-
};
|
|
471
|
+
}), on(initSignalling, (state) => {
|
|
472
|
+
return { ...state, disconnectReason: DisconnectReason.None };
|
|
476
473
|
}), on(destroyRemoteConnections, (state, { reason }) => {
|
|
477
474
|
if (state.disconnectReason === DisconnectReason.Destroy) {
|
|
478
475
|
return state;
|
|
@@ -557,6 +554,10 @@ const unrealReducer = createReducer(initialState$1, on(changeStatusMainVideoOnSc
|
|
|
557
554
|
...initialState$1,
|
|
558
555
|
establishingConnection: state.establishingConnection,
|
|
559
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,
|
|
560
561
|
isFirstSuccessLoad: state.isFirstSuccessLoad,
|
|
561
562
|
matchUrls: state.matchUrls,
|
|
562
563
|
streamClientCompanyId: state.streamClientCompanyId,
|
|
@@ -677,10 +678,18 @@ function OnMessageHandler(e) {
|
|
|
677
678
|
this.handleMessage(msg);
|
|
678
679
|
};
|
|
679
680
|
if (typeof e.data === 'string') {
|
|
680
|
-
|
|
681
|
+
try {
|
|
682
|
+
handleData(e.data);
|
|
683
|
+
}
|
|
684
|
+
catch (err) {
|
|
685
|
+
Logger.warn('OnMessageHandler: failed to parse string message', err);
|
|
686
|
+
}
|
|
681
687
|
}
|
|
682
688
|
else if (e.data instanceof Blob) {
|
|
683
|
-
e.data
|
|
689
|
+
e.data
|
|
690
|
+
.text()
|
|
691
|
+
.then(handleData)
|
|
692
|
+
.catch((err) => Logger.warn('OnMessageHandler: failed to parse Blob message', err));
|
|
684
693
|
}
|
|
685
694
|
}
|
|
686
695
|
|
|
@@ -852,12 +861,15 @@ const getImageFromVideoStream = (takeSizeFrom = 'video', imageOutput, sizes) =>
|
|
|
852
861
|
};
|
|
853
862
|
|
|
854
863
|
/**
|
|
855
|
-
* Generator
|
|
856
|
-
*
|
|
857
|
-
*
|
|
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.
|
|
858
870
|
*/
|
|
859
|
-
let index = 0;
|
|
860
871
|
function* getActiveUrl(urls) {
|
|
872
|
+
let index = 0;
|
|
861
873
|
while (true) {
|
|
862
874
|
yield urls[index++ % urls.length];
|
|
863
875
|
}
|
|
@@ -1079,15 +1091,14 @@ function getResizeValuesUniversal(params) {
|
|
|
1079
1091
|
return { w: makeEven(w), h: makeEven(h), x: 0, y: 0, scale: w / maxSpace.w };
|
|
1080
1092
|
}
|
|
1081
1093
|
|
|
1082
|
-
const removeExileCommands = (state, idToRemove) => {
|
|
1094
|
+
const removeExileCommands = (state, idToRemove, now) => {
|
|
1083
1095
|
const exileTimeout = 10000;
|
|
1084
1096
|
const out = { ...state };
|
|
1085
1097
|
out.commandsInProgress = out.commandsInProgress.filter(whereNot({ id: idToRemove }));
|
|
1086
1098
|
out.totalCommandsCompleted++;
|
|
1087
|
-
const time = new Date().getTime();
|
|
1088
1099
|
const markForDelete = [];
|
|
1089
1100
|
for (const { id, timeStamp } of out.commandsInProgress) {
|
|
1090
|
-
if (
|
|
1101
|
+
if (now - timeStamp > exileTimeout) {
|
|
1091
1102
|
markForDelete.push(id);
|
|
1092
1103
|
}
|
|
1093
1104
|
}
|
|
@@ -1100,19 +1111,19 @@ const initialState = {
|
|
|
1100
1111
|
totalCommandsStarted: 0,
|
|
1101
1112
|
totalCommandsCompleted: 0,
|
|
1102
1113
|
};
|
|
1103
|
-
const commandsReducer = createReducer(initialState, on(commandStarted, (state, { id, command }) => {
|
|
1114
|
+
const commandsReducer = createReducer(initialState, on(commandStarted, (state, { id, command, timeStamp }) => {
|
|
1104
1115
|
return {
|
|
1105
1116
|
...state,
|
|
1106
1117
|
totalCommandsStarted: state.totalCommandsStarted + 1,
|
|
1107
1118
|
commandsInProgress: [
|
|
1108
1119
|
...state.commandsInProgress,
|
|
1109
|
-
{ id, command, timeStamp
|
|
1120
|
+
{ id, command, timeStamp },
|
|
1110
1121
|
],
|
|
1111
1122
|
};
|
|
1112
|
-
}), on(commandCompleted, (state, { id }) => {
|
|
1123
|
+
}), on(commandCompleted, (state, { id, now }) => {
|
|
1113
1124
|
return {
|
|
1114
1125
|
...state,
|
|
1115
|
-
...removeExileCommands(state, id),
|
|
1126
|
+
...removeExileCommands(state, id, now),
|
|
1116
1127
|
};
|
|
1117
1128
|
}), on(destroyUnrealScene, setCirrusDisconnected, () => {
|
|
1118
1129
|
return initialState;
|
|
@@ -1241,14 +1252,17 @@ class UnrealEffects {
|
|
|
1241
1252
|
onDataChannelClose: this.unrealInitialConfig?.reconnect?.onDataChannelClose ??
|
|
1242
1253
|
DEFAULT_RECONNECT_ON_DATACHANNEL_CLOSE,
|
|
1243
1254
|
};
|
|
1244
|
-
this.
|
|
1255
|
+
this.destroyConnectionsCleanup$ = createEffect(() => {
|
|
1245
1256
|
return this.actions$.pipe(ofType(destroyUnrealScene), tap(() => {
|
|
1246
1257
|
this.commandsSender.destroy();
|
|
1247
1258
|
this.videoService.setContainer(null);
|
|
1248
|
-
})
|
|
1259
|
+
}));
|
|
1260
|
+
}, { dispatch: false });
|
|
1261
|
+
this.destroyConnections$ = createEffect(() => {
|
|
1262
|
+
return this.actions$.pipe(ofType(destroyUnrealScene), map(() => destroyRemoteConnections({ reason: DisconnectReason.Destroy })));
|
|
1249
1263
|
});
|
|
1250
1264
|
this.changeStreamResolution$ = createEffect(() => {
|
|
1251
|
-
return this.actions$.pipe(ofType(changeStreamResolutionAction), filter((size) => !!(size?.width && size?.height)),
|
|
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 &&
|
|
1252
1266
|
newSizes.height === savedSizes.height)), map(([sizes]) => changeStreamResolutionSuccessAction({
|
|
1253
1267
|
width: sizes.width,
|
|
1254
1268
|
height: sizes.height,
|
|
@@ -1339,10 +1353,10 @@ class UnrealEffects {
|
|
|
1339
1353
|
* a visible flicker (video flashing to black).
|
|
1340
1354
|
*/
|
|
1341
1355
|
this.forceViewportNotReady$ = createEffect(() => {
|
|
1342
|
-
return this.actions$.pipe(ofType(destroyRemoteConnections),
|
|
1356
|
+
return this.actions$.pipe(ofType(destroyRemoteConnections), concatLatestFrom(() => this.store.select(unrealFeature.selectViewportReady)), filter(([, viewportReady]) => viewportReady), map(() => setViewportNotReady()));
|
|
1343
1357
|
});
|
|
1344
1358
|
this.resetCirrusOnDestroy$ = createEffect(() => {
|
|
1345
|
-
return this.actions$.pipe(ofType(destroyRemoteConnections),
|
|
1359
|
+
return this.actions$.pipe(ofType(destroyRemoteConnections), concatLatestFrom(() => this.store.select(unrealFeature.selectCirrusConnected)), filter(([, cirrusConnected]) => cirrusConnected), map(() => setCirrusDisconnected()));
|
|
1346
1360
|
});
|
|
1347
1361
|
this.destroyRemoteConnections$ = createEffect(() => {
|
|
1348
1362
|
return this.actions$.pipe(ofType(destroyRemoteConnections), tap(({ reason }) => {
|
|
@@ -1354,7 +1368,7 @@ class UnrealEffects {
|
|
|
1354
1368
|
}), filter(({ reason }) => reason === DisconnectReason.DropConnection), map(() => dropConnection()));
|
|
1355
1369
|
});
|
|
1356
1370
|
this.destroyConnectionAndRestart$ = createEffect(() => {
|
|
1357
|
-
return this.actions$.pipe(ofType(setCirrusDisconnected),
|
|
1371
|
+
return this.actions$.pipe(ofType(setCirrusDisconnected), concatLatestFrom(() => this.store.select(unrealFeature.selectDisconnectReason)), filter(([, reason]) => reason === DisconnectReason.DataChannelTimeout), map(() => initSignalling()));
|
|
1358
1372
|
});
|
|
1359
1373
|
this.showUnrealError$ = createEffect(() => {
|
|
1360
1374
|
return this.actions$.pipe(ofType(showUnrealErrorMessage), map(({ code }) => getRtcErrorMessage(code)), distinctUntilChanged(), filter(Truthy), switchMap((content) => this.dialog.open(UnrealErrorModalComponent, {
|
|
@@ -1363,7 +1377,7 @@ class UnrealEffects {
|
|
|
1363
1377
|
}).closed), map(() => showUnrealErrorMessage({ code: null })));
|
|
1364
1378
|
});
|
|
1365
1379
|
this.unrealReceiverReady$ = createEffect(() => {
|
|
1366
|
-
return this.actions$.pipe(ofType(dataChannelConnected),
|
|
1380
|
+
return this.actions$.pipe(ofType(dataChannelConnected), concatLatestFrom(() => this.store.select(unrealFeature.selectIsReconnecting)), filter(([, isReconnecting]) => !isReconnecting), map(() => dataChannelReady()));
|
|
1367
1381
|
});
|
|
1368
1382
|
this.initAppByDataChannelReady$ = createEffect(() => {
|
|
1369
1383
|
return this.actions$.pipe(ofType(dataChannelReady), tap(() => {
|
|
@@ -1379,8 +1393,8 @@ class UnrealEffects {
|
|
|
1379
1393
|
}), switchMap(() =>
|
|
1380
1394
|
// Wait for initial commands to be sent before sending the loopback.
|
|
1381
1395
|
// commandStarted fires for each sent command; debounce ensures
|
|
1382
|
-
// we proceed only after no more commands are started for
|
|
1383
|
-
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 &&
|
|
1384
1398
|
s.framesPerSecond > MINIMAL_FPS &&
|
|
1385
1399
|
(s.framesDecoded ?? 0) > 15), take(1), map(() => setLoopBackCommandIsCompleted()))));
|
|
1386
1400
|
});
|
|
@@ -1390,8 +1404,12 @@ class UnrealEffects {
|
|
|
1390
1404
|
this.startStream$ = createEffect(() => {
|
|
1391
1405
|
return this.actions$.pipe(ofType(startStream), map(() => initSignalling()));
|
|
1392
1406
|
});
|
|
1393
|
-
|
|
1394
|
-
|
|
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()));
|
|
1395
1413
|
});
|
|
1396
1414
|
/**
|
|
1397
1415
|
* Effect: `handleDataChannelClosed$`
|
|
@@ -1556,7 +1574,7 @@ class CommandsEffects {
|
|
|
1556
1574
|
this.listenUnrealCallbackByInitSignalling$ = createEffect(() => {
|
|
1557
1575
|
return this.actions$.pipe(ofType(initSignalling), filter(() => !this.unrealInitialConfig?.playwright), switchMap(() => this.actions$.pipe(ofType(unrealCommandCallback), map(({ json }) => 'commandCallback' in json
|
|
1558
1576
|
? json.commandCallback?.correlationId
|
|
1559
|
-
: json?.correlationId), filter(Truthy), map((id) => commandCompleted({ id })))));
|
|
1577
|
+
: json?.correlationId), filter(Truthy), map((id) => commandCompleted({ id, now: Date.now() })))));
|
|
1560
1578
|
});
|
|
1561
1579
|
}
|
|
1562
1580
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: CommandsEffects, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
@@ -1593,7 +1611,6 @@ class AFKService extends SubService {
|
|
|
1593
1611
|
this.unrealConfig = inject(UNREAL_CONFIG, { optional: true });
|
|
1594
1612
|
/** Ticks every time warnTimeout changes or resetAfk action fires */
|
|
1595
1613
|
this.afkResetTrigger = toSignal(merge(this.store.select(selectWarnTimeout), this.actions$.pipe(ofType(resetAfk))));
|
|
1596
|
-
this.disconnected = toSignal(this.disconnect$);
|
|
1597
1614
|
this.destroyRef.onDestroy(() => this.clearTimers());
|
|
1598
1615
|
if (this.unrealConfig?.playwright) {
|
|
1599
1616
|
return;
|
|
@@ -1614,12 +1631,12 @@ class AFKService extends SubService {
|
|
|
1614
1631
|
untracked(() => this.startAfkWarningTimer());
|
|
1615
1632
|
}
|
|
1616
1633
|
});
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
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());
|
|
1623
1640
|
this.actions$
|
|
1624
1641
|
.pipe(ofType(resetUserInactivityTimeout), takeUntil$1(this.destroy$))
|
|
1625
1642
|
.subscribe(() => this.reset());
|
|
@@ -1707,9 +1724,24 @@ class LoadingProgressService {
|
|
|
1707
1724
|
constructor() {
|
|
1708
1725
|
this.#store = inject(Store);
|
|
1709
1726
|
this.loaderVisible = this.#store.selectSignal(selectShowLoader);
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
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 }));
|
|
1713
1745
|
this.smoothPercents = toSignal(this.#boundedProgress$.pipe(floatToSmoothPercents(), distinctUntilChanged()));
|
|
1714
1746
|
this.percents = toSignal(this.#boundedProgress$.pipe(floatToPercents(), distinctUntilChanged()));
|
|
1715
1747
|
this.loadingStatusChanged = computed(() => ({
|
|
@@ -1718,6 +1750,19 @@ class LoadingProgressService {
|
|
|
1718
1750
|
}), ...(ngDevMode ? [{ debugName: "loadingStatusChanged" }] : /* istanbul ignore next */ []));
|
|
1719
1751
|
}
|
|
1720
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.
|
|
1721
1766
|
#boundedProgress$;
|
|
1722
1767
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: LoadingProgressService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
1723
1768
|
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: LoadingProgressService }); }
|
|
@@ -1740,12 +1785,8 @@ class FreezeFrameService extends SubService {
|
|
|
1740
1785
|
if (this.unrealConfig?.playwright) {
|
|
1741
1786
|
return;
|
|
1742
1787
|
}
|
|
1743
|
-
this.store
|
|
1744
|
-
.
|
|
1745
|
-
.pipe(filter(Truthy), takeUntil(this.destroy$))
|
|
1746
|
-
.subscribe(() => this.invalidate());
|
|
1747
|
-
this.actions$
|
|
1748
|
-
.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$))
|
|
1749
1790
|
.subscribe(() => this.invalidate());
|
|
1750
1791
|
}
|
|
1751
1792
|
setData(view) {
|
|
@@ -2099,7 +2140,9 @@ class VideoService extends SubService {
|
|
|
2099
2140
|
video.disablePictureInPicture = true;
|
|
2100
2141
|
this.video = video;
|
|
2101
2142
|
this.container?.appendChild(this.video);
|
|
2102
|
-
this.video
|
|
2143
|
+
fromEvent(this.video, 'play')
|
|
2144
|
+
.pipe(takeUntil$1(merge(this.disconnect$, this.destroy$)))
|
|
2145
|
+
.subscribe(() => this.changeStatusMainVideoOnScene(true));
|
|
2103
2146
|
fromEvent(this.video, 'loadedmetadata')
|
|
2104
2147
|
.pipe(take(1), filter((data) => this.video === data.target), tapLog('VideoService loadedmetadata:'), filter(() => !!this.video))
|
|
2105
2148
|
.subscribe(() => this.store.dispatch(videoStreamInitialized()));
|
|
@@ -2127,6 +2170,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImpo
|
|
|
2127
2170
|
class CommandTelemetryService {
|
|
2128
2171
|
constructor() {
|
|
2129
2172
|
this.actions$ = inject(Actions);
|
|
2173
|
+
this.destroyRef = inject(DestroyRef);
|
|
2174
|
+
this.destroy$ = new Subject();
|
|
2130
2175
|
this.appId = 'metabox-telemetry';
|
|
2131
2176
|
this.commandsSent = {};
|
|
2132
2177
|
this.exileTimeout = 60000;
|
|
@@ -2137,6 +2182,10 @@ class CommandTelemetryService {
|
|
|
2137
2182
|
this.unrealConfig = inject(UNREAL_CONFIG, {
|
|
2138
2183
|
optional: true,
|
|
2139
2184
|
});
|
|
2185
|
+
this.destroyRef.onDestroy(() => {
|
|
2186
|
+
this.destroy$.next();
|
|
2187
|
+
this.destroy$.complete();
|
|
2188
|
+
});
|
|
2140
2189
|
this.init();
|
|
2141
2190
|
}
|
|
2142
2191
|
init() {
|
|
@@ -2149,7 +2198,9 @@ class CommandTelemetryService {
|
|
|
2149
2198
|
this.userId = localStorage.getItem(this.appId) || this.uuid;
|
|
2150
2199
|
sessionStorage.setItem(this.appId, this.sessionId);
|
|
2151
2200
|
localStorage.setItem(this.appId, this.userId);
|
|
2152
|
-
timer(0, this.pollingTime)
|
|
2201
|
+
timer(0, this.pollingTime)
|
|
2202
|
+
.pipe(takeUntil(this.destroy$))
|
|
2203
|
+
.subscribe(() => {
|
|
2153
2204
|
this.send(this.payloads);
|
|
2154
2205
|
});
|
|
2155
2206
|
}
|
|
@@ -2271,7 +2322,7 @@ class CommandTelemetryService {
|
|
|
2271
2322
|
.pipe(ofType(unrealCommandCallback), filter(({ json }) => {
|
|
2272
2323
|
const callback = 'commandCallback' in json ? json.commandCallback : json;
|
|
2273
2324
|
return Truthy(callback?.trackingId);
|
|
2274
|
-
}))
|
|
2325
|
+
}), takeUntil(this.destroy$))
|
|
2275
2326
|
.subscribe(({ json }) => {
|
|
2276
2327
|
const callback = 'commandCallback' in json ? json.commandCallback : json;
|
|
2277
2328
|
const timeStampsRaw = 'timeStamp' in json ? json?.timeStamp : undefined;
|
|
@@ -2311,7 +2362,7 @@ class CommandTelemetryService {
|
|
|
2311
2362
|
* Starts tracking the time for the command.
|
|
2312
2363
|
* Finally, calls the provided function with the modified data object.
|
|
2313
2364
|
*
|
|
2314
|
-
* @param {
|
|
2365
|
+
* @param {ToBeDecorated} funcToDecorate - The function to be decorated with telemetry tracking.
|
|
2315
2366
|
* @param {MetaBoxCommandPacket} data - The data object to be passed to the function. It will be augmented with a unique tracking ID.
|
|
2316
2367
|
*/
|
|
2317
2368
|
wrapper(funcToDecorate, data) {
|
|
@@ -2547,14 +2598,15 @@ function OrchestrationErrorHandler(msg) {
|
|
|
2547
2598
|
const errorInfo = Object.values(ORCHESTRATION_ERROR_CODES).find(({ code }) => code === msg.payload.code);
|
|
2548
2599
|
this.store.dispatch(setOrchestrationMessage({ message: errorInfo?.description }));
|
|
2549
2600
|
this.orchestrationRetrySub?.unsubscribe();
|
|
2550
|
-
this.orchestrationRetrySub = timer(POLLING_TIME)
|
|
2601
|
+
this.orchestrationRetrySub = timer(POLLING_TIME)
|
|
2602
|
+
.pipe(takeUntil(this.destroy$))
|
|
2603
|
+
.subscribe(() => this.sendRequestStream());
|
|
2551
2604
|
}
|
|
2552
2605
|
}
|
|
2553
2606
|
|
|
2554
2607
|
class SignallingService extends SubService {
|
|
2555
2608
|
constructor() {
|
|
2556
2609
|
super();
|
|
2557
|
-
this.action$ = inject(Actions);
|
|
2558
2610
|
this.httpClient = inject(HttpClient);
|
|
2559
2611
|
this.regionsPingService = inject(RegionsPingService);
|
|
2560
2612
|
this.commandTelemetryService = inject(CommandTelemetryService);
|
|
@@ -2567,7 +2619,7 @@ class SignallingService extends SubService {
|
|
|
2567
2619
|
this.onConfig$ = new Subject();
|
|
2568
2620
|
this.onWebRtcIce$ = new Subject();
|
|
2569
2621
|
this.onWebRtcAnswer$ = new Subject();
|
|
2570
|
-
this.abort$ = this.
|
|
2622
|
+
this.abort$ = this.actions$.pipe(ofType(abortEstablishingConnection), tap(() => this.close({
|
|
2571
2623
|
code: 1000,
|
|
2572
2624
|
reason: DisconnectReason.AbortEstablishingConnection,
|
|
2573
2625
|
})));
|
|
@@ -2579,8 +2631,9 @@ class SignallingService extends SubService {
|
|
|
2579
2631
|
return;
|
|
2580
2632
|
}
|
|
2581
2633
|
this.setHandlersFromStream();
|
|
2582
|
-
this.
|
|
2583
|
-
|
|
2634
|
+
const stop$ = merge(this.abort$, this.destroy$);
|
|
2635
|
+
this.actions$
|
|
2636
|
+
.pipe(ofType(dataChannelReady), takeUntil(stop$))
|
|
2584
2637
|
.subscribe(() => {
|
|
2585
2638
|
this.send({
|
|
2586
2639
|
type: 'p2pEstablished',
|
|
@@ -2588,10 +2641,10 @@ class SignallingService extends SubService {
|
|
|
2588
2641
|
sessionId: generateUuid(),
|
|
2589
2642
|
});
|
|
2590
2643
|
});
|
|
2591
|
-
this.
|
|
2644
|
+
this.actions$
|
|
2592
2645
|
.pipe(ofType(dataChannelReady), switchMap(() => this.store
|
|
2593
2646
|
.select(unrealFeature.selectViewportReady)
|
|
2594
|
-
.pipe(filter(Truthy), take(1))), takeUntil(
|
|
2647
|
+
.pipe(filter(Truthy), take(1))), takeUntil(stop$))
|
|
2595
2648
|
.subscribe(() => {
|
|
2596
2649
|
this.send({
|
|
2597
2650
|
type: 'streamPreparedForUserInteraction',
|
|
@@ -2608,11 +2661,12 @@ class SignallingService extends SubService {
|
|
|
2608
2661
|
return EMPTY;
|
|
2609
2662
|
}
|
|
2610
2663
|
Logger.log('CONNECT TO SIGNALLING');
|
|
2611
|
-
// Preparation for WebSocket Orchestration
|
|
2612
|
-
this.correlationId = generateUuid();
|
|
2613
|
-
this.commandTelemetryService.reset();
|
|
2614
|
-
this.startEstablishingConnection();
|
|
2615
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();
|
|
2616
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$));
|
|
2617
2671
|
}).pipe(
|
|
2618
2672
|
// Retry the whole attempt on error, advancing to the next URL each time.
|
|
@@ -3633,7 +3687,12 @@ class FileReceiverService extends SubService {
|
|
|
3633
3687
|
this.reset();
|
|
3634
3688
|
}
|
|
3635
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 }); }
|
|
3636
3692
|
}
|
|
3693
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: FileReceiverService, decorators: [{
|
|
3694
|
+
type: Injectable
|
|
3695
|
+
}], ctorParameters: () => [] });
|
|
3637
3696
|
|
|
3638
3697
|
const filteredLogs = [
|
|
3639
3698
|
{
|
|
@@ -3641,9 +3700,6 @@ const filteredLogs = [
|
|
|
3641
3700
|
},
|
|
3642
3701
|
];
|
|
3643
3702
|
class AggregatorService extends SubService {
|
|
3644
|
-
/**
|
|
3645
|
-
* Never called for now
|
|
3646
|
-
*/
|
|
3647
3703
|
constructor() {
|
|
3648
3704
|
super();
|
|
3649
3705
|
this.selectStreamConfig = this.store.selectSignal(selectStreamConfig);
|
|
@@ -3682,11 +3738,9 @@ class AggregatorService extends SubService {
|
|
|
3682
3738
|
.pipe(filter(Truthy));
|
|
3683
3739
|
merge(unlockViewport$, this.disconnect$)
|
|
3684
3740
|
.pipe(takeUntil(this.destroy$))
|
|
3685
|
-
.subscribe(() =>
|
|
3686
|
-
this.removeLoadScreen();
|
|
3687
|
-
});
|
|
3741
|
+
.subscribe(() => this.removeLoadScreen());
|
|
3688
3742
|
if (!this.selectStreamConfig()?.autoStart ||
|
|
3689
|
-
location.
|
|
3743
|
+
location.protocol === 'http:') {
|
|
3690
3744
|
this.removeLoadScreen();
|
|
3691
3745
|
}
|
|
3692
3746
|
InputOptions.controlScheme = ControlScheme.HoveringMouse;
|
|
@@ -3933,14 +3987,27 @@ class UnrealCommunicatorService {
|
|
|
3933
3987
|
this.commandTelemetryService = inject(CommandTelemetryService);
|
|
3934
3988
|
this.webRtcPlayerService = inject(WebRtcPlayerService);
|
|
3935
3989
|
this.unrealConfig = inject(UNREAL_CONFIG, { optional: true });
|
|
3990
|
+
this.destroyRef = inject(DestroyRef);
|
|
3936
3991
|
this.destroy$ = new Subject();
|
|
3937
3992
|
this.cirrusConnected = this.store.selectSignal(unrealFeature.selectCirrusConnected);
|
|
3938
3993
|
this.afkIgnoredCommands = new Set(['executeConsoleCommand']);
|
|
3939
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()));
|
|
3940
4005
|
this.init();
|
|
3941
4006
|
}
|
|
3942
4007
|
init() {
|
|
3943
|
-
this.isPlaywrightMode()
|
|
4008
|
+
if (this.isPlaywrightMode()) {
|
|
4009
|
+
return;
|
|
4010
|
+
}
|
|
3944
4011
|
this.emitUIInteraction = this.commandTelemetryService.decorate(this.emitUIInteraction.bind(this));
|
|
3945
4012
|
// Listen to the dataChannelConnected ACTION (point-in-time event) instead
|
|
3946
4013
|
// of the store selector. The selector reads the current state and can fire on
|
|
@@ -3960,7 +4027,9 @@ class UnrealCommunicatorService {
|
|
|
3960
4027
|
this.destroy$ = new Subject();
|
|
3961
4028
|
}
|
|
3962
4029
|
sendCommandToUnreal(data) {
|
|
3963
|
-
this.isPlaywrightMode()
|
|
4030
|
+
if (this.isPlaywrightMode()) {
|
|
4031
|
+
return;
|
|
4032
|
+
}
|
|
3964
4033
|
if (this.webRtcPlayerService.canSend()) {
|
|
3965
4034
|
const uniqId = data?.correlationId ?? generateUuid();
|
|
3966
4035
|
const sendData = { ...data, correlationId: uniqId };
|
|
@@ -3970,7 +4039,11 @@ class UnrealCommunicatorService {
|
|
|
3970
4039
|
}
|
|
3971
4040
|
this.emitUIInteraction(sendData);
|
|
3972
4041
|
this.skipAfkReset = false;
|
|
3973
|
-
this.store.dispatch(commandStarted({
|
|
4042
|
+
this.store.dispatch(commandStarted({
|
|
4043
|
+
id: uniqId,
|
|
4044
|
+
command: data.command,
|
|
4045
|
+
timeStamp: Date.now(),
|
|
4046
|
+
}));
|
|
3974
4047
|
}
|
|
3975
4048
|
}
|
|
3976
4049
|
emitUIInteraction(descriptor) {
|
|
@@ -3991,10 +4064,12 @@ class UnrealCommunicatorService {
|
|
|
3991
4064
|
this.emitDescriptor(MessageType.Command, descriptor);
|
|
3992
4065
|
}
|
|
3993
4066
|
sendInputData(data) {
|
|
3994
|
-
this.isPlaywrightMode()
|
|
4067
|
+
if (this.isPlaywrightMode()) {
|
|
4068
|
+
return;
|
|
4069
|
+
}
|
|
3995
4070
|
try {
|
|
3996
4071
|
if (!this.skipAfkReset) {
|
|
3997
|
-
this.
|
|
4072
|
+
this.resetAfk$.next();
|
|
3998
4073
|
}
|
|
3999
4074
|
this.webRtcPlayerService.send(data);
|
|
4000
4075
|
}
|
|
@@ -4005,9 +4080,7 @@ class UnrealCommunicatorService {
|
|
|
4005
4080
|
}
|
|
4006
4081
|
}
|
|
4007
4082
|
isPlaywrightMode() {
|
|
4008
|
-
|
|
4009
|
-
return;
|
|
4010
|
-
}
|
|
4083
|
+
return !!this.unrealConfig?.playwright;
|
|
4011
4084
|
}
|
|
4012
4085
|
emitDescriptor(messageType, descriptor) {
|
|
4013
4086
|
// Convert the descriptor object into a JSON string.
|
|
@@ -4093,7 +4166,7 @@ class InputService extends SubService {
|
|
|
4093
4166
|
}
|
|
4094
4167
|
const visibilityHiddenTrigger$ = fromEvent(document, 'visibilitychange').pipe(map$1(() => document.visibilityState === 'hidden'), filter(Truthy));
|
|
4095
4168
|
const visibilityVisibleTrigger$ = fromEvent(document, 'visibilitychange').pipe(map$1(() => document.visibilityState === 'visible'), filter(Truthy), startWith(true));
|
|
4096
|
-
this.deactivatedVideoTrigger$ = merge(this.disconnect$, this.reInit$, visibilityHiddenTrigger$);
|
|
4169
|
+
this.deactivatedVideoTrigger$ = merge(this.disconnect$, this.reInit$, visibilityHiddenTrigger$, this.destroy$);
|
|
4097
4170
|
merge(visibilityVisibleTrigger$, this.store.select(unrealFeature.selectViewportReady).pipe(filter(Truthy)))
|
|
4098
4171
|
.pipe(filter(() => !!this.video), takeUntil$1(this.destroy$))
|
|
4099
4172
|
.subscribe(() => this.setup());
|
|
@@ -4526,7 +4599,12 @@ class InputService extends SubService {
|
|
|
4526
4599
|
this.sendInputData(data.buffer);
|
|
4527
4600
|
});
|
|
4528
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 }); }
|
|
4529
4604
|
}
|
|
4605
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: InputService, decorators: [{
|
|
4606
|
+
type: Injectable
|
|
4607
|
+
}], ctorParameters: () => [] });
|
|
4530
4608
|
|
|
4531
4609
|
class RegionsPingService {
|
|
4532
4610
|
constructor() {
|
|
@@ -4545,56 +4623,50 @@ class RegionsPingService {
|
|
|
4545
4623
|
// flow (start / providers-fetched / resolved). The final
|
|
4546
4624
|
// `regionResolved` forwards the winner's full ping-sample array
|
|
4547
4625
|
// straight from `PingResult.result` — no aggregation.
|
|
4548
|
-
|
|
4549
|
-
|
|
4550
|
-
|
|
4551
|
-
|
|
4552
|
-
|
|
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
|
+
});
|
|
4553
4633
|
}
|
|
4554
4634
|
getPingResult(providers) {
|
|
4555
|
-
const
|
|
4556
|
-
this.
|
|
4557
|
-
|
|
4558
|
-
|
|
4559
|
-
|
|
4560
|
-
|
|
4561
|
-
|
|
4562
|
-
|
|
4563
|
-
|
|
4564
|
-
|
|
4565
|
-
|
|
4566
|
-
|
|
4567
|
-
|
|
4568
|
-
|
|
4569
|
-
|
|
4570
|
-
|
|
4571
|
-
|
|
4572
|
-
|
|
4573
|
-
|
|
4574
|
-
}
|
|
4575
|
-
catch {
|
|
4576
|
-
pingResults.push({ region_code: undefined, result: [] });
|
|
4577
|
-
}
|
|
4578
|
-
finally {
|
|
4579
|
-
semaphore.release();
|
|
4580
|
-
}
|
|
4581
|
-
const okValues = pingResults.filter((pingResult) => pingResult.result.length > 1);
|
|
4582
|
-
if (okValues.length) {
|
|
4583
|
-
return okValues[0];
|
|
4584
|
-
}
|
|
4585
|
-
return okValues;
|
|
4586
|
-
});
|
|
4587
|
-
await Promise.all(tasks);
|
|
4588
|
-
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
|
+
}
|
|
4589
4654
|
});
|
|
4655
|
+
try {
|
|
4656
|
+
return await Promise.any(tasks);
|
|
4657
|
+
}
|
|
4658
|
+
catch {
|
|
4659
|
+
return null;
|
|
4660
|
+
}
|
|
4590
4661
|
}
|
|
4591
|
-
async checkLatency(endpoint, timeout
|
|
4662
|
+
async checkLatency(endpoint, timeout) {
|
|
4592
4663
|
const pingResults = [];
|
|
4664
|
+
const sep = endpoint.includes('?') ? '&' : '?';
|
|
4593
4665
|
for (let i = 0; i < this.config.ping_count; i++) {
|
|
4594
4666
|
const startTime = performance.now();
|
|
4595
4667
|
try {
|
|
4596
4668
|
await this.fetchWithTimeout(this.config.bust_cache
|
|
4597
|
-
? `${endpoint}
|
|
4669
|
+
? `${endpoint}${sep}cache_buster=${performance.now()}`
|
|
4598
4670
|
: endpoint, {
|
|
4599
4671
|
method: 'GET',
|
|
4600
4672
|
mode: 'no-cors',
|
|
@@ -4603,11 +4675,11 @@ class RegionsPingService {
|
|
|
4603
4675
|
Pragma: 'no-cache',
|
|
4604
4676
|
Expires: '0',
|
|
4605
4677
|
},
|
|
4606
|
-
credentials: 'omit',
|
|
4678
|
+
credentials: 'omit',
|
|
4607
4679
|
}, timeout);
|
|
4608
4680
|
}
|
|
4609
4681
|
catch {
|
|
4610
|
-
//
|
|
4682
|
+
// intentional: finally pushes the elapsed time as the latency sample
|
|
4611
4683
|
}
|
|
4612
4684
|
finally {
|
|
4613
4685
|
const latency = Math.round(performance.now() - startTime);
|
|
@@ -4637,10 +4709,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImpo
|
|
|
4637
4709
|
type: Injectable
|
|
4638
4710
|
}] });
|
|
4639
4711
|
|
|
4640
|
-
class StreamStatusTelemetryService {
|
|
4712
|
+
class StreamStatusTelemetryService extends SubService {
|
|
4641
4713
|
constructor() {
|
|
4642
|
-
|
|
4643
|
-
this.actions$ = inject(Actions);
|
|
4714
|
+
super();
|
|
4644
4715
|
this.videoService = inject(VideoService);
|
|
4645
4716
|
this.unrealConfig = inject(UNREAL_CONFIG, { optional: true });
|
|
4646
4717
|
this.init();
|
|
@@ -4670,8 +4741,8 @@ class StreamStatusTelemetryService {
|
|
|
4670
4741
|
signalingReadyTime,
|
|
4671
4742
|
viewPortReadyTime,
|
|
4672
4743
|
})));
|
|
4673
|
-
}))
|
|
4674
|
-
.subscribe((data) => this.
|
|
4744
|
+
}), takeUntil$1(this.destroy$))
|
|
4745
|
+
.subscribe((data) => this.store.dispatch(trackMixpanelEvent({ event: 'streamStatus', data })));
|
|
4675
4746
|
}
|
|
4676
4747
|
/**
|
|
4677
4748
|
* Emits `undefined` until `readyState$` first emits a truthy value,
|
|
@@ -4702,9 +4773,6 @@ class StreamStatusTelemetryService {
|
|
|
4702
4773
|
VideoEncoderQP,
|
|
4703
4774
|
};
|
|
4704
4775
|
}
|
|
4705
|
-
trackEventToMixPanel(data) {
|
|
4706
|
-
this.store.dispatch(trackMixpanelEvent({ event: 'streamStatus', data }));
|
|
4707
|
-
}
|
|
4708
4776
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: StreamStatusTelemetryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
4709
4777
|
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: StreamStatusTelemetryService }); }
|
|
4710
4778
|
}
|
|
@@ -4726,6 +4794,8 @@ class StreamStatusTelemetryService2 {
|
|
|
4726
4794
|
this.videoService = inject(VideoService);
|
|
4727
4795
|
this.signallingService = inject(SignallingService);
|
|
4728
4796
|
this.unrealConfig = inject(UNREAL_CONFIG, { optional: true });
|
|
4797
|
+
this.destroyRef = inject(DestroyRef);
|
|
4798
|
+
this.destroy$ = new Subject();
|
|
4729
4799
|
/**
|
|
4730
4800
|
* Per-browser anonymous user id. Read from `localStorage` on first access;
|
|
4731
4801
|
* if missing, a new UUID is generated and persisted. Included as
|
|
@@ -4941,6 +5011,13 @@ class StreamStatusTelemetryService2 {
|
|
|
4941
5011
|
],
|
|
4942
5012
|
},
|
|
4943
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
|
+
});
|
|
4944
5021
|
if (this.unrealConfig?.playwright ||
|
|
4945
5022
|
!this.unrealConfig?.streamTelemetryV2) {
|
|
4946
5023
|
return;
|
|
@@ -4965,16 +5042,18 @@ class StreamStatusTelemetryService2 {
|
|
|
4965
5042
|
init() {
|
|
4966
5043
|
// Start a new session on every initSignalling dispatch.
|
|
4967
5044
|
this.actions$
|
|
4968
|
-
.pipe(ofType(initSignalling))
|
|
5045
|
+
.pipe(ofType(initSignalling), takeUntil$1(this.destroy$))
|
|
4969
5046
|
.subscribe(() => this.startSession());
|
|
4970
5047
|
// End the current session when Cirrus drops (true → false transition).
|
|
4971
5048
|
this.store
|
|
4972
5049
|
.select(unrealFeature.selectCirrusConnected)
|
|
4973
|
-
.pipe(pairwise(), filter$1(([prev, curr]) => prev && !curr))
|
|
5050
|
+
.pipe(pairwise(), filter$1(([prev, curr]) => prev && !curr), takeUntil$1(this.destroy$))
|
|
4974
5051
|
.subscribe(() => this.endSession('disconnected'));
|
|
4975
5052
|
// Close the session when the tab is hidden / being evicted from cache.
|
|
4976
5053
|
// `pagehide` fires in more cases than `beforeunload` (covers iOS Safari, bfcache).
|
|
4977
|
-
fromEvent(window, 'pagehide')
|
|
5054
|
+
fromEvent(window, 'pagehide')
|
|
5055
|
+
.pipe(takeUntil$1(this.destroy$))
|
|
5056
|
+
.subscribe(() => this.endSession('pagehide'));
|
|
4978
5057
|
}
|
|
4979
5058
|
startSession() {
|
|
4980
5059
|
// If a previous session is still active, close it first.
|
|
@@ -5434,17 +5513,19 @@ class FileHandlerService {
|
|
|
5434
5513
|
observeFileResponse(mimetype, data, sender, timeOut = 60000) {
|
|
5435
5514
|
const correlationId = generateUuid();
|
|
5436
5515
|
const out = { ...data, correlationId };
|
|
5437
|
-
|
|
5438
|
-
|
|
5439
|
-
|
|
5440
|
-
|
|
5441
|
-
|
|
5442
|
-
|
|
5443
|
-
|
|
5444
|
-
|
|
5445
|
-
|
|
5446
|
-
|
|
5447
|
-
|
|
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
|
+
});
|
|
5448
5529
|
}
|
|
5449
5530
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: FileHandlerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
5450
5531
|
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: FileHandlerService }); }
|
|
@@ -5454,25 +5535,21 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImpo
|
|
|
5454
5535
|
}] });
|
|
5455
5536
|
|
|
5456
5537
|
class AnalyticsService extends SubService {
|
|
5457
|
-
/** Signals monitorMouse inner subscriptions to stop (viewport lost / new session). */
|
|
5458
5538
|
#stopMouse$ = new Subject();
|
|
5459
|
-
sendEventToMixPanel(name, params = {}) {
|
|
5460
|
-
Logger.log('Analytics Event:', name, params);
|
|
5461
|
-
this.store.dispatch(saveAnalyticsEvent({ event: { name, params } }));
|
|
5462
|
-
}
|
|
5463
5539
|
constructor() {
|
|
5464
5540
|
super();
|
|
5465
|
-
const abortSignal$ = this.store
|
|
5466
|
-
.select(unrealFeature.selectViewportReady)
|
|
5467
|
-
.pipe(filter$1(Falsy), map(() => void 0));
|
|
5468
5541
|
this.store
|
|
5469
5542
|
.select(unrealFeature.selectViewportReady)
|
|
5470
|
-
.pipe(distinctUntilChanged(),
|
|
5471
|
-
.subscribe(() =>
|
|
5543
|
+
.pipe(distinctUntilChanged(), takeUntil$1(this.destroy$))
|
|
5544
|
+
.subscribe((ready) => {
|
|
5545
|
+
this.#stopMouse$.next();
|
|
5546
|
+
if (ready) {
|
|
5547
|
+
this.monitorMouse();
|
|
5548
|
+
}
|
|
5549
|
+
});
|
|
5472
5550
|
}
|
|
5473
|
-
monitorMouse(
|
|
5474
|
-
this.#stopMouse$.
|
|
5475
|
-
const stop$ = merge(this.#stopMouse$.asObservable(), abortSignal$);
|
|
5551
|
+
monitorMouse() {
|
|
5552
|
+
const stop$ = this.#stopMouse$.asObservable();
|
|
5476
5553
|
const element = document.getElementById('streamingVideo');
|
|
5477
5554
|
if (!element) {
|
|
5478
5555
|
return;
|
|
@@ -5537,6 +5614,10 @@ class AnalyticsService extends SubService {
|
|
|
5537
5614
|
});
|
|
5538
5615
|
});
|
|
5539
5616
|
}
|
|
5617
|
+
sendEventToMixPanel(name, params = {}) {
|
|
5618
|
+
Logger.log('Analytics Event:', name, params);
|
|
5619
|
+
this.store.dispatch(saveAnalyticsEvent({ event: { name, params } }));
|
|
5620
|
+
}
|
|
5540
5621
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: AnalyticsService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
5541
5622
|
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: AnalyticsService }); }
|
|
5542
5623
|
}
|
|
@@ -5617,10 +5698,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImpo
|
|
|
5617
5698
|
* the existing `CommandsEffects.setMaxFps$` effect that sends the
|
|
5618
5699
|
* `t.MaxFPS <value>` console command to Unreal Engine.
|
|
5619
5700
|
*/
|
|
5620
|
-
class FpsMonitorService {
|
|
5701
|
+
class FpsMonitorService extends SubService {
|
|
5621
5702
|
constructor() {
|
|
5703
|
+
super();
|
|
5622
5704
|
this.videoService = inject(VideoService);
|
|
5623
|
-
this.store = inject(Store);
|
|
5624
5705
|
this.unrealConfig = inject(UNREAL_CONFIG, { optional: true });
|
|
5625
5706
|
/** Discrete FPS targets the controller can switch between (ascending order). Last 0 is an unlimited framerate indicator*/
|
|
5626
5707
|
this.FPS_STEPS = [30, 40, 50, 0];
|
|
@@ -5659,9 +5740,6 @@ class FpsMonitorService {
|
|
|
5659
5740
|
this.samples = [];
|
|
5660
5741
|
this.lastDecisionTime = Date.now();
|
|
5661
5742
|
this.lastUpgrade = 'hold';
|
|
5662
|
-
this.disconnect$ = this.store
|
|
5663
|
-
.select(unrealFeature.selectCirrusConnected)
|
|
5664
|
-
.pipe(skip(1), filter(Falsy));
|
|
5665
5743
|
this.init();
|
|
5666
5744
|
}
|
|
5667
5745
|
init() {
|
|
@@ -5671,17 +5749,19 @@ class FpsMonitorService {
|
|
|
5671
5749
|
// Reset mutable state and re-subscribe when a new session starts
|
|
5672
5750
|
this.store
|
|
5673
5751
|
.select(unrealFeature.selectViewportReady)
|
|
5674
|
-
.pipe(filter(Boolean))
|
|
5752
|
+
.pipe(filter(Boolean), takeUntil(this.destroy$))
|
|
5675
5753
|
.subscribe(() => this.startSession());
|
|
5676
5754
|
// Reset mutable state when session ends
|
|
5677
|
-
this.disconnect
|
|
5755
|
+
this.disconnect$
|
|
5756
|
+
.pipe(takeUntil(this.destroy$))
|
|
5757
|
+
.subscribe(() => this.resetSession());
|
|
5678
5758
|
}
|
|
5679
5759
|
startSession() {
|
|
5680
5760
|
// Tear down previous stats subscription if any
|
|
5681
5761
|
this.session$.next();
|
|
5682
5762
|
this.resetMutableState();
|
|
5683
5763
|
this.videoService.videoStats$
|
|
5684
|
-
.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$)))
|
|
5685
5765
|
.subscribe(([stats]) => this.processTick(stats));
|
|
5686
5766
|
}
|
|
5687
5767
|
resetSession() {
|
|
@@ -5967,14 +6047,16 @@ class UnrealCallbackService {
|
|
|
5967
6047
|
*/
|
|
5968
6048
|
observeCommandResponse(data, sender, timeOut = 60000, dispatchOnTimeout = true) {
|
|
5969
6049
|
const correlationId = generateUuid();
|
|
5970
|
-
|
|
5971
|
-
|
|
5972
|
-
|
|
5973
|
-
|
|
5974
|
-
|
|
5975
|
-
|
|
5976
|
-
|
|
5977
|
-
|
|
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
|
+
});
|
|
5978
6060
|
}
|
|
5979
6061
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: UnrealCallbackService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
5980
6062
|
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: UnrealCallbackService }); }
|
|
@@ -6290,7 +6372,7 @@ class ResumeStreamButtonComponent {
|
|
|
6290
6372
|
constructor() {
|
|
6291
6373
|
this.#store = inject(Store);
|
|
6292
6374
|
this.streamConfig = this.#store.selectSignal(selectStreamConfig);
|
|
6293
|
-
this.isSecondStart = signal(
|
|
6375
|
+
this.isSecondStart = signal(this.streamConfig()?.autoStart && location.protocol === 'https:', ...(ngDevMode ? [{ debugName: "isSecondStart" }] : /* istanbul ignore next */ []));
|
|
6294
6376
|
this.isLoaderScreenVisible = this.#store.selectSignal(selectIsLoaderScreenVisible);
|
|
6295
6377
|
this.showReconnectPopup = this.#store.selectSignal(selectShowReconnectPopup);
|
|
6296
6378
|
this.isExistMatchUrls = this.#store.selectSignal(selectIsExistMatchUrls);
|
|
@@ -6650,6 +6732,14 @@ class LowBandwidthDetectorComponent {
|
|
|
6650
6732
|
this.videoService = inject(VideoService);
|
|
6651
6733
|
this.isDevMode = inject(DevModeService).isDevMode;
|
|
6652
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.
|
|
6653
6743
|
this.isLowBandwidth$ = of(true);
|
|
6654
6744
|
this.lowBandwidthTrigger$ = this.isLowBandwidth$.pipe(distinctUntilChanged$1(), filter(Truthy));
|
|
6655
6745
|
this.highBandwidthTrigger$ = this.isLowBandwidth$.pipe(distinctUntilChanged$1(), filter(Falsy));
|