Make part.gap writable and handle part muxing errors as gaps (#7814)

* Make part.gap writable and handle part muxing errors as gaps
* Fix regression in part picking introduced in #7797
* Fix regression in fmp4 endPTS parsing introduced in #7807
* Fix handling of audio fragment parsing error prior to appending init-segment
This commit is contained in:
Rob Walch
2026-04-23 08:52:29 -07:00
committed by GitHub
parent 0d2b39677d
commit 3e23c7dcd2
12 changed files with 69 additions and 110 deletions
+1 -1
View File
@@ -4517,7 +4517,7 @@ export class Part extends BaseSegment {
// (undocumented)
readonly fragOffset: number;
// (undocumented)
readonly gap: boolean;
gap: boolean;
// (undocumented)
readonly independent: boolean;
// (undocumented)
@@ -905,6 +905,9 @@ class AudioStreamController
// If we are, subsequently check if the currently loading fragment (fragCurrent) has changed.
if (this.fragContextChanged(frag) || !details) {
this.fragmentTracker.removeFragment(frag);
if (initSegment?.tracks) {
this.resetTransmuxer();
}
return;
}
+8 -5
View File
@@ -1525,7 +1525,6 @@ export default class BaseStreamController
if (frag.stats.retry > 1) {
return true;
}
frag.stats.retry++;
}
return false;
}
@@ -1664,7 +1663,7 @@ export default class BaseStreamController
if (nextPart > -1 && targetBufferTime < part.start) {
break;
}
const loaded = part.loaded;
const loaded = part.loaded || part.gap;
if (loaded) {
nextPart = -1;
} else if (
@@ -1673,7 +1672,7 @@ export default class BaseStreamController
) {
nextPart = i;
}
contiguous = loaded;
contiguous = loaded && !part.gap;
}
const part = partList[nextPart];
if (part && part.fragment !== frag) {
@@ -2016,7 +2015,7 @@ export default class BaseStreamController
data.frag = context.frag;
}
}
const frag = data.frag;
const { frag, part } = data;
// Handle frag error related to caller's filterType
if (!frag || !this.levels || frag.type !== filterType) {
return;
@@ -2054,7 +2053,11 @@ export default class BaseStreamController
!isUnusableKeyError(data)
) {
this.resetFragmentErrors(filterType);
this.treatAsGap(frag);
if (part) {
part.gap = true;
} else {
this.treatAsGap(frag);
}
errorAction.resolved = true;
} else if ((retry || noAlternate) && retryCount < retryConfig.maxNumRetry) {
const offlineStatus = offlineHttpStatus(data.response?.code);
+1 -1
View File
@@ -250,7 +250,7 @@ export class FragmentTracker implements ComponentAPI {
const partial = isFragHint || streamInfo.partial === true;
fragmentEntity.range[elementaryStream] = this.getBufferedTimes(
frag,
data.part,
part,
partial,
timeRange,
);
+15 -3
View File
@@ -349,7 +349,7 @@ class TSDemuxer implements Demuxer {
if (audioData && (pes = parsePES(audioData, this.logger))) {
switch (audioTrack.segmentCodec) {
case 'aac':
this.parseAACPES(audioTrack, pes);
this.parseAACPES(audioTrack, pes, chunkMeta);
break;
case 'mp3':
this.parseMPEGPES(audioTrack, pes);
@@ -421,6 +421,7 @@ class TSDemuxer implements Demuxer {
this.observer,
this.logger,
this.config,
chunkMeta,
);
// only update track id if track PID found while parsing PMT
@@ -478,6 +479,7 @@ class TSDemuxer implements Demuxer {
new Error(
`Found ${tsPacketErrors} TS packet/s that do not start with 0x47`,
),
chunkMeta,
undefined,
this.logger,
);
@@ -560,7 +562,7 @@ class TSDemuxer implements Demuxer {
if (audioData && (pes = parsePES(audioData, this.logger))) {
switch (audioTrack.segmentCodec) {
case 'aac':
this.parseAACPES(audioTrack, pes);
this.parseAACPES(audioTrack, pes, chunkMeta);
break;
case 'mp3':
this.parseMPEGPES(audioTrack, pes);
@@ -671,7 +673,11 @@ class TSDemuxer implements Demuxer {
this.videoIntegrityChecker = null;
}
private parseAACPES(track: DemuxedAudioTrack, pes: PES) {
private parseAACPES(
track: DemuxedAudioTrack,
pes: PES,
chunkMeta: ChunkMetadata,
) {
let startOffset = 0;
const aacOverFlow = this.aacOverFlow;
let data = pes.data;
@@ -712,6 +718,7 @@ class TSDemuxer implements Demuxer {
emitParsingError(
this.observer,
new Error(reason),
chunkMeta,
recoverable,
this.logger,
);
@@ -922,6 +929,7 @@ function parsePMT(
observer: HlsEventEmitter,
logger: ILogger,
config: HlsConfig,
chunkMeta: ChunkMetadata,
) {
const result = {
audioPid: -1,
@@ -1062,6 +1070,7 @@ function parsePMT(
emitParsingError(
observer,
new Error('Unsupported EC-3 in M2TS found'),
chunkMeta,
undefined,
logger,
);
@@ -1078,6 +1087,7 @@ function parsePMT(
emitParsingError(
observer,
new Error('Unsupported HEVC in M2TS found'),
chunkMeta,
undefined,
logger,
);
@@ -1099,6 +1109,7 @@ function parsePMT(
function emitParsingError(
observer: HlsEventEmitter,
error: Error,
chunkMeta: ChunkMetadata,
levelRetry: boolean | undefined,
logger: ILogger,
) {
@@ -1108,6 +1119,7 @@ function emitParsingError(
details: ErrorDetails.FRAG_PARSING_ERROR,
fatal: false,
levelRetry,
chunkMeta,
error,
reason: error.message,
});
+1 -1
View File
@@ -433,11 +433,11 @@ export class Fragment extends BaseSegment {
export class Part extends BaseSegment {
public readonly fragOffset: number = 0;
public readonly duration: number = 0;
public readonly gap: boolean = false;
public readonly independent: boolean = false;
public readonly relurl: string;
public readonly fragment: MediaFragment;
public readonly index: number;
public gap: boolean = false;
constructor(
partAttrs: AttrList,
+5 -1
View File
@@ -280,6 +280,7 @@ export default class MP4Remuxer extends Logger implements Remuxer {
playlistType === PlaylistLevelType.AUDIO
? videoTimeOffset
: undefined,
chunkMeta,
);
if (enoughVideoSamples) {
const audioTrackLength = audio ? audio.endPTS - audio.startPTS : 0;
@@ -713,6 +714,7 @@ export default class MP4Remuxer extends Logger implements Remuxer {
type: ErrorTypes.MUX_ERROR,
details: ErrorDetails.REMUX_ALLOC_ERROR,
fatal: false,
chunkMeta,
error: err,
bytes: mdatSize,
reason: `fail allocating video mdat ${mdatSize}`,
@@ -932,7 +934,8 @@ export default class MP4Remuxer extends Logger implements Remuxer {
timeOffset: number,
contiguous: boolean,
accurateTimeOffset: boolean,
videoTimeOffset?: number,
videoTimeOffset: number | undefined,
chunkMeta: ChunkMetadata,
): RemuxedTrack | undefined {
const inputTimeScale: number = track.inputTimeScale;
const mp4timeScale: number = track.samplerate
@@ -1134,6 +1137,7 @@ export default class MP4Remuxer extends Logger implements Remuxer {
type: ErrorTypes.MUX_ERROR,
details: ErrorDetails.REMUX_ALLOC_ERROR,
fatal: false,
chunkMeta,
error: err,
bytes: mdatSize,
reason: `fail allocating audio mdat ${mdatSize}`,
+16 -7
View File
@@ -8,9 +8,9 @@ import { getCodecCompatibleName } from '../utils/codecs';
import { type ILogger, Logger } from '../utils/logger';
import { patchEncyptionData, writeUint32 } from '../utils/mp4-tools';
import { getSampleData, parseInitSegment } from '../utils/mp4-tools';
import type { HlsEventEmitter } from '../events';
import type { TrackFragmentSample } from './mp4-generator';
import type { HlsConfig } from '../config';
import type { HlsEventEmitter } from '../events';
import type { DecryptData } from '../loader/level-key';
import type {
DemuxedAudioTrack,
@@ -32,6 +32,7 @@ import type { InitData, InitDataTrack, TrackTimes } from '../utils/mp4-tools';
import type { TimestampOffset } from '../utils/timescale-conversion';
class PassThroughRemuxer extends Logger implements Remuxer {
private readonly observer: HlsEventEmitter;
private emitInitSegment: boolean = false;
private audioCodec?: string;
private videoCodec?: string;
@@ -48,9 +49,16 @@ class PassThroughRemuxer extends Logger implements Remuxer {
logger: ILogger,
) {
super('passthrough-remuxer', logger);
this.observer = observer;
}
public destroy() {}
public destroy() {
if (this.observer) {
this.observer.removeAllListeners();
}
// @ts-ignore
this.observer = null;
}
public resetTimeStamp(defaultInitPTS: TimestampOffset | null) {
this.lastEndTime = null;
@@ -393,14 +401,15 @@ class PassThroughRemuxer extends Logger implements Remuxer {
const startDTS = decodeTime - initPTS.baseTime / initPTS.timescale;
const endDTS = startDTS + duration;
const startPTS =
baseOffsetSamples?.ptsMin !== undefined
hasVideo && baseOffsetSamples?.ptsMin !== undefined
? baseOffsetSamples.ptsMin / baseOffsetSamples.timescale -
initPTS.baseTime / initPTS.timescale
: startDTS;
const endPTS = baseOffsetSamples?.ptsMax
? baseOffsetSamples.ptsMax / baseOffsetSamples.timescale -
initPTS.baseTime / initPTS.timescale
: endDTS;
const endPTS =
hasVideo && baseOffsetSamples?.ptsMax
? baseOffsetSamples.ptsMax / baseOffsetSamples.timescale -
initPTS.baseTime / initPTS.timescale
: endDTS;
// For troubleshooting duplicates of https://github.com/video-dev/hls.js/issues/6777
// if (videoSampleTimestamps) {
+3 -3
View File
@@ -32,9 +32,7 @@ export function shouldAlignOnDiscontinuities(
function adjustFragmentStart(frag: Fragment, sliding: number) {
const start = frag.start + sliding;
frag.startPTS = start;
frag.setStart(start);
frag.endPTS = start + frag.duration;
}
export function adjustSlidingStart(sliding: number, details: LevelDetails) {
@@ -148,7 +146,9 @@ export function alignMediaPlaylistByPDT(
frag = findFirstFragWithCC(fragments, targetCC);
}
if (!refFrag || !frag) {
refFrag = refFragments[Math.floor(refFragments.length / 2)];
refFrag =
refFragments[Math.floor(refFragments.length / 2)] ||
refFragments.filter((f) => !!f)[0];
frag =
findFirstFragWithCC(fragments, refFrag.cc) ||
fragments[Math.floor(fragments.length / 2)];
+2
View File
@@ -294,6 +294,8 @@ export function mergeDetails(
(oldPart: Part, newPart: Part) => {
newPart.elementaryStreams = oldPart.elementaryStreams;
newPart.stats = oldPart.stats;
// Use locally set gap or GAP attribute introduced in playlist
newPart.gap = oldPart.gap || newPart.gap;
},
);
+3 -2
View File
@@ -759,11 +759,12 @@ export function getSampleData(
type: track.type,
});
// get start DTS
let baseTime: number | undefined;
const tfdt = findBox(traf, ['tfdt'])[0];
if (tfdt as any) {
const version = tfdt[0];
let baseTime = readUint32(tfdt, 4);
baseTime = readUint32(tfdt, 4);
if (version === 1) {
// If value is too large, assume signed 64-bit. Negative track fragment decode times are invalid, but they exist in the wild.
// This prevents large values from being used for initPTS, which can cause playlist sync issues.
@@ -815,7 +816,7 @@ export function getSampleData(
}
const truns = findBox(traf, ['trun']);
let sampleDTS = trackTimes.start || 0;
let sampleDTS = baseTime || 0;
let rawDuration = 0;
let sampleDuration = defaultSampleDuration;
for (let j = 0; j < truns.length; j++) {
+11 -86
View File
@@ -18,8 +18,6 @@ use(sinonChai);
const mockReferenceFrag = objToFragment({
start: 20,
startPTS: 20,
endPTS: 24,
duration: 4,
cc: 0,
});
@@ -27,22 +25,16 @@ const mockReferenceFrag = objToFragment({
const mockFrags = [
{
start: 0,
startPTS: 0,
endPTS: 4,
duration: 4,
cc: 0,
},
{
start: 4,
startPTS: 4,
endPTS: 8,
duration: 4,
cc: 1,
},
{
start: 8,
startPTS: 8,
endPTS: 16,
duration: 8,
cc: 1,
},
@@ -58,29 +50,23 @@ describe('discontinuities', function () {
const expected = [
{
start: 20,
startPTS: 20,
endPTS: 24,
duration: 4,
cc: 0,
},
{
start: 24,
startPTS: 24,
endPTS: 28,
duration: 4,
cc: 1,
},
{
start: 28,
startPTS: 28,
endPTS: 36,
duration: 8,
cc: 1,
},
].map(objToFragment);
adjustSlidingStart(mockReferenceFrag.start, details);
expect(expected).to.deep.equal(details.fragments);
expect(details.fragments).to.deep.equal(expected);
expect(details.alignedSliding).to.be.true;
});
@@ -94,30 +80,22 @@ describe('discontinuities', function () {
fragments: [
{
start: 18,
startPTS: undefined,
endPTS: undefined,
duration: 2,
programDateTime: 1629821766107,
},
{
start: 20,
startPTS: undefined,
endPTS: 22,
duration: 2,
programDateTime: 1629821768107,
},
{
start: 22,
startPTS: 22,
endPTS: 30,
duration: 8,
programDateTime: 1629821770107,
},
].map(objToFragment),
fragmentHint: objToFragment({
start: 30,
startPTS: 30,
endPTS: 32,
duration: 2,
programDateTime: 1629821778107,
}),
@@ -128,8 +106,6 @@ describe('discontinuities', function () {
fragments: [
{
start: 18,
startPTS: undefined,
endPTS: undefined,
duration: 2,
programDateTime: 1629821768107,
},
@@ -144,30 +120,22 @@ describe('discontinuities', function () {
fragments: [
{
start: 16,
startPTS: 16,
endPTS: 18,
duration: 2,
programDateTime: 1629821766107,
},
{
start: 18,
startPTS: 18,
endPTS: 20,
duration: 2,
programDateTime: 1629821768107,
},
{
start: 20,
startPTS: 20,
endPTS: 28,
duration: 8,
programDateTime: 1629821770107,
},
].map(objToFragment),
fragmentHint: objToFragment({
start: 28,
startPTS: 28,
endPTS: 30,
duration: 2,
programDateTime: 1629821778107,
}),
@@ -197,32 +165,24 @@ describe('discontinuities', function () {
fragments: [
{
start: 20,
startPTS: 20,
endPTS: 24,
duration: 4,
cc: 1,
programDateTime: 1503892800000,
},
{
start: 24,
startPTS: 24,
endPTS: 28,
duration: 4,
cc: 2,
programDateTime: 1503892850000,
},
{
start: 28,
startPTS: 28,
endPTS: 36,
duration: 8,
cc: 3,
programDateTime: 1501111110000,
},
{
start: 28,
startPTS: 28,
endPTS: 36,
duration: 8,
cc: 3,
programDateTime: 1501111118000,
@@ -235,32 +195,24 @@ describe('discontinuities', function () {
fragments: [
{
start: 0,
startPTS: 0,
endPTS: 4,
duration: 4,
cc: 2,
programDateTime: 1503892850000,
},
{
start: 4,
startPTS: 4,
endPTS: 8,
duration: 4,
cc: 3,
programDateTime: 1501111110000,
},
{
start: 8,
startPTS: 8,
endPTS: 12,
duration: 4,
cc: 3,
programDateTime: 1501111114000,
},
{
start: 12,
startPTS: 12,
endPTS: 16,
duration: 4,
cc: 4,
programDateTime: 1503892854000,
@@ -276,32 +228,24 @@ describe('discontinuities', function () {
fragments: [
{
start: 24,
startPTS: 24,
endPTS: 28,
duration: 4,
cc: 2,
programDateTime: 1503892850000,
},
{
start: 28,
startPTS: 28,
endPTS: 32,
duration: 4,
cc: 3,
programDateTime: 1501111110000,
},
{
start: 32,
startPTS: 32,
endPTS: 36,
duration: 4,
cc: 3,
programDateTime: 1501111114000,
},
{
start: 36,
startPTS: 36,
endPTS: 40,
duration: 4,
cc: 4,
programDateTime: 1503892854000,
@@ -326,24 +270,18 @@ describe('discontinuities', function () {
fragments: [
{
start: 20,
startPTS: 20,
endPTS: 24,
duration: 4,
cc: 0,
programDateTime: 1503892800000,
},
{
start: 24,
startPTS: 24,
endPTS: 28,
duration: 4,
cc: 1,
programDateTime: 1503892804000,
},
{
start: 28,
startPTS: 28,
endPTS: 36,
duration: 8,
cc: 1,
programDateTime: 1503892808000,
@@ -356,24 +294,18 @@ describe('discontinuities', function () {
fragments: [
{
start: 0,
startPTS: 0,
endPTS: 4,
duration: 4,
cc: 2,
programDateTime: 1503892850000,
},
{
start: 4,
startPTS: 4,
endPTS: 8,
duration: 4,
cc: 2,
programDateTime: 1503892854000,
},
{
start: 8,
startPTS: 8,
endPTS: 16,
duration: 8,
cc: 3,
programDateTime: 1503892858000,
@@ -389,8 +321,6 @@ describe('discontinuities', function () {
fragments: [
{
start: 70,
startPTS: 70,
endPTS: 74,
duration: 4,
cc: 2,
programDateTime: 1503892850000,
@@ -398,8 +328,6 @@ describe('discontinuities', function () {
},
{
start: 74,
startPTS: 74,
endPTS: 78,
duration: 4,
cc: 2,
programDateTime: 1503892854000,
@@ -407,8 +335,6 @@ describe('discontinuities', function () {
},
{
start: 78,
startPTS: 78,
endPTS: 86,
duration: 8,
cc: 3,
programDateTime: 1503892858000,
@@ -431,10 +357,9 @@ describe('discontinuities', function () {
describe('alignDiscontinuities', function () {
it('aligns playlists (LevelDetails fragments starts) based on overlapping discontinuity sequence change', function () {
const prevDetails = objToLevelDetails({
fragments: [
mockReferenceFrag,
{ start: 24, startPTS: 24, endPTS: 28, duration: 4, cc: 1 },
].map(objToFragment),
fragments: [mockReferenceFrag, { start: 24, duration: 4, cc: 1 }].map(
objToFragment,
),
});
const curDetails = objToLevelDetails({
fragments: mockFrags.map(objToFragment),
@@ -448,8 +373,8 @@ describe('discontinuities', function () {
const prevDetails = objToLevelDetails({
fragments: [
mockReferenceFrag,
{ start: 24, startPTS: 24, endPTS: 28, duration: 4, cc: 1 },
{ start: 28, startPTS: 28, endPTS: 32, duration: 4, cc: 2 },
{ start: 24, duration: 4, cc: 1 },
{ start: 28, duration: 4, cc: 2 },
].map(objToFragment),
});
const curDetails = objToLevelDetails({
@@ -464,15 +389,15 @@ describe('discontinuities', function () {
const prevDetails = objToLevelDetails({
fragments: [
mockReferenceFrag,
{ start: 24, startPTS: 24, endPTS: 28, duration: 4.5, cc: 1 },
{ start: 28.5, startPTS: 28, endPTS: 32, duration: 4, cc: 2 },
{ start: 24, duration: 4.5, cc: 1 },
{ start: 28.5, duration: 4, cc: 2 },
].map(objToFragment),
});
const curDetails = objToLevelDetails({
fragments: [
{ start: 0, startPTS: 0, endPTS: 4, duration: 4.5, cc: 1 },
{ start: 4.5, startPTS: 4.5, endPTS: 8.5, duration: 4, cc: 2 },
{ start: 8.5, startPTS: 4.5, endPTS: 12.5, duration: 4, cc: 3 },
{ start: 0, duration: 4.5, cc: 1 },
{ start: 4.5, duration: 4, cc: 2 },
{ start: 8.5, duration: 4, cc: 3 },
].map(objToFragment),
});
alignDiscontinuities(curDetails, prevDetails, logger);