app/assets/javascripts/mediaelement_rails/mediaelementplayer.js in mediaelement_rails-0.8.1 vs app/assets/javascripts/mediaelement_rails/mediaelementplayer.js in mediaelement_rails-0.8.2

- old
+ new

@@ -10,10 +10,23 @@ * License: MIT * */ if (typeof jQuery != 'undefined') { mejs.$ = jQuery; +} else if (typeof Zepto != 'undefined') { + mejs.$ = Zepto; + + // define `outerWidth` method which has not been realized in Zepto + Zepto.fn.outerWidth = function(includeMargin) { + var width = $(this).width(); + if (includeMargin) { + width += parseInt($(this).css('margin-right'), 10); + width += parseInt($(this).css('margin-left'), 10); + } + return width + } + } else if (typeof ender != 'undefined') { mejs.$ = ender; } (function ($) { @@ -58,13 +71,29 @@ loop: false, // rewind to beginning when media ends autoRewind: true, // resize to media dimensions enableAutosize: true, + + /* + * Time format to use. Default: 'mm:ss' + * Supported units: + * h: hour + * m: minute + * s: second + * f: frame count + * When using 'hh', 'mm', 'ss' or 'ff' we always display 2 digits. + * If you use 'h', 'm', 's' or 'f' we display 1 digit if possible. + * + * Example to display 75 seconds: + * Format 'mm:ss': 01:15 + * Format 'm:ss': 1:15 + * Format 'm:s': 1:15 + */ + timeFormat: '', // forces the hour marker (##:00:00) alwaysShowHours: false, - // show framecount in timecode (##:00:00:00) showTimecodeFrameCount: false, // used when showTimecodeFrameCount is set to true framesPerSecond: 25, @@ -100,13 +129,13 @@ 32, // SPACE 179 // GOOGLE play/pause button ], action: function(player, media) { if (media.paused || media.ended) { - player.play(); + media.play(); } else { - player.pause(); + media.pause(); } } }, { keys: [38], // UP @@ -215,16 +244,17 @@ // these will be reset after the MediaElement.success fires t.$media = t.$node = $(node); t.node = t.media = t.$media[0]; + if(!t.node) { + return + } + // check for existing player if (typeof t.node.player != 'undefined') { return t.node.player; - } else { - // attach player to DOM node for reference - t.node.player = t; } // try to get options from data-mejsoptions if (typeof o == 'undefined') { @@ -232,10 +262,23 @@ } // extend default options t.options = $.extend({},mejs.MepDefaults,o); + if (!t.options.timeFormat) { + // Generate the time format according to options + t.options.timeFormat = 'mm:ss'; + if (t.options.alwaysShowHours) { + t.options.timeFormat = 'hh:mm:ss'; + } + if (t.options.showTimecodeFrameCount) { + t.options.timeFormat += ':ff'; + } + } + + mejs.Utility.calculateTimeFormat(0, t.options, t.options.framesPerSecond || 25); + // unique ID t.id = 'mep_' + mejs.mepIndex++; // add to player array (for focus events) mejs.players[t.id] = t; @@ -349,10 +392,13 @@ } else { // normal way of moving it into place (doesn't work on iOS) t.container.find('.mejs-mediaelement').append(t.$media); } + + // needs to be assigned here, after iOS remap + t.node.player = t; // find parts t.controls = t.container.find('.mejs-controls'); t.layers = t.container.find('.mejs-layers'); @@ -741,10 +787,19 @@ if (!t.isFullScreen) { t.setPlayerSize(t.width, t.height); t.setControlsSize(); } }, false); + + // Only change the time format when necessary + var duration = null; + t.media.addEventListener('timeupdate',function() { + if (duration !== this.duration) { + duration = this.duration; + mejs.Utility.calculateTimeFormat(duration, t.options, t.options.framesPerSecond || 25); + } + }, false); t.container.focusout(function (e) { if( e.relatedTarget ) { //FF is working on supporting focusout https://bugzilla.mozilla.org/show_bug.cgi?id=687787 var $target = $(e.relatedTarget); if (t.keyboardAction && $target.parents('.mejs-container').length === 0) { @@ -821,11 +876,11 @@ if (typeof height != 'undefined') { t.height = height; } // detect 100% mode - use currentStyle for IE since css() doesn't return percentages - if (t.height.toString().indexOf('%') > 0 || t.$node.css('max-width') === '100%' || (t.$node[0].currentStyle && t.$node[0].currentStyle.maxWidth === '100%')) { + if (t.height.toString().indexOf('%') > 0 || (t.$node.css('max-width') !== 'none' && t.$node.css('max-width') !== 't.width') || (t.$node[0].currentStyle && t.$node[0].currentStyle.maxWidth === '100%')) { // do we have the native dimensions yet? var nativeWidth = (function() { if (t.isVideo) { if (t.media.videoWidth && t.media.videoWidth > 0) { @@ -862,11 +917,11 @@ // When we use percent, the newHeight can't be calculated so we get the container height if (isNaN(newHeight)) { newHeight = parentHeight; } - if (t.container.parent()[0].tagName.toLowerCase() === 'body') { // && t.container.siblings().count == 0) { + if (t.container.parent().length > 0 && t.container.parent()[0].tagName.toLowerCase() === 'body') { // && t.container.siblings().count == 0) { parentWidth = $(window).width(); newHeight = $(window).height(); } if ( newHeight && parentWidth ) { @@ -905,27 +960,18 @@ .width(t.width) .height(t.height); } - // special case for big play button so it doesn't go over the controls area - var playLayer = t.layers.find('.mejs-overlay-play'), - playButton = playLayer.find('.mejs-overlay-button'); - - playLayer.height(t.container.height() - t.controls.height()); - playButton.css('margin-top', '-' + (playButton.height()/2 - t.controls.height()/2).toString() + 'px' ); - }, setControlsSize: function() { var t = this, usedWidth = 0, railWidth = 0, rail = t.controls.find('.mejs-time-rail'), total = t.controls.find('.mejs-time-total'), - current = t.controls.find('.mejs-time-current'), - loaded = t.controls.find('.mejs-time-loaded'), others = rail.siblings(), lastControl = others.last(), lastControlPosition = null; // skip calculation if hidden @@ -964,19 +1010,16 @@ rail.width(railWidth); // dark space total.width(railWidth - (total.outerWidth(true) - total.width())); if (lastControl.css('position') != 'absolute') { - lastControlPosition = lastControl.position(); + lastControlPosition = lastControl.length ? lastControl.position() : null; railWidth--; } } while (lastControlPosition !== null && lastControlPosition.top > 0 && railWidth > 0); - if (t.setProgressRail) - t.setProgressRail(); - if (t.setCurrentRail) - t.setCurrentRail(); + t.container.trigger('controlsresize'); }, buildposter: function(player, controls, layers, media) { var t = this, @@ -1013,11 +1056,11 @@ var t = this, posterDiv = t.container.find('.mejs-poster'), posterImg = posterDiv.find('img'); if (posterImg.length === 0) { - posterImg = $('<img width="100%" height="100%" />').appendTo(posterDiv); + posterImg = $('<img width="100%" height="100%" alt="" />').appendTo(posterDiv); } posterImg.attr('src', url); posterDiv.css({'background-image' : 'url(' + url + ')'}); }, @@ -1125,15 +1168,16 @@ controls.find('.mejs-time-buffering').hide(); clearTimeout(media.canplayTimeout); // Clear timeout inside 'loadeddata' to prevent 'canplay' to fire twice }, false); // error handling - media.addEventListener('error',function() { + media.addEventListener('error',function(e) { + t.handleError(e); loading.hide(); - controls.find('.mejs-time-buffering').hide(); + bigPlay.hide(); error.show(); - error.find('mejs-overlay-error').html("Error loading this resource"); + error.find('.mejs-overlay-error').html("Error loading this resource"); }, false); media.addEventListener('keydown', function(e) { t.onkeydown(player, media, e); }, false); @@ -1237,10 +1281,12 @@ setSrc: function(src) { this.media.setSrc(src); }, remove: function() { var t = this, featureIndex, feature; + + t.container.prev('.mejs-offscreen').remove(); // invoke features cleanup for (featureIndex in t.options.features) { feature = t.options.features[featureIndex]; if (t['clean' + feature]) { @@ -1282,10 +1328,19 @@ }, rebuildtracks: function(){ var t = this; t.findTracks(); t.buildtracks(t, t.controls, t.layers, t.media); + }, + resetSize: function(){ + var t = this; + // webkit has trouble doing this without a delay + setTimeout(function () { + // + t.setPlayerSize(t.width, t.height); + t.setControlsSize(); + }, 50); } }; (function(){ var rwindow = /^((after|before)print|(before)?unload|hashchange|message|o(ff|n)line|page(hide|show)|popstate|resize|storage)\b/; @@ -1431,13 +1486,13 @@ }); // STOP BUTTON $.extend(MediaElementPlayer.prototype, { buildstop: function(player, controls, layers, media) { - var t = this, - stop = - $('<div class="mejs-button mejs-stop-button mejs-stop">' + + var t = this; + + $('<div class="mejs-button mejs-stop-button mejs-stop">' + '<button type="button" aria-controls="' + t.id + '" title="' + t.options.stopText + '" aria-label="' + t.options.stopText + '"></button>' + '</div>') .appendTo(controls) .click(function() { if (!media.paused) { @@ -1446,12 +1501,12 @@ if (media.currentTime > 0) { media.setCurrentTime(0); media.pause(); controls.find('.mejs-time-current').width('0px'); controls.find('.mejs-time-handle').css('left', '0px'); - controls.find('.mejs-time-float-current').html( mejs.Utility.secondsToTimeCode(0) ); - controls.find('.mejs-currenttime').html( mejs.Utility.secondsToTimeCode(0) ); + controls.find('.mejs-time-float-current').html( mejs.Utility.secondsToTimeCode(0, player.options)); + controls.find('.mejs-currenttime').html( mejs.Utility.secondsToTimeCode(0, player.options)); layers.find('.mejs-poster').show(); } }); } }); @@ -1470,19 +1525,20 @@ buildprogress: function(player, controls, layers, media) { $('<div class="mejs-time-rail">' + '<span class="mejs-time-total mejs-time-slider">' + //'<span class="mejs-offscreen">' + this.options.progessHelpText + '</span>' + - '<span class="mejs-time-buffering"></span>' + - '<span class="mejs-time-loaded"></span>' + - '<span class="mejs-time-current"></span>' + - '<span class="mejs-time-handle"></span>' + - '<span class="mejs-time-float">' + - '<span class="mejs-time-float-current">00:00</span>' + - '<span class="mejs-time-float-corner"></span>' + + '<span class="mejs-time-buffering"></span>' + + '<span class="mejs-time-loaded"></span>' + + '<span class="mejs-time-current"></span>' + + '<span class="mejs-time-handle"></span>' + + '<span class="mejs-time-float">' + + '<span class="mejs-time-float-current">00:00</span>' + + '<span class="mejs-time-float-corner"></span>' + + '</span>' + '</span>' + - '</div>') + '</div>') .appendTo(controls); controls.find('.mejs-time-buffering').hide(); var t = this, @@ -1501,13 +1557,15 @@ newTime = 0, pos = 0, x; // mouse or touch position relative to the object - if (e.originalEvent.changedTouches) { + if (e.originalEvent && e.originalEvent.changedTouches) { x = e.originalEvent.changedTouches[0].pageX; - }else{ + } else if (e.changedTouches) { // for Zepto + x = e.changedTouches[0].pageX; + } else { x = e.pageX; } if (media.duration) { if (x < offset.left) { @@ -1526,11 +1584,11 @@ } // position floating time box if (!mejs.MediaFeatures.hasTouch) { timefloat.css('left', pos); - timefloatcurrent.html( mejs.Utility.secondsToTimeCode(newTime) ); + timefloatcurrent.html( mejs.Utility.secondsToTimeCode(newTime, player.options) ); timefloat.show(); } } }, mouseIsDown = false, @@ -1541,11 +1599,11 @@ // Accessibility for slider var updateSlider = function (e) { var seconds = media.currentTime, timeSliderText = mejs.i18n.t('Time Slider'), - time = mejs.Utility.secondsToTimeCode(seconds), + time = mejs.Utility.secondsToTimeCode(seconds, player.options), duration = media.duration; slider.attr({ 'aria-label': timeSliderText, 'aria-valuemin': 0, @@ -1676,10 +1734,14 @@ player.setProgressRail(e); player.setCurrentRail(e); updateSlider(e); }, false); + t.container.on('controlsresize', function() { + player.setProgressRail(); + player.setCurrentRail(); + }); // store for later use t.loaded = loaded; t.total = total; t.current = current; @@ -1692,12 +1754,12 @@ target = (e !== undefined) ? e.target : t.media, percent = null; // newest HTML5 spec has buffered array (FF4, Webkit) if (target && target.buffered && target.buffered.length > 0 && target.buffered.end && target.duration) { - // TODO: account for a real array with multiple values (only Firefox 4 has this so far) - percent = target.buffered.end(0) / target.duration; + // account for a real array with multiple values - always read the end of the last buffer + percent = target.buffered.end(target.buffered.length - 1) / target.duration; } // Some browsers (e.g., FF3.6 and Safari 5) cannot calculate target.bufferered.end() // to be anything other than 0. If the byte count is available we use this instead. // Browsers that support the else if do not seem to have the bufferedBytes value and // should skip to there. Tested in Safari 5, Webkit head, FF3.6, Chrome 6, IE 7/8. @@ -1736,10 +1798,11 @@ } } }); })(mejs.$); + (function($) { // options $.extend(mejs.MepDefaults, { duration: -1, @@ -1752,12 +1815,11 @@ buildcurrent: function(player, controls, layers, media) { var t = this; $('<div class="mejs-time" role="timer" aria-live="off">' + '<span class="mejs-currenttime">' + - (player.options.alwaysShowHours ? '00:' : '') + - (player.options.showTimecodeFrameCount? '00:00:00':'00:00') + + mejs.Utility.secondsToTimeCode(0, player.options) + '</span>'+ '</div>') .appendTo(controls); t.currenttime = t.controls.find('.mejs-currenttime'); @@ -1772,27 +1834,21 @@ var t = this; if (controls.children().last().find('.mejs-currenttime').length > 0) { $(t.options.timeAndDurationSeparator + '<span class="mejs-duration">' + - (t.options.duration > 0 ? - mejs.Utility.secondsToTimeCode(t.options.duration, t.options.alwaysShowHours || t.media.duration > 3600, t.options.showTimecodeFrameCount, t.options.framesPerSecond || 25) : - ((player.options.alwaysShowHours ? '00:' : '') + (player.options.showTimecodeFrameCount? '00:00:00':'00:00')) - ) + + mejs.Utility.secondsToTimeCode(t.options.duration, t.options) + '</span>') .appendTo(controls.find('.mejs-time')); } else { // add class to current time controls.find('.mejs-currenttime').parent().addClass('mejs-currenttime-container'); $('<div class="mejs-time mejs-duration-container">'+ '<span class="mejs-duration">' + - (t.options.duration > 0 ? - mejs.Utility.secondsToTimeCode(t.options.duration, t.options.alwaysShowHours || t.media.duration > 3600, t.options.showTimecodeFrameCount, t.options.framesPerSecond || 25) : - ((player.options.alwaysShowHours ? '00:' : '') + (player.options.showTimecodeFrameCount? '00:00:00':'00:00')) - ) + + mejs.Utility.secondsToTimeCode(t.options.duration, t.options) + '</span>' + '</div>') .appendTo(controls); } @@ -1805,22 +1861,22 @@ updateCurrent: function() { var t = this; if (t.currenttime) { - t.currenttime.html(mejs.Utility.secondsToTimeCode(t.media.currentTime, t.options.alwaysShowHours || t.media.duration > 3600, t.options.showTimecodeFrameCount, t.options.framesPerSecond || 25)); + t.currenttime.html(mejs.Utility.secondsToTimeCode(t.media.currentTime, t.options)); } }, updateDuration: function() { var t = this; //Toggle the long video class if the video is longer than an hour. t.container.toggleClass("mejs-long-video", t.media.duration > 3600); if (t.durationD && (t.options.duration > 0 || t.media.duration)) { - t.durationD.html(mejs.Utility.secondsToTimeCode(t.options.duration > 0 ? t.options.duration : t.media.duration, t.options.alwaysShowHours, t.options.showTimecodeFrameCount, t.options.framesPerSecond || 25)); + t.durationD.html(mejs.Utility.secondsToTimeCode(t.options.duration > 0 ? t.options.duration : t.media.duration, t.options)); } } }); })(mejs.$); @@ -1896,12 +1952,14 @@ volume = Math.min(volume,1); // ajust mute button style if (volume === 0) { mute.removeClass('mejs-mute').addClass('mejs-unmute'); + mute.children('button').attr('title', mejs.i18n.t('Unmute')).attr('aria-label', mejs.i18n.t('Unmute')); } else { mute.removeClass('mejs-unmute').addClass('mejs-mute'); + mute.children('button').attr('title', mejs.i18n.t('Mute')).attr('aria-label', mejs.i18n.t('Mute')); } // top/left of full size volume slider background var totalPosition = volumeTotal.position(); // position slider @@ -1943,11 +2001,10 @@ // calculate the new volume based on the moust position if (mode === 'vertical') { var railHeight = volumeTotal.height(), - totalTop = parseInt(volumeTotal.css('top').replace(/px/,''),10), newY = e.pageY - totalOffset.top; volume = (railHeight - newY) / railHeight; // the controls just hide themselves (usually when mouse moves too far up) @@ -2076,29 +2133,29 @@ mute.removeClass('mejs-unmute').addClass('mejs-mute'); } } updateVolumeSlider(e); }, false); - - if (t.container.is(':visible')) { - // set initial volume - positionVolumeHandle(player.options.startVolume); - - // mutes the media and sets the volume icon muted if the initial volume is set to 0 - if (player.options.startVolume === 0) { - media.setMuted(true); - } - - // shim gets the startvolume as a parameter, but we have to set it on the native <video> and <audio> elements - if (media.pluginType === 'native') { - media.setVolume(player.options.startVolume); - } + + // mutes the media and sets the volume icon muted if the initial volume is set to 0 + if (player.options.startVolume === 0) { + media.setMuted(true); } + + // shim gets the startvolume as a parameter, but we have to set it on the native <video> and <audio> elements + if (media.pluginType === 'native') { + media.setVolume(player.options.startVolume); + } + + t.container.on('controlsresize', function() { + positionVolumeHandle(media.volume); + }); } }); })(mejs.$); + (function($) { $.extend(mejs.MepDefaults, { usePluginFullScreen: true, newWindowCallback: function() { return '';}, @@ -2141,13 +2198,10 @@ player.globalBind(mejs.MediaFeatures.fullScreenEventName, func); } var t = this, - normalHeight = 0, - normalWidth = 0, - container = player.container, fullscreenBtn = $('<div class="mejs-button mejs-fullscreen-button">' + '<button type="button" aria-controls="' + t.id + '" title="' + t.options.fullscreenText + '" aria-label="' + t.options.fullscreenText + '"></button>' + '</div>') .appendTo(controls); @@ -2373,10 +2427,13 @@ t.globalBind('keydown',function (e) { if (((mejs.MediaFeatures.hasTrueNativeFullScreen && mejs.MediaFeatures.isFullScreen()) || t.isFullScreen) && e.keyCode == 27) { player.exitFullScreen(); } }); + + t.normalHeight = 0; + t.normalWidth = 0; }, cleanfullscreen: function(player) { player.exitFullScreen(); @@ -2397,12 +2454,12 @@ // set it to not show scroll bars so 100% will work $(document.documentElement).addClass('mejs-fullscreen'); // store sizing - normalHeight = t.container.height(); - normalWidth = t.container.width(); + t.normalHeight = t.container.height(); + t.normalWidth = t.container.width(); // attempt to do true fullscreen (Safari 5.1 and Firefox Nightly only for now) if (t.media.pluginType === 'native') { if (mejs.MediaFeatures.hasTrueNativeFullScreen) { @@ -2413,30 +2470,41 @@ // sometimes exiting from fullscreen doesn't work // notably in Chrome <iframe>. Fixed in version 17 setTimeout(function checkFullscreen() { if (t.isNativeFullScreen) { - var zoomMultiplier = window["devicePixelRatio"] || 1; + var zoomMultiplier = window["devicePixelRatio"] || 1, // Use a percent error margin since devicePixelRatio is a float and not exact. - var percentErrorMargin = 0.002; // 0.2% - var windowWidth = zoomMultiplier * $(window).width(); - var screenWidth = screen.width; - var absDiff = Math.abs(screenWidth - windowWidth); - var marginError = screenWidth * percentErrorMargin; + percentErrorMargin = 0.002, // 0.2% + windowWidth = zoomMultiplier * $(window).width(), + screenWidth = screen.width, + // ** 13twelve + // Screen width is sort of useless: http://www.quirksmode.org/blog/archives/2013/11/screenwidth_is.html + // My rMBP ignores devicePixelRatio when returning the values, so fullscreen would always fail the "suddenly not fullscreen" test + // Theory: the gap between reported values should give us an indication of browser behavior with screen.width and devicePixelRatio + zoomedWindowWidth = zoomMultiplier * windowWidth; + + if (Math.abs(screenWidth-windowWidth) > Math.abs(screenWidth-zoomedWindowWidth)) { + // screen.width is likely true pixels, not CSS pixels, so we need to use the zoomed window width for comparison + windowWidth = zoomedWindowWidth; + } + // ** / 13twelve + var absDiff = Math.abs(screenWidth - windowWidth), + marginError = screenWidth * percentErrorMargin; + // check if the video is suddenly not really fullscreen if (absDiff > marginError) { // manually exit t.exitFullScreen(); } else { // test again setTimeout(checkFullscreen, 500); } } - - - }, 500); + + }, 1000); } } else if (mejs.MediaFeatures.hasSemiNativeFullScreen) { t.media.webkitEnterFullscreen(); return; @@ -2514,10 +2582,12 @@ t.setControlsSize(); t.isFullScreen = true; t.container.find('.mejs-captions-text').css('font-size', screen.width / t.width * 1.00 * 100 + '%'); t.container.find('.mejs-captions-position').css('bottom', '45px'); + + t.container.trigger('enteredfullscreen'); }, exitFullScreen: function() { var t = this; @@ -2540,49 +2610,52 @@ // restore scroll bars to document $(document.documentElement).removeClass('mejs-fullscreen'); t.container .removeClass('mejs-container-fullscreen') - .width(normalWidth) - .height(normalHeight); - //.css({position: '', left: '', top: '', right: '', bottom: '', overflow: 'inherit', width: normalWidth + 'px', height: normalHeight + 'px', 'z-index': 1}); + .width(t.normalWidth) + .height(t.normalHeight); if (t.media.pluginType === 'native') { t.$media - .width(normalWidth) - .height(normalHeight); + .width(t.normalWidth) + .height(t.normalHeight); } else { t.container.find('.mejs-shim') - .width(normalWidth) - .height(normalHeight); + .width(t.normalWidth) + .height(t.normalHeight); - t.media.setVideoSize(normalWidth, normalHeight); + t.media.setVideoSize(t.normalWidth, t.normalHeight); } t.layers.children('div') - .width(normalWidth) - .height(normalHeight); + .width(t.normalWidth) + .height(t.normalHeight); t.fullscreenBtn .removeClass('mejs-unfullscreen') .addClass('mejs-fullscreen'); t.setControlsSize(); t.isFullScreen = false; t.container.find('.mejs-captions-text').css('font-size',''); t.container.find('.mejs-captions-position').css('bottom', ''); + + t.container.trigger('exitedfullscreen'); } }); })(mejs.$); (function($) { // Speed $.extend(mejs.MepDefaults, { + // We also support to pass object like this: + // [{name: 'Slow', value: '0.75'}, {name: 'Normal', value: '1.00'}, ...] speeds: ['2.00', '1.50', '1.25', '1.00', '0.75'], defaultSpeed: '1.00', speedChar: 'x' @@ -2597,57 +2670,94 @@ if (t.media.pluginType == 'native') { var speedButton = null, speedSelector = null, playbackSpeed = null, - html = '<div class="mejs-button mejs-speed-button">' + - '<button type="button">' + t.options.defaultSpeed + t.options.speedChar + '</button>' + - '<div class="mejs-speed-selector">' + - '<ul>'; - - if ($.inArray(t.options.defaultSpeed, t.options.speeds) === -1) { - t.options.speeds.push(t.options.defaultSpeed); + inputId = null; + + var speeds = []; + var defaultInArray = false; + for (var i=0, len=t.options.speeds.length; i < len; i++) { + var s = t.options.speeds[i]; + if (typeof(s) === 'string'){ + speeds.push({ + name: s + t.options.speedChar, + value: s + }); + if(s === t.options.defaultSpeed) { + defaultInArray = true; + } + } + else { + speeds.push(s); + if(s.value === t.options.defaultSpeed) { + defaultInArray = true; + } + } } - t.options.speeds.sort(function(a, b) { - return parseFloat(b) - parseFloat(a); + if (!defaultInArray) { + speeds.push({ + name: t.options.defaultSpeed + t.options.speedChar, + value: t.options.defaultSpeed + }); + } + + speeds.sort(function(a, b) { + return parseFloat(b.value) - parseFloat(a.value); }); - for (var i = 0, il = t.options.speeds.length; i<il; i++) { + var getSpeedNameFromValue = function(value) { + for(i=0,len=speeds.length; i <len; i++) { + if (speeds[i].value === value) { + return speeds[i].name; + } + } + }; + + var html = '<div class="mejs-button mejs-speed-button">' + + '<button type="button">' + getSpeedNameFromValue(t.options.defaultSpeed) + '</button>' + + '<div class="mejs-speed-selector">' + + '<ul>'; + + for (i = 0, il = speeds.length; i<il; i++) { + inputId = t.id + '-speed-' + speeds[i].value; html += '<li>' + '<input type="radio" name="speed" ' + - 'value="' + t.options.speeds[i] + '" ' + - 'id="' + t.options.speeds[i] + '" ' + - (t.options.speeds[i] == t.options.defaultSpeed ? ' checked' : '') + + 'value="' + speeds[i].value + '" ' + + 'id="' + inputId + '" ' + + (speeds[i].value === t.options.defaultSpeed ? ' checked' : '') + ' />' + - '<label for="' + t.options.speeds[i] + '" ' + - (t.options.speeds[i] == t.options.defaultSpeed ? ' class="mejs-speed-selected"' : '') + - '>' + t.options.speeds[i] + t.options.speedChar + '</label>' + + '<label for="' + inputId + '" ' + + (speeds[i].value === t.options.defaultSpeed ? ' class="mejs-speed-selected"' : '') + + '>' + speeds[i].name + '</label>' + '</li>'; } html += '</ul></div></div>'; speedButton = $(html).appendTo(controls); - speedSelector = speedButton.find('.mejs-speed-selector'); + speedSelector = speedButton.find('.mejs-speed-selector'); - playbackspeed = t.options.defaultSpeed; + playbackSpeed = t.options.defaultSpeed; speedSelector .on('click', 'input[type="radio"]', function() { var newSpeed = $(this).attr('value'); - playbackspeed = newSpeed; + playbackSpeed = newSpeed; media.playbackRate = parseFloat(newSpeed); - speedButton.find('button').html(newSpeed + t.options.speedChar); + speedButton.find('button').html(getSpeedNameFromValue(newSpeed)); speedButton.find('.mejs-speed-selected').removeClass('mejs-speed-selected'); speedButton.find('input[type="radio"]:checked').next().addClass('mejs-speed-selected'); }); - - speedSelector - .height( - speedButton.find('.mejs-speed-selector ul').outerHeight(true) + - speedButton.find('.mejs-speed-translations').outerHeight(true)) - .css('top', (-1 * speedSelector.height()) + 'px'); + speedButton + .one( 'mouseenter focusin', function() { + speedSelector + .height( + speedButton.find('.mejs-speed-selector ul').outerHeight(true) + + speedButton.find('.mejs-speed-translations').outerHeight(true)) + .css('top', (-1 * speedSelector.height()) + 'px'); + }); } } }); })(mejs.$); @@ -2659,10 +2769,14 @@ // this will automatically turn on a <track> startLanguage: '', tracksText: mejs.i18n.t('Captions/Subtitles'), + // By default, no WAI-ARIA live region - don't make a + // screen reader speak captions over an audio track. + tracksAriaLive: false, + // option to remove the [cc] button when no <track kind="subtitles"> are present hideCaptionsButtonWhenEmpty: true, // If true and we only have one track, change captions to popup toggleCaptionsButtonWhenOnlyOne: false, @@ -2686,12 +2800,13 @@ buildtracks: function(player, controls, layers, media) { if (player.tracks.length === 0) return; var t = this, - i, - options = ''; + attr = t.options.tracksAriaLive ? + 'role="log" aria-live="assertive" aria-atomic="false"' : '', + i; if (t.domNode.textTracks) { // if browser will do native captions, prefer mejs captions, loop through tracks and hide for (i = t.domNode.textTracks.length - 1; i >= 0; i--) { t.domNode.textTracks[i].mode = "hidden"; } @@ -2699,11 +2814,12 @@ t.cleartracks(player, controls, layers, media); player.chapters = $('<div class="mejs-chapters mejs-layer"></div>') .prependTo(layers).hide(); player.captions = - $('<div class="mejs-captions-layer mejs-layer"><div class="mejs-captions-position mejs-captions-position-hover" role="log" aria-live="assertive" aria-atomic="false"><span class="mejs-captions-text"></span></div></div>') + $('<div class="mejs-captions-layer mejs-layer"><div class="mejs-captions-position mejs-captions-position-hover" ' + + attr + '><span class="mejs-captions-text"></span></div></div>') .prependTo(layers).hide(); player.captionsText = player.captions.find('.mejs-captions-text'); player.captionsButton = $('<div class="mejs-button mejs-captions-button">'+ '<button type="button" aria-controls="' + t.id + '" title="' + t.options.tracksText + '" aria-label="' + t.options.tracksText + '"></button>'+ @@ -2819,10 +2935,14 @@ $(this).css('display','block'); }); } }); + t.container.on('controlsresize', function() { + t.adjustLanguageBox(); + }); + // check for autoplay if (player.node.getAttribute('autoplay') !== null) { player.chapters.css('visibility','hidden'); } }, @@ -2870,12 +2990,10 @@ track = t.tracks[index], after = function() { track.isLoaded = true; - // create button - //t.addTrackButton(track.srclang); t.enableTrackButton(track.srclang, track.label); t.loadNextTrack(); }; @@ -2906,10 +3024,11 @@ if (track.kind == 'slides') { t.setupSlides(track); } }, error: function() { + t.removeTrackButton(track.srclang); t.loadNextTrack(); } }); }, @@ -2931,10 +3050,18 @@ $('#' + t.id + '_captions_' + lang).prop('checked', true).trigger('click'); } t.adjustLanguageBox(); }, + + removeTrackButton: function(lang) { + var t = this; + + t.captionsButton.find('input[value=' + lang + ']').closest('li').remove(); + + t.adjustLanguageBox(); + }, addTrackButton: function(lang, label) { var t = this; if (label === '') { label = mejs.language.codes[lang] || lang; @@ -2968,11 +3095,11 @@ hasSubtitles = false; // check if any subtitles if (t.options.hideCaptionsButtonWhenEmpty) { for (i=0; i<t.tracks.length; i++) { - if (t.tracks[i].kind == 'subtitles') { + if (t.tracks[i].kind == 'subtitles' && t.tracks[i].isLoaded) { hasSubtitles = true; break; } } @@ -3114,11 +3241,11 @@ t.chapters.append( $( '<div class="mejs-chapter" rel="' + chapters.entries.times[i].start + '" style="left: ' + usedPercent.toString() + '%;width: ' + percent.toString() + '%;">' + '<div class="mejs-chapter-block' + ((i==chapters.entries.times.length-1) ? ' mejs-chapter-block-last' : '') + '">' + '<span class="ch-title">' + chapters.entries.text[i] + '</span>' + - '<span class="ch-time">' + mejs.Utility.secondsToTimeCode(chapters.entries.times[i].start) + '&ndash;' + mejs.Utility.secondsToTimeCode(chapters.entries.times[i].stop) + '</span>' + + '<span class="ch-time">' + mejs.Utility.secondsToTimeCode(chapters.entries.times[i].start, t.options) + '&ndash;' + mejs.Utility.secondsToTimeCode(chapters.entries.times[i].stop, t.options) + '</span>' + '</div>' + '</div>')); usedPercent += percent; } @@ -3264,12 +3391,10 @@ i = 0, container = trackText.children("div").eq(0), lines = container.find("p"), styleNode = trackText.find("#" + container.attr("style")), styles, - begin, - end, text, entries = {text:[], times:[]}; if (styleNode.length) { @@ -3534,9 +3659,41 @@ } }); })(mejs.$); +(function($) { + // skip back button + + $.extend(mejs.MepDefaults, { + skipBackInterval: 30, + // %1 will be replaced with skipBackInterval in this string + skipBackText: mejs.i18n.t('Skip back %1 seconds') + }); + + $.extend(MediaElementPlayer.prototype, { + buildskipback: function(player, controls, layers, media) { + var + t = this, + // Replace %1 with skip back interval + backText = t.options.skipBackText.replace('%1', t.options.skipBackInterval), + // create the loop button + loop = + $('<div class="mejs-button mejs-skip-back-button">' + + '<button type="button" aria-controls="' + t.id + '" title="' + backText + '" aria-label="' + backText + '">' + t.options.skipBackInterval + '</button>' + + '</div>') + // append it to the toolbar + .appendTo(controls) + // add a click toggle event + .click(function() { + media.setCurrentTime(Math.max(media.currentTime - t.options.skipBackInterval, 0)); + $(this).find('button').blur(); + }); + } + }); + +})(mejs.$); + /** * Postroll plugin */ (function($) { \ No newline at end of file