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} */