@epicgames-ps/lib-pixelstreamingfrontend-ue5.5 1.2.4 → 1.3.0

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.
Files changed (106) hide show
  1. package/CHANGELOG.md +25 -2
  2. package/dist/cjs/AFK/AFKController.js +2 -2
  3. package/dist/cjs/AFK/AFKController.js.map +1 -1
  4. package/dist/cjs/Config/Config.js +19 -0
  5. package/dist/cjs/Config/Config.js.map +1 -1
  6. package/dist/cjs/DataChannel/DataChannelController.js +2 -2
  7. package/dist/cjs/DataChannel/DataChannelController.js.map +1 -1
  8. package/dist/cjs/DataChannel/DataChannelSender.js +2 -2
  9. package/dist/cjs/DataChannel/DataChannelSender.js.map +1 -1
  10. package/dist/cjs/Inputs/GamepadController.js +5 -6
  11. package/dist/cjs/Inputs/GamepadController.js.map +1 -1
  12. package/dist/cjs/Inputs/InputClassesFactory.js +3 -3
  13. package/dist/cjs/Inputs/InputClassesFactory.js.map +1 -1
  14. package/dist/cjs/Inputs/KeyCodes.js +13 -1
  15. package/dist/cjs/Inputs/KeyCodes.js.map +1 -1
  16. package/dist/cjs/Inputs/KeyboardController.js +12 -0
  17. package/dist/cjs/Inputs/KeyboardController.js.map +1 -1
  18. package/dist/cjs/Inputs/MouseController.js +2 -1
  19. package/dist/cjs/Inputs/MouseController.js.map +1 -1
  20. package/dist/cjs/Inputs/MouseControllerHovering.js +71 -6
  21. package/dist/cjs/Inputs/MouseControllerHovering.js.map +1 -1
  22. package/dist/cjs/Inputs/MouseControllerLocked.js +18 -3
  23. package/dist/cjs/Inputs/MouseControllerLocked.js.map +1 -1
  24. package/dist/cjs/PeerConnectionController/PeerConnectionController.js +71 -72
  25. package/dist/cjs/PeerConnectionController/PeerConnectionController.js.map +1 -1
  26. package/dist/cjs/PixelStreaming/PixelStreaming.js +25 -0
  27. package/dist/cjs/PixelStreaming/PixelStreaming.js.map +1 -1
  28. package/dist/cjs/UeInstanceMessage/SendMessageController.js +2 -3
  29. package/dist/cjs/UeInstanceMessage/SendMessageController.js.map +1 -1
  30. package/dist/cjs/UeInstanceMessage/StreamMessageController.js.map +1 -1
  31. package/dist/cjs/VideoPlayer/StreamController.js +3 -3
  32. package/dist/cjs/VideoPlayer/StreamController.js.map +1 -1
  33. package/dist/cjs/VideoPlayer/VideoPlayer.js +17 -6
  34. package/dist/cjs/VideoPlayer/VideoPlayer.js.map +1 -1
  35. package/dist/cjs/WebRtcPlayer/WebRtcPlayerController.js +2 -2
  36. package/dist/cjs/WebRtcPlayer/WebRtcPlayerController.js.map +1 -1
  37. package/dist/cjs/WebXR/WebXRController.js +14 -11
  38. package/dist/cjs/WebXR/WebXRController.js.map +1 -1
  39. package/dist/esm/AFK/AFKController.js +2 -2
  40. package/dist/esm/AFK/AFKController.js.map +1 -1
  41. package/dist/esm/Config/Config.js +19 -0
  42. package/dist/esm/Config/Config.js.map +1 -1
  43. package/dist/esm/DataChannel/DataChannelController.js +2 -2
  44. package/dist/esm/DataChannel/DataChannelController.js.map +1 -1
  45. package/dist/esm/DataChannel/DataChannelSender.js +2 -2
  46. package/dist/esm/DataChannel/DataChannelSender.js.map +1 -1
  47. package/dist/esm/Inputs/GamepadController.js +5 -6
  48. package/dist/esm/Inputs/GamepadController.js.map +1 -1
  49. package/dist/esm/Inputs/InputClassesFactory.js +3 -3
  50. package/dist/esm/Inputs/InputClassesFactory.js.map +1 -1
  51. package/dist/esm/Inputs/KeyCodes.js +13 -1
  52. package/dist/esm/Inputs/KeyCodes.js.map +1 -1
  53. package/dist/esm/Inputs/KeyboardController.js +12 -0
  54. package/dist/esm/Inputs/KeyboardController.js.map +1 -1
  55. package/dist/esm/Inputs/MouseController.js +2 -1
  56. package/dist/esm/Inputs/MouseController.js.map +1 -1
  57. package/dist/esm/Inputs/MouseControllerHovering.js +71 -6
  58. package/dist/esm/Inputs/MouseControllerHovering.js.map +1 -1
  59. package/dist/esm/Inputs/MouseControllerLocked.js +18 -3
  60. package/dist/esm/Inputs/MouseControllerLocked.js.map +1 -1
  61. package/dist/esm/PeerConnectionController/PeerConnectionController.js +71 -72
  62. package/dist/esm/PeerConnectionController/PeerConnectionController.js.map +1 -1
  63. package/dist/esm/PixelStreaming/PixelStreaming.js +25 -0
  64. package/dist/esm/PixelStreaming/PixelStreaming.js.map +1 -1
  65. package/dist/esm/UeInstanceMessage/SendMessageController.js +2 -3
  66. package/dist/esm/UeInstanceMessage/SendMessageController.js.map +1 -1
  67. package/dist/esm/UeInstanceMessage/StreamMessageController.js.map +1 -1
  68. package/dist/esm/VideoPlayer/StreamController.js +3 -3
  69. package/dist/esm/VideoPlayer/StreamController.js.map +1 -1
  70. package/dist/esm/VideoPlayer/VideoPlayer.js +18 -7
  71. package/dist/esm/VideoPlayer/VideoPlayer.js.map +1 -1
  72. package/dist/esm/WebRtcPlayer/WebRtcPlayerController.js +2 -2
  73. package/dist/esm/WebRtcPlayer/WebRtcPlayerController.js.map +1 -1
  74. package/dist/esm/WebXR/WebXRController.js +14 -11
  75. package/dist/esm/WebXR/WebXRController.js.map +1 -1
  76. package/dist/types/Config/Config.d.ts +8 -0
  77. package/dist/types/Inputs/InputClassesFactory.d.ts +1 -1
  78. package/dist/types/Inputs/MouseController.d.ts +3 -1
  79. package/dist/types/Inputs/MouseControllerHovering.d.ts +10 -1
  80. package/dist/types/Inputs/MouseControllerLocked.d.ts +2 -1
  81. package/dist/types/PeerConnectionController/PeerConnectionController.d.ts +3 -3
  82. package/dist/types/PixelStreaming/PixelStreaming.d.ts +9 -1
  83. package/dist/types/UeInstanceMessage/StreamMessageController.d.ts +3 -3
  84. package/dist/types/VideoPlayer/VideoPlayer.d.ts +1 -0
  85. package/eslint.config.mjs +1 -8
  86. package/package.json +4 -4
  87. package/src/AFK/AFKController.ts +2 -2
  88. package/src/Config/Config.ts +61 -9
  89. package/src/DataChannel/DataChannelController.ts +2 -2
  90. package/src/DataChannel/DataChannelSender.ts +2 -2
  91. package/src/Inputs/GamepadController.ts +5 -6
  92. package/src/Inputs/InputClassesFactory.ts +5 -3
  93. package/src/Inputs/KeyCodes.ts +13 -1
  94. package/src/Inputs/KeyboardController.ts +11 -1
  95. package/src/Inputs/MouseController.ts +5 -1
  96. package/src/Inputs/MouseControllerHovering.ts +79 -6
  97. package/src/Inputs/MouseControllerLocked.ts +20 -3
  98. package/src/PeerConnectionController/PeerConnectionController.ts +22 -22
  99. package/src/PixelStreaming/PixelStreaming.ts +26 -0
  100. package/src/UeInstanceMessage/SendMessageController.ts +2 -3
  101. package/src/UeInstanceMessage/StreamMessageController.ts +3 -3
  102. package/src/VideoPlayer/StreamController.ts +3 -3
  103. package/src/VideoPlayer/VideoPlayer.test.ts +141 -0
  104. package/src/VideoPlayer/VideoPlayer.ts +26 -10
  105. package/src/WebRtcPlayer/WebRtcPlayerController.ts +2 -2
  106. package/src/WebXR/WebXRController.ts +15 -11
@@ -5,6 +5,7 @@ import { InputCoordTranslator } from '../Util/InputCoordTranslator';
5
5
  import { VideoPlayer } from '../VideoPlayer/VideoPlayer';
6
6
  import type { ActiveKeys } from './InputClassesFactory';
7
7
  import { IInputController } from './IInputController';
8
+ import { Config } from '../Config/Config';
8
9
 
9
10
  /**
10
11
  * Extra types for Document and WheelEvent
@@ -29,6 +30,7 @@ export class MouseController implements IInputController {
29
30
  streamMessageController: StreamMessageController;
30
31
  coordinateConverter: InputCoordTranslator;
31
32
  activeKeys: ActiveKeys;
33
+ config: Config;
32
34
 
33
35
  // bound listeners
34
36
  onEnterListener: (event: MouseEvent) => void;
@@ -38,12 +40,14 @@ export class MouseController implements IInputController {
38
40
  streamMessageController: StreamMessageController,
39
41
  videoPlayer: VideoPlayer,
40
42
  coordinateConverter: InputCoordTranslator,
41
- activeKeys: ActiveKeys
43
+ activeKeys: ActiveKeys,
44
+ config: Config
42
45
  ) {
43
46
  this.streamMessageController = streamMessageController;
44
47
  this.coordinateConverter = coordinateConverter;
45
48
  this.videoPlayer = videoPlayer;
46
49
  this.activeKeys = activeKeys;
50
+ this.config = config;
47
51
 
48
52
  this.onEnterListener = this.onMouseEnter.bind(this);
49
53
  this.onLeaveListener = this.onMouseLeave.bind(this);
@@ -4,6 +4,7 @@ import { InputCoordTranslator } from '../Util/InputCoordTranslator';
4
4
  import { VideoPlayer } from '../VideoPlayer/VideoPlayer';
5
5
  import type { ActiveKeys } from './InputClassesFactory';
6
6
  import { MouseController } from './MouseController';
7
+ import { Config, Flags } from '../Config/Config';
7
8
 
8
9
  /**
9
10
  * A mouse controller that allows the mouse to freely float over the video document.
@@ -18,13 +19,21 @@ export class MouseControllerHovering extends MouseController {
18
19
  onMouseMoveListener: (event: MouseEvent) => void;
19
20
  onContextMenuListener: (event: MouseEvent) => void;
20
21
 
22
+ // Buttons currently held down. While non-empty, mousemove/mouseup are
23
+ // listened for on `window` rather than the video element so the press is
24
+ // tracked even when the cursor leaves the element. UE pairs every
25
+ // MouseDown with a later MouseUp; without this the engine can be left
26
+ // with a stuck button when the user releases outside the video element.
27
+ private pressedButtons = new Set<number>();
28
+
21
29
  constructor(
22
30
  streamMessageController: StreamMessageController,
23
31
  videoPlayer: VideoPlayer,
24
32
  coordinateConverter: InputCoordTranslator,
25
- activeKeys: ActiveKeys
33
+ activeKeys: ActiveKeys,
34
+ config: Config
26
35
  ) {
27
- super(streamMessageController, videoPlayer, coordinateConverter, activeKeys);
36
+ super(streamMessageController, videoPlayer, coordinateConverter, activeKeys, config);
28
37
  this.videoElementParent = videoPlayer.getVideoParentElement() as HTMLDivElement;
29
38
  this.onMouseUpListener = this.onMouseUp.bind(this);
30
39
  this.onMouseDownListener = this.onMouseDown.bind(this);
@@ -52,26 +61,73 @@ export class MouseControllerHovering extends MouseController {
52
61
  this.videoElementParent.removeEventListener('contextmenu', this.onContextMenuListener);
53
62
  this.videoElementParent.removeEventListener('wheel', this.onMouseWheelListener);
54
63
  this.videoElementParent.removeEventListener('dblclick', this.onMouseDblClickListener);
64
+ // If a button was held when unregister was called, clean up the
65
+ // window-level listeners too.
66
+ if (this.pressedButtons.size > 0) {
67
+ window.removeEventListener('mousemove', this.onMouseMoveListener);
68
+ window.removeEventListener('mouseup', this.onMouseUpListener);
69
+ this.pressedButtons.clear();
70
+ }
55
71
 
56
72
  super.unregister();
57
73
  }
58
74
 
75
+ private startCapturing() {
76
+ // Move move/up listeners off the element and onto the window so they
77
+ // keep firing while the cursor is outside the video.
78
+ this.videoElementParent.removeEventListener('mousemove', this.onMouseMoveListener);
79
+ this.videoElementParent.removeEventListener('mouseup', this.onMouseUpListener);
80
+ window.addEventListener('mousemove', this.onMouseMoveListener);
81
+ window.addEventListener('mouseup', this.onMouseUpListener);
82
+ }
83
+
84
+ private stopCapturing() {
85
+ window.removeEventListener('mousemove', this.onMouseMoveListener);
86
+ window.removeEventListener('mouseup', this.onMouseUpListener);
87
+ this.videoElementParent.addEventListener('mousemove', this.onMouseMoveListener);
88
+ this.videoElementParent.addEventListener('mouseup', this.onMouseUpListener);
89
+ }
90
+
91
+ /**
92
+ * Compute (offsetX, offsetY) relative to the video element from a window-
93
+ * level event whose `target` may be any other element on the page.
94
+ */
95
+ private offsetFromVideo(event: MouseEvent): { x: number; y: number } {
96
+ if (event.currentTarget === this.videoElementParent) {
97
+ return { x: event.offsetX, y: event.offsetY };
98
+ }
99
+ const rect = this.videoElementParent.getBoundingClientRect();
100
+ return { x: event.clientX - rect.left, y: event.clientY - rect.top };
101
+ }
102
+
59
103
  private onMouseDown(event: MouseEvent) {
60
104
  if (!this.videoPlayer.isVideoReady()) {
61
105
  return;
62
106
  }
63
- const coord = this.coordinateConverter.translateUnsigned(event.offsetX, event.offsetY);
107
+ const off = this.offsetFromVideo(event);
108
+ const coord = this.coordinateConverter.translateUnsigned(off.x, off.y);
64
109
  this.streamMessageController.toStreamerHandlers.get('MouseDown')([event.button, coord.x, coord.y]);
65
110
  event.preventDefault();
111
+
112
+ if (this.pressedButtons.size === 0) {
113
+ this.startCapturing();
114
+ }
115
+ this.pressedButtons.add(event.button);
66
116
  }
67
117
 
68
118
  private onMouseUp(event: MouseEvent) {
69
119
  if (!this.videoPlayer.isVideoReady()) {
70
120
  return;
71
121
  }
72
- const coord = this.coordinateConverter.translateUnsigned(event.offsetX, event.offsetY);
122
+ const off = this.offsetFromVideo(event);
123
+ const coord = this.coordinateConverter.translateUnsigned(off.x, off.y);
73
124
  this.streamMessageController.toStreamerHandlers.get('MouseUp')([event.button, coord.x, coord.y]);
74
125
  event.preventDefault();
126
+
127
+ this.pressedButtons.delete(event.button);
128
+ if (this.pressedButtons.size === 0) {
129
+ this.stopCapturing();
130
+ }
75
131
  }
76
132
 
77
133
  private onContextMenu(event: MouseEvent) {
@@ -85,7 +141,8 @@ export class MouseControllerHovering extends MouseController {
85
141
  if (!this.videoPlayer.isVideoReady()) {
86
142
  return;
87
143
  }
88
- const coord = this.coordinateConverter.translateUnsigned(event.offsetX, event.offsetY);
144
+ const off = this.offsetFromVideo(event);
145
+ const coord = this.coordinateConverter.translateUnsigned(off.x, off.y);
89
146
  const delta = this.coordinateConverter.translateSigned(event.movementX, event.movementY);
90
147
  this.streamMessageController.toStreamerHandlers.get('MouseMove')([
91
148
  coord.x,
@@ -93,7 +150,12 @@ export class MouseControllerHovering extends MouseController {
93
150
  delta.x,
94
151
  delta.y
95
152
  ]);
96
- event.preventDefault();
153
+ // Only call preventDefault when the event originated on the video
154
+ // element. On window-level events the target may be a page element
155
+ // for which preventDefault would be wrong.
156
+ if (event.currentTarget === this.videoElementParent) {
157
+ event.preventDefault();
158
+ }
97
159
  }
98
160
 
99
161
  private onMouseWheel(event: WheelEvent) {
@@ -115,5 +177,16 @@ export class MouseControllerHovering extends MouseController {
115
177
  }
116
178
  const coord = this.coordinateConverter.translateUnsigned(event.offsetX, event.offsetY);
117
179
  this.streamMessageController.toStreamerHandlers.get('MouseDouble')([event.button, coord.x, coord.y]);
180
+
181
+ // The streamer plugin treats `MouseDouble` as a press-class event (it routes to
182
+ // Slate's RoutePointerDoubleClickEvent / IGenericApplicationMessageHandler::OnMouseDoubleClick)
183
+ // but never synthesizes the matching release. The browser's preceding `mouseup` was
184
+ // already consumed by the prior `MouseUp` message, so without this UE is left thinking
185
+ // the button is still held — manifesting as e.g. camera pans that latch on after a
186
+ // double-click. See issue #10.
187
+ // Disable Flags.MouseDoubleClickAutoRelease to restore the pre-fix behaviour.
188
+ if (this.config.isFlagEnabled(Flags.MouseDoubleClickAutoRelease)) {
189
+ this.streamMessageController.toStreamerHandlers.get('MouseUp')([event.button, coord.x, coord.y]);
190
+ }
118
191
  }
119
192
  }
@@ -5,6 +5,7 @@ import { InputCoordTranslator, TranslatedCoordUnsigned } from '../Util/InputCoor
5
5
  import { VideoPlayer } from '../VideoPlayer/VideoPlayer';
6
6
  import type { ActiveKeys } from './InputClassesFactory';
7
7
  import { MouseController } from './MouseController';
8
+ import { Config, Flags } from '../Config/Config';
8
9
 
9
10
  /**
10
11
  * A mouse controller that locks the mouse to the video document and prevents it from leaving the window
@@ -28,9 +29,10 @@ export class MouseControllerLocked extends MouseController {
28
29
  streamMessageController: StreamMessageController,
29
30
  videoPlayer: VideoPlayer,
30
31
  coordinateConverter: InputCoordTranslator,
31
- activeKeys: ActiveKeys
32
+ activeKeys: ActiveKeys,
33
+ config: Config
32
34
  ) {
33
- super(streamMessageController, videoPlayer, coordinateConverter, activeKeys);
35
+ super(streamMessageController, videoPlayer, coordinateConverter, activeKeys, config);
34
36
  this.videoElementParent = videoPlayer.getVideoParentElement() as HTMLDivElement;
35
37
  this.x = this.videoElementParent.getBoundingClientRect().width / 2;
36
38
  this.y = this.videoElementParent.getBoundingClientRect().height / 2;
@@ -85,7 +87,7 @@ export class MouseControllerLocked extends MouseController {
85
87
  }
86
88
 
87
89
  private onRequestLock() {
88
- this.videoElementParent.requestPointerLock();
90
+ void this.videoElementParent.requestPointerLock();
89
91
  }
90
92
 
91
93
  private onLockStateChange() {
@@ -191,5 +193,20 @@ export class MouseControllerLocked extends MouseController {
191
193
  this.normalizedCoord.x,
192
194
  this.normalizedCoord.y
193
195
  ]);
196
+
197
+ // The streamer plugin treats `MouseDouble` as a press-class event (it routes to
198
+ // Slate's RoutePointerDoubleClickEvent / IGenericApplicationMessageHandler::OnMouseDoubleClick)
199
+ // but never synthesizes the matching release. The browser's preceding `mouseup` was
200
+ // already consumed by the prior `MouseUp` message, so without this UE is left thinking
201
+ // the button is still held — manifesting as e.g. camera pans that latch on after a
202
+ // double-click. See issue #10.
203
+ // Disable Flags.MouseDoubleClickAutoRelease to restore the pre-fix behaviour.
204
+ if (this.config.isFlagEnabled(Flags.MouseDoubleClickAutoRelease)) {
205
+ this.streamMessageController.toStreamerHandlers.get('MouseUp')([
206
+ event.button,
207
+ this.normalizedCoord.x,
208
+ this.normalizedCoord.y
209
+ ]);
210
+ }
194
211
  }
195
212
  }
@@ -60,7 +60,7 @@ export class PeerConnectionController {
60
60
  * Create an offer for the Web RTC handshake and send the offer to the signaling server via websocket
61
61
  * @param offerOptions - RTC Offer Options
62
62
  */
63
- async createOffer(offerOptions: RTCOfferOptions, config: Config) {
63
+ createOffer(offerOptions: RTCOfferOptions, config: Config) {
64
64
  Logger.Info('Create Offer');
65
65
 
66
66
  const isLocalhostConnection = location.hostname === 'localhost' || location.hostname === '127.0.0.1';
@@ -78,13 +78,13 @@ export class PeerConnectionController {
78
78
  );
79
79
  }
80
80
 
81
- this.setupTransceiversAsync(useMic, useCamera).finally(() => {
81
+ void this.setupTransceiversAsync(useMic, useCamera).finally(() => {
82
82
  this.peerConnection
83
83
  ?.createOffer(offerOptions)
84
84
  .then((offer: RTCSessionDescriptionInit) => {
85
85
  this.showTextOverlayConnecting();
86
86
  offer.sdp = this.mungeSDP(offer.sdp, useMic);
87
- this.peerConnection?.setLocalDescription(offer);
87
+ void this.peerConnection?.setLocalDescription(offer);
88
88
  this.onSendWebRTCOffer(offer);
89
89
  })
90
90
  .catch(() => {
@@ -96,7 +96,7 @@ export class PeerConnectionController {
96
96
  /**
97
97
  * Receive offer from UE side and process it as the remote description of this peer connection
98
98
  */
99
- async receiveOffer(offer: RTCSessionDescriptionInit, config: Config) {
99
+ receiveOffer(offer: RTCSessionDescriptionInit, config: Config) {
100
100
  Logger.Info('Receive Offer');
101
101
 
102
102
  // If UE or JSStreamer did send abs-capture-time RTP header extension to a non-Chrome browser
@@ -110,7 +110,7 @@ export class PeerConnectionController {
110
110
  );
111
111
  }
112
112
 
113
- this.peerConnection?.setRemoteDescription(offer).then(() => {
113
+ void this.peerConnection?.setRemoteDescription(offer).then(() => {
114
114
  // Fire event for when remote offer description is set
115
115
  this.onSetRemoteDescription(offer);
116
116
 
@@ -136,7 +136,7 @@ export class PeerConnectionController {
136
136
  this.fuzzyIntersectUEAndBrowserCodecs(offer)
137
137
  );
138
138
 
139
- this.setupTransceiversAsync(useMic, useCamera).finally(() => {
139
+ void this.setupTransceiversAsync(useMic, useCamera).finally(() => {
140
140
  this.peerConnection
141
141
  ?.createAnswer()
142
142
  .then((Answer: RTCSessionDescriptionInit) => {
@@ -158,7 +158,7 @@ export class PeerConnectionController {
158
158
  * @param answer - RTC Session Descriptor from the Signaling Server
159
159
  */
160
160
  receiveAnswer(answer: RTCSessionDescriptionInit) {
161
- this.peerConnection?.setRemoteDescription(answer);
161
+ void this.peerConnection?.setRemoteDescription(answer);
162
162
 
163
163
  // Add our list of preferred codecs, in order of preference
164
164
  this.config.setOptionSettingOptions(
@@ -171,7 +171,7 @@ export class PeerConnectionController {
171
171
  * Generate Aggregated Stats and then fire a onVideo Stats event
172
172
  */
173
173
  generateStats() {
174
- this.peerConnection.getStats().then((statsData: RTCStatsReport) => {
174
+ void this.peerConnection?.getStats().then((statsData: RTCStatsReport) => {
175
175
  this.aggregatedStats.processStats(statsData);
176
176
 
177
177
  this.onVideoStats(this.aggregatedStats);
@@ -293,15 +293,15 @@ export class PeerConnectionController {
293
293
  }
294
294
  }
295
295
 
296
- this.peerConnection?.addIceCandidate(iceCandidate);
296
+ void this.peerConnection?.addIceCandidate(iceCandidate);
297
297
  }
298
298
 
299
299
  /**
300
300
  * When the RTC Peer Connection Signaling server state Changes
301
301
  * @param state - Signaling Server State Change Event
302
302
  */
303
- handleSignalStateChange(state: Event) {
304
- Logger.Info('signaling state change: ' + state);
303
+ handleSignalStateChange(_state: Event) {
304
+ Logger.Info('signaling state change: ' + this.peerConnection?.signalingState);
305
305
  }
306
306
 
307
307
  /**
@@ -309,7 +309,7 @@ export class PeerConnectionController {
309
309
  * @param state - Ice Connection State
310
310
  */
311
311
  handleIceConnectionStateChange(state: Event) {
312
- Logger.Info('ice connection state change: ' + state);
312
+ Logger.Info('ice connection state change: ' + this.peerConnection?.iceConnectionState);
313
313
  this.onIceConnectionStateChange(state);
314
314
  }
315
315
 
@@ -326,13 +326,13 @@ export class PeerConnectionController {
326
326
  * @param event - The webRtc track event
327
327
  */
328
328
  handleOnTrack(event: RTCTrackEvent) {
329
- if (event.streams.length < 1 || event.streams[0].id == 'probator') {
329
+ if (event.streams.length < 1 || event.streams[0].id === 'probator') {
330
330
  return;
331
331
  }
332
- if (event.track.kind == 'video') {
332
+ if (event.track.kind === 'video') {
333
333
  this.videoTrack = event.track;
334
334
  }
335
- if (event.track.kind == 'audio') {
335
+ if (event.track.kind === 'audio') {
336
336
  this.audioTrack = event.track;
337
337
  }
338
338
  this.onTrack(event);
@@ -538,8 +538,8 @@ export class PeerConnectionController {
538
538
  for (const transceiver of this.peerConnection?.getTransceivers() ?? []) {
539
539
  if (RTCUtils.canTransceiverReceiveVideo(transceiver)) {
540
540
  for (const track of stream.getTracks()) {
541
- if (track.kind && track.kind == 'video') {
542
- transceiver.sender.replaceTrack(track);
541
+ if (track.kind === 'video') {
542
+ void transceiver.sender.replaceTrack(track);
543
543
  transceiver.direction = 'sendrecv';
544
544
  }
545
545
  }
@@ -547,7 +547,7 @@ export class PeerConnectionController {
547
547
  }
548
548
  } else {
549
549
  for (const track of stream.getTracks()) {
550
- if (track.kind && track.kind == 'video') {
550
+ if (track.kind === 'video') {
551
551
  this.peerConnection?.addTransceiver(track, {
552
552
  direction: 'sendrecv'
553
553
  });
@@ -587,8 +587,8 @@ export class PeerConnectionController {
587
587
  for (const transceiver of this.peerConnection?.getTransceivers() ?? []) {
588
588
  if (RTCUtils.canTransceiverReceiveAudio(transceiver)) {
589
589
  for (const track of stream.getTracks()) {
590
- if (track.kind && track.kind == 'audio') {
591
- transceiver.sender.replaceTrack(track);
590
+ if (track.kind === 'audio') {
591
+ void transceiver.sender.replaceTrack(track);
592
592
  transceiver.direction = 'sendrecv';
593
593
  }
594
594
  }
@@ -596,7 +596,7 @@ export class PeerConnectionController {
596
596
  }
597
597
  } else {
598
598
  for (const track of stream.getTracks()) {
599
- if (track.kind && track.kind == 'audio') {
599
+ if (track.kind === 'audio') {
600
600
  this.peerConnection?.addTransceiver(track, {
601
601
  direction: 'sendrecv'
602
602
  });
@@ -692,7 +692,7 @@ export class PeerConnectionController {
692
692
  .join(';');
693
693
  const match = matcher.exec(str);
694
694
  if (match !== null) {
695
- if (c.name == 'VP9') {
695
+ if (c.name === 'VP9') {
696
696
  // UE answers don't specify profile but we know we want profile 0
697
697
  c.parameters = {
698
698
  'profile-id': '0'
@@ -518,6 +518,32 @@ export class PixelStreaming {
518
518
  _onVideoInitialized() {
519
519
  this._eventEmitter.dispatchEvent(new VideoInitializedEvent());
520
520
  this._videoStartTime = Date.now();
521
+ this.checkForAutoEnterVR();
522
+ }
523
+
524
+ /**
525
+ * If the AutoEnterVR flag is set and an immersive-vr session is supported,
526
+ * request the WebXR session. Browsers typically require a user gesture for
527
+ * `requestSession`; if no gesture is currently active the request will be
528
+ * rejected and a warning is logged. Callers that need a guaranteed entry
529
+ * (e.g. AutoConnect from a fresh page load) should still wire up a button.
530
+ */
531
+ private checkForAutoEnterVR() {
532
+ if (!this.config.isFlagEnabled(Flags.AutoEnterVR)) {
533
+ return;
534
+ }
535
+ WebXRController.isSessionSupported('immersive-vr')
536
+ .then((supported: boolean) => {
537
+ if (!supported) {
538
+ Logger.Info('AutoEnterVR is on but immersive-vr is not supported on this device.');
539
+ return;
540
+ }
541
+ this._webXrController.xrClicked();
542
+ })
543
+ .catch((err: unknown) => {
544
+ const msg = err instanceof Error ? err.message : JSON.stringify(err);
545
+ Logger.Warning(`AutoEnterVR check failed: ${msg}`);
546
+ });
521
547
  }
522
548
 
523
549
  /**
@@ -62,7 +62,6 @@ export class SendMessageController {
62
62
  }
63
63
 
64
64
  let byteLength = 0;
65
- const textEncoder = new TextEncoder();
66
65
  // One loop to calculate the length in bytes of all of the provided data
67
66
  messageData.forEach((element: number | string, idx: number) => {
68
67
  const type = messageFormat.structure[idx];
@@ -90,8 +89,8 @@ export class SendMessageController {
90
89
  case 'string':
91
90
  // 2 bytes for string length
92
91
  byteLength += 2;
93
- // 2 bytes per characters
94
- byteLength += 2 * textEncoder.encode(element as string).length;
92
+ // 2 bytes per character
93
+ byteLength += 2 * (element as string).length;
95
94
  break;
96
95
  }
97
96
  });
@@ -8,8 +8,8 @@ export class ToStreamerMessage {
8
8
  }
9
9
 
10
10
  export class StreamMessageController {
11
- toStreamerHandlers: Map<string, (messageData?: Array<number | string> | undefined) => void>;
12
- fromStreamerHandlers: Map<string, (messageType: string, messageData?: ArrayBuffer | undefined) => void>;
11
+ toStreamerHandlers: Map<string, (messageData?: Array<number | string>) => void>;
12
+ fromStreamerHandlers: Map<string, (messageType: string, messageData?: ArrayBuffer) => void>;
13
13
  // Type Format
14
14
  toStreamerMessages: Map<string, ToStreamerMessage>;
15
15
  // ID Type
@@ -204,7 +204,7 @@ export class StreamMessageController {
204
204
  registerMessageHandler(
205
205
  messageDirection: MessageDirection,
206
206
  messageType: string,
207
- messageHandler: (messageData?: unknown | undefined) => void
207
+ messageHandler: (messageData?: unknown) => void
208
208
  ) {
209
209
  switch (messageDirection) {
210
210
  case MessageDirection.ToStreamer:
@@ -27,7 +27,7 @@ export class StreamController {
27
27
  Logger.Info('handleOnTrack ' + JSON.stringify(rtcTrackEvent.streams));
28
28
  // Do not add the track if the ID is `probator` as this is special track created by mediasoup for bitrate probing.
29
29
  // Refer to https://github.com/EpicGamesExt/PixelStreamingInfrastructure/pull/86 for more details.
30
- if (rtcTrackEvent.streams.length < 1 || rtcTrackEvent.streams[0].id == 'probator') {
30
+ if (rtcTrackEvent.streams.length < 1 || rtcTrackEvent.streams[0].id === 'probator') {
31
31
  return;
32
32
  }
33
33
 
@@ -44,11 +44,11 @@ export class StreamController {
44
44
  );
45
45
  }
46
46
 
47
- if (rtcTrackEvent.track.kind == 'audio') {
47
+ if (rtcTrackEvent.track.kind === 'audio') {
48
48
  this.CreateAudioTrack(rtcTrackEvent.streams[0]);
49
49
  return;
50
50
  } else if (
51
- rtcTrackEvent.track.kind == 'video' &&
51
+ rtcTrackEvent.track.kind === 'video' &&
52
52
  videoElement.srcObject !== rtcTrackEvent.streams[0]
53
53
  ) {
54
54
  videoElement.srcObject = rtcTrackEvent.streams[0];
@@ -0,0 +1,141 @@
1
+ import { Logger } from '@epicgames-ps/lib-pixelstreamingcommon-ue5.5';
2
+ import { Config, Flags, NumericParameters } from '../Config/Config';
3
+ import { mockRTCRtpReceiver, unmockRTCRtpReceiver } from '../__test__/mockRTCRtpReceiver';
4
+ import { VideoPlayer } from './VideoPlayer';
5
+
6
+ /**
7
+ * Tests for the ViewportResScale numeric parameter added to VideoPlayer.
8
+ *
9
+ * The callback onMatchViewportResolutionCallback is invoked with the scaled
10
+ * viewport dimensions when MatchViewportResolution is enabled. We validate:
11
+ * - default scale (1.0) leaves dimensions unchanged
12
+ * - explicit scale multiplies both dimensions
13
+ * - non-integer products are rounded to integers
14
+ * - dimensions > 4096 emit a warning via Logger
15
+ * - a Config missing the setting falls back to 1.0 instead of throwing
16
+ */
17
+ describe('VideoPlayer.updateVideoStreamSize — ViewportResScale', () => {
18
+ let parent: HTMLDivElement;
19
+ let config: Config;
20
+ let player: VideoPlayer;
21
+ let callback: jest.Mock;
22
+
23
+ const setViewportSize = (w: number, h: number) => {
24
+ Object.defineProperty(parent, 'clientWidth', { configurable: true, value: w });
25
+ Object.defineProperty(parent, 'clientHeight', { configurable: true, value: h });
26
+ };
27
+
28
+ beforeEach(() => {
29
+ mockRTCRtpReceiver();
30
+ parent = document.createElement('div');
31
+ document.body.appendChild(parent);
32
+
33
+ config = new Config({ initialSettings: { [Flags.MatchViewportResolution]: true } });
34
+
35
+ player = new VideoPlayer(parent, config);
36
+ callback = jest.fn();
37
+ player.onMatchViewportResolutionCallback = callback;
38
+
39
+ // Bypass the 300ms throttle in updateVideoStreamSize.
40
+ (player as unknown as { lastTimeResized: number }).lastTimeResized = 0;
41
+ });
42
+
43
+ afterEach(() => {
44
+ player.destroy();
45
+ parent.remove();
46
+ unmockRTCRtpReceiver();
47
+ jest.restoreAllMocks();
48
+ });
49
+
50
+ it('passes viewport dimensions through unchanged when scale is 1.0 (default)', () => {
51
+ setViewportSize(375, 667);
52
+ player.updateVideoStreamSize();
53
+ expect(callback).toHaveBeenCalledWith(375, 667);
54
+ });
55
+
56
+ it('multiplies both dimensions by the configured scale', () => {
57
+ config.setNumericSetting(NumericParameters.ViewportResScale, 2.0);
58
+ setViewportSize(375, 667);
59
+
60
+ // lastTimeResized was updated on construction, reset again.
61
+ (player as unknown as { lastTimeResized: number }).lastTimeResized = 0;
62
+ player.updateVideoStreamSize();
63
+
64
+ expect(callback).toHaveBeenCalledWith(750, 1334);
65
+ });
66
+
67
+ it('rounds non-integer products to integers', () => {
68
+ config.setNumericSetting(NumericParameters.ViewportResScale, 1.5);
69
+ setViewportSize(375, 667);
70
+
71
+ (player as unknown as { lastTimeResized: number }).lastTimeResized = 0;
72
+ player.updateVideoStreamSize();
73
+
74
+ // 375 * 1.5 = 562.5 → 563, 667 * 1.5 = 1000.5 → 1001
75
+ expect(callback).toHaveBeenCalledWith(563, 1001);
76
+ const [w, h] = callback.mock.calls[0] as [number, number];
77
+ expect(Number.isInteger(w)).toBe(true);
78
+ expect(Number.isInteger(h)).toBe(true);
79
+ });
80
+
81
+ it('logs a warning when scaled width or height exceeds 4096', () => {
82
+ const warnSpy = jest.spyOn(Logger, 'Warning').mockImplementation(() => {});
83
+
84
+ config.setNumericSetting(NumericParameters.ViewportResScale, 3.0);
85
+ setViewportSize(2000, 1000); // 2000*3 = 6000 > 4096
86
+
87
+ (player as unknown as { lastTimeResized: number }).lastTimeResized = 0;
88
+ player.updateVideoStreamSize();
89
+
90
+ expect(warnSpy).toHaveBeenCalledTimes(1);
91
+ expect(warnSpy.mock.calls[0][0]).toContain('4096');
92
+ expect(warnSpy.mock.calls[0][0]).toContain('6000');
93
+ expect(callback).toHaveBeenCalledWith(6000, 3000);
94
+ });
95
+
96
+ it('does not warn when scaled dimensions stay within the encoder limit', () => {
97
+ const warnSpy = jest.spyOn(Logger, 'Warning').mockImplementation(() => {});
98
+
99
+ config.setNumericSetting(NumericParameters.ViewportResScale, 2.0);
100
+ setViewportSize(1920, 1080); // 3840 x 2160, under 4096
101
+
102
+ (player as unknown as { lastTimeResized: number }).lastTimeResized = 0;
103
+ player.updateVideoStreamSize();
104
+
105
+ expect(warnSpy).not.toHaveBeenCalled();
106
+ });
107
+
108
+ it('falls back to scale 1.0 when the setting is not registered on the Config', () => {
109
+ const strippedConfig = new Config({ initialSettings: { [Flags.MatchViewportResolution]: true } });
110
+ // Remove the registration to simulate a custom Config subclass that omits it.
111
+ const params = (strippedConfig as unknown as { numericParameters: Map<string, unknown> })
112
+ .numericParameters;
113
+ params.delete(NumericParameters.ViewportResScale);
114
+
115
+ const strippedParent = document.createElement('div');
116
+ document.body.appendChild(strippedParent);
117
+ const strippedPlayer = new VideoPlayer(strippedParent, strippedConfig);
118
+ const strippedCallback = jest.fn();
119
+ strippedPlayer.onMatchViewportResolutionCallback = strippedCallback;
120
+
121
+ Object.defineProperty(strippedParent, 'clientWidth', { configurable: true, value: 500 });
122
+ Object.defineProperty(strippedParent, 'clientHeight', { configurable: true, value: 400 });
123
+
124
+ (strippedPlayer as unknown as { lastTimeResized: number }).lastTimeResized = 0;
125
+ expect(() => strippedPlayer.updateVideoStreamSize()).not.toThrow();
126
+ expect(strippedCallback).toHaveBeenCalledWith(500, 400);
127
+
128
+ strippedPlayer.destroy();
129
+ strippedParent.remove();
130
+ });
131
+
132
+ it('does not invoke the callback when MatchViewportResolution is disabled', () => {
133
+ config.setFlagEnabled(Flags.MatchViewportResolution, false);
134
+ setViewportSize(375, 667);
135
+
136
+ (player as unknown as { lastTimeResized: number }).lastTimeResized = 0;
137
+ player.updateVideoStreamSize();
138
+
139
+ expect(callback).not.toHaveBeenCalled();
140
+ });
141
+ });