blob: 5660cefdf562bee2228175cbd02a1dabd1952974 [file] [log] [blame]
Philipp Schradere625ba22020-11-16 20:11:37 -08001import * as configuration from 'org_frc971/aos/configuration_generated';
2import * as web_proxy from 'org_frc971/aos/network/web_proxy_generated';
3import {Builder} from 'org_frc971/external/com_github_google_flatbuffers/ts/builder';
4import {ByteBuffer} from 'org_frc971/external/com_github_google_flatbuffers/ts/byte-buffer';
5
6import Configuration = configuration.aos.Configuration;
7import MessageHeader = web_proxy.aos.web_proxy.MessageHeader;
8import WebSocketIce = web_proxy.aos.web_proxy.WebSocketIce;
9import WebSocketMessage = web_proxy.aos.web_proxy.WebSocketMessage;
10import Payload = web_proxy.aos.web_proxy.Payload;
11import WebSocketSdp = web_proxy.aos.web_proxy.WebSocketSdp;
12import SdpType = web_proxy.aos.web_proxy.SdpType;
Alex Perry5f474f22020-02-01 12:14:24 -080013
14// There is one handler for each DataChannel, it maintains the state of
15// multi-part messages and delegates to a callback when the message is fully
16// assembled.
17export class Handler {
18 private dataBuffer: Uint8Array|null = null;
19 private receivedMessageLength: number = 0;
20 constructor(
James Kuszmaul48413bf2020-09-01 19:19:05 -070021 private readonly handlerFunc:
22 (data: Uint8Array, sentTime: number) => void,
Philipp Schrader47445a02020-11-14 17:31:04 -080023 private readonly channel: RTCDataChannel) {
Alex Perry5f474f22020-02-01 12:14:24 -080024 channel.addEventListener('message', (e) => this.handleMessage(e));
25 }
26
27 handleMessage(e: MessageEvent): void {
Philipp Schradere625ba22020-11-16 20:11:37 -080028 const fbBuffer = new ByteBuffer(new Uint8Array(e.data));
29 const messageHeader = MessageHeader.getRootAsMessageHeader(
30 fbBuffer as unknown as flatbuffers.ByteBuffer);
James Kuszmaul48413bf2020-09-01 19:19:05 -070031 const time = messageHeader.monotonicSentTime().toFloat64() * 1e-9;
Alex Perry5f474f22020-02-01 12:14:24 -080032 // Short circuit if only one packet
Alex Perry22824d72020-02-29 17:11:43 -080033 if (messageHeader.packetCount() === 1) {
James Kuszmaul48413bf2020-09-01 19:19:05 -070034 this.handlerFunc(messageHeader.dataArray(), time);
Alex Perry5f474f22020-02-01 12:14:24 -080035 return;
36 }
37
38 if (messageHeader.packetIndex() === 0) {
39 this.dataBuffer = new Uint8Array(messageHeader.length());
Alex Perry22824d72020-02-29 17:11:43 -080040 this.receivedMessageLength = 0;
41 }
42 if (!messageHeader.dataLength()) {
43 return;
Alex Perry5f474f22020-02-01 12:14:24 -080044 }
45 this.dataBuffer.set(
46 messageHeader.dataArray(),
47 this.receivedMessageLength);
48 this.receivedMessageLength += messageHeader.dataLength();
49
50 if (messageHeader.packetIndex() === messageHeader.packetCount() - 1) {
James Kuszmaul48413bf2020-09-01 19:19:05 -070051 this.handlerFunc(this.dataBuffer, time);
Alex Perry5f474f22020-02-01 12:14:24 -080052 }
53 }
54}
Alex Perryb3b50792020-01-18 16:13:45 -080055
56// Analogous to the Connection class in //aos/network/web_proxy.h. Because most
57// of the apis are native in JS, it is much simpler.
58export class Connection {
59 private webSocketConnection: WebSocket|null = null;
60 private rtcPeerConnection: RTCPeerConnection|null = null;
Philipp Schrader47445a02020-11-14 17:31:04 -080061 private dataChannel: RTCDataChannel|null = null;
Alex Perryb3b50792020-01-18 16:13:45 -080062 private webSocketUrl: string;
Alex Perry6249aaf2020-02-29 14:51:49 -080063
Philipp Schradere625ba22020-11-16 20:11:37 -080064 private configInternal: Configuration|null = null;
Alex Perry6249aaf2020-02-29 14:51:49 -080065 // A set of functions that accept the config to handle.
Philipp Schradere625ba22020-11-16 20:11:37 -080066 private readonly configHandlers = new Set<(config: Configuration) => void>();
Alex Perry6249aaf2020-02-29 14:51:49 -080067
James Kuszmaul48413bf2020-09-01 19:19:05 -070068 private readonly handlerFuncs =
69 new Map<string, (data: Uint8Array, sentTime: number) => void>();
Alex Perry5f474f22020-02-01 12:14:24 -080070 private readonly handlers = new Set<Handler>();
Alex Perryb3b50792020-01-18 16:13:45 -080071
72 constructor() {
73 const server = location.host;
74 this.webSocketUrl = `ws://${server}/ws`;
75 }
76
Philipp Schradere625ba22020-11-16 20:11:37 -080077 addConfigHandler(handler: (config: Configuration) => void): void {
Alex Perry6249aaf2020-02-29 14:51:49 -080078 this.configHandlers.add(handler);
79 }
80
Alex Perryb49a3fb2020-02-29 15:26:54 -080081 /**
82 * Add a handler for a specific message type. Until we need to handle
83 * different channel names with the same type differently, this is good
84 * enough.
85 */
Philipp Schradere625ba22020-11-16 20:11:37 -080086 addHandler(id: string, handler: (data: Uint8Array, sentTime: number) => void): void {
Alex Perry5f474f22020-02-01 12:14:24 -080087 this.handlerFuncs.set(id, handler);
88 }
89
Alex Perryb3b50792020-01-18 16:13:45 -080090 connect(): void {
91 this.webSocketConnection = new WebSocket(this.webSocketUrl);
92 this.webSocketConnection.binaryType = 'arraybuffer';
93 this.webSocketConnection.addEventListener(
94 'open', () => this.onWebSocketOpen());
95 this.webSocketConnection.addEventListener(
96 'message', (e) => this.onWebSocketMessage(e));
97 }
98
Alex Perry3dfcb812020-03-04 19:32:17 -080099 getConfig() {
Philipp Schrader47445a02020-11-14 17:31:04 -0800100 return this.configInternal;
Alex Perry6249aaf2020-02-29 14:51:49 -0800101 }
102
Alex Perry5f474f22020-02-01 12:14:24 -0800103 // Handle messages on the DataChannel. Handles the Configuration message as
104 // all other messages are sent on specific DataChannels.
James Kuszmaul1ec74432020-07-30 20:26:45 -0700105 onConfigMessage(data: Uint8Array): void {
Philipp Schradere625ba22020-11-16 20:11:37 -0800106 const fbBuffer = new ByteBuffer(data);
107 this.configInternal = Configuration.getRootAsConfiguration(
108 fbBuffer as unknown as flatbuffers.ByteBuffer);
Alex Perry3dfcb812020-03-04 19:32:17 -0800109 for (const handler of Array.from(this.configHandlers)) {
Alex Perry6249aaf2020-02-29 14:51:49 -0800110 handler(this.configInternal);
Alex Perry5f474f22020-02-01 12:14:24 -0800111 }
112 }
113
114 onDataChannel(ev: RTCDataChannelEvent): void {
115 const channel = ev.channel;
116 const name = channel.label;
117 const channelType = name.split('/').pop();
118 const handlerFunc = this.handlerFuncs.get(channelType);
119 this.handlers.add(new Handler(handlerFunc, channel));
Alex Perryb3b50792020-01-18 16:13:45 -0800120 }
121
122 onIceCandidate(e: RTCPeerConnectionIceEvent): void {
Alex Perryb3b50792020-01-18 16:13:45 -0800123 if (!e.candidate) {
124 return;
125 }
126 const candidate = e.candidate;
Philipp Schradere625ba22020-11-16 20:11:37 -0800127 const builder = new Builder(512);
Alex Perryb3b50792020-01-18 16:13:45 -0800128 const candidateString = builder.createString(candidate.candidate);
129 const sdpMidString = builder.createString(candidate.sdpMid);
130
Philipp Schradere625ba22020-11-16 20:11:37 -0800131 const iceFb = WebSocketIce.createWebSocketIce(
132 builder as unknown as flatbuffers.Builder, candidateString,
133 sdpMidString, candidate.sdpMLineIndex);
134 const messageFb = WebSocketMessage.createWebSocketMessage(
135 builder as unknown as flatbuffers.Builder, Payload.WebSocketIce, iceFb);
Alex Perryb3b50792020-01-18 16:13:45 -0800136 builder.finish(messageFb);
137 const array = builder.asUint8Array();
138 this.webSocketConnection.send(array.buffer.slice(array.byteOffset));
139 }
140
141 // Called for new SDPs. Make sure to set it locally and remotely.
Philipp Schrader47445a02020-11-14 17:31:04 -0800142 onOfferCreated(description: RTCSessionDescriptionInit): void {
Alex Perryb3b50792020-01-18 16:13:45 -0800143 this.rtcPeerConnection.setLocalDescription(description);
Philipp Schradere625ba22020-11-16 20:11:37 -0800144 const builder = new Builder(512);
Alex Perryb3b50792020-01-18 16:13:45 -0800145 const offerString = builder.createString(description.sdp);
146
Philipp Schradere625ba22020-11-16 20:11:37 -0800147 const webSocketSdp = WebSocketSdp.createWebSocketSdp(
148 builder as unknown as flatbuffers.Builder, SdpType.OFFER, offerString);
149 const message = WebSocketMessage.createWebSocketMessage(
150 builder as unknown as flatbuffers.Builder, Payload.WebSocketSdp,
151 webSocketSdp);
Alex Perryb3b50792020-01-18 16:13:45 -0800152 builder.finish(message);
153 const array = builder.asUint8Array();
154 this.webSocketConnection.send(array.buffer.slice(array.byteOffset));
155 }
156
157 // We now have a websocket, so start setting up the peer connection. We only
158 // want a DataChannel, so create it and then create an offer to send.
159 onWebSocketOpen(): void {
160 this.rtcPeerConnection = new RTCPeerConnection({});
Alex Perry5f474f22020-02-01 12:14:24 -0800161 this.rtcPeerConnection.addEventListener(
Alex Perry22824d72020-02-29 17:11:43 -0800162 'datachannel', (e) => this.onDataChannel(e));
Alex Perry5f474f22020-02-01 12:14:24 -0800163 this.dataChannel = this.rtcPeerConnection.createDataChannel('signalling');
James Kuszmaul1ec74432020-07-30 20:26:45 -0700164 this.handlers.add(
165 new Handler((data) => this.onConfigMessage(data), this.dataChannel));
Philipp Schrader47445a02020-11-14 17:31:04 -0800166 // TODO(james): Is this used? Can we delete it?
167 // window.dc = this.dataChannel;
Alex Perryb3b50792020-01-18 16:13:45 -0800168 this.rtcPeerConnection.addEventListener(
169 'icecandidate', (e) => this.onIceCandidate(e));
170 this.rtcPeerConnection.createOffer().then(
171 (offer) => this.onOfferCreated(offer));
172 }
173
174 // When we receive a websocket message, we need to determine what type it is
175 // and handle appropriately. Either by setting the remote description or
176 // adding the remote ice candidate.
177 onWebSocketMessage(e: MessageEvent): void {
Alex Perryb3b50792020-01-18 16:13:45 -0800178 const buffer = new Uint8Array(e.data)
Philipp Schradere625ba22020-11-16 20:11:37 -0800179 const fbBuffer = new ByteBuffer(buffer);
180 const message = WebSocketMessage.getRootAsWebSocketMessage(
181 fbBuffer as unknown as flatbuffers.ByteBuffer);
Alex Perryb3b50792020-01-18 16:13:45 -0800182 switch (message.payloadType()) {
Philipp Schradere625ba22020-11-16 20:11:37 -0800183 case Payload.WebSocketSdp:
184 const sdpFb = message.payload(new WebSocketSdp());
185 if (sdpFb.type() !== SdpType.ANSWER) {
Alex Perryb3b50792020-01-18 16:13:45 -0800186 console.log('got something other than an answer back');
187 break;
188 }
189 this.rtcPeerConnection.setRemoteDescription(new RTCSessionDescription(
190 {'type': 'answer', 'sdp': sdpFb.payload()}));
191 break;
Philipp Schradere625ba22020-11-16 20:11:37 -0800192 case Payload.WebSocketIce:
193 const iceFb = message.payload(new WebSocketIce());
Alex Perryb3b50792020-01-18 16:13:45 -0800194 const candidate = {} as RTCIceCandidateInit;
195 candidate.candidate = iceFb.candidate();
196 candidate.sdpMid = iceFb.sdpMid();
197 candidate.sdpMLineIndex = iceFb.sdpMLineIndex();
198 this.rtcPeerConnection.addIceCandidate(candidate);
199 break;
200 default:
201 console.log('got an unknown message');
202 break;
203 }
204 }
Alex Perry6249aaf2020-02-29 14:51:49 -0800205
206 /**
Alex Perryb49a3fb2020-02-29 15:26:54 -0800207 * Subscribes to messages. Only the most recent connect message is in use. Any
208 * channels not specified in the message are implicitely unsubscribed.
Alex Perry6249aaf2020-02-29 14:51:49 -0800209 * @param a Finished flatbuffer.Builder containing a Connect message to send.
210 */
211 sendConnectMessage(builder: any) {
Alex Perry3dfcb812020-03-04 19:32:17 -0800212 const array = builder.asUint8Array();
Alex Perry6249aaf2020-02-29 14:51:49 -0800213 this.dataChannel.send(array.buffer.slice(array.byteOffset));
214 }
Alex Perryb3b50792020-01-18 16:13:45 -0800215}