/*****************************************************
*
* Copyright 2010 Adobe Systems Incorporated. All Rights Reserved.
*
*****************************************************
* The contents of this file are subject to the Berkeley Software Distribution (BSD) Licence
* (the "License"); you may not use this file except in
* compliance with the License.
*
* Software distributed under the License is distributed on an "AS IS"
* basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
* License for the specific language governing rights and limitations
* under the License.
*
*
* The Initial Developer of the Original Code is Adobe Systems Incorporated.
* Portions created by Adobe Systems Incorporated are Copyright (C) 2010 Adobe Systems
* Incorporated. All Rights Reserved.
*
*****************************************************/
/**
* jQuery plugin that generate the necessary video playback mark-up.
* @param {Object} /iPad/i
*/
(function($, undefined){
/**
* Adapts the options of the video player based on the device/browser capabilities.
*/
var AdaptiveExperienceConfigurator = function(){
};
var adaptiveExperienceConfiguratorMethods = {
initialize: function(){
this.userAgent = navigator.userAgent;
this.isiPad = this.userAgent.match(/iPad/i) != null;
this.isiPhone = this.userAgent.match(/iPhone/i) != null;
this.isAndroid = this.userAgent.match(/Android/i) != null;
this.screenWidth = screen.width;
this.screenHeight = screen.width;
this.isPhone = this.screenHeight < 360;
this.isTablet = this.screenHeight >= 360 && this.screenHeight <= 768;
this.isDesktop = this.screenHeight > 768;
this.hasHTML5VideoCapability = !!document.createElement('video').canPlayType;
this.hasFlashPlayerCapability = this.detectFlashPlayerCapability();
this.flashPlayerVersion = swfobject.getFlashPlayerVersion();
},
/**
* This is exploration code - sniff user agents for detecting Flash Capability.
*
* ... exploration code, far from being complete
*/
detectFlashPlayerCapability: function(){
// iPhone & iPad don't have flash capability
var result = this.isDesktop;
result &= !(this.isiPad && this.isiPhone);
// Assume that Android 2.2 phones & tablets have flash support...
// this is slightly exagerated by, this is exploration code
result |= !this.isDesktop && (this.userAgent.match(/Android 2.2/i) != null);
return result;
},
adapt: function(options){
if (!this.userAgent) {
// Initialize lazily
this.initialize();
}
// First, extend with default values
options = $.extend({}, $.fn.strobemediaplayback.defaults, options);
this.changed = true;
var i = 0, n = this.rules.length;
while (this.changed) {
this.changed = false;
for (i = 0; i < n; i++) {
this.rules[i](this, options);
}
this.changed = false;
}
return options;
},
setOption: function(options, name, value){
if (!options.hasOwnProperty(name) || options[name] != value) {
options[name] = value;
this.changed = true;
}
},
rules: [ //playerImplementation
function(context, options){
if (options.favorFlashOverHtml5Video && context.hasFlashPlayerCapability) {
context.setOption(options, "useHTML5", false);
}
else
if (!options.favorFlashOverHtml5Video && context.hasHTML5VideoCapability) {
context.setOption(options, "useHTML5", true);
}
else
if (options.favorFlashOverHtml5Video) {
context.setOption(options, "useHTML5", !context.hasFlashPlayerCapability);
}
else {
context.setOption(options, "useHTML5", context.hasHTML5VideoCapability);
}
}, //neverUseJavaScriptControlsOnIPhone:
function(context, options){
if (context.isiPhone) {
context.setOption(options, "javascriptControls", false);
}
}, //hideVolumeControlOnIPad:
function(context, options){
if (context.isiPad) {
context.setOption(options, "disabledControls", ".volume");
}
}, // No Flash & No HTML5 Video
function(context, options){
if (!context.hasFlashPlayerCapability && !context.hasHTML5VideoCapability) {
context.setOption(options, "javascriptControls", false);
context.setOption(options, "displayAlternativeContent", true);
}
}
]
};
AdaptiveExperienceConfigurator.prototype = adaptiveExperienceConfiguratorMethods;
$.fn.adaptiveexperienceconfigurator = new AdaptiveExperienceConfigurator();
var StrobeMediaPlayback = function(element, options){
this.$element = $(element);
this.options = $.extend({}, $.fn.strobemediaplayback.defaults, options);
};
// HACK: keeps the reference to the context of the function which uses swfobject
// - needed for the swfobject.js error callback handler.
var onFlashEmbedCompleteThisReference = null;
var strobeMediaPlaybackMethods = {
initialize: function(){
if (this.options.useHTML5) {
var $video = $("<video></video>");
$video.attr("id", this.options.id);
$video.attr("class", "smp_video");
$video.attr("preload", "none");
$video.attr("width", this.options.width);
$video.attr("height", this.options.height);
$video.attr("src", this.options.src);
if (this.options.loop)
{
$video.attr("loop", "loop");
}
if (this.options.autoPlay)
{
$video.attr("autoplay", "autoplay");
}
if (this.options.controlBarMode != "none")
{
$video.attr("controls", "controls");
}
if (this.options.poster != "")
{
$video.attr("poster", this.options.poster);
}
this.$element.replaceWith($video);
}
else {
this.options.queryString = $.fn.strobemediaplayback.generateQueryString(this.options);
var flashvars = this.options;
var params = {
allowFullScreen: "true"
};
var attributes = {
id: this.options.id,
name: this.options.id
};
onFlashEmbedCompleteThisReference = this;
swfobject.embedSWF(this.options.swf,
this.$element.attr("id"),
this.options.width,
this.options.height,
this.options.minimumFlashPlayerVersion,
this.options.expressInstallSwfUrl,
flashvars, params, attributes,
this.onFlashEmbedComplete);
}
if (this.options.useHTML5 && typeof(onJavaScriptBridgeCreated) == "function") {
// TODO: reimplmenet this so that it uses options.javascriptCallbackFunction
onJavaScriptBridgeCreated(this.options.id, true);
}
},
onFlashEmbedComplete: function(event)
{
if (!event.success && $.fn.adaptiveexperienceconfigurator.hasHTML5VideoCapability)
{
onFlashEmbedCompleteThisReference.useHTML5 = true;
onFlashEmbedCompleteThisReference.initialize();
}
}
}
StrobeMediaPlayback.prototype = strobeMediaPlaybackMethods;
$.fn.strobemediaplayback = function(options){
var instances = [], i;
var result = this.each(function(){
instances.push(new StrobeMediaPlayback(this, options));
});
for (i = 0; i < instances.length; i++) {
instances[i].initialize();
}
return result;
};
/**
* Plug-in default values
*/
$.fn.strobemediaplayback.defaults = {
favorFlashOverHtml5Video: true,
swf: "StrobeMediaPlayback.swf",
javascriptCallbackFunction: "org.strobemediaplayback.triggerHandler",
minimumFlashPlayerVersion: "10.1.0",
expressInstallSwfUrl: "expressInstall.swf",
autoPlay: false,
loop: false,
controlBarMode: "docked",
poster: ""
};
/**
* Utitility method that will retrieve the page parameters from the Query String.
*/
$.fn.strobemediaplayback.parseQueryString = function(queryString){
var options = {};
var queryPairs = queryString.split('&'), queryPair, n = queryPairs.length;
for (var i = 0; i < n; i++) {
queryPair = queryPairs[i].split('=');
if (queryPair[1] == "true" || queryPair[1] == "false") {
options[queryPair[0]] = (queryPair[1] == "true");
}
else {
var number = parseFloat(queryPair[1]);
if (!isNaN(number)) {
options[queryPair[0]] = number;
}
else {
options[queryPair[0]] = queryPair[1];
}
}
}
return options;
}
/**
* Utitility method that will retrieve the page parameters from the Query String.
*/
$.fn.strobemediaplayback.generateQueryString = function(options){
var queryStrings = [];
for (var key in options) {
if (queryStrings.length > 0) {
queryStrings.push("&");
}
queryStrings.push(encodeURIComponent(key));
queryStrings.push("=");
queryStrings.push((options[key]));
}
return queryStrings.join("");
}
/**
* Custom video playback controls
*/
var writableProperties = "src preload currentTime defaultPlaybackRate playbackRate autoplay loop controls volume muted".split(" ");
var timeRangeProperties = "played seekable buffered".split(" ");
var timeRangeMethods = {
start: function(index){
return this._start[index];
},
end: function(index){
return this._end[index];
}
}
var monitorChanges = function(monitor){
var i = writableProperties.length;
while (i--) {
var propertyName = writableProperties[i];
if (monitor.cc.hasOwnProperty(propertyName) &&
monitor.videoElement.hasOwnProperty(propertyName) &&
monitor.cc[propertyName] != monitor.videoElement[propertyName]) {
var setter = "set" + propertyName.charAt(0).toUpperCase() + propertyName.substring(1);
monitor.$strobeMediaPlayback[0][setter](monitor.videoElement[propertyName]);
monitor.cc[propertyName] = monitor.videoElement[propertyName];
}
}
setTimeout(function(){
monitorChanges(monitor)
}, 500);
};
var VideoElementMonitor = function($strobeMediaPlayback){
this.$strobeMediaPlayback = $strobeMediaPlayback;
this.strobeMediaPlayback = $strobeMediaPlayback[0];
this.videoElement = {
duration: 0,
currentTime: 0,
paused: true,
muted: false
};
this.cc = {
duration: 0,
currentTime: 0,
paused: true,
muted: false
};
//
this.videoElement.play = jQuery.proxy(this.strobeMediaPlayback.play2, this.strobeMediaPlayback);
this.videoElement.pause = jQuery.proxy(this.strobeMediaPlayback.pause, this.strobeMediaPlayback);
this.videoElement.load = jQuery.proxy(this.load, this);
this.$videoElement = $(this.videoElement);
}
var isPropertyChanged = function(object, cc, propertyName)
{
return !object.hasOwnProperty(propertyName) && object[propertyName] != cc[propertyName];
}
var videoElementMonitorMethods = {
load: function(){
this.strobeMediaPlayback.setSrc(this.videoElement.src);
this.strobeMediaPlayback.load();
},
update: function(properties, events, monitor){
var propertyName;
for (propertyName in properties) {
if (jQuery.inArray("emptied", events) < 0 &&
monitor.cc.hasOwnProperty(propertyName) &&
monitor.videoElement.hasOwnProperty(propertyName) &&
(monitor.cc[propertyName] != monitor.videoElement[propertyName] && !isNaN(monitor.cc[propertyName]) && !isNaN(monitor.videoElement[propertyName]))) {
// this value changed
continue;
}
monitor.cc[propertyName] = properties[propertyName];
monitor.videoElement[propertyName] = properties[propertyName];
if (jQuery.inArray(propertyName, timeRangeProperties) >= 0) {
monitor.videoElement[propertyName].start = timeRangeMethods.start;
monitor.videoElement[propertyName].end = timeRangeMethods.end;
}
}
if (events) {
var i = events.length;
while (i--) {
monitor.$videoElement.triggerHandler(events[i]);
}
}
}
}
VideoElementMonitor.prototype = videoElementMonitorMethods;
/**
*
*/
var StrobeMediaPlaybackChrome = function(element, options){
this.$window = $(window);
this.$document = $(document);
this.$element = $(element);
this.options = $.extend({}, $.fn.strobemediaplaybackchrome.defaults, options);
};
var strobeMediaPlaybackChromeMethods = {
initialize: function(){
if (!this.options.javascriptControls) {
this.$element.find(".strobeMediaPlaybackControlBar,.smp-error,.playoverlay").hide();
return;
}
this.$player = $("#" + this.options.id)
this.player = this.$player.get(0);
// TODO: Encapsulate this in a dedicated class
if (!this.options.useHTML5) {
this.monitor = new VideoElementMonitor(this.$player);
this.$player.data("videoElement", this.monitor.$videoElement);
this.player = this.monitor.videoElement;
this.$player = this.monitor.$videoElement;
org.strobemediaplayback.proxied[this.options.id] = this.monitor;
monitorChanges(this.monitor);
}
this.sliding = false;
this.playOverlay = this.$element.find('.playoverlay');
this.play = this.$element.find('.smp.play');
this.mute = this.$element.find('.smp.volume');
this.time = this.$element.find('.time');
this.currentTimeLabel = this.$element.find('.currentTime');
this.durationLabel = this.$element.find('.duration');
this.errorOverlay = this.$element.find('.smp-error');
this.fullscreen = this.$element.find('.fullscreen');
this.play.bind('click', this, this.onPlayClick);
this.playOverlay.bind('click', this, this.onPlayClick);
this.mute.bind('click', this, this.onMuteClick);
this.fullscreen.bind('click', this, this.onFullScreenClick);
this.$player.bind("play", this, this.onPlay);
this.$player.bind("pause", this, this.onPause);
this.$player.bind("volumechange", this, this.onVolumeChange);
this.$player.bind("durationchange", this, this.onDurationChange);
this.$player.bind("timeupdate", this, this.onTimeUpdate);
this.$player.bind("waiting", this, this.onWaiting);
this.$player.bind("seeking", this, this.onSeeking);
this.$player.bind("seeked", this, this.onSeeked);
this.$player.bind("ended", this, this.onPause);
this.$player.bind("error", this, this.onError);
this.$player.bind("progress", this, this.onProgress);
this.timeTrack = this.$element.find(".video-track");
this.slider = this.$element.find(".slider");
this.played = this.$element.find(".played");
this.buffered = this.$element.find(".buffered");
this.slider.bind("mousedown", this, this.onSliderMouseDown);
this.slider.bind("touchstart", this, this.onSliderMouseDown);
// Keep here for further experimentation
//this.slider.bind("touchmove", this, this.onTouchMove);
this.timeTrack.bind('mousedown', this, this.onTimeTrackClick);
this.$window.bind("orientationchange", this, this.onOrinetationChangeOrResize);
this.$window.bind("resize", this, this.onOrinetationChangeOrResize);
if (options.disabledControls) {
this.$element.find(options.disabledControls).addClass("disabled");
}
this.isFullScreen = false;
this.layoutControlBar(this.options.width, this.options.height);
},
onSliderMouseDown: function(event){
var duration, time, player;
if (!event.data.sliding) {
event.preventDefault();
// TODO: Move the sliding code into a special widget
player = event.data.player;
event.data.sliding = true;
duration = player.duration;
event.data.onProgress(event);
var timeTrack = event.data.timeTrack;
var slider = event.data.slider;
var moveTarget = event.data.$document;
moveTarget.bind("mousemove", event.data, onMouseMove);
moveTarget.bind("touchmove", event.data, onMouseMove);
moveTarget.bind("mouseup", event.data, onMouseUp);
moveTarget.bind("touchend", event.data, onMouseUp);
moveTarget.bind("touchcancel", event.data, onTouchCancel);
}
function onMouseMove(event){
event.preventDefault();
var timeTrackWidth = event.data.timeTrack.outerWidth();
var offsetLeft = event.data.timeTrack.offset().left;
var x = event.clientX;
var originalEvent = event.originalEvent;
if (typeof x == 'undefined' && originalEvent && originalEvent.touches && originalEvent.touches.length > 0) {
x = originalEvent.touches[0].pageX;
}
var relativePosition = (x - offsetLeft) / (timeTrackWidth);
time = duration * relativePosition;
if (time < duration && time > 0) {
var timePercent = (Math.max(0, time) / duration * 100);
event.data.slider.css({
"left": timePercent + "%"
});
event.data.played.css({
"width": timePercent + "%"
});
event.data.seekTime = time;
event.data.onProgress(event);
}
};
function onMouseUp(event){
moveTarget.unbind("mousemove");
moveTarget.unbind("touchmove");
moveTarget.unbind("mouseup");
moveTarget.unbind("touchend");
if (time > 0) {
event.data.seekTime = 0;
player.currentTime = time;
}
event.data.sliding = false;
};
function onTouchCancel(event){
event.data.seekTime = 0;
event.data.sliding = false;
};
},
onOrinetationChangeOrResize: function(event){
event.data.layoutControlBar(event.data.options.width, event.data.options.height);
},
onPlayClick: function(event){
var player = event.data.player;
if (player.paused) {
player.play();
}
else {
player.pause();
}
},
onMuteClick: function(event){
var player = event.data.player;
player.muted = !player.muted;
},
onFullScreenClick: function(event){
event.data.$element.parent().toggleClass("fullscreen-mode");
},
onTimeTrackClick: function(event){
var duration = event.data.player.duration;
var timeTrackWidth = event.data.timeTrack.outerWidth();
var offsetLeft = event.data.timeTrack.offset().left;
var relativePosition = (event.clientX - offsetLeft) / (timeTrackWidth);
var time = duration * relativePosition;
if (time > 0) {
event.data.player.currentTime = time;
}
$("#seekDebug").html("clientX=" + event.clientX + " width=" + timeTrackWidth + " duration=" + duration + " time=" + time);
},
onPlay: function(event){
event.data.errorOverlay.hide();
event.data.play.removeClass("play").addClass("pause");
if (event.data.useHTML5 &&
event.data.options.hasOwnProperty("playButtonOverlay") &&
event.data.options.playButtonOverlay) {
event.data.playOverlay.fadeOut(600);
}
},
onPause: function(event){
event.data.play.removeClass("pause").addClass("play");
if (event.data.useHTML5 && event.data.options.playButtonOverlay) {
event.data.playOverlay.fadeIn(600);
}
},
onWaiting: function(event){
// $("#debug").append("BUFFERING");
event.data.buffered.css({
"width": 0
});
},
onError: function(event){
//$("#debug").append("ERROR" + event.data.player.error.code);
if (event.data.useHTML5) {
var message;
switch (event.target.error.code) {
case event.target.error.MEDIA_ERR_ABORTED:
message = 'You aborted the video playback.';
break;
case event.target.error.MEDIA_ERR_NETWORK:
message = 'A network error caused the video download to fail part-way.';
break;
case event.target.error.MEDIA_ERR_DECODE:
message = 'The video playback was aborted due to a corruption problem or because the video used features your browser did not support.';
break;
case event.target.error.MEDIA_ERR_SRC_NOT_SUPPORTED:
message = 'The video could not be loaded, either because the server or network failed or because the format is not supported.';
break;
default:
message = 'An unknown error occurred.';
break;
}
//$("#debug").append(message);
event.data.errorOverlay.html(message);
event.data.errorOverlay.show();
}
},
onSeeking: function(event){
// $("#debug").append("SEEKING");
},
onSeeked: function(event){
event.data.onProgress(event);
},
onVolumeChange: function(event){
if (event.data.player.muted) {
event.data.mute.addClass("mute");
}
else {
event.data.mute.removeClass("mute");
}
},
onDurationChange: function(event){
var duration = event.data.player.duration;
var currentTime = event.data.player.currentTime;
var timeDuration = formatTimeStatus(currentTime, duration);
event.data.currentTimeLabel.html(timeDuration[0]);
event.data.durationLabel.html(timeDuration[1]);
},
onTimeUpdate: function(event){
if (event.data.sliding) {
return;
}
var duration = event.data.player.duration;
var currentTime = event.data.player.currentTime;
var timeDuration = formatTimeStatus(currentTime, duration);
event.data.currentTimeLabel.html(timeDuration[0]);
event.data.durationLabel.html(timeDuration[1]);
var timePercent = (Math.max(0, currentTime) / duration * 100);
event.data.slider.css({
"left": timePercent + "%"
});
event.data.played.css({
"width": timePercent + "%"
});
event.data.onProgress(event);
},
onProgress: function(event){
var bufferedPercent = 0;
if (!event.data.player.seeking) {
var buffered = event.data.player.buffered;
var time = event.data.seekTime || Math.max(0, event.data.player.currentTime);
//$("#debug").append(buffered.length + "-" + buffered.end(buffered.length - 1));
var timePercent = time / event.data.player.duration * 100;
if (buffered) {
var lastBuffered = buffered.end(buffered.length - 1);
bufferedPercent = (lastBuffered / event.data.player.duration) * 100;
bufferedPercent -= timePercent;
//$("#debug").append(bufferedPercent);
}
if (timePercent + bufferedPercent > 100) {
bufferedPercent = 100 - timePercent;
}
}
var css = {
"left": timePercent + "%",
"width": bufferedPercent + "%"
}
if (bufferedPercent + timePercent > 99) {
event.data.buffered.addClass("done")
}
else {
event.data.buffered.removeClass("done")
}
event.data.buffered.css(css);
},
layoutControlBar: function(newWidth, newHeight){
if (this.useHTML5 && this.options.playButtonOverlay) {
this.playOverlay.fadeIn(600);
this.playOverlay.css({
"left": (newWidth / 2 - this.playOverlay.width() / 2) + "px",
"top": (newHeight / 2 - this.playOverlay.height() / 2) + "px"
});
}
$('.video-progress2').css({
"width": (newWidth - 200) + "px"
});
//$('.strobeMediaPlaybackControlBar').fadeIn(600);
$('.strobeMediaPlaybackControlBar').css({
"width": newWidth - 6 + "px"
});
}
}
StrobeMediaPlaybackChrome.prototype = strobeMediaPlaybackChromeMethods;
/**
* jQuery plugin hook
*/
$.fn.strobemediaplaybackchrome = function(options){
var instances = [], i;
var result = this.each(function(){
instances.push(new StrobeMediaPlaybackChrome(this, options));
});
for (i = 0; i < instances.length; i++) {
instances[i].initialize();
}
return result;
};
/**
* jQuery plugin defaults
*/
$.fn.strobemediaplaybackchrome.defaults = {
javascriptControls: false
};
// Internals, private functions
function onMouseMove(event){
showControlBar();
}
function formatTimeStatus(currentPosition, totalDuration){
var h;
var m;
var s;
function prettyPrintSeconds(seconds, leadingMinutes, leadingHours){
seconds = Math.floor(isNaN(seconds) ? 0 : Math.max(0, seconds));
h = Math.floor(seconds / 3600);
m = Math.floor(seconds % 3600 / 60);
s = seconds % 60;
return ((h > 0 || leadingHours) ? (h + ":") : "") +
(((h > 0 || leadingMinutes) && m < 10) ? "0" : "") +
m +
":" +
(s < 10 ? "0" : "") +
s;
}
var totalDurationString = prettyPrintSeconds(totalDuration);
var currentPositionString = prettyPrintSeconds(currentPosition, h > 0 || m > 9, h > 0);
return [currentPositionString, totalDurationString];
}
})(jQuery);
/*
* Generate org.strobemediaplayback namespace - which will be used by the
* Flash/Strobe Media Playback once it is ready
*/
if (typeof org == 'undefined') {
var org = {};
}
if (typeof org.strobemediaplayback == 'undefined') {
org.strobemediaplayback = {};
}
if (typeof org.strobemediaplayback.proxied == 'undefined') {
org.strobemediaplayback.proxied = {};
}
org.strobemediaplayback.triggerHandler = function(id, eventName, updatedProperties){
if (eventName == "onJavaScriptBridgeCreated") {
if (typeof onJavaScriptBridgeCreated == "function") {
onJavaScriptBridgeCreated(id);
}
}
else {
if (typeof org.strobemediaplayback.proxied[id] != 'undefined') {
org.strobemediaplayback.proxied[id].update(updatedProperties, [eventName], org.strobemediaplayback.proxied[id]);
}
}
}