mpd-parser.cjs.js 84 KB

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