Skip to content
This repository has been archived by the owner on Jan 12, 2019. It is now read-only.

Commit

Permalink
feature: Support for Akamai-style Redundant HLS (#990)
Browse files Browse the repository at this point in the history
* stable sorting and always pick primary first
  • Loading branch information
zhuangs authored and mjneil committed Feb 9, 2017
1 parent e276883 commit 67ddaba
Show file tree
Hide file tree
Showing 2 changed files with 116 additions and 75 deletions.
147 changes: 72 additions & 75 deletions src/videojs-contrib-hls.js
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,26 @@ const handleHlsLoadedMetadata = function(qualityLevels, hls) {
handleHlsMediaChange(qualityLevels, hls.playlists);
};

/**
* Resuable stable sort function
*
* @param {Playlists} array
* @param {Function} sortFn Different comparators
* @function stableSort
*/
const stableSort = function(array, sortFn) {
let newArray = array.slice();

array.sort(function(left, right) {
let cmp = sortFn(left, right);

if (cmp === 0) {
return newArray.indexOf(left) - newArray.indexOf(right);
}
return cmp;
});
};

/**
* Chooses the appropriate media playlist based on the current
* bandwidth estimate and the player size.
Expand All @@ -124,103 +144,80 @@ const handleHlsLoadedMetadata = function(qualityLevels, hls) {
* bandwidth, accounting for some amount of bandwidth variance
*/
Hls.STANDARD_PLAYLIST_SELECTOR = function() {
let effectiveBitrate;
let sortedPlaylists = this.playlists.master.playlists.slice();
let bandwidthPlaylists = [];
let i;
let variant;
let bandwidthBestVariant;
let resolutionPlusOne;
let resolutionPlusOneAttribute;
let resolutionBestVariant;
let width;
let height;
let systemBandwidth;
let haveResolution;
let resolutionPlusOneList = [];
let resolutionPlusOneSmallest = [];
let resolutionBestVariantList = [];

sortedPlaylists.sort(Hls.comparePlaylistBandwidth);
stableSort(sortedPlaylists, Hls.comparePlaylistBandwidth);

// filter out any playlists that have been excluded due to
// incompatible configurations or playback errors
sortedPlaylists = sortedPlaylists.filter(Playlist.isEnabled);

// filter out any variant that has greater effective bitrate
// than the current estimated bandwidth
i = sortedPlaylists.length;
while (i--) {
variant = sortedPlaylists[i];

// ignore playlists without bandwidth information
if (!variant.attributes || !variant.attributes.BANDWIDTH) {
continue;
}

effectiveBitrate = variant.attributes.BANDWIDTH * BANDWIDTH_VARIANCE;

if (effectiveBitrate < this.systemBandwidth) {
bandwidthPlaylists.push(variant);

// since the playlists are sorted in ascending order by
// bandwidth, the first viable variant is the best
if (!bandwidthBestVariant) {
bandwidthBestVariant = variant;
}
}
}
systemBandwidth = this.systemBandwidth;
bandwidthPlaylists = sortedPlaylists.filter(function(elem) {
return elem.attributes &&
elem.attributes.BANDWIDTH &&
elem.attributes.BANDWIDTH * BANDWIDTH_VARIANCE < systemBandwidth;
});

i = bandwidthPlaylists.length;
// get all of the renditions with the same (highest) bandwidth
// and then taking the very first element
bandwidthBestVariant = bandwidthPlaylists.filter(function(elem) {
return elem.attributes.BANDWIDTH === bandwidthPlaylists[bandwidthPlaylists.length - 1].attributes.BANDWIDTH;
})[0];

// sort variants by resolution
bandwidthPlaylists.sort(Hls.comparePlaylistResolution);

// forget our old variant from above,
// or we might choose that in high-bandwidth scenarios
// (this could be the lowest bitrate rendition as we go through all of them above)
variant = null;
stableSort(bandwidthPlaylists, Hls.comparePlaylistResolution);

width = parseInt(safeGetComputedStyle(this.tech_.el(), 'width'), 10);
height = parseInt(safeGetComputedStyle(this.tech_.el(), 'height'), 10);

// iterate through the bandwidth-filtered playlists and find
// best rendition by player dimension
while (i--) {
variant = bandwidthPlaylists[i];

// ignore playlists without resolution information
if (!variant.attributes ||
!variant.attributes.RESOLUTION ||
!variant.attributes.RESOLUTION.width ||
!variant.attributes.RESOLUTION.height) {
continue;
}

// since the playlists are sorted, the first variant that has
// dimensions less than or equal to the player size is the best
let variantResolution = variant.attributes.RESOLUTION;
// filter out playlists without resolution information
haveResolution = bandwidthPlaylists.filter(function(elem) {
return elem.attributes &&
elem.attributes.RESOLUTION &&
elem.attributes.RESOLUTION.width &&
elem.attributes.RESOLUTION.height;
});

if (variantResolution.width === width &&
variantResolution.height === height) {
// if we have the exact resolution as the player use it
resolutionPlusOne = null;
resolutionBestVariant = variant;
break;
} else if (variantResolution.width < width &&
variantResolution.height < height) {
// if both dimensions are less than the player use the
// previous (next-largest) variant
break;
} else if (!resolutionPlusOne ||
(variantResolution.width < resolutionPlusOneAttribute.width &&
variantResolution.height < resolutionPlusOneAttribute.height)) {
// If we still haven't found a good match keep a
// reference to the previous variant for the next loop
// iteration

// By only saving variants if they are smaller than the
// previously saved variant, we ensure that we also pick
// the highest bandwidth variant that is just-larger-than
// the video player
resolutionPlusOne = variant;
resolutionPlusOneAttribute = resolutionPlusOne.attributes.RESOLUTION;
}
// if we have the exact resolution as the player use it
resolutionBestVariantList = haveResolution.filter(function(elem) {
return elem.attributes.RESOLUTION.width === width &&
elem.attributes.RESOLUTION.height === height;
});
// ensure that we pick the highest bandwidth variant that have exact resolution
resolutionBestVariant = resolutionBestVariantList.filter(function(elem) {
return elem.attributes.BANDWIDTH === resolutionBestVariantList[resolutionBestVariantList.length - 1].attributes.BANDWIDTH;
})[0];

// find the smallest variant that is larger than the player
// if there is no match of exact resolution
if (!resolutionBestVariant) {
resolutionPlusOneList = haveResolution.filter(function(elem) {
return elem.attributes.RESOLUTION.width > width ||
elem.attributes.RESOLUTION.height > height;
});
// find all the variants have the same smallest resolution
resolutionPlusOneSmallest = resolutionPlusOneList.filter(function(elem) {
return elem.attributes.RESOLUTION.width === resolutionPlusOneList[0].attributes.RESOLUTION.width &&
elem.attributes.RESOLUTION.height === resolutionPlusOneList[0].attributes.RESOLUTION.height;
});
// ensure that we also pick the highest bandwidth variant that
// is just-larger-than the video player
resolutionPlusOne = resolutionPlusOneSmallest.filter(function(elem) {
return elem.attributes.BANDWIDTH === resolutionPlusOneSmallest[resolutionPlusOneSmallest.length - 1].attributes.BANDWIDTH;
})[0];
}

// fallback chain of variants
Expand Down
44 changes: 44 additions & 0 deletions test/videojs-contrib-hls.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -662,6 +662,50 @@ QUnit.test('selects a playlist below the current bandwidth', function(assert) {
assert.equal(this.player.tech_.hls.stats.bandwidth, 10, 'bandwidth set above');
});

QUnit.test('selects a primary rendtion when there are multiple rendtions share same attributes', function(assert) {
let playlist;

this.player.src({
src: 'manifest/master.m3u8',
type: 'application/vnd.apple.mpegurl'
});
openMediaSource(this.player, this.clock);
standardXHRResponse(this.requests[0]);

// covers playlists with same bandwidth but different resolution and different bandwidth but same resolution
this.player.tech_.hls.playlists.master.playlists[0].attributes.BANDWIDTH = 528;
this.player.tech_.hls.playlists.master.playlists[1].attributes.BANDWIDTH = 528;
this.player.tech_.hls.playlists.master.playlists[2].attributes.BANDWIDTH = 728;
this.player.tech_.hls.playlists.master.playlists[3].attributes.BANDWIDTH = 728;

this.player.tech_.hls.bandwidth = 1000;

playlist = this.player.tech_.hls.selectPlaylist();
assert.strictEqual(playlist,
this.player.tech_.hls.playlists.master.playlists[2],
'select the rendition with largest bandwidth and just-larger-than video player');

// verify stats
assert.equal(this.player.tech_.hls.stats.bandwidth, 1000, 'bandwidth set above');

// covers playlists share same bandwidth and resolutions
this.player.tech_.hls.playlists.master.playlists[0].attributes.BANDWIDTH = 728;
this.player.tech_.hls.playlists.master.playlists[0].attributes.RESOLUTION.width = 960;
this.player.tech_.hls.playlists.master.playlists[0].attributes.RESOLUTION.height = 540;
this.player.tech_.hls.playlists.master.playlists[1].attributes.BANDWIDTH = 728;
this.player.tech_.hls.playlists.master.playlists[2].attributes.BANDWIDTH = 728;
this.player.tech_.hls.playlists.master.playlists[2].attributes.RESOLUTION.width = 960;
this.player.tech_.hls.playlists.master.playlists[2].attributes.RESOLUTION.height = 540;
this.player.tech_.hls.playlists.master.playlists[3].attributes.BANDWIDTH = 728;

this.player.tech_.hls.bandwidth = 1000;

playlist = this.player.tech_.hls.selectPlaylist();
assert.strictEqual(playlist,
this.player.tech_.hls.playlists.master.playlists[0],
'the primary rendition is selected');
});

QUnit.test('allows initial bandwidth to be provided', function(assert) {
this.player.src({
src: 'manifest/master.m3u8',
Expand Down

0 comments on commit 67ddaba

Please sign in to comment.