Skip to content

Commit

Permalink
feat(DASH): Add MPD Chaining support (#6641)
Browse files Browse the repository at this point in the history
Close #3926
  • Loading branch information
avelad committed May 21, 2024
1 parent ec4bc1d commit 82c5149
Show file tree
Hide file tree
Showing 20 changed files with 176 additions and 4 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ DASH features supported:
- WebVTT and TTML
- CEA-608/708 captions
- Multi-codec variants (on platforms with changeType support)
- MPD chaining

DASH features **not** supported:
- Xlink with actuate=onRequest
Expand Down
26 changes: 26 additions & 0 deletions demo/common/assets.js
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,9 @@ shakaAssets.Feature = {

// Set if the asset is VR.
VR: 'VR',

// Set if the asset has MPD Chaining.
MPD_CHAINING: 'MPD Chaining',
};


Expand Down Expand Up @@ -303,6 +306,20 @@ shakaAssets.testAssets = [
.addFeature(shakaAssets.Feature.SUBTITLES)
.addFeature(shakaAssets.Feature.WEBM)
.addFeature(shakaAssets.Feature.OFFLINE),
new ShakaDemoAssetInfo(
/* name= */ 'Angel One (multicodec, multilingual, mpd chaining)',
/* iconUri= */ 'https://storage.googleapis.com/shaka-asset-icons/angel_one.png',
/* manifestUri= */ 'https://storage.googleapis.com/shaka-demo-assets/angel-one/dash_chaining.mpd',
/* source= */ shakaAssets.Source.SHAKA)
.addDescription('A clip from a classic Star Trek TNG episode, presented in MPEG-DASH.')
.markAsFeatured('Angel One')
.addFeature(shakaAssets.Feature.DASH)
.addFeature(shakaAssets.Feature.MP4)
.addFeature(shakaAssets.Feature.MULTIPLE_LANGUAGES)
.addFeature(shakaAssets.Feature.SUBTITLES)
.addFeature(shakaAssets.Feature.WEBM)
.addFeature(shakaAssets.Feature.OFFLINE)
.addFeature(shakaAssets.Feature.MPD_CHAINING),
new ShakaDemoAssetInfo(
/* name= */ 'Angel One (multicodec, multilingual, Widevine)',
/* iconUri= */ 'https://storage.googleapis.com/shaka-asset-icons/angel_one.png',
Expand Down Expand Up @@ -994,6 +1011,15 @@ shakaAssets.testAssets = [
.addFeature(shakaAssets.Feature.MP4)
.addFeature(shakaAssets.Feature.LIVE)
.addFeature(shakaAssets.Feature.THUMBNAILS),
new ShakaDemoAssetInfo(
/* name= */ 'DASH-IF - Regular chaining, Live',
/* iconUri= */ '',
/* manifestUri= */ 'https://dash.akamaized.net/dash264/TestCasesIOP33/MPDChaining/regular_chain/1/manifest_regular_MPDChaining_live.mpd',
/* source= */ shakaAssets.Source.DASH_IF)
.addFeature(shakaAssets.Feature.DASH)
.addFeature(shakaAssets.Feature.MP4)
.addFeature(shakaAssets.Feature.LIVE)
.addFeature(shakaAssets.Feature.MPD_CHAINING),
// End DASH-IF Assets }}}

// bitcodin assets {{{
Expand Down
6 changes: 5 additions & 1 deletion demo/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -564,7 +564,11 @@ shakaDemo.Config = class {
.addBoolInput_('Use native HLS on Safari (Clear)',
'streaming.useNativeHlsOnSafari')
.addBoolInput_('Use native HLS for FairPlay',
'streaming.useNativeHlsForFairPlay');
'streaming.useNativeHlsForFairPlay')
.addNumberInput_('Time window at end to preload next URL',
'streaming.preloadNextUrlWindow',
/* canBeDecimal= */ true,
/* canBeZero= */ true);
this.addRetrySection_('streaming', 'Streaming Retry Parameters');
}

Expand Down
2 changes: 2 additions & 0 deletions demo/search.js
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,8 @@ shakaDemo.Search = class {
'Filters for assets that use Content Steering.');
this.makeBooleanInput_(specialContainer, Feature.VR, FEATURE,
'Filters for assets that are VR.');
this.makeBooleanInput_(specialContainer, Feature.MPD_CHAINING, FEATURE,
'Filters for assets that have MPD Chaining');

container.appendChild(this.resultsDiv_);
}
Expand Down
5 changes: 4 additions & 1 deletion externs/shaka/manifest.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@
* sequenceMode: boolean,
* ignoreManifestTimestampsInSegmentsMode: boolean,
* type: string,
* serviceDescription: ?shaka.extern.ServiceDescription
* serviceDescription: ?shaka.extern.ServiceDescription,
* nextUrl: ?string
* }}
*
* @description
Expand Down Expand Up @@ -93,6 +94,8 @@
* @property {?shaka.extern.ServiceDescription} serviceDescription
* The service description for the manifest. Used to adapt playbackRate to
* decrease latency.
* @property {?string} nextUrl
* The next url to play.
*
* @exportDoc
*/
Expand Down
9 changes: 8 additions & 1 deletion externs/shaka/player.js
Original file line number Diff line number Diff line change
Expand Up @@ -1252,7 +1252,8 @@ shaka.extern.ManifestConfiguration;
* vodDynamicPlaybackRate: boolean,
* vodDynamicPlaybackRateLowBufferRate: number,
* vodDynamicPlaybackRateBufferRatio: number,
* infiniteLiveStreamDuration: boolean
* infiniteLiveStreamDuration: boolean,
* preloadNextUrlWindow: number
* }}
*
* @description
Expand Down Expand Up @@ -1442,6 +1443,12 @@ shaka.extern.ManifestConfiguration;
* If <code>true</code>, the media source live duration
* set as a<code>Infinity</code>
* Defaults to <code> false </code>.
* @property {number} preloadNextUrlWindow
* The window of time at the end of the presentation to begin preloading the
* next URL, such as one specified by a urn:mpeg:dash:chaining:2016 element
* in DASH. Measured in seconds. If the value is 0, the next URL will not
* be preloaded at all.
* Defaults to <code> 30 </code>.
* @exportDoc
*/
shaka.extern.StreamingConfiguration;
Expand Down
27 changes: 27 additions & 0 deletions lib/dash/dash_parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -697,6 +697,7 @@ shaka.dash.DashParser = class {
ignoreManifestTimestampsInSegmentsMode: false,
type: shaka.media.ManifestParser.DASH,
serviceDescription: this.parseServiceDescription_(mpd),
nextUrl: this.parseMpdChaining_(mpd),
};

// We only need to do clock sync when we're using presentation start
Expand Down Expand Up @@ -782,6 +783,32 @@ shaka.dash.DashParser = class {
return null;
}

/**
* Reads chaining url.
*
* @param {!shaka.extern.xml.Node} mpd
* @return {?string}
* @private
*/
parseMpdChaining_(mpd) {
const TXml = shaka.util.TXml;
const supplementalProperties =
TXml.findChildren(mpd, 'SupplementalProperty');

if (!supplementalProperties.length) {
return null;
}

for (const prop of supplementalProperties) {
const schemeId = prop.attributes['schemeIdUri'];
if (schemeId == 'urn:mpeg:dash:chaining:2016') {
return prop.attributes['value'];
}
}

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 @@ -880,6 +880,7 @@ shaka.hls.HlsParser = class {
this.config_.hls.ignoreManifestTimestampsInSegmentsMode,
type: shaka.media.ManifestParser.HLS,
serviceDescription: null,
nextUrl: null,
};

// If there is no 'CODECS' attribute in the manifest and codec guessing is
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 @@ -419,6 +419,7 @@ shaka.mss.MssParser = class {
ignoreManifestTimestampsInSegmentsMode: false,
type: shaka.media.ManifestParser.MSS,
serviceDescription: null,
nextUrl: 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 @@ -93,6 +93,7 @@ shaka.offline.ManifestConverter = class {
ignoreManifestTimestampsInSegmentsMode: false,
type: manifestDB.type || shaka.media.ManifestParser.UNKNOWN,
serviceDescription: null,
nextUrl: null,
};
}

Expand Down
37 changes: 37 additions & 0 deletions lib/player.js
Original file line number Diff line number Diff line change
Expand Up @@ -779,6 +779,9 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
this.checkVariantsTimer_ =
new shaka.util.Timer(() => this.checkVariants_());

/** @private {?shaka.media.PreloadManager} */
this.preloadNextUrl_ = null;

// Even though |attach| will start in later interpreter cycles, it should be
// the LAST thing we do in the constructor because conceptually it relies on
// player having been initialized.
Expand Down Expand Up @@ -1337,6 +1340,14 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
this.drmEngine_ = null;
}

if (this.preloadNextUrl_ &&
this.assetUri_ != this.preloadNextUrl_.getAssetUri()) {
if (!this.preloadNextUrl_.isDestroyed()) {
this.preloadNextUrl_.destroy();
}
this.preloadNextUrl_ = null;
}

this.assetUri_ = null;
this.mimeType_ = null;
this.bufferObserver_ = null;
Expand Down Expand Up @@ -1431,6 +1442,9 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
}

if (this.assetUri_) {
// Note: This is used to avoid the destruction of the nextUrl
// preloadManager that can be the current one.
this.assetUri_ = assetUri;
await this.unload(/* initializeMediaSource= */ false);
}

Expand Down Expand Up @@ -1632,6 +1646,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
// properly destroyed or released.
await preloadManager.destroy();
}
this.preloadNextUrl_ = null;
}
}

Expand Down Expand Up @@ -2406,6 +2421,28 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
this.loadEventManager_.listen(
mediaElement, 'timeupdate', onVideoProgress);
this.onVideoProgress_();
if (this.manifest_.nextUrl) {
if (this.config_.streaming.preloadNextUrlWindow > 0) {
const onTimeUpdate = async () => {
const timeToEnd = this.video_.duration - this.video_.currentTime;
if (!isNaN(timeToEnd)) {
if (timeToEnd <= this.config_.streaming.preloadNextUrlWindow) {
this.loadEventManager_.unlisten(
mediaElement, 'timeupdate', onTimeUpdate);
goog.asserts.assert(this.manifest_.nextUrl,
'this.manifest_.nextUrl should be valid.');
this.preloadNextUrl_ =
await this.preload(this.manifest_.nextUrl);
}
}
};
this.loadEventManager_.listen(
mediaElement, 'timeupdate', onTimeUpdate);
}
this.loadEventManager_.listen(mediaElement, 'ended', () => {
this.load(this.preloadNextUrl_ || this.manifest_.nextUrl);
});
}
}

if (this.adManager_) {
Expand Down
1 change: 1 addition & 0 deletions lib/util/player_configuration.js
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,7 @@ shaka.util.PlayerConfiguration = class {
vodDynamicPlaybackRateLowBufferRate: 0.95,
vodDynamicPlaybackRateBufferRatio: 0.5,
infiniteLiveStreamDuration: false,
preloadNextUrlWindow: 30,
};

// WebOS, Tizen, Chromecast and Hisense have long hardware pipelines
Expand Down
2 changes: 2 additions & 0 deletions roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ v5.0 - 2024 Q4
v4.9 - 2024 Q3
- DASH patch manifests
https://github.com/shaka-project/shaka-player/issues/2228
- DASH: MPD chaining
https://github.com/shaka-project/shaka-player/issues/3926

=====

Expand Down
17 changes: 17 additions & 0 deletions test/dash/dash_parser_manifest_unit.js
Original file line number Diff line number Diff line change
Expand Up @@ -2630,6 +2630,23 @@ describe('DashParser Manifest', () => {
});
});

it('parses urn:mpeg:dash:chaining:2016', async () => {
const source = [
'<MPD minBufferTime="PT75S" type="dynamic"',
' availabilityStartTime="1970-01-01T00:00:00Z">',
' <SupplementalProperty schemeIdUri="urn:mpeg:dash:chaining:2016"',
' value="https://nextUrl" />',
'</MPD>',
].join('\n');

fakeNetEngine.setResponseText('dummy://foo', source);

/** @type {shaka.extern.Manifest} */
const manifest = await parser.start('dummy://foo', playerInterface);

expect(manifest.nextUrl).toBe('https://nextUrl');
});

it('parses urn:mpeg:dash:ssr:2023', async () => { // eslint-disable-line max-len
const manifestText = [
'<MPD minBufferTime="PT75S">',
Expand Down
1 change: 1 addition & 0 deletions test/media/playhead_unit.js
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ describe('Playhead', () => {
ignoreManifestTimestampsInSegmentsMode: false,
type: 'UNKNOWN',
serviceDescription: null,
nextUrl: 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 @@ -601,6 +601,7 @@ describe('StreamingEngine', () => {
ignoreManifestTimestampsInSegmentsMode: false,
type: 'UNKNOWN',
serviceDescription: null,
nextUrl: null,
variants: [{
id: 1,
video: {
Expand Down
26 changes: 26 additions & 0 deletions test/player_integration.js
Original file line number Diff line number Diff line change
Expand Up @@ -1416,6 +1416,32 @@ describe('Player', () => {
await waiter.waitUntilPlayheadReachesOrFailOnTimeout(video, 1, 10);
});

describe('supports nextUrl', () => {
const urlWithNextUrl = 'test:sintel_next_url_compiled';

it('with preload', async () => {
player.configure('streaming.preloadNextUrlWindow', 30);
await player.load(urlWithNextUrl);
await video.play();
await waiter.timeoutAfter(30).waitForEnd(video);
expect(player.getAssetUri()).toBe(urlWithNextUrl);
// Delay needed to load the next URL.
await shaka.test.Util.delay(1);
expect(player.getAssetUri()).not.toBe(urlWithNextUrl);
});

it('without preload', async () => {
player.configure('streaming.preloadNextUrlWindow', 0);
await player.load(urlWithNextUrl);
await video.play();
await waiter.timeoutAfter(30).waitForEnd(video);
expect(player.getAssetUri()).toBe(urlWithNextUrl);
// Delay needed to load the next URL.
await shaka.test.Util.delay(1);
expect(player.getAssetUri()).not.toBe(urlWithNextUrl);
});
});

describe('buffer gap', () => {
// Regression test for issue #6339.
it('skip initial buffer gap', async () => {
Expand Down
2 changes: 2 additions & 0 deletions test/test/util/manifest_generator.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,8 @@ shaka.test.ManifestGenerator.Manifest = class {
this.type = 'UNKNOWN';
/** @type {?shaka.extern.ServiceDescription} */
this.serviceDescription = null;
/** @type {?string} */
this.nextUrl = null;


/** @type {shaka.extern.Manifest} */
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 @@ -320,6 +320,7 @@ shaka.test.StreamingEngineUtil = class {
ignoreManifestTimestampsInSegmentsMode: false,
type: 'UNKNOWN',
serviceDescription: null,
nextUrl: null,
};

/** @type {shaka.extern.Variant} */
Expand Down
13 changes: 12 additions & 1 deletion test/test/util/test_scheme.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ let ExtraMetadataType;
* licenseServers: (!Object.<string, string>|undefined),
* licenseRequestHeaders: (!Object.<string, string>|undefined),
* customizeStream: (function(shaka.test.ManifestGenerator.Stream)|undefined),
* sequenceMode: (boolean|undefined)
* sequenceMode: (boolean|undefined),
* nextUrl: (string|undefined)
* }}
*/
let MetadataType;
Expand Down Expand Up @@ -289,6 +290,9 @@ shaka.test.TestScheme = class {
const manifest = shaka.test.ManifestGenerator.generate((manifest) => {
manifest.presentationTimeline.setDuration(data.duration);
manifest.sequenceMode = data.sequenceMode || false;
if (data.nextUrl) {
manifest.nextUrl = data.nextUrl + suffix;
}

const videoResolutions = data.videoResolutions || [undefined];
const audioLanguages = data.audioLanguages ||
Expand Down Expand Up @@ -588,6 +592,13 @@ shaka.test.TestScheme.DATA = {
duration: 30,
},

'sintel_next_url': {
video: sintelVideoSegment,
audio: sintelAudioSegment,
duration: 5,
nextUrl: 'test:sintel',
},

// https://github.com/shaka-project/shaka-player/issues/2553
'forced_subs_simulation': {
audio: sintelAudioSegment,
Expand Down

0 comments on commit 82c5149

Please sign in to comment.