m3u8.js 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291
  1. "use strict";
  2. var path = require('path'),
  3. util = require('util'),
  4. carrier = require('carrier'),
  5. debug = require('debug')('hls:m3u8');
  6. exports.M3U8Playlist = M3U8Playlist;
  7. exports.M3U8Segment = M3U8Segment;
  8. exports.ParserError = ParserError;
  9. exports.parse = parse;
  10. //exports.stringify = stringify;
  11. function M3U8Playlist() {
  12. this.variant = false;
  13. // initialize to default values
  14. this.version = 1; // V1
  15. this.allow_cache = true;
  16. this.i_frames_only = false; // V4
  17. this.target_duration = undefined;
  18. this.first_seq_no = 0;
  19. this.type = undefined; // V?
  20. this.ended = false;
  21. this.segments = [];
  22. // for variant streams
  23. this.programs = {};
  24. this.groups = {};
  25. }
  26. M3U8Playlist.prototype.PlaylistType = {
  27. EVENT: 'EVENT',
  28. VOD: 'VOD'
  29. };
  30. M3U8Playlist.prototype.totalDuration = function() {
  31. return this.segments.reduce(function(sum, segment) {
  32. return sum + segment.duration;
  33. }, 0);
  34. };
  35. M3U8Playlist.prototype.isLive = function() {
  36. return !(this.ended || this.type === this.PlaylistType.VOD);
  37. };
  38. M3U8Playlist.prototype.startSeqNo = function(full) {
  39. if (!this.isLive() || full) return this.first_seq_no;
  40. var duration = this.target_duration * 3;
  41. for (var i=this.segments.length-1; i>0; i--) {
  42. duration -= this.segments[i].duration;
  43. if (duration < 0) break;
  44. }
  45. // TODO: validate that correct seqNo is returned
  46. return this.first_seq_no + i;
  47. };
  48. M3U8Playlist.prototype.lastSeqNo = function() {
  49. return this.first_seq_no + this.segments.length - 1;
  50. };
  51. // return whether the seqNo is in the index
  52. M3U8Playlist.prototype.isValidSeqNo = function(seqNo) {
  53. return (seqNo >= this.first_seq_no) && (seqNo <= this.lastSeqNo());
  54. };
  55. M3U8Playlist.prototype.getSegment = function(seqNo) {
  56. // TODO: should we check for number type and throw if not?
  57. var index = seqNo-this.first_seq_no;
  58. if (index < 0 || index > this.segments.length)
  59. return null;
  60. return this.segments[index];
  61. };
  62. M3U8Playlist.prototype.toString = function() {
  63. // TODO:
  64. return 'M3U8Playlist';
  65. };
  66. function M3U8Segment(uri, meta, version) {
  67. this.duration = meta.duration;
  68. this.title = meta.title;
  69. this.uri = uri;
  70. this.discontinuity = meta.discontinuity || false;
  71. // optional
  72. if (meta.program_time)
  73. this.program_time = meta.program_time;
  74. if (meta.key)
  75. this.key = meta.key;
  76. if (version >= 5 && meta.map)
  77. this.map = meta.map;
  78. }
  79. M3U8Segment.prototype.toString = function() {
  80. // TODO: support writing all the information
  81. return '#EXTINF '+this.duration.toFixed(3)+','+this.title + '\n' + this.uri + '\n';
  82. };
  83. function parse(stream, cb) {
  84. var m3u8 = new M3U8Playlist(),
  85. line_no = 0,
  86. meta = {};
  87. stream.on('error', ReportError);
  88. var cr = carrier.carry(stream);
  89. cr.on('line', ParseLine);
  90. cr.on('end', Complete);
  91. function cleanup() {
  92. stream.removeListener('error', ReportError);
  93. cr.removeListener('line', ParseLine);
  94. cr.removeListener('end', Complete);
  95. }
  96. function ReportError(err) {
  97. cleanup();
  98. cb(err);
  99. }
  100. function Complete() {
  101. if (line_no === 0)
  102. return ReportError(new ParserError('No line data', '', -1))
  103. cleanup();
  104. cb(null, m3u8);
  105. }
  106. function ParseExt(cmd, arg) {
  107. if (!(cmd in extParser))
  108. return false;
  109. debug('parsing ext', cmd, arg);
  110. extParser[cmd](arg);
  111. return true;
  112. }
  113. // AttrList's are currently handled without any implicit knowledge of key/type mapping
  114. function ParseAttrList(input) {
  115. // TODO: handle newline escapes in quoted-string's
  116. var re = /(.+?)=((?:\".*?\")|.*?)(?:,|$)/g;
  117. // var re = /(.+?)=(?:(?:\"(.*?)\")|(.*?))(?:,|$)/g;
  118. var match, attrs = {};
  119. while ((match = re.exec(input)) !== null)
  120. attrs[match[1].toLowerCase()] = match[2];
  121. debug('parsed attributes', attrs);
  122. return attrs;
  123. }
  124. function unquote(str) {
  125. return str.slice(1,-1);
  126. }
  127. function ParseLine(line) {
  128. line_no += 1;
  129. if (line_no === 1) {
  130. if (line !== '#EXTM3U')
  131. return ReportError(new ParserError('Missing required #EXTM3U header', line, line_no));
  132. return;
  133. }
  134. if (!line.length) return; // blank lines are ignored (3.1)
  135. if (line[0] === '#') {
  136. var matches = line.match(/^(#EXT[^:]*):?(.*)/);
  137. if (!matches)
  138. return debug('ignoring comment', line);
  139. var cmd = matches[1],
  140. arg = matches[2];
  141. if (!ParseExt(cmd, arg))
  142. return ReportError(new ParserError('Unknown #EXT: '+cmd, line, line_no));
  143. } else if (m3u8.variant) {
  144. var id = meta.info['program-id'];
  145. if (!(id in m3u8.programs))
  146. m3u8.programs[id] = [];
  147. meta.uri = line;
  148. m3u8.programs[id].push(meta);
  149. meta = {};
  150. } else {
  151. if (!('duration' in meta))
  152. return ReportError(new ParserError('Missing #EXTINF before media file URI', line, line_no));
  153. m3u8.segments.push(new M3U8Segment(line, meta, m3u8.version));
  154. meta = {};
  155. }
  156. }
  157. // TODO: add more validation logic
  158. var extParser = {
  159. '#EXT-X-VERSION': function(arg) {
  160. m3u8.version = parseInt(arg, 10);
  161. if (m3u8.version >= 4)
  162. for (var attrname in extParserV4) { extParser[attrname] = extParser[attrname]; }
  163. if (m3u8.version >= 5)
  164. for (var attrname in extParserV5) { extParser[attrname] = extParser[attrname]; }
  165. },
  166. '#EXT-X-TARGETDURATION': function(arg) {
  167. m3u8.target_duration = parseInt(arg, 10);
  168. },
  169. '#EXT-X-ALLOW-CACHE': function(arg) {
  170. m3u8.allow_cache = (arg!=='NO');
  171. },
  172. '#EXT-X-MEDIA-SEQUENCE': function(arg) {
  173. m3u8.first_seq_no = parseInt(arg, 10);
  174. },
  175. '#EXT-X-PLAYLIST-TYPE': function(arg) {
  176. m3u8.type = arg;
  177. },
  178. '#EXT-X-ENDLIST': function(arg) {
  179. m3u8.ended = true;
  180. },
  181. '#EXTINF': function(arg) {
  182. var n = arg.split(',');
  183. meta.duration = parseFloat(n.shift());
  184. meta.title = n.join(',');
  185. if (meta.duration <= 0)
  186. return ReportError(new ParserError('Invalid duration', line, line_no));
  187. },
  188. '#EXT-X-KEY': function(arg) {
  189. meta.key = ParseAttrList(arg);
  190. },
  191. '#EXT-X-PROGRAM-DATE-TIME': function(arg) {
  192. meta.program_time = new Date(arg);
  193. },
  194. '#EXT-X-DISCONTINUITY': function(arg) {
  195. meta.discontinuity = true;
  196. },
  197. // variant
  198. '#EXT-X-STREAM-INF': function(arg) {
  199. m3u8.variant = true;
  200. meta.info = ParseAttrList(arg);
  201. },
  202. // variant v4 since variant streams are not required to specify version
  203. '#EXT-X-MEDIA': function(arg) {
  204. //m3u8.variant = true;
  205. var attrs = ParseAttrList(arg),
  206. id = unquote(attrs['group-id']);
  207. if (!(id in m3u8.groups)) {
  208. m3u8.groups[id] = [];
  209. m3u8.groups[id].type = attrs.type;
  210. }
  211. m3u8.groups[id].push(attrs);
  212. },
  213. '#EXT-X-I-FRAME-STREAM-INF': function(arg) {
  214. m3u8.variant = true;
  215. debug('not yet supported', '#EXT-X-I-FRAME-STREAM-INF');
  216. }
  217. };
  218. var extParserV4 = {
  219. '#EXT-X-I-FRAMES-ONLY': function(arg) {
  220. m3u8.i_frames_only = true;
  221. },
  222. '#EXT-X-BYTERANGE': function(arg) {
  223. var n = arg.split('@');
  224. meta.byterange = {length:parseInt(n[0], 10)};
  225. if (n.length > 1)
  226. meta.byterange.offset = parseInt(n[1], 10);
  227. }
  228. }
  229. var extParserV5 = {
  230. '#EXT-X-MAP': function(arg) {
  231. meta.map = ParseAttrList(arg);
  232. }
  233. }
  234. }
  235. function ParserError(msg, line, line_no, constr) {
  236. Error.captureStackTrace(this, constr || this);
  237. this.message = msg || 'Error';
  238. this.line = line;
  239. this.lineNumber = line_no;
  240. }
  241. util.inherits(ParserError, Error);
  242. ParserError.prototype.name = 'Parser Error';