Skip to content

Commit 757de7d

Browse files
authored
Fix/media fragment needed (#3772)
* WiP: Fix seek into gaps * Add settings parameter to enable/disable seekGapFix * WiP: Unit tests * WiP: Optimizations for getValidSeekTimeCloseToTargetTime using unit tests * Optimizations for getValidSeekTimeCloseToTargetTime using unit tests * Deactivate seek gap fix by default * Increase gap seek threshold * Disable seekGapFix
1 parent d05d1ca commit 757de7d

File tree

5 files changed

+429
-51
lines changed

5 files changed

+429
-51
lines changed

index.d.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,10 @@ declare namespace dashjs {
178178
},
179179
buffer?: {
180180
enableSeekDecorrelationFix?: boolean,
181+
seekGapFix?: {
182+
enabled?: boolean,
183+
threshold?: number
184+
},
181185
fastSwitchEnabled?: boolean,
182186
flushBufferAtTrackSwitch?: boolean,
183187
reuseExistingSourceBuffers?: boolean,
@@ -1479,7 +1483,12 @@ declare namespace dashjs {
14791483

14801484
export type MetricType = 'ManifestUpdate' | 'RequestsQueue';
14811485
export type TrackSwitchMode = 'alwaysReplace' | 'neverReplace';
1482-
export type TrackSelectionMode = 'highestSelectionPriority' | 'highestBitrate' | 'firstTrack' | 'highestEfficiency' | 'widestRange';
1486+
export type TrackSelectionMode =
1487+
'highestSelectionPriority'
1488+
| 'highestBitrate'
1489+
| 'firstTrack'
1490+
| 'highestEfficiency'
1491+
| 'widestRange';
14831492

14841493
export function supportsMediaSource(): boolean;
14851494

src/core/Settings.js

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,11 @@ import {HTTPRequest} from '../streaming/vo/metrics/HTTPRequest';
8585
* keepProtectionMediaKeys: false
8686
* },
8787
* buffer: {
88-
enableSeekDecorrelationFix: true,
88+
* enableSeekDecorrelationFix: true,
89+
* seekGapFix: {
90+
* enabled: false,
91+
* threshold: 1
92+
* },
8993
* fastSwitchEnabled: true,
9094
* flushBufferAtTrackSwitch: false,
9195
* reuseExistingSourceBuffers: true,
@@ -241,11 +245,14 @@ import {HTTPRequest} from '../streaming/vo/metrics/HTTPRequest';
241245

242246
/**
243247
* @typedef {Object} Buffer
244-
* @property {boolean} [enableSeekDecorrelationFix=true]
248+
* @property {boolean} [enableSeekDecorrelationFix=false]
245249
* Enables a workaround for playback start on some devices, e.g. WebOS 4.9.
246250
* It is necessary because some browsers do not support setting currentTime on video element to a value that is outside of current buffer.
247251
*
248252
* If you experience unexpected seeking triggered by BufferController, you can try setting this value to false.
253+
* @property {object} [seekGapFix={enabled=true,threshold=1}]
254+
* Enables the adjustment of the seek target once no valid segment request could be generated for a specific seek time. This can happen if the user seeks to a position for which there is a gap in the timeline.
255+
*
249256
* @property {boolean} [fastSwitchEnabled=true]
250257
* When enabled, after an ABR up-switch in quality, instead of requesting and appending the next fragment at the end of the current buffer range it is requested and appended closer to the current time.
251258
*
@@ -773,6 +780,10 @@ function Settings() {
773780
},
774781
buffer: {
775782
enableSeekDecorrelationFix: false,
783+
seekGapFix: {
784+
enabled: false,
785+
threshold: 1
786+
},
776787
fastSwitchEnabled: true,
777788
flushBufferAtTrackSwitch: false,
778789
reuseExistingSourceBuffers: true,

src/dash/DashHandler.js

Lines changed: 87 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ import {
4040
import DashConstants from './constants/DashConstants';
4141

4242

43+
const DEFAULT_ADJUST_SEEK_TIME_THRESHOLD = 0.5;
44+
45+
4346
function DashHandler(config) {
4447

4548
config = config || {};
@@ -296,6 +299,88 @@ function DashHandler(config) {
296299
return request;
297300
}
298301

302+
/**
303+
* This function returns a time for which we can generate a request. It is supposed to be as close as possible to the target time.
304+
* This is useful in scenarios in which the user seeks into a gap. We will not find a valid request then and need to adjust the seektime.
305+
* @param {number} time
306+
* @param {object} mediaInfo
307+
* @param {object} representation
308+
* @param {number} targetThreshold
309+
*/
310+
function getValidSeekTimeCloseToTargetTime(time, mediaInfo, representation, targetThreshold) {
311+
try {
312+
313+
if (isNaN(time) || !mediaInfo || !representation) {
314+
return NaN;
315+
}
316+
317+
if (time < 0) {
318+
time = 0;
319+
}
320+
321+
if (isNaN(targetThreshold)) {
322+
targetThreshold = DEFAULT_ADJUST_SEEK_TIME_THRESHOLD;
323+
}
324+
325+
if (getSegmentRequestForTime(mediaInfo, representation, time)) {
326+
return time;
327+
}
328+
329+
const start = representation.adaptation.period.start;
330+
const end = representation.adaptation.period.start + representation.adaptation.period.duration;
331+
let currentUpperTime = Math.min(time + targetThreshold, end);
332+
let currentLowerTime = Math.max(time - targetThreshold, start);
333+
let adjustedTime = NaN;
334+
let targetRequest = null;
335+
336+
while (currentUpperTime <= end || currentLowerTime >= start) {
337+
let upperRequest = null;
338+
let lowerRequest = null;
339+
if (currentUpperTime <= end) {
340+
upperRequest = getSegmentRequestForTime(mediaInfo, representation, currentUpperTime);
341+
}
342+
if (currentLowerTime >= start) {
343+
lowerRequest = getSegmentRequestForTime(mediaInfo, representation, currentLowerTime);
344+
}
345+
346+
if (lowerRequest) {
347+
adjustedTime = currentLowerTime;
348+
targetRequest = lowerRequest;
349+
break;
350+
} else if (upperRequest) {
351+
adjustedTime = currentUpperTime;
352+
targetRequest = upperRequest;
353+
break;
354+
}
355+
356+
currentUpperTime += targetThreshold;
357+
currentLowerTime -= targetThreshold;
358+
}
359+
360+
if (targetRequest) {
361+
const requestEndTime = targetRequest.startTime + targetRequest.duration;
362+
363+
// Keep the original start time in case it is covered by a segment
364+
if (time >= targetRequest.startTime && requestEndTime - time > targetThreshold) {
365+
return time;
366+
}
367+
368+
// If target time is before the start of the request use request starttime
369+
if (time < targetRequest.startTime) {
370+
return targetRequest.startTime;
371+
}
372+
373+
return Math.min(requestEndTime - targetThreshold, adjustedTime);
374+
}
375+
376+
return adjustedTime;
377+
378+
379+
} catch (e) {
380+
return NaN;
381+
}
382+
}
383+
299384
function getCurrentIndex() {
300385
return lastSegment ? lastSegment.index : -1;
301386
}
@@ -316,7 +401,8 @@ function DashHandler(config) {
316401
getNextSegmentRequest,
317402
isLastSegmentRequested,
318403
reset,
319-
getNextSegmentRequestIdempotent
404+
getNextSegmentRequestIdempotent,
405+
getValidSeekTimeCloseToTargetTime
320406
};
321407

322408
setup();

src/streaming/StreamProcessor.js

Lines changed: 63 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -380,7 +380,7 @@ function StreamProcessor(config) {
380380
return;
381381
}
382382
// Init segment not in cache, send new request
383-
const request = dashHandler ? dashHandler.getInitRequest(getMediaInfo(), rep) : null;
383+
const request = dashHandler ? dashHandler.getInitRequest(mediaInfo, rep) : null;
384384
if (request) {
385385
fragmentModel.executeRequest(request);
386386
} else if (rescheduleIfNoRequest) {
@@ -397,58 +397,75 @@ function StreamProcessor(config) {
397397
* @private
398398
*/
399399
function _onMediaFragmentNeeded(e, rescheduleIfNoRequest = true) {
400-
401-
if (manifestUpdateInProgress) {
400+
// Don't schedule next fragments while updating manifest or pruning to avoid buffer inconsistencies
401+
if (manifestUpdateInProgress || bufferController.getIsPruningInProgress()) {
402402
_noValidRequest();
403403
return;
404404
}
405405

406-
let request = null;
407-
408-
// Don't schedule next fragments while pruning to avoid buffer inconsistencies
409-
if (!bufferController.getIsPruningInProgress()) {
410-
request = _getFragmentRequest();
411-
if (request) {
412-
shouldUseExplicitTimeForRequest = false;
413-
if (!isNaN(request.startTime + request.duration)) {
414-
bufferingTime = request.startTime + request.duration;
415-
}
416-
request.delayLoadingTime = new Date().getTime() + scheduleController.getTimeToLoadDelay();
417-
scheduleController.setTimeToLoadDelay(0);
418-
}
406+
let request = _getFragmentRequest();
407+
if (request) {
408+
shouldUseExplicitTimeForRequest = false;
409+
_mediaRequestGenerated(request);
410+
} else {
411+
_noMediaRequestGenerated(rescheduleIfNoRequest);
419412
}
413+
}
420414

421-
if (request) {
422-
if (!_shouldIgnoreRequest(request)) {
423-
logger.debug(`Next fragment request url for stream id ${streamInfo.id} and media type ${type} is ${request.url}`);
424-
fragmentModel.executeRequest(request);
425-
} else {
426-
logger.warn(`Fragment request url ${request.url} for stream id ${streamInfo.id} and media type ${type} is on the ignore list and will be skipped`);
427-
_noValidRequest();
428-
}
415+
/**
416+
* If we generated a valid media request we can execute the request. In some cases the segment might be blacklisted.
417+
* @param {object} request
418+
* @private
419+
*/
420+
function _mediaRequestGenerated(request) {
421+
if (!isNaN(request.startTime + request.duration)) {
422+
bufferingTime = request.startTime + request.duration;
429423
}
430-
else {
431-
// Check if the media is finished. If so, no need to schedule another request
432-
const representation = representationController.getCurrentRepresentation();
433-
const isLastSegmentRequested = dashHandler.isLastSegmentRequested(representation, bufferingTime);
434-
435-
if (isLastSegmentRequested) {
436-
const segmentIndex = dashHandler.getCurrentIndex();
437-
logger.debug(`Segment requesting for stream ${streamInfo.id} has finished`);
438-
eventBus.trigger(Events.STREAM_REQUESTING_COMPLETED, { segmentIndex }, {
439-
streamId: streamInfo.id,
440-
mediaType: type
441-
});
442-
bufferController.segmentRequestingCompleted(segmentIndex);
443-
scheduleController.clearScheduleTimer();
424+
request.delayLoadingTime = new Date().getTime() + scheduleController.getTimeToLoadDelay();
425+
scheduleController.setTimeToLoadDelay(0);
426+
if (!_shouldIgnoreRequest(request)) {
427+
logger.debug(`Next fragment request url for stream id ${streamInfo.id} and media type ${type} is ${request.url}`);
428+
fragmentModel.executeRequest(request);
429+
} else {
430+
logger.warn(`Fragment request url ${request.url} for stream id ${streamInfo.id} and media type ${type} is on the ignore list and will be skipped`);
431+
_noValidRequest();
432+
}
433+
}
434+
435+
/**
436+
* We could not generate a valid request. Check if the media is finished, we are stuck in a gap or simply need to wait for the next segment to be available.
437+
* @param {boolean} rescheduleIfNoRequest
438+
* @private
439+
*/
440+
function _noMediaRequestGenerated(rescheduleIfNoRequest) {
441+
const representation = representationController.getCurrentRepresentation();
442+
443+
// If this statement is true we are stuck. A static manifest does not change and we did not find a valid request for the target time
444+
// There is no point in trying again. We need to adjust the time in order to find a valid request. This can happen if the user/app seeked into a gap.
445+
if (settings.get().streaming.buffer.seekGapFix.enabled && !isDynamic && shouldUseExplicitTimeForRequest && playbackController.isSeeking()) {
446+
const adjustedTime = dashHandler.getValidSeekTimeCloseToTargetTime(bufferingTime, mediaInfo, representation, settings.get().streaming.buffer.seekGapFix.threshold);
447+
if (!isNaN(adjustedTime)) {
448+
playbackController.seek(adjustedTime, false, false);
444449
return;
445450
}
451+
}
446452

447-
// Reschedule
448-
if (rescheduleIfNoRequest) {
449-
// Use case - Playing at the bleeding live edge and frag is not available yet. Cycle back around.
450-
_noValidRequest();
451-
}
453+
// Check if the media is finished. If so, no need to schedule another request
454+
const isLastSegmentRequested = dashHandler.isLastSegmentRequested(representation, bufferingTime);
455+
if (isLastSegmentRequested) {
456+
const segmentIndex = dashHandler.getCurrentIndex();
457+
logger.debug(`Segment requesting for stream ${streamInfo.id} has finished`);
458+
eventBus.trigger(Events.STREAM_REQUESTING_COMPLETED, { segmentIndex }, {
459+
streamId: streamInfo.id,
460+
mediaType: type
461+
});
462+
bufferController.segmentRequestingCompleted(segmentIndex);
463+
scheduleController.clearScheduleTimer();
464+
return;
465+
}
466+
467+
if (rescheduleIfNoRequest) {
468+
_noValidRequest();
452469
}
453470
}
454471

@@ -486,9 +503,9 @@ function StreamProcessor(config) {
486503
const representation = representationController && representationInfo ? representationController.getRepresentationForQuality(representationInfo.quality) : null;
487504

488505
if (useTime) {
489-
request = dashHandler.getSegmentRequestForTime(getMediaInfo(), representation, bufferingTime);
506+
request = dashHandler.getSegmentRequestForTime(mediaInfo, representation, bufferingTime);
490507
} else {
491-
request = dashHandler.getNextSegmentRequest(getMediaInfo(), representation);
508+
request = dashHandler.getNextSegmentRequest(mediaInfo, representation);
492509
}
493510
}
494511

@@ -918,7 +935,7 @@ function StreamProcessor(config) {
918935
representationController.getRepresentationForQuality(representationInfo.quality) : null;
919936

920937
let request = dashHandler.getNextSegmentRequestIdempotent(
921-
getMediaInfo(),
938+
mediaInfo,
922939
representation
923940
);
924941

0 commit comments

Comments
 (0)