@3dsource/angular-unreal-module 0.0.101-dev.4 → 0.0.101-dev.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -979,7 +979,7 @@ const selectSignalingParameters = createSelector(unrealFeature.selectSsData, unr
|
|
|
979
979
|
* Returns Scene load progress as a float value in between 0-1
|
|
980
980
|
*/
|
|
981
981
|
const selectTotalProgress = createSelector(unrealFeature.selectStatusPercentSignallingServer, selectCommandProgress, unrealFeature.selectAwsInstance, unrealFeature.selectDataChannelConnected, (statusPercentSignallingServer, commandProgress, awsInstance, dataChannelConnected) => {
|
|
982
|
-
//TODO make better mechanism to detect is new orchestration
|
|
982
|
+
// TODO make better mechanism to detect is new orchestration
|
|
983
983
|
const isNewOrchestration = String(awsInstance?.wsUrl).match(/^wss:\/\/orchestration-websocket/i);
|
|
984
984
|
const newOrchestrationProgressComplete = dataChannelConnected
|
|
985
985
|
? 100
|
|
@@ -1378,7 +1378,7 @@ class CommandsEffects {
|
|
|
1378
1378
|
this.listenUnrealCallbackByInitSignalling$ = createEffect(() => {
|
|
1379
1379
|
return this.actions$.pipe(ofType(initSignalling), filter(() => !this.unrealInitialConfig?.playwright), switchMap(() => this.actions$.pipe(ofType(unrealCommandCallback), map(({ json }) => 'commandCallback' in json
|
|
1380
1380
|
? json.commandCallback?.correlationId
|
|
1381
|
-
: json?.correlationId), filter(Truthy),
|
|
1381
|
+
: json?.correlationId), filter(Truthy), map((id) => commandCompleted({ id })))));
|
|
1382
1382
|
});
|
|
1383
1383
|
}
|
|
1384
1384
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: CommandsEffects, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
@@ -2113,8 +2113,13 @@ function createWebSocket(wsUrl) {
|
|
|
2113
2113
|
Logger.info('Creating socket', wsUrl);
|
|
2114
2114
|
return defer(() => new Observable((observer) => {
|
|
2115
2115
|
const ws = new WebSocket(wsUrl);
|
|
2116
|
+
let opened = false;
|
|
2117
|
+
let emitted = false;
|
|
2116
2118
|
const onOpen = () => {
|
|
2119
|
+
opened = true;
|
|
2120
|
+
emitted = true;
|
|
2117
2121
|
observer.next(ws); // connection established
|
|
2122
|
+
observer.complete(); // single-value observable — done
|
|
2118
2123
|
};
|
|
2119
2124
|
const onError = (ev) => {
|
|
2120
2125
|
const err = ev && 'error' in ev && ev?.error instanceof Error
|
|
@@ -2123,7 +2128,7 @@ function createWebSocket(wsUrl) {
|
|
|
2123
2128
|
observer.error(err);
|
|
2124
2129
|
};
|
|
2125
2130
|
const onClose = (ev) => {
|
|
2126
|
-
if (
|
|
2131
|
+
if (!opened) {
|
|
2127
2132
|
observer.error(new Error(`WebSocket closed before open (code ${ev.code})`));
|
|
2128
2133
|
}
|
|
2129
2134
|
};
|
|
@@ -2134,6 +2139,14 @@ function createWebSocket(wsUrl) {
|
|
|
2134
2139
|
ws.removeEventListener('open', onOpen);
|
|
2135
2140
|
ws.removeEventListener('error', onError);
|
|
2136
2141
|
ws.removeEventListener('close', onClose);
|
|
2142
|
+
// Only close the socket if it was never delivered to the consumer.
|
|
2143
|
+
// Once emitted, the consumer owns the socket (e.g. connectToCirrus
|
|
2144
|
+
// attaches its own handlers). Closing here would break downstream.
|
|
2145
|
+
if (!emitted &&
|
|
2146
|
+
(ws.readyState === WebSocket.CONNECTING ||
|
|
2147
|
+
ws.readyState === WebSocket.OPEN)) {
|
|
2148
|
+
ws.close();
|
|
2149
|
+
}
|
|
2137
2150
|
};
|
|
2138
2151
|
})).pipe(timeout$1({
|
|
2139
2152
|
first: WS_TIMEOUT, // fail if no 'open' event in this window
|
|
@@ -2231,7 +2244,7 @@ class SignallingService extends SubService {
|
|
|
2231
2244
|
this.correlationId = generateUuid();
|
|
2232
2245
|
this.commandTelemetryService.reset();
|
|
2233
2246
|
this.startEstablishingConnection();
|
|
2234
|
-
return defer(() => this.getRegion().pipe(tap((region) => this.region.set(region ?? '')), map$1((region) => this.adaptUrlsToRegion(urlList, region)), switchMap((urls) => this.getAwsInstance(urls).pipe(take(1))), tap((awsInstance) => this.store.dispatch(setAwsInstance(awsInstance))), switchMap(({ wsUrl }) => this.connectToCirrus(wsUrl ?? '')
|
|
2247
|
+
return defer(() => this.getRegion().pipe(tap((region) => this.region.set(region ?? '')), map$1((region) => this.adaptUrlsToRegion(urlList, region)), switchMap((urls) => this.getAwsInstance(urls).pipe(take(1))), tap((awsInstance) => this.store.dispatch(setAwsInstance(awsInstance))), switchMap(({ wsUrl }) => this.connectToCirrus(wsUrl ?? '').pipe(filter(Truthy), take(1))), takeUntil(this.abort$))).pipe(
|
|
2235
2248
|
// Retry the whole attempt on error, advancing to the next URL each time.
|
|
2236
2249
|
retry({
|
|
2237
2250
|
delay: (err, attempt) => {
|
|
@@ -4057,2091 +4070,2102 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImpor
|
|
|
4057
4070
|
type: Injectable
|
|
4058
4071
|
}], ctorParameters: () => [] });
|
|
4059
4072
|
|
|
4060
|
-
|
|
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 {
|
|
4073
|
+
/*
|
|
4074
|
+
* Copyright (c) 2026.
|
|
4075
|
+
* 3DSource.com. Sergii Karanda steve@3dsource.com. All Rights Reserved.
|
|
4076
|
+
*/
|
|
4077
|
+
class WebRtcPlayerService2 extends SubService {
|
|
4072
4078
|
constructor() {
|
|
4073
4079
|
super();
|
|
4080
|
+
this.onDataChannelMessage$ = new Subject();
|
|
4081
|
+
this.signallingSrv = inject(SignallingService);
|
|
4082
|
+
this.videoService = inject(VideoService);
|
|
4083
|
+
this.commandTelemetryService = inject(CommandTelemetryService);
|
|
4084
|
+
this.cfg = {};
|
|
4085
|
+
this.useMic = false;
|
|
4086
|
+
this.forceTURN = false;
|
|
4087
|
+
this.forceMonoAudio = false;
|
|
4088
|
+
/** Whether the remote SDP has been applied to the current peer connection */
|
|
4089
|
+
this.remoteDescriptionSet = false;
|
|
4090
|
+
/** ICE candidates received before the remote description was set */
|
|
4091
|
+
this.iceCandidateBuffer = [];
|
|
4092
|
+
this.isReconnecting = this.store.selectSignal(unrealFeature.selectIsReconnecting);
|
|
4093
|
+
this.init();
|
|
4094
|
+
this.actions$.pipe(ofType(dropDC)).subscribe(() => this.dcClient?.close());
|
|
4095
|
+
this.actions$.pipe(ofType(dropPC)).subscribe(() => this.pcClient?.close());
|
|
4074
4096
|
}
|
|
4075
4097
|
init() {
|
|
4076
|
-
|
|
4077
|
-
|
|
4078
|
-
|
|
4079
|
-
|
|
4080
|
-
|
|
4081
|
-
|
|
4082
|
-
|
|
4083
|
-
|
|
4084
|
-
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
|
|
4089
|
-
|
|
4090
|
-
|
|
4091
|
-
|
|
4092
|
-
|
|
4093
|
-
|
|
4094
|
-
|
|
4095
|
-
|
|
4096
|
-
|
|
4097
|
-
|
|
4098
|
+
this.signallingSrv.onConfig$.subscribe((config) => {
|
|
4099
|
+
this.videoService.create();
|
|
4100
|
+
this.setConfig(config);
|
|
4101
|
+
});
|
|
4102
|
+
// Server-initiated offer: set remote description and respond with answer
|
|
4103
|
+
this.signallingSrv.onOffer$.pipe(filter(Truthy)).subscribe((offer) => {
|
|
4104
|
+
void this.handleServerOffer(offer);
|
|
4105
|
+
});
|
|
4106
|
+
// Answer to a client-initiated offer
|
|
4107
|
+
this.signallingSrv.onWebRtcAnswer$
|
|
4108
|
+
.pipe(filter(Truthy))
|
|
4109
|
+
.subscribe((answer) => {
|
|
4110
|
+
void this.handleServerAnswer(answer);
|
|
4111
|
+
});
|
|
4112
|
+
// ICE candidates — buffered until remote description is set
|
|
4113
|
+
this.signallingSrv.onWebRtcIce$.subscribe((candidate) => {
|
|
4114
|
+
this.commandTelemetryService.trackStopCommand('EXT-iceCandidate', true, {
|
|
4115
|
+
multi: true,
|
|
4116
|
+
});
|
|
4117
|
+
if (this.remoteDescriptionSet) {
|
|
4118
|
+
this.handleCandidateFromServer(candidate);
|
|
4119
|
+
}
|
|
4120
|
+
else {
|
|
4121
|
+
this.iceCandidateBuffer.push(candidate);
|
|
4122
|
+
}
|
|
4123
|
+
});
|
|
4098
4124
|
}
|
|
4099
|
-
|
|
4100
|
-
|
|
4125
|
+
/**
|
|
4126
|
+
* Handles an SDP offer received from the server.
|
|
4127
|
+
* Creates a new peer connection, applies the remote offer,
|
|
4128
|
+
* then responds with an SDP answer.
|
|
4129
|
+
*
|
|
4130
|
+
* The DataChannel is NOT created here — the server creates it
|
|
4131
|
+
* and we receive it via the `datachannel` event on the peer connection.
|
|
4132
|
+
*/
|
|
4133
|
+
async handleServerOffer(offer) {
|
|
4134
|
+
Logger.info('Received offer from server, creating answer');
|
|
4135
|
+
try {
|
|
4136
|
+
this.remoteDescriptionSet = false;
|
|
4137
|
+
this.iceCandidateBuffer.length = 0;
|
|
4138
|
+
if (!this.pcClient) {
|
|
4139
|
+
this.setupPeerConnection(this.cfg);
|
|
4140
|
+
}
|
|
4141
|
+
// Offer already defines media transceivers — don't add extras
|
|
4142
|
+
await this.pcClient.setRemoteDescription(offer);
|
|
4143
|
+
this.remoteDescriptionSet = true;
|
|
4144
|
+
this.flushIceCandidateBuffer();
|
|
4145
|
+
await this.createAnswer(this.pcClient);
|
|
4146
|
+
}
|
|
4147
|
+
catch (e) {
|
|
4148
|
+
Logger.error('Failed to handle server offer', e);
|
|
4149
|
+
this.store.dispatch(disconnectStream({
|
|
4150
|
+
reason: DisconnectReason.WebRTCError,
|
|
4151
|
+
message: 'WebRTCError: Failed to handle server offer',
|
|
4152
|
+
}));
|
|
4153
|
+
}
|
|
4101
4154
|
}
|
|
4102
|
-
|
|
4103
|
-
|
|
4104
|
-
|
|
4105
|
-
|
|
4106
|
-
|
|
4107
|
-
|
|
4108
|
-
|
|
4109
|
-
|
|
4110
|
-
|
|
4111
|
-
|
|
4155
|
+
/**
|
|
4156
|
+
* Handles an SDP answer received from the server in response to our offer.
|
|
4157
|
+
* Applies the remote description and flushes any buffered ICE candidates.
|
|
4158
|
+
*/
|
|
4159
|
+
async handleServerAnswer(answer) {
|
|
4160
|
+
Logger.info('Received answer from server');
|
|
4161
|
+
try {
|
|
4162
|
+
await this.pcClient?.setRemoteDescription(answer);
|
|
4163
|
+
this.remoteDescriptionSet = true;
|
|
4164
|
+
this.flushIceCandidateBuffer();
|
|
4165
|
+
}
|
|
4166
|
+
catch (e) {
|
|
4167
|
+
Logger.error('Failed to set remote description from answer', e);
|
|
4168
|
+
}
|
|
4112
4169
|
}
|
|
4113
|
-
|
|
4114
|
-
|
|
4115
|
-
|
|
4116
|
-
|
|
4117
|
-
|
|
4118
|
-
|
|
4119
|
-
|
|
4120
|
-
class FreezeFramePlaywrightService extends FreezeFrameService {
|
|
4121
|
-
init() {
|
|
4122
|
-
return;
|
|
4170
|
+
/** Drains buffered ICE candidates after the remote description has been set */
|
|
4171
|
+
flushIceCandidateBuffer() {
|
|
4172
|
+
for (const candidate of this.iceCandidateBuffer) {
|
|
4173
|
+
this.handleCandidateFromServer(candidate);
|
|
4174
|
+
}
|
|
4175
|
+
this.iceCandidateBuffer.length = 0;
|
|
4123
4176
|
}
|
|
4124
|
-
|
|
4125
|
-
|
|
4126
|
-
|
|
4127
|
-
|
|
4128
|
-
|
|
4129
|
-
|
|
4130
|
-
|
|
4131
|
-
|
|
4132
|
-
|
|
4133
|
-
|
|
4177
|
+
/**
|
|
4178
|
+
* Creates a client-initiated WebRTC offer and sends it to the server.
|
|
4179
|
+
* When the client offers, it also creates the DataChannel.
|
|
4180
|
+
* Used for peer reconnection through the existing signaling connection.
|
|
4181
|
+
*/
|
|
4182
|
+
async createWebRtcOffer() {
|
|
4183
|
+
Logger.info('Creating client-initiated offer');
|
|
4184
|
+
try {
|
|
4185
|
+
this.remoteDescriptionSet = false;
|
|
4186
|
+
this.iceCandidateBuffer.length = 0;
|
|
4187
|
+
if (!this.pcClient) {
|
|
4188
|
+
this.setupPeerConnection(this.cfg);
|
|
4189
|
+
}
|
|
4190
|
+
// Client offers: transceivers first, then DC, then offer
|
|
4191
|
+
await this.setupTransceiversAsync(this.pcClient);
|
|
4192
|
+
this.dcClient = this.createDataChannel(this.pcClient, 'UnrealDcConnection', { ordered: true });
|
|
4193
|
+
await this.createOffer(this.pcClient);
|
|
4194
|
+
}
|
|
4195
|
+
catch (e) {
|
|
4196
|
+
Logger.error('Failed to create WebRTC offer', e);
|
|
4197
|
+
this.store.dispatch(disconnectStream({
|
|
4198
|
+
reason: DisconnectReason.WebRTCError,
|
|
4199
|
+
message: 'WebRTCError: Offer Error',
|
|
4200
|
+
}));
|
|
4201
|
+
}
|
|
4134
4202
|
}
|
|
4135
|
-
|
|
4136
|
-
|
|
4137
|
-
|
|
4138
|
-
|
|
4139
|
-
|
|
4140
|
-
|
|
4141
|
-
|
|
4142
|
-
|
|
4143
|
-
|
|
4144
|
-
|
|
4203
|
+
/**
|
|
4204
|
+
* Initializes a new RTCPeerConnection with event handlers.
|
|
4205
|
+
* Transceivers are NOT added here — the caller is responsible for adding
|
|
4206
|
+
* them at the correct point depending on the negotiation flow.
|
|
4207
|
+
*/
|
|
4208
|
+
setupPeerConnection(config) {
|
|
4209
|
+
this.closePC();
|
|
4210
|
+
Logger.log('Creating new RTCPeerConnection with config');
|
|
4211
|
+
this.pcClient = new RTCPeerConnection(config);
|
|
4212
|
+
this.commandTelemetryService.trackStartCommand('EXT-Browser ICE gathering');
|
|
4213
|
+
this.commandTelemetryService.trackStartCommand('EXT-Browser ICE connection');
|
|
4214
|
+
this.commandTelemetryService.trackStartCommand('EXT-Signaling state change');
|
|
4215
|
+
this.handlePeerConnectionEvents(this.pcClient);
|
|
4216
|
+
this.disconnect$.pipe(take(1)).subscribe(() => {
|
|
4217
|
+
this.closePC();
|
|
4218
|
+
});
|
|
4145
4219
|
}
|
|
4146
|
-
|
|
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;
|
|
4220
|
+
createDataChannel(pc, label, options) {
|
|
4221
|
+
const datachannel = pc.createDataChannel(label, options);
|
|
4222
|
+
return this.setupDataChannelCallbacks(datachannel);
|
|
4156
4223
|
}
|
|
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;
|
|
4224
|
+
handleCandidateFromServer(iceCandidate) {
|
|
4225
|
+
try {
|
|
4226
|
+
const candidate = new RTCIceCandidate(iceCandidate);
|
|
4227
|
+
Logger.info('[Unreal ICE candidate]', '| Type=', candidate.type, '| Protocol=', candidate.protocol, '| Address=', candidate.address, '| Port=', candidate.port, '|');
|
|
4228
|
+
// If forcing TURN, reject any candidates not relay
|
|
4229
|
+
if (this.forceTURN) {
|
|
4230
|
+
if (candidate.candidate.indexOf('relay') < 0) {
|
|
4231
|
+
Logger.warn('Dropping candidate because it was not TURN relay.', '| Type=', candidate.type, '| Protocol=', candidate.protocol, '| Address=', candidate.address, '| Port=', candidate.port, '|');
|
|
4232
|
+
return;
|
|
4233
|
+
}
|
|
4234
|
+
}
|
|
4235
|
+
this.pcClient?.addIceCandidate(candidate).catch((e) => {
|
|
4236
|
+
Logger.warn('Failed to add ICE candidate', e);
|
|
4237
|
+
});
|
|
4238
|
+
}
|
|
4239
|
+
catch (e) {
|
|
4240
|
+
Logger.error('Error handleCandidateFromServer', e);
|
|
4241
|
+
}
|
|
4510
4242
|
}
|
|
4511
4243
|
/**
|
|
4512
|
-
*
|
|
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).
|
|
4547
|
-
*
|
|
4548
|
-
* load < 0.5 → 0.0 (below 10 Mbit/s — plenty of headroom)
|
|
4549
|
-
* load 0.5..0.8 → linear ramp (e.g. 13M/20M = 0.65 → 0.5)
|
|
4550
|
-
* load > 0.8 → 1.0 (above 16 Mbit/s — near capacity)
|
|
4551
|
-
*/
|
|
4552
|
-
membershipBitrateHigh(load) {
|
|
4553
|
-
if (load < 0.5)
|
|
4554
|
-
return 0.0;
|
|
4555
|
-
if (load > 0.8)
|
|
4556
|
-
return 1.0;
|
|
4557
|
-
return (load - 0.5) / 0.3;
|
|
4558
|
-
}
|
|
4559
|
-
// ---------------------------------------------------------------------------
|
|
4560
|
-
// Step actions
|
|
4561
|
-
// ---------------------------------------------------------------------------
|
|
4562
|
-
upgradeFps(decision, qp) {
|
|
4563
|
-
if (decision === 'hold')
|
|
4564
|
-
return;
|
|
4565
|
-
const now = Date.now();
|
|
4566
|
-
// Throttle: cooldown depends on (previous decision → current decision) pair.
|
|
4567
|
-
const isStepUp = decision === 'increase';
|
|
4568
|
-
const cooldown = this.lastUpgrade === 'decrease'
|
|
4569
|
-
? isStepUp
|
|
4570
|
-
? this.COOLDOWN_INCREASE_AFTER_DECREASE_MS
|
|
4571
|
-
: this.COOLDOWN_DECREASE_AFTER_DECREASE_MS
|
|
4572
|
-
: this.COOLDOWN_AFTER_INCREASE_MS;
|
|
4573
|
-
if (now - this.lastDecisionTime < cooldown) {
|
|
4574
|
-
return;
|
|
4575
|
-
}
|
|
4576
|
-
this.lastUpgrade = decision;
|
|
4577
|
-
const quality = qpToQuality(qp);
|
|
4578
|
-
const newTarget = this.getNextFpsTarget(this.currentFpsTarget, decision);
|
|
4579
|
-
Logger.info(`[FpsMonitor] Decision: ${decision} → FPS: ${newTarget}, Quality: ${quality}`);
|
|
4580
|
-
if (newTarget !== this.currentFpsTarget) {
|
|
4581
|
-
this.currentFpsTarget = newTarget;
|
|
4582
|
-
this.store.dispatch(setMaxFps({ maxFps: newTarget }));
|
|
4583
|
-
}
|
|
4584
|
-
this.lastDecisionTime = now;
|
|
4585
|
-
this.samples = [];
|
|
4586
|
-
}
|
|
4587
|
-
/**
|
|
4588
|
-
* Get the next FPS target based on current target and decision.
|
|
4589
|
-
* Clamps to valid FPS_STEPS range.
|
|
4590
|
-
*/
|
|
4591
|
-
getNextFpsTarget(currentTarget, decision) {
|
|
4592
|
-
const currentIndex = this.FPS_STEPS.indexOf(currentTarget);
|
|
4593
|
-
// If current target is not in FPS_STEPS, find closest one
|
|
4594
|
-
const validIndex = currentIndex === -1
|
|
4595
|
-
? this.findClosestFpsIndex(currentTarget)
|
|
4596
|
-
: currentIndex;
|
|
4597
|
-
const delta = decision === 'increase' ? 1 : -1;
|
|
4598
|
-
const newIndex = clampf(0, this.FPS_STEPS.length - 1, validIndex + delta);
|
|
4599
|
-
return this.FPS_STEPS[newIndex];
|
|
4600
|
-
}
|
|
4601
|
-
/**
|
|
4602
|
-
* Find the closest FPS step index for a given target value.
|
|
4603
|
-
*/
|
|
4604
|
-
findClosestFpsIndex(target) {
|
|
4605
|
-
let closestIndex = 0;
|
|
4606
|
-
let closestDiff = Math.abs(this.FPS_STEPS[0] - target);
|
|
4607
|
-
for (let i = 1; i < this.FPS_STEPS.length; i++) {
|
|
4608
|
-
const diff = Math.abs(this.FPS_STEPS[i] - target);
|
|
4609
|
-
if (diff < closestDiff) {
|
|
4610
|
-
closestDiff = diff;
|
|
4611
|
-
closestIndex = i;
|
|
4612
|
-
}
|
|
4613
|
-
}
|
|
4614
|
-
return closestIndex;
|
|
4615
|
-
}
|
|
4616
|
-
/** Average all collected samples for the current evaluation window. */
|
|
4617
|
-
averageSamples() {
|
|
4618
|
-
const len = this.samples.length;
|
|
4619
|
-
if (len === 0)
|
|
4620
|
-
return { fps: 0, bitrate: 0, qp: 0 };
|
|
4621
|
-
let fps = 0;
|
|
4622
|
-
let bitrate = 0;
|
|
4623
|
-
let qp = 0;
|
|
4624
|
-
for (const s of this.samples) {
|
|
4625
|
-
fps += s.fps;
|
|
4626
|
-
bitrate += s.bitrate;
|
|
4627
|
-
qp += s.qp;
|
|
4628
|
-
}
|
|
4629
|
-
return { fps: fps / len, bitrate: bitrate / len, qp: qp / len };
|
|
4630
|
-
}
|
|
4631
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: FpsMonitorService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
4632
|
-
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: FpsMonitorService }); }
|
|
4633
|
-
}
|
|
4634
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: FpsMonitorService, decorators: [{
|
|
4635
|
-
type: Injectable
|
|
4636
|
-
}], ctorParameters: () => [] });
|
|
4637
|
-
|
|
4638
|
-
class UnrealCallbackService {
|
|
4639
|
-
constructor() {
|
|
4640
|
-
this.actions$ = inject(Actions);
|
|
4641
|
-
}
|
|
4642
|
-
/**
|
|
4643
|
-
* Listens for unreal command callbacks matching a specific command or event type.
|
|
4244
|
+
* Reorders the `m=video` line in the SDP so that payload types matching
|
|
4245
|
+
* the given H.264 `profile-level-id` appear first, making the browser
|
|
4246
|
+
* prefer that profile during codec negotiation.
|
|
4644
4247
|
*
|
|
4645
|
-
*
|
|
4646
|
-
*
|
|
4248
|
+
* A `profile-level-id` is a 3-byte hex string (`XXYYZZ`) defined in
|
|
4249
|
+
* RFC 6184 that encodes three H.264 parameters:
|
|
4250
|
+
* - **Profile IDC** (`XX`) — the H.264 profile.
|
|
4251
|
+
* - **Profile IOP** (`YY`) — constraint flags that further narrow the profile.
|
|
4252
|
+
* - **Level IDC** (`ZZ`) — the level, which caps resolution/framerate/bitrate.
|
|
4647
4253
|
*
|
|
4648
|
-
*
|
|
4649
|
-
* @returns Observable that emits matching callback payloads
|
|
4254
|
+
* ### H.264 profiles relevant to Unreal Engine Pixel Streaming
|
|
4650
4255
|
*
|
|
4651
|
-
*
|
|
4652
|
-
*
|
|
4653
|
-
*
|
|
4654
|
-
*
|
|
4655
|
-
*
|
|
4656
|
-
*
|
|
4256
|
+
* | profile-level-id | Profile | Level | Notes |
|
|
4257
|
+
* |------------------|----------------------------|-------|----------------------------------------------|
|
|
4258
|
+
* | `42001f` | Baseline | 3.1 | No B-frames, low latency; legacy devices |
|
|
4259
|
+
* | `42e01f` | Constrained Baseline | 3.1 | Widest HW decode support; **recommended** |
|
|
4260
|
+
* | `4d001f` | Main | 3.1 | B-frames + CABAC; better compression |
|
|
4261
|
+
* | `4d4028` | Main | 4.0 | Up to 1080p60; common desktop target |
|
|
4262
|
+
* | `640028` | High | 4.0 | Best quality/compression; most GPUs support |
|
|
4263
|
+
* | `64002a` | High | 4.2 | Up to 1080p120 or 4K30 |
|
|
4264
|
+
* | `640033` | High | 5.1 | Up to 4K60; requires modern HW |
|
|
4265
|
+
* | `640034` | High | 5.2 | Up to 4K120; high-end HW only |
|
|
4266
|
+
* | `6e001f` | High 10 | 3.1 | 10-bit color; limited browser support |
|
|
4267
|
+
* | `f4001f` | High 4:4:4 Predictive | 3.1 | Lossless capable; rare in browsers |
|
|
4657
4268
|
*
|
|
4658
|
-
*
|
|
4659
|
-
*
|
|
4660
|
-
*
|
|
4661
|
-
*
|
|
4662
|
-
*
|
|
4269
|
+
* UE Pixel Streaming defaults to Constrained Baseline (`42e01f`) or High (`6400xx`)
|
|
4270
|
+
* depending on engine version and `WebRTC.H264Profile` CVar.
|
|
4271
|
+
*
|
|
4272
|
+
* We prioritize `42e01f` because it guarantees hardware decoding on
|
|
4273
|
+
* virtually all client devices (desktop, mobile, embedded), avoiding
|
|
4274
|
+
* fallback to software decoding or negotiation of a higher profile
|
|
4275
|
+
* that the client GPU may not accelerate.
|
|
4663
4276
|
*/
|
|
4664
|
-
|
|
4665
|
-
|
|
4666
|
-
|
|
4667
|
-
|
|
4277
|
+
prioritizeH264Profile(sdp, profileLevelId) {
|
|
4278
|
+
const lines = sdp.split('\r\n');
|
|
4279
|
+
const targetPts = [];
|
|
4280
|
+
for (const line of lines) {
|
|
4281
|
+
if (line.startsWith('a=fmtp:') &&
|
|
4282
|
+
line.toLowerCase().includes(`profile-level-id=${profileLevelId}`)) {
|
|
4283
|
+
const pt = line.split(':')[1]?.split(' ')[0];
|
|
4284
|
+
if (pt)
|
|
4285
|
+
targetPts.push(pt);
|
|
4668
4286
|
}
|
|
4669
|
-
|
|
4670
|
-
|
|
4671
|
-
|
|
4672
|
-
|
|
4673
|
-
|
|
4287
|
+
}
|
|
4288
|
+
if (targetPts.length === 0)
|
|
4289
|
+
return sdp;
|
|
4290
|
+
return lines
|
|
4291
|
+
.map((line) => {
|
|
4292
|
+
if (!line.startsWith('m=video'))
|
|
4293
|
+
return line;
|
|
4294
|
+
const parts = line.split(' ');
|
|
4295
|
+
// parts: m=video <port> <proto> <pt1> <pt2> ...
|
|
4296
|
+
const header = parts.slice(0, 3);
|
|
4297
|
+
const pts = parts.slice(3);
|
|
4298
|
+
const prioritized = [
|
|
4299
|
+
...targetPts.filter((pt) => pts.includes(pt)),
|
|
4300
|
+
...pts.filter((pt) => !targetPts.includes(pt)),
|
|
4301
|
+
];
|
|
4302
|
+
return [...header, ...prioritized].join(' ');
|
|
4303
|
+
})
|
|
4304
|
+
.join('\r\n');
|
|
4305
|
+
}
|
|
4306
|
+
mungeSDP(sdp) {
|
|
4307
|
+
let mungedSDP = this.prioritizeH264Profile(sdp, '42e01f');
|
|
4308
|
+
mungedSDP = mungedSDP.replace(/(a=fmtp:\d+ .*level-asymmetry-allowed=.*)\r\n/gm, '$1;x-google-start-bitrate=10000;x-google-max-bitrate=100000\r\n');
|
|
4309
|
+
// Set max bitrate to highest bitrate Opus supports
|
|
4310
|
+
let audioSDP = 'maxaveragebitrate=510000;';
|
|
4311
|
+
if (this.useMic) {
|
|
4312
|
+
// Set the max capture rate to 48khz (so we can send high-quality audio from mic)
|
|
4313
|
+
audioSDP += 'sprop-maxcapturerate=48000;';
|
|
4314
|
+
}
|
|
4315
|
+
// Force mono or stereo based on whether ?forceMono was passed or not
|
|
4316
|
+
audioSDP += this.forceMonoAudio ? 'stereo=0;' : 'stereo=1;';
|
|
4317
|
+
// Enable in-band forward error correction for opus audio
|
|
4318
|
+
audioSDP += 'useinbandfec=1';
|
|
4319
|
+
// We use the line 'useinbandfec=1' (which Opus uses) to set our Opus specific audio parameters.
|
|
4320
|
+
mungedSDP = mungedSDP?.replace('useinbandfec=1', audioSDP);
|
|
4321
|
+
return mungedSDP;
|
|
4674
4322
|
}
|
|
4675
4323
|
/**
|
|
4676
|
-
*
|
|
4677
|
-
* Replaces the old `observeCommandResponse` standalone function.
|
|
4678
|
-
*
|
|
4679
|
-
* @param data - The command data to send
|
|
4680
|
-
* @param sender - Function to send the command to Unreal
|
|
4681
|
-
* @param timeOut - Timeout in ms (default 60000)
|
|
4682
|
-
* @param dispatchOnTimeout - Whether to emit on timeout (default true)
|
|
4683
|
-
*
|
|
4684
|
-
* @example
|
|
4685
|
-
* ```ts
|
|
4686
|
-
* this.unrealCallbackService
|
|
4687
|
-
* .observeCommandResponse<FLoopBackCommand['command']>(
|
|
4688
|
-
* { command: MetaBoxCommand.FLoopBackCommand },
|
|
4689
|
-
* (data) => this.communicator.sendCommandToUnreal(data),
|
|
4690
|
-
* )
|
|
4691
|
-
* .subscribe();
|
|
4692
|
-
* ```
|
|
4324
|
+
* Closes existing PeerConnection and DataChannel, resets negotiation state.
|
|
4693
4325
|
*/
|
|
4694
|
-
|
|
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;
|
|
4326
|
+
closePC() {
|
|
4327
|
+
if (this.pcClient) {
|
|
4328
|
+
Logger.warn('Closing existing PeerConnection');
|
|
4754
4329
|
}
|
|
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}`);
|
|
4330
|
+
this.pcClient?.close();
|
|
4331
|
+
this.pcClient = null;
|
|
4332
|
+
if (this.dcClient) {
|
|
4333
|
+
Logger.warn('Closing existing DataChannel');
|
|
4768
4334
|
}
|
|
4335
|
+
this.dcClient?.close();
|
|
4336
|
+
this.dcClient = null;
|
|
4337
|
+
this.remoteDescriptionSet = false;
|
|
4338
|
+
this.iceCandidateBuffer.length = 0;
|
|
4769
4339
|
}
|
|
4770
|
-
|
|
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
|
-
});
|
|
4340
|
+
canSend() {
|
|
4341
|
+
return this.dcClient && this.dcClient.readyState === 'open';
|
|
4833
4342
|
}
|
|
4834
|
-
|
|
4835
|
-
this.
|
|
4343
|
+
send(data) {
|
|
4344
|
+
if (this.canSend()) {
|
|
4345
|
+
this.dcClient?.send(data);
|
|
4346
|
+
}
|
|
4347
|
+
else {
|
|
4348
|
+
Logger.colored('black', 'yellow', `DataChannel connection not prepared or in state: ${this.dcClient?.readyState}`);
|
|
4349
|
+
// Trigger peer reconnection if DC is not open and we're not already reconnecting
|
|
4350
|
+
this.tryReconnectOnSendFailure();
|
|
4351
|
+
}
|
|
4836
4352
|
}
|
|
4837
|
-
|
|
4838
|
-
|
|
4353
|
+
/**
|
|
4354
|
+
* Attempts to reconnect the peer when send fails due to DataChannel not being open.
|
|
4355
|
+
* This handles cases where the DC connection is broken but no error events are fired.
|
|
4356
|
+
*/
|
|
4357
|
+
tryReconnectOnSendFailure() {
|
|
4358
|
+
// Skip if already reconnecting or signaling is not connected
|
|
4359
|
+
if (this.isReconnecting()) {
|
|
4360
|
+
Logger.info('[Reconnect] Already reconnecting, skipping duplicate attempt');
|
|
4839
4361
|
return;
|
|
4840
4362
|
}
|
|
4841
|
-
|
|
4842
|
-
|
|
4843
|
-
|
|
4844
|
-
|
|
4845
|
-
|
|
4846
|
-
this.
|
|
4847
|
-
|
|
4848
|
-
|
|
4849
|
-
|
|
4850
|
-
|
|
4851
|
-
[...this.data].reverse().forEach((val, index) => {
|
|
4852
|
-
const h = (val || 0) * scaleY;
|
|
4853
|
-
this.ctx.fillStyle = this.getColor(this.color(), h);
|
|
4854
|
-
this.ctx.fillRect(index * this.tickStep(), this.canvas.height - h, this.tickStep(), h);
|
|
4855
|
-
});
|
|
4856
|
-
this.ctx.restore();
|
|
4363
|
+
if (!this.signallingSrv.isConnected()) {
|
|
4364
|
+
Logger.warn('[Reconnect] Cannot reconnect: Signaling not connected');
|
|
4365
|
+
return;
|
|
4366
|
+
}
|
|
4367
|
+
// Check if DC exists but is in a non-open state (closing, closed, or connecting stuck)
|
|
4368
|
+
const dcState = this.dcClient?.readyState;
|
|
4369
|
+
if (dcState && dcState !== 'open') {
|
|
4370
|
+
Logger.warn(`[Reconnect] DataChannel in "${dcState}" state during send, initiating peer reconnection`);
|
|
4371
|
+
this.store.dispatch(reconnectPeer({ attempt: 1, maxAttempts: 3, delay: 0 }));
|
|
4372
|
+
}
|
|
4857
4373
|
}
|
|
4858
|
-
|
|
4859
|
-
|
|
4860
|
-
|
|
4861
|
-
|
|
4862
|
-
|
|
4863
|
-
|
|
4864
|
-
|
|
4865
|
-
|
|
4374
|
+
/**
|
|
4375
|
+
* Reconnects only the WebRTC peer connection without re-establishing signaling.
|
|
4376
|
+
* Closes the current PeerConnection and creates a new client-initiated offer
|
|
4377
|
+
* through the existing WebSocket connection.
|
|
4378
|
+
* @returns true if reconnection was initiated, false if signaling is not connected
|
|
4379
|
+
*/
|
|
4380
|
+
reconnectPeer() {
|
|
4381
|
+
if (!this.signallingSrv.isConnected()) {
|
|
4382
|
+
Logger.error('[Reconnect] Cannot reconnect peer: Signaling not connected');
|
|
4383
|
+
return false;
|
|
4384
|
+
}
|
|
4385
|
+
if (!this.cfg || Object.keys(this.cfg).length === 0) {
|
|
4386
|
+
Logger.error('[Reconnect] Cannot reconnect peer: No config available');
|
|
4387
|
+
return false;
|
|
4866
4388
|
}
|
|
4389
|
+
Logger.info('[Reconnect] Initiating peer reconnection');
|
|
4390
|
+
// Close only the PeerConnection, keep WebSocket alive
|
|
4391
|
+
this.closePC();
|
|
4392
|
+
// Create a new WebRTC offer using the stored config
|
|
4393
|
+
void this.createWebRtcOffer();
|
|
4394
|
+
return true;
|
|
4867
4395
|
}
|
|
4868
|
-
|
|
4869
|
-
|
|
4870
|
-
|
|
4871
|
-
|
|
4396
|
+
setConfig(parOptions = {}) {
|
|
4397
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
4398
|
+
//**********************
|
|
4399
|
+
//Config setup
|
|
4400
|
+
//**********************
|
|
4401
|
+
this.cfg = { ...parOptions.peerConnectionOptions };
|
|
4402
|
+
this.forceTURN = urlParams.has('ForceTURN');
|
|
4403
|
+
if (this.forceTURN) {
|
|
4404
|
+
Logger.info('Forcing TURN usage by setting ICE Transport Policy in peer connection config.');
|
|
4405
|
+
this.cfg.iceTransportPolicy = 'relay';
|
|
4872
4406
|
}
|
|
4873
|
-
|
|
4874
|
-
|
|
4407
|
+
this.cfg.bundlePolicy = urlParams.has('ForceMaxBundle')
|
|
4408
|
+
? 'max-bundle'
|
|
4409
|
+
: 'balanced';
|
|
4410
|
+
this.cfg.iceCandidatePoolSize = 1;
|
|
4411
|
+
// When ?useMic check for SSL or localhost
|
|
4412
|
+
const isLocalhostConnection = location.hostname === 'localhost' || location.hostname === '127.0.0.1';
|
|
4413
|
+
const isHttpsConnection = location.protocol === 'https:';
|
|
4414
|
+
if (this.useMic && !isLocalhostConnection && !isHttpsConnection) {
|
|
4415
|
+
this.useMic = false;
|
|
4416
|
+
Logger.error('Microphone access in the browser will not work if you are not on HTTPS or localhost. Disabling mic access.');
|
|
4417
|
+
Logger.error("For testing you can enable HTTP microphone access Chrome by visiting chrome://flags/ and enabling 'unsafely-treat-insecure-origin-as-secure'");
|
|
4875
4418
|
}
|
|
4876
|
-
this.
|
|
4419
|
+
this.setupPeerConnection(this.cfg);
|
|
4877
4420
|
}
|
|
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
|
-
|
|
4421
|
+
handlePeerConnectionEvents(pcClient) {
|
|
4422
|
+
fromEvent(pcClient, 'signalingstatechange')
|
|
4423
|
+
.pipe(map$1(() => this.pcClient?.signalingState), takeUntil(this.disconnect$))
|
|
4424
|
+
.subscribe((state) => {
|
|
4425
|
+
Logger.info('[Signaling state change] |', state, '|');
|
|
4426
|
+
this.commandTelemetryService.trackStopCommand('EXT-Signaling state change', true, { state, multi: true });
|
|
4427
|
+
});
|
|
4428
|
+
fromEvent(pcClient, 'iceconnectionstatechange')
|
|
4429
|
+
.pipe(map$1(() => this.pcClient?.iceConnectionState), takeUntil(this.disconnect$))
|
|
4430
|
+
.subscribe((state) => {
|
|
4431
|
+
Logger.info('[Browser ICE connection] |', state, '|');
|
|
4432
|
+
this.commandTelemetryService.trackStopCommand('EXT-Browser ICE connection', true, { state, multi: true });
|
|
4433
|
+
});
|
|
4434
|
+
fromEvent(pcClient, 'icegatheringstatechange')
|
|
4435
|
+
.pipe(map$1(() => this.pcClient?.iceGatheringState), takeUntil(this.disconnect$))
|
|
4436
|
+
.subscribe((state) => {
|
|
4437
|
+
Logger.info('[Browser ICE gathering] |', state, '|');
|
|
4438
|
+
this.commandTelemetryService.trackStopCommand('EXT-Browser ICE gathering', true, { state, multi: true });
|
|
4439
|
+
});
|
|
4440
|
+
fromEvent(pcClient, 'connectionstatechange')
|
|
4441
|
+
.pipe(map$1(() => this.pcClient?.connectionState), takeUntil(this.disconnect$))
|
|
4442
|
+
.subscribe((state) => {
|
|
4443
|
+
Logger.info('[RTCPeerConnection state] |', state, '|');
|
|
4444
|
+
});
|
|
4445
|
+
fromEvent(pcClient, 'icecandidate')
|
|
4446
|
+
.pipe(map$1((e) => e.candidate), filter(Truthy), tap$1((candidate) => Logger.info('[Browser ICE candidate]', '| Type=', candidate.type, '| Protocol=', candidate.protocol, '| Address=', candidate.address, '| Port=', candidate.port, '|')), takeUntil(this.disconnect$))
|
|
4447
|
+
.subscribe((candidate) => this.signallingSrv.send({ type: 'iceCandidate', candidate }));
|
|
4448
|
+
// When the server creates the DataChannel (server-offer flow),
|
|
4449
|
+
// we receive it here via the 'datachannel' event
|
|
4450
|
+
fromEvent(pcClient, 'datachannel')
|
|
4451
|
+
.pipe(tap$1((e) => Logger.info(`[DATACHANNEL] Received remote DC: label="${e.channel.label}" id=${e.channel.id} protocol="${e.channel.protocol}" ordered=${e.channel.ordered}`)), map$1((e) => e.channel), takeUntil(this.disconnect$))
|
|
4452
|
+
.subscribe((datachannel) => {
|
|
4453
|
+
this.dcClient = this.setupDataChannelCallbacks(datachannel);
|
|
4454
|
+
});
|
|
4455
|
+
const onTrack$ = fromEvent(pcClient, 'track').pipe(takeUntil(this.disconnect$), filter((e) => !!e.track), tap$1((e) => Logger.info('Got track - ' +
|
|
4456
|
+
e.track.kind +
|
|
4457
|
+
' id=' +
|
|
4458
|
+
e.track.id +
|
|
4459
|
+
' readyState=' +
|
|
4460
|
+
e.track.readyState)), share());
|
|
4461
|
+
onTrack$
|
|
4462
|
+
.pipe(filter((e) => e.track.kind === 'audio'), map$1((e) => e.streams[0]))
|
|
4463
|
+
.subscribe((stream) => this.videoService.attachAudioStream(stream));
|
|
4464
|
+
onTrack$
|
|
4465
|
+
.pipe(filter((e) => e.track.kind === 'video'), map$1((e) => e.streams[0]), tap$1(() => Logger.info('Set video source from video track ontrack.')))
|
|
4466
|
+
.subscribe((stream) => {
|
|
4467
|
+
if (this.pcClient) {
|
|
4468
|
+
this.videoService.attachVideoStream(stream, this.pcClient);
|
|
4469
|
+
}
|
|
4470
|
+
});
|
|
4471
|
+
}
|
|
4472
|
+
async setupTransceiversAsync(pc) {
|
|
4473
|
+
const hasTransceivers = pc.getTransceivers().length > 0;
|
|
4474
|
+
// Set up a transceiver for getting UE video
|
|
4475
|
+
pc.addTransceiver('video', { direction: 'recvonly' });
|
|
4476
|
+
// Set up a transceiver for sending mic audio to UE and receiving audio from UE
|
|
4477
|
+
if (!this.useMic) {
|
|
4478
|
+
pc.addTransceiver('audio', { direction: 'recvonly' });
|
|
4479
|
+
}
|
|
4480
|
+
else {
|
|
4481
|
+
const audioSendOptions = this.useMic
|
|
4482
|
+
? {
|
|
4483
|
+
autoGainControl: false,
|
|
4484
|
+
channelCount: 1,
|
|
4485
|
+
echoCancellation: false,
|
|
4486
|
+
latency: 0,
|
|
4487
|
+
noiseSuppression: false,
|
|
4488
|
+
sampleRate: 48000,
|
|
4489
|
+
sampleSize: 16,
|
|
4490
|
+
volume: 1.0,
|
|
4491
|
+
}
|
|
4492
|
+
: false;
|
|
4493
|
+
// Note using mic on android chrome requires SSL or chrome://flags/ "unsafely-treat-insecure-origin-as-secure"
|
|
4494
|
+
const stream = await navigator.mediaDevices.getUserMedia({
|
|
4495
|
+
video: false,
|
|
4496
|
+
audio: audioSendOptions,
|
|
4497
|
+
});
|
|
4498
|
+
if (stream) {
|
|
4499
|
+
if (hasTransceivers) {
|
|
4500
|
+
for (const transceiver of pc.getTransceivers()) {
|
|
4501
|
+
if (transceiver &&
|
|
4502
|
+
transceiver.receiver &&
|
|
4503
|
+
transceiver.receiver.track &&
|
|
4504
|
+
transceiver.receiver.track.kind === 'audio') {
|
|
4505
|
+
for (const track of stream.getTracks()) {
|
|
4506
|
+
if (track.kind && track.kind === 'audio') {
|
|
4507
|
+
await transceiver.sender.replaceTrack(track);
|
|
4508
|
+
transceiver.direction = 'sendrecv';
|
|
4509
|
+
}
|
|
4510
|
+
}
|
|
4511
|
+
}
|
|
4512
|
+
}
|
|
4513
|
+
}
|
|
4514
|
+
else {
|
|
4515
|
+
for (const track of stream.getTracks()) {
|
|
4516
|
+
if (track.kind && track.kind === 'audio') {
|
|
4517
|
+
pc.addTransceiver(track, { direction: 'sendrecv' });
|
|
4518
|
+
}
|
|
4519
|
+
}
|
|
4520
|
+
}
|
|
4521
|
+
}
|
|
4522
|
+
else {
|
|
4523
|
+
pc.addTransceiver('audio', { direction: 'recvonly' });
|
|
4524
|
+
}
|
|
4525
|
+
}
|
|
4526
|
+
}
|
|
4527
|
+
setupDataChannelCallbacks(datachannel) {
|
|
4528
|
+
try {
|
|
4529
|
+
Logger.log(`[DATACHANNEL] Setting up datachannel "${datachannel.label}" callbacks...`);
|
|
4530
|
+
this.commandTelemetryService.trackStartCommand('EXT-DataChannel Connection');
|
|
4531
|
+
// Inform browser we would like binary data as an ArrayBuffer (FF chooses Blob by default!)
|
|
4532
|
+
datachannel.binaryType = 'arraybuffer';
|
|
4533
|
+
fromEvent(datachannel, 'close')
|
|
4534
|
+
.pipe(
|
|
4535
|
+
// Only dispatch disconnect if not already reconnecting to avoid infinite loops
|
|
4536
|
+
filter(() => !this.isReconnecting()), take(1))
|
|
4537
|
+
.subscribe((e) => {
|
|
4538
|
+
Logger.warn(`[DATACHANNEL] Data channel disconnected: ${datachannel.label}(${datachannel.id})`, e);
|
|
4539
|
+
this.store.dispatch(disconnectStream({
|
|
4540
|
+
reason: DisconnectReason.DataChannelClosed,
|
|
4541
|
+
message: 'DataChannelClosed',
|
|
4542
|
+
}));
|
|
4543
|
+
});
|
|
4544
|
+
fromEvent(datachannel, 'error')
|
|
4545
|
+
.pipe(takeUntil(this.disconnect$))
|
|
4546
|
+
.subscribe((e) => {
|
|
4547
|
+
const message = `[DATACHANNEL] Data channel "${datachannel.label}" error. id:${datachannel.id}`;
|
|
4548
|
+
Logger.error(message, e);
|
|
4549
|
+
});
|
|
4550
|
+
fromEvent(datachannel, 'open')
|
|
4551
|
+
.pipe(tap$1(() => {
|
|
4552
|
+
Logger.info(`[DATACHANNEL] Data channel "${datachannel.label}" connected. id:${datachannel.id}`);
|
|
4553
|
+
}), takeUntil(this.disconnect$))
|
|
4554
|
+
.subscribe(() => {
|
|
4555
|
+
this.commandTelemetryService.trackStopCommand('EXT-DataChannel Connection');
|
|
4556
|
+
this.onDataChannelConnected();
|
|
4557
|
+
});
|
|
4558
|
+
fromEvent(datachannel, 'message')
|
|
4559
|
+
.pipe(
|
|
4560
|
+
/* tap((e: MessageEvent) => {
|
|
4561
|
+
const view = new Uint8Array(e.data);
|
|
4562
|
+
Logger.info(
|
|
4563
|
+
`[DATACHANNEL] Message received on "${datachannel.label}" id=${datachannel.id}, byteCode=${view[0]}, size=${view.byteLength}`,
|
|
4564
|
+
);
|
|
4565
|
+
}),*/
|
|
4566
|
+
map$1((e) => e.data), takeUntil(this.disconnect$))
|
|
4567
|
+
.subscribe((data) => this.onDataChannelMessage$.next(data));
|
|
4568
|
+
return datachannel;
|
|
4569
|
+
}
|
|
4570
|
+
catch (e) {
|
|
4571
|
+
Logger.warn('[DATACHANNEL] Datachannel setup caused an exception: ', e);
|
|
4572
|
+
return null;
|
|
4573
|
+
}
|
|
4903
4574
|
}
|
|
4904
|
-
|
|
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));
|
|
4575
|
+
onDataChannelConnected() {
|
|
4576
|
+
let statusMessage = 'Session connected, video stream starting.';
|
|
4577
|
+
if (navigator.userAgent.includes('Firefox')) {
|
|
4578
|
+
statusMessage +=
|
|
4579
|
+
'\n If you are experiencing connection problems please try Google Chrome';
|
|
4580
|
+
}
|
|
4581
|
+
this.store.dispatch(dataChannelConnected({ statusMessage }));
|
|
4942
4582
|
}
|
|
4943
4583
|
/**
|
|
4944
|
-
*
|
|
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.
|
|
4584
|
+
* Creates an SDP offer, applies SDP munging, sets it as local description,
|
|
4585
|
+
* and sends it to the server via signaling.
|
|
4586
|
+
* Transceivers already configure media directions, so deprecated
|
|
4587
|
+
* offerToReceive* options are not used.
|
|
4950
4588
|
*/
|
|
4951
|
-
|
|
4952
|
-
const objectElement = document.getElementById(this.unrealInitialConfig?.screenLockerContainerId ||
|
|
4953
|
-
SCREEN_LOCKER_CONTAINER_ID);
|
|
4589
|
+
async createOffer(pcClient) {
|
|
4954
4590
|
try {
|
|
4955
|
-
const
|
|
4956
|
-
|
|
4591
|
+
const offer = await pcClient.createOffer();
|
|
4592
|
+
offer.sdp = this.mungeSDP(offer.sdp ?? '');
|
|
4593
|
+
await pcClient.setLocalDescription(offer);
|
|
4594
|
+
this.signallingSrv.send(offer);
|
|
4957
4595
|
}
|
|
4958
4596
|
catch {
|
|
4959
|
-
|
|
4597
|
+
Logger.error("Couldn't create offer");
|
|
4960
4598
|
}
|
|
4961
4599
|
}
|
|
4962
|
-
|
|
4963
|
-
|
|
4600
|
+
/**
|
|
4601
|
+
* Creates an SDP answer, applies SDP munging, sets it as local description,
|
|
4602
|
+
* and sends it to the server via signaling.
|
|
4603
|
+
*/
|
|
4604
|
+
async createAnswer(pcClient) {
|
|
4605
|
+
try {
|
|
4606
|
+
const answer = await pcClient.createAnswer();
|
|
4607
|
+
answer.sdp = this.mungeSDP(answer.sdp ?? '');
|
|
4608
|
+
await pcClient.setLocalDescription(answer);
|
|
4609
|
+
this.signallingSrv.send(answer);
|
|
4610
|
+
}
|
|
4611
|
+
catch (e) {
|
|
4612
|
+
Logger.error("Couldn't create answer", e);
|
|
4613
|
+
}
|
|
4614
|
+
}
|
|
4615
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: WebRtcPlayerService2, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
4616
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: WebRtcPlayerService2 }); }
|
|
4964
4617
|
}
|
|
4965
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type:
|
|
4966
|
-
type:
|
|
4967
|
-
|
|
4618
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: WebRtcPlayerService2, decorators: [{
|
|
4619
|
+
type: Injectable
|
|
4620
|
+
}], ctorParameters: () => [] });
|
|
4621
|
+
|
|
4622
|
+
class AfkPlaywrightService extends AFKService {
|
|
4623
|
+
init() {
|
|
4624
|
+
return;
|
|
4625
|
+
}
|
|
4626
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: AfkPlaywrightService, deps: null, target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
4627
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: AfkPlaywrightService }); }
|
|
4628
|
+
}
|
|
4629
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: AfkPlaywrightService, decorators: [{
|
|
4630
|
+
type: Injectable
|
|
4968
4631
|
}] });
|
|
4969
4632
|
|
|
4970
|
-
class
|
|
4633
|
+
class SignallingPlaywrightService extends SignallingService {
|
|
4971
4634
|
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();
|
|
4635
|
+
super();
|
|
5013
4636
|
}
|
|
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();
|
|
4637
|
+
init() {
|
|
4638
|
+
return;
|
|
5044
4639
|
}
|
|
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))));
|
|
4640
|
+
connectToSignaling() {
|
|
4641
|
+
this.store.dispatch(setUnrealPlaywrightConfig());
|
|
4642
|
+
this.store.dispatch(dataChannelConnected({
|
|
4643
|
+
statusMessage: 'Playwright set data channel connected',
|
|
4644
|
+
}));
|
|
4645
|
+
return EMPTY;
|
|
5058
4646
|
}
|
|
5059
|
-
|
|
5060
|
-
|
|
5061
|
-
|
|
4647
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: SignallingPlaywrightService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
4648
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: SignallingPlaywrightService }); }
|
|
4649
|
+
}
|
|
4650
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: SignallingPlaywrightService, decorators: [{
|
|
4651
|
+
type: Injectable
|
|
4652
|
+
}], ctorParameters: () => [] });
|
|
4653
|
+
|
|
4654
|
+
class UnrealCommunicatorPlaywrightService extends UnrealCommunicatorService {
|
|
4655
|
+
init() {
|
|
4656
|
+
return;
|
|
5062
4657
|
}
|
|
5063
|
-
|
|
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();
|
|
4658
|
+
sendCommandToUnreal() {
|
|
4659
|
+
return;
|
|
5077
4660
|
}
|
|
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
|
-
}
|
|
4661
|
+
sendInputData() {
|
|
4662
|
+
return;
|
|
5087
4663
|
}
|
|
5088
|
-
|
|
5089
|
-
|
|
5090
|
-
|
|
5091
|
-
|
|
5092
|
-
|
|
4664
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: UnrealCommunicatorPlaywrightService, deps: null, target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
4665
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: UnrealCommunicatorPlaywrightService }); }
|
|
4666
|
+
}
|
|
4667
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: UnrealCommunicatorPlaywrightService, decorators: [{
|
|
4668
|
+
type: Injectable
|
|
4669
|
+
}] });
|
|
4670
|
+
|
|
4671
|
+
class AggregatorPlaywrightService extends AggregatorService {
|
|
4672
|
+
init() {
|
|
4673
|
+
return;
|
|
5093
4674
|
}
|
|
5094
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type:
|
|
5095
|
-
static { this.ɵ
|
|
4675
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: AggregatorPlaywrightService, deps: null, target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
4676
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: AggregatorPlaywrightService }); }
|
|
5096
4677
|
}
|
|
5097
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type:
|
|
5098
|
-
type:
|
|
5099
|
-
|
|
5100
|
-
}], ctorParameters: () => [] });
|
|
4678
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: AggregatorPlaywrightService, decorators: [{
|
|
4679
|
+
type: Injectable
|
|
4680
|
+
}] });
|
|
5101
4681
|
|
|
5102
|
-
class
|
|
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());
|
|
4682
|
+
class FreezeFramePlaywrightService extends FreezeFrameService {
|
|
4683
|
+
init() {
|
|
4684
|
+
return;
|
|
5122
4685
|
}
|
|
5123
|
-
|
|
5124
|
-
|
|
5125
|
-
|
|
5126
|
-
|
|
5127
|
-
|
|
5128
|
-
|
|
5129
|
-
|
|
5130
|
-
|
|
4686
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: FreezeFramePlaywrightService, deps: null, target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
4687
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: FreezeFramePlaywrightService }); }
|
|
4688
|
+
}
|
|
4689
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: FreezeFramePlaywrightService, decorators: [{
|
|
4690
|
+
type: Injectable
|
|
4691
|
+
}] });
|
|
4692
|
+
|
|
4693
|
+
class CommandTelemetryPlaywrightService extends CommandTelemetryService {
|
|
4694
|
+
init() {
|
|
4695
|
+
return;
|
|
5131
4696
|
}
|
|
5132
|
-
|
|
5133
|
-
|
|
4697
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: CommandTelemetryPlaywrightService, deps: null, target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
4698
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: CommandTelemetryPlaywrightService }); }
|
|
4699
|
+
}
|
|
4700
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: CommandTelemetryPlaywrightService, decorators: [{
|
|
4701
|
+
type: Injectable
|
|
4702
|
+
}] });
|
|
4703
|
+
|
|
4704
|
+
class StreamStatusTelemetryPlaywrightService extends StreamStatusTelemetryService {
|
|
4705
|
+
init() {
|
|
4706
|
+
return;
|
|
5134
4707
|
}
|
|
5135
|
-
|
|
5136
|
-
|
|
4708
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: StreamStatusTelemetryPlaywrightService, deps: null, target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
4709
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: StreamStatusTelemetryPlaywrightService }); }
|
|
4710
|
+
}
|
|
4711
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: StreamStatusTelemetryPlaywrightService, decorators: [{
|
|
4712
|
+
type: Injectable
|
|
4713
|
+
}] });
|
|
4714
|
+
|
|
4715
|
+
class ConsoleExtensionsPlaywrightService extends ConsoleExtensionsService {
|
|
4716
|
+
init() {
|
|
4717
|
+
return;
|
|
5137
4718
|
}
|
|
5138
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type:
|
|
5139
|
-
static { this.ɵ
|
|
4719
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ConsoleExtensionsPlaywrightService, deps: null, target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
4720
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ConsoleExtensionsPlaywrightService }); }
|
|
5140
4721
|
}
|
|
5141
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type:
|
|
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"] }]
|
|
4722
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ConsoleExtensionsPlaywrightService, decorators: [{
|
|
4723
|
+
type: Injectable
|
|
5144
4724
|
}] });
|
|
5145
4725
|
|
|
5146
|
-
class
|
|
4726
|
+
class FileReceiverPlaywrightService extends FileReceiverService {
|
|
4727
|
+
init() {
|
|
4728
|
+
return;
|
|
4729
|
+
}
|
|
4730
|
+
}
|
|
4731
|
+
|
|
4732
|
+
/**
|
|
4733
|
+
* Adaptive FPS controller that monitors the incoming WebRTC video stream
|
|
4734
|
+
* quality and dynamically adjusts Unreal Engine's `t.MaxFPS` console variable.
|
|
4735
|
+
*
|
|
4736
|
+
* ## How it works
|
|
4737
|
+
*
|
|
4738
|
+
* The service subscribes to `VideoService.videoStats$` (emitted every 250 ms)
|
|
4739
|
+
* and collects samples of FPS, bitrate, and QP (Quantisation Parameter).
|
|
4740
|
+
* Every {@link SAMPLE_WINDOW} ticks (~2 s) it averages the collected samples
|
|
4741
|
+
* and feeds them into a set of **fuzzy-logic membership functions** that
|
|
4742
|
+
* classify each metric into overlapping quality bands.
|
|
4743
|
+
*
|
|
4744
|
+
* ### Input signals
|
|
4745
|
+
*
|
|
4746
|
+
* **1. FPS ratio** = `actualFPS / currentTargetFPS` (0..1+)
|
|
4747
|
+
*
|
|
4748
|
+
* | Band | Range | Meaning |
|
|
4749
|
+
* |--------|---------------------|-----------------------------------------------|
|
|
4750
|
+
* | low | ratio < 0.7 → 1.0, linear ramp 0.7..0.9 → 0 | Stream cannot keep up with the target |
|
|
4751
|
+
* | ok | triangle 0.8..1.0, peak at 0.95 | Stream roughly matches the target |
|
|
4752
|
+
* | high | ratio 0.9..1.0 → ramp, >1.0 → 1.0 | Stream meets or exceeds the target |
|
|
4753
|
+
*
|
|
4754
|
+
* **2. Bitrate load** = `currentBitrate / MAX_BITRATE` (0..1+)
|
|
4755
|
+
*
|
|
4756
|
+
* `MAX_BITRATE` = 20 Mbit/s — the reference ceiling for the encoder.
|
|
4757
|
+
*
|
|
4758
|
+
* | Band | Range | Meaning |
|
|
4759
|
+
* |--------|-------------------------------------------------|-----------------------------------------------|
|
|
4760
|
+
* | high | load 0.5..0.8 → ramp, >0.8 → 1.0 | Encoder is using most of the available bandwidth |
|
|
4761
|
+
* | not high | load < 0.5 → 0.0 | Bandwidth headroom is available |
|
|
4762
|
+
*
|
|
4763
|
+
* **3. Quality** — mapped from QP via `mapQpToQuality()` (see {@link Quality}):
|
|
4764
|
+
*
|
|
4765
|
+
* | Quality | QP range | Meaning |
|
|
4766
|
+
* |----------|-----------|-------------------------------------------------|
|
|
4767
|
+
* | `lime` | QP <= 26 | Good — encoder has enough headroom |
|
|
4768
|
+
* | `orange` | QP 27..35 | Fair — encoder is under moderate pressure |
|
|
4769
|
+
* | `red` | QP > 35 | Poor — heavy compression, visible artefacts |
|
|
4770
|
+
*
|
|
4771
|
+
* ### Decision rules (evaluated in priority order, first match wins)
|
|
4772
|
+
*
|
|
4773
|
+
* | # | Condition | Decision |
|
|
4774
|
+
* |---|------------------------------------------------------------|--------------|
|
|
4775
|
+
* | 1 | FPS ratio is **low** (membership > 0.5) | **decrease** |
|
|
4776
|
+
* | 2 | FPS ratio is **ok** AND quality is `red` | **decrease** |
|
|
4777
|
+
* | 3 | FPS ratio is **ok** AND bitrate **high** AND quality is `orange` | **hold** |
|
|
4778
|
+
* | 4 | FPS ratio is **high** AND quality is `lime` AND bitrate **not high** | **increase** |
|
|
4779
|
+
* | — | Everything else | **hold** |
|
|
4780
|
+
*
|
|
4781
|
+
* ## Cooldown-based throttling
|
|
4782
|
+
*
|
|
4783
|
+
* The service continuously evaluates fuzzy rules every sample window.
|
|
4784
|
+
* After each FPS step change it enforces a cooldown before the next
|
|
4785
|
+
* change can be applied. The cooldown duration depends on the
|
|
4786
|
+
* (previous decision → next decision) pair:
|
|
4787
|
+
*
|
|
4788
|
+
* | Previous | Next | Cooldown |
|
|
4789
|
+
* |-----------|-----------|-----------------------------------------------------|
|
|
4790
|
+
* | decrease | decrease | {@link COOLDOWN_DECREASE_AFTER_DECREASE_MS} (2 s) |
|
|
4791
|
+
* | decrease | increase | {@link COOLDOWN_INCREASE_AFTER_DECREASE_MS} (10 s) |
|
|
4792
|
+
* | increase | decrease | {@link COOLDOWN_AFTER_INCREASE_MS} (2 s) |
|
|
4793
|
+
* | increase | increase | {@link COOLDOWN_AFTER_INCREASE_MS} (2 s) |
|
|
4794
|
+
*
|
|
4795
|
+
* This means the system reacts quickly after an increase (2 s in any
|
|
4796
|
+
* direction) but is cautious about stepping back up after a decrease
|
|
4797
|
+
* (10 s), while still allowing consecutive decreases at 2 s intervals.
|
|
4798
|
+
*
|
|
4799
|
+
* ## FPS dispatch
|
|
4800
|
+
*
|
|
4801
|
+
* FPS changes are dispatched via the NgRx `setMaxFps` action, which triggers
|
|
4802
|
+
* the existing `CommandsEffects.setMaxFps$` effect that sends the
|
|
4803
|
+
* `t.MaxFPS <value>` console command to Unreal Engine.
|
|
4804
|
+
*/
|
|
4805
|
+
class FpsMonitorService {
|
|
5147
4806
|
constructor() {
|
|
5148
|
-
this.
|
|
4807
|
+
this.videoService = inject(VideoService);
|
|
5149
4808
|
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
|
-
|
|
4809
|
+
/** Discrete FPS targets the controller can switch between (ascending order). */
|
|
4810
|
+
this.FPS_STEPS = [30, 40, 50, 60];
|
|
4811
|
+
/** Reference ceiling for bitrate (bits/sec) used to normalise bitrate load to 0..1. */
|
|
4812
|
+
this.MAX_BITRATE = 20_000_000;
|
|
4813
|
+
/**
|
|
4814
|
+
* Cooldown (ms) for any decision (increase or decrease) following an increase.
|
|
4815
|
+
* After an increase the system should react faster.
|
|
4816
|
+
*/
|
|
4817
|
+
this.COOLDOWN_AFTER_INCREASE_MS = 2_000;
|
|
4818
|
+
/**
|
|
4819
|
+
* Cooldown (ms) for a decrease following a previous decrease.
|
|
4820
|
+
*/
|
|
4821
|
+
this.COOLDOWN_DECREASE_AFTER_DECREASE_MS = 2_000;
|
|
4822
|
+
/**
|
|
4823
|
+
* Cooldown (ms) for an increase following a previous decrease.
|
|
4824
|
+
* The stream needs more time to stabilise before stepping back up.
|
|
4825
|
+
*/
|
|
4826
|
+
this.COOLDOWN_INCREASE_AFTER_DECREASE_MS = 10_000;
|
|
4827
|
+
/**
|
|
4828
|
+
* Number of videoStats$ ticks to accumulate before evaluating.
|
|
4829
|
+
* At 250 ms per tick this gives a ~2 s sliding evaluation window,
|
|
4830
|
+
* long enough to smooth out single-frame jitter.
|
|
4831
|
+
*/
|
|
4832
|
+
this.SAMPLE_WINDOW = 8;
|
|
4833
|
+
/** Current FPS target; starts at max (60 FPS) and adapts downward/upward. */
|
|
4834
|
+
this.currentFpsTarget = this.FPS_STEPS[this.FPS_STEPS.length - 1];
|
|
4835
|
+
this.samples = [];
|
|
4836
|
+
this.lastDecisionTime = Date.now();
|
|
4837
|
+
this.lastUpgrade = 'hold';
|
|
4838
|
+
this.init();
|
|
4839
|
+
}
|
|
4840
|
+
init() {
|
|
4841
|
+
this.videoService.videoStats$
|
|
4842
|
+
.pipe(withLatestFrom(this.store.select(unrealFeature.selectViewportReady)), filter(([, viewportReady]) => viewportReady))
|
|
4843
|
+
.subscribe(([stats]) => this.processTick(stats));
|
|
4844
|
+
}
|
|
4845
|
+
/**
|
|
4846
|
+
* Called on every videoStats$ emission (~250 ms).
|
|
4847
|
+
* Accumulates samples and evaluates once per SAMPLE_WINDOW.
|
|
4848
|
+
*/
|
|
4849
|
+
processTick(stats) {
|
|
4850
|
+
const { framesPerSecond, bitrate, VideoEncoderQP } = stats.aggregatedStats;
|
|
4851
|
+
this.samples.push({
|
|
4852
|
+
fps: framesPerSecond ?? 0,
|
|
4853
|
+
bitrate: bitrate ?? 0,
|
|
4854
|
+
qp: VideoEncoderQP ?? 0,
|
|
5170
4855
|
});
|
|
4856
|
+
// Wait until we have enough samples for a reliable average.
|
|
4857
|
+
if (this.samples.length < this.SAMPLE_WINDOW) {
|
|
4858
|
+
return;
|
|
4859
|
+
}
|
|
4860
|
+
const avg = this.averageSamples();
|
|
4861
|
+
this.samples = [];
|
|
4862
|
+
this.handleMonitoring(avg, this.currentFpsTarget);
|
|
5171
4863
|
}
|
|
5172
|
-
|
|
5173
|
-
const
|
|
5174
|
-
|
|
5175
|
-
|
|
5176
|
-
|
|
5177
|
-
|
|
5178
|
-
|
|
5179
|
-
|
|
5180
|
-
|
|
4864
|
+
handleMonitoring(avg, currentTarget) {
|
|
4865
|
+
const decision = this.evaluateDecision(avg, currentTarget);
|
|
4866
|
+
this.upgradeFps(decision, avg.qp);
|
|
4867
|
+
}
|
|
4868
|
+
/**
|
|
4869
|
+
* Combines fuzzy membership values into a single discrete decision.
|
|
4870
|
+
* Rules are evaluated in priority order — first match wins.
|
|
4871
|
+
*
|
|
4872
|
+
* Inputs:
|
|
4873
|
+
* - `fpsRatio` = avgFPS / currentTargetFPS (e.g. 45/60 = 0.75)
|
|
4874
|
+
* - `bitrateLoad` = avgBitrate / 20 Mbit/s (e.g. 12M/20M = 0.6)
|
|
4875
|
+
* - `quality` = mapQpToQuality(avgQP) → 'lime' | 'orange' | 'red'
|
|
4876
|
+
*/
|
|
4877
|
+
evaluateDecision(avg, currentTarget) {
|
|
4878
|
+
const fpsRatio = currentTarget > 0 ? avg.fps / currentTarget : 1;
|
|
4879
|
+
const bitrateLoad = avg.bitrate / this.MAX_BITRATE;
|
|
4880
|
+
const fpsLow = this.membershipFpsLow(fpsRatio);
|
|
4881
|
+
const fpsOk = this.membershipFpsOk(fpsRatio);
|
|
4882
|
+
const fpsHigh = this.membershipFpsHigh(fpsRatio);
|
|
4883
|
+
const bitrateHigh = this.membershipBitrateHigh(bitrateLoad);
|
|
4884
|
+
// Quality is derived from QP: lime (QP <= 26), orange (QP 27..35), red (QP > 35).
|
|
4885
|
+
const quality = qpToQuality(avg.qp);
|
|
4886
|
+
// Rule 1: FPS clearly below target (ratio roughly < 0.8) → step down immediately.
|
|
4887
|
+
if (fpsLow > 0.5) {
|
|
4888
|
+
return 'decrease';
|
|
5181
4889
|
}
|
|
5182
|
-
|
|
5183
|
-
|
|
5184
|
-
|
|
5185
|
-
|
|
5186
|
-
|
|
5187
|
-
if (matchAbsolutePath) {
|
|
5188
|
-
sourceElement.src = src;
|
|
4890
|
+
// Rule 2: FPS looks acceptable numerically, but encoder quality is 'red'
|
|
4891
|
+
// (QP > 35 — heavy compression artefacts). Reducing target FPS frees
|
|
4892
|
+
// encoder headroom and improves visual quality.
|
|
4893
|
+
if (fpsOk > 0.4 && quality === 'red') {
|
|
4894
|
+
return 'decrease';
|
|
5189
4895
|
}
|
|
5190
|
-
|
|
5191
|
-
|
|
4896
|
+
// Rule 3: FPS is ok, but bitrate is above ~50-80% of 20 Mbit/s ceiling
|
|
4897
|
+
// and quality is 'orange' (QP 27..35). Near capacity — don't push higher.
|
|
4898
|
+
if (fpsOk > 0.4 && bitrateHigh > 0.5 && quality === 'orange') {
|
|
4899
|
+
return 'hold';
|
|
5192
4900
|
}
|
|
5193
|
-
|
|
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();
|
|
4901
|
+
// Rule 4: FPS meets/exceeds target (ratio >= ~0.95), quality is 'lime'
|
|
4902
|
+
// (QP <= 26), and bitrate is below 50% of ceiling — safe to step up.
|
|
4903
|
+
if (fpsHigh > 0.5 &&
|
|
4904
|
+
quality === 'lime' &&
|
|
4905
|
+
bitrateHigh < 0.5 &&
|
|
4906
|
+
this.currentFpsTarget < this.FPS_STEPS[this.FPS_STEPS.length - 1]) {
|
|
4907
|
+
return 'increase';
|
|
5210
4908
|
}
|
|
5211
|
-
return
|
|
5212
|
-
}
|
|
5213
|
-
get pixelRatio() {
|
|
5214
|
-
return window.devicePixelRatio;
|
|
4909
|
+
return 'hold';
|
|
5215
4910
|
}
|
|
5216
|
-
|
|
5217
|
-
|
|
4911
|
+
// ---------------------------------------------------------------------------
|
|
4912
|
+
// Fuzzy membership functions
|
|
4913
|
+
//
|
|
4914
|
+
// Each function maps a normalised input (0..1+) to a membership degree (0..1).
|
|
4915
|
+
// Overlapping trapezoid/triangle shapes let multiple categories be partially
|
|
4916
|
+
// active at the same time, which makes the rules less brittle than hard thresholds.
|
|
4917
|
+
// ---------------------------------------------------------------------------
|
|
4918
|
+
/**
|
|
4919
|
+
* FPS ratio membership "low".
|
|
4920
|
+
* Input: ratio = actualFPS / targetFPS (e.g. 42/60 = 0.70).
|
|
4921
|
+
*
|
|
4922
|
+
* ratio < 0.7 → 1.0 (clearly failing, e.g. 40/60)
|
|
4923
|
+
* ratio 0.7..0.9 → linear ramp down (e.g. 0.8 → 0.5)
|
|
4924
|
+
* ratio > 0.9 → 0.0 (keeping up)
|
|
4925
|
+
*/
|
|
4926
|
+
membershipFpsLow(ratio) {
|
|
4927
|
+
if (ratio < 0.7)
|
|
4928
|
+
return 1.0;
|
|
4929
|
+
if (ratio > 0.9)
|
|
4930
|
+
return 0.0;
|
|
4931
|
+
return (0.9 - ratio) / 0.2;
|
|
5218
4932
|
}
|
|
5219
|
-
|
|
5220
|
-
|
|
4933
|
+
/**
|
|
4934
|
+
* FPS ratio membership "ok" — triangle shape.
|
|
4935
|
+
* Input: ratio = actualFPS / targetFPS.
|
|
4936
|
+
*
|
|
4937
|
+
* ratio < 0.8 → 0.0
|
|
4938
|
+
* ratio 0.8..0.95 → ramp up, peak at 0.95 (e.g. 57/60)
|
|
4939
|
+
* ratio 0.95..1.0 → ramp down
|
|
4940
|
+
* ratio > 1.0 → 0.0
|
|
4941
|
+
*/
|
|
4942
|
+
membershipFpsOk(ratio) {
|
|
4943
|
+
if (ratio < 0.8)
|
|
4944
|
+
return 0.0;
|
|
4945
|
+
if (ratio < 0.95)
|
|
4946
|
+
return (ratio - 0.8) / 0.15;
|
|
4947
|
+
if (ratio <= 1.0)
|
|
4948
|
+
return (1.0 - ratio) / 0.05;
|
|
4949
|
+
return 0.0;
|
|
5221
4950
|
}
|
|
5222
|
-
|
|
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
|
-
});
|
|
4951
|
+
/**
|
|
4952
|
+
* FPS ratio membership "high".
|
|
4953
|
+
* Input: ratio = actualFPS / targetFPS.
|
|
4954
|
+
*
|
|
4955
|
+
* ratio < 0.9 → 0.0
|
|
4956
|
+
* ratio 0.9..1.0 → linear ramp up (e.g. 0.95 → 0.5)
|
|
4957
|
+
* ratio >= 1.0 → 1.0 (meeting or exceeding target, e.g. 60/60)
|
|
4958
|
+
*/
|
|
4959
|
+
membershipFpsHigh(ratio) {
|
|
4960
|
+
if (ratio < 0.9)
|
|
4961
|
+
return 0.0;
|
|
4962
|
+
if (ratio > 1.0)
|
|
4963
|
+
return 1.0;
|
|
4964
|
+
return (ratio - 0.9) / 0.1;
|
|
5269
4965
|
}
|
|
5270
|
-
|
|
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));
|
|
4966
|
+
/**
|
|
4967
|
+
* Bitrate load membership "high".
|
|
4968
|
+
* Input: load = currentBitrate / MAX_BITRATE (20 Mbit/s).
|
|
4969
|
+
*
|
|
4970
|
+
* load < 0.5 → 0.0 (below 10 Mbit/s — plenty of headroom)
|
|
4971
|
+
* load 0.5..0.8 → linear ramp (e.g. 13M/20M = 0.65 → 0.5)
|
|
4972
|
+
* load > 0.8 → 1.0 (above 16 Mbit/s — near capacity)
|
|
4973
|
+
*/
|
|
4974
|
+
membershipBitrateHigh(load) {
|
|
4975
|
+
if (load < 0.5)
|
|
4976
|
+
return 0.0;
|
|
4977
|
+
if (load > 0.8)
|
|
4978
|
+
return 1.0;
|
|
4979
|
+
return (load - 0.5) / 0.3;
|
|
5299
4980
|
}
|
|
5300
|
-
|
|
5301
|
-
|
|
5302
|
-
|
|
5303
|
-
|
|
5304
|
-
|
|
5305
|
-
|
|
5306
|
-
|
|
5307
|
-
|
|
5308
|
-
|
|
5309
|
-
|
|
5310
|
-
|
|
5311
|
-
|
|
5312
|
-
|
|
4981
|
+
// ---------------------------------------------------------------------------
|
|
4982
|
+
// Step actions
|
|
4983
|
+
// ---------------------------------------------------------------------------
|
|
4984
|
+
upgradeFps(decision, qp) {
|
|
4985
|
+
if (decision === 'hold')
|
|
4986
|
+
return;
|
|
4987
|
+
const now = Date.now();
|
|
4988
|
+
// Throttle: cooldown depends on (previous decision → current decision) pair.
|
|
4989
|
+
const isStepUp = decision === 'increase';
|
|
4990
|
+
const cooldown = this.lastUpgrade === 'decrease'
|
|
4991
|
+
? isStepUp
|
|
4992
|
+
? this.COOLDOWN_INCREASE_AFTER_DECREASE_MS
|
|
4993
|
+
: this.COOLDOWN_DECREASE_AFTER_DECREASE_MS
|
|
4994
|
+
: this.COOLDOWN_AFTER_INCREASE_MS;
|
|
4995
|
+
if (now - this.lastDecisionTime < cooldown) {
|
|
4996
|
+
return;
|
|
5313
4997
|
}
|
|
5314
|
-
this.
|
|
4998
|
+
this.lastUpgrade = decision;
|
|
4999
|
+
const quality = qpToQuality(qp);
|
|
5000
|
+
const newTarget = this.getNextFpsTarget(this.currentFpsTarget, decision);
|
|
5001
|
+
Logger.info(`[FpsMonitor] Decision: ${decision} → FPS: ${newTarget}, Quality: ${quality}`);
|
|
5002
|
+
if (newTarget !== this.currentFpsTarget) {
|
|
5003
|
+
this.currentFpsTarget = newTarget;
|
|
5004
|
+
this.store.dispatch(setMaxFps({ maxFps: newTarget }));
|
|
5005
|
+
}
|
|
5006
|
+
this.lastDecisionTime = now;
|
|
5007
|
+
this.samples = [];
|
|
5008
|
+
}
|
|
5009
|
+
/**
|
|
5010
|
+
* Get the next FPS target based on current target and decision.
|
|
5011
|
+
* Clamps to valid FPS_STEPS range.
|
|
5012
|
+
*/
|
|
5013
|
+
getNextFpsTarget(currentTarget, decision) {
|
|
5014
|
+
const currentIndex = this.FPS_STEPS.indexOf(currentTarget);
|
|
5015
|
+
// If current target is not in FPS_STEPS, find closest one
|
|
5016
|
+
const validIndex = currentIndex === -1
|
|
5017
|
+
? this.findClosestFpsIndex(currentTarget)
|
|
5018
|
+
: currentIndex;
|
|
5019
|
+
const delta = decision === 'increase' ? 1 : -1;
|
|
5020
|
+
const newIndex = clampf(0, this.FPS_STEPS.length - 1, validIndex + delta);
|
|
5021
|
+
return this.FPS_STEPS[newIndex];
|
|
5315
5022
|
}
|
|
5316
5023
|
/**
|
|
5317
|
-
*
|
|
5318
|
-
* It takes into account the pixel ratio of the device's screen and the orientation of the layout.
|
|
5319
|
-
* The method uses the `fitIntoRectangle` function to calculate the dimensions that would allow the layout to fit into the maximum space while maintaining its aspect ratio.
|
|
5320
|
-
* It also sets the maximum limits for the width and height based on the layout's orientation.
|
|
5321
|
-
* If the scale of the fitted dimensions is less than 1, it assigns the fitted dimensions to the output. Otherwise, it sets the scale of the output to 1.
|
|
5322
|
-
* @returns {Area} An object of type `Area` that contains the calculated dimensions for resizing the layout.
|
|
5024
|
+
* Find the closest FPS step index for a given target value.
|
|
5323
5025
|
*/
|
|
5324
|
-
|
|
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;
|
|
5026
|
+
findClosestFpsIndex(target) {
|
|
5027
|
+
let closestIndex = 0;
|
|
5028
|
+
let closestDiff = Math.abs(this.FPS_STEPS[0] - target);
|
|
5029
|
+
for (let i = 1; i < this.FPS_STEPS.length; i++) {
|
|
5030
|
+
const diff = Math.abs(this.FPS_STEPS[i] - target);
|
|
5031
|
+
if (diff < closestDiff) {
|
|
5032
|
+
closestDiff = diff;
|
|
5033
|
+
closestIndex = i;
|
|
5034
|
+
}
|
|
5351
5035
|
}
|
|
5352
|
-
|
|
5353
|
-
|
|
5354
|
-
|
|
5036
|
+
return closestIndex;
|
|
5037
|
+
}
|
|
5038
|
+
/** Average all collected samples for the current evaluation window. */
|
|
5039
|
+
averageSamples() {
|
|
5040
|
+
const len = this.samples.length;
|
|
5041
|
+
if (len === 0)
|
|
5042
|
+
return { fps: 0, bitrate: 0, qp: 0 };
|
|
5043
|
+
let fps = 0;
|
|
5044
|
+
let bitrate = 0;
|
|
5045
|
+
let qp = 0;
|
|
5046
|
+
for (const s of this.samples) {
|
|
5047
|
+
fps += s.fps;
|
|
5048
|
+
bitrate += s.bitrate;
|
|
5049
|
+
qp += s.qp;
|
|
5355
5050
|
}
|
|
5356
|
-
|
|
5357
|
-
* Ensure that the size is always even.
|
|
5358
|
-
*/
|
|
5359
|
-
out.w = this.makeEven(out.w);
|
|
5360
|
-
out.h = this.makeEven(out.h);
|
|
5361
|
-
return { ...out };
|
|
5051
|
+
return { fps: fps / len, bitrate: bitrate / len, qp: qp / len };
|
|
5362
5052
|
}
|
|
5363
|
-
|
|
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;
|
|
5053
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: FpsMonitorService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
5054
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: FpsMonitorService }); }
|
|
5055
|
+
}
|
|
5056
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: FpsMonitorService, decorators: [{
|
|
5057
|
+
type: Injectable
|
|
5058
|
+
}], ctorParameters: () => [] });
|
|
5059
|
+
|
|
5060
|
+
class FpsMonitorPlaywrightService extends FpsMonitorService {
|
|
5061
|
+
init() {
|
|
5062
|
+
return;
|
|
5381
5063
|
}
|
|
5382
|
-
|
|
5383
|
-
|
|
5384
|
-
|
|
5385
|
-
|
|
5386
|
-
|
|
5064
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: FpsMonitorPlaywrightService, deps: null, target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
5065
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: FpsMonitorPlaywrightService }); }
|
|
5066
|
+
}
|
|
5067
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: FpsMonitorPlaywrightService, decorators: [{
|
|
5068
|
+
type: Injectable
|
|
5069
|
+
}] });
|
|
5070
|
+
|
|
5071
|
+
class InputPlaywrightService extends InputService {
|
|
5072
|
+
init() {
|
|
5073
|
+
return;
|
|
5387
5074
|
}
|
|
5388
|
-
|
|
5389
|
-
|
|
5075
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: InputPlaywrightService, deps: null, target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
5076
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: InputPlaywrightService }); }
|
|
5077
|
+
}
|
|
5078
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: InputPlaywrightService, decorators: [{
|
|
5079
|
+
type: Injectable
|
|
5080
|
+
}] });
|
|
5081
|
+
|
|
5082
|
+
const ReceivedMimeTypes = {
|
|
5083
|
+
ApplicationJson: 'application/json',
|
|
5084
|
+
};
|
|
5085
|
+
|
|
5086
|
+
class FileHandlerService {
|
|
5087
|
+
constructor() {
|
|
5088
|
+
this.fileService = inject(FileReceiverService);
|
|
5089
|
+
this.fileHandlers = {
|
|
5090
|
+
[ReceivedMimeTypes.ApplicationJson]: this.handleJsonFile.bind(this),
|
|
5091
|
+
};
|
|
5390
5092
|
}
|
|
5391
|
-
|
|
5392
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: UnrealSceneComponent, isStandalone: true, selector: "app-unreal-scene", inputs: { isStudio: { classPropertyName: "isStudio", publicName: "isStudio", isSignal: true, isRequired: false, transformFunction: null }, useContainerAsSizeProvider: { classPropertyName: "useContainerAsSizeProvider", publicName: "useContainerAsSizeProvider", isSignal: true, isRequired: false, transformFunction: null }, studioResolutionSize: { classPropertyName: "studioResolutionSize", publicName: "studioResolutionSize", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { changeMouseOverScene: "changeMouseOverScene" }, host: { listeners: { "mouseover": "onMouseOver()", "mouseout": "onMouseOut()" } }, viewQueries: [{ propertyName: "videoElement", first: true, predicate: ["videoContainer"], descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"frame unreal-container\">\n <div #videoContainer [class.lightModeCursor]=\"lightMode()\" id=\"player\"></div>\n\n <app-intro-src />\n <app-image-loading-src />\n <app-resume-stream-button />\n <app-unreal-status />\n <app-freeze-frame />\n <app-afk-timeout-modal />\n\n <ng-content select=\"app-pdf\"></ng-content>\n <ng-content select=\"app-show-case\"></ng-content>\n <ng-content select=\"app-low-bandwidth-detector\"></ng-content>\n\n @if (isDevMode) {\n <app-video-stats />\n }\n</div>\n", styles: [".unreal-container{position:absolute;top:0;left:0;width:100%;height:100%;background-color:#fff}.unreal-container #player{position:absolute;width:100%;height:100%;display:flex;justify-content:center;align-items:center;overflow:hidden}.unreal-container.select{cursor:url('data:image/svg+xml,<svg width=\"24\" height=\"24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M15.15 21.375a1.423 1.423 0 0 1-1.15.063 1.487 1.487 0 0 1-.85-.788l-3-6.45-2.325 3.25c-.283.4-.658.525-1.125.375-.467-.15-.7-.466-.7-.95V4.05c0-.417.188-.717.563-.9.375-.183.729-.142 1.062.125l10.1 7.95c.383.284.496.65.337 1.1-.158.45-.479.675-.962.675h-4.2l2.975 6.375c.183.384.204.767.063 1.15a1.487 1.487 0 0 1-.788.85Z\" fill=\"%23000\"/><path d=\"m12.697 20.861.002.005c.237.495.617.852 1.128 1.04.515.191 1.039.16 1.539-.08a1.987 1.987 0 0 0 1.04-1.128 1.922 1.922 0 0 0-.079-1.536v-.003L13.684 13.5H17.1c.32 0 .628-.075.89-.26.263-.184.438-.447.544-.749.106-.3.135-.617.04-.925a1.458 1.458 0 0 0-.545-.738L7.936 2.883a1.51 1.51 0 0 0-.768-.336 1.471 1.471 0 0 0-.825.154 1.495 1.495 0 0 0-.626.547 1.492 1.492 0 0 0-.217.802v12.825c0 .324.08.634.272.897.193.261.467.43.775.53.31.099.632.12.942.016.31-.103.555-.313.743-.578h.001l1.825-2.552 2.639 5.673Z\" stroke=\"%23fff\" stroke-opacity=\".7\"/></svg>') 6 2,auto}.unreal-container.orbit{cursor:url('data:image/svg+xml,<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" fill=\"none\"><path fill=\"%23000\" d=\"M11.998 21.6c-1.966 0-3.737-.53-5.312-1.588-1.575-1.058-2.738-2.437-3.488-4.137a.852.852 0 0 1 .013-.713.867.867 0 0 1 .512-.487.86.86 0 0 1 .688.037c.225.109.387.28.487.513.617 1.35 1.559 2.45 2.825 3.3 1.267.85 2.692 1.275 4.275 1.275 1.384 0 2.646-.33 3.788-.988a7.985 7.985 0 0 0 2.762-2.612h-.85a.873.873 0 0 1-.64-.257.863.863 0 0 1-.26-.638.88.88 0 0 1 .26-.643.864.864 0 0 1 .64-.262h3c.255 0 .47.086.642.259a.87.87 0 0 1 .258.64v3a.874.874 0 0 1-.257.642.863.863 0 0 1-.637.259.88.88 0 0 1-.643-.259.865.865 0 0 1-.263-.641v-.725a9.95 9.95 0 0 1-3.35 2.925c-1.35.733-2.833 1.1-4.45 1.1Zm0-17.4c-1.383 0-2.645.33-3.787.987A7.985 7.985 0 0 0 5.448 7.8h.85c.255 0 .47.086.642.257a.863.863 0 0 1 .258.638.88.88 0 0 1-.258.642.864.864 0 0 1-.642.263h-3a.87.87 0 0 1-.64-.259.87.87 0 0 1-.26-.641v-3c0-.255.086-.469.258-.641a.863.863 0 0 1 .637-.26.88.88 0 0 1 .643.26.864.864 0 0 1 .262.64v.726A9.95 9.95 0 0 1 7.548 3.5c1.35-.733 2.834-1.1 4.45-1.1 2 0 3.788.542 5.363 1.625 1.575 1.083 2.736 2.49 3.482 4.22a.724.724 0 0 1-.045.646.926.926 0 0 1-.525.437.87.87 0 0 1-.687-.04.977.977 0 0 1-.488-.513c-.616-1.35-1.558-2.45-2.825-3.3-1.266-.85-2.691-1.275-4.275-1.275Zm0 10.8a2.893 2.893 0 0 1-2.125-.875A2.893 2.893 0 0 1 8.998 12c0-.833.292-1.542.875-2.125A2.893 2.893 0 0 1 11.998 9c.834 0 1.542.292 2.125.875.584.583.875 1.292.875 2.125s-.291 1.542-.875 2.125a2.893 2.893 0 0 1-2.125.875Z\"/><path stroke=\"%23fff\" stroke-opacity=\".7\" d=\"m2.739 16.072.002.005c.789 1.788 2.014 3.24 3.666 4.35 1.66 1.116 3.53 1.673 5.591 1.673 1.696 0 3.262-.386 4.69-1.16a10.54 10.54 0 0 0 2.714-2.097c.069.167.171.32.308.454.272.268.613.403.994.403.38 0 .722-.135.992-.406s.402-.614.402-.994v-3c0-.381-.134-.724-.405-.995a1.37 1.37 0 0 0-.995-.405h-3c-.383 0-.727.137-.997.411a1.38 1.38 0 0 0-.403.994c0 .38.135.723.407.992.238.237.532.369.858.397a7.552 7.552 0 0 1-2.027 1.685c-1.062.613-2.237.92-3.538.92-1.488 0-2.815-.397-3.996-1.19-1.19-.798-2.07-1.826-2.647-3.087-.147-.34-.393-.6-.727-.76a1.36 1.36 0 0 0-1.073-.058c-.37.133-.64.397-.8.754a1.351 1.351 0 0 0-.016 1.114Zm4.553-8.37a1.36 1.36 0 0 0-.858-.396A7.552 7.552 0 0 1 8.46 5.62c1.062-.613 2.237-.921 3.537-.921 1.489 0 2.816.398 3.997 1.19 1.19.8 2.07 1.827 2.646 3.088.148.34.394.6.728.76a1.37 1.37 0 0 0 1.068.063c.347-.12.62-.346.799-.668.187-.338.215-.71.072-1.072l-.003-.007-.003-.007c-.784-1.818-2.006-3.298-3.658-4.434C15.982 2.47 14.094 1.9 11.998 1.9c-1.695 0-3.261.385-4.688 1.16a10.536 10.536 0 0 0-2.715 2.096 1.364 1.364 0 0 0-.308-.453 1.38 1.38 0 0 0-.994-.403c-.38 0-.722.135-.992.406s-.403.614-.403.994v3c0 .38.135.724.406.995.27.27.614.405.994.405h3c.384 0 .727-.137.998-.412a1.38 1.38 0 0 0 .402-.993c0-.38-.135-.723-.406-.992ZM9.52 14.48a3.393 3.393 0 0 0 2.478 1.02c.965 0 1.8-.342 2.479-1.02a3.392 3.392 0 0 0 1.021-2.48c0-.963-.343-1.8-1.021-2.478A3.392 3.392 0 0 0 11.998 8.5c-.964 0-1.8.343-2.478 1.021A3.392 3.392 0 0 0 8.498 12c0 .964.344 1.8 1.022 2.479Z\"/></svg>') 12 12,auto}.unreal-container.pan{cursor:url('data:image/svg+xml,<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" fill=\"none\"><path fill=\"%23000\" d=\"M11.997 10.075a1.01 1.01 0 0 1-.738-.313 1.008 1.008 0 0 1-.312-.737V6.15l-.95.95a.998.998 0 0 1-.763.312 1.09 1.09 0 0 1-.763-.337 1.06 1.06 0 0 1-.324-.775c0-.3.108-.558.324-.775l2.776-2.775c.117-.116.237-.2.362-.25a1.04 1.04 0 0 1 .775 0c.125.05.246.134.363.25l2.8 2.8c.216.217.32.47.313.762a1.096 1.096 0 0 1-.338.763 1.058 1.058 0 0 1-.775.325c-.3 0-.558-.108-.775-.325l-.925-.925v2.875c0 .283-.104.53-.312.737a1.01 1.01 0 0 1-.738.313Zm0 11.5a1.04 1.04 0 0 1-.388-.075 1.103 1.103 0 0 1-.362-.25l-2.8-2.8a1.002 1.002 0 0 1-.313-.762c.009-.292.121-.546.337-.763.217-.217.475-.325.775-.325.3 0 .559.108.775.325l.926.925v-2.875c0-.283.104-.529.312-.737a1.01 1.01 0 0 1 .738-.313c.283 0 .529.104.738.313.208.208.312.454.312.737v2.875l.95-.95a.998.998 0 0 1 .762-.312c.292.008.546.12.763.337.216.217.324.475.324.775 0 .3-.108.559-.324.775l-2.775 2.775c-.117.116-.238.2-.363.25a1.04 1.04 0 0 1-.387.075Zm4.925-6.05a1.058 1.058 0 0 1-.326-.774c0-.3.109-.559.326-.776l.925-.925H14.97c-.283 0-.528-.104-.736-.312a1.01 1.01 0 0 1-.313-.738c0-.283.104-.53.313-.738.208-.208.453-.312.737-.312h2.875l-.95-.95a.998.998 0 0 1-.313-.762 1.09 1.09 0 0 1 .338-.764 1.06 1.06 0 0 1 .775-.324c.3 0 .558.108.775.324l2.774 2.776c.117.117.2.238.251.363a1.04 1.04 0 0 1 0 .775c-.05.124-.134.245-.25.362l-2.8 2.8c-.217.216-.471.32-.762.313a1.096 1.096 0 0 1-.763-.337Zm-11.4 0L2.746 12.75a1.103 1.103 0 0 1-.25-.362 1.04 1.04 0 0 1 0-.775c.05-.125.133-.246.25-.363l2.8-2.8c.216-.216.47-.32.762-.313.292.01.546.121.763.337.217.217.325.476.325.776 0 .3-.108.558-.325.775l-.925.925h2.875c.283 0 .529.104.737.312.208.209.313.455.313.738s-.105.53-.313.738a1.008 1.008 0 0 1-.737.312H6.147l.95.95c.217.217.32.471.312.762a1.09 1.09 0 0 1-.337.764 1.06 1.06 0 0 1-.775.324c-.3 0-.559-.108-.776-.324Z\"/><path stroke=\"%23fff\" stroke-opacity=\".7\" d=\"M9.22 7.912c.436.013.821-.15 1.13-.458l.097-.097v1.668c0 .422.16.793.458 1.09.298.299.67.46 1.092.46.422 0 .793-.161 1.091-.46.298-.297.459-.668.459-1.09V7.357l.071.072c.309.308.693.471 1.13.471.435 0 .82-.163 1.128-.471.301-.303.47-.676.483-1.102v-.001a1.501 1.501 0 0 0-.46-1.13l-2.799-2.8a1.6 1.6 0 0 0-.528-.36l-.003-.001a1.54 1.54 0 0 0-1.145 0l-.002.001a1.6 1.6 0 0 0-.529.36L8.118 5.172l-.001.001a1.56 1.56 0 0 0-.47 1.128c0 .435.163.82.47 1.128.303.303.676.472 1.103.484Zm0 0 .014-.5-.015.5Zm-1.311 6.865a1.497 1.497 0 0 0-.458-1.13l-.097-.097h1.668c.421 0 .793-.16 1.09-.458.298-.299.46-.67.46-1.092a1.51 1.51 0 0 0-.46-1.091 1.507 1.507 0 0 0-1.09-.459H7.354l.071-.072c.309-.308.472-.692.472-1.128 0-.436-.163-.82-.472-1.13a1.596 1.596 0 0 0-1.101-.482h-.002a1.501 1.501 0 0 0-1.129.459l-2.8 2.8a1.6 1.6 0 0 0-.36.528l-.001.003a1.54 1.54 0 0 0 0 1.144v.003c.081.2.208.375.362.529l2.774 2.775c.309.308.693.47 1.129.47.435 0 .82-.162 1.128-.47.303-.302.472-.676.484-1.102Zm0 0-.5-.014.5.015v-.001Zm3.038-8.627-.5.5.5.207V6.15Zm.475 15.814.002.001a1.54 1.54 0 0 0 1.145 0h.002c.2-.081.375-.208.53-.362l2.775-2.774a1.56 1.56 0 0 0 .47-1.129c0-.435-.162-.82-.47-1.128a1.589 1.589 0 0 0-1.103-.484 1.497 1.497 0 0 0-1.13.458l-.096.097v-1.668c0-.421-.161-.793-.459-1.09a1.509 1.509 0 0 0-1.091-.46c-.422 0-.794.161-1.092.46a1.507 1.507 0 0 0-.458 1.09v1.668l-.072-.072a1.558 1.558 0 0 0-1.129-.471c-.436 0-.82.163-1.128.471l-.001.001c-.301.302-.47.675-.483 1.101v.001c-.012.437.151.821.46 1.13l2.8 2.8c.153.153.328.28.528.36Zm5.146-6.085c.303.302.676.47 1.102.484a1.5 1.5 0 0 0 1.13-.46l2.8-2.8a1.6 1.6 0 0 0 .36-.528l.002-.003a1.54 1.54 0 0 0 0-1.144l-.001-.003a1.6 1.6 0 0 0-.361-.529l-2.774-2.775h-.001a1.56 1.56 0 0 0-1.128-.47c-.436 0-.82.162-1.128.47h-.001a1.59 1.59 0 0 0-.484 1.102c-.012.437.15.822.459 1.13l.097.097h-1.668c-.422 0-.793.16-1.09.459-.299.298-.46.67-.46 1.091 0 .422.161.793.46 1.092.297.297.668.458 1.09.458h1.668l-.072.072a1.558 1.558 0 0 0-.472 1.129c0 .435.164.82.472 1.128ZM6.147 13.05l.5.5.207-.5h-.707Z\"/></svg>') 12 12,auto}.unreal-container.dolly{cursor:url('data:image/svg+xml,<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" fill=\"none\"><mask id=\"a\" width=\"24\" height=\"24\" x=\"0\" y=\"0\" maskUnits=\"userSpaceOnUse\" style=\"mask-type:alpha\"><path fill=\"%23127392\" d=\"M0 0h24v24H0z\"/></mask><g mask=\"url(%23a)\"><path fill=\"%23000\" d=\"M12.002 20.575a1.026 1.026 0 0 1-.75-.325l-2.55-2.55a.933.933 0 0 1-.287-.713c.008-.275.113-.513.313-.713.2-.2.441-.3.725-.3.283 0 .524.1.724.3l.776.776V6.95l-.8.8c-.2.2-.438.296-.712.288a1.018 1.018 0 0 1-.713-.312c-.2-.2-.3-.442-.3-.726s.1-.525.3-.725l2.524-2.525a1.026 1.026 0 0 1 1.5 0l2.55 2.55c.2.2.296.438.289.713a1.023 1.023 0 0 1-.314.713c-.2.2-.441.3-.724.3a.988.988 0 0 1-.725-.3l-.775-.776v10.1l.8-.8c.2-.2.437-.296.712-.288.275.008.512.112.712.312.2.2.3.442.3.726s-.1.525-.3.725l-2.524 2.525a1.026 1.026 0 0 1-.75.325Z\"/><path stroke=\"%23fff\" stroke-opacity=\".7\" d=\"M13.553 8.153c.28.245.62.373 1 .373.415 0 .784-.153 1.078-.447.287-.287.446-.644.46-1.05v-.002a1.432 1.432 0 0 0-.435-1.08l-2.55-2.55-.354.353.354-.354a1.526 1.526 0 0 0-2.207 0L8.374 5.921A1.487 1.487 0 0 0 7.928 7c0 .416.152.785.446 1.08.288.287.645.446 1.052.457l4.127-.384Zm0 0v7.693m0-7.693a1.667 1.667 0 0 1-.079-.074l-3.021.075v7.693a1.482 1.482 0 0 0-1-.373c-.416 0-.785.153-1.079.447a1.522 1.522 0 0 0-.46 1.05v.002c-.011.417.14.786.435 1.08l2.55 2.55a1.527 1.527 0 0 0 2.207 0l2.525-2.524c.294-.294.446-.663.446-1.079 0-.416-.152-.785-.446-1.08a1.518 1.518 0 0 0-1.052-.457m0 0-.014.5.014-.5Zm0 0a1.424 1.424 0 0 0-1.027.383m0 0a1.583 1.583 0 0 0-.053.05l.354.354m-.3-.404v.104l.3.3m0 0-.3-.3v.6l.3-.3Z\"/></g></svg>%0A') 12 12,auto}.unreal-container.disabled:before{content:\"\";position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px)}#playerUI{position:absolute;z-index:10;width:100%}#statsContainer{display:none;text-align:left;background-color:#000c}#stats{padding:6px;font-weight:700;font-size:14px;color:#0f0}canvas{position:absolute;image-rendering:crisp-edges}#overlay{position:absolute;top:0;right:2%;z-index:100;padding:4px;border-top-width:0;-webkit-border-bottom-right-radius:5px;-moz-border-radius-bottomright:5px;border-bottom-right-radius:5px;-webkit-border-bottom-left-radius:5px;-moz-border-radius-bottomleft:5px;border-bottom-left-radius:5px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-touch-callout:none}.overlay{font-weight:lighter;font-family:var(--src-font-family-body)}#overlayButton:hover{cursor:pointer}#overlayButton{font-size:40px;text-align:right}#overlaySettings{display:none;width:300px;padding:4px}#videoMessageOverlay{position:absolute;z-index:20;width:100%;margin:auto;font-size:1.8em;font-family:var(--src-font-family-body)}#playButton{display:inline-block;height:auto}img#playButton{width:10%;max-width:241px}#UIInteraction{position:fixed}#UIInteractionButtonBoundary{padding:2px}#UIInteractionButton{cursor:pointer}#hiddenInput{position:absolute;left:-10%;width:0;opacity:0}#editTextButton{position:absolute;width:40px;height:40px}\n"], dependencies: [{ kind: "component", type: AfkTimeoutModalComponent, selector: "app-afk-timeout-modal" }, { kind: "component", type: UnrealStatusComponent, selector: "app-unreal-status" }, { kind: "component", type: FreezeFrameComponent, selector: "app-freeze-frame" }, { kind: "component", type: VideoStatsComponent, selector: "app-video-stats" }, { kind: "component", type: ResumeStreamButtonComponent, selector: "app-resume-stream-button" }, { kind: "component", type: ImageLoadingSrcComponent, selector: "app-image-loading-src" }, { kind: "component", type: IntroSrcComponent, selector: "app-intro-src" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
5093
|
+
handleJsonFile(data, correlationId) {
|
|
5094
|
+
const { blob } = data;
|
|
5095
|
+
return from(blob.text()).pipe(map$1((text) => JSON.parse(text)), filter(({ commandCallback }) => {
|
|
5096
|
+
return commandCallback.correlationId === correlationId;
|
|
5097
|
+
}));
|
|
5098
|
+
}
|
|
5099
|
+
observeFileResponse(mimetype, data, sender, timeOut = 60000) {
|
|
5100
|
+
const correlationId = generateUuid();
|
|
5101
|
+
const out = { ...data, correlationId };
|
|
5102
|
+
const observable = this.fileService.fileComplete$.pipe(switchMap((data) => {
|
|
5103
|
+
if (data.valid && this.fileHandlers[mimetype]) {
|
|
5104
|
+
return this.fileHandlers[mimetype](data, correlationId).pipe(filter(Truthy));
|
|
5105
|
+
}
|
|
5106
|
+
return of(null);
|
|
5107
|
+
}), timeout(timeOut), catchError((e) => {
|
|
5108
|
+
Logger.error(e);
|
|
5109
|
+
return of(null);
|
|
5110
|
+
}), take(1));
|
|
5111
|
+
setTimeout(() => sender(out), 0);
|
|
5112
|
+
return observable;
|
|
5113
|
+
}
|
|
5114
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: FileHandlerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
5115
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: FileHandlerService }); }
|
|
5393
5116
|
}
|
|
5394
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type:
|
|
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
|
-
}] } });
|
|
5117
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: FileHandlerService, decorators: [{
|
|
5118
|
+
type: Injectable
|
|
5119
|
+
}] });
|
|
5412
5120
|
|
|
5413
|
-
class
|
|
5414
|
-
#dialogRef = inject(DialogRef);
|
|
5121
|
+
class AnalyticsService {
|
|
5415
5122
|
#store = inject(Store);
|
|
5416
|
-
|
|
5417
|
-
|
|
5123
|
+
#destroy$ = new Subject();
|
|
5124
|
+
sendEventToMixPanel(name, params = {}) {
|
|
5125
|
+
Logger.log('Analytics Event:', name, params);
|
|
5126
|
+
this.#store.dispatch(saveAnalyticsEvent({ event: { name, params } }));
|
|
5418
5127
|
}
|
|
5419
|
-
|
|
5128
|
+
constructor() {
|
|
5129
|
+
const abortSignal$ = this.#store
|
|
5130
|
+
.select(unrealFeature.selectViewportReady)
|
|
5131
|
+
.pipe(filter$1(Falsy), map(() => void 0));
|
|
5420
5132
|
this.#store
|
|
5421
|
-
.select(unrealFeature.
|
|
5422
|
-
.pipe(
|
|
5423
|
-
.subscribe(
|
|
5424
|
-
|
|
5425
|
-
|
|
5426
|
-
|
|
5427
|
-
|
|
5428
|
-
|
|
5133
|
+
.select(unrealFeature.selectViewportReady)
|
|
5134
|
+
.pipe(distinctUntilChanged(), filter$1(Truthy), tap(() => this.monitorMouse(abortSignal$)))
|
|
5135
|
+
.subscribe();
|
|
5136
|
+
}
|
|
5137
|
+
monitorMouse(abortSignal$) {
|
|
5138
|
+
this.#destroy$.next();
|
|
5139
|
+
const stop$ = merge(this.#destroy$.asObservable(), abortSignal$);
|
|
5140
|
+
const element = document.getElementById('streamingVideo');
|
|
5141
|
+
if (!element) {
|
|
5142
|
+
return;
|
|
5143
|
+
}
|
|
5144
|
+
// Monitor mousewheel events
|
|
5145
|
+
const mousewheel$ = fromEvent(element, 'wheel').pipe(takeUntil$1(stop$), throttleTime(1000));
|
|
5146
|
+
// Monitor mouse button states
|
|
5147
|
+
const mousedown$ = fromEvent(element, 'mousedown').pipe(takeUntil$1(stop$));
|
|
5148
|
+
const touchStart$ = fromEvent(element, 'touchstart').pipe(takeUntil$1(stop$));
|
|
5149
|
+
const touchMove$ = fromEvent(element, 'touchmove').pipe(takeUntil$1(stop$), throttleTime(200));
|
|
5150
|
+
// Track mouse movement while the left or right button is pressed
|
|
5151
|
+
mousedown$
|
|
5152
|
+
.pipe(filter$1((event) => event.button === 0 || event.button === 2), tap((downEvent) => {
|
|
5153
|
+
this.sendEventToMixPanel('mouse_interaction', {
|
|
5154
|
+
viewX: downEvent.clientX / element.clientWidth,
|
|
5155
|
+
viewY: downEvent.clientY / element.clientHeight,
|
|
5156
|
+
action: downEvent.button === 0 ? 'spinning' : 'panning',
|
|
5157
|
+
});
|
|
5158
|
+
}))
|
|
5159
|
+
.subscribe();
|
|
5160
|
+
// Track mouse movement while the left or right button is pressed
|
|
5161
|
+
touchStart$
|
|
5162
|
+
.pipe(tap((downEvent) => {
|
|
5163
|
+
const totalTouches = downEvent.touches.length;
|
|
5164
|
+
const touch = downEvent.touches[0];
|
|
5165
|
+
this.sendEventToMixPanel('touch_interaction', {
|
|
5166
|
+
viewX: touch.clientX / element.clientWidth,
|
|
5167
|
+
viewY: touch.clientY / element.clientHeight,
|
|
5168
|
+
action: totalTouches === 1 ? 'spinning' : 'panning',
|
|
5169
|
+
});
|
|
5170
|
+
}))
|
|
5171
|
+
.subscribe();
|
|
5172
|
+
let initialTouchDistance = 0;
|
|
5173
|
+
// Subscribe to mousewheel events
|
|
5174
|
+
touchMove$.subscribe((event) => {
|
|
5175
|
+
const totalTouches = event.touches.length;
|
|
5176
|
+
if (totalTouches < 2) {
|
|
5177
|
+
return;
|
|
5178
|
+
}
|
|
5179
|
+
// Calculate the current distance between two touches
|
|
5180
|
+
const touch1 = event.touches[0];
|
|
5181
|
+
const touch2 = event.touches[1];
|
|
5182
|
+
const currentDistance = Math.sqrt(Math.pow(touch2.clientX - touch1.clientX, 2) +
|
|
5183
|
+
Math.pow(touch2.clientY - touch1.clientY, 2));
|
|
5184
|
+
// Determine if zooming in or out based on distance change
|
|
5185
|
+
const deltaDistance = currentDistance - initialTouchDistance;
|
|
5186
|
+
const isZoomingIn = deltaDistance > 0;
|
|
5187
|
+
// Only send event if there's a significant change (avoid noise)
|
|
5188
|
+
if (Math.abs(deltaDistance) > 10) {
|
|
5189
|
+
this.sendEventToMixPanel('touch_zoom_interaction', {
|
|
5190
|
+
delta: isZoomingIn ? 1 : -1,
|
|
5191
|
+
action: isZoomingIn ? 'zoomin' : 'zoomout',
|
|
5192
|
+
});
|
|
5193
|
+
// Update initial distance for next comparison
|
|
5194
|
+
initialTouchDistance = currentDistance;
|
|
5429
5195
|
}
|
|
5430
|
-
|
|
5196
|
+
});
|
|
5197
|
+
mousewheel$.subscribe((event) => {
|
|
5198
|
+
this.sendEventToMixPanel('mouse_zoom_interaction', {
|
|
5199
|
+
delta: -(event.deltaY / Math.abs(event.deltaY)),
|
|
5200
|
+
action: event.deltaY < 0 ? 'zoomin' : 'zoomout',
|
|
5201
|
+
});
|
|
5431
5202
|
});
|
|
5432
5203
|
}
|
|
5433
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type:
|
|
5434
|
-
static { this.ɵ
|
|
5204
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: AnalyticsService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
5205
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: AnalyticsService }); }
|
|
5435
5206
|
}
|
|
5436
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type:
|
|
5437
|
-
type:
|
|
5438
|
-
|
|
5439
|
-
}] });
|
|
5207
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: AnalyticsService, decorators: [{
|
|
5208
|
+
type: Injectable
|
|
5209
|
+
}], ctorParameters: () => [] });
|
|
5440
5210
|
|
|
5441
|
-
class
|
|
5211
|
+
class UnrealCallbackService {
|
|
5442
5212
|
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
|
-
}
|
|
5213
|
+
this.actions$ = inject(Actions);
|
|
5453
5214
|
}
|
|
5454
|
-
|
|
5455
|
-
|
|
5456
|
-
|
|
5215
|
+
/**
|
|
5216
|
+
* Listens for unreal command callbacks matching a specific command or event type.
|
|
5217
|
+
*
|
|
5218
|
+
* Supports both MetaBoxCommand keys (e.g. `MetaBoxCommand.FLoadProductCommand`)
|
|
5219
|
+
* and custom Unreal callback events (e.g. `'cameraChanged'`, `'onSceneState'`).
|
|
5220
|
+
*
|
|
5221
|
+
* @param command - The command or event key to filter callbacks by
|
|
5222
|
+
* @returns Observable that emits matching callback payloads
|
|
5223
|
+
*
|
|
5224
|
+
* @example
|
|
5225
|
+
* ```ts
|
|
5226
|
+
* // Listen for a MetaBox command callback
|
|
5227
|
+
* this.unrealCallbackService
|
|
5228
|
+
* .fromUnrealCallback(MetaBoxCommand.FLoadProductCommand)
|
|
5229
|
+
* .subscribe(data => console.log(data));
|
|
5230
|
+
*
|
|
5231
|
+
* // Listen for a custom Unreal event
|
|
5232
|
+
* this.unrealCallbackService
|
|
5233
|
+
* .fromUnrealCallback('cameraChanged')
|
|
5234
|
+
* .subscribe(data => console.log(data));
|
|
5235
|
+
* ```
|
|
5236
|
+
*/
|
|
5237
|
+
fromUnrealCallback(command) {
|
|
5238
|
+
return this.actions$.pipe(ofType(unrealCommandCallback), map$1(({ json }) => {
|
|
5239
|
+
if ('commandCallback' in json) {
|
|
5240
|
+
return json.commandCallback;
|
|
5241
|
+
}
|
|
5242
|
+
return json;
|
|
5243
|
+
}), filter((json) => json.command === command), map$1((data) => ({
|
|
5244
|
+
command: data.command,
|
|
5245
|
+
payload: 'payload' in data ? data.payload : null,
|
|
5246
|
+
})));
|
|
5457
5247
|
}
|
|
5458
|
-
|
|
5459
|
-
|
|
5460
|
-
|
|
5461
|
-
|
|
5462
|
-
|
|
5248
|
+
/**
|
|
5249
|
+
* Sends a command and observes the matching response by correlationId.
|
|
5250
|
+
* Replaces the old `observeCommandResponse` standalone function.
|
|
5251
|
+
*
|
|
5252
|
+
* @param data - The command data to send
|
|
5253
|
+
* @param sender - Function to send the command to Unreal
|
|
5254
|
+
* @param timeOut - Timeout in ms (default 60000)
|
|
5255
|
+
* @param dispatchOnTimeout - Whether to emit on timeout (default true)
|
|
5256
|
+
*
|
|
5257
|
+
* @example
|
|
5258
|
+
* ```ts
|
|
5259
|
+
* this.unrealCallbackService
|
|
5260
|
+
* .observeCommandResponse<FLoopBackCommand['command']>(
|
|
5261
|
+
* { command: MetaBoxCommand.FLoopBackCommand },
|
|
5262
|
+
* (data) => this.communicator.sendCommandToUnreal(data),
|
|
5263
|
+
* )
|
|
5264
|
+
* .subscribe();
|
|
5265
|
+
* ```
|
|
5266
|
+
*/
|
|
5267
|
+
observeCommandResponse(data, sender, timeOut = 60000, dispatchOnTimeout = true) {
|
|
5268
|
+
const correlationId = generateUuid();
|
|
5269
|
+
const observable = this.actions$.pipe(ofType(unrealCommandCallback), map$1(({ json }) => 'commandCallback' in json ? json.commandCallback : json), filter((json) => json.correlationId === correlationId), timeout(timeOut), catchError(() => dispatchOnTimeout
|
|
5270
|
+
? of({ command: 'timeout', correlationId: 'timeout' })
|
|
5271
|
+
: of(null)), take(1), tapLog('Command Response Observer'), filter(Truthy), map$1((data) => ({
|
|
5272
|
+
command: data.command,
|
|
5273
|
+
payload: 'payload' in data ? data.payload : null,
|
|
5274
|
+
})));
|
|
5275
|
+
setTimeout(() => sender({ ...data, correlationId }), 0);
|
|
5276
|
+
return observable;
|
|
5463
5277
|
}
|
|
5464
|
-
|
|
5465
|
-
|
|
5466
|
-
|
|
5467
|
-
|
|
5468
|
-
|
|
5469
|
-
|
|
5470
|
-
|
|
5471
|
-
|
|
5472
|
-
|
|
5473
|
-
this
|
|
5278
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: UnrealCallbackService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
5279
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: UnrealCallbackService }); }
|
|
5280
|
+
}
|
|
5281
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: UnrealCallbackService, decorators: [{
|
|
5282
|
+
type: Injectable
|
|
5283
|
+
}] });
|
|
5284
|
+
|
|
5285
|
+
class AfkTimeoutModalComponent {
|
|
5286
|
+
constructor() {
|
|
5287
|
+
this.#store = inject(Store);
|
|
5288
|
+
this.#afkService = inject(AFKService);
|
|
5289
|
+
this.isAfkTimerVisible = this.#store.selectSignal(unrealFeature.selectIsAfkTimerVisible);
|
|
5290
|
+
this.afkCountdown = this.#store.selectSignal(unrealFeature.selectAfkCountdown);
|
|
5474
5291
|
}
|
|
5475
|
-
|
|
5476
|
-
|
|
5477
|
-
|
|
5478
|
-
|
|
5479
|
-
});
|
|
5292
|
+
#store;
|
|
5293
|
+
#afkService;
|
|
5294
|
+
resetAfk() {
|
|
5295
|
+
this.#afkService.reset();
|
|
5480
5296
|
}
|
|
5481
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type:
|
|
5482
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type:
|
|
5297
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: AfkTimeoutModalComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
5298
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: AfkTimeoutModalComponent, isStandalone: true, selector: "app-afk-timeout-modal", ngImport: i0, template: "@if (isAfkTimerVisible()) {\n <div (click)=\"resetAfk()\" class=\"clickableState\" id=\"videoPlayOverlay\">\n <div class=\"resume-box\">\n <div aria-hidden=\"true\" class=\"resume-box__pic\" role=\"presentation\">\n <div [innerHTML]=\"afkCountdown()\" class=\"text-number\"></div>\n </div>\n <div class=\"resume-box__text\">\n <h3 class=\"resume-box__heading\">Session will time out soon</h3>\n <p>\n No activity detected. Press 'Continue' if you wish to keep your\n session active\n </p>\n </div>\n <src-button\n [isFullWidth]=\"true\"\n [data-testid]=\"'continue-session'\"\n weight=\"primary\"\n size=\"lg\"\n >\n Continue\n </src-button>\n </div>\n </div>\n}\n", styles: ["#videoPlayOverlay{position:absolute;z-index:30;top:0;width:100%;height:100%;font-size:1.8em;font-family:var(--src-font-family-body);background-color:#646464b3}.clickableState{display:flex;justify-content:center;align-items:center;cursor:pointer}.textDisplayState{display:flex}.hiddenState{display:none}.resume-box{width:340px;padding:32px 20px 20px;flex-direction:column;align-items:center;border-radius:var(--src-border-rounded-parent, 8px);background:var(--src-surface-container-main, #fff);box-shadow:0 26px 80px #0003,0 0 1px #0003}.resume-box .resume-box__pic{width:72px;height:72px;margin:0 auto 22px;border-radius:48px;background:#ecf0f2;padding:12px;display:flex;align-items:center;justify-content:center}.resume-box .resume-box__pic .text-number{color:var(--src-colors-text-default, #1f2937);text-align:center;font-family:var(--src-font-family-body);font-size:30px;font-style:normal;font-weight:400;line-height:24px}.resume-box__text{margin-bottom:18px}.resume-box__text p{text-align:center;font-family:var(--src-font-family-body);font-size:var(--src-font-size-sm, 14px);font-style:normal;font-weight:400;line-height:24px;color:var(--src-text-body-secondary, #6b7280)}.resume-box__text .resume-box__heading{color:var(--src-text-body-main, #1f2937);text-align:center;font-family:var(--src-font-family-body);font-size:18px;font-style:normal;font-weight:500;line-height:26px;margin-bottom:8px}\n"], dependencies: [{ kind: "component", type: SourceButtonComponent, selector: "src-button", inputs: ["type", "appearance", "weight", "size", "context", "customClass", "isFullWidth", "isPressed", "isDisabled", "isLoading", "isInverted", "iconButton", "srcButtonConfig", "formID", "data-testid", "ariaLabel"], outputs: ["onClick", "onSubmit"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
5483
5299
|
}
|
|
5484
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type:
|
|
5300
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: AfkTimeoutModalComponent, decorators: [{
|
|
5485
5301
|
type: Component,
|
|
5486
|
-
args: [{ selector: 'app-
|
|
5302
|
+
args: [{ selector: 'app-afk-timeout-modal', changeDetection: ChangeDetectionStrategy.OnPush, imports: [SourceButtonComponent], template: "@if (isAfkTimerVisible()) {\n <div (click)=\"resetAfk()\" class=\"clickableState\" id=\"videoPlayOverlay\">\n <div class=\"resume-box\">\n <div aria-hidden=\"true\" class=\"resume-box__pic\" role=\"presentation\">\n <div [innerHTML]=\"afkCountdown()\" class=\"text-number\"></div>\n </div>\n <div class=\"resume-box__text\">\n <h3 class=\"resume-box__heading\">Session will time out soon</h3>\n <p>\n No activity detected. Press 'Continue' if you wish to keep your\n session active\n </p>\n </div>\n <src-button\n [isFullWidth]=\"true\"\n [data-testid]=\"'continue-session'\"\n weight=\"primary\"\n size=\"lg\"\n >\n Continue\n </src-button>\n </div>\n </div>\n}\n", styles: ["#videoPlayOverlay{position:absolute;z-index:30;top:0;width:100%;height:100%;font-size:1.8em;font-family:var(--src-font-family-body);background-color:#646464b3}.clickableState{display:flex;justify-content:center;align-items:center;cursor:pointer}.textDisplayState{display:flex}.hiddenState{display:none}.resume-box{width:340px;padding:32px 20px 20px;flex-direction:column;align-items:center;border-radius:var(--src-border-rounded-parent, 8px);background:var(--src-surface-container-main, #fff);box-shadow:0 26px 80px #0003,0 0 1px #0003}.resume-box .resume-box__pic{width:72px;height:72px;margin:0 auto 22px;border-radius:48px;background:#ecf0f2;padding:12px;display:flex;align-items:center;justify-content:center}.resume-box .resume-box__pic .text-number{color:var(--src-colors-text-default, #1f2937);text-align:center;font-family:var(--src-font-family-body);font-size:30px;font-style:normal;font-weight:400;line-height:24px}.resume-box__text{margin-bottom:18px}.resume-box__text p{text-align:center;font-family:var(--src-font-family-body);font-size:var(--src-font-size-sm, 14px);font-style:normal;font-weight:400;line-height:24px;color:var(--src-text-body-secondary, #6b7280)}.resume-box__text .resume-box__heading{color:var(--src-text-body-main, #1f2937);text-align:center;font-family:var(--src-font-family-body);font-size:18px;font-style:normal;font-weight:500;line-height:26px;margin-bottom:8px}\n"] }]
|
|
5487
5303
|
}] });
|
|
5488
5304
|
|
|
5489
|
-
class
|
|
5305
|
+
class FreezeFrameComponent {
|
|
5490
5306
|
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));
|
|
5307
|
+
this.#store = inject(Store);
|
|
5308
|
+
this.freezeFrameProgressMessageFromVideo = this.#store.selectSignal(selectFreezeFrameProgressMessageFromVideo);
|
|
5309
|
+
this.combinedFreeze = this.#store.selectSignal(selectFreezeFrameCombinedDataUrl);
|
|
5518
5310
|
}
|
|
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();
|
|
5311
|
+
#store;
|
|
5312
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: FreezeFrameComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
5313
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: FreezeFrameComponent, isStandalone: true, selector: "app-freeze-frame", ngImport: i0, template: "@let image = combinedFreeze();\n@let progress = freezeFrameProgressMessageFromVideo();\n\n<div class=\"freeze-images\">\n @if (image) {\n <img [src]=\"image\" class=\"videoStillImage\" alt=\"freezeFrameImage\" />\n }\n\n @if (progress) {\n <div class=\"progress-status\">\n {{ progress }}\n </div>\n }\n</div>\n", styles: [".freeze-images{pointer-events:none;position:absolute;left:0;top:0;width:100%;height:100%;z-index:1}.freeze-images .freezeFrameOverlay{top:0;left:0;width:100%;height:100%;position:absolute;object-fit:cover;object-position:center}.freeze-images .videoStillImage{position:absolute;z-index:1;top:0;left:0;width:100%;height:100%;object-fit:cover}.freeze-images .progress-status{position:absolute;top:50%;left:50%;z-index:1;display:flex;flex-direction:column;justify-content:center;align-items:center;width:auto;height:auto;margin:0;padding:10px;background-color:#ffffffb3;border:1px solid grey;transform:translate(-50%,-50%)}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
5314
|
+
}
|
|
5315
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: FreezeFrameComponent, decorators: [{
|
|
5316
|
+
type: Component,
|
|
5317
|
+
args: [{ selector: 'app-freeze-frame', changeDetection: ChangeDetectionStrategy.OnPush, template: "@let image = combinedFreeze();\n@let progress = freezeFrameProgressMessageFromVideo();\n\n<div class=\"freeze-images\">\n @if (image) {\n <img [src]=\"image\" class=\"videoStillImage\" alt=\"freezeFrameImage\" />\n }\n\n @if (progress) {\n <div class=\"progress-status\">\n {{ progress }}\n </div>\n }\n</div>\n", styles: [".freeze-images{pointer-events:none;position:absolute;left:0;top:0;width:100%;height:100%;z-index:1}.freeze-images .freezeFrameOverlay{top:0;left:0;width:100%;height:100%;position:absolute;object-fit:cover;object-position:center}.freeze-images .videoStillImage{position:absolute;z-index:1;top:0;left:0;width:100%;height:100%;object-fit:cover}.freeze-images .progress-status{position:absolute;top:50%;left:50%;z-index:1;display:flex;flex-direction:column;justify-content:center;align-items:center;width:auto;height:auto;margin:0;padding:10px;background-color:#ffffffb3;border:1px solid grey;transform:translate(-50%,-50%)}\n"] }]
|
|
5318
|
+
}] });
|
|
5319
|
+
|
|
5320
|
+
class SafePipe {
|
|
5321
|
+
constructor() {
|
|
5322
|
+
this.sanitizer = inject(DomSanitizer);
|
|
5534
5323
|
}
|
|
5535
|
-
|
|
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
|
-
}));
|
|
5324
|
+
transform(value, type) {
|
|
5325
|
+
if (!value) {
|
|
5326
|
+
return value;
|
|
5567
5327
|
}
|
|
5568
|
-
|
|
5569
|
-
|
|
5570
|
-
|
|
5571
|
-
|
|
5572
|
-
|
|
5328
|
+
switch (type) {
|
|
5329
|
+
case 'html':
|
|
5330
|
+
return this.sanitizer.bypassSecurityTrustHtml(value);
|
|
5331
|
+
case 'style':
|
|
5332
|
+
return this.sanitizer.bypassSecurityTrustStyle(value);
|
|
5333
|
+
case 'script':
|
|
5334
|
+
return this.sanitizer.bypassSecurityTrustScript(value);
|
|
5335
|
+
case 'url':
|
|
5336
|
+
return this.sanitizer.bypassSecurityTrustUrl(value);
|
|
5337
|
+
case 'resourceUrl':
|
|
5338
|
+
return this.sanitizer.bypassSecurityTrustResourceUrl(value);
|
|
5339
|
+
default:
|
|
5340
|
+
throw new Error(`Invalid safe type specified: ${type}`);
|
|
5573
5341
|
}
|
|
5574
5342
|
}
|
|
5575
|
-
|
|
5576
|
-
|
|
5577
|
-
|
|
5578
|
-
|
|
5343
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: SafePipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
|
|
5344
|
+
static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.2.0", ngImport: i0, type: SafePipe, isStandalone: true, name: "safe" }); }
|
|
5345
|
+
}
|
|
5346
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: SafePipe, decorators: [{
|
|
5347
|
+
type: Pipe,
|
|
5348
|
+
args: [{
|
|
5349
|
+
name: 'safe',
|
|
5350
|
+
}]
|
|
5351
|
+
}] });
|
|
5352
|
+
|
|
5353
|
+
class LowBandwidthModalComponent {
|
|
5354
|
+
constructor() {
|
|
5355
|
+
this.dialogRef = inject(DialogRef);
|
|
5579
5356
|
}
|
|
5580
|
-
|
|
5581
|
-
this.
|
|
5582
|
-
.open(LowBandwidthModalComponent)
|
|
5583
|
-
.closed.pipe(filter(Truthy))
|
|
5584
|
-
.subscribe(() => {
|
|
5585
|
-
this.toggleIndicator(false);
|
|
5586
|
-
this.store.dispatch(changeLowBandwidth({ lowBandwidth: false }));
|
|
5587
|
-
});
|
|
5357
|
+
close(value) {
|
|
5358
|
+
this.dialogRef.close(value || false);
|
|
5588
5359
|
}
|
|
5589
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type:
|
|
5590
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "
|
|
5360
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: LowBandwidthModalComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
5361
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.0", type: LowBandwidthModalComponent, isStandalone: true, selector: "app-low-bandwidth-modal", ngImport: i0, template: "<div [attr.data-testid]=\"'low-bandwidth'\" class=\"src-modal src-modal--lbm\">\n <header class=\"src-modal__header\">\n <h6 [attr.data-testid]=\"'title'\" class=\"src-modal__title\">\n Unstable Connection\n </h6>\n </header>\n <section class=\"src-modal__body\">\n <div\n [innerHtml]=\"\n 'Fluid Interactivity Modes were disabled due to an unstable connection. Showcase Gallery Mode is enabled. To regain full functionality, switch to Interactive Mode.'\n | safe: 'html'\n \"\n class=\"src-modal__scroll-box\"\n ></div>\n </section>\n <footer class=\"src-modal__footer\">\n <div class=\"src-modal__buttons\">\n <src-button\n (onClick)=\"close(true)\"\n [data-testid]=\"'switch-to-interactive-mode'\"\n >\n Switch\n </src-button>\n\n <src-button\n (onClick)=\"close()\"\n [data-testid]=\"'close-lbm-modal'\"\n weight=\"primary\"\n >\n Ok\n </src-button>\n </div>\n </footer>\n</div>\n", styles: [".src-modal--lbm{width:360px}.src-modal--lbm .src-modal__body{font-size:14px;line-height:24px}.src-modal--lbm .src-modal__buttons{display:flex;gap:8px}\n"], dependencies: [{ kind: "component", type: SourceButtonComponent, selector: "src-button", inputs: ["type", "appearance", "weight", "size", "context", "customClass", "isFullWidth", "isPressed", "isDisabled", "isLoading", "isInverted", "iconButton", "srcButtonConfig", "formID", "data-testid", "ariaLabel"], outputs: ["onClick", "onSubmit"] }, { kind: "pipe", type: SafePipe, name: "safe" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
5591
5362
|
}
|
|
5592
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type:
|
|
5363
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: LowBandwidthModalComponent, decorators: [{
|
|
5593
5364
|
type: Component,
|
|
5594
|
-
args: [{ selector: 'app-low-bandwidth-
|
|
5365
|
+
args: [{ selector: 'app-low-bandwidth-modal', changeDetection: ChangeDetectionStrategy.OnPush, imports: [SafePipe, SourceButtonComponent], template: "<div [attr.data-testid]=\"'low-bandwidth'\" class=\"src-modal src-modal--lbm\">\n <header class=\"src-modal__header\">\n <h6 [attr.data-testid]=\"'title'\" class=\"src-modal__title\">\n Unstable Connection\n </h6>\n </header>\n <section class=\"src-modal__body\">\n <div\n [innerHtml]=\"\n 'Fluid Interactivity Modes were disabled due to an unstable connection. Showcase Gallery Mode is enabled. To regain full functionality, switch to Interactive Mode.'\n | safe: 'html'\n \"\n class=\"src-modal__scroll-box\"\n ></div>\n </section>\n <footer class=\"src-modal__footer\">\n <div class=\"src-modal__buttons\">\n <src-button\n (onClick)=\"close(true)\"\n [data-testid]=\"'switch-to-interactive-mode'\"\n >\n Switch\n </src-button>\n\n <src-button\n (onClick)=\"close()\"\n [data-testid]=\"'close-lbm-modal'\"\n weight=\"primary\"\n >\n Ok\n </src-button>\n </div>\n </footer>\n</div>\n", styles: [".src-modal--lbm{width:360px}.src-modal--lbm .src-modal__body{font-size:14px;line-height:24px}.src-modal--lbm .src-modal__buttons{display:flex;gap:8px}\n"] }]
|
|
5595
5366
|
}] });
|
|
5596
5367
|
|
|
5597
|
-
|
|
5598
|
-
* Copyright (c) 2026.
|
|
5599
|
-
* 3DSource.com. Sergii Karanda steve@3dsource.com. All Rights Reserved.
|
|
5600
|
-
*/
|
|
5601
|
-
class WebRtcPlayerService2 extends SubService {
|
|
5368
|
+
class ImageLoadingSrcComponent {
|
|
5602
5369
|
constructor() {
|
|
5603
|
-
|
|
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());
|
|
5370
|
+
this.#store = inject(Store);
|
|
5371
|
+
this.isLoaderScreenVisible = this.#store.selectSignal(selectIsLoaderScreenVisible);
|
|
5372
|
+
this.imageLoadingSrc = this.#store.selectSignal(unrealFeature.selectImageLoadingSrc);
|
|
5620
5373
|
}
|
|
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
|
-
|
|
5374
|
+
#store;
|
|
5375
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ImageLoadingSrcComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
5376
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: ImageLoadingSrcComponent, isStandalone: true, selector: "app-image-loading-src", ngImport: i0, template: "@let image = imageLoadingSrc();\n@if (isLoaderScreenVisible()) {\n <div class=\"preload\">\n <div class=\"back\">\n @if (image) {\n <img [ngSrc]=\"image\" fill loading=\"lazy\" alt=\"image loading src\" />\n }\n </div>\n </div>\n}\n", styles: [".preload{position:absolute;top:0;left:0;width:100%;height:100%;text-align:center}.preload .back{position:absolute;top:0;left:0;display:flex;justify-content:center;align-items:center;width:100%;height:100%}.preload .back img{width:100%;height:100%;object-fit:cover}.preload .back{background-color:#fff}\n"], dependencies: [{ kind: "directive", type: NgOptimizedImage, selector: "img[ngSrc]", inputs: ["ngSrc", "ngSrcset", "sizes", "width", "height", "decoding", "loading", "priority", "loaderParams", "disableOptimizedSrcset", "fill", "placeholder", "placeholderConfig", "src", "srcset"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
5377
|
+
}
|
|
5378
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ImageLoadingSrcComponent, decorators: [{
|
|
5379
|
+
type: Component,
|
|
5380
|
+
args: [{ selector: 'app-image-loading-src', changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgOptimizedImage], template: "@let image = imageLoadingSrc();\n@if (isLoaderScreenVisible()) {\n <div class=\"preload\">\n <div class=\"back\">\n @if (image) {\n <img [ngSrc]=\"image\" fill loading=\"lazy\" alt=\"image loading src\" />\n }\n </div>\n </div>\n}\n", styles: [".preload{position:absolute;top:0;left:0;width:100%;height:100%;text-align:center}.preload .back{position:absolute;top:0;left:0;display:flex;justify-content:center;align-items:center;width:100%;height:100%}.preload .back img{width:100%;height:100%;object-fit:cover}.preload .back{background-color:#fff}\n"] }]
|
|
5381
|
+
}] });
|
|
5382
|
+
|
|
5383
|
+
class StatGraphComponent {
|
|
5384
|
+
constructor() {
|
|
5385
|
+
this.label = input.required(...(ngDevMode ? [{ debugName: "label" }] : []));
|
|
5386
|
+
this.stat = input(...(ngDevMode ? [undefined, { debugName: "stat" }] : []));
|
|
5387
|
+
this.color = input('#417bc9', ...(ngDevMode ? [{ debugName: "color" }] : []));
|
|
5388
|
+
this.tickStep = input(3, ...(ngDevMode ? [{ debugName: "tickStep" }] : []));
|
|
5389
|
+
this.el = inject(ElementRef);
|
|
5390
|
+
this.graph = viewChild('graph', ...(ngDevMode ? [{ debugName: "graph" }] : []));
|
|
5391
|
+
this.data = [];
|
|
5392
|
+
this.max = signal(0, ...(ngDevMode ? [{ debugName: "max" }] : []));
|
|
5393
|
+
this.min = signal(0, ...(ngDevMode ? [{ debugName: "min" }] : []));
|
|
5394
|
+
this.current = signal(0, ...(ngDevMode ? [{ debugName: "current" }] : []));
|
|
5395
|
+
effect(() => this.pushData(this.stat()?.value ?? null));
|
|
5396
|
+
afterNextRender({
|
|
5397
|
+
read: () => {
|
|
5398
|
+
const graph = this.graph();
|
|
5399
|
+
if (graph) {
|
|
5400
|
+
this.canvas = graph.nativeElement;
|
|
5401
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
5402
|
+
this.ctx = this.canvas.getContext('2d');
|
|
5403
|
+
}
|
|
5404
|
+
},
|
|
5647
5405
|
});
|
|
5648
5406
|
}
|
|
5649
|
-
|
|
5650
|
-
|
|
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);
|
|
5407
|
+
resize() {
|
|
5408
|
+
this.draw();
|
|
5409
|
+
}
|
|
5410
|
+
draw() {
|
|
5411
|
+
if (!this.canvas) {
|
|
5412
|
+
return;
|
|
5670
5413
|
}
|
|
5671
|
-
|
|
5672
|
-
|
|
5673
|
-
|
|
5674
|
-
|
|
5675
|
-
|
|
5676
|
-
|
|
5414
|
+
const all = this.data.filter((r) => r !== null && !isNaN(r));
|
|
5415
|
+
this.max.set(Math.max(...all));
|
|
5416
|
+
this.min.set(Math.min(...all));
|
|
5417
|
+
const scaleY = this.canvas.height / (this.max() || 1);
|
|
5418
|
+
this.current.set(all[all.length - 1] || 0);
|
|
5419
|
+
this.ctx.save();
|
|
5420
|
+
const bounds = this.el.nativeElement.getBoundingClientRect();
|
|
5421
|
+
this.canvas.width = bounds.width;
|
|
5422
|
+
this.canvas.height = bounds.height - 30; // -p height
|
|
5423
|
+
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
|
5424
|
+
[...this.data].reverse().forEach((val, index) => {
|
|
5425
|
+
const h = (val || 0) * scaleY;
|
|
5426
|
+
this.ctx.fillStyle = this.getColor(this.color(), h);
|
|
5427
|
+
this.ctx.fillRect(index * this.tickStep(), this.canvas.height - h, this.tickStep(), h);
|
|
5428
|
+
});
|
|
5429
|
+
this.ctx.restore();
|
|
5430
|
+
}
|
|
5431
|
+
getColor(fillStyle, value = 0) {
|
|
5432
|
+
const { r, g, b } = HEXtoRGB(fillStyle);
|
|
5433
|
+
/* eslint-disable-next-line prefer-const */
|
|
5434
|
+
let [h, s, v] = RGBtoHSV(r, g, b);
|
|
5435
|
+
v = inverseLerp(0, this.canvas.height, value);
|
|
5436
|
+
{
|
|
5437
|
+
const [r, g, b] = HSVtoRGB(h, s, v);
|
|
5438
|
+
return '#' + RGBtoHEX(r, g, b);
|
|
5677
5439
|
}
|
|
5678
5440
|
}
|
|
5679
|
-
|
|
5680
|
-
|
|
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();
|
|
5441
|
+
pushData(stat) {
|
|
5442
|
+
this.data.push(stat);
|
|
5443
|
+
if (!this.canvas) {
|
|
5444
|
+
return;
|
|
5689
5445
|
}
|
|
5690
|
-
|
|
5691
|
-
|
|
5446
|
+
if (this.data.length > this.canvas.width / this.tickStep()) {
|
|
5447
|
+
this.data.shift();
|
|
5692
5448
|
}
|
|
5449
|
+
this.draw();
|
|
5693
5450
|
}
|
|
5694
|
-
|
|
5695
|
-
|
|
5696
|
-
|
|
5697
|
-
|
|
5698
|
-
|
|
5699
|
-
|
|
5451
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: StatGraphComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
5452
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "21.2.0", type: StatGraphComponent, isStandalone: true, selector: "app-stat-graph", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, stat: { classPropertyName: "stat", publicName: "stat", isSignal: true, isRequired: false, transformFunction: null }, color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: false, transformFunction: null }, tickStep: { classPropertyName: "tickStep", publicName: "tickStep", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "window:resize": "resize()" } }, viewQueries: [{ propertyName: "graph", first: true, predicate: ["graph"], descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"content\">\n <p>{{ label() }}: {{ current() }} | Min:{{ min() }} | Max:{{ max() }}</p>\n <canvas #graph class=\"graph\"></canvas>\n</div>\n", styles: [".content{width:100%;height:100%}.content p{color:#fff;margin:5px 0 0;height:25px}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
5453
|
+
}
|
|
5454
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: StatGraphComponent, decorators: [{
|
|
5455
|
+
type: Component,
|
|
5456
|
+
args: [{ selector: 'app-stat-graph', changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"content\">\n <p>{{ label() }}: {{ current() }} | Min:{{ min() }} | Max:{{ max() }}</p>\n <canvas #graph class=\"graph\"></canvas>\n</div>\n", styles: [".content{width:100%;height:100%}.content p{color:#fff;margin:5px 0 0;height:25px}\n"] }]
|
|
5457
|
+
}], ctorParameters: () => [], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], stat: [{ type: i0.Input, args: [{ isSignal: true, alias: "stat", required: false }] }], color: [{ type: i0.Input, args: [{ isSignal: true, alias: "color", required: false }] }], tickStep: [{ type: i0.Input, args: [{ isSignal: true, alias: "tickStep", required: false }] }], graph: [{ type: i0.ViewChild, args: ['graph', { isSignal: true }] }], resize: [{
|
|
5458
|
+
type: HostListener,
|
|
5459
|
+
args: ['window:resize']
|
|
5460
|
+
}] } });
|
|
5461
|
+
|
|
5462
|
+
class UnrealStatusComponent {
|
|
5463
|
+
constructor() {
|
|
5464
|
+
this.unrealInitialConfig = inject(UNREAL_CONFIG);
|
|
5465
|
+
this.destroyRef = inject(DestroyRef);
|
|
5466
|
+
this.store = inject(Store);
|
|
5467
|
+
this.resetPercentageSignal$ = this.store
|
|
5468
|
+
.select(selectShowLoader)
|
|
5469
|
+
.pipe(filter(Falsy));
|
|
5470
|
+
this.showLoader = this.store.selectSignal(selectShowLoader);
|
|
5471
|
+
/**
|
|
5472
|
+
* An observable that emits smoothed percentage values from 0 to 100.
|
|
5473
|
+
*/
|
|
5474
|
+
this.messagePercents = toSignal(this.store.select(selectTotalProgress).pipe(keepMaxUntilReset(this.resetPercentageSignal$), floatToSmoothPercents(), distinctUntilChanged(), map$1((progress) => progress || 0)));
|
|
5475
|
+
this.awsInstance = this.store.selectSignal(unrealFeature.selectAwsInstance);
|
|
5476
|
+
}
|
|
5477
|
+
ngOnInit() {
|
|
5478
|
+
/**
|
|
5479
|
+
* A subject that emits null values to trigger nullification.
|
|
5480
|
+
*/
|
|
5481
|
+
const nullify$ = new Subject();
|
|
5482
|
+
/**
|
|
5483
|
+
* An observable that emits null values with a delay of 2000 milliseconds.
|
|
5484
|
+
*/
|
|
5485
|
+
const nullifyObs$ = nullify$
|
|
5486
|
+
.asObservable()
|
|
5487
|
+
.pipe(delay(2000));
|
|
5488
|
+
/**
|
|
5489
|
+
* An observable that emits the status message from the Unreal feature store.
|
|
5490
|
+
*/
|
|
5491
|
+
const signalingStatus$ = this.store.select(unrealFeature.selectStatusMessage);
|
|
5492
|
+
/**
|
|
5493
|
+
* An observable that emits connection status messages based on the Cirrus connection state.
|
|
5494
|
+
* Then call side effect to nullify the message.
|
|
5495
|
+
*/
|
|
5496
|
+
const cirrusConnected$ = this.store
|
|
5497
|
+
.select(unrealFeature.selectCirrusConnected)
|
|
5498
|
+
.pipe(skip(1), switchMap((connected) => connected
|
|
5499
|
+
? of('Creating Socket...')
|
|
5500
|
+
: of('Connection closed.').pipe(tap(() => nullify$.next(null)))));
|
|
5501
|
+
/**
|
|
5502
|
+
* An observable that emits the viewport visibility state.
|
|
5503
|
+
*/
|
|
5504
|
+
const viewportTrigger$ = this.store.select(unrealFeature.selectViewportReady);
|
|
5505
|
+
/**
|
|
5506
|
+
* An observable that will nullify any message displayed after 500 mSec, when viewport changes its state.
|
|
5507
|
+
*/
|
|
5508
|
+
const streamToggle$ = merge(viewportTrigger$.pipe(filter(Falsy), skip(1)), viewportTrigger$.pipe(filter(Truthy))).pipe(delay(500), map$1(() => null));
|
|
5509
|
+
/**
|
|
5510
|
+
* A subscription that merges various status observables and emits debounced status via postMessage.
|
|
5511
|
+
*/
|
|
5512
|
+
merge(nullifyObs$, cirrusConnected$, signalingStatus$, streamToggle$)
|
|
5513
|
+
.pipe(tapLog('STATUS=>'), debounceTime$1(0), takeUntilDestroyed(this.destroyRef))
|
|
5514
|
+
.subscribe((status) => this.sendMessageToIndex(status));
|
|
5700
5515
|
}
|
|
5701
5516
|
/**
|
|
5702
|
-
*
|
|
5703
|
-
*
|
|
5704
|
-
*
|
|
5517
|
+
* Sends a message to the embedded iframe identified by the default value '3dsource_start_screen' ID.
|
|
5518
|
+
* This function tries to post a message to the iframe's content window. If the content
|
|
5519
|
+
* window is not accessible (e.g., due to cross-origin policies), the function fails silently.
|
|
5520
|
+
*
|
|
5521
|
+
* @param {string} text The message to be sent to the iframe. This could be a status update
|
|
5522
|
+
* or any other relevant information that the iframe needs to process.
|
|
5705
5523
|
*/
|
|
5706
|
-
|
|
5707
|
-
|
|
5524
|
+
sendMessageToIndex(text) {
|
|
5525
|
+
const objectElement = document.getElementById(this.unrealInitialConfig?.screenLockerContainerId ||
|
|
5526
|
+
SCREEN_LOCKER_CONTAINER_ID);
|
|
5708
5527
|
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);
|
|
5528
|
+
const objectDocument = objectElement.contentWindow;
|
|
5529
|
+
objectDocument?.postMessage(text, '*');
|
|
5718
5530
|
}
|
|
5719
|
-
catch
|
|
5720
|
-
|
|
5721
|
-
this.store.dispatch(disconnectStream({
|
|
5722
|
-
reason: DisconnectReason.WebRTCError,
|
|
5723
|
-
message: 'WebRTCError: Offer Error',
|
|
5724
|
-
}));
|
|
5531
|
+
catch {
|
|
5532
|
+
// Fails silently if the iframe's content window cannot be accessed
|
|
5725
5533
|
}
|
|
5726
5534
|
}
|
|
5727
|
-
|
|
5728
|
-
|
|
5729
|
-
|
|
5730
|
-
|
|
5731
|
-
|
|
5732
|
-
|
|
5733
|
-
|
|
5734
|
-
|
|
5735
|
-
|
|
5736
|
-
|
|
5737
|
-
this.
|
|
5738
|
-
this.
|
|
5739
|
-
this.
|
|
5740
|
-
this.
|
|
5741
|
-
|
|
5742
|
-
|
|
5535
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: UnrealStatusComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
5536
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: UnrealStatusComponent, isStandalone: true, selector: "app-unreal-status", ngImport: i0, template: "@if (showLoader()) {\n <div class=\"status-box\">\n @if (awsInstance()?.message) {\n <p>{{ awsInstance()?.message }}</p>\n } @else {\n <div class=\"status-box__message\">\n <src-loading\n [size]=\"30\"\n [backgroundStrokeColor]=\"'#E5E7EB'\"\n [progressStrokeColor]=\"'#1F2937'\"\n />\n {{ messagePercents() }}%\n </div>\n }\n </div>\n}\n", styles: ["app-stat-graph{height:30px;width:100%}.status-box{position:absolute;top:80%;left:50%;transform:translate(-50%,-50%);z-index:5;display:flex;justify-content:center;align-items:center;width:auto;max-width:500px;margin:0;background:var(--src-surface-container-main, #fff);border:none;border-radius:var(--src-border-rounded-full, 9999px);box-shadow:0 8px 20px #1718181f,0 3px 6px #17181814;pointer-events:none}.status-box p{padding:10px 15px;margin:0;text-align:center}.status-box__message{display:flex;align-items:center;padding:12px 20px 12px 16px;color:var(--src-text-body-main, #1f2937);font-size:16px;font-weight:400;line-height:24px}.status-box__message .status-box__message_text{display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden}.status-box__message src-loading{margin-right:12px}\n"], dependencies: [{ kind: "component", type: SourceLoadingComponent, selector: "src-loading", inputs: ["size", "progress", "lineCap", "backgroundStrokeColor", "progressStrokeColor", "strokeWidth", "data-testid", "role", "ariaLabel", "ariaValueNow", "ariaValueMin", "ariaValueMax"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
5537
|
+
}
|
|
5538
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: UnrealStatusComponent, decorators: [{
|
|
5539
|
+
type: Component,
|
|
5540
|
+
args: [{ selector: 'app-unreal-status', changeDetection: ChangeDetectionStrategy.OnPush, imports: [SourceLoadingComponent], template: "@if (showLoader()) {\n <div class=\"status-box\">\n @if (awsInstance()?.message) {\n <p>{{ awsInstance()?.message }}</p>\n } @else {\n <div class=\"status-box__message\">\n <src-loading\n [size]=\"30\"\n [backgroundStrokeColor]=\"'#E5E7EB'\"\n [progressStrokeColor]=\"'#1F2937'\"\n />\n {{ messagePercents() }}%\n </div>\n }\n </div>\n}\n", styles: ["app-stat-graph{height:30px;width:100%}.status-box{position:absolute;top:80%;left:50%;transform:translate(-50%,-50%);z-index:5;display:flex;justify-content:center;align-items:center;width:auto;max-width:500px;margin:0;background:var(--src-surface-container-main, #fff);border:none;border-radius:var(--src-border-rounded-full, 9999px);box-shadow:0 8px 20px #1718181f,0 3px 6px #17181814;pointer-events:none}.status-box p{padding:10px 15px;margin:0;text-align:center}.status-box__message{display:flex;align-items:center;padding:12px 20px 12px 16px;color:var(--src-text-body-main, #1f2937);font-size:16px;font-weight:400;line-height:24px}.status-box__message .status-box__message_text{display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden}.status-box__message src-loading{margin-right:12px}\n"] }]
|
|
5541
|
+
}] });
|
|
5542
|
+
|
|
5543
|
+
class VideoStatsComponent {
|
|
5544
|
+
constructor() {
|
|
5545
|
+
this.collapsed = signal(false, ...(ngDevMode ? [{ debugName: "collapsed" }] : []));
|
|
5546
|
+
this.store = inject(Store);
|
|
5547
|
+
this.videoService = inject(VideoService);
|
|
5548
|
+
this.isDevMode = inject(DevModeService).isDevMode;
|
|
5549
|
+
this.obs$ = this.videoService.videoStats$;
|
|
5550
|
+
this.viewportReady = this.store.selectSignal(unrealFeature.selectViewportReady);
|
|
5551
|
+
this.ssInfo = this.store.selectSignal(selectSignalingParameters);
|
|
5552
|
+
this.fpsTick = toSignal(this.obs$.pipe(map$1((stat) => ({
|
|
5553
|
+
value: stat.aggregatedStats.framesPerSecond || 0,
|
|
5554
|
+
}))));
|
|
5555
|
+
this.fpsAvgTick = toSignal(this.obs$.pipe(map$1((stat) => ({
|
|
5556
|
+
value: stat.aggregatedStats.avgFrameRate,
|
|
5557
|
+
}))));
|
|
5558
|
+
this.bitrateTick = toSignal(this.obs$.pipe(map$1((stat) => ({
|
|
5559
|
+
value: stat.aggregatedStats.bitrate,
|
|
5560
|
+
}))));
|
|
5561
|
+
this.videoQP = toSignal(this.obs$.pipe(map$1((stat) => ({
|
|
5562
|
+
value: stat.aggregatedStats.VideoEncoderQP,
|
|
5563
|
+
}))));
|
|
5564
|
+
this.bitrateAverageTick = toSignal(this.obs$.pipe(map$1((stat) => ({
|
|
5565
|
+
value: stat.aggregatedStats.avgBitrate,
|
|
5566
|
+
}))));
|
|
5567
|
+
this.videoStatus = toSignal(this.obs$.pipe(map$1((event) => this.isDevMode &&
|
|
5568
|
+
{
|
|
5569
|
+
...event.aggregatedStats,
|
|
5570
|
+
quality: event.quality,
|
|
5571
|
+
}), filter(Boolean), map$1((data) => {
|
|
5572
|
+
return this.fields
|
|
5573
|
+
.map((key) => {
|
|
5574
|
+
return {
|
|
5575
|
+
key,
|
|
5576
|
+
value: data[key],
|
|
5577
|
+
};
|
|
5578
|
+
})
|
|
5579
|
+
.filter((r) => !this.elementsToShow.includes(r.key));
|
|
5580
|
+
})));
|
|
5581
|
+
this.graphList = [];
|
|
5582
|
+
this.idKey = 'com.source3d.unreal.stat';
|
|
5583
|
+
this.elementsToShow = [];
|
|
5584
|
+
this.trigger$ = new BehaviorSubject(undefined);
|
|
5585
|
+
this.getFields();
|
|
5743
5586
|
}
|
|
5744
|
-
|
|
5745
|
-
|
|
5746
|
-
|
|
5587
|
+
ngOnInit() {
|
|
5588
|
+
this.fields = [
|
|
5589
|
+
'kalmanBitrate',
|
|
5590
|
+
'bitrateDrop',
|
|
5591
|
+
'runTime',
|
|
5592
|
+
'pixelRatio',
|
|
5593
|
+
'frameHeight',
|
|
5594
|
+
'frameWidth',
|
|
5595
|
+
':-------------',
|
|
5596
|
+
'jitter',
|
|
5597
|
+
'MBytesReceived',
|
|
5598
|
+
'framesDecoded',
|
|
5599
|
+
'framesDropped',
|
|
5600
|
+
'framesReceived',
|
|
5601
|
+
'keyFramesDecoded',
|
|
5602
|
+
'currentRoundTripTime',
|
|
5603
|
+
'packetsLost',
|
|
5604
|
+
':-------------',
|
|
5605
|
+
'quality',
|
|
5606
|
+
'videoCodec',
|
|
5607
|
+
'kind',
|
|
5608
|
+
'type',
|
|
5609
|
+
'codecs',
|
|
5610
|
+
'selectedPair',
|
|
5611
|
+
//'pliCount',
|
|
5612
|
+
//'sliCount',
|
|
5613
|
+
//'transportId',
|
|
5614
|
+
//'id',
|
|
5615
|
+
];
|
|
5616
|
+
this.updateGraph();
|
|
5747
5617
|
}
|
|
5748
|
-
|
|
5749
|
-
|
|
5750
|
-
const
|
|
5751
|
-
|
|
5752
|
-
|
|
5753
|
-
|
|
5754
|
-
|
|
5755
|
-
|
|
5756
|
-
|
|
5757
|
-
|
|
5758
|
-
}
|
|
5759
|
-
|
|
5760
|
-
|
|
5761
|
-
});
|
|
5762
|
-
}
|
|
5763
|
-
catch (e) {
|
|
5764
|
-
Logger.error('Error handleCandidateFromServer', e);
|
|
5765
|
-
}
|
|
5618
|
+
updateGraph() {
|
|
5619
|
+
this.graphList = this.fields.map((key) => {
|
|
5620
|
+
const stat = this.obs$.pipe(map$1((val) => {
|
|
5621
|
+
const value = val.aggregatedStats[key];
|
|
5622
|
+
return { value };
|
|
5623
|
+
}));
|
|
5624
|
+
return {
|
|
5625
|
+
key,
|
|
5626
|
+
color: Math.floor(Math.random() * 16777216).toString(),
|
|
5627
|
+
stat,
|
|
5628
|
+
};
|
|
5629
|
+
});
|
|
5630
|
+
this.graphList$ = of(this.graphList).pipe(combineLatestWith(this.trigger$), map$1(([graphList]) => graphList.filter((r) => this.elementsToShow.includes(r.key))));
|
|
5766
5631
|
}
|
|
5767
|
-
|
|
5768
|
-
|
|
5769
|
-
|
|
5770
|
-
* prefer that profile during codec negotiation.
|
|
5771
|
-
*
|
|
5772
|
-
* A `profile-level-id` is a 3-byte hex string (`XXYYZZ`) defined in
|
|
5773
|
-
* RFC 6184 that encodes three H.264 parameters:
|
|
5774
|
-
* - **Profile IDC** (`XX`) — the H.264 profile.
|
|
5775
|
-
* - **Profile IOP** (`YY`) — constraint flags that further narrow the profile.
|
|
5776
|
-
* - **Level IDC** (`ZZ`) — the level, which caps resolution/framerate/bitrate.
|
|
5777
|
-
*
|
|
5778
|
-
* ### H.264 profiles relevant to Unreal Engine Pixel Streaming
|
|
5779
|
-
*
|
|
5780
|
-
* | profile-level-id | Profile | Level | Notes |
|
|
5781
|
-
* |------------------|----------------------------|-------|----------------------------------------------|
|
|
5782
|
-
* | `42001f` | Baseline | 3.1 | No B-frames, low latency; legacy devices |
|
|
5783
|
-
* | `42e01f` | Constrained Baseline | 3.1 | Widest HW decode support; **recommended** |
|
|
5784
|
-
* | `4d001f` | Main | 3.1 | B-frames + CABAC; better compression |
|
|
5785
|
-
* | `4d4028` | Main | 4.0 | Up to 1080p60; common desktop target |
|
|
5786
|
-
* | `640028` | High | 4.0 | Best quality/compression; most GPUs support |
|
|
5787
|
-
* | `64002a` | High | 4.2 | Up to 1080p120 or 4K30 |
|
|
5788
|
-
* | `640033` | High | 5.1 | Up to 4K60; requires modern HW |
|
|
5789
|
-
* | `640034` | High | 5.2 | Up to 4K120; high-end HW only |
|
|
5790
|
-
* | `6e001f` | High 10 | 3.1 | 10-bit color; limited browser support |
|
|
5791
|
-
* | `f4001f` | High 4:4:4 Predictive | 3.1 | Lossless capable; rare in browsers |
|
|
5792
|
-
*
|
|
5793
|
-
* UE Pixel Streaming defaults to Constrained Baseline (`42e01f`) or High (`6400xx`)
|
|
5794
|
-
* depending on engine version and `WebRTC.H264Profile` CVar.
|
|
5795
|
-
*
|
|
5796
|
-
* We prioritize `42e01f` because it guarantees hardware decoding on
|
|
5797
|
-
* virtually all client devices (desktop, mobile, embedded), avoiding
|
|
5798
|
-
* fallback to software decoding or negotiation of a higher profile
|
|
5799
|
-
* that the client GPU may not accelerate.
|
|
5800
|
-
*/
|
|
5801
|
-
prioritizeH264Profile(sdp, profileLevelId) {
|
|
5802
|
-
const lines = sdp.split('\r\n');
|
|
5803
|
-
const targetPts = [];
|
|
5804
|
-
for (const line of lines) {
|
|
5805
|
-
if (line.startsWith('a=fmtp:') &&
|
|
5806
|
-
line.toLowerCase().includes(`profile-level-id=${profileLevelId}`)) {
|
|
5807
|
-
const pt = line.split(':')[1]?.split(' ')[0];
|
|
5808
|
-
if (pt)
|
|
5809
|
-
targetPts.push(pt);
|
|
5810
|
-
}
|
|
5811
|
-
}
|
|
5812
|
-
if (targetPts.length === 0)
|
|
5813
|
-
return sdp;
|
|
5814
|
-
return lines
|
|
5815
|
-
.map((line) => {
|
|
5816
|
-
if (!line.startsWith('m=video'))
|
|
5817
|
-
return line;
|
|
5818
|
-
const parts = line.split(' ');
|
|
5819
|
-
// parts: m=video <port> <proto> <pt1> <pt2> ...
|
|
5820
|
-
const header = parts.slice(0, 3);
|
|
5821
|
-
const pts = parts.slice(3);
|
|
5822
|
-
const prioritized = [
|
|
5823
|
-
...targetPts.filter((pt) => pts.includes(pt)),
|
|
5824
|
-
...pts.filter((pt) => !targetPts.includes(pt)),
|
|
5825
|
-
];
|
|
5826
|
-
return [...header, ...prioritized].join(' ');
|
|
5827
|
-
})
|
|
5828
|
-
.join('\r\n');
|
|
5632
|
+
toggle() {
|
|
5633
|
+
this.collapsed.update((collapsed) => !collapsed);
|
|
5634
|
+
this.writeFields();
|
|
5829
5635
|
}
|
|
5830
|
-
|
|
5831
|
-
|
|
5832
|
-
|
|
5833
|
-
|
|
5834
|
-
|
|
5835
|
-
|
|
5836
|
-
|
|
5837
|
-
|
|
5636
|
+
toggleGraph($event, el, value) {
|
|
5637
|
+
$event.stopPropagation();
|
|
5638
|
+
if ((value && typeof value !== 'number') ||
|
|
5639
|
+
(typeof value === 'number' && isNaN(value))) {
|
|
5640
|
+
return;
|
|
5641
|
+
}
|
|
5642
|
+
if (this.elementsToShow.includes(el)) {
|
|
5643
|
+
this.elementsToShow = this.elementsToShow.filter((item) => item !== el);
|
|
5838
5644
|
}
|
|
5839
|
-
|
|
5840
|
-
|
|
5841
|
-
|
|
5842
|
-
|
|
5843
|
-
|
|
5844
|
-
mungedSDP = mungedSDP?.replace('useinbandfec=1', audioSDP);
|
|
5845
|
-
return mungedSDP;
|
|
5645
|
+
else {
|
|
5646
|
+
this.elementsToShow.push(el);
|
|
5647
|
+
}
|
|
5648
|
+
this.writeFields();
|
|
5649
|
+
this.trigger$.next();
|
|
5846
5650
|
}
|
|
5847
|
-
|
|
5848
|
-
|
|
5849
|
-
|
|
5850
|
-
|
|
5851
|
-
|
|
5852
|
-
Logger.warn('Closing existing PeerConnection');
|
|
5651
|
+
getFields() {
|
|
5652
|
+
try {
|
|
5653
|
+
const { elements, collapsed } = JSON.parse(localStorage.getItem(this.idKey)) || [];
|
|
5654
|
+
this.elementsToShow = elements || [];
|
|
5655
|
+
this.collapsed.set(collapsed);
|
|
5853
5656
|
}
|
|
5854
|
-
|
|
5855
|
-
|
|
5856
|
-
if (this.dcClient) {
|
|
5857
|
-
Logger.warn('Closing existing DataChannel');
|
|
5657
|
+
catch {
|
|
5658
|
+
this.elementsToShow = [];
|
|
5858
5659
|
}
|
|
5859
|
-
this.dcClient?.close();
|
|
5860
|
-
this.dcClient = null;
|
|
5861
|
-
this.remoteDescriptionSet = false;
|
|
5862
|
-
this.iceCandidateBuffer.length = 0;
|
|
5863
5660
|
}
|
|
5864
|
-
|
|
5865
|
-
|
|
5661
|
+
writeFields() {
|
|
5662
|
+
localStorage.setItem(this.idKey, JSON.stringify({
|
|
5663
|
+
collapsed: this.collapsed(),
|
|
5664
|
+
elements: this.elementsToShow,
|
|
5665
|
+
}));
|
|
5866
5666
|
}
|
|
5867
|
-
|
|
5868
|
-
if (
|
|
5869
|
-
|
|
5667
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: VideoStatsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
5668
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: VideoStatsComponent, isStandalone: true, selector: "app-video-stats", ngImport: i0, template: "@if (videoStatus() && viewportReady()) {\n <div class=\"settings-container\">\n <button (click)=\"toggle()\" class=\"gear-button\">Stats</button>\n <div (click)=\"toggle()\" [class.min]=\"collapsed()\" id=\"aggregatedStats\">\n <div class=\"forNerds\">\n <div (click)=\"$event.stopPropagation()\" class=\"static\">\n <li>\n <app-stat-graph\n [stat]=\"fpsTick()!\"\n label=\"FPS (higher is better)\"\n />\n </li>\n <li>\n <app-stat-graph\n [stat]=\"videoQP()!\"\n label=\"QP (lower is better)\"\n color=\"#D5ff07\"\n />\n </li>\n <li>\n <app-stat-graph\n [stat]=\"bitrateTick()!\"\n label=\"Bitrate\"\n color=\"#D57F07\"\n />\n </li>\n </div>\n <div>\n @for (graph of graphList$ | async; track graph) {\n <li (click)=\"toggleGraph($event, graph.key, 0)\" class=\"graph\">\n <app-stat-graph\n [label]=\"graph.key\"\n [stat]=\"(graph.stat | async)!\"\n [color]=\"graph.color\"\n />\n </li>\n }\n </div>\n </div>\n\n @for (el of videoStatus(); track el.key + $index) {\n <div (click)=\"toggleGraph($event, el.key, el.value)\" class=\"stat\">\n <span>{{ el.key }}: </span>{{ el.value | json }}\n </div>\n }\n </div>\n </div>\n @if (!collapsed()) {\n <div [innerHTML]=\"ssInfo() | safe: 'html'\" class=\"ssInfo\"></div>\n }\n}\n", styles: [".settings-container{position:absolute;top:65px;left:10px;z-index:1000;bottom:10px}.gear-button{left:0;width:50px;position:absolute;background:none;cursor:pointer;font-size:10px;font-weight:500;border:1px solid;border-radius:9px;background:#0162cc;color:#fff;padding:4px}.gear-button>*{color:#fff;filter:drop-shadow(1px 1px 1px rgb(0,0,0))}#aggregatedStats{max-height:calc(100% - 25px);overflow-y:auto;margin-top:25px;left:20px;z-index:31;max-width:400px;padding:10px;background-color:#fff3;border:1px solid grey}#aggregatedStats>*{color:#fff;text-shadow:-1px -1px 0 #000,1px -1px 0 #000,-1px 1px 0 #000,1px 1px 0 #000}#aggregatedStats:empty{display:none}#aggregatedStats button{position:absolute;right:5px;top:5px}#aggregatedStats.min{display:none}#aggregatedStats .forNerds{width:100%}#aggregatedStats .forNerds li{list-style-type:none;height:60px}#aggregatedStats .forNerds li app-stat-graph{display:block;width:100%;height:100%}#aggregatedStats .stat span{font-weight:700}#aggregatedStats .stat,#aggregatedStats .graph{cursor:pointer}#aggregatedStats .static{pointer-events:all}.ssInfo{position:absolute;top:0;left:50%;transform:translate(-50%);color:#fff;text-shadow:-1px -1px 0 #000,1px -1px 0 #000,-1px 1px 0 #000,1px 1px 0 #000;text-align:center;width:80%;pointer-events:none}\n"], dependencies: [{ kind: "component", type: StatGraphComponent, selector: "app-stat-graph", inputs: ["label", "stat", "color", "tickStep"] }, { kind: "pipe", type: AsyncPipe, name: "async" }, { kind: "pipe", type: JsonPipe, name: "json" }, { kind: "pipe", type: SafePipe, name: "safe" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
5669
|
+
}
|
|
5670
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: VideoStatsComponent, decorators: [{
|
|
5671
|
+
type: Component,
|
|
5672
|
+
args: [{ selector: 'app-video-stats', changeDetection: ChangeDetectionStrategy.OnPush, imports: [StatGraphComponent, AsyncPipe, JsonPipe, SafePipe], template: "@if (videoStatus() && viewportReady()) {\n <div class=\"settings-container\">\n <button (click)=\"toggle()\" class=\"gear-button\">Stats</button>\n <div (click)=\"toggle()\" [class.min]=\"collapsed()\" id=\"aggregatedStats\">\n <div class=\"forNerds\">\n <div (click)=\"$event.stopPropagation()\" class=\"static\">\n <li>\n <app-stat-graph\n [stat]=\"fpsTick()!\"\n label=\"FPS (higher is better)\"\n />\n </li>\n <li>\n <app-stat-graph\n [stat]=\"videoQP()!\"\n label=\"QP (lower is better)\"\n color=\"#D5ff07\"\n />\n </li>\n <li>\n <app-stat-graph\n [stat]=\"bitrateTick()!\"\n label=\"Bitrate\"\n color=\"#D57F07\"\n />\n </li>\n </div>\n <div>\n @for (graph of graphList$ | async; track graph) {\n <li (click)=\"toggleGraph($event, graph.key, 0)\" class=\"graph\">\n <app-stat-graph\n [label]=\"graph.key\"\n [stat]=\"(graph.stat | async)!\"\n [color]=\"graph.color\"\n />\n </li>\n }\n </div>\n </div>\n\n @for (el of videoStatus(); track el.key + $index) {\n <div (click)=\"toggleGraph($event, el.key, el.value)\" class=\"stat\">\n <span>{{ el.key }}: </span>{{ el.value | json }}\n </div>\n }\n </div>\n </div>\n @if (!collapsed()) {\n <div [innerHTML]=\"ssInfo() | safe: 'html'\" class=\"ssInfo\"></div>\n }\n}\n", styles: [".settings-container{position:absolute;top:65px;left:10px;z-index:1000;bottom:10px}.gear-button{left:0;width:50px;position:absolute;background:none;cursor:pointer;font-size:10px;font-weight:500;border:1px solid;border-radius:9px;background:#0162cc;color:#fff;padding:4px}.gear-button>*{color:#fff;filter:drop-shadow(1px 1px 1px rgb(0,0,0))}#aggregatedStats{max-height:calc(100% - 25px);overflow-y:auto;margin-top:25px;left:20px;z-index:31;max-width:400px;padding:10px;background-color:#fff3;border:1px solid grey}#aggregatedStats>*{color:#fff;text-shadow:-1px -1px 0 #000,1px -1px 0 #000,-1px 1px 0 #000,1px 1px 0 #000}#aggregatedStats:empty{display:none}#aggregatedStats button{position:absolute;right:5px;top:5px}#aggregatedStats.min{display:none}#aggregatedStats .forNerds{width:100%}#aggregatedStats .forNerds li{list-style-type:none;height:60px}#aggregatedStats .forNerds li app-stat-graph{display:block;width:100%;height:100%}#aggregatedStats .stat span{font-weight:700}#aggregatedStats .stat,#aggregatedStats .graph{cursor:pointer}#aggregatedStats .static{pointer-events:all}.ssInfo{position:absolute;top:0;left:50%;transform:translate(-50%);color:#fff;text-shadow:-1px -1px 0 #000,1px -1px 0 #000,-1px 1px 0 #000,1px 1px 0 #000;text-align:center;width:80%;pointer-events:none}\n"] }]
|
|
5673
|
+
}], ctorParameters: () => [] });
|
|
5674
|
+
|
|
5675
|
+
class ResumeStreamButtonComponent {
|
|
5676
|
+
constructor() {
|
|
5677
|
+
this.actions$ = inject(Actions);
|
|
5678
|
+
this.store = inject(Store);
|
|
5679
|
+
this.videoService = inject(VideoService);
|
|
5680
|
+
this.destroyRef = inject(DestroyRef);
|
|
5681
|
+
this.showReconnectPopup = this.store.selectSignal(selectShowReconnectPopup);
|
|
5682
|
+
this.isLoaderScreenVisible = this.store.selectSignal(selectIsLoaderScreenVisible);
|
|
5683
|
+
this.streamConfig = this.store.selectSignal(selectStreamConfig);
|
|
5684
|
+
this.isExistMatchUrls = this.store.selectSignal(selectIsExistMatchUrls);
|
|
5685
|
+
this.isSecondStart = signal(!!(this.streamConfig()?.autoStart && location.href.match(/^https/gi)), ...(ngDevMode ? [{ debugName: "isSecondStart" }] : []));
|
|
5686
|
+
this.isShowResumeButton = computed(() => this.isLoaderScreenVisible() &&
|
|
5687
|
+
this.showReconnectPopup() &&
|
|
5688
|
+
this.isExistMatchUrls(), ...(ngDevMode ? [{ debugName: "isShowResumeButton" }] : []));
|
|
5689
|
+
}
|
|
5690
|
+
ngOnInit() {
|
|
5691
|
+
this.store
|
|
5692
|
+
.select(unrealFeature.selectCirrusConnected)
|
|
5693
|
+
.pipe(skip$1(1), filter(Falsy), takeUntilDestroyed(this.destroyRef))
|
|
5694
|
+
.subscribe(() => this.onDisconnect());
|
|
5695
|
+
}
|
|
5696
|
+
ngAfterViewInit() {
|
|
5697
|
+
this.actions$
|
|
5698
|
+
.pipe(ofType(videoStreamInitialized), takeUntilDestroyed(this.destroyRef), filter(() => !!this.videoService.video?.paused))
|
|
5699
|
+
.subscribe(() => {
|
|
5700
|
+
Logger.info('Simulate Press Play');
|
|
5701
|
+
this.videoService.play();
|
|
5702
|
+
this.store.dispatch(resetAfk());
|
|
5703
|
+
});
|
|
5704
|
+
}
|
|
5705
|
+
connect() {
|
|
5706
|
+
this.store.dispatch(startStream({ config: { autoStart: true } }));
|
|
5707
|
+
}
|
|
5708
|
+
onDisconnect() {
|
|
5709
|
+
this.isSecondStart.set(true);
|
|
5710
|
+
}
|
|
5711
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ResumeStreamButtonComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
5712
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: ResumeStreamButtonComponent, isStandalone: true, selector: "app-resume-stream-button", ngImport: i0, template: "@if (isShowResumeButton()) {\n <div class=\"resume-box\">\n @if (isSecondStart()) {\n <div class=\"resume-box__text\">\n Your stream has been paused due to inactivity\n </div>\n }\n\n <src-button\n (onClick)=\"connect()\"\n [isFullWidth]=\"true\"\n [data-testid]=\"'connect-button'\"\n size=\"lg\"\n weight=\"primary\"\n class=\"connect-button\"\n >\n {{ isSecondStart() ? 'Resume' : 'Start' }}\n </src-button>\n </div>\n}\n", styles: [".resume-box{padding:16px;position:absolute;bottom:0;left:50%;gap:12px;z-index:6;display:flex;transform:translate(-50%,-50%);flex-direction:column;align-items:center;border-radius:var(--src-border-rounded-parent, 8px);background:var(--src-surface-container-main, #fff);box-shadow:0 26px 80px #0003,0 0 1px #0003}.resume-box .connect-button{width:100%}.resume-box .resume-box__text{color:var(--src-color-grey-500, #6b7280);text-align:center;font-family:var(--src-font-family-body);font-size:14px;font-style:normal;font-weight:400;line-height:24px}\n"], dependencies: [{ kind: "component", type: SourceButtonComponent, selector: "src-button", inputs: ["type", "appearance", "weight", "size", "context", "customClass", "isFullWidth", "isPressed", "isDisabled", "isLoading", "isInverted", "iconButton", "srcButtonConfig", "formID", "data-testid", "ariaLabel"], outputs: ["onClick", "onSubmit"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
5713
|
+
}
|
|
5714
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ResumeStreamButtonComponent, decorators: [{
|
|
5715
|
+
type: Component,
|
|
5716
|
+
args: [{ selector: 'app-resume-stream-button', changeDetection: ChangeDetectionStrategy.OnPush, imports: [SourceButtonComponent], template: "@if (isShowResumeButton()) {\n <div class=\"resume-box\">\n @if (isSecondStart()) {\n <div class=\"resume-box__text\">\n Your stream has been paused due to inactivity\n </div>\n }\n\n <src-button\n (onClick)=\"connect()\"\n [isFullWidth]=\"true\"\n [data-testid]=\"'connect-button'\"\n size=\"lg\"\n weight=\"primary\"\n class=\"connect-button\"\n >\n {{ isSecondStart() ? 'Resume' : 'Start' }}\n </src-button>\n </div>\n}\n", styles: [".resume-box{padding:16px;position:absolute;bottom:0;left:50%;gap:12px;z-index:6;display:flex;transform:translate(-50%,-50%);flex-direction:column;align-items:center;border-radius:var(--src-border-rounded-parent, 8px);background:var(--src-surface-container-main, #fff);box-shadow:0 26px 80px #0003,0 0 1px #0003}.resume-box .connect-button{width:100%}.resume-box .resume-box__text{color:var(--src-color-grey-500, #6b7280);text-align:center;font-family:var(--src-font-family-body);font-size:14px;font-style:normal;font-weight:400;line-height:24px}\n"] }]
|
|
5717
|
+
}] });
|
|
5718
|
+
|
|
5719
|
+
class IntroSrcComponent {
|
|
5720
|
+
constructor() {
|
|
5721
|
+
this.videoElement = viewChild('videoPlayerCover', ...(ngDevMode ? [{ debugName: "videoElement" }] : []));
|
|
5722
|
+
this.store = inject(Store);
|
|
5723
|
+
this.imageIntroSrc = this.store.selectSignal(unrealFeature.selectImageIntroSrc);
|
|
5724
|
+
this.videoIntroSrc = this.store.selectSignal(unrealFeature.selectVideoIntroSrc);
|
|
5725
|
+
this.viewportReady = this.store.selectSignal(unrealFeature.selectViewportReady);
|
|
5726
|
+
effect(() => {
|
|
5727
|
+
const viewportReady = this.viewportReady();
|
|
5728
|
+
const videoIntroSrc = this.videoIntroSrc();
|
|
5729
|
+
const imageIntroSrc = this.imageIntroSrc();
|
|
5730
|
+
untracked(() => {
|
|
5731
|
+
if (viewportReady && (videoIntroSrc || imageIntroSrc)) {
|
|
5732
|
+
this.store.dispatch(resetIntroSrc());
|
|
5733
|
+
}
|
|
5734
|
+
});
|
|
5735
|
+
});
|
|
5736
|
+
effect(() => {
|
|
5737
|
+
const videoIntroSrc = this.videoIntroSrc();
|
|
5738
|
+
const videoElement = this.videoElement();
|
|
5739
|
+
if (!videoIntroSrc || !videoElement) {
|
|
5740
|
+
return;
|
|
5741
|
+
}
|
|
5742
|
+
untracked(() => this.createVideo(videoIntroSrc, videoElement.nativeElement));
|
|
5743
|
+
});
|
|
5744
|
+
}
|
|
5745
|
+
createVideo(src, videoContainer) {
|
|
5746
|
+
const matchAbsolutePath = !!src.match(/.*\.(mp4|WebM)(\?.*)?$/i);
|
|
5747
|
+
videoContainer.innerHTML = ''; // This will remove all existing content within the layout
|
|
5748
|
+
const videoElement = document.createElement('video');
|
|
5749
|
+
videoElement.autoplay = true;
|
|
5750
|
+
videoElement.muted = true;
|
|
5751
|
+
videoElement.loop = true;
|
|
5752
|
+
if (!matchAbsolutePath) {
|
|
5753
|
+
videoElement.poster = `${src}/cover.webp`;
|
|
5754
|
+
}
|
|
5755
|
+
videoElement.style.position = 'absolute';
|
|
5756
|
+
videoElement.style.width = '100%';
|
|
5757
|
+
videoElement.style.height = '100%';
|
|
5758
|
+
videoElement.style.objectFit = 'cover';
|
|
5759
|
+
const sourceElement = document.createElement('source');
|
|
5760
|
+
if (matchAbsolutePath) {
|
|
5761
|
+
sourceElement.src = src;
|
|
5870
5762
|
}
|
|
5871
5763
|
else {
|
|
5872
|
-
|
|
5873
|
-
// Trigger peer reconnection if DC is not open and we're not already reconnecting
|
|
5874
|
-
this.tryReconnectOnSendFailure();
|
|
5764
|
+
sourceElement.src = `${src}/video.mp4`;
|
|
5875
5765
|
}
|
|
5766
|
+
sourceElement.type = 'video/mp4';
|
|
5767
|
+
videoElement.appendChild(sourceElement);
|
|
5768
|
+
videoElement.innerHTML += 'Browser not supported';
|
|
5769
|
+
videoContainer.appendChild(videoElement);
|
|
5876
5770
|
}
|
|
5877
|
-
|
|
5878
|
-
|
|
5879
|
-
|
|
5880
|
-
|
|
5881
|
-
|
|
5882
|
-
|
|
5883
|
-
|
|
5884
|
-
|
|
5885
|
-
|
|
5886
|
-
|
|
5887
|
-
if (!this.
|
|
5888
|
-
|
|
5889
|
-
return;
|
|
5771
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: IntroSrcComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
5772
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: IntroSrcComponent, isStandalone: true, selector: "app-intro-src", viewQueries: [{ propertyName: "videoElement", first: true, predicate: ["videoPlayerCover"], descendants: true, isSignal: true }], ngImport: i0, template: "@let image = imageIntroSrc();\n@if (image) {\n <div class=\"backImage\">\n <img [ngSrc]=\"image\" fill alt=\"imageSrc\" />\n </div>\n} @else if (videoIntroSrc()) {\n <div #videoPlayerCover class=\"backVideo\"></div>\n}\n", styles: [".backVideo{position:absolute;top:0;left:0;display:flex;justify-content:center;align-items:center;width:100%;height:100%;z-index:1}.backVideo video{position:absolute;width:100%;height:100%;object-fit:cover}.backImage{position:absolute;top:0;left:0;display:flex;justify-content:center;align-items:center;width:100%;height:100%;z-index:1}.backImage img{position:absolute;width:100%;height:100%;object-fit:cover}\n"], dependencies: [{ kind: "directive", type: NgOptimizedImage, selector: "img[ngSrc]", inputs: ["ngSrc", "ngSrcset", "sizes", "width", "height", "decoding", "loading", "priority", "loaderParams", "disableOptimizedSrcset", "fill", "placeholder", "placeholderConfig", "src", "srcset"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
5773
|
+
}
|
|
5774
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: IntroSrcComponent, decorators: [{
|
|
5775
|
+
type: Component,
|
|
5776
|
+
args: [{ selector: 'app-intro-src', imports: [NgOptimizedImage], changeDetection: ChangeDetectionStrategy.OnPush, template: "@let image = imageIntroSrc();\n@if (image) {\n <div class=\"backImage\">\n <img [ngSrc]=\"image\" fill alt=\"imageSrc\" />\n </div>\n} @else if (videoIntroSrc()) {\n <div #videoPlayerCover class=\"backVideo\"></div>\n}\n", styles: [".backVideo{position:absolute;top:0;left:0;display:flex;justify-content:center;align-items:center;width:100%;height:100%;z-index:1}.backVideo video{position:absolute;width:100%;height:100%;object-fit:cover}.backImage{position:absolute;top:0;left:0;display:flex;justify-content:center;align-items:center;width:100%;height:100%;z-index:1}.backImage img{position:absolute;width:100%;height:100%;object-fit:cover}\n"] }]
|
|
5777
|
+
}], ctorParameters: () => [], propDecorators: { videoElement: [{ type: i0.ViewChild, args: ['videoPlayerCover', { isSignal: true }] }] } });
|
|
5778
|
+
|
|
5779
|
+
class UnrealSceneComponent {
|
|
5780
|
+
get resizeValues() {
|
|
5781
|
+
if (!this.isStudio()) {
|
|
5782
|
+
return this.getResizeValuesUniversal();
|
|
5890
5783
|
}
|
|
5891
|
-
|
|
5892
|
-
|
|
5893
|
-
|
|
5894
|
-
|
|
5895
|
-
|
|
5784
|
+
return this.getResizeValues();
|
|
5785
|
+
}
|
|
5786
|
+
get pixelRatio() {
|
|
5787
|
+
return window.devicePixelRatio;
|
|
5788
|
+
}
|
|
5789
|
+
onMouseOver() {
|
|
5790
|
+
this.changeMouseOverScene.emit(true);
|
|
5791
|
+
}
|
|
5792
|
+
onMouseOut() {
|
|
5793
|
+
this.changeMouseOverScene.emit(false);
|
|
5794
|
+
}
|
|
5795
|
+
constructor() {
|
|
5796
|
+
this.isStudio = input(false, ...(ngDevMode ? [{ debugName: "isStudio" }] : []));
|
|
5797
|
+
this.useContainerAsSizeProvider = input(true, ...(ngDevMode ? [{ debugName: "useContainerAsSizeProvider" }] : []));
|
|
5798
|
+
this.studioResolutionSize = input({
|
|
5799
|
+
width: FULL_HD_WIDTH,
|
|
5800
|
+
height: FULL_HD_HEIGHT,
|
|
5801
|
+
}, ...(ngDevMode ? [{ debugName: "studioResolutionSize" }] : []));
|
|
5802
|
+
this.studioResolutionValues = toObservable(computed(() => {
|
|
5803
|
+
const values = this.studioResolutionSize();
|
|
5804
|
+
return { width: values.width, height: values.height };
|
|
5805
|
+
}));
|
|
5806
|
+
this.videoElement = viewChild('videoContainer', ...(ngDevMode ? [{ debugName: "videoElement" }] : []));
|
|
5807
|
+
this.changeMouseOverScene = output();
|
|
5808
|
+
this.width = FULL_HD_WIDTH;
|
|
5809
|
+
this.height = FULL_HD_HEIGHT;
|
|
5810
|
+
this.actions$ = inject(Actions);
|
|
5811
|
+
this.store = inject(Store);
|
|
5812
|
+
this.isExistMatchUrls = this.store.selectSignal(selectIsExistMatchUrls);
|
|
5813
|
+
this.isDevMode = inject(DevModeService).isDevMode;
|
|
5814
|
+
this.lightMode = this.store.selectSignal(unrealFeature.selectLowBandwidth);
|
|
5815
|
+
this.isFreezeFrameLoading = this.store.selectSignal(selectIsFreezeFrameLoading);
|
|
5816
|
+
this.commandsSender = inject(UnrealCommunicatorService);
|
|
5817
|
+
this.videoService = inject(VideoService);
|
|
5818
|
+
this.element = inject(ElementRef);
|
|
5819
|
+
this.destroyRef = inject(DestroyRef);
|
|
5820
|
+
this.destroyRef.onDestroy(() => this.store.dispatch(destroyUnrealScene()));
|
|
5821
|
+
effect(() => {
|
|
5822
|
+
const videoElement = this.videoElement();
|
|
5823
|
+
const isExistMatchUrls = this.isExistMatchUrls();
|
|
5824
|
+
if (videoElement && isExistMatchUrls) {
|
|
5825
|
+
this.store.dispatch(initSignalling());
|
|
5826
|
+
}
|
|
5827
|
+
});
|
|
5828
|
+
afterNextRender({
|
|
5829
|
+
read: () => {
|
|
5830
|
+
const videoElement = this.videoElement();
|
|
5831
|
+
if (videoElement) {
|
|
5832
|
+
this.videoService.setContainer(videoElement.nativeElement);
|
|
5833
|
+
if (this.element?.nativeElement) {
|
|
5834
|
+
fromEvent(this.element?.nativeElement, 'contextmenu')
|
|
5835
|
+
.pipe(takeUntilDestroyed(this.destroyRef))
|
|
5836
|
+
.subscribe((event) => event.preventDefault());
|
|
5837
|
+
}
|
|
5838
|
+
this.listenResizeValues();
|
|
5839
|
+
}
|
|
5840
|
+
},
|
|
5841
|
+
});
|
|
5842
|
+
}
|
|
5843
|
+
listenResizeValues() {
|
|
5844
|
+
const customSizeInputChanged$ = this.studioResolutionValues.pipe(filter(Truthy), distinctUntilChanged(), filter(({ width, height }) => this.width !== width || this.height !== height), map$1(({ width, height }) => ({ width, height })), tap(({ width, height }) => {
|
|
5845
|
+
this.width = width;
|
|
5846
|
+
this.height = height;
|
|
5847
|
+
}));
|
|
5848
|
+
const resizeRequired$ = merge(this.actions$.pipe(ofType(videoStreamInitialized)), fromEvent(window, 'resize'));
|
|
5849
|
+
const videoStreamSizeChanged$ = this.videoService?.videoStats$.pipe(filter(() => !this.isFreezeFrameLoading()), map$1((stats) => ({
|
|
5850
|
+
w: stats.aggregatedStats.frameWidth || 0,
|
|
5851
|
+
h: stats.aggregatedStats.frameHeight || 0,
|
|
5852
|
+
})), distinctUntilChanged((previous, current) => fpIsASameAsB(previous.w, current.w, SAME_SIZE_THRESHOLD) &&
|
|
5853
|
+
fpIsASameAsB(previous.h, current.h, SAME_SIZE_THRESHOLD)));
|
|
5854
|
+
merge(customSizeInputChanged$, resizeRequired$, videoStreamSizeChanged$)
|
|
5855
|
+
.pipe(debounceTime(DEBOUNCE_TO_MANY_RESIZE_CALLS), switchMap(() => {
|
|
5856
|
+
const key = Math.random();
|
|
5857
|
+
// Monitoring the size of the video stream
|
|
5858
|
+
return timer(0, 300).pipe(take(3), map$1(() => key));
|
|
5859
|
+
}), map$1((key) => ({ key, ...this.resizeValues })), distinctUntilChanged((previous, current) => fpIsASameAsB(previous.w, current.w, SAME_SIZE_THRESHOLD) &&
|
|
5860
|
+
fpIsASameAsB(previous.h, current.h, SAME_SIZE_THRESHOLD) &&
|
|
5861
|
+
previous.key === current.key), tap((size) => this.adaptVideo(size)), distinctUntilChanged((previous, current) => fpIsASameAsB(previous.w, current.w, SAME_SIZE_THRESHOLD) &&
|
|
5862
|
+
fpIsASameAsB(previous.h, current.h, SAME_SIZE_THRESHOLD)), combineLatestWith(this.store
|
|
5863
|
+
.select(unrealFeature.selectDataChannelConnected)
|
|
5864
|
+
.pipe(filter(Truthy))), takeUntilDestroyed(this.destroyRef))
|
|
5865
|
+
.subscribe(([size]) => {
|
|
5866
|
+
this.store.dispatch(changeStreamResolutionAction({ width: size.w, height: size.h }));
|
|
5867
|
+
this.sendResize(size);
|
|
5868
|
+
});
|
|
5869
|
+
this.actions$
|
|
5870
|
+
.pipe(ofType(forceResizeViewport), takeUntilDestroyed(this.destroyRef))
|
|
5871
|
+
.subscribe(() => this.sendResize(this.resizeValues));
|
|
5872
|
+
}
|
|
5873
|
+
adaptVideo(size) {
|
|
5874
|
+
const video = this.videoService.video;
|
|
5875
|
+
const areaSize = this.resizeValues;
|
|
5876
|
+
if (video?.style) {
|
|
5877
|
+
const pR = this.pixelRatio;
|
|
5878
|
+
video.width = this.makeEven(size.w / pR / areaSize.scale);
|
|
5879
|
+
video.height = this.makeEven(size.h / pR / areaSize.scale);
|
|
5880
|
+
if (!this.isStudio()) {
|
|
5881
|
+
video.style.overflow = 'hidden';
|
|
5882
|
+
video.style.objectFit = 'cover';
|
|
5883
|
+
video.style.width = `${video.width}px`;
|
|
5884
|
+
video.style.height = `${video.height}px`;
|
|
5885
|
+
}
|
|
5896
5886
|
}
|
|
5887
|
+
this.store.dispatch(videoAdaptedToContainer());
|
|
5897
5888
|
}
|
|
5898
5889
|
/**
|
|
5899
|
-
*
|
|
5900
|
-
*
|
|
5901
|
-
*
|
|
5902
|
-
*
|
|
5890
|
+
* This method calculates the dimensions for resizing a layout in a configurator stream.
|
|
5891
|
+
* It takes into account the pixel ratio of the device's screen and the orientation of the layout.
|
|
5892
|
+
* The method uses the `fitIntoRectangle` function to calculate the dimensions that would allow the layout to fit into the maximum space while maintaining its aspect ratio.
|
|
5893
|
+
* It also sets the maximum limits for the width and height based on the layout's orientation.
|
|
5894
|
+
* If the scale of the fitted dimensions is less than 1, it assigns the fitted dimensions to the output. Otherwise, it sets the scale of the output to 1.
|
|
5895
|
+
* @returns {Area} An object of type `Area` that contains the calculated dimensions for resizing the layout.
|
|
5903
5896
|
*/
|
|
5904
|
-
|
|
5905
|
-
|
|
5906
|
-
|
|
5907
|
-
|
|
5897
|
+
getResizeValuesUniversal() {
|
|
5898
|
+
const pR = this.pixelRatio;
|
|
5899
|
+
let containerW = window.innerWidth || 2;
|
|
5900
|
+
let containerH = window.innerHeight || 2;
|
|
5901
|
+
if (this.useContainerAsSizeProvider()) {
|
|
5902
|
+
containerW = this.videoElement()?.nativeElement.offsetWidth || 2;
|
|
5903
|
+
containerH = this.videoElement()?.nativeElement.offsetHeight || 2;
|
|
5908
5904
|
}
|
|
5909
|
-
|
|
5910
|
-
|
|
5911
|
-
|
|
5905
|
+
const maxSpace = { w: containerW * pR, h: containerH * pR };
|
|
5906
|
+
const isVertical = containerW < containerH;
|
|
5907
|
+
let maxH = FULL_HD_HEIGHT;
|
|
5908
|
+
let maxW = FULL_HD_WIDTH;
|
|
5909
|
+
const params = new URLSearchParams(location.search);
|
|
5910
|
+
const maxRes = params.get('maxresolution');
|
|
5911
|
+
if (Truthy(maxRes)) {
|
|
5912
|
+
maxH = FULL_HD_HEIGHT * 2;
|
|
5913
|
+
maxW = FULL_HD_WIDTH * 2;
|
|
5912
5914
|
}
|
|
5913
|
-
|
|
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';
|
|
5915
|
+
const maxLimits = isVertical
|
|
5916
|
+
? { w: maxH, h: maxW, x: 0, y: 0 }
|
|
5917
|
+
: { w: maxW, h: maxH, x: 0, y: 0 };
|
|
5918
|
+
const props = fitIntoRectangle({ x: 0, y: 0, w: containerW, h: containerH }, { x: 0, y: 0, ...maxSpace });
|
|
5919
|
+
let out = { ...props };
|
|
5920
|
+
const fitIntoMax = fitIntoRectangle(props, maxLimits);
|
|
5921
|
+
if (fitIntoMax.scale < 1) {
|
|
5922
|
+
// If reducing size to 1920x1080 is required, do it
|
|
5923
|
+
out = fitIntoMax;
|
|
5930
5924
|
}
|
|
5931
|
-
|
|
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'");
|
|
5925
|
+
else {
|
|
5926
|
+
// Otherwise, use the lowest resolution because the layout size is smaller than 1920x1080
|
|
5927
|
+
out.scale = 1;
|
|
5942
5928
|
}
|
|
5943
|
-
|
|
5929
|
+
/**
|
|
5930
|
+
* Ensure that the size is always even.
|
|
5931
|
+
*/
|
|
5932
|
+
out.w = this.makeEven(out.w);
|
|
5933
|
+
out.h = this.makeEven(out.h);
|
|
5934
|
+
return { ...out };
|
|
5944
5935
|
}
|
|
5945
|
-
|
|
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);
|
|
5936
|
+
getResizeValues() {
|
|
5937
|
+
const pR = this.pixelRatio;
|
|
5938
|
+
const containerW = pR * (this.videoElement()?.nativeElement.offsetWidth || 2);
|
|
5939
|
+
const containerH = pR * (this.videoElement()?.nativeElement.offsetHeight || 2);
|
|
5940
|
+
const maxSpace = { w: containerW, h: containerH };
|
|
5941
|
+
const videoSizePixelPerfect = fitIntoRectangle({ x: 0, y: 0, w: this.width, h: this.height }, { x: 0, y: 0, ...maxSpace });
|
|
5942
|
+
const out = fitIntoRectangle(videoSizePixelPerfect, {
|
|
5943
|
+
x: 0,
|
|
5944
|
+
y: 0,
|
|
5945
|
+
w: Math.min(containerW, FULL_HD_WIDTH),
|
|
5946
|
+
h: Math.min(containerH, FULL_HD_HEIGHT),
|
|
5978
5947
|
});
|
|
5979
|
-
|
|
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
|
-
}
|
|
5948
|
+
/**
|
|
5949
|
+
* Ensure that the size is always even.
|
|
5950
|
+
*/
|
|
5951
|
+
out.w = this.makeEven(out.w);
|
|
5952
|
+
out.h = this.makeEven(out.h);
|
|
5953
|
+
return out;
|
|
5954
|
+
}
|
|
5955
|
+
sendResize(size) {
|
|
5956
|
+
this.commandsSender.sendCommandToUnreal({
|
|
5957
|
+
command: MetaBoxCommand.FChangeResolutionCommand,
|
|
5958
|
+
payload: { resolution: { x: size.w, y: size.h } },
|
|
5994
5959
|
});
|
|
5995
5960
|
}
|
|
5996
|
-
|
|
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' });
|
|
5961
|
+
makeEven(value) {
|
|
5962
|
+
return Math.floor((value + 1) / 2) * 2;
|
|
5963
|
+
}
|
|
5964
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: UnrealSceneComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
5965
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: UnrealSceneComponent, isStandalone: true, selector: "app-unreal-scene", inputs: { isStudio: { classPropertyName: "isStudio", publicName: "isStudio", isSignal: true, isRequired: false, transformFunction: null }, useContainerAsSizeProvider: { classPropertyName: "useContainerAsSizeProvider", publicName: "useContainerAsSizeProvider", isSignal: true, isRequired: false, transformFunction: null }, studioResolutionSize: { classPropertyName: "studioResolutionSize", publicName: "studioResolutionSize", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { changeMouseOverScene: "changeMouseOverScene" }, host: { listeners: { "mouseover": "onMouseOver()", "mouseout": "onMouseOut()" } }, viewQueries: [{ propertyName: "videoElement", first: true, predicate: ["videoContainer"], descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"frame unreal-container\">\n <div #videoContainer [class.lightModeCursor]=\"lightMode()\" id=\"player\"></div>\n\n <app-intro-src />\n <app-image-loading-src />\n <app-resume-stream-button />\n <app-unreal-status />\n <app-freeze-frame />\n <app-afk-timeout-modal />\n\n <ng-content select=\"app-pdf\"></ng-content>\n <ng-content select=\"app-show-case\"></ng-content>\n <ng-content select=\"app-low-bandwidth-detector\"></ng-content>\n\n @if (isDevMode) {\n <app-video-stats />\n }\n</div>\n", styles: [".unreal-container{position:absolute;top:0;left:0;width:100%;height:100%;background-color:#fff}.unreal-container #player{position:absolute;width:100%;height:100%;display:flex;justify-content:center;align-items:center;overflow:hidden}.unreal-container.select{cursor:url('data:image/svg+xml,<svg width=\"24\" height=\"24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M15.15 21.375a1.423 1.423 0 0 1-1.15.063 1.487 1.487 0 0 1-.85-.788l-3-6.45-2.325 3.25c-.283.4-.658.525-1.125.375-.467-.15-.7-.466-.7-.95V4.05c0-.417.188-.717.563-.9.375-.183.729-.142 1.062.125l10.1 7.95c.383.284.496.65.337 1.1-.158.45-.479.675-.962.675h-4.2l2.975 6.375c.183.384.204.767.063 1.15a1.487 1.487 0 0 1-.788.85Z\" fill=\"%23000\"/><path d=\"m12.697 20.861.002.005c.237.495.617.852 1.128 1.04.515.191 1.039.16 1.539-.08a1.987 1.987 0 0 0 1.04-1.128 1.922 1.922 0 0 0-.079-1.536v-.003L13.684 13.5H17.1c.32 0 .628-.075.89-.26.263-.184.438-.447.544-.749.106-.3.135-.617.04-.925a1.458 1.458 0 0 0-.545-.738L7.936 2.883a1.51 1.51 0 0 0-.768-.336 1.471 1.471 0 0 0-.825.154 1.495 1.495 0 0 0-.626.547 1.492 1.492 0 0 0-.217.802v12.825c0 .324.08.634.272.897.193.261.467.43.775.53.31.099.632.12.942.016.31-.103.555-.313.743-.578h.001l1.825-2.552 2.639 5.673Z\" stroke=\"%23fff\" stroke-opacity=\".7\"/></svg>') 6 2,auto}.unreal-container.orbit{cursor:url('data:image/svg+xml,<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" fill=\"none\"><path fill=\"%23000\" d=\"M11.998 21.6c-1.966 0-3.737-.53-5.312-1.588-1.575-1.058-2.738-2.437-3.488-4.137a.852.852 0 0 1 .013-.713.867.867 0 0 1 .512-.487.86.86 0 0 1 .688.037c.225.109.387.28.487.513.617 1.35 1.559 2.45 2.825 3.3 1.267.85 2.692 1.275 4.275 1.275 1.384 0 2.646-.33 3.788-.988a7.985 7.985 0 0 0 2.762-2.612h-.85a.873.873 0 0 1-.64-.257.863.863 0 0 1-.26-.638.88.88 0 0 1 .26-.643.864.864 0 0 1 .64-.262h3c.255 0 .47.086.642.259a.87.87 0 0 1 .258.64v3a.874.874 0 0 1-.257.642.863.863 0 0 1-.637.259.88.88 0 0 1-.643-.259.865.865 0 0 1-.263-.641v-.725a9.95 9.95 0 0 1-3.35 2.925c-1.35.733-2.833 1.1-4.45 1.1Zm0-17.4c-1.383 0-2.645.33-3.787.987A7.985 7.985 0 0 0 5.448 7.8h.85c.255 0 .47.086.642.257a.863.863 0 0 1 .258.638.88.88 0 0 1-.258.642.864.864 0 0 1-.642.263h-3a.87.87 0 0 1-.64-.259.87.87 0 0 1-.26-.641v-3c0-.255.086-.469.258-.641a.863.863 0 0 1 .637-.26.88.88 0 0 1 .643.26.864.864 0 0 1 .262.64v.726A9.95 9.95 0 0 1 7.548 3.5c1.35-.733 2.834-1.1 4.45-1.1 2 0 3.788.542 5.363 1.625 1.575 1.083 2.736 2.49 3.482 4.22a.724.724 0 0 1-.045.646.926.926 0 0 1-.525.437.87.87 0 0 1-.687-.04.977.977 0 0 1-.488-.513c-.616-1.35-1.558-2.45-2.825-3.3-1.266-.85-2.691-1.275-4.275-1.275Zm0 10.8a2.893 2.893 0 0 1-2.125-.875A2.893 2.893 0 0 1 8.998 12c0-.833.292-1.542.875-2.125A2.893 2.893 0 0 1 11.998 9c.834 0 1.542.292 2.125.875.584.583.875 1.292.875 2.125s-.291 1.542-.875 2.125a2.893 2.893 0 0 1-2.125.875Z\"/><path stroke=\"%23fff\" stroke-opacity=\".7\" d=\"m2.739 16.072.002.005c.789 1.788 2.014 3.24 3.666 4.35 1.66 1.116 3.53 1.673 5.591 1.673 1.696 0 3.262-.386 4.69-1.16a10.54 10.54 0 0 0 2.714-2.097c.069.167.171.32.308.454.272.268.613.403.994.403.38 0 .722-.135.992-.406s.402-.614.402-.994v-3c0-.381-.134-.724-.405-.995a1.37 1.37 0 0 0-.995-.405h-3c-.383 0-.727.137-.997.411a1.38 1.38 0 0 0-.403.994c0 .38.135.723.407.992.238.237.532.369.858.397a7.552 7.552 0 0 1-2.027 1.685c-1.062.613-2.237.92-3.538.92-1.488 0-2.815-.397-3.996-1.19-1.19-.798-2.07-1.826-2.647-3.087-.147-.34-.393-.6-.727-.76a1.36 1.36 0 0 0-1.073-.058c-.37.133-.64.397-.8.754a1.351 1.351 0 0 0-.016 1.114Zm4.553-8.37a1.36 1.36 0 0 0-.858-.396A7.552 7.552 0 0 1 8.46 5.62c1.062-.613 2.237-.921 3.537-.921 1.489 0 2.816.398 3.997 1.19 1.19.8 2.07 1.827 2.646 3.088.148.34.394.6.728.76a1.37 1.37 0 0 0 1.068.063c.347-.12.62-.346.799-.668.187-.338.215-.71.072-1.072l-.003-.007-.003-.007c-.784-1.818-2.006-3.298-3.658-4.434C15.982 2.47 14.094 1.9 11.998 1.9c-1.695 0-3.261.385-4.688 1.16a10.536 10.536 0 0 0-2.715 2.096 1.364 1.364 0 0 0-.308-.453 1.38 1.38 0 0 0-.994-.403c-.38 0-.722.135-.992.406s-.403.614-.403.994v3c0 .38.135.724.406.995.27.27.614.405.994.405h3c.384 0 .727-.137.998-.412a1.38 1.38 0 0 0 .402-.993c0-.38-.135-.723-.406-.992ZM9.52 14.48a3.393 3.393 0 0 0 2.478 1.02c.965 0 1.8-.342 2.479-1.02a3.392 3.392 0 0 0 1.021-2.48c0-.963-.343-1.8-1.021-2.478A3.392 3.392 0 0 0 11.998 8.5c-.964 0-1.8.343-2.478 1.021A3.392 3.392 0 0 0 8.498 12c0 .964.344 1.8 1.022 2.479Z\"/></svg>') 12 12,auto}.unreal-container.pan{cursor:url('data:image/svg+xml,<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" fill=\"none\"><path fill=\"%23000\" d=\"M11.997 10.075a1.01 1.01 0 0 1-.738-.313 1.008 1.008 0 0 1-.312-.737V6.15l-.95.95a.998.998 0 0 1-.763.312 1.09 1.09 0 0 1-.763-.337 1.06 1.06 0 0 1-.324-.775c0-.3.108-.558.324-.775l2.776-2.775c.117-.116.237-.2.362-.25a1.04 1.04 0 0 1 .775 0c.125.05.246.134.363.25l2.8 2.8c.216.217.32.47.313.762a1.096 1.096 0 0 1-.338.763 1.058 1.058 0 0 1-.775.325c-.3 0-.558-.108-.775-.325l-.925-.925v2.875c0 .283-.104.53-.312.737a1.01 1.01 0 0 1-.738.313Zm0 11.5a1.04 1.04 0 0 1-.388-.075 1.103 1.103 0 0 1-.362-.25l-2.8-2.8a1.002 1.002 0 0 1-.313-.762c.009-.292.121-.546.337-.763.217-.217.475-.325.775-.325.3 0 .559.108.775.325l.926.925v-2.875c0-.283.104-.529.312-.737a1.01 1.01 0 0 1 .738-.313c.283 0 .529.104.738.313.208.208.312.454.312.737v2.875l.95-.95a.998.998 0 0 1 .762-.312c.292.008.546.12.763.337.216.217.324.475.324.775 0 .3-.108.559-.324.775l-2.775 2.775c-.117.116-.238.2-.363.25a1.04 1.04 0 0 1-.387.075Zm4.925-6.05a1.058 1.058 0 0 1-.326-.774c0-.3.109-.559.326-.776l.925-.925H14.97c-.283 0-.528-.104-.736-.312a1.01 1.01 0 0 1-.313-.738c0-.283.104-.53.313-.738.208-.208.453-.312.737-.312h2.875l-.95-.95a.998.998 0 0 1-.313-.762 1.09 1.09 0 0 1 .338-.764 1.06 1.06 0 0 1 .775-.324c.3 0 .558.108.775.324l2.774 2.776c.117.117.2.238.251.363a1.04 1.04 0 0 1 0 .775c-.05.124-.134.245-.25.362l-2.8 2.8c-.217.216-.471.32-.762.313a1.096 1.096 0 0 1-.763-.337Zm-11.4 0L2.746 12.75a1.103 1.103 0 0 1-.25-.362 1.04 1.04 0 0 1 0-.775c.05-.125.133-.246.25-.363l2.8-2.8c.216-.216.47-.32.762-.313.292.01.546.121.763.337.217.217.325.476.325.776 0 .3-.108.558-.325.775l-.925.925h2.875c.283 0 .529.104.737.312.208.209.313.455.313.738s-.105.53-.313.738a1.008 1.008 0 0 1-.737.312H6.147l.95.95c.217.217.32.471.312.762a1.09 1.09 0 0 1-.337.764 1.06 1.06 0 0 1-.775.324c-.3 0-.559-.108-.776-.324Z\"/><path stroke=\"%23fff\" stroke-opacity=\".7\" d=\"M9.22 7.912c.436.013.821-.15 1.13-.458l.097-.097v1.668c0 .422.16.793.458 1.09.298.299.67.46 1.092.46.422 0 .793-.161 1.091-.46.298-.297.459-.668.459-1.09V7.357l.071.072c.309.308.693.471 1.13.471.435 0 .82-.163 1.128-.471.301-.303.47-.676.483-1.102v-.001a1.501 1.501 0 0 0-.46-1.13l-2.799-2.8a1.6 1.6 0 0 0-.528-.36l-.003-.001a1.54 1.54 0 0 0-1.145 0l-.002.001a1.6 1.6 0 0 0-.529.36L8.118 5.172l-.001.001a1.56 1.56 0 0 0-.47 1.128c0 .435.163.82.47 1.128.303.303.676.472 1.103.484Zm0 0 .014-.5-.015.5Zm-1.311 6.865a1.497 1.497 0 0 0-.458-1.13l-.097-.097h1.668c.421 0 .793-.16 1.09-.458.298-.299.46-.67.46-1.092a1.51 1.51 0 0 0-.46-1.091 1.507 1.507 0 0 0-1.09-.459H7.354l.071-.072c.309-.308.472-.692.472-1.128 0-.436-.163-.82-.472-1.13a1.596 1.596 0 0 0-1.101-.482h-.002a1.501 1.501 0 0 0-1.129.459l-2.8 2.8a1.6 1.6 0 0 0-.36.528l-.001.003a1.54 1.54 0 0 0 0 1.144v.003c.081.2.208.375.362.529l2.774 2.775c.309.308.693.47 1.129.47.435 0 .82-.162 1.128-.47.303-.302.472-.676.484-1.102Zm0 0-.5-.014.5.015v-.001Zm3.038-8.627-.5.5.5.207V6.15Zm.475 15.814.002.001a1.54 1.54 0 0 0 1.145 0h.002c.2-.081.375-.208.53-.362l2.775-2.774a1.56 1.56 0 0 0 .47-1.129c0-.435-.162-.82-.47-1.128a1.589 1.589 0 0 0-1.103-.484 1.497 1.497 0 0 0-1.13.458l-.096.097v-1.668c0-.421-.161-.793-.459-1.09a1.509 1.509 0 0 0-1.091-.46c-.422 0-.794.161-1.092.46a1.507 1.507 0 0 0-.458 1.09v1.668l-.072-.072a1.558 1.558 0 0 0-1.129-.471c-.436 0-.82.163-1.128.471l-.001.001c-.301.302-.47.675-.483 1.101v.001c-.012.437.151.821.46 1.13l2.8 2.8c.153.153.328.28.528.36Zm5.146-6.085c.303.302.676.47 1.102.484a1.5 1.5 0 0 0 1.13-.46l2.8-2.8a1.6 1.6 0 0 0 .36-.528l.002-.003a1.54 1.54 0 0 0 0-1.144l-.001-.003a1.6 1.6 0 0 0-.361-.529l-2.774-2.775h-.001a1.56 1.56 0 0 0-1.128-.47c-.436 0-.82.162-1.128.47h-.001a1.59 1.59 0 0 0-.484 1.102c-.012.437.15.822.459 1.13l.097.097h-1.668c-.422 0-.793.16-1.09.459-.299.298-.46.67-.46 1.091 0 .422.161.793.46 1.092.297.297.668.458 1.09.458h1.668l-.072.072a1.558 1.558 0 0 0-.472 1.129c0 .435.164.82.472 1.128ZM6.147 13.05l.5.5.207-.5h-.707Z\"/></svg>') 12 12,auto}.unreal-container.dolly{cursor:url('data:image/svg+xml,<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" fill=\"none\"><mask id=\"a\" width=\"24\" height=\"24\" x=\"0\" y=\"0\" maskUnits=\"userSpaceOnUse\" style=\"mask-type:alpha\"><path fill=\"%23127392\" d=\"M0 0h24v24H0z\"/></mask><g mask=\"url(%23a)\"><path fill=\"%23000\" d=\"M12.002 20.575a1.026 1.026 0 0 1-.75-.325l-2.55-2.55a.933.933 0 0 1-.287-.713c.008-.275.113-.513.313-.713.2-.2.441-.3.725-.3.283 0 .524.1.724.3l.776.776V6.95l-.8.8c-.2.2-.438.296-.712.288a1.018 1.018 0 0 1-.713-.312c-.2-.2-.3-.442-.3-.726s.1-.525.3-.725l2.524-2.525a1.026 1.026 0 0 1 1.5 0l2.55 2.55c.2.2.296.438.289.713a1.023 1.023 0 0 1-.314.713c-.2.2-.441.3-.724.3a.988.988 0 0 1-.725-.3l-.775-.776v10.1l.8-.8c.2-.2.437-.296.712-.288.275.008.512.112.712.312.2.2.3.442.3.726s-.1.525-.3.725l-2.524 2.525a1.026 1.026 0 0 1-.75.325Z\"/><path stroke=\"%23fff\" stroke-opacity=\".7\" d=\"M13.553 8.153c.28.245.62.373 1 .373.415 0 .784-.153 1.078-.447.287-.287.446-.644.46-1.05v-.002a1.432 1.432 0 0 0-.435-1.08l-2.55-2.55-.354.353.354-.354a1.526 1.526 0 0 0-2.207 0L8.374 5.921A1.487 1.487 0 0 0 7.928 7c0 .416.152.785.446 1.08.288.287.645.446 1.052.457l4.127-.384Zm0 0v7.693m0-7.693a1.667 1.667 0 0 1-.079-.074l-3.021.075v7.693a1.482 1.482 0 0 0-1-.373c-.416 0-.785.153-1.079.447a1.522 1.522 0 0 0-.46 1.05v.002c-.011.417.14.786.435 1.08l2.55 2.55a1.527 1.527 0 0 0 2.207 0l2.525-2.524c.294-.294.446-.663.446-1.079 0-.416-.152-.785-.446-1.08a1.518 1.518 0 0 0-1.052-.457m0 0-.014.5.014-.5Zm0 0a1.424 1.424 0 0 0-1.027.383m0 0a1.583 1.583 0 0 0-.053.05l.354.354m-.3-.404v.104l.3.3m0 0-.3-.3v.6l.3-.3Z\"/></g></svg>%0A') 12 12,auto}.unreal-container.disabled:before{content:\"\";position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px)}#playerUI{position:absolute;z-index:10;width:100%}#statsContainer{display:none;text-align:left;background-color:#000c}#stats{padding:6px;font-weight:700;font-size:14px;color:#0f0}canvas{position:absolute;image-rendering:crisp-edges}#overlay{position:absolute;top:0;right:2%;z-index:100;padding:4px;border-top-width:0;-webkit-border-bottom-right-radius:5px;-moz-border-radius-bottomright:5px;border-bottom-right-radius:5px;-webkit-border-bottom-left-radius:5px;-moz-border-radius-bottomleft:5px;border-bottom-left-radius:5px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-touch-callout:none}.overlay{font-weight:lighter;font-family:var(--src-font-family-body)}#overlayButton:hover{cursor:pointer}#overlayButton{font-size:40px;text-align:right}#overlaySettings{display:none;width:300px;padding:4px}#videoMessageOverlay{position:absolute;z-index:20;width:100%;margin:auto;font-size:1.8em;font-family:var(--src-font-family-body)}#playButton{display:inline-block;height:auto}img#playButton{width:10%;max-width:241px}#UIInteraction{position:fixed}#UIInteractionButtonBoundary{padding:2px}#UIInteractionButton{cursor:pointer}#hiddenInput{position:absolute;left:-10%;width:0;opacity:0}#editTextButton{position:absolute;width:40px;height:40px}\n"], dependencies: [{ kind: "component", type: AfkTimeoutModalComponent, selector: "app-afk-timeout-modal" }, { kind: "component", type: UnrealStatusComponent, selector: "app-unreal-status" }, { kind: "component", type: FreezeFrameComponent, selector: "app-freeze-frame" }, { kind: "component", type: VideoStatsComponent, selector: "app-video-stats" }, { kind: "component", type: ResumeStreamButtonComponent, selector: "app-resume-stream-button" }, { kind: "component", type: ImageLoadingSrcComponent, selector: "app-image-loading-src" }, { kind: "component", type: IntroSrcComponent, selector: "app-intro-src" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
5966
|
+
}
|
|
5967
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: UnrealSceneComponent, decorators: [{
|
|
5968
|
+
type: Component,
|
|
5969
|
+
args: [{ selector: 'app-unreal-scene', changeDetection: ChangeDetectionStrategy.OnPush, imports: [
|
|
5970
|
+
AfkTimeoutModalComponent,
|
|
5971
|
+
UnrealStatusComponent,
|
|
5972
|
+
FreezeFrameComponent,
|
|
5973
|
+
VideoStatsComponent,
|
|
5974
|
+
ResumeStreamButtonComponent,
|
|
5975
|
+
ImageLoadingSrcComponent,
|
|
5976
|
+
IntroSrcComponent,
|
|
5977
|
+
], template: "<div class=\"frame unreal-container\">\n <div #videoContainer [class.lightModeCursor]=\"lightMode()\" id=\"player\"></div>\n\n <app-intro-src />\n <app-image-loading-src />\n <app-resume-stream-button />\n <app-unreal-status />\n <app-freeze-frame />\n <app-afk-timeout-modal />\n\n <ng-content select=\"app-pdf\"></ng-content>\n <ng-content select=\"app-show-case\"></ng-content>\n <ng-content select=\"app-low-bandwidth-detector\"></ng-content>\n\n @if (isDevMode) {\n <app-video-stats />\n }\n</div>\n", styles: [".unreal-container{position:absolute;top:0;left:0;width:100%;height:100%;background-color:#fff}.unreal-container #player{position:absolute;width:100%;height:100%;display:flex;justify-content:center;align-items:center;overflow:hidden}.unreal-container.select{cursor:url('data:image/svg+xml,<svg width=\"24\" height=\"24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M15.15 21.375a1.423 1.423 0 0 1-1.15.063 1.487 1.487 0 0 1-.85-.788l-3-6.45-2.325 3.25c-.283.4-.658.525-1.125.375-.467-.15-.7-.466-.7-.95V4.05c0-.417.188-.717.563-.9.375-.183.729-.142 1.062.125l10.1 7.95c.383.284.496.65.337 1.1-.158.45-.479.675-.962.675h-4.2l2.975 6.375c.183.384.204.767.063 1.15a1.487 1.487 0 0 1-.788.85Z\" fill=\"%23000\"/><path d=\"m12.697 20.861.002.005c.237.495.617.852 1.128 1.04.515.191 1.039.16 1.539-.08a1.987 1.987 0 0 0 1.04-1.128 1.922 1.922 0 0 0-.079-1.536v-.003L13.684 13.5H17.1c.32 0 .628-.075.89-.26.263-.184.438-.447.544-.749.106-.3.135-.617.04-.925a1.458 1.458 0 0 0-.545-.738L7.936 2.883a1.51 1.51 0 0 0-.768-.336 1.471 1.471 0 0 0-.825.154 1.495 1.495 0 0 0-.626.547 1.492 1.492 0 0 0-.217.802v12.825c0 .324.08.634.272.897.193.261.467.43.775.53.31.099.632.12.942.016.31-.103.555-.313.743-.578h.001l1.825-2.552 2.639 5.673Z\" stroke=\"%23fff\" stroke-opacity=\".7\"/></svg>') 6 2,auto}.unreal-container.orbit{cursor:url('data:image/svg+xml,<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" fill=\"none\"><path fill=\"%23000\" d=\"M11.998 21.6c-1.966 0-3.737-.53-5.312-1.588-1.575-1.058-2.738-2.437-3.488-4.137a.852.852 0 0 1 .013-.713.867.867 0 0 1 .512-.487.86.86 0 0 1 .688.037c.225.109.387.28.487.513.617 1.35 1.559 2.45 2.825 3.3 1.267.85 2.692 1.275 4.275 1.275 1.384 0 2.646-.33 3.788-.988a7.985 7.985 0 0 0 2.762-2.612h-.85a.873.873 0 0 1-.64-.257.863.863 0 0 1-.26-.638.88.88 0 0 1 .26-.643.864.864 0 0 1 .64-.262h3c.255 0 .47.086.642.259a.87.87 0 0 1 .258.64v3a.874.874 0 0 1-.257.642.863.863 0 0 1-.637.259.88.88 0 0 1-.643-.259.865.865 0 0 1-.263-.641v-.725a9.95 9.95 0 0 1-3.35 2.925c-1.35.733-2.833 1.1-4.45 1.1Zm0-17.4c-1.383 0-2.645.33-3.787.987A7.985 7.985 0 0 0 5.448 7.8h.85c.255 0 .47.086.642.257a.863.863 0 0 1 .258.638.88.88 0 0 1-.258.642.864.864 0 0 1-.642.263h-3a.87.87 0 0 1-.64-.259.87.87 0 0 1-.26-.641v-3c0-.255.086-.469.258-.641a.863.863 0 0 1 .637-.26.88.88 0 0 1 .643.26.864.864 0 0 1 .262.64v.726A9.95 9.95 0 0 1 7.548 3.5c1.35-.733 2.834-1.1 4.45-1.1 2 0 3.788.542 5.363 1.625 1.575 1.083 2.736 2.49 3.482 4.22a.724.724 0 0 1-.045.646.926.926 0 0 1-.525.437.87.87 0 0 1-.687-.04.977.977 0 0 1-.488-.513c-.616-1.35-1.558-2.45-2.825-3.3-1.266-.85-2.691-1.275-4.275-1.275Zm0 10.8a2.893 2.893 0 0 1-2.125-.875A2.893 2.893 0 0 1 8.998 12c0-.833.292-1.542.875-2.125A2.893 2.893 0 0 1 11.998 9c.834 0 1.542.292 2.125.875.584.583.875 1.292.875 2.125s-.291 1.542-.875 2.125a2.893 2.893 0 0 1-2.125.875Z\"/><path stroke=\"%23fff\" stroke-opacity=\".7\" d=\"m2.739 16.072.002.005c.789 1.788 2.014 3.24 3.666 4.35 1.66 1.116 3.53 1.673 5.591 1.673 1.696 0 3.262-.386 4.69-1.16a10.54 10.54 0 0 0 2.714-2.097c.069.167.171.32.308.454.272.268.613.403.994.403.38 0 .722-.135.992-.406s.402-.614.402-.994v-3c0-.381-.134-.724-.405-.995a1.37 1.37 0 0 0-.995-.405h-3c-.383 0-.727.137-.997.411a1.38 1.38 0 0 0-.403.994c0 .38.135.723.407.992.238.237.532.369.858.397a7.552 7.552 0 0 1-2.027 1.685c-1.062.613-2.237.92-3.538.92-1.488 0-2.815-.397-3.996-1.19-1.19-.798-2.07-1.826-2.647-3.087-.147-.34-.393-.6-.727-.76a1.36 1.36 0 0 0-1.073-.058c-.37.133-.64.397-.8.754a1.351 1.351 0 0 0-.016 1.114Zm4.553-8.37a1.36 1.36 0 0 0-.858-.396A7.552 7.552 0 0 1 8.46 5.62c1.062-.613 2.237-.921 3.537-.921 1.489 0 2.816.398 3.997 1.19 1.19.8 2.07 1.827 2.646 3.088.148.34.394.6.728.76a1.37 1.37 0 0 0 1.068.063c.347-.12.62-.346.799-.668.187-.338.215-.71.072-1.072l-.003-.007-.003-.007c-.784-1.818-2.006-3.298-3.658-4.434C15.982 2.47 14.094 1.9 11.998 1.9c-1.695 0-3.261.385-4.688 1.16a10.536 10.536 0 0 0-2.715 2.096 1.364 1.364 0 0 0-.308-.453 1.38 1.38 0 0 0-.994-.403c-.38 0-.722.135-.992.406s-.403.614-.403.994v3c0 .38.135.724.406.995.27.27.614.405.994.405h3c.384 0 .727-.137.998-.412a1.38 1.38 0 0 0 .402-.993c0-.38-.135-.723-.406-.992ZM9.52 14.48a3.393 3.393 0 0 0 2.478 1.02c.965 0 1.8-.342 2.479-1.02a3.392 3.392 0 0 0 1.021-2.48c0-.963-.343-1.8-1.021-2.478A3.392 3.392 0 0 0 11.998 8.5c-.964 0-1.8.343-2.478 1.021A3.392 3.392 0 0 0 8.498 12c0 .964.344 1.8 1.022 2.479Z\"/></svg>') 12 12,auto}.unreal-container.pan{cursor:url('data:image/svg+xml,<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" fill=\"none\"><path fill=\"%23000\" d=\"M11.997 10.075a1.01 1.01 0 0 1-.738-.313 1.008 1.008 0 0 1-.312-.737V6.15l-.95.95a.998.998 0 0 1-.763.312 1.09 1.09 0 0 1-.763-.337 1.06 1.06 0 0 1-.324-.775c0-.3.108-.558.324-.775l2.776-2.775c.117-.116.237-.2.362-.25a1.04 1.04 0 0 1 .775 0c.125.05.246.134.363.25l2.8 2.8c.216.217.32.47.313.762a1.096 1.096 0 0 1-.338.763 1.058 1.058 0 0 1-.775.325c-.3 0-.558-.108-.775-.325l-.925-.925v2.875c0 .283-.104.53-.312.737a1.01 1.01 0 0 1-.738.313Zm0 11.5a1.04 1.04 0 0 1-.388-.075 1.103 1.103 0 0 1-.362-.25l-2.8-2.8a1.002 1.002 0 0 1-.313-.762c.009-.292.121-.546.337-.763.217-.217.475-.325.775-.325.3 0 .559.108.775.325l.926.925v-2.875c0-.283.104-.529.312-.737a1.01 1.01 0 0 1 .738-.313c.283 0 .529.104.738.313.208.208.312.454.312.737v2.875l.95-.95a.998.998 0 0 1 .762-.312c.292.008.546.12.763.337.216.217.324.475.324.775 0 .3-.108.559-.324.775l-2.775 2.775c-.117.116-.238.2-.363.25a1.04 1.04 0 0 1-.387.075Zm4.925-6.05a1.058 1.058 0 0 1-.326-.774c0-.3.109-.559.326-.776l.925-.925H14.97c-.283 0-.528-.104-.736-.312a1.01 1.01 0 0 1-.313-.738c0-.283.104-.53.313-.738.208-.208.453-.312.737-.312h2.875l-.95-.95a.998.998 0 0 1-.313-.762 1.09 1.09 0 0 1 .338-.764 1.06 1.06 0 0 1 .775-.324c.3 0 .558.108.775.324l2.774 2.776c.117.117.2.238.251.363a1.04 1.04 0 0 1 0 .775c-.05.124-.134.245-.25.362l-2.8 2.8c-.217.216-.471.32-.762.313a1.096 1.096 0 0 1-.763-.337Zm-11.4 0L2.746 12.75a1.103 1.103 0 0 1-.25-.362 1.04 1.04 0 0 1 0-.775c.05-.125.133-.246.25-.363l2.8-2.8c.216-.216.47-.32.762-.313.292.01.546.121.763.337.217.217.325.476.325.776 0 .3-.108.558-.325.775l-.925.925h2.875c.283 0 .529.104.737.312.208.209.313.455.313.738s-.105.53-.313.738a1.008 1.008 0 0 1-.737.312H6.147l.95.95c.217.217.32.471.312.762a1.09 1.09 0 0 1-.337.764 1.06 1.06 0 0 1-.775.324c-.3 0-.559-.108-.776-.324Z\"/><path stroke=\"%23fff\" stroke-opacity=\".7\" d=\"M9.22 7.912c.436.013.821-.15 1.13-.458l.097-.097v1.668c0 .422.16.793.458 1.09.298.299.67.46 1.092.46.422 0 .793-.161 1.091-.46.298-.297.459-.668.459-1.09V7.357l.071.072c.309.308.693.471 1.13.471.435 0 .82-.163 1.128-.471.301-.303.47-.676.483-1.102v-.001a1.501 1.501 0 0 0-.46-1.13l-2.799-2.8a1.6 1.6 0 0 0-.528-.36l-.003-.001a1.54 1.54 0 0 0-1.145 0l-.002.001a1.6 1.6 0 0 0-.529.36L8.118 5.172l-.001.001a1.56 1.56 0 0 0-.47 1.128c0 .435.163.82.47 1.128.303.303.676.472 1.103.484Zm0 0 .014-.5-.015.5Zm-1.311 6.865a1.497 1.497 0 0 0-.458-1.13l-.097-.097h1.668c.421 0 .793-.16 1.09-.458.298-.299.46-.67.46-1.092a1.51 1.51 0 0 0-.46-1.091 1.507 1.507 0 0 0-1.09-.459H7.354l.071-.072c.309-.308.472-.692.472-1.128 0-.436-.163-.82-.472-1.13a1.596 1.596 0 0 0-1.101-.482h-.002a1.501 1.501 0 0 0-1.129.459l-2.8 2.8a1.6 1.6 0 0 0-.36.528l-.001.003a1.54 1.54 0 0 0 0 1.144v.003c.081.2.208.375.362.529l2.774 2.775c.309.308.693.47 1.129.47.435 0 .82-.162 1.128-.47.303-.302.472-.676.484-1.102Zm0 0-.5-.014.5.015v-.001Zm3.038-8.627-.5.5.5.207V6.15Zm.475 15.814.002.001a1.54 1.54 0 0 0 1.145 0h.002c.2-.081.375-.208.53-.362l2.775-2.774a1.56 1.56 0 0 0 .47-1.129c0-.435-.162-.82-.47-1.128a1.589 1.589 0 0 0-1.103-.484 1.497 1.497 0 0 0-1.13.458l-.096.097v-1.668c0-.421-.161-.793-.459-1.09a1.509 1.509 0 0 0-1.091-.46c-.422 0-.794.161-1.092.46a1.507 1.507 0 0 0-.458 1.09v1.668l-.072-.072a1.558 1.558 0 0 0-1.129-.471c-.436 0-.82.163-1.128.471l-.001.001c-.301.302-.47.675-.483 1.101v.001c-.012.437.151.821.46 1.13l2.8 2.8c.153.153.328.28.528.36Zm5.146-6.085c.303.302.676.47 1.102.484a1.5 1.5 0 0 0 1.13-.46l2.8-2.8a1.6 1.6 0 0 0 .36-.528l.002-.003a1.54 1.54 0 0 0 0-1.144l-.001-.003a1.6 1.6 0 0 0-.361-.529l-2.774-2.775h-.001a1.56 1.56 0 0 0-1.128-.47c-.436 0-.82.162-1.128.47h-.001a1.59 1.59 0 0 0-.484 1.102c-.012.437.15.822.459 1.13l.097.097h-1.668c-.422 0-.793.16-1.09.459-.299.298-.46.67-.46 1.091 0 .422.161.793.46 1.092.297.297.668.458 1.09.458h1.668l-.072.072a1.558 1.558 0 0 0-.472 1.129c0 .435.164.82.472 1.128ZM6.147 13.05l.5.5.207-.5h-.707Z\"/></svg>') 12 12,auto}.unreal-container.dolly{cursor:url('data:image/svg+xml,<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" fill=\"none\"><mask id=\"a\" width=\"24\" height=\"24\" x=\"0\" y=\"0\" maskUnits=\"userSpaceOnUse\" style=\"mask-type:alpha\"><path fill=\"%23127392\" d=\"M0 0h24v24H0z\"/></mask><g mask=\"url(%23a)\"><path fill=\"%23000\" d=\"M12.002 20.575a1.026 1.026 0 0 1-.75-.325l-2.55-2.55a.933.933 0 0 1-.287-.713c.008-.275.113-.513.313-.713.2-.2.441-.3.725-.3.283 0 .524.1.724.3l.776.776V6.95l-.8.8c-.2.2-.438.296-.712.288a1.018 1.018 0 0 1-.713-.312c-.2-.2-.3-.442-.3-.726s.1-.525.3-.725l2.524-2.525a1.026 1.026 0 0 1 1.5 0l2.55 2.55c.2.2.296.438.289.713a1.023 1.023 0 0 1-.314.713c-.2.2-.441.3-.724.3a.988.988 0 0 1-.725-.3l-.775-.776v10.1l.8-.8c.2-.2.437-.296.712-.288.275.008.512.112.712.312.2.2.3.442.3.726s-.1.525-.3.725l-2.524 2.525a1.026 1.026 0 0 1-.75.325Z\"/><path stroke=\"%23fff\" stroke-opacity=\".7\" d=\"M13.553 8.153c.28.245.62.373 1 .373.415 0 .784-.153 1.078-.447.287-.287.446-.644.46-1.05v-.002a1.432 1.432 0 0 0-.435-1.08l-2.55-2.55-.354.353.354-.354a1.526 1.526 0 0 0-2.207 0L8.374 5.921A1.487 1.487 0 0 0 7.928 7c0 .416.152.785.446 1.08.288.287.645.446 1.052.457l4.127-.384Zm0 0v7.693m0-7.693a1.667 1.667 0 0 1-.079-.074l-3.021.075v7.693a1.482 1.482 0 0 0-1-.373c-.416 0-.785.153-1.079.447a1.522 1.522 0 0 0-.46 1.05v.002c-.011.417.14.786.435 1.08l2.55 2.55a1.527 1.527 0 0 0 2.207 0l2.525-2.524c.294-.294.446-.663.446-1.079 0-.416-.152-.785-.446-1.08a1.518 1.518 0 0 0-1.052-.457m0 0-.014.5.014-.5Zm0 0a1.424 1.424 0 0 0-1.027.383m0 0a1.583 1.583 0 0 0-.053.05l.354.354m-.3-.404v.104l.3.3m0 0-.3-.3v.6l.3-.3Z\"/></g></svg>%0A') 12 12,auto}.unreal-container.disabled:before{content:\"\";position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px)}#playerUI{position:absolute;z-index:10;width:100%}#statsContainer{display:none;text-align:left;background-color:#000c}#stats{padding:6px;font-weight:700;font-size:14px;color:#0f0}canvas{position:absolute;image-rendering:crisp-edges}#overlay{position:absolute;top:0;right:2%;z-index:100;padding:4px;border-top-width:0;-webkit-border-bottom-right-radius:5px;-moz-border-radius-bottomright:5px;border-bottom-right-radius:5px;-webkit-border-bottom-left-radius:5px;-moz-border-radius-bottomleft:5px;border-bottom-left-radius:5px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-touch-callout:none}.overlay{font-weight:lighter;font-family:var(--src-font-family-body)}#overlayButton:hover{cursor:pointer}#overlayButton{font-size:40px;text-align:right}#overlaySettings{display:none;width:300px;padding:4px}#videoMessageOverlay{position:absolute;z-index:20;width:100%;margin:auto;font-size:1.8em;font-family:var(--src-font-family-body)}#playButton{display:inline-block;height:auto}img#playButton{width:10%;max-width:241px}#UIInteraction{position:fixed}#UIInteractionButtonBoundary{padding:2px}#UIInteractionButton{cursor:pointer}#hiddenInput{position:absolute;left:-10%;width:0;opacity:0}#editTextButton{position:absolute;width:40px;height:40px}\n"] }]
|
|
5978
|
+
}], ctorParameters: () => [], propDecorators: { isStudio: [{ type: i0.Input, args: [{ isSignal: true, alias: "isStudio", required: false }] }], useContainerAsSizeProvider: [{ type: i0.Input, args: [{ isSignal: true, alias: "useContainerAsSizeProvider", required: false }] }], studioResolutionSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "studioResolutionSize", required: false }] }], videoElement: [{ type: i0.ViewChild, args: ['videoContainer', { isSignal: true }] }], changeMouseOverScene: [{ type: i0.Output, args: ["changeMouseOverScene"] }], onMouseOver: [{
|
|
5979
|
+
type: HostListener,
|
|
5980
|
+
args: ['mouseover']
|
|
5981
|
+
}], onMouseOut: [{
|
|
5982
|
+
type: HostListener,
|
|
5983
|
+
args: ['mouseout']
|
|
5984
|
+
}] } });
|
|
5985
|
+
|
|
5986
|
+
class WebrtcErrorModalComponent {
|
|
5987
|
+
#dialogRef = inject(DialogRef);
|
|
5988
|
+
#store = inject(Store);
|
|
5989
|
+
close() {
|
|
5990
|
+
this.#dialogRef.close();
|
|
5991
|
+
}
|
|
5992
|
+
closeModalWithCirrusDisconnect() {
|
|
5993
|
+
this.#store
|
|
5994
|
+
.select(unrealFeature.selectCirrusConnected)
|
|
5995
|
+
.pipe(take(1))
|
|
5996
|
+
.subscribe((isConnected) => {
|
|
5997
|
+
if (!isConnected) {
|
|
5998
|
+
this.#store.dispatch(disconnectStream({
|
|
5999
|
+
reason: DisconnectReason.DropConnection,
|
|
6000
|
+
message: 'Modal Drop Connection',
|
|
6001
|
+
}));
|
|
6048
6002
|
}
|
|
6003
|
+
this.close();
|
|
6004
|
+
});
|
|
6005
|
+
}
|
|
6006
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: WebrtcErrorModalComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
6007
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.0", type: WebrtcErrorModalComponent, isStandalone: true, selector: "app-webrtc-error-modal", ngImport: i0, template: "<div [attr.data-testid]=\"'webrtc-error-modal'\" class=\"src-modal\">\n <header class=\"src-modal__header\"></header>\n <section class=\"src-modal__body\">\n <div [attr.data-testid]=\"'title'\" class=\"src-modal__title\">\n WebRTC error\n </div>\n <div style=\"text-align: left\">\n An internet connection type (WebRTC) appears to be blocked either by your\n browser settings or your current network. If WebRTC is blocked on your\n browser you may be able to adjust these settings yourself based on\n instructions here:\n <a href=\"https://myownconference.com/blog/en/webrtc/\" target=\"blank\"\n >https://myownconference.com/blog/en/webrtc/</a\n >\n <br /><br />\n Trying a different web browser may help confirm this as well. If WebRTC is\n blocked by your network, try switching to a different network if possible\n or contact your network administrator.<br /><br />\n WebRTC is common, safe and increasingly utilised method for streaming real\n time 3D experiences via a web browser. It typically consumes no more\n bandwidth than streaming an HD video.\n </div>\n </section>\n <footer class=\"src-modal__footer\">\n <src-button\n (onClick)=\"closeModalWithCirrusDisconnect()\"\n [data-testid]=\"'close-webrtc-error-modal'\"\n weight=\"primary\"\n >\n Ok\n </src-button>\n </footer>\n</div>\n", styles: [".src-modal{--modalBodyPadding: 20px 8px 20px 20px;display:grid;grid-template-columns:minmax(0,1fr);grid-template-rows:auto minmax(0,1fr) auto}.src-modal ::ng-deep .ng-scroll-content{--_scrollbar-content-width: initial;--_viewport-padding-right: 12px}.src-modal__body{width:100%}.src-modal__body p{color:var(--src-text-body-main, #1f2937);font-size:14px;font-style:normal;font-weight:400;line-height:24px;margin-top:0}.src-modal__body a{word-wrap:break-word;white-space:normal}\n"], dependencies: [{ kind: "component", type: SourceButtonComponent, selector: "src-button", inputs: ["type", "appearance", "weight", "size", "context", "customClass", "isFullWidth", "isPressed", "isDisabled", "isLoading", "isInverted", "iconButton", "srcButtonConfig", "formID", "data-testid", "ariaLabel"], outputs: ["onClick", "onSubmit"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
6008
|
+
}
|
|
6009
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: WebrtcErrorModalComponent, decorators: [{
|
|
6010
|
+
type: Component,
|
|
6011
|
+
args: [{ selector: 'app-webrtc-error-modal', changeDetection: ChangeDetectionStrategy.OnPush, imports: [SourceButtonComponent], template: "<div [attr.data-testid]=\"'webrtc-error-modal'\" class=\"src-modal\">\n <header class=\"src-modal__header\"></header>\n <section class=\"src-modal__body\">\n <div [attr.data-testid]=\"'title'\" class=\"src-modal__title\">\n WebRTC error\n </div>\n <div style=\"text-align: left\">\n An internet connection type (WebRTC) appears to be blocked either by your\n browser settings or your current network. If WebRTC is blocked on your\n browser you may be able to adjust these settings yourself based on\n instructions here:\n <a href=\"https://myownconference.com/blog/en/webrtc/\" target=\"blank\"\n >https://myownconference.com/blog/en/webrtc/</a\n >\n <br /><br />\n Trying a different web browser may help confirm this as well. If WebRTC is\n blocked by your network, try switching to a different network if possible\n or contact your network administrator.<br /><br />\n WebRTC is common, safe and increasingly utilised method for streaming real\n time 3D experiences via a web browser. It typically consumes no more\n bandwidth than streaming an HD video.\n </div>\n </section>\n <footer class=\"src-modal__footer\">\n <src-button\n (onClick)=\"closeModalWithCirrusDisconnect()\"\n [data-testid]=\"'close-webrtc-error-modal'\"\n weight=\"primary\"\n >\n Ok\n </src-button>\n </footer>\n</div>\n", styles: [".src-modal{--modalBodyPadding: 20px 8px 20px 20px;display:grid;grid-template-columns:minmax(0,1fr);grid-template-rows:auto minmax(0,1fr) auto}.src-modal ::ng-deep .ng-scroll-content{--_scrollbar-content-width: initial;--_viewport-padding-right: 12px}.src-modal__body{width:100%}.src-modal__body p{color:var(--src-text-body-main, #1f2937);font-size:14px;font-style:normal;font-weight:400;line-height:24px;margin-top:0}.src-modal__body a{word-wrap:break-word;white-space:normal}\n"] }]
|
|
6012
|
+
}] });
|
|
6013
|
+
|
|
6014
|
+
class FilterSettingsComponent {
|
|
6015
|
+
constructor() {
|
|
6016
|
+
this.store = inject(Store);
|
|
6017
|
+
this.storageKey = 'LBMFilterSettings';
|
|
6018
|
+
this.defaultFilterModel = { ...DefaultFilterModel };
|
|
6019
|
+
this.settings = { ...DefaultFilterModel };
|
|
6020
|
+
}
|
|
6021
|
+
ngOnInit() {
|
|
6022
|
+
const savedSettings = localStorage.getItem(this.storageKey);
|
|
6023
|
+
if (savedSettings) {
|
|
6024
|
+
this.dispatch(JSON.parse(savedSettings));
|
|
6049
6025
|
}
|
|
6050
6026
|
}
|
|
6051
|
-
|
|
6027
|
+
togglePanel() {
|
|
6028
|
+
this.settings.panelOpen = !this.settings.panelOpen;
|
|
6029
|
+
this.saveSettings();
|
|
6030
|
+
}
|
|
6031
|
+
saveSettings() {
|
|
6032
|
+
const valid = Object.entries(this.settings).reduce((acc, [key, value]) => {
|
|
6033
|
+
return Truthy(value) ? { ...acc, [key]: value } : acc;
|
|
6034
|
+
}, {});
|
|
6035
|
+
this.dispatch({ ...DefaultFilterModel, ...valid });
|
|
6036
|
+
}
|
|
6037
|
+
dispatch(settings) {
|
|
6052
6038
|
try {
|
|
6053
|
-
|
|
6054
|
-
this.commandTelemetryService.trackStartCommand('EXT-DataChannel Connection');
|
|
6055
|
-
// Inform browser we would like binary data as an ArrayBuffer (FF chooses Blob by default!)
|
|
6056
|
-
datachannel.binaryType = 'arraybuffer';
|
|
6057
|
-
fromEvent(datachannel, 'close')
|
|
6058
|
-
.pipe(
|
|
6059
|
-
// Only dispatch disconnect if not already reconnecting to avoid infinite loops
|
|
6060
|
-
filter(() => !this.isReconnecting()), take(1))
|
|
6061
|
-
.subscribe((e) => {
|
|
6062
|
-
Logger.warn(`[DATACHANNEL] Data channel disconnected: ${datachannel.label}(${datachannel.id})`, e);
|
|
6063
|
-
this.store.dispatch(disconnectStream({
|
|
6064
|
-
reason: DisconnectReason.DataChannelClosed,
|
|
6065
|
-
message: 'DataChannelClosed',
|
|
6066
|
-
}));
|
|
6067
|
-
});
|
|
6068
|
-
fromEvent(datachannel, 'error')
|
|
6069
|
-
.pipe(takeUntil(this.disconnect$))
|
|
6070
|
-
.subscribe((e) => {
|
|
6071
|
-
const message = `[DATACHANNEL] Data channel "${datachannel.label}" error. id:${datachannel.id}`;
|
|
6072
|
-
Logger.error(message, e);
|
|
6073
|
-
});
|
|
6074
|
-
fromEvent(datachannel, 'open')
|
|
6075
|
-
.pipe(tap$1(() => {
|
|
6076
|
-
Logger.info(`[DATACHANNEL] Data channel "${datachannel.label}" connected. id:${datachannel.id}`);
|
|
6077
|
-
}), takeUntil(this.disconnect$))
|
|
6078
|
-
.subscribe(() => {
|
|
6079
|
-
this.commandTelemetryService.trackStopCommand('EXT-DataChannel Connection');
|
|
6080
|
-
this.onDataChannelConnected();
|
|
6081
|
-
});
|
|
6082
|
-
fromEvent(datachannel, 'message')
|
|
6083
|
-
.pipe(
|
|
6084
|
-
/* tap((e: MessageEvent) => {
|
|
6085
|
-
const view = new Uint8Array(e.data);
|
|
6086
|
-
Logger.info(
|
|
6087
|
-
`[DATACHANNEL] Message received on "${datachannel.label}" id=${datachannel.id}, byteCode=${view[0]}, size=${view.byteLength}`,
|
|
6088
|
-
);
|
|
6089
|
-
}),*/
|
|
6090
|
-
map$1((e) => e.data), takeUntil(this.disconnect$))
|
|
6091
|
-
.subscribe((data) => this.onDataChannelMessage$.next(data));
|
|
6092
|
-
return datachannel;
|
|
6039
|
+
localStorage.setItem(this.storageKey, JSON.stringify(settings));
|
|
6093
6040
|
}
|
|
6094
6041
|
catch (e) {
|
|
6095
|
-
Logger.
|
|
6096
|
-
return null;
|
|
6042
|
+
Logger.error('Storage not available, Error saving filter settings:', e);
|
|
6097
6043
|
}
|
|
6044
|
+
Object.assign(this.settings, settings);
|
|
6045
|
+
Object.assign(FilterModel, settings);
|
|
6046
|
+
this.store.dispatch(setKalmanSettings({ settings }));
|
|
6098
6047
|
}
|
|
6099
|
-
|
|
6100
|
-
|
|
6101
|
-
|
|
6102
|
-
|
|
6103
|
-
|
|
6104
|
-
}
|
|
6105
|
-
this.store.dispatch(dataChannelConnected({ statusMessage }));
|
|
6048
|
+
reset() {
|
|
6049
|
+
this.dispatch({
|
|
6050
|
+
...DefaultFilterModel,
|
|
6051
|
+
panelOpen: this.settings.panelOpen,
|
|
6052
|
+
});
|
|
6106
6053
|
}
|
|
6107
|
-
|
|
6108
|
-
|
|
6109
|
-
|
|
6110
|
-
|
|
6111
|
-
|
|
6112
|
-
|
|
6113
|
-
|
|
6114
|
-
|
|
6115
|
-
|
|
6116
|
-
|
|
6117
|
-
|
|
6118
|
-
|
|
6119
|
-
|
|
6120
|
-
|
|
6121
|
-
|
|
6122
|
-
|
|
6054
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: FilterSettingsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
6055
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: FilterSettingsComponent, isStandalone: true, selector: "app-filter-settings", ngImport: i0, template: "<div class=\"settings-container\">\n <button (click)=\"togglePanel()\" class=\"gear-button\">LBM</button>\n @if (settings.panelOpen) {\n <div class=\"settings-panel\">\n <h3>LBM Trigger Settings</h3>\n <h5>Data Flow Monitor</h5>\n <form>\n <label>\n Monitoring delay:\n <input\n [(ngModel)]=\"settings.monitoringDelayTime\"\n [placeholder]=\"defaultFilterModel.monitoringDelayTime\"\n type=\"number\"\n name=\"monitoringDelayTime\"\n />\n </label>\n\n <label>\n Minimum Bitrate (kbit/s):\n <input\n [(ngModel)]=\"settings.minimumBitrate\"\n [placeholder]=\"defaultFilterModel.minimumBitrate\"\n type=\"number\"\n name=\"minimumBitrate\"\n />\n </label>\n <label>\n Yellow Flag (%):\n <input\n [(ngModel)]=\"settings.yellowFlag\"\n [placeholder]=\"defaultFilterModel.yellowFlag\"\n type=\"number\"\n name=\"yellowFlag\"\n />\n </label>\n <label>\n Red Flag (%):\n <input\n [(ngModel)]=\"settings.redFlag\"\n [placeholder]=\"defaultFilterModel.redFlag\"\n type=\"number\"\n name=\"redFlag\"\n />\n </label>\n <label>\n Minimum FPS:\n <input\n [(ngModel)]=\"settings.minimumFps\"\n [placeholder]=\"defaultFilterModel.minimumFps\"\n type=\"number\"\n name=\"minimumFps\"\n />\n </label>\n <hr />\n <h5>Kalman Filter</h5>\n <label>\n Initial Bitrate Estimate (kbit/s):\n <input\n [(ngModel)]=\"settings.initialBitrateEstimate\"\n [placeholder]=\"defaultFilterModel.initialBitrateEstimate\"\n type=\"number\"\n name=\"initialBitrateEstimate\"\n />\n </label>\n <label>\n Initial Error Covariance:\n <input\n [(ngModel)]=\"settings.initialErrorCovariance\"\n [placeholder]=\"defaultFilterModel.initialErrorCovariance\"\n type=\"number\"\n name=\"initialErrorCovariance\"\n />\n </label>\n <label>\n Process Noise (Q):\n <input\n [(ngModel)]=\"settings.processNoise\"\n [placeholder]=\"defaultFilterModel.processNoise\"\n type=\"number\"\n name=\"processNoise\"\n />\n </label>\n <label>\n Measurement Noise (R):\n <input\n [(ngModel)]=\"settings.measurementNoise\"\n [placeholder]=\"defaultFilterModel.measurementNoise\"\n type=\"number\"\n name=\"measurementNoise\"\n />\n </label>\n </form>\n <br />\n <br />\n <div class=\"apply-button-container\">\n <button (click)=\"reset()\">Reset</button>\n <button (click)=\"saveSettings()\">Apply</button>\n </div>\n </div>\n }\n</div>\n", styles: [".settings-container{position:absolute;top:65px;right:10px;z-index:1000}.gear-button{right:0;width:50px;position:absolute;cursor:pointer;font-size:10px;background:none;font-weight:500;border:1px solid;border-radius:9px;background:#0162cc;color:#fff;padding:4px}.gear-button>*{color:#e6f419;filter:drop-shadow(1px 1px 1px rgb(0,0,0))}.settings-panel{font-size:13px;background:#fff;border:1px solid #ccc;padding:15px;margin-top:25px;border-radius:4px;box-shadow:0 2px 8px #0003}.settings-panel h3{margin-top:0}.settings-panel form{display:flex;flex-direction:column}.settings-panel label{margin-bottom:10px;display:flex;flex-direction:column}.settings-panel input{padding:4px;margin-top:4px;border:1px solid #ccc;border-radius:2px}.apply-button-container{position:absolute;bottom:15px;right:15px}.apply-button-container button{padding:6px 12px;border:none;background-color:#1976d2;color:#fff;border-radius:4px;cursor:pointer;margin:2px}.apply-button-container button:hover{background-color:#1565c0}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i1.NgForm, selector: "form:not([ngNoForm]):not([formGroup]):not([formArray]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
6056
|
+
}
|
|
6057
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: FilterSettingsComponent, decorators: [{
|
|
6058
|
+
type: Component,
|
|
6059
|
+
args: [{ selector: 'app-filter-settings', imports: [FormsModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"settings-container\">\n <button (click)=\"togglePanel()\" class=\"gear-button\">LBM</button>\n @if (settings.panelOpen) {\n <div class=\"settings-panel\">\n <h3>LBM Trigger Settings</h3>\n <h5>Data Flow Monitor</h5>\n <form>\n <label>\n Monitoring delay:\n <input\n [(ngModel)]=\"settings.monitoringDelayTime\"\n [placeholder]=\"defaultFilterModel.monitoringDelayTime\"\n type=\"number\"\n name=\"monitoringDelayTime\"\n />\n </label>\n\n <label>\n Minimum Bitrate (kbit/s):\n <input\n [(ngModel)]=\"settings.minimumBitrate\"\n [placeholder]=\"defaultFilterModel.minimumBitrate\"\n type=\"number\"\n name=\"minimumBitrate\"\n />\n </label>\n <label>\n Yellow Flag (%):\n <input\n [(ngModel)]=\"settings.yellowFlag\"\n [placeholder]=\"defaultFilterModel.yellowFlag\"\n type=\"number\"\n name=\"yellowFlag\"\n />\n </label>\n <label>\n Red Flag (%):\n <input\n [(ngModel)]=\"settings.redFlag\"\n [placeholder]=\"defaultFilterModel.redFlag\"\n type=\"number\"\n name=\"redFlag\"\n />\n </label>\n <label>\n Minimum FPS:\n <input\n [(ngModel)]=\"settings.minimumFps\"\n [placeholder]=\"defaultFilterModel.minimumFps\"\n type=\"number\"\n name=\"minimumFps\"\n />\n </label>\n <hr />\n <h5>Kalman Filter</h5>\n <label>\n Initial Bitrate Estimate (kbit/s):\n <input\n [(ngModel)]=\"settings.initialBitrateEstimate\"\n [placeholder]=\"defaultFilterModel.initialBitrateEstimate\"\n type=\"number\"\n name=\"initialBitrateEstimate\"\n />\n </label>\n <label>\n Initial Error Covariance:\n <input\n [(ngModel)]=\"settings.initialErrorCovariance\"\n [placeholder]=\"defaultFilterModel.initialErrorCovariance\"\n type=\"number\"\n name=\"initialErrorCovariance\"\n />\n </label>\n <label>\n Process Noise (Q):\n <input\n [(ngModel)]=\"settings.processNoise\"\n [placeholder]=\"defaultFilterModel.processNoise\"\n type=\"number\"\n name=\"processNoise\"\n />\n </label>\n <label>\n Measurement Noise (R):\n <input\n [(ngModel)]=\"settings.measurementNoise\"\n [placeholder]=\"defaultFilterModel.measurementNoise\"\n type=\"number\"\n name=\"measurementNoise\"\n />\n </label>\n </form>\n <br />\n <br />\n <div class=\"apply-button-container\">\n <button (click)=\"reset()\">Reset</button>\n <button (click)=\"saveSettings()\">Apply</button>\n </div>\n </div>\n }\n</div>\n", styles: [".settings-container{position:absolute;top:65px;right:10px;z-index:1000}.gear-button{right:0;width:50px;position:absolute;cursor:pointer;font-size:10px;background:none;font-weight:500;border:1px solid;border-radius:9px;background:#0162cc;color:#fff;padding:4px}.gear-button>*{color:#e6f419;filter:drop-shadow(1px 1px 1px rgb(0,0,0))}.settings-panel{font-size:13px;background:#fff;border:1px solid #ccc;padding:15px;margin-top:25px;border-radius:4px;box-shadow:0 2px 8px #0003}.settings-panel h3{margin-top:0}.settings-panel form{display:flex;flex-direction:column}.settings-panel label{margin-bottom:10px;display:flex;flex-direction:column}.settings-panel input{padding:4px;margin-top:4px;border:1px solid #ccc;border-radius:2px}.apply-button-container{position:absolute;bottom:15px;right:15px}.apply-button-container button{padding:6px 12px;border:none;background-color:#1976d2;color:#fff;border-radius:4px;cursor:pointer;margin:2px}.apply-button-container button:hover{background-color:#1565c0}\n"] }]
|
|
6060
|
+
}] });
|
|
6061
|
+
|
|
6062
|
+
class LowBandwidthDetectorComponent {
|
|
6063
|
+
constructor() {
|
|
6064
|
+
this.store = inject(Store);
|
|
6065
|
+
this.dialog = inject(Dialog);
|
|
6066
|
+
this.destroyRef = inject(DestroyRef);
|
|
6067
|
+
this.videoService = inject(VideoService);
|
|
6068
|
+
this.isDevMode = inject(DevModeService).isDevMode;
|
|
6069
|
+
this.viewportReady$ = this.store.select(unrealFeature.selectViewportReady);
|
|
6070
|
+
this.isLowBandwidth$ = this.store.select(unrealFeature.selectLowBandwidth);
|
|
6071
|
+
this.lowBandwidthTrigger$ = this.isLowBandwidth$.pipe(distinctUntilChanged$1(), filter(Truthy));
|
|
6072
|
+
this.highBandwidthTrigger$ = this.isLowBandwidth$.pipe(distinctUntilChanged$1(), filter(Falsy));
|
|
6073
|
+
this.viewPortDestroyedTrigger$ = this.viewportReady$.pipe(distinctUntilChanged$1(), filter(Falsy));
|
|
6074
|
+
this.aggregatedStats$ = this.videoService.videoStats$.pipe(map((data) => data.aggregatedStats), share());
|
|
6075
|
+
this.nativeQuality$ = this.videoService.videoStats$.pipe(map((data) => data.quality));
|
|
6076
|
+
this.monitoringTrigger$ = this.viewportReady$
|
|
6077
|
+
.pipe(combineLatestWith(this.isLowBandwidth$), map(([viewPortReady, isLowBandwidth]) => viewPortReady && !isLowBandwidth))
|
|
6078
|
+
.pipe(share());
|
|
6079
|
+
this.reTrigger$ = this.monitoringTrigger$.pipe(map(() => ({ quality: 'lime' })));
|
|
6080
|
+
this.canSwitchToLowBand$ = this.monitoringTrigger$.pipe(debounceTime(100), switchMap((canSwitch) => concat(of(false), timer(FilterModel.monitoringDelayTime).pipe(map(() => canSwitch))).pipe(take(2), takeUntil$1(this.monitoringTrigger$))), distinctUntilChanged$1(), share());
|
|
6081
|
+
this.videoQuality$ = merge(this.nativeQuality$, this.reTrigger$).pipe(share(), takeUntil$1(this.viewPortDestroyedTrigger$));
|
|
6082
|
+
this.fps$ = this.aggregatedStats$.pipe(map((data) => data.framesPerSecond || 0));
|
|
6083
|
+
this.bitrateDrop$ = this.aggregatedStats$.pipe(map((data) => data.dataFlowCheckResult), filter((br) => br.isDropDetected), takeUntil$1(merge(this.viewPortDestroyedTrigger$, this.reTrigger$)));
|
|
6084
|
+
this.fps = toSignal(this.fps$, { initialValue: 0 });
|
|
6085
|
+
this.nativeQuality = toSignal(this.nativeQuality$);
|
|
6086
|
+
this.isLowBandwidth = toSignal(this.isLowBandwidth$);
|
|
6087
|
+
this.aggregatedStats = toSignal(this.aggregatedStats$);
|
|
6088
|
+
this.isReducedQuality = signal(false, ...(ngDevMode ? [{ debugName: "isReducedQuality" }] : []));
|
|
6089
|
+
this.isIndicatorExpanded = signal(false, ...(ngDevMode ? [{ debugName: "isIndicatorExpanded" }] : []));
|
|
6090
|
+
this.isIndicatorExpandedTrigger$ = toObservable(this.isIndicatorExpanded).pipe(skip(1), distinctUntilChanged$1(), filter(Falsy));
|
|
6123
6091
|
}
|
|
6124
|
-
|
|
6125
|
-
|
|
6126
|
-
|
|
6127
|
-
|
|
6128
|
-
|
|
6129
|
-
|
|
6130
|
-
|
|
6131
|
-
|
|
6132
|
-
|
|
6133
|
-
|
|
6092
|
+
ngOnInit() {
|
|
6093
|
+
this.lowBandwidthTrigger$
|
|
6094
|
+
.pipe(takeUntilDestroyed(this.destroyRef))
|
|
6095
|
+
.subscribe(() => this.toggleIndicator(true));
|
|
6096
|
+
merge(this.viewPortDestroyedTrigger$, this.highBandwidthTrigger$)
|
|
6097
|
+
.pipe(takeUntilDestroyed(this.destroyRef))
|
|
6098
|
+
.subscribe(() => this.isReducedQuality.set(false));
|
|
6099
|
+
this.canSwitchToLowBand$
|
|
6100
|
+
.pipe(filter(Truthy), tap(() => {
|
|
6101
|
+
BITRATE_MONITOR.reset();
|
|
6102
|
+
this.isReducedQuality.set(false);
|
|
6103
|
+
}), switchMap(() => this.bitrateDrop$), tap(this.trySetLowBandwidth.bind(this)), map((dataFlowResult) => dataFlowResult?.isDropDetected), distinctUntilChanged$1(), tap(() => {
|
|
6104
|
+
this.isReducedQuality.set(true);
|
|
6105
|
+
}), switchMap(() => this.videoQuality$), filter((quality) => quality === 'lime'), tap(() => this.isReducedQuality.set(false)), takeUntilDestroyed(this.destroyRef))
|
|
6106
|
+
.subscribe();
|
|
6107
|
+
}
|
|
6108
|
+
trySetLowBandwidth(dataFlowResult) {
|
|
6109
|
+
// Evaluate FPS and native quality
|
|
6110
|
+
const isFpsLow = this.fps() < FilterModel.minimumFps;
|
|
6111
|
+
const isNativeQualityRed = this.nativeQuality() === 'red';
|
|
6112
|
+
const shouldSwitch = isNativeQualityRed || isFpsLow;
|
|
6113
|
+
// Exit if conditions are not met or already in low bandwidth
|
|
6114
|
+
if (!shouldSwitch || this.isLowBandwidth())
|
|
6115
|
+
return;
|
|
6116
|
+
const { quality, activeMedian } = dataFlowResult;
|
|
6117
|
+
const isQualityRed = quality === 'red';
|
|
6118
|
+
const isLowBitrate = activeMedian < FilterModel.minimumBitrate;
|
|
6119
|
+
// Final check for a red drop or low bitrate, or low FPS
|
|
6120
|
+
if (isQualityRed || isLowBitrate || isFpsLow) {
|
|
6121
|
+
let reason;
|
|
6122
|
+
if (isFpsLow) {
|
|
6123
|
+
reason = `FPS < ${FilterModel.minimumFps}`;
|
|
6124
|
+
}
|
|
6125
|
+
else if (quality === 'red') {
|
|
6126
|
+
reason = `Drop > ${FilterModel.redFlag}%`;
|
|
6127
|
+
}
|
|
6128
|
+
else {
|
|
6129
|
+
reason = `Speed < ${FilterModel.minimumBitrate}`;
|
|
6130
|
+
}
|
|
6131
|
+
Logger.info(`LBM DETECTED: ${reason}, switching to low bandwidth; `, dataFlowResult.message);
|
|
6132
|
+
this.store.dispatch(changeLowBandwidth({
|
|
6133
|
+
lowBandwidth: true,
|
|
6134
|
+
stats: {
|
|
6135
|
+
dataFlowResult,
|
|
6136
|
+
reason,
|
|
6137
|
+
triggerTime: this.aggregatedStats()?.runTime ?? 0,
|
|
6138
|
+
},
|
|
6139
|
+
}));
|
|
6134
6140
|
}
|
|
6135
|
-
|
|
6136
|
-
|
|
6141
|
+
}
|
|
6142
|
+
toggleIndicator(value) {
|
|
6143
|
+
this.isIndicatorExpanded.set(value);
|
|
6144
|
+
if (value) {
|
|
6145
|
+
this.hideIndicatorAfterDelay();
|
|
6137
6146
|
}
|
|
6138
6147
|
}
|
|
6139
|
-
|
|
6140
|
-
|
|
6148
|
+
hideIndicatorAfterDelay() {
|
|
6149
|
+
timer(3000)
|
|
6150
|
+
.pipe(take(1), takeUntil$1(this.isIndicatorExpandedTrigger$))
|
|
6151
|
+
.subscribe(() => this.toggleIndicator(false));
|
|
6152
|
+
}
|
|
6153
|
+
openLBMDialog() {
|
|
6154
|
+
this.dialog
|
|
6155
|
+
.open(LowBandwidthModalComponent)
|
|
6156
|
+
.closed.pipe(filter(Truthy))
|
|
6157
|
+
.subscribe(() => {
|
|
6158
|
+
this.toggleIndicator(false);
|
|
6159
|
+
this.store.dispatch(changeLowBandwidth({ lowBandwidth: false }));
|
|
6160
|
+
});
|
|
6161
|
+
}
|
|
6162
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: LowBandwidthDetectorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
6163
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: LowBandwidthDetectorComponent, isStandalone: true, selector: "app-low-bandwidth-detector", ngImport: i0, template: "@if (isReducedQuality() || isLowBandwidth()) {\n <div\n [class.expanded]=\"isIndicatorExpanded() && isLowBandwidth()\"\n class=\"lbm-indicator freeze-loader\"\n >\n <div (click)=\"toggleIndicator(true)\" class=\"lbm-icon\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 20 20\">\n <path\n fill=\"#85888E\"\n d=\"M7.189 3.605c-.73.145-1.438.35-2.126.614A14.412 14.412 0 0 0 .896 6.666a1.08 1.08 0 0 0-.375.844c0 .34.118.629.354.865s.524.36.865.375c.34.014.65-.09.927-.313a11.89 11.89 0 0 1 3.385-1.916 10.94 10.94 0 0 1 1.235-.375l-.098-2.541ZM7.385 8.708a9.107 9.107 0 0 0-2.906 1.48c-.264.194-.402.464-.416.812-.014.347.104.646.354.896.236.236.524.364.864.385.34.02.664-.073.97-.281.422-.29.878-.53 1.368-.721a2.545 2.545 0 0 1-.166-.814l-.068-1.757ZM12.386 11.267c.094-.25.15-.52.161-.802l.068-1.755a9.019 9.019 0 0 1 2.927 1.52c.264.193.4.46.406.801.007.34-.114.635-.364.885a1.213 1.213 0 0 1-.865.365A1.614 1.614 0 0 1 13.77 12a6.574 6.574 0 0 0-1.385-.733ZM12.713 6.146l.098-2.542c.73.146 1.438.351 2.127.615 1.541.59 2.93 1.406 4.166 2.447.25.223.379.5.386.834.007.333-.115.625-.365.875a1.253 1.253 0 0 1-.864.375c-.34.014-.65-.09-.927-.313a11.892 11.892 0 0 0-3.386-1.916 10.94 10.94 0 0 0-1.235-.375ZM8.813 16.187c.32.32.715.48 1.187.48.472 0 .868-.16 1.188-.48.32-.32.479-.715.479-1.187 0-.473-.16-.868-.48-1.188-.319-.32-.715-.479-1.187-.479-.472 0-.868.16-1.187.48-.32.319-.48.714-.48 1.187 0 .472.16.868.48 1.187Z\"\n />\n <path\n fill=\"#fff\"\n fill-rule=\"evenodd\"\n d=\"M10 .833c-.91 0-1.637.756-1.602 1.665l.304 7.92a1.299 1.299 0 0 0 2.596 0l.305-7.92A1.604 1.604 0 0 0 10 .833ZM8.813 16.187c.32.32.715.48 1.187.48.472 0 .868-.16 1.188-.48.32-.32.479-.715.479-1.187 0-.473-.16-.868-.48-1.188-.319-.32-.715-.479-1.187-.479-.472 0-.868.16-1.187.48-.32.319-.48.714-.48 1.187 0 .472.16.868.48 1.187Z\"\n clip-rule=\"evenodd\"\n />\n </svg>\n </div>\n\n <div\n [class.lbm-message--open]=\"isLowBandwidth() && isIndicatorExpanded()\"\n class=\"lbm-message\"\n >\n <p class=\"lbm-description\">\n Fluid Interactivity Modes were disabled due to an unstable connection.\n\n <button\n (click)=\"openLBMDialog()\"\n [attr.data-testid]=\"'learn-more-lbm'\"\n type=\"button\"\n class=\"lbm-learn-more\"\n >\n Learn more\n </button>\n </p>\n <button\n (click)=\"toggleIndicator(false)\"\n [attr.data-testid]=\"'close-lbm-indicator'\"\n type=\"button\"\n class=\"lbm-close\"\n >\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"20\"\n height=\"20\"\n viewBox=\"0 0 20 20\"\n fill=\"none\"\n >\n <path\n d=\"M10.0001 11.2289L6.68618 14.5428C6.51951 14.7095 6.31818 14.7895 6.08218 14.7828C5.84618 14.7755 5.64485 14.6885 5.47818 14.5218C5.31151 14.3552 5.22818 14.1505 5.22818 13.9078C5.22818 13.6645 5.31151 13.4595 5.47818 13.2928L8.77107 9.99992L5.45718 6.68603C5.29051 6.51936 5.21051 6.3147 5.21718 6.07203C5.22451 5.8287 5.31151 5.6237 5.47818 5.45703C5.64485 5.29036 5.84951 5.20703 6.09218 5.20703C6.33551 5.20703 6.54051 5.29036 6.70718 5.45703L10.0001 8.77092L13.314 5.45703C13.4806 5.29036 13.6853 5.20703 13.928 5.20703C14.1713 5.20703 14.3763 5.29036 14.543 5.45703C14.7096 5.6237 14.793 5.8287 14.793 6.07203C14.793 6.3147 14.7096 6.51936 14.543 6.68603L11.2291 9.99992L14.543 13.3138C14.7096 13.4805 14.793 13.6818 14.793 13.9178C14.793 14.1538 14.7096 14.3552 14.543 14.5218C14.3763 14.6885 14.1713 14.7718 13.928 14.7718C13.6853 14.7718 13.4806 14.6885 13.314 14.5218L10.0001 11.2289Z\"\n fill=\"white\"\n />\n </svg>\n </button>\n </div>\n </div>\n}\n\n@if (isDevMode) {\n <app-filter-settings />\n}\n", styles: [".freeze-loader,.lbm-indicator{--lbmPositionTop: 18px;--lbmPositionLeft: 18px;--lbmPositionRight: 18px;--lbmIndicatorSize: 36px;--lbmIndicatorBorderRadius: 8px;--lbmIndicatorCloseSize: 20px;--lbmPadding: 10px;--lbmTextSize: 13px;--lbmLineHeight: 20px;--lbmTextColor: #ffffff;--lbmBackgroundColor: rgba(17, 24, 39, .7);position:absolute;top:var(--lbmPositionTop);left:var(--lbmPositionLeft);z-index:10;display:flex;justify-content:center;align-items:flex-start;min-width:var(--lbmIndicatorSize);min-height:var(--lbmIndicatorSize);overflow:hidden}.freeze-loader.expanded,.lbm-indicator.expanded{--lbmPositionTop: 8px;--lbmPositionLeft: 8px;--lbmPositionRight: 8px;width:auto;max-width:calc(100% - var(--lbmPositionLeft) - var(--lbmPositionRight));padding:var(--lbmPadding);background:var(--lbmBackgroundColor);-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);border-radius:var(--lbmIndicatorBorderRadius);gap:12px}.freeze-loader .lbm-icon,.lbm-indicator .lbm-icon{display:flex;width:var(--lbmIndicatorSize);height:var(--lbmIndicatorSize);padding:8px;color:#fff;background-color:var(--lbmBackgroundColor);border-radius:50%;flex-shrink:0;cursor:pointer}.freeze-loader .lbm-icon svg,.freeze-loader .lbm-icon img,.lbm-indicator .lbm-icon svg,.lbm-indicator .lbm-icon img{width:100%;height:100%}.freeze-loader .lbm-close,.lbm-indicator .lbm-close{position:absolute;top:0;right:0;width:var(--lbmIndicatorCloseSize);height:var(--lbmIndicatorCloseSize);background-color:transparent;border:none;padding:0;cursor:pointer}.freeze-loader .lbm-message,.lbm-indicator .lbm-message{position:relative;opacity:0;height:0;max-width:0;padding-right:24px;pointer-events:none;visibility:hidden;animation:closeMessage .3s forwards}.freeze-loader .lbm-message--open,.lbm-indicator .lbm-message--open{position:relative;visibility:visible;display:flex;align-items:center;align-self:center;pointer-events:all;gap:12px;max-width:fit-content;height:auto;opacity:1;animation:openMessage .3s forwards}.freeze-loader .lbm-description,.lbm-indicator .lbm-description{margin:0;font-size:var(--lbmTextSize);font-weight:400;line-height:var(--lbmLineHeight);color:var(--lbmTextColor)}.freeze-loader .lbm-learn-more,.lbm-indicator .lbm-learn-more{display:inline-flex;padding:0;font-size:var(--lbmTextSize);font-weight:400;line-height:var(--lbmLineHeight);color:var(--lbmTextColor);-webkit-text-decoration-line:underline;text-decoration-line:underline;background-color:transparent;border:none;cursor:pointer}.freeze-loader .lbm-learn-more:hover,.lbm-indicator .lbm-learn-more:hover{text-decoration:none}@keyframes openMessage{0%{opacity:0;max-width:0;height:0}10%{height:auto;max-width:fit-content}to{opacity:1;height:auto;max-width:fit-content}}@keyframes closeMessage{0%{opacity:1;max-width:fit-content}to{opacity:0;max-width:0}}\n"], dependencies: [{ kind: "component", type: FilterSettingsComponent, selector: "app-filter-settings" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
6141
6164
|
}
|
|
6142
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type:
|
|
6143
|
-
type:
|
|
6144
|
-
|
|
6165
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: LowBandwidthDetectorComponent, decorators: [{
|
|
6166
|
+
type: Component,
|
|
6167
|
+
args: [{ selector: 'app-low-bandwidth-detector', changeDetection: ChangeDetectionStrategy.OnPush, imports: [FilterSettingsComponent], template: "@if (isReducedQuality() || isLowBandwidth()) {\n <div\n [class.expanded]=\"isIndicatorExpanded() && isLowBandwidth()\"\n class=\"lbm-indicator freeze-loader\"\n >\n <div (click)=\"toggleIndicator(true)\" class=\"lbm-icon\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 20 20\">\n <path\n fill=\"#85888E\"\n d=\"M7.189 3.605c-.73.145-1.438.35-2.126.614A14.412 14.412 0 0 0 .896 6.666a1.08 1.08 0 0 0-.375.844c0 .34.118.629.354.865s.524.36.865.375c.34.014.65-.09.927-.313a11.89 11.89 0 0 1 3.385-1.916 10.94 10.94 0 0 1 1.235-.375l-.098-2.541ZM7.385 8.708a9.107 9.107 0 0 0-2.906 1.48c-.264.194-.402.464-.416.812-.014.347.104.646.354.896.236.236.524.364.864.385.34.02.664-.073.97-.281.422-.29.878-.53 1.368-.721a2.545 2.545 0 0 1-.166-.814l-.068-1.757ZM12.386 11.267c.094-.25.15-.52.161-.802l.068-1.755a9.019 9.019 0 0 1 2.927 1.52c.264.193.4.46.406.801.007.34-.114.635-.364.885a1.213 1.213 0 0 1-.865.365A1.614 1.614 0 0 1 13.77 12a6.574 6.574 0 0 0-1.385-.733ZM12.713 6.146l.098-2.542c.73.146 1.438.351 2.127.615 1.541.59 2.93 1.406 4.166 2.447.25.223.379.5.386.834.007.333-.115.625-.365.875a1.253 1.253 0 0 1-.864.375c-.34.014-.65-.09-.927-.313a11.892 11.892 0 0 0-3.386-1.916 10.94 10.94 0 0 0-1.235-.375ZM8.813 16.187c.32.32.715.48 1.187.48.472 0 .868-.16 1.188-.48.32-.32.479-.715.479-1.187 0-.473-.16-.868-.48-1.188-.319-.32-.715-.479-1.187-.479-.472 0-.868.16-1.187.48-.32.319-.48.714-.48 1.187 0 .472.16.868.48 1.187Z\"\n />\n <path\n fill=\"#fff\"\n fill-rule=\"evenodd\"\n d=\"M10 .833c-.91 0-1.637.756-1.602 1.665l.304 7.92a1.299 1.299 0 0 0 2.596 0l.305-7.92A1.604 1.604 0 0 0 10 .833ZM8.813 16.187c.32.32.715.48 1.187.48.472 0 .868-.16 1.188-.48.32-.32.479-.715.479-1.187 0-.473-.16-.868-.48-1.188-.319-.32-.715-.479-1.187-.479-.472 0-.868.16-1.187.48-.32.319-.48.714-.48 1.187 0 .472.16.868.48 1.187Z\"\n clip-rule=\"evenodd\"\n />\n </svg>\n </div>\n\n <div\n [class.lbm-message--open]=\"isLowBandwidth() && isIndicatorExpanded()\"\n class=\"lbm-message\"\n >\n <p class=\"lbm-description\">\n Fluid Interactivity Modes were disabled due to an unstable connection.\n\n <button\n (click)=\"openLBMDialog()\"\n [attr.data-testid]=\"'learn-more-lbm'\"\n type=\"button\"\n class=\"lbm-learn-more\"\n >\n Learn more\n </button>\n </p>\n <button\n (click)=\"toggleIndicator(false)\"\n [attr.data-testid]=\"'close-lbm-indicator'\"\n type=\"button\"\n class=\"lbm-close\"\n >\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"20\"\n height=\"20\"\n viewBox=\"0 0 20 20\"\n fill=\"none\"\n >\n <path\n d=\"M10.0001 11.2289L6.68618 14.5428C6.51951 14.7095 6.31818 14.7895 6.08218 14.7828C5.84618 14.7755 5.64485 14.6885 5.47818 14.5218C5.31151 14.3552 5.22818 14.1505 5.22818 13.9078C5.22818 13.6645 5.31151 13.4595 5.47818 13.2928L8.77107 9.99992L5.45718 6.68603C5.29051 6.51936 5.21051 6.3147 5.21718 6.07203C5.22451 5.8287 5.31151 5.6237 5.47818 5.45703C5.64485 5.29036 5.84951 5.20703 6.09218 5.20703C6.33551 5.20703 6.54051 5.29036 6.70718 5.45703L10.0001 8.77092L13.314 5.45703C13.4806 5.29036 13.6853 5.20703 13.928 5.20703C14.1713 5.20703 14.3763 5.29036 14.543 5.45703C14.7096 5.6237 14.793 5.8287 14.793 6.07203C14.793 6.3147 14.7096 6.51936 14.543 6.68603L11.2291 9.99992L14.543 13.3138C14.7096 13.4805 14.793 13.6818 14.793 13.9178C14.793 14.1538 14.7096 14.3552 14.543 14.5218C14.3763 14.6885 14.1713 14.7718 13.928 14.7718C13.6853 14.7718 13.4806 14.6885 13.314 14.5218L10.0001 11.2289Z\"\n fill=\"white\"\n />\n </svg>\n </button>\n </div>\n </div>\n}\n\n@if (isDevMode) {\n <app-filter-settings />\n}\n", styles: [".freeze-loader,.lbm-indicator{--lbmPositionTop: 18px;--lbmPositionLeft: 18px;--lbmPositionRight: 18px;--lbmIndicatorSize: 36px;--lbmIndicatorBorderRadius: 8px;--lbmIndicatorCloseSize: 20px;--lbmPadding: 10px;--lbmTextSize: 13px;--lbmLineHeight: 20px;--lbmTextColor: #ffffff;--lbmBackgroundColor: rgba(17, 24, 39, .7);position:absolute;top:var(--lbmPositionTop);left:var(--lbmPositionLeft);z-index:10;display:flex;justify-content:center;align-items:flex-start;min-width:var(--lbmIndicatorSize);min-height:var(--lbmIndicatorSize);overflow:hidden}.freeze-loader.expanded,.lbm-indicator.expanded{--lbmPositionTop: 8px;--lbmPositionLeft: 8px;--lbmPositionRight: 8px;width:auto;max-width:calc(100% - var(--lbmPositionLeft) - var(--lbmPositionRight));padding:var(--lbmPadding);background:var(--lbmBackgroundColor);-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);border-radius:var(--lbmIndicatorBorderRadius);gap:12px}.freeze-loader .lbm-icon,.lbm-indicator .lbm-icon{display:flex;width:var(--lbmIndicatorSize);height:var(--lbmIndicatorSize);padding:8px;color:#fff;background-color:var(--lbmBackgroundColor);border-radius:50%;flex-shrink:0;cursor:pointer}.freeze-loader .lbm-icon svg,.freeze-loader .lbm-icon img,.lbm-indicator .lbm-icon svg,.lbm-indicator .lbm-icon img{width:100%;height:100%}.freeze-loader .lbm-close,.lbm-indicator .lbm-close{position:absolute;top:0;right:0;width:var(--lbmIndicatorCloseSize);height:var(--lbmIndicatorCloseSize);background-color:transparent;border:none;padding:0;cursor:pointer}.freeze-loader .lbm-message,.lbm-indicator .lbm-message{position:relative;opacity:0;height:0;max-width:0;padding-right:24px;pointer-events:none;visibility:hidden;animation:closeMessage .3s forwards}.freeze-loader .lbm-message--open,.lbm-indicator .lbm-message--open{position:relative;visibility:visible;display:flex;align-items:center;align-self:center;pointer-events:all;gap:12px;max-width:fit-content;height:auto;opacity:1;animation:openMessage .3s forwards}.freeze-loader .lbm-description,.lbm-indicator .lbm-description{margin:0;font-size:var(--lbmTextSize);font-weight:400;line-height:var(--lbmLineHeight);color:var(--lbmTextColor)}.freeze-loader .lbm-learn-more,.lbm-indicator .lbm-learn-more{display:inline-flex;padding:0;font-size:var(--lbmTextSize);font-weight:400;line-height:var(--lbmLineHeight);color:var(--lbmTextColor);-webkit-text-decoration-line:underline;text-decoration-line:underline;background-color:transparent;border:none;cursor:pointer}.freeze-loader .lbm-learn-more:hover,.lbm-indicator .lbm-learn-more:hover{text-decoration:none}@keyframes openMessage{0%{opacity:0;max-width:0;height:0}10%{height:auto;max-width:fit-content}to{opacity:1;height:auto;max-width:fit-content}}@keyframes closeMessage{0%{opacity:1;max-width:fit-content}to{opacity:0;max-width:0}}\n"] }]
|
|
6168
|
+
}] });
|
|
6145
6169
|
|
|
6146
6170
|
function provideAngularUnrealModule(config) {
|
|
6147
6171
|
return makeEnvironmentProviders([
|
|
@@ -6151,18 +6175,23 @@ function provideAngularUnrealModule(config) {
|
|
|
6151
6175
|
ConsoleExtensionsService,
|
|
6152
6176
|
InputService,
|
|
6153
6177
|
VideoService,
|
|
6178
|
+
RegionsPingService,
|
|
6179
|
+
FileReceiverService,
|
|
6180
|
+
FileHandlerService,
|
|
6181
|
+
AnalyticsService,
|
|
6182
|
+
UnrealCallbackService,
|
|
6154
6183
|
{
|
|
6155
6184
|
provide: WebRtcPlayerService,
|
|
6156
6185
|
useClass: config?.newWebRtcConnector
|
|
6157
6186
|
? WebRtcPlayerService2
|
|
6158
6187
|
: WebRtcPlayerService,
|
|
6159
6188
|
},
|
|
6160
|
-
|
|
6161
|
-
|
|
6162
|
-
|
|
6163
|
-
|
|
6164
|
-
|
|
6165
|
-
|
|
6189
|
+
{
|
|
6190
|
+
provide: FpsMonitorService,
|
|
6191
|
+
useClass: config?.playwright || config?.fpsMonitor
|
|
6192
|
+
? FpsMonitorPlaywrightService
|
|
6193
|
+
: FpsMonitorService,
|
|
6194
|
+
},
|
|
6166
6195
|
{
|
|
6167
6196
|
provide: StreamStatusTelemetryService,
|
|
6168
6197
|
useClass: config?.playwright
|
|
@@ -6232,5 +6261,5 @@ function provideAngularUnrealModule(config) {
|
|
|
6232
6261
|
* Generated bundle index. Do not edit.
|
|
6233
6262
|
*/
|
|
6234
6263
|
|
|
6235
|
-
export { AFKService, AfkPlaywrightService, AfkTimeoutModalComponent, AggregatorPlaywrightService, AggregatorService, AnalyticsService, AnswerHandler, CONSOLE_COMMAND_DISABLE_MESSAGES, CONSOLE_COMMAND_ENABLE_MESSAGES, CONSOLE_COMMAND_PIXEL_QUALITY, CommandTelemetryPlaywrightService, CommandTelemetryService, CommandsEffects, ConfigHandler, ConsoleExtensionsPlaywrightService, ConsoleExtensionsService, ControlScheme, DATA_CHANNEL_CONNECTION_TIMEOUT, DEBOUNCE_TO_MANY_RESIZE_CALLS, DEFAULT_AFK_TIMEOUT, DEFAULT_AFK_TIMEOUT_PERIOD, DEFAULT_RECONNECT_DELAY_MS, DEFAULT_RECONNECT_ENABLED, DEFAULT_RECONNECT_MAX_ATTEMPTS, DEFAULT_RECONNECT_ON_DATACHANNEL_CLOSE, DEFAULT_RECONNECT_ON_ICE_FAILURE, DataFlowMonitor, DevModeService, DisconnectReason, FULL_HD_HEIGHT, FULL_HD_WIDTH, FileHandlerService, FileReceiverPlaywrightService, FileReceiverService, FilterSettingsComponent, FpsMonitorService, FreezeFrameComponent, FreezeFramePlaywrightService, FreezeFrameService, IceCandidateHandler, ImageLoadingSrcComponent, InputOptions, InputPlaywrightService, InputService, InstanceReadyHandler, InstanceReservedHandler, IntroSrcComponent, KalmanFilter1D, LatencyTimings, LowBandwidthDetectorComponent, LowBandwidthModalComponent, MINIMAL_FPS, MessageType, MouseButton, MouseButtonsMask, OnCloseHandler, OnErrorHandler, OnMessageHandler, OnOpenHandler, OrchestrationMessageTypes, POLLING_TIME, PingHandler, PlayerCountHandler, RegionsPingService, SAME_SIZE_THRESHOLD, SCREEN_LOCKER_CONTAINER_ID, SIGNALLING_PERCENT_VALUE, SSInfoHandler, STREAMING_VIDEO_ID, SafePipe, SignallingPlaywrightService, SignallingService, SpecialKeyCodes, StatGraphComponent, StreamStatusTelemetryPlaywrightService, StreamStatusTelemetryService, SubService, ToClientMessage, UNREAL_CONFIG, UnrealCallbackService, UnrealCommunicatorPlaywrightService, UnrealCommunicatorService, UnrealEffects, UnrealSceneComponent, UnrealStatusMessage, VideoService, VideoStatsComponent, WSCloseCode_CIRRUS_ABNORMAL_CLOSURE, WSCloseCode_CIRRUS_MAX_PLAYERS_ERROR, WSCloseCode_CIRRUS_PLAYER_DISCONNECTED, WSCloseCode_CIRRUS_STREAMER_KIKED_PLAYER, WSCloseCode_FORCE_CIRRUS_CLOSE, WSCloseCode_NORMAL_AFK_TIMEOUT, WSCloseCode_NORMAL_CLOSURE, WSCloseCode_NORMAL_MANUAL_DISCONNECT, WSCloseCode_UNKNOWN, WSCloseCodes, WS_OPEN_STATE, WS_TIMEOUT, WebRtcPlayerService, WebrtcErrorModalComponent, abortEstablishingConnection, changeLowBandwidth, changeStatusMainVideoOnScene, changeStreamResolutionAction, changeStreamResolutionSuccessAction, commandCompleted, commandStarted, commandsFeature, commandsReducer, dataChannelConnected, dataChannelReady, decodeData, destroyRemoteConnections, destroyUnrealScene, disconnectStream, dispatchResize, dropConnection, dropDC, dropPC, floatToSmoothPercents, forceResizeViewport, fromResizeObserver, getActiveUrl, getImageFromVideoStream, getRtcErrorMessage, iceConnectionFailed, initSignalling, keepMaxUntilReset, provideAngularUnrealModule, qpToQuality, reconnectPeer, reconnectPeerFailed, reconnectPeerSuccess, resetAfk, resetConfig, resetDataChannelForReconnect, resetIntroSrc, resetWarnTimeout, saveAnalyticsEvent, selectClientAndViewIds, selectCommandProgress, selectCommandsInProgress, selectFreezeFrameCombinedDataUrl, selectFreezeFrameDataUrl, selectFreezeFrameDataUrlFromVideo, selectFreezeFrameProgressMessageFromVideo, selectIsAutostart, selectIsExistMatchUrls, selectIsFreezeFrameLoading, selectIsLoaderScreenVisible, selectIsShowPopupWithoutAutoStart, selectIsVideoPlayingAndDataChannelConnected, selectLastCommandInProgress, selectShowLoader, selectShowReconnectPopup, selectSignalingParameters, selectStreamConfig, selectTotalProgress, selectWarnTimeout, setAfkTimerCountdown, setAfkTimerHide, setAfkTimerVisible, setAwsInstance, setCirrusConnected, setCirrusDisconnected, setConfig, setDataChannelConnected, setEstablishingConnection, setFreezeFrame, setFreezeFrameFromVideo, setIntroImageSrc, setIntroVideoSrc, setKalmanSettings, setLoadingImageSrc, setLoopBackCommandIsCompleted, setMaxFps, setOrchestrationContext, setOrchestrationMessage, setOrchestrationParameters, setOrchestrationProgress, setSignalingName, setStatusMessage, setStatusPercentSignallingServer, setStreamClientCompanyId, setStreamViewId, setUnrealPlaywrightConfig, setViewportNotReady, setViewportReady, showUnrealErrorMessage, smoothTransition, startStream, trackMixpanelEvent, unrealCommandCallback, unrealFeature, unrealReducer, updateCirrusInfo, videoAdaptedToContainer, videoStreamInitialized };
|
|
6264
|
+
export { AFKService, AfkPlaywrightService, AfkTimeoutModalComponent, AggregatorPlaywrightService, AggregatorService, AnalyticsService, AnswerHandler, CONSOLE_COMMAND_DISABLE_MESSAGES, CONSOLE_COMMAND_ENABLE_MESSAGES, CONSOLE_COMMAND_PIXEL_QUALITY, CommandTelemetryPlaywrightService, CommandTelemetryService, CommandsEffects, ConfigHandler, ConsoleExtensionsPlaywrightService, ConsoleExtensionsService, ControlScheme, DATA_CHANNEL_CONNECTION_TIMEOUT, DEBOUNCE_TO_MANY_RESIZE_CALLS, DEFAULT_AFK_TIMEOUT, DEFAULT_AFK_TIMEOUT_PERIOD, DEFAULT_RECONNECT_DELAY_MS, DEFAULT_RECONNECT_ENABLED, DEFAULT_RECONNECT_MAX_ATTEMPTS, DEFAULT_RECONNECT_ON_DATACHANNEL_CLOSE, DEFAULT_RECONNECT_ON_ICE_FAILURE, DataFlowMonitor, DevModeService, DisconnectReason, FULL_HD_HEIGHT, FULL_HD_WIDTH, FileHandlerService, FileReceiverPlaywrightService, FileReceiverService, FilterSettingsComponent, FpsMonitorPlaywrightService, FpsMonitorService, FreezeFrameComponent, FreezeFramePlaywrightService, FreezeFrameService, IceCandidateHandler, ImageLoadingSrcComponent, InputOptions, InputPlaywrightService, InputService, InstanceReadyHandler, InstanceReservedHandler, IntroSrcComponent, KalmanFilter1D, LatencyTimings, LowBandwidthDetectorComponent, LowBandwidthModalComponent, MINIMAL_FPS, MessageType, MouseButton, MouseButtonsMask, OnCloseHandler, OnErrorHandler, OnMessageHandler, OnOpenHandler, OrchestrationMessageTypes, POLLING_TIME, PingHandler, PlayerCountHandler, RegionsPingService, SAME_SIZE_THRESHOLD, SCREEN_LOCKER_CONTAINER_ID, SIGNALLING_PERCENT_VALUE, SSInfoHandler, STREAMING_VIDEO_ID, SafePipe, SignallingPlaywrightService, SignallingService, SpecialKeyCodes, StatGraphComponent, StreamStatusTelemetryPlaywrightService, StreamStatusTelemetryService, SubService, ToClientMessage, UNREAL_CONFIG, UnrealCallbackService, UnrealCommunicatorPlaywrightService, UnrealCommunicatorService, UnrealEffects, UnrealSceneComponent, UnrealStatusMessage, VideoService, VideoStatsComponent, WSCloseCode_CIRRUS_ABNORMAL_CLOSURE, WSCloseCode_CIRRUS_MAX_PLAYERS_ERROR, WSCloseCode_CIRRUS_PLAYER_DISCONNECTED, WSCloseCode_CIRRUS_STREAMER_KIKED_PLAYER, WSCloseCode_FORCE_CIRRUS_CLOSE, WSCloseCode_NORMAL_AFK_TIMEOUT, WSCloseCode_NORMAL_CLOSURE, WSCloseCode_NORMAL_MANUAL_DISCONNECT, WSCloseCode_UNKNOWN, WSCloseCodes, WS_OPEN_STATE, WS_TIMEOUT, WebRtcPlayerService, WebRtcPlayerService2, WebrtcErrorModalComponent, abortEstablishingConnection, changeLowBandwidth, changeStatusMainVideoOnScene, changeStreamResolutionAction, changeStreamResolutionSuccessAction, commandCompleted, commandStarted, commandsFeature, commandsReducer, dataChannelConnected, dataChannelReady, decodeData, destroyRemoteConnections, destroyUnrealScene, disconnectStream, dispatchResize, dropConnection, dropDC, dropPC, floatToSmoothPercents, forceResizeViewport, fromResizeObserver, getActiveUrl, getImageFromVideoStream, getRtcErrorMessage, iceConnectionFailed, initSignalling, keepMaxUntilReset, provideAngularUnrealModule, qpToQuality, reconnectPeer, reconnectPeerFailed, reconnectPeerSuccess, resetAfk, resetConfig, resetDataChannelForReconnect, resetIntroSrc, resetWarnTimeout, saveAnalyticsEvent, selectClientAndViewIds, selectCommandProgress, selectCommandsInProgress, selectFreezeFrameCombinedDataUrl, selectFreezeFrameDataUrl, selectFreezeFrameDataUrlFromVideo, selectFreezeFrameProgressMessageFromVideo, selectIsAutostart, selectIsExistMatchUrls, selectIsFreezeFrameLoading, selectIsLoaderScreenVisible, selectIsShowPopupWithoutAutoStart, selectIsVideoPlayingAndDataChannelConnected, selectLastCommandInProgress, selectShowLoader, selectShowReconnectPopup, selectSignalingParameters, selectStreamConfig, selectTotalProgress, selectWarnTimeout, setAfkTimerCountdown, setAfkTimerHide, setAfkTimerVisible, setAwsInstance, setCirrusConnected, setCirrusDisconnected, setConfig, setDataChannelConnected, setEstablishingConnection, setFreezeFrame, setFreezeFrameFromVideo, setIntroImageSrc, setIntroVideoSrc, setKalmanSettings, setLoadingImageSrc, setLoopBackCommandIsCompleted, setMaxFps, setOrchestrationContext, setOrchestrationMessage, setOrchestrationParameters, setOrchestrationProgress, setSignalingName, setStatusMessage, setStatusPercentSignallingServer, setStreamClientCompanyId, setStreamViewId, setUnrealPlaywrightConfig, setViewportNotReady, setViewportReady, showUnrealErrorMessage, smoothTransition, startStream, trackMixpanelEvent, unrealCommandCallback, unrealFeature, unrealReducer, updateCirrusInfo, videoAdaptedToContainer, videoStreamInitialized };
|
|
6236
6265
|
//# sourceMappingURL=3dsource-angular-unreal-module.mjs.map
|