Skip to content

Commit

Permalink
Implementation of MPEG-DASH 5th Edition Patching Semantics (#3451)
Browse files Browse the repository at this point in the history
* Implementation of MPEG-DASH 5th Edition Patching Semantics

- Full support for DASH specified xpath restrictions
- Support for add/replace/delete operations in patch
- Validation of Patch on receive
- Handling of empty Patch semantics

This commit is rebased first pass on to latest dash.js, includes the
additional fixes previously provided:
- Patch operation ironing fix (#1) - @thmatuza
- Xpath indexing and add attribute operation (#2) - @chanyk-joseph

* Addressing PR Feedback

* Add missing additional test case

Co-authored-by: Daniel Silhavy <daniel.silhavy@fokus.fraunhofer.de>
  • Loading branch information
technogeek00 and dsilhavy committed Feb 8, 2021
1 parent 4ea2736 commit d10013a
Show file tree
Hide file tree
Showing 20 changed files with 2,263 additions and 10 deletions.
185 changes: 185 additions & 0 deletions src/dash/DashAdapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import ManifestInfo from './vo/ManifestInfo';
import Event from './vo/Event';
import FactoryMaker from '../core/FactoryMaker';
import DashManifestModel from './models/DashManifestModel';
import PatchManifestModel from './models/PatchManifestModel';

/**
* @module DashAdapter
Expand All @@ -45,6 +46,7 @@ import DashManifestModel from './models/DashManifestModel';
function DashAdapter() {
let instance,
dashManifestModel,
patchManifestModel,
voPeriods,
voAdaptations,
currentMediaInfo,
Expand All @@ -57,6 +59,7 @@ function DashAdapter() {

function setup() {
dashManifestModel = DashManifestModel(context).getInstance();
patchManifestModel = PatchManifestModel(context).getInstance();
reset();
}

Expand Down Expand Up @@ -633,6 +636,48 @@ function DashAdapter() {
return dashManifestModel.getManifestUpdatePeriod(manifest, latencyOfLastUpdate);
}

/**
* Returns the publish time from the manifest
* @param {object} manifest
* @returns {Date|null} publishTime
* @memberOf module:DashAdapter
* @instance
*/
function getPublishTime(manifest) {
return dashManifestModel.getPublishTime(manifest);
}

/**
* Returns the patch location of the MPD if one exists and it is still valid
* @param {object} manifest
* @returns {(String|null)} patch location
* @memberOf module:DashAdapter
* @instance
*/
function getPatchLocation(manifest) {
const patchLocation = dashManifestModel.getPatchLocation(manifest);
const publishTime = dashManifestModel.getPublishTime(manifest);

// short-circuit when no patch location or publish time exists
if (!patchLocation || !publishTime) {
return null;
}

// if a ttl is provided, ensure patch location has not expired
if (patchLocation.hasOwnProperty('ttl') && publishTime) {
// attribute describes number of seconds as a double
const ttl = parseFloat(patchLocation.ttl) * 1000;

// check if the patch location has expired, if so do not consider it
if (publishTime.getTime() + ttl <= new Date().getTime()) {
return null;
}
}

// the patch location exists and, if a ttl applies, has not expired
return patchLocation.__text;
}

/**
* Checks if the manifest has a DVB profile
* @param {object} manifest
Expand All @@ -645,6 +690,15 @@ function DashAdapter() {
return dashManifestModel.hasProfile(manifest, PROFILE_DVB);
}

/**
* Checks if the manifest is actually just a patch manifest
* @param {object} manifest
* @return {boolean}
*/
function getIsPatch(manifest) {
return patchManifestModel.getIsPatch(manifest);
}

/**
*
* @param {object} node
Expand Down Expand Up @@ -757,6 +811,132 @@ function DashAdapter() {
currentMediaInfo = {};
}

/**
* Checks if the supplied manifest is compatible for application of the supplied patch
* @param {object} manifest
* @param {object} patch
* @return {boolean}
*/
function isPatchValid(manifest, patch) {
let manifestId = dashManifestModel.getId(manifest);
let patchManifestId = patchManifestModel.getMpdId(patch);
let manifestPublishTime = dashManifestModel.getPublishTime(manifest);
let patchPublishTime = patchManifestModel.getPublishTime(patch);
let originalManifestPublishTime = patchManifestModel.getOriginalPublishTime(patch);

// Patches are considered compatible if the following are true
// - MPD@id == Patch@mpdId
// - MPD@publishTime == Patch@originalPublishTime
// - MPD@publishTime < Patch@publishTime
// - All values in comparison exist
return !!(manifestId && patchManifestId && (manifestId == patchManifestId) &&
manifestPublishTime && originalManifestPublishTime && (manifestPublishTime.getTime() == originalManifestPublishTime.getTime()) &&
patchPublishTime && (manifestPublishTime.getTime() < patchPublishTime.getTime()));
}

/**
* Takes a given patch and applies it to the provided manifest, assumes patch is valid for manifest
* @param {object} manifest
* @param {object} patch
*/
function applyPatchToManifest(manifest, patch) {
// get all operations from the patch and apply them in document order
patchManifestModel.getPatchOperations(patch)
.forEach((operation) => {
let result = operation.getMpdTarget(manifest);

// operation supplies a path that doesn't match mpd, skip
if (result === null) {
return;
}

let {name, target, leaf} = result;

// short circuit for attribute selectors
if (operation.xpath.findsAttribute()) {
switch (operation.action) {
case 'add':
case 'replace':
// add and replace are just setting the value
target[name] = operation.value;
break;
case 'remove':
// remove is deleting the value
delete target[name];
break;
}
return;
}

// determine the relative insert position prior to possible removal
let relativePosition = (target[name + '_asArray'] || []).indexOf(leaf);
let insertBefore = (operation.position === 'prepend' || operation.position === 'before');

// perform removal operation first, we have already capture the appropriate relative position
if (operation.action === 'remove' || operation.action === 'replace') {
// note that we ignore the 'ws' attribute of patch operations as it does not effect parsed mpd operations

// purge the directly named entity
delete target[name];

// if we did have a positional reference we need to purge from array set and restore X2JS proper semantics
if (relativePosition != -1) {
let targetArray = target[name + '_asArray'];
targetArray.splice(relativePosition, 1);
if (targetArray.length > 1) {
target[name] = targetArray;
} else if (targetArray.length == 1) {
// xml parsing semantics, singular asArray must be non-array in the unsuffixed key
target[name] = targetArray[0];
} else {
// all nodes of this type deleted, remove entry
delete target[name + '_asArray'];
}
}
}

// Perform any add/replace operations now, technically RFC5261 only allows a single element to take the
// place of a replaced element while the add case allows an arbitrary number of children.
// Due to the both operations requiring the same insertion logic they have been combined here and we will
// not enforce single child operations for replace, assertions should be made at patch parse time if necessary
if (operation.action === 'add' || operation.action === 'replace') {
// value will be an object with element name keys pointing to arrays of objects
Object.keys(operation.value).forEach((insert) => {
let insertNodes = operation.value[insert];

let updatedNodes = target[insert + '_asArray'] || [];
if (updatedNodes.length === 0 && target[insert]) {
updatedNodes.push(target[insert]);
}

if (updatedNodes.length === 0) {
// no original nodes for this element type
updatedNodes = insertNodes;
} else {
// compute the position we need to insert at, default to end of set
let position = updatedNodes.length;
if (insert == name && relativePosition != -1) {
// if the inserted element matches the operation target (not leaf) and there is a relative position we
// want the inserted position to be set such that our insertion is relative to original position
// since replace has modified the array length we reduce the insert point by 1
position = relativePosition + (insertBefore ? 0 : 1) + (operation.action == 'replace' ? -1 : 0);
} else {
// otherwise we are in an add append/prepend case or replace case that removed the target name completely
position = insertBefore ? 0 : updatedNodes.length;
}

// we dont have to perform element removal for the replace case as that was done above
updatedNodes.splice.apply(updatedNodes, [position, 0].concat(insertNodes));
}

// now we properly reset the element keys on the target to match parsing semantics
target[insert + '_asArray'] = updatedNodes;
target[insert] = updatedNodes.length == 1 ? updatedNodes[0] : updatedNodes;
});
}
});
}

// #endregion PUBLIC FUNCTIONS

// #region PRIVATE FUNCTIONS
Expand Down Expand Up @@ -983,15 +1163,20 @@ function DashAdapter() {
getDuration: getDuration,
getRegularPeriods: getRegularPeriods,
getLocation: getLocation,
getPatchLocation: getPatchLocation,
getManifestUpdatePeriod: getManifestUpdatePeriod,
getPublishTime,
getIsDVB: getIsDVB,
getIsPatch: getIsPatch,
getBaseURLsFromElement: getBaseURLsFromElement,
getRepresentationSortFunction: getRepresentationSortFunction,
getCodec: getCodec,
getVoAdaptations: getVoAdaptations,
getVoPeriods: getVoPeriods,
getPeriodById,
setCurrentMediaInfo: setCurrentMediaInfo,
isPatchValid: isPatchValid,
applyPatchToManifest: applyPatchToManifest,
reset: reset
};

Expand Down
4 changes: 4 additions & 0 deletions src/dash/constants/DashConstants.js
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,10 @@ class DashConstants {
this.SERVICE_DESCRIPTION_SCOPE = 'Scope';
this.SERVICE_DESCRIPTION_LATENCY = 'Latency';
this.SERVICE_DESCRIPTION_PLAYBACK_RATE = 'PlaybackRate';
this.PATCH_LOCATION = 'PatchLocation';
this.PUBLISH_TIME = 'publishTime';
this.ORIGINAL_PUBLISH_TIME = 'originalPublishTime';
this.ORIGINAL_MPD_ID = 'mpdId';
}

constructor () {
Expand Down
27 changes: 27 additions & 0 deletions src/dash/models/DashManifestModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,10 @@ function DashManifestModel() {
return isDynamic;
}

function getId(manifest) {
return (manifest && manifest[DashConstants.ID]) || null;
}

function hasProfile(manifest, profile) {
let has = false;

Expand Down Expand Up @@ -378,6 +382,10 @@ function DashManifestModel() {
return isNaN(delay) ? delay : Math.max(delay - latencyOfLastUpdate, 1);
}

function getPublishTime(manifest) {
return manifest && manifest.hasOwnProperty(DashConstants.PUBLISH_TIME) ? new Date(manifest[DashConstants.PUBLISH_TIME]) : null;
}

function getRepresentationCount(adaptation) {
return adaptation && Array.isArray(adaptation.Representation_asArray) ? adaptation.Representation_asArray.length : 0;
}
Expand Down Expand Up @@ -748,6 +756,10 @@ function DashManifestModel() {
if (manifest.hasOwnProperty(DashConstants.MAX_SEGMENT_DURATION)) {
mpd.maxSegmentDuration = manifest.maxSegmentDuration;
}

if (manifest.hasOwnProperty(DashConstants.PUBLISH_TIME)) {
mpd.publishTime = new Date(manifest.publishTime);
}
}

return mpd;
Expand Down Expand Up @@ -1040,6 +1052,18 @@ function DashManifestModel() {
return undefined;
}

function getPatchLocation(manifest) {
if (manifest && manifest.hasOwnProperty(DashConstants.PATCH_LOCATION)) {
// only include support for single patch location currently
manifest.PatchLocation = manifest.PatchLocation_asArray[0];

return manifest.PatchLocation;
}

// no patch location provided
return undefined;
}

function getSuggestedPresentationDelay(mpd) {
return mpd && mpd.hasOwnProperty(DashConstants.SUGGESTED_PRESENTATION_DELAY) ? mpd.suggestedPresentationDelay : null;
}
Expand Down Expand Up @@ -1135,10 +1159,12 @@ function DashManifestModel() {
getLabelsForAdaptation: getLabelsForAdaptation,
getContentProtectionData: getContentProtectionData,
getIsDynamic: getIsDynamic,
getId: getId,
hasProfile: hasProfile,
getDuration: getDuration,
getBandwidth: getBandwidth,
getManifestUpdatePeriod: getManifestUpdatePeriod,
getPublishTime: getPublishTime,
getRepresentationCount: getRepresentationCount,
getBitrateListForAdaptation: getBitrateListForAdaptation,
getRepresentationFor: getRepresentationFor,
Expand All @@ -1154,6 +1180,7 @@ function DashManifestModel() {
getBaseURLsFromElement: getBaseURLsFromElement,
getRepresentationSortFunction: getRepresentationSortFunction,
getLocation: getLocation,
getPatchLocation: getPatchLocation,
getSuggestedPresentationDelay: getSuggestedPresentationDelay,
getAvailabilityStartTime: getAvailabilityStartTime,
getServiceDescriptions: getServiceDescriptions,
Expand Down
Loading

0 comments on commit d10013a

Please sign in to comment.