View file mediaelement-mediaelement-6d66563/src/js/features/progress.js

File size: 19.59Kb
'use strict';

import document from 'global/document';
import {config} from '../player';
import MediaElementPlayer from '../player';
import i18n from '../core/i18n';
import {IS_FIREFOX, 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();
					}

					player.getElement(player.container).querySelector(`.${config.classPrefix}time-total`).focus();

					// 5%
					const newTime = Math.max(player.currentTime - player.options.defaultSeekBackwardInterval(player), 0);
					player.setCurrentTime(newTime);
				}
			}
		},
		{
			keys: [
				39, // RIGHT
				228 // Google TV forward
			],
			action: (player) => {

				if (!isNaN(player.duration) && player.duration > 0) {
					if (player.isVideo) {
						player.showControls();
						player.startControlsTimer();
					}

					player.getElement(player.container).querySelector(`.${config.classPrefix}time-total`).focus();

					// 5%
					const newTime = Math.min(player.currentTime + player.options.defaultSeekForwardInterval(player), player.duration);
					player.setCurrentTime(newTime);
				}
			}
		});

		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 <= 0.02) ? 0 : 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', 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);
					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
					case 32: // space
						if (IS_FIREFOX) {
							if (t.paused) {
								t.play();
							} else {
								t.pause();
							}
						}
						return;
					default:
						return;
				}

				seekTime = seekTime < 0 ? 0 : (seekTime >= duration ? duration : Math.floor(seekTime));
				lastKeyPressTime = new Date();
				if (!startedPaused) {
					player.pause();
				}


				if (seekTime < t.getDuration() && !startedPaused) {
					setTimeout(restartPlayer, 1100);
				}

				t.setCurrentTime(seekTime);
				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');
					}
				}
			}
		}
	}
});