hls.js
Version:
JavaScript HLS client using MediaSourceExtension
199 lines (187 loc) • 6.16 kB
text/typescript
import { appendUint8Array } from '../../utils/mp4-tools';
import type {
DemuxedUserdataTrack,
DemuxedVideoTrack,
VideoSample,
VideoSampleUnit,
} from '../../types/demuxer';
import type { ParsedVideoSample } from '../tsdemuxer';
import type { PES } from '../tsdemuxer';
abstract class BaseVideoParser {
protected VideoSample: ParsedVideoSample | null = null;
protected createVideoSample(
key: boolean,
pts: number | undefined,
dts: number | undefined,
): ParsedVideoSample {
return {
key,
frame: false,
pts,
dts,
units: [],
length: 0,
};
}
protected getLastNalUnit(
samples: VideoSample[],
): VideoSampleUnit | undefined {
let VideoSample = this.VideoSample;
let lastUnit: VideoSampleUnit | undefined;
// try to fallback to previous sample if current one is empty
if (!VideoSample || VideoSample.units.length === 0) {
VideoSample = samples[samples.length - 1];
}
if (VideoSample?.units) {
const units = VideoSample.units;
lastUnit = units[units.length - 1];
}
return lastUnit;
}
protected pushAccessUnit(
VideoSample: ParsedVideoSample,
videoTrack: DemuxedVideoTrack,
) {
if (VideoSample.units.length && VideoSample.frame) {
// if sample does not have PTS/DTS, patch with last sample PTS/DTS
if (VideoSample.pts === undefined) {
const samples = videoTrack.samples;
const nbSamples = samples.length;
if (nbSamples) {
const lastSample = samples[nbSamples - 1];
VideoSample.pts = lastSample.pts;
VideoSample.dts = lastSample.dts;
} else {
// dropping samples, no timestamp found
videoTrack.dropped++;
return;
}
}
videoTrack.samples.push(VideoSample as VideoSample);
}
}
abstract parsePES(
track: DemuxedVideoTrack,
textTrack: DemuxedUserdataTrack,
pes: PES,
last: boolean,
);
protected abstract getNALuType(data: Uint8Array, offset: number): number;
protected parseNALu(
track: DemuxedVideoTrack,
array: Uint8Array,
endOfSegment: boolean,
): 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;
// NALu type is value read from offset 0
lastUnitType = this.getNALuType(array, 0);
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 = this.getNALuType(array, i);
// 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 BaseVideoParser;