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 MPD Chaining support #6641

Merged
merged 5 commits into from
May 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be awaited, right?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes :(

}
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 () => {
avelad marked this conversation as resolved.
Show resolved Hide resolved
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(
avelad marked this conversation as resolved.
Show resolved Hide resolved
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
Loading