Tyler Chatow | b3850c1 | 2020-02-26 20:55:48 -0800 | [diff] [blame^] | 1 | import {Builder, ByteBuffer} from 'flatbuffers'; |
| 2 | import {Payload, SdpType, WebSocketIce, WebSocketMessage, WebSocketSdp} from 'org_frc971/aos/network/web_proxy_generated'; |
| 3 | |
| 4 | // Port 9 is used to indicate an active (outgoing) TCP connection. The server |
| 5 | // would send a corresponding candidate with the actual TCP port it is |
| 6 | // listening on. Ignore NaN since it doesn't tell us anything about whether the |
| 7 | // port selected will have issues through the FMS firewall. |
| 8 | function validPort(port: number): boolean { |
| 9 | return Number.isNaN(port) || port == 9 || (port >= 1180 && port <= 1190) || |
| 10 | (port >= 5800 && port <= 5810); |
| 11 | } |
| 12 | |
| 13 | // Some browsers don't support the port property so provide our own function |
| 14 | // to get it. |
| 15 | function getIcePort(candidate: RTCIceCandidate): number { |
| 16 | if (candidate.port === undefined) { |
| 17 | return Number(candidate.candidate.split(' ')[5]); |
| 18 | } else { |
| 19 | return candidate.port; |
| 20 | } |
| 21 | } |
| 22 | |
| 23 | function isMdnsAddress(address: string): boolean { |
| 24 | return address.includes('.local'); |
| 25 | } |
| 26 | |
| 27 | function getIceAddress(candidate: RTCIceCandidate): string { |
| 28 | if (candidate.address === undefined) { |
| 29 | return candidate.candidate.split(' ')[4]; |
| 30 | } else { |
| 31 | return candidate.address; |
| 32 | } |
| 33 | } |
| 34 | |
| 35 | export class Connection { |
| 36 | private webSocketConnection: WebSocket|null = null; |
| 37 | private rtcPeerConnection: RTCPeerConnection|null = null; |
| 38 | private html5VideoElement: HTMLMediaElement|null = null; |
| 39 | private webSocketUrl: string; |
| 40 | private statsInterval: number; |
| 41 | |
| 42 | private candidateNominatedId: string; |
| 43 | private lastRtpTimestamp: number = 0; |
| 44 | private lastBytesReceived: number = 0; |
| 45 | private lastFramesDecoded: number = 0; |
| 46 | |
| 47 | |
| 48 | constructor() { |
| 49 | const server = location.host; |
| 50 | this.webSocketUrl = `ws://${server}/ws`; |
| 51 | |
| 52 | for (let elem of document.getElementsByClassName('page_hostname')) { |
| 53 | (elem as HTMLElement).innerText = location.hostname; |
| 54 | } |
| 55 | } |
| 56 | |
| 57 | connect(): void { |
| 58 | this.html5VideoElement = |
| 59 | (document.getElementById('stream') as HTMLMediaElement); |
| 60 | |
| 61 | this.webSocketConnection = new WebSocket(this.webSocketUrl); |
| 62 | this.webSocketConnection.binaryType = 'arraybuffer'; |
| 63 | this.webSocketConnection.addEventListener( |
| 64 | 'message', (e) => this.onWebSocketMessage(e)); |
| 65 | } |
| 66 | |
| 67 | |
| 68 | checkRemoteCandidate(candidate: RTCIceCandidate) { |
| 69 | const port = getIcePort(candidate); |
| 70 | if (!validPort(port)) { |
| 71 | document.getElementById('bad_remote_port_port').innerText = |
| 72 | port.toString(); |
| 73 | document.getElementById('bad_remote_port_error').style['display'] = |
| 74 | 'inherit'; |
| 75 | } |
| 76 | const address = getIceAddress(candidate); |
| 77 | if (isMdnsAddress(address)) { |
| 78 | document.getElementById('bad_remote_address_address').innerText = address; |
| 79 | document.getElementById('bad_remote_address_error').style['display'] = |
| 80 | 'inherit'; |
| 81 | } |
| 82 | } |
| 83 | |
| 84 | checkLocalCandidate(candidate: RTCIceCandidate) { |
| 85 | const port = getIcePort(candidate); |
| 86 | if (!validPort(port)) { |
| 87 | document.getElementById('bad_local_port_port').innerText = |
| 88 | port.toString(); |
| 89 | document.getElementById('bad_local_port_error').style['display'] = |
| 90 | 'inherit'; |
| 91 | } |
| 92 | const address = getIceAddress(candidate); |
| 93 | if (isMdnsAddress(address)) { |
| 94 | document.getElementById('bad_local_address_address').innerText = address; |
| 95 | document.getElementById('bad_local_address_error').style['display'] = |
| 96 | 'inherit'; |
| 97 | } |
| 98 | } |
| 99 | |
| 100 | onLocalDescription(desc: RTCSessionDescriptionInit): void { |
| 101 | console.log('Local description: ' + JSON.stringify(desc)); |
| 102 | this.rtcPeerConnection.setLocalDescription(desc).then(() => { |
| 103 | const builder = new Builder(512); |
| 104 | const sdpFb = WebSocketSdp.createWebSocketSdp( |
| 105 | builder, SdpType.ANSWER, builder.createString(desc.sdp)); |
| 106 | const message = WebSocketMessage.createWebSocketMessage( |
| 107 | builder, Payload.WebSocketSdp, sdpFb); |
| 108 | builder.finish(message); |
| 109 | const array = builder.asUint8Array(); |
| 110 | |
| 111 | this.webSocketConnection.send(array.buffer.slice(array.byteOffset)); |
| 112 | }); |
| 113 | } |
| 114 | |
| 115 | onIncomingSDP(sdp: RTCSessionDescriptionInit): void { |
| 116 | console.log('Incoming SDP: ' + JSON.stringify(sdp)); |
| 117 | this.rtcPeerConnection.setRemoteDescription(sdp); |
| 118 | this.rtcPeerConnection.createAnswer().then( |
| 119 | (e) => this.onLocalDescription(e)); |
| 120 | } |
| 121 | |
| 122 | onIncomingICE(ice: RTCIceCandidateInit): void { |
| 123 | let candidate = new RTCIceCandidate(ice); |
| 124 | console.log('Incoming ICE: ' + JSON.stringify(ice)); |
| 125 | this.rtcPeerConnection.addIceCandidate(candidate); |
| 126 | |
| 127 | // If end of candidates, won't have a port. |
| 128 | if (candidate.candidate !== '') { |
| 129 | this.checkRemoteCandidate(candidate); |
| 130 | } |
| 131 | } |
| 132 | |
| 133 | onRequestStats(track: MediaStreamTrack): void { |
| 134 | this.rtcPeerConnection.getStats(track).then((stats) => { |
| 135 | // getStats returns a list of stats of various types in an implementation |
| 136 | // defined order. We would like to get the protocol in use. This is found |
| 137 | // in remote-candidate. However, (again, implementation defined), some |
| 138 | // browsers return only remote-candidate's in use, while others return all |
| 139 | // of them that attempted negotiation. To figure this out, look at the |
| 140 | // currently nominated candidate-pair, then match up it's remote with a |
| 141 | // remote-candidate we see later. Since the order isn't defined, store the |
| 142 | // id in this in case the remote-candidate comes before candidate-pair. |
| 143 | |
| 144 | for (let dict of stats.values()) { |
| 145 | if (dict.type === 'candidate-pair' && dict.nominated) { |
| 146 | this.candidateNominatedId = dict.remoteCandidateId; |
| 147 | } |
| 148 | if (dict.type === 'remote-candidate' && |
| 149 | dict.id === this.candidateNominatedId) { |
| 150 | document.getElementById('stats_protocol').innerText = dict.protocol; |
| 151 | } |
| 152 | if (dict.type === 'inbound-rtp') { |
| 153 | const timestamp = dict.timestamp; |
| 154 | const bytes_now = dict.bytesReceived; |
| 155 | const frames_decoded = dict.framesDecoded; |
| 156 | |
| 157 | document.getElementById('stats_bps').innerText = |
| 158 | Math.round( |
| 159 | (bytes_now - this.lastBytesReceived) * 8 /* bits */ / |
| 160 | 1024 /* kbits */ / (timestamp - this.lastRtpTimestamp) * |
| 161 | 1000 /* ms */) |
| 162 | .toString(); |
| 163 | |
| 164 | document.getElementById('stats_fps').innerText = |
| 165 | (Math.round( |
| 166 | (frames_decoded - this.lastFramesDecoded) / |
| 167 | (timestamp - this.lastRtpTimestamp) * 1000 /* ms */ * 10) / |
| 168 | 10).toString(); |
| 169 | |
| 170 | |
| 171 | this.lastRtpTimestamp = timestamp; |
| 172 | this.lastBytesReceived = bytes_now; |
| 173 | this.lastFramesDecoded = frames_decoded; |
| 174 | } |
| 175 | } |
| 176 | }); |
| 177 | } |
| 178 | |
| 179 | onAddRemoteStream(event: RTCTrackEvent): void { |
| 180 | const stream = event.streams[0]; |
| 181 | this.html5VideoElement.srcObject = stream; |
| 182 | |
| 183 | const track = stream.getTracks()[0]; |
| 184 | this.statsInterval = |
| 185 | window.setInterval(() => this.onRequestStats(track), 1000); |
| 186 | } |
| 187 | |
| 188 | onIceCandidate(event: RTCPeerConnectionIceEvent): void { |
| 189 | if (event.candidate == null) { |
| 190 | return; |
| 191 | } |
| 192 | |
| 193 | console.log( |
| 194 | 'Sending ICE candidate out: ' + JSON.stringify(event.candidate)); |
| 195 | |
| 196 | const builder = new Builder(512); |
| 197 | const iceFb = WebSocketIce.createWebSocketIce( |
| 198 | builder, builder.createString(event.candidate.candidate), null, |
| 199 | event.candidate.sdpMLineIndex); |
| 200 | const message = WebSocketMessage.createWebSocketMessage( |
| 201 | builder, Payload.WebSocketIce, iceFb); |
| 202 | builder.finish(message); |
| 203 | const array = builder.asUint8Array(); |
| 204 | |
| 205 | this.webSocketConnection.send(array.buffer.slice(array.byteOffset)); |
| 206 | |
| 207 | // If end of candidates, won't have a port. |
| 208 | if (event.candidate.candidate !== '') { |
| 209 | this.checkLocalCandidate(event.candidate); |
| 210 | } |
| 211 | } |
| 212 | |
| 213 | // When we receive a websocket message, we need to determine what type it is |
| 214 | // and handle appropriately. Either by setting the remote description or |
| 215 | // adding the remote ice candidate. |
| 216 | onWebSocketMessage(e: MessageEvent): void { |
| 217 | const buffer = new Uint8Array(e.data) |
| 218 | const fbBuffer = new ByteBuffer(buffer); |
| 219 | const message = WebSocketMessage.getRootAsWebSocketMessage(fbBuffer); |
| 220 | |
| 221 | if (!this.rtcPeerConnection) { |
| 222 | this.rtcPeerConnection = new RTCPeerConnection(); |
| 223 | this.rtcPeerConnection.ontrack = (e) => this.onAddRemoteStream(e); |
| 224 | this.rtcPeerConnection.onicecandidate = (e) => this.onIceCandidate(e); |
| 225 | } |
| 226 | |
| 227 | switch (message.payloadType()) { |
| 228 | case Payload.WebSocketSdp: |
| 229 | const sdpFb = message.payload(new WebSocketSdp()); |
| 230 | const sdp: |
| 231 | RTCSessionDescriptionInit = {type: 'offer', sdp: sdpFb.payload()}; |
| 232 | |
| 233 | this.onIncomingSDP(sdp); |
| 234 | break; |
| 235 | case Payload.WebSocketIce: |
| 236 | const iceFb = message.payload(new WebSocketIce()); |
| 237 | const candidate = {} as RTCIceCandidateInit; |
| 238 | candidate.candidate = iceFb.candidate(); |
| 239 | candidate.sdpMLineIndex = iceFb.sdpMLineIndex(); |
| 240 | candidate.sdpMid = iceFb.sdpMid(); |
| 241 | this.onIncomingICE(candidate); |
| 242 | break; |
| 243 | default: |
| 244 | console.log('got an unknown message'); |
| 245 | break; |
| 246 | } |
| 247 | } |
| 248 | } |