Skip to content

Commit

Permalink
fix(DASH): Patch manifest Adaptationset indexing, @n=<Numbering> and @t
Browse files Browse the repository at this point in the history
…=<time> (#7131)

Fixes #7128
  • Loading branch information
Iragne authored and joeyparrish committed Aug 20, 2024
1 parent b31a57f commit 2913fd3
Show file tree
Hide file tree
Showing 4 changed files with 331 additions and 16 deletions.
38 changes: 31 additions & 7 deletions lib/dash/dash_parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -1043,12 +1043,16 @@ shaka.dash.DashParser = class {
getContextIdsFromPath_(paths) {
let periodId = '';
let adaptationSetId = '';
let adaptationSetPosition = -1;
let representationId = '';
for (const node of paths) {
if (node.name === 'Period') {
periodId = node.id;
} else if (node.name === 'AdaptationSet') {
adaptationSetId = node.id;
if (node.position !== null) {
adaptationSetPosition = node.position;
}
} else if (node.name === 'Representation') {
representationId = node.id;
}
Expand All @@ -1060,14 +1064,27 @@ shaka.dash.DashParser = class {
if (representationId) {
contextIds.push(periodId + ',' + representationId);
} else {
for (const context of this.contextCache_.values()) {
if (context.period.id === periodId &&
context.adaptationSet.id === adaptationSetId &&
context.representation.id) {
contextIds.push(periodId + ',' + context.representation.id);
if (adaptationSetId) {
for (const context of this.contextCache_.values()) {
if (context.period.id === periodId &&
context.adaptationSet.id === adaptationSetId &&
context.representation.id) {
contextIds.push(periodId + ',' + context.representation.id);
}
}
} else {
if (adaptationSetPosition > -1) {
for (const context of this.contextCache_.values()) {
if (context.period.id === periodId &&
context.adaptationSet.position === adaptationSetPosition &&
context.representation.id) {
contextIds.push(periodId + ',' + context.representation.id);
}
}
}
}
}

return contextIds;
}

Expand Down Expand Up @@ -1466,7 +1483,8 @@ shaka.dash.DashParser = class {
const adaptationSetNodes =
TXml.findChildren(periodInfo.node, 'AdaptationSet');
const adaptationSets = adaptationSetNodes
.map((node) => this.parseAdaptationSet_(context, node))
.map((node, position) =>
this.parseAdaptationSet_(context, position, node))
.filter(Functional.isNotNull);

// For dynamic manifests, we use rep IDs internally, and they must be
Expand Down Expand Up @@ -1573,18 +1591,20 @@ shaka.dash.DashParser = class {
* Parses an AdaptationSet XML element.
*
* @param {shaka.dash.DashParser.Context} context
* @param {number} position
* @param {!shaka.extern.xml.Node} elem The AdaptationSet element.
* @return {?shaka.dash.DashParser.AdaptationInfo}
* @private
*/
parseAdaptationSet_(context, elem) {
parseAdaptationSet_(context, position, elem) {
const TXml = shaka.util.TXml;
const Functional = shaka.util.Functional;
const ManifestParserUtils = shaka.util.ManifestParserUtils;
const ContentType = ManifestParserUtils.ContentType;
const ContentProtection = shaka.dash.ContentProtection;

context.adaptationSet = this.createFrame_(elem, context.period, null);
context.adaptationSet.position = position;

let main = false;
const roleElements = TXml.findChildren(elem, 'Role');
Expand Down Expand Up @@ -2236,6 +2256,7 @@ shaka.dash.DashParser = class {
pixelAspectRatio: frameRef.pixelAspectRatio,
emsgSchemeIdUris: frameRef.emsgSchemeIdUris,
id: frameRef.id,
position: frameRef.position,
numChannels: frameRef.numChannels,
audioSamplingRate: frameRef.audioSamplingRate,
availabilityTimeOffset: frameRef.availabilityTimeOffset,
Expand Down Expand Up @@ -3000,6 +3021,7 @@ shaka.dash.DashParser.RequestSegmentCallback;
* pixelAspectRatio: (string|undefined),
* emsgSchemeIdUris: !Array.<string>,
* id: ?string,
* position: (number|undefined),
* language: ?string,
* numChannels: ?number,
* audioSamplingRate: ?number,
Expand Down Expand Up @@ -3040,6 +3062,8 @@ shaka.dash.DashParser.RequestSegmentCallback;
* emsg registered schemeIdUris.
* @property {?string} id
* The ID of the element.
* @property {number|undefined} position
* Position of the element used for indexing in case of no id
* @property {?string} language
* The original language of the element.
* @property {?number} numChannels
Expand Down
41 changes: 40 additions & 1 deletion lib/util/tXml.js
Original file line number Diff line number Diff line change
Expand Up @@ -753,11 +753,18 @@ shaka.util.TXml = class {
// We only want the id attribute in which case
// /'(.*?)'/ will suffice to get it.
const idAttr = path.match(/(@id='(.*?)')/);
const tAttr = path.match(/(@t='(\d+)')/);
const numberIndex = path.match(/(@n='(\d+)')/);
const position = path.match(/\[(\d+)\]/);
returnPaths.push({
name: nodeName[0],
id: idAttr ?
idAttr[0].match(/'(.*?)'/)[0].replace(/'/gm, '') : null,
t: tAttr ?
Number(tAttr[0].match(/'(.*?)'/)[0].replace(/'/gm, '')) : null,
n: numberIndex ?
Number(numberIndex[0].match(/'(.*?)'/)[0].replace(/'/gm, '')):
null,
// position is counted from 1, so make it readable for devs
position: position ? Number(position[1]) - 1 : null,
attribute: path.split('/@')[1] || null,
Expand Down Expand Up @@ -787,6 +794,14 @@ shaka.util.TXml = class {
const position = patchNode.attributes['pos'] || null;

let index = lastNode.position;
if (index == null) {
if (lastNode.t !== null) {
index = TXml.nodePositionByAttribute_(nodes, 't', lastNode.t);
}
if (lastNode.n !== null) {
index = TXml.nodePositionByAttribute_(nodes, 'n', lastNode.n);
}
}
if (index === null) {
index = position === 'prepend' ? 0 : nodes.length;
} else if (position === 'prepend') {
Expand All @@ -798,7 +813,7 @@ shaka.util.TXml = class {
const attribute = lastNode.attribute;

// Modify attribute
if (attribute) {
if (attribute && nodes[index]) {
TXml.modifyNodeAttribute(nodes[index], action, attribute,
TXml.getContents(patchNode) || '');
// Rearrange nodes
Expand All @@ -814,6 +829,28 @@ shaka.util.TXml = class {
}


/**
* Search the node index by the t attribute
* and return the index. if not found return null
* @param {!Array<shaka.extern.xml.Node>} nodes
* @param {!string} attribute
* @param {!number} value
* @private
*/
static nodePositionByAttribute_(nodes, attribute, value) {
let index = 0;
for (const node of nodes) {
const attrs = node.attributes;
const val = Number(attrs[attribute]);
if (val === value) {
return index;
}
index++;
}
return null;
}


/**
* @param {!shaka.extern.xml.Node} node
* @param {string} action
Expand Down Expand Up @@ -875,6 +912,8 @@ shaka.util.TXml.knownNameSpaces_ = new Map([]);
* @typedef {{
* name: string,
* id: ?string,
* t: ?number,
* n: ?number,
* position: ?number,
* attribute: ?string
* }}
Expand Down
177 changes: 177 additions & 0 deletions test/dash/dash_parser_patch_unit.js
Original file line number Diff line number Diff line change
Expand Up @@ -471,5 +471,182 @@ describe('DashParser Patch', () => {
ManifestParser.makeReference('s3.mp4', 2, 3, originalUri),
]);
});

it('modify @r attribute of an S element with @t=', async () => {
const xPath = '/' + [
'MPD',
'Period[@id=\'1\']',
'AdaptationSet[@id=\'1\']',
'Representation[@id=\'3\']',
'SegmentTemplate',
'SegmentTimeline',
'S',
].join('/');
const patchText = [
`<Patch mpdId="${mpdId}"`,
` originalPublishTime="${publishTime.toUTCString()}"">`,
` <add sel="${xPath}" pos="after">`,
' <S d="3" t="1" />',
' </add>',
'</Patch>',
].join('\n');

const xPath2 = '/' + [
'MPD',
'Period[@id=\'1\']',
'AdaptationSet[@id=\'1\']',
'Representation[@id=\'3\']',
'SegmentTemplate',
'SegmentTimeline',
'S',
].join('/');
const patchText2 = [
`<Patch mpdId="${mpdId}"`,
` originalPublishTime="${publishTime.toUTCString()}"">`,
` <add sel="${xPath2}" pos="after">`,
' <S d="3" t="4" />',
' </add>',
'</Patch>',
].join('\n');

const xPath3 = '/' + [
'MPD',
'Period[@id=&#39;1&#39;]',
'AdaptationSet[@id=&#39;1&#39;]',
'Representation[@id=&#39;3&#39;]',
'SegmentTemplate',
'SegmentTimeline',
'S[@t=&#39;4&#39;]/@r',
].join('/');
const patchText3 = [
`<Patch mpdId="${mpdId}"`,
` originalPublishTime="${publishTime.toUTCString()}">`,
` <replace sel="${xPath3}">`,
' 1',
' </replace>',
'</Patch>',
].join('\n');

fakeNetEngine.setResponseText('dummy://bar', patchText);

const manifest = await parser.start('dummy://foo', playerInterface);
const stream = manifest.variants[0].video;
expect(stream).toBeTruthy();
await stream.createSegmentIndex();
ManifestParser.verifySegmentIndex(stream, [
ManifestParser.makeReference('s1.mp4', 0, 1, originalUri),
]);

await updateManifest();
ManifestParser.verifySegmentIndex(stream, [
ManifestParser.makeReference('s1.mp4', 0, 1, originalUri),
ManifestParser.makeReference('s2.mp4', 1, 4, originalUri),
]);

fakeNetEngine.setResponseText('dummy://bar', patchText2);
await updateManifest();
ManifestParser.verifySegmentIndex(stream, [
ManifestParser.makeReference('s1.mp4', 0, 1, originalUri),
ManifestParser.makeReference('s2.mp4', 1, 4, originalUri),
ManifestParser.makeReference('s3.mp4', 4, 7, originalUri),
]);

fakeNetEngine.setResponseText('dummy://bar', patchText3);
await updateManifest();
ManifestParser.verifySegmentIndex(stream, [
ManifestParser.makeReference('s1.mp4', 0, 1, originalUri),
ManifestParser.makeReference('s2.mp4', 1, 4, originalUri),
ManifestParser.makeReference('s3.mp4', 4, 7, originalUri),
ManifestParser.makeReference('s4.mp4', 7, 10, originalUri),
]);
});
it('modify @r attribute of an S element with @n=', async () => {
const xPath = '/' + [
'MPD',
'Period[@id=\'1\']',
'AdaptationSet[@id=\'1\']',
'Representation[@id=\'3\']',
'SegmentTemplate',
'SegmentTimeline',
'S',
].join('/');
const patchText = [
`<Patch mpdId="${mpdId}"`,
` originalPublishTime="${publishTime.toUTCString()}"">`,
` <add sel="${xPath}" pos="after">`,
' <S d="3" t="1" />',
' </add>',
'</Patch>',
].join('\n');

const xPath2 = '/' + [
'MPD',
'Period[@id=\'1\']',
'AdaptationSet[@id=\'1\']',
'Representation[@id=\'3\']',
'SegmentTemplate',
'SegmentTimeline',
'S',
].join('/');
const patchText2 = [
`<Patch mpdId="${mpdId}"`,
` originalPublishTime="${publishTime.toUTCString()}"">`,
` <add sel="${xPath2}" pos="after">`,
' <S d="3" t="4" n="3" />',
' </add>',
'</Patch>',
].join('\n');

const xPath3 = '/' + [
'MPD',
'Period[@id=&#39;1&#39;]',
'AdaptationSet[@id=&#39;1&#39;]',
'Representation[@id=&#39;3&#39;]',
'SegmentTemplate',
'SegmentTimeline',
'S[@n=&#39;3&#39;]/@r',
].join('/');
const patchText3 = [
`<Patch mpdId="${mpdId}"`,
` originalPublishTime="${publishTime.toUTCString()}">`,
` <replace sel="${xPath3}">`,
' 1',
' </replace>',
'</Patch>',
].join('\n');

fakeNetEngine.setResponseText('dummy://bar', patchText);

const manifest = await parser.start('dummy://foo', playerInterface);
const stream = manifest.variants[0].video;
expect(stream).toBeTruthy();
await stream.createSegmentIndex();
ManifestParser.verifySegmentIndex(stream, [
ManifestParser.makeReference('s1.mp4', 0, 1, originalUri),
]);

await updateManifest();
ManifestParser.verifySegmentIndex(stream, [
ManifestParser.makeReference('s1.mp4', 0, 1, originalUri),
ManifestParser.makeReference('s2.mp4', 1, 4, originalUri),
]);

fakeNetEngine.setResponseText('dummy://bar', patchText2);
await updateManifest();
ManifestParser.verifySegmentIndex(stream, [
ManifestParser.makeReference('s1.mp4', 0, 1, originalUri),
ManifestParser.makeReference('s2.mp4', 1, 4, originalUri),
ManifestParser.makeReference('s3.mp4', 4, 7, originalUri),
]);

fakeNetEngine.setResponseText('dummy://bar', patchText3);
await updateManifest();
ManifestParser.verifySegmentIndex(stream, [
ManifestParser.makeReference('s1.mp4', 0, 1, originalUri),
ManifestParser.makeReference('s2.mp4', 1, 4, originalUri),
ManifestParser.makeReference('s3.mp4', 4, 7, originalUri),
ManifestParser.makeReference('s4.mp4', 7, 10, originalUri),
]);
});
});
});
Loading

0 comments on commit 2913fd3

Please sign in to comment.