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

Add option to select lowest rendition available on startup #1212

Merged
merged 19 commits into from
Aug 16, 2017

Conversation

OshinKaramian
Copy link
Contributor

@OshinKaramian OshinKaramian commented Jul 26, 2017

Description

I'm still working through unit tests because many of them broke, but would like eyes on this sooner rather than later in case I went down a bizarre path.

This PR will select the lowest bitrate playlist with video on startup. If no such playlist exists, it picks the lowest bitrate playlist (assuming everything else is audo only).

This feature is gated behind an options flag. In order to enable it set options to:

{
  html5: {
    hls: {
      enableLowInitialPlaylist: true
   }
}

Requirements Checklist

  • Feature implemented / Bug fixed
  • If necessary, more likely in a feature request than a bug fix
  • Reviewed by Two Core Contributors

@@ -66,36 +67,6 @@ const objectChanged = function(a, b) {
};

/**
* Parses a codec string to retrieve the number of codecs specified,
Copy link
Contributor Author

@OshinKaramian OshinKaramian Jul 26, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function doesn't seem to be reliant on being here (there are a few other functions here that could be moved as well). I shifted this into a util module as I wanted to use this to check whether a playlist contained video or not in the playlist selection algorithm below.

return playlistsWithVideo[0];
}

return playlists[0];
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made an assumption here that may or may not be true, where the playlist are sorted by bitrate. If that's not true I can add a sort in here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You'll have to sort, no guarantee of order

@@ -328,6 +330,10 @@ class HlsHandler extends Component {
this.selectPlaylist ?
this.selectPlaylist.bind(this) : Hls.STANDARD_PLAYLIST_SELECTOR.bind(this);

this.masterPlaylistController_.initialSelectPlaylist =
this.selectPlaylist ?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After a source change, this will set the initialSelectPlaylist to be the standard selector and not the initial one. You should add another properties to the source handler for selectInitialPlaylist and then this becomes this.selectInitialPlaylist ? this.selectInitialPlaylist.bind(this) : Hls.INITIAL_PLAYLIST_SELECTOR.bind(this).

side note, by my snippet you'll see i personally prefer selectInitialPlaylist over initialSelectPlaylist, but either is descriptive and fine, so feel free to change or keep the naming. @gesinger probably has his own preference

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, I reworked things a bit (I set the selection in the master-playlist-controller) and must have mucked that up.

* exists pick the lowest audio rendition.
*/
export const lowestBitrateCompatibleVariantSelector = function() {
const { playlists } = this.playlists.master;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since sort messes with the array, you should use a copy of playslists with slice

const playlistsWithVideo =
playlists.filter(playlist => parseCodecs(playlist.attributes.CODECS).videoCodec);

const selectedPlaylists = playlistsWithVideo.length !== 0 ? playlistsWithVideo : playlists;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you could omit this and just return playlistsWithVideo[0] || playlists[0];

@@ -212,6 +212,50 @@ QUnit.test('resets SegmentLoader when seeking in flash for both in and out of bu

});

QUnit.only('selects lowest bitrate rendition when enableLowInitialPlaylist is set',
function(assert) {
this.player = createPlayer({ html5: { hls: { enableLowInitialPlaylist: true } } });
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since the initial selector is called within the loadedplaylist event handler which fires everytime a live playlist is refreshed, I think it would be worth simulating a live refresh and confirming that the initial selector is not called a second time

let numCallsToSelectPlaylist = 0;

// master
this.standardXHRResponse(this.requests.shift());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These two responses should go after the sourceopen trigger, also I would expect that the numCallsToSelectInitialPlaylistCalls should increment after just the master response

this.enableLowInitialPlaylist ?
this.selectInitialPlaylist : this.selectPlaylist;

this.initialMedia_ = initialPlaylistSelector();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can do this without needing to create the initialPlaylistSelector variable


this.clock.tick(1);

// Trigger playlist event which should utilize selectInitialPlaylist and
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this comment is a bit outdated now


// Simulate a live reload
this.masterPlaylistController.masterPlaylistLoader_.trigger('loadedplaylist');
this.clock.tick(5);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't need this tick

/**
* Chooses the appropriate media playlist, which in this case is the lowest bitrate
* one with video. If no renditions with video exist, return the lowest audio rendition.
*
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add Expects to be called within the context of an instance of HlsHandler to this. see lastBandwidthSelector jsdoc

@mjneil
Copy link
Contributor

mjneil commented Jul 28, 2017

Do you think it would be appropriate to move the other codec functions/constants within MasterPlaylistController.js into utils/codecs.js?

specifically

  • defaultCodecs
  • mapLegacyAvcCodecs_
  • makeMimeTypeString
  • getContainerType
  • getCodecs
  • mimeTypesForPlaylist_

@OshinKaramian
Copy link
Contributor Author

I do think so as I kind of started doing that then pulled back. I was trying to go with a softer touch on this PR (also it's unclear if this will end up getting folded into the greater project). I think doing that in a separate PR might make the most sense.

@OshinKaramian
Copy link
Contributor Author

@mjneil I opened up #1217 in response to your comment about moving some more generic things out of master-playlist-controller.

@@ -283,7 +254,8 @@ export class MasterPlaylistController extends videojs.EventTarget {
bandwidth,
externHls,
useCueTags,
blacklistDuration
blacklistDuration,
enableLowInitialPlaylist
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'll want this added to the README eventually

@@ -429,8 +402,17 @@ export class MasterPlaylistController extends videojs.EventTarget {
let updatedPlaylist = this.masterPlaylistLoader_.media();

if (!updatedPlaylist) {
// select the initial variant
this.initialMedia_ = this.selectPlaylist();
let selectedMedia;
Copy link
Contributor Author

@OshinKaramian OshinKaramian Aug 3, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a note here.

One of the tests was breaking on this (https://github.com/videojs/videojs-contrib-hls/blob/fast-start-time/test/videojs-contrib-hls.test.js#L1221) (I didn't have the temp variable, and instead set this.initialMedia_ twice, once in each if block).

Once I set things to the temporary variable the problem resolved itself. I'm assuming there are side effects off of setting this.initialMedia_... I didn't really dig deep enough to understand what was going on. It seems a little dangerous to have side effects off of setting a variable (when the setter isn't explicitly a function) if this is the case.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably dig into why this was happening since I don't think it should cause the test to break

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looked into this a bit more, its because if you set this.initialMedia_ and then try and load that media, but the request fails e.g. 404, when on the final rendition, we get back to here, and updatedPlaylist is still undefined because we haven't successfully loaded one yet, but initialMedia_ has been set from the previous time here. I think it would make sense to set initialMedia_ in the loadedmetadata handler if it hasnt been set yet as the first time we get into loadedmetadata is after the first media playlist has been loaded.

Copy link
Contributor Author

@OshinKaramian OshinKaramian Aug 8, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That loop seems like a code smell to me, though I can't think of a better way to deal with it in the current context.

I believe I handled the other scenario in your comment below with the latest commit. I'll look at this one as well.

@@ -1786,7 +1787,7 @@ QUnit.test('uses mobile default bandwidth if browser is Android', function(asser
openMediaSource(this.player, this.clock);

assert.equal(this.player.tech_.hls.bandwidth,
500000,
4194304,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We discussed that we were going to use the desktop bandwidth number everywhere, so this test had to be updated to account for that (I figured I would update over removing).

@@ -375,5 +375,5 @@ export const lowestBitrateCompatibleVariantSelector = function() {
const playlistsWithVideo =
playlists.filter(playlist => parseCodecs(playlist.attributes.CODECS).videoCodec);

return playlistsWithVideo[0] || playlists[0];
return playlistsWithVideo[0] || null;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instead of returning null, you could call the last bandwidth selector with the current context
return playlistsWithVideo[0] || lasBandwidthSelector.call(this); I think this would also remove the need to for the temporary selectedMedia

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If possible I'd like to avoid coupling this function to a separate algorithm, and allow the consumer to decide the alternative. I think long term adding the complexity here over elsewhere will be a bit confusing, as well as making unit testing and changes harder/more complex.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

makes sense 👍

const playlists = this.playlists.master.playlists.slice();

// Sort ascending by bitrate
stableSort(playlists,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on the findings from the 404 unit test failing (discussion in other comment thread), this selector should probably filter by isEnabled similar to the simpleSelector since this may try and re-select a playlist that just 404'd

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, I believe the latest commit should deal with that scenario.


// filter out any playlists that have been excluded due to
// incompatible configurations or playback errors
const enabledPlaylists = playlists.filter(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this could be one line const enabledPlaylists = playlists.filter(Playlist.isEnabled);

// then this takes precedence over the enableLowInitialPlaylist option
this.options_.enableLowInitialPlaylist =
(this.options_.enableLowInitialPlaylist &&
this.options_.bandwidth === INITIAL_BANDWIDTH) || false;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the || false is not necessary a && b will just be the value of b when a == true

'Selected lowest compatible playlist with video assets');
});

test('lowestBitrateCompatibleVariantSelector picks lowest audio rendition if no video exists',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The description is no longer accurate (now returns null) Same goes for the assertion description below

* exists pick the lowest audio rendition.
*/
export const lowestBitrateCompatibleVariantSelector = function() {
const playlists = this.playlists.master.playlists.slice();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could replace this slice call with the enabled filter, then you wouldn't need to do it later or have the local const

@OshinKaramian OshinKaramian changed the title [HOLD ON MERGE] Select lowest rendition available on startup Select lowest rendition available on startup Aug 16, 2017
@OshinKaramian OshinKaramian changed the title Select lowest rendition available on startup Add option to select lowest rendition available on startup Aug 16, 2017
@mjneil mjneil merged commit 2d9b3ad into master Aug 16, 2017
@forbesjo forbesjo deleted the fast-start-time branch July 2, 2018 19:11
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants