mpd-parser.cjs.js 89 KB

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