diff --git a/externs/shaka/manifest.js b/externs/shaka/manifest.js
index 9615f308bc..67ebeb9965 100644
--- a/externs/shaka/manifest.js
+++ b/externs/shaka/manifest.js
@@ -20,7 +20,8 @@
* minBufferTime: number,
* sequenceMode: boolean,
* ignoreManifestTimestampsInSegmentsMode: boolean,
- * type: string
+ * type: string,
+ * serviceDescription: ?shaka.extern.ServiceDescription
* }}
*
* @description
@@ -89,6 +90,9 @@
* @property {string} type
* Indicates the type of the manifest. It can be 'HLS'
or
* 'DASH'
.
+ * @property {?shaka.extern.ServiceDescription} serviceDescription
+ * The service description for the manifest. Used to adapt playbackRate to
+ * decrease latency.
*
* @exportDoc
*/
@@ -118,6 +122,26 @@ shaka.extern.Manifest;
*/
shaka.extern.InitDataOverride;
+/**
+ * @typedef {{
+ * maxLatency: ?number,
+ * maxPlaybackRate: ?number
+ * }}
+ *
+ * @description
+ * Maximum latency and playback rate for a manifest. When max latency is reached
+ * playbackrate is updated to maxPlaybackRate to decrease latency.
+ * More information {@link https://dashif.org/docs/CR-Low-Latency-Live-r8.pdf here}.
+ *
+ * @property {?number} maxLatency
+ * Maximum latency in seconds.
+ * @property {?number} maxPlaybackRate
+ * Maximum playback rate.
+ *
+ * @exportDoc
+ */
+shaka.extern.ServiceDescription;
+
/**
* @typedef {{
diff --git a/lib/dash/dash_parser.js b/lib/dash/dash_parser.js
index aff6e43444..8454130134 100644
--- a/lib/dash/dash_parser.js
+++ b/lib/dash/dash_parser.js
@@ -509,6 +509,7 @@ shaka.dash.DashParser = class {
sequenceMode: this.config_.dash.sequenceMode,
ignoreManifestTimestampsInSegmentsMode: false,
type: shaka.media.ManifestParser.DASH,
+ serviceDescription: this.parseServiceDescription_(mpd),
};
// We only need to do clock sync when we're using presentation start
@@ -547,6 +548,39 @@ shaka.dash.DashParser = class {
this.playerInterface_.makeTextStreamsForClosedCaptions(this.manifest_);
}
+ /**
+ * Reads maxLatency and maxPlaybackRate properties from service
+ * description element.
+ *
+ * @param {!Element} mpd
+ * @return {?shaka.extern.ServiceDescription}
+ * @private
+ */
+ parseServiceDescription_(mpd) {
+ const XmlUtils = shaka.util.XmlUtils;
+ const elem = XmlUtils.findChild(mpd, 'ServiceDescription');
+
+ if (!elem ) {
+ return null;
+ }
+
+ const latencyNode = XmlUtils.findChild(elem, 'Latency');
+ const playbackRateNode = XmlUtils.findChild(elem, 'PlaybackRate');
+
+ if ((latencyNode && latencyNode.getAttribute('max')) || playbackRateNode) {
+ const maxLatency = latencyNode && latencyNode.getAttribute('max') ?
+ parseInt(latencyNode.getAttribute('max'), 10) / 1000 :
+ null;
+ const maxPlaybackRate = playbackRateNode ?
+ parseFloat(playbackRateNode.getAttribute('max')) :
+ null;
+
+ return {maxLatency, maxPlaybackRate};
+ }
+
+ return null;
+ }
+
/**
* Reads and parses the periods from the manifest. This first does some
* partial parsing so the start and duration is available when parsing
diff --git a/lib/hls/hls_parser.js b/lib/hls/hls_parser.js
index 869bcd0456..167a755c4c 100644
--- a/lib/hls/hls_parser.js
+++ b/lib/hls/hls_parser.js
@@ -801,6 +801,7 @@ shaka.hls.HlsParser = class {
ignoreManifestTimestampsInSegmentsMode:
this.config_.hls.ignoreManifestTimestampsInSegmentsMode,
type: shaka.media.ManifestParser.HLS,
+ serviceDescription: null,
};
this.playerInterface_.makeTextStreamsForClosedCaptions(this.manifest_);
}
diff --git a/lib/mss/mss_parser.js b/lib/mss/mss_parser.js
index 0f3260d740..6b99a056b2 100644
--- a/lib/mss/mss_parser.js
+++ b/lib/mss/mss_parser.js
@@ -376,6 +376,7 @@ shaka.mss.MssParser = class {
sequenceMode: this.config_.mss.sequenceMode,
ignoreManifestTimestampsInSegmentsMode: false,
type: shaka.media.ManifestParser.MSS,
+ serviceDescription: null,
};
// This is the first point where we have a meaningful presentation start
diff --git a/lib/offline/manifest_converter.js b/lib/offline/manifest_converter.js
index b85122c67c..3e56dab90e 100644
--- a/lib/offline/manifest_converter.js
+++ b/lib/offline/manifest_converter.js
@@ -91,6 +91,7 @@ shaka.offline.ManifestConverter = class {
sequenceMode: manifestDB.sequenceMode || false,
ignoreManifestTimestampsInSegmentsMode: false,
type: manifestDB.type || shaka.media.ManifestParser.UNKNOWN,
+ serviceDescription: null,
};
}
diff --git a/lib/player.js b/lib/player.js
index 82a09f3b2f..d7ecc9f9e9 100644
--- a/lib/player.js
+++ b/lib/player.js
@@ -2278,7 +2278,8 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
'arbitrary language initially');
}
- if (this.isLive() && this.config_.streaming.liveSync) {
+ if (this.isLive() && (this.config_.streaming.liveSync ||
+ this.manifest_.serviceDescription)) {
const onTimeUpdate = () => this.onTimeUpdate_();
this.loadEventManager_.listen(mediaElement, 'timeupdate', onTimeUpdate);
}
@@ -5728,8 +5729,24 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
// Bad stream?
return;
}
- const liveSyncMaxLatency = this.config_.streaming.liveSyncMaxLatency;
- const liveSyncPlaybackRate = this.config_.streaming.liveSyncPlaybackRate;
+
+ let liveSyncMaxLatency;
+ let liveSyncPlaybackRate;
+ if (this.config_.streaming.liveSync) {
+ liveSyncMaxLatency = this.config_.streaming.liveSyncMaxLatency;
+ liveSyncPlaybackRate = this.config_.streaming.liveSyncPlaybackRate;
+ } else {
+ // serviceDescription must override if it is defined in the MPD and
+ // liveSync configuration is not set.
+ if (this.manifest_ && this.manifest_.serviceDescription) {
+ liveSyncMaxLatency = this.manifest_.serviceDescription.maxLatency ||
+ this.config_.streaming.liveSyncMaxLatency;
+ liveSyncPlaybackRate =
+ this.manifest_.serviceDescription.maxPlaybackRate ||
+ this.config_.streaming.liveSyncPlaybackRate;
+ }
+ }
+
const playbackRate = this.video_.playbackRate;
const latency = seekRange.end - this.video_.currentTime;
let offset = 0;
@@ -5745,8 +5762,12 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
}
}
- if ((latency - offset) > liveSyncMaxLatency) {
+ if (liveSyncMaxLatency && liveSyncPlaybackRate &&
+ (latency - offset) > liveSyncMaxLatency) {
if (playbackRate != liveSyncPlaybackRate) {
+ shaka.log.debug('Latency (' + latency + 's) ' +
+ 'is greater than liveSyncMaxLatency (' + liveSyncMaxLatency + 's). ' +
+ 'Updating playbackRate to ' + liveSyncPlaybackRate);
this.trickPlay(liveSyncPlaybackRate);
}
} else if (playbackRate !== 1 && playbackRate !== 0) {
diff --git a/test/dash/dash_parser_manifest_unit.js b/test/dash/dash_parser_manifest_unit.js
index 9da853692d..9d635cf7d3 100644
--- a/test/dash/dash_parser_manifest_unit.js
+++ b/test/dash/dash_parser_manifest_unit.js
@@ -2556,4 +2556,26 @@ describe('DashParser Manifest', () => {
expect(segments[0][1].startTime).toBe(15);
expect(segments[1][1].startTime).toBe(15);
});
+
+ describe('Parses ServiceDescription', () => {
+ it('with PlaybackRate and Latency', async () => {
+ const source = [
+ '',
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ '',
+ ].join('\n');
+
+ fakeNetEngine.setResponseText('dummy://foo', source);
+
+ /** @type {shaka.extern.Manifest} */
+ const manifest = await parser.start('dummy://foo', playerInterface);
+
+ expect(manifest.serviceDescription.maxLatency).toBe(2);
+ expect(manifest.serviceDescription.maxPlaybackRate).toBe(1.1);
+ });
+ });
});
diff --git a/test/media/playhead_unit.js b/test/media/playhead_unit.js
index e09ddf3713..0b7cca091b 100644
--- a/test/media/playhead_unit.js
+++ b/test/media/playhead_unit.js
@@ -136,6 +136,7 @@ describe('Playhead', () => {
sequenceMode: false,
ignoreManifestTimestampsInSegmentsMode: false,
type: 'UNKNOWN',
+ serviceDescription: null,
};
config = shaka.util.PlayerConfiguration.createDefault().streaming;
diff --git a/test/media/streaming_engine_integration.js b/test/media/streaming_engine_integration.js
index 40982e8e20..d19e1c47c0 100644
--- a/test/media/streaming_engine_integration.js
+++ b/test/media/streaming_engine_integration.js
@@ -597,6 +597,7 @@ describe('StreamingEngine', () => {
sequenceMode: false,
ignoreManifestTimestampsInSegmentsMode: false,
type: 'UNKNOWN',
+ serviceDescription: null,
variants: [{
id: 1,
video: {
diff --git a/test/test/util/manifest_generator.js b/test/test/util/manifest_generator.js
index d766732823..ec750ef018 100644
--- a/test/test/util/manifest_generator.js
+++ b/test/test/util/manifest_generator.js
@@ -101,6 +101,9 @@ shaka.test.ManifestGenerator.Manifest = class {
this.ignoreManifestTimestampsInSegmentsMode = false;
/** @type {string} */
this.type = 'UNKNOWN';
+ /** @type {?shaka.extern.ServiceDescription} */
+ this.serviceDescription = null;
+
/** @type {shaka.extern.Manifest} */
const foo = this;
diff --git a/test/test/util/streaming_engine_util.js b/test/test/util/streaming_engine_util.js
index 353565ab90..9620c90523 100644
--- a/test/test/util/streaming_engine_util.js
+++ b/test/test/util/streaming_engine_util.js
@@ -286,6 +286,7 @@ shaka.test.StreamingEngineUtil = class {
sequenceMode: false,
ignoreManifestTimestampsInSegmentsMode: false,
type: 'UNKNOWN',
+ serviceDescription: null,
};
/** @type {shaka.extern.Variant} */