Skip to content

Instantly share code, notes, and snippets.

@dmlap
Last active August 29, 2015 14:13
Show Gist options
  • Save dmlap/b70dbdbcaccc6707c531 to your computer and use it in GitHub Desktop.
Save dmlap/b70dbdbcaccc6707c531 to your computer and use it in GitHub Desktop.
Media Playlists

Media Playlists

Goals

  1. Single video plugins are compatible without needing to be playlist-aware
  2. Playlist events provide a consistent and predictable lifecycle for upstream developers

API

// load a playlist into a player
player.playlist([{
  sources: [firstMp4, firstOgv],
  poster: '//example.com/poster.jpg'
}, {
  sources: [secondMp4, secondOgv],
  textTracks: [{
    kind: 'captions'
    language: 'en'
  }]
}]);

// the current item is available
console.assert(player.playlist.item() === 0);
player.playlist.item(7); // change the current item to 7
console.assert(player.playlist[player.playlist.item()].indexOf(player.currentSrc()) >= 0);
console.log(player.playlist[1]); // the media object for playlist item 1
console.log(player.playlist.length); // print the number of items in the playlist

// playlist capabilities
// advance to the next media item in the playlist
player.playlist.next();
// return to the previous media item in the playlist
player.playlist.previous();
// advance to the next media item in the playlist as soon as the current item ends
player.playlist.autoadvance(0);
// advance to the next media item in the playlist 15 seconds after the current item ends
player.playlist.autoadvance(15);
// cancel auto-advance behavior
player.playlist.autoadvance(null);

// when the ended event fires for the last video in a playlist, playlistended fires
player.on('playlistended', function() {
  alert("We're all out of videos! Check back soon.");
});

Autoadvance

Autoadvance is off by default. If autoadvance is set to a negative number, an error is thrown. If autoadvance is set to zero, the next playlist item will be automatically loaded and begin playback when the ended event fires for the current item. If autoadvance is set to a number greater than zero, advancing the playlist and playing the next item will be delayed the specified number of seconds after the ended event. During the period between the firing of the ended event and advancing to the next playlist item, autoadvance behavior can be cancelled by calling the autoadvance method with any argument.

Ads Enhancements

One of the primary challenges with developing a playlist plugin is interacting safely with ad libraries. To simplify this process, we will prefix player events that happend during ad playback. This will ensure that plugins that do not wish to be aware of ads will see an event stream that is consistent with single video playback while still providing the notification to developers who need it.

The ended event will require special handling. It is not always possible for an ad integration to know before ended that a postroll is available and configured. That means contrib-ads may not be able to make the decision to bubble the ended event synchronously. We propose adding another timeout to the end of video playback and delaying the ended event until:

  • the ad integration has played a postroll and called endLinearAdMode()
  • the ad integration has indicated there is no postroll
  • the postroll timeout has expired

After these changes, the playlist plugin can treat playback with and without ads identically and use standard HTML5 video events to manage its state.

Timeline

Playlists should allow the creation of players that include:

  1. Zero or more pre-content segments. This can include advertisements, start slates, or bumpers for instance.
  2. A content video with zero or more interstitial segments. A mid-roll advertisement is an example of an interstitial segment.
  3. Zero or more post-content segments. Post-roll advertisements and end-slates are examples.
  4. An optional autoadvance delay.

Ad Timeline

Representation

Playlists will be accessible through a Javascript API. There will be no DOM representation.

VideoJS Playlist Plugins

@gkatsev
Copy link

gkatsev commented Jan 20, 2015

In no particular order:

  • Is this not going to be a videojs plugin?
  • I'm not a huge fan of using new.
  • There needs to be a way to specify no auto advancement.
    • I like having negative value, particularly -1, (or null value) mean no auto advancement.

@volkina
Copy link

volkina commented Jan 20, 2015

Auto-advancement should be optional. Would be nice if user can set that during playback

@dmlap
Copy link
Author

dmlap commented Jan 20, 2015

@gkatsev: I was imagining it would be a new plugin or a fork of one of the existing video.js plugins. Sorry you feel that way about new :)

@volkina @gkatsev: I was thinking autoadvance === null || autoadvance === undefined would indicate no auto-advance. I'll update the API example section with that. Let me know if you'd prefer another mechanism.

@tyard
Copy link

tyard commented Jan 21, 2015

I assume there would be something like playlist.length available? hasNext() or hasPrevious() would be convenient but obviously not necessary.

So I am clear, all those video level events would still be available during playlist playback correct? So we could listen to "ended" to display an end slate, which then could be removed when the next video started after autoadvance kicked in?

Any use case for a loop property? You could certainly listen for the playlist to end and then just set the item back to 0, but you would lose the autoadvance countdown in that case (unless you manually did that as well).

Finally, I am not clear exactly on how the player is told to play a playlist item. Does that occur on assignment of the playlist:

player.playlist = playlist;
or
player.src = playlist;

at which point whichever is set as item (defaulting to 0) is played?

Does it occur when item is set on playlist:

player.playlist.item = 7;

at which point it immediately swaps the source?

next() and previous() are clearly changing the src, but I am not clear on how the playlist is first "started."

@gkatsev
Copy link

gkatsev commented Jan 21, 2015

If it's a plugin, there's even less reason for new :P
Also, autoadvance should be cancelable.

@dmlap
Copy link
Author

dmlap commented Jan 22, 2015

@tyard:

I assume there would be something like playlist.length available? hasNext() or hasPrevious() would be convenient but obviously not necessary.

I was imagining player.playlist was an Array-like object, so it would have a length property as well. I'll update the gist with that.

So I am clear, all those video level events would still be available during playlist playback correct?

Yes, we wouldn't be changing any of the video level events.

Any use case for a loop property?

I'm sure some people would want this. The video element itself has a loop property that I thought about overloading to loop playlists when they're in use. I'm not sure if that's a good idea or not.

Finally, I am not clear exactly on how the player is told to play a playlist item.

I've updated the gist to remove the player.src = playlist option. I meant that to be an alternative to player.playlist = playlist, not another way of doing the same thing. I believe that setting the playlist would cause the first video to be loaded.

Does it occur when item is set on playlist:

Yes, I was thinking setting the item property meant "move ahead to this video in the playlist". During playback item would report the index of the current video being played.

@heff
Copy link

heff commented Jan 22, 2015

If this is meant to be a plugin it should probably look a little different

  • use a function setter/getter for player.playlist
  • pass the array of video objects to player.playlist() instead of creating an instance of a Playlist class first
// Provide the array of videos and load the first in the list
player.playlist([
  { sources: ..., poster: ... },
  { sources: ..., poster: ... }
]);

// Alternative setup through player plugin options
videojs(id, { 
  plugins: { 
    playlist: [ ... ]
  } 
});

// Get the array of videos
player.playlist();
// --> [{ sources: ..., poster: ... }, { sources: ..., poster: ... }]

// the rest of it could be the same
player.playlist.next();
player.playlist.previous();

currentItem might be more clear than item. Specifically if it's going to be used as a setter.

@dmlap
Copy link
Author

dmlap commented Jan 22, 2015

@heff: the method looks more plugin-y, the setter looks more native-y. I wish we looked more native in general so I went that route here. Either way gets the job done so if there's more support for the plugin route, I'm happy to change it. It costs very little in this case to do both, so that's a possibility as well.

I'm not a huge fan of creating new "classes" of objects (e.g. new Playlist(...)) but I felt that was more consistent with the designs the WHATWG/W3C end up with. @gkatsev seems to prefer the plain array approach as well though, so I'll update the gist.

@heff
Copy link

heff commented Jan 22, 2015

if there's more support for the plugin route

No IE8 support for Object.defineProperty on the player object, so can't easily create a native-y setter that responds to changes. e.g. I would expect player.playlist = playlist to synchronously kick off loading of the first playlist item.

I prefer native-y setters/getters too but if we're gonna switch how we do properties we should do it all at once, not one property at a time, otherwise it will create inconsistencies in the API.

@mmcc
Copy link

mmcc commented Jan 22, 2015

Say a dev is building a Netflix style interface, so they set autoadvance to something like 30 seconds. Is there a callback or transition event to indicate that the playlist is now between two items? The use case I have in mind is that the interface can then be customized to reflect things such as "coming up" or a countdown, blah blah.

@heff
Copy link

heff commented Jan 22, 2015

Should autoadvance support a time option, or should that just be true/false and the custom interface controls how much time before the next video? Turning off auto advance and calling next() when done.

@dmlap
Copy link
Author

dmlap commented Jan 22, 2015

@mmcc: yes, I think adding an event once the autoadvance delay begins is a good idea. I'll add that.

@heff: I was planning on creating a custom element for the playlist object in IE8, similar to the TextTrack solution we adopted. In this case, we would just keep the reference in memory, not insert it into the DOM.

I think being able to set an autoadvance delay without having to muck around with timeouts yourself is a handy feature. If a developer really wanted complex autoadvance behavior, they could turn off the autoadvance we provide and implement it themselves with whatever logic they want.

@dmlap
Copy link
Author

dmlap commented Jan 22, 2015

@mmcc: rethought the new event. I think ended should fire when the autoadvance delay begins and loadstart would fire once the new video was loaded. Given that, I don't think there's a need for a new event.

@heff
Copy link

heff commented Jan 22, 2015

similar to the TextTrack solution we adopted

Yeah, that makes sense, and was the right move for text tracks. I think the player API reasoning still applies. For player.playlist to be a native-y property, the player would have to be an element.

I think being able to set an autoadvance delay without having to muck around with timeouts yourself is a handy feature

Ok, that's fine with me.

@mmcc
Copy link

mmcc commented Jan 22, 2015

So just to play devil/steve's advocate here (we talked about this at lunch), what does the autoadvance delay alone really buy a dev? Thinking about the most basic use-case where someone just sets autoadvance to 15 seconds. The first video ends and the player just...sits there for 15 seconds. That would be pretty confusing UX unless there was some indicator of a timer.

Assuming we didn't have a delay, it doesn't seem like there's much work to implement it. Pseudo code:

player.playlist.on('transition', function() {
  showNeatoTransitionThingy();

  setTimeout(function() {
    player.playlist.next();
  }, 15000);
});

As I was typing that out, I started thinking we should just add a countdown somewhere to the UI and keep the delay and document this as an option. That keeps the developer ease of the default but doesn't leave the user hanging.

@gkatsev
Copy link

gkatsev commented Jan 22, 2015

Personally, I don't think we should be using getters/setters for new API we're building out that isn't trying to mimic native functionality.

@dmlap
Copy link
Author

dmlap commented Jan 23, 2015

Note to self: add a playlistended event

@apadhye
Copy link

apadhye commented Feb 9, 2015

I wish we could make auto-advance behave this way:
autoadvance(0) = plays immediately
autoadvance(positive integer) = plays with those many seconds delay
autoadvance(anything else) = autoadvance(default) = no auto advance

We can throw an error for invalid values for the publisher, so they know what's wrong, but at least this will not adversely affect the viewing experience for the end user.

Also adStart/adEnd is now adModeStart/adModeEnd

@mboles
Copy link

mboles commented Feb 26, 2015

  1. I could foresee a use case where the playlist would repeat. Could that be an option somehow on autoadvance, when the last video in the playlist plays it starts again from first video?
  2. Would it be possible to play an ad just at the beginning and/or just the end of the playlist?

@pcting
Copy link

pcting commented Feb 26, 2015

@dmlap, is there working code for this playlist proposal already? i'm in the middle of implementing a playlist plugin on a heavily modified a forked of videojs-playLists. i'm hoping to conform to this spec.

with regards to @mboles point #1, maybe a new property "loop" can be introduced for toggling between wrapping to the start/end of the playlist:

var nextIndex_ = function(i, len) {
  return Math.min(len - 1, i + 1);
};

var loopNextIndex_ = function(i, len) {
  if (i >= len - 1) {
    return 0;
  } else {
    return nextIndex_(i, len);
  }
};

var previousIndex_ = function(i, len) {
  return Math.max(0, i - 1);
};

var loopPreviousIndex_ = function(i, len) {
  if (i <= 0) {
    return len - 1;
  } else {
    return previousIndex_(i, len);
  }
};

player.playlist.next = function() {
  var func = options.loop ? loopNextIndex_ : nextIndex_;
  setItem_(func(options.index, options.items.length));
  player.trigger('next');
};

player.playlist.previous = function() {
  var func = options.loop ? loopPreviousIndex_ : previousIndex_;
  setItem_(func(options.index, options.items.length));
  player.trigger('previous');
};

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment