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 { |