manifest.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343
  1. import videojs from 'video.js';
  2. import window from 'global/window';
  3. import { Parser as M3u8Parser } from 'm3u8-parser';
  4. import { resolveUrl } from './resolve-url';
  5. import { getLastParts, isAudioOnly } from './playlist.js';
  6. const { log } = videojs;
  7. export const createPlaylistID = (index, uri) => {
  8. return `${index}-${uri}`;
  9. };
  10. // default function for creating a group id
  11. export const groupID = (type, group, label) => {
  12. return `placeholder-uri-${type}-${group}-${label}`;
  13. };
  14. /**
  15. * Parses a given m3u8 playlist
  16. *
  17. * @param {Function} [onwarn]
  18. * a function to call when the parser triggers a warning event.
  19. * @param {Function} [oninfo]
  20. * a function to call when the parser triggers an info event.
  21. * @param {string} manifestString
  22. * The downloaded manifest string
  23. * @param {Object[]} [customTagParsers]
  24. * An array of custom tag parsers for the m3u8-parser instance
  25. * @param {Object[]} [customTagMappers]
  26. * An array of custom tag mappers for the m3u8-parser instance
  27. * @param {boolean} [llhls]
  28. * Whether to keep ll-hls features in the manifest after parsing.
  29. * @return {Object}
  30. * The manifest object
  31. */
  32. export const parseManifest = ({
  33. onwarn,
  34. oninfo,
  35. manifestString,
  36. customTagParsers = [],
  37. customTagMappers = [],
  38. llhls
  39. }) => {
  40. const parser = new M3u8Parser();
  41. if (onwarn) {
  42. parser.on('warn', onwarn);
  43. }
  44. if (oninfo) {
  45. parser.on('info', oninfo);
  46. }
  47. customTagParsers.forEach(customParser => parser.addParser(customParser));
  48. customTagMappers.forEach(mapper => parser.addTagMapper(mapper));
  49. parser.push(manifestString);
  50. parser.end();
  51. const manifest = parser.manifest;
  52. // remove llhls features from the parsed manifest
  53. // if we don't want llhls support.
  54. if (!llhls) {
  55. [
  56. 'preloadSegment',
  57. 'skip',
  58. 'serverControl',
  59. 'renditionReports',
  60. 'partInf',
  61. 'partTargetDuration'
  62. ].forEach(function(k) {
  63. if (manifest.hasOwnProperty(k)) {
  64. delete manifest[k];
  65. }
  66. });
  67. if (manifest.segments) {
  68. manifest.segments.forEach(function(segment) {
  69. ['parts', 'preloadHints'].forEach(function(k) {
  70. if (segment.hasOwnProperty(k)) {
  71. delete segment[k];
  72. }
  73. });
  74. });
  75. }
  76. }
  77. if (!manifest.targetDuration) {
  78. let targetDuration = 10;
  79. if (manifest.segments && manifest.segments.length) {
  80. targetDuration = manifest
  81. .segments.reduce((acc, s) => Math.max(acc, s.duration), 0);
  82. }
  83. if (onwarn) {
  84. onwarn({ message: `manifest has no targetDuration defaulting to ${targetDuration}` });
  85. }
  86. manifest.targetDuration = targetDuration;
  87. }
  88. const parts = getLastParts(manifest);
  89. if (parts.length && !manifest.partTargetDuration) {
  90. const partTargetDuration = parts.reduce((acc, p) => Math.max(acc, p.duration), 0);
  91. if (onwarn) {
  92. onwarn({ message: `manifest has no partTargetDuration defaulting to ${partTargetDuration}` });
  93. log.error('LL-HLS manifest has parts but lacks required #EXT-X-PART-INF:PART-TARGET value. See https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis-09#section-4.4.3.7. Playback is not guaranteed.');
  94. }
  95. manifest.partTargetDuration = partTargetDuration;
  96. }
  97. return manifest;
  98. };
  99. /**
  100. * Loops through all supported media groups in main and calls the provided
  101. * callback for each group
  102. *
  103. * @param {Object} main
  104. * The parsed main manifest object
  105. * @param {Function} callback
  106. * Callback to call for each media group
  107. */
  108. export const forEachMediaGroup = (main, callback) => {
  109. if (!main.mediaGroups) {
  110. return;
  111. }
  112. ['AUDIO', 'SUBTITLES'].forEach((mediaType) => {
  113. if (!main.mediaGroups[mediaType]) {
  114. return;
  115. }
  116. for (const groupKey in main.mediaGroups[mediaType]) {
  117. for (const labelKey in main.mediaGroups[mediaType][groupKey]) {
  118. const mediaProperties = main.mediaGroups[mediaType][groupKey][labelKey];
  119. callback(mediaProperties, mediaType, groupKey, labelKey);
  120. }
  121. }
  122. });
  123. };
  124. /**
  125. * Adds properties and attributes to the playlist to keep consistent functionality for
  126. * playlists throughout VHS.
  127. *
  128. * @param {Object} config
  129. * Arguments object
  130. * @param {Object} config.playlist
  131. * The media playlist
  132. * @param {string} [config.uri]
  133. * The uri to the media playlist (if media playlist is not from within a main
  134. * playlist)
  135. * @param {string} id
  136. * ID to use for the playlist
  137. */
  138. export const setupMediaPlaylist = ({ playlist, uri, id }) => {
  139. playlist.id = id;
  140. playlist.playlistErrors_ = 0;
  141. if (uri) {
  142. // For media playlists, m3u8-parser does not have access to a URI, as HLS media
  143. // playlists do not contain their own source URI, but one is needed for consistency in
  144. // VHS.
  145. playlist.uri = uri;
  146. }
  147. // For HLS main playlists, even though certain attributes MUST be defined, the
  148. // stream may still be played without them.
  149. // For HLS media playlists, m3u8-parser does not attach an attributes object to the
  150. // manifest.
  151. //
  152. // To avoid undefined reference errors through the project, and make the code easier
  153. // to write/read, add an empty attributes object for these cases.
  154. playlist.attributes = playlist.attributes || {};
  155. };
  156. /**
  157. * Adds ID, resolvedUri, and attributes properties to each playlist of the main, where
  158. * necessary. In addition, creates playlist IDs for each playlist and adds playlist ID to
  159. * playlist references to the playlists array.
  160. *
  161. * @param {Object} main
  162. * The main playlist
  163. */
  164. export const setupMediaPlaylists = (main) => {
  165. let i = main.playlists.length;
  166. while (i--) {
  167. const playlist = main.playlists[i];
  168. setupMediaPlaylist({
  169. playlist,
  170. id: createPlaylistID(i, playlist.uri)
  171. });
  172. playlist.resolvedUri = resolveUrl(main.uri, playlist.uri);
  173. main.playlists[playlist.id] = playlist;
  174. // URI reference added for backwards compatibility
  175. main.playlists[playlist.uri] = playlist;
  176. // Although the spec states an #EXT-X-STREAM-INF tag MUST have a BANDWIDTH attribute,
  177. // the stream can be played without it. Although an attributes property may have been
  178. // added to the playlist to prevent undefined references, issue a warning to fix the
  179. // manifest.
  180. if (!playlist.attributes.BANDWIDTH) {
  181. log.warn('Invalid playlist STREAM-INF detected. Missing BANDWIDTH attribute.');
  182. }
  183. }
  184. };
  185. /**
  186. * Adds resolvedUri properties to each media group.
  187. *
  188. * @param {Object} main
  189. * The main playlist
  190. */
  191. export const resolveMediaGroupUris = (main) => {
  192. forEachMediaGroup(main, (properties) => {
  193. if (properties.uri) {
  194. properties.resolvedUri = resolveUrl(main.uri, properties.uri);
  195. }
  196. });
  197. };
  198. /**
  199. * Creates a main playlist wrapper to insert a sole media playlist into.
  200. *
  201. * @param {Object} media
  202. * Media playlist
  203. * @param {string} uri
  204. * The media URI
  205. *
  206. * @return {Object}
  207. * main playlist
  208. */
  209. export const mainForMedia = (media, uri) => {
  210. const id = createPlaylistID(0, uri);
  211. const main = {
  212. mediaGroups: {
  213. 'AUDIO': {},
  214. 'VIDEO': {},
  215. 'CLOSED-CAPTIONS': {},
  216. 'SUBTITLES': {}
  217. },
  218. uri: window.location.href,
  219. resolvedUri: window.location.href,
  220. playlists: [{
  221. uri,
  222. id,
  223. resolvedUri: uri,
  224. // m3u8-parser does not attach an attributes property to media playlists so make
  225. // sure that the property is attached to avoid undefined reference errors
  226. attributes: {}
  227. }]
  228. };
  229. // set up ID reference
  230. main.playlists[id] = main.playlists[0];
  231. // URI reference added for backwards compatibility
  232. main.playlists[uri] = main.playlists[0];
  233. return main;
  234. };
  235. /**
  236. * Does an in-place update of the main manifest to add updated playlist URI references
  237. * as well as other properties needed by VHS that aren't included by the parser.
  238. *
  239. * @param {Object} main
  240. * main manifest object
  241. * @param {string} uri
  242. * The source URI
  243. * @param {function} createGroupID
  244. * A function to determine how to create the groupID for mediaGroups
  245. */
  246. export const addPropertiesToMain = (main, uri, createGroupID = groupID) => {
  247. main.uri = uri;
  248. for (let i = 0; i < main.playlists.length; i++) {
  249. if (!main.playlists[i].uri) {
  250. // Set up phony URIs for the playlists since playlists are referenced by their URIs
  251. // throughout VHS, but some formats (e.g., DASH) don't have external URIs
  252. // TODO: consider adding dummy URIs in mpd-parser
  253. const phonyUri = `placeholder-uri-${i}`;
  254. main.playlists[i].uri = phonyUri;
  255. }
  256. }
  257. const audioOnlyMain = isAudioOnly(main);
  258. forEachMediaGroup(main, (properties, mediaType, groupKey, labelKey) => {
  259. // add a playlist array under properties
  260. if (!properties.playlists || !properties.playlists.length) {
  261. // If the manifest is audio only and this media group does not have a uri, check
  262. // if the media group is located in the main list of playlists. If it is, don't add
  263. // placeholder properties as it shouldn't be considered an alternate audio track.
  264. if (audioOnlyMain && mediaType === 'AUDIO' && !properties.uri) {
  265. for (let i = 0; i < main.playlists.length; i++) {
  266. const p = main.playlists[i];
  267. if (p.attributes && p.attributes.AUDIO && p.attributes.AUDIO === groupKey) {
  268. return;
  269. }
  270. }
  271. }
  272. properties.playlists = [Object.assign({}, properties)];
  273. }
  274. properties.playlists.forEach(function(p, i) {
  275. const groupId = createGroupID(mediaType, groupKey, labelKey, p);
  276. const id = createPlaylistID(i, groupId);
  277. if (p.uri) {
  278. p.resolvedUri = p.resolvedUri || resolveUrl(main.uri, p.uri);
  279. } else {
  280. // DEPRECATED, this has been added to prevent a breaking change.
  281. // previously we only ever had a single media group playlist, so
  282. // we mark the first playlist uri without prepending the index as we used to
  283. // ideally we would do all of the playlists the same way.
  284. p.uri = i === 0 ? groupId : id;
  285. // don't resolve a placeholder uri to an absolute url, just use
  286. // the placeholder again
  287. p.resolvedUri = p.uri;
  288. }
  289. p.id = p.id || id;
  290. // add an empty attributes object, all playlists are
  291. // expected to have this.
  292. p.attributes = p.attributes || {};
  293. // setup ID and URI references (URI for backwards compatibility)
  294. main.playlists[p.id] = p;
  295. main.playlists[p.uri] = p;
  296. });
  297. });
  298. setupMediaPlaylists(main);
  299. resolveMediaGroupUris(main);
  300. };