@3dsource/angular-unreal-module 0.0.93 → 0.0.97

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,9 +1,9 @@
1
1
  import * as i0 from '@angular/core';
2
- import { InjectionToken, inject, Injectable, signal, makeEnvironmentProviders, provideEnvironmentInitializer, ChangeDetectionStrategy, Component, Pipe, ElementRef, input, HostListener, Input, ViewChild, DestroyRef, computed, viewChild, effect, untracked, output, afterNextRender } from '@angular/core';
2
+ import { InjectionToken, inject, Injectable, signal, ChangeDetectionStrategy, Component, Pipe, ElementRef, input, HostListener, Input, ViewChild, DestroyRef, computed, viewChild, effect, untracked, output, afterNextRender, makeEnvironmentProviders, provideEnvironmentInitializer } from '@angular/core';
3
3
  import { filter, withLatestFrom, distinctUntilChanged, switchMap, catchError, timeout, tap, map as map$1, takeUntil, exhaustMap, debounceTime, takeWhile, delay, skip as skip$1 } from 'rxjs/operators';
4
- import { createAction, props, Store, provideState, createReducer, on, createFeature, createSelector } from '@ngrx/store';
4
+ import { createAction, props, createReducer, on, createFeature, Store, createSelector, provideState } from '@ngrx/store';
5
5
  import { concatLatestFrom } from '@ngrx/operators';
6
- import { Actions, ofType, provideEffects, createEffect } from '@ngrx/effects';
6
+ import { Actions, ofType, createEffect, provideEffects } from '@ngrx/effects';
7
7
  import { skip, share, merge, Subject, interval, of, map, from, take, fromEvent, timer, throwError, defer, Observable, switchMap as switchMap$1, retry, timeout as timeout$1, tap as tap$1, startWith, takeUntil as takeUntil$1, auditTime, EMPTY, filter as filter$1, throttleTime, debounceTime as debounceTime$1, mergeMap, scan, concatMap, animationFrameScheduler, combineLatestWith, BehaviorSubject, distinctUntilChanged as distinctUntilChanged$1, concat } from 'rxjs';
8
8
  import { Falsy, Truthy, Logger, calculateMedian, clampf, Signal, tapLog, generateUuid, COLOR_CODES, where, KeyboardNumericCode, InvertedKeyMap, Semaphore, lerp, getCanvasCached, getSnapshot, whereNot, HEXtoRGB, RGBtoHSV, inverseLerp, HSVtoRGB, RGBtoHEX, fpIsASameAsB, fitIntoRectangle } from '@3dsource/utils';
9
9
  import { HttpClient } from '@angular/common/http';
@@ -312,6 +312,266 @@ const DEFAULT_RECONNECT_DELAY_MS = 1000;
312
312
  const DEFAULT_RECONNECT_ON_ICE_FAILURE = true;
313
313
  const DEFAULT_RECONNECT_ON_DATACHANNEL_CLOSE = true;
314
314
 
315
+ const initialState = {
316
+ streamRequestContext: null,
317
+ environmentId: null,
318
+ lowBandwidthStats: undefined,
319
+ wasInitialized: false,
320
+ isFirstSuccessLoad: false,
321
+ isAfkTimerVisible: false,
322
+ lowBandwidth: false,
323
+ cirrusConnected: false,
324
+ establishingConnection: false,
325
+ isReconnecting: false,
326
+ viewportReady: false,
327
+ dataChannelConnected: false,
328
+ isVideoPlaying: false,
329
+ statusPercentSignallingServer: null,
330
+ statusMessage: null,
331
+ errorMessage: null,
332
+ ssInfo: null,
333
+ ssData: null,
334
+ streamResolution: { width: null, height: null },
335
+ freezeFrameFromVideo: { dataUrl: null, progress: null },
336
+ freezeFrame: { dataUrl: null, progress: null },
337
+ disconnectReason: DisconnectReason.None,
338
+ awsInstance: {
339
+ wsUrl: null,
340
+ instanceName: null,
341
+ pollingUrl: null,
342
+ progressComplete: 0,
343
+ },
344
+ streamConfig: {
345
+ autoStart: true,
346
+ warnTimeout: DEFAULT_AFK_TIMEOUT,
347
+ },
348
+ loaderCommands: {
349
+ commandsInProgress: [],
350
+ totalCommandsStarted: 0,
351
+ totalCommandsCompleted: 0,
352
+ },
353
+ matchUrls: [],
354
+ streamClientCompanyId: '',
355
+ streamViewId: 'default',
356
+ videoIntroSrc: null,
357
+ imageIntroSrc: null,
358
+ imageLoadingSrc: '',
359
+ analyticsEvent: null,
360
+ };
361
+ const unrealReducer = createReducer(initialState, on(changeLowBandwidth, (state, { lowBandwidth, stats }) => ({
362
+ ...state,
363
+ lowBandwidth: lowBandwidth,
364
+ lowBandwidthStats: lowBandwidth ? stats : undefined,
365
+ })), on(changeStatusMainVideoOnScene, (state, { isVideoPlaying }) => {
366
+ return { ...state, isVideoPlaying: isVideoPlaying };
367
+ }), on(setAwsInstance, (state, { instanceName, wsUrl, pollingUrl }) => {
368
+ return { ...state, awsInstance: { instanceName, wsUrl, pollingUrl } };
369
+ }), on(setViewportReady, (state) => {
370
+ return {
371
+ ...state,
372
+ viewportReady: true,
373
+ statusMessage: null,
374
+ errorMessage: null,
375
+ };
376
+ }), on(setViewportNotReady, (state) => {
377
+ return { ...state, viewportReady: false };
378
+ }), on(updateCirrusInfo, (state, { ssInfo, ssData }) => {
379
+ return {
380
+ ...state,
381
+ ssInfo: ssInfo, // For back compatibility
382
+ ssData: ssData, // Contains all the data from the ssInfo as an object
383
+ };
384
+ }), on(changeStreamResolutionSuccessAction, (state, { width, height }) => {
385
+ return { ...state, streamResolution: { width: width, height: height } };
386
+ }), on(setFreezeFrame, (state, freezeFrame) => {
387
+ return {
388
+ ...state,
389
+ freezeFrame: {
390
+ dataUrl: freezeFrame.progress === 0 ||
391
+ freezeFrame.progress === 1 ||
392
+ freezeFrame.progress === null
393
+ ? freezeFrame.dataUrl
394
+ : state.freezeFrame.dataUrl,
395
+ progress: freezeFrame.progress || null,
396
+ },
397
+ };
398
+ }), on(disconnectStream, (state, errorMessage) => {
399
+ if (state.dataChannelConnected ||
400
+ state.disconnectReason === DisconnectReason.Destroy) {
401
+ return state;
402
+ }
403
+ return {
404
+ ...state,
405
+ errorMessage,
406
+ statusMessage: null,
407
+ wasInitialized: true,
408
+ };
409
+ }), on(setFreezeFrameFromVideo, (state, freezeFrameFromVideo) => {
410
+ return {
411
+ ...state,
412
+ freezeFrameFromVideo: {
413
+ dataUrl: freezeFrameFromVideo.dataUrl,
414
+ progress: freezeFrameFromVideo.progress || null,
415
+ },
416
+ };
417
+ }), on(setStatusMessage, (state, { statusMessage }) => {
418
+ return { ...state, statusMessage };
419
+ }), on(setEstablishingConnection, (state, { value }) => {
420
+ return { ...state, establishingConnection: value };
421
+ }), on(setStatusPercentSignallingServer, (state, { percent }) => {
422
+ return { ...state, statusPercentSignallingServer: percent };
423
+ }), on(dataChannelConnected, (state, { statusMessage }) => {
424
+ return {
425
+ ...state,
426
+ statusMessage,
427
+ dataChannelConnected: true,
428
+ establishingConnection: false,
429
+ wasInitialized: true,
430
+ };
431
+ }), on(setConfig, startStream, (state, { config }) => {
432
+ return { ...state, streamConfig: { ...state.streamConfig, ...config } };
433
+ }), on(resetConfig, (state) => {
434
+ return { ...state, streamConfig: initialState.streamConfig };
435
+ }), on(resetWarnTimeout, (state) => {
436
+ return {
437
+ ...state,
438
+ streamConfig: { ...state.streamConfig, warnTimeout: DEFAULT_AFK_TIMEOUT },
439
+ };
440
+ }), on(initSignalling, (state, { resetDisconnectionReason = true }) => {
441
+ return {
442
+ ...state,
443
+ disconnectReason: resetDisconnectionReason
444
+ ? DisconnectReason.None
445
+ : state.disconnectReason,
446
+ };
447
+ }), on(destroyRemoteConnections, (state, { reason }) => {
448
+ if (state.disconnectReason === DisconnectReason.Destroy) {
449
+ return state;
450
+ }
451
+ return { ...state, disconnectReason: reason };
452
+ }), on(reconnectPeerSuccess, (state) => {
453
+ return { ...state, isReconnecting: false };
454
+ }), on(setSignalingName, (state, { instanceName }) => {
455
+ return { ...state, awsInstance: { ...state.awsInstance, instanceName } };
456
+ }), on(setOrchestrationParameters, (state, { instanceName, streamRequestId, eta }) => {
457
+ return {
458
+ ...state,
459
+ awsInstance: {
460
+ ...state.awsInstance,
461
+ message: undefined,
462
+ instanceName,
463
+ streamRequestId,
464
+ eta,
465
+ },
466
+ };
467
+ }), on(setOrchestrationProgress, (state, { progressComplete }) => {
468
+ return {
469
+ ...state,
470
+ awsInstance: {
471
+ ...state.awsInstance,
472
+ progressComplete,
473
+ },
474
+ };
475
+ }), on(setOrchestrationMessage, (state, { message }) => {
476
+ return {
477
+ ...state,
478
+ awsInstance: {
479
+ ...state.awsInstance,
480
+ message,
481
+ },
482
+ };
483
+ }), on(setLoopBackCommandIsCompleted, (state) => {
484
+ return { ...state, isFirstSuccessLoad: true };
485
+ }), on(setAfkTimerVisible, (state) => {
486
+ return { ...state, isAfkTimerVisible: true };
487
+ }), on(setAfkTimerHide, (state) => {
488
+ return { ...state, isAfkTimerVisible: false };
489
+ }), on(setOrchestrationContext, (state, { urls, environmentId, streamRequestContext }) => {
490
+ return {
491
+ ...state,
492
+ matchUrls: urls,
493
+ environmentId,
494
+ streamRequestContext,
495
+ };
496
+ }), on(setStreamClientCompanyId, (state, { id }) => {
497
+ return { ...state, streamClientCompanyId: id };
498
+ }), on(setStreamViewId, (state, { id }) => {
499
+ return { ...state, streamViewId: id };
500
+ }), on(setIntroImageSrc, (state, { src }) => {
501
+ return { ...state, imageIntroSrc: src };
502
+ }), on(setLoadingImageSrc, (state, { src }) => {
503
+ return { ...state, imageLoadingSrc: src };
504
+ }), on(setIntroVideoSrc, (state, { src }) => {
505
+ return { ...state, videoIntroSrc: src };
506
+ }), on(resetIntroSrc, (state) => {
507
+ return { ...state, imageIntroSrc: '', videoIntroSrc: '' };
508
+ }), on(saveAnalyticsEvent, (state, { event }) => {
509
+ return { ...state, analyticsEvent: event };
510
+ }), on(commandStarted, (state, { id, command }) => {
511
+ return {
512
+ ...state,
513
+ loaderCommands: {
514
+ ...state.loaderCommands,
515
+ totalCommandsStarted: state.loaderCommands.totalCommandsStarted + 1,
516
+ commandsInProgress: [
517
+ ...state.loaderCommands.commandsInProgress,
518
+ { id, command, timeStamp: new Date().getTime() },
519
+ ],
520
+ },
521
+ };
522
+ }), on(commandCompleted, (state, { id }) => {
523
+ return {
524
+ ...state,
525
+ loaderCommands: removeExileCommands(state.loaderCommands, id),
526
+ };
527
+ }), on(setCirrusConnected, (state) => {
528
+ return { ...state, cirrusConnected: true };
529
+ }), on(setCirrusDisconnected, (state) => {
530
+ return {
531
+ ...initialState,
532
+ establishingConnection: state.establishingConnection,
533
+ disconnectReason: state.disconnectReason,
534
+ wasInitialized: state.wasInitialized,
535
+ isFirstSuccessLoad: state.isFirstSuccessLoad,
536
+ matchUrls: state.matchUrls,
537
+ streamClientCompanyId: state.streamClientCompanyId,
538
+ streamViewId: state.streamViewId,
539
+ imageIntroSrc: state.imageIntroSrc,
540
+ videoIntroSrc: state.videoIntroSrc,
541
+ imageLoadingSrc: state.imageLoadingSrc,
542
+ environmentId: state.environmentId,
543
+ streamRequestContext: state.streamRequestContext,
544
+ streamConfig: state.streamConfig,
545
+ };
546
+ }), on(dropConnection, (state) => {
547
+ return { ...state, establishingConnection: false };
548
+ }), on(resetDataChannelForReconnect, (state) => {
549
+ return {
550
+ ...state,
551
+ isReconnecting: true,
552
+ dataChannelConnected: false,
553
+ viewportReady: false,
554
+ statusMessage: 'Reconnecting...',
555
+ };
556
+ }), on(setUnrealPlaywrightConfig, (state) => {
557
+ return {
558
+ ...state,
559
+ wasInitialized: true,
560
+ isFirstSuccessLoad: true,
561
+ cirrusConnected: true,
562
+ viewportReady: true,
563
+ dataChannelConnected: true,
564
+ isVideoPlaying: true,
565
+ };
566
+ }), on(destroyUnrealScene, () => {
567
+ return initialState;
568
+ }));
569
+
570
+ const unrealFeature = createFeature({
571
+ name: 'unrealFeature',
572
+ reducer: unrealReducer,
573
+ });
574
+
315
575
  class SubService {
316
576
  constructor() {
317
577
  this.store = inject(Store);
@@ -3331,546 +3591,538 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
3331
3591
  type: Injectable
3332
3592
  }] });
3333
3593
 
3334
- class FreezeFramePlaywrightService extends FreezeFrameService {
3335
- init() {
3336
- return;
3337
- }
3338
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: FreezeFramePlaywrightService, deps: null, target: i0.ɵɵFactoryTarget.Injectable }); }
3339
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: FreezeFramePlaywrightService }); }
3340
- }
3341
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: FreezeFramePlaywrightService, decorators: [{
3342
- type: Injectable
3343
- }] });
3344
-
3345
- class CommandTelemetryPlaywrightService extends CommandTelemetryService {
3346
- init() {
3347
- return;
3348
- }
3349
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CommandTelemetryPlaywrightService, deps: null, target: i0.ɵɵFactoryTarget.Injectable }); }
3350
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CommandTelemetryPlaywrightService }); }
3351
- }
3352
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CommandTelemetryPlaywrightService, decorators: [{
3353
- type: Injectable
3354
- }] });
3355
-
3356
- class StreamStatusTelemetryPlaywrightService extends StreamStatusTelemetryService {
3357
- init() {
3358
- return;
3359
- }
3360
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: StreamStatusTelemetryPlaywrightService, deps: null, target: i0.ɵɵFactoryTarget.Injectable }); }
3361
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: StreamStatusTelemetryPlaywrightService }); }
3362
- }
3363
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: StreamStatusTelemetryPlaywrightService, decorators: [{
3364
- type: Injectable
3365
- }] });
3366
-
3367
- class ConsoleExtensionsPlaywrightService extends ConsoleExtensionsService {
3368
- init() {
3369
- return;
3370
- }
3371
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ConsoleExtensionsPlaywrightService, deps: null, target: i0.ɵɵFactoryTarget.Injectable }); }
3372
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ConsoleExtensionsPlaywrightService }); }
3373
- }
3374
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ConsoleExtensionsPlaywrightService, decorators: [{
3375
- type: Injectable
3376
- }] });
3377
-
3378
- class FileReceiverPlaywrightService extends FileReceiverService {
3379
- init() {
3380
- return;
3381
- }
3382
- }
3383
-
3384
- class InputPlaywrightService extends InputService {
3385
- init() {
3386
- return;
3387
- }
3388
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: InputPlaywrightService, deps: null, target: i0.ɵɵFactoryTarget.Injectable }); }
3389
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: InputPlaywrightService }); }
3390
- }
3391
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: InputPlaywrightService, decorators: [{
3392
- type: Injectable
3393
- }] });
3394
-
3395
- /*
3396
- * Copyright (c) 2025.
3397
- * 3DSource.com. Sergii Karanda steve@3dsource.com. All Rights Reserved.
3398
- */
3399
- const ReceivedMimeTypes = {
3400
- ApplicationJson: 'application/json',
3401
- };
3402
-
3403
- class FileHandlerService {
3404
- constructor() {
3405
- this.fileService = inject(FileReceiverService);
3406
- this.fileHandlers = {
3407
- [ReceivedMimeTypes.ApplicationJson]: this.handleJsonFile.bind(this),
3408
- };
3409
- }
3410
- handleJsonFile(data, correlationId) {
3411
- const { blob } = data;
3412
- return from(blob.text()).pipe(map$1((text) => JSON.parse(text)), filter(({ commandCallback }) => {
3413
- return commandCallback.correlationId === correlationId;
3414
- }));
3415
- }
3416
- observeFileResponse(mimetype, data, sender, timeOut = 60000) {
3417
- const correlationId = generateUuid();
3418
- const out = { ...data, correlationId };
3419
- const observable = this.fileService.fileComplete$.pipe(switchMap$1((data) => {
3420
- const { valid } = data;
3421
- if (valid && this.fileHandlers[mimetype]) {
3422
- return this.fileHandlers[mimetype](data, correlationId).pipe(filter(Truthy));
3423
- }
3424
- return of(null);
3425
- }), timeout$1(timeOut), catchError((e) => {
3426
- Logger.error(e);
3427
- return of(null);
3428
- }), take(1));
3429
- setTimeout(() => sender(out), 0);
3430
- return observable;
3594
+ class FreezeFramePlaywrightService extends FreezeFrameService {
3595
+ init() {
3596
+ return;
3431
3597
  }
3432
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: FileHandlerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
3433
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: FileHandlerService }); }
3598
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: FreezeFramePlaywrightService, deps: null, target: i0.ɵɵFactoryTarget.Injectable }); }
3599
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: FreezeFramePlaywrightService }); }
3434
3600
  }
3435
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: FileHandlerService, decorators: [{
3601
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: FreezeFramePlaywrightService, decorators: [{
3436
3602
  type: Injectable
3437
3603
  }] });
3438
3604
 
3439
- class AnalyticsService {
3440
- #store = inject(Store);
3441
- #destroy$ = new Subject();
3442
- sendEventToMixPanel(name, params = {}) {
3443
- Logger.log('Analytics Event:', name, params);
3444
- this.#store.dispatch(saveAnalyticsEvent({ event: { name, params } }));
3445
- }
3446
- constructor() {
3447
- const abortSignal$ = this.#store
3448
- .select(unrealFeature.selectViewportReady)
3449
- .pipe(filter$1(Falsy), map(() => void 0));
3450
- this.#store
3451
- .select(unrealFeature.selectViewportReady)
3452
- .pipe(distinctUntilChanged(), filter$1(Truthy), tap(() => this.monitorMouse(abortSignal$)))
3453
- .subscribe();
3605
+ class CommandTelemetryPlaywrightService extends CommandTelemetryService {
3606
+ init() {
3607
+ return;
3454
3608
  }
3455
- monitorMouse(abortSignal$) {
3456
- this.#destroy$.next();
3457
- const stop$ = merge(this.#destroy$.asObservable(), abortSignal$);
3458
- const element = document.getElementById('streamingVideo');
3459
- if (!element) {
3460
- return;
3461
- }
3462
- // Monitor mousewheel events
3463
- const mousewheel$ = fromEvent(element, 'wheel').pipe(takeUntil$1(stop$), throttleTime(1000));
3464
- // Monitor mouse button states
3465
- const mousedown$ = fromEvent(element, 'mousedown').pipe(takeUntil$1(stop$));
3466
- const touchStart$ = fromEvent(element, 'touchstart').pipe(takeUntil$1(stop$));
3467
- const touchMove$ = fromEvent(element, 'touchmove').pipe(takeUntil$1(stop$), throttleTime(200));
3468
- // Track mouse movement while the left or right button is pressed
3469
- mousedown$
3470
- .pipe(filter$1((event) => event.button === 0 || event.button === 2), tap((downEvent) => {
3471
- const { offsetX, offsetY } = this.getOffset(downEvent, element);
3472
- this.sendEventToMixPanel('mouse_interaction', {
3473
- viewX: offsetX / element.clientWidth,
3474
- viewY: offsetY / element.clientHeight,
3475
- action: downEvent.button === 0 ? 'spinning' : 'panning',
3476
- });
3477
- }))
3478
- .subscribe();
3479
- // Track mouse movement while the left or right button is pressed
3480
- touchStart$
3481
- .pipe(tap((downEvent) => {
3482
- const totalTouches = downEvent.touches.length;
3483
- const touch = downEvent.touches[0];
3484
- const { offsetX, offsetY } = this.getOffset(touch, element);
3485
- this.sendEventToMixPanel('touch_interaction', {
3486
- viewX: offsetX / element.clientWidth,
3487
- viewY: offsetY / element.clientHeight,
3488
- action: totalTouches === 1 ? 'spinning' : 'panning',
3489
- });
3490
- }))
3491
- .subscribe();
3492
- let initialTouchDistance = 0;
3493
- // Subscribe to mousewheel events
3494
- touchMove$.subscribe((event) => {
3495
- const totalTouches = event.touches.length;
3496
- if (totalTouches < 2) {
3497
- return;
3498
- }
3499
- // Calculate the current distance between two touches
3500
- const touch1 = event.touches[0];
3501
- const touch2 = event.touches[1];
3502
- const offset1 = this.getOffset(touch1, element);
3503
- const offset2 = this.getOffset(touch2, element);
3504
- const currentDistance = Math.sqrt(Math.pow(offset2.offsetX - offset1.offsetX, 2) +
3505
- Math.pow(offset2.offsetY - offset1.offsetY, 2));
3506
- // Determine if zooming in or out based on distance change
3507
- const deltaDistance = currentDistance - initialTouchDistance;
3508
- const isZoomingIn = deltaDistance > 0;
3509
- // Only send event if there's a significant change (avoid noise)
3510
- if (Math.abs(deltaDistance) > 10) {
3511
- this.sendEventToMixPanel('touch_zoom_interaction', {
3512
- delta: isZoomingIn ? 1 : -1,
3513
- action: isZoomingIn ? 'zoomin' : 'zoomout',
3514
- });
3515
- // Update initial distance for next comparison
3516
- initialTouchDistance = currentDistance;
3517
- }
3518
- });
3519
- mousewheel$.subscribe((event) => {
3520
- this.sendEventToMixPanel('mouse_zoom_interaction', {
3521
- delta: -(event.deltaY / Math.abs(event.deltaY)),
3522
- action: event.deltaY < 0 ? 'zoomin' : 'zoomout',
3523
- });
3524
- });
3609
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CommandTelemetryPlaywrightService, deps: null, target: i0.ɵɵFactoryTarget.Injectable }); }
3610
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CommandTelemetryPlaywrightService }); }
3611
+ }
3612
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CommandTelemetryPlaywrightService, decorators: [{
3613
+ type: Injectable
3614
+ }] });
3615
+
3616
+ class StreamStatusTelemetryPlaywrightService extends StreamStatusTelemetryService {
3617
+ init() {
3618
+ return;
3525
3619
  }
3526
- getOffset(event, element) {
3527
- const rect = element.getBoundingClientRect(); // in viewport coords
3528
- const offsetX = event.clientX - rect.left; // relative to element
3529
- const offsetY = event.clientY - rect.top;
3530
- return { offsetX, offsetY };
3620
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: StreamStatusTelemetryPlaywrightService, deps: null, target: i0.ɵɵFactoryTarget.Injectable }); }
3621
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: StreamStatusTelemetryPlaywrightService }); }
3622
+ }
3623
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: StreamStatusTelemetryPlaywrightService, decorators: [{
3624
+ type: Injectable
3625
+ }] });
3626
+
3627
+ class ConsoleExtensionsPlaywrightService extends ConsoleExtensionsService {
3628
+ init() {
3629
+ return;
3531
3630
  }
3532
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: AnalyticsService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
3533
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: AnalyticsService }); }
3631
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ConsoleExtensionsPlaywrightService, deps: null, target: i0.ɵɵFactoryTarget.Injectable }); }
3632
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ConsoleExtensionsPlaywrightService }); }
3534
3633
  }
3535
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: AnalyticsService, decorators: [{
3634
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ConsoleExtensionsPlaywrightService, decorators: [{
3536
3635
  type: Injectable
3537
- }], ctorParameters: () => [] });
3636
+ }] });
3538
3637
 
3539
- function provideAngularUnrealModule(config) {
3540
- return makeEnvironmentProviders([
3541
- provideState(unrealFeature),
3542
- provideEffects([UnrealEffects]),
3543
- ConsoleExtensionsService,
3544
- InputService,
3545
- VideoService,
3546
- WebRtcPlayerService,
3547
- RegionsPingService,
3548
- FileReceiverService,
3549
- FileHandlerService,
3550
- AnalyticsService,
3551
- {
3552
- provide: StreamStatusTelemetryService,
3553
- useClass: config?.playwright
3554
- ? StreamStatusTelemetryPlaywrightService
3555
- : StreamStatusTelemetryService,
3556
- },
3557
- {
3558
- provide: CommandTelemetryService,
3559
- useClass: config?.playwright
3560
- ? CommandTelemetryPlaywrightService
3561
- : CommandTelemetryService,
3562
- },
3563
- {
3564
- provide: AggregatorService,
3565
- useClass: config?.playwright
3566
- ? AggregatorPlaywrightService
3567
- : AggregatorService,
3568
- },
3569
- {
3570
- provide: FreezeFrameService,
3571
- useClass: config?.playwright
3572
- ? FreezeFramePlaywrightService
3573
- : FreezeFrameService,
3574
- },
3575
- {
3576
- provide: UnrealCommunicatorService,
3577
- useClass: config?.playwright
3578
- ? UnrealCommunicatorPlaywrightService
3579
- : UnrealCommunicatorService,
3580
- },
3581
- {
3582
- provide: AFKService,
3583
- useClass: config?.playwright ? AfkPlaywrightService : AFKService,
3584
- },
3585
- {
3586
- provide: SignallingService,
3587
- useClass: config?.playwright
3588
- ? SignallingPlaywrightService
3589
- : SignallingService,
3590
- },
3591
- {
3592
- provide: ConsoleExtensionsService,
3593
- useClass: config?.playwright
3594
- ? ConsoleExtensionsPlaywrightService
3595
- : ConsoleExtensionsService,
3596
- },
3597
- {
3598
- provide: FileReceiverService,
3599
- useClass: config?.playwright
3600
- ? FileReceiverPlaywrightService
3601
- : FileReceiverService,
3602
- },
3603
- provideEnvironmentInitializer(() => {
3604
- inject(AggregatorService);
3605
- inject(InputService);
3606
- inject(StreamStatusTelemetryService);
3607
- inject(ConsoleExtensionsService);
3608
- inject(AFKService);
3609
- inject(FreezeFrameService);
3610
- inject(AnalyticsService);
3611
- }),
3612
- ]);
3638
+ class FileReceiverPlaywrightService extends FileReceiverService {
3639
+ init() {
3640
+ return;
3641
+ }
3613
3642
  }
3614
3643
 
3615
- const initialState = {
3616
- streamRequestContext: null,
3617
- environmentId: null,
3618
- lowBandwidthStats: undefined,
3619
- wasInitialized: false,
3620
- isFirstSuccessLoad: false,
3621
- isAfkTimerVisible: false,
3622
- lowBandwidth: false,
3623
- cirrusConnected: false,
3624
- establishingConnection: false,
3625
- isReconnecting: false,
3626
- viewportReady: false,
3627
- dataChannelConnected: false,
3628
- isVideoPlaying: false,
3629
- statusPercentSignallingServer: null,
3630
- statusMessage: null,
3631
- errorMessage: null,
3632
- ssInfo: null,
3633
- ssData: null,
3634
- streamResolution: { width: null, height: null },
3635
- freezeFrameFromVideo: { dataUrl: null, progress: null },
3636
- freezeFrame: { dataUrl: null, progress: null },
3637
- disconnectReason: DisconnectReason.None,
3638
- awsInstance: {
3639
- wsUrl: null,
3640
- instanceName: null,
3641
- pollingUrl: null,
3642
- progressComplete: 0,
3643
- },
3644
- streamConfig: {
3645
- autoStart: true,
3646
- warnTimeout: DEFAULT_AFK_TIMEOUT,
3647
- },
3648
- loaderCommands: {
3649
- commandsInProgress: [],
3650
- totalCommandsStarted: 0,
3651
- totalCommandsCompleted: 0,
3652
- },
3653
- matchUrls: [],
3654
- streamClientCompanyId: '',
3655
- streamViewId: 'default',
3656
- videoIntroSrc: null,
3657
- imageIntroSrc: null,
3658
- imageLoadingSrc: '',
3659
- analyticsEvent: null,
3644
+ class InputPlaywrightService extends InputService {
3645
+ init() {
3646
+ return;
3647
+ }
3648
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: InputPlaywrightService, deps: null, target: i0.ɵɵFactoryTarget.Injectable }); }
3649
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: InputPlaywrightService }); }
3650
+ }
3651
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: InputPlaywrightService, decorators: [{
3652
+ type: Injectable
3653
+ }] });
3654
+
3655
+ /*
3656
+ * Copyright (c) 2025.
3657
+ * 3DSource.com. Sergii Karanda steve@3dsource.com. All Rights Reserved.
3658
+ */
3659
+ const ReceivedMimeTypes = {
3660
+ ApplicationJson: 'application/json',
3660
3661
  };
3661
- const unrealReducer = createReducer(initialState, on(changeLowBandwidth, (state, { lowBandwidth, stats }) => ({
3662
- ...state,
3663
- lowBandwidth: lowBandwidth,
3664
- lowBandwidthStats: lowBandwidth ? stats : undefined,
3665
- })), on(changeStatusMainVideoOnScene, (state, { isVideoPlaying }) => {
3666
- return { ...state, isVideoPlaying: isVideoPlaying };
3667
- }), on(setAwsInstance, (state, { instanceName, wsUrl, pollingUrl }) => {
3668
- return { ...state, awsInstance: { instanceName, wsUrl, pollingUrl } };
3669
- }), on(setViewportReady, (state) => {
3670
- return {
3671
- ...state,
3672
- viewportReady: true,
3673
- statusMessage: null,
3674
- errorMessage: null,
3675
- };
3676
- }), on(setViewportNotReady, (state) => {
3677
- return { ...state, viewportReady: false };
3678
- }), on(updateCirrusInfo, (state, { ssInfo, ssData }) => {
3679
- return {
3680
- ...state,
3681
- ssInfo: ssInfo, // For back compatibility
3682
- ssData: ssData, // Contains all the data from the ssInfo as an object
3683
- };
3684
- }), on(changeStreamResolutionSuccessAction, (state, { width, height }) => {
3685
- return { ...state, streamResolution: { width: width, height: height } };
3686
- }), on(setFreezeFrame, (state, freezeFrame) => {
3687
- return {
3688
- ...state,
3689
- freezeFrame: {
3690
- dataUrl: freezeFrame.progress === 0 ||
3691
- freezeFrame.progress === 1 ||
3692
- freezeFrame.progress === null
3693
- ? freezeFrame.dataUrl
3694
- : state.freezeFrame.dataUrl,
3695
- progress: freezeFrame.progress || null,
3696
- },
3697
- };
3698
- }), on(disconnectStream, (state, errorMessage) => {
3699
- if (state.dataChannelConnected ||
3700
- state.disconnectReason === DisconnectReason.Destroy) {
3701
- return state;
3662
+
3663
+ class FileHandlerService {
3664
+ constructor() {
3665
+ this.fileService = inject(FileReceiverService);
3666
+ this.fileHandlers = {
3667
+ [ReceivedMimeTypes.ApplicationJson]: this.handleJsonFile.bind(this),
3668
+ };
3702
3669
  }
3703
- return {
3704
- ...state,
3705
- errorMessage,
3706
- statusMessage: null,
3707
- wasInitialized: true,
3708
- };
3709
- }), on(setFreezeFrameFromVideo, (state, freezeFrameFromVideo) => {
3710
- return {
3711
- ...state,
3712
- freezeFrameFromVideo: {
3713
- dataUrl: freezeFrameFromVideo.dataUrl,
3714
- progress: freezeFrameFromVideo.progress || null,
3715
- },
3716
- };
3717
- }), on(setStatusMessage, (state, { statusMessage }) => {
3718
- return { ...state, statusMessage };
3719
- }), on(setEstablishingConnection, (state, { value }) => {
3720
- return { ...state, establishingConnection: value };
3721
- }), on(setStatusPercentSignallingServer, (state, { percent }) => {
3722
- return { ...state, statusPercentSignallingServer: percent };
3723
- }), on(dataChannelConnected, (state, { statusMessage }) => {
3724
- return {
3725
- ...state,
3726
- statusMessage,
3727
- dataChannelConnected: true,
3728
- establishingConnection: false,
3729
- wasInitialized: true,
3730
- };
3731
- }), on(setConfig, startStream, (state, { config }) => {
3732
- return { ...state, streamConfig: { ...state.streamConfig, ...config } };
3733
- }), on(resetConfig, (state) => {
3734
- return { ...state, streamConfig: initialState.streamConfig };
3735
- }), on(resetWarnTimeout, (state) => {
3736
- return {
3737
- ...state,
3738
- streamConfig: { ...state.streamConfig, warnTimeout: DEFAULT_AFK_TIMEOUT },
3739
- };
3740
- }), on(initSignalling, (state, { resetDisconnectionReason = true }) => {
3741
- return {
3742
- ...state,
3743
- disconnectReason: resetDisconnectionReason
3744
- ? DisconnectReason.None
3745
- : state.disconnectReason,
3746
- };
3747
- }), on(destroyRemoteConnections, (state, { reason }) => {
3748
- if (state.disconnectReason === DisconnectReason.Destroy) {
3749
- return state;
3670
+ handleJsonFile(data, correlationId) {
3671
+ const { blob } = data;
3672
+ return from(blob.text()).pipe(map$1((text) => JSON.parse(text)), filter(({ commandCallback }) => {
3673
+ return commandCallback.correlationId === correlationId;
3674
+ }));
3750
3675
  }
3751
- return { ...state, disconnectReason: reason };
3752
- }), on(reconnectPeerSuccess, (state) => {
3753
- return { ...state, isReconnecting: false };
3754
- }), on(setSignalingName, (state, { instanceName }) => {
3755
- return { ...state, awsInstance: { ...state.awsInstance, instanceName } };
3756
- }), on(setOrchestrationParameters, (state, { instanceName, streamRequestId, eta }) => {
3757
- return {
3758
- ...state,
3759
- awsInstance: {
3760
- ...state.awsInstance,
3761
- message: undefined,
3762
- instanceName,
3763
- streamRequestId,
3764
- eta,
3765
- },
3766
- };
3767
- }), on(setOrchestrationProgress, (state, { progressComplete }) => {
3768
- return {
3769
- ...state,
3770
- awsInstance: {
3771
- ...state.awsInstance,
3772
- progressComplete,
3773
- },
3774
- };
3775
- }), on(setOrchestrationMessage, (state, { message }) => {
3776
- return {
3777
- ...state,
3778
- awsInstance: {
3779
- ...state.awsInstance,
3780
- message,
3781
- },
3782
- };
3783
- }), on(setLoopBackCommandIsCompleted, (state) => {
3784
- return { ...state, isFirstSuccessLoad: true };
3785
- }), on(setAfkTimerVisible, (state) => {
3786
- return { ...state, isAfkTimerVisible: true };
3787
- }), on(setAfkTimerHide, (state) => {
3788
- return { ...state, isAfkTimerVisible: false };
3789
- }), on(setOrchestrationContext, (state, { urls, environmentId, streamRequestContext }) => {
3790
- return {
3791
- ...state,
3792
- matchUrls: urls,
3793
- environmentId,
3794
- streamRequestContext,
3795
- };
3796
- }), on(setStreamClientCompanyId, (state, { id }) => {
3797
- return { ...state, streamClientCompanyId: id };
3798
- }), on(setStreamViewId, (state, { id }) => {
3799
- return { ...state, streamViewId: id };
3800
- }), on(setIntroImageSrc, (state, { src }) => {
3801
- return { ...state, imageIntroSrc: src };
3802
- }), on(setLoadingImageSrc, (state, { src }) => {
3803
- return { ...state, imageLoadingSrc: src };
3804
- }), on(setIntroVideoSrc, (state, { src }) => {
3805
- return { ...state, videoIntroSrc: src };
3806
- }), on(resetIntroSrc, (state) => {
3807
- return { ...state, imageIntroSrc: '', videoIntroSrc: '' };
3808
- }), on(saveAnalyticsEvent, (state, { event }) => {
3809
- return { ...state, analyticsEvent: event };
3810
- }), on(commandStarted, (state, { id, command }) => {
3811
- return {
3812
- ...state,
3813
- loaderCommands: {
3814
- ...state.loaderCommands,
3815
- totalCommandsStarted: state.loaderCommands.totalCommandsStarted + 1,
3816
- commandsInProgress: [
3817
- ...state.loaderCommands.commandsInProgress,
3818
- { id, command, timeStamp: new Date().getTime() },
3819
- ],
3820
- },
3821
- };
3822
- }), on(commandCompleted, (state, { id }) => {
3823
- return {
3824
- ...state,
3825
- loaderCommands: removeExileCommands(state.loaderCommands, id),
3826
- };
3827
- }), on(setCirrusConnected, (state) => {
3828
- return { ...state, cirrusConnected: true };
3829
- }), on(setCirrusDisconnected, (state) => {
3830
- return {
3831
- ...initialState,
3832
- establishingConnection: state.establishingConnection,
3833
- disconnectReason: state.disconnectReason,
3834
- wasInitialized: state.wasInitialized,
3835
- isFirstSuccessLoad: state.isFirstSuccessLoad,
3836
- matchUrls: state.matchUrls,
3837
- streamClientCompanyId: state.streamClientCompanyId,
3838
- streamViewId: state.streamViewId,
3839
- imageIntroSrc: state.imageIntroSrc,
3840
- videoIntroSrc: state.videoIntroSrc,
3841
- imageLoadingSrc: state.imageLoadingSrc,
3842
- environmentId: state.environmentId,
3843
- streamRequestContext: state.streamRequestContext,
3844
- streamConfig: state.streamConfig,
3845
- };
3846
- }), on(dropConnection, (state) => {
3847
- return { ...state, establishingConnection: false };
3848
- }), on(resetDataChannelForReconnect, (state) => {
3849
- return {
3850
- ...state,
3851
- isReconnecting: true,
3852
- dataChannelConnected: false,
3853
- viewportReady: false,
3854
- statusMessage: 'Reconnecting...',
3855
- };
3856
- }), on(setUnrealPlaywrightConfig, (state) => {
3857
- return {
3858
- ...state,
3859
- wasInitialized: true,
3860
- isFirstSuccessLoad: true,
3861
- cirrusConnected: true,
3862
- viewportReady: true,
3863
- dataChannelConnected: true,
3864
- isVideoPlaying: true,
3865
- };
3866
- }), on(destroyUnrealScene, () => {
3867
- return initialState;
3868
- }));
3676
+ observeFileResponse(mimetype, data, sender, timeOut = 60000) {
3677
+ const correlationId = generateUuid();
3678
+ const out = { ...data, correlationId };
3679
+ const observable = this.fileService.fileComplete$.pipe(switchMap$1((data) => {
3680
+ const { valid } = data;
3681
+ if (valid && this.fileHandlers[mimetype]) {
3682
+ return this.fileHandlers[mimetype](data, correlationId).pipe(filter(Truthy));
3683
+ }
3684
+ return of(null);
3685
+ }), timeout$1(timeOut), catchError((e) => {
3686
+ Logger.error(e);
3687
+ return of(null);
3688
+ }), take(1));
3689
+ setTimeout(() => sender(out), 0);
3690
+ return observable;
3691
+ }
3692
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: FileHandlerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
3693
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: FileHandlerService }); }
3694
+ }
3695
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: FileHandlerService, decorators: [{
3696
+ type: Injectable
3697
+ }] });
3698
+
3699
+ class AnalyticsService {
3700
+ #store = inject(Store);
3701
+ #destroy$ = new Subject();
3702
+ sendEventToMixPanel(name, params = {}) {
3703
+ Logger.log('Analytics Event:', name, params);
3704
+ this.#store.dispatch(saveAnalyticsEvent({ event: { name, params } }));
3705
+ }
3706
+ constructor() {
3707
+ const abortSignal$ = this.#store
3708
+ .select(unrealFeature.selectViewportReady)
3709
+ .pipe(filter$1(Falsy), map(() => void 0));
3710
+ this.#store
3711
+ .select(unrealFeature.selectViewportReady)
3712
+ .pipe(distinctUntilChanged(), filter$1(Truthy), tap(() => this.monitorMouse(abortSignal$)))
3713
+ .subscribe();
3714
+ }
3715
+ monitorMouse(abortSignal$) {
3716
+ this.#destroy$.next();
3717
+ const stop$ = merge(this.#destroy$.asObservable(), abortSignal$);
3718
+ const element = document.getElementById('streamingVideo');
3719
+ if (!element) {
3720
+ return;
3721
+ }
3722
+ // Monitor mousewheel events
3723
+ const mousewheel$ = fromEvent(element, 'wheel').pipe(takeUntil$1(stop$), throttleTime(1000));
3724
+ // Monitor mouse button states
3725
+ const mousedown$ = fromEvent(element, 'mousedown').pipe(takeUntil$1(stop$));
3726
+ const touchStart$ = fromEvent(element, 'touchstart').pipe(takeUntil$1(stop$));
3727
+ const touchMove$ = fromEvent(element, 'touchmove').pipe(takeUntil$1(stop$), throttleTime(200));
3728
+ // Track mouse movement while the left or right button is pressed
3729
+ mousedown$
3730
+ .pipe(filter$1((event) => event.button === 0 || event.button === 2), tap((downEvent) => {
3731
+ const { offsetX, offsetY } = this.getOffset(downEvent, element);
3732
+ this.sendEventToMixPanel('mouse_interaction', {
3733
+ viewX: offsetX / element.clientWidth,
3734
+ viewY: offsetY / element.clientHeight,
3735
+ action: downEvent.button === 0 ? 'spinning' : 'panning',
3736
+ });
3737
+ }))
3738
+ .subscribe();
3739
+ // Track mouse movement while the left or right button is pressed
3740
+ touchStart$
3741
+ .pipe(tap((downEvent) => {
3742
+ const totalTouches = downEvent.touches.length;
3743
+ const touch = downEvent.touches[0];
3744
+ const { offsetX, offsetY } = this.getOffset(touch, element);
3745
+ this.sendEventToMixPanel('touch_interaction', {
3746
+ viewX: offsetX / element.clientWidth,
3747
+ viewY: offsetY / element.clientHeight,
3748
+ action: totalTouches === 1 ? 'spinning' : 'panning',
3749
+ });
3750
+ }))
3751
+ .subscribe();
3752
+ let initialTouchDistance = 0;
3753
+ // Subscribe to mousewheel events
3754
+ touchMove$.subscribe((event) => {
3755
+ const totalTouches = event.touches.length;
3756
+ if (totalTouches < 2) {
3757
+ return;
3758
+ }
3759
+ // Calculate the current distance between two touches
3760
+ const touch1 = event.touches[0];
3761
+ const touch2 = event.touches[1];
3762
+ const offset1 = this.getOffset(touch1, element);
3763
+ const offset2 = this.getOffset(touch2, element);
3764
+ const currentDistance = Math.sqrt(Math.pow(offset2.offsetX - offset1.offsetX, 2) +
3765
+ Math.pow(offset2.offsetY - offset1.offsetY, 2));
3766
+ // Determine if zooming in or out based on distance change
3767
+ const deltaDistance = currentDistance - initialTouchDistance;
3768
+ const isZoomingIn = deltaDistance > 0;
3769
+ // Only send event if there's a significant change (avoid noise)
3770
+ if (Math.abs(deltaDistance) > 10) {
3771
+ this.sendEventToMixPanel('touch_zoom_interaction', {
3772
+ delta: isZoomingIn ? 1 : -1,
3773
+ action: isZoomingIn ? 'zoomin' : 'zoomout',
3774
+ });
3775
+ // Update initial distance for next comparison
3776
+ initialTouchDistance = currentDistance;
3777
+ }
3778
+ });
3779
+ mousewheel$.subscribe((event) => {
3780
+ this.sendEventToMixPanel('mouse_zoom_interaction', {
3781
+ delta: -(event.deltaY / Math.abs(event.deltaY)),
3782
+ action: event.deltaY < 0 ? 'zoomin' : 'zoomout',
3783
+ });
3784
+ });
3785
+ }
3786
+ getOffset(event, element) {
3787
+ const rect = element.getBoundingClientRect(); // in viewport coords
3788
+ const offsetX = event.clientX - rect.left; // relative to element
3789
+ const offsetY = event.clientY - rect.top;
3790
+ return { offsetX, offsetY };
3791
+ }
3792
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: AnalyticsService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
3793
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: AnalyticsService }); }
3794
+ }
3795
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: AnalyticsService, decorators: [{
3796
+ type: Injectable
3797
+ }], ctorParameters: () => [] });
3869
3798
 
3870
- const unrealFeature = createFeature({
3871
- name: 'unrealFeature',
3872
- reducer: unrealReducer,
3873
- });
3799
+ /**
3800
+ * Adaptive FPS controller that monitors the incoming WebRTC video stream
3801
+ * quality and dynamically adjusts Unreal Engine's `t.MaxFPS` console variable.
3802
+ *
3803
+ * ## How it works
3804
+ *
3805
+ * The service subscribes to `VideoService.videoStats$` (emitted every 250 ms)
3806
+ * and collects samples of FPS, bitrate, and QP (Quantisation Parameter).
3807
+ * Every {@link SAMPLE_WINDOW} ticks (~2 s) it averages the collected samples
3808
+ * and feeds them into a set of **fuzzy-logic membership functions** that
3809
+ * classify each metric into overlapping quality bands.
3810
+ *
3811
+ * ### Input signals
3812
+ *
3813
+ * **1. FPS ratio** = `actualFPS / currentTargetFPS` (0..1+)
3814
+ *
3815
+ * | Band | Range | Meaning |
3816
+ * |--------|---------------------|-----------------------------------------------|
3817
+ * | low | ratio < 0.7 → 1.0, linear ramp 0.7..0.9 → 0 | Stream cannot keep up with the target |
3818
+ * | ok | triangle 0.8..1.0, peak at 0.95 | Stream roughly matches the target |
3819
+ * | high | ratio 0.9..1.0 → ramp, >1.0 → 1.0 | Stream meets or exceeds the target |
3820
+ *
3821
+ * **2. Bitrate load** = `currentBitrate / MAX_BITRATE` (0..1+)
3822
+ *
3823
+ * `MAX_BITRATE` = 20 Mbit/s — the reference ceiling for the encoder.
3824
+ *
3825
+ * | Band | Range | Meaning |
3826
+ * |--------|-------------------------------------------------|-----------------------------------------------|
3827
+ * | high | load 0.5..0.8 → ramp, >0.8 → 1.0 | Encoder is using most of the available bandwidth |
3828
+ * | not high | load < 0.5 → 0.0 | Bandwidth headroom is available |
3829
+ *
3830
+ * **3. Quality** — mapped from QP via `mapQpToQuality()` (see {@link Quality}):
3831
+ *
3832
+ * | Quality | QP range | Meaning |
3833
+ * |----------|-----------|-------------------------------------------------|
3834
+ * | `lime` | QP <= 26 | Good — encoder has enough headroom |
3835
+ * | `orange` | QP 27..35 | Fair — encoder is under moderate pressure |
3836
+ * | `red` | QP > 35 | Poor — heavy compression, visible artefacts |
3837
+ *
3838
+ * ### Decision rules (evaluated in priority order, first match wins)
3839
+ *
3840
+ * | # | Condition | Decision |
3841
+ * |---|------------------------------------------------------------|--------------|
3842
+ * | 1 | FPS ratio is **low** (membership > 0.5) | **decrease** |
3843
+ * | 2 | FPS ratio is **ok** AND quality is `red` | **decrease** |
3844
+ * | 3 | FPS ratio is **ok** AND bitrate **high** AND quality is `orange` | **hold** |
3845
+ * | 4 | FPS ratio is **high** AND quality is `lime` AND bitrate **not high** | **increase** |
3846
+ * | — | Everything else | **hold** |
3847
+ *
3848
+ * ## Cooldown-based throttling
3849
+ *
3850
+ * The service continuously evaluates fuzzy rules every sample window.
3851
+ * After each FPS step change it enforces a cooldown before the next
3852
+ * change can be applied. The cooldown duration depends on the
3853
+ * (previous decision → next decision) pair:
3854
+ *
3855
+ * | Previous | Next | Cooldown |
3856
+ * |-----------|-----------|-----------------------------------------------------|
3857
+ * | decrease | decrease | {@link COOLDOWN_DECREASE_AFTER_DECREASE_MS} (2 s) |
3858
+ * | decrease | increase | {@link COOLDOWN_INCREASE_AFTER_DECREASE_MS} (10 s) |
3859
+ * | increase | decrease | {@link COOLDOWN_AFTER_INCREASE_MS} (2 s) |
3860
+ * | increase | increase | {@link COOLDOWN_AFTER_INCREASE_MS} (2 s) |
3861
+ *
3862
+ * This means the system reacts quickly after an increase (2 s in any
3863
+ * direction) but is cautious about stepping back up after a decrease
3864
+ * (10 s), while still allowing consecutive decreases at 2 s intervals.
3865
+ *
3866
+ * ## FPS dispatch
3867
+ *
3868
+ * FPS changes are dispatched via the NgRx `setMaxFps` action, which triggers
3869
+ * the existing `UnrealEffects.setMaxFps$` effect that sends the
3870
+ * `t.MaxFPS <value>` console command to Unreal Engine.
3871
+ */
3872
+ class FpsMonitorService {
3873
+ constructor() {
3874
+ this.videoService = inject(VideoService);
3875
+ this.store = inject(Store);
3876
+ /** Discrete FPS targets the controller can switch between (ascending order). */
3877
+ this.FPS_STEPS = [30, 40, 50, 60];
3878
+ /** Reference ceiling for bitrate (bits/sec) used to normalise bitrate load to 0..1. */
3879
+ this.MAX_BITRATE = 20_000_000;
3880
+ /**
3881
+ * Cooldown (ms) for any decision (increase or decrease) following an increase.
3882
+ * After an increase the system should react faster.
3883
+ */
3884
+ this.COOLDOWN_AFTER_INCREASE_MS = 2_000;
3885
+ /**
3886
+ * Cooldown (ms) for a decrease following a previous decrease.
3887
+ */
3888
+ this.COOLDOWN_DECREASE_AFTER_DECREASE_MS = 2_000;
3889
+ /**
3890
+ * Cooldown (ms) for an increase following a previous decrease.
3891
+ * The stream needs more time to stabilise before stepping back up.
3892
+ */
3893
+ this.COOLDOWN_INCREASE_AFTER_DECREASE_MS = 10_000;
3894
+ /**
3895
+ * Number of videoStats$ ticks to accumulate before evaluating.
3896
+ * At 250 ms per tick this gives a ~2 s sliding evaluation window,
3897
+ * long enough to smooth out single-frame jitter.
3898
+ */
3899
+ this.SAMPLE_WINDOW = 8;
3900
+ /** Current FPS target; starts at max (60 FPS) and adapts downward/upward. */
3901
+ this.currentFpsTarget = this.FPS_STEPS[this.FPS_STEPS.length - 1];
3902
+ this.samples = [];
3903
+ this.lastDecisionTime = Date.now();
3904
+ this.lastUpgrade = 'hold';
3905
+ this.init();
3906
+ }
3907
+ init() {
3908
+ this.videoService.videoStats$
3909
+ .pipe(withLatestFrom(this.store.select(unrealFeature.selectViewportReady)), filter(([, viewportReady]) => viewportReady))
3910
+ .subscribe(([stats]) => this.processTick(stats));
3911
+ }
3912
+ /**
3913
+ * Called on every videoStats$ emission (~250 ms).
3914
+ * Accumulates samples and evaluates once per SAMPLE_WINDOW.
3915
+ */
3916
+ processTick(stats) {
3917
+ const { framesPerSecond, bitrate, VideoEncoderQP } = stats.aggregatedStats;
3918
+ this.samples.push({
3919
+ fps: framesPerSecond ?? 0,
3920
+ bitrate: bitrate ?? 0,
3921
+ qp: VideoEncoderQP ?? 0,
3922
+ });
3923
+ // Wait until we have enough samples for a reliable average.
3924
+ if (this.samples.length < this.SAMPLE_WINDOW) {
3925
+ return;
3926
+ }
3927
+ const avg = this.averageSamples();
3928
+ this.samples = [];
3929
+ this.handleMonitoring(avg, this.currentFpsTarget);
3930
+ }
3931
+ handleMonitoring(avg, currentTarget) {
3932
+ const decision = this.evaluateDecision(avg, currentTarget);
3933
+ this.upgradeFps(decision, avg.qp);
3934
+ }
3935
+ /**
3936
+ * Combines fuzzy membership values into a single discrete decision.
3937
+ * Rules are evaluated in priority order — first match wins.
3938
+ *
3939
+ * Inputs:
3940
+ * - `fpsRatio` = avgFPS / currentTargetFPS (e.g. 45/60 = 0.75)
3941
+ * - `bitrateLoad` = avgBitrate / 20 Mbit/s (e.g. 12M/20M = 0.6)
3942
+ * - `quality` = mapQpToQuality(avgQP) → 'lime' | 'orange' | 'red'
3943
+ */
3944
+ evaluateDecision(avg, currentTarget) {
3945
+ const fpsRatio = currentTarget > 0 ? avg.fps / currentTarget : 1;
3946
+ const bitrateLoad = avg.bitrate / this.MAX_BITRATE;
3947
+ const fpsLow = this.membershipFpsLow(fpsRatio);
3948
+ const fpsOk = this.membershipFpsOk(fpsRatio);
3949
+ const fpsHigh = this.membershipFpsHigh(fpsRatio);
3950
+ const bitrateHigh = this.membershipBitrateHigh(bitrateLoad);
3951
+ // Quality is derived from QP: lime (QP <= 26), orange (QP 27..35), red (QP > 35).
3952
+ const quality = mapQpToQuality(avg.qp);
3953
+ // Rule 1: FPS clearly below target (ratio roughly < 0.8) → step down immediately.
3954
+ if (fpsLow > 0.5) {
3955
+ return 'decrease';
3956
+ }
3957
+ // Rule 2: FPS looks acceptable numerically, but encoder quality is 'red'
3958
+ // (QP > 35 — heavy compression artefacts). Reducing target FPS frees
3959
+ // encoder headroom and improves visual quality.
3960
+ if (fpsOk > 0.4 && quality === 'red') {
3961
+ return 'decrease';
3962
+ }
3963
+ // Rule 3: FPS is ok, but bitrate is above ~50-80% of 20 Mbit/s ceiling
3964
+ // and quality is 'orange' (QP 27..35). Near capacity — don't push higher.
3965
+ if (fpsOk > 0.4 && bitrateHigh > 0.5 && quality === 'orange') {
3966
+ return 'hold';
3967
+ }
3968
+ // Rule 4: FPS meets/exceeds target (ratio >= ~0.95), quality is 'lime'
3969
+ // (QP <= 26), and bitrate is below 50% of ceiling — safe to step up.
3970
+ if (fpsHigh > 0.5 &&
3971
+ quality === 'lime' &&
3972
+ bitrateHigh < 0.5 &&
3973
+ this.currentFpsTarget < this.FPS_STEPS[this.FPS_STEPS.length - 1]) {
3974
+ return 'increase';
3975
+ }
3976
+ return 'hold';
3977
+ }
3978
+ // ---------------------------------------------------------------------------
3979
+ // Fuzzy membership functions
3980
+ //
3981
+ // Each function maps a normalised input (0..1+) to a membership degree (0..1).
3982
+ // Overlapping trapezoid/triangle shapes let multiple categories be partially
3983
+ // active at the same time, which makes the rules less brittle than hard thresholds.
3984
+ // ---------------------------------------------------------------------------
3985
+ /**
3986
+ * FPS ratio membership "low".
3987
+ * Input: ratio = actualFPS / targetFPS (e.g. 42/60 = 0.70).
3988
+ *
3989
+ * ratio < 0.7 → 1.0 (clearly failing, e.g. 40/60)
3990
+ * ratio 0.7..0.9 → linear ramp down (e.g. 0.8 → 0.5)
3991
+ * ratio > 0.9 → 0.0 (keeping up)
3992
+ */
3993
+ membershipFpsLow(ratio) {
3994
+ if (ratio < 0.7)
3995
+ return 1.0;
3996
+ if (ratio > 0.9)
3997
+ return 0.0;
3998
+ return (0.9 - ratio) / 0.2;
3999
+ }
4000
+ /**
4001
+ * FPS ratio membership "ok" — triangle shape.
4002
+ * Input: ratio = actualFPS / targetFPS.
4003
+ *
4004
+ * ratio < 0.8 → 0.0
4005
+ * ratio 0.8..0.95 → ramp up, peak at 0.95 (e.g. 57/60)
4006
+ * ratio 0.95..1.0 → ramp down
4007
+ * ratio > 1.0 → 0.0
4008
+ */
4009
+ membershipFpsOk(ratio) {
4010
+ if (ratio < 0.8)
4011
+ return 0.0;
4012
+ if (ratio < 0.95)
4013
+ return (ratio - 0.8) / 0.15;
4014
+ if (ratio <= 1.0)
4015
+ return (1.0 - ratio) / 0.05;
4016
+ return 0.0;
4017
+ }
4018
+ /**
4019
+ * FPS ratio membership "high".
4020
+ * Input: ratio = actualFPS / targetFPS.
4021
+ *
4022
+ * ratio < 0.9 → 0.0
4023
+ * ratio 0.9..1.0 → linear ramp up (e.g. 0.95 → 0.5)
4024
+ * ratio >= 1.0 → 1.0 (meeting or exceeding target, e.g. 60/60)
4025
+ */
4026
+ membershipFpsHigh(ratio) {
4027
+ if (ratio < 0.9)
4028
+ return 0.0;
4029
+ if (ratio > 1.0)
4030
+ return 1.0;
4031
+ return (ratio - 0.9) / 0.1;
4032
+ }
4033
+ /**
4034
+ * Bitrate load membership "high".
4035
+ * Input: load = currentBitrate / MAX_BITRATE (20 Mbit/s).
4036
+ *
4037
+ * load < 0.5 → 0.0 (below 10 Mbit/s — plenty of headroom)
4038
+ * load 0.5..0.8 → linear ramp (e.g. 13M/20M = 0.65 → 0.5)
4039
+ * load > 0.8 → 1.0 (above 16 Mbit/s — near capacity)
4040
+ */
4041
+ membershipBitrateHigh(load) {
4042
+ if (load < 0.5)
4043
+ return 0.0;
4044
+ if (load > 0.8)
4045
+ return 1.0;
4046
+ return (load - 0.5) / 0.3;
4047
+ }
4048
+ // ---------------------------------------------------------------------------
4049
+ // Step actions
4050
+ // ---------------------------------------------------------------------------
4051
+ upgradeFps(decision, qp) {
4052
+ if (decision === 'hold')
4053
+ return;
4054
+ const now = Date.now();
4055
+ // Throttle: cooldown depends on (previous decision → current decision) pair.
4056
+ const isStepUp = decision === 'increase';
4057
+ const cooldown = this.lastUpgrade === 'decrease'
4058
+ ? isStepUp
4059
+ ? this.COOLDOWN_INCREASE_AFTER_DECREASE_MS
4060
+ : this.COOLDOWN_DECREASE_AFTER_DECREASE_MS
4061
+ : this.COOLDOWN_AFTER_INCREASE_MS;
4062
+ if (now - this.lastDecisionTime < cooldown) {
4063
+ return;
4064
+ }
4065
+ this.lastUpgrade = decision;
4066
+ const quality = mapQpToQuality(qp);
4067
+ const newTarget = this.getNextFpsTarget(this.currentFpsTarget, decision);
4068
+ Logger.info(`[FpsMonitor] Decision: ${decision} → FPS: ${newTarget}, Quality: ${quality}`);
4069
+ if (newTarget !== this.currentFpsTarget) {
4070
+ this.currentFpsTarget = newTarget;
4071
+ this.store.dispatch(setMaxFps({ maxFps: newTarget }));
4072
+ }
4073
+ this.lastDecisionTime = now;
4074
+ this.samples = [];
4075
+ }
4076
+ /**
4077
+ * Get the next FPS target based on current target and decision.
4078
+ * Clamps to valid FPS_STEPS range.
4079
+ */
4080
+ getNextFpsTarget(currentTarget, decision) {
4081
+ const currentIndex = this.FPS_STEPS.indexOf(currentTarget);
4082
+ // If current target is not in FPS_STEPS, find closest one
4083
+ const validIndex = currentIndex === -1
4084
+ ? this.findClosestFpsIndex(currentTarget)
4085
+ : currentIndex;
4086
+ const delta = decision === 'increase' ? 1 : -1;
4087
+ const newIndex = clampf(0, this.FPS_STEPS.length - 1, validIndex + delta);
4088
+ return this.FPS_STEPS[newIndex];
4089
+ }
4090
+ /**
4091
+ * Find the closest FPS step index for a given target value.
4092
+ */
4093
+ findClosestFpsIndex(target) {
4094
+ let closestIndex = 0;
4095
+ let closestDiff = Math.abs(this.FPS_STEPS[0] - target);
4096
+ for (let i = 1; i < this.FPS_STEPS.length; i++) {
4097
+ const diff = Math.abs(this.FPS_STEPS[i] - target);
4098
+ if (diff < closestDiff) {
4099
+ closestDiff = diff;
4100
+ closestIndex = i;
4101
+ }
4102
+ }
4103
+ return closestIndex;
4104
+ }
4105
+ /** Average all collected samples for the current evaluation window. */
4106
+ averageSamples() {
4107
+ const len = this.samples.length;
4108
+ if (len === 0)
4109
+ return { fps: 0, bitrate: 0, qp: 0 };
4110
+ let fps = 0;
4111
+ let bitrate = 0;
4112
+ let qp = 0;
4113
+ for (const s of this.samples) {
4114
+ fps += s.fps;
4115
+ bitrate += s.bitrate;
4116
+ qp += s.qp;
4117
+ }
4118
+ return { fps: fps / len, bitrate: bitrate / len, qp: qp / len };
4119
+ }
4120
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: FpsMonitorService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
4121
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: FpsMonitorService }); }
4122
+ }
4123
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: FpsMonitorService, decorators: [{
4124
+ type: Injectable
4125
+ }], ctorParameters: () => [] });
3874
4126
 
3875
4127
  class UnrealErrorModalComponent {
3876
4128
  constructor() {
@@ -4615,7 +4867,7 @@ class LatencyTimings {
4615
4867
  OnAllLatencyTimingsReady(_) { }
4616
4868
  }
4617
4869
 
4618
- function mapQpToQuality(VideoEncoderQP) {
4870
+ const mapQpToQuality = (VideoEncoderQP) => {
4619
4871
  const orangeQP = 26;
4620
4872
  const redQP = 35;
4621
4873
  let quality = 'lime';
@@ -4626,17 +4878,17 @@ function mapQpToQuality(VideoEncoderQP) {
4626
4878
  quality = 'orange';
4627
4879
  }
4628
4880
  return quality;
4629
- }
4881
+ };
4630
4882
 
4631
4883
  const removeExileCommands = (state, idToRemove) => {
4632
- const exileTimout = 10000;
4884
+ const exileTimeout = 10000;
4633
4885
  const out = { ...state };
4634
4886
  out.commandsInProgress = out.commandsInProgress.filter(whereNot({ id: idToRemove }));
4635
4887
  out.totalCommandsCompleted++;
4636
4888
  const time = new Date().getTime();
4637
4889
  const markForDelete = [];
4638
4890
  Object.entries(out.commandsInProgress).forEach(([, { id, timeStamp }]) => {
4639
- if (time - timeStamp > exileTimout) {
4891
+ if (time - timeStamp > exileTimeout) {
4640
4892
  markForDelete.push(id);
4641
4893
  }
4642
4894
  });
@@ -5585,9 +5837,87 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
5585
5837
  args: [{ selector: 'app-low-bandwidth-detector', changeDetection: ChangeDetectionStrategy.OnPush, imports: [FilterSettingsComponent], template: "@if (isReducedQuality() || isLowBandwidth()) {\n <div\n [class.expanded]=\"isIndicatorExpanded() && isLowBandwidth()\"\n class=\"lbm-indicator freeze-loader\"\n >\n <div (click)=\"toggleIndicator(true)\" class=\"lbm-icon\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 20 20\">\n <path\n fill=\"#85888E\"\n d=\"M7.189 3.605c-.73.145-1.438.35-2.126.614A14.412 14.412 0 0 0 .896 6.666a1.08 1.08 0 0 0-.375.844c0 .34.118.629.354.865s.524.36.865.375c.34.014.65-.09.927-.313a11.89 11.89 0 0 1 3.385-1.916 10.94 10.94 0 0 1 1.235-.375l-.098-2.541ZM7.385 8.708a9.107 9.107 0 0 0-2.906 1.48c-.264.194-.402.464-.416.812-.014.347.104.646.354.896.236.236.524.364.864.385.34.02.664-.073.97-.281.422-.29.878-.53 1.368-.721a2.545 2.545 0 0 1-.166-.814l-.068-1.757ZM12.386 11.267c.094-.25.15-.52.161-.802l.068-1.755a9.019 9.019 0 0 1 2.927 1.52c.264.193.4.46.406.801.007.34-.114.635-.364.885a1.213 1.213 0 0 1-.865.365A1.614 1.614 0 0 1 13.77 12a6.574 6.574 0 0 0-1.385-.733ZM12.713 6.146l.098-2.542c.73.146 1.438.351 2.127.615 1.541.59 2.93 1.406 4.166 2.447.25.223.379.5.386.834.007.333-.115.625-.365.875a1.253 1.253 0 0 1-.864.375c-.34.014-.65-.09-.927-.313a11.892 11.892 0 0 0-3.386-1.916 10.94 10.94 0 0 0-1.235-.375ZM8.813 16.187c.32.32.715.48 1.187.48.472 0 .868-.16 1.188-.48.32-.32.479-.715.479-1.187 0-.473-.16-.868-.48-1.188-.319-.32-.715-.479-1.187-.479-.472 0-.868.16-1.187.48-.32.319-.48.714-.48 1.187 0 .472.16.868.48 1.187Z\"\n />\n <path\n fill=\"#fff\"\n fill-rule=\"evenodd\"\n d=\"M10 .833c-.91 0-1.637.756-1.602 1.665l.304 7.92a1.299 1.299 0 0 0 2.596 0l.305-7.92A1.604 1.604 0 0 0 10 .833ZM8.813 16.187c.32.32.715.48 1.187.48.472 0 .868-.16 1.188-.48.32-.32.479-.715.479-1.187 0-.473-.16-.868-.48-1.188-.319-.32-.715-.479-1.187-.479-.472 0-.868.16-1.187.48-.32.319-.48.714-.48 1.187 0 .472.16.868.48 1.187Z\"\n clip-rule=\"evenodd\"\n />\n </svg>\n </div>\n\n <div\n [class.lbm-message--open]=\"isLowBandwidth() && isIndicatorExpanded()\"\n class=\"lbm-message\"\n >\n <p class=\"lbm-description\">\n Fluid Interactivity Modes were disabled due to an unstable connection.\n\n <button\n (click)=\"openLBMDialog()\"\n [attr.data-testid]=\"'learn-more-lbm'\"\n type=\"button\"\n class=\"lbm-learn-more\"\n >\n Learn more\n </button>\n </p>\n <button\n (click)=\"toggleIndicator(false)\"\n [attr.data-testid]=\"'close-lbm-indicator'\"\n type=\"button\"\n class=\"lbm-close\"\n >\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"20\"\n height=\"20\"\n viewBox=\"0 0 20 20\"\n fill=\"none\"\n >\n <path\n d=\"M10.0001 11.2289L6.68618 14.5428C6.51951 14.7095 6.31818 14.7895 6.08218 14.7828C5.84618 14.7755 5.64485 14.6885 5.47818 14.5218C5.31151 14.3552 5.22818 14.1505 5.22818 13.9078C5.22818 13.6645 5.31151 13.4595 5.47818 13.2928L8.77107 9.99992L5.45718 6.68603C5.29051 6.51936 5.21051 6.3147 5.21718 6.07203C5.22451 5.8287 5.31151 5.6237 5.47818 5.45703C5.64485 5.29036 5.84951 5.20703 6.09218 5.20703C6.33551 5.20703 6.54051 5.29036 6.70718 5.45703L10.0001 8.77092L13.314 5.45703C13.4806 5.29036 13.6853 5.20703 13.928 5.20703C14.1713 5.20703 14.3763 5.29036 14.543 5.45703C14.7096 5.6237 14.793 5.8287 14.793 6.07203C14.793 6.3147 14.7096 6.51936 14.543 6.68603L11.2291 9.99992L14.543 13.3138C14.7096 13.4805 14.793 13.6818 14.793 13.9178C14.793 14.1538 14.7096 14.3552 14.543 14.5218C14.3763 14.6885 14.1713 14.7718 13.928 14.7718C13.6853 14.7718 13.4806 14.6885 13.314 14.5218L10.0001 11.2289Z\"\n fill=\"white\"\n />\n </svg>\n </button>\n </div>\n </div>\n}\n\n@if (isDevMode) {\n <app-filter-settings />\n}\n", styles: [".freeze-loader,.lbm-indicator{--lbmPositionTop: 18px;--lbmPositionLeft: 18px;--lbmPositionRight: 18px;--lbmIndicatorSize: 36px;--lbmIndicatorBorderRadius: 8px;--lbmIndicatorCloseSize: 20px;--lbmPadding: 10px;--lbmTextSize: 13px;--lbmLineHeight: 20px;--lbmTextColor: #ffffff;--lbmBackgroundColor: rgba(17, 24, 39, .7);position:absolute;top:var(--lbmPositionTop);left:var(--lbmPositionLeft);z-index:10;display:flex;justify-content:center;align-items:flex-start;min-width:var(--lbmIndicatorSize);min-height:var(--lbmIndicatorSize);overflow:hidden}.freeze-loader.expanded,.lbm-indicator.expanded{--lbmPositionTop: 8px;--lbmPositionLeft: 8px;--lbmPositionRight: 8px;width:auto;max-width:calc(100% - var(--lbmPositionLeft) - var(--lbmPositionRight));padding:var(--lbmPadding);background:var(--lbmBackgroundColor);-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);border-radius:var(--lbmIndicatorBorderRadius);gap:12px}.freeze-loader .lbm-icon,.lbm-indicator .lbm-icon{display:flex;width:var(--lbmIndicatorSize);height:var(--lbmIndicatorSize);padding:8px;color:#fff;background-color:var(--lbmBackgroundColor);border-radius:50%;flex-shrink:0;cursor:pointer}.freeze-loader .lbm-icon svg,.freeze-loader .lbm-icon img,.lbm-indicator .lbm-icon svg,.lbm-indicator .lbm-icon img{width:100%;height:100%}.freeze-loader .lbm-close,.lbm-indicator .lbm-close{position:absolute;top:0;right:0;width:var(--lbmIndicatorCloseSize);height:var(--lbmIndicatorCloseSize);background-color:transparent;border:none;padding:0;cursor:pointer}.freeze-loader .lbm-message,.lbm-indicator .lbm-message{position:relative;opacity:0;height:0;max-width:0;padding-right:24px;pointer-events:none;visibility:hidden;animation:closeMessage .3s forwards}.freeze-loader .lbm-message--open,.lbm-indicator .lbm-message--open{position:relative;visibility:visible;display:flex;align-items:center;align-self:center;pointer-events:all;gap:12px;max-width:fit-content;height:auto;opacity:1;animation:openMessage .3s forwards}.freeze-loader .lbm-description,.lbm-indicator .lbm-description{margin:0;font-size:var(--lbmTextSize);font-weight:400;line-height:var(--lbmLineHeight);color:var(--lbmTextColor)}.freeze-loader .lbm-learn-more,.lbm-indicator .lbm-learn-more{display:inline-flex;padding:0;font-size:var(--lbmTextSize);font-weight:400;line-height:var(--lbmLineHeight);color:var(--lbmTextColor);-webkit-text-decoration-line:underline;text-decoration-line:underline;background-color:transparent;border:none;cursor:pointer}.freeze-loader .lbm-learn-more:hover,.lbm-indicator .lbm-learn-more:hover{text-decoration:none}@keyframes openMessage{0%{opacity:0;max-width:0;height:0}10%{height:auto;max-width:fit-content}to{opacity:1;height:auto;max-width:fit-content}}@keyframes closeMessage{0%{opacity:1;max-width:fit-content}to{opacity:0;max-width:0}}\n"] }]
5586
5838
  }] });
5587
5839
 
5840
+ function provideAngularUnrealModule(config) {
5841
+ return makeEnvironmentProviders([
5842
+ provideState(unrealFeature),
5843
+ provideEffects([UnrealEffects]),
5844
+ ConsoleExtensionsService,
5845
+ InputService,
5846
+ VideoService,
5847
+ WebRtcPlayerService,
5848
+ RegionsPingService,
5849
+ FileReceiverService,
5850
+ FileHandlerService,
5851
+ FpsMonitorService,
5852
+ AnalyticsService,
5853
+ {
5854
+ provide: StreamStatusTelemetryService,
5855
+ useClass: config?.playwright
5856
+ ? StreamStatusTelemetryPlaywrightService
5857
+ : StreamStatusTelemetryService,
5858
+ },
5859
+ {
5860
+ provide: CommandTelemetryService,
5861
+ useClass: config?.playwright
5862
+ ? CommandTelemetryPlaywrightService
5863
+ : CommandTelemetryService,
5864
+ },
5865
+ {
5866
+ provide: AggregatorService,
5867
+ useClass: config?.playwright
5868
+ ? AggregatorPlaywrightService
5869
+ : AggregatorService,
5870
+ },
5871
+ {
5872
+ provide: FreezeFrameService,
5873
+ useClass: config?.playwright
5874
+ ? FreezeFramePlaywrightService
5875
+ : FreezeFrameService,
5876
+ },
5877
+ {
5878
+ provide: UnrealCommunicatorService,
5879
+ useClass: config?.playwright
5880
+ ? UnrealCommunicatorPlaywrightService
5881
+ : UnrealCommunicatorService,
5882
+ },
5883
+ {
5884
+ provide: AFKService,
5885
+ useClass: config?.playwright ? AfkPlaywrightService : AFKService,
5886
+ },
5887
+ {
5888
+ provide: SignallingService,
5889
+ useClass: config?.playwright
5890
+ ? SignallingPlaywrightService
5891
+ : SignallingService,
5892
+ },
5893
+ {
5894
+ provide: ConsoleExtensionsService,
5895
+ useClass: config?.playwright
5896
+ ? ConsoleExtensionsPlaywrightService
5897
+ : ConsoleExtensionsService,
5898
+ },
5899
+ {
5900
+ provide: FileReceiverService,
5901
+ useClass: config?.playwright
5902
+ ? FileReceiverPlaywrightService
5903
+ : FileReceiverService,
5904
+ },
5905
+ provideEnvironmentInitializer(() => {
5906
+ inject(AggregatorService);
5907
+ inject(InputService);
5908
+ inject(StreamStatusTelemetryService);
5909
+ inject(ConsoleExtensionsService);
5910
+ inject(AFKService);
5911
+ inject(FreezeFrameService);
5912
+ inject(AnalyticsService);
5913
+ inject(FpsMonitorService);
5914
+ }),
5915
+ ]);
5916
+ }
5917
+
5588
5918
  /**
5589
5919
  * Generated bundle index. Do not edit.
5590
5920
  */
5591
5921
 
5592
- export { AFKService, AfkPlaywrightService, AggregatorPlaywrightService, AggregatorService, AnalyticsService, AnswerHandler, CONSOLE_COMMAND_DISABLE_MESSAGES, CONSOLE_COMMAND_ENABLE_MESSAGES, CONSOLE_COMMAND_PIXEL_QUALITY, ClickableOverlayComponent, CommandTelemetryPlaywrightService, CommandTelemetryService, ConfigHandler, ConsoleExtensionsPlaywrightService, ConsoleExtensionsService, 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, DataFlowMonitor, DevModeService, DisconnectReason, EControlSchemeType, EMessageType, EToClientMessageType, FULL_HD_HEIGHT, FULL_HD_WIDTH, FileHandlerService, FileReceiverPlaywrightService, FileReceiverService, FilterSettingsComponent, FreezeFrameComponent, FreezeFramePlaywrightService, FreezeFrameService, IceCandidateHandler, ImageLoadingSrcComponent, InputOptions, InputPlaywrightService, InputService, InstanceReadyHandler, InstanceReservedHandler, IntroSrcComponent, KalmanFilter1D, LatencyTimings, LowBandwidthDetectorComponent, LowBandwidthModalComponent, MINIMAL_FPS, MouseButton, MouseButtonsMask, OnCloseHandler, OnErrorHandler, OnMessageHandler, OnOpenHandler, OrchestrationMessageTypes, POLLING_TIME, PingHandler, PlayerCountHandler, RegionsPingService, ResetTelemetry, SAME_SIZE_THRESHOLD, SCREEN_LOCKER_CONTAINER_ID, SIGNALLING_PERCENT_VALUE, SSInfoHandler, STREAMING_VIDEO_ID, SafePipe, SignallingPlaywrightService, SignallingService, SpecialKeyCodes, StatGraphComponent, StreamStatusTelemetryPlaywrightService, StreamStatusTelemetryService, SubService, TelemetryStart, TelemetryStop, UNREAL_CONFIG, UnrealCommunicatorPlaywrightService, UnrealCommunicatorService, UnrealEffects, UnrealInternalSignalEvents, UnrealSceneComponent, UnrealStatusMessage, VideoRecorder, 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, changeLowBandwidth, changeStatusMainVideoOnScene, changeStreamResolutionAction, changeStreamResolutionSuccessAction, commandCompleted, commandStarted, dataChannelConnected, dataChannelReady, decodeData, destroyRemoteConnections, destroyUnrealScene, disconnectStream, dispatchResize, dropConnection, floatToSmoothPercents, forceResizeUnrealVideo, fromResizeObserver, fromSignal, fromUnrealCallBackSignal, getActiveUrl, getImageFromVideoStream, getRtcErrorMessage, iceConnectionFailed, initSignalling, initialState, isLoaderScreenVisible, keepMaxUntilReset, mapQpToQuality, observeCommandResponse, provideAngularUnrealModule, reconnectPeer, reconnectPeerFailed, reconnectPeerSuccess, removeExileCommands, resetAfk, resetAfkAction, resetConfig, resetDataChannelForReconnect, resetIntroSrc, resetWarnTimeout, saveAnalyticsEvent, selectClientAndViewIds, selectCommandProgress, selectCommandsInProgress, selectFreezeFrameCombinedDataUrl, selectFreezeFrameDataUrl, selectFreezeFrameDataUrlFromVideo, selectFreezeFrameProgressMessageFromVideo, selectIsAutostart, selectIsExistMatchUrls, selectIsFreezeFrameLoading, selectIsVideoPlayingAndDataChannelConnected, selectLastCommandInProgress, selectLoaderCommands, selectShowLoader, selectShowReconnectPopup, selectSignalingParameters, selectStreamConfig, selectTotalProgress, selectWarnTimeout, sendSignal, setAfkTimerHide, setAfkTimerVisible, setAwsInstance, setCirrusConnected, setCirrusDisconnected, setConfig, setDataChannelConnected, setEstablishingConnection, setFreezeFrame, setFreezeFrameFromVideo, setIntroImageSrc, setIntroVideoSrc, setLoadingImageSrc, setLoopBackCommandIsCompleted, setMaxFps, setOrchestrationContext, setOrchestrationMessage, setOrchestrationParameters, setOrchestrationProgress, setSignalingName, setStatusMessage, setStatusPercentSignallingServer, setStreamClientCompanyId, setStreamViewId, setUnrealPlaywrightConfig, setViewportNotReady, setViewportReady, showPopupWithoutAutoStart, showUnrealErrorMessage, smoothTransition, startStream, trackMixpanelEvent, unrealFeature, unrealReducer, updateCirrusInfo };
5922
+ export { AFKService, AfkPlaywrightService, AggregatorPlaywrightService, AggregatorService, AnalyticsService, AnswerHandler, CONSOLE_COMMAND_DISABLE_MESSAGES, CONSOLE_COMMAND_ENABLE_MESSAGES, CONSOLE_COMMAND_PIXEL_QUALITY, ClickableOverlayComponent, CommandTelemetryPlaywrightService, CommandTelemetryService, ConfigHandler, ConsoleExtensionsPlaywrightService, ConsoleExtensionsService, 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, DataFlowMonitor, DevModeService, DisconnectReason, EControlSchemeType, EMessageType, EToClientMessageType, FULL_HD_HEIGHT, FULL_HD_WIDTH, FileHandlerService, FileReceiverPlaywrightService, FileReceiverService, FilterSettingsComponent, FpsMonitorService, FreezeFrameComponent, FreezeFramePlaywrightService, FreezeFrameService, IceCandidateHandler, ImageLoadingSrcComponent, InputOptions, InputPlaywrightService, InputService, InstanceReadyHandler, InstanceReservedHandler, IntroSrcComponent, KalmanFilter1D, LatencyTimings, LowBandwidthDetectorComponent, LowBandwidthModalComponent, MINIMAL_FPS, MouseButton, MouseButtonsMask, OnCloseHandler, OnErrorHandler, OnMessageHandler, OnOpenHandler, OrchestrationMessageTypes, POLLING_TIME, PingHandler, PlayerCountHandler, RegionsPingService, ResetTelemetry, SAME_SIZE_THRESHOLD, SCREEN_LOCKER_CONTAINER_ID, SIGNALLING_PERCENT_VALUE, SSInfoHandler, STREAMING_VIDEO_ID, SafePipe, SignallingPlaywrightService, SignallingService, SpecialKeyCodes, StatGraphComponent, StreamStatusTelemetryPlaywrightService, StreamStatusTelemetryService, SubService, TelemetryStart, TelemetryStop, UNREAL_CONFIG, UnrealCommunicatorPlaywrightService, UnrealCommunicatorService, UnrealEffects, UnrealInternalSignalEvents, UnrealSceneComponent, UnrealStatusMessage, VideoRecorder, 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, changeLowBandwidth, changeStatusMainVideoOnScene, changeStreamResolutionAction, changeStreamResolutionSuccessAction, commandCompleted, commandStarted, dataChannelConnected, dataChannelReady, decodeData, destroyRemoteConnections, destroyUnrealScene, disconnectStream, dispatchResize, dropConnection, floatToSmoothPercents, forceResizeUnrealVideo, fromResizeObserver, fromSignal, fromUnrealCallBackSignal, getActiveUrl, getImageFromVideoStream, getRtcErrorMessage, iceConnectionFailed, initSignalling, initialState, isLoaderScreenVisible, keepMaxUntilReset, mapQpToQuality, observeCommandResponse, provideAngularUnrealModule, reconnectPeer, reconnectPeerFailed, reconnectPeerSuccess, removeExileCommands, resetAfk, resetAfkAction, resetConfig, resetDataChannelForReconnect, resetIntroSrc, resetWarnTimeout, saveAnalyticsEvent, selectClientAndViewIds, selectCommandProgress, selectCommandsInProgress, selectFreezeFrameCombinedDataUrl, selectFreezeFrameDataUrl, selectFreezeFrameDataUrlFromVideo, selectFreezeFrameProgressMessageFromVideo, selectIsAutostart, selectIsExistMatchUrls, selectIsFreezeFrameLoading, selectIsVideoPlayingAndDataChannelConnected, selectLastCommandInProgress, selectLoaderCommands, selectShowLoader, selectShowReconnectPopup, selectSignalingParameters, selectStreamConfig, selectTotalProgress, selectWarnTimeout, sendSignal, setAfkTimerHide, setAfkTimerVisible, setAwsInstance, setCirrusConnected, setCirrusDisconnected, setConfig, setDataChannelConnected, setEstablishingConnection, setFreezeFrame, setFreezeFrameFromVideo, setIntroImageSrc, setIntroVideoSrc, setLoadingImageSrc, setLoopBackCommandIsCompleted, setMaxFps, setOrchestrationContext, setOrchestrationMessage, setOrchestrationParameters, setOrchestrationProgress, setSignalingName, setStatusMessage, setStatusPercentSignallingServer, setStreamClientCompanyId, setStreamViewId, setUnrealPlaywrightConfig, setViewportNotReady, setViewportReady, showPopupWithoutAutoStart, showUnrealErrorMessage, smoothTransition, startStream, trackMixpanelEvent, unrealFeature, unrealReducer, updateCirrusInfo };
5593
5923
  //# sourceMappingURL=3dsource-angular-unreal-module.mjs.map