// // 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{ 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()