| @@ -0,0 +1,76 @@ | |||
| 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')) | |||
| @@ -1,7 +1,15 @@ | |||
| const ScreenShareConfig = { | |||
| toggle() { | |||
| if(ScreenShare.isOn && ScreenShare.isStreaming) { | |||
| wire({kind: 'screen-sharing-stop'}) | |||
| } | |||
| else { | |||
| ScreenShare.requestScreen() | |||
| } | |||
| }, | |||
| view() { | |||
| const checked = ScreenShare.isOn | |||
| const onclick = ScreenShare.toggle | |||
| const onclick = ScreenShareConfig.toggle | |||
| return m('label.styled', | |||
| m('input[type=checkbox]', {checked, onclick}), | |||
| 'screen-share', | |||
| @@ -9,31 +17,46 @@ const ScreenShareConfig = { | |||
| } | |||
| } | |||
| 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() { | |||
| const style = { | |||
| 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', () => { | |||
| doNotLog.add('screen-share-start') | |||
| doNotLog.add('screen-share-stop') | |||
| Headers.push([ScreenShareConfig]) | |||
| Apps.push([ScreenShare, {key: 'screen-share-container'}]) | |||
| }) | |||
| setTimeout(ScreenShare.requestScreen, 200) | |||
| @@ -52,12 +52,6 @@ const updateOtherVideo = (target, dom) => { | |||
| dom.srcObject = new MediaStream() | |||
| let rpc = null | |||
| const rpcConfig = {iceServers: [{ | |||
| urls: ['stun:stun.pico.chat:5349', 'turn:turn.pico.chat:5349'], | |||
| username: 'roderic', | |||
| credential: 'tomodachi', | |||
| }]} | |||
| const stopRpc = () => { | |||
| rpc && rpc.close() | |||
| dom.srcObject.getTracks().forEach(track => { | |||
| @@ -72,57 +66,7 @@ const updateOtherVideo = (target, dom) => { | |||
| rpc = new RTCPeerConnection(rpcConfig) | |||
| document.querySelector('video.self').srcObject.getTracks() | |||
| .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 = { | |||
| setUp: (username) => ({dom}) => { | |||
| @@ -6,11 +6,12 @@ | |||
| <link rel="stylesheet" href="/apps/chat.css"/> | |||
| <script src="/libs/mithril.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="/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/screen.js"></script> --> | |||
| </head> | |||
| @@ -37,7 +37,7 @@ listen('state', ({detail}) => { | |||
| delete detail.kind | |||
| Object.assign(State, detail) | |||
| }) | |||
| const doNotLog = new Set(['login', 'state', 'post', 'peerInfo', 'volumeMapMove']) | |||
| const doNotLog = new Set(['state']) | |||
| /* | |||
| * | |||
| * UTILS | |||
| @@ -86,7 +86,6 @@ const connect = (username) => { | |||
| } | |||
| State.websocket.onclose = (e) => { | |||
| State.online.forEach(signalPeerStop) | |||
| if(!e.wasClean) { | |||
| setTimeout(connect, 1000, username) | |||
| } | |||