Browse Source

Inroads

master
Roderic Day 5 years ago
parent
commit
d66ce19889
3 changed files with 159 additions and 65 deletions
  1. +1
    -1
      makefile
  2. +13
    -6
      pico.css
  3. +145
    -58
      pico.js

+ 1
- 1
makefile View File

@@ -1,4 +1,4 @@
default: test
default: deploy

test: venv/
venv/bin/python -u test.py

+ 13
- 6
pico.css View File

@@ -5,13 +5,12 @@ body {
.chat {
display: grid;
grid-template-areas:
'posts users'
'actions users'
'posts online'
'actions online'
;
grid-template-columns: 1fr auto;
grid-template-rows: 1fr auto auto;
grid-template-rows: 150px auto;

height: 150px;
position: fixed;
top: 0;

@@ -26,8 +25,8 @@ body {
.source {
font-weight: bold;
}
.users {
grid-area: users;
.online {
grid-area: online;
}
.posts {
grid-area: posts;
@@ -40,3 +39,11 @@ body {
display: inline;
padding-left: var(--pad);
}
.video-container {
float: left;
}
video {
width: 120px;
transform: scaleX(-1);
background-color: black;
}

+ 145
- 58
pico.js View File

@@ -1,52 +1,89 @@
const State = {
websocket: null,
online: [],
posts: [],
users: [],
media: null,
username: null,
}

/*
*
* WEBRTC
* SIGNALING
*
*/
const rpcCall = async (username) => {
const constraints = {audio: true, video: true}
const media = await navigator.mediaDevices.getUserMedia(constraints)
const rpc = new RTCPeerConnection({iceServers: []})
// rpc.oniceconnectionstatechange = (e) => {
// console.log(e)
// }
// rpc.onicecandidate = (e) => {
// e.candidate
// const candidate = new RTCIceCandidate();
// rpc.addIceCandidate(candidate);
// }
// rpc.ontrack = (e) => {
// console.log(e)
// // appendVideo(media)
// }
const wire = (message) => State.websocket.send(JSON.stringify(message))
const signal = (message) => dispatchEvent(new CustomEvent(message.kind, {detail: message}))
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'])

const makeOffer = async () => {
rpc.addStream(media)
const offer = await rpc.createOffer()
await rpc.setLocalDescription(offer)
return {msgType: 'offer', rsd: rpc.localDescription}
/*
*
* WEBRTC
*
*/
const rpcs = {}
const streams = {}
const getOrCreateRpc = (username) => {
if(!rpcs[username] && State.media) {
const rpc = new RTCPeerConnection({iceServers: [{urls: 'stun:stun.sipgate.net:3478'}]})
State.media.getTracks().forEach(track => rpc.addTrack(track, State.media))
rpc.onicecandidate = ({candidate}) => {
if(candidate) {
wire({kind: 'peerInfo', value: {type: 'candidate', candidate}})
}
}
rpc.ontrack = (e) => {
streams[username] = e.streams[0]
m.redraw()
}
rpc.onclose = (e) => {
console.log(username, e)
}
rpcs[username] = rpc
}

const makeAnswer = async (msg) => {
const offer = new RTCSessionDescription(msg.rsd)
await rpc.setRemoteDescription(offer)
const answer = await rpc.createAnswer()
await rpc.setLocalDescription(answer)
return {msgType: 'answer', rsd: rpc.localDescription}
return rpcs[username]
}
const onPeerInfo = async ({detail: message}) => {
if(State.username === message.source) {
return
}

const finishShake = async (msg) => {
const answer = new RTCSessionDescription(msg.rsd)
await rpc.setRemoteDescription(answer)

const rpc = getOrCreateRpc(message.source)
if(rpc && message.value.type === 'request') {
const localOffer = await rpc.createOffer()
await rpc.setLocalDescription(localOffer)
wire({kind: 'peerInfo', value: localOffer})
}
else if(rpc && message.value.type === 'offer') {
const remoteOffer = new RTCSessionDescription(message.value)
await rpc.setRemoteDescription(remoteOffer)
const localAnswer = await rpc.createAnswer()
await rpc.setLocalDescription(localAnswer)
wire({kind: 'peerInfo', value: localAnswer})
}
else if(rpc && message.value.type === 'answer') {
const remoteAnswer = new RTCSessionDescription(message.value)
await rpc.setRemoteDescription(remoteAnswer)
}
else if(rpc && message.value.type === 'candidate') {
const candidate = new RTCIceCandidate(message.value.candidate)
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(rpcs[message.source]) {
rpcs[message.source].close()
delete rpcs[message.source]
}
}
else {
console.log('uncaught', message)
}
finishShake(await makeAnswer(await makeOffer()))
}

/*
@@ -63,6 +100,52 @@ const scrollIntoView = (vnode) => {
const prettyTime = (ts) => {
return ts.slice(11, 19)
}
const Video = {
appendStream: (stream) => ({dom}) => {
dom.autoplay = true
dom.muted = true
dom.srcObject = stream
},
view({attrs}) {
return m('.video-container',
m('.video-source', attrs.username),
m('video', {playsinline: true, oncreate: Video.appendStream(attrs.stream)}),
)
},
}
const Media = {
constraints: {audio: true, video: {width: {ideal: 320}, facingMode: 'user'}},
turnOn: async () => {
State.media = await navigator.mediaDevices.getUserMedia(Media.constraints)
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
},
view() {
if(!State.media) {
return m('.media',
m('button', {onclick: Media.turnOn}, 'turn media on'),
)
}
else {
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]) =>
m(Video, {username, stream})
),
),
)
}
}
}
const Login = {
sendLogin: (e) => {
e.preventDefault()
@@ -71,13 +154,20 @@ const Login = {
connect(username)
},
sendLogout: (e) => {
State.websocket.send(JSON.stringify({action: 'logout'}))
State.media && Media.turnOff()
wire({kind: 'logout'})
State.posts = []
},
view() {
const attrs = {
oncreate: autoFocus,
name: 'username',
autocomplete: 'off',
value: localStorage.username,
}
return m('.login',
m('form', {onsubmit: Login.sendLogin},
m('input', {oncreate: autoFocus, name: 'username', autocomplete: 'off', value: localStorage.username}),
m('input', attrs),
m('button', 'Login'),
),
State.kind === 'error' && m('.error', State.info),
@@ -88,26 +178,29 @@ const Chat = {
sendPost: (e) => {
e.preventDefault()
const field = e.target.text
State.websocket.send(JSON.stringify({action: 'post', text: field.value}))
field.value = ''
if(field.value) {
wire({kind: 'post', value: field.value})
field.value = ''
}
},
view() {
return m('.chat',
m('.posts',
State.posts.map(post => m('.post', {oncreate: scrollIntoView},
m('.ts', prettyTime(post.ts)),
m('.source', post.source),
m('.text', post.text),
m('.source', post.source || '~'),
m('.text', post.value),
)),
),
m('form.actions', {onsubmit: Chat.sendPost},
m('input', {oncreate: autoFocus, name: 'text', autocomplete: 'off'}),
m('button', 'Send'),
),
m('.users',
m('.online',
m('button', {onclick: Login.sendLogout}, 'Logout'),
m('ul.user-list', State.users.map(username => m('li', username))),
m('ul.user-list', State.online.map(username => m('li', username))),
),
m(Media),
)
},
}
@@ -130,27 +223,21 @@ const connect = (username) => {
State.websocket = new WebSocket(wsUrl)

State.websocket.onopen = (e) => {
State.websocket.send(JSON.stringify({action: 'login', username}))
wire({kind: 'login', value: username})
}

State.websocket.onmessage = (e) => {
const message = JSON.parse(e.data)
if(message.kind === 'post') {
State.posts.push(message)
}
else if(message.kind === 'update') {
Object.assign(State, message)
if(message.info) {
State.posts.push({ts: message.ts, source: '~', text: message.info})
}
}
else if(message.kind === 'error') {
Object.assign(State, message)
Login.sendLogout()

if(message.online) {
State.online = message.online
}
else {

if(!doNotLog.has(message.kind)) {
console.log(message)
}
signal(message)

m.redraw()
}


Loading…
Cancel
Save