Skip to content

Commit

Permalink
Implementation of MPEG-DASH 5th Edition Patching Semantics
Browse files Browse the repository at this point in the history
- 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
  • Loading branch information
technogeek00 committed Nov 11, 2020
1 parent 03ed196 commit bb99f27
Show file tree
Hide file tree
Showing 18 changed files with 1,374 additions and 8 deletions.
163 changes: 163 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 @@ -598,6 +601,31 @@ function DashAdapter() {
return dashManifestModel.getManifestUpdatePeriod(manifest, latencyOfLastUpdate);
}

/**
* 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);
if (patchLocation && publishTime) {
// grab the ttl from the patch location
const ttl = parseInt(patchLocation.ttl, 10) * 1000;

// if the patch location has not expired provide the location back
if (publishTime.getTime() + ttl > new Date().getTime()) {
// actual url is the text of the node
return patchLocation.__text;
}
}

// either there is no patch location or it has expired
return null;
}

/**
* Checks if the manifest has a DVB profile
* @param {object} manifest
Expand All @@ -610,6 +638,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 @@ -718,6 +755,128 @@ 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 publishTime = dashManifestModel.getPublishTime(manifest);
let originalPublishTime = patchManifestModel.getOriginalPublishTime(patch);

// Patches are considered compatible if the following are true
// - MPD@id == Patch@mpdId
// - MPD@publishTime == Patch@originalPublishTime
// - All values in comparison exist
return manifestId && patchManifestId && (manifestId == patchManifestId) &&
publishTime && originalPublishTime && (publishTime.getTime() == originalPublishTime.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
position = relativePosition + (insertBefore ? 0 : 1);
} 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 @@ -941,15 +1100,19 @@ function DashAdapter() {
getDuration: getDuration,
getRegularPeriods: getRegularPeriods,
getLocation: getLocation,
getPatchLocation: getPatchLocation,
getManifestUpdatePeriod: getManifestUpdatePeriod,
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
28 changes: 28 additions & 0 deletions src/dash/models/DashManifestModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,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 @@ -365,6 +369,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 @@ -735,6 +743,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 @@ -1023,6 +1035,19 @@ 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];

// handle attribute enabled and attribute less case
return manifest.PatchLocation.__text || 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 @@ -1116,10 +1141,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 @@ -1134,6 +1161,7 @@ function DashManifestModel() {
getBaseURLsFromElement: getBaseURLsFromElement,
getRepresentationSortFunction: getRepresentationSortFunction,
getLocation: getLocation,
getPatchLocation: getPatchLocation,
getSuggestedPresentationDelay: getSuggestedPresentationDelay,
getAvailabilityStartTime: getAvailabilityStartTime,
getServiceDescriptions: getServiceDescriptions,
Expand Down
Loading

0 comments on commit bb99f27

Please sign in to comment.