Needle Engine

Changes between version 3.48.1 and 3.48.3
Files changed (8) hide show
  1. plugins/next/next.js +4 -1
  2. src/engine/engine_networking_streams.ts +20 -3
  3. src/engine/engine_types.ts +3 -0
  4. src/engine/xr/NeedleXRSession.ts +44 -11
  5. src/engine/codegen/register_types.ts +2 -2
  6. src/engine-components/ScreenCapture.ts +1 -2
  7. src/engine-components/Voip.ts +26 -8
  8. plugins/next/alias.cjs +42 -0
plugins/next/next.js CHANGED
@@ -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
- // import { ApplyLicensePlugin } from './license.js';
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
 
src/engine/engine_networking_streams.ts CHANGED
@@ -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 { type Context } from "./engine_context.js";
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: Context, peer: PeerHandle) {
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;
src/engine/engine_types.ts CHANGED
@@ -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
 
src/engine/xr/NeedleXRSession.ts CHANGED
@@ -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`), docs: https://developer.mozilla.org/en-US/docs/Web/API/XRSessionMode
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 (mode === "immersive-ar" && isiOS()) {
376
- const arSupported = await this.isARSupported();
377
- if (!arSupported && InternalUSDZRegistry.exportAndOpen()) {
378
- return null;
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 { return this.session.isSystemKeyboardSupported; }
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
  };
src/engine/codegen/register_types.ts CHANGED
@@ -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);
src/engine-components/ScreenCapture.ts CHANGED
@@ -217,8 +217,7 @@
217
217
  this._videoPlayer.setVideo(this._currentStream);
218
218
  }
219
219
  });
220
- const handle = PeerHandle.getOrCreate(this.context, this.guid);
221
- this._net = new NetworkedStreams(this.context, handle);
220
+ this._net = new NetworkedStreams(this);
222
221
  }
223
222
 
224
223
  /** @internal */
src/engine-components/Voip.ts CHANGED
@@ -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 stream = await navigator.mediaDevices.getUserMedia({ audio: audio ?? true, video: false })
275
- .catch((err) => {
276
- console.warn("VOIP failed getting audio stream", err);
277
- return null;
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
 
plugins/next/alias.cjs ADDED
@@ -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
+ }