xrfragment/dist/xrfragment.plugin.p2p.js

393 lines
40 KiB
JavaScript
Raw Normal View History

/*
* v0.5.1 generated at Wed Jan 15 10:52:05 AM CET 2025
* https://xrfragment.org
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
2024-01-03 15:23:34 +01:00
(function(){
const MAX_BUFFERED_AMOUNT=65536,ICECOMPLETE_TIMEOUT=5e3,CHANNEL_CLOSING_TIMEOUT=5e3;function randombytes(e){const t=new Uint8Array(e);for(let s=0;s<e;s++)t[s]=256*Math.random()|0;return t}function getBrowserRTC(){if("undefined"==typeof globalThis)return null;const e={RTCPeerConnection:globalThis.RTCPeerConnection||globalThis.mozRTCPeerConnection||globalThis.webkitRTCPeerConnection,RTCSessionDescription:globalThis.RTCSessionDescription||globalThis.mozRTCSessionDescription||globalThis.webkitRTCSessionDescription,RTCIceCandidate:globalThis.RTCIceCandidate||globalThis.mozRTCIceCandidate||globalThis.webkitRTCIceCandidate};return e.RTCPeerConnection?e:null}function errCode(e,t){return Object.defineProperty(e,"code",{value:t,enumerable:!0,configurable:!0}),e}function filterTrickle(e){return e.replace(/a=ice-options:trickle\s\n/g,"")}function warn(e){console.warn(e)}class Peer{constructor(e={}){if(this._map=new Map,this._id=randombytes(4).toString("hex").slice(0,7),this._doDebug=e.debug,this._debug("new peer %o",e),this.channelName=e.initiator?e.channelName||randombytes(20).toString("hex"):null,this.initiator=e.initiator||!1,this.channelConfig=e.channelConfig||Peer.channelConfig,this.channelNegotiated=this.channelConfig.negotiated,this.config=Object.assign({},Peer.config,e.config),this.offerOptions=e.offerOptions||{},this.answerOptions=e.answerOptions||{},this.sdpTransform=e.sdpTransform||(e=>e),this.streams=e.streams||(e.stream?[e.stream]:[]),this.trickle=void 0===e.trickle||e.trickle,this.allowHalfTrickle=void 0!==e.allowHalfTrickle&&e.allowHalfTrickle,this.iceCompleteTimeout=e.iceCompleteTimeout||5e3,this.destroyed=!1,this.destroying=!1,this._connected=!1,this.remoteAddress=void 0,this.remoteFamily=void 0,this.remotePort=void 0,this.localAddress=void 0,this.localFamily=void 0,this.localPort=void 0,this._wrtc=e.wrtc&&"object"==typeof e.wrtc?e.wrtc:getBrowserRTC(),!this._wrtc)throw"undefined"==typeof window?errCode(new Error("No WebRTC support: Specify `opts.wrtc` option in this environment"),"ERR_WEBRTC_SUPPORT"):errCode(new Error("No WebRTC support: Not a supported browser"),"ERR_WEBRTC_SUPPORT");this._pcReady=!1,this._channelReady=!1,this._iceComplete=!1,this._iceCompleteTimer=null,this._channel=null,this._pendingCandidates=[],this._isNegotiating=!1,this._firstNegotiation=!0,this._batchedNegotiation=!1,this._queuedNegotiation=!1,this._sendersAwaitingStable=[],this._senderMap=new Map,this._closingInterval=null,this._remoteTracks=[],this._remoteStreams=[],this._chunk=null,this._cb=null,this._interval=null;try{this._pc=new this._wrtc.RTCPeerConnection(this.config)}catch(e){return void this.destroy(errCode(e,"ERR_PC_CONSTRUCTOR"))}this._isReactNativeWebrtc="number"==typeof this._pc._peerConnectionId,this._pc.oniceconnectionstatechange=()=>{this._onIceStateChange()},this._pc.onicegatheringstatechange=()=>{this._onIceStateChange()},this._pc.onconnectionstatechange=()=>{this._onConnectionStateChange()},this._pc.onsignalingstatechange=()=>{this._onSignalingStateChange()},this._pc.onicecandidate=e=>{this._onIceCandidate(e)},"object"==typeof this._pc.peerIdentity&&this._pc.peerIdentity.catch((e=>{this.destroy(errCode(e,"ERR_PC_PEER_IDENTITY"))})),this.initiator||this.channelNegotiated?this._setupData({channel:this._pc.createDataChannel(this.channelName,this.channelConfig)}):this._pc.ondatachannel=e=>{this._setupData(e)},this.streams&&this.streams.forEach((e=>{this.addStream(e)})),this._pc.ontrack=e=>{this._onTrack(e)},this._debug("initial negotiation"),this._needsNegotiation()}get bufferSize(){return this._channel&&this._channel.bufferedAmount||0}get connected(){return this._connected&&"open"===this._channel.readyState}address(){return{port:this.localPort,family:this.localFamily,address:this.localAddress}}signal(e){if(!this.destroying){if(this.destroyed)throw errCode(new Error("cannot signal after peer is destroyed"),"ERR_DESTROYED");if("string"==typeof e)try{e=JSON.parse(e)}catch(t){e={}}this._debug("signal()"),e.renegotiate&&this.initiator&&(this._debug("got request to renegotiate"),this._needsNegotiation()),e.transceiverRequest&&t
window.trystero = (opts) => new Proxy({
2024-01-29 21:19:04 +01:00
profile:{
2024-01-03 15:23:34 +01:00
type: 'network',
name: 'Peer2Peer',
2024-01-29 21:19:04 +01:00
description: 'WebRTC over bittorrent for signaling & encryption',
2024-01-03 15:23:34 +01:00
url: 'https://github.com/dmotz/trystero',
protocol: 'trystero://',
video: true,
audio: true,
chat: true,
scene: true
},
html: {
generic: (opts) => `<div>
2024-07-12 19:25:10 +02:00
<a class="badge ruler">Peer2Peer</a><br>
2024-01-03 15:23:34 +01:00
<table>
<tr>
<td>nickname</td>
<td>
2024-01-29 21:19:04 +01:00
<input type="text" id="nickname" placeholder="your nickname" maxlength="18" onkeydown="trystero.nickname = this.value"/>
2024-01-03 15:23:34 +01:00
</td>
</tr>
</table>
</div>
`
},
room: null, // { selfId: .... } when connected
link: '',
selfId: null,
selfStream: null,
nickname: '',
connected: false,
useWebcam: false,
useChat: false,
useScene: false,
videos: {},
names: {},
ping: { send: null, get: null },
chat: { send: null, get: null },
name: { send: null, get: null },
href: { send: null, get: null },
init(){
frontend.plugin['trystero'] = this
$connections.webcam = $connections.webcam.concat([this])
$connections.chatnetwork = $connections.chatnetwork.concat([this])
$connections.scene = $connections.scene.concat([this])
2024-04-16 18:44:55 +02:00
this.selfId = selfId // selfId is a trystero global (unique per session)
2024-01-03 15:23:34 +01:00
this.reactToConnectionHrefs()
this.nickname = localStorage.getItem("nickname") || `human${String(Math.random()).substr(5,4)}`
2024-04-16 18:44:55 +02:00
this.names[ this.selfId ] = this.nickname
2024-01-03 15:23:34 +01:00
document.addEventListener('network.connect', (e) => this.connect(e.detail) )
document.addEventListener('network.init', () => {
let meeting = network.getMeetingFromUrl(document.location.href)
2024-01-29 21:19:04 +01:00
if( meeting.match(this.profile.protocol) ){
2024-01-03 15:23:34 +01:00
this.parseLink( meeting )
}
})
},
confirmConnected(){
if( !this.connected ){
this.connected = true
2024-01-29 21:19:04 +01:00
frontend.emit('network.connected',{plugin:this,username: this.nickname})
2024-01-03 15:23:34 +01:00
}
},
async connect(opts){
// embedded https://github.com/dmotz/trystero (trystero-torrent.min.js build)
2024-01-29 21:19:04 +01:00
if( opts.selectedWebcam == this.profile.name ) this.useWebcam = true
if( opts.selectedChatnetwork == this.profile.name ) this.useChat = true
if( opts.selectedScene == this.profile.name ) this.useScene = true
2024-01-03 15:23:34 +01:00
if( this.useWebcam || this.useChat || this.useScene ){
2024-01-29 21:19:04 +01:00
this.createLink() // ensure link
console.log("connecting "+this.profile.name)
2024-01-03 15:23:34 +01:00
console.log("trystero link: "+this.link)
this.room = joinRoom( {appId: 'xrfragment'}, this.link )
2024-01-29 21:19:04 +01:00
$chat.send({message:`Share the meeting link <a onclick="frontend.share()">by clicking here</a>`,class:['info']})
2024-01-03 15:23:34 +01:00
$chat.send({message:"waiting for other humans..",class:['info']})
// setup trystero events
const [sendPing, getPing] = this.room.makeAction('ping')
this.ping.send = sendPing
this.ping.get = getPing
const [sendName, getName] = this.room.makeAction('name')
this.name.send = sendName
this.name.get = getName
// start pinging
this.ping.pinger = setInterval( () => this.ping.send({ping:true}), 3000 )
this.ping.get((data,peerId) => this.confirmConnected() )
// listen for peers naming themselves
this.name.get((name, peerId) => {
this.confirmConnected()
this.names[peerId] = name
})
// send name to peers who join later
this.room.onPeerJoin( (peerId) => {
this.confirmConnected()
this.names[peerId] = name
this.name.send(this.nickname, peerId )
$chat.send({message:"a new human joined",class:['info']})
})
// delete name of people leaving
this.room.onPeerLeave( (peerId) => delete this.names[peerId] )
if( this.useWebcam ) this.initWebcam()
if( this.useChat ) this.initChat()
2024-01-29 21:19:04 +01:00
if( this.useScene ) this.initScene()
2024-01-03 15:23:34 +01:00
}
},
initChat(){
const [sendChat, getChat] = this.room.makeAction('chat')
this.chat.send = sendChat
this.chat.get = getChat
document.addEventListener('network.send', (e) => {
2024-01-29 21:19:04 +01:00
console.log("trystero")
2024-01-03 15:23:34 +01:00
this.chat.send({...e.detail, from: this.nickname, pos: network.pos }) // send to P2P network
})
// prime chatlog of other people joining
this.room.onPeerJoin( (peerId) => {
if( $chat.getChatLog().length > 0 ) this.chat.send({prime: $chat.getChatLog() }, peerId )
})
// listen for chatmsg
this.chat.get((data, peerId) => {
if( data.prime ){ // first prime is 'truth'
if( this.chat.primed || $chat.getChatLog().length > 0 ) return // only prime once
$chat.$messages.innerHTML += data.prime
$chat.$messages.scrollTop = $chat.$messages.scrollHeight // scroll down
this.chat.primed = true
}else $chat.send({ ...data}) // send to screen
})
},
async initWebcam(){
2024-01-29 21:19:04 +01:00
if( !$connections.$audioInput.value && !$connections.$videoInput.value ) return // nothing to do
2024-01-03 15:23:34 +01:00
// get a local audio stream from the microphone
this.selfStream = await navigator.mediaDevices.getUserMedia({
audio: $connections.$audioInput.value,
video: $connections.$videoInput.value
})
this.room.addStream(this.selfStream)
2024-01-29 21:19:04 +01:00
this.getVideo(this.selfId,{create:true,stream: this.selfStream})
2024-01-03 15:23:34 +01:00
// send stream + chatlog to peers who join later
this.room.onPeerJoin( (peerId) => this.room.addStream( this.selfStream, peerId))
this.room.onPeerStream((stream, peerId) => {
let video = this.getVideo(peerId,{create:true, stream})
this.videos[ this.names[peerId] || peerId ] = video
})
this.room.onPeerLeave( (peerId) => {
let video = this.getVideo(peerId)
if( video ){
video.remove()
delete this.videos[peerId]
}
})
},
initScene(){
// setup trystero events
const [sendHref, getHref] = this.room.makeAction('name')
this.href.send = sendHref
this.href.get = getHref
this.href.get((data,peerId) => {
xrf.hashbus.pub(data.href)
})
},
getVideo(peerId,opts){
opts = opts || {}
let video = this.videos[ this.names[peerId] ] || this.videos[ peerId ]
if (!video && opts.create) {
video = document.createElement('video')
video.autoplay = true
// add video element to the DOM
if( opts.stream ) video.srcObject = opts.stream
2024-01-29 21:19:04 +01:00
console.log("creating video for peerId "+this.selfId)
this.videos[ this.selfId ] = video
2024-01-03 15:23:34 +01:00
$chat.$videos.appendChild(video)
}
},
send(opts){ $chat.send({...opts, source: 'trystero'}) },
createLink(opts){
if( !this.link ){
const meeting = network.getMeetingFromUrl(document.location.href)
2024-01-29 21:19:04 +01:00
this.link = network.meetingLink = meeting.match("trystero://") ? meeting : `trystero://r/${network.randomRoom()}:bittorrent`
2024-01-03 15:23:34 +01:00
}
2024-04-16 18:44:55 +02:00
if( !xrf.navigator.URI.hash.meet ) xrf.navigator.URI.hash.meet = this.link
2024-01-03 15:23:34 +01:00
},
config(opts){
2024-01-29 21:19:04 +01:00
opts = {...opts, ...this.profile }
2024-01-03 15:23:34 +01:00
this.el = document.createElement('div')
this.el.innerHTML = this.html.generic(opts)
this.el.querySelector('#nickname').value = this.nickname
this.el.querySelector('#nickname').addEventListener('change', (e) => localStorage.setItem("nickname",e.target.value) )
// resolve ip
return this.el
},
2024-01-29 21:19:04 +01:00
info(opts){
window.notify(`${this.profile.name} is ${this.profile.description} <br>by using a serverless technology called <a href="https://webrtc.org/" target="_blank">webRTC</a> via <a href="${this.profile.url}" target="_blank">trystero</a>.<br>You can basically make up your own channelname or choose an existing one.<br>Use this for hasslefree anonymous meetings.`)
},
2024-01-03 15:23:34 +01:00
parseLink(url){
2024-01-29 21:19:04 +01:00
if( !url.match(this.profile.protocol) ) return
let parts = url.replace(this.profile.protocol,'').split("/")
2024-01-03 15:23:34 +01:00
if( parts[0] == 'r' ){ // this.room
let roomid = parts[1].replace(/:.*/,'')
let server = parts[1].replace(/.*:/,'')
if( server != 'bittorrent' ) return window.notify("only bittorrent is supported for trystero (for now) :/")
this.link = url
2024-01-29 21:19:04 +01:00
$connections.show({
chatnetwork:this.profile.name,
scene: this.profile.name,
webcam: this.profile.name
})
2024-01-03 15:23:34 +01:00
return true
}
return false
},
reactToConnectionHrefs(){
xrf.addEventListener('href', (opts) => {
let {mesh} = opts
if( !opts.click ) return
this.parseLink(mesh.userData.href)
let href = mesh.userData.href
let isLocal = href[0] == '#'
let isTeleport = href.match(/(pos=|http:)/)
if( isLocal && !isTeleport && this.href.send ) this.href.send({href})
})
2024-04-16 15:19:08 +02:00
let hashvars = xrf.URI.parse( document.location.hash ).XRF
2024-01-29 21:19:04 +01:00
if( hashvars.meet ) this.parseLink(hashvars.meet.string)
2024-01-03 15:23:34 +01:00
}
},
{
// auto-trigger events on changes
get(data,k,receiver){ return data[k] },
set(data,k,v){
let from = data[k]
data[k] = v
//switch( k ){
// default: elcene.dispatchEvent({type:`trystero.${k}.change`, from, to:v})
//}
}
})
document.addEventListener('$connections:ready', (e) => {
trystero(e.detail).init()
})
//window.meeting = window.meeting||{}
//window.meeting.trystero = async function(el,this){
//
// // embed https://github.com/dmotz/trystero (trystero-torrent.min.js build)
// const { joinRoom } = await import("./../../../dist/trystero-torrent.min.js");
// this.room = {
// this.room: null,
// link: null,
// selfId: null,
// names: {},
// chat: { send: null, get: null },
// name: { send: null, get: null },
// config: {appId: this.data.id }
// }
//
// this.sendName = null
//
// this.send = (opts) => com.send({...opts, source: 'trystero'})
//
// el.addEventListener('remove', () => {
// if( this.room.room ) this.room.room.leave()
// })
//
// el.addEventListener('connect', async () => {
// let this.room = this.room
//
// this.room.link = this.data.link
// if( !room.linkmatch(/(#|&)meet/) ){
// this.room.link = this.room.link.match(/#/) ? '&meet' : '#meet'
// }
// this.room.room = joinRoom( this.room.config, this.room.link )
// this.selfId = this.room.selfId
//
// this.send({
// message: "joined meeting at "+roomname.replace(/(#|&)meet/,''), // dont trigger init()
// classes: ["info"],
// sendNetwork:false
// })
//
// this.send({
// message:"copied meeting link to clipboard",
// classes: ["info"],
// sendNetwork:false
// })
//
// // setup trystero events
// const [sendName, getName] = this.room.makeAction('name')
// const [sendChat, getChat] = this.room.makeAction('chat')
// this.chat.send = sendChat
// this.chat.get = getChat
// this.name.send = sendName
// this.name.get = getName
//
// // tell other peers currently in the this.room our name
// this.names[ this.selfId ] = this.nickname.substr(0,15)
// this.name.send( this.nickname )
//
// // listen for peers naming themselves
// this.name.get((name, peerId) => (room.names[peerId] = name))
//
// // send self stream to peers currently in the this.room
// this.room.addStream(this.selfStream)
//
// // send stream + chatlog to peers who join later
// this.room.onPeerJoin( (peerId) => {
// this.room.addStream( this.selfStream, peerId)
// this.name.send( this.nickname, peerId)
// this.chat.send({prime: com.log}, peerId )
// })
//
// this.room.onPeerLeave( (peerId) => {
// console.log(`${room.names[peerId] || 'a visitor'} left`)
// if( com.videos[peerId] ){
// com.videos[peerId].remove()
// delete com.videos[peerId]
// }
// delete this.names[peerId]
// })
//
// // this.room streams from other peers
// this.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.)
// this.audios[peerId] = audio
// })
//
// this.room.onPeerStream((stream, peerId) => {
// com.createVideoElement(stream,peerId)
// })
//
// // listen for chatmsg
// this.chat.get((data, peerId) => {
// if( data.prime ){
// if( com.log.length > 0 ) return // only prime once
// console.log("receiving prime")
// data.prime.map( (l) => this.send({message:l, sendLocal:false ) ) // send log to screen
// this.chat.primed = true
// }
// this.send({ ...data, sendLocal: false}) // send to screen
// })
//
// }
//
//
//}
}).apply({})