@3dsource/angular-unreal-module 0.0.35 → 0.0.36
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
2
|
import { InjectionToken, inject, Injectable, ChangeDetectionStrategy, Component, Pipe, DestroyRef, signal, ElementRef, input, HostListener, Input, ViewChild, computed, output } from '@angular/core';
|
|
3
|
-
import { filter, withLatestFrom, distinctUntilChanged, first, catchError, map, tap, delay, takeUntil,
|
|
3
|
+
import { filter, withLatestFrom, distinctUntilChanged, switchMap, first, catchError, map as map$1, tap, delay, takeUntil, debounceTime, exhaustMap, skip as skip$1 } from 'rxjs/operators';
|
|
4
4
|
import { createAction, props, createReducer, on, createFeature, Store, createSelector } from '@ngrx/store';
|
|
5
5
|
import { Actions, createEffect, ofType } from '@ngrx/effects';
|
|
6
|
-
import { skip, share, merge,
|
|
6
|
+
import { skip, share, merge, Subject, interval, map, from, take, fromEvent, timer, of, combineLatest, switchMap as switchMap$1, timeout, retryWhen, tap as tap$1, startWith, combineLatestWith, takeUntil as takeUntil$1, auditTime, EMPTY, debounceTime as debounceTime$1, scan, Observable, BehaviorSubject, first as first$1, distinctUntilChanged as distinctUntilChanged$1, concat } from 'rxjs';
|
|
7
7
|
import { concatLatestFrom, mapResponse } from '@ngrx/operators';
|
|
8
|
-
import { Falsy, Truthy, Logger,
|
|
8
|
+
import { Falsy, Truthy, Logger, calculateMedian, clampf, Signal, tapLog, generateUuid, COLOR_CODES, where, KeyboardNumericCode, InvertedKeyMap, Semaphore, isEmpty, lerp, smoothTransition, getCanvasCached, getSnapshot, whereNot, HEXtoRGB, RGBtoHSV, inverseLerp, HSVtoRGB, RGBtoHEX, fpIsASameAsB, fitIntoRectangle } from '@3dsource/utils';
|
|
9
9
|
import { toSignal, takeUntilDestroyed, toObservable } from '@angular/core/rxjs-interop';
|
|
10
10
|
import { HttpClient } from '@angular/common/http';
|
|
11
11
|
import { DialogRef, DIALOG_DATA, Dialog } from '@angular/cdk/dialog';
|
|
@@ -678,6 +678,309 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.0", ngImpor
|
|
|
678
678
|
type: Injectable
|
|
679
679
|
}] });
|
|
680
680
|
|
|
681
|
+
class DataFlowMonitor {
|
|
682
|
+
/**
|
|
683
|
+
* Initializes the DataFlowMonitor monitor.
|
|
684
|
+
* @param yellowFlagThresholdPercentage - The percentage drop to trigger a YELLOW warning (default: 15%).
|
|
685
|
+
* @param redFlagThresholdPercentage - The percentage drop to trigger a RED warning (default: 30%).
|
|
686
|
+
* @param historyBufferLength - buffer length (default: 100).
|
|
687
|
+
* @param splitPoint - The point at which to split the history buffer into two halves (default: 0.5).
|
|
688
|
+
*/
|
|
689
|
+
constructor(yellowFlagThresholdPercentage = 15, redFlagThresholdPercentage = 30, historyBufferLength = 100, splitPoint = 0.5) {
|
|
690
|
+
this.yellowFlagThresholdPercentage = yellowFlagThresholdPercentage;
|
|
691
|
+
this.redFlagThresholdPercentage = redFlagThresholdPercentage;
|
|
692
|
+
this.historyBufferLength = historyBufferLength;
|
|
693
|
+
this.splitPoint = splitPoint;
|
|
694
|
+
this.dataHistory = [];
|
|
695
|
+
}
|
|
696
|
+
reset() {
|
|
697
|
+
this.dataHistory.length = 0;
|
|
698
|
+
}
|
|
699
|
+
config(data) {
|
|
700
|
+
this.reset();
|
|
701
|
+
this.yellowFlagThresholdPercentage = data.yellowFlag;
|
|
702
|
+
this.redFlagThresholdPercentage = data.redFlag;
|
|
703
|
+
}
|
|
704
|
+
/**
|
|
705
|
+
* Adds a new bitrate measurement and checks for significant drops.
|
|
706
|
+
* @param currentValue - The current bitrate in kbps.
|
|
707
|
+
* @returns BitrateCheckResult indicating if a drop was detected.
|
|
708
|
+
*/
|
|
709
|
+
addValue(currentValue) {
|
|
710
|
+
if (isNaN(currentValue) || currentValue < 1) {
|
|
711
|
+
return {};
|
|
712
|
+
}
|
|
713
|
+
const historyLength = this.dataHistory.length;
|
|
714
|
+
if (historyLength > this.historyBufferLength) {
|
|
715
|
+
this.dataHistory.shift();
|
|
716
|
+
}
|
|
717
|
+
this.dataHistory.push(currentValue);
|
|
718
|
+
if (historyLength < this.historyBufferLength) {
|
|
719
|
+
this.dataHistory.length = this.historyBufferLength;
|
|
720
|
+
this.dataHistory.fill(currentValue, 0, this.historyBufferLength);
|
|
721
|
+
}
|
|
722
|
+
const splitIndex = Math.floor(historyLength * this.splitPoint);
|
|
723
|
+
const firstHalf = this.dataHistory.slice(0, splitIndex);
|
|
724
|
+
const secondHalf = this.dataHistory.slice(splitIndex);
|
|
725
|
+
const firstHalfMedian = calculateMedian(firstHalf);
|
|
726
|
+
const secondHalfMedian = calculateMedian(secondHalf);
|
|
727
|
+
const dropPercentage = clampf(0, 100, ((firstHalfMedian - secondHalfMedian) / firstHalfMedian) * 100);
|
|
728
|
+
const isRedFlag = dropPercentage >= this.redFlagThresholdPercentage;
|
|
729
|
+
if (dropPercentage >= this.yellowFlagThresholdPercentage) {
|
|
730
|
+
return {
|
|
731
|
+
config: {
|
|
732
|
+
yellowFlagThresholdPercentage: this.yellowFlagThresholdPercentage,
|
|
733
|
+
redFlagThresholdPercentage: this.redFlagThresholdPercentage,
|
|
734
|
+
historyBufferLength: this.historyBufferLength,
|
|
735
|
+
splitPoint: this.splitPoint,
|
|
736
|
+
},
|
|
737
|
+
isDropDetected: true,
|
|
738
|
+
dropPercentage,
|
|
739
|
+
dataHistory: [...this.dataHistory],
|
|
740
|
+
activeMedian: secondHalfMedian,
|
|
741
|
+
quality: isRedFlag ? 'red' : 'orange',
|
|
742
|
+
message: `Significant flow drop detected: ${dropPercentage.toFixed(2)}% (from ${firstHalfMedian} to ${secondHalfMedian})`,
|
|
743
|
+
};
|
|
744
|
+
}
|
|
745
|
+
return {
|
|
746
|
+
config: {
|
|
747
|
+
yellowFlagThresholdPercentage: this.yellowFlagThresholdPercentage,
|
|
748
|
+
redFlagThresholdPercentage: this.redFlagThresholdPercentage,
|
|
749
|
+
historyBufferLength: this.historyBufferLength,
|
|
750
|
+
splitPoint: this.splitPoint,
|
|
751
|
+
},
|
|
752
|
+
isDropDetected: false,
|
|
753
|
+
dropPercentage,
|
|
754
|
+
dataHistory: [...this.dataHistory],
|
|
755
|
+
activeMedian: secondHalfMedian,
|
|
756
|
+
quality: 'lime',
|
|
757
|
+
message: 'Stable flow',
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
/**
|
|
763
|
+
* Default LBM Filter Parameters
|
|
764
|
+
*/
|
|
765
|
+
const DefaultFilterModel = {
|
|
766
|
+
minimumBitrate: 1600,
|
|
767
|
+
yellowFlag: 30,
|
|
768
|
+
redFlag: 50,
|
|
769
|
+
minimumFps: 5,
|
|
770
|
+
monitoringDelayTime: 5000,
|
|
771
|
+
initialBitrateEstimate: 1600,
|
|
772
|
+
initialErrorCovariance: 1,
|
|
773
|
+
processNoise: 3,
|
|
774
|
+
measurementNoise: 500,
|
|
775
|
+
panelOpen: false,
|
|
776
|
+
};
|
|
777
|
+
/**
|
|
778
|
+
* Global LBM Filter Parameters
|
|
779
|
+
*/
|
|
780
|
+
const FilterModel = { ...DefaultFilterModel };
|
|
781
|
+
/**
|
|
782
|
+
* Bitrate Monitor Static Class
|
|
783
|
+
*/
|
|
784
|
+
const BITRATE_MONITOR = new DataFlowMonitor(FilterModel.yellowFlag, FilterModel.redFlag, 20);
|
|
785
|
+
|
|
786
|
+
class VideoService extends SubService {
|
|
787
|
+
constructor() {
|
|
788
|
+
super();
|
|
789
|
+
this.latencyTestTimings = new LatencyTimings();
|
|
790
|
+
this.videoTrack$ = new Subject();
|
|
791
|
+
this.VideoEncoderQP = 0;
|
|
792
|
+
this.aggregatedStats = {};
|
|
793
|
+
this.kalmanFilter1D = new KalmanFilter1D(FilterModel.initialBitrateEstimate, FilterModel.initialErrorCovariance, FilterModel.processNoise, FilterModel.measurementNoise);
|
|
794
|
+
/**
|
|
795
|
+
* Aggregate video stats and emit it as videoStats$
|
|
796
|
+
*/
|
|
797
|
+
this.videoStats$ = this.videoTrack$.pipe(
|
|
798
|
+
// IMPORTANT! DO NOT CHANGE THOSE NUMBERS, LBM Stats are based on those values
|
|
799
|
+
switchMap(({ pcClient }) => interval(250).pipe(map(() => pcClient))), switchMap((pcClient) => from(this.getStats(pcClient))), filter(Truthy), share());
|
|
800
|
+
Signal.on('setKalmanParams').subscribe((data) => {
|
|
801
|
+
this.kalmanFilter1D.config(data);
|
|
802
|
+
BITRATE_MONITOR.config(data);
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
setContainer(container) {
|
|
806
|
+
this.container = container;
|
|
807
|
+
}
|
|
808
|
+
setLatencyTimings(latencyTimings) {
|
|
809
|
+
this.latencyTestTimings.SetUETimings(latencyTimings);
|
|
810
|
+
}
|
|
811
|
+
setEncoder(data) {
|
|
812
|
+
this.VideoEncoderQP = Number(new TextDecoder('utf-16').decode(data));
|
|
813
|
+
}
|
|
814
|
+
create() {
|
|
815
|
+
this.disconnect$.pipe(take(1)).subscribe(() => this.destroy());
|
|
816
|
+
this.destroy();
|
|
817
|
+
this.createWebRtcVideo();
|
|
818
|
+
this.createWebRtcAudio();
|
|
819
|
+
}
|
|
820
|
+
attachVideoStream(stream, pcClient) {
|
|
821
|
+
if (this.video) {
|
|
822
|
+
this.video.srcObject = stream;
|
|
823
|
+
this.videoTrack$.next({ stream, pcClient });
|
|
824
|
+
}
|
|
825
|
+
else {
|
|
826
|
+
console.error('Video element is not defined.');
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
attachAudioStream(stream) {
|
|
830
|
+
// do nothing the video has the same media stream as the audio track we have here (they are linked)
|
|
831
|
+
if (this.video?.srcObject === stream) {
|
|
832
|
+
return;
|
|
833
|
+
}
|
|
834
|
+
// video element has some other media stream that is not associated with this audio track
|
|
835
|
+
else if (this.audio &&
|
|
836
|
+
this.video?.srcObject &&
|
|
837
|
+
this.video?.srcObject !== stream) {
|
|
838
|
+
this.audio.srcObject = stream;
|
|
839
|
+
this.playAudio(this.audio);
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
play() {
|
|
843
|
+
void this.video?.play();
|
|
844
|
+
}
|
|
845
|
+
async getStats(pcClient) {
|
|
846
|
+
if (!pcClient) {
|
|
847
|
+
return null;
|
|
848
|
+
}
|
|
849
|
+
const stats = await pcClient.getStats(null);
|
|
850
|
+
return this.generateAggregatedStatsFunction(stats);
|
|
851
|
+
}
|
|
852
|
+
generateAggregatedStatsFunction(stats) {
|
|
853
|
+
const newStat = {};
|
|
854
|
+
// store each type of codec we can get stats on
|
|
855
|
+
newStat.codecs = {};
|
|
856
|
+
newStat.currentRoundTripTime = -1;
|
|
857
|
+
stats.forEach((stat) => {
|
|
858
|
+
// Get the inbound-rtp for video
|
|
859
|
+
if (stat.type === 'inbound-rtp' && stat.kind === 'video') {
|
|
860
|
+
Object.assign(newStat, stat);
|
|
861
|
+
newStat.bytesReceivedStart =
|
|
862
|
+
this.aggregatedStats && this.aggregatedStats.bytesReceivedStart
|
|
863
|
+
? this.aggregatedStats.bytesReceivedStart
|
|
864
|
+
: stat.bytesReceived;
|
|
865
|
+
newStat.framesDecodedStart =
|
|
866
|
+
this.aggregatedStats && this.aggregatedStats.framesDecodedStart
|
|
867
|
+
? this.aggregatedStats.framesDecodedStart
|
|
868
|
+
: stat.framesDecoded;
|
|
869
|
+
newStat.timestampStart =
|
|
870
|
+
this.aggregatedStats && this.aggregatedStats.timestampStart
|
|
871
|
+
? this.aggregatedStats.timestampStart
|
|
872
|
+
: stat.timestamp;
|
|
873
|
+
if (this.aggregatedStats && this.aggregatedStats.timestamp) {
|
|
874
|
+
// Get the mimetype of the video codec being used
|
|
875
|
+
if (stat.codecId &&
|
|
876
|
+
this.aggregatedStats.codecs &&
|
|
877
|
+
Object.hasOwn(this.aggregatedStats.codecs, stat.codecId)) {
|
|
878
|
+
newStat.videoCodec = this.aggregatedStats.codecs[stat.codecId];
|
|
879
|
+
}
|
|
880
|
+
if (this.aggregatedStats.bytesReceived) {
|
|
881
|
+
// bitrate = bits received since last time / number of ms since last time
|
|
882
|
+
// This is automatically in kbits (where k=1000) since time is in ms and stat we want is in seconds (so a '* 1000' then a '/ 1000' would negate each other)
|
|
883
|
+
newStat.bitrate = Math.floor((8 *
|
|
884
|
+
(newStat.bytesReceived - this.aggregatedStats.bytesReceived)) /
|
|
885
|
+
(newStat.timestamp - this.aggregatedStats.timestamp));
|
|
886
|
+
}
|
|
887
|
+
if (this.aggregatedStats.bytesReceivedStart) {
|
|
888
|
+
newStat.avgBitrate = Math.floor((8 *
|
|
889
|
+
(newStat?.bytesReceived -
|
|
890
|
+
this.aggregatedStats.bytesReceivedStart)) /
|
|
891
|
+
(newStat.timestamp - this.aggregatedStats.timestampStart));
|
|
892
|
+
}
|
|
893
|
+
if (this.aggregatedStats.framesDecodedStart) {
|
|
894
|
+
newStat.avgFrameRate = Math.floor((newStat.framesDecoded -
|
|
895
|
+
this.aggregatedStats.framesDecodedStart) /
|
|
896
|
+
((newStat.timestamp - this.aggregatedStats.timestampStart) /
|
|
897
|
+
1000));
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
if (stat.type === 'candidate-pair' &&
|
|
902
|
+
Object.hasOwn(stat, 'currentRoundTripTime')) {
|
|
903
|
+
newStat.currentRoundTripTime = stat.currentRoundTripTime ?? 0;
|
|
904
|
+
}
|
|
905
|
+
// Store mimetype of each codec
|
|
906
|
+
if (Object.hasOwn(newStat, 'codecs') &&
|
|
907
|
+
stat.type === 'codec' &&
|
|
908
|
+
stat.mimeType &&
|
|
909
|
+
stat.id) {
|
|
910
|
+
const codecId = stat.id;
|
|
911
|
+
newStat.codecs[codecId] = stat.mimeType
|
|
912
|
+
.replace('video/', '')
|
|
913
|
+
.replace('audio/', '');
|
|
914
|
+
}
|
|
915
|
+
});
|
|
916
|
+
newStat.pixelRatio = +window.devicePixelRatio.toFixed(2);
|
|
917
|
+
if (this.aggregatedStats.receiveToCompositeMs) {
|
|
918
|
+
newStat.receiveToCompositeMs = this.aggregatedStats.receiveToCompositeMs;
|
|
919
|
+
this.latencyTestTimings.SetFrameDisplayDeltaTime(this.aggregatedStats.receiveToCompositeMs);
|
|
920
|
+
}
|
|
921
|
+
// Calculate duration of run
|
|
922
|
+
newStat.runTime = Math.floor((newStat.timestamp - newStat.timestampStart) / 1000);
|
|
923
|
+
newStat.kalmanBitrate = Math.floor(this.kalmanFilter1D.update(newStat.bitrate || FilterModel.initialBitrateEstimate));
|
|
924
|
+
newStat.dataFlowCheckResult = BITRATE_MONITOR.addValue(newStat.kalmanBitrate);
|
|
925
|
+
newStat.bitrateDrop = Math.floor(newStat.dataFlowCheckResult.dropPercentage);
|
|
926
|
+
this.aggregatedStats = newStat;
|
|
927
|
+
return this.onAggregatedStats(newStat);
|
|
928
|
+
}
|
|
929
|
+
onAggregatedStats(aggregatedStats) {
|
|
930
|
+
return {
|
|
931
|
+
quality: mapQpToQuality(this.VideoEncoderQP),
|
|
932
|
+
aggregatedStats: {
|
|
933
|
+
...aggregatedStats,
|
|
934
|
+
VideoEncoderQP: this.VideoEncoderQP,
|
|
935
|
+
},
|
|
936
|
+
};
|
|
937
|
+
}
|
|
938
|
+
destroy() {
|
|
939
|
+
this.video = null;
|
|
940
|
+
this.audio = null;
|
|
941
|
+
while (this.container?.firstChild) {
|
|
942
|
+
this.container.removeChild(this.container.firstChild);
|
|
943
|
+
}
|
|
944
|
+
this.store.dispatch(changeStatusMainVideoOnScene({ isVideoPlaying: false }));
|
|
945
|
+
}
|
|
946
|
+
createWebRtcVideo() {
|
|
947
|
+
dispatchResize();
|
|
948
|
+
const video = document.createElement('video');
|
|
949
|
+
video.id = STREAMING_VIDEO_ID;
|
|
950
|
+
video.playsInline = true;
|
|
951
|
+
video.muted = true;
|
|
952
|
+
video.autoplay = true;
|
|
953
|
+
video.preload = 'auto';
|
|
954
|
+
video.disablePictureInPicture = true;
|
|
955
|
+
this.video = video;
|
|
956
|
+
this.container?.appendChild(this.video);
|
|
957
|
+
this.video.addEventListener('play', () => {
|
|
958
|
+
this.store.dispatch(changeStatusMainVideoOnScene({ isVideoPlaying: true }));
|
|
959
|
+
});
|
|
960
|
+
fromEvent(this.video, 'loadedmetadata')
|
|
961
|
+
.pipe(first(), filter((data) => this.video === data.target), tapLog('VideoService loadedmetadata:'))
|
|
962
|
+
.subscribe(() => sendSignal(UnrealInternalSignalEvents.OnVideoInitialized, this.video));
|
|
963
|
+
}
|
|
964
|
+
createWebRtcAudio() {
|
|
965
|
+
const audio = new Audio();
|
|
966
|
+
audio.id = 'streamingAudio';
|
|
967
|
+
this.audio = audio;
|
|
968
|
+
}
|
|
969
|
+
playAudio(audio) {
|
|
970
|
+
from(audio.play())
|
|
971
|
+
.pipe(catchError(() => fromEvent(document, 'click')), take(1))
|
|
972
|
+
.subscribe({
|
|
973
|
+
next: () => audio.play(),
|
|
974
|
+
error: () => ({}),
|
|
975
|
+
});
|
|
976
|
+
}
|
|
977
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.0", ngImport: i0, type: VideoService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
978
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.1.0", ngImport: i0, type: VideoService }); }
|
|
979
|
+
}
|
|
980
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.0", ngImport: i0, type: VideoService, decorators: [{
|
|
981
|
+
type: Injectable
|
|
982
|
+
}], ctorParameters: () => [] });
|
|
983
|
+
|
|
681
984
|
class CommandTelemetryService {
|
|
682
985
|
constructor() {
|
|
683
986
|
this.appId = 'metabox-telemetry';
|
|
@@ -947,7 +1250,7 @@ class SignallingService extends SubService {
|
|
|
947
1250
|
.pipe(tapLog('MatchMakerUrls changed:')),
|
|
948
1251
|
this.store.select(selectIsAutostart).pipe(tapLog('Autostart is:')),
|
|
949
1252
|
])
|
|
950
|
-
.pipe(filter(([, autoStart]) => autoStart), map(([matchMakerUrls]) => [...matchMakerUrls].filter(Truthy)), filter((urls) => urls.length > 0))
|
|
1253
|
+
.pipe(filter(([, autoStart]) => autoStart), map$1(([matchMakerUrls]) => [...matchMakerUrls].filter(Truthy)), filter((urls) => urls.length > 0))
|
|
951
1254
|
.subscribe((urls) => this.connectToSignaling(urls));
|
|
952
1255
|
this.store
|
|
953
1256
|
.select(selectWsUrl)
|
|
@@ -1007,7 +1310,7 @@ class SignallingService extends SubService {
|
|
|
1007
1310
|
if (!clientId || !viewId) {
|
|
1008
1311
|
console.error('Client ID or View ID is not set');
|
|
1009
1312
|
}
|
|
1010
|
-
}), filter(([clientId, viewId]) => !!clientId && !!viewId), tapLog('Stream Id =>'), switchMap(([clientId, viewId]) => this.httpClient.get(`${signalingUrl}${clientId}/${viewId}`)), timeout(4000), filter((data) => {
|
|
1313
|
+
}), filter(([clientId, viewId]) => !!clientId && !!viewId), tapLog('Stream Id =>'), switchMap$1(([clientId, viewId]) => this.httpClient.get(`${signalingUrl}${clientId}/${viewId}`)), timeout(4000), filter((data) => {
|
|
1011
1314
|
TelemetryStop('getSignaling', { ...data, multi: true });
|
|
1012
1315
|
if (data.signallingServer === '' || data.error) {
|
|
1013
1316
|
this.showStatusMessage(data?.mm_message || 'Server not found');
|
|
@@ -1154,106 +1457,65 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.0", ngImpor
|
|
|
1154
1457
|
type: Injectable
|
|
1155
1458
|
}], ctorParameters: () => [] });
|
|
1156
1459
|
|
|
1157
|
-
class VideoService extends SubService {
|
|
1158
|
-
create() {
|
|
1159
|
-
this.disconnect$.pipe(take(1)).subscribe(() => this.destroy());
|
|
1160
|
-
this.destroy();
|
|
1161
|
-
this.createWebRtcVideo();
|
|
1162
|
-
this.createWebRtcAudio();
|
|
1163
|
-
}
|
|
1164
|
-
destroy() {
|
|
1165
|
-
this.video = null;
|
|
1166
|
-
this.audio = null;
|
|
1167
|
-
while (this.container?.firstChild) {
|
|
1168
|
-
this.container.removeChild(this.container.firstChild);
|
|
1169
|
-
}
|
|
1170
|
-
this.store.dispatch(changeStatusMainVideoOnScene({ isVideoPlaying: false }));
|
|
1171
|
-
}
|
|
1172
|
-
createWebRtcVideo() {
|
|
1173
|
-
dispatchResize();
|
|
1174
|
-
const video = document.createElement('video');
|
|
1175
|
-
video.id = STREAMING_VIDEO_ID;
|
|
1176
|
-
video.playsInline = true;
|
|
1177
|
-
video.muted = true;
|
|
1178
|
-
video.autoplay = true;
|
|
1179
|
-
video.preload = 'auto';
|
|
1180
|
-
video.disablePictureInPicture = true;
|
|
1181
|
-
this.video = video;
|
|
1182
|
-
this.container?.appendChild(this.video);
|
|
1183
|
-
this.video.addEventListener('play', () => {
|
|
1184
|
-
this.store.dispatch(changeStatusMainVideoOnScene({ isVideoPlaying: true }));
|
|
1185
|
-
});
|
|
1186
|
-
fromEvent(this.video, 'loadedmetadata')
|
|
1187
|
-
.pipe(first(), filter((data) => this.video === data.target), tapLog('VideoService loadedmetadata:'))
|
|
1188
|
-
.subscribe(() => sendSignal(UnrealInternalSignalEvents.OnVideoInitialized, this.video));
|
|
1189
|
-
}
|
|
1190
|
-
attachVideoStream(stream) {
|
|
1191
|
-
if (this.video) {
|
|
1192
|
-
this.video.srcObject = stream;
|
|
1193
|
-
}
|
|
1194
|
-
else {
|
|
1195
|
-
console.error('Video element is not defined.');
|
|
1196
|
-
}
|
|
1197
|
-
}
|
|
1198
|
-
attachAudioStream(stream) {
|
|
1199
|
-
// do nothing the video has the same media stream as the audio track we have here (they are linked)
|
|
1200
|
-
if (this.video?.srcObject === stream) {
|
|
1201
|
-
return;
|
|
1202
|
-
}
|
|
1203
|
-
// video element has some other media stream that is not associated with this audio track
|
|
1204
|
-
else if (this.audio &&
|
|
1205
|
-
this.video?.srcObject &&
|
|
1206
|
-
this.video?.srcObject !== stream) {
|
|
1207
|
-
this.audio.srcObject = stream;
|
|
1208
|
-
this.playAudio(this.audio);
|
|
1209
|
-
}
|
|
1210
|
-
}
|
|
1211
|
-
createWebRtcAudio() {
|
|
1212
|
-
const audio = new Audio();
|
|
1213
|
-
audio.id = 'streamingAudio';
|
|
1214
|
-
this.audio = audio;
|
|
1215
|
-
}
|
|
1216
|
-
playAudio(audio) {
|
|
1217
|
-
from(audio.play())
|
|
1218
|
-
.pipe(catchError(() => fromEvent(document, 'click')), take(1))
|
|
1219
|
-
.subscribe({
|
|
1220
|
-
next: () => audio.play(),
|
|
1221
|
-
error: () => ({}),
|
|
1222
|
-
});
|
|
1223
|
-
}
|
|
1224
|
-
play() {
|
|
1225
|
-
void this.video?.play();
|
|
1226
|
-
}
|
|
1227
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.0", ngImport: i0, type: VideoService, deps: null, target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
1228
|
-
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.1.0", ngImport: i0, type: VideoService }); }
|
|
1229
|
-
}
|
|
1230
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.0", ngImport: i0, type: VideoService, decorators: [{
|
|
1231
|
-
type: Injectable
|
|
1232
|
-
}] });
|
|
1233
|
-
|
|
1234
1460
|
class WebRtcPlayerService extends SubService {
|
|
1235
1461
|
constructor() {
|
|
1236
|
-
super(
|
|
1237
|
-
this.onAudioTrack$ = new Subject();
|
|
1238
|
-
this.onVideoTrack$ = new Subject();
|
|
1462
|
+
super();
|
|
1239
1463
|
this.onDataChannelMessage$ = new Subject();
|
|
1240
|
-
this.
|
|
1241
|
-
this.
|
|
1464
|
+
this.signallingSrv = inject(SignallingService);
|
|
1465
|
+
this.videoService = inject(VideoService);
|
|
1242
1466
|
this.cfg = {};
|
|
1243
1467
|
this.useMic = false;
|
|
1244
1468
|
this.forceTURN = false;
|
|
1245
|
-
this.preferSFU = false;
|
|
1246
|
-
this.autoPlayAudio = true;
|
|
1247
|
-
this.startVideoMuted = true;
|
|
1248
1469
|
this.forceMonoAudio = false;
|
|
1249
|
-
|
|
1470
|
+
/**
|
|
1471
|
+
* Never called for now
|
|
1472
|
+
*/
|
|
1473
|
+
this.destroy$ = new Subject();
|
|
1474
|
+
this.listenSignaling();
|
|
1475
|
+
}
|
|
1476
|
+
listenSignaling() {
|
|
1477
|
+
const iceCandidateBuffer = [];
|
|
1478
|
+
const processBuffer = () => {
|
|
1479
|
+
iceCandidateBuffer.forEach((cnd) => {
|
|
1480
|
+
this.handleCandidateFromServer(cnd);
|
|
1481
|
+
});
|
|
1482
|
+
iceCandidateBuffer.length = 0;
|
|
1483
|
+
};
|
|
1484
|
+
this.signallingSrv.onConfig$
|
|
1485
|
+
.pipe(takeUntil(this.destroy$))
|
|
1486
|
+
.subscribe((config) => {
|
|
1487
|
+
this.videoService.create();
|
|
1488
|
+
this.setConfig(config);
|
|
1489
|
+
});
|
|
1490
|
+
this.signallingSrv.onWebRtcAnswer$
|
|
1491
|
+
.pipe(filter(Truthy), takeUntil(this.destroy$))
|
|
1492
|
+
.subscribe((answer) => {
|
|
1493
|
+
this.receiveAnswer(answer);
|
|
1494
|
+
processBuffer();
|
|
1495
|
+
});
|
|
1496
|
+
this.signallingSrv.onOffer$
|
|
1497
|
+
.pipe(filter(Truthy), takeUntil(this.destroy$))
|
|
1498
|
+
.subscribe(() => {
|
|
1499
|
+
void this.createWebRtcOffer();
|
|
1500
|
+
});
|
|
1501
|
+
this.signallingSrv.onWebRtcIce$
|
|
1502
|
+
.pipe(withLatestFrom(this.signallingSrv.onWebRtcAnswer$), takeUntil(this.destroy$))
|
|
1503
|
+
.subscribe(([candidate, answerEstablished]) => {
|
|
1504
|
+
TelemetryStop('iceCandidate', {
|
|
1505
|
+
multi: true,
|
|
1506
|
+
});
|
|
1507
|
+
iceCandidateBuffer.push(candidate);
|
|
1508
|
+
if (answerEstablished) {
|
|
1509
|
+
processBuffer();
|
|
1510
|
+
}
|
|
1511
|
+
});
|
|
1250
1512
|
}
|
|
1251
1513
|
async createWebRtcOffer() {
|
|
1252
1514
|
Logger.info('Creating offer');
|
|
1253
1515
|
let offer;
|
|
1254
1516
|
try {
|
|
1255
1517
|
clearTimeout(this.webRtcErrorTimeout);
|
|
1256
|
-
offer = await this.
|
|
1518
|
+
offer = await this.createPeerConnection(this.cfg);
|
|
1257
1519
|
this.webRtcErrorTimeout = setTimeout(() => {
|
|
1258
1520
|
if (this.pcClient?.connectionState?.match(/connecting|failed/gi)) {
|
|
1259
1521
|
this.dispatchWebRtcError('WebRTCError: Timeout');
|
|
@@ -1337,55 +1599,20 @@ class WebRtcPlayerService extends SubService {
|
|
|
1337
1599
|
//**********************
|
|
1338
1600
|
//Config setup
|
|
1339
1601
|
//**********************
|
|
1340
|
-
this.cfg =
|
|
1341
|
-
typeof parOptions.peerConnectionOptions !== 'undefined'
|
|
1342
|
-
? parOptions.peerConnectionOptions
|
|
1343
|
-
: {};
|
|
1344
|
-
//this.cfg.sdpSemantics = 'unified-plan';
|
|
1345
|
-
// If this is true in Chrome 89+ SDP is sent that is incompatible with UE Pixel Streaming 4.26 and below.
|
|
1346
|
-
// However 4.27 Pixel Streaming does not need this set to false as it supports `offerExtmapAllowMixed`.
|
|
1347
|
-
// tdlr; uncomment this line for older versions of Pixel Streaming that need Chrome 89+.
|
|
1348
|
-
//this.cfg.offerExtmapAllowMixed = false;
|
|
1602
|
+
this.cfg = { ...parOptions.peerConnectionOptions };
|
|
1349
1603
|
this.forceTURN = urlParams.has('ForceTURN');
|
|
1350
1604
|
if (this.forceTURN) {
|
|
1351
1605
|
Logger.info('Forcing TURN usage by setting ICE Transport Policy in peer connection config.');
|
|
1352
1606
|
this.cfg.iceTransportPolicy = 'relay';
|
|
1353
1607
|
}
|
|
1354
|
-
this.cfg.bundlePolicy = '
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
this.cfg.bundlePolicy = 'max-bundle';
|
|
1358
|
-
}
|
|
1608
|
+
this.cfg.bundlePolicy = urlParams.has('ForceMaxBundle')
|
|
1609
|
+
? 'max-bundle'
|
|
1610
|
+
: 'balanced';
|
|
1359
1611
|
//**********************
|
|
1360
1612
|
//Variables
|
|
1361
1613
|
//**********************
|
|
1362
1614
|
this.pcClient = null;
|
|
1363
1615
|
this.dcClient = null;
|
|
1364
|
-
this.sdpConstraints = {
|
|
1365
|
-
offerToReceiveAudio: false, //Note: if you don't need audio you can get improved latency by turning this off.
|
|
1366
|
-
offerToReceiveVideo: true,
|
|
1367
|
-
};
|
|
1368
|
-
// See https://www.w3.org/TR/webrtc/#dom-rtcdatachannelinit for values (this is needed for Firefox to be consistent with Chrome.)
|
|
1369
|
-
this.dataChannelOptions = { ordered: true };
|
|
1370
|
-
// This is useful if the video/audio needs to autoplay (without user input) as browsers do not allow autoplay non-muted of sound sources without user interaction.
|
|
1371
|
-
this.startVideoMuted =
|
|
1372
|
-
typeof parOptions?.['startVideoMuted'] !== 'undefined'
|
|
1373
|
-
? !!parOptions?.['startVideoMuted']
|
|
1374
|
-
: false;
|
|
1375
|
-
this.autoPlayAudio =
|
|
1376
|
-
typeof parOptions?.['autoPlayAudio'] !== 'undefined'
|
|
1377
|
-
? !!parOptions?.['autoPlayAudio']
|
|
1378
|
-
: true;
|
|
1379
|
-
// To force mono playback of WebRTC audio
|
|
1380
|
-
this.forceMonoAudio = urlParams.has('ForceMonoAudio');
|
|
1381
|
-
if (this.forceMonoAudio) {
|
|
1382
|
-
Logger.info('Will attempt to force mono audio by munging the sdp in the browser.');
|
|
1383
|
-
}
|
|
1384
|
-
// To enable mic in browser use SSL/localhost and have ?useMic in the query string.
|
|
1385
|
-
this.useMic = urlParams.has('useMic');
|
|
1386
|
-
if (!this.useMic) {
|
|
1387
|
-
Logger.info('Microphone access is not enabled. Pass ?useMic in the url to enable it.');
|
|
1388
|
-
}
|
|
1389
1616
|
// When ?useMic check for SSL or localhost
|
|
1390
1617
|
const isLocalhostConnection = location.hostname === 'localhost' || location.hostname === '127.0.0.1';
|
|
1391
1618
|
const isHttpsConnection = location.protocol === 'https:';
|
|
@@ -1394,11 +1621,6 @@ class WebRtcPlayerService extends SubService {
|
|
|
1394
1621
|
Logger.error('Microphone access in the browser will not work if you are not on HTTPS or localhost. Disabling mic access.');
|
|
1395
1622
|
Logger.error("For testing you can enable HTTP microphone access Chrome by visiting chrome://flags/ and enabling 'unsafely-treat-insecure-origin-as-secure'");
|
|
1396
1623
|
}
|
|
1397
|
-
// Prefer SFU or P2P connection
|
|
1398
|
-
this.preferSFU = urlParams.has('preferSFU');
|
|
1399
|
-
Logger.info(this.preferSFU
|
|
1400
|
-
? 'The browser will signal it would prefer an SFU connection. Remove ?preferSFU from the url to signal for P2P usage.'
|
|
1401
|
-
: 'The browser will signal for a P2P connection. Pass ?preferSFU in the url to signal for SFU usage.');
|
|
1402
1624
|
}
|
|
1403
1625
|
dispatchWebRtcError(message) {
|
|
1404
1626
|
Logger.error(message);
|
|
@@ -1407,409 +1629,188 @@ class WebRtcPlayerService extends SubService {
|
|
|
1407
1629
|
message,
|
|
1408
1630
|
}));
|
|
1409
1631
|
}
|
|
1410
|
-
async
|
|
1632
|
+
async createPeerConnection(config) {
|
|
1411
1633
|
this.closePC();
|
|
1412
1634
|
this.pcClient = new RTCPeerConnection(config);
|
|
1413
1635
|
TelemetryStart('Browser ICE gathering');
|
|
1414
1636
|
TelemetryStart('Browser ICE connection');
|
|
1415
1637
|
TelemetryStart('Signaling state change');
|
|
1416
|
-
|
|
1417
|
-
|
|
1638
|
+
this.handlePeerConnectionEvents(this.pcClient);
|
|
1639
|
+
await this.setupTransceiversAsync(this.pcClient);
|
|
1640
|
+
this.dcClient = this.createDataChannel(this.pcClient, 'cirrus', {
|
|
1641
|
+
// See https://www.w3.org/TR/webrtc/#dom-rtcdatachannelinit for values (this is needed for Firefox to be consistent with Chrome.)
|
|
1642
|
+
ordered: true,
|
|
1643
|
+
});
|
|
1644
|
+
this.disconnect$.pipe(first()).subscribe(() => {
|
|
1645
|
+
this.closePC();
|
|
1646
|
+
});
|
|
1647
|
+
await this.createOffer(this.pcClient);
|
|
1648
|
+
}
|
|
1649
|
+
handlePeerConnectionEvents(pcClient) {
|
|
1650
|
+
fromEvent(pcClient, 'signalingstatechange')
|
|
1651
|
+
.pipe(map$1(() => this.pcClient?.signalingState), takeUntil(this.disconnect$))
|
|
1418
1652
|
.subscribe((state) => {
|
|
1419
1653
|
Logger.info('[Signaling state change] |', state, '|');
|
|
1420
1654
|
TelemetryStop('Signaling state change', { state, multi: true });
|
|
1421
1655
|
});
|
|
1422
|
-
fromEvent(
|
|
1423
|
-
.pipe(map(() => this.pcClient?.iceConnectionState), takeUntil(this.disconnect$))
|
|
1656
|
+
fromEvent(pcClient, 'iceconnectionstatechange')
|
|
1657
|
+
.pipe(map$1(() => this.pcClient?.iceConnectionState), takeUntil(this.disconnect$))
|
|
1424
1658
|
.subscribe((state) => {
|
|
1425
1659
|
Logger.info('[Browser ICE connection] |', state, '|');
|
|
1426
1660
|
TelemetryStop('Browser ICE connection', { state, multi: true });
|
|
1427
1661
|
});
|
|
1428
|
-
fromEvent(
|
|
1429
|
-
.pipe(map(() => this.pcClient?.iceGatheringState), takeUntil(this.disconnect$))
|
|
1662
|
+
fromEvent(pcClient, 'icegatheringstatechange')
|
|
1663
|
+
.pipe(map$1(() => this.pcClient?.iceGatheringState), takeUntil(this.disconnect$))
|
|
1430
1664
|
.subscribe((state) => {
|
|
1431
1665
|
Logger.info('[Browser ICE gathering] |', state, '|');
|
|
1432
1666
|
TelemetryStop('Browser ICE gathering', { state, multi: true });
|
|
1433
1667
|
});
|
|
1434
|
-
fromEvent(
|
|
1435
|
-
.pipe(map(() => this.pcClient?.connectionState), takeUntil(this.disconnect$))
|
|
1668
|
+
fromEvent(pcClient, 'connectionstatechange')
|
|
1669
|
+
.pipe(map$1(() => this.pcClient?.connectionState), takeUntil(this.disconnect$))
|
|
1436
1670
|
.subscribe((state) => {
|
|
1437
1671
|
Logger.info('[RTCPeerConnection state] |', state, '|');
|
|
1438
1672
|
});
|
|
1439
|
-
fromEvent(
|
|
1440
|
-
.pipe(map((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$))
|
|
1441
|
-
.subscribe((
|
|
1442
|
-
fromEvent(
|
|
1443
|
-
.pipe(tap$1(() => Logger.info('Data channel created for us by browser as we are a receiving peer.')), map((e) => e.channel), takeUntil(this.disconnect$))
|
|
1673
|
+
fromEvent(pcClient, 'icecandidate')
|
|
1674
|
+
.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$))
|
|
1675
|
+
.subscribe((candidate) => this.signallingSrv.send(candidate));
|
|
1676
|
+
fromEvent(pcClient, 'datachannel')
|
|
1677
|
+
.pipe(tap$1(() => Logger.info('Data channel created for us by browser as we are a receiving peer.')), map$1((e) => e.channel), takeUntil(this.disconnect$))
|
|
1444
1678
|
.subscribe((datachannel) => {
|
|
1445
1679
|
this.dcClient = this.setupDataChannelCallbacks(datachannel);
|
|
1446
1680
|
});
|
|
1447
|
-
const onTrack$ = fromEvent(
|
|
1681
|
+
const onTrack$ = fromEvent(pcClient, 'track').pipe(filter((e) => !!e.track), tap$1((e) => Logger.info('Got track - ' +
|
|
1448
1682
|
e.track.kind +
|
|
1449
1683
|
' id=' +
|
|
1450
1684
|
e.track.id +
|
|
1451
1685
|
' readyState=' +
|
|
1452
|
-
e.track.readyState)), share());
|
|
1453
|
-
onTrack$
|
|
1454
|
-
.pipe(filter((e) => e.track.kind === 'audio'), map((e) => e.streams[0]), takeUntil(this.disconnect$))
|
|
1455
|
-
.subscribe((
|
|
1456
|
-
onTrack$
|
|
1457
|
-
|
|
1458
|
-
.
|
|
1459
|
-
|
|
1460
|
-
this.dcClient = this.createDataChannel(this.pcClient, 'cirrus', this.dataChannelOptions);
|
|
1461
|
-
this.disconnect$.pipe(first()).subscribe(() => {
|
|
1462
|
-
this.closePC();
|
|
1463
|
-
});
|
|
1464
|
-
await this.handleCreateOffer();
|
|
1465
|
-
}
|
|
1466
|
-
async setupTransceiversAsync(pc) {
|
|
1467
|
-
const hasTransceivers = pc.getTransceivers().length > 0;
|
|
1468
|
-
// Setup a transceiver for getting UE video
|
|
1469
|
-
pc.addTransceiver('video', { direction: 'recvonly' });
|
|
1470
|
-
// Setup a transceiver for sending mic audio to UE and receiving audio from UE
|
|
1471
|
-
if (!this.useMic) {
|
|
1472
|
-
pc.addTransceiver('audio', { direction: 'recvonly' });
|
|
1473
|
-
}
|
|
1474
|
-
else {
|
|
1475
|
-
const audioSendOptions = this.useMic
|
|
1476
|
-
? {
|
|
1477
|
-
autoGainControl: false,
|
|
1478
|
-
channelCount: 1,
|
|
1479
|
-
echoCancellation: false,
|
|
1480
|
-
latency: 0,
|
|
1481
|
-
noiseSuppression: false,
|
|
1482
|
-
sampleRate: 48000,
|
|
1483
|
-
sampleSize: 16,
|
|
1484
|
-
volume: 1.0,
|
|
1485
|
-
}
|
|
1486
|
-
: false;
|
|
1487
|
-
// Note using mic on android chrome requires SSL or chrome://flags/ "unsafely-treat-insecure-origin-as-secure"
|
|
1488
|
-
const stream = await navigator.mediaDevices.getUserMedia({
|
|
1489
|
-
video: false,
|
|
1490
|
-
audio: audioSendOptions,
|
|
1491
|
-
});
|
|
1492
|
-
if (stream) {
|
|
1493
|
-
if (hasTransceivers) {
|
|
1494
|
-
for (const transceiver of pc.getTransceivers()) {
|
|
1495
|
-
if (transceiver &&
|
|
1496
|
-
transceiver.receiver &&
|
|
1497
|
-
transceiver.receiver.track &&
|
|
1498
|
-
transceiver.receiver.track.kind === 'audio') {
|
|
1499
|
-
for (const track of stream.getTracks()) {
|
|
1500
|
-
if (track.kind && track.kind == 'audio') {
|
|
1501
|
-
transceiver.sender.replaceTrack(track);
|
|
1502
|
-
transceiver.direction = 'sendrecv';
|
|
1503
|
-
}
|
|
1504
|
-
}
|
|
1505
|
-
}
|
|
1506
|
-
}
|
|
1507
|
-
}
|
|
1508
|
-
else {
|
|
1509
|
-
for (const track of stream.getTracks()) {
|
|
1510
|
-
if (track.kind && track.kind == 'audio') {
|
|
1511
|
-
pc.addTransceiver(track, { direction: 'sendrecv' });
|
|
1512
|
-
}
|
|
1513
|
-
}
|
|
1514
|
-
}
|
|
1515
|
-
}
|
|
1516
|
-
else {
|
|
1517
|
-
pc.addTransceiver('audio', { direction: 'recvonly' });
|
|
1518
|
-
}
|
|
1519
|
-
}
|
|
1520
|
-
}
|
|
1521
|
-
setupDataChannelCallbacks(datachannel) {
|
|
1522
|
-
try {
|
|
1523
|
-
TelemetryStart('DataChannel Connection');
|
|
1524
|
-
// Inform browser we would like binary data as an ArrayBuffer (FF chooses Blob by default!)
|
|
1525
|
-
datachannel.binaryType = 'arraybuffer';
|
|
1526
|
-
fromEvent(datachannel, 'close')
|
|
1527
|
-
.pipe(first())
|
|
1528
|
-
.subscribe((e) => {
|
|
1529
|
-
Logger.warn(`[DATACHANNEL] Data channel disconnected: ${datachannel.label}(${datachannel.id})`, e);
|
|
1530
|
-
this.store.dispatch(destroyRemoteConnections({
|
|
1531
|
-
disconnectReason: DisconnectReason.dataChannelClosed,
|
|
1532
|
-
}));
|
|
1533
|
-
});
|
|
1534
|
-
fromEvent(datachannel, 'error')
|
|
1535
|
-
.pipe(takeUntil(this.disconnect$))
|
|
1536
|
-
.subscribe((e) => {
|
|
1537
|
-
const message = `[DATACHANNEL] Data channel error: ${datachannel.label}(${datachannel.id})`;
|
|
1538
|
-
Logger.error(message, e);
|
|
1539
|
-
});
|
|
1540
|
-
fromEvent(datachannel, 'open')
|
|
1541
|
-
.pipe(tap$1(() => {
|
|
1542
|
-
Logger.info(`[DATACHANNEL] Data channel connected: ${datachannel.label}(${datachannel.id})`);
|
|
1543
|
-
}), takeUntil(this.disconnect$))
|
|
1544
|
-
.subscribe(() => {
|
|
1545
|
-
TelemetryStop('DataChannel Connection');
|
|
1546
|
-
this.onDataChannelConnected();
|
|
1547
|
-
});
|
|
1548
|
-
fromEvent(datachannel, 'message')
|
|
1549
|
-
.pipe(map((e) => e.data), takeUntil(this.disconnect$))
|
|
1550
|
-
.subscribe((data) => this.onDataChannelMessage$.next(data));
|
|
1551
|
-
return datachannel;
|
|
1552
|
-
}
|
|
1553
|
-
catch (e) {
|
|
1554
|
-
Logger.warn('[DATACHANNEL] Datachannel setup caused an exception: ', e);
|
|
1555
|
-
return null;
|
|
1556
|
-
}
|
|
1557
|
-
}
|
|
1558
|
-
onDataChannelConnected() {
|
|
1559
|
-
let text = 'Session connected, video stream starting.';
|
|
1560
|
-
if (navigator.userAgent.includes('Firefox')) {
|
|
1561
|
-
text +=
|
|
1562
|
-
'\n If you are experiencing connection problems please try Google Chrome';
|
|
1563
|
-
}
|
|
1564
|
-
this.store.dispatch(setStatusMessage({ message: text }));
|
|
1565
|
-
this.store.dispatch(setDataChannelConnected({ value: true }));
|
|
1566
|
-
}
|
|
1567
|
-
async handleCreateOffer() {
|
|
1568
|
-
try {
|
|
1569
|
-
const offer = await this.pcClient.createOffer(this.sdpConstraints);
|
|
1570
|
-
this.mungeSDP(offer);
|
|
1571
|
-
this.pcClient?.setLocalDescription(offer);
|
|
1572
|
-
this.onWebRtcOffer$.next(offer);
|
|
1573
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
1574
|
-
}
|
|
1575
|
-
catch (error) {
|
|
1576
|
-
Logger.error("Couldn't create offer");
|
|
1577
|
-
}
|
|
1578
|
-
}
|
|
1579
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.0", ngImport: i0, type: WebRtcPlayerService, deps: null, target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
1580
|
-
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.1.0", ngImport: i0, type: WebRtcPlayerService }); }
|
|
1581
|
-
}
|
|
1582
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.0", ngImport: i0, type: WebRtcPlayerService, decorators: [{
|
|
1583
|
-
type: Injectable
|
|
1584
|
-
}] });
|
|
1585
|
-
|
|
1586
|
-
class DataFlowMonitor {
|
|
1587
|
-
/**
|
|
1588
|
-
* Initializes the DataFlowMonitor monitor.
|
|
1589
|
-
* @param yellowFlagThresholdPercentage - The percentage drop to trigger a YELLOW warning (default: 15%).
|
|
1590
|
-
* @param redFlagThresholdPercentage - The percentage drop to trigger a RED warning (default: 30%).
|
|
1591
|
-
* @param historyBufferLength - buffer length (default: 100).
|
|
1592
|
-
* @param splitPoint - The point at which to split the history buffer into two halves (default: 0.5).
|
|
1593
|
-
*/
|
|
1594
|
-
constructor(yellowFlagThresholdPercentage = 15, redFlagThresholdPercentage = 30, historyBufferLength = 100, splitPoint = 0.5) {
|
|
1595
|
-
this.yellowFlagThresholdPercentage = yellowFlagThresholdPercentage;
|
|
1596
|
-
this.redFlagThresholdPercentage = redFlagThresholdPercentage;
|
|
1597
|
-
this.historyBufferLength = historyBufferLength;
|
|
1598
|
-
this.splitPoint = splitPoint;
|
|
1599
|
-
this.dataHistory = [];
|
|
1600
|
-
}
|
|
1601
|
-
reset() {
|
|
1602
|
-
this.dataHistory.length = 0;
|
|
1603
|
-
}
|
|
1604
|
-
config(data) {
|
|
1605
|
-
this.reset();
|
|
1606
|
-
this.yellowFlagThresholdPercentage = data.yellowFlag;
|
|
1607
|
-
this.redFlagThresholdPercentage = data.redFlag;
|
|
1608
|
-
}
|
|
1609
|
-
/**
|
|
1610
|
-
* Adds a new bitrate measurement and checks for significant drops.
|
|
1611
|
-
* @param currentValue - The current bitrate in kbps.
|
|
1612
|
-
* @returns BitrateCheckResult indicating if a drop was detected.
|
|
1613
|
-
*/
|
|
1614
|
-
addValue(currentValue) {
|
|
1615
|
-
if (isNaN(currentValue) || currentValue < 1) {
|
|
1616
|
-
return {};
|
|
1617
|
-
}
|
|
1618
|
-
const historyLength = this.dataHistory.length;
|
|
1619
|
-
if (historyLength > this.historyBufferLength) {
|
|
1620
|
-
this.dataHistory.shift();
|
|
1621
|
-
}
|
|
1622
|
-
this.dataHistory.push(currentValue);
|
|
1623
|
-
if (historyLength < this.historyBufferLength) {
|
|
1624
|
-
this.dataHistory.length = this.historyBufferLength;
|
|
1625
|
-
this.dataHistory.fill(currentValue, 0, this.historyBufferLength);
|
|
1626
|
-
}
|
|
1627
|
-
const splitIndex = Math.floor(historyLength * this.splitPoint);
|
|
1628
|
-
const firstHalf = this.dataHistory.slice(0, splitIndex);
|
|
1629
|
-
const secondHalf = this.dataHistory.slice(splitIndex);
|
|
1630
|
-
const firstHalfMedian = calculateMedian(firstHalf);
|
|
1631
|
-
const secondHalfMedian = calculateMedian(secondHalf);
|
|
1632
|
-
const dropPercentage = clampf(0, 100, ((firstHalfMedian - secondHalfMedian) / firstHalfMedian) * 100);
|
|
1633
|
-
const isRedFlag = dropPercentage >= this.redFlagThresholdPercentage;
|
|
1634
|
-
if (dropPercentage >= this.yellowFlagThresholdPercentage) {
|
|
1635
|
-
return {
|
|
1636
|
-
config: {
|
|
1637
|
-
yellowFlagThresholdPercentage: this.yellowFlagThresholdPercentage,
|
|
1638
|
-
redFlagThresholdPercentage: this.redFlagThresholdPercentage,
|
|
1639
|
-
historyBufferLength: this.historyBufferLength,
|
|
1640
|
-
splitPoint: this.splitPoint,
|
|
1641
|
-
},
|
|
1642
|
-
isDropDetected: true,
|
|
1643
|
-
dropPercentage,
|
|
1644
|
-
dataHistory: [...this.dataHistory],
|
|
1645
|
-
activeMedian: secondHalfMedian,
|
|
1646
|
-
quality: isRedFlag ? 'red' : 'orange',
|
|
1647
|
-
message: `Significant flow drop detected: ${dropPercentage.toFixed(2)}% (from ${firstHalfMedian} to ${secondHalfMedian})`,
|
|
1648
|
-
};
|
|
1649
|
-
}
|
|
1650
|
-
return {
|
|
1651
|
-
config: {
|
|
1652
|
-
yellowFlagThresholdPercentage: this.yellowFlagThresholdPercentage,
|
|
1653
|
-
redFlagThresholdPercentage: this.redFlagThresholdPercentage,
|
|
1654
|
-
historyBufferLength: this.historyBufferLength,
|
|
1655
|
-
splitPoint: this.splitPoint,
|
|
1656
|
-
},
|
|
1657
|
-
isDropDetected: false,
|
|
1658
|
-
dropPercentage,
|
|
1659
|
-
dataHistory: [...this.dataHistory],
|
|
1660
|
-
activeMedian: secondHalfMedian,
|
|
1661
|
-
quality: 'lime',
|
|
1662
|
-
message: 'Stable flow',
|
|
1663
|
-
};
|
|
1664
|
-
}
|
|
1665
|
-
}
|
|
1666
|
-
|
|
1667
|
-
/**
|
|
1668
|
-
* Default LBM Filter Parameters
|
|
1669
|
-
*/
|
|
1670
|
-
const DefaultFilterModel = {
|
|
1671
|
-
minimumBitrate: 1600,
|
|
1672
|
-
yellowFlag: 30,
|
|
1673
|
-
redFlag: 50,
|
|
1674
|
-
minimumFps: 5,
|
|
1675
|
-
monitoringDelayTime: 5000,
|
|
1676
|
-
initialBitrateEstimate: 1600,
|
|
1677
|
-
initialErrorCovariance: 1,
|
|
1678
|
-
processNoise: 3,
|
|
1679
|
-
measurementNoise: 500,
|
|
1680
|
-
panelOpen: false,
|
|
1681
|
-
};
|
|
1682
|
-
/**
|
|
1683
|
-
* Global LBM Filter Parameters
|
|
1684
|
-
*/
|
|
1685
|
-
const FilterModel = { ...DefaultFilterModel };
|
|
1686
|
-
/**
|
|
1687
|
-
* Bitrate Monitor Static Class
|
|
1688
|
-
*/
|
|
1689
|
-
const BITRATE_MONITOR = new DataFlowMonitor(FilterModel.yellowFlag, FilterModel.redFlag, 20);
|
|
1690
|
-
|
|
1691
|
-
class VideoStreamStatusService extends SubService {
|
|
1692
|
-
constructor() {
|
|
1693
|
-
super();
|
|
1694
|
-
this.latencyTestTimings = new LatencyTimings();
|
|
1695
|
-
this.webrtcPlayer = inject(WebRtcPlayerService);
|
|
1696
|
-
this.aggregatedStats = {};
|
|
1697
|
-
this.VideoEncoderQP = 0;
|
|
1698
|
-
this.kalmanFilter1D = new KalmanFilter1D(FilterModel.initialBitrateEstimate, FilterModel.initialErrorCovariance, FilterModel.processNoise, FilterModel.measurementNoise);
|
|
1699
|
-
this.videoStats$ = this.aggregateStats();
|
|
1700
|
-
Signal.on('setKalmanParams').subscribe((data) => {
|
|
1701
|
-
this.kalmanFilter1D.config(data);
|
|
1702
|
-
BITRATE_MONITOR.config(data);
|
|
1703
|
-
});
|
|
1704
|
-
}
|
|
1705
|
-
/**
|
|
1706
|
-
* Aggregate video stats and emit it as videoStats$
|
|
1707
|
-
*/
|
|
1708
|
-
aggregateStats() {
|
|
1709
|
-
return this.webrtcPlayer.onVideoTrack$.pipe(
|
|
1710
|
-
// IMPORTANT! DO NOT CHANGE THOSE NUMBERS, LBM Stats are based on those values
|
|
1711
|
-
switchMap$1(() => interval(250)), switchMap$1(() => from(this.getStats(this.webrtcPlayer.pcClient))), filter(Truthy), share());
|
|
1712
|
-
}
|
|
1713
|
-
setEncoder(data) {
|
|
1714
|
-
this.VideoEncoderQP = Number(new TextDecoder('utf-16').decode(data));
|
|
1715
|
-
}
|
|
1716
|
-
async getStats(pcClient) {
|
|
1717
|
-
if (!pcClient) {
|
|
1718
|
-
return null;
|
|
1719
|
-
}
|
|
1720
|
-
const stats = await pcClient.getStats(null);
|
|
1721
|
-
return this.generateAggregatedStatsFunction(stats);
|
|
1686
|
+
e.track.readyState)), share());
|
|
1687
|
+
onTrack$
|
|
1688
|
+
.pipe(filter((e) => e.track.kind === 'audio'), map$1((e) => e.streams[0]), takeUntil(this.disconnect$))
|
|
1689
|
+
.subscribe((stream) => this.videoService.attachAudioStream(stream));
|
|
1690
|
+
const videoTrack$ = onTrack$.pipe(filter((e) => e.track.kind === 'video'), map$1((e) => e.streams[0]), tap$1(() => Logger.info('Set video source from video track ontrack.')), takeUntil(this.disconnect$), share());
|
|
1691
|
+
videoTrack$.subscribe((stream) => {
|
|
1692
|
+
this.videoService.attachVideoStream(stream, this.pcClient);
|
|
1693
|
+
});
|
|
1722
1694
|
}
|
|
1723
|
-
|
|
1724
|
-
const
|
|
1725
|
-
//
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
(
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1695
|
+
async setupTransceiversAsync(pc) {
|
|
1696
|
+
const hasTransceivers = pc.getTransceivers().length > 0;
|
|
1697
|
+
// Setup a transceiver for getting UE video
|
|
1698
|
+
pc.addTransceiver('video', { direction: 'recvonly' });
|
|
1699
|
+
// Setup a transceiver for sending mic audio to UE and receiving audio from UE
|
|
1700
|
+
if (!this.useMic) {
|
|
1701
|
+
pc.addTransceiver('audio', { direction: 'recvonly' });
|
|
1702
|
+
}
|
|
1703
|
+
else {
|
|
1704
|
+
const audioSendOptions = this.useMic
|
|
1705
|
+
? {
|
|
1706
|
+
autoGainControl: false,
|
|
1707
|
+
channelCount: 1,
|
|
1708
|
+
echoCancellation: false,
|
|
1709
|
+
latency: 0,
|
|
1710
|
+
noiseSuppression: false,
|
|
1711
|
+
sampleRate: 48000,
|
|
1712
|
+
sampleSize: 16,
|
|
1713
|
+
volume: 1.0,
|
|
1714
|
+
}
|
|
1715
|
+
: false;
|
|
1716
|
+
// Note using mic on android chrome requires SSL or chrome://flags/ "unsafely-treat-insecure-origin-as-secure"
|
|
1717
|
+
const stream = await navigator.mediaDevices.getUserMedia({
|
|
1718
|
+
video: false,
|
|
1719
|
+
audio: audioSendOptions,
|
|
1720
|
+
});
|
|
1721
|
+
if (stream) {
|
|
1722
|
+
if (hasTransceivers) {
|
|
1723
|
+
for (const transceiver of pc.getTransceivers()) {
|
|
1724
|
+
if (transceiver &&
|
|
1725
|
+
transceiver.receiver &&
|
|
1726
|
+
transceiver.receiver.track &&
|
|
1727
|
+
transceiver.receiver.track.kind === 'audio') {
|
|
1728
|
+
for (const track of stream.getTracks()) {
|
|
1729
|
+
if (track.kind && track.kind == 'audio') {
|
|
1730
|
+
transceiver.sender.replaceTrack(track);
|
|
1731
|
+
transceiver.direction = 'sendrecv';
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1763
1735
|
}
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1736
|
+
}
|
|
1737
|
+
else {
|
|
1738
|
+
for (const track of stream.getTracks()) {
|
|
1739
|
+
if (track.kind && track.kind == 'audio') {
|
|
1740
|
+
pc.addTransceiver(track, { direction: 'sendrecv' });
|
|
1741
|
+
}
|
|
1769
1742
|
}
|
|
1770
1743
|
}
|
|
1771
1744
|
}
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
newStat.currentRoundTripTime = stat.currentRoundTripTime ?? 0;
|
|
1775
|
-
}
|
|
1776
|
-
// Store mimetype of each codec
|
|
1777
|
-
if (Object.hasOwn(newStat, 'codecs') &&
|
|
1778
|
-
stat.type === 'codec' &&
|
|
1779
|
-
stat.mimeType &&
|
|
1780
|
-
stat.id) {
|
|
1781
|
-
const codecId = stat.id;
|
|
1782
|
-
newStat.codecs[codecId] = stat.mimeType
|
|
1783
|
-
.replace('video/', '')
|
|
1784
|
-
.replace('audio/', '');
|
|
1745
|
+
else {
|
|
1746
|
+
pc.addTransceiver('audio', { direction: 'recvonly' });
|
|
1785
1747
|
}
|
|
1786
|
-
});
|
|
1787
|
-
newStat.pixelRatio = +window.devicePixelRatio.toFixed(2);
|
|
1788
|
-
if (this.aggregatedStats.receiveToCompositeMs) {
|
|
1789
|
-
newStat.receiveToCompositeMs = this.aggregatedStats.receiveToCompositeMs;
|
|
1790
|
-
this.latencyTestTimings.SetFrameDisplayDeltaTime(this.aggregatedStats.receiveToCompositeMs);
|
|
1791
1748
|
}
|
|
1792
|
-
// Calculate duration of run
|
|
1793
|
-
newStat.runTime = Math.floor((newStat.timestamp - newStat.timestampStart) / 1000);
|
|
1794
|
-
newStat.kalmanBitrate = Math.floor(this.kalmanFilter1D.update(newStat.bitrate || FilterModel.initialBitrateEstimate));
|
|
1795
|
-
newStat.dataFlowCheckResult = BITRATE_MONITOR.addValue(newStat.kalmanBitrate);
|
|
1796
|
-
newStat.bitrateDrop = Math.floor(newStat.dataFlowCheckResult.dropPercentage);
|
|
1797
|
-
this.aggregatedStats = newStat;
|
|
1798
|
-
return this.onAggregatedStats(newStat);
|
|
1799
1749
|
}
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1750
|
+
setupDataChannelCallbacks(datachannel) {
|
|
1751
|
+
try {
|
|
1752
|
+
TelemetryStart('DataChannel Connection');
|
|
1753
|
+
// Inform browser we would like binary data as an ArrayBuffer (FF chooses Blob by default!)
|
|
1754
|
+
datachannel.binaryType = 'arraybuffer';
|
|
1755
|
+
fromEvent(datachannel, 'close')
|
|
1756
|
+
.pipe(first())
|
|
1757
|
+
.subscribe((e) => {
|
|
1758
|
+
Logger.warn(`[DATACHANNEL] Data channel disconnected: ${datachannel.label}(${datachannel.id})`, e);
|
|
1759
|
+
this.store.dispatch(destroyRemoteConnections({
|
|
1760
|
+
disconnectReason: DisconnectReason.dataChannelClosed,
|
|
1761
|
+
}));
|
|
1762
|
+
});
|
|
1763
|
+
fromEvent(datachannel, 'error')
|
|
1764
|
+
.pipe(takeUntil(this.disconnect$))
|
|
1765
|
+
.subscribe((e) => {
|
|
1766
|
+
const message = `[DATACHANNEL] Data channel error: ${datachannel.label}(${datachannel.id})`;
|
|
1767
|
+
Logger.error(message, e);
|
|
1768
|
+
});
|
|
1769
|
+
fromEvent(datachannel, 'open')
|
|
1770
|
+
.pipe(tap$1(() => {
|
|
1771
|
+
Logger.info(`[DATACHANNEL] Data channel connected: ${datachannel.label}(${datachannel.id})`);
|
|
1772
|
+
}), takeUntil(this.disconnect$))
|
|
1773
|
+
.subscribe(() => {
|
|
1774
|
+
TelemetryStop('DataChannel Connection');
|
|
1775
|
+
this.onDataChannelConnected();
|
|
1776
|
+
});
|
|
1777
|
+
fromEvent(datachannel, 'message')
|
|
1778
|
+
.pipe(map$1((e) => e.data), takeUntil(this.disconnect$))
|
|
1779
|
+
.subscribe((data) => this.onDataChannelMessage$.next(data));
|
|
1780
|
+
return datachannel;
|
|
1781
|
+
}
|
|
1782
|
+
catch (e) {
|
|
1783
|
+
Logger.warn('[DATACHANNEL] Datachannel setup caused an exception: ', e);
|
|
1784
|
+
return null;
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
onDataChannelConnected() {
|
|
1788
|
+
let text = 'Session connected, video stream starting.';
|
|
1789
|
+
if (navigator.userAgent.includes('Firefox')) {
|
|
1790
|
+
text +=
|
|
1791
|
+
'\n If you are experiencing connection problems please try Google Chrome';
|
|
1792
|
+
}
|
|
1793
|
+
this.store.dispatch(setStatusMessage({ message: text }));
|
|
1794
|
+
this.store.dispatch(setDataChannelConnected({ value: true }));
|
|
1795
|
+
}
|
|
1796
|
+
async createOffer(pcClient) {
|
|
1797
|
+
try {
|
|
1798
|
+
const offer = await pcClient.createOffer({
|
|
1799
|
+
offerToReceiveAudio: false, //Note: if you don't need audio you can get improved latency by turning this off.
|
|
1800
|
+
offerToReceiveVideo: true,
|
|
1801
|
+
});
|
|
1802
|
+
this.mungeSDP(offer);
|
|
1803
|
+
pcClient?.setLocalDescription(offer);
|
|
1804
|
+
this.signallingSrv.send(offer);
|
|
1805
|
+
}
|
|
1806
|
+
catch {
|
|
1807
|
+
Logger.error("Couldn't create offer");
|
|
1808
|
+
}
|
|
1808
1809
|
}
|
|
1809
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.0", ngImport: i0, type:
|
|
1810
|
-
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.1.0", ngImport: i0, type:
|
|
1810
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.0", ngImport: i0, type: WebRtcPlayerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
1811
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.1.0", ngImport: i0, type: WebRtcPlayerService }); }
|
|
1811
1812
|
}
|
|
1812
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.0", ngImport: i0, type:
|
|
1813
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.0", ngImport: i0, type: WebRtcPlayerService, decorators: [{
|
|
1813
1814
|
type: Injectable
|
|
1814
1815
|
}], ctorParameters: () => [] });
|
|
1815
1816
|
|
|
@@ -1852,24 +1853,25 @@ const deepEqual = (a, b) => {
|
|
|
1852
1853
|
return false;
|
|
1853
1854
|
};
|
|
1854
1855
|
|
|
1856
|
+
const filteredLogs = [
|
|
1857
|
+
{
|
|
1858
|
+
command: 'onChangeSequence',
|
|
1859
|
+
},
|
|
1860
|
+
];
|
|
1855
1861
|
class AggregatorService extends SubService {
|
|
1856
|
-
#unrealInitialConfig;
|
|
1857
1862
|
constructor() {
|
|
1858
1863
|
super();
|
|
1859
1864
|
this.selectStreamConfig = toSignal(this.store.select(selectStreamConfig));
|
|
1860
1865
|
this.videoService = inject(VideoService);
|
|
1861
1866
|
this.webrtcPlayer = inject(WebRtcPlayerService);
|
|
1862
1867
|
this.freezeFrame = inject(FreezeFrameService);
|
|
1863
|
-
this.
|
|
1864
|
-
this.
|
|
1865
|
-
this.#unrealInitialConfig = inject(UNREAL_CONFIG);
|
|
1868
|
+
this.unrealInitialConfig = inject(UNREAL_CONFIG);
|
|
1869
|
+
this.responseEventListeners = new Map();
|
|
1866
1870
|
/**
|
|
1867
|
-
*
|
|
1868
|
-
* @
|
|
1871
|
+
* Never called for now
|
|
1869
1872
|
*/
|
|
1870
1873
|
this.destroy$ = new Subject();
|
|
1871
1874
|
this.listenWebRTC();
|
|
1872
|
-
this.listenSignaling();
|
|
1873
1875
|
this.initialize();
|
|
1874
1876
|
}
|
|
1875
1877
|
init() {
|
|
@@ -1885,13 +1887,10 @@ class AggregatorService extends SubService {
|
|
|
1885
1887
|
this.startListenCallbacks();
|
|
1886
1888
|
}
|
|
1887
1889
|
});
|
|
1888
|
-
|
|
1890
|
+
const unlockViewport$ = this.store
|
|
1889
1891
|
.select(unrealFeature.selectViewportReady)
|
|
1890
1892
|
.pipe(filter(Truthy));
|
|
1891
|
-
this.
|
|
1892
|
-
this.removeLoadScreen();
|
|
1893
|
-
});
|
|
1894
|
-
this.disconnect$.subscribe(() => {
|
|
1893
|
+
merge(unlockViewport$, this.disconnect$).subscribe(() => {
|
|
1895
1894
|
this.removeLoadScreen();
|
|
1896
1895
|
});
|
|
1897
1896
|
if (!this.selectStreamConfig()?.autoStart ||
|
|
@@ -1899,26 +1898,6 @@ class AggregatorService extends SubService {
|
|
|
1899
1898
|
this.removeLoadScreen();
|
|
1900
1899
|
}
|
|
1901
1900
|
InputOptions.controlScheme = EControlSchemeType.HoveringMouse;
|
|
1902
|
-
this.addExternalResponses();
|
|
1903
|
-
}
|
|
1904
|
-
/**
|
|
1905
|
-
* @deprecated
|
|
1906
|
-
* Hack for 3DS platform;
|
|
1907
|
-
* TODO remove ASAP, use {@link BasicConfiguratorApiService.toMetaBoxHandlers.watchCallbacks}
|
|
1908
|
-
* Add external responses to the list of responses
|
|
1909
|
-
*/
|
|
1910
|
-
addExternalResponses() {
|
|
1911
|
-
/*Signal.on('ADDRESPONSE')
|
|
1912
|
-
.pipe(takeUntil(this.destroy$))
|
|
1913
|
-
.subscribe((data: any) => {
|
|
1914
|
-
this.addResponseEventListener(data.name!, (event) => {
|
|
1915
|
-
this.externalApi.sendMessage({
|
|
1916
|
-
action: FromMetaBoxApiAction.UnrealResponse,
|
|
1917
|
-
name: data.name,
|
|
1918
|
-
responseEvent: event,
|
|
1919
|
-
});
|
|
1920
|
-
});
|
|
1921
|
-
});*/
|
|
1922
1901
|
}
|
|
1923
1902
|
addResponseEventListener(name, listener) {
|
|
1924
1903
|
this.responseEventListeners.set(name, listener);
|
|
@@ -1939,24 +1918,9 @@ class AggregatorService extends SubService {
|
|
|
1939
1918
|
// hiddenInput.blur();
|
|
1940
1919
|
}
|
|
1941
1920
|
}
|
|
1942
|
-
exposeApi() {
|
|
1943
|
-
// const globals: Window = window;
|
|
1944
|
-
// if ('apiReady' in globals) {
|
|
1945
|
-
// try {
|
|
1946
|
-
// const api = {
|
|
1947
|
-
// setStreamData: (streamData: StreamData) => {
|
|
1948
|
-
// this.streamConfig.setConfig(streamData.params);
|
|
1949
|
-
// },
|
|
1950
|
-
// };
|
|
1951
|
-
// globals.apiReady(api);
|
|
1952
|
-
// } catch (e) {
|
|
1953
|
-
// /!* empty *!/
|
|
1954
|
-
// }
|
|
1955
|
-
// }
|
|
1956
|
-
}
|
|
1957
1921
|
removeLoadScreen() {
|
|
1958
1922
|
document
|
|
1959
|
-
.getElementById(this
|
|
1923
|
+
.getElementById(this.unrealInitialConfig?.screenLockerContainerId ||
|
|
1960
1924
|
SCREEN_LOCKER_CONTAINER_ID)
|
|
1961
1925
|
?.remove();
|
|
1962
1926
|
}
|
|
@@ -1997,66 +1961,12 @@ class AggregatorService extends SubService {
|
|
|
1997
1961
|
Logger.colored(...COLOR_CODES.FROM_UNREAL, json);
|
|
1998
1962
|
}
|
|
1999
1963
|
resetResponseList() {
|
|
2000
|
-
this.responseEventListeners
|
|
2001
|
-
}
|
|
2002
|
-
listenSignaling() {
|
|
2003
|
-
const iceCandidateBuffer = [];
|
|
2004
|
-
const processBuffer = () => {
|
|
2005
|
-
iceCandidateBuffer.forEach((cnd) => {
|
|
2006
|
-
this.webrtcPlayer.handleCandidateFromServer(cnd);
|
|
2007
|
-
});
|
|
2008
|
-
iceCandidateBuffer.length = 0;
|
|
2009
|
-
};
|
|
2010
|
-
this.signallingSrv.onConfig$
|
|
2011
|
-
.pipe(takeUntil(this.destroy$))
|
|
2012
|
-
.subscribe((config) => {
|
|
2013
|
-
this.videoService.create();
|
|
2014
|
-
this.webrtcPlayer.setConfig(config);
|
|
2015
|
-
});
|
|
2016
|
-
this.signallingSrv.onWebRtcAnswer$
|
|
2017
|
-
.pipe(filter(Truthy), takeUntil(this.destroy$))
|
|
2018
|
-
.subscribe((answer) => {
|
|
2019
|
-
this.webrtcPlayer.receiveAnswer(answer);
|
|
2020
|
-
processBuffer();
|
|
2021
|
-
});
|
|
2022
|
-
this.signallingSrv.onOffer$
|
|
2023
|
-
.pipe(filter(Truthy), takeUntil(this.destroy$))
|
|
2024
|
-
.subscribe(() => {
|
|
2025
|
-
void this.webrtcPlayer.createWebRtcOffer();
|
|
2026
|
-
});
|
|
2027
|
-
this.signallingSrv.onWebRtcIce$
|
|
2028
|
-
.pipe(withLatestFrom(this.signallingSrv.onWebRtcAnswer$), takeUntil(this.destroy$))
|
|
2029
|
-
.subscribe(([candidate, answerEstablished]) => {
|
|
2030
|
-
TelemetryStop('iceCandidate', {
|
|
2031
|
-
multi: true,
|
|
2032
|
-
});
|
|
2033
|
-
iceCandidateBuffer.push(candidate);
|
|
2034
|
-
if (answerEstablished) {
|
|
2035
|
-
processBuffer();
|
|
2036
|
-
}
|
|
2037
|
-
});
|
|
1964
|
+
this.responseEventListeners.clear();
|
|
2038
1965
|
}
|
|
2039
1966
|
listenWebRTC() {
|
|
2040
|
-
this.webrtcPlayer.onWebRtcOffer$
|
|
2041
|
-
.pipe(takeUntil(this.destroy$))
|
|
2042
|
-
.subscribe((offer) => this.signallingSrv.send(offer));
|
|
2043
|
-
this.webrtcPlayer.onWebRtcCandidate$
|
|
2044
|
-
.pipe(takeUntil(this.destroy$))
|
|
2045
|
-
.subscribe((candidate) => this.signallingSrv.send(JSON.stringify({
|
|
2046
|
-
type: 'iceCandidate',
|
|
2047
|
-
candidate: candidate,
|
|
2048
|
-
})));
|
|
2049
1967
|
this.webrtcPlayer.onDataChannelMessage$
|
|
2050
1968
|
.pipe(takeUntil(this.destroy$))
|
|
2051
1969
|
.subscribe((data) => this.dataChannelMessageHandler(data));
|
|
2052
|
-
this.webrtcPlayer.onAudioTrack$
|
|
2053
|
-
.pipe(takeUntil(this.destroy$))
|
|
2054
|
-
.subscribe((stream) => this.videoService.attachAudioStream(stream));
|
|
2055
|
-
this.webrtcPlayer.onVideoTrack$
|
|
2056
|
-
.pipe(takeUntil(this.destroy$))
|
|
2057
|
-
.subscribe((stream) => {
|
|
2058
|
-
this.videoService.attachVideoStream(stream);
|
|
2059
|
-
});
|
|
2060
1970
|
}
|
|
2061
1971
|
dataChannelMessageHandler(data) {
|
|
2062
1972
|
const view = new Uint8Array(data);
|
|
@@ -2097,13 +2007,13 @@ class AggregatorService extends SubService {
|
|
|
2097
2007
|
this.freezeFrame.invalidate();
|
|
2098
2008
|
break;
|
|
2099
2009
|
case EToClientMessageType.VideoEncoderAvgQP:
|
|
2100
|
-
this.
|
|
2010
|
+
this.videoService.setEncoder(anyData);
|
|
2101
2011
|
break;
|
|
2102
2012
|
case EToClientMessageType.LatencyTest: {
|
|
2103
2013
|
const latencyTimings = decodeData(anyData);
|
|
2104
2014
|
Logger.colored(...COLOR_CODES.FROM_UNREAL, 'Got latency timings :', latencyTimings);
|
|
2105
2015
|
Logger.colored(...COLOR_CODES.FROM_UNREAL, latencyTimings);
|
|
2106
|
-
this.
|
|
2016
|
+
this.videoService.setLatencyTimings(latencyTimings);
|
|
2107
2017
|
break;
|
|
2108
2018
|
}
|
|
2109
2019
|
case EToClientMessageType.InitialSettings:
|
|
@@ -2125,11 +2035,6 @@ class AggregatorService extends SubService {
|
|
|
2125
2035
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.0", ngImport: i0, type: AggregatorService, decorators: [{
|
|
2126
2036
|
type: Injectable
|
|
2127
2037
|
}], ctorParameters: () => [] });
|
|
2128
|
-
const filteredLogs = [
|
|
2129
|
-
{
|
|
2130
|
-
command: 'onChangeSequence',
|
|
2131
|
-
},
|
|
2132
|
-
];
|
|
2133
2038
|
|
|
2134
2039
|
class ConsoleExtensionsService extends SubService {
|
|
2135
2040
|
constructor() {
|
|
@@ -2161,7 +2066,7 @@ class ConsoleExtensionsService extends SubService {
|
|
|
2161
2066
|
window.restartApp = () => {
|
|
2162
2067
|
this.store
|
|
2163
2068
|
.select(unrealFeature.selectAwsInstance)
|
|
2164
|
-
.pipe(first(), filter((data) => !!data.instanceName), tapLog('Instance', `Restart initiated`), switchMap((data) => this.httpClient.get(`//${data.instanceName}/restartapp`)), catchError(() => of(null)))
|
|
2069
|
+
.pipe(first(), filter((data) => !!data.instanceName), tapLog('Instance', `Restart initiated`), switchMap$1((data) => this.httpClient.get(`//${data.instanceName}/restartapp`)), catchError(() => of(null)))
|
|
2165
2070
|
.subscribe();
|
|
2166
2071
|
};
|
|
2167
2072
|
window.unrealHelp = () => {
|
|
@@ -2514,8 +2419,8 @@ class InputService extends SubService {
|
|
|
2514
2419
|
offsetByY: 0,
|
|
2515
2420
|
offsetByX: 0,
|
|
2516
2421
|
};
|
|
2517
|
-
const visibilityHiddenTrigger$ = fromEvent(document, 'visibilitychange').pipe(map(() => document.visibilityState === 'hidden'), filter(Truthy));
|
|
2518
|
-
const visibilityVisibleTrigger$ = fromEvent(document, 'visibilitychange').pipe(map(() => document.visibilityState === 'visible'), filter(Truthy), startWith(true));
|
|
2422
|
+
const visibilityHiddenTrigger$ = fromEvent(document, 'visibilitychange').pipe(map$1(() => document.visibilityState === 'hidden'), filter(Truthy));
|
|
2423
|
+
const visibilityVisibleTrigger$ = fromEvent(document, 'visibilitychange').pipe(map$1(() => document.visibilityState === 'visible'), filter(Truthy), startWith(true));
|
|
2519
2424
|
this.deactivatedVideoTrigger$ = merge(this.disconnect$, this.reInit$, visibilityHiddenTrigger$);
|
|
2520
2425
|
fromSignal(UnrealInternalSignalEvents.OnVideoInitialized)
|
|
2521
2426
|
.pipe(combineLatestWith(visibilityVisibleTrigger$, this.store
|
|
@@ -2974,7 +2879,7 @@ class RegionsPingService {
|
|
|
2974
2879
|
}
|
|
2975
2880
|
}
|
|
2976
2881
|
getFastest(regionListUrl = this.unrealInitialConfig?.regionsPingUrl || '') {
|
|
2977
|
-
return this.getProviders(regionListUrl).pipe(switchMap((providers) => this.getPingResult(providers)), catchError(() => of(null)), map((data) => data?.region_code));
|
|
2882
|
+
return this.getProviders(regionListUrl).pipe(switchMap$1((providers) => this.getPingResult(providers)), catchError(() => of(null)), map$1((data) => data?.region_code));
|
|
2978
2883
|
}
|
|
2979
2884
|
getPingResult(providers) {
|
|
2980
2885
|
const regions = providers.regions;
|
|
@@ -3060,9 +2965,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.0", ngImpor
|
|
|
3060
2965
|
}], ctorParameters: () => [] });
|
|
3061
2966
|
|
|
3062
2967
|
class StreamStatusTelemetryService {
|
|
3063
|
-
#store = inject(Store);
|
|
3064
|
-
#videoStatGenerator = inject(VideoStreamStatusService);
|
|
3065
2968
|
constructor() {
|
|
2969
|
+
this.store = inject(Store);
|
|
2970
|
+
this.videoService = inject(VideoService);
|
|
3066
2971
|
this.initTelemetry();
|
|
3067
2972
|
}
|
|
3068
2973
|
mapEventData(data, signalingServer, isLowBandwidth, lbmStats) {
|
|
@@ -3085,12 +2990,12 @@ class StreamStatusTelemetryService {
|
|
|
3085
2990
|
};
|
|
3086
2991
|
}
|
|
3087
2992
|
initTelemetry() {
|
|
3088
|
-
this
|
|
3089
|
-
.pipe(withLatestFrom(this
|
|
2993
|
+
this.videoService.videoStats$
|
|
2994
|
+
.pipe(withLatestFrom(this.store.select(unrealFeature.selectAwsInstance), this.store.select(unrealFeature.selectLowBandwidth), this.store.select(unrealFeature.selectLowBandwidthStats)), auditTime(5000), map$1(([data, signalingServer, isLowBandwidth, lbmStats]) => this.mapEventData(data, signalingServer, isLowBandwidth, lbmStats)))
|
|
3090
2995
|
.subscribe((data) => this.trackEventToMixPanel(data));
|
|
3091
2996
|
}
|
|
3092
2997
|
trackEventToMixPanel(data) {
|
|
3093
|
-
this
|
|
2998
|
+
this.store.dispatch(trackMixpanelEvent({ event: 'streamStatus', data }));
|
|
3094
2999
|
}
|
|
3095
3000
|
init() {
|
|
3096
3001
|
// do nothing, just to not forget to initialize Service
|
|
@@ -3130,15 +3035,15 @@ class UnrealEffects {
|
|
|
3130
3035
|
this.signallingService = inject(SignallingService);
|
|
3131
3036
|
this.scrollStrategy = inject(ScrollStrategyOptions).block();
|
|
3132
3037
|
this.commandsSender = inject(UnrealCommunicatorService);
|
|
3133
|
-
this.
|
|
3038
|
+
this.videoService = inject(VideoService);
|
|
3134
3039
|
this.cirrusDisconnectEffect$ = createEffect(() => {
|
|
3135
|
-
return this.actions$.pipe(ofType(setCirrusDisconnected), map
|
|
3040
|
+
return this.actions$.pipe(ofType(setCirrusDisconnected), map(() => resetUnrealState()));
|
|
3136
3041
|
});
|
|
3137
3042
|
this.destroyConnections$ = createEffect(() => {
|
|
3138
|
-
return this.actions$.pipe(ofType(destroyConnectionsAndResetState), map
|
|
3043
|
+
return this.actions$.pipe(ofType(destroyConnectionsAndResetState), map(() => destroyRemoteConnections({ disconnectReason: DisconnectReason.reset })));
|
|
3139
3044
|
});
|
|
3140
3045
|
this.resetState$ = createEffect(() => {
|
|
3141
|
-
return this.actions$.pipe(ofType(destroyConnectionsAndResetState), map
|
|
3046
|
+
return this.actions$.pipe(ofType(destroyConnectionsAndResetState), map(() => resetUnrealState()));
|
|
3142
3047
|
});
|
|
3143
3048
|
this.destroyRemoteConnections$ = createEffect(() => {
|
|
3144
3049
|
return this.actions$.pipe(ofType(destroyRemoteConnections), tap(({ disconnectReason }) => {
|
|
@@ -3153,16 +3058,16 @@ class UnrealEffects {
|
|
|
3153
3058
|
return this.actions$.pipe(ofType(changeStreamResolutionAction), filter((size) => !!(size?.width && size?.height)), concatLatestFrom(() => [
|
|
3154
3059
|
this.store.select(unrealFeature.selectStreamResolution),
|
|
3155
3060
|
]), filter(([newSizes, savedSizes]) => !(newSizes.width === savedSizes.width &&
|
|
3156
|
-
newSizes.height === savedSizes.height)), map
|
|
3061
|
+
newSizes.height === savedSizes.height)), map(([sizes]) => changeStreamResolutionSuccessAction({
|
|
3157
3062
|
width: sizes.width,
|
|
3158
3063
|
height: sizes.height,
|
|
3159
3064
|
})));
|
|
3160
3065
|
});
|
|
3161
3066
|
this.forceLBMOff$ = createEffect(() => {
|
|
3162
|
-
return fromEvent(document, 'visibilitychange').pipe(map
|
|
3067
|
+
return fromEvent(document, 'visibilitychange').pipe(map(() => document.visibilityState === 'visible'), filter(Truthy), tapLog('forceLBMOff$'), map(() => changeLowBandwidth({ lowBandwidth: false })));
|
|
3163
3068
|
});
|
|
3164
3069
|
this.resetFreezeFrameOnLowBandwidthTriggered$ = createEffect(() => {
|
|
3165
|
-
return this.actions$.pipe(ofType(changeLowBandwidth), map
|
|
3070
|
+
return this.actions$.pipe(ofType(changeLowBandwidth), map((state) => state.lowBandwidth), distinctUntilChanged(), map(() => setFreezeFrame({ dataUrl: null, progress: null })));
|
|
3166
3071
|
});
|
|
3167
3072
|
this.sendLbmStat$ = this.store
|
|
3168
3073
|
.select(unrealFeature.selectViewportReady)
|
|
@@ -3184,10 +3089,10 @@ class UnrealEffects {
|
|
|
3184
3089
|
.pipe(catchError(() => EMPTY))))))
|
|
3185
3090
|
.subscribe();
|
|
3186
3091
|
this.resetAfk$ = createEffect(() => {
|
|
3187
|
-
return this.actions$.pipe(ofType(setConfig, resetWarnTimeout, resetConfig), map
|
|
3092
|
+
return this.actions$.pipe(ofType(setConfig, resetWarnTimeout, resetConfig), map(() => resetAfkAction()));
|
|
3188
3093
|
});
|
|
3189
3094
|
this.resetFreezeFrameOnViewportDestroy$ = createEffect(() => {
|
|
3190
|
-
return this.actions$.pipe(ofType(setViewportReady), filter(Falsy), map
|
|
3095
|
+
return this.actions$.pipe(ofType(setViewportReady), filter(Falsy), map(() => setFreezeFrame({ dataUrl: null, progress: null })));
|
|
3191
3096
|
});
|
|
3192
3097
|
this.setMaxFps$ = createEffect(() => {
|
|
3193
3098
|
return this.actions$.pipe(ofType(setMaxFps), tap(({ maxFps }) => {
|
|
@@ -3210,7 +3115,7 @@ class UnrealEffects {
|
|
|
3210
3115
|
this.store.select(unrealFeature.selectDataChannelConnected),
|
|
3211
3116
|
]), filter(([, , isCirrusConnected, isDataChannelConnected]) => {
|
|
3212
3117
|
return isCirrusConnected && !isDataChannelConnected;
|
|
3213
|
-
}), switchMap(([awsInstance, ssData]) => {
|
|
3118
|
+
}), switchMap$1(([awsInstance, ssData]) => {
|
|
3214
3119
|
const error = `DataChannel connection timeout ${DATA_CHANNEL_CONNECTION_TIMEOUT}ms, requesting new signaling.`;
|
|
3215
3120
|
Logger.error(error);
|
|
3216
3121
|
return this.http
|
|
@@ -3225,25 +3130,25 @@ class UnrealEffects {
|
|
|
3225
3130
|
Logger.error('Error sending timeout info:', error);
|
|
3226
3131
|
return of(null);
|
|
3227
3132
|
}));
|
|
3228
|
-
}), map
|
|
3133
|
+
}), map(() => destroyRemoteConnections({
|
|
3229
3134
|
disconnectReason: DisconnectReason.dataChannelTimeout,
|
|
3230
3135
|
})));
|
|
3231
3136
|
});
|
|
3232
3137
|
this.destroyConnectionAndRestart$ = createEffect(() => {
|
|
3233
|
-
return this.actions$.pipe(ofType(destroyRemoteConnections), filter(({ disconnectReason }) => disconnectReason === DisconnectReason.dataChannelTimeout), switchMap(() => this.actions$.pipe(ofType(resetUnrealState), first())), map
|
|
3138
|
+
return this.actions$.pipe(ofType(destroyRemoteConnections), filter(({ disconnectReason }) => disconnectReason === DisconnectReason.dataChannelTimeout), switchMap$1(() => this.actions$.pipe(ofType(resetUnrealState), first())), map(() => initSignalling()));
|
|
3234
3139
|
});
|
|
3235
3140
|
this.showUnrealError$ = createEffect(() => {
|
|
3236
|
-
return this.actions$.pipe(ofType(showUnrealErrorMessage), map
|
|
3141
|
+
return this.actions$.pipe(ofType(showUnrealErrorMessage), map(({ code }) => getRtcErrorMessage(code)), distinctUntilChanged(), filter(Truthy), switchMap$1((content) => this.dialog.open(UnrealErrorModalComponent, {
|
|
3237
3142
|
scrollStrategy: this.scrollStrategy,
|
|
3238
3143
|
data: { content },
|
|
3239
|
-
}).closed), map
|
|
3144
|
+
}).closed), map(() => showUnrealErrorMessage({ code: null })));
|
|
3240
3145
|
});
|
|
3241
3146
|
this.sendStatisticsToUnreal$ = createEffect(() => {
|
|
3242
3147
|
return this.actions$.pipe(ofType(showUnrealErrorMessage), filter(({ code }) => code === 404), concatLatestFrom(() => [
|
|
3243
3148
|
this.store.select(unrealFeature.selectAwsInstance),
|
|
3244
3149
|
this.store.select(unrealFeature.selectStreamClientCompanyId),
|
|
3245
3150
|
this.store.select(unrealFeature.selectStreamViewId),
|
|
3246
|
-
]), switchMap(([action, instance, companyId, viewId]) => this.http
|
|
3151
|
+
]), switchMap$1(([action, instance, companyId, viewId]) => this.http
|
|
3247
3152
|
.post(this.unrealInitialConfig?.customErrorsEndpoint || '', {
|
|
3248
3153
|
client: instance.pollingUrl,
|
|
3249
3154
|
session_uuid: companyId,
|
|
@@ -3266,21 +3171,21 @@ class UnrealEffects {
|
|
|
3266
3171
|
}));
|
|
3267
3172
|
this.commandsSender.sendCommandToUnreal(getDebugModeCommand({ value: this.isDevMode }));
|
|
3268
3173
|
TelemetryStart('SetViewportReady');
|
|
3269
|
-
}), switchMap(() => this.store.select(selectCommandsInProgress).pipe(debounceTime$1(500), first(), switchMap(() => observeCommandResponse(getLoopBackCommand(), (data) => this.commandsSender.sendCommandToUnreal(data))), switchMap(() => this.
|
|
3174
|
+
}), switchMap$1(() => this.store.select(selectCommandsInProgress).pipe(debounceTime$1(500), first(), switchMap$1(() => observeCommandResponse(getLoopBackCommand(), (data) => this.commandsSender.sendCommandToUnreal(data))), switchMap$1(() => this.videoService.videoStats$), filter((videoStats) => !!videoStats.aggregatedStats.framesPerSecond &&
|
|
3270
3175
|
videoStats.aggregatedStats.framesPerSecond > MINIMAL_FPS &&
|
|
3271
|
-
(videoStats.aggregatedStats.framesDecoded || 0) > 15), first(), map
|
|
3176
|
+
(videoStats.aggregatedStats.framesDecoded || 0) > 15), first(), map(() => setLoopBackCommandIsCompleted()))));
|
|
3272
3177
|
});
|
|
3273
3178
|
this.setViewportReadyBySetLoopBackCommandIsCompleted$ = createEffect(() => {
|
|
3274
|
-
return this.actions$.pipe(ofType(setLoopBackCommandIsCompleted), tap(() => TelemetryStop('SetViewportReady', { ready: true })), map
|
|
3179
|
+
return this.actions$.pipe(ofType(setLoopBackCommandIsCompleted), tap(() => TelemetryStop('SetViewportReady', { ready: true })), map(() => setViewportReady({ value: true })));
|
|
3275
3180
|
});
|
|
3276
3181
|
this.listenUnrealCallbackByInitSignalling$ = createEffect(() => {
|
|
3277
|
-
return this.actions$.pipe(ofType(initSignalling), switchMap(() => fromSignal(UnrealInternalSignalEvents.UnrealCallback).pipe(map
|
|
3278
|
-
?.correlationId), filter(Truthy), map
|
|
3182
|
+
return this.actions$.pipe(ofType(initSignalling), switchMap$1(() => fromSignal(UnrealInternalSignalEvents.UnrealCallback).pipe(map(({ json }) => json?.commandCallback
|
|
3183
|
+
?.correlationId), filter(Truthy), map((id) => commandCompleted({ id })))));
|
|
3279
3184
|
});
|
|
3280
3185
|
this.setConfigByInitSignalling$ = createEffect(() => {
|
|
3281
|
-
return this.actions$.pipe(ofType(initSignalling), concatLatestFrom(() => this.store.select(unrealFeature.selectMatchUrls)), switchMap(() => this.store
|
|
3186
|
+
return this.actions$.pipe(ofType(initSignalling), concatLatestFrom(() => this.store.select(unrealFeature.selectMatchUrls)), switchMap$1(() => this.store
|
|
3282
3187
|
.select(unrealFeature.selectMatchUrls)
|
|
3283
|
-
.pipe(filter((item) => item?.length > 0))), map
|
|
3188
|
+
.pipe(filter((item) => item?.length > 0))), map((matchMakerUrls) => {
|
|
3284
3189
|
if (isEmpty(matchMakerUrls)) {
|
|
3285
3190
|
throw Error('Signalling URL(s) is empty');
|
|
3286
3191
|
}
|
|
@@ -3428,7 +3333,7 @@ const clampAndKeepMaxPercents = () => {
|
|
|
3428
3333
|
function observeCommandResponse(data, sender, timeOut = 60000, dispatchOnTimeout = true) {
|
|
3429
3334
|
const correlationId = generateUuid();
|
|
3430
3335
|
const out = { ...data, correlationId };
|
|
3431
|
-
const observable = fromSignal(UnrealInternalSignalEvents.UnrealCallback).pipe(map(({ json }) => {
|
|
3336
|
+
const observable = fromSignal(UnrealInternalSignalEvents.UnrealCallback).pipe(map$1(({ json }) => {
|
|
3432
3337
|
// Some callbacks do not have commandCallback wrapper,
|
|
3433
3338
|
// so first we check if it exists,
|
|
3434
3339
|
// then fallback to JSON itself
|
|
@@ -3461,7 +3366,7 @@ function decodeData(anyData) {
|
|
|
3461
3366
|
function fromSignal(event) {
|
|
3462
3367
|
return Signal.on(event).pipe(
|
|
3463
3368
|
//tapLog('fromSignal', event), // <== for debugging, do not remove
|
|
3464
|
-
map((data) => data.detail));
|
|
3369
|
+
map$1((data) => data.detail));
|
|
3465
3370
|
}
|
|
3466
3371
|
/**
|
|
3467
3372
|
* UnrealInternalSignalEvents commands list wrapper sender
|
|
@@ -3472,12 +3377,12 @@ function sendSignal(event, ...args) {
|
|
|
3472
3377
|
Signal.broadcast(event, { detail: args?.[0] || null });
|
|
3473
3378
|
}
|
|
3474
3379
|
function fromUnrealCallBackSignal(command) {
|
|
3475
|
-
return fromSignal(UnrealInternalSignalEvents.UnrealCallback).pipe(map((data) => data?.json), map((json) => {
|
|
3380
|
+
return fromSignal(UnrealInternalSignalEvents.UnrealCallback).pipe(map$1((data) => data?.json), map$1((json) => {
|
|
3476
3381
|
if ('commandCallback' in json) {
|
|
3477
3382
|
return json?.commandCallback;
|
|
3478
3383
|
}
|
|
3479
3384
|
return json;
|
|
3480
|
-
}), filter((json) => json.command === command), map((data) => data), map((data) => {
|
|
3385
|
+
}), filter((json) => json.command === command), map$1((data) => data), map$1((data) => {
|
|
3481
3386
|
if ('payload' in data) {
|
|
3482
3387
|
return {
|
|
3483
3388
|
command: data.command,
|
|
@@ -3513,7 +3418,7 @@ const floatToSmoothPercents = () => {
|
|
|
3513
3418
|
}), {
|
|
3514
3419
|
previous: 0,
|
|
3515
3420
|
current: 0,
|
|
3516
|
-
}), switchMap(({ previous, current }) => {
|
|
3421
|
+
}), switchMap$1(({ previous, current }) => {
|
|
3517
3422
|
if (Math.abs(current - previous) > 0.95) {
|
|
3518
3423
|
// Assume that big difference seems like switch between 0 and 1 immediately after resetLoaderState(),
|
|
3519
3424
|
// so it should go to 'current' without transitions
|
|
@@ -3521,7 +3426,7 @@ const floatToSmoothPercents = () => {
|
|
|
3521
3426
|
}
|
|
3522
3427
|
return smoothTransition(previous, current, 300);
|
|
3523
3428
|
}), // 300ms duration for smooth transition
|
|
3524
|
-
map((value) => Math.floor(value * 100)));
|
|
3429
|
+
map$1((value) => Math.floor(value * 100)));
|
|
3525
3430
|
};
|
|
3526
3431
|
|
|
3527
3432
|
const getImageFromVideoStream = (takeSizeFrom = 'video', imageOutput, sizes) => {
|
|
@@ -3912,7 +3817,7 @@ class VideoRecorder {
|
|
|
3912
3817
|
|
|
3913
3818
|
class ClickableOverlayComponent {
|
|
3914
3819
|
constructor() {
|
|
3915
|
-
this.state = toSignal(fromSignal(UnrealInternalSignalEvents.ClickableOverlay).pipe(map((data) => (typeof data === 'object' ? data : null))));
|
|
3820
|
+
this.state = toSignal(fromSignal(UnrealInternalSignalEvents.ClickableOverlay).pipe(map$1((data) => (typeof data === 'object' ? data : null))));
|
|
3916
3821
|
}
|
|
3917
3822
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.0", ngImport: i0, type: ClickableOverlayComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
3918
3823
|
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.1.0", type: ClickableOverlayComponent, isStandalone: true, selector: "app-clickable-overlay", ngImport: i0, template: "@if (state()) {\r\n <div\r\n (click)=\"state()?.onOverlayClick()\"\r\n [ngClass]=\"state()?.className\"\r\n id=\"videoPlayOverlay\"\r\n >\r\n @if (state()?.isActivityDetected) {\r\n <div class=\"resume-box\">\r\n <div aria-hidden=\"true\" class=\"resume-box__pic\" role=\"presentation\">\r\n <div [innerHTML]=\"state()?.message\" class=\"text-number\"></div>\r\n </div>\r\n <div class=\"resume-box__text\">\r\n <h3 class=\"resume-box__heading\">Session will time out soon</h3>\r\n <p>\r\n No activity detected. Press 'Continue' if you wish to keep your\r\n session active\r\n </p>\r\n </div>\r\n <src-button\r\n [colorScheme]=\"'primary'\"\r\n [isFullWidth]=\"true\"\r\n [size]=\"'large'\"\r\n [data-testid]=\"'continue-session'\"\r\n >\r\n Continue\r\n </src-button>\r\n </div>\r\n }\r\n </div>\r\n}\r\n", styles: ["#videoPlayOverlay{position:absolute;z-index:30;top:0;width:100%;height:100%;font-size:1.8em;font-family:var(--src-font-family-sans);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-br-medium, 8px);background:var(--src-color-bg-default, #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-sans);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-sans);font-size:var(--src-fs-base);font-style:normal;font-weight:400;line-height:24px;color:var(--src-color-text-default-subdued, #6b7280)}.resume-box__text .resume-box__heading{color:var(--src-color-bg-default, #1f2937);text-align:center;font-family:var(--src-font-family-sans);font-size:18px;font-style:normal;font-weight:500;line-height:26px;margin-bottom:8px}\n"], dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "component", type: SourceButtonComponent, selector: "src-button", inputs: ["type", "appearance", "colorScheme", "size", "customClass", "hasDisclosure", "isFullWidth", "isPressed", "isDisabled", "isLoading", "iconButton", "srcButtonConfig", "formID", "data-testid"], outputs: ["onClick", "onSubmit"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
@@ -3924,11 +3829,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.0", ngImpor
|
|
|
3924
3829
|
|
|
3925
3830
|
class FreezeFrameComponent {
|
|
3926
3831
|
constructor() {
|
|
3927
|
-
this
|
|
3928
|
-
this.freezeFrameProgressMessageFromVideo = toSignal(this
|
|
3929
|
-
this.combinedFreeze = toSignal(this
|
|
3832
|
+
this.store = inject(Store);
|
|
3833
|
+
this.freezeFrameProgressMessageFromVideo = toSignal(this.store.select(selectFreezeFrameProgressMessageFromVideo));
|
|
3834
|
+
this.combinedFreeze = toSignal(this.store.select(selectFreezeFrameCombinedDataUrl));
|
|
3930
3835
|
}
|
|
3931
|
-
#store;
|
|
3932
3836
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.0", ngImport: i0, type: FreezeFrameComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
3933
3837
|
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.1.0", type: FreezeFrameComponent, isStandalone: true, selector: "app-freeze-frame", ngImport: i0, template: "<div class=\"freeze-images\">\r\n @if (combinedFreeze()) {\r\n <img\r\n [src]=\"combinedFreeze()\"\r\n class=\"videoStillImage\"\r\n alt=\"freezeFrameImage\"\r\n />\r\n }\r\n\r\n @if (freezeFrameProgressMessageFromVideo()) {\r\n <div class=\"progress-status\">\r\n {{ freezeFrameProgressMessageFromVideo() }}\r\n </div>\r\n }\r\n</div>\r\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 }); }
|
|
3934
3838
|
}
|
|
@@ -4111,21 +4015,21 @@ class VideoStatsComponent {
|
|
|
4111
4015
|
constructor() {
|
|
4112
4016
|
this.collapsed = false;
|
|
4113
4017
|
this.store = inject(Store);
|
|
4114
|
-
this.
|
|
4018
|
+
this.videoService = inject(VideoService);
|
|
4115
4019
|
this.isDevMode = inject(DevModeService).isDevMode;
|
|
4116
|
-
this.obs$ = this.
|
|
4020
|
+
this.obs$ = this.videoService.videoStats$;
|
|
4117
4021
|
this.viewportReady = toSignal(this.store.select(unrealFeature.selectViewportReady));
|
|
4118
4022
|
this.ssInfo = toSignal(this.store.select(selectSignalingParameters));
|
|
4119
|
-
this.fpsTick = toSignal(this.obs$.pipe(map((stat, index) => ({ value: stat.aggregatedStats.framesPerSecond, hash: index }))));
|
|
4120
|
-
this.fpsAvgTick = toSignal(this.obs$.pipe(map((stat, index) => ({ value: stat.aggregatedStats.avgFrameRate, hash: index }))));
|
|
4121
|
-
this.bitrateTick = toSignal(this.obs$.pipe(map((stat, index) => ({ value: stat.aggregatedStats.bitrate, hash: index }))));
|
|
4122
|
-
this.videoQP = toSignal(this.obs$.pipe(map((stat, index) => ({ value: stat.aggregatedStats.VideoEncoderQP, hash: index }))));
|
|
4123
|
-
this.bitrateAverageTick = toSignal(this.obs$.pipe(map((stat, index) => ({ value: stat.aggregatedStats.avgBitrate, hash: index }))));
|
|
4124
|
-
this.videoStatus = toSignal(this.obs$.pipe(map((event) => this.isDevMode &&
|
|
4023
|
+
this.fpsTick = toSignal(this.obs$.pipe(map$1((stat, index) => ({ value: stat.aggregatedStats.framesPerSecond, hash: index }))));
|
|
4024
|
+
this.fpsAvgTick = toSignal(this.obs$.pipe(map$1((stat, index) => ({ value: stat.aggregatedStats.avgFrameRate, hash: index }))));
|
|
4025
|
+
this.bitrateTick = toSignal(this.obs$.pipe(map$1((stat, index) => ({ value: stat.aggregatedStats.bitrate, hash: index }))));
|
|
4026
|
+
this.videoQP = toSignal(this.obs$.pipe(map$1((stat, index) => ({ value: stat.aggregatedStats.VideoEncoderQP, hash: index }))));
|
|
4027
|
+
this.bitrateAverageTick = toSignal(this.obs$.pipe(map$1((stat, index) => ({ value: stat.aggregatedStats.avgBitrate, hash: index }))));
|
|
4028
|
+
this.videoStatus = toSignal(this.obs$.pipe(map$1((event) => this.isDevMode &&
|
|
4125
4029
|
{
|
|
4126
4030
|
...event.aggregatedStats,
|
|
4127
4031
|
quality: event.quality,
|
|
4128
|
-
}), filter(Boolean), map((data) => {
|
|
4032
|
+
}), filter(Boolean), map$1((data) => {
|
|
4129
4033
|
return this.fields
|
|
4130
4034
|
.map((key) => {
|
|
4131
4035
|
return {
|
|
@@ -4173,7 +4077,7 @@ class VideoStatsComponent {
|
|
|
4173
4077
|
}
|
|
4174
4078
|
updateGraph() {
|
|
4175
4079
|
this.graphList = this.fields.map((key) => {
|
|
4176
|
-
const stat = this.obs$.pipe(map((val, index) => {
|
|
4080
|
+
const stat = this.obs$.pipe(map$1((val, index) => {
|
|
4177
4081
|
const value = val.aggregatedStats[key];
|
|
4178
4082
|
return { value, hash: index };
|
|
4179
4083
|
}));
|
|
@@ -4183,7 +4087,7 @@ class VideoStatsComponent {
|
|
|
4183
4087
|
stat,
|
|
4184
4088
|
};
|
|
4185
4089
|
});
|
|
4186
|
-
this.graphList$ = of(this.graphList).pipe(combineLatestWith(this.trigger$), map(([graphList]) => graphList.filter((r) => this.elementsToShow.includes(r.key))));
|
|
4090
|
+
this.graphList$ = of(this.graphList).pipe(combineLatestWith(this.trigger$), map$1(([graphList]) => graphList.filter((r) => this.elementsToShow.includes(r.key))));
|
|
4187
4091
|
}
|
|
4188
4092
|
toggle() {
|
|
4189
4093
|
this.collapsed = !this.collapsed;
|
|
@@ -4220,20 +4124,20 @@ class VideoStatsComponent {
|
|
|
4220
4124
|
}));
|
|
4221
4125
|
}
|
|
4222
4126
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.0", ngImport: i0, type: VideoStatsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
4223
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.1.0", type: VideoStatsComponent, isStandalone: true, selector: "app-video-stats", ngImport: i0, template: "@if (videoStatus()) {\r\n @if (viewportReady()) {\r\n <div class=\"settings-container\">\r\n <button (click)=\"toggle()\" class=\"gear-button\">Stats</button>\r\n <div (click)=\"toggle()\" [class.min]=\"collapsed\" id=\"aggregatedStats\">\r\n <div class=\"forNerds\">\r\n <div (click)=\"$event.stopPropagation()\" class=\"static\">\r\n <li>\r\n <app-stat-graph\r\n [dataTick]=\"fpsTick()\"\r\n label=\"FPS (higher is better)\"\r\n />\r\n </li>\r\n <li>\r\n <app-stat-graph\r\n [dataTick]=\"videoQP()\"\r\n label=\"QP (lower is better)\"\r\n color=\"#D5ff07\"\r\n />\r\n </li>\r\n
|
|
4127
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.1.0", type: VideoStatsComponent, isStandalone: true, selector: "app-video-stats", ngImport: i0, template: "@if (videoStatus()) {\r\n @if (viewportReady()) {\r\n <div class=\"settings-container\">\r\n <button (click)=\"toggle()\" class=\"gear-button\">Stats</button>\r\n <div (click)=\"toggle()\" [class.min]=\"collapsed\" id=\"aggregatedStats\">\r\n <div class=\"forNerds\">\r\n <div (click)=\"$event.stopPropagation()\" class=\"static\">\r\n <li>\r\n <app-stat-graph\r\n [dataTick]=\"fpsTick()\"\r\n label=\"FPS (higher is better)\"\r\n />\r\n </li>\r\n <li>\r\n <app-stat-graph\r\n [dataTick]=\"videoQP()\"\r\n label=\"QP (lower is better)\"\r\n color=\"#D5ff07\"\r\n />\r\n </li>\r\n <li>\r\n <app-stat-graph\r\n [dataTick]=\"bitrateTick()\"\r\n label=\"Bitrate\"\r\n color=\"#D57F07\"\r\n />\r\n </li>\r\n </div>\r\n <div>\r\n @for (graph of graphList$ | async; track graph) {\r\n <li (click)=\"toggleGraph($event, graph.key, 0)\" class=\"graph\">\r\n <app-stat-graph\r\n [label]=\"graph.key\"\r\n [dataTick]=\"graph.stat | async\"\r\n [color]=\"graph.color\"\r\n />\r\n </li>\r\n }\r\n </div>\r\n </div>\r\n\r\n @for (el of videoStatus(); track el.key + $index) {\r\n <div (click)=\"toggleGraph($event, el.key, el.value)\" class=\"stat\">\r\n <span>{{ el.key }}: </span>{{ el.value | json }}\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n @if (!collapsed) {\r\n <div [innerHTML]=\"ssInfo() | safe: 'html'\" class=\"ssInfo\"></div>\r\n }\r\n }\r\n}\r\n", styles: [".settings-container{position:absolute;top:65px;left:10px;z-index:1000;bottom:10px}.gear-button{left:0;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{cursor:pointer}#aggregatedStats .stat span{font-weight:700}#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: ["color", "tickStep", "label", "dataTick"] }, { kind: "pipe", type: AsyncPipe, name: "async" }, { kind: "pipe", type: JsonPipe, name: "json" }, { kind: "pipe", type: SafePipe, name: "safe" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
4224
4128
|
}
|
|
4225
4129
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.0", ngImport: i0, type: VideoStatsComponent, decorators: [{
|
|
4226
4130
|
type: Component,
|
|
4227
|
-
args: [{ selector: 'app-video-stats', changeDetection: ChangeDetectionStrategy.OnPush, imports: [StatGraphComponent, AsyncPipe, JsonPipe, SafePipe], template: "@if (videoStatus()) {\r\n @if (viewportReady()) {\r\n <div class=\"settings-container\">\r\n <button (click)=\"toggle()\" class=\"gear-button\">Stats</button>\r\n <div (click)=\"toggle()\" [class.min]=\"collapsed\" id=\"aggregatedStats\">\r\n <div class=\"forNerds\">\r\n <div (click)=\"$event.stopPropagation()\" class=\"static\">\r\n <li>\r\n <app-stat-graph\r\n [dataTick]=\"fpsTick()\"\r\n label=\"FPS (higher is better)\"\r\n />\r\n </li>\r\n <li>\r\n <app-stat-graph\r\n [dataTick]=\"videoQP()\"\r\n label=\"QP (lower is better)\"\r\n color=\"#D5ff07\"\r\n />\r\n </li>\r\n
|
|
4131
|
+
args: [{ selector: 'app-video-stats', changeDetection: ChangeDetectionStrategy.OnPush, imports: [StatGraphComponent, AsyncPipe, JsonPipe, SafePipe], template: "@if (videoStatus()) {\r\n @if (viewportReady()) {\r\n <div class=\"settings-container\">\r\n <button (click)=\"toggle()\" class=\"gear-button\">Stats</button>\r\n <div (click)=\"toggle()\" [class.min]=\"collapsed\" id=\"aggregatedStats\">\r\n <div class=\"forNerds\">\r\n <div (click)=\"$event.stopPropagation()\" class=\"static\">\r\n <li>\r\n <app-stat-graph\r\n [dataTick]=\"fpsTick()\"\r\n label=\"FPS (higher is better)\"\r\n />\r\n </li>\r\n <li>\r\n <app-stat-graph\r\n [dataTick]=\"videoQP()\"\r\n label=\"QP (lower is better)\"\r\n color=\"#D5ff07\"\r\n />\r\n </li>\r\n <li>\r\n <app-stat-graph\r\n [dataTick]=\"bitrateTick()\"\r\n label=\"Bitrate\"\r\n color=\"#D57F07\"\r\n />\r\n </li>\r\n </div>\r\n <div>\r\n @for (graph of graphList$ | async; track graph) {\r\n <li (click)=\"toggleGraph($event, graph.key, 0)\" class=\"graph\">\r\n <app-stat-graph\r\n [label]=\"graph.key\"\r\n [dataTick]=\"graph.stat | async\"\r\n [color]=\"graph.color\"\r\n />\r\n </li>\r\n }\r\n </div>\r\n </div>\r\n\r\n @for (el of videoStatus(); track el.key + $index) {\r\n <div (click)=\"toggleGraph($event, el.key, el.value)\" class=\"stat\">\r\n <span>{{ el.key }}: </span>{{ el.value | json }}\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n @if (!collapsed) {\r\n <div [innerHTML]=\"ssInfo() | safe: 'html'\" class=\"ssInfo\"></div>\r\n }\r\n }\r\n}\r\n", styles: [".settings-container{position:absolute;top:65px;left:10px;z-index:1000;bottom:10px}.gear-button{left:0;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{cursor:pointer}#aggregatedStats .stat span{font-weight:700}#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"] }]
|
|
4228
4132
|
}], ctorParameters: () => [] });
|
|
4229
4133
|
|
|
4230
4134
|
class UnrealStatusComponent {
|
|
4231
4135
|
constructor() {
|
|
4232
|
-
this
|
|
4233
|
-
this
|
|
4136
|
+
this.store = inject(Store);
|
|
4137
|
+
this.unrealInitialConfig = inject(UNREAL_CONFIG, {
|
|
4234
4138
|
optional: true,
|
|
4235
4139
|
});
|
|
4236
|
-
this
|
|
4140
|
+
this.onDisconnect$ = this.store
|
|
4237
4141
|
.select(unrealFeature.selectCirrusConnected)
|
|
4238
4142
|
.pipe(filter(Falsy));
|
|
4239
4143
|
this.isDevMode = inject(DevModeService).isDevMode;
|
|
@@ -4241,11 +4145,8 @@ class UnrealStatusComponent {
|
|
|
4241
4145
|
/**
|
|
4242
4146
|
* An observable that emits smoothed percentage values from 0 to 100.
|
|
4243
4147
|
*/
|
|
4244
|
-
this.messagePercents$ = toSignal(this
|
|
4148
|
+
this.messagePercents$ = toSignal(this.onDisconnect$.pipe(switchMap$1(() => this.store.select(selectTotalProgress).pipe(floatToSmoothPercents(), clampAndKeepMaxPercents(), combineLatestWith(this.store.select(selectShowLoader)), map$1(([progress, showLoader]) => (showLoader ? progress || 0 : null))))));
|
|
4245
4149
|
}
|
|
4246
|
-
#store;
|
|
4247
|
-
#unrealInitialConfig;
|
|
4248
|
-
#onDisconnect$;
|
|
4249
4150
|
ngOnInit() {
|
|
4250
4151
|
/**
|
|
4251
4152
|
* A subject that emits null values to trigger nullification.
|
|
@@ -4260,24 +4161,24 @@ class UnrealStatusComponent {
|
|
|
4260
4161
|
/**
|
|
4261
4162
|
* An observable that emits the status message from the Unreal feature store.
|
|
4262
4163
|
*/
|
|
4263
|
-
const signalingStatus$ = this
|
|
4164
|
+
const signalingStatus$ = this.store.select(unrealFeature.selectStatusMessage);
|
|
4264
4165
|
/**
|
|
4265
4166
|
* An observable that emits connection status messages based on the Cirrus connection state.
|
|
4266
4167
|
* Then call side effect to nullify the message.
|
|
4267
4168
|
*/
|
|
4268
|
-
const cirrusConnected$ = this
|
|
4169
|
+
const cirrusConnected$ = this.store
|
|
4269
4170
|
.select(unrealFeature.selectCirrusConnected)
|
|
4270
|
-
.pipe(skip(1), switchMap((connected) => connected
|
|
4171
|
+
.pipe(skip(1), switchMap$1((connected) => connected
|
|
4271
4172
|
? of('Creating Socket...')
|
|
4272
4173
|
: of('Connection closed.').pipe(tap(() => nullify$.next(null)))));
|
|
4273
4174
|
/**
|
|
4274
4175
|
* An observable that emits the viewport visibility state.
|
|
4275
4176
|
*/
|
|
4276
|
-
const viewportTrigger$ = this
|
|
4177
|
+
const viewportTrigger$ = this.store.select(unrealFeature.selectViewportReady);
|
|
4277
4178
|
/**
|
|
4278
4179
|
* An observable that will nullify any message displayed after 500 mSec, when viewport changes its state.
|
|
4279
4180
|
*/
|
|
4280
|
-
const streamToggle$ = merge(viewportTrigger$.pipe(filter(Falsy), skip(1)), viewportTrigger$.pipe(filter(Truthy))).pipe(delay(500), map(() => null));
|
|
4181
|
+
const streamToggle$ = merge(viewportTrigger$.pipe(filter(Falsy), skip(1)), viewportTrigger$.pipe(filter(Truthy))).pipe(delay(500), map$1(() => null));
|
|
4281
4182
|
/**
|
|
4282
4183
|
* A subscription that merges various status observables and emits debounced status via postMessage.
|
|
4283
4184
|
*/
|
|
@@ -4294,7 +4195,7 @@ class UnrealStatusComponent {
|
|
|
4294
4195
|
* or any other relevant information that the iframe needs to process.
|
|
4295
4196
|
*/
|
|
4296
4197
|
sendMessageToIndex(text) {
|
|
4297
|
-
const objectElement = document.getElementById(this
|
|
4198
|
+
const objectElement = document.getElementById(this.unrealInitialConfig?.screenLockerContainerId ||
|
|
4298
4199
|
SCREEN_LOCKER_CONTAINER_ID);
|
|
4299
4200
|
try {
|
|
4300
4201
|
const objectDocument = objectElement.contentWindow;
|
|
@@ -4334,7 +4235,6 @@ class UnrealSceneComponent {
|
|
|
4334
4235
|
this.commandsSender = inject(UnrealCommunicatorService);
|
|
4335
4236
|
this.inputService = inject(InputService);
|
|
4336
4237
|
this.videoService = inject(VideoService);
|
|
4337
|
-
this.stats = inject(VideoStreamStatusService);
|
|
4338
4238
|
this.streamTelemetryService = inject(StreamStatusTelemetryService);
|
|
4339
4239
|
this.element = inject(ElementRef);
|
|
4340
4240
|
this.destroyRef = inject(DestroyRef);
|
|
@@ -4364,23 +4264,23 @@ class UnrealSceneComponent {
|
|
|
4364
4264
|
fromEvent(this.element.nativeElement, 'contextmenu')
|
|
4365
4265
|
.pipe(takeUntilDestroyed(this.destroyRef))
|
|
4366
4266
|
.subscribe((event) => event.preventDefault());
|
|
4367
|
-
this.videoService.
|
|
4368
|
-
const customSizeInputChanged$ = this.studioResolutionValues.pipe(filter(Truthy), distinctUntilChanged(), filter(({ width, height }) => this.width !== width || this.height !== height), map(({ width, height }) => ({ width, height })), tap(({ width, height }) => {
|
|
4267
|
+
this.videoService.setContainer(this.container.nativeElement);
|
|
4268
|
+
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 }) => {
|
|
4369
4269
|
this.width = width;
|
|
4370
4270
|
this.height = height;
|
|
4371
4271
|
}));
|
|
4372
4272
|
const resizeRequired$ = merge(fromSignal(UnrealInternalSignalEvents.OnVideoInitialized), fromEvent(window, 'resize'));
|
|
4373
|
-
const videoStreamSizeChanged$ = this.
|
|
4273
|
+
const videoStreamSizeChanged$ = this.videoService?.videoStats$.pipe(filter(() => !this.isFreezeFrameLoading()), map$1((stats) => ({
|
|
4374
4274
|
w: stats.aggregatedStats.frameWidth || 0,
|
|
4375
4275
|
h: stats.aggregatedStats.frameHeight || 0,
|
|
4376
4276
|
})), distinctUntilChanged((previous, current) => fpIsASameAsB(previous.w, current.w, SAME_SIZE_THRESHOLD) &&
|
|
4377
4277
|
fpIsASameAsB(previous.h, current.h, SAME_SIZE_THRESHOLD)));
|
|
4378
4278
|
merge(customSizeInputChanged$, resizeRequired$, videoStreamSizeChanged$)
|
|
4379
|
-
.pipe(debounceTime$1(DEBOUNCE_TO_MANY_RESIZE_CALLS), switchMap(() => {
|
|
4279
|
+
.pipe(debounceTime$1(DEBOUNCE_TO_MANY_RESIZE_CALLS), switchMap$1(() => {
|
|
4380
4280
|
const key = Math.random();
|
|
4381
4281
|
// Monitoring the size of the video stream
|
|
4382
|
-
return timer(0, 300).pipe(take(3), map(() => key));
|
|
4383
|
-
}), map((key) => ({ key, ...this.resizeValues })), distinctUntilChanged((previous, current) => fpIsASameAsB(previous.w, current.w, SAME_SIZE_THRESHOLD) &&
|
|
4282
|
+
return timer(0, 300).pipe(take(3), map$1(() => key));
|
|
4283
|
+
}), map$1((key) => ({ key, ...this.resizeValues })), distinctUntilChanged((previous, current) => fpIsASameAsB(previous.w, current.w, SAME_SIZE_THRESHOLD) &&
|
|
4384
4284
|
fpIsASameAsB(previous.h, current.h, SAME_SIZE_THRESHOLD) &&
|
|
4385
4285
|
previous.key === current.key), tap((size) => this.adaptVideo(size)), distinctUntilChanged((previous, current) => fpIsASameAsB(previous.w, current.w, SAME_SIZE_THRESHOLD) &&
|
|
4386
4286
|
fpIsASameAsB(previous.h, current.h, SAME_SIZE_THRESHOLD)), combineLatestWith(this.store
|
|
@@ -4523,7 +4423,7 @@ class VideoLockerComponent {
|
|
|
4523
4423
|
this.videoIntroSrc$ = this.store.select(unrealFeature.selectVideoIntroSrc);
|
|
4524
4424
|
this.imageIntroSrc$ = this.store
|
|
4525
4425
|
.select(unrealFeature.selectImageIntroSrc)
|
|
4526
|
-
.pipe(map
|
|
4426
|
+
.pipe(map((src) => {
|
|
4527
4427
|
// TODO need for demos site and need to delete this and move to demos
|
|
4528
4428
|
if (!src) {
|
|
4529
4429
|
return null;
|
|
@@ -4587,19 +4487,18 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.0", ngImpor
|
|
|
4587
4487
|
class WebrtcErrorModalComponent {
|
|
4588
4488
|
constructor() {
|
|
4589
4489
|
this.dialogRef = inject(DialogRef);
|
|
4590
|
-
this
|
|
4490
|
+
this.store = inject(Store);
|
|
4591
4491
|
}
|
|
4592
|
-
#store;
|
|
4593
4492
|
close() {
|
|
4594
4493
|
this.dialogRef.close();
|
|
4595
4494
|
}
|
|
4596
4495
|
closeModalWithCirrusDisconnect() {
|
|
4597
|
-
this
|
|
4496
|
+
this.store
|
|
4598
4497
|
.select(unrealFeature.selectCirrusConnected)
|
|
4599
4498
|
.pipe(first())
|
|
4600
4499
|
.subscribe((isConnected) => {
|
|
4601
4500
|
if (!isConnected) {
|
|
4602
|
-
this
|
|
4501
|
+
this.store.dispatch(destroyConnectionsAndResetState());
|
|
4603
4502
|
}
|
|
4604
4503
|
this.close();
|
|
4605
4504
|
});
|
|
@@ -4664,23 +4563,23 @@ class LowBandwidthDetectorComponent {
|
|
|
4664
4563
|
this.store = inject(Store);
|
|
4665
4564
|
this.dialog = inject(Dialog);
|
|
4666
4565
|
this.destroyRef = inject(DestroyRef);
|
|
4667
|
-
this.
|
|
4566
|
+
this.videoService = inject(VideoService);
|
|
4668
4567
|
this.devModeService = inject(DevModeService);
|
|
4669
4568
|
this.viewportReady$ = this.store.select(unrealFeature.selectViewportReady);
|
|
4670
4569
|
this.isLowBandwidth$ = this.store.select(unrealFeature.selectLowBandwidth);
|
|
4671
4570
|
this.lowBandwidthTrigger$ = this.isLowBandwidth$.pipe(distinctUntilChanged$1(), filter(Truthy));
|
|
4672
4571
|
this.highBandwidthTrigger$ = this.isLowBandwidth$.pipe(distinctUntilChanged$1(), filter(Falsy));
|
|
4673
4572
|
this.viewPortDestroyedTrigger$ = this.viewportReady$.pipe(distinctUntilChanged$1(), filter(Falsy));
|
|
4674
|
-
this.aggregatedStats$ = this.
|
|
4675
|
-
this.nativeQuality$ = this.
|
|
4573
|
+
this.aggregatedStats$ = this.videoService.videoStats$.pipe(map((data) => data.aggregatedStats), share());
|
|
4574
|
+
this.nativeQuality$ = this.videoService.videoStats$.pipe(map((data) => data.quality));
|
|
4676
4575
|
this.monitoringTrigger$ = this.viewportReady$
|
|
4677
|
-
.pipe(combineLatestWith(this.isLowBandwidth$), map
|
|
4576
|
+
.pipe(combineLatestWith(this.isLowBandwidth$), map(([viewPortReady, isLowBandwidth]) => viewPortReady && !isLowBandwidth))
|
|
4678
4577
|
.pipe(share());
|
|
4679
|
-
this.reTrigger$ = this.monitoringTrigger$.pipe(map
|
|
4680
|
-
this.canSwitchToLowBand$ = this.monitoringTrigger$.pipe(debounceTime$1(100), switchMap((canSwitch) => concat(of(false), timer(FilterModel.monitoringDelayTime).pipe(map
|
|
4578
|
+
this.reTrigger$ = this.monitoringTrigger$.pipe(map(() => ({ quality: 'lime' })));
|
|
4579
|
+
this.canSwitchToLowBand$ = this.monitoringTrigger$.pipe(debounceTime$1(100), switchMap$1((canSwitch) => concat(of(false), timer(FilterModel.monitoringDelayTime).pipe(map(() => canSwitch))).pipe(take(2), takeUntil$1(this.monitoringTrigger$))), distinctUntilChanged$1(), tapLog('DROP: [LOWBANDWIDTH] Monitoring Active =>'), share());
|
|
4681
4580
|
this.videoQuality$ = merge(this.nativeQuality$, this.reTrigger$).pipe(share(), takeUntil$1(this.viewPortDestroyedTrigger$));
|
|
4682
|
-
this.fps$ = this.aggregatedStats$.pipe(map
|
|
4683
|
-
this.bitrateDrop$ = this.aggregatedStats$.pipe(map
|
|
4581
|
+
this.fps$ = this.aggregatedStats$.pipe(map((data) => data.framesPerSecond || 0));
|
|
4582
|
+
this.bitrateDrop$ = this.aggregatedStats$.pipe(map((data) => data.dataFlowCheckResult), filter((br) => br.isDropDetected), takeUntil$1(merge(this.viewPortDestroyedTrigger$, this.reTrigger$)));
|
|
4684
4583
|
this.fps = toSignal(this.fps$);
|
|
4685
4584
|
this.nativeQuality = toSignal(this.nativeQuality$);
|
|
4686
4585
|
this.isLowBandwidth = toSignal(this.isLowBandwidth$);
|
|
@@ -4700,9 +4599,9 @@ class LowBandwidthDetectorComponent {
|
|
|
4700
4599
|
.pipe(filter(Truthy), tap(() => {
|
|
4701
4600
|
BITRATE_MONITOR.reset();
|
|
4702
4601
|
this.isReducedQuality.set(false);
|
|
4703
|
-
}), switchMap(() => this.bitrateDrop$), tap(this.trySetLowBandwidth.bind(this)), map
|
|
4602
|
+
}), switchMap$1(() => this.bitrateDrop$), tap(this.trySetLowBandwidth.bind(this)), map((dataFlowResult) => dataFlowResult?.isDropDetected), distinctUntilChanged$1(), tap(() => {
|
|
4704
4603
|
this.isReducedQuality.set(true);
|
|
4705
|
-
}), switchMap(() => this.videoQuality$), filter((quality) => quality === 'lime'), tap(() => this.isReducedQuality.set(false)), takeUntilDestroyed(this.destroyRef))
|
|
4604
|
+
}), switchMap$1(() => this.videoQuality$), filter((quality) => quality === 'lime'), tap(() => this.isReducedQuality.set(false)), takeUntilDestroyed(this.destroyRef))
|
|
4706
4605
|
.subscribe();
|
|
4707
4606
|
}
|
|
4708
4607
|
trySetLowBandwidth(dataFlowResult) {
|
|
@@ -4771,5 +4670,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.0", ngImpor
|
|
|
4771
4670
|
* Generated bundle index. Do not edit.
|
|
4772
4671
|
*/
|
|
4773
4672
|
|
|
4774
|
-
export { AFKService, AfkRestartScreenLockerComponent, AggregatorService, AnswerHandler, CONSOLE_COMMAND_DISABLE_MESSAGES, CONSOLE_COMMAND_ENABLE_MESSAGES, CONSOLE_COMMAND_PIXEL_QUALITY, ClickableOverlayComponent, CommandTelemetryService, ConfigHandler, ConsoleExtensionsService, DATA_CHANNEL_CONNECTION_TIMEOUT, DEBOUNCE_TO_MANY_RESIZE_CALLS, DEFAULT_TIMEOUT_PERIOD, DEFAULT_WARN_TIMEOUT, DataFlowMonitor, DevModeService, DisconnectReason, EControlSchemeType, EMessageType, EToClientMessageType, FULL_HD_HEIGHT, FULL_HD_WIDTH, FilterSettingsComponent, FreezeFrameComponent, FreezeFrameService, IceCandidateHandler, InputOptions, InputService, InstanceReadyHandler, InstanceReservedHandler, KalmanFilter1D, LatencyTimings, LowBandwidthDetectorComponent, LowBandwidthModalComponent, MINIMAL_FPS, MouseButton, MouseButtonsMask, OnCloseHandler, OnErrorHandler, OnMessageHandler, OnOpenHandler, OrchestrationMessageTypes, PingHandler, PlayerCountHandler, RegionsPingService, ResetTelemetry, SAME_SIZE_THRESHOLD, SCREEN_LOCKER_CONTAINER_ID, SSInfoHandler, STREAMING_VIDEO_ID, SafePipe, SignallingService, SpecialKeyCodes, StatGraphComponent, StreamStatusTelemetryService, SubService, TelemetryStart, TelemetryStop, UNREAL_CONFIG, UnrealCommunicatorService, UnrealEffects, UnrealInternalSignalEvents, UnrealSceneComponent, UnrealStatusMessage, VideoLockerComponent, VideoRecorder, VideoService, VideoStatsComponent,
|
|
4673
|
+
export { AFKService, AfkRestartScreenLockerComponent, AggregatorService, AnswerHandler, CONSOLE_COMMAND_DISABLE_MESSAGES, CONSOLE_COMMAND_ENABLE_MESSAGES, CONSOLE_COMMAND_PIXEL_QUALITY, ClickableOverlayComponent, CommandTelemetryService, ConfigHandler, ConsoleExtensionsService, DATA_CHANNEL_CONNECTION_TIMEOUT, DEBOUNCE_TO_MANY_RESIZE_CALLS, DEFAULT_TIMEOUT_PERIOD, DEFAULT_WARN_TIMEOUT, DataFlowMonitor, DevModeService, DisconnectReason, EControlSchemeType, EMessageType, EToClientMessageType, FULL_HD_HEIGHT, FULL_HD_WIDTH, FilterSettingsComponent, FreezeFrameComponent, FreezeFrameService, IceCandidateHandler, InputOptions, InputService, InstanceReadyHandler, InstanceReservedHandler, KalmanFilter1D, LatencyTimings, LowBandwidthDetectorComponent, LowBandwidthModalComponent, MINIMAL_FPS, MouseButton, MouseButtonsMask, OnCloseHandler, OnErrorHandler, OnMessageHandler, OnOpenHandler, OrchestrationMessageTypes, PingHandler, PlayerCountHandler, RegionsPingService, ResetTelemetry, SAME_SIZE_THRESHOLD, SCREEN_LOCKER_CONTAINER_ID, SSInfoHandler, STREAMING_VIDEO_ID, SafePipe, SignallingService, SpecialKeyCodes, StatGraphComponent, StreamStatusTelemetryService, SubService, TelemetryStart, TelemetryStop, UNREAL_CONFIG, UnrealCommunicatorService, UnrealEffects, UnrealInternalSignalEvents, UnrealSceneComponent, UnrealStatusMessage, VideoLockerComponent, VideoRecorder, VideoService, VideoStatsComponent, WSCloseCode_NORMAL_AFK_TIMEOUT, WSCloseCode_NORMAL_CIRRUS_CLOSED, WSCloseCode_NORMAL_CLOSURE, WSCloseCode_NORMAL_MANUAL_DISCONNECT, WSCloseCodes, WS_OPEN_STATE, WS_TIMEOUT, WebRtcPlayerService, WebrtcErrorModalComponent, alignProductsToPlaneCommand, changeLowBandwidth, changeStatusMainVideoOnScene, changeStreamResolutionAction, changeStreamResolutionSuccessAction, clampAndKeepMaxPercents, clampPanToProductsCommand, commandCompleted, commandStarted, decodeData, destroyConnectionsAndResetState, destroyRemoteConnections, dispatchResize, floatToSmoothPercents, forceResizeUnrealVideo, fromResizeObserver, fromSignal, fromUnrealCallBackSignal, getActiveUrl, getApplyCameraPresetCommand, getApplyZoomCommand, getCameraBoxCommand, getCameraRecenterCommand, getCameraSettingsCommand, getChangeGizmoTypeCommand, getChangeResolutionCommand, getClickSnapCommand, getControlSensitivityCommand, getDebugModeCommand, getDragCommand, getDragSequenceCommand, getDropCommand, getEnableComposureCommand, getEnableControlsCommand, getEnableSceneStateCallbackCommand, getEnableSpinnerModeCommand, getEnableTeleportCommand, getExecuteConsoleCommand, getFitToObjectsCommand, getFreezeFrameCommand, getImageFromVideoStream, getInitSequenceByObjectNameCommand, getJumpToSequenceCommand, getLoadLevelCommand, getLoadProductCommand, getLoadSceneStateCommand, getLoopBackCommand, getMoveSelectedCommand, getPauseSequenceCommand, getPlaySequenceCommand, getRequestCameraPresetCommand, getResetControlClampsCommand, getRotateSelectedCommand, getRtcErrorMessage, getSelectProductByObjectNameCommand, getSetCameraControlClampsCommand, getSetControlCameraModeCommand, getSetFpsCommand, getSetMaterialCommand, getSetPawnMovementModeCommand, getSetSettingsSequenceCommand, getStopSequenceCommand, getTakeRenderCommand, getTakeSpinnerRenderCommand, getTakeSpinnerRenderPreviewCommand, getUnLoadAllProductsCommand, getUnLoadProductByObjectNameCommand, getUnselectAllProductsCommand, getWeatherCommand, initSignalling, initialState, mapQpToQuality, observeCommandResponse, removeExileCommands, resetAfk, resetAfkAction, resetConfig, resetIntroSrc, resetUnrealState, resetUnrealStateAction, resetWarnTimeout, selectCommandProgress, selectCommandsInProgress, selectFreezeFrameCombinedDataUrl, selectFreezeFrameDataUrl, selectFreezeFrameDataUrlFromVideo, selectFreezeFrameProgressMessageFromVideo, selectIsAutostart, selectIsFreezeFrameLoading, selectIsVideoPlayingAndDataChannelConnected, selectLastCommandInProgress, selectLoaderCommands, selectMatchUrls, selectShowLoader, selectShowReconnectPopup, selectSignalingParameters, selectStreamConfig, selectTotalProgress, selectWarnTimeout, selectWsUrl, sendSignal, setAwsInstance, setCirrusConnected, setCirrusDisconnected, setConfig, setDataChannelConnected, setErrorMessage, setEstablishingConnection, setFreezeFrame, setFreezeFrameFromVideo, setIntroImageSrc, setIntroVideoSrc, setLoadingImageSrc, setLoopBackCommandIsCompleted, setMatchUrls, setMaxFps, setProductsLocationCommand, setSignalingName, setStatusMessage, setStatusPercentSignallingServer, setStreamClientCompanyId, setStreamViewId, setViewportReady, showUnrealErrorMessage, trackMixpanelEvent, unLoadAllLevelsCommand, unrealFeature, unrealReducer, updateCirrusInfo };
|
|
4775
4674
|
//# sourceMappingURL=3dsource-angular-unreal-module.mjs.map
|