mediaelement
Version:
One file. Any browser. Same UI.
684 lines (614 loc) • 20.9 kB
JavaScript
;
import document from 'global/document';
import {config} from '../player';
import MediaElementPlayer from '../player';
import i18n from '../core/i18n';
import {IS_IOS, IS_ANDROID, SUPPORT_PASSIVE_EVENT} from '../utils/constants';
import {secondsToTimeCode} from '../utils/time';
import {offset, addClass, removeClass, hasClass} from '../utils/dom';
/**
* Progress/loaded bar
*
* This feature creates a progress bar with a slider in the control bar, and updates it based on native events.
*/
// Feature configuration
Object.assign(config, {
/**
* Enable tooltip that shows time in progress bar
* @type {Boolean}
*/
enableProgressTooltip: true,
/**
* Enable smooth behavior when hovering progress bar
* @type {Boolean}
*/
useSmoothHover: true,
/**
* If set to `true`, the `Live Broadcast` message will be displayed no matter if `duration` is a valid number
*/
forceLive: false
});
Object.assign(MediaElementPlayer.prototype, {
/**
* Feature constructor.
*
* Always has to be prefixed with `build` and the name that will be used in MepDefaults.features list
* @param {MediaElementPlayer} player
* @param {HTMLElement} controls
* @param {HTMLElement} layers
* @param {HTMLElement} media
*/
buildprogress (player, controls, layers, media) {
let
lastKeyPressTime = 0,
mouseIsDown = false,
startedPaused = false
;
const
t = this,
autoRewindInitial = player.options.autoRewind,
tooltip = player.options.enableProgressTooltip ?
`<span class="${t.options.classPrefix}time-float">` +
`<span class="${t.options.classPrefix}time-float-current">00:00</span>` +
`<span class="${t.options.classPrefix}time-float-corner"></span>` +
`</span>` : '',
rail = document.createElement('div')
;
rail.className = `${t.options.classPrefix}time-rail`;
rail.innerHTML = `<span class="${t.options.classPrefix}time-total ${t.options.classPrefix}time-slider">` +
`<span class="${t.options.classPrefix}time-buffering"></span>` +
`<span class="${t.options.classPrefix}time-loaded"></span>` +
`<span class="${t.options.classPrefix}time-current"></span>` +
`<span class="${t.options.classPrefix}time-hovered no-hover"></span>` +
`<span class="${t.options.classPrefix}time-handle"><span class="${t.options.classPrefix}time-handle-content"></span></span>` +
`${tooltip}` +
`</span>`;
t.addControlElement(rail, 'progress');
t.options.keyActions.push({
keys: [
37, // LEFT
227 // Google TV rewind
],
action: (player) => {
if (!isNaN(player.duration) && player.duration > 0) {
if (player.isVideo) {
player.showControls();
player.startControlsTimer();
}
var timeSlider = player.getElement(player.container).querySelector(`.${t.options.classPrefix}time-total`);
if (timeSlider) {
timeSlider.focus();
}
// 5%
const newTime = Math.max(player.currentTime - player.options.defaultSeekBackwardInterval(player), 0);
// pause to track current time
if (!player.paused) {
player.pause();
}
// make sure time is updated after 'pause' event is processed
setTimeout(function() {
player.setCurrentTime(newTime, true);
}, 0);
// start again to track new time
setTimeout(function() {
player.play();
}, 0);
}
}
},
{
keys: [
39, // RIGHT
228 // Google TV forward
],
action: (player) => {
if (!isNaN(player.duration) && player.duration > 0) {
if (player.isVideo) {
player.showControls();
player.startControlsTimer();
}
var timeSlider = player.getElement(player.container).querySelector(`.${t.options.classPrefix}time-total`);
if (timeSlider) {
timeSlider.focus();
}
// 5%
const newTime = Math.min(player.currentTime + player.options.defaultSeekForwardInterval(player), player.duration);
// pause to track current time
if (!player.paused) {
player.pause();
}
// make sure time is updated after 'pause' event is processed
setTimeout(function() {
player.setCurrentTime(newTime, true);
}, 0);
// start again to track new time
setTimeout(function() {
player.play();
}, 0);
}
}
});
t.rail = controls.querySelector(`.${t.options.classPrefix}time-rail`);
t.total = controls.querySelector(`.${t.options.classPrefix}time-total`);
t.loaded = controls.querySelector(`.${t.options.classPrefix}time-loaded`);
t.current = controls.querySelector(`.${t.options.classPrefix}time-current`);
t.handle = controls.querySelector(`.${t.options.classPrefix}time-handle`);
t.timefloat = controls.querySelector(`.${t.options.classPrefix}time-float`);
t.timefloatcurrent = controls.querySelector(`.${t.options.classPrefix}time-float-current`);
t.slider = controls.querySelector(`.${t.options.classPrefix}time-slider`);
t.hovered = controls.querySelector(`.${t.options.classPrefix}time-hovered`);
t.buffer = controls.querySelector(`.${t.options.classPrefix}time-buffering`);
t.newTime = 0;
t.forcedHandlePause = false;
t.setTransformStyle = (element, value) => {
element.style.transform = value;
element.style.webkitTransform = value;
element.style.MozTransform = value;
element.style.msTransform = value;
element.style.OTransform = value;
};
t.buffer.style.display = 'none';
/**
*
* @private
* @param {Event} e
*/
let
handleMouseMove = (e) => {
const
totalStyles = getComputedStyle(t.total),
offsetStyles = offset(t.total),
width = t.total.offsetWidth,
transform = (() => {
if (totalStyles.webkitTransform !== undefined) {
return 'webkitTransform';
} else if (totalStyles.mozTransform !== undefined) {
return 'mozTransform ';
} else if (totalStyles.oTransform !== undefined) {
return 'oTransform';
} else if (totalStyles.msTransform !== undefined) {
return 'msTransform';
} else {
return 'transform';
}
})(),
cssMatrix = (() => {
if ('WebKitCSSMatrix' in window) {
return 'WebKitCSSMatrix';
} else if ('MSCSSMatrix' in window) {
return 'MSCSSMatrix';
} else if ('CSSMatrix' in window) {
return 'CSSMatrix';
}
})()
;
let
percentage = 0,
leftPos = 0,
pos = 0,
x
;
// mouse or touch position relative to the object
if (e.originalEvent && e.originalEvent.changedTouches) {
x = e.originalEvent.changedTouches[0].pageX;
} else if (e.changedTouches) { // for Zepto
x = e.changedTouches[0].pageX;
} else {
x = e.pageX;
}
if (t.getDuration()) {
if (x < offsetStyles.left) {
x = offsetStyles.left;
} else if (x > width + offsetStyles.left) {
x = width + offsetStyles.left;
}
pos = x - offsetStyles.left;
percentage = (pos / width);
t.newTime = percentage * t.getDuration();
// fake seek to where the mouse is
if (mouseIsDown && t.getCurrentTime() !== null && t.newTime.toFixed(4) !== t.getCurrentTime().toFixed(4)) {
t.setCurrentRailHandle(t.newTime);
t.updateCurrent(t.newTime);
}
// position floating time box
if (!IS_IOS && !IS_ANDROID) {
if (pos < 0){
pos = 0;
}
if (t.options.useSmoothHover && cssMatrix !== null && typeof window[cssMatrix] !== 'undefined') {
const
matrix = new window[cssMatrix](getComputedStyle(t.handle)[transform]),
handleLocation = matrix.m41,
hoverScaleX = pos/parseFloat(getComputedStyle(t.total).width) - handleLocation/parseFloat(getComputedStyle(t.total).width)
;
t.hovered.style.left = `${handleLocation}px`;
t.setTransformStyle(t.hovered,`scaleX(${hoverScaleX})`);
t.hovered.setAttribute('pos', pos);
if (hoverScaleX >= 0) {
removeClass(t.hovered, 'negative');
} else {
addClass(t.hovered, 'negative');
}
}
// Add correct position of tooltip if rail is 100%
if (t.timefloat) {
const
half = t.timefloat.offsetWidth / 2,
offsetContainer = mejs.Utils.offset(t.getElement(t.container)),
tooltipStyles = getComputedStyle(t.timefloat)
;
if ((x - offsetContainer.left) < t.timefloat.offsetWidth) {
leftPos = half;
} else if ((x - offsetContainer.left) >= t.getElement(t.container).offsetWidth - half) {
leftPos = t.total.offsetWidth - half;
} else {
leftPos = pos;
}
if (hasClass(t.getElement(t.container), `${t.options.classPrefix}long-video`)) {
leftPos += parseFloat(tooltipStyles.marginLeft)/2 + t.timefloat.offsetWidth/2;
}
t.timefloat.style.left = `${leftPos}px`;
t.timefloatcurrent.innerHTML = secondsToTimeCode(t.newTime, player.options.alwaysShowHours, player.options.showTimecodeFrameCount, player.options.framesPerSecond, player.options.secondsDecimalLength, player.options.timeFormat);
t.timefloat.style.display = 'block';
}
}
} else if (!IS_IOS && !IS_ANDROID && t.timefloat) {
leftPos = t.timefloat.offsetWidth + width >= t.getElement(t.container).offsetWidth ? t.timefloat.offsetWidth / 2 : 0;
t.timefloat.style.left = leftPos + 'px';
t.timefloat.style.left = `${leftPos}px`;
t.timefloat.style.display = 'block';
}
},
/**
* Update elements in progress bar for accessibility purposes only when player is paused.
*
* This is to avoid attempts to repeat the time over and over again when media is playing.
* @private
*/
updateSlider = () => {
const
seconds = t.getCurrentTime(),
timeSliderText = i18n.t('mejs.time-slider'),
time = secondsToTimeCode(seconds, player.options.alwaysShowHours, player.options.showTimecodeFrameCount, player.options.framesPerSecond, player.options.secondsDecimalLength, player.options.timeFormat),
duration = t.getDuration()
;
t.slider.setAttribute('role', 'slider');
t.slider.tabIndex = 0;
if (media.paused) {
t.slider.setAttribute('aria-label', timeSliderText);
t.slider.setAttribute('aria-valuemin', 0);
t.slider.setAttribute('aria-valuemax', isNaN(duration) ? 0 : duration);
t.slider.setAttribute('aria-valuenow', seconds);
t.slider.setAttribute('aria-valuetext', time);
} else {
t.slider.removeAttribute('aria-label');
t.slider.removeAttribute('aria-valuemin');
t.slider.removeAttribute('aria-valuemax');
t.slider.removeAttribute('aria-valuenow');
t.slider.removeAttribute('aria-valuetext');
}
},
/**
*
* @private
*/
restartPlayer = () => {
if (new Date() - lastKeyPressTime >= 1000) {
t.play();
}
},
handleMouseup = () => {
if (mouseIsDown && t.getCurrentTime() !== null && t.newTime.toFixed(4) !== t.getCurrentTime().toFixed(4)) {
t.setCurrentTime(t.newTime, true);
t.setCurrentRailHandle(t.newTime);
t.updateCurrent(t.newTime);
}
if (t.forcedHandlePause) {
t.slider.focus();
t.play();
}
t.forcedHandlePause = false;
}
;
// Events
t.slider.addEventListener('focus', () => {
player.options.autoRewind = false;
});
t.slider.addEventListener('blur', () => {
player.options.autoRewind = autoRewindInitial;
});
t.slider.addEventListener('keydown', (e) => {
if ((new Date() - lastKeyPressTime) >= 1000) {
startedPaused = t.paused;
}
if (t.options.enableKeyboard && t.options.keyActions.length) {
const
keyCode = e.which || e.keyCode || 0,
duration = t.getDuration(),
seekForward = player.options.defaultSeekForwardInterval(media),
seekBackward = player.options.defaultSeekBackwardInterval(media)
;
let seekTime = t.getCurrentTime();
const volume = t.getElement(t.container).querySelector(`.${t.options.classPrefix }volume-slider`);
if (keyCode === 38 || keyCode === 40) {
if (volume) {
volume.style.display = 'block';
}
if (t.isVideo) {
t.showControls();
t.startControlsTimer();
}
const
newVolume = keyCode === 38 ? Math.min(t.volume + 0.1, 1) : Math.max(t.volume - 0.1, 0),
mutePlayer = newVolume <= 0
;
t.setVolume(newVolume);
t.setMuted(mutePlayer);
return;
} else {
if (volume) {
volume.style.display = 'none';
}
}
switch (keyCode) {
case 37: // left
if (t.getDuration() !== Infinity) {
seekTime -= seekBackward;
}
break;
case 39: // Right
if (t.getDuration() !== Infinity) {
seekTime += seekForward;
}
break;
case 36: // Home
seekTime = 0;
break;
case 35: // end
seekTime = duration;
break;
case 13: // enter
if (t.paused) {
t.play();
} else {
t.pause();
}
return;
default:
return;
}
seekTime = seekTime < 0 || isNaN(seekTime) ? 0 : (seekTime >= duration ? duration : Math.floor(seekTime));
lastKeyPressTime = new Date();
if (!startedPaused) {
player.pause();
}
// make sure time is updated after 'pause' event is processed
setTimeout(function() {
t.setCurrentTime(seekTime, true);
}, 0);
if (seekTime < t.getDuration() && !startedPaused) {
setTimeout(restartPlayer, 1100);
}
player.showControls();
e.preventDefault();
e.stopPropagation();
}
});
const events = ['mousedown', 'touchstart'];
// Required to manipulate mouse movements that require drag 'n' drop properly
t.slider.addEventListener('dragstart', () => false);
for (let i = 0, total = events.length; i < total; i++) {
t.slider.addEventListener(events[i], (e) => {
t.forcedHandlePause = false;
if (t.getDuration() !== Infinity) {
// only handle left clicks or touch
if (e.which === 1 || e.which === 0) {
if (!t.paused) {
t.pause();
t.forcedHandlePause = true;
}
mouseIsDown = true;
handleMouseMove(e);
const endEvents = ['mouseup', 'touchend'];
for (let j = 0, totalEvents = endEvents.length; j < totalEvents; j++) {
t.getElement(t.container).addEventListener(endEvents[j], (event) => {
const target = event.target;
if (target === t.slider || target.closest(`.${t.options.classPrefix}time-slider`)) {
handleMouseMove(event);
}
});
}
t.globalBind('mouseup.dur touchend.dur', () => {
handleMouseup();
mouseIsDown = false;
if (t.timefloat) {
t.timefloat.style.display = 'none';
}
});
}
}
}, (SUPPORT_PASSIVE_EVENT && events[i] === 'touchstart') ? { passive: true } : false);
}
t.slider.addEventListener('mouseenter', (e) => {
if (e.target === t.slider && t.getDuration() !== Infinity) {
t.getElement(t.container).addEventListener('mousemove', (event) => {
const target = event.target;
if (target === t.slider || target.closest(`.${t.options.classPrefix}time-slider`)) {
handleMouseMove(event);
}
});
if (t.timefloat && !IS_IOS && !IS_ANDROID) {
t.timefloat.style.display = 'block';
}
if (t.hovered && !IS_IOS && !IS_ANDROID && t.options.useSmoothHover) {
removeClass(t.hovered, 'no-hover');
}
}
});
t.slider.addEventListener('mouseleave', () => {
if (t.getDuration() !== Infinity) {
if (!mouseIsDown) {
if (t.timefloat) {
t.timefloat.style.display = 'none';
}
if (t.hovered && t.options.useSmoothHover) {
addClass(t.hovered, 'no-hover');
}
}
}
});
// If media is does not have a finite duration, remove progress bar interaction
// and indicate that is a live broadcast
t.broadcastCallback = (e) => {
const broadcast = controls.querySelector(`.${t.options.classPrefix}broadcast`);
if (!t.options.forceLive && t.getDuration() !== Infinity) {
if (broadcast) {
t.slider.style.display = '';
broadcast.remove();
}
player.setProgressRail(e);
if (!t.forcedHandlePause) {
player.setCurrentRail(e);
}
updateSlider();
} else if (!broadcast && t.options.forceLive) {
const label = document.createElement('span');
label.className = `${t.options.classPrefix}broadcast`;
label.innerText = i18n.t('mejs.live-broadcast');
t.slider.style.display = 'none';
t.rail.appendChild(label);
}
};
media.addEventListener('progress', t.broadcastCallback);
media.addEventListener('timeupdate', t.broadcastCallback);
media.addEventListener('play', () => {
t.buffer.style.display = 'none';
});
media.addEventListener('playing', () => {
t.buffer.style.display = 'none';
});
media.addEventListener('seeking', () => {
t.buffer.style.display = '';
});
media.addEventListener('seeked', () => {
t.buffer.style.display = 'none';
});
media.addEventListener('pause', () => {
t.buffer.style.display = 'none';
});
media.addEventListener('waiting', () => {
t.buffer.style.display = '';
});
media.addEventListener('loadeddata', () => {
t.buffer.style.display = '';
});
media.addEventListener('canplay', () => {
t.buffer.style.display = 'none';
});
media.addEventListener('error', () => {
t.buffer.style.display = 'none';
});
t.getElement(t.container).addEventListener('controlsresize', (e) => {
if (t.getDuration() !== Infinity) {
player.setProgressRail(e);
if (!t.forcedHandlePause) {
player.setCurrentRail(e);
}
}
});
},
cleanprogress (player, controls, layers, media) {
media.removeEventListener('progress', player.broadcastCallback);
media.removeEventListener('timeupdate', player.broadcastCallback);
if (player.rail) {
player.rail.remove();
}
},
/**
* Calculate the progress on the media and update progress bar's width
*
* @param {Event} e
*/
setProgressRail (e) {
const
t = this,
target = (e !== undefined) ? (e.detail.target || e.target) : t.media
;
let percent = null;
// newest HTML5 spec has buffered array (FF4, Webkit)
if (target && target.buffered && target.buffered.length > 0 && target.buffered.end && t.getDuration()) {
// account for a real array with multiple values - always read the end of the last buffer
percent = target.buffered.end(target.buffered.length - 1) / t.getDuration();
}
// 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.
else if (target && target.bytesTotal !== undefined && target.bytesTotal > 0 && target.bufferedBytes !== undefined) {
percent = target.bufferedBytes / target.bytesTotal;
}
// Firefox 3 with an Ogg file seems to go this way
else if (e && e.lengthComputable && e.total !== 0) {
percent = e.loaded / e.total;
}
// finally update the progress bar
if (percent !== null) {
percent = Math.min(1, Math.max(0, percent));
// update loaded bar
if (t.loaded) {
t.setTransformStyle(t.loaded,`scaleX(${percent})`);
}
}
},
/**
* Update the slider's width depending on the time assigned
*
* @param {Number} fakeTime
*/
setCurrentRailHandle (fakeTime) {
const t = this;
t.setCurrentRailMain(t, fakeTime);
},
/**
* Update the slider's width depending on the current time
*
*/
setCurrentRail () {
const t = this;
t.setCurrentRailMain(t);
},
/**
* Method that handles the calculation of the width of the rail.
*
* @param {MediaElementPlayer} t
* @param {?Number} fakeTime
*/
setCurrentRailMain (t, fakeTime) {
if (t.getCurrentTime() !== undefined && t.getDuration()) {
const nTime = (typeof fakeTime === 'undefined') ? t.getCurrentTime() : fakeTime;
// update bar and handle
if (t.total && t.handle) {
const tW = parseFloat(getComputedStyle(t.total).width);
let
newWidth = Math.round(tW * nTime / t.getDuration()),
handlePos = newWidth - Math.round(t.handle.offsetWidth / 2)
;
handlePos = (handlePos < 0) ? 0 : handlePos;
t.setTransformStyle(t.current,`scaleX(${newWidth/tW})`);
t.setTransformStyle(t.handle,`translateX(${handlePos}px)`);
if (t.options.useSmoothHover && !hasClass(t.hovered, 'no-hover')) {
let pos = parseInt(t.hovered.getAttribute('pos'), 10);
pos = (isNaN(pos)) ? 0 : pos;
const hoverScaleX = pos/tW - handlePos/tW;
t.hovered.style.left = `${handlePos}px`;
t.setTransformStyle(t.hovered,`scaleX(${hoverScaleX})`);
if (hoverScaleX >= 0) {
removeClass(t.hovered, 'negative');
} else {
addClass(t.hovered, 'negative');
}
}
}
}
}
});