@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@epicgames-ps/lib-pixelstreamingfrontend-ue5.5",
3
- "version": "0.0.10",
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.12",
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/EpicGames/PixelStreamingInfrastructure.git"
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
+
@@ -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 = "WaitForStreamer" as const;
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, [defaultTextValue], defaultOnChangeListener);
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
- candidatePair: CandidatePairStats;
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
- this.candidatePair.bytesReceived = stat.bytesReceived;
124
- this.candidatePair.bytesSent = stat.bytesSent;
125
- this.candidatePair.localCandidateId = stat.localCandidateId;
126
- this.candidatePair.remoteCandidateId = stat.remoteCandidateId;
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 = 'local-candidate';
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
- writable: boolean;
17
+ remoteCandidateId: string;
14
18
  selected: boolean;
15
19
  state: string;
16
- currentRoundTripTime: number;
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
- this.peerConnection?.getStats(null).then((StatsData: RTCStatsReport) => {
180
+ const statsHandler = (StatsData: RTCStatsReport) => {
179
181
  this.aggregatedStats.processStats(StatsData);
180
- this.onVideoStats(this.aggregatedStats);
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 { MessageReceive } from '@epicgames-ps/lib-pixelstreamingcommon-ue5.5';
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 triggerConfigMessage = () =>
36
- webSocketTriggerFunctions.triggerOnMessage?.({
37
- type: MessageReceive.MessageRecvTypes.CONFIG,
38
- peerConnectionOptions: {}
39
- });
40
- const triggerStreamerListMessage = (streamerIdList: string[]) =>
41
- webSocketTriggerFunctions.triggerOnMessage?.({
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: MessageReceive.MessageRecvTypes.STREAMER_LIST,
259
+ type: Messages.streamerList.typeName,
277
260
  ids: streamerIdList
278
261
  }),
279
262
  autoSelectedStreamerId: streamerId,
280
- wantedStreamerId: null
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: MessageReceive.MessageRecvTypes.STREAMER_LIST,
284
+ type: Messages.streamerList.typeName,
302
285
  ids: extendedStreamerIdList
303
286
  }),
304
- autoSelectedStreamerId: null,
305
- wantedStreamerId: null
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
- candidatePair: expect.objectContaining({
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
- mockHTMLMediaElement({ ableToPlay: false });
448
-
449
- const config = new Config({ initialSettings: {ss: mockSignallingUrl}});
450
- const streamRejectedSpy = jest.fn();
451
- const pixelStreaming = new PixelStreaming(config);
452
- pixelStreaming.addEventListener("playStreamRejected", streamRejectedSpy);
453
- pixelStreaming.connect();
454
-
455
- establishMockedPixelStreamingConnection();
456
-
457
- pixelStreaming.play();
458
- await flushPromises();
459
-
460
- expect(streamRejectedSpy).toHaveBeenCalled();
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!
@@ -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 = null;
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 = null;
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?: object) => void);
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?: object) {
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);
@@ -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
- candidatePair: CandidatePairStats;
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
- writable: boolean;
15
+ remoteCandidateId: string;
12
16
  selected: boolean;
13
17
  state: string;
14
- currentRoundTripTime: number;
18
+ timestamp: number;
19
+ transportId: string;
20
+ type: string;
21
+ writable: boolean;
15
22
  }