diff --git a/package-lock.json b/package-lock.json
index 679b0455e..fd1df5778 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -77,6 +77,16 @@
}
}
},
+ "@videojs/vhs-utils": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@videojs/vhs-utils/-/vhs-utils-1.2.1.tgz",
+ "integrity": "sha512-lYakbWMtmJ1ih8Q8pSyv8Mj6IJch0J+h9D9LQQdvNfKR6a47hsmYgoiHPafcl19gV0h7NHtzunEcWdxVlVJDjQ==",
+ "requires": {
+ "@babel/runtime": "^7.5.5",
+ "global": "^4.3.2",
+ "url-toolkit": "^2.1.6"
+ }
+ },
"JSONStream": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz",
@@ -3029,7 +3039,8 @@
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
"integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
- "dev": true
+ "dev": true,
+ "optional": true
},
"aproba": {
"version": "1.2.0",
@@ -3053,13 +3064,15 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
- "dev": true
+ "dev": true,
+ "optional": true
},
"brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
+ "optional": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@@ -3076,19 +3089,22 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz",
"integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=",
- "dev": true
+ "dev": true,
+ "optional": true
},
"concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
- "dev": true
+ "dev": true,
+ "optional": true
},
"console-control-strings": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
"integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=",
- "dev": true
+ "dev": true,
+ "optional": true
},
"core-util-is": {
"version": "1.0.2",
@@ -3219,7 +3235,8 @@
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=",
- "dev": true
+ "dev": true,
+ "optional": true
},
"ini": {
"version": "1.3.5",
@@ -3233,6 +3250,7 @@
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
"integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
"dev": true,
+ "optional": true,
"requires": {
"number-is-nan": "^1.0.0"
}
@@ -3249,6 +3267,7 @@
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
"dev": true,
+ "optional": true,
"requires": {
"brace-expansion": "^1.1.7"
}
@@ -3257,13 +3276,15 @@
"version": "0.0.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
"integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=",
- "dev": true
+ "dev": true,
+ "optional": true
},
"minipass": {
"version": "2.3.5",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-2.3.5.tgz",
"integrity": "sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA==",
"dev": true,
+ "optional": true,
"requires": {
"safe-buffer": "^5.1.2",
"yallist": "^3.0.0"
@@ -3284,6 +3305,7 @@
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
"integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
"dev": true,
+ "optional": true,
"requires": {
"minimist": "0.0.8"
}
@@ -3372,7 +3394,8 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz",
"integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=",
- "dev": true
+ "dev": true,
+ "optional": true
},
"object-assign": {
"version": "4.1.1",
@@ -3386,6 +3409,7 @@
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
"dev": true,
+ "optional": true,
"requires": {
"wrappy": "1"
}
@@ -3481,7 +3505,8 @@
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
- "dev": true
+ "dev": true,
+ "optional": true
},
"safer-buffer": {
"version": "2.1.2",
@@ -3523,6 +3548,7 @@
"resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
"integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
"dev": true,
+ "optional": true,
"requires": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",
@@ -3544,6 +3570,7 @@
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
"integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
"dev": true,
+ "optional": true,
"requires": {
"ansi-regex": "^2.0.0"
}
@@ -3592,13 +3619,15 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
- "dev": true
+ "dev": true,
+ "optional": true
},
"yallist": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz",
"integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==",
- "dev": true
+ "dev": true,
+ "optional": true
}
}
},
@@ -5857,12 +5886,14 @@
"dev": true
},
"mpd-parser": {
- "version": "0.8.1",
- "resolved": "https://registry.npmjs.org/mpd-parser/-/mpd-parser-0.8.1.tgz",
- "integrity": "sha512-WBTJ1bKk8OLUIxBh6s1ju1e2yz/5CzhPbgi6P3F3kJHKhGy1Z+ElvEnuzEbtC/dnbRcJtMXazE3f93N5LLdp9Q==",
+ "version": "0.10.0",
+ "resolved": "https://registry.npmjs.org/mpd-parser/-/mpd-parser-0.10.0.tgz",
+ "integrity": "sha512-eIqkH/2osPr7tIIjhRmDWqm2wdJ7Q8oPfWvdjealzsLV2D2oNe0a0ae2gyYYs1sw5e5hdssDA2V6Sz8MW+Uvvw==",
"requires": {
+ "@babel/runtime": "^7.5.5",
+ "@videojs/vhs-utils": "^1.1.0",
"global": "^4.3.2",
- "url-toolkit": "^2.1.1"
+ "xmldom": "^0.1.27"
}
},
"ms": {
@@ -8803,6 +8834,11 @@
"integrity": "sha512-MjGsXhKG8YjTKrDCXseFo3ClbMGvUD4en29H2Cev1dv4P/chlpw6KdYmlCWDkhosBVKRDjM836+3e3pm1cBNJA==",
"dev": true
},
+ "xmldom": {
+ "version": "0.1.31",
+ "resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.1.31.tgz",
+ "integrity": "sha512-yS2uJflVQs6n+CyjHoaBmVSqIDevTAWrzMmjG1Gc7h1qQ7uVozNhEPJAwZXWyGQ/Gafo3fCwrcaokezLPupVyQ=="
+ },
"xmlhttprequest-ssl": {
"version": "1.5.3",
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.3.tgz",
diff --git a/package.json b/package.json
index 95d7cacb1..c4ad6c623 100644
--- a/package.json
+++ b/package.json
@@ -71,7 +71,7 @@
"aes-decrypter": "3.0.0",
"global": "^4.3.0",
"m3u8-parser": "4.4.0",
- "mpd-parser": "0.8.1",
+ "mpd-parser": "0.10.0",
"mux.js": "5.4.0",
"url-toolkit": "^2.1.3",
"video.js": "^6.8.0 || ^7.0.0"
diff --git a/src/master-playlist-controller.js b/src/master-playlist-controller.js
index c5678d87f..04cd14546 100644
--- a/src/master-playlist-controller.js
+++ b/src/master-playlist-controller.js
@@ -937,7 +937,9 @@ export class MasterPlaylistController extends videojs.EventTarget {
return;
}
- mainSeekable = Hls.Playlist.seekable(media, expired);
+ const suggestedPresentationDelay = this.masterPlaylistLoader_.master.suggestedPresentationDelay
+
+ mainSeekable = Hls.Playlist.seekable(media, expired, suggestedPresentationDelay);
if (mainSeekable.length === 0) {
return;
@@ -951,7 +953,7 @@ export class MasterPlaylistController extends videojs.EventTarget {
return;
}
- audioSeekable = Hls.Playlist.seekable(media, expired);
+ audioSeekable = Hls.Playlist.seekable(media, expired, suggestedPresentationDelay);
if (audioSeekable.length === 0) {
return;
diff --git a/src/playlist.js b/src/playlist.js
index 7d57fdc44..b8c11283a 100644
--- a/src/playlist.js
+++ b/src/playlist.js
@@ -216,21 +216,36 @@ export const sumDurations = function(playlist, startIndex, endIndex) {
* window which is the duration of the last segment plus 2 target durations from the end
* of the playlist.
*
+ * A liveEdgePadding can be provided which will be used instead of calculating the safe live edge.
+ * This corresponds to suggestedPresentationDelay in DASH manifests.
+ *
* @param {Object} playlist
* a media playlist object
+ * @param {Number} [liveEdgePadding]
+ * A number in seconds indicating how far from the end we want to be.
+ * If provided, this value is used instead of calculating the safe live index from the target durations.
+ * Corresponds to suggestedPresentationDelay in DASH manifests.
* @return {Number}
* The media index of the segment at the safe live point. 0 if there is no "safe"
* point.
* @function safeLiveIndex
*/
-export const safeLiveIndex = function(playlist) {
+export const safeLiveIndex = function(playlist, liveEdgePadding) {
if (!playlist.segments.length) {
return 0;
}
- let i = playlist.segments.length - 1;
- let distanceFromEnd = playlist.segments[i].duration || playlist.targetDuration;
- const safeDistance = distanceFromEnd + playlist.targetDuration * 2;
+ let i = playlist.segments.length;
+ let lastSegmentDuration = playlist.segments[i - 1].duration || playlist.targetDuration;
+ const safeDistance = typeof liveEdgePadding === 'number' ?
+ liveEdgePadding :
+ lastSegmentDuration + playlist.targetDuration * 2;
+
+ if (safeDistance === 0) {
+ return i;
+ }
+
+ let distanceFromEnd = 0;
while (i--) {
distanceFromEnd += playlist.segments[i].duration;
@@ -253,10 +268,16 @@ export const safeLiveIndex = function(playlist) {
* playlist end calculation should consider the safe live end
* (truncate the playlist end by three segments). This is normally
* used for calculating the end of the playlist's seekable range.
+ * This takes into account the value of liveEdgePadding.
+ * Setting liveEdgePadding to 0 is equivalent to setting this to false.
+ * @param {Number} liveEdgePadding a number indicating how far from the end of the playlist we should be in seconds.
+ * If this is provided, it is used in the safe live end calculation.
+ * Setting useSafeLiveEnd=false or liveEdgePadding=0 are equivalent.
+ * Corresponds to suggestedPresentationDelay in DASH manifests.
* @returns {Number} the end time of playlist
* @function playlistEnd
*/
-export const playlistEnd = function(playlist, expired, useSafeLiveEnd) {
+export const playlistEnd = function(playlist, expired, useSafeLiveEnd, liveEdgePadding) {
if (!playlist || !playlist.segments) {
return null;
}
@@ -270,7 +291,7 @@ export const playlistEnd = function(playlist, expired, useSafeLiveEnd) {
expired = expired || 0;
- const endSequence = useSafeLiveEnd ? safeLiveIndex(playlist) : playlist.segments.length;
+ const endSequence = useSafeLiveEnd ? safeLiveIndex(playlist, liveEdgePadding) : playlist.segments.length;
return intervalDuration(playlist,
playlist.mediaSequence + endSequence,
@@ -289,13 +310,15 @@ export const playlistEnd = function(playlist, expired, useSafeLiveEnd) {
* dropped off the front of the playlist in a live scenario
* @param {Number=} expired the amount of time that has
* dropped off the front of the playlist in a live scenario
+ * @param {Number} liveEdgePadding how far from the end of the playlist we should be in seconds.
+ * Corresponds to suggestedPresentationDelay in DASH manifests.
* @return {TimeRanges} the periods of time that are valid targets
* for seeking
*/
-export const seekable = function(playlist, expired) {
+export const seekable = function(playlist, expired, liveEdgePadding) {
let useSafeLiveEnd = true;
let seekableStart = expired || 0;
- let seekableEnd = playlistEnd(playlist, expired, useSafeLiveEnd);
+ let seekableEnd = playlistEnd(playlist, expired, useSafeLiveEnd, liveEdgePadding);
if (seekableEnd === null) {
return createTimeRange();
diff --git a/test/master-playlist-controller.test.js b/test/master-playlist-controller.test.js
index d5c5de5ef..61290c2cd 100644
--- a/test/master-playlist-controller.test.js
+++ b/test/master-playlist-controller.test.js
@@ -1839,6 +1839,7 @@ function(assert) {
let mainTimeRanges = [];
let audioTimeRanges = [];
+ this.masterPlaylistController.masterPlaylistLoader_.master = {};
this.masterPlaylistController.masterPlaylistLoader_.media = () => mainMedia;
this.masterPlaylistController.syncController_.getExpiredTime = () => 0;
@@ -1973,6 +1974,7 @@ function(assert) {
Playlist.seekable = () => {
return videojs.createTimeRanges(mainTimeRanges);
};
+ this.masterPlaylistController.masterPlaylistLoader_.master = {};
this.masterPlaylistController.masterPlaylistLoader_.media = () => media;
this.masterPlaylistController.syncController_.getExpiredTime = () => 0;
diff --git a/test/playlist.test.js b/test/playlist.test.js
index 1193cae4e..1f3d6ed56 100644
--- a/test/playlist.test.js
+++ b/test/playlist.test.js
@@ -492,6 +492,116 @@ QUnit.test('safeLiveIndex is 0 when no safe live point', function(assert) {
'returns media index 0 when playlist has no safe live point');
});
+QUnit.test('safeLiveIndex accounts for liveEdgePadding in simple case', function(assert) {
+ const playlist = {
+ targetDuration: 6,
+ mediaSequence: 10,
+ syncInfo: {
+ time: 0,
+ mediaSequence: 10
+ },
+ segments: [
+ {
+ duration: 6
+ },
+ {
+ duration: 6
+ },
+ {
+ duration: 6
+ },
+ {
+ duration: 6
+ },
+ {
+ duration: 6
+ },
+ {
+ duration: 6
+ }
+ ]
+ };
+
+ assert.equal(Playlist.safeLiveIndex(playlist, 36), 0,
+ 'returns 0 when liveEdgePadding is 30 and duration is 6');
+
+ assert.equal(Playlist.safeLiveIndex(playlist, 30), 1,
+ 'returns 1 when liveEdgePadding is 30 and duration is 6');
+
+ assert.equal(Playlist.safeLiveIndex(playlist, 24), 2,
+ 'returns 2 when liveEdgePadding is 24 and duration is 6');
+
+ assert.equal(Playlist.safeLiveIndex(playlist, 18), 3,
+ 'returns 3 when liveEdgePadding is 18 and duration is 6');
+
+ assert.equal(Playlist.safeLiveIndex(playlist, 12), 4,
+ 'returns 4 when liveEdgePadding is 12 and duration is 6');
+
+ assert.equal(Playlist.safeLiveIndex(playlist, 6), 5,
+ 'returns 5 when liveEdgePadding is 6 and duration is 6');
+
+ assert.equal(Playlist.safeLiveIndex(playlist, 0), 6,
+ 'returns 6 when liveEdgePadding is 0 and duration is 6');
+});
+
+QUnit.test('safeLiveIndex accounts for liveEdgePadding in non-simple case', function(assert) {
+ const playlist = {
+ targetDuration: 6,
+ mediaSequence: 10,
+ syncInfo: {
+ time: 0,
+ mediaSequence: 10
+ },
+ segments: [
+ {
+ duration: 3
+ },
+ {
+ duration: 6
+ },
+ {
+ duration: 6
+ },
+ {
+ duration: 3
+ },
+ {
+ duration: 3
+ },
+ {
+ duration: 0.5
+ }
+ ]
+ };
+
+ assert.equal(Playlist.safeLiveIndex(playlist, 24), 0,
+ 'returns 0 when liveEdgePadding is 24');
+
+ assert.equal(Playlist.safeLiveIndex(playlist, 18), 1,
+ 'returns 1 when liveEdgePadding is 18');
+
+ assert.equal(Playlist.safeLiveIndex(playlist, 12), 2,
+ 'returns 2 when liveEdgePadding is 12');
+
+ assert.equal(Playlist.safeLiveIndex(playlist, 6), 3,
+ 'returns 3 when liveEdgePadding is 6');
+
+ assert.equal(Playlist.safeLiveIndex(playlist, 4), 3,
+ 'returns 3 when liveEdgePadding is 4');
+
+ assert.equal(Playlist.safeLiveIndex(playlist, 1), 4,
+ 'returns 4 when liveEdgePadding is 1');
+
+ assert.equal(Playlist.safeLiveIndex(playlist, 0.5), 5,
+ 'returns 5 when liveEdgePadding is 0.5');
+
+ assert.equal(Playlist.safeLiveIndex(playlist, 0.25), 5,
+ 'returns 5 when liveEdgePadding is 0.25');
+
+ assert.equal(Playlist.safeLiveIndex(playlist, 0), 6,
+ 'returns 6 when liveEdgePadding is 0');
+});
+
QUnit.test(
'seekable end and playlist end account for non-zero starting VOD media sequence',
function(assert) {
diff --git a/utils/stats/index.html b/utils/stats/index.html
index ea3173757..10abeac11 100644
--- a/utils/stats/index.html
+++ b/utils/stats/index.html
@@ -20,7 +20,7 @@
-
+