janusweb 1.5.56 → 1.7.1
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/.github/workflows/deploy.yml +71 -0
- package/Dockerfile +14 -0
- package/README.md +258 -19
- package/janusxr.com/.args +2 -0
- package/janusxr.com/.init.lua +30 -0
- package/media/assets/webui/apps/comms/voip.js +27 -25
- package/media/assets/webui/apps/editor/editor.js +16 -0
- package/media/assets/webui/apps/locomotion/locomotion-assets.json +3 -0
- package/media/assets/webui/apps/locomotion/teleport.mp3 +0 -0
- package/media/assets/webui/apps/locomotion/teleporter.js +75 -43
- package/media/assets/webui/apps/navigation/navigation.js +6 -1
- package/media/assets/webui/apps/xrfragment/level0-sidecarfile.js +308 -0
- package/media/assets/webui/apps/xrfragment/level2-hyperlink.js +265 -0
- package/media/assets/webui/apps/xrfragment/level7-engine-prefixes.export.js +93 -0
- package/media/assets/webui/apps/xrfragment/level7-engine-prefixes.import.js +229 -0
- package/media/assets/webui/apps/xrfragment/patch/metaquest-fix-flickering.js +15 -0
- package/media/assets/webui/apps/xrfragment/xrfragment.json +14 -0
- package/media/assets/webui/apps/xrmenu/images/teleport.png +0 -0
- package/media/assets/webui/apps/xrmenu/xrmenu-assets.json +2 -1
- package/media/assets/webui/apps/xrmenu/xrmenu.js +39 -15
- package/media/assets/webui/default.json +1 -0
- package/media/images/portal.svg +130 -0
- package/media/index.html +48 -1
- package/package.json +1 -1
- package/scripts/client.js +24 -0
- package/scripts/config.js +12 -1
- package/scripts/janusbase.js +14 -22
- package/scripts/janusghost.js +2 -2
- package/scripts/janusparagraph.js +195 -12
- package/scripts/janusplayer.js +9 -6
- package/scripts/room.js +91 -44
- package/scripts/sound.js +1 -1
- package/scripts/text.js +2 -1
- package/scripts/translators/paragraph/html-xml-rss.js +47 -0
- package/scripts/translators/peertube.js +89 -0
- package/scripts/translators/xrfragments.js +93 -0
- package/tests/room/paragraph.xml +47 -0
- package/utils/build.sh +1 -0
- package/utils/clean-build.sh +5 -0
- package/utils/dev-link.sh +75 -0
- package/utils/release.binary.sh +36 -0
- package/utils/release.docker.sh +11 -0
|
@@ -1,8 +1,15 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* for room-specific teleport sound use:
|
|
3
|
+
*
|
|
4
|
+
* <room teleport_sound_id="mysound" ...>
|
|
5
|
+
*
|
|
6
|
+
*/
|
|
7
|
+
|
|
1
8
|
janus.registerElement('locomotion_teleporter', {
|
|
2
9
|
active: false,
|
|
3
10
|
longpresstime: 500,
|
|
4
11
|
deadzone: 5,
|
|
5
|
-
xrplayer: null,
|
|
12
|
+
xrplayer: null, // *TODO* use janus.engine.client.xrplayer instead (and allow desktop too)
|
|
6
13
|
linesegments: 100,
|
|
7
14
|
|
|
8
15
|
create() {
|
|
@@ -102,7 +109,13 @@ janus.registerElement('locomotion_teleporter', {
|
|
|
102
109
|
});
|
|
103
110
|
player.head.add(this.shroud._target);
|
|
104
111
|
this.particles.particle_vel = V(-.4, 0, -.4); // FIXME - particle velocity isn't being set on spawn
|
|
105
|
-
|
|
112
|
+
|
|
113
|
+
let locomotion = janus.ui.apps.default.apps.locomotion;
|
|
114
|
+
let asseturl = locomotion.resolveFullURL('./locomotion-assets.json');
|
|
115
|
+
fetch(asseturl).then(res => res.json()).then(assetlist => {
|
|
116
|
+
this.assetpack = elation.engine.assets.loadJSON(assetlist, locomotion.resolveFullURL('./'));
|
|
117
|
+
this.sound = room.createObject('Sound', { id: room.teleport_sound_id || 'button-teleport' }, this);
|
|
118
|
+
})
|
|
106
119
|
|
|
107
120
|
this.disableCursor();
|
|
108
121
|
window.addEventListener('mousemove', this.handleMouseMove);
|
|
@@ -133,13 +146,15 @@ janus.registerElement('locomotion_teleporter', {
|
|
|
133
146
|
});
|
|
134
147
|
this.activateControlContext('teleporter');
|
|
135
148
|
|
|
136
|
-
//
|
|
137
|
-
|
|
149
|
+
// emulated mouse-events (via webxr handtracking e.g.)
|
|
150
|
+
room.addEventListener('mousemove', (ev) => this.handleMouseMove(ev));
|
|
151
|
+
room.addEventListener('mousedown', (ev) => this.handleMouseDown(ev));
|
|
152
|
+
room.addEventListener('mouseup', (ev) => this.handleMouseUp(ev));
|
|
138
153
|
elation.events.add(janus._target, 'room_change', (ev) => this.handleRoomChange(ev));
|
|
139
154
|
},
|
|
140
155
|
update() {
|
|
141
156
|
if (this.active) {
|
|
142
|
-
let hand = this.xrplayer.trackedobjects['hand_' + this.teleporthand]
|
|
157
|
+
let hand = this.xrplayer ? this.xrplayer.trackedobjects['hand_' + this.teleporthand] : null
|
|
143
158
|
let startpoint,
|
|
144
159
|
segments = this.linesegments,
|
|
145
160
|
duration = .25,
|
|
@@ -152,11 +167,13 @@ janus.registerElement('locomotion_teleporter', {
|
|
|
152
167
|
if (hand) {
|
|
153
168
|
startpoint = hand.pointer.localToWorld(V(0, 0, 0));
|
|
154
169
|
v0 = hand.pointer.localToWorld(V(0, 0, -speed)).sub(startpoint);
|
|
170
|
+
hand.teleportPos = hand.teleportPos || v0
|
|
155
171
|
} else {
|
|
156
172
|
startpoint = player.head.localToWorld(V(0,0,0));
|
|
157
173
|
v0 = player.head.localToWorld(V(0,0,-speed)).sub(startpoint);
|
|
158
174
|
return; // Head-locked teleporter is awkward, do we really still support this?
|
|
159
175
|
}
|
|
176
|
+
|
|
160
177
|
let i = 0,
|
|
161
178
|
p0 = startpoint.clone(),
|
|
162
179
|
dir = v0.clone().normalize();
|
|
@@ -181,6 +198,8 @@ janus.registerElement('locomotion_teleporter', {
|
|
|
181
198
|
this.pos = player.worldToLocal(hit.point);
|
|
182
199
|
this.localToWorld(this.particles.emitter_pos.set(0,0,0));
|
|
183
200
|
this.updateTeleportAngle();
|
|
201
|
+
// track movement to cancel longpresstimer
|
|
202
|
+
if( hand ) hand.teleportPos = hand.teleportPos || this.pos.clone()
|
|
184
203
|
}
|
|
185
204
|
laserpoints[i * 2].copy(p0);
|
|
186
205
|
laserpoints[i * 2 + 1].copy(p1);
|
|
@@ -231,7 +250,6 @@ janus.registerElement('locomotion_teleporter', {
|
|
|
231
250
|
if (len > .8) {
|
|
232
251
|
this.updateTeleportAngle();
|
|
233
252
|
} else if (len < .01) {
|
|
234
|
-
this.showShroud();
|
|
235
253
|
this.teleport();
|
|
236
254
|
this.teleportactive = false;
|
|
237
255
|
this.teleporthand = false;
|
|
@@ -262,16 +280,23 @@ janus.registerElement('locomotion_teleporter', {
|
|
|
262
280
|
|
|
263
281
|
}
|
|
264
282
|
},
|
|
265
|
-
teleport() {
|
|
283
|
+
teleport(mouse) {
|
|
266
284
|
//if (!room.teleport) return;
|
|
267
285
|
let pos = player.localToWorld(this.pos.clone());
|
|
268
286
|
pos.y += player.fatness; // FIXME - This accounts for wonky player sphere collider, when we switch to a capsule we won't need it
|
|
269
287
|
player.pos = pos;
|
|
270
288
|
player.vel = V(0,0,.01); // "wake up" physics engine
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
289
|
+
console.log('Teleport player', pos);
|
|
290
|
+
this.showShroud();
|
|
291
|
+
if( this.sound ){
|
|
292
|
+
this.sound.pos = pos;
|
|
293
|
+
this.sound.play();
|
|
294
|
+
}
|
|
295
|
+
if( this.xrplayer && this.teleportangle && !mouse){
|
|
296
|
+
player.angular.set(0,0,0);
|
|
297
|
+
let playerangle = Math.atan2(this.pos.x, this.pos.z);
|
|
298
|
+
this.xrplayer.orientation._target.setFromEuler(new THREE.Euler(0, this.teleportangle - playerangle, 0));
|
|
299
|
+
}
|
|
275
300
|
},
|
|
276
301
|
updateTeleportAngle() {
|
|
277
302
|
let controls = this.activecontrols;
|
|
@@ -282,27 +307,28 @@ janus.registerElement('locomotion_teleporter', {
|
|
|
282
307
|
handleMouseDown(ev) {
|
|
283
308
|
if (!room.teleport) return;
|
|
284
309
|
if (ev.button == 0 && (player.enabled || janus.hmd)) {
|
|
310
|
+
if( ev.inputSourceObject && this.xrplayer ){ // translate webxr emulated mousedown event
|
|
311
|
+
this.teleporthand = ev.inputSourceObject.device.handedness
|
|
312
|
+
let hand = this.xrplayer.trackedobjects['hand_' + this.teleporthand];
|
|
313
|
+
if( hand ) hand.teleportPos = false
|
|
314
|
+
}
|
|
285
315
|
if (!this.longpresstimer) {
|
|
286
|
-
|
|
316
|
+
this.longpresstimer = setTimeout(() => { console.log('timer fired'); this.enableCursor(); }, this.longpresstime);
|
|
287
317
|
this.mousediff = [0,0];
|
|
288
|
-
}
|
|
289
|
-
//this.active = true;
|
|
318
|
+
}else this.cancelLongPress()
|
|
290
319
|
}
|
|
291
320
|
},
|
|
292
321
|
handleMouseMove(ev) {
|
|
293
322
|
if (!room.teleport) return;
|
|
294
|
-
if (this.longpresstimer) {
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
this.longpresstimer = false;
|
|
301
|
-
}
|
|
302
|
-
}
|
|
323
|
+
//if (this.longpresstimer) {
|
|
324
|
+
// this.mousediff[0] += ev.movementX;
|
|
325
|
+
// this.mousediff[1] += ev.movementY
|
|
326
|
+
// var distance = Math.sqrt(this.mousediff[0] * this.mousediff[0] + this.mousediff[1] * this.mousediff[1]);
|
|
327
|
+
// if (distance > this.deadzone) this.cancelLongPress()
|
|
328
|
+
//}
|
|
303
329
|
},
|
|
304
330
|
handleTouchMove(ev) {
|
|
305
|
-
if (!room.teleport) return;
|
|
331
|
+
if (!room.teleport || !this.active) return;
|
|
306
332
|
if (this.longpresstimer) {
|
|
307
333
|
var touch = ev.changedTouches[0];
|
|
308
334
|
|
|
@@ -315,9 +341,7 @@ janus.registerElement('locomotion_teleporter', {
|
|
|
315
341
|
}
|
|
316
342
|
this.lasttouch = [touch.clientX, touch.clientY];
|
|
317
343
|
var distance = Math.sqrt(this.mousediff[0] * this.mousediff[0] + this.mousediff[1] * this.mousediff[1]);
|
|
318
|
-
if (distance > this.deadzone)
|
|
319
|
-
clearTimeout(this.longpresstimer);
|
|
320
|
-
}
|
|
344
|
+
if (distance > this.deadzone) this.cancelLongPress()
|
|
321
345
|
if (this.active) {
|
|
322
346
|
ev.stopPropagation();
|
|
323
347
|
ev.preventDefault();
|
|
@@ -325,42 +349,50 @@ janus.registerElement('locomotion_teleporter', {
|
|
|
325
349
|
}
|
|
326
350
|
},
|
|
327
351
|
handleMouseUp(ev) {
|
|
328
|
-
|
|
329
|
-
if (this.
|
|
330
|
-
|
|
331
|
-
this.
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
this.sound.pos = this.pos;
|
|
337
|
-
this.sound.play();
|
|
338
|
-
*/
|
|
339
|
-
this.showShroud();
|
|
340
|
-
this.teleport();
|
|
341
|
-
}
|
|
342
|
-
this.disableCursor();
|
|
352
|
+
if (!room.teleport) return;
|
|
353
|
+
if (this.teleportactive) {
|
|
354
|
+
this.teleport(true);
|
|
355
|
+
this.active = false
|
|
356
|
+
this.teleportactive = false;
|
|
357
|
+
this.teleporthand = false;
|
|
358
|
+
this.disableCursor();
|
|
359
|
+
}else if( this.longpresstimer ) this.disableCursor();
|
|
343
360
|
},
|
|
344
361
|
enableCursor() {
|
|
345
362
|
if (!room.teleport) return;
|
|
363
|
+
if( this.xrplayer ){
|
|
364
|
+
let hand = this.xrplayer.trackedobjects['hand_' + this.teleporthand];
|
|
365
|
+
if( hand ) hand.pointer.laser.visible = false // prevent (portal)clicks
|
|
366
|
+
}
|
|
367
|
+
this.active = true;
|
|
368
|
+
this.teleportactive = true;
|
|
346
369
|
this.visible = true;
|
|
347
370
|
this.particles.resetParticles();
|
|
348
371
|
this.particles.visible = true;
|
|
349
372
|
if (this.laser.room !== room._target) room.appendChild(this.laser);
|
|
350
373
|
if (this.particles.room !== room._target) room.appendChild(this.particles);
|
|
351
374
|
this.laser.visible = true;
|
|
352
|
-
this.active = true;
|
|
353
375
|
this.particles.start();
|
|
354
376
|
},
|
|
355
377
|
disableCursor() {
|
|
378
|
+
if( this.xrplayer ){
|
|
379
|
+
let hand = this.xrplayer.trackedobjects['hand_' + this.teleporthand];
|
|
380
|
+
if( hand ) hand.pointer.laser.visible = true // restore original laser
|
|
381
|
+
}
|
|
382
|
+
this.teleportactive = false;
|
|
356
383
|
this.visible = false;
|
|
357
384
|
this.particles.visible = false;
|
|
358
385
|
this.laser.visible = false;
|
|
359
386
|
this.active = false;
|
|
360
387
|
this.particles.stop();
|
|
388
|
+
this.cancelLongPress()
|
|
361
389
|
},
|
|
362
390
|
showShroud() {
|
|
363
391
|
this.shroud.visible = true;
|
|
364
392
|
this.shroud.opacity = 1;
|
|
393
|
+
},
|
|
394
|
+
cancelLongPress(){
|
|
395
|
+
clearTimeout(this.longpresstimer);
|
|
396
|
+
this.longpresstimer = false
|
|
365
397
|
}
|
|
366
398
|
});
|
|
@@ -391,7 +391,7 @@ elation.elements.define('janus.ui.urlbar', class extends elation.elements.ui.pan
|
|
|
391
391
|
handleAccept(ev) {
|
|
392
392
|
var url = this.input.value;
|
|
393
393
|
if (url.length > 0) {
|
|
394
|
-
if (url.indexOf(':') == -1) {
|
|
394
|
+
if (url.indexOf(':') == -1 && !url.match(/^\.\//) ) {
|
|
395
395
|
url = 'http://' + url;
|
|
396
396
|
}
|
|
397
397
|
if (ev.altKey) {
|
|
@@ -408,6 +408,11 @@ elation.elements.define('janus.ui.urlbar', class extends elation.elements.ui.pan
|
|
|
408
408
|
}
|
|
409
409
|
}
|
|
410
410
|
handleBlur(ev) {
|
|
411
|
+
// enter-key on Quest devices (onscreen keyboards) is not emitted (blur-event instead)
|
|
412
|
+
// https://developers.meta.com/horizon/documentation/web/webxr-keyboard/
|
|
413
|
+
if (navigator.userAgent.indexOf('OculusBrowser') != -1) {
|
|
414
|
+
this.handleAccept({altKey:true}) // get straight to it
|
|
415
|
+
}
|
|
411
416
|
setTimeout(() => {
|
|
412
417
|
this.suggestions.hide();
|
|
413
418
|
}, 100);
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
/* XR URI Fragments spec (https://xrfragment.org)
|
|
2
|
+
*
|
|
3
|
+
* level0 sidecar files as per the XR URI Fragments spec
|
|
4
|
+
* https://xrfragment.org/#sidecar%20files
|
|
5
|
+
*
|
|
6
|
+
* NOTE: you can also load sidecarfiles in a roomscript manually:
|
|
7
|
+
*
|
|
8
|
+
* elation.events.add(null, 'room_load_complete', function(e){
|
|
9
|
+
* if( room?.sidecarfile ) room.sidecarfile.load()
|
|
10
|
+
* })
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
elation.require([], function() {
|
|
14
|
+
|
|
15
|
+
elation.extend('janusweb.sidecarfile', class {
|
|
16
|
+
constructor(object) {
|
|
17
|
+
this.scene = elation.engine.instances.default.systems.world.scene['world-3d']
|
|
18
|
+
this._object = object
|
|
19
|
+
this.extension = /\.(gltf|glb|dae)$/
|
|
20
|
+
this.extensionXRF = /\.xrf\./
|
|
21
|
+
if( room.url.match(this.extension) && this.isXRF(this.hideSystemFolder) ){
|
|
22
|
+
this.load()
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
load(){
|
|
27
|
+
this.cleanup()
|
|
28
|
+
this.loadWebVTT() // https://xrfragment.org/#sidecar%20files
|
|
29
|
+
this.loadSound() // https://xrfragment.org/#sidecar%20files
|
|
30
|
+
this.initStartButton()
|
|
31
|
+
this.initSubtitle()
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
hideSystemFolder(o){
|
|
35
|
+
// https://xrfragment.org/#system%20folders
|
|
36
|
+
if(o.name[0] == '_'){
|
|
37
|
+
o.visible = false
|
|
38
|
+
o.pickable = false
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
isXRF(cb){
|
|
43
|
+
// for XRF heuristics see https://xrfragment.org/#%F0%9F%93%9C%20level0%3A%20File
|
|
44
|
+
let heuristic = room.url.match(this.extensionXRF)
|
|
45
|
+
this.scene.traverse( (o) => {
|
|
46
|
+
if( o.userData?.href ) heuristic = true // has XRF hyperlink
|
|
47
|
+
if( o.name[0] == '_' ) heuristic = true // has XRF system folder
|
|
48
|
+
})
|
|
49
|
+
return heuristic
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
loadSound(){ // https://xrfragment.org/#sidecar%20files
|
|
53
|
+
room.removeObject('xrf_audio')
|
|
54
|
+
const audio = room.url.replace(this.extension,'.ogg')
|
|
55
|
+
room.loadNewAsset("sound", {id:"xrf_audio", src:audio,proxy:false})
|
|
56
|
+
this.sound = room.createObject('sound',{ id: "xrf_audio", js_id: 'xrf_audio', loop:true })
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
loadWebVTT(){ // https://xrfragment.org/#sidecar%20files
|
|
60
|
+
const webvtt = room.url.replace(this.extension,'.vtt')
|
|
61
|
+
fetch( webvtt )
|
|
62
|
+
.then( (res) => res.text() )
|
|
63
|
+
.then( (webvtt) => {
|
|
64
|
+
this.webvtt = this.parseWEBVTT(webvtt)
|
|
65
|
+
})
|
|
66
|
+
.catch( () => false ) // no biggy (optional)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
initStartButton(){
|
|
70
|
+
this.btn = room.createObject('object',{
|
|
71
|
+
id: 'cube',
|
|
72
|
+
js_id: 'btnstart',
|
|
73
|
+
pos: player.localToWorld( V(0,1.8,-1) ),
|
|
74
|
+
scale: '0.5 0.125 0.01',
|
|
75
|
+
col: '0.5 0.5 0.5',
|
|
76
|
+
lighting:false,
|
|
77
|
+
sync: true,
|
|
78
|
+
billboard: 'y',
|
|
79
|
+
collision_id: 'cube'
|
|
80
|
+
})
|
|
81
|
+
const label = this.btn.createObject('text',{
|
|
82
|
+
text: 'Start experience',
|
|
83
|
+
lighting:false,
|
|
84
|
+
col: '0 0 0',
|
|
85
|
+
pos: '0 -0.15 0.5',
|
|
86
|
+
scale: '1.2 5 1.2',
|
|
87
|
+
})
|
|
88
|
+
this.btn.addEventListener('click', () => this.start() )
|
|
89
|
+
this.positionStartButton()
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
positionStartButton(){
|
|
93
|
+
setTimeout( () => {
|
|
94
|
+
this.btn.pos = player.localToWorld( V(0,1.8,-1) )
|
|
95
|
+
}, 500)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
initSubtitle(){
|
|
99
|
+
this.subtitle = player.createObject('paragraph',{
|
|
100
|
+
js_id: 'subtitle',
|
|
101
|
+
pos: '0 1.7 -1.5',
|
|
102
|
+
lighting: false,
|
|
103
|
+
pickable: false,
|
|
104
|
+
css: `.paragraphcontainer{
|
|
105
|
+
background: transparent;
|
|
106
|
+
text-align:center;
|
|
107
|
+
height:100%;
|
|
108
|
+
width:100%;
|
|
109
|
+
display: flex;
|
|
110
|
+
align-items: center;
|
|
111
|
+
font-size:55px;
|
|
112
|
+
line-height:55px;
|
|
113
|
+
text-shadow: 0px 0px 2px #FFFF;
|
|
114
|
+
color:black;
|
|
115
|
+
}
|
|
116
|
+
.subtitle{
|
|
117
|
+
width:100%;
|
|
118
|
+
}
|
|
119
|
+
.loading{
|
|
120
|
+
background:#FFF;
|
|
121
|
+
display:inline-block;
|
|
122
|
+
font-size:22px;
|
|
123
|
+
font-family: monospace;
|
|
124
|
+
line-height:30px;
|
|
125
|
+
padding:20px;
|
|
126
|
+
border-radius:15px;
|
|
127
|
+
}
|
|
128
|
+
`,
|
|
129
|
+
text_col: '1 1 1',
|
|
130
|
+
back_col: '1 1 1'
|
|
131
|
+
})
|
|
132
|
+
this.subtitle.objects['3d'].depthWrite = false
|
|
133
|
+
this.subtitle.objects['3d'].depthTest = false
|
|
134
|
+
this.subtitle.objects['3d'].renderOrder = 100
|
|
135
|
+
this.subtitle.setHTML = (html) => {
|
|
136
|
+
this.subtitle.visible = true
|
|
137
|
+
this.subtitle.text = `<div class='subtitle'>${html}</div>`
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
update(){
|
|
142
|
+
if( this.update.id || !this.playing ) return // throttle
|
|
143
|
+
const advance = () => {
|
|
144
|
+
if( !this?.sound?.timeoffset && this?.sound?.audio?.context){
|
|
145
|
+
this.sound.timeoffset = this.sound.audio.context.currentTime
|
|
146
|
+
}
|
|
147
|
+
if( this?.sound?.timeoffset != undefined &&
|
|
148
|
+
this.webvtt?.items[ this.webvtt?.i ] &&
|
|
149
|
+
this?.sound?.playStarted &&
|
|
150
|
+
this?.sound?.audio?.context?.currentTime ){
|
|
151
|
+
|
|
152
|
+
const time = this.sound.audio.context.currentTime - this.sound.timeoffset;
|
|
153
|
+
let item = this.webvtt.items[ this.webvtt.i ]
|
|
154
|
+
if( !item.seen && time > item.start.ts ){
|
|
155
|
+
this.subtitle.setHTML(`
|
|
156
|
+
${ item.who ? `<b>${item.who}:</b><br/>` : '' }
|
|
157
|
+
${item.text.replace(/\n/g,'<br/>')}
|
|
158
|
+
`)
|
|
159
|
+
if( room.hyperlink && item.href && !item.seen ){
|
|
160
|
+
room.hyperlink.execute(item.href, {portalActivateDelay:6000})
|
|
161
|
+
}
|
|
162
|
+
item.seen = true
|
|
163
|
+
}
|
|
164
|
+
if( item.seen && time > item.stop.ts ){
|
|
165
|
+
this.subtitle.visible = false
|
|
166
|
+
this.webvtt.i++
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
this.update.id = false
|
|
170
|
+
}
|
|
171
|
+
this.update.id = setTimeout( () => advance(), 100 ) // throttle
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// naive webvtt parser
|
|
175
|
+
parseWEBVTT(text) {
|
|
176
|
+
let i = 0;
|
|
177
|
+
const WEBVTT_HEADER = /^WEBVTT/i;
|
|
178
|
+
const TIME_LINE = /^([0-9:.]+)\s+-->\s+([0-9:.]+)(?:\s+(.+))?$/;
|
|
179
|
+
const WHO = /^<v ([^>]+)>/
|
|
180
|
+
const HREF = / href:([^ ]+)/
|
|
181
|
+
const result = { type: "WEBVTT", items: [], i: 0 };
|
|
182
|
+
const lines = text.split(/\r?\n/)
|
|
183
|
+
.map( (line) => line.trim() )
|
|
184
|
+
const vttToSeconds = (vttString) => {
|
|
185
|
+
const parts = vttString.split(':');
|
|
186
|
+
let hours = 0, minutes = 0, secondsWithMs;
|
|
187
|
+
if (parts.length === 3) { [hours, minutes, secondsWithMs] = parts; }
|
|
188
|
+
else { [minutes, secondsWithMs] = parts; } // Format is MM:SS.mmm
|
|
189
|
+
const [seconds, milliseconds] = secondsWithMs.split('.');
|
|
190
|
+
return (
|
|
191
|
+
(parseInt(hours) * 3600) +
|
|
192
|
+
(parseInt(minutes) * 60) +
|
|
193
|
+
(parseInt(seconds) * 1) +
|
|
194
|
+
parseInt(milliseconds || 0)
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Skip the WEBVTT header line
|
|
199
|
+
if (WEBVTT_HEADER.test(lines[i].trim())) i++
|
|
200
|
+
while (i < lines.length) {
|
|
201
|
+
if ( lines[i] === "") { i++; continue; } // Skip empty lines
|
|
202
|
+
// Match cue timing line
|
|
203
|
+
const timeMatch = lines[i].match(TIME_LINE);
|
|
204
|
+
if (timeMatch) {
|
|
205
|
+
const item = {
|
|
206
|
+
start: {str:timeMatch[1], ts: vttToSeconds(timeMatch[1]) },
|
|
207
|
+
stop: {str:timeMatch[2], ts: vttToSeconds(timeMatch[2]) },
|
|
208
|
+
href: lines[i].match(HREF) ? lines[i].match(HREF)[1] : false,
|
|
209
|
+
who: false,
|
|
210
|
+
text: ""
|
|
211
|
+
};
|
|
212
|
+
i++;
|
|
213
|
+
const speakerMatch = lines[i]?.match(WHO);
|
|
214
|
+
if (speakerMatch) {
|
|
215
|
+
item.who = speakerMatch[1];
|
|
216
|
+
lines[i] = lines[i].replace(WHO,'');
|
|
217
|
+
}
|
|
218
|
+
// Collect all text lines until next empty line
|
|
219
|
+
while (i < lines.length && lines[i].trim() !== "") {
|
|
220
|
+
item.text += (item.text ? "\n" : "") + lines[i] ;
|
|
221
|
+
i++;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
result.items.push(item);
|
|
225
|
+
} else { i++; }
|
|
226
|
+
}
|
|
227
|
+
return result;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
start(){
|
|
231
|
+
// reset subtitles
|
|
232
|
+
if( this?.webvtt?.items ){
|
|
233
|
+
this.webvtt.items.map( (item) => item.seen = undefined )
|
|
234
|
+
this.webvtt.i = 0;
|
|
235
|
+
}
|
|
236
|
+
// start sound
|
|
237
|
+
this.sound.pos = '0 0 0'
|
|
238
|
+
if( this.sound.isPlaying ) this.sound.stop()
|
|
239
|
+
this.sound.timeoffset = undefined
|
|
240
|
+
this.sound.play()
|
|
241
|
+
// ensure subtitle
|
|
242
|
+
this.subtitle.visible = false
|
|
243
|
+
player.add(this.subtitle)
|
|
244
|
+
// hide button
|
|
245
|
+
this.btn.visible = false
|
|
246
|
+
this.btn.pickable = false
|
|
247
|
+
// mark playing
|
|
248
|
+
this.playing = true
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
stop(){
|
|
253
|
+
this.playing = false
|
|
254
|
+
this.update.id = false
|
|
255
|
+
this.subtitle.visible = false
|
|
256
|
+
this.subtitle.text = ''
|
|
257
|
+
this.btn.visible = true
|
|
258
|
+
this.btn.pickable = true
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
cleanup(){
|
|
262
|
+
const deleteJsId = (js_id) => {
|
|
263
|
+
for( let i in player.children )
|
|
264
|
+
if( player.children[i].js_id == js_id )
|
|
265
|
+
player.remove( player.children[i] )
|
|
266
|
+
}
|
|
267
|
+
deleteJsId('subtitle')
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
})
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
xrf_install_sidecarfiles = function(){
|
|
274
|
+
if( !room.objects?.scene?.modelasset?.loaded ) {
|
|
275
|
+
return setTimeout( xrf_install_sidecarfiles, 300 )
|
|
276
|
+
}
|
|
277
|
+
if( !room.sidecarfile ) room.sidecarfile = new elation.janusweb.sidecarfile(room);
|
|
278
|
+
else{
|
|
279
|
+
room.sidecarfile.positionStartButton() // wait for user being spawned
|
|
280
|
+
room.sidecarfile.stop()
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
xrf_install_sidecarfiles()
|
|
285
|
+
|
|
286
|
+
elation.events.add(null, 'room_load_complete', xrf_install_sidecarfiles )
|
|
287
|
+
elation.events.add(null, 'room_enable', xrf_install_sidecarfiles )
|
|
288
|
+
elation.events.add(null, 'janusweb_script_frame', function(){
|
|
289
|
+
if( room?.sidecarfile ) room.sidecarfile.update()
|
|
290
|
+
})
|
|
291
|
+
elation.events.add(null, 'room_load_start', function(e){
|
|
292
|
+
if( !e.data ) return
|
|
293
|
+
if( room?.sidecarfile?.subtitle ) room.sidecarfile.subtitle.setHTML(`<div class='loading'>🔗 ${e.data.name}<br/><br/>please wait..</div>`)
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
elation.events.add(null, 'room_disable', function(){
|
|
297
|
+
if( room?.sidecarfile?.subtitle ) room.sidecarfile.stop()
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
// some convenience WebVTT cue settings => room function mappings
|
|
301
|
+
// href:#fadeAudioOut&spawnhere => room.fadeAudioOut()
|
|
302
|
+
// href:#myfunc=3 => room.myfunc(3)
|
|
303
|
+
elation.events.add(null, 'href', function(e){
|
|
304
|
+
if( room?.hyperlink ){
|
|
305
|
+
const {url,hash} = room.hyperlink.getUrlObject(e.data.href)
|
|
306
|
+
hash.forEach( (v,k) => { if( room[k] ) room[k](v) })
|
|
307
|
+
}
|
|
308
|
+
})
|