Tip:
Highlight text to annotate it
X
[Applause]
Ryan Cannon: Well thanks for giving me a great out. [laughs]
My name's Ryan Cannon. I work for the NFL's Digital Media Group. I'm manager of Application
Development there, which means I run our frontend team that does most of the NFL.com and some
of our other sites.
This summer I was given a challenge. I was asked to join forces with the club site's
team in a separate business unit and transform the NFL's existing video experience into a
multi-site video platform. We were already pretty happy with how the Flash side of our
video player looked and worked, and we wanted to share it with some of our other properties.
In addition we wanted to rewrite our HTML5 video player to be an equivalent experience
to our Flash version, because the iPad and devices in general are becoming a bigger part
of our audience. The last thing is we wanted to expand support of the player to some new
and legacy media types that these other properties use that we didn't.
The goals of the project. We wanted to leverage what we were already doing well, combine the
resources both in terms of developers and testing of the two different business units,
and to be able to share that knowledge, to not have a single person or group as a point
of failure for such a critical project. As well as support as many of the business cases
that we could come up with for playing video on the site, as well as allow the player to
integrate with two very different content ecosystems, two separate CMSs, databases,
and everything, as well as not be tied to either group's release schedule and have a
single codebase for pushing out bug fixes and features, while still being fast and only
loading the code that we needed.
Here are some examples. I have some wonderful screenshots of websites, but since we have
a little bit of internet I figured I'd show you guys. This is the NFL.com video player,
this is the Flash version. I'm showing Dave Dameshek's Shame Report. We've got a very
minimal UI. If I fast forward to the end here you can see we've also got... Maybe we don't
have as good internet as I hoped. Well, the internet is not going to be judicious to us
today. Come on internet, let's go. Anyway.
We also serve some related content and integrate with our features such as NFL.com Rewards.
On here is our continuous play feature, taking down to play the next video on the list. In
addition, this is the HTML5 version. I can show you this is Safari with plugins turned
off so there's no Flash available. Playing the same video, the UI looks pretty much the
same. We've got some branding, we support fullscreen, volume controls and all of that
as well as the same features as far as promoting related content, continuous play, things like
that.
How did we do it? Let's fast forward through these. There are a few other business cases
that we wanted to support too, things like mid-roll ad breaks on the iPad, takeover videos
with no chrome, live and streaming audio, and Facebook embedding as well.
Looking at our analytics, we chose these browsers to target and to focus our testing on, because
we had limited resources there. The interesting thing is that most of the desktop browsers
have gone into this auto-updating model which has made it really easy to keep up to date
with testing on them, but both in terms of devices and Internet Explorer we're really
stuck with testing on every single one of those different devices.
As far as playback methods go, Flash used to be the way that all video was done. Now
new Macs are coming without Flash installed on them, the iPad doesn't support it at all,
and Android browsers it kind of depends on the device whether you get Flash pre-installed
or not. HTML5 video is actually much better supported. It's only on some of the older
versions of IE... I even believe new IE10 supports HTML5 video.
But that doesn't tell the whole story, because unless you want to transcode your video into
multiple different formats, the support looks quite a bit different. Chrome has talked about
removing support for MPEG-4 video or for H.264 video. Android you're still not sure if it's
supported, and Firefox doesn't support it at all.
We came up with a testing matrix where we would prefer Flash, for a few reasons I'll
get into in a minute, and support HTML5 on some of the other browsers where we need it
to.
Fair warning: this is done in YUI 3.5. We lag a little bit behind the cutting edge in
terms of our YUI version, and most of the solutions in here, they work for us and we're
hoping that they work for you. If not, my Twitter handle is Dav Glass.
[laughter]
Feel free to let me know.
The basic architecture of the player. nfl-video is a YUI module. The code is delivered through
a collection of static files served via Apache. PHP-based Minify compresses and concatenates
both the style sheets and JavaScript. The module is included via a seed file, just like
YUI. The main class Video.Player inherits from Y.Widget. When it's initialized, Video.Player
actually detects the capabilities of the browser, figures out whether you support HTML5 or Flash
or what to give you and based on that makes a second call to get all of the assets that
you'll need to display the rest of the video.
These are the four scenarios that we're supporting right now. Flash, obviously, and HTML5 for
different playback methods. We also have the ability to force users to upgrade their Flash
player to watch video, and native to promote viewing of our videos in some native applications
if we can't show you in certain contexts based on some agreements that we have with some
of our partners. Then Video.Player proxies its methods to the plugins.
This is our basic YUI configuration. Basically we set up our own group called nflui and point
at Minify for all of our assets, and we define our module dependency tree here as well.
For a developer that wants to implement the player then the task is relatively simple.
You create an empty div on the page, you load the seed file into the page. Those numbers
in the URL, what they do is they trigger a behavior of Minify that will actually send
far future expires headers, which allows us fairly fine grain control of when the caches
expires on these particular videos. When we force an update we can decide to manually
push all the users over to that specific new version. Those numbers are automatically generated
by our build process. Then you can use the nfl-video like any other module in YUI.
Instantiating the video player looks very similar to instantiating a widget. We leverage
most of the widget class's configuration properties, like height, width, and render, as well as
providing some configuration both in terms of global site configuration to work on multiple
websites as well as the specific video ID that the developer wants to play. We have
a few other configuration options available based on some of the business cases of the
player.
Here's the full code example of what a developer implementing the player on the page would
do.
Controlling the player. The player also supports several public methods. Here are some examples.
You can load content into the player after it's been initialized, you can pause a video
and play seek. Pretty much anything that a user could do via native controls, we can
also do via JavaScript as the page is running. We have a lot of flexibility in terms of integrating
with other applications that might be running on the page.
We also support attributes. Like this is how our continuous play works, is by developer
toggling on continuous play true or false.
Events are also a core part of what the video player does. We support a huge suite of events
that are published globally, and this tells other applications what the video player's
doing. This allows us to integrate things like rewards to give users points when they
watch a video, as well as allow the rest of the page to maybe dim when an ad plays or
things like that.
We have three layers of configuration on the player. I've talked about them a little bit
already. The placement is just attributes via on the page. We also have a content-specific
JSON file for each video that's loaded that tells us everything that we need to know about
how to play it. That includes things like the title of the video, CDN data as far as
where the actual file's located and things like that. Then each site has its own JSON
file as well that loads in data like branding, some specific business requirements around
which CDNs to use and whether we should load balance them, and things like that.
Each one of these web services is wrapped around Y.Base. We do that to create a class
locally on the player rather than just interacting directly with the JSON. We do this for one
major reason, and this is for documentation. We use YUIDoc for this player. Pretty much
all the code in the player is documented this way. This is an example of YUIDoc Dav went
over a little bit earlier.
We found this to be a great tool. Just by running a simple line of code on your script
we generate a whole website where new developers to the project can get access to what the
video player does, how it works, and have a good insight into how the code all fits
together. We found this to be a great help in getting new developers into the projects.
We have a similar application called ASDoc that we use that comes bundled with the Flash
player. It works very similar to YUIDoc. It does introspect parameters and return types
in Flash which is very nice, but it's also kind of grumpy. It will warn you for things
that the Flash compiler doesn't warn you about and fail to compile your documentation. It's
actually really slow. Using ASDoc for a little while, you'll become very, very enamored with
YUIDoc for JavaScript. But the code looks very similar and it generates a documentation
site just like Adobe does it we use for this.
Talk a little bit about Flash. I know this is a JavaScript conference but in terms of
video, Flash isn't going away any time soon. There's a couple of reasons for that. There's
a lot of things that Flash can do that HTML5 video just isn't ready for yet. You can't
protect content in HTML5 video. HTML5 can't stream to all browsers yet, there's millions
of hours of online video that's been encoded in formats that HTML5 can't play. HTML5 doesn't
yet have an event interface for embedded metadata, so if you're trying to encode data about what's
actually on the screen to your video player, it becomes very difficult to react to that.
And you can't choose a bitrate for the user based on their actual connection speed and
whether or not they're dropping frames, which we do in Flash.
Flash also has very robust control over your connection. You can find out all kinds of
quality of service information, and it has a lot of vendor support right now as well.
But it's not all bad. One thing we talked about is using Flash as a polyfill. For the
browsers that don't support it, add a small Flash layer that replaces the video element
and use that instead. But we already had a Flash player that worked, so that was one
of the main reasons we decided not to do it. The second reason not to use Flash as simply
a polyfill is Facebook. Open graph, if you want your videos to play inline in the content
stream it requires a self-contained Flash video player. You can pass it parameters,
but if you provide an HTML URL it's just going to redirect to that page. This was a valuable
thing for us.
But that's not bad because YUI has great Flash support for the most part. Y.SWF is the class
that you use to embed Flash in websites. It allows you to fire native YUI custom events
directly from ActionScript, and this is really cool. There's some great examples in the documentation
around using the YUI bridge class, and the end result is that in your JavaScript code
you can just listen for events.
As well, there's also the ability to call externalInterface methods from the Y.SWF instance
directly back into ActionScript. You can have two-way communication between your Flash movies
and your page in pretty much exactly the same way that JavaScript does. That allows us to
really hide the actual implementation of whether you're watching a Flash player or HTML5 video
player behind this event interface.
There are a couple of things that we had to work around in using Y.SWF. The main ones
are that YUI's ExpressInstall feature, which is how you have the user upgrade their Flash
player, doesn't work. There's a bug in it in the YUI ticketing system. It's been targeted
for a while. But there's a good workaround, and that's to use SWFObject's ExpressInstall
SWF. SWFObjects is another open source video Flash embedding component, and it'll do the
trick.
Then Y.SWF also doesn't have a destructor method, so if your application involves creating
and destroying a lot of Flash movies you're going to want to be careful. We wrote our
own destructor in order to handle that case.
Talking about HTML5 video now. Our HTML5Player, the plugin that renders the HTML5 video has
a model view controller interface summarized here. We extend Y.Base to wrap those JSON
web services. Our views all inherit from Y.Widget just like the main player does. Our controller
is... What I consider the controller is just a single attribute called state and a whole
lot of events and event listeners.
The models I mentioned a little bit before. They allow us to create awesome documentation,
they allow us to set some great defaults for our properties so we can have as minimal of
an API as possible. I'm not going to go too much into them, but they work great.
Our views are all widgets, and they all stacked on top of each other just using absolute positioning.
We have the poster frame that loads in the... Starting with the video, and then we have
the poster frame that loads in the image assets for that video, the actual controls that the
user can interact with, an end state, an error state, and a loading state. They inherit from
Y.Widget. Our timecodes are done with slider and they're all fully integrated with YUI.
Stack of sub-views. This is an example of just how we instantiate that. We have a single
widgetConfig. We use the width and height from the host and we all render them right
into the contentBox. We use widget's built in visibility method to show and hide those
modules. We just use CSS to actually handle the showing and the hiding.
One of the reasons why I really love widget is its render life cycle I feel makes a really
approachable structure for a developer to learn about a new module. When you call render
on a widget it calls these three methods, renderUI, bindUI, and syncUI, in a row. By
default none of them do anything, but it provides a great gateway for you to organize and structure
your code.
This is an example of how we handle that. For an example of an attribute called headline,
we render the DOM in the renderUI method, we give it a specific class name, and then
we have an event handler for headlineChange. For bindUI we set that handler and then when
syncUI runs we actually call that exact same event handler with the new value set to the
current value that the player needs to mimic. We use this pattern all throughout the player
to keep state in our views.
We also skin all of our views in CSS using the skinnable property in loader. Every view
is its own module and by setting skinnable to true and setting up our folder structure
like this, we get automatic loading of CSS files, specifically for these modules, which
allows us to keep our code modular and organized.
We generate all of these CSS files using sass. We use widget's getClassName method a lot
to keep our CSS very flat. This allows us to have minimal nesting, which keeps our selectors
nice and fast, the browser doesn't have to search up the DOM tree for different class
names, and it also prevents styling conflicts lower down on the page if we specifically
target these classes.
The one main benefit that we found to using these widget sub-views is how well they can
be tested in isolation. I've got a screenshot here, but I've got even better a live demo
here. This is our controls widget, and you can see I can show and hide. There we go.
I'm being foiled by my awesome event listeners. As I can change the attributes on this...
I can change the attributes easily and the rest of the UI will react too. I can set the
current time to 1,000 seconds and we'll get... You can see we get progress bars, sliders,
and things like that. This makes visual verification and finding CSS bugs for things like this
really easy.
The main attribute for the HTML5Player class is state. We've got some pre-defined variable
shortcuts for each of the states. State acts as basically a traffic cop for the entire
application. Whenever the stateChange event fires, we switch to the previous and new values
of the attribute and clean up or toggle on or off views that need to come up.
We use custom events in the player quite a bit, and these allow the different modules
to interact with each other on the page. When the videoNode fires a playing event, we fire
our own custom play event. This play event is the same. We use variable shortcuts for
this because it also is normalized with the Flash events as well. Interacting with the
legacy Flash event names made it easier to read and see rather than putting all these
strings in our code. But that event is then listened to by our OmnitureTrackingController
to send a tracking event to our analytic suite. We use this throughout the player as well.
One thing about YUI's custom events is that there are four ways to register events. I
think this is really important for developers working on YUI to understand, is that the
on and after phase of events, on is fired as the event is happening, on lets you prevent
the event from occurring if the event is customized as preventable. After happens after the event
has already occurred and has successfully gone through all the other on event handlers.
My recommendation is to pretty much always use after if you're actually interested in
knowing that the event happened rather than on, so that if a new feature later on has
to interact with that event and perhaps stop it then you don't have to refactor old code
to say oh, actually I only want to know if this was successful or not.
Here's an example of us leveraging this. When a user hits the play button or a developer
calls the play method on the player, instead of actually manually starting the video playback
at that point, we fire a play request event. Its default method actually then sets the
state to playing and plays the video. This way something like our advertising controller
can listen for that event, cancel it, and then show a pre-roll ad.
Let's talk a little bit about ads. Ads are generally pretty easy. We use Google's IMA
SDK. Its API is very similar to the Flash version, which is great. It's robust and well
documented, they've got some great examples. The workflow is simply you create a URL-based
ad request, you handle events fired from these IMA Ad Objects, and you pass the object to
your video player and it manages juggling sources. Then for tracking clicks on ads and
launching sites, you just put an invisible DOM element over your video and the framework
handles the rest.
Ads aren't always easy, they can be really tricky too. Because the bugs can occur at
many levels, advertising infrastructure is really leviathan. There are all sorts of places
that the code can break, in yours, in the library. We keep finding new and interesting
ways for ads to fail. Sometimes error events will get fired, sometimes an exception will
get thrown, sometimes nothing at all will happen. When you're implementing ads in an
HTML5 video player you've got to be prepared for pretty much anything that could possibly
go wrong, because odds are it will.
Debugging ads is really difficult. You can't introspect the IMA's Ad Objects. It's really
tough to figure out what's going on, so yeah, just try and plan ahead is my advice.
One other thing that we found is that on iOS when you click on an ad, the video pauses
but then you have to make sure to handle the case when the user comes back to the page.
If you've hidden your play and pause controls for ads so that users can't skip them then
they've got to have some way to get back into the video and continue playing the ad when
they come back. There's no event that iOS services for you to be able to notice that.
One other thing that broke for us was that companion ads that you put on your page, they
come in as a div filled with HTML and those can sometimes conflict with your DOM, especially
if you're using rich media ads. I recommend creating an iframe dynamically and loading
them on the page that way.
Third party dependencies, like IMA's ad framework. Last year at YUIConf I came and said that
there were two things that we still had to figure out, and the first one was working
with third party JavaScript. I've come up with a solution that works pretty well for
us now. We wanted to make sure that we didn't load the same library multiple times if there
were multiple instances of the player on the page, and we wanted to avoid race conditions
when these libraries load things asynchronously.
The way that we solved it... This is the standard way that Google's documentation suggests that
you load it, is to include a seed file, setOnLoadCallback, and then load the video and eventually this
function will get called. What we wanted to do is create a wrap around this that the function
loads Google IMA if it's necessary, and then the callback gets called once the library's
actually ready. You can call this function hundreds of times but the library will only
get loaded once.
The way that we actually built this was using YUI's event infrastructure, specifically the
Y.Global instance. Y.Global is shared between every instance of YUI that's created on the
page. What we listen for is when this IMA loaded event is fired, we call the callback
that's passed into the function. You can return event handler to detach if you no longer become
interested in it.
Then if the custom event doesn't already exist then you just publish that event and tell
it to fire once, and then new event listeners will be notified as soon as that event...
The next time a function comes through, if the library's already loaded then the new
listeners will be notified right away. Then we load the IMA library one time. This is
just an example of doing that and then firing the custom event.
All right. The second thing that we had yet to figure out was building a font service.
If you notice on our HTML5 video player we have some of NFL's custom fonts in the UI.
YUI web fonts for most use cases, for the 80 per cent, they work really well. You define
your font and you set styles, you can include multiple formats in order to support all the
browsers that you need, and then you use that font family name in your CSS.
But this approach has some drawbacks. The first one is that font files are big. Even
just a US-ASCII can get around 30ks when our files are running, and are loaded whether
the browser renders them or not. If you have a lot of different type faces, including them
in a global style sheet is going to incur a lot of costs in terms of downloading the
files. If you try to load them asynchronously, what your files will do is you'll see the
default font for that element show up and then your page might bounce around a bit and
then your font will load in, and that's no good either.
Then lastly is if your web fonts are reloaded every time they are defined, causing elements
on the page to flicker. If you've got a web font stack defined in your page and you're
happy with it, and then you have a module that's sometimes loaded on the page and that
module also defines that same font, it'll cause that file to re-download the content
to disappear and reappear. It looks kind of gross.
We're using YUI and loader as a web font service. What that means is that we define a module
called for the font, just a style sheet that has all that font definition in it. Then we
can use that module as a dependency for other ones. Our controls require this font so we
define it and then it's automatically loaded when the page loads. Then if the page itself
has a static font requirement, all we need to do is load that style sheet on the page
as well and then call YUI.add for that module. This registers that the module's loaded with
YUI and it'll only be loaded exactly one time.
This is an example of all of the font files that we're supporting. We're building this
out and potentially using it to share with our other partners and vendors as well to
help them match our brand experience when they want to.
The last thing that we need to do to really make video work with YUI is add support for
HTML5's MediaElement. MediaElement is the API for both the video and audio nodes that's
been newly defined in some of the later HTML specs. It's not natively supported by YUI.
Adding event support was actually pretty easy. There's a property of Node called DOM events,
and if you add more members to that object those events then become natively supported.
Adding this one line to the DOM events for the playing will allow YUI to listen for the
playing event fired from the video element. Since we built this, or since we did this
ourselves, we then later on saw YUI do the same thing in this Node event HTML5 module.
But there are other methods on media nodes that other nodes don't support, like play.
This is an example of how I add a play method to the node instance. All it does is proxy
whatever arguments the element receives and calls them directly on and returns the return
value. We did this for play(), pause(), canPlayType() which detects format support, and requestFullScreen()
and cancelFullScreen().
Full screen was a special case because the API for full screen hasn't been completely
defined yet. All the browsers implement it a little bit differently, but they were similar
enough that we could abstract them into a single API. We added full screen support by
defining an object with properties test, isFullScreen, requestFullScreen, and cancelFullScreen, and
the event name of the actual full screen event that the browser fires, and then we just loop
through each one of those and return the first one that that browser supports. If it finds
any support at all then supportsFullScreen is set to true, otherwise it's set to false.
All right, I'm running a little bit out of time. I'll talk real quick about testing.
We struggled with testing. We've always done just visual verification for NFL.com. We have
a QA team that is fantastic and works really hard at finding our bugs. We set out with
a goal to build an entire test suite for this video player using YUI's test framework, but
we had some trouble with it. We built some awesome test pages but our stub data was really
hard to manage. When a test passed it didn't necessarily indicate that an implementation
was correct. As the project went on, we found out testing was taking more time that it was
actually saving us.
About midway through the project, as deadline pressure mounted, we decided we needed to
change our approach. This actually made Dav's presentation this morning about Grover and
Yeti, I'm really excited about because I'm hoping this helps us do what we haven't been
able to do yet, which is get this automated testing part finished out.
All right, I'm going to skip some of this stuff. Some of these slides will be available,
so you guys can check them out for some of the stuff I skipped.
Two things I still have left to solve because I can't leave any answer completely finished.
Module name collisions. When you're defining YUI as a web service that leverages YUI, you
have to realize that if a module name is already defined you can have module name conflicts
and we don't really have a great way yet of resolving those. There's no name spacing of
groups or anything like that.
Then the second one is that if you're trying to integrate on a page that already has YUI
and you guys are both using different versions, there's really not a great way to resolve
that. This is kind of a contrived example because no one would ever write code like
this, but it shows that if you have two different seed files loaded on your page you might get
something unexpected, which can be problematic.
Then I'm just going to finish up with a shameless plug. If you find this stuff interesting,
and the problems that the NFL's trying to solve, we're hiring. We're looking for developers.
We work out of Culver City just outside of Los Angeles, and if you're interested you
can come talk to me after this presentation. These are some of the positions that we have
open on our official jobs page, and there's plenty more available too.
This is some credits of the stuff that I included, and then I'm ready to open up for questions.
You can see slides, examples, and some other things on the website for this project.
[Applause]
Audience member: We're going to open it up for questions.
Audience member: I noticed that you checked the Y.SWF object. Have you ever run into problems
with the Y.SWF detect, or if the SWF is at least Flash version of a certain amount?
Ryan: We found a version a bug. I don't know if it was a bug, but it was an incompatibility
with Flash 10.0.0 and IE. Our SWF detect logic actually has a code for it where it says if
the browser is IE then require at least Flash 10.1, otherwise force them to upgrade. But
all other browsers we support all the way back to Flash 6 to force them to upgrade.
Audience member: And then are you going to go ahead and download the 3.7.3 to get the
new IE10 guys compatible with YUI? Because as far as I know right now I think if you're
on anything less than 3.7.3 I don't think IE10 will work very well.
Ryan: Will we? Yes. [laughs]
Audience member: What does it do right now? I'm just curious. Have you tested it?
Ryan: We haven't tested it yet.
Audience member: Okay.
Audience member: Could you go over your font solution again? I missed it. I saw the set
up to the problem, but I missed what you did to solve it.
Ryan: Sure. I can back up just a little bit. The main thing we did is we defined each of
the fonts in their own style sheet. We defined them in modules in our own YUI group. What
this does is that style sheet contains no logic other than just the font face definition.
Then other modules can add that module to their requires list. When we request for NFL
video controls here it'll say oh, I need the font endzone-sans font CSS, so I'll go get
that first.
Then the second part of that is that if the actual page has static HTML elements that
always require that font then you need to embed using the standard link that same font
file. We have some tricks to actually concatenate the font files together, but it's not really
relevant to this code example. Then by calling YUI.add on the page and just passing in an
empty function, then YUI registers that module as having already been loaded and doesn't
go get it again.
Audience member: So you had trouble with it reloading?
Ryan: Right, yes. This prevents you from loading the font file again after the font has already
been loaded on the page, yeah.
Audience member: That was happening whenever there was an @font-face declaration?
Ryan: When you have the font file loaded, or the font defined statically in another
CSS file, or embedded on the page in a style block or any way that a font can get loaded,
and then a module that's conditionally on the page... Like, for example, sometimes we
throw our video player up on the home page. If the home page doesn't already have that
font on it then we have to go get it. We want to make sure that we could do that without
risking reloading the font and having all of our styles flash all over the place.
Audience member: Great, do we have any other questions? Okay, can we give one more big
hand for Ryan Cannon.
[Applause]