| @@ -1,9 +1,10 @@ | |||
| const State = { | |||
| username: null, | |||
| websocket: null, | |||
| online: [], | |||
| posts: [], | |||
| media: null, | |||
| username: null, | |||
| rpcs: {}, | |||
| media: {}, | |||
| } | |||
| /* | |||
| @@ -13,43 +14,44 @@ const State = { | |||
| */ | |||
| const wire = (message) => State.websocket.send(JSON.stringify(message)) | |||
| const signal = (message) => dispatchEvent(new CustomEvent(message.kind, {detail: message})) | |||
| const signalPeerStop = (username) => signal({kind: 'peerInfo', value: {type: 'stop'}, source: username}) | |||
| const listen = (kind, handler) => addEventListener(kind, handler) | |||
| listen('login', ({detail}) => State.username = detail.value) | |||
| listen('post', ({detail}) => State.posts.push(detail)) | |||
| listen('peerInfo', (e) => onPeerInfo(e)) | |||
| const doNotLog = new Set(['login', 'post', 'peerInfo']) | |||
| /* | |||
| * | |||
| * WEBRTC | |||
| * | |||
| */ | |||
| const rpcs = {} | |||
| const streams = {} | |||
| const getOrCreateRpc = (username) => { | |||
| if(!rpcs[username] && State.media) { | |||
| if(State.username === username) { | |||
| return | |||
| } | |||
| const myStream = State.media[State.username] | |||
| if(!State.rpcs[username] && myStream) { | |||
| const rpc = new RTCPeerConnection({iceServers: [{urls: 'stun:stun.sipgate.net:3478'}]}) | |||
| State.media.getTracks().forEach(track => rpc.addTrack(track, State.media)) | |||
| myStream.getTracks().forEach(track => rpc.addTrack(track, myStream)) | |||
| rpc.onicecandidate = ({candidate}) => { | |||
| if(candidate) { | |||
| wire({kind: 'peerInfo', value: {type: 'candidate', candidate}}) | |||
| } | |||
| } | |||
| rpc.ontrack = (e) => { | |||
| streams[username] = e.streams[0] | |||
| State.media[username] = e.streams[0] | |||
| m.redraw() | |||
| } | |||
| rpc.onclose = (e) => { | |||
| console.log(username, e) | |||
| } | |||
| rpcs[username] = rpc | |||
| State.rpcs[username] = rpc | |||
| } | |||
| return rpcs[username] | |||
| return State.rpcs[username] | |||
| } | |||
| const onPeerInfo = async ({detail: message}) => { | |||
| if(State.username === message.source) { | |||
| return | |||
| } | |||
| const rpc = getOrCreateRpc(message.source) | |||
| if(rpc && message.value.type === 'request') { | |||
| const localOffer = await rpc.createOffer() | |||
| @@ -72,16 +74,16 @@ const onPeerInfo = async ({detail: message}) => { | |||
| rpc.addIceCandidate(candidate) | |||
| } | |||
| else if(message.value.type === 'stop') { | |||
| if(streams[message.source]) { | |||
| streams[message.source].getTracks().map(track => track.stop()) | |||
| delete streams[message.source] | |||
| if(State.media[message.source]) { | |||
| State.media[message.source].getTracks().map(track => track.stop()) | |||
| delete State.media[message.source] | |||
| } | |||
| if(rpcs[message.source]) { | |||
| rpcs[message.source].close() | |||
| delete rpcs[message.source] | |||
| if(State.rpcs[message.source]) { | |||
| State.rpcs[message.source].close() | |||
| delete State.rpcs[message.source] | |||
| } | |||
| } | |||
| else { | |||
| else if(rpc) { | |||
| console.log('uncaught', message) | |||
| } | |||
| } | |||
| @@ -101,34 +103,34 @@ const prettyTime = (ts) => { | |||
| return ts.slice(11, 19) | |||
| } | |||
| const Video = { | |||
| appendStream: (stream) => ({dom}) => { | |||
| appendStream: ({username, stream}) => ({dom}) => { | |||
| dom.autoplay = true | |||
| dom.muted = true | |||
| dom.muted = (username === State.username) | |||
| dom.srcObject = stream | |||
| }, | |||
| view({attrs}) { | |||
| const rpc = State.rpcs[attrs.username] || {iceConnectionState: m.trust(' ')} | |||
| return m('.video-container', | |||
| m('.video-source', attrs.username), | |||
| m('video', {playsinline: true, oncreate: Video.appendStream(attrs.stream)}), | |||
| m('.video-state', rpc.iceConnectionState), | |||
| m('video', {playsinline: true, oncreate: Video.appendStream(attrs)}), | |||
| ) | |||
| }, | |||
| } | |||
| const Media = { | |||
| constraints: {audio: true, video: {width: {ideal: 320}, facingMode: 'user'}}, | |||
| turnOn: async () => { | |||
| State.media = await navigator.mediaDevices.getUserMedia(Media.constraints) | |||
| const media = await navigator.mediaDevices.getUserMedia(Media.constraints) | |||
| State.media[State.username] = media | |||
| wire({kind: 'peerInfo', value: {type: 'request'}}) | |||
| m.redraw() | |||
| }, | |||
| turnOff: () => { | |||
| wire({kind: 'peerInfo', value: {type: 'stop'}}) | |||
| // signal internally | |||
| Object.keys(rpcs).forEach(source => signal({kind: 'peerInfo', value: {type: 'stop'}, source})) | |||
| State.media.getTracks().map(track => track.stop()) | |||
| delete State.media | |||
| State.online.forEach(signalPeerStop) | |||
| }, | |||
| view() { | |||
| if(!State.media) { | |||
| if(!State.media[State.username]) { | |||
| return m('.media', | |||
| m('button', {onclick: Media.turnOn}, 'turn media on'), | |||
| ) | |||
| @@ -137,8 +139,7 @@ const Media = { | |||
| return m('.media', | |||
| m('button', {onclick: Media.turnOff}, 'turn media off'), | |||
| m('.videos', | |||
| m(Video, {username: State.username, stream: State.media}), | |||
| Object.entries(streams).map(([username, stream]) => | |||
| Object.entries(State.media).map(([username, stream]) => | |||
| m(Video, {username, stream}) | |||
| ), | |||
| ), | |||
| @@ -154,7 +155,7 @@ const Login = { | |||
| connect(username) | |||
| }, | |||
| sendLogout: (e) => { | |||
| State.media && Media.turnOff() | |||
| Media.turnOff() | |||
| wire({kind: 'logout'}) | |||
| State.posts = [] | |||
| }, | |||
| @@ -170,7 +171,7 @@ const Login = { | |||
| m('input', attrs), | |||
| m('button', 'Login'), | |||
| ), | |||
| State.kind === 'error' && m('.error', State.info), | |||
| m('.error', State.info), | |||
| ) | |||
| }, | |||
| } | |||
| @@ -251,3 +252,4 @@ const connect = (username) => { | |||
| if(localStorage.username) { | |||
| connect(localStorage.username) | |||
| } | |||
| addEventListener('pagehide', Media.turnOff) | |||