mpd-parser.es.js 88 KB

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