constructor(player, videoContainer, video, vrCanvas, config) {
    super();

    /** @private {boolean} */
    this.enabled_ = true;

    /** @private {shaka.extern.UIConfiguration} */
    this.config_ = config;

    /** @private {shaka.cast.CastProxy} */
    this.castProxy_ = new shaka.cast.CastProxy(
        video, player, this.config_.castReceiverAppId,
        this.config_.castAndroidReceiverCompatible);

    /** @private {boolean} */
    this.castAllowed_ = true;

    /** @private {HTMLMediaElement} */
    this.video_ = this.castProxy_.getVideo();

    /** @private {HTMLMediaElement} */
    this.localVideo_ = video;

    /** @private {shaka.Player} */
    this.player_ = this.castProxy_.getPlayer();

    /** @private {shaka.Player} */
    this.localPlayer_ = player;

    /** @private {!HTMLElement} */
    this.videoContainer_ = videoContainer;

    /** @private {?HTMLCanvasElement} */
    this.vrCanvas_ = vrCanvas;

    /** @private {shaka.extern.IAdManager} */
    this.adManager_ = this.player_.getAdManager();

    /** @private {?shaka.extern.IAd} */
    this.ad_ = null;

    /** @private {?shaka.extern.IUISeekBar} */
    this.seekBar_ = null;

    /** @private {boolean} */
    this.isSeeking_ = false;

    /** @private {!Array<!HTMLElement>} */
    this.menus_ = [];

    /**
     * Individual controls which, when hovered or tab-focused, will force the
     * controls to be shown.
     * @private {!Array<!Element>}
     */
    this.showOnHoverControls_ = [];

    /** @private {boolean} */
    this.recentMouseMovement_ = false;

    /**
     * This timer is used to detect when the user has stopped moving the mouse
     * and we should fade out the ui.
     *
     * @private {shaka.util.Timer}
     */
    this.mouseStillTimer_ = new shaka.util.Timer(() => {
      this.onMouseStill_();
    });

    /**
     * This timer is used to delay the fading of the UI.
     *
     * @private {shaka.util.Timer}
     */
    this.fadeControlsTimer_ = new shaka.util.Timer(() => {
      this.controlsContainer_.removeAttribute('shown');
      this.dispatchVisibilityEvent_();
      this.computeShakaTextContainerSize_();

      if (this.contextMenu_) {
        this.contextMenu_.closeMenu();
      }

      // If there's an overflow menu open, keep it this way for a couple of
      // seconds in case a user immediately initiates another mouse move to
      // interact with the menus. If that didn't happen, go ahead and hide
      // the menus.
      this.hideSettingsMenusTimer_.tickAfter(
          /* seconds= */ this.config_.closeMenusDelay);
    });

    /**
     * This timer will be used to hide all settings menus. When the timer ticks
     * it will force all controls to invisible.
     *
     * Rather than calling the callback directly, |Controls| will always call it
     * through the timer to avoid conflicts.
     *
     * @private {shaka.util.Timer}
     */
    this.hideSettingsMenusTimer_ = new shaka.util.Timer(() => {
      for (const menu of this.menus_) {
        shaka.ui.Utils.setDisplay(menu, /* visible= */ false);
      }
      if (this.config_.enableTooltips) {
        this.controlsButtonPanel_.classList.add('shaka-tooltips-on');
      }
    });

    /**
     * This timer is used to regularly update the time and seek range elements
     * so that we are communicating the current state as accurately as possibly.
     *
     * Unlike the other timers, this timer does not "own" the callback because
     * this timer is acting like a heartbeat.
     *
     * @private {shaka.util.Timer}
     */
    this.timeAndSeekRangeTimer_ = new shaka.util.Timer(() => {
      // Suppress timer-based updates if the controls are hidden.
      if (this.isOpaque()) {
        this.updateTimeAndSeekRange_();
      }
    });

    /** @private {?number} */
    this.lastTouchEventTime_ = null;

    /** @private {!Array<!shaka.extern.IUIElement>} */
    this.elements_ = [];

    /** @private {shaka.ui.Localization} */
    this.localization_ = shaka.ui.Controls.createLocalization_();

    /** @private {shaka.util.EventManager} */
    this.eventManager_ = new shaka.util.EventManager();

    /** @private {?shaka.ui.VRManager} */
    this.vr_ = null;

    // Configure and create the layout of the controls
    this.configure(this.config_);
    this.addEventListeners_();
    this.setupMediaSession_();

    /**
     * The pressed keys set is used to record which keys are currently pressed
     * down, so we can know what keys are pressed at the same time.
     * Used by the focusInsideOverflowMenu_() function.
     * @private {!Set<string>}
     */
    this.pressedKeys_ = new Set();

    // We might've missed a caststatuschanged event from the proxy between
    // the controls creation and initializing. Run onCastStatusChange_()
    // to ensure we have the casting state right.
    this.onCastStatusChange_();

    // Start this timer after we are finished initializing everything,
    this.timeAndSeekRangeTimer_.tickEvery(this.config_.refreshTickInSeconds);

    this.eventManager_.listen(this.localization_,
        shaka.ui.Localization.LOCALE_CHANGED, (e) => {
          const locale = e['locales'][0];
          this.adManager_.setLocale(locale);
          this.videoContainer_.setAttribute('lang', locale);
        });

    this.adManager_.initInterstitial(
        this.getClientSideAdContainer(), this.localPlayer_, this.localVideo_);

    this.eventManager_.listen(this.player_, 'texttrackvisibility', () => {
      this.computeShakaTextContainerSize_();
    });
  }
@param {!shaka.Player} player
@param {!HTMLElement} videoContainer
@param {!HTMLMediaElement} video
@param {?HTMLCanvasElement} vrCanvas
@param {shaka.extern.UIConfiguration} config