@epicgames-ps/lib-pixelstreamingfrontend-ue5.5 0.0.11 → 0.1.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/.cspell.json +35 -0
- package/dist/lib-pixelstreamingfrontend.esm.js +1 -1
- package/dist/lib-pixelstreamingfrontend.js +1 -1
- package/package.json +3 -2
- package/readme.md +1 -1
- package/src/Config/Config.ts +1 -1
- package/src/Inputs/FakeTouchController.ts +1 -1
- package/src/Inputs/GamepadController.ts +2 -2
- package/src/Inputs/IMouseEvents.ts +1 -1
- package/src/Inputs/ITouchController.ts +1 -1
- package/src/Inputs/KeyboardController.ts +2 -2
- package/src/Inputs/LockedMouseEvents.ts +2 -2
- package/src/Inputs/MouseController.ts +1 -1
- package/src/Inputs/TouchController.ts +1 -1
- package/src/Inputs/XRGamepadController.ts +44 -26
- package/src/PeerConnectionController/PeerConnectionController.ts +20 -4
- package/src/PixelStreaming/PixelStreaming.test.ts +34 -51
- package/src/PixelStreaming/PixelStreaming.ts +5 -5
- package/src/Util/CoordinateConverter.ts +6 -5
- package/src/Util/RTCUtils.ts +2 -2
- package/src/VideoPlayer/StreamController.ts +6 -0
- package/src/WebRtcPlayer/WebRtcPlayerController.ts +21 -2
- package/src/WebXR/WebXRController.ts +368 -179
- package/src/__test__/mockWebSocket.ts +5 -5
- package/types/Inputs/GamepadController.d.ts +1 -1
- package/types/Inputs/IMouseEvents.d.ts +1 -1
- package/types/Inputs/ITouchController.d.ts +1 -1
- package/types/Inputs/KeyboardController.d.ts +1 -1
- package/types/Inputs/LockedMouseEvents.d.ts +1 -1
- package/types/Inputs/XRGamepadController.d.ts +8 -1
- package/types/PeerConnectionController/PeerConnectionController.d.ts +2 -0
- package/types/PixelStreaming/PixelStreaming.d.ts +1 -1
- package/types/Util/RTCUtils.d.ts +2 -2
- package/types/WebXR/WebXRController.d.ts +19 -3
- package/src/Util/WebGLUtils.ts +0 -49
- package/src/Util/WebXRUtils.ts +0 -25
- package/types/Util/WebGLUtils.d.ts +0 -4
- package/types/Util/WebXRUtils.d.ts +0 -9
|
@@ -2,8 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
import { Logger } from '@epicgames-ps/lib-pixelstreamingcommon-ue5.5';
|
|
4
4
|
import { WebRtcPlayerController } from '../WebRtcPlayer/WebRtcPlayerController';
|
|
5
|
-
import { WebGLUtils } from '../Util/WebGLUtils';
|
|
6
|
-
import { Controller } from '../Inputs/GamepadTypes';
|
|
7
5
|
import { XRGamepadController } from '../Inputs/XRGamepadController';
|
|
8
6
|
import { XrFrameEvent } from '../Util/EventEmitter'
|
|
9
7
|
import { Flags } from '../pixelstreamingfrontend';
|
|
@@ -12,18 +10,31 @@ export class WebXRController {
|
|
|
12
10
|
private xrSession: XRSession;
|
|
13
11
|
private xrRefSpace: XRReferenceSpace;
|
|
14
12
|
private gl: WebGL2RenderingContext;
|
|
13
|
+
private xrViewerPose : XRViewerPose = null;
|
|
14
|
+
// Used for comparisons to ensure two numbers are close enough.
|
|
15
|
+
private EPSILON = 0.0000001;
|
|
15
16
|
|
|
16
17
|
private positionLocation: number;
|
|
17
18
|
private texcoordLocation: number;
|
|
18
|
-
private resolutionLocation: WebGLUniformLocation;
|
|
19
|
-
private offsetLocation: WebGLUniformLocation;
|
|
20
19
|
|
|
21
20
|
private positionBuffer: WebGLBuffer;
|
|
22
21
|
private texcoordBuffer: WebGLBuffer;
|
|
23
22
|
|
|
23
|
+
private videoTexture: WebGLTexture = null;
|
|
24
|
+
private prevVideoWidth: number = 0;
|
|
25
|
+
private prevVideoHeight: number = 0;
|
|
26
|
+
|
|
24
27
|
private webRtcController: WebRtcPlayerController;
|
|
25
28
|
private xrGamepadController: XRGamepadController;
|
|
26
|
-
|
|
29
|
+
|
|
30
|
+
private leftView: XRView = null;
|
|
31
|
+
private rightView: XRView = null;
|
|
32
|
+
|
|
33
|
+
// Store the HMD data we have last sent (not all of it is needed every frame unless it changes)
|
|
34
|
+
private lastSentLeftEyeProj: Float32Array = null;
|
|
35
|
+
private lastSentRightEyeProj: Float32Array = null;
|
|
36
|
+
private lastSentRelativeLeftEyePos: DOMPointReadOnly = null;
|
|
37
|
+
private lastSentRelativeRightEyePos: DOMPointReadOnly = null;
|
|
27
38
|
|
|
28
39
|
onSessionStarted: EventTarget;
|
|
29
40
|
onSessionEnded: EventTarget;
|
|
@@ -32,7 +43,6 @@ export class WebXRController {
|
|
|
32
43
|
constructor(webRtcPlayerController: WebRtcPlayerController) {
|
|
33
44
|
this.xrSession = null;
|
|
34
45
|
this.webRtcController = webRtcPlayerController;
|
|
35
|
-
this.xrControllers = [];
|
|
36
46
|
this.xrGamepadController = new XRGamepadController(
|
|
37
47
|
this.webRtcController.streamMessageController
|
|
38
48
|
);
|
|
@@ -43,8 +53,15 @@ export class WebXRController {
|
|
|
43
53
|
|
|
44
54
|
public xrClicked() {
|
|
45
55
|
if (!this.xrSession) {
|
|
56
|
+
|
|
57
|
+
if(!navigator.xr){
|
|
58
|
+
Logger.Error(Logger.GetStackTrace(), "This browser does not support XR.");
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
46
62
|
navigator.xr
|
|
47
|
-
|
|
63
|
+
/* Request immersive-vr session without any optional features. */
|
|
64
|
+
.requestSession('immersive-vr', { optionalFeatures: [] })
|
|
48
65
|
.then((session: XRSession) => {
|
|
49
66
|
this.onXrSessionStarted(session);
|
|
50
67
|
});
|
|
@@ -59,31 +76,59 @@ export class WebXRController {
|
|
|
59
76
|
this.onSessionEnded.dispatchEvent(new Event('xrSessionEnded'));
|
|
60
77
|
}
|
|
61
78
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
this.xrSession = session;
|
|
66
|
-
this.xrSession.addEventListener('end', () => {
|
|
67
|
-
this.onXrSessionEnded();
|
|
68
|
-
});
|
|
69
|
-
|
|
79
|
+
initGL() {
|
|
80
|
+
if (this.gl) { return; }
|
|
70
81
|
const canvas = document.createElement('canvas');
|
|
71
82
|
this.gl = canvas.getContext('webgl2', {
|
|
72
83
|
xrCompatible: true
|
|
73
84
|
});
|
|
74
85
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
86
|
+
// Set our clear color
|
|
87
|
+
this.gl.clearColor(0.0, 0.0, 0.0, 1);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
initShaders() {
|
|
91
|
+
|
|
92
|
+
// shader source code
|
|
93
|
+
const vertexShaderSource: string =
|
|
94
|
+
`
|
|
95
|
+
attribute vec2 a_position;
|
|
96
|
+
attribute vec2 a_texCoord;
|
|
97
|
+
|
|
98
|
+
// varyings
|
|
99
|
+
varying vec2 v_texCoord;
|
|
100
|
+
|
|
101
|
+
void main() {
|
|
102
|
+
gl_Position = vec4(a_position.x, a_position.y, 0, 1);
|
|
103
|
+
// pass the texCoord to the fragment shader
|
|
104
|
+
// The GPU will interpolate this value between points.
|
|
105
|
+
v_texCoord = a_texCoord;
|
|
106
|
+
}
|
|
107
|
+
`;
|
|
108
|
+
|
|
109
|
+
const fragmentShaderSource: string =
|
|
110
|
+
`
|
|
111
|
+
precision mediump float;
|
|
112
|
+
|
|
113
|
+
// our texture
|
|
114
|
+
uniform sampler2D u_image;
|
|
115
|
+
|
|
116
|
+
// the texCoords passed in from the vertex shader.
|
|
117
|
+
varying vec2 v_texCoord;
|
|
118
|
+
|
|
119
|
+
void main() {
|
|
120
|
+
gl_FragColor = texture2D(u_image, v_texCoord);
|
|
121
|
+
}
|
|
122
|
+
`;
|
|
78
123
|
|
|
79
124
|
// setup vertex shader
|
|
80
125
|
const vertexShader = this.gl.createShader(this.gl.VERTEX_SHADER);
|
|
81
|
-
this.gl.shaderSource(vertexShader,
|
|
126
|
+
this.gl.shaderSource(vertexShader, vertexShaderSource);
|
|
82
127
|
this.gl.compileShader(vertexShader);
|
|
83
128
|
|
|
84
129
|
// setup fragment shader
|
|
85
130
|
const fragmentShader = this.gl.createShader(this.gl.FRAGMENT_SHADER);
|
|
86
|
-
this.gl.shaderSource(fragmentShader,
|
|
131
|
+
this.gl.shaderSource(fragmentShader, fragmentShaderSource);
|
|
87
132
|
this.gl.compileShader(fragmentShader);
|
|
88
133
|
|
|
89
134
|
// setup GLSL program
|
|
@@ -102,93 +147,298 @@ export class WebXRController {
|
|
|
102
147
|
shaderProgram,
|
|
103
148
|
'a_texCoord'
|
|
104
149
|
);
|
|
105
|
-
|
|
106
|
-
this.positionBuffer = this.gl.createBuffer();
|
|
107
|
-
// Bind it to ARRAY_BUFFER (think of it as ARRAY_BUFFER = positionBuffer)
|
|
108
|
-
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.positionBuffer);
|
|
109
|
-
|
|
110
|
-
// Turn on the position attribute
|
|
111
|
-
this.gl.enableVertexAttribArray(this.positionLocation);
|
|
112
|
-
// Create a texture.
|
|
113
|
-
const texture = this.gl.createTexture();
|
|
114
|
-
this.gl.bindTexture(this.gl.TEXTURE_2D, texture);
|
|
115
|
-
// Set the parameters so we can render any size image.
|
|
116
|
-
this.gl.texParameteri(
|
|
117
|
-
this.gl.TEXTURE_2D,
|
|
118
|
-
this.gl.TEXTURE_WRAP_S,
|
|
119
|
-
this.gl.CLAMP_TO_EDGE
|
|
120
|
-
);
|
|
121
|
-
this.gl.texParameteri(
|
|
122
|
-
this.gl.TEXTURE_2D,
|
|
123
|
-
this.gl.TEXTURE_WRAP_T,
|
|
124
|
-
this.gl.CLAMP_TO_EDGE
|
|
125
|
-
);
|
|
126
|
-
this.gl.texParameteri(
|
|
127
|
-
this.gl.TEXTURE_2D,
|
|
128
|
-
this.gl.TEXTURE_MIN_FILTER,
|
|
129
|
-
this.gl.NEAREST
|
|
130
|
-
);
|
|
131
|
-
this.gl.texParameteri(
|
|
132
|
-
this.gl.TEXTURE_2D,
|
|
133
|
-
this.gl.TEXTURE_MAG_FILTER,
|
|
134
|
-
this.gl.NEAREST
|
|
135
|
-
);
|
|
150
|
+
}
|
|
136
151
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
this.
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
152
|
+
updateVideoTexture(){
|
|
153
|
+
|
|
154
|
+
if(!this.videoTexture){
|
|
155
|
+
// Create our texture that we use in our shader
|
|
156
|
+
// and bind it once because we never use any other texture.
|
|
157
|
+
this.videoTexture = this.gl.createTexture();
|
|
158
|
+
this.gl.bindTexture(this.gl.TEXTURE_2D, this.videoTexture);
|
|
159
|
+
|
|
160
|
+
// Set the parameters so we can render any size image.
|
|
161
|
+
this.gl.texParameteri(
|
|
162
|
+
this.gl.TEXTURE_2D,
|
|
163
|
+
this.gl.TEXTURE_WRAP_S,
|
|
164
|
+
this.gl.CLAMP_TO_EDGE
|
|
165
|
+
);
|
|
166
|
+
this.gl.texParameteri(
|
|
167
|
+
this.gl.TEXTURE_2D,
|
|
168
|
+
this.gl.TEXTURE_WRAP_T,
|
|
169
|
+
this.gl.CLAMP_TO_EDGE
|
|
170
|
+
);
|
|
171
|
+
this.gl.texParameteri(
|
|
172
|
+
this.gl.TEXTURE_2D,
|
|
173
|
+
this.gl.TEXTURE_MIN_FILTER,
|
|
174
|
+
this.gl.LINEAR
|
|
175
|
+
);
|
|
176
|
+
this.gl.texParameteri(
|
|
177
|
+
this.gl.TEXTURE_2D,
|
|
178
|
+
this.gl.TEXTURE_MAG_FILTER,
|
|
179
|
+
this.gl.LINEAR
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const videoHeight = this.webRtcController.videoPlayer.getVideoElement().videoHeight;
|
|
184
|
+
const videoWidth = this.webRtcController.videoPlayer.getVideoElement().videoWidth;
|
|
185
|
+
|
|
186
|
+
if(this.prevVideoHeight != videoHeight || this.prevVideoWidth != videoWidth){
|
|
187
|
+
// Do full update of texture if dimensions do not match
|
|
188
|
+
this.gl.texImage2D(
|
|
189
|
+
this.gl.TEXTURE_2D,
|
|
190
|
+
0,
|
|
191
|
+
this.gl.RGBA,
|
|
192
|
+
videoWidth,
|
|
193
|
+
videoHeight,
|
|
194
|
+
0,
|
|
195
|
+
this.gl.RGBA,
|
|
196
|
+
this.gl.UNSIGNED_BYTE,
|
|
197
|
+
this.webRtcController.videoPlayer.getVideoElement()
|
|
198
|
+
);
|
|
199
|
+
} else {
|
|
200
|
+
// If dimensions match just update the sub region
|
|
201
|
+
this.gl.texSubImage2D(
|
|
202
|
+
this.gl.TEXTURE_2D,
|
|
203
|
+
0,
|
|
204
|
+
0,
|
|
205
|
+
0,
|
|
206
|
+
videoWidth,
|
|
207
|
+
videoHeight,
|
|
208
|
+
this.gl.RGBA,
|
|
209
|
+
this.gl.UNSIGNED_BYTE,
|
|
210
|
+
this.webRtcController.videoPlayer.getVideoElement()
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Update prev video width/height
|
|
215
|
+
this.prevVideoHeight = videoHeight;
|
|
216
|
+
this.prevVideoWidth = videoWidth;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
initBuffers(){
|
|
220
|
+
// Create out position buffer and its vertex shader attribute
|
|
221
|
+
{
|
|
222
|
+
// Create a buffer to put the the vertices of the plane we will draw the video stream onto
|
|
223
|
+
this.positionBuffer = this.gl.createBuffer();
|
|
224
|
+
// Bind the position buffer
|
|
225
|
+
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.positionBuffer);
|
|
226
|
+
// Enable `positionLocation` to be used as vertex shader attribute
|
|
227
|
+
this.gl.enableVertexAttribArray(this.positionLocation);
|
|
228
|
+
|
|
229
|
+
// Note: positions are passed in clip-space coordinates [-1..1] so no need to convert in-shader
|
|
230
|
+
// prettier-ignore
|
|
231
|
+
this.gl.bufferData(
|
|
232
|
+
this.gl.ARRAY_BUFFER,
|
|
233
|
+
new Float32Array([
|
|
234
|
+
-1.0, 1.0,
|
|
235
|
+
1.0, 1.0,
|
|
236
|
+
-1.0, -1.0,
|
|
237
|
+
-1.0, -1.0,
|
|
238
|
+
1.0, 1.0,
|
|
239
|
+
1.0, -1.0
|
|
240
|
+
]),
|
|
241
|
+
this.gl.STATIC_DRAW
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
// Tell position attribute of the vertex shader how to get data out of the bound buffer (the positionBuffer)
|
|
245
|
+
this.gl.vertexAttribPointer(
|
|
246
|
+
this.positionLocation,
|
|
247
|
+
2 /*size*/,
|
|
248
|
+
this.gl.FLOAT /*type*/,
|
|
249
|
+
false /*normalize*/,
|
|
250
|
+
0 /*stride*/,
|
|
251
|
+
0 /*offset*/
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Create our texture coordinate buffers for accessing our texture
|
|
256
|
+
{
|
|
257
|
+
this.texcoordBuffer = this.gl.createBuffer();
|
|
258
|
+
// Bind the texture coordinate buffer
|
|
259
|
+
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.texcoordBuffer);
|
|
260
|
+
// Enable `texcoordLocation` to be used as a vertex shader attribute
|
|
261
|
+
this.gl.enableVertexAttribArray(this.texcoordLocation);
|
|
262
|
+
|
|
263
|
+
// The texture coordinates to apply for rectangle we are drawing
|
|
264
|
+
this.gl.bufferData(
|
|
265
|
+
this.gl.ARRAY_BUFFER,
|
|
266
|
+
new Float32Array([
|
|
267
|
+
0.0, 0.0,
|
|
268
|
+
1.0, 0.0,
|
|
269
|
+
0.0, 1.0,
|
|
270
|
+
0.0, 1.0,
|
|
271
|
+
1.0, 0.0,
|
|
272
|
+
1.0, 1.0
|
|
273
|
+
]),
|
|
274
|
+
this.gl.STATIC_DRAW
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
// Tell texture coordinate attribute of the vertex shader how to get data out of the bound buffer (the texcoordBuffer)
|
|
278
|
+
this.gl.vertexAttribPointer(
|
|
279
|
+
this.texcoordLocation,
|
|
280
|
+
2 /*size*/,
|
|
281
|
+
this.gl.FLOAT /*type*/,
|
|
282
|
+
false /*normalize*/,
|
|
283
|
+
0 /*stride*/,
|
|
284
|
+
0 /*offset*/
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
onXrSessionStarted(session: XRSession) {
|
|
290
|
+
Logger.Log(Logger.GetStackTrace(), 'XR Session started');
|
|
291
|
+
|
|
292
|
+
this.xrSession = session;
|
|
293
|
+
this.xrSession.addEventListener('end', () => {
|
|
294
|
+
this.onXrSessionEnded();
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
// Initialization
|
|
298
|
+
this.initGL();
|
|
299
|
+
this.initShaders();
|
|
300
|
+
this.initBuffers();
|
|
147
301
|
|
|
148
302
|
session.requestReferenceSpace('local').then((refSpace) => {
|
|
149
303
|
this.xrRefSpace = refSpace;
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
304
|
+
|
|
305
|
+
// Set up our base layer (i.e. a projection layer that fills the entire XR viewport).
|
|
306
|
+
this.xrSession.updateRenderState({
|
|
307
|
+
baseLayer: new XRWebGLLayer(this.xrSession, this.gl)
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
// Update target framerate to 90 fps if 90 fps is supported in this XR device
|
|
311
|
+
if(this.xrSession.supportedFrameRates) {
|
|
312
|
+
for (const frameRate of this.xrSession.supportedFrameRates) {
|
|
313
|
+
if(frameRate == 90){
|
|
314
|
+
session.updateTargetFrameRate(90);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Binding to each new frame to get latest XR updates
|
|
320
|
+
this.xrSession.requestAnimationFrame(this.onXrFrame.bind(this));
|
|
154
321
|
});
|
|
155
322
|
|
|
156
323
|
this.onSessionStarted.dispatchEvent(new Event('xrSessionStarted'));
|
|
157
324
|
}
|
|
158
325
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
326
|
+
areArraysEqual(a: Float32Array, b: Float32Array) : boolean {
|
|
327
|
+
return a.length === b.length && a.every((element, index) => Math.abs(element - b[index]) <= this.EPSILON);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
arePointsEqual(a: DOMPointReadOnly, b: DOMPointReadOnly) : boolean {
|
|
331
|
+
return Math.abs(a.x - b.x) >= this.EPSILON && Math.abs(a.y - b.y) >= this.EPSILON && Math.abs(a.z - b.z) >= this.EPSILON;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
sendXRDataToUE() {
|
|
335
|
+
if(this.leftView == null || this.rightView == null) {
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// We selectively send either the `XREyeViews` or `XRHMDTransform`
|
|
340
|
+
// messages over the datachannel. The reason for this selective sending is that
|
|
341
|
+
// the `XREyeViews` is a much larger message and changes infrequently (e.g. only when user changes headset IPD).
|
|
342
|
+
// Therefore, we only need to send it once on startup and then any time it changes.
|
|
343
|
+
// The rest of the time we can send the `XRHMDTransform` message.
|
|
344
|
+
let shouldSendEyeViews = this.lastSentLeftEyeProj == null ||
|
|
345
|
+
this.lastSentRightEyeProj == null ||
|
|
346
|
+
this.lastSentRelativeLeftEyePos == null ||
|
|
347
|
+
this.lastSentRelativeRightEyePos == null;
|
|
348
|
+
|
|
349
|
+
const leftEyeTrans = this.leftView.transform.matrix;
|
|
350
|
+
const leftEyeProj = this.leftView.projectionMatrix;
|
|
351
|
+
const rightEyeTrans = this.rightView.transform.matrix;
|
|
352
|
+
const rightEyeProj = this.rightView.projectionMatrix;
|
|
353
|
+
const hmdTrans = this.xrViewerPose.transform.matrix;
|
|
354
|
+
|
|
355
|
+
// Check if projection matrices have changed
|
|
356
|
+
if(!shouldSendEyeViews && this.lastSentLeftEyeProj != null && this.lastSentRightEyeProj != null) {
|
|
357
|
+
const leftEyeProjUnchanged = this.areArraysEqual(leftEyeProj, this.lastSentLeftEyeProj);
|
|
358
|
+
const rightEyeProjUnchanged = this.areArraysEqual(rightEyeProj, this.lastSentRightEyeProj);
|
|
359
|
+
shouldSendEyeViews = leftEyeProjUnchanged == false || rightEyeProjUnchanged == false;
|
|
360
|
+
}
|
|
167
361
|
|
|
362
|
+
const leftEyeRelativePos = new DOMPointReadOnly(
|
|
363
|
+
this.leftView.transform.position.x - this.xrViewerPose.transform.position.x,
|
|
364
|
+
this.leftView.transform.position.y - this.xrViewerPose.transform.position.y,
|
|
365
|
+
this.leftView.transform.position.z - this.xrViewerPose.transform.position.z,
|
|
366
|
+
1.0
|
|
367
|
+
);
|
|
368
|
+
|
|
369
|
+
const rightEyeRelativePos = new DOMPointReadOnly(
|
|
370
|
+
this.leftView.transform.position.x - this.xrViewerPose.transform.position.x,
|
|
371
|
+
this.leftView.transform.position.y - this.xrViewerPose.transform.position.y,
|
|
372
|
+
this.leftView.transform.position.z - this.xrViewerPose.transform.position.z,
|
|
373
|
+
1.0
|
|
374
|
+
);
|
|
375
|
+
|
|
376
|
+
// Check if relative eye pos has changed (e.g IPD changed)
|
|
377
|
+
if(!shouldSendEyeViews && this.lastSentRelativeLeftEyePos != null && this.lastSentRelativeRightEyePos != null) {
|
|
378
|
+
const leftEyePosUnchanged = this.arePointsEqual(leftEyeRelativePos, this.lastSentRelativeLeftEyePos);
|
|
379
|
+
const rightEyePosUnchanged = this.arePointsEqual(rightEyeRelativePos, this.lastSentRelativeRightEyePos);
|
|
380
|
+
shouldSendEyeViews = leftEyePosUnchanged == false || rightEyePosUnchanged == false;
|
|
381
|
+
// Note: We are not checking if EyeView rotation changes (as far as I know no HMD supports changing this value at runtime).
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if(shouldSendEyeViews) {
|
|
385
|
+
// send transform (4x4) and projection matrix (4x4) data for each eye (left first, then right)
|
|
168
386
|
// prettier-ignore
|
|
387
|
+
this.webRtcController.streamMessageController.toStreamerHandlers.get('XREyeViews')([
|
|
388
|
+
// Left eye 4x4 transform matrix
|
|
389
|
+
leftEyeTrans[0], leftEyeTrans[4], leftEyeTrans[8], leftEyeTrans[12],
|
|
390
|
+
leftEyeTrans[1], leftEyeTrans[5], leftEyeTrans[9], leftEyeTrans[13],
|
|
391
|
+
leftEyeTrans[2], leftEyeTrans[6], leftEyeTrans[10], leftEyeTrans[14],
|
|
392
|
+
leftEyeTrans[3], leftEyeTrans[7], leftEyeTrans[11], leftEyeTrans[15],
|
|
393
|
+
// Left eye 4x4 projection matrix
|
|
394
|
+
leftEyeProj[0], leftEyeProj[4], leftEyeProj[8], leftEyeProj[12],
|
|
395
|
+
leftEyeProj[1], leftEyeProj[5], leftEyeProj[9], leftEyeProj[13],
|
|
396
|
+
leftEyeProj[2], leftEyeProj[6], leftEyeProj[10], leftEyeProj[14],
|
|
397
|
+
leftEyeProj[3], leftEyeProj[7], leftEyeProj[11], leftEyeProj[15],
|
|
398
|
+
// Right eye 4x4 transform matrix
|
|
399
|
+
rightEyeTrans[0], rightEyeTrans[4], rightEyeTrans[8], rightEyeTrans[12],
|
|
400
|
+
rightEyeTrans[1], rightEyeTrans[5], rightEyeTrans[9], rightEyeTrans[13],
|
|
401
|
+
rightEyeTrans[2], rightEyeTrans[6], rightEyeTrans[10], rightEyeTrans[14],
|
|
402
|
+
rightEyeTrans[3], rightEyeTrans[7], rightEyeTrans[11], rightEyeTrans[15],
|
|
403
|
+
// right eye 4x4 projection matrix
|
|
404
|
+
rightEyeProj[0], rightEyeProj[4], rightEyeProj[8], rightEyeProj[12],
|
|
405
|
+
rightEyeProj[1], rightEyeProj[5], rightEyeProj[9], rightEyeProj[13],
|
|
406
|
+
rightEyeProj[2], rightEyeProj[6], rightEyeProj[10], rightEyeProj[14],
|
|
407
|
+
rightEyeProj[3], rightEyeProj[7], rightEyeProj[11], rightEyeProj[15],
|
|
408
|
+
// HMD 4x4 transform
|
|
409
|
+
hmdTrans[0], hmdTrans[4], hmdTrans[8], hmdTrans[12],
|
|
410
|
+
hmdTrans[1], hmdTrans[5], hmdTrans[9], hmdTrans[13],
|
|
411
|
+
hmdTrans[2], hmdTrans[6], hmdTrans[10], hmdTrans[14],
|
|
412
|
+
hmdTrans[3], hmdTrans[7], hmdTrans[11], hmdTrans[15],
|
|
413
|
+
]);
|
|
414
|
+
this.lastSentLeftEyeProj = leftEyeProj;
|
|
415
|
+
this.lastSentRightEyeProj = rightEyeProj;
|
|
416
|
+
this.lastSentRelativeLeftEyePos = leftEyeRelativePos;
|
|
417
|
+
this.lastSentRelativeRightEyePos = rightEyeRelativePos;
|
|
418
|
+
}
|
|
419
|
+
else {
|
|
420
|
+
// If we don't need to the entire eye views being sent just send the HMD transform
|
|
169
421
|
this.webRtcController.streamMessageController.toStreamerHandlers.get('XRHMDTransform')([
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
422
|
+
// HMD 4x4 transform
|
|
423
|
+
hmdTrans[0], hmdTrans[4], hmdTrans[8], hmdTrans[12],
|
|
424
|
+
hmdTrans[1], hmdTrans[5], hmdTrans[9], hmdTrans[13],
|
|
425
|
+
hmdTrans[2], hmdTrans[6], hmdTrans[10], hmdTrans[14],
|
|
426
|
+
hmdTrans[3], hmdTrans[7], hmdTrans[11], hmdTrans[15],
|
|
174
427
|
]);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
175
430
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
this.
|
|
431
|
+
onXrFrame(time: DOMHighResTimeStamp, frame: XRFrame) {
|
|
432
|
+
this.xrViewerPose = frame.getViewerPose(this.xrRefSpace);
|
|
433
|
+
if (this.xrViewerPose) {
|
|
434
|
+
this.updateViews();
|
|
435
|
+
if(this.leftView == null || this.rightView == null) {
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
181
438
|
|
|
182
|
-
|
|
183
|
-
this.
|
|
184
|
-
|
|
185
|
-
0,
|
|
186
|
-
this.gl.RGBA,
|
|
187
|
-
this.gl.RGBA,
|
|
188
|
-
this.gl.UNSIGNED_BYTE,
|
|
189
|
-
this.webRtcController.videoPlayer.getVideoElement()
|
|
190
|
-
);
|
|
191
|
-
this.render(this.webRtcController.videoPlayer.getVideoElement());
|
|
439
|
+
this.sendXRDataToUE();
|
|
440
|
+
this.updateVideoTexture();
|
|
441
|
+
this.render();
|
|
192
442
|
}
|
|
193
443
|
|
|
194
444
|
if (this.webRtcController.config.isFlagEnabled(Flags.XRControllerInput)) {
|
|
@@ -209,105 +459,44 @@ export class WebXRController {
|
|
|
209
459
|
this.onXrFrame(time, frame)
|
|
210
460
|
);
|
|
211
461
|
|
|
212
|
-
this.onFrame.dispatchEvent(new XrFrameEvent({
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
462
|
+
this.onFrame.dispatchEvent(new XrFrameEvent({ time, frame }));
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
private updateViews() {
|
|
466
|
+
if(!this.xrViewerPose) {
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
for (const view of this.xrViewerPose.views) {
|
|
470
|
+
if (view.eye === "left") {
|
|
471
|
+
this.leftView = view;
|
|
472
|
+
}
|
|
473
|
+
else if(view.eye === "right") {
|
|
474
|
+
this.rightView = view;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
216
477
|
}
|
|
217
478
|
|
|
218
|
-
private render(
|
|
479
|
+
private render() {
|
|
219
480
|
if (!this.gl) {
|
|
220
481
|
return;
|
|
221
482
|
}
|
|
222
483
|
|
|
484
|
+
// Bind the framebuffer to the base layer's framebuffer
|
|
223
485
|
const glLayer = this.xrSession.renderState.baseLayer;
|
|
224
|
-
this.gl.
|
|
225
|
-
0,
|
|
226
|
-
0,
|
|
227
|
-
glLayer.framebufferWidth,
|
|
228
|
-
glLayer.framebufferHeight
|
|
229
|
-
);
|
|
230
|
-
this.gl.uniform4f(this.offsetLocation, 1.0, 1.0, 0.0, 0.0);
|
|
231
|
-
|
|
232
|
-
// Set rectangle
|
|
233
|
-
// prettier-ignore
|
|
234
|
-
this.gl.bufferData(
|
|
235
|
-
this.gl.ARRAY_BUFFER,
|
|
236
|
-
new Float32Array([
|
|
237
|
-
0, 0,
|
|
238
|
-
videoElement.videoWidth, 0,
|
|
239
|
-
0, videoElement.videoHeight,
|
|
240
|
-
0, videoElement.videoHeight,
|
|
241
|
-
videoElement.videoWidth, 0,
|
|
242
|
-
videoElement.videoWidth, videoElement.videoHeight
|
|
243
|
-
]),
|
|
244
|
-
this.gl.STATIC_DRAW
|
|
245
|
-
);
|
|
486
|
+
this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, glLayer.framebuffer);
|
|
246
487
|
|
|
247
|
-
//
|
|
248
|
-
this.gl.
|
|
249
|
-
this.gl.bufferData(
|
|
250
|
-
this.gl.ARRAY_BUFFER,
|
|
251
|
-
new Float32Array([
|
|
252
|
-
0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0
|
|
253
|
-
]),
|
|
254
|
-
this.gl.STATIC_DRAW
|
|
255
|
-
);
|
|
488
|
+
// Set the relevant portion of clip space
|
|
489
|
+
this.gl.viewport(0, 0, glLayer.framebufferWidth, glLayer.framebufferHeight);
|
|
256
490
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
let normalize; // normalize the data
|
|
260
|
-
let stride; // 0 = move forward size * sizeof(type) each iteration to get the next position
|
|
261
|
-
let offset; // start position of the buffer
|
|
262
|
-
|
|
263
|
-
// Bind the position buffer.
|
|
264
|
-
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.positionBuffer);
|
|
265
|
-
// Tell the position attribute how to get data out of positionBuffer (ARRAY_BUFFER)
|
|
266
|
-
size = 2; // 2 components per iteration
|
|
267
|
-
type = this.gl.FLOAT; // the data is 32bit floats
|
|
268
|
-
normalize = false; // don't normalize the data
|
|
269
|
-
stride = 0; // 0 = move forward size * sizeof(type) each iteration to get the next position
|
|
270
|
-
offset = 0; // start at the beginning of the buffer
|
|
271
|
-
this.gl.vertexAttribPointer(
|
|
272
|
-
this.positionLocation,
|
|
273
|
-
size,
|
|
274
|
-
type,
|
|
275
|
-
normalize,
|
|
276
|
-
stride,
|
|
277
|
-
offset
|
|
278
|
-
);
|
|
279
|
-
// Turn on the texcoord attribute
|
|
280
|
-
this.gl.enableVertexAttribArray(this.texcoordLocation);
|
|
281
|
-
// bind the texcoord buffer.
|
|
282
|
-
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.texcoordBuffer);
|
|
283
|
-
// Tell the texcoord attribute how to get data out of texcoordBuffer (ARRAY_BUFFER)
|
|
284
|
-
size = 2; // 2 components per iteration
|
|
285
|
-
type = this.gl.FLOAT; // the data is 32bit floats
|
|
286
|
-
normalize = false; // don't normalize the data
|
|
287
|
-
stride = 0; // 0 = move forward size * sizeof(type) each iteration to get the next position
|
|
288
|
-
offset = 0; // start at the beginning of the buffer
|
|
289
|
-
this.gl.vertexAttribPointer(
|
|
290
|
-
this.texcoordLocation,
|
|
291
|
-
size,
|
|
292
|
-
type,
|
|
293
|
-
normalize,
|
|
294
|
-
stride,
|
|
295
|
-
offset
|
|
296
|
-
);
|
|
297
|
-
// set the resolution
|
|
298
|
-
this.gl.uniform2f(
|
|
299
|
-
this.resolutionLocation,
|
|
300
|
-
videoElement.videoWidth,
|
|
301
|
-
videoElement.videoHeight
|
|
302
|
-
);
|
|
303
|
-
// draw the rectangle.
|
|
304
|
-
const primitiveType = this.gl.TRIANGLES;
|
|
305
|
-
const count = 6;
|
|
306
|
-
offset = 0;
|
|
307
|
-
this.gl.drawArrays(primitiveType, offset, count);
|
|
491
|
+
// Draw the rectangle we will show the video stream texture on
|
|
492
|
+
this.gl.drawArrays(this.gl.TRIANGLES /*primitiveType*/, 0 /*offset*/, 6 /*count*/);
|
|
308
493
|
}
|
|
309
494
|
|
|
310
495
|
static isSessionSupported(mode: XRSessionMode): Promise<boolean> {
|
|
496
|
+
if (location.protocol !== "https:") {
|
|
497
|
+
Logger.Info(null, "WebXR requires https, if you want WebXR use https.");
|
|
498
|
+
}
|
|
499
|
+
|
|
311
500
|
if (navigator.xr) {
|
|
312
501
|
return navigator.xr.isSessionSupported(mode);
|
|
313
502
|
} else {
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { BaseMessage } from '@epicgames-ps/lib-pixelstreamingcommon-ue5.5';
|
|
2
|
+
|
|
1
3
|
export interface MockWebSocketSpyFunctions {
|
|
2
4
|
constructorSpy: null | ((url: string) => void);
|
|
3
5
|
openSpy: null | ((event: Event) => void);
|
|
@@ -12,7 +14,7 @@ export interface MockWebSocketTriggerFunctions {
|
|
|
12
14
|
triggerOnOpen: null | (() => void);
|
|
13
15
|
triggerOnError: null | (() => void);
|
|
14
16
|
triggerOnClose: null | ((closeReason?: CloseEventInit) => void);
|
|
15
|
-
triggerOnMessage: null | ((message?:
|
|
17
|
+
triggerOnMessage: null | ((message?: BaseMessage) => void);
|
|
16
18
|
triggerOnMessageBinary: null | ((message?: Blob) => void);
|
|
17
19
|
triggerRemoteClose: null | ((code?: number, reason?: string) => void);
|
|
18
20
|
}
|
|
@@ -88,10 +90,8 @@ export class MockWebSocketImpl extends WebSocket {
|
|
|
88
90
|
this.close(code, reason);
|
|
89
91
|
}
|
|
90
92
|
|
|
91
|
-
triggerOnMessage(message
|
|
92
|
-
const data = message
|
|
93
|
-
? JSON.stringify(message)
|
|
94
|
-
: JSON.stringify({ type: 'test' });
|
|
93
|
+
triggerOnMessage(message: BaseMessage) {
|
|
94
|
+
const data = JSON.stringify(message);
|
|
95
95
|
const event = new MessageEvent('message', { data });
|
|
96
96
|
this.onmessage?.(event);
|
|
97
97
|
spyFunctions.messageSpy?.(event);
|