Last active
May 13, 2016 14:24
-
-
Save formigone/7569ef180a2eeaf264261e61cd81f8a9 to your computer and use it in GitHub Desktop.
A plain HTML5 video player styled to taste, and driven by React.js
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
<!doctype html> | |
<html> | |
<head> | |
<title>mp4</title> | |
<style> | |
.emVid { | |
position: relative; | |
bottom: 0; | |
left: 0; | |
background: #000; | |
width: 100%; | |
} | |
.emVid:fullscreen { | |
height: 100%; | |
} | |
.emVid video { | |
width: 100%; | |
} | |
.emVid--progress { | |
background: #333; | |
height: 2px; | |
width: 100%; | |
position: absolute; | |
bottom: 0; | |
left: 0; | |
overflow: hidden; | |
} | |
.emVid--progress-done { | |
background: #ffcc00; | |
height: 2px; | |
width: 100%; | |
transition: width 0.05s; | |
} | |
.emVid--ctr { | |
background: rgba(0, 0, 0, 0.5); | |
height: 50px; | |
width: 100%; | |
position: absolute; | |
bottom: 2px; | |
left: 0; | |
overflow: hidden; | |
} | |
.emVid--ctr--btn { | |
color: #fff; | |
border: none; | |
background: transparent; | |
height: 100%; | |
padding: 0 20px; | |
margin: 0; | |
cursor: pointer; | |
} | |
.emVid--ctr--btn:disabled { | |
color: #555; | |
} | |
.emVid--ctr--btn__left { | |
float: left; | |
margin-left: 20px; | |
} | |
.emVid--ctr--btn__right { | |
float: right; | |
margin-right: 0; | |
} | |
.emVid--ctr--btn__right-most { | |
margin-right: 20px; | |
} | |
.emVid--ctr--btn__muted { | |
color: #c00; | |
} | |
.emVid--ctr--timer { | |
color: #fff; | |
padding: 0; | |
margin: 1.2em 0 0; | |
font-size: 1em; | |
display: inline-block; | |
line-height: 100%; | |
} | |
.emVid--ctr--timer__muted { | |
color: #555; | |
} | |
</style> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.14.8/react.min.js"></script> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.14.8/react-dom.js"></script> | |
</head> | |
<body> | |
<div class="container" id="container"></div> | |
<script> | |
var el = React.createElement; | |
function isMobile(ua) { | |
ua = ua || navigator.userAgent; | |
if (typeof ua !== 'string') { | |
return false; | |
} | |
var match = ua.match(/Android|BlackBerry|iPhone|iPad|iPod|Opera Mini|IEMobile/i) || ''; | |
return match.length > 0; | |
} | |
var Prog = React.createClass({ | |
getDefaultProps: function () { | |
return { | |
percent: 0 | |
}; | |
}, | |
render: function () { | |
return ( | |
el('div', { className: 'emVid--progress' }, ( | |
el('div', { | |
className: 'emVid--progress-done', | |
style: { width: this.props.percent * 100 + '%' } | |
}) | |
)) | |
); | |
} | |
}); | |
var Ctr = React.createClass({ | |
getDefaultProps: function () { | |
return { | |
onPlay: function () { }, | |
onPause: function () { }, | |
onVolumeChange: function () { }, | |
onFullScreen: function () { }, | |
isPlaying: false, | |
loading: true, | |
volume: 0.5, | |
position: 0, | |
duration: 0 | |
}; | |
}, | |
getInitialState: function(){ | |
return { | |
muted: false, | |
fullScreen: false | |
}; | |
}, | |
componentWillReceiveProps: function(oldProps){ | |
if (oldProps.volume <= 0 && !this.state.muted) { | |
this.setState({ | |
muted: true | |
}); | |
} | |
}, | |
changeVolume: function(val){ | |
var pr = this.props; | |
var newVol = pr.volume + val; | |
this.setState({ | |
muted: newVol <= 0 | |
}); | |
pr.onVolumeChange(val); | |
}, | |
toggleMute: function() { | |
var muted = this.state.muted; | |
var pr = this.props; | |
if (muted) { | |
pr.onVolumeChange(0.5); | |
} else { | |
pr.onVolumeChange(-1); | |
} | |
this.setState({ | |
muted: !muted | |
}); | |
}, | |
toggleFullScreen: function(){ | |
var st = this.state; | |
this.props.onFullScreen(st.fullScreen); | |
this.setState({ | |
fullScreen: !st.fullScreen | |
}); | |
}, | |
_formatTime: function(seconds) { | |
var sec = parseInt(seconds, 10); | |
var hours = Math.floor(sec / 3600); | |
var min = Math.floor((sec - (hours * 3600)) / 60); | |
var seconds = sec - (hours * 3600) - (min * 60); | |
return min + ':' + (seconds < 10 ? '0' + seconds : seconds); | |
}, | |
render: function () { | |
var pr = this.props; | |
var st = this.state; | |
var hiddenOnMobile = isMobile() ? { display: 'none' } : { }; | |
var duration = this._formatTime(pr.duration); | |
var position = this._formatTime(pr.position); | |
var currentTime = position + ' / ' + duration; | |
if (pr.loading){ | |
currentTime = '-- : --'; | |
} | |
var playPauseBtn = pr.isPlaying ? | |
el('button', { | |
tabIndex: 1, | |
className: 'emVid--ctr--btn emVid--ctr--btn__left fa fa-pause', | |
key: 'pause', | |
disabled: pr.loading, | |
onClick: pr.onPause | |
}) : | |
el('button', { | |
tabIndex: 1, | |
className: 'emVid--ctr--btn emVid--ctr--btn__left fa fa-play', | |
key: 'play', | |
disabled: pr.loading, | |
onClick: pr.onPlay | |
}); | |
return ( | |
el('div', { className: 'emVid--ctr' }, [ | |
playPauseBtn, | |
el('span', { key: 'currTime', className: 'emVid--ctr--timer ' + (pr.loading ? 'emVid--ctr--timer__muted' : '') }, currentTime), | |
el('button', { | |
tabIndex: 13, | |
className: 'emVid--ctr--btn emVid--ctr--btn__right emVid--ctr--btn__right-most fa fa-' + (st.fullScreen ? 'compress' : 'expand'), | |
key: 'full', | |
onClick: this.toggleFullScreen, | |
disabled: pr.loading | |
}), | |
el('button', { | |
tabIndex: 12, | |
style: hiddenOnMobile, | |
className: 'emVid--ctr--btn emVid--ctr--btn__right fa fa-volume-up', | |
key: 'volUp', | |
onClick: this.changeVolume.bind(null, 0.1), | |
disabled: pr.loading || pr.volume >= 1 | |
}), | |
el('button', { | |
tabIndex: 11, | |
style: hiddenOnMobile, | |
className: 'emVid--ctr--btn emVid--ctr--btn__right fa fa-volume-down', | |
key: 'volDown', | |
onClick: this.changeVolume.bind(null, -0.1), | |
disabled: pr.loading || pr.volume <= 0 | |
}), | |
el('button', { | |
tabIndex: 10, | |
style: hiddenOnMobile, | |
className: 'emVid--ctr--btn emVid--ctr--btn__right fa fa-volume-off ' + (st.muted ? 'emVid--ctr--btn__muted' : ''), | |
key: 'mute', | |
onClick: this.toggleMute, | |
disabled: pr.loading | |
}) | |
]) | |
); | |
} | |
}); | |
var Vid = React.createClass({ | |
getDefaultProps: function () { | |
return { | |
src: '', | |
onEnded: function () { } | |
}; | |
}, | |
getInitialState: function () { | |
return { | |
duration: 0, | |
position: 0, | |
volume: 0, | |
isPlaying: false, | |
isLoading: true, | |
_timer: 0 | |
}; | |
}, | |
componentDidMount: function () { | |
var vidEl = this.refs.video; | |
if (vidEl !== null) { | |
vidEl.addEventListener('loadeddata', this._vidLoadedData); | |
vidEl.addEventListener('ended', this._vidEnded); | |
} | |
}, | |
componentWillUnmount: function(){ | |
var vidEl = this.refs.video; | |
if (vidEl !== null) { | |
vidEl.removeEventListener('loadeddata', this._vidLoadedData); | |
vidEl.removeEventListener('ended', this._vidEnded); | |
} | |
}, | |
_vidLoadedData: function () { | |
var vidEl = this.refs.video; | |
if (vidEl !== null) { | |
this.setState({ | |
isLoading: false, | |
duration: vidEl.duration, | |
volume: Number(localStorage.getItem('emVolume') || this.state.volume) | |
}); | |
} | |
}, | |
_vidEnded: function () { | |
this.setState({ | |
isLoading: true, | |
isPlaying: false | |
}); | |
this.props.onEnded(); | |
}, | |
updateProgress: function () { | |
var state = this.state; | |
if (state.isPlaying) { | |
this.setState({ | |
position: this.refs.video.currentTime, | |
_timer: setTimeout(this.updateProgress, 10) | |
}); | |
} | |
}, | |
updateVolume: function (add) { | |
var vol = this.state.volume + add; | |
if (vol < 0) { | |
vol = 0; | |
} else if (vol > 1) { | |
vol = 1; | |
} | |
this.refs.video.volume = vol; | |
localStorage.setItem('emVolume', vol); | |
this.setState({ | |
volume: vol | |
}) | |
}, | |
onPlay: function () { | |
var vidEl = this.refs.video; | |
vidEl.play(true); | |
clearTimeout(this.state._timer); | |
setTimeout(this.updateProgress, 0); | |
this.updateVolume(0); | |
this.setState({ | |
isPlaying: true | |
}); | |
}, | |
onPause: function () { | |
var vidEl = this.refs.video; | |
vidEl.pause(true); | |
this.setState({ | |
isPlaying: false | |
}); | |
}, | |
onFullScreen: function(exit){ | |
var elem = this.refs.videoContainer; | |
var fs = function(){ }; | |
if (exit) { | |
fs = elem.exitFullscreen || | |
elem.mozCancelFullScreen || | |
elem.webkitExitFullscreen || | |
elem.msExitFullscreen; | |
if (!fs) { | |
document.webkitCancelFullScreen(); | |
return; | |
} | |
} else { | |
fs = elem.requestFullscreen || | |
elem.msRequestFullscreen || | |
elem.mozRequestFullScreen || | |
elem.webkitRequestFullscreen; | |
} | |
fs.call(elem); | |
}, | |
render: function () { | |
var pr = this.props; | |
var st = this.state; | |
return ( | |
el('div', { className: 'emVid', ref: 'videoContainer' }, [ | |
el('video', { key: 'video', src: pr.src, ref: 'video' }), | |
el(Ctr, { | |
key: 'controls', | |
isPlaying: st.isPlaying, | |
onPlay: this.onPlay, | |
onPause: this.onPause, | |
onVolumeChange: this.updateVolume, | |
onFullScreen: this.onFullScreen, | |
volume: st.volume, | |
loading: st.isLoading, | |
duration: st.duration, | |
position: st.position | |
}), | |
el(Prog, { key: 'progress', percent: st.isLoading ? 0 : st.position / st.duration }) | |
]) | |
); | |
} | |
}); | |
ReactDOM.render(React.createElement(Vid, { src: 'vid.mp4' }), document.getElementById('container')); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment