|
- const Toggle = {
- view({attrs: {label, value}}) {
- const onclick = () => {
- StreamContainer[value] = !StreamContainer[value]
- updateSelfVideo()
- }
- const style = {
- 'font-family': 'monospace',
- 'display': 'inline-flex',
- 'justify-content': 'center',
- 'align-items': 'center',
- 'padding': '0 0.5em',
- }
- const checked = StreamContainer[value]
- return m('label', {style},
- m('input[type=checkbox]', {checked, onclick}),
- label,
- )
- }
- }
- const VideoConfig = {
- get video() {
- return StreamContainer.videoOn
- && State.online.length < 10
- && params.get('v') !== '0'
- && {width: {ideal: 320}, facingMode: 'user', frameRate: 26}
- },
- get audio() {
- return StreamContainer.audioOn
- && params.get('a') !== '0'
- },
- view() {
- return [
- m(Toggle, {label: 'video', value: 'videoOn'}),
- m(Toggle, {label: 'audio', value: 'audioOn'}),
- ]
- }
- }
- const updateSelfVideo = async () => {
- const video = document.querySelector('video.self')
-
- video.srcObject.getTracks().forEach(track => {
- track.stop()
- video.srcObject.removeTrack(track)
- delete track
- })
-
- if(StreamContainer.videoOn || StreamContainer.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.pico.chat:5349', 'turn:turn.pico.chat:5349'],
- username: 'roderic',
- credential: 'tomodachi',
- }]}
-
- 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))
-
- 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 = {
- 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)
- }
- },
- 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',
- display: 'block',
- color: 'white',
- overflow: 'hidden',
- }
- const styleMeta = {
- position: 'absolute',
- display: 'flex',
- alignItems: 'center',
- justifyContent: 'center',
- height: '100%',
- width: '100%',
- fontFamily: 'monospace',
- fontSize: 'xxx-large',
- }
- const styleVideo = {
- objectFit: Settings.get('blackBars') ? 'contain' : 'cover',
- width: '100%',
- height: '100%',
- transform: username === State.username ? 'scaleX(-1)' : 'scaleX(1)',
- }
- return m('.video-container', {style: styleOuter},
- m('.video-info', {style: styleMeta},
- m('.username', username),
- ),
- m('video[playsinline][autoplay]', {
- style: styleVideo,
- oncreate: this.setUp(username),
- onremove: this.tearDown(username),
- }),
- )
- },
- }
- const StreamContainer = {
- videoOn: true,
- audioOn: true,
- view() {
- const dims = [
- Math.floor((1 + Math.sqrt(4 * State.online.length - 3)) / 2),
- Math.ceil(Math.sqrt(State.online.length)),
- ].map(n => Array(n).fill('1fr').join(' '))
-
- if(innerHeight > innerWidth) dims.reverse()
- const style = {
- backgroundColor: 'black',
- height: '100%',
- overflow: 'hidden',
- display: 'grid',
- gridTemplateRows: dims[0],
- gridTemplateColumns: dims[1],
- }
- return m('.videos', {style},
- State.online.map(username => m(Video, {key: username, 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', () => {
- Headers.push([VideoConfig])
- Apps.push([StreamContainer, {key: 'stream-container'}])
- })
|