| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278 |
- var path = require('path'),
- util = require('util'),
- carrier = require('carrier'),
- debug = require('debug')('hls:m3u8');
- exports.M3U8Playlist = M3U8Playlist;
- exports.M3U8Segment = M3U8Segment;
- exports.ParserError = ParserError;
- exports.parse = parse;
- //exports.stringify = stringify;
- function M3U8Playlist() {
- this.variant = false;
- // initialize to default values
- this.version = 1; // V1
- this.allow_cache = true;
- this.i_frames_only = false; // V4
- this.target_duration = undefined;
- this.first_seq_no = 0;
- this.type = undefined; // V?
- this.ended = false;
- this.segments = [];
- // for variant streams
- this.programs = {};
- this.groups = {};
- }
- M3U8Playlist.prototype.PlaylistType = {
- EVENT: 'EVENT',
- VOD: 'VOD'
- };
- M3U8Playlist.prototype.totalDuration = function() {
- return this.segments.reduce(function(sum, segment) {
- return sum + segment.duration;
- }, 0);
- };
- M3U8Playlist.prototype.isLive = function() {
- return !(this.ended || this.type === this.PlaylistType.VOD);
- };
- M3U8Playlist.prototype.startSeqNo = function(full) {
- if (!this.isLive() || full) return this.first_seq_no;
- var duration = this.target_duration * 3;
- for (var i=this.segments.length-1; i>0; i--) {
- duration -= this.segments[i].duration;
- if (duration < 0) break;
- }
- // TODO: validate that correct seqNo is returned
- return this.first_seq_no + i;
- };
- M3U8Playlist.prototype.lastSeqNo = function() {
- return this.first_seq_no + this.segments.length - 1;
- };
- M3U8Playlist.prototype.getSegment = function(seqNo) {
- // TODO: should we check for number type and throw if not?
- var index = seqNo-this.first_seq_no;
- if (index < 0 || index > this.segments.length)
- return null;
- return this.segments[index];
- };
- M3U8Playlist.prototype.toString = function() {
- // TODO:
- return 'M3U8Playlist';
- };
- function M3U8Segment(uri, meta, version) {
- this.duration = meta.duration;
- this.title = meta.title;
- this.uri = uri;
- this.discontinuity = meta.discontinuity || false;
- // optional
- if (meta.program_time)
- this.program_time = meta.program_time;
- if (meta.key)
- this.key = meta.key;
- if (version >= 5 && meta.map)
- this.map = meta.map;
- }
- M3U8Segment.prototype.toString = function() {
- // TODO: support writing all the information
- return '#EXTINF '+this.duration.toFixed(3)+','+this.title + '\n' + this.uri + '\n';
- };
- function parse(stream, cb) {
- var m3u8 = new M3U8Playlist(),
- line_no = 0,
- meta = {};
- stream.on('error', ReportError);
- var cr = carrier.carry(stream);
- cr.on('line', ParseLine);
- cr.on('end', Complete);
- function ReportError(err) {
- stream.removeListener('error', ReportError);
- cr.removeListener('line', ParseLine);
- cr.removeListener('end', Complete);
- cb(err);
- }
- function Complete() {
- stream.removeListener('error', ReportError);
- cb(null, m3u8);
- }
- function ParseExt(cmd, arg) {
- if (!(cmd in extParser))
- return false;
- debug('parsing ext', cmd, arg);
- extParser[cmd](arg);
- return true;
- }
-
- // AttrList's are currently handled without any implicit knowledge of key/type mapping
- function ParseAttrList(input) {
- // TODO: handle newline escapes in quoted-string's
- var re = /(.+?)=((?:\".*?\")|.*?)(?:,|$)/g;
- // var re = /(.+?)=(?:(?:\"(.*?)\")|(.*?))(?:,|$)/g;
- var match, attrs = {};
- while ((match = re.exec(input)) !== null)
- attrs[match[1].toLowerCase()] = match[2];
- debug('parsed attributes', attrs);
- return attrs;
- }
- function unquote(str) {
- return str.slice(1,-1);
- }
- function ParseLine(line) {
- line_no += 1;
- if (line_no === 1) {
- if (line !== '#EXTM3U')
- return ReportError(new ParserError('Missing required #EXTM3U header', line, line_no));
- return;
- }
- if (!line.length) return; // blank lines are ignored (3.1)
-
- if (line[0] === '#') {
- var matches = line.match(/^(#EXT[^:]*):?(.*)/);
- if (!matches)
- return debug('ignoring comment', line);
- var cmd = matches[1],
- arg = matches[2];
- if (!ParseExt(cmd, arg))
- return ReportError(new ParserError('Unknown #EXT: '+cmd, line, line_no));
- } else if (m3u8.variant) {
- var id = meta.info['program-id'];
- if (!(id in m3u8.programs))
- m3u8.programs[id] = [];
- meta.uri = line;
- m3u8.programs[id].push(meta);
- meta = {};
- } else {
- if (!('duration' in meta))
- return ReportError(new ParserError('Missing #EXTINF before media file URI', line, line_no));
- m3u8.segments.push(new M3U8Segment(line, meta, m3u8.version));
- meta = {};
- }
- }
- // TODO: add more validation logic
- var extParser = {
- '#EXT-X-VERSION': function(arg) {
- m3u8.version = parseInt(arg, 10);
- if (m3u8.version >= 4)
- for (var attrname in extParserV4) { extParser[attrname] = extParser[attrname]; }
- if (m3u8.version >= 5)
- for (var attrname in extParserV5) { extParser[attrname] = extParser[attrname]; }
- },
- '#EXT-X-TARGETDURATION': function(arg) {
- m3u8.target_duration = parseInt(arg, 10);
- },
- '#EXT-X-ALLOW-CACHE': function(arg) {
- m3u8.allow_cache = (arg!=='NO');
- },
- '#EXT-X-MEDIA-SEQUENCE': function(arg) {
- m3u8.first_seq_no = parseInt(arg, 10);
- },
- '#EXT-X-PLAYLIST-TYPE': function(arg) {
- m3u8.type = arg;
- },
- '#EXT-X-ENDLIST': function(arg) {
- m3u8.ended = true;
- },
- '#EXTINF': function(arg) {
- var n = arg.split(',');
- meta.duration = parseFloat(n.shift());
- meta.title = n.join(',');
- if (meta.duration <= 0)
- return ReportError(new ParserError('Invalid duration', line, line_no));
- },
- '#EXT-X-KEY': function(arg) {
- meta.key = ParseAttrList(arg);
- },
- '#EXT-X-PROGRAM-DATE-TIME': function(arg) {
- meta.program_time = new Date(arg);
- },
- '#EXT-X-DISCONTINUITY': function(arg) {
- meta.discontinuity = true;
- },
- // variant
- '#EXT-X-STREAM-INF': function(arg) {
- m3u8.variant = true;
- meta.info = ParseAttrList(arg);
- },
- // variant v4 since variant streams are not required to specify version
- '#EXT-X-MEDIA': function(arg) {
- //m3u8.variant = true;
- var attrs = ParseAttrList(arg),
- id = unquote(attrs['group-id']);
- if (!(id in m3u8.groups)) {
- m3u8.groups[id] = [];
- m3u8.groups[id].type = attrs.type;
- }
- m3u8.groups[id].push(attrs);
- },
- '#EXT-X-I-FRAME-STREAM-INF': function(arg) {
- m3u8.variant = true;
- debug('not yet supported', '#EXT-X-I-FRAME-STREAM-INF');
- }
- };
- var extParserV4 = {
- '#EXT-X-I-FRAMES-ONLY': function(arg) {
- m3u8.i_frames_only = true;
- },
- '#EXT-X-BYTERANGE': function(arg) {
- var n = arg.split('@');
- meta.byterange = {length:parseInt(n[0], 10)};
- if (n.length > 1)
- meta.byterange.offset = parseInt(n[1], 10);
- }
- }
- var extParserV5 = {
- '#EXT-X-MAP': function(arg) {
- meta.map = ParseAttrList(arg);
- }
- }
- }
- function ParserError(msg, line, line_no, constr) {
- Error.captureStackTrace(this, constr || this);
- this.message = msg || 'Error';
- this.line = line;
- this.line_no = line_no;
- }
- util.inherits(ParserError, Error);
- ParserError.prototype.name = 'Parser Error';
|