Skip to content

Hls.js

HLS.js is a JavaScript library that plays HLS in browsers with support for MSE.

Events

MANIFEST_PARSED

fired after manifest has been parsed.

  onManifestParsed(event, data) {
    // index of first quality level appearing in Manifest.
    const firstLevel = data.firstLevel;
    if (firstLevel >= data.levels.length) {
      const msg1 = `Overflow firstLevel(${firstLevel}) index, `;
      const msg2 = `levels.length(${data.levels.length})`;
      throw new IllegalStateException(msg1 + msg2);
    }

    const level = data.levels[firstLevel];
    const levelDetails = level.details;
    if (!levelDetails) {
      return;
    }

    this.fragments = levelDetails.fragments;
    this.totalDuration = levelDetails.totalduration;
    const count = this.fragments.length;
    console.debug(`Total fragments count=${count},totalDuration=${this.totalDuration}`);
  }

FRAG_CHANGED

fired when fragment matching with current video position is changing.

  onFragChanged(event, data) {
    this.currentFragment = data.frag;
    if (!this.currentFragment) {
      throw new UndefinedException('Current fragment is undefined or null');
    }

    const sn = this.currentFragment.sn;
    console.debug(`Change current fragment: #${sn}`);

    if (!this.currentFragment.relurl) {
      throw new EmptyException('Not exists fragment url');
    }

    // this.currentFragmentMoment = parseFragmentDateTime(this.currentFragment.relurl);
    // this.currentFragmentVideoTime = this.video.currentTime;

    // const ct = this.video.currentTime;
    // const d = this.video.duration;
    // console.debug(`Video information: ct=${ct},d=${d}`);
  }

그 밖의 주요 이벤트

사용하진 않았지만...

// hls.on(Hls.Events.ERROR, (e,d) => {console.debug(e,d);});
// hls.on(Hls.Events.FRAG_BUFFERED, (e,d) => {console.debug(e,d);});
// hls.on(Hls.Events.FRAG_LOADING, (e,d) => {console.debug(e,d);});
// hls.on(Hls.Events.FRAG_LOADED, (e,d) => {console.debug(e,d);});
// hls.on(Hls.Events.FRAG_PARSED, (e,d) => {console.debug(e,d);});
// hls.on(Hls.Events.BUFFER_APPENDED, (e,d) => {console.debug(e,d);});
// hls.on(Hls.Events.BUFFER_EOS, (e,d) => {console.debug(e,d);});
// hls.on(Hls.Events.LEVEL_PTS_UPDATED, (e,d) => {console.debug(e,d);});

Set up token authentication

// append authorization header for encryption token
Object.assign(config, {
  xhrSetup: (xhr, url) => {
    const encryptionKeyUrl = 'ENCRYPTION KEY URL'
    // check for encryption key url and append only for that xhr
    if (encryptionKeyUrl !== url) {
      return
    }
    xhr.setRequestHeader('Authorization', 'Bearer=' + token)
  }
})

// append authorization header for DRM token
Object.assign(config, {
  licenseXhrSetup: (xhr, url) => {
    xhr.setRequestHeader('Authorization', 'Bearer=' + token)
  }
})

updateRecordRanges

획득한 Fragment 목록을 사용하여 비디오의 재생 범위를 계산하는 방법. recc의 HlsPlayer에서 사용되었다.

const ONE_SECOND_TO_MILLISECONDS = 1000;
const FRAGMENT_DATETIME_REGEX = /(\d+)-(\d+)-(\d+)\/(\d+)_(\d+)_(\d+)\.(\d+)\.ts$/;

function parseFragmentDateTime(url: string) {
  const match = FRAGMENT_DATETIME_REGEX.exec(url);
  if (!match) {
    throw new IllegalStateException('The URL `Fragment` is malformed');
  }

  const year = match[1];
  const month = match[2];
  const day = match[3];
  const hour = match[4];
  const minute = match[5];
  const second = match[6];
  const micro = match[7];
  const time = `${year}-${month}-${day}T${hour}:${minute}:${second}.${micro}`;
  return moment(time);
}

  onManifestParsed(event, data) {
    // index of first quality level appearing in Manifest.
    const firstLevel = data.firstLevel;
    if (firstLevel >= data.levels.length) {
      const msg1 = `Overflow firstLevel(${firstLevel}) index, `;
      const msg2 = `levels.length(${data.levels.length})`;
      throw new IllegalStateException(msg1 + msg2);
    }

    const level = data.levels[firstLevel];
    const levelDetails = level.details;
    if (!levelDetails) {
      return;
    }

    this.fragments = levelDetails.fragments;
    this.totalDuration = levelDetails.totalduration;
    const count = this.fragments.length;
    console.debug(`Total fragments count=${count},totalDuration=${this.totalDuration}`);
    this.updateRecordRanges(this.fragments);

    const body = {
      date: this.date,
      device_uid: this.deviceNumber,
    } as VmsFilterEventQ;

    this.$api2.postVmsEventsFilter(this.group, this.project, body)
        .then(items => {
          this.events = items;
          console.debug(`Total events count: ${this.events.length}`);
          this.updateEventRanges(this.events);
        })
        .catch(error => {
          this.events = [];
        });
  }

  updateRecordRanges(fragments: Array<Fragment>) {
    if (fragments.length === 0) {
      throw new EmptyException('Not exists fragments');
    }

    const resolution = 2 * ONE_SECOND_TO_MILLISECONDS;

    const result = [] as Array<RatioRange>;
    let prevStart: undefined | moment.Moment;
    let prevLast: undefined | moment.Moment;
    let mergedCount = 0;

    for (let i = 0; i < fragments.length; ++i) {
      const f = fragments[i];
      if (!f.relurl) {
        throw new EmptyException('Not exists fragment url');
      }

      const start = parseFragmentDateTime(f.relurl);
      const durationMilliseconds = Math.round(f.duration * ONE_SECOND_TO_MILLISECONDS);
      const last = start.clone().add(durationMilliseconds, 'milliseconds');

      if (prevStart && prevLast) {
        const diff = start.diff(prevLast, 'milliseconds')
        const absDiff = Math.abs(diff);
        if (absDiff < resolution) {
          // This is a `continuous` fragment.
          prevLast = last;
          mergedCount++;
        } else {
          // Flush the previous `range`.
          result.push({
            start: prevStart.valueOf(),
            last: prevLast.valueOf(),
            count: mergedCount,
          } as RatioRange);

          // New beginning.
          prevStart = start;
          prevLast = last;
          mergedCount = 1;
        }
      } else {
        // First beginning.
        prevStart = start;
        prevLast = last;
        mergedCount = 1;
      }
    }

    // Last flush.
    if (prevStart && prevLast) {
      result.push({
        start: prevStart.valueOf(),
        last: prevLast.valueOf(),
        count: mergedCount,
      } as RatioRange);
    }

    const beginDateText = `${this.date}T00:00:00`;
    const begin = moment(beginDateText).valueOf();
    this.recordRanges = result.map(x => {
      return {
        start: (x.start - begin) / MILLISECONDS_TO_DAY,
        last: (x.last - begin) / MILLISECONDS_TO_DAY,
        count: x.count,
      };
    })
  }

video.currentTime

  // absTimeToCurrentMilliseconds(time: moment.Moment) {
  //   const findIndex = this.findNearestRatioRangeIndex(time);
  //   let totalMilliseconds = 0.0;
  //   for (let i = 0; i < findIndex; ++i) {
  //     const range = this.recordRanges[i];
  //     const startValue = range.startTime.valueOf();
  //     const lastValue = range.lastTime.valueOf();
  //     console.assert(lastValue >= startValue);
  //     totalMilliseconds += (lastValue - startValue);
  //   }
  //
  //   const timeValue = time.valueOf();
  //   const range = this.recordRanges[findIndex];
  //   const startValue = range.startTime.valueOf();
  //   const lastValue = range.lastTime.valueOf();
  //   if (timeValue < startValue) {
  //     return totalMilliseconds;
  //   } else if (lastValue < timeValue) {
  //     return totalMilliseconds + (lastValue - startValue);
  //   } else {
  //     console.assert(startValue <= timeValue && timeValue <= lastValue);
  //     return totalMilliseconds + (timeValue - startValue);
  //   }
  // }
  //
  // currentMillisecondsToAbsTime(currentMilliseconds: number) {
  //   let remainMilliseconds = currentMilliseconds;
  //   let findIndex = 0;
  //   for (; findIndex < this.recordRanges.length; ++findIndex) {
  //     const range = this.recordRanges[findIndex];
  //     const startValue = range.startTime.valueOf();
  //     const lastValue = range.lastTime.valueOf();
  //     console.assert(lastValue >= startValue);
  //     const durationMilliseconds = lastValue - startValue;
  //
  //     if (durationMilliseconds >= remainMilliseconds) {
  //       break;
  //     }
  //     remainMilliseconds -= durationMilliseconds;
  //   }
  //
  //   const range = this.recordRanges[findIndex];
  //   const start = range.startTime;
  //   const result = start.clone();
  //   result.add(remainMilliseconds, 'milliseconds')
  //   return result;
  // }

Example

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Demo AV Muxing</title>
</head>
<body>
    <video id="video" loop playsinline autoplay muted controls></video>
    <script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script>
    <script>
        var video = document.getElementById('video');
        var videoSrc = './live_stream.m3u8';
        if (Hls.isSupported()) {
            var hls = new Hls();
            hls.loadSource(videoSrc);
            hls.attachMedia(video);
            console.debug("1")
            hls.on(Hls.Events.MANIFEST_PARSED, () => {
                video.muted = 'muted';
                video.autoplay = 'autoplay';
                video.playsinline = 'true';
                video.play();
                console.debug("2")
            });
        } else {
            alert('Unsupported HLS.js component');
        }
    </script>
</body>
</html>

See also

Favorite site