mpd-parser.js 98 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896
  1. /*! @name mpd-parser @version 1.1.1 @license Apache-2.0 */
  2. (function (global, factory) {
  3. typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('@xmldom/xmldom')) :
  4. typeof define === 'function' && define.amd ? define(['exports', '@xmldom/xmldom'], factory) :
  5. (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.mpdParser = {}, global.window));
  6. }(this, (function (exports, xmldom) { 'use strict';
  7. var version = "1.1.1";
  8. const isObject = obj => {
  9. return !!obj && typeof obj === 'object';
  10. };
  11. const merge = (...objects) => {
  12. return objects.reduce((result, source) => {
  13. if (typeof source !== 'object') {
  14. return result;
  15. }
  16. Object.keys(source).forEach(key => {
  17. if (Array.isArray(result[key]) && Array.isArray(source[key])) {
  18. result[key] = result[key].concat(source[key]);
  19. } else if (isObject(result[key]) && isObject(source[key])) {
  20. result[key] = merge(result[key], source[key]);
  21. } else {
  22. result[key] = source[key];
  23. }
  24. });
  25. return result;
  26. }, {});
  27. };
  28. const values = o => Object.keys(o).map(k => o[k]);
  29. const range = (start, end) => {
  30. const result = [];
  31. for (let i = start; i < end; i++) {
  32. result.push(i);
  33. }
  34. return result;
  35. };
  36. const flatten = lists => lists.reduce((x, y) => x.concat(y), []);
  37. const from = list => {
  38. if (!list.length) {
  39. return [];
  40. }
  41. const result = [];
  42. for (let i = 0; i < list.length; i++) {
  43. result.push(list[i]);
  44. }
  45. return result;
  46. };
  47. const findIndexes = (l, key) => l.reduce((a, e, i) => {
  48. if (e[key]) {
  49. a.push(i);
  50. }
  51. return a;
  52. }, []);
  53. /**
  54. * Returns a union of the included lists provided each element can be identified by a key.
  55. *
  56. * @param {Array} list - list of lists to get the union of
  57. * @param {Function} keyFunction - the function to use as a key for each element
  58. *
  59. * @return {Array} the union of the arrays
  60. */
  61. const union = (lists, keyFunction) => {
  62. return values(lists.reduce((acc, list) => {
  63. list.forEach(el => {
  64. acc[keyFunction(el)] = el;
  65. });
  66. return acc;
  67. }, {}));
  68. };
  69. var errors = {
  70. INVALID_NUMBER_OF_PERIOD: 'INVALID_NUMBER_OF_PERIOD',
  71. DASH_EMPTY_MANIFEST: 'DASH_EMPTY_MANIFEST',
  72. DASH_INVALID_XML: 'DASH_INVALID_XML',
  73. NO_BASE_URL: 'NO_BASE_URL',
  74. MISSING_SEGMENT_INFORMATION: 'MISSING_SEGMENT_INFORMATION',
  75. SEGMENT_TIME_UNSPECIFIED: 'SEGMENT_TIME_UNSPECIFIED',
  76. UNSUPPORTED_UTC_TIMING_SCHEME: 'UNSUPPORTED_UTC_TIMING_SCHEME'
  77. };
  78. var urlToolkit = {exports: {}};
  79. (function (module, exports) {
  80. // see https://tools.ietf.org/html/rfc1808
  81. (function (root) {
  82. var URL_REGEX = /^(?=((?:[a-zA-Z0-9+\-.]+:)?))\1(?=((?:\/\/[^\/?#]*)?))\2(?=((?:(?:[^?#\/]*\/)*[^;?#\/]*)?))\3((?:;[^?#]*)?)(\?[^#]*)?(#[^]*)?$/;
  83. var FIRST_SEGMENT_REGEX = /^(?=([^\/?#]*))\1([^]*)$/;
  84. var SLASH_DOT_REGEX = /(?:\/|^)\.(?=\/)/g;
  85. var SLASH_DOT_DOT_REGEX = /(?:\/|^)\.\.\/(?!\.\.\/)[^\/]*(?=\/)/g;
  86. var URLToolkit = {
  87. // If opts.alwaysNormalize is true then the path will always be normalized even when it starts with / or //
  88. // E.g
  89. // With opts.alwaysNormalize = false (default, spec compliant)
  90. // http://a.com/b/cd + /e/f/../g => http://a.com/e/f/../g
  91. // With opts.alwaysNormalize = true (not spec compliant)
  92. // http://a.com/b/cd + /e/f/../g => http://a.com/e/g
  93. buildAbsoluteURL: function (baseURL, relativeURL, opts) {
  94. opts = opts || {}; // remove any remaining space and CRLF
  95. baseURL = baseURL.trim();
  96. relativeURL = relativeURL.trim();
  97. if (!relativeURL) {
  98. // 2a) If the embedded URL is entirely empty, it inherits the
  99. // entire base URL (i.e., is set equal to the base URL)
  100. // and we are done.
  101. if (!opts.alwaysNormalize) {
  102. return baseURL;
  103. }
  104. var basePartsForNormalise = URLToolkit.parseURL(baseURL);
  105. if (!basePartsForNormalise) {
  106. throw new Error('Error trying to parse base URL.');
  107. }
  108. basePartsForNormalise.path = URLToolkit.normalizePath(basePartsForNormalise.path);
  109. return URLToolkit.buildURLFromParts(basePartsForNormalise);
  110. }
  111. var relativeParts = URLToolkit.parseURL(relativeURL);
  112. if (!relativeParts) {
  113. throw new Error('Error trying to parse relative URL.');
  114. }
  115. if (relativeParts.scheme) {
  116. // 2b) If the embedded URL starts with a scheme name, it is
  117. // interpreted as an absolute URL and we are done.
  118. if (!opts.alwaysNormalize) {
  119. return relativeURL;
  120. }
  121. relativeParts.path = URLToolkit.normalizePath(relativeParts.path);
  122. return URLToolkit.buildURLFromParts(relativeParts);
  123. }
  124. var baseParts = URLToolkit.parseURL(baseURL);
  125. if (!baseParts) {
  126. throw new Error('Error trying to parse base URL.');
  127. }
  128. if (!baseParts.netLoc && baseParts.path && baseParts.path[0] !== '/') {
  129. // If netLoc missing and path doesn't start with '/', assume everthing before the first '/' is the netLoc
  130. // This causes 'example.com/a' to be handled as '//example.com/a' instead of '/example.com/a'
  131. var pathParts = FIRST_SEGMENT_REGEX.exec(baseParts.path);
  132. baseParts.netLoc = pathParts[1];
  133. baseParts.path = pathParts[2];
  134. }
  135. if (baseParts.netLoc && !baseParts.path) {
  136. baseParts.path = '/';
  137. }
  138. var builtParts = {
  139. // 2c) Otherwise, the embedded URL inherits the scheme of
  140. // the base URL.
  141. scheme: baseParts.scheme,
  142. netLoc: relativeParts.netLoc,
  143. path: null,
  144. params: relativeParts.params,
  145. query: relativeParts.query,
  146. fragment: relativeParts.fragment
  147. };
  148. if (!relativeParts.netLoc) {
  149. // 3) If the embedded URL's <net_loc> is non-empty, we skip to
  150. // Step 7. Otherwise, the embedded URL inherits the <net_loc>
  151. // (if any) of the base URL.
  152. builtParts.netLoc = baseParts.netLoc; // 4) If the embedded URL path is preceded by a slash "/", the
  153. // path is not relative and we skip to Step 7.
  154. if (relativeParts.path[0] !== '/') {
  155. if (!relativeParts.path) {
  156. // 5) If the embedded URL path is empty (and not preceded by a
  157. // slash), then the embedded URL inherits the base URL path
  158. builtParts.path = baseParts.path; // 5a) if the embedded URL's <params> is non-empty, we skip to
  159. // step 7; otherwise, it inherits the <params> of the base
  160. // URL (if any) and
  161. if (!relativeParts.params) {
  162. builtParts.params = baseParts.params; // 5b) if the embedded URL's <query> is non-empty, we skip to
  163. // step 7; otherwise, it inherits the <query> of the base
  164. // URL (if any) and we skip to step 7.
  165. if (!relativeParts.query) {
  166. builtParts.query = baseParts.query;
  167. }
  168. }
  169. } else {
  170. // 6) The last segment of the base URL's path (anything
  171. // following the rightmost slash "/", or the entire path if no
  172. // slash is present) is removed and the embedded URL's path is
  173. // appended in its place.
  174. var baseURLPath = baseParts.path;
  175. var newPath = baseURLPath.substring(0, baseURLPath.lastIndexOf('/') + 1) + relativeParts.path;
  176. builtParts.path = URLToolkit.normalizePath(newPath);
  177. }
  178. }
  179. }
  180. if (builtParts.path === null) {
  181. builtParts.path = opts.alwaysNormalize ? URLToolkit.normalizePath(relativeParts.path) : relativeParts.path;
  182. }
  183. return URLToolkit.buildURLFromParts(builtParts);
  184. },
  185. parseURL: function (url) {
  186. var parts = URL_REGEX.exec(url);
  187. if (!parts) {
  188. return null;
  189. }
  190. return {
  191. scheme: parts[1] || '',
  192. netLoc: parts[2] || '',
  193. path: parts[3] || '',
  194. params: parts[4] || '',
  195. query: parts[5] || '',
  196. fragment: parts[6] || ''
  197. };
  198. },
  199. normalizePath: function (path) {
  200. // The following operations are
  201. // then applied, in order, to the new path:
  202. // 6a) All occurrences of "./", where "." is a complete path
  203. // segment, are removed.
  204. // 6b) If the path ends with "." as a complete path segment,
  205. // that "." is removed.
  206. path = path.split('').reverse().join('').replace(SLASH_DOT_REGEX, ''); // 6c) All occurrences of "<segment>/../", where <segment> is a
  207. // complete path segment not equal to "..", are removed.
  208. // Removal of these path segments is performed iteratively,
  209. // removing the leftmost matching pattern on each iteration,
  210. // until no matching pattern remains.
  211. // 6d) If the path ends with "<segment>/..", where <segment> is a
  212. // complete path segment not equal to "..", that
  213. // "<segment>/.." is removed.
  214. while (path.length !== (path = path.replace(SLASH_DOT_DOT_REGEX, '')).length) {}
  215. return path.split('').reverse().join('');
  216. },
  217. buildURLFromParts: function (parts) {
  218. return parts.scheme + parts.netLoc + parts.path + parts.params + parts.query + parts.fragment;
  219. }
  220. };
  221. module.exports = URLToolkit;
  222. })();
  223. })(urlToolkit);
  224. var URLToolkit = urlToolkit.exports;
  225. var DEFAULT_LOCATION = 'http://example.com';
  226. var resolveUrl = function resolveUrl(baseUrl, relativeUrl) {
  227. // return early if we don't need to resolve
  228. if (/^[a-z]+:/i.test(relativeUrl)) {
  229. return relativeUrl;
  230. } // if baseUrl is a data URI, ignore it and resolve everything relative to window.location
  231. if (/^data:/.test(baseUrl)) {
  232. baseUrl = window.location && window.location.href || '';
  233. } // IE11 supports URL but not the URL constructor
  234. // feature detect the behavior we want
  235. var nativeURL = typeof window.URL === 'function';
  236. var protocolLess = /^\/\//.test(baseUrl); // remove location if window.location isn't available (i.e. we're in node)
  237. // and if baseUrl isn't an absolute url
  238. var removeLocation = !window.location && !/\/\//i.test(baseUrl); // if the base URL is relative then combine with the current location
  239. if (nativeURL) {
  240. baseUrl = new window.URL(baseUrl, window.location || DEFAULT_LOCATION);
  241. } else if (!/\/\//i.test(baseUrl)) {
  242. baseUrl = URLToolkit.buildAbsoluteURL(window.location && window.location.href || '', baseUrl);
  243. }
  244. if (nativeURL) {
  245. var newUrl = new URL(relativeUrl, baseUrl); // if we're a protocol-less url, remove the protocol
  246. // and if we're location-less, remove the location
  247. // otherwise, return the url unmodified
  248. if (removeLocation) {
  249. return newUrl.href.slice(DEFAULT_LOCATION.length);
  250. } else if (protocolLess) {
  251. return newUrl.href.slice(newUrl.protocol.length);
  252. }
  253. return newUrl.href;
  254. }
  255. return URLToolkit.buildAbsoluteURL(baseUrl, relativeUrl);
  256. };
  257. /**
  258. * @typedef {Object} SingleUri
  259. * @property {string} uri - relative location of segment
  260. * @property {string} resolvedUri - resolved location of segment
  261. * @property {Object} byterange - Object containing information on how to make byte range
  262. * requests following byte-range-spec per RFC2616.
  263. * @property {String} byterange.length - length of range request
  264. * @property {String} byterange.offset - byte offset of range request
  265. *
  266. * @see https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35.1
  267. */
  268. /**
  269. * Converts a URLType node (5.3.9.2.3 Table 13) to a segment object
  270. * that conforms to how m3u8-parser is structured
  271. *
  272. * @see https://github.com/videojs/m3u8-parser
  273. *
  274. * @param {string} baseUrl - baseUrl provided by <BaseUrl> nodes
  275. * @param {string} source - source url for segment
  276. * @param {string} range - optional range used for range calls,
  277. * follows RFC 2616, Clause 14.35.1
  278. * @return {SingleUri} full segment information transformed into a format similar
  279. * to m3u8-parser
  280. */
  281. const urlTypeToSegment = ({
  282. baseUrl = '',
  283. source = '',
  284. range = '',
  285. indexRange = ''
  286. }) => {
  287. const segment = {
  288. uri: source,
  289. resolvedUri: resolveUrl(baseUrl || '', source)
  290. };
  291. if (range || indexRange) {
  292. const rangeStr = range ? range : indexRange;
  293. const ranges = rangeStr.split('-'); // default to parsing this as a BigInt if possible
  294. let startRange = window.BigInt ? window.BigInt(ranges[0]) : parseInt(ranges[0], 10);
  295. let endRange = window.BigInt ? window.BigInt(ranges[1]) : parseInt(ranges[1], 10); // convert back to a number if less than MAX_SAFE_INTEGER
  296. if (startRange < Number.MAX_SAFE_INTEGER && typeof startRange === 'bigint') {
  297. startRange = Number(startRange);
  298. }
  299. if (endRange < Number.MAX_SAFE_INTEGER && typeof endRange === 'bigint') {
  300. endRange = Number(endRange);
  301. }
  302. let length;
  303. if (typeof endRange === 'bigint' || typeof startRange === 'bigint') {
  304. length = window.BigInt(endRange) - window.BigInt(startRange) + window.BigInt(1);
  305. } else {
  306. length = endRange - startRange + 1;
  307. }
  308. if (typeof length === 'bigint' && length < Number.MAX_SAFE_INTEGER) {
  309. length = Number(length);
  310. } // byterange should be inclusive according to
  311. // RFC 2616, Clause 14.35.1
  312. segment.byterange = {
  313. length,
  314. offset: startRange
  315. };
  316. }
  317. return segment;
  318. };
  319. const byteRangeToString = byterange => {
  320. // `endRange` is one less than `offset + length` because the HTTP range
  321. // header uses inclusive ranges
  322. let endRange;
  323. if (typeof byterange.offset === 'bigint' || typeof byterange.length === 'bigint') {
  324. endRange = window.BigInt(byterange.offset) + window.BigInt(byterange.length) - window.BigInt(1);
  325. } else {
  326. endRange = byterange.offset + byterange.length - 1;
  327. }
  328. return `${byterange.offset}-${endRange}`;
  329. };
  330. /**
  331. * parse the end number attribue that can be a string
  332. * number, or undefined.
  333. *
  334. * @param {string|number|undefined} endNumber
  335. * The end number attribute.
  336. *
  337. * @return {number|null}
  338. * The result of parsing the end number.
  339. */
  340. const parseEndNumber = endNumber => {
  341. if (endNumber && typeof endNumber !== 'number') {
  342. endNumber = parseInt(endNumber, 10);
  343. }
  344. if (isNaN(endNumber)) {
  345. return null;
  346. }
  347. return endNumber;
  348. };
  349. /**
  350. * Functions for calculating the range of available segments in static and dynamic
  351. * manifests.
  352. */
  353. const segmentRange = {
  354. /**
  355. * Returns the entire range of available segments for a static MPD
  356. *
  357. * @param {Object} attributes
  358. * Inheritied MPD attributes
  359. * @return {{ start: number, end: number }}
  360. * The start and end numbers for available segments
  361. */
  362. static(attributes) {
  363. const {
  364. duration,
  365. timescale = 1,
  366. sourceDuration,
  367. periodDuration
  368. } = attributes;
  369. const endNumber = parseEndNumber(attributes.endNumber);
  370. const segmentDuration = duration / timescale;
  371. if (typeof endNumber === 'number') {
  372. return {
  373. start: 0,
  374. end: endNumber
  375. };
  376. }
  377. if (typeof periodDuration === 'number') {
  378. return {
  379. start: 0,
  380. end: periodDuration / segmentDuration
  381. };
  382. }
  383. return {
  384. start: 0,
  385. end: sourceDuration / segmentDuration
  386. };
  387. },
  388. /**
  389. * Returns the current live window range of available segments for a dynamic MPD
  390. *
  391. * @param {Object} attributes
  392. * Inheritied MPD attributes
  393. * @return {{ start: number, end: number }}
  394. * The start and end numbers for available segments
  395. */
  396. dynamic(attributes) {
  397. const {
  398. NOW,
  399. clientOffset,
  400. availabilityStartTime,
  401. timescale = 1,
  402. duration,
  403. periodStart = 0,
  404. minimumUpdatePeriod = 0,
  405. timeShiftBufferDepth = Infinity
  406. } = attributes;
  407. const endNumber = parseEndNumber(attributes.endNumber); // clientOffset is passed in at the top level of mpd-parser and is an offset calculated
  408. // after retrieving UTC server time.
  409. const now = (NOW + clientOffset) / 1000; // WC stands for Wall Clock.
  410. // Convert the period start time to EPOCH.
  411. const periodStartWC = availabilityStartTime + periodStart; // Period end in EPOCH is manifest's retrieval time + time until next update.
  412. const periodEndWC = now + minimumUpdatePeriod;
  413. const periodDuration = periodEndWC - periodStartWC;
  414. const segmentCount = Math.ceil(periodDuration * timescale / duration);
  415. const availableStart = Math.floor((now - periodStartWC - timeShiftBufferDepth) * timescale / duration);
  416. const availableEnd = Math.floor((now - periodStartWC) * timescale / duration);
  417. return {
  418. start: Math.max(0, availableStart),
  419. end: typeof endNumber === 'number' ? endNumber : Math.min(segmentCount, availableEnd)
  420. };
  421. }
  422. };
  423. /**
  424. * Maps a range of numbers to objects with information needed to build the corresponding
  425. * segment list
  426. *
  427. * @name toSegmentsCallback
  428. * @function
  429. * @param {number} number
  430. * Number of the segment
  431. * @param {number} index
  432. * Index of the number in the range list
  433. * @return {{ number: Number, duration: Number, timeline: Number, time: Number }}
  434. * Object with segment timing and duration info
  435. */
  436. /**
  437. * Returns a callback for Array.prototype.map for mapping a range of numbers to
  438. * information needed to build the segment list.
  439. *
  440. * @param {Object} attributes
  441. * Inherited MPD attributes
  442. * @return {toSegmentsCallback}
  443. * Callback map function
  444. */
  445. const toSegments = attributes => number => {
  446. const {
  447. duration,
  448. timescale = 1,
  449. periodStart,
  450. startNumber = 1
  451. } = attributes;
  452. return {
  453. number: startNumber + number,
  454. duration: duration / timescale,
  455. timeline: periodStart,
  456. time: number * duration
  457. };
  458. };
  459. /**
  460. * Returns a list of objects containing segment timing and duration info used for
  461. * building the list of segments. This uses the @duration attribute specified
  462. * in the MPD manifest to derive the range of segments.
  463. *
  464. * @param {Object} attributes
  465. * Inherited MPD attributes
  466. * @return {{number: number, duration: number, time: number, timeline: number}[]}
  467. * List of Objects with segment timing and duration info
  468. */
  469. const parseByDuration = attributes => {
  470. const {
  471. type,
  472. duration,
  473. timescale = 1,
  474. periodDuration,
  475. sourceDuration
  476. } = attributes;
  477. const {
  478. start,
  479. end
  480. } = segmentRange[type](attributes);
  481. const segments = range(start, end).map(toSegments(attributes));
  482. if (type === 'static') {
  483. const index = segments.length - 1; // section is either a period or the full source
  484. const sectionDuration = typeof periodDuration === 'number' ? periodDuration : sourceDuration; // final segment may be less than full segment duration
  485. segments[index].duration = sectionDuration - duration / timescale * index;
  486. }
  487. return segments;
  488. };
  489. /**
  490. * Translates SegmentBase into a set of segments.
  491. * (DASH SPEC Section 5.3.9.3.2) contains a set of <SegmentURL> nodes. Each
  492. * node should be translated into segment.
  493. *
  494. * @param {Object} attributes
  495. * Object containing all inherited attributes from parent elements with attribute
  496. * names as keys
  497. * @return {Object.<Array>} list of segments
  498. */
  499. const segmentsFromBase = attributes => {
  500. const {
  501. baseUrl,
  502. initialization = {},
  503. sourceDuration,
  504. indexRange = '',
  505. periodStart,
  506. presentationTime,
  507. number = 0,
  508. duration
  509. } = attributes; // base url is required for SegmentBase to work, per spec (Section 5.3.9.2.1)
  510. if (!baseUrl) {
  511. throw new Error(errors.NO_BASE_URL);
  512. }
  513. const initSegment = urlTypeToSegment({
  514. baseUrl,
  515. source: initialization.sourceURL,
  516. range: initialization.range
  517. });
  518. const segment = urlTypeToSegment({
  519. baseUrl,
  520. source: baseUrl,
  521. indexRange
  522. });
  523. segment.map = initSegment; // If there is a duration, use it, otherwise use the given duration of the source
  524. // (since SegmentBase is only for one total segment)
  525. if (duration) {
  526. const segmentTimeInfo = parseByDuration(attributes);
  527. if (segmentTimeInfo.length) {
  528. segment.duration = segmentTimeInfo[0].duration;
  529. segment.timeline = segmentTimeInfo[0].timeline;
  530. }
  531. } else if (sourceDuration) {
  532. segment.duration = sourceDuration;
  533. segment.timeline = periodStart;
  534. } // If presentation time is provided, these segments are being generated by SIDX
  535. // references, and should use the time provided. For the general case of SegmentBase,
  536. // there should only be one segment in the period, so its presentation time is the same
  537. // as its period start.
  538. segment.presentationTime = presentationTime || periodStart;
  539. segment.number = number;
  540. return [segment];
  541. };
  542. /**
  543. * Given a playlist, a sidx box, and a baseUrl, update the segment list of the playlist
  544. * according to the sidx information given.
  545. *
  546. * playlist.sidx has metadadata about the sidx where-as the sidx param
  547. * is the parsed sidx box itself.
  548. *
  549. * @param {Object} playlist the playlist to update the sidx information for
  550. * @param {Object} sidx the parsed sidx box
  551. * @return {Object} the playlist object with the updated sidx information
  552. */
  553. const addSidxSegmentsToPlaylist$1 = (playlist, sidx, baseUrl) => {
  554. // Retain init segment information
  555. const initSegment = playlist.sidx.map ? playlist.sidx.map : null; // Retain source duration from initial main manifest parsing
  556. const sourceDuration = playlist.sidx.duration; // Retain source timeline
  557. const timeline = playlist.timeline || 0;
  558. const sidxByteRange = playlist.sidx.byterange;
  559. const sidxEnd = sidxByteRange.offset + sidxByteRange.length; // Retain timescale of the parsed sidx
  560. const timescale = sidx.timescale; // referenceType 1 refers to other sidx boxes
  561. const mediaReferences = sidx.references.filter(r => r.referenceType !== 1);
  562. const segments = [];
  563. const type = playlist.endList ? 'static' : 'dynamic';
  564. const periodStart = playlist.sidx.timeline;
  565. let presentationTime = periodStart;
  566. let number = playlist.mediaSequence || 0; // firstOffset is the offset from the end of the sidx box
  567. let startIndex; // eslint-disable-next-line
  568. if (typeof sidx.firstOffset === 'bigint') {
  569. startIndex = window.BigInt(sidxEnd) + sidx.firstOffset;
  570. } else {
  571. startIndex = sidxEnd + sidx.firstOffset;
  572. }
  573. for (let i = 0; i < mediaReferences.length; i++) {
  574. const reference = sidx.references[i]; // size of the referenced (sub)segment
  575. const size = reference.referencedSize; // duration of the referenced (sub)segment, in the timescale
  576. // this will be converted to seconds when generating segments
  577. const duration = reference.subsegmentDuration; // should be an inclusive range
  578. let endIndex; // eslint-disable-next-line
  579. if (typeof startIndex === 'bigint') {
  580. endIndex = startIndex + window.BigInt(size) - window.BigInt(1);
  581. } else {
  582. endIndex = startIndex + size - 1;
  583. }
  584. const indexRange = `${startIndex}-${endIndex}`;
  585. const attributes = {
  586. baseUrl,
  587. timescale,
  588. timeline,
  589. periodStart,
  590. presentationTime,
  591. number,
  592. duration,
  593. sourceDuration,
  594. indexRange,
  595. type
  596. };
  597. const segment = segmentsFromBase(attributes)[0];
  598. if (initSegment) {
  599. segment.map = initSegment;
  600. }
  601. segments.push(segment);
  602. if (typeof startIndex === 'bigint') {
  603. startIndex += window.BigInt(size);
  604. } else {
  605. startIndex += size;
  606. }
  607. presentationTime += duration / timescale;
  608. number++;
  609. }
  610. playlist.segments = segments;
  611. return playlist;
  612. };
  613. /**
  614. * Loops through all supported media groups in master and calls the provided
  615. * callback for each group
  616. *
  617. * @param {Object} master
  618. * The parsed master manifest object
  619. * @param {string[]} groups
  620. * The media groups to call the callback for
  621. * @param {Function} callback
  622. * Callback to call for each media group
  623. */
  624. var forEachMediaGroup = function forEachMediaGroup(master, groups, callback) {
  625. groups.forEach(function (mediaType) {
  626. for (var groupKey in master.mediaGroups[mediaType]) {
  627. for (var labelKey in master.mediaGroups[mediaType][groupKey]) {
  628. var mediaProperties = master.mediaGroups[mediaType][groupKey][labelKey];
  629. callback(mediaProperties, mediaType, groupKey, labelKey);
  630. }
  631. }
  632. });
  633. };
  634. const SUPPORTED_MEDIA_TYPES = ['AUDIO', 'SUBTITLES']; // allow one 60fps frame as leniency (arbitrarily chosen)
  635. const TIME_FUDGE = 1 / 60;
  636. /**
  637. * Given a list of timelineStarts, combines, dedupes, and sorts them.
  638. *
  639. * @param {TimelineStart[]} timelineStarts - list of timeline starts
  640. *
  641. * @return {TimelineStart[]} the combined and deduped timeline starts
  642. */
  643. const getUniqueTimelineStarts = timelineStarts => {
  644. return union(timelineStarts, ({
  645. timeline
  646. }) => timeline).sort((a, b) => a.timeline > b.timeline ? 1 : -1);
  647. };
  648. /**
  649. * Finds the playlist with the matching NAME attribute.
  650. *
  651. * @param {Array} playlists - playlists to search through
  652. * @param {string} name - the NAME attribute to search for
  653. *
  654. * @return {Object|null} the matching playlist object, or null
  655. */
  656. const findPlaylistWithName = (playlists, name) => {
  657. for (let i = 0; i < playlists.length; i++) {
  658. if (playlists[i].attributes.NAME === name) {
  659. return playlists[i];
  660. }
  661. }
  662. return null;
  663. };
  664. /**
  665. * Gets a flattened array of media group playlists.
  666. *
  667. * @param {Object} manifest - the main manifest object
  668. *
  669. * @return {Array} the media group playlists
  670. */
  671. const getMediaGroupPlaylists = manifest => {
  672. let mediaGroupPlaylists = [];
  673. forEachMediaGroup(manifest, SUPPORTED_MEDIA_TYPES, (properties, type, group, label) => {
  674. mediaGroupPlaylists = mediaGroupPlaylists.concat(properties.playlists || []);
  675. });
  676. return mediaGroupPlaylists;
  677. };
  678. /**
  679. * Updates the playlist's media sequence numbers.
  680. *
  681. * @param {Object} config - options object
  682. * @param {Object} config.playlist - the playlist to update
  683. * @param {number} config.mediaSequence - the mediaSequence number to start with
  684. */
  685. const updateMediaSequenceForPlaylist = ({
  686. playlist,
  687. mediaSequence
  688. }) => {
  689. playlist.mediaSequence = mediaSequence;
  690. playlist.segments.forEach((segment, index) => {
  691. segment.number = playlist.mediaSequence + index;
  692. });
  693. };
  694. /**
  695. * Updates the media and discontinuity sequence numbers of newPlaylists given oldPlaylists
  696. * and a complete list of timeline starts.
  697. *
  698. * If no matching playlist is found, only the discontinuity sequence number of the playlist
  699. * will be updated.
  700. *
  701. * Since early available timelines are not supported, at least one segment must be present.
  702. *
  703. * @param {Object} config - options object
  704. * @param {Object[]} oldPlaylists - the old playlists to use as a reference
  705. * @param {Object[]} newPlaylists - the new playlists to update
  706. * @param {Object} timelineStarts - all timelineStarts seen in the stream to this point
  707. */
  708. const updateSequenceNumbers = ({
  709. oldPlaylists,
  710. newPlaylists,
  711. timelineStarts
  712. }) => {
  713. newPlaylists.forEach(playlist => {
  714. playlist.discontinuitySequence = timelineStarts.findIndex(function ({
  715. timeline
  716. }) {
  717. return timeline === playlist.timeline;
  718. }); // Playlists NAMEs come from DASH Representation IDs, which are mandatory
  719. // (see ISO_23009-1-2012 5.3.5.2).
  720. //
  721. // If the same Representation existed in a prior Period, it will retain the same NAME.
  722. const oldPlaylist = findPlaylistWithName(oldPlaylists, playlist.attributes.NAME);
  723. if (!oldPlaylist) {
  724. // Since this is a new playlist, the media sequence values can start from 0 without
  725. // consequence.
  726. return;
  727. } // TODO better support for live SIDX
  728. //
  729. // As of this writing, mpd-parser does not support multiperiod SIDX (in live or VOD).
  730. // This is evident by a playlist only having a single SIDX reference. In a multiperiod
  731. // playlist there would need to be multiple SIDX references. In addition, live SIDX is
  732. // not supported when the SIDX properties change on refreshes.
  733. //
  734. // In the future, if support needs to be added, the merging logic here can be called
  735. // after SIDX references are resolved. For now, exit early to prevent exceptions being
  736. // thrown due to undefined references.
  737. if (playlist.sidx) {
  738. return;
  739. } // Since we don't yet support early available timelines, we don't need to support
  740. // playlists with no segments.
  741. const firstNewSegment = playlist.segments[0];
  742. const oldMatchingSegmentIndex = oldPlaylist.segments.findIndex(function (oldSegment) {
  743. return Math.abs(oldSegment.presentationTime - firstNewSegment.presentationTime) < TIME_FUDGE;
  744. }); // No matching segment from the old playlist means the entire playlist was refreshed.
  745. // In this case the media sequence should account for this update, and the new segments
  746. // should be marked as discontinuous from the prior content, since the last prior
  747. // timeline was removed.
  748. if (oldMatchingSegmentIndex === -1) {
  749. updateMediaSequenceForPlaylist({
  750. playlist,
  751. mediaSequence: oldPlaylist.mediaSequence + oldPlaylist.segments.length
  752. });
  753. playlist.segments[0].discontinuity = true;
  754. playlist.discontinuityStarts.unshift(0); // No matching segment does not necessarily mean there's missing content.
  755. //
  756. // If the new playlist's timeline is the same as the last seen segment's timeline,
  757. // then a discontinuity can be added to identify that there's potentially missing
  758. // content. If there's no missing content, the discontinuity should still be rather
  759. // harmless. It's possible that if segment durations are accurate enough, that the
  760. // existence of a gap can be determined using the presentation times and durations,
  761. // but if the segment timing info is off, it may introduce more problems than simply
  762. // adding the discontinuity.
  763. //
  764. // If the new playlist's timeline is different from the last seen segment's timeline,
  765. // then a discontinuity can be added to identify that this is the first seen segment
  766. // of a new timeline. However, the logic at the start of this function that
  767. // determined the disconinuity sequence by timeline index is now off by one (the
  768. // discontinuity of the newest timeline hasn't yet fallen off the manifest...since
  769. // we added it), so the disconinuity sequence must be decremented.
  770. //
  771. // A period may also have a duration of zero, so the case of no segments is handled
  772. // here even though we don't yet support early available periods.
  773. if (!oldPlaylist.segments.length && playlist.timeline > oldPlaylist.timeline || oldPlaylist.segments.length && playlist.timeline > oldPlaylist.segments[oldPlaylist.segments.length - 1].timeline) {
  774. playlist.discontinuitySequence--;
  775. }
  776. return;
  777. } // If the first segment matched with a prior segment on a discontinuity (it's matching
  778. // on the first segment of a period), then the discontinuitySequence shouldn't be the
  779. // timeline's matching one, but instead should be the one prior, and the first segment
  780. // of the new manifest should be marked with a discontinuity.
  781. //
  782. // The reason for this special case is that discontinuity sequence shows how many
  783. // discontinuities have fallen off of the playlist, and discontinuities are marked on
  784. // the first segment of a new "timeline." Because of this, while DASH will retain that
  785. // Period while the "timeline" exists, HLS keeps track of it via the discontinuity
  786. // sequence, and that first segment is an indicator, but can be removed before that
  787. // timeline is gone.
  788. const oldMatchingSegment = oldPlaylist.segments[oldMatchingSegmentIndex];
  789. if (oldMatchingSegment.discontinuity && !firstNewSegment.discontinuity) {
  790. firstNewSegment.discontinuity = true;
  791. playlist.discontinuityStarts.unshift(0);
  792. playlist.discontinuitySequence--;
  793. }
  794. updateMediaSequenceForPlaylist({
  795. playlist,
  796. mediaSequence: oldPlaylist.segments[oldMatchingSegmentIndex].number
  797. });
  798. });
  799. };
  800. /**
  801. * Given an old parsed manifest object and a new parsed manifest object, updates the
  802. * sequence and timing values within the new manifest to ensure that it lines up with the
  803. * old.
  804. *
  805. * @param {Array} oldManifest - the old main manifest object
  806. * @param {Array} newManifest - the new main manifest object
  807. *
  808. * @return {Object} the updated new manifest object
  809. */
  810. const positionManifestOnTimeline = ({
  811. oldManifest,
  812. newManifest
  813. }) => {
  814. // Starting from v4.1.2 of the IOP, section 4.4.3.3 states:
  815. //
  816. // "MPD@availabilityStartTime and Period@start shall not be changed over MPD updates."
  817. //
  818. // This was added from https://github.com/Dash-Industry-Forum/DASH-IF-IOP/issues/160
  819. //
  820. // Because of this change, and the difficulty of supporting periods with changing start
  821. // times, periods with changing start times are not supported. This makes the logic much
  822. // simpler, since periods with the same start time can be considerred the same period
  823. // across refreshes.
  824. //
  825. // To give an example as to the difficulty of handling periods where the start time may
  826. // change, if a single period manifest is refreshed with another manifest with a single
  827. // period, and both the start and end times are increased, then the only way to determine
  828. // if it's a new period or an old one that has changed is to look through the segments of
  829. // each playlist and determine the presentation time bounds to find a match. In addition,
  830. // if the period start changed to exceed the old period end, then there would be no
  831. // match, and it would not be possible to determine whether the refreshed period is a new
  832. // one or the old one.
  833. const oldPlaylists = oldManifest.playlists.concat(getMediaGroupPlaylists(oldManifest));
  834. const newPlaylists = newManifest.playlists.concat(getMediaGroupPlaylists(newManifest)); // Save all seen timelineStarts to the new manifest. Although this potentially means that
  835. // there's a "memory leak" in that it will never stop growing, in reality, only a couple
  836. // of properties are saved for each seen Period. Even long running live streams won't
  837. // generate too many Periods, unless the stream is watched for decades. In the future,
  838. // this can be optimized by mapping to discontinuity sequence numbers for each timeline,
  839. // but it may not become an issue, and the additional info can be useful for debugging.
  840. newManifest.timelineStarts = getUniqueTimelineStarts([oldManifest.timelineStarts, newManifest.timelineStarts]);
  841. updateSequenceNumbers({
  842. oldPlaylists,
  843. newPlaylists,
  844. timelineStarts: newManifest.timelineStarts
  845. });
  846. return newManifest;
  847. };
  848. const generateSidxKey = sidx => sidx && sidx.uri + '-' + byteRangeToString(sidx.byterange);
  849. const mergeDiscontiguousPlaylists = playlists => {
  850. const mergedPlaylists = values(playlists.reduce((acc, playlist) => {
  851. // assuming playlist IDs are the same across periods
  852. // TODO: handle multiperiod where representation sets are not the same
  853. // across periods
  854. const name = playlist.attributes.id + (playlist.attributes.lang || '');
  855. if (!acc[name]) {
  856. // First Period
  857. acc[name] = playlist;
  858. acc[name].attributes.timelineStarts = [];
  859. } else {
  860. // Subsequent Periods
  861. if (playlist.segments) {
  862. // first segment of subsequent periods signal a discontinuity
  863. if (playlist.segments[0]) {
  864. playlist.segments[0].discontinuity = true;
  865. }
  866. acc[name].segments.push(...playlist.segments);
  867. } // bubble up contentProtection, this assumes all DRM content
  868. // has the same contentProtection
  869. if (playlist.attributes.contentProtection) {
  870. acc[name].attributes.contentProtection = playlist.attributes.contentProtection;
  871. }
  872. }
  873. acc[name].attributes.timelineStarts.push({
  874. // Although they represent the same number, it's important to have both to make it
  875. // compatible with HLS potentially having a similar attribute.
  876. start: playlist.attributes.periodStart,
  877. timeline: playlist.attributes.periodStart
  878. });
  879. return acc;
  880. }, {}));
  881. return mergedPlaylists.map(playlist => {
  882. playlist.discontinuityStarts = findIndexes(playlist.segments || [], 'discontinuity');
  883. return playlist;
  884. });
  885. };
  886. const addSidxSegmentsToPlaylist = (playlist, sidxMapping) => {
  887. const sidxKey = generateSidxKey(playlist.sidx);
  888. const sidxMatch = sidxKey && sidxMapping[sidxKey] && sidxMapping[sidxKey].sidx;
  889. if (sidxMatch) {
  890. addSidxSegmentsToPlaylist$1(playlist, sidxMatch, playlist.sidx.resolvedUri);
  891. }
  892. return playlist;
  893. };
  894. const addSidxSegmentsToPlaylists = (playlists, sidxMapping = {}) => {
  895. if (!Object.keys(sidxMapping).length) {
  896. return playlists;
  897. }
  898. for (const i in playlists) {
  899. playlists[i] = addSidxSegmentsToPlaylist(playlists[i], sidxMapping);
  900. }
  901. return playlists;
  902. };
  903. const formatAudioPlaylist = ({
  904. attributes,
  905. segments,
  906. sidx,
  907. mediaSequence,
  908. discontinuitySequence,
  909. discontinuityStarts
  910. }, isAudioOnly) => {
  911. const playlist = {
  912. attributes: {
  913. NAME: attributes.id,
  914. BANDWIDTH: attributes.bandwidth,
  915. CODECS: attributes.codecs,
  916. ['PROGRAM-ID']: 1
  917. },
  918. uri: '',
  919. endList: attributes.type === 'static',
  920. timeline: attributes.periodStart,
  921. resolvedUri: '',
  922. targetDuration: attributes.duration,
  923. discontinuitySequence,
  924. discontinuityStarts,
  925. timelineStarts: attributes.timelineStarts,
  926. mediaSequence,
  927. segments
  928. };
  929. if (attributes.contentProtection) {
  930. playlist.contentProtection = attributes.contentProtection;
  931. }
  932. if (sidx) {
  933. playlist.sidx = sidx;
  934. }
  935. if (isAudioOnly) {
  936. playlist.attributes.AUDIO = 'audio';
  937. playlist.attributes.SUBTITLES = 'subs';
  938. }
  939. return playlist;
  940. };
  941. const formatVttPlaylist = ({
  942. attributes,
  943. segments,
  944. mediaSequence,
  945. discontinuityStarts,
  946. discontinuitySequence
  947. }) => {
  948. if (typeof segments === 'undefined') {
  949. // vtt tracks may use single file in BaseURL
  950. segments = [{
  951. uri: attributes.baseUrl,
  952. timeline: attributes.periodStart,
  953. resolvedUri: attributes.baseUrl || '',
  954. duration: attributes.sourceDuration,
  955. number: 0
  956. }]; // targetDuration should be the same duration as the only segment
  957. attributes.duration = attributes.sourceDuration;
  958. }
  959. const m3u8Attributes = {
  960. NAME: attributes.id,
  961. BANDWIDTH: attributes.bandwidth,
  962. ['PROGRAM-ID']: 1
  963. };
  964. if (attributes.codecs) {
  965. m3u8Attributes.CODECS = attributes.codecs;
  966. }
  967. return {
  968. attributes: m3u8Attributes,
  969. uri: '',
  970. endList: attributes.type === 'static',
  971. timeline: attributes.periodStart,
  972. resolvedUri: attributes.baseUrl || '',
  973. targetDuration: attributes.duration,
  974. timelineStarts: attributes.timelineStarts,
  975. discontinuityStarts,
  976. discontinuitySequence,
  977. mediaSequence,
  978. segments
  979. };
  980. };
  981. const organizeAudioPlaylists = (playlists, sidxMapping = {}, isAudioOnly = false) => {
  982. let mainPlaylist;
  983. const formattedPlaylists = playlists.reduce((a, playlist) => {
  984. const role = playlist.attributes.role && playlist.attributes.role.value || '';
  985. const language = playlist.attributes.lang || '';
  986. let label = playlist.attributes.label || 'main';
  987. if (language && !playlist.attributes.label) {
  988. const roleLabel = role ? ` (${role})` : '';
  989. label = `${playlist.attributes.lang}${roleLabel}`;
  990. }
  991. if (!a[label]) {
  992. a[label] = {
  993. language,
  994. autoselect: true,
  995. default: role === 'main',
  996. playlists: [],
  997. uri: ''
  998. };
  999. }
  1000. const formatted = addSidxSegmentsToPlaylist(formatAudioPlaylist(playlist, isAudioOnly), sidxMapping);
  1001. a[label].playlists.push(formatted);
  1002. if (typeof mainPlaylist === 'undefined' && role === 'main') {
  1003. mainPlaylist = playlist;
  1004. mainPlaylist.default = true;
  1005. }
  1006. return a;
  1007. }, {}); // if no playlists have role "main", mark the first as main
  1008. if (!mainPlaylist) {
  1009. const firstLabel = Object.keys(formattedPlaylists)[0];
  1010. formattedPlaylists[firstLabel].default = true;
  1011. }
  1012. return formattedPlaylists;
  1013. };
  1014. const organizeVttPlaylists = (playlists, sidxMapping = {}) => {
  1015. return playlists.reduce((a, playlist) => {
  1016. const label = playlist.attributes.label || playlist.attributes.lang || 'text';
  1017. if (!a[label]) {
  1018. a[label] = {
  1019. language: label,
  1020. default: false,
  1021. autoselect: false,
  1022. playlists: [],
  1023. uri: ''
  1024. };
  1025. }
  1026. a[label].playlists.push(addSidxSegmentsToPlaylist(formatVttPlaylist(playlist), sidxMapping));
  1027. return a;
  1028. }, {});
  1029. };
  1030. const organizeCaptionServices = captionServices => captionServices.reduce((svcObj, svc) => {
  1031. if (!svc) {
  1032. return svcObj;
  1033. }
  1034. svc.forEach(service => {
  1035. const {
  1036. channel,
  1037. language
  1038. } = service;
  1039. svcObj[language] = {
  1040. autoselect: false,
  1041. default: false,
  1042. instreamId: channel,
  1043. language
  1044. };
  1045. if (service.hasOwnProperty('aspectRatio')) {
  1046. svcObj[language].aspectRatio = service.aspectRatio;
  1047. }
  1048. if (service.hasOwnProperty('easyReader')) {
  1049. svcObj[language].easyReader = service.easyReader;
  1050. }
  1051. if (service.hasOwnProperty('3D')) {
  1052. svcObj[language]['3D'] = service['3D'];
  1053. }
  1054. });
  1055. return svcObj;
  1056. }, {});
  1057. const formatVideoPlaylist = ({
  1058. attributes,
  1059. segments,
  1060. sidx,
  1061. discontinuityStarts
  1062. }) => {
  1063. const playlist = {
  1064. attributes: {
  1065. NAME: attributes.id,
  1066. AUDIO: 'audio',
  1067. SUBTITLES: 'subs',
  1068. RESOLUTION: {
  1069. width: attributes.width,
  1070. height: attributes.height
  1071. },
  1072. CODECS: attributes.codecs,
  1073. BANDWIDTH: attributes.bandwidth,
  1074. ['PROGRAM-ID']: 1
  1075. },
  1076. uri: '',
  1077. endList: attributes.type === 'static',
  1078. timeline: attributes.periodStart,
  1079. resolvedUri: '',
  1080. targetDuration: attributes.duration,
  1081. discontinuityStarts,
  1082. timelineStarts: attributes.timelineStarts,
  1083. segments
  1084. };
  1085. if (attributes.frameRate) {
  1086. playlist.attributes['FRAME-RATE'] = attributes.frameRate;
  1087. }
  1088. if (attributes.contentProtection) {
  1089. playlist.contentProtection = attributes.contentProtection;
  1090. }
  1091. if (sidx) {
  1092. playlist.sidx = sidx;
  1093. }
  1094. return playlist;
  1095. };
  1096. const videoOnly = ({
  1097. attributes
  1098. }) => attributes.mimeType === 'video/mp4' || attributes.mimeType === 'video/webm' || attributes.contentType === 'video';
  1099. const audioOnly = ({
  1100. attributes
  1101. }) => attributes.mimeType === 'audio/mp4' || attributes.mimeType === 'audio/webm' || attributes.contentType === 'audio';
  1102. const vttOnly = ({
  1103. attributes
  1104. }) => attributes.mimeType === 'text/vtt' || attributes.contentType === 'text';
  1105. /**
  1106. * Contains start and timeline properties denoting a timeline start. For DASH, these will
  1107. * be the same number.
  1108. *
  1109. * @typedef {Object} TimelineStart
  1110. * @property {number} start - the start time of the timeline
  1111. * @property {number} timeline - the timeline number
  1112. */
  1113. /**
  1114. * Adds appropriate media and discontinuity sequence values to the segments and playlists.
  1115. *
  1116. * Throughout mpd-parser, the `number` attribute is used in relation to `startNumber`, a
  1117. * DASH specific attribute used in constructing segment URI's from templates. However, from
  1118. * an HLS perspective, the `number` attribute on a segment would be its `mediaSequence`
  1119. * value, which should start at the original media sequence value (or 0) and increment by 1
  1120. * for each segment thereafter. Since DASH's `startNumber` values are independent per
  1121. * period, it doesn't make sense to use it for `number`. Instead, assume everything starts
  1122. * from a 0 mediaSequence value and increment from there.
  1123. *
  1124. * Note that VHS currently doesn't use the `number` property, but it can be helpful for
  1125. * debugging and making sense of the manifest.
  1126. *
  1127. * For live playlists, to account for values increasing in manifests when periods are
  1128. * removed on refreshes, merging logic should be used to update the numbers to their
  1129. * appropriate values (to ensure they're sequential and increasing).
  1130. *
  1131. * @param {Object[]} playlists - the playlists to update
  1132. * @param {TimelineStart[]} timelineStarts - the timeline starts for the manifest
  1133. */
  1134. const addMediaSequenceValues = (playlists, timelineStarts) => {
  1135. // increment all segments sequentially
  1136. playlists.forEach(playlist => {
  1137. playlist.mediaSequence = 0;
  1138. playlist.discontinuitySequence = timelineStarts.findIndex(function ({
  1139. timeline
  1140. }) {
  1141. return timeline === playlist.timeline;
  1142. });
  1143. if (!playlist.segments) {
  1144. return;
  1145. }
  1146. playlist.segments.forEach((segment, index) => {
  1147. segment.number = index;
  1148. });
  1149. });
  1150. };
  1151. /**
  1152. * Given a media group object, flattens all playlists within the media group into a single
  1153. * array.
  1154. *
  1155. * @param {Object} mediaGroupObject - the media group object
  1156. *
  1157. * @return {Object[]}
  1158. * The media group playlists
  1159. */
  1160. const flattenMediaGroupPlaylists = mediaGroupObject => {
  1161. if (!mediaGroupObject) {
  1162. return [];
  1163. }
  1164. return Object.keys(mediaGroupObject).reduce((acc, label) => {
  1165. const labelContents = mediaGroupObject[label];
  1166. return acc.concat(labelContents.playlists);
  1167. }, []);
  1168. };
  1169. const toM3u8 = ({
  1170. dashPlaylists,
  1171. locations,
  1172. sidxMapping = {},
  1173. previousManifest,
  1174. eventStream
  1175. }) => {
  1176. if (!dashPlaylists.length) {
  1177. return {};
  1178. } // grab all main manifest attributes
  1179. const {
  1180. sourceDuration: duration,
  1181. type,
  1182. suggestedPresentationDelay,
  1183. minimumUpdatePeriod
  1184. } = dashPlaylists[0].attributes;
  1185. const videoPlaylists = mergeDiscontiguousPlaylists(dashPlaylists.filter(videoOnly)).map(formatVideoPlaylist);
  1186. const audioPlaylists = mergeDiscontiguousPlaylists(dashPlaylists.filter(audioOnly));
  1187. const vttPlaylists = mergeDiscontiguousPlaylists(dashPlaylists.filter(vttOnly));
  1188. const captions = dashPlaylists.map(playlist => playlist.attributes.captionServices).filter(Boolean);
  1189. const manifest = {
  1190. allowCache: true,
  1191. discontinuityStarts: [],
  1192. segments: [],
  1193. endList: true,
  1194. mediaGroups: {
  1195. AUDIO: {},
  1196. VIDEO: {},
  1197. ['CLOSED-CAPTIONS']: {},
  1198. SUBTITLES: {}
  1199. },
  1200. uri: '',
  1201. duration,
  1202. playlists: addSidxSegmentsToPlaylists(videoPlaylists, sidxMapping)
  1203. };
  1204. if (minimumUpdatePeriod >= 0) {
  1205. manifest.minimumUpdatePeriod = minimumUpdatePeriod * 1000;
  1206. }
  1207. if (locations) {
  1208. manifest.locations = locations;
  1209. }
  1210. if (type === 'dynamic') {
  1211. manifest.suggestedPresentationDelay = suggestedPresentationDelay;
  1212. }
  1213. if (eventStream && eventStream.length > 0) {
  1214. manifest.eventStream = eventStream;
  1215. }
  1216. const isAudioOnly = manifest.playlists.length === 0;
  1217. const organizedAudioGroup = audioPlaylists.length ? organizeAudioPlaylists(audioPlaylists, sidxMapping, isAudioOnly) : null;
  1218. const organizedVttGroup = vttPlaylists.length ? organizeVttPlaylists(vttPlaylists, sidxMapping) : null;
  1219. const formattedPlaylists = videoPlaylists.concat(flattenMediaGroupPlaylists(organizedAudioGroup), flattenMediaGroupPlaylists(organizedVttGroup));
  1220. const playlistTimelineStarts = formattedPlaylists.map(({
  1221. timelineStarts
  1222. }) => timelineStarts);
  1223. manifest.timelineStarts = getUniqueTimelineStarts(playlistTimelineStarts);
  1224. addMediaSequenceValues(formattedPlaylists, manifest.timelineStarts);
  1225. if (organizedAudioGroup) {
  1226. manifest.mediaGroups.AUDIO.audio = organizedAudioGroup;
  1227. }
  1228. if (organizedVttGroup) {
  1229. manifest.mediaGroups.SUBTITLES.subs = organizedVttGroup;
  1230. }
  1231. if (captions.length) {
  1232. manifest.mediaGroups['CLOSED-CAPTIONS'].cc = organizeCaptionServices(captions);
  1233. }
  1234. if (previousManifest) {
  1235. return positionManifestOnTimeline({
  1236. oldManifest: previousManifest,
  1237. newManifest: manifest
  1238. });
  1239. }
  1240. return manifest;
  1241. };
  1242. /**
  1243. * Calculates the R (repetition) value for a live stream (for the final segment
  1244. * in a manifest where the r value is negative 1)
  1245. *
  1246. * @param {Object} attributes
  1247. * Object containing all inherited attributes from parent elements with attribute
  1248. * names as keys
  1249. * @param {number} time
  1250. * current time (typically the total time up until the final segment)
  1251. * @param {number} duration
  1252. * duration property for the given <S />
  1253. *
  1254. * @return {number}
  1255. * R value to reach the end of the given period
  1256. */
  1257. const getLiveRValue = (attributes, time, duration) => {
  1258. const {
  1259. NOW,
  1260. clientOffset,
  1261. availabilityStartTime,
  1262. timescale = 1,
  1263. periodStart = 0,
  1264. minimumUpdatePeriod = 0
  1265. } = attributes;
  1266. const now = (NOW + clientOffset) / 1000;
  1267. const periodStartWC = availabilityStartTime + periodStart;
  1268. const periodEndWC = now + minimumUpdatePeriod;
  1269. const periodDuration = periodEndWC - periodStartWC;
  1270. return Math.ceil((periodDuration * timescale - time) / duration);
  1271. };
  1272. /**
  1273. * Uses information provided by SegmentTemplate.SegmentTimeline to determine segment
  1274. * timing and duration
  1275. *
  1276. * @param {Object} attributes
  1277. * Object containing all inherited attributes from parent elements with attribute
  1278. * names as keys
  1279. * @param {Object[]} segmentTimeline
  1280. * List of objects representing the attributes of each S element contained within
  1281. *
  1282. * @return {{number: number, duration: number, time: number, timeline: number}[]}
  1283. * List of Objects with segment timing and duration info
  1284. */
  1285. const parseByTimeline = (attributes, segmentTimeline) => {
  1286. const {
  1287. type,
  1288. minimumUpdatePeriod = 0,
  1289. media = '',
  1290. sourceDuration,
  1291. timescale = 1,
  1292. startNumber = 1,
  1293. periodStart: timeline
  1294. } = attributes;
  1295. const segments = [];
  1296. let time = -1;
  1297. for (let sIndex = 0; sIndex < segmentTimeline.length; sIndex++) {
  1298. const S = segmentTimeline[sIndex];
  1299. const duration = S.d;
  1300. const repeat = S.r || 0;
  1301. const segmentTime = S.t || 0;
  1302. if (time < 0) {
  1303. // first segment
  1304. time = segmentTime;
  1305. }
  1306. if (segmentTime && segmentTime > time) {
  1307. // discontinuity
  1308. // TODO: How to handle this type of discontinuity
  1309. // timeline++ here would treat it like HLS discontuity and content would
  1310. // get appended without gap
  1311. // E.G.
  1312. // <S t="0" d="1" />
  1313. // <S d="1" />
  1314. // <S d="1" />
  1315. // <S t="5" d="1" />
  1316. // would have $Time$ values of [0, 1, 2, 5]
  1317. // should this be appened at time positions [0, 1, 2, 3],(#EXT-X-DISCONTINUITY)
  1318. // or [0, 1, 2, gap, gap, 5]? (#EXT-X-GAP)
  1319. // does the value of sourceDuration consider this when calculating arbitrary
  1320. // negative @r repeat value?
  1321. // E.G. Same elements as above with this added at the end
  1322. // <S d="1" r="-1" />
  1323. // with a sourceDuration of 10
  1324. // Would the 2 gaps be included in the time duration calculations resulting in
  1325. // 8 segments with $Time$ values of [0, 1, 2, 5, 6, 7, 8, 9] or 10 segments
  1326. // with $Time$ values of [0, 1, 2, 5, 6, 7, 8, 9, 10, 11] ?
  1327. time = segmentTime;
  1328. }
  1329. let count;
  1330. if (repeat < 0) {
  1331. const nextS = sIndex + 1;
  1332. if (nextS === segmentTimeline.length) {
  1333. // last segment
  1334. if (type === 'dynamic' && minimumUpdatePeriod > 0 && media.indexOf('$Number$') > 0) {
  1335. count = getLiveRValue(attributes, time, duration);
  1336. } else {
  1337. // TODO: This may be incorrect depending on conclusion of TODO above
  1338. count = (sourceDuration * timescale - time) / duration;
  1339. }
  1340. } else {
  1341. count = (segmentTimeline[nextS].t - time) / duration;
  1342. }
  1343. } else {
  1344. count = repeat + 1;
  1345. }
  1346. const end = startNumber + segments.length + count;
  1347. let number = startNumber + segments.length;
  1348. while (number < end) {
  1349. segments.push({
  1350. number,
  1351. duration: duration / timescale,
  1352. time,
  1353. timeline
  1354. });
  1355. time += duration;
  1356. number++;
  1357. }
  1358. }
  1359. return segments;
  1360. };
  1361. const identifierPattern = /\$([A-z]*)(?:(%0)([0-9]+)d)?\$/g;
  1362. /**
  1363. * Replaces template identifiers with corresponding values. To be used as the callback
  1364. * for String.prototype.replace
  1365. *
  1366. * @name replaceCallback
  1367. * @function
  1368. * @param {string} match
  1369. * Entire match of identifier
  1370. * @param {string} identifier
  1371. * Name of matched identifier
  1372. * @param {string} format
  1373. * Format tag string. Its presence indicates that padding is expected
  1374. * @param {string} width
  1375. * Desired length of the replaced value. Values less than this width shall be left
  1376. * zero padded
  1377. * @return {string}
  1378. * Replacement for the matched identifier
  1379. */
  1380. /**
  1381. * Returns a function to be used as a callback for String.prototype.replace to replace
  1382. * template identifiers
  1383. *
  1384. * @param {Obect} values
  1385. * Object containing values that shall be used to replace known identifiers
  1386. * @param {number} values.RepresentationID
  1387. * Value of the Representation@id attribute
  1388. * @param {number} values.Number
  1389. * Number of the corresponding segment
  1390. * @param {number} values.Bandwidth
  1391. * Value of the Representation@bandwidth attribute.
  1392. * @param {number} values.Time
  1393. * Timestamp value of the corresponding segment
  1394. * @return {replaceCallback}
  1395. * Callback to be used with String.prototype.replace to replace identifiers
  1396. */
  1397. const identifierReplacement = values => (match, identifier, format, width) => {
  1398. if (match === '$$') {
  1399. // escape sequence
  1400. return '$';
  1401. }
  1402. if (typeof values[identifier] === 'undefined') {
  1403. return match;
  1404. }
  1405. const value = '' + values[identifier];
  1406. if (identifier === 'RepresentationID') {
  1407. // Format tag shall not be present with RepresentationID
  1408. return value;
  1409. }
  1410. if (!format) {
  1411. width = 1;
  1412. } else {
  1413. width = parseInt(width, 10);
  1414. }
  1415. if (value.length >= width) {
  1416. return value;
  1417. }
  1418. return `${new Array(width - value.length + 1).join('0')}${value}`;
  1419. };
  1420. /**
  1421. * Constructs a segment url from a template string
  1422. *
  1423. * @param {string} url
  1424. * Template string to construct url from
  1425. * @param {Obect} values
  1426. * Object containing values that shall be used to replace known identifiers
  1427. * @param {number} values.RepresentationID
  1428. * Value of the Representation@id attribute
  1429. * @param {number} values.Number
  1430. * Number of the corresponding segment
  1431. * @param {number} values.Bandwidth
  1432. * Value of the Representation@bandwidth attribute.
  1433. * @param {number} values.Time
  1434. * Timestamp value of the corresponding segment
  1435. * @return {string}
  1436. * Segment url with identifiers replaced
  1437. */
  1438. const constructTemplateUrl = (url, values) => url.replace(identifierPattern, identifierReplacement(values));
  1439. /**
  1440. * Generates a list of objects containing timing and duration information about each
  1441. * segment needed to generate segment uris and the complete segment object
  1442. *
  1443. * @param {Object} attributes
  1444. * Object containing all inherited attributes from parent elements with attribute
  1445. * names as keys
  1446. * @param {Object[]|undefined} segmentTimeline
  1447. * List of objects representing the attributes of each S element contained within
  1448. * the SegmentTimeline element
  1449. * @return {{number: number, duration: number, time: number, timeline: number}[]}
  1450. * List of Objects with segment timing and duration info
  1451. */
  1452. const parseTemplateInfo = (attributes, segmentTimeline) => {
  1453. if (!attributes.duration && !segmentTimeline) {
  1454. // if neither @duration or SegmentTimeline are present, then there shall be exactly
  1455. // one media segment
  1456. return [{
  1457. number: attributes.startNumber || 1,
  1458. duration: attributes.sourceDuration,
  1459. time: 0,
  1460. timeline: attributes.periodStart
  1461. }];
  1462. }
  1463. if (attributes.duration) {
  1464. return parseByDuration(attributes);
  1465. }
  1466. return parseByTimeline(attributes, segmentTimeline);
  1467. };
  1468. /**
  1469. * Generates a list of segments using information provided by the SegmentTemplate element
  1470. *
  1471. * @param {Object} attributes
  1472. * Object containing all inherited attributes from parent elements with attribute
  1473. * names as keys
  1474. * @param {Object[]|undefined} segmentTimeline
  1475. * List of objects representing the attributes of each S element contained within
  1476. * the SegmentTimeline element
  1477. * @return {Object[]}
  1478. * List of segment objects
  1479. */
  1480. const segmentsFromTemplate = (attributes, segmentTimeline) => {
  1481. const templateValues = {
  1482. RepresentationID: attributes.id,
  1483. Bandwidth: attributes.bandwidth || 0
  1484. };
  1485. const {
  1486. initialization = {
  1487. sourceURL: '',
  1488. range: ''
  1489. }
  1490. } = attributes;
  1491. const mapSegment = urlTypeToSegment({
  1492. baseUrl: attributes.baseUrl,
  1493. source: constructTemplateUrl(initialization.sourceURL, templateValues),
  1494. range: initialization.range
  1495. });
  1496. const segments = parseTemplateInfo(attributes, segmentTimeline);
  1497. return segments.map(segment => {
  1498. templateValues.Number = segment.number;
  1499. templateValues.Time = segment.time;
  1500. const uri = constructTemplateUrl(attributes.media || '', templateValues); // See DASH spec section 5.3.9.2.2
  1501. // - if timescale isn't present on any level, default to 1.
  1502. const timescale = attributes.timescale || 1; // - if presentationTimeOffset isn't present on any level, default to 0
  1503. const presentationTimeOffset = attributes.presentationTimeOffset || 0;
  1504. const presentationTime = // Even if the @t attribute is not specified for the segment, segment.time is
  1505. // calculated in mpd-parser prior to this, so it's assumed to be available.
  1506. attributes.periodStart + (segment.time - presentationTimeOffset) / timescale;
  1507. const map = {
  1508. uri,
  1509. timeline: segment.timeline,
  1510. duration: segment.duration,
  1511. resolvedUri: resolveUrl(attributes.baseUrl || '', uri),
  1512. map: mapSegment,
  1513. number: segment.number,
  1514. presentationTime
  1515. };
  1516. return map;
  1517. });
  1518. };
  1519. /**
  1520. * Converts a <SegmentUrl> (of type URLType from the DASH spec 5.3.9.2 Table 14)
  1521. * to an object that matches the output of a segment in videojs/mpd-parser
  1522. *
  1523. * @param {Object} attributes
  1524. * Object containing all inherited attributes from parent elements with attribute
  1525. * names as keys
  1526. * @param {Object} segmentUrl
  1527. * <SegmentURL> node to translate into a segment object
  1528. * @return {Object} translated segment object
  1529. */
  1530. const SegmentURLToSegmentObject = (attributes, segmentUrl) => {
  1531. const {
  1532. baseUrl,
  1533. initialization = {}
  1534. } = attributes;
  1535. const initSegment = urlTypeToSegment({
  1536. baseUrl,
  1537. source: initialization.sourceURL,
  1538. range: initialization.range
  1539. });
  1540. const segment = urlTypeToSegment({
  1541. baseUrl,
  1542. source: segmentUrl.media,
  1543. range: segmentUrl.mediaRange
  1544. });
  1545. segment.map = initSegment;
  1546. return segment;
  1547. };
  1548. /**
  1549. * Generates a list of segments using information provided by the SegmentList element
  1550. * SegmentList (DASH SPEC Section 5.3.9.3.2) contains a set of <SegmentURL> nodes. Each
  1551. * node should be translated into segment.
  1552. *
  1553. * @param {Object} attributes
  1554. * Object containing all inherited attributes from parent elements with attribute
  1555. * names as keys
  1556. * @param {Object[]|undefined} segmentTimeline
  1557. * List of objects representing the attributes of each S element contained within
  1558. * the SegmentTimeline element
  1559. * @return {Object.<Array>} list of segments
  1560. */
  1561. const segmentsFromList = (attributes, segmentTimeline) => {
  1562. const {
  1563. duration,
  1564. segmentUrls = [],
  1565. periodStart
  1566. } = attributes; // Per spec (5.3.9.2.1) no way to determine segment duration OR
  1567. // if both SegmentTimeline and @duration are defined, it is outside of spec.
  1568. if (!duration && !segmentTimeline || duration && segmentTimeline) {
  1569. throw new Error(errors.SEGMENT_TIME_UNSPECIFIED);
  1570. }
  1571. const segmentUrlMap = segmentUrls.map(segmentUrlObject => SegmentURLToSegmentObject(attributes, segmentUrlObject));
  1572. let segmentTimeInfo;
  1573. if (duration) {
  1574. segmentTimeInfo = parseByDuration(attributes);
  1575. }
  1576. if (segmentTimeline) {
  1577. segmentTimeInfo = parseByTimeline(attributes, segmentTimeline);
  1578. }
  1579. const segments = segmentTimeInfo.map((segmentTime, index) => {
  1580. if (segmentUrlMap[index]) {
  1581. const segment = segmentUrlMap[index]; // See DASH spec section 5.3.9.2.2
  1582. // - if timescale isn't present on any level, default to 1.
  1583. const timescale = attributes.timescale || 1; // - if presentationTimeOffset isn't present on any level, default to 0
  1584. const presentationTimeOffset = attributes.presentationTimeOffset || 0;
  1585. segment.timeline = segmentTime.timeline;
  1586. segment.duration = segmentTime.duration;
  1587. segment.number = segmentTime.number;
  1588. segment.presentationTime = periodStart + (segmentTime.time - presentationTimeOffset) / timescale;
  1589. return segment;
  1590. } // Since we're mapping we should get rid of any blank segments (in case
  1591. // the given SegmentTimeline is handling for more elements than we have
  1592. // SegmentURLs for).
  1593. }).filter(segment => segment);
  1594. return segments;
  1595. };
  1596. const generateSegments = ({
  1597. attributes,
  1598. segmentInfo
  1599. }) => {
  1600. let segmentAttributes;
  1601. let segmentsFn;
  1602. if (segmentInfo.template) {
  1603. segmentsFn = segmentsFromTemplate;
  1604. segmentAttributes = merge(attributes, segmentInfo.template);
  1605. } else if (segmentInfo.base) {
  1606. segmentsFn = segmentsFromBase;
  1607. segmentAttributes = merge(attributes, segmentInfo.base);
  1608. } else if (segmentInfo.list) {
  1609. segmentsFn = segmentsFromList;
  1610. segmentAttributes = merge(attributes, segmentInfo.list);
  1611. }
  1612. const segmentsInfo = {
  1613. attributes
  1614. };
  1615. if (!segmentsFn) {
  1616. return segmentsInfo;
  1617. }
  1618. const segments = segmentsFn(segmentAttributes, segmentInfo.segmentTimeline); // The @duration attribute will be used to determin the playlist's targetDuration which
  1619. // must be in seconds. Since we've generated the segment list, we no longer need
  1620. // @duration to be in @timescale units, so we can convert it here.
  1621. if (segmentAttributes.duration) {
  1622. const {
  1623. duration,
  1624. timescale = 1
  1625. } = segmentAttributes;
  1626. segmentAttributes.duration = duration / timescale;
  1627. } else if (segments.length) {
  1628. // if there is no @duration attribute, use the largest segment duration as
  1629. // as target duration
  1630. segmentAttributes.duration = segments.reduce((max, segment) => {
  1631. return Math.max(max, Math.ceil(segment.duration));
  1632. }, 0);
  1633. } else {
  1634. segmentAttributes.duration = 0;
  1635. }
  1636. segmentsInfo.attributes = segmentAttributes;
  1637. segmentsInfo.segments = segments; // This is a sidx box without actual segment information
  1638. if (segmentInfo.base && segmentAttributes.indexRange) {
  1639. segmentsInfo.sidx = segments[0];
  1640. segmentsInfo.segments = [];
  1641. }
  1642. return segmentsInfo;
  1643. };
  1644. const toPlaylists = representations => representations.map(generateSegments);
  1645. const findChildren = (element, name) => from(element.childNodes).filter(({
  1646. tagName
  1647. }) => tagName === name);
  1648. const getContent = element => element.textContent.trim();
  1649. /**
  1650. * Converts the provided string that may contain a division operation to a number.
  1651. *
  1652. * @param {string} value - the provided string value
  1653. *
  1654. * @return {number} the parsed string value
  1655. */
  1656. const parseDivisionValue = value => {
  1657. return parseFloat(value.split('/').reduce((prev, current) => prev / current));
  1658. };
  1659. const parseDuration = str => {
  1660. const SECONDS_IN_YEAR = 365 * 24 * 60 * 60;
  1661. const SECONDS_IN_MONTH = 30 * 24 * 60 * 60;
  1662. const SECONDS_IN_DAY = 24 * 60 * 60;
  1663. const SECONDS_IN_HOUR = 60 * 60;
  1664. const SECONDS_IN_MIN = 60; // P10Y10M10DT10H10M10.1S
  1665. const durationRegex = /P(?:(\d*)Y)?(?:(\d*)M)?(?:(\d*)D)?(?:T(?:(\d*)H)?(?:(\d*)M)?(?:([\d.]*)S)?)?/;
  1666. const match = durationRegex.exec(str);
  1667. if (!match) {
  1668. return 0;
  1669. }
  1670. const [year, month, day, hour, minute, second] = match.slice(1);
  1671. 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);
  1672. };
  1673. const parseDate = str => {
  1674. // Date format without timezone according to ISO 8601
  1675. // YYY-MM-DDThh:mm:ss.ssssss
  1676. 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
  1677. // expressed by ending with 'Z'
  1678. if (dateRegex.test(str)) {
  1679. str += 'Z';
  1680. }
  1681. return Date.parse(str);
  1682. };
  1683. const parsers = {
  1684. /**
  1685. * Specifies the duration of the entire Media Presentation. Format is a duration string
  1686. * as specified in ISO 8601
  1687. *
  1688. * @param {string} value
  1689. * value of attribute as a string
  1690. * @return {number}
  1691. * The duration in seconds
  1692. */
  1693. mediaPresentationDuration(value) {
  1694. return parseDuration(value);
  1695. },
  1696. /**
  1697. * Specifies the Segment availability start time for all Segments referred to in this
  1698. * MPD. For a dynamic manifest, it specifies the anchor for the earliest availability
  1699. * time. Format is a date string as specified in ISO 8601
  1700. *
  1701. * @param {string} value
  1702. * value of attribute as a string
  1703. * @return {number}
  1704. * The date as seconds from unix epoch
  1705. */
  1706. availabilityStartTime(value) {
  1707. return parseDate(value) / 1000;
  1708. },
  1709. /**
  1710. * Specifies the smallest period between potential changes to the MPD. Format is a
  1711. * duration string as specified in ISO 8601
  1712. *
  1713. * @param {string} value
  1714. * value of attribute as a string
  1715. * @return {number}
  1716. * The duration in seconds
  1717. */
  1718. minimumUpdatePeriod(value) {
  1719. return parseDuration(value);
  1720. },
  1721. /**
  1722. * Specifies the suggested presentation delay. Format is a
  1723. * duration string as specified in ISO 8601
  1724. *
  1725. * @param {string} value
  1726. * value of attribute as a string
  1727. * @return {number}
  1728. * The duration in seconds
  1729. */
  1730. suggestedPresentationDelay(value) {
  1731. return parseDuration(value);
  1732. },
  1733. /**
  1734. * specifices the type of mpd. Can be either "static" or "dynamic"
  1735. *
  1736. * @param {string} value
  1737. * value of attribute as a string
  1738. *
  1739. * @return {string}
  1740. * The type as a string
  1741. */
  1742. type(value) {
  1743. return value;
  1744. },
  1745. /**
  1746. * Specifies the duration of the smallest time shifting buffer for any Representation
  1747. * in the MPD. Format is a duration string as specified in ISO 8601
  1748. *
  1749. * @param {string} value
  1750. * value of attribute as a string
  1751. * @return {number}
  1752. * The duration in seconds
  1753. */
  1754. timeShiftBufferDepth(value) {
  1755. return parseDuration(value);
  1756. },
  1757. /**
  1758. * Specifies the PeriodStart time of the Period relative to the availabilityStarttime.
  1759. * Format is a duration string as specified in ISO 8601
  1760. *
  1761. * @param {string} value
  1762. * value of attribute as a string
  1763. * @return {number}
  1764. * The duration in seconds
  1765. */
  1766. start(value) {
  1767. return parseDuration(value);
  1768. },
  1769. /**
  1770. * Specifies the width of the visual presentation
  1771. *
  1772. * @param {string} value
  1773. * value of attribute as a string
  1774. * @return {number}
  1775. * The parsed width
  1776. */
  1777. width(value) {
  1778. return parseInt(value, 10);
  1779. },
  1780. /**
  1781. * Specifies the height of the visual presentation
  1782. *
  1783. * @param {string} value
  1784. * value of attribute as a string
  1785. * @return {number}
  1786. * The parsed height
  1787. */
  1788. height(value) {
  1789. return parseInt(value, 10);
  1790. },
  1791. /**
  1792. * Specifies the bitrate of the representation
  1793. *
  1794. * @param {string} value
  1795. * value of attribute as a string
  1796. * @return {number}
  1797. * The parsed bandwidth
  1798. */
  1799. bandwidth(value) {
  1800. return parseInt(value, 10);
  1801. },
  1802. /**
  1803. * Specifies the frame rate of the representation
  1804. *
  1805. * @param {string} value
  1806. * value of attribute as a string
  1807. * @return {number}
  1808. * The parsed frame rate
  1809. */
  1810. frameRate(value) {
  1811. return parseDivisionValue(value);
  1812. },
  1813. /**
  1814. * Specifies the number of the first Media Segment in this Representation in the Period
  1815. *
  1816. * @param {string} value
  1817. * value of attribute as a string
  1818. * @return {number}
  1819. * The parsed number
  1820. */
  1821. startNumber(value) {
  1822. return parseInt(value, 10);
  1823. },
  1824. /**
  1825. * Specifies the timescale in units per seconds
  1826. *
  1827. * @param {string} value
  1828. * value of attribute as a string
  1829. * @return {number}
  1830. * The parsed timescale
  1831. */
  1832. timescale(value) {
  1833. return parseInt(value, 10);
  1834. },
  1835. /**
  1836. * Specifies the presentationTimeOffset.
  1837. *
  1838. * @param {string} value
  1839. * value of the attribute as a string
  1840. *
  1841. * @return {number}
  1842. * The parsed presentationTimeOffset
  1843. */
  1844. presentationTimeOffset(value) {
  1845. return parseInt(value, 10);
  1846. },
  1847. /**
  1848. * Specifies the constant approximate Segment duration
  1849. * NOTE: The <Period> element also contains an @duration attribute. This duration
  1850. * specifies the duration of the Period. This attribute is currently not
  1851. * supported by the rest of the parser, however we still check for it to prevent
  1852. * errors.
  1853. *
  1854. * @param {string} value
  1855. * value of attribute as a string
  1856. * @return {number}
  1857. * The parsed duration
  1858. */
  1859. duration(value) {
  1860. const parsedValue = parseInt(value, 10);
  1861. if (isNaN(parsedValue)) {
  1862. return parseDuration(value);
  1863. }
  1864. return parsedValue;
  1865. },
  1866. /**
  1867. * Specifies the Segment duration, in units of the value of the @timescale.
  1868. *
  1869. * @param {string} value
  1870. * value of attribute as a string
  1871. * @return {number}
  1872. * The parsed duration
  1873. */
  1874. d(value) {
  1875. return parseInt(value, 10);
  1876. },
  1877. /**
  1878. * Specifies the MPD start time, in @timescale units, the first Segment in the series
  1879. * starts relative to the beginning of the Period
  1880. *
  1881. * @param {string} value
  1882. * value of attribute as a string
  1883. * @return {number}
  1884. * The parsed time
  1885. */
  1886. t(value) {
  1887. return parseInt(value, 10);
  1888. },
  1889. /**
  1890. * Specifies the repeat count of the number of following contiguous Segments with the
  1891. * same duration expressed by the value of @d
  1892. *
  1893. * @param {string} value
  1894. * value of attribute as a string
  1895. * @return {number}
  1896. * The parsed number
  1897. */
  1898. r(value) {
  1899. return parseInt(value, 10);
  1900. },
  1901. /**
  1902. * Specifies the presentationTime.
  1903. *
  1904. * @param {string} value
  1905. * value of the attribute as a string
  1906. *
  1907. * @return {number}
  1908. * The parsed presentationTime
  1909. */
  1910. presentationTime(value) {
  1911. return parseInt(value, 10);
  1912. },
  1913. /**
  1914. * Default parser for all other attributes. Acts as a no-op and just returns the value
  1915. * as a string
  1916. *
  1917. * @param {string} value
  1918. * value of attribute as a string
  1919. * @return {string}
  1920. * Unparsed value
  1921. */
  1922. DEFAULT(value) {
  1923. return value;
  1924. }
  1925. };
  1926. /**
  1927. * Gets all the attributes and values of the provided node, parses attributes with known
  1928. * types, and returns an object with attribute names mapped to values.
  1929. *
  1930. * @param {Node} el
  1931. * The node to parse attributes from
  1932. * @return {Object}
  1933. * Object with all attributes of el parsed
  1934. */
  1935. const parseAttributes = el => {
  1936. if (!(el && el.attributes)) {
  1937. return {};
  1938. }
  1939. return from(el.attributes).reduce((a, e) => {
  1940. const parseFn = parsers[e.name] || parsers.DEFAULT;
  1941. a[e.name] = parseFn(e.value);
  1942. return a;
  1943. }, {});
  1944. };
  1945. var atob = function atob(s) {
  1946. return window.atob ? window.atob(s) : Buffer.from(s, 'base64').toString('binary');
  1947. };
  1948. function decodeB64ToUint8Array(b64Text) {
  1949. var decodedString = atob(b64Text);
  1950. var array = new Uint8Array(decodedString.length);
  1951. for (var i = 0; i < decodedString.length; i++) {
  1952. array[i] = decodedString.charCodeAt(i);
  1953. }
  1954. return array;
  1955. }
  1956. const keySystemsMap = {
  1957. 'urn:uuid:1077efec-c0b2-4d02-ace3-3c1e52e2fb4b': 'org.w3.clearkey',
  1958. 'urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed': 'com.widevine.alpha',
  1959. 'urn:uuid:9a04f079-9840-4286-ab92-e65be0885f95': 'com.microsoft.playready',
  1960. 'urn:uuid:f239e769-efa3-4850-9c16-a903c6932efb': 'com.adobe.primetime'
  1961. };
  1962. /**
  1963. * Builds a list of urls that is the product of the reference urls and BaseURL values
  1964. *
  1965. * @param {string[]} referenceUrls
  1966. * List of reference urls to resolve to
  1967. * @param {Node[]} baseUrlElements
  1968. * List of BaseURL nodes from the mpd
  1969. * @return {string[]}
  1970. * List of resolved urls
  1971. */
  1972. const buildBaseUrls = (referenceUrls, baseUrlElements) => {
  1973. if (!baseUrlElements.length) {
  1974. return referenceUrls;
  1975. }
  1976. return flatten(referenceUrls.map(function (reference) {
  1977. return baseUrlElements.map(function (baseUrlElement) {
  1978. return resolveUrl(reference, getContent(baseUrlElement));
  1979. });
  1980. }));
  1981. };
  1982. /**
  1983. * Contains all Segment information for its containing AdaptationSet
  1984. *
  1985. * @typedef {Object} SegmentInformation
  1986. * @property {Object|undefined} template
  1987. * Contains the attributes for the SegmentTemplate node
  1988. * @property {Object[]|undefined} segmentTimeline
  1989. * Contains a list of atrributes for each S node within the SegmentTimeline node
  1990. * @property {Object|undefined} list
  1991. * Contains the attributes for the SegmentList node
  1992. * @property {Object|undefined} base
  1993. * Contains the attributes for the SegmentBase node
  1994. */
  1995. /**
  1996. * Returns all available Segment information contained within the AdaptationSet node
  1997. *
  1998. * @param {Node} adaptationSet
  1999. * The AdaptationSet node to get Segment information from
  2000. * @return {SegmentInformation}
  2001. * The Segment information contained within the provided AdaptationSet
  2002. */
  2003. const getSegmentInformation = adaptationSet => {
  2004. const segmentTemplate = findChildren(adaptationSet, 'SegmentTemplate')[0];
  2005. const segmentList = findChildren(adaptationSet, 'SegmentList')[0];
  2006. const segmentUrls = segmentList && findChildren(segmentList, 'SegmentURL').map(s => merge({
  2007. tag: 'SegmentURL'
  2008. }, parseAttributes(s)));
  2009. const segmentBase = findChildren(adaptationSet, 'SegmentBase')[0];
  2010. const segmentTimelineParentNode = segmentList || segmentTemplate;
  2011. const segmentTimeline = segmentTimelineParentNode && findChildren(segmentTimelineParentNode, 'SegmentTimeline')[0];
  2012. const segmentInitializationParentNode = segmentList || segmentBase || segmentTemplate;
  2013. const segmentInitialization = segmentInitializationParentNode && findChildren(segmentInitializationParentNode, 'Initialization')[0]; // SegmentTemplate is handled slightly differently, since it can have both
  2014. // @initialization and an <Initialization> node. @initialization can be templated,
  2015. // while the node can have a url and range specified. If the <SegmentTemplate> has
  2016. // both @initialization and an <Initialization> subelement we opt to override with
  2017. // the node, as this interaction is not defined in the spec.
  2018. const template = segmentTemplate && parseAttributes(segmentTemplate);
  2019. if (template && segmentInitialization) {
  2020. template.initialization = segmentInitialization && parseAttributes(segmentInitialization);
  2021. } else if (template && template.initialization) {
  2022. // If it is @initialization we convert it to an object since this is the format that
  2023. // later functions will rely on for the initialization segment. This is only valid
  2024. // for <SegmentTemplate>
  2025. template.initialization = {
  2026. sourceURL: template.initialization
  2027. };
  2028. }
  2029. const segmentInfo = {
  2030. template,
  2031. segmentTimeline: segmentTimeline && findChildren(segmentTimeline, 'S').map(s => parseAttributes(s)),
  2032. list: segmentList && merge(parseAttributes(segmentList), {
  2033. segmentUrls,
  2034. initialization: parseAttributes(segmentInitialization)
  2035. }),
  2036. base: segmentBase && merge(parseAttributes(segmentBase), {
  2037. initialization: parseAttributes(segmentInitialization)
  2038. })
  2039. };
  2040. Object.keys(segmentInfo).forEach(key => {
  2041. if (!segmentInfo[key]) {
  2042. delete segmentInfo[key];
  2043. }
  2044. });
  2045. return segmentInfo;
  2046. };
  2047. /**
  2048. * Contains Segment information and attributes needed to construct a Playlist object
  2049. * from a Representation
  2050. *
  2051. * @typedef {Object} RepresentationInformation
  2052. * @property {SegmentInformation} segmentInfo
  2053. * Segment information for this Representation
  2054. * @property {Object} attributes
  2055. * Inherited attributes for this Representation
  2056. */
  2057. /**
  2058. * Maps a Representation node to an object containing Segment information and attributes
  2059. *
  2060. * @name inheritBaseUrlsCallback
  2061. * @function
  2062. * @param {Node} representation
  2063. * Representation node from the mpd
  2064. * @return {RepresentationInformation}
  2065. * Representation information needed to construct a Playlist object
  2066. */
  2067. /**
  2068. * Returns a callback for Array.prototype.map for mapping Representation nodes to
  2069. * Segment information and attributes using inherited BaseURL nodes.
  2070. *
  2071. * @param {Object} adaptationSetAttributes
  2072. * Contains attributes inherited by the AdaptationSet
  2073. * @param {string[]} adaptationSetBaseUrls
  2074. * Contains list of resolved base urls inherited by the AdaptationSet
  2075. * @param {SegmentInformation} adaptationSetSegmentInfo
  2076. * Contains Segment information for the AdaptationSet
  2077. * @return {inheritBaseUrlsCallback}
  2078. * Callback map function
  2079. */
  2080. const inheritBaseUrls = (adaptationSetAttributes, adaptationSetBaseUrls, adaptationSetSegmentInfo) => representation => {
  2081. const repBaseUrlElements = findChildren(representation, 'BaseURL');
  2082. const repBaseUrls = buildBaseUrls(adaptationSetBaseUrls, repBaseUrlElements);
  2083. const attributes = merge(adaptationSetAttributes, parseAttributes(representation));
  2084. const representationSegmentInfo = getSegmentInformation(representation);
  2085. return repBaseUrls.map(baseUrl => {
  2086. return {
  2087. segmentInfo: merge(adaptationSetSegmentInfo, representationSegmentInfo),
  2088. attributes: merge(attributes, {
  2089. baseUrl
  2090. })
  2091. };
  2092. });
  2093. };
  2094. /**
  2095. * Tranforms a series of content protection nodes to
  2096. * an object containing pssh data by key system
  2097. *
  2098. * @param {Node[]} contentProtectionNodes
  2099. * Content protection nodes
  2100. * @return {Object}
  2101. * Object containing pssh data by key system
  2102. */
  2103. const generateKeySystemInformation = contentProtectionNodes => {
  2104. return contentProtectionNodes.reduce((acc, node) => {
  2105. 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
  2106. // as a lowercase string it also mentions it should be treated as case-insensitive on input. Since the key system
  2107. // UUIDs in the keySystemsMap are hardcoded as lowercase in the codebase there isn't any reason not to do
  2108. // .toLowerCase() on the input UUID string from the manifest (at least I could not think of one).
  2109. if (attributes.schemeIdUri) {
  2110. attributes.schemeIdUri = attributes.schemeIdUri.toLowerCase();
  2111. }
  2112. const keySystem = keySystemsMap[attributes.schemeIdUri];
  2113. if (keySystem) {
  2114. acc[keySystem] = {
  2115. attributes
  2116. };
  2117. const psshNode = findChildren(node, 'cenc:pssh')[0];
  2118. if (psshNode) {
  2119. const pssh = getContent(psshNode);
  2120. acc[keySystem].pssh = pssh && decodeB64ToUint8Array(pssh);
  2121. }
  2122. }
  2123. return acc;
  2124. }, {});
  2125. }; // defined in ANSI_SCTE 214-1 2016
  2126. const parseCaptionServiceMetadata = service => {
  2127. // 608 captions
  2128. if (service.schemeIdUri === 'urn:scte:dash:cc:cea-608:2015') {
  2129. const values = typeof service.value !== 'string' ? [] : service.value.split(';');
  2130. return values.map(value => {
  2131. let channel;
  2132. let language; // default language to value
  2133. language = value;
  2134. if (/^CC\d=/.test(value)) {
  2135. [channel, language] = value.split('=');
  2136. } else if (/^CC\d$/.test(value)) {
  2137. channel = value;
  2138. }
  2139. return {
  2140. channel,
  2141. language
  2142. };
  2143. });
  2144. } else if (service.schemeIdUri === 'urn:scte:dash:cc:cea-708:2015') {
  2145. const values = typeof service.value !== 'string' ? [] : service.value.split(';');
  2146. return values.map(value => {
  2147. const flags = {
  2148. // service or channel number 1-63
  2149. 'channel': undefined,
  2150. // language is a 3ALPHA per ISO 639.2/B
  2151. // field is required
  2152. 'language': undefined,
  2153. // BIT 1/0 or ?
  2154. // default value is 1, meaning 16:9 aspect ratio, 0 is 4:3, ? is unknown
  2155. 'aspectRatio': 1,
  2156. // BIT 1/0
  2157. // easy reader flag indicated the text is tailed to the needs of beginning readers
  2158. // default 0, or off
  2159. 'easyReader': 0,
  2160. // BIT 1/0
  2161. // If 3d metadata is present (CEA-708.1) then 1
  2162. // default 0
  2163. '3D': 0
  2164. };
  2165. if (/=/.test(value)) {
  2166. const [channel, opts = ''] = value.split('=');
  2167. flags.channel = channel;
  2168. flags.language = value;
  2169. opts.split(',').forEach(opt => {
  2170. const [name, val] = opt.split(':');
  2171. if (name === 'lang') {
  2172. flags.language = val; // er for easyReadery
  2173. } else if (name === 'er') {
  2174. flags.easyReader = Number(val); // war for wide aspect ratio
  2175. } else if (name === 'war') {
  2176. flags.aspectRatio = Number(val);
  2177. } else if (name === '3D') {
  2178. flags['3D'] = Number(val);
  2179. }
  2180. });
  2181. } else {
  2182. flags.language = value;
  2183. }
  2184. if (flags.channel) {
  2185. flags.channel = 'SERVICE' + flags.channel;
  2186. }
  2187. return flags;
  2188. });
  2189. }
  2190. };
  2191. /**
  2192. * A map callback that will parse all event stream data for a collection of periods
  2193. * DASH ISO_IEC_23009 5.10.2.2
  2194. * https://dashif-documents.azurewebsites.net/Events/master/event.html#mpd-event-timing
  2195. *
  2196. * @param {PeriodInformation} period object containing necessary period information
  2197. * @return a collection of parsed eventstream event objects
  2198. */
  2199. const toEventStream = period => {
  2200. // get and flatten all EventStreams tags and parse attributes and children
  2201. return flatten(findChildren(period.node, 'EventStream').map(eventStream => {
  2202. const eventStreamAttributes = parseAttributes(eventStream);
  2203. const schemeIdUri = eventStreamAttributes.schemeIdUri; // find all Events per EventStream tag and map to return objects
  2204. return findChildren(eventStream, 'Event').map(event => {
  2205. const eventAttributes = parseAttributes(event);
  2206. const presentationTime = eventAttributes.presentationTime || 0;
  2207. const timescale = eventStreamAttributes.timescale || 1;
  2208. const duration = eventAttributes.duration || 0;
  2209. const start = presentationTime / timescale + period.attributes.start;
  2210. return {
  2211. schemeIdUri,
  2212. value: eventStreamAttributes.value,
  2213. id: eventAttributes.id,
  2214. start,
  2215. end: start + duration / timescale,
  2216. messageData: getContent(event) || eventAttributes.messageData,
  2217. contentEncoding: eventStreamAttributes.contentEncoding,
  2218. presentationTimeOffset: eventStreamAttributes.presentationTimeOffset || 0
  2219. };
  2220. });
  2221. }));
  2222. };
  2223. /**
  2224. * Maps an AdaptationSet node to a list of Representation information objects
  2225. *
  2226. * @name toRepresentationsCallback
  2227. * @function
  2228. * @param {Node} adaptationSet
  2229. * AdaptationSet node from the mpd
  2230. * @return {RepresentationInformation[]}
  2231. * List of objects containing Representaion information
  2232. */
  2233. /**
  2234. * Returns a callback for Array.prototype.map for mapping AdaptationSet nodes to a list of
  2235. * Representation information objects
  2236. *
  2237. * @param {Object} periodAttributes
  2238. * Contains attributes inherited by the Period
  2239. * @param {string[]} periodBaseUrls
  2240. * Contains list of resolved base urls inherited by the Period
  2241. * @param {string[]} periodSegmentInfo
  2242. * Contains Segment Information at the period level
  2243. * @return {toRepresentationsCallback}
  2244. * Callback map function
  2245. */
  2246. const toRepresentations = (periodAttributes, periodBaseUrls, periodSegmentInfo) => adaptationSet => {
  2247. const adaptationSetAttributes = parseAttributes(adaptationSet);
  2248. const adaptationSetBaseUrls = buildBaseUrls(periodBaseUrls, findChildren(adaptationSet, 'BaseURL'));
  2249. const role = findChildren(adaptationSet, 'Role')[0];
  2250. const roleAttributes = {
  2251. role: parseAttributes(role)
  2252. };
  2253. let attrs = merge(periodAttributes, adaptationSetAttributes, roleAttributes);
  2254. const accessibility = findChildren(adaptationSet, 'Accessibility')[0];
  2255. const captionServices = parseCaptionServiceMetadata(parseAttributes(accessibility));
  2256. if (captionServices) {
  2257. attrs = merge(attrs, {
  2258. captionServices
  2259. });
  2260. }
  2261. const label = findChildren(adaptationSet, 'Label')[0];
  2262. if (label && label.childNodes.length) {
  2263. const labelVal = label.childNodes[0].nodeValue.trim();
  2264. attrs = merge(attrs, {
  2265. label: labelVal
  2266. });
  2267. }
  2268. const contentProtection = generateKeySystemInformation(findChildren(adaptationSet, 'ContentProtection'));
  2269. if (Object.keys(contentProtection).length) {
  2270. attrs = merge(attrs, {
  2271. contentProtection
  2272. });
  2273. }
  2274. const segmentInfo = getSegmentInformation(adaptationSet);
  2275. const representations = findChildren(adaptationSet, 'Representation');
  2276. const adaptationSetSegmentInfo = merge(periodSegmentInfo, segmentInfo);
  2277. return flatten(representations.map(inheritBaseUrls(attrs, adaptationSetBaseUrls, adaptationSetSegmentInfo)));
  2278. };
  2279. /**
  2280. * Contains all period information for mapping nodes onto adaptation sets.
  2281. *
  2282. * @typedef {Object} PeriodInformation
  2283. * @property {Node} period.node
  2284. * Period node from the mpd
  2285. * @property {Object} period.attributes
  2286. * Parsed period attributes from node plus any added
  2287. */
  2288. /**
  2289. * Maps a PeriodInformation object to a list of Representation information objects for all
  2290. * AdaptationSet nodes contained within the Period.
  2291. *
  2292. * @name toAdaptationSetsCallback
  2293. * @function
  2294. * @param {PeriodInformation} period
  2295. * Period object containing necessary period information
  2296. * @param {number} periodStart
  2297. * Start time of the Period within the mpd
  2298. * @return {RepresentationInformation[]}
  2299. * List of objects containing Representaion information
  2300. */
  2301. /**
  2302. * Returns a callback for Array.prototype.map for mapping Period nodes to a list of
  2303. * Representation information objects
  2304. *
  2305. * @param {Object} mpdAttributes
  2306. * Contains attributes inherited by the mpd
  2307. * @param {string[]} mpdBaseUrls
  2308. * Contains list of resolved base urls inherited by the mpd
  2309. * @return {toAdaptationSetsCallback}
  2310. * Callback map function
  2311. */
  2312. const toAdaptationSets = (mpdAttributes, mpdBaseUrls) => (period, index) => {
  2313. const periodBaseUrls = buildBaseUrls(mpdBaseUrls, findChildren(period.node, 'BaseURL'));
  2314. const periodAttributes = merge(mpdAttributes, {
  2315. periodStart: period.attributes.start
  2316. });
  2317. if (typeof period.attributes.duration === 'number') {
  2318. periodAttributes.periodDuration = period.attributes.duration;
  2319. }
  2320. const adaptationSets = findChildren(period.node, 'AdaptationSet');
  2321. const periodSegmentInfo = getSegmentInformation(period.node);
  2322. return flatten(adaptationSets.map(toRepresentations(periodAttributes, periodBaseUrls, periodSegmentInfo)));
  2323. };
  2324. /**
  2325. * Gets Period@start property for a given period.
  2326. *
  2327. * @param {Object} options
  2328. * Options object
  2329. * @param {Object} options.attributes
  2330. * Period attributes
  2331. * @param {Object} [options.priorPeriodAttributes]
  2332. * Prior period attributes (if prior period is available)
  2333. * @param {string} options.mpdType
  2334. * The MPD@type these periods came from
  2335. * @return {number|null}
  2336. * The period start, or null if it's an early available period or error
  2337. */
  2338. const getPeriodStart = ({
  2339. attributes,
  2340. priorPeriodAttributes,
  2341. mpdType
  2342. }) => {
  2343. // Summary of period start time calculation from DASH spec section 5.3.2.1
  2344. //
  2345. // A period's start is the first period's start + time elapsed after playing all
  2346. // prior periods to this one. Periods continue one after the other in time (without
  2347. // gaps) until the end of the presentation.
  2348. //
  2349. // The value of Period@start should be:
  2350. // 1. if Period@start is present: value of Period@start
  2351. // 2. if previous period exists and it has @duration: previous Period@start +
  2352. // previous Period@duration
  2353. // 3. if this is first period and MPD@type is 'static': 0
  2354. // 4. in all other cases, consider the period an "early available period" (note: not
  2355. // currently supported)
  2356. // (1)
  2357. if (typeof attributes.start === 'number') {
  2358. return attributes.start;
  2359. } // (2)
  2360. if (priorPeriodAttributes && typeof priorPeriodAttributes.start === 'number' && typeof priorPeriodAttributes.duration === 'number') {
  2361. return priorPeriodAttributes.start + priorPeriodAttributes.duration;
  2362. } // (3)
  2363. if (!priorPeriodAttributes && mpdType === 'static') {
  2364. return 0;
  2365. } // (4)
  2366. // There is currently no logic for calculating the Period@start value if there is
  2367. // no Period@start or prior Period@start and Period@duration available. This is not made
  2368. // explicit by the DASH interop guidelines or the DASH spec, however, since there's
  2369. // nothing about any other resolution strategies, it's implied. Thus, this case should
  2370. // be considered an early available period, or error, and null should suffice for both
  2371. // of those cases.
  2372. return null;
  2373. };
  2374. /**
  2375. * Traverses the mpd xml tree to generate a list of Representation information objects
  2376. * that have inherited attributes from parent nodes
  2377. *
  2378. * @param {Node} mpd
  2379. * The root node of the mpd
  2380. * @param {Object} options
  2381. * Available options for inheritAttributes
  2382. * @param {string} options.manifestUri
  2383. * The uri source of the mpd
  2384. * @param {number} options.NOW
  2385. * Current time per DASH IOP. Default is current time in ms since epoch
  2386. * @param {number} options.clientOffset
  2387. * Client time difference from NOW (in milliseconds)
  2388. * @return {RepresentationInformation[]}
  2389. * List of objects containing Representation information
  2390. */
  2391. const inheritAttributes = (mpd, options = {}) => {
  2392. const {
  2393. manifestUri = '',
  2394. NOW = Date.now(),
  2395. clientOffset = 0
  2396. } = options;
  2397. const periodNodes = findChildren(mpd, 'Period');
  2398. if (!periodNodes.length) {
  2399. throw new Error(errors.INVALID_NUMBER_OF_PERIOD);
  2400. }
  2401. const locations = findChildren(mpd, 'Location');
  2402. const mpdAttributes = parseAttributes(mpd);
  2403. const mpdBaseUrls = buildBaseUrls([manifestUri], findChildren(mpd, 'BaseURL')); // See DASH spec section 5.3.1.2, Semantics of MPD element. Default type to 'static'.
  2404. mpdAttributes.type = mpdAttributes.type || 'static';
  2405. mpdAttributes.sourceDuration = mpdAttributes.mediaPresentationDuration || 0;
  2406. mpdAttributes.NOW = NOW;
  2407. mpdAttributes.clientOffset = clientOffset;
  2408. if (locations.length) {
  2409. mpdAttributes.locations = locations.map(getContent);
  2410. }
  2411. const periods = []; // Since toAdaptationSets acts on individual periods right now, the simplest approach to
  2412. // adding properties that require looking at prior periods is to parse attributes and add
  2413. // missing ones before toAdaptationSets is called. If more such properties are added, it
  2414. // may be better to refactor toAdaptationSets.
  2415. periodNodes.forEach((node, index) => {
  2416. const attributes = parseAttributes(node); // Use the last modified prior period, as it may contain added information necessary
  2417. // for this period.
  2418. const priorPeriod = periods[index - 1];
  2419. attributes.start = getPeriodStart({
  2420. attributes,
  2421. priorPeriodAttributes: priorPeriod ? priorPeriod.attributes : null,
  2422. mpdType: mpdAttributes.type
  2423. });
  2424. periods.push({
  2425. node,
  2426. attributes
  2427. });
  2428. });
  2429. return {
  2430. locations: mpdAttributes.locations,
  2431. representationInfo: flatten(periods.map(toAdaptationSets(mpdAttributes, mpdBaseUrls))),
  2432. eventStream: flatten(periods.map(toEventStream))
  2433. };
  2434. };
  2435. const stringToMpdXml = manifestString => {
  2436. if (manifestString === '') {
  2437. throw new Error(errors.DASH_EMPTY_MANIFEST);
  2438. }
  2439. const parser = new xmldom.DOMParser();
  2440. let xml;
  2441. let mpd;
  2442. try {
  2443. xml = parser.parseFromString(manifestString, 'application/xml');
  2444. mpd = xml && xml.documentElement.tagName === 'MPD' ? xml.documentElement : null;
  2445. } catch (e) {// ie 11 throwsw on invalid xml
  2446. }
  2447. if (!mpd || mpd && mpd.getElementsByTagName('parsererror').length > 0) {
  2448. throw new Error(errors.DASH_INVALID_XML);
  2449. }
  2450. return mpd;
  2451. };
  2452. /**
  2453. * Parses the manifest for a UTCTiming node, returning the nodes attributes if found
  2454. *
  2455. * @param {string} mpd
  2456. * XML string of the MPD manifest
  2457. * @return {Object|null}
  2458. * Attributes of UTCTiming node specified in the manifest. Null if none found
  2459. */
  2460. const parseUTCTimingScheme = mpd => {
  2461. const UTCTimingNode = findChildren(mpd, 'UTCTiming')[0];
  2462. if (!UTCTimingNode) {
  2463. return null;
  2464. }
  2465. const attributes = parseAttributes(UTCTimingNode);
  2466. switch (attributes.schemeIdUri) {
  2467. case 'urn:mpeg:dash:utc:http-head:2014':
  2468. case 'urn:mpeg:dash:utc:http-head:2012':
  2469. attributes.method = 'HEAD';
  2470. break;
  2471. case 'urn:mpeg:dash:utc:http-xsdate:2014':
  2472. case 'urn:mpeg:dash:utc:http-iso:2014':
  2473. case 'urn:mpeg:dash:utc:http-xsdate:2012':
  2474. case 'urn:mpeg:dash:utc:http-iso:2012':
  2475. attributes.method = 'GET';
  2476. break;
  2477. case 'urn:mpeg:dash:utc:direct:2014':
  2478. case 'urn:mpeg:dash:utc:direct:2012':
  2479. attributes.method = 'DIRECT';
  2480. attributes.value = Date.parse(attributes.value);
  2481. break;
  2482. case 'urn:mpeg:dash:utc:http-ntp:2014':
  2483. case 'urn:mpeg:dash:utc:ntp:2014':
  2484. case 'urn:mpeg:dash:utc:sntp:2014':
  2485. default:
  2486. throw new Error(errors.UNSUPPORTED_UTC_TIMING_SCHEME);
  2487. }
  2488. return attributes;
  2489. };
  2490. const VERSION = version;
  2491. /*
  2492. * Given a DASH manifest string and options, parses the DASH manifest into an object in the
  2493. * form outputed by m3u8-parser and accepted by videojs/http-streaming.
  2494. *
  2495. * For live DASH manifests, if `previousManifest` is provided in options, then the newly
  2496. * parsed DASH manifest will have its media sequence and discontinuity sequence values
  2497. * updated to reflect its position relative to the prior manifest.
  2498. *
  2499. * @param {string} manifestString - the DASH manifest as a string
  2500. * @param {options} [options] - any options
  2501. *
  2502. * @return {Object} the manifest object
  2503. */
  2504. const parse = (manifestString, options = {}) => {
  2505. const parsedManifestInfo = inheritAttributes(stringToMpdXml(manifestString), options);
  2506. const playlists = toPlaylists(parsedManifestInfo.representationInfo);
  2507. return toM3u8({
  2508. dashPlaylists: playlists,
  2509. locations: parsedManifestInfo.locations,
  2510. sidxMapping: options.sidxMapping,
  2511. previousManifest: options.previousManifest,
  2512. eventStream: parsedManifestInfo.eventStream
  2513. });
  2514. };
  2515. /**
  2516. * Parses the manifest for a UTCTiming node, returning the nodes attributes if found
  2517. *
  2518. * @param {string} manifestString
  2519. * XML string of the MPD manifest
  2520. * @return {Object|null}
  2521. * Attributes of UTCTiming node specified in the manifest. Null if none found
  2522. */
  2523. const parseUTCTiming = manifestString => parseUTCTimingScheme(stringToMpdXml(manifestString));
  2524. exports.VERSION = VERSION;
  2525. exports.addSidxSegmentsToPlaylist = addSidxSegmentsToPlaylist$1;
  2526. exports.generateSidxKey = generateSidxKey;
  2527. exports.inheritAttributes = inheritAttributes;
  2528. exports.parse = parse;
  2529. exports.parseUTCTiming = parseUTCTiming;
  2530. exports.stringToMpdXml = stringToMpdXml;
  2531. exports.toM3u8 = toM3u8;
  2532. exports.toPlaylists = toPlaylists;
  2533. Object.defineProperty(exports, '__esModule', { value: true });
  2534. })));