ld-audio
Version:
Last Draft audio plugin
308 lines (273 loc) • 8.76 kB
JavaScript
/*
* Copyright (c) 2016, vace.nz (https://github.com/vacenz)
*
* License: MIT
*/
import React, { Component, PropTypes } from 'react'
import ReactDOM from 'react-dom'
import styled, { keyframes } from 'styled-components'
import 'whatwg-fetch'
class Play extends Component {
render() {
return (
<svg fill='#000000' height='24' viewBox='0 0 24 24' width='24'>
<path d='M8 5v14l11-7z'/>
<path d='M0 0h24v24H0z' fill='none'/>
</svg>
);
}
}
class Pause extends Component {
render() {
return (
<svg fill='#000000' height='24' viewBox='0 0 24 24' width='24'>
<path d='M6 19h4V5H6v14zm8-14v14h4V5h-4z'/>
<path d='M0 0h24v24H0z' fill='none'/>
</svg>
);
}
}
const config = { client_id: 'c5a171200f3a0a73a523bba14a1e0a29' }
let FormatTime = function(totalSeconds) {
let hours = Math.floor(totalSeconds / 3600);
let minutes = Math.floor((totalSeconds - hours * 3600) / 60);
let seconds = Math.floor(totalSeconds - hours * 3600 - minutes * 60);
let result = hours < 1 ? '' : hours + ':';
result += minutes;
result += ':' + (seconds < 10 ? '0' + seconds : seconds);
return result;
}
export default class extends Component {
static get propTypes () {
return {
url: PropTypes.string
}
}
constructor (props) {
let resolveUrl = `http://api.soundcloud.com/resolve.json?url=${props.url}/tracks&client_id=${config.client_id}`
fetch(resolveUrl, {
method: 'get'
}).then((response) => {
return response.json()
}).then((track) => {
this.initPlayer(track.id, track.title)
})
super(props)
this.state = {
loading: true,
playing: false,
audioPlayer: null,
percent_remains: 100,
percent_progress_remains: 100,
duration: '0:00',
current_time: '0:00',
client_id: config.client_id
}
}
initPlayer (audio_id, title) {
this.setState({audio_id, title}, () => {
this.setState({loading: false}, () => {
this.setState({ audioPlayer: ReactDOM.findDOMNode(this.refs.audio) }, () => {
this.state.audioPlayer.ontimeupdate = () => { this.timeUpdated() };
this.state.audioPlayer.onprogress = () => { this.progressUpdated() };
})
})
})
}
togglePlay () {
const { playing, audioPlayer } = this.state;
this.setState({ playing: !playing, showAudioPlayer: true }, () => {
if (audioPlayer.paused) {
audioPlayer.play()
}
if (!this.state.playing) {
if (!audioPlayer.buffered.length) return;
audioPlayer.pause()
}
})
}
timeUpdated () {
const { playing, audioPlayer } = this.state;
if(audioPlayer == undefined){ return }
let percent = (audioPlayer.currentTime / audioPlayer.duration) * 100
this.setState({ current_time: FormatTime(audioPlayer.currentTime) })
this.setState({ duration: FormatTime(audioPlayer.duration) })
this.setState({ percent_remains: 100 - percent})
}
progressUpdated () {
const { playing, audioPlayer } = this.state;
if (audioPlayer == undefined) return;
if (!audioPlayer.buffered.length) return;
var bufferedEnd = audioPlayer.buffered.end(audioPlayer.buffered.length - 1);
if (audioPlayer.duration > 0) {
let percent_remains = (bufferedEnd / audioPlayer.duration) * 100
this.setState({ percent_progress_remains: 100 - percent_remains})
}
}
positionChange (e) {
const { audioPlayer, playing } = this.state
if (!playing) { return }
let elem = ReactDOM.findDOMNode(this.refs.progress)
let elemRect = elem.getClientRects()
let elemLeft = elemRect[0].left
let elemWidth = elemRect[0].width
let clickPositionLeft = e.pageX
let percent_remains = 100 - ( (clickPositionLeft - elemLeft) / elemWidth * 100 )
let newTime = audioPlayer.duration - ( audioPlayer.duration * (percent_remains / 100) )
audioPlayer.currentTime = Math.floor(newTime)
setTimeout( () => {
if(audioPlayer.paused) { this.togglePlay() }
}, 1000)
}
renderPlayerIcons() {
const { playing } = this.state
if (playing) {
return (
<PlayerControlIconPause className='ld-player-control-icons-pause'>
<PlayerControlIcon className='ld-player-control-icon' onClick={::this.togglePlay}>
<Pause />
</PlayerControlIcon>
</PlayerControlIconPause>
);
}
return (
<PlayerControlIconPlay className='ld-player-control-icons-play'>
<PlayerControlIcon className='ld-player-control-icon' onClick={::this.togglePlay}>
<Play />
</PlayerControlIcon>
</PlayerControlIconPlay>
)
}
render () {
const {
playing, audio_id, audioPlayer, percent_remains, title,
duration, current_time, client_id, loading, percent_progress_remains
} = this.state
if (loading) {
return (
<Player className='ld-player'>
<Loader height='36' viewBox='0 0 36 36' width='36' className='ld-player-loader'>
<path d='M28.4,6.6C25.7,4.3,22.3,3,18.5,3C9.9,3,3,9.9,3,18.5S9.9,34,18.5,34c4.3,0,8.1-1.7,11-4.5l-1.8-1.8 c-2.3,2.3-5.6,3.8-9.1,3.8c-7.1,0-13-5.8-13-13s5.8-13,13-13c3.1,0,5.9,1.1,8.1,2.9l-4.5,4.5h8H33H33V2L28.4,6.6z' fill='currentColor' />
</Loader>
</Player>
)
}
if (audio_id !== undefined) {
let streamUrl = `https://api.soundcloud.com/tracks/${audio_id}/stream?client_id=${client_id}`
let time_remains = { transform: `translateX(-${percent_remains.toString()}%)` }
let progress_remains = { transform: `translateX(-${percent_progress_remains.toString()}%)` }
return (
<Player className='player'>
<audio id='audio' preload='none' ref='audio' src={streamUrl} />
<PlayerControl className='ld-player-control'>
{ this.renderPlayerIcons() }
</PlayerControl>
<PlayerDisplay className='ld-player-display' onClick={::this.positionChange}>
<div><h4>{title}</h4></div>
<PlayerProgress className='ld-player-progress'>
<PlayerProgressTime className='ld-player-progress-time'>{current_time}</PlayerProgressTime>
<PlayerProgressBar className='ld-player-progress-bar'>
<PlayerProgressBarWrapper ref='progress' className='ld-player-progress-bar-wrapper'>
<Progress
className='ld-player-progress-bar-progress'
style={progress_remains} />
<PlayerProgressBarPercent
className='ld-player-progress-bar-percent'
style={time_remains} />
</PlayerProgressBarWrapper>
</PlayerProgressBar>
<PlayerProgressTime className='ld-player-progress-time'>{duration}</PlayerProgressTime>
</PlayerProgress>
</PlayerDisplay>
</Player>
)
}
}
}
const Player = styled.div`
transition: color 125ms ease-in-out;
background: white;
position: relative;
padding: 0.5rem;
z-index: 5;
font-size: 1.4rem;
display: flex;
align-items: center;
`
const PlayerControl = styled.div`
margin-right: 1.2rem;
min-width: 6rem;
transition: fill 125ms ease-in-out;
`
const PlayerControlIcon = styled.div`
cursor: pointer;
display: inline-block;
margin-right: 0.4rem;
`
const PlayerControlIconPlay = styled.div`
`
const PlayerControlIconPause = styled.div`
fill: #f50;
`
const PlayerDisplay = styled.div`
flex: 1 1 100%;
margin-bottom: 0;
`
const PlayerProgress = styled.div`
display: flex;
flex-direction: row;
flex-wrap: nowrap;
align-items: center;
margin: 0;
`
const PlayerProgressTime = styled.span`
font-size: 1.1rem;
letter-spacing: 0.1rem;
line-height: 1rem;
color: #444;
flex: 0 1 auto;
vertical-align: middle;
`
const PlayerProgressBar = styled.span`
flex: 1 1 auto;
width: 100%;
display: block;
padding: 0.5rem 0;
margin: 0 0.5rem;
`
const PlayerProgressBarWrapper = styled.span`
cursor: pointer;
display: block;
position: relative;
width: 100%;
height: .4rem;
background: #eee;
border-radius: 2px;
overflow: hidden;
transform: translateZ(0);
`
const Progress = styled.span`
background: pink;
`
const PlayerProgressBarPercent = styled.span`
transition: transform .2s;
position: absolute;
width: 100%;
height: 4px;
background: #f50;
display: block;
`
const rotate360 = keyframes`
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
`
const Loader = styled.svg`
animation: ${rotate360} 0.8s linear infinite;
margin: 0 auto;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 100;
width: 40px;
height: 40px;
`