/* UI Component- Base class for all UI objects
================================================================================ */
_V_.Player = _V_.Component.extend({
init: function(tag, addOptions, ready){
this.tag = tag; // Store the original tag used to set options
var el = this.el = _V_.createElement("div"), // Div to contain video and controls
options = this.options = {};
// Set Options
_V_.merge(options, _V_.options); // Copy Global Defaults
_V_.merge(options, this.getVideoTagSettings()); // Override with Video Tag Options
_V_.merge(options, addOptions); // Override/extend with options from setup call
// Add callback to ready queue
this.ready(ready);
// Store controls setting, and then remove immediately so native controls don't flash.
tag.removeAttribute("controls");
// Poster will be handled by a manual
tag.removeAttribute("poster");
// Make player findable on elements
tag.player = el.player = this;
// Wrap video tag in div (el/box) container
tag.parentNode.insertBefore(el, tag);
el.appendChild(tag); // Breaks iPhone, fixed in HTML5 setup.
// Give video tag properties to box
// ID will now reference box, not the video tag
this.id = el.id = tag.id;
el.className = tag.className;
// Update tag id/class for use as HTML5 playback tech
tag.id += "_html5_api";
tag.className = "vjs-tech";
// Make player easily findable by ID
_V_.players[el.id] = this;
// Make box use width/height of tag, or default 300x150
el.setAttribute("width", options.width);
el.setAttribute("height", options.height);
// Enforce with CSS since width/height attrs don't work on divs
el.style.width = options.width+"px";
el.style.height = options.height+"px";
// Remove width/height attrs from tag so CSS can make it 100% width/height
tag.removeAttribute("width");
tag.removeAttribute("height");
// Empty video tag sources and tracks so the built-in player doesn't use them also.
if (tag.hasChildNodes()) {
var nrOfChildNodes = tag.childNodes.length;
for (var i=0,j=tag.childNodes;i 0) {
this.addTextTracks(options.tracks);
}
// If there are no sources when the player is initialized,
// load the first supported playback technology.
if (!options.sources || options.sources.length == 0) {
for (var i=0,j=options.techOrder; i 0) {
techOptions.startTime = this.values.currentTime;
}
this.values.src = source.src;
}
// Initialize tech instance
this.tech = new _V_[techName](this, techOptions);
this.tech.ready(techReady);
},
unloadTech: function(){
this.tech.destroy();
// Turn off any manual progress or timeupdate tracking
if (this.manualProgress) { this.manualProgressOff(); }
if (this.manualTimeUpdates) { this.manualTimeUpdatesOff(); }
this.tech = false;
},
// There's many issues around changing the size of a Flash (or other plugin) object.
// First is a plugin reload issue in Firefox that has been around for 11 years: https://bugzilla.mozilla.org/show_bug.cgi?id=90268
// Then with the new fullscreen API, Mozilla and webkit browsers will reload the flash object after going to fullscreen.
// To get around this, we're unloading the tech, caching source and currentTime values, and reloading the tech once the plugin is resized.
// reloadTech: function(betweenFn){
// _V_.log("unloadingTech")
// this.unloadTech();
// _V_.log("unloadedTech")
// if (betweenFn) { betweenFn.call(); }
// _V_.log("LoadingTech")
// this.loadTech(this.techName, { src: this.values.src })
// _V_.log("loadedTech")
// },
/* Fallbacks for unsupported event types
================================================================================ */
// Manually trigger progress events based on changes to the buffered amount
// Many flash players and older HTML5 browsers don't send progress or progress-like events
manualProgressOn: function(){
this.manualProgress = true;
// Trigger progress watching when a source begins loading
this.trackProgress();
// Watch for a native progress event call on the tech element
// In HTML5, some older versions don't support the progress event
// So we're assuming they don't, and turning off manual progress if they do.
this.tech.on("progress", function(){
// Remove this listener from the element
this.removeEvent("progress", arguments.callee);
// Update known progress support for this playback technology
this.support.progressEvent = true;
// Turn off manual progress tracking
this.player.manualProgressOff();
});
},
manualProgressOff: function(){
this.manualProgress = false;
this.stopTrackingProgress();
},
trackProgress: function(){
this.progressInterval = setInterval(_V_.proxy(this, function(){
// Don't trigger unless buffered amount is greater than last time
// log(this.values.bufferEnd, this.buffered().end(0), this.duration())
/* TODO: update for multiple buffered regions */
if (this.values.bufferEnd < this.buffered().end(0)) {
this.trigger("progress");
} else if (this.bufferedPercent() == 1) {
this.stopTrackingProgress();
this.trigger("progress"); // Last update
}
}), 500);
},
stopTrackingProgress: function(){ clearInterval(this.progressInterval); },
/* Time Tracking -------------------------------------------------------------- */
manualTimeUpdatesOn: function(){
this.manualTimeUpdates = true;
this.on("play", this.trackCurrentTime);
this.on("pause", this.stopTrackingCurrentTime);
// timeupdate is also called by .currentTime whenever current time is set
// Watch for native timeupdate event
this.tech.on("timeupdate", function(){
// Remove this listener from the element
this.removeEvent("timeupdate", arguments.callee);
// Update known progress support for this playback technology
this.support.timeupdateEvent = true;
// Turn off manual progress tracking
this.player.manualTimeUpdatesOff();
});
},
manualTimeUpdatesOff: function(){
this.manualTimeUpdates = false;
this.stopTrackingCurrentTime();
this.removeEvent("play", this.trackCurrentTime);
this.removeEvent("pause", this.stopTrackingCurrentTime);
},
trackCurrentTime: function(){
if (this.currentTimeInterval) { this.stopTrackingCurrentTime(); }
this.currentTimeInterval = setInterval(_V_.proxy(this, function(){
this.trigger("timeupdate");
}), 250); // 42 = 24 fps // 250 is what Webkit uses // FF uses 15
},
// Turn off play progress tracking (when paused or dragging)
stopTrackingCurrentTime: function(){ clearInterval(this.currentTimeInterval); },
/* Player event handlers (how the player reacts to certain events)
================================================================================ */
onEnded: function(){
if (this.options.loop) {
this.currentTime(0);
this.play();
} else {
this.pause();
this.currentTime(0);
this.pause();
}
},
onPlay: function(){
_V_.removeClass(this.el, "vjs-paused");
_V_.addClass(this.el, "vjs-playing");
},
onPause: function(){
_V_.removeClass(this.el, "vjs-playing");
_V_.addClass(this.el, "vjs-paused");
},
onProgress: function(){
// Add custom event for when source is finished downloading.
if (this.bufferedPercent() == 1) {
this.trigger("loadedalldata");
}
},
onError: function(e) {
_V_.log("Video Error", e);
},
/* Player API
================================================================================ */
// Pass values to the playback tech
techCall: function(method, arg){
// If it's not ready yet, call method when it is
if (!this.tech.isReady) {
this.tech.ready(function(){
this[method](arg);
});
// Otherwise call method now
} else {
try {
this.tech[method](arg);
} catch(e) {
_V_.log(e);
}
}
},
// Get calls can't wait for the tech, and sometimes don't need to.
techGet: function(method){
// Make sure tech is ready
if (this.tech.isReady) {
// Flash likes to die and reload when you hide or reposition it.
// In these cases the object methods go away and we get errors.
// When that happens we'll catch the errors and inform tech that it's not ready any more.
try {
return this.tech[method]();
} catch(e) {
// When building additional tech libs, an expected method may not be defined yet
if (this.tech[method] === undefined) {
_V_.log("Video.js: " + method + " method not defined for "+this.techName+" playback technology.", e);
} else {
// When a method isn't available on the object it throws a TypeError
if (e.name == "TypeError") {
_V_.log("Video.js: " + method + " unavailable on "+this.techName+" playback technology element.", e);
this.tech.isReady = false;
} else {
_V_.log(e);
}
}
}
}
return;
},
// Method for calling methods on the current playback technology
// techCall: function(method, arg){
//
// // if (this.isReady) {
// //
// // } else {
// // _V_.log("The playback technology API is not ready yet. Use player.ready(myFunction)."+" ["+method+"]", arguments.callee.caller.arguments.callee.caller.arguments.callee.caller)
// // return false;
// // // throw new Error("The playback technology API is not ready yet. Use player.ready(myFunction)."+" ["+method+"]");
// // }
// },
// http://dev.w3.org/html5/spec/video.html#dom-media-play
play: function(){
this.techCall("play");
return this;
},
// http://dev.w3.org/html5/spec/video.html#dom-media-pause
pause: function(){
this.techCall("pause");
return this;
},
// http://dev.w3.org/html5/spec/video.html#dom-media-paused
// The initial state of paused should be true (in Safari it's actually false)
paused: function(){
return (this.techGet("paused") === false) ? false : true;
},
// http://dev.w3.org/html5/spec/video.html#dom-media-currenttime
currentTime: function(seconds){
if (seconds !== undefined) {
// Cache the last set value for smoother scrubbing.
this.values.lastSetCurrentTime = seconds;
this.techCall("setCurrentTime", seconds);
// Improve the accuracy of manual timeupdates
if (this.manualTimeUpdates) { this.trigger("timeupdate"); }
return this;
}
// Cache last currentTime and return
// Default to 0 seconds
return this.values.currentTime = (this.techGet("currentTime") || 0);
},
// http://dev.w3.org/html5/spec/video.html#dom-media-duration
// Duration should return NaN if not available. ParseFloat will turn false-ish values to NaN.
duration: function(){
return parseFloat(this.techGet("duration"));
},
// Calculates how much time is left. Not in spec, but useful.
remainingTime: function(){
return this.duration() - this.currentTime();
},
// http://dev.w3.org/html5/spec/video.html#dom-media-buffered
// Buffered returns a timerange object. Kind of like an array of portions of the video that have been downloaded.
// So far no browsers return more than one range (portion)
buffered: function(){
var buffered = this.techGet("buffered"),
start = 0,
end = this.values.bufferEnd = this.values.bufferEnd || 0, // Default end to 0 and store in values
timeRange;
if (buffered && buffered.length > 0 && buffered.end(0) !== end) {
end = buffered.end(0);
// Storing values allows them be overridden by setBufferedFromProgress
this.values.bufferEnd = end;
}
return _V_.createTimeRange(start, end);
},
// Calculates amount of buffer is full. Not in spec but useful.
bufferedPercent: function(){
return (this.duration()) ? this.buffered().end(0) / this.duration() : 0;
},
// http://dev.w3.org/html5/spec/video.html#dom-media-volume
volume: function(percentAsDecimal){
var vol;
if (percentAsDecimal !== undefined) {
vol = Math.max(0, Math.min(1, parseFloat(percentAsDecimal))); // Force value to between 0 and 1
this.values.volume = vol;
this.techCall("setVolume", vol);
_V_.setLocalStorage("volume", vol);
return this;
}
// Default to 1 when returning current volume.
vol = parseFloat(this.techGet("volume"));
return (isNaN(vol)) ? 1 : vol;
},
// http://dev.w3.org/html5/spec/video.html#attr-media-muted
muted: function(muted){
if (muted !== undefined) {
this.techCall("setMuted", muted);
return this;
}
return this.techGet("muted") || false; // Default to false
},
// http://dev.w3.org/html5/spec/dimension-attributes.html#attr-dim-height
// Video tag width/height only work in pixels. No percents.
// We could potentially allow percents but won't for now until we can do testing around it.
width: function(width, skipListeners){
if (width !== undefined) {
this.el.width = width;
this.el.style.width = width+"px";
// skipListeners allows us to avoid triggering the resize event when setting both width and height
if (!skipListeners) { this.trigger("resize"); }
return this;
}
return parseInt(this.el.getAttribute("width"));
},
height: function(height){
if (height !== undefined) {
this.el.height = height;
this.el.style.height = height+"px";
this.trigger("resize");
return this;
}
return parseInt(this.el.getAttribute("height"));
},
// Set both width and height at the same time.
size: function(width, height){
// Skip resize listeners on width for optimization
return this.width(width, true).height(height);
},
// Check if current tech can support native fullscreen (e.g. with built in controls lik iOS, so not our flash swf)
supportsFullScreen: function(){ return this.techGet("supportsFullScreen") || false; },
// Turn on fullscreen (or window) mode
requestFullScreen: function(){
var requestFullScreen = _V_.support.requestFullScreen;
this.isFullScreen = true;
// Check for browser element fullscreen support
if (requestFullScreen) {
// Trigger fullscreenchange event after change
_V_.on(document, requestFullScreen.eventName, this.proxy(function(){
this.isFullScreen = document[requestFullScreen.isFullScreen];
// If cancelling fullscreen, remove event listener.
if (this.isFullScreen == false) {
_V_.removeEvent(document, requestFullScreen.eventName, arguments.callee);
}
this.trigger("fullscreenchange");
}));
// Flash and other plugins get reloaded when you take their parent to fullscreen.
// To fix that we'll remove the tech, and reload it after the resize has finished.
if (this.tech.support.fullscreenResize === false && this.options.flash.iFrameMode != true) {
this.pause();
this.unloadTech();
_V_.on(document, requestFullScreen.eventName, this.proxy(function(){
_V_.removeEvent(document, requestFullScreen.eventName, arguments.callee);
this.loadTech(this.techName, { src: this.values.src });
}));
this.el[requestFullScreen.requestFn]();
} else {
this.el[requestFullScreen.requestFn]();
}
} else if (this.tech.supportsFullScreen()) {
this.trigger("fullscreenchange");
this.techCall("enterFullScreen");
} else {
this.trigger("fullscreenchange");
this.enterFullWindow();
}
return this;
},
cancelFullScreen: function(){
var requestFullScreen = _V_.support.requestFullScreen;
this.isFullScreen = false;
// Check for browser element fullscreen support
if (requestFullScreen) {
// Flash and other plugins get reloaded when you take their parent to fullscreen.
// To fix that we'll remove the tech, and reload it after the resize has finished.
if (this.tech.support.fullscreenResize === false && this.options.flash.iFrameMode != true) {
this.pause();
this.unloadTech();
_V_.on(document, requestFullScreen.eventName, this.proxy(function(){
_V_.removeEvent(document, requestFullScreen.eventName, arguments.callee);
this.loadTech(this.techName, { src: this.values.src })
}));
document[requestFullScreen.cancelFn]();
} else {
document[requestFullScreen.cancelFn]();
}
} else if (this.tech.supportsFullScreen()) {
this.techCall("exitFullScreen");
this.trigger("fullscreenchange");
} else {
this.exitFullWindow();
this.trigger("fullscreenchange");
}
return this;
},
// When fullscreen isn't supported we can stretch the video container to as wide as the browser will let us.
enterFullWindow: function(){
this.isFullWindow = true;
// Storing original doc overflow value to return to when fullscreen is off
this.docOrigOverflow = document.documentElement.style.overflow;
// Add listener for esc key to exit fullscreen
_V_.on(document, "keydown", _V_.proxy(this, this.fullWindowOnEscKey));
// Hide any scroll bars
document.documentElement.style.overflow = 'hidden';
// Apply fullscreen styles
_V_.addClass(document.body, "vjs-full-window");
_V_.addClass(this.el, "vjs-fullscreen");
this.trigger("enterFullWindow");
},
fullWindowOnEscKey: function(event){
if (event.keyCode == 27) {
if (this.isFullScreen == true) {
this.cancelFullScreen();
} else {
this.exitFullWindow();
}
}
},
exitFullWindow: function(){
this.isFullWindow = false;
_V_.removeEvent(document, "keydown", this.fullWindowOnEscKey);
// Unhide scroll bars.
document.documentElement.style.overflow = this.docOrigOverflow;
// Remove fullscreen styles
_V_.removeClass(document.body, "vjs-full-window");
_V_.removeClass(this.el, "vjs-fullscreen");
// Resize the box, controller, and poster to original sizes
// this.positionAll();
this.trigger("exitFullWindow");
},
selectSource: function(sources){
// Loop through each playback technology in the options order
for (var i=0,j=this.options.techOrder;i