前沿
最近直播业务可谓是如火如荼,作为前端开发工程师,我也决定对直播技术有个简单的了解,不再继续看热闹。
常见方案
目前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.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的一个直播的技术方案。
- 采集视频流,并完成分片
- 前端请求获取m3u8文件
- 解析m3u8文件,获取ts文件
- ts文件转码放入ArrayBuffer
- MediaSource API进行合流,用video标签输出直播
其中如果天然支持HLS的浏览器,其实就是自己完成了3 4 5三个流程。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。