Skip to content

Commit c453d11

Browse files
committed
DASH: Be more optimistic on the buffer depth of DASH contents
Overview -------- We were debugging some ad insertion usage, where an ad would be dynamically inserted at a live content's start on the initial Manifest request. An application reported to us that while playing such content with the RxPlayer, we were starting a few seconds into the ad instead of right at the beginning of it. When looking at segment requests we saw that the first media segment was never requested: we always started with the second one instead. Issue ----- After investigation, I found out that one of the issue, was that the `MPD@timeShiftBufferDepth` attribute of the produced stream was on the first Manifest request exactly equal to the duration of the video segments generated until now. Yet the RxPlayer always interpreted that it could only play segments whose ending timestamp is inferior or equal to the "live edge" (the live position basically, which monotically increases as time increases) and, more importantly here, whose starting timestamp is superior or equal to that same live edge minus the `timeShiftBufferDepth`. All this is not shocking, and other players have a similar interpretation here, but I noticed that this was why we were not requesting the first segment on the problematic content: as some milliseconds passed between the time the Manifest is parsed and the time at which we began choosing a segment to request, our "live edge" as further in the future (by the same amount of milliseconds) and thus the minimum timestamp we can request will also be further in the future - and ahead of the first segment position by the same amount of milliseconds. How to fix this --------------- So the fix seemed to be to either raise the `MPD@timeShiftBufferDepth` server-side or to raise the tolerance of what can still be requested RxPlayer-side. I dived into the DASH specification (5th edition) - which is a little hard to interpret here - but I found the following passage: > Assume the parameter availabilityTimeOffset is determined as the sum of all values of @availabilityTimeOffset on all levels that are processed in determining the URL for the corresponding segment. If the attribute @availabilityTimeOffset is not present, the value is of availabilityTimeOffset is 0. Then for services with MPD@type='dynamic', the Segment availability start time Tavail[i] for a Segment i in a specific Period is determined as MPD@availabilityStartTime + PeriodStart + MediaSegment[i].startTime + MediaSegment[i].duration - availabilityTimeOffset and the Segment availability end time is determined as MPD@availabilityStartTime + PeriodStart + MediaSegment[i].startTime + @timeshiftBufferDepth + 2*MediaSegment[i].duration At the very end, it seems that we have to re-apply a segment's duration, on top of the `timeShiftBufferDepth` to know when it is not available anymore (they indicate "2 times the duration"). So I guess we may have been too pessimistic with how we previously considered whether a segment could be requested or not. How other players treat this ---------------------------- Because the people behind the ad-insertion tool told me they succeeded to rely on the shaka-player, I looked at what they did on this issue. It turns out that they have roughly the same interpretation that we had for the minimum requestable position (`live edge - timeshiftBufferDepth`) but their "live edge" was basically offset by `- MPD@maxSegmentDuration`, so in the end they were removing the MPD's maximum segment duration from the minimum requestable position relatively to us. If I had to guess why they do that, it may be that they clonflate the "live edge" concept (the timestamp pointing to the current last media data basically) and what in the RxPlayer we call the "maximum safe position" (the last timestamp for which we're sure that a segment is currently available) where the RxPlayer make those two very different concepts and properties.
1 parent 4faffb4 commit c453d11

File tree

8 files changed

+68
-34
lines changed

8 files changed

+68
-34
lines changed

src/parsers/manifest/dash/common/__tests__/manifest_bounds_calculator.test.ts

Lines changed: 29 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@ describe("DASH parsers - ManifestBoundsCalculator", () => {
99
availabilityStartTime: 0,
1010
serverTimestampOffset: undefined,
1111
});
12-
expect(manifestBoundsCalculator.getEstimatedMinimumSegmentTime()).toEqual(undefined);
13-
expect(manifestBoundsCalculator.getEstimatedMinimumSegmentTime()).toEqual(undefined);
14-
expect(manifestBoundsCalculator.getEstimatedMinimumSegmentTime()).toEqual(undefined);
12+
expect(manifestBoundsCalculator.getEstimatedMinimumSegmentTime(0)).toEqual(undefined);
13+
expect(manifestBoundsCalculator.getEstimatedMinimumSegmentTime(3)).toEqual(undefined);
14+
expect(manifestBoundsCalculator.getEstimatedMinimumSegmentTime(5)).toEqual(undefined);
1515
});
1616

1717
it("should return 0 through `getEstimatedMinimumSegmentTime` for a static content", () => {
@@ -21,10 +21,10 @@ describe("DASH parsers - ManifestBoundsCalculator", () => {
2121
availabilityStartTime: 0,
2222
serverTimestampOffset: 555555,
2323
});
24-
expect(manifestBoundsCalculator.getEstimatedMinimumSegmentTime()).toEqual(0);
24+
expect(manifestBoundsCalculator.getEstimatedMinimumSegmentTime(0)).toEqual(0);
2525
manifestBoundsCalculator.setLastPosition(5555, 2135);
26-
expect(manifestBoundsCalculator.getEstimatedMinimumSegmentTime()).toEqual(0);
27-
expect(manifestBoundsCalculator.getEstimatedMinimumSegmentTime()).toEqual(0);
26+
expect(manifestBoundsCalculator.getEstimatedMinimumSegmentTime(4)).toEqual(0);
27+
expect(manifestBoundsCalculator.getEstimatedMinimumSegmentTime(5)).toEqual(0);
2828
});
2929

3030
it("should return 0 through `getEstimatedMinimumSegmentTime` if the `serverTimestampOffset` was never set nor the last position for a dynamic content with no timeShiftBufferDepth", () => {
@@ -34,9 +34,9 @@ describe("DASH parsers - ManifestBoundsCalculator", () => {
3434
availabilityStartTime: 0,
3535
serverTimestampOffset: undefined,
3636
});
37-
expect(manifestBoundsCalculator.getEstimatedMinimumSegmentTime()).toEqual(0);
38-
expect(manifestBoundsCalculator.getEstimatedMinimumSegmentTime()).toEqual(0);
39-
expect(manifestBoundsCalculator.getEstimatedMinimumSegmentTime()).toEqual(0);
37+
expect(manifestBoundsCalculator.getEstimatedMinimumSegmentTime(0)).toEqual(0);
38+
expect(manifestBoundsCalculator.getEstimatedMinimumSegmentTime(5)).toEqual(0);
39+
expect(manifestBoundsCalculator.getEstimatedMinimumSegmentTime(28)).toEqual(0);
4040
});
4141

4242
it("should return `false` through `lastPositionIsKnown` if `setLastPositionOffset` was never called", () => {
@@ -47,7 +47,7 @@ describe("DASH parsers - ManifestBoundsCalculator", () => {
4747
serverTimestampOffset: undefined,
4848
});
4949
expect(manifestBoundsCalculator.lastPositionIsKnown()).toEqual(false);
50-
expect(manifestBoundsCalculator.getEstimatedMinimumSegmentTime()).toEqual(undefined);
50+
expect(manifestBoundsCalculator.getEstimatedMinimumSegmentTime(0)).toEqual(undefined);
5151
expect(manifestBoundsCalculator.lastPositionIsKnown()).toEqual(false);
5252
});
5353

@@ -86,9 +86,10 @@ describe("DASH parsers - ManifestBoundsCalculator", () => {
8686
});
8787
manifestBoundsCalculator.setLastPosition(1000, 10);
8888
performanceNow = 25000;
89-
expect(manifestBoundsCalculator.getEstimatedMinimumSegmentTime()).toEqual(1010);
89+
expect(manifestBoundsCalculator.getEstimatedMinimumSegmentTime(0)).toEqual(1010);
90+
expect(manifestBoundsCalculator.getEstimatedMinimumSegmentTime(50)).toEqual(960);
9091
performanceNow = 35000;
91-
expect(manifestBoundsCalculator.getEstimatedMinimumSegmentTime()).toEqual(1020);
92+
expect(manifestBoundsCalculator.getEstimatedMinimumSegmentTime(0)).toEqual(1020);
9293
mockPerformanceNow.mockRestore();
9394
});
9495

@@ -104,18 +105,27 @@ describe("DASH parsers - ManifestBoundsCalculator", () => {
104105
serverTimestampOffset: 7000,
105106
});
106107
manifestBoundsCalculator.setLastPosition(3000, 10);
107-
expect(manifestBoundsCalculator.getEstimatedMinimumSegmentTime()).toEqual(
108+
expect(manifestBoundsCalculator.getEstimatedMinimumSegmentTime(0)).toEqual(
108109
7 + 5 - 4 - 3,
109110
);
111+
expect(manifestBoundsCalculator.getEstimatedMinimumSegmentTime(4)).toEqual(
112+
7 + 5 - 4 - 3 - 4,
113+
);
110114
performanceNow = 25000;
111-
expect(manifestBoundsCalculator.getEstimatedMinimumSegmentTime()).toEqual(
115+
expect(manifestBoundsCalculator.getEstimatedMinimumSegmentTime(0)).toEqual(
112116
7 + 25 - 4 - 3,
113117
);
118+
expect(manifestBoundsCalculator.getEstimatedMinimumSegmentTime(10)).toEqual(
119+
7 + 25 - 4 - 3 - 10,
120+
);
114121
performanceNow = 35000;
115122
manifestBoundsCalculator.setLastPosition(84546464, 5642);
116-
expect(manifestBoundsCalculator.getEstimatedMinimumSegmentTime()).toEqual(
123+
expect(manifestBoundsCalculator.getEstimatedMinimumSegmentTime(0)).toEqual(
117124
7 + 35 - 4 - 3,
118125
);
126+
expect(manifestBoundsCalculator.getEstimatedMinimumSegmentTime(5)).toEqual(
127+
7 + 35 - 4 - 3 - 5,
128+
);
119129
mockPerformanceNow.mockRestore();
120130
});
121131

@@ -132,10 +142,12 @@ describe("DASH parsers - ManifestBoundsCalculator", () => {
132142
});
133143
manifestBoundsCalculator.setLastPosition(1000, 0);
134144
performanceNow = 50000;
135-
expect(manifestBoundsCalculator.getEstimatedMinimumSegmentTime()).toEqual(1045);
145+
expect(manifestBoundsCalculator.getEstimatedMinimumSegmentTime(0)).toEqual(1045);
146+
expect(manifestBoundsCalculator.getEstimatedMinimumSegmentTime(40)).toEqual(1005);
136147
manifestBoundsCalculator.setLastPosition(0, 0);
137148
performanceNow = 55000;
138-
expect(manifestBoundsCalculator.getEstimatedMinimumSegmentTime()).toEqual(50);
149+
expect(manifestBoundsCalculator.getEstimatedMinimumSegmentTime(0)).toEqual(50);
150+
expect(manifestBoundsCalculator.getEstimatedMinimumSegmentTime(40)).toEqual(10);
139151
mockPerformanceNow.mockRestore();
140152
});
141153

src/parsers/manifest/dash/common/indexes/template.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -557,7 +557,9 @@ export default class TemplateRepresentationIndex implements IRepresentationIndex
557557
}
558558

559559
const { duration, timescale } = this._index;
560-
const firstPosition = this._manifestBoundsCalculator.getEstimatedMinimumSegmentTime();
560+
const firstPosition = this._manifestBoundsCalculator.getEstimatedMinimumSegmentTime(
561+
duration / timescale,
562+
);
561563
if (firstPosition === undefined) {
562564
return undefined;
563565
}

src/parsers/manifest/dash/common/indexes/timeline/timeline_representation_index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -796,7 +796,9 @@ export default class TimelineRepresentationIndex implements IRepresentationIndex
796796
if (!this._isDynamic) {
797797
return;
798798
}
799-
const firstPosition = this._manifestBoundsCalculator.getEstimatedMinimumSegmentTime();
799+
const firstPosition = this._manifestBoundsCalculator.getEstimatedMinimumSegmentTime(
800+
(this._index.timeline[0]?.duration ?? 0) / this._index.timescale,
801+
);
800802
if (isNullOrUndefined(firstPosition)) {
801803
return; // we don't know yet
802804
}

src/parsers/manifest/dash/common/manifest_bounds_calculator.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -109,10 +109,14 @@ export default class ManifestBoundsCalculator {
109109
/**
110110
* Estimate a minimum bound for the content from the last set segment time
111111
* and buffer depth.
112-
* Consider that it is only an estimation, not the real value.
112+
* Consider that it is only an estimate, not the real value.
113+
* @param {number} segmentDuration - In DASH, the buffer depth actually also
114+
* depend on a corresponding's segment duration (e.g. a segment become
115+
* unavailable once the `timeShiftBufferDepth` + its duration has elapsed).
116+
* This argument can thus be set the approximate duration of a segment.
113117
* @return {number|undefined}
114118
*/
115-
getEstimatedMinimumSegmentTime(): number | undefined {
119+
getEstimatedMinimumSegmentTime(segmentDuration: number): number | undefined {
116120
if (!this._isDynamic || this._timeShiftBufferDepth === null) {
117121
return 0;
118122
}
@@ -121,7 +125,7 @@ export default class ManifestBoundsCalculator {
121125
if (maximumBound === undefined) {
122126
return undefined;
123127
}
124-
const minimumBound = maximumBound - this._timeShiftBufferDepth;
128+
const minimumBound = maximumBound - (this._timeShiftBufferDepth + segmentDuration);
125129
return minimumBound;
126130
}
127131

src/parsers/manifest/dash/common/parse_mpd.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,7 @@ function parseCompleteIntermediateRepresentation(
258258
args.referenceDateTime,
259259
);
260260
const timeShiftBufferDepth = rootAttributes.timeShiftBufferDepth;
261+
const maxSegmentDuration = rootAttributes.maxSegmentDuration;
261262
const { externalClockOffset: clockOffset, unsafelyBaseOnPreviousManifest } = args;
262263

263264
const { externalClockOffset } = args;
@@ -279,7 +280,6 @@ function parseCompleteIntermediateRepresentation(
279280
manifestBoundsCalculator,
280281
manifestProfiles: mpdIR.attributes.profiles,
281282
receivedTime: args.manifestReceivedTime,
282-
timeShiftBufferDepth,
283283
unsafelyBaseOnPreviousManifest,
284284
xlinkInfos,
285285
xmlNamespaces: mpdIR.attributes.namespaces,
@@ -381,6 +381,20 @@ function parseCompleteIntermediateRepresentation(
381381
// can go even lower in terms of depth
382382
minimumTime = minimumSafePosition;
383383
timeshiftDepth = timeShiftBufferDepth ?? null;
384+
if (timeshiftDepth !== null) {
385+
// The DASH spec implies that a segment is still available after a given
386+
// `timeShiftBufferDepth` for a time equal to its duration
387+
// (What I interpret from "ISO/IEC 23009-1 fifth edition 2022-08
388+
// A.3.4 Media Segment list restrictions).
389+
//
390+
// This `timeshiftDepth` property is global for the whole Manifest (and
391+
// not per segment), thus we cannot do exactly that, but we can take the
392+
// anounced `maxSegmentDuration` by default instead. This may be a little
393+
// too optimistic, but would in reality not lead to a lot of issues as
394+
// this `timeshiftDepth` property is not the one that should be relied on
395+
// to know which segment can or cannot be requested anymore.
396+
timeshiftDepth += maxSegmentDuration ?? 0;
397+
}
384398
if (
385399
timeshiftDepth !== null &&
386400
minimumTime !== undefined &&

tests/integration/scenarios/dash_live.test.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -250,8 +250,8 @@ describe("DASH live content (SegmentTimeline)", function () {
250250
});
251251

252252
expect(manifestLoaderCalledTimes).to.equal(1);
253-
await checkAfterSleepWithBackoff(null, () => {
254-
expect(player.getMinimumPosition()).to.be.closeTo(1527507768, 1);
253+
await checkAfterSleepWithBackoff({ maxTimeMs: 2000 }, () => {
254+
expect(player.getMinimumPosition()).to.be.closeTo(1527507763, 1);
255255
});
256256
});
257257
});

tests/integration/scenarios/dash_live_multi_periods.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ describe("DASH live content multi-periods (SegmentTemplate)", function () {
5757
const maxPos = player.getMaximumPosition();
5858
expect(maxPos).to.be.closeTo(now, 2);
5959
const minPos = player.getMinimumPosition();
60-
expect(minPos).to.be.closeTo(now - manifestInfos.tsbd, 2);
60+
expect(minPos).to.be.closeTo(now - manifestInfos.tsbd - 2, 2);
6161
expect(manifestLoaderCalledTimes).to.equal(1);
6262
});
6363
});

tests/integration/scenarios/dash_live_utc_timings.test.js

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ describe("DASH live - UTCTimings", () => {
3131
{ maxTimeMs: 1000 },
3232
{
3333
resolveWhen() {
34-
expect(player.getMinimumPosition()).to.be.closeTo(1553521448, 3);
34+
expect(player.getMinimumPosition()).to.be.closeTo(1553521446, 3);
3535
expect(player.getMaximumPosition()).to.be.closeTo(1553521748, 3);
3636
},
3737
untilSuccess() {
@@ -56,7 +56,7 @@ describe("DASH live - UTCTimings", () => {
5656
{ maxTimeMs: 1000 },
5757
{
5858
resolveWhen() {
59-
expect(player.getMinimumPosition()).to.be.closeTo(1553521748, 1);
59+
expect(player.getMinimumPosition()).to.be.closeTo(1553521746, 1);
6060
expect(player.getMaximumPosition()).to.be.closeTo(1553522048, 1);
6161
},
6262
untilSuccess() {
@@ -95,7 +95,7 @@ describe("DASH live - UTCTimings", () => {
9595
{ maxTimeMs: 1000 },
9696
{
9797
resolveWhen() {
98-
expect(player.getMinimumPosition()).to.be.closeTo(1558791848, 3);
98+
expect(player.getMinimumPosition()).to.be.closeTo(1558791846, 3);
9999
},
100100
untilSuccess() {
101101
expect(player.getMinimumPosition()).to.equal(null);
@@ -119,7 +119,7 @@ describe("DASH live - UTCTimings", () => {
119119
{ maxTimeMs: 1000 },
120120
{
121121
resolveWhen() {
122-
expect(player.getMinimumPosition()).to.be.closeTo(1553521748, 1);
122+
expect(player.getMinimumPosition()).to.be.closeTo(1553521746, 1);
123123
expect(player.getMaximumPosition()).to.be.closeTo(1553522048, 1);
124124
},
125125
untilSuccess() {
@@ -158,7 +158,7 @@ describe("DASH live - UTCTimings", () => {
158158
Date.now() / 1000 - manifestInfos.availabilityStartTime;
159159
const minimumPosition = maximumPosition - timeShiftBufferDepth;
160160

161-
expect(player.getMinimumPosition()).to.be.closeTo(minimumPosition, 3);
161+
expect(player.getMinimumPosition()).to.be.closeTo(minimumPosition - 2, 3);
162162
expect(player.getMaximumPosition()).to.be.closeTo(maximumPosition, 3);
163163
},
164164
untilSuccess() {
@@ -184,7 +184,7 @@ describe("DASH live - UTCTimings", () => {
184184
{ maxTimeMs: 1000 },
185185
{
186186
resolveWhen() {
187-
expect(player.getMinimumPosition()).to.be.closeTo(1553521748, 1);
187+
expect(player.getMinimumPosition()).to.be.closeTo(1553521746, 1);
188188
expect(player.getMaximumPosition()).to.be.closeTo(1553522048, 1);
189189
},
190190
untilSuccess() {
@@ -218,7 +218,7 @@ describe("DASH live - UTCTimings", () => {
218218
{ maxTimeMs: 1000 },
219219
{
220220
resolveWhen() {
221-
expect(player.getMinimumPosition()).to.be.closeTo(1553521448, 3);
221+
expect(player.getMinimumPosition()).to.be.closeTo(1553521446, 3);
222222
},
223223
untilSuccess() {
224224
expect(player.getMinimumPosition()).to.equal(null);
@@ -242,7 +242,7 @@ describe("DASH live - UTCTimings", () => {
242242
{ maxTimeMs: 1000 },
243243
{
244244
resolveWhen() {
245-
expect(player.getMinimumPosition()).to.be.closeTo(1553521748, 1);
245+
expect(player.getMinimumPosition()).to.be.closeTo(1553521746, 1);
246246
expect(player.getMaximumPosition()).to.be.closeTo(1553522048, 1);
247247
},
248248
untilSuccess() {

0 commit comments

Comments
 (0)