mpd-parser.js 103 KB

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