PLAYER SPRING '17

 

Front & Back Again, a developer's tale by Chris & Eric

Everyone WATCHES our videos.

They should.

 

They're really good.

Where and How they're being watched, is all a matter of (embed) context.

Even with the safest & most robust of precautions, embed behaviors,

in real life...

Are highly unpredictable.

When developing or gathering requirements you're never fully certain how the player is being used. To the user there is no context.

Some are very uniqorny™, they really stand out from in the crowd.

In order to classify the different scenarios, we have our player contexts aka EMBED Codes.

THE FLOW

Player Context - EMBED Codes!!!

to achieve a near complete, uniform deployment of embed code types across our O&O sites, so that maintaining the complexity & scope of our application does not grow exponentially.

 

This also helps to significantly normalize & improve the accuracy of the results of all of our data gathering tools & efforts.

We want:

Player Contexts - Inline Player

Only context where LoaderJS is requested directly.

 

Provides no quarantining or boundary of global JS window, DOM, or CSS.

 

Employed on thescene.com, Vogue & NYer home pages, Vemba (soon to be deprecated), and a handful of others.

 

Has only 2 contexts: Inline & Inline Playlist.

Player Contexts - Script Embed

Most common & reliable embed type.

 

  • Prevents conflicts with boundary of global JS window, DOM, or CSS.
  • Implementation is black boxed.
  • Has access to host page / window.
  • Can listen for events on parent page / window, resize, omniture, comscore, sparrow etc.
  • Consistently fluid in both height & width, can organically grow on comapnion:renderred & companion:removed events.
  • Has only 2 contexts: Script & Script Playlist.
<script async src="//player.cnevids.com/embedjs/51ffeed757af82ba91000001/video/57ea60cafd2e615b93000001.js"></script>

Player Contexts - IFrame Embeds

<!-- 1. IFRAME DIMENSIONS -->
<iframe width="560" height="390" allowtransparency="true" frameBorder="0" scrolling="no" src="//localhost:3000/embed/575f9795ba4aa151cd000002/5511d7c561646d557d030000" allowfullscreen webkitallowfullscreen mozallowfullscreen msallowfullscreen></iframe>

<!-- 2. IFRAME DIMENSIONS COMPANION -->
<iframe width="560" height="365" allowtransparency="true" frameBorder="0" scrolling="no" src="//localhost:3000/embed/575f9795ba4aa151cd000002/5511d7c561646d557d030000" allowfullscreen webkitallowfullscreen mozallowfullscreen msallowfullscreen></iframe>

<!-- 3. IFRAME DIMENSIONS PLAYLIST -->
<iframe width="650px" height="580" allowtransparency="true" frameBorder="0" scrolling="no" src="//player-backend-staging.cnevids.com/embed/57ea60cafd2e615b93000001/51ffeed757af82ba91000001" allowfullscreen webkitallowfullscreen mozallowfullscreen msallowfullscreen></iframe>

<!-- 4. IFRAME DIMENSIONS PLAYLIST COMPANION -->
<iframe width="650px" height="505" allowtransparency="true" frameBorder="0" scrolling="no" src="//player-backend-staging.cnevids.com/embed/57ea60cafd2e615b93000001/51ffeed757af82ba91000001" allowfullscreen webkitallowfullscreen mozallowfullscreen msallowfullscreen></iframe>

<!-- 5. IFRAME NO DIMENSIONS -->
<iframe allowtransparency="true" frameBorder="0" scrolling="no" src="//player-backend-staging.cnevids.com/embed/57ea60cafd2e615b93000001/51ffeed757af82ba91000001" allowfullscreen webkitallowfullscreen mozallowfullscreen msallowfullscreen></iframe>

<!-- 6. IFRAME NO DIMENSIONS PLAYLIST -->
<iframe allowtransparency="true" frameBorder="0" scrolling="no" src="//player-backend-staging.cnevids.com/embed/51097beb8ef9aff9f5000001/playlist/5790ee5cfd2e610dcb000002" allowfullscreen webkitallowfullscreen mozallowfullscreen msallowfullscreen></iframe>

<!-- 7. IFRAME 16:9 FLUID -->
<div style="box-sizing:content-box;display:inline-block;height:0;padding-top:56.25%;position:relative;transition:height 300ms ease-in-out;vertical-align:top;width:100%;">
  <iframe style="height:1px !important;left:0;min-height:100%;min-width:100%;position:absolute;top:0;width:1px !important;" src="//player-backend.cnevids.com/embed/52fe9541c2b4c00eb417ad38/51ffef2e57af82ba6f000002" allowtransparency="true" frameBorder="0" scrolling="no" allowfullscreen webkitallowfullscreen mozallowfullscreen msallowfullscreen></iframe>
</div>

<!-- 8. IFRAME 16:9 FLUID PLAYLIST -->
<div style="box-sizing:content-box;display:inline-block;height:190px;padding-top:56.25%;position:relative;transition:height 300ms ease-in-out;vertical-align:top;width:100%;">
  <iframe style="height:1px !important;left:0;min-height:100%;min-width:100%;position:absolute;top:0;width:1px !important;" src="//player-backend-staging.cnevids.com/embed/51097beb8ef9aff9f5000001/playlist/5790ee5cfd2e610dcb000002" allowtransparency="true" frameBorder="0" scrolling="no" allowfullscreen webkitallowfullscreen mozallowfullscreen msallowfullscreen></iframe>
</div>

<!-- 9. IFRAME 16:9 FLUID (forced companion) -->
<div style="box-sizing:content-box;display:inline-block;height:75px;padding-top:56.25%;position:relative;transition:height 300ms ease-in-out;vertical-align:top;width:100%;">
  <iframe style="height:1px !important;left:0;min-height:100%;min-width:100%;position:absolute;top:0;width:1px !important;" src="//localhost:3000/embed/57ea60cafd2e615b93000001/51ffeed757af82ba91000001" allowtransparency="true" frameBorder="0" scrolling="no" allowfullscreen webkitallowfullscreen mozallowfullscreen msallowfullscreen></iframe>
</div>

<!-- 10. IFRAME 16:9 FLUID Playlist (forced companion) -->     
<div style="box-sizing:content-box;display:inline-block;height:265px;padding-top:56.25%;position:relative;transition:height 300ms ease-in-out;vertical-align:top;width:100%;">
  <iframe style="height:1px !important;left:0;min-height:100%;min-width:100%;position:absolute;top:0;width:1px !important;" src="//localhost:3000/embed/57ea60cafd2e615b93000001/51ffeed757af82ba91000001" allowtransparency="true" frameBorder="0" scrolling="no" allowfullscreen webkitallowfullscreen mozallowfullscreen msallowfullscreen></iframe>
</div>

Player Contexts - IFrame Embeds

Least common & reliable embed type.

 

  • Most static, cannot react to or access parent / host eventrs & properties.
  • Unique codes & contexts must be generated for individual  visual combinations & situations: Hard/Absent dimensions, Fluidity, Playlist, Companion.
  • Very Easy to misuse.
  • Difficult to maintain.
  • Has more than 10 contexts.

Player Contexts - Other Contexts

AMP - IFrame

  • Good for SEO Crawlability.
  • Can be fluid, natively.
  • We should roll our own official codes & guidelines for soon.

 

OEmbed

 

  • Formulated specifically for embedding on tumblr, e.g. vogue.tumblr.com.
  • Implements specialized asynchronicity not available in other embeds / contexts.

Player Architecture

(Rx & video.js)

RxJS

RxJS is a Javascript library ideal for dealing with highly asynchronous code. 

 

It provides a way to model time, so that you can think of an event in your application as a function of other events, rather than state flags.

 

The fact that a video player is nothing but a continuous stream

of disparate events (time updates, player notifications, load requests, user interaction, to name a few) makes it a good candidate for RxJS.

 

Is for SUCKERS!!!

Reducing state in these situations is often a good thing for your sanity

export default function quartile({ percentComplete$, loadedData$ }) {
  const quartilesReached$ = Observable.merge(
    percentComplete$.filter(percent => percent >= 0.25).take(1).mapTo(1),
    percentComplete$.filter(percent => percent >= 0.5).take(1).mapTo(2),
    percentComplete$.filter(percent => percent >= 0.75).take(1).mapTo(3)
  );

  return loadedData$.switchMap(() => {
    return quartilesReached$.map(value => ({ type: 'quartile', value: value }));
  });
}
(function(vjs) {
  var quartileTracking = function() {
    var lastTime, quartileTimes, quartileWasFired;

    var player = this;
    var init = function() {
      lastTime = 0;
      quartileTimes = [0, 0, 0];
      quartileWasFired = [false,false,false];
    };
    init();

    var calculateQuartileTimes = function() {
      var duration = player.duration();
      if (duration == Infinity) {
        duration = player.player_.duration();
      }
      if (duration !== NaN && duration > 0) {
        var quartileLength = duration / 4;
        quartileTimes = [quartileLength, quartileLength * 2, quartileLength * 3];
      }
    };

    var fireQuartile = function(i) {
      quartileWasFired[i] = true;
      switch (i) {
        case 0: player.trigger('cne:quartile-1'); break;
        case 1: player.trigger('cne:quartile-2'); break;
        case 2: player.trigger('cne:quartile-3'); break;
      }
    };

    player.on('ended', function(e) {
      init();
    });

    player.on('loadedmetadata', function() {
      calculateQuartileTimes();
    });

    player.on('timeupdate', function() {
      var duration = this.duration();
      if (!duration || duration == NaN) return;

      // We don't want to fire quartiles while the user is scrubbing
      if (this.scrubbing) return;

      // In case this is the first timeupdate with a valid duration, calculate the new quartiles
      if (quartileTimes[0] == 0 || quartileTimes[0] == Infinity || duration == Infinity) {
        calculateQuartileTimes();
        return;
      }

      for (var i=0; i<3; i++) {
        if (!quartileWasFired[i] && lastTime <= quartileTimes[i] && this.currentTime() > quartileTimes[i]) {
          fireQuartile(i);
        }
      }
      lastTime = this.currentTime();
    });
  };

  vjs.plugin('quartileTracking', quartileTracking);

})(window.videojs);

Resources

Shortest RxJS crash course of all time

Key data types are Observables & Subscribers.

 

Subscribers must provide at least one of the following three methods for notifications: next(), complete(), error().

 

Subscribers opt into these notifications by passing itself to .subscribe() on an Observable instance.

Observables

Subscribers

Shortest RxJS crash course of all time (pt. 2)

By convention, we always append a $ to the names of Observable instances.

 

Observables:

  • Are always based on some kind of 'producer' (mouse click, Promise, WebSocket, video.js event...).
  • Emit zero or more events. A collection of Observable events over time is called a stream.
  • ​​Provide operators for composition.

 

Through a series of compositions, you can transform producer events into a stream of events that are meaningful to your application

 

Model-View-Intent

Nothing to do with MVC.

 

Traditional model deals with modeling data.
Our model deals with modeling events.

 

Is similar to the 'data down, actions up'

mantra of React.

PlayerFacade

Client-facing API for the player.


As the name implies, this is a middle-man designed to prevent clients from directly accessing our player.


Includes setters: play(), pause(), volume(), load(), setNextVideo()
& getters: currentTime(), currentVideo(), duration(), isPlaying(), volume() (again).


Leverages RxJS Subjects, which are a special kind of Observable that don't require a producer.

 

 

Intent Streams

Creates streams out of user interactions with our player's components.


Combines these with streams from playerFacade to generate streams that represent a request to do something with our player.

 

  • Play.
  • Open Share Tools.
  • Load Video.
  • ...

 

Other Inputs

playerEvent$ creates streams out of DOM events coming from our player (these could be either from a video or ad).


configStreams creates single-event streams that come either from the initial player response (playerConfig$, environment$) or miscellaneous set-up (e.g. user agent checking).
 

windowEvents creates streams off of, well, events happening on the window.

Model

Contains the bulk of our business logic.

 

 

Leverages Rx to turn the various input streams into streams that make sense for our player and analytics.

  return applyDispose({
    adClickThrough$,
    companionAdShowing$,
    adBlocked$,
    adComplete$,
    adCountdown$,
    adDisplayAdElsToCleanup$,
    adDisplaying$,
    adDuration$,
    adFetch$,
    adFetchResult$,
    adFirstQuartile$,
    adMidPoint$,
    adOverlay$,
    adOverlayComplete$,
    adPreventSeek$: adPreventSeek(playerEvent$, adDisplaying$),
    adResponseErrors$,
    adSkipButtonViewable$,
    adStarted$,
    adThirdQuartile$,
    analyticsMute$,
    analyticsVolume$,
    animationInterval360$,
    autoplay$,
    closeMessage$,
    companionAd$,
    cne360LatLong$,
    cne360MouseActive$,
    cne360MouseDrag$,
    cneError$,
    cneOverlay$,
    cneWaiting$,
    beaconSource$,
    currentSource$: currentPlayableSource$,
    currentTimeInMs$: secondsToMs(currentTime$),
    currentVideo$,
    dispose$,
    enterFullScreen$,
    environment$,
    exitFullScreen$,
    hasStarted$,
    inSyncWithLiveStream$,
    infinityId$,
    intentSubject,
    is360Supported$,
    isLiveVideo$,
    isMobile$,
    isPlaying$,
    isSeeking$,
    loadVideoId$,
    mute$,
    muteToggle$,
    nonLinearAd$,
    nonLinearAdShouldHide$,
    onReadyCallback$,
    pause$,
    percentComplete$: contentVideoEvent$.withType('percentComplete').pluck('value'),
    play$: mergedPlay$,
    playerConfig$,
    playerEvent$,
    quartile$: contentVideoEvent$.withType('quartile').pluck('value'),
    replay$: replay(currentVideo$, contentVideoEvent$.withType('ended'), contentVideoEvent$.withType('play')),
    save$,
    scrubberThumb$,
    seekLatest$,
    setAutoplayClass$,
    setPoster$,
    setTime$: msToSeconds(setTimeIntent$),
    shareVideo$: shareVideo({ currentVideo$, shareIntent$ }),
    showLiveEndCard$,
    sponsoredBannerAd$,
    ssl$: environment$.pluck('ssl'),
    target$,
    timeTillSkippable$,
    update360Canvas$,
    updateMessageOverlay$,
    videoStart$,
    videoStartType$,
    videoView$,
    contentVideoEvent$,
    vpaidSubject,
    vpaidTimeout$: vpaidTimeout({ isPlaying$, vpaidSubject }),
    volume$,
    widthHeight$
  });

View

Where the subscriptions happen.


The view has access to the player and its components, and performs updates


 

We try to err away from having logic in the view, though there are many exceptions to this at the moment ¯\_(ツ)_/¯

  seekLatest$.subscribe((isPlaying) => {
    const seekable = player.seekable();
    if (seekable.length) {
      player.currentTime(seekable.end(0));
      if (!isPlaying) {
        // TODO: work this into the play$.
        // Will need to take seekable into account.
        player.play();
      }
    }
  });

Sinks

Where the numbers happen.

 

Entry point for analytics and custom emitted events that do not directly affect our player.

 

Like the view, each of these sinks calls .subscribe() on some Observable to recieve custom events.

 

Each sink currently provides its own logic to transform its input streams, though ideally we would combine this all into one.

VideoJS

Where the magic happens?

 

Open source video player with support for

plugins & add-ons.


Provides a way to create

custom techs & source handlers.

VideoJS Plugins

We don't use them.

VideoJS add-ons

 

Only one currently in use is videojs-contrib-hls.

(that may change with DFP)

 

Our hls add-on usage is pretty basic, but it does provide an API for customizing the video quality if need be.


We this gives us an HLS source-handler for free, which means we don't need to write our own techs

 

VideoJS Techs

That said, we wrote our own techs :)


Keeping this high level this because we'll be throwing it all out for DFP (sorry Scott) :(


For flash sources(.swf) we wrote a tech from the ground up,
for javascript sources, we extended the existing html5 tech with a custom source handler.

VideoJS components

We mainly use custom components.

(see children.js)


They provide handlers for various user interactions,

which we turn into IntentStreams,

and,

we've gone full circle...

Ad

vertisements

Player Spring '17

By Eric Wexler

Player Spring '17

  • 786