Skip to content

Commit

Permalink
Feature/dash handler optimizations (#3727)
Browse files Browse the repository at this point in the history
* WiP: Optimize the DashHandler.js related functionality

* WiP: Optimizations to decide whether a period is completely buffered

* WiP: Optimizations to decide whether a period is completely buffered

* Fix unit tests

* Refactor to numberOfSegments and account for -1 based index

* Add comment to DashHandler.js

* Minor changes to DashHandler.js

* Refactor TimelineSegmentsGetter.js

* WiP: Rewrite DashHandler.js

* Add mediaFinishedInformation to decide whether all segments of a period have been requested

* Add a safety margin of 0.05 to account for rounding issues when comparing media times
  • Loading branch information
dsilhavy authored Aug 17, 2021
1 parent 339f953 commit d39b56b
Show file tree
Hide file tree
Showing 22 changed files with 332 additions and 208 deletions.
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src/core/Settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ import {HTTPRequest} from '../streaming/vo/metrics/HTTPRequest';

/**
* @typedef {Object} Buffer
* @property {boolean} [fastSwitchEnabled=false]
* @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.
*
* When enabled, The maximum time to render a higher quality is current time + (1.5 * fragment duration).
Expand Down
138 changes: 63 additions & 75 deletions src/dash/DashHandler.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import {
replaceTokenForTemplate,
unescapeDollarsInTemplate
} from './utils/SegmentsUtils';
import DashConstants from './constants/DashConstants';


function DashHandler(config) {
Expand All @@ -54,22 +55,20 @@ function DashHandler(config) {

let instance,
logger,
segmentIndex,
lastSegment,
requestedTime,
isDynamicManifest,
dynamicStreamCompleted;
mediaHasFinished;

function setup() {
logger = debug.getLogger(instance);
resetInitialSettings();

eventBus.on(MediaPlayerEvents.DYNAMIC_TO_STATIC, onDynamicToStatic, instance);
eventBus.on(MediaPlayerEvents.DYNAMIC_TO_STATIC, _onDynamicToStatic, instance);
}

function initialize(isDynamic) {
isDynamicManifest = isDynamic;
dynamicStreamCompleted = false;
mediaHasFinished = false;
segmentsController.initialize(isDynamic);
}

Expand All @@ -85,30 +84,16 @@ function DashHandler(config) {
return streamInfo;
}

function setCurrentIndex(value) {
segmentIndex = value;
}

function getCurrentIndex() {
return segmentIndex;
}

function resetIndex() {
segmentIndex = -1;
lastSegment = null;
}

function resetInitialSettings() {
resetIndex();
requestedTime = null;
lastSegment = null;
}

function reset() {
resetInitialSettings();
eventBus.off(MediaPlayerEvents.DYNAMIC_TO_STATIC, onDynamicToStatic, instance);
eventBus.off(MediaPlayerEvents.DYNAMIC_TO_STATIC, _onDynamicToStatic, instance);
}

function setRequestUrl(request, destination, representation) {
function _setRequestUrl(request, destination, representation) {
const baseURL = baseURLController.resolve(representation.path);
let url,
serviceLocation;
Expand Down Expand Up @@ -153,7 +138,7 @@ function DashHandler(config) {
request.mediaInfo = mediaInfo;
request.representationId = representation.id;

if (setRequestUrl(request, representation.initialization, representation)) {
if (_setRequestUrl(request, representation.initialization, representation)) {
request.url = replaceTokenForTemplate(request.url, 'Bandwidth', representation.bandwidth);
return request;
}
Expand Down Expand Up @@ -186,59 +171,72 @@ function DashHandler(config) {
request.availabilityEndTime = segment.availabilityEndTime;
request.wallStartTime = segment.wallStartTime;
request.quality = representation.index;
request.index = segment.availabilityIdx;
request.index = segment.index;
request.mediaInfo = mediaInfo;
request.adaptationIndex = representation.adaptation.index;
request.representationId = representation.id;

if (setRequestUrl(request, url, representation)) {
if (_setRequestUrl(request, url, representation)) {
return request;
}
}

function isMediaFinished(representation, bufferingTime) {
let isFinished = false;
function lastSegmentRequested(representation, bufferingTime) {
if (!representation || !lastSegment) {
return false;
}

if (!representation || !lastSegment) return isFinished;
// Either transition from dynamic to static was done or no next static segment found
if (mediaHasFinished) {
return true;
}

// if the buffer is filled up we are done
// Period is endless
if (!isFinite(representation.adaptation.period.duration)) {
return false;
}

// we are replacing existing stuff.
// we are replacing existing stuff in the buffer for instance after a track switch
if (lastSegment.presentationStartTime + lastSegment.duration > bufferingTime) {
return false;
}


if (isDynamicManifest && dynamicStreamCompleted) {
isFinished = true;
} else if (lastSegment) {
const time = parseFloat((lastSegment.presentationStartTime - representation.adaptation.period.start).toFixed(5));
const endTime = lastSegment.duration > 0 ? time + lastSegment.duration : time;
const duration = representation.adaptation.period.duration;

return isFinite(duration) && endTime >= duration - 0.05;
// Additional segment references may be added to the last period.
// Additional periods may be added to the end of the MPD.
// Segment references SHALL NOT be added to any period other than the last period.
// An MPD update MAY combine adding segment references to the last period with adding of new periods. An MPD update that adds content MAY be combined with an MPD update that removes content.
// The index of the last requested segment is higher than the number of available segments.
// For SegmentTimeline and SegmentTemplate the index does not include the startNumber.
// For SegmentList the index includes the startnumber which is why the numberOfSegments includes this as well
if (representation.mediaFinishedInformation && !isNaN(representation.mediaFinishedInformation.numberOfSegments) && !isNaN(lastSegment.index) && lastSegment.index >= (representation.mediaFinishedInformation.numberOfSegments - 1)) {
// For static manifests and Template addressing we can compare the index against the number of available segments
if (!isDynamicManifest || representation.segmentInfoType === DashConstants.SEGMENT_TEMPLATE) {
return true;
}
// For SegmentList we need to check if the next period is signaled
else if (isDynamicManifest && representation.segmentInfoType === DashConstants.SEGMENT_LIST && representation.adaptation.period.nextPeriodId) {
return true
}
}

return isFinished;
// For dynamic SegmentTimeline manifests we need to check if the next period is already signaled and the segment we fetched before is the last one that is signaled.
// We can not simply use the index, as numberOfSegments might have decreased after an MPD update
return !!(isDynamicManifest && representation.adaptation.period.nextPeriodId && representation.segmentInfoType === DashConstants.SEGMENT_TIMELINE && representation.mediaFinishedInformation &&
!isNaN(representation.mediaFinishedInformation.mediaTimeOfLastSignaledSegment) && lastSegment && !isNaN(lastSegment.mediaStartTime) && !isNaN(lastSegment.duration) && lastSegment.mediaStartTime + lastSegment.duration >= (representation.mediaFinishedInformation.mediaTimeOfLastSignaledSegment - 0.05));
}


function getSegmentRequestForTime(mediaInfo, representation, time) {
let request = null;

if (!representation || !representation.segmentInfoType) {
return request;
}

if (requestedTime !== time) { // When playing at live edge with 0 delay we may loop back with same time and index until it is available. Reduces verboseness of logs.
requestedTime = time;
logger.debug('Getting the request for time : ' + time);
}

const segment = segmentsController.getSegmentByTime(representation, time);
if (segment) {
segmentIndex = segment.availabilityIdx;
lastSegment = segment;
logger.debug('Index for time ' + time + ' is ' + segmentIndex);
logger.debug('Index for time ' + time + ' is ' + segment.index);
request = _getRequestForSegment(mediaInfo, segment);
}

Expand All @@ -253,7 +251,7 @@ function DashHandler(config) {
*/
function getNextSegmentRequestIdempotent(mediaInfo, representation) {
let request = null;
let indexToRequest = segmentIndex + 1;
let indexToRequest = lastSegment ? lastSegment.index + 1 : 0;
const segment = segmentsController.getSegmentByIndex(
representation,
indexToRequest,
Expand All @@ -277,42 +275,34 @@ function DashHandler(config) {
return null;
}

requestedTime = null;
let indexToRequest = lastSegment ? lastSegment.index + 1 : 0;

let indexToRequest = segmentIndex + 1;

// check that there is a segment in this index
const segment = segmentsController.getSegmentByIndex(representation, indexToRequest, lastSegment ? lastSegment.mediaStartTime : -1);
if (!segment && isEndlessMedia(representation) && !dynamicStreamCompleted) {
logger.debug(getType() + ' No segment found at index: ' + indexToRequest + '. Wait for next loop');
return null;
} else {
if (segment) {
request = _getRequestForSegment(mediaInfo, segment);
segmentIndex = segment.availabilityIdx;

// No segment found
if (!segment) {
// Dynamic manifest there might be something available in the next iteration
if (isDynamicManifest && !mediaHasFinished) {
logger.debug(getType() + ' No segment found at index: ' + indexToRequest + '. Wait for next loop');
return null;
} else {
if (isDynamicManifest) {
segmentIndex = indexToRequest - 1;
} else {
segmentIndex = indexToRequest;
}
mediaHasFinished = true;
}
}

if (segment) {
} else {
request = _getRequestForSegment(mediaInfo, segment);
lastSegment = segment;
}

return request;
}

function isEndlessMedia(representation) {
return !isFinite(representation.adaptation.period.duration);
function getCurrentIndex() {
return lastSegment ? lastSegment.index : -1;
}

function onDynamicToStatic() {
function _onDynamicToStatic() {
logger.debug('Dynamic stream complete');
dynamicStreamCompleted = true;
mediaHasFinished = true;
}

instance = {
Expand All @@ -322,12 +312,10 @@ function DashHandler(config) {
getStreamInfo,
getInitRequest,
getSegmentRequestForTime,
getNextSegmentRequest,
setCurrentIndex,
getCurrentIndex,
isMediaFinished,
getNextSegmentRequest,
lastSegmentRequested,
reset,
resetIndex,
getNextSegmentRequestIdempotent
};

Expand Down
6 changes: 5 additions & 1 deletion src/dash/controllers/RepresentationController.js
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ function RepresentationController(config) {
if (data[1] && !data[1].error) {
currentRep = _onSegmentsLoaded(currentRep, data[1]);
}
_setMediaFinishedInformation(currentRep);
_onRepresentationUpdated(currentRep);
resolve();
})
Expand All @@ -151,6 +152,10 @@ function RepresentationController(config) {
});
}

function _setMediaFinishedInformation(representation) {
representation.mediaFinishedInformation = segmentsController.getMediaFinishedInformation(representation);
}

function _onInitLoaded(representation, e) {
if (!e || e.error || !e.representation) {
return representation;
Expand Down Expand Up @@ -192,7 +197,6 @@ function RepresentationController(config) {
}

if (segments.length > 0) {
representation.availableSegmentsNumber = segments.length;
representation.segments = segments;
}

Expand Down
11 changes: 10 additions & 1 deletion src/dash/controllers/SegmentsController.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,12 +91,21 @@ function SegmentsController(config) {
return getter ? getter.getSegmentByTime(representation, time) : null;
}

function getMediaFinishedInformation(representation) {
const getter = getSegmentsGetter(representation);
return getter ? getter.getMediaFinishedInformation(representation) : {
numberOfSegments: 0,
mediaTimeOfLastSignaledSegment: NaN
};
}

instance = {
initialize,
updateInitData,
updateSegmentData,
getSegmentByIndex,
getSegmentByTime
getSegmentByTime,
getMediaFinishedInformation
};

setup();
Expand Down
7 changes: 7 additions & 0 deletions src/dash/models/DashManifestModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -582,6 +582,9 @@ function DashManifestModel() {
}

function calcSegmentDuration(segmentTimeline) {
if (!segmentTimeline || !segmentTimeline.S_asArray) {
return NaN;
}
let s0 = segmentTimeline.S_asArray[0];
let s1 = segmentTimeline.S_asArray[1];
return s0.hasOwnProperty('d') ? s0.d : (s1.t - s0.t);
Expand Down Expand Up @@ -689,6 +692,10 @@ function DashManifestModel() {
voPeriod.duration = realPeriod.duration;
}

if (voPreviousPeriod) {
voPreviousPeriod.nextPeriodId = voPeriod.id;
}

voPeriods.push(voPeriod);
realPreviousPeriod = realPeriod;
voPreviousPeriod = voPeriod;
Expand Down
24 changes: 19 additions & 5 deletions src/dash/utils/ListSegmentsGetter.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,22 @@ function ListSegmentsGetter(config, isDynamic) {
}
}

function getMediaFinishedInformation(representation) {
const mediaFinishedInformation = { numberOfSegments: 0, mediaTimeOfLastSignaledSegment: NaN }

if (!representation) {
return mediaFinishedInformation;
}

const list = representation.adaptation.period.mpd.manifest.Period_asArray[representation.adaptation.period.index].AdaptationSet_asArray[representation.adaptation.index].Representation_asArray[representation.index].SegmentList;
const startNumber = representation && !isNaN(representation.startNumber) ? representation.startNumber : 1;
const offset = Math.max(startNumber - 1, 0);

mediaFinishedInformation.numberOfSegments = offset + list.SegmentURL_asArray.length;

return mediaFinishedInformation
}

function getSegmentByIndex(representation, index) {
checkConfig();

Expand All @@ -71,13 +87,10 @@ function ListSegmentsGetter(config, isDynamic) {
segment.replacementTime = (startNumber + index - 1) * representation.segmentDuration;
segment.media = s.media ? s.media : '';
segment.mediaRange = s.mediaRange;
segment.index = index;
segment.indexRange = s.indexRange;
}
}

representation.availableSegmentsNumber = len;

return segment;
}

Expand All @@ -101,8 +114,9 @@ function ListSegmentsGetter(config, isDynamic) {
}

instance = {
getSegmentByIndex: getSegmentByIndex,
getSegmentByTime: getSegmentByTime
getSegmentByIndex,
getSegmentByTime,
getMediaFinishedInformation
};

return instance;
Expand Down
Loading

0 comments on commit d39b56b

Please sign in to comment.