3412f5fc by Gary Katsevman

Merge pull request #34 from videojs/feature/live-um

Experimental Live HLS support
2 parents 59139145 ae60f03a
# Live HLS Research
This document is a collection of notes on Live HLS implementations in the wild.
There are two varieties of Live HLS. In the first, playlists are
persistent and strictly appended to. In the alternative form, the
maximum number of segments in a playlist is relatively stable and an
old segment is removed every time a new segment becomes available.
On iOS devices, both stream types report a duration of `Infinity`. The
`currentTime` is equal to the amount of the stream that has been
played back on the device.
## Akamai HD2
## OnceLIVE
"Sliding window" live streams.
### Variant Playlists
Once variant playlists look like standard HLS variant playlists.
### Media Playlists
OnceLIVE uses "sliding window" manifests for live playback. The media
playlists do not have an `EXT-X-ENDLIST` and don't declare a
`EXT-X-PLAYLIST-TYPE`. On first request, the stream media playlist
returned four segment URLs with a starting media sequence of one,
preceded by a `EXT-X-DISCONTINUITY` tag. As playback progressed, that
number grew to 13 segment URLs, at which point it stabilized. That
would equate to a steady-state 65 second window at 5 seconds per
segment.
OnceLive documentation is [available on the Unicorn Media
website](http://www.unicornmedia.com/documents/2013/02/oncelive_implementationguide.pdf).
Here's a script to quickly parse out segment URLs:
```shell
curl $ONCE_MEDIA_PLAYLIST | grep '^http'
```
An example media playlist might look something like this:
```m3u8
#EXTM3U
#EXT-X-TARGETDURATION:5
#EXT-X-MEDIA-SEQUENCE:3
#EXTINF:5,3
http://example.com/0/1/content.ts?visitguid=uuid&asseturl=http://once.example.com/asset.lrm&failoverurl=http://example.com/blank.jpg
#EXTINF:5,4
http://example.com/1/2/content.ts?visitguid=uuid&asseturl=http://once.example.com/asset.lrm&failoverurl=http://example.com/blank.jpg
#EXTINF:5,5
http://example.com/2/3/content.ts?visitguid=uuid&asseturl=http://once.example.com/asset.lrm&failoverurl=http://example.com/blank.jpg
#EXTINF:5,6
http://example.com/3/4/content.ts?visitguid=uuid&asseturl=http://once.example.com/asset.lrm&failoverurl=http://example.com/blank.jpg
```
## Zencoder Live
......@@ -344,13 +344,10 @@
byterange.offset = entry.offset;
}
},
'endlist': function() {
this.manifest.endList = true;
},
'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', {
......
(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,15 @@
ok(!event, 'no event is triggered');
});
module('m3u8 parser', {
setup: function() {
parser = new Parser();
}
});
module('m3u8 parser');
test('should create a parser', function() {
notStrictEqual(parser, undefined, 'parser is defined');
test('can be constructed', function() {
notStrictEqual(new Parser(), undefined, 'parser is defined');
});
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]) {
......
......@@ -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
......
{
"allowCache": true,
"mediaSequence": 0,
"playlistType": "VOD",
"segments": [
{
"duration": 10,
......@@ -28,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
......
{
"allowCache": true,
"mediaSequence": 1,
"playlistType": "VOD",
"segments": [
{
"duration": 6.64,
"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
......
{
"allowCache": true,
"mediaSequence": 0,
"playlistType": "VOD",
"segments": [
{
"duration": 10,
......@@ -28,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
......
{
"allowCache": true,
"mediaSequence": 0,
"playlistType": "VOD",
"segments": [
{
"duration": 10,
"uri": "/test/ts-files/zencoder/gogo/00001.ts"
}
]
],
"endList": true
}
\ No newline at end of file
......
{
"allowCache": true,
"mediaSequence": 0,
"playlistType": "VOD",
"segments": [
{
"duration": 10,
......@@ -24,5 +23,6 @@
"uri": "/test/ts-files/zencoder/gogo/00005.ts"
}
],
"targetDuration": 10
"targetDuration": 10,
"endList": true
}
\ No newline at end of file
......
{
"allowCache": true,
"mediaSequence": 0,
"playlistType": "VOD",
"segments": [
{
"duration": 10,
"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
......
{
"allowCache": true,
"mediaSequence": 0,
"playlistType": "VOD",
"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
......
......@@ -105,6 +105,7 @@ module('HLS', {
this.readyState = 4;
this.onreadystatechange();
};
this.abort = function() {};
};
xhrUrls = [];
},
......@@ -170,7 +171,7 @@ test('sets the duration if one is available on the playlist', function() {
type: 'sourceopen'
});
strictEqual(1, calls, 'duration is set');
strictEqual(calls, 2, 'duration is set');
});
test('calculates the duration if needed', function() {
......@@ -181,13 +182,15 @@ test('calculates the duration if needed', function() {
}
durations.push(duration);
};
player.hls('manifest/liveMissingSegmentDuration.m3u8');
player.hls('http://example.com/manifest/missingExtinf.m3u8');
videojs.mediaSources[player.currentSrc()].trigger({
type: 'sourceopen'
});
strictEqual(durations.length, 1, 'duration is set');
strictEqual(durations[0], 6.64 + (2 * 8), 'duration is calculated');
strictEqual(durations.length, 2, 'duration is set');
strictEqual(durations[0],
player.hls.media.segments.length * 10,
'duration is calculated');
});
test('starts downloading a segment on loadedmetadata', function() {
......@@ -400,15 +403,12 @@ test('downloads additional playlists if required', function() {
called = true;
return playlist;
}
playlist.segments = [];
playlist.segments = [1, 1, 1];
return playlist;
};
xhrUrls = [];
// the playlist selection is revisited after a new segment is downloaded
player.currentTime = function() {
return 1;
};
player.trigger('timeupdate');
strictEqual(2, xhrUrls.length, 'requests were made');
......@@ -867,10 +867,246 @@ test('segment 500 should trigger MEDIA_ERR_ABORTED', function () {
test('has no effect if native HLS is available', function() {
videojs.hls.supportsNativeHls = true;
player.hls('manifest/master.m3u8');
player.hls('http://example.com/manifest/master.m3u8');
ok(!(player.currentSrc() in videojs.mediaSources),
'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('http://example.com/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('duration is Infinity for live playlists', function() {
player.hls('http://example.com/manifest/missingEndlist.m3u8');
videojs.mediaSources[player.currentSrc()].trigger({
type: 'sourceopen'
});
strictEqual(Infinity, player.duration(), 'duration is infinity');
});
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');
videojs.mediaSources[player.currentSrc()].trigger({
type: 'sourceopen'
});
strictEqual(callbacks.length, 1, 'full-length refresh scheduled');
callbacks.pop().callback();
strictEqual(1, callbacks.length, 'half-length refresh was scheduled');
strictEqual(callbacks[0].timeout,
player.hls.media.targetDuration / 2 * 1000,
'waited half a target duration');
});
test('merges playlist reloads', function() {
var
oldPlaylist,
callback;
// capture timeouts
window.setTimeout = function(cb) {
callback = cb;
};
player.hls('http://example.com/manifest/missingEndlist.m3u8');
videojs.mediaSources[player.currentSrc()].trigger({
type: 'sourceopen'
});
oldPlaylist = player.hls.media;
callback();
ok(oldPlaylist !== player.hls.media, 'player.hls.media was updated');
});
test('updates the media index when a playlist reloads', function() {
var callback;
window.setTimeout = function(cb) {
callback = cb;
};
// the initial playlist
window.manifests['live-updating'] =
'#EXTM3U\n' +
'#EXTINF:10,\n' +
'0.ts\n' +
'#EXTINF:10,\n' +
'1.ts\n' +
'#EXTINF:10,\n' +
'2.ts\n';
player.hls('http://example.com/live-updating.m3u8');
videojs.mediaSources[player.currentSrc()].trigger({
type: 'sourceopen'
});
// play the stream until 2.ts is playing
player.hls.mediaIndex = 3;
// reload the updated playlist
window.manifests['live-updating'] =
'#EXTM3U\n' +
'#EXTINF:10,\n' +
'1.ts\n' +
'#EXTINF:10,\n' +
'2.ts\n' +
'#EXTINF:10,\n' +
'3.ts\n';
callback();
strictEqual(player.hls.mediaIndex, 2, 'mediaIndex is updated after the reload');
});
test('mediaIndex is zero before the first segment loads', function() {
window.manifests['first-seg-load'] =
'#EXTM3U\n' +
'#EXTINF:10,\n' +
'0.ts\n';
window.XMLHttpRequest = function() {
this.open = function() {};
this.send = function() {};
};
player.hls('http://example.com/first-seg-load.m3u8');
videojs.mediaSources[player.currentSrc()].trigger({
type: 'sourceopen'
});
strictEqual(player.hls.mediaIndex, 0, 'mediaIndex is zero');
});
test('reloads out-of-date live playlists when switching variants', function() {
player.hls('http://example.com/master.m3u8');
videojs.mediaSources[player.currentSrc()].trigger({
type: 'sourceopen'
});
player.hls.master = {
playlists: [{
mediaSequence: 15,
segments: [1, 1, 1]
}, {
uri: 'http://example.com/variant-update.m3u8',
mediaSequence: 0,
segments: [1, 1]
}]
};
// playing segment 15 on playlist zero
player.hls.media = player.hls.master.playlists[0];
player.mediaIndex = 1;
window.manifests['variant-update'] = '#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:16\n' +
'#EXTINF:10,\n' +
'16.ts\n' +
'#EXTINF:10,\n' +
'17.ts\n';
// switch playlists
player.hls.selectPlaylist = function() {
return player.hls.master.playlists[1];
};
// timeupdate downloads segment 16 then switches playlists
player.trigger('timeupdate');
strictEqual(player.mediaIndex, 1, 'mediaIndex points at the next segment');
});
test('does not reload master playlists', function() {
var callbacks = [];
window.setTimeout = function(callback) {
callbacks.push(callback);
};
player.hls('http://example.com/master.m3u8');
videojs.mediaSources[player.currentSrc()].trigger({
type: 'sourceopen'
});
strictEqual(callbacks.length, 0, 'no reload scheduled');
});
test('only reloads the active media playlist', function() {
var callbacks = [], urls = [], responses = [];
window.setTimeout = function(callback) {
callbacks.push(callback);
};
player.hls('http://example.com/missingEndlist.m3u8');
videojs.mediaSources[player.currentSrc()].trigger({
type: 'sourceopen'
});
window.XMLHttpRequest = function() {
this.open = function(method, url) {
urls.push(url);
};
this.send = function() {
var xhr = this;
responses.push(function() {
xhr.readyState = 4;
xhr.responseText = '#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:1\n' +
'#EXTINF:10,\n' +
'1.ts\n';
xhr.response = new Uint8Array([1]).buffer;
xhr.onreadystatechange();
});
};
};
player.hls.selectPlaylist = function() {
return player.hls.master.playlists[1];
};
player.hls.master.playlists.push({
uri: 'http://example.com/switched.m3u8'
});
player.trigger('timeupdate');
strictEqual(callbacks.length, 1, 'a refresh is scheduled');
strictEqual(responses.length, 1, 'segment requested');
responses.shift()(); // segment response
responses.shift()(); // loaded switched.m3u8
urls = [];
callbacks.shift()(); // out-of-date refresh of missingEndlist.m3u8
callbacks.shift()(); // refresh switched.m3u8
strictEqual(urls.length, 1, 'one refresh was made');
strictEqual(urls[0],
'http://example.com/switched.m3u8',
'refreshed the active playlist');
});
})(window, window.videojs);
......