@@ -3,8 +3,9 @@
|
|
3
3
|
import { tryGetNeedleEngineVersion } from '../common/version.js';
|
4
4
|
import { tryGetGenerator } from '../common/generator.js';
|
5
5
|
import { getMeta } from '../common/config.cjs';
|
6
|
-
|
6
|
+
import { alias } from './alias.cjs';
|
7
7
|
|
8
|
+
|
8
9
|
const __filename = fileURLToPath(import.meta.url);
|
9
10
|
const __dirname = dirname(__filename);
|
10
11
|
|
@@ -72,6 +73,8 @@
|
|
72
73
|
loader: resolve(__dirname, 'meshbvhworker.cjs')
|
73
74
|
});
|
74
75
|
|
76
|
+
alias(config);
|
77
|
+
|
75
78
|
return config;
|
76
79
|
}
|
77
80
|
|
@@ -6,9 +6,9 @@
|
|
6
6
|
import { getPeerjsInstance } from "../engine/engine_networking_peer.js";
|
7
7
|
import { showBalloonMessage } from "./debug/index.js";
|
8
8
|
import { Application } from "./engine_application.js";
|
9
|
-
import {
|
9
|
+
import { Context } from "./engine_context.js";
|
10
10
|
import type { IModel } from "./engine_networking_types.js";
|
11
|
-
import { type IComponent } from "./engine_types.js";
|
11
|
+
import { type IComponent,isComponent } from "./engine_types.js";
|
12
12
|
import { getParam } from "./engine_utils.js";
|
13
13
|
|
14
14
|
|
@@ -463,8 +463,25 @@
|
|
463
463
|
*/
|
464
464
|
debug: boolean = false;
|
465
465
|
|
466
|
-
constructor(context:
|
466
|
+
constructor(context: IComponent);
|
467
|
+
constructor(context: Context, guid: string);
|
468
|
+
constructor(context: Context, peer: PeerHandle);
|
469
|
+
constructor(context: Context | IComponent, peer?: PeerHandle | string) {
|
467
470
|
super();
|
471
|
+
|
472
|
+
if (isComponent(context)) {
|
473
|
+
const comp = context;
|
474
|
+
context = comp.context;
|
475
|
+
peer = PeerHandle.getOrCreate(comp.context, comp.guid);
|
476
|
+
}
|
477
|
+
else if (typeof peer === "string") {
|
478
|
+
peer = PeerHandle.getOrCreate(context, peer);
|
479
|
+
}
|
480
|
+
|
481
|
+
if (!context) throw new Error("Failed to create NetworkedStreams because context is undefined");
|
482
|
+
else if(!(context instanceof Context)) throw new Error("Failed to create NetworkedStreams because context is not an instance of Context");
|
483
|
+
if (!peer) throw new Error("Failed to create NetworkedStreams because peer is undefined");
|
484
|
+
|
468
485
|
this.context = context;
|
469
486
|
this.peer = peer;
|
470
487
|
if (debug) this.debug = true;
|
@@ -210,6 +210,9 @@
|
|
210
210
|
get worldQuaternion(): Quaternion;
|
211
211
|
}
|
212
212
|
|
213
|
+
export function isComponent(obj:any) : obj is IComponent {
|
214
|
+
return obj && obj.isComponent;
|
215
|
+
}
|
213
216
|
|
214
217
|
export type ICamera = CameraComponent;
|
215
218
|
|
@@ -240,8 +240,13 @@
|
|
240
240
|
static get currentSessionRequest(): XRSessionMode | null { return this._currentSessionRequestMode; }
|
241
241
|
private static _currentSessionRequestMode: XRSessionMode | null = null;
|
242
242
|
|
243
|
+
/**
|
244
|
+
* @returns the active @type {NeedleXRSession} (if any active) or null
|
245
|
+
*/
|
243
246
|
static get active(): NeedleXRSession | null { return this._activeSession; }
|
244
|
-
/** The active xr session mode (if any xr session is active)
|
247
|
+
/** The active xr session mode (if any xr session is active)
|
248
|
+
* @link https://developer.mozilla.org/en-US/docs/Web/API/XRSessionMode
|
249
|
+
*/
|
245
250
|
static get activeMode() { return this._activeSession?.mode ?? null; }
|
246
251
|
/** XRSystem via navigator.xr access
|
247
252
|
* @link https://developer.mozilla.org/en-US/docs/Web/API/XRSystem
|
@@ -249,17 +254,35 @@
|
|
249
254
|
static get xrSystem(): XRSystem | undefined {
|
250
255
|
return ('xr' in navigator) ? navigator.xr : undefined;
|
251
256
|
}
|
257
|
+
/**
|
258
|
+
* @returns true if the browser supports WebXR (`immersive-vr` or `immersive-ar`)
|
259
|
+
* @link https://developer.mozilla.org/en-US/docs/Web/API/XRSystem/isSessionSupported
|
260
|
+
*/
|
252
261
|
static isXRSupported() { return Promise.all([this.isVRSupported(), this.isARSupported()]).then(res => res.some(e => e)).catch(() => false); }
|
262
|
+
/**
|
263
|
+
* @returns true if the browser supports immersive-vr (WebXR)
|
264
|
+
* @link https://developer.mozilla.org/en-US/docs/Web/API/XRSystem/isSessionSupported
|
265
|
+
*/
|
253
266
|
static isVRSupported() { return this.isSessionSupported("immersive-vr"); }
|
267
|
+
/**
|
268
|
+
* @returns true if the browser supports immersive-ar (WebXR)
|
269
|
+
* @link https://developer.mozilla.org/en-US/docs/Web/API/XRSystem/isSessionSupported
|
270
|
+
*/
|
254
271
|
static isARSupported() { return this.isSessionSupported("immersive-ar"); }
|
272
|
+
/**
|
273
|
+
* @param mode The XRSessionMode to check if it is supported
|
274
|
+
* @returns true if the browser supports the given XRSessionMode
|
275
|
+
*/
|
255
276
|
static isSessionSupported(mode: XRSessionMode) { return this.xrSystem?.isSessionSupported(mode).catch(err => { if (debug) console.error(err); return false }) ?? Promise.resolve(false); }
|
256
277
|
|
257
278
|
private static _currentSessionRequest?: Promise<XRSession>;
|
258
279
|
private static _activeSession: NeedleXRSession | null;
|
259
280
|
|
281
|
+
/** Register to listen to XRSession start events. Unsubscribe with `offXRSessionStart` */
|
260
282
|
static onSessionRequestStart(evt: SessionRequestedEvent) {
|
261
283
|
this._sessionRequestStartListeners.push(evt);
|
262
284
|
}
|
285
|
+
/** Unsubscribe from request start evt. Register with `onSessionRequestStart` */
|
263
286
|
static offSessionRequestStart(evt: SessionRequestedEvent) {
|
264
287
|
const index = this._sessionRequestStartListeners.indexOf(evt);
|
265
288
|
if (index >= 0) this._sessionRequestStartListeners.splice(index, 1);
|
@@ -364,22 +387,32 @@
|
|
364
387
|
}
|
365
388
|
|
366
389
|
/** start a new webXR session (make sure to stop already running sessions before calling this method)
|
367
|
-
* @param mode The XRSessionMode to start (e.g. `immersive-vr` or `immersive-ar`)
|
390
|
+
* @param mode The XRSessionMode to start (e.g. `immersive-vr` or `immersive-ar`) or `ar` to start `immersive-ar` on supported devices OR on iOS devices it will export an interactive USDZ and open in Quicklook.
|
391
|
+
* Get more information about WebXR modes: https://developer.mozilla.org/en-US/docs/Web/API/XRSessionMode
|
368
392
|
* @param init The XRSessionInit to use (optional), docs: https://developer.mozilla.org/en-US/docs/Web/API/XRSessionInit
|
369
393
|
* @param context The Needle Engine context to use
|
370
394
|
*/
|
371
|
-
static async start(mode: XRSessionMode, init?: XRSessionInit, context?: Context): Promise<NeedleXRSession | null> {
|
395
|
+
static async start(mode: XRSessionMode | "ar", init?: XRSessionInit, context?: Context): Promise<NeedleXRSession | null> {
|
372
396
|
|
373
397
|
// handle iOS platform where "immersive-ar" is not supported
|
374
398
|
// TODO: should we add a separate mode (e.g. "AR")? https://linear.app/needle/issue/NE-5303
|
375
|
-
if (
|
376
|
-
|
377
|
-
|
378
|
-
|
399
|
+
if (isiOS()) {
|
400
|
+
if (mode === "ar") {
|
401
|
+
const arSupported = await this.isARSupported();
|
402
|
+
if (!arSupported && InternalUSDZRegistry.exportAndOpen()) {
|
403
|
+
return null;
|
404
|
+
}
|
405
|
+
else {
|
406
|
+
mode = "immersive-ar";
|
407
|
+
}
|
379
408
|
}
|
380
409
|
}
|
410
|
+
else if (mode == "ar") {
|
411
|
+
mode = "immersive-ar";
|
412
|
+
}
|
381
413
|
|
382
414
|
|
415
|
+
|
383
416
|
if (isDevEnvironment() && getParam("debugxrpreroom")) {
|
384
417
|
console.warn("Debug: Starting temporary XR session");
|
385
418
|
await TemporaryXRContext.start(mode, init || NeedleXRSession.getDefaultSessionInit(mode));
|
@@ -406,7 +439,7 @@
|
|
406
439
|
|
407
440
|
// setup session init args, make sure we have default values
|
408
441
|
if (!init) init = {};
|
409
|
-
|
442
|
+
|
410
443
|
switch (mode) {
|
411
444
|
|
412
445
|
// Setup VR initialization parameters
|
@@ -564,7 +597,7 @@
|
|
564
597
|
/**
|
565
598
|
* Check if the session has system keyboard support
|
566
599
|
*/
|
567
|
-
get isSystemKeyboardSupported(): boolean {
|
600
|
+
get isSystemKeyboardSupported(): boolean { return this.session.isSystemKeyboardSupported; }
|
568
601
|
|
569
602
|
/**
|
570
603
|
* @link https://developer.mozilla.org/en-US/docs/Web/API/XRSession/environmentBlendMode
|
@@ -586,7 +619,7 @@
|
|
586
619
|
/** @returns the given controller if it is connected */
|
587
620
|
getController(side: XRHandedness | number) {
|
588
621
|
if (typeof side === "number") return this.controllers[side] || null;
|
589
|
-
return this.controllers.find(c => c.side === side) || null;
|
622
|
+
return this.controllers.find(c => c.side === side) || null;
|
590
623
|
}
|
591
624
|
|
592
625
|
/** Returns true if running in pass through mode in immersive AR (e.g. user is wearing a headset while in AR) */
|
@@ -1069,7 +1102,7 @@
|
|
1069
1102
|
this._defaultRig.gameObject.removeFromParent();
|
1070
1103
|
|
1071
1104
|
enableSpatialConsole(false);
|
1072
|
-
|
1105
|
+
|
1073
1106
|
performance.mark(measure_SessionEndedMarker);
|
1074
1107
|
performance.measure('NeedleXRSession', measure_SessionStartedMarker, measure_SessionEndedMarker);
|
1075
1108
|
};
|
@@ -1,6 +1,6 @@
|
|
1
1
|
/* eslint-disable */
|
2
2
|
import { TypeStore } from "./../engine_typestore.js"
|
3
|
-
|
3
|
+
|
4
4
|
// Import types
|
5
5
|
import { __Ignore } from "../../engine-components/codegen/components.js";
|
6
6
|
import { ActionBuilder } from "../../engine-components/export/usdz/extensions/behavior/BehavioursBuilder.js";
|
@@ -220,7 +220,7 @@
|
|
220
220
|
import { XRFlag } from "../../engine-components/webxr/XRFlag.js";
|
221
221
|
import { XRRig } from "../../engine-components/webxr/WebXRRig.js";
|
222
222
|
import { XRState } from "../../engine-components/webxr/XRFlag.js";
|
223
|
-
|
223
|
+
|
224
224
|
// Register types
|
225
225
|
TypeStore.add("__Ignore", __Ignore);
|
226
226
|
TypeStore.add("ActionBuilder", ActionBuilder);
|
@@ -217,8 +217,7 @@
|
|
217
217
|
this._videoPlayer.setVideo(this._currentStream);
|
218
218
|
}
|
219
219
|
});
|
220
|
-
|
221
|
-
this._net = new NetworkedStreams(this.context, handle);
|
220
|
+
this._net = new NetworkedStreams(this);
|
222
221
|
}
|
223
222
|
|
224
223
|
/** @internal */
|
@@ -1,11 +1,9 @@
|
|
1
|
-
import { AudioAnalyser } from "three";
|
2
|
-
|
3
1
|
import { isDevEnvironment, showBalloonError, showBalloonWarning } from "../engine/debug/index.js";
|
4
2
|
import { Application } from "../engine/engine_application.js";
|
5
3
|
import { RoomEvents } from "../engine/engine_networking.js";
|
6
4
|
import { disposeStream, NetworkedStreamEvents, NetworkedStreams, StreamEndedEvent, StreamReceivedEvent } from "../engine/engine_networking_streams.js"
|
7
5
|
import { serializable } from "../engine/engine_serialization_decorator.js";
|
8
|
-
import { getParam, microphonePermissionsGranted } from "../engine/engine_utils.js";
|
6
|
+
import { getParam, isiOS, microphonePermissionsGranted } from "../engine/engine_utils.js";
|
9
7
|
import { delay } from "../engine/engine_utils.js";
|
10
8
|
import { getIconElement } from "../engine/webcomponents/icons.js";
|
11
9
|
import { Behaviour } from "./Component.js";
|
@@ -271,11 +269,31 @@
|
|
271
269
|
return null;
|
272
270
|
}
|
273
271
|
|
274
|
-
const
|
275
|
-
.
|
276
|
-
|
277
|
-
|
278
|
-
|
272
|
+
const getUserMedia = async (constraints?: MediaTrackConstraints): Promise<MediaStream | null> => {
|
273
|
+
return await navigator.mediaDevices.getUserMedia({ audio: constraints ?? true, video: false })
|
274
|
+
.catch((err) => {
|
275
|
+
console.warn("VOIP failed getting audio stream", err);
|
276
|
+
return null;
|
277
|
+
});;
|
278
|
+
}
|
279
|
+
|
280
|
+
const stream = await getUserMedia(audio);
|
281
|
+
|
282
|
+
if (!stream) return null;
|
283
|
+
|
284
|
+
// NE-5445, on iOS after calling `getUserMedia` it automatically switches the audio to the built-in microphone and speakers even if headphones are connected
|
285
|
+
// if there's no device selected explictly we will try to automatically select an external device
|
286
|
+
if (isiOS() && audio?.deviceId === undefined) {
|
287
|
+
const devices = await navigator.mediaDevices.enumerateDevices();
|
288
|
+
// select anything that doesn't have "iPhone" is likely "AirPods" or other bluetooth headphones
|
289
|
+
const nonBuiltInAudioSource = devices.find((device) => (device.kind === "audioinput" || device.kind === "audiooutput") && !device.label.includes("iPhone"));
|
290
|
+
if (nonBuiltInAudioSource) {
|
291
|
+
const constraints = Object.assign({}, audio);
|
292
|
+
constraints.deviceId = nonBuiltInAudioSource.deviceId;
|
293
|
+
return await getUserMedia(constraints);
|
294
|
+
}
|
295
|
+
}
|
296
|
+
|
279
297
|
return stream;
|
280
298
|
}
|
281
299
|
|
@@ -0,0 +1,42 @@
|
|
1
|
+
const { existsSync } = require("fs");
|
2
|
+
const path = require("path");
|
3
|
+
|
4
|
+
|
5
|
+
|
6
|
+
module.exports.alias = function (config) {
|
7
|
+
const workingDirectory = process.cwd();
|
8
|
+
config.resolve.alias['three'] = path.resolve(workingDirectory, 'node_modules/three');
|
9
|
+
config.resolve.alias['three/examples/jsm'] = path.resolve(workingDirectory, 'node_modules/three/examples/jsm');
|
10
|
+
config.resolve.alias['@needle-tools/engine'] = path.resolve(workingDirectory, 'node_modules/@needle-tools/engine');
|
11
|
+
|
12
|
+
// first check if gltf-progressive exists locally (due to local installation of engine)
|
13
|
+
const gltfProgressive_local = path.resolve(workingDirectory, 'node_modules/@needle-tools/engine/node_modules/@needle-tools/gltf-progressive');
|
14
|
+
if (existsSync(gltfProgressive_local)) {
|
15
|
+
config.resolve.alias['@needle-tools/gltf-progressive'] = gltfProgressive_local;
|
16
|
+
}
|
17
|
+
else {
|
18
|
+
config.resolve.alias['@needle-tools/gltf-progressive'] = path.resolve(workingDirectory, 'node_modules/@needle-tools/gltf-progressive');
|
19
|
+
}
|
20
|
+
|
21
|
+
|
22
|
+
const packageJsonPath = path.resolve(workingDirectory, 'package.json');
|
23
|
+
// now resolve all local package dependencies
|
24
|
+
if (existsSync(packageJsonPath)) {
|
25
|
+
/**
|
26
|
+
* @type {{ dependencies?: { [key: string]: string } }}
|
27
|
+
*/
|
28
|
+
const packageJson = require(packageJsonPath);
|
29
|
+
if (packageJson?.dependencies) {
|
30
|
+
for (const key of Object.keys(packageJson.dependencies)) {
|
31
|
+
const value = packageJson.dependencies[key];
|
32
|
+
if (value.startsWith("file:")) {
|
33
|
+
const pathValue = value.substring(5);
|
34
|
+
config.resolve.alias[key] = path.resolve(workingDirectory, pathValue);
|
35
|
+
}
|
36
|
+
}
|
37
|
+
}
|
38
|
+
}
|
39
|
+
|
40
|
+
console.log(config.resolve.alias);
|
41
|
+
|
42
|
+
}
|