blob: bfe8135e925caf58badf3925959dac07079649bd [file] [log] [blame]
Tyler Chatowb3850c12020-02-26 20:55:48 -08001import {Builder, ByteBuffer} from 'flatbuffers';
2import {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.
8function 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.
15function 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
23function isMdnsAddress(address: string): boolean {
24 return address.includes('.local');
25}
26
27function getIceAddress(candidate: RTCIceCandidate): string {
28 if (candidate.address === undefined) {
29 return candidate.candidate.split(' ')[4];
30 } else {
31 return candidate.address;
32 }
33}
34
35export 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}