diff --git a/app/javascript/mastodon/features/audio/index.js b/app/javascript/mastodon/features/audio/index.js
index 434148e8e7..c47f55dd1e 100644
--- a/app/javascript/mastodon/features/audio/index.js
+++ b/app/javascript/mastodon/features/audio/index.js
@@ -386,13 +386,59 @@ class Audio extends React.PureComponent {
return this.props.foregroundColor || '#ffffff';
}
+ seekBy (time) {
+ const currentTime = this.audio.currentTime + time;
+
+ if (!isNaN(currentTime)) {
+ this.setState({ currentTime }, () => {
+ this.audio.currentTime = currentTime;
+ });
+ }
+ }
+
+ handleAudioKeyDown = e => {
+ // On the audio 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 => {
+ switch(e.key) {
+ case 'k':
+ e.preventDefault();
+ e.stopPropagation();
+ this.togglePlay();
+ break;
+ case 'm':
+ e.preventDefault();
+ e.stopPropagation();
+ this.toggleMute();
+ break;
+ case 'j':
+ e.preventDefault();
+ e.stopPropagation();
+ this.seekBy(-10);
+ break;
+ case 'l':
+ e.preventDefault();
+ e.stopPropagation();
+ this.seekBy(10);
+ break;
+ }
+ }
+
render () {
const { src, intl, alt, editable, autoPlay } = this.props;
const { paused, muted, volume, currentTime, duration, buffer, dragging } = this.state;
const progress = Math.min((currentTime / duration) * 100, 100);
return (
-
+
@@ -432,6 +480,7 @@ class Audio extends React.PureComponent {
className={classNames('video-player__seek__handle', { active: dragging })}
tabIndex='0'
style={{ left: `${progress}%`, backgroundColor: this._getAccentColor() }}
+ onKeyDown={this.handleAudioKeyDown}
/>
diff --git a/app/javascript/mastodon/features/video/index.js b/app/javascript/mastodon/features/video/index.js
index 229a921408..e6c6f4b679 100644
--- a/app/javascript/mastodon/features/video/index.js
+++ b/app/javascript/mastodon/features/video/index.js
@@ -266,6 +266,81 @@ class Video extends React.PureComponent {
}
}, 15);
+ 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.setState({ paused: false }, () => this.video.play());
@@ -484,6 +559,7 @@ class Video extends React.PureComponent {
onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave}
onClick={this.handleClickRoot}
+ onKeyDown={this.handleKeyDown}
tabIndex={0}
>