constructor(video, videoContainer) {
    goog.asserts.assert(videoContainer, 'videoContainer should be valid.');

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

    /** @private {!Array<!shaka.text.Cue>} */
    this.cues_ = [];

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

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

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

    /** @private {?shaka.extern.TextDisplayerConfiguration} */
    this.config_ = null;

    /** @type {HTMLElement} */
    this.textContainer_ = shaka.util.Dom.createHTMLElement('div');
    this.textContainer_.classList.add('shaka-text-container');

    // Set the subtitles text-centered by default.
    this.textContainer_.style.textAlign = 'center';

    // Set the captions in the middle horizontally by default.
    this.textContainer_.style.display = 'flex';
    this.textContainer_.style.flexDirection = 'column';
    this.textContainer_.style.alignItems = 'center';

    // Set the captions at the bottom by default.
    this.textContainer_.style.justifyContent = 'flex-end';

    /** @private {shaka.util.Timer} */
    this.captionsTimer_ = new shaka.util.Timer(() => {
      if (!this.video_.paused) {
        this.updateCaptions_();
      }
    });
    this.configureCaptionsTimer_();

    /**
     * Maps cues to cue elements. Specifically points out the wrapper element of
     * the cue (e.g. the HTML element to put nested cues inside).
     * @private {Map<!shaka.text.Cue, !{
     *   cueElement: !HTMLElement,
     *   regionElement: HTMLElement,
     *   wrapper: !HTMLElement
     * }>}
     */
    this.currentCuesMap_ = new Map();

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

    this.eventManager_.listen(document, 'fullscreenchange', () => {
      this.updateCaptions_(/* forceUpdate= */ true);
    });

    this.eventManager_.listen(this.video_, 'seeking', () => {
      this.updateCaptions_(/* forceUpdate= */ true);
    });

    this.eventManager_.listen(this.video_, 'ratechange', () => {
      this.configureCaptionsTimer_();
    });

    // From: https://html.spec.whatwg.org/multipage/media.html#dom-video-videowidth
    // Whenever the natural width or natural height of the video changes
    // (including, for example, because the selected video track was changed),
    // if the element's readyState attribute is not HAVE_NOTHING, the user
    // agent must queue a media element task given the media element to fire an
    // event named resize at the media element.
    this.eventManager_.listen(this.video_, 'resize', () => {
      const element = /** @type {!HTMLVideoElement} */ (this.video_);
      const width = element.videoWidth;
      const height = element.videoHeight;
      if (width && height) {
        this.aspectRatio_ = width / height;
      } else {
        this.aspectRatio_ = null;
      }
    });

    /** @private {ResizeObserver} */
    this.resizeObserver_ = null;
    if ('ResizeObserver' in window) {
      this.resizeObserver_ = new ResizeObserver(() => {
        this.updateCaptions_(/* forceUpdate= */ true);
      });
      this.resizeObserver_.observe(this.textContainer_);
    }

    /** @private {Map<string, !HTMLElement>} */
    this.regionElements_ = new Map();
  }
Constructor.
@param {HTMLMediaElement} video
@param {HTMLElement} videoContainer