Source: MultiScannerElement.js

import BarcodeDetectorPolyfill from './BarcodeDetectorPolyfill.js';

import { camelJoin, snakeToCamel } from './MultiScannerElement.utils.js';

import {
  MEDIA_TRACK_CONSTRAINTS,
  IMAGE_TRACK_CONSTRAINTS,
  VIDEO_TRACK_CONSTRAINTS,
  BARCODE_SCANNER_ATTRIBUTES,
  SUPPORTED_FORMATS_MAP,
  HEX_COLOR_REGEX,
  STROKE_LINE_JOINS,
} from './MultiScannerElement.constants.js';

// Establish BarcodeDetector ( Polyfill )
BarcodeDetectorPolyfill.setupPolyfill();

/*
    window['BarcodeDetector'] = BarcodeDetectorPolyfill;
    if (!('BarcodeDetector' in window)) {
      return false;
    }
    return true;
  }

await BarcodeDetector.getSupportedFormats()
*/

// prettier-ignore
const default_scanner_html = ` 
  <!--
  Multi-Scanner Style
  -->
  <style>

    #multi-scanner-container {
      display: grid;
      grid-template-areas:
        "main";
      place-items: center;
    }

    #multi-scanner-container > * {
      grid-area: main;
    }

    #multi-scanner-video-webcam {
      border: 1px solid red;

    }

    #multi-scanner-canvas-output {
      border: 1px solid lime;
    }

  </style>

  <!--
  Multi-Scanner Content
  -->
  <span id="multi-scanner-container">
    <video id="multi-scanner-video-webcam" playsinline="true"></video>
    <canvas id="multi-scanner-canvas-output"></canvas>
    <div id="multi-scanner-loading-msg">🎥 Unable to access video stream</div>
  </span>
`

/** Barcode scanning web component capable of scanning multiple formats of barcodes. */
class MultiScannerElement extends HTMLElement {
  static get observedAttributes() {
    /**
     * Possible Observed Attributes for the future.
     * navigator.mediaDevices.getSupportedConstraints().map( c => camelJoin(c, '-'))
     */
    return [
      ...MEDIA_TRACK_CONSTRAINTS,
      ...Object.keys(IMAGE_TRACK_CONSTRAINTS).map((c) => camelJoin(c, '-')),
      ...Object.keys(VIDEO_TRACK_CONSTRAINTS).map((c) => camelJoin(c, '-')),
      ...BARCODE_SCANNER_ATTRIBUTES,
    ];
  }

  /**
   * Create a MultiScannerElement.
   */
  constructor() {
    // Call parent constructor.
    super();

    // Template the Shadow DOM.
    this.attachShadow({ mode: 'open' });
    this.template();

    // States - Changed when interacting with camera and displaying state.
    this._states = ['start', 'pause', 'stop'];

    // State.
    this._state = '';

    // Formats.
    this._formats = Object.values(SUPPORTED_FORMATS_MAP);

    // BarcodeDetector
    // this.barcodeDetector = new BarcodeDetector({ formats: this.formats });
    this.barcodeDetector = new BarcodeDetectorPolyfill({
      formats: this.formats,
    });

    // Set Camera Access Message.
    this.camera_access_message =
      String() +
      'Your Devices Camera information is needed in order ' +
      'for this Scanner to function properly. Are you ' +
      'ready to allow access to your Devices Camera?';

    // Initialize Default settings & constraints.
    this._settings = {};

    // Stream.
    this._stream = null;

    // Track.
    this._track = null;

    // Constraints
    this._constraint_delay = 700;

    // TODO: What we would like the arguments to be.
    this._media_track_constraints = {
      video: { facingMode: 'environment' },
    };

    // CameraId.
    this._cameraId = this.guid();
    this.setAttribute('cameraId', this.cameraId);

    // Cameras.
    //  - Create shared cameras object.
    //  - Add self to cameras.
    if (!this.__proto__.cameras) {
      this.__proto__.cameras = {};
    }
    this.__proto__.cameras[this.cameraId] = this;

    // Canvas Contexts.
    this.canvas_output = this._$Map.$canvas_output.getContext('2d');

    // Line Args
    this._default_line_color = '#ff0000';
    this._default_line_width = 4;
    this._default_line_join = 'round';
    this._default_line_shadow_color = '#ff0000';
    this._default_line_shadow_blur = 4;

    // IMAGE_TRACK_CONSTRAINTS Properties
    Object.entries(IMAGE_TRACK_CONSTRAINTS).map(([c_prop, c_type]) => {
      const c_attr = camelJoin(c_prop);
      Object.defineProperty(this, c_prop, {
        get: () => {
          return this[`_${c_prop}`];
        },
        set: (new_c) => {
          this._constraintPropUpdate({ c_prop, c_attr, new_c, c_type });
        },
      });
    });

    // VIDEO_TRACK_CONSTRAINTS Properties
    Object.entries(VIDEO_TRACK_CONSTRAINTS).map(([c_prop, c_type]) => {
      const c_attr = camelJoin(c_prop);
      Object.defineProperty(this, c_prop, {
        get: () => {
          return this[`_${c_prop}`];
        },
        set: (new_c) => {
          this._constraintPropUpdate({ c_prop, c_attr, new_c, c_type });
        },
      });
    });
  }

  /**
   * Attributes on this element have changed.
   *
   * @param name {string} - Which attribute has chaned.
   * @param oldValue {string} - Previous value of attribute.
   * @param newValue {string} - Current value of attribute.
   */
  attributeChangedCallback(name, oldValue, newValue) {
    console.log(`${name} changed from ${oldValue} to ${newValue}`);
    const c_prop = snakeToCamel(name);
    const c_attr = name;

    /* --- MEDIA_TRACK_CONSTRAINTS - BEGIN ---------------------------------- */
    if (name === 'device') {
      // Check if new device is in videoDevicesPromise.
      if (this._videoDevices) {
        let newDevice = this._videoDevices.find((device) => {
          if (device.deviceId === newValue) {
            return true;
          }

          if (device.label === newValue) {
            return true;
          }

          return false;
        });

        if (newDevice) {
          this._device = newValue;

          if (this.state === 'started') {
            this.restart();
          }
        }
      } else {
        // Default to updating the device if _videoDevices has not been set yet.
        this._device = newValue;
      }
    }
    /* --- MEDIA_TRACK_CONSTRAINTS - END ------------------------------------ */

    /* --- IMAGE_TRACK_CONSTRAINTS - BEGIN ---------------------------------- */
    if (c_prop in IMAGE_TRACK_CONSTRAINTS) {
      const c_type = IMAGE_TRACK_CONSTRAINTS[c_prop];
      const new_c = newValue;
      this._constraintAttrUpdate({ c_prop, c_attr, new_c, c_type });
    }
    /* --- IMAGE_TRACK_CONSTRAINTS - END ------------------------------------ */

    /* --- VIDEO_TRACK_CONSTRAINTS - BEGIN ---------------------------------- */
    if (c_prop in VIDEO_TRACK_CONSTRAINTS) {
      const c_type = VIDEO_TRACK_CONSTRAINTS[c_prop];
      const new_c = newValue;
      this._constraintAttrUpdate({ c_prop, c_attr, new_c, c_type });
    }
    /* --- VIDEO_TRACK_CONSTRAINTS - END ------------------------------------ */

    /* --- BARCODE_SCANNER_ATTRIBUTES - BEGIN ------------------------------- */
    if (name === 'state') {
      if (this._states.includes(newValue)) {
        this._state = newValue;

        // Process state change.
        if (newValue === 'start') {
          this.start();
        } else if (newValue === 'pause') {
          this.pause();
        } else if (newValue === 'stop') {
          this.stop();
        }
      }
    }

    if (name === 'formats') {
      let newFormats = newValue.replaceAll(' ', '').split(',');
      let newValidFormats = newFormats.filter((f) => {
        return Object.values(SUPPORTED_FORMATS_MAP).includes(f);
      });
      this._formats = newValidFormats;
      this.barcodeDetector = new BarcodeDetector({ formats: this.formats });
    }

    if (name === 'web-worker') {
      if (this.hasAttribute(name)) {
        this._web_worker = true;
      } else {
        this._web_worker = false;
      }
    }

    if (name === 'line-color') {
      if (HEX_COLOR_REGEX.test(newValue)) {
        this._line_color = newValue;
      }
    }

    if (name === 'line-width') {
      if (!isNaN(parseInt(newValue)) && parseInt(newValue) > -1) {
        this._line_width = parseFloat(newValue);
      }
    }

    if (name === 'line-shadow-blur') {
      if (!isNaN(parseInt(newValue)) && parseInt(newValue) > -1) {
        this._line_shadow_blur = parseFloat(newValue);
      }
    }

    if (name === 'line-shadow-color') {
      if (HEX_COLOR_REGEX.test(newValue)) {
        this._line_shadow_color = newValue;
      }
    }

    if (name === 'line-join') {
      if (STROKE_LINE_JOINS.includes(newValue)) {
        this._line_join = newValue;
      }
    }
    /* --- BARCODE_SCANNER_ATTRIBUTES - END --------------------------------- */
  }

  /**
   * The element has been connected to the DOM.
   */
  connectedCallback() {}

  /**
   * The element has been disconnected from the DOM.
   */
  disconnectedCallback() {}

  /**
   * Constraint has been updated through the attribute. Sets the private
   * private propterty.
   *
   * Respects supported constraints & track capabilities
   *
   * If a new constraint value is not a part of the supported constraints than
   * the method will return.
   *
   * If the current track_capabilities is set then the new constraint value
   * will be checked to ensure that it valid.
   *
   * @param {Object} obj - An Object
   * @param {String} obj.c_prop - Property (camelCase) to be updated.
   * @param {String} obj.c_attr - Attribute (snakeCase w/ hyphen) that was updated.
   * @param {String} obj.new_c - New value of the constraint.
   * @param {String} obj.c_type - Type of the constraint.
   */
  _constraintAttrUpdate({ c_prop, c_attr, new_c, c_type }) {
    let valid_image_constraints;
    let valid_video_constraints;
    let constraints_constant;
    let current_constraints = { [c_prop]: new_c };

    // Check IMAGE_TRACK_CONSTRAINTS.
    constraints_constant = IMAGE_TRACK_CONSTRAINTS;
    valid_image_constraints = this._reduce_constraints({
      current_constraints,
      constraints_constant,
    });

    // Check VIDEO_TRACK_CONSTRAINTS.
    constraints_constant = VIDEO_TRACK_CONSTRAINTS;
    valid_video_constraints = this._reduce_constraints({
      current_constraints,
      constraints_constant,
    });

    // Short circuit if constraint is invalid.
    if (
      !(
        c_prop in
        {
          ...valid_image_constraints,
          ...valid_video_constraints,
        }
      ) &&
      c_type !== 'Boolean'
    ) {
      return;
    }

    if (c_type === 'String') {
      this[`_${c_prop}`] = new_c;
    }

    if (c_type === 'ConstrainDOMString') {
      this[`_${c_prop}`] = new_c;
    }

    if (c_type === 'Object') {
      this[`_${c_prop}`] = JSON.parse(new_c);
    }

    // Handles min, max & step
    if (c_type === 'ConstrainDouble') {
      this[`_${c_prop}`] = parseFloat(new_c);
    }

    // Handles min, max & step
    if (c_type === 'ConstrainULong') {
      this[`_${c_prop}`] = parseInt(new_c);
    }

    if (c_type === 'Boolean') {
      if (this.hasAttribute(c_attr)) {
        this[`_${c_prop}`] = true;
      } else {
        this[`_${c_prop}`] = false;
      }
    }

    // Apply updated image constraints.
    if (Object.keys(valid_image_constraints).length > 0) {
      this.applyConstraints();
    }

    // Restart Scanner to apply video constraints.
    if (Object.keys(valid_video_constraints).length > 0) {
      if (this.state === 'started') {
        this.restart();
      }
    }
  }

  /**
   * Constraint is being updated through the property. Sets the attribute if
   * validation passes
   *
   * Respects supported constraints & track capabilities
   *
   * If a new constraint value is not a part of the supported constraints than
   * the method will return.
   *
   * If the current track_capabilities is set then the new constraint value
   * will be checked to ensure that it valid.
   *
   *
   * @param {Object} obj - An Object
   * @param {String} obj.c_prop - Property (camelCase) to be updated.
   * @param {String} obj.c_attr - Attribute (snakeCase w/ hyphen) that was updated.
   * @param {String} obj.new_c - New value of the constraint.
   * @param {String} obj.c_type - Type of the constraint.
   */
  _constraintPropUpdate({ c_prop, c_attr, new_c, c_type }) {
    let valid_image_constraints;
    let valid_video_constraints;
    let constraints_constant;
    let current_constraints = { [c_prop]: new_c };

    // Check IMAGE_TRACK_CONSTRAINTS.
    constraints_constant = IMAGE_TRACK_CONSTRAINTS;
    valid_image_constraints = this._reduce_constraints({
      current_constraints,
      constraints_constant,
    });

    // Check VIDEO_TRACK_CONSTRAINTS.
    constraints_constant = VIDEO_TRACK_CONSTRAINTS;
    valid_video_constraints = this._reduce_constraints({
      current_constraints,
      constraints_constant,
    });

    // Short circuit if constraint is invalid.
    if (
      !(
        c_prop in
        {
          ...valid_image_constraints,
          ...valid_video_constraints,
        }
      ) &&
      c_type !== 'Boolean'
    ) {
      return;
    }

    if (c_type === 'String') {
      this.setAttribute(c_attr, new_c);
    }

    if (c_type === 'ConstrainDOMString') {
      this.setAttribute(c_attr, new_c);
    }

    if (c_type === 'Object') {
      this.setAttribute(c_attr, JSON.stringify(new_c));
    }

    if (c_type === 'ConstrainDouble') {
      this.setAttribute(c_attr, parseFloat(new_c));
    }

    if (c_type === 'ConstrainULong') {
      this.setAttribute(c_attr, parseInt(new_c));
    }

    if (c_type === 'Boolean') {
      if (new_c) {
        this.setAttribute(c_attr, '');
      } else {
        this.removeAttribute(c_attr);
      }
    }

    // Apply updated image constraints.
    if (Object.keys(valid_image_constraints).length > 0) {
      this.applyConstraints();
    }

    // Restart Scanner to apply video constraints.
    if (Object.keys(valid_video_constraints).length > 0) {
      if (this.state === 'started') {
        this.restart();
      }
    }
  }

  /**
   * The default template HTML string.
   */
  get _template() {
    return default_scanner_html;
  }

  /**
   * Sets shadow DOM to template HTML string.
   */
  template() {
    this.shadowRoot.innerHTML = this._template;
  }

  /**
   * CameraId for the scanner.
   */
  get cameraId() {
    return this._cameraId;
  }

  /**
   * Getting _stream.
   */
  get stream() {
    return this._stream;
  }

  /**
   * Setting new _stream.
   */
  set stream(newStream) {
    let stream_type = Object.prototype.toString.call(newStream);
    if (stream_type === '[object MediaStream]') {
      this._stream = newStream;
      this._track = this._stream.getTracks()[0];
    }
  }

  /**
   * Getting _track.
   */
  get track() {
    return this._track;
  }

  /**
   * Setting new _track.
   */
  set track(newTrack) {
    let stream_type = Object.prototype.toString.call(newTrack);
    if (stream_type === '[object MediaStreamTrack]') {
      this._track = newTrack;

      // Get Constraints
      this._track_constraints = this._track.getConstraints();
    }
  }

  /**
   * Getting track_capabilities.
   */
  get track_capabilities() {
    // Get Capabilities
    if (this._track !== null && this._track.getCapabilities) {
      return this._track.getCapabilities();
    } else {
      return {};
    }
  }

  /**
   * Getting track_constraints.
   */
  get track_constraints() {
    // Get Capabilities
    if (this._track && this._track.getConstraints) {
      return this._track.getConstraints();
    } else {
      return {};
    }
  }

  /**
   * Getting track_settings.
   */
  get track_settings() {
    // Get Capabilities
    if (this._track && this._track.getSettings) {
      return this._track.getSettings();
    } else {
      return {};
    }
  }

  /**
   * Supported Track Constraints for this browser.
   */
  get _supported_constraints() {
    return navigator.mediaDevices.getSupportedConstraints();
  }

  /**
   * Settings for the scanner.
   */
  get settings() {
    return this._settings;
  }

  /**
   * Get state for the scanner.
   */
  get state() {
    return this._state;
  }

  /**
   * Set state for the scanner.
   */
  set state(newState) {
    let newStateParsed = newState;
    if (newState.endsWith('ed')) {
      newStateParsed = newState.slice(0, newState.length - 2);
    }

    if (this._states.includes(newStateParsed)) {
      this.setAttribute('state', newState);
    }
  }

  /**
   * Get formats for the scanner. The scannable types of barcodes.
   */
  get formats() {
    return this._formats;
  }

  /**
   * Set formats for the scanner. The scannable types of barcodes.
   */
  set formats(newFormats) {
    // Check if pass a string. Convert to an Array.
    if (Object.prototype.toString.call(newFormats) === '[object String]') {
      newFormats = newFormats.replaceAll(' ', '').split(',');
    }

    let newValidFormats = newFormats.filter((f) => {
      return Object.values(SUPPORTED_FORMATS_MAP).includes(f);
    });
    this._formats = newValidFormats;
    this.barcodeDetector = new BarcodeDetector({ formats: this.formats });

    this.setAttribute('formats', newValidFormats.join(', '));
  }

  /**
   * Get device for the scanner.
   */
  get device() {
    let current_device;
    if (this.track_settings && this.track_settings.deviceId) {
      current_device = this.track_settings.deviceId;
    } else {
      current_device = this._device;
    }

    return this._videoDevices.find((device) => {
      if (device.deviceId === current_device) {
        return true;
      }

      if (device.label === current_device) {
        return true;
      }

      return false;
    });
  }

  /**
   * Set device for the scanner.
   */
  set device(newDevice) {
    // Check if new device is in videoDevicesPromise.
    if (this._videoDevices) {
      let foundDevice;

      // newDevice is an object
      if (
        Object.prototype.toString.call(newDevice) === '[object Object]' ||
        Object.prototype.toString.call(newDevice) === '[object InputDeviceInfo]'
      ) {
        foundDevice = this._videoDevices.find((device) => {
          if (newDevice.deviceId && device.deviceId === newDevice.deviceId) {
            return true;
          }

          if (newDevice.label && device.label === newDevice.label) {
            return true;
          }

          return false;
        });
      }

      // newDevice is a string
      if (Object.prototype.toString.call(newDevice) === '[object String]') {
        foundDevice = this._videoDevices.find((device) => {
          if (device.deviceId === newDevice) {
            return true;
          }

          if (device.label === newDevice) {
            return true;
          }

          return false;
        });
      }

      // A device was found.
      if (foundDevice) {
        this.setAttribute('device', foundDevice.deviceId);
      }
    } else {
      // Default to updating the device if _videoDevices has not been set yet.
      this.setAttribute('device', newDevice);
    }
  }

  /**
   * Get web_worker for the scanner.
   */
  get web_worker() {
    return this._web_worker;
  }

  /**
   * Set web_worker for the scanner.
   */
  set web_worker(newWebWorker) {
    this.setAttribute('web-worker', newWebWorker);
  }

  /**
   * Get line_color for the scanner.
   */
  get line_color() {
    return this._line_color || this._default_line_color;
  }

  /**
   * Set line_color for the scanner.
   */
  set line_color(newLineColor) {
    if (HEX_COLOR_REGEX.test(newLineColor)) {
      this.setAttribute('line-color', newLineColor);
    }
  }

  /**
   * Get line_width for the scanner.
   */
  get line_width() {
    return this._line_width || this._default_line_width;
  }

  /**
   * Set line_width for the scanner.
   */
  set line_width(newLineWidth) {
    if (!isNaN(parseInt(newLineWidth)) && parseInt(newLineWidth) > -1) {
      this.setAttribute('line-width', parseInt(newLineWidth));
    }
  }

  /**
   * Get line_shadow_blur for the scanner.
   */
  get line_shadow_blur() {
    return this._line_shadow_blur || this._default_line_shadow_blur;
  }

  /**
   * Set line_shadow_blur for the scanner.
   */
  set line_shadow_blur(newLineShadowBlur) {
    if (
      !isNaN(parseInt(newLineShadowBlur)) &&
      parseInt(newLineShadowBlur) > -1
    ) {
      this.setAttribute('line-shadow-blur', parseInt(newLineShadowBlur));
    }
  }

  /**
   * Get line_shadow_color for the scanner.
   */
  get line_shadow_color() {
    return this._line_shadow_color || this._default_line_shadow_color;
  }

  /**
   * Set line_shadow_color for the scanner.
   */
  set line_shadow_color(newLineShadowColor) {
    if (HEX_COLOR_REGEX.test(newLineShadowColor)) {
      this.setAttribute('line-shadow-color', newLineShadowColor);
    }
  }

  /**
   * Get line_join for the scanner.
   */
  get line_join() {
    return this._line_join || this._default_line_join;
  }

  /**
   * Set line_join for the scanner.
   */
  set line_join(newLineJoin) {
    if (STROKE_LINE_JOINS.includes(newLineJoin)) {
      this.setAttribute('line-join', newLineJoin);
    }
  }

  /**
   * Get line_args for the scanner.
   */
  get line_args() {
    return {
      line_color: this.line_color,
      line_width: this.line_width,
      line_join: this.line_join,
      line_shadow_color: this.line_shadow_color,
      line_shadow_blur: this.line_shadow_blur,
    };
  }

  /**
   * Get videoDevicesPromise available to scan with.
   */
  get videoDevicesPromise() {
    return this._videoDevicesPromise;
  }

  /**
   * Get media_track_constraints
   *
   * Inital argmuents passed to getUserMedia.
   *
   * Defaults to {video: { facingMode: 'environment'}}
   */
  get media_track_constraints() {
    if (this._videoDevices) {
      if (this.device) {
        return { video: { deviceId: { exact: this.device.deviceId } } };
      }
    }
    return { video: { facingMode: 'environment' } };
  }

  /**
   * Reduce constraints based on supported constraints & track capabilities
   *
   * If a new constraint value is not a part of the supported constraints than
   * it will not be included in the return value.
   *
   * If the current track_capabilities is set then the new constraint value
   * will be checked to ensure that it valid. Invalid constraints will not be
   * included in the return value.
   *
   * @param {Object} obj - An Object
   * @param {Object} obj.current_constraints - Constraints already set.
   * @param {Object} obj.constraints_constant - Constant constraints ie Video or Image etc.
   */
  _reduce_constraints({ current_constraints, constraints_constant }) {
    const valid_constraints = Object.entries(current_constraints).filter(
      ([c_prop, c_value]) => {
        // Supported constraints.
        if (!(c_prop in this._supported_constraints)) {
          return false;
        }

        const c_type = constraints_constant[c_prop];

        if (c_type === 'String') {
          if (this.track && Object.keys(this.track_capabilities).length > 0) {
            if (this.track_capabilities[c_prop]) {
              if (this.track_capabilities[c_prop].indexOf(c_value) > -1) {
                return true;
              }
            } else {
              return false;
            }
          } else {
            return true;
          }
        }

        if (c_type === 'ConstrainDOMString') {
          if (this.track && Object.keys(this.track_capabilities).length > 0) {
            if (this.track_capabilities[c_prop]) {
              if (this.track_capabilities[c_prop].indexOf(c_value) > -1) {
                return true;
              }
            } else {
              return false;
            }
          } else {
            return true;
          }
        }

        if (c_type === 'Object') {
          // Check if stringified object.
          if (Object.prototype.toString.call(c_value) === '[object String]') {
            try {
              JSON.parse(c_value);
              return true;
            } catch (error) {
              return false;
            }
          }

          if (Object.prototype.toString.call(c_value) === '[object Object]') {
            return true;
          }
        }

        if (c_type === 'ConstrainDouble') {
          if (!isNaN(parseFloat(c_value))) {
            const c_value_float = parseFloat(c_value);
            if (this.track && Object.keys(this.track_capabilities).length > 0) {
              if (this.track_capabilities[c_prop]) {
                if (
                  c_value_float >= this.track_capabilities[c_prop].min &&
                  c_value_float <= this.track_capabilities[c_prop].max
                ) {
                  if ('step' in this.track_capabilities[c_prop]) {
                    if (
                      c_value_float % this.track_capabilities[c_prop].step ===
                      0
                    ) {
                      return true;
                    }
                  } else {
                    return true;
                  }
                }
              } else {
                return false;
              }
            } else {
              return true;
            }
          }
        }

        if (c_type === 'ConstrainULong') {
          if (!isNaN(parseInt(c_value))) {
            const c_value_float = parseInt(c_value);
            if (this.track && Object.keys(this.track_capabilities).length > 0) {
              if (this.track_capabilities[c_prop]) {
                if (
                  c_value_float >= this.track_capabilities[c_prop].min &&
                  c_value_float <= this.track_capabilities[c_prop].max
                ) {
                  if ('step' in this.track_capabilities[c_prop]) {
                    if (
                      c_value_float % this.track_capabilities[c_prop].step ===
                      0
                    ) {
                      return true;
                    }
                  } else {
                    return true;
                  }
                }
              } else {
                return false;
              }
            } else {
              return true;
            }
          }
        }

        if (c_type === 'Boolean') {
          if (c_value) {
            return true;
          } else {
            return false;
          }
        }
      }
    );

    return valid_constraints.reduce((acc, cur) => {
      return { ...acc, [cur[0]]: cur[1] };
    }, {});
  }

  /**
   * Get image_track_constraints
   *
   * Extra Image Constraints.
   *
   */
  get _image_track_constraints() {
    let output = {};
    Object.entries(IMAGE_TRACK_CONSTRAINTS).map(([c_prop, _]) => {
      if (this[`_${c_prop}`] !== undefined) {
        output[c_prop] = this[`_${c_prop}`];
      }
    });
    return output;
  }

  /**
   * Valid image_track_constraints
   *
   */
  get image_track_constraints() {
    return this._reduce_constraints({
      current_constraints: this._image_track_constraints,
      constraints_constant: IMAGE_TRACK_CONSTRAINTS,
    });
  }

  /**
   * Get video_track_constraints
   *
   * Extra Image Constraints.
   *
   */
  get _video_track_constraints() {
    let output = {};
    Object.entries(VIDEO_TRACK_CONSTRAINTS).map(([c_prop, _]) => {
      if (this[`_${c_prop}`] !== undefined) {
        output[c_prop] = this[`_${c_prop}`];
      }
    });
    return output;
  }

  /**
   * Valid video_track_constraints
   *
   */
  get video_track_constraints() {
    return this._reduce_constraints({
      current_constraints: this._video_track_constraints,
      constraints_constant: VIDEO_TRACK_CONSTRAINTS,
    });
  }

  /**
   * Element Map.
   *
   * @returns {object} Elements within the shadowRoot of this web component.
   */
  get _$Map() {
    const $s = this.shadowRoot;
    return {
      $cont: $s.querySelector('#multi-scanner-container'),
      $loading_msg: $s.querySelector('#multi-scanner-loading-msg'),
      $canvas_output: $s.querySelector('#multi-scanner-canvas-output'),
      $video_webcam: $s.querySelector('#multi-scanner-video-webcam'),
    };
  }

  /**
   * Draws a box on a canvas given Barcode data.
   *
   * TODO: Determine if a code can be drawn in a single canvas path.
   *
   * @method
   * @param {Object}  obj - An Object
   * @param {HTMLCanvasElement}  obj.canvas - a canvas element.
   * @param {Object}  obj.line_args - An Object.
   * @param {string}  obj.line_args.color - hex color of line, defaults to green.
   * @param {int}     obj.line_args.width - width of line, defaults to 4.
   */
  drawCode({ code, canvas, line_args }) {
    // Styling
    canvas.lineWidth = line_args.line_width;
    canvas.strokeStyle = line_args.line_color;
    canvas.shadowColor = line_args.line_shadow_color;
    canvas.shadowBlur = line_args.line_shadow_blur;
    canvas.lineJoin = line_args.line_join;

    // Drawing
    canvas.beginPath();
    canvas.moveTo(code.cornerPoints[0].x, code.cornerPoints[0].y);
    canvas.lineTo(code.cornerPoints[1].x, code.cornerPoints[1].y); // Top
    canvas.lineTo(code.cornerPoints[2].x, code.cornerPoints[2].y); // Right
    canvas.lineTo(code.cornerPoints[3].x, code.cornerPoints[3].y); // Bottom
    canvas.closePath(); // Left ( Links to first point. )
    canvas.stroke();
  }

  /**
   * Create a GUID/UUID string.
   * Globally/Universally Unique ID.
   *
   * @method
   * @returns {string} String representing a UUID.
   */
  guid() {
    function s4() {
      return Math.floor((1 + Math.random()) * 0x10000)
        .toString(16)
        .substring(1);
    }
    return (
      s4() +
      s4() +
      '-' +
      s4() +
      '-' +
      s4() +
      '-' +
      s4() +
      '-' +
      s4() +
      s4() +
      s4()
    );
  }

  /**
   * Applies Constraints to the track/stream.
   *
   * @method
   */

  applyConstraints() {
    const constraints = {
      ...this.image_track_constraints,
      // ...this.video_track_constraints,
    };
    if (this.track) {
      this.track.applyConstraints({
        advanced: Object.keys(constraints).map((k) => {
          return { [k]: constraints[k] };
        }),
      });
    }
  }

  /**
   * When camera is ready.
   *
   * @method
   */
  onCanPlay() {
    setTimeout(() => {
      this.applyConstraints();
    }, this._constraint_delay);
  }

  /**
   * Process the canvas image for Barcodes.
   *
   * Will run every animation frame refresh.
   *
   * @returns {Function} - Used inside a requestAnimationFrame call.
   */
  async processVideoStream() {
    if (
      this._$Map.$video_webcam.readyState ===
      this._$Map.$video_webcam.HAVE_ENOUGH_DATA
    ) {
      // UI.
      this._$Map.$canvas_output.style.display = 'initial';
      this._$Map.$video_webcam.style.display = 'initial';
      this._$Map.$loading_msg.style.display = 'none';

      let vidHeight = this._$Map.$video_webcam.videoHeight;
      let vidWidth = this._$Map.$video_webcam.videoWidth;

      this._$Map.$canvas_output.setAttribute('height', vidHeight);
      this._$Map.$canvas_output.setAttribute('width', vidWidth);

      /*
      this.canvas_output.drawImage(
        this._$Map.$video_webcam,
        0,
        0,
        vidWidth,
        vidHeight
      );
      this.imageData = this.canvas_output.getImageData(
        0,
        0,
        vidWidth,
        vidHeight
      );
      */

      // Could send the video element or ImageData.
      this.codes = await this.barcodeDetector.detect(this._$Map.$video_webcam);
      // this.codes = await this.barcodeDetector.detect(this.imageData);

      if (this.codes.length > 0) {
        this.codes.map((code) => {
          this.drawCode({
            code: code,
            canvas: this.canvas_output,
            line_args: this.line_args,
          });
        });
      }

      //   /*
      //   // TODO: Check Cooldown before on_scan function.
      //   if (this._cooldown) {
      //     // TODO: On Scan.
      //     if (this.on_scan) {
      //       if (this.on_scan.constructor.name === 'AsyncFunction') {
      //         await this.on_scan(this.code.data);
      //       } else {
      //         this.on_scan(this.code.data);
      //       }
      //     }

      //     // Start cooling.
      //     if (this.cooldown) {
      //       this._cooling();
      //     }
      //   }
      //   */
      // }
    }
    this.req_ani = requestAnimationFrame(this.processVideoStream.bind(this));
  }

  /**
   * Start the Scanner.
   *
   * @method
   */
  async start() {
    // Exit if already started.
    if (this.state === 'started') {
      return;
    }

    // Stop other cameras.
    await this.stopAll();

    // Establish Video Devices.
    if (!this.videoDevicesPromise) {
      await this.establishVideoDevices();
    }

    // Update the GUI.
    this._$Map.$canvas_output.style.display = 'initial';
    this._$Map.$video_webcam.style.display = 'initial';

    // Merge media & video track constraints.
    let media_constraints = {};
    media_constraints.video = {
      ...this.media_track_constraints.video,
      ...this.video_track_constraints,
    };

    // Start Video Stream.
    this.stream = await navigator.mediaDevices.getUserMedia(media_constraints);

    // Loading Message
    // this._$Map.$s.show();
    this._$Map.$video_webcam.srcObject = this.stream;
    this._$Map.$loading_msg.innerHTML = '⌛ Loading Scanner...';

    // CanPlay
    this._$Map.$video_webcam.removeEventListener(
      'canplay',
      this.onCanPlay.bind(this)
    );
    this._$Map.$video_webcam.addEventListener(
      'canplay',
      this.onCanPlay.bind(this)
    );

    // Before Start

    // TODO: WebWoker Canplay.
    // TODO: Request Animation Frame.
    if (this.worker && window.Worker) {
      /*
      // TODO: Process WebWoker
      // Worker.
      // this.worker = new Worker('../static/js/worker/getQR.js');
      this.worker = new Worker('./getQR.js');
      this.worker.onmessage = (evt) => {
        this.code = evt.data;
        this.process.call(this);
      };
      this.worker.onerror = (e) => console.log(e.message, e);

      this.video.addEventListener('canplay', () => {
        // UI.
        this._$Map.$loading_msg.style.display = 'none';
        this._$Map.$canvas_output.style.display = 'inital';
        this._$Map.$video.style.display = 'inital';

        let qr_scanner_css = {
          width: '100%',
          height: '100%',
        };
        let scanner_displays = [
          this._$Map.$canvas_output,
          this._$Map.$video,
        ];

        // Set Displays CSS
        scanner_displays.map(($d) => {
          Object.entries(qr_scanner_css).map(([k, v]) => {
            $d.style[k] = v;
          });
        });

        this.draw();
        this.process();
      });
      */
    } else {
      // Tick.
      this.req_ani = requestAnimationFrame(this.processVideoStream.bind(this));
    }

    // Plays Video Camera
    this._$Map.$video_webcam.play();

    // Change the state attribute & property.
    this._state = 'started';
    this.setAttribute('state', 'started');
  }

  /**
   * Pause the Scanner.
   *
   * @method
   */
  async pause() {
    // Exit if already started.
    if (this.state === 'paused') {
      return;
    }

    // Pause the webcam.
    this._$Map.$video_webcam.pause();

    // Change the state attribute & property.
    this._state = 'paused';
    this.setAttribute('state', 'paused');
  }

  /**
   * Stop the Scanner.
   *
   * @method
   */
  async stop() {
    // Exit if already stopped.
    if (this.state === 'stopped') {
      return;
    }

    // Stop All Tracks & remove _stream & _track properties.
    if (this.stream) {
      this.stream.getTracks().forEach(function (track) {
        track.stop();
      });
      delete this._stream;
      delete this._track;
    }

    // Remove the srcObject
    this._$Map.$video_webcam.srcObject = null;

    // TODO: Request Animation Frame
    if (this.req_ani) {
      cancelAnimationFrame(this.req_ani);
      delete this.req_ani;
    }

    /*
    // TODO: Terminate Webworker
    if (this.worker) {
      this.worker.terminate();
    }
    */

    // Update UI
    this._$Map.$loading_msg.innerHTML = 'Scanner Stopped';
    this._$Map.$loading_msg.style.display = 'initial';
    this._$Map.$canvas_output.style.display = 'none';

    // Change the state attribute & property.
    this._state = 'stopped';
    this.setAttribute('state', 'stopped');
  }

  /**
   * Stop & Start Scanner.
   *
   * @method
   */
  async restart() {
    await this.stop();
    await this.start();
  }

  /**
   * Stop all of the Cameras.
   *
   * @method
   */
  async stopAll() {
    if (Object.keys(this.cameras).length > 0) {
      await Promise.all(
        Object.entries(this.cameras).map(async ([, camera]) => {
          await camera.stop();
        })
      );
    }
  }

  /**
   * Processes ImageData checking for Barcodes.
   * This method is synchronous and does not use a WebWorker.
   *
   * @method
   */
  processNonWebWorker() {}

  /**
   * Processes ImageData checking for Barcodes.
   * This method uses a WebWorker.
   *
   * WebWorker is shared between all MultiScannerElements
   *
   * @method
   */
  processWebWorker() {}

  /**
   * Returns the videoDevicesPromise promise. If this promise does not
   * exist already, it calls requestVideoDevices.
   *
   * The this.videoDevicesPromise promise resolves an array of all the
   * attached devices and is shared across all instances
   *
   * Ex: this.__prototype__.videoDevicesPromise
   *
   * @method
   * @returns {Promise} - Resolves an Array of attched video devices.
   */
  async establishVideoDevices() {
    // Resolve the already requested Video Devices.
    if (this.videoDevicesPromise) {
      try {
        const vd = await this.videoDevicesPromise;
        if (vd) {
          return this.videoDevicesPromise;
        }
      } catch (e) {
        console.log(e);
      }
    }

    this.__proto__._videoDevicesPromise = this.requestVideoDevices();
    return this.videoDevicesPromise;
  }

  /**
   * Queries the 'Camera' browser permissions checking if it has been
   * previously granted. Preps user with an alert before prompting user
   * to allow access for the 'Camera' browser permission if not granted.
   *
   * Uses getUserMedia & enumerateDevices to get attached hardware and
   * sort them by 'videoinput' type.
   *
   * @method
   * @returns {Promise} - Resolves an Array of attched video devices.
   */
  async requestVideoDevices() {
    let camera_perm;
    try {
      camera_perm = await navigator.permissions.query({ name: 'camera' });
    } catch (e) {
      camera_perm = { state: 'denied' };
    }

    if (!navigator.mediaDevices || !navigator.mediaDevices.enumerateDevices) {
      throw new Error('enumerateDevices() not supported.');
    }

    if (camera_perm.state !== 'granted') {
      try {
        const allow = confirm(this.camera_access_message);
        if (!allow) {
          throw Error('Denied');
        }
      } catch (err) {
        throw new Error('Camera Permissions Required.');
      }
    }

    try {
      /* --- Necessary for getting device 'label' information. */
      const st = await navigator.mediaDevices.getUserMedia({ video: true });
      st.getTracks().forEach(function (track) {
        track.stop();
      });
      /* --- */

      const devices = await navigator.mediaDevices.enumerateDevices();
      let video_devices = devices.filter((d) => d.kind === 'videoinput');
      this.__proto__._videoDevices = video_devices;
      return video_devices;
    } catch (err) {
      throw new Error('Camera Permissions Required.');
    }
  }
}

customElements.define('multi-scanner', MultiScannerElement);

export { MultiScannerElement };