@@ -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;
|
@@ -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:
|
228
|
+
padding-left: 25px;
|
230
229
|
border-radius: .5em;
|
231
|
-
border: 2px solid rgba(160,160,160,.
|
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
|
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
|
|
@@ -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;
|
@@ -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);
|
@@ -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.
|
131
|
-
this.
|
132
|
-
this.
|
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.
|
147
|
-
|
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
|
-
|
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
|
-
|
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");
|