All files / src/controller cap-level-controller.js

70.51% Statements 55/78
59.65% Branches 34/57
78.95% Functions 15/19
70.51% Lines 55/78
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 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189                  94x             94x 94x 94x 94x 94x 94x       9x                                   3x 3x 3x 3x 3x   2x             4x 4x   2x                 9x       10x                                     5x 1x     4x 1x     4x       5x       5x 5x 5x 5x 5x       10x 10x 10x 10x 1x 1x           4x 4x       4x         4x 4x       4x                       1x       9x 6x         3x 3x       3x         3x   3x 8x 8x 2x 2x       3x          
/*
 * cap stream level to media size dimension controller
*/
 
import Event from '../events';
import EventHandler from '../event-handler';
 
class CapLevelController extends EventHandler {
  constructor (hls) {
    super(hls,
      Event.FPS_DROP_LEVEL_CAPPING,
      Event.MEDIA_ATTACHING,
      Event.MANIFEST_PARSED,
      Event.BUFFER_CODECS,
      Event.MEDIA_DETACHING);
 
    this.autoLevelCapping = Number.POSITIVE_INFINITY;
    this.firstLevel = null;
    this.levels = [];
    this.media = null;
    this.restrictedLevels = [];
    this.timer = null;
  }
 
  destroy () {
    Iif (this.hls.config.capLevelToPlayerSize) {
      this.media = null;
      this._stopCapping();
    }
  }
 
  onFpsDropLevelCapping (data) {
    // Don't add a restricted level more than once
    if (CapLevelController.isLevelAllowed(data.droppedLevel, this.restrictedLevels)) {
      this.restrictedLevels.push(data.droppedLevel);
    }
  }
 
  onMediaAttaching (data) {
    this.media = data.media instanceof window.HTMLVideoElement ? data.media : null;
  }
 
  onManifestParsed (data) {
    const hls = this.hls;
    this.restrictedLevels = [];
    this.levels = data.levels;
    this.firstLevel = data.firstLevel;
    if (hls.config.capLevelToPlayerSize && (data.video || (data.levels.length && data.altAudio))) {
      // Start capping immediately if the manifest has signaled video codecs
      this._startCapping();
    }
  }
 
  // Only activate capping when playing a video stream; otherwise, multi-bitrate audio-only streams will be restricted
  // to the first level
  onBufferCodecs (data) {
    const hls = this.hls;
    if (hls.config.capLevelToPlayerSize && data.video) {
      // If the manifest did not signal a video codec capping has been deferred until we're certain video is present
      this._startCapping();
    }
  }
 
  onLevelsUpdated (data) {
    this.levels = data.levels;
  }
 
  onMediaDetaching () {
    this._stopCapping();
  }
 
  detectPlayerSize () {
    Iif (this.media) {
      let levelsLength = this.levels ? this.levels.length : 0;
      if (levelsLength) {
        const hls = this.hls;
        hls.autoLevelCapping = this.getMaxLevel(levelsLength - 1);
        if (hls.autoLevelCapping > this.autoLevelCapping) {
          // if auto level capping has a higher value for the previous one, flush the buffer using nextLevelSwitch
          // usually happen when the user go to the fullscreen mode.
          hls.streamController.nextLevelSwitch();
        }
        this.autoLevelCapping = hls.autoLevelCapping;
      }
    }
  }
 
  /*
  * returns level should be the one with the dimensions equal or greater than the media (player) dimensions (so the video will be downscaled)
  */
  getMaxLevel (capLevelIndex) {
    if (!this.levels) {
      return -1;
    }
 
    const validLevels = this.levels.filter((level, index) =>
      CapLevelController.isLevelAllowed(index, this.restrictedLevels) && index <= capLevelIndex
    );
 
    return CapLevelController.getMaxLevelByMediaSize(validLevels, this.mediaWidth, this.mediaHeight);
  }
 
  _startCapping () {
    Iif (this.timer) {
      // Don't reset capping if started twice; this can happen if the manifest signals a video codec
      return;
    }
    this.autoLevelCapping = Number.POSITIVE_INFINITY;
    this.hls.firstLevel = this.getMaxLevel(this.firstLevel);
    clearInterval(this.timer);
    this.timer = setInterval(this.detectPlayerSize.bind(this), 1000);
    this.detectPlayerSize();
  }
 
  _stopCapping () {
    this.restrictedLevels = [];
    this.firstLevel = null;
    this.autoLevelCapping = Number.POSITIVE_INFINITY;
    if (this.timer) {
      this.timer = clearInterval(this.timer);
      this.timer = null;
    }
  }
 
  get mediaWidth () {
    let width;
    const media = this.media;
    Iif (media) {
      width = media.width || media.clientWidth || media.offsetWidth;
      width *= CapLevelController.contentScaleFactor;
    }
    return width;
  }
 
  get mediaHeight () {
    let height;
    const media = this.media;
    Iif (media) {
      height = media.height || media.clientHeight || media.offsetHeight;
      height *= CapLevelController.contentScaleFactor;
    }
    return height;
  }
 
  static get contentScaleFactor () {
    let pixelRatio = 1;
    try {
      pixelRatio = window.devicePixelRatio;
    } catch (e) {}
    return pixelRatio;
  }
 
  static isLevelAllowed (level, restrictedLevels = []) {
    return restrictedLevels.indexOf(level) === -1;
  }
 
  static getMaxLevelByMediaSize (levels, width, height) {
    if (!levels || (levels && !levels.length)) {
      return -1;
    }
 
    // Levels can have the same dimensions but differing bandwidths - since levels are ordered, we can look to the next
    // to determine whether we've chosen the greatest bandwidth for the media's dimensions
    const atGreatestBandiwdth = (curLevel, nextLevel) => {
      Iif (!nextLevel) {
        return true;
      }
 
      return curLevel.width !== nextLevel.width || curLevel.height !== nextLevel.height;
    };
 
    // If we run through the loop without breaking, the media's dimensions are greater than every level, so default to
    // the max level
    let maxLevelIndex = levels.length - 1;
 
    for (let i = 0; i < levels.length; i += 1) {
      const level = levels[i];
      if ((level.width >= width || level.height >= height) && atGreatestBandiwdth(level, levels[i + 1])) {
        maxLevelIndex = i;
        break;
      }
    }
 
    return maxLevelIndex;
  }
}
 
export default CapLevelController;