Needle Engine

Changes between version 3.22.0 and 3.22.1
Files changed (5) hide show
  1. src/engine/engine_context.ts +1 -1
  2. src/engine/engine_license.ts +4 -4
  3. src/engine/engine_networking.ts +39 -0
  4. src/engine/codegen/register_types.ts +2 -2
  5. src/engine-components/ScreenCapture.ts +36 -13
src/engine/engine_context.ts CHANGED
@@ -951,7 +951,7 @@
951
951
 
952
952
  this._currentFrameEvent = FrameEvent.Undefined;
953
953
 
954
- if (this.isInXR === false && this.targetFrameRate !== undefined) {
954
+ if (this.isManagedExternally === false && this.isInXR === false && this.targetFrameRate !== undefined) {
955
955
  if (this._lastTimestamp === 0) this._lastTimestamp = timestamp;
956
956
  this._accumulatedTime += (timestamp - this._lastTimestamp) / 1000;
957
957
  this._lastTimestamp = timestamp;
src/engine/engine_license.ts CHANGED
@@ -50,7 +50,6 @@
50
50
  const licenseUrl = "https://engine.needle.tools/licensing/check?location=" + encodeURIComponent(window.location.href) + "&version=" + VERSION + "&generator=" + encodeURIComponent(GENERATOR);
51
51
  const res = await fetch(licenseUrl, {
52
52
  method: "GET",
53
- mode: "no-cors",
54
53
  }).catch();
55
54
  if (res?.status === 200) {
56
55
  applicationIsForbidden = false;
@@ -226,12 +225,13 @@
226
225
  margin-top: .3em;
227
226
  margin-bottom: .5em;
228
227
  padding: .2em;
229
- padding-left: 37px;
228
+ padding-left: 25px;
230
229
  border-radius: .5em;
231
- border: 2px solid rgba(160,160,160,.5);
230
+ border: 2px solid rgba(160,160,160,.3);
232
231
  `;
233
232
  // url must contain https for firefox to make it clickable
234
- const licenseText = "Needle Engine — No license active, commercial use is not allowed. Visit https://needle.tools/pricing for more information and licensing options.";
233
+ const version = VERSION;
234
+ const licenseText = `Needle Engine — No license active, commercial use is not allowed. Visit https://needle.tools/pricing for more information and licensing options! v${version}`;
235
235
  console.log("%c " + licenseText, style);
236
236
  }
237
237
 
src/engine/engine_networking.ts CHANGED
@@ -28,20 +28,32 @@
28
28
  room: string | undefined;
29
29
  }
30
30
 
31
+ /** Events regarding the websocket connection (e.g. when the connection opens) */
31
32
  export enum ConnectionEvents {
32
33
  ConnectionInfo = "connection-start-info"
33
34
  }
34
35
 
36
+ /** Use to listen to room networking events like joining a networked room
37
+ * For example: `this.context.connection.beginListen(RoomEvents.JoinedRoom, () => { })`
38
+ * @link https://engine.needle.tools/docs/networking.html#manual-networking
39
+ * */
35
40
  export enum RoomEvents {
41
+ /** Internal: sent to the server when attempting to join a room */
36
42
  Join = "join-room",
43
+ /** Internal: sent to the server when attempting to leave a room */
37
44
  Leave = "leave-room",
45
+ /** Incoming: When the local user has joined a room */
38
46
  JoinedRoom = "joined-room",
47
+ /** Incoming: When the local user has left a room */
39
48
  LeftRoom = "left-room",
49
+ /** Incoming: When a other user has joined the room */
40
50
  UserJoinedRoom = "user-joined-room",
51
+ /** Incoming: When a other user has left the room */
41
52
  UserLeftRoom = "user-left-room",
42
53
  RoomStateSent = "room-state-sent",
43
54
  }
44
55
 
56
+ /** Received when listening to `RoomEvents.JoinedRoom` event */
45
57
  export class JoinedRoomResponse {
46
58
  room!: string; // room name
47
59
  viewId!: string;
@@ -57,6 +69,8 @@
57
69
  userId!: string;
58
70
  }
59
71
 
72
+ /** The Needle Engine networking server supports the concept of ownership that can be requested. The `OwnershipEvent` enum contains possible outgoing (Request) and incoming (Response) events for communicating ownership.
73
+ * We recommend using the `OwnershipModel` class instead of dealing with those events directly tho. */
60
74
  export enum OwnershipEvent {
61
75
  RequestHasOwner = 'request-has-owner',
62
76
  ResponseHasOwner = "response-has-owner",
@@ -86,6 +100,7 @@
86
100
 
87
101
  declare type WebsocketSendType = IModel | object | boolean | null | string | number;
88
102
 
103
+ /** Class for abstracting the concept of ownership regarding a networked object or component. A component that is owned by another user can not be modified through networking (the server will reject changes) */
89
104
  export class OwnershipModel {
90
105
 
91
106
  public guid: string;
@@ -229,6 +244,7 @@
229
244
  (data: any | flatbuffers.ByteBuffer): void;
230
245
  }
231
246
 
247
+ /** Main class to communicate with the networking backend */
232
248
  export class NetworkConnection implements INetworkConnection {
233
249
 
234
250
  private context: Context;
@@ -251,6 +267,7 @@
251
267
  return this._state[guid];
252
268
  }
253
269
 
270
+ /** The connection id of the local user - it is given by the networking backend and can not be changed */
254
271
  public get connectionId(): string | null {
255
272
  return this._connectionId;
256
273
  }
@@ -259,32 +276,40 @@
259
276
  return debugNet;
260
277
  }
261
278
 
279
+ /** True when connected to the networking backend */
262
280
  public get isConnected(): boolean {
263
281
  return this.connected;
264
282
  }
265
283
 
284
+ /** The name of the room the user is currently connected to */
266
285
  public get currentRoomName(): string | null { return this._currentRoomName; }
286
+ /** True when connected to a room via a regular url, otherwise (when using a view only url) false indicating that the user should not be able to modify the scene */
267
287
  public get allowEditing(): boolean { return this._currentRoomAllowEditing; }
268
288
  // use this to join a room in view mode (see SyncedRoom)
269
289
  public get currentRoomViewId(): string | null { return this._currentRoomViewId; }
270
290
 
291
+ /** True if connected to a networked room. Use the joinRoom function or a `SyncedRoom` component */
271
292
  public get isInRoom(): boolean {
272
293
  return this._isInRoom;
273
294
  }
274
295
 
296
+ /** Latency to currently connected backend server */
275
297
  public get currentLatency(): number {
276
298
  return this._currentDelay;
277
299
  }
278
300
 
301
+ /** A ping is sent to the server at a regular interval while the browser tab is active. This method can be used to send additional ping messages when needed so that the user doesn't get disconnected from the networking backend */
279
302
  public sendPing() {
280
303
  this.send("ping", { time: this.context.time.time });
281
304
  }
282
305
 
306
+ /** Returns true if a user with the given connectionId is in the room */
283
307
  public userIsInRoom(id: string): boolean {
284
308
  return this._currentInRoom.indexOf(id) !== -1;
285
309
  }
286
310
 
287
311
  private _usersInRoomCopy = [];
312
+ /** Returns a list of all user ids in the current room */
288
313
  public usersInRoom(target: string[] | null = null): string[] {
289
314
  if (!target) target = this._usersInRoomCopy;
290
315
  target.length = 0;
@@ -293,6 +318,7 @@
293
318
  return target;
294
319
  }
295
320
 
321
+ /** Joins a networked room. If you don't want to manage a connection yourself you can use a `SyncedRoom` component as well */
296
322
  public joinRoom(room: string, viewOnly: boolean = false): boolean {
297
323
  if (!room) {
298
324
  console.error("Missing room name, can not join: \"" + room + "\"");
@@ -312,6 +338,7 @@
312
338
  return true;
313
339
  }
314
340
 
341
+ /** Use to leave a room that you are currently connected to (use `leaveRoom()` to disconnect from the currently active room but you can also specify a room name) */
315
342
  public leaveRoom(room: string | null = null) {
316
343
  if (!room) room = this.currentRoomName;
317
344
  if (!room) {
@@ -322,6 +349,7 @@
322
349
  return true;
323
350
  }
324
351
 
352
+ /** Send a message to the networking backend - it will broadcasted to all connected users in the same room by default */
325
353
  public send<T extends WebsocketSendType>(key: string | OwnershipEvent, data: T | null = null, queue: SendQueue = SendQueue.Queued) {
326
354
 
327
355
  //@ts-ignore
@@ -341,16 +369,19 @@
341
369
  return this.sendWithWebsocket(key, data, queue);
342
370
  }
343
371
 
372
+ /** Use to delete state for a given guid on the server */
344
373
  public sendDeleteRemoteState(guid: string) {
345
374
  this.send("delete-state", { guid: guid, dontSave: true });
346
375
  delete this._state[guid];
347
376
  }
348
377
 
378
+ /** Use to delete all state in the currently connected room on the server */
349
379
  public sendDeleteRemoteStateAll() {
350
380
  this.send("delete-all-state");
351
381
  this._state = {};
352
382
  }
353
383
 
384
+ /** Send a binary message to the server (broadcasted to all connected users) */
354
385
  public sendBinary(bin: Uint8Array) {
355
386
  if (debugNet) console.log("<< bin", bin.length);
356
387
  this._ws?.send(bin);
@@ -380,6 +411,7 @@
380
411
  this._ws?.send(message);
381
412
  }
382
413
 
414
+ /** Use to start listening to networking events */
383
415
  public beginListen(key: string | OwnershipEvent, callback: Function): Function {
384
416
  if (!this._listeners[key])
385
417
  this._listeners[key] = [];
@@ -389,6 +421,8 @@
389
421
 
390
422
  /**@deprecated please use stopListen instead (2.65.2-pre) */
391
423
  public stopListening(key: string | OwnershipEvent, callback: Function | null) { return this.stopListen(key, callback); }
424
+
425
+ /** Use to stop listening to networking events */
392
426
  public stopListen(key: string | OwnershipEvent, callback: Function | null) {
393
427
  if (!callback) return;
394
428
  if (!this._listeners[key]) return;
@@ -398,6 +432,7 @@
398
432
  }
399
433
  }
400
434
 
435
+ /** Use to start listening to networking binary events */
401
436
  public beginListenBinary(identifier: string, callback: BinaryCallback): BinaryCallback {
402
437
  if (!this._listenersBinary[identifier])
403
438
  this._listenersBinary[identifier] = [];
@@ -405,6 +440,7 @@
405
440
  return callback;
406
441
  }
407
442
 
443
+ /** Use to stop listening to networking binary events */
408
444
  public stopListenBinary(identifier: string, callback: any) {
409
445
  if (!this._listenersBinary[identifier]) return;
410
446
  const index = this._listenersBinary[identifier].indexOf(callback);
@@ -415,10 +451,12 @@
415
451
 
416
452
  private netWebSocketUrlProvider?: INetworkingWebsocketUrlProvider;
417
453
 
454
+ /** Use to override the networking server backend url. This is what the `Networking` component uses to modify the backend url */
418
455
  public registerProvider(prov: INetworkingWebsocketUrlProvider) {
419
456
  this.netWebSocketUrlProvider = prov;
420
457
  }
421
458
 
459
+ /** Used to connect to the networking server */
422
460
  public async connect() {
423
461
  if (this.connected) return Promise.resolve(true);
424
462
  if (debugNet)
@@ -433,6 +471,7 @@
433
471
  return this.connectWebsocket();
434
472
  };
435
473
 
474
+ /** Used to disconnect from the networking server */
436
475
  public disconnect() {
437
476
  this._ws?.close();
438
477
  this._ws = undefined;
src/engine/codegen/register_types.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { TypeStore } from "./../engine_typestore.js"
2
-
2
+
3
3
  // Import types
4
4
  import { __Ignore } from "../../engine-components/codegen/components.js";
5
5
  import { ActionBuilder } from "../../engine-components/export/usdz/extensions/behavior/BehavioursBuilder.js";
@@ -217,7 +217,7 @@
217
217
  import { XRGrabRendering } from "../../engine-components/webxr/WebXRGrabRendering.js";
218
218
  import { XRRig } from "../../engine-components/webxr/WebXRRig.js";
219
219
  import { XRState } from "../../engine-components/XRFlag.js";
220
-
220
+
221
221
  // Register types
222
222
  TypeStore.add("__Ignore", __Ignore);
223
223
  TypeStore.add("ActionBuilder", ActionBuilder);
src/engine-components/ScreenCapture.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  import { serializable } from "../engine/engine_serialization.js";
4
4
  import type { IPointerClickHandler, PointerEventData } from "./ui/PointerEvents.js";
5
5
  import { AudioSource } from "./AudioSource.js";
6
- import { getParam } from "../engine/engine_utils.js";
6
+ import { delay, getParam } from "../engine/engine_utils.js";
7
7
  import { showBalloonWarning } from "../engine/debug/index.js";
8
8
  import { NetworkedStreams, disposeStream, StreamReceivedEvent, StreamEndedEvent, PeerHandle, NetworkedStreamEvents } from "../engine/engine_networking_streams.js";
9
9
  import { RoomEvents } from "../engine/engine_networking.js";
@@ -52,7 +52,6 @@
52
52
  onPointerClick(evt: PointerEventData) {
53
53
  if (!this.allowStartOnClick) return;
54
54
  if (evt && evt.pointerId !== 0) return;
55
- if (this.context.connection.isInRoom === false) return;
56
55
  if (this.isReceiving && this.videoPlayer?.isPlaying) {
57
56
  if (this.videoPlayer)
58
57
  this.videoPlayer.screenspace = !this.videoPlayer.screenspace;
@@ -127,9 +126,9 @@
127
126
  if (debug)
128
127
  console.log("Screensharing", this.name, this);
129
128
  AudioSource.registerWaitForAllowAudio(() => {
130
- if (this.videoPlayer && this._currentStream && this._currentMode === ScreenCaptureMode.Receiving) {
131
- this.videoPlayer.playInBackground = true;
132
- this.videoPlayer.setVideo(this._currentStream);
129
+ if (this._videoPlayer && this._currentStream && this._currentMode === ScreenCaptureMode.Receiving) {
130
+ this._videoPlayer.playInBackground = true;
131
+ this._videoPlayer.setVideo(this._currentStream);
133
132
  }
134
133
  });
135
134
  const handle = PeerHandle.getOrCreate(this.context, this.guid);
@@ -143,8 +142,11 @@
143
142
  //@ts-ignore
144
143
  this._net?.addEventListener(NetworkedStreamEvents.StreamEnded, this.onCallEnded);
145
144
  this.context.connection.beginListen(RoomEvents.JoinedRoom, this.onJoinedRoom);
146
- if (this.context.connection.isInRoom && this.autoConnect) {
147
- this.share();
145
+ if (this.autoConnect) {
146
+ delay(1000).then(() => {
147
+ if (this.enabled && this.autoConnect && !this.isReceiving && !this.isSending && this.context.connection.isInRoom)
148
+ this.share()
149
+ });
148
150
  }
149
151
  }
150
152
 
@@ -158,15 +160,37 @@
158
160
  this.close();
159
161
  }
160
162
 
161
- private onJoinedRoom = () => {
162
- if (this.autoConnect && !this.isSending && !this.isReceiving) {
163
+ private onJoinedRoom = async () => {
164
+ await delay(1000);
165
+ if (this.autoConnect && !this.isSending && !this.isReceiving && this.context.connection.isInRoom) {
163
166
  this.share();
164
167
  }
165
168
  }
166
169
 
170
+ private _ensureVideoPlayer() {
171
+ const vp = new VideoPlayer();
172
+ vp.aspectMode = AspectMode.AdjustWidth;
173
+ GameObject.addComponent(this.gameObject, vp);
174
+ this._videoPlayer = vp;
175
+ }
176
+
177
+ private _activeShareRequest: Promise<void> | null = null;
178
+
167
179
  /** Call to begin screensharing */
168
180
  async share(opts?: ScreenCaptureOptions) {
181
+ if (this._activeShareRequest) return this._activeShareRequest;
182
+ this._activeShareRequest = this.internalShare(opts);
183
+ return this._activeShareRequest.then(() =>{
184
+ this._activeShareRequest = null;
185
+ })
186
+ }
169
187
 
188
+ private async internalShare(opts?: ScreenCaptureOptions) {
189
+ if (this.context.connection.isInRoom === false) {
190
+ console.warn("Can not start screensharing: requires network connection");
191
+ return;
192
+ }
193
+
170
194
  if (opts?.device)
171
195
  this.device = opts.device;
172
196
 
@@ -175,10 +199,7 @@
175
199
  this._videoPlayer = GameObject.getComponent(this.gameObject, VideoPlayer) ?? undefined;
176
200
  }
177
201
  if (!this.videoPlayer) {
178
- const vp = new VideoPlayer();
179
- vp.aspectMode = AspectMode.AdjustWidth;
180
- GameObject.addComponent(this.gameObject, vp);
181
- this._videoPlayer = vp;
202
+ this._ensureVideoPlayer();
182
203
  }
183
204
  if (!this.videoPlayer) {
184
205
  console.warn("Can not share video without a videoPlayer assigned");
@@ -297,6 +318,8 @@
297
318
  const isSending = mode === ScreenCaptureMode.Sending;
298
319
 
299
320
  if (isVideoStream) {
321
+ if (!this._videoPlayer)
322
+ this._ensureVideoPlayer();
300
323
  if (this._videoPlayer)
301
324
  this._videoPlayer.setVideo(stream);
302
325
  else console.error("No video player assigned for video stream");