b858aa92 by Steve Heffernan

Merge pull request #117 from heff/reorg-w-114

Reorg and endOfStream fix
2 parents 74ac9838 21c692d7
......@@ -16,6 +16,7 @@
<script src="src/videojs-hls.js"></script>
<!-- segment handling -->
<script src="src/xhr.js"></script>
<script src="src/flv-tag.js"></script>
<script src="src/exp-golomb.js"></script>
<script src="src/h264-stream.js"></script>
......
/*
* video-js-hls
* videojs-hls
*
*
* Copyright (c) 2013 Brightcove
* Copyright (c) 2014 Brightcove
* All rights reserved.
*/
......@@ -10,273 +9,222 @@
'use strict';
var
// a fudge factor to apply to advertised playlist bitrates to account for
// temporary flucations in client bandwidth
bandwidthVariance = 1.1,
resolveUrl;
/**
* A comparator function to sort two playlist object by bandwidth.
* @param left {object} a media playlist object
* @param right {object} a media playlist object
* @return {number} Greater than zero if the bandwidth attribute of
* left is greater than the corresponding attribute of right. Less
* than zero if the bandwidth of right is greater than left and
* exactly zero if the two are equal.
*/
playlistBandwidth = function(left, right) {
var leftBandwidth, rightBandwidth;
if (left.attributes && left.attributes.BANDWIDTH) {
leftBandwidth = left.attributes.BANDWIDTH;
}
leftBandwidth = leftBandwidth || window.Number.MAX_VALUE;
if (right.attributes && right.attributes.BANDWIDTH) {
rightBandwidth = right.attributes.BANDWIDTH;
}
rightBandwidth = rightBandwidth || window.Number.MAX_VALUE;
return leftBandwidth - rightBandwidth;
},
videojs.Hls = videojs.Flash.extend({
init: function(player, options, ready) {
var
source = options.source,
settings = player.options();
/**
* A comparator function to sort two playlist object by resolution (width).
* @param left {object} a media playlist object
* @param right {object} a media playlist object
* @return {number} Greater than zero if the resolution.width attribute of
* left is greater than the corresponding attribute of right. Less
* than zero if the resolution.width of right is greater than left and
* exactly zero if the two are equal.
*/
playlistResolution = function(left, right) {
var leftWidth, rightWidth;
player.hls = this;
delete options.source;
options.swf = settings.flash.swf;
videojs.Flash.call(this, player, options, ready);
options.source = source;
this.bytesReceived = 0;
if (left.attributes && left.attributes.RESOLUTION && left.attributes.RESOLUTION.width) {
leftWidth = left.attributes.RESOLUTION.width;
// TODO: After video.js#1347 is pulled in move these to the prototype
this.currentTime = function() {
if (this.lastSeekedTime_) {
return this.lastSeekedTime_;
}
// currentTime is zero while the tech is initializing
if (!this.el() || !this.el().vjs_getProperty) {
return 0;
}
return this.el().vjs_getProperty('currentTime');
};
this.setCurrentTime = function(currentTime) {
if (!(this.playlists && this.playlists.media())) {
// return immediately if the metadata is not ready yet
return 0;
}
leftWidth = leftWidth || window.Number.MAX_VALUE;
// save the seek target so currentTime can report it correctly
// while the seek is pending
this.lastSeekedTime_ = currentTime;
if (right.attributes && right.attributes.RESOLUTION && right.attributes.RESOLUTION.width) {
rightWidth = right.attributes.RESOLUTION.width;
}
// determine the requested segment
this.mediaIndex = videojs.Hls.getMediaIndexByTime(this.playlists.media(), currentTime);
rightWidth = rightWidth || window.Number.MAX_VALUE;
// abort any segments still being decoded
this.sourceBuffer.abort();
// NOTE - Fallback to bandwidth sort as appropriate in cases where multiple renditions
// have the same media dimensions/ resolution
if (leftWidth === rightWidth && left.attributes.BANDWIDTH && right.attributes.BANDWIDTH) {
return left.attributes.BANDWIDTH - right.attributes.BANDWIDTH;
} else {
return leftWidth - rightWidth;
// cancel outstanding requests and buffer appends
if (this.segmentXhr_) {
this.segmentXhr_.abort();
}
},
xhr,
/**
* TODO - Document this great feature.
*
* @param playlist
* @param time
* @returns int
*/
getMediaIndexByTime = function(playlist, time) {
var index, counter, timeRanges, currentSegmentRange;
// clear out any buffered segments
this.segmentBuffer_ = [];
timeRanges = [];
for (index = 0; index < playlist.segments.length; index++) {
currentSegmentRange = {};
currentSegmentRange.start = (index === 0) ? 0 : timeRanges[index - 1].end;
currentSegmentRange.end = currentSegmentRange.start + playlist.segments[index].duration;
timeRanges.push(currentSegmentRange);
}
// begin filling the buffer at the new position
this.fillBuffer(currentTime * 1000);
};
for (counter = 0; counter < timeRanges.length; counter++) {
if (time >= timeRanges[counter].start && time < timeRanges[counter].end) {
return counter;
}
videojs.Hls.prototype.src.call(this, options.source && options.source.src);
}
});
return -1;
// Add HLS to the standard tech order
videojs.options.techOrder.unshift('hls');
},
// the desired length of video to maintain in the buffer, in seconds
videojs.Hls.GOAL_BUFFER_LENGTH = 30;
/**
* 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) {
videojs.Hls.prototype.src = function(src) {
var
i,
originalSegment;
// no segments have been loaded from the original playlist
if (mediaIndex === 0) {
return 0;
}
if (!(update && update.segments)) {
// let the media index be zero when there are no segments defined
return 0;
}
self = this,
mediaSource,
source;
// try to sync based on URI
i = update.segments.length;
originalSegment = original.segments[mediaIndex - 1];
while (i--) {
if (originalSegment.uri === update.segments[i].uri) {
return i + 1;
}
}
if (src) {
this.src_ = src;
// sync on media sequence
return (original.mediaSequence + mediaIndex) - update.mediaSequence;
},
mediaSource = new videojs.MediaSource();
source = {
src: videojs.URL.createObjectURL(mediaSource),
type: "video/flv"
};
this.mediaSource = mediaSource;
/**
* Calculate the duration of a playlist from a given start index to a given
* end index.
* @param playlist {object} a media playlist object
* @param startIndex {number} an inclusive lower boundary for the playlist.
* Defaults to 0.
* @param endIndex {number} an exclusive upper boundary for the playlist.
* Defaults to playlist length.
* @return {number} the duration between the start index and end index.
*/
duration = function(playlist, startIndex, endIndex) {
var dur = 0,
segment,
i;
this.segmentBuffer_ = [];
this.segmentParser_ = new videojs.Hls.SegmentParser();
startIndex = startIndex || 0;
endIndex = endIndex !== undefined ? endIndex : (playlist.segments || []).length;
i = endIndex - 1;
// load the MediaSource into the player
this.mediaSource.addEventListener('sourceopen', videojs.bind(this, this.handleSourceOpen));
for (; i >= startIndex; i--) {
segment = playlist.segments[i];
dur += segment.duration || playlist.targetDuration || 0;
this.player().ready(function() {
// do nothing if the tech has been disposed already
// this can occur if someone sets the src in player.ready(), for instance
if (!self.el()) {
return;
}
return dur;
},
/**
* 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
*/
totalDuration = function(playlist) {
if (!playlist) {
return 0;
self.el().vjs_src(source.src);
});
}
};
// if present, use the duration specified in the playlist
if (playlist.totalDuration) {
return playlist.totalDuration;
}
videojs.Hls.prototype.handleSourceOpen = function() {
// construct the video data buffer and set the appropriate MIME type
var
player = this.player(),
settings = player.options().hls || {},
sourceBuffer = this.mediaSource.addSourceBuffer('video/flv; codecs="vp6,aac"'),
oldMediaPlaylist;
// duration should be Infinity for live playlists
if (!playlist.endList) {
return window.Infinity;
}
this.sourceBuffer = sourceBuffer;
sourceBuffer.appendBuffer(this.segmentParser_.getFlvHeader());
return duration(playlist);
},
this.mediaIndex = 0;
this.playlists = new videojs.Hls.PlaylistLoader(this.src_, settings.withCredentials);
resolveUrl,
this.playlists.on('loadedmetadata', videojs.bind(this, function() {
oldMediaPlaylist = this.playlists.media();
initSource = function(player, mediaSource, srcUrl) {
var
segmentParser = new videojs.Hls.SegmentParser(),
settings = videojs.util.mergeOptions({}, player.options().hls),
segmentBuffer = [],
// periodically check if new data needs to be downloaded or
// buffered data should be appended to the source buffer
this.fillBuffer();
player.on('timeupdate', videojs.bind(this, this.fillBuffer));
player.on('timeupdate', videojs.bind(this, this.drainBuffer));
player.on('waiting', videojs.bind(this, this.drainBuffer));
lastSeekedTime,
segmentXhr,
fillBuffer,
drainBuffer,
updateDuration;
player.trigger('loadedmetadata');
}));
this.playlists.on('error', videojs.bind(this, function() {
player.error(this.playlists.error);
}));
player.hls.currentTime = function() {
if (lastSeekedTime) {
return lastSeekedTime;
}
// currentTime is zero while the tech is initializing
if (!this.el() || !this.el().vjs_getProperty) {
return 0;
}
return this.el().vjs_getProperty('currentTime');
};
this.playlists.on('loadedplaylist', videojs.bind(this, function() {
var updatedPlaylist = this.playlists.media();
player.hls.setCurrentTime = function(currentTime) {
if (!(this.playlists && this.playlists.media())) {
// return immediately if the metadata is not ready yet
return 0;
if (!updatedPlaylist) {
// do nothing before an initial media playlist has been activated
return;
}
// save the seek target so currentTime can report it correctly
// while the seek is pending
lastSeekedTime = currentTime;
// determine the requested segment
this.mediaIndex =
getMediaIndexByTime(this.playlists.media(), currentTime);
this.updateDuration(this.playlists.media());
this.mediaIndex = videojs.Hls.translateMediaIndex(this.mediaIndex, oldMediaPlaylist, updatedPlaylist);
oldMediaPlaylist = updatedPlaylist;
}));
// abort any segments still being decoded
this.sourceBuffer.abort();
this.playlists.on('mediachange', function() {
player.trigger('mediachange');
});
};
// cancel outstanding requests and buffer appends
if (segmentXhr) {
segmentXhr.abort();
/**
* Reset the mediaIndex if play() is called after the video has
* ended.
*/
videojs.Hls.prototype.play = function() {
if (this.ended()) {
this.mediaIndex = 0;
}
// clear out any buffered segments
segmentBuffer = [];
// delegate back to the Flash implementation
return videojs.Flash.prototype.play.apply(this, arguments);
};
// begin filling the buffer at the new position
fillBuffer(currentTime * 1000);
};
videojs.Hls.prototype.duration = function() {
var playlists = this.playlists;
if (playlists) {
return videojs.Hls.getPlaylistTotalDuration(playlists.media());
}
return 0;
};
/**
/**
* Update the player duration
*/
updateDuration = function(playlist) {
var oldDuration = player.duration(),
newDuration = totalDuration(playlist);
videojs.Hls.prototype.updateDuration = function(playlist) {
var player = this.player(),
oldDuration = player.duration(),
newDuration = videojs.Hls.getPlaylistTotalDuration(playlist);
// if the duration has changed, invalidate the cached value
if (oldDuration !== newDuration) {
player.trigger('durationchange');
}
};
};
/**
/**
* Abort all outstanding work and cleanup.
*/
videojs.Hls.prototype.dispose = function() {
if (this.segmentXhr_) {
this.segmentXhr_.onreadystatechange = null;
this.segmentXhr_.abort();
}
if (this.playlists) {
this.playlists.dispose();
}
videojs.Flash.prototype.dispose.call(this);
};
/**
* Chooses the appropriate media playlist based on the current
* bandwidth estimate and the player size.
* @return the highest bitrate playlist less than the currently detected
* bandwidth, accounting for some amount of bandwidth variance
*/
player.hls.selectPlaylist = function () {
videojs.Hls.prototype.selectPlaylist = function () {
var
player = this.player(),
effectiveBitrate,
sortedPlaylists = player.hls.playlists.master.playlists.slice(),
sortedPlaylists = this.playlists.master.playlists.slice(),
bandwidthPlaylists = [],
i = sortedPlaylists.length,
variant,
bandwidthBestVariant,
resolutionBestVariant;
sortedPlaylists.sort(playlistBandwidth);
sortedPlaylists.sort(videojs.Hls.comparePlaylistBandwidth);
// filter out any variant that has greater effective bitrate
// than the current estimated bandwidth
......@@ -304,7 +252,7 @@ var
i = bandwidthPlaylists.length;
// sort variants by resolution
bandwidthPlaylists.sort(playlistResolution);
bandwidthPlaylists.sort(videojs.Hls.comparePlaylistResolution);
// iterate through the bandwidth-filtered playlists and find
// best rendition by player dimension
......@@ -331,30 +279,19 @@ var
// fallback chain of variants
return resolutionBestVariant || bandwidthBestVariant || sortedPlaylists[0];
};
/**
* Abort all outstanding work and cleanup.
*/
player.hls.dispose = function() {
if (segmentXhr) {
segmentXhr.onreadystatechange = null;
segmentXhr.abort();
}
if (this.playlists) {
this.playlists.dispose();
}
videojs.Flash.prototype.dispose.call(this);
};
};
/**
/**
* Determines whether there is enough video data currently in the buffer
* and downloads a new segment if the buffered time is less than the goal.
* @param offset (optional) {number} the offset into the downloaded segment
* to seek to, in milliseconds
*/
fillBuffer = function(offset) {
videojs.Hls.prototype.fillBuffer = function(offset) {
var
self = this,
player = this.player(),
settings = player.options().hls || {},
buffered = player.buffered(),
bufferedTime = 0,
segment,
......@@ -362,18 +299,18 @@ var
startTime;
// if there is a request already in flight, do nothing
if (segmentXhr) {
if (this.segmentXhr_) {
return;
}
// if no segments are available, do nothing
if (player.hls.playlists.state === "HAVE_NOTHING" ||
!player.hls.playlists.media().segments) {
if (this.playlists.state === "HAVE_NOTHING" ||
!this.playlists.media().segments) {
return;
}
// if the video has finished downloading, stop trying to buffer
segment = player.hls.playlists.media().segments[player.hls.mediaIndex];
segment = this.playlists.media().segments[this.mediaIndex];
if (!segment) {
return;
}
......@@ -391,17 +328,17 @@ var
}
// resolve the segment URL relative to the playlist
if (player.hls.playlists.media().uri === srcUrl) {
segmentUri = resolveUrl(srcUrl, segment.uri);
if (this.playlists.media().uri === this.src_) {
segmentUri = resolveUrl(this.src_, segment.uri);
} else {
segmentUri = resolveUrl(resolveUrl(srcUrl, player.hls.playlists.media().uri || ''),
segmentUri = resolveUrl(resolveUrl(this.src_, this.playlists.media().uri || ''),
segment.uri);
}
startTime = +new Date();
// request the next segment
segmentXhr = xhr({
this.segmentXhr_ = videojs.Hls.xhr({
url: segmentUri,
responseType: 'arraybuffer',
withCredentials: settings.withCredentials
......@@ -409,23 +346,23 @@ var
var tags;
// the segment request is no longer outstanding
segmentXhr = null;
self.segmentXhr_ = null;
if (error) {
// if a segment request times out, we may have better luck with another playlist
if (error === 'timeout') {
player.hls.bandwidth = 1;
return player.hls.playlists.media(player.hls.selectPlaylist());
self.bandwidth = 1;
return self.playlists.media(self.selectPlaylist());
}
// otherwise, try jumping ahead to the next segment
player.hls.error = {
self.error = {
status: this.status,
message: 'HLS segment request error at URL: ' + url,
code: (this.status >= 500) ? 4 : 2
};
// try moving on to the next segment
player.hls.mediaIndex++;
self.mediaIndex++;
return;
}
......@@ -435,39 +372,39 @@ var
}
// calculate the download bandwidth
player.hls.segmentXhrTime = (+new Date()) - startTime;
player.hls.bandwidth = (this.response.byteLength / player.hls.segmentXhrTime) * 8 * 1000;
player.hls.bytesReceived += this.response.byteLength;
self.segmentXhrTime = (+new Date()) - startTime;
self.bandwidth = (this.response.byteLength / player.hls.segmentXhrTime) * 8 * 1000;
self.bytesReceived += this.response.byteLength;
// transmux the segment data from MP2T to FLV
segmentParser.parseSegmentBinaryData(new Uint8Array(this.response));
segmentParser.flushTags();
self.segmentParser_.parseSegmentBinaryData(new Uint8Array(this.response));
self.segmentParser_.flushTags();
// package up all the work to append the segment
// if the segment is the start of a timestamp discontinuity,
// we have to wait until the sourcebuffer is empty before
// aborting the source buffer processing
tags = [];
while (segmentParser.tagsAvailable()) {
tags.push(segmentParser.getNextTag());
while (self.segmentParser_.tagsAvailable()) {
tags.push(self.segmentParser_.getNextTag());
}
segmentBuffer.push({
mediaIndex: player.hls.mediaIndex,
playlist: player.hls.playlists.media(),
self.segmentBuffer_.push({
mediaIndex: self.mediaIndex,
playlist: self.playlists.media(),
offset: offset,
tags: tags
});
drainBuffer();
self.drainBuffer();
player.hls.mediaIndex++;
self.mediaIndex++;
// figure out what stream the next segment should be downloaded from
// with the updated bandwidth information
player.hls.playlists.media(player.hls.selectPlaylist());
self.playlists.media(self.selectPlaylist());
});
};
};
drainBuffer = function(event) {
videojs.Hls.prototype.drainBuffer = function(event) {
var
i = 0,
mediaIndex,
......@@ -477,7 +414,8 @@ var
segment,
ptsTime,
segmentOffset;
segmentOffset,
segmentBuffer = this.segmentBuffer_;
if (!segmentBuffer.length) {
return;
......@@ -490,7 +428,7 @@ var
segment = playlist.segments[mediaIndex];
event = event || {};
segmentOffset = duration(playlist, 0, mediaIndex) * 1000;
segmentOffset = videojs.Hls.getPlaylistDuration(playlist, 0, mediaIndex) * 1000;
// abort() clears any data queued in the source buffer so wait
// until it empties before calling it when a discontinuity is
......@@ -499,9 +437,9 @@ var
if (event.type !== 'waiting') {
return;
}
player.hls.sourceBuffer.abort();
this.sourceBuffer.abort();
// tell the SWF where playback is continuing in the stitched timeline
player.hls.el().vjs_setProperty('currentTime', segmentOffset * 0.001);
this.el().vjs_setProperty('currentTime', segmentOffset * 0.001);
}
// if we're refilling the buffer after a seek, scan through the muxed
......@@ -515,11 +453,11 @@ var
}
// tell the SWF where we will be seeking to
player.hls.el().vjs_setProperty('currentTime', (tags[i].pts - tags[0].pts + segmentOffset) * 0.001);
this.el().vjs_setProperty('currentTime', (tags[i].pts - tags[0].pts + segmentOffset) * 0.001);
tags = tags.slice(i);
lastSeekedTime = null;
this.lastSeekedTime_ = null;
}
for (i = 0; i < tags.length; i++) {
......@@ -527,83 +465,18 @@ var
// the queue gives control back to the browser between tags
// so that large segments don't cause a "hiccup" in playback
player.hls.sourceBuffer.appendBuffer(tags[i].bytes, player);
this.sourceBuffer.appendBuffer(tags[i].bytes, this.player());
}
// we're done processing this segment
segmentBuffer.shift();
if (mediaIndex === playlist.segments.length) {
mediaSource.endOfStream();
}
};
// load the MediaSource into the player
mediaSource.addEventListener('sourceopen', function() {
// construct the video data buffer and set the appropriate MIME type
var
sourceBuffer = mediaSource.addSourceBuffer('video/flv; codecs="vp6,aac"'),
oldMediaPlaylist;
player.hls.sourceBuffer = sourceBuffer;
sourceBuffer.appendBuffer(segmentParser.getFlvHeader());
player.hls.mediaIndex = 0;
player.hls.playlists =
new videojs.Hls.PlaylistLoader(srcUrl, settings.withCredentials);
player.hls.playlists.on('loadedmetadata', function() {
oldMediaPlaylist = player.hls.playlists.media();
// periodically check if new data needs to be downloaded or
// buffered data should be appended to the source buffer
fillBuffer();
player.on('timeupdate', fillBuffer);
player.on('timeupdate', drainBuffer);
player.on('waiting', drainBuffer);
player.trigger('loadedmetadata');
});
player.hls.playlists.on('error', function() {
player.error(player.hls.playlists.error);
});
player.hls.playlists.on('loadedplaylist', function() {
var updatedPlaylist = player.hls.playlists.media();
if (!updatedPlaylist) {
// do nothing before an initial media playlist has been activated
return;
// transition the sourcebuffer to the ended state if we've hit the end of
// the playlist
if (mediaIndex + 1 === playlist.segments.length) {
this.mediaSource.endOfStream();
}
updateDuration(player.hls.playlists.media());
player.hls.mediaIndex = translateMediaIndex(player.hls.mediaIndex,
oldMediaPlaylist,
updatedPlaylist);
oldMediaPlaylist = updatedPlaylist;
});
player.hls.playlists.on('mediachange', function() {
player.trigger('mediachange');
});
});
};
var mpegurlRE = /^application\/(?:x-|vnd\.apple\.)mpegurl/i;
videojs.Hls = videojs.Flash.extend({
init: function(player, options, ready) {
var
source = options.source,
settings = player.options();
player.hls = this;
delete options.source;
options.swf = settings.flash.swf;
videojs.Flash.call(this, player, options, ready);
options.source = source;
this.bytesReceived = 0;
videojs.Hls.prototype.src.call(this, options.source && options.source.src);
}
});
};
/**
* Whether the browser has built-in HLS support.
......@@ -625,43 +498,6 @@ videojs.Hls.supportsNativeHls = (function() {
(/probably|maybe/).test(vndMpeg);
})();
// the desired length of video to maintain in the buffer, in seconds
videojs.Hls.GOAL_BUFFER_LENGTH = 30;
videojs.Hls.prototype.src = function(src) {
var
player = this.player(),
self = this,
mediaSource,
source;
if (src) {
mediaSource = new videojs.MediaSource();
source = {
src: videojs.URL.createObjectURL(mediaSource),
type: "video/flv"
};
this.mediaSource = mediaSource;
initSource(player, mediaSource, src);
this.player().ready(function() {
// do nothing if the tech has been disposed already
// this can occur if someone sets the src in player.ready(), for instance
if (!self.el()) {
return;
}
self.el().vjs_src(source.src);
});
}
};
videojs.Hls.prototype.duration = function() {
var playlists = this.playlists;
if (playlists) {
return totalDuration(playlists.media());
}
return 0;
};
videojs.Hls.isSupported = function() {
return !videojs.Hls.supportsNativeHls &&
videojs.Flash.isSupported() &&
......@@ -669,89 +505,182 @@ videojs.Hls.isSupported = function() {
};
videojs.Hls.canPlaySource = function(srcObj) {
var mpegurlRE = /^application\/(?:x-|vnd\.apple\.)mpegurl/i;
return mpegurlRE.test(srcObj.type);
};
/**
* 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.
* @param callback (optional) {function} a function to call when the
* request completes. If the request was not successful, the first
* argument will be falsey.
* @return {object} the XMLHttpRequest that was initiated.
* Calculate the duration of a playlist from a given start index to a given
* end index.
* @param playlist {object} a media playlist object
* @param startIndex {number} an inclusive lower boundary for the playlist.
* Defaults to 0.
* @param endIndex {number} an exclusive upper boundary for the playlist.
* Defaults to playlist length.
* @return {number} the duration between the start index and end index.
*/
xhr = videojs.Hls.xhr = function(url, callback) {
var
options = {
method: 'GET',
timeout: 45 * 1000
},
request,
abortTimeout;
videojs.Hls.getPlaylistDuration = function(playlist, startIndex, endIndex) {
var dur = 0,
segment,
i;
startIndex = startIndex || 0;
endIndex = endIndex !== undefined ? endIndex : (playlist.segments || []).length;
i = endIndex - 1;
for (; i >= startIndex; i--) {
segment = playlist.segments[i];
dur += segment.duration || playlist.targetDuration || 0;
}
return dur;
};
/**
* 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
*/
videojs.Hls.getPlaylistTotalDuration = function(playlist) {
if (!playlist) {
return 0;
}
if (typeof callback !== 'function') {
callback = function() {};
// if present, use the duration specified in the playlist
if (playlist.totalDuration) {
return playlist.totalDuration;
}
if (typeof url === 'object') {
options = videojs.util.mergeOptions(options, url);
url = options.url;
// duration should be Infinity for live playlists
if (!playlist.endList) {
return window.Infinity;
}
request = new window.XMLHttpRequest();
request.open(options.method, url);
request.url = url;
return videojs.Hls.getPlaylistDuration(playlist);
};
/**
* 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
*/
videojs.Hls.translateMediaIndex = function(mediaIndex, original, update) {
var
i,
originalSegment;
// no segments have been loaded from the original playlist
if (mediaIndex === 0) {
return 0;
}
if (!(update && update.segments)) {
// let the media index be zero when there are no segments defined
return 0;
}
if (options.responseType) {
request.responseType = options.responseType;
// try to sync based on URI
i = update.segments.length;
originalSegment = original.segments[mediaIndex - 1];
while (i--) {
if (originalSegment.uri === update.segments[i].uri) {
return i + 1;
}
if (options.withCredentials) {
request.withCredentials = true;
}
if (options.timeout) {
if (request.timeout === 0) {
request.timeout = options.timeout;
request.ontimeout = function() {
request.timedout = true;
};
} else {
// polyfill XHR2 by aborting after the timeout
abortTimeout = window.setTimeout(function() {
if (request.readyState !== 4) {
request.timedout = true;
request.abort();
// sync on media sequence
return (original.mediaSequence + mediaIndex) - update.mediaSequence;
};
/**
* TODO - Document this great feature.
*
* @param playlist
* @param time
* @returns int
*/
videojs.Hls.getMediaIndexByTime = function(playlist, time) {
var index, counter, timeRanges, currentSegmentRange;
timeRanges = [];
for (index = 0; index < playlist.segments.length; index++) {
currentSegmentRange = {};
currentSegmentRange.start = (index === 0) ? 0 : timeRanges[index - 1].end;
currentSegmentRange.end = currentSegmentRange.start + playlist.segments[index].duration;
timeRanges.push(currentSegmentRange);
}
}, options.timeout);
for (counter = 0; counter < timeRanges.length; counter++) {
if (time >= timeRanges[counter].start && time < timeRanges[counter].end) {
return counter;
}
}
request.onreadystatechange = function() {
// wait until the request completes
if (this.readyState !== 4) {
return;
return -1;
};
/**
* A comparator function to sort two playlist object by bandwidth.
* @param left {object} a media playlist object
* @param right {object} a media playlist object
* @return {number} Greater than zero if the bandwidth attribute of
* left is greater than the corresponding attribute of right. Less
* than zero if the bandwidth of right is greater than left and
* exactly zero if the two are equal.
*/
videojs.Hls.comparePlaylistBandwidth = function(left, right) {
var leftBandwidth, rightBandwidth;
if (left.attributes && left.attributes.BANDWIDTH) {
leftBandwidth = left.attributes.BANDWIDTH;
}
leftBandwidth = leftBandwidth || window.Number.MAX_VALUE;
if (right.attributes && right.attributes.BANDWIDTH) {
rightBandwidth = right.attributes.BANDWIDTH;
}
rightBandwidth = rightBandwidth || window.Number.MAX_VALUE;
return leftBandwidth - rightBandwidth;
};
// clear outstanding timeouts
window.clearTimeout(abortTimeout);
/**
* A comparator function to sort two playlist object by resolution (width).
* @param left {object} a media playlist object
* @param right {object} a media playlist object
* @return {number} Greater than zero if the resolution.width attribute of
* left is greater than the corresponding attribute of right. Less
* than zero if the resolution.width of right is greater than left and
* exactly zero if the two are equal.
*/
videojs.Hls.comparePlaylistResolution = function(left, right) {
var leftWidth, rightWidth;
// request timeout
if (request.timedout) {
return callback.call(this, 'timeout', url);
if (left.attributes && left.attributes.RESOLUTION && left.attributes.RESOLUTION.width) {
leftWidth = left.attributes.RESOLUTION.width;
}
// request aborted or errored
if (this.status >= 400 || this.status === 0) {
return callback.call(this, true, url);
leftWidth = leftWidth || window.Number.MAX_VALUE;
if (right.attributes && right.attributes.RESOLUTION && right.attributes.RESOLUTION.width) {
rightWidth = right.attributes.RESOLUTION.width;
}
return callback.call(this, false, url);
};
request.send(null);
return request;
rightWidth = rightWidth || window.Number.MAX_VALUE;
// NOTE - Fallback to bandwidth sort as appropriate in cases where multiple renditions
// have the same media dimensions/ resolution
if (leftWidth === rightWidth && left.attributes.BANDWIDTH && right.attributes.BANDWIDTH) {
return left.attributes.BANDWIDTH - right.attributes.BANDWIDTH;
} else {
return leftWidth - rightWidth;
}
};
/**
......@@ -793,7 +722,4 @@ resolveUrl = videojs.Hls.resolveUrl = function(basePath, path) {
return result;
};
// Add HLS to the standard tech order
videojs.options.techOrder.unshift('hls');
})(window, window.videojs, document);
......
(function(videojs){
/**
* Creates and sends an XMLHttpRequest.
* TODO - expose video.js core's XHR and use that instead
*
* @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.
* @param callback (optional) {function} a function to call when the
* request completes. If the request was not successful, the first
* argument will be falsey.
* @return {object} the XMLHttpRequest that was initiated.
*/
videojs.Hls.xhr = function(url, callback) {
var
options = {
method: 'GET',
timeout: 45 * 1000
},
request,
abortTimeout;
if (typeof callback !== 'function') {
callback = function() {};
}
if (typeof url === 'object') {
options = videojs.util.mergeOptions(options, url);
url = options.url;
}
request = new window.XMLHttpRequest();
request.open(options.method, url);
request.url = url;
if (options.responseType) {
request.responseType = options.responseType;
}
if (options.withCredentials) {
request.withCredentials = true;
}
if (options.timeout) {
if (request.timeout === 0) {
request.timeout = options.timeout;
request.ontimeout = function() {
request.timedout = true;
};
} else {
// polyfill XHR2 by aborting after the timeout
abortTimeout = window.setTimeout(function() {
if (request.readyState !== 4) {
request.timedout = true;
request.abort();
}
}, options.timeout);
}
}
request.onreadystatechange = function() {
// wait until the request completes
if (this.readyState !== 4) {
return;
}
// clear outstanding timeouts
window.clearTimeout(abortTimeout);
// request timeout
if (request.timedout) {
return callback.call(this, 'timeout', url);
}
// request aborted or errored
if (this.status >= 400 || this.status === 0) {
return callback.call(this, true, url);
}
return callback.call(this, false, url);
};
request.send(null);
return request;
};
})(window.videojs);
\ No newline at end of file
......@@ -79,6 +79,7 @@ module.exports = function(config) {
'../node_modules/videojs-contrib-media-sources/src/videojs-media-sources.js',
'../test/karma-qunit-shim.js',
'../src/videojs-hls.js',
'../src/xhr.js',
'../src/flv-tag.js',
'../src/exp-golomb.js',
'../src/h264-stream.js',
......
......@@ -43,6 +43,7 @@ module.exports = function(config) {
'../node_modules/videojs-contrib-media-sources/src/videojs-media-sources.js',
'../test/karma-qunit-shim.js',
'../src/videojs-hls.js',
'../src/xhr.js',
'../src/flv-tag.js',
'../src/exp-golomb.js',
'../src/h264-stream.js',
......
......@@ -123,6 +123,7 @@
<script src="../../node_modules/video.js/dist/video-js/video.js"></script>
<script src="../../node_modules/videojs-contrib-media-sources/src/videojs-media-sources.js"></script>
<script src="../../src/videojs-hls.js"></script>
<script src="../../src/xhr.js"></script>
<script src="../../src/stream.js"></script>
<script src="../../src/m3u8/m3u8-parser.js"></script>
<script src="../../src/playlist-loader.js"></script>
......
......@@ -20,6 +20,7 @@
<!-- HLS plugin -->
<script src="../src/videojs-hls.js"></script>
<script src="../src/xhr.js"></script>
<script src="../src/flv-tag.js"></script>
<script src="../src/exp-golomb.js"></script>
<script src="../src/h264-stream.js"></script>
......
......@@ -51,10 +51,18 @@ var
tech.vjs_getProperty = function() {};
tech.vjs_setProperty = function() {};
tech.vjs_src = function() {};
tech.vjs_play = function() {};
videojs.Flash.onReady(tech.id);
return player;
},
openMediaSource = function(player) {
player.hls.mediaSource.trigger({
type: 'sourceopen'
});
// endOfStream triggers an exception if flash isn't available
player.hls.mediaSource.endOfStream = function() {};
},
standardXHRResponse = function(request) {
if (!request.url) {
return;
......@@ -160,9 +168,7 @@ test('starts playing if autoplay is specified', function() {
src: 'manifest/playlist.m3u8',
type: 'application/vnd.apple.mpegurl'
});
player.hls.mediaSource.trigger({
type: 'sourceopen'
});
openMediaSource(player);
standardXHRResponse(requests[0]);
strictEqual(1, plays, 'play was called');
......@@ -179,9 +185,7 @@ test('creates a PlaylistLoader on init', function() {
src:'manifest/playlist.m3u8',
type: 'application/vnd.apple.mpegurl'
});
player.hls.mediaSource.trigger({
type: 'sourceopen'
});
openMediaSource(player);
standardXHRResponse(requests[0]);
ok(loadedmetadata, 'loadedmetadata fires');
ok(player.hls.playlists.master, 'set the master playlist');
......@@ -204,9 +208,7 @@ test('sets the duration if one is available on the playlist', function() {
src: 'manifest/media.m3u8',
type: 'application/vnd.apple.mpegurl'
});
player.hls.mediaSource.trigger({
type: 'sourceopen'
});
openMediaSource(player);
standardXHRResponse(requests[0]);
strictEqual(calls, 1, 'duration is set');
......@@ -226,9 +228,7 @@ test('calculates the duration if needed', function() {
src: 'http://example.com/manifest/missingExtinf.m3u8',
type: 'application/vnd.apple.mpegurl'
});
player.hls.mediaSource.trigger({
type: 'sourceopen'
});
openMediaSource(player);
standardXHRResponse(requests[0]);
strictEqual(durations.length, 1, 'duration is set');
......@@ -245,9 +245,7 @@ test('starts downloading a segment on loadedmetadata', function() {
player.buffered = function() {
return videojs.createTimeRange(0, 0);
};
player.hls.mediaSource.trigger({
type: 'sourceopen'
});
openMediaSource(player);
standardXHRResponse(requests[0]);
standardXHRResponse(requests[1]);
......@@ -263,9 +261,7 @@ test('recognizes absolute URIs and requests them unmodified', function() {
src: 'manifest/absoluteUris.m3u8',
type: 'application/vnd.apple.mpegurl'
});
player.hls.mediaSource.trigger({
type: 'sourceopen'
});
openMediaSource(player);
standardXHRResponse(requests[0]);
standardXHRResponse(requests[1]);
......@@ -279,9 +275,7 @@ test('recognizes domain-relative URLs', function() {
src: 'manifest/domainUris.m3u8',
type: 'application/vnd.apple.mpegurl'
});
player.hls.mediaSource.trigger({
type: 'sourceopen'
});
openMediaSource(player);
standardXHRResponse(requests[0]);
standardXHRResponse(requests[1]);
......@@ -297,9 +291,7 @@ test('re-initializes the tech for each source', function() {
src: 'manifest/master.m3u8',
type: 'application/vnd.apple.mpegurl'
});
player.hls.mediaSource.trigger({
type: 'sourceopen'
});
openMediaSource(player);
firstPlaylists = player.hls.playlists;
firstMSE = player.hls.mediaSource;
......@@ -307,9 +299,7 @@ test('re-initializes the tech for each source', function() {
src: 'manifest/master.m3u8',
type: 'application/vnd.apple.mpegurl'
});
player.hls.mediaSource.trigger({
type: 'sourceopen'
});
openMediaSource(player);
secondPlaylists = player.hls.playlists;
secondMSE = player.hls.mediaSource;
......@@ -326,9 +316,7 @@ test('triggers an error when a master playlist request errors', function() {
src: 'manifest/master.m3u8',
type: 'application/vnd.apple.mpegurl'
});
player.hls.mediaSource.trigger({
type: 'sourceopen'
});
openMediaSource(player);
requests.pop().respond(500);
ok(player.error(), 'an error is triggered');
......@@ -341,9 +329,7 @@ test('downloads media playlists after loading the master', function() {
src: 'manifest/master.m3u8',
type: 'application/vnd.apple.mpegurl'
});
player.hls.mediaSource.trigger({
type: 'sourceopen'
});
openMediaSource(player);
standardXHRResponse(requests[0]);
standardXHRResponse(requests[1]);
......@@ -367,9 +353,7 @@ test('timeupdates do not check to fill the buffer until a media playlist is read
src: 'manifest/media.m3u8',
type: 'application/vnd.apple.mpegurl'
});
player.hls.mediaSource.trigger({
type: 'sourceopen'
});
openMediaSource(player);
player.trigger('timeupdate');
strictEqual(1, requests.length, 'one request was made');
......@@ -381,9 +365,7 @@ test('calculates the bandwidth after downloading a segment', function() {
src: 'manifest/media.m3u8',
type: 'application/vnd.apple.mpegurl'
});
player.hls.mediaSource.trigger({
type: 'sourceopen'
});
openMediaSource(player);
standardXHRResponse(requests[0]);
standardXHRResponse(requests[1]);
......@@ -405,9 +387,7 @@ test('selects a playlist after segment downloads', function() {
calls++;
return player.hls.playlists.master.playlists[0];
};
player.hls.mediaSource.trigger({
type: 'sourceopen'
});
openMediaSource(player);
standardXHRResponse(requests[0]);
standardXHRResponse(requests[1]);
......@@ -433,9 +413,7 @@ test('moves to the next segment if there is a network error', function() {
src: 'manifest/master.m3u8',
type: 'application/vnd.apple.mpegurl'
});
player.hls.mediaSource.trigger({
type: 'sourceopen'
});
openMediaSource(player);
standardXHRResponse(requests[0]);
standardXHRResponse(requests[1]);
......@@ -468,9 +446,7 @@ test('updates the duration after switching playlists', function() {
calls++;
}
};
player.hls.mediaSource.trigger({
type: 'sourceopen'
});
openMediaSource(player);
standardXHRResponse(requests[0]);
standardXHRResponse(requests[1]);
......@@ -490,9 +466,7 @@ test('downloads additional playlists if required', function() {
src: 'manifest/master.m3u8',
type: 'application/vnd.apple.mpegurl'
});
player.hls.mediaSource.trigger({
type: 'sourceopen'
});
openMediaSource(player);
standardXHRResponse(requests[0]);
standardXHRResponse(requests[1]);
......@@ -531,9 +505,7 @@ test('selects a playlist below the current bandwidth', function() {
src: 'manifest/master.m3u8',
type: 'application/vnd.apple.mpegurl'
});
player.hls.mediaSource.trigger({
type: 'sourceopen'
});
openMediaSource(player);
standardXHRResponse(requests[0]);
......@@ -556,9 +528,7 @@ test('raises the minimum bitrate for a stream proportionially', function() {
src: 'manifest/master.m3u8',
type: 'application/vnd.apple.mpegurl'
});
player.hls.mediaSource.trigger({
type: 'sourceopen'
});
openMediaSource(player);
standardXHRResponse(requests[0]);
......@@ -581,9 +551,7 @@ test('uses the lowest bitrate if no other is suitable', function() {
src: 'manifest/master.m3u8',
type: 'application/vnd.apple.mpegurl'
});
player.hls.mediaSource.trigger({
type: 'sourceopen'
});
openMediaSource(player);
standardXHRResponse(requests[0]);
......@@ -605,9 +573,7 @@ test('selects the correct rendition by player dimensions', function() {
type: 'application/vnd.apple.mpegurl'
});
player.hls.mediaSource.trigger({
type: 'sourceopen'
});
openMediaSource(player);
standardXHRResponse(requests[0]);
......@@ -644,9 +610,7 @@ test('does not download the next segment if the buffer is full', function() {
player.buffered = function() {
return videojs.createTimeRange(0, currentTime + videojs.Hls.GOAL_BUFFER_LENGTH);
};
player.hls.mediaSource.trigger({
type: 'sourceopen'
});
openMediaSource(player);
standardXHRResponse(requests[0]);
......@@ -660,9 +624,7 @@ test('downloads the next segment if the buffer is getting low', function() {
src: 'manifest/media.m3u8',
type: 'application/vnd.apple.mpegurl'
});
player.hls.mediaSource.trigger({
type: 'sourceopen'
});
openMediaSource(player);
standardXHRResponse(requests[0]);
standardXHRResponse(requests[1]);
......@@ -691,9 +653,7 @@ test('stops downloading segments at the end of the playlist', function() {
src: 'manifest/media.m3u8',
type: 'application/vnd.apple.mpegurl'
});
player.hls.mediaSource.trigger({
type: 'sourceopen'
});
openMediaSource(player);
standardXHRResponse(requests[0]);
requests = [];
player.hls.mediaIndex = 4;
......@@ -707,9 +667,7 @@ test('only makes one segment request at a time', function() {
src: 'manifest/media.m3u8',
type: 'application/vnd.apple.mpegurl'
});
player.hls.mediaSource.trigger({
type: 'sourceopen'
});
openMediaSource(player);
standardXHRResponse(requests.pop());
player.trigger('timeupdate');
......@@ -723,9 +681,7 @@ test('cancels outstanding XHRs when seeking', function() {
src: 'manifest/media.m3u8',
type: 'application/vnd.apple.mpegurl'
});
player.hls.mediaSource.trigger({
type: 'sourceopen'
});
openMediaSource(player);
standardXHRResponse(requests[0]);
player.hls.media = {
segments: [{
......@@ -764,9 +720,7 @@ test('flushes the parser after each segment', function() {
src: 'manifest/media.m3u8',
type: 'application/vnd.apple.mpegurl'
});
player.hls.mediaSource.trigger({
type: 'sourceopen'
});
openMediaSource(player);
standardXHRResponse(requests[0]);
standardXHRResponse(requests[1]);
......@@ -794,9 +748,7 @@ test('drops tags before the target timestamp when seeking', function() {
src: 'manifest/media.m3u8',
type: 'application/vnd.apple.mpegurl'
});
player.hls.mediaSource.trigger({
type: 'sourceopen'
});
openMediaSource(player);
standardXHRResponse(requests[0]);
standardXHRResponse(requests[1]);
......@@ -836,9 +788,7 @@ test('calls abort() on the SourceBuffer before seeking', function() {
src: 'manifest/media.m3u8',
type: 'application/vnd.apple.mpegurl'
});
player.hls.mediaSource.trigger({
type: 'sourceopen'
});
openMediaSource(player);
standardXHRResponse(requests[0]);
standardXHRResponse(requests[1]);
......@@ -863,9 +813,7 @@ test('playlist 404 should trigger MEDIA_ERR_NETWORK', function() {
src: 'manifest/media.m3u8',
type: 'application/vnd.apple.mpegurl'
});
player.hls.mediaSource.trigger({
type: 'sourceopen'
});
openMediaSource(player);
requests.pop().respond(404);
equal(errorTriggered,
......@@ -883,9 +831,7 @@ test('segment 404 should trigger MEDIA_ERR_NETWORK', function () {
type: 'application/vnd.apple.mpegurl'
});
player.hls.mediaSource.trigger({
type: 'sourceopen'
});
openMediaSource(player);
standardXHRResponse(requests[0]);
requests[1].respond(404);
......@@ -899,9 +845,7 @@ test('segment 500 should trigger MEDIA_ERR_ABORTED', function () {
type: 'application/vnd.apple.mpegurl'
});
player.hls.mediaSource.trigger({
type: 'sourceopen'
});
openMediaSource(player);
standardXHRResponse(requests[0]);
requests[1].respond(500);
......@@ -914,9 +858,7 @@ test('duration is Infinity for live playlists', function() {
src: 'http://example.com/manifest/missingEndlist.m3u8',
type: 'application/vnd.apple.mpegurl'
});
player.hls.mediaSource.trigger({
type: 'sourceopen'
});
openMediaSource(player);
standardXHRResponse(requests[0]);
......@@ -929,9 +871,7 @@ test('updates the media index when a playlist reloads', function() {
src: 'http://example.com/live-updating.m3u8',
type: 'application/vnd.apple.mpegurl'
});
player.hls.mediaSource.trigger({
type: 'sourceopen'
});
openMediaSource(player);
requests[0].respond(200, null,
'#EXTM3U\n' +
......@@ -971,9 +911,7 @@ test('mediaIndex is zero before the first segment loads', function() {
src: 'http://example.com/first-seg-load.m3u8',
type: 'application/vnd.apple.mpegurl'
});
player.hls.mediaSource.trigger({
type: 'sourceopen'
});
openMediaSource(player);
strictEqual(player.hls.mediaIndex, 0, 'mediaIndex is zero');
});
......@@ -983,9 +921,7 @@ test('reloads out-of-date live playlists when switching variants', function() {
src: 'http://example.com/master.m3u8',
type: 'application/vnd.apple.mpegurl'
});
player.hls.mediaSource.trigger({
type: 'sourceopen'
});
openMediaSource(player);
player.hls.master = {
playlists: [{
......@@ -1026,9 +962,7 @@ test('if withCredentials option is used, withCredentials is set on the XHR objec
src: 'http://example.com/media.m3u8',
type: 'application/vnd.apple.mpegurl'
});
player.hls.mediaSource.trigger({
type: 'sourceopen'
});
openMediaSource(player);
ok(requests[0].withCredentials, "with credentials should be set to true if that option is passed in");
});
......@@ -1038,9 +972,7 @@ test('does not break if the playlist has no segments', function() {
type: 'application/vnd.apple.mpegurl'
});
try {
player.hls.mediaSource.trigger({
type: 'sourceopen'
});
openMediaSource(player);
requests[0].respond(200, null,
'#EXTM3U\n' +
'#EXT-X-PLAYLIST-TYPE:VOD\n' +
......@@ -1060,9 +992,7 @@ test('waits until the buffer is empty before appending bytes at a discontinuity'
src: 'disc.m3u8',
type: 'application/vnd.apple.mpegurl'
});
player.hls.mediaSource.trigger({
type: 'sourceopen'
});
openMediaSource(player);
player.currentTime = function() { return currentTime; };
player.buffered = function() {
return videojs.createTimeRange(0, bufferEnd);
......@@ -1109,9 +1039,7 @@ test('clears the segment buffer on seek', function() {
src: 'disc.m3u8',
type: 'application/vnd.apple.mpegurl'
});
player.hls.mediaSource.trigger({
type: 'sourceopen'
});
openMediaSource(player);
oldCurrentTime = player.currentTime;
player.currentTime = function(time) {
if (time !== undefined) {
......@@ -1158,9 +1086,7 @@ test('resets the switching algorithm if a request times out', function() {
src: 'master.m3u8',
type: 'application/vnd.apple.mpegurl'
});
player.hls.mediaSource.trigger({
type: 'sourceopen'
});
openMediaSource(player);
standardXHRResponse(requests.shift()); // master
standardXHRResponse(requests.shift()); // media.m3u8
// simulate a segment timeout
......@@ -1181,9 +1107,7 @@ test('disposes the playlist loader', function() {
src: 'manifest/master.m3u8',
type: 'application/vnd.apple.mpegurl'
});
player.hls.mediaSource.trigger({
type: 'sourceopen'
});
openMediaSource(player);
loaderDispose = player.hls.playlists.dispose;
player.hls.playlists.dispose = function() {
disposes++;
......@@ -1232,9 +1156,7 @@ test('tracks the bytes downloaded', function() {
src: 'http://example.com/media.m3u8',
type: 'application/vnd.apple.mpegurl'
});
player.hls.mediaSource.trigger({
type: 'sourceopen'
});
openMediaSource(player);
strictEqual(player.hls.bytesReceived, 0, 'no bytes received');
......@@ -1270,9 +1192,7 @@ test('re-emits mediachange events', function() {
src: 'http://example.com/media.m3u8',
type: 'application/vnd.apple.mpegurl'
});
player.hls.mediaSource.trigger({
type: 'sourceopen'
});
openMediaSource(player);
player.hls.playlists.trigger('mediachange');
strictEqual(mediaChanges, 1, 'fired mediachange');
......@@ -1302,4 +1222,48 @@ test('can be disposed before finishing initialization', function() {
}
});
test('calls ended() on the media source at the end of a playlist', function() {
var endOfStreams = 0;
player.src({
src: 'http://example.com/media.m3u8',
type: 'application/vnd.apple.mpegurl'
});
openMediaSource(player);
player.hls.mediaSource.endOfStream = function() {
endOfStreams++;
};
// playlist response
requests.shift().respond(200, null,
'#EXTM3U\n' +
'#EXTINF:10,\n' +
'0.ts\n' +
'#EXT-X-ENDLIST\n');
// segment response
requests[0].response = new ArrayBuffer(17);
requests.shift().respond(200, null, '');
strictEqual(endOfStreams, 1, 'ended media source');
});
test('calling play() at the end of a video resets the media index', function() {
player.src({
src: 'http://example.com/media.m3u8',
type: 'application/vnd.apple.mpegurl'
});
openMediaSource(player);
requests.shift().respond(200, null,
'#EXTM3U\n' +
'#EXTINF:10,\n' +
'0.ts\n' +
'#EXT-X-ENDLIST\n');
standardXHRResponse(requests.shift());
strictEqual(player.hls.mediaIndex, 1, 'index is 1 after the first segment');
player.hls.ended = function() {
return true;
};
player.play();
strictEqual(player.hls.mediaIndex, 0, 'index is 1 after the first segment');
});
})(window, window.videojs);
......