9aeee3ee by David LaPalomento

Reload playlists when EXT-X-ENDLIST isn't present

Modify the parser to include an attribute when the endlist tag shows up in a media playlist. Update test playlist parses to add the attribute where appropriate. Trigger a playlist reload and merge if endlist isn't present in the parse. Clean up some formatting.
1 parent cd134feb
......@@ -345,6 +345,9 @@
byterange.offset = entry.offset;
}
},
'endlist': function() {
this.manifest.endList = true;
},
'inf': function() {
if (!('mediaSequence' in this.manifest)) {
this.manifest.mediaSequence = 0;
......@@ -462,12 +465,14 @@
*/
merge = function(base, update) {
var
result = mergeOptions({}, base, update),
result = mergeOptions({}, base),
uri = update.segments[0].uri,
i = base.segments.length,
i = base.segments ? base.segments.length : 0,
byterange,
segment;
result = mergeOptions(result, update);
// align and apply the updated segments
while (i--) {
segment = base.segments[i];
......@@ -488,7 +493,7 @@
}
// concatenate the two arrays if there was no overlap
if (i < 0) {
result.segments = base.segments.concat(update.segments);
result.segments = (base.segments || []).concat(update.segments);
}
return result;
};
......
......@@ -341,8 +341,10 @@ var
variant = bandwidthPlaylists[i];
// ignore playlists without resolution information
if (!variant.attributes || !variant.attributes.RESOLUTION ||
!variant.attributes.RESOLUTION.width || !variant.attributes.RESOLUTION.height) {
if (!variant.attributes ||
!variant.attributes.RESOLUTION ||
!variant.attributes.RESOLUTION.width ||
!variant.attributes.RESOLUTION.height) {
continue;
}
......@@ -350,7 +352,7 @@ var
// dimensions less than or equal to the player size is the
// best
if (variant.attributes.RESOLUTION.width <= player.width() &&
variant.attributes.RESOLUTION.height <= player.height()) {
variant.attributes.RESOLUTION.height <= player.height()) {
resolutionBestVariant = variant;
break;
}
......@@ -375,88 +377,107 @@ var
var xhr = new window.XMLHttpRequest();
xhr.open('GET', url);
xhr.onreadystatechange = function() {
var i, parser, playlist, playlistUri;
if (xhr.readyState === 4) {
if (xhr.status >= 400 || this.status === 0) {
player.hls.error = {
status: xhr.status,
message: 'HLS playlist request error at URL: ' + url,
code: (xhr.status >= 500) ? 4 : 2
};
player.trigger('error');
return;
}
var i, parser, playlist, playlistUri, refreshDelay;
// readystate DONE
parser = new videojs.m3u8.Parser();
parser.push(xhr.responseText);
// wait until the request completes
if (xhr.readyState !== 4) {
return;
}
// master playlists
if (parser.manifest.playlists) {
player.hls.master = parser.manifest;
downloadPlaylist(resolveUrl(url, parser.manifest.playlists[0].uri));
player.trigger('loadedmanifest');
return;
}
if (xhr.status >= 400 || this.status === 0) {
player.hls.error = {
status: xhr.status,
message: 'HLS playlist request error at URL: ' + url,
code: (xhr.status >= 500) ? 4 : 2
};
player.trigger('error');
return;
}
// media playlists
if (player.hls.master) {
// merge this playlist into the master
i = player.hls.master.playlists.length;
while (i--) {
playlist = player.hls.master.playlists[i];
playlistUri = resolveUrl(srcUrl, playlist.uri);
if (playlistUri === url) {
player.hls.master.playlists[i] =
videojs.util.mergeOptions(playlist, parser.manifest);
}
}
} else {
// infer a master playlist if none was previously requested
player.hls.master = {
playlists: [parser.manifest]
};
}
// readystate DONE
parser = new videojs.m3u8.Parser();
parser.push(xhr.responseText);
// master playlists
if (parser.manifest.playlists) {
player.hls.master = parser.manifest;
downloadPlaylist(resolveUrl(url, parser.manifest.playlists[0].uri));
player.trigger('loadedmanifest');
return;
}
// always start playback with the default rendition
if (!player.hls.media) {
player.hls.media = player.hls.master.playlists[0];
// media playlists
refreshDelay = (parser.manifest.targetDuration || 10) * 1000;
if (player.hls.master) {
// merge this playlist into the master
i = player.hls.master.playlists.length;
while (i--) {
playlist = player.hls.master.playlists[i];
playlistUri = resolveUrl(srcUrl, playlist.uri);
if (playlistUri === url) {
// if the playlist is unchanged since the last reload,
// try again after half the target duration
// http://tools.ietf.org/html/draft-pantos-http-live-streaming-12#section-6.3.4
if (playlist.segments &&
playlist.segments.length === parser.manifest.segments.length) {
refreshDelay /= 2;
}
// update the duration
if (parser.manifest.totalDuration) {
player.duration(parser.manifest.totalDuration);
} else {
player.duration(totalDuration(parser.manifest));
player.hls.master.playlists[i] =
videojs.m3u8.merge(playlist, parser.manifest);
}
}
} else {
// infer a master playlist if none was previously requested
player.hls.master = {
playlists: [parser.manifest]
};
}
// periodicaly check if the buffer needs to be refilled
player.on('timeupdate', fillBuffer);
// check the playlist for updates if EXT-X-ENDLIST isn't present
if (!parser.manifest.endList) {
window.setTimeout(function() {
downloadPlaylist(url);
}, refreshDelay);
}
player.trigger('loadedmanifest');
player.trigger('loadedmetadata');
fillBuffer();
return;
}
// always start playback with the default rendition
if (!player.hls.media) {
player.hls.media = player.hls.master.playlists[0];
// select a playlist and download its metadata if necessary
playlist = player.hls.selectPlaylist();
if (!playlist.segments) {
downloadPlaylist(resolveUrl(srcUrl, playlist.uri));
// update the duration
if (parser.manifest.totalDuration) {
player.duration(parser.manifest.totalDuration);
} else {
player.hls.media = playlist;
// update the duration
if (player.hls.media.totalDuration) {
player.duration(player.hls.media.totalDuration);
} else {
player.duration(totalDuration(player.hls.media));
}
player.duration(totalDuration(parser.manifest));
}
// periodicaly check if the buffer needs to be refilled
player.on('timeupdate', fillBuffer);
player.trigger('loadedmanifest');
player.trigger('loadedmetadata');
fillBuffer();
return;
}
// select a playlist and download its metadata if necessary
playlist = player.hls.selectPlaylist();
if (!playlist.segments) {
downloadPlaylist(resolveUrl(srcUrl, playlist.uri));
} else {
player.hls.media = playlist;
// update the duration
if (player.hls.media.totalDuration) {
player.duration(player.hls.media.totalDuration);
} else {
player.duration(totalDuration(player.hls.media));
}
}
player.trigger('loadedmanifest');
};
xhr.send(null);
};
......
......@@ -514,7 +514,7 @@
});
test('merges a manifest that strictly adds to an earlier one', function() {
var key, base, mid, parsed;
var key, base, manifest, mid;
for (key in window.manifests) {
if (window.expected[key]) {
manifest = window.manifests[key];
......
......@@ -20,5 +20,6 @@
"uri": "http://example.com/00004.ts"
}
],
"targetDuration": 10
"targetDuration": 10,
"endList": true
}
\ No newline at end of file
......
......@@ -140,5 +140,6 @@
"uri": "hls_450k_video.ts"
}
],
"targetDuration": 10
"targetDuration": 10,
"endList": true
}
\ No newline at end of file
......
......@@ -12,5 +12,6 @@
"uri": "hls_450k_video.ts"
}
],
"targetDuration": 10
"targetDuration": 10,
"endList": true
}
\ No newline at end of file
......
{
"allowCache": true,
"playlists": [{
"attributes": {
"PROGRAM-ID": 1,
"BANDWIDTH": 240000,
"RESOLUTION": {
"width": 396,
"height": 224
}
"playlists": [
{
"attributes": {
"PROGRAM-ID": 1,
"BANDWIDTH": 240000,
"RESOLUTION": {
"width": 396,
"height": 224
}
},
"uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824686811001&videoId=1824650741001"
},
"uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824686811001&videoId=1824650741001"
}, {
"attributes": {
"PROGRAM-ID": 1,
"BANDWIDTH": 40000
{
"attributes": {
"PROGRAM-ID": 1,
"BANDWIDTH": 40000
},
"uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824683759001&videoId=1824650741001"
},
"uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824683759001&videoId=1824650741001"
}, {
"attributes": {
"PROGRAM-ID": 1,
"BANDWIDTH": 440000,
"RESOLUTION": {
"width": 396,
"height": 224
}
{
"attributes": {
"PROGRAM-ID": 1,
"BANDWIDTH": 440000,
"RESOLUTION": {
"width": 396,
"height": 224
}
},
"uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824686593001&videoId=1824650741001"
},
"uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824686593001&videoId=1824650741001"
}, {
"attributes": {
"PROGRAM-ID": 1,
"BANDWIDTH": 1928000,
"RESOLUTION": {
"width": 960,
"height": 540
}
},
"uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824687660001&videoId=1824650741001"
}]
{
"attributes": {
"PROGRAM-ID": 1,
"BANDWIDTH": 1928000,
"RESOLUTION": {
"width": 960,
"height": 540
}
},
"uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824687660001&videoId=1824650741001"
}
]
}
......
......@@ -136,5 +136,6 @@
"uri": "hls_450k_video.ts"
}
],
"targetDuration": 10
"targetDuration": 10,
"endList": true
}
\ No newline at end of file
......
......@@ -12,5 +12,6 @@
"uri": "hls_450k_video.ts"
}
],
"targetDuration": 10
"targetDuration": 10,
"endList": true
}
\ No newline at end of file
......
......@@ -20,5 +20,6 @@
"uri": "/00004.ts"
}
],
"targetDuration": 10
"targetDuration": 10,
"endList": true
}
\ No newline at end of file
......
......@@ -12,5 +12,6 @@
"uri": "hls_450k_video.ts"
}
],
"targetDuration": 10
"targetDuration": 10,
"endList": true
}
\ No newline at end of file
......
......@@ -20,5 +20,6 @@
"uri": "/test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts"
}
],
"targetDuration": 8
"targetDuration": 8,
"endList": true
}
\ No newline at end of file
......
......@@ -27,5 +27,6 @@
"uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00006.ts"
}
],
"targetDuration": 10
}
"targetDuration": 10,
"endList": true
}
\ No newline at end of file
......
{
"allowCache": true,
"playlists": [{
"attributes": {
"PROGRAM-ID": 1,
"BANDWIDTH": 240000,
"RESOLUTION": {
"width": 396,
"height": 224
}
"playlists": [
{
"attributes": {
"PROGRAM-ID": 1,
"BANDWIDTH": 240000,
"RESOLUTION": {
"width": 396,
"height": 224
}
},
"uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824686811001&videoId=1824650741001"
},
"uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824686811001&videoId=1824650741001"
}, {
"attributes": {
"PROGRAM-ID": 1,
"BANDWIDTH": 40000
{
"attributes": {
"PROGRAM-ID": 1,
"BANDWIDTH": 40000
},
"uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824683759001&videoId=1824650741001"
},
"uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824683759001&videoId=1824650741001"
}, {
"attributes": {
"PROGRAM-ID": 1,
"BANDWIDTH": 440000,
"RESOLUTION": {
"width": 396,
"height": 224
}
{
"attributes": {
"PROGRAM-ID": 1,
"BANDWIDTH": 440000,
"RESOLUTION": {
"width": 396,
"height": 224
}
},
"uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824686593001&videoId=1824650741001"
},
"uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824686593001&videoId=1824650741001"
}, {
"attributes": {
"PROGRAM-ID": 1,
"BANDWIDTH": 1928000,
"RESOLUTION": {
"width": 960,
"height": 540
}
},
"uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824687660001&videoId=1824650741001"
}]
{
"attributes": {
"PROGRAM-ID": 1,
"BANDWIDTH": 1928000,
"RESOLUTION": {
"width": 960,
"height": 540
}
},
"uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824687660001&videoId=1824650741001"
}
]
}
......
......@@ -28,5 +28,6 @@
"uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00006.ts"
}
],
"targetDuration": 10
"targetDuration": 10,
"endList": true
}
\ No newline at end of file
......
......@@ -7,5 +7,6 @@
"uri": "/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts"
}
],
"targetDuration": 8
}
"targetDuration": 8,
"endList": true
}
\ No newline at end of file
......
......@@ -140,5 +140,6 @@
"uri": "hls_450k_video.ts"
}
],
"targetDuration": 10
}
"targetDuration": 10,
"endList": true
}
\ No newline at end of file
......
......@@ -12,5 +12,6 @@
"uri": "hls_450k_video.ts"
}
],
"targetDuration": 10
"targetDuration": 10,
"endList": true
}
\ No newline at end of file
......
......@@ -20,5 +20,6 @@
"uri": "/test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts"
}
],
"targetDuration": 8
"targetDuration": 8,
"endList": true
}
\ No newline at end of file
......
......@@ -27,5 +27,6 @@
"uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00006.ts"
}
],
"targetDuration": 10
}
"targetDuration": 10,
"endList": true
}
\ No newline at end of file
......
......@@ -139,5 +139,6 @@
"duration": 1.4167,
"uri": "hls_450k_video.ts"
}
]
],
"endList": true
}
\ No newline at end of file
......
......@@ -6,5 +6,6 @@
"duration": 10,
"uri": "/test/ts-files/zencoder/gogo/00001.ts"
}
]
}
],
"endList": true
}
\ No newline at end of file
......
......@@ -23,5 +23,6 @@
"uri": "/test/ts-files/zencoder/gogo/00005.ts"
}
],
"targetDuration": 10
}
"targetDuration": 10,
"endList": true
}
\ No newline at end of file
......
......@@ -7,5 +7,6 @@
"uri": "/test/ts-files/zencoder/gogo/00001.ts"
}
],
"targetDuration": 10
}
"targetDuration": 10,
"endList": true
}
\ No newline at end of file
......
{
"allowCache": true,
"playlists": [{
"attributes": {
"PROGRAM-ID": 1,
"BANDWIDTH": 240000,
"RESOLUTION": {
"width": 396,
"height": 224
}
"playlists": [
{
"attributes": {
"PROGRAM-ID": 1,
"BANDWIDTH": 240000,
"RESOLUTION": {
"width": 396,
"height": 224
}
},
"uri": "media.m3u8"
},
"uri": "media.m3u8"
}, {
"attributes": {
"PROGRAM-ID": 1,
"BANDWIDTH": 40000
{
"attributes": {
"PROGRAM-ID": 1,
"BANDWIDTH": 40000
},
"uri": "media1.m3u8"
},
"uri": "media1.m3u8"
}, {
"attributes": {
"PROGRAM-ID": 1,
"BANDWIDTH": 440000,
"RESOLUTION": {
"width": 396,
"height": 224
}
{
"attributes": {
"PROGRAM-ID": 1,
"BANDWIDTH": 440000,
"RESOLUTION": {
"width": 396,
"height": 224
}
},
"uri": "media2.m3u8"
},
"uri": "media2.m3u8"
}, {
"attributes": {
"PROGRAM-ID": 1,
"BANDWIDTH": 1928000,
"RESOLUTION": {
"width": 960,
"height": 540
}
},
"uri": "media3.m3u8"
}]
{
"attributes": {
"PROGRAM-ID": 1,
"BANDWIDTH": 1928000,
"RESOLUTION": {
"width": 960,
"height": 540
}
},
"uri": "media3.m3u8"
}
]
}
......
......@@ -20,5 +20,6 @@
"uri": "00004.ts"
}
],
"targetDuration": 10
"targetDuration": 10,
"endList": true
}
\ No newline at end of file
......
......@@ -20,5 +20,6 @@
"uri": "/test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts"
}
],
"targetDuration": 8
"targetDuration": 8,
"endList": true
}
\ No newline at end of file
......
{
"allowCache": true,
"mediaSequence": 0,
"segments": [
{
"duration": 10,
"uri": "00001.ts"
},
{
"duration": 10,
"uri": "00002.ts"
}
],
"targetDuration": 10
}
#EXTM3U
#EXT-X-TARGETDURATION:10
#EXTINF:10,
00001.ts
#EXTINF:10,
00002.ts
......@@ -16,5 +16,6 @@
"uri": "hls_450k_video.ts"
}
],
"targetDuration": 10
}
"targetDuration": 10,
"endList": true
}
\ No newline at end of file
......
......@@ -20,5 +20,6 @@
"uri": "/test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts"
}
],
"targetDuration": 8
"targetDuration": 8,
"endList": true
}
\ No newline at end of file
......
......@@ -20,5 +20,6 @@
"uri": "/test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts"
}
],
"targetDuration": 8
}
"targetDuration": 8,
"endList": true
}
\ No newline at end of file
......
......@@ -2,16 +2,21 @@
"allowCache": true,
"mediaSequence": 0,
"targetDuration": 10,
"segments": [{
"uri": "001.ts"
}, {
"uri": "002.ts",
"duration": 9
}, {
"uri": "003.ts",
"duration": 7
}, {
"uri": "004.ts",
"duration": 10
}]
"segments": [
{
"uri": "001.ts"
},
{
"uri": "002.ts",
"duration": 9
},
{
"uri": "003.ts",
"duration": 7
},
{
"uri": "004.ts",
"duration": 10
}
]
}
......
......@@ -20,5 +20,6 @@
"uri": "/test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts"
}
],
"targetDuration": 8
"targetDuration": 8,
"endList": true
}
\ No newline at end of file
......
......@@ -140,5 +140,6 @@
"uri": "hls_450k_video.ts"
}
],
"targetDuration": 10
"targetDuration": 10,
"endList": true
}
\ No newline at end of file
......
......@@ -8,5 +8,6 @@
"uri": "/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts"
}
],
"targetDuration": 8
"targetDuration": 8,
"endList": true
}
\ No newline at end of file
......
{
"allowCache": true,
"playlists": [
{
"attributes": {
"PROGRAM-ID": 1
{
"attributes": {
"PROGRAM-ID": 1
},
"uri": "media.m3u8"
},
"uri": "media.m3u8"
},
{
"uri": "media1.m3u8"
}
{
"uri": "media1.m3u8"
}
]
}
......
......@@ -20,5 +20,6 @@
"uri": "/test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts"
}
],
"targetDuration": 8
"targetDuration": 8,
"endList": true
}
\ No newline at end of file
......
......@@ -8,5 +8,6 @@
"uri": "hls_450k_video.ts"
}
],
"targetDuration": 10
"targetDuration": 10,
"endList": true
}
\ No newline at end of file
......
......@@ -181,7 +181,7 @@ test('calculates the duration if needed', function() {
}
durations.push(duration);
};
player.hls('manifest/liveMissingSegmentDuration.m3u8');
player.hls('http://example.com/manifest/liveMissingSegmentDuration.m3u8');
videojs.mediaSources[player.currentSrc()].trigger({
type: 'sourceopen'
});
......@@ -873,4 +873,87 @@ test('has no effect if native HLS is available', function() {
'no media source was opened');
});
test('reloads live playlists', function() {
var callbacks = [];
// capture timeouts
window.setTimeout = function(callback, timeout) {
callbacks.push({ callback: callback, timeout: timeout });
};
player.hls('manifest/missingEndlist.m3u8');
videojs.mediaSources[player.currentSrc()].trigger({
type: 'sourceopen'
});
strictEqual(1, callbacks.length, 'refresh was scheduled');
strictEqual(player.hls.media.targetDuration * 1000,
callbacks[0].timeout,
'waited one target duration');
});
test('does not reload playlists with an endlist tag', function() {
var callbacks = [];
// capture timeouts
window.setTimeout = function(callback, timeout) {
callbacks.push({ callback: callback, timeout: timeout });
};
player.hls('manifest/media.m3u8');
videojs.mediaSources[player.currentSrc()].trigger({
type: 'sourceopen'
});
strictEqual(0, callbacks.length, 'no refresh was scheduled');
});
test('reloads a live playlist after half a target duration if it has not ' +
'changed since the last request', function() {
var callbacks = [];
// capture timeouts
window.setTimeout = function(callback, timeout) {
callbacks.push({ callback: callback, timeout: timeout });
};
player.hls('http://example.com/manifest/missingEndlist.m3u8');
// an identical manifest has already been parsed
player.hls.media = videojs.util.mergeOptions({}, window.expected['missingEndlist']);
player.hls.media.uri = 'http://example.com/manifest/missingEndlist.m3u8';
player.hls.master = {
playlists: [player.hls.media]
};
videojs.mediaSources[player.currentSrc()].trigger({
type: 'sourceopen'
});
strictEqual(1, callbacks.length, 'refresh was scheduled');
strictEqual(player.hls.media.targetDuration / 2 * 1000,
callbacks[0].timeout,
'waited half a target duration');
});
test('merges playlist reloads', function() {
var
realMerge = videojs.m3u8.merge,
merges = 0,
callback;
// capture timeouts and playlist merges
window.setTimeout = function(cb) {
callback = cb;
};
videojs.m3u8.merge = function(base, update) {
merges++;
return update;
};
player.hls('http://example.com/manifest/missingEndlist.m3u8');
videojs.mediaSources[player.currentSrc()].trigger({
type: 'sourceopen'
});
player.hls.media.uri = 'http://example.com/manifest/missingEndlist.m3u8';
callback();
strictEqual(1, merges, 'reloaded playlist was merged');
videojs.m3u8.merge = realMerge;
});
})(window, window.videojs);
......