@3dsource/angular-unreal-module 0.0.113-dev.0 → 0.0.113-dev.2

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,17 +1,17 @@
1
1
  import * as i0 from '@angular/core';
2
2
  import { InjectionToken, inject, ChangeDetectionStrategy, Component, Injectable, DestroyRef, effect, untracked, signal, computed, Pipe, input, ElementRef, viewChild, afterNextRender, HostListener, output, makeEnvironmentProviders, provideEnvironmentInitializer } from '@angular/core';
3
3
  import { SourceButtonComponent, SourceLoadingComponent } from '@3dsource/source-ui-native';
4
- import { merge, map, scan, concatMap, of, animationFrameScheduler, interval, Observable, switchMap, timer, take, timeout, mergeMap, EMPTY, debounceTime, Subject, skip, share, from, fromEvent, throwError, defer, retry, tap as tap$1, startWith, takeUntil as takeUntil$1, throttleTime, asyncScheduler, filter as filter$1, withLatestFrom as withLatestFrom$1, pairwise, Subscription, bufferTime, BehaviorSubject, combineLatestWith, distinctUntilChanged as distinctUntilChanged$1, concat } from 'rxjs';
4
+ import { merge, map, scan, 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, NEVER, startWith, throttleTime, asyncScheduler, filter as filter$1, withLatestFrom as withLatestFrom$1, pairwise, 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, takeUntil, catchError, withLatestFrom, exhaustMap, switchMap as switchMap$1, timeout as timeout$1, debounceTime as debounceTime$1, take as take$1, startWith as startWith$1, delay, skip as skip$1 } from 'rxjs/operators';
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
9
  import { concatLatestFrom } from '@ngrx/operators';
10
10
  import { DialogRef, DIALOG_DATA, Dialog } from '@angular/cdk/dialog';
11
11
  import { HttpClient } from '@angular/common/http';
12
12
  import { ScrollStrategyOptions } from '@angular/cdk/overlay';
13
13
  import { MetaBoxCommand, EPayloadVersion } from '@3dsource/types-unreal';
14
- import { toSignal, takeUntilDestroyed, toObservable } from '@angular/core/rxjs-interop';
14
+ import { toSignal, toObservable, takeUntilDestroyed } from '@angular/core/rxjs-interop';
15
15
  import { v4 } from 'uuid';
16
16
  import { DomSanitizer } from '@angular/platform-browser';
17
17
  import { NgOptimizedImage, AsyncPipe, JsonPipe } from '@angular/common';
@@ -197,8 +197,11 @@ const DESTROY_DISCONNECT_REASONS = [
197
197
 
198
198
  const DEBOUNCE_TO_MANY_RESIZE_CALLS = 100;
199
199
  const SAME_SIZE_THRESHOLD = 1.01;
200
+ // Minimum container dimension in px — prevents division by zero in resize calculations
201
+ const MIN_CONTAINER_SIZE = 2;
200
202
  const MINIMAL_FPS = 6;
201
203
  const STREAMING_VIDEO_ID = 'streamingVideo';
204
+ const STREAMING_AUDIO_ID = 'streamingAudio';
202
205
  const CONSOLE_COMMAND_ENABLE_MESSAGES = 'EnableAllScreenMessages';
203
206
  const CONSOLE_COMMAND_DISABLE_MESSAGES = 'DisableAllScreenMessages';
204
207
  const CONSOLE_COMMAND_PIXEL_QUALITY = 'PixelStreaming.FreezeFrameQuality 95';
@@ -229,7 +232,7 @@ const commandStarted = createAction(scoped$1 `command started`, props());
229
232
  const commandCompleted = createAction(scoped$1 `command completed`, props());
230
233
 
231
234
  const scoped = (templateString) => `[UNREAL] ${templateString[0]}`;
232
- const trackMixpanelEvent = createAction(scoped `track mixpanel event`, props());
235
+ const trackMixpanelEvent = createAction(scoped `track Mixpanel event`, props());
233
236
  const trackTelemetryData = createAction(scoped `track telemetry data`, props());
234
237
  const setMaxFps = createAction(scoped `change fps`, props());
235
238
  const destroyRemoteConnections = createAction(scoped `destroyRemoteConnections`, props());
@@ -275,9 +278,9 @@ const instanceReady = createAction(scoped `instance ready`);
275
278
  const regionPingFetchStart = createAction(scoped `region ping fetch start`);
276
279
  /**
277
280
  * Dispatched once `RegionsPingService.getProviders()` returns the region
278
- * list — i.e. the fetch step is done and the actual per-region ping
281
+ * list — i.e., the fetch step is done and the actual per-region ping
279
282
  * sweep is about to start. Splits the region-discovery waterfall into
280
- * two legs: config fetch vs ping sweep. Observability-only.
283
+ * two legs: config fetch vs. ping sweep. Observability-only.
281
284
  */
282
285
  const regionPingFetchEnd = createAction(scoped `region ping fetch end`);
283
286
  /**
@@ -289,9 +292,9 @@ const regionPingFetchEnd = createAction(scoped `region ping fetch end`);
289
292
  */
290
293
  const regionResolved = createAction(scoped `region resolved`, props());
291
294
  /**
292
- * Dispatched around the HTTP polling roundtrip inside `getAwsInstance`
295
+ * Dispatched around the HTTP polling roundup inside `getAwsInstance`
293
296
  * (old orchestration flow). Only fires when polling is actually used
294
- * (i.e. signaling URL is not a direct `ws://` / `wss://` endpoint).
297
+ * (i.e., signaling URL is not a direct `ws://` / `wss://` endpoint).
295
298
  * Observability-only — no state change.
296
299
  */
297
300
  const pollingStarted = createAction(scoped `polling started`);
@@ -310,11 +313,7 @@ const setAfkTimerVisible = createAction(scoped `set afk timer visible`);
310
313
  const setAfkTimerHide = createAction(scoped `set afk timer hide`);
311
314
  const setAfkTimerCountdown = createAction(scoped `set afk timer count down`, props());
312
315
  const showUnrealErrorMessage = createAction(scoped `show unreal error message`, props());
313
- const initSignalling = createAction(scoped `init signalling`, (data = {
314
- resetDisconnectionReason: true,
315
- }) => ({
316
- resetDisconnectionReason: data.resetDisconnectionReason,
317
- }));
316
+ const initSignalling = createAction(scoped `init signalling`, (resetDisconnectionReason = true) => ({ resetDisconnectionReason }));
318
317
  const startStream = createAction(scoped `startStream`, props());
319
318
  const resetConfig = createAction(scoped `reset config`);
320
319
  const resetAfk = createAction(scoped `reset afk action`);
@@ -339,8 +338,9 @@ const dropDC = createAction(scoped `drop data channel`);
339
338
  const dropPC = createAction(scoped `drop peer connection`);
340
339
  const showLoadingProgress = createAction(scoped `show loading progress`, props());
341
340
  const showResumeStreamPopup = createAction(scoped `show resume stream popup`, props());
342
- const showAfkTimeoutPopup = createAction(scoped `show afk timeout popup`, props());
343
- const setAfkTimeout = createAction(scoped `set afk timeout`, props());
341
+ const showUserInactivityTimeoutPopup = createAction(scoped `show user inactivity timeout popup`, props());
342
+ const setUserInactivityTimeout = createAction(scoped `set user inactivity timeout`, props());
343
+ const resetUserInactivityTimeout = createAction(scoped `reset user inactivity timeout`);
344
344
 
345
345
  const initialState$1 = {
346
346
  streamRequestContext: null,
@@ -372,7 +372,7 @@ const initialState$1 = {
372
372
  autoStart: true,
373
373
  warnTimeout: DEFAULT_AFK_TIMEOUT,
374
374
  },
375
- afkTimeout: DEFAULT_AFK_TIMEOUT,
375
+ userInactivityTimeout: DEFAULT_AFK_TIMEOUT,
376
376
  matchUrls: [],
377
377
  streamClientCompanyId: '',
378
378
  streamViewId: 'default',
@@ -385,9 +385,18 @@ const initialState$1 = {
385
385
  showAfkTimeoutPopup: true,
386
386
  };
387
387
  const unrealReducer = createReducer(initialState$1, on(changeStatusMainVideoOnScene, (state, { isVideoPlaying }) => {
388
- return { ...state, isVideoPlaying: isVideoPlaying };
388
+ return { ...state, isVideoPlaying };
389
389
  }), on(setAwsInstance, (state, { instanceName, wsUrl, pollingUrl }) => {
390
- return { ...state, awsInstance: { instanceName, wsUrl, pollingUrl } };
390
+ return {
391
+ ...state,
392
+ awsInstance: {
393
+ ...state.awsInstance,
394
+ instanceName,
395
+ wsUrl,
396
+ pollingUrl,
397
+ progressComplete: 0,
398
+ },
399
+ };
391
400
  }), on(setViewportReady, (state) => {
392
401
  return {
393
402
  ...state,
@@ -414,15 +423,11 @@ const unrealReducer = createReducer(initialState$1, on(changeStatusMainVideoOnSc
414
423
  },
415
424
  };
416
425
  }), on(disconnectStream, (state, errorMessage) => {
417
- if (state.dataChannelConnected ||
418
- state.disconnectReason === DisconnectReason.Destroy) {
426
+ const alreadyHandled = state.dataChannelConnected ||
427
+ state.disconnectReason === DisconnectReason.Destroy;
428
+ if (alreadyHandled)
419
429
  return state;
420
- }
421
- return {
422
- ...state,
423
- errorMessage,
424
- statusMessage: null,
425
- };
430
+ return { ...state, errorMessage, statusMessage: null };
426
431
  }), on(setFreezeFrameFromVideo, (state, freezeFrameFromVideo) => {
427
432
  return {
428
433
  ...state,
@@ -449,14 +454,17 @@ const unrealReducer = createReducer(initialState$1, on(changeStatusMainVideoOnSc
449
454
  }), on(resetWarnTimeout, (state) => {
450
455
  return {
451
456
  ...state,
452
- streamConfig: { ...state.streamConfig, warnTimeout: state.afkTimeout },
457
+ streamConfig: {
458
+ ...state.streamConfig,
459
+ warnTimeout: state.userInactivityTimeout,
460
+ },
453
461
  };
454
462
  }), on(resetConfig, (state) => {
455
463
  return {
456
464
  ...state,
457
465
  streamConfig: {
458
466
  ...initialState$1.streamConfig,
459
- warnTimeout: state.afkTimeout,
467
+ warnTimeout: state.userInactivityTimeout,
460
468
  },
461
469
  };
462
470
  }), on(initSignalling, (state, { resetDisconnectionReason = true }) => {
@@ -537,12 +545,12 @@ const unrealReducer = createReducer(initialState$1, on(changeStatusMainVideoOnSc
537
545
  return { ...state, showLoadingProgress: visible };
538
546
  }), on(showResumeStreamPopup, (state, { visible }) => {
539
547
  return { ...state, showResumeStreamPopup: visible };
540
- }), on(showAfkTimeoutPopup, (state, { visible }) => {
548
+ }), on(showUserInactivityTimeoutPopup, (state, { visible }) => {
541
549
  return { ...state, showAfkTimeoutPopup: visible };
542
- }), on(setAfkTimeout, (state, { timeout }) => {
550
+ }), on(setUserInactivityTimeout, (state, { timeout }) => {
543
551
  return {
544
552
  ...state,
545
- afkTimeout: timeout,
553
+ userInactivityTimeout: timeout,
546
554
  streamConfig: { ...state.streamConfig, warnTimeout: timeout },
547
555
  };
548
556
  }), on(setCirrusDisconnected, (state) => {
@@ -560,7 +568,7 @@ const unrealReducer = createReducer(initialState$1, on(changeStatusMainVideoOnSc
560
568
  environmentId: state.environmentId,
561
569
  streamRequestContext: state.streamRequestContext,
562
570
  streamConfig: state.streamConfig,
563
- afkTimeout: state.afkTimeout,
571
+ userInactivityTimeout: state.userInactivityTimeout,
564
572
  };
565
573
  }), on(dropConnection, (state) => {
566
574
  return { ...state, establishingConnection: false };
@@ -1079,63 +1087,6 @@ function getResizeValuesUniversal(params) {
1079
1087
  return { w: makeEven(w), h: makeEven(h), x: 0, y: 0, scale: w / maxSpace.w };
1080
1088
  }
1081
1089
 
1082
- //TODO: Remove this legacy method after confirming that `getResizeValuesUniversal`
1083
- // works well in all scenarios and updating any dependent code to use it instead.
1084
- function getResizeValuesUniversalOld(params) {
1085
- const { pixelRatio, containerWidth, containerHeight, maxResolution } = params;
1086
- const maxSpace = {
1087
- w: containerWidth * pixelRatio,
1088
- h: containerHeight * pixelRatio,
1089
- };
1090
- const isVertical = containerWidth < containerHeight;
1091
- let maxH = FULL_HD_HEIGHT;
1092
- let maxW = FULL_HD_WIDTH;
1093
- if (maxResolution) {
1094
- maxH = FULL_HD_HEIGHT * 2;
1095
- maxW = FULL_HD_WIDTH * 2;
1096
- }
1097
- const maxLimits = isVertical
1098
- ? { w: maxH, h: maxW, x: 0, y: 0 }
1099
- : { w: maxW, h: maxH, x: 0, y: 0 };
1100
- const props = fitIntoRectangle({ x: 0, y: 0, w: containerWidth, h: containerHeight }, { x: 0, y: 0, ...maxSpace });
1101
- let out = { ...props };
1102
- const fitIntoMax = fitIntoRectangle(props, maxLimits);
1103
- if (fitIntoMax.scale < 1) {
1104
- // If reducing size to 1920x1080 is required, do it
1105
- out = fitIntoMax;
1106
- }
1107
- else {
1108
- // Otherwise, use the lowest resolution because the layout size is smaller than 1920x1080
1109
- out.scale = 1;
1110
- }
1111
- /**
1112
- * Ensure that the size is always even.
1113
- */
1114
- out.w = makeEven(out.w);
1115
- out.h = makeEven(out.h);
1116
- return { ...out };
1117
- }
1118
- function getResizeStudioValuesOld(params) {
1119
- const { pixelRatio, containerWidth, containerHeight, studioWidth, studioHeight, } = params;
1120
- const maxSpace = {
1121
- w: containerWidth * pixelRatio,
1122
- h: containerHeight * pixelRatio,
1123
- };
1124
- const videoSizePixelPerfect = fitIntoRectangle({ x: 0, y: 0, w: studioWidth, h: studioHeight }, { x: 0, y: 0, ...maxSpace });
1125
- const out = fitIntoRectangle(videoSizePixelPerfect, {
1126
- x: 0,
1127
- y: 0,
1128
- w: Math.min(maxSpace.w, FULL_HD_WIDTH),
1129
- h: Math.min(maxSpace.h, FULL_HD_HEIGHT),
1130
- });
1131
- /**
1132
- * Ensure that the size is always even.
1133
- */
1134
- out.w = makeEven(out.w);
1135
- out.h = makeEven(out.h);
1136
- return out;
1137
- }
1138
-
1139
1090
  const removeExileCommands = (state, idToRemove) => {
1140
1091
  const exileTimeout = 10000;
1141
1092
  const out = { ...state };
@@ -1143,11 +1094,11 @@ const removeExileCommands = (state, idToRemove) => {
1143
1094
  out.totalCommandsCompleted++;
1144
1095
  const time = new Date().getTime();
1145
1096
  const markForDelete = [];
1146
- Object.entries(out.commandsInProgress).forEach(([, { id, timeStamp }]) => {
1097
+ for (const { id, timeStamp } of out.commandsInProgress) {
1147
1098
  if (time - timeStamp > exileTimeout) {
1148
1099
  markForDelete.push(id);
1149
1100
  }
1150
- });
1101
+ }
1151
1102
  out.totalCommandsCompleted += markForDelete.length;
1152
1103
  out.commandsInProgress = out.commandsInProgress.filter((command) => !markForDelete.includes(command.id));
1153
1104
  return out;
@@ -1180,7 +1131,6 @@ const commandsFeature = createFeature({
1180
1131
  reducer: commandsReducer,
1181
1132
  });
1182
1133
 
1183
- const selectCommandsInProgress = createSelector(commandsFeature.selectCommandsInProgress, (commandsInProgress) => commandsInProgress?.length ?? 0);
1184
1134
  const selectCommandProgress = createSelector(commandsFeature.selectTotalCommandsStarted, commandsFeature.selectTotalCommandsCompleted, (totalCommandsStarted, totalCommandsCompleted) => totalCommandsStarted ? totalCommandsCompleted / totalCommandsStarted : 0);
1185
1135
 
1186
1136
  const selectFreezeFrameDataUrlFromVideo = createSelector(unrealFeature.selectFreezeFrameFromVideo, (freezeFrameFromVideo) => freezeFrameFromVideo?.dataUrl || null);
@@ -1194,9 +1144,8 @@ const selectStreamConfig = createSelector(unrealFeature.selectStreamConfig, (con
1194
1144
  const selectWarnTimeout = createSelector(selectStreamConfig, (config) => config?.warnTimeout || DEFAULT_AFK_TIMEOUT);
1195
1145
  const selectIsExistMatchUrls = createSelector(unrealFeature.selectMatchUrls, (urls) => urls?.length > 0);
1196
1146
  const selectIsAutostart = createSelector(selectStreamConfig, (config) => config?.autoStart);
1197
- const selectShowReconnectPopup = createSelector(unrealFeature.selectCirrusConnected, unrealFeature.selectEstablishingConnection, (connected, establishing) => !connected && !establishing);
1198
- const selectShowLoader = createSelector(selectShowReconnectPopup, unrealFeature.selectViewportReady, unrealFeature.selectShowLoadingProgress, (showPopup, viewportReady, selectShowLoadingProgress) => !showPopup && !viewportReady && selectShowLoadingProgress);
1199
- const selectIsVideoPlayingAndDataChannelConnected = createSelector(unrealFeature.selectIsVideoPlaying, unrealFeature.selectDataChannelConnected, (isVideoPlaying, isDataChannelConnected) => isVideoPlaying && isDataChannelConnected);
1147
+ const selectShowReconnectPopup = createSelector(unrealFeature.selectCirrusConnected, unrealFeature.selectEstablishingConnection, (cirrusConnected, establishingConnection) => !cirrusConnected && !establishingConnection);
1148
+ const selectShowLoader = createSelector(selectShowReconnectPopup, unrealFeature.selectViewportReady, (showPopup, viewportReady) => !showPopup && !viewportReady);
1200
1149
  const selectSignalingParameters = createSelector(unrealFeature.selectSsData, unrealFeature.selectAwsInstance, (data, instance) => {
1201
1150
  const info = [];
1202
1151
  if (data) {
@@ -1209,29 +1158,37 @@ const selectSignalingParameters = createSelector(unrealFeature.selectSsData, unr
1209
1158
  return info.join('<br>');
1210
1159
  });
1211
1160
  const selectClientAndViewIds = createSelector(unrealFeature.selectStreamClientCompanyId, unrealFeature.selectStreamViewId, (clientId, viewId) => ({ clientId, viewId }));
1212
- const selectIsLoaderScreenVisible = createSelector(unrealFeature.selectViewportReady, unrealFeature.selectDataChannelConnected, unrealFeature.selectCirrusConnected, (viewReady, cirrusConnected, dataChannelConnected) => !viewReady || !cirrusConnected || !dataChannelConnected);
1161
+ const selectIsFullyConnected = createSelector(unrealFeature.selectViewportReady, unrealFeature.selectDataChannelConnected, unrealFeature.selectCirrusConnected, (viewportReady, dataChannelConnected, cirrusConnected) => viewportReady && dataChannelConnected && cirrusConnected);
1162
+ const selectIsLoaderScreenVisible = createSelector(selectIsFullyConnected, (fullyConnected) => !fullyConnected);
1213
1163
  const selectCirrusConnectionId = createSelector(unrealFeature.selectSsData, (data) => data?.connectionId);
1214
1164
  /**
1215
- * Returns Scene load progress as a float value in between 0-1
1165
+ * Returns Scene load progress as a float value between 0 and 1.
1166
+ *
1167
+ * Progress is split into two phases:
1168
+ * 1. **Signaling** (0 → ~56%): connection polling or orchestration progress.
1169
+ * Starts at 0.1%, so the bar is never visually empty while connecting.
1170
+ * 2. **Commands** (~56% → 100%): Unreal command progress after the data channel connects.
1171
+ *
1172
+ * The split point bumps by +1% once the WebSocket URL is known,
1173
+ * producing a small visual step that indicates the WS handshake succeeded.
1216
1174
  */
1217
1175
  const selectTotalProgress = createSelector(unrealFeature.selectStatusPercentSignallingServer, selectCommandProgress, unrealFeature.selectAwsInstance, unrealFeature.selectDataChannelConnected, (statusPercentSignallingServer, commandProgress, awsInstance, dataChannelConnected) => {
1218
- // TODO make better mechanism to detect is new orchestration
1219
- const isNewOrchestration = String(awsInstance?.wsUrl).match(/^wss:\/\/orchestration-websocket/i);
1220
- const newOrchestrationProgressComplete = dataChannelConnected
1221
- ? 100
1222
- : (awsInstance?.progressComplete || 0) * 100;
1223
- /**
1224
- * Set 57% {@link SIGNALLING_PERCENT_VALUE} immediately when signalingServer?.signalingUrl is available
1225
- * Up to 56% {@link SIGNALLING_PERCENT_VALUE} - 1 of the progress is reserved for signaling server polling
1226
- */
1227
- const signalingPercentValue = isNewOrchestration
1228
- ? newOrchestrationProgressComplete
1229
- : statusPercentSignallingServer;
1230
1176
  const SIGNALLING_MAX = SIGNALLING_PERCENT_VALUE / 100;
1177
+ const ORCHESTRATION_WS_PATTERN = /^wss:\/\/orchestration-websocket/i;
1178
+ const isNewOrchestration = ORCHESTRATION_WS_PATTERN.test(awsInstance?.wsUrl ?? '');
1179
+ // Orchestration progress is 0–1 from the backend; map to 0–1 directly.
1180
+ // Once the data channel is connected, signaling is fully complete (1.0).
1181
+ const signalingProgress = isNewOrchestration
1182
+ ? dataChannelConnected
1183
+ ? 1
1184
+ : (awsInstance?.progressComplete ?? 0)
1185
+ : (statusPercentSignallingServer ?? 0) / 100;
1186
+ // +1% bump once WS URL is known (visual step for WS handshake)
1231
1187
  const splitPoint = awsInstance?.wsUrl
1232
1188
  ? SIGNALLING_MAX + 0.01
1233
1189
  : SIGNALLING_MAX;
1234
- const progressSignaling = lerp(0.001, splitPoint, (signalingPercentValue ?? 0) / 100);
1190
+ // 0.001 minimum so the bar is never completely empty while loading
1191
+ const progressSignaling = lerp(0.001, splitPoint, signalingProgress);
1235
1192
  const progressRest = lerp(0, 1 - splitPoint, commandProgress);
1236
1193
  return clampf(0, 1, progressSignaling + progressRest);
1237
1194
  });
@@ -1284,37 +1241,12 @@ class UnrealEffects {
1284
1241
  onDataChannelClose: this.unrealInitialConfig?.reconnect?.onDataChannelClose ??
1285
1242
  DEFAULT_RECONNECT_ON_DATACHANNEL_CLOSE,
1286
1243
  };
1287
- this.disconnectReasonHandling$ = createEffect(() => {
1288
- return this.actions$.pipe(ofType(disconnectStream), filter(({ reason }) => {
1289
- // Skip DataChannelClosed if peer reconnection is enabled and signaling is connected
1290
- // The peer reconnection effect will handle this case
1291
- if (reason === DisconnectReason.DataChannelClosed &&
1292
- this.reconnectConfig.enabled &&
1293
- this.reconnectConfig.onDataChannelClose &&
1294
- this.signallingService.isConnected()) {
1295
- Logger.info('[Reconnect] Skipping destroy for DataChannelClosed - peer reconnection will handle');
1296
- return false;
1297
- }
1298
- return DESTROY_DISCONNECT_REASONS.includes(reason);
1299
- }), tap(({ reason, message }) => {
1300
- Logger.log(`Disconnect Call=> reason:'${reason}', message:'${message}'`);
1301
- }), map(({ reason }) => destroyRemoteConnections({ reason })));
1302
- });
1303
1244
  this.destroyConnections$ = createEffect(() => {
1304
1245
  return this.actions$.pipe(ofType(destroyUnrealScene), tap(() => {
1305
1246
  this.commandsSender.destroy();
1306
1247
  this.videoService.setContainer(null);
1307
1248
  }), map(() => destroyRemoteConnections({ reason: DisconnectReason.Destroy })));
1308
1249
  });
1309
- this.destroyRemoteConnections$ = createEffect(() => {
1310
- return this.actions$.pipe(ofType(destroyRemoteConnections), tap(({ reason }) => {
1311
- this.signallingService.close({
1312
- code: WSCloseCode_FORCE_CIRRUS_CLOSE,
1313
- reason,
1314
- });
1315
- this.webRtcPlayerService.closePC();
1316
- }), filter(({ reason }) => reason === DisconnectReason.DropConnection), map(() => dropConnection()));
1317
- });
1318
1250
  this.changeStreamResolution$ = createEffect(() => {
1319
1251
  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 &&
1320
1252
  newSizes.height === savedSizes.height)), map(([sizes]) => changeStreamResolutionSuccessAction({
@@ -1323,11 +1255,25 @@ class UnrealEffects {
1323
1255
  })));
1324
1256
  });
1325
1257
  this.resetAfk$ = createEffect(() => {
1326
- return this.actions$.pipe(ofType(setConfig, setAfkTimeout, resetWarnTimeout, resetConfig), map(() => resetAfk()));
1258
+ return this.actions$.pipe(ofType(setConfig, setUserInactivityTimeout, resetWarnTimeout, resetConfig), map(() => resetAfk()));
1327
1259
  });
1328
1260
  this.abortEstablishingConnection$ = createEffect(() => {
1329
- return merge(this.store.select(unrealFeature.selectViewportReady).pipe(filter(Truthy)), this.actions$.pipe(ofType(setCirrusDisconnected, destroyUnrealScene, dropConnection))).pipe(tapLog('ABORT'), map(() => abortEstablishingConnection()));
1261
+ return this.actions$.pipe(ofType(setCirrusDisconnected, dropConnection, destroyUnrealScene), map(() => abortEstablishingConnection()));
1330
1262
  });
1263
+ this.iframeMessaging$ = createEffect(() => {
1264
+ const cirrusConnected$ = this.store
1265
+ .select(unrealFeature.selectCirrusConnected)
1266
+ .pipe(skip(1));
1267
+ // On disconnect: immediately show 'Connection closed.', nullify after 2s
1268
+ const cirrusStatus$ = cirrusConnected$.pipe(switchMap((connected) => connected
1269
+ ? of('Creating Socket...')
1270
+ : concat(of('Connection closed.'), of(null).pipe(delay(2000)))));
1271
+ // Skip initial false; emit null on every viewport state change
1272
+ const viewportReset$ = this.store
1273
+ .select(unrealFeature.selectViewportReady)
1274
+ .pipe(skip(1), delay(500), map(() => null));
1275
+ return merge(cirrusStatus$, this.store.select(unrealFeature.selectStatusMessage), viewportReset$).pipe(debounceTime(0), distinctUntilChanged(), tapLog('STATUS=>'), tap((status) => this.postToScreenLocker(status)));
1276
+ }, { dispatch: false });
1331
1277
  this.setSignalingTo100$ = createEffect(() => {
1332
1278
  return this.actions$.pipe(ofType(setAwsInstance), map(() => setStatusPercentSignallingServer({ percent: 100 })));
1333
1279
  });
@@ -1377,8 +1323,35 @@ class UnrealEffects {
1377
1323
  }));
1378
1324
  }));
1379
1325
  });
1326
+ /**
1327
+ * Effect: `forceViewportNotReady$`
1328
+ *
1329
+ * Ensures the viewport is explicitly marked as *not ready* to prevent
1330
+ * a temporary black screen flash when disconnecting.
1331
+ *
1332
+ * ### Why
1333
+ * - The video stream is disconnected immediately.
1334
+ * - The DataChannel closes slightly later.
1335
+ * - Closing the DataChannel resets the Unreal scene,
1336
+ * which also sets `viewPortReady` to `false`.
1337
+ *
1338
+ * Without this effect, the order of these events may cause
1339
+ * a visible flicker (video flashing to black).
1340
+ */
1341
+ this.forceViewportNotReady$ = createEffect(() => {
1342
+ return this.actions$.pipe(ofType(destroyRemoteConnections), concatLatestFrom(() => this.store.select(unrealFeature.selectViewportReady)), filter(([, viewportReady]) => viewportReady), map(() => setViewportNotReady()));
1343
+ });
1380
1344
  this.resetCirrusOnDestroy$ = createEffect(() => {
1381
- return this.actions$.pipe(ofType(destroyRemoteConnections), map(() => setCirrusDisconnected()));
1345
+ return this.actions$.pipe(ofType(destroyRemoteConnections), concatLatestFrom(() => this.store.select(unrealFeature.selectCirrusConnected)), filter(([, cirrusConnected]) => cirrusConnected), map(() => setCirrusDisconnected()));
1346
+ });
1347
+ this.destroyRemoteConnections$ = createEffect(() => {
1348
+ return this.actions$.pipe(ofType(destroyRemoteConnections), tap(({ reason }) => {
1349
+ this.signallingService.close({
1350
+ code: WSCloseCode_FORCE_CIRRUS_CLOSE,
1351
+ reason,
1352
+ });
1353
+ this.webRtcPlayerService.closePC();
1354
+ }), filter(({ reason }) => reason === DisconnectReason.DropConnection), map(() => dropConnection()));
1382
1355
  });
1383
1356
  this.destroyConnectionAndRestart$ = createEffect(() => {
1384
1357
  return this.actions$.pipe(ofType(setCirrusDisconnected), concatLatestFrom(() => this.store.select(unrealFeature.selectDisconnectReason)), filter(([, reason]) => reason === DisconnectReason.DataChannelTimeout), map(() => initSignalling()));
@@ -1404,7 +1377,7 @@ class UnrealEffects {
1404
1377
  });
1405
1378
  this.commandTelemetryService.trackStartCommand('EXT-SetViewportReady');
1406
1379
  }), switchMap(() =>
1407
- // Wait for initial commands to be sent before sending loopback.
1380
+ // Wait for initial commands to be sent before sending the loopback.
1408
1381
  // commandStarted fires for each sent command; debounce ensures
1409
1382
  // we proceed only after no more commands are started for 100 ms.
1410
1383
  this.actions$.pipe(ofType(commandStarted), debounceTime(100), take(1), switchMap(() => this.unrealCallbackService.observeCommandResponse({ command: MetaBoxCommand.FLoopBackCommand }, (data) => this.commandsSender.sendCommandToUnreal(data))), switchMap(() => this.videoService.videoStats$), filter(({ aggregatedStats: s }) => !!s.framesPerSecond &&
@@ -1417,23 +1390,8 @@ class UnrealEffects {
1417
1390
  this.startStream$ = createEffect(() => {
1418
1391
  return this.actions$.pipe(ofType(startStream), map(() => initSignalling()));
1419
1392
  });
1420
- /**
1421
- * Effect: `forceViewportNotReady$`
1422
- *
1423
- * Ensures the viewport is explicitly marked as *not ready* to prevent
1424
- * a temporary black screen flash when disconnecting.
1425
- *
1426
- * ### Why
1427
- * - The video stream is disconnected immediately.
1428
- * - The DataChannel closes slightly later.
1429
- * - Closing the DataChannel resets the Unreal scene,
1430
- * which also sets `viewPortReady` to `false`.
1431
- *
1432
- * Without this effect, the order of these events may cause
1433
- * a visible flicker (video flashing to black).
1434
- */
1435
- this.forceViewportNotReady$ = createEffect(() => {
1436
- return this.actions$.pipe(ofType(destroyRemoteConnections), map(() => setViewportNotReady()));
1393
+ this.resetAfkOnVideoStart$ = createEffect(() => {
1394
+ return this.actions$.pipe(ofType(videoStreamInitialized), filter(() => !!this.videoService.video?.paused), tap(() => Logger.info('Simulate Press Play')), map(() => resetAfk()));
1437
1395
  });
1438
1396
  /**
1439
1397
  * Effect: `handleDataChannelClosed$`
@@ -1442,11 +1400,22 @@ class UnrealEffects {
1442
1400
  * Only triggers if reconnection is enabled, onDataChannelClose is true,
1443
1401
  * and the signaling WebSocket is still connected.
1444
1402
  */
1403
+ this.disconnectReasonHandling$ = createEffect(() => {
1404
+ return this.actions$.pipe(ofType(disconnectStream), filter(({ reason }) => {
1405
+ // Skip DataChannelClosed if peer reconnection will handle this case
1406
+ if (reason === DisconnectReason.DataChannelClosed &&
1407
+ this.canPeerReconnect()) {
1408
+ Logger.info('[Reconnect] Skipping destroy for DataChannelClosed - peer reconnection will handle');
1409
+ return false;
1410
+ }
1411
+ return DESTROY_DISCONNECT_REASONS.includes(reason);
1412
+ }), tap(({ reason, message }) => {
1413
+ Logger.log(`Disconnect Call=> reason:'${reason}', message:'${message}'`);
1414
+ }), map(({ reason }) => destroyRemoteConnections({ reason })));
1415
+ });
1445
1416
  this.handleDataChannelClosed$ = createEffect(() => {
1446
1417
  return this.actions$.pipe(ofType(disconnectStream), filter(({ reason }) => reason === DisconnectReason.DataChannelClosed &&
1447
- this.reconnectConfig.enabled &&
1448
- this.reconnectConfig.onDataChannelClose &&
1449
- this.signallingService.isConnected()), tap(() => {
1418
+ this.canPeerReconnect()), tap(() => {
1450
1419
  Logger.warn('[Reconnect] DataChannel closed, initiating peer reconnection');
1451
1420
  }), map(() => reconnectPeer({
1452
1421
  attempt: 1,
@@ -1454,6 +1423,11 @@ class UnrealEffects {
1454
1423
  delay: 0,
1455
1424
  })));
1456
1425
  });
1426
+ this.webrtcErrorModalComponent$ = createEffect(() => {
1427
+ return this.actions$.pipe(ofType(disconnectStream), filter(({ reason }) => reason === DisconnectReason.WebRTCError), debounceTime(400), switchMap(() => this.store
1428
+ .select(unrealFeature.selectDataChannelConnected)
1429
+ .pipe(take(1), filter(Falsy))), tap(() => this.dialog.open(WebrtcErrorModalComponent)));
1430
+ }, { dispatch: false });
1457
1431
  /**
1458
1432
  * Effect: `resetStateForPeerReconnect$`
1459
1433
  *
@@ -1525,11 +1499,6 @@ class UnrealEffects {
1525
1499
  });
1526
1500
  }));
1527
1501
  }, { dispatch: false });
1528
- this.webrtcErrorModalComponent$ = createEffect(() => {
1529
- return this.actions$.pipe(ofType(disconnectStream), filter(({ reason }) => reason === DisconnectReason.WebRTCError), debounceTime(400), switchMap(() => this.store
1530
- .select(unrealFeature.selectDataChannelConnected)
1531
- .pipe(take(1), filter(Falsy))), tap(() => this.dialog.open(WebrtcErrorModalComponent)));
1532
- }, { dispatch: false });
1533
1502
  this.sendStatisticsToUnreal$ = createEffect(() => {
1534
1503
  return this.actions$.pipe(ofType(showUnrealErrorMessage), filter(({ code }) => code === 404), withLatestFrom(this.store.select(unrealFeature.selectAwsInstance), this.store.select(unrealFeature.selectStreamClientCompanyId), this.store.select(unrealFeature.selectStreamViewId)), switchMap(([action, instance, companyId, viewId]) => this.http
1535
1504
  .post(this.unrealInitialConfig?.customErrorsEndpoint || '', {
@@ -1557,16 +1526,21 @@ class UnrealEffects {
1557
1526
  // Ensure a single in-flight request per attempt.
1558
1527
  exhaustMap((urls) => this.signallingService.connectToSignaling(urls)));
1559
1528
  }, { dispatch: false });
1560
- /**
1561
- * Effect: `logPeerReconnectSuccess$`
1562
- *
1563
- * Logs successful peer reconnection for telemetry.
1564
- */
1565
- this.logPeerReconnectSuccess$ = createEffect(() => {
1566
- return this.actions$.pipe(ofType(reconnectPeerSuccess), tap(() => {
1567
- Logger.info('[Reconnect] Peer connection restored successfully');
1568
- }));
1569
- }, { dispatch: false });
1529
+ }
1530
+ canPeerReconnect() {
1531
+ return (this.reconnectConfig.enabled &&
1532
+ this.reconnectConfig.onDataChannelClose &&
1533
+ this.signallingService.isConnected());
1534
+ }
1535
+ postToScreenLocker(text) {
1536
+ try {
1537
+ const el = document.getElementById(this.unrealInitialConfig?.screenLockerContainerId ??
1538
+ SCREEN_LOCKER_CONTAINER_ID);
1539
+ el?.contentWindow?.postMessage(text, '*');
1540
+ }
1541
+ catch {
1542
+ // Fails silently if cross-origin
1543
+ }
1570
1544
  }
1571
1545
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: UnrealEffects, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
1572
1546
  static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: UnrealEffects }); }
@@ -1601,7 +1575,7 @@ class SubService {
1601
1575
  this.disconnect$ = this.store
1602
1576
  .select(unrealFeature.selectCirrusConnected)
1603
1577
  .pipe(skip(1), filter(Falsy), share());
1604
- this._destroy = this.destroyRef.onDestroy(() => {
1578
+ this.destroyRef.onDestroy(() => {
1605
1579
  this.destroy$.next();
1606
1580
  this.destroy$.complete();
1607
1581
  });
@@ -1612,13 +1586,13 @@ class AFKService extends SubService {
1612
1586
  constructor() {
1613
1587
  super();
1614
1588
  this.active = false; // Whether the AFK system is currently looking for inactivity.
1615
- this.countdown = 0; // The inactivity warning overlay has a countdown to show time until disconnect.
1589
+ this.countdown = 0;
1616
1590
  this.warnTimeout = this.store.selectSignal(selectWarnTimeout);
1591
+ this.isAfkTimerVisible = this.store.selectSignal(unrealFeature.selectIsAfkTimerVisible);
1617
1592
  this.isViewportReady = this.store.selectSignal(unrealFeature.selectViewportReady);
1618
1593
  this.unrealConfig = inject(UNREAL_CONFIG, { optional: true });
1619
1594
  /** Ticks every time warnTimeout changes or resetAfk action fires */
1620
1595
  this.afkResetTrigger = toSignal(merge(this.store.select(selectWarnTimeout), this.actions$.pipe(ofType(resetAfk))));
1621
- /** Ticks when cirrus disconnects */
1622
1596
  this.disconnected = toSignal(this.disconnect$);
1623
1597
  this.destroyRef.onDestroy(() => this.clearTimers());
1624
1598
  if (this.unrealConfig?.playwright) {
@@ -1640,15 +1614,20 @@ class AFKService extends SubService {
1640
1614
  untracked(() => this.startAfkWarningTimer());
1641
1615
  }
1642
1616
  });
1643
- // Stop on disconnect
1644
1617
  effect(() => {
1645
1618
  const disconnected = this.disconnected();
1646
1619
  if (disconnected !== undefined) {
1647
1620
  untracked(() => this.stop());
1648
1621
  }
1649
1622
  });
1623
+ this.actions$
1624
+ .pipe(ofType(resetUserInactivityTimeout), takeUntil$1(this.destroy$))
1625
+ .subscribe(() => this.reset());
1650
1626
  }
1651
1627
  hideOverlay() {
1628
+ if (!this.isAfkTimerVisible()) {
1629
+ return;
1630
+ }
1652
1631
  this.store.dispatch(setAfkTimerHide());
1653
1632
  }
1654
1633
  /**
@@ -1724,11 +1703,28 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImpo
1724
1703
  type: Injectable
1725
1704
  }], ctorParameters: () => [] });
1726
1705
 
1706
+ class LoadingProgressService {
1707
+ constructor() {
1708
+ this.#store = inject(Store);
1709
+ this.loaderVisible = this.#store.selectSignal(selectShowLoader);
1710
+ this.percents = toSignal(this.#store
1711
+ .select(selectTotalProgress)
1712
+ .pipe(keepMaxUntilReset(toObservable(this.loaderVisible).pipe(filter(Falsy))), floatToSmoothPercents(), distinctUntilChanged()));
1713
+ }
1714
+ #store;
1715
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: LoadingProgressService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
1716
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: LoadingProgressService }); }
1717
+ }
1718
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: LoadingProgressService, decorators: [{
1719
+ type: Injectable
1720
+ }] });
1721
+
1727
1722
  class FreezeFrameService extends SubService {
1728
1723
  constructor() {
1729
1724
  super();
1730
1725
  this.receiving = false;
1731
1726
  this.size = 0;
1727
+ this.jpeg = undefined;
1732
1728
  this.freezeFrameOverlay = new Image();
1733
1729
  this.unrealConfig = inject(UNREAL_CONFIG, { optional: true });
1734
1730
  this.init();
@@ -1842,6 +1838,7 @@ const BITRATE_MONITOR = new DataFlowMonitor(FilterModel.yellowFlag, FilterModel.
1842
1838
  class VideoService extends SubService {
1843
1839
  constructor() {
1844
1840
  super(...arguments);
1841
+ this.isVideoPlaying = this.store.selectSignal(unrealFeature.selectIsVideoPlaying);
1845
1842
  this.video = null;
1846
1843
  this.audio = null;
1847
1844
  this.container = null;
@@ -1881,7 +1878,7 @@ class VideoService extends SubService {
1881
1878
  this.videoTrack$.next({ stream, pcClient });
1882
1879
  }
1883
1880
  else {
1884
- console.error('Video element is not defined.');
1881
+ Logger.error('Video element is not defined.');
1885
1882
  }
1886
1883
  }
1887
1884
  attachAudioStream(stream) {
@@ -1958,7 +1955,7 @@ class VideoService extends SubService {
1958
1955
  catch (err) {
1959
1956
  // Handle InvalidStateError or any transient WebRTC errors gracefully
1960
1957
  if (err instanceof DOMException && err.name === 'InvalidStateError') {
1961
- console.warn('Peer connection is closed or unusable, skipping stats.');
1958
+ Logger.warn('Peer connection is closed or unusable, skipping stats.');
1962
1959
  return null;
1963
1960
  }
1964
1961
  throw err;
@@ -2065,6 +2062,7 @@ class VideoService extends SubService {
2065
2062
  if (!this.video) {
2066
2063
  return;
2067
2064
  }
2065
+ this.changeStatusMainVideoOnScene(false);
2068
2066
  this.video = null;
2069
2067
  this.audio = null;
2070
2068
  this.videoTrack$.next(null);
@@ -2076,7 +2074,12 @@ class VideoService extends SubService {
2076
2074
  while (this.container?.firstChild) {
2077
2075
  this.container.removeChild(this.container.firstChild);
2078
2076
  }
2079
- this.store.dispatch(changeStatusMainVideoOnScene({ isVideoPlaying: false }));
2077
+ }
2078
+ changeStatusMainVideoOnScene(isVideoPlaying) {
2079
+ if (this.isVideoPlaying() === isVideoPlaying) {
2080
+ return;
2081
+ }
2082
+ this.store.dispatch(changeStatusMainVideoOnScene({ isVideoPlaying }));
2080
2083
  }
2081
2084
  createWebRtcVideo() {
2082
2085
  dispatchResize();
@@ -2089,18 +2092,14 @@ class VideoService extends SubService {
2089
2092
  video.disablePictureInPicture = true;
2090
2093
  this.video = video;
2091
2094
  this.container?.appendChild(this.video);
2092
- this.video.addEventListener('play', () => this.store.dispatch(changeStatusMainVideoOnScene({ isVideoPlaying: true })));
2095
+ this.video.addEventListener('play', () => this.changeStatusMainVideoOnScene(true));
2093
2096
  fromEvent(this.video, 'loadedmetadata')
2094
- .pipe(take(1), filter((data) => this.video === data.target), tapLog('VideoService loadedmetadata:'))
2095
- .subscribe(() => {
2096
- if (this.video) {
2097
- this.store.dispatch(videoStreamInitialized());
2098
- }
2099
- });
2097
+ .pipe(take(1), filter((data) => this.video === data.target), tapLog('VideoService loadedmetadata:'), filter(() => !!this.video))
2098
+ .subscribe(() => this.store.dispatch(videoStreamInitialized()));
2100
2099
  }
2101
2100
  createWebRtcAudio() {
2102
2101
  const audio = new Audio();
2103
- audio.id = 'streamingAudio';
2102
+ audio.id = STREAMING_AUDIO_ID;
2104
2103
  this.audio = audio;
2105
2104
  }
2106
2105
  playAudio(audio) {
@@ -2573,7 +2572,9 @@ class SignallingService extends SubService {
2573
2572
  return;
2574
2573
  }
2575
2574
  this.setHandlersFromStream();
2576
- this.action$.pipe(ofType(dataChannelReady)).subscribe(() => {
2575
+ this.action$
2576
+ .pipe(ofType(dataChannelReady), takeUntil(this.abort$))
2577
+ .subscribe(() => {
2577
2578
  this.send({
2578
2579
  type: 'p2pEstablished',
2579
2580
  source: 'front',
@@ -2583,7 +2584,7 @@ class SignallingService extends SubService {
2583
2584
  this.action$
2584
2585
  .pipe(ofType(dataChannelReady), switchMap(() => this.store
2585
2586
  .select(unrealFeature.selectViewportReady)
2586
- .pipe(filter(Truthy), take(1))))
2587
+ .pipe(filter(Truthy), take(1))), takeUntil(this.abort$))
2587
2588
  .subscribe(() => {
2588
2589
  this.send({
2589
2590
  type: 'streamPreparedForUserInteraction',
@@ -2615,10 +2616,10 @@ class SignallingService extends SubService {
2615
2616
  },
2616
2617
  // count: 10, // Optional: cap the number of attempts.
2617
2618
  }),
2618
- // abort$ sits OUTSIDE retry so it kills the entire retry loop,
2619
+ // abort$ sits OUTSIDE retry, so it kills the entire retry loop,
2619
2620
  // not just the current attempt. Each inner createWebSocket also
2620
2621
  // receives abort$ independently, so mid-connection attempts are
2621
- // cancelled too.
2622
+ // canceled too.
2622
2623
  takeUntil(this.abort$));
2623
2624
  }
2624
2625
  startEstablishingConnection() {
@@ -2644,7 +2645,7 @@ class SignallingService extends SubService {
2644
2645
  *
2645
2646
  * 1. **Telemetry** — Starts a `getSignaling` span for end-to-end timing.
2646
2647
  * 2. **Candidate URL selection** — A stateful generator (`urlGen`) is created
2647
- * from `urlsPool`. Each resubscription (e.g. via `retry`) advances to the next URL.
2648
+ * from `urlsPool`. Each resubscription (e.g., via `retry`) advances to the next URL.
2648
2649
  * 3. **Short-circuit for WS URLs** — If the picked candidate already matches
2649
2650
  * `ws://` or `wss://`, emit `{ wsUrl, pollingUrl: null, instanceName: null }`
2650
2651
  * immediately and complete (no HTTP request).
@@ -2656,7 +2657,7 @@ class SignallingService extends SubService {
2656
2657
  * while the request is in flight are **ignored** until completion, preventing overlap.
2657
2658
  * 6. **Per-attempt timeout** — `timeout(WS_TIMEOUT)` caps how long we wait for a response.
2658
2659
  * On timeout, the attempt errors and the outer `retry` schedules the next URL.
2659
- * 7. **Response validation & progress reporting** — A `filter`:
2660
+ * 7. **Response validation and progress reporting** — A `filter`:
2660
2661
  * - Calls `TelemetryStop('getSignaling', { ...data, multi: true })` on receipt.
2661
2662
  * - If `data.signallingServer === ''` or `data.error`:
2662
2663
  * shows a status message, optionally dispatches `setStatusPercentSignallingServer`
@@ -2673,7 +2674,7 @@ class SignallingService extends SubService {
2673
2674
  *
2674
2675
  * ### Concurrency semantics
2675
2676
  *
2676
- * - `exhaustMap` guarantees a **single** HTTP request per attempt; subsequent
2677
+ * - `exhaustMap` guarantees a **single** HTTP request per attempt; later
2677
2678
  * `{clientId, viewId}` emissions are ignored until the current request completes.
2678
2679
  *
2679
2680
  * ### Telemetry semantics
@@ -2707,7 +2708,7 @@ class SignallingService extends SubService {
2707
2708
  * @remarks
2708
2709
  * - The URL generator (`urlGen`) is created **outside** `defer`, so resubscriptions
2709
2710
  * advance the pool. Creating it inside `defer` would restart from the first URL each retry.
2710
- * - `location.protocol` is used to derive `ws` vs `wss`. If this may run in non-browser
2711
+ * - `location.protocol` is used to derive `ws` vs. `wss`. If this may run in non-browser
2711
2712
  * contexts (e.g., SSR), guard or abstract this logic.
2712
2713
  * - Consider capping retries with `count` in `retry({ count, delay })` when appropriate.
2713
2714
  *
@@ -2740,7 +2741,7 @@ class SignallingService extends SubService {
2740
2741
  };
2741
2742
  }));
2742
2743
  }
2743
- // Old (HTTP-polling) orchestration flow — mark the roundtrip so
2744
+ // Old (HTTP-polling) orchestration flow — mark the round trip so
2744
2745
  // the signaling-phase timeline can show it as a distinct segment.
2745
2746
  this.store.dispatch(pollingStarted());
2746
2747
  return this.store.select(selectClientAndViewIds).pipe(
@@ -2914,6 +2915,8 @@ class WebRtcPlayerService extends SubService {
2914
2915
  constructor() {
2915
2916
  super();
2916
2917
  this.onDataChannelMessage$ = new Subject();
2918
+ this.pcClient = null;
2919
+ this.dcClient = null;
2917
2920
  this.signallingSrv = inject(SignallingService);
2918
2921
  this.videoService = inject(VideoService);
2919
2922
  this.commandTelemetryService = inject(CommandTelemetryService);
@@ -2927,29 +2930,37 @@ class WebRtcPlayerService extends SubService {
2927
2930
  this.iceCandidateBuffer = [];
2928
2931
  this.isReconnecting = this.store.selectSignal(unrealFeature.selectIsReconnecting);
2929
2932
  this.init();
2930
- this.actions$.pipe(ofType(dropDC)).subscribe(() => this.dcClient?.close());
2931
- this.actions$.pipe(ofType(dropPC)).subscribe(() => this.pcClient?.close());
2933
+ this.actions$
2934
+ .pipe(ofType(dropDC), takeUntil(this.destroy$))
2935
+ .subscribe(() => this.dcClient?.close());
2936
+ this.actions$
2937
+ .pipe(ofType(dropPC), takeUntil(this.destroy$))
2938
+ .subscribe(() => this.pcClient?.close());
2932
2939
  }
2933
2940
  init() {
2934
- this.signallingSrv.onConfig$.subscribe((config) => {
2941
+ this.signallingSrv.onConfig$
2942
+ .pipe(takeUntil(this.destroy$))
2943
+ .subscribe((config) => {
2935
2944
  this.videoService.create();
2936
2945
  this.setConfig(config);
2937
2946
  });
2938
2947
  // Server-initiated offer: set remote description and respond with answer
2939
- this.signallingSrv.onOffer$.pipe(filter(Truthy)).subscribe((offer) => {
2948
+ this.signallingSrv.onOffer$
2949
+ .pipe(filter(Truthy), takeUntil(this.destroy$))
2950
+ .subscribe((offer) => {
2940
2951
  void this.handleServerOffer(offer);
2941
2952
  });
2942
2953
  // Answer to a client-initiated offer
2943
2954
  this.signallingSrv.onWebRtcAnswer$
2944
- .pipe(filter(Truthy))
2955
+ .pipe(filter(Truthy), takeUntil(this.destroy$))
2945
2956
  .subscribe((answer) => {
2946
2957
  void this.handleServerAnswer(answer);
2947
2958
  });
2948
2959
  // ICE candidates — buffered until remote description is set
2949
- this.signallingSrv.onWebRtcIce$.subscribe((candidate) => {
2950
- this.commandTelemetryService.trackStopCommand('EXT-iceCandidate', true, {
2951
- multi: true,
2952
- });
2960
+ this.signallingSrv.onWebRtcIce$
2961
+ .pipe(takeUntil(this.destroy$))
2962
+ .subscribe((candidate) => {
2963
+ this.commandTelemetryService.trackStopCommand('EXT-iceCandidate', true, { multi: true });
2953
2964
  if (this.remoteDescriptionSet) {
2954
2965
  this.handleCandidateFromServer(candidate);
2955
2966
  }
@@ -2974,11 +2985,15 @@ class WebRtcPlayerService extends SubService {
2974
2985
  if (!this.pcClient) {
2975
2986
  this.setupPeerConnection(this.cfg);
2976
2987
  }
2988
+ const pc = this.pcClient;
2989
+ if (!pc) {
2990
+ return;
2991
+ }
2977
2992
  // Offer already defines media transceivers — don't add extras
2978
- await this.pcClient?.setRemoteDescription(offer);
2993
+ await pc.setRemoteDescription(offer);
2979
2994
  this.remoteDescriptionSet = true;
2980
2995
  this.flushIceCandidateBuffer();
2981
- await this.createAnswer(this.pcClient);
2996
+ await this.createAnswer(pc);
2982
2997
  }
2983
2998
  catch (e) {
2984
2999
  Logger.error('Failed to handle server offer', e);
@@ -3023,10 +3038,16 @@ class WebRtcPlayerService extends SubService {
3023
3038
  if (!this.pcClient) {
3024
3039
  this.setupPeerConnection(this.cfg);
3025
3040
  }
3041
+ const pc = this.pcClient;
3042
+ if (!pc) {
3043
+ return;
3044
+ }
3026
3045
  // Client offers: transceivers first, then DC, then offer
3027
- await this.setupTransceiversAsync(this.pcClient);
3028
- this.dcClient = this.createDataChannel(this.pcClient, 'UnrealDcConnection', { ordered: true });
3029
- await this.createOffer(this.pcClient);
3046
+ await this.setupTransceiversAsync(pc);
3047
+ this.dcClient = this.createDataChannel(pc, 'UnrealDcConnection', {
3048
+ ordered: true,
3049
+ });
3050
+ await this.createOffer(pc);
3030
3051
  }
3031
3052
  catch (e) {
3032
3053
  Logger.error('Failed to create WebRTC offer', e);
@@ -3085,7 +3106,7 @@ class WebRtcPlayerService extends SubService {
3085
3106
  * RFC 6184 that encodes three H.264 parameters:
3086
3107
  * - **Profile IDC** (`XX`) — the H.264 profile.
3087
3108
  * - **Profile IOP** (`YY`) — constraint flags that further narrow the profile.
3088
- * - **Level IDC** (`ZZ`) — the level, which caps resolution/framerate/bitrate.
3109
+ * - **Level IDC** (`ZZ`) — the level which caps resolution/framerate/bitrate.
3089
3110
  *
3090
3111
  * ### H.264 profiles relevant to Unreal Engine Pixel Streaming
3091
3112
  *
@@ -3103,12 +3124,12 @@ class WebRtcPlayerService extends SubService {
3103
3124
  * | `f4001f` | High 4:4:4 Predictive | 3.1 | Lossless capable; rare in browsers |
3104
3125
  *
3105
3126
  * UE Pixel Streaming defaults to Constrained Baseline (`42e01f`) or High (`6400xx`)
3106
- * depending on engine version and `WebRTC.H264Profile` CVar.
3127
+ * depending on the engine version and `WebRTC.H264Profile` CVar.
3107
3128
  *
3108
3129
  * We prioritize `42e01f` because it guarantees hardware decoding on
3109
3130
  * virtually all client devices (desktop, mobile, embedded), avoiding
3110
3131
  * fallback to software decoding or negotiation of a higher profile
3111
- * that the client GPU may not accelerate.
3132
+ * that the client GPU may not speed up.
3112
3133
  */
3113
3134
  prioritizeH264Profile(sdp, profileLevelId) {
3114
3135
  const lines = sdp.split('\r\n');
@@ -3517,7 +3538,7 @@ class FileReceiverService extends SubService {
3517
3538
  }
3518
3539
  this.store
3519
3540
  .select(unrealFeature.selectViewportReady)
3520
- .pipe(filter(Truthy))
3541
+ .pipe(filter(Truthy), takeUntil(this.destroy$))
3521
3542
  .subscribe(() => this.reset());
3522
3543
  }
3523
3544
  reset() {
@@ -3642,7 +3663,7 @@ class AggregatorService extends SubService {
3642
3663
  initialize() {
3643
3664
  this.store
3644
3665
  .select(unrealFeature.selectCirrusConnected)
3645
- .pipe(distinctUntilChanged())
3666
+ .pipe(distinctUntilChanged(), takeUntil(this.destroy$))
3646
3667
  .subscribe((connected) => {
3647
3668
  this.resetResponseList();
3648
3669
  if (connected) {
@@ -3652,7 +3673,9 @@ class AggregatorService extends SubService {
3652
3673
  const unlockViewport$ = this.store
3653
3674
  .select(unrealFeature.selectViewportReady)
3654
3675
  .pipe(filter(Truthy));
3655
- merge(unlockViewport$, this.disconnect$).subscribe(() => {
3676
+ merge(unlockViewport$, this.disconnect$)
3677
+ .pipe(takeUntil(this.destroy$))
3678
+ .subscribe(() => {
3656
3679
  this.removeLoadScreen();
3657
3680
  });
3658
3681
  if (!this.selectStreamConfig()?.autoStart ||
@@ -3664,22 +3687,6 @@ class AggregatorService extends SubService {
3664
3687
  addResponseEventListener(name, listener) {
3665
3688
  this.responseEventListeners.set(name, listener);
3666
3689
  }
3667
- showOnScreenKeyboard(command) {
3668
- if (command.showOnScreenKeyboard) {
3669
- // Show the 'edit text' button.
3670
- //editTextButton.classList.remove('hiddenState');
3671
- // Place the 'edit text' button near the UE4 input widget.
3672
- //let pos = unquantizeAndDenormalizeUnsigned(command.x, command.y);
3673
- //editTextButton.style.top = pos.y.toString() + 'px';
3674
- //editTextButton.style.left = (pos.x - 40).toString() + 'px';
3675
- }
3676
- else {
3677
- // Hide the 'edit text' button.
3678
- //editTextButton.classList.add('hiddenState');
3679
- // Hide the on-screen keyboard.
3680
- // hiddenInput.blur();
3681
- }
3682
- }
3683
3690
  removeLoadScreen() {
3684
3691
  document
3685
3692
  .getElementById(this.unrealConfig?.screenLockerContainerId ||
@@ -3747,12 +3754,8 @@ class AggregatorService extends SubService {
3747
3754
  break;
3748
3755
  }
3749
3756
  case ToClientMessage.Command: {
3750
- const commandAsString = decodeData(anyData);
3751
- Logger.colored(...COLOR_CODES.FROM_UNREAL, commandAsString);
3752
- const command = JSON.parse(commandAsString);
3753
- if (command.command === 'onScreenKeyboard') {
3754
- this.showOnScreenKeyboard(command);
3755
- }
3757
+ const command = JSON.parse(decodeData(anyData));
3758
+ Logger.colored(...COLOR_CODES.FROM_UNREAL, command);
3756
3759
  break;
3757
3760
  }
3758
3761
  case ToClientMessage.FileExtension:
@@ -3857,7 +3860,7 @@ class ConsoleExtensionsService extends SubService {
3857
3860
  command: MetaBoxCommand.FChangeResolutionCommand,
3858
3861
  payload: { resolution },
3859
3862
  });
3860
- return `Max Fps Set to ${resolution}`;
3863
+ return `Resolution set to ${resolution.x}x${resolution.y}`;
3861
3864
  };
3862
3865
  window.setMaxFps = (maxFps) => {
3863
3866
  this.store.dispatch(setMaxFps({ maxFps }));
@@ -3894,6 +3897,10 @@ class DevModeService {
3894
3897
  get isDevMode() {
3895
3898
  return Truthy(localStorage.getItem('devMode'));
3896
3899
  }
3900
+ /**
3901
+ * Called from the browser console to toggle dev mode at runtime.
3902
+ * @preserve
3903
+ */
3897
3904
  setDevMode(value) {
3898
3905
  if (value) {
3899
3906
  localStorage.setItem('devMode', value.toString());
@@ -3932,7 +3939,9 @@ class UnrealCommunicatorService {
3932
3939
  // of the store selector. The selector reads the current state and can fire on
3933
3940
  // stale `true` from a previous session when switching components (Angular
3934
3941
  // creates the new component before destroying the old one).
3935
- this.actions$.pipe(ofType(dataChannelConnected)).subscribe(() => {
3942
+ this.actions$
3943
+ .pipe(ofType(dataChannelConnected), takeUntil$1(this.destroy$))
3944
+ .subscribe(() => {
3936
3945
  this.requestInitialSettings();
3937
3946
  this.requestQualityControl();
3938
3947
  });
@@ -4048,7 +4057,15 @@ class InputService extends SubService {
4048
4057
  KeyboardNumericCode.NumpadDecimal,
4049
4058
  ];
4050
4059
  this.availableKeys = this.defaultKeys;
4060
+ this.options = InputOptions;
4051
4061
  this.reInit$ = new Subject();
4062
+ this.deactivatedVideoTrigger$ = NEVER;
4063
+ this.normalizeAndQuantizeUnsigned = (_x, _y) => ({
4064
+ inRange: false,
4065
+ x: 65535,
4066
+ y: 65535,
4067
+ });
4068
+ this.normalizeAndQuantizeSigned = (_x, _y) => ({ x: 0, y: 0 });
4052
4069
  this.videoBoundingRect = new DOMRect(0, 0, 1, 1);
4053
4070
  this.fingerIds = new Map();
4054
4071
  this.maxByteValue = 255;
@@ -4071,11 +4088,11 @@ class InputService extends SubService {
4071
4088
  const visibilityVisibleTrigger$ = fromEvent(document, 'visibilitychange').pipe(map$1(() => document.visibilityState === 'visible'), filter(Truthy), startWith(true));
4072
4089
  this.deactivatedVideoTrigger$ = merge(this.disconnect$, this.reInit$, visibilityHiddenTrigger$);
4073
4090
  merge(visibilityVisibleTrigger$, this.store.select(unrealFeature.selectViewportReady).pipe(filter(Truthy)))
4074
- .pipe(filter(() => !!this.video))
4091
+ .pipe(filter(() => !!this.video), takeUntil$1(this.destroy$))
4075
4092
  .subscribe(() => this.setup());
4076
4093
  }
4077
4094
  useKeyboardKeys(keys) {
4078
- this.availableKeys = keys || [];
4095
+ this.availableKeys = keys;
4079
4096
  }
4080
4097
  get video() {
4081
4098
  return this.videoService.video;
@@ -4137,49 +4154,47 @@ class InputService extends SubService {
4137
4154
  };
4138
4155
  }
4139
4156
  setupNormalizeAndQuantize(videoElement) {
4140
- if (videoElement) {
4141
- this.videoBoundingRect = videoElement.getBoundingClientRect();
4142
- this.offsetParams = this.getObjectFitOffset(videoElement);
4143
- // Unsigned XY positions are the ratio (0.0..1.0) along a viewport axis,
4144
- // quantized into an uint16 (0..65536).
4145
- // Signed XY deltas are the ratio (-1.0..1.0) along a viewport axis,
4146
- // quantized into an int16 (-32767..32767).
4147
- // This allows the browser viewport and client viewport to have a different
4148
- // size.
4149
- // Hack: Currently we set an out-of-range position to an extreme (65535)
4150
- // as we can't yet accurately detect mouse enter and leave events
4151
- // precisely inside a video with an aspect ratio which causes mattes.
4152
- this.normalizeAndQuantizeUnsigned = (x, y) => {
4153
- const normalizedX = x / videoElement.clientWidth;
4154
- const normalizedY = y / videoElement.clientHeight;
4155
- if (normalizedX < 0.0 ||
4156
- normalizedX > 1.0 ||
4157
- normalizedY < 0.0 ||
4158
- normalizedY > 1.0) {
4159
- return {
4160
- inRange: false,
4161
- x: 65535,
4162
- y: 65535,
4163
- };
4164
- }
4165
- else {
4166
- return {
4167
- inRange: true,
4168
- x: normalizedX * 65536,
4169
- y: normalizedY * 65536,
4170
- };
4171
- }
4172
- };
4173
- // Signed.
4174
- this.normalizeAndQuantizeSigned = (x, y) => {
4175
- const normalizedX = x / (0.5 * videoElement.clientWidth);
4176
- const normalizedY = y / (0.5 * videoElement.clientHeight);
4157
+ this.videoBoundingRect = videoElement.getBoundingClientRect();
4158
+ this.offsetParams = this.getObjectFitOffset(videoElement);
4159
+ // Unsigned XY positions are the ratio (0.0..1.0) along a viewport axis,
4160
+ // quantized into an uint16 (0..65536).
4161
+ // Signed XY deltas are the ratio (-1.0..1.0) along a viewport axis,
4162
+ // quantized into an int16 (-32767..32767).
4163
+ // This allows the browser viewport and client viewport to have a different
4164
+ // size.
4165
+ // Hack: Currently, we set an out-of-range position to an extreme (65535)
4166
+ // as we can't yet accurately detect mouse enter and leave events
4167
+ // precisely inside a video with an aspect ratio which causes mattes.
4168
+ this.normalizeAndQuantizeUnsigned = (x, y) => {
4169
+ const normalizedX = x / videoElement.clientWidth;
4170
+ const normalizedY = y / videoElement.clientHeight;
4171
+ if (normalizedX < 0.0 ||
4172
+ normalizedX > 1.0 ||
4173
+ normalizedY < 0.0 ||
4174
+ normalizedY > 1.0) {
4177
4175
  return {
4178
- x: normalizedX * 32767,
4179
- y: normalizedY * 32767,
4176
+ inRange: false,
4177
+ x: 65535,
4178
+ y: 65535,
4180
4179
  };
4180
+ }
4181
+ else {
4182
+ return {
4183
+ inRange: true,
4184
+ x: normalizedX * 65536,
4185
+ y: normalizedY * 65536,
4186
+ };
4187
+ }
4188
+ };
4189
+ // Signed.
4190
+ this.normalizeAndQuantizeSigned = (x, y) => {
4191
+ const normalizedX = x / (0.5 * videoElement.clientWidth);
4192
+ const normalizedY = y / (0.5 * videoElement.clientHeight);
4193
+ return {
4194
+ x: normalizedX * 32767,
4195
+ y: normalizedY * 32767,
4181
4196
  };
4182
- }
4197
+ };
4183
4198
  }
4184
4199
  sendInputData(data) {
4185
4200
  this.commandsSender.sendInputData(data);
@@ -4219,11 +4234,12 @@ class InputService extends SubService {
4219
4234
  this.fingerIds.set(touch.identifier, finger);
4220
4235
  };
4221
4236
  const forgetTouch = (touch) => {
4222
- this.fingers.push(this.fingerIds.get(touch.identifier));
4237
+ const finger = this.fingerIds.get(touch.identifier);
4238
+ if (finger) {
4239
+ this.fingers.push(finger);
4240
+ }
4223
4241
  // Sort array back into descending order. This means if finger '1' were to lift after finger '0', we would ensure that 0 will be the first index to pop
4224
- this.fingers.sort(function (a, b) {
4225
- return b - a;
4226
- });
4242
+ this.fingers.sort((a, b) => b - a);
4227
4243
  this.fingerIds.delete(touch.identifier);
4228
4244
  };
4229
4245
  const emitTouchData = (type, touches) => {
@@ -4234,21 +4250,24 @@ class InputService extends SubService {
4234
4250
  const touch = touches[t];
4235
4251
  const { x, y } = this.getTouchPositionWithOffset(touch);
4236
4252
  Logger.info(`F${this.fingerIds.get(touch.identifier)}=(${x}, ${y})`);
4237
- const coord = this.normalizeAndQuantizeUnsigned(x, y);
4253
+ const normalizeCoordinates = this.normalizeAndQuantizeUnsigned(x, y);
4238
4254
  const data = new DataView(new ArrayBuffer(9));
4239
4255
  data.setUint8(0, type);
4240
4256
  data.setUint8(1, numTouches);
4241
- data.setUint16(byte, coord.x, true);
4257
+ data.setUint16(byte, normalizeCoordinates.x, true);
4242
4258
  byte += 2;
4243
- data.setUint16(byte, coord.y, true);
4259
+ data.setUint16(byte, normalizeCoordinates.y, true);
4244
4260
  byte += 2;
4245
- data.setUint8(byte, this.fingerIds.get(touch.identifier));
4261
+ const finger = this.fingerIds.get(touch.identifier);
4262
+ if (finger) {
4263
+ data.setUint8(byte, finger);
4264
+ }
4246
4265
  byte += 1;
4247
4266
  // force is between 0.0 and 1.0, so quantize into byte.
4248
4267
  // iOs bug: Forced to be always 0.5 on all devices as we do not use forced touch any way;/
4249
4268
  data.setUint8(byte, this.maxByteValue * 0.5);
4250
4269
  byte += 1;
4251
- data.setUint8(byte, coord.inRange ? 1 : 0);
4270
+ data.setUint8(byte, normalizeCoordinates.inRange ? 1 : 0);
4252
4271
  byte += 1;
4253
4272
  this.sendInputData(data.buffer);
4254
4273
  }
@@ -4291,42 +4310,42 @@ class InputService extends SubService {
4291
4310
  window.focus();
4292
4311
  }
4293
4312
  emitMouseMove(x, y, deltaX, deltaY) {
4294
- const coord = this.normalizeAndQuantizeUnsigned(x, y);
4313
+ const normalizeCoordinates = this.normalizeAndQuantizeUnsigned(x, y);
4295
4314
  const delta = this.normalizeAndQuantizeSigned(deltaX, deltaY);
4296
- const Data = new DataView(new ArrayBuffer(9));
4297
- Data.setUint8(0, MessageType.MouseMove);
4298
- Data.setUint16(1, coord.x, true);
4299
- Data.setUint16(3, coord.y, true);
4300
- Data.setInt16(5, delta.x, true);
4301
- Data.setInt16(7, delta.y, true);
4302
- this.sendInputData(Data.buffer);
4315
+ const data = new DataView(new ArrayBuffer(9));
4316
+ data.setUint8(0, MessageType.MouseMove);
4317
+ data.setUint16(1, normalizeCoordinates.x, true);
4318
+ data.setUint16(3, normalizeCoordinates.y, true);
4319
+ data.setInt16(5, delta.x, true);
4320
+ data.setInt16(7, delta.y, true);
4321
+ this.sendInputData(data.buffer);
4303
4322
  }
4304
4323
  emitMouseDown(button, x, y) {
4305
- const coord = this.normalizeAndQuantizeUnsigned(x, y);
4306
- const Data = new DataView(new ArrayBuffer(6));
4307
- Data.setUint8(0, MessageType.MouseDown);
4308
- Data.setUint8(1, button);
4309
- Data.setUint16(2, coord.x, true);
4310
- Data.setUint16(4, coord.y, true);
4311
- this.sendInputData(Data.buffer);
4324
+ const normalizeCoordinates = this.normalizeAndQuantizeUnsigned(x, y);
4325
+ const data = new DataView(new ArrayBuffer(6));
4326
+ data.setUint8(0, MessageType.MouseDown);
4327
+ data.setUint8(1, button);
4328
+ data.setUint16(2, normalizeCoordinates.x, true);
4329
+ data.setUint16(4, normalizeCoordinates.y, true);
4330
+ this.sendInputData(data.buffer);
4312
4331
  }
4313
4332
  emitMouseUp(button, x, y) {
4314
- const coord = this.normalizeAndQuantizeUnsigned(x, y);
4315
- const Data = new DataView(new ArrayBuffer(6));
4316
- Data.setUint8(0, MessageType.MouseUp);
4317
- Data.setUint8(1, button);
4318
- Data.setUint16(2, coord.x, true);
4319
- Data.setUint16(4, coord.y, true);
4320
- this.sendInputData(Data.buffer);
4333
+ const normalizeCoordinates = this.normalizeAndQuantizeUnsigned(x, y);
4334
+ const data = new DataView(new ArrayBuffer(6));
4335
+ data.setUint8(0, MessageType.MouseUp);
4336
+ data.setUint8(1, button);
4337
+ data.setUint16(2, normalizeCoordinates.x, true);
4338
+ data.setUint16(4, normalizeCoordinates.y, true);
4339
+ this.sendInputData(data.buffer);
4321
4340
  }
4322
4341
  emitMouseWheel(delta, x, y) {
4323
- const coord = this.normalizeAndQuantizeUnsigned(x, y);
4324
- const Data = new DataView(new ArrayBuffer(7));
4325
- Data.setUint8(0, MessageType.MouseWheel);
4326
- Data.setInt16(1, delta, true);
4327
- Data.setUint16(3, coord.x, true);
4328
- Data.setUint16(5, coord.y, true);
4329
- this.sendInputData(Data.buffer);
4342
+ const normalizeCoordinates = this.normalizeAndQuantizeUnsigned(x, y);
4343
+ const data = new DataView(new ArrayBuffer(7));
4344
+ data.setUint8(0, MessageType.MouseWheel);
4345
+ data.setInt16(1, delta, true);
4346
+ data.setUint16(3, normalizeCoordinates.x, true);
4347
+ data.setUint16(5, normalizeCoordinates.y, true);
4348
+ this.sendInputData(data.buffer);
4330
4349
  }
4331
4350
  releaseMouseButtons(buttons, x, y) {
4332
4351
  if (buttons & MouseButtonsMask.PrimaryButton) {
@@ -4345,7 +4364,7 @@ class InputService extends SubService {
4345
4364
  this.emitMouseUp(MouseButton.FifthButton, x, y);
4346
4365
  }
4347
4366
  }
4348
- // If the user has any mouse buttons pressed then press them again.
4367
+ // If the user has any mouse buttons pressed, then press them again.
4349
4368
  pressMouseButtons(buttons, x, y) {
4350
4369
  if (buttons & MouseButtonsMask.PrimaryButton) {
4351
4370
  this.emitMouseDown(MouseButton.MainButton, x, y);
@@ -4364,58 +4383,53 @@ class InputService extends SubService {
4364
4383
  }
4365
4384
  }
4366
4385
  registerHoveringMouseEvents(htmlVideoElement) {
4367
- htmlVideoElement.onmousemove = (e) => {
4386
+ fromEvent(htmlVideoElement, 'mousemove')
4387
+ .pipe(takeUntil$1(this.deactivatedVideoTrigger$))
4388
+ .subscribe((e) => {
4368
4389
  const { x, y } = this.getMousePositionWithOffset(e);
4369
4390
  this.emitMouseMove(x, y, e.movementX, e.movementY);
4370
4391
  e.preventDefault();
4371
- };
4372
- htmlVideoElement.onmousedown = (e) => {
4392
+ });
4393
+ fromEvent(htmlVideoElement, 'mousedown')
4394
+ .pipe(takeUntil$1(this.deactivatedVideoTrigger$))
4395
+ .subscribe((e) => {
4373
4396
  const { x, y } = this.getMousePositionWithOffset(e);
4374
4397
  this.focusWindow();
4375
4398
  this.emitMouseDown(e.button, x, y);
4376
4399
  e.preventDefault();
4377
- };
4378
- htmlVideoElement.onmouseup = (e) => {
4400
+ });
4401
+ fromEvent(htmlVideoElement, 'mouseup')
4402
+ .pipe(takeUntil$1(this.deactivatedVideoTrigger$))
4403
+ .subscribe((e) => {
4379
4404
  const { x, y } = this.getMousePositionWithOffset(e);
4380
4405
  this.emitMouseUp(e.button, x, y);
4381
4406
  e.preventDefault();
4382
- };
4383
- // When the context menu is shown, then it is safest to release the button
4384
- // pressed when the event happened. This will guarantee we will
4385
- // get at least one mouse up corresponding to a mouse down event. Otherwise,
4386
- // the mouse can get stuck.
4387
- // https://github.com/facebook/react/issues/5531
4388
- // playerElement.oncontextmenu = (e) => {
4389
- // e.preventDefault();
4390
- // this.emitMouseUp(e.button, e.offsetX, e.offsetY);
4391
- // };
4407
+ });
4392
4408
  fromEvent(htmlVideoElement, 'wheel')
4393
4409
  .pipe(takeUntil$1(this.deactivatedVideoTrigger$))
4394
4410
  .subscribe((e) => {
4395
4411
  this.emitMouseWheel(e.deltaY * -120, e.offsetX, e.offsetY);
4396
4412
  e.preventDefault();
4397
4413
  });
4398
- htmlVideoElement.pressMouseButtons = (e) => {
4399
- this.pressMouseButtons(e.buttons, e.offsetX, e.offsetY);
4400
- };
4401
- htmlVideoElement.releaseMouseButtons = (e) => {
4402
- this.releaseMouseButtons(e.buttons, e.offsetX, e.offsetY);
4403
- };
4404
4414
  }
4405
4415
  registerMouseEnterAndLeaveEvents(htmlVideoElement) {
4406
- htmlVideoElement.onmouseenter = (e) => {
4416
+ fromEvent(htmlVideoElement, 'mouseenter')
4417
+ .pipe(takeUntil$1(this.deactivatedVideoTrigger$))
4418
+ .subscribe((e) => {
4407
4419
  this.focusWindow();
4408
- const Data = new DataView(new ArrayBuffer(1));
4409
- Data.setUint8(0, MessageType.MouseEnter);
4410
- this.sendInputData(Data.buffer);
4411
- htmlVideoElement.pressMouseButtons(e);
4412
- };
4413
- htmlVideoElement.onmouseleave = (e) => {
4414
- const Data = new DataView(new ArrayBuffer(1));
4415
- Data.setUint8(0, MessageType.MouseLeave);
4416
- this.sendInputData(Data.buffer);
4417
- htmlVideoElement.releaseMouseButtons(e);
4418
- };
4420
+ const data = new DataView(new ArrayBuffer(1));
4421
+ data.setUint8(0, MessageType.MouseEnter);
4422
+ this.sendInputData(data.buffer);
4423
+ this.pressMouseButtons(e.buttons, e.offsetX, e.offsetY);
4424
+ });
4425
+ fromEvent(htmlVideoElement, 'mouseleave')
4426
+ .pipe(takeUntil$1(this.deactivatedVideoTrigger$))
4427
+ .subscribe((e) => {
4428
+ const data = new DataView(new ArrayBuffer(1));
4429
+ data.setUint8(0, MessageType.MouseLeave);
4430
+ this.sendInputData(data.buffer);
4431
+ this.releaseMouseButtons(e.buttons, e.offsetX, e.offsetY);
4432
+ });
4419
4433
  }
4420
4434
  // Browser keys do not have a charCode so we only need to test keyCode.
4421
4435
  isKeyCodeBrowserKey(keyCode) {
@@ -4716,7 +4730,7 @@ class StreamStatusTelemetryService2 {
4716
4730
  /**
4717
4731
  * Whether a session has already started during the current page lifetime.
4718
4732
  * Used to distinguish a fresh page load (bootstrap measurement is
4719
- * meaningful) from a Resume/restart (the document is already loaded,
4733
+ * meaningful) from a Résumé/restart (the document is already loaded,
4720
4734
  * so "bootstrap" is a no-op). Only the first session gets the bootstrap
4721
4735
  * phase anchored at navigation start (perf.now() === 0).
4722
4736
  */
@@ -4725,7 +4739,7 @@ class StreamStatusTelemetryService2 {
4725
4739
  * Rolling snapshot of the currently selected WebRTC ICE candidate pair
4726
4740
  * (local/remote type + url). Scanned over `videoStats$` so the stream
4727
4741
  * *keeps* the first non-null pair even if later stats emit null — WebRTC
4728
- * doesn't un-select a pair mid-session, but the source is nullable. The
4742
+ * doesn't unselect a pair mid-session, but the source is nullable. The
4729
4743
  * `startWith(null)` seed makes `withLatestFrom` on this stream safe even
4730
4744
  * before the first video stat arrives (common at data-channel-open).
4731
4745
  */
@@ -4734,7 +4748,7 @@ class StreamStatusTelemetryService2 {
4734
4748
  * Correlation-matched Observable: emits once when the first `loopBack`
4735
4749
  * command's corresponding `command completed` action fires. Built from
4736
4750
  * the two related actions — the command-started captures the correlation
4737
- * id, the command-completed with that id emits.
4751
+ * id; the command-completed with that id emits.
4738
4752
  */
4739
4753
  this.loopbackReceived$ = this.actions$.pipe(ofType(commandStarted), filter$1(({ command }) => command === 'loopBack'), take(1), switchMap(({ id }) => this.actions$.pipe(ofType(commandCompleted), filter$1((b) => b.id === id), take(1))));
4740
4754
  // Overlay-referenced milestones — hoisted as fields so both
@@ -4765,7 +4779,7 @@ class StreamStatusTelemetryService2 {
4765
4779
  /**
4766
4780
  * Unified phase catalog — the single source of truth for everything
4767
4781
  * this service tracks after `session_started`. Each entry is a phase;
4768
- * its `milestones` array is ordered chronologically and the *last*
4782
+ * its `milestones` array is ordered chronologically, and the *last*
4769
4783
  * entry is the phase closer. When that closer's observable fires, the
4770
4784
  * service emits one generic `stream.phase_ended` wire event with every
4771
4785
  * display field the viewer needs.
@@ -4871,7 +4885,7 @@ class StreamStatusTelemetryService2 {
4871
4885
  // time) into phase-3 params:
4872
4886
  // - `iceCandidateCount` (total ICE candidates from server — too
4873
4887
  // few hints at a restrictive network)
4874
- // - `handshakeFlow` (server-offer vs client-offer; we only use
4888
+ // - `handshakeFlow` (server-offer vs. client-offer; we only use
4875
4889
  // server-offer today, but the detector stays in place)
4876
4890
  // - `instanceType` / `branchInfo` / version / path / app /
4877
4891
  // `connectionId` (streamer identity, from `ssInfo` message)
@@ -4907,7 +4921,7 @@ class StreamStatusTelemetryService2 {
4907
4921
  this.loopbackReceivedEntry,
4908
4922
  // CLOSER — flattens the selected ICE candidate pair into two
4909
4923
  // string params (local / remote), and snapshots the running
4910
- // command counter so phase 5 shows how many commands the client
4924
+ // command counter, so phase 5 shows how many commands the client
4911
4925
  // fired at Unreal before the viewport was ready.
4912
4926
  {
4913
4927
  label: 'Viewport ready',
@@ -4951,7 +4965,7 @@ class StreamStatusTelemetryService2 {
4951
4965
  .select(unrealFeature.selectCirrusConnected)
4952
4966
  .pipe(pairwise(), filter$1(([prev, curr]) => prev && !curr))
4953
4967
  .subscribe(() => this.endSession('disconnected'));
4954
- // Close the session when the tab is hidden / being evicted from bfcache.
4968
+ // Close the session when the tab is hidden / being evicted from cache.
4955
4969
  // `pagehide` fires in more cases than `beforeunload` (covers iOS Safari, bfcache).
4956
4970
  fromEvent(window, 'pagehide').subscribe(() => this.endSession('pagehide'));
4957
4971
  }
@@ -4966,7 +4980,7 @@ class StreamStatusTelemetryService2 {
4966
4980
  // - First session on this page → anchor at navigation start (0).
4967
4981
  // `performance.now()` is already "ms since navigation start", so
4968
4982
  // `session_started.duration` becomes the full page-load + idle-time
4969
- // leading up to the first stream start (i.e. the real bootstrap).
4983
+ // leading up to the first stream start (i.e., the real bootstrap).
4970
4984
  // - Resume/restart sessions → anchor at the session's own start.
4971
4985
  // The document is already loaded — a "bootstrap" phase would either
4972
4986
  // balloon (to include all prior idle time) or be misleading, so we
@@ -5016,7 +5030,7 @@ class StreamStatusTelemetryService2 {
5016
5030
  this.session.handshakeFlow = 'client-offer';
5017
5031
  }));
5018
5032
  // Unreal-command counter — increments on every `commandStarted`
5019
- // action. Snapshotted into the `viewport_ready` closer payload as
5033
+ // action. Snapshot ted into the `viewport_ready` closer payload as
5020
5034
  // phase-5's `commandCount`.
5021
5035
  sub.add(this.actions$.pipe(ofType(commandStarted)).subscribe(() => {
5022
5036
  if (this.session)
@@ -5117,7 +5131,7 @@ class StreamStatusTelemetryService2 {
5117
5131
  * Emit the unified `stream.phase_ended` wire event for a closer. Packs
5118
5132
  * every display field the viewer consumes — phaseNo (derived from the
5119
5133
  * catalog index), label, color, milestones, params, overlays. Boundary
5120
- * labels aren't on the wire — the viewer derives them from neighbour
5134
+ * labels aren't on the wire — the viewer derives them from neighbor
5121
5135
  * phases' `label` fields.
5122
5136
  */
5123
5137
  emitPhaseEnded(bucket, bucketIdx, extrasValue) {
@@ -5188,7 +5202,7 @@ class StreamStatusTelemetryService2 {
5188
5202
  qpP95: qp.p95,
5189
5203
  });
5190
5204
  }
5191
- // FPS low vs current target.
5205
+ // FPS low vs. current target.
5192
5206
  const ref = s.maxFpsTarget > 0 ? s.maxFpsTarget : UNLIMITED_REFERENCE_FPS;
5193
5207
  const fpsLowNow = fps.p50 < ref * FPS_LOW_RATIO;
5194
5208
  if (fpsLowNow && !s.fpsLow) {
@@ -5318,7 +5332,7 @@ class StreamStatusTelemetryService2 {
5318
5332
  // Nearest-rank method: p-th percentile ≡ sorted[ceil(p/100 * n) - 1].
5319
5333
  // Using `floor` would make p95 collapse to `max` for n=20
5320
5334
  // (floor(0.95*20) = 19 = max index), hiding the max line behind p95
5321
- // in charts. `ceil - 1` keeps them distinct: p95 = sorted[18], max = sorted[19].
5335
+ // in charts. `ceil - 1` keeps them distinct: p. 95 = sorted[18], max = sorted[19].
5322
5336
  const pick = (p) => {
5323
5337
  const idx = Math.ceil((p / 100) * sorted.length) - 1;
5324
5338
  return sorted[Math.max(0, Math.min(sorted.length - 1, idx))];
@@ -5331,7 +5345,7 @@ class StreamStatusTelemetryService2 {
5331
5345
  };
5332
5346
  }
5333
5347
  track(event, data) {
5334
- // `userId` is attached to every event so the server can bucket files
5348
+ // `userId` is attached to every event, so the server can bucket files
5335
5349
  // under `data/<userId>/<sessionId>.jsonl` without maintaining a
5336
5350
  // session→user index.
5337
5351
  this.store.dispatch(trackTelemetryData({
@@ -5351,8 +5365,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImpo
5351
5365
  /** Compact one side of an ICE candidate pair to `"type (url)"` or just
5352
5366
  * `type` (host candidates have no URL). */
5353
5367
  function fmtCandidate(c) {
5354
- if (!c)
5368
+ if (!c) {
5355
5369
  return null;
5370
+ }
5356
5371
  return c.url ? `${c.type} (${c.url})` : c.type;
5357
5372
  }
5358
5373
  /**
@@ -5432,22 +5447,21 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImpo
5432
5447
  }] });
5433
5448
 
5434
5449
  class AnalyticsService extends SubService {
5435
- #store = inject(Store);
5436
5450
  /** Signals monitorMouse inner subscriptions to stop (viewport lost / new session). */
5437
5451
  #stopMouse$ = new Subject();
5438
5452
  sendEventToMixPanel(name, params = {}) {
5439
5453
  Logger.log('Analytics Event:', name, params);
5440
- this.#store.dispatch(saveAnalyticsEvent({ event: { name, params } }));
5454
+ this.store.dispatch(saveAnalyticsEvent({ event: { name, params } }));
5441
5455
  }
5442
5456
  constructor() {
5443
5457
  super();
5444
- const abortSignal$ = this.#store
5458
+ const abortSignal$ = this.store
5445
5459
  .select(unrealFeature.selectViewportReady)
5446
5460
  .pipe(filter$1(Falsy), map(() => void 0));
5447
- this.#store
5461
+ this.store
5448
5462
  .select(unrealFeature.selectViewportReady)
5449
- .pipe(distinctUntilChanged(), filter$1(Truthy), tap(() => this.monitorMouse(abortSignal$)))
5450
- .subscribe();
5463
+ .pipe(distinctUntilChanged(), filter$1(Truthy), takeUntil$1(this.destroy$))
5464
+ .subscribe(() => this.monitorMouse(abortSignal$));
5451
5465
  }
5452
5466
  monitorMouse(abortSignal$) {
5453
5467
  this.#stopMouse$.next();
@@ -5530,7 +5544,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImpo
5530
5544
  * ## How it works
5531
5545
  *
5532
5546
  * The service subscribes to `VideoService.videoStats$` (emitted every 250 ms)
5533
- * and collects samples of FPS, bitrate, and QP (Quantisation Parameter).
5547
+ * and collects samples of FPS, bitrate, and QP (Quantization Parameter).
5534
5548
  * Every {@link SAMPLE_WINDOW} ticks (~2 s) it averages the collected samples
5535
5549
  * and feeds them into a set of **fuzzy-logic membership functions** that
5536
5550
  * classify each metric into overlapping quality bands.
@@ -5539,28 +5553,28 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImpo
5539
5553
  *
5540
5554
  * **1. FPS ratio** = `actualFPS / currentTargetFPS` (0..1+)
5541
5555
  *
5542
- * | Band | Range | Meaning |
5556
+ * | Band | Range | Meaning |
5543
5557
  * |--------|---------------------|-----------------------------------------------|
5544
- * | low | ratio < 0.7 → 1.0, linear ramp 0.7..0.9 → 0 | Stream cannot keep up with the target |
5545
- * | ok | triangle 0.8..1.0, peak at 0.95 | Stream roughly matches the target |
5546
- * | high | ratio 0.9..1.0 → ramp, >1.0 → 1.0 | Stream meets or exceeds the target |
5558
+ * | low | ratio < 0.7 → 1.0, linear ramp 0.7..0.9 → 0 | Stream cannot keep up with the target |
5559
+ * | okays | triangle 0.8..1.0, peak at 0.95 | Stream roughly matches the target |
5560
+ * | high | ratio 0.9..1.0 → ramp, >1.0 → 1.0 | Stream meets or exceeds the target |
5547
5561
  *
5548
5562
  * **2. Bitrate load** = `currentBitrate / MAX_BITRATE` (0..1+)
5549
5563
  *
5550
5564
  * `MAX_BITRATE` = 20 Mbit/s — the reference ceiling for the encoder.
5551
5565
  *
5552
- * | Band | Range | Meaning |
5566
+ * | Band | Range | Meaning |
5553
5567
  * |--------|-------------------------------------------------|-----------------------------------------------|
5554
- * | high | load 0.5..0.8 → ramp, >0.8 → 1.0 | Encoder is using most of the available bandwidth |
5555
- * | not high | load < 0.5 → 0.0 | Bandwidth headroom is available |
5568
+ * | high | load 0.5..0.8 → ramp, >0.8 → 1.0 | Encoder is using most of the available bandwidth |
5569
+ * | not high | load < 0.5 → 0.0 | Bandwidth headroom is available |
5556
5570
  *
5557
5571
  * **3. Quality** — mapped from QP via `mapQpToQuality()` (see {@link Quality}):
5558
5572
  *
5559
- * | Quality | QP range | Meaning |
5573
+ * | Quality | QP range | Meaning |
5560
5574
  * |----------|-----------|-------------------------------------------------|
5561
- * | `lime` | QP <= 26 | Good — encoder has enough headroom |
5562
- * | `orange` | QP 27..35 | Fair — encoder is under moderate pressure |
5563
- * | `red` | QP > 35 | Poor — heavy compression, visible artefacts |
5575
+ * | `lime` | QP <= 26 | Good — encoder has enough headroom |
5576
+ * | `orange` | QP 27..35 | Fair — encoder is under moderate pressure |
5577
+ * | `red` | QP > 35 | Poor — heavy compression, visible artifacts |
5564
5578
  *
5565
5579
  * ### Decision rules (evaluated in priority order, first match wins)
5566
5580
  *
@@ -5574,17 +5588,17 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImpo
5574
5588
  *
5575
5589
  * ## Cooldown-based throttling
5576
5590
  *
5577
- * The service continuously evaluates fuzzy rules every sample window.
5591
+ * The service continuously evaluates fuzzy rules in every sample window.
5578
5592
  * After each FPS step change it enforces a cooldown before the next
5579
5593
  * change can be applied. The cooldown duration depends on the
5580
5594
  * (previous decision → next decision) pair:
5581
5595
  *
5582
- * | Previous | Next | Cooldown |
5596
+ * | Previous | Next | Cooldown |
5583
5597
  * |-----------|-----------|-----------------------------------------------------|
5584
- * | decrease | decrease | {@link COOLDOWN_DECREASE_AFTER_DECREASE_MS} (2 s) |
5585
- * | decrease | increase | {@link COOLDOWN_INCREASE_AFTER_DECREASE_MS} (10 s) |
5586
- * | increase | decrease | {@link COOLDOWN_AFTER_INCREASE_MS} (2 s) |
5587
- * | increase | increase | {@link COOLDOWN_AFTER_INCREASE_MS} (2 s) |
5598
+ * | decrease | decrease | {@link COOLDOWN_DECREASE_AFTER_DECREASE_MS} (2 s) |
5599
+ * | decrease | increase | {@link COOLDOWN_INCREASE_AFTER_DECREASE_MS} (10 s) |
5600
+ * | increase | decrease | {@link COOLDOWN_AFTER_INCREASE_MS} (2 s) |
5601
+ * | increase | increase | {@link COOLDOWN_AFTER_INCREASE_MS} (2 s) |
5588
5602
  *
5589
5603
  * This means the system reacts quickly after an increase (2 s in any
5590
5604
  * direction) but is cautious about stepping back up after a decrease
@@ -5601,7 +5615,7 @@ class FpsMonitorService {
5601
5615
  this.videoService = inject(VideoService);
5602
5616
  this.store = inject(Store);
5603
5617
  this.unrealConfig = inject(UNREAL_CONFIG, { optional: true });
5604
- /** Discrete FPS targets the controller can switch between (ascending order). Last 0 is unlimited framerate indicator*/
5618
+ /** Discrete FPS targets the controller can switch between (ascending order). Last 0 is an unlimited framerate indicator*/
5605
5619
  this.FPS_STEPS = [30, 40, 50, 0];
5606
5620
  /**
5607
5621
  * Reference FPS used to compute the FPS ratio when `currentFpsTarget` is 0
@@ -5622,7 +5636,7 @@ class FpsMonitorService {
5622
5636
  this.COOLDOWN_DECREASE_AFTER_DECREASE_MS = 2_000;
5623
5637
  /**
5624
5638
  * Cooldown (ms) for an increase following a previous decrease.
5625
- * The stream needs more time to stabilise before stepping back up.
5639
+ * The stream needs more time to stabilize before stepping back up.
5626
5640
  */
5627
5641
  this.COOLDOWN_INCREASE_AFTER_DECREASE_MS = 10_000;
5628
5642
  /**
@@ -5701,9 +5715,9 @@ class FpsMonitorService {
5701
5715
  * Rules are evaluated in priority order — first match wins.
5702
5716
  *
5703
5717
  * Inputs:
5704
- * - `fpsRatio` = avgFPS / currentTargetFPS (e.g. 45/60 = 0.75)
5705
- * - `bitrateLoad` = avgBitrate / 20 Mbit/s (e.g. 12M/20M = 0.6)
5706
- * - `quality` = mapQpToQuality(avgQP) → 'lime' | 'orange' | 'red'
5718
+ * - `fpsRatio` = avgFPS / currentTargetFPS (e.g., 45/60 = 0.75)
5719
+ * - `bitrateLoad` = avgBitrate / 20 Mbit/s (e.g., 12M/20M = 0.6)
5720
+ * - `quality` = mapQpToQuality(avgQP) → 'lime' | 'orange' | 'red'
5707
5721
  */
5708
5722
  evaluateDecision(avg, currentTarget) {
5709
5723
  const referenceTarget = currentTarget > 0 ? currentTarget : this.UNLIMITED_REFERENCE_FPS;
@@ -5720,17 +5734,17 @@ class FpsMonitorService {
5720
5734
  return 'decrease';
5721
5735
  }
5722
5736
  // Rule 2: FPS looks acceptable numerically, but encoder quality is 'red'
5723
- // (QP > 35 — heavy compression artefacts). Reducing target FPS frees
5737
+ // (QP > 35 — heavy compression artifacts). Reducing target FPS frees
5724
5738
  // encoder headroom and improves visual quality.
5725
5739
  if (fpsOk > 0.4 && quality === 'red') {
5726
5740
  return 'decrease';
5727
5741
  }
5728
- // Rule 3: FPS is ok, but bitrate is above ~50-80% of 20 Mbit/s ceiling
5742
+ // Rule 3: FPS is ok, but the bitrate is above ~50-80% of 20 Mbit/s ceiling
5729
5743
  // and quality is 'orange' (QP 27..35). Near capacity — don't push higher.
5730
5744
  if (fpsOk > 0.4 && bitrateHigh > 0.5 && quality === 'orange') {
5731
5745
  return 'hold';
5732
5746
  }
5733
- // Rule 4: FPS meets/exceeds target (ratio >= ~0.95), quality is 'lime'
5747
+ // Rule 4: FPS meets/exceeds the target (ratio >= ~0.95), quality is 'lime'
5734
5748
  // (QP <= 26), and bitrate is below 50% of ceiling — safe to step up.
5735
5749
  // Note: the last FPS_STEPS value (0) means "no limit", so compare by
5736
5750
  // position in FPS_STEPS rather than numerically.
@@ -5753,11 +5767,11 @@ class FpsMonitorService {
5753
5767
  // ---------------------------------------------------------------------------
5754
5768
  /**
5755
5769
  * FPS ratio membership "low".
5756
- * Input: ratio = actualFPS / targetFPS (e.g. 42/60 = 0.70).
5770
+ * Input: a ratio = actualFPS / targetFPS (e.g., 42/60 = 0.70).
5757
5771
  *
5758
- * ratio < 0.7 → 1.0 (clearly failing, e.g. 40/60)
5759
- * ratio 0.7..0.9 → linear ramp down (e.g. 0.8 → 0.5)
5760
- * ratio > 0.9 → 0.0 (keeping up)
5772
+ * ratio < 0.7 → 1.0 (clearly failing, e.g., 40/60)
5773
+ * ratio 0.7..0.9 → linear ramp down (e.g., 0.8 → 0.5)
5774
+ * a ratio > 0.9 → 0.0 (keeping up)
5761
5775
  */
5762
5776
  membershipFpsLow(ratio) {
5763
5777
  if (ratio < 0.7)
@@ -5770,10 +5784,10 @@ class FpsMonitorService {
5770
5784
  * FPS ratio membership "ok" — triangle shape.
5771
5785
  * Input: ratio = actualFPS / targetFPS.
5772
5786
  *
5773
- * ratio < 0.8 → 0.0
5774
- * ratio 0.8..0.95 → ramp up, peak at 0.95 (e.g. 57/60)
5787
+ * ratio < 0.8 → 0.0
5788
+ * ratio 0.8..0.95 → ramp up, peak at 0.95 (e.g., 57/60)
5775
5789
  * ratio 0.95..1.0 → ramp down
5776
- * ratio > 1.0 → 0.0
5790
+ * a ratio > 1.0 → 0.0
5777
5791
  */
5778
5792
  membershipFpsOk(ratio) {
5779
5793
  if (ratio < 0.8)
@@ -5788,9 +5802,9 @@ class FpsMonitorService {
5788
5802
  * FPS ratio membership "high".
5789
5803
  * Input: ratio = actualFPS / targetFPS.
5790
5804
  *
5791
- * ratio < 0.9 → 0.0
5792
- * ratio 0.9..1.0 → linear ramp up (e.g. 0.95 → 0.5)
5793
- * ratio >= 1.0 → 1.0 (meeting or exceeding target, e.g. 60/60)
5805
+ * ratio < 0.9 → 0.0
5806
+ * ratio 0.9..1.0 → linear ramp up (e.g., 0.95 → 0.5)
5807
+ * a ratio >= 1.0 → 1.0 (meeting or exceeding target, e.g., 60/60)
5794
5808
  */
5795
5809
  membershipFpsHigh(ratio) {
5796
5810
  if (ratio < 0.9)
@@ -5801,11 +5815,11 @@ class FpsMonitorService {
5801
5815
  }
5802
5816
  /**
5803
5817
  * Bitrate load membership "high".
5804
- * Input: load = currentBitrate / MAX_BITRATE (20 Mbit/s).
5818
+ * Input: a load = currentBitrate / MAX_BITRATE (20 Mbit/s).
5805
5819
  *
5806
- * load < 0.5 → 0.0 (below 10 Mbit/s — plenty of headroom)
5807
- * load 0.5..0.8 → linear ramp (e.g. 13M/20M = 0.65 → 0.5)
5808
- * load > 0.8 → 1.0 (above 16 Mbit/s — near capacity)
5820
+ * load < 0.5 → 0.0 (below 10 Mbit/s — plenty of headroom)
5821
+ * load 0.5..0.8 → linear ramp (e.g., 13M/20M = 0.65 → 0.5)
5822
+ * load > 0.8 → 1.0 (above 16 Mbit/s — near capacity)
5809
5823
  */
5810
5824
  membershipBitrateHigh(load) {
5811
5825
  if (load < 0.5)
@@ -5837,14 +5851,14 @@ class FpsMonitorService {
5837
5851
  Logger.info(`[FpsMonitor] Decision: ${decision} → FPS: ${newTarget}, Quality: ${quality}`);
5838
5852
  if (newTarget !== this.currentFpsTarget) {
5839
5853
  this.currentFpsTarget = newTarget;
5840
- this.store.dispatch(setMaxFps({ maxFps: newTarget }));
5854
+ // this.store.dispatch(setMaxFps({ maxFps: newTarget }));
5841
5855
  }
5842
5856
  this.lastDecisionTime = now;
5843
5857
  this.samples = [];
5844
5858
  }
5845
5859
  /**
5846
- * Get the next FPS target based on current target and decision.
5847
- * Clamps to valid FPS_STEPS range.
5860
+ * Get the next FPS target based on the current target and decision.
5861
+ * Clamps to the valid FPS_STEPS range.
5848
5862
  */
5849
5863
  getNextFpsTarget(currentTarget, decision) {
5850
5864
  const currentIndex = this.FPS_STEPS.indexOf(currentTarget);
@@ -5920,12 +5934,7 @@ class UnrealCallbackService {
5920
5934
  * ```
5921
5935
  */
5922
5936
  fromUnrealCallback(command) {
5923
- return this.actions$.pipe(ofType(unrealCommandCallback), map$1(({ json }) => {
5924
- if ('commandCallback' in json) {
5925
- return json.commandCallback;
5926
- }
5927
- return json;
5928
- }), filter((json) => json.command === command), map$1((data) => ({
5937
+ return this.actions$.pipe(ofType(unrealCommandCallback), map$1(({ json }) => 'commandCallback' in json ? json.commandCallback : json), filter((json) => json.command === command), map$1((data) => ({
5929
5938
  command: data.command,
5930
5939
  payload: 'payload' in data ? data.payload : null,
5931
5940
  })));
@@ -6057,15 +6066,15 @@ class ImageLoadingSrcComponent {
6057
6066
  this.imageLoadingSrc = this.#store.selectSignal(unrealFeature.selectImageLoadingSrc);
6058
6067
  }
6059
6068
  #store;
6060
- imageError() {
6069
+ loadingImageError() {
6061
6070
  this.#store.dispatch(setLoadingImageSrc({ src: '' }));
6062
6071
  }
6063
6072
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: ImageLoadingSrcComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
6064
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.11", type: ImageLoadingSrcComponent, isStandalone: true, selector: "app-image-loading-src", ngImport: i0, template: "@let image = imageLoadingSrc();\n@if (isLoaderScreenVisible()) {\n <div class=\"preload\">\n <div class=\"back\">\n @if (image) {\n <img\n (error)=\"imageError()\"\n [ngSrc]=\"image\"\n fill\n loading=\"lazy\"\n alt=\"image loading src\"\n />\n }\n </div>\n </div>\n}\n", styles: [".preload{position:absolute;top:0;left:0;width:100%;height:100%;text-align:center}.preload .back{position:absolute;top:0;left:0;display:flex;justify-content:center;align-items:center;width:100%;height:100%}.preload .back img{width:100%;height:100%;object-fit:cover}.preload .back{background-color:#fff}\n"], dependencies: [{ kind: "directive", type: NgOptimizedImage, selector: "img[ngSrc]", inputs: ["ngSrc", "ngSrcset", "sizes", "width", "height", "decoding", "loading", "priority", "loaderParams", "disableOptimizedSrcset", "fill", "placeholder", "placeholderConfig", "src", "srcset"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
6073
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.11", type: ImageLoadingSrcComponent, isStandalone: true, selector: "app-image-loading-src", ngImport: i0, template: "@let image = imageLoadingSrc();\n@if (isLoaderScreenVisible()) {\n <div class=\"preload\">\n <div class=\"back\">\n @if (image) {\n <img\n (error)=\"loadingImageError()\"\n [ngSrc]=\"image\"\n fill\n loading=\"lazy\"\n alt=\"image loading src\"\n />\n }\n </div>\n </div>\n}\n", styles: [".preload{position:absolute;top:0;left:0;width:100%;height:100%;text-align:center}.preload .back{position:absolute;top:0;left:0;display:flex;justify-content:center;align-items:center;width:100%;height:100%}.preload .back img{width:100%;height:100%;object-fit:cover}.preload .back{background-color:#fff}\n"], dependencies: [{ kind: "directive", type: NgOptimizedImage, selector: "img[ngSrc]", inputs: ["ngSrc", "ngSrcset", "sizes", "width", "height", "decoding", "loading", "priority", "loaderParams", "disableOptimizedSrcset", "fill", "placeholder", "placeholderConfig", "src", "srcset"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
6065
6074
  }
6066
6075
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: ImageLoadingSrcComponent, decorators: [{
6067
6076
  type: Component,
6068
- args: [{ selector: 'app-image-loading-src', changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgOptimizedImage], template: "@let image = imageLoadingSrc();\n@if (isLoaderScreenVisible()) {\n <div class=\"preload\">\n <div class=\"back\">\n @if (image) {\n <img\n (error)=\"imageError()\"\n [ngSrc]=\"image\"\n fill\n loading=\"lazy\"\n alt=\"image loading src\"\n />\n }\n </div>\n </div>\n}\n", styles: [".preload{position:absolute;top:0;left:0;width:100%;height:100%;text-align:center}.preload .back{position:absolute;top:0;left:0;display:flex;justify-content:center;align-items:center;width:100%;height:100%}.preload .back img{width:100%;height:100%;object-fit:cover}.preload .back{background-color:#fff}\n"] }]
6077
+ args: [{ selector: 'app-image-loading-src', changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgOptimizedImage], template: "@let image = imageLoadingSrc();\n@if (isLoaderScreenVisible()) {\n <div class=\"preload\">\n <div class=\"back\">\n @if (image) {\n <img\n (error)=\"loadingImageError()\"\n [ngSrc]=\"image\"\n fill\n loading=\"lazy\"\n alt=\"image loading src\"\n />\n }\n </div>\n </div>\n}\n", styles: [".preload{position:absolute;top:0;left:0;width:100%;height:100%;text-align:center}.preload .back{position:absolute;top:0;left:0;display:flex;justify-content:center;align-items:center;width:100%;height:100%}.preload .back img{width:100%;height:100%;object-fit:cover}.preload .back{background-color:#fff}\n"] }]
6069
6078
  }] });
6070
6079
 
6071
6080
  class StatGraphComponent {
@@ -6152,83 +6161,22 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImpo
6152
6161
 
6153
6162
  class UnrealStatusComponent {
6154
6163
  constructor() {
6155
- this.unrealInitialConfig = inject(UNREAL_CONFIG);
6156
- this.destroyRef = inject(DestroyRef);
6157
- this.store = inject(Store);
6158
- this.resetPercentageSignal$ = this.store
6159
- .select(selectShowLoader)
6160
- .pipe(filter(Falsy));
6161
- this.showLoader = this.store.selectSignal(selectShowLoader);
6162
- /**
6163
- * An observable that emits smoothed percentage values from 0 to 100.
6164
- */
6165
- this.messagePercents = toSignal(this.store.select(selectTotalProgress).pipe(keepMaxUntilReset(this.resetPercentageSignal$), floatToSmoothPercents(), distinctUntilChanged(), map$1((progress) => progress || 0)));
6166
- this.awsInstance = this.store.selectSignal(unrealFeature.selectAwsInstance);
6167
- }
6168
- ngOnInit() {
6169
- /**
6170
- * A subject that emits null values to trigger nullification.
6171
- */
6172
- const nullify$ = new Subject();
6173
- /**
6174
- * An observable that emits null values with a delay of 2000 milliseconds.
6175
- */
6176
- const nullifyObs$ = nullify$
6177
- .asObservable()
6178
- .pipe(delay(2000));
6179
- /**
6180
- * An observable that emits the status message from the Unreal feature store.
6181
- */
6182
- const signalingStatus$ = this.store.select(unrealFeature.selectStatusMessage);
6183
- /**
6184
- * An observable that emits connection status messages based on the Cirrus connection state.
6185
- * Then call side effect to nullify the message.
6186
- */
6187
- const cirrusConnected$ = this.store
6188
- .select(unrealFeature.selectCirrusConnected)
6189
- .pipe(skip(1), switchMap((connected) => connected
6190
- ? of('Creating Socket...')
6191
- : of('Connection closed.').pipe(tap(() => nullify$.next(null)))));
6192
- /**
6193
- * An observable that emits the viewport visibility state.
6194
- */
6195
- const viewportTrigger$ = this.store.select(unrealFeature.selectViewportReady);
6196
- /**
6197
- * An observable that will nullify any message displayed after 500 mSec, when viewport changes its state.
6198
- */
6199
- const streamToggle$ = merge(viewportTrigger$.pipe(filter(Falsy), skip(1)), viewportTrigger$.pipe(filter(Truthy))).pipe(delay(500), map$1(() => null));
6200
- /**
6201
- * A subscription that merges various status observables and emits debounced status via postMessage.
6202
- */
6203
- merge(nullifyObs$, cirrusConnected$, signalingStatus$, streamToggle$)
6204
- .pipe(tapLog('STATUS=>'), debounceTime$1(0), takeUntilDestroyed(this.destroyRef))
6205
- .subscribe((status) => this.sendMessageToIndex(status));
6206
- }
6207
- /**
6208
- * Sends a message to the embedded iframe identified by the default value '3dsource_start_screen' ID.
6209
- * This function tries to post a message to the iframe's content window. If the content
6210
- * window is not accessible (e.g., due to cross-origin policies), the function fails silently.
6211
- *
6212
- * @param {string} text The message to be sent to the iframe. This could be a status update
6213
- * or any other relevant information that the iframe needs to process.
6214
- */
6215
- sendMessageToIndex(text) {
6216
- const objectElement = document.getElementById(this.unrealInitialConfig?.screenLockerContainerId ||
6217
- SCREEN_LOCKER_CONTAINER_ID);
6218
- try {
6219
- const objectDocument = objectElement.contentWindow;
6220
- objectDocument?.postMessage(text, '*');
6221
- }
6222
- catch {
6223
- // Fails silently if the iframe's content window cannot be accessed
6224
- }
6164
+ this.#store = inject(Store);
6165
+ this.#loadingProgress = inject(LoadingProgressService);
6166
+ this.awsInstance = this.#store.selectSignal(unrealFeature.selectAwsInstance);
6167
+ this.messagePercents = this.#loadingProgress.percents;
6168
+ this.showLoader = this.#loadingProgress.loaderVisible;
6169
+ this.showLoadingProgress = this.#store.selectSignal(unrealFeature.selectShowLoadingProgress);
6170
+ this.isShowLoadingPercentsPopup = computed(() => this.showLoader() && this.showLoadingProgress(), ...(ngDevMode ? [{ debugName: "isShowLoadingPercentsPopup" }] : /* istanbul ignore next */ []));
6225
6171
  }
6172
+ #store;
6173
+ #loadingProgress;
6226
6174
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: UnrealStatusComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
6227
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.11", type: UnrealStatusComponent, isStandalone: true, selector: "app-unreal-status", ngImport: i0, template: "@if (showLoader()) {\n @let _awsInstance = awsInstance();\n <div class=\"status-box\">\n @if (_awsInstance?.message) {\n <p>{{ _awsInstance?.message }}</p>\n } @else {\n <div class=\"status-box__message\">\n <src-loading\n [size]=\"30\"\n [backgroundStrokeColor]=\"'#E5E7EB'\"\n [progressStrokeColor]=\"'#1F2937'\"\n />\n {{ messagePercents() }}%\n </div>\n }\n </div>\n}\n", styles: ["app-stat-graph{height:30px;width:100%}.status-box{position:absolute;top:80%;left:50%;transform:translate(-50%,-50%);z-index:5;display:flex;justify-content:center;align-items:center;width:auto;max-width:500px;margin:0;background:var(--src-surface-container-main, #fff);border:none;border-radius:var(--src-border-rounded-full, 9999px);box-shadow:0 8px 20px #1718181f,0 3px 6px #17181814;pointer-events:none}.status-box p{padding:10px 15px;margin:0;text-align:center}.status-box__message{display:flex;align-items:center;padding:12px 20px 12px 16px;color:var(--src-text-body-main, #1f2937);font-size:16px;font-weight:400;line-height:24px}.status-box__message .status-box__message_text{display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden}.status-box__message src-loading{margin-right:12px}\n"], dependencies: [{ kind: "component", type: SourceLoadingComponent, selector: "src-loading", inputs: ["size", "progress", "lineCap", "backgroundStrokeColor", "progressStrokeColor", "strokeWidth", "data-testid", "role", "ariaLabel", "ariaValueNow", "ariaValueMin", "ariaValueMax"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
6175
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.11", type: UnrealStatusComponent, isStandalone: true, selector: "app-unreal-status", ngImport: i0, template: "@if (isShowLoadingPercentsPopup()) {\n @let _awsInstance = awsInstance();\n <div class=\"status-box\">\n @if (_awsInstance?.message) {\n <p>{{ _awsInstance?.message }}</p>\n } @else {\n <div class=\"status-box__message\">\n <src-loading\n [size]=\"30\"\n [backgroundStrokeColor]=\"'#E5E7EB'\"\n [progressStrokeColor]=\"'#1F2937'\"\n />\n {{ messagePercents() }}%\n </div>\n }\n </div>\n}\n", styles: ["app-stat-graph{height:30px;width:100%}.status-box{position:absolute;top:80%;left:50%;transform:translate(-50%,-50%);z-index:5;display:flex;justify-content:center;align-items:center;width:auto;max-width:500px;margin:0;background:var(--src-surface-container-main, #fff);border:none;border-radius:var(--src-border-rounded-full, 9999px);box-shadow:0 8px 20px #1718181f,0 3px 6px #17181814;pointer-events:none}.status-box p{padding:10px 15px;margin:0;text-align:center}.status-box__message{display:flex;align-items:center;padding:12px 20px 12px 16px;color:var(--src-text-body-main, #1f2937);font-size:16px;font-weight:400;line-height:24px}.status-box__message .status-box__message_text{display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden}.status-box__message src-loading{margin-right:12px}\n"], dependencies: [{ kind: "component", type: SourceLoadingComponent, selector: "src-loading", inputs: ["size", "progress", "lineCap", "backgroundStrokeColor", "progressStrokeColor", "strokeWidth", "data-testid", "role", "ariaLabel", "ariaValueNow", "ariaValueMin", "ariaValueMax"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
6228
6176
  }
6229
6177
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: UnrealStatusComponent, decorators: [{
6230
6178
  type: Component,
6231
- args: [{ selector: 'app-unreal-status', changeDetection: ChangeDetectionStrategy.OnPush, imports: [SourceLoadingComponent], template: "@if (showLoader()) {\n @let _awsInstance = awsInstance();\n <div class=\"status-box\">\n @if (_awsInstance?.message) {\n <p>{{ _awsInstance?.message }}</p>\n } @else {\n <div class=\"status-box__message\">\n <src-loading\n [size]=\"30\"\n [backgroundStrokeColor]=\"'#E5E7EB'\"\n [progressStrokeColor]=\"'#1F2937'\"\n />\n {{ messagePercents() }}%\n </div>\n }\n </div>\n}\n", styles: ["app-stat-graph{height:30px;width:100%}.status-box{position:absolute;top:80%;left:50%;transform:translate(-50%,-50%);z-index:5;display:flex;justify-content:center;align-items:center;width:auto;max-width:500px;margin:0;background:var(--src-surface-container-main, #fff);border:none;border-radius:var(--src-border-rounded-full, 9999px);box-shadow:0 8px 20px #1718181f,0 3px 6px #17181814;pointer-events:none}.status-box p{padding:10px 15px;margin:0;text-align:center}.status-box__message{display:flex;align-items:center;padding:12px 20px 12px 16px;color:var(--src-text-body-main, #1f2937);font-size:16px;font-weight:400;line-height:24px}.status-box__message .status-box__message_text{display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden}.status-box__message src-loading{margin-right:12px}\n"] }]
6179
+ args: [{ selector: 'app-unreal-status', changeDetection: ChangeDetectionStrategy.OnPush, imports: [SourceLoadingComponent], template: "@if (isShowLoadingPercentsPopup()) {\n @let _awsInstance = awsInstance();\n <div class=\"status-box\">\n @if (_awsInstance?.message) {\n <p>{{ _awsInstance?.message }}</p>\n } @else {\n <div class=\"status-box__message\">\n <src-loading\n [size]=\"30\"\n [backgroundStrokeColor]=\"'#E5E7EB'\"\n [progressStrokeColor]=\"'#1F2937'\"\n />\n {{ messagePercents() }}%\n </div>\n }\n </div>\n}\n", styles: ["app-stat-graph{height:30px;width:100%}.status-box{position:absolute;top:80%;left:50%;transform:translate(-50%,-50%);z-index:5;display:flex;justify-content:center;align-items:center;width:auto;max-width:500px;margin:0;background:var(--src-surface-container-main, #fff);border:none;border-radius:var(--src-border-rounded-full, 9999px);box-shadow:0 8px 20px #1718181f,0 3px 6px #17181814;pointer-events:none}.status-box p{padding:10px 15px;margin:0;text-align:center}.status-box__message{display:flex;align-items:center;padding:12px 20px 12px 16px;color:var(--src-text-body-main, #1f2937);font-size:16px;font-weight:400;line-height:24px}.status-box__message .status-box__message_text{display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden}.status-box__message src-loading{margin-right:12px}\n"] }]
6232
6180
  }] });
6233
6181
 
6234
6182
  class VideoStatsComponent {
@@ -6240,42 +6188,18 @@ class VideoStatsComponent {
6240
6188
  this.obs$ = this.videoService.videoStats$;
6241
6189
  this.viewportReady = this.store.selectSignal(unrealFeature.selectViewportReady);
6242
6190
  this.ssData = this.store.selectSignal(selectSignalingParameters);
6243
- this.fpsTick = toSignal(this.obs$.pipe(map$1((stat) => ({
6244
- value: stat.aggregatedStats.framesPerSecond || 0,
6245
- }))));
6246
- this.fpsAvgTick = toSignal(this.obs$.pipe(map$1((stat) => ({
6247
- value: stat.aggregatedStats.avgFrameRate,
6248
- }))));
6249
- this.bitrateTick = toSignal(this.obs$.pipe(map$1((stat) => ({
6250
- value: stat.aggregatedStats.bitrate,
6251
- }))));
6252
- this.videoQP = toSignal(this.obs$.pipe(map$1((stat) => ({
6253
- value: stat.aggregatedStats.VideoEncoderQP,
6254
- }))));
6255
- this.bitrateAverageTick = toSignal(this.obs$.pipe(map$1((stat) => ({
6256
- value: stat.aggregatedStats.avgBitrate,
6257
- }))));
6191
+ this.fpsTick = toSignal(this.obs$.pipe(map$1((stat) => ({ value: stat.aggregatedStats.framesPerSecond || 0 }))));
6192
+ this.fpsAvgTick = toSignal(this.obs$.pipe(map$1((stat) => ({ value: stat.aggregatedStats.avgFrameRate }))));
6193
+ this.bitrateTick = toSignal(this.obs$.pipe(map$1((stat) => ({ value: stat.aggregatedStats.bitrate }))));
6194
+ this.videoQP = toSignal(this.obs$.pipe(map$1((stat) => ({ value: stat.aggregatedStats.VideoEncoderQP }))));
6195
+ this.bitrateAverageTick = toSignal(this.obs$.pipe(map$1((stat) => ({ value: stat.aggregatedStats.avgBitrate }))));
6258
6196
  this.videoStatus = toSignal(this.obs$.pipe(map$1((event) => this.isDevMode &&
6259
6197
  {
6260
6198
  ...event.aggregatedStats,
6261
6199
  quality: event.quality,
6262
- }), filter(Boolean), map$1((data) => {
6263
- return this.fields
6264
- .map((key) => {
6265
- return {
6266
- key,
6267
- value: data[key],
6268
- };
6269
- })
6270
- .filter((r) => !this.elementsToShow.includes(r.key));
6271
- })));
6272
- this.graphList = [];
6273
- this.idKey = 'com.source3d.unreal.stat';
6274
- this.elementsToShow = [];
6275
- this.trigger$ = new BehaviorSubject(undefined);
6276
- this.getFields();
6277
- }
6278
- ngOnInit() {
6200
+ }), filter(Boolean), map$1((data) => this.fields
6201
+ .map((key) => ({ key, value: data[key] }))
6202
+ .filter((r) => !this.elementsToShow().includes(r.key)))));
6279
6203
  this.fields = [
6280
6204
  'kalmanBitrate',
6281
6205
  'bitrateDrop',
@@ -6299,26 +6223,21 @@ class VideoStatsComponent {
6299
6223
  'type',
6300
6224
  'codecs',
6301
6225
  'selectedPair',
6302
- //'pliCount',
6303
- //'sliCount',
6304
- //'transportId',
6305
- //'id',
6306
6226
  ];
6307
- this.updateGraph();
6308
- }
6309
- updateGraph() {
6310
- this.graphList = this.fields.map((key) => {
6311
- const stat = this.obs$.pipe(map$1((val) => {
6312
- const value = val.aggregatedStats[key];
6313
- return { value };
6314
- }));
6315
- return {
6316
- key,
6317
- color: Math.floor(Math.random() * 16777216).toString(),
6318
- stat,
6319
- };
6320
- });
6321
- this.graphList$ = of(this.graphList).pipe(combineLatestWith(this.trigger$), map$1(([graphList]) => graphList.filter((r) => this.elementsToShow.includes(r.key))));
6227
+ this.elementsToShow = signal([], ...(ngDevMode ? [{ debugName: "elementsToShow" }] : /* istanbul ignore next */ []));
6228
+ this.idKey = 'com.source3d.unreal.stat';
6229
+ this.graphListItems = this.fields.map((key) => ({
6230
+ key,
6231
+ color: '#' +
6232
+ Math.floor(Math.random() * 16777216)
6233
+ .toString(16)
6234
+ .padStart(6, '0'),
6235
+ stat: this.obs$.pipe(map$1((val) => ({
6236
+ value: val.aggregatedStats[key],
6237
+ }))),
6238
+ }));
6239
+ this.graphList = computed(() => this.graphListItems.filter((r) => this.elementsToShow().includes(r.key)), ...(ngDevMode ? [{ debugName: "graphList" }] : /* istanbul ignore next */ []));
6240
+ this.restoreFromStorage();
6322
6241
  }
6323
6242
  toggle() {
6324
6243
  this.collapsed.update((collapsed) => !collapsed);
@@ -6330,78 +6249,63 @@ class VideoStatsComponent {
6330
6249
  (typeof value === 'number' && isNaN(value))) {
6331
6250
  return;
6332
6251
  }
6333
- if (this.elementsToShow.includes(el)) {
6334
- this.elementsToShow = this.elementsToShow.filter((item) => item !== el);
6335
- }
6336
- else {
6337
- this.elementsToShow.push(el);
6338
- }
6252
+ this.elementsToShow.update((current) => current.includes(el)
6253
+ ? current.filter((item) => item !== el)
6254
+ : [...current, el]);
6339
6255
  this.writeFields();
6340
- this.trigger$.next();
6341
6256
  }
6342
- getFields() {
6257
+ restoreFromStorage() {
6343
6258
  try {
6344
- const { elements, collapsed } = JSON.parse(localStorage.getItem(this.idKey)) || [];
6345
- this.elementsToShow = elements || [];
6346
- this.collapsed.set(collapsed);
6259
+ const { elements, collapsed } = JSON.parse(localStorage.getItem(this.idKey)) || {};
6260
+ this.elementsToShow.set(elements || []);
6261
+ this.collapsed.set(collapsed ?? false);
6347
6262
  }
6348
6263
  catch {
6349
- this.elementsToShow = [];
6264
+ this.elementsToShow.set([]);
6350
6265
  }
6351
6266
  }
6352
6267
  writeFields() {
6353
6268
  localStorage.setItem(this.idKey, JSON.stringify({
6354
6269
  collapsed: this.collapsed(),
6355
- elements: this.elementsToShow,
6270
+ elements: this.elementsToShow(),
6356
6271
  }));
6357
6272
  }
6358
6273
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: VideoStatsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
6359
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.11", type: VideoStatsComponent, isStandalone: true, selector: "app-video-stats", ngImport: i0, template: "@if (videoStatus() && viewportReady()) {\n <div class=\"settings-container\">\n <button (click)=\"toggle()\" class=\"gear-button\">Stats</button>\n <div (click)=\"toggle()\" [class.min]=\"collapsed()\" id=\"aggregatedStats\">\n <div class=\"forNerds\">\n <div (click)=\"$event.stopPropagation()\" class=\"static\">\n <li>\n <app-stat-graph\n [stat]=\"fpsTick()!\"\n label=\"FPS (higher is better)\"\n />\n </li>\n <li>\n <app-stat-graph\n [stat]=\"videoQP()!\"\n label=\"QP (lower is better)\"\n color=\"#D5ff07\"\n />\n </li>\n <li>\n <app-stat-graph\n [stat]=\"bitrateTick()!\"\n label=\"Bitrate\"\n color=\"#D57F07\"\n />\n </li>\n </div>\n <div>\n @for (graph of graphList$ | async; track graph) {\n <li (click)=\"toggleGraph($event, graph.key, 0)\" class=\"graph\">\n <app-stat-graph\n [label]=\"graph.key\"\n [stat]=\"(graph.stat | async)!\"\n [color]=\"graph.color\"\n />\n </li>\n }\n </div>\n </div>\n\n @for (el of videoStatus(); track el.key + $index) {\n <div (click)=\"toggleGraph($event, el.key, el.value)\" class=\"stat\">\n <span>{{ el.key }}: </span>{{ el.value | json }}\n </div>\n }\n </div>\n </div>\n @if (!collapsed()) {\n <div [innerHTML]=\"ssData() | safe: 'html'\" class=\"ssInfo\"></div>\n }\n}\n", styles: [".settings-container{position:absolute;top:65px;left:10px;z-index:1000;bottom:10px}.gear-button{left:0;width:50px;position:absolute;background:none;cursor:pointer;font-size:10px;font-weight:500;border:1px solid;border-radius:9px;background:#0162cc;color:#fff;padding:4px}.gear-button>*{color:#fff;filter:drop-shadow(1px 1px 1px rgb(0,0,0))}#aggregatedStats{max-height:calc(100% - 25px);overflow-y:auto;margin-top:25px;left:20px;z-index:31;max-width:400px;padding:10px;background-color:#fff3;border:1px solid grey}#aggregatedStats>*{color:#fff;text-shadow:-1px -1px 0 #000,1px -1px 0 #000,-1px 1px 0 #000,1px 1px 0 #000}#aggregatedStats:empty{display:none}#aggregatedStats button{position:absolute;right:5px;top:5px}#aggregatedStats.min{display:none}#aggregatedStats .forNerds{width:100%}#aggregatedStats .forNerds li{list-style-type:none;height:60px}#aggregatedStats .forNerds li app-stat-graph{display:block;width:100%;height:100%}#aggregatedStats .stat span{font-weight:700}#aggregatedStats .stat,#aggregatedStats .graph{cursor:pointer}#aggregatedStats .static{pointer-events:all}.ssInfo{position:absolute;top:0;left:50%;transform:translate(-50%);color:#fff;text-shadow:-1px -1px 0 #000,1px -1px 0 #000,-1px 1px 0 #000,1px 1px 0 #000;text-align:center;width:80%;pointer-events:none}\n"], dependencies: [{ kind: "component", type: StatGraphComponent, selector: "app-stat-graph", inputs: ["label", "stat", "color", "tickStep"] }, { kind: "pipe", type: AsyncPipe, name: "async" }, { kind: "pipe", type: JsonPipe, name: "json" }, { kind: "pipe", type: SafePipe, name: "safe" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
6274
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.11", type: VideoStatsComponent, isStandalone: true, selector: "app-video-stats", ngImport: i0, template: "@if (videoStatus() && viewportReady()) {\n <div class=\"settings-container\">\n <button (click)=\"toggle()\" class=\"gear-button\">Stats</button>\n <div (click)=\"toggle()\" [class.min]=\"collapsed()\" id=\"aggregatedStats\">\n <div class=\"forNerds\">\n <div (click)=\"$event.stopPropagation()\" class=\"static\">\n <li>\n <app-stat-graph\n [stat]=\"fpsTick()!\"\n label=\"FPS (higher is better)\"\n />\n </li>\n <li>\n <app-stat-graph\n [stat]=\"videoQP()!\"\n label=\"QP (lower is better)\"\n color=\"#D5ff07\"\n />\n </li>\n <li>\n <app-stat-graph\n [stat]=\"bitrateTick()!\"\n label=\"Bitrate\"\n color=\"#D57F07\"\n />\n </li>\n </div>\n <div>\n @for (graph of graphList(); track graph.key) {\n <li (click)=\"toggleGraph($event, graph.key, 0)\" class=\"graph\">\n <app-stat-graph\n [label]=\"graph.key\"\n [stat]=\"(graph.stat | async)!\"\n [color]=\"graph.color\"\n />\n </li>\n }\n </div>\n </div>\n\n @for (el of videoStatus(); track el.key + $index) {\n <div (click)=\"toggleGraph($event, el.key, el.value)\" class=\"stat\">\n <span>{{ el.key }}: </span>{{ el.value | json }}\n </div>\n }\n </div>\n </div>\n @if (!collapsed()) {\n <div class=\"ssInfo\">\n @for (line of ssData(); track line) {\n <div>{{ line }}</div>\n }\n </div>\n }\n}\n", styles: [".settings-container{position:absolute;top:65px;left:10px;z-index:1000;bottom:10px}.gear-button{left:0;width:50px;position:absolute;background:none;cursor:pointer;font-size:10px;font-weight:500;border:1px solid;border-radius:9px;background:#0162cc;color:#fff;padding:4px}.gear-button>*{color:#fff;filter:drop-shadow(1px 1px 1px rgb(0,0,0))}#aggregatedStats{max-height:calc(100% - 25px);overflow-y:auto;margin-top:25px;left:20px;z-index:31;max-width:400px;padding:10px;background-color:#fff3;border:1px solid grey}#aggregatedStats>*{color:#fff;text-shadow:-1px -1px 0 #000,1px -1px 0 #000,-1px 1px 0 #000,1px 1px 0 #000}#aggregatedStats:empty{display:none}#aggregatedStats button{position:absolute;right:5px;top:5px}#aggregatedStats.min{display:none}#aggregatedStats .forNerds{width:100%}#aggregatedStats .forNerds li{list-style-type:none;height:60px}#aggregatedStats .forNerds li app-stat-graph{display:block;width:100%;height:100%}#aggregatedStats .stat span{font-weight:700}#aggregatedStats .stat,#aggregatedStats .graph{cursor:pointer}#aggregatedStats .static{pointer-events:all}.ssInfo{position:absolute;top:0;left:50%;transform:translate(-50%);color:#fff;text-shadow:-1px -1px 0 #000,1px -1px 0 #000,-1px 1px 0 #000,1px 1px 0 #000;text-align:center;width:80%;pointer-events:none}\n"], dependencies: [{ kind: "component", type: StatGraphComponent, selector: "app-stat-graph", inputs: ["label", "stat", "color", "tickStep"] }, { kind: "pipe", type: AsyncPipe, name: "async" }, { kind: "pipe", type: JsonPipe, name: "json" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
6360
6275
  }
6361
6276
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: VideoStatsComponent, decorators: [{
6362
6277
  type: Component,
6363
- args: [{ selector: 'app-video-stats', changeDetection: ChangeDetectionStrategy.OnPush, imports: [StatGraphComponent, AsyncPipe, JsonPipe, SafePipe], template: "@if (videoStatus() && viewportReady()) {\n <div class=\"settings-container\">\n <button (click)=\"toggle()\" class=\"gear-button\">Stats</button>\n <div (click)=\"toggle()\" [class.min]=\"collapsed()\" id=\"aggregatedStats\">\n <div class=\"forNerds\">\n <div (click)=\"$event.stopPropagation()\" class=\"static\">\n <li>\n <app-stat-graph\n [stat]=\"fpsTick()!\"\n label=\"FPS (higher is better)\"\n />\n </li>\n <li>\n <app-stat-graph\n [stat]=\"videoQP()!\"\n label=\"QP (lower is better)\"\n color=\"#D5ff07\"\n />\n </li>\n <li>\n <app-stat-graph\n [stat]=\"bitrateTick()!\"\n label=\"Bitrate\"\n color=\"#D57F07\"\n />\n </li>\n </div>\n <div>\n @for (graph of graphList$ | async; track graph) {\n <li (click)=\"toggleGraph($event, graph.key, 0)\" class=\"graph\">\n <app-stat-graph\n [label]=\"graph.key\"\n [stat]=\"(graph.stat | async)!\"\n [color]=\"graph.color\"\n />\n </li>\n }\n </div>\n </div>\n\n @for (el of videoStatus(); track el.key + $index) {\n <div (click)=\"toggleGraph($event, el.key, el.value)\" class=\"stat\">\n <span>{{ el.key }}: </span>{{ el.value | json }}\n </div>\n }\n </div>\n </div>\n @if (!collapsed()) {\n <div [innerHTML]=\"ssData() | safe: 'html'\" class=\"ssInfo\"></div>\n }\n}\n", styles: [".settings-container{position:absolute;top:65px;left:10px;z-index:1000;bottom:10px}.gear-button{left:0;width:50px;position:absolute;background:none;cursor:pointer;font-size:10px;font-weight:500;border:1px solid;border-radius:9px;background:#0162cc;color:#fff;padding:4px}.gear-button>*{color:#fff;filter:drop-shadow(1px 1px 1px rgb(0,0,0))}#aggregatedStats{max-height:calc(100% - 25px);overflow-y:auto;margin-top:25px;left:20px;z-index:31;max-width:400px;padding:10px;background-color:#fff3;border:1px solid grey}#aggregatedStats>*{color:#fff;text-shadow:-1px -1px 0 #000,1px -1px 0 #000,-1px 1px 0 #000,1px 1px 0 #000}#aggregatedStats:empty{display:none}#aggregatedStats button{position:absolute;right:5px;top:5px}#aggregatedStats.min{display:none}#aggregatedStats .forNerds{width:100%}#aggregatedStats .forNerds li{list-style-type:none;height:60px}#aggregatedStats .forNerds li app-stat-graph{display:block;width:100%;height:100%}#aggregatedStats .stat span{font-weight:700}#aggregatedStats .stat,#aggregatedStats .graph{cursor:pointer}#aggregatedStats .static{pointer-events:all}.ssInfo{position:absolute;top:0;left:50%;transform:translate(-50%);color:#fff;text-shadow:-1px -1px 0 #000,1px -1px 0 #000,-1px 1px 0 #000,1px 1px 0 #000;text-align:center;width:80%;pointer-events:none}\n"] }]
6278
+ args: [{ selector: 'app-video-stats', changeDetection: ChangeDetectionStrategy.OnPush, imports: [StatGraphComponent, AsyncPipe, JsonPipe], template: "@if (videoStatus() && viewportReady()) {\n <div class=\"settings-container\">\n <button (click)=\"toggle()\" class=\"gear-button\">Stats</button>\n <div (click)=\"toggle()\" [class.min]=\"collapsed()\" id=\"aggregatedStats\">\n <div class=\"forNerds\">\n <div (click)=\"$event.stopPropagation()\" class=\"static\">\n <li>\n <app-stat-graph\n [stat]=\"fpsTick()!\"\n label=\"FPS (higher is better)\"\n />\n </li>\n <li>\n <app-stat-graph\n [stat]=\"videoQP()!\"\n label=\"QP (lower is better)\"\n color=\"#D5ff07\"\n />\n </li>\n <li>\n <app-stat-graph\n [stat]=\"bitrateTick()!\"\n label=\"Bitrate\"\n color=\"#D57F07\"\n />\n </li>\n </div>\n <div>\n @for (graph of graphList(); track graph.key) {\n <li (click)=\"toggleGraph($event, graph.key, 0)\" class=\"graph\">\n <app-stat-graph\n [label]=\"graph.key\"\n [stat]=\"(graph.stat | async)!\"\n [color]=\"graph.color\"\n />\n </li>\n }\n </div>\n </div>\n\n @for (el of videoStatus(); track el.key + $index) {\n <div (click)=\"toggleGraph($event, el.key, el.value)\" class=\"stat\">\n <span>{{ el.key }}: </span>{{ el.value | json }}\n </div>\n }\n </div>\n </div>\n @if (!collapsed()) {\n <div class=\"ssInfo\">\n @for (line of ssData(); track line) {\n <div>{{ line }}</div>\n }\n </div>\n }\n}\n", styles: [".settings-container{position:absolute;top:65px;left:10px;z-index:1000;bottom:10px}.gear-button{left:0;width:50px;position:absolute;background:none;cursor:pointer;font-size:10px;font-weight:500;border:1px solid;border-radius:9px;background:#0162cc;color:#fff;padding:4px}.gear-button>*{color:#fff;filter:drop-shadow(1px 1px 1px rgb(0,0,0))}#aggregatedStats{max-height:calc(100% - 25px);overflow-y:auto;margin-top:25px;left:20px;z-index:31;max-width:400px;padding:10px;background-color:#fff3;border:1px solid grey}#aggregatedStats>*{color:#fff;text-shadow:-1px -1px 0 #000,1px -1px 0 #000,-1px 1px 0 #000,1px 1px 0 #000}#aggregatedStats:empty{display:none}#aggregatedStats button{position:absolute;right:5px;top:5px}#aggregatedStats.min{display:none}#aggregatedStats .forNerds{width:100%}#aggregatedStats .forNerds li{list-style-type:none;height:60px}#aggregatedStats .forNerds li app-stat-graph{display:block;width:100%;height:100%}#aggregatedStats .stat span{font-weight:700}#aggregatedStats .stat,#aggregatedStats .graph{cursor:pointer}#aggregatedStats .static{pointer-events:all}.ssInfo{position:absolute;top:0;left:50%;transform:translate(-50%);color:#fff;text-shadow:-1px -1px 0 #000,1px -1px 0 #000,-1px 1px 0 #000,1px 1px 0 #000;text-align:center;width:80%;pointer-events:none}\n"] }]
6364
6279
  }], ctorParameters: () => [] });
6365
6280
 
6366
6281
  class ResumeStreamButtonComponent {
6282
+ #store;
6367
6283
  constructor() {
6368
- this.actions$ = inject(Actions);
6369
6284
  this.#store = inject(Store);
6370
- this.#videoService = inject(VideoService);
6371
- this.#destroyRef = inject(DestroyRef);
6372
- this.showReconnectPopup = this.#store.selectSignal(selectShowReconnectPopup);
6373
- this.isLoaderScreenVisible = this.#store.selectSignal(selectIsLoaderScreenVisible);
6374
6285
  this.streamConfig = this.#store.selectSignal(selectStreamConfig);
6286
+ this.isSecondStart = signal(!!(this.streamConfig()?.autoStart && location.href.match(/^https/gi)), ...(ngDevMode ? [{ debugName: "isSecondStart" }] : /* istanbul ignore next */ []));
6287
+ this.isLoaderScreenVisible = this.#store.selectSignal(selectIsLoaderScreenVisible);
6288
+ this.showReconnectPopup = this.#store.selectSignal(selectShowReconnectPopup);
6375
6289
  this.isExistMatchUrls = this.#store.selectSignal(selectIsExistMatchUrls);
6376
6290
  this.showResumeStreamPopup = this.#store.selectSignal(unrealFeature.selectShowResumeStreamPopup);
6377
- this.isSecondStart = signal(!!(this.streamConfig()?.autoStart && location.href.match(/^https/gi)), ...(ngDevMode ? [{ debugName: "isSecondStart" }] : /* istanbul ignore next */ []));
6378
6291
  this.isShowResumeButton = computed(() => this.isLoaderScreenVisible() &&
6379
6292
  this.showReconnectPopup() &&
6380
6293
  this.isExistMatchUrls() &&
6381
6294
  this.showResumeStreamPopup(), ...(ngDevMode ? [{ debugName: "isShowResumeButton" }] : /* istanbul ignore next */ []));
6382
- }
6383
- #store;
6384
- #videoService;
6385
- #destroyRef;
6386
- ngOnInit() {
6387
- this.#store
6388
- .select(unrealFeature.selectCirrusConnected)
6389
- .pipe(skip$1(1), filter(Falsy), takeUntilDestroyed(this.#destroyRef))
6390
- .subscribe(() => this.onDisconnect());
6391
- }
6392
- ngAfterViewInit() {
6393
- this.actions$
6394
- .pipe(ofType(videoStreamInitialized), takeUntilDestroyed(this.#destroyRef), filter(() => !!this.#videoService.video?.paused))
6395
- .subscribe(() => {
6396
- Logger.info('Simulate Press Play');
6397
- this.#videoService.play();
6398
- this.#store.dispatch(resetAfk());
6295
+ this.cirrusConnected = this.#store.selectSignal(unrealFeature.selectCirrusConnected);
6296
+ let wasConnected = this.cirrusConnected();
6297
+ effect(() => {
6298
+ const connected = this.cirrusConnected();
6299
+ if (!connected && wasConnected) {
6300
+ this.disconnect();
6301
+ }
6302
+ wasConnected = connected;
6399
6303
  });
6400
6304
  }
6401
6305
  connect() {
6402
6306
  this.#store.dispatch(startStream({ config: { autoStart: true } }));
6403
6307
  }
6404
- onDisconnect() {
6308
+ disconnect() {
6405
6309
  this.isSecondStart.set(true);
6406
6310
  }
6407
6311
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: ResumeStreamButtonComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
@@ -6410,13 +6314,13 @@ class ResumeStreamButtonComponent {
6410
6314
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: ResumeStreamButtonComponent, decorators: [{
6411
6315
  type: Component,
6412
6316
  args: [{ selector: 'app-resume-stream-button', changeDetection: ChangeDetectionStrategy.OnPush, imports: [SourceButtonComponent], template: "@if (isShowResumeButton()) {\n <div class=\"resume-box\">\n @if (isSecondStart()) {\n <div class=\"resume-box__text\">\n Your stream has been paused due to inactivity\n </div>\n }\n\n <src-button\n (onClick)=\"connect()\"\n [isFullWidth]=\"true\"\n [data-testid]=\"'connect-button'\"\n size=\"lg\"\n weight=\"primary\"\n class=\"connect-button\"\n >\n {{ isSecondStart() ? 'Resume' : 'Start' }}\n </src-button>\n </div>\n}\n", styles: [".resume-box{padding:16px;position:absolute;bottom:0;left:50%;gap:12px;z-index:6;display:flex;transform:translate(-50%,-50%);flex-direction:column;align-items:center;border-radius:var(--src-border-rounded-parent, 8px);background:var(--src-surface-container-main, #fff);box-shadow:0 26px 80px #0003,0 0 1px #0003}.resume-box .connect-button{width:100%}.resume-box .resume-box__text{color:var(--src-color-grey-500, #6b7280);text-align:center;font-family:var(--src-font-family-body);font-size:14px;font-style:normal;font-weight:400;line-height:24px}\n"] }]
6413
- }] });
6317
+ }], ctorParameters: () => [] });
6414
6318
 
6415
6319
  class IntroSrcComponent {
6416
6320
  #store;
6417
6321
  constructor() {
6418
- this.videoElement = viewChild('videoPlayerCover', ...(ngDevMode ? [{ debugName: "videoElement" }] : /* istanbul ignore next */ []));
6419
6322
  this.#store = inject(Store);
6323
+ this.videoElement = viewChild('videoPlayerCover', ...(ngDevMode ? [{ debugName: "videoElement" }] : /* istanbul ignore next */ []));
6420
6324
  this.imageIntroSrc = this.#store.selectSignal(unrealFeature.selectImageIntroSrc);
6421
6325
  this.videoIntroSrc = this.#store.selectSignal(unrealFeature.selectVideoIntroSrc);
6422
6326
  this.viewportReady = this.#store.selectSignal(unrealFeature.selectViewportReady);
@@ -6440,6 +6344,9 @@ class IntroSrcComponent {
6440
6344
  });
6441
6345
  });
6442
6346
  }
6347
+ loadingImageError() {
6348
+ this.#store.dispatch(resetIntroSrc());
6349
+ }
6443
6350
  createVideo(src, videoContainer) {
6444
6351
  if (typeof src !== 'string') {
6445
6352
  return;
@@ -6470,15 +6377,12 @@ class IntroSrcComponent {
6470
6377
  sourceElement.addEventListener('error', () => this.#store.dispatch(resetIntroSrc()));
6471
6378
  videoContainer.appendChild(videoElement);
6472
6379
  }
6473
- imageError() {
6474
- this.#store.dispatch(resetIntroSrc());
6475
- }
6476
6380
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: IntroSrcComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
6477
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.11", type: IntroSrcComponent, isStandalone: true, selector: "app-intro-src", viewQueries: [{ propertyName: "videoElement", first: true, predicate: ["videoPlayerCover"], descendants: true, isSignal: true }], ngImport: i0, template: "@let image = imageIntroSrc();\n@if (image) {\n <div class=\"backImage\">\n <img (error)=\"imageError()\" [ngSrc]=\"image\" fill alt=\"imageSrc\" />\n </div>\n} @else if (videoIntroSrc()) {\n <div #videoPlayerCover class=\"backVideo\"></div>\n}\n", styles: [".backVideo{position:absolute;top:0;left:0;display:flex;justify-content:center;align-items:center;width:100%;height:100%;z-index:1}.backVideo video{position:absolute;width:100%;height:100%;object-fit:cover}.backImage{position:absolute;top:0;left:0;display:flex;justify-content:center;align-items:center;width:100%;height:100%;z-index:1}.backImage img{position:absolute;width:100%;height:100%;object-fit:cover}\n"], dependencies: [{ kind: "directive", type: NgOptimizedImage, selector: "img[ngSrc]", inputs: ["ngSrc", "ngSrcset", "sizes", "width", "height", "decoding", "loading", "priority", "loaderParams", "disableOptimizedSrcset", "fill", "placeholder", "placeholderConfig", "src", "srcset"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
6381
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.11", type: IntroSrcComponent, isStandalone: true, selector: "app-intro-src", viewQueries: [{ propertyName: "videoElement", first: true, predicate: ["videoPlayerCover"], descendants: true, isSignal: true }], ngImport: i0, template: "@let image = imageIntroSrc();\n@if (image) {\n <div class=\"backImage\">\n <img (error)=\"loadingImageError()\" [ngSrc]=\"image\" fill alt=\"imageSrc\" />\n </div>\n} @else if (videoIntroSrc()) {\n <div #videoPlayerCover class=\"backVideo\"></div>\n}\n", styles: [".backVideo{position:absolute;top:0;left:0;display:flex;justify-content:center;align-items:center;width:100%;height:100%;z-index:1}.backVideo video{position:absolute;width:100%;height:100%;object-fit:cover}.backImage{position:absolute;top:0;left:0;display:flex;justify-content:center;align-items:center;width:100%;height:100%;z-index:1}.backImage img{position:absolute;width:100%;height:100%;object-fit:cover}\n"], dependencies: [{ kind: "directive", type: NgOptimizedImage, selector: "img[ngSrc]", inputs: ["ngSrc", "ngSrcset", "sizes", "width", "height", "decoding", "loading", "priority", "loaderParams", "disableOptimizedSrcset", "fill", "placeholder", "placeholderConfig", "src", "srcset"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
6478
6382
  }
6479
6383
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: IntroSrcComponent, decorators: [{
6480
6384
  type: Component,
6481
- args: [{ selector: 'app-intro-src', imports: [NgOptimizedImage], changeDetection: ChangeDetectionStrategy.OnPush, template: "@let image = imageIntroSrc();\n@if (image) {\n <div class=\"backImage\">\n <img (error)=\"imageError()\" [ngSrc]=\"image\" fill alt=\"imageSrc\" />\n </div>\n} @else if (videoIntroSrc()) {\n <div #videoPlayerCover class=\"backVideo\"></div>\n}\n", styles: [".backVideo{position:absolute;top:0;left:0;display:flex;justify-content:center;align-items:center;width:100%;height:100%;z-index:1}.backVideo video{position:absolute;width:100%;height:100%;object-fit:cover}.backImage{position:absolute;top:0;left:0;display:flex;justify-content:center;align-items:center;width:100%;height:100%;z-index:1}.backImage img{position:absolute;width:100%;height:100%;object-fit:cover}\n"] }]
6385
+ args: [{ selector: 'app-intro-src', imports: [NgOptimizedImage], changeDetection: ChangeDetectionStrategy.OnPush, template: "@let image = imageIntroSrc();\n@if (image) {\n <div class=\"backImage\">\n <img (error)=\"loadingImageError()\" [ngSrc]=\"image\" fill alt=\"imageSrc\" />\n </div>\n} @else if (videoIntroSrc()) {\n <div #videoPlayerCover class=\"backVideo\"></div>\n}\n", styles: [".backVideo{position:absolute;top:0;left:0;display:flex;justify-content:center;align-items:center;width:100%;height:100%;z-index:1}.backVideo video{position:absolute;width:100%;height:100%;object-fit:cover}.backImage{position:absolute;top:0;left:0;display:flex;justify-content:center;align-items:center;width:100%;height:100%;z-index:1}.backImage img{position:absolute;width:100%;height:100%;object-fit:cover}\n"] }]
6482
6386
  }], ctorParameters: () => [], propDecorators: { videoElement: [{ type: i0.ViewChild, args: ['videoPlayerCover', { isSignal: true }] }] } });
6483
6387
 
6484
6388
  class UnrealSceneComponent {
@@ -6498,19 +6402,19 @@ class UnrealSceneComponent {
6498
6402
  onMouseOut() {
6499
6403
  this.changeMouseOverScene.emit(false);
6500
6404
  }
6405
+ onContextMenu(event) {
6406
+ event.preventDefault();
6407
+ }
6501
6408
  constructor() {
6502
- this.isStudio = input(false, ...(ngDevMode ? [{ debugName: "isStudio" }] : /* istanbul ignore next */ []));
6503
6409
  this.useContainerAsSizeProvider = input(true, ...(ngDevMode ? [{ debugName: "useContainerAsSizeProvider" }] : /* istanbul ignore next */ []));
6504
- this.studioResolutionSize = input({
6410
+ this.isStudio = input(false, ...(ngDevMode ? [{ debugName: "isStudio" }] : /* istanbul ignore next */ []));
6411
+ this.resolutionSize = input({
6505
6412
  width: FULL_HD_WIDTH,
6506
6413
  height: FULL_HD_HEIGHT,
6507
- }, ...(ngDevMode ? [{ debugName: "studioResolutionSize" }] : /* istanbul ignore next */ []));
6508
- this.studioResolutionValues = toObservable(computed(() => {
6509
- const values = this.studioResolutionSize();
6510
- return { width: values.width, height: values.height };
6511
- }));
6512
- this.videoElement = viewChild('videoContainer', ...(ngDevMode ? [{ debugName: "videoElement" }] : /* istanbul ignore next */ []));
6414
+ }, ...(ngDevMode ? [{ debugName: "resolutionSize" }] : /* istanbul ignore next */ []));
6513
6415
  this.changeMouseOverScene = output();
6416
+ this.videoElement = viewChild('videoContainer', ...(ngDevMode ? [{ debugName: "videoElement" }] : /* istanbul ignore next */ []));
6417
+ this.resolutionValues$ = toObservable(this.resolutionSize);
6514
6418
  this.width = FULL_HD_WIDTH;
6515
6419
  this.height = FULL_HD_HEIGHT;
6516
6420
  this.actions$ = inject(Actions);
@@ -6521,38 +6425,34 @@ class UnrealSceneComponent {
6521
6425
  this.maxResolution = Truthy(new URLSearchParams(location.search).get('maxresolution'));
6522
6426
  this.commandsSender = inject(UnrealCommunicatorService);
6523
6427
  this.videoService = inject(VideoService);
6524
- this.element = inject(ElementRef);
6525
6428
  this.destroyRef = inject(DestroyRef);
6526
6429
  this.destroyRef.onDestroy(() => this.#store.dispatch(destroyUnrealScene()));
6527
6430
  effect(() => {
6528
6431
  const videoElement = this.videoElement();
6529
6432
  const isExistMatchUrls = this.isExistMatchUrls();
6530
- if (videoElement && isExistMatchUrls) {
6531
- this.#store.dispatch(initSignalling());
6532
- }
6433
+ untracked(() => {
6434
+ if (videoElement && isExistMatchUrls) {
6435
+ this.#store.dispatch(initSignalling());
6436
+ }
6437
+ });
6533
6438
  });
6534
6439
  afterNextRender({
6535
6440
  read: () => {
6536
6441
  const videoElement = this.videoElement();
6537
6442
  if (videoElement) {
6538
6443
  this.videoService.setContainer(videoElement.nativeElement);
6539
- if (this.element?.nativeElement) {
6540
- fromEvent(this.element?.nativeElement, 'contextmenu')
6541
- .pipe(takeUntilDestroyed(this.destroyRef))
6542
- .subscribe((event) => event.preventDefault());
6543
- }
6544
6444
  this.listenResizeValues();
6545
6445
  }
6546
6446
  },
6547
6447
  });
6548
6448
  }
6549
6449
  listenResizeValues() {
6550
- const customSizeInputChanged$ = this.studioResolutionValues.pipe(filter(Truthy), distinctUntilChanged(), filter(({ width, height }) => this.width !== width || this.height !== height), map$1(({ width, height }) => ({ width, height })), tap(({ width, height }) => {
6450
+ const customSizeInputChanged$ = this.resolutionValues$.pipe(filter(({ width, height }) => this.width !== width || this.height !== height), tap(({ width, height }) => {
6551
6451
  this.width = width;
6552
6452
  this.height = height;
6553
6453
  }));
6554
6454
  const resizeRequired$ = merge(this.actions$.pipe(ofType(videoStreamInitialized)), fromEvent(window, 'resize'));
6555
- const videoStreamSizeChanged$ = this.videoService?.videoStats$.pipe(filter(() => !this.isFreezeFrameLoading()), map$1((stats) => ({
6455
+ const videoStreamSizeChanged$ = this.videoService.videoStats$.pipe(filter(() => !this.isFreezeFrameLoading()), map$1((stats) => ({
6556
6456
  w: stats.aggregatedStats.frameWidth || 0,
6557
6457
  h: stats.aggregatedStats.frameHeight || 0,
6558
6458
  })), distinctUntilChanged((previous, current) => fpIsASameAsB(previous.w, current.w, SAME_SIZE_THRESHOLD) &&
@@ -6560,12 +6460,17 @@ class UnrealSceneComponent {
6560
6460
  merge(customSizeInputChanged$, resizeRequired$, videoStreamSizeChanged$)
6561
6461
  .pipe(debounceTime(DEBOUNCE_TO_MANY_RESIZE_CALLS), switchMap(() => {
6562
6462
  const key = Math.random();
6563
- // Monitoring the size of the video stream
6463
+ // Poll 3 times to capture late-arriving DOM measurements after layout settles
6564
6464
  return timer(0, 300).pipe(take(3), map$1(() => key));
6565
6465
  }), map$1((key) => ({ key, ...this.resizeValues })), distinctUntilChanged((previous, current) => fpIsASameAsB(previous.w, current.w, SAME_SIZE_THRESHOLD) &&
6566
6466
  fpIsASameAsB(previous.h, current.h, SAME_SIZE_THRESHOLD) &&
6567
- previous.key === current.key), tap((size) => this.adaptVideo(size)), distinctUntilChanged((previous, current) => fpIsASameAsB(previous.w, current.w, SAME_SIZE_THRESHOLD) &&
6568
- fpIsASameAsB(previous.h, current.h, SAME_SIZE_THRESHOLD)), combineLatestWith(this.#store
6467
+ previous.key === current.key), tap((size) => this.adaptVideo(size)),
6468
+ // Key excluded: only dispatch when actual pixel dimensions change
6469
+ distinctUntilChanged((previous, current) => fpIsASameAsB(previous.w, current.w, SAME_SIZE_THRESHOLD) &&
6470
+ fpIsASameAsB(previous.h, current.h, SAME_SIZE_THRESHOLD)),
6471
+ // Holds dispatch until data channel is up; retains true after disconnect
6472
+ // so resizes still reach Unreal — cleanup is handled by destroyUnrealScene
6473
+ combineLatestWith(this.#store
6569
6474
  .select(unrealFeature.selectDataChannelConnected)
6570
6475
  .pipe(filter(Truthy))), takeUntilDestroyed(this.destroyRef))
6571
6476
  .subscribe(([size]) => {
@@ -6593,11 +6498,13 @@ class UnrealSceneComponent {
6593
6498
  this.#store.dispatch(videoAdaptedToContainer());
6594
6499
  }
6595
6500
  getResizeValuesUniversal() {
6596
- let containerWidth = window.innerWidth || 2;
6597
- let containerHeight = window.innerHeight || 2;
6501
+ let containerWidth = window.innerWidth || MIN_CONTAINER_SIZE;
6502
+ let containerHeight = window.innerHeight || MIN_CONTAINER_SIZE;
6598
6503
  if (this.useContainerAsSizeProvider()) {
6599
- containerWidth = this.videoElement()?.nativeElement.offsetWidth || 2;
6600
- containerHeight = this.videoElement()?.nativeElement.offsetHeight || 2;
6504
+ containerWidth =
6505
+ this.videoElement()?.nativeElement.offsetWidth || MIN_CONTAINER_SIZE;
6506
+ containerHeight =
6507
+ this.videoElement()?.nativeElement.offsetHeight || MIN_CONTAINER_SIZE;
6601
6508
  }
6602
6509
  const pixelRatio = this.pixelRatio;
6603
6510
  return getResizeValuesUniversal({
@@ -6611,8 +6518,8 @@ class UnrealSceneComponent {
6611
6518
  const videoElement = this.videoElement()?.nativeElement;
6612
6519
  return getResizeStudioValues({
6613
6520
  pixelRatio: this.pixelRatio,
6614
- containerWidth: videoElement?.offsetWidth || 2,
6615
- containerHeight: videoElement?.offsetHeight || 2,
6521
+ containerWidth: videoElement?.offsetWidth || MIN_CONTAINER_SIZE,
6522
+ containerHeight: videoElement?.offsetHeight || MIN_CONTAINER_SIZE,
6616
6523
  studioWidth: this.width,
6617
6524
  studioHeight: this.height,
6618
6525
  maxResolution: this.maxResolution,
@@ -6630,7 +6537,7 @@ class UnrealSceneComponent {
6630
6537
  });
6631
6538
  }
6632
6539
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: UnrealSceneComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
6633
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.11", type: UnrealSceneComponent, isStandalone: true, selector: "app-unreal-scene", inputs: { isStudio: { classPropertyName: "isStudio", publicName: "isStudio", isSignal: true, isRequired: false, transformFunction: null }, useContainerAsSizeProvider: { classPropertyName: "useContainerAsSizeProvider", publicName: "useContainerAsSizeProvider", isSignal: true, isRequired: false, transformFunction: null }, studioResolutionSize: { classPropertyName: "studioResolutionSize", publicName: "studioResolutionSize", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { changeMouseOverScene: "changeMouseOverScene" }, host: { listeners: { "mouseover": "onMouseOver()", "mouseout": "onMouseOut()" } }, viewQueries: [{ propertyName: "videoElement", first: true, predicate: ["videoContainer"], descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"frame unreal-container\">\n <div #videoContainer id=\"player\"></div>\n\n <app-intro-src />\n <app-image-loading-src />\n <app-resume-stream-button />\n <app-unreal-status />\n <app-freeze-frame />\n <app-afk-timeout-modal />\n\n <ng-content select=\"app-pdf\"></ng-content>\n <ng-content select=\"app-show-case\"></ng-content>\n\n @if (isDevMode) {\n <app-video-stats />\n }\n</div>\n", styles: [".unreal-container{position:absolute;top:0;left:0;width:100%;height:100%;background-color:#fff}.unreal-container #player{position:absolute;width:100%;height:100%;display:flex;justify-content:center;align-items:center;overflow:hidden}.unreal-container.select{cursor:url('data:image/svg+xml,<svg width=\"24\" height=\"24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M15.15 21.375a1.423 1.423 0 0 1-1.15.063 1.487 1.487 0 0 1-.85-.788l-3-6.45-2.325 3.25c-.283.4-.658.525-1.125.375-.467-.15-.7-.466-.7-.95V4.05c0-.417.188-.717.563-.9.375-.183.729-.142 1.062.125l10.1 7.95c.383.284.496.65.337 1.1-.158.45-.479.675-.962.675h-4.2l2.975 6.375c.183.384.204.767.063 1.15a1.487 1.487 0 0 1-.788.85Z\" fill=\"%23000\"/><path d=\"m12.697 20.861.002.005c.237.495.617.852 1.128 1.04.515.191 1.039.16 1.539-.08a1.987 1.987 0 0 0 1.04-1.128 1.922 1.922 0 0 0-.079-1.536v-.003L13.684 13.5H17.1c.32 0 .628-.075.89-.26.263-.184.438-.447.544-.749.106-.3.135-.617.04-.925a1.458 1.458 0 0 0-.545-.738L7.936 2.883a1.51 1.51 0 0 0-.768-.336 1.471 1.471 0 0 0-.825.154 1.495 1.495 0 0 0-.626.547 1.492 1.492 0 0 0-.217.802v12.825c0 .324.08.634.272.897.193.261.467.43.775.53.31.099.632.12.942.016.31-.103.555-.313.743-.578h.001l1.825-2.552 2.639 5.673Z\" stroke=\"%23fff\" stroke-opacity=\".7\"/></svg>') 6 2,auto}.unreal-container.orbit{cursor:url('data:image/svg+xml,<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" fill=\"none\"><path fill=\"%23000\" d=\"M11.998 21.6c-1.966 0-3.737-.53-5.312-1.588-1.575-1.058-2.738-2.437-3.488-4.137a.852.852 0 0 1 .013-.713.867.867 0 0 1 .512-.487.86.86 0 0 1 .688.037c.225.109.387.28.487.513.617 1.35 1.559 2.45 2.825 3.3 1.267.85 2.692 1.275 4.275 1.275 1.384 0 2.646-.33 3.788-.988a7.985 7.985 0 0 0 2.762-2.612h-.85a.873.873 0 0 1-.64-.257.863.863 0 0 1-.26-.638.88.88 0 0 1 .26-.643.864.864 0 0 1 .64-.262h3c.255 0 .47.086.642.259a.87.87 0 0 1 .258.64v3a.874.874 0 0 1-.257.642.863.863 0 0 1-.637.259.88.88 0 0 1-.643-.259.865.865 0 0 1-.263-.641v-.725a9.95 9.95 0 0 1-3.35 2.925c-1.35.733-2.833 1.1-4.45 1.1Zm0-17.4c-1.383 0-2.645.33-3.787.987A7.985 7.985 0 0 0 5.448 7.8h.85c.255 0 .47.086.642.257a.863.863 0 0 1 .258.638.88.88 0 0 1-.258.642.864.864 0 0 1-.642.263h-3a.87.87 0 0 1-.64-.259.87.87 0 0 1-.26-.641v-3c0-.255.086-.469.258-.641a.863.863 0 0 1 .637-.26.88.88 0 0 1 .643.26.864.864 0 0 1 .262.64v.726A9.95 9.95 0 0 1 7.548 3.5c1.35-.733 2.834-1.1 4.45-1.1 2 0 3.788.542 5.363 1.625 1.575 1.083 2.736 2.49 3.482 4.22a.724.724 0 0 1-.045.646.926.926 0 0 1-.525.437.87.87 0 0 1-.687-.04.977.977 0 0 1-.488-.513c-.616-1.35-1.558-2.45-2.825-3.3-1.266-.85-2.691-1.275-4.275-1.275Zm0 10.8a2.893 2.893 0 0 1-2.125-.875A2.893 2.893 0 0 1 8.998 12c0-.833.292-1.542.875-2.125A2.893 2.893 0 0 1 11.998 9c.834 0 1.542.292 2.125.875.584.583.875 1.292.875 2.125s-.291 1.542-.875 2.125a2.893 2.893 0 0 1-2.125.875Z\"/><path stroke=\"%23fff\" stroke-opacity=\".7\" d=\"m2.739 16.072.002.005c.789 1.788 2.014 3.24 3.666 4.35 1.66 1.116 3.53 1.673 5.591 1.673 1.696 0 3.262-.386 4.69-1.16a10.54 10.54 0 0 0 2.714-2.097c.069.167.171.32.308.454.272.268.613.403.994.403.38 0 .722-.135.992-.406s.402-.614.402-.994v-3c0-.381-.134-.724-.405-.995a1.37 1.37 0 0 0-.995-.405h-3c-.383 0-.727.137-.997.411a1.38 1.38 0 0 0-.403.994c0 .38.135.723.407.992.238.237.532.369.858.397a7.552 7.552 0 0 1-2.027 1.685c-1.062.613-2.237.92-3.538.92-1.488 0-2.815-.397-3.996-1.19-1.19-.798-2.07-1.826-2.647-3.087-.147-.34-.393-.6-.727-.76a1.36 1.36 0 0 0-1.073-.058c-.37.133-.64.397-.8.754a1.351 1.351 0 0 0-.016 1.114Zm4.553-8.37a1.36 1.36 0 0 0-.858-.396A7.552 7.552 0 0 1 8.46 5.62c1.062-.613 2.237-.921 3.537-.921 1.489 0 2.816.398 3.997 1.19 1.19.8 2.07 1.827 2.646 3.088.148.34.394.6.728.76a1.37 1.37 0 0 0 1.068.063c.347-.12.62-.346.799-.668.187-.338.215-.71.072-1.072l-.003-.007-.003-.007c-.784-1.818-2.006-3.298-3.658-4.434C15.982 2.47 14.094 1.9 11.998 1.9c-1.695 0-3.261.385-4.688 1.16a10.536 10.536 0 0 0-2.715 2.096 1.364 1.364 0 0 0-.308-.453 1.38 1.38 0 0 0-.994-.403c-.38 0-.722.135-.992.406s-.403.614-.403.994v3c0 .38.135.724.406.995.27.27.614.405.994.405h3c.384 0 .727-.137.998-.412a1.38 1.38 0 0 0 .402-.993c0-.38-.135-.723-.406-.992ZM9.52 14.48a3.393 3.393 0 0 0 2.478 1.02c.965 0 1.8-.342 2.479-1.02a3.392 3.392 0 0 0 1.021-2.48c0-.963-.343-1.8-1.021-2.478A3.392 3.392 0 0 0 11.998 8.5c-.964 0-1.8.343-2.478 1.021A3.392 3.392 0 0 0 8.498 12c0 .964.344 1.8 1.022 2.479Z\"/></svg>') 12 12,auto}.unreal-container.pan{cursor:url('data:image/svg+xml,<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" fill=\"none\"><path fill=\"%23000\" d=\"M11.997 10.075a1.01 1.01 0 0 1-.738-.313 1.008 1.008 0 0 1-.312-.737V6.15l-.95.95a.998.998 0 0 1-.763.312 1.09 1.09 0 0 1-.763-.337 1.06 1.06 0 0 1-.324-.775c0-.3.108-.558.324-.775l2.776-2.775c.117-.116.237-.2.362-.25a1.04 1.04 0 0 1 .775 0c.125.05.246.134.363.25l2.8 2.8c.216.217.32.47.313.762a1.096 1.096 0 0 1-.338.763 1.058 1.058 0 0 1-.775.325c-.3 0-.558-.108-.775-.325l-.925-.925v2.875c0 .283-.104.53-.312.737a1.01 1.01 0 0 1-.738.313Zm0 11.5a1.04 1.04 0 0 1-.388-.075 1.103 1.103 0 0 1-.362-.25l-2.8-2.8a1.002 1.002 0 0 1-.313-.762c.009-.292.121-.546.337-.763.217-.217.475-.325.775-.325.3 0 .559.108.775.325l.926.925v-2.875c0-.283.104-.529.312-.737a1.01 1.01 0 0 1 .738-.313c.283 0 .529.104.738.313.208.208.312.454.312.737v2.875l.95-.95a.998.998 0 0 1 .762-.312c.292.008.546.12.763.337.216.217.324.475.324.775 0 .3-.108.559-.324.775l-2.775 2.775c-.117.116-.238.2-.363.25a1.04 1.04 0 0 1-.387.075Zm4.925-6.05a1.058 1.058 0 0 1-.326-.774c0-.3.109-.559.326-.776l.925-.925H14.97c-.283 0-.528-.104-.736-.312a1.01 1.01 0 0 1-.313-.738c0-.283.104-.53.313-.738.208-.208.453-.312.737-.312h2.875l-.95-.95a.998.998 0 0 1-.313-.762 1.09 1.09 0 0 1 .338-.764 1.06 1.06 0 0 1 .775-.324c.3 0 .558.108.775.324l2.774 2.776c.117.117.2.238.251.363a1.04 1.04 0 0 1 0 .775c-.05.124-.134.245-.25.362l-2.8 2.8c-.217.216-.471.32-.762.313a1.096 1.096 0 0 1-.763-.337Zm-11.4 0L2.746 12.75a1.103 1.103 0 0 1-.25-.362 1.04 1.04 0 0 1 0-.775c.05-.125.133-.246.25-.363l2.8-2.8c.216-.216.47-.32.762-.313.292.01.546.121.763.337.217.217.325.476.325.776 0 .3-.108.558-.325.775l-.925.925h2.875c.283 0 .529.104.737.312.208.209.313.455.313.738s-.105.53-.313.738a1.008 1.008 0 0 1-.737.312H6.147l.95.95c.217.217.32.471.312.762a1.09 1.09 0 0 1-.337.764 1.06 1.06 0 0 1-.775.324c-.3 0-.559-.108-.776-.324Z\"/><path stroke=\"%23fff\" stroke-opacity=\".7\" d=\"M9.22 7.912c.436.013.821-.15 1.13-.458l.097-.097v1.668c0 .422.16.793.458 1.09.298.299.67.46 1.092.46.422 0 .793-.161 1.091-.46.298-.297.459-.668.459-1.09V7.357l.071.072c.309.308.693.471 1.13.471.435 0 .82-.163 1.128-.471.301-.303.47-.676.483-1.102v-.001a1.501 1.501 0 0 0-.46-1.13l-2.799-2.8a1.6 1.6 0 0 0-.528-.36l-.003-.001a1.54 1.54 0 0 0-1.145 0l-.002.001a1.6 1.6 0 0 0-.529.36L8.118 5.172l-.001.001a1.56 1.56 0 0 0-.47 1.128c0 .435.163.82.47 1.128.303.303.676.472 1.103.484Zm0 0 .014-.5-.015.5Zm-1.311 6.865a1.497 1.497 0 0 0-.458-1.13l-.097-.097h1.668c.421 0 .793-.16 1.09-.458.298-.299.46-.67.46-1.092a1.51 1.51 0 0 0-.46-1.091 1.507 1.507 0 0 0-1.09-.459H7.354l.071-.072c.309-.308.472-.692.472-1.128 0-.436-.163-.82-.472-1.13a1.596 1.596 0 0 0-1.101-.482h-.002a1.501 1.501 0 0 0-1.129.459l-2.8 2.8a1.6 1.6 0 0 0-.36.528l-.001.003a1.54 1.54 0 0 0 0 1.144v.003c.081.2.208.375.362.529l2.774 2.775c.309.308.693.47 1.129.47.435 0 .82-.162 1.128-.47.303-.302.472-.676.484-1.102Zm0 0-.5-.014.5.015v-.001Zm3.038-8.627-.5.5.5.207V6.15Zm.475 15.814.002.001a1.54 1.54 0 0 0 1.145 0h.002c.2-.081.375-.208.53-.362l2.775-2.774a1.56 1.56 0 0 0 .47-1.129c0-.435-.162-.82-.47-1.128a1.589 1.589 0 0 0-1.103-.484 1.497 1.497 0 0 0-1.13.458l-.096.097v-1.668c0-.421-.161-.793-.459-1.09a1.509 1.509 0 0 0-1.091-.46c-.422 0-.794.161-1.092.46a1.507 1.507 0 0 0-.458 1.09v1.668l-.072-.072a1.558 1.558 0 0 0-1.129-.471c-.436 0-.82.163-1.128.471l-.001.001c-.301.302-.47.675-.483 1.101v.001c-.012.437.151.821.46 1.13l2.8 2.8c.153.153.328.28.528.36Zm5.146-6.085c.303.302.676.47 1.102.484a1.5 1.5 0 0 0 1.13-.46l2.8-2.8a1.6 1.6 0 0 0 .36-.528l.002-.003a1.54 1.54 0 0 0 0-1.144l-.001-.003a1.6 1.6 0 0 0-.361-.529l-2.774-2.775h-.001a1.56 1.56 0 0 0-1.128-.47c-.436 0-.82.162-1.128.47h-.001a1.59 1.59 0 0 0-.484 1.102c-.012.437.15.822.459 1.13l.097.097h-1.668c-.422 0-.793.16-1.09.459-.299.298-.46.67-.46 1.091 0 .422.161.793.46 1.092.297.297.668.458 1.09.458h1.668l-.072.072a1.558 1.558 0 0 0-.472 1.129c0 .435.164.82.472 1.128ZM6.147 13.05l.5.5.207-.5h-.707Z\"/></svg>') 12 12,auto}.unreal-container.dolly{cursor:url('data:image/svg+xml,<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" fill=\"none\"><mask id=\"a\" width=\"24\" height=\"24\" x=\"0\" y=\"0\" maskUnits=\"userSpaceOnUse\" style=\"mask-type:alpha\"><path fill=\"%23127392\" d=\"M0 0h24v24H0z\"/></mask><g mask=\"url(%23a)\"><path fill=\"%23000\" d=\"M12.002 20.575a1.026 1.026 0 0 1-.75-.325l-2.55-2.55a.933.933 0 0 1-.287-.713c.008-.275.113-.513.313-.713.2-.2.441-.3.725-.3.283 0 .524.1.724.3l.776.776V6.95l-.8.8c-.2.2-.438.296-.712.288a1.018 1.018 0 0 1-.713-.312c-.2-.2-.3-.442-.3-.726s.1-.525.3-.725l2.524-2.525a1.026 1.026 0 0 1 1.5 0l2.55 2.55c.2.2.296.438.289.713a1.023 1.023 0 0 1-.314.713c-.2.2-.441.3-.724.3a.988.988 0 0 1-.725-.3l-.775-.776v10.1l.8-.8c.2-.2.437-.296.712-.288.275.008.512.112.712.312.2.2.3.442.3.726s-.1.525-.3.725l-2.524 2.525a1.026 1.026 0 0 1-.75.325Z\"/><path stroke=\"%23fff\" stroke-opacity=\".7\" d=\"M13.553 8.153c.28.245.62.373 1 .373.415 0 .784-.153 1.078-.447.287-.287.446-.644.46-1.05v-.002a1.432 1.432 0 0 0-.435-1.08l-2.55-2.55-.354.353.354-.354a1.526 1.526 0 0 0-2.207 0L8.374 5.921A1.487 1.487 0 0 0 7.928 7c0 .416.152.785.446 1.08.288.287.645.446 1.052.457l4.127-.384Zm0 0v7.693m0-7.693a1.667 1.667 0 0 1-.079-.074l-3.021.075v7.693a1.482 1.482 0 0 0-1-.373c-.416 0-.785.153-1.079.447a1.522 1.522 0 0 0-.46 1.05v.002c-.011.417.14.786.435 1.08l2.55 2.55a1.527 1.527 0 0 0 2.207 0l2.525-2.524c.294-.294.446-.663.446-1.079 0-.416-.152-.785-.446-1.08a1.518 1.518 0 0 0-1.052-.457m0 0-.014.5.014-.5Zm0 0a1.424 1.424 0 0 0-1.027.383m0 0a1.583 1.583 0 0 0-.053.05l.354.354m-.3-.404v.104l.3.3m0 0-.3-.3v.6l.3-.3Z\"/></g></svg>%0A') 12 12,auto}.unreal-container.disabled:before{content:\"\";position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px)}#playerUI{position:absolute;z-index:10;width:100%}#statsContainer{display:none;text-align:left;background-color:#000c}#stats{padding:6px;font-weight:700;font-size:14px;color:#0f0}canvas{position:absolute;image-rendering:crisp-edges}#overlay{position:absolute;top:0;right:2%;z-index:100;padding:4px;border-top-width:0;-webkit-border-bottom-right-radius:5px;-moz-border-radius-bottomright:5px;border-bottom-right-radius:5px;-webkit-border-bottom-left-radius:5px;-moz-border-radius-bottomleft:5px;border-bottom-left-radius:5px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-touch-callout:none}.overlay{font-weight:lighter;font-family:var(--src-font-family-body)}#overlayButton:hover{cursor:pointer}#overlayButton{font-size:40px;text-align:right}#overlaySettings{display:none;width:300px;padding:4px}#videoMessageOverlay{position:absolute;z-index:20;width:100%;margin:auto;font-size:1.8em;font-family:var(--src-font-family-body)}#playButton{display:inline-block;height:auto}img#playButton{width:10%;max-width:241px}#UIInteraction{position:fixed}#UIInteractionButtonBoundary{padding:2px}#UIInteractionButton{cursor:pointer}#hiddenInput{position:absolute;left:-10%;width:0;opacity:0}#editTextButton{position:absolute;width:40px;height:40px}\n"], dependencies: [{ kind: "component", type: AfkTimeoutModalComponent, selector: "app-afk-timeout-modal" }, { kind: "component", type: UnrealStatusComponent, selector: "app-unreal-status" }, { kind: "component", type: FreezeFrameComponent, selector: "app-freeze-frame" }, { kind: "component", type: VideoStatsComponent, selector: "app-video-stats" }, { kind: "component", type: ResumeStreamButtonComponent, selector: "app-resume-stream-button" }, { kind: "component", type: ImageLoadingSrcComponent, selector: "app-image-loading-src" }, { kind: "component", type: IntroSrcComponent, selector: "app-intro-src" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
6540
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.11", type: UnrealSceneComponent, isStandalone: true, selector: "app-unreal-scene", inputs: { useContainerAsSizeProvider: { classPropertyName: "useContainerAsSizeProvider", publicName: "useContainerAsSizeProvider", isSignal: true, isRequired: false, transformFunction: null }, isStudio: { classPropertyName: "isStudio", publicName: "isStudio", isSignal: true, isRequired: false, transformFunction: null }, resolutionSize: { classPropertyName: "resolutionSize", publicName: "resolutionSize", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { changeMouseOverScene: "changeMouseOverScene" }, host: { listeners: { "mouseover": "onMouseOver()", "mouseout": "onMouseOut()", "contextmenu": "onContextMenu($event)" } }, viewQueries: [{ propertyName: "videoElement", first: true, predicate: ["videoContainer"], descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"frame unreal-container\">\n <div #videoContainer id=\"player\"></div>\n\n <app-intro-src />\n <app-image-loading-src />\n <app-resume-stream-button />\n <app-unreal-status />\n <app-freeze-frame />\n <app-afk-timeout-modal />\n\n <ng-content select=\"app-pdf\"></ng-content>\n <ng-content select=\"app-show-case\"></ng-content>\n\n @if (isDevMode) {\n <app-video-stats />\n }\n</div>\n", styles: [".unreal-container{position:absolute;top:0;left:0;width:100%;height:100%;background-color:#fff}.unreal-container #player{position:absolute;width:100%;height:100%;display:flex;justify-content:center;align-items:center;overflow:hidden}.unreal-container.select{cursor:url('data:image/svg+xml,<svg width=\"24\" height=\"24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M15.15 21.375a1.423 1.423 0 0 1-1.15.063 1.487 1.487 0 0 1-.85-.788l-3-6.45-2.325 3.25c-.283.4-.658.525-1.125.375-.467-.15-.7-.466-.7-.95V4.05c0-.417.188-.717.563-.9.375-.183.729-.142 1.062.125l10.1 7.95c.383.284.496.65.337 1.1-.158.45-.479.675-.962.675h-4.2l2.975 6.375c.183.384.204.767.063 1.15a1.487 1.487 0 0 1-.788.85Z\" fill=\"%23000\"/><path d=\"m12.697 20.861.002.005c.237.495.617.852 1.128 1.04.515.191 1.039.16 1.539-.08a1.987 1.987 0 0 0 1.04-1.128 1.922 1.922 0 0 0-.079-1.536v-.003L13.684 13.5H17.1c.32 0 .628-.075.89-.26.263-.184.438-.447.544-.749.106-.3.135-.617.04-.925a1.458 1.458 0 0 0-.545-.738L7.936 2.883a1.51 1.51 0 0 0-.768-.336 1.471 1.471 0 0 0-.825.154 1.495 1.495 0 0 0-.626.547 1.492 1.492 0 0 0-.217.802v12.825c0 .324.08.634.272.897.193.261.467.43.775.53.31.099.632.12.942.016.31-.103.555-.313.743-.578h.001l1.825-2.552 2.639 5.673Z\" stroke=\"%23fff\" stroke-opacity=\".7\"/></svg>') 6 2,auto}.unreal-container.orbit{cursor:url('data:image/svg+xml,<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" fill=\"none\"><path fill=\"%23000\" d=\"M11.998 21.6c-1.966 0-3.737-.53-5.312-1.588-1.575-1.058-2.738-2.437-3.488-4.137a.852.852 0 0 1 .013-.713.867.867 0 0 1 .512-.487.86.86 0 0 1 .688.037c.225.109.387.28.487.513.617 1.35 1.559 2.45 2.825 3.3 1.267.85 2.692 1.275 4.275 1.275 1.384 0 2.646-.33 3.788-.988a7.985 7.985 0 0 0 2.762-2.612h-.85a.873.873 0 0 1-.64-.257.863.863 0 0 1-.26-.638.88.88 0 0 1 .26-.643.864.864 0 0 1 .64-.262h3c.255 0 .47.086.642.259a.87.87 0 0 1 .258.64v3a.874.874 0 0 1-.257.642.863.863 0 0 1-.637.259.88.88 0 0 1-.643-.259.865.865 0 0 1-.263-.641v-.725a9.95 9.95 0 0 1-3.35 2.925c-1.35.733-2.833 1.1-4.45 1.1Zm0-17.4c-1.383 0-2.645.33-3.787.987A7.985 7.985 0 0 0 5.448 7.8h.85c.255 0 .47.086.642.257a.863.863 0 0 1 .258.638.88.88 0 0 1-.258.642.864.864 0 0 1-.642.263h-3a.87.87 0 0 1-.64-.259.87.87 0 0 1-.26-.641v-3c0-.255.086-.469.258-.641a.863.863 0 0 1 .637-.26.88.88 0 0 1 .643.26.864.864 0 0 1 .262.64v.726A9.95 9.95 0 0 1 7.548 3.5c1.35-.733 2.834-1.1 4.45-1.1 2 0 3.788.542 5.363 1.625 1.575 1.083 2.736 2.49 3.482 4.22a.724.724 0 0 1-.045.646.926.926 0 0 1-.525.437.87.87 0 0 1-.687-.04.977.977 0 0 1-.488-.513c-.616-1.35-1.558-2.45-2.825-3.3-1.266-.85-2.691-1.275-4.275-1.275Zm0 10.8a2.893 2.893 0 0 1-2.125-.875A2.893 2.893 0 0 1 8.998 12c0-.833.292-1.542.875-2.125A2.893 2.893 0 0 1 11.998 9c.834 0 1.542.292 2.125.875.584.583.875 1.292.875 2.125s-.291 1.542-.875 2.125a2.893 2.893 0 0 1-2.125.875Z\"/><path stroke=\"%23fff\" stroke-opacity=\".7\" d=\"m2.739 16.072.002.005c.789 1.788 2.014 3.24 3.666 4.35 1.66 1.116 3.53 1.673 5.591 1.673 1.696 0 3.262-.386 4.69-1.16a10.54 10.54 0 0 0 2.714-2.097c.069.167.171.32.308.454.272.268.613.403.994.403.38 0 .722-.135.992-.406s.402-.614.402-.994v-3c0-.381-.134-.724-.405-.995a1.37 1.37 0 0 0-.995-.405h-3c-.383 0-.727.137-.997.411a1.38 1.38 0 0 0-.403.994c0 .38.135.723.407.992.238.237.532.369.858.397a7.552 7.552 0 0 1-2.027 1.685c-1.062.613-2.237.92-3.538.92-1.488 0-2.815-.397-3.996-1.19-1.19-.798-2.07-1.826-2.647-3.087-.147-.34-.393-.6-.727-.76a1.36 1.36 0 0 0-1.073-.058c-.37.133-.64.397-.8.754a1.351 1.351 0 0 0-.016 1.114Zm4.553-8.37a1.36 1.36 0 0 0-.858-.396A7.552 7.552 0 0 1 8.46 5.62c1.062-.613 2.237-.921 3.537-.921 1.489 0 2.816.398 3.997 1.19 1.19.8 2.07 1.827 2.646 3.088.148.34.394.6.728.76a1.37 1.37 0 0 0 1.068.063c.347-.12.62-.346.799-.668.187-.338.215-.71.072-1.072l-.003-.007-.003-.007c-.784-1.818-2.006-3.298-3.658-4.434C15.982 2.47 14.094 1.9 11.998 1.9c-1.695 0-3.261.385-4.688 1.16a10.536 10.536 0 0 0-2.715 2.096 1.364 1.364 0 0 0-.308-.453 1.38 1.38 0 0 0-.994-.403c-.38 0-.722.135-.992.406s-.403.614-.403.994v3c0 .38.135.724.406.995.27.27.614.405.994.405h3c.384 0 .727-.137.998-.412a1.38 1.38 0 0 0 .402-.993c0-.38-.135-.723-.406-.992ZM9.52 14.48a3.393 3.393 0 0 0 2.478 1.02c.965 0 1.8-.342 2.479-1.02a3.392 3.392 0 0 0 1.021-2.48c0-.963-.343-1.8-1.021-2.478A3.392 3.392 0 0 0 11.998 8.5c-.964 0-1.8.343-2.478 1.021A3.392 3.392 0 0 0 8.498 12c0 .964.344 1.8 1.022 2.479Z\"/></svg>') 12 12,auto}.unreal-container.pan{cursor:url('data:image/svg+xml,<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" fill=\"none\"><path fill=\"%23000\" d=\"M11.997 10.075a1.01 1.01 0 0 1-.738-.313 1.008 1.008 0 0 1-.312-.737V6.15l-.95.95a.998.998 0 0 1-.763.312 1.09 1.09 0 0 1-.763-.337 1.06 1.06 0 0 1-.324-.775c0-.3.108-.558.324-.775l2.776-2.775c.117-.116.237-.2.362-.25a1.04 1.04 0 0 1 .775 0c.125.05.246.134.363.25l2.8 2.8c.216.217.32.47.313.762a1.096 1.096 0 0 1-.338.763 1.058 1.058 0 0 1-.775.325c-.3 0-.558-.108-.775-.325l-.925-.925v2.875c0 .283-.104.53-.312.737a1.01 1.01 0 0 1-.738.313Zm0 11.5a1.04 1.04 0 0 1-.388-.075 1.103 1.103 0 0 1-.362-.25l-2.8-2.8a1.002 1.002 0 0 1-.313-.762c.009-.292.121-.546.337-.763.217-.217.475-.325.775-.325.3 0 .559.108.775.325l.926.925v-2.875c0-.283.104-.529.312-.737a1.01 1.01 0 0 1 .738-.313c.283 0 .529.104.738.313.208.208.312.454.312.737v2.875l.95-.95a.998.998 0 0 1 .762-.312c.292.008.546.12.763.337.216.217.324.475.324.775 0 .3-.108.559-.324.775l-2.775 2.775c-.117.116-.238.2-.363.25a1.04 1.04 0 0 1-.387.075Zm4.925-6.05a1.058 1.058 0 0 1-.326-.774c0-.3.109-.559.326-.776l.925-.925H14.97c-.283 0-.528-.104-.736-.312a1.01 1.01 0 0 1-.313-.738c0-.283.104-.53.313-.738.208-.208.453-.312.737-.312h2.875l-.95-.95a.998.998 0 0 1-.313-.762 1.09 1.09 0 0 1 .338-.764 1.06 1.06 0 0 1 .775-.324c.3 0 .558.108.775.324l2.774 2.776c.117.117.2.238.251.363a1.04 1.04 0 0 1 0 .775c-.05.124-.134.245-.25.362l-2.8 2.8c-.217.216-.471.32-.762.313a1.096 1.096 0 0 1-.763-.337Zm-11.4 0L2.746 12.75a1.103 1.103 0 0 1-.25-.362 1.04 1.04 0 0 1 0-.775c.05-.125.133-.246.25-.363l2.8-2.8c.216-.216.47-.32.762-.313.292.01.546.121.763.337.217.217.325.476.325.776 0 .3-.108.558-.325.775l-.925.925h2.875c.283 0 .529.104.737.312.208.209.313.455.313.738s-.105.53-.313.738a1.008 1.008 0 0 1-.737.312H6.147l.95.95c.217.217.32.471.312.762a1.09 1.09 0 0 1-.337.764 1.06 1.06 0 0 1-.775.324c-.3 0-.559-.108-.776-.324Z\"/><path stroke=\"%23fff\" stroke-opacity=\".7\" d=\"M9.22 7.912c.436.013.821-.15 1.13-.458l.097-.097v1.668c0 .422.16.793.458 1.09.298.299.67.46 1.092.46.422 0 .793-.161 1.091-.46.298-.297.459-.668.459-1.09V7.357l.071.072c.309.308.693.471 1.13.471.435 0 .82-.163 1.128-.471.301-.303.47-.676.483-1.102v-.001a1.501 1.501 0 0 0-.46-1.13l-2.799-2.8a1.6 1.6 0 0 0-.528-.36l-.003-.001a1.54 1.54 0 0 0-1.145 0l-.002.001a1.6 1.6 0 0 0-.529.36L8.118 5.172l-.001.001a1.56 1.56 0 0 0-.47 1.128c0 .435.163.82.47 1.128.303.303.676.472 1.103.484Zm0 0 .014-.5-.015.5Zm-1.311 6.865a1.497 1.497 0 0 0-.458-1.13l-.097-.097h1.668c.421 0 .793-.16 1.09-.458.298-.299.46-.67.46-1.092a1.51 1.51 0 0 0-.46-1.091 1.507 1.507 0 0 0-1.09-.459H7.354l.071-.072c.309-.308.472-.692.472-1.128 0-.436-.163-.82-.472-1.13a1.596 1.596 0 0 0-1.101-.482h-.002a1.501 1.501 0 0 0-1.129.459l-2.8 2.8a1.6 1.6 0 0 0-.36.528l-.001.003a1.54 1.54 0 0 0 0 1.144v.003c.081.2.208.375.362.529l2.774 2.775c.309.308.693.47 1.129.47.435 0 .82-.162 1.128-.47.303-.302.472-.676.484-1.102Zm0 0-.5-.014.5.015v-.001Zm3.038-8.627-.5.5.5.207V6.15Zm.475 15.814.002.001a1.54 1.54 0 0 0 1.145 0h.002c.2-.081.375-.208.53-.362l2.775-2.774a1.56 1.56 0 0 0 .47-1.129c0-.435-.162-.82-.47-1.128a1.589 1.589 0 0 0-1.103-.484 1.497 1.497 0 0 0-1.13.458l-.096.097v-1.668c0-.421-.161-.793-.459-1.09a1.509 1.509 0 0 0-1.091-.46c-.422 0-.794.161-1.092.46a1.507 1.507 0 0 0-.458 1.09v1.668l-.072-.072a1.558 1.558 0 0 0-1.129-.471c-.436 0-.82.163-1.128.471l-.001.001c-.301.302-.47.675-.483 1.101v.001c-.012.437.151.821.46 1.13l2.8 2.8c.153.153.328.28.528.36Zm5.146-6.085c.303.302.676.47 1.102.484a1.5 1.5 0 0 0 1.13-.46l2.8-2.8a1.6 1.6 0 0 0 .36-.528l.002-.003a1.54 1.54 0 0 0 0-1.144l-.001-.003a1.6 1.6 0 0 0-.361-.529l-2.774-2.775h-.001a1.56 1.56 0 0 0-1.128-.47c-.436 0-.82.162-1.128.47h-.001a1.59 1.59 0 0 0-.484 1.102c-.012.437.15.822.459 1.13l.097.097h-1.668c-.422 0-.793.16-1.09.459-.299.298-.46.67-.46 1.091 0 .422.161.793.46 1.092.297.297.668.458 1.09.458h1.668l-.072.072a1.558 1.558 0 0 0-.472 1.129c0 .435.164.82.472 1.128ZM6.147 13.05l.5.5.207-.5h-.707Z\"/></svg>') 12 12,auto}.unreal-container.dolly{cursor:url('data:image/svg+xml,<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" fill=\"none\"><mask id=\"a\" width=\"24\" height=\"24\" x=\"0\" y=\"0\" maskUnits=\"userSpaceOnUse\" style=\"mask-type:alpha\"><path fill=\"%23127392\" d=\"M0 0h24v24H0z\"/></mask><g mask=\"url(%23a)\"><path fill=\"%23000\" d=\"M12.002 20.575a1.026 1.026 0 0 1-.75-.325l-2.55-2.55a.933.933 0 0 1-.287-.713c.008-.275.113-.513.313-.713.2-.2.441-.3.725-.3.283 0 .524.1.724.3l.776.776V6.95l-.8.8c-.2.2-.438.296-.712.288a1.018 1.018 0 0 1-.713-.312c-.2-.2-.3-.442-.3-.726s.1-.525.3-.725l2.524-2.525a1.026 1.026 0 0 1 1.5 0l2.55 2.55c.2.2.296.438.289.713a1.023 1.023 0 0 1-.314.713c-.2.2-.441.3-.724.3a.988.988 0 0 1-.725-.3l-.775-.776v10.1l.8-.8c.2-.2.437-.296.712-.288.275.008.512.112.712.312.2.2.3.442.3.726s-.1.525-.3.725l-2.524 2.525a1.026 1.026 0 0 1-.75.325Z\"/><path stroke=\"%23fff\" stroke-opacity=\".7\" d=\"M13.553 8.153c.28.245.62.373 1 .373.415 0 .784-.153 1.078-.447.287-.287.446-.644.46-1.05v-.002a1.432 1.432 0 0 0-.435-1.08l-2.55-2.55-.354.353.354-.354a1.526 1.526 0 0 0-2.207 0L8.374 5.921A1.487 1.487 0 0 0 7.928 7c0 .416.152.785.446 1.08.288.287.645.446 1.052.457l4.127-.384Zm0 0v7.693m0-7.693a1.667 1.667 0 0 1-.079-.074l-3.021.075v7.693a1.482 1.482 0 0 0-1-.373c-.416 0-.785.153-1.079.447a1.522 1.522 0 0 0-.46 1.05v.002c-.011.417.14.786.435 1.08l2.55 2.55a1.527 1.527 0 0 0 2.207 0l2.525-2.524c.294-.294.446-.663.446-1.079 0-.416-.152-.785-.446-1.08a1.518 1.518 0 0 0-1.052-.457m0 0-.014.5.014-.5Zm0 0a1.424 1.424 0 0 0-1.027.383m0 0a1.583 1.583 0 0 0-.053.05l.354.354m-.3-.404v.104l.3.3m0 0-.3-.3v.6l.3-.3Z\"/></g></svg>%0A') 12 12,auto}.unreal-container.disabled:before{content:\"\";position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px)}#playerUI{position:absolute;z-index:10;width:100%}#statsContainer{display:none;text-align:left;background-color:#000c}#stats{padding:6px;font-weight:700;font-size:14px;color:#0f0}canvas{position:absolute;image-rendering:crisp-edges}#overlay{position:absolute;top:0;right:2%;z-index:100;padding:4px;border-top-width:0;-webkit-border-bottom-right-radius:5px;-moz-border-radius-bottomright:5px;border-bottom-right-radius:5px;-webkit-border-bottom-left-radius:5px;-moz-border-radius-bottomleft:5px;border-bottom-left-radius:5px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-touch-callout:none}.overlay{font-weight:lighter;font-family:var(--src-font-family-body)}#overlayButton:hover{cursor:pointer}#overlayButton{font-size:40px;text-align:right}#overlaySettings{display:none;width:300px;padding:4px}#videoMessageOverlay{position:absolute;z-index:20;width:100%;margin:auto;font-size:1.8em;font-family:var(--src-font-family-body)}#playButton{display:inline-block;height:auto}img#playButton{width:10%;max-width:241px}#UIInteraction{position:fixed}#UIInteractionButtonBoundary{padding:2px}#UIInteractionButton{cursor:pointer}#hiddenInput{position:absolute;left:-10%;width:0;opacity:0}#editTextButton{position:absolute;width:40px;height:40px}\n"], dependencies: [{ kind: "component", type: AfkTimeoutModalComponent, selector: "app-afk-timeout-modal" }, { kind: "component", type: UnrealStatusComponent, selector: "app-unreal-status" }, { kind: "component", type: FreezeFrameComponent, selector: "app-freeze-frame" }, { kind: "component", type: VideoStatsComponent, selector: "app-video-stats" }, { kind: "component", type: ResumeStreamButtonComponent, selector: "app-resume-stream-button" }, { kind: "component", type: ImageLoadingSrcComponent, selector: "app-image-loading-src" }, { kind: "component", type: IntroSrcComponent, selector: "app-intro-src" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
6634
6541
  }
6635
6542
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: UnrealSceneComponent, decorators: [{
6636
6543
  type: Component,
@@ -6643,12 +6550,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImpo
6643
6550
  ImageLoadingSrcComponent,
6644
6551
  IntroSrcComponent,
6645
6552
  ], template: "<div class=\"frame unreal-container\">\n <div #videoContainer id=\"player\"></div>\n\n <app-intro-src />\n <app-image-loading-src />\n <app-resume-stream-button />\n <app-unreal-status />\n <app-freeze-frame />\n <app-afk-timeout-modal />\n\n <ng-content select=\"app-pdf\"></ng-content>\n <ng-content select=\"app-show-case\"></ng-content>\n\n @if (isDevMode) {\n <app-video-stats />\n }\n</div>\n", styles: [".unreal-container{position:absolute;top:0;left:0;width:100%;height:100%;background-color:#fff}.unreal-container #player{position:absolute;width:100%;height:100%;display:flex;justify-content:center;align-items:center;overflow:hidden}.unreal-container.select{cursor:url('data:image/svg+xml,<svg width=\"24\" height=\"24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M15.15 21.375a1.423 1.423 0 0 1-1.15.063 1.487 1.487 0 0 1-.85-.788l-3-6.45-2.325 3.25c-.283.4-.658.525-1.125.375-.467-.15-.7-.466-.7-.95V4.05c0-.417.188-.717.563-.9.375-.183.729-.142 1.062.125l10.1 7.95c.383.284.496.65.337 1.1-.158.45-.479.675-.962.675h-4.2l2.975 6.375c.183.384.204.767.063 1.15a1.487 1.487 0 0 1-.788.85Z\" fill=\"%23000\"/><path d=\"m12.697 20.861.002.005c.237.495.617.852 1.128 1.04.515.191 1.039.16 1.539-.08a1.987 1.987 0 0 0 1.04-1.128 1.922 1.922 0 0 0-.079-1.536v-.003L13.684 13.5H17.1c.32 0 .628-.075.89-.26.263-.184.438-.447.544-.749.106-.3.135-.617.04-.925a1.458 1.458 0 0 0-.545-.738L7.936 2.883a1.51 1.51 0 0 0-.768-.336 1.471 1.471 0 0 0-.825.154 1.495 1.495 0 0 0-.626.547 1.492 1.492 0 0 0-.217.802v12.825c0 .324.08.634.272.897.193.261.467.43.775.53.31.099.632.12.942.016.31-.103.555-.313.743-.578h.001l1.825-2.552 2.639 5.673Z\" stroke=\"%23fff\" stroke-opacity=\".7\"/></svg>') 6 2,auto}.unreal-container.orbit{cursor:url('data:image/svg+xml,<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" fill=\"none\"><path fill=\"%23000\" d=\"M11.998 21.6c-1.966 0-3.737-.53-5.312-1.588-1.575-1.058-2.738-2.437-3.488-4.137a.852.852 0 0 1 .013-.713.867.867 0 0 1 .512-.487.86.86 0 0 1 .688.037c.225.109.387.28.487.513.617 1.35 1.559 2.45 2.825 3.3 1.267.85 2.692 1.275 4.275 1.275 1.384 0 2.646-.33 3.788-.988a7.985 7.985 0 0 0 2.762-2.612h-.85a.873.873 0 0 1-.64-.257.863.863 0 0 1-.26-.638.88.88 0 0 1 .26-.643.864.864 0 0 1 .64-.262h3c.255 0 .47.086.642.259a.87.87 0 0 1 .258.64v3a.874.874 0 0 1-.257.642.863.863 0 0 1-.637.259.88.88 0 0 1-.643-.259.865.865 0 0 1-.263-.641v-.725a9.95 9.95 0 0 1-3.35 2.925c-1.35.733-2.833 1.1-4.45 1.1Zm0-17.4c-1.383 0-2.645.33-3.787.987A7.985 7.985 0 0 0 5.448 7.8h.85c.255 0 .47.086.642.257a.863.863 0 0 1 .258.638.88.88 0 0 1-.258.642.864.864 0 0 1-.642.263h-3a.87.87 0 0 1-.64-.259.87.87 0 0 1-.26-.641v-3c0-.255.086-.469.258-.641a.863.863 0 0 1 .637-.26.88.88 0 0 1 .643.26.864.864 0 0 1 .262.64v.726A9.95 9.95 0 0 1 7.548 3.5c1.35-.733 2.834-1.1 4.45-1.1 2 0 3.788.542 5.363 1.625 1.575 1.083 2.736 2.49 3.482 4.22a.724.724 0 0 1-.045.646.926.926 0 0 1-.525.437.87.87 0 0 1-.687-.04.977.977 0 0 1-.488-.513c-.616-1.35-1.558-2.45-2.825-3.3-1.266-.85-2.691-1.275-4.275-1.275Zm0 10.8a2.893 2.893 0 0 1-2.125-.875A2.893 2.893 0 0 1 8.998 12c0-.833.292-1.542.875-2.125A2.893 2.893 0 0 1 11.998 9c.834 0 1.542.292 2.125.875.584.583.875 1.292.875 2.125s-.291 1.542-.875 2.125a2.893 2.893 0 0 1-2.125.875Z\"/><path stroke=\"%23fff\" stroke-opacity=\".7\" d=\"m2.739 16.072.002.005c.789 1.788 2.014 3.24 3.666 4.35 1.66 1.116 3.53 1.673 5.591 1.673 1.696 0 3.262-.386 4.69-1.16a10.54 10.54 0 0 0 2.714-2.097c.069.167.171.32.308.454.272.268.613.403.994.403.38 0 .722-.135.992-.406s.402-.614.402-.994v-3c0-.381-.134-.724-.405-.995a1.37 1.37 0 0 0-.995-.405h-3c-.383 0-.727.137-.997.411a1.38 1.38 0 0 0-.403.994c0 .38.135.723.407.992.238.237.532.369.858.397a7.552 7.552 0 0 1-2.027 1.685c-1.062.613-2.237.92-3.538.92-1.488 0-2.815-.397-3.996-1.19-1.19-.798-2.07-1.826-2.647-3.087-.147-.34-.393-.6-.727-.76a1.36 1.36 0 0 0-1.073-.058c-.37.133-.64.397-.8.754a1.351 1.351 0 0 0-.016 1.114Zm4.553-8.37a1.36 1.36 0 0 0-.858-.396A7.552 7.552 0 0 1 8.46 5.62c1.062-.613 2.237-.921 3.537-.921 1.489 0 2.816.398 3.997 1.19 1.19.8 2.07 1.827 2.646 3.088.148.34.394.6.728.76a1.37 1.37 0 0 0 1.068.063c.347-.12.62-.346.799-.668.187-.338.215-.71.072-1.072l-.003-.007-.003-.007c-.784-1.818-2.006-3.298-3.658-4.434C15.982 2.47 14.094 1.9 11.998 1.9c-1.695 0-3.261.385-4.688 1.16a10.536 10.536 0 0 0-2.715 2.096 1.364 1.364 0 0 0-.308-.453 1.38 1.38 0 0 0-.994-.403c-.38 0-.722.135-.992.406s-.403.614-.403.994v3c0 .38.135.724.406.995.27.27.614.405.994.405h3c.384 0 .727-.137.998-.412a1.38 1.38 0 0 0 .402-.993c0-.38-.135-.723-.406-.992ZM9.52 14.48a3.393 3.393 0 0 0 2.478 1.02c.965 0 1.8-.342 2.479-1.02a3.392 3.392 0 0 0 1.021-2.48c0-.963-.343-1.8-1.021-2.478A3.392 3.392 0 0 0 11.998 8.5c-.964 0-1.8.343-2.478 1.021A3.392 3.392 0 0 0 8.498 12c0 .964.344 1.8 1.022 2.479Z\"/></svg>') 12 12,auto}.unreal-container.pan{cursor:url('data:image/svg+xml,<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" fill=\"none\"><path fill=\"%23000\" d=\"M11.997 10.075a1.01 1.01 0 0 1-.738-.313 1.008 1.008 0 0 1-.312-.737V6.15l-.95.95a.998.998 0 0 1-.763.312 1.09 1.09 0 0 1-.763-.337 1.06 1.06 0 0 1-.324-.775c0-.3.108-.558.324-.775l2.776-2.775c.117-.116.237-.2.362-.25a1.04 1.04 0 0 1 .775 0c.125.05.246.134.363.25l2.8 2.8c.216.217.32.47.313.762a1.096 1.096 0 0 1-.338.763 1.058 1.058 0 0 1-.775.325c-.3 0-.558-.108-.775-.325l-.925-.925v2.875c0 .283-.104.53-.312.737a1.01 1.01 0 0 1-.738.313Zm0 11.5a1.04 1.04 0 0 1-.388-.075 1.103 1.103 0 0 1-.362-.25l-2.8-2.8a1.002 1.002 0 0 1-.313-.762c.009-.292.121-.546.337-.763.217-.217.475-.325.775-.325.3 0 .559.108.775.325l.926.925v-2.875c0-.283.104-.529.312-.737a1.01 1.01 0 0 1 .738-.313c.283 0 .529.104.738.313.208.208.312.454.312.737v2.875l.95-.95a.998.998 0 0 1 .762-.312c.292.008.546.12.763.337.216.217.324.475.324.775 0 .3-.108.559-.324.775l-2.775 2.775c-.117.116-.238.2-.363.25a1.04 1.04 0 0 1-.387.075Zm4.925-6.05a1.058 1.058 0 0 1-.326-.774c0-.3.109-.559.326-.776l.925-.925H14.97c-.283 0-.528-.104-.736-.312a1.01 1.01 0 0 1-.313-.738c0-.283.104-.53.313-.738.208-.208.453-.312.737-.312h2.875l-.95-.95a.998.998 0 0 1-.313-.762 1.09 1.09 0 0 1 .338-.764 1.06 1.06 0 0 1 .775-.324c.3 0 .558.108.775.324l2.774 2.776c.117.117.2.238.251.363a1.04 1.04 0 0 1 0 .775c-.05.124-.134.245-.25.362l-2.8 2.8c-.217.216-.471.32-.762.313a1.096 1.096 0 0 1-.763-.337Zm-11.4 0L2.746 12.75a1.103 1.103 0 0 1-.25-.362 1.04 1.04 0 0 1 0-.775c.05-.125.133-.246.25-.363l2.8-2.8c.216-.216.47-.32.762-.313.292.01.546.121.763.337.217.217.325.476.325.776 0 .3-.108.558-.325.775l-.925.925h2.875c.283 0 .529.104.737.312.208.209.313.455.313.738s-.105.53-.313.738a1.008 1.008 0 0 1-.737.312H6.147l.95.95c.217.217.32.471.312.762a1.09 1.09 0 0 1-.337.764 1.06 1.06 0 0 1-.775.324c-.3 0-.559-.108-.776-.324Z\"/><path stroke=\"%23fff\" stroke-opacity=\".7\" d=\"M9.22 7.912c.436.013.821-.15 1.13-.458l.097-.097v1.668c0 .422.16.793.458 1.09.298.299.67.46 1.092.46.422 0 .793-.161 1.091-.46.298-.297.459-.668.459-1.09V7.357l.071.072c.309.308.693.471 1.13.471.435 0 .82-.163 1.128-.471.301-.303.47-.676.483-1.102v-.001a1.501 1.501 0 0 0-.46-1.13l-2.799-2.8a1.6 1.6 0 0 0-.528-.36l-.003-.001a1.54 1.54 0 0 0-1.145 0l-.002.001a1.6 1.6 0 0 0-.529.36L8.118 5.172l-.001.001a1.56 1.56 0 0 0-.47 1.128c0 .435.163.82.47 1.128.303.303.676.472 1.103.484Zm0 0 .014-.5-.015.5Zm-1.311 6.865a1.497 1.497 0 0 0-.458-1.13l-.097-.097h1.668c.421 0 .793-.16 1.09-.458.298-.299.46-.67.46-1.092a1.51 1.51 0 0 0-.46-1.091 1.507 1.507 0 0 0-1.09-.459H7.354l.071-.072c.309-.308.472-.692.472-1.128 0-.436-.163-.82-.472-1.13a1.596 1.596 0 0 0-1.101-.482h-.002a1.501 1.501 0 0 0-1.129.459l-2.8 2.8a1.6 1.6 0 0 0-.36.528l-.001.003a1.54 1.54 0 0 0 0 1.144v.003c.081.2.208.375.362.529l2.774 2.775c.309.308.693.47 1.129.47.435 0 .82-.162 1.128-.47.303-.302.472-.676.484-1.102Zm0 0-.5-.014.5.015v-.001Zm3.038-8.627-.5.5.5.207V6.15Zm.475 15.814.002.001a1.54 1.54 0 0 0 1.145 0h.002c.2-.081.375-.208.53-.362l2.775-2.774a1.56 1.56 0 0 0 .47-1.129c0-.435-.162-.82-.47-1.128a1.589 1.589 0 0 0-1.103-.484 1.497 1.497 0 0 0-1.13.458l-.096.097v-1.668c0-.421-.161-.793-.459-1.09a1.509 1.509 0 0 0-1.091-.46c-.422 0-.794.161-1.092.46a1.507 1.507 0 0 0-.458 1.09v1.668l-.072-.072a1.558 1.558 0 0 0-1.129-.471c-.436 0-.82.163-1.128.471l-.001.001c-.301.302-.47.675-.483 1.101v.001c-.012.437.151.821.46 1.13l2.8 2.8c.153.153.328.28.528.36Zm5.146-6.085c.303.302.676.47 1.102.484a1.5 1.5 0 0 0 1.13-.46l2.8-2.8a1.6 1.6 0 0 0 .36-.528l.002-.003a1.54 1.54 0 0 0 0-1.144l-.001-.003a1.6 1.6 0 0 0-.361-.529l-2.774-2.775h-.001a1.56 1.56 0 0 0-1.128-.47c-.436 0-.82.162-1.128.47h-.001a1.59 1.59 0 0 0-.484 1.102c-.012.437.15.822.459 1.13l.097.097h-1.668c-.422 0-.793.16-1.09.459-.299.298-.46.67-.46 1.091 0 .422.161.793.46 1.092.297.297.668.458 1.09.458h1.668l-.072.072a1.558 1.558 0 0 0-.472 1.129c0 .435.164.82.472 1.128ZM6.147 13.05l.5.5.207-.5h-.707Z\"/></svg>') 12 12,auto}.unreal-container.dolly{cursor:url('data:image/svg+xml,<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" fill=\"none\"><mask id=\"a\" width=\"24\" height=\"24\" x=\"0\" y=\"0\" maskUnits=\"userSpaceOnUse\" style=\"mask-type:alpha\"><path fill=\"%23127392\" d=\"M0 0h24v24H0z\"/></mask><g mask=\"url(%23a)\"><path fill=\"%23000\" d=\"M12.002 20.575a1.026 1.026 0 0 1-.75-.325l-2.55-2.55a.933.933 0 0 1-.287-.713c.008-.275.113-.513.313-.713.2-.2.441-.3.725-.3.283 0 .524.1.724.3l.776.776V6.95l-.8.8c-.2.2-.438.296-.712.288a1.018 1.018 0 0 1-.713-.312c-.2-.2-.3-.442-.3-.726s.1-.525.3-.725l2.524-2.525a1.026 1.026 0 0 1 1.5 0l2.55 2.55c.2.2.296.438.289.713a1.023 1.023 0 0 1-.314.713c-.2.2-.441.3-.724.3a.988.988 0 0 1-.725-.3l-.775-.776v10.1l.8-.8c.2-.2.437-.296.712-.288.275.008.512.112.712.312.2.2.3.442.3.726s-.1.525-.3.725l-2.524 2.525a1.026 1.026 0 0 1-.75.325Z\"/><path stroke=\"%23fff\" stroke-opacity=\".7\" d=\"M13.553 8.153c.28.245.62.373 1 .373.415 0 .784-.153 1.078-.447.287-.287.446-.644.46-1.05v-.002a1.432 1.432 0 0 0-.435-1.08l-2.55-2.55-.354.353.354-.354a1.526 1.526 0 0 0-2.207 0L8.374 5.921A1.487 1.487 0 0 0 7.928 7c0 .416.152.785.446 1.08.288.287.645.446 1.052.457l4.127-.384Zm0 0v7.693m0-7.693a1.667 1.667 0 0 1-.079-.074l-3.021.075v7.693a1.482 1.482 0 0 0-1-.373c-.416 0-.785.153-1.079.447a1.522 1.522 0 0 0-.46 1.05v.002c-.011.417.14.786.435 1.08l2.55 2.55a1.527 1.527 0 0 0 2.207 0l2.525-2.524c.294-.294.446-.663.446-1.079 0-.416-.152-.785-.446-1.08a1.518 1.518 0 0 0-1.052-.457m0 0-.014.5.014-.5Zm0 0a1.424 1.424 0 0 0-1.027.383m0 0a1.583 1.583 0 0 0-.053.05l.354.354m-.3-.404v.104l.3.3m0 0-.3-.3v.6l.3-.3Z\"/></g></svg>%0A') 12 12,auto}.unreal-container.disabled:before{content:\"\";position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px)}#playerUI{position:absolute;z-index:10;width:100%}#statsContainer{display:none;text-align:left;background-color:#000c}#stats{padding:6px;font-weight:700;font-size:14px;color:#0f0}canvas{position:absolute;image-rendering:crisp-edges}#overlay{position:absolute;top:0;right:2%;z-index:100;padding:4px;border-top-width:0;-webkit-border-bottom-right-radius:5px;-moz-border-radius-bottomright:5px;border-bottom-right-radius:5px;-webkit-border-bottom-left-radius:5px;-moz-border-radius-bottomleft:5px;border-bottom-left-radius:5px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-touch-callout:none}.overlay{font-weight:lighter;font-family:var(--src-font-family-body)}#overlayButton:hover{cursor:pointer}#overlayButton{font-size:40px;text-align:right}#overlaySettings{display:none;width:300px;padding:4px}#videoMessageOverlay{position:absolute;z-index:20;width:100%;margin:auto;font-size:1.8em;font-family:var(--src-font-family-body)}#playButton{display:inline-block;height:auto}img#playButton{width:10%;max-width:241px}#UIInteraction{position:fixed}#UIInteractionButtonBoundary{padding:2px}#UIInteractionButton{cursor:pointer}#hiddenInput{position:absolute;left:-10%;width:0;opacity:0}#editTextButton{position:absolute;width:40px;height:40px}\n"] }]
6646
- }], ctorParameters: () => [], propDecorators: { isStudio: [{ type: i0.Input, args: [{ isSignal: true, alias: "isStudio", required: false }] }], useContainerAsSizeProvider: [{ type: i0.Input, args: [{ isSignal: true, alias: "useContainerAsSizeProvider", required: false }] }], studioResolutionSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "studioResolutionSize", required: false }] }], videoElement: [{ type: i0.ViewChild, args: ['videoContainer', { isSignal: true }] }], changeMouseOverScene: [{ type: i0.Output, args: ["changeMouseOverScene"] }], onMouseOver: [{
6553
+ }], ctorParameters: () => [], propDecorators: { useContainerAsSizeProvider: [{ type: i0.Input, args: [{ isSignal: true, alias: "useContainerAsSizeProvider", required: false }] }], isStudio: [{ type: i0.Input, args: [{ isSignal: true, alias: "isStudio", required: false }] }], resolutionSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "resolutionSize", required: false }] }], changeMouseOverScene: [{ type: i0.Output, args: ["changeMouseOverScene"] }], videoElement: [{ type: i0.ViewChild, args: ['videoContainer', { isSignal: true }] }], onMouseOver: [{
6647
6554
  type: HostListener,
6648
6555
  args: ['mouseover']
6649
6556
  }], onMouseOut: [{
6650
6557
  type: HostListener,
6651
6558
  args: ['mouseout']
6559
+ }], onContextMenu: [{
6560
+ type: HostListener,
6561
+ args: ['contextmenu', ['$event']]
6652
6562
  }] } });
6653
6563
 
6654
6564
  class WebrtcErrorModalComponent {
@@ -6659,9 +6569,6 @@ class WebrtcErrorModalComponent {
6659
6569
  }
6660
6570
  #dialogRef;
6661
6571
  #store;
6662
- close() {
6663
- this.#dialogRef.close();
6664
- }
6665
6572
  closeModalWithCirrusDisconnect() {
6666
6573
  if (!this.isCirrusConnected()) {
6667
6574
  this.#store.dispatch(disconnectStream({
@@ -6671,6 +6578,9 @@ class WebrtcErrorModalComponent {
6671
6578
  }
6672
6579
  this.close();
6673
6580
  }
6581
+ close() {
6582
+ this.#dialogRef.close();
6583
+ }
6674
6584
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: WebrtcErrorModalComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
6675
6585
  static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.11", type: WebrtcErrorModalComponent, isStandalone: true, selector: "app-webrtc-error-modal", ngImport: i0, template: "<div [attr.data-testid]=\"'webrtc-error-modal'\" class=\"src-modal\">\n <header class=\"src-modal__header\"></header>\n <section class=\"src-modal__body\">\n <div [attr.data-testid]=\"'title'\" class=\"src-modal__title\">\n WebRTC error\n </div>\n <div style=\"text-align: left\">\n An internet connection type (WebRTC) appears to be blocked either by your\n browser settings or your current network. If WebRTC is blocked on your\n browser you may be able to adjust these settings yourself based on\n instructions here:\n <a href=\"https://myownconference.com/blog/en/webrtc/\" target=\"blank\"\n >https://myownconference.com/blog/en/webrtc/</a\n >\n <br /><br />\n Trying a different web browser may help confirm this as well. If WebRTC is\n blocked by your network, try switching to a different network if possible\n or contact your network administrator.<br /><br />\n WebRTC is common, safe and increasingly utilised method for streaming real\n time 3D experiences via a web browser. It typically consumes no more\n bandwidth than streaming an HD video.\n </div>\n </section>\n <footer class=\"src-modal__footer\">\n <src-button\n (onClick)=\"closeModalWithCirrusDisconnect()\"\n [data-testid]=\"'close-webrtc-error-modal'\"\n weight=\"primary\"\n >\n Ok\n </src-button>\n </footer>\n</div>\n", styles: [".src-modal{--modalBodyPadding: 20px 8px 20px 20px;display:grid;grid-template-columns:minmax(0,1fr);grid-template-rows:auto minmax(0,1fr) auto}.src-modal__body{width:100%}.src-modal__body p{color:var(--src-text-body-main, #1f2937);font-size:14px;font-style:normal;font-weight:400;line-height:24px;margin-top:0}.src-modal__body a{word-wrap:break-word;white-space:normal}\n"], dependencies: [{ kind: "component", type: SourceButtonComponent, selector: "src-button", inputs: ["type", "appearance", "weight", "size", "context", "customClass", "isFullWidth", "isPressed", "isDisabled", "isLoading", "isInverted", "srcButtonConfig", "formID", "data-testid", "ariaLabel"], outputs: ["onClick", "onSubmit"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
6676
6586
  }
@@ -6727,12 +6637,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImpo
6727
6637
 
6728
6638
  class LowBandwidthDetectorComponent {
6729
6639
  constructor() {
6730
- this.store = inject(Store);
6640
+ this.#store = inject(Store);
6731
6641
  this.dialog = inject(Dialog);
6732
6642
  this.destroyRef = inject(DestroyRef);
6733
6643
  this.videoService = inject(VideoService);
6734
6644
  this.isDevMode = inject(DevModeService).isDevMode;
6735
- this.viewportReady$ = this.store.select(unrealFeature.selectViewportReady);
6645
+ this.viewportReady$ = this.#store.select(unrealFeature.selectViewportReady);
6736
6646
  this.isLowBandwidth$ = of(true);
6737
6647
  this.lowBandwidthTrigger$ = this.isLowBandwidth$.pipe(distinctUntilChanged$1(), filter(Truthy));
6738
6648
  this.highBandwidthTrigger$ = this.isLowBandwidth$.pipe(distinctUntilChanged$1(), filter(Falsy));
@@ -6755,6 +6665,7 @@ class LowBandwidthDetectorComponent {
6755
6665
  this.isIndicatorExpanded = signal(false, ...(ngDevMode ? [{ debugName: "isIndicatorExpanded" }] : /* istanbul ignore next */ []));
6756
6666
  this.isIndicatorExpandedTrigger$ = toObservable(this.isIndicatorExpanded).pipe(skip(1), distinctUntilChanged$1(), filter(Falsy));
6757
6667
  }
6668
+ #store;
6758
6669
  ngOnInit() {
6759
6670
  this.lowBandwidthTrigger$
6760
6671
  .pipe(takeUntilDestroyed(this.destroyRef))
@@ -6853,6 +6764,7 @@ function provideUnrealModule() {
6853
6764
  StreamStatusTelemetryService2,
6854
6765
  FpsMonitorService,
6855
6766
  WebRtcPlayerService,
6767
+ LoadingProgressService,
6856
6768
  provideEnvironmentInitializer(() => {
6857
6769
  inject(AggregatorService);
6858
6770
  inject(InputService);
@@ -6871,5 +6783,5 @@ function provideUnrealModule() {
6871
6783
  * Generated bundle index. Do not edit.
6872
6784
  */
6873
6785
 
6874
- export { AFKService, AfkTimeoutModalComponent, AggregatorService, AnalyticsService, AnswerHandler, CONSOLE_COMMAND_DISABLE_MESSAGES, CONSOLE_COMMAND_ENABLE_MESSAGES, CONSOLE_COMMAND_PIXEL_QUALITY, CommandTelemetryService, CommandsEffects, ConfigHandler, ConsoleExtensionsService, ControlScheme, DATA_CHANNEL_CONNECTION_TIMEOUT, DEBOUNCE_TO_MANY_RESIZE_CALLS, DEFAULT_AFK_TIMEOUT, DEFAULT_AFK_TIMEOUT_PERIOD, DEFAULT_RECONNECT_DELAY_MS, DEFAULT_RECONNECT_ENABLED, DEFAULT_RECONNECT_MAX_ATTEMPTS, DEFAULT_RECONNECT_ON_DATACHANNEL_CLOSE, DEFAULT_RECONNECT_ON_ICE_FAILURE, DESTROY_DISCONNECT_REASONS, DataFlowMonitor, DevModeService, DisconnectReason, FULL_HD_HEIGHT, FULL_HD_WIDTH, FileHandlerService, FileReceiverService, FilterSettingsComponent, FpsMonitorService, FreezeFrameComponent, FreezeFrameService, IceCandidateHandler, ImageLoadingSrcComponent, InputOptions, InputService, InstanceReadyHandler, InstanceReservedHandler, IntroSrcComponent, KalmanFilter1D, LatencyTimings, LowBandwidthDetectorComponent, LowBandwidthModalComponent, MINIMAL_FPS, MessageType, MouseButton, MouseButtonsMask, OnCloseHandler, OnErrorHandler, OnMessageHandler, OnOpenHandler, OrchestrationMessageTypes, POLLING_TIME, PingHandler, PlayerCountHandler, RegionsPingService, SAME_SIZE_THRESHOLD, SCREEN_LOCKER_CONTAINER_ID, SIGNALLING_PERCENT_VALUE, SSInfoHandler, STREAMING_VIDEO_ID, SafePipe, SignallingService, SpecialKeyCodes, StatGraphComponent, StreamStatusTelemetryService, StreamStatusTelemetryService2, SubService, ToClientMessage, UNREAL_CONFIG, UnrealCallbackService, UnrealCommunicatorService, UnrealEffects, UnrealSceneComponent, UnrealStatusMessage, VideoService, VideoStatsComponent, WSCloseCode_CIRRUS_ABNORMAL_CLOSURE, WSCloseCode_CIRRUS_MAX_PLAYERS_ERROR, WSCloseCode_CIRRUS_PLAYER_DISCONNECTED, WSCloseCode_CIRRUS_STREAMER_KIKED_PLAYER, WSCloseCode_FORCE_CIRRUS_CLOSE, WSCloseCode_NORMAL_AFK_TIMEOUT, WSCloseCode_NORMAL_CLOSURE, WSCloseCode_NORMAL_MANUAL_DISCONNECT, WSCloseCode_UNKNOWN, WSCloseCodes, WS_OPEN_STATE, WS_TIMEOUT, WebRtcPlayerService, WebrtcErrorModalComponent, abortEstablishingConnection, answerSent, changeStatusMainVideoOnScene, changeStreamResolutionAction, changeStreamResolutionSuccessAction, commandCompleted, commandStarted, commandsFeature, commandsReducer, dataChannelConnected, dataChannelReady, decodeData, destroyRemoteConnections, destroyUnrealScene, disconnectStream, dispatchResize, dropConnection, dropDC, dropPC, floatToSmoothPercents, forceResizeViewport, fromResizeObserver, getActiveUrl, getImageFromVideoStream, getResizeStudioValues, getResizeStudioValuesOld, getResizeValuesUniversal, getResizeValuesUniversalOld, getRtcErrorMessage, iceConnectionFailed, initSignalling, instanceReady, keepMaxUntilReset, makeEven, pollingEnded, pollingStarted, provideUnrealModule, qpToQuality, reconnectPeer, reconnectPeerFailed, reconnectPeerSuccess, regionPingFetchEnd, regionPingFetchStart, regionResolved, resetAfk, resetConfig, resetDataChannelForReconnect, resetIntroSrc, resetWarnTimeout, saveAnalyticsEvent, selectCirrusConnectionId, selectClientAndViewIds, selectCommandProgress, selectCommandsInProgress, selectFreezeFrameCombinedDataUrl, selectFreezeFrameDataUrl, selectFreezeFrameDataUrlFromVideo, selectFreezeFrameProgressMessageFromVideo, selectIsAutostart, selectIsExistMatchUrls, selectIsFreezeFrameLoading, selectIsLoaderScreenVisible, selectIsVideoPlayingAndDataChannelConnected, selectShowLoader, selectShowReconnectPopup, selectSignalingParameters, selectStreamConfig, selectTotalProgress, selectWarnTimeout, setAfkTimeout, setAfkTimerCountdown, setAfkTimerHide, setAfkTimerVisible, setAwsInstance, setCirrusConnected, setCirrusDisconnected, setConfig, setEstablishingConnection, setFreezeFrame, setFreezeFrameFromVideo, setIntroImageSrc, setIntroVideoSrc, setLoadingImageSrc, setLoopBackCommandIsCompleted, setMaxFps, setOrchestrationContext, setOrchestrationMessage, setOrchestrationParameters, setOrchestrationProgress, setSignalingName, setStatusMessage, setStatusPercentSignallingServer, setStreamClientCompanyId, setStreamViewId, setUnrealPlaywrightConfig, setViewportNotReady, setViewportReady, showAfkTimeoutPopup, showLoadingProgress, showResumeStreamPopup, showUnrealErrorMessage, smoothTransition, startStream, trackMixpanelEvent, trackTelemetryData, unrealCommandCallback, unrealFeature, unrealReducer, updateCirrusInfo, videoAdaptedToContainer, videoStreamInitialized };
6786
+ export { AFKService, AfkTimeoutModalComponent, AggregatorService, AnalyticsService, AnswerHandler, CONSOLE_COMMAND_DISABLE_MESSAGES, CONSOLE_COMMAND_ENABLE_MESSAGES, CONSOLE_COMMAND_PIXEL_QUALITY, CommandTelemetryService, CommandsEffects, ConfigHandler, ConsoleExtensionsService, ControlScheme, DATA_CHANNEL_CONNECTION_TIMEOUT, DEBOUNCE_TO_MANY_RESIZE_CALLS, DEFAULT_AFK_TIMEOUT, DEFAULT_AFK_TIMEOUT_PERIOD, DEFAULT_RECONNECT_DELAY_MS, DEFAULT_RECONNECT_ENABLED, DEFAULT_RECONNECT_MAX_ATTEMPTS, DEFAULT_RECONNECT_ON_DATACHANNEL_CLOSE, DEFAULT_RECONNECT_ON_ICE_FAILURE, DESTROY_DISCONNECT_REASONS, DataFlowMonitor, DevModeService, DisconnectReason, FULL_HD_HEIGHT, FULL_HD_WIDTH, FileHandlerService, FileReceiverService, FilterSettingsComponent, FpsMonitorService, FreezeFrameComponent, FreezeFrameService, IceCandidateHandler, ImageLoadingSrcComponent, InputOptions, InputService, InstanceReadyHandler, InstanceReservedHandler, IntroSrcComponent, KalmanFilter1D, LatencyTimings, LoadingProgressService, LowBandwidthDetectorComponent, LowBandwidthModalComponent, MINIMAL_FPS, MIN_CONTAINER_SIZE, MessageType, MouseButton, MouseButtonsMask, OnCloseHandler, OnErrorHandler, OnMessageHandler, OnOpenHandler, OrchestrationMessageTypes, POLLING_TIME, PingHandler, PlayerCountHandler, RegionsPingService, SAME_SIZE_THRESHOLD, SCREEN_LOCKER_CONTAINER_ID, SIGNALLING_PERCENT_VALUE, SSInfoHandler, STREAMING_AUDIO_ID, STREAMING_VIDEO_ID, SafePipe, SignallingService, SpecialKeyCodes, StatGraphComponent, StreamStatusTelemetryService, StreamStatusTelemetryService2, SubService, ToClientMessage, UNREAL_CONFIG, UnrealCallbackService, UnrealCommunicatorService, UnrealEffects, UnrealSceneComponent, UnrealStatusMessage, VideoService, VideoStatsComponent, WSCloseCode_CIRRUS_ABNORMAL_CLOSURE, WSCloseCode_CIRRUS_MAX_PLAYERS_ERROR, WSCloseCode_CIRRUS_PLAYER_DISCONNECTED, WSCloseCode_CIRRUS_STREAMER_KIKED_PLAYER, WSCloseCode_FORCE_CIRRUS_CLOSE, WSCloseCode_NORMAL_AFK_TIMEOUT, WSCloseCode_NORMAL_CLOSURE, WSCloseCode_NORMAL_MANUAL_DISCONNECT, WSCloseCode_UNKNOWN, WSCloseCodes, WS_OPEN_STATE, WS_TIMEOUT, WebRtcPlayerService, WebrtcErrorModalComponent, abortEstablishingConnection, answerSent, changeStatusMainVideoOnScene, changeStreamResolutionAction, changeStreamResolutionSuccessAction, commandCompleted, commandStarted, commandsFeature, commandsReducer, dataChannelConnected, dataChannelReady, decodeData, destroyRemoteConnections, destroyUnrealScene, disconnectStream, dispatchResize, dropConnection, dropDC, dropPC, floatToSmoothPercents, forceResizeViewport, fromResizeObserver, getActiveUrl, getImageFromVideoStream, getResizeStudioValues, getResizeValuesUniversal, getRtcErrorMessage, iceConnectionFailed, initSignalling, instanceReady, keepMaxUntilReset, makeEven, pollingEnded, pollingStarted, provideUnrealModule, qpToQuality, reconnectPeer, reconnectPeerFailed, reconnectPeerSuccess, regionPingFetchEnd, regionPingFetchStart, regionResolved, resetAfk, resetConfig, resetDataChannelForReconnect, resetIntroSrc, resetUserInactivityTimeout, resetWarnTimeout, saveAnalyticsEvent, selectCirrusConnectionId, selectClientAndViewIds, selectCommandProgress, selectFreezeFrameCombinedDataUrl, selectFreezeFrameDataUrl, selectFreezeFrameDataUrlFromVideo, selectFreezeFrameProgressMessageFromVideo, selectIsAutostart, selectIsExistMatchUrls, selectIsFreezeFrameLoading, selectIsFullyConnected, selectIsLoaderScreenVisible, selectShowLoader, selectShowReconnectPopup, selectSignalingParameters, selectStreamConfig, selectTotalProgress, selectWarnTimeout, setAfkTimerCountdown, setAfkTimerHide, setAfkTimerVisible, setAwsInstance, setCirrusConnected, setCirrusDisconnected, setConfig, setEstablishingConnection, setFreezeFrame, setFreezeFrameFromVideo, setIntroImageSrc, setIntroVideoSrc, setLoadingImageSrc, setLoopBackCommandIsCompleted, setMaxFps, setOrchestrationContext, setOrchestrationMessage, setOrchestrationParameters, setOrchestrationProgress, setSignalingName, setStatusMessage, setStatusPercentSignallingServer, setStreamClientCompanyId, setStreamViewId, setUnrealPlaywrightConfig, setUserInactivityTimeout, setViewportNotReady, setViewportReady, showLoadingProgress, showResumeStreamPopup, showUnrealErrorMessage, showUserInactivityTimeoutPopup, smoothTransition, startStream, trackMixpanelEvent, trackTelemetryData, unrealCommandCallback, unrealFeature, unrealReducer, updateCirrusInfo, videoAdaptedToContainer, videoStreamInitialized };
6875
6787
  //# sourceMappingURL=3dsource-angular-unreal-module.mjs.map