Bläddra i källkod

Screen sharing works

master
Roderic Day 4 år sedan
förälder
incheckning
4d6d75d1aa
8 ändrade filer med 249 tillägg och 167 borttagningar
  1. +45
    -110
      apps/rpc.js
  2. +133
    -0
      apps/rpcOld.js
  3. +47
    -34
      apps/screen.js
  4. +15
    -11
      apps/streams.js
  5. +7
    -3
      pico.html
  6. +1
    -4
      pico.js
  7. +1
    -2
      pico.py
  8. +0
    -3
      test.py

+ 45
- 110
apps/rpc.js Visa fil

@@ -1,131 +1,66 @@
const connections = {}
const datachannels = {}
const streams = {}
const screen = {}
const allStreams = {}

function setStream({source, stream}) {
if(streams[source] && streams[source].id !== stream.id) {
streams[source].getTracks().forEach(track => track.stop())
}

streams[source] = stream
m.redraw()

if(source === State.username) {
reloadAllStreams()
}
}

function reloadAllStreams() {
State.online.forEach(username => {
const rpc = connections[username]
if(rpc) {
rpc.getSenders().map(s => rpc.removeTrack(s))
}
signal({kind: 'rpc', value: {type: 'request'}, source: username})
})
// https://stackoverflow.com/a/2117523
function uuidv4() {
return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c =>
(c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
)
}

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

rpc.onicecandidate = ({candidate}) => {
if(candidate && candidate.candidate) {
const value = {type: 'candidate', candidate}
wire({kind: 'rpc', value, target})
const value = JSON.parse(JSON.stringify(candidate))
wire({kind: 'ice-candidate', value: {...value, uid}, target})
}
}
rpc.ontrack = ({streams: [stream]}) => {
setStream({source: target, stream})
stream.getTracks().forEach(tr => ScreenShare.stream.addTrack(tr, stream))
}
rpc.onconnectionstatechange = () => {
if(rpc.connectionState === 'failed') {
console.log(target, 'failed, retry!')
wire({kind: 'rpc', value: {type: 'request'}, target})
}
console.log(rpc.connectionState)
}
rpc.ondatachannel = ({channel}) => {
datachannels[target] = channel
datachannels[target].onmessage = ({data}) => console.log(data)
return rpc
}

// for testing purposes
const msg = `rpc established from ${target} to ${State.username}`
datachannels[target].send(msg)
console.log(msg)
}
addEventListener('screen-start', async ({detail}) => {
const {target, stream} = detail.value
const uid = uuidv4()
const rpc = newRPC(uid, target)
allStreams[uid] = {target, rpc}

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})
}
}
stream.getTracks().forEach(tr => rpc.addTrack(tr, stream))
const localOffer = await rpc.createOffer()
await rpc.setLocalDescription(localOffer)

async function handlePeerInfo({source: target, value}) {
const rpc = connections[target]
wire({kind: 'screen-offer', value: {...localOffer, uid}, target})
})

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

const stream = streams[State.username]
if(stream) {
stream.getTracks().forEach(track => {
try { rpc.addTrack(track, stream) }
catch { }
})
}
await rpc.setRemoteDescription(detail.value)
const localAnswer = await rpc.createAnswer()
await rpc.setLocalDescription(localAnswer)

if(value.type === 'request') {
const localOffer = await rpc.createOffer()
await rpc.setLocalDescription(localOffer)
wire({kind: 'rpc', 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: 'rpc', 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)
}
}
wire({kind: 'screen-answer', value: {...localAnswer, uid}, target})
})

function destroyConnection(username) {
if(streams[username]) {
streams[username].getTracks().forEach(t => t.stop())
delete streams[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]
}
}
addEventListener('screen-answer', async ({detail}) => {
await allStreams[detail.value.uid].rpc.setRemoteDescription(detail.value)
})

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('ice-candidate', async ({detail}) => {
await allStreams[detail.value.uid].rpc.addIceCandidate(detail.value)
})

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'))
addEventListener('load', () => {
doNotLog.add('screen-start')
doNotLog.add('screen-offer')
doNotLog.add('screen-answer')
doNotLog.add('ice-candidate')
})

+ 133
- 0
apps/rpcOld.js Visa fil

@@ -0,0 +1,133 @@
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'))

+ 47
- 34
apps/screen.js Visa fil

@@ -1,57 +1,71 @@
const ScreenShareConfig = {
toggle() {
if(ScreenShare.isOn && ScreenShare.isStreaming) {
wire({kind: 'screen-sharing-stop'})
view() {
const start = async () => {
await ScreenShare.getStream()
wire({kind: 'screen-share-start'})
}
const stop = () => wire({kind: 'screen-share-stop'})
if (ScreenShare.source === null) {
return m('button', {onclick: start}, 'start screen sharing')
}
else if (ScreenShare.isSelf) {
return m('button', {onclick: stop}, 'stop screen sharing')
}
else {
ScreenShare.requestScreen()
return m('button[disabled]', `${ScreenShare.source} is screen sharing`)
}
},
view() {
const checked = ScreenShare.isOn
const onclick = ScreenShareConfig.toggle
return m('label.styled',
m('input[type=checkbox]', {checked, onclick}),
'screen-share',
)
}
}
const ScreenShare = {
streamer: null,
stream: null,
get isStreaming() {
return ScreenShare.streamer === State.username
source: null,
stream: new MediaStream(),
get isSelf() {
return ScreenShare.source === State.username
},
get isOn() {
return !! ScreenShare.streamer
async getStream() {
const stream = await navigator.mediaDevices.getDisplayMedia({})
stream.getTracks()
.forEach(tr => ScreenShare.stream.addTrack(tr, stream))
},
async requestScreen() {
// ScreenShare.stream = await navigator.mediaDevices.getDisplayMedia()
// ScreenShare.streamer = State.username
// wire({kind: 'screen-sharing-start'})
async stopStream() {
ScreenShare.stream.getTracks()
.forEach(tr => tr.stop())
ScreenShare.stream = new MediaStream()
},
view() {
const style = {
overflow: 'scroll',
backgroundColor: 'black',
backgroundColor: 'gray',
color: 'white',
fontFamily: 'monospace',
position: 'relative',
}
return ScreenShare.isOn && m('.screen-share', {style},
m('.streamer', `${ScreenShare.streamer}'s stream`),
m('video.screen[playsinline][autoplay]', {srcObject: ScreenShare.stream}),
return m('.screen-share', {style},
ScreenShare.isSelf ? m('span', 'You are sharing your screen') :
ScreenShare.source ? m('video[playsinline][autoplay]', {srcObject: ScreenShare.stream})
: [],
)
},
}
addEventListener('screen-sharing-start', ({detail}) => {
ScreenShare.streamer = detail.source
function sendScreen(target, stream) {
signal({kind: 'screen-start', value: {target, stream}})
}
addEventListener('screen-share-start', async ({detail: {source}}) => {
const isNew = ScreenShare.source === null
ScreenShare.source = source
if(isNew && ScreenShare.isSelf) {
State.others.forEach(user => sendScreen(user, ScreenShare.stream))
}
})
addEventListener('screen-share-stop', () => {
ScreenShare.stopStream()
ScreenShare.source = null
})
addEventListener('screen-sharing-stop', ({detail}) => {
if(ScreenShare.stream) {
ScreenShare.stream.getTracks().forEach(track => track.stop())
ScreenShare.stream = null
addEventListener('join', ({detail: {value: user}}) => {
if(ScreenShare.isSelf) {
wire({kind: 'screen-share-start', target: user})
sendScreen(user, ScreenShare.stream)
}
ScreenShare.streamer = null
})
addEventListener('load', () => {
doNotLog.add('screen-share-start')
@@ -59,4 +73,3 @@ addEventListener('load', () => {
Headers.push([ScreenShareConfig])
Apps.push([ScreenShare, {key: 'screen-share-container'}])
})
setTimeout(ScreenShare.requestScreen, 200)

+ 15
- 11
apps/streams.js Visa fil

@@ -30,18 +30,15 @@ const VideoConfig = {
}
}
const requestStream = async () => {
if(StreamContainer.videoOn || StreamContainer.audioOn) {
await navigator.mediaDevices
.getUserMedia(VideoConfig)
.then(stream =>
signal({kind: 'stream', value: {source: State.username, stream}})
)
.catch(e => console.log(e))
}
else {
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 = {
setUp: (username) => ({dom}) => {
@@ -49,6 +46,13 @@ const Video = {
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}}) {
const styleOuter = {
@@ -79,8 +83,8 @@ const Video = {
),
m('video[playsinline][autoplay]', {
style: styleVideo,
srcObject: streams[username],
oncreate: this.setUp(username),
onremove: this.tearDown(username),
}),
)
},

+ 7
- 3
pico.html Visa fil

@@ -6,11 +6,12 @@
<link rel="stylesheet" href="/apps/chat.css"/>
<script src="/libs/mithril.min.js"></script>
<script src="/libs/marked.min.js"></script>
<!-- <script src="/libs/purify.min.js"></script> -->
<script src="/libs/purify.min.js"></script>
<script src="/pico.js" defer></script>
<script src="/apps/rpc.js"></script>
<script src="/apps/streams.js"></script>
<!-- <script src="/apps/screen.js"></script> -->
<script src="/apps/screen.js"></script>
<!-- <script src="/apps/rpc.js"></script> -->
<!-- <script src="/apps/streams.js"></script> -->
<!-- <script src="/apps/chat.js"></script> -->
<!-- <script src="/apps/volume.js"></script> -->
</head>
@@ -38,6 +39,9 @@ header {
justify-items: start;
margin-right: auto;
}
video {
background-color: black;
}
main {
overflow: hidden;
display: grid;

+ 1
- 4
pico.js Visa fil

@@ -21,6 +21,7 @@ const params = (new URL(document.location)).searchParams
*
*/
addEventListener('resize', () => m.redraw())
// message: {kind, value, target?}
const wire = (message) => State.websocket.send(JSON.stringify(message))
const signal = (message) => dispatchEvent(new CustomEvent(message.kind, {detail: message}))
const listen = (kind, handler) => {
@@ -209,11 +210,7 @@ const Base = {
}
const Headers = [
['button', {onclick: Base.sendLogout}, 'log-out'],
// m('button', {onclick: VolumeMap.toggle}, 'volume'),
// m('button', {onclick: ScreenShare.toggle}, 'screen'),
]
const Apps = [
// m(Shadow, {key: 'map-shadow', app: VolumeMap}),
// m(Shadow, {key: 'screen-shadow', app: ScreenShare}),
]
addEventListener('load', () => m.mount(document.body, Base))

+ 1
- 2
pico.py Visa fil

@@ -127,8 +127,7 @@ async def handle(ws, path, server_name):
else:
value = data.get('value')
if 'target' in data:
recipients = {username, data['target']}
targets = {v for k, v in room.items() if k in recipients}
targets = {v for k, v in room.items() if k == data['target']}
await broadcast(source=username, value=value, targets=targets)
else:
await broadcast(source=username, value=value)

+ 0
- 3
test.py Visa fil

@@ -147,7 +147,6 @@ def test_private_message():
- {'kind': 'state', 'online': ['Norman', 'Ray']}
- {'kind': 'state', 'online': ['Norman', 'Ray', 'Emma']}
+ {'kind': 'post', 'value': '1', 'target': 'Ray'}
- {'kind': 'post', 'value': '1', 'source': 'Norman'}
- {'kind': 'post', 'value': '3', 'source': 'Emma'}
)
client2 = _make_client('ws://localhost:8642/x', 0.11, Script()
@@ -157,7 +156,6 @@ def test_private_message():
- {'kind': 'state', 'online': ['Norman', 'Ray', 'Emma']}
- {'kind': 'post', 'value': '1', 'source': 'Norman'}
+ {'kind': 'post', 'value': '2', 'target': 'Emma'}
- {'kind': 'post', 'value': '2', 'source': 'Ray'}
- {'kind': 'state', 'online': ['Ray', 'Emma']}
)
client3 = _make_client('ws://localhost:8642/x', 0.12, Script()
@@ -166,7 +164,6 @@ def test_private_message():
- {'kind': 'state', 'online': ['Norman', 'Ray', 'Emma']}
- {'kind': 'post', 'value': '2', 'source': 'Ray'}
+ {'kind': 'post', 'value': '3', 'target': 'Norman'}
- {'kind': 'post', 'value': '3', 'source': 'Emma'}
- {'kind': 'state', 'online': ['Ray', 'Emma']}
- {'kind': 'state', 'online': ['Emma']}
)

Laddar…
Avbryt
Spara