onTimeUpdate_() {
    const playbackRate = this.video_.playbackRate;
    const isLive = this.isLive();

    if (this.config_.streaming.vodDynamicPlaybackRate && !isLive) {
      const minPlaybackRate =
        this.config_.streaming.vodDynamicPlaybackRateLowBufferRate;
      const bufferFullness = this.getBufferFullness();
      const bufferThreshold =
        this.config_.streaming.vodDynamicPlaybackRateBufferRatio;

      if (bufferFullness <= bufferThreshold) {
        if (playbackRate != minPlaybackRate) {
          shaka.log.debug('Buffer fullness ratio (' + bufferFullness + ') ' +
            'is less than the vodDynamicPlaybackRateBufferRatio (' +
            bufferThreshold + '). Updating playbackRate to ' + minPlaybackRate);
          this.trickPlay(minPlaybackRate, /* useTrickPlayTrack= */ false);
        }
      } else if (bufferFullness == 1) {
        if (playbackRate !== this.playRateController_.getDefaultRate()) {
          shaka.log.debug('Buffer is full. Cancel trick play.');
          this.cancelTrickPlay();
        }
      }
    }

    // If the live stream has reached its end, do not sync.
    if (!isLive) {
      return;
    }

    const seekRange = this.seekRange();
    if (!Number.isFinite(seekRange.end)) {
      return;
    }
    const currentTime = this.video_.currentTime;
    if (currentTime < seekRange.start) {
      // Bad stream?
      return;
    }

    // We don't want to block the user from pausing the stream.
    if (this.video_.paused) {
      return;
    }

    let targetLatency;
    let maxLatency;
    let maxPlaybackRate;
    let minLatency;
    let minPlaybackRate;
    const targetLatencyTolerance =
      this.config_.streaming.liveSync.targetLatencyTolerance;
    const dynamicTargetLatency =
      this.config_.streaming.liveSync.dynamicTargetLatency.enabled;
    const stabilityThreshold =
      this.config_.streaming.liveSync.dynamicTargetLatency.stabilityThreshold;

    if (this.config_.streaming.liveSync &&
      this.config_.streaming.liveSync.enabled) {
      targetLatency = this.config_.streaming.liveSync.targetLatency;
      maxLatency = targetLatency + targetLatencyTolerance;
      minLatency = Math.max(0, targetLatency - targetLatencyTolerance);
      maxPlaybackRate = this.config_.streaming.liveSync.maxPlaybackRate;
      minPlaybackRate = this.config_.streaming.liveSync.minPlaybackRate;
    } else {
      // serviceDescription must override if it is defined in the MPD and
      // liveSync configuration is not set.
      if (this.manifest_ && this.manifest_.serviceDescription) {
        targetLatency = this.manifest_.serviceDescription.targetLatency;
        if (this.manifest_.serviceDescription.targetLatency != null) {
          maxLatency = this.manifest_.serviceDescription.targetLatency +
              targetLatencyTolerance;
        } else if (this.manifest_.serviceDescription.maxLatency != null) {
          maxLatency = this.manifest_.serviceDescription.maxLatency;
        }
        if (this.manifest_.serviceDescription.targetLatency != null) {
          minLatency = Math.max(0,
              this.manifest_.serviceDescription.targetLatency -
              targetLatencyTolerance);
        } else if (this.manifest_.serviceDescription.minLatency != null) {
          minLatency = this.manifest_.serviceDescription.minLatency;
        }
        maxPlaybackRate =
          this.manifest_.serviceDescription.maxPlaybackRate ||
          this.config_.streaming.liveSync.maxPlaybackRate;
        minPlaybackRate =
          this.manifest_.serviceDescription.minPlaybackRate ||
          this.config_.streaming.liveSync.minPlaybackRate;
      }
    }

    if (!this.currentTargetLatency_ && typeof targetLatency === 'number') {
      this.currentTargetLatency_ = targetLatency;
    }

    const maxAttempts =
        this.config_.streaming.liveSync.dynamicTargetLatency.maxAttempts;
    if (dynamicTargetLatency && this.targetLatencyReached_ &&
      this.currentTargetLatency_ !== null &&
      typeof targetLatency === 'number' &&
      this.rebufferingCount_ < maxAttempts &&
      (Date.now() - this.targetLatencyReached_) > stabilityThreshold * 1000) {
      const dynamicMinLatency =
          this.config_.streaming.liveSync.dynamicTargetLatency.minLatency;
      const latencyIncrement = (targetLatency - dynamicMinLatency) / 2;
      this.currentTargetLatency_ = Math.max(
          this.currentTargetLatency_ - latencyIncrement,
          // current target latency should be within the tolerance of the min
          // latency to not overshoot it
          dynamicMinLatency + targetLatencyTolerance);
      this.targetLatencyReached_ = Date.now();
    }
    if (dynamicTargetLatency && this.currentTargetLatency_ !== null) {
      maxLatency = this.currentTargetLatency_ + targetLatencyTolerance;
      minLatency = this.currentTargetLatency_ - targetLatencyTolerance;
    }

    const latency = seekRange.end - this.video_.currentTime;
    let offset = 0;
    // In src= mode, the seek range isn't updated frequently enough, so we need
    // to fudge the latency number with an offset.  The playback rate is used
    // as an offset, since that is the amount we catch up 1 second of
    // accelerated playback.
    if (this.loadMode_ == shaka.Player.LoadMode.SRC_EQUALS) {
      const buffered = this.video_.buffered;
      if (buffered.length > 0) {
        const bufferedEnd = buffered.end(buffered.length - 1);
        offset = Math.max(maxPlaybackRate, bufferedEnd - seekRange.end);
      }
    }

    const panicMode = this.config_.streaming.liveSync.panicMode;
    const panicThreshold =
        this.config_.streaming.liveSync.panicThreshold * 1000;
    const timeSinceLastRebuffer =
        Date.now() - this.bufferObserver_.getLastRebufferTime();
    if (panicMode && !minPlaybackRate) {
      minPlaybackRate = this.config_.streaming.liveSync.minPlaybackRate;
    }

    if (panicMode && minPlaybackRate &&
        timeSinceLastRebuffer <= panicThreshold) {
      if (playbackRate != minPlaybackRate) {
        shaka.log.debug('Time since last rebuffer (' +
          timeSinceLastRebuffer + 's) ' +
          'is less than the live sync panicThreshold (' + panicThreshold +
          's). Updating playbackRate to ' + minPlaybackRate);
        this.trickPlay(minPlaybackRate, /* useTrickPlayTrack= */ false);
      }
    } else if (maxLatency != undefined && maxPlaybackRate &&
      (latency - offset) > maxLatency) {
      if (playbackRate != maxPlaybackRate) {
        shaka.log.debug('Latency (' + latency + 's) is greater than ' +
          'live sync maxLatency (' + maxLatency + 's). ' +
          'Updating playbackRate to ' + maxPlaybackRate);
        this.trickPlay(maxPlaybackRate, /* useTrickPlayTrack= */ false);
      }
      this.targetLatencyReached_ = null;
    } else if (minLatency != undefined && minPlaybackRate &&
        (latency - offset) < minLatency) {
      if (playbackRate != minPlaybackRate) {
        shaka.log.debug('Latency (' + latency + 's) is smaller than ' +
          'live sync minLatency (' + minLatency + 's). ' +
          'Updating playbackRate to ' + minPlaybackRate);
        this.trickPlay(minPlaybackRate, /* useTrickPlayTrack= */ false);
      }
      this.targetLatencyReached_ = null;
    } else if (playbackRate !== this.playRateController_.getDefaultRate()) {
      this.cancelTrickPlay();
      this.targetLatencyReached_ = Date.now();
    }
  }
Callback for liveSync and vodDynamicPlaybackRate

@private