| const connections = {} | |||||
| const datachannels = {} | |||||
| function createConnection(target) { | |||||
| const rpc = new RTCPeerConnection(rpcConfig) | |||||
| rpc.onicecandidate = ({candidate}) => { | |||||
| if(candidate && candidate.candidate) { | |||||
| const value = {type: 'candidate', candidate} | |||||
| wire({kind: 'rpc', value, target}) | |||||
| } | |||||
| } | |||||
| rpc.ontrack = ({track}) => { | |||||
| console.log(track) | |||||
| } | |||||
| rpc.onconnectionstatechange = () => { | |||||
| if(rpc.connectionState === 'failed') { | |||||
| console.log(target, 'failed, retry!') | |||||
| wire({kind: 'rpc', value: {type: 'request'}, target}) | |||||
| } | |||||
| } | |||||
| rpc.ondatachannel = ({channel}) => { | |||||
| datachannels[target] = channel | |||||
| datachannels[target].onmessage = ({data}) => console.log(data) | |||||
| // for testing purposes | |||||
| const msg = `rpc established from ${target} to ${State.username}` | |||||
| datachannels[target].send(msg) | |||||
| console.log(msg) | |||||
| } | |||||
| connections[target] = rpc | |||||
| if(State.username > target) { | |||||
| datachannels[target] = rpc.createDataChannel('test') | |||||
| datachannels[target].onmessage = ({data}) => console.log(data) | |||||
| signal({kind: 'rpc', value: {type: 'request'}, source: target}) | |||||
| } | |||||
| } | |||||
| async function handlePeerInfo({source: target, value}) { | |||||
| const rpc = connections[target] | |||||
| if(!rpc) { | |||||
| return | |||||
| } | |||||
| if(value.type === 'request') { | |||||
| const localOffer = await rpc.createOffer() | |||||
| await rpc.setLocalDescription(localOffer) | |||||
| wire({kind: 'rpc', value: localOffer, target}) | |||||
| } | |||||
| else if(value.type === 'offer') { | |||||
| const remoteOffer = new RTCSessionDescription(value) | |||||
| await rpc.setRemoteDescription(remoteOffer) | |||||
| const localAnswer = await rpc.createAnswer() | |||||
| await rpc.setLocalDescription(localAnswer) | |||||
| wire({kind: 'rpc', value: localAnswer, target}) | |||||
| } | |||||
| else if(value.type === 'answer') { | |||||
| const remoteAnswer = new RTCSessionDescription(value) | |||||
| await rpc.setRemoteDescription(remoteAnswer) | |||||
| } | |||||
| else if(value.type === 'candidate') { | |||||
| const candidate = new RTCIceCandidate(value.candidate) | |||||
| await rpc.addIceCandidate(candidate) | |||||
| } | |||||
| } | |||||
| function removeConnection(user) { | |||||
| const rpc = connections[user] | |||||
| if(rpc) { | |||||
| delete connections[user] | |||||
| } | |||||
| } | |||||
| addEventListener('rpc', (e) => handlePeerInfo(e.detail)) | |||||
| addEventListener('join', (e) => createConnection(e.detail.value)) | |||||
| addEventListener('load', () => doNotLog.add('rpc')) |
| const ScreenShareConfig = { | const ScreenShareConfig = { | ||||
| toggle() { | |||||
| if(ScreenShare.isOn && ScreenShare.isStreaming) { | |||||
| wire({kind: 'screen-sharing-stop'}) | |||||
| } | |||||
| else { | |||||
| ScreenShare.requestScreen() | |||||
| } | |||||
| }, | |||||
| view() { | view() { | ||||
| const checked = ScreenShare.isOn | const checked = ScreenShare.isOn | ||||
| const onclick = ScreenShare.toggle | |||||
| const onclick = ScreenShareConfig.toggle | |||||
| return m('label.styled', | return m('label.styled', | ||||
| m('input[type=checkbox]', {checked, onclick}), | m('input[type=checkbox]', {checked, onclick}), | ||||
| 'screen-share', | 'screen-share', | ||||
| } | } | ||||
| } | } | ||||
| const ScreenShare = { | const ScreenShare = { | ||||
| isOn: false, | |||||
| start() { | |||||
| const screen = document.querySelector('video.screen') | |||||
| if(screen) { | |||||
| navigator.mediaDevices.getDisplayMedia() | |||||
| .then(s => {screen.srcObject = s}) | |||||
| .catch(e => console.error(e)) | |||||
| } | |||||
| streamer: null, | |||||
| stream: null, | |||||
| get isStreaming() { | |||||
| return ScreenShare.streamer === State.username | |||||
| }, | }, | ||||
| toggle() { | |||||
| ScreenShare.isOn = !ScreenShare.isOn | |||||
| if(ScreenShare.isOn) { | |||||
| // setTimeout(ScreenShare.start, 100) | |||||
| } | |||||
| get isOn() { | |||||
| return !! ScreenShare.streamer | |||||
| }, | |||||
| async requestScreen() { | |||||
| // ScreenShare.stream = await navigator.mediaDevices.getDisplayMedia() | |||||
| // ScreenShare.streamer = State.username | |||||
| // wire({kind: 'screen-sharing-start'}) | |||||
| }, | }, | ||||
| view() { | view() { | ||||
| const style = { | const style = { | ||||
| overflow: 'scroll', | overflow: 'scroll', | ||||
| backgroundColor: 'black', | |||||
| color: 'white', | |||||
| fontFamily: 'monospace', | |||||
| } | } | ||||
| return ScreenShare.isOn && m('div', {style}, | |||||
| m('video.screen[playsinline][autoplay]'), | |||||
| return ScreenShare.isOn && m('.screen-share', {style}, | |||||
| m('.streamer', `${ScreenShare.streamer}'s stream`), | |||||
| m('video.screen[playsinline][autoplay]', {srcObject: ScreenShare.stream}), | |||||
| ) | ) | ||||
| }, | }, | ||||
| } | } | ||||
| addEventListener('screen-sharing-start', ({detail}) => { | |||||
| ScreenShare.streamer = detail.source | |||||
| }) | |||||
| addEventListener('screen-sharing-stop', ({detail}) => { | |||||
| if(ScreenShare.stream) { | |||||
| ScreenShare.stream.getTracks().forEach(track => track.stop()) | |||||
| ScreenShare.stream = null | |||||
| } | |||||
| ScreenShare.streamer = null | |||||
| }) | |||||
| addEventListener('load', () => { | addEventListener('load', () => { | ||||
| doNotLog.add('screen-share-start') | |||||
| doNotLog.add('screen-share-stop') | |||||
| Headers.push([ScreenShareConfig]) | Headers.push([ScreenShareConfig]) | ||||
| Apps.push([ScreenShare, {key: 'screen-share-container'}]) | Apps.push([ScreenShare, {key: 'screen-share-container'}]) | ||||
| }) | }) | ||||
| setTimeout(ScreenShare.requestScreen, 200) |
| dom.srcObject = new MediaStream() | dom.srcObject = new MediaStream() | ||||
| let rpc = null | let rpc = null | ||||
| const rpcConfig = {iceServers: [{ | |||||
| urls: ['stun:stun.pico.chat:5349', 'turn:turn.pico.chat:5349'], | |||||
| username: 'roderic', | |||||
| credential: 'tomodachi', | |||||
| }]} | |||||
| const stopRpc = () => { | const stopRpc = () => { | ||||
| rpc && rpc.close() | rpc && rpc.close() | ||||
| dom.srcObject.getTracks().forEach(track => { | dom.srcObject.getTracks().forEach(track => { | ||||
| rpc = new RTCPeerConnection(rpcConfig) | rpc = new RTCPeerConnection(rpcConfig) | ||||
| document.querySelector('video.self').srcObject.getTracks() | document.querySelector('video.self').srcObject.getTracks() | ||||
| .forEach(t => rpc.addTrack(t)) | .forEach(t => rpc.addTrack(t)) | ||||
| rpc.onicecandidate = ({candidate}) => { | |||||
| if(candidate && candidate.candidate) { | |||||
| const value = {type: 'candidate', candidate} | |||||
| wire({kind: 'peerInfo', value, target}) | |||||
| } | |||||
| } | |||||
| rpc.ontrack = ({track}) => { | |||||
| dom.srcObject.addTrack(track) | |||||
| dom.srcObject = dom.srcObject | |||||
| } | |||||
| rpc.onconnectionstatechange = () => { | |||||
| if(rpc.connectionState === 'failed') { | |||||
| console.log(target, 'failed, retry!') | |||||
| wire({kind: 'peerInfo', value: {type: 'request'}, target}) | |||||
| } | |||||
| } | |||||
| } | |||||
| dom.listener = async ({detail: {source, value}}) => { | |||||
| if(source !== target) { | |||||
| return | |||||
| } | |||||
| console.log(source, value.type) | |||||
| if(value.type === 'request') { | |||||
| resetRpc() | |||||
| const localOffer = await rpc.createOffer() | |||||
| await rpc.setLocalDescription(localOffer) | |||||
| wire({kind: 'peerInfo', value: localOffer, target}) | |||||
| } | |||||
| else if(value.type === 'offer') { | |||||
| resetRpc() | |||||
| const remoteOffer = new RTCSessionDescription(value) | |||||
| await rpc.setRemoteDescription(remoteOffer) | |||||
| const localAnswer = await rpc.createAnswer() | |||||
| await rpc.setLocalDescription(localAnswer) | |||||
| wire({kind: 'peerInfo', value: localAnswer, target}) | |||||
| } | |||||
| else if(value.type === 'answer') { | |||||
| const remoteAnswer = new RTCSessionDescription(value) | |||||
| await rpc.setRemoteDescription(remoteAnswer) | |||||
| } | |||||
| else if(value.type === 'candidate') { | |||||
| const candidate = new RTCIceCandidate(value.candidate) | |||||
| await rpc.addIceCandidate(candidate) | |||||
| } | |||||
| else if(value.type === 'stop') { | |||||
| stopRpc() | |||||
| } | |||||
| } | } | ||||
| addEventListener('peerInfo', dom.listener) | |||||
| } | } | ||||
| const Video = { | const Video = { | ||||
| setUp: (username) => ({dom}) => { | setUp: (username) => ({dom}) => { |
| <link rel="stylesheet" href="/apps/chat.css"/> | <link rel="stylesheet" href="/apps/chat.css"/> | ||||
| <script src="/libs/mithril.min.js"></script> | <script src="/libs/mithril.min.js"></script> | ||||
| <script src="/libs/marked.min.js"></script> | <script src="/libs/marked.min.js"></script> | ||||
| <script src="/libs/purify.min.js"></script> | |||||
| <!-- <script src="/libs/purify.min.js"></script> --> | |||||
| <script src="/pico.js" defer></script> | <script src="/pico.js" defer></script> | ||||
| <script src="/apps/streams.js"></script> | |||||
| <script src="/apps/screen.js"></script> | |||||
| <script src="/apps/chat.js"></script> | |||||
| <script src="/apps/rpc.js"></script> | |||||
| <!-- <script src="/apps/streams.js"></script> --> | |||||
| <!-- <script src="/apps/screen.js"></script> --> | |||||
| <!-- <script src="/apps/chat.js"></script> --> | |||||
| <!-- <script src="/apps/volume.js"></script> --> | <!-- <script src="/apps/volume.js"></script> --> | ||||
| <!-- <script src="/apps/screen.js"></script> --> | <!-- <script src="/apps/screen.js"></script> --> | ||||
| </head> | </head> |
| delete detail.kind | delete detail.kind | ||||
| Object.assign(State, detail) | Object.assign(State, detail) | ||||
| }) | }) | ||||
| const doNotLog = new Set(['login', 'state', 'post', 'peerInfo', 'volumeMapMove']) | |||||
| const doNotLog = new Set(['state']) | |||||
| /* | /* | ||||
| * | * | ||||
| * UTILS | * UTILS | ||||
| } | } | ||||
| State.websocket.onclose = (e) => { | State.websocket.onclose = (e) => { | ||||
| State.online.forEach(signalPeerStop) | |||||
| if(!e.wasClean) { | if(!e.wasClean) { | ||||
| setTimeout(connect, 1000, username) | setTimeout(connect, 1000, username) | ||||
| } | } |