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}
>