# All files / src/loader fragment.ts

80% Statements 36/45
68.29% Branches 28/41
100% Functions 0/0
80% Lines 36/45

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
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184

    29x
    29x
    29x
    4x

    25x

    29x
    29x

    29x
    26x

    29x

    35x
    6x

    29x

    16x

    19x

    21x

    21x
    10x
    10x

    10x

    21x

    10x

    10x
    1x

    9x

    9x

    10x

    10x
    40x

    10x

    10x

    10x
    10x
    10x
    10x

    10x

    import { buildAbsoluteURL } from 'url-toolkit';
    import { logger } from '../utils/logger';
    import LevelKey from './level-key';

    export enum ElementaryStreamTypes {
      AUDIO = 'audio',
      VIDEO = 'video',
    }

    export default class Fragment {
      private _url: string | null = null;
      private _byteRange: number[] | null = null;
      private _decryptdata: LevelKey | null = null;

      // Holds the types of data this fragment supports
      private _elementaryStreams: Record<ElementaryStreamTypes, boolean> = {
        [ElementaryStreamTypes.AUDIO]: false,
        [ElementaryStreamTypes.VIDEO]: false
      };

      public rawProgramDateTime: string | null = null;
      public programDateTime: number | null = null;
      public tagList: Array<string[]> = [];

      // TODO: Move at least baseurl to constructor.
      // Currently we do a two-pass construction as use the Fragment class almost like a object for holding parsing state.
      // It may make more sense to just use a POJO to keep state during the parsing phase.
      // Have Fragment be the representation once we have a known state?
      // Something to think on.

      // relurl is the portion of the URL that comes from inside the playlist.
      public relurl!: string;
      // baseurl is the URL to the playlist
      public baseurl!: string;
      // EXTINF has to be present for a m3u8 to be considered valid
      public duration!: number;
      // sn notates the sequence number for a segment, and if set to a string can be 'initSegment'
      public sn: number | 'initSegment' = 0;
      // levelkey is the EXT-X-KEY that applies to this segment for decryption
      // core difference from the private field _decryptdata is the lack of the initialized IV
      // _decryptdata will set the IV for this segment based on the segment number in the fragment
      public levelkey?: LevelKey;

      // setByteRange converts a EXT-X-BYTERANGE attribute into a two element array
      setByteRange (value: string, previousFrag?: Fragment) {
        const params = value.split('@', 2);
        const byteRange: number[] = [];
        if (params.length === 1) {
          byteRange[0] = previousFrag ? previousFrag.byteRangeEndOffset : 0;
        } else {
          byteRange[0] = parseInt(params[1]);
        }
        byteRange[1] = parseInt(params[0]) + byteRange[0];
        this._byteRange = byteRange;
      }

      get url () {
        if (!this._url && this.relurl) {
          this._url = buildAbsoluteURL(this.baseurl, this.relurl, { alwaysNormalize: true });
        }

        return this._url;
      }

      set url (value) {
        this._url = value;
      }

      get byteRange (): number[] {
        if (!this._byteRange) {
          return [];
        }

        return this._byteRange;
      }

      /**
       * @type {number}
       */
      get byteRangeStartOffset () {
        return this.byteRange[0];
      }

      get byteRangeEndOffset () {
        return this.byteRange[1];
      }

      get decryptdata (): LevelKey | null {
        Iif (!this.levelkey && !this._decryptdata) {
          return null;
        }

        if (!this._decryptdata && this.levelkey) {
          let sn = this.sn;
          Iif (typeof sn !== 'number') {
            // We are fetching decryption data for a initialization segment
            // If the segment was encrypted with AES-128
            // It must have an IV defined. We cannot substitute the Segment Number in.
            if (this.levelkey && this.levelkey.method === 'AES-128' && !this.levelkey.iv) {
              logger.warn(`missing IV for initialization segment with method="${this.levelkey.method}" - compliance issue`);
            }

            /*
            Be converted to a Number.
            'initSegment' will become NaN.
            NaN, which when converted through ToInt32() -> +0.
            ---
            Explicitly set sn to resulting value from implicit conversions 'initSegment' values for IV generation.
            */
            sn = 0;
          }
          this._decryptdata = this.setDecryptDataFromLevelKey(this.levelkey, sn);
        }

        return this._decryptdata;
      }

      get endProgramDateTime () {
        Iif (this.programDateTime === null) {
          return null;
        }

        if (!Number.isFinite(this.programDateTime)) {
          return null;
        }

        let duration = !Number.isFinite(this.duration) ? 0 : this.duration;

        return this.programDateTime + (duration * 1000);
      }

      get encrypted () {
        return !!((this.decryptdata && this.decryptdata.uri !== null) && (this.decryptdata.key === null));
      }

      /**
       * @param {ElementaryStreamTypes} type
       */
      addElementaryStream (type: ElementaryStreamTypes) {
        this._elementaryStreams[type] = true;
      }

      /**
       * @param {ElementaryStreamTypes} type
       */
      hasElementaryStream (type: ElementaryStreamTypes) {
        return this._elementaryStreams[type] === true;
      }

      /**
       * Utility method for parseLevelPlaylist to create an initialization vector for a given segment
       * @param {number} segmentNumber - segment number to generate IV with
       * @returns {Uint8Array}
       */
      createInitializationVector (segmentNumber: number): Uint8Array {
        let uint8View = new Uint8Array(16);

        for (let i = 12; i < 16; i++) {
          uint8View[i] = (segmentNumber >> 8 * (15 - i)) & 0xff;
        }

        return uint8View;
      }

      /**
       * Utility method for parseLevelPlaylist to get a fragment's decryption data from the currently parsed encryption key data
       * @param levelkey - a playlist's encryption info
       * @param segmentNumber - the fragment's segment number
       * @returns {LevelKey} - an object to be applied as a fragment's decryptdata
       */
      setDecryptDataFromLevelKey (levelkey: LevelKey, segmentNumber: number): LevelKey {
        let decryptdata = levelkey;

        Eif (levelkey && levelkey.method && levelkey.uri && !levelkey.iv) {
          decryptdata = new LevelKey(levelkey.baseuri, levelkey.reluri);
          decryptdata.method = levelkey.method;
          decryptdata.iv = this.createInitializationVector(segmentNumber);
        }

        return decryptdata;
      }
    }