/**
* ipad.js 3.2.2. The Flowplayer ipad/iphone fallback.
*
* Copyright 2010, 2011 Flowplayer Oy
* By Thomas Dubois <thomas@flowplayer.org>
*
* This file is part of Flowplayer.
*
* Flowplayer is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Flowplayer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Flowplayer. If not, see <http://www.gnu.org/licenses/>.
*
* Date: 2011-01-10 07:50:57 -0500 (Mon, 10 Jan 2011)
* Revision: 4901
*/
$f.addPlugin("ipad", function(options) {
var STATE_UNLOADED = -1;
var STATE_LOADED = 0;
var STATE_UNSTARTED = 1;
var STATE_BUFFERING = 2;
var STATE_PLAYING = 3;
var STATE_PAUSED = 4;
var STATE_ENDED = 5;
var self = this;
var currentVolume = 1;
var onStartFired = false;
var stopping = false;
var playAfterSeek = false;
var activeIndex = 0;
var activePlaylist = [];
var clipDefaults = {
accelerated: false, // unused
autoBuffering: false,
autoPlay: true,
baseUrl: null,
bufferLength: 3, // unused
connectionProvider: null, // unused
cuepointMultiplier: 1000,
cuepoints: [],
controls: {}, // unused
duration: 0, // not yet implemented
extension: '',
fadeInSpeed: 1000, // not yet implemented
fadeOutSpeed: 1000, // not yet implemented
image: false, // unused
linkUrl: null, // not yet implemented
linkWindow: '_self', // not yet implemented
live: false, // unused
metaData: {},
originalUrl: null,
position: 0, // unused
playlist: [], // unused
provider: 'http',
scaling: 'scale', // not yet implemented
seekableOnBegin: false, // unused
start: 0, // not yet implemented
url: null,
urlResolvers: [] // unused
};
var currentState = STATE_UNLOADED;
var previousState= STATE_UNLOADED;
var isiDevice = /iPad|iPhone|iPod/i.test(navigator.userAgent);
var video = null;
function extend(to, from, includeFuncs) {
if (from) {
for (key in from) {
if (key) {
if ( from[key] && typeof from[key] == "function" && ! includeFuncs )
continue;
if ( from[key] && typeof from[key] == "object" && from[key].length == undefined) {
var cp = {};
extend(cp, from[key]);
to[key] = cp;
} else {
to[key] = from[key];
}
}
}
}
return to;
}
var opts = {
simulateiDevice: false,
controlsSizeRatio: 1.5,
controls: true,
debug: false,
validExtensions: /mov|m4v|mp4|avi/gi
};
extend(opts, options);
// some util funcs
function log() {
if ( opts.debug ) {
if ( isiDevice ) {
var str = [].splice.call(arguments,0).join(', ');
console.log.apply(console, [str]);
} else {
console.log.apply(console, arguments);
}
}
}
function stateDescription(state) {
switch(state) {
case -1: return "UNLOADED";
case 0: return "LOADED";
case 1: return "UNSTARTED";
case 2: return "BUFFERING";
case 3: return "PLAYING";
case 4: return "PAUSED";
case 5: return "ENDED";
}
return "UNKOWN";
}
function actionAllowed(eventName) {
var ret = $f.fireEvent(self.id(), "onBefore"+eventName, activeIndex);
return ret !== false;
}
function stopEvent(e) {
e.stopPropagation();
e.preventDefault();
return false;
}
function setState(state, force) {
if ( currentState == STATE_UNLOADED && ! force )
return;
previousState = currentState;
currentState = state;
stopPlayTimeTracker();
if ( state == STATE_PLAYING )
startPlayTimeTracker();
log(stateDescription(state));
}
function resetState() {
video.fp_stop();
onStartFired = false;
stopping = false;
playAfterSeek= false;
// call twice so previous state is unstarted too
setState(STATE_UNSTARTED);
setState(STATE_UNSTARTED);
}
/* PLAY TIME TRACKING */
var _playTimeTracker = null;
function startPlayTimeTracker() {
if ( _playTimeTracker )
return;
console.log("starting tracker");
_playTimeTracker = setInterval(onTimeTracked, 100);
onTimeTracked();
}
function stopPlayTimeTracker() {
clearInterval(_playTimeTracker);
_playTimeTracker = null;
}
function onTimeTracked() {
// cue points handling
var currentTime = Math.floor(video.fp_getTime() * 10) * 100;
var duration = Math.floor(video.duration * 10) * 100;
var fireTime = (new Date()).time;
// find nearest cuepoint and fire it. Should be moved to avoid closure compilation each time
function fireCuePointsIfNeeded(time, cues) {
time = time >= 0 ? time : duration - Math.abs(time);
for ( var i = 0; i < cues.length; i++ ) {
// if cue point fired in the future, reset it
if ( cues[i].lastTimeFired > fireTime ) {
cues[i].lastTimeFired = -1;
} else if ( cues[i].lastTimeFired + 500 > fireTime ) { // cuepoint was fired less that 500 ms ago, don't do anything
continue;
} else {
if ( time == currentTime || // we got the right tick
(currentTime - 500 < time && currentTime > time) ) { // we missed one
cues[i].lastTimeFired = fireTime;
$f.fireEvent(self.id(), 'onCuepoint', activeIndex, cues[i].fnId, cues[i].parameters);
}
}
}
}
$f.each(self.getCommonClip()._cuepoints , fireCuePointsIfNeeded);
$f.each(activePlaylist[activeIndex]._cuepoints, fireCuePointsIfNeeded);
}
function replay() {
resetState();
playAfterSeek = true;
video.fp_seek(0);
}
function scaleVideo(clip) {
}
// internal func, maps flowplayer's API
function addAPI() {
function fixClip(clip) {
var extendedClip = {};
extend(extendedClip, clipDefaults);
extend(extendedClip, self.getCommonClip());
extend(extendedClip, clip);
if ( extendedClip.ipadUrl )
url = decodeURIComponent(extendedClip.ipadUrl);
else if ( extendedClip.url )
url = extendedClip.url;
if ( url && url.indexOf('://') == -1 && extendedClip.baseUrl )
url = extendedClip.baseUrl + '/' + url;
extendedClip.originalUrl = extendedClip.url;
extendedClip.completeUrl = url + "/" + extendedClip.url + "/playlist.m3u8";
extendedClip.extension = extendedClip.completeUrl.substr(extendedClip.originalUrl.lastIndexOf('.'));
extendedClip.type = 'video';
// remove this
delete extendedClip.index;
log("fixed clip", extendedClip);
return extendedClip;
}
video.fp_play = function(clip, inStream, /* private one, handy for playlists */ forcePlay) {
var url = null;
var autoBuffering = true;
var autoPlay = true;
log("Calling play() " + clip, clip);
if ( inStream ) {
log("ERROR: inStream clips not yet supported");
return;
}
// we got a param :
// array, index, clip obj, url
if ( clip !== undefined ) {
// simply change the index
if ( typeof clip == "number" ) {
if ( activeIndex >= activePlaylist.length )
return;
activeIndex = clip;
clip = activePlaylist[activeIndex];
} else {
// String
if ( typeof clip == "string" ) {
clip = {
url: clip
};
}
// replace playlist
video.fp_setPlaylist(clip.length !== undefined ? clip : [clip]);
}
if ( ! opts.validExtensions.test(activePlaylist[activeIndex].extension)) {
if ( activePlaylist.length > 1 && activeIndex < (activePlaylist.length - 1) ) {
// not the last clip in the playlist
log("Not last clip in the playlist, moving to next one");
video.fp_play(++activeIndex, false, true);
}
return;
}
clip = activePlaylist[activeIndex];
url = clip.completeUrl;
if ( clip.autoBuffering !== undefined && clip.autoBuffering === false )
autoBuffering = false;
if ( clip.autoPlay === undefined || clip.autoPlay === true || forcePlay === true ) {
autoBuffering = true;
autoPlay = true;
} else {
autoPlay = false;
}
} else {
log("clip was not given, simply calling video.play, if not already buffering");
// clip was not given, simply calling play
if ( currentState != STATE_BUFFERING )
video.play();
return;
}
log("about to play "+ url, autoBuffering, autoPlay);
// we have a new clip to play
resetState();
if ( url ) {
log("Changing SRC attribute"+ url);
video.setAttribute('src', url);
}
//return;
// autoBuffering is true or we just called play
if ( autoBuffering ) {
if ( ! actionAllowed('Begin') )
return false;
$f.fireEvent(self.id(), 'onBegin', activeIndex);
log("calling video.load()");
video.load();
}
// auto
if ( autoPlay ) {
log("calling video.play()");
video.play();
}
}
video.fp_pause = function() {
log("pause called");
if ( ! actionAllowed('Pause') )
return false;
video.pause();
};
video.fp_resume = function() {
log("resume called");
if ( ! actionAllowed('Resume') )
return false;
video.play();
};
video.fp_stop = function() {
log("stop called");
if ( ! actionAllowed('Stop') )
return false;
stopping = true;
video.pause();
try {
video.currentTime = 0;
} catch(ignored) {}
};
video.fp_seek = function(position) {
log("seek called "+ position);
if ( ! actionAllowed('Seek') )
return false;
var seconds = 0;
var position = position + "";
if ( position.charAt(position.length-1) == '%' ) {
var percentage = parseInt(position.substr(0, position.length-1)) / 100;
var duration = video.duration;
seconds = duration * percentage;
} else {
seconds = position;
}
try {
video.currentTime = seconds;
} catch(e) {
log("Wrong seek time");
}
};
video.fp_getTime = function() {
// log("getTime called");
return video.currentTime;
};
video.fp_mute = function() {
log("mute called");
if ( ! actionAllowed('Mute') )
return false;
currentVolume = video.volume;
video.volume = 0;
};
video.fp_unmute = function() {
if ( ! actionAllowed('Unmute') )
return false;
video.volume = currentVolume;
};
video.fp_getVolume = function() {
return video.volume * 100;
};
video.fp_setVolume = function(volume) {
if ( ! actionAllowed('Volume') )
return false;
video.volume = volume / 100;
};
video.fp_toggle = function() {
log('toggle called');
if ( self.getState() == STATE_ENDED ) {
replay();
return;
}
if ( video.paused )
video.fp_play();
else
video.fp_pause();
};
video.fp_isPaused = function() {
return video.paused;
};
video.fp_isPlaying = function() {
return ! video.paused;
};
video.fp_getPlugin = function(name) {
if ( name == 'canvas' || name == 'controls' ) {
var config = self.getConfig();
//log("looking for config for "+ name, config);
return config['plugins'] && config['plugins'][name] ? config['plugins'][name] : null;
}
log("ERROR: no support for "+ name +" plugin on iDevices");
return null;
};
/*
video.fp_css = function(name, css) {
if ( self.plugins[name] && self.plugins[name]._api &&
self.plugins[name]['_api'] && self.plugins[name]['_api']['css'] &&
self.plugins[name]['_api']['css'] instanceof Function )
return self.plugins[name]['_api']['css']();
return self;
}*/
video.fp_close = function() {
setState(STATE_UNLOADED);
video.parentNode.removeChild(video);
video = null;
};
video.fp_getStatus = function() {
var bufferStart = 0;
var bufferEnd = 0;
try {
bufferStart = video.buffered.start();
bufferEnd = video.buffered.end();
} catch(ignored) {}
return {
bufferStart: bufferStart,
bufferEnd: bufferEnd,
state: currentState,
time: video.fp_getTime(),
muted: video.muted,
volume: video.fp_getVolume()
};
};
video.fp_getState = function() {
return currentState;
};
video.fp_startBuffering = function() {
if ( currentState == STATE_UNSTARTED )
video.load();
};
video.fp_setPlaylist = function(playlist) {
log("Setting playlist");
activeIndex = 0;
for ( var i = 0; i < playlist.length; i++ )
playlist[i] = fixClip(playlist[i]);
activePlaylist = playlist;
// keep flowplayer.js in sync
$f.fireEvent(self.id(), 'onPlaylistReplace', playlist);
};
video.fp_addClip = function(clip, index) {
clip = fixClip(clip);
activePlaylist.splice(index, 0, clip);
// keep flowplayer.js in sync
$f.fireEvent(self.id(), 'onClipAdd', clip, index);
};
video.fp_updateClip = function(clip, index) {
extend(activePlaylist[index], clip);
return activePlaylist[index];
};
video.fp_getVersion = function() {
return '3.2.3';
}
video.fp_isFullscreen = function() {
return false; //video.webkitDisplayingFullscreen;
}
video.fp_toggleFullscreen = function() {
if ( video.fp_isFullscreen() )
video.webkitExitFullscreen();
else
video.webkitEnterFullscreen();
}
video.fp_addCuepoints = function(points, index, fnId) {
var clip = index == -1 ? self.getCommonClip() : activePlaylist[index];
clip._cuepoints = clip._cuepoints || {};
points = points instanceof Array ? points : [points];
for ( var i = 0; i < points.length; i++ ) {
var time = typeof points[i] == "object" ? (points[i]['time'] || null) : points[i];
if ( time == null ) continue;
time = Math.floor(time / 100) * 100;
var parameters = time;
if ( typeof points[i] == "object" ) {
parameters = extend({}, points[i], false);
if ( parameters['time'] != undefined ) delete parameters['time'];
if ( parameters['parameters'] != undefined ) {
extend(parameters, parameters['parameters'], false);
delete parameters['parameters'];
}
}
clip._cuepoints[time] = clip._cuepoints[time] || [];
clip._cuepoints[time].push({fnId: fnId, lastTimeFired: -1, parameters: parameters});
}
}
// install all other core API with dummy function
// core API methods
$f.each(("toggleFullscreen,stopBuffering,reset,playFeed,setKeyboardShortcutsEnabled,isKeyboardShortcutsEnabled,css,animate,showPlugin,hidePlugin,togglePlugin,fadeTo,invoke,loadPlugin").split(","),
function() {
var name = this;
video["fp_"+name] = function() {
log("ERROR: unsupported API on iDevices "+ name);
return false;
};
}
);
}
// Internal func, maps Flowplayer's events
function addListeners() {
/* CLIP EVENTS MAPPING */
var events = [ 'abort',
'canplay',
'canplaythrough',
'durationchange',
'emptied',
'ended',
'error',
'loadeddata',
'loadedmetadata',
'loadstart',
'pause',
'play',
'playing',
'progress',
'ratechange',
'seeked',
'seeking',
'stalled',
'suspend',
// 'timeupdate',
'volumechange',
'waiting'];
var eventsLogger = function(e) {
log("Got event "+ e.type, e);
}
for ( var i = 0; i < events.length; i++ )
video.addEventListener(events[i], eventsLogger, false);
var onBufferEmpty = function(e) {
log("got onBufferEmpty event "+e.type)
setState(STATE_BUFFERING);
$f.fireEvent(self.id(), 'onBufferEmpty', activeIndex);
};
video.addEventListener('emptied', onBufferEmpty, false);
video.addEventListener('waiting', onBufferEmpty, false);
var onBufferFull = function(e) {
if ( previousState == STATE_UNSTARTED || previousState == STATE_BUFFERING ) {
// wait for play event, nothing to do
} else {
log("Restoring old state "+ stateDescription(previousState));
setState(previousState);
}
$f.fireEvent(self.id(), 'onBufferFull', activeIndex);
};
video.addEventListener('canplay', onBufferFull, false);
video.addEventListener('canplaythrough', onBufferFull, false);
var onMetaData = function(e) {
// update clip
video.fp_updateClip({duration: video.duration, metaData: {duration: video.duration}}, activeIndex);
activePlaylist[activeIndex].duration = video.duration;
$f.fireEvent(self.id(), 'onMetaData', activeIndex, activePlaylist[activeIndex]);
};
video.addEventListener('loadedmetadata', onMetaData, false);
video.addEventListener('durationchange', onMetaData, false);
var onStart = function(e) {
if ( currentState == STATE_PAUSED ) {
if ( ! actionAllowed('Resume') ) {
// user initiated resume
log("Resume disallowed, pausing");
video.fp_pause();
return stopEvent(e);
}
$f.fireEvent(self.id(), 'onResume', activeIndex);
}
setState(STATE_PLAYING);
if ( ! onStartFired ) {
onStartFired = true;
$f.fireEvent(self.id(), 'onStart', activeIndex);
}
};
video.addEventListener('playing', onStart, false);
var onFinish = function(e) {
if ( ! actionAllowed('Finish') ) {
if ( activePlaylist.length == 1 ) {
//In the case of a single clip, the player will start from the beginning of the clip.
log("Active playlist only has one clip, onBeforeFinish returned false. Replaying");
replay();
} else if ( activeIndex != (activePlaylist.length -1) ) {
// In the case of an ordinary clip in a playlist, the "Play again" button will appear.
// oops, we don't have any play again button yet :)
// simply go to the beginning of the video
log("Not the last clip in the playlist, but onBeforeFinish returned false. Returning to the beginning of current clip");
video.fp_seek(0);
} else {
//In the case of the final clip in a playlist, the player will start from the beginning of the playlist.
log("Last clip in playlist, but onBeforeFinish returned false, start again from the beginning");
video.fp_play(0);
}
return stopEvent(e);
} // action was canceled
setState(STATE_ENDED);
$f.fireEvent(self.id(), 'onFinish', activeIndex);
if ( activePlaylist.length > 1 && activeIndex < (activePlaylist.length - 1) ) {
// not the last clip in the playlist
log("Not last clip in the playlist, moving to next one");
video.fp_play(++activeIndex, false, true);
}
};
video.addEventListener('ended', onFinish, false);
var onError = function(e) {
setState(STATE_LOADED, true);
$f.fireEvent(self.id(), 'onError', activeIndex, 201);
if ( opts.onFail && opts.onFail instanceof Function )
opts.onFail.apply(self, []);
};
video.addEventListener('error', onError, false);
var onPause = function(e) {
log("got pause event from player" + self.id());
if ( stopping )
return;
if ( currentState == STATE_BUFFERING && previousState == STATE_UNSTARTED ) {
log("forcing play");
setTimeout(function() { video.play(); }, 0);
return;// stopEvent(e);
}
if ( ! actionAllowed('Pause') ) {
// user initiated pause
video.fp_resume();
return stopEvent(e);
}
setState(STATE_PAUSED);
$f.fireEvent(self.id(), 'onPause', activeIndex);
}
video.addEventListener('pause', onPause, false);
var onSeek = function(e) {
$f.fireEvent(self.id(), 'onBeforeSeek', activeIndex);
};
video.addEventListener('seeking', onSeek, false);
var onSeekDone = function(e) {
if ( stopping ) {
stopping = false;
$f.fireEvent(self.id(), 'onStop', activeIndex);
}
else
$f.fireEvent(self.id(), 'onSeek', activeIndex);
log("seek done, currentState", stateDescription(currentState));
if ( playAfterSeek ) {
playAfterSeek = false;
video.fp_play();
} else if ( currentState != STATE_PLAYING )
video.fp_pause();
};
video.addEventListener('seeked', onSeekDone, false);
/* PLAYER EVENTS MAPPING */
var onVolumeChange = function(e) {
// add onBeforeQwe here
$f.fireEvent(self.id(), 'onVolume', video.fp_getVolume());
};
video.addEventListener('volumechange', onVolumeChange, false);
}
// this is called only on iDevices
function onPlayerLoaded() {
video.fp_play(0);
//installControlbar();
}
function installControlbar() {
// if we're on an iDevice, try to load the js controlbar if needed
/*
if ( self['controls'] == undefined )
return; // js controlbar not loaded
var controlsConf = {};
if ( self.getConfig() && self.getConfig()['plugins'] && self.getConfig()['plugins']['controls'] )
controlsConf = self.getConfig()['plugins']['controls'];
var controlsRoot = document.createElement('div');
// dynamically load js, css file according to swf url ?
// something more smart here
controlsRoot.style.position = "absolute";
controlsRoot.style.bottom = 0;
self.getParent().children[0].appendChild(controlsRoot);
self.controls(controlsRoot, {heightRatio: opts.controlsSizeRatio }, controlsConf);
*/
}
// Here we are getting serious. If we're on an iDevice, we don't care about Flash embed.
// replace it by ours so we can install a video html5 tag instead when FP's init will be called.
if ( isiDevice || opts.simulateiDevice ) {
if ( ! window.flashembed.__replaced ) {
var realFlashembed = window.flashembed;
window.flashembed = function(root, opts, conf) {
// DON'T, I mean, DON'T use self here as we are in a global func
if (typeof root == 'string') {
root = document.getElementById(root.replace("#", ""));
}
// not found
if (!root) { return; }
var style = window.getComputedStyle(root, null);
var width = parseInt(style.width);
var height= parseInt(style.height);
// clearing root
while(root.firstChild)
root.removeChild(root.firstChild);
var container = document.createElement('div');
var api = document.createElement('video');
container.appendChild(api);
root.appendChild(container);
//var hasBuiltinControls = conf.config['plugins'] == undefined || (conf.config['plugins'] && conf.config['plugins']['controls'] && conf.config['plugins']['controls'] != null
// && self['controls'] == undefined); // we make a careful use of "self", as we're looking in the prototype
// styling container
container.style.height = height+'px';
container.style.width = width+'px';
container.style.display= 'block';
container.style.position = 'relative';
container.style.background = '-webkit-gradient(linear, left top, left bottom, from(rgba(0, 0, 0, 0.5)), to(rgba(0, 0, 0, 0.7)))';
container.style.cursor = 'default';
container.style.webkitUserDrag = 'none';
// styling video tag
api.style.height = '100%';
api.style.width = '100%';
api.style.display= 'block';
api.id = opts.id;
api.name = opts.id;
api.style.cursor = 'pointer';
api.style.webkitUserDrag = 'none';
api.type="video/mp4";
// if ( hasBuiltinControls )
// api.controls="controls";
api.playerConfig = conf.config;
// tell the player we are ready and go back to player's closure
$f.fireEvent(conf.config.playerId, 'onLoad', 'player');
//api.fp_play(conf.config.playlist);
};
flashembed.getVersion = realFlashembed.getVersion;
flashembed.asString = realFlashembed.asString;
flashembed.isSupported = function() {return true;}
flashembed.__replaced = true;
}
// hack so we get the onload event before everybody and we can set the api
var __fireEvent = self._fireEvent;
// only on iDevice, of course
self._fireEvent = function(a) {
if ( a[0] == 'onLoad' && a[1] == 'player' ) {
video = self.getParent().querySelector('video');
if ( opts.controls )
video.controls="controls";
addAPI();
addListeners();
setState(STATE_LOADED, true);
// set up first clip
video.fp_setPlaylist(video.playerConfig.playlist);
// we are loaded
onPlayerLoaded();
__fireEvent.apply(self, [a]);
}
var shouldFireEvent = currentState != STATE_UNLOADED;
if ( currentState == STATE_UNLOADED && typeof a == 'string' )
shouldFireEvent = true;
if ( shouldFireEvent )
return __fireEvent.apply(self, [a]);
}
// please, don't ask me why, but if you call video.clientHeight while the video is buffering
// it will be stuck buffering
self._swfHeight = function() {
return parseInt(video.style.height);
}
self.hasiPadSupport = function() {
return true;
}
} // end of iDevice test
// some chaining
return self;
}); 
