0de2141b by brandonocasey

fixed style issues, bare playlist-loader conversion

fixed pre-existing assertion duplication issues
1 parent a07f52ee
......@@ -12,44 +12,16 @@ import Stream from './stream';
import m3u8 from './m3u8';
/**
* Returns a new array of segments that is the result of merging
* properties from an older list of segments onto an updated
* list. No properties on the updated playlist will be overridden.
* @param original {array} the outdated list of segments
* @param update {array} the updated list of segments
* @param offset {number} (optional) the index of the first update
* segment in the original segment list. For non-live playlists,
* this should always be zero and does not need to be
* specified. For live playlists, it should be the difference
* between the media sequence numbers in the original and updated
* playlists.
* @return a list of merged segment objects
*/
const updateSegments = function(original, update, offset) {
let result = update.slice();
let length;
let i;
offset = offset || 0;
length = Math.min(original.length, update.length + offset);
for (i = offset; i < length; i++) {
result[i - offset] = mergeOptions(original[i], result[i - offset]);
}
return result;
};
/**
* Returns a new master playlist that is the result of merging an
* updated media playlist into the original version. If the
* updated media playlist does not match any of the playlist
* entries in the original master playlist, null is returned.
* @param master {object} a parsed master M3U8 object
* @param media {object} a parsed media M3U8 object
* @return {object} a new object that represents the original
* master playlist with the updated media playlist merged in, or
* null if the merge produced no change.
*/
* Returns a new master playlist that is the result of merging an
* updated media playlist into the original version. If the
* updated media playlist does not match any of the playlist
* entries in the original master playlist, null is returned.
* @param master {object} a parsed master M3U8 object
* @param media {object} a parsed media M3U8 object
* @return {object} a new object that represents the original
* master playlist with the updated media playlist merged in, or
* null if the merge produced no change.
*/
const updateMaster = function(master, media) {
let changed = false;
let result = mergeOptions(master, {});
......@@ -85,383 +57,408 @@ const updateMaster = function(master, media) {
return changed ? result : null;
};
export default class PlaylistLoader extends Stream {
constructor(srcUrl, withCredentials) {
super();
this.srcUrl_ = srcUrl;
this.withCredentials_ = withCredentials;
this.mediaUpdateTimeout_ = null;
// initialize the loader state
this.state = 'HAVE_NOTHING';
// track the time that has expired from the live window
// this allows the seekable start range to be calculated even if
// all segments with timing information have expired
this.expired_ = 0;
// a flag that disables "expired time"-tracking this setting has
// no effect when not playing a live stream
this.trackExpiredTime_ = false;
if (!this.srcUrl_) {
throw new Error('A non-empty playlist URL is required');
}
// In a live list, don't keep track of the expired time until
// HLS tells us that "first play" has commenced
this.on('firstplay', function() {
this.trackExpiredTime_ = true;
});
// live playlist staleness timeout
this.on('mediaupdatetimeout', () => {
if (this.state !== 'HAVE_METADATA') {
// only refresh the media playlist if no other activity is going on
return;
}
this.state = 'HAVE_CURRENT_METADATA';
this.request_ = XhrModule({
uri: resolveUrl(this.master.uri, this.media().uri),
withCredentials: this.withCredentials_
}, (error, request) => {
if (error) {
return this.playlistRequestError_(request, this.media().uri);
}
this.haveMetadata_(request, this.media().uri);
});
});
// request the specified URL
this.request_ = XhrModule({
uri: this.srcUrl_,
withCredentials: this.withCredentials_
}, (error, request) => {
let parser = new m3u8.Parser();
let i;
// clear the loader's request reference
this.request_ = null;
if (error) {
this.error = {
status: request.status,
message: 'HLS playlist request error at URL: ' + this.srcUrl_,
responseText: request.responseText,
// MEDIA_ERR_NETWORK
code: 2
};
return this.trigger('error');
}
parser.push(request.responseText);
parser.end();
this.state = 'HAVE_MASTER';
parser.manifest.uri = this.srcUrl_;
// loaded a master playlist
if (parser.manifest.playlists) {
this.master = parser.manifest;
// setup by-URI lookups
i = this.master.playlists.length;
while (i--) {
this.master.playlists[this.master.playlists[i].uri] =
this.master.playlists[i];
}
this.trigger('loadedplaylist');
if (!this.request_) {
// no media playlist was specifically selected so start
// from the first listed one
this.media(parser.manifest.playlists[0]);
}
return;
}
// loaded a media playlist
// infer a master playlist if none was previously requested
this.master = {
uri: window.location.href,
playlists: [{
uri: this.srcUrl_
}]
};
this.master.playlists[this.srcUrl_] = this.master.playlists[0];
this.haveMetadata_(request, this.srcUrl_);
return this.trigger('loadedmetadata');
});
}
playlistRequestError_(xhr, url, startingState) {
this.setBandwidth(this.request_ || xhr);
// any in-flight request is now finished
this.request_ = null;
if (startingState) {
this.state = startingState;
}
this.error = {
playlist: this.master.playlists[url],
status: xhr.status,
message: 'HLS playlist request error at URL: ' + url,
responseText: xhr.responseText,
code: (xhr.status >= 500) ? 4 : 2
};
this.trigger('error');
}
// update the playlist loader's state in response to a new or
// updated playlist.
haveMetadata_(xhr, url) {
let parser;
let refreshDelay;
let update;
this.setBandwidth(this.request_ || xhr);
// any in-flight request is now finished
this.request_ = null;
this.state = 'HAVE_METADATA';
parser = new m3u8.Parser();
parser.push(xhr.responseText);
parser.end();
parser.manifest.uri = url;
// merge this playlist into the master
update = updateMaster(this.master, parser.manifest);
refreshDelay = (parser.manifest.targetDuration || 10) * 1000;
if (update) {
this.master = update;
this.updateMediaPlaylist_(parser.manifest);
} else {
// if the playlist is unchanged since the last reload,
// try again after half the target duration
refreshDelay /= 2;
}
// refresh live playlists after a target duration passes
if (!this.media().endList) {
this.clearMediaUpdateTimeout_();
this.mediaUpdateTimeout_ = window.setTimeout(() => {
this.trigger('mediaupdatetimeout');
}, refreshDelay);
}
this.trigger('loadedplaylist');
}
clearMediaUpdateTimeout_() {
if (this.mediaUpdateTimeout_) {
window.clearTimeout(this.mediaUpdateTimeout_);
}
}
requestDispose_() {
if (this.request_) {
this.request_.onreadystatechange = null;
this.request_.abort();
this.request_ = null;
}
}
/**
* Abort any outstanding work and clean up.
*/
dispose() {
this.requestDispose_();
this.clearMediaUpdateTimeout_();
super.dispose();
}
/**
* When called without any arguments, returns the currently
* active media playlist. When called with a single argument,
* triggers the playlist loader to asynchronously switch to the
* specified media playlist. Calling this method while the
* loader is in the HAVE_NOTHING causes an error to be emitted
* but otherwise has no effect.
* @param playlist (optional) {object} the parsed media playlist
* object to switch to
*/
media(playlist) {
let startingState = this.state;
let mediaChange;
// getter
if (!playlist) {
return this.media_;
}
// setter
if (this.state === 'HAVE_NOTHING') {
throw new Error('Cannot switch media playlist from ' + this.state);
}
// find the playlist object if the target playlist has been
// specified by URI
if (typeof playlist === 'string') {
if (!this.master.playlists[playlist]) {
throw new Error('Unknown playlist URI: ' + playlist);
}
playlist = this.master.playlists[playlist];
}
mediaChange = !this.media_ || playlist.uri !== this.media_.uri;
// switch to fully loaded playlists immediately
if (this.master.playlists[playlist.uri].endList) {
// abort outstanding playlist requests
this.requestDispose_();
this.state = 'HAVE_METADATA';
this.media_ = playlist;
// trigger media change if the active media has been updated
if (mediaChange) {
this.trigger('mediachange');
}
return;
}
// switching to the active playlist is a no-op
if (!mediaChange) {
return;
}
this.state = 'SWITCHING_MEDIA';
// there is already an outstanding playlist request
if (this.request_) {
if (resolveUrl(this.master.uri, playlist.uri) === this.request_.url) {
// requesting to switch to the same playlist multiple times
// has no effect after the first
return;
}
this.requestDispose_();
}
// request the new playlist
this.request_ = XhrModule({
uri: resolveUrl(this.master.uri, playlist.uri),
withCredentials: this.withCredentials_
}, (error, request) => {
if (error) {
return this.playlistRequestError_(request, playlist.uri, startingState);
}
this.haveMetadata_(request, playlist.uri);
if (error) {
return;
}
// fire loadedmetadata the first time a media playlist is loaded
if (startingState === 'HAVE_MASTER') {
this.trigger('loadedmetadata');
} else {
this.trigger('mediachange');
}
});
}
setBandwidth(xhr) {
this.bandwidth = xhr.bandwidth;
}
/**
* Update the PlaylistLoader state to reflect the changes in an
* update to the current media playlist.
* @param update {object} the updated media playlist object
*/
updateMediaPlaylist_(update) {
let outdated;
let i;
let segment;
outdated = this.media_;
this.media_ = this.master.playlists[update.uri];
if (!outdated) {
return;
}
// don't track expired time until this flag is truthy
if (!this.trackExpiredTime_) {
return;
}
// if the update was the result of a rendition switch do not
// attempt to calculate expired_ since media-sequences need not
// correlate between renditions/variants
if (update.uri !== outdated.uri) {
return;
}
// try using precise timing from first segment of the updated
// playlist
if (update.segments.length) {
if (typeof update.segments[0].start !== 'undefined') {
this.expired_ = update.segments[0].start;
return;
} else if (typeof update.segments[0].end !== 'undefined') {
this.expired_ = update.segments[0].end - update.segments[0].duration;
return;
}
}
// calculate expired by walking the outdated playlist
i = update.mediaSequence - outdated.mediaSequence - 1;
for (; i >= 0; i--) {
segment = outdated.segments[i];
/**
* Returns a new array of segments that is the result of merging
* properties from an older list of segments onto an updated
* list. No properties on the updated playlist will be overridden.
* @param original {array} the outdated list of segments
* @param update {array} the updated list of segments
* @param offset {number} (optional) the index of the first update
* segment in the original segment list. For non-live playlists,
* this should always be zero and does not need to be
* specified. For live playlists, it should be the difference
* between the media sequence numbers in the original and updated
* playlists.
* @return a list of merged segment objects
*/
const updateSegments = function(original, update, offset) {
let result = update.slice();
let length;
let i;
if (!segment) {
// we missed information on this segment completely between
// playlist updates so we'll have to take an educated guess
// once we begin buffering again, any error we introduce can
// be corrected
this.expired_ += outdated.targetDuration || 10;
continue;
}
offset = offset || 0;
length = Math.min(original.length, update.length + offset);
if (typeof segment.end !== 'undefined') {
this.expired_ = segment.end;
return;
}
if (typeof segment.start !== 'undefined') {
this.expired_ = segment.start + segment.duration;
return;
}
this.expired_ += segment.duration;
}
for (i = offset; i < length; i++) {
result[i - offset] = mergeOptions(original[i], result[i - offset]);
}
return result;
};
export default class PlaylistLoader extends Stream {
constructor(srcUrl, withCredentials) {
super();
let loader = this;
let dispose;
let mediaUpdateTimeout;
let request;
let playlistRequestError;
let haveMetadata;
// a flag that disables "expired time"-tracking this setting has
// no effect when not playing a live stream
this.trackExpiredTime_ = false;
if (!srcUrl) {
throw new Error('A non-empty playlist URL is required');
}
playlistRequestError = function(xhr, url, startingState) {
loader.setBandwidth(request || xhr);
// any in-flight request is now finished
request = null;
if (startingState) {
loader.state = startingState;
}
loader.error = {
playlist: loader.master.playlists[url],
status: xhr.status,
message: 'HLS playlist request error at URL: ' + url,
responseText: xhr.responseText,
code: (xhr.status >= 500) ? 4 : 2
};
loader.trigger('error');
};
// update the playlist loader's state in response to a new or
// updated playlist.
haveMetadata = function(xhr, url) {
let parser;
let refreshDelay;
let update;
loader.setBandwidth(request || xhr);
// any in-flight request is now finished
request = null;
loader.state = 'HAVE_METADATA';
parser = new m3u8.Parser();
parser.push(xhr.responseText);
parser.end();
parser.manifest.uri = url;
// merge this playlist into the master
update = updateMaster(loader.master, parser.manifest);
refreshDelay = (parser.manifest.targetDuration || 10) * 1000;
if (update) {
loader.master = update;
loader.updateMediaPlaylist_(parser.manifest);
} else {
// if the playlist is unchanged since the last reload,
// try again after half the target duration
refreshDelay /= 2;
}
// refresh live playlists after a target duration passes
if (!loader.media().endList) {
window.clearTimeout(mediaUpdateTimeout);
mediaUpdateTimeout = window.setTimeout(function() {
loader.trigger('mediaupdatetimeout');
}, refreshDelay);
}
loader.trigger('loadedplaylist');
};
// initialize the loader state
loader.state = 'HAVE_NOTHING';
// track the time that has expired from the live window
// this allows the seekable start range to be calculated even if
// all segments with timing information have expired
this.expired_ = 0;
// capture the prototype dispose function
dispose = this.dispose;
/**
* Abort any outstanding work and clean up.
*/
loader.dispose = function() {
if (request) {
request.onreadystatechange = null;
request.abort();
request = null;
}
window.clearTimeout(mediaUpdateTimeout);
dispose.call(this);
};
/**
* When called without any arguments, returns the currently
* active media playlist. When called with a single argument,
* triggers the playlist loader to asynchronously switch to the
* specified media playlist. Calling this method while the
* loader is in the HAVE_NOTHING causes an error to be emitted
* but otherwise has no effect.
* @param playlist (optional) {object} the parsed media playlist
* object to switch to
*/
loader.media = function(playlist) {
let startingState = loader.state;
let mediaChange;
// getter
if (!playlist) {
return loader.media_;
}
// setter
if (loader.state === 'HAVE_NOTHING') {
throw new Error('Cannot switch media playlist from ' + loader.state);
}
// find the playlist object if the target playlist has been
// specified by URI
if (typeof playlist === 'string') {
if (!loader.master.playlists[playlist]) {
throw new Error('Unknown playlist URI: ' + playlist);
}
playlist = loader.master.playlists[playlist];
}
mediaChange = !loader.media_ || playlist.uri !== loader.media_.uri;
// switch to fully loaded playlists immediately
if (loader.master.playlists[playlist.uri].endList) {
// abort outstanding playlist requests
if (request) {
request.onreadystatechange = null;
request.abort();
request = null;
}
loader.state = 'HAVE_METADATA';
loader.media_ = playlist;
// trigger media change if the active media has been updated
if (mediaChange) {
loader.trigger('mediachange');
}
return;
}
// switching to the active playlist is a no-op
if (!mediaChange) {
return;
}
loader.state = 'SWITCHING_MEDIA';
// there is already an outstanding playlist request
if (request) {
if (resolveUrl(loader.master.uri, playlist.uri) === request.url) {
// requesting to switch to the same playlist multiple times
// has no effect after the first
return;
}
request.onreadystatechange = null;
request.abort();
request = null;
}
// request the new playlist
request = XhrModule({
uri: resolveUrl(loader.master.uri, playlist.uri),
withCredentials
}, function(error, request) {
if (error) {
return playlistRequestError(request, playlist.uri, startingState);
}
haveMetadata(request, playlist.uri);
// fire loadedmetadata the first time a media playlist is loaded
if (startingState === 'HAVE_MASTER') {
loader.trigger('loadedmetadata');
} else {
loader.trigger('mediachange');
}
});
};
loader.setBandwidth = function(xhr) {
loader.bandwidth = xhr.bandwidth;
};
// In a live list, don't keep track of the expired time until
// HLS tells us that "first play" has commenced
loader.on('firstplay', function() {
this.trackExpiredTime_ = true;
});
// live playlist staleness timeout
loader.on('mediaupdatetimeout', function() {
if (loader.state !== 'HAVE_METADATA') {
// only refresh the media playlist if no other activity is going on
return;
}
loader.state = 'HAVE_CURRENT_METADATA';
request = XhrModule({
uri: resolveUrl(loader.master.uri, loader.media().uri),
withCredentials
}, function(error, request) {
if (error) {
return playlistRequestError(request, loader.media().uri);
}
haveMetadata(request, loader.media().uri);
});
});
// request the specified URL
request = XhrModule({
uri: srcUrl,
withCredentials
}, function(error, req) {
let parser;
let i;
// clear the loader's request reference
request = null;
if (error) {
loader.error = {
status: req.status,
message: 'HLS playlist request error at URL: ' + srcUrl,
responseText: req.responseText,
// MEDIA_ERR_NETWORK
code: 2
};
return loader.trigger('error');
}
parser = new m3u8.Parser();
parser.push(req.responseText);
parser.end();
loader.state = 'HAVE_MASTER';
parser.manifest.uri = srcUrl;
// loaded a master playlist
if (parser.manifest.playlists) {
loader.master = parser.manifest;
// setup by-URI lookups
i = loader.master.playlists.length;
while (i--) {
loader.master.playlists[loader.master.playlists[i].uri] = loader.master.playlists[i];
}
loader.trigger('loadedplaylist');
if (!request) {
// no media playlist was specifically selected so start
// from the first listed one
loader.media(parser.manifest.playlists[0]);
}
return;
}
// loaded a media playlist
// infer a master playlist if none was previously requested
loader.master = {
uri: window.location.href,
playlists: [{
uri: srcUrl
}]
};
loader.master.playlists[srcUrl] = loader.master.playlists[0];
haveMetadata(req, srcUrl);
return loader.trigger('loadedmetadata');
});
}
/**
* Determine the index of the segment that contains a specified
* playback position in the current media playlist. Early versions
* of the HLS specification require segment durations to be rounded
* to the nearest integer which means it may not be possible to
* determine the correct segment for a playback position if that
* position is within .5 seconds of the segment duration. This
* function will always return the lower of the two possible indices
* in those cases.
*
* @param time {number} The number of seconds since the earliest
* possible position to determine the containing segment for
* @returns {number} The number of the media segment that contains
* that time position. If the specified playback position is outside
* the time range of the current set of media segments, the return
* value will be clamped to the index of the segment containing the
* closest playback position that is currently available.
*/
getMediaIndexForTime_(time) {
let i;
* Update the PlaylistLoader state to reflect the changes in an
* update to the current media playlist.
* @param update {object} the updated media playlist object
*/
updateMediaPlaylist_(update) {
let outdated;
let i;
let segment;
outdated = this.media_;
this.media_ = this.master.playlists[update.uri];
if (!outdated) {
return;
}
// don't track expired time until this flag is truthy
if (!this.trackExpiredTime_) {
return;
}
// if the update was the result of a rendition switch do not
// attempt to calculate expired_ since media-sequences need not
// correlate between renditions/variants
if (update.uri !== outdated.uri) {
return;
}
// try using precise timing from first segment of the updated
// playlist
if (update.segments.length) {
if (update.segments[0].start !== undefined) {
this.expired_ = update.segments[0].start;
return;
} else if (update.segments[0].end !== undefined) {
this.expired_ = update.segments[0].end - update.segments[0].duration;
return;
}
}
// calculate expired by walking the outdated playlist
i = update.mediaSequence - outdated.mediaSequence - 1;
for (; i >= 0; i--) {
segment = outdated.segments[i];
if (!segment) {
// we missed information on this segment completely between
// playlist updates so we'll have to take an educated guess
// once we begin buffering again, any error we introduce can
// be corrected
this.expired_ += outdated.targetDuration || 10;
continue;
}
if (segment.end !== undefined) {
this.expired_ = segment.end;
return;
}
if (segment.start !== undefined) {
this.expired_ = segment.start + segment.duration;
return;
}
this.expired_ += segment.duration;
}
}
/**
* Determine the index of the segment that contains a specified
* playback position in the current media playlist. Early versions
* of the HLS specification require segment durations to be rounded
* to the nearest integer which means it may not be possible to
* determine the correct segment for a playback position if that
* position is within .5 seconds of the segment duration. This
* function will always return the lower of the two possible indices
* in those cases.
*
* @param time {number} The number of seconds since the earliest
* possible position to determine the containing segment for
* @returns {number} The number of the media segment that contains
* that time position. If the specified playback position is outside
* the time range of the current set of media segments, the return
* value will be clamped to the index of the segment containing the
* closest playback position that is currently available.
*/
getMediaIndexForTime_(time) {
let i;
let segment;
let originalTime = time;
let numSegments = this.media_.segments.length;
......@@ -471,97 +468,99 @@ export default class PlaylistLoader extends Stream {
let knownStart;
let knownEnd;
if (!this.media_) {
return 0;
}
// when the requested position is earlier than the current set of
// segments, return the earliest segment index
if (time < 0) {
return 0;
}
// find segments with known timing information that bound the
// target time
for (i = 0; i < numSegments; i++) {
segment = this.media_.segments[i];
if (segment.end) {
if (segment.end > time) {
knownEnd = segment.end;
endIndex = i;
break;
} else {
knownStart = segment.end;
startIndex = i + 1;
}
}
}
// use the bounds we just found and playlist information to
// estimate the segment that contains the time we are looking for
if (typeof startIndex !== 'undefined') {
// We have a known-start point that is before our desired time so
// walk from that point forwards
time = time - knownStart;
for (i = startIndex; i < (endIndex || numSegments); i++) {
segment = this.media_.segments[i];
time -= segment.duration;
if (time < 0) {
return i;
}
}
if (i >= endIndex) {
// We haven't found a segment but we did hit a known end point
// so fallback to interpolating between the segment index
// based on the known span of the timeline we are dealing with
// and the number of segments inside that span
return startIndex + Math.floor(((originalTime - knownStart) /
(knownEnd - knownStart)) *
(endIndex - startIndex));
}
// We _still_ haven't found a segment so load the last one
return lastSegment;
} else if (typeof endIndex !== 'undefined') {
// We _only_ have a known-end point that is after our desired time so
// walk from that point backwards
time = knownEnd - time;
for (i = endIndex; i >= 0; i--) {
segment = this.media_.segments[i];
time -= segment.duration;
if (time < 0) {
return i;
}
}
// We haven't found a segment so load the first one if time is zero
if (time === 0) {
return 0;
}
return -1;
}
// We known nothing so walk from the front of the playlist,
// subtracting durations until we find a segment that contains
// time and return it
time = time - this.expired_;
if (time < 0) {
return -1;
}
for (i = 0; i < numSegments; i++) {
segment = this.media_.segments[i];
time -= segment.duration;
if (time < 0) {
return i;
}
}
// We are out of possible candidates so load the last one...
// The last one is the least likely to overlap a buffer and therefore
// the one most likely to tell us something about the timeline
return lastSegment;
}
if (!this.media_) {
return 0;
}
// when the requested position is earlier than the current set of
// segments, return the earliest segment index
if (time < 0) {
return 0;
}
// find segments with known timing information that bound the
// target time
for (i = 0; i < numSegments; i++) {
segment = this.media_.segments[i];
if (segment.end) {
if (segment.end > time) {
knownEnd = segment.end;
endIndex = i;
break;
} else {
knownStart = segment.end;
startIndex = i + 1;
}
}
}
// use the bounds we just found and playlist information to
// estimate the segment that contains the time we are looking for
if (startIndex !== undefined) {
// We have a known-start point that is before our desired time so
// walk from that point forwards
time = time - knownStart;
for (i = startIndex; i < (endIndex || numSegments); i++) {
segment = this.media_.segments[i];
time -= segment.duration;
if (time < 0) {
return i;
}
}
if (i >= endIndex) {
// We haven't found a segment but we did hit a known end point
// so fallback to interpolating between the segment index
// based on the known span of the timeline we are dealing with
// and the number of segments inside that span
return startIndex + Math.floor(
((originalTime - knownStart) / (knownEnd - knownStart)) *
(endIndex - startIndex));
}
// We _still_ haven't found a segment so load the last one
return lastSegment;
} else if (endIndex !== undefined) {
// We _only_ have a known-end point that is after our desired time so
// walk from that point backwards
time = knownEnd - time;
for (i = endIndex; i >= 0; i--) {
segment = this.media_.segments[i];
time -= segment.duration;
if (time < 0) {
return i;
}
}
// We haven't found a segment so load the first one if time is zero
if (time === 0) {
return 0;
} else {
return -1;
}
} else {
// We known nothing so walk from the front of the playlist,
// subtracting durations until we find a segment that contains
// time and return it
time = time - this.expired_;
if (time < 0) {
return -1;
}
for (i = 0; i < numSegments; i++) {
segment = this.media_.segments[i];
time -= segment.duration;
if (time < 0) {
return i;
}
}
// We are out of possible candidates so load the last one...
// The last one is the least likely to overlap a buffer and therefore
// the one most likely to tell us something about the timeline
return lastSegment;
}
}
}
......
......@@ -35,10 +35,8 @@ const xhr = function(options, callback) {
response.statusCode !== 200 &&
response.statusCode !== 206 &&
response.statusCode !== 0) {
error = new Error(
'XHR Failed with a response of: ' +
(request && (request.response || request.responseText))
);
error = new Error('XHR Failed with a response of: ' +
(request && (request.response || request.responseText)));
}
callback(error, request);
......
......@@ -12,10 +12,6 @@ const urlTo = function(path) {
.join('/');
};
const respond = function(request, string) {
return request.respond(200, null, string);
};
QUnit.module('Playlist Loader', {
beforeEach() {
// fake XHRs
......@@ -57,16 +53,16 @@ QUnit.test('starts without any metadata', function() {
QUnit.test('starts with no expired time', function() {
let loader = new PlaylistLoader('media.m3u8');
respond(this.requests.pop(),
'#EXTM3U\n' +
'#EXTINF:10,\n' +
'0.ts\n');
this.requests.pop().respond(200, null,
'#EXTM3U\n' +
'#EXTINF:10,\n' +
'0.ts\n');
QUnit.equal(loader.expired_,
0,
'zero seconds expired');
});
QUnit.test('this.requests the initial playlist immediately', function() {
QUnit.test('requests the initial playlist immediately', function() {
/* eslint-disable no-unused-vars */
let loader = new PlaylistLoader('master.m3u8');
/* eslint-enable no-unused-vars */
......@@ -84,10 +80,10 @@ QUnit.test('moves to HAVE_MASTER after loading a master playlist', function() {
loader.on('loadedplaylist', function() {
state = loader.state;
});
respond(this.requests.pop(),
'#EXTM3U\n' +
'#EXT-X-STREAM-INF:\n' +
'media.m3u8\n');
this.requests.pop().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-STREAM-INF:\n' +
'media.m3u8\n');
QUnit.ok(loader.master, 'the master playlist is available');
QUnit.strictEqual(state, 'HAVE_MASTER', 'the state at loadedplaylist correct');
});
......@@ -99,28 +95,27 @@ QUnit.test('jumps to HAVE_METADATA when initialized with a media playlist', func
loader.on('loadedmetadata', function() {
loadedmetadatas++;
});
respond(this.requests.pop(),
'#EXTM3U\n' +
'#EXTINF:10,\n' +
'0.ts\n' +
'#EXT-X-ENDLIST\n');
this.requests.pop().respond(200, null,
'#EXTM3U\n' +
'#EXTINF:10,\n' +
'0.ts\n' +
'#EXT-X-ENDLIST\n');
QUnit.ok(loader.master, 'infers a master playlist');
QUnit.ok(loader.media(), 'sets the media playlist');
QUnit.ok(loader.media().uri, 'sets the media playlist URI');
QUnit.strictEqual(loader.state, 'HAVE_METADATA', 'the state is correct');
QUnit.strictEqual(this.requests.length, 0, 'no more this.requests are made');
QUnit.strictEqual(this.requests.length, 0, 'no more requests are made');
QUnit.strictEqual(loadedmetadatas, 1, 'fired one loadedmetadata');
});
QUnit.test(
'jumps to HAVE_METADATA when initialized with a live media playlist',
QUnit.test('jumps to HAVE_METADATA when initialized with a live media playlist',
function() {
let loader = new PlaylistLoader('media.m3u8');
respond(this.requests.pop(),
'#EXTM3U\n' +
'#EXTINF:10,\n' +
'0.ts\n');
this.requests.pop().respond(200, null,
'#EXTM3U\n' +
'#EXTINF:10,\n' +
'0.ts\n');
QUnit.ok(loader.master, 'infers a master playlist');
QUnit.ok(loader.media(), 'sets the media playlist');
QUnit.strictEqual(loader.state, 'HAVE_METADATA', 'the state is correct');
......@@ -137,23 +132,23 @@ QUnit.test('moves to HAVE_METADATA after loading a media playlist', function() {
loader.on('loadedmetadata', function() {
loadedMetadata++;
});
respond(this.requests.pop(),
'#EXTM3U\n' +
'#EXT-X-STREAM-INF:\n' +
'media.m3u8\n' +
'alt.m3u8\n');
this.requests.pop().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-STREAM-INF:\n' +
'media.m3u8\n' +
'alt.m3u8\n');
QUnit.strictEqual(loadedPlaylist, 1, 'fired loadedplaylist once');
QUnit.strictEqual(loadedMetadata, 0, 'did not fire loadedmetadata');
QUnit.strictEqual(this.requests.length, 1, 'this.requests the media playlist');
QUnit.strictEqual(this.requests.length, 1, 'requests the media playlist');
QUnit.strictEqual(this.requests[0].method, 'GET', 'GETs the media playlist');
QUnit.strictEqual(this.requests[0].url,
urlTo('media.m3u8'),
'this.requests the first playlist');
'requests the first playlist');
respond(this.requests.pop(),
'#EXTM3U\n' +
'#EXTINF:10,\n' +
'0.ts\n');
this.requests.pop().respond(200, null,
'#EXTM3U\n' +
'#EXTINF:10,\n' +
'0.ts\n');
QUnit.ok(loader.master, 'sets the master playlist');
QUnit.ok(loader.media(), 'sets the media playlist');
QUnit.strictEqual(loadedPlaylist, 2, 'fired loadedplaylist twice');
......@@ -164,10 +159,10 @@ QUnit.test('moves to HAVE_METADATA after loading a media playlist', function() {
QUnit.test('moves to HAVE_CURRENT_METADATA when refreshing the playlist', function() {
let loader = new PlaylistLoader('live.m3u8');
respond(this.requests.pop(),
'#EXTM3U\n' +
'#EXTINF:10,\n' +
'0.ts\n');
this.requests.pop().respond(200, null,
'#EXTM3U\n' +
'#EXTINF:10,\n' +
'0.ts\n');
// 10s, one target duration
this.clock.tick(10 * 1000);
QUnit.strictEqual(loader.state, 'HAVE_CURRENT_METADATA', 'the state is correct');
......@@ -180,48 +175,47 @@ QUnit.test('moves to HAVE_CURRENT_METADATA when refreshing the playlist', functi
QUnit.test('returns to HAVE_METADATA after refreshing the playlist', function() {
let loader = new PlaylistLoader('live.m3u8');
respond(this.requests.pop(),
'#EXTM3U\n' +
'#EXTINF:10,\n' +
'0.ts\n');
this.requests.pop().respond(200, null,
'#EXTM3U\n' +
'#EXTINF:10,\n' +
'0.ts\n');
// 10s, one target duration
this.clock.tick(10 * 1000);
respond(this.requests.pop(),
'#EXTM3U\n' +
'#EXTINF:10,\n' +
'1.ts\n');
this.requests.pop().respond(200, null,
'#EXTM3U\n' +
'#EXTINF:10,\n' +
'1.ts\n');
QUnit.strictEqual(loader.state, 'HAVE_METADATA', 'the state is correct');
});
QUnit.test(
'does not increment expired seconds before firstplay is triggered',
QUnit.test('does not increment expired seconds before firstplay is triggered',
function() {
let loader = new PlaylistLoader('live.m3u8');
respond(this.requests.pop(),
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:0\n' +
'#EXTINF:10,\n' +
'0.ts\n' +
'#EXTINF:10,\n' +
'1.ts\n' +
'#EXTINF:10,\n' +
'2.ts\n' +
'#EXTINF:10,\n' +
'3.ts\n');
this.requests.pop().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:0\n' +
'#EXTINF:10,\n' +
'0.ts\n' +
'#EXTINF:10,\n' +
'1.ts\n' +
'#EXTINF:10,\n' +
'2.ts\n' +
'#EXTINF:10,\n' +
'3.ts\n');
// 10s, one target duration
this.clock.tick(10 * 1000);
respond(this.requests.pop(),
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:1\n' +
'#EXTINF:10,\n' +
'1.ts\n' +
'#EXTINF:10,\n' +
'2.ts\n' +
'#EXTINF:10,\n' +
'3.ts\n' +
'#EXTINF:10,\n' +
'4.ts\n');
this.requests.pop().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:1\n' +
'#EXTINF:10,\n' +
'1.ts\n' +
'#EXTINF:10,\n' +
'2.ts\n' +
'#EXTINF:10,\n' +
'3.ts\n' +
'#EXTINF:10,\n' +
'4.ts\n');
QUnit.equal(loader.expired_, 0, 'expired one segment');
});
......@@ -229,30 +223,30 @@ QUnit.test('increments expired seconds after a segment is removed', function() {
let loader = new PlaylistLoader('live.m3u8');
loader.trigger('firstplay');
respond(this.requests.pop(),
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:0\n' +
'#EXTINF:10,\n' +
'0.ts\n' +
'#EXTINF:10,\n' +
'1.ts\n' +
'#EXTINF:10,\n' +
'2.ts\n' +
'#EXTINF:10,\n' +
'3.ts\n');
this.requests.pop().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:0\n' +
'#EXTINF:10,\n' +
'0.ts\n' +
'#EXTINF:10,\n' +
'1.ts\n' +
'#EXTINF:10,\n' +
'2.ts\n' +
'#EXTINF:10,\n' +
'3.ts\n');
// 10s, one target duration
this.clock.tick(10 * 1000);
respond(this.requests.pop(),
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:1\n' +
'#EXTINF:10,\n' +
'1.ts\n' +
'#EXTINF:10,\n' +
'2.ts\n' +
'#EXTINF:10,\n' +
'3.ts\n' +
'#EXTINF:10,\n' +
'4.ts\n');
this.requests.pop().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:1\n' +
'#EXTINF:10,\n' +
'1.ts\n' +
'#EXTINF:10,\n' +
'2.ts\n' +
'#EXTINF:10,\n' +
'3.ts\n' +
'#EXTINF:10,\n' +
'4.ts\n');
QUnit.equal(loader.expired_, 10, 'expired one segment');
});
......@@ -260,100 +254,98 @@ QUnit.test('increments expired seconds after a discontinuity', function() {
let loader = new PlaylistLoader('live.m3u8');
loader.trigger('firstplay');
respond(this.requests.pop(),
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:0\n' +
'#EXTINF:10,\n' +
'0.ts\n' +
'#EXTINF:3,\n' +
'1.ts\n' +
'#EXT-X-DISCONTINUITY\n' +
'#EXTINF:4,\n' +
'2.ts\n');
this.requests.pop().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:0\n' +
'#EXTINF:10,\n' +
'0.ts\n' +
'#EXTINF:3,\n' +
'1.ts\n' +
'#EXT-X-DISCONTINUITY\n' +
'#EXTINF:4,\n' +
'2.ts\n');
// 10s, one target duration
this.clock.tick(10 * 1000);
respond(this.requests.pop(),
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:1\n' +
'#EXTINF:3,\n' +
'1.ts\n' +
'#EXT-X-DISCONTINUITY\n' +
'#EXTINF:4,\n' +
'2.ts\n');
this.requests.pop().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:1\n' +
'#EXTINF:3,\n' +
'1.ts\n' +
'#EXT-X-DISCONTINUITY\n' +
'#EXTINF:4,\n' +
'2.ts\n');
QUnit.equal(loader.expired_, 10, 'expired one segment');
// 10s, one target duration
this.clock.tick(10 * 1000);
respond(this.requests.pop(),
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:2\n' +
'#EXT-X-DISCONTINUITY\n' +
'#EXTINF:4,\n' +
'2.ts\n');
this.requests.pop().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:2\n' +
'#EXT-X-DISCONTINUITY\n' +
'#EXTINF:4,\n' +
'2.ts\n');
QUnit.equal(loader.expired_, 13, 'no expirations after the discontinuity yet');
// 10s, one target duration
this.clock.tick(10 * 1000);
respond(this.requests.pop(),
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:3\n' +
'#EXT-X-DISCONTINUITY-SEQUENCE:1\n' +
'#EXTINF:10,\n' +
'3.ts\n');
this.requests.pop().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:3\n' +
'#EXT-X-DISCONTINUITY-SEQUENCE:1\n' +
'#EXTINF:10,\n' +
'3.ts\n');
QUnit.equal(loader.expired_, 17, 'tracked expiration across the discontinuity');
});
QUnit.test(
'tracks expired seconds properly when two discontinuities expire at once',
QUnit.test('tracks expired seconds properly when two discontinuities expire at once',
function() {
let loader = new PlaylistLoader('live.m3u8');
loader.trigger('firstplay');
respond(this.requests.pop(),
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:0\n' +
'#EXTINF:4,\n' +
'0.ts\n' +
'#EXT-X-DISCONTINUITY\n' +
'#EXTINF:5,\n' +
'1.ts\n' +
'#EXT-X-DISCONTINUITY\n' +
'#EXTINF:6,\n' +
'2.ts\n' +
'#EXTINF:7,\n' +
'3.ts\n');
this.requests.pop().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:0\n' +
'#EXTINF:4,\n' +
'0.ts\n' +
'#EXT-X-DISCONTINUITY\n' +
'#EXTINF:5,\n' +
'1.ts\n' +
'#EXT-X-DISCONTINUITY\n' +
'#EXTINF:6,\n' +
'2.ts\n' +
'#EXTINF:7,\n' +
'3.ts\n');
this.clock.tick(10 * 1000);
respond(this.requests.pop(),
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:3\n' +
'#EXT-X-DISCONTINUITY-SEQUENCE:2\n' +
'#EXTINF:7,\n' +
'3.ts\n');
this.requests.pop().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:3\n' +
'#EXT-X-DISCONTINUITY-SEQUENCE:2\n' +
'#EXTINF:7,\n' +
'3.ts\n');
QUnit.equal(loader.expired_, 4 + 5 + 6, 'tracked multiple expiring discontinuities');
});
QUnit.test(
'estimates expired if an entire window elapses between live playlist updates',
QUnit.test('estimates expired if an entire window elapses between live playlist updates',
function() {
let loader = new PlaylistLoader('live.m3u8');
loader.trigger('firstplay');
respond(this.requests.pop(),
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:0\n' +
'#EXTINF:4,\n' +
'0.ts\n' +
'#EXTINF:5,\n' +
'1.ts\n');
this.requests.pop().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:0\n' +
'#EXTINF:4,\n' +
'0.ts\n' +
'#EXTINF:5,\n' +
'1.ts\n');
this.clock.tick(10 * 1000);
respond(this.requests.pop(),
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:4\n' +
'#EXTINF:6,\n' +
'4.ts\n' +
'#EXTINF:7,\n' +
'5.ts\n');
this.requests.pop().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:4\n' +
'#EXTINF:6,\n' +
'4.ts\n' +
'#EXTINF:7,\n' +
'5.ts\n');
QUnit.equal(loader.expired_,
4 + 5 + (2 * 10),
......@@ -380,10 +372,10 @@ QUnit.test('errors when an initial media playlist request fails', function() {
loader.on('error', function() {
errors.push(loader.error);
});
respond(this.requests.pop(),
'#EXTM3U\n' +
'#EXT-X-STREAM-INF:\n' +
'media.m3u8\n');
this.requests.pop().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-STREAM-INF:\n' +
'media.m3u8\n');
QUnit.strictEqual(errors.length, 0, 'emitted no errors');
......@@ -394,25 +386,24 @@ QUnit.test('errors when an initial media playlist request fails', function() {
});
// http://tools.ietf.org/html/draft-pantos-http-live-streaming-12#section-6.3.4
QUnit.test(
'halves the refresh timeout if a playlist is unchanged since the last reload',
QUnit.test('halves the refresh timeout if a playlist is unchanged since the last reload',
function() {
/* eslint-disable no-unused-vars */
let loader = new PlaylistLoader('live.m3u8');
/* eslint-enable no-unused-vars */
respond(this.requests.pop(),
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:0\n' +
'#EXTINF:10,\n' +
'0.ts\n');
this.requests.pop().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:0\n' +
'#EXTINF:10,\n' +
'0.ts\n');
// trigger a refresh
this.clock.tick(10 * 1000);
respond(this.requests.pop(),
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:0\n' +
'#EXTINF:10,\n' +
'0.ts\n');
this.requests.pop().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:0\n' +
'#EXTINF:10,\n' +
'0.ts\n');
// half the default target-duration
this.clock.tick(5 * 1000);
......@@ -426,15 +417,15 @@ QUnit.test('preserves segment metadata across playlist refreshes', function() {
let loader = new PlaylistLoader('live.m3u8');
let segment;
respond(this.requests.pop(),
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:0\n' +
'#EXTINF:10,\n' +
'0.ts\n' +
'#EXTINF:10,\n' +
'1.ts\n' +
'#EXTINF:10,\n' +
'2.ts\n');
this.requests.pop().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:0\n' +
'#EXTINF:10,\n' +
'0.ts\n' +
'#EXTINF:10,\n' +
'1.ts\n' +
'#EXTINF:10,\n' +
'2.ts\n');
// add PTS info to 1.ts
segment = loader.media().segments[1];
segment.minVideoPts = 14;
......@@ -443,13 +434,13 @@ QUnit.test('preserves segment metadata across playlist refreshes', function() {
// trigger a refresh
this.clock.tick(10 * 1000);
respond(this.requests.pop(),
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:1\n' +
'#EXTINF:10,\n' +
'1.ts\n' +
'#EXTINF:10,\n' +
'2.ts\n');
this.requests.pop().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:1\n' +
'#EXTINF:10,\n' +
'1.ts\n' +
'#EXTINF:10,\n' +
'2.ts\n');
QUnit.deepEqual(loader.media().segments[0], segment, 'preserved segment attributes');
});
......@@ -463,25 +454,25 @@ QUnit.test('clears the update timeout when switching quality', function() {
refreshes++;
});
// deliver the master
respond(this.requests.pop(),
'#EXTM3U\n' +
'#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
'live-low.m3u8\n' +
'#EXT-X-STREAM-INF:BANDWIDTH=2\n' +
'live-high.m3u8\n');
this.requests.pop().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
'live-low.m3u8\n' +
'#EXT-X-STREAM-INF:BANDWIDTH=2\n' +
'live-high.m3u8\n');
// deliver the low quality playlist
respond(this.requests.pop(),
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:0\n' +
'#EXTINF:10,\n' +
'low-0.ts\n');
this.requests.pop().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:0\n' +
'#EXTINF:10,\n' +
'low-0.ts\n');
// change to a higher quality playlist
loader.media('live-high.m3u8');
respond(this.requests.pop(),
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:0\n' +
'#EXTINF:10,\n' +
'high-0.ts\n');
this.requests.pop().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:0\n' +
'#EXTINF:10,\n' +
'high-0.ts\n');
// trigger a refresh
this.clock.tick(10 * 1000);
......@@ -493,18 +484,18 @@ QUnit.test('media-sequence updates are considered a playlist change', function()
let loader = new PlaylistLoader('live.m3u8');
/* eslint-enable no-unused-vars */
respond(this.requests.pop(),
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:0\n' +
'#EXTINF:10,\n' +
'0.ts\n');
this.requests.pop().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:0\n' +
'#EXTINF:10,\n' +
'0.ts\n');
// trigger a refresh
this.clock.tick(10 * 1000);
respond(this.requests.pop(),
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:1\n' +
'#EXTINF:10,\n' +
'0.ts\n');
this.requests.pop().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:1\n' +
'#EXTINF:10,\n' +
'0.ts\n');
// half the default target-duration
this.clock.tick(5 * 1000);
......@@ -519,11 +510,11 @@ QUnit.test('emits an error if a media refresh fails', function() {
loader.on('error', function() {
errors++;
});
respond(this.requests.pop(),
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:0\n' +
'#EXTINF:10,\n' +
'0.ts\n');
this.requests.pop().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:0\n' +
'#EXTINF:10,\n' +
'0.ts\n');
// trigger a refresh
this.clock.tick(10 * 1000);
this.requests.pop().respond(500, null, errorResponseText);
......@@ -538,26 +529,26 @@ QUnit.test('emits an error if a media refresh fails', function() {
QUnit.test('switches media playlists when requested', function() {
let loader = new PlaylistLoader('master.m3u8');
respond(this.requests.pop(),
'#EXTM3U\n' +
'#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
'low.m3u8\n' +
'#EXT-X-STREAM-INF:BANDWIDTH=2\n' +
'high.m3u8\n');
respond(this.requests.pop(),
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:0\n' +
'#EXTINF:10,\n' +
'low-0.ts\n');
this.requests.pop().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
'low.m3u8\n' +
'#EXT-X-STREAM-INF:BANDWIDTH=2\n' +
'high.m3u8\n');
this.requests.pop().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:0\n' +
'#EXTINF:10,\n' +
'low-0.ts\n');
loader.media(loader.master.playlists[1]);
QUnit.strictEqual(loader.state, 'SWITCHING_MEDIA', 'updated the state');
respond(this.requests.pop(),
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:0\n' +
'#EXTINF:10,\n' +
'high-0.ts\n');
this.requests.pop().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:0\n' +
'#EXTINF:10,\n' +
'high-0.ts\n');
QUnit.strictEqual(loader.state, 'HAVE_METADATA', 'switched active media');
QUnit.strictEqual(loader.media(),
loader.master.playlists[1],
......@@ -570,38 +561,38 @@ QUnit.test('can switch playlists immediately after the master is downloaded', fu
loader.on('loadedplaylist', function() {
loader.media('high.m3u8');
});
respond(this.requests.pop(),
'#EXTM3U\n' +
'#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
'low.m3u8\n' +
'#EXT-X-STREAM-INF:BANDWIDTH=2\n' +
'high.m3u8\n');
this.requests.pop().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
'low.m3u8\n' +
'#EXT-X-STREAM-INF:BANDWIDTH=2\n' +
'high.m3u8\n');
QUnit.equal(this.requests[0].url, urlTo('high.m3u8'), 'switched variants immediately');
});
QUnit.test('can switch media playlists based on URI', function() {
let loader = new PlaylistLoader('master.m3u8');
respond(this.requests.pop(),
'#EXTM3U\n' +
'#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
'low.m3u8\n' +
'#EXT-X-STREAM-INF:BANDWIDTH=2\n' +
'high.m3u8\n');
respond(this.requests.pop(),
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:0\n' +
'#EXTINF:10,\n' +
'low-0.ts\n');
this.requests.pop().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
'low.m3u8\n' +
'#EXT-X-STREAM-INF:BANDWIDTH=2\n' +
'high.m3u8\n');
this.requests.pop().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:0\n' +
'#EXTINF:10,\n' +
'low-0.ts\n');
loader.media('high.m3u8');
QUnit.strictEqual(loader.state, 'SWITCHING_MEDIA', 'updated the state');
respond(this.requests.pop(),
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:0\n' +
'#EXTINF:10,\n' +
'high-0.ts\n');
this.requests.pop().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:0\n' +
'#EXTINF:10,\n' +
'high-0.ts\n');
QUnit.strictEqual(loader.state, 'HAVE_METADATA', 'switched active media');
QUnit.strictEqual(loader.media(),
loader.master.playlists[1],
......@@ -611,17 +602,17 @@ QUnit.test('can switch media playlists based on URI', function() {
QUnit.test('aborts in-flight playlist refreshes when switching', function() {
let loader = new PlaylistLoader('master.m3u8');
respond(this.requests.pop(),
'#EXTM3U\n' +
'#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
'low.m3u8\n' +
'#EXT-X-STREAM-INF:BANDWIDTH=2\n' +
'high.m3u8\n');
respond(this.requests.pop(),
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:0\n' +
'#EXTINF:10,\n' +
'low-0.ts\n');
this.requests.pop().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
'low.m3u8\n' +
'#EXT-X-STREAM-INF:BANDWIDTH=2\n' +
'high.m3u8\n');
this.requests.pop().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:0\n' +
'#EXTINF:10,\n' +
'low-0.ts\n');
this.clock.tick(10 * 1000);
loader.media('high.m3u8');
QUnit.strictEqual(this.requests[0].aborted, true, 'aborted refresh request');
......@@ -633,87 +624,86 @@ QUnit.test('aborts in-flight playlist refreshes when switching', function() {
QUnit.test('switching to the active playlist is a no-op', function() {
let loader = new PlaylistLoader('master.m3u8');
respond(this.requests.pop(),
'#EXTM3U\n' +
'#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
'low.m3u8\n' +
'#EXT-X-STREAM-INF:BANDWIDTH=2\n' +
'high.m3u8\n');
respond(this.requests.pop(),
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:0\n' +
'#EXTINF:10,\n' +
'low-0.ts\n' +
'#EXT-X-ENDLIST\n');
this.requests.pop().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
'low.m3u8\n' +
'#EXT-X-STREAM-INF:BANDWIDTH=2\n' +
'high.m3u8\n');
this.requests.pop().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:0\n' +
'#EXTINF:10,\n' +
'low-0.ts\n' +
'#EXT-X-ENDLIST\n');
loader.media('low.m3u8');
QUnit.strictEqual(this.requests.length, 0, 'no this.requests are sent');
QUnit.strictEqual(this.requests.length, 0, 'no requests are sent');
});
QUnit.test('switching to the active live playlist is a no-op', function() {
let loader = new PlaylistLoader('master.m3u8');
respond(this.requests.pop(),
'#EXTM3U\n' +
'#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
'low.m3u8\n' +
'#EXT-X-STREAM-INF:BANDWIDTH=2\n' +
'high.m3u8\n');
respond(this.requests.pop(),
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:0\n' +
'#EXTINF:10,\n' +
'low-0.ts\n');
this.requests.pop().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
'low.m3u8\n' +
'#EXT-X-STREAM-INF:BANDWIDTH=2\n' +
'high.m3u8\n');
this.requests.pop().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:0\n' +
'#EXTINF:10,\n' +
'low-0.ts\n');
loader.media('low.m3u8');
QUnit.strictEqual(this.requests.length, 0, 'no this.requests are sent');
QUnit.strictEqual(this.requests.length, 0, 'no requests are sent');
});
QUnit.test('switches back to loaded playlists without re-requesting them', function() {
let loader = new PlaylistLoader('master.m3u8');
respond(this.requests.pop(),
'#EXTM3U\n' +
'#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
'low.m3u8\n' +
'#EXT-X-STREAM-INF:BANDWIDTH=2\n' +
'high.m3u8\n');
respond(this.requests.pop(),
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:0\n' +
'#EXTINF:10,\n' +
'low-0.ts\n' +
'#EXT-X-ENDLIST\n');
this.requests.pop().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
'low.m3u8\n' +
'#EXT-X-STREAM-INF:BANDWIDTH=2\n' +
'high.m3u8\n');
this.requests.pop().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:0\n' +
'#EXTINF:10,\n' +
'low-0.ts\n' +
'#EXT-X-ENDLIST\n');
loader.media('high.m3u8');
respond(this.requests.pop(),
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:0\n' +
'#EXTINF:10,\n' +
'high-0.ts\n' +
'#EXT-X-ENDLIST\n');
this.requests.pop().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:0\n' +
'#EXTINF:10,\n' +
'high-0.ts\n' +
'#EXT-X-ENDLIST\n');
loader.media('low.m3u8');
QUnit.strictEqual(this.requests.length, 0, 'no outstanding this.requests');
QUnit.strictEqual(this.requests.length, 0, 'no outstanding requests');
QUnit.strictEqual(loader.state, 'HAVE_METADATA', 'returned to loaded playlist');
});
QUnit.test(
'aborts outstanding this.requests if switching back to an already loaded playlist',
QUnit.test('aborts outstanding requests if switching back to an already loaded playlist',
function() {
let loader = new PlaylistLoader('master.m3u8');
respond(this.requests.pop(),
'#EXTM3U\n' +
'#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
'low.m3u8\n' +
'#EXT-X-STREAM-INF:BANDWIDTH=2\n' +
'high.m3u8\n');
respond(this.requests.pop(),
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:0\n' +
'#EXTINF:10,\n' +
'low-0.ts\n' +
'#EXT-X-ENDLIST\n');
this.requests.pop().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
'low.m3u8\n' +
'#EXT-X-STREAM-INF:BANDWIDTH=2\n' +
'high.m3u8\n');
this.requests.pop().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:0\n' +
'#EXTINF:10,\n' +
'low-0.ts\n' +
'#EXT-X-ENDLIST\n');
loader.media('high.m3u8');
loader.media('low.m3u8');
......@@ -732,23 +722,22 @@ function() {
'switched to loaded playlist');
});
QUnit.test(
'does not abort this.requests when the same playlist is re-requested',
QUnit.test('does not abort requests when the same playlist is re-requested',
function() {
let loader = new PlaylistLoader('master.m3u8');
respond(this.requests.pop(),
'#EXTM3U\n' +
'#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
'low.m3u8\n' +
'#EXT-X-STREAM-INF:BANDWIDTH=2\n' +
'high.m3u8\n');
respond(this.requests.pop(),
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:0\n' +
'#EXTINF:10,\n' +
'low-0.ts\n' +
'#EXT-X-ENDLIST\n');
this.requests.pop().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
'low.m3u8\n' +
'#EXT-X-STREAM-INF:BANDWIDTH=2\n' +
'high.m3u8\n');
this.requests.pop().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:0\n' +
'#EXTINF:10,\n' +
'low-0.ts\n' +
'#EXT-X-ENDLIST\n');
loader.media('high.m3u8');
loader.media('high.m3u8');
......@@ -763,23 +752,22 @@ QUnit.test('throws an error if a media switch is initiated too early', function(
loader.media('high.m3u8');
}, 'threw an error from HAVE_NOTHING');
respond(this.requests.pop(),
'#EXTM3U\n' +
'#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
'low.m3u8\n' +
'#EXT-X-STREAM-INF:BANDWIDTH=2\n' +
'high.m3u8\n');
this.requests.pop().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
'low.m3u8\n' +
'#EXT-X-STREAM-INF:BANDWIDTH=2\n' +
'high.m3u8\n');
});
QUnit.test(
'throws an error if a switch to an unrecognized playlist is requested',
QUnit.test('throws an error if a switch to an unrecognized playlist is requested',
function() {
let loader = new PlaylistLoader('master.m3u8');
respond(this.requests.pop(),
'#EXTM3U\n' +
'#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
'media.m3u8\n');
this.requests.pop().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
'media.m3u8\n');
QUnit.throws(function() {
loader.media('unrecognized.m3u8');
......@@ -789,11 +777,11 @@ function() {
QUnit.test('dispose cancels the refresh timeout', function() {
let loader = new PlaylistLoader('live.m3u8');
respond(this.requests.pop(),
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:0\n' +
'#EXTINF:10,\n' +
'0.ts\n');
this.requests.pop().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:0\n' +
'#EXTINF:10,\n' +
'0.ts\n');
loader.dispose();
// a lot of time passes...
this.clock.tick(15 * 1000);
......@@ -801,25 +789,24 @@ QUnit.test('dispose cancels the refresh timeout', function() {
QUnit.strictEqual(this.requests.length, 0, 'no refresh request was made');
});
QUnit.test('dispose aborts pending refresh this.requests', function() {
QUnit.test('dispose aborts pending refresh requests', function() {
let loader = new PlaylistLoader('live.m3u8');
respond(this.requests.pop(),
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:0\n' +
'#EXTINF:10,\n' +
'0.ts\n');
this.requests.pop().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:0\n' +
'#EXTINF:10,\n' +
'0.ts\n');
this.clock.tick(10 * 1000);
loader.dispose();
QUnit.ok(this.requests[0].aborted, 'refresh request aborted');
QUnit.ok(
!this.requests[0].onreadystatechange,
'onreadystatechange handler should not exist after dispose called'
QUnit.ok(!this.requests[0].onreadystatechange,
'onreadystatechange handler should not exist after dispose called'
);
});
QUnit.test('errors if this.requests take longer than 45s', function() {
QUnit.test('errors if requests take longer than 45s', function() {
let loader = new PlaylistLoader('media.m3u8');
let errors = 0;
......@@ -839,29 +826,29 @@ QUnit.test('triggers an event when the active media changes', function() {
loader.on('mediachange', function() {
mediaChanges++;
});
respond(this.requests.pop(),
'#EXTM3U\n' +
'#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
'low.m3u8\n' +
'#EXT-X-STREAM-INF:BANDWIDTH=2\n' +
'high.m3u8\n');
respond(this.requests.shift(),
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:0\n' +
'#EXTINF:10,\n' +
'low-0.ts\n' +
'#EXT-X-ENDLIST\n');
this.requests.pop().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
'low.m3u8\n' +
'#EXT-X-STREAM-INF:BANDWIDTH=2\n' +
'high.m3u8\n');
this.requests.shift().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:0\n' +
'#EXTINF:10,\n' +
'low-0.ts\n' +
'#EXT-X-ENDLIST\n');
QUnit.strictEqual(mediaChanges, 0, 'initial selection is not a media change');
loader.media('high.m3u8');
QUnit.strictEqual(mediaChanges, 0, 'mediachange does not fire immediately');
respond(this.requests.shift(),
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:0\n' +
'#EXTINF:10,\n' +
'high-0.ts\n' +
'#EXT-X-ENDLIST\n');
this.requests.shift().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:0\n' +
'#EXTINF:10,\n' +
'high-0.ts\n' +
'#EXT-X-ENDLIST\n');
QUnit.strictEqual(mediaChanges, 1, 'fired a mediachange');
// switch back to an already loaded playlist
......@@ -876,16 +863,16 @@ QUnit.test('triggers an event when the active media changes', function() {
QUnit.test('can get media index by playback position for non-live videos', function() {
let loader = new PlaylistLoader('media.m3u8');
respond(this.requests.shift(),
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:0\n' +
'#EXTINF:4,\n' +
'0.ts\n' +
'#EXTINF:5,\n' +
'1.ts\n' +
'#EXTINF:6,\n' +
'2.ts\n' +
'#EXT-X-ENDLIST\n');
this.requests.shift().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:0\n' +
'#EXTINF:4,\n' +
'0.ts\n' +
'#EXTINF:5,\n' +
'1.ts\n' +
'#EXTINF:6,\n' +
'2.ts\n' +
'#EXT-X-ENDLIST\n');
QUnit.equal(loader.getMediaIndexForTime_(-1),
0,
......@@ -901,31 +888,30 @@ QUnit.test('can get media index by playback position for non-live videos', funct
QUnit.test('returns the lower index when calculating for a segment boundary', function() {
let loader = new PlaylistLoader('media.m3u8');
respond(this.requests.shift(),
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:0\n' +
'#EXTINF:4,\n' +
'0.ts\n' +
'#EXTINF:5,\n' +
'1.ts\n' +
'#EXT-X-ENDLIST\n');
this.requests.shift().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:0\n' +
'#EXTINF:4,\n' +
'0.ts\n' +
'#EXTINF:5,\n' +
'1.ts\n' +
'#EXT-X-ENDLIST\n');
QUnit.equal(loader.getMediaIndexForTime_(4), 1, 'rounds up exact matches');
QUnit.equal(loader.getMediaIndexForTime_(3.7), 0, 'rounds down');
QUnit.equal(loader.getMediaIndexForTime_(4.5), 1, 'rounds up at 0.5');
});
QUnit.test(
'accounts for non-zero starting segment time when calculating media index',
QUnit.test('accounts for non-zero starting segment time when calculating media index',
function() {
let loader = new PlaylistLoader('media.m3u8');
respond(this.requests.shift(),
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:1001\n' +
'#EXTINF:4,\n' +
'1001.ts\n' +
'#EXTINF:5,\n' +
'1002.ts\n');
this.requests.shift().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:1001\n' +
'#EXTINF:4,\n' +
'1001.ts\n' +
'#EXTINF:5,\n' +
'1002.ts\n');
loader.media().segments[0].end = 154;
QUnit.equal(loader.getMediaIndexForTime_(0),
......@@ -943,9 +929,6 @@ function() {
QUnit.equal(loader.getMediaIndexForTime_(50 + 100 + 2),
0,
'calculates within the first segment');
QUnit.equal(loader.getMediaIndexForTime_(50 + 100 + 2),
0,
'calculates within the first segment');
QUnit.equal(loader.getMediaIndexForTime_(50 + 100 + 4),
1,
'calculates within the second segment');
......@@ -961,13 +944,13 @@ QUnit.test('prefers precise segment timing when tracking expired time', function
let loader = new PlaylistLoader('media.m3u8');
loader.trigger('firstplay');
respond(this.requests.shift(),
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:1001\n' +
'#EXTINF:4,\n' +
'1001.ts\n' +
'#EXTINF:5,\n' +
'1002.ts\n');
this.requests.shift().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:1001\n' +
'#EXTINF:4,\n' +
'1001.ts\n' +
'#EXTINF:5,\n' +
'1002.ts\n');
// setup the loader with an "imprecise" value as if it had been
// accumulating segment durations as they expire
loader.expired_ = 160;
......@@ -981,11 +964,11 @@ QUnit.test('prefers precise segment timing when tracking expired time', function
// trigger a playlist refresh
this.clock.tick(10 * 1000);
respond(this.requests.shift(),
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:1002\n' +
'#EXTINF:5,\n' +
'1002.ts\n');
this.requests.shift().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:1002\n' +
'#EXTINF:5,\n' +
'1002.ts\n');
QUnit.equal(loader.getMediaIndexForTime_(150 + 4 + 1),
0,
'tracks precise expired times');
......@@ -994,13 +977,13 @@ QUnit.test('prefers precise segment timing when tracking expired time', function
QUnit.test('accounts for expired time when calculating media index', function() {
let loader = new PlaylistLoader('media.m3u8');
respond(this.requests.shift(),
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:1001\n' +
'#EXTINF:4,\n' +
'1001.ts\n' +
'#EXTINF:5,\n' +
'1002.ts\n');
this.requests.shift().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:1001\n' +
'#EXTINF:4,\n' +
'1001.ts\n' +
'#EXTINF:5,\n' +
'1002.ts\n');
loader.expired_ = 150;
QUnit.equal(loader.getMediaIndexForTime_(0),
......@@ -1015,9 +998,6 @@ QUnit.test('accounts for expired time when calculating media index', function()
QUnit.equal(loader.getMediaIndexForTime_(50 + 100 + 2),
0,
'calculates within the first segment');
QUnit.equal(loader.getMediaIndexForTime_(50 + 100 + 2),
0,
'calculates within the first segment');
QUnit.equal(loader.getMediaIndexForTime_(50 + 100 + 4.5),
1,
'calculates within the second segment');
......@@ -1030,11 +1010,11 @@ QUnit.test('does not misintrepret playlists missing newlines at the end', functi
let loader = new PlaylistLoader('media.m3u8');
// no newline
respond(this.requests.shift(),
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:0\n' +
'#EXTINF:10,\n' +
'low-0.ts\n' +
'#EXT-X-ENDLIST');
this.requests.shift().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:0\n' +
'#EXTINF:10,\n' +
'low-0.ts\n' +
'#EXT-X-ENDLIST');
QUnit.ok(loader.media().endList, 'flushed the final line of input');
});
......