初探HLS

前沿

最近直播业务可谓是如火如荼,作为前端开发工程师,我也决定对直播技术有个简单的了解,不再继续看热闹。

常见方案

目前web的直播方案以HLS最为多见,HLS在很多浏览器上是原生支持的,也就是说可以直接用video标签去播放,而不支持的浏览器则可以使用HLS.js来完成直播。

m3u8

在web直播间中的控制台,你可以发现大量的m3u8后缀的文件,而这个文件的内容,一般都是这样的

#EXTM3U
#EXT-X-VERSION:3
#EXT-X-ALLOW-CACHE:NO
#EXT-X-MEDIA-SEQUENCE:1592483121
#EXT-X-TARGETDURATION:4
#EXTINF:3.049,
3100_c1c_f7593ecde9d142ca9f4d0f0cfbf8ff95-1592483121.ts
#EXTINF:3,
3100_c1c_f7593ecde9d142ca9f4d0f0cfbf8ff95-1592483122.ts
#EXTINF:3.05,
3100_c1c_f7593ecde9d142ca9f4d0f0cfbf8ff95-1592483123.ts

本文对于m3u8文件中的详细字段含义就不详细说明了,可以看一下m3u8文件格式详解。这里只需要明确m3u8就是ts文件的一个列表,定义了视频分片的地址,分片的信息等等。只需要按顺序加载这些ts文件,解码后就可以完成直播。对于如果生成m3u8文件,感兴趣的可以看一下node-m3u.

MPEG2-TS

这里需要知道这些.ts文件都是MPEG2-TS文件。

MPEG2-TS(Transport Stream“传输流”;又称TS、TP、MPEG-TS 或 M2T)是用于音效、图像与数据的通信协定。

初探HLS_第1张图片

而hls.js中就涉及到了对MPEG2-TS的处理,我们需要的packet data就是视频信息的二进制内容,以下就是库中完成MPEG2-TS解码的主要源码。

    // loop through TS packets
    for (start = syncOffset; start < len; start += 188) {
      if (data[start] === 0x47) {
        stt = !!(data[start + 1] & 0x40);
        // pid is a 13-bit field starting at the last bit of TS[1]
        pid = ((data[start + 1] & 0x1f) << 8) + data[start + 2];
        atf = (data[start + 3] & 0x30) >> 4;
        // if an adaption field is present, its length is specified by the fifth byte of the TS packet header.
        if (atf > 1) {
          offset = start + 5 + data[start + 4];
          // continue if there is only adaptation field
          if (offset === (start + 188)) {
            continue;
          }
        } else {
          offset = start + 4;
        }
        switch (pid) {
        case avcId:
          if (stt) {
            if (avcData && (pes = parsePES(avcData))) {
              parseAVCPES(pes, false);
            }

            avcData = { data: [], size: 0 };
          }
          if (avcData) {
            avcData.data.push(data.subarray(offset, start + 188));
            avcData.size += start + 188 - offset;
          }
          break;
        case audioId:
          if (stt) {
            if (audioData && (pes = parsePES(audioData))) {
              if (audioTrack.isAAC) {
                parseAACPES(pes);
              } else {
                parseMPEGPES(pes);
              }
            }
            audioData = { data: [], size: 0 };
          }
          if (audioData) {
            audioData.data.push(data.subarray(offset, start + 188));
            audioData.size += start + 188 - offset;
          }
          break;
        case id3Id:
          if (stt) {
            if (id3Data && (pes = parsePES(id3Data))) {
              parseID3PES(pes);
            }

            id3Data = { data: [], size: 0 };
          }
          if (id3Data) {
            id3Data.data.push(data.subarray(offset, start + 188));
            id3Data.size += start + 188 - offset;
          }
          break;
        case 0:
          if (stt) {
            offset += data[offset] + 1;
          }

          pmtId = this._pmtId = parsePAT(data, offset);
          break;
        case pmtId:
          if (stt) {
            offset += data[offset] + 1;
          }

          let parsedPIDs = parsePMT(data, offset, this.typeSupported.mpeg === true || this.typeSupported.mp3 === true, this.sampleAes != null);

          // only update track id if track PID found while parsing PMT
          // this is to avoid resetting the PID to -1 in case
          // track PID transiently disappears from the stream
          // this could happen in case of transient missing audio samples for example
          // NOTE this is only the PID of the track as found in TS,
          // but we are not using this for MP4 track IDs.
          avcId = parsedPIDs.avc;
          if (avcId > 0) {
            avcTrack.pid = avcId;
          }

          audioId = parsedPIDs.audio;
          if (audioId > 0) {
            audioTrack.pid = audioId;
            audioTrack.isAAC = parsedPIDs.isAAC;
          }
          id3Id = parsedPIDs.id3;
          if (id3Id > 0) {
            id3Track.pid = id3Id;
          }

          if (unknownPIDs && !pmtParsed) {
            logger.log('reparse from beginning');
            unknownPIDs = false;
            // we set it to -188, the += 188 in the for loop will reset start to 0
            start = syncOffset - 188;
          }
          pmtParsed = this.pmtParsed = true;
          break;
        case 17:
        case 0x1fff:
          break;
        default:
          unknownPIDs = true;
          break;
        }
      } else {
        this.observer.trigger(Event.ERROR, { type: ErrorTypes.MEDIA_ERROR, details: ErrorDetails.FRAG_PARSING_ERROR, fatal: false, reason: 'TS packet did not start with 0x47' });
      }
    }

完成了解码之后,就要进行下一步视频合流,需要用到MediaSource API。合流后的产物就是video 标签可以播放的视频流了。

总结

经过上面的了解,大致梳理出了HLS的一个直播的技术方案。

  1. 采集视频流,并完成分片
  2. 前端请求获取m3u8文件
  3. 解析m3u8文件,获取ts文件
  4. ts文件转码放入ArrayBuffer
  5. MediaSource API进行合流,用video标签输出直播

其中如果天然支持HLS的浏览器,其实就是自己完成了3 4 5三个流程。

你可能感兴趣的:(javascript,hls)