1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896 |
- /*! @name mpd-parser @version 1.1.1 @license Apache-2.0 */
- (function (global, factory) {
- typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('@xmldom/xmldom')) :
- typeof define === 'function' && define.amd ? define(['exports', '@xmldom/xmldom'], factory) :
- (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.mpdParser = {}, global.window));
- }(this, (function (exports, xmldom) { 'use strict';
- var version = "1.1.1";
- const isObject = obj => {
- return !!obj && typeof obj === 'object';
- };
- 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;
- }, {});
- };
- const values = o => Object.keys(o).map(k => o[k]);
- const range = (start, end) => {
- const result = [];
- for (let i = start; i < end; i++) {
- result.push(i);
- }
- return result;
- };
- const flatten = lists => lists.reduce((x, y) => x.concat(y), []);
- const from = list => {
- if (!list.length) {
- return [];
- }
- const result = [];
- for (let i = 0; i < list.length; i++) {
- result.push(list[i]);
- }
- return result;
- };
- 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
- */
- const union = (lists, keyFunction) => {
- return values(lists.reduce((acc, list) => {
- list.forEach(el => {
- acc[keyFunction(el)] = el;
- });
- return acc;
- }, {}));
- };
- var errors = {
- INVALID_NUMBER_OF_PERIOD: 'INVALID_NUMBER_OF_PERIOD',
- 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'
- };
- var urlToolkit = {exports: {}};
- (function (module, exports) {
- // see https://tools.ietf.org/html/rfc1808
- (function (root) {
- var URL_REGEX = /^(?=((?:[a-zA-Z0-9+\-.]+:)?))\1(?=((?:\/\/[^\/?#]*)?))\2(?=((?:(?:[^?#\/]*\/)*[^;?#\/]*)?))\3((?:;[^?#]*)?)(\?[^#]*)?(#[^]*)?$/;
- var FIRST_SEGMENT_REGEX = /^(?=([^\/?#]*))\1([^]*)$/;
- var SLASH_DOT_REGEX = /(?:\/|^)\.(?=\/)/g;
- var SLASH_DOT_DOT_REGEX = /(?:\/|^)\.\.\/(?!\.\.\/)[^\/]*(?=\/)/g;
- var URLToolkit = {
- // If opts.alwaysNormalize is true then the path will always be normalized even when it starts with / or //
- // E.g
- // With opts.alwaysNormalize = false (default, spec compliant)
- // http://a.com/b/cd + /e/f/../g => http://a.com/e/f/../g
- // With opts.alwaysNormalize = true (not spec compliant)
- // http://a.com/b/cd + /e/f/../g => http://a.com/e/g
- buildAbsoluteURL: function (baseURL, relativeURL, opts) {
- opts = opts || {}; // remove any remaining space and CRLF
- baseURL = baseURL.trim();
- relativeURL = relativeURL.trim();
- if (!relativeURL) {
- // 2a) If the embedded URL is entirely empty, it inherits the
- // entire base URL (i.e., is set equal to the base URL)
- // and we are done.
- if (!opts.alwaysNormalize) {
- return baseURL;
- }
- var basePartsForNormalise = URLToolkit.parseURL(baseURL);
- if (!basePartsForNormalise) {
- throw new Error('Error trying to parse base URL.');
- }
- basePartsForNormalise.path = URLToolkit.normalizePath(basePartsForNormalise.path);
- return URLToolkit.buildURLFromParts(basePartsForNormalise);
- }
- var relativeParts = URLToolkit.parseURL(relativeURL);
- if (!relativeParts) {
- throw new Error('Error trying to parse relative URL.');
- }
- if (relativeParts.scheme) {
- // 2b) If the embedded URL starts with a scheme name, it is
- // interpreted as an absolute URL and we are done.
- if (!opts.alwaysNormalize) {
- return relativeURL;
- }
- relativeParts.path = URLToolkit.normalizePath(relativeParts.path);
- return URLToolkit.buildURLFromParts(relativeParts);
- }
- var baseParts = URLToolkit.parseURL(baseURL);
- if (!baseParts) {
- throw new Error('Error trying to parse base URL.');
- }
- if (!baseParts.netLoc && baseParts.path && baseParts.path[0] !== '/') {
- // If netLoc missing and path doesn't start with '/', assume everthing before the first '/' is the netLoc
- // This causes 'example.com/a' to be handled as '//example.com/a' instead of '/example.com/a'
- var pathParts = FIRST_SEGMENT_REGEX.exec(baseParts.path);
- baseParts.netLoc = pathParts[1];
- baseParts.path = pathParts[2];
- }
- if (baseParts.netLoc && !baseParts.path) {
- baseParts.path = '/';
- }
- var builtParts = {
- // 2c) Otherwise, the embedded URL inherits the scheme of
- // the base URL.
- scheme: baseParts.scheme,
- netLoc: relativeParts.netLoc,
- path: null,
- params: relativeParts.params,
- query: relativeParts.query,
- fragment: relativeParts.fragment
- };
- if (!relativeParts.netLoc) {
- // 3) If the embedded URL's <net_loc> is non-empty, we skip to
- // Step 7. Otherwise, the embedded URL inherits the <net_loc>
- // (if any) of the base URL.
- builtParts.netLoc = baseParts.netLoc; // 4) If the embedded URL path is preceded by a slash "/", the
- // path is not relative and we skip to Step 7.
- if (relativeParts.path[0] !== '/') {
- if (!relativeParts.path) {
- // 5) If the embedded URL path is empty (and not preceded by a
- // slash), then the embedded URL inherits the base URL path
- builtParts.path = baseParts.path; // 5a) if the embedded URL's <params> is non-empty, we skip to
- // step 7; otherwise, it inherits the <params> of the base
- // URL (if any) and
- if (!relativeParts.params) {
- builtParts.params = baseParts.params; // 5b) if the embedded URL's <query> is non-empty, we skip to
- // step 7; otherwise, it inherits the <query> of the base
- // URL (if any) and we skip to step 7.
- if (!relativeParts.query) {
- builtParts.query = baseParts.query;
- }
- }
- } else {
- // 6) The last segment of the base URL's path (anything
- // following the rightmost slash "/", or the entire path if no
- // slash is present) is removed and the embedded URL's path is
- // appended in its place.
- var baseURLPath = baseParts.path;
- var newPath = baseURLPath.substring(0, baseURLPath.lastIndexOf('/') + 1) + relativeParts.path;
- builtParts.path = URLToolkit.normalizePath(newPath);
- }
- }
- }
- if (builtParts.path === null) {
- builtParts.path = opts.alwaysNormalize ? URLToolkit.normalizePath(relativeParts.path) : relativeParts.path;
- }
- return URLToolkit.buildURLFromParts(builtParts);
- },
- parseURL: function (url) {
- var parts = URL_REGEX.exec(url);
- if (!parts) {
- return null;
- }
- return {
- scheme: parts[1] || '',
- netLoc: parts[2] || '',
- path: parts[3] || '',
- params: parts[4] || '',
- query: parts[5] || '',
- fragment: parts[6] || ''
- };
- },
- normalizePath: function (path) {
- // The following operations are
- // then applied, in order, to the new path:
- // 6a) All occurrences of "./", where "." is a complete path
- // segment, are removed.
- // 6b) If the path ends with "." as a complete path segment,
- // that "." is removed.
- path = path.split('').reverse().join('').replace(SLASH_DOT_REGEX, ''); // 6c) All occurrences of "<segment>/../", where <segment> is a
- // complete path segment not equal to "..", are removed.
- // Removal of these path segments is performed iteratively,
- // removing the leftmost matching pattern on each iteration,
- // until no matching pattern remains.
- // 6d) If the path ends with "<segment>/..", where <segment> is a
- // complete path segment not equal to "..", that
- // "<segment>/.." is removed.
- while (path.length !== (path = path.replace(SLASH_DOT_DOT_REGEX, '')).length) {}
- return path.split('').reverse().join('');
- },
- buildURLFromParts: function (parts) {
- return parts.scheme + parts.netLoc + parts.path + parts.params + parts.query + parts.fragment;
- }
- };
- module.exports = URLToolkit;
- })();
- })(urlToolkit);
- var URLToolkit = urlToolkit.exports;
- var DEFAULT_LOCATION = 'http://example.com';
- var resolveUrl = function resolveUrl(baseUrl, relativeUrl) {
- // return early if we don't need to resolve
- if (/^[a-z]+:/i.test(relativeUrl)) {
- return relativeUrl;
- } // if baseUrl is a data URI, ignore it and resolve everything relative to window.location
- if (/^data:/.test(baseUrl)) {
- baseUrl = window.location && window.location.href || '';
- } // IE11 supports URL but not the URL constructor
- // feature detect the behavior we want
- var nativeURL = typeof window.URL === 'function';
- var protocolLess = /^\/\//.test(baseUrl); // remove location if window.location isn't available (i.e. we're in node)
- // and if baseUrl isn't an absolute url
- var removeLocation = !window.location && !/\/\//i.test(baseUrl); // if the base URL is relative then combine with the current location
- if (nativeURL) {
- baseUrl = new window.URL(baseUrl, window.location || DEFAULT_LOCATION);
- } else if (!/\/\//i.test(baseUrl)) {
- baseUrl = URLToolkit.buildAbsoluteURL(window.location && window.location.href || '', baseUrl);
- }
- if (nativeURL) {
- var newUrl = new URL(relativeUrl, baseUrl); // if we're a protocol-less url, remove the protocol
- // and if we're location-less, remove the location
- // otherwise, return the url unmodified
- if (removeLocation) {
- return newUrl.href.slice(DEFAULT_LOCATION.length);
- } else if (protocolLess) {
- return newUrl.href.slice(newUrl.protocol.length);
- }
- return newUrl.href;
- }
- return URLToolkit.buildAbsoluteURL(baseUrl, relativeUrl);
- };
- /**
- * @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
- */
- 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;
- };
- 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}`;
- };
- /**
- * 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.
- */
- 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
- */
- 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
- */
- 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;
- };
- /**
- * 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
- */
- 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 = urlTypeToSegment({
- baseUrl,
- source: initialization.sourceURL,
- range: initialization.range
- });
- const segment = urlTypeToSegment({
- 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
- */
- const addSidxSegmentsToPlaylist$1 = (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;
- };
- /**
- * Loops through all supported media groups in master and calls the provided
- * callback for each group
- *
- * @param {Object} master
- * The parsed master manifest object
- * @param {string[]} groups
- * The media groups to call the callback for
- * @param {Function} callback
- * Callback to call for each media group
- */
- var forEachMediaGroup = function forEachMediaGroup(master, groups, callback) {
- groups.forEach(function (mediaType) {
- for (var groupKey in master.mediaGroups[mediaType]) {
- for (var labelKey in master.mediaGroups[mediaType][groupKey]) {
- var mediaProperties = master.mediaGroups[mediaType][groupKey][labelKey];
- callback(mediaProperties, mediaType, groupKey, labelKey);
- }
- }
- });
- };
- 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
- */
- 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
- */
- 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
- */
- 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
- */
- 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
- */
- 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
- */
- 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;
- };
- const generateSidxKey = sidx => sidx && sidx.uri + '-' + byteRangeToString(sidx.byterange);
- const mergeDiscontiguousPlaylists = playlists => {
- const mergedPlaylists = values(playlists.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;
- }, {}));
- return mergedPlaylists.map(playlist => {
- playlist.discontinuityStarts = findIndexes(playlist.segments || [], 'discontinuity');
- return playlist;
- });
- };
- const addSidxSegmentsToPlaylist = (playlist, sidxMapping) => {
- const sidxKey = generateSidxKey(playlist.sidx);
- const sidxMatch = sidxKey && sidxMapping[sidxKey] && sidxMapping[sidxKey].sidx;
- if (sidxMatch) {
- addSidxSegmentsToPlaylist$1(playlist, sidxMatch, playlist.sidx.resolvedUri);
- }
- return playlist;
- };
- const addSidxSegmentsToPlaylists = (playlists, sidxMapping = {}) => {
- if (!Object.keys(sidxMapping).length) {
- return playlists;
- }
- for (const i in playlists) {
- playlists[i] = addSidxSegmentsToPlaylist(playlists[i], sidxMapping);
- }
- return playlists;
- };
- 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: '',
- targetDuration: attributes.duration,
- discontinuitySequence,
- discontinuityStarts,
- timelineStarts: attributes.timelineStarts,
- mediaSequence,
- segments
- };
- if (attributes.contentProtection) {
- playlist.contentProtection = attributes.contentProtection;
- }
- if (sidx) {
- playlist.sidx = sidx;
- }
- if (isAudioOnly) {
- playlist.attributes.AUDIO = 'audio';
- playlist.attributes.SUBTITLES = 'subs';
- }
- return playlist;
- };
- 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;
- }
- return {
- attributes: m3u8Attributes,
- uri: '',
- endList: attributes.type === 'static',
- timeline: attributes.periodStart,
- resolvedUri: attributes.baseUrl || '',
- targetDuration: attributes.duration,
- timelineStarts: attributes.timelineStarts,
- discontinuityStarts,
- discontinuitySequence,
- mediaSequence,
- segments
- };
- };
- 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;
- };
- const organizeVttPlaylists = (playlists, sidxMapping = {}) => {
- return playlists.reduce((a, playlist) => {
- const label = playlist.attributes.label || playlist.attributes.lang || 'text';
- if (!a[label]) {
- a[label] = {
- language: label,
- 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;
- }, {});
- 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: '',
- 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 (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
- */
- 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
- */
- const flattenMediaGroupPlaylists = mediaGroupObject => {
- if (!mediaGroupObject) {
- return [];
- }
- return Object.keys(mediaGroupObject).reduce((acc, label) => {
- const labelContents = mediaGroupObject[label];
- return acc.concat(labelContents.playlists);
- }, []);
- };
- const toM3u8 = ({
- dashPlaylists,
- locations,
- 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 (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;
- };
- /**
- * 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
- */
- 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;
- };
- 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
- */
- 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
- */
- 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
- */
- 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
- */
- 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;
- });
- };
- /**
- * 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 = urlTypeToSegment({
- baseUrl,
- source: initialization.sourceURL,
- range: initialization.range
- });
- const segment = urlTypeToSegment({
- 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
- */
- 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;
- };
- 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;
- };
- const toPlaylists = representations => representations.map(generateSegments);
- const findChildren = (element, name) => from(element.childNodes).filter(({
- tagName
- }) => tagName === name);
- const getContent = element => element.textContent.trim();
- /**
- * 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
- */
- const parseDivisionValue = value => {
- return parseFloat(value.split('/').reduce((prev, current) => prev / current));
- };
- 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);
- };
- 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);
- };
- 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
- */
- 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;
- }, {});
- };
- var atob = function atob(s) {
- return window.atob ? window.atob(s) : Buffer.from(s, 'base64').toString('binary');
- };
- function decodeB64ToUint8Array(b64Text) {
- var decodedString = atob(b64Text);
- var array = new Uint8Array(decodedString.length);
- for (var i = 0; i < decodedString.length; i++) {
- array[i] = decodedString.charCodeAt(i);
- }
- return 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'
- };
- /**
- * Builds a list of urls that is the product of the reference urls and BaseURL values
- *
- * @param {string[]} referenceUrls
- * List of reference urls to resolve to
- * @param {Node[]} baseUrlElements
- * List of BaseURL nodes from the mpd
- * @return {string[]}
- * List of resolved urls
- */
- const buildBaseUrls = (referenceUrls, baseUrlElements) => {
- if (!baseUrlElements.length) {
- return referenceUrls;
- }
- return flatten(referenceUrls.map(function (reference) {
- return baseUrlElements.map(function (baseUrlElement) {
- return resolveUrl(reference, getContent(baseUrlElement));
- });
- }));
- };
- /**
- * 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
- */
- 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 {string[]} adaptationSetBaseUrls
- * Contains list of resolved base urls inherited by the AdaptationSet
- * @param {SegmentInformation} adaptationSetSegmentInfo
- * Contains Segment information for the AdaptationSet
- * @return {inheritBaseUrlsCallback}
- * Callback map function
- */
- 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
- 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
- */
- 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 {string[]} periodBaseUrls
- * Contains list of resolved base urls inherited by the Period
- * @param {string[]} periodSegmentInfo
- * Contains Segment Information at the period level
- * @return {toRepresentationsCallback}
- * Callback map function
- */
- 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 {string[]} mpdBaseUrls
- * Contains list of resolved base urls inherited by the mpd
- * @return {toAdaptationSetsCallback}
- * Callback map function
- */
- 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)));
- };
- /**
- * 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
- */
- 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
- */
- const inheritAttributes = (mpd, options = {}) => {
- const {
- manifestUri = '',
- NOW = Date.now(),
- clientOffset = 0
- } = 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([manifestUri], findChildren(mpd, 'BaseURL')); // 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,
- representationInfo: flatten(periods.map(toAdaptationSets(mpdAttributes, mpdBaseUrls))),
- eventStream: flatten(periods.map(toEventStream))
- };
- };
- const stringToMpdXml = manifestString => {
- if (manifestString === '') {
- throw new Error(errors.DASH_EMPTY_MANIFEST);
- }
- const parser = new xmldom.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 throwsw on invalid xml
- }
- if (!mpd || mpd && mpd.getElementsByTagName('parsererror').length > 0) {
- throw new Error(errors.DASH_INVALID_XML);
- }
- return mpd;
- };
- /**
- * 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
- */
- 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;
- };
- 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,
- 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));
- exports.VERSION = VERSION;
- exports.addSidxSegmentsToPlaylist = addSidxSegmentsToPlaylist$1;
- exports.generateSidxKey = generateSidxKey;
- exports.inheritAttributes = inheritAttributes;
- exports.parse = parse;
- exports.parseUTCTiming = parseUTCTiming;
- exports.stringToMpdXml = stringToMpdXml;
- exports.toM3u8 = toM3u8;
- exports.toPlaylists = toPlaylists;
- Object.defineProperty(exports, '__esModule', { value: true });
- })));
|