@epicgames-ps/lib-pixelstreamingfrontend-ue5.5 0.0.10 → 0.0.12
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.
- package/dist/lib-pixelstreamingfrontend.esm.js +1 -1
- package/dist/lib-pixelstreamingfrontend.js +1 -1
- package/package.json +4 -3
- package/src/Config/Config.ts +15 -1
- package/src/Config/SettingOption.ts +1 -1
- package/src/Inputs/TouchController.ts +2 -2
- package/src/PeerConnectionController/AggregatedStats.ts +34 -13
- package/src/PeerConnectionController/CandidatePairStats.ts +10 -3
- package/src/PeerConnectionController/CandidateStat.ts +4 -2
- package/src/PeerConnectionController/PeerConnectionController.ts +18 -2
- package/src/PixelStreaming/PixelStreaming.test.ts +33 -50
- package/src/PixelStreaming/PixelStreaming.ts +34 -1
- package/src/Util/EventEmitter.ts +12 -1
- package/src/VideoPlayer/StreamController.ts +6 -0
- package/src/WebRtcPlayer/WebRtcPlayerController.ts +3 -2
- package/src/__test__/mockWebSocket.ts +5 -5
- package/types/Config/Config.d.ts +1 -0
- package/types/PeerConnectionController/AggregatedStats.d.ts +8 -1
- package/types/PeerConnectionController/CandidatePairStats.d.ts +10 -3
- package/types/PeerConnectionController/CandidateStat.d.ts +4 -2
- package/types/PeerConnectionController/PeerConnectionController.d.ts +2 -0
- package/types/PixelStreaming/PixelStreaming.d.ts +2 -1
- package/types/Util/EventEmitter.d.ts +8 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@epicgames-ps/lib-pixelstreamingfrontend-ue5.5",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.12",
|
|
4
4
|
"description": "Frontend library for Unreal Engine 5.5 Pixel Streaming",
|
|
5
5
|
"main": "dist/lib-pixelstreamingfrontend.js",
|
|
6
6
|
"module": "dist/lib-pixelstreamingfrontend.esm.js",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"spellcheck": "cspell \"{README.md,.github/*.md,src/**/*.ts}\""
|
|
17
17
|
},
|
|
18
18
|
"devDependencies": {
|
|
19
|
-
"@epicgames-ps/lib-pixelstreamingcommon-ue5.5": "^0.0.
|
|
19
|
+
"@epicgames-ps/lib-pixelstreamingcommon-ue5.5": "^0.0.14",
|
|
20
20
|
"@types/jest": "27.5.1",
|
|
21
21
|
"@types/webxr": "^0.5.1",
|
|
22
22
|
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
},
|
|
38
38
|
"repository": {
|
|
39
39
|
"type": "git",
|
|
40
|
-
"url": "https://github.com/
|
|
40
|
+
"url": "https://github.com/EpicGamesExt/PixelStreamingInfrastructure.git"
|
|
41
41
|
},
|
|
42
42
|
"author": "Epic Games",
|
|
43
43
|
"license": "MIT",
|
|
@@ -45,3 +45,4 @@
|
|
|
45
45
|
"access": "public"
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
|
+
|
package/src/Config/Config.ts
CHANGED
|
@@ -31,7 +31,8 @@ export class Flags {
|
|
|
31
31
|
static TouchInput = 'TouchInput' as const;
|
|
32
32
|
static GamepadInput = 'GamepadInput' as const;
|
|
33
33
|
static XRControllerInput = 'XRControllerInput' as const;
|
|
34
|
-
static WaitForStreamer =
|
|
34
|
+
static WaitForStreamer = 'WaitForStreamer' as const;
|
|
35
|
+
static HideUI = 'HideUI' as const;
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
export type FlagsKeys = Exclude<keyof typeof Flags, 'prototype'>;
|
|
@@ -500,6 +501,19 @@ export class Config {
|
|
|
500
501
|
useUrlParams
|
|
501
502
|
)
|
|
502
503
|
);
|
|
504
|
+
|
|
505
|
+
this.flags.set(
|
|
506
|
+
Flags.HideUI,
|
|
507
|
+
new SettingFlag(
|
|
508
|
+
Flags.HideUI,
|
|
509
|
+
'Hide the UI overlay',
|
|
510
|
+
'Will hide all UI overlay details',
|
|
511
|
+
settings && Object.prototype.hasOwnProperty.call(settings, Flags.HideUI) ?
|
|
512
|
+
settings[Flags.HideUI] :
|
|
513
|
+
false,
|
|
514
|
+
useUrlParams
|
|
515
|
+
)
|
|
516
|
+
);
|
|
503
517
|
|
|
504
518
|
/**
|
|
505
519
|
* Numeric parameters
|
|
@@ -24,7 +24,7 @@ export class SettingOption<
|
|
|
24
24
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
25
25
|
defaultOnChangeListener: (changedValue: unknown, setting: SettingBase) => void = () => { /* Do nothing, to be overridden. */ }
|
|
26
26
|
) {
|
|
27
|
-
super(id, label, description,
|
|
27
|
+
super(id, label, description, defaultTextValue, defaultOnChangeListener);
|
|
28
28
|
|
|
29
29
|
this.options = options;
|
|
30
30
|
const urlParams = new URLSearchParams(window.location.search);
|
|
@@ -178,7 +178,7 @@ export class TouchController implements ITouchController {
|
|
|
178
178
|
coord.x,
|
|
179
179
|
coord.y,
|
|
180
180
|
this.fingerIds.get(touch.identifier),
|
|
181
|
-
this.maxByteValue * touch.force,
|
|
181
|
+
this.maxByteValue * (touch.force > 0 ? touch.force : 1),
|
|
182
182
|
coord.inRange ? 1 : 0
|
|
183
183
|
]);
|
|
184
184
|
break;
|
|
@@ -198,7 +198,7 @@ export class TouchController implements ITouchController {
|
|
|
198
198
|
coord.x,
|
|
199
199
|
coord.y,
|
|
200
200
|
this.fingerIds.get(touch.identifier),
|
|
201
|
-
this.maxByteValue * touch.force,
|
|
201
|
+
this.maxByteValue * (touch.force > 0 ? touch.force : 1),
|
|
202
202
|
coord.inRange ? 1 : 0
|
|
203
203
|
]);
|
|
204
204
|
break;
|
|
@@ -25,7 +25,7 @@ export class AggregatedStats {
|
|
|
25
25
|
inboundAudioStats: InboundAudioStats;
|
|
26
26
|
lastVideoStats: InboundVideoStats;
|
|
27
27
|
lastAudioStats: InboundAudioStats;
|
|
28
|
-
|
|
28
|
+
candidatePairs: Array<CandidatePairStats>;
|
|
29
29
|
DataChannelStats: DataChannelStats;
|
|
30
30
|
localCandidates: Array<CandidateStat>;
|
|
31
31
|
remoteCandidates: Array<CandidateStat>;
|
|
@@ -33,11 +33,11 @@ export class AggregatedStats {
|
|
|
33
33
|
sessionStats: SessionStats;
|
|
34
34
|
streamStats: StreamStats;
|
|
35
35
|
codecs: Map<string, string>;
|
|
36
|
+
transportStats: RTCTransportStats;
|
|
36
37
|
|
|
37
38
|
constructor() {
|
|
38
39
|
this.inboundVideoStats = new InboundVideoStats();
|
|
39
40
|
this.inboundAudioStats = new InboundAudioStats();
|
|
40
|
-
this.candidatePair = new CandidatePairStats();
|
|
41
41
|
this.DataChannelStats = new DataChannelStats();
|
|
42
42
|
this.outBoundVideoStats = new OutBoundVideoStats();
|
|
43
43
|
this.sessionStats = new SessionStats();
|
|
@@ -52,6 +52,7 @@ export class AggregatedStats {
|
|
|
52
52
|
processStats(rtcStatsReport: RTCStatsReport) {
|
|
53
53
|
this.localCandidates = new Array<CandidateStat>();
|
|
54
54
|
this.remoteCandidates = new Array<CandidateStat>();
|
|
55
|
+
this.candidatePairs = new Array<CandidatePairStats>();
|
|
55
56
|
|
|
56
57
|
rtcStatsReport.forEach((stat) => {
|
|
57
58
|
const type: RTCStatsTypePS = stat.type;
|
|
@@ -94,6 +95,7 @@ export class AggregatedStats {
|
|
|
94
95
|
this.handleTrack(stat);
|
|
95
96
|
break;
|
|
96
97
|
case 'transport':
|
|
98
|
+
this.handleTransport(stat);
|
|
97
99
|
break;
|
|
98
100
|
case 'stream':
|
|
99
101
|
this.handleStream(stat);
|
|
@@ -120,16 +122,10 @@ export class AggregatedStats {
|
|
|
120
122
|
* @param stat - the stats coming in from ice candidates
|
|
121
123
|
*/
|
|
122
124
|
handleCandidatePair(stat: CandidatePairStats) {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
this.
|
|
126
|
-
|
|
127
|
-
this.candidatePair.nominated = stat.nominated;
|
|
128
|
-
this.candidatePair.readable = stat.readable;
|
|
129
|
-
this.candidatePair.selected = stat.selected;
|
|
130
|
-
this.candidatePair.writable = stat.writable;
|
|
131
|
-
this.candidatePair.state = stat.state;
|
|
132
|
-
this.candidatePair.currentRoundTripTime = stat.currentRoundTripTime;
|
|
125
|
+
|
|
126
|
+
// Add the candidate pair to the candidate pair array
|
|
127
|
+
this.candidatePairs.push(stat)
|
|
128
|
+
|
|
133
129
|
}
|
|
134
130
|
|
|
135
131
|
/**
|
|
@@ -162,6 +158,8 @@ export class AggregatedStats {
|
|
|
162
158
|
localCandidate.protocol = stat.protocol;
|
|
163
159
|
localCandidate.candidateType = stat.candidateType;
|
|
164
160
|
localCandidate.id = stat.id;
|
|
161
|
+
localCandidate.relayProtocol = stat.relayProtocol;
|
|
162
|
+
localCandidate.transportId = stat.transportId;
|
|
165
163
|
this.localCandidates.push(localCandidate);
|
|
166
164
|
}
|
|
167
165
|
|
|
@@ -171,12 +169,14 @@ export class AggregatedStats {
|
|
|
171
169
|
*/
|
|
172
170
|
handleRemoteCandidate(stat: CandidateStat) {
|
|
173
171
|
const RemoteCandidate = new CandidateStat();
|
|
174
|
-
RemoteCandidate.label = '
|
|
172
|
+
RemoteCandidate.label = 'remote-candidate';
|
|
175
173
|
RemoteCandidate.address = stat.address;
|
|
176
174
|
RemoteCandidate.port = stat.port;
|
|
177
175
|
RemoteCandidate.protocol = stat.protocol;
|
|
178
176
|
RemoteCandidate.id = stat.id;
|
|
179
177
|
RemoteCandidate.candidateType = stat.candidateType;
|
|
178
|
+
RemoteCandidate.relayProtocol = stat.relayProtocol;
|
|
179
|
+
RemoteCandidate.transportId = stat.transportId
|
|
180
180
|
this.remoteCandidates.push(RemoteCandidate);
|
|
181
181
|
}
|
|
182
182
|
|
|
@@ -269,6 +269,11 @@ export class AggregatedStats {
|
|
|
269
269
|
}
|
|
270
270
|
}
|
|
271
271
|
|
|
272
|
+
handleTransport(stat: RTCTransportStats){
|
|
273
|
+
this.transportStats = stat;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
|
|
272
277
|
handleCodec(stat: CodecStats) {
|
|
273
278
|
const codecId = stat.id;
|
|
274
279
|
const codecType = `${stat.mimeType
|
|
@@ -308,4 +313,20 @@ export class AggregatedStats {
|
|
|
308
313
|
isNumber(value: unknown): boolean {
|
|
309
314
|
return typeof value === 'number' && isFinite(value);
|
|
310
315
|
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Helper function to return the active candidate pair
|
|
319
|
+
* @returns The candidate pair that is currently receiving data
|
|
320
|
+
*/
|
|
321
|
+
public getActiveCandidatePair(): CandidatePairStats | null {
|
|
322
|
+
|
|
323
|
+
// Check if the RTCTransport stat is not undefined
|
|
324
|
+
if (this.transportStats){
|
|
325
|
+
// Return the candidate pair that matches the transport candidate pair id
|
|
326
|
+
return this.candidatePairs.find((candidatePair) => candidatePair.id === this.transportStats.selectedCandidatePairId, null);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Fall back to the selected candidate pair
|
|
330
|
+
return this.candidatePairs.find((candidatePair) => candidatePair.selected, null);
|
|
331
|
+
}
|
|
311
332
|
}
|
|
@@ -6,12 +6,19 @@
|
|
|
6
6
|
export class CandidatePairStats {
|
|
7
7
|
bytesReceived: number;
|
|
8
8
|
bytesSent: number;
|
|
9
|
+
currentRoundTripTime: number;
|
|
10
|
+
id: string;
|
|
11
|
+
lastPacketReceivedTimestamp: number;
|
|
12
|
+
lastPacketSentTimestamp: number;
|
|
9
13
|
localCandidateId: string;
|
|
10
|
-
remoteCandidateId: string;
|
|
11
14
|
nominated: boolean;
|
|
15
|
+
priority: number;
|
|
12
16
|
readable: boolean;
|
|
13
|
-
|
|
17
|
+
remoteCandidateId: string;
|
|
14
18
|
selected: boolean;
|
|
15
19
|
state: string;
|
|
16
|
-
|
|
20
|
+
timestamp: number;
|
|
21
|
+
transportId: string;
|
|
22
|
+
type: string;
|
|
23
|
+
writable: boolean;
|
|
17
24
|
}
|
|
@@ -4,10 +4,12 @@
|
|
|
4
4
|
* ICE Candidate Stat collected from the RTC Stats Report
|
|
5
5
|
*/
|
|
6
6
|
export class CandidateStat {
|
|
7
|
-
label: string;
|
|
8
|
-
id: string;
|
|
9
7
|
address: string;
|
|
10
8
|
candidateType: string;
|
|
9
|
+
id: string;
|
|
10
|
+
label: string;
|
|
11
11
|
port: number;
|
|
12
12
|
protocol: 'tcp' | 'udp';
|
|
13
|
+
relayProtocol: 'tcp' | 'udp' | 'tls';
|
|
14
|
+
transportId: string;
|
|
13
15
|
}
|
|
@@ -15,6 +15,8 @@ export class PeerConnectionController {
|
|
|
15
15
|
config: Config;
|
|
16
16
|
preferredCodec: string;
|
|
17
17
|
updateCodecSelection: boolean;
|
|
18
|
+
videoTrack: MediaStreamTrack;
|
|
19
|
+
audioTrack: MediaStreamTrack;
|
|
18
20
|
|
|
19
21
|
/**
|
|
20
22
|
* Create a new RTC Peer Connection client
|
|
@@ -175,10 +177,15 @@ export class PeerConnectionController {
|
|
|
175
177
|
* Generate Aggregated Stats and then fire a onVideo Stats event
|
|
176
178
|
*/
|
|
177
179
|
generateStats() {
|
|
178
|
-
|
|
180
|
+
const statsHandler = (StatsData: RTCStatsReport) => {
|
|
179
181
|
this.aggregatedStats.processStats(StatsData);
|
|
180
|
-
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const audioPromise = this.peerConnection?.getStats(this.audioTrack).then(statsHandler);
|
|
185
|
+
const videoPromise = this.peerConnection?.getStats(this.videoTrack).then(statsHandler);
|
|
181
186
|
|
|
187
|
+
Promise.allSettled([audioPromise, videoPromise]).then(() => {
|
|
188
|
+
this.onVideoStats(this.aggregatedStats);
|
|
182
189
|
// Update the preferred codec selection based on what was actually negotiated
|
|
183
190
|
if (this.updateCodecSelection && !!this.aggregatedStats.inboundVideoStats.codecId) {
|
|
184
191
|
this.config.setOptionSettingValue(
|
|
@@ -300,6 +307,15 @@ export class PeerConnectionController {
|
|
|
300
307
|
* @param event - The webRtc track event
|
|
301
308
|
*/
|
|
302
309
|
handleOnTrack(event: RTCTrackEvent) {
|
|
310
|
+
if (event.streams.length < 1 || event.streams[0].id == 'probator') {
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
if (event.track.kind == 'video') {
|
|
314
|
+
this.videoTrack = event.track;
|
|
315
|
+
}
|
|
316
|
+
if (event.track.kind == 'audio') {
|
|
317
|
+
this.audioTrack = event.track;
|
|
318
|
+
}
|
|
303
319
|
this.onTrack(event);
|
|
304
320
|
}
|
|
305
321
|
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
import { PixelStreaming } from './PixelStreaming';
|
|
7
7
|
import { SettingsChangedEvent, StreamerListMessageEvent, WebRtcConnectedEvent, WebRtcSdpEvent } from '../Util/EventEmitter';
|
|
8
8
|
import { mockWebSocket, MockWebSocketSpyFunctions, MockWebSocketTriggerFunctions, unmockWebSocket } from '../__test__/mockWebSocket';
|
|
9
|
-
import {
|
|
9
|
+
import { BaseMessage, Messages, MessageHelpers } from '@epicgames-ps/lib-pixelstreamingcommon-ue5.5';
|
|
10
10
|
import { mockRTCPeerConnection, MockRTCPeerConnectionSpyFunctions, MockRTCPeerConnectionTriggerFunctions, unmockRTCPeerConnection } from '../__test__/mockRTCPeerConnection';
|
|
11
11
|
import { mockHTMLMediaElement, mockMediaStream, unmockMediaStream } from '../__test__/mockMediaStream';
|
|
12
12
|
import { InitialSettings } from '../DataChannel/InitialSettings';
|
|
@@ -32,26 +32,13 @@ describe('PixelStreaming', () => {
|
|
|
32
32
|
|
|
33
33
|
const triggerWebSocketOpen = () =>
|
|
34
34
|
webSocketTriggerFunctions.triggerOnOpen?.();
|
|
35
|
-
const
|
|
36
|
-
webSocketTriggerFunctions.triggerOnMessage?.(
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
type: MessageReceive.MessageRecvTypes.STREAMER_LIST,
|
|
43
|
-
ids: streamerIdList
|
|
44
|
-
});
|
|
45
|
-
const triggerSdpOfferMessage = () =>
|
|
46
|
-
webSocketTriggerFunctions.triggerOnMessage?.({
|
|
47
|
-
type: MessageReceive.MessageRecvTypes.OFFER,
|
|
48
|
-
sdp
|
|
49
|
-
});
|
|
50
|
-
const triggerIceCandidateMessage = () =>
|
|
51
|
-
webSocketTriggerFunctions.triggerOnMessage?.({
|
|
52
|
-
type: MessageReceive.MessageRecvTypes.ICE_CANDIDATE,
|
|
53
|
-
candidate: iceCandidate
|
|
54
|
-
});
|
|
35
|
+
const triggerSignallingMessage = (message: BaseMessage) => {
|
|
36
|
+
webSocketTriggerFunctions.triggerOnMessage?.(message);
|
|
37
|
+
}
|
|
38
|
+
const triggerConfigMessage = () => triggerSignallingMessage(MessageHelpers.createMessage(Messages.config, { peerConnectionOptions: {} }));
|
|
39
|
+
const triggerStreamerListMessage = (streamerIdList: string[]) => triggerSignallingMessage(MessageHelpers.createMessage(Messages.streamerList, { ids: streamerIdList }));
|
|
40
|
+
const triggerSdpOfferMessage = () => triggerSignallingMessage(MessageHelpers.createMessage(Messages.offer, { sdp }));
|
|
41
|
+
const triggerIceCandidateMessage = () => triggerSignallingMessage(MessageHelpers.createMessage(Messages.iceCandidate, { candidate: iceCandidate }));
|
|
55
42
|
const triggerIceConnectionState = (state: RTCIceConnectionState) =>
|
|
56
43
|
rtcPeerConnectionTriggerFunctions.triggerIceConnectionStateChange?.(
|
|
57
44
|
state
|
|
@@ -200,17 +187,13 @@ describe('PixelStreaming', () => {
|
|
|
200
187
|
|
|
201
188
|
it('should automatically reconnect and request streamer list N times on websocket close', () => {
|
|
202
189
|
const config = new Config({ initialSettings: {ss: mockSignallingUrl, AutoConnect: true, MaxReconnectAttempts: 3}});
|
|
203
|
-
const autoconnectedSpy = jest.fn();
|
|
204
|
-
|
|
205
190
|
const pixelStreaming = new PixelStreaming(config);
|
|
206
|
-
pixelStreaming.addEventListener("webRtcAutoConnect", autoconnectedSpy);
|
|
207
191
|
|
|
208
192
|
expect(webSocketSpyFunctions.constructorSpy).toHaveBeenCalledWith(mockSignallingUrl);
|
|
209
193
|
expect(webSocketSpyFunctions.constructorSpy).toHaveBeenCalledTimes(1);
|
|
210
194
|
expect(webSocketSpyFunctions.closeSpy).not.toHaveBeenCalled();
|
|
211
195
|
|
|
212
|
-
|
|
213
|
-
webSocketTriggerFunctions.triggerRemoteClose();
|
|
196
|
+
webSocketTriggerFunctions.triggerRemoteClose?.();
|
|
214
197
|
|
|
215
198
|
expect(webSocketSpyFunctions.closeSpy).toHaveBeenCalled();
|
|
216
199
|
|
|
@@ -273,11 +256,11 @@ describe('PixelStreaming', () => {
|
|
|
273
256
|
|
|
274
257
|
expect(streamerListSpy).toHaveBeenCalledWith(new StreamerListMessageEvent({
|
|
275
258
|
messageStreamerList: expect.objectContaining({
|
|
276
|
-
type:
|
|
259
|
+
type: Messages.streamerList.typeName,
|
|
277
260
|
ids: streamerIdList
|
|
278
261
|
}),
|
|
279
262
|
autoSelectedStreamerId: streamerId,
|
|
280
|
-
wantedStreamerId:
|
|
263
|
+
wantedStreamerId: ''
|
|
281
264
|
}));
|
|
282
265
|
expect(webSocketSpyFunctions.sendSpy).toHaveBeenCalledWith(
|
|
283
266
|
expect.stringMatching(/"type":"subscribe".*MOCK_PIXEL_STREAMING/)
|
|
@@ -298,11 +281,11 @@ describe('PixelStreaming', () => {
|
|
|
298
281
|
|
|
299
282
|
expect(streamerListSpy).toHaveBeenCalledWith(new StreamerListMessageEvent({
|
|
300
283
|
messageStreamerList: expect.objectContaining({
|
|
301
|
-
type:
|
|
284
|
+
type: Messages.streamerList.typeName,
|
|
302
285
|
ids: extendedStreamerIdList
|
|
303
286
|
}),
|
|
304
|
-
autoSelectedStreamerId:
|
|
305
|
-
wantedStreamerId:
|
|
287
|
+
autoSelectedStreamerId: '',
|
|
288
|
+
wantedStreamerId: ''
|
|
306
289
|
}));
|
|
307
290
|
expect(webSocketSpyFunctions.sendSpy).not.toHaveBeenCalledWith(
|
|
308
291
|
expect.stringMatching(/"type":"subscribe"/)
|
|
@@ -400,9 +383,9 @@ describe('PixelStreaming', () => {
|
|
|
400
383
|
expect.objectContaining({
|
|
401
384
|
data: {
|
|
402
385
|
aggregatedStats: expect.objectContaining({
|
|
403
|
-
|
|
404
|
-
bytesReceived: 123
|
|
405
|
-
|
|
386
|
+
candidatePairs: [
|
|
387
|
+
expect.objectContaining({ bytesReceived: 123 })
|
|
388
|
+
],
|
|
406
389
|
localCandidates: [
|
|
407
390
|
expect.objectContaining({ address: 'mock-address' })
|
|
408
391
|
]
|
|
@@ -443,22 +426,22 @@ describe('PixelStreaming', () => {
|
|
|
443
426
|
expect(streamSpy).toHaveBeenCalled();
|
|
444
427
|
});
|
|
445
428
|
|
|
446
|
-
it('should emit playStreamRejected if video play is rejected', async () => {
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
});
|
|
429
|
+
// it('should emit playStreamRejected if video play is rejected', async () => {
|
|
430
|
+
// mockHTMLMediaElement({ ableToPlay: false });
|
|
431
|
+
//
|
|
432
|
+
// const config = new Config({ initialSettings: {ss: mockSignallingUrl}});
|
|
433
|
+
// const streamRejectedSpy = jest.fn();
|
|
434
|
+
// const pixelStreaming = new PixelStreaming(config);
|
|
435
|
+
// pixelStreaming.addEventListener("playStreamRejected", streamRejectedSpy);
|
|
436
|
+
// pixelStreaming.connect();
|
|
437
|
+
//
|
|
438
|
+
// establishMockedPixelStreamingConnection();
|
|
439
|
+
//
|
|
440
|
+
// pixelStreaming.play();
|
|
441
|
+
// await flushPromises();
|
|
442
|
+
//
|
|
443
|
+
// expect(streamRejectedSpy).toHaveBeenCalled();
|
|
444
|
+
// });
|
|
462
445
|
|
|
463
446
|
it('should send data through the data channel when emitCommand is called', () => {
|
|
464
447
|
mockHTMLMediaElement({ ableToPlay: true, readyState: 2 });
|
|
@@ -28,7 +28,8 @@ import {
|
|
|
28
28
|
WebRtcSdpEvent,
|
|
29
29
|
DataChannelLatencyTestResponseEvent,
|
|
30
30
|
DataChannelLatencyTestResultEvent,
|
|
31
|
-
PlayerCountEvent
|
|
31
|
+
PlayerCountEvent,
|
|
32
|
+
WebRtcTCPRelayDetectedEvent
|
|
32
33
|
} from '../Util/EventEmitter';
|
|
33
34
|
import { WebXRController } from '../WebXR/WebXRController';
|
|
34
35
|
import { MessageDirection } from '../UeInstanceMessage/StreamMessageController';
|
|
@@ -61,6 +62,7 @@ export class PixelStreaming {
|
|
|
61
62
|
protected _webRtcController: WebRtcPlayerController;
|
|
62
63
|
protected _webXrController: WebXRController;
|
|
63
64
|
protected _dataChannelLatencyTestController: DataChannelLatencyTestController;
|
|
65
|
+
|
|
64
66
|
/**
|
|
65
67
|
* Configuration object. You can read or modify config through this object. Whenever
|
|
66
68
|
* the configuration is changed, the library will emit a `settingsChanged` event.
|
|
@@ -115,6 +117,15 @@ export class PixelStreaming {
|
|
|
115
117
|
this.onScreenKeyboardHelper.showOnScreenKeyboard(command);
|
|
116
118
|
|
|
117
119
|
this._webXrController = new WebXRController(this._webRtcController);
|
|
120
|
+
|
|
121
|
+
this._setupWebRtcTCPRelayDetection = this._setupWebRtcTCPRelayDetection.bind(this)
|
|
122
|
+
|
|
123
|
+
// Add event listener for the webRtcConnected event
|
|
124
|
+
this._eventEmitter.addEventListener("webRtcConnected", (_: WebRtcConnectedEvent) => {
|
|
125
|
+
|
|
126
|
+
// Bind to the stats received event
|
|
127
|
+
this._eventEmitter.addEventListener("statsReceived", this._setupWebRtcTCPRelayDetection);
|
|
128
|
+
});
|
|
118
129
|
}
|
|
119
130
|
|
|
120
131
|
/**
|
|
@@ -626,6 +637,28 @@ export class PixelStreaming {
|
|
|
626
637
|
);
|
|
627
638
|
}
|
|
628
639
|
|
|
640
|
+
// Sets up to emit the webrtc tcp relay detect event
|
|
641
|
+
_setupWebRtcTCPRelayDetection(statsReceivedEvent: StatsReceivedEvent) {
|
|
642
|
+
// Get the active candidate pair
|
|
643
|
+
const activeCandidatePair = statsReceivedEvent.data.aggregatedStats.getActiveCandidatePair();
|
|
644
|
+
|
|
645
|
+
// Check if the active candidate pair is not null
|
|
646
|
+
if (activeCandidatePair != null) {
|
|
647
|
+
|
|
648
|
+
// Get the local candidate assigned to the active candidate pair
|
|
649
|
+
const localCandidate = statsReceivedEvent.data.aggregatedStats.localCandidates.find((candidate) => candidate.id == activeCandidatePair.localCandidateId, null)
|
|
650
|
+
|
|
651
|
+
// Check if the local candidate is not null, candidate type is relay and the relay protocol is tcp
|
|
652
|
+
if (localCandidate != null && localCandidate.candidateType == 'relay' && localCandidate.relayProtocol == 'tcp') {
|
|
653
|
+
|
|
654
|
+
// Send the web rtc tcp relay detected event
|
|
655
|
+
this._eventEmitter.dispatchEvent(new WebRtcTCPRelayDetectedEvent());
|
|
656
|
+
}
|
|
657
|
+
// The check is completed and the stats listen event can be removed
|
|
658
|
+
this._eventEmitter.removeEventListener("statsReceived", this._setupWebRtcTCPRelayDetection);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
629
662
|
/**
|
|
630
663
|
* Request a connection latency test.
|
|
631
664
|
* NOTE: There are plans to refactor all request* functions. Expect changes if you use this!
|
package/src/Util/EventEmitter.ts
CHANGED
|
@@ -537,6 +537,16 @@ export class PlayerCountEvent extends Event {
|
|
|
537
537
|
}
|
|
538
538
|
}
|
|
539
539
|
|
|
540
|
+
/**
|
|
541
|
+
* An event that is emitted when the webRTC connections is relayed over TCP.
|
|
542
|
+
*/
|
|
543
|
+
export class WebRtcTCPRelayDetectedEvent extends Event {
|
|
544
|
+
readonly type: 'webRtcTCPRelayDetected';
|
|
545
|
+
constructor() {
|
|
546
|
+
super('webRtcTCPRelayDetected');
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
540
550
|
export type PixelStreamingEvent =
|
|
541
551
|
| AfkWarningActivateEvent
|
|
542
552
|
| AfkWarningUpdateEvent
|
|
@@ -573,7 +583,8 @@ export type PixelStreamingEvent =
|
|
|
573
583
|
| XrSessionStartedEvent
|
|
574
584
|
| XrSessionEndedEvent
|
|
575
585
|
| XrFrameEvent
|
|
576
|
-
| PlayerCountEvent
|
|
586
|
+
| PlayerCountEvent
|
|
587
|
+
| WebRtcTCPRelayDetectedEvent;
|
|
577
588
|
|
|
578
589
|
export class EventEmitter extends EventTarget {
|
|
579
590
|
/**
|
|
@@ -31,6 +31,12 @@ export class StreamController {
|
|
|
31
31
|
'handleOnTrack ' + JSON.stringify(rtcTrackEvent.streams),
|
|
32
32
|
6
|
|
33
33
|
);
|
|
34
|
+
// Do not add the track if the ID is `probator` as this is special track created by mediasoup for bitrate probing.
|
|
35
|
+
// Refer to https://github.com/EpicGamesExt/PixelStreamingInfrastructure/pull/86 for more details.
|
|
36
|
+
if (rtcTrackEvent.streams.length < 1 || rtcTrackEvent.streams[0].id == 'probator') {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
34
40
|
const videoElement = this.videoElementProvider.getVideoElement();
|
|
35
41
|
|
|
36
42
|
if (rtcTrackEvent.track) {
|
|
@@ -222,6 +222,7 @@ export class WebRtcPlayerController {
|
|
|
222
222
|
const message = MessageHelpers.createMessage(Messages.listStreamers);
|
|
223
223
|
this.protocol.sendMessage(message);
|
|
224
224
|
}
|
|
225
|
+
this.reconnectAttempt = 0;
|
|
225
226
|
});
|
|
226
227
|
this.protocol.transport.addListener('error', () => {
|
|
227
228
|
// dont really need to do anything here since the close event should follow.
|
|
@@ -1324,7 +1325,7 @@ export class WebRtcPlayerController {
|
|
|
1324
1325
|
6
|
|
1325
1326
|
);
|
|
1326
1327
|
|
|
1327
|
-
let wantedStreamerId: string =
|
|
1328
|
+
let wantedStreamerId: string = '';
|
|
1328
1329
|
|
|
1329
1330
|
// get the current selected streamer id option
|
|
1330
1331
|
const streamerIDOption = this.config.getSettingOption(OptionParameters.StreamerId);
|
|
@@ -1342,7 +1343,7 @@ export class WebRtcPlayerController {
|
|
|
1342
1343
|
settingOptions
|
|
1343
1344
|
);
|
|
1344
1345
|
|
|
1345
|
-
let autoSelectedStreamerId: string =
|
|
1346
|
+
let autoSelectedStreamerId: string = '';
|
|
1346
1347
|
const waitForStreamer = this.config.isFlagEnabled(Flags.WaitForStreamer);
|
|
1347
1348
|
const reconnectLimit = this.config.getNumericSettingValue(NumericParameters.MaxReconnectAttempts);
|
|
1348
1349
|
const reconnectDelay = this.config.getNumericSettingValue(NumericParameters.StreamerAutoJoinInterval);
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { BaseMessage } from '@epicgames-ps/lib-pixelstreamingcommon-ue5.5';
|
|
2
|
+
|
|
1
3
|
export interface MockWebSocketSpyFunctions {
|
|
2
4
|
constructorSpy: null | ((url: string) => void);
|
|
3
5
|
openSpy: null | ((event: Event) => void);
|
|
@@ -12,7 +14,7 @@ export interface MockWebSocketTriggerFunctions {
|
|
|
12
14
|
triggerOnOpen: null | (() => void);
|
|
13
15
|
triggerOnError: null | (() => void);
|
|
14
16
|
triggerOnClose: null | ((closeReason?: CloseEventInit) => void);
|
|
15
|
-
triggerOnMessage: null | ((message?:
|
|
17
|
+
triggerOnMessage: null | ((message?: BaseMessage) => void);
|
|
16
18
|
triggerOnMessageBinary: null | ((message?: Blob) => void);
|
|
17
19
|
triggerRemoteClose: null | ((code?: number, reason?: string) => void);
|
|
18
20
|
}
|
|
@@ -88,10 +90,8 @@ export class MockWebSocketImpl extends WebSocket {
|
|
|
88
90
|
this.close(code, reason);
|
|
89
91
|
}
|
|
90
92
|
|
|
91
|
-
triggerOnMessage(message
|
|
92
|
-
const data = message
|
|
93
|
-
? JSON.stringify(message)
|
|
94
|
-
: JSON.stringify({ type: 'test' });
|
|
93
|
+
triggerOnMessage(message: BaseMessage) {
|
|
94
|
+
const data = JSON.stringify(message);
|
|
95
95
|
const event = new MessageEvent('message', { data });
|
|
96
96
|
this.onmessage?.(event);
|
|
97
97
|
spyFunctions.messageSpy?.(event);
|
package/types/Config/Config.d.ts
CHANGED
|
@@ -27,6 +27,7 @@ export declare class Flags {
|
|
|
27
27
|
static GamepadInput: "GamepadInput";
|
|
28
28
|
static XRControllerInput: "XRControllerInput";
|
|
29
29
|
static WaitForStreamer: "WaitForStreamer";
|
|
30
|
+
static HideUI: "HideUI";
|
|
30
31
|
}
|
|
31
32
|
export type FlagsKeys = Exclude<keyof typeof Flags, 'prototype'>;
|
|
32
33
|
export type FlagsIds = typeof Flags[FlagsKeys];
|
|
@@ -12,7 +12,7 @@ export declare class AggregatedStats {
|
|
|
12
12
|
inboundAudioStats: InboundAudioStats;
|
|
13
13
|
lastVideoStats: InboundVideoStats;
|
|
14
14
|
lastAudioStats: InboundAudioStats;
|
|
15
|
-
|
|
15
|
+
candidatePairs: Array<CandidatePairStats>;
|
|
16
16
|
DataChannelStats: DataChannelStats;
|
|
17
17
|
localCandidates: Array<CandidateStat>;
|
|
18
18
|
remoteCandidates: Array<CandidateStat>;
|
|
@@ -20,6 +20,7 @@ export declare class AggregatedStats {
|
|
|
20
20
|
sessionStats: SessionStats;
|
|
21
21
|
streamStats: StreamStats;
|
|
22
22
|
codecs: Map<string, string>;
|
|
23
|
+
transportStats: RTCTransportStats;
|
|
23
24
|
constructor();
|
|
24
25
|
/**
|
|
25
26
|
* Gather all the information from the RTC Peer Connection Report
|
|
@@ -67,6 +68,7 @@ export declare class AggregatedStats {
|
|
|
67
68
|
* @param stat - video track stats
|
|
68
69
|
*/
|
|
69
70
|
handleTrack(stat: InboundTrackStats): void;
|
|
71
|
+
handleTransport(stat: RTCTransportStats): void;
|
|
70
72
|
handleCodec(stat: CodecStats): void;
|
|
71
73
|
handleSessionStatistics(videoStartTime: number, inputController: boolean | null, videoEncoderAvgQP: number): void;
|
|
72
74
|
/**
|
|
@@ -74,4 +76,9 @@ export declare class AggregatedStats {
|
|
|
74
76
|
* @param value - the number to be checked
|
|
75
77
|
*/
|
|
76
78
|
isNumber(value: unknown): boolean;
|
|
79
|
+
/**
|
|
80
|
+
* Helper function to return the active candidate pair
|
|
81
|
+
* @returns The candidate pair that is currently receiving data
|
|
82
|
+
*/
|
|
83
|
+
getActiveCandidatePair(): CandidatePairStats | null;
|
|
77
84
|
}
|
|
@@ -4,12 +4,19 @@
|
|
|
4
4
|
export declare class CandidatePairStats {
|
|
5
5
|
bytesReceived: number;
|
|
6
6
|
bytesSent: number;
|
|
7
|
+
currentRoundTripTime: number;
|
|
8
|
+
id: string;
|
|
9
|
+
lastPacketReceivedTimestamp: number;
|
|
10
|
+
lastPacketSentTimestamp: number;
|
|
7
11
|
localCandidateId: string;
|
|
8
|
-
remoteCandidateId: string;
|
|
9
12
|
nominated: boolean;
|
|
13
|
+
priority: number;
|
|
10
14
|
readable: boolean;
|
|
11
|
-
|
|
15
|
+
remoteCandidateId: string;
|
|
12
16
|
selected: boolean;
|
|
13
17
|
state: string;
|
|
14
|
-
|
|
18
|
+
timestamp: number;
|
|
19
|
+
transportId: string;
|
|
20
|
+
type: string;
|
|
21
|
+
writable: boolean;
|
|
15
22
|
}
|