@epicgames-ps/lib-pixelstreamingfrontend-ue5.5 1.2.5 → 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.
- package/CHANGELOG.md +19 -2
- package/dist/cjs/AFK/AFKController.js +2 -2
- package/dist/cjs/AFK/AFKController.js.map +1 -1
- package/dist/cjs/Config/Config.js +19 -0
- package/dist/cjs/Config/Config.js.map +1 -1
- package/dist/cjs/DataChannel/DataChannelController.js +2 -2
- package/dist/cjs/DataChannel/DataChannelController.js.map +1 -1
- package/dist/cjs/DataChannel/DataChannelSender.js +2 -2
- package/dist/cjs/DataChannel/DataChannelSender.js.map +1 -1
- package/dist/cjs/Inputs/GamepadController.js +5 -6
- package/dist/cjs/Inputs/GamepadController.js.map +1 -1
- package/dist/cjs/Inputs/InputClassesFactory.js +3 -3
- package/dist/cjs/Inputs/InputClassesFactory.js.map +1 -1
- package/dist/cjs/Inputs/KeyCodes.js +13 -1
- package/dist/cjs/Inputs/KeyCodes.js.map +1 -1
- package/dist/cjs/Inputs/KeyboardController.js +12 -0
- package/dist/cjs/Inputs/KeyboardController.js.map +1 -1
- package/dist/cjs/Inputs/MouseController.js +2 -1
- package/dist/cjs/Inputs/MouseController.js.map +1 -1
- package/dist/cjs/Inputs/MouseControllerHovering.js +71 -6
- package/dist/cjs/Inputs/MouseControllerHovering.js.map +1 -1
- package/dist/cjs/Inputs/MouseControllerLocked.js +18 -3
- package/dist/cjs/Inputs/MouseControllerLocked.js.map +1 -1
- package/dist/cjs/PeerConnectionController/PeerConnectionController.js +71 -72
- package/dist/cjs/PeerConnectionController/PeerConnectionController.js.map +1 -1
- package/dist/cjs/PixelStreaming/PixelStreaming.js +25 -0
- package/dist/cjs/PixelStreaming/PixelStreaming.js.map +1 -1
- package/dist/cjs/UeInstanceMessage/SendMessageController.js +2 -3
- package/dist/cjs/UeInstanceMessage/SendMessageController.js.map +1 -1
- package/dist/cjs/UeInstanceMessage/StreamMessageController.js.map +1 -1
- package/dist/cjs/VideoPlayer/StreamController.js +3 -3
- package/dist/cjs/VideoPlayer/StreamController.js.map +1 -1
- package/dist/cjs/VideoPlayer/VideoPlayer.js +17 -6
- package/dist/cjs/VideoPlayer/VideoPlayer.js.map +1 -1
- package/dist/cjs/WebRtcPlayer/WebRtcPlayerController.js +2 -2
- package/dist/cjs/WebRtcPlayer/WebRtcPlayerController.js.map +1 -1
- package/dist/cjs/WebXR/WebXRController.js +5 -5
- package/dist/cjs/WebXR/WebXRController.js.map +1 -1
- package/dist/esm/AFK/AFKController.js +2 -2
- package/dist/esm/AFK/AFKController.js.map +1 -1
- package/dist/esm/Config/Config.js +19 -0
- package/dist/esm/Config/Config.js.map +1 -1
- package/dist/esm/DataChannel/DataChannelController.js +2 -2
- package/dist/esm/DataChannel/DataChannelController.js.map +1 -1
- package/dist/esm/DataChannel/DataChannelSender.js +2 -2
- package/dist/esm/DataChannel/DataChannelSender.js.map +1 -1
- package/dist/esm/Inputs/GamepadController.js +5 -6
- package/dist/esm/Inputs/GamepadController.js.map +1 -1
- package/dist/esm/Inputs/InputClassesFactory.js +3 -3
- package/dist/esm/Inputs/InputClassesFactory.js.map +1 -1
- package/dist/esm/Inputs/KeyCodes.js +13 -1
- package/dist/esm/Inputs/KeyCodes.js.map +1 -1
- package/dist/esm/Inputs/KeyboardController.js +12 -0
- package/dist/esm/Inputs/KeyboardController.js.map +1 -1
- package/dist/esm/Inputs/MouseController.js +2 -1
- package/dist/esm/Inputs/MouseController.js.map +1 -1
- package/dist/esm/Inputs/MouseControllerHovering.js +71 -6
- package/dist/esm/Inputs/MouseControllerHovering.js.map +1 -1
- package/dist/esm/Inputs/MouseControllerLocked.js +18 -3
- package/dist/esm/Inputs/MouseControllerLocked.js.map +1 -1
- package/dist/esm/PeerConnectionController/PeerConnectionController.js +71 -72
- package/dist/esm/PeerConnectionController/PeerConnectionController.js.map +1 -1
- package/dist/esm/PixelStreaming/PixelStreaming.js +25 -0
- package/dist/esm/PixelStreaming/PixelStreaming.js.map +1 -1
- package/dist/esm/UeInstanceMessage/SendMessageController.js +2 -3
- package/dist/esm/UeInstanceMessage/SendMessageController.js.map +1 -1
- package/dist/esm/UeInstanceMessage/StreamMessageController.js.map +1 -1
- package/dist/esm/VideoPlayer/StreamController.js +3 -3
- package/dist/esm/VideoPlayer/StreamController.js.map +1 -1
- package/dist/esm/VideoPlayer/VideoPlayer.js +18 -7
- package/dist/esm/VideoPlayer/VideoPlayer.js.map +1 -1
- package/dist/esm/WebRtcPlayer/WebRtcPlayerController.js +2 -2
- package/dist/esm/WebRtcPlayer/WebRtcPlayerController.js.map +1 -1
- package/dist/esm/WebXR/WebXRController.js +5 -5
- package/dist/esm/WebXR/WebXRController.js.map +1 -1
- package/dist/types/Config/Config.d.ts +8 -0
- package/dist/types/Inputs/InputClassesFactory.d.ts +1 -1
- package/dist/types/Inputs/MouseController.d.ts +3 -1
- package/dist/types/Inputs/MouseControllerHovering.d.ts +10 -1
- package/dist/types/Inputs/MouseControllerLocked.d.ts +2 -1
- package/dist/types/PeerConnectionController/PeerConnectionController.d.ts +3 -3
- package/dist/types/PixelStreaming/PixelStreaming.d.ts +9 -1
- package/dist/types/UeInstanceMessage/StreamMessageController.d.ts +3 -3
- package/dist/types/VideoPlayer/VideoPlayer.d.ts +1 -0
- package/eslint.config.mjs +1 -8
- package/package.json +4 -4
- package/src/AFK/AFKController.ts +2 -2
- package/src/Config/Config.ts +61 -9
- package/src/DataChannel/DataChannelController.ts +2 -2
- package/src/DataChannel/DataChannelSender.ts +2 -2
- package/src/Inputs/GamepadController.ts +5 -6
- package/src/Inputs/InputClassesFactory.ts +5 -3
- package/src/Inputs/KeyCodes.ts +13 -1
- package/src/Inputs/KeyboardController.ts +11 -1
- package/src/Inputs/MouseController.ts +5 -1
- package/src/Inputs/MouseControllerHovering.ts +79 -6
- package/src/Inputs/MouseControllerLocked.ts +20 -3
- package/src/PeerConnectionController/PeerConnectionController.ts +22 -22
- package/src/PixelStreaming/PixelStreaming.ts +26 -0
- package/src/UeInstanceMessage/SendMessageController.ts +2 -3
- package/src/UeInstanceMessage/StreamMessageController.ts +3 -3
- package/src/VideoPlayer/StreamController.ts +3 -3
- package/src/VideoPlayer/VideoPlayer.test.ts +141 -0
- package/src/VideoPlayer/VideoPlayer.ts +26 -10
- package/src/WebRtcPlayer/WebRtcPlayerController.ts +2 -2
- package/src/WebXR/WebXRController.ts +5 -5
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
304
|
-
Logger.Info('signaling state change: ' +
|
|
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: ' +
|
|
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
|
|
329
|
+
if (event.streams.length < 1 || event.streams[0].id === 'probator') {
|
|
330
330
|
return;
|
|
331
331
|
}
|
|
332
|
-
if (event.track.kind
|
|
332
|
+
if (event.track.kind === 'video') {
|
|
333
333
|
this.videoTrack = event.track;
|
|
334
334
|
}
|
|
335
|
-
if (event.track.kind
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
94
|
-
byteLength += 2 *
|
|
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>
|
|
12
|
-
fromStreamerHandlers: Map<string, (messageType: string, messageData?: ArrayBuffer
|
|
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
|
|
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
|
|
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
|
|
47
|
+
if (rtcTrackEvent.track.kind === 'audio') {
|
|
48
48
|
this.CreateAudioTrack(rtcTrackEvent.streams[0]);
|
|
49
49
|
return;
|
|
50
50
|
} else if (
|
|
51
|
-
rtcTrackEvent.track.kind
|
|
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
|
+
});
|