| const connections = {} | const connections = {} | ||||
| const datachannels = {} | const datachannels = {} | ||||
| const streams = {} | |||||
| const screen = {} | |||||
| function setTracks({source, tracks}) { | |||||
| streams[source] = streams[source] || new MediaStream() | |||||
| streams[source].getTracks().forEach(t => streams[source].removeTrack(t)) | |||||
| tracks.forEach(track => streams[source].addTrack(track)) | |||||
| m.redraw() | |||||
| if(source === State.username) { | |||||
| reloadAllStreams() | |||||
| } | |||||
| } | |||||
| function reloadAllStreams() { | |||||
| State.online.forEach(username => { | |||||
| signal({kind: 'rpc', value: {type: 'request'}, source: username}) | |||||
| }) | |||||
| } | |||||
| function getOwnTracks() { | |||||
| if(streams[State.username]) { | |||||
| return streams[State.username].getTracks() | |||||
| } | |||||
| else { | |||||
| return [] | |||||
| } | |||||
| } | |||||
| function createConnection(target) { | function createConnection(target) { | ||||
| const rpc = new RTCPeerConnection(rpcConfig) | const rpc = new RTCPeerConnection(rpcConfig) | ||||
| wire({kind: 'rpc', value, target}) | wire({kind: 'rpc', value, target}) | ||||
| } | } | ||||
| } | } | ||||
| rpc.ontrack = ({track}) => { | |||||
| console.log(track) | |||||
| rpc.ontrack = () => { | |||||
| const tracks = rpc.getReceivers().map(t => t.track) | |||||
| setTracks({source: target, tracks: tracks}) | |||||
| } | } | ||||
| rpc.onconnectionstatechange = () => { | rpc.onconnectionstatechange = () => { | ||||
| if(rpc.connectionState === 'failed') { | if(rpc.connectionState === 'failed') { | ||||
| async function handlePeerInfo({source: target, value}) { | async function handlePeerInfo({source: target, value}) { | ||||
| const rpc = connections[target] | const rpc = connections[target] | ||||
| if(!rpc) { | if(!rpc) { | ||||
| return | return | ||||
| } | } | ||||
| const olds = rpc.getSenders().map(t => t.track) | |||||
| const news = getOwnTracks() | |||||
| rpc.getSenders().filter(s => !news.includes(s.track)).forEach(s => rpc.removeTrack(s)) | |||||
| news.filter(track => !olds.includes(track)).forEach(t => t && rpc.addTrack(t)) | |||||
| if(value.type === 'request') { | if(value.type === 'request') { | ||||
| const localOffer = await rpc.createOffer() | const localOffer = await rpc.createOffer() | ||||
| await rpc.setLocalDescription(localOffer) | await rpc.setLocalDescription(localOffer) | ||||
| } | } | ||||
| else if(value.type === 'answer') { | else if(value.type === 'answer') { | ||||
| const remoteAnswer = new RTCSessionDescription(value) | const remoteAnswer = new RTCSessionDescription(value) | ||||
| await rpc.setRemoteDescription(remoteAnswer) | |||||
| await rpc.setRemoteDescription(remoteAnswer).catch(e => e) | |||||
| } | } | ||||
| else if(value.type === 'candidate') { | else if(value.type === 'candidate') { | ||||
| const candidate = new RTCIceCandidate(value.candidate) | const candidate = new RTCIceCandidate(value.candidate) | ||||
| await rpc.addIceCandidate(candidate) | |||||
| await rpc.addIceCandidate(candidate).catch(e => e) | |||||
| } | } | ||||
| } | } | ||||
| function removeConnection(user) { | |||||
| const rpc = connections[user] | |||||
| if(rpc) { | |||||
| delete connections[user] | |||||
| function destroyConnection(username) { | |||||
| if(streams[username]) { | |||||
| streams[username].getTracks().forEach(t => t.stop()) | |||||
| delete streams[username] | |||||
| } | |||||
| if(datachannels[username]) { | |||||
| datachannels[username].close() | |||||
| delete datachannels[username] | |||||
| } | |||||
| if(connections[username]) { | |||||
| connections[username].getReceivers().forEach(r => r.track.stop()) | |||||
| connections[username].close() | |||||
| delete connections[username] | |||||
| } | } | ||||
| } | } | ||||
| addEventListener('tracks', (e) => setTracks(e.detail.value)) | |||||
| addEventListener('rpc', (e) => handlePeerInfo(e.detail)) | addEventListener('rpc', (e) => handlePeerInfo(e.detail)) | ||||
| addEventListener('join', (e) => createConnection(e.detail.value)) | addEventListener('join', (e) => createConnection(e.detail.value)) | ||||
| addEventListener('leave', (e) => destroyConnection(e.detail.value)) | |||||
| addEventListener('load', () => doNotLog.add('rpc')) | addEventListener('load', () => doNotLog.add('rpc')) |
| view({attrs: {label, value}}) { | view({attrs: {label, value}}) { | ||||
| const onclick = () => { | const onclick = () => { | ||||
| StreamContainer[value] = !StreamContainer[value] | StreamContainer[value] = !StreamContainer[value] | ||||
| updateSelfVideo() | |||||
| requestStream() | |||||
| } | } | ||||
| const checked = StreamContainer[value] | const checked = StreamContainer[value] | ||||
| return m('label.styled', | return m('label.styled', | ||||
| ] | ] | ||||
| } | } | ||||
| } | } | ||||
| const updateSelfVideo = async () => { | |||||
| const video = document.querySelector('video.self') | |||||
| video.srcObject.getTracks().forEach(track => { | |||||
| track.stop() | |||||
| video.srcObject.removeTrack(track) | |||||
| delete track | |||||
| }) | |||||
| const requestStream = async () => { | |||||
| if(StreamContainer.videoOn || StreamContainer.audioOn) { | if(StreamContainer.videoOn || StreamContainer.audioOn) { | ||||
| await navigator.mediaDevices | await navigator.mediaDevices | ||||
| .getUserMedia(VideoConfig) | .getUserMedia(VideoConfig) | ||||
| .then(s => s.getTracks().forEach(t => video.srcObject.addTrack(t))) | |||||
| .then(s => signal({kind: 'tracks', value: {source: State.username, tracks: s.getTracks()}})) | |||||
| .catch(e => console.error(e)) | .catch(e => console.error(e)) | ||||
| } | } | ||||
| video.srcObject = video.srcObject // safari | |||||
| wire({kind: 'peerInfo', value: {type: 'request'}}) | |||||
| } | |||||
| const updateOtherVideo = (target, dom) => { | |||||
| dom.srcObject = new MediaStream() | |||||
| let rpc = null | |||||
| const stopRpc = () => { | |||||
| rpc && rpc.close() | |||||
| dom.srcObject.getTracks().forEach(track => { | |||||
| track.stop() | |||||
| dom.srcObject.removeTrack(track) | |||||
| delete track | |||||
| }) | |||||
| } | |||||
| const resetRpc = () => { | |||||
| stopRpc() | |||||
| rpc = new RTCPeerConnection(rpcConfig) | |||||
| document.querySelector('video.self').srcObject.getTracks() | |||||
| .forEach(t => rpc.addTrack(t)) | |||||
| } | |||||
| } | } | ||||
| const Video = { | const Video = { | ||||
| setUp: (username) => ({dom}) => { | setUp: (username) => ({dom}) => { | ||||
| dom.username = username | |||||
| dom.srcObject = new MediaStream() | |||||
| if(username === State.username) { | if(username === State.username) { | ||||
| dom.classList.add('self') | |||||
| dom.muted = true | dom.muted = true | ||||
| updateSelfVideo() | |||||
| } | |||||
| else { | |||||
| updateOtherVideo(username, dom) | |||||
| requestStream() | |||||
| } | } | ||||
| }, | }, | ||||
| tearDown: (username) => ({dom}) => { | |||||
| removeEventListener('peerInfo', dom.listener) | |||||
| dom.srcObject.getTracks().forEach(track => { | |||||
| track.stop() | |||||
| dom.srcObject.removeTrack(track) | |||||
| delete track | |||||
| }) | |||||
| }, | |||||
| view({attrs: {username}}) { | view({attrs: {username}}) { | ||||
| const styleOuter = { | const styleOuter = { | ||||
| position: 'relative', | position: 'relative', | ||||
| ), | ), | ||||
| m('video[playsinline][autoplay]', { | m('video[playsinline][autoplay]', { | ||||
| style: styleVideo, | style: styleVideo, | ||||
| srcObject: streams[username], | |||||
| oncreate: this.setUp(username), | oncreate: this.setUp(username), | ||||
| onremove: this.tearDown(username), | |||||
| }), | }), | ||||
| ) | ) | ||||
| }, | }, | ||||
| ) | ) | ||||
| }, | }, | ||||
| } | } | ||||
| const signalPeerStop = (username) => { | |||||
| signal({kind: 'peerInfo', value: {type: 'stop'}, source: username}) | |||||
| } | |||||
| addEventListener('pagehide', () => State.online.forEach(signalPeerStop)) | |||||
| addEventListener('logout', () => State.online.forEach(signalPeerStop)) | |||||
| addEventListener('load', () => { | addEventListener('load', () => { | ||||
| Headers.push([VideoConfig]) | Headers.push([VideoConfig]) | ||||
| Apps.push([StreamContainer, {key: 'stream-container'}]) | Apps.push([StreamContainer, {key: 'stream-container'}]) |
| <!-- <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/rpc.js"></script> | <script src="/apps/rpc.js"></script> | ||||
| <!-- <script src="/apps/streams.js"></script> --> | |||||
| <script src="/apps/streams.js"></script> | |||||
| <!-- <script src="/apps/screen.js"></script> --> | <!-- <script src="/apps/screen.js"></script> --> | ||||
| <!-- <script src="/apps/chat.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> --> | |||||
| </head> | </head> | ||||
| <style> | <style> | ||||
| body { | body { |