addEventListener('rpc-new', ({detail}) => { const {uid, kind, target} = detail.value if(kind === 'video') { const sender = VideoShare.streams[State.username] const receiver = VideoShare.resetStream(target) signal({kind: 'rpc-setup', value: {uid, sender, receiver}}) } }) const Video = { view({attrs: {username}}) { const styleVideo = { objectFit: Settings.get('blackBars') ? 'contain' : 'cover', width: '100%', height: '100%', transform: username === State.username ? 'scaleX(-1)' : 'scaleX(1)', } return m('.video-container', m('.video-info', m(`.label-${username}`, username), ), m('video[playsinline][autoplay]', { style: styleVideo, oncreate: ({dom}) => { dom.muted = username === State.username dom.srcObject = VideoShare.streams[username] }, onupdate: ({dom}) => { if(dom.srcObject !== VideoShare.streams[username]) { dom.srcObject = VideoShare.streams[username] } }, onremove: () => VideoShare.resetStream(username), }), ) }, } const Toggle = { view({attrs: {key, label}}) { const onclick = () => { VideoShare[key] = !VideoShare[key] VideoShare.getStream() } const checked = VideoShare[key] return m('label.styled', m('input[type=checkbox]', {onclick, checked}), label, ) }, } const VideoShareConfig = { get video() { return VideoShare.videoOn && State.online.length < 10 && params.get('v') !== '0' && {width: {ideal: 320}, facingMode: 'user', frameRate: 26} }, get audio() { return VideoShare.audioOn && params.get('a') !== '0' }, view() { return [ m(Toggle, {key: 'videoOn', label: 'video'}), m(Toggle, {key: 'audioOn', label: 'audio'}), ] }, } const VideoShare = { videoOn: true, audioOn: true, streams: {}, oncreate(e) { VideoShare.getStream() }, resetStream(target) { if(VideoShare.streams[target]) { VideoShare.streams[target].getTracks().forEach(tr => tr.stop()) } VideoShare.streams[target] = new MediaStream() return VideoShare.streams[target] }, async getStream() { VideoShare.resetStream(State.username) await navigator.mediaDevices.getUserMedia(VideoShareConfig) .catch(error => console.error(error)) .then(stream => {VideoShare.streams[State.username] = stream}) m.redraw() State.others.forEach(target => { signal({kind: 'rpc-needed', value: {kind: 'video', target}}) }) }, reLayout({dom}) { const COUNT = dom.children.length || 1 let [fnx, fny] = [ Math.floor((1 + Math.sqrt(4 * COUNT - 3)) / 2), Math.ceil(Math.sqrt(COUNT)), ] let max = 0 for([nx, ny] of [[1, COUNT], [fnx, fny], [COUNT, 1]]) { let h = dom.clientHeight / ny let w = dom.clientWidth / nx if(w > h) { w = Math.min(w, h * 4/3) } else { h = Math.min(h, w * 3/4) } if(h * w > max) { max = h * w fnx = nx fny = ny } } dom.style['grid-template-columns'] = Array(fnx).fill('1fr').join(' ') dom.style['grid-template-rows'] = Array(fny).fill('1fr').join(' ') }, view() { const style = { backgroundColor: 'black', overflow: 'hidden', display: 'grid', } const oncreate = VideoShare.reLayout const onupdate = VideoShare.reLayout return m('.videos', {style, oncreate, onupdate}, State.online.map(username => m(Video, {key: username, username})) ) }, } addEventListener('load', () => { Headers.push([VideoShareConfig]) Apps.push([VideoShare, {key: 'stream-container'}]) })