From cd7175aec399175724f5ca7f46d9188934789afa Mon Sep 17 00:00:00 2001 From: Aziz Khoury Date: Thu, 18 Feb 2016 03:53:47 -0500 Subject: [PATCH 01/41] add support for EXT-X-PROGRAM-DATE-TIME read, it's already support in m3u.toString --- parser.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/parser.js b/parser.js index d4d3f4d..4731d18 100644 --- a/parser.js +++ b/parser.js @@ -81,11 +81,19 @@ m3uParser.prototype['EXTINF'] = function parseInf(data) { this.currentItem.set('discontinuity', true); this.playlistDiscontinuity = false; } + if (this.playlistDate) { + this.currentItem.set('date', this.playlistDate); + this.playlistDate = null; + } +}; + +m3uParser.prototype['EXT-X-PROGRAM-DATE-TIME'] = function parseInf(data) { + this.playlistDate = new Date(data); }; m3uParser.prototype['EXT-X-DISCONTINUITY'] = function parseInf() { this.playlistDiscontinuity = true; -} +}; m3uParser.prototype['EXT-X-BYTERANGE'] = function parseByteRange(data) { this.currentItem.set('byteRange', data); @@ -107,7 +115,7 @@ m3uParser.prototype['EXT-X-MEDIA'] = function(data) { m3uParser.prototype.parseAttributes = function parseAttributes(data) { data = data.split(NON_QUOTED_COMMA); - var self = this; + return data.map(function(attribute) { var keyValue = attribute.split(/=(.+)/).map(function(str) { return str.trim(); From 05c9cda9b83041dcd5f1ec85d45ead7f0156c08e Mon Sep 17 00:00:00 2001 From: Aziz Khoury Date: Thu, 18 Feb 2016 03:56:24 -0500 Subject: [PATCH 02/41] add support for slice, sliceSeconds, sliceDates, clone along with printing out the ENFLIST if the playlistType is not available, since it's optional --- m3u.js | 95 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 93 insertions(+), 2 deletions(-) diff --git a/m3u.js b/m3u.js index ce4cbd8..cffe4ba 100644 --- a/m3u.js +++ b/m3u.js @@ -1,3 +1,5 @@ +var util = require('util'); + var M3U = module.exports = function M3U() { this.items = { PlaylistItem: [], @@ -42,7 +44,7 @@ M3U.prototype.addPlaylistItem = function addPlaylistItem(data) { }; M3U.prototype.removePlaylistItem = function removePlaylistItem(index) { - if (index < this.items.PlaylistItem.length && index >= 0){ + if (index < this.items.PlaylistItem.length && index >= 0) { this.items.PlaylistItem.splice(index, 1); } else { throw new RangeError('M3U PlaylistItem out of range'); @@ -89,6 +91,91 @@ M3U.prototype.merge = function merge(m3u) { return this; }; +M3U.prototype.slice = function slice(start, end) { + var m3u = this.clone(); + + var len = m3u.items.PlaylistItem.length; + + start = !start || start < 0 ? 0 : start; + end = end == null || end > len ? len : end; + + m3u.items.PlaylistItem = m3u.items.PlaylistItem.slice(start, end); + + return m3u; +}; + +M3U.prototype.sliceSeconds = function slice(from, to) { + var start = null; + var end = null; + + var total = 0; + + if (util.isNumber(from) && util.isNumber(to) && from > to) { + throw 'target `to` value, if truthy, must be greater than the `from` value'; + } + + this.items.PlaylistItem.some(function(item, i) { + total += item.properties.duration; + + if (total >= from && start == null) { + start = i; + if (to == null) { + return true; + } + } + + if (total >= to && end == null) { + end = i + 1; + return true; + } + }); + + return this.slice(start, end); +}; + +M3U.prototype.sliceDates = function slice(from, to) { + var start = null; + var end = null; + + if (!util.isDate(from) && !util.isDate(to)) { + throw 'sliceDates requires that at least 1 of the arguments to be a Date object'; + } + + if (util.isNumber(from)) { + from = new Date(to.getTime() - from * 1000); + } else if (util.isNumber(to)) { + to = new Date(from.getTime() + to * 1000); + } + + if (util.isDate(from) && util.isDate(to) && from > to) { + throw 'target `to` date value, if available, must be greater than the `from` date value'; + } + + var current; + + this.items.PlaylistItem.some(function(item, i) { + current = item.properties.date; + + if (!current) { + throw 'Playlist segment does not have a date field, you must specify EXT-X-PROGRAM-DATE-TIME for each segment in order to sliceDate()'; + } + + if (current >= from && start == null) { + start = i; + if (to == null) { + return true; + } + } + + if (current >= to && end == null) { + end = i + 1; + return true; + } + }); + + return this.slice(start, end); +}; + M3U.prototype.toString = function toString() { var self = this; var output = ['#EXTM3U']; @@ -106,7 +193,7 @@ M3U.prototype.toString = function toString() { if (this.items.PlaylistItem.length) { output.push(this.items.PlaylistItem.map(itemToString).join('\n')); - if (this.get('playlistType') === 'VOD') { + if (this.get('playlistType') == null || this.get('playlistType') === 'VOD') { output.push('#EXT-X-ENDLIST'); } } else { @@ -124,6 +211,10 @@ M3U.prototype.toString = function toString() { return output.join('\n') + '\n'; }; +M3U.prototype.clone = function clone() { + return M3U.unserialize(this.serialize()); +}; + M3U.prototype.serialize = function serialize() { var object = { properties: this.properties, items: {} }; var self = this; From d213e213db1e03ad103b7281a1788dd52d7ff510 Mon Sep 17 00:00:00 2001 From: Aziz Khoury Date: Thu, 18 Feb 2016 04:00:35 -0500 Subject: [PATCH 03/41] live test should include playlistType=EVENT, since it's optional and if it's not there, it is assumed to be a VOD --- test/m3u.test.js | 1 + 1 file changed, 1 insertion(+) diff --git a/test/m3u.test.js b/test/m3u.test.js index d163f7b..13df900 100644 --- a/test/m3u.test.js +++ b/test/m3u.test.js @@ -198,6 +198,7 @@ describe('m3u', function() { describe('writeLive', function() { it('should return a string not ending with #EXT-X-ENDLIST', function() { var m3u1 = getM3u(); + m3u1.set('playlistType', 'EVENT'); m3u1.addPlaylistItem({}); var output = m3u1.toString(); From a61a689bd92ca4be3187ae702ccfaea015c73a29 Mon Sep 17 00:00:00 2001 From: Aziz Khoury Date: Fri, 19 Feb 2016 13:39:20 -0500 Subject: [PATCH 04/41] if you find an endlist when parsing, dont output it --- m3u.js | 2 +- parser.js | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/m3u.js b/m3u.js index cffe4ba..230855f 100644 --- a/m3u.js +++ b/m3u.js @@ -193,7 +193,7 @@ M3U.prototype.toString = function toString() { if (this.items.PlaylistItem.length) { output.push(this.items.PlaylistItem.map(itemToString).join('\n')); - if (this.get('playlistType') == null || this.get('playlistType') === 'VOD') { + if ((this.get('foundEndlist') && this.get('playlistType') == null) || this.get('playlistType') === 'VOD') { output.push('#EXT-X-ENDLIST'); } } else { diff --git a/parser.js b/parser.js index 4731d18..cc26924 100644 --- a/parser.js +++ b/parser.js @@ -41,7 +41,12 @@ m3uParser.prototype.parse = function parse(line) { this.linesRead++; return true; } - if (['', '#EXT-X-ENDLIST'].indexOf(line) > -1) return true; + + if (['', '#EXT-X-ENDLIST'].indexOf(line) > -1) { + this.m3u.set('foundEndlist', true); + return true; + } + if (line.indexOf('#') == 0) { this.parseLine(line); } else { From e9f3cca59c32893302b1f3348da97e825a5e804e Mon Sep 17 00:00:00 2001 From: Aziz Khoury Date: Sat, 20 Feb 2016 02:23:33 -0500 Subject: [PATCH 05/41] add support for custom parse, i.e. set date using filename if the program-date-time isnt available --- m3u.js | 19 ++++++++++++++++--- parser.js | 13 ++++++++++--- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/m3u.js b/m3u.js index 230855f..730cf24 100644 --- a/m3u.js +++ b/m3u.js @@ -94,6 +94,10 @@ M3U.prototype.merge = function merge(m3u) { M3U.prototype.slice = function slice(start, end) { var m3u = this.clone(); + if (start == null && end == null) { + return m3u; + } + var len = m3u.items.PlaylistItem.length; start = !start || start < 0 ? 0 : start; @@ -124,7 +128,7 @@ M3U.prototype.sliceSeconds = function slice(from, to) { } } - if (total >= to && end == null) { + if (total <= to && end == null) { end = i + 1; return true; } @@ -147,6 +151,14 @@ M3U.prototype.sliceDates = function slice(from, to) { to = new Date(from.getTime() + to * 1000); } + if (!from) { + from = new Date(0); + } + + if (!to) { + to = new Date(); + } + if (util.isDate(from) && util.isDate(to) && from > to) { throw 'target `to` date value, if available, must be greater than the `from` date value'; } @@ -167,8 +179,8 @@ M3U.prototype.sliceDates = function slice(from, to) { } } - if (current >= to && end == null) { - end = i + 1; + if (current > to && end == null) { + end = i; return true; } }); @@ -179,6 +191,7 @@ M3U.prototype.sliceDates = function slice(from, to) { M3U.prototype.toString = function toString() { var self = this; var output = ['#EXTM3U']; + Object.keys(this.properties).forEach(function(key) { var tagKey = propertyMap.findByKey(key); var tag = tagKey ? tagKey.tag : key; diff --git a/parser.js b/parser.js index cc26924..50f2c35 100644 --- a/parser.js +++ b/parser.js @@ -9,12 +9,14 @@ var util = require('util'), // used for splitting strings by commas not within double quotes var NON_QUOTED_COMMA = /,(?=(?:[^"]|"[^"]*")*$)/; -var m3uParser = module.exports = function m3uParser() { +var m3uParser = module.exports = function m3uParser(options) { ChunkedStream.apply(this, ['\n', true]); this.linesRead = 0; this.m3u = new M3U; + this.options = options || {}; + this.on('data', this.parse.bind(this)); var self = this; this.on('end', function() { @@ -26,8 +28,8 @@ util.inherits(m3uParser, ChunkedStream); m3uParser.M3U = M3U; -m3uParser.createStream = function() { - return new m3uParser; +m3uParser.createStream = function(options) { + return new m3uParser(options); }; m3uParser.prototype.parse = function parse(line) { @@ -54,6 +56,11 @@ m3uParser.prototype.parse = function parse(line) { this.addItem(new PlaylistItem); } this.currentItem.set('uri', line); + + if (typeof this.options.beforeItemEmit == 'function') { + this.currentItem = this.options.beforeItemEmit(this.currentItem); + } + this.emit('item', this.currentItem); } this.linesRead++; From 298df090838b336c9e31c8abc7eb0037ebe78308 Mon Sep 17 00:00:00 2001 From: Aziz Khoury Date: Sat, 20 Feb 2016 02:27:14 -0500 Subject: [PATCH 06/41] gte --- m3u.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/m3u.js b/m3u.js index 730cf24..1c5948d 100644 --- a/m3u.js +++ b/m3u.js @@ -179,7 +179,7 @@ M3U.prototype.sliceDates = function slice(from, to) { } } - if (current > to && end == null) { + if (current >= to && end == null) { end = i; return true; } From 991825f67a8dffac3d725ba25849ee010c39ec47 Mon Sep 17 00:00:00 2001 From: Aziz Khoury Date: Mon, 22 Feb 2016 14:06:17 -0500 Subject: [PATCH 07/41] sliceSeconds fix --- m3u.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/m3u.js b/m3u.js index 1c5948d..255cefc 100644 --- a/m3u.js +++ b/m3u.js @@ -101,8 +101,11 @@ M3U.prototype.slice = function slice(start, end) { var len = m3u.items.PlaylistItem.length; start = !start || start < 0 ? 0 : start; - end = end == null || end > len ? len : end; + if (end == null || end > len) { + end = len; + } + m3u.set('foundEndlist', true); m3u.items.PlaylistItem = m3u.items.PlaylistItem.slice(start, end); return m3u; @@ -128,7 +131,7 @@ M3U.prototype.sliceSeconds = function slice(from, to) { } } - if (total <= to && end == null) { + if (total >= to && end == null) { end = i + 1; return true; } From b0e52bcce0c8eacd427ff9202075913aaa6e0ff6 Mon Sep 17 00:00:00 2001 From: Aziz Khoury Date: Mon, 22 Feb 2016 14:09:44 -0500 Subject: [PATCH 08/41] comment: --- m3u.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/m3u.js b/m3u.js index 255cefc..7adb2fb 100644 --- a/m3u.js +++ b/m3u.js @@ -105,7 +105,9 @@ M3U.prototype.slice = function slice(start, end) { end = len; } + // everytime you slice, the assumption here is to make the outputed playlist look like a VOD m3u.set('foundEndlist', true); + m3u.items.PlaylistItem = m3u.items.PlaylistItem.slice(start, end); return m3u; From 00de3ed4f6a8dbd70d6aa7eba7d818b0b96a8d9d Mon Sep 17 00:00:00 2001 From: Aziz Khoury Date: Mon, 22 Feb 2016 15:05:26 -0500 Subject: [PATCH 09/41] slice fixes --- m3u.js | 42 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 38 insertions(+), 4 deletions(-) diff --git a/m3u.js b/m3u.js index 7adb2fb..9198365 100644 --- a/m3u.js +++ b/m3u.js @@ -123,8 +123,20 @@ M3U.prototype.sliceSeconds = function slice(from, to) { throw 'target `to` value, if truthy, must be greater than the `from` value'; } + var duration = this.totalDuration(); + if (util.isNumber(from) && from > duration) { + start = this.items.PlaylistItem.length; + } + + if (util.isNumber(to) && to <= 0) { + end = 0; + } + + var currentIndex = 0; + this.items.PlaylistItem.some(function(item, i) { total += item.properties.duration; + currentIndex = i; if (total >= from && start == null) { start = i; @@ -164,6 +176,20 @@ M3U.prototype.sliceDates = function slice(from, to) { to = new Date(); } + var firstDate = ((this.items.PlaylistItem[0] || {}).properties || {}).date; + var lastDate = ((this.items.PlaylistItem[this.items.PlaylistItem.length - 1] || {}).properties || {}).date; + if (!firstDate || !lastDate) { + throw 'Playlist segments does look like that they have a valid date field, you must specify EXT-X-PROGRAM-DATE-TIME for each segment in order to sliceDate(), or set the date on your own using the beforeItemEmit hook when you setup the parser.'; + } + + if (from > lastDate) { + start = this.items.PlaylistItem.length; + } + + if (to <= firstDate) { + end = 0; + } + if (util.isDate(from) && util.isDate(to) && from > to) { throw 'target `to` date value, if available, must be greater than the `from` date value'; } @@ -173,10 +199,6 @@ M3U.prototype.sliceDates = function slice(from, to) { this.items.PlaylistItem.some(function(item, i) { current = item.properties.date; - if (!current) { - throw 'Playlist segment does not have a date field, you must specify EXT-X-PROGRAM-DATE-TIME for each segment in order to sliceDate()'; - } - if (current >= from && start == null) { start = i; if (to == null) { @@ -201,6 +223,9 @@ M3U.prototype.toString = function toString() { var tagKey = propertyMap.findByKey(key); var tag = tagKey ? tagKey.tag : key; + if (ignoredProperties[key]) { + return; + } if (dataTypes[key] == 'boolean') { output.push('#' + tag); } else { @@ -235,6 +260,9 @@ M3U.prototype.clone = function clone() { M3U.prototype.serialize = function serialize() { var object = { properties: this.properties, items: {} }; + object.properties.totalDuration = this.totalDuration(); + delete object.properties.foundEndlist; + var self = this; Object.keys(this.items).forEach(function(constructor) { object.items[constructor] = self.items[constructor].map(serializeItem); @@ -245,6 +273,8 @@ M3U.prototype.serialize = function serialize() { M3U.unserialize = function unserialize(object) { var m3u = new M3U; m3u.properties = object.properties; + delete m3u.properties.totalDuration; + Object.keys(object.items).forEach(function(constructor) { m3u.items[constructor] = object.items[constructor].map( Item.unserialize.bind(null, M3U[constructor]) @@ -273,6 +303,10 @@ var coerce = { } }; +var ignoredProperties = { + foundEndlist : 1 +}; + var dataTypes = { iframesOnly : 'boolean', targetDuration : 'integer', From 1a8f4c0bc650682d05e620f84014594c64f74024 Mon Sep 17 00:00:00 2001 From: Aziz Khoury Date: Tue, 23 Feb 2016 13:56:31 -0500 Subject: [PATCH 10/41] deep copy when cloning --- m3u.js | 38 +++++++++++++++++++++++++++----------- m3u/AttributeList.js | 2 +- m3u/Item.js | 5 ++++- 3 files changed, 32 insertions(+), 13 deletions(-) diff --git a/m3u.js b/m3u.js index 9198365..6c0d4e7 100644 --- a/m3u.js +++ b/m3u.js @@ -91,7 +91,7 @@ M3U.prototype.merge = function merge(m3u) { return this; }; -M3U.prototype.slice = function slice(start, end) { +M3U.prototype.sliceIndex = M3U.prototype.slice = function slice(start, end) { var m3u = this.clone(); if (start == null && end == null) { @@ -105,8 +105,10 @@ M3U.prototype.slice = function slice(start, end) { end = len; } - // everytime you slice, the assumption here is to make the outputed playlist look like a VOD - m3u.set('foundEndlist', true); + // if live and both start & end were within the length of the stream, make it look like a VOD + if (! m3u.isVOD() && start < len && end < len) { + m3u.set('playlistType', 'VOD'); + } m3u.items.PlaylistItem = m3u.items.PlaylistItem.slice(start, end); @@ -159,6 +161,7 @@ M3U.prototype.sliceDates = function slice(from, to) { var end = null; if (!util.isDate(from) && !util.isDate(to)) { + console.log(from, to); throw 'sliceDates requires that at least 1 of the arguments to be a Date object'; } @@ -223,9 +226,10 @@ M3U.prototype.toString = function toString() { var tagKey = propertyMap.findByKey(key); var tag = tagKey ? tagKey.tag : key; - if (ignoredProperties[key]) { + if (toStringIgnoredProperties[key]) { return; } + if (dataTypes[key] == 'boolean') { output.push('#' + tag); } else { @@ -236,7 +240,7 @@ M3U.prototype.toString = function toString() { if (this.items.PlaylistItem.length) { output.push(this.items.PlaylistItem.map(itemToString).join('\n')); - if ((this.get('foundEndlist') && this.get('playlistType') == null) || this.get('playlistType') === 'VOD') { + if (this.isVOD()) { output.push('#EXT-X-ENDLIST'); } } else { @@ -254,16 +258,28 @@ M3U.prototype.toString = function toString() { return output.join('\n') + '\n'; }; +M3U.prototype.isVOD = function clone() { + return this.get('foundEndlist') || this.get('playlistType') === 'VOD'; +}; + +M3U.prototype.isLive = function clone() { + return !this.isVOD(); +}; + M3U.prototype.clone = function clone() { return M3U.unserialize(this.serialize()); }; -M3U.prototype.serialize = function serialize() { - var object = { properties: this.properties, items: {} }; +M3U.prototype.toJSON = function toJSON() { + var object = this.serialize(); object.properties.totalDuration = this.totalDuration(); - delete object.properties.foundEndlist; + return object; +}; - var self = this; +M3U.prototype.serialize = function serialize() { + var object = { properties: JSON.parse(JSON.stringify(this.properties)), items: {} }; + + var self = this; Object.keys(this.items).forEach(function(constructor) { object.items[constructor] = self.items[constructor].map(serializeItem); }); @@ -303,8 +319,8 @@ var coerce = { } }; -var ignoredProperties = { - foundEndlist : 1 +var toStringIgnoredProperties = { + foundEndlist : true }; var dataTypes = { diff --git a/m3u/AttributeList.js b/m3u/AttributeList.js index e32b6d6..ab8028e 100644 --- a/m3u/AttributeList.js +++ b/m3u/AttributeList.js @@ -56,7 +56,7 @@ AttributeList.prototype.toString = function toString() { }; AttributeList.prototype.serialize = function serialize() { - return this.attributes; + return JSON.parse(JSON.stringify(this.attributes)); }; AttributeList.unserialize = function unserialize(object) { diff --git a/m3u/Item.js b/m3u/Item.js index 9c4f56e..41ef65b 100644 --- a/m3u/Item.js +++ b/m3u/Item.js @@ -31,9 +31,12 @@ Item.prototype.set = function set(key, value) { }; Item.prototype.serialize = function serialize() { + var attrs = JSON.parse(JSON.stringify(this.properties)); + attrs.date = attrs.date ? new Date(attrs.date) : attrs.date; + return { attributes : this.attributes.serialize(), - properties : this.properties + properties : attrs } }; From dec75e75c6ba71bc34d79d5ec137a6cab3beb0d5 Mon Sep 17 00:00:00 2001 From: Aziz Khoury Date: Tue, 23 Feb 2016 17:34:46 -0500 Subject: [PATCH 11/41] support lax mode, less restrictive m3u8 playlist parsing to enable merging a playlist with a tail of another --- parser.js | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/parser.js b/parser.js index 50f2c35..af23fb3 100644 --- a/parser.js +++ b/parser.js @@ -34,14 +34,23 @@ m3uParser.createStream = function(options) { m3uParser.prototype.parse = function parse(line) { line = line.trim(); + if (this.linesRead == 0) { - if (line != '#EXTM3U') { + var extm3uSkipped = false; + + if (line != '#EXTM3U' && !this.options.lax) { return this.emit('error', new Error( 'Non-valid M3U file. First line: ' + line )); } + if (line != '#EXTM3U' && this.options.lax) { + extm3uSkipped = true; + } this.linesRead++; - return true; + + if (!extm3uSkipped) { + return true; + } } if (['', '#EXT-X-ENDLIST'].indexOf(line) > -1) { From 42e216405394f97ccd40aa04c17000c3be57599f Mon Sep 17 00:00:00 2001 From: Aziz Khoury Date: Tue, 23 Feb 2016 17:35:33 -0500 Subject: [PATCH 12/41] real unique merge support, vs the older-merge which behaved just like concat, also added concat ability --- m3u.js | 34 +++++++++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/m3u.js b/m3u.js index 6c0d4e7..a35415d 100644 --- a/m3u.js +++ b/m3u.js @@ -81,13 +81,41 @@ M3U.prototype.totalDuration = function totalDuration() { }, 0); }; -M3U.prototype.merge = function merge(m3u) { +M3U.prototype.concat = function concat(m3u) { if (m3u.get('targetDuration') > this.get('targetDuration')) { this.set('targetDuration', m3u.get('targetDuration')); } - m3u.items.PlaylistItem[0].set('discontinuity', true); + + if (m3u.items.PlaylistItem[0]) { + m3u.items.PlaylistItem[0].set('discontinuity', true); + } + this.items.PlaylistItem = this.items.PlaylistItem.concat(m3u.items.PlaylistItem); + return this; +}; + +M3U.prototype.merge = function merge(m3u) { + var uri0 = ((m3u.items.PlaylistItem[0] || {}).properties || {}).uri; + + this.concat(m3u); + + var segments = this.items.PlaylistItem; + for(var i = 0; i < segments.length; ++i) { + for(var j= i + 1; j < segments.length; ++j) { + if(segments[i].properties.uri == segments[j].properties.uri) { + if (uri0 == segments[j].properties.uri) { + segments[i].set('discontinuity', true); + } + segments.splice(j--, 1); + } + } + } + + if (m3u.get('foundEndlist')) { + this.set('foundEndlist', true); + } + this.items.PlaylistItem = segments; return this; }; @@ -293,7 +321,7 @@ M3U.unserialize = function unserialize(object) { Object.keys(object.items).forEach(function(constructor) { m3u.items[constructor] = object.items[constructor].map( - Item.unserialize.bind(null, M3U[constructor]) + Item.unserialize.bind(null, M3U[constructor]) ); }); return m3u; From bc701ebaffd4a66c90b3bc37fa19de5433476a0a Mon Sep 17 00:00:00 2001 From: Aziz Khoury Date: Wed, 24 Feb 2016 13:39:12 -0500 Subject: [PATCH 13/41] typos --- m3u.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/m3u.js b/m3u.js index a35415d..74ccd91 100644 --- a/m3u.js +++ b/m3u.js @@ -119,7 +119,7 @@ M3U.prototype.merge = function merge(m3u) { return this; }; -M3U.prototype.sliceIndex = M3U.prototype.slice = function slice(start, end) { +M3U.prototype.slice = M3U.prototype.sliceIndex = function slice(start, end) { var m3u = this.clone(); if (start == null && end == null) { @@ -286,19 +286,19 @@ M3U.prototype.toString = function toString() { return output.join('\n') + '\n'; }; -M3U.prototype.isVOD = function clone() { +M3U.prototype.isVOD = function isVOD () { return this.get('foundEndlist') || this.get('playlistType') === 'VOD'; }; -M3U.prototype.isLive = function clone() { +M3U.prototype.isLive = function isLive () { return !this.isVOD(); }; -M3U.prototype.clone = function clone() { +M3U.prototype.clone = function clone () { return M3U.unserialize(this.serialize()); }; -M3U.prototype.toJSON = function toJSON() { +M3U.prototype.toJSON = function toJSON () { var object = this.serialize(); object.properties.totalDuration = this.totalDuration(); return object; From f743636c290381e61c88ff692d29bfba05baf24a Mon Sep 17 00:00:00 2001 From: Aziz Khoury Date: Thu, 25 Feb 2016 16:05:47 -0500 Subject: [PATCH 14/41] adding tests + comments --- m3u.js | 4 ++ test/m3u.test.js | 113 ++++++++++++++++++++++++++++++++++++++++++-- test/parser.test.js | 69 +++++++++++++++++++++++++-- 3 files changed, 177 insertions(+), 9 deletions(-) diff --git a/m3u.js b/m3u.js index 74ccd91..e6c6750 100644 --- a/m3u.js +++ b/m3u.js @@ -81,6 +81,8 @@ M3U.prototype.totalDuration = function totalDuration() { }, 0); }; + +// todo: thought: I think concat() should return a clone, not self mutate. M3U.prototype.concat = function concat(m3u) { if (m3u.get('targetDuration') > this.get('targetDuration')) { this.set('targetDuration', m3u.get('targetDuration')); @@ -94,6 +96,8 @@ M3U.prototype.concat = function concat(m3u) { return this; }; +// todo: thought: I think merge() should return a clone, not self mutate. +// this one would break backward compatibility though M3U.prototype.merge = function merge(m3u) { var uri0 = ((m3u.items.PlaylistItem[0] || {}).properties || {}).uri; diff --git a/test/m3u.test.js b/test/m3u.test.js index 13df900..5630056 100644 --- a/test/m3u.test.js +++ b/test/m3u.test.js @@ -117,8 +117,8 @@ describe('m3u', function() { }); }); - describe('#merge', function() { - it('should merge PlaylistItems from two m3us, creating a discontinuity', function() { + describe('#concat', function() { + it('should concat PlaylistItems from two m3us, creating a discontinuity', function() { var m3u1 = getM3u(); m3u1.addPlaylistItem({}); @@ -137,6 +137,42 @@ describe('m3u', function() { }).length.should.eql(1); }); + it('should use the largest targetDuration', function() { + var m3u1 = getM3u(); + m3u1.set('targetDuration', 10); + m3u1.addPlaylistItem({}); + + var m3u2 = getM3u(); + m3u2.set('targetDuration', 11); + m3u2.addPlaylistItem({}); + m3u1.concat(m3u2); + m3u1.get('targetDuration').should.eql(11); + }); + }); + + describe('#merge', function() { + it('should uniquely merge PlaylistItems from two m3us, creating a discontinuity', function() { + var m3u1 = getM3u(); + + m3u1.addPlaylistItem({uri: 'a'}); + m3u1.addPlaylistItem({uri: 'b'}); + m3u1.addPlaylistItem({uri: 'c'}); + + var m3u2 = getM3u(); + m3u2.addPlaylistItem({uri: 'c'}); + m3u2.addPlaylistItem({uri: 'd'}); + + var itemWithDiscontinuity = m3u2.items.PlaylistItem[0]; + m3u1.merge(m3u2); + + itemWithDiscontinuity.get('discontinuity').should.be.true; + m3u1.items.PlaylistItem.filter(function(item) { + return item.get('discontinuity'); + }).length.should.eql(1); + + m3u1.items.PlaylistItem.length.should.eql(4); + }); + it('should use the largest targetDuration', function() { var m3u1 = getM3u(); m3u1.set('targetDuration', 10); @@ -150,6 +186,53 @@ describe('m3u', function() { }); }); + describe('#slice || #sliceIndex', function() { + it('should slice from 1 index to another', function() { + var m3u1 = getM3u(); + + m3u1.addPlaylistItem({}); + m3u1.addPlaylistItem({}); + m3u1.addPlaylistItem({}); + m3u1.addPlaylistItem({}); + + var m3u2 = m3u1.slice(1, 3); + m3u2.items.PlaylistItem.length.should.eql(2); + + }); + }); + + describe('#sliceSeconds', function() { + it('should sliceSeconds from a specific `second` to another', function() { + var m3u1 = getM3u(); + + m3u1.addPlaylistItem({duration: 5}); + m3u1.addPlaylistItem({duration: 5}); + m3u1.addPlaylistItem({duration: 5}); + m3u1.addPlaylistItem({duration: 5}); + + var m3u2 = m3u1.sliceSeconds(5, 15); + m3u2.items.PlaylistItem.length.should.eql(3); + + }); + }); + + describe('#sliceDates', function() { + it('should sliceDates from a date to another', function() { + var m3u1 = getM3u(); + + var ms0 = +new Date(); + + m3u1.addPlaylistItem({date: new Date(ms0)}); + m3u1.addPlaylistItem({date: new Date(ms0 + 5000)}); + m3u1.addPlaylistItem({date: new Date(ms0 + 10000)}); + m3u1.addPlaylistItem({date: new Date(ms0 + 15000)}); + + var m3u2 = m3u1.sliceDates(new Date(ms0 + 5000), new Date(ms0 + 10001)); + m3u2.items.PlaylistItem.length.should.eql(2); + + }); + }); + describe('#serialize', function(done) { it('should return an object containing items and properties', function(done) { getVariantM3U(function(error, m3u) { @@ -183,6 +266,28 @@ describe('m3u', function() { }); }); + describe('clone', function() { + it('should return a new M3U object with the same items and properties', function() { + var item = new M3U.PlaylistItem({ key: 'uri', value: '/path' }); + var data = { + properties: { + targetDuration: 10 + }, + items: { + PlaylistItem: [ item.serialize() ] + } + }; + var m3u = M3U.unserialize(data); + m3u.properties.should.eql(data.properties); + item.should.eql(m3u.items.PlaylistItem[0]); + + var m3u1 = m3u.clone(); + m3u1.properties.should.eql(data.properties); + item.should.eql(m3u1.items.PlaylistItem[0]); + + }); + }); + describe('writeVOD', function() { it('should return a string ending with #EXT-X-ENDLIST', function() { var m3u1 = getM3u(); @@ -208,9 +313,7 @@ describe('m3u', function() { }); function getM3u() { - var m3u = M3U.create(); - - return m3u; + return M3U.create(); } function getVariantM3U(callback) { diff --git a/test/parser.test.js b/test/parser.test.js index 4ffda67..a282e60 100644 --- a/test/parser.test.js +++ b/test/parser.test.js @@ -13,6 +13,54 @@ describe('parser', function() { parser.write('NOT VALID\n'); }); + describe('#options.lax', function() { + it('should forgive if #EXTM3U is not there, this is useful for mergin large and live tails of m3u8', function(done) { + var parser = getParser({lax: true}); + var text = '' + // + '#EXTM3U\n' + + '#EXT-X-TARGETDURATION:10\n' + + '#EXT-X-VERSION:4\n' + + '#EXTINF:10,\n' + + '1.ts\n' + + '#EXTINF:10,\n' + + '2.ts\n'; + + parser.on('m3u', function() { + done(); + }); + + parser.write(text); + parser.end(); + }); + }); + + describe('#options.beforeItemEmit', function() { + it('should call beforeItemEmit hook before item emit', function(done) { + var called = 0; + + var parser = getParser({beforeItemEmit: function() { + called++; + }}); + + var text = '' + + '#EXTM3U\n' + + '#EXT-X-TARGETDURATION:10\n' + + '#EXT-X-VERSION:4\n' + + '#EXTINF:10,\n' + + '1.ts\n' + + '#EXTINF:10,\n' + + '2.ts\n'; + + parser.on('m3u', function() { + called.should.eql(2); + done(); + }); + + parser.write(text); + parser.end(); + }); + }); + describe('#parseLine', function() { it('should call known tags', function() { var parser = getParser(); @@ -82,6 +130,21 @@ describe('parser', function() { }); }); + describe('#EXT-X-PROGRAM-DATE-TIME', function() { + it('should parse date value on subsequent playlist item', function() { + var parser = getParser(); + + var d = (new Date()).toISOString(); + + parser['EXT-X-PROGRAM-DATE-TIME'](d); + parser.EXTINF('4.5,some title'); + parser.currentItem.constructor.name.should.eql('PlaylistItem'); + parser.currentItem.get('duration').should.eql(4.5); + parser.currentItem.get('title').should.eql('some title'); + parser.currentItem.get('date').toISOString().should.eql(d); + }); + }); + describe('#EXT-X-STREAM-INF', function() { it('should create a new Stream item', function() { var parser = getParser(); @@ -126,8 +189,6 @@ describe('parser', function() { }); }); -function getParser() { - var parser = m3u8.createStream(); - - return parser; +function getParser(options) { + return m3u8.createStream(options); } From 69c7db18eecfdbeb803720757b9c3746d0cc5d86 Mon Sep 17 00:00:00 2001 From: Aziz Khoury Date: Thu, 25 Feb 2016 16:33:50 -0500 Subject: [PATCH 15/41] 0.0.7 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 886673b..5d465ea 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name" : "m3u8", - "version" : "0.0.6", + "version" : "0.0.7", "description" : "streaming m3u8 parser for Apple's HTTP Live Streaming protocol", "main" : "./parser.js", "keywords" : [ From 4b50878c482135e0414f35448eb19e9452b7ba63 Mon Sep 17 00:00:00 2001 From: Aziz Khoury Date: Wed, 2 Mar 2016 20:38:45 -0500 Subject: [PATCH 16/41] mainly added m3u.mergeDates(), along with its dependencies m3u.sortDates(), m3u.findDateGaps() and m3u.insertPlaylistItemsAfter() --- m3u.js | 144 +++++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 139 insertions(+), 5 deletions(-) diff --git a/m3u.js b/m3u.js index e6c6750..19a4393 100644 --- a/m3u.js +++ b/m3u.js @@ -35,7 +35,6 @@ M3U.prototype.set = function setProperty(key, value) { M3U.prototype.addItem = function addItem(item) { this.items[item.constructor.name].push(item); - return this; }; @@ -43,6 +42,31 @@ M3U.prototype.addPlaylistItem = function addPlaylistItem(data) { this.items.PlaylistItem.push(M3U.PlaylistItem.create(data)); }; +M3U.prototype.insertPlaylistItemsAfter = function insertPlaylistItemsAfter (newItems, afterItem) { + var index = this.items.PlaylistItem.length; + + if (!(afterItem instanceof M3U.PlaylistItem)) { + afterItem = M3U.PlaylistItem.create(afterItem); + } + + newItems = [].concat(newItems).map(function(newItem) { + if (!(newItem instanceof M3U.PlaylistItem)) { + return M3U.PlaylistItem.create(newItem); + } + return newItem; + }); + + this.items.PlaylistItem.some(function(item, i) { + if (item.properties.uri == afterItem.properties.uri) { + index = i; + return true; + } + }); + + this.items.PlaylistItem = this.items.PlaylistItem.slice(0, index + 1).concat(newItems).concat(this.items.PlaylistItem.slice(index + 1)); + return this; +}; + M3U.prototype.removePlaylistItem = function removePlaylistItem(index) { if (index < this.items.PlaylistItem.length && index >= 0) { this.items.PlaylistItem.splice(index, 1); @@ -99,11 +123,15 @@ M3U.prototype.concat = function concat(m3u) { // todo: thought: I think merge() should return a clone, not self mutate. // this one would break backward compatibility though M3U.prototype.merge = function merge(m3u) { - var uri0 = ((m3u.items.PlaylistItem[0] || {}).properties || {}).uri; + if (m3u.get('mediaSequence') < this.get('mediaSequence')) { + this.set('mediaSequence', m3u.get('mediaSequence')); + } + var uri0 = ((m3u.items.PlaylistItem[0] || {}).properties || {}).uri; this.concat(m3u); var segments = this.items.PlaylistItem; + for(var i = 0; i < segments.length; ++i) { for(var j= i + 1; j < segments.length; ++j) { if(segments[i].properties.uri == segments[j].properties.uri) { @@ -119,10 +147,96 @@ M3U.prototype.merge = function merge(m3u) { this.set('foundEndlist', true); } - this.items.PlaylistItem = segments; return this; }; +// todo: thought: I think mergeDates() should return a clone, not self mutate. +M3U.prototype.mergeDates = function mergeDates (m3uB, options) { + options = options || {}; + + var clone = this.clone(); + clone.merge(m3uB); + clone.sortDates(); + + var dateA0, dateAN, m3uPre, m3uPost; + if (this.items.PlaylistItem.length) { + dateA0 = this.items.PlaylistItem[0].get('date'); + dateAN = this.items.PlaylistItem[this.items.PlaylistItem.length - 1].get('date'); + } + m3uPre = dateA0 ? clone.sliceDates(null, new Date((+new Date(dateA0)) - 1000)) : createM3U(); // -1 sec to make it exclusive + m3uPost = dateAN ? clone.sliceDates(new Date((+new Date(dateAN)) + 1000)) : createM3U(); // +1 sec to make it exclusive + + + var gaps = this.findDateGaps(options); + gaps.forEach(function(gap) { + var m3u8Gap = m3uB.sliceDates(new Date(gap.starts), new Date(gap.ends)); + + if (m3u8Gap.items.PlaylistItem.length) { + m3u8Gap.items.PlaylistItem[0] && m3uPost.items.PlaylistItem[0].set('discontinuity', true); + gap.beforeItem.set('discontinuity', true); + this.insertPlaylistItemsAfter(m3u8Gap.items.PlaylistItem, gap.afterItem); + } + }.bind(this)); + + if (m3uPre.items.PlaylistItem.length) { + this.items.PlaylistItem[0] && this.items.PlaylistItem[0].set('discontinuity', true); + } + + if (m3uPost.items.PlaylistItem.length) { + m3uPost.items.PlaylistItem[0].set('discontinuity', true); + } + + this.items.PlaylistItem = m3uPre.concat(this).concat(m3uPost).items.PlaylistItem; + + return this; +}; + +M3U.prototype.findDateGaps = function findDateGaps (options) { + options = options || {}; + options.msMargin = options.msMargin == null ? 1500 : options.msMargin; + + var gaps = []; + var segments = this.items.PlaylistItem; + var that = this; + + segments.forEach(function(item, i) { + var itemNext = segments[i + 1]; + + var se = itemStartsEnds(item); + var seNext = itemStartsEnds(itemNext); + + if (seNext && (seNext.starts - se.ends > options.msMargin)) { + var duration = (seNext.starts - se.ends) / 1000; + gaps.push({ + index: i + 1, + starts: se.ends, + ends: seNext.starts, + duration: duration, + approximateMissingItems: duration / that.get('targetDuration'), + beforeItem: itemNext, + afterItem: item + }); + } + }); + + return gaps; +}; + +M3U.prototype.sortDates = function sortDates () { + this.items.PlaylistItem.sort(function(playlistItem1, playlistItem2) { + var d1 = playlistItem1.properties.date; + var d2 = playlistItem2.properties.date; + + if (!util.isDate(d1) || !d1 || !util.isDate(d2) || !d2) { + throw datesError; + } + + return d1 < d2 ? -1 : d1 > d2 ? 1 : 0; + }); + return this; +}; + + M3U.prototype.slice = M3U.prototype.sliceIndex = function slice(start, end) { var m3u = this.clone(); @@ -193,7 +307,6 @@ M3U.prototype.sliceDates = function slice(from, to) { var end = null; if (!util.isDate(from) && !util.isDate(to)) { - console.log(from, to); throw 'sliceDates requires that at least 1 of the arguments to be a Date object'; } @@ -213,8 +326,9 @@ M3U.prototype.sliceDates = function slice(from, to) { var firstDate = ((this.items.PlaylistItem[0] || {}).properties || {}).date; var lastDate = ((this.items.PlaylistItem[this.items.PlaylistItem.length - 1] || {}).properties || {}).date; + if (!firstDate || !lastDate) { - throw 'Playlist segments does look like that they have a valid date field, you must specify EXT-X-PROGRAM-DATE-TIME for each segment in order to sliceDate(), or set the date on your own using the beforeItemEmit hook when you setup the parser.'; + throw datesError; } if (from > lastDate) { @@ -331,6 +445,23 @@ M3U.unserialize = function unserialize(object) { return m3u; }; +function itemStartsEnds(item) { + if (!item) { + return; + } + + var date = item.get('date'); + if (!util.isDate(date) || !date) { + throw datesError; + } + + var starts = date.getTime(); + return { + starts: starts, + ends: starts + (item.get('duration') * 1000) + } +} + function itemToString(item) { return item.toString(); } @@ -371,6 +502,8 @@ var propertyMap = [ { tag: 'EXT-X-VERSION', key: 'version' } ]; +var datesError = 'Playlist segments do not look like that they have a valid date fields, you must specify EXT-X-PROGRAM-DATE-TIME for each segment in order to sliceDate(), or set the date on your own using the beforeItemEmit hook when you setup the parser.'; + propertyMap.findByTag = function findByTag(tag) { return propertyMap[propertyMap.map(function(tagKey) { return tagKey.tag; @@ -382,3 +515,4 @@ propertyMap.findByKey = function findByKey(key) { return tagKey.key; }).indexOf(key)]; }; + From 84d27ef24350770023244d57679e74deaad699dc Mon Sep 17 00:00:00 2001 From: Aziz Khoury Date: Thu, 3 Mar 2016 15:44:57 -0500 Subject: [PATCH 17/41] m3u.merge() is back the way it was, to be backward compatible, also renamed all new functions to be more explicit, also added test for m3u.mergeByDate() --- m3u.js | 146 +++++++++++++++++++++-------------------------- test/m3u.test.js | 86 +++++++++++++++++++++++----- 2 files changed, 139 insertions(+), 93 deletions(-) diff --git a/m3u.js b/m3u.js index 19a4393..e343b2a 100644 --- a/m3u.js +++ b/m3u.js @@ -105,32 +105,40 @@ M3U.prototype.totalDuration = function totalDuration() { }, 0); }; +M3U.prototype.concat = function concat (m3u) { + var clone = this.clone(); -// todo: thought: I think concat() should return a clone, not self mutate. -M3U.prototype.concat = function concat(m3u) { - if (m3u.get('targetDuration') > this.get('targetDuration')) { - this.set('targetDuration', m3u.get('targetDuration')); + if (m3u.get('targetDuration') > clone.get('targetDuration')) { + clone.set('targetDuration', m3u.get('targetDuration')); } if (m3u.items.PlaylistItem[0]) { m3u.items.PlaylistItem[0].set('discontinuity', true); } - this.items.PlaylistItem = this.items.PlaylistItem.concat(m3u.items.PlaylistItem); + clone.items.PlaylistItem = clone.items.PlaylistItem.concat(m3u.items.PlaylistItem); + + return clone; +}; + +// backward-compatible merge function, that just concats and mutates self +// todo: remove this, since it's really a merge, it's just a concat() +M3U.prototype.merge = function merge (m3u) { + var clone = this.concat(m3u); + this.items.PlaylistItem = clone.items.PlaylistItem; + this.set('targetDuration', clone.get('targetDuration')); return this; }; -// todo: thought: I think merge() should return a clone, not self mutate. -// this one would break backward compatibility though -M3U.prototype.merge = function merge(m3u) { - if (m3u.get('mediaSequence') < this.get('mediaSequence')) { - this.set('mediaSequence', m3u.get('mediaSequence')); - } +M3U.prototype.mergeByUri = function mergeByUri (m3u) { + var clone = this.concat(m3u); + if (m3u.get('mediaSequence') < clone.get('mediaSequence')) { + clone.set('mediaSequence', m3u.get('mediaSequence')); + } var uri0 = ((m3u.items.PlaylistItem[0] || {}).properties || {}).uri; - this.concat(m3u); - var segments = this.items.PlaylistItem; + var segments = clone.items.PlaylistItem; for(var i = 0; i < segments.length; ++i) { for(var j= i + 1; j < segments.length; ++j) { @@ -144,51 +152,47 @@ M3U.prototype.merge = function merge(m3u) { } if (m3u.get('foundEndlist')) { - this.set('foundEndlist', true); + clone.set('foundEndlist', true); } - return this; + return clone; }; -// todo: thought: I think mergeDates() should return a clone, not self mutate. -M3U.prototype.mergeDates = function mergeDates (m3uB, options) { - options = options || {}; +M3U.prototype.mergeByDate = function mergeByDate (m3u, options) { + var clone = this.clone(m3u); - var clone = this.clone(); - clone.merge(m3uB); - clone.sortDates(); + options = options || {}; + var len = clone.items.PlaylistItem.length; var dateA0, dateAN, m3uPre, m3uPost; - if (this.items.PlaylistItem.length) { - dateA0 = this.items.PlaylistItem[0].get('date'); - dateAN = this.items.PlaylistItem[this.items.PlaylistItem.length - 1].get('date'); - } - m3uPre = dateA0 ? clone.sliceDates(null, new Date((+new Date(dateA0)) - 1000)) : createM3U(); // -1 sec to make it exclusive - m3uPost = dateAN ? clone.sliceDates(new Date((+new Date(dateAN)) + 1000)) : createM3U(); // +1 sec to make it exclusive + if (len) { + dateA0 = clone.items.PlaylistItem[0].get('date'); + dateAN = clone.items.PlaylistItem[clone.items.PlaylistItem.length - 1].get('date'); + } + m3uPre = dateA0 ? m3u.sliceByDate(null, new Date((+new Date(dateA0)) - 1)) : createM3U(); // -1 ms to make it exclusive + m3uPost = dateAN ? m3u.sliceByDate(new Date((+new Date(dateAN)) + 1)) : createM3U(); // +1 ms to make it exclusive - var gaps = this.findDateGaps(options); + var gaps = clone.findDateGaps(options); gaps.forEach(function(gap) { - var m3u8Gap = m3uB.sliceDates(new Date(gap.starts), new Date(gap.ends)); + var m3u8Gap = m3u.sliceByDate(new Date(gap.starts), new Date(gap.ends)); if (m3u8Gap.items.PlaylistItem.length) { m3u8Gap.items.PlaylistItem[0] && m3uPost.items.PlaylistItem[0].set('discontinuity', true); gap.beforeItem.set('discontinuity', true); - this.insertPlaylistItemsAfter(m3u8Gap.items.PlaylistItem, gap.afterItem); + clone.insertPlaylistItemsAfter(m3u8Gap.items.PlaylistItem, gap.afterItem); } - }.bind(this)); + }); if (m3uPre.items.PlaylistItem.length) { - this.items.PlaylistItem[0] && this.items.PlaylistItem[0].set('discontinuity', true); + clone.items.PlaylistItem[0] && clone.items.PlaylistItem[0].set('discontinuity', true); } if (m3uPost.items.PlaylistItem.length) { m3uPost.items.PlaylistItem[0].set('discontinuity', true); } - this.items.PlaylistItem = m3uPre.concat(this).concat(m3uPost).items.PlaylistItem; - - return this; + return m3uPre.concat(clone).concat(m3uPost) }; M3U.prototype.findDateGaps = function findDateGaps (options) { @@ -222,22 +226,7 @@ M3U.prototype.findDateGaps = function findDateGaps (options) { return gaps; }; -M3U.prototype.sortDates = function sortDates () { - this.items.PlaylistItem.sort(function(playlistItem1, playlistItem2) { - var d1 = playlistItem1.properties.date; - var d2 = playlistItem2.properties.date; - - if (!util.isDate(d1) || !d1 || !util.isDate(d2) || !d2) { - throw datesError; - } - - return d1 < d2 ? -1 : d1 > d2 ? 1 : 0; - }); - return this; -}; - - -M3U.prototype.slice = M3U.prototype.sliceIndex = function slice(start, end) { +M3U.prototype.sliceByIndex = M3U.prototype.slice = function sliceByIndex (start, end) { var m3u = this.clone(); if (start == null && end == null) { @@ -261,7 +250,7 @@ M3U.prototype.slice = M3U.prototype.sliceIndex = function slice(start, end) { return m3u; }; -M3U.prototype.sliceSeconds = function slice(from, to) { +M3U.prototype.sliceBySeconds = function sliceBySeconds (from, to) { var start = null; var end = null; @@ -299,15 +288,15 @@ M3U.prototype.sliceSeconds = function slice(from, to) { } }); - return this.slice(start, end); + return this.sliceByIndex(start, end); }; -M3U.prototype.sliceDates = function slice(from, to) { +M3U.prototype.sliceByDate = function sliceByDate (from, to) { var start = null; var end = null; if (!util.isDate(from) && !util.isDate(to)) { - throw 'sliceDates requires that at least 1 of the arguments to be a Date object'; + throw new Error('at least 1 of the arguments needs to be a Date object'); } if (util.isNumber(from)) { @@ -316,19 +305,19 @@ M3U.prototype.sliceDates = function slice(from, to) { to = new Date(from.getTime() + to * 1000); } + var firstDate = ((this.items.PlaylistItem[0] || {}).properties || {}).date; + var lastDate = ((this.items.PlaylistItem[this.items.PlaylistItem.length - 1] || {}).properties || {}).date; + + if (!firstDate || !lastDate) { + throw new Error('Playlist segments do not look like that they have a valid date fields, you must specify EXT-X-PROGRAM-DATE-TIME for each segment in order to sliceDate(), or set the date on your own using the beforeItemEmit hook when you setup the parser.'); + } + if (!from) { from = new Date(0); } if (!to) { - to = new Date(); - } - - var firstDate = ((this.items.PlaylistItem[0] || {}).properties || {}).date; - var lastDate = ((this.items.PlaylistItem[this.items.PlaylistItem.length - 1] || {}).properties || {}).date; - - if (!firstDate || !lastDate) { - throw datesError; + to = new Date(lastDate.getTime() + 1); } if (from > lastDate) { @@ -340,7 +329,7 @@ M3U.prototype.sliceDates = function slice(from, to) { } if (util.isDate(from) && util.isDate(to) && from > to) { - throw 'target `to` date value, if available, must be greater than the `from` date value'; + throw new Error('target `to` date value, if available, must be greater than the `from` date value'); } var current; @@ -361,10 +350,10 @@ M3U.prototype.sliceDates = function slice(from, to) { } }); - return this.slice(start, end); + return this.sliceByIndex(start, end); }; -M3U.prototype.toString = function toString() { +M3U.prototype.toString = function toString () { var self = this; var output = ['#EXTM3U']; @@ -422,7 +411,7 @@ M3U.prototype.toJSON = function toJSON () { return object; }; -M3U.prototype.serialize = function serialize() { +M3U.prototype.serialize = function serialize () { var object = { properties: JSON.parse(JSON.stringify(this.properties)), items: {} }; var self = this; @@ -432,7 +421,7 @@ M3U.prototype.serialize = function serialize() { return object; }; -M3U.unserialize = function unserialize(object) { +M3U.unserialize = function unserialize (object) { var m3u = new M3U; m3u.properties = object.properties; delete m3u.properties.totalDuration; @@ -445,14 +434,14 @@ M3U.unserialize = function unserialize(object) { return m3u; }; -function itemStartsEnds(item) { +function itemStartsEnds (item) { if (!item) { return; } var date = item.get('date'); if (!util.isDate(date) || !date) { - throw datesError; + throw new Error('Playlist segments do not look like that they have a valid date fields, you must specify EXT-X-PROGRAM-DATE-TIME for each segment in order to sliceDate(), or set the date on your own using the beforeItemEmit hook when you setup the parser.'); } var starts = date.getTime(); @@ -462,22 +451,22 @@ function itemStartsEnds(item) { } } -function itemToString(item) { +function itemToString (item) { return item.toString(); } -function serializeItem(item) { +function serializeItem (item) { return item.serialize(); } var coerce = { - boolean: function coerceBoolean(value) { + boolean: function coerceBoolean (value) { return true; }, - integer: function coerceInteger(value) { + integer: function coerceInteger (value) { return parseInt(value, 10); }, - unknown: function coerceUnknown(value) { + unknown: function coerceUnknown (value) { return value; } }; @@ -502,17 +491,14 @@ var propertyMap = [ { tag: 'EXT-X-VERSION', key: 'version' } ]; -var datesError = 'Playlist segments do not look like that they have a valid date fields, you must specify EXT-X-PROGRAM-DATE-TIME for each segment in order to sliceDate(), or set the date on your own using the beforeItemEmit hook when you setup the parser.'; - -propertyMap.findByTag = function findByTag(tag) { +propertyMap.findByTag = function findByTag (tag) { return propertyMap[propertyMap.map(function(tagKey) { return tagKey.tag; }).indexOf(tag)]; }; -propertyMap.findByKey = function findByKey(key) { +propertyMap.findByKey = function findByKey (key) { return propertyMap[propertyMap.map(function(tagKey) { return tagKey.key; }).indexOf(key)]; }; - diff --git a/test/m3u.test.js b/test/m3u.test.js index 5630056..9de0ef3 100644 --- a/test/m3u.test.js +++ b/test/m3u.test.js @@ -4,6 +4,7 @@ var fs = require('fs'), should = require('should'); describe('m3u', function() { + describe('#set', function() { it('should set property on m3u', function() { var m3u = getM3u(); @@ -118,7 +119,7 @@ describe('m3u', function() { }); describe('#concat', function() { - it('should concat PlaylistItems from two m3us, creating a discontinuity', function() { + it('should concat PlaylistItems from two m3us and return a new m3u, creating a discontinuity', function() { var m3u1 = getM3u(); m3u1.addPlaylistItem({}); @@ -130,7 +131,7 @@ describe('m3u', function() { m3u2.addPlaylistItem({}); var itemWithDiscontinuity = m3u2.items.PlaylistItem[0]; - m3u1.merge(m3u2); + m3u1 = m3u1.merge(m3u2); itemWithDiscontinuity.get('discontinuity').should.be.true; m3u1.items.PlaylistItem.filter(function(item) { return item.get('discontinuity'); @@ -145,13 +146,13 @@ describe('m3u', function() { var m3u2 = getM3u(); m3u2.set('targetDuration', 11); m3u2.addPlaylistItem({}); - m3u1.concat(m3u2); + m3u1 = m3u1.concat(m3u2); m3u1.get('targetDuration').should.eql(11); }); }); describe('#merge', function() { - it('should uniquely merge PlaylistItems from two m3us, creating a discontinuity', function() { + it('should just merge (as in concat) PlaylistItems from two m3us by self mutating the current m3u, creating a discontinuity', function() { var m3u1 = getM3u(); m3u1.addPlaylistItem({uri: 'a'}); @@ -170,7 +171,7 @@ describe('m3u', function() { return item.get('discontinuity'); }).length.should.eql(1); - m3u1.items.PlaylistItem.length.should.eql(4); + m3u1.items.PlaylistItem.length.should.eql(5); }); it('should use the largest targetDuration', function() { @@ -186,7 +187,66 @@ describe('m3u', function() { }); }); - describe('#slice || #sliceIndex', function() { + describe('#mergeByUri', function() { + it('should uniquely merge PlaylistItems from two m3us using URIs, creating a discontinuity', function() { + var m3u1 = getM3u(); + + m3u1.addPlaylistItem({uri: 'a'}); + m3u1.addPlaylistItem({uri: 'b'}); + m3u1.addPlaylistItem({uri: 'c'}); + + var m3u2 = getM3u(); + m3u2.addPlaylistItem({uri: 'c'}); + m3u2.addPlaylistItem({uri: 'd'}); + + var itemWithDiscontinuity = m3u2.items.PlaylistItem[0]; + m3u1 = m3u1.mergeByUri(m3u2); + + itemWithDiscontinuity.get('discontinuity').should.be.true; + m3u1.items.PlaylistItem.filter(function(item) { + return item.get('discontinuity'); + }).length.should.eql(1); + + m3u1.items.PlaylistItem.length.should.eql(4); + }); + }); + + describe('#mergeByDate', function() { + it('should uniquely merge PlaylistItems from two m3us using Dates, creating some discontinuities', function() { + var m3u1 = getM3u(); + var ms0 = +new Date() - (24 * 60 * 60 * 1000); + + m3u1.addPlaylistItem({uri: 'a.3', date: new Date(ms0)}); + m3u1.addPlaylistItem({uri: 'a.4', date: new Date(ms0 + 10000)}); + m3u1.addPlaylistItem({uri: 'a.6', date: new Date(ms0 + 30000)}); + + var m3u2 = getM3u(); + m3u2.addPlaylistItem({uri: 'b.1', date: new Date(ms0 - 20000)}); + m3u2.addPlaylistItem({uri: 'b.2', date: new Date(ms0 - 10000)}); + m3u2.addPlaylistItem({uri: 'b.5', date: new Date(ms0 + 20000)}); + m3u2.addPlaylistItem({uri: 'b.6', date: new Date(ms0 + 30000)}); + m3u2.addPlaylistItem({uri: 'b.7', date: new Date(ms0 + 40000)}); + + m3u1 = m3u1.mergeByDate(m3u2); + + m3u1.items.PlaylistItem.length.should.eql(7); + + m3u1.items.PlaylistItem[0].get('uri').should.be.eql('b.1'); + m3u1.items.PlaylistItem[1].get('uri').should.be.eql('b.2'); + m3u1.items.PlaylistItem[2].get('uri').should.be.eql('a.3'); + m3u1.items.PlaylistItem[3].get('uri').should.be.eql('a.4'); + m3u1.items.PlaylistItem[4].get('uri').should.be.eql('b.5'); + m3u1.items.PlaylistItem[5].get('uri').should.be.eql('a.6'); + m3u1.items.PlaylistItem[6].get('uri').should.be.eql('b.7'); + + m3u1.items.PlaylistItem[2].get('discontinuity').should.be.true; +// m3u1.items.PlaylistItem[4].get('discontinuity').should.be.true; + m3u1.items.PlaylistItem[5].get('discontinuity').should.be.true; + m3u1.items.PlaylistItem[6].get('discontinuity').should.be.true; + }); + }); + + describe('#sliceByIndex', function() { it('should slice from 1 index to another', function() { var m3u1 = getM3u(); @@ -195,14 +255,14 @@ describe('m3u', function() { m3u1.addPlaylistItem({}); m3u1.addPlaylistItem({}); - var m3u2 = m3u1.slice(1, 3); + var m3u2 = m3u1.sliceByIndex(1, 3); m3u2.items.PlaylistItem.length.should.eql(2); }); }); - describe('#sliceSeconds', function() { - it('should sliceSeconds from a specific `second` to another', function() { + describe('#sliceBySeconds', function() { + it('should sliceBySeconds from a specific `second` to another', function() { var m3u1 = getM3u(); m3u1.addPlaylistItem({duration: 5}); @@ -210,14 +270,14 @@ describe('m3u', function() { m3u1.addPlaylistItem({duration: 5}); m3u1.addPlaylistItem({duration: 5}); - var m3u2 = m3u1.sliceSeconds(5, 15); + var m3u2 = m3u1.sliceBySeconds(5, 15); m3u2.items.PlaylistItem.length.should.eql(3); }); }); - describe('#sliceDates', function() { - it('should sliceDates from a date to another', function() { + describe('#sliceByDate', function() { + it('should sliceByDate from a date to another', function() { var m3u1 = getM3u(); var ms0 = +new Date(); @@ -227,7 +287,7 @@ describe('m3u', function() { m3u1.addPlaylistItem({date: new Date(ms0 + 10000)}); m3u1.addPlaylistItem({date: new Date(ms0 + 15000)}); - var m3u2 = m3u1.sliceDates(new Date(ms0 + 5000), new Date(ms0 + 10001)); + var m3u2 = m3u1.sliceByDate(new Date(ms0 + 5000), new Date(ms0 + 10001)); m3u2.items.PlaylistItem.length.should.eql(2); }); From 2792d1bc9aec4e41c06a37e2d52135ffe419a036 Mon Sep 17 00:00:00 2001 From: Aziz Khoury Date: Thu, 3 Mar 2016 16:01:11 -0500 Subject: [PATCH 18/41] discontinuity bug fix --- m3u.js | 2 +- test/m3u.test.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/m3u.js b/m3u.js index e343b2a..8d85cef 100644 --- a/m3u.js +++ b/m3u.js @@ -178,7 +178,7 @@ M3U.prototype.mergeByDate = function mergeByDate (m3u, options) { var m3u8Gap = m3u.sliceByDate(new Date(gap.starts), new Date(gap.ends)); if (m3u8Gap.items.PlaylistItem.length) { - m3u8Gap.items.PlaylistItem[0] && m3uPost.items.PlaylistItem[0].set('discontinuity', true); + m3u8Gap.items.PlaylistItem[0] && m3u8Gap.items.PlaylistItem[0].set('discontinuity', true); gap.beforeItem.set('discontinuity', true); clone.insertPlaylistItemsAfter(m3u8Gap.items.PlaylistItem, gap.afterItem); } diff --git a/test/m3u.test.js b/test/m3u.test.js index 9de0ef3..2b3053c 100644 --- a/test/m3u.test.js +++ b/test/m3u.test.js @@ -240,7 +240,7 @@ describe('m3u', function() { m3u1.items.PlaylistItem[6].get('uri').should.be.eql('b.7'); m3u1.items.PlaylistItem[2].get('discontinuity').should.be.true; -// m3u1.items.PlaylistItem[4].get('discontinuity').should.be.true; + m3u1.items.PlaylistItem[4].get('discontinuity').should.be.true; m3u1.items.PlaylistItem[5].get('discontinuity').should.be.true; m3u1.items.PlaylistItem[6].get('discontinuity').should.be.true; }); From e9f56c2bb2dff909e3dd6b58225c45833baa028d Mon Sep 17 00:00:00 2001 From: Aziz Khoury Date: Thu, 3 Mar 2016 16:02:42 -0500 Subject: [PATCH 19/41] var name --- m3u.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/m3u.js b/m3u.js index 8d85cef..ea30ed6 100644 --- a/m3u.js +++ b/m3u.js @@ -175,12 +175,12 @@ M3U.prototype.mergeByDate = function mergeByDate (m3u, options) { var gaps = clone.findDateGaps(options); gaps.forEach(function(gap) { - var m3u8Gap = m3u.sliceByDate(new Date(gap.starts), new Date(gap.ends)); + var m3uGap = m3u.sliceByDate(new Date(gap.starts), new Date(gap.ends)); - if (m3u8Gap.items.PlaylistItem.length) { - m3u8Gap.items.PlaylistItem[0] && m3u8Gap.items.PlaylistItem[0].set('discontinuity', true); + if (m3uGap.items.PlaylistItem.length) { + m3uGap.items.PlaylistItem[0] && m3uGap.items.PlaylistItem[0].set('discontinuity', true); gap.beforeItem.set('discontinuity', true); - clone.insertPlaylistItemsAfter(m3u8Gap.items.PlaylistItem, gap.afterItem); + clone.insertPlaylistItemsAfter(m3uGap.items.PlaylistItem, gap.afterItem); } }); From 49b87095d522f09cb085204a03d3f6fae8769c16 Mon Sep 17 00:00:00 2001 From: Aziz Khoury Date: Thu, 3 Mar 2016 17:04:23 -0500 Subject: [PATCH 20/41] fix vod state after mergeByDate --- m3u.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/m3u.js b/m3u.js index ea30ed6..fc6cce6 100644 --- a/m3u.js +++ b/m3u.js @@ -192,7 +192,16 @@ M3U.prototype.mergeByDate = function mergeByDate (m3u, options) { m3uPost.items.PlaylistItem[0].set('discontinuity', true); } - return m3uPre.concat(clone).concat(m3uPost) + var result = m3uPre.concat(clone).concat(m3uPost); + + var m3uTail = m3uPost.items.PlaylistItem.length ? m3uPost : clone.items.PlaylistItem.length ? clone : m3uPre; + if (m3uTail.isVOD()) { + result.set('playlistType', 'VOD'); + } else { + result.set('playlistType', 'EVENT'); + } + + return result; }; M3U.prototype.findDateGaps = function findDateGaps (options) { From 3ba7418177fe9a3b69dca700f1800f58e8bb88c8 Mon Sep 17 00:00:00 2001 From: Aziz Khoury Date: Fri, 4 Mar 2016 17:31:11 -0500 Subject: [PATCH 21/41] added m3u.sortByDate(), m3u.sortByUri(), m3u.isDateSupported(), fixed minor date slicing bug --- m3u.js | 40 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/m3u.js b/m3u.js index fc6cce6..03f9028 100644 --- a/m3u.js +++ b/m3u.js @@ -204,6 +204,38 @@ M3U.prototype.mergeByDate = function mergeByDate (m3u, options) { return result; }; +M3U.prototype.sortByDate = function sortByDate () { + if (! this.isDateSupported()) { + return this; + } + + this.items.PlaylistItem.sort(function(playlistItem1, playlistItem2) { + var d1 = playlistItem1.properties.date; + var d2 = playlistItem2.properties.date; + + return d1 < d2 ? -1 : d1 > d2 ? 1 : 0; + }); + + return this; +}; + +M3U.prototype.sortByUri = function sortByUri (options) { + options = options || {}; + + this.items.PlaylistItem.sort(function(playlistItem1, playlistItem2) { + var u1 = playlistItem1.properties.uri; + var u2 = playlistItem2.properties.uri; + + if (!options.useFullPath) { + u1 = u1.split('/').pop(); + u2 = u2.split('/').pop(); + } + + return u1 < u2 ? -1 : u1 > u2 ? 1 : 0; + }); + return this; +}; + M3U.prototype.findDateGaps = function findDateGaps (options) { options = options || {}; options.msMargin = options.msMargin == null ? 1500 : options.msMargin; @@ -322,7 +354,7 @@ M3U.prototype.sliceByDate = function sliceByDate (from, to) { } if (!from) { - from = new Date(0); + from = new Date(firstDate.getTime() - 1); } if (!to) { @@ -402,6 +434,12 @@ M3U.prototype.toString = function toString () { return output.join('\n') + '\n'; }; + +M3U.prototype.isDateSupported = function isDateSupported () { + var date = ((this.items.PlaylistItem[0] || {}).properties || {}).date; + return date ? util.isDate(date) : undefined; +}; + M3U.prototype.isVOD = function isVOD () { return this.get('foundEndlist') || this.get('playlistType') === 'VOD'; }; From dabc7e24775b5928b128e24d9add53b73b14e1db Mon Sep 17 00:00:00 2001 From: Aziz Khoury Date: Tue, 8 Mar 2016 16:13:44 -0500 Subject: [PATCH 22/41] date check, only if dates were passed in --- m3u.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/m3u.js b/m3u.js index 03f9028..23777ab 100644 --- a/m3u.js +++ b/m3u.js @@ -353,6 +353,10 @@ M3U.prototype.sliceByDate = function sliceByDate (from, to) { throw new Error('Playlist segments do not look like that they have a valid date fields, you must specify EXT-X-PROGRAM-DATE-TIME for each segment in order to sliceDate(), or set the date on your own using the beforeItemEmit hook when you setup the parser.'); } + if (util.isDate(from) && util.isDate(to) && from > to) { + throw new Error('target `to` date value, if available, must be greater than the `from` date value'); + } + if (!from) { from = new Date(firstDate.getTime() - 1); } @@ -369,10 +373,6 @@ M3U.prototype.sliceByDate = function sliceByDate (from, to) { end = 0; } - if (util.isDate(from) && util.isDate(to) && from > to) { - throw new Error('target `to` date value, if available, must be greater than the `from` date value'); - } - var current; this.items.PlaylistItem.some(function(item, i) { From 3446a73696e7d4024f2ecd40b795705e8de16f98 Mon Sep 17 00:00:00 2001 From: Aziz Khoury Date: Wed, 23 Mar 2016 18:10:14 -0400 Subject: [PATCH 23/41] covering slicing edge cases, 1 end slice --- m3u.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/m3u.js b/m3u.js index 23777ab..bcfe5f1 100644 --- a/m3u.js +++ b/m3u.js @@ -316,14 +316,14 @@ M3U.prototype.sliceBySeconds = function sliceBySeconds (from, to) { total += item.properties.duration; currentIndex = i; - if (total >= from && start == null) { + if (from != null && total >= from && start == null) { start = i; if (to == null) { return true; } } - if (total >= to && end == null) { + if (to != null && total >= to && end == null) { end = i + 1; return true; } @@ -378,14 +378,14 @@ M3U.prototype.sliceByDate = function sliceByDate (from, to) { this.items.PlaylistItem.some(function(item, i) { current = item.properties.date; - if (current >= from && start == null) { + if (from != null && current >= from && start == null) { start = i; if (to == null) { return true; } } - if (current >= to && end == null) { + if (to != null && current >= to && end == null) { end = i; return true; } From d7e73f3e9cfff119b48ff3970335996a79049693 Mon Sep 17 00:00:00 2001 From: akhoury Date: Thu, 7 Apr 2016 14:35:50 -0400 Subject: [PATCH 24/41] support NodeJS 0.10 and add rangeBounds checkers --- .editorConfig | 0 m3u.js | 91 ++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 90 insertions(+), 1 deletion(-) create mode 100644 .editorConfig diff --git a/.editorConfig b/.editorConfig new file mode 100644 index 0000000..e69de29 diff --git a/m3u.js b/m3u.js index bcfe5f1..944f19b 100644 --- a/m3u.js +++ b/m3u.js @@ -1,4 +1,17 @@ -var util = require('util'); +var util; +try { + util = require('util'); +} catch(e) { + util = {}; +} + +util.isNumber = util.isNumber || function (n) { + return !isNaN(parseFloat(n)) && isFinite(n); +}; + +util.isDate = util.isDate || function (d) { + return d instanceof Date && !isNaN(d.valueOf()); +}; var M3U = module.exports = function M3U() { this.items = { @@ -394,6 +407,82 @@ M3U.prototype.sliceByDate = function sliceByDate (from, to) { return this.sliceByIndex(start, end); }; +M3U.prototype.isRangeWithinIndexBounds = function isRangeWithinSecondsBounds (from, to) { + + var len = this.items.PlaylistItem.length; + + if (!len) { + return false; + } + + var left = true; + var right = true; + + if (from != null) { + left = !!this.items.PlaylistItem[from]; + } + + if (to != null) { + right = !!this.items.PlaylistItem[to]; + } + + return left && right; +}; + +M3U.prototype.isRangeWithinSecondsBounds = function isRangeWithinSecondsBounds (from, to) { + + var len = this.items.PlaylistItem.length; + + if (!len) { + return false; + } + + var left = true; + var right = true; + + if (from != null) { + left = 0 <= from; + } + + if (to != null) { + right = to <= this.totalDuration(); + } + + return left && right; +}; + +M3U.prototype.isRangeWithinDateBounds = function isRangeWithinDateBounds (from, to) { + + if (!util.isDate(from) && !util.isDate(to)) { + throw new Error('at least 1 of the arguments needs to be a Date object'); + } + + if (util.isNumber(from)) { + from = new Date(to.getTime() - from * 1000); + } else if (util.isNumber(to)) { + to = new Date(from.getTime() + to * 1000); + } + + var len = this.items.PlaylistItem.length; + + if (!len) { + return false; + } + + var left = true; + var right = true; + + if (from != null) { + left = this.items.PlaylistItem[0].properties.date <= from; + } + + if (to != null) { + right = to <= this.items.PlaylistItem[len - 1].properties.date; + } + + return left && right; +}; + M3U.prototype.toString = function toString () { var self = this; var output = ['#EXTM3U']; From d09dd57c14325947a6de5a109d8b9725062b7b35 Mon Sep 17 00:00:00 2001 From: akhoury Date: Thu, 7 Apr 2016 14:36:14 -0400 Subject: [PATCH 25/41] project tidy files --- .gitignore | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/.gitignore b/.gitignore index 3c3629e..db906bb 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,37 @@ +.idea +.DS_Store + +# Logs +logs +*.log +npm-debug.log* + +# Runtime data +pids +*.pid +*.seed + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directories node_modules +jspm_packages + +# Optional npm cache directory +.npm + +# Optional REPL history +.node_repl_history From 51d2efe567fdd877f8fc582acea68df8d386334a Mon Sep 17 00:00:00 2001 From: akhoury Date: Thu, 7 Apr 2016 14:39:43 -0400 Subject: [PATCH 26/41] w --- .editorConfig | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 .editorConfig diff --git a/.editorConfig b/.editorConfig deleted file mode 100644 index e69de29..0000000 From f2247c296330c7b71deed750c7dd27232ae42eb9 Mon Sep 17 00:00:00 2001 From: akhoury Date: Thu, 7 Apr 2016 14:40:25 -0400 Subject: [PATCH 27/41] editorconfig --- .editorconfig | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..acb3b84 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,11 @@ +# EditorConfig is awesome: http://EditorConfig.org + +# top-most EditorConfig file +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +indent_style = space +indent_size = 2 From a14733856bf4d0b351ec2ba3bdd631e99db8d2fe Mon Sep 17 00:00:00 2001 From: akhoury Date: Thu, 7 Apr 2016 17:30:51 -0400 Subject: [PATCH 28/41] added isRangeWithinBounds helper function --- m3u.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/m3u.js b/m3u.js index 944f19b..11306f8 100644 --- a/m3u.js +++ b/m3u.js @@ -407,7 +407,18 @@ M3U.prototype.sliceByDate = function sliceByDate (from, to) { return this.sliceByIndex(start, end); }; -M3U.prototype.isRangeWithinIndexBounds = function isRangeWithinSecondsBounds (from, to) { +M3U.prototype.isRangeWithinBounds = function isRangeWithinBounds (unit, from, to) { + switch (unit) { + case 'date': + return this.isRangeWithinDateBounds(from, to); + case 'seconds': + return this.isRangeWithinSecondsBounds(from, to); + case 'index': + return this.isRangeWithinIndexBounds(from, to); + } +}; + +M3U.prototype.isRangeWithinIndexBounds = function isRangeWithinIndexBounds (from, to) { var len = this.items.PlaylistItem.length; From 8dedf80b20d153c4095dc744a45d89a905359883 Mon Sep 17 00:00:00 2001 From: akhoury Date: Mon, 11 Apr 2016 17:31:50 -0400 Subject: [PATCH 29/41] make sure both sides are inclusive shile slicing --- m3u.js | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/m3u.js b/m3u.js index 11306f8..75c8359 100644 --- a/m3u.js +++ b/m3u.js @@ -314,6 +314,10 @@ M3U.prototype.sliceBySeconds = function sliceBySeconds (from, to) { throw 'target `to` value, if truthy, must be greater than the `from` value'; } + if (!this.items.PlaylistItem.length) { + return this.sliceByIndex(); + } + var duration = this.totalDuration(); if (util.isNumber(from) && from > duration) { start = this.items.PlaylistItem.length; @@ -329,7 +333,7 @@ M3U.prototype.sliceBySeconds = function sliceBySeconds (from, to) { total += item.properties.duration; currentIndex = i; - if (from != null && total >= from && start == null) { + if (from != null && total >= from && start == null) { // left-side-inclusive start = i; if (to == null) { return true; @@ -337,7 +341,7 @@ M3U.prototype.sliceBySeconds = function sliceBySeconds (from, to) { } if (to != null && total >= to && end == null) { - end = i + 1; + end = total == to ? i + 1 : i; // right-side-inclusive return true; } }); @@ -353,6 +357,10 @@ M3U.prototype.sliceByDate = function sliceByDate (from, to) { throw new Error('at least 1 of the arguments needs to be a Date object'); } + if (!this.items.PlaylistItem.length) { + return this.sliceByIndex(); + } + if (util.isNumber(from)) { from = new Date(to.getTime() - from * 1000); } else if (util.isNumber(to)) { @@ -391,7 +399,7 @@ M3U.prototype.sliceByDate = function sliceByDate (from, to) { this.items.PlaylistItem.some(function(item, i) { current = item.properties.date; - if (from != null && current >= from && start == null) { + if (from != null && current >= from && start == null) { // right-side-inclusive start = i; if (to == null) { return true; @@ -399,7 +407,7 @@ M3U.prototype.sliceByDate = function sliceByDate (from, to) { } if (to != null && current >= to && end == null) { - end = i; + end = current == to ? i + 1 : i; // // left-side-inclusive return true; } }); @@ -575,7 +583,7 @@ M3U.unserialize = function unserialize (object) { Object.keys(object.items).forEach(function(constructor) { m3u.items[constructor] = object.items[constructor].map( - Item.unserialize.bind(null, M3U[constructor]) + Item.unserialize.bind(null, M3U[constructor]) ); }); return m3u; From 967d7659458075b9effca932eae51e7907f95fd8 Mon Sep 17 00:00:00 2001 From: akhoury Date: Mon, 11 Apr 2016 18:10:17 -0400 Subject: [PATCH 30/41] set the VOD/live mode correctly after dateMerge and uriMerge --- m3u.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/m3u.js b/m3u.js index 75c8359..a792c39 100644 --- a/m3u.js +++ b/m3u.js @@ -164,8 +164,11 @@ M3U.prototype.mergeByUri = function mergeByUri (m3u) { } } - if (m3u.get('foundEndlist')) { - clone.set('foundEndlist', true); + if (m3u.isVOD()) { + clone.set('playlistType', 'VOD'); + } else { + clone.set('playlistType', 'EVENT'); + clone.set('foundEndlist', false); } return clone; @@ -212,6 +215,7 @@ M3U.prototype.mergeByDate = function mergeByDate (m3u, options) { result.set('playlistType', 'VOD'); } else { result.set('playlistType', 'EVENT'); + result.set('foundEndlist', false); } return result; From 8f0d784ac06c88a807c0dd0d424c3ee747239495 Mon Sep 17 00:00:00 2001 From: Aziz Khoury Date: Fri, 15 Apr 2016 02:05:48 -0400 Subject: [PATCH 31/41] node code util is crap, also comments fix --- m3u.js | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/m3u.js b/m3u.js index a792c39..97b73fb 100644 --- a/m3u.js +++ b/m3u.js @@ -1,15 +1,8 @@ -var util; -try { - util = require('util'); -} catch(e) { - util = {}; -} - -util.isNumber = util.isNumber || function (n) { +var util = {}; +util.isNumber = function (n) { return !isNaN(parseFloat(n)) && isFinite(n); }; - -util.isDate = util.isDate || function (d) { +util.isDate = function (d) { return d instanceof Date && !isNaN(d.valueOf()); }; @@ -135,7 +128,7 @@ M3U.prototype.concat = function concat (m3u) { }; // backward-compatible merge function, that just concats and mutates self -// todo: remove this, since it's really a merge, it's just a concat() +// todo: remove this, since it's really not a merge, it's just a concat() M3U.prototype.merge = function merge (m3u) { var clone = this.concat(m3u); this.items.PlaylistItem = clone.items.PlaylistItem; @@ -143,6 +136,7 @@ M3U.prototype.merge = function merge (m3u) { return this; }; +// todo: too O(n^2) too slow - maybe use a hash of URIs M3U.prototype.mergeByUri = function mergeByUri (m3u) { var clone = this.concat(m3u); From 53e0be51768437fe4d53188f7dfbee82718e6839 Mon Sep 17 00:00:00 2001 From: Aziz Khoury Date: Fri, 15 Apr 2016 03:14:39 -0400 Subject: [PATCH 32/41] when concat preserve VOD/Live state --- m3u.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/m3u.js b/m3u.js index 97b73fb..38a5205 100644 --- a/m3u.js +++ b/m3u.js @@ -124,6 +124,13 @@ M3U.prototype.concat = function concat (m3u) { clone.items.PlaylistItem = clone.items.PlaylistItem.concat(m3u.items.PlaylistItem); + if (m3u.isVOD()) { + clone.set('playlistType', 'VOD'); + } else { + clone.set('playlistType', 'EVENT'); + clone.set('foundEndlist', false); + } + return clone; }; From 7151198909bb8580f7c25d088cf03648377e9ca4 Mon Sep 17 00:00:00 2001 From: Aziz Khoury Date: Thu, 12 May 2016 15:50:00 -0400 Subject: [PATCH 33/41] wtf ? ``` { "message": "createM3U is not defined", "stack": "ReferenceError: createM3U is not defined\n at M3U.mergeByDate (/Users/akhoury/Perforce/akhoury_azizs-mac-mini_4287/speech/ramp/sites/m3u8-service/node_modules/m3u8/m3u.js:190:80)\n at /Users/akhoury/Perforce/akhoury_azizs-mac-mini_4287/speech/ramp/sites/m3u8-service/lib/index.js:295:24\n at tryCatchResolve (/Users/akhoury/Perforce/akhoury_azizs-mac-mini_4287/speech/ramp/sites/m3u8-service/node_modules/when/lib/apply.js:46:23)\n at callAndResolve (/Users/akhoury/Perforce/akhoury_azizs-mac-mini_4287/speech/ramp/sites/m3u8-service/node_modules/when/lib/apply.js:30:12)\n at callAndResolveNext (/Users/akhoury/Perforce/akhoury_azizs-mac-mini_4287/speech/ramp/sites/m3u8-service/node_modules/when/lib/apply.js:40:4)\n at tryCatchReject3 (/Users/akhoury/Perforce/akhoury_azizs-mac-mini_4287/speech/ramp/sites/m3u8-service/node_modules/when/lib/makePromise.js:856:7)\n at runContinuation3 (/Users/akhoury/Perforce/akhoury_azizs-mac-mini_4287/speech/ramp/sites/m3u8-service/node_modules/when/lib/makePromise.js:814:4)\n at Fulfilled.fold (/Users/akhoury/Perforce/akhoury_azizs-mac-mini_4287/speech/ramp/sites/m3u8-service/node_modules/when/lib/makePromise.js:588:4)\n at callAndResolve (/Users/akhoury/Perforce/akhoury_azizs-mac-mini_4287/speech/ramp/sites/m3u8-service/node_modules/when/lib/apply.js:34:12)\n at callAndResolveNext (/Users/akhoury/Perforce/akhoury_azizs-mac-mini_4287/speech/ramp/sites/m3u8-service/node_modules/when/lib/apply.js:40:4)" } ``` --- m3u.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/m3u.js b/m3u.js index 38a5205..d674a74 100644 --- a/m3u.js +++ b/m3u.js @@ -187,8 +187,8 @@ M3U.prototype.mergeByDate = function mergeByDate (m3u, options) { dateA0 = clone.items.PlaylistItem[0].get('date'); dateAN = clone.items.PlaylistItem[clone.items.PlaylistItem.length - 1].get('date'); } - m3uPre = dateA0 ? m3u.sliceByDate(null, new Date((+new Date(dateA0)) - 1)) : createM3U(); // -1 ms to make it exclusive - m3uPost = dateAN ? m3u.sliceByDate(new Date((+new Date(dateAN)) + 1)) : createM3U(); // +1 ms to make it exclusive + m3uPre = dateA0 ? m3u.sliceByDate(null, new Date((+new Date(dateA0)) - 1)) : M3U.create(); // -1 ms to make it exclusive + m3uPost = dateAN ? m3u.sliceByDate(new Date((+new Date(dateAN)) + 1)) : M3U.create(); // +1 ms to make it exclusive var gaps = clone.findDateGaps(options); gaps.forEach(function(gap) { From 6803773a2e09a6b215ac840c37f2233630d42bd2 Mon Sep 17 00:00:00 2001 From: akhoury Date: Sun, 22 May 2016 11:18:51 -0400 Subject: [PATCH 34/41] add isMaster() --- m3u.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/m3u.js b/m3u.js index d674a74..3987805 100644 --- a/m3u.js +++ b/m3u.js @@ -561,6 +561,10 @@ M3U.prototype.isLive = function isLive () { return !this.isVOD(); }; +M3U.prototype.isMaster = function isMaster () { + return !! (this.items.StreamItem.length || this.items.MediaItem.length || this.items.IframeStreamItem.length); +}; + M3U.prototype.clone = function clone () { return M3U.unserialize(this.serialize()); }; From 0390f413606e88a7d9d375a157d0c6a0fd23e32e Mon Sep 17 00:00:00 2001 From: Aziz Khoury Date: Tue, 24 May 2016 15:45:22 -0400 Subject: [PATCH 35/41] fix isRangeWithinDateBounds upper bound bug --- m3u.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/m3u.js b/m3u.js index 3987805..d94c5f9 100644 --- a/m3u.js +++ b/m3u.js @@ -501,7 +501,7 @@ M3U.prototype.isRangeWithinDateBounds = function isRangeWithinDateBounds (from, } if (to != null) { - right = to <= this.items.PlaylistItem[len - 1].properties.date; + right = this.items.PlaylistItem[len - 1].properties.date <= to; } return left && right; From c0a390d7f044b0255534b708bb86bdcea788faa5 Mon Sep 17 00:00:00 2001 From: Aziz Khoury Date: Tue, 31 May 2016 13:51:22 -0400 Subject: [PATCH 36/41] correct EXT-X-MEDIA-SEQUENCE calculation on slicing --- m3u.js | 8 ++++++++ test/m3u.test.js | 4 +++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/m3u.js b/m3u.js index d94c5f9..bf1ff57 100644 --- a/m3u.js +++ b/m3u.js @@ -147,6 +147,7 @@ M3U.prototype.merge = function merge (m3u) { M3U.prototype.mergeByUri = function mergeByUri (m3u) { var clone = this.concat(m3u); + // todo: also, i don't think this is correct if (m3u.get('mediaSequence') < clone.get('mediaSequence')) { clone.set('mediaSequence', m3u.get('mediaSequence')); } @@ -304,6 +305,13 @@ M3U.prototype.sliceByIndex = M3U.prototype.slice = function sliceByIndex (start, m3u.set('playlistType', 'VOD'); } + var mediaSequence = m3u.get('mediaSequence'); + // assume 1 if it doesn't exists https://tools.ietf.org/html/draft-pantos-http-live-streaming-01#section-3.1.2 + if (!mediaSequence || mediaSequence < 0) { + mediaSequence = 1; + } + m3u.set('mediaSequence', mediaSequence + start); + m3u.items.PlaylistItem = m3u.items.PlaylistItem.slice(start, end); return m3u; diff --git a/test/m3u.test.js b/test/m3u.test.js index 2b3053c..8820749 100644 --- a/test/m3u.test.js +++ b/test/m3u.test.js @@ -254,10 +254,12 @@ describe('m3u', function() { m3u1.addPlaylistItem({}); m3u1.addPlaylistItem({}); m3u1.addPlaylistItem({}); + m3u1.set('mediaSequence', 5); var m3u2 = m3u1.sliceByIndex(1, 3); - m3u2.items.PlaylistItem.length.should.eql(2); + m3u2.get('mediaSequence').should.eql(6); + m3u2.items.PlaylistItem.length.should.eql(2); }); }); From 21e49b1b73556207cf40d8eb046dc963c75feddd Mon Sep 17 00:00:00 2001 From: Aziz Khoury Date: Wed, 8 Jun 2016 17:12:05 -0400 Subject: [PATCH 37/41] date slicing/merging is more accurate now --- m3u.js | 41 +++++++++++++++++++++++++++------------ test/m3u.test.js | 50 +++++++++++++++++++++++++++++++++++------------- 2 files changed, 66 insertions(+), 25 deletions(-) diff --git a/m3u.js b/m3u.js index bf1ff57..74f86e1 100644 --- a/m3u.js +++ b/m3u.js @@ -177,19 +177,23 @@ M3U.prototype.mergeByUri = function mergeByUri (m3u) { }; M3U.prototype.mergeByDate = function mergeByDate (m3u, options) { + var clone = this.clone(m3u); options = options || {}; var len = clone.items.PlaylistItem.length; - var dateA0, dateAN, m3uPre, m3uPost; + var item0, itemN, dateA0, dateAN, m3uPre, m3uPost; if (len) { - dateA0 = clone.items.PlaylistItem[0].get('date'); - dateAN = clone.items.PlaylistItem[clone.items.PlaylistItem.length - 1].get('date'); + item0 = clone.items.PlaylistItem[0]; + dateA0 = item0.get('date'); + itemN = clone.items.PlaylistItem[clone.items.PlaylistItem.length - 1]; + dateAN = itemN.get('date'); } - m3uPre = dateA0 ? m3u.sliceByDate(null, new Date((+new Date(dateA0)) - 1)) : M3U.create(); // -1 ms to make it exclusive - m3uPost = dateAN ? m3u.sliceByDate(new Date((+new Date(dateAN)) + 1)) : M3U.create(); // +1 ms to make it exclusive + + m3uPre = dateA0 ? m3u.sliceByDate(null, new Date((+new Date(dateA0)))) : M3U.create(); + m3uPost = dateAN ? m3u.sliceByDate(new Date((+new Date(dateAN)) + itemN.get('duration') * 1000)) : M3U.create(); var gaps = clone.findDateGaps(options); gaps.forEach(function(gap) { @@ -354,7 +358,9 @@ M3U.prototype.sliceBySeconds = function sliceBySeconds (from, to) { } if (to != null && total >= to && end == null) { - end = total == to ? i + 1 : i; // right-side-inclusive + // we're adding the +1 here to include the current segment, + // it is still considered exclusive, since the current value here is the total duration, so the end of each segment + end = i + 1; return true; } }); @@ -407,20 +413,31 @@ M3U.prototype.sliceByDate = function sliceByDate (from, to) { end = 0; } - var current; + from = from && from.valueOf ? from.valueOf() : from; + to = to && to.valueOf ? to.valueOf(): to; + + var currentStart; + var currentEnd; this.items.PlaylistItem.some(function(item, i) { - current = item.properties.date; + currentStart = new Date(item.properties.date); + currentEnd = new Date(item.properties.date); + currentEnd.setSeconds(currentStart.getSeconds() + item.properties.duration); - if (from != null && current >= from && start == null) { // right-side-inclusive - start = i; + currentStart = currentEnd.valueOf(); + currentEnd = currentEnd.valueOf(); + + if (from != null && currentEnd >= from && start == null) { // right-side-inclusive + start = currentEnd == from ? i + 1 : i; // still exclude directly behind segment if (to == null) { return true; } } - if (to != null && current >= to && end == null) { - end = current == to ? i + 1 : i; // // left-side-inclusive + if (to != null && currentEnd >= to && end == null) { + // we're adding the +1 here to include the current segment, + // it is still considered exclusive, since the current = date + duration, so the end of each segment + end = currentStart >= to ? i + 1 : i; return true; } }); diff --git a/test/m3u.test.js b/test/m3u.test.js index 8820749..55f6f5f 100644 --- a/test/m3u.test.js +++ b/test/m3u.test.js @@ -216,16 +216,16 @@ describe('m3u', function() { var m3u1 = getM3u(); var ms0 = +new Date() - (24 * 60 * 60 * 1000); - m3u1.addPlaylistItem({uri: 'a.3', date: new Date(ms0)}); - m3u1.addPlaylistItem({uri: 'a.4', date: new Date(ms0 + 10000)}); - m3u1.addPlaylistItem({uri: 'a.6', date: new Date(ms0 + 30000)}); + m3u1.addPlaylistItem({uri: 'a.3', date: new Date(ms0), duration: 10}); + m3u1.addPlaylistItem({uri: 'a.4', date: new Date(ms0 + 10000), duration: 10}); + m3u1.addPlaylistItem({uri: 'a.6', date: new Date(ms0 + 30000), duration: 10}); var m3u2 = getM3u(); - m3u2.addPlaylistItem({uri: 'b.1', date: new Date(ms0 - 20000)}); - m3u2.addPlaylistItem({uri: 'b.2', date: new Date(ms0 - 10000)}); - m3u2.addPlaylistItem({uri: 'b.5', date: new Date(ms0 + 20000)}); - m3u2.addPlaylistItem({uri: 'b.6', date: new Date(ms0 + 30000)}); - m3u2.addPlaylistItem({uri: 'b.7', date: new Date(ms0 + 40000)}); + m3u2.addPlaylistItem({uri: 'b.1', date: new Date(ms0 - 20000), duration: 10}); + m3u2.addPlaylistItem({uri: 'b.2', date: new Date(ms0 - 10000), duration: 10}); + m3u2.addPlaylistItem({uri: 'b.5', date: new Date(ms0 + 20000), duration: 10}); + m3u2.addPlaylistItem({uri: 'b.6', date: new Date(ms0 + 30000), duration: 10}); + m3u2.addPlaylistItem({uri: 'b.7', date: new Date(ms0 + 40000), duration: 10}); m3u1 = m3u1.mergeByDate(m3u2); @@ -283,15 +283,39 @@ describe('m3u', function() { var m3u1 = getM3u(); var ms0 = +new Date(); + var duration = 10; - m3u1.addPlaylistItem({date: new Date(ms0)}); - m3u1.addPlaylistItem({date: new Date(ms0 + 5000)}); - m3u1.addPlaylistItem({date: new Date(ms0 + 10000)}); - m3u1.addPlaylistItem({date: new Date(ms0 + 15000)}); + var len = 4; + for (var i = 0; i < len; i++) { + m3u1.addPlaylistItem({date: new Date(ms0 + (duration * i * 1000)), duration: duration}); + } - var m3u2 = m3u1.sliceByDate(new Date(ms0 + 5000), new Date(ms0 + 10001)); + var m3uA = m3u1.sliceByDate(new Date(ms0 + 7000), new Date(ms0 + 17000)); + m3uA.items.PlaylistItem.length.should.eql(2); + + var m3uB = m3u1.sliceByDate(new Date(ms0 + 10000), new Date(ms0 + 20000)); + m3uB.items.PlaylistItem.length.should.eql(1); + + var m3uC = m3u1.sliceByDate(new Date(ms0 + 10000), new Date(ms0 + 31000)); + m3uC.items.PlaylistItem.length.should.eql(3); + + var m3uD = m3u1.sliceByDate(new Date(ms0 + 11000), new Date(ms0 + 21000)); + m3uD.items.PlaylistItem.length.should.eql(2); + + var m3uE = m3u1.sliceByDate(new Date(ms0 + 11000), new Date(ms0 + 20000)); + m3uE.items.PlaylistItem.length.should.eql(1); + + var m3u2 = m3u1.sliceByDate(new Date(ms0 + 10000), new Date(ms0 + 21000)); + m3u2.items.PlaylistItem[0].properties.date.valueOf().should.eql((new Date(ms0 + (duration /* *1 */ * 1000))).valueOf()); + m3u2.items.PlaylistItem[m3u2.items.PlaylistItem.length - 1].properties.date.valueOf().should.eql((new Date(ms0 + (duration * 2 * 1000))).valueOf()); m3u2.items.PlaylistItem.length.should.eql(2); + var m3u3 = m3u1.sliceByDate(new Date(ms0 + 10001), new Date(ms0 + 20000)); + m3u3.items.PlaylistItem.length.should.eql(1); + + var m3u4 = m3u1.sliceByDate(new Date(ms0 + 10000), new Date(ms0 + 20001)); + m3u4.items.PlaylistItem.length.should.eql(2); + }); }); From 450eb3759c6abad117229fd0c62ff73d0fb94d10 Mon Sep 17 00:00:00 2001 From: Aziz Khoury Date: Wed, 22 Jun 2016 11:56:59 -0400 Subject: [PATCH 38/41] reset target duration when slicing/concat/merging/adding/removing items --- m3u.js | 50 +++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 43 insertions(+), 7 deletions(-) diff --git a/m3u.js b/m3u.js index 74f86e1..dd6cb9d 100644 --- a/m3u.js +++ b/m3u.js @@ -45,11 +45,14 @@ M3U.prototype.addItem = function addItem(item) { }; M3U.prototype.addPlaylistItem = function addPlaylistItem(data) { - this.items.PlaylistItem.push(M3U.PlaylistItem.create(data)); + var newItem = M3U.PlaylistItem.create(data); + this.maybeSetTargetDuration(newItem.get('duration')); + this.items.PlaylistItem.push(newItem); }; M3U.prototype.insertPlaylistItemsAfter = function insertPlaylistItemsAfter (newItems, afterItem) { var index = this.items.PlaylistItem.length; + var self = this; if (!(afterItem instanceof M3U.PlaylistItem)) { afterItem = M3U.PlaylistItem.create(afterItem); @@ -57,8 +60,9 @@ M3U.prototype.insertPlaylistItemsAfter = function insertPlaylistItemsAfter (newI newItems = [].concat(newItems).map(function(newItem) { if (!(newItem instanceof M3U.PlaylistItem)) { - return M3U.PlaylistItem.create(newItem); + newItem = M3U.PlaylistItem.create(newItem); } + self.maybeSetTargetDuration(newItem.get('duration')); return newItem; }); @@ -70,6 +74,7 @@ M3U.prototype.insertPlaylistItemsAfter = function insertPlaylistItemsAfter (newI }); this.items.PlaylistItem = this.items.PlaylistItem.slice(0, index + 1).concat(newItems).concat(this.items.PlaylistItem.slice(index + 1)); + this.resetTargetDuration(true); return this; }; @@ -79,6 +84,7 @@ M3U.prototype.removePlaylistItem = function removePlaylistItem(index) { } else { throw new RangeError('M3U PlaylistItem out of range'); } + this.resetTargetDuration(true); }; M3U.prototype.addMediaItem = function addMediaItem(data) { @@ -111,12 +117,37 @@ M3U.prototype.totalDuration = function totalDuration() { }, 0); }; -M3U.prototype.concat = function concat (m3u) { - var clone = this.clone(); +// if one is passed in, try it, if none is passed, iterate and find it. +M3U.prototype.resetTargetDuration = function resetTargetDuration (newTargetDuration) { + var self = this; + + // if you just want to set it to a number, don't use this function, nor maybeSetTargetDuration, just use this.set('targetDuration', Math.round(newTargetDuration)) + + if (util.isNumber(newTargetDuration)) { + this.maybeSetTargetDuration(newTargetDuration); + } else { + // force reset, so we set it to 0, this way, the 1st item will set it. + if (newTargetDuration === true) { + this.set('targetDuration', 0); + } + this.items.PlaylistItem.forEach(function(item) { + self.maybeSetTargetDuration(item.get('duration')); + }); + } + return this.get('targetDuration'); +}; - if (m3u.get('targetDuration') > clone.get('targetDuration')) { - clone.set('targetDuration', m3u.get('targetDuration')); +M3U.prototype.maybeSetTargetDuration = function maybeSetTargetDuration (newTargetDuration) { + // round to nearest integer https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-4.3.3.1 + newTargetDuration = Math.round(newTargetDuration); + + if (newTargetDuration > this.get('targetDuration')) { + this.set('targetDuration', newTargetDuration); } +}; + +M3U.prototype.concat = function concat (m3u) { + var clone = this.clone(); if (m3u.items.PlaylistItem[0]) { m3u.items.PlaylistItem[0].set('discontinuity', true); @@ -131,6 +162,8 @@ M3U.prototype.concat = function concat (m3u) { clone.set('foundEndlist', false); } + clone.resetTargetDuration(m3u.get('targetDuration')); + return clone; }; @@ -139,7 +172,7 @@ M3U.prototype.concat = function concat (m3u) { M3U.prototype.merge = function merge (m3u) { var clone = this.concat(m3u); this.items.PlaylistItem = clone.items.PlaylistItem; - this.set('targetDuration', clone.get('targetDuration')); + this.resetTargetDuration(clone.get('targetDuration')); return this; }; @@ -162,6 +195,8 @@ M3U.prototype.mergeByUri = function mergeByUri (m3u) { segments[i].set('discontinuity', true); } segments.splice(j--, 1); + } else { + clone.maybeSetTargetDuration(segments[i].get('duration')); } } } @@ -317,6 +352,7 @@ M3U.prototype.sliceByIndex = M3U.prototype.slice = function sliceByIndex (start, m3u.set('mediaSequence', mediaSequence + start); m3u.items.PlaylistItem = m3u.items.PlaylistItem.slice(start, end); + m3u.resetTargetDuration(true); return m3u; }; From 0a3403577b65076f5581fbb42970b6f0cb3826b1 Mon Sep 17 00:00:00 2001 From: Aziz Khoury Date: Mon, 27 Jun 2016 23:38:16 -0400 Subject: [PATCH 39/41] delete playlistType when sliding window --- m3u.js | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/m3u.js b/m3u.js index dd6cb9d..a447480 100644 --- a/m3u.js +++ b/m3u.js @@ -339,9 +339,25 @@ M3U.prototype.sliceByIndex = M3U.prototype.slice = function sliceByIndex (start, end = len; } - // if live and both start & end were within the length of the stream, make it look like a VOD - if (! m3u.isVOD() && start < len && end < len) { - m3u.set('playlistType', 'VOD'); + if (m3u.isLive()) { + if (start < len && end < len) { + // if live and both start & end were within the length of the stream, make it look like a VOD + m3u.set('playlistType', 'VOD'); + + } else if (start > 0 && end == len) { + /* + One thing I noticed recently is that if we are implementing a LIVE sliding window, we can't have a `playlistType`, (otherwise Safari refuses to play more than 1 segment) + + https://tools.ietf.org/html/draft-pantos-http-live-streaming-07#page-19 + > the Playlist file MAY contain an EXT-X-PLAYLIST-TYPE tag + > with a value of either EVENT or VOD. If the tag is present and has a + > value of EVENT, the server MUST NOT change or delete any part of the + > Playlist file (although it MAY append lines to it) + + So, If we slice an m3u and `isLive()` is true, and the `start` value of slicing is greater than 0, then we need to remove the playlistType + */ + delete m3u.properties['playlistType']; + } } var mediaSequence = m3u.get('mediaSequence'); From 059c78d903d502bdcc8b2c236f2acef2c9b70936 Mon Sep 17 00:00:00 2001 From: Aziz Khoury Date: Tue, 28 Jun 2016 00:05:56 -0400 Subject: [PATCH 40/41] delete playlistType all the time when live, it's uncessary --- m3u.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/m3u.js b/m3u.js index a447480..178d3ad 100644 --- a/m3u.js +++ b/m3u.js @@ -158,7 +158,7 @@ M3U.prototype.concat = function concat (m3u) { if (m3u.isVOD()) { clone.set('playlistType', 'VOD'); } else { - clone.set('playlistType', 'EVENT'); + delete clone.properties['playlistType']; clone.set('foundEndlist', false); } @@ -204,7 +204,7 @@ M3U.prototype.mergeByUri = function mergeByUri (m3u) { if (m3u.isVOD()) { clone.set('playlistType', 'VOD'); } else { - clone.set('playlistType', 'EVENT'); + delete clone.properties['playlistType']; clone.set('foundEndlist', false); } @@ -255,7 +255,7 @@ M3U.prototype.mergeByDate = function mergeByDate (m3u, options) { if (m3uTail.isVOD()) { result.set('playlistType', 'VOD'); } else { - result.set('playlistType', 'EVENT'); + delete result.properties['playlistType']; result.set('foundEndlist', false); } From 9ebaaec52aa5a67ded054b13cde51cb5a8d78d45 Mon Sep 17 00:00:00 2001 From: Viktor Zdanovich Date: Wed, 10 Jan 2018 19:36:01 +0400 Subject: [PATCH 41/41] added EXT-X-DISCONTINUITY-SEQUENCE to be set through discontinuitySequence prop --- m3u.js | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/m3u.js b/m3u.js index 178d3ad..28869b6 100644 --- a/m3u.js +++ b/m3u.js @@ -717,19 +717,21 @@ var toStringIgnoredProperties = { }; var dataTypes = { - iframesOnly : 'boolean', - targetDuration : 'integer', - mediaSequence : 'integer', - version : 'integer' + iframesOnly : 'boolean', + targetDuration : 'integer', + mediaSequence : 'integer', + discontinuitySequence : 'integer', + version : 'integer' }; var propertyMap = [ - { tag: 'EXT-X-ALLOW-CACHE', key: 'allowCache' }, - { tag: 'EXT-X-I-FRAMES-ONLY', key: 'iframesOnly' }, - { tag: 'EXT-X-MEDIA-SEQUENCE', key: 'mediaSequence' }, - { tag: 'EXT-X-PLAYLIST-TYPE', key: 'playlistType' }, - { tag: 'EXT-X-TARGETDURATION', key: 'targetDuration' }, - { tag: 'EXT-X-VERSION', key: 'version' } + { tag: 'EXT-X-ALLOW-CACHE', key: 'allowCache' }, + { tag: 'EXT-X-I-FRAMES-ONLY', key: 'iframesOnly' }, + { tag: 'EXT-X-MEDIA-SEQUENCE', key: 'mediaSequence' }, + { tag: 'EXT-X-DISCONTINUITY-SEQUENCE', key: 'discontinuitySequence' }, + { tag: 'EXT-X-PLAYLIST-TYPE', key: 'playlistType' }, + { tag: 'EXT-X-TARGETDURATION', key: 'targetDuration' }, + { tag: 'EXT-X-VERSION', key: 'version' } ]; propertyMap.findByTag = function findByTag (tag) {