Building The Insights Video Experience

For this year’s annual JW Insights Conference, we decided to try something new. Our live streaming and post-event experiences needed to be engaging, intuitive, and elegant. Here’s how we built the live streaming experience and post-event player for JW Insights 2016.

Down to the Wire

Projects surrounding a scheduled live event can be somewhat unwieldy, especially in the few days before the event. Details and assets are often delivered at the last minute, which is why the tools you use can mean the difference between hitting the nail on the head and smashing your thumb. We had multiple versions of the Insights 2016 event page which needed to be published in quick succession, including three distinct variations: pre-event, event, and post-event. JW Player and JW Platform made building the live streaming experience simple, straightforward, and most of all, fast.

Setting Up The Live Stream Player

live-stream

We start with a player config object. This object holds the setup variables and the custom playlist values for the live stream tracks.

var streamConfig = {
  setup: {
    aspectratio: '16:9',
    autostart: 'true',
    width: '100%'
  },
  mainroom: {
    file: '//hls-tv.hblive.com/default/hblive/publishers.m3u8',
    image: '//assets-jpcust.jwpsrv.com/thumbs/3SI1aAig-1280.jpg',
    title: 'JW Insights Main Room',
    streamid: 'mainroom',
    mediaid: '3SI1aAig'
  },
  breakoutroom: {
    file: '//hls-tv.hblive.com/default/hblive/technologists.m3u8',
    image: '//assets-jpcust.jwpsrv.com/thumbs/YLBQspzA-1280.jpg',
    title: 'JW Insights Breakout Room',
    streamid: 'breakoutroom',
    mediaid: 'YLBQspzA'
  },
  error: {
    file: '//content.jwplatform.com/videos/4CfV2Dwq-YNBzQTUi.mp4',
    image: '//content.jwplatform.com/thumbs/4CfV2Dwq-1280.jpg',
    title: 'There was an issue connecting to the stream.',
    mediaid: '4CfV2Dwq',
    streamid: 'error',
    repeat: true
  }
};

(Note: this playlist format mimics that of the platform’s JSON feed, which we’ll see later on.)

After that, we simply need to set up the player:

// To keep track of the playlist track we're on, and set up the default track
var currentStream = 'mainroom';

// Combine the `setup` and `mainroom` config objects for convenience
var streamSetup = $.extend(streamConfig['setup'], streamConfig[currentStream]);

// Initialize and set up the player
var insightsPlayer = jwplayer('js-insights-player');
insightsPlayer.setup(streamSetup);

Cool. So the player gets set up, and since we combine the setup and mainroom config objects, the player defaults to the “Main Room” live stream when it loads. Next, we need a function to control the switching and loading of the two different tracks.

var playStream = function(track) {
  var isError = track === 'error';

  insightsPlayer.load(streamConfig[track]).play(true).setControls(!isError);
  
  if (!isError) {
    currentStream = track;
  }
};

This function, when given a track name, grabs the property from the streamConfig and sets it to play. You’ll also notice that if the track is error, then the player’s controls are disabled. We set currentStream to the track if it’s not an error. This is useful with some added some events:

insightsPlayer.on('error', function(event) {
  playStream('error');
});

insightsPlayer.on('complete', function(event) {
  playStream(currentStream);
});

If there’s an error, the player switches to the error file. The video is a 10-second still that displays an informational slide. Once that completes, the player tries to play the previously determined currentStream.

Now all that’s left is to add some page elements which allow viewers to swap back and forth between the two tracks. The simple solution is to add two buttons and attach events to each which call playStream. The mechanism I created to do this was tied directly to the “Event Schedule” section of the page, and involved a lot of quirky maneuvering. After gathering the dates from all of the events, it would compare that list against the current system time and place a copy of the corresponding schedule blocks underneath the video. Those blocks would then be given an onclick event which would change the video as described earlier. In hindsight, I probably should have used a time normalization library like moment.js to perform the heavy lifting needed to sync the schedule with the time. That functionality is beyond the scope of this post, but you can view the original JS module here.

Building the Event Highlights Player

Events Highlights Player

After the event, we wanted to provide the recordings of each presentation. In previous years, we created separate pages for each video. This year, we built a player with dynamic playlists using the Dashboard’s JSON endpoints.

dashboard-json-feed

I put each `playlistId` into an array so they could all be retrieved and used to generate the playlist partials. Although I would’ve liked to use ES6 Promises, we still support Internet Explorer 11, so the fastest way to get similar results was to use JQuery Deferred Objects.

var playlistIds = [ '1tFz6rOk', '0PjsB797', 'tk1HtAav', 'Xzk5aAIg' ];

var getFeedUrl = function(id) {
  // generate the playlist url for the ajax call
  return '//content.jwplatform.com/feeds/' + id + '.json';
};

var getPlaylist = function(id) {
  var def = $.Deferred(); // create a deferred object for the upcoming call

  $.ajax({
    url: getFeedUrl(id),
    dataType: 'JSON',
    cache: false,
    success: function(data) {
      def.resolve(data);
    }
  });

  return def.promise();
};

Now, we just need to loop over all playlist ids and get the data:

var playlists = [];

var getPlaylists = function() {
  var deferreds = [];

  $.each(playlistIds, function(key, val) {
    deferreds.push(getPlaylist(val));
  });

  // $.when allows us to wait until all deferred objects are resolved
  $.when.apply($, deferreds).done(function() {

    $.each(arguments, function(key, val) {
      playlists.push(val); // add each playlist blob to the playlists array
    });

    populateLists();
  });
};

The next step is to generate the HTML content with the playlist data. I used Mustache, but you can use any template engine you like. Here’s what that looks like:

<script id="js-playlist-template" type="text/template">
  <div class="playlist-switcher">
    {{ #playlists }}
    <a class="js-playlist-title playlist-title" href="#{{ feedid }}"><span class="playlist-title--label">{{ title }}</span></a>
    {{ /playlists }}
  </div>
  <div class="js-playlists playlists">
    {{ #playlists }}
    <ul class="js-playlist-list playlist-list" data-feedid="{{ feedid }}">
      {{ #playlist }}
      <li class="playlist-item">
        <a href="/insights/2016/{{ custom.slug }}/" class="js-playlist-item playlist-item--link" data-mediaid="{{ mediaid }}">
          <div class="img-container">
            <img src="{{{ image }}}" alt="{{ description }}">
          </div>
          <span class="title">{{ title }}</span>
        </a>
      </li>
      {{ /playlist }}
    </ul>
    {{ /playlists }}
  </div>
</script>

The populateLists function will take the playlist data and generate the playlists. Then, once the content is there, we can attach an event listener that sets the currentPlaylist and currentVideo, then loads the video. Then, finally, we can set up the player.

var insightsPlayer, currentPlaylist, currentVideo;

var populateLists = function() {
	var playlistTemplate = $('#js-playlist-template').html();
	var rendered = Mustache.render(playlistTemplate, { 'playlists': playlists });
	
	$('#js-playlists').append(rendered);

	// now that we have all of the playlists, let's set up the player
	setupPlayer();

	// Clicking a playlist's title will change the visible playlist in the widget
	$('.js-playlist-title').on('click', function(e) {
		var feedId = e.currentTarget.getAttribute('href');
		e.preventDefault();

		setActivePlaylist(feedId);
	});

	// set up a click event that will load the selected video and highlight it
	$('.js-playlist-list').on('click', '.js-playlist-item', function(e) {
		var mediaId = e.currentTarget.dataset.mediaid,
				feedId = e.delegateTarget.dataset.feedid;

		e.preventDefault();

		currentPlaylist = playlists.find(function(obj) {
			return obj.feedid === feedId;
		});

		currentVideo = currentPlaylist.playlist.find(function(obj) {
			return obj.mediaid === mediaId;
		});

		loadVideo(currentPlaylist, currentVideo);
		setActiveVideo(mediaId);
	});
};

The loadVideo function is pretty straightforward. It loads the current playlist into the player, then sets the playlistItem to the current video that was selected. We then make sure the correct video link is displayed or styled as active.

var loadVideo = function(currentPlaylist, currentVideo) {

	var thisPlaylistItem = currentPlaylist.playlist.indexOf(currentVideo);

	insightsPlayer.load(currentPlaylist.playlist).playlistItem(thisPlaylistItem);
	setActiveVideo(currentVideo.mediaid);
};

var setActiveVideo = function(mediaId, shouldScroll) {
	$('.js-playlist-item').each(function(i, el) {
		$(this).toggleClass('is-active', el.dataset.mediaid === mediaId);
	});

	// make the playlist element scroll the currently active video into its view
	if (shouldScroll) {
		playlistsContainer = $('.js-playlists');
		activeVideo = playlistsContainer.find('.is-active').first();
		playlistsContainer.scrollTop(playlistsContainer.scrollTop() + activeVideo.position().top);
	}
};

Did you catch where we set up the player? Now that the playlists are in place, the events are set up, and the active video and playlist links can be set, it’s time to actually add the player.

var setupPlayer = function() {
	insightsPlayer = jwplayer('js-insights-player').setup({
		width: '100%',
		aspectRatio: '16:9',
		playlist: playlists[0].playlist

	// Protip: you can chain jwplayer's functions like this
	}).on('play', function(e) {

		setActiveVideo(insightsPlayer.getPlaylistItem().mediaid, e.playReason === 'playlist');

	}).on('playlistItem', function(e) {
		// Set the heading above the video to match the currently set video's title
		$('.js-video-title').html(insightsPlayer.getPlaylistItem().title);

	}).on('ready', function() {
		
		currentPlaylist = playlists[0];
		currentVideo = currentPlaylist.playlist[0];

		setActivePlaylist();
		setActiveVideo(currentVideo.mediaid);
	});
};

I omitted the setActivePlaylist function, since all it does is switch the playlist widget that’s visible. There’s also a component in the live implementation which uses history.replaceState to change the page’s url, but I removed it here since it requires some server configurations and I have plans to expand that portion into a stand-alone topic.

If you’re a web developer and would like to see the Event Highlights Player in action, check out the JW Insights page and open Developer Tools – we use source maps so you can check out the original, uncompressed code.