@3dsource/angular-unreal-module 0.0.101-dev.4 → 0.0.101-dev.6

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.
@@ -979,7 +979,7 @@ const selectSignalingParameters = createSelector(unrealFeature.selectSsData, unr
979
979
  * Returns Scene load progress as a float value in between 0-1
980
980
  */
981
981
  const selectTotalProgress = createSelector(unrealFeature.selectStatusPercentSignallingServer, selectCommandProgress, unrealFeature.selectAwsInstance, unrealFeature.selectDataChannelConnected, (statusPercentSignallingServer, commandProgress, awsInstance, dataChannelConnected) => {
982
- //TODO make better mechanism to detect is new orchestration
982
+ // TODO make better mechanism to detect is new orchestration
983
983
  const isNewOrchestration = String(awsInstance?.wsUrl).match(/^wss:\/\/orchestration-websocket/i);
984
984
  const newOrchestrationProgressComplete = dataChannelConnected
985
985
  ? 100
@@ -1378,7 +1378,7 @@ class CommandsEffects {
1378
1378
  this.listenUnrealCallbackByInitSignalling$ = createEffect(() => {
1379
1379
  return this.actions$.pipe(ofType(initSignalling), filter(() => !this.unrealInitialConfig?.playwright), switchMap(() => this.actions$.pipe(ofType(unrealCommandCallback), map(({ json }) => 'commandCallback' in json
1380
1380
  ? json.commandCallback?.correlationId
1381
- : json?.correlationId), filter(Truthy), tapLog('Unreal Command Callback'), map((id) => commandCompleted({ id })))));
1381
+ : json?.correlationId), filter(Truthy), map((id) => commandCompleted({ id })))));
1382
1382
  });
1383
1383
  }
1384
1384
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: CommandsEffects, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
@@ -2113,8 +2113,13 @@ function createWebSocket(wsUrl) {
2113
2113
  Logger.info('Creating socket', wsUrl);
2114
2114
  return defer(() => new Observable((observer) => {
2115
2115
  const ws = new WebSocket(wsUrl);
2116
+ let opened = false;
2117
+ let emitted = false;
2116
2118
  const onOpen = () => {
2119
+ opened = true;
2120
+ emitted = true;
2117
2121
  observer.next(ws); // connection established
2122
+ observer.complete(); // single-value observable — done
2118
2123
  };
2119
2124
  const onError = (ev) => {
2120
2125
  const err = ev && 'error' in ev && ev?.error instanceof Error
@@ -2123,7 +2128,7 @@ function createWebSocket(wsUrl) {
2123
2128
  observer.error(err);
2124
2129
  };
2125
2130
  const onClose = (ev) => {
2126
- if (ws.readyState !== WebSocket.OPEN) {
2131
+ if (!opened) {
2127
2132
  observer.error(new Error(`WebSocket closed before open (code ${ev.code})`));
2128
2133
  }
2129
2134
  };
@@ -2134,6 +2139,14 @@ function createWebSocket(wsUrl) {
2134
2139
  ws.removeEventListener('open', onOpen);
2135
2140
  ws.removeEventListener('error', onError);
2136
2141
  ws.removeEventListener('close', onClose);
2142
+ // Only close the socket if it was never delivered to the consumer.
2143
+ // Once emitted, the consumer owns the socket (e.g. connectToCirrus
2144
+ // attaches its own handlers). Closing here would break downstream.
2145
+ if (!emitted &&
2146
+ (ws.readyState === WebSocket.CONNECTING ||
2147
+ ws.readyState === WebSocket.OPEN)) {
2148
+ ws.close();
2149
+ }
2137
2150
  };
2138
2151
  })).pipe(timeout$1({
2139
2152
  first: WS_TIMEOUT, // fail if no 'open' event in this window
@@ -2231,7 +2244,7 @@ class SignallingService extends SubService {
2231
2244
  this.correlationId = generateUuid();
2232
2245
  this.commandTelemetryService.reset();
2233
2246
  this.startEstablishingConnection();
2234
- return defer(() => this.getRegion().pipe(tap((region) => this.region.set(region ?? '')), map$1((region) => this.adaptUrlsToRegion(urlList, region)), switchMap((urls) => this.getAwsInstance(urls).pipe(take(1))), tap((awsInstance) => this.store.dispatch(setAwsInstance(awsInstance))), switchMap(({ wsUrl }) => this.connectToCirrus(wsUrl ?? '')), filter(Truthy), take(1), takeUntil(this.abort$))).pipe(
2247
+ return defer(() => this.getRegion().pipe(tap((region) => this.region.set(region ?? '')), map$1((region) => this.adaptUrlsToRegion(urlList, region)), switchMap((urls) => this.getAwsInstance(urls).pipe(take(1))), tap((awsInstance) => this.store.dispatch(setAwsInstance(awsInstance))), switchMap(({ wsUrl }) => this.connectToCirrus(wsUrl ?? '').pipe(filter(Truthy), take(1))), takeUntil(this.abort$))).pipe(
2235
2248
  // Retry the whole attempt on error, advancing to the next URL each time.
2236
2249
  retry({
2237
2250
  delay: (err, attempt) => {
@@ -4057,2091 +4070,2102 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImpor
4057
4070
  type: Injectable
4058
4071
  }], ctorParameters: () => [] });
4059
4072
 
4060
- class AfkPlaywrightService extends AFKService {
4061
- init() {
4062
- return;
4063
- }
4064
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: AfkPlaywrightService, deps: null, target: i0.ɵɵFactoryTarget.Injectable }); }
4065
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: AfkPlaywrightService }); }
4066
- }
4067
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: AfkPlaywrightService, decorators: [{
4068
- type: Injectable
4069
- }] });
4070
-
4071
- class SignallingPlaywrightService extends SignallingService {
4073
+ /*
4074
+ * Copyright (c) 2026.
4075
+ * 3DSource.com. Sergii Karanda steve@3dsource.com. All Rights Reserved.
4076
+ */
4077
+ class WebRtcPlayerService2 extends SubService {
4072
4078
  constructor() {
4073
4079
  super();
4080
+ this.onDataChannelMessage$ = new Subject();
4081
+ this.signallingSrv = inject(SignallingService);
4082
+ this.videoService = inject(VideoService);
4083
+ this.commandTelemetryService = inject(CommandTelemetryService);
4084
+ this.cfg = {};
4085
+ this.useMic = false;
4086
+ this.forceTURN = false;
4087
+ this.forceMonoAudio = false;
4088
+ /** Whether the remote SDP has been applied to the current peer connection */
4089
+ this.remoteDescriptionSet = false;
4090
+ /** ICE candidates received before the remote description was set */
4091
+ this.iceCandidateBuffer = [];
4092
+ this.isReconnecting = this.store.selectSignal(unrealFeature.selectIsReconnecting);
4093
+ this.init();
4094
+ this.actions$.pipe(ofType(dropDC)).subscribe(() => this.dcClient?.close());
4095
+ this.actions$.pipe(ofType(dropPC)).subscribe(() => this.pcClient?.close());
4074
4096
  }
4075
4097
  init() {
4076
- return;
4077
- }
4078
- connectToSignaling() {
4079
- this.store.dispatch(setUnrealPlaywrightConfig());
4080
- this.store.dispatch(dataChannelConnected({
4081
- statusMessage: 'Playwright set data channel connected',
4082
- }));
4083
- return EMPTY;
4084
- }
4085
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: SignallingPlaywrightService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
4086
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: SignallingPlaywrightService }); }
4087
- }
4088
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: SignallingPlaywrightService, decorators: [{
4089
- type: Injectable
4090
- }], ctorParameters: () => [] });
4091
-
4092
- class UnrealCommunicatorPlaywrightService extends UnrealCommunicatorService {
4093
- init() {
4094
- return;
4095
- }
4096
- sendCommandToUnreal() {
4097
- return;
4098
+ this.signallingSrv.onConfig$.subscribe((config) => {
4099
+ this.videoService.create();
4100
+ this.setConfig(config);
4101
+ });
4102
+ // Server-initiated offer: set remote description and respond with answer
4103
+ this.signallingSrv.onOffer$.pipe(filter(Truthy)).subscribe((offer) => {
4104
+ void this.handleServerOffer(offer);
4105
+ });
4106
+ // Answer to a client-initiated offer
4107
+ this.signallingSrv.onWebRtcAnswer$
4108
+ .pipe(filter(Truthy))
4109
+ .subscribe((answer) => {
4110
+ void this.handleServerAnswer(answer);
4111
+ });
4112
+ // ICE candidates — buffered until remote description is set
4113
+ this.signallingSrv.onWebRtcIce$.subscribe((candidate) => {
4114
+ this.commandTelemetryService.trackStopCommand('EXT-iceCandidate', true, {
4115
+ multi: true,
4116
+ });
4117
+ if (this.remoteDescriptionSet) {
4118
+ this.handleCandidateFromServer(candidate);
4119
+ }
4120
+ else {
4121
+ this.iceCandidateBuffer.push(candidate);
4122
+ }
4123
+ });
4098
4124
  }
4099
- sendInputData() {
4100
- return;
4125
+ /**
4126
+ * Handles an SDP offer received from the server.
4127
+ * Creates a new peer connection, applies the remote offer,
4128
+ * then responds with an SDP answer.
4129
+ *
4130
+ * The DataChannel is NOT created here — the server creates it
4131
+ * and we receive it via the `datachannel` event on the peer connection.
4132
+ */
4133
+ async handleServerOffer(offer) {
4134
+ Logger.info('Received offer from server, creating answer');
4135
+ try {
4136
+ this.remoteDescriptionSet = false;
4137
+ this.iceCandidateBuffer.length = 0;
4138
+ if (!this.pcClient) {
4139
+ this.setupPeerConnection(this.cfg);
4140
+ }
4141
+ // Offer already defines media transceivers — don't add extras
4142
+ await this.pcClient.setRemoteDescription(offer);
4143
+ this.remoteDescriptionSet = true;
4144
+ this.flushIceCandidateBuffer();
4145
+ await this.createAnswer(this.pcClient);
4146
+ }
4147
+ catch (e) {
4148
+ Logger.error('Failed to handle server offer', e);
4149
+ this.store.dispatch(disconnectStream({
4150
+ reason: DisconnectReason.WebRTCError,
4151
+ message: 'WebRTCError: Failed to handle server offer',
4152
+ }));
4153
+ }
4101
4154
  }
4102
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: UnrealCommunicatorPlaywrightService, deps: null, target: i0.ɵɵFactoryTarget.Injectable }); }
4103
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: UnrealCommunicatorPlaywrightService }); }
4104
- }
4105
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: UnrealCommunicatorPlaywrightService, decorators: [{
4106
- type: Injectable
4107
- }] });
4108
-
4109
- class AggregatorPlaywrightService extends AggregatorService {
4110
- init() {
4111
- return;
4155
+ /**
4156
+ * Handles an SDP answer received from the server in response to our offer.
4157
+ * Applies the remote description and flushes any buffered ICE candidates.
4158
+ */
4159
+ async handleServerAnswer(answer) {
4160
+ Logger.info('Received answer from server');
4161
+ try {
4162
+ await this.pcClient?.setRemoteDescription(answer);
4163
+ this.remoteDescriptionSet = true;
4164
+ this.flushIceCandidateBuffer();
4165
+ }
4166
+ catch (e) {
4167
+ Logger.error('Failed to set remote description from answer', e);
4168
+ }
4112
4169
  }
4113
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: AggregatorPlaywrightService, deps: null, target: i0.ɵɵFactoryTarget.Injectable }); }
4114
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: AggregatorPlaywrightService }); }
4115
- }
4116
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: AggregatorPlaywrightService, decorators: [{
4117
- type: Injectable
4118
- }] });
4119
-
4120
- class FreezeFramePlaywrightService extends FreezeFrameService {
4121
- init() {
4122
- return;
4170
+ /** Drains buffered ICE candidates after the remote description has been set */
4171
+ flushIceCandidateBuffer() {
4172
+ for (const candidate of this.iceCandidateBuffer) {
4173
+ this.handleCandidateFromServer(candidate);
4174
+ }
4175
+ this.iceCandidateBuffer.length = 0;
4123
4176
  }
4124
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: FreezeFramePlaywrightService, deps: null, target: i0.ɵɵFactoryTarget.Injectable }); }
4125
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: FreezeFramePlaywrightService }); }
4126
- }
4127
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: FreezeFramePlaywrightService, decorators: [{
4128
- type: Injectable
4129
- }] });
4130
-
4131
- class CommandTelemetryPlaywrightService extends CommandTelemetryService {
4132
- init() {
4133
- return;
4177
+ /**
4178
+ * Creates a client-initiated WebRTC offer and sends it to the server.
4179
+ * When the client offers, it also creates the DataChannel.
4180
+ * Used for peer reconnection through the existing signaling connection.
4181
+ */
4182
+ async createWebRtcOffer() {
4183
+ Logger.info('Creating client-initiated offer');
4184
+ try {
4185
+ this.remoteDescriptionSet = false;
4186
+ this.iceCandidateBuffer.length = 0;
4187
+ if (!this.pcClient) {
4188
+ this.setupPeerConnection(this.cfg);
4189
+ }
4190
+ // Client offers: transceivers first, then DC, then offer
4191
+ await this.setupTransceiversAsync(this.pcClient);
4192
+ this.dcClient = this.createDataChannel(this.pcClient, 'UnrealDcConnection', { ordered: true });
4193
+ await this.createOffer(this.pcClient);
4194
+ }
4195
+ catch (e) {
4196
+ Logger.error('Failed to create WebRTC offer', e);
4197
+ this.store.dispatch(disconnectStream({
4198
+ reason: DisconnectReason.WebRTCError,
4199
+ message: 'WebRTCError: Offer Error',
4200
+ }));
4201
+ }
4134
4202
  }
4135
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: CommandTelemetryPlaywrightService, deps: null, target: i0.ɵɵFactoryTarget.Injectable }); }
4136
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: CommandTelemetryPlaywrightService }); }
4137
- }
4138
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: CommandTelemetryPlaywrightService, decorators: [{
4139
- type: Injectable
4140
- }] });
4141
-
4142
- class StreamStatusTelemetryPlaywrightService extends StreamStatusTelemetryService {
4143
- init() {
4144
- return;
4203
+ /**
4204
+ * Initializes a new RTCPeerConnection with event handlers.
4205
+ * Transceivers are NOT added here — the caller is responsible for adding
4206
+ * them at the correct point depending on the negotiation flow.
4207
+ */
4208
+ setupPeerConnection(config) {
4209
+ this.closePC();
4210
+ Logger.log('Creating new RTCPeerConnection with config');
4211
+ this.pcClient = new RTCPeerConnection(config);
4212
+ this.commandTelemetryService.trackStartCommand('EXT-Browser ICE gathering');
4213
+ this.commandTelemetryService.trackStartCommand('EXT-Browser ICE connection');
4214
+ this.commandTelemetryService.trackStartCommand('EXT-Signaling state change');
4215
+ this.handlePeerConnectionEvents(this.pcClient);
4216
+ this.disconnect$.pipe(take(1)).subscribe(() => {
4217
+ this.closePC();
4218
+ });
4145
4219
  }
4146
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: StreamStatusTelemetryPlaywrightService, deps: null, target: i0.ɵɵFactoryTarget.Injectable }); }
4147
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: StreamStatusTelemetryPlaywrightService }); }
4148
- }
4149
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: StreamStatusTelemetryPlaywrightService, decorators: [{
4150
- type: Injectable
4151
- }] });
4152
-
4153
- class ConsoleExtensionsPlaywrightService extends ConsoleExtensionsService {
4154
- init() {
4155
- return;
4220
+ createDataChannel(pc, label, options) {
4221
+ const datachannel = pc.createDataChannel(label, options);
4222
+ return this.setupDataChannelCallbacks(datachannel);
4156
4223
  }
4157
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ConsoleExtensionsPlaywrightService, deps: null, target: i0.ɵɵFactoryTarget.Injectable }); }
4158
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ConsoleExtensionsPlaywrightService }); }
4159
- }
4160
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ConsoleExtensionsPlaywrightService, decorators: [{
4161
- type: Injectable
4162
- }] });
4163
-
4164
- class FileReceiverPlaywrightService extends FileReceiverService {
4165
- init() {
4166
- return;
4167
- }
4168
- }
4169
-
4170
- class InputPlaywrightService extends InputService {
4171
- init() {
4172
- return;
4173
- }
4174
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: InputPlaywrightService, deps: null, target: i0.ɵɵFactoryTarget.Injectable }); }
4175
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: InputPlaywrightService }); }
4176
- }
4177
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: InputPlaywrightService, decorators: [{
4178
- type: Injectable
4179
- }] });
4180
-
4181
- const ReceivedMimeTypes = {
4182
- ApplicationJson: 'application/json',
4183
- };
4184
-
4185
- class FileHandlerService {
4186
- constructor() {
4187
- this.fileService = inject(FileReceiverService);
4188
- this.fileHandlers = {
4189
- [ReceivedMimeTypes.ApplicationJson]: this.handleJsonFile.bind(this),
4190
- };
4191
- }
4192
- handleJsonFile(data, correlationId) {
4193
- const { blob } = data;
4194
- return from(blob.text()).pipe(map$1((text) => JSON.parse(text)), filter(({ commandCallback }) => {
4195
- return commandCallback.correlationId === correlationId;
4196
- }));
4197
- }
4198
- observeFileResponse(mimetype, data, sender, timeOut = 60000) {
4199
- const correlationId = generateUuid();
4200
- const out = { ...data, correlationId };
4201
- const observable = this.fileService.fileComplete$.pipe(switchMap((data) => {
4202
- if (data.valid && this.fileHandlers[mimetype]) {
4203
- return this.fileHandlers[mimetype](data, correlationId).pipe(filter(Truthy));
4204
- }
4205
- return of(null);
4206
- }), timeout(timeOut), catchError((e) => {
4207
- Logger.error(e);
4208
- return of(null);
4209
- }), take(1));
4210
- setTimeout(() => sender(out), 0);
4211
- return observable;
4212
- }
4213
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: FileHandlerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
4214
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: FileHandlerService }); }
4215
- }
4216
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: FileHandlerService, decorators: [{
4217
- type: Injectable
4218
- }] });
4219
-
4220
- class AnalyticsService {
4221
- #store = inject(Store);
4222
- #destroy$ = new Subject();
4223
- sendEventToMixPanel(name, params = {}) {
4224
- Logger.log('Analytics Event:', name, params);
4225
- this.#store.dispatch(saveAnalyticsEvent({ event: { name, params } }));
4226
- }
4227
- constructor() {
4228
- const abortSignal$ = this.#store
4229
- .select(unrealFeature.selectViewportReady)
4230
- .pipe(filter$1(Falsy), map(() => void 0));
4231
- this.#store
4232
- .select(unrealFeature.selectViewportReady)
4233
- .pipe(distinctUntilChanged(), filter$1(Truthy), tap(() => this.monitorMouse(abortSignal$)))
4234
- .subscribe();
4235
- }
4236
- monitorMouse(abortSignal$) {
4237
- this.#destroy$.next();
4238
- const stop$ = merge(this.#destroy$.asObservable(), abortSignal$);
4239
- const element = document.getElementById('streamingVideo');
4240
- if (!element) {
4241
- return;
4242
- }
4243
- // Monitor mousewheel events
4244
- const mousewheel$ = fromEvent(element, 'wheel').pipe(takeUntil$1(stop$), throttleTime(1000));
4245
- // Monitor mouse button states
4246
- const mousedown$ = fromEvent(element, 'mousedown').pipe(takeUntil$1(stop$));
4247
- const touchStart$ = fromEvent(element, 'touchstart').pipe(takeUntil$1(stop$));
4248
- const touchMove$ = fromEvent(element, 'touchmove').pipe(takeUntil$1(stop$), throttleTime(200));
4249
- // Track mouse movement while the left or right button is pressed
4250
- mousedown$
4251
- .pipe(filter$1((event) => event.button === 0 || event.button === 2), tap((downEvent) => {
4252
- this.sendEventToMixPanel('mouse_interaction', {
4253
- viewX: downEvent.clientX / element.clientWidth,
4254
- viewY: downEvent.clientY / element.clientHeight,
4255
- action: downEvent.button === 0 ? 'spinning' : 'panning',
4256
- });
4257
- }))
4258
- .subscribe();
4259
- // Track mouse movement while the left or right button is pressed
4260
- touchStart$
4261
- .pipe(tap((downEvent) => {
4262
- const totalTouches = downEvent.touches.length;
4263
- const touch = downEvent.touches[0];
4264
- this.sendEventToMixPanel('touch_interaction', {
4265
- viewX: touch.clientX / element.clientWidth,
4266
- viewY: touch.clientY / element.clientHeight,
4267
- action: totalTouches === 1 ? 'spinning' : 'panning',
4268
- });
4269
- }))
4270
- .subscribe();
4271
- let initialTouchDistance = 0;
4272
- // Subscribe to mousewheel events
4273
- touchMove$.subscribe((event) => {
4274
- const totalTouches = event.touches.length;
4275
- if (totalTouches < 2) {
4276
- return;
4277
- }
4278
- // Calculate the current distance between two touches
4279
- const touch1 = event.touches[0];
4280
- const touch2 = event.touches[1];
4281
- const currentDistance = Math.sqrt(Math.pow(touch2.clientX - touch1.clientX, 2) +
4282
- Math.pow(touch2.clientY - touch1.clientY, 2));
4283
- // Determine if zooming in or out based on distance change
4284
- const deltaDistance = currentDistance - initialTouchDistance;
4285
- const isZoomingIn = deltaDistance > 0;
4286
- // Only send event if there's a significant change (avoid noise)
4287
- if (Math.abs(deltaDistance) > 10) {
4288
- this.sendEventToMixPanel('touch_zoom_interaction', {
4289
- delta: isZoomingIn ? 1 : -1,
4290
- action: isZoomingIn ? 'zoomin' : 'zoomout',
4291
- });
4292
- // Update initial distance for next comparison
4293
- initialTouchDistance = currentDistance;
4294
- }
4295
- });
4296
- mousewheel$.subscribe((event) => {
4297
- this.sendEventToMixPanel('mouse_zoom_interaction', {
4298
- delta: -(event.deltaY / Math.abs(event.deltaY)),
4299
- action: event.deltaY < 0 ? 'zoomin' : 'zoomout',
4300
- });
4301
- });
4302
- }
4303
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: AnalyticsService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
4304
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: AnalyticsService }); }
4305
- }
4306
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: AnalyticsService, decorators: [{
4307
- type: Injectable
4308
- }], ctorParameters: () => [] });
4309
-
4310
- /**
4311
- * Adaptive FPS controller that monitors the incoming WebRTC video stream
4312
- * quality and dynamically adjusts Unreal Engine's `t.MaxFPS` console variable.
4313
- *
4314
- * ## How it works
4315
- *
4316
- * The service subscribes to `VideoService.videoStats$` (emitted every 250 ms)
4317
- * and collects samples of FPS, bitrate, and QP (Quantisation Parameter).
4318
- * Every {@link SAMPLE_WINDOW} ticks (~2 s) it averages the collected samples
4319
- * and feeds them into a set of **fuzzy-logic membership functions** that
4320
- * classify each metric into overlapping quality bands.
4321
- *
4322
- * ### Input signals
4323
- *
4324
- * **1. FPS ratio** = `actualFPS / currentTargetFPS` (0..1+)
4325
- *
4326
- * | Band | Range | Meaning |
4327
- * |--------|---------------------|-----------------------------------------------|
4328
- * | low | ratio < 0.7 → 1.0, linear ramp 0.7..0.9 → 0 | Stream cannot keep up with the target |
4329
- * | ok | triangle 0.8..1.0, peak at 0.95 | Stream roughly matches the target |
4330
- * | high | ratio 0.9..1.0 → ramp, >1.0 → 1.0 | Stream meets or exceeds the target |
4331
- *
4332
- * **2. Bitrate load** = `currentBitrate / MAX_BITRATE` (0..1+)
4333
- *
4334
- * `MAX_BITRATE` = 20 Mbit/s — the reference ceiling for the encoder.
4335
- *
4336
- * | Band | Range | Meaning |
4337
- * |--------|-------------------------------------------------|-----------------------------------------------|
4338
- * | high | load 0.5..0.8 → ramp, >0.8 → 1.0 | Encoder is using most of the available bandwidth |
4339
- * | not high | load < 0.5 → 0.0 | Bandwidth headroom is available |
4340
- *
4341
- * **3. Quality** — mapped from QP via `mapQpToQuality()` (see {@link Quality}):
4342
- *
4343
- * | Quality | QP range | Meaning |
4344
- * |----------|-----------|-------------------------------------------------|
4345
- * | `lime` | QP <= 26 | Good — encoder has enough headroom |
4346
- * | `orange` | QP 27..35 | Fair — encoder is under moderate pressure |
4347
- * | `red` | QP > 35 | Poor — heavy compression, visible artefacts |
4348
- *
4349
- * ### Decision rules (evaluated in priority order, first match wins)
4350
- *
4351
- * | # | Condition | Decision |
4352
- * |---|------------------------------------------------------------|--------------|
4353
- * | 1 | FPS ratio is **low** (membership > 0.5) | **decrease** |
4354
- * | 2 | FPS ratio is **ok** AND quality is `red` | **decrease** |
4355
- * | 3 | FPS ratio is **ok** AND bitrate **high** AND quality is `orange` | **hold** |
4356
- * | 4 | FPS ratio is **high** AND quality is `lime` AND bitrate **not high** | **increase** |
4357
- * | — | Everything else | **hold** |
4358
- *
4359
- * ## Cooldown-based throttling
4360
- *
4361
- * The service continuously evaluates fuzzy rules every sample window.
4362
- * After each FPS step change it enforces a cooldown before the next
4363
- * change can be applied. The cooldown duration depends on the
4364
- * (previous decision → next decision) pair:
4365
- *
4366
- * | Previous | Next | Cooldown |
4367
- * |-----------|-----------|-----------------------------------------------------|
4368
- * | decrease | decrease | {@link COOLDOWN_DECREASE_AFTER_DECREASE_MS} (2 s) |
4369
- * | decrease | increase | {@link COOLDOWN_INCREASE_AFTER_DECREASE_MS} (10 s) |
4370
- * | increase | decrease | {@link COOLDOWN_AFTER_INCREASE_MS} (2 s) |
4371
- * | increase | increase | {@link COOLDOWN_AFTER_INCREASE_MS} (2 s) |
4372
- *
4373
- * This means the system reacts quickly after an increase (2 s in any
4374
- * direction) but is cautious about stepping back up after a decrease
4375
- * (10 s), while still allowing consecutive decreases at 2 s intervals.
4376
- *
4377
- * ## FPS dispatch
4378
- *
4379
- * FPS changes are dispatched via the NgRx `setMaxFps` action, which triggers
4380
- * the existing `CommandsEffects.setMaxFps$` effect that sends the
4381
- * `t.MaxFPS <value>` console command to Unreal Engine.
4382
- */
4383
- class FpsMonitorService {
4384
- constructor() {
4385
- this.videoService = inject(VideoService);
4386
- this.store = inject(Store);
4387
- /** Discrete FPS targets the controller can switch between (ascending order). */
4388
- this.FPS_STEPS = [30, 40, 50, 60];
4389
- /** Reference ceiling for bitrate (bits/sec) used to normalise bitrate load to 0..1. */
4390
- this.MAX_BITRATE = 20_000_000;
4391
- /**
4392
- * Cooldown (ms) for any decision (increase or decrease) following an increase.
4393
- * After an increase the system should react faster.
4394
- */
4395
- this.COOLDOWN_AFTER_INCREASE_MS = 2_000;
4396
- /**
4397
- * Cooldown (ms) for a decrease following a previous decrease.
4398
- */
4399
- this.COOLDOWN_DECREASE_AFTER_DECREASE_MS = 2_000;
4400
- /**
4401
- * Cooldown (ms) for an increase following a previous decrease.
4402
- * The stream needs more time to stabilise before stepping back up.
4403
- */
4404
- this.COOLDOWN_INCREASE_AFTER_DECREASE_MS = 10_000;
4405
- /**
4406
- * Number of videoStats$ ticks to accumulate before evaluating.
4407
- * At 250 ms per tick this gives a ~2 s sliding evaluation window,
4408
- * long enough to smooth out single-frame jitter.
4409
- */
4410
- this.SAMPLE_WINDOW = 8;
4411
- /** Current FPS target; starts at max (60 FPS) and adapts downward/upward. */
4412
- this.currentFpsTarget = this.FPS_STEPS[this.FPS_STEPS.length - 1];
4413
- this.samples = [];
4414
- this.lastDecisionTime = Date.now();
4415
- this.lastUpgrade = 'hold';
4416
- this.init();
4417
- }
4418
- init() {
4419
- this.videoService.videoStats$
4420
- .pipe(withLatestFrom(this.store.select(unrealFeature.selectViewportReady)), filter(([, viewportReady]) => viewportReady))
4421
- .subscribe(([stats]) => this.processTick(stats));
4422
- }
4423
- /**
4424
- * Called on every videoStats$ emission (~250 ms).
4425
- * Accumulates samples and evaluates once per SAMPLE_WINDOW.
4426
- */
4427
- processTick(stats) {
4428
- const { framesPerSecond, bitrate, VideoEncoderQP } = stats.aggregatedStats;
4429
- this.samples.push({
4430
- fps: framesPerSecond ?? 0,
4431
- bitrate: bitrate ?? 0,
4432
- qp: VideoEncoderQP ?? 0,
4433
- });
4434
- // Wait until we have enough samples for a reliable average.
4435
- if (this.samples.length < this.SAMPLE_WINDOW) {
4436
- return;
4437
- }
4438
- const avg = this.averageSamples();
4439
- this.samples = [];
4440
- this.handleMonitoring(avg, this.currentFpsTarget);
4441
- }
4442
- handleMonitoring(avg, currentTarget) {
4443
- const decision = this.evaluateDecision(avg, currentTarget);
4444
- this.upgradeFps(decision, avg.qp);
4445
- }
4446
- /**
4447
- * Combines fuzzy membership values into a single discrete decision.
4448
- * Rules are evaluated in priority order — first match wins.
4449
- *
4450
- * Inputs:
4451
- * - `fpsRatio` = avgFPS / currentTargetFPS (e.g. 45/60 = 0.75)
4452
- * - `bitrateLoad` = avgBitrate / 20 Mbit/s (e.g. 12M/20M = 0.6)
4453
- * - `quality` = mapQpToQuality(avgQP) → 'lime' | 'orange' | 'red'
4454
- */
4455
- evaluateDecision(avg, currentTarget) {
4456
- const fpsRatio = currentTarget > 0 ? avg.fps / currentTarget : 1;
4457
- const bitrateLoad = avg.bitrate / this.MAX_BITRATE;
4458
- const fpsLow = this.membershipFpsLow(fpsRatio);
4459
- const fpsOk = this.membershipFpsOk(fpsRatio);
4460
- const fpsHigh = this.membershipFpsHigh(fpsRatio);
4461
- const bitrateHigh = this.membershipBitrateHigh(bitrateLoad);
4462
- // Quality is derived from QP: lime (QP <= 26), orange (QP 27..35), red (QP > 35).
4463
- const quality = qpToQuality(avg.qp);
4464
- // Rule 1: FPS clearly below target (ratio roughly < 0.8) → step down immediately.
4465
- if (fpsLow > 0.5) {
4466
- return 'decrease';
4467
- }
4468
- // Rule 2: FPS looks acceptable numerically, but encoder quality is 'red'
4469
- // (QP > 35 — heavy compression artefacts). Reducing target FPS frees
4470
- // encoder headroom and improves visual quality.
4471
- if (fpsOk > 0.4 && quality === 'red') {
4472
- return 'decrease';
4473
- }
4474
- // Rule 3: FPS is ok, but bitrate is above ~50-80% of 20 Mbit/s ceiling
4475
- // and quality is 'orange' (QP 27..35). Near capacity — don't push higher.
4476
- if (fpsOk > 0.4 && bitrateHigh > 0.5 && quality === 'orange') {
4477
- return 'hold';
4478
- }
4479
- // Rule 4: FPS meets/exceeds target (ratio >= ~0.95), quality is 'lime'
4480
- // (QP <= 26), and bitrate is below 50% of ceiling — safe to step up.
4481
- if (fpsHigh > 0.5 &&
4482
- quality === 'lime' &&
4483
- bitrateHigh < 0.5 &&
4484
- this.currentFpsTarget < this.FPS_STEPS[this.FPS_STEPS.length - 1]) {
4485
- return 'increase';
4486
- }
4487
- return 'hold';
4488
- }
4489
- // ---------------------------------------------------------------------------
4490
- // Fuzzy membership functions
4491
- //
4492
- // Each function maps a normalised input (0..1+) to a membership degree (0..1).
4493
- // Overlapping trapezoid/triangle shapes let multiple categories be partially
4494
- // active at the same time, which makes the rules less brittle than hard thresholds.
4495
- // ---------------------------------------------------------------------------
4496
- /**
4497
- * FPS ratio membership "low".
4498
- * Input: ratio = actualFPS / targetFPS (e.g. 42/60 = 0.70).
4499
- *
4500
- * ratio < 0.7 → 1.0 (clearly failing, e.g. 40/60)
4501
- * ratio 0.7..0.9 → linear ramp down (e.g. 0.8 → 0.5)
4502
- * ratio > 0.9 → 0.0 (keeping up)
4503
- */
4504
- membershipFpsLow(ratio) {
4505
- if (ratio < 0.7)
4506
- return 1.0;
4507
- if (ratio > 0.9)
4508
- return 0.0;
4509
- return (0.9 - ratio) / 0.2;
4224
+ handleCandidateFromServer(iceCandidate) {
4225
+ try {
4226
+ const candidate = new RTCIceCandidate(iceCandidate);
4227
+ Logger.info('[Unreal ICE candidate]', '| Type=', candidate.type, '| Protocol=', candidate.protocol, '| Address=', candidate.address, '| Port=', candidate.port, '|');
4228
+ // If forcing TURN, reject any candidates not relay
4229
+ if (this.forceTURN) {
4230
+ if (candidate.candidate.indexOf('relay') < 0) {
4231
+ Logger.warn('Dropping candidate because it was not TURN relay.', '| Type=', candidate.type, '| Protocol=', candidate.protocol, '| Address=', candidate.address, '| Port=', candidate.port, '|');
4232
+ return;
4233
+ }
4234
+ }
4235
+ this.pcClient?.addIceCandidate(candidate).catch((e) => {
4236
+ Logger.warn('Failed to add ICE candidate', e);
4237
+ });
4238
+ }
4239
+ catch (e) {
4240
+ Logger.error('Error handleCandidateFromServer', e);
4241
+ }
4510
4242
  }
4511
4243
  /**
4512
- * FPS ratio membership "ok" — triangle shape.
4513
- * Input: ratio = actualFPS / targetFPS.
4514
- *
4515
- * ratio < 0.8 → 0.0
4516
- * ratio 0.8..0.95 → ramp up, peak at 0.95 (e.g. 57/60)
4517
- * ratio 0.95..1.0 → ramp down
4518
- * ratio > 1.0 → 0.0
4519
- */
4520
- membershipFpsOk(ratio) {
4521
- if (ratio < 0.8)
4522
- return 0.0;
4523
- if (ratio < 0.95)
4524
- return (ratio - 0.8) / 0.15;
4525
- if (ratio <= 1.0)
4526
- return (1.0 - ratio) / 0.05;
4527
- return 0.0;
4528
- }
4529
- /**
4530
- * FPS ratio membership "high".
4531
- * Input: ratio = actualFPS / targetFPS.
4532
- *
4533
- * ratio < 0.9 → 0.0
4534
- * ratio 0.9..1.0 → linear ramp up (e.g. 0.95 → 0.5)
4535
- * ratio >= 1.0 → 1.0 (meeting or exceeding target, e.g. 60/60)
4536
- */
4537
- membershipFpsHigh(ratio) {
4538
- if (ratio < 0.9)
4539
- return 0.0;
4540
- if (ratio > 1.0)
4541
- return 1.0;
4542
- return (ratio - 0.9) / 0.1;
4543
- }
4544
- /**
4545
- * Bitrate load membership "high".
4546
- * Input: load = currentBitrate / MAX_BITRATE (20 Mbit/s).
4547
- *
4548
- * load < 0.5 → 0.0 (below 10 Mbit/s — plenty of headroom)
4549
- * load 0.5..0.8 → linear ramp (e.g. 13M/20M = 0.65 → 0.5)
4550
- * load > 0.8 → 1.0 (above 16 Mbit/s — near capacity)
4551
- */
4552
- membershipBitrateHigh(load) {
4553
- if (load < 0.5)
4554
- return 0.0;
4555
- if (load > 0.8)
4556
- return 1.0;
4557
- return (load - 0.5) / 0.3;
4558
- }
4559
- // ---------------------------------------------------------------------------
4560
- // Step actions
4561
- // ---------------------------------------------------------------------------
4562
- upgradeFps(decision, qp) {
4563
- if (decision === 'hold')
4564
- return;
4565
- const now = Date.now();
4566
- // Throttle: cooldown depends on (previous decision → current decision) pair.
4567
- const isStepUp = decision === 'increase';
4568
- const cooldown = this.lastUpgrade === 'decrease'
4569
- ? isStepUp
4570
- ? this.COOLDOWN_INCREASE_AFTER_DECREASE_MS
4571
- : this.COOLDOWN_DECREASE_AFTER_DECREASE_MS
4572
- : this.COOLDOWN_AFTER_INCREASE_MS;
4573
- if (now - this.lastDecisionTime < cooldown) {
4574
- return;
4575
- }
4576
- this.lastUpgrade = decision;
4577
- const quality = qpToQuality(qp);
4578
- const newTarget = this.getNextFpsTarget(this.currentFpsTarget, decision);
4579
- Logger.info(`[FpsMonitor] Decision: ${decision} → FPS: ${newTarget}, Quality: ${quality}`);
4580
- if (newTarget !== this.currentFpsTarget) {
4581
- this.currentFpsTarget = newTarget;
4582
- this.store.dispatch(setMaxFps({ maxFps: newTarget }));
4583
- }
4584
- this.lastDecisionTime = now;
4585
- this.samples = [];
4586
- }
4587
- /**
4588
- * Get the next FPS target based on current target and decision.
4589
- * Clamps to valid FPS_STEPS range.
4590
- */
4591
- getNextFpsTarget(currentTarget, decision) {
4592
- const currentIndex = this.FPS_STEPS.indexOf(currentTarget);
4593
- // If current target is not in FPS_STEPS, find closest one
4594
- const validIndex = currentIndex === -1
4595
- ? this.findClosestFpsIndex(currentTarget)
4596
- : currentIndex;
4597
- const delta = decision === 'increase' ? 1 : -1;
4598
- const newIndex = clampf(0, this.FPS_STEPS.length - 1, validIndex + delta);
4599
- return this.FPS_STEPS[newIndex];
4600
- }
4601
- /**
4602
- * Find the closest FPS step index for a given target value.
4603
- */
4604
- findClosestFpsIndex(target) {
4605
- let closestIndex = 0;
4606
- let closestDiff = Math.abs(this.FPS_STEPS[0] - target);
4607
- for (let i = 1; i < this.FPS_STEPS.length; i++) {
4608
- const diff = Math.abs(this.FPS_STEPS[i] - target);
4609
- if (diff < closestDiff) {
4610
- closestDiff = diff;
4611
- closestIndex = i;
4612
- }
4613
- }
4614
- return closestIndex;
4615
- }
4616
- /** Average all collected samples for the current evaluation window. */
4617
- averageSamples() {
4618
- const len = this.samples.length;
4619
- if (len === 0)
4620
- return { fps: 0, bitrate: 0, qp: 0 };
4621
- let fps = 0;
4622
- let bitrate = 0;
4623
- let qp = 0;
4624
- for (const s of this.samples) {
4625
- fps += s.fps;
4626
- bitrate += s.bitrate;
4627
- qp += s.qp;
4628
- }
4629
- return { fps: fps / len, bitrate: bitrate / len, qp: qp / len };
4630
- }
4631
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: FpsMonitorService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
4632
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: FpsMonitorService }); }
4633
- }
4634
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: FpsMonitorService, decorators: [{
4635
- type: Injectable
4636
- }], ctorParameters: () => [] });
4637
-
4638
- class UnrealCallbackService {
4639
- constructor() {
4640
- this.actions$ = inject(Actions);
4641
- }
4642
- /**
4643
- * Listens for unreal command callbacks matching a specific command or event type.
4244
+ * Reorders the `m=video` line in the SDP so that payload types matching
4245
+ * the given H.264 `profile-level-id` appear first, making the browser
4246
+ * prefer that profile during codec negotiation.
4644
4247
  *
4645
- * Supports both MetaBoxCommand keys (e.g. `MetaBoxCommand.FLoadProductCommand`)
4646
- * and custom Unreal callback events (e.g. `'cameraChanged'`, `'onSceneState'`).
4248
+ * A `profile-level-id` is a 3-byte hex string (`XXYYZZ`) defined in
4249
+ * RFC 6184 that encodes three H.264 parameters:
4250
+ * - **Profile IDC** (`XX`) — the H.264 profile.
4251
+ * - **Profile IOP** (`YY`) — constraint flags that further narrow the profile.
4252
+ * - **Level IDC** (`ZZ`) — the level, which caps resolution/framerate/bitrate.
4647
4253
  *
4648
- * @param command - The command or event key to filter callbacks by
4649
- * @returns Observable that emits matching callback payloads
4254
+ * ### H.264 profiles relevant to Unreal Engine Pixel Streaming
4650
4255
  *
4651
- * @example
4652
- * ```ts
4653
- * // Listen for a MetaBox command callback
4654
- * this.unrealCallbackService
4655
- * .fromUnrealCallback(MetaBoxCommand.FLoadProductCommand)
4656
- * .subscribe(data => console.log(data));
4256
+ * | profile-level-id | Profile | Level | Notes |
4257
+ * |------------------|----------------------------|-------|----------------------------------------------|
4258
+ * | `42001f` | Baseline | 3.1 | No B-frames, low latency; legacy devices |
4259
+ * | `42e01f` | Constrained Baseline | 3.1 | Widest HW decode support; **recommended** |
4260
+ * | `4d001f` | Main | 3.1 | B-frames + CABAC; better compression |
4261
+ * | `4d4028` | Main | 4.0 | Up to 1080p60; common desktop target |
4262
+ * | `640028` | High | 4.0 | Best quality/compression; most GPUs support |
4263
+ * | `64002a` | High | 4.2 | Up to 1080p120 or 4K30 |
4264
+ * | `640033` | High | 5.1 | Up to 4K60; requires modern HW |
4265
+ * | `640034` | High | 5.2 | Up to 4K120; high-end HW only |
4266
+ * | `6e001f` | High 10 | 3.1 | 10-bit color; limited browser support |
4267
+ * | `f4001f` | High 4:4:4 Predictive | 3.1 | Lossless capable; rare in browsers |
4657
4268
  *
4658
- * // Listen for a custom Unreal event
4659
- * this.unrealCallbackService
4660
- * .fromUnrealCallback('cameraChanged')
4661
- * .subscribe(data => console.log(data));
4662
- * ```
4269
+ * UE Pixel Streaming defaults to Constrained Baseline (`42e01f`) or High (`6400xx`)
4270
+ * depending on engine version and `WebRTC.H264Profile` CVar.
4271
+ *
4272
+ * We prioritize `42e01f` because it guarantees hardware decoding on
4273
+ * virtually all client devices (desktop, mobile, embedded), avoiding
4274
+ * fallback to software decoding or negotiation of a higher profile
4275
+ * that the client GPU may not accelerate.
4663
4276
  */
4664
- fromUnrealCallback(command) {
4665
- return this.actions$.pipe(ofType(unrealCommandCallback), map$1(({ json }) => {
4666
- if ('commandCallback' in json) {
4667
- return json.commandCallback;
4277
+ prioritizeH264Profile(sdp, profileLevelId) {
4278
+ const lines = sdp.split('\r\n');
4279
+ const targetPts = [];
4280
+ for (const line of lines) {
4281
+ if (line.startsWith('a=fmtp:') &&
4282
+ line.toLowerCase().includes(`profile-level-id=${profileLevelId}`)) {
4283
+ const pt = line.split(':')[1]?.split(' ')[0];
4284
+ if (pt)
4285
+ targetPts.push(pt);
4668
4286
  }
4669
- return json;
4670
- }), filter((json) => json.command === command), map$1((data) => ({
4671
- command: data.command,
4672
- payload: 'payload' in data ? data.payload : null,
4673
- })));
4287
+ }
4288
+ if (targetPts.length === 0)
4289
+ return sdp;
4290
+ return lines
4291
+ .map((line) => {
4292
+ if (!line.startsWith('m=video'))
4293
+ return line;
4294
+ const parts = line.split(' ');
4295
+ // parts: m=video <port> <proto> <pt1> <pt2> ...
4296
+ const header = parts.slice(0, 3);
4297
+ const pts = parts.slice(3);
4298
+ const prioritized = [
4299
+ ...targetPts.filter((pt) => pts.includes(pt)),
4300
+ ...pts.filter((pt) => !targetPts.includes(pt)),
4301
+ ];
4302
+ return [...header, ...prioritized].join(' ');
4303
+ })
4304
+ .join('\r\n');
4305
+ }
4306
+ mungeSDP(sdp) {
4307
+ let mungedSDP = this.prioritizeH264Profile(sdp, '42e01f');
4308
+ mungedSDP = mungedSDP.replace(/(a=fmtp:\d+ .*level-asymmetry-allowed=.*)\r\n/gm, '$1;x-google-start-bitrate=10000;x-google-max-bitrate=100000\r\n');
4309
+ // Set max bitrate to highest bitrate Opus supports
4310
+ let audioSDP = 'maxaveragebitrate=510000;';
4311
+ if (this.useMic) {
4312
+ // Set the max capture rate to 48khz (so we can send high-quality audio from mic)
4313
+ audioSDP += 'sprop-maxcapturerate=48000;';
4314
+ }
4315
+ // Force mono or stereo based on whether ?forceMono was passed or not
4316
+ audioSDP += this.forceMonoAudio ? 'stereo=0;' : 'stereo=1;';
4317
+ // Enable in-band forward error correction for opus audio
4318
+ audioSDP += 'useinbandfec=1';
4319
+ // We use the line 'useinbandfec=1' (which Opus uses) to set our Opus specific audio parameters.
4320
+ mungedSDP = mungedSDP?.replace('useinbandfec=1', audioSDP);
4321
+ return mungedSDP;
4674
4322
  }
4675
4323
  /**
4676
- * Sends a command and observes the matching response by correlationId.
4677
- * Replaces the old `observeCommandResponse` standalone function.
4678
- *
4679
- * @param data - The command data to send
4680
- * @param sender - Function to send the command to Unreal
4681
- * @param timeOut - Timeout in ms (default 60000)
4682
- * @param dispatchOnTimeout - Whether to emit on timeout (default true)
4683
- *
4684
- * @example
4685
- * ```ts
4686
- * this.unrealCallbackService
4687
- * .observeCommandResponse<FLoopBackCommand['command']>(
4688
- * { command: MetaBoxCommand.FLoopBackCommand },
4689
- * (data) => this.communicator.sendCommandToUnreal(data),
4690
- * )
4691
- * .subscribe();
4692
- * ```
4324
+ * Closes existing PeerConnection and DataChannel, resets negotiation state.
4693
4325
  */
4694
- observeCommandResponse(data, sender, timeOut = 60000, dispatchOnTimeout = true) {
4695
- const correlationId = generateUuid();
4696
- const observable = this.actions$.pipe(ofType(unrealCommandCallback), map$1(({ json }) => 'commandCallback' in json ? json.commandCallback : json), filter((json) => json.correlationId === correlationId), timeout(timeOut), catchError(() => dispatchOnTimeout
4697
- ? of({ command: 'timeout', correlationId: 'timeout' })
4698
- : of(null)), take(1), tapLog('Command Response Observer'), filter(Truthy), map$1((data) => ({
4699
- command: data.command,
4700
- payload: 'payload' in data ? data.payload : null,
4701
- })));
4702
- setTimeout(() => sender({ ...data, correlationId }), 0);
4703
- return observable;
4704
- }
4705
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: UnrealCallbackService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
4706
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: UnrealCallbackService }); }
4707
- }
4708
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: UnrealCallbackService, decorators: [{
4709
- type: Injectable
4710
- }] });
4711
-
4712
- class AfkTimeoutModalComponent {
4713
- constructor() {
4714
- this.#store = inject(Store);
4715
- this.#afkService = inject(AFKService);
4716
- this.isAfkTimerVisible = this.#store.selectSignal(unrealFeature.selectIsAfkTimerVisible);
4717
- this.afkCountdown = this.#store.selectSignal(unrealFeature.selectAfkCountdown);
4718
- }
4719
- #store;
4720
- #afkService;
4721
- resetAfk() {
4722
- this.#afkService.reset();
4723
- }
4724
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: AfkTimeoutModalComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
4725
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: AfkTimeoutModalComponent, isStandalone: true, selector: "app-afk-timeout-modal", ngImport: i0, template: "@if (isAfkTimerVisible()) {\n <div (click)=\"resetAfk()\" class=\"clickableState\" id=\"videoPlayOverlay\">\n <div class=\"resume-box\">\n <div aria-hidden=\"true\" class=\"resume-box__pic\" role=\"presentation\">\n <div [innerHTML]=\"afkCountdown()\" class=\"text-number\"></div>\n </div>\n <div class=\"resume-box__text\">\n <h3 class=\"resume-box__heading\">Session will time out soon</h3>\n <p>\n No activity detected. Press 'Continue' if you wish to keep your\n session active\n </p>\n </div>\n <src-button\n [isFullWidth]=\"true\"\n [data-testid]=\"'continue-session'\"\n weight=\"primary\"\n size=\"lg\"\n >\n Continue\n </src-button>\n </div>\n </div>\n}\n", styles: ["#videoPlayOverlay{position:absolute;z-index:30;top:0;width:100%;height:100%;font-size:1.8em;font-family:var(--src-font-family-body);background-color:#646464b3}.clickableState{display:flex;justify-content:center;align-items:center;cursor:pointer}.textDisplayState{display:flex}.hiddenState{display:none}.resume-box{width:340px;padding:32px 20px 20px;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 .resume-box__pic{width:72px;height:72px;margin:0 auto 22px;border-radius:48px;background:#ecf0f2;padding:12px;display:flex;align-items:center;justify-content:center}.resume-box .resume-box__pic .text-number{color:var(--src-colors-text-default, #1f2937);text-align:center;font-family:var(--src-font-family-body);font-size:30px;font-style:normal;font-weight:400;line-height:24px}.resume-box__text{margin-bottom:18px}.resume-box__text p{text-align:center;font-family:var(--src-font-family-body);font-size:var(--src-font-size-sm, 14px);font-style:normal;font-weight:400;line-height:24px;color:var(--src-text-body-secondary, #6b7280)}.resume-box__text .resume-box__heading{color:var(--src-text-body-main, #1f2937);text-align:center;font-family:var(--src-font-family-body);font-size:18px;font-style:normal;font-weight:500;line-height:26px;margin-bottom:8px}\n"], dependencies: [{ kind: "component", type: SourceButtonComponent, selector: "src-button", inputs: ["type", "appearance", "weight", "size", "context", "customClass", "isFullWidth", "isPressed", "isDisabled", "isLoading", "isInverted", "iconButton", "srcButtonConfig", "formID", "data-testid", "ariaLabel"], outputs: ["onClick", "onSubmit"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
4726
- }
4727
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: AfkTimeoutModalComponent, decorators: [{
4728
- type: Component,
4729
- args: [{ selector: 'app-afk-timeout-modal', changeDetection: ChangeDetectionStrategy.OnPush, imports: [SourceButtonComponent], template: "@if (isAfkTimerVisible()) {\n <div (click)=\"resetAfk()\" class=\"clickableState\" id=\"videoPlayOverlay\">\n <div class=\"resume-box\">\n <div aria-hidden=\"true\" class=\"resume-box__pic\" role=\"presentation\">\n <div [innerHTML]=\"afkCountdown()\" class=\"text-number\"></div>\n </div>\n <div class=\"resume-box__text\">\n <h3 class=\"resume-box__heading\">Session will time out soon</h3>\n <p>\n No activity detected. Press 'Continue' if you wish to keep your\n session active\n </p>\n </div>\n <src-button\n [isFullWidth]=\"true\"\n [data-testid]=\"'continue-session'\"\n weight=\"primary\"\n size=\"lg\"\n >\n Continue\n </src-button>\n </div>\n </div>\n}\n", styles: ["#videoPlayOverlay{position:absolute;z-index:30;top:0;width:100%;height:100%;font-size:1.8em;font-family:var(--src-font-family-body);background-color:#646464b3}.clickableState{display:flex;justify-content:center;align-items:center;cursor:pointer}.textDisplayState{display:flex}.hiddenState{display:none}.resume-box{width:340px;padding:32px 20px 20px;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 .resume-box__pic{width:72px;height:72px;margin:0 auto 22px;border-radius:48px;background:#ecf0f2;padding:12px;display:flex;align-items:center;justify-content:center}.resume-box .resume-box__pic .text-number{color:var(--src-colors-text-default, #1f2937);text-align:center;font-family:var(--src-font-family-body);font-size:30px;font-style:normal;font-weight:400;line-height:24px}.resume-box__text{margin-bottom:18px}.resume-box__text p{text-align:center;font-family:var(--src-font-family-body);font-size:var(--src-font-size-sm, 14px);font-style:normal;font-weight:400;line-height:24px;color:var(--src-text-body-secondary, #6b7280)}.resume-box__text .resume-box__heading{color:var(--src-text-body-main, #1f2937);text-align:center;font-family:var(--src-font-family-body);font-size:18px;font-style:normal;font-weight:500;line-height:26px;margin-bottom:8px}\n"] }]
4730
- }] });
4731
-
4732
- class FreezeFrameComponent {
4733
- constructor() {
4734
- this.#store = inject(Store);
4735
- this.freezeFrameProgressMessageFromVideo = this.#store.selectSignal(selectFreezeFrameProgressMessageFromVideo);
4736
- this.combinedFreeze = this.#store.selectSignal(selectFreezeFrameCombinedDataUrl);
4737
- }
4738
- #store;
4739
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: FreezeFrameComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
4740
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: FreezeFrameComponent, isStandalone: true, selector: "app-freeze-frame", ngImport: i0, template: "@let image = combinedFreeze();\n@let progress = freezeFrameProgressMessageFromVideo();\n\n<div class=\"freeze-images\">\n @if (image) {\n <img [src]=\"image\" class=\"videoStillImage\" alt=\"freezeFrameImage\" />\n }\n\n @if (progress) {\n <div class=\"progress-status\">\n {{ progress }}\n </div>\n }\n</div>\n", styles: [".freeze-images{pointer-events:none;position:absolute;left:0;top:0;width:100%;height:100%;z-index:1}.freeze-images .freezeFrameOverlay{top:0;left:0;width:100%;height:100%;position:absolute;object-fit:cover;object-position:center}.freeze-images .videoStillImage{position:absolute;z-index:1;top:0;left:0;width:100%;height:100%;object-fit:cover}.freeze-images .progress-status{position:absolute;top:50%;left:50%;z-index:1;display:flex;flex-direction:column;justify-content:center;align-items:center;width:auto;height:auto;margin:0;padding:10px;background-color:#ffffffb3;border:1px solid grey;transform:translate(-50%,-50%)}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
4741
- }
4742
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: FreezeFrameComponent, decorators: [{
4743
- type: Component,
4744
- args: [{ selector: 'app-freeze-frame', changeDetection: ChangeDetectionStrategy.OnPush, template: "@let image = combinedFreeze();\n@let progress = freezeFrameProgressMessageFromVideo();\n\n<div class=\"freeze-images\">\n @if (image) {\n <img [src]=\"image\" class=\"videoStillImage\" alt=\"freezeFrameImage\" />\n }\n\n @if (progress) {\n <div class=\"progress-status\">\n {{ progress }}\n </div>\n }\n</div>\n", styles: [".freeze-images{pointer-events:none;position:absolute;left:0;top:0;width:100%;height:100%;z-index:1}.freeze-images .freezeFrameOverlay{top:0;left:0;width:100%;height:100%;position:absolute;object-fit:cover;object-position:center}.freeze-images .videoStillImage{position:absolute;z-index:1;top:0;left:0;width:100%;height:100%;object-fit:cover}.freeze-images .progress-status{position:absolute;top:50%;left:50%;z-index:1;display:flex;flex-direction:column;justify-content:center;align-items:center;width:auto;height:auto;margin:0;padding:10px;background-color:#ffffffb3;border:1px solid grey;transform:translate(-50%,-50%)}\n"] }]
4745
- }] });
4746
-
4747
- class SafePipe {
4748
- constructor() {
4749
- this.sanitizer = inject(DomSanitizer);
4750
- }
4751
- transform(value, type) {
4752
- if (!value) {
4753
- return value;
4326
+ closePC() {
4327
+ if (this.pcClient) {
4328
+ Logger.warn('Closing existing PeerConnection');
4754
4329
  }
4755
- switch (type) {
4756
- case 'html':
4757
- return this.sanitizer.bypassSecurityTrustHtml(value);
4758
- case 'style':
4759
- return this.sanitizer.bypassSecurityTrustStyle(value);
4760
- case 'script':
4761
- return this.sanitizer.bypassSecurityTrustScript(value);
4762
- case 'url':
4763
- return this.sanitizer.bypassSecurityTrustUrl(value);
4764
- case 'resourceUrl':
4765
- return this.sanitizer.bypassSecurityTrustResourceUrl(value);
4766
- default:
4767
- throw new Error(`Invalid safe type specified: ${type}`);
4330
+ this.pcClient?.close();
4331
+ this.pcClient = null;
4332
+ if (this.dcClient) {
4333
+ Logger.warn('Closing existing DataChannel');
4768
4334
  }
4335
+ this.dcClient?.close();
4336
+ this.dcClient = null;
4337
+ this.remoteDescriptionSet = false;
4338
+ this.iceCandidateBuffer.length = 0;
4769
4339
  }
4770
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: SafePipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
4771
- static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.2.0", ngImport: i0, type: SafePipe, isStandalone: true, name: "safe" }); }
4772
- }
4773
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: SafePipe, decorators: [{
4774
- type: Pipe,
4775
- args: [{
4776
- name: 'safe',
4777
- }]
4778
- }] });
4779
-
4780
- class LowBandwidthModalComponent {
4781
- constructor() {
4782
- this.dialogRef = inject(DialogRef);
4783
- }
4784
- close(value) {
4785
- this.dialogRef.close(value || false);
4786
- }
4787
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: LowBandwidthModalComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
4788
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.0", type: LowBandwidthModalComponent, isStandalone: true, selector: "app-low-bandwidth-modal", ngImport: i0, template: "<div [attr.data-testid]=\"'low-bandwidth'\" class=\"src-modal src-modal--lbm\">\n <header class=\"src-modal__header\">\n <h6 [attr.data-testid]=\"'title'\" class=\"src-modal__title\">\n Unstable Connection\n </h6>\n </header>\n <section class=\"src-modal__body\">\n <div\n [innerHtml]=\"\n 'Fluid Interactivity Modes were disabled due to an unstable connection. Showcase Gallery Mode is enabled. To regain full functionality, switch to Interactive Mode.'\n | safe: 'html'\n \"\n class=\"src-modal__scroll-box\"\n ></div>\n </section>\n <footer class=\"src-modal__footer\">\n <div class=\"src-modal__buttons\">\n <src-button\n (onClick)=\"close(true)\"\n [data-testid]=\"'switch-to-interactive-mode'\"\n >\n Switch\n </src-button>\n\n <src-button\n (onClick)=\"close()\"\n [data-testid]=\"'close-lbm-modal'\"\n weight=\"primary\"\n >\n Ok\n </src-button>\n </div>\n </footer>\n</div>\n", styles: [".src-modal--lbm{width:360px}.src-modal--lbm .src-modal__body{font-size:14px;line-height:24px}.src-modal--lbm .src-modal__buttons{display:flex;gap:8px}\n"], dependencies: [{ kind: "component", type: SourceButtonComponent, selector: "src-button", inputs: ["type", "appearance", "weight", "size", "context", "customClass", "isFullWidth", "isPressed", "isDisabled", "isLoading", "isInverted", "iconButton", "srcButtonConfig", "formID", "data-testid", "ariaLabel"], outputs: ["onClick", "onSubmit"] }, { kind: "pipe", type: SafePipe, name: "safe" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
4789
- }
4790
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: LowBandwidthModalComponent, decorators: [{
4791
- type: Component,
4792
- args: [{ selector: 'app-low-bandwidth-modal', changeDetection: ChangeDetectionStrategy.OnPush, imports: [SafePipe, SourceButtonComponent], template: "<div [attr.data-testid]=\"'low-bandwidth'\" class=\"src-modal src-modal--lbm\">\n <header class=\"src-modal__header\">\n <h6 [attr.data-testid]=\"'title'\" class=\"src-modal__title\">\n Unstable Connection\n </h6>\n </header>\n <section class=\"src-modal__body\">\n <div\n [innerHtml]=\"\n 'Fluid Interactivity Modes were disabled due to an unstable connection. Showcase Gallery Mode is enabled. To regain full functionality, switch to Interactive Mode.'\n | safe: 'html'\n \"\n class=\"src-modal__scroll-box\"\n ></div>\n </section>\n <footer class=\"src-modal__footer\">\n <div class=\"src-modal__buttons\">\n <src-button\n (onClick)=\"close(true)\"\n [data-testid]=\"'switch-to-interactive-mode'\"\n >\n Switch\n </src-button>\n\n <src-button\n (onClick)=\"close()\"\n [data-testid]=\"'close-lbm-modal'\"\n weight=\"primary\"\n >\n Ok\n </src-button>\n </div>\n </footer>\n</div>\n", styles: [".src-modal--lbm{width:360px}.src-modal--lbm .src-modal__body{font-size:14px;line-height:24px}.src-modal--lbm .src-modal__buttons{display:flex;gap:8px}\n"] }]
4793
- }] });
4794
-
4795
- class ImageLoadingSrcComponent {
4796
- constructor() {
4797
- this.#store = inject(Store);
4798
- this.isLoaderScreenVisible = this.#store.selectSignal(selectIsLoaderScreenVisible);
4799
- this.imageLoadingSrc = this.#store.selectSignal(unrealFeature.selectImageLoadingSrc);
4800
- }
4801
- #store;
4802
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ImageLoadingSrcComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
4803
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", 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 [ngSrc]=\"image\" fill loading=\"lazy\" alt=\"image loading src\" />\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 }); }
4804
- }
4805
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ImageLoadingSrcComponent, decorators: [{
4806
- type: Component,
4807
- 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 [ngSrc]=\"image\" fill loading=\"lazy\" alt=\"image loading src\" />\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"] }]
4808
- }] });
4809
-
4810
- class StatGraphComponent {
4811
- constructor() {
4812
- this.label = input.required(...(ngDevMode ? [{ debugName: "label" }] : []));
4813
- this.stat = input(...(ngDevMode ? [undefined, { debugName: "stat" }] : []));
4814
- this.color = input('#417bc9', ...(ngDevMode ? [{ debugName: "color" }] : []));
4815
- this.tickStep = input(3, ...(ngDevMode ? [{ debugName: "tickStep" }] : []));
4816
- this.el = inject(ElementRef);
4817
- this.graph = viewChild('graph', ...(ngDevMode ? [{ debugName: "graph" }] : []));
4818
- this.data = [];
4819
- this.max = signal(0, ...(ngDevMode ? [{ debugName: "max" }] : []));
4820
- this.min = signal(0, ...(ngDevMode ? [{ debugName: "min" }] : []));
4821
- this.current = signal(0, ...(ngDevMode ? [{ debugName: "current" }] : []));
4822
- effect(() => this.pushData(this.stat()?.value ?? null));
4823
- afterNextRender({
4824
- read: () => {
4825
- const graph = this.graph();
4826
- if (graph) {
4827
- this.canvas = graph.nativeElement;
4828
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
4829
- this.ctx = this.canvas.getContext('2d');
4830
- }
4831
- },
4832
- });
4340
+ canSend() {
4341
+ return this.dcClient && this.dcClient.readyState === 'open';
4833
4342
  }
4834
- resize() {
4835
- this.draw();
4343
+ send(data) {
4344
+ if (this.canSend()) {
4345
+ this.dcClient?.send(data);
4346
+ }
4347
+ else {
4348
+ Logger.colored('black', 'yellow', `DataChannel connection not prepared or in state: ${this.dcClient?.readyState}`);
4349
+ // Trigger peer reconnection if DC is not open and we're not already reconnecting
4350
+ this.tryReconnectOnSendFailure();
4351
+ }
4836
4352
  }
4837
- draw() {
4838
- if (!this.canvas) {
4353
+ /**
4354
+ * Attempts to reconnect the peer when send fails due to DataChannel not being open.
4355
+ * This handles cases where the DC connection is broken but no error events are fired.
4356
+ */
4357
+ tryReconnectOnSendFailure() {
4358
+ // Skip if already reconnecting or signaling is not connected
4359
+ if (this.isReconnecting()) {
4360
+ Logger.info('[Reconnect] Already reconnecting, skipping duplicate attempt');
4839
4361
  return;
4840
4362
  }
4841
- const all = this.data.filter((r) => r !== null && !isNaN(r));
4842
- this.max.set(Math.max(...all));
4843
- this.min.set(Math.min(...all));
4844
- const scaleY = this.canvas.height / (this.max() || 1);
4845
- this.current.set(all[all.length - 1] || 0);
4846
- this.ctx.save();
4847
- const bounds = this.el.nativeElement.getBoundingClientRect();
4848
- this.canvas.width = bounds.width;
4849
- this.canvas.height = bounds.height - 30; // -p height
4850
- this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
4851
- [...this.data].reverse().forEach((val, index) => {
4852
- const h = (val || 0) * scaleY;
4853
- this.ctx.fillStyle = this.getColor(this.color(), h);
4854
- this.ctx.fillRect(index * this.tickStep(), this.canvas.height - h, this.tickStep(), h);
4855
- });
4856
- this.ctx.restore();
4363
+ if (!this.signallingSrv.isConnected()) {
4364
+ Logger.warn('[Reconnect] Cannot reconnect: Signaling not connected');
4365
+ return;
4366
+ }
4367
+ // Check if DC exists but is in a non-open state (closing, closed, or connecting stuck)
4368
+ const dcState = this.dcClient?.readyState;
4369
+ if (dcState && dcState !== 'open') {
4370
+ Logger.warn(`[Reconnect] DataChannel in "${dcState}" state during send, initiating peer reconnection`);
4371
+ this.store.dispatch(reconnectPeer({ attempt: 1, maxAttempts: 3, delay: 0 }));
4372
+ }
4857
4373
  }
4858
- getColor(fillStyle, value = 0) {
4859
- const { r, g, b } = HEXtoRGB(fillStyle);
4860
- /* eslint-disable-next-line prefer-const */
4861
- let [h, s, v] = RGBtoHSV(r, g, b);
4862
- v = inverseLerp(0, this.canvas.height, value);
4863
- {
4864
- const [r, g, b] = HSVtoRGB(h, s, v);
4865
- return '#' + RGBtoHEX(r, g, b);
4374
+ /**
4375
+ * Reconnects only the WebRTC peer connection without re-establishing signaling.
4376
+ * Closes the current PeerConnection and creates a new client-initiated offer
4377
+ * through the existing WebSocket connection.
4378
+ * @returns true if reconnection was initiated, false if signaling is not connected
4379
+ */
4380
+ reconnectPeer() {
4381
+ if (!this.signallingSrv.isConnected()) {
4382
+ Logger.error('[Reconnect] Cannot reconnect peer: Signaling not connected');
4383
+ return false;
4384
+ }
4385
+ if (!this.cfg || Object.keys(this.cfg).length === 0) {
4386
+ Logger.error('[Reconnect] Cannot reconnect peer: No config available');
4387
+ return false;
4866
4388
  }
4389
+ Logger.info('[Reconnect] Initiating peer reconnection');
4390
+ // Close only the PeerConnection, keep WebSocket alive
4391
+ this.closePC();
4392
+ // Create a new WebRTC offer using the stored config
4393
+ void this.createWebRtcOffer();
4394
+ return true;
4867
4395
  }
4868
- pushData(stat) {
4869
- this.data.push(stat);
4870
- if (!this.canvas) {
4871
- return;
4396
+ setConfig(parOptions = {}) {
4397
+ const urlParams = new URLSearchParams(window.location.search);
4398
+ //**********************
4399
+ //Config setup
4400
+ //**********************
4401
+ this.cfg = { ...parOptions.peerConnectionOptions };
4402
+ this.forceTURN = urlParams.has('ForceTURN');
4403
+ if (this.forceTURN) {
4404
+ Logger.info('Forcing TURN usage by setting ICE Transport Policy in peer connection config.');
4405
+ this.cfg.iceTransportPolicy = 'relay';
4872
4406
  }
4873
- if (this.data.length > this.canvas.width / this.tickStep()) {
4874
- this.data.shift();
4407
+ this.cfg.bundlePolicy = urlParams.has('ForceMaxBundle')
4408
+ ? 'max-bundle'
4409
+ : 'balanced';
4410
+ this.cfg.iceCandidatePoolSize = 1;
4411
+ // When ?useMic check for SSL or localhost
4412
+ const isLocalhostConnection = location.hostname === 'localhost' || location.hostname === '127.0.0.1';
4413
+ const isHttpsConnection = location.protocol === 'https:';
4414
+ if (this.useMic && !isLocalhostConnection && !isHttpsConnection) {
4415
+ this.useMic = false;
4416
+ Logger.error('Microphone access in the browser will not work if you are not on HTTPS or localhost. Disabling mic access.');
4417
+ Logger.error("For testing you can enable HTTP microphone access Chrome by visiting chrome://flags/ and enabling 'unsafely-treat-insecure-origin-as-secure'");
4875
4418
  }
4876
- this.draw();
4419
+ this.setupPeerConnection(this.cfg);
4877
4420
  }
4878
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: StatGraphComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
4879
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "21.2.0", type: StatGraphComponent, isStandalone: true, selector: "app-stat-graph", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, stat: { classPropertyName: "stat", publicName: "stat", isSignal: true, isRequired: false, transformFunction: null }, color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: false, transformFunction: null }, tickStep: { classPropertyName: "tickStep", publicName: "tickStep", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "window:resize": "resize()" } }, viewQueries: [{ propertyName: "graph", first: true, predicate: ["graph"], descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"content\">\n <p>{{ label() }}: {{ current() }} | Min:{{ min() }} | Max:{{ max() }}</p>\n <canvas #graph class=\"graph\"></canvas>\n</div>\n", styles: [".content{width:100%;height:100%}.content p{color:#fff;margin:5px 0 0;height:25px}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
4880
- }
4881
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: StatGraphComponent, decorators: [{
4882
- type: Component,
4883
- args: [{ selector: 'app-stat-graph', changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"content\">\n <p>{{ label() }}: {{ current() }} | Min:{{ min() }} | Max:{{ max() }}</p>\n <canvas #graph class=\"graph\"></canvas>\n</div>\n", styles: [".content{width:100%;height:100%}.content p{color:#fff;margin:5px 0 0;height:25px}\n"] }]
4884
- }], ctorParameters: () => [], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], stat: [{ type: i0.Input, args: [{ isSignal: true, alias: "stat", required: false }] }], color: [{ type: i0.Input, args: [{ isSignal: true, alias: "color", required: false }] }], tickStep: [{ type: i0.Input, args: [{ isSignal: true, alias: "tickStep", required: false }] }], graph: [{ type: i0.ViewChild, args: ['graph', { isSignal: true }] }], resize: [{
4885
- type: HostListener,
4886
- args: ['window:resize']
4887
- }] } });
4888
-
4889
- class UnrealStatusComponent {
4890
- constructor() {
4891
- this.unrealInitialConfig = inject(UNREAL_CONFIG);
4892
- this.destroyRef = inject(DestroyRef);
4893
- this.store = inject(Store);
4894
- this.resetPercentageSignal$ = this.store
4895
- .select(selectShowLoader)
4896
- .pipe(filter(Falsy));
4897
- this.showLoader = this.store.selectSignal(selectShowLoader);
4898
- /**
4899
- * An observable that emits smoothed percentage values from 0 to 100.
4900
- */
4901
- this.messagePercents = toSignal(this.store.select(selectTotalProgress).pipe(keepMaxUntilReset(this.resetPercentageSignal$), floatToSmoothPercents(), distinctUntilChanged(), map$1((progress) => progress || 0)));
4902
- this.awsInstance = this.store.selectSignal(unrealFeature.selectAwsInstance);
4421
+ handlePeerConnectionEvents(pcClient) {
4422
+ fromEvent(pcClient, 'signalingstatechange')
4423
+ .pipe(map$1(() => this.pcClient?.signalingState), takeUntil(this.disconnect$))
4424
+ .subscribe((state) => {
4425
+ Logger.info('[Signaling state change] |', state, '|');
4426
+ this.commandTelemetryService.trackStopCommand('EXT-Signaling state change', true, { state, multi: true });
4427
+ });
4428
+ fromEvent(pcClient, 'iceconnectionstatechange')
4429
+ .pipe(map$1(() => this.pcClient?.iceConnectionState), takeUntil(this.disconnect$))
4430
+ .subscribe((state) => {
4431
+ Logger.info('[Browser ICE connection] |', state, '|');
4432
+ this.commandTelemetryService.trackStopCommand('EXT-Browser ICE connection', true, { state, multi: true });
4433
+ });
4434
+ fromEvent(pcClient, 'icegatheringstatechange')
4435
+ .pipe(map$1(() => this.pcClient?.iceGatheringState), takeUntil(this.disconnect$))
4436
+ .subscribe((state) => {
4437
+ Logger.info('[Browser ICE gathering] |', state, '|');
4438
+ this.commandTelemetryService.trackStopCommand('EXT-Browser ICE gathering', true, { state, multi: true });
4439
+ });
4440
+ fromEvent(pcClient, 'connectionstatechange')
4441
+ .pipe(map$1(() => this.pcClient?.connectionState), takeUntil(this.disconnect$))
4442
+ .subscribe((state) => {
4443
+ Logger.info('[RTCPeerConnection state] |', state, '|');
4444
+ });
4445
+ fromEvent(pcClient, 'icecandidate')
4446
+ .pipe(map$1((e) => e.candidate), filter(Truthy), tap$1((candidate) => Logger.info('[Browser ICE candidate]', '| Type=', candidate.type, '| Protocol=', candidate.protocol, '| Address=', candidate.address, '| Port=', candidate.port, '|')), takeUntil(this.disconnect$))
4447
+ .subscribe((candidate) => this.signallingSrv.send({ type: 'iceCandidate', candidate }));
4448
+ // When the server creates the DataChannel (server-offer flow),
4449
+ // we receive it here via the 'datachannel' event
4450
+ fromEvent(pcClient, 'datachannel')
4451
+ .pipe(tap$1((e) => Logger.info(`[DATACHANNEL] Received remote DC: label="${e.channel.label}" id=${e.channel.id} protocol="${e.channel.protocol}" ordered=${e.channel.ordered}`)), map$1((e) => e.channel), takeUntil(this.disconnect$))
4452
+ .subscribe((datachannel) => {
4453
+ this.dcClient = this.setupDataChannelCallbacks(datachannel);
4454
+ });
4455
+ const onTrack$ = fromEvent(pcClient, 'track').pipe(takeUntil(this.disconnect$), filter((e) => !!e.track), tap$1((e) => Logger.info('Got track - ' +
4456
+ e.track.kind +
4457
+ ' id=' +
4458
+ e.track.id +
4459
+ ' readyState=' +
4460
+ e.track.readyState)), share());
4461
+ onTrack$
4462
+ .pipe(filter((e) => e.track.kind === 'audio'), map$1((e) => e.streams[0]))
4463
+ .subscribe((stream) => this.videoService.attachAudioStream(stream));
4464
+ onTrack$
4465
+ .pipe(filter((e) => e.track.kind === 'video'), map$1((e) => e.streams[0]), tap$1(() => Logger.info('Set video source from video track ontrack.')))
4466
+ .subscribe((stream) => {
4467
+ if (this.pcClient) {
4468
+ this.videoService.attachVideoStream(stream, this.pcClient);
4469
+ }
4470
+ });
4471
+ }
4472
+ async setupTransceiversAsync(pc) {
4473
+ const hasTransceivers = pc.getTransceivers().length > 0;
4474
+ // Set up a transceiver for getting UE video
4475
+ pc.addTransceiver('video', { direction: 'recvonly' });
4476
+ // Set up a transceiver for sending mic audio to UE and receiving audio from UE
4477
+ if (!this.useMic) {
4478
+ pc.addTransceiver('audio', { direction: 'recvonly' });
4479
+ }
4480
+ else {
4481
+ const audioSendOptions = this.useMic
4482
+ ? {
4483
+ autoGainControl: false,
4484
+ channelCount: 1,
4485
+ echoCancellation: false,
4486
+ latency: 0,
4487
+ noiseSuppression: false,
4488
+ sampleRate: 48000,
4489
+ sampleSize: 16,
4490
+ volume: 1.0,
4491
+ }
4492
+ : false;
4493
+ // Note using mic on android chrome requires SSL or chrome://flags/ "unsafely-treat-insecure-origin-as-secure"
4494
+ const stream = await navigator.mediaDevices.getUserMedia({
4495
+ video: false,
4496
+ audio: audioSendOptions,
4497
+ });
4498
+ if (stream) {
4499
+ if (hasTransceivers) {
4500
+ for (const transceiver of pc.getTransceivers()) {
4501
+ if (transceiver &&
4502
+ transceiver.receiver &&
4503
+ transceiver.receiver.track &&
4504
+ transceiver.receiver.track.kind === 'audio') {
4505
+ for (const track of stream.getTracks()) {
4506
+ if (track.kind && track.kind === 'audio') {
4507
+ await transceiver.sender.replaceTrack(track);
4508
+ transceiver.direction = 'sendrecv';
4509
+ }
4510
+ }
4511
+ }
4512
+ }
4513
+ }
4514
+ else {
4515
+ for (const track of stream.getTracks()) {
4516
+ if (track.kind && track.kind === 'audio') {
4517
+ pc.addTransceiver(track, { direction: 'sendrecv' });
4518
+ }
4519
+ }
4520
+ }
4521
+ }
4522
+ else {
4523
+ pc.addTransceiver('audio', { direction: 'recvonly' });
4524
+ }
4525
+ }
4526
+ }
4527
+ setupDataChannelCallbacks(datachannel) {
4528
+ try {
4529
+ Logger.log(`[DATACHANNEL] Setting up datachannel "${datachannel.label}" callbacks...`);
4530
+ this.commandTelemetryService.trackStartCommand('EXT-DataChannel Connection');
4531
+ // Inform browser we would like binary data as an ArrayBuffer (FF chooses Blob by default!)
4532
+ datachannel.binaryType = 'arraybuffer';
4533
+ fromEvent(datachannel, 'close')
4534
+ .pipe(
4535
+ // Only dispatch disconnect if not already reconnecting to avoid infinite loops
4536
+ filter(() => !this.isReconnecting()), take(1))
4537
+ .subscribe((e) => {
4538
+ Logger.warn(`[DATACHANNEL] Data channel disconnected: ${datachannel.label}(${datachannel.id})`, e);
4539
+ this.store.dispatch(disconnectStream({
4540
+ reason: DisconnectReason.DataChannelClosed,
4541
+ message: 'DataChannelClosed',
4542
+ }));
4543
+ });
4544
+ fromEvent(datachannel, 'error')
4545
+ .pipe(takeUntil(this.disconnect$))
4546
+ .subscribe((e) => {
4547
+ const message = `[DATACHANNEL] Data channel "${datachannel.label}" error. id:${datachannel.id}`;
4548
+ Logger.error(message, e);
4549
+ });
4550
+ fromEvent(datachannel, 'open')
4551
+ .pipe(tap$1(() => {
4552
+ Logger.info(`[DATACHANNEL] Data channel "${datachannel.label}" connected. id:${datachannel.id}`);
4553
+ }), takeUntil(this.disconnect$))
4554
+ .subscribe(() => {
4555
+ this.commandTelemetryService.trackStopCommand('EXT-DataChannel Connection');
4556
+ this.onDataChannelConnected();
4557
+ });
4558
+ fromEvent(datachannel, 'message')
4559
+ .pipe(
4560
+ /* tap((e: MessageEvent) => {
4561
+ const view = new Uint8Array(e.data);
4562
+ Logger.info(
4563
+ `[DATACHANNEL] Message received on "${datachannel.label}" id=${datachannel.id}, byteCode=${view[0]}, size=${view.byteLength}`,
4564
+ );
4565
+ }),*/
4566
+ map$1((e) => e.data), takeUntil(this.disconnect$))
4567
+ .subscribe((data) => this.onDataChannelMessage$.next(data));
4568
+ return datachannel;
4569
+ }
4570
+ catch (e) {
4571
+ Logger.warn('[DATACHANNEL] Datachannel setup caused an exception: ', e);
4572
+ return null;
4573
+ }
4903
4574
  }
4904
- ngOnInit() {
4905
- /**
4906
- * A subject that emits null values to trigger nullification.
4907
- */
4908
- const nullify$ = new Subject();
4909
- /**
4910
- * An observable that emits null values with a delay of 2000 milliseconds.
4911
- */
4912
- const nullifyObs$ = nullify$
4913
- .asObservable()
4914
- .pipe(delay(2000));
4915
- /**
4916
- * An observable that emits the status message from the Unreal feature store.
4917
- */
4918
- const signalingStatus$ = this.store.select(unrealFeature.selectStatusMessage);
4919
- /**
4920
- * An observable that emits connection status messages based on the Cirrus connection state.
4921
- * Then call side effect to nullify the message.
4922
- */
4923
- const cirrusConnected$ = this.store
4924
- .select(unrealFeature.selectCirrusConnected)
4925
- .pipe(skip(1), switchMap((connected) => connected
4926
- ? of('Creating Socket...')
4927
- : of('Connection closed.').pipe(tap(() => nullify$.next(null)))));
4928
- /**
4929
- * An observable that emits the viewport visibility state.
4930
- */
4931
- const viewportTrigger$ = this.store.select(unrealFeature.selectViewportReady);
4932
- /**
4933
- * An observable that will nullify any message displayed after 500 mSec, when viewport changes its state.
4934
- */
4935
- const streamToggle$ = merge(viewportTrigger$.pipe(filter(Falsy), skip(1)), viewportTrigger$.pipe(filter(Truthy))).pipe(delay(500), map$1(() => null));
4936
- /**
4937
- * A subscription that merges various status observables and emits debounced status via postMessage.
4938
- */
4939
- merge(nullifyObs$, cirrusConnected$, signalingStatus$, streamToggle$)
4940
- .pipe(debounceTime$1(0), tapLog('STATUS=>'), takeUntilDestroyed(this.destroyRef))
4941
- .subscribe((status) => this.sendMessageToIndex(status));
4575
+ onDataChannelConnected() {
4576
+ let statusMessage = 'Session connected, video stream starting.';
4577
+ if (navigator.userAgent.includes('Firefox')) {
4578
+ statusMessage +=
4579
+ '\n If you are experiencing connection problems please try Google Chrome';
4580
+ }
4581
+ this.store.dispatch(dataChannelConnected({ statusMessage }));
4942
4582
  }
4943
4583
  /**
4944
- * Sends a message to the embedded iframe identified by the default value '3dsource_start_screen' ID.
4945
- * This function tries to post a message to the iframe's content window. If the content
4946
- * window is not accessible (e.g., due to cross-origin policies), the function fails silently.
4947
- *
4948
- * @param {string} text The message to be sent to the iframe. This could be a status update
4949
- * or any other relevant information that the iframe needs to process.
4584
+ * Creates an SDP offer, applies SDP munging, sets it as local description,
4585
+ * and sends it to the server via signaling.
4586
+ * Transceivers already configure media directions, so deprecated
4587
+ * offerToReceive* options are not used.
4950
4588
  */
4951
- sendMessageToIndex(text) {
4952
- const objectElement = document.getElementById(this.unrealInitialConfig?.screenLockerContainerId ||
4953
- SCREEN_LOCKER_CONTAINER_ID);
4589
+ async createOffer(pcClient) {
4954
4590
  try {
4955
- const objectDocument = objectElement.contentWindow;
4956
- objectDocument?.postMessage(text, '*');
4591
+ const offer = await pcClient.createOffer();
4592
+ offer.sdp = this.mungeSDP(offer.sdp ?? '');
4593
+ await pcClient.setLocalDescription(offer);
4594
+ this.signallingSrv.send(offer);
4957
4595
  }
4958
4596
  catch {
4959
- // Fails silently if the iframe's content window cannot be accessed
4597
+ Logger.error("Couldn't create offer");
4960
4598
  }
4961
4599
  }
4962
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: UnrealStatusComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
4963
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: UnrealStatusComponent, isStandalone: true, selector: "app-unreal-status", ngImport: i0, template: "@if (showLoader()) {\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 }); }
4600
+ /**
4601
+ * Creates an SDP answer, applies SDP munging, sets it as local description,
4602
+ * and sends it to the server via signaling.
4603
+ */
4604
+ async createAnswer(pcClient) {
4605
+ try {
4606
+ const answer = await pcClient.createAnswer();
4607
+ answer.sdp = this.mungeSDP(answer.sdp ?? '');
4608
+ await pcClient.setLocalDescription(answer);
4609
+ this.signallingSrv.send(answer);
4610
+ }
4611
+ catch (e) {
4612
+ Logger.error("Couldn't create answer", e);
4613
+ }
4614
+ }
4615
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: WebRtcPlayerService2, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
4616
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: WebRtcPlayerService2 }); }
4964
4617
  }
4965
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: UnrealStatusComponent, decorators: [{
4966
- type: Component,
4967
- args: [{ selector: 'app-unreal-status', changeDetection: ChangeDetectionStrategy.OnPush, imports: [SourceLoadingComponent], template: "@if (showLoader()) {\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"] }]
4618
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: WebRtcPlayerService2, decorators: [{
4619
+ type: Injectable
4620
+ }], ctorParameters: () => [] });
4621
+
4622
+ class AfkPlaywrightService extends AFKService {
4623
+ init() {
4624
+ return;
4625
+ }
4626
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: AfkPlaywrightService, deps: null, target: i0.ɵɵFactoryTarget.Injectable }); }
4627
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: AfkPlaywrightService }); }
4628
+ }
4629
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: AfkPlaywrightService, decorators: [{
4630
+ type: Injectable
4968
4631
  }] });
4969
4632
 
4970
- class VideoStatsComponent {
4633
+ class SignallingPlaywrightService extends SignallingService {
4971
4634
  constructor() {
4972
- this.collapsed = signal(false, ...(ngDevMode ? [{ debugName: "collapsed" }] : []));
4973
- this.store = inject(Store);
4974
- this.videoService = inject(VideoService);
4975
- this.isDevMode = inject(DevModeService).isDevMode;
4976
- this.obs$ = this.videoService.videoStats$;
4977
- this.viewportReady = this.store.selectSignal(unrealFeature.selectViewportReady);
4978
- this.ssInfo = this.store.selectSignal(selectSignalingParameters);
4979
- this.fpsTick = toSignal(this.obs$.pipe(map$1((stat) => ({
4980
- value: stat.aggregatedStats.framesPerSecond || 0,
4981
- }))));
4982
- this.fpsAvgTick = toSignal(this.obs$.pipe(map$1((stat) => ({
4983
- value: stat.aggregatedStats.avgFrameRate,
4984
- }))));
4985
- this.bitrateTick = toSignal(this.obs$.pipe(map$1((stat) => ({
4986
- value: stat.aggregatedStats.bitrate,
4987
- }))));
4988
- this.videoQP = toSignal(this.obs$.pipe(map$1((stat) => ({
4989
- value: stat.aggregatedStats.VideoEncoderQP,
4990
- }))));
4991
- this.bitrateAverageTick = toSignal(this.obs$.pipe(map$1((stat) => ({
4992
- value: stat.aggregatedStats.avgBitrate,
4993
- }))));
4994
- this.videoStatus = toSignal(this.obs$.pipe(map$1((event) => this.isDevMode &&
4995
- {
4996
- ...event.aggregatedStats,
4997
- quality: event.quality,
4998
- }), filter(Boolean), map$1((data) => {
4999
- return this.fields
5000
- .map((key) => {
5001
- return {
5002
- key,
5003
- value: data[key],
5004
- };
5005
- })
5006
- .filter((r) => !this.elementsToShow.includes(r.key));
5007
- })));
5008
- this.graphList = [];
5009
- this.idKey = 'com.source3d.unreal.stat';
5010
- this.elementsToShow = [];
5011
- this.trigger$ = new BehaviorSubject(undefined);
5012
- this.getFields();
4635
+ super();
5013
4636
  }
5014
- ngOnInit() {
5015
- this.fields = [
5016
- 'kalmanBitrate',
5017
- 'bitrateDrop',
5018
- 'runTime',
5019
- 'pixelRatio',
5020
- 'frameHeight',
5021
- 'frameWidth',
5022
- ':-------------',
5023
- 'jitter',
5024
- 'MBytesReceived',
5025
- 'framesDecoded',
5026
- 'framesDropped',
5027
- 'framesReceived',
5028
- 'keyFramesDecoded',
5029
- 'currentRoundTripTime',
5030
- 'packetsLost',
5031
- ':-------------',
5032
- 'quality',
5033
- 'videoCodec',
5034
- 'kind',
5035
- 'type',
5036
- 'codecs',
5037
- 'selectedPair',
5038
- //'pliCount',
5039
- //'sliCount',
5040
- //'transportId',
5041
- //'id',
5042
- ];
5043
- this.updateGraph();
4637
+ init() {
4638
+ return;
5044
4639
  }
5045
- updateGraph() {
5046
- this.graphList = this.fields.map((key) => {
5047
- const stat = this.obs$.pipe(map$1((val) => {
5048
- const value = val.aggregatedStats[key];
5049
- return { value };
5050
- }));
5051
- return {
5052
- key,
5053
- color: Math.floor(Math.random() * 16777216).toString(),
5054
- stat,
5055
- };
5056
- });
5057
- this.graphList$ = of(this.graphList).pipe(combineLatestWith(this.trigger$), map$1(([graphList]) => graphList.filter((r) => this.elementsToShow.includes(r.key))));
4640
+ connectToSignaling() {
4641
+ this.store.dispatch(setUnrealPlaywrightConfig());
4642
+ this.store.dispatch(dataChannelConnected({
4643
+ statusMessage: 'Playwright set data channel connected',
4644
+ }));
4645
+ return EMPTY;
5058
4646
  }
5059
- toggle() {
5060
- this.collapsed.update((collapsed) => !collapsed);
5061
- this.writeFields();
4647
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: SignallingPlaywrightService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
4648
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: SignallingPlaywrightService }); }
4649
+ }
4650
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: SignallingPlaywrightService, decorators: [{
4651
+ type: Injectable
4652
+ }], ctorParameters: () => [] });
4653
+
4654
+ class UnrealCommunicatorPlaywrightService extends UnrealCommunicatorService {
4655
+ init() {
4656
+ return;
5062
4657
  }
5063
- toggleGraph($event, el, value) {
5064
- $event.stopPropagation();
5065
- if ((value && typeof value !== 'number') ||
5066
- (typeof value === 'number' && isNaN(value))) {
5067
- return;
5068
- }
5069
- if (this.elementsToShow.includes(el)) {
5070
- this.elementsToShow = this.elementsToShow.filter((item) => item !== el);
5071
- }
5072
- else {
5073
- this.elementsToShow.push(el);
5074
- }
5075
- this.writeFields();
5076
- this.trigger$.next();
4658
+ sendCommandToUnreal() {
4659
+ return;
5077
4660
  }
5078
- getFields() {
5079
- try {
5080
- const { elements, collapsed } = JSON.parse(localStorage.getItem(this.idKey)) || [];
5081
- this.elementsToShow = elements || [];
5082
- this.collapsed.set(collapsed);
5083
- }
5084
- catch {
5085
- this.elementsToShow = [];
5086
- }
4661
+ sendInputData() {
4662
+ return;
5087
4663
  }
5088
- writeFields() {
5089
- localStorage.setItem(this.idKey, JSON.stringify({
5090
- collapsed: this.collapsed(),
5091
- elements: this.elementsToShow,
5092
- }));
4664
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: UnrealCommunicatorPlaywrightService, deps: null, target: i0.ɵɵFactoryTarget.Injectable }); }
4665
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: UnrealCommunicatorPlaywrightService }); }
4666
+ }
4667
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: UnrealCommunicatorPlaywrightService, decorators: [{
4668
+ type: Injectable
4669
+ }] });
4670
+
4671
+ class AggregatorPlaywrightService extends AggregatorService {
4672
+ init() {
4673
+ return;
5093
4674
  }
5094
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: VideoStatsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
5095
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", 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 2</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]=\"ssInfo() | 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 }); }
4675
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: AggregatorPlaywrightService, deps: null, target: i0.ɵɵFactoryTarget.Injectable }); }
4676
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: AggregatorPlaywrightService }); }
5096
4677
  }
5097
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: VideoStatsComponent, decorators: [{
5098
- type: Component,
5099
- 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 2</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]=\"ssInfo() | 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"] }]
5100
- }], ctorParameters: () => [] });
4678
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: AggregatorPlaywrightService, decorators: [{
4679
+ type: Injectable
4680
+ }] });
5101
4681
 
5102
- class ResumeStreamButtonComponent {
5103
- constructor() {
5104
- this.actions$ = inject(Actions);
5105
- this.store = inject(Store);
5106
- this.videoService = inject(VideoService);
5107
- this.destroyRef = inject(DestroyRef);
5108
- this.showReconnectPopup = this.store.selectSignal(selectShowReconnectPopup);
5109
- this.isLoaderScreenVisible = this.store.selectSignal(selectIsLoaderScreenVisible);
5110
- this.streamConfig = this.store.selectSignal(selectStreamConfig);
5111
- this.isExistMatchUrls = this.store.selectSignal(selectIsExistMatchUrls);
5112
- this.isSecondStart = signal(!!(this.streamConfig()?.autoStart && location.href.match(/^https/gi)), ...(ngDevMode ? [{ debugName: "isSecondStart" }] : []));
5113
- this.isShowResumeButton = computed(() => this.isLoaderScreenVisible() &&
5114
- this.showReconnectPopup() &&
5115
- this.isExistMatchUrls(), ...(ngDevMode ? [{ debugName: "isShowResumeButton" }] : []));
5116
- }
5117
- ngOnInit() {
5118
- this.store
5119
- .select(unrealFeature.selectCirrusConnected)
5120
- .pipe(skip$1(1), filter(Falsy), takeUntilDestroyed(this.destroyRef))
5121
- .subscribe(() => this.onDisconnect());
4682
+ class FreezeFramePlaywrightService extends FreezeFrameService {
4683
+ init() {
4684
+ return;
5122
4685
  }
5123
- ngAfterViewInit() {
5124
- this.actions$
5125
- .pipe(ofType(videoStreamInitialized), takeUntilDestroyed(this.destroyRef), filter(() => !!this.videoService.video?.paused))
5126
- .subscribe(() => {
5127
- Logger.info('Simulate Press Play');
5128
- this.videoService.play();
5129
- this.store.dispatch(resetAfk());
5130
- });
4686
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: FreezeFramePlaywrightService, deps: null, target: i0.ɵɵFactoryTarget.Injectable }); }
4687
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: FreezeFramePlaywrightService }); }
4688
+ }
4689
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: FreezeFramePlaywrightService, decorators: [{
4690
+ type: Injectable
4691
+ }] });
4692
+
4693
+ class CommandTelemetryPlaywrightService extends CommandTelemetryService {
4694
+ init() {
4695
+ return;
5131
4696
  }
5132
- connect() {
5133
- this.store.dispatch(startStream({ config: { autoStart: true } }));
4697
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: CommandTelemetryPlaywrightService, deps: null, target: i0.ɵɵFactoryTarget.Injectable }); }
4698
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: CommandTelemetryPlaywrightService }); }
4699
+ }
4700
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: CommandTelemetryPlaywrightService, decorators: [{
4701
+ type: Injectable
4702
+ }] });
4703
+
4704
+ class StreamStatusTelemetryPlaywrightService extends StreamStatusTelemetryService {
4705
+ init() {
4706
+ return;
5134
4707
  }
5135
- onDisconnect() {
5136
- this.isSecondStart.set(true);
4708
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: StreamStatusTelemetryPlaywrightService, deps: null, target: i0.ɵɵFactoryTarget.Injectable }); }
4709
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: StreamStatusTelemetryPlaywrightService }); }
4710
+ }
4711
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: StreamStatusTelemetryPlaywrightService, decorators: [{
4712
+ type: Injectable
4713
+ }] });
4714
+
4715
+ class ConsoleExtensionsPlaywrightService extends ConsoleExtensionsService {
4716
+ init() {
4717
+ return;
5137
4718
  }
5138
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ResumeStreamButtonComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
5139
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: ResumeStreamButtonComponent, isStandalone: true, selector: "app-resume-stream-button", ngImport: i0, 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"], dependencies: [{ kind: "component", type: SourceButtonComponent, selector: "src-button", inputs: ["type", "appearance", "weight", "size", "context", "customClass", "isFullWidth", "isPressed", "isDisabled", "isLoading", "isInverted", "iconButton", "srcButtonConfig", "formID", "data-testid", "ariaLabel"], outputs: ["onClick", "onSubmit"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
4719
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ConsoleExtensionsPlaywrightService, deps: null, target: i0.ɵɵFactoryTarget.Injectable }); }
4720
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ConsoleExtensionsPlaywrightService }); }
5140
4721
  }
5141
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ResumeStreamButtonComponent, decorators: [{
5142
- type: Component,
5143
- 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"] }]
4722
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ConsoleExtensionsPlaywrightService, decorators: [{
4723
+ type: Injectable
5144
4724
  }] });
5145
4725
 
5146
- class IntroSrcComponent {
4726
+ class FileReceiverPlaywrightService extends FileReceiverService {
4727
+ init() {
4728
+ return;
4729
+ }
4730
+ }
4731
+
4732
+ /**
4733
+ * Adaptive FPS controller that monitors the incoming WebRTC video stream
4734
+ * quality and dynamically adjusts Unreal Engine's `t.MaxFPS` console variable.
4735
+ *
4736
+ * ## How it works
4737
+ *
4738
+ * The service subscribes to `VideoService.videoStats$` (emitted every 250 ms)
4739
+ * and collects samples of FPS, bitrate, and QP (Quantisation Parameter).
4740
+ * Every {@link SAMPLE_WINDOW} ticks (~2 s) it averages the collected samples
4741
+ * and feeds them into a set of **fuzzy-logic membership functions** that
4742
+ * classify each metric into overlapping quality bands.
4743
+ *
4744
+ * ### Input signals
4745
+ *
4746
+ * **1. FPS ratio** = `actualFPS / currentTargetFPS` (0..1+)
4747
+ *
4748
+ * | Band | Range | Meaning |
4749
+ * |--------|---------------------|-----------------------------------------------|
4750
+ * | low | ratio < 0.7 → 1.0, linear ramp 0.7..0.9 → 0 | Stream cannot keep up with the target |
4751
+ * | ok | triangle 0.8..1.0, peak at 0.95 | Stream roughly matches the target |
4752
+ * | high | ratio 0.9..1.0 → ramp, >1.0 → 1.0 | Stream meets or exceeds the target |
4753
+ *
4754
+ * **2. Bitrate load** = `currentBitrate / MAX_BITRATE` (0..1+)
4755
+ *
4756
+ * `MAX_BITRATE` = 20 Mbit/s — the reference ceiling for the encoder.
4757
+ *
4758
+ * | Band | Range | Meaning |
4759
+ * |--------|-------------------------------------------------|-----------------------------------------------|
4760
+ * | high | load 0.5..0.8 → ramp, >0.8 → 1.0 | Encoder is using most of the available bandwidth |
4761
+ * | not high | load < 0.5 → 0.0 | Bandwidth headroom is available |
4762
+ *
4763
+ * **3. Quality** — mapped from QP via `mapQpToQuality()` (see {@link Quality}):
4764
+ *
4765
+ * | Quality | QP range | Meaning |
4766
+ * |----------|-----------|-------------------------------------------------|
4767
+ * | `lime` | QP <= 26 | Good — encoder has enough headroom |
4768
+ * | `orange` | QP 27..35 | Fair — encoder is under moderate pressure |
4769
+ * | `red` | QP > 35 | Poor — heavy compression, visible artefacts |
4770
+ *
4771
+ * ### Decision rules (evaluated in priority order, first match wins)
4772
+ *
4773
+ * | # | Condition | Decision |
4774
+ * |---|------------------------------------------------------------|--------------|
4775
+ * | 1 | FPS ratio is **low** (membership > 0.5) | **decrease** |
4776
+ * | 2 | FPS ratio is **ok** AND quality is `red` | **decrease** |
4777
+ * | 3 | FPS ratio is **ok** AND bitrate **high** AND quality is `orange` | **hold** |
4778
+ * | 4 | FPS ratio is **high** AND quality is `lime` AND bitrate **not high** | **increase** |
4779
+ * | — | Everything else | **hold** |
4780
+ *
4781
+ * ## Cooldown-based throttling
4782
+ *
4783
+ * The service continuously evaluates fuzzy rules every sample window.
4784
+ * After each FPS step change it enforces a cooldown before the next
4785
+ * change can be applied. The cooldown duration depends on the
4786
+ * (previous decision → next decision) pair:
4787
+ *
4788
+ * | Previous | Next | Cooldown |
4789
+ * |-----------|-----------|-----------------------------------------------------|
4790
+ * | decrease | decrease | {@link COOLDOWN_DECREASE_AFTER_DECREASE_MS} (2 s) |
4791
+ * | decrease | increase | {@link COOLDOWN_INCREASE_AFTER_DECREASE_MS} (10 s) |
4792
+ * | increase | decrease | {@link COOLDOWN_AFTER_INCREASE_MS} (2 s) |
4793
+ * | increase | increase | {@link COOLDOWN_AFTER_INCREASE_MS} (2 s) |
4794
+ *
4795
+ * This means the system reacts quickly after an increase (2 s in any
4796
+ * direction) but is cautious about stepping back up after a decrease
4797
+ * (10 s), while still allowing consecutive decreases at 2 s intervals.
4798
+ *
4799
+ * ## FPS dispatch
4800
+ *
4801
+ * FPS changes are dispatched via the NgRx `setMaxFps` action, which triggers
4802
+ * the existing `CommandsEffects.setMaxFps$` effect that sends the
4803
+ * `t.MaxFPS <value>` console command to Unreal Engine.
4804
+ */
4805
+ class FpsMonitorService {
5147
4806
  constructor() {
5148
- this.videoElement = viewChild('videoPlayerCover', ...(ngDevMode ? [{ debugName: "videoElement" }] : []));
4807
+ this.videoService = inject(VideoService);
5149
4808
  this.store = inject(Store);
5150
- this.imageIntroSrc = this.store.selectSignal(unrealFeature.selectImageIntroSrc);
5151
- this.videoIntroSrc = this.store.selectSignal(unrealFeature.selectVideoIntroSrc);
5152
- this.viewportReady = this.store.selectSignal(unrealFeature.selectViewportReady);
5153
- effect(() => {
5154
- const viewportReady = this.viewportReady();
5155
- const videoIntroSrc = this.videoIntroSrc();
5156
- const imageIntroSrc = this.imageIntroSrc();
5157
- untracked(() => {
5158
- if (viewportReady && (videoIntroSrc || imageIntroSrc)) {
5159
- this.store.dispatch(resetIntroSrc());
5160
- }
5161
- });
5162
- });
5163
- effect(() => {
5164
- const videoIntroSrc = this.videoIntroSrc();
5165
- const videoElement = this.videoElement();
5166
- if (!videoIntroSrc || !videoElement) {
5167
- return;
5168
- }
5169
- untracked(() => this.createVideo(videoIntroSrc, videoElement.nativeElement));
4809
+ /** Discrete FPS targets the controller can switch between (ascending order). */
4810
+ this.FPS_STEPS = [30, 40, 50, 60];
4811
+ /** Reference ceiling for bitrate (bits/sec) used to normalise bitrate load to 0..1. */
4812
+ this.MAX_BITRATE = 20_000_000;
4813
+ /**
4814
+ * Cooldown (ms) for any decision (increase or decrease) following an increase.
4815
+ * After an increase the system should react faster.
4816
+ */
4817
+ this.COOLDOWN_AFTER_INCREASE_MS = 2_000;
4818
+ /**
4819
+ * Cooldown (ms) for a decrease following a previous decrease.
4820
+ */
4821
+ this.COOLDOWN_DECREASE_AFTER_DECREASE_MS = 2_000;
4822
+ /**
4823
+ * Cooldown (ms) for an increase following a previous decrease.
4824
+ * The stream needs more time to stabilise before stepping back up.
4825
+ */
4826
+ this.COOLDOWN_INCREASE_AFTER_DECREASE_MS = 10_000;
4827
+ /**
4828
+ * Number of videoStats$ ticks to accumulate before evaluating.
4829
+ * At 250 ms per tick this gives a ~2 s sliding evaluation window,
4830
+ * long enough to smooth out single-frame jitter.
4831
+ */
4832
+ this.SAMPLE_WINDOW = 8;
4833
+ /** Current FPS target; starts at max (60 FPS) and adapts downward/upward. */
4834
+ this.currentFpsTarget = this.FPS_STEPS[this.FPS_STEPS.length - 1];
4835
+ this.samples = [];
4836
+ this.lastDecisionTime = Date.now();
4837
+ this.lastUpgrade = 'hold';
4838
+ this.init();
4839
+ }
4840
+ init() {
4841
+ this.videoService.videoStats$
4842
+ .pipe(withLatestFrom(this.store.select(unrealFeature.selectViewportReady)), filter(([, viewportReady]) => viewportReady))
4843
+ .subscribe(([stats]) => this.processTick(stats));
4844
+ }
4845
+ /**
4846
+ * Called on every videoStats$ emission (~250 ms).
4847
+ * Accumulates samples and evaluates once per SAMPLE_WINDOW.
4848
+ */
4849
+ processTick(stats) {
4850
+ const { framesPerSecond, bitrate, VideoEncoderQP } = stats.aggregatedStats;
4851
+ this.samples.push({
4852
+ fps: framesPerSecond ?? 0,
4853
+ bitrate: bitrate ?? 0,
4854
+ qp: VideoEncoderQP ?? 0,
5170
4855
  });
4856
+ // Wait until we have enough samples for a reliable average.
4857
+ if (this.samples.length < this.SAMPLE_WINDOW) {
4858
+ return;
4859
+ }
4860
+ const avg = this.averageSamples();
4861
+ this.samples = [];
4862
+ this.handleMonitoring(avg, this.currentFpsTarget);
5171
4863
  }
5172
- createVideo(src, videoContainer) {
5173
- const matchAbsolutePath = !!src.match(/.*\.(mp4|WebM)(\?.*)?$/i);
5174
- videoContainer.innerHTML = ''; // This will remove all existing content within the layout
5175
- const videoElement = document.createElement('video');
5176
- videoElement.autoplay = true;
5177
- videoElement.muted = true;
5178
- videoElement.loop = true;
5179
- if (!matchAbsolutePath) {
5180
- videoElement.poster = `${src}/cover.webp`;
4864
+ handleMonitoring(avg, currentTarget) {
4865
+ const decision = this.evaluateDecision(avg, currentTarget);
4866
+ this.upgradeFps(decision, avg.qp);
4867
+ }
4868
+ /**
4869
+ * Combines fuzzy membership values into a single discrete decision.
4870
+ * Rules are evaluated in priority order — first match wins.
4871
+ *
4872
+ * Inputs:
4873
+ * - `fpsRatio` = avgFPS / currentTargetFPS (e.g. 45/60 = 0.75)
4874
+ * - `bitrateLoad` = avgBitrate / 20 Mbit/s (e.g. 12M/20M = 0.6)
4875
+ * - `quality` = mapQpToQuality(avgQP) → 'lime' | 'orange' | 'red'
4876
+ */
4877
+ evaluateDecision(avg, currentTarget) {
4878
+ const fpsRatio = currentTarget > 0 ? avg.fps / currentTarget : 1;
4879
+ const bitrateLoad = avg.bitrate / this.MAX_BITRATE;
4880
+ const fpsLow = this.membershipFpsLow(fpsRatio);
4881
+ const fpsOk = this.membershipFpsOk(fpsRatio);
4882
+ const fpsHigh = this.membershipFpsHigh(fpsRatio);
4883
+ const bitrateHigh = this.membershipBitrateHigh(bitrateLoad);
4884
+ // Quality is derived from QP: lime (QP <= 26), orange (QP 27..35), red (QP > 35).
4885
+ const quality = qpToQuality(avg.qp);
4886
+ // Rule 1: FPS clearly below target (ratio roughly < 0.8) → step down immediately.
4887
+ if (fpsLow > 0.5) {
4888
+ return 'decrease';
5181
4889
  }
5182
- videoElement.style.position = 'absolute';
5183
- videoElement.style.width = '100%';
5184
- videoElement.style.height = '100%';
5185
- videoElement.style.objectFit = 'cover';
5186
- const sourceElement = document.createElement('source');
5187
- if (matchAbsolutePath) {
5188
- sourceElement.src = src;
4890
+ // Rule 2: FPS looks acceptable numerically, but encoder quality is 'red'
4891
+ // (QP > 35 — heavy compression artefacts). Reducing target FPS frees
4892
+ // encoder headroom and improves visual quality.
4893
+ if (fpsOk > 0.4 && quality === 'red') {
4894
+ return 'decrease';
5189
4895
  }
5190
- else {
5191
- sourceElement.src = `${src}/video.mp4`;
4896
+ // Rule 3: FPS is ok, but bitrate is above ~50-80% of 20 Mbit/s ceiling
4897
+ // and quality is 'orange' (QP 27..35). Near capacity — don't push higher.
4898
+ if (fpsOk > 0.4 && bitrateHigh > 0.5 && quality === 'orange') {
4899
+ return 'hold';
5192
4900
  }
5193
- sourceElement.type = 'video/mp4';
5194
- videoElement.appendChild(sourceElement);
5195
- videoElement.innerHTML += 'Browser not supported';
5196
- videoContainer.appendChild(videoElement);
5197
- }
5198
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: IntroSrcComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
5199
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", 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 [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 }); }
5200
- }
5201
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: IntroSrcComponent, decorators: [{
5202
- type: Component,
5203
- args: [{ selector: 'app-intro-src', imports: [NgOptimizedImage], changeDetection: ChangeDetectionStrategy.OnPush, template: "@let image = imageIntroSrc();\n@if (image) {\n <div class=\"backImage\">\n <img [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"] }]
5204
- }], ctorParameters: () => [], propDecorators: { videoElement: [{ type: i0.ViewChild, args: ['videoPlayerCover', { isSignal: true }] }] } });
5205
-
5206
- class UnrealSceneComponent {
5207
- get resizeValues() {
5208
- if (!this.isStudio()) {
5209
- return this.getResizeValuesUniversal();
4901
+ // Rule 4: FPS meets/exceeds target (ratio >= ~0.95), quality is 'lime'
4902
+ // (QP <= 26), and bitrate is below 50% of ceiling — safe to step up.
4903
+ if (fpsHigh > 0.5 &&
4904
+ quality === 'lime' &&
4905
+ bitrateHigh < 0.5 &&
4906
+ this.currentFpsTarget < this.FPS_STEPS[this.FPS_STEPS.length - 1]) {
4907
+ return 'increase';
5210
4908
  }
5211
- return this.getResizeValues();
5212
- }
5213
- get pixelRatio() {
5214
- return window.devicePixelRatio;
4909
+ return 'hold';
5215
4910
  }
5216
- onMouseOver() {
5217
- this.changeMouseOverScene.emit(true);
4911
+ // ---------------------------------------------------------------------------
4912
+ // Fuzzy membership functions
4913
+ //
4914
+ // Each function maps a normalised input (0..1+) to a membership degree (0..1).
4915
+ // Overlapping trapezoid/triangle shapes let multiple categories be partially
4916
+ // active at the same time, which makes the rules less brittle than hard thresholds.
4917
+ // ---------------------------------------------------------------------------
4918
+ /**
4919
+ * FPS ratio membership "low".
4920
+ * Input: ratio = actualFPS / targetFPS (e.g. 42/60 = 0.70).
4921
+ *
4922
+ * ratio < 0.7 → 1.0 (clearly failing, e.g. 40/60)
4923
+ * ratio 0.7..0.9 → linear ramp down (e.g. 0.8 → 0.5)
4924
+ * ratio > 0.9 → 0.0 (keeping up)
4925
+ */
4926
+ membershipFpsLow(ratio) {
4927
+ if (ratio < 0.7)
4928
+ return 1.0;
4929
+ if (ratio > 0.9)
4930
+ return 0.0;
4931
+ return (0.9 - ratio) / 0.2;
5218
4932
  }
5219
- onMouseOut() {
5220
- this.changeMouseOverScene.emit(false);
4933
+ /**
4934
+ * FPS ratio membership "ok" — triangle shape.
4935
+ * Input: ratio = actualFPS / targetFPS.
4936
+ *
4937
+ * ratio < 0.8 → 0.0
4938
+ * ratio 0.8..0.95 → ramp up, peak at 0.95 (e.g. 57/60)
4939
+ * ratio 0.95..1.0 → ramp down
4940
+ * ratio > 1.0 → 0.0
4941
+ */
4942
+ membershipFpsOk(ratio) {
4943
+ if (ratio < 0.8)
4944
+ return 0.0;
4945
+ if (ratio < 0.95)
4946
+ return (ratio - 0.8) / 0.15;
4947
+ if (ratio <= 1.0)
4948
+ return (1.0 - ratio) / 0.05;
4949
+ return 0.0;
5221
4950
  }
5222
- constructor() {
5223
- this.isStudio = input(false, ...(ngDevMode ? [{ debugName: "isStudio" }] : []));
5224
- this.useContainerAsSizeProvider = input(true, ...(ngDevMode ? [{ debugName: "useContainerAsSizeProvider" }] : []));
5225
- this.studioResolutionSize = input({
5226
- width: FULL_HD_WIDTH,
5227
- height: FULL_HD_HEIGHT,
5228
- }, ...(ngDevMode ? [{ debugName: "studioResolutionSize" }] : []));
5229
- this.studioResolutionValues = toObservable(computed(() => {
5230
- const values = this.studioResolutionSize();
5231
- return { width: values.width, height: values.height };
5232
- }));
5233
- this.videoElement = viewChild('videoContainer', ...(ngDevMode ? [{ debugName: "videoElement" }] : []));
5234
- this.changeMouseOverScene = output();
5235
- this.width = FULL_HD_WIDTH;
5236
- this.height = FULL_HD_HEIGHT;
5237
- this.actions$ = inject(Actions);
5238
- this.store = inject(Store);
5239
- this.isExistMatchUrls = this.store.selectSignal(selectIsExistMatchUrls);
5240
- this.isDevMode = inject(DevModeService).isDevMode;
5241
- this.lightMode = this.store.selectSignal(unrealFeature.selectLowBandwidth);
5242
- this.isFreezeFrameLoading = this.store.selectSignal(selectIsFreezeFrameLoading);
5243
- this.commandsSender = inject(UnrealCommunicatorService);
5244
- this.videoService = inject(VideoService);
5245
- this.element = inject(ElementRef);
5246
- this.destroyRef = inject(DestroyRef);
5247
- this.destroyRef.onDestroy(() => this.store.dispatch(destroyUnrealScene()));
5248
- effect(() => {
5249
- const videoElement = this.videoElement();
5250
- const isExistMatchUrls = this.isExistMatchUrls();
5251
- if (videoElement && isExistMatchUrls) {
5252
- this.store.dispatch(initSignalling());
5253
- }
5254
- });
5255
- afterNextRender({
5256
- read: () => {
5257
- const videoElement = this.videoElement();
5258
- if (videoElement) {
5259
- this.videoService.setContainer(videoElement.nativeElement);
5260
- if (this.element?.nativeElement) {
5261
- fromEvent(this.element?.nativeElement, 'contextmenu')
5262
- .pipe(takeUntilDestroyed(this.destroyRef))
5263
- .subscribe((event) => event.preventDefault());
5264
- }
5265
- this.listenResizeValues();
5266
- }
5267
- },
5268
- });
4951
+ /**
4952
+ * FPS ratio membership "high".
4953
+ * Input: ratio = actualFPS / targetFPS.
4954
+ *
4955
+ * ratio < 0.9 → 0.0
4956
+ * ratio 0.9..1.0 → linear ramp up (e.g. 0.95 → 0.5)
4957
+ * ratio >= 1.0 → 1.0 (meeting or exceeding target, e.g. 60/60)
4958
+ */
4959
+ membershipFpsHigh(ratio) {
4960
+ if (ratio < 0.9)
4961
+ return 0.0;
4962
+ if (ratio > 1.0)
4963
+ return 1.0;
4964
+ return (ratio - 0.9) / 0.1;
5269
4965
  }
5270
- listenResizeValues() {
5271
- 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 }) => {
5272
- this.width = width;
5273
- this.height = height;
5274
- }));
5275
- const resizeRequired$ = merge(this.actions$.pipe(ofType(videoStreamInitialized)), fromEvent(window, 'resize'));
5276
- const videoStreamSizeChanged$ = this.videoService?.videoStats$.pipe(filter(() => !this.isFreezeFrameLoading()), map$1((stats) => ({
5277
- w: stats.aggregatedStats.frameWidth || 0,
5278
- h: stats.aggregatedStats.frameHeight || 0,
5279
- })), distinctUntilChanged((previous, current) => fpIsASameAsB(previous.w, current.w, SAME_SIZE_THRESHOLD) &&
5280
- fpIsASameAsB(previous.h, current.h, SAME_SIZE_THRESHOLD)));
5281
- merge(customSizeInputChanged$, resizeRequired$, videoStreamSizeChanged$)
5282
- .pipe(debounceTime(DEBOUNCE_TO_MANY_RESIZE_CALLS), switchMap(() => {
5283
- const key = Math.random();
5284
- // Monitoring the size of the video stream
5285
- return timer(0, 300).pipe(take(3), map$1(() => key));
5286
- }), map$1((key) => ({ key, ...this.resizeValues })), distinctUntilChanged((previous, current) => fpIsASameAsB(previous.w, current.w, SAME_SIZE_THRESHOLD) &&
5287
- fpIsASameAsB(previous.h, current.h, SAME_SIZE_THRESHOLD) &&
5288
- previous.key === current.key), tap((size) => this.adaptVideo(size)), distinctUntilChanged((previous, current) => fpIsASameAsB(previous.w, current.w, SAME_SIZE_THRESHOLD) &&
5289
- fpIsASameAsB(previous.h, current.h, SAME_SIZE_THRESHOLD)), combineLatestWith(this.store
5290
- .select(unrealFeature.selectDataChannelConnected)
5291
- .pipe(filter(Truthy))), takeUntilDestroyed(this.destroyRef))
5292
- .subscribe(([size]) => {
5293
- this.store.dispatch(changeStreamResolutionAction({ width: size.w, height: size.h }));
5294
- this.sendResize(size);
5295
- });
5296
- this.actions$
5297
- .pipe(ofType(forceResizeViewport), takeUntilDestroyed(this.destroyRef))
5298
- .subscribe(() => this.sendResize(this.resizeValues));
4966
+ /**
4967
+ * Bitrate load membership "high".
4968
+ * Input: load = currentBitrate / MAX_BITRATE (20 Mbit/s).
4969
+ *
4970
+ * load < 0.5 → 0.0 (below 10 Mbit/s — plenty of headroom)
4971
+ * load 0.5..0.8 → linear ramp (e.g. 13M/20M = 0.65 → 0.5)
4972
+ * load > 0.8 → 1.0 (above 16 Mbit/s — near capacity)
4973
+ */
4974
+ membershipBitrateHigh(load) {
4975
+ if (load < 0.5)
4976
+ return 0.0;
4977
+ if (load > 0.8)
4978
+ return 1.0;
4979
+ return (load - 0.5) / 0.3;
5299
4980
  }
5300
- adaptVideo(size) {
5301
- const video = this.videoService.video;
5302
- const areaSize = this.resizeValues;
5303
- if (video?.style) {
5304
- const pR = this.pixelRatio;
5305
- video.width = this.makeEven(size.w / pR / areaSize.scale);
5306
- video.height = this.makeEven(size.h / pR / areaSize.scale);
5307
- if (!this.isStudio()) {
5308
- video.style.overflow = 'hidden';
5309
- video.style.objectFit = 'cover';
5310
- video.style.width = `${video.width}px`;
5311
- video.style.height = `${video.height}px`;
5312
- }
4981
+ // ---------------------------------------------------------------------------
4982
+ // Step actions
4983
+ // ---------------------------------------------------------------------------
4984
+ upgradeFps(decision, qp) {
4985
+ if (decision === 'hold')
4986
+ return;
4987
+ const now = Date.now();
4988
+ // Throttle: cooldown depends on (previous decision → current decision) pair.
4989
+ const isStepUp = decision === 'increase';
4990
+ const cooldown = this.lastUpgrade === 'decrease'
4991
+ ? isStepUp
4992
+ ? this.COOLDOWN_INCREASE_AFTER_DECREASE_MS
4993
+ : this.COOLDOWN_DECREASE_AFTER_DECREASE_MS
4994
+ : this.COOLDOWN_AFTER_INCREASE_MS;
4995
+ if (now - this.lastDecisionTime < cooldown) {
4996
+ return;
5313
4997
  }
5314
- this.store.dispatch(videoAdaptedToContainer());
4998
+ this.lastUpgrade = decision;
4999
+ const quality = qpToQuality(qp);
5000
+ const newTarget = this.getNextFpsTarget(this.currentFpsTarget, decision);
5001
+ Logger.info(`[FpsMonitor] Decision: ${decision} → FPS: ${newTarget}, Quality: ${quality}`);
5002
+ if (newTarget !== this.currentFpsTarget) {
5003
+ this.currentFpsTarget = newTarget;
5004
+ this.store.dispatch(setMaxFps({ maxFps: newTarget }));
5005
+ }
5006
+ this.lastDecisionTime = now;
5007
+ this.samples = [];
5008
+ }
5009
+ /**
5010
+ * Get the next FPS target based on current target and decision.
5011
+ * Clamps to valid FPS_STEPS range.
5012
+ */
5013
+ getNextFpsTarget(currentTarget, decision) {
5014
+ const currentIndex = this.FPS_STEPS.indexOf(currentTarget);
5015
+ // If current target is not in FPS_STEPS, find closest one
5016
+ const validIndex = currentIndex === -1
5017
+ ? this.findClosestFpsIndex(currentTarget)
5018
+ : currentIndex;
5019
+ const delta = decision === 'increase' ? 1 : -1;
5020
+ const newIndex = clampf(0, this.FPS_STEPS.length - 1, validIndex + delta);
5021
+ return this.FPS_STEPS[newIndex];
5315
5022
  }
5316
5023
  /**
5317
- * This method calculates the dimensions for resizing a layout in a configurator stream.
5318
- * It takes into account the pixel ratio of the device's screen and the orientation of the layout.
5319
- * The method uses the `fitIntoRectangle` function to calculate the dimensions that would allow the layout to fit into the maximum space while maintaining its aspect ratio.
5320
- * It also sets the maximum limits for the width and height based on the layout's orientation.
5321
- * If the scale of the fitted dimensions is less than 1, it assigns the fitted dimensions to the output. Otherwise, it sets the scale of the output to 1.
5322
- * @returns {Area} An object of type `Area` that contains the calculated dimensions for resizing the layout.
5024
+ * Find the closest FPS step index for a given target value.
5323
5025
  */
5324
- getResizeValuesUniversal() {
5325
- const pR = this.pixelRatio;
5326
- let containerW = window.innerWidth || 2;
5327
- let containerH = window.innerHeight || 2;
5328
- if (this.useContainerAsSizeProvider()) {
5329
- containerW = this.videoElement()?.nativeElement.offsetWidth || 2;
5330
- containerH = this.videoElement()?.nativeElement.offsetHeight || 2;
5331
- }
5332
- const maxSpace = { w: containerW * pR, h: containerH * pR };
5333
- const isVertical = containerW < containerH;
5334
- let maxH = FULL_HD_HEIGHT;
5335
- let maxW = FULL_HD_WIDTH;
5336
- const params = new URLSearchParams(location.search);
5337
- const maxRes = params.get('maxresolution');
5338
- if (Truthy(maxRes)) {
5339
- maxH = FULL_HD_HEIGHT * 2;
5340
- maxW = FULL_HD_WIDTH * 2;
5341
- }
5342
- const maxLimits = isVertical
5343
- ? { w: maxH, h: maxW, x: 0, y: 0 }
5344
- : { w: maxW, h: maxH, x: 0, y: 0 };
5345
- const props = fitIntoRectangle({ x: 0, y: 0, w: containerW, h: containerH }, { x: 0, y: 0, ...maxSpace });
5346
- let out = { ...props };
5347
- const fitIntoMax = fitIntoRectangle(props, maxLimits);
5348
- if (fitIntoMax.scale < 1) {
5349
- // If reducing size to 1920x1080 is required, do it
5350
- out = fitIntoMax;
5026
+ findClosestFpsIndex(target) {
5027
+ let closestIndex = 0;
5028
+ let closestDiff = Math.abs(this.FPS_STEPS[0] - target);
5029
+ for (let i = 1; i < this.FPS_STEPS.length; i++) {
5030
+ const diff = Math.abs(this.FPS_STEPS[i] - target);
5031
+ if (diff < closestDiff) {
5032
+ closestDiff = diff;
5033
+ closestIndex = i;
5034
+ }
5351
5035
  }
5352
- else {
5353
- // Otherwise, use the lowest resolution because the layout size is smaller than 1920x1080
5354
- out.scale = 1;
5036
+ return closestIndex;
5037
+ }
5038
+ /** Average all collected samples for the current evaluation window. */
5039
+ averageSamples() {
5040
+ const len = this.samples.length;
5041
+ if (len === 0)
5042
+ return { fps: 0, bitrate: 0, qp: 0 };
5043
+ let fps = 0;
5044
+ let bitrate = 0;
5045
+ let qp = 0;
5046
+ for (const s of this.samples) {
5047
+ fps += s.fps;
5048
+ bitrate += s.bitrate;
5049
+ qp += s.qp;
5355
5050
  }
5356
- /**
5357
- * Ensure that the size is always even.
5358
- */
5359
- out.w = this.makeEven(out.w);
5360
- out.h = this.makeEven(out.h);
5361
- return { ...out };
5051
+ return { fps: fps / len, bitrate: bitrate / len, qp: qp / len };
5362
5052
  }
5363
- getResizeValues() {
5364
- const pR = this.pixelRatio;
5365
- const containerW = pR * (this.videoElement()?.nativeElement.offsetWidth || 2);
5366
- const containerH = pR * (this.videoElement()?.nativeElement.offsetHeight || 2);
5367
- const maxSpace = { w: containerW, h: containerH };
5368
- const videoSizePixelPerfect = fitIntoRectangle({ x: 0, y: 0, w: this.width, h: this.height }, { x: 0, y: 0, ...maxSpace });
5369
- const out = fitIntoRectangle(videoSizePixelPerfect, {
5370
- x: 0,
5371
- y: 0,
5372
- w: Math.min(containerW, FULL_HD_WIDTH),
5373
- h: Math.min(containerH, FULL_HD_HEIGHT),
5374
- });
5375
- /**
5376
- * Ensure that the size is always even.
5377
- */
5378
- out.w = this.makeEven(out.w);
5379
- out.h = this.makeEven(out.h);
5380
- return out;
5053
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: FpsMonitorService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
5054
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: FpsMonitorService }); }
5055
+ }
5056
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: FpsMonitorService, decorators: [{
5057
+ type: Injectable
5058
+ }], ctorParameters: () => [] });
5059
+
5060
+ class FpsMonitorPlaywrightService extends FpsMonitorService {
5061
+ init() {
5062
+ return;
5381
5063
  }
5382
- sendResize(size) {
5383
- this.commandsSender.sendCommandToUnreal({
5384
- command: MetaBoxCommand.FChangeResolutionCommand,
5385
- payload: { resolution: { x: size.w, y: size.h } },
5386
- });
5064
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: FpsMonitorPlaywrightService, deps: null, target: i0.ɵɵFactoryTarget.Injectable }); }
5065
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: FpsMonitorPlaywrightService }); }
5066
+ }
5067
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: FpsMonitorPlaywrightService, decorators: [{
5068
+ type: Injectable
5069
+ }] });
5070
+
5071
+ class InputPlaywrightService extends InputService {
5072
+ init() {
5073
+ return;
5387
5074
  }
5388
- makeEven(value) {
5389
- return Math.floor((value + 1) / 2) * 2;
5075
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: InputPlaywrightService, deps: null, target: i0.ɵɵFactoryTarget.Injectable }); }
5076
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: InputPlaywrightService }); }
5077
+ }
5078
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: InputPlaywrightService, decorators: [{
5079
+ type: Injectable
5080
+ }] });
5081
+
5082
+ const ReceivedMimeTypes = {
5083
+ ApplicationJson: 'application/json',
5084
+ };
5085
+
5086
+ class FileHandlerService {
5087
+ constructor() {
5088
+ this.fileService = inject(FileReceiverService);
5089
+ this.fileHandlers = {
5090
+ [ReceivedMimeTypes.ApplicationJson]: this.handleJsonFile.bind(this),
5091
+ };
5390
5092
  }
5391
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: UnrealSceneComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
5392
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", 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 [class.lightModeCursor]=\"lightMode()\" 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 <ng-content select=\"app-low-bandwidth-detector\"></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 }); }
5093
+ handleJsonFile(data, correlationId) {
5094
+ const { blob } = data;
5095
+ return from(blob.text()).pipe(map$1((text) => JSON.parse(text)), filter(({ commandCallback }) => {
5096
+ return commandCallback.correlationId === correlationId;
5097
+ }));
5098
+ }
5099
+ observeFileResponse(mimetype, data, sender, timeOut = 60000) {
5100
+ const correlationId = generateUuid();
5101
+ const out = { ...data, correlationId };
5102
+ const observable = this.fileService.fileComplete$.pipe(switchMap((data) => {
5103
+ if (data.valid && this.fileHandlers[mimetype]) {
5104
+ return this.fileHandlers[mimetype](data, correlationId).pipe(filter(Truthy));
5105
+ }
5106
+ return of(null);
5107
+ }), timeout(timeOut), catchError((e) => {
5108
+ Logger.error(e);
5109
+ return of(null);
5110
+ }), take(1));
5111
+ setTimeout(() => sender(out), 0);
5112
+ return observable;
5113
+ }
5114
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: FileHandlerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
5115
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: FileHandlerService }); }
5393
5116
  }
5394
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: UnrealSceneComponent, decorators: [{
5395
- type: Component,
5396
- args: [{ selector: 'app-unreal-scene', changeDetection: ChangeDetectionStrategy.OnPush, imports: [
5397
- AfkTimeoutModalComponent,
5398
- UnrealStatusComponent,
5399
- FreezeFrameComponent,
5400
- VideoStatsComponent,
5401
- ResumeStreamButtonComponent,
5402
- ImageLoadingSrcComponent,
5403
- IntroSrcComponent,
5404
- ], template: "<div class=\"frame unreal-container\">\n <div #videoContainer [class.lightModeCursor]=\"lightMode()\" 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 <ng-content select=\"app-low-bandwidth-detector\"></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"] }]
5405
- }], 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: [{
5406
- type: HostListener,
5407
- args: ['mouseover']
5408
- }], onMouseOut: [{
5409
- type: HostListener,
5410
- args: ['mouseout']
5411
- }] } });
5117
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: FileHandlerService, decorators: [{
5118
+ type: Injectable
5119
+ }] });
5412
5120
 
5413
- class WebrtcErrorModalComponent {
5414
- #dialogRef = inject(DialogRef);
5121
+ class AnalyticsService {
5415
5122
  #store = inject(Store);
5416
- close() {
5417
- this.#dialogRef.close();
5123
+ #destroy$ = new Subject();
5124
+ sendEventToMixPanel(name, params = {}) {
5125
+ Logger.log('Analytics Event:', name, params);
5126
+ this.#store.dispatch(saveAnalyticsEvent({ event: { name, params } }));
5418
5127
  }
5419
- closeModalWithCirrusDisconnect() {
5128
+ constructor() {
5129
+ const abortSignal$ = this.#store
5130
+ .select(unrealFeature.selectViewportReady)
5131
+ .pipe(filter$1(Falsy), map(() => void 0));
5420
5132
  this.#store
5421
- .select(unrealFeature.selectCirrusConnected)
5422
- .pipe(take(1))
5423
- .subscribe((isConnected) => {
5424
- if (!isConnected) {
5425
- this.#store.dispatch(disconnectStream({
5426
- reason: DisconnectReason.DropConnection,
5427
- message: 'Modal Drop Connection',
5428
- }));
5133
+ .select(unrealFeature.selectViewportReady)
5134
+ .pipe(distinctUntilChanged(), filter$1(Truthy), tap(() => this.monitorMouse(abortSignal$)))
5135
+ .subscribe();
5136
+ }
5137
+ monitorMouse(abortSignal$) {
5138
+ this.#destroy$.next();
5139
+ const stop$ = merge(this.#destroy$.asObservable(), abortSignal$);
5140
+ const element = document.getElementById('streamingVideo');
5141
+ if (!element) {
5142
+ return;
5143
+ }
5144
+ // Monitor mousewheel events
5145
+ const mousewheel$ = fromEvent(element, 'wheel').pipe(takeUntil$1(stop$), throttleTime(1000));
5146
+ // Monitor mouse button states
5147
+ const mousedown$ = fromEvent(element, 'mousedown').pipe(takeUntil$1(stop$));
5148
+ const touchStart$ = fromEvent(element, 'touchstart').pipe(takeUntil$1(stop$));
5149
+ const touchMove$ = fromEvent(element, 'touchmove').pipe(takeUntil$1(stop$), throttleTime(200));
5150
+ // Track mouse movement while the left or right button is pressed
5151
+ mousedown$
5152
+ .pipe(filter$1((event) => event.button === 0 || event.button === 2), tap((downEvent) => {
5153
+ this.sendEventToMixPanel('mouse_interaction', {
5154
+ viewX: downEvent.clientX / element.clientWidth,
5155
+ viewY: downEvent.clientY / element.clientHeight,
5156
+ action: downEvent.button === 0 ? 'spinning' : 'panning',
5157
+ });
5158
+ }))
5159
+ .subscribe();
5160
+ // Track mouse movement while the left or right button is pressed
5161
+ touchStart$
5162
+ .pipe(tap((downEvent) => {
5163
+ const totalTouches = downEvent.touches.length;
5164
+ const touch = downEvent.touches[0];
5165
+ this.sendEventToMixPanel('touch_interaction', {
5166
+ viewX: touch.clientX / element.clientWidth,
5167
+ viewY: touch.clientY / element.clientHeight,
5168
+ action: totalTouches === 1 ? 'spinning' : 'panning',
5169
+ });
5170
+ }))
5171
+ .subscribe();
5172
+ let initialTouchDistance = 0;
5173
+ // Subscribe to mousewheel events
5174
+ touchMove$.subscribe((event) => {
5175
+ const totalTouches = event.touches.length;
5176
+ if (totalTouches < 2) {
5177
+ return;
5178
+ }
5179
+ // Calculate the current distance between two touches
5180
+ const touch1 = event.touches[0];
5181
+ const touch2 = event.touches[1];
5182
+ const currentDistance = Math.sqrt(Math.pow(touch2.clientX - touch1.clientX, 2) +
5183
+ Math.pow(touch2.clientY - touch1.clientY, 2));
5184
+ // Determine if zooming in or out based on distance change
5185
+ const deltaDistance = currentDistance - initialTouchDistance;
5186
+ const isZoomingIn = deltaDistance > 0;
5187
+ // Only send event if there's a significant change (avoid noise)
5188
+ if (Math.abs(deltaDistance) > 10) {
5189
+ this.sendEventToMixPanel('touch_zoom_interaction', {
5190
+ delta: isZoomingIn ? 1 : -1,
5191
+ action: isZoomingIn ? 'zoomin' : 'zoomout',
5192
+ });
5193
+ // Update initial distance for next comparison
5194
+ initialTouchDistance = currentDistance;
5429
5195
  }
5430
- this.close();
5196
+ });
5197
+ mousewheel$.subscribe((event) => {
5198
+ this.sendEventToMixPanel('mouse_zoom_interaction', {
5199
+ delta: -(event.deltaY / Math.abs(event.deltaY)),
5200
+ action: event.deltaY < 0 ? 'zoomin' : 'zoomout',
5201
+ });
5431
5202
  });
5432
5203
  }
5433
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: WebrtcErrorModalComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
5434
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.0", 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 ::ng-deep .ng-scroll-content{--_scrollbar-content-width: initial;--_viewport-padding-right: 12px}.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", "iconButton", "srcButtonConfig", "formID", "data-testid", "ariaLabel"], outputs: ["onClick", "onSubmit"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
5204
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: AnalyticsService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
5205
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: AnalyticsService }); }
5435
5206
  }
5436
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: WebrtcErrorModalComponent, decorators: [{
5437
- type: Component,
5438
- args: [{ selector: 'app-webrtc-error-modal', changeDetection: ChangeDetectionStrategy.OnPush, imports: [SourceButtonComponent], 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 ::ng-deep .ng-scroll-content{--_scrollbar-content-width: initial;--_viewport-padding-right: 12px}.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"] }]
5439
- }] });
5207
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: AnalyticsService, decorators: [{
5208
+ type: Injectable
5209
+ }], ctorParameters: () => [] });
5440
5210
 
5441
- class FilterSettingsComponent {
5211
+ class UnrealCallbackService {
5442
5212
  constructor() {
5443
- this.store = inject(Store);
5444
- this.storageKey = 'LBMFilterSettings';
5445
- this.defaultFilterModel = { ...DefaultFilterModel };
5446
- this.settings = { ...DefaultFilterModel };
5447
- }
5448
- ngOnInit() {
5449
- const savedSettings = localStorage.getItem(this.storageKey);
5450
- if (savedSettings) {
5451
- this.dispatch(JSON.parse(savedSettings));
5452
- }
5213
+ this.actions$ = inject(Actions);
5453
5214
  }
5454
- togglePanel() {
5455
- this.settings.panelOpen = !this.settings.panelOpen;
5456
- this.saveSettings();
5215
+ /**
5216
+ * Listens for unreal command callbacks matching a specific command or event type.
5217
+ *
5218
+ * Supports both MetaBoxCommand keys (e.g. `MetaBoxCommand.FLoadProductCommand`)
5219
+ * and custom Unreal callback events (e.g. `'cameraChanged'`, `'onSceneState'`).
5220
+ *
5221
+ * @param command - The command or event key to filter callbacks by
5222
+ * @returns Observable that emits matching callback payloads
5223
+ *
5224
+ * @example
5225
+ * ```ts
5226
+ * // Listen for a MetaBox command callback
5227
+ * this.unrealCallbackService
5228
+ * .fromUnrealCallback(MetaBoxCommand.FLoadProductCommand)
5229
+ * .subscribe(data => console.log(data));
5230
+ *
5231
+ * // Listen for a custom Unreal event
5232
+ * this.unrealCallbackService
5233
+ * .fromUnrealCallback('cameraChanged')
5234
+ * .subscribe(data => console.log(data));
5235
+ * ```
5236
+ */
5237
+ fromUnrealCallback(command) {
5238
+ return this.actions$.pipe(ofType(unrealCommandCallback), map$1(({ json }) => {
5239
+ if ('commandCallback' in json) {
5240
+ return json.commandCallback;
5241
+ }
5242
+ return json;
5243
+ }), filter((json) => json.command === command), map$1((data) => ({
5244
+ command: data.command,
5245
+ payload: 'payload' in data ? data.payload : null,
5246
+ })));
5457
5247
  }
5458
- saveSettings() {
5459
- const valid = Object.entries(this.settings).reduce((acc, [key, value]) => {
5460
- return Truthy(value) ? { ...acc, [key]: value } : acc;
5461
- }, {});
5462
- this.dispatch({ ...DefaultFilterModel, ...valid });
5248
+ /**
5249
+ * Sends a command and observes the matching response by correlationId.
5250
+ * Replaces the old `observeCommandResponse` standalone function.
5251
+ *
5252
+ * @param data - The command data to send
5253
+ * @param sender - Function to send the command to Unreal
5254
+ * @param timeOut - Timeout in ms (default 60000)
5255
+ * @param dispatchOnTimeout - Whether to emit on timeout (default true)
5256
+ *
5257
+ * @example
5258
+ * ```ts
5259
+ * this.unrealCallbackService
5260
+ * .observeCommandResponse<FLoopBackCommand['command']>(
5261
+ * { command: MetaBoxCommand.FLoopBackCommand },
5262
+ * (data) => this.communicator.sendCommandToUnreal(data),
5263
+ * )
5264
+ * .subscribe();
5265
+ * ```
5266
+ */
5267
+ observeCommandResponse(data, sender, timeOut = 60000, dispatchOnTimeout = true) {
5268
+ const correlationId = generateUuid();
5269
+ const observable = this.actions$.pipe(ofType(unrealCommandCallback), map$1(({ json }) => 'commandCallback' in json ? json.commandCallback : json), filter((json) => json.correlationId === correlationId), timeout(timeOut), catchError(() => dispatchOnTimeout
5270
+ ? of({ command: 'timeout', correlationId: 'timeout' })
5271
+ : of(null)), take(1), tapLog('Command Response Observer'), filter(Truthy), map$1((data) => ({
5272
+ command: data.command,
5273
+ payload: 'payload' in data ? data.payload : null,
5274
+ })));
5275
+ setTimeout(() => sender({ ...data, correlationId }), 0);
5276
+ return observable;
5463
5277
  }
5464
- dispatch(settings) {
5465
- try {
5466
- localStorage.setItem(this.storageKey, JSON.stringify(settings));
5467
- }
5468
- catch (e) {
5469
- Logger.error('Storage not available, Error saving filter settings:', e);
5470
- }
5471
- Object.assign(this.settings, settings);
5472
- Object.assign(FilterModel, settings);
5473
- this.store.dispatch(setKalmanSettings({ settings }));
5278
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: UnrealCallbackService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
5279
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: UnrealCallbackService }); }
5280
+ }
5281
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: UnrealCallbackService, decorators: [{
5282
+ type: Injectable
5283
+ }] });
5284
+
5285
+ class AfkTimeoutModalComponent {
5286
+ constructor() {
5287
+ this.#store = inject(Store);
5288
+ this.#afkService = inject(AFKService);
5289
+ this.isAfkTimerVisible = this.#store.selectSignal(unrealFeature.selectIsAfkTimerVisible);
5290
+ this.afkCountdown = this.#store.selectSignal(unrealFeature.selectAfkCountdown);
5474
5291
  }
5475
- reset() {
5476
- this.dispatch({
5477
- ...DefaultFilterModel,
5478
- panelOpen: this.settings.panelOpen,
5479
- });
5292
+ #store;
5293
+ #afkService;
5294
+ resetAfk() {
5295
+ this.#afkService.reset();
5480
5296
  }
5481
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: FilterSettingsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
5482
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: FilterSettingsComponent, isStandalone: true, selector: "app-filter-settings", ngImport: i0, template: "<div class=\"settings-container\">\n <button (click)=\"togglePanel()\" class=\"gear-button\">LBM</button>\n @if (settings.panelOpen) {\n <div class=\"settings-panel\">\n <h3>LBM Trigger Settings</h3>\n <h5>Data Flow Monitor</h5>\n <form>\n <label>\n Monitoring delay:\n <input\n [(ngModel)]=\"settings.monitoringDelayTime\"\n [placeholder]=\"defaultFilterModel.monitoringDelayTime\"\n type=\"number\"\n name=\"monitoringDelayTime\"\n />\n </label>\n\n <label>\n Minimum Bitrate (kbit/s):\n <input\n [(ngModel)]=\"settings.minimumBitrate\"\n [placeholder]=\"defaultFilterModel.minimumBitrate\"\n type=\"number\"\n name=\"minimumBitrate\"\n />\n </label>\n <label>\n Yellow Flag (%):\n <input\n [(ngModel)]=\"settings.yellowFlag\"\n [placeholder]=\"defaultFilterModel.yellowFlag\"\n type=\"number\"\n name=\"yellowFlag\"\n />\n </label>\n <label>\n Red Flag (%):\n <input\n [(ngModel)]=\"settings.redFlag\"\n [placeholder]=\"defaultFilterModel.redFlag\"\n type=\"number\"\n name=\"redFlag\"\n />\n </label>\n <label>\n Minimum FPS:\n <input\n [(ngModel)]=\"settings.minimumFps\"\n [placeholder]=\"defaultFilterModel.minimumFps\"\n type=\"number\"\n name=\"minimumFps\"\n />\n </label>\n <hr />\n <h5>Kalman Filter</h5>\n <label>\n Initial Bitrate Estimate (kbit/s):\n <input\n [(ngModel)]=\"settings.initialBitrateEstimate\"\n [placeholder]=\"defaultFilterModel.initialBitrateEstimate\"\n type=\"number\"\n name=\"initialBitrateEstimate\"\n />\n </label>\n <label>\n Initial Error Covariance:\n <input\n [(ngModel)]=\"settings.initialErrorCovariance\"\n [placeholder]=\"defaultFilterModel.initialErrorCovariance\"\n type=\"number\"\n name=\"initialErrorCovariance\"\n />\n </label>\n <label>\n Process Noise (Q):\n <input\n [(ngModel)]=\"settings.processNoise\"\n [placeholder]=\"defaultFilterModel.processNoise\"\n type=\"number\"\n name=\"processNoise\"\n />\n </label>\n <label>\n Measurement Noise (R):\n <input\n [(ngModel)]=\"settings.measurementNoise\"\n [placeholder]=\"defaultFilterModel.measurementNoise\"\n type=\"number\"\n name=\"measurementNoise\"\n />\n </label>\n </form>\n <br />\n <br />\n <div class=\"apply-button-container\">\n <button (click)=\"reset()\">Reset</button>\n <button (click)=\"saveSettings()\">Apply</button>\n </div>\n </div>\n }\n</div>\n", styles: [".settings-container{position:absolute;top:65px;right:10px;z-index:1000}.gear-button{right:0;width:50px;position:absolute;cursor:pointer;font-size:10px;background:none;font-weight:500;border:1px solid;border-radius:9px;background:#0162cc;color:#fff;padding:4px}.gear-button>*{color:#e6f419;filter:drop-shadow(1px 1px 1px rgb(0,0,0))}.settings-panel{font-size:13px;background:#fff;border:1px solid #ccc;padding:15px;margin-top:25px;border-radius:4px;box-shadow:0 2px 8px #0003}.settings-panel h3{margin-top:0}.settings-panel form{display:flex;flex-direction:column}.settings-panel label{margin-bottom:10px;display:flex;flex-direction:column}.settings-panel input{padding:4px;margin-top:4px;border:1px solid #ccc;border-radius:2px}.apply-button-container{position:absolute;bottom:15px;right:15px}.apply-button-container button{padding:6px 12px;border:none;background-color:#1976d2;color:#fff;border-radius:4px;cursor:pointer;margin:2px}.apply-button-container button:hover{background-color:#1565c0}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i1.NgForm, selector: "form:not([ngNoForm]):not([formGroup]):not([formArray]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
5297
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: AfkTimeoutModalComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
5298
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: AfkTimeoutModalComponent, isStandalone: true, selector: "app-afk-timeout-modal", ngImport: i0, template: "@if (isAfkTimerVisible()) {\n <div (click)=\"resetAfk()\" class=\"clickableState\" id=\"videoPlayOverlay\">\n <div class=\"resume-box\">\n <div aria-hidden=\"true\" class=\"resume-box__pic\" role=\"presentation\">\n <div [innerHTML]=\"afkCountdown()\" class=\"text-number\"></div>\n </div>\n <div class=\"resume-box__text\">\n <h3 class=\"resume-box__heading\">Session will time out soon</h3>\n <p>\n No activity detected. Press 'Continue' if you wish to keep your\n session active\n </p>\n </div>\n <src-button\n [isFullWidth]=\"true\"\n [data-testid]=\"'continue-session'\"\n weight=\"primary\"\n size=\"lg\"\n >\n Continue\n </src-button>\n </div>\n </div>\n}\n", styles: ["#videoPlayOverlay{position:absolute;z-index:30;top:0;width:100%;height:100%;font-size:1.8em;font-family:var(--src-font-family-body);background-color:#646464b3}.clickableState{display:flex;justify-content:center;align-items:center;cursor:pointer}.textDisplayState{display:flex}.hiddenState{display:none}.resume-box{width:340px;padding:32px 20px 20px;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 .resume-box__pic{width:72px;height:72px;margin:0 auto 22px;border-radius:48px;background:#ecf0f2;padding:12px;display:flex;align-items:center;justify-content:center}.resume-box .resume-box__pic .text-number{color:var(--src-colors-text-default, #1f2937);text-align:center;font-family:var(--src-font-family-body);font-size:30px;font-style:normal;font-weight:400;line-height:24px}.resume-box__text{margin-bottom:18px}.resume-box__text p{text-align:center;font-family:var(--src-font-family-body);font-size:var(--src-font-size-sm, 14px);font-style:normal;font-weight:400;line-height:24px;color:var(--src-text-body-secondary, #6b7280)}.resume-box__text .resume-box__heading{color:var(--src-text-body-main, #1f2937);text-align:center;font-family:var(--src-font-family-body);font-size:18px;font-style:normal;font-weight:500;line-height:26px;margin-bottom:8px}\n"], dependencies: [{ kind: "component", type: SourceButtonComponent, selector: "src-button", inputs: ["type", "appearance", "weight", "size", "context", "customClass", "isFullWidth", "isPressed", "isDisabled", "isLoading", "isInverted", "iconButton", "srcButtonConfig", "formID", "data-testid", "ariaLabel"], outputs: ["onClick", "onSubmit"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
5483
5299
  }
5484
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: FilterSettingsComponent, decorators: [{
5300
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: AfkTimeoutModalComponent, decorators: [{
5485
5301
  type: Component,
5486
- args: [{ selector: 'app-filter-settings', imports: [FormsModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"settings-container\">\n <button (click)=\"togglePanel()\" class=\"gear-button\">LBM</button>\n @if (settings.panelOpen) {\n <div class=\"settings-panel\">\n <h3>LBM Trigger Settings</h3>\n <h5>Data Flow Monitor</h5>\n <form>\n <label>\n Monitoring delay:\n <input\n [(ngModel)]=\"settings.monitoringDelayTime\"\n [placeholder]=\"defaultFilterModel.monitoringDelayTime\"\n type=\"number\"\n name=\"monitoringDelayTime\"\n />\n </label>\n\n <label>\n Minimum Bitrate (kbit/s):\n <input\n [(ngModel)]=\"settings.minimumBitrate\"\n [placeholder]=\"defaultFilterModel.minimumBitrate\"\n type=\"number\"\n name=\"minimumBitrate\"\n />\n </label>\n <label>\n Yellow Flag (%):\n <input\n [(ngModel)]=\"settings.yellowFlag\"\n [placeholder]=\"defaultFilterModel.yellowFlag\"\n type=\"number\"\n name=\"yellowFlag\"\n />\n </label>\n <label>\n Red Flag (%):\n <input\n [(ngModel)]=\"settings.redFlag\"\n [placeholder]=\"defaultFilterModel.redFlag\"\n type=\"number\"\n name=\"redFlag\"\n />\n </label>\n <label>\n Minimum FPS:\n <input\n [(ngModel)]=\"settings.minimumFps\"\n [placeholder]=\"defaultFilterModel.minimumFps\"\n type=\"number\"\n name=\"minimumFps\"\n />\n </label>\n <hr />\n <h5>Kalman Filter</h5>\n <label>\n Initial Bitrate Estimate (kbit/s):\n <input\n [(ngModel)]=\"settings.initialBitrateEstimate\"\n [placeholder]=\"defaultFilterModel.initialBitrateEstimate\"\n type=\"number\"\n name=\"initialBitrateEstimate\"\n />\n </label>\n <label>\n Initial Error Covariance:\n <input\n [(ngModel)]=\"settings.initialErrorCovariance\"\n [placeholder]=\"defaultFilterModel.initialErrorCovariance\"\n type=\"number\"\n name=\"initialErrorCovariance\"\n />\n </label>\n <label>\n Process Noise (Q):\n <input\n [(ngModel)]=\"settings.processNoise\"\n [placeholder]=\"defaultFilterModel.processNoise\"\n type=\"number\"\n name=\"processNoise\"\n />\n </label>\n <label>\n Measurement Noise (R):\n <input\n [(ngModel)]=\"settings.measurementNoise\"\n [placeholder]=\"defaultFilterModel.measurementNoise\"\n type=\"number\"\n name=\"measurementNoise\"\n />\n </label>\n </form>\n <br />\n <br />\n <div class=\"apply-button-container\">\n <button (click)=\"reset()\">Reset</button>\n <button (click)=\"saveSettings()\">Apply</button>\n </div>\n </div>\n }\n</div>\n", styles: [".settings-container{position:absolute;top:65px;right:10px;z-index:1000}.gear-button{right:0;width:50px;position:absolute;cursor:pointer;font-size:10px;background:none;font-weight:500;border:1px solid;border-radius:9px;background:#0162cc;color:#fff;padding:4px}.gear-button>*{color:#e6f419;filter:drop-shadow(1px 1px 1px rgb(0,0,0))}.settings-panel{font-size:13px;background:#fff;border:1px solid #ccc;padding:15px;margin-top:25px;border-radius:4px;box-shadow:0 2px 8px #0003}.settings-panel h3{margin-top:0}.settings-panel form{display:flex;flex-direction:column}.settings-panel label{margin-bottom:10px;display:flex;flex-direction:column}.settings-panel input{padding:4px;margin-top:4px;border:1px solid #ccc;border-radius:2px}.apply-button-container{position:absolute;bottom:15px;right:15px}.apply-button-container button{padding:6px 12px;border:none;background-color:#1976d2;color:#fff;border-radius:4px;cursor:pointer;margin:2px}.apply-button-container button:hover{background-color:#1565c0}\n"] }]
5302
+ args: [{ selector: 'app-afk-timeout-modal', changeDetection: ChangeDetectionStrategy.OnPush, imports: [SourceButtonComponent], template: "@if (isAfkTimerVisible()) {\n <div (click)=\"resetAfk()\" class=\"clickableState\" id=\"videoPlayOverlay\">\n <div class=\"resume-box\">\n <div aria-hidden=\"true\" class=\"resume-box__pic\" role=\"presentation\">\n <div [innerHTML]=\"afkCountdown()\" class=\"text-number\"></div>\n </div>\n <div class=\"resume-box__text\">\n <h3 class=\"resume-box__heading\">Session will time out soon</h3>\n <p>\n No activity detected. Press 'Continue' if you wish to keep your\n session active\n </p>\n </div>\n <src-button\n [isFullWidth]=\"true\"\n [data-testid]=\"'continue-session'\"\n weight=\"primary\"\n size=\"lg\"\n >\n Continue\n </src-button>\n </div>\n </div>\n}\n", styles: ["#videoPlayOverlay{position:absolute;z-index:30;top:0;width:100%;height:100%;font-size:1.8em;font-family:var(--src-font-family-body);background-color:#646464b3}.clickableState{display:flex;justify-content:center;align-items:center;cursor:pointer}.textDisplayState{display:flex}.hiddenState{display:none}.resume-box{width:340px;padding:32px 20px 20px;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 .resume-box__pic{width:72px;height:72px;margin:0 auto 22px;border-radius:48px;background:#ecf0f2;padding:12px;display:flex;align-items:center;justify-content:center}.resume-box .resume-box__pic .text-number{color:var(--src-colors-text-default, #1f2937);text-align:center;font-family:var(--src-font-family-body);font-size:30px;font-style:normal;font-weight:400;line-height:24px}.resume-box__text{margin-bottom:18px}.resume-box__text p{text-align:center;font-family:var(--src-font-family-body);font-size:var(--src-font-size-sm, 14px);font-style:normal;font-weight:400;line-height:24px;color:var(--src-text-body-secondary, #6b7280)}.resume-box__text .resume-box__heading{color:var(--src-text-body-main, #1f2937);text-align:center;font-family:var(--src-font-family-body);font-size:18px;font-style:normal;font-weight:500;line-height:26px;margin-bottom:8px}\n"] }]
5487
5303
  }] });
5488
5304
 
5489
- class LowBandwidthDetectorComponent {
5305
+ class FreezeFrameComponent {
5490
5306
  constructor() {
5491
- this.store = inject(Store);
5492
- this.dialog = inject(Dialog);
5493
- this.destroyRef = inject(DestroyRef);
5494
- this.videoService = inject(VideoService);
5495
- this.isDevMode = inject(DevModeService).isDevMode;
5496
- this.viewportReady$ = this.store.select(unrealFeature.selectViewportReady);
5497
- this.isLowBandwidth$ = this.store.select(unrealFeature.selectLowBandwidth);
5498
- this.lowBandwidthTrigger$ = this.isLowBandwidth$.pipe(distinctUntilChanged$1(), filter(Truthy));
5499
- this.highBandwidthTrigger$ = this.isLowBandwidth$.pipe(distinctUntilChanged$1(), filter(Falsy));
5500
- this.viewPortDestroyedTrigger$ = this.viewportReady$.pipe(distinctUntilChanged$1(), filter(Falsy));
5501
- this.aggregatedStats$ = this.videoService.videoStats$.pipe(map((data) => data.aggregatedStats), share());
5502
- this.nativeQuality$ = this.videoService.videoStats$.pipe(map((data) => data.quality));
5503
- this.monitoringTrigger$ = this.viewportReady$
5504
- .pipe(combineLatestWith(this.isLowBandwidth$), map(([viewPortReady, isLowBandwidth]) => viewPortReady && !isLowBandwidth))
5505
- .pipe(share());
5506
- this.reTrigger$ = this.monitoringTrigger$.pipe(map(() => ({ quality: 'lime' })));
5507
- this.canSwitchToLowBand$ = this.monitoringTrigger$.pipe(debounceTime(100), switchMap((canSwitch) => concat(of(false), timer(FilterModel.monitoringDelayTime).pipe(map(() => canSwitch))).pipe(take(2), takeUntil$1(this.monitoringTrigger$))), distinctUntilChanged$1(), share());
5508
- this.videoQuality$ = merge(this.nativeQuality$, this.reTrigger$).pipe(share(), takeUntil$1(this.viewPortDestroyedTrigger$));
5509
- this.fps$ = this.aggregatedStats$.pipe(map((data) => data.framesPerSecond || 0));
5510
- this.bitrateDrop$ = this.aggregatedStats$.pipe(map((data) => data.dataFlowCheckResult), filter((br) => br.isDropDetected), takeUntil$1(merge(this.viewPortDestroyedTrigger$, this.reTrigger$)));
5511
- this.fps = toSignal(this.fps$, { initialValue: 0 });
5512
- this.nativeQuality = toSignal(this.nativeQuality$);
5513
- this.isLowBandwidth = toSignal(this.isLowBandwidth$);
5514
- this.aggregatedStats = toSignal(this.aggregatedStats$);
5515
- this.isReducedQuality = signal(false, ...(ngDevMode ? [{ debugName: "isReducedQuality" }] : []));
5516
- this.isIndicatorExpanded = signal(false, ...(ngDevMode ? [{ debugName: "isIndicatorExpanded" }] : []));
5517
- this.isIndicatorExpandedTrigger$ = toObservable(this.isIndicatorExpanded).pipe(skip(1), distinctUntilChanged$1(), filter(Falsy));
5307
+ this.#store = inject(Store);
5308
+ this.freezeFrameProgressMessageFromVideo = this.#store.selectSignal(selectFreezeFrameProgressMessageFromVideo);
5309
+ this.combinedFreeze = this.#store.selectSignal(selectFreezeFrameCombinedDataUrl);
5518
5310
  }
5519
- ngOnInit() {
5520
- this.lowBandwidthTrigger$
5521
- .pipe(takeUntilDestroyed(this.destroyRef))
5522
- .subscribe(() => this.toggleIndicator(true));
5523
- merge(this.viewPortDestroyedTrigger$, this.highBandwidthTrigger$)
5524
- .pipe(takeUntilDestroyed(this.destroyRef))
5525
- .subscribe(() => this.isReducedQuality.set(false));
5526
- this.canSwitchToLowBand$
5527
- .pipe(filter(Truthy), tap(() => {
5528
- BITRATE_MONITOR.reset();
5529
- this.isReducedQuality.set(false);
5530
- }), switchMap(() => this.bitrateDrop$), tap(this.trySetLowBandwidth.bind(this)), map((dataFlowResult) => dataFlowResult?.isDropDetected), distinctUntilChanged$1(), tap(() => {
5531
- this.isReducedQuality.set(true);
5532
- }), switchMap(() => this.videoQuality$), filter((quality) => quality === 'lime'), tap(() => this.isReducedQuality.set(false)), takeUntilDestroyed(this.destroyRef))
5533
- .subscribe();
5311
+ #store;
5312
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: FreezeFrameComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
5313
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: FreezeFrameComponent, isStandalone: true, selector: "app-freeze-frame", ngImport: i0, template: "@let image = combinedFreeze();\n@let progress = freezeFrameProgressMessageFromVideo();\n\n<div class=\"freeze-images\">\n @if (image) {\n <img [src]=\"image\" class=\"videoStillImage\" alt=\"freezeFrameImage\" />\n }\n\n @if (progress) {\n <div class=\"progress-status\">\n {{ progress }}\n </div>\n }\n</div>\n", styles: [".freeze-images{pointer-events:none;position:absolute;left:0;top:0;width:100%;height:100%;z-index:1}.freeze-images .freezeFrameOverlay{top:0;left:0;width:100%;height:100%;position:absolute;object-fit:cover;object-position:center}.freeze-images .videoStillImage{position:absolute;z-index:1;top:0;left:0;width:100%;height:100%;object-fit:cover}.freeze-images .progress-status{position:absolute;top:50%;left:50%;z-index:1;display:flex;flex-direction:column;justify-content:center;align-items:center;width:auto;height:auto;margin:0;padding:10px;background-color:#ffffffb3;border:1px solid grey;transform:translate(-50%,-50%)}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
5314
+ }
5315
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: FreezeFrameComponent, decorators: [{
5316
+ type: Component,
5317
+ args: [{ selector: 'app-freeze-frame', changeDetection: ChangeDetectionStrategy.OnPush, template: "@let image = combinedFreeze();\n@let progress = freezeFrameProgressMessageFromVideo();\n\n<div class=\"freeze-images\">\n @if (image) {\n <img [src]=\"image\" class=\"videoStillImage\" alt=\"freezeFrameImage\" />\n }\n\n @if (progress) {\n <div class=\"progress-status\">\n {{ progress }}\n </div>\n }\n</div>\n", styles: [".freeze-images{pointer-events:none;position:absolute;left:0;top:0;width:100%;height:100%;z-index:1}.freeze-images .freezeFrameOverlay{top:0;left:0;width:100%;height:100%;position:absolute;object-fit:cover;object-position:center}.freeze-images .videoStillImage{position:absolute;z-index:1;top:0;left:0;width:100%;height:100%;object-fit:cover}.freeze-images .progress-status{position:absolute;top:50%;left:50%;z-index:1;display:flex;flex-direction:column;justify-content:center;align-items:center;width:auto;height:auto;margin:0;padding:10px;background-color:#ffffffb3;border:1px solid grey;transform:translate(-50%,-50%)}\n"] }]
5318
+ }] });
5319
+
5320
+ class SafePipe {
5321
+ constructor() {
5322
+ this.sanitizer = inject(DomSanitizer);
5534
5323
  }
5535
- trySetLowBandwidth(dataFlowResult) {
5536
- // Evaluate FPS and native quality
5537
- const isFpsLow = this.fps() < FilterModel.minimumFps;
5538
- const isNativeQualityRed = this.nativeQuality() === 'red';
5539
- const shouldSwitch = isNativeQualityRed || isFpsLow;
5540
- // Exit if conditions are not met or already in low bandwidth
5541
- if (!shouldSwitch || this.isLowBandwidth())
5542
- return;
5543
- const { quality, activeMedian } = dataFlowResult;
5544
- const isQualityRed = quality === 'red';
5545
- const isLowBitrate = activeMedian < FilterModel.minimumBitrate;
5546
- // Final check for a red drop or low bitrate, or low FPS
5547
- if (isQualityRed || isLowBitrate || isFpsLow) {
5548
- let reason;
5549
- if (isFpsLow) {
5550
- reason = `FPS < ${FilterModel.minimumFps}`;
5551
- }
5552
- else if (quality === 'red') {
5553
- reason = `Drop > ${FilterModel.redFlag}%`;
5554
- }
5555
- else {
5556
- reason = `Speed < ${FilterModel.minimumBitrate}`;
5557
- }
5558
- Logger.info(`LBM DETECTED: ${reason}, switching to low bandwidth; `, dataFlowResult.message);
5559
- this.store.dispatch(changeLowBandwidth({
5560
- lowBandwidth: true,
5561
- stats: {
5562
- dataFlowResult,
5563
- reason,
5564
- triggerTime: this.aggregatedStats()?.runTime ?? 0,
5565
- },
5566
- }));
5324
+ transform(value, type) {
5325
+ if (!value) {
5326
+ return value;
5567
5327
  }
5568
- }
5569
- toggleIndicator(value) {
5570
- this.isIndicatorExpanded.set(value);
5571
- if (value) {
5572
- this.hideIndicatorAfterDelay();
5328
+ switch (type) {
5329
+ case 'html':
5330
+ return this.sanitizer.bypassSecurityTrustHtml(value);
5331
+ case 'style':
5332
+ return this.sanitizer.bypassSecurityTrustStyle(value);
5333
+ case 'script':
5334
+ return this.sanitizer.bypassSecurityTrustScript(value);
5335
+ case 'url':
5336
+ return this.sanitizer.bypassSecurityTrustUrl(value);
5337
+ case 'resourceUrl':
5338
+ return this.sanitizer.bypassSecurityTrustResourceUrl(value);
5339
+ default:
5340
+ throw new Error(`Invalid safe type specified: ${type}`);
5573
5341
  }
5574
5342
  }
5575
- hideIndicatorAfterDelay() {
5576
- timer(3000)
5577
- .pipe(take(1), takeUntil$1(this.isIndicatorExpandedTrigger$))
5578
- .subscribe(() => this.toggleIndicator(false));
5343
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: SafePipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
5344
+ static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.2.0", ngImport: i0, type: SafePipe, isStandalone: true, name: "safe" }); }
5345
+ }
5346
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: SafePipe, decorators: [{
5347
+ type: Pipe,
5348
+ args: [{
5349
+ name: 'safe',
5350
+ }]
5351
+ }] });
5352
+
5353
+ class LowBandwidthModalComponent {
5354
+ constructor() {
5355
+ this.dialogRef = inject(DialogRef);
5579
5356
  }
5580
- openLBMDialog() {
5581
- this.dialog
5582
- .open(LowBandwidthModalComponent)
5583
- .closed.pipe(filter(Truthy))
5584
- .subscribe(() => {
5585
- this.toggleIndicator(false);
5586
- this.store.dispatch(changeLowBandwidth({ lowBandwidth: false }));
5587
- });
5357
+ close(value) {
5358
+ this.dialogRef.close(value || false);
5588
5359
  }
5589
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: LowBandwidthDetectorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
5590
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: LowBandwidthDetectorComponent, isStandalone: true, selector: "app-low-bandwidth-detector", ngImport: i0, 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"], dependencies: [{ kind: "component", type: FilterSettingsComponent, selector: "app-filter-settings" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
5360
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: LowBandwidthModalComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
5361
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.0", type: LowBandwidthModalComponent, isStandalone: true, selector: "app-low-bandwidth-modal", ngImport: i0, template: "<div [attr.data-testid]=\"'low-bandwidth'\" class=\"src-modal src-modal--lbm\">\n <header class=\"src-modal__header\">\n <h6 [attr.data-testid]=\"'title'\" class=\"src-modal__title\">\n Unstable Connection\n </h6>\n </header>\n <section class=\"src-modal__body\">\n <div\n [innerHtml]=\"\n 'Fluid Interactivity Modes were disabled due to an unstable connection. Showcase Gallery Mode is enabled. To regain full functionality, switch to Interactive Mode.'\n | safe: 'html'\n \"\n class=\"src-modal__scroll-box\"\n ></div>\n </section>\n <footer class=\"src-modal__footer\">\n <div class=\"src-modal__buttons\">\n <src-button\n (onClick)=\"close(true)\"\n [data-testid]=\"'switch-to-interactive-mode'\"\n >\n Switch\n </src-button>\n\n <src-button\n (onClick)=\"close()\"\n [data-testid]=\"'close-lbm-modal'\"\n weight=\"primary\"\n >\n Ok\n </src-button>\n </div>\n </footer>\n</div>\n", styles: [".src-modal--lbm{width:360px}.src-modal--lbm .src-modal__body{font-size:14px;line-height:24px}.src-modal--lbm .src-modal__buttons{display:flex;gap:8px}\n"], dependencies: [{ kind: "component", type: SourceButtonComponent, selector: "src-button", inputs: ["type", "appearance", "weight", "size", "context", "customClass", "isFullWidth", "isPressed", "isDisabled", "isLoading", "isInverted", "iconButton", "srcButtonConfig", "formID", "data-testid", "ariaLabel"], outputs: ["onClick", "onSubmit"] }, { kind: "pipe", type: SafePipe, name: "safe" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
5591
5362
  }
5592
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: LowBandwidthDetectorComponent, decorators: [{
5363
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: LowBandwidthModalComponent, decorators: [{
5593
5364
  type: Component,
5594
- 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"] }]
5365
+ args: [{ selector: 'app-low-bandwidth-modal', changeDetection: ChangeDetectionStrategy.OnPush, imports: [SafePipe, SourceButtonComponent], template: "<div [attr.data-testid]=\"'low-bandwidth'\" class=\"src-modal src-modal--lbm\">\n <header class=\"src-modal__header\">\n <h6 [attr.data-testid]=\"'title'\" class=\"src-modal__title\">\n Unstable Connection\n </h6>\n </header>\n <section class=\"src-modal__body\">\n <div\n [innerHtml]=\"\n 'Fluid Interactivity Modes were disabled due to an unstable connection. Showcase Gallery Mode is enabled. To regain full functionality, switch to Interactive Mode.'\n | safe: 'html'\n \"\n class=\"src-modal__scroll-box\"\n ></div>\n </section>\n <footer class=\"src-modal__footer\">\n <div class=\"src-modal__buttons\">\n <src-button\n (onClick)=\"close(true)\"\n [data-testid]=\"'switch-to-interactive-mode'\"\n >\n Switch\n </src-button>\n\n <src-button\n (onClick)=\"close()\"\n [data-testid]=\"'close-lbm-modal'\"\n weight=\"primary\"\n >\n Ok\n </src-button>\n </div>\n </footer>\n</div>\n", styles: [".src-modal--lbm{width:360px}.src-modal--lbm .src-modal__body{font-size:14px;line-height:24px}.src-modal--lbm .src-modal__buttons{display:flex;gap:8px}\n"] }]
5595
5366
  }] });
5596
5367
 
5597
- /*
5598
- * Copyright (c) 2026.
5599
- * 3DSource.com. Sergii Karanda steve@3dsource.com. All Rights Reserved.
5600
- */
5601
- class WebRtcPlayerService2 extends SubService {
5368
+ class ImageLoadingSrcComponent {
5602
5369
  constructor() {
5603
- super();
5604
- this.onDataChannelMessage$ = new Subject();
5605
- this.signallingSrv = inject(SignallingService);
5606
- this.videoService = inject(VideoService);
5607
- this.commandTelemetryService = inject(CommandTelemetryService);
5608
- this.cfg = {};
5609
- this.useMic = false;
5610
- this.forceTURN = false;
5611
- this.forceMonoAudio = false;
5612
- /** Whether the remote SDP has been applied to the current peer connection */
5613
- this.remoteDescriptionSet = false;
5614
- /** ICE candidates received before the remote description was set */
5615
- this.iceCandidateBuffer = [];
5616
- this.isReconnecting = this.store.selectSignal(unrealFeature.selectIsReconnecting);
5617
- this.init();
5618
- this.actions$.pipe(ofType(dropDC)).subscribe(() => this.dcClient?.close());
5619
- this.actions$.pipe(ofType(dropPC)).subscribe(() => this.pcClient?.close());
5370
+ this.#store = inject(Store);
5371
+ this.isLoaderScreenVisible = this.#store.selectSignal(selectIsLoaderScreenVisible);
5372
+ this.imageLoadingSrc = this.#store.selectSignal(unrealFeature.selectImageLoadingSrc);
5620
5373
  }
5621
- init() {
5622
- this.signallingSrv.onConfig$.subscribe((config) => {
5623
- this.videoService.create();
5624
- this.setConfig(config);
5625
- });
5626
- // Server-initiated offer: set remote description and respond with answer
5627
- this.signallingSrv.onOffer$.pipe(filter(Truthy)).subscribe((offer) => {
5628
- void this.handleServerOffer(offer);
5629
- });
5630
- // Answer to a client-initiated offer
5631
- this.signallingSrv.onWebRtcAnswer$
5632
- .pipe(filter(Truthy))
5633
- .subscribe((answer) => {
5634
- void this.handleServerAnswer(answer);
5635
- });
5636
- // ICE candidates — buffered until remote description is set
5637
- this.signallingSrv.onWebRtcIce$.subscribe((candidate) => {
5638
- this.commandTelemetryService.trackStopCommand('EXT-iceCandidate', true, {
5639
- multi: true,
5640
- });
5641
- if (this.remoteDescriptionSet) {
5642
- this.handleCandidateFromServer(candidate);
5643
- }
5644
- else {
5645
- this.iceCandidateBuffer.push(candidate);
5646
- }
5374
+ #store;
5375
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ImageLoadingSrcComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
5376
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", 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 [ngSrc]=\"image\" fill loading=\"lazy\" alt=\"image loading src\" />\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 }); }
5377
+ }
5378
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ImageLoadingSrcComponent, decorators: [{
5379
+ type: Component,
5380
+ 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 [ngSrc]=\"image\" fill loading=\"lazy\" alt=\"image loading src\" />\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"] }]
5381
+ }] });
5382
+
5383
+ class StatGraphComponent {
5384
+ constructor() {
5385
+ this.label = input.required(...(ngDevMode ? [{ debugName: "label" }] : []));
5386
+ this.stat = input(...(ngDevMode ? [undefined, { debugName: "stat" }] : []));
5387
+ this.color = input('#417bc9', ...(ngDevMode ? [{ debugName: "color" }] : []));
5388
+ this.tickStep = input(3, ...(ngDevMode ? [{ debugName: "tickStep" }] : []));
5389
+ this.el = inject(ElementRef);
5390
+ this.graph = viewChild('graph', ...(ngDevMode ? [{ debugName: "graph" }] : []));
5391
+ this.data = [];
5392
+ this.max = signal(0, ...(ngDevMode ? [{ debugName: "max" }] : []));
5393
+ this.min = signal(0, ...(ngDevMode ? [{ debugName: "min" }] : []));
5394
+ this.current = signal(0, ...(ngDevMode ? [{ debugName: "current" }] : []));
5395
+ effect(() => this.pushData(this.stat()?.value ?? null));
5396
+ afterNextRender({
5397
+ read: () => {
5398
+ const graph = this.graph();
5399
+ if (graph) {
5400
+ this.canvas = graph.nativeElement;
5401
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
5402
+ this.ctx = this.canvas.getContext('2d');
5403
+ }
5404
+ },
5647
5405
  });
5648
5406
  }
5649
- /**
5650
- * Handles an SDP offer received from the server.
5651
- * Creates a new peer connection, applies the remote offer,
5652
- * then responds with an SDP answer.
5653
- *
5654
- * The DataChannel is NOT created here — the server creates it
5655
- * and we receive it via the `datachannel` event on the peer connection.
5656
- */
5657
- async handleServerOffer(offer) {
5658
- Logger.info('Received offer from server, creating answer');
5659
- try {
5660
- this.remoteDescriptionSet = false;
5661
- this.iceCandidateBuffer.length = 0;
5662
- if (!this.pcClient) {
5663
- this.setupPeerConnection(this.cfg);
5664
- }
5665
- // Offer already defines media transceivers — don't add extras
5666
- await this.pcClient.setRemoteDescription(offer);
5667
- this.remoteDescriptionSet = true;
5668
- this.flushIceCandidateBuffer();
5669
- await this.createAnswer(this.pcClient);
5407
+ resize() {
5408
+ this.draw();
5409
+ }
5410
+ draw() {
5411
+ if (!this.canvas) {
5412
+ return;
5670
5413
  }
5671
- catch (e) {
5672
- Logger.error('Failed to handle server offer', e);
5673
- this.store.dispatch(disconnectStream({
5674
- reason: DisconnectReason.WebRTCError,
5675
- message: 'WebRTCError: Failed to handle server offer',
5676
- }));
5414
+ const all = this.data.filter((r) => r !== null && !isNaN(r));
5415
+ this.max.set(Math.max(...all));
5416
+ this.min.set(Math.min(...all));
5417
+ const scaleY = this.canvas.height / (this.max() || 1);
5418
+ this.current.set(all[all.length - 1] || 0);
5419
+ this.ctx.save();
5420
+ const bounds = this.el.nativeElement.getBoundingClientRect();
5421
+ this.canvas.width = bounds.width;
5422
+ this.canvas.height = bounds.height - 30; // -p height
5423
+ this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
5424
+ [...this.data].reverse().forEach((val, index) => {
5425
+ const h = (val || 0) * scaleY;
5426
+ this.ctx.fillStyle = this.getColor(this.color(), h);
5427
+ this.ctx.fillRect(index * this.tickStep(), this.canvas.height - h, this.tickStep(), h);
5428
+ });
5429
+ this.ctx.restore();
5430
+ }
5431
+ getColor(fillStyle, value = 0) {
5432
+ const { r, g, b } = HEXtoRGB(fillStyle);
5433
+ /* eslint-disable-next-line prefer-const */
5434
+ let [h, s, v] = RGBtoHSV(r, g, b);
5435
+ v = inverseLerp(0, this.canvas.height, value);
5436
+ {
5437
+ const [r, g, b] = HSVtoRGB(h, s, v);
5438
+ return '#' + RGBtoHEX(r, g, b);
5677
5439
  }
5678
5440
  }
5679
- /**
5680
- * Handles an SDP answer received from the server in response to our offer.
5681
- * Applies the remote description and flushes any buffered ICE candidates.
5682
- */
5683
- async handleServerAnswer(answer) {
5684
- Logger.info('Received answer from server');
5685
- try {
5686
- await this.pcClient?.setRemoteDescription(answer);
5687
- this.remoteDescriptionSet = true;
5688
- this.flushIceCandidateBuffer();
5441
+ pushData(stat) {
5442
+ this.data.push(stat);
5443
+ if (!this.canvas) {
5444
+ return;
5689
5445
  }
5690
- catch (e) {
5691
- Logger.error('Failed to set remote description from answer', e);
5446
+ if (this.data.length > this.canvas.width / this.tickStep()) {
5447
+ this.data.shift();
5692
5448
  }
5449
+ this.draw();
5693
5450
  }
5694
- /** Drains buffered ICE candidates after the remote description has been set */
5695
- flushIceCandidateBuffer() {
5696
- for (const candidate of this.iceCandidateBuffer) {
5697
- this.handleCandidateFromServer(candidate);
5698
- }
5699
- this.iceCandidateBuffer.length = 0;
5451
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: StatGraphComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
5452
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "21.2.0", type: StatGraphComponent, isStandalone: true, selector: "app-stat-graph", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, stat: { classPropertyName: "stat", publicName: "stat", isSignal: true, isRequired: false, transformFunction: null }, color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: false, transformFunction: null }, tickStep: { classPropertyName: "tickStep", publicName: "tickStep", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "window:resize": "resize()" } }, viewQueries: [{ propertyName: "graph", first: true, predicate: ["graph"], descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"content\">\n <p>{{ label() }}: {{ current() }} | Min:{{ min() }} | Max:{{ max() }}</p>\n <canvas #graph class=\"graph\"></canvas>\n</div>\n", styles: [".content{width:100%;height:100%}.content p{color:#fff;margin:5px 0 0;height:25px}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
5453
+ }
5454
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: StatGraphComponent, decorators: [{
5455
+ type: Component,
5456
+ args: [{ selector: 'app-stat-graph', changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"content\">\n <p>{{ label() }}: {{ current() }} | Min:{{ min() }} | Max:{{ max() }}</p>\n <canvas #graph class=\"graph\"></canvas>\n</div>\n", styles: [".content{width:100%;height:100%}.content p{color:#fff;margin:5px 0 0;height:25px}\n"] }]
5457
+ }], ctorParameters: () => [], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], stat: [{ type: i0.Input, args: [{ isSignal: true, alias: "stat", required: false }] }], color: [{ type: i0.Input, args: [{ isSignal: true, alias: "color", required: false }] }], tickStep: [{ type: i0.Input, args: [{ isSignal: true, alias: "tickStep", required: false }] }], graph: [{ type: i0.ViewChild, args: ['graph', { isSignal: true }] }], resize: [{
5458
+ type: HostListener,
5459
+ args: ['window:resize']
5460
+ }] } });
5461
+
5462
+ class UnrealStatusComponent {
5463
+ constructor() {
5464
+ this.unrealInitialConfig = inject(UNREAL_CONFIG);
5465
+ this.destroyRef = inject(DestroyRef);
5466
+ this.store = inject(Store);
5467
+ this.resetPercentageSignal$ = this.store
5468
+ .select(selectShowLoader)
5469
+ .pipe(filter(Falsy));
5470
+ this.showLoader = this.store.selectSignal(selectShowLoader);
5471
+ /**
5472
+ * An observable that emits smoothed percentage values from 0 to 100.
5473
+ */
5474
+ this.messagePercents = toSignal(this.store.select(selectTotalProgress).pipe(keepMaxUntilReset(this.resetPercentageSignal$), floatToSmoothPercents(), distinctUntilChanged(), map$1((progress) => progress || 0)));
5475
+ this.awsInstance = this.store.selectSignal(unrealFeature.selectAwsInstance);
5476
+ }
5477
+ ngOnInit() {
5478
+ /**
5479
+ * A subject that emits null values to trigger nullification.
5480
+ */
5481
+ const nullify$ = new Subject();
5482
+ /**
5483
+ * An observable that emits null values with a delay of 2000 milliseconds.
5484
+ */
5485
+ const nullifyObs$ = nullify$
5486
+ .asObservable()
5487
+ .pipe(delay(2000));
5488
+ /**
5489
+ * An observable that emits the status message from the Unreal feature store.
5490
+ */
5491
+ const signalingStatus$ = this.store.select(unrealFeature.selectStatusMessage);
5492
+ /**
5493
+ * An observable that emits connection status messages based on the Cirrus connection state.
5494
+ * Then call side effect to nullify the message.
5495
+ */
5496
+ const cirrusConnected$ = this.store
5497
+ .select(unrealFeature.selectCirrusConnected)
5498
+ .pipe(skip(1), switchMap((connected) => connected
5499
+ ? of('Creating Socket...')
5500
+ : of('Connection closed.').pipe(tap(() => nullify$.next(null)))));
5501
+ /**
5502
+ * An observable that emits the viewport visibility state.
5503
+ */
5504
+ const viewportTrigger$ = this.store.select(unrealFeature.selectViewportReady);
5505
+ /**
5506
+ * An observable that will nullify any message displayed after 500 mSec, when viewport changes its state.
5507
+ */
5508
+ const streamToggle$ = merge(viewportTrigger$.pipe(filter(Falsy), skip(1)), viewportTrigger$.pipe(filter(Truthy))).pipe(delay(500), map$1(() => null));
5509
+ /**
5510
+ * A subscription that merges various status observables and emits debounced status via postMessage.
5511
+ */
5512
+ merge(nullifyObs$, cirrusConnected$, signalingStatus$, streamToggle$)
5513
+ .pipe(tapLog('STATUS=>'), debounceTime$1(0), takeUntilDestroyed(this.destroyRef))
5514
+ .subscribe((status) => this.sendMessageToIndex(status));
5700
5515
  }
5701
5516
  /**
5702
- * Creates a client-initiated WebRTC offer and sends it to the server.
5703
- * When the client offers, it also creates the DataChannel.
5704
- * Used for peer reconnection through the existing signaling connection.
5517
+ * Sends a message to the embedded iframe identified by the default value '3dsource_start_screen' ID.
5518
+ * This function tries to post a message to the iframe's content window. If the content
5519
+ * window is not accessible (e.g., due to cross-origin policies), the function fails silently.
5520
+ *
5521
+ * @param {string} text The message to be sent to the iframe. This could be a status update
5522
+ * or any other relevant information that the iframe needs to process.
5705
5523
  */
5706
- async createWebRtcOffer() {
5707
- Logger.info('Creating client-initiated offer');
5524
+ sendMessageToIndex(text) {
5525
+ const objectElement = document.getElementById(this.unrealInitialConfig?.screenLockerContainerId ||
5526
+ SCREEN_LOCKER_CONTAINER_ID);
5708
5527
  try {
5709
- this.remoteDescriptionSet = false;
5710
- this.iceCandidateBuffer.length = 0;
5711
- if (!this.pcClient) {
5712
- this.setupPeerConnection(this.cfg);
5713
- }
5714
- // Client offers: transceivers first, then DC, then offer
5715
- await this.setupTransceiversAsync(this.pcClient);
5716
- this.dcClient = this.createDataChannel(this.pcClient, 'UnrealDcConnection', { ordered: true });
5717
- await this.createOffer(this.pcClient);
5528
+ const objectDocument = objectElement.contentWindow;
5529
+ objectDocument?.postMessage(text, '*');
5718
5530
  }
5719
- catch (e) {
5720
- Logger.error('Failed to create WebRTC offer', e);
5721
- this.store.dispatch(disconnectStream({
5722
- reason: DisconnectReason.WebRTCError,
5723
- message: 'WebRTCError: Offer Error',
5724
- }));
5531
+ catch {
5532
+ // Fails silently if the iframe's content window cannot be accessed
5725
5533
  }
5726
5534
  }
5727
- /**
5728
- * Initializes a new RTCPeerConnection with event handlers.
5729
- * Transceivers are NOT added here — the caller is responsible for adding
5730
- * them at the correct point depending on the negotiation flow.
5731
- */
5732
- setupPeerConnection(config) {
5733
- this.closePC();
5734
- Logger.log('Creating new RTCPeerConnection with config');
5735
- this.pcClient = new RTCPeerConnection(config);
5736
- this.commandTelemetryService.trackStartCommand('EXT-Browser ICE gathering');
5737
- this.commandTelemetryService.trackStartCommand('EXT-Browser ICE connection');
5738
- this.commandTelemetryService.trackStartCommand('EXT-Signaling state change');
5739
- this.handlePeerConnectionEvents(this.pcClient);
5740
- this.disconnect$.pipe(take(1)).subscribe(() => {
5741
- this.closePC();
5742
- });
5535
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: UnrealStatusComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
5536
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: UnrealStatusComponent, isStandalone: true, selector: "app-unreal-status", ngImport: i0, template: "@if (showLoader()) {\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 }); }
5537
+ }
5538
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: UnrealStatusComponent, decorators: [{
5539
+ type: Component,
5540
+ args: [{ selector: 'app-unreal-status', changeDetection: ChangeDetectionStrategy.OnPush, imports: [SourceLoadingComponent], template: "@if (showLoader()) {\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"] }]
5541
+ }] });
5542
+
5543
+ class VideoStatsComponent {
5544
+ constructor() {
5545
+ this.collapsed = signal(false, ...(ngDevMode ? [{ debugName: "collapsed" }] : []));
5546
+ this.store = inject(Store);
5547
+ this.videoService = inject(VideoService);
5548
+ this.isDevMode = inject(DevModeService).isDevMode;
5549
+ this.obs$ = this.videoService.videoStats$;
5550
+ this.viewportReady = this.store.selectSignal(unrealFeature.selectViewportReady);
5551
+ this.ssInfo = this.store.selectSignal(selectSignalingParameters);
5552
+ this.fpsTick = toSignal(this.obs$.pipe(map$1((stat) => ({
5553
+ value: stat.aggregatedStats.framesPerSecond || 0,
5554
+ }))));
5555
+ this.fpsAvgTick = toSignal(this.obs$.pipe(map$1((stat) => ({
5556
+ value: stat.aggregatedStats.avgFrameRate,
5557
+ }))));
5558
+ this.bitrateTick = toSignal(this.obs$.pipe(map$1((stat) => ({
5559
+ value: stat.aggregatedStats.bitrate,
5560
+ }))));
5561
+ this.videoQP = toSignal(this.obs$.pipe(map$1((stat) => ({
5562
+ value: stat.aggregatedStats.VideoEncoderQP,
5563
+ }))));
5564
+ this.bitrateAverageTick = toSignal(this.obs$.pipe(map$1((stat) => ({
5565
+ value: stat.aggregatedStats.avgBitrate,
5566
+ }))));
5567
+ this.videoStatus = toSignal(this.obs$.pipe(map$1((event) => this.isDevMode &&
5568
+ {
5569
+ ...event.aggregatedStats,
5570
+ quality: event.quality,
5571
+ }), filter(Boolean), map$1((data) => {
5572
+ return this.fields
5573
+ .map((key) => {
5574
+ return {
5575
+ key,
5576
+ value: data[key],
5577
+ };
5578
+ })
5579
+ .filter((r) => !this.elementsToShow.includes(r.key));
5580
+ })));
5581
+ this.graphList = [];
5582
+ this.idKey = 'com.source3d.unreal.stat';
5583
+ this.elementsToShow = [];
5584
+ this.trigger$ = new BehaviorSubject(undefined);
5585
+ this.getFields();
5743
5586
  }
5744
- createDataChannel(pc, label, options) {
5745
- const datachannel = pc.createDataChannel(label, options);
5746
- return this.setupDataChannelCallbacks(datachannel);
5587
+ ngOnInit() {
5588
+ this.fields = [
5589
+ 'kalmanBitrate',
5590
+ 'bitrateDrop',
5591
+ 'runTime',
5592
+ 'pixelRatio',
5593
+ 'frameHeight',
5594
+ 'frameWidth',
5595
+ ':-------------',
5596
+ 'jitter',
5597
+ 'MBytesReceived',
5598
+ 'framesDecoded',
5599
+ 'framesDropped',
5600
+ 'framesReceived',
5601
+ 'keyFramesDecoded',
5602
+ 'currentRoundTripTime',
5603
+ 'packetsLost',
5604
+ ':-------------',
5605
+ 'quality',
5606
+ 'videoCodec',
5607
+ 'kind',
5608
+ 'type',
5609
+ 'codecs',
5610
+ 'selectedPair',
5611
+ //'pliCount',
5612
+ //'sliCount',
5613
+ //'transportId',
5614
+ //'id',
5615
+ ];
5616
+ this.updateGraph();
5747
5617
  }
5748
- handleCandidateFromServer(iceCandidate) {
5749
- try {
5750
- const candidate = new RTCIceCandidate(iceCandidate);
5751
- Logger.info('[Unreal ICE candidate]', '| Type=', candidate.type, '| Protocol=', candidate.protocol, '| Address=', candidate.address, '| Port=', candidate.port, '|');
5752
- // If forcing TURN, reject any candidates not relay
5753
- if (this.forceTURN) {
5754
- if (candidate.candidate.indexOf('relay') < 0) {
5755
- Logger.warn('Dropping candidate because it was not TURN relay.', '| Type=', candidate.type, '| Protocol=', candidate.protocol, '| Address=', candidate.address, '| Port=', candidate.port, '|');
5756
- return;
5757
- }
5758
- }
5759
- this.pcClient?.addIceCandidate(candidate).catch((e) => {
5760
- Logger.warn('Failed to add ICE candidate', e);
5761
- });
5762
- }
5763
- catch (e) {
5764
- Logger.error('Error handleCandidateFromServer', e);
5765
- }
5618
+ updateGraph() {
5619
+ this.graphList = this.fields.map((key) => {
5620
+ const stat = this.obs$.pipe(map$1((val) => {
5621
+ const value = val.aggregatedStats[key];
5622
+ return { value };
5623
+ }));
5624
+ return {
5625
+ key,
5626
+ color: Math.floor(Math.random() * 16777216).toString(),
5627
+ stat,
5628
+ };
5629
+ });
5630
+ this.graphList$ = of(this.graphList).pipe(combineLatestWith(this.trigger$), map$1(([graphList]) => graphList.filter((r) => this.elementsToShow.includes(r.key))));
5766
5631
  }
5767
- /**
5768
- * Reorders the `m=video` line in the SDP so that payload types matching
5769
- * the given H.264 `profile-level-id` appear first, making the browser
5770
- * prefer that profile during codec negotiation.
5771
- *
5772
- * A `profile-level-id` is a 3-byte hex string (`XXYYZZ`) defined in
5773
- * RFC 6184 that encodes three H.264 parameters:
5774
- * - **Profile IDC** (`XX`) — the H.264 profile.
5775
- * - **Profile IOP** (`YY`) — constraint flags that further narrow the profile.
5776
- * - **Level IDC** (`ZZ`) — the level, which caps resolution/framerate/bitrate.
5777
- *
5778
- * ### H.264 profiles relevant to Unreal Engine Pixel Streaming
5779
- *
5780
- * | profile-level-id | Profile | Level | Notes |
5781
- * |------------------|----------------------------|-------|----------------------------------------------|
5782
- * | `42001f` | Baseline | 3.1 | No B-frames, low latency; legacy devices |
5783
- * | `42e01f` | Constrained Baseline | 3.1 | Widest HW decode support; **recommended** |
5784
- * | `4d001f` | Main | 3.1 | B-frames + CABAC; better compression |
5785
- * | `4d4028` | Main | 4.0 | Up to 1080p60; common desktop target |
5786
- * | `640028` | High | 4.0 | Best quality/compression; most GPUs support |
5787
- * | `64002a` | High | 4.2 | Up to 1080p120 or 4K30 |
5788
- * | `640033` | High | 5.1 | Up to 4K60; requires modern HW |
5789
- * | `640034` | High | 5.2 | Up to 4K120; high-end HW only |
5790
- * | `6e001f` | High 10 | 3.1 | 10-bit color; limited browser support |
5791
- * | `f4001f` | High 4:4:4 Predictive | 3.1 | Lossless capable; rare in browsers |
5792
- *
5793
- * UE Pixel Streaming defaults to Constrained Baseline (`42e01f`) or High (`6400xx`)
5794
- * depending on engine version and `WebRTC.H264Profile` CVar.
5795
- *
5796
- * We prioritize `42e01f` because it guarantees hardware decoding on
5797
- * virtually all client devices (desktop, mobile, embedded), avoiding
5798
- * fallback to software decoding or negotiation of a higher profile
5799
- * that the client GPU may not accelerate.
5800
- */
5801
- prioritizeH264Profile(sdp, profileLevelId) {
5802
- const lines = sdp.split('\r\n');
5803
- const targetPts = [];
5804
- for (const line of lines) {
5805
- if (line.startsWith('a=fmtp:') &&
5806
- line.toLowerCase().includes(`profile-level-id=${profileLevelId}`)) {
5807
- const pt = line.split(':')[1]?.split(' ')[0];
5808
- if (pt)
5809
- targetPts.push(pt);
5810
- }
5811
- }
5812
- if (targetPts.length === 0)
5813
- return sdp;
5814
- return lines
5815
- .map((line) => {
5816
- if (!line.startsWith('m=video'))
5817
- return line;
5818
- const parts = line.split(' ');
5819
- // parts: m=video <port> <proto> <pt1> <pt2> ...
5820
- const header = parts.slice(0, 3);
5821
- const pts = parts.slice(3);
5822
- const prioritized = [
5823
- ...targetPts.filter((pt) => pts.includes(pt)),
5824
- ...pts.filter((pt) => !targetPts.includes(pt)),
5825
- ];
5826
- return [...header, ...prioritized].join(' ');
5827
- })
5828
- .join('\r\n');
5632
+ toggle() {
5633
+ this.collapsed.update((collapsed) => !collapsed);
5634
+ this.writeFields();
5829
5635
  }
5830
- mungeSDP(sdp) {
5831
- let mungedSDP = this.prioritizeH264Profile(sdp, '42e01f');
5832
- mungedSDP = mungedSDP.replace(/(a=fmtp:\d+ .*level-asymmetry-allowed=.*)\r\n/gm, '$1;x-google-start-bitrate=10000;x-google-max-bitrate=100000\r\n');
5833
- // Set max bitrate to highest bitrate Opus supports
5834
- let audioSDP = 'maxaveragebitrate=510000;';
5835
- if (this.useMic) {
5836
- // Set the max capture rate to 48khz (so we can send high-quality audio from mic)
5837
- audioSDP += 'sprop-maxcapturerate=48000;';
5636
+ toggleGraph($event, el, value) {
5637
+ $event.stopPropagation();
5638
+ if ((value && typeof value !== 'number') ||
5639
+ (typeof value === 'number' && isNaN(value))) {
5640
+ return;
5641
+ }
5642
+ if (this.elementsToShow.includes(el)) {
5643
+ this.elementsToShow = this.elementsToShow.filter((item) => item !== el);
5838
5644
  }
5839
- // Force mono or stereo based on whether ?forceMono was passed or not
5840
- audioSDP += this.forceMonoAudio ? 'stereo=0;' : 'stereo=1;';
5841
- // Enable in-band forward error correction for opus audio
5842
- audioSDP += 'useinbandfec=1';
5843
- // We use the line 'useinbandfec=1' (which Opus uses) to set our Opus specific audio parameters.
5844
- mungedSDP = mungedSDP?.replace('useinbandfec=1', audioSDP);
5845
- return mungedSDP;
5645
+ else {
5646
+ this.elementsToShow.push(el);
5647
+ }
5648
+ this.writeFields();
5649
+ this.trigger$.next();
5846
5650
  }
5847
- /**
5848
- * Closes existing PeerConnection and DataChannel, resets negotiation state.
5849
- */
5850
- closePC() {
5851
- if (this.pcClient) {
5852
- Logger.warn('Closing existing PeerConnection');
5651
+ getFields() {
5652
+ try {
5653
+ const { elements, collapsed } = JSON.parse(localStorage.getItem(this.idKey)) || [];
5654
+ this.elementsToShow = elements || [];
5655
+ this.collapsed.set(collapsed);
5853
5656
  }
5854
- this.pcClient?.close();
5855
- this.pcClient = null;
5856
- if (this.dcClient) {
5857
- Logger.warn('Closing existing DataChannel');
5657
+ catch {
5658
+ this.elementsToShow = [];
5858
5659
  }
5859
- this.dcClient?.close();
5860
- this.dcClient = null;
5861
- this.remoteDescriptionSet = false;
5862
- this.iceCandidateBuffer.length = 0;
5863
5660
  }
5864
- canSend() {
5865
- return this.dcClient && this.dcClient.readyState === 'open';
5661
+ writeFields() {
5662
+ localStorage.setItem(this.idKey, JSON.stringify({
5663
+ collapsed: this.collapsed(),
5664
+ elements: this.elementsToShow,
5665
+ }));
5866
5666
  }
5867
- send(data) {
5868
- if (this.canSend()) {
5869
- this.dcClient?.send(data);
5667
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: VideoStatsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
5668
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", 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]=\"ssInfo() | 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 }); }
5669
+ }
5670
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: VideoStatsComponent, decorators: [{
5671
+ type: Component,
5672
+ 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]=\"ssInfo() | 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"] }]
5673
+ }], ctorParameters: () => [] });
5674
+
5675
+ class ResumeStreamButtonComponent {
5676
+ constructor() {
5677
+ this.actions$ = inject(Actions);
5678
+ this.store = inject(Store);
5679
+ this.videoService = inject(VideoService);
5680
+ this.destroyRef = inject(DestroyRef);
5681
+ this.showReconnectPopup = this.store.selectSignal(selectShowReconnectPopup);
5682
+ this.isLoaderScreenVisible = this.store.selectSignal(selectIsLoaderScreenVisible);
5683
+ this.streamConfig = this.store.selectSignal(selectStreamConfig);
5684
+ this.isExistMatchUrls = this.store.selectSignal(selectIsExistMatchUrls);
5685
+ this.isSecondStart = signal(!!(this.streamConfig()?.autoStart && location.href.match(/^https/gi)), ...(ngDevMode ? [{ debugName: "isSecondStart" }] : []));
5686
+ this.isShowResumeButton = computed(() => this.isLoaderScreenVisible() &&
5687
+ this.showReconnectPopup() &&
5688
+ this.isExistMatchUrls(), ...(ngDevMode ? [{ debugName: "isShowResumeButton" }] : []));
5689
+ }
5690
+ ngOnInit() {
5691
+ this.store
5692
+ .select(unrealFeature.selectCirrusConnected)
5693
+ .pipe(skip$1(1), filter(Falsy), takeUntilDestroyed(this.destroyRef))
5694
+ .subscribe(() => this.onDisconnect());
5695
+ }
5696
+ ngAfterViewInit() {
5697
+ this.actions$
5698
+ .pipe(ofType(videoStreamInitialized), takeUntilDestroyed(this.destroyRef), filter(() => !!this.videoService.video?.paused))
5699
+ .subscribe(() => {
5700
+ Logger.info('Simulate Press Play');
5701
+ this.videoService.play();
5702
+ this.store.dispatch(resetAfk());
5703
+ });
5704
+ }
5705
+ connect() {
5706
+ this.store.dispatch(startStream({ config: { autoStart: true } }));
5707
+ }
5708
+ onDisconnect() {
5709
+ this.isSecondStart.set(true);
5710
+ }
5711
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ResumeStreamButtonComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
5712
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: ResumeStreamButtonComponent, isStandalone: true, selector: "app-resume-stream-button", ngImport: i0, 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"], dependencies: [{ kind: "component", type: SourceButtonComponent, selector: "src-button", inputs: ["type", "appearance", "weight", "size", "context", "customClass", "isFullWidth", "isPressed", "isDisabled", "isLoading", "isInverted", "iconButton", "srcButtonConfig", "formID", "data-testid", "ariaLabel"], outputs: ["onClick", "onSubmit"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
5713
+ }
5714
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ResumeStreamButtonComponent, decorators: [{
5715
+ type: Component,
5716
+ 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"] }]
5717
+ }] });
5718
+
5719
+ class IntroSrcComponent {
5720
+ constructor() {
5721
+ this.videoElement = viewChild('videoPlayerCover', ...(ngDevMode ? [{ debugName: "videoElement" }] : []));
5722
+ this.store = inject(Store);
5723
+ this.imageIntroSrc = this.store.selectSignal(unrealFeature.selectImageIntroSrc);
5724
+ this.videoIntroSrc = this.store.selectSignal(unrealFeature.selectVideoIntroSrc);
5725
+ this.viewportReady = this.store.selectSignal(unrealFeature.selectViewportReady);
5726
+ effect(() => {
5727
+ const viewportReady = this.viewportReady();
5728
+ const videoIntroSrc = this.videoIntroSrc();
5729
+ const imageIntroSrc = this.imageIntroSrc();
5730
+ untracked(() => {
5731
+ if (viewportReady && (videoIntroSrc || imageIntroSrc)) {
5732
+ this.store.dispatch(resetIntroSrc());
5733
+ }
5734
+ });
5735
+ });
5736
+ effect(() => {
5737
+ const videoIntroSrc = this.videoIntroSrc();
5738
+ const videoElement = this.videoElement();
5739
+ if (!videoIntroSrc || !videoElement) {
5740
+ return;
5741
+ }
5742
+ untracked(() => this.createVideo(videoIntroSrc, videoElement.nativeElement));
5743
+ });
5744
+ }
5745
+ createVideo(src, videoContainer) {
5746
+ const matchAbsolutePath = !!src.match(/.*\.(mp4|WebM)(\?.*)?$/i);
5747
+ videoContainer.innerHTML = ''; // This will remove all existing content within the layout
5748
+ const videoElement = document.createElement('video');
5749
+ videoElement.autoplay = true;
5750
+ videoElement.muted = true;
5751
+ videoElement.loop = true;
5752
+ if (!matchAbsolutePath) {
5753
+ videoElement.poster = `${src}/cover.webp`;
5754
+ }
5755
+ videoElement.style.position = 'absolute';
5756
+ videoElement.style.width = '100%';
5757
+ videoElement.style.height = '100%';
5758
+ videoElement.style.objectFit = 'cover';
5759
+ const sourceElement = document.createElement('source');
5760
+ if (matchAbsolutePath) {
5761
+ sourceElement.src = src;
5870
5762
  }
5871
5763
  else {
5872
- Logger.colored('black', 'yellow', `DataChannel connection not prepared or in state: ${this.dcClient?.readyState}`);
5873
- // Trigger peer reconnection if DC is not open and we're not already reconnecting
5874
- this.tryReconnectOnSendFailure();
5764
+ sourceElement.src = `${src}/video.mp4`;
5875
5765
  }
5766
+ sourceElement.type = 'video/mp4';
5767
+ videoElement.appendChild(sourceElement);
5768
+ videoElement.innerHTML += 'Browser not supported';
5769
+ videoContainer.appendChild(videoElement);
5876
5770
  }
5877
- /**
5878
- * Attempts to reconnect the peer when send fails due to DataChannel not being open.
5879
- * This handles cases where the DC connection is broken but no error events are fired.
5880
- */
5881
- tryReconnectOnSendFailure() {
5882
- // Skip if already reconnecting or signaling is not connected
5883
- if (this.isReconnecting()) {
5884
- Logger.info('[Reconnect] Already reconnecting, skipping duplicate attempt');
5885
- return;
5886
- }
5887
- if (!this.signallingSrv.isConnected()) {
5888
- Logger.warn('[Reconnect] Cannot reconnect: Signaling not connected');
5889
- return;
5771
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: IntroSrcComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
5772
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", 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 [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 }); }
5773
+ }
5774
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: IntroSrcComponent, decorators: [{
5775
+ type: Component,
5776
+ args: [{ selector: 'app-intro-src', imports: [NgOptimizedImage], changeDetection: ChangeDetectionStrategy.OnPush, template: "@let image = imageIntroSrc();\n@if (image) {\n <div class=\"backImage\">\n <img [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"] }]
5777
+ }], ctorParameters: () => [], propDecorators: { videoElement: [{ type: i0.ViewChild, args: ['videoPlayerCover', { isSignal: true }] }] } });
5778
+
5779
+ class UnrealSceneComponent {
5780
+ get resizeValues() {
5781
+ if (!this.isStudio()) {
5782
+ return this.getResizeValuesUniversal();
5890
5783
  }
5891
- // Check if DC exists but is in a non-open state (closing, closed, or connecting stuck)
5892
- const dcState = this.dcClient?.readyState;
5893
- if (dcState && dcState !== 'open') {
5894
- Logger.warn(`[Reconnect] DataChannel in "${dcState}" state during send, initiating peer reconnection`);
5895
- this.store.dispatch(reconnectPeer({ attempt: 1, maxAttempts: 3, delay: 0 }));
5784
+ return this.getResizeValues();
5785
+ }
5786
+ get pixelRatio() {
5787
+ return window.devicePixelRatio;
5788
+ }
5789
+ onMouseOver() {
5790
+ this.changeMouseOverScene.emit(true);
5791
+ }
5792
+ onMouseOut() {
5793
+ this.changeMouseOverScene.emit(false);
5794
+ }
5795
+ constructor() {
5796
+ this.isStudio = input(false, ...(ngDevMode ? [{ debugName: "isStudio" }] : []));
5797
+ this.useContainerAsSizeProvider = input(true, ...(ngDevMode ? [{ debugName: "useContainerAsSizeProvider" }] : []));
5798
+ this.studioResolutionSize = input({
5799
+ width: FULL_HD_WIDTH,
5800
+ height: FULL_HD_HEIGHT,
5801
+ }, ...(ngDevMode ? [{ debugName: "studioResolutionSize" }] : []));
5802
+ this.studioResolutionValues = toObservable(computed(() => {
5803
+ const values = this.studioResolutionSize();
5804
+ return { width: values.width, height: values.height };
5805
+ }));
5806
+ this.videoElement = viewChild('videoContainer', ...(ngDevMode ? [{ debugName: "videoElement" }] : []));
5807
+ this.changeMouseOverScene = output();
5808
+ this.width = FULL_HD_WIDTH;
5809
+ this.height = FULL_HD_HEIGHT;
5810
+ this.actions$ = inject(Actions);
5811
+ this.store = inject(Store);
5812
+ this.isExistMatchUrls = this.store.selectSignal(selectIsExistMatchUrls);
5813
+ this.isDevMode = inject(DevModeService).isDevMode;
5814
+ this.lightMode = this.store.selectSignal(unrealFeature.selectLowBandwidth);
5815
+ this.isFreezeFrameLoading = this.store.selectSignal(selectIsFreezeFrameLoading);
5816
+ this.commandsSender = inject(UnrealCommunicatorService);
5817
+ this.videoService = inject(VideoService);
5818
+ this.element = inject(ElementRef);
5819
+ this.destroyRef = inject(DestroyRef);
5820
+ this.destroyRef.onDestroy(() => this.store.dispatch(destroyUnrealScene()));
5821
+ effect(() => {
5822
+ const videoElement = this.videoElement();
5823
+ const isExistMatchUrls = this.isExistMatchUrls();
5824
+ if (videoElement && isExistMatchUrls) {
5825
+ this.store.dispatch(initSignalling());
5826
+ }
5827
+ });
5828
+ afterNextRender({
5829
+ read: () => {
5830
+ const videoElement = this.videoElement();
5831
+ if (videoElement) {
5832
+ this.videoService.setContainer(videoElement.nativeElement);
5833
+ if (this.element?.nativeElement) {
5834
+ fromEvent(this.element?.nativeElement, 'contextmenu')
5835
+ .pipe(takeUntilDestroyed(this.destroyRef))
5836
+ .subscribe((event) => event.preventDefault());
5837
+ }
5838
+ this.listenResizeValues();
5839
+ }
5840
+ },
5841
+ });
5842
+ }
5843
+ listenResizeValues() {
5844
+ 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 }) => {
5845
+ this.width = width;
5846
+ this.height = height;
5847
+ }));
5848
+ const resizeRequired$ = merge(this.actions$.pipe(ofType(videoStreamInitialized)), fromEvent(window, 'resize'));
5849
+ const videoStreamSizeChanged$ = this.videoService?.videoStats$.pipe(filter(() => !this.isFreezeFrameLoading()), map$1((stats) => ({
5850
+ w: stats.aggregatedStats.frameWidth || 0,
5851
+ h: stats.aggregatedStats.frameHeight || 0,
5852
+ })), distinctUntilChanged((previous, current) => fpIsASameAsB(previous.w, current.w, SAME_SIZE_THRESHOLD) &&
5853
+ fpIsASameAsB(previous.h, current.h, SAME_SIZE_THRESHOLD)));
5854
+ merge(customSizeInputChanged$, resizeRequired$, videoStreamSizeChanged$)
5855
+ .pipe(debounceTime(DEBOUNCE_TO_MANY_RESIZE_CALLS), switchMap(() => {
5856
+ const key = Math.random();
5857
+ // Monitoring the size of the video stream
5858
+ return timer(0, 300).pipe(take(3), map$1(() => key));
5859
+ }), map$1((key) => ({ key, ...this.resizeValues })), distinctUntilChanged((previous, current) => fpIsASameAsB(previous.w, current.w, SAME_SIZE_THRESHOLD) &&
5860
+ fpIsASameAsB(previous.h, current.h, SAME_SIZE_THRESHOLD) &&
5861
+ previous.key === current.key), tap((size) => this.adaptVideo(size)), distinctUntilChanged((previous, current) => fpIsASameAsB(previous.w, current.w, SAME_SIZE_THRESHOLD) &&
5862
+ fpIsASameAsB(previous.h, current.h, SAME_SIZE_THRESHOLD)), combineLatestWith(this.store
5863
+ .select(unrealFeature.selectDataChannelConnected)
5864
+ .pipe(filter(Truthy))), takeUntilDestroyed(this.destroyRef))
5865
+ .subscribe(([size]) => {
5866
+ this.store.dispatch(changeStreamResolutionAction({ width: size.w, height: size.h }));
5867
+ this.sendResize(size);
5868
+ });
5869
+ this.actions$
5870
+ .pipe(ofType(forceResizeViewport), takeUntilDestroyed(this.destroyRef))
5871
+ .subscribe(() => this.sendResize(this.resizeValues));
5872
+ }
5873
+ adaptVideo(size) {
5874
+ const video = this.videoService.video;
5875
+ const areaSize = this.resizeValues;
5876
+ if (video?.style) {
5877
+ const pR = this.pixelRatio;
5878
+ video.width = this.makeEven(size.w / pR / areaSize.scale);
5879
+ video.height = this.makeEven(size.h / pR / areaSize.scale);
5880
+ if (!this.isStudio()) {
5881
+ video.style.overflow = 'hidden';
5882
+ video.style.objectFit = 'cover';
5883
+ video.style.width = `${video.width}px`;
5884
+ video.style.height = `${video.height}px`;
5885
+ }
5896
5886
  }
5887
+ this.store.dispatch(videoAdaptedToContainer());
5897
5888
  }
5898
5889
  /**
5899
- * Reconnects only the WebRTC peer connection without re-establishing signaling.
5900
- * Closes the current PeerConnection and creates a new client-initiated offer
5901
- * through the existing WebSocket connection.
5902
- * @returns true if reconnection was initiated, false if signaling is not connected
5890
+ * This method calculates the dimensions for resizing a layout in a configurator stream.
5891
+ * It takes into account the pixel ratio of the device's screen and the orientation of the layout.
5892
+ * The method uses the `fitIntoRectangle` function to calculate the dimensions that would allow the layout to fit into the maximum space while maintaining its aspect ratio.
5893
+ * It also sets the maximum limits for the width and height based on the layout's orientation.
5894
+ * If the scale of the fitted dimensions is less than 1, it assigns the fitted dimensions to the output. Otherwise, it sets the scale of the output to 1.
5895
+ * @returns {Area} An object of type `Area` that contains the calculated dimensions for resizing the layout.
5903
5896
  */
5904
- reconnectPeer() {
5905
- if (!this.signallingSrv.isConnected()) {
5906
- Logger.error('[Reconnect] Cannot reconnect peer: Signaling not connected');
5907
- return false;
5897
+ getResizeValuesUniversal() {
5898
+ const pR = this.pixelRatio;
5899
+ let containerW = window.innerWidth || 2;
5900
+ let containerH = window.innerHeight || 2;
5901
+ if (this.useContainerAsSizeProvider()) {
5902
+ containerW = this.videoElement()?.nativeElement.offsetWidth || 2;
5903
+ containerH = this.videoElement()?.nativeElement.offsetHeight || 2;
5908
5904
  }
5909
- if (!this.cfg || Object.keys(this.cfg).length === 0) {
5910
- Logger.error('[Reconnect] Cannot reconnect peer: No config available');
5911
- return false;
5905
+ const maxSpace = { w: containerW * pR, h: containerH * pR };
5906
+ const isVertical = containerW < containerH;
5907
+ let maxH = FULL_HD_HEIGHT;
5908
+ let maxW = FULL_HD_WIDTH;
5909
+ const params = new URLSearchParams(location.search);
5910
+ const maxRes = params.get('maxresolution');
5911
+ if (Truthy(maxRes)) {
5912
+ maxH = FULL_HD_HEIGHT * 2;
5913
+ maxW = FULL_HD_WIDTH * 2;
5912
5914
  }
5913
- Logger.info('[Reconnect] Initiating peer reconnection');
5914
- // Close only the PeerConnection, keep WebSocket alive
5915
- this.closePC();
5916
- // Create a new WebRTC offer using the stored config
5917
- void this.createWebRtcOffer();
5918
- return true;
5919
- }
5920
- setConfig(parOptions = {}) {
5921
- const urlParams = new URLSearchParams(window.location.search);
5922
- //**********************
5923
- //Config setup
5924
- //**********************
5925
- this.cfg = { ...parOptions.peerConnectionOptions };
5926
- this.forceTURN = urlParams.has('ForceTURN');
5927
- if (this.forceTURN) {
5928
- Logger.info('Forcing TURN usage by setting ICE Transport Policy in peer connection config.');
5929
- this.cfg.iceTransportPolicy = 'relay';
5915
+ const maxLimits = isVertical
5916
+ ? { w: maxH, h: maxW, x: 0, y: 0 }
5917
+ : { w: maxW, h: maxH, x: 0, y: 0 };
5918
+ const props = fitIntoRectangle({ x: 0, y: 0, w: containerW, h: containerH }, { x: 0, y: 0, ...maxSpace });
5919
+ let out = { ...props };
5920
+ const fitIntoMax = fitIntoRectangle(props, maxLimits);
5921
+ if (fitIntoMax.scale < 1) {
5922
+ // If reducing size to 1920x1080 is required, do it
5923
+ out = fitIntoMax;
5930
5924
  }
5931
- this.cfg.bundlePolicy = urlParams.has('ForceMaxBundle')
5932
- ? 'max-bundle'
5933
- : 'balanced';
5934
- this.cfg.iceCandidatePoolSize = 1;
5935
- // When ?useMic check for SSL or localhost
5936
- const isLocalhostConnection = location.hostname === 'localhost' || location.hostname === '127.0.0.1';
5937
- const isHttpsConnection = location.protocol === 'https:';
5938
- if (this.useMic && !isLocalhostConnection && !isHttpsConnection) {
5939
- this.useMic = false;
5940
- Logger.error('Microphone access in the browser will not work if you are not on HTTPS or localhost. Disabling mic access.');
5941
- Logger.error("For testing you can enable HTTP microphone access Chrome by visiting chrome://flags/ and enabling 'unsafely-treat-insecure-origin-as-secure'");
5925
+ else {
5926
+ // Otherwise, use the lowest resolution because the layout size is smaller than 1920x1080
5927
+ out.scale = 1;
5942
5928
  }
5943
- this.setupPeerConnection(this.cfg);
5929
+ /**
5930
+ * Ensure that the size is always even.
5931
+ */
5932
+ out.w = this.makeEven(out.w);
5933
+ out.h = this.makeEven(out.h);
5934
+ return { ...out };
5944
5935
  }
5945
- handlePeerConnectionEvents(pcClient) {
5946
- fromEvent(pcClient, 'signalingstatechange')
5947
- .pipe(map$1(() => this.pcClient?.signalingState), takeUntil(this.disconnect$))
5948
- .subscribe((state) => {
5949
- Logger.info('[Signaling state change] |', state, '|');
5950
- this.commandTelemetryService.trackStopCommand('EXT-Signaling state change', true, { state, multi: true });
5951
- });
5952
- fromEvent(pcClient, 'iceconnectionstatechange')
5953
- .pipe(map$1(() => this.pcClient?.iceConnectionState), takeUntil(this.disconnect$))
5954
- .subscribe((state) => {
5955
- Logger.info('[Browser ICE connection] |', state, '|');
5956
- this.commandTelemetryService.trackStopCommand('EXT-Browser ICE connection', true, { state, multi: true });
5957
- });
5958
- fromEvent(pcClient, 'icegatheringstatechange')
5959
- .pipe(map$1(() => this.pcClient?.iceGatheringState), takeUntil(this.disconnect$))
5960
- .subscribe((state) => {
5961
- Logger.info('[Browser ICE gathering] |', state, '|');
5962
- this.commandTelemetryService.trackStopCommand('EXT-Browser ICE gathering', true, { state, multi: true });
5963
- });
5964
- fromEvent(pcClient, 'connectionstatechange')
5965
- .pipe(map$1(() => this.pcClient?.connectionState), takeUntil(this.disconnect$))
5966
- .subscribe((state) => {
5967
- Logger.info('[RTCPeerConnection state] |', state, '|');
5968
- });
5969
- fromEvent(pcClient, 'icecandidate')
5970
- .pipe(map$1((e) => e.candidate), filter(Truthy), tap$1((candidate) => Logger.info('[Browser ICE candidate]', '| Type=', candidate.type, '| Protocol=', candidate.protocol, '| Address=', candidate.address, '| Port=', candidate.port, '|')), takeUntil(this.disconnect$))
5971
- .subscribe((candidate) => this.signallingSrv.send({ type: 'iceCandidate', candidate }));
5972
- // When the server creates the DataChannel (server-offer flow),
5973
- // we receive it here via the 'datachannel' event
5974
- fromEvent(pcClient, 'datachannel')
5975
- .pipe(tap$1((e) => Logger.info(`[DATACHANNEL] Received remote DC: label="${e.channel.label}" id=${e.channel.id} protocol="${e.channel.protocol}" ordered=${e.channel.ordered}`)), map$1((e) => e.channel), takeUntil(this.disconnect$))
5976
- .subscribe((datachannel) => {
5977
- this.dcClient = this.setupDataChannelCallbacks(datachannel);
5936
+ getResizeValues() {
5937
+ const pR = this.pixelRatio;
5938
+ const containerW = pR * (this.videoElement()?.nativeElement.offsetWidth || 2);
5939
+ const containerH = pR * (this.videoElement()?.nativeElement.offsetHeight || 2);
5940
+ const maxSpace = { w: containerW, h: containerH };
5941
+ const videoSizePixelPerfect = fitIntoRectangle({ x: 0, y: 0, w: this.width, h: this.height }, { x: 0, y: 0, ...maxSpace });
5942
+ const out = fitIntoRectangle(videoSizePixelPerfect, {
5943
+ x: 0,
5944
+ y: 0,
5945
+ w: Math.min(containerW, FULL_HD_WIDTH),
5946
+ h: Math.min(containerH, FULL_HD_HEIGHT),
5978
5947
  });
5979
- const onTrack$ = fromEvent(pcClient, 'track').pipe(takeUntil(this.disconnect$), filter((e) => !!e.track), tap$1((e) => Logger.info('Got track - ' +
5980
- e.track.kind +
5981
- ' id=' +
5982
- e.track.id +
5983
- ' readyState=' +
5984
- e.track.readyState)), share());
5985
- onTrack$
5986
- .pipe(filter((e) => e.track.kind === 'audio'), map$1((e) => e.streams[0]))
5987
- .subscribe((stream) => this.videoService.attachAudioStream(stream));
5988
- onTrack$
5989
- .pipe(filter((e) => e.track.kind === 'video'), map$1((e) => e.streams[0]), tap$1(() => Logger.info('Set video source from video track ontrack.')))
5990
- .subscribe((stream) => {
5991
- if (this.pcClient) {
5992
- this.videoService.attachVideoStream(stream, this.pcClient);
5993
- }
5948
+ /**
5949
+ * Ensure that the size is always even.
5950
+ */
5951
+ out.w = this.makeEven(out.w);
5952
+ out.h = this.makeEven(out.h);
5953
+ return out;
5954
+ }
5955
+ sendResize(size) {
5956
+ this.commandsSender.sendCommandToUnreal({
5957
+ command: MetaBoxCommand.FChangeResolutionCommand,
5958
+ payload: { resolution: { x: size.w, y: size.h } },
5994
5959
  });
5995
5960
  }
5996
- async setupTransceiversAsync(pc) {
5997
- const hasTransceivers = pc.getTransceivers().length > 0;
5998
- // Set up a transceiver for getting UE video
5999
- pc.addTransceiver('video', { direction: 'recvonly' });
6000
- // Set up a transceiver for sending mic audio to UE and receiving audio from UE
6001
- if (!this.useMic) {
6002
- pc.addTransceiver('audio', { direction: 'recvonly' });
6003
- }
6004
- else {
6005
- const audioSendOptions = this.useMic
6006
- ? {
6007
- autoGainControl: false,
6008
- channelCount: 1,
6009
- echoCancellation: false,
6010
- latency: 0,
6011
- noiseSuppression: false,
6012
- sampleRate: 48000,
6013
- sampleSize: 16,
6014
- volume: 1.0,
6015
- }
6016
- : false;
6017
- // Note using mic on android chrome requires SSL or chrome://flags/ "unsafely-treat-insecure-origin-as-secure"
6018
- const stream = await navigator.mediaDevices.getUserMedia({
6019
- video: false,
6020
- audio: audioSendOptions,
6021
- });
6022
- if (stream) {
6023
- if (hasTransceivers) {
6024
- for (const transceiver of pc.getTransceivers()) {
6025
- if (transceiver &&
6026
- transceiver.receiver &&
6027
- transceiver.receiver.track &&
6028
- transceiver.receiver.track.kind === 'audio') {
6029
- for (const track of stream.getTracks()) {
6030
- if (track.kind && track.kind === 'audio') {
6031
- await transceiver.sender.replaceTrack(track);
6032
- transceiver.direction = 'sendrecv';
6033
- }
6034
- }
6035
- }
6036
- }
6037
- }
6038
- else {
6039
- for (const track of stream.getTracks()) {
6040
- if (track.kind && track.kind === 'audio') {
6041
- pc.addTransceiver(track, { direction: 'sendrecv' });
6042
- }
6043
- }
6044
- }
6045
- }
6046
- else {
6047
- pc.addTransceiver('audio', { direction: 'recvonly' });
5961
+ makeEven(value) {
5962
+ return Math.floor((value + 1) / 2) * 2;
5963
+ }
5964
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: UnrealSceneComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
5965
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", 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 [class.lightModeCursor]=\"lightMode()\" 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 <ng-content select=\"app-low-bandwidth-detector\"></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 }); }
5966
+ }
5967
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: UnrealSceneComponent, decorators: [{
5968
+ type: Component,
5969
+ args: [{ selector: 'app-unreal-scene', changeDetection: ChangeDetectionStrategy.OnPush, imports: [
5970
+ AfkTimeoutModalComponent,
5971
+ UnrealStatusComponent,
5972
+ FreezeFrameComponent,
5973
+ VideoStatsComponent,
5974
+ ResumeStreamButtonComponent,
5975
+ ImageLoadingSrcComponent,
5976
+ IntroSrcComponent,
5977
+ ], template: "<div class=\"frame unreal-container\">\n <div #videoContainer [class.lightModeCursor]=\"lightMode()\" 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 <ng-content select=\"app-low-bandwidth-detector\"></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"] }]
5978
+ }], 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: [{
5979
+ type: HostListener,
5980
+ args: ['mouseover']
5981
+ }], onMouseOut: [{
5982
+ type: HostListener,
5983
+ args: ['mouseout']
5984
+ }] } });
5985
+
5986
+ class WebrtcErrorModalComponent {
5987
+ #dialogRef = inject(DialogRef);
5988
+ #store = inject(Store);
5989
+ close() {
5990
+ this.#dialogRef.close();
5991
+ }
5992
+ closeModalWithCirrusDisconnect() {
5993
+ this.#store
5994
+ .select(unrealFeature.selectCirrusConnected)
5995
+ .pipe(take(1))
5996
+ .subscribe((isConnected) => {
5997
+ if (!isConnected) {
5998
+ this.#store.dispatch(disconnectStream({
5999
+ reason: DisconnectReason.DropConnection,
6000
+ message: 'Modal Drop Connection',
6001
+ }));
6048
6002
  }
6003
+ this.close();
6004
+ });
6005
+ }
6006
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: WebrtcErrorModalComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
6007
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.0", 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 ::ng-deep .ng-scroll-content{--_scrollbar-content-width: initial;--_viewport-padding-right: 12px}.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", "iconButton", "srcButtonConfig", "formID", "data-testid", "ariaLabel"], outputs: ["onClick", "onSubmit"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
6008
+ }
6009
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: WebrtcErrorModalComponent, decorators: [{
6010
+ type: Component,
6011
+ args: [{ selector: 'app-webrtc-error-modal', changeDetection: ChangeDetectionStrategy.OnPush, imports: [SourceButtonComponent], 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 ::ng-deep .ng-scroll-content{--_scrollbar-content-width: initial;--_viewport-padding-right: 12px}.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"] }]
6012
+ }] });
6013
+
6014
+ class FilterSettingsComponent {
6015
+ constructor() {
6016
+ this.store = inject(Store);
6017
+ this.storageKey = 'LBMFilterSettings';
6018
+ this.defaultFilterModel = { ...DefaultFilterModel };
6019
+ this.settings = { ...DefaultFilterModel };
6020
+ }
6021
+ ngOnInit() {
6022
+ const savedSettings = localStorage.getItem(this.storageKey);
6023
+ if (savedSettings) {
6024
+ this.dispatch(JSON.parse(savedSettings));
6049
6025
  }
6050
6026
  }
6051
- setupDataChannelCallbacks(datachannel) {
6027
+ togglePanel() {
6028
+ this.settings.panelOpen = !this.settings.panelOpen;
6029
+ this.saveSettings();
6030
+ }
6031
+ saveSettings() {
6032
+ const valid = Object.entries(this.settings).reduce((acc, [key, value]) => {
6033
+ return Truthy(value) ? { ...acc, [key]: value } : acc;
6034
+ }, {});
6035
+ this.dispatch({ ...DefaultFilterModel, ...valid });
6036
+ }
6037
+ dispatch(settings) {
6052
6038
  try {
6053
- Logger.log(`[DATACHANNEL] Setting up datachannel "${datachannel.label}" callbacks...`);
6054
- this.commandTelemetryService.trackStartCommand('EXT-DataChannel Connection');
6055
- // Inform browser we would like binary data as an ArrayBuffer (FF chooses Blob by default!)
6056
- datachannel.binaryType = 'arraybuffer';
6057
- fromEvent(datachannel, 'close')
6058
- .pipe(
6059
- // Only dispatch disconnect if not already reconnecting to avoid infinite loops
6060
- filter(() => !this.isReconnecting()), take(1))
6061
- .subscribe((e) => {
6062
- Logger.warn(`[DATACHANNEL] Data channel disconnected: ${datachannel.label}(${datachannel.id})`, e);
6063
- this.store.dispatch(disconnectStream({
6064
- reason: DisconnectReason.DataChannelClosed,
6065
- message: 'DataChannelClosed',
6066
- }));
6067
- });
6068
- fromEvent(datachannel, 'error')
6069
- .pipe(takeUntil(this.disconnect$))
6070
- .subscribe((e) => {
6071
- const message = `[DATACHANNEL] Data channel "${datachannel.label}" error. id:${datachannel.id}`;
6072
- Logger.error(message, e);
6073
- });
6074
- fromEvent(datachannel, 'open')
6075
- .pipe(tap$1(() => {
6076
- Logger.info(`[DATACHANNEL] Data channel "${datachannel.label}" connected. id:${datachannel.id}`);
6077
- }), takeUntil(this.disconnect$))
6078
- .subscribe(() => {
6079
- this.commandTelemetryService.trackStopCommand('EXT-DataChannel Connection');
6080
- this.onDataChannelConnected();
6081
- });
6082
- fromEvent(datachannel, 'message')
6083
- .pipe(
6084
- /* tap((e: MessageEvent) => {
6085
- const view = new Uint8Array(e.data);
6086
- Logger.info(
6087
- `[DATACHANNEL] Message received on "${datachannel.label}" id=${datachannel.id}, byteCode=${view[0]}, size=${view.byteLength}`,
6088
- );
6089
- }),*/
6090
- map$1((e) => e.data), takeUntil(this.disconnect$))
6091
- .subscribe((data) => this.onDataChannelMessage$.next(data));
6092
- return datachannel;
6039
+ localStorage.setItem(this.storageKey, JSON.stringify(settings));
6093
6040
  }
6094
6041
  catch (e) {
6095
- Logger.warn('[DATACHANNEL] Datachannel setup caused an exception: ', e);
6096
- return null;
6042
+ Logger.error('Storage not available, Error saving filter settings:', e);
6097
6043
  }
6044
+ Object.assign(this.settings, settings);
6045
+ Object.assign(FilterModel, settings);
6046
+ this.store.dispatch(setKalmanSettings({ settings }));
6098
6047
  }
6099
- onDataChannelConnected() {
6100
- let statusMessage = 'Session connected, video stream starting.';
6101
- if (navigator.userAgent.includes('Firefox')) {
6102
- statusMessage +=
6103
- '\n If you are experiencing connection problems please try Google Chrome';
6104
- }
6105
- this.store.dispatch(dataChannelConnected({ statusMessage }));
6048
+ reset() {
6049
+ this.dispatch({
6050
+ ...DefaultFilterModel,
6051
+ panelOpen: this.settings.panelOpen,
6052
+ });
6106
6053
  }
6107
- /**
6108
- * Creates an SDP offer, applies SDP munging, sets it as local description,
6109
- * and sends it to the server via signaling.
6110
- * Transceivers already configure media directions, so deprecated
6111
- * offerToReceive* options are not used.
6112
- */
6113
- async createOffer(pcClient) {
6114
- try {
6115
- const offer = await pcClient.createOffer();
6116
- offer.sdp = this.mungeSDP(offer.sdp ?? '');
6117
- await pcClient.setLocalDescription(offer);
6118
- this.signallingSrv.send(offer);
6119
- }
6120
- catch {
6121
- Logger.error("Couldn't create offer");
6122
- }
6054
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: FilterSettingsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
6055
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: FilterSettingsComponent, isStandalone: true, selector: "app-filter-settings", ngImport: i0, template: "<div class=\"settings-container\">\n <button (click)=\"togglePanel()\" class=\"gear-button\">LBM</button>\n @if (settings.panelOpen) {\n <div class=\"settings-panel\">\n <h3>LBM Trigger Settings</h3>\n <h5>Data Flow Monitor</h5>\n <form>\n <label>\n Monitoring delay:\n <input\n [(ngModel)]=\"settings.monitoringDelayTime\"\n [placeholder]=\"defaultFilterModel.monitoringDelayTime\"\n type=\"number\"\n name=\"monitoringDelayTime\"\n />\n </label>\n\n <label>\n Minimum Bitrate (kbit/s):\n <input\n [(ngModel)]=\"settings.minimumBitrate\"\n [placeholder]=\"defaultFilterModel.minimumBitrate\"\n type=\"number\"\n name=\"minimumBitrate\"\n />\n </label>\n <label>\n Yellow Flag (%):\n <input\n [(ngModel)]=\"settings.yellowFlag\"\n [placeholder]=\"defaultFilterModel.yellowFlag\"\n type=\"number\"\n name=\"yellowFlag\"\n />\n </label>\n <label>\n Red Flag (%):\n <input\n [(ngModel)]=\"settings.redFlag\"\n [placeholder]=\"defaultFilterModel.redFlag\"\n type=\"number\"\n name=\"redFlag\"\n />\n </label>\n <label>\n Minimum FPS:\n <input\n [(ngModel)]=\"settings.minimumFps\"\n [placeholder]=\"defaultFilterModel.minimumFps\"\n type=\"number\"\n name=\"minimumFps\"\n />\n </label>\n <hr />\n <h5>Kalman Filter</h5>\n <label>\n Initial Bitrate Estimate (kbit/s):\n <input\n [(ngModel)]=\"settings.initialBitrateEstimate\"\n [placeholder]=\"defaultFilterModel.initialBitrateEstimate\"\n type=\"number\"\n name=\"initialBitrateEstimate\"\n />\n </label>\n <label>\n Initial Error Covariance:\n <input\n [(ngModel)]=\"settings.initialErrorCovariance\"\n [placeholder]=\"defaultFilterModel.initialErrorCovariance\"\n type=\"number\"\n name=\"initialErrorCovariance\"\n />\n </label>\n <label>\n Process Noise (Q):\n <input\n [(ngModel)]=\"settings.processNoise\"\n [placeholder]=\"defaultFilterModel.processNoise\"\n type=\"number\"\n name=\"processNoise\"\n />\n </label>\n <label>\n Measurement Noise (R):\n <input\n [(ngModel)]=\"settings.measurementNoise\"\n [placeholder]=\"defaultFilterModel.measurementNoise\"\n type=\"number\"\n name=\"measurementNoise\"\n />\n </label>\n </form>\n <br />\n <br />\n <div class=\"apply-button-container\">\n <button (click)=\"reset()\">Reset</button>\n <button (click)=\"saveSettings()\">Apply</button>\n </div>\n </div>\n }\n</div>\n", styles: [".settings-container{position:absolute;top:65px;right:10px;z-index:1000}.gear-button{right:0;width:50px;position:absolute;cursor:pointer;font-size:10px;background:none;font-weight:500;border:1px solid;border-radius:9px;background:#0162cc;color:#fff;padding:4px}.gear-button>*{color:#e6f419;filter:drop-shadow(1px 1px 1px rgb(0,0,0))}.settings-panel{font-size:13px;background:#fff;border:1px solid #ccc;padding:15px;margin-top:25px;border-radius:4px;box-shadow:0 2px 8px #0003}.settings-panel h3{margin-top:0}.settings-panel form{display:flex;flex-direction:column}.settings-panel label{margin-bottom:10px;display:flex;flex-direction:column}.settings-panel input{padding:4px;margin-top:4px;border:1px solid #ccc;border-radius:2px}.apply-button-container{position:absolute;bottom:15px;right:15px}.apply-button-container button{padding:6px 12px;border:none;background-color:#1976d2;color:#fff;border-radius:4px;cursor:pointer;margin:2px}.apply-button-container button:hover{background-color:#1565c0}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i1.NgForm, selector: "form:not([ngNoForm]):not([formGroup]):not([formArray]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
6056
+ }
6057
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: FilterSettingsComponent, decorators: [{
6058
+ type: Component,
6059
+ args: [{ selector: 'app-filter-settings', imports: [FormsModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"settings-container\">\n <button (click)=\"togglePanel()\" class=\"gear-button\">LBM</button>\n @if (settings.panelOpen) {\n <div class=\"settings-panel\">\n <h3>LBM Trigger Settings</h3>\n <h5>Data Flow Monitor</h5>\n <form>\n <label>\n Monitoring delay:\n <input\n [(ngModel)]=\"settings.monitoringDelayTime\"\n [placeholder]=\"defaultFilterModel.monitoringDelayTime\"\n type=\"number\"\n name=\"monitoringDelayTime\"\n />\n </label>\n\n <label>\n Minimum Bitrate (kbit/s):\n <input\n [(ngModel)]=\"settings.minimumBitrate\"\n [placeholder]=\"defaultFilterModel.minimumBitrate\"\n type=\"number\"\n name=\"minimumBitrate\"\n />\n </label>\n <label>\n Yellow Flag (%):\n <input\n [(ngModel)]=\"settings.yellowFlag\"\n [placeholder]=\"defaultFilterModel.yellowFlag\"\n type=\"number\"\n name=\"yellowFlag\"\n />\n </label>\n <label>\n Red Flag (%):\n <input\n [(ngModel)]=\"settings.redFlag\"\n [placeholder]=\"defaultFilterModel.redFlag\"\n type=\"number\"\n name=\"redFlag\"\n />\n </label>\n <label>\n Minimum FPS:\n <input\n [(ngModel)]=\"settings.minimumFps\"\n [placeholder]=\"defaultFilterModel.minimumFps\"\n type=\"number\"\n name=\"minimumFps\"\n />\n </label>\n <hr />\n <h5>Kalman Filter</h5>\n <label>\n Initial Bitrate Estimate (kbit/s):\n <input\n [(ngModel)]=\"settings.initialBitrateEstimate\"\n [placeholder]=\"defaultFilterModel.initialBitrateEstimate\"\n type=\"number\"\n name=\"initialBitrateEstimate\"\n />\n </label>\n <label>\n Initial Error Covariance:\n <input\n [(ngModel)]=\"settings.initialErrorCovariance\"\n [placeholder]=\"defaultFilterModel.initialErrorCovariance\"\n type=\"number\"\n name=\"initialErrorCovariance\"\n />\n </label>\n <label>\n Process Noise (Q):\n <input\n [(ngModel)]=\"settings.processNoise\"\n [placeholder]=\"defaultFilterModel.processNoise\"\n type=\"number\"\n name=\"processNoise\"\n />\n </label>\n <label>\n Measurement Noise (R):\n <input\n [(ngModel)]=\"settings.measurementNoise\"\n [placeholder]=\"defaultFilterModel.measurementNoise\"\n type=\"number\"\n name=\"measurementNoise\"\n />\n </label>\n </form>\n <br />\n <br />\n <div class=\"apply-button-container\">\n <button (click)=\"reset()\">Reset</button>\n <button (click)=\"saveSettings()\">Apply</button>\n </div>\n </div>\n }\n</div>\n", styles: [".settings-container{position:absolute;top:65px;right:10px;z-index:1000}.gear-button{right:0;width:50px;position:absolute;cursor:pointer;font-size:10px;background:none;font-weight:500;border:1px solid;border-radius:9px;background:#0162cc;color:#fff;padding:4px}.gear-button>*{color:#e6f419;filter:drop-shadow(1px 1px 1px rgb(0,0,0))}.settings-panel{font-size:13px;background:#fff;border:1px solid #ccc;padding:15px;margin-top:25px;border-radius:4px;box-shadow:0 2px 8px #0003}.settings-panel h3{margin-top:0}.settings-panel form{display:flex;flex-direction:column}.settings-panel label{margin-bottom:10px;display:flex;flex-direction:column}.settings-panel input{padding:4px;margin-top:4px;border:1px solid #ccc;border-radius:2px}.apply-button-container{position:absolute;bottom:15px;right:15px}.apply-button-container button{padding:6px 12px;border:none;background-color:#1976d2;color:#fff;border-radius:4px;cursor:pointer;margin:2px}.apply-button-container button:hover{background-color:#1565c0}\n"] }]
6060
+ }] });
6061
+
6062
+ class LowBandwidthDetectorComponent {
6063
+ constructor() {
6064
+ this.store = inject(Store);
6065
+ this.dialog = inject(Dialog);
6066
+ this.destroyRef = inject(DestroyRef);
6067
+ this.videoService = inject(VideoService);
6068
+ this.isDevMode = inject(DevModeService).isDevMode;
6069
+ this.viewportReady$ = this.store.select(unrealFeature.selectViewportReady);
6070
+ this.isLowBandwidth$ = this.store.select(unrealFeature.selectLowBandwidth);
6071
+ this.lowBandwidthTrigger$ = this.isLowBandwidth$.pipe(distinctUntilChanged$1(), filter(Truthy));
6072
+ this.highBandwidthTrigger$ = this.isLowBandwidth$.pipe(distinctUntilChanged$1(), filter(Falsy));
6073
+ this.viewPortDestroyedTrigger$ = this.viewportReady$.pipe(distinctUntilChanged$1(), filter(Falsy));
6074
+ this.aggregatedStats$ = this.videoService.videoStats$.pipe(map((data) => data.aggregatedStats), share());
6075
+ this.nativeQuality$ = this.videoService.videoStats$.pipe(map((data) => data.quality));
6076
+ this.monitoringTrigger$ = this.viewportReady$
6077
+ .pipe(combineLatestWith(this.isLowBandwidth$), map(([viewPortReady, isLowBandwidth]) => viewPortReady && !isLowBandwidth))
6078
+ .pipe(share());
6079
+ this.reTrigger$ = this.monitoringTrigger$.pipe(map(() => ({ quality: 'lime' })));
6080
+ this.canSwitchToLowBand$ = this.monitoringTrigger$.pipe(debounceTime(100), switchMap((canSwitch) => concat(of(false), timer(FilterModel.monitoringDelayTime).pipe(map(() => canSwitch))).pipe(take(2), takeUntil$1(this.monitoringTrigger$))), distinctUntilChanged$1(), share());
6081
+ this.videoQuality$ = merge(this.nativeQuality$, this.reTrigger$).pipe(share(), takeUntil$1(this.viewPortDestroyedTrigger$));
6082
+ this.fps$ = this.aggregatedStats$.pipe(map((data) => data.framesPerSecond || 0));
6083
+ this.bitrateDrop$ = this.aggregatedStats$.pipe(map((data) => data.dataFlowCheckResult), filter((br) => br.isDropDetected), takeUntil$1(merge(this.viewPortDestroyedTrigger$, this.reTrigger$)));
6084
+ this.fps = toSignal(this.fps$, { initialValue: 0 });
6085
+ this.nativeQuality = toSignal(this.nativeQuality$);
6086
+ this.isLowBandwidth = toSignal(this.isLowBandwidth$);
6087
+ this.aggregatedStats = toSignal(this.aggregatedStats$);
6088
+ this.isReducedQuality = signal(false, ...(ngDevMode ? [{ debugName: "isReducedQuality" }] : []));
6089
+ this.isIndicatorExpanded = signal(false, ...(ngDevMode ? [{ debugName: "isIndicatorExpanded" }] : []));
6090
+ this.isIndicatorExpandedTrigger$ = toObservable(this.isIndicatorExpanded).pipe(skip(1), distinctUntilChanged$1(), filter(Falsy));
6123
6091
  }
6124
- /**
6125
- * Creates an SDP answer, applies SDP munging, sets it as local description,
6126
- * and sends it to the server via signaling.
6127
- */
6128
- async createAnswer(pcClient) {
6129
- try {
6130
- const answer = await pcClient.createAnswer();
6131
- answer.sdp = this.mungeSDP(answer.sdp ?? '');
6132
- await pcClient.setLocalDescription(answer);
6133
- this.signallingSrv.send(answer);
6092
+ ngOnInit() {
6093
+ this.lowBandwidthTrigger$
6094
+ .pipe(takeUntilDestroyed(this.destroyRef))
6095
+ .subscribe(() => this.toggleIndicator(true));
6096
+ merge(this.viewPortDestroyedTrigger$, this.highBandwidthTrigger$)
6097
+ .pipe(takeUntilDestroyed(this.destroyRef))
6098
+ .subscribe(() => this.isReducedQuality.set(false));
6099
+ this.canSwitchToLowBand$
6100
+ .pipe(filter(Truthy), tap(() => {
6101
+ BITRATE_MONITOR.reset();
6102
+ this.isReducedQuality.set(false);
6103
+ }), switchMap(() => this.bitrateDrop$), tap(this.trySetLowBandwidth.bind(this)), map((dataFlowResult) => dataFlowResult?.isDropDetected), distinctUntilChanged$1(), tap(() => {
6104
+ this.isReducedQuality.set(true);
6105
+ }), switchMap(() => this.videoQuality$), filter((quality) => quality === 'lime'), tap(() => this.isReducedQuality.set(false)), takeUntilDestroyed(this.destroyRef))
6106
+ .subscribe();
6107
+ }
6108
+ trySetLowBandwidth(dataFlowResult) {
6109
+ // Evaluate FPS and native quality
6110
+ const isFpsLow = this.fps() < FilterModel.minimumFps;
6111
+ const isNativeQualityRed = this.nativeQuality() === 'red';
6112
+ const shouldSwitch = isNativeQualityRed || isFpsLow;
6113
+ // Exit if conditions are not met or already in low bandwidth
6114
+ if (!shouldSwitch || this.isLowBandwidth())
6115
+ return;
6116
+ const { quality, activeMedian } = dataFlowResult;
6117
+ const isQualityRed = quality === 'red';
6118
+ const isLowBitrate = activeMedian < FilterModel.minimumBitrate;
6119
+ // Final check for a red drop or low bitrate, or low FPS
6120
+ if (isQualityRed || isLowBitrate || isFpsLow) {
6121
+ let reason;
6122
+ if (isFpsLow) {
6123
+ reason = `FPS < ${FilterModel.minimumFps}`;
6124
+ }
6125
+ else if (quality === 'red') {
6126
+ reason = `Drop > ${FilterModel.redFlag}%`;
6127
+ }
6128
+ else {
6129
+ reason = `Speed < ${FilterModel.minimumBitrate}`;
6130
+ }
6131
+ Logger.info(`LBM DETECTED: ${reason}, switching to low bandwidth; `, dataFlowResult.message);
6132
+ this.store.dispatch(changeLowBandwidth({
6133
+ lowBandwidth: true,
6134
+ stats: {
6135
+ dataFlowResult,
6136
+ reason,
6137
+ triggerTime: this.aggregatedStats()?.runTime ?? 0,
6138
+ },
6139
+ }));
6134
6140
  }
6135
- catch (e) {
6136
- Logger.error("Couldn't create answer", e);
6141
+ }
6142
+ toggleIndicator(value) {
6143
+ this.isIndicatorExpanded.set(value);
6144
+ if (value) {
6145
+ this.hideIndicatorAfterDelay();
6137
6146
  }
6138
6147
  }
6139
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: WebRtcPlayerService2, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
6140
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: WebRtcPlayerService2 }); }
6148
+ hideIndicatorAfterDelay() {
6149
+ timer(3000)
6150
+ .pipe(take(1), takeUntil$1(this.isIndicatorExpandedTrigger$))
6151
+ .subscribe(() => this.toggleIndicator(false));
6152
+ }
6153
+ openLBMDialog() {
6154
+ this.dialog
6155
+ .open(LowBandwidthModalComponent)
6156
+ .closed.pipe(filter(Truthy))
6157
+ .subscribe(() => {
6158
+ this.toggleIndicator(false);
6159
+ this.store.dispatch(changeLowBandwidth({ lowBandwidth: false }));
6160
+ });
6161
+ }
6162
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: LowBandwidthDetectorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
6163
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: LowBandwidthDetectorComponent, isStandalone: true, selector: "app-low-bandwidth-detector", ngImport: i0, 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"], dependencies: [{ kind: "component", type: FilterSettingsComponent, selector: "app-filter-settings" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
6141
6164
  }
6142
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: WebRtcPlayerService2, decorators: [{
6143
- type: Injectable
6144
- }], ctorParameters: () => [] });
6165
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: LowBandwidthDetectorComponent, decorators: [{
6166
+ type: Component,
6167
+ 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"] }]
6168
+ }] });
6145
6169
 
6146
6170
  function provideAngularUnrealModule(config) {
6147
6171
  return makeEnvironmentProviders([
@@ -6151,18 +6175,23 @@ function provideAngularUnrealModule(config) {
6151
6175
  ConsoleExtensionsService,
6152
6176
  InputService,
6153
6177
  VideoService,
6178
+ RegionsPingService,
6179
+ FileReceiverService,
6180
+ FileHandlerService,
6181
+ AnalyticsService,
6182
+ UnrealCallbackService,
6154
6183
  {
6155
6184
  provide: WebRtcPlayerService,
6156
6185
  useClass: config?.newWebRtcConnector
6157
6186
  ? WebRtcPlayerService2
6158
6187
  : WebRtcPlayerService,
6159
6188
  },
6160
- RegionsPingService,
6161
- FileReceiverService,
6162
- FileHandlerService,
6163
- FpsMonitorService,
6164
- AnalyticsService,
6165
- UnrealCallbackService,
6189
+ {
6190
+ provide: FpsMonitorService,
6191
+ useClass: config?.playwright || config?.fpsMonitor
6192
+ ? FpsMonitorPlaywrightService
6193
+ : FpsMonitorService,
6194
+ },
6166
6195
  {
6167
6196
  provide: StreamStatusTelemetryService,
6168
6197
  useClass: config?.playwright
@@ -6232,5 +6261,5 @@ function provideAngularUnrealModule(config) {
6232
6261
  * Generated bundle index. Do not edit.
6233
6262
  */
6234
6263
 
6235
- export { AFKService, AfkPlaywrightService, AfkTimeoutModalComponent, AggregatorPlaywrightService, AggregatorService, AnalyticsService, AnswerHandler, CONSOLE_COMMAND_DISABLE_MESSAGES, CONSOLE_COMMAND_ENABLE_MESSAGES, CONSOLE_COMMAND_PIXEL_QUALITY, CommandTelemetryPlaywrightService, CommandTelemetryService, CommandsEffects, ConfigHandler, ConsoleExtensionsPlaywrightService, 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, DataFlowMonitor, DevModeService, DisconnectReason, 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, 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, SignallingPlaywrightService, SignallingService, SpecialKeyCodes, StatGraphComponent, StreamStatusTelemetryPlaywrightService, StreamStatusTelemetryService, SubService, ToClientMessage, UNREAL_CONFIG, UnrealCallbackService, UnrealCommunicatorPlaywrightService, 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, changeLowBandwidth, changeStatusMainVideoOnScene, changeStreamResolutionAction, changeStreamResolutionSuccessAction, commandCompleted, commandStarted, commandsFeature, commandsReducer, dataChannelConnected, dataChannelReady, decodeData, destroyRemoteConnections, destroyUnrealScene, disconnectStream, dispatchResize, dropConnection, dropDC, dropPC, floatToSmoothPercents, forceResizeViewport, fromResizeObserver, getActiveUrl, getImageFromVideoStream, getRtcErrorMessage, iceConnectionFailed, initSignalling, keepMaxUntilReset, provideAngularUnrealModule, qpToQuality, reconnectPeer, reconnectPeerFailed, reconnectPeerSuccess, resetAfk, resetConfig, resetDataChannelForReconnect, resetIntroSrc, resetWarnTimeout, saveAnalyticsEvent, selectClientAndViewIds, selectCommandProgress, selectCommandsInProgress, selectFreezeFrameCombinedDataUrl, selectFreezeFrameDataUrl, selectFreezeFrameDataUrlFromVideo, selectFreezeFrameProgressMessageFromVideo, selectIsAutostart, selectIsExistMatchUrls, selectIsFreezeFrameLoading, selectIsLoaderScreenVisible, selectIsShowPopupWithoutAutoStart, selectIsVideoPlayingAndDataChannelConnected, selectLastCommandInProgress, selectShowLoader, selectShowReconnectPopup, selectSignalingParameters, selectStreamConfig, selectTotalProgress, selectWarnTimeout, setAfkTimerCountdown, setAfkTimerHide, setAfkTimerVisible, setAwsInstance, setCirrusConnected, setCirrusDisconnected, setConfig, setDataChannelConnected, setEstablishingConnection, setFreezeFrame, setFreezeFrameFromVideo, setIntroImageSrc, setIntroVideoSrc, setKalmanSettings, setLoadingImageSrc, setLoopBackCommandIsCompleted, setMaxFps, setOrchestrationContext, setOrchestrationMessage, setOrchestrationParameters, setOrchestrationProgress, setSignalingName, setStatusMessage, setStatusPercentSignallingServer, setStreamClientCompanyId, setStreamViewId, setUnrealPlaywrightConfig, setViewportNotReady, setViewportReady, showUnrealErrorMessage, smoothTransition, startStream, trackMixpanelEvent, unrealCommandCallback, unrealFeature, unrealReducer, updateCirrusInfo, videoAdaptedToContainer, videoStreamInitialized };
6264
+ export { AFKService, AfkPlaywrightService, AfkTimeoutModalComponent, AggregatorPlaywrightService, AggregatorService, AnalyticsService, AnswerHandler, CONSOLE_COMMAND_DISABLE_MESSAGES, CONSOLE_COMMAND_ENABLE_MESSAGES, CONSOLE_COMMAND_PIXEL_QUALITY, CommandTelemetryPlaywrightService, CommandTelemetryService, CommandsEffects, ConfigHandler, ConsoleExtensionsPlaywrightService, 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, DataFlowMonitor, DevModeService, DisconnectReason, FULL_HD_HEIGHT, FULL_HD_WIDTH, FileHandlerService, FileReceiverPlaywrightService, FileReceiverService, FilterSettingsComponent, FpsMonitorPlaywrightService, FpsMonitorService, FreezeFrameComponent, FreezeFramePlaywrightService, FreezeFrameService, IceCandidateHandler, ImageLoadingSrcComponent, InputOptions, InputPlaywrightService, 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, SignallingPlaywrightService, SignallingService, SpecialKeyCodes, StatGraphComponent, StreamStatusTelemetryPlaywrightService, StreamStatusTelemetryService, SubService, ToClientMessage, UNREAL_CONFIG, UnrealCallbackService, UnrealCommunicatorPlaywrightService, 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, WebRtcPlayerService2, WebrtcErrorModalComponent, abortEstablishingConnection, changeLowBandwidth, changeStatusMainVideoOnScene, changeStreamResolutionAction, changeStreamResolutionSuccessAction, commandCompleted, commandStarted, commandsFeature, commandsReducer, dataChannelConnected, dataChannelReady, decodeData, destroyRemoteConnections, destroyUnrealScene, disconnectStream, dispatchResize, dropConnection, dropDC, dropPC, floatToSmoothPercents, forceResizeViewport, fromResizeObserver, getActiveUrl, getImageFromVideoStream, getRtcErrorMessage, iceConnectionFailed, initSignalling, keepMaxUntilReset, provideAngularUnrealModule, qpToQuality, reconnectPeer, reconnectPeerFailed, reconnectPeerSuccess, resetAfk, resetConfig, resetDataChannelForReconnect, resetIntroSrc, resetWarnTimeout, saveAnalyticsEvent, selectClientAndViewIds, selectCommandProgress, selectCommandsInProgress, selectFreezeFrameCombinedDataUrl, selectFreezeFrameDataUrl, selectFreezeFrameDataUrlFromVideo, selectFreezeFrameProgressMessageFromVideo, selectIsAutostart, selectIsExistMatchUrls, selectIsFreezeFrameLoading, selectIsLoaderScreenVisible, selectIsShowPopupWithoutAutoStart, selectIsVideoPlayingAndDataChannelConnected, selectLastCommandInProgress, selectShowLoader, selectShowReconnectPopup, selectSignalingParameters, selectStreamConfig, selectTotalProgress, selectWarnTimeout, setAfkTimerCountdown, setAfkTimerHide, setAfkTimerVisible, setAwsInstance, setCirrusConnected, setCirrusDisconnected, setConfig, setDataChannelConnected, setEstablishingConnection, setFreezeFrame, setFreezeFrameFromVideo, setIntroImageSrc, setIntroVideoSrc, setKalmanSettings, setLoadingImageSrc, setLoopBackCommandIsCompleted, setMaxFps, setOrchestrationContext, setOrchestrationMessage, setOrchestrationParameters, setOrchestrationProgress, setSignalingName, setStatusMessage, setStatusPercentSignallingServer, setStreamClientCompanyId, setStreamViewId, setUnrealPlaywrightConfig, setViewportNotReady, setViewportReady, showUnrealErrorMessage, smoothTransition, startStream, trackMixpanelEvent, unrealCommandCallback, unrealFeature, unrealReducer, updateCirrusInfo, videoAdaptedToContainer, videoStreamInitialized };
6236
6265
  //# sourceMappingURL=3dsource-angular-unreal-module.mjs.map