cd134feb by David LaPalomento

Add function to merge an update to a media playlist

When an ENDLIST tag is not present, media playlists should be continually re-requested to check for updates. The updated versions of the playlist must be reconciled with the client's metadata to continue playback. Added a single function to manage this process for media playlists with and without media sequence information, using URIs and byterange information to match segments.
Removed parser code that defaulted the playlist type to VOD if no type was specified. The spec allows live streams to omit the playlist type if the server intends to remove segments from the playlist. None of the runtime code actually referenced playlist type so it may have been a bit premature to parse it at all.
1 parent ce47528c
......@@ -35,7 +35,8 @@
Stream = videojs.hls.Stream,
LineStream,
ParseStream,
Parser;
Parser,
merge;
/**
* A stream that buffers string input and generates a `data` event for each
......@@ -345,12 +346,6 @@
}
},
'inf': function() {
if (!this.manifest.playlistType) {
this.manifest.playlistType = 'VOD';
this.trigger('info', {
message: 'defaulting playlist type to VOD'
});
}
if (!('mediaSequence' in this.manifest)) {
this.manifest.mediaSequence = 0;
this.trigger('info', {
......@@ -458,9 +453,50 @@
this.lineStream.push('\n');
};
/**
* Merges two versions of a media playlist.
* @param base {object} the earlier version of the media playlist.
* @param update {object} the updates to apply to the base playlist.
* @return {object} a new media playlist object that combines the
* information in the two arguments.
*/
merge = function(base, update) {
var
result = mergeOptions({}, base, update),
uri = update.segments[0].uri,
i = base.segments.length,
byterange,
segment;
// align and apply the updated segments
while (i--) {
segment = base.segments[i];
if (uri === segment.uri) {
// if there is no byterange information, match by URI
if (!segment.byterange) {
result.segments = base.segments.slice(0, i).concat(update.segments);
break;
}
// if a byterange is specified, make sure the segments match exactly
byterange = update.segments[0].byterange || {};
if (segment.byterange.offset === byterange.offset &&
segment.byterange.length === byterange.length) {
result.segments = base.segments.slice(0, i).concat(update.segments);
break;
}
}
}
// concatenate the two arrays if there was no overlap
if (i < 0) {
result.segments = base.segments.concat(update.segments);
}
return result;
};
window.videojs.m3u8 = {
LineStream: LineStream,
ParseStream: ParseStream,
Parser: Parser
Parser: Parser,
merge: merge
};
})(window.videojs, window.parseInt, window.isFinite, window.videojs.util.mergeOptions);
......
(function(window, undefined) {
var
//manifestController = this.manifestController,
ParseStream = window.videojs.m3u8.ParseStream,
m3u8 = window.videojs.m3u8,
ParseStream = m3u8.ParseStream,
parseStream,
LineStream = window.videojs.m3u8.LineStream,
LineStream = m3u8.LineStream,
lineStream,
Parser = window.videojs.m3u8.Parser,
Parser = m3u8.Parser,
parser;
/*
......@@ -506,19 +507,140 @@
ok(!event, 'no event is triggered');
});
module('m3u8 parser', {
setup: function() {
module('m3u8 parser');
test('can be constructed', function() {
notStrictEqual(new Parser(), undefined, 'parser is defined');
});
test('merges a manifest that strictly adds to an earlier one', function() {
var key, base, mid, parsed;
for (key in window.manifests) {
if (window.expected[key]) {
manifest = window.manifests[key];
// parse the first half of the manifest
mid = manifest.length / 2;
parser = new Parser();
parser.push(manifest.substring(0, mid));
base = parser.manifest;
if (!base.segments) {
// only test merges for media playlists
continue;
}
// attach the partial manifest to a new parser
parser = new Parser();
parser.push(manifest);
// merge the manifests together
deepEqual(m3u8.merge(base, parser.manifest),
window.expected[key],
key + '.m3u8 was parsed correctly');
}
}
});
test('should create a parser', function() {
notStrictEqual(parser, undefined, 'parser is defined');
test('merges overlapping segments without media sequences', function() {
var base;
parser = new Parser();
parser.push('#EXTM3U\n');
parser.push('#EXTINF:10,\n');
parser.push('0.ts\n');
parser.push('#EXTINF:10,\n');
parser.push('1.ts\n');
base = parser.manifest;
parser = new Parser();
parser.push('#EXTM3U\n');
parser.push('#EXTINF:10,\n');
parser.push('1.ts\n');
parser.push('#EXTINF:10,\n');
parser.push('2.ts\n');
deepEqual({
allowCache: true,
mediaSequence: 0,
segments: [{ duration: 10, uri: '0.ts'},
{ duration: 10, uri: '1.ts' },
{ duration: 10, uri: '2.ts' }]
}, m3u8.merge(base, parser.manifest), 'merges segment additions');
});
test('appends non-overlapping segments without media sequences', function() {
var base;
parser = new Parser();
parser.push('#EXTM3U\n');
parser.push('#EXTINF:10,\n');
parser.push('0.ts\n');
base = parser.manifest;
parser = new Parser();
parser.push('#EXTM3U\n');
parser.push('#EXTINF:10,\n');
parser.push('1.ts\n');
deepEqual({
allowCache: true,
mediaSequence: 0,
segments: [{ duration: 10, uri: '0.ts'},
{ duration: 10, uri: '1.ts' }]
}, m3u8.merge(base, parser.manifest), 'appends segment additions');
});
test('replaces segments when merging with a higher media sequence number', function() {
var base;
parser = new Parser();
parser.push('#EXTM3U\n');
parser.push('#EXT-X-MEDIA-SEQUENCE:3\n');
parser.push('#EXTINF:10,\n');
parser.push('3.ts\n');
base = parser.manifest;
parser = new Parser();
parser.push('#EXTM3U\n');
parser.push('#EXT-X-MEDIA-SEQUENCE:7\n');
parser.push('#EXTINF:10,\n');
parser.push('7.ts\n');
base = parser.manifest;
deepEqual({
allowCache: true,
mediaSequence: 7,
segments: [{ duration: 10, uri: '7.ts' }]
}, m3u8.merge(base, parser.manifest), 'replaces segments');
});
test('replaces overlapping segments when media sequence is present', function() {
var base;
parser = new Parser();
parser.push('#EXTM3U\n');
parser.push('#EXT-X-MEDIA-SEQUENCE:3\n');
parser.push('#EXTINF:10,\n');
parser.push('3.ts\n');
parser.push('#EXTINF:10,\n');
parser.push('4.ts\n');
base = parser.manifest;
parser = new Parser();
parser.push('#EXTM3U\n');
parser.push('#EXT-X-MEDIA-SEQUENCE:4\n');
parser.push('#EXTINF:10,\n');
parser.push('4.ts\n');
parser.push('#EXTINF:10,\n');
parser.push('5.ts\n');
base = parser.manifest;
deepEqual({
allowCache: true,
mediaSequence: 4,
segments: [{ duration: 10, uri: '4.ts' },
{ duration: 10, uri: '5.ts' }]
}, m3u8.merge(base, parser.manifest), 'replaces segments');
});
module('m3u8s');
test('parses the example manifests as expected', function() {
test('parses static manifests as expected', function() {
var key;
for (key in window.manifests) {
if (window.expected[key]) {
......
{
"allowCache": true,
"mediaSequence": 0,
"playlistType": "VOD",
"segments": [
{
"duration": 10,
......
{
"allowCache": true,
"mediaSequence": 1,
"playlistType": "VOD",
"segments": [
{
"duration": 6.64,
......
{
"allowCache": true,
"mediaSequence": 0,
"playlistType": "VOD",
"segments": [
{
"duration": 10,
......
{
"allowCache": true,
"mediaSequence": 0,
"playlistType": "VOD",
"segments": [
{
"duration": 10,
......
{
"allowCache": true,
"mediaSequence": 0,
"playlistType": "VOD",
"segments": [
{
"duration": 10,
......
{
"allowCache": true,
"mediaSequence": 0,
"playlistType": "VOD",
"segments": [
{
"duration": 10,
......
{
"allowCache": true,
"mediaSequence": 0,
"playlistType": "VOD",
"targetDuration": 10,
"segments": [{
"uri": "001.ts"
......