constructor(mediaElement, videoContainer = null, dependencyInjector) {
    super();

    /** @private {shaka.Player.LoadMode} */
    this.loadMode_ = shaka.Player.LoadMode.NOT_LOADED;

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

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

    /**
     * Since we may not always have a text displayer created (e.g. before |load|
     * is called), we need to track what text visibility SHOULD be so that we
     * can ensure that when we create the text displayer. When we create our
     * text displayer, we will use this to show (or not show) text as per the
     * user's requests.
     *
     * @private {boolean}
     */
    this.isTextVisible_ = false;

    /**
     * For listeners scoped to the lifetime of the Player instance.
     * @private {shaka.util.EventManager}
     */
    this.globalEventManager_ = new shaka.util.EventManager();

    /**
     * For listeners scoped to the lifetime of the media element attachment.
     * @private {shaka.util.EventManager}
     */
    this.attachEventManager_ = new shaka.util.EventManager();

    /**
     * For listeners scoped to the lifetime of the loaded content.
     * @private {shaka.util.EventManager}
     */
    this.loadEventManager_ = new shaka.util.EventManager();

    /**
     * For listeners scoped to the lifetime of the loaded content.
     * @private {shaka.util.EventManager}
     */
    this.trickPlayEventManager_ = new shaka.util.EventManager();

    /**
     * For listeners scoped to the lifetime of the ad manager.
     * @private {shaka.util.EventManager}
     */
    this.adManagerEventManager_ = new shaka.util.EventManager();

    /** @private {shaka.net.NetworkingEngine} */
    this.networkingEngine_ = null;

    /** @private {shaka.drm.DrmEngine} */
    this.drmEngine_ = null;

    /** @private {shaka.media.MediaSourceEngine} */
    this.mediaSourceEngine_ = null;

    /** @private {shaka.media.Playhead} */
    this.playhead_ = null;

    /**
     * Incremented whenever a top-level operation (load, attach, etc) is
     * performed.
     * Used to determine if a load operation has been interrupted.
     * @private {number}
     */
    this.operationId_ = 0;

    /** @private {!shaka.util.Mutex} */
    this.mutex_ = new shaka.util.Mutex();

    /**
     * The playhead observers are used to monitor the position of the playhead
     * and some other source of data (e.g. buffered content), and raise events.
     *
     * @private {shaka.media.PlayheadObserverManager}
     */
    this.playheadObservers_ = null;

    /**
     * This is our control over the playback rate of the media element. This
     * provides the missing functionality that we need to provide trick play,
     * for example a negative playback rate.
     *
     * @private {shaka.media.PlayRateController}
     */
    this.playRateController_ = null;

    // We use the buffering observer and timer to track when we move from having
    // enough buffered content to not enough. They only exist when content has
    // been loaded and are not re-used between loads.
    /** @private {shaka.util.Timer} */
    this.bufferPoller_ = null;

    /** @private {shaka.media.BufferingObserver} */
    this.bufferObserver_ = null;

    /**
     * @private {shaka.media.RegionTimeline<
     *     shaka.extern.TimelineRegionInfo>}
     */
    this.regionTimeline_ = null;

    /**
     * @private {shaka.media.RegionTimeline<
     *     shaka.extern.MetadataTimelineRegionInfo>}
     */
    this.metadataRegionTimeline_ = null;

    /**
     * @private {shaka.media.RegionTimeline<
     *     shaka.extern.EmsgTimelineRegionInfo>}
     */
    this.emsgRegionTimeline_ = null;

    /** @private {shaka.util.CmcdManager} */
    this.cmcdManager_ = null;

    /** @private {shaka.util.CmsdManager} */
    this.cmsdManager_ = null;

    // This is the canvas element that will be used for rendering LCEVC
    // enhanced frames.
    /** @private {?HTMLCanvasElement} */
    this.lcevcCanvas_ = null;

    // This is the LCEVC Decoder object to decode LCEVC.
    /** @private {?shaka.lcevc.Dec} */
    this.lcevcDec_ = null;

    /** @private {shaka.media.QualityObserver} */
    this.qualityObserver_ = null;

    /** @private {shaka.media.StreamingEngine} */
    this.streamingEngine_ = null;

    /** @private {shaka.extern.ManifestParser} */
    this.parser_ = null;

    /** @private {?shaka.extern.ManifestParser.Factory} */
    this.parserFactory_ = null;

    /** @private {?shaka.extern.Manifest} */
    this.manifest_ = null;

    /** @private {?string} */
    this.assetUri_ = null;

    /** @private {?string} */
    this.mimeType_ = null;

    /** @private {?number|Date} */
    this.startTime_ = null;

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

    /** @private {shaka.extern.AbrManager} */
    this.abrManager_ = null;

    /**
     * The factory that was used to create the abrManager_ instance.
     * @private {?shaka.extern.AbrManager.Factory}
     */
    this.abrManagerFactory_ = null;

    /**
     * Contains an ID for use with creating streams.  The manifest parser should
     * start with small IDs, so this starts with a large one.
     * @private {number}
     */
    this.nextExternalStreamId_ = 1e9;

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

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

    /** @private {number} */
    this.completionPercent_ = -1;

    /** @private {?shaka.extern.PlayerConfiguration} */
    this.config_ = this.defaultConfig_();

    /** @private {!Object} */
    this.lowLatencyConfig_ =
        shaka.util.PlayerConfiguration.createDefaultForLL();

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

    /** @private {number} */
    this.rebufferingCount_ = -1;

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

    /**
     * The TextDisplayerFactory that was last used to make a text displayer.
     * Stored so that we can tell if a new type of text displayer is desired.
     * @private {?shaka.extern.TextDisplayer.Factory}
     */
    this.lastTextFactory_;

    /** @private {shaka.extern.Resolution} */
    this.maxHwRes_ = {width: Infinity, height: Infinity};

    /** @private {!shaka.media.ManifestFilterer} */
    this.manifestFilterer_ = new shaka.media.ManifestFilterer(
        this.config_, this.maxHwRes_, null);

    /** @private {!Array<shaka.media.PreloadManager>} */
    this.createdPreloadManagers_ = [];

    /** @private {shaka.util.Stats} */
    this.stats_ = null;

    /** @private {!shaka.media.AdaptationSetCriteria} */
    this.currentAdaptationSetCriteria_ =
        this.config_.adaptationSetCriteriaFactory();
    this.currentAdaptationSetCriteria_.configure({
      language: this.config_.preferredAudioLanguage,
      role: this.config_.preferredVariantRole,
      channelCount: 0,
      hdrLevel: this.config_.preferredVideoHdrLevel,
      spatialAudio: this.config_.preferSpatialAudio,
      videoLayout: this.config_.preferredVideoLayout,
      audioLabel: this.config_.preferredAudioLabel,
      videoLabel: this.config_.preferredVideoLabel,
      codecSwitchingStrategy:
          this.config_.mediaSource.codecSwitchingStrategy,
      audioCodec: '',
      activeAudioCodec: '',
      activeAudioChannelCount: 0,
      preferredAudioCodecs: this.config_.preferredAudioCodecs,
      preferredAudioChannelCount: this.config_.preferredAudioChannelCount,
    });

    /** @private {string} */
    this.currentTextLanguage_ = this.config_.preferredTextLanguage;

    /** @private {string} */
    this.currentTextRole_ = this.config_.preferredTextRole;

    /** @private {boolean} */
    this.currentTextForced_ = this.config_.preferForcedSubs;

    /** @private {!Array<function(): (!Promise | undefined)>} */
    this.cleanupOnUnload_ = [];

    if (dependencyInjector) {
      dependencyInjector(this);
    }


    // Create the CMCD manager so client data can be attached to all requests
    this.cmcdManager_ = this.createCmcd_();

    this.cmsdManager_ = this.createCmsd_();

    this.networkingEngine_ = this.createNetworkingEngine();

    /** @private {shaka.extern.IAdManager} */
    this.adManager_ = null;

    /** @private {shaka.extern.IQueueManager} */
    this.queueManager_ = null;

    /** @private {?shaka.media.PreloadManager} */
    this.preloadDueAdManager_ = null;

    /** @private {HTMLMediaElement} */
    this.preloadDueAdManagerVideo_ = null;

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

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

    /** @private {shaka.util.Timer} */
    this.preloadDueAdManagerTimer_ = new shaka.util.Timer(async () => {
      if (this.preloadDueAdManager_) {
        goog.asserts.assert(this.preloadDueAdManagerVideo_, 'Must have video');
        await this.attach(
            this.preloadDueAdManagerVideo_, /* initializeMediaSource= */ true);
        await this.load(this.preloadDueAdManager_);
        if (!this.preloadDueAdManagerVideoEnded_) {
          this.preloadDueAdManagerVideo_.play();
        } else {
          this.preloadDueAdManagerVideo_.pause();
        }
        this.preloadDueAdManager_ = null;
        this.preloadDueAdManagerVideoEnded_ = false;
      }
    });

    if (shaka.Player.adManagerFactory_) {
      this.adManager_ = shaka.Player.adManagerFactory_();
      this.adManager_.configure(this.config_.ads);

      // Note: we don't use shaka.ads.Utils.AD_CONTENT_PAUSE_REQUESTED to
      // avoid add a optional module in the player.
      this.adManagerEventManager_.listen(
          this.adManager_, 'ad-content-pause-requested', async (e) => {
            this.preloadDueAdManagerTimer_.stop();
            if (!this.preloadDueAdManager_) {
              this.preloadDueAdManagerVideo_ = this.video_;
              this.preloadDueAdManagerVideoEnded_ = this.isEnded();
              const saveLivePosition = /** @type {boolean} */(
                e['saveLivePosition']) || false;
              this.preloadDueAdManager_ = await this.detachAndSavePreload(
                  /* keepAdManager= */ true, saveLivePosition);
            }
          });

      // Note: we don't use shaka.ads.Utils.AD_CONTENT_RESUME_REQUESTED to
      // avoid add a optional module in the player.
      this.adManagerEventManager_.listen(
          this.adManager_, 'ad-content-resume-requested', (e) => {
            const offset = /** @type {number} */(e['offset']) || 0;
            if (this.preloadDueAdManager_) {
              this.preloadDueAdManager_.setOffsetToStartTime(offset);
            }
            this.preloadDueAdManagerTimer_.tickAfter(0.1);
          });

      // Note: we don't use shaka.ads.Utils.AD_CONTENT_ATTACH_REQUESTED to
      // avoid add a optional module in the player.
      this.adManagerEventManager_.listen(
          this.adManager_, 'ad-content-attach-requested', async (e) => {
            if (!this.video_ && this.preloadDueAdManagerVideo_) {
              goog.asserts.assert(this.preloadDueAdManagerVideo_,
                  'Must have video');
              await this.attach(this.preloadDueAdManagerVideo_,
                  /* initializeMediaSource= */ true);
            }
          });
    }

    if (shaka.Player.queueManagerFactory_) {
      this.queueManager_ = shaka.Player.queueManagerFactory_(this);
      this.queueManager_.configure(this.config_.queue);
    }

    // If the browser comes back online after being offline, then try to play
    // again.
    this.globalEventManager_.listen(window, 'online', () => {
      this.restoreDisabledVariants_();
      this.retryStreaming();
    });

    /** @private {shaka.util.Timer} */
    this.checkVariantsTimer_ =
        new shaka.util.Timer(() => this.checkVariants_());

    /** @private {?shaka.media.PreloadManager} */
    this.preloadNextUrl_ = null;

    // Even though |attach| will start in later interpreter cycles, it should be
    // the LAST thing we do in the constructor because conceptually it relies on
    // player having been initialized.
    if (mediaElement) {
      shaka.Deprecate.deprecateFeature(5,
          'Player w/ mediaElement',
          'Please migrate from initializing Player with a mediaElement; ' +
          'use the attach method instead.');
      this.attach(mediaElement, /* initializeMediaSource= */ true);
    }

    /** @private {?shaka.extern.TextDisplayer} */
    this.textDisplayer_ = null;
  }
@param {HTMLMediaElement=} mediaElement
   When provided, the player will attach to <code>mediaElement</code>,
   similar to calling <code>attach</code>. When not provided, the player
   will remain detached.
@param {HTMLElement=} videoContainer
   The videoContainer to construct UITextDisplayer
@param {function(shaka.Player)=} dependencyInjector Optional callback
  which is called to inject mocks into the Player.  Used for testing.