mpd-parser.es.js 83 KB

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