<section id="index" class="wrapper"></section>
<section class="instruction">
<h1 class="u-title">Custom Audio Player</h1>
<p>User can: </p>
<ol>
<li>Play the audio</li>
<li>Pause the audio</li>
<li>Change audio position using the slider (with mouse / keyboard)</li>
</ol>
<p>In the slider, user can:</p>
<ul>
<li>Use mouse or keyboard to change current time of audio</li>
<li>Can focus the slider</li>
<li>Can use mouse to change position</li>
<li>Can use left or right key in keyboard to change position</li>
</ul>
<p>Keybindings for slider (after focused):</p>
<li>Top: to 100 (max)</li>
<li>Bottom: to 0 (min)</li>
<li>Left: -1 step</li>
<li>Right: +1 step</li>
<li>Top: +10 steps</li>
<li>Bottom: -10 steps</li>
</section>
$theme: #ccc;
//media query
$mobile: "only screen and (min-width : 576px)";
$laptop: "only screen and (min-width : 992px)";
/*------------------------------------*\
#Base
\*------------------------------------*/
body {
font-family: "Lato", "Calibri", sans-serif;
width: 100%;
height: auto;
min-height: 100%;
background: white;
line-height: 1.48;
margin: 0;
padding: 1em;
overflow: auto;
}
html {
width: 100%;
margin: 0;
padding: 0;
overflow: auto;
}
* {
box-sizing: border-box;
}
header,
section {
margin-bottom: 4em;
@media #{$mobile} {
margin-bottom: 2em;
}
}
/*------------------------------------*\
#Audio Player
\*------------------------------------*/
.u-bg {
position: relative;
display: block;
background-image: none !important;
width: auto;
height: auto;
margin: 0;
background-size: cover;
background-position: center;
background-repeat: no-repeat;
}
.c-audio {
display: flex;
align-items: center;
width: 100%;
height: auto;
background: $theme;
border-radius: 0.25em;
padding: 0 1em 0 0;
.l-play {
position: relative;
justify-content: center;
appearance: none;
width: 12%;
min-width: 40px;
height: 40px;
border: 0;
background: $theme;
color: white;
text-align: center;
cursor: pointer;
border-radius: 0;
padding: 0;
&:focus {
outline: none;
box-shadow: 1px 1px 1px 0px rgba(25,25,25,0.2);
}
&__play,
&__pause {
background-size: contain;
background-position: center;
background-repeat: no-repeat;
}
&__play {
background-image: url('data:image/svg+xml;utf8,<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 100 100" style="enable-background:new 0 0 100 100;" xml:space="preserve"><g id="playbtn"><g transform="translate(-670.000000, -353.000000)"><path id="play_btn" style="fill:%23FFFFFF;" d="M740.3,403.5l-31.7-20.8v41.6L740.3,403.5z"/></g></g></svg>');
}
&__pause {
background-image: url('data:image/svg+xml;utf8,<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 100 100" style="enable-background:new 0 0 100 100;" xml:space="preserve"><g id="pause"><g transform="translate(-968.000000, -353.000000)"><path id="pause_btn" style="fill:%23FFFFFF;" d="M1003.6,382.7v41.6h7.9v-41.6H1003.6z M1025.4,382.7v41.6h7.9v-41.6H1025.4z"/></g></g></svg>');
}
}
&__slider {
position: relative;
display: flex;
align-items: center;
width: 88%;
height: 40px;
overflow: hidden;
cursor: pointer;
&:focus {
outline: none;
box-shadow: 1px 1px 1px 0px rgba(25,25,25,0.2);
}
}
&__length {
position: relative;
width: 100%;
height: 0.4em;
border-radius: 0.5em;
background: rgba(19, 19, 19, 0.3);
overflow: hidden;
}
&__bar {
position: absolute;
display: block;
left: 0;
top: 0;
width: 100%;
height: 0.4em;
border-radius: 0.5em;
border: 0;
appearance: none;
transition: none;
overflow: hidden;
fill: white;
stroke: none;
&:focus {
outline: 1;
box-shadow: 13px 12px 12px -4px rgba(19, 19, 19, 0.5);
}
}
}
View Compiled
class Player extends React.Component {
constructor(props) {
super(props);
this.state = {
isPlay: null,
currDuration: 0,
percentage: 0,
seekBar: 0
};
this.audioFile = React.createRef();
this.audioSeekBar = React.createRef();
this.getCurrDuration = this.getCurrDuration.bind(this);
this.controlAudio = this.controlAudio.bind(this);
this.onClick = this.onClick.bind(this);
this.onKeyDown = this.onKeyDown.bind(this);
}
getCurrDuration() {
const audio = this.audioFile.current;
const percentage = ((audio.currentTime / audio.duration) * 100).toFixed(2);
// get current percentage and duration
this.setState({
currDuration: audio.currentTime,
percentage
});
}
controlAudio() {
const audio = this.audioFile.current;
if (audio.duration > 0 && !audio.paused) {
// pause audio and stop counting
audio.pause();
this.setState({
isPlay: false
});
} else {
// play audio
audio.play();
this.setState({
isPlay: true
});
}
}
onKeyDown(e) {
// when user focus in audio slider and clicks keys inside key list, will change current time of audio
const audio = this.audioFile.current;
const isLeft = 37;
const isRight = 39;
const isTop = 38;
const isBottom = 40;
const isHome = 36;
const isEnd = 35;
const keyList = [isLeft,isRight,isTop,isBottom,isHome,isEnd];
if (keyList.indexOf(e.keyCode) >= 0) {
let percentage;
switch(e.keyCode) {
case isLeft:
percentage = parseFloat(this.state.percentage) - 1
break;
case isRight:
percentage = parseFloat(this.state.percentage) + 1
break;
case isTop:
percentage = parseFloat(this.state.percentage) + 10
break;
case isBottom:
percentage = parseFloat(this.state.percentage) - 10
break;
case isHome:
percentage = 0
break;
case isEnd:
percentage = 99.9 // 100 would trigger onEnd, so only 99.9
break;
default:
break;
}
// add boundary for percentage, cannot be bigger than 100 or smaller than zero
if(percentage > 100) {
percentage = 100
} else if(percentage < 0) {
percentage = 0
}
this.setState({
percentage
});
audio.currentTime = audio.duration * (percentage / 100);
}
}
onClick(e) {
const seekBar = this.audioSeekBar.current;
const audio = this.audioFile.current;
const pos =
(e.pageX -
(seekBar.getBoundingClientRect().x ||
seekBar.getBoundingClientRect().left)) /
seekBar.getClientRects()[0].width;
this.setState({
percentage: pos * 100
});
audio.currentTime = audio.duration * pos;
}
render() {
const { percentage, isPlay } = this.state;
const { path, image } = this.props;
return (
<React.Fragment>
<div className="u-bg" style={{ backgroundImage: `url("${image}")` }}>
<div className="c-audio" aria-label="Audio Player" role="region">
<button
title={!isPlay || isPlay === null ? 'Play' : 'Pause'}
className={
!isPlay || isPlay === null
? 'c-audio u-btn l-play l-play__play'
: 'c-audio u-btn l-play l-play__pause'
}
aria-controls="audio1"
onClick={this.controlAudio}
aria-label={!isPlay || isPlay === null ? 'Play' : 'Pause'}
/>
<div
className="c-audio__slider"
onKeyDown={this.onKeyDown}
onClick={this.onClick}
tabIndex="0"
aria-valuetext="seek audio bar"
aria-valuemax="100"
aria-valuemin="0"
aria-valuenow={Math.round(percentage)}
role="slider"
ref={this.audioSeekBar}
>
<div className="c-audio__length">
<svg
className="c-audio__bar"
viewBox="0 0 100% 6"
xmlns="http://www.w3.org/2000/svg"
>
<rect x="0" width={percentage + '%'} height="6" rx="3" />
</svg>
</div>
</div>
</div>
</div>
<audio
className="c-audio__sound"
id="audio1"
src={path}
onTimeUpdate={this.getCurrDuration}
onEnded={() => {
this.audioFile.current.currentTime = 0;
this.setState({
isPlay: false,
currentTime: 0,
percentage: 0
});
}}
ref={this.audioFile}
>
<track kind="captions" />
</audio>
</React.Fragment>
);
}
}
Player.propTypes = {
path: PropTypes.string,
image: PropTypes.string
};
class App extends React.Component {
render() {
return (
<Player
path="//cdn.atrera.com/audio/Marcel_Pequel_-_01_-_One.mp3"
image="//cdn.atrera.com/images/cover_yz2mak.jpg"
/>
);
}
}
ReactDOM.render(<App />, document.getElementById("index"));
View Compiled