@l5i/dashjs
Version:
A reference client implementation for the playback of MPEG DASH via Javascript and compliant browsers.
286 lines (231 loc) • 11.6 kB
JavaScript
/**
* The copyright in this software is being made available under the BSD License,
* included below. This software may be subject to other third party and contributor
* rights, including patent rights, and no such rights are granted under this license.
*
* Copyright (c) 2013, Dash Industry Forum.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation and/or
* other materials provided with the distribution.
* * Neither the name of Dash Industry Forum nor the names of its
* contributors may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS AS IS AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
* IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
* INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
import Segment from './../vo/Segment';
function zeroPadToLength(numStr, minStrLength) {
while (numStr.length < minStrLength) {
numStr = '0' + numStr;
}
return numStr;
}
function getNumberForSegment(segment, segmentIndex) {
return segment.representation.startNumber + segmentIndex;
}
export function unescapeDollarsInTemplate(url) {
return url ? url.split('$$').join('$') : url;
}
export function replaceIDForTemplate(url, value) {
if (!value || !url || url.indexOf('$RepresentationID$') === -1) { return url; }
let v = value.toString();
return url.split('$RepresentationID$').join(v);
}
export function replaceTokenForTemplate(url, token, value) {
const formatTag = '%0';
let startPos,
endPos,
formatTagPos,
specifier,
width,
paddedValue;
const tokenLen = token.length;
const formatTagLen = formatTag.length;
if (!url) {
return url;
}
// keep looping round until all instances of <token> have been
// replaced. once that has happened, startPos below will be -1
// and the completed url will be returned.
while (true) {
// check if there is a valid $<token>...$ identifier
// if not, return the url as is.
startPos = url.indexOf('$' + token);
if (startPos < 0) {
return url;
}
// the next '$' must be the end of the identifier
// if there isn't one, return the url as is.
endPos = url.indexOf('$', startPos + tokenLen);
if (endPos < 0) {
return url;
}
// now see if there is an additional format tag suffixed to
// the identifier within the enclosing '$' characters
formatTagPos = url.indexOf(formatTag, startPos + tokenLen);
if (formatTagPos > startPos && formatTagPos < endPos) {
specifier = url.charAt(endPos - 1);
width = parseInt(url.substring(formatTagPos + formatTagLen, endPos - 1), 10);
// support the minimum specifiers required by IEEE 1003.1
// (d, i , o, u, x, and X) for completeness
switch (specifier) {
// treat all int types as uint,
// hence deliberate fallthrough
case 'd':
case 'i':
case 'u':
paddedValue = zeroPadToLength(value.toString(), width);
break;
case 'x':
paddedValue = zeroPadToLength(value.toString(16), width);
break;
case 'X':
paddedValue = zeroPadToLength(value.toString(16), width).toUpperCase();
break;
case 'o':
paddedValue = zeroPadToLength(value.toString(8), width);
break;
default:
return url;
}
} else {
paddedValue = value;
}
url = url.substring(0, startPos) + paddedValue + url.substring(endPos + 1);
}
}
export function getIndexBasedSegment(timelineConverter, isDynamic, representation, index) {
let seg,
duration,
presentationStartTime,
presentationEndTime;
duration = representation.segmentDuration;
/*
* From spec - If neither @duration attribute nor SegmentTimeline element is present, then the Representation
* shall contain exactly one Media Segment. The MPD start time is 0 and the MPD duration is obtained
* in the same way as for the last Media Segment in the Representation.
*/
if (isNaN(duration)) {
duration = representation.adaptation.period.duration;
}
presentationStartTime = parseFloat((representation.adaptation.period.start + (index * duration)).toFixed(5));
presentationEndTime = parseFloat((presentationStartTime + duration).toFixed(5));
seg = new Segment();
seg.representation = representation;
seg.duration = duration;
seg.presentationStartTime = presentationStartTime;
seg.mediaStartTime = timelineConverter.calcMediaTimeFromPresentationTime(seg.presentationStartTime, representation);
seg.availabilityStartTime = timelineConverter.calcAvailabilityStartTimeFromPresentationTime(seg.presentationStartTime, representation.adaptation.period.mpd, isDynamic);
seg.availabilityEndTime = timelineConverter.calcAvailabilityEndTimeFromPresentationTime(presentationEndTime, representation.adaptation.period.mpd, isDynamic);
// at this wall clock time, the video element currentTime should be seg.presentationStartTime
seg.wallStartTime = timelineConverter.calcWallTimeForSegment(seg, isDynamic);
seg.replacementNumber = getNumberForSegment(seg, index);
seg.availabilityIdx = index;
return seg;
}
export function getTimeBasedSegment(timelineConverter, isDynamic, representation, time, duration, fTimescale, url, range, index, tManifest) {
const scaledTime = time / fTimescale;
const scaledDuration = Math.min(duration / fTimescale, representation.adaptation.period.mpd.maxSegmentDuration);
let presentationStartTime,
presentationEndTime,
seg;
presentationStartTime = timelineConverter.calcPresentationTimeFromMediaTime(scaledTime, representation);
presentationEndTime = presentationStartTime + scaledDuration;
seg = new Segment();
seg.representation = representation;
seg.duration = scaledDuration;
seg.mediaStartTime = scaledTime;
seg.presentationStartTime = presentationStartTime;
// For SegmentTimeline every segment is available at loadedTime
seg.availabilityStartTime = representation.adaptation.period.mpd.manifest.loadedTime;
seg.availabilityEndTime = timelineConverter.calcAvailabilityEndTimeFromPresentationTime(presentationEndTime, representation.adaptation.period.mpd, isDynamic);
// at this wall clock time, the video element currentTime should be seg.presentationStartTime
seg.wallStartTime = timelineConverter.calcWallTimeForSegment(seg, isDynamic);
seg.replacementTime = tManifest ? tManifest : time;
seg.replacementNumber = getNumberForSegment(seg, index);
url = replaceTokenForTemplate(url, 'Number', seg.replacementNumber);
url = replaceTokenForTemplate(url, 'Time', seg.replacementTime);
seg.media = url;
seg.mediaRange = range;
seg.availabilityIdx = index;
return seg;
}
export function getSegmentByIndex(index, representation) {
if (!representation || !representation.segments) return null;
const ln = representation.segments.length;
let seg,
i;
if (index < ln) {
seg = representation.segments[index];
if (seg && seg.availabilityIdx === index) {
return seg;
}
}
for (i = 0; i < ln; i++) {
seg = representation.segments[i];
if (seg && seg.availabilityIdx === index) {
return seg;
}
}
return null;
}
export function decideSegmentListRangeForTemplate(timelineConverter, isDynamic, representation, requestedTime, index, givenAvailabilityUpperLimit) {
const duration = representation.segmentDuration;
const minBufferTime = representation.adaptation.period.mpd.manifest.minBufferTime;
const availabilityWindow = representation.segmentAvailabilityRange;
let periodRelativeRange = {
start: timelineConverter.calcPeriodRelativeTimeFromMpdRelativeTime(representation, availabilityWindow ? availabilityWindow.start : NaN),
end: timelineConverter.calcPeriodRelativeTimeFromMpdRelativeTime(representation, availabilityWindow ? availabilityWindow.end : NaN)
};
const currentSegmentList = representation.segments;
const availabilityLowerLimit = 2 * duration;
const availabilityUpperLimit = givenAvailabilityUpperLimit || Math.max(2 * minBufferTime, 10 * duration);
let originAvailabilityTime = NaN;
let originSegment = null;
let start,
end,
range;
periodRelativeRange.start = Math.max(periodRelativeRange.start, 0);
if (isDynamic && !timelineConverter.isTimeSyncCompleted()) {
start = Math.floor(periodRelativeRange.start / duration);
end = Math.floor(periodRelativeRange.end / duration);
range = {start: start, end: end};
return range;
}
// if segments exist we should try to find the latest buffered time, which is the presentation time of the
// segment for the current index
if (currentSegmentList && currentSegmentList.length > 0) {
originSegment = getSegmentByIndex(index, representation);
if (originSegment) {
originAvailabilityTime = timelineConverter.calcPeriodRelativeTimeFromMpdRelativeTime(representation, originSegment.presentationStartTime);
} else {
originAvailabilityTime = index > 0 ? index * duration :
timelineConverter.calcPeriodRelativeTimeFromMpdRelativeTime(representation, requestedTime);
}
} else {
// If no segments exist, but index > 0, it means that we switch to the other representation, so
// we should proceed from this time.
// Otherwise we should start from the beginning for static mpds or from the end (live edge) for dynamic mpds
originAvailabilityTime = index > 0 ? index * duration : isDynamic ? periodRelativeRange.end : periodRelativeRange.start;
}
// segment list should not be out of the availability window range
start = Math.floor(Math.max(originAvailabilityTime - availabilityLowerLimit, periodRelativeRange.start) / duration);
end = Math.floor(Math.min(start + availabilityUpperLimit / duration, periodRelativeRange.end / duration));
range = {start: start, end: end};
return range;
}