Front & Back Again, a developer's tale by Chris & Eric
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.
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.
<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.
Player Contexts - Other Contexts
AMP - IFrame
OEmbed
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
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
By convention, we always append a $ to the names of Observable instances.
Observables:
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.
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...