|
|
|
|
|
|
|
|
}, |
|
|
}, |
|
|
toggle: (property) => () => { |
|
|
toggle: (property) => () => { |
|
|
VideoConfig[property] = !VideoConfig[property] |
|
|
VideoConfig[property] = !VideoConfig[property] |
|
|
VideoSelf.update() |
|
|
|
|
|
|
|
|
updateSelfVideo() |
|
|
} |
|
|
} |
|
|
}) |
|
|
}) |
|
|
const VideoSelf = { |
|
|
|
|
|
async update() { |
|
|
|
|
|
const video = document.querySelector('video.self') |
|
|
|
|
|
video.srcObject.getTracks().forEach(track => { |
|
|
|
|
|
|
|
|
const updateSelfVideo = async () => { |
|
|
|
|
|
const video = document.querySelector('video.self') |
|
|
|
|
|
|
|
|
|
|
|
video.srcObject.getTracks().forEach(track => { |
|
|
|
|
|
track.stop() |
|
|
|
|
|
video.srcObject.removeTrack(track) |
|
|
|
|
|
delete track |
|
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
|
|
if(VideoConfig.videoOn || VideoConfig.audioOn) { |
|
|
|
|
|
await navigator.mediaDevices |
|
|
|
|
|
.getUserMedia(VideoConfig) |
|
|
|
|
|
.then(s => s.getTracks().forEach(t => video.srcObject.addTrack(t))) |
|
|
|
|
|
.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 rpcConfig = {iceServers: [{urls: 'stun:stun.sipgate.net:3478'}]} |
|
|
|
|
|
|
|
|
|
|
|
const stopRpc = () => { |
|
|
|
|
|
rpc && rpc.close() |
|
|
|
|
|
dom.srcObject.getTracks().forEach(track => { |
|
|
track.stop() |
|
|
track.stop() |
|
|
video.srcObject.removeTrack(track) |
|
|
|
|
|
|
|
|
dom.srcObject.removeTrack(track) |
|
|
delete track |
|
|
delete track |
|
|
}) |
|
|
}) |
|
|
stream = new MediaStream() |
|
|
|
|
|
if(VideoConfig.videoOn || VideoConfig.audioOn) { |
|
|
|
|
|
await navigator.mediaDevices |
|
|
|
|
|
.getUserMedia(VideoConfig) |
|
|
|
|
|
.then(s => s.getTracks().forEach(t => stream.addTrack(t))) |
|
|
|
|
|
.catch(e => console.error(e)) |
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const resetRpc = () => { |
|
|
|
|
|
stopRpc() |
|
|
|
|
|
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}) |
|
|
|
|
|
} |
|
|
} |
|
|
} |
|
|
video.srcObject = stream |
|
|
|
|
|
wire({kind: 'peerInfo', value: {type: 'request'}}) |
|
|
|
|
|
}, |
|
|
|
|
|
setUp: ({dom}) => { |
|
|
|
|
|
dom.playsinline = true |
|
|
|
|
|
dom.autoplay = true |
|
|
|
|
|
dom.muted = true |
|
|
|
|
|
dom.srcObject = new MediaStream() |
|
|
|
|
|
VideoSelf.update() |
|
|
|
|
|
}, |
|
|
|
|
|
view({attrs: {username}}) { |
|
|
|
|
|
const styleOuter = { |
|
|
|
|
|
position: 'relative', |
|
|
|
|
|
display: 'block', |
|
|
|
|
|
backgroundColor: 'black', |
|
|
|
|
|
color: 'white', |
|
|
|
|
|
overflow: 'hidden', |
|
|
|
|
|
|
|
|
rpc.ontrack = ({track}) => { |
|
|
|
|
|
dom.srcObject.addTrack(track) |
|
|
|
|
|
dom.srcObject = dom.srcObject |
|
|
} |
|
|
} |
|
|
const styleInner = { |
|
|
|
|
|
objectFit: 'cover', |
|
|
|
|
|
width: '100%', |
|
|
|
|
|
height: '100%', |
|
|
|
|
|
transform: 'scaleX(-1)', |
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
dom.listener = async ({detail: {source, value}}) => { |
|
|
|
|
|
if(source !== target) { |
|
|
|
|
|
return |
|
|
} |
|
|
} |
|
|
return m('.video-container', {style: styleOuter}, |
|
|
|
|
|
m('.video-info', {style: {position: 'absolute', zIndex: 999}}, |
|
|
|
|
|
m('span', {style: {padding: '5px'}}, username), |
|
|
|
|
|
), |
|
|
|
|
|
m('video.self', {style: styleInner, oncreate: this.setUp}), |
|
|
|
|
|
) |
|
|
|
|
|
}, |
|
|
|
|
|
|
|
|
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 VideoOther = { |
|
|
|
|
|
|
|
|
const Video = { |
|
|
setUp: (username) => ({dom}) => { |
|
|
setUp: (username) => ({dom}) => { |
|
|
dom.playsinline = true |
|
|
|
|
|
dom.autoplay = true |
|
|
|
|
|
dom.srcObject = new MediaStream() |
|
|
dom.srcObject = new MediaStream() |
|
|
let rpc = null |
|
|
|
|
|
|
|
|
|
|
|
const rpcConfig = {iceServers: [{urls: 'stun:stun.sipgate.net:3478'}]} |
|
|
|
|
|
|
|
|
|
|
|
const stopRpc = () => { |
|
|
|
|
|
rpc && rpc.close() |
|
|
|
|
|
dom.srcObject.getTracks().forEach(track => { |
|
|
|
|
|
track.stop() |
|
|
|
|
|
dom.srcObject.removeTrack(track) |
|
|
|
|
|
delete track |
|
|
|
|
|
}) |
|
|
|
|
|
|
|
|
if(username === State.username) { |
|
|
|
|
|
dom.classList.add('self') |
|
|
|
|
|
dom.muted = true |
|
|
|
|
|
updateSelfVideo() |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
const resetRpc = () => { |
|
|
|
|
|
stopRpc() |
|
|
|
|
|
rpc = new RTCPeerConnection(rpcConfig) |
|
|
|
|
|
dom.srcObject = new MediaStream() |
|
|
|
|
|
document.querySelector('video.self').srcObject.getTracks() |
|
|
|
|
|
.forEach(t => rpc.addTrack(t)) |
|
|
|
|
|
|
|
|
|
|
|
rpc.onicecandidate = ({candidate}) => { |
|
|
|
|
|
if(candidate && candidate.candidate) { |
|
|
|
|
|
wire({kind: 'peerInfo', value: {type: 'candidate', candidate}, target: username}) |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
rpc.ontrack = ({track}) => { |
|
|
|
|
|
dom.srcObject.addTrack(track) |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const onPeerInfo = async ({detail: {source, value}}) => { |
|
|
|
|
|
if(source !== username) { |
|
|
|
|
|
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: username}) |
|
|
|
|
|
} |
|
|
|
|
|
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: username}) |
|
|
|
|
|
} |
|
|
|
|
|
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() |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
else { |
|
|
|
|
|
updateOtherVideo(username, dom) |
|
|
} |
|
|
} |
|
|
|
|
|
}, |
|
|
|
|
|
tearDown: (username) => ({dom}) => { |
|
|
|
|
|
removeEventListener('peerInfo', dom.listener) |
|
|
|
|
|
|
|
|
addEventListener('peerInfo', onPeerInfo) |
|
|
|
|
|
|
|
|
dom.srcObject.getTracks().forEach(track => { |
|
|
|
|
|
track.stop() |
|
|
|
|
|
dom.srcObject.removeTrack(track) |
|
|
|
|
|
delete track |
|
|
|
|
|
}) |
|
|
}, |
|
|
}, |
|
|
view({attrs: {username}}) { |
|
|
view({attrs: {username}}) { |
|
|
const styleOuter = { |
|
|
const styleOuter = { |
|
|
|
|
|
|
|
|
height: '100%', |
|
|
height: '100%', |
|
|
transform: 'scaleX(-1)', |
|
|
transform: 'scaleX(-1)', |
|
|
} |
|
|
} |
|
|
return m('.video-container', {style: styleOuter}, |
|
|
|
|
|
|
|
|
return m('.video-container', {key: username, style: styleOuter}, |
|
|
m('.video-info', {style: {position: 'absolute', zIndex: 999}}, |
|
|
m('.video-info', {style: {position: 'absolute', zIndex: 999}}, |
|
|
m('span', {style: {padding: '5px'}}, username), |
|
|
m('span', {style: {padding: '5px'}}, username), |
|
|
), |
|
|
), |
|
|
m('video', {style: styleInner, oncreate: this.setUp(username)}), |
|
|
|
|
|
|
|
|
m('video[playsinline][autoplay]', { |
|
|
|
|
|
style: styleInner, |
|
|
|
|
|
oncreate: this.setUp(username), |
|
|
|
|
|
onremove: this.tearDown(username), |
|
|
|
|
|
}), |
|
|
) |
|
|
) |
|
|
}, |
|
|
}, |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
return '1fr' |
|
|
return '1fr' |
|
|
}, |
|
|
}, |
|
|
view() { |
|
|
view() { |
|
|
|
|
|
const dims = [StreamContainer.getRows(), StreamContainer.getColumns()] |
|
|
|
|
|
if(screen.height > screen.width) dims.reverse() |
|
|
const style = { |
|
|
const style = { |
|
|
display: 'grid', |
|
|
display: 'grid', |
|
|
padding: '3px', |
|
|
padding: '3px', |
|
|
gridGap: '3px', |
|
|
gridGap: '3px', |
|
|
height: '80vh', |
|
|
|
|
|
gridTemplateColumns: StreamContainer.getColumns(), |
|
|
|
|
|
gridTemplateRows: StreamContainer.getRows(), |
|
|
|
|
|
|
|
|
height: '70vh', |
|
|
|
|
|
gridTemplateRows: dims[0], |
|
|
|
|
|
gridTemplateColumns: dims[1], |
|
|
} |
|
|
} |
|
|
return m('div', |
|
|
return m('div', |
|
|
m('.video-controls', |
|
|
m('.video-controls', |
|
|
|
|
|
|
|
|
m('button', {onclick: VideoConfig.toggle('audioOn')}, 'audio'), |
|
|
m('button', {onclick: VideoConfig.toggle('audioOn')}, 'audio'), |
|
|
), |
|
|
), |
|
|
m('.videos', {style}, |
|
|
m('.videos', {style}, |
|
|
m(VideoSelf, {username: State.username}), |
|
|
|
|
|
|
|
|
m(Video, {username: State.username}), |
|
|
State.online.filter(username => username != State.username) |
|
|
State.online.filter(username => username != State.username) |
|
|
.map(username => m(VideoOther, {username})) |
|
|
|
|
|
|
|
|
.map(username => m(Video, {username})) |
|
|
), |
|
|
), |
|
|
) |
|
|
) |
|
|
}, |
|
|
}, |