Merge branch 'video-player' into 'develop'

Video player changes from Mastodon

See merge request soapbox-pub/soapbox-fe!816
draftjs
Alex Gleason 2021-10-22 18:14:49 +00:00
commit 03541ccc6f
1 zmienionych plików z 136 dodań i 27 usunięć

Wyświetl plik

@ -3,7 +3,7 @@ import { connect } from 'react-redux';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { fromJS, is } from 'immutable'; import { fromJS, is } from 'immutable';
import { throttle } from 'lodash'; import { throttle, debounce } from 'lodash';
import classNames from 'classnames'; import classNames from 'classnames';
import { isFullscreen, requestFullscreen, exitFullscreen } from '../ui/util/fullscreen'; import { isFullscreen, requestFullscreen, exitFullscreen } from '../ui/util/fullscreen';
import Icon from 'soapbox/components/icon'; 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'), 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 => { setPlayerRef = c => {
this.player = c; this.player = c;
if (c) { if (this.player) {
if (this.props.cacheWidth) this.props.cacheWidth(this.player.offsetWidth); this._setDimensions();
this.setState({
containerWidth: c.offsetWidth,
});
} }
} }
_setDimensions() {
const width = this.player.offsetWidth;
if (this.props.cacheWidth) {
this.props.cacheWidth(width);
}
this.setState({
containerWidth: width,
});
}
setVideoRef = c => { setVideoRef = c => {
this.video = c; this.video = c;
@ -212,16 +212,17 @@ class Video extends React.PureComponent {
} }
handleMouseVolSlide = throttle(e => { handleMouseVolSlide = throttle(e => {
const rect = this.volume.getBoundingClientRect(); const { x } = getPointerPosition(this.volume, e);
const x = (e.clientX - rect.left) / this.volWidth; //x position within the element.
if(!isNaN(x)) { if(!isNaN(x)) {
let slideamt = x; let slideamt = x;
if(x > 1) { if(x > 1) {
slideamt = 1; slideamt = 1;
} else if(x < 0) { } else if(x < 0) {
slideamt = 0; slideamt = 0;
} }
this.video.volume = slideamt; this.video.volume = slideamt;
this.setState({ volume: slideamt }); this.setState({ volume: slideamt });
} }
@ -261,11 +262,86 @@ class Video extends React.PureComponent {
} }
}, 60); }, 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 = () => { togglePlay = () => {
if (this.state.paused) { if (this.state.paused) {
this.video.play(); this.setState({ paused: false }, () => this.video.play());
} else { } 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('webkitfullscreenchange', this.handleFullscreenChange, true);
document.addEventListener('mozfullscreenchange', this.handleFullscreenChange, true); document.addEventListener('mozfullscreenchange', this.handleFullscreenChange, true);
document.addEventListener('MSFullscreenChange', this.handleFullscreenChange, true); document.addEventListener('MSFullscreenChange', this.handleFullscreenChange, true);
window.addEventListener('scroll', this.handleScroll);
window.addEventListener('resize', this.handleResize, { passive: true });
} }
componentWillUnmount() { componentWillUnmount() {
window.removeEventListener('scroll', this.handleScroll);
window.removeEventListener('resize', this.handleResize);
document.removeEventListener('fullscreenchange', this.handleFullscreenChange, true); document.removeEventListener('fullscreenchange', this.handleFullscreenChange, true);
document.removeEventListener('webkitfullscreenchange', this.handleFullscreenChange, true); document.removeEventListener('webkitfullscreenchange', this.handleFullscreenChange, true);
document.removeEventListener('mozfullscreenchange', 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 = () => { handleFullscreenChange = () => {
this.setState({ fullscreen: isFullscreen() }); this.setState({ fullscreen: isFullscreen() });
} }
@ -316,8 +419,11 @@ class Video extends React.PureComponent {
} }
toggleMute = () => { toggleMute = () => {
this.video.muted = !this.video.muted; const muted = !this.video.muted;
this.setState({ muted: this.video.muted });
this.setState({ muted }, () => {
this.video.muted = muted;
});
} }
toggleReveal = () => { toggleReveal = () => {
@ -419,6 +525,7 @@ class Video extends React.PureComponent {
onMouseEnter={this.handleMouseEnter} onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave} onMouseLeave={this.handleMouseLeave}
onClick={this.handleClickRoot} onClick={this.handleClickRoot}
onKeyDown={this.handleKeyDown}
tabIndex={0} tabIndex={0}
> >
<Blurhash <Blurhash
@ -441,6 +548,7 @@ class Video extends React.PureComponent {
height={height || 300} height={height || 300}
volume={volume} volume={volume}
onClick={this.togglePlay} onClick={this.togglePlay}
onKeyDown={this.handleVideoKeyDown}
onPlay={this.handlePlay} onPlay={this.handlePlay}
onPause={this.handlePause} onPause={this.handlePause}
onTimeUpdate={this.handleTimeUpdate} onTimeUpdate={this.handleTimeUpdate}
@ -464,13 +572,14 @@ class Video extends React.PureComponent {
className={classNames('video-player__seek__handle', { active: dragging })} className={classNames('video-player__seek__handle', { active: dragging })}
tabIndex='0' tabIndex='0'
style={{ left: `${progress}%` }} style={{ left: `${progress}%` }}
onKeyDown={this.handleVideoKeyDown}
/> />
</div> </div>
<div className='video-player__buttons-bar'> <div className='video-player__buttons-bar'>
<div className='video-player__buttons left'> <div className='video-player__buttons left'>
<button type='button' aria-label={intl.formatMessage(paused ? messages.play : messages.pause)} className='player-button' onClick={this.togglePlay} autoFocus={detailed}><Icon src={paused ? require('@tabler/icons/icons/player-play.svg') : require('@tabler/icons/icons/player-pause.svg')} /></button> <button type='button' title={intl.formatMessage(paused ? messages.play : messages.pause)} aria-label={intl.formatMessage(paused ? messages.play : messages.pause)} className='player-button' onClick={this.togglePlay} autoFocus={detailed}><Icon src={paused ? require('@tabler/icons/icons/player-play.svg') : require('@tabler/icons/icons/player-pause.svg')} /></button>
<button type='button' aria-label={intl.formatMessage(muted ? messages.unmute : messages.mute)} className='player-button' onClick={this.toggleMute}><Icon src={muted ? require('@tabler/icons/icons/volume-3.svg') : require('@tabler/icons/icons/volume.svg')} /></button> <button type='button' title={intl.formatMessage(muted ? messages.unmute : messages.mute)} aria-label={intl.formatMessage(muted ? messages.unmute : messages.mute)} className='player-button' onClick={this.toggleMute}><Icon src={muted ? require('@tabler/icons/icons/volume-3.svg') : require('@tabler/icons/icons/volume.svg')} /></button>
<div className={classNames('video-player__volume', { active: this.state.hovered })} onMouseDown={this.handleVolumeMouseDown} ref={this.setVolumeRef}> <div className={classNames('video-player__volume', { active: this.state.hovered })} onMouseDown={this.handleVolumeMouseDown} ref={this.setVolumeRef}>
<div className='video-player__volume__current' style={{ width: `${volume * 100}%` }} /> <div className='video-player__volume__current' style={{ width: `${volume * 100}%` }} />
@ -493,10 +602,10 @@ class Video extends React.PureComponent {
</div> </div>
<div className='video-player__buttons right'> <div className='video-player__buttons right'>
{(sensitive && !onCloseVideo) && <button type='button' aria-label={intl.formatMessage(messages.hide)} className='player-button' onClick={this.toggleReveal}><Icon src={require('@tabler/icons/icons/eye-off.svg')} /></button>} {(sensitive && !onCloseVideo) && <button type='button' title={intl.formatMessage(messages.hide)} aria-label={intl.formatMessage(messages.hide)} className='player-button' onClick={this.toggleReveal}><Icon src={require('@tabler/icons/icons/eye-off.svg')} /></button>}
{(!fullscreen && onOpenVideo) && <button type='button' aria-label={intl.formatMessage(messages.expand)} className='player-button' onClick={this.handleOpenVideo}><Icon src={require('@tabler/icons/icons/maximize.svg')} /></button>} {(!fullscreen && onOpenVideo) && <button type='button' title={intl.formatMessage(messages.expand)} aria-label={intl.formatMessage(messages.expand)} className='player-button' onClick={this.handleOpenVideo}><Icon src={require('@tabler/icons/icons/maximize.svg')} /></button>}
{onCloseVideo && <button type='button' aria-label={intl.formatMessage(messages.close)} className='player-button' onClick={this.handleCloseVideo}><Icon src={require('@tabler/icons/icons/minimize.svg')} /></button>} {onCloseVideo && <button type='button' title={intl.formatMessage(messages.close)} aria-label={intl.formatMessage(messages.close)} className='player-button' onClick={this.handleCloseVideo}><Icon src={require('@tabler/icons/icons/minimize.svg')} /></button>}
<button type='button' aria-label={intl.formatMessage(fullscreen ? messages.exit_fullscreen : messages.fullscreen)} className='player-button' onClick={this.toggleFullscreen}><Icon src={fullscreen ? require('@tabler/icons/icons/minimize.svg') : require('@tabler/icons/icons/arrows-maximize.svg')} /></button> <button type='button' title={intl.formatMessage(fullscreen ? messages.exit_fullscreen : messages.fullscreen)} aria-label={intl.formatMessage(fullscreen ? messages.exit_fullscreen : messages.fullscreen)} className='player-button' onClick={this.toggleFullscreen}><Icon src={fullscreen ? require('@tabler/icons/icons/minimize.svg') : require('@tabler/icons/icons/arrows-maximize.svg')} /></button>
</div> </div>
</div> </div>
</div> </div>