Handle subscription of messages in the webapp.

Messages larger than a threshold are split and reassembled due to the
size limit in webrtc. Threshold may have to be adjusted somewhere between
64KiB and 256KiB.

This also includes a basic handler for a ping message and a more
advanced image handler.

Change-Id: If66acfb1bb84e9d3ff686994a94b1480cb70b2aa
diff --git a/y2020/www/image_handler.ts b/y2020/www/image_handler.ts
new file mode 100644
index 0000000..abaf831
--- /dev/null
+++ b/y2020/www/image_handler.ts
@@ -0,0 +1,61 @@
+import {frc971} from 'y2020/vision/vision_generated';
+
+export class ImageHandler {
+  private canvas = document.createElement('canvas');
+
+  constructor() {
+    document.body.appendChild(this.canvas);
+  }
+
+  handleImage(data: Uint8Array) {
+    const fbBuffer = new flatbuffers.ByteBuffer(data);
+    const image = frc971.vision.CameraImage.getRootAsCameraImage(fbBuffer);
+
+    const width = image.cols();
+    const height = image.rows();
+    if (width === 0 || height === 0) {
+      return;
+    }
+    const imageBuffer = new Uint8ClampedArray(width * height * 4); // RGBA
+
+    // Read four bytes (YUYV) from the data and transform into two pixels of
+    // RGBA for canvas
+    for (const j = 0; j < height; j++) {
+      for (const i = 0; i < width; i += 2) {
+        const y1 = image.data((j * width + i) * 2);
+        const u = image.data((j * width + i) * 2 + 1);
+        const y2 = image.data((j * width + i + 1) * 2);
+        const v = image.data((j * width + i + 1) * 2 + 1);
+
+        // Based on https://en.wikipedia.org/wiki/YUV#Converting_between_Y%E2%80%B2UV_and_RGB
+        const c1 = y1 - 16;
+        const c2 = y2 - 16;
+        const d = u - 128;
+        const e = v - 128;
+
+        imageBuffer[(j * width + i) * 4 + 0] = (298 * c1 + 409 * e + 128) >> 8;
+        imageBuffer[(j * width + i) * 4 + 1] =
+            (298 * c1 - 100 * d - 208 * e + 128) >> 8;
+        imageBuffer[(j * width + i) * 4 + 2] = (298 * c1 + 516 * d + 128) >> 8;
+        imageBuffer[(j * width + i) * 4 + 3] = 255;
+        imageBuffer[(j * width + i) * 4 + 4] = (298 * c2 + 409 * e + 128) >> 8;
+        imageBuffer[(j * width + i) * 4 + 5] =
+            (298 * c2 - 100 * d - 208 * e + 128) >> 8;
+        imageBuffer[(j * width + i) * 4 + 6] = (298 * c2 + 516 * d + 128) >> 8;
+        imageBuffer[(j * width + i) * 4 + 7] = 255;
+      }
+    }
+
+    const ctx = this.canvas.getContext('2d');
+
+    this.canvas.width = width;
+    this.canvas.height = height;
+    const idata = ctx.createImageData(width, height);
+    idata.data.set(imageBuffer);
+    ctx.putImageData(idata, 0, 0);
+  }
+
+  getId() {
+    return frc971.vision.CameraImage.getFullyQualifiedName();
+  }
+}