Skip to content

Instantly share code, notes, and snippets.

@lapluviosilla
Created September 21, 2010 22:47
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save lapluviosilla/590735 to your computer and use it in GitHub Desktop.
Save lapluviosilla/590735 to your computer and use it in GitHub Desktop.
/**
* 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