@tianfeng98/hls.js
Version:
HLS.js is a JavaScript library that supports playing MPEG-TS and HEVC encoded HLS streams in browsers with support for MSE.
307 lines (287 loc) • 8.65 kB
text/typescript
import BaseVideoParser from './base-video-parser';
import {
DemuxedVideoTrack,
DemuxedUserdataTrack,
VideoSampleUnit,
} from '../../types/demuxer';
import {
appendUint8Array,
parseSEIMessageFromNALu,
} from '../../utils/mp4-tools';
import type { PES } from '../tsdemuxer';
import H265Parser from './hevc-parser';
class HevcVideoParser extends BaseVideoParser {
public parseAVCPES(
track: DemuxedVideoTrack,
textTrack: DemuxedUserdataTrack,
pes: PES,
last: boolean,
duration: number,
) {
const units = this.parseAVCNALu(track, pes.data);
const debug = false;
let VideoSample = this.VideoSample;
let push: boolean;
let spsfound = false;
// free pes.data to save up some memory
(pes as any).data = null;
// if new NAL units found and last sample still there, let's push ...
// this helps parsing streams with missing AUD (only do this if AUD never found)
if (VideoSample && units.length && !track.audFound) {
this.pushAccessUnit(VideoSample, track);
VideoSample = this.VideoSample = this.createVideoSample(
false,
pes.pts,
pes.dts,
'',
);
}
units.forEach((unit) => {
switch (unit.type) {
// NDR
case 0:
case 1: {
push = true;
if (!VideoSample) {
VideoSample = this.VideoSample = this.createVideoSample(
true,
pes.pts,
pes.dts,
'',
);
}
if (debug) {
VideoSample.debug += 'NDR ';
}
VideoSample.frame = true;
break;
// IDR
}
case 19:
case 20:
case 21:
push = true;
// handle PES not starting with AUD
if (!VideoSample) {
VideoSample = this.VideoSample = this.createVideoSample(
true,
pes.pts,
pes.dts,
'',
);
}
if (debug) {
VideoSample.debug += 'IDR ';
}
VideoSample.key = true;
VideoSample.frame = true;
break;
case 32:
// VPS
push = true;
if (debug && VideoSample) {
VideoSample.debug += 'VPS ';
}
if (!track.vps) {
track.vps = [unit.data];
}
break;
case 33:
// SPS
push = true;
spsfound = true;
if (debug && VideoSample) {
VideoSample.debug += 'SPS ';
}
if (!track.sps) {
const sps = unit.data;
const config = H265Parser.parseSPS(sps);
track.width = config.codec_size.width;
track.height = config.codec_size.height;
track.pixelRatio = [
config.sar_ratio.width,
config.sar_ratio.height,
];
track.sps = [sps];
track.duration = duration;
track.codec = config.codec_mimetype;
track.details = {
...track.details,
...config,
};
}
break;
case 34:
// hevc PPS
push = true;
if (debug && VideoSample) {
VideoSample.debug += 'PPS ';
}
if (!track.pps) {
track.pps = [unit.data];
const details = H265Parser.parsePPS(unit.data);
track.details = {
...track.details,
...details,
};
}
break;
// AUD
case 35:
push = false;
track.audFound = true;
if (VideoSample) {
this.pushAccessUnit(VideoSample, track);
}
VideoSample = this.VideoSample = this.createVideoSample(
false,
pes.pts,
pes.dts,
debug ? 'AUD ' : '',
);
break;
// SEI
case 39: {
push = true;
if (debug && VideoSample) {
VideoSample.debug += 'SEI ';
}
parseSEIMessageFromNALu(
unit.data,
1,
pes.pts as number,
textTrack.samples,
);
break;
}
default:
push = true;
break;
}
if (VideoSample && push) {
const units = VideoSample.units;
units.push(unit);
}
});
// if last PES packet, push samples
if (last && VideoSample) {
this.pushAccessUnit(VideoSample, track);
this.VideoSample = null;
}
}
private parseAVCNALu(
track: DemuxedVideoTrack,
array: Uint8Array,
): Array<{
data: Uint8Array;
type: number;
state?: number;
}> {
const len = array.byteLength;
let state = track.naluState || 0;
const lastState = state;
const units: VideoSampleUnit[] = [];
let i = 0;
let value: number;
let overflow: number;
let unitType: number;
let lastUnitStart = -1;
let lastUnitType: number = 0;
// logger.log('PES:' + Hex.hexDump(array));
if (state === -1) {
// special use case where we found 3 or 4-byte start codes exactly at the end of previous PES packet
lastUnitStart = 0;
lastUnitType = (array[0] >> 1) & 0x3f;
state = 0;
i = 1;
}
while (i < len) {
value = array[i++];
// optimization. state 0 and 1 are the predominant case. let's handle them outside of the switch/case
if (!state) {
state = value ? 0 : 1;
continue;
}
if (state === 1) {
state = value ? 0 : 2;
continue;
}
// here we have state either equal to 2 or 3
if (!value) {
state = 3;
} else if (value === 1) {
overflow = i - state - 1;
if (lastUnitStart >= 0) {
const unit: VideoSampleUnit = {
data: array.subarray(lastUnitStart, overflow),
type: lastUnitType,
};
// logger.log('pushing NALU, type/size:' + unit.type + '/' + unit.data.byteLength);
units.push(unit);
} else {
// lastUnitStart is undefined => this is the first start code found in this PES packet
// first check if start code delimiter is overlapping between 2 PES packets,
// ie it started in last packet (lastState not zero)
// and ended at the beginning of this PES packet (i <= 4 - lastState)
const lastUnit = this.getLastNalUnit(track.samples);
if (lastUnit) {
if (lastState && i <= 4 - lastState) {
// start delimiter overlapping between PES packets
// strip start delimiter bytes from the end of last NAL unit
// check if lastUnit had a state different from zero
if (lastUnit.state) {
// strip last bytes
lastUnit.data = lastUnit.data.subarray(
0,
lastUnit.data.byteLength - lastState,
);
}
}
// If NAL units are not starting right at the beginning of the PES packet, push preceding data into previous NAL unit.
if (overflow > 0) {
// logger.log('first NALU found with overflow:' + overflow);
lastUnit.data = appendUint8Array(
lastUnit.data,
array.subarray(0, overflow),
);
lastUnit.state = 0;
}
}
}
// check if we can read unit type
if (i < len) {
unitType = (array[i] >> 1) & 0x3f;
// logger.log('find NALU @ offset:' + i + ',type:' + unitType);
lastUnitStart = i;
lastUnitType = unitType;
state = 0;
} else {
// not enough byte to read unit type. let's read it on next PES parsing
state = -1;
}
} else {
state = 0;
}
}
if (lastUnitStart >= 0 && state >= 0) {
const unit: VideoSampleUnit = {
data: array.subarray(lastUnitStart, len),
type: lastUnitType,
state: state,
};
units.push(unit);
// logger.log('pushing NALU, type/size/state:' + unit.type + '/' + unit.data.byteLength + '/' + state);
}
// no NALu found
if (units.length === 0) {
// append pes.data to previous NAL unit
const lastUnit = this.getLastNalUnit(track.samples);
if (lastUnit) {
lastUnit.data = appendUint8Array(lastUnit.data, array);
}
}
track.naluState = state;
return units;
}
}
export default HevcVideoParser;