/* 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