| @@ -1,5 +1,33 @@ | |||
| const connections = {} | |||
| 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) { | |||
| const rpc = new RTCPeerConnection(rpcConfig) | |||
| @@ -10,8 +38,9 @@ function createConnection(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 = () => { | |||
| if(rpc.connectionState === 'failed') { | |||
| @@ -39,9 +68,16 @@ function createConnection(target) { | |||
| async function handlePeerInfo({source: target, value}) { | |||
| const rpc = connections[target] | |||
| if(!rpc) { | |||
| 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') { | |||
| const localOffer = await rpc.createOffer() | |||
| await rpc.setLocalDescription(localOffer) | |||
| @@ -56,21 +92,32 @@ async function handlePeerInfo({source: target, value}) { | |||
| } | |||
| else if(value.type === 'answer') { | |||
| const remoteAnswer = new RTCSessionDescription(value) | |||
| await rpc.setRemoteDescription(remoteAnswer) | |||
| await rpc.setRemoteDescription(remoteAnswer).catch(e => e) | |||
| } | |||
| else if(value.type === '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('join', (e) => createConnection(e.detail.value)) | |||
| addEventListener('leave', (e) => destroyConnection(e.detail.value)) | |||
| addEventListener('load', () => doNotLog.add('rpc')) | |||
| @@ -2,7 +2,7 @@ const Toggle = { | |||
| view({attrs: {label, value}}) { | |||
| const onclick = () => { | |||
| StreamContainer[value] = !StreamContainer[value] | |||
| updateSelfVideo() | |||
| requestStream() | |||
| } | |||
| const checked = StreamContainer[value] | |||
| return m('label.styled', | |||
| @@ -29,67 +29,21 @@ const VideoConfig = { | |||
| ] | |||
| } | |||
| } | |||
| 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) { | |||
| await navigator.mediaDevices | |||
| .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)) | |||
| } | |||
| 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 = { | |||
| setUp: (username) => ({dom}) => { | |||
| dom.username = username | |||
| dom.srcObject = new MediaStream() | |||
| if(username === State.username) { | |||
| dom.classList.add('self') | |||
| 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}}) { | |||
| const styleOuter = { | |||
| position: 'relative', | |||
| @@ -119,8 +73,8 @@ const Video = { | |||
| ), | |||
| m('video[playsinline][autoplay]', { | |||
| style: styleVideo, | |||
| srcObject: streams[username], | |||
| oncreate: this.setUp(username), | |||
| onremove: this.tearDown(username), | |||
| }), | |||
| ) | |||
| }, | |||
| @@ -148,11 +102,6 @@ const StreamContainer = { | |||
| ) | |||
| }, | |||
| } | |||
| 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', () => { | |||
| Headers.push([VideoConfig]) | |||
| Apps.push([StreamContainer, {key: 'stream-container'}]) | |||
| @@ -9,11 +9,10 @@ | |||
| <!-- <script src="/libs/purify.min.js"></script> --> | |||
| <script src="/pico.js" defer></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/chat.js"></script> --> | |||
| <!-- <script src="/apps/volume.js"></script> --> | |||
| <!-- <script src="/apps/screen.js"></script> --> | |||
| </head> | |||
| <style> | |||
| body { | |||