|  |  | @@ -10,124 +10,114 @@ const VideoConfig = Object.seal({ | 
		
	
		
			
			|  |  |  | }, | 
		
	
		
			
			|  |  |  | toggle: (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() | 
		
	
		
			
			|  |  |  | video.srcObject.removeTrack(track) | 
		
	
		
			
			|  |  |  | dom.srcObject.removeTrack(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}) => { | 
		
	
		
			
			|  |  |  | dom.playsinline = true | 
		
	
		
			
			|  |  |  | dom.autoplay = true | 
		
	
		
			
			|  |  |  | 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}}) { | 
		
	
		
			
			|  |  |  | const styleOuter = { | 
		
	
	
		
			
			|  |  | @@ -143,11 +133,15 @@ const VideoOther = { | 
		
	
		
			
			|  |  |  | height: '100%', | 
		
	
		
			
			|  |  |  | 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('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), | 
		
	
		
			
			|  |  |  | }), | 
		
	
		
			
			|  |  |  | ) | 
		
	
		
			
			|  |  |  | }, | 
		
	
		
			
			|  |  |  | } | 
		
	
	
		
			
			|  |  | @@ -166,13 +160,15 @@ const StreamContainer = { | 
		
	
		
			
			|  |  |  | return '1fr' | 
		
	
		
			
			|  |  |  | }, | 
		
	
		
			
			|  |  |  | view() { | 
		
	
		
			
			|  |  |  | const dims = [StreamContainer.getRows(), StreamContainer.getColumns()] | 
		
	
		
			
			|  |  |  | if(screen.height > screen.width) dims.reverse() | 
		
	
		
			
			|  |  |  | const style = { | 
		
	
		
			
			|  |  |  | display: 'grid', | 
		
	
		
			
			|  |  |  | padding: '3px', | 
		
	
		
			
			|  |  |  | gridGap: '3px', | 
		
	
		
			
			|  |  |  | height: '80vh', | 
		
	
		
			
			|  |  |  | gridTemplateColumns: StreamContainer.getColumns(), | 
		
	
		
			
			|  |  |  | gridTemplateRows: StreamContainer.getRows(), | 
		
	
		
			
			|  |  |  | height: '70vh', | 
		
	
		
			
			|  |  |  | gridTemplateRows: dims[0], | 
		
	
		
			
			|  |  |  | gridTemplateColumns: dims[1], | 
		
	
		
			
			|  |  |  | } | 
		
	
		
			
			|  |  |  | return m('div', | 
		
	
		
			
			|  |  |  | m('.video-controls', | 
		
	
	
		
			
			|  |  | @@ -180,9 +176,9 @@ const StreamContainer = { | 
		
	
		
			
			|  |  |  | m('button', {onclick: VideoConfig.toggle('audioOn')}, 'audio'), | 
		
	
		
			
			|  |  |  | ), | 
		
	
		
			
			|  |  |  | m('.videos', {style}, | 
		
	
		
			
			|  |  |  | m(VideoSelf, {username: State.username}), | 
		
	
		
			
			|  |  |  | m(Video, {username: State.username}), | 
		
	
		
			
			|  |  |  | State.online.filter(username => username != State.username) | 
		
	
		
			
			|  |  |  | .map(username => m(VideoOther, {username})) | 
		
	
		
			
			|  |  |  | .map(username => m(Video, {username})) | 
		
	
		
			
			|  |  |  | ), | 
		
	
		
			
			|  |  |  | ) | 
		
	
		
			
			|  |  |  | }, |