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 styleOuter = { position: 'relative', display: 'block', color: 'white', overflow: 'hidden', } const styleMeta = { position: 'absolute', display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100%', width: '100%', fontFamily: 'monospace', fontSize: 'xxx-large', } const styleVideo = { objectFit: Settings.get('blackBars') ? 'contain' : 'cover', width: '100%', height: '100%', transform: username === State.username ? 'scaleX(-1)' : 'scaleX(1)', } return m('.video-container', {style: styleOuter}, m('.video-info', {style: styleMeta}, m('.username', username), ), m('video[playsinline][autoplay]', { style: styleVideo, srcObject: VideoShare.streams[username], oncreate: ({dom}) => {dom.muted = username === State.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}}) ) }, view() { const dims = [ Math.floor((1 + Math.sqrt(4 * State.online.length - 3)) / 2), Math.ceil(Math.sqrt(State.online.length)), ].map(n => Array(n || 1).fill('1fr').join(' ')) if(innerHeight > innerWidth) dims.reverse() const style = { backgroundColor: 'black', overflow: 'hidden', display: 'grid', gridTemplateRows: dims[0], gridTemplateColumns: dims[1], } return m('.videos', {style}, State.online.map(username => m(Video, {key: username, username})) ) }, } addEventListener('join', ({detail: {value: target}}) => { signal({kind: 'rpc-needed', value: {kind: 'video', target}}) }) addEventListener('load', () => { Headers.push([VideoShareConfig]) Apps.push([VideoShare, {key: 'stream-container'}]) })