Skip to content

Commit b26a5b8

Browse files
authored
Feature: buffer robustness (#3743)
* Refactor PlaybackController.js * Refactor BufferController.js * Refactor SourceBufferSink.js * Reset MSE after MEDIA_ERROR_DECODE and blacklist the segment that caused the error * Fix unit test * Account for SegmentBase content when blacklisting segments
1 parent e431e8b commit b26a5b8

File tree

10 files changed

+238
-132
lines changed

10 files changed

+238
-132
lines changed

src/core/Settings.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -884,6 +884,11 @@ function Settings() {
884884
rtpSafetyFactor: 5,
885885
mode: Constants.CMCD_MODE_QUERY
886886
}
887+
},
888+
errors: {
889+
recoverAttempts: {
890+
mediaErrorDecode: 5
891+
}
887892
}
888893
};
889894

src/core/events/CoreEvents.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,10 +62,13 @@ class CoreEvents extends EventsBase {
6262
this.MEDIA_FRAGMENT_LOADED = 'mediaFragmentLoaded';
6363
this.MEDIA_FRAGMENT_NEEDED = 'mediaFragmentNeeded';
6464
this.QUOTA_EXCEEDED = 'quotaExceeded';
65+
this.SEGMENT_LOCATION_BLACKLIST_ADD = 'segmentLocationBlacklistAdd';
66+
this.SEGMENT_LOCATION_BLACKLIST_CHANGED = 'segmentLocationBlacklistChanged';
6567
this.SERVICE_LOCATION_BLACKLIST_ADD = 'serviceLocationBlacklistAdd';
6668
this.SERVICE_LOCATION_BLACKLIST_CHANGED = 'serviceLocationBlacklistChanged';
6769
this.SET_FRAGMENTED_TEXT_AFTER_DISABLED = 'setFragmentedTextAfterDisabled';
6870
this.SET_NON_FRAGMENTED_TEXT = 'setNonFragmentedText';
71+
this.SOURCE_BUFFER_ERROR = 'sourceBufferError';
6972
this.STREAMS_COMPOSED = 'streamsComposed';
7073
this.STREAM_BUFFERING_COMPLETED = 'streamBufferingCompleted';
7174
this.STREAM_REQUESTING_COMPLETED = 'streamRequestingCompleted';

src/streaming/SourceBufferSink.js

Lines changed: 41 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import Errors from '../core/errors/Errors';
3535
import Settings from '../core/Settings';
3636
import constants from './constants/Constants';
3737
import {HTTPRequest} from './vo/metrics/HTTPRequest';
38+
import Events from '../core/events/Events';
3839

3940
const APPEND_WINDOW_START_OFFSET = 0.1;
4041
const APPEND_WINDOW_END_OFFSET = 0.01;
@@ -51,6 +52,7 @@ function SourceBufferSink(config) {
5152
const context = this.context;
5253
const settings = Settings(context).getInstance();
5354
const textController = config.textController;
55+
const eventBus = config.eventBus;
5456

5557
let instance,
5658
type,
@@ -63,6 +65,7 @@ function SourceBufferSink(config) {
6365
let appendQueue = [];
6466
let isAppendingInProgress = false;
6567
let mediaSource = config.mediaSource;
68+
let lastRequestAppended = null;
6669

6770
function setup() {
6871
logger = Debug(context).getInstance().getLogger(instance);
@@ -91,7 +94,7 @@ function SourceBufferSink(config) {
9194

9295
function changeType(codec) {
9396
return new Promise((resolve) => {
94-
waitForUpdateEnd(() => {
97+
_waitForUpdateEnd(() => {
9598
if (buffer.changeType) {
9699
buffer.changeType(codec);
97100
}
@@ -149,17 +152,17 @@ function SourceBufferSink(config) {
149152
// use updateend event if possible
150153
if (typeof buffer.addEventListener === 'function') {
151154
try {
152-
buffer.addEventListener('updateend', updateEndHandler, false);
153-
buffer.addEventListener('error', errHandler, false);
154-
buffer.addEventListener('abort', errHandler, false);
155+
buffer.addEventListener('updateend', _updateEndHandler, false);
156+
buffer.addEventListener('error', _errHandler, false);
157+
buffer.addEventListener('abort', _errHandler, false);
155158

156159
} catch (err) {
157160
// use setInterval to periodically check if updating has been completed
158-
intervalId = setInterval(updateEndHandler, CHECK_INTERVAL);
161+
intervalId = setInterval(_updateEndHandler, CHECK_INTERVAL);
159162
}
160163
} else {
161164
// use setInterval to periodically check if updating has been completed
162-
intervalId = setInterval(updateEndHandler, CHECK_INTERVAL);
165+
intervalId = setInterval(_updateEndHandler, CHECK_INTERVAL);
163166
}
164167
}
165168

@@ -170,9 +173,9 @@ function SourceBufferSink(config) {
170173
function _removeEventListeners() {
171174
try {
172175
if (typeof buffer.removeEventListener === 'function') {
173-
buffer.removeEventListener('updateend', updateEndHandler, false);
174-
buffer.removeEventListener('error', errHandler, false);
175-
buffer.removeEventListener('abort', errHandler, false);
176+
buffer.removeEventListener('updateend', _updateEndHandler, false);
177+
buffer.removeEventListener('error', _errHandler, false);
178+
buffer.removeEventListener('abort', _errHandler, false);
176179
}
177180
clearInterval(intervalId);
178181
} catch (e) {
@@ -188,7 +191,7 @@ function SourceBufferSink(config) {
188191
return;
189192
}
190193

191-
waitForUpdateEnd(() => {
194+
_waitForUpdateEnd(() => {
192195
try {
193196
if (!buffer) {
194197
resolve();
@@ -227,7 +230,7 @@ function SourceBufferSink(config) {
227230
return;
228231
}
229232

230-
waitForUpdateEnd(() => {
233+
_waitForUpdateEnd(() => {
231234
try {
232235
if (buffer.timestampOffset !== MSETimeOffset && !isNaN(MSETimeOffset)) {
233236
buffer.timestampOffset = MSETimeOffset;
@@ -258,6 +261,7 @@ function SourceBufferSink(config) {
258261
}
259262
buffer = null;
260263
}
264+
lastRequestAppended = null;
261265
}
262266

263267
function getBuffer() {
@@ -273,7 +277,7 @@ function SourceBufferSink(config) {
273277
}
274278
}
275279

276-
function append(chunk) {
280+
function append(chunk, request = null) {
277281
return new Promise((resolve, reject) => {
278282
if (!chunk) {
279283
reject({
@@ -282,14 +286,14 @@ function SourceBufferSink(config) {
282286
});
283287
return;
284288
}
285-
appendQueue.push({ data: chunk, promise: { resolve, reject } });
286-
waitForUpdateEnd(appendNextInQueue.bind(this));
289+
appendQueue.push({ data: chunk, promise: { resolve, reject }, request });
290+
_waitForUpdateEnd(_appendNextInQueue.bind(this));
287291
});
288292
}
289293

290294
function _abortBeforeAppend() {
291295
return new Promise((resolve) => {
292-
waitForUpdateEnd(() => {
296+
_waitForUpdateEnd(() => {
293297
// Save the append window, which is reset on abort().
294298
const appendWindowStart = buffer.appendWindowStart;
295299
const appendWindowEnd = buffer.appendWindowEnd;
@@ -313,11 +317,11 @@ function SourceBufferSink(config) {
313317
return;
314318
}
315319

316-
waitForUpdateEnd(function () {
320+
_waitForUpdateEnd(function () {
317321
try {
318322
buffer.remove(start, end);
319323
// updating is in progress, we should wait for it to complete before signaling that this operation is done
320-
waitForUpdateEnd(function () {
324+
_waitForUpdateEnd(function () {
321325
resolve({
322326
from: start,
323327
to: end,
@@ -342,7 +346,7 @@ function SourceBufferSink(config) {
342346
});
343347
}
344348

345-
function appendNextInQueue() {
349+
function _appendNextInQueue() {
346350
if (isAppendingInProgress) {
347351
return;
348352
}
@@ -355,7 +359,7 @@ function SourceBufferSink(config) {
355359
const afterSuccess = function () {
356360
isAppendingInProgress = false;
357361
if (appendQueue.length > 0) {
358-
appendNextInQueue.call(this);
362+
_appendNextInQueue.call(this);
359363
}
360364
// Init segments are cached. In any other case we dont need the chunk bytes anymore and can free the memory
361365
if (nextChunk && nextChunk.data && nextChunk.data.segmentType && nextChunk.data.segmentType !== HTTPRequest.INIT_SEGMENT_TYPE) {
@@ -365,6 +369,7 @@ function SourceBufferSink(config) {
365369
};
366370

367371
try {
372+
lastRequestAppended = nextChunk.request;
368373
if (nextChunk.data.bytes.byteLength === 0) {
369374
afterSuccess.call(this);
370375
} else {
@@ -374,12 +379,12 @@ function SourceBufferSink(config) {
374379
buffer.append(nextChunk.data.bytes, nextChunk.data);
375380
}
376381
// updating is in progress, we should wait for it to complete before signaling that this operation is done
377-
waitForUpdateEnd(afterSuccess.bind(this));
382+
_waitForUpdateEnd(afterSuccess.bind(this));
378383
}
379384
} catch (err) {
380385
logger.fatal('SourceBuffer append failed "' + err + '"');
381386
if (appendQueue.length > 0) {
382-
appendNextInQueue();
387+
_appendNextInQueue();
383388
} else {
384389
isAppendingInProgress = false;
385390
}
@@ -395,7 +400,7 @@ function SourceBufferSink(config) {
395400
try {
396401
appendQueue = [];
397402
if (mediaSource.readyState === 'open') {
398-
waitForUpdateEnd(() => {
403+
_waitForUpdateEnd(() => {
399404
buffer.abort();
400405
resolve();
401406
});
@@ -412,36 +417,42 @@ function SourceBufferSink(config) {
412417

413418
}
414419

415-
function executeCallback() {
420+
function _executeCallback() {
416421
if (callbacks.length > 0) {
417422
if (!buffer.updating) {
418423
const cb = callbacks.shift();
419424
cb();
420425
// Try to execute next callback if still not updating
421-
executeCallback();
426+
_executeCallback();
422427
}
423428
}
424429
}
425430

426-
function updateEndHandler() {
431+
function _updateEndHandler() {
427432
// if updating is still in progress do nothing and wait for the next check again.
428433
if (buffer.updating) {
429434
return;
430435
}
431436

432437
// updating is completed, now we can stop checking and resolve the promise
433-
executeCallback();
438+
_executeCallback();
434439
}
435440

436-
function errHandler() {
437-
logger.error('SourceBufferSink error');
441+
function _errHandler(e) {
442+
const error = e.target || {};
443+
_triggerEvent(Events.SOURCE_BUFFER_ERROR, { error, lastRequestAppended })
438444
}
439445

440-
function waitForUpdateEnd(callback) {
446+
function _triggerEvent(eventType, data) {
447+
let payload = data || {};
448+
eventBus.trigger(eventType, payload, { streamId: mediaInfo.streamInfo.id, mediaType: type });
449+
}
450+
451+
function _waitForUpdateEnd(callback) {
441452
callbacks.push(callback);
442453

443454
if (!buffer.updating) {
444-
executeCallback();
455+
_executeCallback();
445456
}
446457
}
447458

@@ -454,7 +465,6 @@ function SourceBufferSink(config) {
454465
abort,
455466
reset,
456467
updateTimestampOffset,
457-
waitForUpdateEnd,
458468
initializeForStreamSwitch,
459469
initializeForFirstUse,
460470
updateAppendWindow,

src/streaming/Stream.js

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import FactoryMaker from '../core/FactoryMaker';
4141
import DashJSError from './vo/DashJSError';
4242
import BoxParser from './utils/BoxParser';
4343
import URLUtils from './utils/URLUtils';
44+
import BlacklistController from './controllers/BlacklistController';
4445

4546

4647
const MEDIA_TYPES = [Constants.VIDEO, Constants.AUDIO, Constants.TEXT, Constants.MUXED, Constants.IMAGE];
@@ -84,6 +85,7 @@ function Stream(config) {
8485
isUpdating,
8586
fragmentController,
8687
thumbnailController,
88+
segmentBlacklistController,
8789
preloaded,
8890
boxParser,
8991
debug,
@@ -101,6 +103,11 @@ function Stream(config) {
101103

102104
boxParser = BoxParser(context).getInstance();
103105

106+
segmentBlacklistController = BlacklistController(context).create({
107+
updateEventName: Events.SEGMENT_LOCATION_BLACKLIST_CHANGED,
108+
addBlacklistEventName: Events.SEGMENT_LOCATION_BLACKLIST_ADD
109+
});
110+
104111
fragmentController = FragmentController(context).create({
105112
streamInfo: streamInfo,
106113
mediaPlayerModel: mediaPlayerModel,
@@ -430,24 +437,25 @@ function Stream(config) {
430437
const isFragmented = mediaInfo ? mediaInfo.isFragmented : null;
431438

432439
let streamProcessor = StreamProcessor(context).create({
433-
streamInfo: streamInfo,
434-
type: type,
435-
mimeType: mimeType,
436-
timelineConverter: timelineConverter,
437-
adapter: adapter,
438-
manifestModel: manifestModel,
439-
mediaPlayerModel: mediaPlayerModel,
440-
fragmentModel: fragmentModel,
440+
streamInfo,
441+
type,
442+
mimeType,
443+
timelineConverter,
444+
adapter,
445+
manifestModel,
446+
mediaPlayerModel,
447+
fragmentModel,
441448
dashMetrics: config.dashMetrics,
442449
baseURLController: config.baseURLController,
443450
segmentBaseController: config.segmentBaseController,
444-
abrController: abrController,
445-
playbackController: playbackController,
446-
mediaController: mediaController,
447-
textController: textController,
448-
errHandler: errHandler,
449-
settings: settings,
450-
boxParser: boxParser
451+
abrController,
452+
playbackController,
453+
mediaController,
454+
textController,
455+
errHandler,
456+
settings,
457+
boxParser,
458+
segmentBlacklistController
451459
});
452460

453461
streamProcessor.initialize(mediaSource, hasVideoTrack, isFragmented);
@@ -560,6 +568,11 @@ function Stream(config) {
560568
abrController.clearDataForStream(streamInfo.id);
561569
}
562570

571+
if (segmentBlacklistController) {
572+
segmentBlacklistController.reset();
573+
segmentBlacklistController = null;
574+
}
575+
563576
resetInitialSettings(keepBuffers);
564577

565578
streamInfo = null;

0 commit comments

Comments
 (0)