Source: MultiStreamViewer.js

/**
 * @author Russell Toris - russell.toris@gmail.com
 */
import EventEmitter2 from 'eventemitter2';
import Button from './models/Button';
import Viewer from './Viewer';

/**
 * A MultiStreamViewer can be used to stream multiple MJPEG topics into a canvas.
 *
 * Emits the following events:
 *   * 'warning' - emitted if the given topic is unavailable
 *   * 'change' - emitted with the topic name that the canvas was changed to
 *
 */
class MultiStreamViewer extends EventEmitter2 {
  /**
   *
   * @param {Object} options - possible keys include:
   * @param {string} options.divID - the ID of the HTML div to place the canvas in
   * @param {number} options.width - the width of the canvas
   * @param {number} options.height - the height of the canvas
   * @param {string} options.host - the hostname of the MJPEG server
   * @param {number} [options.port] (optional) - the port to connect to
   * @param {number} [options.quality] (optional) - the quality of the stream (from 1-100)
   * @param {Array<string>} options.topics - an array of topics to stream
   * @param {Array<string>} [options.labels] (optional) - an array of labels associated with each topic
   * @param {number} [options.defaultStream] (optional) - the index of the default stream to use
   */
  constructor(options) {
    super();
    options = options || {};
    this.divID = options.divID;
    this.width = options.width;
    this.height = options.height;
    this.host = options.host;
    this.port = options.port || 8080;
    this.quality = options.quality;
    this.topics = options.topics;
    this.labels = options.labels;
    this.defaultStream = options.defaultStream || 0;
    this.currentTopic = this.topics[this.defaultStream];

    // create an overlay canvas for the button
    const canvas = document.createElement('canvas');
    canvas.width = this.width;
    canvas.height = this.height;
    this.canvas = canvas;

    // menu div
    const menu = document.createElement('div');
    menu.style.display = 'none';
    document.getElementsByTagName('body')[0].appendChild(menu);
    this.menu = menu;

    // button for the error
    const buttonHeight = this.height / 8;
    const buttonPadding = 10;
    const button = new Button({
      text: 'Edit',
      height: buttonHeight,
    });
    const buttonWidth = button.width;

    // use a regular viewer internally
    const viewer = new Viewer({
      divID: this.divID,
      width: this.width,
      height: this.height,
      host: this.host,
      port: this.port,
      quality: this.quality,
      topic: this.currentTopic,
      overlay: this.canvas,
    });

    // catch the events
    viewer.on('warning', (warning) => {
      this.emit('warning', warning);
    });
    viewer.on('change', (topic) => {
      this.currentTopic = topic;
      this.emit('change', topic);
    });

    // add the event listener
    this.buttonTimer = null;
    this.menuOpen = false;
    this.hasButton = false;
    viewer.canvas.addEventListener(
      'mousemove',
      (e) => {
        this.clearButton();

        if (!this.menuOpen) {
          this.hasButton = true;
          // add the button
          button.redraw();
          const context = this.canvas.getContext('2d');
          context.drawImage(
            button.canvas,
            buttonPadding,
            this.height - (buttonHeight + buttonPadding)
          );

          // display the button for 3 seconds
          this.buttonTimer = setInterval(() => {
            // clear the overlay canvas
            this.clearButton();
          }, 3000);
        } else {
          this.fadeImage();
        }
      },
      false
    );

    // add the click listener
    viewer.canvas.addEventListener(
      'click',
      function (e) {
        // check if the button is there
        if (this.hasButton) {
          // determine the click position
          var offsetLeft = 0;
          var offsetTop = 0;
          var element = viewer.canvas;
          while (
            element &&
            !isNaN(element.offsetLeft) &&
            !isNaN(element.offsetTop)
          ) {
            offsetLeft += element.offsetLeft - element.scrollLeft;
            offsetTop += element.offsetTop - element.scrollTop;
            element = element.offsetParent;
          }

          var x = e.pageX - offsetLeft;
          var y = e.pageY - offsetTop;

          // check if we are in the 'edit' button
          if (
            x < buttonWidth + buttonPadding &&
            x > buttonPadding &&
            y > this.height - (buttonHeight + buttonPadding) &&
            y < this.height - buttonPadding
          ) {
            this.menuOpen = true;
            this.clearButton();

            // create the menu
            var heading = document.createElement('span');
            heading.innerHTML = '<h2>Camera Streams</h2><hr /><br />';
            menu.appendChild(heading);
            var form = document.createElement('form');
            var streamLabel = document.createElement('label');
            streamLabel.setAttribute('for', 'stream');
            streamLabel.innerHTML = 'Stream: ';
            form.appendChild(streamLabel);
            var streamMenu = document.createElement('select');
            streamMenu.setAttribute('name', 'stream');
            // add each option
            for (var i = 0; i < this.topics.length; i++) {
              var option = document.createElement('option');
              // check if this is the selected option
              if (this.topics[i] === this.currentTopic) {
                option.setAttribute('selected', 'selected');
              }
              option.setAttribute('value', this.topics[i]);
              // check for a label
              if (this.labels) {
                option.innerHTML = this.labels[i];
              } else {
                option.innerHTML += this.topics[i];
              }
              streamMenu.appendChild(option);
            }
            form.appendChild(streamMenu);
            menu.appendChild(form);
            menu.appendChild(document.createElement('br'));
            var close = document.createElement('button');
            close.innerHTML = 'Close';
            menu.appendChild(close);

            // display the menu
            menu.style.position = 'absolute';
            menu.style.top = offsetTop + 'px';
            menu.style.left = offsetLeft + 'px';
            menu.style.width = this.width + 'px';
            menu.style.display = 'block';

            streamMenu.addEventListener(
              'click',
              function () {
                var topic = streamMenu.options[streamMenu.selectedIndex].value;
                // make sure it is a new stream
                if (topic !== this.currentTopic) {
                  viewer.changeStream(topic);
                }
              },
              false
            );

            // handle the interactions
            close.addEventListener(
              'click',
              function (e) {
                // close the menu
                menu.innerHTML = '';
                menu.style.display = 'none';
                this.menuOpen = false;
                const context = this.canvas.getContext('2d');
                context.clearRect(0, 0, this.canvas.width, this.canvas.height);
              },
              false
            );
          }
        }
      },
      false
    );
  }

  /**
   * Clear the button from the overlay canvas.
   */
  clearButton() {
    if (this.buttonTimer) {
      window.clearInterval(this.buttonTimer);
      // clear the button
      const context = this.canvas.getContext('2d');
      context.clearRect(0, 0, this.canvas.width, this.canvas.height);
      this.hasButton = false;
    }
  }

  /**
   * Fades the stream by putting an overlay on it.
   */
  fadeImage() {
    const context = this.canvas.getContext('2d');
    context.clearRect(0, 0, this.canvas.width, this.canvas.height);
    // create the white box
    context.globalAlpha = 0.44;
    context.beginPath();
    context.rect(0, 0, this.width, this.height);
    context.fillStyle = '#fefefe';
    context.fill();
    context.globalAlpha = 1;
  }
}

export default MultiStreamViewer;