Roderic Day 4 лет назад
Родитель
Сommit
68255ad6ce
5 измененных файлов: 132 добавлений и 223 удалений
  1. +53
    -28
      apps/rpc.js
  2. +0
    -133
      apps/rpcOld.js
  3. +8
    -0
      apps/screen.js
  4. +68
    -59
      apps/video.js
  5. +3
    -3
      pico.html

+ 53
- 28
apps/rpc.js Просмотреть файл

const allStreams = {}
const allRPCs = {}


// https://stackoverflow.com/a/2117523 // https://stackoverflow.com/a/2117523
function uuidv4() { function uuidv4() {
) )
} }


function newRPC(uid, target) {
addEventListener('rpc-needed', ({detail}) => {
const {kind, target} = detail.value
const isInitiatedLocally = !detail.source
const uid = detail.value.uid || uuidv4()
const rpc = new RTCPeerConnection(rpcConfig) const rpc = new RTCPeerConnection(rpcConfig)
rpc.onicecandidate = ({candidate}) => { rpc.onicecandidate = ({candidate}) => {
if(candidate && candidate.candidate) { if(candidate && candidate.candidate) {
const value = JSON.parse(JSON.stringify(candidate))
wire({kind: 'ice-candidate', value: {...value, uid}, target})
const cand = JSON.parse(JSON.stringify(candidate))
wire({kind: 'ice-candidate', value: {...cand, uid}, target})
} }
} }
rpc.ontrack = ({streams: [stream]}) => { rpc.ontrack = ({streams: [stream]}) => {
stream.getTracks().forEach(tr => ScreenShare.stream.addTrack(tr, stream))
stream.getTracks().forEach(track =>
allRPCs[uid].receiver.addTrack(track, stream)
)
} }
rpc.onconnectionstatechange = () => {
console.log(rpc.connectionState)
rpc.oniceconnectionstatechange = (e) => {
console.log(rpc.iceConnectionState)
} }
return rpc
}
rpc.onnegotiationneeded = (e) => {
// if(isInitiatedLocally) {
// signal({kind: 'rpc-initiate', value: {uid}})
// }
}
allRPCs[uid] = {kind, target, rpc, isInitiatedLocally}
signal({kind: 'rpc-new', value: {kind, target, uid}})
if(isInitiatedLocally) {
wire({kind: 'rpc-needed', value: {kind, target: State.username, uid}, target})
}
})

addEventListener('rpc-setup', async ({detail}) => {
const {uid, sender, receiver} = detail.value
allRPCs[uid].sender = sender
allRPCs[uid].receiver = receiver

if(sender) {
sender.getTracks().forEach(tr => allRPCs[uid].rpc.addTrack(tr, sender))
}

if(allRPCs[uid].isInitiatedLocally) {
signal({kind: 'rpc-initiate', value: {uid}})
}
})


addEventListener('screen-start', async ({detail}) => {
const {target, stream} = detail.value
const uid = uuidv4()
const rpc = newRPC(uid, target)
allStreams[uid] = {target, rpc}
addEventListener('rpc-initiate', async({detail}) => {
const {uid} = detail.value
const {rpc, target} = allRPCs[uid]


stream.getTracks().forEach(tr => rpc.addTrack(tr, stream))
const localOffer = await rpc.createOffer() const localOffer = await rpc.createOffer()
await rpc.setLocalDescription(localOffer) await rpc.setLocalDescription(localOffer)


wire({kind: 'screen-offer', value: {...localOffer, uid}, target})
wire({kind: 'rpc-offer', value: {...localOffer, uid}, target})
}) })


addEventListener('screen-offer', async ({detail}) => {
const target = detail.source
const uid = detail.value.uid
const rpc = newRPC(uid, target)
allStreams[uid] = {target, rpc}
addEventListener('rpc-offer', async ({detail}) => {
const {uid} = detail.value
const {rpc, target} = allRPCs[uid]


await rpc.setRemoteDescription(detail.value) await rpc.setRemoteDescription(detail.value)
const localAnswer = await rpc.createAnswer() const localAnswer = await rpc.createAnswer()
await rpc.setLocalDescription(localAnswer) await rpc.setLocalDescription(localAnswer)


wire({kind: 'screen-answer', value: {...localAnswer, uid}, target})
wire({kind: 'rpc-answer', value: {...localAnswer, uid}, target})
}) })


addEventListener('screen-answer', async ({detail}) => {
await allStreams[detail.value.uid].rpc.setRemoteDescription(detail.value)
addEventListener('rpc-answer', async ({detail}) => {
const {uid} = detail.value
await allRPCs[uid].rpc.setRemoteDescription(detail.value)
}) })


addEventListener('ice-candidate', async ({detail}) => { addEventListener('ice-candidate', async ({detail}) => {
await allStreams[detail.value.uid].rpc.addIceCandidate(detail.value)
const {uid} = detail.value
await allRPCs[uid].rpc.addIceCandidate(detail.value)
}) })


addEventListener('load', () => { addEventListener('load', () => {
doNotLog.add('screen-start')
doNotLog.add('screen-offer')
doNotLog.add('screen-answer')
doNotLog.add('rpc-needed')
doNotLog.add('rpc-offer')
doNotLog.add('rpc-answer')
doNotLog.add('ice-candidate') doNotLog.add('ice-candidate')
}) })

+ 0
- 133
apps/rpcOld.js Просмотреть файл

const connections = {}
const datachannels = {}
const videoElements = {}

function setStream({source, stream}) {
const element = videoElements[source]
const active = element && element.srcObject
if(active && active.id !== stream.id) {
videoElements[source].srcObject.getTracks().forEach(tr => tr.stop())
}

element.srcObject = stream

if(source === State.username) {
signal({kind: 'stream-refresh'})
}
}

function reloadAllStreams() {
State.online.forEach(username => {
const rpc = connections[username]
if(rpc) {
rpc.getSenders().map(s => console.log(s) || rpc.removeTrack(s))
}
signal({kind: 'rpc', value: {type: 'request'}, source: username})
})
}

function createConnection(target) {
const rpc = new RTCPeerConnection(rpcConfig)

rpc.onicecandidate = ({candidate}) => {
if(candidate && candidate.candidate) {
const value = {type: 'candidate', candidate}
wire({kind: 'rpc', value, target})
}
}
rpc.ontrack = ({streams: [stream]}) => {
setStream({source: target, stream})
}
rpc.onconnectionstatechange = () => {
if(rpc.connectionState === 'failed') {
console.log(target, 'failed, retry!')
wire({kind: 'rpc', value: {type: 'request'}, target})
}
}
rpc.ondatachannel = ({channel}) => {
datachannels[target] = channel
datachannels[target].onmessage = ({data}) => console.log(data)

// for testing purposes
const msg = `rpc established from ${target} to ${State.username}`
datachannels[target].send(msg)
console.log(msg)
}

rpc.onnegotiationneeded = () => {
console.log('lmao renegotiate bi')
}

connections[target] = rpc
if(State.username > target) {
datachannels[target] = rpc.createDataChannel('test')
datachannels[target].onmessage = ({data}) => console.log(data)
signal({kind: 'rpc', value: {type: 'request'}, source: target})
}
}

function setSenders(rpc, username) {
const element = videoElements[username]
const stream = element && element.srcObject
rpc.getSenders().forEach(se => rpc.removeTrack(se))
if(stream) {
stream.getTracks().forEach(tr => rpc.addTrack(tr, stream))
}
}

async function handlePeerInfo({source: target, value, kind}) {
const rpc = connections[target]

if(!rpc) {
return
}

if(value.type === 'request') {
const localOffer = await rpc.createOffer()
await rpc.setLocalDescription(localOffer)
wire({kind, value: localOffer, target})
}
else if(value.type === 'offer') {
const remoteOffer = new RTCSessionDescription(value)
await rpc.setRemoteDescription(remoteOffer)
const localAnswer = await rpc.createAnswer()
await rpc.setLocalDescription(localAnswer)
wire({kind, value: localAnswer, target})
}
else if(value.type === 'answer') {
const remoteAnswer = new RTCSessionDescription(value)
await rpc.setRemoteDescription(remoteAnswer).catch(e => e)
}
else if(value.type === 'candidate') {
const candidate = new RTCIceCandidate(value.candidate)
await rpc.addIceCandidate(candidate).catch(e => e)
}
}

function destroyConnection(username) {
if(datachannels[username]) {
datachannels[username].close()
delete datachannels[username]
}
if(connections[username]) {
connections[username].getReceivers().forEach(r => r.track.stop())
connections[username].close()
delete connections[username]
}
}

function destroyAll() {
const people = new Set()
const collect = [connections, datachannels, streams, screen]
.map(collection => Object.keys(collection))
.forEach(keys => keys.forEach(key => people.add(key)))
people.forEach(destroyConnection)
}

addEventListener('stream-refresh', reloadAllStreams)
addEventListener('stream', (e) => setStream(e.detail.value))
addEventListener('rpc', (e) => handlePeerInfo(e.detail))
addEventListener('join', (e) => createConnection(e.detail.value))
addEventListener('leave', (e) => destroyConnection(e.detail.value))
addEventListener('logout', () => destroyAll())
addEventListener('load', () => doNotLog.add('rpc'))

+ 8
- 0
apps/screen.js Просмотреть файл

addEventListener('rpc-new', ({detail}) => {
const {uid, kind} = detail.value
if(kind === 'screen') {
const sender = ScreenShare.isSelf && ScreenShare.stream
const receiver = !ScreenShare.isSelf && ScreenShare.stream
signal({kind: 'rpc-setup', value: {uid, sender, receiver}})
}
})
const ScreenShareConfig = { const ScreenShareConfig = {
view() { view() {
const start = async () => { const start = async () => {

apps/streams.js → apps/video.js Просмотреть файл

const Toggle = {
view({attrs: {label, value}}) {
const onclick = () => {
StreamContainer[value] = !StreamContainer[value]
requestStream()
}
const checked = StreamContainer[value]
return m('label.styled',
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'}),
]
addEventListener('rpc-new', ({detail}) => {
const {uid, kind, target} = detail.value
if(kind === 'video') {
const sender = VideoShare.streams[State.username]
const receiver = VideoShare.resetStream(target)
signal({kind: 'rpc-setup', value: {uid, sender, receiver}})
} }
}
const requestStream = async () => {
await navigator.mediaDevices
.getUserMedia(VideoConfig)
.then(stream =>
signal({kind: 'stream', value: {source: State.username, stream}})
)
.catch(() => {
const stream = new MediaStream()
signal({kind: 'stream', value: {source: State.username, stream}})
})
}
})
const Video = { const Video = {
setUp: (username) => ({dom}) => {
if(username === State.username) {
dom.muted = true
requestStream()
}
videoElements[username] = dom
},
tearDown: (username) => () => {
const stream = videoElements[username].srcObject
if(stream) stream.getTracks().forEach(tr => tr.stop())
videoElements[username] = null
delete videoElements[username]
},
view({attrs: {username}}) { view({attrs: {username}}) {
const styleOuter = { const styleOuter = {
position: 'relative', position: 'relative',
), ),
m('video[playsinline][autoplay]', { m('video[playsinline][autoplay]', {
style: styleVideo, style: styleVideo,
oncreate: this.setUp(username),
onremove: this.tearDown(username),
srcObject: VideoShare.streams[username],
oncreate: ({dom}) => {dom.muted = username === State.username},
onremove: () => VideoShare.resetStream(username),
}), }),
) )
}, },
} }
const StreamContainer = {
const Toggle = {
view({attrs: {key, label}}) {
const onclick = () => {
VideoShare[key] = !VideoShare[key]
VideoShare.getStream()
}
const checked = VideoShare[key]
return m('label.styled',
m('input[type=checkbox]', {onclick, checked}),
label,
)
},
}
const VideoShareConfig = {
get video() {
return VideoShare.videoOn
&& State.online.length < 10
&& params.get('v') !== '0'
&& {width: {ideal: 320}, facingMode: 'user', frameRate: 26}
},
get audio() {
return VideoShare.audioOn
&& params.get('a') !== '0'
},
view() {
return [
m(Toggle, {key: 'videoOn', label: 'video'}),
m(Toggle, {key: 'audioOn', label: 'audio'}),
]
},
}
const VideoShare = {
videoOn: true, videoOn: true,
audioOn: true, audioOn: true,
streams: {},
oncreate(e) {
VideoShare.getStream()
},
resetStream(target) {
if(VideoShare.streams[target]) {
VideoShare.streams[target].getTracks().forEach(tr => tr.stop())
}
VideoShare.streams[target] = new MediaStream()
return VideoShare.streams[target]
},
async getStream() {
VideoShare.resetStream(State.username)
await navigator.mediaDevices.getUserMedia(VideoShareConfig)
.catch(error => console.error(error))
.then(stream => {VideoShare.streams[State.username] = stream})
m.redraw()
State.others.forEach(target =>
signal({kind: 'rpc-needed', value: {kind: 'video', target}})
)
},
view() { view() {
const dims = [ const dims = [
Math.floor((1 + Math.sqrt(4 * State.online.length - 3)) / 2), Math.floor((1 + Math.sqrt(4 * State.online.length - 3)) / 2),
) )
}, },
} }
addEventListener('join', ({detail: {value: target}}) => {
signal({kind: 'rpc-needed', value: {kind: 'video', target}})
})
addEventListener('load', () => { addEventListener('load', () => {
Headers.push([VideoConfig])
Apps.push([StreamContainer, {key: 'stream-container'}])
Headers.push([VideoShareConfig])
Apps.push([VideoShare, {key: 'stream-container'}])
}) })

+ 3
- 3
pico.html Просмотреть файл

<script src="/libs/purify.min.js"></script> <script src="/libs/purify.min.js"></script>
<script src="/pico.js" defer></script> <script src="/pico.js" defer></script>
<script src="/apps/rpc.js"></script> <script src="/apps/rpc.js"></script>
<script src="/apps/screen.js"></script>
<!-- <script src="/apps/rpc.js"></script> -->
<!-- <script src="/apps/streams.js"></script> -->
<script src="/apps/video.js"></script>
<!-- <script src="/apps/screen.js"></script> -->
<!-- <script src="/apps/chat.js"></script> --> <!-- <script src="/apps/chat.js"></script> -->
<!-- <script src="/apps/volume.js"></script> --> <!-- <script src="/apps/volume.js"></script> -->
</head> </head>
justify-content: center; justify-content: center;
align-items: center; align-items: center;
padding: 0 0.5em; padding: 0 0.5em;
user-select: none;
} }
header { header {
display: grid; display: grid;

Загрузка…
Отмена
Сохранить