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