Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(DASH): Add support for service descriptions #5394

Merged
merged 9 commits into from
Jul 5, 2023
26 changes: 25 additions & 1 deletion externs/shaka/manifest.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@
* minBufferTime: number,
* sequenceMode: boolean,
* ignoreManifestTimestampsInSegmentsMode: boolean,
* type: string
* type: string,
* serviceDescription: ?shaka.extern.ServiceDescription
* }}
*
* @description
Expand Down Expand Up @@ -89,6 +90,9 @@
* @property {string} type
* Indicates the type of the manifest. It can be <code>'HLS'</code> or
* <code>'DASH'</code>.
* @property {?shaka.extern.ServiceDescription} serviceDescription
* The service description for the manifest. Used to adapt playbackRate to
* decrease latency.
*
* @exportDoc
*/
Expand Down Expand Up @@ -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 {{
Expand Down
34 changes: 34 additions & 0 deletions lib/dash/dash_parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions lib/hls/hls_parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -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_);
}
Expand Down
1 change: 1 addition & 0 deletions lib/mss/mss_parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions lib/offline/manifest_converter.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ shaka.offline.ManifestConverter = class {
sequenceMode: manifestDB.sequenceMode || false,
ignoreManifestTimestampsInSegmentsMode: false,
type: manifestDB.type || shaka.media.ManifestParser.UNKNOWN,
serviceDescription: null,
};
}

Expand Down
29 changes: 25 additions & 4 deletions lib/player.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
avelad marked this conversation as resolved.
Show resolved Hide resolved
const onTimeUpdate = () => this.onTimeUpdate_();
this.loadEventManager_.listen(mediaElement, 'timeupdate', onTimeUpdate);
}
Expand Down Expand Up @@ -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;
Expand All @@ -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) {
Expand Down
22 changes: 22 additions & 0 deletions test/dash/dash_parser_manifest_unit.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
'<MPD minBufferTime="PT75S" type="dynamic"',
' availabilityStartTime="1970-01-01T00:00:00Z">',
' <ServiceDescription id="0">',
' <Latency max="2000" min="2000" referenceId="0" target="4000" />',
' <PlaybackRate max="1.10" min="0.96" />',
' </ServiceDescription>',
'</MPD>',
].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);
});
});
});
1 change: 1 addition & 0 deletions test/media/playhead_unit.js
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ describe('Playhead', () => {
sequenceMode: false,
ignoreManifestTimestampsInSegmentsMode: false,
type: 'UNKNOWN',
serviceDescription: null,
};

config = shaka.util.PlayerConfiguration.createDefault().streaming;
Expand Down
1 change: 1 addition & 0 deletions test/media/streaming_engine_integration.js
Original file line number Diff line number Diff line change
Expand Up @@ -597,6 +597,7 @@ describe('StreamingEngine', () => {
sequenceMode: false,
ignoreManifestTimestampsInSegmentsMode: false,
type: 'UNKNOWN',
serviceDescription: null,
variants: [{
id: 1,
video: {
Expand Down
3 changes: 3 additions & 0 deletions test/test/util/manifest_generator.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions test/test/util/streaming_engine_util.js
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,7 @@ shaka.test.StreamingEngineUtil = class {
sequenceMode: false,
ignoreManifestTimestampsInSegmentsMode: false,
type: 'UNKNOWN',
serviceDescription: null,
};

/** @type {shaka.extern.Variant} */
Expand Down