Created
September 21, 2010 22:47
-
-
Save lapluviosilla/590735 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Video Sync tool for fast seeking without | |
* breaking video because of an event overload | |
* | |
* Developed for ThinkWell Player | |
*/ | |
(function($) { | |
//Shortcuts (to increase compression) | |
var videosync = 'videosync', | |
TRUE = true, | |
FALSE = false, | |
vsync, | |
vsyncPublic, | |
vsyncPrivate, | |
isiPad = navigator.userAgent.match(/iPad/i) !== null, | |
// Event Strings (to increase compression) | |
vsync_started = 'videosync_started', // Started syncing to syncTime | |
vsync_stopped = 'videosync_stopped', // Stopped syncing to syncTime | |
vsync_sync = 'videosync_sync', // Thrown on every video sync | |
vsync_synced = 'videosync_synced', // Fully synced to syncTime / syncing is idle | |
vsync_seek = 'videosync_seek', // Thrown on every syncTime update | |
// Internal Namespaced Event String | |
v_seeked = 'seeked.sync', | |
v_timeupdate = 'timeupdate.sync', | |
defaults = { | |
settings: { | |
// Auto pause/play when done syncing | |
autopause: TRUE, | |
autoplay: TRUE | |
}, | |
status: { | |
syncing: FALSE, | |
seeking: FALSE, | |
lastSync: null, | |
lastSeek: null | |
}, | |
flags: { | |
stopSync: FALSE, | |
idleSync: FALSE | |
}, | |
syncTime: null | |
}, | |
//Cached jQuery Object Variables | |
$videoEl, | |
// Cached variables for use across multiple functions (loaded from data) | |
videoEl, | |
settings, | |
/* If syncing then status.syncing = true. | |
* If video is currently seeking then status.seeking = true. | |
* ( used for performance / no animations during seek / no timeupdates during sync ) | |
*/ | |
status, | |
flags, | |
syncTime; | |
//**************** | |
// PUBLIC FUNCTIONS | |
// Usage format: $.fn.videosync.seek(); , $(target).videosync(); | |
//**************** | |
vsync = function(options) { | |
var $this = this; | |
if (!$this.length) { | |
// the selector didn't match anything, and we should go ahead and return. | |
return $this; | |
} | |
$this.each(function() { | |
var $el = $(this); | |
var data = $el.data(videosync); | |
// If data is defined then the videosync has already been initialized | |
if (typeof(data) !== 'undefined' && data !== null) { return; } | |
// Initialize data with defaults | |
data = $.extend(TRUE, {}, defaults); | |
$.extend(TRUE, data.settings, options); | |
$el.data(videosync, data); | |
}); | |
return $this; | |
}; | |
vsyncPublic = vsync.prototype; | |
vsyncPublic.defaults = defaults.settings; | |
vsyncPublic.isSyncing = function() { | |
if (this.$this.length != 1) {return;} | |
loadData(this.$this.get(0)); | |
return status.syncing; | |
}; | |
vsyncPublic.play = function() { | |
if (this.$this.length != 1) {return;} | |
loadData(this.$this.get(0)); | |
if (status.syncing) { | |
settings.autoplay = TRUE; | |
saveData(this.$this.get(0)); | |
} else { | |
videoEl.play(); | |
} | |
}; | |
vsyncPublic.autoplay = function(autoplay) { | |
if (this.$this.length != 1) {return;} | |
loadData(this.$this.get(0)); | |
if (!status.syncing) {settings.autoplay = autoplay;saveData(this.$this.get(0));} | |
}; | |
//****************** | |
// PRIVATE FUNCTIONS | |
// Functions under the vsyncPrivate namespace can assume that data is loaded | |
// Call with vsyncPrivate.call if data context is needed | |
// Call directly if data is already loaded | |
// Use wrappers as needed for delayed calls or event handlers | |
//****************** | |
vsyncPrivate = {}; | |
/** | |
* Call private function with data context | |
* @private | |
*/ | |
vsyncPrivate.call = function(el, func) { | |
loadData(el); | |
// slice arguments we want to "pass through" | |
var args = Array.prototype.slice.call(arguments).slice(2); | |
var rt = func.apply(el, args); | |
saveData(el); | |
return rt; | |
}; | |
/** | |
* Wrap private function (for events) | |
* @private | |
*/ | |
vsyncPrivate.wrap = function(el, func) { | |
return function() { | |
var args = Array.prototype.slice.call(arguments); | |
vsyncPrivate.call.apply(vsyncPrivate, [el, func].concat(args)); | |
}; | |
}; | |
/** | |
* Wrap private function for multiple element calls (jquery selectors) | |
* NOTE: Used only for public wrappers at the moment | |
* @private | |
*/ | |
vsyncPrivate.multiWrap = function(func) { | |
return function() { | |
var args = Array.prototype.slice.call(arguments); | |
var retvals = []; | |
this.$this.each(function() { | |
retvals.push(vsyncPrivate.call.apply(vsyncPrivate, [this,func].concat(args))); | |
}); | |
// If called on only 1 el then return the value unwrapped | |
return (retvals.length == 1) ? retvals[0] : retvals; | |
}; | |
}; | |
/** | |
* Start syncing video to syncTime | |
*/ | |
vsyncPrivate.start = function() { | |
debug(vsync_started); | |
// If already syncing then cancel any abort and just let it keep on syncing | |
if (status.syncing) {flags.stopSync = FALSE; return;} | |
status.syncing = TRUE; | |
if (settings.autopause) { videoEl.pause(); } | |
$videoEl.bind(v_seeked, vsyncPrivate.wrap(videoEl, vsyncPrivate.seeked)); | |
//$videoEl.bind(v_timeupdate, vsyncPrivate.wrap(videoEl, vsyncPrivate.timeupdate)); | |
// set sync to current video time | |
syncTime = round(videoEl.currentTime); | |
status.lastSync = syncTime; | |
status.lastSeek = syncTime; | |
// TODO: check if we need this or not | |
// vsyncPrivate.sync(); | |
}; | |
/** | |
* Stop syncing video to syncTime | |
*/ | |
vsyncPrivate.stop = function() { | |
if (!status.syncing) {return;} | |
// If sync is idle then stop right away, if not then abort on next synced seek | |
if (flags.idleSync) { | |
vsyncPrivate.stopped(); | |
} else { | |
flags.stopSync = TRUE; | |
vsyncPrivate.sync(); | |
} | |
}; | |
/** | |
* Called when sync has completely stopped | |
*/ | |
vsyncPrivate.stopped = function() { | |
debug(vsync_stopped); | |
if (!status.syncing) {return;} | |
$videoEl.unbind(v_seeked); | |
//$videoEl.unbind(v_timeupdate); | |
if (settings.autoplay) { videoEl.play(); } | |
status.syncing = FALSE; | |
if (!status.idleSync) {$videoEl.trigger(vsync_synced);} | |
$videoEl.trigger(vsync_stopped); | |
}; | |
/** | |
* Queue up a seek/sync | |
* If syncing then update syncTime, otherwise start a sync process | |
* @param time that we want to potentially sync to | |
*/ | |
vsyncPrivate.seek = function(time) { | |
if (!status.syncing) { | |
vsyncPrivate.start(); | |
flags.stopSync = TRUE; | |
} | |
syncTime = round(time); | |
vsyncPrivate.sync(); | |
}; | |
/** | |
* Sync video to syncTime | |
* @private | |
*/ | |
vsyncPrivate.sync = function() { | |
/* Make sure not seeking && not already synced | |
* && syncTime is not in range of the last seek/sync */ | |
var lastSync = status.lastSync; | |
var lastSeek = status.lastSeek; | |
debug('sync'); | |
if (status.seeking && !videoEl.seeking) {status.seeking = false;} | |
// If stopSync flag is true then stop was called while seeking, so stop | |
// If still not synced then wait until synced before stopping | |
if (!status.seeking && flags.stopSync && syncTime == status.lastSync) { | |
flags.stopSync = FALSE; | |
vsyncPrivate.stopped(); | |
return; | |
} | |
// If out of sync then sync, if not then flag idle | |
if (!status.seeking && syncTime != lastSync && | |
(syncTime > lastSync || lastSeek > syncTime)) { | |
status.seeking = TRUE; | |
flags.idleSync = FALSE; | |
// Hack to get iPad to work | |
if (isiPad) { | |
syncTime = (syncTime < 1 ? 1 : syncTime); | |
} | |
status.lastSync = syncTime; | |
$videoEl.trigger(vsync_sync); | |
videoEl.currentTime = syncTime; | |
} else if (!status.seeking) { | |
debug('idle'); | |
flags.idleSync = TRUE; | |
$videoEl.trigger(vsync_synced); | |
} | |
}; | |
/** | |
* Called when video is done seeking | |
* Note: should only be called by seek event handler | |
* @private | |
*/ | |
vsyncPrivate.seeked = function() { | |
status.seeking = FALSE; | |
status.lastSeek = round(videoEl.currentTime); | |
vsyncPrivate.sync(); | |
}; | |
vsyncPrivate.timeupdate = function() { | |
// iPad hack | |
if (isiPad && videoEl.seeking && videoEl.currentTime < 1) { | |
vsyncPrivate.seeked(); | |
} | |
}; | |
//****************** | |
// PUBLIC WRAPPERS | |
//****************** | |
vsyncPublic.start = vsyncPrivate.multiWrap(vsyncPrivate.start); | |
vsyncPublic.stop = vsyncPrivate.multiWrap(vsyncPrivate.stop); | |
vsyncPublic.seek = vsyncPrivate.multiWrap(vsyncPrivate.seek); | |
vsyncPublic.add = vsyncPrivate.multiWrap(vsyncPrivate.add); | |
//****************** | |
// UTILITY FUNCTIONS | |
// Utility functions cannot assume that the data context | |
// is loaded and must only on arguments. | |
//****************** | |
/** | |
* Loads video sync data into temporary variables | |
* for easier access. Temporary variables are shared | |
* across videos so they must be reloaded every time. | |
* @param el the video to load the data for | |
* @private | |
*/ | |
function loadData(el) { | |
var $el = $(el); | |
var data = $el.data(videosync); | |
// if there's no data then initialize the videosync | |
if (typeof(data) === 'undefined' || data === null) {$el.videosync(); data = loadData(el); return data;} | |
videoEl = el; | |
$videoEl = $el; | |
settings = data.settings; | |
status = data.status; | |
flags = data.flags; | |
syncTime = data.syncTime; | |
return data; | |
} | |
/** | |
* Saves data from temporary variables | |
* @see #loadData | |
* @private | |
*/ | |
function saveData(el) { | |
var $el = $(el); | |
var data = $el.data(videosync); | |
if (data === null) { return; } | |
// TODO: verify that data does indeed update | |
// Only need to save non object/array data values | |
data.syncTime = syncTime; | |
return data; | |
} | |
function round(time, places) { | |
if (typeof(places) === 'undefined') {places = 2;} | |
var mod = Math.pow(10, places); | |
return (Math.round(time * mod) / mod); | |
} | |
// private function for console debugging | |
function debug($obj) { | |
if (window.console && window.console.log) { | |
window.console.log(videosync + ' --- ' + $obj); | |
} | |
} | |
// Proxy jquery init so we can set $this for videosync namespace | |
var proxied = $.fn.init; | |
$.fn.init = function() { | |
var obj = proxied.apply(this, arguments); | |
if (!obj.videosync) { | |
var func = $.proxy(vsync, obj); | |
$.extend(func, new vsync()); | |
$.extend(true, obj, {videosync: func}); | |
obj.videosync.$this = obj; | |
} | |
return obj; | |
}; | |
$.fn.init.prototype = proxied.prototype; | |
})(jQuery); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment