Skip to content

Commit

Permalink
Improve handling of SourceBuffer creation errors
Browse files Browse the repository at this point in the history
  • Loading branch information
Rob Walch committed Mar 24, 2021
1 parent 0341fd7 commit bdafd25
Show file tree
Hide file tree
Showing 8 changed files with 52 additions and 9 deletions.
4 changes: 4 additions & 0 deletions MIGRATING.md
Expand Up @@ -53,6 +53,10 @@ Event order and content have changed in some places. See **Breaking Changes** be
- `SUBTITLE_LOAD_ERROR`
- `SUBTITLE_TRACK_LOAD_TIMEOUT`
- `UNKNOWN`
- Added additional error detail for streams that cannot start because source buffer(s) could not be created after parsing media codecs
- `BUFFER_INCOMPATIBLE_CODECS_ERROR` will fire instead of `BUFFER_CREATED` with an empty `tracks` list. This media error
is fatal and not recoverable. If you encounter this error make sure you include the correct CODECS string in
your manifest, as this is most likely to occur when attempting to play a fragmented mp4 playlist with unknown codecs.

### Fragment Stats

Expand Down
2 changes: 2 additions & 0 deletions api-extractor/report/hls.js.api.md
Expand Up @@ -383,6 +383,8 @@ export enum ErrorDetails {
// (undocumented)
BUFFER_FULL_ERROR = "bufferFullError",
// (undocumented)
BUFFER_INCOMPATIBLE_CODECS_ERROR = "bufferIncompatibleCodecsError",
// (undocumented)
BUFFER_NUDGE_ON_STALL = "bufferNudgeOnStall",
// (undocumented)
BUFFER_SEEK_OVER_HOLE = "bufferSeekOverHole",
Expand Down
4 changes: 3 additions & 1 deletion docs/API.md
Expand Up @@ -1540,7 +1540,9 @@ Full list of errors is described below:
- `Hls.ErrorDetails.FRAG_PARSING_ERROR` - raised when fragment parsing fails
- data: { type : `MEDIA_ERROR`, details : `Hls.ErrorDetails.FRAG_PARSING_ERROR`, fatal : `true` or `false`, reason : failure reason }
- `Hls.ErrorDetails.BUFFER_ADD_CODEC_ERROR` - raised when MediaSource fails to add new sourceBuffer
- data: { type : `MEDIA_ERROR`, details : `Hls.ErrorDetails.BUFFER_ADD_CODEC_ERROR`, fatal : `false`, err : error raised by MediaSource, mimeType: mimeType on which the failure happened }
- data: { type : `MEDIA_ERROR`, details : `Hls.ErrorDetails.BUFFER_ADD_CODEC_ERROR`, fatal : `false`, error : error raised by MediaSource, mimeType: mimeType on which the failure happened }
- `Hls.ErrorDetails.BUFFER_INCOMPATIBLE_CODECS_ERROR` - raised when no MediaSource(s) could be created based on track codec(s)
- data: { type : `MEDIA_ERROR`, details : `Hls.ErrorDetails.BUFFER_INCOMPATIBLE_CODECS_ERROR`, fatal : `true`, reason : failure reason }
- `Hls.ErrorDetails.BUFFER_APPEND_ERROR` - raised when exception is raised while calling buffer append
- data: { type : `MEDIA_ERROR`, details : `Hls.ErrorDetails.BUFFER_APPEND_ERROR`, fatal : `true` or `false`, parent : parent stream controller }
- `Hls.ErrorDetails.BUFFER_APPENDING_ERROR` - raised when exception is raised during buffer appending
Expand Down
1 change: 1 addition & 0 deletions docs/design.md
Expand Up @@ -287,6 +287,7 @@ design idea is pretty simple :
- if auto level switch is enabled and loaded frag level is greater than 0, this error is not fatal: in that case [src/controller/level-controller.ts][] will trigger an emergency switch down to level 0.
- if frag level is 0 or auto level switch is disabled, this error is marked as fatal and a call to `hls.startLoad()` could help recover it.
- `BUFFER_ADD_CODEC_ERROR` is raised by [src/controller/buffer-controller.ts][] when an exception is raised when calling mediaSource.addSourceBuffer(). this error is non fatal.
- `BUFFER_INCOMPATIBLE_CODECS_ERROR` is raised by [src/controller/buffer-controller.ts][] when an exception is raised when all attempts to add SourceBuffer(s) failed. this error is fatal.
- `BUFFER_APPEND_ERROR` is raised by [src/controller/buffer-controller.ts][] when an exception is raised when calling sourceBuffer.appendBuffer(). this error is non fatal and become fatal after config.appendErrorMaxRetry retries. when fatal, a call to `hls.recoverMediaError()` could help recover it.
- `BUFFER_APPENDING_ERROR` is raised by [src/controller/buffer-controller.ts][] after SourceBuffer appending error. this error is fatal and a call to `hls.recoverMediaError()` could help recover it.
- `BUFFER_FULL_ERROR` is raised by [src/controller/buffer-controller.ts][] if sourcebuffer is full
Expand Down
19 changes: 16 additions & 3 deletions src/controller/buffer-controller.ts
Expand Up @@ -659,7 +659,17 @@ export default class BufferController implements ComponentAPI {
this.createSourceBuffers(pendingTracks);
this.pendingTracks = {};
// append any pending segments now !
Object.keys(this.sourceBuffer).forEach((type: SourceBufferName) => {
const buffers = Object.keys(this.sourceBuffer);
if (buffers.length === 0) {
this.hls.trigger(Events.ERROR, {
type: ErrorTypes.MEDIA_ERROR,
details: ErrorDetails.BUFFER_INCOMPATIBLE_CODECS_ERROR,
fatal: true,
reason: 'could not create source buffer for media codec(s)',
});
return;
}
buffers.forEach((type: SourceBufferName) => {
operationQueue.executeNext(type);
});
}
Expand All @@ -670,7 +680,7 @@ export default class BufferController implements ComponentAPI {
if (!mediaSource) {
throw Error('createSourceBuffers called when mediaSource was null');
}

let tracksCreated = 0;
for (const trackName in tracks) {
if (!sourceBuffer[trackName]) {
const track = tracks[trackName as keyof TrackSet];
Expand Down Expand Up @@ -698,6 +708,7 @@ export default class BufferController implements ComponentAPI {
levelCodec: track.levelCodec,
id: track.id,
};
tracksCreated++;
} catch (err) {
logger.error(
`[buffer-controller]: error while trying to add sourceBuffer: ${err.message}`
Expand All @@ -712,7 +723,9 @@ export default class BufferController implements ComponentAPI {
}
}
}
this.hls.trigger(Events.BUFFER_CREATED, { tracks: this.tracks });
if (tracksCreated) {
this.hls.trigger(Events.BUFFER_CREATED, { tracks: this.tracks });
}
}

// Keep as arrow functions so that we can directly reference these functions directly as event listeners
Expand Down
4 changes: 3 additions & 1 deletion src/errors.ts
Expand Up @@ -60,8 +60,10 @@ export enum ErrorDetails {
KEY_LOAD_ERROR = 'keyLoadError',
// Identifier for decrypt key load timeout error - data: { frag : fragment object}
KEY_LOAD_TIMEOUT = 'keyLoadTimeOut',
// Triggered when an exception occurs while adding a sourceBuffer to MediaSource - data : { err : exception , mimeType : mimeType }
// Triggered when an exception occurs while adding a sourceBuffer to MediaSource - data : { error : exception , mimeType : mimeType }
BUFFER_ADD_CODEC_ERROR = 'bufferAddCodecError',
// Triggered when source buffer(s) could not be created using level (manifest CODECS attribute), parsed media, or best guess codec(s) - data: { reason : error reason }
BUFFER_INCOMPATIBLE_CODECS_ERROR = 'bufferIncompatibleCodecsError',
// Identifier for a buffer append error - data: append error description
BUFFER_APPEND_ERROR = 'bufferAppendError',
// Identifier for a buffer appending error event - data: appending error description
Expand Down
11 changes: 8 additions & 3 deletions src/utils/xhr-loader.ts
Expand Up @@ -170,17 +170,22 @@ class XhrLoader implements Loader<LoaderContext> {
}
stats.loaded = stats.total = len;

const onProgress = this.callbacks!.onProgress;
if (!this.callbacks) {
return;
}
const onProgress = this.callbacks.onProgress;
if (onProgress) {
onProgress(stats, context, data, xhr);
}

if (!this.callbacks) {
return;
}
const response = {
url: xhr.responseURL,
data: data,
};

this.callbacks!.onSuccess(response, stats, context, xhr);
this.callbacks.onSuccess(response, stats, context, xhr);
} else {
// if max nb of retries reached or if http status between 400 and 499 (such error cannot be recovered, retrying is useless), return error
if (
Expand Down
16 changes: 15 additions & 1 deletion tests/unit/controller/buffer-controller.js
Expand Up @@ -169,6 +169,7 @@ describe('BufferController tests', function () {
bufferController.onMediaAttaching(Events.MEDIA_ATTACHING, {
media: video,
});
sandbox.stub(bufferController.mediaSource, 'addSourceBuffer');

hls.on(Hls.Events.BUFFER_CREATED, (event, data) => {
const tracks = data.tracks;
Expand All @@ -177,7 +178,20 @@ describe('BufferController tests', function () {
done();
});

bufferController.pendingTracks = { video: { codec: 'testing' } };
hls.once(Hls.Events.ERROR, (event, data) => {
// Async timeout prevents assertion from throwing in event handler
self.setTimeout(() => {
expect(data.error.message).to.equal(null);
done();
});
});

bufferController.pendingTracks = {
video: {
container: 'video/mp4',
codec: 'avc1.42e01e',
},
};
bufferController.checkPendingTracks();

video = null;
Expand Down

0 comments on commit bdafd25

Please sign in to comment.