forked from jchomaz/Vulture
Tracking de l'application VApp (IHM du jeu)
This commit is contained in:
10
VApp/node_modules/mpd-parser/src/errors.js
generated
vendored
Normal file
10
VApp/node_modules/mpd-parser/src/errors.js
generated
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
export default {
|
||||
INVALID_NUMBER_OF_PERIOD: 'INVALID_NUMBER_OF_PERIOD',
|
||||
INVALID_NUMBER_OF_CONTENT_STEERING: 'INVALID_NUMBER_OF_CONTENT_STEERING',
|
||||
DASH_EMPTY_MANIFEST: 'DASH_EMPTY_MANIFEST',
|
||||
DASH_INVALID_XML: 'DASH_INVALID_XML',
|
||||
NO_BASE_URL: 'NO_BASE_URL',
|
||||
MISSING_SEGMENT_INFORMATION: 'MISSING_SEGMENT_INFORMATION',
|
||||
SEGMENT_TIME_UNSPECIFIED: 'SEGMENT_TIME_UNSPECIFIED',
|
||||
UNSUPPORTED_UTC_TIMING_SCHEME: 'UNSUPPORTED_UTC_TIMING_SCHEME'
|
||||
};
|
||||
59
VApp/node_modules/mpd-parser/src/index.js
generated
vendored
Normal file
59
VApp/node_modules/mpd-parser/src/index.js
generated
vendored
Normal file
@@ -0,0 +1,59 @@
|
||||
import { version } from '../package.json';
|
||||
import { toM3u8, generateSidxKey } from './toM3u8';
|
||||
import { toPlaylists } from './toPlaylists';
|
||||
import { inheritAttributes } from './inheritAttributes';
|
||||
import { stringToMpdXml } from './stringToMpdXml';
|
||||
import { parseUTCTimingScheme } from './parseUTCTimingScheme';
|
||||
import {addSidxSegmentsToPlaylist} from './segment/segmentBase.js';
|
||||
|
||||
const VERSION = version;
|
||||
|
||||
/*
|
||||
* Given a DASH manifest string and options, parses the DASH manifest into an object in the
|
||||
* form outputed by m3u8-parser and accepted by videojs/http-streaming.
|
||||
*
|
||||
* For live DASH manifests, if `previousManifest` is provided in options, then the newly
|
||||
* parsed DASH manifest will have its media sequence and discontinuity sequence values
|
||||
* updated to reflect its position relative to the prior manifest.
|
||||
*
|
||||
* @param {string} manifestString - the DASH manifest as a string
|
||||
* @param {options} [options] - any options
|
||||
*
|
||||
* @return {Object} the manifest object
|
||||
*/
|
||||
const parse = (manifestString, options = {}) => {
|
||||
const parsedManifestInfo = inheritAttributes(stringToMpdXml(manifestString), options);
|
||||
const playlists = toPlaylists(parsedManifestInfo.representationInfo);
|
||||
|
||||
return toM3u8({
|
||||
dashPlaylists: playlists,
|
||||
locations: parsedManifestInfo.locations,
|
||||
contentSteering: parsedManifestInfo.contentSteeringInfo,
|
||||
sidxMapping: options.sidxMapping,
|
||||
previousManifest: options.previousManifest,
|
||||
eventStream: parsedManifestInfo.eventStream
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Parses the manifest for a UTCTiming node, returning the nodes attributes if found
|
||||
*
|
||||
* @param {string} manifestString
|
||||
* XML string of the MPD manifest
|
||||
* @return {Object|null}
|
||||
* Attributes of UTCTiming node specified in the manifest. Null if none found
|
||||
*/
|
||||
const parseUTCTiming = (manifestString) =>
|
||||
parseUTCTimingScheme(stringToMpdXml(manifestString));
|
||||
|
||||
export {
|
||||
VERSION,
|
||||
parse,
|
||||
parseUTCTiming,
|
||||
stringToMpdXml,
|
||||
inheritAttributes,
|
||||
toPlaylists,
|
||||
toM3u8,
|
||||
addSidxSegmentsToPlaylist,
|
||||
generateSidxKey
|
||||
};
|
||||
643
VApp/node_modules/mpd-parser/src/inheritAttributes.js
generated
vendored
Normal file
643
VApp/node_modules/mpd-parser/src/inheritAttributes.js
generated
vendored
Normal file
@@ -0,0 +1,643 @@
|
||||
import { flatten } from './utils/list';
|
||||
import { merge } from './utils/object';
|
||||
import { findChildren, getContent } from './utils/xml';
|
||||
import { parseAttributes } from './parseAttributes';
|
||||
import errors from './errors';
|
||||
import resolveUrl from '@videojs/vhs-utils/es/resolve-url';
|
||||
import decodeB64ToUint8Array from '@videojs/vhs-utils/es/decode-b64-to-uint8-array';
|
||||
|
||||
const keySystemsMap = {
|
||||
'urn:uuid:1077efec-c0b2-4d02-ace3-3c1e52e2fb4b': 'org.w3.clearkey',
|
||||
'urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed': 'com.widevine.alpha',
|
||||
'urn:uuid:9a04f079-9840-4286-ab92-e65be0885f95': 'com.microsoft.playready',
|
||||
'urn:uuid:f239e769-efa3-4850-9c16-a903c6932efb': 'com.adobe.primetime',
|
||||
// ISO_IEC 23009-1_2022 5.8.5.2.2 The mp4 Protection Scheme
|
||||
'urn:mpeg:dash:mp4protection:2011': 'mp4protection'
|
||||
};
|
||||
|
||||
/**
|
||||
* Builds a list of urls that is the product of the reference urls and BaseURL values
|
||||
*
|
||||
* @param {Object[]} references
|
||||
* List of objects containing the reference URL as well as its attributes
|
||||
* @param {Node[]} baseUrlElements
|
||||
* List of BaseURL nodes from the mpd
|
||||
* @return {Object[]}
|
||||
* List of objects with resolved urls and attributes
|
||||
*/
|
||||
export const buildBaseUrls = (references, baseUrlElements) => {
|
||||
if (!baseUrlElements.length) {
|
||||
return references;
|
||||
}
|
||||
|
||||
return flatten(references.map(function(reference) {
|
||||
return baseUrlElements.map(function(baseUrlElement) {
|
||||
const initialBaseUrl = getContent(baseUrlElement);
|
||||
const resolvedBaseUrl = resolveUrl(reference.baseUrl, initialBaseUrl);
|
||||
|
||||
const finalBaseUrl = merge(
|
||||
parseAttributes(baseUrlElement),
|
||||
{ baseUrl: resolvedBaseUrl }
|
||||
);
|
||||
|
||||
// If the URL is resolved, we want to get the serviceLocation from the reference
|
||||
// assuming there is no serviceLocation on the initialBaseUrl
|
||||
if (resolvedBaseUrl !== initialBaseUrl && !finalBaseUrl.serviceLocation && reference.serviceLocation) {
|
||||
finalBaseUrl.serviceLocation = reference.serviceLocation;
|
||||
}
|
||||
|
||||
return finalBaseUrl;
|
||||
});
|
||||
}));
|
||||
};
|
||||
|
||||
/**
|
||||
* Contains all Segment information for its containing AdaptationSet
|
||||
*
|
||||
* @typedef {Object} SegmentInformation
|
||||
* @property {Object|undefined} template
|
||||
* Contains the attributes for the SegmentTemplate node
|
||||
* @property {Object[]|undefined} segmentTimeline
|
||||
* Contains a list of atrributes for each S node within the SegmentTimeline node
|
||||
* @property {Object|undefined} list
|
||||
* Contains the attributes for the SegmentList node
|
||||
* @property {Object|undefined} base
|
||||
* Contains the attributes for the SegmentBase node
|
||||
*/
|
||||
|
||||
/**
|
||||
* Returns all available Segment information contained within the AdaptationSet node
|
||||
*
|
||||
* @param {Node} adaptationSet
|
||||
* The AdaptationSet node to get Segment information from
|
||||
* @return {SegmentInformation}
|
||||
* The Segment information contained within the provided AdaptationSet
|
||||
*/
|
||||
export const getSegmentInformation = (adaptationSet) => {
|
||||
const segmentTemplate = findChildren(adaptationSet, 'SegmentTemplate')[0];
|
||||
const segmentList = findChildren(adaptationSet, 'SegmentList')[0];
|
||||
const segmentUrls = segmentList && findChildren(segmentList, 'SegmentURL')
|
||||
.map(s => merge({ tag: 'SegmentURL' }, parseAttributes(s)));
|
||||
const segmentBase = findChildren(adaptationSet, 'SegmentBase')[0];
|
||||
const segmentTimelineParentNode = segmentList || segmentTemplate;
|
||||
const segmentTimeline = segmentTimelineParentNode &&
|
||||
findChildren(segmentTimelineParentNode, 'SegmentTimeline')[0];
|
||||
const segmentInitializationParentNode = segmentList || segmentBase || segmentTemplate;
|
||||
const segmentInitialization = segmentInitializationParentNode &&
|
||||
findChildren(segmentInitializationParentNode, 'Initialization')[0];
|
||||
|
||||
// SegmentTemplate is handled slightly differently, since it can have both
|
||||
// @initialization and an <Initialization> node. @initialization can be templated,
|
||||
// while the node can have a url and range specified. If the <SegmentTemplate> has
|
||||
// both @initialization and an <Initialization> subelement we opt to override with
|
||||
// the node, as this interaction is not defined in the spec.
|
||||
const template = segmentTemplate && parseAttributes(segmentTemplate);
|
||||
|
||||
if (template && segmentInitialization) {
|
||||
template.initialization =
|
||||
(segmentInitialization && parseAttributes(segmentInitialization));
|
||||
} else if (template && template.initialization) {
|
||||
// If it is @initialization we convert it to an object since this is the format that
|
||||
// later functions will rely on for the initialization segment. This is only valid
|
||||
// for <SegmentTemplate>
|
||||
template.initialization = { sourceURL: template.initialization };
|
||||
}
|
||||
|
||||
const segmentInfo = {
|
||||
template,
|
||||
segmentTimeline: segmentTimeline &&
|
||||
findChildren(segmentTimeline, 'S').map(s => parseAttributes(s)),
|
||||
list: segmentList && merge(
|
||||
parseAttributes(segmentList),
|
||||
{
|
||||
segmentUrls,
|
||||
initialization: parseAttributes(segmentInitialization)
|
||||
}
|
||||
),
|
||||
base: segmentBase && merge(parseAttributes(segmentBase), {
|
||||
initialization: parseAttributes(segmentInitialization)
|
||||
})
|
||||
};
|
||||
|
||||
Object.keys(segmentInfo).forEach(key => {
|
||||
if (!segmentInfo[key]) {
|
||||
delete segmentInfo[key];
|
||||
}
|
||||
});
|
||||
|
||||
return segmentInfo;
|
||||
};
|
||||
|
||||
/**
|
||||
* Contains Segment information and attributes needed to construct a Playlist object
|
||||
* from a Representation
|
||||
*
|
||||
* @typedef {Object} RepresentationInformation
|
||||
* @property {SegmentInformation} segmentInfo
|
||||
* Segment information for this Representation
|
||||
* @property {Object} attributes
|
||||
* Inherited attributes for this Representation
|
||||
*/
|
||||
|
||||
/**
|
||||
* Maps a Representation node to an object containing Segment information and attributes
|
||||
*
|
||||
* @name inheritBaseUrlsCallback
|
||||
* @function
|
||||
* @param {Node} representation
|
||||
* Representation node from the mpd
|
||||
* @return {RepresentationInformation}
|
||||
* Representation information needed to construct a Playlist object
|
||||
*/
|
||||
|
||||
/**
|
||||
* Returns a callback for Array.prototype.map for mapping Representation nodes to
|
||||
* Segment information and attributes using inherited BaseURL nodes.
|
||||
*
|
||||
* @param {Object} adaptationSetAttributes
|
||||
* Contains attributes inherited by the AdaptationSet
|
||||
* @param {Object[]} adaptationSetBaseUrls
|
||||
* List of objects containing resolved base URLs and attributes
|
||||
* inherited by the AdaptationSet
|
||||
* @param {SegmentInformation} adaptationSetSegmentInfo
|
||||
* Contains Segment information for the AdaptationSet
|
||||
* @return {inheritBaseUrlsCallback}
|
||||
* Callback map function
|
||||
*/
|
||||
export const inheritBaseUrls =
|
||||
(adaptationSetAttributes, adaptationSetBaseUrls, adaptationSetSegmentInfo) =>
|
||||
(representation) => {
|
||||
const repBaseUrlElements = findChildren(representation, 'BaseURL');
|
||||
const repBaseUrls = buildBaseUrls(adaptationSetBaseUrls, repBaseUrlElements);
|
||||
const attributes = merge(adaptationSetAttributes, parseAttributes(representation));
|
||||
const representationSegmentInfo = getSegmentInformation(representation);
|
||||
|
||||
return repBaseUrls.map(baseUrl => {
|
||||
return {
|
||||
segmentInfo: merge(adaptationSetSegmentInfo, representationSegmentInfo),
|
||||
attributes: merge(attributes, baseUrl)
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Tranforms a series of content protection nodes to
|
||||
* an object containing pssh data by key system
|
||||
*
|
||||
* @param {Node[]} contentProtectionNodes
|
||||
* Content protection nodes
|
||||
* @return {Object}
|
||||
* Object containing pssh data by key system
|
||||
*/
|
||||
const generateKeySystemInformation = (contentProtectionNodes) => {
|
||||
return contentProtectionNodes.reduce((acc, node) => {
|
||||
const attributes = parseAttributes(node);
|
||||
|
||||
// Although it could be argued that according to the UUID RFC spec the UUID string (a-f chars) should be generated
|
||||
// as a lowercase string it also mentions it should be treated as case-insensitive on input. Since the key system
|
||||
// UUIDs in the keySystemsMap are hardcoded as lowercase in the codebase there isn't any reason not to do
|
||||
// .toLowerCase() on the input UUID string from the manifest (at least I could not think of one).
|
||||
if (attributes.schemeIdUri) {
|
||||
attributes.schemeIdUri = attributes.schemeIdUri.toLowerCase();
|
||||
}
|
||||
const keySystem = keySystemsMap[attributes.schemeIdUri];
|
||||
|
||||
if (keySystem) {
|
||||
acc[keySystem] = { attributes };
|
||||
|
||||
const psshNode = findChildren(node, 'cenc:pssh')[0];
|
||||
|
||||
if (psshNode) {
|
||||
const pssh = getContent(psshNode);
|
||||
|
||||
acc[keySystem].pssh = pssh && decodeB64ToUint8Array(pssh);
|
||||
}
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
};
|
||||
|
||||
// defined in ANSI_SCTE 214-1 2016
|
||||
export const parseCaptionServiceMetadata = (service) => {
|
||||
// 608 captions
|
||||
if (service.schemeIdUri === 'urn:scte:dash:cc:cea-608:2015') {
|
||||
const values = typeof service.value !== 'string' ? [] : service.value.split(';');
|
||||
|
||||
return values.map((value) => {
|
||||
let channel;
|
||||
let language;
|
||||
|
||||
// default language to value
|
||||
language = value;
|
||||
|
||||
if (/^CC\d=/.test(value)) {
|
||||
[channel, language] = value.split('=');
|
||||
} else if (/^CC\d$/.test(value)) {
|
||||
channel = value;
|
||||
}
|
||||
|
||||
return {channel, language};
|
||||
});
|
||||
} else if (service.schemeIdUri === 'urn:scte:dash:cc:cea-708:2015') {
|
||||
const values = typeof service.value !== 'string' ? [] : service.value.split(';');
|
||||
|
||||
return values.map((value) => {
|
||||
const flags = {
|
||||
// service or channel number 1-63
|
||||
'channel': undefined,
|
||||
|
||||
// language is a 3ALPHA per ISO 639.2/B
|
||||
// field is required
|
||||
'language': undefined,
|
||||
|
||||
// BIT 1/0 or ?
|
||||
// default value is 1, meaning 16:9 aspect ratio, 0 is 4:3, ? is unknown
|
||||
'aspectRatio': 1,
|
||||
|
||||
// BIT 1/0
|
||||
// easy reader flag indicated the text is tailed to the needs of beginning readers
|
||||
// default 0, or off
|
||||
'easyReader': 0,
|
||||
|
||||
// BIT 1/0
|
||||
// If 3d metadata is present (CEA-708.1) then 1
|
||||
// default 0
|
||||
'3D': 0
|
||||
};
|
||||
|
||||
if (/=/.test(value)) {
|
||||
|
||||
const [channel, opts = ''] = value.split('=');
|
||||
|
||||
flags.channel = channel;
|
||||
flags.language = value;
|
||||
|
||||
opts.split(',').forEach((opt) => {
|
||||
const [name, val] = opt.split(':');
|
||||
|
||||
if (name === 'lang') {
|
||||
flags.language = val;
|
||||
|
||||
// er for easyReadery
|
||||
} else if (name === 'er') {
|
||||
flags.easyReader = Number(val);
|
||||
|
||||
// war for wide aspect ratio
|
||||
} else if (name === 'war') {
|
||||
flags.aspectRatio = Number(val);
|
||||
|
||||
} else if (name === '3D') {
|
||||
flags['3D'] = Number(val);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
flags.language = value;
|
||||
}
|
||||
|
||||
if (flags.channel) {
|
||||
flags.channel = 'SERVICE' + flags.channel;
|
||||
}
|
||||
|
||||
return flags;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* A map callback that will parse all event stream data for a collection of periods
|
||||
* DASH ISO_IEC_23009 5.10.2.2
|
||||
* https://dashif-documents.azurewebsites.net/Events/master/event.html#mpd-event-timing
|
||||
*
|
||||
* @param {PeriodInformation} period object containing necessary period information
|
||||
* @return a collection of parsed eventstream event objects
|
||||
*/
|
||||
export const toEventStream = (period) => {
|
||||
// get and flatten all EventStreams tags and parse attributes and children
|
||||
return flatten(findChildren(period.node, 'EventStream').map((eventStream) => {
|
||||
const eventStreamAttributes = parseAttributes(eventStream);
|
||||
const schemeIdUri = eventStreamAttributes.schemeIdUri;
|
||||
|
||||
// find all Events per EventStream tag and map to return objects
|
||||
return findChildren(eventStream, 'Event').map((event) => {
|
||||
const eventAttributes = parseAttributes(event);
|
||||
const presentationTime = eventAttributes.presentationTime || 0;
|
||||
const timescale = eventStreamAttributes.timescale || 1;
|
||||
const duration = eventAttributes.duration || 0;
|
||||
const start = (presentationTime / timescale) + period.attributes.start;
|
||||
|
||||
return {
|
||||
schemeIdUri,
|
||||
value: eventStreamAttributes.value,
|
||||
id: eventAttributes.id,
|
||||
start,
|
||||
end: start + (duration / timescale),
|
||||
messageData: getContent(event) || eventAttributes.messageData,
|
||||
contentEncoding: eventStreamAttributes.contentEncoding,
|
||||
presentationTimeOffset: eventStreamAttributes.presentationTimeOffset || 0
|
||||
};
|
||||
});
|
||||
}));
|
||||
};
|
||||
|
||||
/**
|
||||
* Maps an AdaptationSet node to a list of Representation information objects
|
||||
*
|
||||
* @name toRepresentationsCallback
|
||||
* @function
|
||||
* @param {Node} adaptationSet
|
||||
* AdaptationSet node from the mpd
|
||||
* @return {RepresentationInformation[]}
|
||||
* List of objects containing Representaion information
|
||||
*/
|
||||
|
||||
/**
|
||||
* Returns a callback for Array.prototype.map for mapping AdaptationSet nodes to a list of
|
||||
* Representation information objects
|
||||
*
|
||||
* @param {Object} periodAttributes
|
||||
* Contains attributes inherited by the Period
|
||||
* @param {Object[]} periodBaseUrls
|
||||
* Contains list of objects with resolved base urls and attributes
|
||||
* inherited by the Period
|
||||
* @param {string[]} periodSegmentInfo
|
||||
* Contains Segment Information at the period level
|
||||
* @return {toRepresentationsCallback}
|
||||
* Callback map function
|
||||
*/
|
||||
export const toRepresentations =
|
||||
(periodAttributes, periodBaseUrls, periodSegmentInfo) => (adaptationSet) => {
|
||||
const adaptationSetAttributes = parseAttributes(adaptationSet);
|
||||
const adaptationSetBaseUrls = buildBaseUrls(
|
||||
periodBaseUrls,
|
||||
findChildren(adaptationSet, 'BaseURL')
|
||||
);
|
||||
const role = findChildren(adaptationSet, 'Role')[0];
|
||||
const roleAttributes = { role: parseAttributes(role) };
|
||||
|
||||
let attrs = merge(
|
||||
periodAttributes,
|
||||
adaptationSetAttributes,
|
||||
roleAttributes
|
||||
);
|
||||
|
||||
const accessibility = findChildren(adaptationSet, 'Accessibility')[0];
|
||||
const captionServices = parseCaptionServiceMetadata(parseAttributes(accessibility));
|
||||
|
||||
if (captionServices) {
|
||||
attrs = merge(attrs, { captionServices });
|
||||
}
|
||||
|
||||
const label = findChildren(adaptationSet, 'Label')[0];
|
||||
|
||||
if (label && label.childNodes.length) {
|
||||
const labelVal = label.childNodes[0].nodeValue.trim();
|
||||
|
||||
attrs = merge(attrs, { label: labelVal });
|
||||
}
|
||||
|
||||
const contentProtection = generateKeySystemInformation(findChildren(adaptationSet, 'ContentProtection'));
|
||||
|
||||
if (Object.keys(contentProtection).length) {
|
||||
attrs = merge(attrs, { contentProtection });
|
||||
}
|
||||
|
||||
const segmentInfo = getSegmentInformation(adaptationSet);
|
||||
const representations = findChildren(adaptationSet, 'Representation');
|
||||
const adaptationSetSegmentInfo = merge(periodSegmentInfo, segmentInfo);
|
||||
|
||||
return flatten(representations.map(inheritBaseUrls(attrs, adaptationSetBaseUrls, adaptationSetSegmentInfo)));
|
||||
};
|
||||
|
||||
/**
|
||||
* Contains all period information for mapping nodes onto adaptation sets.
|
||||
*
|
||||
* @typedef {Object} PeriodInformation
|
||||
* @property {Node} period.node
|
||||
* Period node from the mpd
|
||||
* @property {Object} period.attributes
|
||||
* Parsed period attributes from node plus any added
|
||||
*/
|
||||
|
||||
/**
|
||||
* Maps a PeriodInformation object to a list of Representation information objects for all
|
||||
* AdaptationSet nodes contained within the Period.
|
||||
*
|
||||
* @name toAdaptationSetsCallback
|
||||
* @function
|
||||
* @param {PeriodInformation} period
|
||||
* Period object containing necessary period information
|
||||
* @param {number} periodStart
|
||||
* Start time of the Period within the mpd
|
||||
* @return {RepresentationInformation[]}
|
||||
* List of objects containing Representaion information
|
||||
*/
|
||||
|
||||
/**
|
||||
* Returns a callback for Array.prototype.map for mapping Period nodes to a list of
|
||||
* Representation information objects
|
||||
*
|
||||
* @param {Object} mpdAttributes
|
||||
* Contains attributes inherited by the mpd
|
||||
* @param {Object[]} mpdBaseUrls
|
||||
* Contains list of objects with resolved base urls and attributes
|
||||
* inherited by the mpd
|
||||
* @return {toAdaptationSetsCallback}
|
||||
* Callback map function
|
||||
*/
|
||||
export const toAdaptationSets = (mpdAttributes, mpdBaseUrls) => (period, index) => {
|
||||
const periodBaseUrls = buildBaseUrls(mpdBaseUrls, findChildren(period.node, 'BaseURL'));
|
||||
const periodAttributes = merge(mpdAttributes, {
|
||||
periodStart: period.attributes.start
|
||||
});
|
||||
|
||||
if (typeof period.attributes.duration === 'number') {
|
||||
periodAttributes.periodDuration = period.attributes.duration;
|
||||
}
|
||||
const adaptationSets = findChildren(period.node, 'AdaptationSet');
|
||||
const periodSegmentInfo = getSegmentInformation(period.node);
|
||||
|
||||
return flatten(adaptationSets.map(toRepresentations(periodAttributes, periodBaseUrls, periodSegmentInfo)));
|
||||
};
|
||||
|
||||
/**
|
||||
* Tranforms an array of content steering nodes into an object
|
||||
* containing CDN content steering information from the MPD manifest.
|
||||
*
|
||||
* For more information on the DASH spec for Content Steering parsing, see:
|
||||
* https://dashif.org/docs/DASH-IF-CTS-00XX-Content-Steering-Community-Review.pdf
|
||||
*
|
||||
* @param {Node[]} contentSteeringNodes
|
||||
* Content steering nodes
|
||||
* @param {Function} eventHandler
|
||||
* The event handler passed into the parser options to handle warnings
|
||||
* @return {Object}
|
||||
* Object containing content steering data
|
||||
*/
|
||||
export const generateContentSteeringInformation = (contentSteeringNodes, eventHandler) => {
|
||||
// If there are more than one ContentSteering tags, throw an error
|
||||
if (contentSteeringNodes.length > 1) {
|
||||
eventHandler({ type: 'warn', message: 'The MPD manifest should contain no more than one ContentSteering tag' });
|
||||
}
|
||||
|
||||
// Return a null value if there are no ContentSteering tags
|
||||
if (!contentSteeringNodes.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const infoFromContentSteeringTag =
|
||||
merge({serverURL: getContent(contentSteeringNodes[0])}, parseAttributes(contentSteeringNodes[0]));
|
||||
|
||||
// Converts `queryBeforeStart` to a boolean, as well as setting the default value
|
||||
// to `false` if it doesn't exist
|
||||
infoFromContentSteeringTag.queryBeforeStart = (infoFromContentSteeringTag.queryBeforeStart === 'true');
|
||||
|
||||
return infoFromContentSteeringTag;
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets Period@start property for a given period.
|
||||
*
|
||||
* @param {Object} options
|
||||
* Options object
|
||||
* @param {Object} options.attributes
|
||||
* Period attributes
|
||||
* @param {Object} [options.priorPeriodAttributes]
|
||||
* Prior period attributes (if prior period is available)
|
||||
* @param {string} options.mpdType
|
||||
* The MPD@type these periods came from
|
||||
* @return {number|null}
|
||||
* The period start, or null if it's an early available period or error
|
||||
*/
|
||||
export const getPeriodStart = ({ attributes, priorPeriodAttributes, mpdType }) => {
|
||||
// Summary of period start time calculation from DASH spec section 5.3.2.1
|
||||
//
|
||||
// A period's start is the first period's start + time elapsed after playing all
|
||||
// prior periods to this one. Periods continue one after the other in time (without
|
||||
// gaps) until the end of the presentation.
|
||||
//
|
||||
// The value of Period@start should be:
|
||||
// 1. if Period@start is present: value of Period@start
|
||||
// 2. if previous period exists and it has @duration: previous Period@start +
|
||||
// previous Period@duration
|
||||
// 3. if this is first period and MPD@type is 'static': 0
|
||||
// 4. in all other cases, consider the period an "early available period" (note: not
|
||||
// currently supported)
|
||||
|
||||
// (1)
|
||||
if (typeof attributes.start === 'number') {
|
||||
return attributes.start;
|
||||
}
|
||||
|
||||
// (2)
|
||||
if (priorPeriodAttributes &&
|
||||
typeof priorPeriodAttributes.start === 'number' &&
|
||||
typeof priorPeriodAttributes.duration === 'number') {
|
||||
return priorPeriodAttributes.start + priorPeriodAttributes.duration;
|
||||
}
|
||||
|
||||
// (3)
|
||||
if (!priorPeriodAttributes && mpdType === 'static') {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// (4)
|
||||
// There is currently no logic for calculating the Period@start value if there is
|
||||
// no Period@start or prior Period@start and Period@duration available. This is not made
|
||||
// explicit by the DASH interop guidelines or the DASH spec, however, since there's
|
||||
// nothing about any other resolution strategies, it's implied. Thus, this case should
|
||||
// be considered an early available period, or error, and null should suffice for both
|
||||
// of those cases.
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Traverses the mpd xml tree to generate a list of Representation information objects
|
||||
* that have inherited attributes from parent nodes
|
||||
*
|
||||
* @param {Node} mpd
|
||||
* The root node of the mpd
|
||||
* @param {Object} options
|
||||
* Available options for inheritAttributes
|
||||
* @param {string} options.manifestUri
|
||||
* The uri source of the mpd
|
||||
* @param {number} options.NOW
|
||||
* Current time per DASH IOP. Default is current time in ms since epoch
|
||||
* @param {number} options.clientOffset
|
||||
* Client time difference from NOW (in milliseconds)
|
||||
* @return {RepresentationInformation[]}
|
||||
* List of objects containing Representation information
|
||||
*/
|
||||
export const inheritAttributes = (mpd, options = {}) => {
|
||||
const {
|
||||
manifestUri = '',
|
||||
NOW = Date.now(),
|
||||
clientOffset = 0,
|
||||
// TODO: For now, we are expecting an eventHandler callback function
|
||||
// to be passed into the mpd parser as an option.
|
||||
// In the future, we should enable stream parsing by using the Stream class from vhs-utils.
|
||||
// This will support new features including a standardized event handler.
|
||||
// See the m3u8 parser for examples of how stream parsing is currently used for HLS parsing.
|
||||
// https://github.com/videojs/vhs-utils/blob/88d6e10c631e57a5af02c5a62bc7376cd456b4f5/src/stream.js#L9
|
||||
eventHandler = function() {}
|
||||
} = options;
|
||||
const periodNodes = findChildren(mpd, 'Period');
|
||||
|
||||
if (!periodNodes.length) {
|
||||
throw new Error(errors.INVALID_NUMBER_OF_PERIOD);
|
||||
}
|
||||
|
||||
const locations = findChildren(mpd, 'Location');
|
||||
|
||||
const mpdAttributes = parseAttributes(mpd);
|
||||
const mpdBaseUrls = buildBaseUrls([{ baseUrl: manifestUri }], findChildren(mpd, 'BaseURL'));
|
||||
const contentSteeringNodes = findChildren(mpd, 'ContentSteering');
|
||||
|
||||
// See DASH spec section 5.3.1.2, Semantics of MPD element. Default type to 'static'.
|
||||
mpdAttributes.type = mpdAttributes.type || 'static';
|
||||
mpdAttributes.sourceDuration = mpdAttributes.mediaPresentationDuration || 0;
|
||||
mpdAttributes.NOW = NOW;
|
||||
mpdAttributes.clientOffset = clientOffset;
|
||||
|
||||
if (locations.length) {
|
||||
mpdAttributes.locations = locations.map(getContent);
|
||||
}
|
||||
|
||||
const periods = [];
|
||||
|
||||
// Since toAdaptationSets acts on individual periods right now, the simplest approach to
|
||||
// adding properties that require looking at prior periods is to parse attributes and add
|
||||
// missing ones before toAdaptationSets is called. If more such properties are added, it
|
||||
// may be better to refactor toAdaptationSets.
|
||||
periodNodes.forEach((node, index) => {
|
||||
const attributes = parseAttributes(node);
|
||||
// Use the last modified prior period, as it may contain added information necessary
|
||||
// for this period.
|
||||
const priorPeriod = periods[index - 1];
|
||||
|
||||
attributes.start = getPeriodStart({
|
||||
attributes,
|
||||
priorPeriodAttributes: priorPeriod ? priorPeriod.attributes : null,
|
||||
mpdType: mpdAttributes.type
|
||||
});
|
||||
|
||||
periods.push({
|
||||
node,
|
||||
attributes
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
locations: mpdAttributes.locations,
|
||||
contentSteeringInfo: generateContentSteeringInformation(contentSteeringNodes, eventHandler),
|
||||
// TODO: There are occurences where this `representationInfo` array contains undesired
|
||||
// duplicates. This generally occurs when there are multiple BaseURL nodes that are
|
||||
// direct children of the MPD node. When we attempt to resolve URLs from a combination of the
|
||||
// parent BaseURL and a child BaseURL, and the value does not resolve,
|
||||
// we end up returning the child BaseURL multiple times.
|
||||
// We need to determine a way to remove these duplicates in a safe way.
|
||||
// See: https://github.com/videojs/mpd-parser/pull/17#discussion_r162750527
|
||||
representationInfo: flatten(periods.map(toAdaptationSets(mpdAttributes, mpdBaseUrls))),
|
||||
eventStream: flatten(periods.map(toEventStream))
|
||||
};
|
||||
};
|
||||
293
VApp/node_modules/mpd-parser/src/parseAttributes.js
generated
vendored
Normal file
293
VApp/node_modules/mpd-parser/src/parseAttributes.js
generated
vendored
Normal file
@@ -0,0 +1,293 @@
|
||||
import { parseDivisionValue } from './utils/string';
|
||||
import { from } from './utils/list';
|
||||
import { parseDuration, parseDate } from './utils/time';
|
||||
|
||||
// TODO: maybe order these in some way that makes it easy to find specific attributes
|
||||
export const parsers = {
|
||||
/**
|
||||
* Specifies the duration of the entire Media Presentation. Format is a duration string
|
||||
* as specified in ISO 8601
|
||||
*
|
||||
* @param {string} value
|
||||
* value of attribute as a string
|
||||
* @return {number}
|
||||
* The duration in seconds
|
||||
*/
|
||||
mediaPresentationDuration(value) {
|
||||
return parseDuration(value);
|
||||
},
|
||||
|
||||
/**
|
||||
* Specifies the Segment availability start time for all Segments referred to in this
|
||||
* MPD. For a dynamic manifest, it specifies the anchor for the earliest availability
|
||||
* time. Format is a date string as specified in ISO 8601
|
||||
*
|
||||
* @param {string} value
|
||||
* value of attribute as a string
|
||||
* @return {number}
|
||||
* The date as seconds from unix epoch
|
||||
*/
|
||||
availabilityStartTime(value) {
|
||||
return parseDate(value) / 1000;
|
||||
},
|
||||
|
||||
/**
|
||||
* Specifies the smallest period between potential changes to the MPD. Format is a
|
||||
* duration string as specified in ISO 8601
|
||||
*
|
||||
* @param {string} value
|
||||
* value of attribute as a string
|
||||
* @return {number}
|
||||
* The duration in seconds
|
||||
*/
|
||||
minimumUpdatePeriod(value) {
|
||||
return parseDuration(value);
|
||||
},
|
||||
|
||||
/**
|
||||
* Specifies the suggested presentation delay. Format is a
|
||||
* duration string as specified in ISO 8601
|
||||
*
|
||||
* @param {string} value
|
||||
* value of attribute as a string
|
||||
* @return {number}
|
||||
* The duration in seconds
|
||||
*/
|
||||
suggestedPresentationDelay(value) {
|
||||
return parseDuration(value);
|
||||
},
|
||||
|
||||
/**
|
||||
* specifices the type of mpd. Can be either "static" or "dynamic"
|
||||
*
|
||||
* @param {string} value
|
||||
* value of attribute as a string
|
||||
*
|
||||
* @return {string}
|
||||
* The type as a string
|
||||
*/
|
||||
type(value) {
|
||||
return value;
|
||||
},
|
||||
|
||||
/**
|
||||
* Specifies the duration of the smallest time shifting buffer for any Representation
|
||||
* in the MPD. Format is a duration string as specified in ISO 8601
|
||||
*
|
||||
* @param {string} value
|
||||
* value of attribute as a string
|
||||
* @return {number}
|
||||
* The duration in seconds
|
||||
*/
|
||||
timeShiftBufferDepth(value) {
|
||||
return parseDuration(value);
|
||||
},
|
||||
|
||||
/**
|
||||
* Specifies the PeriodStart time of the Period relative to the availabilityStarttime.
|
||||
* Format is a duration string as specified in ISO 8601
|
||||
*
|
||||
* @param {string} value
|
||||
* value of attribute as a string
|
||||
* @return {number}
|
||||
* The duration in seconds
|
||||
*/
|
||||
start(value) {
|
||||
return parseDuration(value);
|
||||
},
|
||||
|
||||
/**
|
||||
* Specifies the width of the visual presentation
|
||||
*
|
||||
* @param {string} value
|
||||
* value of attribute as a string
|
||||
* @return {number}
|
||||
* The parsed width
|
||||
*/
|
||||
width(value) {
|
||||
return parseInt(value, 10);
|
||||
},
|
||||
|
||||
/**
|
||||
* Specifies the height of the visual presentation
|
||||
*
|
||||
* @param {string} value
|
||||
* value of attribute as a string
|
||||
* @return {number}
|
||||
* The parsed height
|
||||
*/
|
||||
height(value) {
|
||||
return parseInt(value, 10);
|
||||
},
|
||||
|
||||
/**
|
||||
* Specifies the bitrate of the representation
|
||||
*
|
||||
* @param {string} value
|
||||
* value of attribute as a string
|
||||
* @return {number}
|
||||
* The parsed bandwidth
|
||||
*/
|
||||
bandwidth(value) {
|
||||
return parseInt(value, 10);
|
||||
},
|
||||
|
||||
/**
|
||||
* Specifies the frame rate of the representation
|
||||
*
|
||||
* @param {string} value
|
||||
* value of attribute as a string
|
||||
* @return {number}
|
||||
* The parsed frame rate
|
||||
*/
|
||||
frameRate(value) {
|
||||
return parseDivisionValue(value);
|
||||
},
|
||||
|
||||
/**
|
||||
* Specifies the number of the first Media Segment in this Representation in the Period
|
||||
*
|
||||
* @param {string} value
|
||||
* value of attribute as a string
|
||||
* @return {number}
|
||||
* The parsed number
|
||||
*/
|
||||
startNumber(value) {
|
||||
return parseInt(value, 10);
|
||||
},
|
||||
|
||||
/**
|
||||
* Specifies the timescale in units per seconds
|
||||
*
|
||||
* @param {string} value
|
||||
* value of attribute as a string
|
||||
* @return {number}
|
||||
* The parsed timescale
|
||||
*/
|
||||
timescale(value) {
|
||||
return parseInt(value, 10);
|
||||
},
|
||||
|
||||
/**
|
||||
* Specifies the presentationTimeOffset.
|
||||
*
|
||||
* @param {string} value
|
||||
* value of the attribute as a string
|
||||
*
|
||||
* @return {number}
|
||||
* The parsed presentationTimeOffset
|
||||
*/
|
||||
presentationTimeOffset(value) {
|
||||
return parseInt(value, 10);
|
||||
},
|
||||
|
||||
/**
|
||||
* Specifies the constant approximate Segment duration
|
||||
* NOTE: The <Period> element also contains an @duration attribute. This duration
|
||||
* specifies the duration of the Period. This attribute is currently not
|
||||
* supported by the rest of the parser, however we still check for it to prevent
|
||||
* errors.
|
||||
*
|
||||
* @param {string} value
|
||||
* value of attribute as a string
|
||||
* @return {number}
|
||||
* The parsed duration
|
||||
*/
|
||||
duration(value) {
|
||||
const parsedValue = parseInt(value, 10);
|
||||
|
||||
if (isNaN(parsedValue)) {
|
||||
return parseDuration(value);
|
||||
}
|
||||
|
||||
return parsedValue;
|
||||
},
|
||||
|
||||
/**
|
||||
* Specifies the Segment duration, in units of the value of the @timescale.
|
||||
*
|
||||
* @param {string} value
|
||||
* value of attribute as a string
|
||||
* @return {number}
|
||||
* The parsed duration
|
||||
*/
|
||||
d(value) {
|
||||
return parseInt(value, 10);
|
||||
},
|
||||
|
||||
/**
|
||||
* Specifies the MPD start time, in @timescale units, the first Segment in the series
|
||||
* starts relative to the beginning of the Period
|
||||
*
|
||||
* @param {string} value
|
||||
* value of attribute as a string
|
||||
* @return {number}
|
||||
* The parsed time
|
||||
*/
|
||||
t(value) {
|
||||
return parseInt(value, 10);
|
||||
},
|
||||
|
||||
/**
|
||||
* Specifies the repeat count of the number of following contiguous Segments with the
|
||||
* same duration expressed by the value of @d
|
||||
*
|
||||
* @param {string} value
|
||||
* value of attribute as a string
|
||||
* @return {number}
|
||||
* The parsed number
|
||||
*/
|
||||
r(value) {
|
||||
return parseInt(value, 10);
|
||||
},
|
||||
|
||||
/**
|
||||
* Specifies the presentationTime.
|
||||
*
|
||||
* @param {string} value
|
||||
* value of the attribute as a string
|
||||
*
|
||||
* @return {number}
|
||||
* The parsed presentationTime
|
||||
*/
|
||||
presentationTime(value) {
|
||||
return parseInt(value, 10);
|
||||
},
|
||||
|
||||
/**
|
||||
* Default parser for all other attributes. Acts as a no-op and just returns the value
|
||||
* as a string
|
||||
*
|
||||
* @param {string} value
|
||||
* value of attribute as a string
|
||||
* @return {string}
|
||||
* Unparsed value
|
||||
*/
|
||||
DEFAULT(value) {
|
||||
return value;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets all the attributes and values of the provided node, parses attributes with known
|
||||
* types, and returns an object with attribute names mapped to values.
|
||||
*
|
||||
* @param {Node} el
|
||||
* The node to parse attributes from
|
||||
* @return {Object}
|
||||
* Object with all attributes of el parsed
|
||||
*/
|
||||
export const parseAttributes = (el) => {
|
||||
if (!(el && el.attributes)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return from(el.attributes)
|
||||
.reduce((a, e) => {
|
||||
const parseFn = parsers[e.name] || parsers.DEFAULT;
|
||||
|
||||
a[e.name] = parseFn(e.value);
|
||||
|
||||
return a;
|
||||
}, {});
|
||||
};
|
||||
46
VApp/node_modules/mpd-parser/src/parseUTCTimingScheme.js
generated
vendored
Normal file
46
VApp/node_modules/mpd-parser/src/parseUTCTimingScheme.js
generated
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
import { findChildren } from './utils/xml';
|
||||
import { parseAttributes } from './parseAttributes';
|
||||
import errors from './errors';
|
||||
|
||||
/**
|
||||
* Parses the manifest for a UTCTiming node, returning the nodes attributes if found
|
||||
*
|
||||
* @param {string} mpd
|
||||
* XML string of the MPD manifest
|
||||
* @return {Object|null}
|
||||
* Attributes of UTCTiming node specified in the manifest. Null if none found
|
||||
*/
|
||||
export const parseUTCTimingScheme = (mpd) => {
|
||||
const UTCTimingNode = findChildren(mpd, 'UTCTiming')[0];
|
||||
|
||||
if (!UTCTimingNode) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const attributes = parseAttributes(UTCTimingNode);
|
||||
|
||||
switch (attributes.schemeIdUri) {
|
||||
case 'urn:mpeg:dash:utc:http-head:2014':
|
||||
case 'urn:mpeg:dash:utc:http-head:2012':
|
||||
attributes.method = 'HEAD';
|
||||
break;
|
||||
case 'urn:mpeg:dash:utc:http-xsdate:2014':
|
||||
case 'urn:mpeg:dash:utc:http-iso:2014':
|
||||
case 'urn:mpeg:dash:utc:http-xsdate:2012':
|
||||
case 'urn:mpeg:dash:utc:http-iso:2012':
|
||||
attributes.method = 'GET';
|
||||
break;
|
||||
case 'urn:mpeg:dash:utc:direct:2014':
|
||||
case 'urn:mpeg:dash:utc:direct:2012':
|
||||
attributes.method = 'DIRECT';
|
||||
attributes.value = Date.parse(attributes.value);
|
||||
break;
|
||||
case 'urn:mpeg:dash:utc:http-ntp:2014':
|
||||
case 'urn:mpeg:dash:utc:ntp:2014':
|
||||
case 'urn:mpeg:dash:utc:sntp:2014':
|
||||
default:
|
||||
throw new Error(errors.UNSUPPORTED_UTC_TIMING_SCHEME);
|
||||
}
|
||||
|
||||
return attributes;
|
||||
};
|
||||
240
VApp/node_modules/mpd-parser/src/playlist-merge.js
generated
vendored
Normal file
240
VApp/node_modules/mpd-parser/src/playlist-merge.js
generated
vendored
Normal file
@@ -0,0 +1,240 @@
|
||||
import { forEachMediaGroup } from '@videojs/vhs-utils/es/media-groups';
|
||||
import { union } from './utils/list';
|
||||
|
||||
const SUPPORTED_MEDIA_TYPES = ['AUDIO', 'SUBTITLES'];
|
||||
// allow one 60fps frame as leniency (arbitrarily chosen)
|
||||
const TIME_FUDGE = 1 / 60;
|
||||
|
||||
/**
|
||||
* Given a list of timelineStarts, combines, dedupes, and sorts them.
|
||||
*
|
||||
* @param {TimelineStart[]} timelineStarts - list of timeline starts
|
||||
*
|
||||
* @return {TimelineStart[]} the combined and deduped timeline starts
|
||||
*/
|
||||
export const getUniqueTimelineStarts = (timelineStarts) => {
|
||||
return union(timelineStarts, ({ timeline }) => timeline)
|
||||
.sort((a, b) => (a.timeline > b.timeline) ? 1 : -1);
|
||||
};
|
||||
|
||||
/**
|
||||
* Finds the playlist with the matching NAME attribute.
|
||||
*
|
||||
* @param {Array} playlists - playlists to search through
|
||||
* @param {string} name - the NAME attribute to search for
|
||||
*
|
||||
* @return {Object|null} the matching playlist object, or null
|
||||
*/
|
||||
export const findPlaylistWithName = (playlists, name) => {
|
||||
for (let i = 0; i < playlists.length; i++) {
|
||||
if (playlists[i].attributes.NAME === name) {
|
||||
return playlists[i];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets a flattened array of media group playlists.
|
||||
*
|
||||
* @param {Object} manifest - the main manifest object
|
||||
*
|
||||
* @return {Array} the media group playlists
|
||||
*/
|
||||
export const getMediaGroupPlaylists = (manifest) => {
|
||||
let mediaGroupPlaylists = [];
|
||||
|
||||
forEachMediaGroup(manifest, SUPPORTED_MEDIA_TYPES, (properties, type, group, label) => {
|
||||
mediaGroupPlaylists = mediaGroupPlaylists.concat(properties.playlists || []);
|
||||
});
|
||||
|
||||
return mediaGroupPlaylists;
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the playlist's media sequence numbers.
|
||||
*
|
||||
* @param {Object} config - options object
|
||||
* @param {Object} config.playlist - the playlist to update
|
||||
* @param {number} config.mediaSequence - the mediaSequence number to start with
|
||||
*/
|
||||
export const updateMediaSequenceForPlaylist = ({ playlist, mediaSequence }) => {
|
||||
playlist.mediaSequence = mediaSequence;
|
||||
playlist.segments.forEach((segment, index) => {
|
||||
segment.number = playlist.mediaSequence + index;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the media and discontinuity sequence numbers of newPlaylists given oldPlaylists
|
||||
* and a complete list of timeline starts.
|
||||
*
|
||||
* If no matching playlist is found, only the discontinuity sequence number of the playlist
|
||||
* will be updated.
|
||||
*
|
||||
* Since early available timelines are not supported, at least one segment must be present.
|
||||
*
|
||||
* @param {Object} config - options object
|
||||
* @param {Object[]} oldPlaylists - the old playlists to use as a reference
|
||||
* @param {Object[]} newPlaylists - the new playlists to update
|
||||
* @param {Object} timelineStarts - all timelineStarts seen in the stream to this point
|
||||
*/
|
||||
export const updateSequenceNumbers = ({ oldPlaylists, newPlaylists, timelineStarts }) => {
|
||||
newPlaylists.forEach((playlist) => {
|
||||
playlist.discontinuitySequence = timelineStarts.findIndex(function({
|
||||
timeline
|
||||
}) {
|
||||
return timeline === playlist.timeline;
|
||||
});
|
||||
|
||||
// Playlists NAMEs come from DASH Representation IDs, which are mandatory
|
||||
// (see ISO_23009-1-2012 5.3.5.2).
|
||||
//
|
||||
// If the same Representation existed in a prior Period, it will retain the same NAME.
|
||||
const oldPlaylist = findPlaylistWithName(oldPlaylists, playlist.attributes.NAME);
|
||||
|
||||
if (!oldPlaylist) {
|
||||
// Since this is a new playlist, the media sequence values can start from 0 without
|
||||
// consequence.
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO better support for live SIDX
|
||||
//
|
||||
// As of this writing, mpd-parser does not support multiperiod SIDX (in live or VOD).
|
||||
// This is evident by a playlist only having a single SIDX reference. In a multiperiod
|
||||
// playlist there would need to be multiple SIDX references. In addition, live SIDX is
|
||||
// not supported when the SIDX properties change on refreshes.
|
||||
//
|
||||
// In the future, if support needs to be added, the merging logic here can be called
|
||||
// after SIDX references are resolved. For now, exit early to prevent exceptions being
|
||||
// thrown due to undefined references.
|
||||
if (playlist.sidx) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Since we don't yet support early available timelines, we don't need to support
|
||||
// playlists with no segments.
|
||||
const firstNewSegment = playlist.segments[0];
|
||||
const oldMatchingSegmentIndex = oldPlaylist.segments.findIndex(function(oldSegment) {
|
||||
return (
|
||||
Math.abs(oldSegment.presentationTime - firstNewSegment.presentationTime) < TIME_FUDGE
|
||||
);
|
||||
});
|
||||
|
||||
// No matching segment from the old playlist means the entire playlist was refreshed.
|
||||
// In this case the media sequence should account for this update, and the new segments
|
||||
// should be marked as discontinuous from the prior content, since the last prior
|
||||
// timeline was removed.
|
||||
if (oldMatchingSegmentIndex === -1) {
|
||||
updateMediaSequenceForPlaylist({
|
||||
playlist,
|
||||
mediaSequence: oldPlaylist.mediaSequence + oldPlaylist.segments.length
|
||||
});
|
||||
playlist.segments[0].discontinuity = true;
|
||||
playlist.discontinuityStarts.unshift(0);
|
||||
|
||||
// No matching segment does not necessarily mean there's missing content.
|
||||
//
|
||||
// If the new playlist's timeline is the same as the last seen segment's timeline,
|
||||
// then a discontinuity can be added to identify that there's potentially missing
|
||||
// content. If there's no missing content, the discontinuity should still be rather
|
||||
// harmless. It's possible that if segment durations are accurate enough, that the
|
||||
// existence of a gap can be determined using the presentation times and durations,
|
||||
// but if the segment timing info is off, it may introduce more problems than simply
|
||||
// adding the discontinuity.
|
||||
//
|
||||
// If the new playlist's timeline is different from the last seen segment's timeline,
|
||||
// then a discontinuity can be added to identify that this is the first seen segment
|
||||
// of a new timeline. However, the logic at the start of this function that
|
||||
// determined the disconinuity sequence by timeline index is now off by one (the
|
||||
// discontinuity of the newest timeline hasn't yet fallen off the manifest...since
|
||||
// we added it), so the disconinuity sequence must be decremented.
|
||||
//
|
||||
// A period may also have a duration of zero, so the case of no segments is handled
|
||||
// here even though we don't yet support early available periods.
|
||||
if ((!oldPlaylist.segments.length && playlist.timeline > oldPlaylist.timeline) ||
|
||||
(oldPlaylist.segments.length && playlist.timeline >
|
||||
oldPlaylist.segments[oldPlaylist.segments.length - 1].timeline)) {
|
||||
playlist.discontinuitySequence--;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// If the first segment matched with a prior segment on a discontinuity (it's matching
|
||||
// on the first segment of a period), then the discontinuitySequence shouldn't be the
|
||||
// timeline's matching one, but instead should be the one prior, and the first segment
|
||||
// of the new manifest should be marked with a discontinuity.
|
||||
//
|
||||
// The reason for this special case is that discontinuity sequence shows how many
|
||||
// discontinuities have fallen off of the playlist, and discontinuities are marked on
|
||||
// the first segment of a new "timeline." Because of this, while DASH will retain that
|
||||
// Period while the "timeline" exists, HLS keeps track of it via the discontinuity
|
||||
// sequence, and that first segment is an indicator, but can be removed before that
|
||||
// timeline is gone.
|
||||
const oldMatchingSegment = oldPlaylist.segments[oldMatchingSegmentIndex];
|
||||
|
||||
if (oldMatchingSegment.discontinuity && !firstNewSegment.discontinuity) {
|
||||
firstNewSegment.discontinuity = true;
|
||||
playlist.discontinuityStarts.unshift(0);
|
||||
playlist.discontinuitySequence--;
|
||||
}
|
||||
|
||||
updateMediaSequenceForPlaylist({
|
||||
playlist,
|
||||
mediaSequence: oldPlaylist.segments[oldMatchingSegmentIndex].number
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Given an old parsed manifest object and a new parsed manifest object, updates the
|
||||
* sequence and timing values within the new manifest to ensure that it lines up with the
|
||||
* old.
|
||||
*
|
||||
* @param {Array} oldManifest - the old main manifest object
|
||||
* @param {Array} newManifest - the new main manifest object
|
||||
*
|
||||
* @return {Object} the updated new manifest object
|
||||
*/
|
||||
export const positionManifestOnTimeline = ({ oldManifest, newManifest }) => {
|
||||
// Starting from v4.1.2 of the IOP, section 4.4.3.3 states:
|
||||
//
|
||||
// "MPD@availabilityStartTime and Period@start shall not be changed over MPD updates."
|
||||
//
|
||||
// This was added from https://github.com/Dash-Industry-Forum/DASH-IF-IOP/issues/160
|
||||
//
|
||||
// Because of this change, and the difficulty of supporting periods with changing start
|
||||
// times, periods with changing start times are not supported. This makes the logic much
|
||||
// simpler, since periods with the same start time can be considerred the same period
|
||||
// across refreshes.
|
||||
//
|
||||
// To give an example as to the difficulty of handling periods where the start time may
|
||||
// change, if a single period manifest is refreshed with another manifest with a single
|
||||
// period, and both the start and end times are increased, then the only way to determine
|
||||
// if it's a new period or an old one that has changed is to look through the segments of
|
||||
// each playlist and determine the presentation time bounds to find a match. In addition,
|
||||
// if the period start changed to exceed the old period end, then there would be no
|
||||
// match, and it would not be possible to determine whether the refreshed period is a new
|
||||
// one or the old one.
|
||||
const oldPlaylists = oldManifest.playlists.concat(getMediaGroupPlaylists(oldManifest));
|
||||
const newPlaylists = newManifest.playlists.concat(getMediaGroupPlaylists(newManifest));
|
||||
|
||||
// Save all seen timelineStarts to the new manifest. Although this potentially means that
|
||||
// there's a "memory leak" in that it will never stop growing, in reality, only a couple
|
||||
// of properties are saved for each seen Period. Even long running live streams won't
|
||||
// generate too many Periods, unless the stream is watched for decades. In the future,
|
||||
// this can be optimized by mapping to discontinuity sequence numbers for each timeline,
|
||||
// but it may not become an issue, and the additional info can be useful for debugging.
|
||||
newManifest.timelineStarts = getUniqueTimelineStarts([oldManifest.timelineStarts,
|
||||
newManifest.timelineStarts]);
|
||||
|
||||
updateSequenceNumbers({
|
||||
oldPlaylists,
|
||||
newPlaylists,
|
||||
timelineStarts: newManifest.timelineStarts
|
||||
});
|
||||
|
||||
return newManifest;
|
||||
};
|
||||
172
VApp/node_modules/mpd-parser/src/segment/durationTimeParser.js
generated
vendored
Normal file
172
VApp/node_modules/mpd-parser/src/segment/durationTimeParser.js
generated
vendored
Normal file
@@ -0,0 +1,172 @@
|
||||
import { range } from '../utils/list';
|
||||
|
||||
/**
|
||||
* parse the end number attribue that can be a string
|
||||
* number, or undefined.
|
||||
*
|
||||
* @param {string|number|undefined} endNumber
|
||||
* The end number attribute.
|
||||
*
|
||||
* @return {number|null}
|
||||
* The result of parsing the end number.
|
||||
*/
|
||||
const parseEndNumber = (endNumber) => {
|
||||
if (endNumber && typeof endNumber !== 'number') {
|
||||
endNumber = parseInt(endNumber, 10);
|
||||
}
|
||||
|
||||
if (isNaN(endNumber)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return endNumber;
|
||||
};
|
||||
|
||||
/**
|
||||
* Functions for calculating the range of available segments in static and dynamic
|
||||
* manifests.
|
||||
*/
|
||||
export const segmentRange = {
|
||||
/**
|
||||
* Returns the entire range of available segments for a static MPD
|
||||
*
|
||||
* @param {Object} attributes
|
||||
* Inheritied MPD attributes
|
||||
* @return {{ start: number, end: number }}
|
||||
* The start and end numbers for available segments
|
||||
*/
|
||||
static(attributes) {
|
||||
const {
|
||||
duration,
|
||||
timescale = 1,
|
||||
sourceDuration,
|
||||
periodDuration
|
||||
} = attributes;
|
||||
const endNumber = parseEndNumber(attributes.endNumber);
|
||||
const segmentDuration = duration / timescale;
|
||||
|
||||
if (typeof endNumber === 'number') {
|
||||
return { start: 0, end: endNumber };
|
||||
}
|
||||
|
||||
if (typeof periodDuration === 'number') {
|
||||
return { start: 0, end: periodDuration / segmentDuration };
|
||||
}
|
||||
|
||||
return { start: 0, end: sourceDuration / segmentDuration };
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the current live window range of available segments for a dynamic MPD
|
||||
*
|
||||
* @param {Object} attributes
|
||||
* Inheritied MPD attributes
|
||||
* @return {{ start: number, end: number }}
|
||||
* The start and end numbers for available segments
|
||||
*/
|
||||
dynamic(attributes) {
|
||||
const {
|
||||
NOW,
|
||||
clientOffset,
|
||||
availabilityStartTime,
|
||||
timescale = 1,
|
||||
duration,
|
||||
periodStart = 0,
|
||||
minimumUpdatePeriod = 0,
|
||||
timeShiftBufferDepth = Infinity
|
||||
} = attributes;
|
||||
const endNumber = parseEndNumber(attributes.endNumber);
|
||||
// clientOffset is passed in at the top level of mpd-parser and is an offset calculated
|
||||
// after retrieving UTC server time.
|
||||
const now = (NOW + clientOffset) / 1000;
|
||||
// WC stands for Wall Clock.
|
||||
// Convert the period start time to EPOCH.
|
||||
const periodStartWC = availabilityStartTime + periodStart;
|
||||
// Period end in EPOCH is manifest's retrieval time + time until next update.
|
||||
const periodEndWC = now + minimumUpdatePeriod;
|
||||
const periodDuration = periodEndWC - periodStartWC;
|
||||
const segmentCount = Math.ceil(periodDuration * timescale / duration);
|
||||
const availableStart =
|
||||
Math.floor((now - periodStartWC - timeShiftBufferDepth) * timescale / duration);
|
||||
const availableEnd = Math.floor((now - periodStartWC) * timescale / duration);
|
||||
|
||||
return {
|
||||
start: Math.max(0, availableStart),
|
||||
end: typeof endNumber === 'number' ? endNumber : Math.min(segmentCount, availableEnd)
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Maps a range of numbers to objects with information needed to build the corresponding
|
||||
* segment list
|
||||
*
|
||||
* @name toSegmentsCallback
|
||||
* @function
|
||||
* @param {number} number
|
||||
* Number of the segment
|
||||
* @param {number} index
|
||||
* Index of the number in the range list
|
||||
* @return {{ number: Number, duration: Number, timeline: Number, time: Number }}
|
||||
* Object with segment timing and duration info
|
||||
*/
|
||||
|
||||
/**
|
||||
* Returns a callback for Array.prototype.map for mapping a range of numbers to
|
||||
* information needed to build the segment list.
|
||||
*
|
||||
* @param {Object} attributes
|
||||
* Inherited MPD attributes
|
||||
* @return {toSegmentsCallback}
|
||||
* Callback map function
|
||||
*/
|
||||
export const toSegments = (attributes) => (number) => {
|
||||
const {
|
||||
duration,
|
||||
timescale = 1,
|
||||
periodStart,
|
||||
startNumber = 1
|
||||
} = attributes;
|
||||
|
||||
return {
|
||||
number: startNumber + number,
|
||||
duration: duration / timescale,
|
||||
timeline: periodStart,
|
||||
time: number * duration
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a list of objects containing segment timing and duration info used for
|
||||
* building the list of segments. This uses the @duration attribute specified
|
||||
* in the MPD manifest to derive the range of segments.
|
||||
*
|
||||
* @param {Object} attributes
|
||||
* Inherited MPD attributes
|
||||
* @return {{number: number, duration: number, time: number, timeline: number}[]}
|
||||
* List of Objects with segment timing and duration info
|
||||
*/
|
||||
export const parseByDuration = (attributes) => {
|
||||
const {
|
||||
type,
|
||||
duration,
|
||||
timescale = 1,
|
||||
periodDuration,
|
||||
sourceDuration
|
||||
} = attributes;
|
||||
|
||||
const { start, end } = segmentRange[type](attributes);
|
||||
const segments = range(start, end).map(toSegments(attributes));
|
||||
|
||||
if (type === 'static') {
|
||||
const index = segments.length - 1;
|
||||
// section is either a period or the full source
|
||||
const sectionDuration =
|
||||
typeof periodDuration === 'number' ? periodDuration : sourceDuration;
|
||||
|
||||
// final segment may be less than full segment duration
|
||||
segments[index].duration = sectionDuration - (duration / timescale * index);
|
||||
}
|
||||
|
||||
return segments;
|
||||
};
|
||||
157
VApp/node_modules/mpd-parser/src/segment/segmentBase.js
generated
vendored
Normal file
157
VApp/node_modules/mpd-parser/src/segment/segmentBase.js
generated
vendored
Normal file
@@ -0,0 +1,157 @@
|
||||
import errors from '../errors';
|
||||
import urlTypeConverter from './urlType';
|
||||
import { parseByDuration } from './durationTimeParser';
|
||||
import window from 'global/window';
|
||||
|
||||
/**
|
||||
* Translates SegmentBase into a set of segments.
|
||||
* (DASH SPEC Section 5.3.9.3.2) contains a set of <SegmentURL> nodes. Each
|
||||
* node should be translated into segment.
|
||||
*
|
||||
* @param {Object} attributes
|
||||
* Object containing all inherited attributes from parent elements with attribute
|
||||
* names as keys
|
||||
* @return {Object.<Array>} list of segments
|
||||
*/
|
||||
export const segmentsFromBase = (attributes) => {
|
||||
const {
|
||||
baseUrl,
|
||||
initialization = {},
|
||||
sourceDuration,
|
||||
indexRange = '',
|
||||
periodStart,
|
||||
presentationTime,
|
||||
number = 0,
|
||||
duration
|
||||
} = attributes;
|
||||
|
||||
// base url is required for SegmentBase to work, per spec (Section 5.3.9.2.1)
|
||||
if (!baseUrl) {
|
||||
throw new Error(errors.NO_BASE_URL);
|
||||
}
|
||||
|
||||
const initSegment = urlTypeConverter({
|
||||
baseUrl,
|
||||
source: initialization.sourceURL,
|
||||
range: initialization.range
|
||||
});
|
||||
|
||||
const segment = urlTypeConverter({ baseUrl, source: baseUrl, indexRange });
|
||||
|
||||
segment.map = initSegment;
|
||||
|
||||
// If there is a duration, use it, otherwise use the given duration of the source
|
||||
// (since SegmentBase is only for one total segment)
|
||||
if (duration) {
|
||||
const segmentTimeInfo = parseByDuration(attributes);
|
||||
|
||||
if (segmentTimeInfo.length) {
|
||||
segment.duration = segmentTimeInfo[0].duration;
|
||||
segment.timeline = segmentTimeInfo[0].timeline;
|
||||
}
|
||||
} else if (sourceDuration) {
|
||||
segment.duration = sourceDuration;
|
||||
segment.timeline = periodStart;
|
||||
}
|
||||
|
||||
// If presentation time is provided, these segments are being generated by SIDX
|
||||
// references, and should use the time provided. For the general case of SegmentBase,
|
||||
// there should only be one segment in the period, so its presentation time is the same
|
||||
// as its period start.
|
||||
segment.presentationTime = presentationTime || periodStart;
|
||||
segment.number = number;
|
||||
|
||||
return [segment];
|
||||
};
|
||||
|
||||
/**
|
||||
* Given a playlist, a sidx box, and a baseUrl, update the segment list of the playlist
|
||||
* according to the sidx information given.
|
||||
*
|
||||
* playlist.sidx has metadadata about the sidx where-as the sidx param
|
||||
* is the parsed sidx box itself.
|
||||
*
|
||||
* @param {Object} playlist the playlist to update the sidx information for
|
||||
* @param {Object} sidx the parsed sidx box
|
||||
* @return {Object} the playlist object with the updated sidx information
|
||||
*/
|
||||
export const addSidxSegmentsToPlaylist = (playlist, sidx, baseUrl) => {
|
||||
// Retain init segment information
|
||||
const initSegment = playlist.sidx.map ? playlist.sidx.map : null;
|
||||
// Retain source duration from initial main manifest parsing
|
||||
const sourceDuration = playlist.sidx.duration;
|
||||
// Retain source timeline
|
||||
const timeline = playlist.timeline || 0;
|
||||
const sidxByteRange = playlist.sidx.byterange;
|
||||
const sidxEnd = sidxByteRange.offset + sidxByteRange.length;
|
||||
// Retain timescale of the parsed sidx
|
||||
const timescale = sidx.timescale;
|
||||
// referenceType 1 refers to other sidx boxes
|
||||
const mediaReferences = sidx.references.filter(r => r.referenceType !== 1);
|
||||
const segments = [];
|
||||
const type = playlist.endList ? 'static' : 'dynamic';
|
||||
const periodStart = playlist.sidx.timeline;
|
||||
let presentationTime = periodStart;
|
||||
let number = playlist.mediaSequence || 0;
|
||||
|
||||
// firstOffset is the offset from the end of the sidx box
|
||||
let startIndex;
|
||||
|
||||
// eslint-disable-next-line
|
||||
if (typeof sidx.firstOffset === 'bigint') {
|
||||
startIndex = window.BigInt(sidxEnd) + sidx.firstOffset;
|
||||
} else {
|
||||
startIndex = sidxEnd + sidx.firstOffset;
|
||||
}
|
||||
|
||||
for (let i = 0; i < mediaReferences.length; i++) {
|
||||
const reference = sidx.references[i];
|
||||
// size of the referenced (sub)segment
|
||||
const size = reference.referencedSize;
|
||||
// duration of the referenced (sub)segment, in the timescale
|
||||
// this will be converted to seconds when generating segments
|
||||
const duration = reference.subsegmentDuration;
|
||||
// should be an inclusive range
|
||||
let endIndex;
|
||||
|
||||
// eslint-disable-next-line
|
||||
if (typeof startIndex === 'bigint') {
|
||||
endIndex = startIndex + window.BigInt(size) - window.BigInt(1);
|
||||
} else {
|
||||
endIndex = startIndex + size - 1;
|
||||
}
|
||||
const indexRange = `${startIndex}-${endIndex}`;
|
||||
|
||||
const attributes = {
|
||||
baseUrl,
|
||||
timescale,
|
||||
timeline,
|
||||
periodStart,
|
||||
presentationTime,
|
||||
number,
|
||||
duration,
|
||||
sourceDuration,
|
||||
indexRange,
|
||||
type
|
||||
};
|
||||
|
||||
const segment = segmentsFromBase(attributes)[0];
|
||||
|
||||
if (initSegment) {
|
||||
segment.map = initSegment;
|
||||
}
|
||||
|
||||
segments.push(segment);
|
||||
if (typeof startIndex === 'bigint') {
|
||||
startIndex += window.BigInt(size);
|
||||
} else {
|
||||
startIndex += size;
|
||||
}
|
||||
presentationTime += duration / timescale;
|
||||
number++;
|
||||
}
|
||||
|
||||
playlist.segments = segments;
|
||||
|
||||
return playlist;
|
||||
};
|
||||
99
VApp/node_modules/mpd-parser/src/segment/segmentList.js
generated
vendored
Normal file
99
VApp/node_modules/mpd-parser/src/segment/segmentList.js
generated
vendored
Normal file
@@ -0,0 +1,99 @@
|
||||
import { parseByTimeline } from './timelineTimeParser';
|
||||
import { parseByDuration } from './durationTimeParser';
|
||||
import urlTypeConverter from './urlType';
|
||||
import errors from '../errors';
|
||||
|
||||
/**
|
||||
* Converts a <SegmentUrl> (of type URLType from the DASH spec 5.3.9.2 Table 14)
|
||||
* to an object that matches the output of a segment in videojs/mpd-parser
|
||||
*
|
||||
* @param {Object} attributes
|
||||
* Object containing all inherited attributes from parent elements with attribute
|
||||
* names as keys
|
||||
* @param {Object} segmentUrl
|
||||
* <SegmentURL> node to translate into a segment object
|
||||
* @return {Object} translated segment object
|
||||
*/
|
||||
const SegmentURLToSegmentObject = (attributes, segmentUrl) => {
|
||||
const { baseUrl, initialization = {} } = attributes;
|
||||
|
||||
const initSegment = urlTypeConverter({
|
||||
baseUrl,
|
||||
source: initialization.sourceURL,
|
||||
range: initialization.range
|
||||
});
|
||||
|
||||
const segment = urlTypeConverter({
|
||||
baseUrl,
|
||||
source: segmentUrl.media,
|
||||
range: segmentUrl.mediaRange
|
||||
});
|
||||
|
||||
segment.map = initSegment;
|
||||
|
||||
return segment;
|
||||
};
|
||||
|
||||
/**
|
||||
* Generates a list of segments using information provided by the SegmentList element
|
||||
* SegmentList (DASH SPEC Section 5.3.9.3.2) contains a set of <SegmentURL> nodes. Each
|
||||
* node should be translated into segment.
|
||||
*
|
||||
* @param {Object} attributes
|
||||
* Object containing all inherited attributes from parent elements with attribute
|
||||
* names as keys
|
||||
* @param {Object[]|undefined} segmentTimeline
|
||||
* List of objects representing the attributes of each S element contained within
|
||||
* the SegmentTimeline element
|
||||
* @return {Object.<Array>} list of segments
|
||||
*/
|
||||
export const segmentsFromList = (attributes, segmentTimeline) => {
|
||||
const {
|
||||
duration,
|
||||
segmentUrls = [],
|
||||
periodStart
|
||||
} = attributes;
|
||||
|
||||
// Per spec (5.3.9.2.1) no way to determine segment duration OR
|
||||
// if both SegmentTimeline and @duration are defined, it is outside of spec.
|
||||
if ((!duration && !segmentTimeline) ||
|
||||
(duration && segmentTimeline)) {
|
||||
throw new Error(errors.SEGMENT_TIME_UNSPECIFIED);
|
||||
}
|
||||
|
||||
const segmentUrlMap = segmentUrls.map(segmentUrlObject =>
|
||||
SegmentURLToSegmentObject(attributes, segmentUrlObject));
|
||||
let segmentTimeInfo;
|
||||
|
||||
if (duration) {
|
||||
segmentTimeInfo = parseByDuration(attributes);
|
||||
}
|
||||
|
||||
if (segmentTimeline) {
|
||||
segmentTimeInfo = parseByTimeline(attributes, segmentTimeline);
|
||||
}
|
||||
|
||||
const segments = segmentTimeInfo.map((segmentTime, index) => {
|
||||
if (segmentUrlMap[index]) {
|
||||
const segment = segmentUrlMap[index];
|
||||
// See DASH spec section 5.3.9.2.2
|
||||
// - if timescale isn't present on any level, default to 1.
|
||||
const timescale = attributes.timescale || 1;
|
||||
// - if presentationTimeOffset isn't present on any level, default to 0
|
||||
const presentationTimeOffset = attributes.presentationTimeOffset || 0;
|
||||
|
||||
segment.timeline = segmentTime.timeline;
|
||||
segment.duration = segmentTime.duration;
|
||||
segment.number = segmentTime.number;
|
||||
segment.presentationTime =
|
||||
periodStart + ((segmentTime.time - presentationTimeOffset) / timescale);
|
||||
|
||||
return segment;
|
||||
}
|
||||
// Since we're mapping we should get rid of any blank segments (in case
|
||||
// the given SegmentTimeline is handling for more elements than we have
|
||||
// SegmentURLs for).
|
||||
}).filter(segment => segment);
|
||||
|
||||
return segments;
|
||||
};
|
||||
182
VApp/node_modules/mpd-parser/src/segment/segmentTemplate.js
generated
vendored
Normal file
182
VApp/node_modules/mpd-parser/src/segment/segmentTemplate.js
generated
vendored
Normal file
@@ -0,0 +1,182 @@
|
||||
import resolveUrl from '@videojs/vhs-utils/es/resolve-url';
|
||||
import urlTypeToSegment from './urlType';
|
||||
import { parseByTimeline } from './timelineTimeParser';
|
||||
import { parseByDuration } from './durationTimeParser';
|
||||
|
||||
const identifierPattern = /\$([A-z]*)(?:(%0)([0-9]+)d)?\$/g;
|
||||
|
||||
/**
|
||||
* Replaces template identifiers with corresponding values. To be used as the callback
|
||||
* for String.prototype.replace
|
||||
*
|
||||
* @name replaceCallback
|
||||
* @function
|
||||
* @param {string} match
|
||||
* Entire match of identifier
|
||||
* @param {string} identifier
|
||||
* Name of matched identifier
|
||||
* @param {string} format
|
||||
* Format tag string. Its presence indicates that padding is expected
|
||||
* @param {string} width
|
||||
* Desired length of the replaced value. Values less than this width shall be left
|
||||
* zero padded
|
||||
* @return {string}
|
||||
* Replacement for the matched identifier
|
||||
*/
|
||||
|
||||
/**
|
||||
* Returns a function to be used as a callback for String.prototype.replace to replace
|
||||
* template identifiers
|
||||
*
|
||||
* @param {Obect} values
|
||||
* Object containing values that shall be used to replace known identifiers
|
||||
* @param {number} values.RepresentationID
|
||||
* Value of the Representation@id attribute
|
||||
* @param {number} values.Number
|
||||
* Number of the corresponding segment
|
||||
* @param {number} values.Bandwidth
|
||||
* Value of the Representation@bandwidth attribute.
|
||||
* @param {number} values.Time
|
||||
* Timestamp value of the corresponding segment
|
||||
* @return {replaceCallback}
|
||||
* Callback to be used with String.prototype.replace to replace identifiers
|
||||
*/
|
||||
export const identifierReplacement = (values) => (match, identifier, format, width) => {
|
||||
if (match === '$$') {
|
||||
// escape sequence
|
||||
return '$';
|
||||
}
|
||||
|
||||
if (typeof values[identifier] === 'undefined') {
|
||||
return match;
|
||||
}
|
||||
|
||||
const value = '' + values[identifier];
|
||||
|
||||
if (identifier === 'RepresentationID') {
|
||||
// Format tag shall not be present with RepresentationID
|
||||
return value;
|
||||
}
|
||||
|
||||
if (!format) {
|
||||
width = 1;
|
||||
} else {
|
||||
width = parseInt(width, 10);
|
||||
}
|
||||
|
||||
if (value.length >= width) {
|
||||
return value;
|
||||
}
|
||||
|
||||
return `${(new Array(width - value.length + 1)).join('0')}${value}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Constructs a segment url from a template string
|
||||
*
|
||||
* @param {string} url
|
||||
* Template string to construct url from
|
||||
* @param {Obect} values
|
||||
* Object containing values that shall be used to replace known identifiers
|
||||
* @param {number} values.RepresentationID
|
||||
* Value of the Representation@id attribute
|
||||
* @param {number} values.Number
|
||||
* Number of the corresponding segment
|
||||
* @param {number} values.Bandwidth
|
||||
* Value of the Representation@bandwidth attribute.
|
||||
* @param {number} values.Time
|
||||
* Timestamp value of the corresponding segment
|
||||
* @return {string}
|
||||
* Segment url with identifiers replaced
|
||||
*/
|
||||
export const constructTemplateUrl = (url, values) =>
|
||||
url.replace(identifierPattern, identifierReplacement(values));
|
||||
|
||||
/**
|
||||
* Generates a list of objects containing timing and duration information about each
|
||||
* segment needed to generate segment uris and the complete segment object
|
||||
*
|
||||
* @param {Object} attributes
|
||||
* Object containing all inherited attributes from parent elements with attribute
|
||||
* names as keys
|
||||
* @param {Object[]|undefined} segmentTimeline
|
||||
* List of objects representing the attributes of each S element contained within
|
||||
* the SegmentTimeline element
|
||||
* @return {{number: number, duration: number, time: number, timeline: number}[]}
|
||||
* List of Objects with segment timing and duration info
|
||||
*/
|
||||
export const parseTemplateInfo = (attributes, segmentTimeline) => {
|
||||
if (!attributes.duration && !segmentTimeline) {
|
||||
// if neither @duration or SegmentTimeline are present, then there shall be exactly
|
||||
// one media segment
|
||||
return [{
|
||||
number: attributes.startNumber || 1,
|
||||
duration: attributes.sourceDuration,
|
||||
time: 0,
|
||||
timeline: attributes.periodStart
|
||||
}];
|
||||
}
|
||||
|
||||
if (attributes.duration) {
|
||||
return parseByDuration(attributes);
|
||||
}
|
||||
|
||||
return parseByTimeline(attributes, segmentTimeline);
|
||||
};
|
||||
|
||||
/**
|
||||
* Generates a list of segments using information provided by the SegmentTemplate element
|
||||
*
|
||||
* @param {Object} attributes
|
||||
* Object containing all inherited attributes from parent elements with attribute
|
||||
* names as keys
|
||||
* @param {Object[]|undefined} segmentTimeline
|
||||
* List of objects representing the attributes of each S element contained within
|
||||
* the SegmentTimeline element
|
||||
* @return {Object[]}
|
||||
* List of segment objects
|
||||
*/
|
||||
export const segmentsFromTemplate = (attributes, segmentTimeline) => {
|
||||
const templateValues = {
|
||||
RepresentationID: attributes.id,
|
||||
Bandwidth: attributes.bandwidth || 0
|
||||
};
|
||||
|
||||
const { initialization = { sourceURL: '', range: '' } } = attributes;
|
||||
|
||||
const mapSegment = urlTypeToSegment({
|
||||
baseUrl: attributes.baseUrl,
|
||||
source: constructTemplateUrl(initialization.sourceURL, templateValues),
|
||||
range: initialization.range
|
||||
});
|
||||
|
||||
const segments = parseTemplateInfo(attributes, segmentTimeline);
|
||||
|
||||
return segments.map(segment => {
|
||||
templateValues.Number = segment.number;
|
||||
templateValues.Time = segment.time;
|
||||
|
||||
const uri = constructTemplateUrl(attributes.media || '', templateValues);
|
||||
// See DASH spec section 5.3.9.2.2
|
||||
// - if timescale isn't present on any level, default to 1.
|
||||
const timescale = attributes.timescale || 1;
|
||||
// - if presentationTimeOffset isn't present on any level, default to 0
|
||||
const presentationTimeOffset = attributes.presentationTimeOffset || 0;
|
||||
const presentationTime =
|
||||
// Even if the @t attribute is not specified for the segment, segment.time is
|
||||
// calculated in mpd-parser prior to this, so it's assumed to be available.
|
||||
attributes.periodStart + ((segment.time - presentationTimeOffset) / timescale);
|
||||
|
||||
const map = {
|
||||
uri,
|
||||
timeline: segment.timeline,
|
||||
duration: segment.duration,
|
||||
resolvedUri: resolveUrl(attributes.baseUrl || '', uri),
|
||||
map: mapSegment,
|
||||
number: segment.number,
|
||||
presentationTime
|
||||
};
|
||||
|
||||
return map;
|
||||
});
|
||||
};
|
||||
129
VApp/node_modules/mpd-parser/src/segment/timelineTimeParser.js
generated
vendored
Normal file
129
VApp/node_modules/mpd-parser/src/segment/timelineTimeParser.js
generated
vendored
Normal file
@@ -0,0 +1,129 @@
|
||||
/**
|
||||
* Calculates the R (repetition) value for a live stream (for the final segment
|
||||
* in a manifest where the r value is negative 1)
|
||||
*
|
||||
* @param {Object} attributes
|
||||
* Object containing all inherited attributes from parent elements with attribute
|
||||
* names as keys
|
||||
* @param {number} time
|
||||
* current time (typically the total time up until the final segment)
|
||||
* @param {number} duration
|
||||
* duration property for the given <S />
|
||||
*
|
||||
* @return {number}
|
||||
* R value to reach the end of the given period
|
||||
*/
|
||||
const getLiveRValue = (attributes, time, duration) => {
|
||||
const {
|
||||
NOW,
|
||||
clientOffset,
|
||||
availabilityStartTime,
|
||||
timescale = 1,
|
||||
periodStart = 0,
|
||||
minimumUpdatePeriod = 0
|
||||
} = attributes;
|
||||
const now = (NOW + clientOffset) / 1000;
|
||||
const periodStartWC = availabilityStartTime + periodStart;
|
||||
const periodEndWC = now + minimumUpdatePeriod;
|
||||
const periodDuration = periodEndWC - periodStartWC;
|
||||
|
||||
return Math.ceil(((periodDuration * timescale) - time) / duration);
|
||||
};
|
||||
|
||||
/**
|
||||
* Uses information provided by SegmentTemplate.SegmentTimeline to determine segment
|
||||
* timing and duration
|
||||
*
|
||||
* @param {Object} attributes
|
||||
* Object containing all inherited attributes from parent elements with attribute
|
||||
* names as keys
|
||||
* @param {Object[]} segmentTimeline
|
||||
* List of objects representing the attributes of each S element contained within
|
||||
*
|
||||
* @return {{number: number, duration: number, time: number, timeline: number}[]}
|
||||
* List of Objects with segment timing and duration info
|
||||
*/
|
||||
export const parseByTimeline = (attributes, segmentTimeline) => {
|
||||
const {
|
||||
type,
|
||||
minimumUpdatePeriod = 0,
|
||||
media = '',
|
||||
sourceDuration,
|
||||
timescale = 1,
|
||||
startNumber = 1,
|
||||
periodStart: timeline
|
||||
} = attributes;
|
||||
const segments = [];
|
||||
let time = -1;
|
||||
|
||||
for (let sIndex = 0; sIndex < segmentTimeline.length; sIndex++) {
|
||||
const S = segmentTimeline[sIndex];
|
||||
const duration = S.d;
|
||||
const repeat = S.r || 0;
|
||||
const segmentTime = S.t || 0;
|
||||
|
||||
if (time < 0) {
|
||||
// first segment
|
||||
time = segmentTime;
|
||||
}
|
||||
|
||||
if (segmentTime && segmentTime > time) {
|
||||
// discontinuity
|
||||
|
||||
// TODO: How to handle this type of discontinuity
|
||||
// timeline++ here would treat it like HLS discontuity and content would
|
||||
// get appended without gap
|
||||
// E.G.
|
||||
// <S t="0" d="1" />
|
||||
// <S d="1" />
|
||||
// <S d="1" />
|
||||
// <S t="5" d="1" />
|
||||
// would have $Time$ values of [0, 1, 2, 5]
|
||||
// should this be appened at time positions [0, 1, 2, 3],(#EXT-X-DISCONTINUITY)
|
||||
// or [0, 1, 2, gap, gap, 5]? (#EXT-X-GAP)
|
||||
// does the value of sourceDuration consider this when calculating arbitrary
|
||||
// negative @r repeat value?
|
||||
// E.G. Same elements as above with this added at the end
|
||||
// <S d="1" r="-1" />
|
||||
// with a sourceDuration of 10
|
||||
// Would the 2 gaps be included in the time duration calculations resulting in
|
||||
// 8 segments with $Time$ values of [0, 1, 2, 5, 6, 7, 8, 9] or 10 segments
|
||||
// with $Time$ values of [0, 1, 2, 5, 6, 7, 8, 9, 10, 11] ?
|
||||
|
||||
time = segmentTime;
|
||||
}
|
||||
|
||||
let count;
|
||||
|
||||
if (repeat < 0) {
|
||||
const nextS = sIndex + 1;
|
||||
|
||||
if (nextS === segmentTimeline.length) {
|
||||
// last segment
|
||||
if (type === 'dynamic' &&
|
||||
minimumUpdatePeriod > 0 &&
|
||||
media.indexOf('$Number$') > 0) {
|
||||
count = getLiveRValue(attributes, time, duration);
|
||||
} else {
|
||||
// TODO: This may be incorrect depending on conclusion of TODO above
|
||||
count = ((sourceDuration * timescale) - time) / duration;
|
||||
}
|
||||
} else {
|
||||
count = (segmentTimeline[nextS].t - time) / duration;
|
||||
}
|
||||
} else {
|
||||
count = repeat + 1;
|
||||
}
|
||||
|
||||
const end = startNumber + segments.length + count;
|
||||
let number = startNumber + segments.length;
|
||||
|
||||
while (number < end) {
|
||||
segments.push({ number, duration: duration / timescale, time, timeline });
|
||||
time += duration;
|
||||
number++;
|
||||
}
|
||||
}
|
||||
|
||||
return segments;
|
||||
};
|
||||
89
VApp/node_modules/mpd-parser/src/segment/urlType.js
generated
vendored
Normal file
89
VApp/node_modules/mpd-parser/src/segment/urlType.js
generated
vendored
Normal file
@@ -0,0 +1,89 @@
|
||||
import resolveUrl from '@videojs/vhs-utils/es/resolve-url';
|
||||
import window from 'global/window';
|
||||
|
||||
/**
|
||||
* @typedef {Object} SingleUri
|
||||
* @property {string} uri - relative location of segment
|
||||
* @property {string} resolvedUri - resolved location of segment
|
||||
* @property {Object} byterange - Object containing information on how to make byte range
|
||||
* requests following byte-range-spec per RFC2616.
|
||||
* @property {String} byterange.length - length of range request
|
||||
* @property {String} byterange.offset - byte offset of range request
|
||||
*
|
||||
* @see https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35.1
|
||||
*/
|
||||
|
||||
/**
|
||||
* Converts a URLType node (5.3.9.2.3 Table 13) to a segment object
|
||||
* that conforms to how m3u8-parser is structured
|
||||
*
|
||||
* @see https://github.com/videojs/m3u8-parser
|
||||
*
|
||||
* @param {string} baseUrl - baseUrl provided by <BaseUrl> nodes
|
||||
* @param {string} source - source url for segment
|
||||
* @param {string} range - optional range used for range calls,
|
||||
* follows RFC 2616, Clause 14.35.1
|
||||
* @return {SingleUri} full segment information transformed into a format similar
|
||||
* to m3u8-parser
|
||||
*/
|
||||
export const urlTypeToSegment = ({ baseUrl = '', source = '', range = '', indexRange = '' }) => {
|
||||
const segment = {
|
||||
uri: source,
|
||||
resolvedUri: resolveUrl(baseUrl || '', source)
|
||||
};
|
||||
|
||||
if (range || indexRange) {
|
||||
const rangeStr = range ? range : indexRange;
|
||||
const ranges = rangeStr.split('-');
|
||||
|
||||
// default to parsing this as a BigInt if possible
|
||||
let startRange = window.BigInt ? window.BigInt(ranges[0]) : parseInt(ranges[0], 10);
|
||||
let endRange = window.BigInt ? window.BigInt(ranges[1]) : parseInt(ranges[1], 10);
|
||||
|
||||
// convert back to a number if less than MAX_SAFE_INTEGER
|
||||
if (startRange < Number.MAX_SAFE_INTEGER && typeof startRange === 'bigint') {
|
||||
startRange = Number(startRange);
|
||||
}
|
||||
|
||||
if (endRange < Number.MAX_SAFE_INTEGER && typeof endRange === 'bigint') {
|
||||
endRange = Number(endRange);
|
||||
}
|
||||
|
||||
let length;
|
||||
|
||||
if (typeof endRange === 'bigint' || typeof startRange === 'bigint') {
|
||||
length = window.BigInt(endRange) - window.BigInt(startRange) + window.BigInt(1);
|
||||
} else {
|
||||
length = endRange - startRange + 1;
|
||||
}
|
||||
|
||||
if (typeof length === 'bigint' && length < Number.MAX_SAFE_INTEGER) {
|
||||
length = Number(length);
|
||||
}
|
||||
|
||||
// byterange should be inclusive according to
|
||||
// RFC 2616, Clause 14.35.1
|
||||
segment.byterange = {
|
||||
length,
|
||||
offset: startRange
|
||||
};
|
||||
}
|
||||
|
||||
return segment;
|
||||
};
|
||||
|
||||
export const byteRangeToString = (byterange) => {
|
||||
// `endRange` is one less than `offset + length` because the HTTP range
|
||||
// header uses inclusive ranges
|
||||
let endRange;
|
||||
|
||||
if (typeof byterange.offset === 'bigint' || typeof byterange.length === 'bigint') {
|
||||
endRange = window.BigInt(byterange.offset) + window.BigInt(byterange.length) - window.BigInt(1);
|
||||
} else {
|
||||
endRange = byterange.offset + byterange.length - 1;
|
||||
}
|
||||
|
||||
return `${byterange.offset}-${endRange}`;
|
||||
};
|
||||
|
||||
export default urlTypeToSegment;
|
||||
27
VApp/node_modules/mpd-parser/src/stringToMpdXml.js
generated
vendored
Normal file
27
VApp/node_modules/mpd-parser/src/stringToMpdXml.js
generated
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
import {DOMParser} from '@xmldom/xmldom';
|
||||
import errors from './errors';
|
||||
|
||||
export const stringToMpdXml = (manifestString) => {
|
||||
if (manifestString === '') {
|
||||
throw new Error(errors.DASH_EMPTY_MANIFEST);
|
||||
}
|
||||
|
||||
const parser = new DOMParser();
|
||||
let xml;
|
||||
let mpd;
|
||||
|
||||
try {
|
||||
xml = parser.parseFromString(manifestString, 'application/xml');
|
||||
mpd = xml && xml.documentElement.tagName === 'MPD' ?
|
||||
xml.documentElement : null;
|
||||
} catch (e) {
|
||||
// ie 11 throws on invalid xml
|
||||
}
|
||||
|
||||
if (!mpd || mpd &&
|
||||
mpd.getElementsByTagName('parsererror').length > 0) {
|
||||
throw new Error(errors.DASH_INVALID_XML);
|
||||
}
|
||||
|
||||
return mpd;
|
||||
};
|
||||
522
VApp/node_modules/mpd-parser/src/toM3u8.js
generated
vendored
Normal file
522
VApp/node_modules/mpd-parser/src/toM3u8.js
generated
vendored
Normal file
@@ -0,0 +1,522 @@
|
||||
import { values } from './utils/object';
|
||||
import { findIndexes } from './utils/list';
|
||||
import { addSidxSegmentsToPlaylist as addSidxSegmentsToPlaylist_ } from './segment/segmentBase';
|
||||
import { byteRangeToString } from './segment/urlType';
|
||||
import {
|
||||
getUniqueTimelineStarts,
|
||||
positionManifestOnTimeline
|
||||
} from './playlist-merge';
|
||||
|
||||
export const generateSidxKey = (sidx) => sidx &&
|
||||
sidx.uri + '-' + byteRangeToString(sidx.byterange);
|
||||
|
||||
const mergeDiscontiguousPlaylists = playlists => {
|
||||
// Break out playlists into groups based on their baseUrl
|
||||
const playlistsByBaseUrl = playlists.reduce(function(acc, cur) {
|
||||
if (!acc[cur.attributes.baseUrl]) {
|
||||
acc[cur.attributes.baseUrl] = [];
|
||||
}
|
||||
|
||||
acc[cur.attributes.baseUrl].push(cur);
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
let allPlaylists = [];
|
||||
|
||||
Object.values(playlistsByBaseUrl).forEach((playlistGroup) => {
|
||||
const mergedPlaylists = values(playlistGroup.reduce((acc, playlist) => {
|
||||
// assuming playlist IDs are the same across periods
|
||||
// TODO: handle multiperiod where representation sets are not the same
|
||||
// across periods
|
||||
const name = playlist.attributes.id + (playlist.attributes.lang || '');
|
||||
|
||||
if (!acc[name]) {
|
||||
// First Period
|
||||
acc[name] = playlist;
|
||||
acc[name].attributes.timelineStarts = [];
|
||||
} else {
|
||||
// Subsequent Periods
|
||||
if (playlist.segments) {
|
||||
// first segment of subsequent periods signal a discontinuity
|
||||
if (playlist.segments[0]) {
|
||||
playlist.segments[0].discontinuity = true;
|
||||
}
|
||||
acc[name].segments.push(...playlist.segments);
|
||||
}
|
||||
|
||||
// bubble up contentProtection, this assumes all DRM content
|
||||
// has the same contentProtection
|
||||
if (playlist.attributes.contentProtection) {
|
||||
acc[name].attributes.contentProtection =
|
||||
playlist.attributes.contentProtection;
|
||||
}
|
||||
}
|
||||
|
||||
acc[name].attributes.timelineStarts.push({
|
||||
// Although they represent the same number, it's important to have both to make it
|
||||
// compatible with HLS potentially having a similar attribute.
|
||||
start: playlist.attributes.periodStart,
|
||||
timeline: playlist.attributes.periodStart
|
||||
});
|
||||
|
||||
return acc;
|
||||
}, {}));
|
||||
|
||||
allPlaylists = allPlaylists.concat(mergedPlaylists);
|
||||
});
|
||||
|
||||
return allPlaylists.map(playlist => {
|
||||
playlist.discontinuityStarts =
|
||||
findIndexes(playlist.segments || [], 'discontinuity');
|
||||
|
||||
return playlist;
|
||||
});
|
||||
};
|
||||
|
||||
export const addSidxSegmentsToPlaylist = (playlist, sidxMapping) => {
|
||||
const sidxKey = generateSidxKey(playlist.sidx);
|
||||
const sidxMatch = sidxKey && sidxMapping[sidxKey] && sidxMapping[sidxKey].sidx;
|
||||
|
||||
if (sidxMatch) {
|
||||
addSidxSegmentsToPlaylist_(playlist, sidxMatch, playlist.sidx.resolvedUri);
|
||||
}
|
||||
|
||||
return playlist;
|
||||
};
|
||||
|
||||
export const addSidxSegmentsToPlaylists = (playlists, sidxMapping = {}) => {
|
||||
if (!Object.keys(sidxMapping).length) {
|
||||
return playlists;
|
||||
}
|
||||
|
||||
for (const i in playlists) {
|
||||
playlists[i] = addSidxSegmentsToPlaylist(playlists[i], sidxMapping);
|
||||
}
|
||||
|
||||
return playlists;
|
||||
};
|
||||
|
||||
export const formatAudioPlaylist = ({
|
||||
attributes,
|
||||
segments,
|
||||
sidx,
|
||||
mediaSequence,
|
||||
discontinuitySequence,
|
||||
discontinuityStarts
|
||||
}, isAudioOnly) => {
|
||||
const playlist = {
|
||||
attributes: {
|
||||
NAME: attributes.id,
|
||||
BANDWIDTH: attributes.bandwidth,
|
||||
CODECS: attributes.codecs,
|
||||
['PROGRAM-ID']: 1
|
||||
},
|
||||
uri: '',
|
||||
endList: attributes.type === 'static',
|
||||
timeline: attributes.periodStart,
|
||||
resolvedUri: attributes.baseUrl || '',
|
||||
targetDuration: attributes.duration,
|
||||
discontinuitySequence,
|
||||
discontinuityStarts,
|
||||
timelineStarts: attributes.timelineStarts,
|
||||
mediaSequence,
|
||||
segments
|
||||
};
|
||||
|
||||
if (attributes.contentProtection) {
|
||||
playlist.contentProtection = attributes.contentProtection;
|
||||
}
|
||||
|
||||
if (attributes.serviceLocation) {
|
||||
playlist.attributes.serviceLocation = attributes.serviceLocation;
|
||||
}
|
||||
|
||||
if (sidx) {
|
||||
playlist.sidx = sidx;
|
||||
}
|
||||
|
||||
if (isAudioOnly) {
|
||||
playlist.attributes.AUDIO = 'audio';
|
||||
playlist.attributes.SUBTITLES = 'subs';
|
||||
}
|
||||
|
||||
return playlist;
|
||||
};
|
||||
|
||||
export const formatVttPlaylist = ({
|
||||
attributes,
|
||||
segments,
|
||||
mediaSequence,
|
||||
discontinuityStarts,
|
||||
discontinuitySequence
|
||||
}) => {
|
||||
if (typeof segments === 'undefined') {
|
||||
// vtt tracks may use single file in BaseURL
|
||||
segments = [{
|
||||
uri: attributes.baseUrl,
|
||||
timeline: attributes.periodStart,
|
||||
resolvedUri: attributes.baseUrl || '',
|
||||
duration: attributes.sourceDuration,
|
||||
number: 0
|
||||
}];
|
||||
|
||||
// targetDuration should be the same duration as the only segment
|
||||
attributes.duration = attributes.sourceDuration;
|
||||
}
|
||||
|
||||
const m3u8Attributes = {
|
||||
NAME: attributes.id,
|
||||
BANDWIDTH: attributes.bandwidth,
|
||||
['PROGRAM-ID']: 1
|
||||
};
|
||||
|
||||
if (attributes.codecs) {
|
||||
m3u8Attributes.CODECS = attributes.codecs;
|
||||
}
|
||||
const vttPlaylist = {
|
||||
attributes: m3u8Attributes,
|
||||
uri: '',
|
||||
endList: attributes.type === 'static',
|
||||
timeline: attributes.periodStart,
|
||||
resolvedUri: attributes.baseUrl || '',
|
||||
targetDuration: attributes.duration,
|
||||
timelineStarts: attributes.timelineStarts,
|
||||
discontinuityStarts,
|
||||
discontinuitySequence,
|
||||
mediaSequence,
|
||||
segments
|
||||
};
|
||||
|
||||
if (attributes.serviceLocation) {
|
||||
vttPlaylist.attributes.serviceLocation = attributes.serviceLocation;
|
||||
}
|
||||
|
||||
return vttPlaylist;
|
||||
};
|
||||
|
||||
export const organizeAudioPlaylists = (playlists, sidxMapping = {}, isAudioOnly = false) => {
|
||||
let mainPlaylist;
|
||||
|
||||
const formattedPlaylists = playlists.reduce((a, playlist) => {
|
||||
const role = playlist.attributes.role &&
|
||||
playlist.attributes.role.value || '';
|
||||
const language = playlist.attributes.lang || '';
|
||||
|
||||
let label = playlist.attributes.label || 'main';
|
||||
|
||||
if (language && !playlist.attributes.label) {
|
||||
const roleLabel = role ? ` (${role})` : '';
|
||||
|
||||
label = `${playlist.attributes.lang}${roleLabel}`;
|
||||
}
|
||||
|
||||
if (!a[label]) {
|
||||
a[label] = {
|
||||
language,
|
||||
autoselect: true,
|
||||
default: role === 'main',
|
||||
playlists: [],
|
||||
uri: ''
|
||||
};
|
||||
}
|
||||
|
||||
const formatted = addSidxSegmentsToPlaylist(formatAudioPlaylist(playlist, isAudioOnly), sidxMapping);
|
||||
|
||||
a[label].playlists.push(formatted);
|
||||
|
||||
if (typeof mainPlaylist === 'undefined' && role === 'main') {
|
||||
mainPlaylist = playlist;
|
||||
mainPlaylist.default = true;
|
||||
}
|
||||
|
||||
return a;
|
||||
}, {});
|
||||
|
||||
// if no playlists have role "main", mark the first as main
|
||||
if (!mainPlaylist) {
|
||||
const firstLabel = Object.keys(formattedPlaylists)[0];
|
||||
|
||||
formattedPlaylists[firstLabel].default = true;
|
||||
}
|
||||
|
||||
return formattedPlaylists;
|
||||
};
|
||||
|
||||
export const organizeVttPlaylists = (playlists, sidxMapping = {}) => {
|
||||
return playlists.reduce((a, playlist) => {
|
||||
const label = playlist.attributes.label || playlist.attributes.lang || 'text';
|
||||
const language = playlist.attributes.lang || 'und';
|
||||
|
||||
if (!a[label]) {
|
||||
a[label] = {
|
||||
language,
|
||||
default: false,
|
||||
autoselect: false,
|
||||
playlists: [],
|
||||
uri: ''
|
||||
};
|
||||
}
|
||||
a[label].playlists.push(addSidxSegmentsToPlaylist(formatVttPlaylist(playlist), sidxMapping));
|
||||
|
||||
return a;
|
||||
}, {});
|
||||
};
|
||||
|
||||
const organizeCaptionServices = (captionServices) => captionServices.reduce((svcObj, svc) => {
|
||||
if (!svc) {
|
||||
return svcObj;
|
||||
}
|
||||
|
||||
svc.forEach((service) => {
|
||||
const {
|
||||
channel,
|
||||
language
|
||||
} = service;
|
||||
|
||||
svcObj[language] = {
|
||||
autoselect: false,
|
||||
default: false,
|
||||
instreamId: channel,
|
||||
language
|
||||
};
|
||||
|
||||
if (service.hasOwnProperty('aspectRatio')) {
|
||||
svcObj[language].aspectRatio = service.aspectRatio;
|
||||
}
|
||||
if (service.hasOwnProperty('easyReader')) {
|
||||
svcObj[language].easyReader = service.easyReader;
|
||||
}
|
||||
if (service.hasOwnProperty('3D')) {
|
||||
svcObj[language]['3D'] = service['3D'];
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
return svcObj;
|
||||
}, {});
|
||||
|
||||
export const formatVideoPlaylist = ({
|
||||
attributes,
|
||||
segments,
|
||||
sidx,
|
||||
discontinuityStarts
|
||||
}) => {
|
||||
const playlist = {
|
||||
attributes: {
|
||||
NAME: attributes.id,
|
||||
AUDIO: 'audio',
|
||||
SUBTITLES: 'subs',
|
||||
RESOLUTION: {
|
||||
width: attributes.width,
|
||||
height: attributes.height
|
||||
},
|
||||
CODECS: attributes.codecs,
|
||||
BANDWIDTH: attributes.bandwidth,
|
||||
['PROGRAM-ID']: 1
|
||||
},
|
||||
uri: '',
|
||||
endList: attributes.type === 'static',
|
||||
timeline: attributes.periodStart,
|
||||
resolvedUri: attributes.baseUrl || '',
|
||||
targetDuration: attributes.duration,
|
||||
discontinuityStarts,
|
||||
timelineStarts: attributes.timelineStarts,
|
||||
segments
|
||||
};
|
||||
|
||||
if (attributes.frameRate) {
|
||||
playlist.attributes['FRAME-RATE'] = attributes.frameRate;
|
||||
}
|
||||
|
||||
if (attributes.contentProtection) {
|
||||
playlist.contentProtection = attributes.contentProtection;
|
||||
}
|
||||
|
||||
if (attributes.serviceLocation) {
|
||||
playlist.attributes.serviceLocation = attributes.serviceLocation;
|
||||
}
|
||||
|
||||
if (sidx) {
|
||||
playlist.sidx = sidx;
|
||||
}
|
||||
|
||||
return playlist;
|
||||
};
|
||||
|
||||
const videoOnly = ({ attributes }) =>
|
||||
attributes.mimeType === 'video/mp4' || attributes.mimeType === 'video/webm' || attributes.contentType === 'video';
|
||||
const audioOnly = ({ attributes }) =>
|
||||
attributes.mimeType === 'audio/mp4' || attributes.mimeType === 'audio/webm' || attributes.contentType === 'audio';
|
||||
const vttOnly = ({ attributes }) =>
|
||||
attributes.mimeType === 'text/vtt' || attributes.contentType === 'text';
|
||||
|
||||
/**
|
||||
* Contains start and timeline properties denoting a timeline start. For DASH, these will
|
||||
* be the same number.
|
||||
*
|
||||
* @typedef {Object} TimelineStart
|
||||
* @property {number} start - the start time of the timeline
|
||||
* @property {number} timeline - the timeline number
|
||||
*/
|
||||
|
||||
/**
|
||||
* Adds appropriate media and discontinuity sequence values to the segments and playlists.
|
||||
*
|
||||
* Throughout mpd-parser, the `number` attribute is used in relation to `startNumber`, a
|
||||
* DASH specific attribute used in constructing segment URI's from templates. However, from
|
||||
* an HLS perspective, the `number` attribute on a segment would be its `mediaSequence`
|
||||
* value, which should start at the original media sequence value (or 0) and increment by 1
|
||||
* for each segment thereafter. Since DASH's `startNumber` values are independent per
|
||||
* period, it doesn't make sense to use it for `number`. Instead, assume everything starts
|
||||
* from a 0 mediaSequence value and increment from there.
|
||||
*
|
||||
* Note that VHS currently doesn't use the `number` property, but it can be helpful for
|
||||
* debugging and making sense of the manifest.
|
||||
*
|
||||
* For live playlists, to account for values increasing in manifests when periods are
|
||||
* removed on refreshes, merging logic should be used to update the numbers to their
|
||||
* appropriate values (to ensure they're sequential and increasing).
|
||||
*
|
||||
* @param {Object[]} playlists - the playlists to update
|
||||
* @param {TimelineStart[]} timelineStarts - the timeline starts for the manifest
|
||||
*/
|
||||
export const addMediaSequenceValues = (playlists, timelineStarts) => {
|
||||
// increment all segments sequentially
|
||||
playlists.forEach((playlist) => {
|
||||
playlist.mediaSequence = 0;
|
||||
playlist.discontinuitySequence = timelineStarts.findIndex(function({
|
||||
timeline
|
||||
}) {
|
||||
return timeline === playlist.timeline;
|
||||
});
|
||||
|
||||
if (!playlist.segments) {
|
||||
return;
|
||||
}
|
||||
|
||||
playlist.segments.forEach((segment, index) => {
|
||||
segment.number = index;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Given a media group object, flattens all playlists within the media group into a single
|
||||
* array.
|
||||
*
|
||||
* @param {Object} mediaGroupObject - the media group object
|
||||
*
|
||||
* @return {Object[]}
|
||||
* The media group playlists
|
||||
*/
|
||||
export const flattenMediaGroupPlaylists = (mediaGroupObject) => {
|
||||
if (!mediaGroupObject) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return Object.keys(mediaGroupObject).reduce((acc, label) => {
|
||||
const labelContents = mediaGroupObject[label];
|
||||
|
||||
return acc.concat(labelContents.playlists);
|
||||
}, []);
|
||||
};
|
||||
|
||||
export const toM3u8 = ({
|
||||
dashPlaylists,
|
||||
locations,
|
||||
contentSteering,
|
||||
sidxMapping = {},
|
||||
previousManifest,
|
||||
eventStream
|
||||
}) => {
|
||||
if (!dashPlaylists.length) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// grab all main manifest attributes
|
||||
const {
|
||||
sourceDuration: duration,
|
||||
type,
|
||||
suggestedPresentationDelay,
|
||||
minimumUpdatePeriod
|
||||
} = dashPlaylists[0].attributes;
|
||||
|
||||
const videoPlaylists = mergeDiscontiguousPlaylists(dashPlaylists.filter(videoOnly)).map(formatVideoPlaylist);
|
||||
const audioPlaylists = mergeDiscontiguousPlaylists(dashPlaylists.filter(audioOnly));
|
||||
const vttPlaylists = mergeDiscontiguousPlaylists(dashPlaylists.filter(vttOnly));
|
||||
const captions = dashPlaylists.map((playlist) => playlist.attributes.captionServices).filter(Boolean);
|
||||
|
||||
const manifest = {
|
||||
allowCache: true,
|
||||
discontinuityStarts: [],
|
||||
segments: [],
|
||||
endList: true,
|
||||
mediaGroups: {
|
||||
AUDIO: {},
|
||||
VIDEO: {},
|
||||
['CLOSED-CAPTIONS']: {},
|
||||
SUBTITLES: {}
|
||||
},
|
||||
uri: '',
|
||||
duration,
|
||||
playlists: addSidxSegmentsToPlaylists(videoPlaylists, sidxMapping)
|
||||
};
|
||||
|
||||
if (minimumUpdatePeriod >= 0) {
|
||||
manifest.minimumUpdatePeriod = minimumUpdatePeriod * 1000;
|
||||
}
|
||||
|
||||
if (locations) {
|
||||
manifest.locations = locations;
|
||||
}
|
||||
|
||||
if (contentSteering) {
|
||||
manifest.contentSteering = contentSteering;
|
||||
}
|
||||
|
||||
if (type === 'dynamic') {
|
||||
manifest.suggestedPresentationDelay = suggestedPresentationDelay;
|
||||
}
|
||||
|
||||
if (eventStream && eventStream.length > 0) {
|
||||
manifest.eventStream = eventStream;
|
||||
}
|
||||
|
||||
const isAudioOnly = manifest.playlists.length === 0;
|
||||
const organizedAudioGroup = audioPlaylists.length ?
|
||||
organizeAudioPlaylists(audioPlaylists, sidxMapping, isAudioOnly) : null;
|
||||
const organizedVttGroup = vttPlaylists.length ?
|
||||
organizeVttPlaylists(vttPlaylists, sidxMapping) : null;
|
||||
const formattedPlaylists = videoPlaylists.concat(
|
||||
flattenMediaGroupPlaylists(organizedAudioGroup),
|
||||
flattenMediaGroupPlaylists(organizedVttGroup)
|
||||
);
|
||||
const playlistTimelineStarts =
|
||||
formattedPlaylists.map(({ timelineStarts }) => timelineStarts);
|
||||
|
||||
manifest.timelineStarts = getUniqueTimelineStarts(playlistTimelineStarts);
|
||||
|
||||
addMediaSequenceValues(formattedPlaylists, manifest.timelineStarts);
|
||||
|
||||
if (organizedAudioGroup) {
|
||||
manifest.mediaGroups.AUDIO.audio = organizedAudioGroup;
|
||||
}
|
||||
|
||||
if (organizedVttGroup) {
|
||||
manifest.mediaGroups.SUBTITLES.subs = organizedVttGroup;
|
||||
}
|
||||
|
||||
if (captions.length) {
|
||||
manifest.mediaGroups['CLOSED-CAPTIONS'].cc = organizeCaptionServices(captions);
|
||||
}
|
||||
|
||||
if (previousManifest) {
|
||||
return positionManifestOnTimeline({
|
||||
oldManifest: previousManifest,
|
||||
newManifest: manifest
|
||||
});
|
||||
}
|
||||
|
||||
return manifest;
|
||||
};
|
||||
60
VApp/node_modules/mpd-parser/src/toPlaylists.js
generated
vendored
Normal file
60
VApp/node_modules/mpd-parser/src/toPlaylists.js
generated
vendored
Normal file
@@ -0,0 +1,60 @@
|
||||
import { merge } from './utils/object';
|
||||
import { segmentsFromTemplate } from './segment/segmentTemplate';
|
||||
import { segmentsFromList } from './segment/segmentList';
|
||||
import { segmentsFromBase } from './segment/segmentBase';
|
||||
|
||||
export const generateSegments = ({ attributes, segmentInfo }) => {
|
||||
let segmentAttributes;
|
||||
let segmentsFn;
|
||||
|
||||
if (segmentInfo.template) {
|
||||
segmentsFn = segmentsFromTemplate;
|
||||
segmentAttributes = merge(attributes, segmentInfo.template);
|
||||
} else if (segmentInfo.base) {
|
||||
segmentsFn = segmentsFromBase;
|
||||
segmentAttributes = merge(attributes, segmentInfo.base);
|
||||
} else if (segmentInfo.list) {
|
||||
segmentsFn = segmentsFromList;
|
||||
segmentAttributes = merge(attributes, segmentInfo.list);
|
||||
}
|
||||
|
||||
const segmentsInfo = {
|
||||
attributes
|
||||
};
|
||||
|
||||
if (!segmentsFn) {
|
||||
return segmentsInfo;
|
||||
}
|
||||
|
||||
const segments = segmentsFn(segmentAttributes, segmentInfo.segmentTimeline);
|
||||
|
||||
// The @duration attribute will be used to determin the playlist's targetDuration which
|
||||
// must be in seconds. Since we've generated the segment list, we no longer need
|
||||
// @duration to be in @timescale units, so we can convert it here.
|
||||
if (segmentAttributes.duration) {
|
||||
const { duration, timescale = 1 } = segmentAttributes;
|
||||
|
||||
segmentAttributes.duration = duration / timescale;
|
||||
} else if (segments.length) {
|
||||
// if there is no @duration attribute, use the largest segment duration as
|
||||
// as target duration
|
||||
segmentAttributes.duration = segments.reduce((max, segment) => {
|
||||
return Math.max(max, Math.ceil(segment.duration));
|
||||
}, 0);
|
||||
} else {
|
||||
segmentAttributes.duration = 0;
|
||||
}
|
||||
|
||||
segmentsInfo.attributes = segmentAttributes;
|
||||
segmentsInfo.segments = segments;
|
||||
|
||||
// This is a sidx box without actual segment information
|
||||
if (segmentInfo.base && segmentAttributes.indexRange) {
|
||||
segmentsInfo.sidx = segments[0];
|
||||
segmentsInfo.segments = [];
|
||||
}
|
||||
|
||||
return segmentsInfo;
|
||||
};
|
||||
|
||||
export const toPlaylists = (representations) => representations.map(generateSegments);
|
||||
53
VApp/node_modules/mpd-parser/src/utils/list.js
generated
vendored
Normal file
53
VApp/node_modules/mpd-parser/src/utils/list.js
generated
vendored
Normal file
@@ -0,0 +1,53 @@
|
||||
import { values } from './object';
|
||||
|
||||
export const range = (start, end) => {
|
||||
const result = [];
|
||||
|
||||
for (let i = start; i < end; i++) {
|
||||
result.push(i);
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export const flatten = lists => lists.reduce((x, y) => x.concat(y), []);
|
||||
|
||||
export const from = list => {
|
||||
if (!list.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const result = [];
|
||||
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
result.push(list[i]);
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export const findIndexes = (l, key) => l.reduce((a, e, i) => {
|
||||
if (e[key]) {
|
||||
a.push(i);
|
||||
}
|
||||
|
||||
return a;
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Returns a union of the included lists provided each element can be identified by a key.
|
||||
*
|
||||
* @param {Array} list - list of lists to get the union of
|
||||
* @param {Function} keyFunction - the function to use as a key for each element
|
||||
*
|
||||
* @return {Array} the union of the arrays
|
||||
*/
|
||||
export const union = (lists, keyFunction) => {
|
||||
return values(lists.reduce((acc, list) => {
|
||||
list.forEach((el) => {
|
||||
acc[keyFunction(el)] = el;
|
||||
});
|
||||
|
||||
return acc;
|
||||
}, {}));
|
||||
};
|
||||
27
VApp/node_modules/mpd-parser/src/utils/object.js
generated
vendored
Normal file
27
VApp/node_modules/mpd-parser/src/utils/object.js
generated
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
const isObject = (obj) => {
|
||||
return !!obj && typeof obj === 'object';
|
||||
};
|
||||
|
||||
export const merge = (...objects) => {
|
||||
|
||||
return objects.reduce((result, source) => {
|
||||
|
||||
if (typeof source !== 'object') {
|
||||
return result;
|
||||
}
|
||||
|
||||
Object.keys(source).forEach(key => {
|
||||
|
||||
if (Array.isArray(result[key]) && Array.isArray(source[key])) {
|
||||
result[key] = result[key].concat(source[key]);
|
||||
} else if (isObject(result[key]) && isObject(source[key])) {
|
||||
result[key] = merge(result[key], source[key]);
|
||||
} else {
|
||||
result[key] = source[key];
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}, {});
|
||||
};
|
||||
|
||||
export const values = o => Object.keys(o).map(k => o[k]);
|
||||
10
VApp/node_modules/mpd-parser/src/utils/string.js
generated
vendored
Normal file
10
VApp/node_modules/mpd-parser/src/utils/string.js
generated
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Converts the provided string that may contain a division operation to a number.
|
||||
*
|
||||
* @param {string} value - the provided string value
|
||||
*
|
||||
* @return {number} the parsed string value
|
||||
*/
|
||||
export const parseDivisionValue = (value) => {
|
||||
return parseFloat(value.split('/').reduce((prev, current) => prev / current));
|
||||
};
|
||||
39
VApp/node_modules/mpd-parser/src/utils/time.js
generated
vendored
Normal file
39
VApp/node_modules/mpd-parser/src/utils/time.js
generated
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
export const parseDuration = (str) => {
|
||||
const SECONDS_IN_YEAR = 365 * 24 * 60 * 60;
|
||||
const SECONDS_IN_MONTH = 30 * 24 * 60 * 60;
|
||||
const SECONDS_IN_DAY = 24 * 60 * 60;
|
||||
const SECONDS_IN_HOUR = 60 * 60;
|
||||
const SECONDS_IN_MIN = 60;
|
||||
|
||||
// P10Y10M10DT10H10M10.1S
|
||||
const durationRegex =
|
||||
/P(?:(\d*)Y)?(?:(\d*)M)?(?:(\d*)D)?(?:T(?:(\d*)H)?(?:(\d*)M)?(?:([\d.]*)S)?)?/;
|
||||
const match = durationRegex.exec(str);
|
||||
|
||||
if (!match) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const [year, month, day, hour, minute, second] = match.slice(1);
|
||||
|
||||
return (parseFloat(year || 0) * SECONDS_IN_YEAR +
|
||||
parseFloat(month || 0) * SECONDS_IN_MONTH +
|
||||
parseFloat(day || 0) * SECONDS_IN_DAY +
|
||||
parseFloat(hour || 0) * SECONDS_IN_HOUR +
|
||||
parseFloat(minute || 0) * SECONDS_IN_MIN +
|
||||
parseFloat(second || 0));
|
||||
};
|
||||
|
||||
export const parseDate = (str) => {
|
||||
// Date format without timezone according to ISO 8601
|
||||
// YYY-MM-DDThh:mm:ss.ssssss
|
||||
const dateRegex = /^\d+-\d+-\d+T\d+:\d+:\d+(\.\d+)?$/;
|
||||
|
||||
// If the date string does not specifiy a timezone, we must specifiy UTC. This is
|
||||
// expressed by ending with 'Z'
|
||||
if (dateRegex.test(str)) {
|
||||
str += 'Z';
|
||||
}
|
||||
|
||||
return Date.parse(str);
|
||||
};
|
||||
6
VApp/node_modules/mpd-parser/src/utils/xml.js
generated
vendored
Normal file
6
VApp/node_modules/mpd-parser/src/utils/xml.js
generated
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
import { from } from './list';
|
||||
|
||||
export const findChildren = (element, name) =>
|
||||
from(element.childNodes).filter(({tagName}) => tagName === name);
|
||||
|
||||
export const getContent = element => element.textContent.trim();
|
||||
Reference in New Issue
Block a user