This commit is contained in:
Leon van Kammen 2023-12-13 19:09:58 +01:00
parent 5ec4c8ca8a
commit edbc729a12
13 changed files with 1014 additions and 1276 deletions

1
dist/trystero-torrent.min.js vendored Normal file

File diff suppressed because one or more lines are too long

14
dist/utils.js vendored
View file

@ -55,13 +55,17 @@ function setupConsole(el){
function setupUrlBar(el,XRF){ function setupUrlBar(el,XRF){
let inIframe = window.location !== window.parent.location let inIframe = window.location !== window.parent.location
let ids = ['#overlay','a#embed','a#source','a#model','#qrcode'] let ids = ['#overlay','a#embed','a#clone','a#model','a#meeting','#qrcode']
let showButtons = () => { let showButtons = (state) => {
ids.map( (i) => $(i).style.display = 'block' ) ids.map( (i) => $(i).style.display = state ? 'inline-block' : 'none' )
$('a#more').style.display = 'none' $('a#more').style.display = state ? 'none' : 'inline-block'
if( inIframe ) $('#uri').style.display = 'block' if( inIframe ) $('#uri').style.display = 'block'
} }
$('a#more').addEventListener('click', () => showButtons() ) $('a#more').addEventListener('click', () => showButtons(true) )
$('a#meeting').addEventListener('click', () => {
document.querySelector('a-scene').setAttribute('meeting', 'id: xrfragments')
showButtons(false)
})
XRF.addEventListener('hash', () => reflectUrl() ) XRF.addEventListener('hash', () => reflectUrl() )
const reflectUrl = window.reflectUrl = (url) => { const reflectUrl = window.reflectUrl = (url) => {

View file

@ -1,5 +1,5 @@
/* /*
* v0.5.1 generated at Tue Dec 12 06:06:16 PM CET 2023 * v0.5.1 generated at Wed Dec 13 07:05:52 PM CET 2023
* https://xrfragment.org * https://xrfragment.org
* SPDX-License-Identifier: MPL-2.0 * SPDX-License-Identifier: MPL-2.0
*/ */
@ -2381,13 +2381,17 @@ function setupConsole(el){
function setupUrlBar(el,XRF){ function setupUrlBar(el,XRF){
let inIframe = window.location !== window.parent.location let inIframe = window.location !== window.parent.location
let ids = ['#overlay','a#embed','a#source','a#model','#qrcode'] let ids = ['#overlay','a#embed','a#clone','a#model','a#meeting','#qrcode']
let showButtons = () => { let showButtons = (state) => {
ids.map( (i) => $(i).style.display = 'block' ) ids.map( (i) => $(i).style.display = state ? 'inline-block' : 'none' )
$('a#more').style.display = 'none' $('a#more').style.display = state ? 'none' : 'inline-block'
if( inIframe ) $('#uri').style.display = 'block' if( inIframe ) $('#uri').style.display = 'block'
} }
$('a#more').addEventListener('click', () => showButtons() ) $('a#more').addEventListener('click', () => showButtons(true) )
$('a#meeting').addEventListener('click', () => {
document.querySelector('a-scene').setAttribute('meeting', 'id: xrfragments')
showButtons(false)
})
XRF.addEventListener('hash', () => reflectUrl() ) XRF.addEventListener('hash', () => reflectUrl() )
const reflectUrl = window.reflectUrl = (url) => { const reflectUrl = window.reflectUrl = (url) => {
@ -2768,6 +2772,280 @@ window.AFRAME.registerComponent('xrf', {
}, },
}) })
AFRAME.registerComponent('meeting', {
schema:{
id:{ required:true, type:'string'}
},
init: function(){
// embed https://github.com/dmotz/trystero (trystero-torrent.min.js build)
// add css+html
let el = document.createElement("div")
el.innerHTML += `<style type="text/css">
#videos{
display:grid-auto-columns;
grid-column-gap:5px;
margin-bottom:15px;
position: fixed;
top: 0;
left: 0;
bottom: 0;
right: 0;
margin: 15px;
z-index:1500;
}
#videos > video{
border-radius:7px;
display:inline-block;
background:black;
width:80px;
height:60px;
margin-right:5px;
margin-bottom:5px;
vertical-align:top;
pointer-events:all;
}
#videos > video:hover{
filter: brightness(1.8);
cursor:pointer;
}
#chatbar {
z-index: 1500;
position: fixed;
bottom: 20px;
left: 20px;
width: 48%;
background: white;
padding: 7px 0px 7px 15px;
border-radius: 30px;
max-width: 500px;
box-sizing: border-box;
box-shadow: 0px 0px 5px 5px #0002;
}
#chatbar input{
border:none;
width:93%;
}
#chat{
position: absolute;
top: 100px;
left: 0;
right: 0;
bottom: 110px;
padding: 15px;
pointer-events: none;
}
#chat .msg{
background: #fffc;
display: inline-block;
padding: 6px 17px;
border-radius: 20px;
color: #000c;
margin-bottom: 10px;
}
#chat,#chatbar,#chatbar *, #chat *{
font-family:monospace;
font-size:16px;
}
#chatbar * {
font-size:20px;
}
}
</style>
<div id="videos" style="pointer-events:none"></div>
<div id="chat"></div>
<div id="chatbar">
<input id="chatline" type="text" placeholder="chat here"></input>
</div>`
document.body.appendChild(el)
this.trysteroInit()
},
trysteroInit: async function(){
const { joinRoom } = await import("/dist/trystero-torrent.min.js");
const roomname = document.location.href.replace(/#.*/,'')
const config = this.config = {appId: this.data.id }
const room = this.room = joinRoom(config, roomname )
console.log("starting webrtc room: "+roomname)
const idsToNames = this.idsToNames = {}
const [sendName, getName] = room.makeAction('name')
const [sendChat, getChat] = this.room.makeAction('chat')
this.sendChat = sendChat
this.sendName = sendName
this.getChat = getChat
this.getName = getName
// tell other peers currently in the room our name
let name = prompt('enter your name:')
idsToNames[ room.selfId ] = name.substr(0,15)
sendName( name )
// listen for peers naming themselves
getName((name, peerId) => (idsToNames[peerId] = name))
this.initChat()
// this object can store audio instances for later
const peerAudios = this.peerAudios = {}
const peerVideos = this.peerVideos = {}
// get a local audio stream from the microphone
const selfStream = await navigator.mediaDevices.getUserMedia({
audio: true,
video: {
width: 320,
height: 240,
frameRate: {
ideal: 30,
min: 10
}
}
})
let meVideo = this.addVideo(selfStream, room.selfId)
meVideo.muted = true
// send stream to peers currently in the room
room.addStream(selfStream)
// send stream + chatlog to peers who join later
room.onPeerJoin( (peerId) => {
console.log(`${idsToNames[peerId] || 'a visitor'} joined`)
room.addStream(selfStream, peerId)
sendName( name, peerId)
this.sendChat({prime: this.chat.log}, peerId )
})
room.onPeerLeave( (peerId) => {
console.log(`${idsToNames[peerId] || 'a visitor'} left`)
if( peerVideos[peerId] ){
peerVideos[peerId].remove()
delete peerVideos[peerId]
}
delete idsToNames[peerId]
})
// handle streams from other peers
room.onPeerStream((stream, peerId) => {
// create an audio instance and set the incoming stream
const audio = new Audio()
audio.srcObject = stream
audio.autoplay = true
// add the audio to peerAudio object if you want to address it for something
// later (volume, etc.)
peerAudios[peerId] = audio
})
room.onPeerStream((stream, peerId) => {
this.addVideo(stream,peerId)
})
},
addVideo: function(stream,peerId){
let video = this.peerVideos[peerId]
const videoContainer = document.getElementById('videos')
// if this peer hasn't sent a stream before, create a video element
if (!video) {
video = document.createElement('video')
video.autoplay = true
// add video element to the DOM
videoContainer.appendChild(video)
}
video.srcObject = stream
video.resize = (state) => {
if( video.resize.state == undefined ) video.resize.state = false
if( state == undefined ) state = (video.resize.state = !video.resize.state )
video.style.width = state ? '320px' : '80px'
video.style.height = state ? '200px' : '60px'
}
video.addEventListener('click', () => video.resize() )
this.peerVideos[peerId] = video
return video
},
createElement: function(str){
let el = document.createElement("div")
el.className = "msg"
el.innerHTML = this.linkify(str)
return el
},
// central function to broadcast stuff to chat
send: function(str){
if( !this.sendChat ) return
this.sendChat({content:str}) // send to network
this.chat.append(str) // send to screen
},
initChat: function(){
let chatline = this.chatline = document.querySelector("#chatline")
let chat = this.chat = document.querySelector("#chat")
chat.log = [] // save raw chatlog to prime new visitors
chat.append = (str) => {
if( !str ) return
str = str.replace('\n', "<br>")
this.chat.appendChild(this.createElement(str)) // send to screen
this.chat.innerHTML += '<br>'
this.chat.log.push(str)
}
let send = () => {
let str = `${this.idsToNames[ this.room.selfId ]}: ${chatline.value.substr(0,65515).trim()}`
this.send(str)
}
chatline.addEventListener("keydown", (e) => {
if( e.key !== "Enter" ) return
send()
chatline.value = ''
})
// listen for chatmsg
this.getChat((data, peerId) => {
if( data.prime ){
if( this.chat.primed ) return // only prime once
console.log("receiving prime")
data.prime.map( (l) => chat.append(l) ) // send log to screen
this.chat.primed = true
}
chat.append(data.content) // send to screen
})
return this
},
linkify: function(t){
const isValidHttpUrl = s => {
let u
try {u = new URL(s)}
catch (_) {return false}
return u.protocol.startsWith("http")
}
const m = t.match(/(?<=\s|^)[a-zA-Z0-9-:/]+\.[a-zA-Z0-9-].+?(?=[.,;:?!-]?(?:\s|$))/g)
if (!m) return t
const a = []
m.forEach(x => {
const [t1, ...t2] = t.split(x)
a.push(t1)
t = t2.join(x)
const y = (!(x.match(/:\/\//)) ? 'https://' : '') + x
if (isNaN(x) && isValidHttpUrl(y))
a.push('<a href="' + y + '" target="_blank" style="pointer-events:all">' + y.split('/')[2] + '</a>')
else
a.push(x)
})
a.push(t)
return a.join('')
}
});
// look-controls turns off autoUpdateMatrix (of player) which // look-controls turns off autoUpdateMatrix (of player) which
// will break teleporting and other stuff // will break teleporting and other stuff
// overriding this is easier then adding updateMatrixWorld() everywhere else // overriding this is easier then adding updateMatrixWorld() everywhere else

View file

@ -1,5 +1,5 @@
/* /*
* v0.5.1 generated at Tue Dec 12 06:06:16 PM CET 2023 * v0.5.1 generated at Wed Dec 13 07:05:52 PM CET 2023
* https://xrfragment.org * https://xrfragment.org
* SPDX-License-Identifier: MPL-2.0 * SPDX-License-Identifier: MPL-2.0
*/ */
@ -2375,13 +2375,17 @@ function setupConsole(el){
function setupUrlBar(el,XRF){ function setupUrlBar(el,XRF){
let inIframe = window.location !== window.parent.location let inIframe = window.location !== window.parent.location
let ids = ['#overlay','a#embed','a#source','a#model','#qrcode'] let ids = ['#overlay','a#embed','a#clone','a#model','a#meeting','#qrcode']
let showButtons = () => { let showButtons = (state) => {
ids.map( (i) => $(i).style.display = 'block' ) ids.map( (i) => $(i).style.display = state ? 'inline-block' : 'none' )
$('a#more').style.display = 'none' $('a#more').style.display = state ? 'none' : 'inline-block'
if( inIframe ) $('#uri').style.display = 'block' if( inIframe ) $('#uri').style.display = 'block'
} }
$('a#more').addEventListener('click', () => showButtons() ) $('a#more').addEventListener('click', () => showButtons(true) )
$('a#meeting').addEventListener('click', () => {
document.querySelector('a-scene').setAttribute('meeting', 'id: xrfragments')
showButtons(false)
})
XRF.addEventListener('hash', () => reflectUrl() ) XRF.addEventListener('hash', () => reflectUrl() )
const reflectUrl = window.reflectUrl = (url) => { const reflectUrl = window.reflectUrl = (url) => {
@ -2762,6 +2766,280 @@ window.AFRAME.registerComponent('xrf', {
}, },
}) })
AFRAME.registerComponent('meeting', {
schema:{
id:{ required:true, type:'string'}
},
init: function(){
// embed https://github.com/dmotz/trystero (trystero-torrent.min.js build)
// add css+html
let el = document.createElement("div")
el.innerHTML += `<style type="text/css">
#videos{
display:grid-auto-columns;
grid-column-gap:5px;
margin-bottom:15px;
position: fixed;
top: 0;
left: 0;
bottom: 0;
right: 0;
margin: 15px;
z-index:1500;
}
#videos > video{
border-radius:7px;
display:inline-block;
background:black;
width:80px;
height:60px;
margin-right:5px;
margin-bottom:5px;
vertical-align:top;
pointer-events:all;
}
#videos > video:hover{
filter: brightness(1.8);
cursor:pointer;
}
#chatbar {
z-index: 1500;
position: fixed;
bottom: 20px;
left: 20px;
width: 48%;
background: white;
padding: 7px 0px 7px 15px;
border-radius: 30px;
max-width: 500px;
box-sizing: border-box;
box-shadow: 0px 0px 5px 5px #0002;
}
#chatbar input{
border:none;
width:93%;
}
#chat{
position: absolute;
top: 100px;
left: 0;
right: 0;
bottom: 110px;
padding: 15px;
pointer-events: none;
}
#chat .msg{
background: #fffc;
display: inline-block;
padding: 6px 17px;
border-radius: 20px;
color: #000c;
margin-bottom: 10px;
}
#chat,#chatbar,#chatbar *, #chat *{
font-family:monospace;
font-size:16px;
}
#chatbar * {
font-size:20px;
}
}
</style>
<div id="videos" style="pointer-events:none"></div>
<div id="chat"></div>
<div id="chatbar">
<input id="chatline" type="text" placeholder="chat here"></input>
</div>`
document.body.appendChild(el)
this.trysteroInit()
},
trysteroInit: async function(){
const { joinRoom } = await import("/dist/trystero-torrent.min.js");
const roomname = document.location.href.replace(/#.*/,'')
const config = this.config = {appId: this.data.id }
const room = this.room = joinRoom(config, roomname )
console.log("starting webrtc room: "+roomname)
const idsToNames = this.idsToNames = {}
const [sendName, getName] = room.makeAction('name')
const [sendChat, getChat] = this.room.makeAction('chat')
this.sendChat = sendChat
this.sendName = sendName
this.getChat = getChat
this.getName = getName
// tell other peers currently in the room our name
let name = prompt('enter your name:')
idsToNames[ room.selfId ] = name.substr(0,15)
sendName( name )
// listen for peers naming themselves
getName((name, peerId) => (idsToNames[peerId] = name))
this.initChat()
// this object can store audio instances for later
const peerAudios = this.peerAudios = {}
const peerVideos = this.peerVideos = {}
// get a local audio stream from the microphone
const selfStream = await navigator.mediaDevices.getUserMedia({
audio: true,
video: {
width: 320,
height: 240,
frameRate: {
ideal: 30,
min: 10
}
}
})
let meVideo = this.addVideo(selfStream, room.selfId)
meVideo.muted = true
// send stream to peers currently in the room
room.addStream(selfStream)
// send stream + chatlog to peers who join later
room.onPeerJoin( (peerId) => {
console.log(`${idsToNames[peerId] || 'a visitor'} joined`)
room.addStream(selfStream, peerId)
sendName( name, peerId)
this.sendChat({prime: this.chat.log}, peerId )
})
room.onPeerLeave( (peerId) => {
console.log(`${idsToNames[peerId] || 'a visitor'} left`)
if( peerVideos[peerId] ){
peerVideos[peerId].remove()
delete peerVideos[peerId]
}
delete idsToNames[peerId]
})
// handle streams from other peers
room.onPeerStream((stream, peerId) => {
// create an audio instance and set the incoming stream
const audio = new Audio()
audio.srcObject = stream
audio.autoplay = true
// add the audio to peerAudio object if you want to address it for something
// later (volume, etc.)
peerAudios[peerId] = audio
})
room.onPeerStream((stream, peerId) => {
this.addVideo(stream,peerId)
})
},
addVideo: function(stream,peerId){
let video = this.peerVideos[peerId]
const videoContainer = document.getElementById('videos')
// if this peer hasn't sent a stream before, create a video element
if (!video) {
video = document.createElement('video')
video.autoplay = true
// add video element to the DOM
videoContainer.appendChild(video)
}
video.srcObject = stream
video.resize = (state) => {
if( video.resize.state == undefined ) video.resize.state = false
if( state == undefined ) state = (video.resize.state = !video.resize.state )
video.style.width = state ? '320px' : '80px'
video.style.height = state ? '200px' : '60px'
}
video.addEventListener('click', () => video.resize() )
this.peerVideos[peerId] = video
return video
},
createElement: function(str){
let el = document.createElement("div")
el.className = "msg"
el.innerHTML = this.linkify(str)
return el
},
// central function to broadcast stuff to chat
send: function(str){
if( !this.sendChat ) return
this.sendChat({content:str}) // send to network
this.chat.append(str) // send to screen
},
initChat: function(){
let chatline = this.chatline = document.querySelector("#chatline")
let chat = this.chat = document.querySelector("#chat")
chat.log = [] // save raw chatlog to prime new visitors
chat.append = (str) => {
if( !str ) return
str = str.replace('\n', "<br>")
this.chat.appendChild(this.createElement(str)) // send to screen
this.chat.innerHTML += '<br>'
this.chat.log.push(str)
}
let send = () => {
let str = `${this.idsToNames[ this.room.selfId ]}: ${chatline.value.substr(0,65515).trim()}`
this.send(str)
}
chatline.addEventListener("keydown", (e) => {
if( e.key !== "Enter" ) return
send()
chatline.value = ''
})
// listen for chatmsg
this.getChat((data, peerId) => {
if( data.prime ){
if( this.chat.primed ) return // only prime once
console.log("receiving prime")
data.prime.map( (l) => chat.append(l) ) // send log to screen
this.chat.primed = true
}
chat.append(data.content) // send to screen
})
return this
},
linkify: function(t){
const isValidHttpUrl = s => {
let u
try {u = new URL(s)}
catch (_) {return false}
return u.protocol.startsWith("http")
}
const m = t.match(/(?<=\s|^)[a-zA-Z0-9-:/]+\.[a-zA-Z0-9-].+?(?=[.,;:?!-]?(?:\s|$))/g)
if (!m) return t
const a = []
m.forEach(x => {
const [t1, ...t2] = t.split(x)
a.push(t1)
t = t2.join(x)
const y = (!(x.match(/:\/\//)) ? 'https://' : '') + x
if (isNaN(x) && isValidHttpUrl(y))
a.push('<a href="' + y + '" target="_blank" style="pointer-events:all">' + y.split('/')[2] + '</a>')
else
a.push(x)
})
a.push(t)
return a.join('')
}
});
// look-controls turns off autoUpdateMatrix (of player) which // look-controls turns off autoUpdateMatrix (of player) which
// will break teleporting and other stuff // will break teleporting and other stuff
// overriding this is easier then adding updateMatrixWorld() everywhere else // overriding this is easier then adding updateMatrixWorld() everywhere else

File diff suppressed because it is too large Load diff

View file

@ -1,5 +1,5 @@
/* /*
* v0.5.1 generated at Tue Dec 12 06:06:16 PM CET 2023 * v0.5.1 generated at Wed Dec 13 07:05:52 PM CET 2023
* https://xrfragment.org * https://xrfragment.org
* SPDX-License-Identifier: MPL-2.0 * SPDX-License-Identifier: MPL-2.0
*/ */

View file

@ -1,5 +1,5 @@
/* /*
* v0.5.1 generated at Tue Dec 12 06:06:16 PM CET 2023 * v0.5.1 generated at Wed Dec 13 07:05:52 PM CET 2023
* https://xrfragment.org * https://xrfragment.org
* SPDX-License-Identifier: MPL-2.0 * SPDX-License-Identifier: MPL-2.0
*/ */

View file

@ -20,13 +20,7 @@
<input type="text" id="uri" value="" onchange="AFRAME.XRF.navigator.to( $('#uri').value )" style="display:none"/> <input type="text" id="uri" value="" onchange="AFRAME.XRF.navigator.to( $('#uri').value )" style="display:none"/>
</div> </div>
<!-- open AFRAME inspector: $('a-scene').components.inspector.openInspector() -->
<a class="btn-foot" id="source" target="_blank" href="https://github.com/coderofsalvation/xrfragment-helloworld"> host</a>
<a class="btn-foot" id="embed" target="_blank" onclick="window.embed()">🔗 share</a>
<a class="btn-foot" id="model" target="_blank" onclick="window.download()">⬇️ scene</a>
<a class="btn-foot" id="more" target="_blank">XRF</a>
<textarea style="display:none"></textarea> <textarea style="display:none"></textarea>
<canvas id="qrcode" style="display:none" width="300" height="300"></canvas>
<a-scene xr-mode-ui="XRMode: xr" renderer="colorManagement: true; highRefreshRate:true" light="defaultLightsEnabled: false"> <a-scene xr-mode-ui="XRMode: xr" renderer="colorManagement: true; highRefreshRate:true" light="defaultLightsEnabled: false">
<a-entity id="player" wasd-controls look-controls> <a-entity id="player" wasd-controls look-controls>
@ -96,5 +90,15 @@
}) })
</script> </script>
<!-- open AFRAME inspector: $('a-scene').components.inspector.openInspector() -->
<div class="footer">
<canvas id="qrcode" class="btn-foot" style="display:none" width="121" height="121"></canvas>
<a class="btn-foot" id="meeting" target="_blank">🧑‍🤝‍🧑 meeting</a>
<a class="btn-foot" id="clone" target="_blank" href="https://github.com/coderofsalvation/xrfragment-helloworld"> clone</a>
<a class="btn-foot" id="embed" target="_blank" onclick="window.embed()">🔗 share</a>
<a class="btn-foot" id="model" target="_blank" onclick="window.download()">⬇️ scene</a>
<a class="btn-foot" id="more" target="_blank"></a>
</div>
</body> </body>
</html> </html>

BIN
example/assets/blocky.glb Normal file

Binary file not shown.

View file

@ -120,7 +120,7 @@ input[type="submit"] {
text-decoration:none; text-decoration:none;
} }
a.btn-foot#embed{ a.btn-foot#clone{
color: #888; color: #888;
bottom: 129px; bottom: 129px;
} }
@ -133,14 +133,20 @@ a.btn-foot#more{
bottom:72px; bottom:72px;
width: 37px; width: 37px;
min-width: 37px; min-width: 37px;
font-size:23px;
text-align: center; text-align: center;
} }
a.btn-foot#source{ a.btn-foot#embed{
bottom:186px; bottom:186px;
} }
a#source, a.btn-foot#meeting{
bottom:246px;
}
a#meeting,
a#clone,
a#embed, a#embed,
a#model{ a#model{
display:none display:none
@ -217,9 +223,9 @@ html{
transition: all ease .5s; transition: all ease .5s;
border-radius: 3px; border-radius: 3px;
box-shadow: 0 0 4px 0 #0007; box-shadow: 0 0 4px 0 #0007;
left: 20px; right: 20px;
position: fixed; position: fixed;
bottom: 20px; top: 20px;
} }
.js-snackbar { .js-snackbar {
@ -291,18 +297,15 @@ html{
#qrcode{ #qrcode{
z-index: 2000; z-index: 2000;
position: absolute; width: 111px;
border: 5px solid #1c1c3299;
top: 66px;
right: 20px;
width: 175px;
border-radius: 20px; border-radius: 20px;
background: #fff; background: #fff;
border: 5px solid #888;
border-radius: 15px; border-radius: 15px;
margin: 20px 20px;
overflow: hidden; overflow: hidden;
-webkit-mask-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAA5JREFUeNpiYGBgAAgwAAAEAAGbA+oJAAAAAElFTkSuQmCC); -webkit-mask-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAA5JREFUeNpiYGBgAAgwAAAEAAGbA+oJAAAAAElFTkSuQmCC);
bottom: 306px;
height: 121px;
right: 19px;
} }
input#share{ input#share{

View file

@ -55,13 +55,17 @@ export function setupConsole(el){
export function setupUrlBar(el,XRF){ export function setupUrlBar(el,XRF){
let inIframe = window.location !== window.parent.location let inIframe = window.location !== window.parent.location
let ids = ['#overlay','a#embed','a#source','a#model','#qrcode'] let ids = ['#overlay','a#embed','a#clone','a#model','a#meeting','#qrcode']
let showButtons = () => { let showButtons = (state) => {
ids.map( (i) => $(i).style.display = 'block' ) ids.map( (i) => $(i).style.display = state ? 'inline-block' : 'none' )
$('a#more').style.display = 'none' $('a#more').style.display = state ? 'none' : 'inline-block'
if( inIframe ) $('#uri').style.display = 'block' if( inIframe ) $('#uri').style.display = 'block'
} }
$('a#more').addEventListener('click', () => showButtons() ) $('a#more').addEventListener('click', () => showButtons(true) )
$('a#meeting').addEventListener('click', () => {
document.querySelector('a-scene').setAttribute('meeting', 'id: xrfragments')
showButtons(false)
})
XRF.addEventListener('hash', () => reflectUrl() ) XRF.addEventListener('hash', () => reflectUrl() )
const reflectUrl = window.reflectUrl = (url) => { const reflectUrl = window.reflectUrl = (url) => {

BIN
example/assets/sloth.glb Normal file

Binary file not shown.

View file

@ -0,0 +1,274 @@
AFRAME.registerComponent('meeting', {
schema:{
id:{ required:true, type:'string'}
},
init: function(){
// embed https://github.com/dmotz/trystero (trystero-torrent.min.js build)
// add css+html
let el = document.createElement("div")
el.innerHTML += `<style type="text/css">
#videos{
display:grid-auto-columns;
grid-column-gap:5px;
margin-bottom:15px;
position: fixed;
top: 0;
left: 0;
bottom: 0;
right: 0;
margin: 15px;
z-index:1500;
}
#videos > video{
border-radius:7px;
display:inline-block;
background:black;
width:80px;
height:60px;
margin-right:5px;
margin-bottom:5px;
vertical-align:top;
pointer-events:all;
}
#videos > video:hover{
filter: brightness(1.8);
cursor:pointer;
}
#chatbar {
z-index: 1500;
position: fixed;
bottom: 20px;
left: 20px;
width: 48%;
background: white;
padding: 7px 0px 7px 15px;
border-radius: 30px;
max-width: 500px;
box-sizing: border-box;
box-shadow: 0px 0px 5px 5px #0002;
}
#chatbar input{
border:none;
width:93%;
}
#chat{
position: absolute;
top: 100px;
left: 0;
right: 0;
bottom: 110px;
padding: 15px;
pointer-events: none;
}
#chat .msg{
background: #fffc;
display: inline-block;
padding: 6px 17px;
border-radius: 20px;
color: #000c;
margin-bottom: 10px;
}
#chat,#chatbar,#chatbar *, #chat *{
font-family:monospace;
font-size:16px;
}
#chatbar * {
font-size:20px;
}
}
</style>
<div id="videos" style="pointer-events:none"></div>
<div id="chat"></div>
<div id="chatbar">
<input id="chatline" type="text" placeholder="chat here"></input>
</div>`
document.body.appendChild(el)
this.trysteroInit()
},
trysteroInit: async function(){
const { joinRoom } = await import("/dist/trystero-torrent.min.js");
const roomname = document.location.href.replace(/#.*/,'')
const config = this.config = {appId: this.data.id }
const room = this.room = joinRoom(config, roomname )
console.log("starting webrtc room: "+roomname)
const idsToNames = this.idsToNames = {}
const [sendName, getName] = room.makeAction('name')
const [sendChat, getChat] = this.room.makeAction('chat')
this.sendChat = sendChat
this.sendName = sendName
this.getChat = getChat
this.getName = getName
// tell other peers currently in the room our name
let name = prompt('enter your name:')
idsToNames[ room.selfId ] = name.substr(0,15)
sendName( name )
// listen for peers naming themselves
getName((name, peerId) => (idsToNames[peerId] = name))
this.initChat()
// this object can store audio instances for later
const peerAudios = this.peerAudios = {}
const peerVideos = this.peerVideos = {}
// get a local audio stream from the microphone
const selfStream = await navigator.mediaDevices.getUserMedia({
audio: true,
video: {
width: 320,
height: 240,
frameRate: {
ideal: 30,
min: 10
}
}
})
let meVideo = this.addVideo(selfStream, room.selfId)
meVideo.muted = true
// send stream to peers currently in the room
room.addStream(selfStream)
// send stream + chatlog to peers who join later
room.onPeerJoin( (peerId) => {
console.log(`${idsToNames[peerId] || 'a visitor'} joined`)
room.addStream(selfStream, peerId)
sendName( name, peerId)
this.sendChat({prime: this.chat.log}, peerId )
})
room.onPeerLeave( (peerId) => {
console.log(`${idsToNames[peerId] || 'a visitor'} left`)
if( peerVideos[peerId] ){
peerVideos[peerId].remove()
delete peerVideos[peerId]
}
delete idsToNames[peerId]
})
// handle streams from other peers
room.onPeerStream((stream, peerId) => {
// create an audio instance and set the incoming stream
const audio = new Audio()
audio.srcObject = stream
audio.autoplay = true
// add the audio to peerAudio object if you want to address it for something
// later (volume, etc.)
peerAudios[peerId] = audio
})
room.onPeerStream((stream, peerId) => {
this.addVideo(stream,peerId)
})
},
addVideo: function(stream,peerId){
let video = this.peerVideos[peerId]
const videoContainer = document.getElementById('videos')
// if this peer hasn't sent a stream before, create a video element
if (!video) {
video = document.createElement('video')
video.autoplay = true
// add video element to the DOM
videoContainer.appendChild(video)
}
video.srcObject = stream
video.resize = (state) => {
if( video.resize.state == undefined ) video.resize.state = false
if( state == undefined ) state = (video.resize.state = !video.resize.state )
video.style.width = state ? '320px' : '80px'
video.style.height = state ? '200px' : '60px'
}
video.addEventListener('click', () => video.resize() )
this.peerVideos[peerId] = video
return video
},
createElement: function(str){
let el = document.createElement("div")
el.className = "msg"
el.innerHTML = this.linkify(str)
return el
},
// central function to broadcast stuff to chat
send: function(str){
if( !this.sendChat ) return
this.sendChat({content:str}) // send to network
this.chat.append(str) // send to screen
},
initChat: function(){
let chatline = this.chatline = document.querySelector("#chatline")
let chat = this.chat = document.querySelector("#chat")
chat.log = [] // save raw chatlog to prime new visitors
chat.append = (str) => {
if( !str ) return
str = str.replace('\n', "<br>")
this.chat.appendChild(this.createElement(str)) // send to screen
this.chat.innerHTML += '<br>'
this.chat.log.push(str)
}
let send = () => {
let str = `${this.idsToNames[ this.room.selfId ]}: ${chatline.value.substr(0,65515).trim()}`
this.send(str)
}
chatline.addEventListener("keydown", (e) => {
if( e.key !== "Enter" ) return
send()
chatline.value = ''
})
// listen for chatmsg
this.getChat((data, peerId) => {
if( data.prime ){
if( this.chat.primed ) return // only prime once
console.log("receiving prime")
data.prime.map( (l) => chat.append(l) ) // send log to screen
this.chat.primed = true
}
chat.append(data.content) // send to screen
})
return this
},
linkify: function(t){
const isValidHttpUrl = s => {
let u
try {u = new URL(s)}
catch (_) {return false}
return u.protocol.startsWith("http")
}
const m = t.match(/(?<=\s|^)[a-zA-Z0-9-:/]+\.[a-zA-Z0-9-].+?(?=[.,;:?!-]?(?:\s|$))/g)
if (!m) return t
const a = []
m.forEach(x => {
const [t1, ...t2] = t.split(x)
a.push(t1)
t = t2.join(x)
const y = (!(x.match(/:\/\//)) ? 'https://' : '') + x
if (isNaN(x) && isValidHttpUrl(y))
a.push('<a href="' + y + '" target="_blank" style="pointer-events:all">' + y.split('/')[2] + '</a>')
else
a.push(x)
})
a.push(t)
return a.join('')
}
});