mediaelement
Version:
One file. Any browser. Same UI.
467 lines (416 loc) • 14.4 kB
JavaScript
;
import document from 'global/document';
import {config} from '../player';
import MediaElementPlayer from '../player';
import i18n from '../core/i18n';
import {IS_ANDROID, IS_IOS} from '../utils/constants';
import {isString, createEvent} from '../utils/general';
import {addClass, removeClass, offset} from '../utils/dom';
import {generateControlButton} from '../utils/generate';
/**
* Volume button
*
* This feature enables the displaying of a Volume button in the control bar, and also contains logic to manipulate its
* events, such as sliding up/down (or left/right, if vertical), muting/unmuting media, etc.
*/
// Feature configuration
Object.assign(config, {
/**
* @type {?String}
*/
muteText: null,
/**
* @type {?String}
*/
unmuteText: null,
/**
* @type {?String}
*/
allyVolumeControlText: null,
/**
* @type {Boolean}
*/
hideVolumeOnTouchDevices: true,
/**
* @type {String}
*/
audioVolume: 'horizontal',
/**
* @type {String}
*/
videoVolume: 'vertical',
/**
* Initial volume when the player starts (overridden by user cookie)
* @type {Number}
*/
startVolume: 0.8
});
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
*/
buildvolume (player, controls, layers, media) {
// Android and iOS don't support volume controls
if ((IS_ANDROID || IS_IOS) && this.options.hideVolumeOnTouchDevices) {
return;
}
const
t = this,
mode = (t.isVideo) ? t.options.videoVolume : t.options.audioVolume,
muteText = isString(t.options.muteText) ? t.options.muteText : i18n.t('mejs.mute'),
unmuteText = isString(t.options.unmuteText) ? t.options.unmuteText : i18n.t('mejs.unmute'),
volumeControlText = isString(t.options.allyVolumeControlText) ? t.options.allyVolumeControlText : i18n.t('mejs.volume-help-text'),
mute = document.createElement('div')
;
mute.className = `${t.options.classPrefix}button ${t.options.classPrefix}volume-button ${t.options.classPrefix}mute`;
mute.innerHTML = mode === 'horizontal' ?
generateControlButton(t.id, muteText, muteText, `${t.media.options.iconSprite}`, ['icon-mute', 'icon-unmute'], `${t.options.classPrefix}`, '', `${t.options.classPrefix}horizontal-volume-slider`) :
generateControlButton(t.id, muteText, muteText, `${t.media.options.iconSprite}`, ['icon-mute', 'icon-unmute'], `${t.options.classPrefix}`, '', `${t.options.classPrefix}volume-slider`) +
`<a class="${t.options.classPrefix}volume-slider" ` +
`aria-label="${i18n.t('mejs.volume-slider')}" aria-valuemin="0" aria-valuemax="100" role="slider" ` +
`aria-orientation="vertical">` +
`<span class="${t.options.classPrefix}offscreen" id="${t.options.classPrefix}volume-slider">${volumeControlText}</span>` +
`<div class="${t.options.classPrefix}volume-total">` +
`<div class="${t.options.classPrefix}volume-current"></div>` +
`<div class="${t.options.classPrefix}volume-handle"></div>` +
`</div>` +
`</a>`;
t.addControlElement(mute, 'volume');
t.options.keyActions.push({
keys: [38], // UP
action: (player) => {
const volumeSlider = player.getElement(player.container).querySelector(`.${t.options.classPrefix}volume-slider`);
if (volumeSlider && volumeSlider.matches(':focus')) {
volumeSlider.style.display = 'block';
}
if (player.isVideo) {
player.showControls();
player.startControlsTimer();
}
const newVolume = Math.min(player.volume + 0.1, 1);
player.setVolume(newVolume);
if (newVolume > 0) {
player.setMuted(false);
}
}
},
{
keys: [40], // DOWN
action: (player) => {
const volumeSlider = player.getElement(player.container).querySelector(`.${t.options.classPrefix}volume-slider`);
if (volumeSlider) {
volumeSlider.style.display = 'block';
}
if (player.isVideo) {
player.showControls();
player.startControlsTimer();
}
const newVolume = Math.max(player.volume - 0.1, 0);
player.setVolume(newVolume);
if (newVolume <= 0.1) {
player.setMuted(true);
}
}
},
{
keys: [77], // M
action: (player) => {
const volumeSlider = player.getElement(player.container).querySelector(`.${t.options.classPrefix}volume-slider`);
if (volumeSlider) {
volumeSlider.style.display = 'block';
}
if (player.isVideo) {
player.showControls();
player.startControlsTimer();
}
if (player.media.muted) {
player.setMuted(false);
} else {
player.setMuted(true);
}
}
});
// horizontal version
if (mode === 'horizontal') {
const anchor = document.createElement('a');
anchor.className = `${t.options.classPrefix}horizontal-volume-slider`;
anchor.setAttribute('aria-label', i18n.t('mejs.volume-slider'));
anchor.setAttribute('aria-valuemin', 0);
anchor.setAttribute('aria-valuemax', 100);
anchor.setAttribute('aria-valuenow', 100);
anchor.setAttribute('role', 'slider');
anchor.innerHTML += `<span class="${t.options.classPrefix}offscreen" id="${t.options.classPrefix}horizontal-volume-slider">${volumeControlText}</span>` +
`<div class="${t.options.classPrefix}horizontal-volume-total">` +
`<div class="${t.options.classPrefix}horizontal-volume-current"></div>` +
`<div class="${t.options.classPrefix}horizontal-volume-handle"></div>` +
`</div>`;
mute.parentNode.insertBefore(anchor, mute.nextSibling);
}
let
mouseIsDown = false,
mouseIsOver = false,
modified = false,
/**
* @private
*/
updateVolumeSlider = () => {
const volume = Math.floor(media.volume * 100);
volumeSlider.setAttribute('aria-valuenow', volume);
volumeSlider.setAttribute('aria-valuetext', `${volume}%`);
}
;
const
volumeSlider = mode === 'vertical' ? t.getElement(t.container).querySelector(`.${t.options.classPrefix}volume-slider`) :
t.getElement(t.container).querySelector(`.${t.options.classPrefix}horizontal-volume-slider`),
volumeTotal = mode === 'vertical' ? t.getElement(t.container).querySelector(`.${t.options.classPrefix}volume-total`) :
t.getElement(t.container).querySelector(`.${t.options.classPrefix}horizontal-volume-total`),
volumeCurrent = mode === 'vertical' ? t.getElement(t.container).querySelector(`.${t.options.classPrefix}volume-current`) :
t.getElement(t.container).querySelector(`.${t.options.classPrefix}horizontal-volume-current`),
volumeHandle = mode === 'vertical' ? t.getElement(t.container).querySelector(`.${t.options.classPrefix}volume-handle`) :
t.getElement(t.container).querySelector(`.${t.options.classPrefix}horizontal-volume-handle`),
/**
* @private
* @param {Number} volume
*/
positionVolumeHandle = (volume) => {
if (volume === null || isNaN(volume) || volume === undefined) {
return;
}
// correct to 0-1
volume = Math.max(0, volume);
volume = Math.min(volume, 1);
// adjust mute button style
if (volume === 0) {
removeClass(mute, `${t.options.classPrefix}mute`);
addClass(mute, `${t.options.classPrefix}unmute`);
const button = mute.firstElementChild;
button.setAttribute('title', unmuteText);
button.setAttribute('aria-label', unmuteText);
} else {
removeClass(mute, `${t.options.classPrefix}unmute`);
addClass(mute, `${t.options.classPrefix}mute`);
const button = mute.firstElementChild;
button.setAttribute('title', muteText);
button.setAttribute('aria-label', muteText);
}
const
volumePercentage = `${(volume * 100)}%`,
volumeStyles = getComputedStyle(volumeHandle)
;
// position slider
if (mode === 'vertical') {
volumeCurrent.style.bottom = 0;
volumeCurrent.style.height = volumePercentage;
volumeHandle.style.bottom = volumePercentage;
volumeHandle.style.marginBottom = `${(-parseFloat(volumeStyles.height) / 2)}px`;
} else {
volumeCurrent.style.left = 0;
volumeCurrent.style.width = volumePercentage;
volumeHandle.style.left = volumePercentage;
volumeHandle.style.marginLeft = `${(-parseFloat(volumeStyles.width) / 2)}px`;
}
},
/**
* @private
*/
handleVolumeMove = (e) => {
const
totalOffset = offset(volumeTotal),
volumeStyles = getComputedStyle(volumeTotal)
;
modified = true;
let volume = null;
// calculate the new volume based on the most recent position
if (mode === 'vertical') {
const
railHeight = parseFloat(volumeStyles.height),
newY = e.pageY - totalOffset.top
;
volume = (railHeight - newY) / railHeight;
// the controls just hide themselves (usually when mouse moves too far up)
if (totalOffset.top === 0 || totalOffset.left === 0) {
return;
}
} else {
const
railWidth = parseFloat(volumeStyles.width),
newX = e.pageX - totalOffset.left
;
volume = newX / railWidth;
}
// ensure the volume isn't outside 0-1
volume = Math.max(0, volume);
volume = Math.min(volume, 1);
// position the slider and handle
positionVolumeHandle(volume);
// set the media object (this will trigger the `volumechange` event)
t.setMuted((volume === 0));
t.setVolume(volume);
e.preventDefault();
e.stopPropagation();
},
toggleMute = () => {
if (t.muted) {
positionVolumeHandle(0);
removeClass(mute, `${t.options.classPrefix}mute`);
addClass(mute, `${t.options.classPrefix}unmute`);
} else {
positionVolumeHandle(media.volume);
removeClass(mute, `${t.options.classPrefix}unmute`);
addClass(mute, `${t.options.classPrefix}mute`);
}
}
;
player.getElement(player.container).addEventListener('keydown', (e) => {
const hasFocus = !!(e.target.closest(`.${t.options.classPrefix}container`));
if (!hasFocus && mode === 'vertical') {
volumeSlider.style.display = 'none';
}
});
mute.addEventListener('mouseenter', (e) => {
if (e.target === mute) {
volumeSlider.style.display = 'block';
mouseIsOver = true;
e.preventDefault();
e.stopPropagation();
}
});
mute.addEventListener('focusin', () => {
volumeSlider.style.display = 'block';
mouseIsOver = true;
});
mute.addEventListener('focusout', (e) => {
if ((!e.relatedTarget || (e.relatedTarget && !e.relatedTarget.matches(`.${t.options.classPrefix}volume-slider`))) &&
mode === 'vertical') {
volumeSlider.style.display = 'none';
}
});
mute.addEventListener('mouseleave', () => {
mouseIsOver = false;
if (!mouseIsDown && mode === 'vertical') {
volumeSlider.style.display = 'none';
}
});
mute.addEventListener('focusout', () => {
mouseIsOver = false;
});
mute.addEventListener('keydown', (e) => {
if (t.options.enableKeyboard && t.options.keyActions.length) {
let
keyCode = e.which || e.keyCode || 0,
volume = media.volume
;
switch (keyCode) {
case 38: // Up
volume = Math.min(volume + 0.1, 1);
break;
case 40: // Down
volume = Math.max(0, volume - 0.1);
break;
default:
return true;
}
mouseIsDown = false;
positionVolumeHandle(volume);
media.setVolume(volume);
e.preventDefault();
e.stopPropagation();
}
});
mute.querySelector('button').addEventListener('click', () => {
media.setMuted(!media.muted);
const event = createEvent('volumechange', media);
media.dispatchEvent(event);
});
// Events
volumeSlider.addEventListener('dragstart', () => false);
volumeSlider.addEventListener('mouseover', () => {
mouseIsOver = true;
});
volumeSlider.addEventListener('focusin', () => {
volumeSlider.style.display = 'block';
mouseIsOver = true;
});
volumeSlider.addEventListener('focusout', () => {
mouseIsOver = false;
if (!mouseIsDown && mode === 'vertical') {
volumeSlider.style.display = 'none';
}
});
volumeSlider.addEventListener('mousedown', (e) => {
handleVolumeMove(e);
t.globalBind('mousemove.vol', (event) => {
const target = event.target;
// check for `closest` fixes firefox error: https://github.com/mediaelement/mediaelement/pull/2840/files
const targetHasClosest = typeof target.closest == 'function';
const targetSliderElement = target.closest(
(mode === 'vertical' ? `.${t.options.classPrefix}volume-slider` : `.${t.options.classPrefix}horizontal-volume-slider`)
)
if (mouseIsDown && (target === volumeSlider || (targetHasClosest && targetSliderElement))) {
handleVolumeMove(event);
}
});
t.globalBind('mouseup.vol', () => {
mouseIsDown = false;
if (!mouseIsOver && mode === 'vertical') {
volumeSlider.style.display = 'none';
}
});
mouseIsDown = true;
e.preventDefault();
e.stopPropagation();
});
// listen for volume change events from other sources
media.addEventListener('volumechange', (e) => {
if (!mouseIsDown) {
toggleMute();
}
updateVolumeSlider(e);
});
let rendered = false;
media.addEventListener('rendererready', function () {
if (!modified) {
setTimeout(() => {
rendered = true;
if (player.options.startVolume === 0 || media.originalNode.muted) {
media.setMuted(true);
}
media.setVolume(player.options.startVolume);
t.setControlsSize();
}, 250);
}
});
media.addEventListener('loadedmetadata', function () {
setTimeout(() => {
if (!modified && !rendered) {
if (player.options.startVolume === 0 || media.originalNode.muted) {
media.setMuted(true);
}
if(player.options.startVolume === 0){
player.options.startVolume = 0;
}
media.setVolume(player.options.startVolume);
t.setControlsSize();
}
rendered = false;
}, 250);
});
// mute the media and sets the volume icon muted if the initial volume is set to 0
if (player.options.startVolume === 0 || media.originalNode.muted) {
media.setMuted(true);
if(player.options.startVolume === 0){
player.options.startVolume = 0;
}
toggleMute();
}
t.getElement(t.container).addEventListener('controlsresize', () => {
toggleMute();
});
}
});