Last active
September 15, 2018 19:30
-
-
Save adueck/a8217fddcb4648c64fff29df83af2f59 to your computer and use it in GitHub Desktop.
Keyboard shortcuts and swiping gestures added to facilitate seeking back and forth on a page using react-player
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// This is an example page from a next.js app | |
// I've added buttons below the player to control seeking and speed | |
// As well as keyboard shortcuts and swiping to control the seeking | |
// I wanted users to be able to seek around the video and see the timecode pop up | |
// wherever they were on the page, even if the video was far out of view. | |
import Head from 'next/head' | |
import VisibilitySensor from 'react-visibility-sensor'; | |
import keydown from 'react-keydown' | |
// Using react-player | |
import ReactPlayer from 'react-player' | |
// And react-swipeable | |
import Swipeable from 'react-swipeable' | |
// HOTKEYS used by react-keydown to trigger the seeking and pausing | |
const HOTKEYS = [ 'shift+left', 'shift+right', 'ctrl+left', 'ctrl+right', 'cmd+left', 'cmd+right', 'shift+space', 'ctrl+space', 'space' ]; | |
// To convert the seconds into a nice timecode to display when user seeks | |
function toMMSS(sec) { | |
var sec_num = parseInt(sec, 10); // don't forget the second param | |
var hours = Math.floor(sec_num / 3600); | |
var minutes = Math.floor((sec_num - (hours * 3600)) / 60); | |
var seconds = sec_num - (hours * 3600) - (minutes * 60); | |
if (hours < 10) {hours = "0"+hours;} | |
if (minutes < 10) {minutes = "0"+minutes;} | |
if (seconds < 10) {seconds = "0"+seconds;} | |
return minutes+':'+seconds; | |
} | |
@keydown( HOTKEYS ) | |
class SwipeablePlayerPage extends React.Component { | |
constructor(props) { | |
super(props); | |
this.state = { | |
playbackRate: 1, | |
playing: false, | |
timecodeDisplay: null | |
}; | |
this.PlayerControls = this.PlayerControls.bind(this); | |
this.seekBack = this.seekBack.bind(this); | |
this.seekForward = this.seekForward.bind(this); | |
this.swipedLeft = this.swipedLeft.bind(this); | |
this.swipedRight = this.swipedRight.bind(this); | |
this.visibilityChange = this.visibilityChange.bind(this); | |
} | |
timecodeTimer = null; | |
// Keyhandling functions | |
componentWillReceiveProps( { keydown } ) { | |
if (keydown.event) { | |
if ( keydown.event.key == 'ArrowRight' ) { | |
keydown.event.preventDefault(); | |
this.seekForward(); | |
} | |
if ( keydown.event.key == 'ArrowLeft' ) { | |
keydown.event.preventDefault() | |
this.seekBack(); | |
} | |
if ( keydown.event.key == ' ' ) { | |
keydown.event.preventDefault(); | |
this.setState(prevState => ({ | |
playing: !prevState.playing | |
})); | |
} | |
} | |
} | |
seekInterval = 2.5; | |
setPlaybackRate = e => { | |
this.setState({ playbackRate: parseFloat(e) }) | |
} | |
ref = player => { | |
this.player = player | |
} | |
// Function to seek backwards and display a timecode overlay | |
seekBack() { | |
const timecodeDisplayTime = 1200; | |
this.player.seekTo(this.player.getCurrentTime() - this.seekInterval); | |
const seconds = this.player.getCurrentTime(); | |
this.setState({ timecodeDisplay: toMMSS(seconds) }); | |
if (this.timecodeTimer) { | |
clearTimeout(this.timecodeTimer); | |
this.timecodeTimer = setTimeout(() => this.setState({ timecodeDisplay: null}), timecodeDisplayTime); | |
} | |
else this.timecodeTimer = setTimeout(() => this.setState({ timecodeDisplay: null}), timecodeDisplayTime); | |
} | |
// Function to seek forwards and display a timecode overlay | |
seekForward() { | |
const timecodeDisplayTime = 1200; | |
this.player.seekTo(this.player.getCurrentTime() + this.seekInterval); | |
const seconds = this.player.getCurrentTime(); | |
this.setState({ timecodeDisplay: toMMSS(seconds) }); | |
if (this.timecodeTimer) { | |
clearTimeout(this.timecodeTimer); | |
this.timecodeTimer = setTimeout(() => this.setState({ timecodeDisplay: null}), timecodeDisplayTime); | |
} | |
else this.timecodeTimer = setTimeout(() => this.setState({ timecodeDisplay: null}), timecodeDisplayTime); | |
} | |
// Function handlers called on swipe actions. | |
swipedRight() { | |
this.seekForward(); | |
} | |
swipedLeft() { | |
this.seekBack(); | |
} | |
// Checking if the player has been scrolled out of view (to display the timecode as an overlay when necessary) | |
visibilityChange(isVisible) { | |
this.setState({ playerIsVisible: isVisible ? true : false }); | |
} | |
// Extra buttons below the player for seeking and speed control | |
PlayerControls() { | |
return ( | |
<div className="player-controls"> | |
<div className="controls-centered"> | |
<button className="mini-btn" style={{ marginRight: '5px' }} onClick={() => this.setPlaybackRate(0.5)}>0.5x</button> | |
<button className="mini-btn" style={{ marginRight: '5px' }} onClick={() => this.setPlaybackRate(0.75)}>0.75x</button> | |
<button className="mini-btn" onClick={() => this.setPlaybackRate(1)}>1x</button> | |
</div> | |
<div className="controls-left"> | |
<button className="mini-btn" onClick={this.seekBack}>{`<<`}</button> | |
</div> | |
<div className="controls-right"> | |
<button className="mini-btn" onClick={this.seekForward}>{`>>`}</button> | |
</div> | |
<style jsx>{` | |
.player-controls { | |
margin-top: 0.5em; | |
margin-bottom: 0.5em; | |
font-size: 13px; | |
display: flex; | |
} | |
.controls-left { | |
order: -1; | |
} | |
.controls-centered { | |
flex: 1; | |
text-align: center; | |
} | |
.mini-btn { | |
background: white; | |
font: inherit; | |
padding: 0.1em 0.4em; | |
margin: 0 0.1em; | |
border: 0.5px solid grey; | |
border-radius: 5px; | |
} | |
.mini-btn:active { | |
background: #eee; | |
} | |
.mini-btn:focus { | |
outline: none; | |
} | |
.mini-btn:hover { | |
cursor: pointer; | |
} | |
.mini-btn-right { | |
float: right; | |
} | |
`}</style> | |
</div> | |
) | |
} | |
// Extra buttons below the player for seeking and speed control | |
PlayerControls() { | |
return ( | |
<div className="player-controls"> | |
<div className="controls-centered"> | |
<button className="mini-btn" style={{ marginRight: '5px' }} onClick={() => this.setPlaybackRate(0.5)}>0.5x</button> | |
<button className="mini-btn" style={{ marginRight: '5px' }} onClick={() => this.setPlaybackRate(0.75)}>0.75x</button> | |
<button className="mini-btn" onClick={() => this.setPlaybackRate(1)}>1x</button> | |
</div> | |
<div className="controls-left"> | |
<button className="mini-btn" onClick={this.seekBack}>{`<<`}</button> | |
</div> | |
<div className="controls-right"> | |
<button className="mini-btn" onClick={this.seekForward}>{`>>`}</button> | |
</div> | |
<style jsx>{` | |
.player-controls { | |
margin-top: 0.5em; | |
margin-bottom: 0.5em; | |
font-size: 13px; | |
display: flex; | |
} | |
.controls-left { | |
order: -1; | |
} | |
.controls-centered { | |
flex: 1; | |
text-align: center; | |
} | |
.mini-btn { | |
background: white; | |
font: inherit; | |
padding: 0.1em 0.4em; | |
margin: 0 0.1em; | |
border: 0.5px solid grey; | |
border-radius: 5px; | |
} | |
.mini-btn:active { | |
background: #eee; | |
} | |
.mini-btn:focus { | |
outline: none; | |
} | |
.mini-btn:hover { | |
cursor: pointer; | |
} | |
.mini-btn-right { | |
float: right; | |
} | |
`}</style> | |
</div> | |
) | |
} | |
render() { | |
return ( | |
<Layout> | |
{/* Swipeable area covering the whole page */} | |
<Swipeable onSwipedRight={this.swipedRight} onSwipedLeft={this.swipedLeft}> | |
<h2>{this.props.title}</h2> | |
{/* Display time code overlay when seeking when the player is out of view or playing */} | |
{ ((this.state.playing || !this.state.playerIsVisible) && this.state.timecodeDisplay) && <div style={{ position: 'fixed', left: 0, right: 0, margin: '5% auto', zIndex: '5', top: '40px', backgroundColor: 'rgba(190, 190, 190, 0.8)', color: 'white', borderRadius: '5px', margin: '0 auto', width: '40px', padding: '5px 10px' }}>{this.state.timecodeDisplay}</div>} | |
{/* Video Player Block */} | |
<div style={{ maxWidth: '600px'}}> | |
<div className="player-wrapper"> | |
<ReactPlayer | |
url='https://www.youtube.com/watch?v=L_XJ_s5IsQc' | |
ref={this.ref} | |
controls={true} | |
width='100%' | |
height='100%' | |
playbackRate={this.state.playbackRate} | |
onPlay={() => this.setState({ playing: true })} | |
onPause={() => this.setState({ playing: false })} | |
playing={this.state.playing} | |
style={{ position: 'absolute', | |
top: 0, | |
left: 0 }} | |
/> | |
</div> | |
{/* Sense if player's moved out of screen */} | |
<VisibilitySensor onChange={this.visibilityChange} /> | |
{/* Extra controls for speed and seeking below player */} | |
<this.PlayerControls /> | |
</div> | |
<div> | |
<p>Other Page Content Here</p> | |
<p>A bunch of stuff...</p> | |
<p>Can swipe anywhere here</p> | |
<p>Or however big the page gets</p> | |
<p>Also you can press shift/ctl/cmd + left/right to seek back and forth</p> | |
<p>And shift/ctl/cmd + space to play/pause</p> | |
</div> | |
</Swipeable> | |
<style jsx>{` | |
.player-wrapper { | |
position: relative; | |
padding-top: 56.25% /* Player ratio: 100 / (1280 / 720) */ | |
} | |
.text-block { | |
margin-top: 1em; | |
} | |
.small-clickable { | |
float: right; | |
margin-right: 0; | |
} | |
.small-clickable:hover { | |
cursor: pointer; | |
} | |
.flex-grid { | |
display: flex; | |
justify-content: flex-start; | |
flex-flow: row wrap; | |
} | |
@media (max-width: 400px) { | |
.flex-grid { | |
display: block; | |
} | |
} | |
`}</style> | |
</Layout> | |
) | |
} | |
} | |
export default SwipeablePlayerPage | |
const Layout = (props) => ( | |
<div> | |
<Head> | |
<title>Swipeable Sample</title> | |
<meta name="viewport" content="initial-scale=1.0, width=device-width maximum-scale=1, user-scalable=0" key="viewport"/> | |
</Head> | |
<div className="site"> | |
<main className="content"> | |
<div className="container"> | |
{props.children} | |
</div> | |
</main> | |
</div> | |
<style jsx global>{` | |
html, body { | |
margin: 0; | |
padding: 0; | |
font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol"; | |
line-height: 1.42857143; | |
color: #333; | |
backgroundColor: #fff; | |
} | |
.container { | |
margin: 1em; | |
padding: 1em; | |
} | |
`}</style> | |
</div> | |
) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment