1342 lines
29 KiB
JavaScript
1342 lines
29 KiB
JavaScript
|
//
|
||
|
// 2023.06.18
|
||
|
// support
|
||
|
// - chrome
|
||
|
// - edge
|
||
|
// - ff
|
||
|
// - safari
|
||
|
// ...
|
||
|
// mobile / pc (linux/iOS/macOS/windows/android)
|
||
|
// ...
|
||
|
// cross device/platform
|
||
|
//
|
||
|
//
|
||
|
// next:
|
||
|
// - db
|
||
|
// - save user name (kv)
|
||
|
// - use username in messaging.
|
||
|
//
|
||
|
// .............................
|
||
|
//
|
||
|
//
|
||
|
// next: select cam
|
||
|
// 'c' to toggle. upg: ui (in user icon) .. see your stats and pick settings there.
|
||
|
//
|
||
|
// upg: remember specific mute mike setting (even if switch video input.)
|
||
|
// .......
|
||
|
//
|
||
|
// https://github.com/diafygi/webcrypto-examples/#ecdsa
|
||
|
//
|
||
|
// next: add db, c, input box, etc
|
||
|
//
|
||
|
// upg: better linky type things
|
||
|
//
|
||
|
// next: video display mangagement. choose based on environemnt. (see (with demo) what css can do automatically)
|
||
|
// - goal to keep full bleed.
|
||
|
//
|
||
|
// next: handle switch between audio/video mode.
|
||
|
// - tap video to toggle video off
|
||
|
//
|
||
|
// upg: copy/paste media
|
||
|
//
|
||
|
//
|
||
|
// upg: prn -- better input/output selection (when click user icon)
|
||
|
//
|
||
|
// upg: chat (with hyper and media links)
|
||
|
//
|
||
|
// upg: upload
|
||
|
// - media, video, pics special treated?
|
||
|
//
|
||
|
// upg: display media (screenshare) prn
|
||
|
//
|
||
|
//
|
||
|
// upg: HTMLCanvasElement.captureStream().
|
||
|
// - make a shared whiteboard thing (allow pple to 'draw' on the whiteboard they see) prn.
|
||
|
//
|
||
|
// next: toggle mute audio
|
||
|
// - spacebar 'm'
|
||
|
// - click mic button
|
||
|
//
|
||
|
// upg: automatically activate mic if allowed (check permissions/try?) or? prn.
|
||
|
//
|
||
|
// upg: support voice call?
|
||
|
// - toggle off video if pic audio?
|
||
|
// - toggle again to switch? or?
|
||
|
// - or put line under mode <-- this. underline mic or cam .. upg sliding animation on toggle.
|
||
|
//
|
||
|
// hide speaker by default once audio works (only have mute page option if user sets that as option in ui)
|
||
|
//
|
||
|
// hack reload the page every 5 seconds if no peers? (and no streams yet)
|
||
|
// > upg: review source to see why this might be.
|
||
|
//
|
||
|
// toggle cam switches off cam. (remove underline)
|
||
|
//
|
||
|
// toggle mic mutes audio out. (video or audio mode) .. have to turn off video mode to swich to audio only mode.
|
||
|
//
|
||
|
// click on cam when on mic mode .. turns off mic and turns on cam
|
||
|
// (upg: can/should we have different tracks, a video track and an audio track, yes that would be eaier/better if in sync?) .. but use the same ui.
|
||
|
//
|
||
|
// upg: tap to toggle peer view.
|
||
|
// - time on call, video or audio on. and/or
|
||
|
//
|
||
|
// upg: opt: integrate with nostr (share addr?)
|
||
|
// > with ai readable text messages?
|
||
|
//
|
||
|
// upg: add version number in uio.
|
||
|
//
|
||
|
// upg: display change on peers.
|
||
|
//
|
||
|
// upg: allow u=bob to suggest a username (but the user has to accept it)
|
||
|
|
||
|
import randStr from './lib/random-string.js'
|
||
|
//import NoSleep from './lib/nosleep.js' // try inlcuding in main or minifted version?
|
||
|
|
||
|
import {trystero,idb} from './lib/bundle.js'
|
||
|
|
||
|
const {openDB} = idb
|
||
|
|
||
|
const {joinRoom} = trystero
|
||
|
|
||
|
console.log(openDB)
|
||
|
|
||
|
let VER = '[5]'
|
||
|
console.log('VERSION 2023.06.20 #1945 '+VER)
|
||
|
|
||
|
const url = new URL(location.href)
|
||
|
const {searchParams} = url
|
||
|
let searchU = searchParams.get('u')
|
||
|
|
||
|
document.querySelector('.version').textContent = VER
|
||
|
|
||
|
let roomTag = location.hash
|
||
|
if(!roomTag){
|
||
|
roomTag = (await randStr(6)).toLowerCase()
|
||
|
location.hash = roomTag
|
||
|
roomTag = location.hash
|
||
|
}
|
||
|
|
||
|
console.log('roomTag',{roomTag})
|
||
|
|
||
|
//
|
||
|
// upg: api interface.
|
||
|
//
|
||
|
let tableName = n=>n
|
||
|
|
||
|
const db = await openDB('main',1,{upgrade:(...args)=>{
|
||
|
const [db,oldVersion,newVersion,transaction,e] = args
|
||
|
console.log('upgrade',{oldVersion,newVersion})
|
||
|
|
||
|
let t = tableName
|
||
|
|
||
|
let s
|
||
|
s = db.createObjectStore(t('kv'),{keyPath:'key',unique:true}) // {key:'bob',value:'12345'}
|
||
|
|
||
|
// s.createIndex('unitId','unitId')
|
||
|
}})
|
||
|
|
||
|
const kv = {
|
||
|
t:tableName,
|
||
|
get : async function(k){
|
||
|
const {t} = this
|
||
|
const r = await db.get(t('kv'),k)
|
||
|
return r?.value
|
||
|
},
|
||
|
|
||
|
set : async function(k,v){
|
||
|
const {t} = this
|
||
|
let m = {key:k,value:v}
|
||
|
const r = await db.put(t('kv'),m)
|
||
|
return r
|
||
|
}
|
||
|
}//kv
|
||
|
|
||
|
if(searchU){
|
||
|
await kv.set('username',searchU)
|
||
|
}
|
||
|
let username = await kv.get('username')
|
||
|
|
||
|
console.log('username',username)
|
||
|
|
||
|
|
||
|
const ce = (...args)=>{
|
||
|
const [type,value] = args
|
||
|
let d = cc(...args)
|
||
|
if(value) // upg: if str
|
||
|
d.textContent = value
|
||
|
|
||
|
return d
|
||
|
}//func
|
||
|
|
||
|
const dB = document.body
|
||
|
let q = n=>dB.querySelector(n)
|
||
|
let cc = n=>document.createElement(n||'div')
|
||
|
let ca = (a,b)=>a.appendChild(b)
|
||
|
const dM = q('main')
|
||
|
const dL = q('.log')
|
||
|
const dD = q('main.display')
|
||
|
const dPc = q('.peer-count')
|
||
|
const dPre = q('.preview')
|
||
|
const dI = q('footer .input input')
|
||
|
const dTl = q('.display .text-log')
|
||
|
|
||
|
// upg: support #spaces
|
||
|
//
|
||
|
// upg: lib compliler
|
||
|
//
|
||
|
// upg: notificaton option (offline and online)
|
||
|
//
|
||
|
// upg: bring in libraries (for working with data etc.)
|
||
|
|
||
|
const {subtle} = window.crypto
|
||
|
|
||
|
const genSignKey = async n=>{
|
||
|
|
||
|
let k = await subtle.generateKey({
|
||
|
name: "ECDSA",
|
||
|
namedCurve: "P-384"
|
||
|
},
|
||
|
true, //whether the key is extractable (i.e. can be used in exportKey)
|
||
|
["sign", "verify"] //can be any combination of "sign" and "verify"
|
||
|
)
|
||
|
|
||
|
const jPk = await subtle.exportKey('jwk',k.publicKey)
|
||
|
const jSk = await subtle.exportKey('jwk',k.privateKey)
|
||
|
|
||
|
return {key:k,jPk,jSk}
|
||
|
}//
|
||
|
|
||
|
let sigKey = await genSignKey()
|
||
|
console.log({sigKey})
|
||
|
|
||
|
// ----------------------------------
|
||
|
onhashchange = (e) => {
|
||
|
const {oldURL,newURL} = e
|
||
|
// upg: join nrew room and update display (and/or?) // nice if keep old room open till all clear sending (using room class abstraction)
|
||
|
setTimeout(n=>location.reload(),150) //upg: ask to reload or?
|
||
|
}
|
||
|
|
||
|
|
||
|
const room = joinRoom({appId:'--linky-rendezvous'},'alpha-'+roomTag)
|
||
|
|
||
|
|
||
|
// ----------------------
|
||
|
//
|
||
|
const [send,recv] = room.makeAction('message')
|
||
|
|
||
|
//upg: use sync ping/query.
|
||
|
recv((d,p)=>{
|
||
|
console.log('recived data from peer',p,d)
|
||
|
const {kind} = d|| {}
|
||
|
if(kind == 'message'){
|
||
|
d.from = 'them'
|
||
|
logMessage(d)
|
||
|
}
|
||
|
})
|
||
|
|
||
|
|
||
|
let peers = {}
|
||
|
|
||
|
room.onPeerJoin(p=>{
|
||
|
console.log('peer join',p)
|
||
|
// upg: update() // to set has-peer in body (with peer-join) only if 5 seconds peer.
|
||
|
//dB.classList.add('peer-join')// upg: timer to turn off peer join after 5 seconds.
|
||
|
//
|
||
|
// upg: do peer
|
||
|
|
||
|
// upg: put peer in a wait list if we've not got the stream yet...
|
||
|
if(stream){
|
||
|
console.log('adding existing stream to for new peer',p)
|
||
|
const {s,m} = stream
|
||
|
room.addStream(s,p,m)
|
||
|
}
|
||
|
|
||
|
let ping = async n=>{
|
||
|
let r = await room.ping(p)
|
||
|
console.log('ping update',p,r+'ms')
|
||
|
peers[p]._ping = setTimeout(ping,5000)
|
||
|
}
|
||
|
|
||
|
|
||
|
peers[p] = {
|
||
|
pid:p,
|
||
|
udate:Date.now()/1000,
|
||
|
ping
|
||
|
}
|
||
|
|
||
|
ping()
|
||
|
|
||
|
//upg: animate peer icon (flash)
|
||
|
update()
|
||
|
})
|
||
|
|
||
|
const removeMedia = n=>{
|
||
|
//upg: stop media first? prn.
|
||
|
dD.removeChild(n)
|
||
|
}
|
||
|
|
||
|
|
||
|
room.onPeerLeave(p=>{
|
||
|
console.log('peer leave',p,peerMedia)
|
||
|
dB.classList.remove('peer-join') //and/or? use has-peers set from update()
|
||
|
|
||
|
|
||
|
let l = []
|
||
|
peerMedia.forEach(v=>{ // or? (use index based lookup?) though there can be multiple.
|
||
|
console.log('review',v)
|
||
|
const {dom,meta,container} = v
|
||
|
if(p == v.p){
|
||
|
removeMedia(container)
|
||
|
}
|
||
|
else
|
||
|
l.push(v)
|
||
|
})
|
||
|
|
||
|
peerMedia = l // restore
|
||
|
|
||
|
clearTimeout(peers[p]._ping) // NOTE CONNECTED --- upg: call peer.done()
|
||
|
delete peers[p]
|
||
|
|
||
|
update()
|
||
|
})
|
||
|
|
||
|
|
||
|
//let _toAdd = []
|
||
|
let peerMedia = []
|
||
|
room.onPeerStream((s,p,m)=>{ // upg on peer track?
|
||
|
// s = stream
|
||
|
// p = peer
|
||
|
// m = metadata
|
||
|
|
||
|
// and/or detect media?
|
||
|
//
|
||
|
let {kind} = m||{}
|
||
|
|
||
|
console.log('peer stream: add stream',{kind},{s,p,m})
|
||
|
|
||
|
;{
|
||
|
// https://developer.mozilla.org/en-US/docs/Web/API/OfflineAudioContext
|
||
|
// https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API/Using_Web_Audio_API
|
||
|
// https://web.dev/webaudio-intro/
|
||
|
// https://developer.mozilla.org/en-US/docs/Web/API/GainNode/GainNode
|
||
|
// https://developer.mozilla.org/en-US/docs/Web/API/AnalyserNode/AnalyserNode
|
||
|
// https://developer.mozilla.org/en-US/docs/Web/API/AudioContext/createMediaElementSource
|
||
|
// https://developer.mozilla.org/en-US/docs/Web/API/AudioContext/createMediaStreamSource
|
||
|
// https://developer.mozilla.org/en-US/docs/Web/API/AnalyserNode/getFloatTimeDomainData
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
//
|
||
|
// upg: send what we're getting back to them?
|
||
|
// upg: process each input stream and display value
|
||
|
//
|
||
|
// > mainly for speaker detection for main view.
|
||
|
// - toggle views?
|
||
|
// - default full edge speaker view video with overlays.
|
||
|
// - second is full edge titled video.
|
||
|
// - and?
|
||
|
//
|
||
|
// (note will rairly be more than 3?)
|
||
|
//
|
||
|
//
|
||
|
// - might have screensharing too (it takes priorty) with pip speaker.
|
||
|
//
|
||
|
// what do we need to see? localy, what helps? prn.
|
||
|
// - just that we've got the stream and/or?
|
||
|
//
|
||
|
//
|
||
|
// >> WE PROB WANT TO DO THIS REMOTE SIDE AND SEND THE RESULT TO PEER
|
||
|
// - this will help early jump speaker selection.'
|
||
|
//
|
||
|
try{
|
||
|
// also send telmetry from the user (then we'll have details)
|
||
|
// - send on/off events.
|
||
|
// upg: make this a monitoring lib class with events
|
||
|
const ac = new AudioContext()
|
||
|
// const sampleRate = 44100
|
||
|
//const ac = new OfflineAudioContext(//new AudioContext() // see.
|
||
|
// {
|
||
|
// numberOfChannesl:2,
|
||
|
// length: sampleRate*40,
|
||
|
// sampleRate
|
||
|
// })
|
||
|
//console.log({ac})
|
||
|
//const options = {
|
||
|
// mediaStream: s
|
||
|
// }
|
||
|
const source = ac.createMediaStreamSource(s)
|
||
|
//console.log('remote SOURCE',source)
|
||
|
|
||
|
const gain = new GainNode(ac,{gain:0})
|
||
|
const meter = new AnalyserNode(ac) //upg: lower grained monitory.
|
||
|
|
||
|
const data = new Float32Array(meter.frequencyBinCount)
|
||
|
const byteData = new Uint8Array(meter.frequencyBinCount)
|
||
|
|
||
|
source.connect(meter).connect(gain).connect(ac.destination)
|
||
|
|
||
|
// https://developer.mozilla.org/en-US/docs/Web/API/AnalyserNode/getByteFrequencyData
|
||
|
|
||
|
// upg: monitor the common (long time) high average for baseline to detect speech.
|
||
|
// - upg: use time domain to detect?
|
||
|
// - upg: libraries to detect speech
|
||
|
// upg: detect actual speech client side (using local detection) .. and send that also data telem to peers. .. eg and/or -- https://github.com/solyarisoftware/webad
|
||
|
setInterval(n=>{
|
||
|
return
|
||
|
//upg: put in groups and count groups and/or algo or?
|
||
|
meter.getFloatFrequencyData(data)
|
||
|
meter.getByteFrequencyData(byteData) //-0 ~ -165 etc. -0 is loud. -165 is soft.
|
||
|
//console.log(data)
|
||
|
//let x = byteData.join(', ')
|
||
|
//dB.querySelector('.debug').textContent = x
|
||
|
let a = 0
|
||
|
let {length} = data
|
||
|
let g = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
|
||
|
for(let i=0;i<length;i++){
|
||
|
let v = data[i]
|
||
|
let vv = Math.abs(v)
|
||
|
let vvv = Math.round(vv)
|
||
|
let gi = vvv%g.length
|
||
|
g[gi]++
|
||
|
a+=vv
|
||
|
if(i<3){
|
||
|
//console.log({v,vv,vvv,gi})
|
||
|
}
|
||
|
}
|
||
|
let aa = a/length
|
||
|
//console.log(g)
|
||
|
|
||
|
//dB.querySelector('.debug').textContent = aa
|
||
|
},3333)
|
||
|
|
||
|
|
||
|
}
|
||
|
catch(e){
|
||
|
//upg: cache till user gesture. (how to hook, hook our own and/or callfunction
|
||
|
console.log(e)
|
||
|
}
|
||
|
|
||
|
};
|
||
|
|
||
|
|
||
|
s.addEventListener('removetrack',e=>{
|
||
|
console.log('stream remove track',e)
|
||
|
//upg: smarter
|
||
|
let lt = s.getTracks()
|
||
|
if(lt.length == 0){
|
||
|
console.log('remove media display',{s,p,m,dc})
|
||
|
try{
|
||
|
dD.removeChild(dc)
|
||
|
}
|
||
|
catch(e){
|
||
|
console.log(e)
|
||
|
}
|
||
|
}
|
||
|
})
|
||
|
|
||
|
|
||
|
s.addEventListener('addtrack',e=>{
|
||
|
console.log('stream add track',e)
|
||
|
})
|
||
|
|
||
|
|
||
|
//upg: how to detect if video?
|
||
|
// -- upg: ignore if unknown kind
|
||
|
|
||
|
const d = document.createElement(kind)
|
||
|
d.srcObject = s
|
||
|
d.muted = !interacted // so video can play still.
|
||
|
d.autoplay = true
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
const onPlaying = e=>{
|
||
|
// try to unmute (video can play without audio)
|
||
|
console.log('onplaying peer stream')
|
||
|
d.removeEventListener('playing',onPlaying)
|
||
|
startWakeLock()
|
||
|
|
||
|
//setTimeout(n=>{
|
||
|
// console.log('trying umute.')
|
||
|
// d.muted = false //try to unmute .. this might pause things?
|
||
|
// },3500)
|
||
|
}//func
|
||
|
|
||
|
d.addEventListener('playing',onPlaying)
|
||
|
|
||
|
|
||
|
//setTimeout(n=>{
|
||
|
//try to play: upg: keep trying or? //upg: check global wantsMute if create that.
|
||
|
// if(interacted) // upg: or just try to unmute anyway? .. donno how long need to wait since autoplay
|
||
|
// d.muted = false
|
||
|
// },150)
|
||
|
|
||
|
|
||
|
let dc = d
|
||
|
|
||
|
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/video
|
||
|
//
|
||
|
if(kind == 'video'){
|
||
|
//d.playsinline = true
|
||
|
d.setAttribute('playsinline','') // or = true ok?
|
||
|
dc = makeVideo(d)
|
||
|
}
|
||
|
|
||
|
dD.appendChild(dc)
|
||
|
|
||
|
peerMedia.push({p,dom:d,container:dc,meta:m}) //type:'audio'
|
||
|
//d.play()
|
||
|
|
||
|
update()
|
||
|
})
|
||
|
|
||
|
//const flushInteractions(){}
|
||
|
|
||
|
// upg: a generic class to do this.
|
||
|
// upg: fix this?
|
||
|
let hasPeersState = {
|
||
|
value: false,
|
||
|
limit:1,
|
||
|
ago:0,
|
||
|
try:function(...args){
|
||
|
const [v,cb,x] = args
|
||
|
const now = performance.now()/1000
|
||
|
console.log('this try',v,{x},this)
|
||
|
if(this.value !=v){
|
||
|
const delta = (now-this.ago)
|
||
|
console.log({delta},this.ago)
|
||
|
if(delta >= this.limit){ // = roughly
|
||
|
console.log('ok to change',this.value)
|
||
|
cb(this.value)
|
||
|
this.ago = now
|
||
|
}//if
|
||
|
else{
|
||
|
let next = ((this.ago+this.limit)-now)*1000 // +?
|
||
|
console.log(next)
|
||
|
clearTimeout(this._timeout)
|
||
|
this._timeout = setTimeout(n=>this.try(...args,'retry'),next)
|
||
|
}
|
||
|
}//if
|
||
|
}//fn
|
||
|
}//obj
|
||
|
|
||
|
let _update = 0
|
||
|
const update = n=>{
|
||
|
_update++
|
||
|
|
||
|
let now = performance.now()/1000
|
||
|
|
||
|
const l = room.getPeers()
|
||
|
console.log(l)
|
||
|
dPc.textContent = l.length+1 // +1 to count self.
|
||
|
|
||
|
let peerCount = l.length
|
||
|
|
||
|
let audioLive = false
|
||
|
let videoLive = false
|
||
|
|
||
|
if(stream){
|
||
|
const {s} = stream
|
||
|
const tl = s.getTracks()
|
||
|
console.log('update',{tl})
|
||
|
tl.forEach(v=>{
|
||
|
let {kind,enabled} = v
|
||
|
if(enabled && kind =='audio')
|
||
|
audioLive = true
|
||
|
|
||
|
if(enabled && kind =='video')
|
||
|
videoLive = true
|
||
|
})
|
||
|
}
|
||
|
|
||
|
dB.classList[audioLive?'add':'remove']('audio-is-live')
|
||
|
dB.classList[videoLive?'add':'remove']('video-is-live')
|
||
|
|
||
|
//upg: time trigger this (so only changes once per second)
|
||
|
let hasPeers = (peerCount>0)
|
||
|
hasPeersState.try(hasPeers,n=>{
|
||
|
console.log('has peers',n)
|
||
|
dB.classList[hasPeers?'add':'remove']('has-peers')
|
||
|
})
|
||
|
|
||
|
|
||
|
let hasPeerVideo = false // hack; upg: and/or ../ upg where delete peer video?
|
||
|
;(peerMedia||[]).forEach(v=>{
|
||
|
const {meta} = v
|
||
|
const {kind} = meta
|
||
|
console.log('checking peer media',v,{meta,kind})
|
||
|
if(kind == 'video')
|
||
|
hasPeerVideo = true
|
||
|
})
|
||
|
|
||
|
|
||
|
dB.classList[hasPeerVideo?'add':'remove']('has-peer-video') // and/or:
|
||
|
|
||
|
|
||
|
// and or this shouldn't be in update or why not here? (so long as don't quick move)
|
||
|
let state = {
|
||
|
audioLive,videoLive,peerCount,interacted,hasPeerVideo
|
||
|
}
|
||
|
//upg: send available media etc?
|
||
|
//upg: toggle button to call about
|
||
|
//
|
||
|
//upg: comfort level based sharing with who etc.
|
||
|
// - close friends share (cached?) about mee.
|
||
|
|
||
|
send({kind:'update',layer:_update,state})
|
||
|
}
|
||
|
|
||
|
//paint = intial create once
|
||
|
|
||
|
const makeVideo = n=>{
|
||
|
|
||
|
const d = document.createElement('div')
|
||
|
d.classList.add('video-wrapper')
|
||
|
d.appendChild(n)
|
||
|
return d
|
||
|
}
|
||
|
|
||
|
|
||
|
let stream = false
|
||
|
const addStream = async (s,m)=>{
|
||
|
//
|
||
|
// add stream to peers (and remember for other peers)
|
||
|
//
|
||
|
console.log('add stream',s)
|
||
|
//upg: switch to video audio if pick that, or seperate streams?
|
||
|
|
||
|
stream = {s,m}// and/or?
|
||
|
room.addStream(s,null,m) // and/or?
|
||
|
console.log('stream added for peers',s,m)
|
||
|
}//func
|
||
|
|
||
|
// -----------------------------
|
||
|
const toggleAudio = n=>{
|
||
|
if(stream){
|
||
|
let {s} = stream
|
||
|
console.log('is video, toggle audio')
|
||
|
const tl = s.getAudioTracks()
|
||
|
console.log("audo tracks",tl)
|
||
|
let vl = []
|
||
|
|
||
|
tl.forEach(v=>{
|
||
|
v.enabled = !v.enabled
|
||
|
vl.push(v.enabled)
|
||
|
})
|
||
|
|
||
|
console.log('new state',vl)
|
||
|
|
||
|
update()
|
||
|
}//if
|
||
|
}//func
|
||
|
|
||
|
let _toggleMic = false
|
||
|
const toggleMic = async n=>{
|
||
|
if(!_toggleMic){
|
||
|
console.log('toggle mic')
|
||
|
|
||
|
if(!stream){
|
||
|
//upg: switch to video audio if pick that, or seperate streams?
|
||
|
|
||
|
const {mediaDevices:m} = navigator
|
||
|
|
||
|
|
||
|
const l = await m.enumerateDevices()
|
||
|
console.log({l})
|
||
|
|
||
|
const s = await m.getUserMedia({video:false,audio:true})
|
||
|
|
||
|
addStream(s,{kind:'audio'})
|
||
|
|
||
|
dB.classList.add('mic-on')
|
||
|
}
|
||
|
else {
|
||
|
console.log('has stream',stream)
|
||
|
const {s,m} = stream
|
||
|
const {kind} = m
|
||
|
const isVideo = (kind == 'video')
|
||
|
const isAudio = (kind == 'audio')
|
||
|
|
||
|
if(isAudio){
|
||
|
console.log('stop audio',s)
|
||
|
|
||
|
//upg: use the same stream but add/remove tracks? or?
|
||
|
//room.removeStream(s)
|
||
|
const cl = s.getTracks()
|
||
|
console.log({cl})
|
||
|
cl.forEach(v=>{
|
||
|
console.log({v})
|
||
|
room.removeTrack(v,s)
|
||
|
stream.m.kind = 'empty'
|
||
|
})
|
||
|
|
||
|
stream = false
|
||
|
}
|
||
|
else {
|
||
|
toggleAudio()
|
||
|
}
|
||
|
}//else
|
||
|
|
||
|
update()
|
||
|
_toggleMic = false
|
||
|
}//if
|
||
|
}//func
|
||
|
|
||
|
const switchCam = async n=>{
|
||
|
|
||
|
// upg: debounce
|
||
|
//
|
||
|
// upg: review
|
||
|
//
|
||
|
// upg: alpha sort deviceId (or does it always report in the same order every query?)
|
||
|
|
||
|
if(stream){
|
||
|
console.log('switchCam')
|
||
|
|
||
|
const {s} = stream
|
||
|
|
||
|
const a = await aboutStream(s)
|
||
|
console.log({a})
|
||
|
|
||
|
let f = a.find(v=>v.kind=='video')
|
||
|
console.log({f})
|
||
|
// upg: what if more than one video? does that happen when we're screen sharing?
|
||
|
|
||
|
if(f){
|
||
|
const {mediaDevices:m} = navigator
|
||
|
const l = await m.enumerateDevices()
|
||
|
console.log({l})
|
||
|
|
||
|
//
|
||
|
// https://developer.mozilla.org/en-US/docs/Web/API/MediaDeviceInfo
|
||
|
|
||
|
let ff = l.find(v=>v.deviceId == f.deviceId)
|
||
|
console.log({ff})
|
||
|
|
||
|
let i = l.indexOf(ff)
|
||
|
console.log({i})
|
||
|
|
||
|
let next = false
|
||
|
|
||
|
l.forEach((v,ii)=>{
|
||
|
const {deviceId,kind,label,groupId} = v
|
||
|
//groupId = same physical device
|
||
|
//kind = "videoinput", "audioinput" or "audiooutput"
|
||
|
//label = "External USB Webcam c201"
|
||
|
|
||
|
if(kind == 'videoinput' && ii > i){
|
||
|
console.log('found!',label,deviceId)
|
||
|
next = {deviceId,device:v}
|
||
|
}
|
||
|
})
|
||
|
|
||
|
if(!next){
|
||
|
const fff = l.find(v=>v.kind=='videoinput')
|
||
|
console.log({fff,f})
|
||
|
if(fff && fff.deviceId != f.deviceId){
|
||
|
next = fff
|
||
|
}
|
||
|
}//if
|
||
|
|
||
|
console.log('switch to',next)
|
||
|
}//if
|
||
|
}//if
|
||
|
else
|
||
|
console.log('no stream',stream)
|
||
|
}//func
|
||
|
|
||
|
|
||
|
const aboutStream = s=>{
|
||
|
|
||
|
let a = []
|
||
|
|
||
|
if(s){
|
||
|
const {mediaDevices:m} = navigator
|
||
|
|
||
|
//
|
||
|
// https://developer.mozilla.org/en-US/docs/Web/API/MediaStreamTrack
|
||
|
const tl = s.getTracks()
|
||
|
console.log({tl})
|
||
|
|
||
|
// upg: send device code etc to peers?
|
||
|
|
||
|
//
|
||
|
// https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackSettings
|
||
|
tl.forEach((n,i)=>{
|
||
|
const {contentHint,kind,label,muted,readyState} = n
|
||
|
const s = n.getSettings()
|
||
|
const c = n.getConstraints()
|
||
|
const { deviceId,groupId, ///all
|
||
|
|
||
|
//some
|
||
|
aspectRatio,
|
||
|
cursor,
|
||
|
displaySurface,
|
||
|
frameRate,
|
||
|
height,
|
||
|
resizeMode,
|
||
|
width
|
||
|
} = s
|
||
|
console.log({s,c})
|
||
|
console.log(i+1,{deviceId,kind,label,contentHint})
|
||
|
|
||
|
const aa = {
|
||
|
contentHint,kind,label,muted,readyState,deviceId,groupId,
|
||
|
|
||
|
aspectRatio,cursor,displaySurface,frameRate,height,width,resizeMode,
|
||
|
|
||
|
track:n
|
||
|
}
|
||
|
|
||
|
a.push(aa)
|
||
|
})
|
||
|
}//if
|
||
|
|
||
|
return a
|
||
|
}//func
|
||
|
|
||
|
let _toggleCam = false
|
||
|
const toggleCam = async n=>{
|
||
|
if(!_toggleCam){
|
||
|
|
||
|
let dd // display to append
|
||
|
|
||
|
if(stream){
|
||
|
console.log('has stream',stream)
|
||
|
const {s,m} = stream
|
||
|
const {kind} = m
|
||
|
const isVideo = (kind == 'video')
|
||
|
const isAudio = (kind == 'audio')
|
||
|
|
||
|
if(isVideo){
|
||
|
console.log('stop video',s)
|
||
|
|
||
|
//upg: use the same stream but add/remove tracks? or?
|
||
|
//room.removeStream(s)
|
||
|
const cl = s.getTracks()
|
||
|
console.log({cl})
|
||
|
cl.forEach(v=>{
|
||
|
console.log({v})
|
||
|
room.removeTrack(v,s)
|
||
|
stream.m.kind = 'empty'
|
||
|
})
|
||
|
|
||
|
//// -- remove preview
|
||
|
dPre.innerHTML = '' // or?
|
||
|
|
||
|
stream = false
|
||
|
}
|
||
|
}
|
||
|
else {
|
||
|
|
||
|
|
||
|
//upg: show ui progress (cam might take a bit.)
|
||
|
// - pulse animation while is-enabling-cam.
|
||
|
|
||
|
_toggleCam = true
|
||
|
console.log('toggle cam')
|
||
|
//upg: switch to video audio if pick that, or seperate streams?
|
||
|
|
||
|
const {mediaDevices:m} = navigator
|
||
|
|
||
|
const l = await m.enumerateDevices()
|
||
|
console.log({l})
|
||
|
//upg : remember
|
||
|
|
||
|
const audio = true // upg: and/or?
|
||
|
const video = true
|
||
|
const media = {video,audio}
|
||
|
const s = await m.getUserMedia(media) // does it work if we send mic/cam sperately? in sync?
|
||
|
|
||
|
//
|
||
|
// https://developer.mozilla.org/en-US/docs/Web/API/MediaStreamTrack
|
||
|
const tl = s.getTracks()
|
||
|
console.log({tl})
|
||
|
|
||
|
// upg: send device code etc to peers?
|
||
|
|
||
|
//
|
||
|
// https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackSettings
|
||
|
tl.forEach((n,i)=>{
|
||
|
const {contentHint,kind,label,muted,readyState} = n
|
||
|
const s = n.getSettings()
|
||
|
const c = n.getConstraints()
|
||
|
const {deviceId,groupId} = s
|
||
|
console.log({s,c})
|
||
|
console.log(i+1,{deviceId,kind,label,contentHint})
|
||
|
})
|
||
|
|
||
|
//
|
||
|
// https://developer.mozilla.org/en-US/docs/Web/API/MediaStream
|
||
|
console.log('got media device stream',s)
|
||
|
|
||
|
|
||
|
|
||
|
addStream(s,{kind:'video',media})
|
||
|
|
||
|
// preview
|
||
|
const d = document.createElement('video')
|
||
|
//d.playsinline = true
|
||
|
d.setAttribute('playsinline','') // or = true ok?
|
||
|
dd = makeVideo(d)
|
||
|
d.muted = true // or?
|
||
|
d.autoplay = true
|
||
|
d.srcObject = s
|
||
|
//d.play()
|
||
|
|
||
|
dPre.innerHTML = ''
|
||
|
dPre.appendChild(dd)
|
||
|
|
||
|
dB.classList.add('cam-on')
|
||
|
}//else
|
||
|
|
||
|
update()
|
||
|
_toggleCam = false
|
||
|
}//if
|
||
|
}//func
|
||
|
|
||
|
const addDisplay = async n=>{
|
||
|
console.log('add display')
|
||
|
const {mediaDevices:m} = navigator
|
||
|
|
||
|
// https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getDisplayMedia
|
||
|
|
||
|
const audio = false // upg: and/or?
|
||
|
const video = true
|
||
|
const media = {video,audio}
|
||
|
const s = await m.getDisplayMedia(media) // does it work if we send mic/cam sperately? in sync?
|
||
|
|
||
|
const a = await aboutStream(s)
|
||
|
console.log('display media',s,{a})
|
||
|
|
||
|
addStream(s,{kind:'video',media,displayMedia:true}) //or
|
||
|
|
||
|
// upg: how to display that you're sharing screen? (tile previews?)
|
||
|
}
|
||
|
|
||
|
|
||
|
dB.querySelector('.mic.button').onclick = e=>{
|
||
|
toggleMic()
|
||
|
}
|
||
|
|
||
|
|
||
|
dB.querySelector('.cam.button').onclick = e=>{
|
||
|
toggleCam()
|
||
|
}
|
||
|
|
||
|
|
||
|
dB.querySelector('.peers.button').onclick = e=>{
|
||
|
//hack
|
||
|
switchCam()
|
||
|
}
|
||
|
|
||
|
|
||
|
//
|
||
|
// https://developer.mozilla.org/en-US/docs/Web/API/Screen_Wake_Lock_API
|
||
|
//
|
||
|
// https://github.com/richtr/NoSleep.js
|
||
|
//
|
||
|
// upg: turn wakelock on when there's a stream, (incomming or out going?) at least for outgoing video prn
|
||
|
//
|
||
|
let wakeLock = null
|
||
|
const startWakeLock = async n=>{
|
||
|
//
|
||
|
// upg
|
||
|
// noSleep = new NoSleep()
|
||
|
// noSleep.enable() // on user interaction (so can play video)
|
||
|
// https://github.com/richtr/NoSleep.js
|
||
|
//
|
||
|
if('wakeLock' in navigator){
|
||
|
try{
|
||
|
wakeLock = await navigator.wakeLock.request('screen')
|
||
|
console.log('requested wakelock',wakeLock)
|
||
|
// upg: await awakeLock.release() -- when not sending stream
|
||
|
dB.classList.add('wakelock-active')
|
||
|
}
|
||
|
catch(e){
|
||
|
console.log(e,e,name,e.message)
|
||
|
}
|
||
|
}
|
||
|
else {
|
||
|
console.log('Wakelock not supported.')
|
||
|
// upg: move this indicator settings section (don't need to know in some envs?)
|
||
|
dB.classList.add('wakelock-not-supported')
|
||
|
}
|
||
|
}
|
||
|
|
||
|
document.addEventListener('release',e=>{
|
||
|
console.log('wake lock released')
|
||
|
dB.classList.remove('wakelock-active')
|
||
|
})
|
||
|
|
||
|
document.addEventListener('visibilitychange',async e=>{
|
||
|
const {visibilityState:v} = document // hidden, visible
|
||
|
console.log('new visibility',v,e)
|
||
|
// >> upg: send visibility state (per user's affiliation comfortabliiity)
|
||
|
if(v === 'visible'){
|
||
|
await startWakeLock()
|
||
|
}//if
|
||
|
})
|
||
|
|
||
|
dB.querySelector('.reload-button').addEventListener('click',e=>{
|
||
|
location.reload()
|
||
|
})
|
||
|
|
||
|
let interacted = false
|
||
|
const interactionEvent = e=>{
|
||
|
if(!interacted){
|
||
|
interacted = true
|
||
|
console.log('interacted!')
|
||
|
dB.removeEventListener('click',interactionEvent)
|
||
|
dB.classList.add('has-interacted') // upg: more complated than that for intential muting (set a default is_muted = true state. upg: remember it?
|
||
|
|
||
|
peerMedia.forEach(v=>{
|
||
|
console.log('need play?',v)
|
||
|
const {p,dom,meta} = v
|
||
|
console.log({p,dom,meta})
|
||
|
dom.muted = false
|
||
|
dom.play() // and/or? // if audio?
|
||
|
})
|
||
|
|
||
|
startWakeLock()
|
||
|
}//if
|
||
|
}//funt
|
||
|
|
||
|
dB.addEventListener('click',interactionEvent)
|
||
|
|
||
|
const goAudio = n=>{
|
||
|
//mute/unmute or start audio stream
|
||
|
if(stream)
|
||
|
toggleAudio()
|
||
|
else
|
||
|
toggleMic()
|
||
|
}
|
||
|
|
||
|
|
||
|
// upg: use db and sync method for catchup.
|
||
|
|
||
|
const logMessage = n=>{
|
||
|
let {value:v,from,name} = n || {}
|
||
|
|
||
|
username = username || ''
|
||
|
|
||
|
console.log('log messge',{n})
|
||
|
const d = ce()
|
||
|
|
||
|
const fromMe =(from=='me')
|
||
|
|
||
|
d.classList.add('item',fromMe?'from-me':'from-them')
|
||
|
|
||
|
let dA = ce() // upg: ce('text content' or something to appendChild)
|
||
|
dA.textContent = name
|
||
|
dA.classList.add('meta')
|
||
|
d.appendChild(dA)
|
||
|
|
||
|
//let dB = ce('div',(fromMe?">> ":'')+v)
|
||
|
|
||
|
let dB = ce('div',v) //{c:'abc'} ?? -c jfiwoefij -ce -fji jf) .. command line style and/or luange style interopated cc() or?
|
||
|
dB.classList = 'content'
|
||
|
ca(d,dB) // append dB to d
|
||
|
|
||
|
|
||
|
|
||
|
dTl.appendChild(d)
|
||
|
|
||
|
while(dTl.childElementCount > 10)
|
||
|
dTl.removeChild(dTl.firstChild)
|
||
|
|
||
|
}//func
|
||
|
|
||
|
|
||
|
dI.onfocus = e=>{
|
||
|
console.log('FOCUS!')
|
||
|
}
|
||
|
dI.onblur = e=>{
|
||
|
console.log('BLUR!')
|
||
|
}
|
||
|
//upg: use a tick and/or watch return from focus and a check focucs function?
|
||
|
|
||
|
dI.onkeydown = e=>{
|
||
|
const {key} = e
|
||
|
|
||
|
if(key == 'Enter'){
|
||
|
console.log({key})
|
||
|
const v = (dI.value).trim()
|
||
|
|
||
|
if(v){
|
||
|
// 'me' means use peer (peer's assocated cid)
|
||
|
const m = {
|
||
|
kind:'message',from:'me',value:v,name:username
|
||
|
}
|
||
|
|
||
|
dI.value = ''
|
||
|
|
||
|
logMessage(m)
|
||
|
send(m)
|
||
|
e.preventDefault()
|
||
|
}//if
|
||
|
}
|
||
|
}//func
|
||
|
|
||
|
//
|
||
|
// Hey bob, this is my secret message to you.
|
||
|
//
|
||
|
// I'm using your public key nicknamed 'xyz'.
|
||
|
//
|
||
|
// Here's the message, it's in base64url format.
|
||
|
//
|
||
|
// Reply back when you get this, thanks!
|
||
|
//
|
||
|
//
|
||
|
// note: i'm using protocol cirta 2023.06.15.
|
||
|
//
|
||
|
// ---------------------------
|
||
|
// FJIfIOJoifejwoifjieowsdsafijFJEIFjDFSK
|
||
|
// DSFJKEFOIFJOEjfwieoioejwfoiewifoewiofj
|
||
|
// etc...
|
||
|
//
|
||
|
//
|
||
|
//
|
||
|
//
|
||
|
//
|
||
|
|
||
|
|
||
|
|
||
|
const focusInput = n=>{
|
||
|
dI.focus()
|
||
|
}
|
||
|
|
||
|
// ------------------
|
||
|
document.addEventListener('keydown',e=>{
|
||
|
// only if not focused on typing input.
|
||
|
const {activeElement:aE} = document
|
||
|
|
||
|
const inputFocus = dI == aE
|
||
|
//console.log({inputFocus})
|
||
|
|
||
|
if(inputFocus) return // keys go to input box.
|
||
|
|
||
|
const {key} = e
|
||
|
if(key == 'c'){
|
||
|
switchCam()
|
||
|
}
|
||
|
else
|
||
|
if(key == 'd'){
|
||
|
addDisplay()
|
||
|
}
|
||
|
else
|
||
|
if(key == ' '){
|
||
|
goAudio()
|
||
|
}
|
||
|
else
|
||
|
if(key == 'Escape'){
|
||
|
focusInput()
|
||
|
}
|
||
|
else {
|
||
|
// or?
|
||
|
focusInput()
|
||
|
}
|
||
|
//if
|
||
|
|
||
|
interactionEvent()
|
||
|
})//func
|
||
|
|
||
|
// next generate ecdh https://github.com/diafygi/webcrypto-examples/#ecdh
|
||
|
// - this one per room? or?
|
||
|
|
||
|
//ugp: get key if not generated.. make each room have it's own pub sig key? or allow the same id between #rooms? (prn start with shared?)
|
||
|
//
|
||
|
//
|
||
|
// >> [new message]
|
||
|
// > encrypt [generate a new public/disposable key]
|
||
|
// > sign
|
||
|
// > make message to send {M}
|
||
|
// > (add delvery header (mainly the room id))
|
||
|
// > push to log
|
||
|
// - for later query.
|
||
|
//
|
||
|
// .. remember the rooms
|
||
|
//
|
||
|
//
|
||
|
|
||
|
//
|
||
|
// uid: x|y is domain.
|
||
|
//
|
||
|
|
||
|
//
|
||
|
// upg: db setup
|
||
|
//
|
||
|
// > messages {
|
||
|
// id,
|
||
|
// from:uid,
|
||
|
// to:uid,
|
||
|
// room,
|
||
|
// message,
|
||
|
// (message network sent,)
|
||
|
// udate
|
||
|
// }
|
||
|
// :room-id
|
||
|
// :room-udate
|
||
|
// :room-from-udate
|
||
|
// prn
|
||
|
//
|
||
|
// > system-log {
|
||
|
// id,
|
||
|
// from:uid
|
||
|
// to:uid
|
||
|
// udate,
|
||
|
// kind:'key-share',
|
||
|
// value:{} // and or.. make log include everything with search filterws... to start, yes prob that.
|
||
|
// }
|
||
|
//
|
||
|
// > rooms {
|
||
|
// id,
|
||
|
// name,
|
||
|
// (keys), //etc
|
||
|
// udate
|
||
|
// }
|
||
|
// :name
|
||
|
// //list of rooms
|
||
|
// use this and sync manager, to pull new messages (espl if visit room)
|
||
|
//
|
||
|
//
|
||
|
// upg: > search { ((re)buildable)
|
||
|
// word,
|
||
|
// ref:'room-id',
|
||
|
// from,
|
||
|
// to,
|
||
|
// udate
|
||
|
// }
|
||
|
// :word
|
||
|
// :from-word
|
||
|
// :to-word
|
||
|
// etc
|
||
|
//
|
||
|
// > inbox .. so can process even if not done and restart
|
||
|
// > outbox ..
|
||
|
//
|
||
|
// > [use a nostr type req query]
|
||
|
// > make fid = 000000000000000001 < prefix padding so can do..
|
||
|
// : since :room-000000000000003
|
||
|
//
|
||
|
//
|
||
|
//
|
||
|
// upg: could lock _message data with hashkey unlock when sign in. [most ppl this is not an issue.. pro mode feature?
|
||
|
//
|
||
|
|
||
|
|
||
|
|
||
|
let _n = 0
|
||
|
|
||
|
setInterval(n=>{
|
||
|
_n++
|
||
|
|
||
|
const {scrollHeight,clientHeight,scrollTop} = dM
|
||
|
|
||
|
let size = (scrollHeight-(clientHeight+scrollTop))
|
||
|
let isMaxScroll = size < 7
|
||
|
|
||
|
let doAutoScroll = isMaxScroll
|
||
|
|
||
|
const d = document.createElement('div')
|
||
|
d.classList.add('info')
|
||
|
d.textContent = 'uuu'
|
||
|
dL.appendChild(d)
|
||
|
|
||
|
const dd = document.createElement('div')
|
||
|
dd.classList.add('content')
|
||
|
dd.innerText = 'hello! ('+_n+')'
|
||
|
dL.appendChild(dd)
|
||
|
|
||
|
document.title = _n
|
||
|
|
||
|
//upg: wait for content to load if remote loaded content before scroll (need a event queue if then)
|
||
|
|
||
|
|
||
|
//upg detect if a snap to focus and jump to scroll positon
|
||
|
if(doAutoScroll){
|
||
|
dM.scrollTo({top:scrollHeight,left:0,behavior:'instant'})
|
||
|
}
|
||
|
|
||
|
},1000)
|
||
|
|
||
|
/*
|
||
|
let _doPing = false
|
||
|
const doPing = async n=>{
|
||
|
if(!_doPing){
|
||
|
_doPing = true
|
||
|
|
||
|
_sincePing = performance.now()/1000
|
||
|
|
||
|
const l = room.getPeers()
|
||
|
console.log('ping',l)
|
||
|
let ll = l.map(async v=>{
|
||
|
let p = await room.ping(v)
|
||
|
return {peerId:v,ping:p}
|
||
|
}
|
||
|
)
|
||
|
let lr = await Promise.all(ll)
|
||
|
console.log('ping result',{lr}) // upg: how to use?
|
||
|
_doPing = false
|
||
|
}//if
|
||
|
}//func
|
||
|
*/
|
||
|
|
||
|
let _sincePeers = 0 // upg: reset this if no peers again.
|
||
|
//let _sincePing = false
|
||
|
|
||
|
|
||
|
let _tickCount = 0
|
||
|
const tick = n=>{
|
||
|
_tickCount++
|
||
|
|
||
|
//console.log('check tick',_tickCount)
|
||
|
const now = performance.now()/1000
|
||
|
|
||
|
let pc = room.getPeers() //upg: on peer change run update to set dB.has-peers
|
||
|
let hasPeers = (pc.length > 0)
|
||
|
if(!hasPeers){
|
||
|
|
||
|
if(now-_sincePeers > 11){
|
||
|
dB.classList.add('long-time-no-peers')
|
||
|
//upg: offer link
|
||
|
//location.reload()
|
||
|
}
|
||
|
|
||
|
}//if
|
||
|
else {
|
||
|
dB.classList.remove('long-time-no-peers')
|
||
|
_sincePeers = performance.now()/1000
|
||
|
//console.log('done with tick',{hasPeers})
|
||
|
}
|
||
|
|
||
|
// if(_sincePing === false || now-_sincePing > 10){
|
||
|
// console.log('do ping test')
|
||
|
// doPing()
|
||
|
// }
|
||
|
|
||
|
|
||
|
setTimeout(tick,500)
|
||
|
}//func
|
||
|
|
||
|
tick()
|
||
|
|
||
|
console.log('ready.')
|
||
|
|
||
|
const me = async n=> {
|
||
|
const f = await fetch('https://mee.pages.dev')
|
||
|
const j = await f.json()
|
||
|
console.log({j})
|
||
|
}
|
||
|
// upg: optin to provide this (upg: as part of a resource package?)
|
||
|
// https://developer.mozilla.org/en-US/docs/Web/API/Navigator/userAgent
|
||
|
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Client_hints
|
||
|
// https://developer.mozilla.org/en-US/docs/Web/API/User-Agent_Client_Hints_API
|
||
|
//
|
||
|
// upg: ask user for name / icon.
|
||
|
//
|
||
|
// me()
|
||
|
|
||
|
|