/* * jPlayer Plugin for jQuery JavaScript Library * http://www.happyworm.com/jquery/jplayer * * Copyright (c) 2009 - 2010 Happyworm Ltd * Dual licensed under the MIT and GPL licenses. * - http://www.opensource.org/licenses/mit-license.php * - http://www.gnu.org/copyleft/gpl.html * * Author: Mark J Panaghiston * Version: 2.0.0 * Date: 20th December 2010 */ (function($, undefined) { // Adapted from jquery.ui.widget.js (1.8.7): $.widget.bridge $.fn.jPlayer = function( options ) { var name = "jPlayer"; var isMethodCall = typeof options === "string", args = Array.prototype.slice.call( arguments, 1 ), returnValue = this; // allow multiple hashes to be passed on init options = !isMethodCall && args.length ? $.extend.apply( null, [ true, options ].concat(args) ) : options; // prevent calls to internal methods if ( isMethodCall && options.charAt( 0 ) === "_" ) { return returnValue; } if ( isMethodCall ) { this.each(function() { var instance = $.data( this, name ), methodValue = instance && $.isFunction( instance[options] ) ? instance[ options ].apply( instance, args ) : instance; if ( methodValue !== instance && methodValue !== undefined ) { returnValue = methodValue; return false; } }); } else { this.each(function() { var instance = $.data( this, name ); if ( instance ) { instance.option( options || {} )._init(); // Orig jquery.ui.widget.js code: Not recommend for jPlayer. ie., Applying new options to an existing instance (via the jPlayer constructor) and performing the _init(). The _init() is what concerns me. It would leave a lot of event handlers acting on jPlayer instance and the interface. instance.option( options || {} ); // The new constructor only changes the options. Changing options only has basic support atm. } else { $.data( this, name, new $.jPlayer( options, this ) ); } }); } return returnValue; }; $.jPlayer = function( options, element ) { // allow instantiation without initializing for simple inheritance if ( arguments.length ) { this.element = $(element); this.options = $.extend(true, {}, this.options, options ); var self = this; this.element.bind( "remove.jPlayer", function() { self.destroy(); }); this._init(); } }; // End of: (Adapted from jquery.ui.widget.js (1.8.7)) $.jPlayer.event = { ready: "jPlayer_ready", resize: "jPlayer_resize", // Not implemented. error: "jPlayer_error", // Event error code in event.jPlayer.error.type. See $.jPlayer.error warning: "jPlayer_warning", // Event warning code in event.jPlayer.warning.type. See $.jPlayer.warning // Other events match HTML5 spec. loadstart: "jPlayer_loadstart", progress: "jPlayer_progress", suspend: "jPlayer_suspend", abort: "jPlayer_abort", emptied: "jPlayer_emptied", stalled: "jPlayer_stalled", play: "jPlayer_play", pause: "jPlayer_pause", loadedmetadata: "jPlayer_loadedmetadata", loadeddata: "jPlayer_loadeddata", waiting: "jPlayer_waiting", playing: "jPlayer_playing", canplay: "jPlayer_canplay", canplaythrough: "jPlayer_canplaythrough", seeking: "jPlayer_seeking", seeked: "jPlayer_seeked", timeupdate: "jPlayer_timeupdate", ended: "jPlayer_ended", ratechange: "jPlayer_ratechange", durationchange: "jPlayer_durationchange", volumechange: "jPlayer_volumechange" }; $.jPlayer.htmlEvent = [ // These HTML events are bubbled through to the jPlayer event, without any internal action. "loadstart", // "progress", // jPlayer uses internally before bubbling. // "suspend", // jPlayer uses internally before bubbling. "abort", // "error", // jPlayer uses internally before bubbling. "emptied", "stalled", // "play", // jPlayer uses internally before bubbling. // "pause", // jPlayer uses internally before bubbling. "loadedmetadata", "loadeddata", // "waiting", // jPlayer uses internally before bubbling. // "playing", // jPlayer uses internally before bubbling. // "canplay", // jPlayer fixes the volume (for Chrome) before bubbling. "canplaythrough", // "seeking", // jPlayer uses internally before bubbling. // "seeked", // jPlayer uses internally before bubbling. // "timeupdate", // jPlayer uses internally before bubbling. // "ended", // jPlayer uses internally before bubbling. "ratechange" // "durationchange" // jPlayer uses internally before bubbling. // "volumechange" // Handled by jPlayer in volume() method, primarily due to the volume fix (for Chrome) in the canplay event. [*] Need to review whether the latest Chrome still needs the fix sometime. ]; $.jPlayer.pause = function() { // $.each($.jPlayer.instances, function(i, element) { $.each($.jPlayer.prototype.instances, function(i, element) { if(element.data("jPlayer").status.srcSet) { // Check that media is set otherwise would cause error event. element.jPlayer("pause"); } }); }; $.jPlayer.timeFormat = { showHour: false, showMin: true, showSec: true, padHour: false, padMin: true, padSec: true, sepHour: ":", sepMin: ":", sepSec: "" }; $.jPlayer.convertTime = function(sec) { var myTime = new Date(sec * 1000); var hour = myTime.getUTCHours(); var min = myTime.getUTCMinutes(); var sec = myTime.getUTCSeconds(); var strHour = ($.jPlayer.timeFormat.padHour && hour < 10) ? "0" + hour : hour; var strMin = ($.jPlayer.timeFormat.padMin && min < 10) ? "0" + min : min; var strSec = ($.jPlayer.timeFormat.padSec && sec < 10) ? "0" + sec : sec; return (($.jPlayer.timeFormat.showHour) ? strHour + $.jPlayer.timeFormat.sepHour : "") + (($.jPlayer.timeFormat.showMin) ? strMin + $.jPlayer.timeFormat.sepMin : "") + (($.jPlayer.timeFormat.showSec) ? strSec + $.jPlayer.timeFormat.sepSec : ""); }; // Adapting jQuery 1.4.4 code for jQuery.browser. Required since jQuery 1.3.2 does not detect Chrome as webkit. $.jPlayer.uaMatch = function( ua ) { var ua = ua.toLowerCase(); // Useragent RegExp var rwebkit = /(webkit)[ \/]([\w.]+)/; var ropera = /(opera)(?:.*version)?[ \/]([\w.]+)/; var rmsie = /(msie) ([\w.]+)/; var rmozilla = /(mozilla)(?:.*? rv:([\w.]+))?/; var match = rwebkit.exec( ua ) || ropera.exec( ua ) || rmsie.exec( ua ) || ua.indexOf("compatible") < 0 && rmozilla.exec( ua ) || []; return { browser: match[1] || "", version: match[2] || "0" }; }; $.jPlayer.browser = { }; var browserMatch = $.jPlayer.uaMatch(navigator.userAgent); if ( browserMatch.browser ) { $.jPlayer.browser[ browserMatch.browser ] = true; $.jPlayer.browser.version = browserMatch.version; } $.jPlayer.prototype = { count: 0, // Static Variable: Change it via prototype. version: { // Static Object script: "2.0.0", needFlash: "2.0.0", flash: "unknown" }, options: { // Instanced in $.jPlayer() constructor swfPath: "js", // Path to Jplayer.swf. Can be relative, absolute or server root relative. solution: "html, flash", // Valid solutions: html, flash. Order defines priority. 1st is highest, supplied: "mp3", // Defines which formats jPlayer will try and support and the priority by the order. 1st is highest, preload: 'metadata', // HTML5 Spec values: none, metadata, auto. volume: 0.8, // The volume. Number 0 to 1. muted: false, backgroundColor: "#000000", // To define the jPlayer div and Flash background color. cssSelectorAncestor: "#jp_interface_1", cssSelector: { videoPlay: ".jp-video-play", play: ".jp-play", pause: ".jp-pause", stop: ".jp-stop", seekBar: ".jp-seek-bar", playBar: ".jp-play-bar", mute: ".jp-mute", unmute: ".jp-unmute", volumeBar: ".jp-volume-bar", volumeBarValue: ".jp-volume-bar-value", currentTime: ".jp-current-time", duration: ".jp-duration" }, // globalVolume: false, // Not implemented: Set to make volume changes affect all jPlayer instances // globalMute: false, // Not implemented: Set to make mute changes affect all jPlayer instances idPrefix: "jp", // Prefix for the ids of html elements created by jPlayer. For flash, this must not include characters: . - + * / \ errorAlerts: false, warningAlerts: false }, instances: {}, // Static Object status: { // Instanced in _init() src: "", media: {}, paused: true, format: {}, formatType: "", waitForPlay: true, // Same as waitForLoad except in case where preloading. waitForLoad: true, srcSet: false, video: false, // True if playing a video seekPercent: 0, currentPercentRelative: 0, currentPercentAbsolute: 0, currentTime: 0, duration: 0 }, _status: { // Instanced in _init(): These status values are persistent. ie., Are not affected by a status reset. volume: undefined, // Set by constructor option/default. muted: false, // Set by constructor option/default. width: 0, // Read from CSS height: 0 // Read from CSS }, internal: { // Instanced in _init() ready: false, instance: undefined, htmlDlyCmdId: undefined }, solution: { // Static Object: Defines the solutions built in jPlayer. html: true, flash: true }, // 'MPEG-4 support' : canPlayType('video/mp4; codecs="mp4v.20.8"') format: { // Static Object mp3: { codec: 'audio/mpeg; codecs="mp3"', flashCanPlay: true, media: 'audio' }, m4a: { // AAC / MP4 codec: 'audio/mp4; codecs="mp4a.40.2"', flashCanPlay: true, media: 'audio' }, oga: { // OGG codec: 'audio/ogg; codecs="vorbis"', flashCanPlay: false, media: 'audio' }, wav: { // PCM codec: 'audio/wav; codecs="1"', flashCanPlay: false, media: 'audio' }, webma: { // WEBM codec: 'audio/webm; codecs="vorbis"', flashCanPlay: false, media: 'audio' }, m4v: { // H.264 / MP4 codec: 'video/mp4; codecs="avc1.42E01E, mp4a.40.2"', flashCanPlay: true, media: 'video' }, ogv: { // OGG codec: 'video/ogg; codecs="theora, vorbis"', flashCanPlay: false, media: 'video' }, webmv: { // WEBM codec: 'video/webm; codecs="vorbis, vp8"', flashCanPlay: false, media: 'video' } }, _init: function() { var self = this; this.element.empty(); this.status = $.extend({}, this.status, this._status); // Copy static to unique instance. Adds the status propeties that persist through a reset. NB: Might want to use $.jPlayer.prototype.status instead once options completely implmented and _init() returned to $.fn.jPlayer plugin. this.internal = $.extend({}, this.internal); // Copy static to unique instance. this.formats = []; // Array based on supplied string option. Order defines priority. this.solutions = []; // Array based on solution string option. Order defines priority. this.require = {}; // Which media types are required: video, audio. this.htmlElement = {}; // DOM elements created by jPlayer this.html = {}; // In _init()'s this.desired code and setmedia(): Accessed via this[solution], where solution from this.solutions array. this.html.audio = {}; this.html.video = {}; this.flash = {}; // In _init()'s this.desired code and setmedia(): Accessed via this[solution], where solution from this.solutions array. this.css = {}; this.css.cs = {}; // Holds the css selector strings this.css.jq = {}; // Holds jQuery selectors. ie., $(css.cs.method) this.status.volume = this._limitValue(this.options.volume, 0, 1); // Set volume status from constructor option. this.status.muted = this.options.muted; // Set muted status from constructor option. this.status.width = this.element.css('width'); // Sets from CSS. this.status.height = this.element.css('height'); // Sets from CSS. this.element.css({'background-color': this.options.backgroundColor}); // Create the formats array, with prority based on the order of the supplied formats string $.each(this.options.supplied.toLowerCase().split(","), function(index1, value1) { var format = value1.replace(/^\s+|\s+$/g, ""); //trim if(self.format[format]) { // Check format is valid. var dupFound = false; $.each(self.formats, function(index2, value2) { // Check for duplicates if(format === value2) { dupFound = true; return false; } }); if(!dupFound) { self.formats.push(format); } } }); // Create the solutions array, with prority based on the order of the solution string $.each(this.options.solution.toLowerCase().split(","), function(index1, value1) { var solution = value1.replace(/^\s+|\s+$/g, ""); //trim if(self.solution[solution]) { // Check solution is valid. var dupFound = false; $.each(self.solutions, function(index2, value2) { // Check for duplicates if(solution === value2) { dupFound = true; return false; } }); if(!dupFound) { self.solutions.push(solution); } } }); this.internal.instance = "jp_" + this.count; this.instances[this.internal.instance] = this.element; // Check the jPlayer div has an id and create one if required. Important for Flash to know the unique id for comms. if(this.element.attr("id") === "") { this.element.attr("id", this.options.idPrefix + "_jplayer_" + this.count); } this.internal.self = $.extend({}, { id: this.element.attr("id"), jq: this.element }); this.internal.audio = $.extend({}, { id: this.options.idPrefix + "_audio_" + this.count, jq: undefined }); this.internal.video = $.extend({}, { id: this.options.idPrefix + "_video_" + this.count, jq: undefined }); this.internal.flash = $.extend({}, { id: this.options.idPrefix + "_flash_" + this.count, jq: undefined, swf: this.options.swfPath// + ((this.options.swfPath !== "" && this.options.swfPath.slice(-1) !== "/") ? "/" : "") }); this.internal.poster = $.extend({}, { id: this.options.idPrefix + "_poster_" + this.count, jq: undefined }); // Register listeners defined in the constructor $.each($.jPlayer.event, function(eventName,eventType) { if(self.options[eventName] !== undefined) { self.element.bind(eventType + ".jPlayer", self.options[eventName]); // With .jPlayer namespace. self.options[eventName] = undefined; // Destroy the handler pointer copy on the options. Reason, events can be added/removed in other ways so this could be obsolete and misleading. } }); // Create the poster image. this.htmlElement.poster = document.createElement('img'); this.htmlElement.poster.id = this.internal.poster.id; this.htmlElement.poster.onload = function() { // Note that this did not work on Firefox 3.6: poster.addEventListener("onload", function() {}, false); Did not investigate x-browser. if(!self.status.video || self.status.waitForPlay) { self.internal.poster.jq.show(); } }; this.element.append(this.htmlElement.poster); this.internal.poster.jq = $("#" + this.internal.poster.id); this.internal.poster.jq.css({'width': this.status.width, 'height': this.status.height}); this.internal.poster.jq.hide(); // Determine if we require solutions for audio, video or both media types. this.require.audio = false; this.require.video = false; $.each(this.formats, function(priority, format) { self.require[self.format[format].media] = true; }); this.html.audio.available = false; if(this.require.audio) { // If a supplied format is audio this.htmlElement.audio = document.createElement('audio'); this.htmlElement.audio.id = this.internal.audio.id; this.html.audio.available = !!this.htmlElement.audio.canPlayType; } this.html.video.available = false; if(this.require.video) { // If a supplied format is video this.htmlElement.video = document.createElement('video'); this.htmlElement.video.id = this.internal.video.id; this.html.video.available = !!this.htmlElement.video.canPlayType; } this.flash.available = this._checkForFlash(10); // IE9 forced to false due to ExternalInterface problem. this.html.canPlay = {}; this.flash.canPlay = {}; $.each(this.formats, function(priority, format) { self.html.canPlay[format] = self.html[self.format[format].media].available && "" !== self.htmlElement[self.format[format].media].canPlayType(self.format[format].codec); self.flash.canPlay[format] = self.format[format].flashCanPlay && self.flash.available; }); this.html.desired = false; this.flash.desired = false; $.each(this.solutions, function(solutionPriority, solution) { if(solutionPriority === 0) { self[solution].desired = true; } else { var audioCanPlay = false; var videoCanPlay = false; $.each(self.formats, function(formatPriority, format) { if(self[self.solutions[0]].canPlay[format]) { // The other solution can play if(self.format[format].media === 'video') { videoCanPlay = true; } else { audioCanPlay = true; } } }); self[solution].desired = (self.require.audio && !audioCanPlay) || (self.require.video && !videoCanPlay); } }); // This is what jPlayer will support, based on solution and supplied. this.html.support = {}; this.flash.support = {}; $.each(this.formats, function(priority, format) { self.html.support[format] = self.html.canPlay[format] && self.html.desired; self.flash.support[format] = self.flash.canPlay[format] && self.flash.desired; }); // If jPlayer is supporting any format in a solution, then the solution is used. this.html.used = false; this.flash.used = false; $.each(this.solutions, function(solutionPriority, solution) { $.each(self.formats, function(formatPriority, format) { if(self[solution].support[format]) { self[solution].used = true; return false; } }); }); // If neither html nor flash are being used by this browser, then media playback is not possible. Trigger an error event. if(!(this.html.used || this.flash.used)) { this._error( { type: $.jPlayer.error.NO_SOLUTION, context: "{solution:'" + this.options.solution + "', supplied:'" + this.options.supplied + "'}", message: $.jPlayer.errorMsg.NO_SOLUTION, hint: $.jPlayer.errorHint.NO_SOLUTION }); } // Init solution active state and the event gates to false. this.html.active = false; this.html.audio.gate = false; this.html.video.gate = false; this.flash.active = false; this.flash.gate = false; // Add the flash solution if it is being used. if(this.flash.used) { var flashVars = 'id=' + escape(this.internal.self.id) + '&vol=' + this.status.volume + '&muted=' + this.status.muted; if($.browser.msie && Number($.browser.version) <= 8) { var html_obj = ''; obj_param[1] = ''; obj_param[2] = ''; obj_param[3] = ''; obj_param[4] = ''; var ie_dom = document.createElement(html_obj); for(var i=0; i < obj_param.length; i++) { ie_dom.appendChild(document.createElement(obj_param[i])); } this.element.append(ie_dom); } else { var html_embed = ' 0) ? 100 * ct / this.status.duration : 0; if((typeof media.seekable === "object") && (media.seekable.length > 0)) { sp = (this.status.duration > 0) ? 100 * media.seekable.end(media.seekable.length-1) / this.status.duration : 100; cpr = 100 * media.currentTime / media.seekable.end(media.seekable.length-1); } else { sp = 100; cpr = cpa; } if(override) { ct = 0; cpr = 0; cpa = 0; } this.status.seekPercent = sp; this.status.currentPercentRelative = cpr; this.status.currentPercentAbsolute = cpa; this.status.currentTime = ct; }, _resetStatus: function() { var self = this; this.status = $.extend({}, this.status, $.jPlayer.prototype.status); // Maintains the status properties that persist through a reset. ie., The properties of this._status, contained in the current this.status. }, _trigger: function(eventType, error, warning) { // eventType always valid as called using $.jPlayer.event.eventType var event = $.Event(eventType); event.jPlayer = {}; event.jPlayer.version = $.extend({}, this.version); event.jPlayer.status = $.extend(true, {}, this.status); // Deep copy event.jPlayer.html = $.extend(true, {}, this.html); // Deep copy event.jPlayer.flash = $.extend(true, {}, this.flash); // Deep copy if(error) event.jPlayer.error = $.extend({}, error); if(warning) event.jPlayer.warning = $.extend({}, warning); this.element.trigger(event); }, jPlayerFlashEvent: function(eventType, status) { // Called from Flash if(eventType === $.jPlayer.event.ready && !this.internal.ready) { this.internal.ready = true; this.version.flash = status.version; if(this.version.needFlash !== this.version.flash) { this._error( { type: $.jPlayer.error.VERSION, context: this.version.flash, message: $.jPlayer.errorMsg.VERSION + this.version.flash, hint: $.jPlayer.errorHint.VERSION }); } this._trigger(eventType); } if(this.flash.gate) { switch(eventType) { case $.jPlayer.event.progress: this._getFlashStatus(status); this._updateInterface(); this._trigger(eventType); break; case $.jPlayer.event.timeupdate: this._getFlashStatus(status); this._updateInterface(); this._trigger(eventType); break; case $.jPlayer.event.play: this._seeked(); this._updateButtons(true); this._trigger(eventType); break; case $.jPlayer.event.pause: this._updateButtons(false); this._trigger(eventType); break; case $.jPlayer.event.ended: this._updateButtons(false); this._trigger(eventType); break; case $.jPlayer.event.error: this.status.waitForLoad = true; // Allows the load operation to try again. this.status.waitForPlay = true; // Reset since a play was captured. if(this.status.video) { this.internal.flash.jq.css({'width':'0px', 'height':'0px'}); } if(this._validString(this.status.media.poster)) { this.internal.poster.jq.show(); } if(this.css.jq.videoPlay.length) { this.css.jq.videoPlay.show(); } if(this.status.video) { // Set up for another try. Execute before error event. this._flash_setVideo(this.status.media); } else { this._flash_setAudio(this.status.media); } this._error( { type: $.jPlayer.error.URL, context:status.src, message: $.jPlayer.errorMsg.URL, hint: $.jPlayer.errorHint.URL }); break; case $.jPlayer.event.seeking: this._seeking(); this._trigger(eventType); break; case $.jPlayer.event.seeked: this._seeked(); this._trigger(eventType); break; default: this._trigger(eventType); } } return false; }, _getFlashStatus: function(status) { this.status.seekPercent = status.seekPercent; this.status.currentPercentRelative = status.currentPercentRelative; this.status.currentPercentAbsolute = status.currentPercentAbsolute; this.status.currentTime = status.currentTime; this.status.duration = status.duration; }, _updateButtons: function(playing) { this.status.paused = !playing; if(this.css.jq.play.length && this.css.jq.pause.length) { if(playing) { this.css.jq.play.hide(); this.css.jq.pause.show(); } else { this.css.jq.play.show(); this.css.jq.pause.hide(); } } }, _updateInterface: function() { if(this.css.jq.seekBar.length) { this.css.jq.seekBar.width(this.status.seekPercent+"%"); } if(this.css.jq.playBar.length) { this.css.jq.playBar.width(this.status.currentPercentRelative+"%"); } if(this.css.jq.currentTime.length) { this.css.jq.currentTime.text($.jPlayer.convertTime(this.status.currentTime)); } if(this.css.jq.duration.length) { this.css.jq.duration.text($.jPlayer.convertTime(this.status.duration)); } }, _seeking: function() { if(this.css.jq.seekBar.length) { this.css.jq.seekBar.addClass("jp-seeking-bg"); } }, _seeked: function() { if(this.css.jq.seekBar.length) { this.css.jq.seekBar.removeClass("jp-seeking-bg"); } }, setMedia: function(media) { /* media[format] = String: URL of format. Must contain all of the supplied option's video or audio formats. * media.poster = String: Video poster URL. * media.subtitles = String: * NOT IMPLEMENTED * URL of subtitles SRT file * media.chapters = String: * NOT IMPLEMENTED * URL of chapters SRT file * media.stream = Boolean: * NOT IMPLEMENTED * Designating actual media streams. ie., "false/undefined" for files. Plan to refresh the flash every so often. */ var self = this; this._seeked(); clearTimeout(this.internal.htmlDlyCmdId); // Clears any delayed commands used in the HTML solution. // Store the current html gates, since we need for clearMedia() conditions. var audioGate = this.html.audio.gate; var videoGate = this.html.video.gate; var supported = false; $.each(this.formats, function(formatPriority, format) { var isVideo = self.format[format].media === 'video'; $.each(self.solutions, function(solutionPriority, solution) { if(self[solution].support[format] && self._validString(media[format])) { // Format supported in solution and url given for format. var isHtml = solution === 'html'; if(isVideo) { if(isHtml) { self.html.audio.gate = false; self.html.video.gate = true; self.flash.gate = false; } else { self.html.audio.gate = false; self.html.video.gate = false; self.flash.gate = true; } } else { if(isHtml) { self.html.audio.gate = true; self.html.video.gate = false; self.flash.gate = false; } else { self.html.audio.gate = false; self.html.video.gate = false; self.flash.gate = true; } } // Clear media of the previous solution if: // - it was Flash // - changing from HTML to Flash // - the HTML solution media type (audio or video) remained the same. // Note that, we must be careful with clearMedia() on iPhone, otherwise clearing the video when changing to audio corrupts the built in video player. if(self.flash.active || (self.html.active && self.flash.gate) || (audioGate === self.html.audio.gate && videoGate === self.html.video.gate)) { self.clearMedia(); } else if(audioGate !== self.html.audio.gate && videoGate !== self.html.video.gate) { // If switching between html elements self._html_pause(); // Hide the video if it was being used. if(self.status.video) { self.internal.video.jq.css({'width':'0px', 'height':'0px'}); } self._resetStatus(); // Since clearMedia usually does this. Execute after status.video useage. } if(isVideo) { if(isHtml) { self._html_setVideo(media); self.html.active = true; self.flash.active = false; } else { self._flash_setVideo(media); self.html.active = false; self.flash.active = true; } if(self.css.jq.videoPlay.length) { self.css.jq.videoPlay.show(); } self.status.video = true; } else { if(isHtml) { self._html_setAudio(media); self.html.active = true; self.flash.active = false; } else { self._flash_setAudio(media); self.html.active = false; self.flash.active = true; } if(self.css.jq.videoPlay.length) { self.css.jq.videoPlay.hide(); } self.status.video = false; } supported = true; return false; // Exit $.each } }); if(supported) { return false; // Exit $.each } }); if(supported) { // Set poster after the possible clearMedia() command above. IE had issues since the IMG onload event occurred immediately when cached. ie., The clearMedia() hide the poster. if(this._validString(media.poster)) { if(this.htmlElement.poster.src !== media.poster) { // Since some browsers do not generate img onload event. this.htmlElement.poster.src = media.poster; } else { this.internal.poster.jq.show(); } } else { this.internal.poster.jq.hide(); // Hide if not used, since clearMedia() does not always occur above. ie., HTML audio <-> video switching. } this.status.srcSet = true; this.status.media = $.extend({}, media); this._updateButtons(false); this._updateInterface(); } else { // jPlayer cannot support any formats provided in this browser // Pause here if old media could be playing. Otherwise, playing media being changed to bad media would leave the old media playing. if(this.status.srcSet && !this.status.waitForPlay) { this.pause(); } // Reset all the control flags this.html.audio.gate = false; this.html.video.gate = false; this.flash.gate = false; this.html.active = false; this.flash.active = false; // Reset status and interface. this._resetStatus(); this._updateInterface(); this._updateButtons(false); // Hide the any old media this.internal.poster.jq.hide(); if(this.html.used && this.require.video) { this.internal.video.jq.css({'width':'0px', 'height':'0px'}); } if(this.flash.used) { this.internal.flash.jq.css({'width':'0px', 'height':'0px'}); } // Send an error event this._error( { type: $.jPlayer.error.NO_SUPPORT, context: "{supplied:'" + this.options.supplied + "'}", message: $.jPlayer.errorMsg.NO_SUPPORT, hint: $.jPlayer.errorHint.NO_SUPPORT }); } }, clearMedia: function() { this._resetStatus(); this._updateButtons(false); this.internal.poster.jq.hide(); clearTimeout(this.internal.htmlDlyCmdId); if(this.html.active) { this._html_clearMedia(); } else if(this.flash.active) { this._flash_clearMedia(); } }, load: function() { if(this.status.srcSet) { if(this.html.active) { this._html_load(); } else if(this.flash.active) { this._flash_load(); } } else { this._urlNotSetError("load"); } }, play: function(time) { time = (typeof time === "number") ? time : NaN; // Remove jQuery event from click handler if(this.status.srcSet) { if(this.html.active) { this._html_play(time); } else if(this.flash.active) { this._flash_play(time); } } else { this._urlNotSetError("play"); } }, videoPlay: function(e) { // Handles clicks on the play button over the video poster this.play(); }, pause: function(time) { time = (typeof time === "number") ? time : NaN; // Remove jQuery event from click handler if(this.status.srcSet) { if(this.html.active) { this._html_pause(time); } else if(this.flash.active) { this._flash_pause(time); } } else { this._urlNotSetError("pause"); } }, pauseOthers: function() { var self = this; $.each(this.instances, function(i, element) { if(self.element !== element) { // Do not this instance. if(element.data("jPlayer").status.srcSet) { // Check that media is set otherwise would cause error event. element.jPlayer("pause"); } } }); }, stop: function() { if(this.status.srcSet) { if(this.html.active) { this._html_pause(0); } else if(this.flash.active) { this._flash_pause(0); } } else { this._urlNotSetError("stop"); } }, playHead: function(p) { p = this._limitValue(p, 0, 100); if(this.status.srcSet) { if(this.html.active) { this._html_playHead(p); } else if(this.flash.active) { this._flash_playHead(p); } } else { this._urlNotSetError("playHead"); } }, mute: function() { this.status.muted = true; if(this.html.used) { this._html_mute(true); } if(this.flash.used) { this._flash_mute(true); } this._updateMute(true); this._updateVolume(0); this._trigger($.jPlayer.event.volumechange); }, unmute: function() { this.status.muted = false; if(this.html.used) { this._html_mute(false); } if(this.flash.used) { this._flash_mute(false); } this._updateMute(false); this._updateVolume(this.status.volume); this._trigger($.jPlayer.event.volumechange); }, _updateMute: function(mute) { if(this.css.jq.mute.length && this.css.jq.unmute.length) { if(mute) { this.css.jq.mute.hide(); this.css.jq.unmute.show(); } else { this.css.jq.mute.show(); this.css.jq.unmute.hide(); } } }, volume: function(v) { v = this._limitValue(v, 0, 1); this.status.volume = v; if(this.html.used) { this._html_volume(v); } if(this.flash.used) { this._flash_volume(v); } if(!this.status.muted) { this._updateVolume(v); } this._trigger($.jPlayer.event.volumechange); }, volumeBar: function(e) { // Handles clicks on the volumeBar if(!this.status.muted && this.css.jq.volumeBar) { // Ignore clicks when muted var offset = this.css.jq.volumeBar.offset(); var x = e.pageX - offset.left; var w = this.css.jq.volumeBar.width(); var v = x/w; this.volume(v); } }, volumeBarValue: function(e) { // Handles clicks on the volumeBarValue this.volumeBar(e); }, _updateVolume: function(v) { if(this.css.jq.volumeBarValue.length) { this.css.jq.volumeBarValue.width((v*100)+"%"); } }, _volumeFix: function(v) { // Need to review if this is still necessary on latest Chrome var rnd = 0.001 * Math.random(); // Fix for Chrome 4: Fix volume being set multiple times before playing bug. var fix = (v < 0.5) ? rnd : -rnd; // Fix for Chrome 4: Solves volume change before play bug. (When new vol == old vol Chrome 4 does nothing!) return (v + fix); // Fix for Chrome 4: Event solves initial volume not being set correctly. }, _cssSelectorAncestor: function(ancestor, refresh) { this.options.cssSelectorAncestor = ancestor; if(refresh) { $.each(this.options.cssSelector, function(fn, cssSel) { self._cssSelector(fn, cssSel); }); } }, _cssSelector: function(fn, cssSel) { var self = this; if(typeof cssSel === 'string') { if($.jPlayer.prototype.options.cssSelector[fn]) { if(this.css.jq[fn] && this.css.jq[fn].length) { this.css.jq[fn].unbind(".jPlayer"); } this.options.cssSelector[fn] = cssSel; this.css.cs[fn] = this.options.cssSelectorAncestor + " " + cssSel; if(cssSel) { // Checks for empty string this.css.jq[fn] = $(this.css.cs[fn]); } else { this.css.jq[fn] = []; // To comply with the css.jq[fn].length check before its use. As of jQuery 1.4 could have used $() for an empty set. } if(this.css.jq[fn].length) { var handler = function(e) { self[fn](e); $(this).blur(); return false; } this.css.jq[fn].bind("click.jPlayer", handler); // Using jPlayer namespace } if(cssSel && this.css.jq[fn].length !== 1) { // So empty strings do not generate the warning. ie., they just remove the old one. this._warning( { type: $.jPlayer.warning.CSS_SELECTOR_COUNT, context: this.css.cs[fn], message: $.jPlayer.warningMsg.CSS_SELECTOR_COUNT + this.css.jq[fn].length + " found for " + fn + " method.", hint: $.jPlayer.warningHint.CSS_SELECTOR_COUNT }); } } else { this._warning( { type: $.jPlayer.warning.CSS_SELECTOR_METHOD, context: fn, message: $.jPlayer.warningMsg.CSS_SELECTOR_METHOD, hint: $.jPlayer.warningHint.CSS_SELECTOR_METHOD }); } } else { this._warning( { type: $.jPlayer.warning.CSS_SELECTOR_STRING, context: cssSel, message: $.jPlayer.warningMsg.CSS_SELECTOR_STRING, hint: $.jPlayer.warningHint.CSS_SELECTOR_STRING }); } }, seekBar: function(e) { // Handles clicks on the seekBar if(this.css.jq.seekBar) { var offset = this.css.jq.seekBar.offset(); var x = e.pageX - offset.left; var w = this.css.jq.seekBar.width(); var p = 100*x/w; this.playHead(p); } }, playBar: function(e) { // Handles clicks on the playBar this.seekBar(e); }, currentTime: function(e) { // Handles clicks on the text // Added to avoid errors using cssSelector system for the text }, duration: function(e) { // Handles clicks on the text // Added to avoid errors using cssSelector system for the text }, // Options code adapted from ui.widget.js (1.8.7). Made changes so the key can use dot notation. To match previous getData solution in jPlayer 1. option: function(key, value) { var options = key; // Enables use: options(). Returns a copy of options object if ( arguments.length === 0 ) { return $.extend( true, {}, this.options ); } if(typeof key === "string") { var keys = key.split("."); // Enables use: options("someOption") Returns a copy of the option. Supports dot notation. if(value === undefined) { var opt = $.extend(true, {}, this.options); for(var i = 0; i < keys.length; i++) { if(opt[keys[i]] !== undefined) { opt = opt[keys[i]]; } else { this._warning( { type: $.jPlayer.warning.OPTION_KEY, context: key, message: $.jPlayer.warningMsg.OPTION_KEY, hint: $.jPlayer.warningHint.OPTION_KEY }); return undefined; } } return opt; } // Enables use: options("someOptionObject", someObject}). Creates: {someOptionObject:someObject} // Enables use: options("someOption", someValue). Creates: {someOption:someValue} // Enables use: options("someOptionObject.someOption", someValue). Creates: {someOptionObject:{someOption:someValue}} options = {}; var opt = options; for(var i = 0; i < keys.length; i++) { if(i < keys.length - 1) { opt[keys[i]] = {}; opt = opt[keys[i]]; } else { opt[keys[i]] = value; } } } // Otherwise enables use: options(optionObject). Uses original object (the key) this._setOptions(options); return this; }, _setOptions: function(options) { var self = this; $.each(options, function(key, value) { // This supports the 2 level depth that the options of jPlayer has. Would review if we ever need more depth. self._setOption(key, value); }); return this; }, _setOption: function(key, value) { var self = this; // The ability to set options is limited at this time. switch(key) { case "cssSelectorAncestor" : this.options[key] = value; $.each(self.options.cssSelector, function(fn, cssSel) { // Refresh all associations for new ancestor. self._cssSelector(fn, cssSel); }); break; case "cssSelector" : $.each(value, function(fn, cssSel) { self._cssSelector(fn, cssSel); }); break; } return this; }, // End of: (Options code adapted from ui.widget.js) // The resize() set of functions are not implemented yet. // Basically are currently used to allow Flash debugging without too much hassle. resize: function(css) { // MJP: Want to run some checks on dim {} first. if(this.html.active) { this._resizeHtml(css); } if(this.flash.active) { this._resizeFlash(css); } this._trigger($.jPlayer.event.resize); }, _resizePoster: function(css) { // Not implemented yet }, _resizeHtml: function(css) { // Not implemented yet }, _resizeFlash: function(css) { this.internal.flash.jq.css({'width':css.width, 'height':css.height}); }, _html_initMedia: function() { if(this.status.srcSet && !this.status.waitForPlay) { this.htmlElement.media.pause(); } if(this.options.preload !== 'none') { this._html_load(); } this._trigger($.jPlayer.event.timeupdate); // The flash generates this event for its solution. }, _html_setAudio: function(media) { var self = this; // Always finds a format due to checks in setMedia() $.each(this.formats, function(priority, format) { if(self.html.support[format] && media[format]) { self.status.src = media[format]; self.status.format[format] = true; self.status.formatType = format; return false; } }); this.htmlElement.media = this.htmlElement.audio; this._html_initMedia(); }, _html_setVideo: function(media) { var self = this; // Always finds a format due to checks in setMedia() $.each(this.formats, function(priority, format) { if(self.html.support[format] && media[format]) { self.status.src = media[format]; self.status.format[format] = true; self.status.formatType = format; return false; } }); this.htmlElement.media = this.htmlElement.video; this._html_initMedia(); }, _html_clearMedia: function() { if(this.htmlElement.media) { if(this.htmlElement.media.id === this.internal.video.id) { this.internal.video.jq.css({'width':'0px', 'height':'0px'}); } this.htmlElement.media.pause(); this.htmlElement.media.src = ""; if(!($.browser.msie && Number($.browser.version) >= 9)) { // IE9 Bug: media.load() on broken src causes an exception. In try/catch IE9 generates the error event too, but it is delayed and corrupts jPlayer's event masking. this.htmlElement.media.load(); // Stops an old, "in progress" download from continuing the download. Triggers the loadstart, error and emptied events, due to the empty src. Also an abort event if a download was in progress. } } }, _html_load: function() { if(this.status.waitForLoad) { this.status.waitForLoad = false; this.htmlElement.media.src = this.status.src; try { this.htmlElement.media.load(); // IE9 Beta throws an exception here on broken links. Review again later as IE9 Beta matures } catch(err) {} } clearTimeout(this.internal.htmlDlyCmdId); }, _html_play: function(time) { var self = this; this._html_load(); // Loads if required and clears any delayed commands. this.htmlElement.media.play(); // Before currentTime attempt otherwise Firefox 4 Beta never loads. if(!isNaN(time)) { try { this.htmlElement.media.currentTime = time; } catch(err) { this.internal.htmlDlyCmdId = setTimeout(function() { self.play(time); }, 100); return; // Cancel execution and wait for the delayed command. } } this._html_checkWaitForPlay(); }, _html_pause: function(time) { var self = this; if(time > 0) { // We do not want the stop() command, which does pause(0), causing a load operation. this._html_load(); // Loads if required and clears any delayed commands. } else { clearTimeout(this.internal.htmlDlyCmdId); } // Order of these commands is important for Safari (Win) and IE9. Pause then change currentTime. this.htmlElement.media.pause(); if(!isNaN(time)) { try { this.htmlElement.media.currentTime = time; } catch(err) { this.internal.htmlDlyCmdId = setTimeout(function() { self.pause(time); }, 100); return; // Cancel execution and wait for the delayed command. } } if(time > 0) { // Avoids a setMedia() followed by stop() or pause(0) hiding the video play button. this._html_checkWaitForPlay(); } }, _html_playHead: function(percent) { var self = this; this._html_load(); // Loads if required and clears any delayed commands. try { if((typeof this.htmlElement.media.seekable === "object") && (this.htmlElement.media.seekable.length > 0)) { this.htmlElement.media.currentTime = percent * this.htmlElement.media.seekable.end(this.htmlElement.media.seekable.length-1) / 100; } else if(this.htmlElement.media.duration > 0 && !isNaN(this.htmlElement.media.duration)) { this.htmlElement.media.currentTime = percent * this.htmlElement.media.duration / 100; } else { throw "e"; } } catch(err) { this.internal.htmlDlyCmdId = setTimeout(function() { self.playHead(percent); }, 100); return; // Cancel execution and wait for the delayed command. } if(!this.status.waitForLoad) { this._html_checkWaitForPlay(); } }, _html_checkWaitForPlay: function() { if(this.status.waitForPlay) { this.status.waitForPlay = false; if(this.css.jq.videoPlay.length) { this.css.jq.videoPlay.hide(); } if(this.status.video) { this.internal.poster.jq.hide(); this.internal.video.jq.css({'width': this.status.width, 'height': this.status.height}); } } }, _html_volume: function(v) { if(this.html.audio.available) { this.htmlElement.audio.volume = v; } if(this.html.video.available) { this.htmlElement.video.volume = v; } }, _html_mute: function(m) { if(this.html.audio.available) { this.htmlElement.audio.muted = m; } if(this.html.video.available) { this.htmlElement.video.muted = m; } }, _flash_setAudio: function(media) { var self = this; try { // Always finds a format due to checks in setMedia() $.each(this.formats, function(priority, format) { if(self.flash.support[format] && media[format]) { switch (format) { case "m4a" : self._getMovie().fl_setAudio_m4a(media[format]); break; case "mp3" : self._getMovie().fl_setAudio_mp3(media[format]); break; } self.status.src = media[format]; self.status.format[format] = true; self.status.formatType = format; return false; } }); if(this.options.preload === 'auto') { this._flash_load(); this.status.waitForLoad = false; } } catch(err) { this._flashError(err); } }, _flash_setVideo: function(media) { var self = this; try { // Always finds a format due to checks in setMedia() $.each(this.formats, function(priority, format) { if(self.flash.support[format] && media[format]) { switch (format) { case "m4v" : self._getMovie().fl_setVideo_m4v(media[format]); break; } self.status.src = media[format]; self.status.format[format] = true; self.status.formatType = format; return false; } }); if(this.options.preload === 'auto') { this._flash_load(); this.status.waitForLoad = false; } } catch(err) { this._flashError(err); } }, _flash_clearMedia: function() { this.internal.flash.jq.css({'width':'0px', 'height':'0px'}); // Must do via CSS as setting attr() to zero causes a jQuery error in IE. try { this._getMovie().fl_clearMedia(); } catch(err) { this._flashError(err); } }, _flash_load: function() { try { this._getMovie().fl_load(); } catch(err) { this._flashError(err); } this.status.waitForLoad = false; }, _flash_play: function(time) { try { this._getMovie().fl_play(time); } catch(err) { this._flashError(err); } this.status.waitForLoad = false; this._flash_checkWaitForPlay(); }, _flash_pause: function(time) { try { this._getMovie().fl_pause(time); } catch(err) { this._flashError(err); } if(time > 0) { // Avoids a setMedia() followed by stop() or pause(0) hiding the video play button. this.status.waitForLoad = false; this._flash_checkWaitForPlay(); } }, _flash_playHead: function(p) { try { this._getMovie().fl_play_head(p) } catch(err) { this._flashError(err); } if(!this.status.waitForLoad) { this._flash_checkWaitForPlay(); } }, _flash_checkWaitForPlay: function() { if(this.status.waitForPlay) { this.status.waitForPlay = false; if(this.css.jq.videoPlay.length) { this.css.jq.videoPlay.hide(); } if(this.status.video) { this.internal.poster.jq.hide(); this.internal.flash.jq.css({'width': this.status.width, 'height': this.status.height}); } } }, _flash_volume: function(v) { try { this._getMovie().fl_volume(v); } catch(err) { this._flashError(err); } }, _flash_mute: function(m) { try { this._getMovie().fl_mute(m); } catch(err) { this._flashError(err); } }, _getMovie: function() { return document[this.internal.flash.id]; }, _checkForFlash: function (version) { // Function checkForFlash adapted from FlashReplace by Robert Nyman // http://code.google.com/p/flashreplace/ var flashIsInstalled = false; var flash; if(window.ActiveXObject){ try{ flash = new ActiveXObject(("ShockwaveFlash.ShockwaveFlash." + version)); flashIsInstalled = true; } catch(e){ // Throws an error if the version isn't available } } else if(navigator.plugins && navigator.mimeTypes.length > 0){ flash = navigator.plugins["Shockwave Flash"]; if(flash){ var flashVersion = navigator.plugins["Shockwave Flash"].description.replace(/.*\s(\d+\.\d+).*/, "$1"); if(flashVersion >= version){ flashIsInstalled = true; } } } if($.browser.msie && Number($.browser.version) >= 9) { // IE9 does not work with external interface. With dynamic Flash insertion like jPlayer uses. return false; } else { return flashIsInstalled; } }, _validString: function(url) { return (url && typeof url === "string"); // Empty strings return false }, _limitValue: function(value, min, max) { return (value < min) ? min : ((value > max) ? max : value); }, _urlNotSetError: function(context) { this._error( { type: $.jPlayer.error.URL_NOT_SET, context: context, message: $.jPlayer.errorMsg.URL_NOT_SET, hint: $.jPlayer.errorHint.URL_NOT_SET }); }, _flashError: function(error) { this._error( { type: $.jPlayer.error.FLASH, context: this.internal.flash.swf, message: $.jPlayer.errorMsg.FLASH + error.message, hint: $.jPlayer.errorHint.FLASH }); }, _error: function(error) { this._trigger($.jPlayer.event.error, error); if(this.options.errorAlerts) { this._alert("Error!" + (error.message ? "\n\n" + error.message : "") + (error.hint ? "\n\n" + error.hint : "") + "\n\nContext: " + error.context); } }, _warning: function(warning) { this._trigger($.jPlayer.event.warning, undefined, warning); if(this.options.errorAlerts) { this._alert("Warning!" + (warning.message ? "\n\n" + warning.message : "") + (warning.hint ? "\n\n" + warning.hint : "") + "\n\nContext: " + warning.context); } }, _alert: function(message) { alert("jPlayer " + this.version.script + " : id='" + this.internal.self.id +"' : " + message); } }; $.jPlayer.error = { FLASH: "e_flash", NO_SOLUTION: "e_no_solution", NO_SUPPORT: "e_no_support", URL: "e_url", URL_NOT_SET: "e_url_not_set", VERSION: "e_version" }; $.jPlayer.errorMsg = { FLASH: "jPlayer's Flash fallback is not configured correctly, or a command was issued before the jPlayer Ready event. Details: ", // Used in: _flashError() NO_SOLUTION: "No solution can be found by jPlayer in this browser. Neither HTML nor Flash can be used.", // Used in: _init() NO_SUPPORT: "It is not possible to play any media format provided in setMedia() on this browser using your current options.", // Used in: setMedia() URL: "Media URL could not be loaded.", // Used in: jPlayerFlashEvent() and _addHtmlEventListeners() URL_NOT_SET: "Attempt to issue media playback commands, while no media url is set.", // Used in: load(), play(), pause(), stop() and playHead() VERSION: "jPlayer " + $.jPlayer.prototype.version.script + " needs Jplayer.swf version " + $.jPlayer.prototype.version.needFlash + " but found " // Used in: jPlayerReady() }; $.jPlayer.errorHint = { FLASH: "Check your swfPath option and that Jplayer.swf is there.", NO_SOLUTION: "Review the jPlayer options: support and supplied.", NO_SUPPORT: "Video or audio formats defined in the supplied option are missing.", URL: "Check media URL is valid.", URL_NOT_SET: "Use setMedia() to set the media URL.", VERSION: "Update jPlayer files." }; $.jPlayer.warning = { CSS_SELECTOR_COUNT: "e_css_selector_count", CSS_SELECTOR_METHOD: "e_css_selector_method", CSS_SELECTOR_STRING: "e_css_selector_string", OPTION_KEY: "e_option_key" }; $.jPlayer.warningMsg = { CSS_SELECTOR_COUNT: "The number of methodCssSelectors found did not equal one: ", CSS_SELECTOR_METHOD: "The methodName given in jPlayer('cssSelector') is not a valid jPlayer method.", CSS_SELECTOR_STRING: "The methodCssSelector given in jPlayer('cssSelector') is not a String or is empty.", OPTION_KEY: "The option requested in jPlayer('option') is undefined." }; $.jPlayer.warningHint = { CSS_SELECTOR_COUNT: "Check your css selector and the ancestor.", CSS_SELECTOR_METHOD: "Check your method name.", CSS_SELECTOR_STRING: "Check your css selector is a string.", OPTION_KEY: "Check your option name." }; })(jQuery);