# All files / src/controller gap-controller.js

79.45% Statements 58/73
75.76% Branches 25/33
100% Functions 1/1
78.46% Lines 51/65

Press n or j to go to the next uncovered block, b, p or k for the previous block.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168

    1x
    1x

    13x
    13x
    13x
    13x
    13x

    5x
    5x
    5x

    5x

    1x
    1x
    1x

    1x
    1x
    1x

    4x

    4x

    4x
    4x
    4x
    1x
    1x
    3x

    1x

    3x

    7x
    7x

    7x
    7x

    1x

    7x

    1x
    1x

    3x
    3x

    2x
    2x
    2x

    3x
    3x
    3x
    3x

    3x
    2x
    2x

    2x
    2x

    1x
    1x

     import { BufferHelper } from '../utils/buffer-helper';
    import { ErrorTypes, ErrorDetails } from '../errors';
    import Event from '../events';
    import { logger } from '../utils/logger';

    const stallDebounceInterval = 1000;
    const jumpThreshold = 0.5; // tolerance needed as some browsers stalls playback before reaching buffered range end

    export default class GapController {
      constructor (config, media, fragmentTracker, hls) {
        this.config = config;
        this.media = media;
        this.fragmentTracker = fragmentTracker;
        this.hls = hls;
        this.stallReported = false;
      }

      /**
       * Checks if the playhead is stuck within a gap, and if so, attempts to free it.
       * A gap is an unbuffered range between two buffered ranges (or the start and the first buffered range).
       * @param lastCurrentTime
       * @param buffered
       */
      poll (lastCurrentTime, buffered) {
        const { config, media } = this;
        const currentTime = media.currentTime;
        const tnow = window.performance.now();

        if (currentTime !== lastCurrentTime) {
          // The playhead is now moving, but was previously stalled
          Eif (this.stallReported) {
            logger.warn(`playback not stuck anymore @${currentTime}, after ${Math.round(tnow - this.stalled)}ms`);
            this.stallReported = false;
          }
          this.stalled = null;
          this.nudgeRetry = 0;
          return;
        }

        Iif (media.ended || !media.buffered.length || media.readyState > 2) {
          return;
        }

        Iif (media.seeking && BufferHelper.isBuffered(media, currentTime)) {
          return;
        }

        // The playhead isn't moving but it should be
        // Allow some slack time to for small stalls to resolve themselves
        const stalledDuration = tnow - this.stalled;
        const bufferInfo = BufferHelper.bufferInfo(media, currentTime, config.maxBufferHole);
        if (!this.stalled) {
          this.stalled = tnow;
          return;
        } else if (stalledDuration >= stallDebounceInterval) {
          // Report stalling after trying to fix
          this._reportStall(bufferInfo.len);
        }

        this._tryFixBufferStall(bufferInfo, stalledDuration);
      }

      /**
       * Detects and attempts to fix known buffer stalling issues.
       * @param bufferInfo - The properties of the current buffer.
       * @param stalledDuration - The amount of time Hls.js has been stalling for.
       * @private
       */
      _tryFixBufferStall (bufferInfo, stalledDuration) {
        const { config, fragmentTracker, media } = this;
        const currentTime = media.currentTime;

        const partial = fragmentTracker.getPartialFragment(currentTime);
        if (partial) {
          // Try to skip over the buffer hole caused by a partial fragment
          // This method isn't limited by the size of the gap between buffered ranges
          this._trySkipBufferHole(partial);
        }

        if (bufferInfo.len > jumpThreshold && stalledDuration > config.highBufferWatchdogPeriod * 1000) {
          // Try to nudge currentTime over a buffer hole if we've been stalling for the configured amount of seconds
          // We only try to jump the hole if it's under the configured size
          // Reset stalled so to rearm watchdog timer
          this.stalled = null;
          this._tryNudgeBuffer();
        }
      }

      /**
       * Triggers a BUFFER_STALLED_ERROR event, but only once per stall period.
       * @param bufferLen - The playhead distance from the end of the current buffer segment.
       * @private
       */
      _reportStall (bufferLen) {
        const { hls, media, stallReported } = this;
        if (!stallReported) {
          // Report stalled error once
          this.stallReported = true;
          logger.warn(`Playback stalling at @${media.currentTime} due to low buffer`);
          hls.trigger(Event.ERROR, {
            type: ErrorTypes.MEDIA_ERROR,
            details: ErrorDetails.BUFFER_STALLED_ERROR,
            fatal: false,
            buffer: bufferLen
          });
        }
      }

      /**
       * Attempts to fix buffer stalls by jumping over known gaps caused by partial fragments
       * @param partial - The partial fragment found at the current time (where playback is stalling).
       * @private
       */
      _trySkipBufferHole (partial) {
        const { hls, media } = this;
        const currentTime = media.currentTime;
        let lastEndTime = 0;
        // Check if currentTime is between unbuffered regions of partial fragments
        for (let i = 0; i < media.buffered.length; i++) {
          let startTime = media.buffered.start(i);
          if (currentTime >= lastEndTime && currentTime < startTime) {
            media.currentTime = Math.max(startTime, media.currentTime + 0.1);
            logger.warn(`skipping hole, adjusting currentTime from ${currentTime} to ${media.currentTime}`);
            this.stalled = null;
            hls.trigger(Event.ERROR, {
              type: ErrorTypes.MEDIA_ERROR,
              details: ErrorDetails.BUFFER_SEEK_OVER_HOLE,
              fatal: false,
              reason: `fragment loaded with buffer holes, seeking from ${currentTime} to ${media.currentTime}`,
              frag: partial
            });
            return;
          }
          lastEndTime = media.buffered.end(i);
        }
      }

      /**
       * Attempts to fix buffer stalls by advancing the mediaElement's current time by a small amount.
       * @private
       */
      _tryNudgeBuffer () {
        const { config, hls, media } = this;
        const currentTime = media.currentTime;
        const nudgeRetry = (this.nudgeRetry || 0) + 1;
        this.nudgeRetry = nudgeRetry;

        if (nudgeRetry < config.nudgeMaxRetry) {
          const targetTime = currentTime + nudgeRetry * config.nudgeOffset;
          logger.log(`adjust currentTime from ${currentTime} to ${targetTime}`);
          // playback stalled in buffered area ... let's nudge currentTime to try to overcome this
          media.currentTime = targetTime;
          hls.trigger(Event.ERROR, {
            type: ErrorTypes.MEDIA_ERROR,
            details: ErrorDetails.BUFFER_NUDGE_ON_STALL,
            fatal: false
          });
        } else {
          logger.error(`still stuck in high buffer @${currentTime} after ${config.nudgeMaxRetry}, raise fatal error`);
          hls.trigger(Event.ERROR, {
            type: ErrorTypes.MEDIA_ERROR,
            details: ErrorDetails.BUFFER_STALLED_ERROR,
            fatal: true
          });
        }
      }
    }