@3dsource/angular-unreal-module 0.0.107 → 0.0.109
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.
|
@@ -2878,103 +2878,243 @@ class WebRtcPlayerService extends SubService {
|
|
|
2878
2878
|
this.useMic = false;
|
|
2879
2879
|
this.forceTURN = false;
|
|
2880
2880
|
this.forceMonoAudio = false;
|
|
2881
|
+
/** Whether the remote SDP has been applied to the current peer connection */
|
|
2882
|
+
this.remoteDescriptionSet = false;
|
|
2883
|
+
/** ICE candidates received before the remote description was set */
|
|
2884
|
+
this.iceCandidateBuffer = [];
|
|
2881
2885
|
this.isReconnecting = this.store.selectSignal(unrealFeature.selectIsReconnecting);
|
|
2882
2886
|
this.init();
|
|
2883
2887
|
this.actions$.pipe(ofType(dropDC)).subscribe(() => this.dcClient?.close());
|
|
2884
2888
|
this.actions$.pipe(ofType(dropPC)).subscribe(() => this.pcClient?.close());
|
|
2885
2889
|
}
|
|
2886
2890
|
init() {
|
|
2887
|
-
const iceCandidateBuffer = [];
|
|
2888
|
-
const processBuffer = () => {
|
|
2889
|
-
iceCandidateBuffer.forEach((cnd) => {
|
|
2890
|
-
this.handleCandidateFromServer(cnd);
|
|
2891
|
-
});
|
|
2892
|
-
iceCandidateBuffer.length = 0;
|
|
2893
|
-
};
|
|
2894
2891
|
this.signallingSrv.onConfig$.subscribe((config) => {
|
|
2895
2892
|
this.videoService.create();
|
|
2896
2893
|
this.setConfig(config);
|
|
2897
2894
|
});
|
|
2895
|
+
// Server-initiated offer: set remote description and respond with answer
|
|
2896
|
+
this.signallingSrv.onOffer$.pipe(filter(Truthy)).subscribe((offer) => {
|
|
2897
|
+
void this.handleServerOffer(offer);
|
|
2898
|
+
});
|
|
2899
|
+
// Answer to a client-initiated offer
|
|
2898
2900
|
this.signallingSrv.onWebRtcAnswer$
|
|
2899
2901
|
.pipe(filter(Truthy))
|
|
2900
2902
|
.subscribe((answer) => {
|
|
2901
|
-
this.
|
|
2902
|
-
processBuffer();
|
|
2903
|
-
});
|
|
2904
|
-
this.signallingSrv.onOffer$.pipe(filter(Truthy)).subscribe(() => {
|
|
2905
|
-
void this.createWebRtcOffer();
|
|
2903
|
+
void this.handleServerAnswer(answer);
|
|
2906
2904
|
});
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
.
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
if (
|
|
2913
|
-
|
|
2905
|
+
// ICE candidates — buffered until remote description is set
|
|
2906
|
+
this.signallingSrv.onWebRtcIce$.subscribe((candidate) => {
|
|
2907
|
+
this.commandTelemetryService.trackStopCommand('EXT-iceCandidate', true, {
|
|
2908
|
+
multi: true,
|
|
2909
|
+
});
|
|
2910
|
+
if (this.remoteDescriptionSet) {
|
|
2911
|
+
this.handleCandidateFromServer(candidate);
|
|
2912
|
+
}
|
|
2913
|
+
else {
|
|
2914
|
+
this.iceCandidateBuffer.push(candidate);
|
|
2914
2915
|
}
|
|
2915
2916
|
});
|
|
2916
2917
|
}
|
|
2918
|
+
/**
|
|
2919
|
+
* Handles an SDP offer received from the server.
|
|
2920
|
+
* Creates a new peer connection, applies the remote offer,
|
|
2921
|
+
* then responds with an SDP answer.
|
|
2922
|
+
*
|
|
2923
|
+
* The DataChannel is NOT created here — the server creates it,
|
|
2924
|
+
* and we receive it via the `datachannel` event on the peer connection.
|
|
2925
|
+
*/
|
|
2926
|
+
async handleServerOffer(offer) {
|
|
2927
|
+
Logger.info('Received offer from server, creating answer');
|
|
2928
|
+
try {
|
|
2929
|
+
this.remoteDescriptionSet = false;
|
|
2930
|
+
this.iceCandidateBuffer.length = 0;
|
|
2931
|
+
if (!this.pcClient) {
|
|
2932
|
+
this.setupPeerConnection(this.cfg);
|
|
2933
|
+
}
|
|
2934
|
+
// Offer already defines media transceivers — don't add extras
|
|
2935
|
+
await this.pcClient?.setRemoteDescription(offer);
|
|
2936
|
+
this.remoteDescriptionSet = true;
|
|
2937
|
+
this.flushIceCandidateBuffer();
|
|
2938
|
+
await this.createAnswer(this.pcClient);
|
|
2939
|
+
}
|
|
2940
|
+
catch (e) {
|
|
2941
|
+
Logger.error('Failed to handle server offer', e);
|
|
2942
|
+
this.store.dispatch(disconnectStream({
|
|
2943
|
+
reason: DisconnectReason.WebRTCError,
|
|
2944
|
+
message: 'WebRTCError: Failed to handle server offer',
|
|
2945
|
+
}));
|
|
2946
|
+
}
|
|
2947
|
+
}
|
|
2948
|
+
/**
|
|
2949
|
+
* Handles an SDP answer received from the server in response to our offer.
|
|
2950
|
+
* Applies the remote description and flushes any buffered ICE candidates.
|
|
2951
|
+
*/
|
|
2952
|
+
async handleServerAnswer(answer) {
|
|
2953
|
+
Logger.info('Received answer from server');
|
|
2954
|
+
try {
|
|
2955
|
+
await this.pcClient?.setRemoteDescription(answer);
|
|
2956
|
+
this.remoteDescriptionSet = true;
|
|
2957
|
+
this.flushIceCandidateBuffer();
|
|
2958
|
+
}
|
|
2959
|
+
catch (e) {
|
|
2960
|
+
Logger.error('Failed to set remote description from answer', e);
|
|
2961
|
+
}
|
|
2962
|
+
}
|
|
2963
|
+
/** Drains buffered ICE candidates after the remote description has been set */
|
|
2964
|
+
flushIceCandidateBuffer() {
|
|
2965
|
+
for (const candidate of this.iceCandidateBuffer) {
|
|
2966
|
+
this.handleCandidateFromServer(candidate);
|
|
2967
|
+
}
|
|
2968
|
+
this.iceCandidateBuffer.length = 0;
|
|
2969
|
+
}
|
|
2970
|
+
/**
|
|
2971
|
+
* Creates a client-initiated WebRTC offer and sends it to the server.
|
|
2972
|
+
* When the client offers, it also creates the DataChannel.
|
|
2973
|
+
* Used for peer reconnection through the existing signaling connection.
|
|
2974
|
+
*/
|
|
2917
2975
|
async createWebRtcOffer() {
|
|
2918
|
-
Logger.info('Creating offer');
|
|
2919
|
-
let offer;
|
|
2976
|
+
Logger.info('Creating client-initiated offer');
|
|
2920
2977
|
try {
|
|
2921
|
-
|
|
2978
|
+
this.remoteDescriptionSet = false;
|
|
2979
|
+
this.iceCandidateBuffer.length = 0;
|
|
2980
|
+
if (!this.pcClient) {
|
|
2981
|
+
this.setupPeerConnection(this.cfg);
|
|
2982
|
+
}
|
|
2983
|
+
// Client offers: transceivers first, then DC, then offer
|
|
2984
|
+
await this.setupTransceiversAsync(this.pcClient);
|
|
2985
|
+
this.dcClient = this.createDataChannel(this.pcClient, 'UnrealDcConnection', { ordered: true });
|
|
2986
|
+
await this.createOffer(this.pcClient);
|
|
2922
2987
|
}
|
|
2923
|
-
catch {
|
|
2988
|
+
catch (e) {
|
|
2989
|
+
Logger.error('Failed to create WebRTC offer', e);
|
|
2924
2990
|
this.store.dispatch(disconnectStream({
|
|
2925
2991
|
reason: DisconnectReason.WebRTCError,
|
|
2926
2992
|
message: 'WebRTCError: Offer Error',
|
|
2927
2993
|
}));
|
|
2928
2994
|
}
|
|
2929
|
-
|
|
2995
|
+
}
|
|
2996
|
+
/**
|
|
2997
|
+
* Initializes a new RTCPeerConnection with event handlers.
|
|
2998
|
+
* Transceivers are NOT added here — the caller is responsible for adding
|
|
2999
|
+
* them at the correct point depending on the negotiation flow.
|
|
3000
|
+
*/
|
|
3001
|
+
setupPeerConnection(config) {
|
|
3002
|
+
this.closePC();
|
|
3003
|
+
Logger.log('Creating new RTCPeerConnection with config');
|
|
3004
|
+
this.pcClient = new RTCPeerConnection(config);
|
|
3005
|
+
this.commandTelemetryService.trackStartCommand('EXT-Browser ICE gathering');
|
|
3006
|
+
this.commandTelemetryService.trackStartCommand('EXT-Browser ICE connection');
|
|
3007
|
+
this.commandTelemetryService.trackStartCommand('EXT-Signaling state change');
|
|
3008
|
+
this.handlePeerConnectionEvents(this.pcClient);
|
|
3009
|
+
this.disconnect$.pipe(take(1)).subscribe(() => {
|
|
3010
|
+
this.closePC();
|
|
3011
|
+
});
|
|
2930
3012
|
}
|
|
2931
3013
|
createDataChannel(pc, label, options) {
|
|
2932
|
-
// This is the primary data channel code path when we are "offering"
|
|
2933
3014
|
const datachannel = pc.createDataChannel(label, options);
|
|
2934
3015
|
return this.setupDataChannelCallbacks(datachannel);
|
|
2935
3016
|
}
|
|
2936
|
-
receiveAnswer(answer) {
|
|
2937
|
-
this.pcClient?.setRemoteDescription(answer);
|
|
2938
|
-
}
|
|
2939
3017
|
handleCandidateFromServer(iceCandidate) {
|
|
2940
3018
|
try {
|
|
2941
3019
|
const candidate = new RTCIceCandidate(iceCandidate);
|
|
2942
3020
|
Logger.info('[Unreal ICE candidate]', '| Type=', candidate.type, '| Protocol=', candidate.protocol, '| Address=', candidate.address, '| Port=', candidate.port, '|');
|
|
2943
|
-
//
|
|
3021
|
+
// If forcing TURN, reject any candidates not relay
|
|
2944
3022
|
if (this.forceTURN) {
|
|
2945
|
-
// check if no relay address is found, if so, we are assuming it means no TURN server
|
|
2946
3023
|
if (candidate.candidate.indexOf('relay') < 0) {
|
|
2947
3024
|
Logger.warn('Dropping candidate because it was not TURN relay.', '| Type=', candidate.type, '| Protocol=', candidate.protocol, '| Address=', candidate.address, '| Port=', candidate.port, '|');
|
|
2948
3025
|
return;
|
|
2949
3026
|
}
|
|
2950
3027
|
}
|
|
2951
|
-
this.pcClient?.addIceCandidate(candidate).catch(
|
|
2952
|
-
|
|
3028
|
+
this.pcClient?.addIceCandidate(candidate).catch((e) => {
|
|
3029
|
+
Logger.warn('Failed to add ICE candidate', e);
|
|
2953
3030
|
});
|
|
2954
3031
|
}
|
|
2955
3032
|
catch (e) {
|
|
2956
3033
|
Logger.error('Error handleCandidateFromServer', e);
|
|
2957
3034
|
}
|
|
2958
3035
|
}
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
3036
|
+
/**
|
|
3037
|
+
* Reorders the `m=video` line in the SDP so that payload types matching
|
|
3038
|
+
* the given H.264 `profile-level-id` appear first, making the browser
|
|
3039
|
+
* prefer that profile during codec negotiation.
|
|
3040
|
+
*
|
|
3041
|
+
* A `profile-level-id` is a 3-byte hex string (`XXYYZZ`) defined in
|
|
3042
|
+
* RFC 6184 that encodes three H.264 parameters:
|
|
3043
|
+
* - **Profile IDC** (`XX`) — the H.264 profile.
|
|
3044
|
+
* - **Profile IOP** (`YY`) — constraint flags that further narrow the profile.
|
|
3045
|
+
* - **Level IDC** (`ZZ`) — the level, which caps resolution/framerate/bitrate.
|
|
3046
|
+
*
|
|
3047
|
+
* ### H.264 profiles relevant to Unreal Engine Pixel Streaming
|
|
3048
|
+
*
|
|
3049
|
+
* | profile-level-id | Profile | Level | Notes |
|
|
3050
|
+
* |------------------|----------------------------|-------|----------------------------------------------|
|
|
3051
|
+
* | `42001f` | Baseline | 3.1 | No B-frames, low latency; legacy devices |
|
|
3052
|
+
* | `42e01f` | Constrained Baseline | 3.1 | Widest HW decode support; **recommended** |
|
|
3053
|
+
* | `4d001f` | Main | 3.1 | B-frames + CABAC; better compression |
|
|
3054
|
+
* | `4d4028` | Main | 4.0 | Up to 1080p60; common desktop target |
|
|
3055
|
+
* | `640028` | High | 4.0 | Best quality/compression; most GPUs support |
|
|
3056
|
+
* | `64002a` | High | 4.2 | Up to 1080p120 or 4K30 |
|
|
3057
|
+
* | `640033` | High | 5.1 | Up to 4K60; requires modern HW |
|
|
3058
|
+
* | `640034` | High | 5.2 | Up to 4K120; high-end HW only |
|
|
3059
|
+
* | `6e001f` | High 10 | 3.1 | 10-bit color; limited browser support |
|
|
3060
|
+
* | `f4001f` | High 4:4:4 Predictive | 3.1 | Lossless capable; rare in browsers |
|
|
3061
|
+
*
|
|
3062
|
+
* UE Pixel Streaming defaults to Constrained Baseline (`42e01f`) or High (`6400xx`)
|
|
3063
|
+
* depending on engine version and `WebRTC.H264Profile` CVar.
|
|
3064
|
+
*
|
|
3065
|
+
* We prioritize `42e01f` because it guarantees hardware decoding on
|
|
3066
|
+
* virtually all client devices (desktop, mobile, embedded), avoiding
|
|
3067
|
+
* fallback to software decoding or negotiation of a higher profile
|
|
3068
|
+
* that the client GPU may not accelerate.
|
|
3069
|
+
*/
|
|
3070
|
+
prioritizeH264Profile(sdp, profileLevelId) {
|
|
3071
|
+
const lines = sdp.split('\r\n');
|
|
3072
|
+
const targetPts = [];
|
|
3073
|
+
for (const line of lines) {
|
|
3074
|
+
if (line.startsWith('a=fmtp:') &&
|
|
3075
|
+
line.toLowerCase().includes(`profile-level-id=${profileLevelId}`)) {
|
|
3076
|
+
const pt = line.split(':')[1]?.split(' ')[0];
|
|
3077
|
+
if (pt)
|
|
3078
|
+
targetPts.push(pt);
|
|
3079
|
+
}
|
|
3080
|
+
}
|
|
3081
|
+
if (targetPts.length === 0)
|
|
3082
|
+
return sdp;
|
|
3083
|
+
return lines
|
|
3084
|
+
.map((line) => {
|
|
3085
|
+
if (!line.startsWith('m=video'))
|
|
3086
|
+
return line;
|
|
3087
|
+
const parts = line.split(' ');
|
|
3088
|
+
// parts: m=video <port> <proto> <pt1> <pt2> ...
|
|
3089
|
+
const header = parts.slice(0, 3);
|
|
3090
|
+
const pts = parts.slice(3);
|
|
3091
|
+
const prioritized = [
|
|
3092
|
+
...targetPts.filter((pt) => pts.includes(pt)),
|
|
3093
|
+
...pts.filter((pt) => !targetPts.includes(pt)),
|
|
3094
|
+
];
|
|
3095
|
+
return [...header, ...prioritized].join(' ');
|
|
3096
|
+
})
|
|
3097
|
+
.join('\r\n');
|
|
3098
|
+
}
|
|
3099
|
+
mungeSDP(sdp) {
|
|
3100
|
+
let mungedSDP = this.prioritizeH264Profile(sdp, '42e01f');
|
|
3101
|
+
mungedSDP = mungedSDP.replace(/(a=fmtp:\d+ .*level-asymmetry-allowed=.*)\r\n/gm, '$1;x-google-start-bitrate=10000;x-google-max-bitrate=100000\r\n');
|
|
3102
|
+
// Set max bitrate to highest bitrate Opus supports
|
|
3103
|
+
let audioSDP = 'maxaveragebitrate=510000;';
|
|
2963
3104
|
if (this.useMic) {
|
|
2964
|
-
//
|
|
3105
|
+
// Set the max capture rate to 48khz (so we can send high-quality audio from mic)
|
|
2965
3106
|
audioSDP += 'sprop-maxcapturerate=48000;';
|
|
2966
3107
|
}
|
|
2967
3108
|
// Force mono or stereo based on whether ?forceMono was passed or not
|
|
2968
|
-
audioSDP += this.forceMonoAudio
|
|
2969
|
-
|
|
2970
|
-
: 'sprop-stereo=1;stereo=1;';
|
|
2971
|
-
// enable in-band forward error correction for opus audio
|
|
3109
|
+
audioSDP += this.forceMonoAudio ? 'stereo=0;' : 'stereo=1;';
|
|
3110
|
+
// Enable in-band forward error correction for opus audio
|
|
2972
3111
|
audioSDP += 'useinbandfec=1';
|
|
2973
3112
|
// We use the line 'useinbandfec=1' (which Opus uses) to set our Opus specific audio parameters.
|
|
2974
|
-
|
|
3113
|
+
mungedSDP = mungedSDP?.replace('useinbandfec=1', audioSDP);
|
|
3114
|
+
return mungedSDP;
|
|
2975
3115
|
}
|
|
2976
3116
|
/**
|
|
2977
|
-
* Closes existing PeerConnection
|
|
3117
|
+
* Closes existing PeerConnection and DataChannel, resets negotiation state.
|
|
2978
3118
|
*/
|
|
2979
3119
|
closePC() {
|
|
2980
3120
|
if (this.pcClient) {
|
|
@@ -2987,6 +3127,8 @@ class WebRtcPlayerService extends SubService {
|
|
|
2987
3127
|
}
|
|
2988
3128
|
this.dcClient?.close();
|
|
2989
3129
|
this.dcClient = null;
|
|
3130
|
+
this.remoteDescriptionSet = false;
|
|
3131
|
+
this.iceCandidateBuffer.length = 0;
|
|
2990
3132
|
}
|
|
2991
3133
|
canSend() {
|
|
2992
3134
|
return this.dcClient && this.dcClient.readyState === 'open';
|
|
@@ -3023,9 +3165,9 @@ class WebRtcPlayerService extends SubService {
|
|
|
3023
3165
|
}
|
|
3024
3166
|
}
|
|
3025
3167
|
/**
|
|
3026
|
-
*
|
|
3027
|
-
*
|
|
3028
|
-
* the existing WebSocket connection.
|
|
3168
|
+
* Reconnects only the WebRTC peer connection without re-establishing signaling.
|
|
3169
|
+
* Closes the current PeerConnection and creates a new client-initiated offer
|
|
3170
|
+
* through the existing WebSocket connection.
|
|
3029
3171
|
* @returns true if reconnection was initiated, false if signaling is not connected
|
|
3030
3172
|
*/
|
|
3031
3173
|
reconnectPeer() {
|
|
@@ -3058,11 +3200,7 @@ class WebRtcPlayerService extends SubService {
|
|
|
3058
3200
|
this.cfg.bundlePolicy = urlParams.has('ForceMaxBundle')
|
|
3059
3201
|
? 'max-bundle'
|
|
3060
3202
|
: 'balanced';
|
|
3061
|
-
|
|
3062
|
-
//Variables
|
|
3063
|
-
//**********************
|
|
3064
|
-
this.pcClient = null;
|
|
3065
|
-
this.dcClient = null;
|
|
3203
|
+
this.cfg.iceCandidatePoolSize = 1;
|
|
3066
3204
|
// When ?useMic check for SSL or localhost
|
|
3067
3205
|
const isLocalhostConnection = location.hostname === 'localhost' || location.hostname === '127.0.0.1';
|
|
3068
3206
|
const isHttpsConnection = location.protocol === 'https:';
|
|
@@ -3071,23 +3209,7 @@ class WebRtcPlayerService extends SubService {
|
|
|
3071
3209
|
Logger.error('Microphone access in the browser will not work if you are not on HTTPS or localhost. Disabling mic access.');
|
|
3072
3210
|
Logger.error("For testing you can enable HTTP microphone access Chrome by visiting chrome://flags/ and enabling 'unsafely-treat-insecure-origin-as-secure'");
|
|
3073
3211
|
}
|
|
3074
|
-
|
|
3075
|
-
async createPeerConnection(config) {
|
|
3076
|
-
this.closePC();
|
|
3077
|
-
this.pcClient = new RTCPeerConnection(config);
|
|
3078
|
-
this.commandTelemetryService.trackStartCommand('EXT-Browser ICE gathering');
|
|
3079
|
-
this.commandTelemetryService.trackStartCommand('EXT-Browser ICE connection');
|
|
3080
|
-
this.commandTelemetryService.trackStartCommand('EXT-Signaling state change');
|
|
3081
|
-
this.handlePeerConnectionEvents(this.pcClient);
|
|
3082
|
-
await this.setupTransceiversAsync(this.pcClient);
|
|
3083
|
-
this.dcClient = this.createDataChannel(this.pcClient, 'UnrealDcConnection', {
|
|
3084
|
-
// See https://www.w3.org/TR/webrtc/#dom-rtcdatachannelinit for values (this is needed for Firefox to be consistent with Chrome.)
|
|
3085
|
-
ordered: true,
|
|
3086
|
-
});
|
|
3087
|
-
this.disconnect$.pipe(take(1)).subscribe(() => {
|
|
3088
|
-
this.closePC();
|
|
3089
|
-
});
|
|
3090
|
-
await this.createOffer(this.pcClient);
|
|
3212
|
+
this.setupPeerConnection(this.cfg);
|
|
3091
3213
|
}
|
|
3092
3214
|
handlePeerConnectionEvents(pcClient) {
|
|
3093
3215
|
fromEvent(pcClient, 'signalingstatechange')
|
|
@@ -3116,8 +3238,10 @@ class WebRtcPlayerService extends SubService {
|
|
|
3116
3238
|
fromEvent(pcClient, 'icecandidate')
|
|
3117
3239
|
.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$))
|
|
3118
3240
|
.subscribe((candidate) => this.signallingSrv.send({ type: 'iceCandidate', candidate }));
|
|
3241
|
+
// When the server creates the DataChannel (server-offer flow),
|
|
3242
|
+
// we receive it here via the 'datachannel' event
|
|
3119
3243
|
fromEvent(pcClient, 'datachannel')
|
|
3120
|
-
.pipe(tap$1(() => Logger.info(
|
|
3244
|
+
.pipe(tap$1((e) => Logger.info(`[DATACHANNEL] Received remote DC: label="${e.channel.label}" id=${e.channel.id} protocol="${e.channel.protocol}" ordered=${e.channel.ordered}`)), map$1((e) => e.channel), takeUntil(this.disconnect$))
|
|
3121
3245
|
.subscribe((datachannel) => {
|
|
3122
3246
|
this.dcClient = this.setupDataChannelCallbacks(datachannel);
|
|
3123
3247
|
});
|
|
@@ -3225,7 +3349,14 @@ class WebRtcPlayerService extends SubService {
|
|
|
3225
3349
|
this.onDataChannelConnected();
|
|
3226
3350
|
});
|
|
3227
3351
|
fromEvent(datachannel, 'message')
|
|
3228
|
-
.pipe(
|
|
3352
|
+
.pipe(
|
|
3353
|
+
/* tap((e: MessageEvent) => {
|
|
3354
|
+
const view = new Uint8Array(e.data);
|
|
3355
|
+
Logger.info(
|
|
3356
|
+
`[DATACHANNEL] Message received on "${datachannel.label}" id=${datachannel.id}, byteCode=${view[0]}, size=${view.byteLength}`,
|
|
3357
|
+
);
|
|
3358
|
+
}),*/
|
|
3359
|
+
map$1((e) => e.data), takeUntil(this.disconnect$))
|
|
3229
3360
|
.subscribe((data) => this.onDataChannelMessage$.next(data));
|
|
3230
3361
|
return datachannel;
|
|
3231
3362
|
}
|
|
@@ -3242,20 +3373,38 @@ class WebRtcPlayerService extends SubService {
|
|
|
3242
3373
|
}
|
|
3243
3374
|
this.store.dispatch(dataChannelConnected({ statusMessage }));
|
|
3244
3375
|
}
|
|
3376
|
+
/**
|
|
3377
|
+
* Creates an SDP offer, applies SDP munging, sets it as a local description,
|
|
3378
|
+
* and sends it to the server via signaling.
|
|
3379
|
+
* Transceivers already configure media directions, so deprecated
|
|
3380
|
+
* offerToReceive* options are not used.
|
|
3381
|
+
*/
|
|
3245
3382
|
async createOffer(pcClient) {
|
|
3246
3383
|
try {
|
|
3247
|
-
const offer = await pcClient.createOffer(
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
});
|
|
3251
|
-
this.mungeSDP(offer);
|
|
3252
|
-
pcClient?.setLocalDescription(offer);
|
|
3384
|
+
const offer = await pcClient.createOffer();
|
|
3385
|
+
offer.sdp = this.mungeSDP(offer.sdp ?? '');
|
|
3386
|
+
await pcClient.setLocalDescription(offer);
|
|
3253
3387
|
this.signallingSrv.send(offer);
|
|
3254
3388
|
}
|
|
3255
3389
|
catch {
|
|
3256
3390
|
Logger.error("Couldn't create offer");
|
|
3257
3391
|
}
|
|
3258
3392
|
}
|
|
3393
|
+
/**
|
|
3394
|
+
* Creates an SDP answer, applies SDP munging, sets it as a local description,
|
|
3395
|
+
* and sends it to the server via signaling.
|
|
3396
|
+
*/
|
|
3397
|
+
async createAnswer(pcClient) {
|
|
3398
|
+
try {
|
|
3399
|
+
const answer = await pcClient.createAnswer();
|
|
3400
|
+
answer.sdp = this.mungeSDP(answer.sdp ?? '');
|
|
3401
|
+
await pcClient.setLocalDescription(answer);
|
|
3402
|
+
this.signallingSrv.send(answer);
|
|
3403
|
+
}
|
|
3404
|
+
catch (e) {
|
|
3405
|
+
Logger.error("Couldn't create answer", e);
|
|
3406
|
+
}
|
|
3407
|
+
}
|
|
3259
3408
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: WebRtcPlayerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
3260
3409
|
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: WebRtcPlayerService }); }
|
|
3261
3410
|
}
|
|
@@ -4458,556 +4607,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImpor
|
|
|
4458
4607
|
type: Injectable
|
|
4459
4608
|
}], ctorParameters: () => [] });
|
|
4460
4609
|
|
|
4461
|
-
|
|
4462
|
-
|
|
4463
|
-
|
|
4464
|
-
|
|
4465
|
-
|
|
4466
|
-
this.videoService = inject(VideoService);
|
|
4467
|
-
this.commandTelemetryService = inject(CommandTelemetryService);
|
|
4468
|
-
this.cfg = {};
|
|
4469
|
-
this.useMic = false;
|
|
4470
|
-
this.forceTURN = false;
|
|
4471
|
-
this.forceMonoAudio = false;
|
|
4472
|
-
/** Whether the remote SDP has been applied to the current peer connection */
|
|
4473
|
-
this.remoteDescriptionSet = false;
|
|
4474
|
-
/** ICE candidates received before the remote description was set */
|
|
4475
|
-
this.iceCandidateBuffer = [];
|
|
4476
|
-
this.isReconnecting = this.store.selectSignal(unrealFeature.selectIsReconnecting);
|
|
4477
|
-
this.init();
|
|
4478
|
-
this.actions$.pipe(ofType(dropDC)).subscribe(() => this.dcClient?.close());
|
|
4479
|
-
this.actions$.pipe(ofType(dropPC)).subscribe(() => this.pcClient?.close());
|
|
4480
|
-
}
|
|
4481
|
-
init() {
|
|
4482
|
-
this.signallingSrv.onConfig$.subscribe((config) => {
|
|
4483
|
-
this.videoService.create();
|
|
4484
|
-
this.setConfig(config);
|
|
4485
|
-
});
|
|
4486
|
-
// Server-initiated offer: set remote description and respond with answer
|
|
4487
|
-
this.signallingSrv.onOffer$.pipe(filter(Truthy)).subscribe((offer) => {
|
|
4488
|
-
void this.handleServerOffer(offer);
|
|
4489
|
-
});
|
|
4490
|
-
// Answer to a client-initiated offer
|
|
4491
|
-
this.signallingSrv.onWebRtcAnswer$
|
|
4492
|
-
.pipe(filter(Truthy))
|
|
4493
|
-
.subscribe((answer) => {
|
|
4494
|
-
void this.handleServerAnswer(answer);
|
|
4495
|
-
});
|
|
4496
|
-
// ICE candidates — buffered until remote description is set
|
|
4497
|
-
this.signallingSrv.onWebRtcIce$.subscribe((candidate) => {
|
|
4498
|
-
this.commandTelemetryService.trackStopCommand('EXT-iceCandidate', true, {
|
|
4499
|
-
multi: true,
|
|
4500
|
-
});
|
|
4501
|
-
if (this.remoteDescriptionSet) {
|
|
4502
|
-
this.handleCandidateFromServer(candidate);
|
|
4503
|
-
}
|
|
4504
|
-
else {
|
|
4505
|
-
this.iceCandidateBuffer.push(candidate);
|
|
4506
|
-
}
|
|
4507
|
-
});
|
|
4508
|
-
}
|
|
4509
|
-
/**
|
|
4510
|
-
* Handles an SDP offer received from the server.
|
|
4511
|
-
* Creates a new peer connection, applies the remote offer,
|
|
4512
|
-
* then responds with an SDP answer.
|
|
4513
|
-
*
|
|
4514
|
-
* The DataChannel is NOT created here — the server creates it,
|
|
4515
|
-
* and we receive it via the `datachannel` event on the peer connection.
|
|
4516
|
-
*/
|
|
4517
|
-
async handleServerOffer(offer) {
|
|
4518
|
-
Logger.info('Received offer from server, creating answer');
|
|
4519
|
-
try {
|
|
4520
|
-
this.remoteDescriptionSet = false;
|
|
4521
|
-
this.iceCandidateBuffer.length = 0;
|
|
4522
|
-
if (!this.pcClient) {
|
|
4523
|
-
this.setupPeerConnection(this.cfg);
|
|
4524
|
-
}
|
|
4525
|
-
// Offer already defines media transceivers — don't add extras
|
|
4526
|
-
await this.pcClient?.setRemoteDescription(offer);
|
|
4527
|
-
this.remoteDescriptionSet = true;
|
|
4528
|
-
this.flushIceCandidateBuffer();
|
|
4529
|
-
await this.createAnswer(this.pcClient);
|
|
4530
|
-
}
|
|
4531
|
-
catch (e) {
|
|
4532
|
-
Logger.error('Failed to handle server offer', e);
|
|
4533
|
-
this.store.dispatch(disconnectStream({
|
|
4534
|
-
reason: DisconnectReason.WebRTCError,
|
|
4535
|
-
message: 'WebRTCError: Failed to handle server offer',
|
|
4536
|
-
}));
|
|
4537
|
-
}
|
|
4538
|
-
}
|
|
4539
|
-
/**
|
|
4540
|
-
* Handles an SDP answer received from the server in response to our offer.
|
|
4541
|
-
* Applies the remote description and flushes any buffered ICE candidates.
|
|
4542
|
-
*/
|
|
4543
|
-
async handleServerAnswer(answer) {
|
|
4544
|
-
Logger.info('Received answer from server');
|
|
4545
|
-
try {
|
|
4546
|
-
await this.pcClient?.setRemoteDescription(answer);
|
|
4547
|
-
this.remoteDescriptionSet = true;
|
|
4548
|
-
this.flushIceCandidateBuffer();
|
|
4549
|
-
}
|
|
4550
|
-
catch (e) {
|
|
4551
|
-
Logger.error('Failed to set remote description from answer', e);
|
|
4552
|
-
}
|
|
4553
|
-
}
|
|
4554
|
-
/** Drains buffered ICE candidates after the remote description has been set */
|
|
4555
|
-
flushIceCandidateBuffer() {
|
|
4556
|
-
for (const candidate of this.iceCandidateBuffer) {
|
|
4557
|
-
this.handleCandidateFromServer(candidate);
|
|
4558
|
-
}
|
|
4559
|
-
this.iceCandidateBuffer.length = 0;
|
|
4560
|
-
}
|
|
4561
|
-
/**
|
|
4562
|
-
* Creates a client-initiated WebRTC offer and sends it to the server.
|
|
4563
|
-
* When the client offers, it also creates the DataChannel.
|
|
4564
|
-
* Used for peer reconnection through the existing signaling connection.
|
|
4565
|
-
*/
|
|
4566
|
-
async createWebRtcOffer() {
|
|
4567
|
-
Logger.info('Creating client-initiated offer');
|
|
4568
|
-
try {
|
|
4569
|
-
this.remoteDescriptionSet = false;
|
|
4570
|
-
this.iceCandidateBuffer.length = 0;
|
|
4571
|
-
if (!this.pcClient) {
|
|
4572
|
-
this.setupPeerConnection(this.cfg);
|
|
4573
|
-
}
|
|
4574
|
-
// Client offers: transceivers first, then DC, then offer
|
|
4575
|
-
await this.setupTransceiversAsync(this.pcClient);
|
|
4576
|
-
this.dcClient = this.createDataChannel(this.pcClient, 'UnrealDcConnection', { ordered: true });
|
|
4577
|
-
await this.createOffer(this.pcClient);
|
|
4578
|
-
}
|
|
4579
|
-
catch (e) {
|
|
4580
|
-
Logger.error('Failed to create WebRTC offer', e);
|
|
4581
|
-
this.store.dispatch(disconnectStream({
|
|
4582
|
-
reason: DisconnectReason.WebRTCError,
|
|
4583
|
-
message: 'WebRTCError: Offer Error',
|
|
4584
|
-
}));
|
|
4585
|
-
}
|
|
4586
|
-
}
|
|
4587
|
-
/**
|
|
4588
|
-
* Initializes a new RTCPeerConnection with event handlers.
|
|
4589
|
-
* Transceivers are NOT added here — the caller is responsible for adding
|
|
4590
|
-
* them at the correct point depending on the negotiation flow.
|
|
4591
|
-
*/
|
|
4592
|
-
setupPeerConnection(config) {
|
|
4593
|
-
this.closePC();
|
|
4594
|
-
Logger.log('Creating new RTCPeerConnection with config');
|
|
4595
|
-
this.pcClient = new RTCPeerConnection(config);
|
|
4596
|
-
this.commandTelemetryService.trackStartCommand('EXT-Browser ICE gathering');
|
|
4597
|
-
this.commandTelemetryService.trackStartCommand('EXT-Browser ICE connection');
|
|
4598
|
-
this.commandTelemetryService.trackStartCommand('EXT-Signaling state change');
|
|
4599
|
-
this.handlePeerConnectionEvents(this.pcClient);
|
|
4600
|
-
this.disconnect$.pipe(take(1)).subscribe(() => {
|
|
4601
|
-
this.closePC();
|
|
4602
|
-
});
|
|
4603
|
-
}
|
|
4604
|
-
createDataChannel(pc, label, options) {
|
|
4605
|
-
const datachannel = pc.createDataChannel(label, options);
|
|
4606
|
-
return this.setupDataChannelCallbacks(datachannel);
|
|
4607
|
-
}
|
|
4608
|
-
handleCandidateFromServer(iceCandidate) {
|
|
4609
|
-
try {
|
|
4610
|
-
const candidate = new RTCIceCandidate(iceCandidate);
|
|
4611
|
-
Logger.info('[Unreal ICE candidate]', '| Type=', candidate.type, '| Protocol=', candidate.protocol, '| Address=', candidate.address, '| Port=', candidate.port, '|');
|
|
4612
|
-
// If forcing TURN, reject any candidates not relay
|
|
4613
|
-
if (this.forceTURN) {
|
|
4614
|
-
if (candidate.candidate.indexOf('relay') < 0) {
|
|
4615
|
-
Logger.warn('Dropping candidate because it was not TURN relay.', '| Type=', candidate.type, '| Protocol=', candidate.protocol, '| Address=', candidate.address, '| Port=', candidate.port, '|');
|
|
4616
|
-
return;
|
|
4617
|
-
}
|
|
4618
|
-
}
|
|
4619
|
-
this.pcClient?.addIceCandidate(candidate).catch((e) => {
|
|
4620
|
-
Logger.warn('Failed to add ICE candidate', e);
|
|
4621
|
-
});
|
|
4622
|
-
}
|
|
4623
|
-
catch (e) {
|
|
4624
|
-
Logger.error('Error handleCandidateFromServer', e);
|
|
4625
|
-
}
|
|
4626
|
-
}
|
|
4627
|
-
/**
|
|
4628
|
-
* Reorders the `m=video` line in the SDP so that payload types matching
|
|
4629
|
-
* the given H.264 `profile-level-id` appear first, making the browser
|
|
4630
|
-
* prefer that profile during codec negotiation.
|
|
4631
|
-
*
|
|
4632
|
-
* A `profile-level-id` is a 3-byte hex string (`XXYYZZ`) defined in
|
|
4633
|
-
* RFC 6184 that encodes three H.264 parameters:
|
|
4634
|
-
* - **Profile IDC** (`XX`) — the H.264 profile.
|
|
4635
|
-
* - **Profile IOP** (`YY`) — constraint flags that further narrow the profile.
|
|
4636
|
-
* - **Level IDC** (`ZZ`) — the level, which caps resolution/framerate/bitrate.
|
|
4637
|
-
*
|
|
4638
|
-
* ### H.264 profiles relevant to Unreal Engine Pixel Streaming
|
|
4639
|
-
*
|
|
4640
|
-
* | profile-level-id | Profile | Level | Notes |
|
|
4641
|
-
* |------------------|----------------------------|-------|----------------------------------------------|
|
|
4642
|
-
* | `42001f` | Baseline | 3.1 | No B-frames, low latency; legacy devices |
|
|
4643
|
-
* | `42e01f` | Constrained Baseline | 3.1 | Widest HW decode support; **recommended** |
|
|
4644
|
-
* | `4d001f` | Main | 3.1 | B-frames + CABAC; better compression |
|
|
4645
|
-
* | `4d4028` | Main | 4.0 | Up to 1080p60; common desktop target |
|
|
4646
|
-
* | `640028` | High | 4.0 | Best quality/compression; most GPUs support |
|
|
4647
|
-
* | `64002a` | High | 4.2 | Up to 1080p120 or 4K30 |
|
|
4648
|
-
* | `640033` | High | 5.1 | Up to 4K60; requires modern HW |
|
|
4649
|
-
* | `640034` | High | 5.2 | Up to 4K120; high-end HW only |
|
|
4650
|
-
* | `6e001f` | High 10 | 3.1 | 10-bit color; limited browser support |
|
|
4651
|
-
* | `f4001f` | High 4:4:4 Predictive | 3.1 | Lossless capable; rare in browsers |
|
|
4652
|
-
*
|
|
4653
|
-
* UE Pixel Streaming defaults to Constrained Baseline (`42e01f`) or High (`6400xx`)
|
|
4654
|
-
* depending on engine version and `WebRTC.H264Profile` CVar.
|
|
4655
|
-
*
|
|
4656
|
-
* We prioritize `42e01f` because it guarantees hardware decoding on
|
|
4657
|
-
* virtually all client devices (desktop, mobile, embedded), avoiding
|
|
4658
|
-
* fallback to software decoding or negotiation of a higher profile
|
|
4659
|
-
* that the client GPU may not accelerate.
|
|
4660
|
-
*/
|
|
4661
|
-
prioritizeH264Profile(sdp, profileLevelId) {
|
|
4662
|
-
const lines = sdp.split('\r\n');
|
|
4663
|
-
const targetPts = [];
|
|
4664
|
-
for (const line of lines) {
|
|
4665
|
-
if (line.startsWith('a=fmtp:') &&
|
|
4666
|
-
line.toLowerCase().includes(`profile-level-id=${profileLevelId}`)) {
|
|
4667
|
-
const pt = line.split(':')[1]?.split(' ')[0];
|
|
4668
|
-
if (pt)
|
|
4669
|
-
targetPts.push(pt);
|
|
4670
|
-
}
|
|
4671
|
-
}
|
|
4672
|
-
if (targetPts.length === 0)
|
|
4673
|
-
return sdp;
|
|
4674
|
-
return lines
|
|
4675
|
-
.map((line) => {
|
|
4676
|
-
if (!line.startsWith('m=video'))
|
|
4677
|
-
return line;
|
|
4678
|
-
const parts = line.split(' ');
|
|
4679
|
-
// parts: m=video <port> <proto> <pt1> <pt2> ...
|
|
4680
|
-
const header = parts.slice(0, 3);
|
|
4681
|
-
const pts = parts.slice(3);
|
|
4682
|
-
const prioritized = [
|
|
4683
|
-
...targetPts.filter((pt) => pts.includes(pt)),
|
|
4684
|
-
...pts.filter((pt) => !targetPts.includes(pt)),
|
|
4685
|
-
];
|
|
4686
|
-
return [...header, ...prioritized].join(' ');
|
|
4687
|
-
})
|
|
4688
|
-
.join('\r\n');
|
|
4689
|
-
}
|
|
4690
|
-
mungeSDP(sdp) {
|
|
4691
|
-
let mungedSDP = this.prioritizeH264Profile(sdp, '42e01f');
|
|
4692
|
-
mungedSDP = mungedSDP.replace(/(a=fmtp:\d+ .*level-asymmetry-allowed=.*)\r\n/gm, '$1;x-google-start-bitrate=10000;x-google-max-bitrate=100000\r\n');
|
|
4693
|
-
// Set max bitrate to highest bitrate Opus supports
|
|
4694
|
-
let audioSDP = 'maxaveragebitrate=510000;';
|
|
4695
|
-
if (this.useMic) {
|
|
4696
|
-
// Set the max capture rate to 48khz (so we can send high-quality audio from mic)
|
|
4697
|
-
audioSDP += 'sprop-maxcapturerate=48000;';
|
|
4698
|
-
}
|
|
4699
|
-
// Force mono or stereo based on whether ?forceMono was passed or not
|
|
4700
|
-
audioSDP += this.forceMonoAudio ? 'stereo=0;' : 'stereo=1;';
|
|
4701
|
-
// Enable in-band forward error correction for opus audio
|
|
4702
|
-
audioSDP += 'useinbandfec=1';
|
|
4703
|
-
// We use the line 'useinbandfec=1' (which Opus uses) to set our Opus specific audio parameters.
|
|
4704
|
-
mungedSDP = mungedSDP?.replace('useinbandfec=1', audioSDP);
|
|
4705
|
-
return mungedSDP;
|
|
4706
|
-
}
|
|
4707
|
-
/**
|
|
4708
|
-
* Closes existing PeerConnection and DataChannel, resets negotiation state.
|
|
4709
|
-
*/
|
|
4710
|
-
closePC() {
|
|
4711
|
-
if (this.pcClient) {
|
|
4712
|
-
Logger.warn('Closing existing PeerConnection');
|
|
4713
|
-
}
|
|
4714
|
-
this.pcClient?.close();
|
|
4715
|
-
this.pcClient = null;
|
|
4716
|
-
if (this.dcClient) {
|
|
4717
|
-
Logger.warn('Closing existing DataChannel');
|
|
4718
|
-
}
|
|
4719
|
-
this.dcClient?.close();
|
|
4720
|
-
this.dcClient = null;
|
|
4721
|
-
this.remoteDescriptionSet = false;
|
|
4722
|
-
this.iceCandidateBuffer.length = 0;
|
|
4723
|
-
}
|
|
4724
|
-
canSend() {
|
|
4725
|
-
return this.dcClient && this.dcClient.readyState === 'open';
|
|
4726
|
-
}
|
|
4727
|
-
send(data) {
|
|
4728
|
-
if (this.canSend()) {
|
|
4729
|
-
this.dcClient?.send(data);
|
|
4730
|
-
}
|
|
4731
|
-
else {
|
|
4732
|
-
Logger.colored('black', 'yellow', `DataChannel connection not prepared or in state: ${this.dcClient?.readyState}`);
|
|
4733
|
-
// Trigger peer reconnection if DC is not open and we're not yet reconnecting
|
|
4734
|
-
this.tryReconnectOnSendFailure();
|
|
4735
|
-
}
|
|
4736
|
-
}
|
|
4737
|
-
/**
|
|
4738
|
-
* Attempts to reconnect the peer when send fails due to DataChannel not being open.
|
|
4739
|
-
* This handles cases where the DC connection is broken, but no error events are fired.
|
|
4740
|
-
*/
|
|
4741
|
-
tryReconnectOnSendFailure() {
|
|
4742
|
-
// Skip if already reconnecting or signaling is not connected
|
|
4743
|
-
if (this.isReconnecting()) {
|
|
4744
|
-
Logger.info('[Reconnect] Already reconnecting, skipping duplicate attempt');
|
|
4745
|
-
return;
|
|
4746
|
-
}
|
|
4747
|
-
if (!this.signallingSrv.isConnected()) {
|
|
4748
|
-
Logger.warn('[Reconnect] Cannot reconnect: Signaling not connected');
|
|
4749
|
-
return;
|
|
4750
|
-
}
|
|
4751
|
-
// Check if DC exists but is in a non-open state (closing, closed, or connecting stuck)
|
|
4752
|
-
const dcState = this.dcClient?.readyState;
|
|
4753
|
-
if (dcState && dcState !== 'open') {
|
|
4754
|
-
Logger.warn(`[Reconnect] DataChannel in "${dcState}" state during send, initiating peer reconnection`);
|
|
4755
|
-
this.store.dispatch(reconnectPeer({ attempt: 1, maxAttempts: 3, delay: 0 }));
|
|
4756
|
-
}
|
|
4757
|
-
}
|
|
4758
|
-
/**
|
|
4759
|
-
* Reconnects only the WebRTC peer connection without re-establishing signaling.
|
|
4760
|
-
* Closes the current PeerConnection and creates a new client-initiated offer
|
|
4761
|
-
* through the existing WebSocket connection.
|
|
4762
|
-
* @returns true if reconnection was initiated, false if signaling is not connected
|
|
4763
|
-
*/
|
|
4764
|
-
reconnectPeer() {
|
|
4765
|
-
if (!this.signallingSrv.isConnected()) {
|
|
4766
|
-
Logger.error('[Reconnect] Cannot reconnect peer: Signaling not connected');
|
|
4767
|
-
return false;
|
|
4768
|
-
}
|
|
4769
|
-
if (!this.cfg || Object.keys(this.cfg).length === 0) {
|
|
4770
|
-
Logger.error('[Reconnect] Cannot reconnect peer: No config available');
|
|
4771
|
-
return false;
|
|
4772
|
-
}
|
|
4773
|
-
Logger.info('[Reconnect] Initiating peer reconnection');
|
|
4774
|
-
// Close only the PeerConnection, keep WebSocket alive
|
|
4775
|
-
this.closePC();
|
|
4776
|
-
// Create a new WebRTC offer using the stored config
|
|
4777
|
-
void this.createWebRtcOffer();
|
|
4778
|
-
return true;
|
|
4779
|
-
}
|
|
4780
|
-
setConfig(parOptions = {}) {
|
|
4781
|
-
const urlParams = new URLSearchParams(window.location.search);
|
|
4782
|
-
//**********************
|
|
4783
|
-
//Config setup
|
|
4784
|
-
//**********************
|
|
4785
|
-
this.cfg = { ...parOptions.peerConnectionOptions };
|
|
4786
|
-
this.forceTURN = urlParams.has('ForceTURN');
|
|
4787
|
-
if (this.forceTURN) {
|
|
4788
|
-
Logger.info('Forcing TURN usage by setting ICE Transport Policy in peer connection config.');
|
|
4789
|
-
this.cfg.iceTransportPolicy = 'relay';
|
|
4790
|
-
}
|
|
4791
|
-
this.cfg.bundlePolicy = urlParams.has('ForceMaxBundle')
|
|
4792
|
-
? 'max-bundle'
|
|
4793
|
-
: 'balanced';
|
|
4794
|
-
this.cfg.iceCandidatePoolSize = 1;
|
|
4795
|
-
// When ?useMic check for SSL or localhost
|
|
4796
|
-
const isLocalhostConnection = location.hostname === 'localhost' || location.hostname === '127.0.0.1';
|
|
4797
|
-
const isHttpsConnection = location.protocol === 'https:';
|
|
4798
|
-
if (this.useMic && !isLocalhostConnection && !isHttpsConnection) {
|
|
4799
|
-
this.useMic = false;
|
|
4800
|
-
Logger.error('Microphone access in the browser will not work if you are not on HTTPS or localhost. Disabling mic access.');
|
|
4801
|
-
Logger.error("For testing you can enable HTTP microphone access Chrome by visiting chrome://flags/ and enabling 'unsafely-treat-insecure-origin-as-secure'");
|
|
4802
|
-
}
|
|
4803
|
-
this.setupPeerConnection(this.cfg);
|
|
4804
|
-
}
|
|
4805
|
-
handlePeerConnectionEvents(pcClient) {
|
|
4806
|
-
fromEvent(pcClient, 'signalingstatechange')
|
|
4807
|
-
.pipe(map$1(() => this.pcClient?.signalingState), takeUntil(this.disconnect$))
|
|
4808
|
-
.subscribe((state) => {
|
|
4809
|
-
Logger.info('[Signaling state change] |', state, '|');
|
|
4810
|
-
this.commandTelemetryService.trackStopCommand('EXT-Signaling state change', true, { state, multi: true });
|
|
4811
|
-
});
|
|
4812
|
-
fromEvent(pcClient, 'iceconnectionstatechange')
|
|
4813
|
-
.pipe(map$1(() => this.pcClient?.iceConnectionState), takeUntil(this.disconnect$))
|
|
4814
|
-
.subscribe((state) => {
|
|
4815
|
-
Logger.info('[Browser ICE connection] |', state, '|');
|
|
4816
|
-
this.commandTelemetryService.trackStopCommand('EXT-Browser ICE connection', true, { state, multi: true });
|
|
4817
|
-
});
|
|
4818
|
-
fromEvent(pcClient, 'icegatheringstatechange')
|
|
4819
|
-
.pipe(map$1(() => this.pcClient?.iceGatheringState), takeUntil(this.disconnect$))
|
|
4820
|
-
.subscribe((state) => {
|
|
4821
|
-
Logger.info('[Browser ICE gathering] |', state, '|');
|
|
4822
|
-
this.commandTelemetryService.trackStopCommand('EXT-Browser ICE gathering', true, { state, multi: true });
|
|
4823
|
-
});
|
|
4824
|
-
fromEvent(pcClient, 'connectionstatechange')
|
|
4825
|
-
.pipe(map$1(() => this.pcClient?.connectionState), takeUntil(this.disconnect$))
|
|
4826
|
-
.subscribe((state) => {
|
|
4827
|
-
Logger.info('[RTCPeerConnection state] |', state, '|');
|
|
4828
|
-
});
|
|
4829
|
-
fromEvent(pcClient, 'icecandidate')
|
|
4830
|
-
.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$))
|
|
4831
|
-
.subscribe((candidate) => this.signallingSrv.send({ type: 'iceCandidate', candidate }));
|
|
4832
|
-
// When the server creates the DataChannel (server-offer flow),
|
|
4833
|
-
// we receive it here via the 'datachannel' event
|
|
4834
|
-
fromEvent(pcClient, 'datachannel')
|
|
4835
|
-
.pipe(tap$1((e) => Logger.info(`[DATACHANNEL] Received remote DC: label="${e.channel.label}" id=${e.channel.id} protocol="${e.channel.protocol}" ordered=${e.channel.ordered}`)), map$1((e) => e.channel), takeUntil(this.disconnect$))
|
|
4836
|
-
.subscribe((datachannel) => {
|
|
4837
|
-
this.dcClient = this.setupDataChannelCallbacks(datachannel);
|
|
4838
|
-
});
|
|
4839
|
-
const onTrack$ = fromEvent(pcClient, 'track').pipe(takeUntil(this.disconnect$), filter((e) => !!e.track), tap$1((e) => Logger.info('Got track - ' +
|
|
4840
|
-
e.track.kind +
|
|
4841
|
-
' id=' +
|
|
4842
|
-
e.track.id +
|
|
4843
|
-
' readyState=' +
|
|
4844
|
-
e.track.readyState)), share());
|
|
4845
|
-
onTrack$
|
|
4846
|
-
.pipe(filter((e) => e.track.kind === 'audio'), map$1((e) => e.streams[0]))
|
|
4847
|
-
.subscribe((stream) => this.videoService.attachAudioStream(stream));
|
|
4848
|
-
onTrack$
|
|
4849
|
-
.pipe(filter((e) => e.track.kind === 'video'), map$1((e) => e.streams[0]), tap$1(() => Logger.info('Set video source from video track ontrack.')))
|
|
4850
|
-
.subscribe((stream) => {
|
|
4851
|
-
if (this.pcClient) {
|
|
4852
|
-
this.videoService.attachVideoStream(stream, this.pcClient);
|
|
4853
|
-
}
|
|
4854
|
-
});
|
|
4855
|
-
}
|
|
4856
|
-
async setupTransceiversAsync(pc) {
|
|
4857
|
-
const hasTransceivers = pc.getTransceivers().length > 0;
|
|
4858
|
-
// Set up a transceiver for getting UE video
|
|
4859
|
-
pc.addTransceiver('video', { direction: 'recvonly' });
|
|
4860
|
-
// Set up a transceiver for sending mic audio to UE and receiving audio from UE
|
|
4861
|
-
if (!this.useMic) {
|
|
4862
|
-
pc.addTransceiver('audio', { direction: 'recvonly' });
|
|
4863
|
-
}
|
|
4864
|
-
else {
|
|
4865
|
-
const audioSendOptions = this.useMic
|
|
4866
|
-
? {
|
|
4867
|
-
autoGainControl: false,
|
|
4868
|
-
channelCount: 1,
|
|
4869
|
-
echoCancellation: false,
|
|
4870
|
-
latency: 0,
|
|
4871
|
-
noiseSuppression: false,
|
|
4872
|
-
sampleRate: 48000,
|
|
4873
|
-
sampleSize: 16,
|
|
4874
|
-
volume: 1.0,
|
|
4875
|
-
}
|
|
4876
|
-
: false;
|
|
4877
|
-
// Note using mic on android chrome requires SSL or chrome://flags/ "unsafely-treat-insecure-origin-as-secure"
|
|
4878
|
-
const stream = await navigator.mediaDevices.getUserMedia({
|
|
4879
|
-
video: false,
|
|
4880
|
-
audio: audioSendOptions,
|
|
4881
|
-
});
|
|
4882
|
-
if (stream) {
|
|
4883
|
-
if (hasTransceivers) {
|
|
4884
|
-
for (const transceiver of pc.getTransceivers()) {
|
|
4885
|
-
if (transceiver &&
|
|
4886
|
-
transceiver.receiver &&
|
|
4887
|
-
transceiver.receiver.track &&
|
|
4888
|
-
transceiver.receiver.track.kind === 'audio') {
|
|
4889
|
-
for (const track of stream.getTracks()) {
|
|
4890
|
-
if (track.kind && track.kind === 'audio') {
|
|
4891
|
-
await transceiver.sender.replaceTrack(track);
|
|
4892
|
-
transceiver.direction = 'sendrecv';
|
|
4893
|
-
}
|
|
4894
|
-
}
|
|
4895
|
-
}
|
|
4896
|
-
}
|
|
4897
|
-
}
|
|
4898
|
-
else {
|
|
4899
|
-
for (const track of stream.getTracks()) {
|
|
4900
|
-
if (track.kind && track.kind === 'audio') {
|
|
4901
|
-
pc.addTransceiver(track, { direction: 'sendrecv' });
|
|
4902
|
-
}
|
|
4903
|
-
}
|
|
4904
|
-
}
|
|
4905
|
-
}
|
|
4906
|
-
else {
|
|
4907
|
-
pc.addTransceiver('audio', { direction: 'recvonly' });
|
|
4908
|
-
}
|
|
4909
|
-
}
|
|
4910
|
-
}
|
|
4911
|
-
setupDataChannelCallbacks(datachannel) {
|
|
4912
|
-
try {
|
|
4913
|
-
Logger.log(`[DATACHANNEL] Setting up datachannel "${datachannel.label}" callbacks...`);
|
|
4914
|
-
this.commandTelemetryService.trackStartCommand('EXT-DataChannel Connection');
|
|
4915
|
-
// Inform browser we would like binary data as an ArrayBuffer (FF chooses Blob by default!)
|
|
4916
|
-
datachannel.binaryType = 'arraybuffer';
|
|
4917
|
-
fromEvent(datachannel, 'close')
|
|
4918
|
-
.pipe(
|
|
4919
|
-
// Only dispatch disconnect if not already reconnecting to avoid infinite loops
|
|
4920
|
-
filter(() => !this.isReconnecting()), take(1))
|
|
4921
|
-
.subscribe((e) => {
|
|
4922
|
-
Logger.warn(`[DATACHANNEL] Data channel disconnected: ${datachannel.label}(${datachannel.id})`, e);
|
|
4923
|
-
this.store.dispatch(disconnectStream({
|
|
4924
|
-
reason: DisconnectReason.DataChannelClosed,
|
|
4925
|
-
message: 'DataChannelClosed',
|
|
4926
|
-
}));
|
|
4927
|
-
});
|
|
4928
|
-
fromEvent(datachannel, 'error')
|
|
4929
|
-
.pipe(takeUntil(this.disconnect$))
|
|
4930
|
-
.subscribe((e) => {
|
|
4931
|
-
const message = `[DATACHANNEL] Data channel "${datachannel.label}" error. id:${datachannel.id}`;
|
|
4932
|
-
Logger.error(message, e);
|
|
4933
|
-
});
|
|
4934
|
-
fromEvent(datachannel, 'open')
|
|
4935
|
-
.pipe(tap$1(() => {
|
|
4936
|
-
Logger.info(`[DATACHANNEL] Data channel "${datachannel.label}" connected. id:${datachannel.id}`);
|
|
4937
|
-
}), takeUntil(this.disconnect$))
|
|
4938
|
-
.subscribe(() => {
|
|
4939
|
-
this.commandTelemetryService.trackStopCommand('EXT-DataChannel Connection');
|
|
4940
|
-
this.onDataChannelConnected();
|
|
4941
|
-
});
|
|
4942
|
-
fromEvent(datachannel, 'message')
|
|
4943
|
-
.pipe(
|
|
4944
|
-
/* tap((e: MessageEvent) => {
|
|
4945
|
-
const view = new Uint8Array(e.data);
|
|
4946
|
-
Logger.info(
|
|
4947
|
-
`[DATACHANNEL] Message received on "${datachannel.label}" id=${datachannel.id}, byteCode=${view[0]}, size=${view.byteLength}`,
|
|
4948
|
-
);
|
|
4949
|
-
}),*/
|
|
4950
|
-
map$1((e) => e.data), takeUntil(this.disconnect$))
|
|
4951
|
-
.subscribe((data) => this.onDataChannelMessage$.next(data));
|
|
4952
|
-
return datachannel;
|
|
4953
|
-
}
|
|
4954
|
-
catch (e) {
|
|
4955
|
-
Logger.warn('[DATACHANNEL] Datachannel setup caused an exception: ', e);
|
|
4956
|
-
return null;
|
|
4957
|
-
}
|
|
4958
|
-
}
|
|
4959
|
-
onDataChannelConnected() {
|
|
4960
|
-
let statusMessage = 'Session connected, video stream starting.';
|
|
4961
|
-
if (navigator.userAgent.includes('Firefox')) {
|
|
4962
|
-
statusMessage +=
|
|
4963
|
-
'\n If you are experiencing connection problems please try Google Chrome';
|
|
4964
|
-
}
|
|
4965
|
-
this.store.dispatch(dataChannelConnected({ statusMessage }));
|
|
4966
|
-
}
|
|
4967
|
-
/**
|
|
4968
|
-
* Creates an SDP offer, applies SDP munging, sets it as a local description,
|
|
4969
|
-
* and sends it to the server via signaling.
|
|
4970
|
-
* Transceivers already configure media directions, so deprecated
|
|
4971
|
-
* offerToReceive* options are not used.
|
|
4972
|
-
*/
|
|
4973
|
-
async createOffer(pcClient) {
|
|
4974
|
-
try {
|
|
4975
|
-
const offer = await pcClient.createOffer();
|
|
4976
|
-
offer.sdp = this.mungeSDP(offer.sdp ?? '');
|
|
4977
|
-
await pcClient.setLocalDescription(offer);
|
|
4978
|
-
this.signallingSrv.send(offer);
|
|
4979
|
-
}
|
|
4980
|
-
catch {
|
|
4981
|
-
Logger.error("Couldn't create offer");
|
|
4982
|
-
}
|
|
4983
|
-
}
|
|
4984
|
-
/**
|
|
4985
|
-
* Creates an SDP answer, applies SDP munging, sets it as a local description,
|
|
4986
|
-
* and sends it to the server via signaling.
|
|
4987
|
-
*/
|
|
4988
|
-
async createAnswer(pcClient) {
|
|
4989
|
-
try {
|
|
4990
|
-
const answer = await pcClient.createAnswer();
|
|
4991
|
-
answer.sdp = this.mungeSDP(answer.sdp ?? '');
|
|
4992
|
-
await pcClient.setLocalDescription(answer);
|
|
4993
|
-
this.signallingSrv.send(answer);
|
|
4994
|
-
}
|
|
4995
|
-
catch (e) {
|
|
4996
|
-
Logger.error("Couldn't create answer", e);
|
|
4997
|
-
}
|
|
4998
|
-
}
|
|
4999
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: WebRtcPlayerService2, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
5000
|
-
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: WebRtcPlayerService2 }); }
|
|
5001
|
-
}
|
|
5002
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: WebRtcPlayerService2, decorators: [{
|
|
5003
|
-
type: Injectable
|
|
5004
|
-
}], ctorParameters: () => [] });
|
|
5005
|
-
|
|
5006
|
-
const ReceivedMimeTypes = {
|
|
5007
|
-
ApplicationJson: 'application/json',
|
|
5008
|
-
};
|
|
5009
|
-
|
|
5010
|
-
class FileHandlerService {
|
|
4610
|
+
const ReceivedMimeTypes = {
|
|
4611
|
+
ApplicationJson: 'application/json',
|
|
4612
|
+
};
|
|
4613
|
+
|
|
4614
|
+
class FileHandlerService {
|
|
5011
4615
|
constructor() {
|
|
5012
4616
|
this.fileService = inject(FileReceiverService);
|
|
5013
4617
|
this.fileHandlers = {
|
|
@@ -5212,8 +4816,14 @@ class FpsMonitorService {
|
|
|
5212
4816
|
this.videoService = inject(VideoService);
|
|
5213
4817
|
this.store = inject(Store);
|
|
5214
4818
|
this.unrealConfig = inject(UNREAL_CONFIG, { optional: true });
|
|
5215
|
-
/** Discrete FPS targets the controller can switch between (ascending order). */
|
|
5216
|
-
this.FPS_STEPS = [30, 40, 50,
|
|
4819
|
+
/** Discrete FPS targets the controller can switch between (ascending order). Last 0 is unlimited framerate indicator*/
|
|
4820
|
+
this.FPS_STEPS = [30, 40, 50, 0];
|
|
4821
|
+
/**
|
|
4822
|
+
* Reference FPS used to compute the FPS ratio when `currentFpsTarget` is 0
|
|
4823
|
+
* (unlimited). Keeps adaptive logic operational at the unlimited step:
|
|
4824
|
+
* if the stream drops well below this value, fuzzy rules can still fire.
|
|
4825
|
+
*/
|
|
4826
|
+
this.UNLIMITED_REFERENCE_FPS = 60;
|
|
5217
4827
|
/** Reference ceiling for bitrate (bits/sec) used to normalize a bitrate load to 0..1. */
|
|
5218
4828
|
this.MAX_BITRATE = 20_000_000;
|
|
5219
4829
|
/**
|
|
@@ -5311,7 +4921,8 @@ class FpsMonitorService {
|
|
|
5311
4921
|
* - `quality` = mapQpToQuality(avgQP) → 'lime' | 'orange' | 'red'
|
|
5312
4922
|
*/
|
|
5313
4923
|
evaluateDecision(avg, currentTarget) {
|
|
5314
|
-
const
|
|
4924
|
+
const referenceTarget = currentTarget > 0 ? currentTarget : this.UNLIMITED_REFERENCE_FPS;
|
|
4925
|
+
const fpsRatio = avg.fps / referenceTarget;
|
|
5315
4926
|
const bitrateLoad = avg.bitrate / this.MAX_BITRATE;
|
|
5316
4927
|
const fpsLow = this.membershipFpsLow(fpsRatio);
|
|
5317
4928
|
const fpsOk = this.membershipFpsOk(fpsRatio);
|
|
@@ -5336,10 +4947,14 @@ class FpsMonitorService {
|
|
|
5336
4947
|
}
|
|
5337
4948
|
// Rule 4: FPS meets/exceeds target (ratio >= ~0.95), quality is 'lime'
|
|
5338
4949
|
// (QP <= 26), and bitrate is below 50% of ceiling — safe to step up.
|
|
4950
|
+
// Note: the last FPS_STEPS value (0) means "no limit", so compare by
|
|
4951
|
+
// position in FPS_STEPS rather than numerically.
|
|
4952
|
+
const currentIndex = this.FPS_STEPS.indexOf(this.currentFpsTarget);
|
|
5339
4953
|
if (fpsHigh > 0.5 &&
|
|
5340
4954
|
quality === 'lime' &&
|
|
5341
4955
|
bitrateHigh < 0.5 &&
|
|
5342
|
-
|
|
4956
|
+
currentIndex !== -1 &&
|
|
4957
|
+
currentIndex < this.FPS_STEPS.length - 1) {
|
|
5343
4958
|
return 'increase';
|
|
5344
4959
|
}
|
|
5345
4960
|
return 'hold';
|
|
@@ -6085,7 +5700,6 @@ class UnrealSceneComponent {
|
|
|
6085
5700
|
this.changeMouseOverScene.emit(false);
|
|
6086
5701
|
}
|
|
6087
5702
|
constructor() {
|
|
6088
|
-
this.unrealConfig = inject(UNREAL_CONFIG);
|
|
6089
5703
|
this.isStudio = input(false, ...(ngDevMode ? [{ debugName: "isStudio" }] : /* istanbul ignore next */ []));
|
|
6090
5704
|
this.useContainerAsSizeProvider = input(true, ...(ngDevMode ? [{ debugName: "useContainerAsSizeProvider" }] : /* istanbul ignore next */ []));
|
|
6091
5705
|
this.studioResolutionSize = input({
|
|
@@ -6188,16 +5802,7 @@ class UnrealSceneComponent {
|
|
|
6188
5802
|
containerHeight = this.videoElement()?.nativeElement.offsetHeight || 2;
|
|
6189
5803
|
}
|
|
6190
5804
|
const pixelRatio = this.pixelRatio;
|
|
6191
|
-
|
|
6192
|
-
// New Approach
|
|
6193
|
-
return getResizeValuesUniversal({
|
|
6194
|
-
pixelRatio,
|
|
6195
|
-
containerWidth,
|
|
6196
|
-
containerHeight,
|
|
6197
|
-
maxResolution: this.maxResolution,
|
|
6198
|
-
});
|
|
6199
|
-
}
|
|
6200
|
-
return getResizeValuesUniversalOld({
|
|
5805
|
+
return getResizeValuesUniversal({
|
|
6201
5806
|
pixelRatio,
|
|
6202
5807
|
containerWidth,
|
|
6203
5808
|
containerHeight,
|
|
@@ -6206,17 +5811,7 @@ class UnrealSceneComponent {
|
|
|
6206
5811
|
}
|
|
6207
5812
|
getResizeStudioValues() {
|
|
6208
5813
|
const videoElement = this.videoElement()?.nativeElement;
|
|
6209
|
-
|
|
6210
|
-
return getResizeStudioValues({
|
|
6211
|
-
pixelRatio: this.pixelRatio,
|
|
6212
|
-
containerWidth: videoElement?.offsetWidth || 2,
|
|
6213
|
-
containerHeight: videoElement?.offsetHeight || 2,
|
|
6214
|
-
studioWidth: this.width,
|
|
6215
|
-
studioHeight: this.height,
|
|
6216
|
-
maxResolution: this.maxResolution,
|
|
6217
|
-
});
|
|
6218
|
-
}
|
|
6219
|
-
return getResizeStudioValuesOld({
|
|
5814
|
+
return getResizeStudioValues({
|
|
6220
5815
|
pixelRatio: this.pixelRatio,
|
|
6221
5816
|
containerWidth: videoElement?.offsetWidth || 2,
|
|
6222
5817
|
containerHeight: videoElement?.offsetHeight || 2,
|
|
@@ -6464,15 +6059,7 @@ function provideUnrealModule() {
|
|
|
6464
6059
|
SignallingService,
|
|
6465
6060
|
StreamStatusTelemetryService,
|
|
6466
6061
|
FpsMonitorService,
|
|
6467
|
-
|
|
6468
|
-
provide: WebRtcPlayerService,
|
|
6469
|
-
useFactory: () => {
|
|
6470
|
-
const config = inject(UNREAL_CONFIG, { optional: true });
|
|
6471
|
-
return config?.newWebRtcConnector
|
|
6472
|
-
? new WebRtcPlayerService2()
|
|
6473
|
-
: new WebRtcPlayerService();
|
|
6474
|
-
},
|
|
6475
|
-
},
|
|
6062
|
+
WebRtcPlayerService,
|
|
6476
6063
|
provideEnvironmentInitializer(() => {
|
|
6477
6064
|
inject(AggregatorService);
|
|
6478
6065
|
inject(InputService);
|
|
@@ -6490,5 +6077,5 @@ function provideUnrealModule() {
|
|
|
6490
6077
|
* Generated bundle index. Do not edit.
|
|
6491
6078
|
*/
|
|
6492
6079
|
|
|
6493
|
-
export { AFKService, AfkTimeoutModalComponent, AggregatorService, AnalyticsService, AnswerHandler, CONSOLE_COMMAND_DISABLE_MESSAGES, CONSOLE_COMMAND_ENABLE_MESSAGES, CONSOLE_COMMAND_PIXEL_QUALITY, CommandTelemetryService, CommandsEffects, ConfigHandler, ConsoleExtensionsService, ControlScheme, DATA_CHANNEL_CONNECTION_TIMEOUT, DEBOUNCE_TO_MANY_RESIZE_CALLS, DEFAULT_AFK_TIMEOUT, DEFAULT_AFK_TIMEOUT_PERIOD, DEFAULT_RECONNECT_DELAY_MS, DEFAULT_RECONNECT_ENABLED, DEFAULT_RECONNECT_MAX_ATTEMPTS, DEFAULT_RECONNECT_ON_DATACHANNEL_CLOSE, DEFAULT_RECONNECT_ON_ICE_FAILURE, DataFlowMonitor, DevModeService, DisconnectReason, FULL_HD_HEIGHT, FULL_HD_WIDTH, FileHandlerService, FileReceiverService, FilterSettingsComponent, FpsMonitorService, FreezeFrameComponent, FreezeFrameService, IceCandidateHandler, ImageLoadingSrcComponent, InputOptions, InputService, InstanceReadyHandler, InstanceReservedHandler, IntroSrcComponent, KalmanFilter1D, LatencyTimings, LowBandwidthDetectorComponent, LowBandwidthModalComponent, MINIMAL_FPS, MessageType, MouseButton, MouseButtonsMask, OnCloseHandler, OnErrorHandler, OnMessageHandler, OnOpenHandler, OrchestrationMessageTypes, POLLING_TIME, PingHandler, PlayerCountHandler, RegionsPingService, SAME_SIZE_THRESHOLD, SCREEN_LOCKER_CONTAINER_ID, SIGNALLING_PERCENT_VALUE, SSInfoHandler, STREAMING_VIDEO_ID, SafePipe, SignallingService, SpecialKeyCodes, StatGraphComponent, StreamStatusTelemetryService, SubService, ToClientMessage, UNREAL_CONFIG, UnrealCallbackService, UnrealCommunicatorService, UnrealEffects, UnrealSceneComponent, UnrealStatusMessage, VideoService, VideoStatsComponent, WSCloseCode_CIRRUS_ABNORMAL_CLOSURE, WSCloseCode_CIRRUS_MAX_PLAYERS_ERROR, WSCloseCode_CIRRUS_PLAYER_DISCONNECTED, WSCloseCode_CIRRUS_STREAMER_KIKED_PLAYER, WSCloseCode_FORCE_CIRRUS_CLOSE, WSCloseCode_NORMAL_AFK_TIMEOUT, WSCloseCode_NORMAL_CLOSURE, WSCloseCode_NORMAL_MANUAL_DISCONNECT, WSCloseCode_UNKNOWN, WSCloseCodes, WS_OPEN_STATE, WS_TIMEOUT, WebRtcPlayerService,
|
|
6080
|
+
export { AFKService, AfkTimeoutModalComponent, AggregatorService, AnalyticsService, AnswerHandler, CONSOLE_COMMAND_DISABLE_MESSAGES, CONSOLE_COMMAND_ENABLE_MESSAGES, CONSOLE_COMMAND_PIXEL_QUALITY, CommandTelemetryService, CommandsEffects, ConfigHandler, ConsoleExtensionsService, ControlScheme, DATA_CHANNEL_CONNECTION_TIMEOUT, DEBOUNCE_TO_MANY_RESIZE_CALLS, DEFAULT_AFK_TIMEOUT, DEFAULT_AFK_TIMEOUT_PERIOD, DEFAULT_RECONNECT_DELAY_MS, DEFAULT_RECONNECT_ENABLED, DEFAULT_RECONNECT_MAX_ATTEMPTS, DEFAULT_RECONNECT_ON_DATACHANNEL_CLOSE, DEFAULT_RECONNECT_ON_ICE_FAILURE, DataFlowMonitor, DevModeService, DisconnectReason, FULL_HD_HEIGHT, FULL_HD_WIDTH, FileHandlerService, FileReceiverService, FilterSettingsComponent, FpsMonitorService, FreezeFrameComponent, FreezeFrameService, IceCandidateHandler, ImageLoadingSrcComponent, InputOptions, InputService, InstanceReadyHandler, InstanceReservedHandler, IntroSrcComponent, KalmanFilter1D, LatencyTimings, LowBandwidthDetectorComponent, LowBandwidthModalComponent, MINIMAL_FPS, MessageType, MouseButton, MouseButtonsMask, OnCloseHandler, OnErrorHandler, OnMessageHandler, OnOpenHandler, OrchestrationMessageTypes, POLLING_TIME, PingHandler, PlayerCountHandler, RegionsPingService, SAME_SIZE_THRESHOLD, SCREEN_LOCKER_CONTAINER_ID, SIGNALLING_PERCENT_VALUE, SSInfoHandler, STREAMING_VIDEO_ID, SafePipe, SignallingService, SpecialKeyCodes, StatGraphComponent, StreamStatusTelemetryService, SubService, ToClientMessage, UNREAL_CONFIG, UnrealCallbackService, UnrealCommunicatorService, UnrealEffects, UnrealSceneComponent, UnrealStatusMessage, VideoService, VideoStatsComponent, WSCloseCode_CIRRUS_ABNORMAL_CLOSURE, WSCloseCode_CIRRUS_MAX_PLAYERS_ERROR, WSCloseCode_CIRRUS_PLAYER_DISCONNECTED, WSCloseCode_CIRRUS_STREAMER_KIKED_PLAYER, WSCloseCode_FORCE_CIRRUS_CLOSE, WSCloseCode_NORMAL_AFK_TIMEOUT, WSCloseCode_NORMAL_CLOSURE, WSCloseCode_NORMAL_MANUAL_DISCONNECT, WSCloseCode_UNKNOWN, WSCloseCodes, WS_OPEN_STATE, WS_TIMEOUT, WebRtcPlayerService, WebrtcErrorModalComponent, abortEstablishingConnection, changeLowBandwidth, changeStatusMainVideoOnScene, changeStreamResolutionAction, changeStreamResolutionSuccessAction, commandCompleted, commandStarted, commandsFeature, commandsReducer, dataChannelConnected, dataChannelReady, decodeData, destroyRemoteConnections, destroyUnrealScene, disconnectStream, dispatchResize, dropConnection, dropDC, dropPC, floatToSmoothPercents, forceResizeViewport, fromResizeObserver, getActiveUrl, getImageFromVideoStream, getResizeStudioValues, getResizeStudioValuesOld, getResizeValuesUniversal, getResizeValuesUniversalOld, getRtcErrorMessage, iceConnectionFailed, initSignalling, keepMaxUntilReset, makeEven, provideUnrealModule, qpToQuality, reconnectPeer, reconnectPeerFailed, reconnectPeerSuccess, resetAfk, resetConfig, resetDataChannelForReconnect, resetIntroSrc, resetWarnTimeout, saveAnalyticsEvent, selectClientAndViewIds, selectCommandProgress, selectCommandsInProgress, selectFreezeFrameCombinedDataUrl, selectFreezeFrameDataUrl, selectFreezeFrameDataUrlFromVideo, selectFreezeFrameProgressMessageFromVideo, selectIsAutostart, selectIsExistMatchUrls, selectIsFreezeFrameLoading, selectIsLoaderScreenVisible, selectIsVideoPlayingAndDataChannelConnected, selectShowLoader, selectShowReconnectPopup, selectSignalingParameters, selectStreamConfig, selectTotalProgress, selectWarnTimeout, setAfkTimerCountdown, setAfkTimerHide, setAfkTimerVisible, setAwsInstance, setCirrusConnected, setCirrusDisconnected, setConfig, setDataChannelConnected, setEstablishingConnection, setFreezeFrame, setFreezeFrameFromVideo, setIntroImageSrc, setIntroVideoSrc, setKalmanSettings, setLoadingImageSrc, setLoopBackCommandIsCompleted, setMaxFps, setOrchestrationContext, setOrchestrationMessage, setOrchestrationParameters, setOrchestrationProgress, setSignalingName, setStatusMessage, setStatusPercentSignallingServer, setStreamClientCompanyId, setStreamViewId, setUnrealPlaywrightConfig, setViewportNotReady, setViewportReady, showUnrealErrorMessage, smoothTransition, startStream, trackMixpanelEvent, unrealCommandCallback, unrealFeature, unrealReducer, updateCirrusInfo, videoAdaptedToContainer, videoStreamInitialized };
|
|
6494
6081
|
//# sourceMappingURL=3dsource-angular-unreal-module.mjs.map
|