parser.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390
  1. 'use strict';
  2. Object.defineProperty(exports, "__esModule", {
  3. value: true
  4. });
  5. var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
  6. var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
  7. var _stream = require('./stream');
  8. var _stream2 = _interopRequireDefault(_stream);
  9. var _lineStream = require('./line-stream');
  10. var _lineStream2 = _interopRequireDefault(_lineStream);
  11. var _parseStream = require('./parse-stream');
  12. var _parseStream2 = _interopRequireDefault(_parseStream);
  13. function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
  14. function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
  15. function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
  16. function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } /**
  17. * @file m3u8/parser.js
  18. */
  19. /**
  20. * A parser for M3U8 files. The current interpretation of the input is
  21. * exposed as a property `manifest` on parser objects. It's just two lines to
  22. * create and parse a manifest once you have the contents available as a string:
  23. *
  24. * ```js
  25. * var parser = new m3u8.Parser();
  26. * parser.push(xhr.responseText);
  27. * ```
  28. *
  29. * New input can later be applied to update the manifest object by calling
  30. * `push` again.
  31. *
  32. * The parser attempts to create a usable manifest object even if the
  33. * underlying input is somewhat nonsensical. It emits `info` and `warning`
  34. * events during the parse if it encounters input that seems invalid or
  35. * requires some property of the manifest object to be defaulted.
  36. *
  37. * @class Parser
  38. * @extends Stream
  39. */
  40. var Parser = function (_Stream) {
  41. _inherits(Parser, _Stream);
  42. function Parser() {
  43. _classCallCheck(this, Parser);
  44. var _this = _possibleConstructorReturn(this, (Parser.__proto__ || Object.getPrototypeOf(Parser)).call(this));
  45. _this.lineStream = new _lineStream2['default']();
  46. _this.parseStream = new _parseStream2['default']();
  47. _this.lineStream.pipe(_this.parseStream);
  48. /* eslint-disable consistent-this */
  49. var self = _this;
  50. /* eslint-enable consistent-this */
  51. var uris = [];
  52. var currentUri = {};
  53. // if specified, the active EXT-X-MAP definition
  54. var currentMap = void 0;
  55. // if specified, the active decryption key
  56. var _key = void 0;
  57. var noop = function noop() {};
  58. var defaultMediaGroups = {
  59. 'AUDIO': {},
  60. 'VIDEO': {},
  61. 'CLOSED-CAPTIONS': {},
  62. 'SUBTITLES': {}
  63. };
  64. // group segments into numbered timelines delineated by discontinuities
  65. var currentTimeline = 0;
  66. // the manifest is empty until the parse stream begins delivering data
  67. _this.manifest = {
  68. allowCache: true,
  69. discontinuityStarts: [],
  70. segments: []
  71. };
  72. // update the manifest with the m3u8 entry from the parse stream
  73. _this.parseStream.on('data', function (entry) {
  74. var mediaGroup = void 0;
  75. var rendition = void 0;
  76. ({
  77. tag: function tag() {
  78. // switch based on the tag type
  79. (({
  80. 'allow-cache': function allowCache() {
  81. this.manifest.allowCache = entry.allowed;
  82. if (!('allowed' in entry)) {
  83. this.trigger('info', {
  84. message: 'defaulting allowCache to YES'
  85. });
  86. this.manifest.allowCache = true;
  87. }
  88. },
  89. byterange: function byterange() {
  90. var byterange = {};
  91. if ('length' in entry) {
  92. currentUri.byterange = byterange;
  93. byterange.length = entry.length;
  94. if (!('offset' in entry)) {
  95. this.trigger('info', {
  96. message: 'defaulting offset to zero'
  97. });
  98. entry.offset = 0;
  99. }
  100. }
  101. if ('offset' in entry) {
  102. currentUri.byterange = byterange;
  103. byterange.offset = entry.offset;
  104. }
  105. },
  106. endlist: function endlist() {
  107. this.manifest.endList = true;
  108. },
  109. inf: function inf() {
  110. if (!('mediaSequence' in this.manifest)) {
  111. this.manifest.mediaSequence = 0;
  112. this.trigger('info', {
  113. message: 'defaulting media sequence to zero'
  114. });
  115. }
  116. if (!('discontinuitySequence' in this.manifest)) {
  117. this.manifest.discontinuitySequence = 0;
  118. this.trigger('info', {
  119. message: 'defaulting discontinuity sequence to zero'
  120. });
  121. }
  122. if (entry.duration > 0) {
  123. currentUri.duration = entry.duration;
  124. }
  125. if (entry.duration === 0) {
  126. currentUri.duration = 0.01;
  127. this.trigger('info', {
  128. message: 'updating zero segment duration to a small value'
  129. });
  130. }
  131. this.manifest.segments = uris;
  132. },
  133. key: function key() {
  134. if (!entry.attributes) {
  135. this.trigger('warn', {
  136. message: 'ignoring key declaration without attribute list'
  137. });
  138. return;
  139. }
  140. // clear the active encryption key
  141. if (entry.attributes.METHOD === 'NONE') {
  142. _key = null;
  143. return;
  144. }
  145. if (!entry.attributes.URI) {
  146. this.trigger('warn', {
  147. message: 'ignoring key declaration without URI'
  148. });
  149. return;
  150. }
  151. if (!entry.attributes.METHOD) {
  152. this.trigger('warn', {
  153. message: 'defaulting key method to AES-128'
  154. });
  155. }
  156. // setup an encryption key for upcoming segments
  157. _key = {
  158. method: entry.attributes.METHOD || 'AES-128',
  159. uri: entry.attributes.URI
  160. };
  161. if (typeof entry.attributes.IV !== 'undefined') {
  162. _key.iv = entry.attributes.IV;
  163. }
  164. },
  165. 'media-sequence': function mediaSequence() {
  166. if (!isFinite(entry.number)) {
  167. this.trigger('warn', {
  168. message: 'ignoring invalid media sequence: ' + entry.number
  169. });
  170. return;
  171. }
  172. this.manifest.mediaSequence = entry.number;
  173. },
  174. 'discontinuity-sequence': function discontinuitySequence() {
  175. if (!isFinite(entry.number)) {
  176. this.trigger('warn', {
  177. message: 'ignoring invalid discontinuity sequence: ' + entry.number
  178. });
  179. return;
  180. }
  181. this.manifest.discontinuitySequence = entry.number;
  182. currentTimeline = entry.number;
  183. },
  184. 'playlist-type': function playlistType() {
  185. if (!/VOD|EVENT/.test(entry.playlistType)) {
  186. this.trigger('warn', {
  187. message: 'ignoring unknown playlist type: ' + entry.playlist
  188. });
  189. return;
  190. }
  191. this.manifest.playlistType = entry.playlistType;
  192. },
  193. map: function map() {
  194. currentMap = {};
  195. if (entry.uri) {
  196. currentMap.uri = entry.uri;
  197. }
  198. if (entry.byterange) {
  199. currentMap.byterange = entry.byterange;
  200. }
  201. },
  202. 'stream-inf': function streamInf() {
  203. this.manifest.playlists = uris;
  204. this.manifest.mediaGroups = this.manifest.mediaGroups || defaultMediaGroups;
  205. if (!entry.attributes) {
  206. this.trigger('warn', {
  207. message: 'ignoring empty stream-inf attributes'
  208. });
  209. return;
  210. }
  211. if (!currentUri.attributes) {
  212. currentUri.attributes = {};
  213. }
  214. _extends(currentUri.attributes, entry.attributes);
  215. },
  216. media: function media() {
  217. this.manifest.mediaGroups = this.manifest.mediaGroups || defaultMediaGroups;
  218. if (!(entry.attributes && entry.attributes.TYPE && entry.attributes['GROUP-ID'] && entry.attributes.NAME)) {
  219. this.trigger('warn', {
  220. message: 'ignoring incomplete or missing media group'
  221. });
  222. return;
  223. }
  224. // find the media group, creating defaults as necessary
  225. var mediaGroupType = this.manifest.mediaGroups[entry.attributes.TYPE];
  226. mediaGroupType[entry.attributes['GROUP-ID']] = mediaGroupType[entry.attributes['GROUP-ID']] || {};
  227. mediaGroup = mediaGroupType[entry.attributes['GROUP-ID']];
  228. // collect the rendition metadata
  229. rendition = {
  230. 'default': /yes/i.test(entry.attributes.DEFAULT)
  231. };
  232. if (rendition['default']) {
  233. rendition.autoselect = true;
  234. } else {
  235. rendition.autoselect = /yes/i.test(entry.attributes.AUTOSELECT);
  236. }
  237. if (entry.attributes.LANGUAGE) {
  238. rendition.language = entry.attributes.LANGUAGE;
  239. }
  240. if (entry.attributes.URI) {
  241. rendition.uri = entry.attributes.URI;
  242. }
  243. if (entry.attributes['INSTREAM-ID']) {
  244. rendition.instreamId = entry.attributes['INSTREAM-ID'];
  245. }
  246. if (entry.attributes.CHARACTERISTICS) {
  247. rendition.characteristics = entry.attributes.CHARACTERISTICS;
  248. }
  249. if (entry.attributes.FORCED) {
  250. rendition.forced = /yes/i.test(entry.attributes.FORCED);
  251. }
  252. // insert the new rendition
  253. mediaGroup[entry.attributes.NAME] = rendition;
  254. },
  255. discontinuity: function discontinuity() {
  256. currentTimeline += 1;
  257. currentUri.discontinuity = true;
  258. this.manifest.discontinuityStarts.push(uris.length);
  259. },
  260. 'program-date-time': function programDateTime() {
  261. this.manifest.dateTimeString = entry.dateTimeString;
  262. this.manifest.dateTimeObject = entry.dateTimeObject;
  263. },
  264. targetduration: function targetduration() {
  265. if (!isFinite(entry.duration) || entry.duration < 0) {
  266. this.trigger('warn', {
  267. message: 'ignoring invalid target duration: ' + entry.duration
  268. });
  269. return;
  270. }
  271. this.manifest.targetDuration = entry.duration;
  272. },
  273. totalduration: function totalduration() {
  274. if (!isFinite(entry.duration) || entry.duration < 0) {
  275. this.trigger('warn', {
  276. message: 'ignoring invalid total duration: ' + entry.duration
  277. });
  278. return;
  279. }
  280. this.manifest.totalDuration = entry.duration;
  281. },
  282. 'cue-out': function cueOut() {
  283. currentUri.cueOut = entry.data;
  284. },
  285. 'cue-out-cont': function cueOutCont() {
  286. currentUri.cueOutCont = entry.data;
  287. },
  288. 'cue-in': function cueIn() {
  289. currentUri.cueIn = entry.data;
  290. }
  291. })[entry.tagType] || noop).call(self);
  292. },
  293. uri: function uri() {
  294. currentUri.uri = entry.uri;
  295. uris.push(currentUri);
  296. // if no explicit duration was declared, use the target duration
  297. if (this.manifest.targetDuration && !('duration' in currentUri)) {
  298. this.trigger('warn', {
  299. message: 'defaulting segment duration to the target duration'
  300. });
  301. currentUri.duration = this.manifest.targetDuration;
  302. }
  303. // annotate with encryption information, if necessary
  304. if (_key) {
  305. currentUri.key = _key;
  306. }
  307. currentUri.timeline = currentTimeline;
  308. // annotate with initialization segment information, if necessary
  309. if (currentMap) {
  310. currentUri.map = currentMap;
  311. }
  312. // prepare for the next URI
  313. currentUri = {};
  314. },
  315. comment: function comment() {
  316. // comments are not important for playback
  317. }
  318. })[entry.type].call(self);
  319. });
  320. return _this;
  321. }
  322. /**
  323. * Parse the input string and update the manifest object.
  324. *
  325. * @param {String} chunk a potentially incomplete portion of the manifest
  326. */
  327. _createClass(Parser, [{
  328. key: 'push',
  329. value: function push(chunk) {
  330. this.lineStream.push(chunk);
  331. }
  332. /**
  333. * Flush any remaining input. This can be handy if the last line of an M3U8
  334. * manifest did not contain a trailing newline but the file has been
  335. * completely received.
  336. */
  337. }, {
  338. key: 'end',
  339. value: function end() {
  340. // flush any buffered input
  341. this.lineStream.push('\n');
  342. }
  343. }]);
  344. return Parser;
  345. }(_stream2['default']);
  346. exports['default'] = Parser;