Tackling Video
with YUI
by Ryan Cannon
YUIConf 2012

The challenge

Goals

Examples

…and many more.

Browser Matrix

Based on market share.

Internet
Explorer
Chrome Firefox Safari Mobile
Safari
Android
Browser
Internet Explorer Chrome Firefox Safari Mobile Safari Android
7, 8 & 9 Latest Latest Latest 4.3, 5.1 & 6.0 3.2, 4.0.4 & 4.1

Playback Methods

Flash

HTML5

H.264

Testing Matrix

Flash

HTML5

Fair Warning:
YUI 3.5

Basic Architecture

nfl-video is a YUI module.

YUI Configuration

Our seed file sets up YUI to look for our our code.

var YUI_config = {
// YUI config
groups: {
nflui: {
base: "http://...",
comboBase: "http://...",
combine: true,
filter: {
searchExp: '([^\\/]+)\\/\\1-min\\.(js|css)',
replaceStr: "$1/$1.$2"
},
modules: {
// module dependency tree
}
}
}
};

Using the video player

Create an empty element on the page to render the player into.

<div id="player"></div>

Load the seed file.

<script src="http://.../min/?12345&g=nflui"></script>

Use the module like any other in YUI.

YUI().use("nfl-video", function (Y) {
// Y.NFL.Video.Player is ready
});

Configuring the player

var player = new Y.NFL.Video.Player({
configURL: "http://../config.json",
contentId: video_id,
height: 360,
width: 640,
render: "#player"
});

Using the video player

Complete code example.

<div id="player"></div>
<script src="http://.../min/?12345&g=nflui"></script>
<script>
YUI().use("nfl-video", function (Y) {
var player = new Y.NFL.Video.Player({
configURL: "http://../config.json",
contentId: video_id,
height: 360,
width: 640,
render: "#player"
});
});
</script>

Controlling the player

The player supports a few public methods:

player.loadContentId(video_id);
player.pauseVideo();

…and attributes:

player.set("continuous", true);
player.set("nextVideo", {
id: next_video_id,
thumbnail: "http://…",
headline: "…"
});

Events glue everything together

Video.PlayerEvent:

  • AD_COMPLETE
  • AD_LOADED
  • AD_START
  • AVAILABLE
  • CONFIG_LOADED
  • FULLSCREEN
  • INITIALIZED
  • PAUSE
  • PLAY
  • PLAY_NEXT
  • PLAYBACK_COMPLETE
  • POST_ROLL_START
  • PRE_ROLL_COMPLETE
  • PRE_ROLL_START
  • READY_TO_PLAY
  • RESTART
  • SEEK
  • STREAM_LENGTH
  • UNAVAILABLE
  • VOLUME_CHANGE

Three layers of configuration

Placement
via attributes on the page
Content
via a content-specifc JSON file, one for each video
Global
via a site-specific JSON file, one for each site

Every Web service call has a wrapper class in both the JavaScript and ActionScript layers

Documentation

Documenting JS with YUIDoc

/**
Loads a JSON file from the supplied URL
@method loadJSON
@param {String} url URL of the JSON file
@param {Boolean} autoplay Whether or not to begin playback immediately after loading.
@chainable
**/

Runing:

$ yuidoc -o doc/js js

Generates…

Documenting Flash with ASDoc

Documenting Flash with ASDoc

/**
 * Creates an instance of an VideoPlayer.
 *
 * @constructor
 * @param _width The width the player should be when added to the stage.
 * @param _height The height the player should be when added to the stage.
 */
public function VideoPlayer(_width:int = 640, _height:int = 360) {
  …
}

Running

$ asdoc
  -doc-sources=as/src
  -output=doc/as
  -compiler.library-path=as/swc

Generates…

Flash
…isn't going away.

Flash isn’t going away

Some things Flash can do that <video /> can’t:

Flash also has:

Why not use Flash as a polyfill?

Facebook

Open graph currently requires a self-contained SWF video player.

<meta name="og:video"
content="http://…/player.swf?…">
<meta name="og:video:type"
content="application/x-shockwave-flash">
<meta name="og:video:width" content="384">
<meta name="og:video:height" content="216">

YUI has great
Flash support
for the most part

Y.SWF (the good parts)

Y.SWF (the bad parts)

HTML <video />

HTML5Player structure

Models
that extend Y.Base that wrap JSON-based Web services
Views
a stack of views that all inherit from Y.Widget
Controller
A single attribute, state, and a whole lot of events

HTML5Player structure - Models

/**
 * URL for more videos links on the hover, error and blocked
 * states of the player.
 * @attribute moreVideosURL
 * @default "http://www.nfl.com/videos"
 * @type String
 * @writeOnce
 */
moreVideosURL: {
  value: "http://www.nfl.com/videos",
  writeOnce: true
},

HTML5Player structure - Views

The stack of sub-views all inherit from Y.Widget.

  1. video
  2. poster
  3. controls
  4. continuous
  5. error
  6. loading

HTML5Player structure - Views

THe stack of sub-views all inherit from Y.Widget.

_uiRenderPlayer: function () {
// get config settings directly from the host
var host = this.get("host"),
widgetConfig = {
width: host.get("width"),
height: host.get("height"),
render: host.get("contentBox"),
visible: false
};

// initialize widget sub-views
this._errorFrame = new Video.ErrorFrame(widgetConfig);
this._loadingFrame = new Video.LoadingFrame(widgetConfig);
this._posterFrame = new Video.PosterFrame(widgetConfig);
this._endState = new Video.PlayerEndState(widgetConfig);
}

HTML5Player structure - Views

renderUI
Create DOM elements and store references to them.
bindUI
Add event listeners.
syncUI
Set up the UI from initial values.
renderUI: function () {
var headlineContainer = Y.Node.create("<div></div>");
headlineContainer.addClass(this.getClassName("headline"));
this._headlineContainer = headlineContainer;
},
afterHeadlineChange: function (e) {
this._headlineContainer.setContent(e.newVal);
},
bindUI: function () {
this.after("headlineChange", this.afterHeadlineChange);
},
syncUI: function {
this.afterHeadlineChange({ newVal: this.get("headline") });
}

HTML5Player structure - Views

The skinnable property of loader allows us to keep CSS modular.

"nfl-video-poster": {
requires: ["base-build", "widget-base"],
skinnable: true
}

Module asset structure:

nfl-video-poster/
  assets/
    skins/
      sam/
        nfl-video-poster.css
        sprite.png
  nfl-video-poster.js

HTML5Player structure - Views

Widget's getClassName method allows for fast, semantic selectors.

.yui3-nfl-video-loading {
// …
}
.yui3-nfl-video-loading-hidden {
// …
}
.yui3-nfl-video-loading-content {
// …
}
.yui3-nfl-video-loading-messaging {
// …
}
.yui3-nfl-video-loading-spinner {
// …
}

Sub-views structured as Widgets can be tested in isolation.

HTML5Player structure - Controller

The state attribute governs the UI.

Video.VideoState = {
NONE: "none",
LOADING: "loading",
AD_PLAYING: "adplaying",
AD_PAUSED: "adpaused",
PLAYING: "playing",
PAUSED: "paused",
POSTER: "poster",
END: "end",
ERROR: "error"
};

HTML5Player structure - Controller

The state attribute governs the UI.

afterStateChange: function (e) {
// depending on what's currently showing, clean up
switch (e.prevVal) {
case VideoState.ERROR:
this._errorFrame.hide();
break;
// etc.
}
// set up what we want to show
switch (e.newVal) {
case VideoState.ERROR:
this._controls.hide();
this._errorFrame.show();
break;
// etc.
}
};

HTML5Player structure - Events

Events notify other modules that something has happened.

In Video.HTML5Player:

this._videoNode.after("playing", function () {
this.fire(Video.PlayerEvent.PLAY);
}, this);

In Video.OmnitureTrackingController:

player.after(PlayerEvent.PLAY, function () {
// track a video start
});

Aside - YUI’s Event Lifecycle

There are four ways to register an event listener:

onceAfter
called the first time the event fires and isn’t cancelled
after
called every time the event fires and isn’t cancelled
once
called the first time the event is fired
on
called every time the event is fired

Use the registration method highest on the list that matches your needs.

HTML5Player structure - Events

Leverage YUI’s custom event infrastructure to allow submodules to intercept user-initiated actions.

// publish a custom event for requesting a video to play
initializer: function () {
this.publish(HTML5VideoEvent.PLAY_REQUEST, {
defaultFn: this._defPlayRequestFn,
preventable: true
});
},
// If the event is successful, actually play the video
_defPlayRequestFn: function {
this.set(STATE, VideoState.PLAYING);
this._videoNode.play();
},
// public method only requests a play
play: function () {
this.fire(HTML5VideoEvent.PLAY_REQUEST);
}

HTML5Player structure - Events

Intercepting a play request.

player.on(HTML5VideoEvent.PLAY_REQUEST, function (e) {
if (shouldShowPreRoll) {
event.halt();
// load the pre-roll ad
}
});

Ads

Ads are pretty easy

Google’s IMA SDK is very similar to their Flash version. It’s a robust, well-documented API with great examples.

Workflow:

…except when ads are hard

Third-party dependencies

Third-party dependencies

Things to be concerned about:

Loading third-party dependencies

The standard way to load:

<script src="http://www.google.com/jsapi"></script>
<script>
google.setOnLoadCallback(onSdkLoaded);
google.load("ima", "1");

function onSdkLoaded() {
var adsLoader = new google.ima.AdsLoader();
}
</script>

Loading third-party dependencies

YUI.add("google-ima", function (Y) {

Y.loadGoogleIMA = function (google) {
// google.ima library is loaded and ready
};

});

Loading third-party dependencies

var eventName = "ima:loaded";
Y.loadGoogleIMA = function (cb) {
// set an event handler
var handle = Y.Global.once(eventName, function () {
if (Y.Lang.isFunction(cb)) {
cb(Y.config.win.google);
}
});
// load the library
// …
// return the handler
return handle;
};

Loading third-party dependencies

If it doesn’t already exist, publish a custom event on Y.Global for when the library loads with fireOnce set to true, then load the library

var eventName = "ima:loaded";
function loadIMAOnce () {
// …
}
Y.loadGoogleIMA = function (cb) {
// set an event handler
// load the library
if (!Y.Global.hasEvent(eventName)) {
Y.Global.publish(eventName, { fireOnce: true });
loadIMAOnce();
}
// return the handler
};

Loading third-party dependencies

Once the library is loaded, fire your custom event.

var eventName = "ima:loaded";
function loadIMAOnce () {
Y.Get.script("http://www.google.com/jsapi", {
onSuccess: googleLoaderReady
});
}
function googleLoaderReady() {
var google = Y.config.win.google;
google.load("ima", "1", {
callback: function () {
// the library is fully loaded
Y.Global.fire(eventName, google);
}
});
}

The YUI-based Web Font service

Simple Web font embedding

<style>
@font-face {
font-family: "NFLEndzoneSansMedium";
font-style: normal;
font-weight: normal;
src: url("//:") format("no404"),
url("assets/medium.eot?iefix") format("eot"),
url("assets/medium.woff") format("woff"),
url("assets/medium.ttf") format("truetype");
}
body {
font-family: NFLEndzoneSansMedium, sans-serif;
}
</style>

Simple Web Font embedding

Has some drawbacks.

A YUI-based web font service

Use Loader for conditionally-loaded content.

var YUI_config = {

groups: {
nflui: {

modules: {
"font-endzone-sans": { type: "css" },
"nfl-video-controls": { requires: ["font-endzone-sans", … ] }
}
}
}
};

A YUI-based web font service

For static content, call YUI.add.

<link rel="stylesheet"
href="http://…/font-endzone-sans/font-endzone-sans.css">
<script>
YUI.add("font-endzone-sans", function () {});
</script>

Adding HTML5 MediaElement support to YUI

MediaElement - Events

Add support for media events.

// Adding
Y.Node.DOM_EVENTS.playing = 1;
// enables
Y.one("video").on("playing", function (e) {
/* … */
});

…or just use the node-event-html5 module.

MediaElement - Methods

Allows you to call methods like play() directly from a Node instance.

if (!Y.Node.prototype.play) {
Y.Node.addMethod("play", function () {
// the Node instance is the first argument
var args = Y.Array(arguments),
node = args.shift();

// if the method exists, call it and return the results
if (Y.Lang.isFunction(node.play)) {
return node.play.apply(node, args);
}
});
}

We added support for play(), pause(), canPlayType(), requestFullScreen() & cancelFullScreen().

MediaElement - Full Screen

Y.Node.SUPPORTS_FULLSCREEN = Y.Array.some([
// objects with properties test, isFullScreen,
// requestFullScreen, cancelFullScreen, and event
specAPI, webKitAPI, mozAPI
], function (api) {
var supported = api.test();
if (supported) {
// add api methods to Y.Node
}
return supported;
});

Testing

Automation - YUI Test Framework

We built some awesome test pages with YTF.

Automated testing: lessons learned

Device Tips

Debugging Tools

A few things
we still have
to solve

Module name collisions

YUI({
groups: {
group1: {
conflict: {}
},
group2: {
conflict: {}
}
}
}).use("conflict", function (Y) {
// what code do I have?
});

YUI global clobbering

Potential problem when providing a YUI-based web service.

<!-- Your code uses the latest -->
<script src="http://yui.yahooapis.com/3.7.3/build/yui/yui-min.js"></script>

<!-- The implementer relies on an earlier version -->
<script src="http://yui.yahooapis.com/3.4.0/build/yui/yui-min.js"></script>

<!-- The result -->
<script>
YUI().use("router", function (Y) {
/* Y.Controller is undefined */
});
</script>

Interesting?

We're Hiring

nfl.com/jobs

Credits

Thanks.
ryancannon.com/yuiconf2012