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) | ||||
} | } |