1 const body
= document
.createElement('body')
2 const root
= document
.createElement('div')
3 document
.title
= "Strapp.io Client"
4 const conf
= {"iceServers": [{ "urls": "stun:stun.1.google.com:19302" }] }
8 /* TODO: duplicate in both client.js and host.js */
9 function getPublicKey() {
10 return new Promise( (resolve
, reject
) => {
11 /* Check local storage for public key */
12 if (!window
.localStorage
.getItem('public-key')) {
13 /* If doesn't exist, generate public and private key pair, store in
15 crypto
.subtle
.generateKey(
18 publicExponent
: new Uint8Array([0x01, 0x00, 0x01]),
19 hash
: {name
: "SHA-256"}
22 ['encrypt', 'decrypt']
24 /* TODO: Do we need to store the private key as well? */
25 crypto
.subtle
.exportKey('jwk', keyPair
.publicKey
)
26 .then((exportedKey
) => {
27 window
.localStorage
.setItem('publicKey', exportedKey
)
28 console
.log('public key is' + window
.localStorage
.getItem('publicKey'))
35 resolve(window
.localStorage
.getItem('publicKey'))
41 function postServer(url
, data
) {
42 const request
= new XMLHttpRequest()
43 request
.open('POST', url
, true)
44 request
.setRequestHeader('Content-Type', 'application/json' )
45 request
.setRequestHeader('X-Strapp-Type', 'ice-candidate-submission')
49 /* TODO: All this does is wrap a function in a promise. Allows pollServerForAnswer
50 to call itself recursively with the same promise */
51 function pollServer(url
, clientPubKey
, func
) {
52 return new Promise((resolve
, reject
) => {
53 func(url
, clientPubKey
, resolve
, reject
)
57 /* Poll the server. Send get request, wait for timeout, send another request.
58 Do this until...? Can be used for either reconnecting or waiting for answer*/
59 function pollServerForAnswer(url
, data
, resolve
, reject
) {
60 const request
= new XMLHttpRequest()
61 request
.open('GET', url
, true)
62 /* But there is no JSON? */
63 request
.setRequestHeader('Content-Type', 'application/json' )
64 request
.setRequestHeader('X-Strapp-Type', 'client-sdp-offer')
65 request
.setRequestHeader('X-Client-Offer', JSON
.stringify(data
))
66 request
.onreadystatechange
= () => {
67 if (request
.status
=== 200) {
68 if(request
.readyState
=== 4) {
69 console
.log('Client: Recieved Answer from Host')
71 resolve(request
.response
)
74 else if (request
.status
=== 504) {
75 console
.log('timed out, resending')
76 pollServerForAnswer(url
, data
, resolve
, reject
)
79 reject('server unhandled response of status ' + request
.status
)
85 /* Poll server for ice candidates until ice is complete */
86 function pollServerForICECandidate(cpc
, url
, pubKey
) {
87 let intervalID
= window
.setInterval(() => {
88 if (cpc
.iceConnectionState
.localeCompare('connected') !== 0
89 && cpc
.iceConnectionState
.localeCompare('completed') !== 0) {
90 console
.log('Client: Polling server begin for intervalID = ' + intervalID
)
91 console
.log('Client: Requesting ICE Candidates from server')
92 const request
= new XMLHttpRequest()
93 request
.open('GET', url
, true)
94 request
.setRequestHeader('Content-Type', 'application/json' )
95 request
.setRequestHeader('X-Strapp-Type', 'ice-candidate-request')
96 request
.setRequestHeader('X-client-pubkey', pubKey
)
97 request
.onreadystatechange
= () => {
98 if (request
.status
=== 200) {
99 if(request
.readyState
=== 4) {
100 console
.log('Client: Recieved ICE response from Host')
101 let response
= JSON
.parse(request
.response
)
102 switch(response
['iceState']) {
104 cpc
.addIceCandidate(new RTCIceCandidate(response
.ice
))
106 case "g": /* Gathering so let interval keep polling */
108 case "c": /* host iceState == Complete, stop bugging it */
109 clearInterval(intervalID
)
113 console
.log('Unhandled iceState in pollServerForICECandidate()' + response
['iceState'])
119 console
.log('server unhandled response of status ' + request
.status
)
120 clearInterval(intervalID
)
127 clearInterval(intervalID
)
132 /* Create and send offer -> Send ICE Candidates -> Poll for ICE Candidates */
133 getPublicKey().then((cpk
) => {
134 console
.log('Client: Create and send offer')
135 const cpc
= new RTCPeerConnection(conf
)
137 cpc
.oniceconnectionstatechange
= () => {
138 console
.log('iceConnectionState = ' + cpc
.iceConnectionState
)
141 cpc
.onnegotiationneeded
= () => {
142 console
.log('negotiation needed!')
143 cpc
.createOffer().then((offer
) => {
144 return cpc
.setLocalDescription(offer
)
147 console
.log('Client: Sending offer to host')
150 sdp
: cpc
.localDescription
,
153 return pollServer(window
.location
, offer
, pollServerForAnswer
)
154 }).then((serverResponse
) => {
155 const answer
= JSON
.parse(serverResponse
)
156 console
.log('Client: Polling for ICE candidates')
157 pollServerForICECandidate(cpc
, window
.location
, cpk
.n
)
158 cpc
.setRemoteDescription(answer
.sdp
)
159 cpc
.onicecandidate
= (event
) => {
160 if (event
.candidate
) {
161 console
.log('Client: Sending ice candidate to host')
162 postServer(window
.location
, JSON
.stringify({
164 ice
: event
.candidate
,
169 console
.log('Client: No more Ice Candidates to send')
175 console
.log('error in sdp handshake: ' + err
)
178 /* Start data channel */
179 dataChannel
= cpc
.createDataChannel("sendChannel");
180 cpc
.ontrack
= (event
) => {
181 console
.log(`track event is ${event}`)
182 let remoteRTPSenders
= cpc
.getSenders()
183 let remoteRTPReceivers
= cpc
.getReceivers()
184 console
.log(remoteRTPReceivers
)
185 /* Add each remoteRTPSenders.track to own stream */
186 let video
= document
.querySelector('video')
187 video
.autoplay
= true
189 hostScreen
= new MediaStream([remoteRTPReceivers
[0].track
])
190 if(!video
.srcObject
) {
191 video
.srcObject
= hostScreen
193 console
.log(hostScreen
.getVideoTracks())
194 console
.log(video
.srcObject
)
196 video
.onloadedmetadata
= () => {
200 dataChannel
.onmessage
= (msg
) => {
201 /* Get mediaStream from host and add it to the video */
202 let hostMessage
= JSON
.parse(msg
.data
)
203 cpc
.setRemoteDescription(hostMessage
.sdp
).then(() => {
204 cpc
.createAnswer().then((answer
) => {
205 return cpc
.setLocalDescription(answer
)
207 dataChannel
.send(JSON
.stringify({
208 "cmd": "> screen dataChannel",
209 "sdp": cpc
.localDescription
216 dataChannel
.onopen
= () => {
217 document
.body
.innerHTML
= (`<div><button> Connection with host established! </button></div> <video controls></video>`)
221 document
.addEventListener('DOMContentLoaded', () => {
223 document
.body
.innerHTML
= `<button> Setting up connection with host </button>`