76b977c7 by Gary Katsevman

Merge branch 'master' into saucytravis3_thefinaldimension

Conflicts:
	package.json
2 parents 56795555 3412f5fc
# 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
......@@ -61,7 +61,7 @@
type="application/x-mpegURL">
</video>
<script>
videojs.options.flash.swf = 'node_modules/videojs-swf/dist/video-js.swf';
videojs.options.flash.swf = 'node_modules/video.js/dist/video-js/video-js.swf';
// initialize the player
var player = videojs('video');
......
{
"name": "videojs-contrib-hls",
"version": "0.3.1",
"version": "0.3.2",
"engines": {
"node": ">= 0.10.12"
},
......@@ -34,10 +34,12 @@
"karma-phantomjs-launcher": "~0.1.1",
"karma-safari-launcher": "~0.1.1",
"karma-qunit": "~0.1.1"
"video.js": "^4.5"
},
"peerDependencies": {
"video.js": "^4.5"
},
"dependencies": {
"video.js": "git+https://github.com/videojs/video.js.git#v4.4.1",
"videojs-swf": "git+https://github.com/videojs/video-js-swf.git#v4.4.0",
"videojs-contrib-media-sources": "git+https://github.com/videojs/videojs-contrib-media-sources.git"
}
}
......
......@@ -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', {
......
......@@ -95,6 +95,44 @@ var
},
/**
* Creates and sends an XMLHttpRequest.
* @param options {string | object} if this argument is a string, it
* is intrepreted as a URL and a simple GET request is
* inititated. If it is an object, it should contain a `url`
* property that indicates the URL to request and optionally a
* `method` which is the type of HTTP request to send.
* @return {object} the XMLHttpRequest that was initiated.
*/
xhr = function(url, callback) {
var
options = {
method: 'GET'
},
request;
if (typeof url === 'object') {
options = videojs.util.mergeOptions(options, url);
url = options.url;
}
request = new window.XMLHttpRequest();
request.open(options.method, url);
request.onreadystatechange = function() {
// wait until the request completes
if (this.readyState !== 4) {
return;
}
// request error
if (this.status >= 400 || this.status === 0) {
return callback.call(this, true, url);
}
return callback.call(this, false, url);
};
request.send(null);
return request;
},
/**
* TODO - Document this great feature.
*
* @param playlist
......@@ -123,6 +161,42 @@ var
},
/**
* Determine the media index in one playlist that corresponds to a
* specified media index in another. This function can be used to
* calculate a new segment position when a playlist is reloaded or a
* variant playlist is becoming active.
* @param mediaIndex {number} the index into the original playlist
* to translate
* @param original {object} the playlist to translate the media
* index from
* @param update {object} the playlist to translate the media index
* to
* @param {number} the corresponding media index in the updated
* playlist
*/
translateMediaIndex = function(mediaIndex, original, update) {
var
i = update.segments.length,
originalSegment;
// no segments have been loaded from the original playlist
if (mediaIndex === 0) {
return 0;
}
// try to sync based on URI
originalSegment = original.segments[mediaIndex - 1];
while (i--) {
if (originalSegment.uri === update.segments[i].uri) {
return i + 1;
}
}
// sync on media sequence
return (original.mediaSequence + mediaIndex) - update.mediaSequence;
},
/**
* Calculate the total duration for a playlist based on segment metadata.
* @param playlist {object} a media playlist object
* @return {number} the currently known duration, in seconds
......@@ -130,8 +204,24 @@ var
totalDuration = function(playlist) {
var
duration = 0,
i = playlist.segments.length,
i,
segment;
if (!playlist.segments) {
return 0;
}
i = playlist.segments.length;
// if present, use the duration specified in the playlist
if (playlist.totalDuration) {
return playlist.totalDuration;
}
// duration should be Infinity for live playlists
if (!playlist.endList) {
return window.Infinity;
}
while (i--) {
segment = playlist.segments[i];
duration += segment.duration || playlist.targetDuration || 0;
......@@ -198,9 +288,11 @@ var
}),
srcUrl,
playlistXhr,
segmentXhr,
downloadPlaylist,
fillBuffer;
loadedPlaylist,
fillBuffer,
updateCurrentPlaylist;
// if the video element supports HLS natively, do nothing
if (videojs.hls.supportsNativeHls) {
......@@ -288,6 +380,36 @@ var
fillBuffer(currentTime * 1000);
});
/**
* Determine whether the current media playlist should be changed
* and trigger a switch if necessary. If a sufficiently fresh
* version of the target playlist is available, the switch will take
* effect immediately. Otherwise, the target playlist will be
* refreshed.
*/
updateCurrentPlaylist = function() {
var playlist, mediaSequence;
playlist = player.hls.selectPlaylist();
mediaSequence = player.hls.mediaIndex + (player.hls.media.mediaSequence || 0);
if (!playlist.segments ||
mediaSequence < (playlist.mediaSequence || 0) ||
mediaSequence > (playlist.mediaSequence || 0) + playlist.segments.length) {
if (playlistXhr) {
playlistXhr.abort();
}
playlistXhr = xhr(resolveUrl(srcUrl, playlist.uri), loadedPlaylist);
} else {
player.hls.mediaIndex =
translateMediaIndex(player.hls.mediaIndex,
player.hls.media,
playlist);
player.hls.media = playlist;
// update the duration
player.duration(totalDuration(player.hls.media));
}
};
/**
* Chooses the appropriate media playlist based on the current
......@@ -341,8 +463,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;
}
......@@ -361,63 +485,72 @@ var
};
/**
* Download an M3U8 and update the current manifest object. If the provided
* URL is a master playlist, the default variant will be downloaded and
* parsed as well. Triggers `loadedmanifest` once for each playlist that is
* downloaded and `loadedmetadata` after at least one media playlist has
* been parsed. Whether multiple playlists were downloaded or not, when
* `loadedmetadata` fires a parsed or inferred master playlist object will
* be available as `player.hls.master`.
* Callback that is invoked when a media playlist finishes
* downloading. Triggers `loadedmanifest` once for each playlist
* that is downloaded and `loadedmetadata` after at least one
* media playlist has been parsed.
*
* @param error {*} truthy if the request was not successful
* @param url {string} a URL to the M3U8 file to process
*/
downloadPlaylist = function(url) {
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) {
loadedPlaylist = function(error, url) {
var i, parser, playlist, playlistUri, refreshDelay;
// clear the current playlist XHR
playlistXhr = null;
if (error) {
player.hls.error = {
status: xhr.status,
status: this.status,
message: 'HLS playlist request error at URL: ' + url,
code: (xhr.status >= 500) ? 4 : 2
code: (this.status >= 500) ? 4 : 2
};
player.trigger('error');
return;
return player.trigger('error');
}
// readystate DONE
parser = new videojs.m3u8.Parser();
parser.push(xhr.responseText);
parser.push(this.responseText);
// master playlists
if (parser.manifest.playlists) {
player.hls.master = parser.manifest;
downloadPlaylist(resolveUrl(url, parser.manifest.playlists[0].uri));
player.trigger('loadedmanifest');
return;
}
// media playlists
if (player.hls.master) {
// merge this playlist into the master
i = player.hls.master.playlists.length;
refreshDelay = (parser.manifest.targetDuration || 10) * 1000;
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;
}
player.hls.master.playlists[i] =
videojs.util.mergeOptions(playlist, parser.manifest);
if (playlist !== player.hls.media) {
continue;
}
// determine the new mediaIndex if we're updating the
// current media playlist
player.hls.mediaIndex =
translateMediaIndex(player.hls.mediaIndex,
playlist,
parser.manifest);
player.hls.media = parser.manifest;
}
} else {
// infer a master playlist if none was previously requested
player.hls.master = {
playlists: [parser.manifest]
};
}
// check the playlist for updates if EXT-X-ENDLIST isn't present
if (!parser.manifest.endList) {
window.setTimeout(function() {
if (!playlistXhr &&
resolveUrl(srcUrl, player.hls.media.uri) === url) {
playlistXhr = xhr(url, loadedPlaylist);
}
}, refreshDelay);
}
// always start playback with the default rendition
......@@ -425,11 +558,7 @@ var
player.hls.media = player.hls.master.playlists[0];
// update the duration
if (parser.manifest.totalDuration) {
player.duration(parser.manifest.totalDuration);
} else {
player.duration(totalDuration(parser.manifest));
}
// periodicaly check if the buffer needs to be refilled
player.on('timeupdate', fillBuffer);
......@@ -441,24 +570,9 @@ var
}
// 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));
}
}
updateCurrentPlaylist();
player.trigger('loadedmanifest');
}
};
xhr.send(null);
};
/**
......@@ -495,16 +609,19 @@ var
return;
}
// resolve the segment URL relative to the playlist
if (player.hls.media.uri === srcUrl) {
segmentUri = resolveUrl(srcUrl, segment.uri);
} else {
segmentUri = resolveUrl(resolveUrl(srcUrl, player.hls.media.uri || ''),
segment.uri);
}
// request the next segment
segmentXhr = new window.XMLHttpRequest();
segmentXhr.open('GET', segmentUri);
segmentXhr.responseType = 'arraybuffer';
segmentXhr.onreadystatechange = function() {
var playlist;
// wait until the request completes
if (this.readyState !== 4) {
return;
......@@ -559,12 +676,7 @@ var
// figure out what stream the next segment should be downloaded from
// with the updated bandwidth information
playlist = player.hls.selectPlaylist();
if (!playlist.segments) {
downloadPlaylist(resolveUrl(srcUrl, playlist.uri));
} else {
player.hls.media = playlist;
}
updateCurrentPlaylist();
};
startTime = +new Date();
segmentXhr.send(null);
......@@ -578,7 +690,26 @@ var
sourceBuffer.appendBuffer(segmentParser.getFlvHeader());
player.hls.mediaIndex = 0;
downloadPlaylist(srcUrl);
xhr(srcUrl, function(error, url) {
var uri, parser = new videojs.m3u8.Parser();
parser.push(this.responseText);
// master playlists
if (parser.manifest.playlists) {
player.hls.master = parser.manifest;
playlistXhr = xhr(resolveUrl(url, parser.manifest.playlists[0].uri), loadedPlaylist);
return player.trigger('loadedmanifest');
} else {
// infer a master playlist if a media playlist is loaded directly
uri = resolveUrl(window.location.href, url);
player.hls.master = {
playlists: [{
uri: uri
}]
};
loadedPlaylist.call(this, error, uri);
}
});
});
player.src([{
src: videojs.URL.createObjectURL(mediaSource),
......
(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": [{
"playlists": [
{
"attributes": {
"PROGRAM-ID": 1,
"BANDWIDTH": 240000,
......@@ -10,13 +11,15 @@
}
},
"uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824686811001&videoId=1824650741001"
}, {
},
{
"attributes": {
"PROGRAM-ID": 1,
"BANDWIDTH": 40000
},
"uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824683759001&videoId=1824650741001"
}, {
},
{
"attributes": {
"PROGRAM-ID": 1,
"BANDWIDTH": 440000,
......@@ -26,7 +29,8 @@
}
},
"uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824686593001&videoId=1824650741001"
}, {
},
{
"attributes": {
"PROGRAM-ID": 1,
"BANDWIDTH": 1928000,
......@@ -36,5 +40,6 @@
}
},
"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": [{
"playlists": [
{
"attributes": {
"PROGRAM-ID": 1,
"BANDWIDTH": 240000,
......@@ -10,13 +11,15 @@
}
},
"uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824686811001&videoId=1824650741001"
}, {
},
{
"attributes": {
"PROGRAM-ID": 1,
"BANDWIDTH": 40000
},
"uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824683759001&videoId=1824650741001"
}, {
},
{
"attributes": {
"PROGRAM-ID": 1,
"BANDWIDTH": 440000,
......@@ -26,7 +29,8 @@
}
},
"uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824686593001&videoId=1824650741001"
}, {
},
{
"attributes": {
"PROGRAM-ID": 1,
"BANDWIDTH": 1928000,
......@@ -36,5 +40,6 @@
}
},
"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": [{
"playlists": [
{
"attributes": {
"PROGRAM-ID": 1,
"BANDWIDTH": 240000,
......@@ -10,13 +11,15 @@
}
},
"uri": "media.m3u8"
}, {
},
{
"attributes": {
"PROGRAM-ID": 1,
"BANDWIDTH": 40000
},
"uri": "media1.m3u8"
}, {
},
{
"attributes": {
"PROGRAM-ID": 1,
"BANDWIDTH": 440000,
......@@ -26,7 +29,8 @@
}
},
"uri": "media2.m3u8"
}, {
},
{
"attributes": {
"PROGRAM-ID": 1,
"BANDWIDTH": 1928000,
......@@ -36,5 +40,6 @@
}
},
"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": [{
"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
......
......@@ -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);
......