function MonitorStream(monitorData) {
  this.id = monitorData.id;
  this.connKey = monitorData.connKey;
  this.auth_relay = auth_relay;
  this.auth_hash = auth_hash;
  this.url = monitorData.url;
  this.url_to_zms = monitorData.url_to_zms;
  this.width = monitorData.width;
  this.height = monitorData.height;
  this.scale = 100;
  this.status = null;
  this.lastAlarmState = STATE_IDLE;
  this.statusCmdTimer = null;
  this.streamCmdTimer = null;
  this.streamCmdParms = {
    view: 'request',
    request: 'stream',
    connkey: this.connKey
  };
  this.ajaxQueue = null;
  this.type = monitorData.type;
  this.refresh = monitorData.refresh;

  this.buttons = {}; // index by name
  this.setButton = function(name, element) {
    this.buttons[name] = element;
  };

  this.bottomElement = null;
  this.setBottomElement = function(e) {
    if (!e) {
      console.error("Empty bottomElement");
    }
    this.bottomElement = e;
  };

  this.img_onerror = function() {
    console.log('Image stream has been stopped! Stopping streamCmd');
    this.streamCmdTimer = clearTimeout(this.streamCmdTimer);
  };
  this.img_onload = function() {
    if (!this.streamCmdTimer) {
      console.log('Image stream has loaded! starting streamCmd for '+this.connKey);
      this.streamCmdTimer = setTimeout(this.streamCmdQuery.bind(this), statusRefreshTimeout);
    }
  };

  this.element = null;
  this.getElement = function() {
    if (this.element) return this.element;
    this.element = document.getElementById('liveStream'+this.id);
    if (!this.element) {
      console.error("No img for #liveStream"+this.id);
    }
    return this.element;
  };
  this.getFrame = function() {
    if (this.frame) return this.frame;
    this.frame = document.getElementById('imageFeed'+this.id);
    if (!this.frame) {
      console.error("No frame div for #imageFeed"+this.id);
    }
    return this.frame;
  };

  /* if the img element didn't have a src, this would fill it in, causing it to show. */
  this.show = function() {
    const stream = this.getElement();
    if (!stream.src) {
      stream.src = this.url_to_zms+"&mode=single&scale=100&connkey="+this.connKey+this.auth_relay;
    }
  };

  /* scale should be '0' for auto, or an integer value
   * width should be auto, 100%, integer +px
   * height should be auto, 100%, integer +px
   * */
  this.setScale = function(newscale, width, height) {
    const img = this.getElement();
    if (!img) {
      console.log("No img in setScale");
      return;
    }

    this.scale = newscale;

    // Scale the frame
    monitor_frame = $j('#monitor'+this.id);
    if (!monitor_frame) {
      console.log('Error finding frame');
      return;
    }

    if (((newscale == '0') || (newscale==0) || (newscale=='auto')) && (width=='auto' || !width)) {
      if (!this.bottomElement) {
        newscale = parseInt(100*monitor_frame.width() / this.width);
        // We don't want to change the existing css, cuz it might be 59% or 123px or auto;
        width = monitor_frame.css('width');
      } else {
        var newSize = scaleToFit(this.width, this.height, $j(img), $j(this.bottomElement));
        width = newSize.width+'px';
        height = newSize.height+'px';
        newscale = parseInt(newSize.autoScale);
      }
    } else if (parseInt(width) || parseInt(height)) {
      if (width) {
        newscale = parseInt(100*parseInt(width)/this.width);
      } else if (height) {
        newscale = parseInt(100*parseInt(height)/this.height);
        width = parseInt(this.width * newscale / 100)+'px';
      }
    } else {
      // a numeric scale, must take actual monitor dimensions and calculate
      width = Math.round(parseInt(this.width) * newscale / 100)+'px';
      height = Math.round(parseInt(this.height) * newscale / 100)+'px';
    }
    if (width && (width != '0px') &&
      ((monitor_frame[0].style.width===undefined) || (-1 == monitor_frame[0].style.width.search('%')))
      ) {
      monitor_frame.css('width', width);
    }

    //img.style.width = width;
    if (height && height != '0px') img.style.height = height;
    img.setAttribute('width', '100%');
    img.setAttribute('height', 'auto');
    this.setStreamScale(newscale);
  }; // setscale

  this.setStreamScale = function(newscale) {
    const img = this.getElement();
    if (!img) {
      console.log("No img in setScale");
      return;
    }
    const stream_frame = $j('#monitor'+this.id);
    if (!newscale) {
      newscale = parseInt(100*parseInt(stream_frame.width())/this.width);
    }
    if (img.nodeName == 'IMG') {
      if (newscale > 100) newscale = 100; // we never request a larger image, as it just wastes bandwidth
      if (newscale <= 0) newscale = 100;
      const oldSrc = img.src;
      if (!oldSrc) {
        console.log('No src on img?!');
        console.log(img);
        return;
      }
      const newSrc = oldSrc.replace(/scale=\d+/i, 'scale='+newscale);
      if (newSrc != oldSrc) {
        this.streamCmdTimer = clearTimeout(this.streamCmdTimer);
        // We know that only the first zms will get the command because the
        // second can't open the commandQueue until the first exits
        // This is necessary because safari will never close the first image
        if ((-1 != img.src.search('connkey')) && (-1 != img.src.search('mode=single'))) {
          console.log("Sending quit");
          this.streamCommand(CMD_QUIT);
        }
        console.log("Changing src to " + newSrc);
        img.src = '';
        img.src = newSrc;
      }
    }
  }; // setscale

  this.start = function(delay) {
    // zms stream
    const stream = this.getElement();
    if (!stream) return;
    if (!stream.src) {
      // Website Monitors won't have an img tag, neither will video
      console.log('No src for #liveStream'+this.id);
      console.log(stream);
      return;
    }
    this.statusCmdTimer = clearTimeout(this.statusCmdTimer);
    // Step 1 make sure we are streaming instead of a static image
    if (stream.getAttribute('loading') == 'lazy') {
      stream.setAttribute('loading', 'eager');
    }
    src = stream.src.replace(/mode=single/i, 'mode=jpeg');
    if (-1 == src.search('connkey')) {
      src += '&connkey='+this.connKey;
    }
    if (stream.src != src) {
      console.log("Setting to streaming: " + src);
      stream.src = '';
      stream.src = src;
    }
    stream.onerror = this.img_onerror.bind(this);
    stream.onload = this.img_onload.bind(this);
  }; // this.start

  this.stop = function() {
    if ( 0 ) {
      const stream = this.getElement();
      if (!stream) return;
      src = stream.src.replace(/mode=jpeg/i, 'mode=single');
      if (stream.src != src) {
        console.log("Setting to stopped");
        stream.src = '';
        stream.src = src;
      }
    }
    this.streamCommand(CMD_STOP);
    this.statusCmdTimer = clearTimeout(this.statusCmdTimer);
    this.streamCmdTimer = clearTimeout(this.streamCmdTimer);
  };
  this.kill = function() {
    const stream = this.getElement();
    if (!stream) return;
    stream.onerror = null;
    stream.onload = null;
    this.statusCmdTimer = clearTimeout(this.statusCmdTimer);
    this.streamCmdTimer = clearTimeout(this.streamCmdTimer);
  };
  this.pause = function() {
    this.streamCommand(CMD_PAUSE);
  };
  this.play = function() {
    this.streamCommand(CMD_PLAY);
  };

  this.eventHandler = function(event) {
    console.log(event);
  };

  this.onclick = function(evt) {
    console.log('onclick');
  };

  this.setup_onclick = function(func) {
    if (func) {
      this.onclick = func;
    }
    if (this.onclick) {
      const el = this.getFrame();
      if (!el) return;
      el.addEventListener('click', this.onclick, false);
    }
  };

  this.disable_onclick = function() {
    const el = this.getElement();
    if (!el) return;
    el.removeEventListener('click', this.onclick);
  };

  this.onpause = function() {
    console.log('onpause');
  };
  this.setup_onpause = function(func) {
    this.onpause = func;
  };
  this.onplay = null;
  this.setup_onplay = function(func) {
    this.onplay = func;
  };

  this.setStateClass = function(jobj, stateClass) {
    if (!jobj) {
      console.log("No obj in setStateClass");
      return;
    }
    if (!jobj.hasClass(stateClass)) {
      if (stateClass != 'alarm') jobj.removeClass('alarm');
      if (stateClass != 'alert') jobj.removeClass('alert');
      if (stateClass != 'idle') jobj.removeClass('idle');

      jobj.addClass(stateClass);
    }
  };

  this.setAlarmState = function(alarmState) {
    var stateClass = '';
    if (alarmState == STATE_ALARM) {
      stateClass = 'alarm';
    } else if (alarmState == STATE_ALERT) {
      stateClass = 'alert';
    }

    const stateValue = $j('#stateValue'+this.id);
    if (stateValue.length) {
      stateValue.text(stateStrings[alarmState]);
      if (stateClass) {
        stateValue.addClass(stateClass);
      } else {
        stateValue.removeClass();
      }
    }
    const monitorFrame = $j('#monitor'+this.id);
    if (monitorFrame.length) this.setStateClass(monitorFrame, stateClass);

    const isAlarmed = ( alarmState == STATE_ALARM || alarmState == STATE_ALERT );
    const wasAlarmed = ( this.lastAlarmState == STATE_ALARM || this.lastAlarmState == STATE_ALERT );

    const newAlarm = ( isAlarmed && !wasAlarmed );
    const oldAlarm = ( !isAlarmed && wasAlarmed );

    if (newAlarm) {
      if (ZM_WEB_SOUND_ON_ALARM) {
        // Enable the alarm sound
        const isIE = window.document.documentMode ? true : false;
        if (!isIE) {
          $j('#alarmSound').removeClass('hidden');
        } else {
          $j('#MediaPlayer').trigger('play');
        }
      }
      if (ZM_WEB_POPUP_ON_ALARM) {
        window.focus();
      }
      if (this.onalarm) {
        this.onalarm();
      }
    }
    if (oldAlarm) { // done with an event do a refresh
      if (ZM_WEB_SOUND_ON_ALARM) {
        // Disable alarm sound
        const isIE = window.document.documentMode ? true : false;
        if (!isIE) {
          $j('#alarmSound').addClass('hidden');
        } else {
          $j('#MediaPlayer').trigger('pause');
        }
      }
      if (this.onalarm) {
        this.onalarm();
      }
    }
    this.lastAlarmState = alarmState;
  }; // end function setAlarmState( currentAlarmState )

  this.onalarm = null;
  this.setup_onalarm = function(func) {
    this.onalarm = func;
  };

  this.onFailure = function(jqxhr, textStatus, error) {
    // Assuming temporary problem, retry in a bit.

    clearTimeout(this.streamCmdTimer);
    this.streamCmdTimer = setTimeout(this.streamCmdQuery.bind(this), 10*statusRefreshTimeout);
    logAjaxFail(jqxhr, textStatus, error);
    if (error == 'Unauthorized') {
      window.location.reload();
    }
  };

  this.getStreamCmdResponse = function(respObj, respText) {
    var stream = this.getElement();
    if (!stream) {
      return;
    }

    //watchdogOk('stream');
    //this.streamCmdTimer = clearTimeout(this.streamCmdTimer);

    if (respObj.result == 'Ok') {
      if (respObj.status) {
        const streamStatus = this.status = respObj.status;

        if (this.type != 'WebSite') {
          let viewingFPSValue = $j('#viewingFPSValue'+this.id);
          if (!viewingFPSValue.length) viewingFPSValue = $j('#fpsValue'+this.id);
          const captureFPSValue = $j('#captureFPSValue'+this.id);
          const analysisFPSValue = $j('#analysisFPSValue'+this.id);

          this.status.fps = this.status.fps.toLocaleString(undefined, {minimumFractionDigits: 1, maximumFractionDigits: 1});
          if (viewingFPSValue.length && (viewingFPSValue.text != this.status.fps)) {
            viewingFPSValue.text(this.status.fps);
          }
          this.status.analysisfps = this.status.analysisfps.toLocaleString(undefined, {minimumFractionDigits: 1, maximumFractionDigits: 1});
          if (analysisFPSValue.length && (analysisFPSValue.text != this.status.analysisfps)) {
            analysisFPSValue.text(this.status.analysisfps);
          }
          this.status.capturefps = this.status.capturefps.toLocaleString(undefined, {minimumFractionDigits: 1, maximumFractionDigits: 1});
          if (captureFPSValue.length && (captureFPSValue.text != this.status.capturefps)) {
            captureFPSValue.text(this.status.capturefps);
          }

          const levelValue = $j('#levelValue');
          if (levelValue.length) {
            levelValue.text(this.status.level);
            var newClass = 'ok';
            if (this.status.level > 95) {
              newClass = 'alarm';
            } else if (this.status.level > 80) {
              newClass = 'alert';
            }
            levelValue.removeClass();
            levelValue.addClass(newClass);
          }

          const delayString = secsToTime(this.status.delay);

          if (this.status.paused == true) {
            $j('#modeValue'+this.id).text('Paused');
            $j('#rate'+this.id).addClass('hidden');
            $j('#delayValue'+this.id).text(delayString);
            $j('#delay'+this.id).removeClass('hidden');
            $j('#level'+this.id).removeClass('hidden');
            this.onpause();
          } else if (this.status.delayed == true) {
            $j('#modeValue'+this.id).text('Replay');
            $j('#rateValue'+this.id).text(this.status.rate);
            $j('#rate'+this.id).removeClass('hidden');
            $j('#delayValue'+this.id).text(delayString);
            $j('#delay'+this.id).removeClass('hidden');
            $j('#level'+this.id).removeClass('hidden');
            if (this.status.rate == 1) {
              if (this.onplay) this.onplay();
            } else if (this.status.rate > 0) {
              if (this.status.rate < 1) {
                streamCmdSlowFwd(false);
              } else {
                streamCmdFastFwd(false);
              }
            } else {
              if (this.status.rate > -1) {
                streamCmdSlowRev(false);
              } else {
                streamCmdFastRev(false);
              }
            } // rate
          } else {
            $j('#modeValue'+this.id).text('Live');
            $j('#rate'+this.id).addClass('hidden');
            $j('#delay'+this.id).addClass('hidden');
            $j('#level'+this.id).addClass('hidden');
            if (this.onplay) this.onplay();
          } // end if paused or delayed

          $j('#zoomValue'+this.id).text(this.status.zoom);
          if (this.status.zoom == '1.0') {
            $j('#zoom'+this.id).addClass('hidden');
          }
          if ('zoomOutBtn' in this.buttons) {
            if (this.status.zoom == '1.0') {
              setButtonState('zoomOutBtn', 'unavail');
            } else {
              setButtonState('zoomOutBtn', 'inactive');
            }
          }
        } // end if compact montage

        this.setAlarmState(this.status.state);

        if (canEdit.Monitors) {
          if (streamStatus.enabled) {
            if ('enableAlarmButton' in this.buttons) {
              if (!this.buttons.enableAlarmButton.hasClass('disabled')) {
                this.buttons.enableAlarmButton.addClass('disabled');
                this.buttons.enableAlarmButton.prop('title', disableAlarmsStr);
              }
            }
            if ('forceAlarmButton' in this.buttons) {
              if (streamStatus.forced) {
                if (! this.buttons.forceAlarmButton.hasClass('disabled')) {
                  this.buttons.forceAlarmButton.addClass('disabled');
                  this.buttons.forceAlarmButton.prop('title', cancelForcedAlarmStr);
                }
              } else {
                if (this.buttons.forceAlarmButton.hasClass('disabled')) {
                  this.buttons.forceAlarmButton.removeClass('disabled');
                  this.buttons.forceAlarmButton.prop('title', forceAlarmStr);
                }
              }
              this.buttons.forceAlarmButton.prop('disabled', false);
            }
          } else {
            if ('enableAlarmButton' in this.buttons) {
              this.buttons.enableAlarmButton.removeClass('disabled');
              this.buttons.enableAlarmButton.prop('title', enableAlarmsStr);
            }
            if ('forceAlarmButton' in this.buttons) {
              this.buttons.forceAlarmButton.prop('disabled', true);
            }
          }
          if ('enableAlarmButton' in this.buttons) {
            this.buttons.enableAlarmButton.prop('disabled', false);
          }
        } // end if canEdit.Monitors

        if (this.status.auth) {
          if (this.status.auth != this.auth_hash) {
            // Don't reload the stream because it causes annoying flickering. Wait until the stream breaks.
            console.log("Changed auth from " + this.auth_hash + " to " + this.status.auth);
            this.streamCmdParms.auth = auth_hash = this.auth_hash = this.status.auth;
          }
        } // end if have a new auth hash
      } // end if has state
    } else {
      console.error(respObj.message);
      // Try to reload the image stream.
      if (stream.src) {
        console.log('Reloading stream: ' + stream.src);
        src = stream.src.replace(/rand=\d+/i, 'rand='+Math.floor((Math.random() * 1000000) ));
        src = src.replace(/auth=\w+/i, 'auth='+this.auth_hash);
        // Maybe updated auth
        if (src != stream.src) {
          stream.src = '';
          stream.src = src;
        } else {
          console.log("Failed to update rand on stream src");
        }
      }
    } // end if Ok or not
  };

  this.statusQuery = function() {
    this.streamCommand(CMD_QUERY);
    this.statusCmdTimer = setTimeout(this.statusQuery.bind(this), statusRefreshTimeout);
  };

  this.streamCmdQuery = function(resent) {
    if (this.type != 'WebSite') {
      this.streamCmdParms.command = CMD_QUERY;
      this.streamCmdReq(this.streamCmdParms);
    }
    this.streamCmdTimer = setTimeout(this.streamCmdQuery.bind(this), statusRefreshTimeout);
  };

  this.streamCommand = function(command) {
    if (typeof(command) == 'object') {
      for (const key in command) this.streamCmdParms[key] = command[key];
    } else {
      this.streamCmdParms.command = command;
    }
    this.streamCmdReq(this.streamCmdParms);
  };

  this.alarmCommand = function(command) {
    if (this.ajaxQueue) {
      console.log("Aborting in progress ajax for alarm");
      // Doing this for responsiveness, but we could be aborting something important. Need smarter logic
      this.ajaxQueue.abort();
    }
    const alarmCmdParms = Object.assign({}, this.streamCmdParms);
    alarmCmdParms.request = 'alarm';
    alarmCmdParms.command = command;
    alarmCmdParms.id = this.id;

    this.ajaxQueue = jQuery.ajaxQueue({
      url: this.url + (auth_relay?'?'+auth_relay:''),
      xhrFields: {withCredentials: true},
      data: alarmCmdParms,
      dataType: "json"
    })
        .done(this.getStreamCmdResponse.bind(this))
        .fail(this.onFailure.bind(this));
  };

  if (this.type != 'WebSite') {
    $j.ajaxSetup({timeout: AJAX_TIMEOUT});
    if (auth_hash) {
      this.streamCmdParms.auth = auth_hash;
    } else if (auth_relay) {
      this.streamCmdParms.auth_relay = auth_relay;
    }

    this.streamCmdReq = function(streamCmdParms) {
      this.ajaxQueue = jQuery.ajaxQueue({
        url: this.url,
        xhrFields: { withCredentials: true },
        data: streamCmdParms,
        dataType: "json"
      })
          .done(this.getStreamCmdResponse.bind(this))
          .fail(this.onFailure.bind(this));
    };
  }
  this.analyse_frames = true;
  this.show_analyse_frames = function(toggle) {
    this.analyse_frames = toggle;
    this.streamCmdParms.command = this.analyse_frames ? CMD_ANALYZE_ON : CMD_ANALYZE_OFF;
    this.streamCmdReq(this.streamCmdParms);
  };
} // end function MonitorStream
