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

Implementation of MPEG-DASH 5th Edition Patching Semantics #3451

Merged
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
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) &&
dsilhavy marked this conversation as resolved.
Show resolved Hide resolved
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