sforkowany z mirror/soapbox
Merge branch 'video-player' into 'develop'
Video player changes from Mastodon See merge request soapbox-pub/soapbox-fe!816draftjs
commit
03541ccc6f
|
@ -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>
|
||||||
|
|
Ładowanie…
Reference in New Issue