瀏覽代碼

Inroads

master
Roderic Day 5 年之前
父節點
當前提交
d66ce19889
共有 3 個文件被更改,包括 159 次插入65 次删除
  1. +1
    -1
      makefile
  2. +13
    -6
      pico.css
  3. +145
    -58
      pico.js

+ 1
- 1
makefile 查看文件

default: test
default: deploy


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

+ 13
- 6
pico.css 查看文件

.chat { .chat {
display: grid; display: grid;
grid-template-areas: grid-template-areas:
'posts users'
'actions users'
'posts online'
'actions online'
; ;
grid-template-columns: 1fr auto; grid-template-columns: 1fr auto;
grid-template-rows: 1fr auto auto;
grid-template-rows: 150px auto;


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


.source { .source {
font-weight: bold; font-weight: bold;
} }
.users {
grid-area: users;
.online {
grid-area: online;
} }
.posts { .posts {
grid-area: posts; grid-area: posts;
display: inline; display: inline;
padding-left: var(--pad); padding-left: var(--pad);
} }
.video-container {
float: left;
}
video {
width: 120px;
transform: scaleX(-1);
background-color: black;
}

+ 145
- 58
pico.js 查看文件

const State = { const State = {
websocket: null, websocket: null,
online: [],
posts: [], 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()))
} }


/* /*
const prettyTime = (ts) => { const prettyTime = (ts) => {
return ts.slice(11, 19) 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 = { const Login = {
sendLogin: (e) => { sendLogin: (e) => {
e.preventDefault() e.preventDefault()
connect(username) connect(username)
}, },
sendLogout: (e) => { sendLogout: (e) => {
State.websocket.send(JSON.stringify({action: 'logout'}))
State.media && Media.turnOff()
wire({kind: 'logout'})
State.posts = [] State.posts = []
}, },
view() { view() {
const attrs = {
oncreate: autoFocus,
name: 'username',
autocomplete: 'off',
value: localStorage.username,
}
return m('.login', return m('.login',
m('form', {onsubmit: Login.sendLogin}, m('form', {onsubmit: Login.sendLogin},
m('input', {oncreate: autoFocus, name: 'username', autocomplete: 'off', value: localStorage.username}),
m('input', attrs),
m('button', 'Login'), m('button', 'Login'),
), ),
State.kind === 'error' && m('.error', State.info), State.kind === 'error' && m('.error', State.info),
sendPost: (e) => { sendPost: (e) => {
e.preventDefault() e.preventDefault()
const field = e.target.text 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() { view() {
return m('.chat', return m('.chat',
m('.posts', m('.posts',
State.posts.map(post => m('.post', {oncreate: scrollIntoView}, State.posts.map(post => m('.post', {oncreate: scrollIntoView},
m('.ts', prettyTime(post.ts)), 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('form.actions', {onsubmit: Chat.sendPost},
m('input', {oncreate: autoFocus, name: 'text', autocomplete: 'off'}), m('input', {oncreate: autoFocus, name: 'text', autocomplete: 'off'}),
m('button', 'Send'), m('button', 'Send'),
), ),
m('.users',
m('.online',
m('button', {onclick: Login.sendLogout}, 'Logout'), 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),
) )
}, },
} }
State.websocket = new WebSocket(wsUrl) State.websocket = new WebSocket(wsUrl)


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


State.websocket.onmessage = (e) => { State.websocket.onmessage = (e) => {
const message = JSON.parse(e.data) 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) console.log(message)
} }
signal(message)

m.redraw() m.redraw()
} }



Loading…
取消
儲存