diff --git a/package-lock.json b/package-lock.json index e71b92cf79..8960962850 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "dashjs", - "version": "4.0.0", + "version": "4.0.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/src/core/Settings.js b/src/core/Settings.js index 85b0fb1fcb..7c14892cb9 100644 --- a/src/core/Settings.js +++ b/src/core/Settings.js @@ -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). diff --git a/src/dash/DashHandler.js b/src/dash/DashHandler.js index 5e49f11c4f..30f46e14fb 100644 --- a/src/dash/DashHandler.js +++ b/src/dash/DashHandler.js @@ -37,6 +37,7 @@ import { replaceTokenForTemplate, unescapeDollarsInTemplate } from './utils/SegmentsUtils'; +import DashConstants from './constants/DashConstants'; function DashHandler(config) { @@ -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); } @@ -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; @@ -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; } @@ -186,42 +171,61 @@ 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; @@ -229,16 +233,10 @@ function DashHandler(config) { 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); } @@ -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, @@ -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 = { @@ -322,12 +312,10 @@ function DashHandler(config) { getStreamInfo, getInitRequest, getSegmentRequestForTime, - getNextSegmentRequest, - setCurrentIndex, getCurrentIndex, - isMediaFinished, + getNextSegmentRequest, + lastSegmentRequested, reset, - resetIndex, getNextSegmentRequestIdempotent }; diff --git a/src/dash/controllers/RepresentationController.js b/src/dash/controllers/RepresentationController.js index 021f8a1ae9..a09306f75d 100644 --- a/src/dash/controllers/RepresentationController.js +++ b/src/dash/controllers/RepresentationController.js @@ -142,6 +142,7 @@ function RepresentationController(config) { if (data[1] && !data[1].error) { currentRep = _onSegmentsLoaded(currentRep, data[1]); } + _setMediaFinishedInformation(currentRep); _onRepresentationUpdated(currentRep); resolve(); }) @@ -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; @@ -192,7 +197,6 @@ function RepresentationController(config) { } if (segments.length > 0) { - representation.availableSegmentsNumber = segments.length; representation.segments = segments; } diff --git a/src/dash/controllers/SegmentsController.js b/src/dash/controllers/SegmentsController.js index c1944217e0..ecccad8180 100644 --- a/src/dash/controllers/SegmentsController.js +++ b/src/dash/controllers/SegmentsController.js @@ -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(); diff --git a/src/dash/models/DashManifestModel.js b/src/dash/models/DashManifestModel.js index 91ee7a26c3..6a636941b8 100644 --- a/src/dash/models/DashManifestModel.js +++ b/src/dash/models/DashManifestModel.js @@ -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); @@ -689,6 +692,10 @@ function DashManifestModel() { voPeriod.duration = realPeriod.duration; } + if (voPreviousPeriod) { + voPreviousPeriod.nextPeriodId = voPeriod.id; + } + voPeriods.push(voPeriod); realPreviousPeriod = realPeriod; voPreviousPeriod = voPeriod; diff --git a/src/dash/utils/ListSegmentsGetter.js b/src/dash/utils/ListSegmentsGetter.js index 5c458e00cd..973c047eeb 100644 --- a/src/dash/utils/ListSegmentsGetter.js +++ b/src/dash/utils/ListSegmentsGetter.js @@ -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(); @@ -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; } @@ -101,8 +114,9 @@ function ListSegmentsGetter(config, isDynamic) { } instance = { - getSegmentByIndex: getSegmentByIndex, - getSegmentByTime: getSegmentByTime + getSegmentByIndex, + getSegmentByTime, + getMediaFinishedInformation }; return instance; diff --git a/src/dash/utils/SegmentBaseGetter.js b/src/dash/utils/SegmentBaseGetter.js index 3bddfcfdf5..e1491574b1 100644 --- a/src/dash/utils/SegmentBaseGetter.js +++ b/src/dash/utils/SegmentBaseGetter.js @@ -46,6 +46,18 @@ function SegmentBaseGetter(config) { } } + function getMediaFinishedInformation(representation) { + const mediaFinishedInformation = { numberOfSegments: 0, mediaTimeOfLastSignaledSegment: NaN } + + if (!representation || !representation.segments) { + return mediaFinishedInformation + } + + mediaFinishedInformation.numberOfSegments = representation.segments.length; + + return mediaFinishedInformation; + } + function getSegmentByIndex(representation, index) { checkConfig(); @@ -57,7 +69,7 @@ function SegmentBaseGetter(config) { let seg; if (index < len) { seg = representation.segments[index]; - if (seg && seg.availabilityIdx === index) { + if (seg && seg.index === index) { return seg; } } @@ -65,7 +77,7 @@ function SegmentBaseGetter(config) { for (let i = 0; i < len; i++) { seg = representation.segments[i]; - if (seg && seg.availabilityIdx === index) { + if (seg && seg.index === index) { return seg; } } @@ -91,21 +103,21 @@ function SegmentBaseGetter(config) { let idx = -1; let epsilon, - frag, + seg, ft, fd, i; if (segments && ln > 0) { for (i = 0; i < ln; i++) { - frag = segments[i]; - ft = frag.presentationStartTime; - fd = frag.duration; + seg = segments[i]; + ft = seg.presentationStartTime; + fd = seg.duration; epsilon = fd / 2; if ((time + epsilon) >= ft && (time - epsilon) < (ft + fd)) { - idx = frag.availabilityIdx; + idx = seg.index; break; } } @@ -115,8 +127,9 @@ function SegmentBaseGetter(config) { } instance = { - getSegmentByIndex: getSegmentByIndex, - getSegmentByTime: getSegmentByTime + getSegmentByIndex, + getSegmentByTime, + getMediaFinishedInformation }; return instance; diff --git a/src/dash/utils/SegmentsUtils.js b/src/dash/utils/SegmentsUtils.js index 6632afb944..4a29b47c33 100644 --- a/src/dash/utils/SegmentsUtils.js +++ b/src/dash/utils/SegmentsUtils.js @@ -129,7 +129,7 @@ export function replaceTokenForTemplate(url, token, value) { } } -function getSegment(representation, duration, presentationStartTime, mediaStartTime, availabilityStartTime, +function getSegment(representation, duration, presentationStartTime, mediaStartTime, timelineConverter, presentationEndTime, isDynamic, index) { let seg = new Segment(); @@ -137,11 +137,11 @@ function getSegment(representation, duration, presentationStartTime, mediaStartT seg.duration = duration; seg.presentationStartTime = presentationStartTime; seg.mediaStartTime = mediaStartTime; - seg.availabilityStartTime = availabilityStartTime; + seg.availabilityStartTime = timelineConverter.calcAvailabilityStartTimeFromPresentationTime(presentationEndTime, representation, isDynamic); seg.availabilityEndTime = timelineConverter.calcAvailabilityEndTimeFromPresentationTime(presentationEndTime + duration, representation, isDynamic); seg.wallStartTime = timelineConverter.calcWallTimeForSegment(seg, isDynamic); seg.replacementNumber = getNumberForSegment(seg, index); - seg.availabilityIdx = index; + seg.index = index; return seg; } @@ -193,9 +193,8 @@ export function getIndexBasedSegment(timelineConverter, isDynamic, representatio presentationEndTime = parseFloat((presentationStartTime + duration).toFixed(5)); const mediaTime = timelineConverter.calcMediaTimeFromPresentationTime(presentationStartTime, representation); - const availabilityStartTime = timelineConverter.calcAvailabilityStartTimeFromPresentationTime(presentationEndTime, representation, isDynamic); - const segment = getSegment(representation, duration, presentationStartTime, mediaTime, availabilityStartTime, + const segment = getSegment(representation, duration, presentationStartTime, mediaTime, timelineConverter, presentationEndTime, isDynamic, index); if (!isSegmentAvailable(timelineConverter, representation, segment, isDynamic)) { @@ -216,11 +215,8 @@ export function getTimeBasedSegment(timelineConverter, isDynamic, representation presentationStartTime = timelineConverter.calcPresentationTimeFromMediaTime(scaledTime, representation); presentationEndTime = presentationStartTime + scaledDuration; - const availabilityStartTime = timelineConverter.calcAvailabilityStartTimeFromPresentationTime(presentationEndTime, representation, isDynamic); - seg = getSegment(representation, scaledDuration, presentationStartTime, scaledTime, - availabilityStartTime, timelineConverter, presentationEndTime, isDynamic, index); if (!isSegmentAvailable(timelineConverter, representation, seg, isDynamic)) { diff --git a/src/dash/utils/TemplateSegmentsGetter.js b/src/dash/utils/TemplateSegmentsGetter.js index b0a77371fa..9167e1635c 100644 --- a/src/dash/utils/TemplateSegmentsGetter.js +++ b/src/dash/utils/TemplateSegmentsGetter.js @@ -46,6 +46,22 @@ function TemplateSegmentsGetter(config, isDynamic) { } } + function getMediaFinishedInformation(representation) { + const mediaFinishedInformation = { numberOfSegments: 0, mediaTimeOfLastSignaledSegment: NaN } + if (!representation) { + return mediaFinishedInformation + } + + const duration = representation.segmentDuration; + if (isNaN(duration)) { + mediaFinishedInformation.numberOfSegments = 1; + } else { + mediaFinishedInformation.numberOfSegments = Math.ceil(representation.adaptation.period.duration / duration); + } + + return mediaFinishedInformation; + } + function getSegmentByIndex(representation, index) { checkConfig(); @@ -55,11 +71,12 @@ function TemplateSegmentsGetter(config, isDynamic) { const template = representation.adaptation.period.mpd.manifest.Period_asArray[representation.adaptation.period.index].AdaptationSet_asArray[representation.adaptation.index].Representation_asArray[representation.index].SegmentTemplate; + // This is the index without @startNumber index = Math.max(index, 0); const seg = getIndexBasedSegment(timelineConverter, isDynamic, representation, index); if (seg) { - seg.replacementTime = Math.round((index - 1) * representation.segmentDuration * representation.timescale,10); + seg.replacementTime = Math.round((index - 1) * representation.segmentDuration * representation.timescale, 10); let url = template.media; url = replaceTokenForTemplate(url, 'Number', seg.replacementNumber); @@ -67,14 +84,6 @@ function TemplateSegmentsGetter(config, isDynamic) { seg.media = url; } - const duration = representation.segmentDuration; - - if (isNaN(duration)) { - representation.availableSegmentsNumber = 1; - } else { - representation.availableSegmentsNumber = Math.ceil(representation.adaptation.period.duration / duration); - } - return seg; } @@ -91,6 +100,7 @@ function TemplateSegmentsGetter(config, isDynamic) { return null; } + // Calculate the relative time for the requested time in this period let periodTime = timelineConverter.calcPeriodRelativeTimeFromMpdRelativeTime(representation, requestedTime); const index = Math.floor(periodTime / duration); @@ -99,7 +109,8 @@ function TemplateSegmentsGetter(config, isDynamic) { instance = { getSegmentByIndex, - getSegmentByTime + getSegmentByTime, + getMediaFinishedInformation }; return instance; diff --git a/src/dash/utils/TimelineSegmentsGetter.js b/src/dash/utils/TimelineSegmentsGetter.js index 920b720b81..3e6bfb837b 100644 --- a/src/dash/utils/TimelineSegmentsGetter.js +++ b/src/dash/utils/TimelineSegmentsGetter.js @@ -48,6 +48,64 @@ function TimelineSegmentsGetter(config, isDynamic) { } } + function getMediaFinishedInformation(representation) { + if (!representation) { + return 0; + } + + const base = representation.adaptation.period.mpd.manifest.Period_asArray[representation.adaptation.period.index].AdaptationSet_asArray[representation.adaptation.index].Representation_asArray[representation.index].SegmentTemplate || + representation.adaptation.period.mpd.manifest.Period_asArray[representation.adaptation.period.index].AdaptationSet_asArray[representation.adaptation.index].Representation_asArray[representation.index].SegmentList; + const timeline = base.SegmentTimeline; + + let time = 0; + let scaledTime = 0; + let availableSegments = 0; + + let fragments, + frag, + i, + len, + j, + repeat, + fTimescale; + + fTimescale = representation.timescale; + fragments = timeline.S_asArray; + + len = fragments.length; + + for (i = 0; i < len; i++) { + frag = fragments[i]; + repeat = 0; + if (frag.hasOwnProperty('r')) { + repeat = frag.r; + } + + // For a repeated S element, t belongs only to the first segment + if (frag.hasOwnProperty('t')) { + time = frag.t; + scaledTime = time / fTimescale; + } + + // This is a special case: "A negative value of the @r attribute of the S element indicates that the duration indicated in @d attribute repeats until the start of the next S element, the end of the Period or until the + // next MPD update." + if (repeat < 0) { + const nextFrag = fragments[i + 1]; + repeat = _calculateRepeatCountForNegativeR(representation, nextFrag, frag, fTimescale, scaledTime); + } + + for (j = 0; j <= repeat; j++) { + availableSegments++; + + time += frag.d; + scaledTime = time / fTimescale; + } + } + + // We need to account for the index of the segments starting at 0. We subtract 1 + return { numberOfSegments: availableSegments, mediaTimeOfLastSignaledSegment: scaledTime }; + } + function iterateSegments(representation, iterFunc) { const base = representation.adaptation.period.mpd.manifest.Period_asArray[representation.adaptation.period.index].AdaptationSet_asArray[representation.adaptation.index].Representation_asArray[representation.index].SegmentTemplate || representation.adaptation.period.mpd.manifest.Period_asArray[representation.adaptation.period.index].AdaptationSet_asArray[representation.adaptation.index].Representation_asArray[representation.index].SegmentList; @@ -56,7 +114,7 @@ function TimelineSegmentsGetter(config, isDynamic) { let time = 0; let scaledTime = 0; - let availabilityIdx = -1; + let relativeIdx = -1; let fragments, frag, @@ -64,8 +122,6 @@ function TimelineSegmentsGetter(config, isDynamic) { len, j, repeat, - repeatEndTime, - nextFrag, fTimescale; fTimescale = representation.timescale; @@ -89,53 +145,52 @@ function TimelineSegmentsGetter(config, isDynamic) { // This is a special case: "A negative value of the @r attribute of the S element indicates that the duration indicated in @d attribute repeats until the start of the next S element, the end of the Period or until the // next MPD update." if (repeat < 0) { - nextFrag = fragments[i + 1]; - - if (nextFrag && nextFrag.hasOwnProperty('t')) { - repeatEndTime = nextFrag.t / fTimescale; - } else { - try { - let availabilityEnd = 0; - if (!isNaN(representation.adaptation.period.start) && !isNaN(representation.adaptation.period.duration) && isFinite(representation.adaptation.period.duration)) { - // use end of the Period - availabilityEnd = representation.adaptation.period.start + representation.adaptation.period.duration; - } else { - // use DVR window - const dvrWindow = dashMetrics.getCurrentDVRInfo(); - availabilityEnd = !isNaN(dvrWindow.end) ? dvrWindow.end : 0; - } - repeatEndTime = timelineConverter.calcMediaTimeFromPresentationTime(availabilityEnd, representation); - representation.segmentDuration = frag.d / fTimescale; - } catch (e) { - repeatEndTime = 0; - } - } - - repeat = Math.max(Math.ceil((repeatEndTime - scaledTime) / (frag.d / fTimescale)) - 1, 0); + const nextFrag = fragments[i + 1]; + repeat = _calculateRepeatCountForNegativeR(representation, nextFrag, frag, fTimescale, scaledTime); } for (j = 0; j <= repeat && !breakIterator; j++) { - availabilityIdx++; + relativeIdx++; - breakIterator = iterFunc(time, scaledTime, base, list, frag, fTimescale, availabilityIdx, i); + breakIterator = iterFunc(time, scaledTime, base, list, frag, fTimescale, relativeIdx, i); if (breakIterator) { representation.segmentDuration = frag.d / fTimescale; - - // check if there is at least one more segment - if (j < repeat - 1 || i < len - 1) { - availabilityIdx++; - } } time += frag.d; scaledTime = time / fTimescale; } } + } - representation.availableSegmentsNumber = availabilityIdx; + function _calculateRepeatCountForNegativeR(representation, nextFrag, frag, fTimescale, scaledTime) { + let repeatEndTime; + + if (nextFrag && nextFrag.hasOwnProperty('t')) { + repeatEndTime = nextFrag.t / fTimescale; + } else { + try { + let availabilityEnd = 0; + if (!isNaN(representation.adaptation.period.start) && !isNaN(representation.adaptation.period.duration) && isFinite(representation.adaptation.period.duration)) { + // use end of the Period + availabilityEnd = representation.adaptation.period.start + representation.adaptation.period.duration; + } else { + // use DVR window + const dvrWindow = dashMetrics.getCurrentDVRInfo(); + availabilityEnd = !isNaN(dvrWindow.end) ? dvrWindow.end : 0; + } + repeatEndTime = timelineConverter.calcMediaTimeFromPresentationTime(availabilityEnd, representation); + representation.segmentDuration = frag.d / fTimescale; + } catch (e) { + repeatEndTime = 0; + } + } + + return Math.max(Math.ceil((repeatEndTime - scaledTime) / (frag.d / fTimescale)) - 1, 0); } + function getSegmentByIndex(representation, index, lastSegmentTime) { checkConfig(); @@ -146,7 +201,7 @@ function TimelineSegmentsGetter(config, isDynamic) { let segment = null; let found = false; - iterateSegments(representation, function (time, scaledTime, base, list, frag, fTimescale, availabilityIdx, i) { + iterateSegments(representation, function (time, scaledTime, base, list, frag, fTimescale, relativeIdx, i) { if (found || lastSegmentTime < 0) { let media = base.media; let mediaRange = frag.mediaRange; @@ -165,7 +220,7 @@ function TimelineSegmentsGetter(config, isDynamic) { fTimescale, media, mediaRange, - availabilityIdx, + relativeIdx, frag.tManifest); return true; @@ -194,7 +249,7 @@ function TimelineSegmentsGetter(config, isDynamic) { let segment = null; const requiredMediaTime = timelineConverter.calcMediaTimeFromPresentationTime(requestedTime, representation); - iterateSegments(representation, function (time, scaledTime, base, list, frag, fTimescale, availabilityIdx, i) { + iterateSegments(representation, function (time, scaledTime, base, list, frag, fTimescale, relativeIdx, i) { // In some cases when requiredMediaTime = actual end time of the last segment // it is possible that this time a bit exceeds the declared end time of the last segment. // in this case we still need to include the last segment in the segment list. @@ -216,7 +271,7 @@ function TimelineSegmentsGetter(config, isDynamic) { fTimescale, media, mediaRange, - availabilityIdx, + relativeIdx, frag.tManifest); return true; @@ -230,8 +285,9 @@ function TimelineSegmentsGetter(config, isDynamic) { instance = { - getSegmentByIndex: getSegmentByIndex, - getSegmentByTime: getSegmentByTime + getSegmentByIndex, + getSegmentByTime, + getMediaFinishedInformation }; return instance; diff --git a/src/dash/vo/Period.js b/src/dash/vo/Period.js index 10094b7b02..f5c114c3ff 100644 --- a/src/dash/vo/Period.js +++ b/src/dash/vo/Period.js @@ -39,9 +39,10 @@ class Period { this.duration = NaN; this.start = NaN; this.mpd = null; + this.nextPeriodId = null; } } Period.DEFAULT_ID = 'defaultId'; -export default Period; \ No newline at end of file +export default Period; diff --git a/src/dash/vo/Representation.js b/src/dash/vo/Representation.js index 7b5ccaaac0..6c820edd5c 100644 --- a/src/dash/vo/Representation.js +++ b/src/dash/vo/Representation.js @@ -53,7 +53,8 @@ class Representation { this.presentationTimeOffset = 0; // Set the source buffer timeOffset to this this.MSETimeOffset = NaN; - this.availableSegmentsNumber = 0; + // The information we need in the DashHandler to determine whether the last segment has been loaded + this.mediaFinishedInformation = { numberOfSegments: 0, mediaTimeOfLastSignaledSegment: NaN }; this.bandwidth = NaN; this.width = NaN; this.height = NaN; diff --git a/src/dash/vo/Segment.js b/src/dash/vo/Segment.js index 3b9b79c985..33d6559ad7 100644 --- a/src/dash/vo/Segment.js +++ b/src/dash/vo/Segment.js @@ -35,6 +35,7 @@ class Segment { constructor() { this.indexRange = null; + // The index of the segment in the list of segments. We start at 0 this.index = null; this.mediaRange = null; this.media = null; @@ -52,8 +53,6 @@ class Segment { this.availabilityStartTime = NaN; // Ignore and discard this segment after this.availabilityEndTime = NaN; - // The index of the segment inside the availability window - this.availabilityIdx = NaN; // For dynamic mpd's, this is the wall clock time that the video // element currentTime should be presentationStartTime this.wallStartTime = NaN; @@ -61,4 +60,4 @@ class Segment { } } -export default Segment; \ No newline at end of file +export default Segment; diff --git a/src/offline/OfflineStreamProcessor.js b/src/offline/OfflineStreamProcessor.js index 1fc5699639..b5688bbe2c 100644 --- a/src/offline/OfflineStreamProcessor.js +++ b/src/offline/OfflineStreamProcessor.js @@ -333,7 +333,7 @@ function OfflineStreamProcessor(config) { } function getAvailableSegmentsNumber() { - return representationController.getCurrentRepresentation().availableSegmentsNumber + 1; // do not forget init segment + return representationController.getCurrentRepresentation().numberOfSegments + 1; // do not forget init segment } function updateProgression () { diff --git a/src/streaming/StreamProcessor.js b/src/streaming/StreamProcessor.js index f26c08dc30..e207ebd560 100644 --- a/src/streaming/StreamProcessor.js +++ b/src/streaming/StreamProcessor.js @@ -403,16 +403,17 @@ function StreamProcessor(config) { let request = null; const representation = representationController.getCurrentRepresentation(); - const isMediaFinished = dashHandler.isMediaFinished(representation, bufferingTime); + const lastSegmentRequested = dashHandler.lastSegmentRequested(representation, bufferingTime); // Check if the media is finished. If so, no need to schedule another request - if (isMediaFinished) { + if (lastSegmentRequested) { 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; } diff --git a/src/streaming/controllers/BufferController.js b/src/streaming/controllers/BufferController.js index 0f2452bdd9..6996bfac7e 100644 --- a/src/streaming/controllers/BufferController.js +++ b/src/streaming/controllers/BufferController.js @@ -100,7 +100,6 @@ function BufferController(config) { eventBus.on(Events.INIT_FRAGMENT_LOADED, _onInitFragmentLoaded, instance); eventBus.on(Events.MEDIA_FRAGMENT_LOADED, _onMediaFragmentLoaded, instance); - eventBus.on(Events.STREAM_REQUESTING_COMPLETED, _onStreamRequestingCompleted, instance); eventBus.on(Events.WALLCLOCK_TIME_UPDATED, _onWallclockTimeUpdated, instance); eventBus.on(MediaPlayerEvents.PLAYBACK_PLAYING, _onPlaybackPlaying, instance); @@ -875,9 +874,9 @@ function BufferController(config) { return Promise.resolve(); } - function _onStreamRequestingCompleted(e) { - if (!isNaN(e.segmentIndex)) { - maximumIndex = e.segmentIndex; + function segmentRequestingCompleted(segmentIndex) { + if (!isNaN(segmentIndex)) { + maximumIndex = segmentIndex; _checkIfBufferingCompleted(); } } @@ -1029,7 +1028,6 @@ function BufferController(config) { eventBus.off(Events.INIT_FRAGMENT_LOADED, _onInitFragmentLoaded, this); eventBus.off(Events.MEDIA_FRAGMENT_LOADED, _onMediaFragmentLoaded, this); eventBus.off(Events.WALLCLOCK_TIME_UPDATED, _onWallclockTimeUpdated, this); - eventBus.off(Events.STREAM_REQUESTING_COMPLETED, _onStreamRequestingCompleted, this); eventBus.off(MediaPlayerEvents.PLAYBACK_PLAYING, _onPlaybackPlaying, this); eventBus.off(MediaPlayerEvents.PLAYBACK_PROGRESS, _onPlaybackProgression, this); @@ -1067,7 +1065,8 @@ function BufferController(config) { clearBuffers, pruneAllSafely, updateBufferTimestampOffset, - setSeekTarget + setSeekTarget, + segmentRequestingCompleted }; setup(); diff --git a/src/streaming/text/NotFragmentedTextBufferController.js b/src/streaming/text/NotFragmentedTextBufferController.js index 6822978f59..0e8cc6dcd3 100644 --- a/src/streaming/text/NotFragmentedTextBufferController.js +++ b/src/streaming/text/NotFragmentedTextBufferController.js @@ -200,6 +200,10 @@ function NotFragmentedTextBufferController(config) { } + function segmentRequestingCompleted() { + + } + function pruneAllSafely() { return Promise.resolve(); } @@ -233,7 +237,8 @@ function NotFragmentedTextBufferController(config) { setSeekTarget, updateAppendWindow, pruneAllSafely, - updateBufferTimestampOffset + updateBufferTimestampOffset, + segmentRequestingCompleted }; setup(); diff --git a/test/unit/dash.utils.ListSegmentsGetter.js b/test/unit/dash.utils.ListSegmentsGetter.js index 87e1603388..66ce6d5c39 100644 --- a/test/unit/dash.utils.ListSegmentsGetter.js +++ b/test/unit/dash.utils.ListSegmentsGetter.js @@ -97,12 +97,19 @@ describe('ListSegmentsGetter', () => { }); }); - describe('availableSegmentsNumber calculation', () => { - it('should calculate representation segment range correctly', () => { + describe('mediaFinishedInformation calculation', () => { + it('should calculate the right number of segments', () => { const representation = createRepresentationMock(); - listSegmentsGetter.getSegmentByIndex(representation, 0); - expect(representation.availableSegmentsNumber).to.equal(5); + const mediaFinishedInformation = listSegmentsGetter.getMediaFinishedInformation(representation); + expect(mediaFinishedInformation.numberOfSegments).to.equal(5); + }); + + it('mediaTimeOfLastSignaledSegments should be NaN', () => { + const representation = createRepresentationMock(); + + const mediaFinishedInformation = listSegmentsGetter.getMediaFinishedInformation(representation); + expect(mediaFinishedInformation.mediaTimeOfLastSignaledSegment).to.be.NaN; }); }); @@ -111,17 +118,17 @@ describe('ListSegmentsGetter', () => { const representation = createRepresentationMock(); let seg = listSegmentsGetter.getSegmentByIndex(representation, 0); - expect(seg.availabilityIdx).to.equal(0); + expect(seg.index).to.equal(0); expect(seg.presentationStartTime).to.equal(0); expect(seg.duration).to.equal(10); seg = listSegmentsGetter.getSegmentByIndex(representation, 1); - expect(seg.availabilityIdx).to.equal(1); + expect(seg.index).to.equal(1); expect(seg.presentationStartTime).to.equal(10); expect(seg.duration).to.equal(10); seg = listSegmentsGetter.getSegmentByIndex(representation, 2); - expect(seg.availabilityIdx).to.equal(2); + expect(seg.index).to.equal(2); expect(seg.presentationStartTime).to.equal(20); expect(seg.duration).to.equal(10); }); @@ -147,17 +154,17 @@ describe('ListSegmentsGetter', () => { const representation = createRepresentationMock(); let seg = listSegmentsGetter.getSegmentByTime(representation, 0); - expect(seg.availabilityIdx).to.equal(0); + expect(seg.index).to.equal(0); expect(seg.presentationStartTime).to.equal(0); expect(seg.duration).to.equal(10); seg = listSegmentsGetter.getSegmentByTime(representation, 22); - expect(seg.availabilityIdx).to.equal(2); + expect(seg.index).to.equal(2); expect(seg.presentationStartTime).to.equal(20); expect(seg.duration).to.equal(10); seg = listSegmentsGetter.getSegmentByTime(representation, 32); - expect(seg.availabilityIdx).to.equal(3); + expect(seg.index).to.equal(3); expect(seg.presentationStartTime).to.equal(30); expect(seg.duration).to.equal(10); }); diff --git a/test/unit/dash.utils.SegmentBaseGetter.js b/test/unit/dash.utils.SegmentBaseGetter.js index 24ee332ff4..6f987c3c6b 100644 --- a/test/unit/dash.utils.SegmentBaseGetter.js +++ b/test/unit/dash.utils.SegmentBaseGetter.js @@ -12,7 +12,7 @@ function createRepresentationMock() { for (let i = 0; i < 5; i++) { representation.segments.push({ - availabilityIdx: i, + index: i, duration: 5, mediaStartTime: i * 5, presentationStartTime: i * 5, @@ -63,17 +63,17 @@ describe('SegmentBaseGetter', () => { const representation = createRepresentationMock(); let seg = segmentBaseGetter.getSegmentByIndex(representation, 0); - expect(seg.availabilityIdx).to.equal(0); + expect(seg.index).to.equal(0); expect(seg.presentationStartTime).to.equal(0); expect(seg.duration).to.equal(5); seg = segmentBaseGetter.getSegmentByIndex(representation, 1); - expect(seg.availabilityIdx).to.equal(1); + expect(seg.index).to.equal(1); expect(seg.presentationStartTime).to.equal(5); expect(seg.duration).to.equal(5); seg = segmentBaseGetter.getSegmentByIndex(representation, 2); - expect(seg.availabilityIdx).to.equal(2); + expect(seg.index).to.equal(2); expect(seg.presentationStartTime).to.equal(10); expect(seg.duration).to.equal(5); }); @@ -91,17 +91,17 @@ describe('SegmentBaseGetter', () => { const representation = createRepresentationMock(); let seg = segmentBaseGetter.getSegmentByTime(representation, 0); - expect(seg.availabilityIdx).to.equal(0); + expect(seg.index).to.equal(0); expect(seg.presentationStartTime).to.equal(0); expect(seg.duration).to.equal(5); seg = segmentBaseGetter.getSegmentByTime(representation, 6); - expect(seg.availabilityIdx).to.equal(0); + expect(seg.index).to.equal(0); expect(seg.presentationStartTime).to.equal(0); expect(seg.duration).to.equal(5); seg = segmentBaseGetter.getSegmentByTime(representation, 12); - expect(seg.availabilityIdx).to.equal(1); + expect(seg.index).to.equal(1); expect(seg.presentationStartTime).to.equal(5); expect(seg.duration).to.equal(5); }); diff --git a/test/unit/dash.utils.TemplateSegmentsGetter.js b/test/unit/dash.utils.TemplateSegmentsGetter.js index 3b35c4f725..f6d9b1ff65 100644 --- a/test/unit/dash.utils.TemplateSegmentsGetter.js +++ b/test/unit/dash.utils.TemplateSegmentsGetter.js @@ -45,18 +45,25 @@ describe('TemplateSegmentsGetter', () => { representation.segmentAvailabilityWindow = {start: 0, end: 100}; representation.segmentDuration = undefined; - templateSegmentsGetter.getSegmentByIndex(representation, 0); - expect(representation.availableSegmentsNumber).to.equal(1); + const mediaFinishedInformation = templateSegmentsGetter.getMediaFinishedInformation(representation); + expect(mediaFinishedInformation.numberOfSegments).to.equal(1); }); - it('should calculate representation segment range correctly', () => { + it('should calculate availableSegmentsNumber correctly', () => { const representation = voHelper.getDummyRepresentation(Constants.VIDEO); - representation.segmentAvailabilityWindow = {start: 0, end: 100}; representation.segmentDuration = 5; - templateSegmentsGetter.getSegmentByIndex(representation, 0); - expect(representation.availableSegmentsNumber).to.equal(20); + representation.segmentAvailabilityWindow = {start: 0, end: 100}; + let mediaFinishedInformation = templateSegmentsGetter.getMediaFinishedInformation(representation); + expect(mediaFinishedInformation.numberOfSegments).to.equal(20); + + representation.segmentAvailabilityWindow = {start: 0, end: 101}; + representation.adaptation.period.duration = 101; + mediaFinishedInformation = templateSegmentsGetter.getMediaFinishedInformation(representation); + expect(mediaFinishedInformation.numberOfSegments).to.equal(21); }); + + }); describe('getSegmentByIndex', () => { @@ -66,17 +73,17 @@ describe('TemplateSegmentsGetter', () => { representation.segmentDuration = 1; let seg = templateSegmentsGetter.getSegmentByIndex(representation, 0); - expect(seg.availabilityIdx).to.equal(0); + expect(seg.index).to.equal(0); expect(seg.presentationStartTime).to.equal(0); expect(seg.duration).to.equal(1); seg = templateSegmentsGetter.getSegmentByIndex(representation, 1); - expect(seg.availabilityIdx).to.equal(1); + expect(seg.index).to.equal(1); expect(seg.presentationStartTime).to.equal(1); expect(seg.duration).to.equal(1); seg = templateSegmentsGetter.getSegmentByIndex(representation, 2); - expect(seg.availabilityIdx).to.equal(2); + expect(seg.index).to.equal(2); expect(seg.presentationStartTime).to.equal(2); expect(seg.duration).to.equal(1); }); @@ -107,22 +114,22 @@ describe('TemplateSegmentsGetter', () => { representation.segmentDuration = 5; let seg = templateSegmentsGetter.getSegmentByTime(representation, 0); - expect(seg.availabilityIdx).to.equal(0); + expect(seg.index).to.equal(0); expect(seg.presentationStartTime).to.equal(0); expect(seg.duration).to.equal(5); seg = templateSegmentsGetter.getSegmentByTime(representation, 3); - expect(seg.availabilityIdx).to.equal(0); + expect(seg.index).to.equal(0); expect(seg.presentationStartTime).to.equal(0); expect(seg.duration).to.equal(5); seg = templateSegmentsGetter.getSegmentByTime(representation, 12); - expect(seg.availabilityIdx).to.equal(2); + expect(seg.index).to.equal(2); expect(seg.presentationStartTime).to.equal(10); expect(seg.duration).to.equal(5); seg = templateSegmentsGetter.getSegmentByTime(representation, 17); - expect(seg.availabilityIdx).to.equal(3); + expect(seg.index).to.equal(3); expect(seg.presentationStartTime).to.equal(15); expect(seg.duration).to.equal(5); }); diff --git a/test/unit/dash.utils.TimelineSegmentsGetter.js b/test/unit/dash.utils.TimelineSegmentsGetter.js index e4130d9ee6..1b1afc8ad1 100644 --- a/test/unit/dash.utils.TimelineSegmentsGetter.js +++ b/test/unit/dash.utils.TimelineSegmentsGetter.js @@ -80,11 +80,17 @@ describe('TimelineSegmentsGetter', () => { }); describe('availableSegmentsNumber calculation', () => { - it('should calculate representation segment range correctly', () => { + it('should calculate the number of available segments correctly', () => { const representation = createRepresentationMock(); - timelineSegmentsGetter.getSegmentByIndex(representation, 0); - expect(representation.availableSegmentsNumber).to.equal(25); + const mediaFinishedInformation = timelineSegmentsGetter.getMediaFinishedInformation(representation); + expect(mediaFinishedInformation.numberOfSegments).to.equal(26); + }); + it('should calculate the media time of the last segment correctly', () => { + const representation = createRepresentationMock(); + + const mediaFinishedInformation = timelineSegmentsGetter.getMediaFinishedInformation(representation); + expect(mediaFinishedInformation.mediaTimeOfLastSignaledSegment).to.equal(101.1); }); }); @@ -93,17 +99,17 @@ describe('TimelineSegmentsGetter', () => { const representation = createRepresentationMock(); let seg = timelineSegmentsGetter.getSegmentByIndex(representation, 0, -1); - expect(seg.availabilityIdx).to.equal(0); + expect(seg.index).to.equal(0); expect(seg.presentationStartTime).to.equal(0); expect(seg.duration).to.equal(4.004); seg = timelineSegmentsGetter.getSegmentByIndex(representation, 1, 0); - expect(seg.availabilityIdx).to.equal(1); + expect(seg.index).to.equal(1); expect(seg.presentationStartTime).to.equal(4.004); expect(seg.duration).to.equal(4.004); seg = timelineSegmentsGetter.getSegmentByIndex(representation, 2, 4.004); - expect(seg.availabilityIdx).to.equal(2); + expect(seg.index).to.equal(2); expect(seg.presentationStartTime).to.equal(8.008); expect(seg.duration).to.equal(4.004); }); @@ -132,32 +138,32 @@ describe('TimelineSegmentsGetter', () => { expect(seg.duration).to.equal(4.004); seg = timelineSegmentsGetter.getSegmentByTime(representation, 3); - expect(seg.availabilityIdx).to.equal(0); + expect(seg.index).to.equal(0); expect(seg.presentationStartTime).to.equal(0); expect(seg.duration).to.equal(4.004); seg = timelineSegmentsGetter.getSegmentByTime(representation, 5); - expect(seg.availabilityIdx).to.equal(1); + expect(seg.index).to.equal(1); expect(seg.presentationStartTime).to.equal(4.004); expect(seg.duration).to.equal(4.004); seg = timelineSegmentsGetter.getSegmentByTime(representation, 22); - expect(seg.availabilityIdx).to.equal(5); + expect(seg.index).to.equal(5); expect(seg.presentationStartTime).to.equal(20.02); expect(seg.duration).to.equal(4.004); seg = timelineSegmentsGetter.getSegmentByTime(representation, 53); - expect(seg.availabilityIdx).to.equal(13); + expect(seg.index).to.equal(13); expect(seg.presentationStartTime).to.equal(52.052); expect(seg.duration).to.equal(4.004); seg = timelineSegmentsGetter.getSegmentByTime(representation, 4.004); - expect(seg.availabilityIdx).to.equal(1); + expect(seg.index).to.equal(1); expect(seg.presentationStartTime).to.equal(4.004); expect(seg.duration).to.equal(4.004); seg = timelineSegmentsGetter.getSegmentByTime(representation, 100.2); - expect(seg.availabilityIdx).to.equal(25); + expect(seg.index).to.equal(25); expect(seg.presentationStartTime).to.equal(100.1); expect(seg.duration).to.equal(1); });