diff --git a/app/soapbox/features/video/index.js b/app/soapbox/features/video/index.js index 69c16ee47..9fc3831bb 100644 --- a/app/soapbox/features/video/index.js +++ b/app/soapbox/features/video/index.js @@ -3,7 +3,7 @@ import { connect } from 'react-redux'; import PropTypes from 'prop-types'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import { fromJS, is } from 'immutable'; -import { throttle } from 'lodash'; +import { throttle, debounce } from 'lodash'; import classNames from 'classnames'; import { isFullscreen, requestFullscreen, exitFullscreen } from '../ui/util/fullscreen'; import Icon from 'soapbox/components/icon'; @@ -139,26 +139,26 @@ class Video extends React.PureComponent { revealed: this.props.visible !== undefined ? this.props.visible : (this.props.displayMedia !== 'hide_all' && !this.props.sensitive || this.props.displayMedia === 'show_all'), }; - // hard coded in components.scss - // any way to get ::before values programatically? - volWidth = 50; - volOffset = 70; - volHandleOffset = v => { - const offset = v * this.volWidth + this.volOffset; - return (offset > 110) ? 110 : offset; - } - setPlayerRef = c => { this.player = c; - if (c) { - if (this.props.cacheWidth) this.props.cacheWidth(this.player.offsetWidth); - this.setState({ - containerWidth: c.offsetWidth, - }); + if (this.player) { + this._setDimensions(); } } + _setDimensions() { + const width = this.player.offsetWidth; + + if (this.props.cacheWidth) { + this.props.cacheWidth(width); + } + + this.setState({ + containerWidth: width, + }); + } + setVideoRef = c => { this.video = c; @@ -212,16 +212,17 @@ class Video extends React.PureComponent { } handleMouseVolSlide = throttle(e => { - const rect = this.volume.getBoundingClientRect(); - const x = (e.clientX - rect.left) / this.volWidth; //x position within the element. + const { x } = getPointerPosition(this.volume, e); if(!isNaN(x)) { let slideamt = x; + if(x > 1) { slideamt = 1; } else if(x < 0) { slideamt = 0; } + this.video.volume = slideamt; this.setState({ volume: slideamt }); } @@ -261,11 +262,86 @@ class Video extends React.PureComponent { } }, 60); + seekBy(time) { + const currentTime = this.video.currentTime + time; + + if (!isNaN(currentTime)) { + this.setState({ currentTime }, () => { + this.video.currentTime = currentTime; + }); + } + } + + handleVideoKeyDown = e => { + // On the video element or the seek bar, we can safely use the space bar + // for playback control because there are no buttons to press + + if (e.key === ' ') { + e.preventDefault(); + e.stopPropagation(); + this.togglePlay(); + } + } + + handleKeyDown = e => { + const frameTime = 1 / 25; + + switch(e.key) { + case 'k': + e.preventDefault(); + e.stopPropagation(); + this.togglePlay(); + break; + case 'm': + e.preventDefault(); + e.stopPropagation(); + this.toggleMute(); + break; + case 'f': + e.preventDefault(); + e.stopPropagation(); + this.toggleFullscreen(); + break; + case 'j': + e.preventDefault(); + e.stopPropagation(); + this.seekBy(-10); + break; + case 'l': + e.preventDefault(); + e.stopPropagation(); + this.seekBy(10); + break; + case ',': + e.preventDefault(); + e.stopPropagation(); + this.seekBy(-frameTime); + break; + case '.': + e.preventDefault(); + e.stopPropagation(); + this.seekBy(frameTime); + break; + } + + // If we are in fullscreen mode, we don't want any hotkeys + // interacting with the UI that's not visible + + if (this.state.fullscreen) { + e.preventDefault(); + e.stopPropagation(); + + if (e.key === 'Escape') { + exitFullscreen(); + } + } + } + togglePlay = () => { if (this.state.paused) { - this.video.play(); + this.setState({ paused: false }, () => this.video.play()); } else { - this.video.pause(); + this.setState({ paused: true }, () => this.video.pause()); } } @@ -282,9 +358,15 @@ class Video extends React.PureComponent { document.addEventListener('webkitfullscreenchange', this.handleFullscreenChange, true); document.addEventListener('mozfullscreenchange', this.handleFullscreenChange, true); document.addEventListener('MSFullscreenChange', this.handleFullscreenChange, true); + + window.addEventListener('scroll', this.handleScroll); + window.addEventListener('resize', this.handleResize, { passive: true }); } componentWillUnmount() { + window.removeEventListener('scroll', this.handleScroll); + window.removeEventListener('resize', this.handleResize); + document.removeEventListener('fullscreenchange', this.handleFullscreenChange, true); document.removeEventListener('webkitfullscreenchange', this.handleFullscreenChange, true); document.removeEventListener('mozfullscreenchange', this.handleFullscreenChange, true); @@ -303,6 +385,27 @@ class Video extends React.PureComponent { } } + handleResize = debounce(() => { + if (this.player) { + this._setDimensions(); + } + }, 250, { + trailing: true, + }); + + handleScroll = throttle(() => { + if (!this.video) { + return; + } + + const { top, height } = this.video.getBoundingClientRect(); + const inView = (top <= (window.innerHeight || document.documentElement.clientHeight)) && (top + height >= 0); + + if (!this.state.paused && !inView) { + this.setState({ paused: true }, () => this.video.pause()); + } + }, 150, { trailing: true }) + handleFullscreenChange = () => { this.setState({ fullscreen: isFullscreen() }); } @@ -316,8 +419,11 @@ class Video extends React.PureComponent { } toggleMute = () => { - this.video.muted = !this.video.muted; - this.setState({ muted: this.video.muted }); + const muted = !this.video.muted; + + this.setState({ muted }, () => { + this.video.muted = muted; + }); } toggleReveal = () => { @@ -419,6 +525,7 @@ class Video extends React.PureComponent { onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave} onClick={this.handleClickRoot} + onKeyDown={this.handleKeyDown} tabIndex={0} >
- - + +
@@ -493,10 +602,10 @@ class Video extends React.PureComponent {
- {(sensitive && !onCloseVideo) && } - {(!fullscreen && onOpenVideo) && } - {onCloseVideo && } - + {(sensitive && !onCloseVideo) && } + {(!fullscreen && onOpenVideo) && } + {onCloseVideo && } +