Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions api-extractor/report/hls.js.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -803,6 +803,12 @@ export interface BufferEOSData {
//
// @public (undocumented)
export interface BufferFlushedData {
// (undocumented)
end: number;
// (undocumented)
error?: Error;
// (undocumented)
start: number;
// (undocumented)
type: SourceBufferName;
}
Expand Down
12 changes: 11 additions & 1 deletion src/controller/base-stream-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1342,7 +1342,7 @@ export default class BaseStreamController
const config = this.config;
const minLength = Math.max(
Math.min(threshold - fragDuration, config.maxBufferLength),
fragDuration,
fragDuration / 2,
);
const reducedLength = Math.max(
threshold - fragDuration * 3,
Expand Down Expand Up @@ -2038,6 +2038,16 @@ export default class BaseStreamController
this.warn(
`Buffer full error while media.currentTime (${this.getLoadPosition()}) is not buffered, flush ${playlistType} buffer`,
);
} else if (
bufferedInfo.nextStart &&
frag &&
bufferedInfo.nextStart > frag.start
) {
this.flushMainBuffer(
bufferedInfo.nextStart,
Number.POSITIVE_INFINITY,
playlistType === PlaylistLevelType.AUDIO ? playlistType : undefined,
);
}
if (frag) {
this.fragmentTracker.removeFragment(frag);
Expand Down
69 changes: 24 additions & 45 deletions src/controller/buffer-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,10 +118,7 @@ export default class BufferController extends Logger implements ComponentAPI {
audiovideo: 0,
};
private appendError?: ErrorData;
// Tracks whether a QuotaExceededError back-buffer eviction is in progress
// for a given SourceBuffer type. While true, subsequent QuotaExceededErrors
// on the same type piggyback on the pending eviction instead of triggering
// their own or emitting BUFFER_FULL_ERROR.
// Tracks whether a QuotaExceededError back-buffer eviction is in progress for a given SourceBuffer type.
private _quotaEvictionPending: Partial<Record<SourceBufferName, boolean>> =
{};
// Record of required or created buffers by type. SourceBuffer is stored in Track.buffer once created.
Expand Down Expand Up @@ -859,7 +856,6 @@ transfer tracks: ${stringify(transferredTracks, (key, value) => (key === 'initSe
}

const fragStart = (part || frag).start;
let quotaEvictionAttempted = false;
const operation: BufferOperation = {
label: `append-${type}`,
execute: () => {
Expand Down Expand Up @@ -914,31 +910,22 @@ transfer tracks: ${stringify(transferredTracks, (key, value) => (key === 'initSe
`quota` in error;

if (isQuotaError) {
// An eviction is already in flight for this type — piggyback on it
// by queuing a retry after the pending remove, no new eviction needed.
if (this._quotaEvictionPending[type]) {
this.log(
`QuotaExceededError on "${type}" sn: ${sn} — eviction already pending, queuing retry`,
);
this.insertNext([operation], type);
return;
}

// First QuotaExceededError for this type: evict minimum back buffer
if (!quotaEvictionAttempted) {
if (!this._quotaEvictionPending[type]) {
const evictEnd = this.getBackBufferEvictionTarget(
type,
data.byteLength,
frag.type,
);
if (evictEnd > 0) {
quotaEvictionAttempted = true;
this._quotaEvictionPending[type] = true;
this.log(
`QuotaExceededError on "${type}" append sn: ${sn} — evicting back buffer to ${evictEnd.toFixed(3)}s and retrying`,
);
const removeOp = this.getQuotaEvictionFlushOp(type, 0, evictEnd);
this.insertNext([removeOp, operation], type);
const removeOp = this.getFlushOp(type, 0, evictEnd);
const clearOp = this.getClearEvictionPendingOp(type);

this.insertNext([removeOp, operation, clearOp], type);
return;
}
this.warn(
Expand Down Expand Up @@ -1014,30 +1001,15 @@ transfer tracks: ${stringify(transferredTracks, (key, value) => (key === 'initSe
this.append(operation, type, this.isPending(this.tracks[type]));
}

// Like getFlushOp but clears _quotaEvictionPending on complete/error
private getQuotaEvictionFlushOp(
type: SourceBufferName,
start: number,
end: number,
): BufferOperation {
this.log(`queuing quota-eviction "${type}" remove ${start}-${end}`);
private getClearEvictionPendingOp(type: string): BufferOperation {
return {
label: 'remove',
label: 'clear',
execute: () => {
this.removeExecutor(type, start, end);
},
onStart: () => {},
onComplete: () => {
this._quotaEvictionPending[type] = false;
this.hls.trigger(Events.BUFFER_FLUSHED, { type });
},
onError: (error: Error) => {
this._quotaEvictionPending[type] = false;
this.warn(
`Failed to remove ${start}-${end} from "${type}" SourceBuffer (quota eviction)`,
error,
);
},
onStart: () => {},
onComplete: () => {},
onError: () => {},
};
}

Expand All @@ -1057,13 +1029,19 @@ transfer tracks: ${stringify(transferredTracks, (key, value) => (key === 'initSe
},
onComplete: () => {
// logger.debug(`[buffer-controller]: Finished flushing ${data.startOffset} -> ${data.endOffset} for ${type} Source Buffer`);
this.hls.trigger(Events.BUFFER_FLUSHED, { type });
this.hls.trigger(Events.BUFFER_FLUSHED, { type, start, end });
},
onError: (error: Error) => {
this.warn(
`Failed to remove ${start}-${end} from "${type}" SourceBuffer`,
error,
);
this.hls.trigger(Events.BUFFER_FLUSHED, {
type,
start: 0,
end: 0,
error,
});
},
};
}
Expand Down Expand Up @@ -1677,6 +1655,8 @@ transfer tracks: ${stringify(transferredTracks, (key, value) => (key === 'initSe
if (removedRanges?.length) {
this.hls.trigger(Events.BUFFER_FLUSHED, {
type: type,
start: removedRanges.start(0),
end: removedRanges.end(removedRanges.length - 1),
});
}
},
Expand Down Expand Up @@ -1837,11 +1817,9 @@ transfer tracks: ${stringify(transferredTracks, (key, value) => (key === 'initSe
const track = this.tracks[type];
const sb = track?.buffer;
if (!media || !mediaSource || !sb) {
this.warn(
throw new Error(
`Attempting to remove from the ${type} SourceBuffer, but it does not exist`,
);
this.shiftAndExecuteNext(type);
return;
}
const mediaDuration = Number.isFinite(media.duration)
? media.duration
Expand All @@ -1858,8 +1836,9 @@ transfer tracks: ${stringify(transferredTracks, (key, value) => (key === 'initSe
);
sb.remove(removeStart, removeEnd);
} else {
// Cycle the queue
this.shiftAndExecuteNext(type);
throw new Error(
`Cannot remove ${removeEnd <= removeStart ? `invalid range (${removeStart} >= ${removeEnd}) ` : ''}from the ${type} SourceBuffer${track.ending ? ' while track ending' : ''}`,
);
}
}

Expand Down
3 changes: 3 additions & 0 deletions src/types/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,9 @@ export interface BufferFlushingData {

export interface BufferFlushedData {
type: SourceBufferName;
start: number;
end: number;
error?: Error;
}

export interface ManifestLoadingData {
Expand Down
25 changes: 22 additions & 3 deletions tests/unit/controller/buffer-controller-operations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -780,8 +780,7 @@ describe('BufferController with attached media', function () {
).to.have.been.calledOnce;
});

it('dequeues the remove operation if the requested remove range is not valid', function () {
// Does not flush if start greater than end
it('Errors and signals flushed with error when the requested remove range is not valid', function () {
hls.trigger(Events.BUFFER_FLUSHING, {
startOffset: 9001,
endOffset: 9000,
Expand Down Expand Up @@ -811,7 +810,27 @@ describe('BufferController with attached media', function () {
expect(
triggerSpy,
'Only Events.BUFFER_FLUSHING should have been triggered',
).to.have.been.calledOnce;
).to.have.been.calledThrice;
const err1 = triggerSpy.getCall(1).lastArg.error;
const err2 = triggerSpy.getCall(2).lastArg.error;
expect(triggerSpy).to.have.been.calledWith(Events.BUFFER_FLUSHED, {
start: 0,
end: 0,
type: 'video',
error: err1,
});
expect(err1.message).to.eq(
'Cannot remove invalid range (9001 >= 9000) from the video SourceBuffer',
);
expect(triggerSpy).to.have.been.calledWith(Events.BUFFER_FLUSHED, {
start: 0,
end: 0,
type: 'audio',
error: err2,
});
expect(err2.message).to.eq(
'Cannot remove invalid range (9001 >= 9000) from the audio SourceBuffer',
);
});
});

Expand Down
Loading