Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix/media fragment needed #3772

Merged
merged 9 commits into from
Sep 20, 2021
11 changes: 10 additions & 1 deletion index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,10 @@ declare namespace dashjs {
},
buffer?: {
enableSeekDecorrelationFix?: boolean,
seekGapFix?: {
enabled?: boolean,
threshold?: number
},
fastSwitchEnabled?: boolean,
flushBufferAtTrackSwitch?: boolean,
reuseExistingSourceBuffers?: boolean,
Expand Down Expand Up @@ -1479,7 +1483,12 @@ declare namespace dashjs {

export type MetricType = 'ManifestUpdate' | 'RequestsQueue';
export type TrackSwitchMode = 'alwaysReplace' | 'neverReplace';
export type TrackSelectionMode = 'highestSelectionPriority' | 'highestBitrate' | 'firstTrack' | 'highestEfficiency' | 'widestRange';
export type TrackSelectionMode =
'highestSelectionPriority'
| 'highestBitrate'
| 'firstTrack'
| 'highestEfficiency'
| 'widestRange';

export function supportsMediaSource(): boolean;

Expand Down
15 changes: 13 additions & 2 deletions src/core/Settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,11 @@ import {HTTPRequest} from '../streaming/vo/metrics/HTTPRequest';
* keepProtectionMediaKeys: false
* },
* buffer: {
enableSeekDecorrelationFix: true,
* enableSeekDecorrelationFix: true,
* seekGapFix: {
* enabled: false,
* threshold: 1
* },
* fastSwitchEnabled: true,
* flushBufferAtTrackSwitch: false,
* reuseExistingSourceBuffers: true,
Expand Down Expand Up @@ -241,11 +245,14 @@ import {HTTPRequest} from '../streaming/vo/metrics/HTTPRequest';

/**
* @typedef {Object} Buffer
* @property {boolean} [enableSeekDecorrelationFix=true]
* @property {boolean} [enableSeekDecorrelationFix=false]
* Enables a workaround for playback start on some devices, e.g. WebOS 4.9.
* It is necessary because some browsers do not support setting currentTime on video element to a value that is outside of current buffer.
*
* If you experience unexpected seeking triggered by BufferController, you can try setting this value to false.
* @property {object} [seekGapFix={enabled=true,threshold=1}]
* 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.
*
* @property {boolean} [fastSwitchEnabled=true]
* 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.
*
Expand Down Expand Up @@ -773,6 +780,10 @@ function Settings() {
},
buffer: {
enableSeekDecorrelationFix: false,
seekGapFix: {
enabled: false,
threshold: 1
},
fastSwitchEnabled: true,
flushBufferAtTrackSwitch: false,
reuseExistingSourceBuffers: true,
Expand Down
88 changes: 87 additions & 1 deletion src/dash/DashHandler.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ import {
import DashConstants from './constants/DashConstants';


const DEFAULT_ADJUST_SEEK_TIME_THRESHOLD = 0.5;


function DashHandler(config) {

config = config || {};
Expand Down Expand Up @@ -296,6 +299,88 @@ function DashHandler(config) {
return request;
}

/**
* 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.
* 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.
* @param {number} time
* @param {object} mediaInfo
* @param {object} representation
* @param {number} targetThreshold
*/
function getValidSeekTimeCloseToTargetTime(time, mediaInfo, representation, targetThreshold) {
try {

if (isNaN(time) || !mediaInfo || !representation) {
return NaN;
}

if (time < 0) {
time = 0;
}

if (isNaN(targetThreshold)) {
targetThreshold = DEFAULT_ADJUST_SEEK_TIME_THRESHOLD;
}

if (getSegmentRequestForTime(mediaInfo, representation, time)) {
return time;
}

const start = representation.adaptation.period.start;
const end = representation.adaptation.period.start + representation.adaptation.period.duration;
let currentUpperTime = Math.min(time + targetThreshold, end);
let currentLowerTime = Math.max(time - targetThreshold, start);
let adjustedTime = NaN;
let targetRequest = null;

while (currentUpperTime <= end || currentLowerTime >= start) {
let upperRequest = null;
let lowerRequest = null;
if (currentUpperTime <= end) {
upperRequest = getSegmentRequestForTime(mediaInfo, representation, currentUpperTime);
}
if (currentLowerTime >= start) {
lowerRequest = getSegmentRequestForTime(mediaInfo, representation, currentLowerTime);
}

if (lowerRequest) {
adjustedTime = currentLowerTime;
targetRequest = lowerRequest;
break;
} else if (upperRequest) {
adjustedTime = currentUpperTime;
targetRequest = upperRequest;
break;
}

currentUpperTime += targetThreshold;
currentLowerTime -= targetThreshold;
}

if (targetRequest) {
const requestEndTime = targetRequest.startTime + targetRequest.duration;

// Keep the original start time in case it is covered by a segment
if (time >= targetRequest.startTime && requestEndTime - time > targetThreshold) {
return time;
}

// If target time is before the start of the request use request starttime
if (time < targetRequest.startTime) {
return targetRequest.startTime;
}

return Math.min(requestEndTime - targetThreshold, adjustedTime);
}

return adjustedTime;


} catch (e) {
return NaN;
}
}

function getCurrentIndex() {
return lastSegment ? lastSegment.index : -1;
}
Expand All @@ -316,7 +401,8 @@ function DashHandler(config) {
getNextSegmentRequest,
isLastSegmentRequested,
reset,
getNextSegmentRequestIdempotent
getNextSegmentRequestIdempotent,
getValidSeekTimeCloseToTargetTime
};

setup();
Expand Down
109 changes: 63 additions & 46 deletions src/streaming/StreamProcessor.js
Original file line number Diff line number Diff line change
Expand Up @@ -380,7 +380,7 @@ function StreamProcessor(config) {
return;
}
// Init segment not in cache, send new request
const request = dashHandler ? dashHandler.getInitRequest(getMediaInfo(), rep) : null;
const request = dashHandler ? dashHandler.getInitRequest(mediaInfo, rep) : null;
if (request) {
fragmentModel.executeRequest(request);
} else if (rescheduleIfNoRequest) {
Expand All @@ -397,58 +397,75 @@ function StreamProcessor(config) {
* @private
*/
function _onMediaFragmentNeeded(e, rescheduleIfNoRequest = true) {

if (manifestUpdateInProgress) {
// Don't schedule next fragments while updating manifest or pruning to avoid buffer inconsistencies
if (manifestUpdateInProgress || bufferController.getIsPruningInProgress()) {
_noValidRequest();
return;
}

let request = null;

// Don't schedule next fragments while pruning to avoid buffer inconsistencies
if (!bufferController.getIsPruningInProgress()) {
request = _getFragmentRequest();
if (request) {
shouldUseExplicitTimeForRequest = false;
if (!isNaN(request.startTime + request.duration)) {
bufferingTime = request.startTime + request.duration;
}
request.delayLoadingTime = new Date().getTime() + scheduleController.getTimeToLoadDelay();
scheduleController.setTimeToLoadDelay(0);
}
let request = _getFragmentRequest();
if (request) {
shouldUseExplicitTimeForRequest = false;
_mediaRequestGenerated(request);
} else {
_noMediaRequestGenerated(rescheduleIfNoRequest);
}
}

if (request) {
if (!_shouldIgnoreRequest(request)) {
logger.debug(`Next fragment request url for stream id ${streamInfo.id} and media type ${type} is ${request.url}`);
fragmentModel.executeRequest(request);
} else {
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`);
_noValidRequest();
}
/**
* If we generated a valid media request we can execute the request. In some cases the segment might be blacklisted.
* @param {object} request
* @private
*/
function _mediaRequestGenerated(request) {
if (!isNaN(request.startTime + request.duration)) {
bufferingTime = request.startTime + request.duration;
}
else {
// Check if the media is finished. If so, no need to schedule another request
const representation = representationController.getCurrentRepresentation();
const isLastSegmentRequested = dashHandler.isLastSegmentRequested(representation, bufferingTime);

if (isLastSegmentRequested) {
const segmentIndex = dashHandler.getCurrentIndex();
logger.debug(`Segment requesting for stream ${streamInfo.id} has finished`);
eventBus.trigger(Events.STREAM_REQUESTING_COMPLETED, { segmentIndex }, {
streamId: streamInfo.id,
mediaType: type
});
bufferController.segmentRequestingCompleted(segmentIndex);
scheduleController.clearScheduleTimer();
request.delayLoadingTime = new Date().getTime() + scheduleController.getTimeToLoadDelay();
scheduleController.setTimeToLoadDelay(0);
if (!_shouldIgnoreRequest(request)) {
logger.debug(`Next fragment request url for stream id ${streamInfo.id} and media type ${type} is ${request.url}`);
fragmentModel.executeRequest(request);
} else {
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`);
_noValidRequest();
}
}

/**
* 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.
* @param {boolean} rescheduleIfNoRequest
* @private
*/
function _noMediaRequestGenerated(rescheduleIfNoRequest) {
const representation = representationController.getCurrentRepresentation();

// 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
// 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.
if (settings.get().streaming.buffer.seekGapFix.enabled && !isDynamic && shouldUseExplicitTimeForRequest && playbackController.isSeeking()) {
const adjustedTime = dashHandler.getValidSeekTimeCloseToTargetTime(bufferingTime, mediaInfo, representation, settings.get().streaming.buffer.seekGapFix.threshold);
if (!isNaN(adjustedTime)) {
playbackController.seek(adjustedTime, false, false);
return;
}
}

// Reschedule
if (rescheduleIfNoRequest) {
// Use case - Playing at the bleeding live edge and frag is not available yet. Cycle back around.
_noValidRequest();
}
// Check if the media is finished. If so, no need to schedule another request
const isLastSegmentRequested = dashHandler.isLastSegmentRequested(representation, bufferingTime);
if (isLastSegmentRequested) {
const segmentIndex = dashHandler.getCurrentIndex();
logger.debug(`Segment requesting for stream ${streamInfo.id} has finished`);
eventBus.trigger(Events.STREAM_REQUESTING_COMPLETED, { segmentIndex }, {
streamId: streamInfo.id,
mediaType: type
});
bufferController.segmentRequestingCompleted(segmentIndex);
scheduleController.clearScheduleTimer();
return;
}

if (rescheduleIfNoRequest) {
_noValidRequest();
}
}

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

if (useTime) {
request = dashHandler.getSegmentRequestForTime(getMediaInfo(), representation, bufferingTime);
request = dashHandler.getSegmentRequestForTime(mediaInfo, representation, bufferingTime);
} else {
request = dashHandler.getNextSegmentRequest(getMediaInfo(), representation);
request = dashHandler.getNextSegmentRequest(mediaInfo, representation);
}
}

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

let request = dashHandler.getNextSegmentRequestIdempotent(
getMediaInfo(),
mediaInfo,
representation
);

Expand Down
Loading