Skip to content

Commit

Permalink
Merge pull request #58 from contacto-io/feature/CNTO-6850-audio-playe…
Browse files Browse the repository at this point in the history
…r-improvements

[CNTO-6850] - New Audio Player
  • Loading branch information
Abhishek-plivo committed Jul 26, 2023
2 parents 2bca7a7 + 8e79b12 commit 2fbaa51
Show file tree
Hide file tree
Showing 11 changed files with 366 additions and 3 deletions.
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"cascader",
"contacto",
"datepicker",
"listbox"
"listbox",
"wavesurfer"
]
}
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"registry": "https://npm.pkg.github.com"
},
"repository": "git://github.com/contacto-io/contacto-console",
"version": "0.5.0",
"version": "0.5.10",
"main": "build/index.js",
"module": "build/index.es.js",
"files": [
Expand Down Expand Up @@ -105,6 +105,7 @@
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-sortable-hoc": "^2.0.0",
"wavesurfer.js": "^7.0.3",
"simplebar": "^5.3.5",
"simplebar-react": "^2.3.5"
}
Expand Down
36 changes: 36 additions & 0 deletions src/components/AudioPlayer/components/PlayPauseIcon.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/* eslint-disable max-len */
import React from 'react'

export default function PlayPauseIcon({ isPlaying = false, size = 32 }) {
if (!isPlaying) {
return (
<svg
width={size}
height={size}
viewBox={`0 0 32 32`}
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M16.0001 2.6665C8.64008 2.6665 2.66675 8.63984 2.66675 15.9998C2.66675 23.3598 8.64008 29.3332 16.0001 29.3332C23.3601 29.3332 29.3334 23.3598 29.3334 15.9998C29.3334 8.63984 23.3601 2.6665 16.0001 2.6665ZM12.6667 19.5598V12.4398C12.6667 11.3865 13.8401 10.7465 14.7201 11.3198L20.2534 14.8798C21.0667 15.3998 21.0667 16.5998 20.2534 17.1198L14.7201 20.6798C13.8401 21.2532 12.6667 20.6132 12.6667 19.5598Z"
fill="#0040E4"
/>
</svg>
)
}

return (
<svg
width={size}
height={size}
viewBox={`0 0 32 32`}
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M15.9998 2.6665C8.63984 2.6665 2.6665 8.63984 2.6665 15.9998C2.6665 23.3598 8.63984 29.3332 15.9998 29.3332C23.3598 29.3332 29.3332 23.3598 29.3332 15.9998C29.3332 8.63984 23.3598 2.6665 15.9998 2.6665ZM13.3332 21.3332C12.5998 21.3332 11.9998 20.7332 11.9998 19.9998V11.9998C11.9998 11.2665 12.5998 10.6665 13.3332 10.6665C14.0665 10.6665 14.6665 11.2665 14.6665 11.9998V19.9998C14.6665 20.7332 14.0665 21.3332 13.3332 21.3332ZM18.6665 21.3332C17.9332 21.3332 17.3332 20.7332 17.3332 19.9998V11.9998C17.3332 11.2665 17.9332 10.6665 18.6665 10.6665C19.3998 10.6665 19.9998 11.2665 19.9998 11.9998V19.9998C19.9998 20.7332 19.3998 21.3332 18.6665 21.3332Z"
fill="#0040E4"
/>
</svg>
)
}
42 changes: 42 additions & 0 deletions src/components/AudioPlayer/components/PlaybackSpeed.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import React, { useEffect, useState } from 'react'
import { Popover } from 'antd'
import { Button } from '../../Button/index'

const speeds = [0.8, 1, 1.2, 1.5, 1.7, 2, 2.5]
export default function PlaybackSpeed({ waveSurfer }) {
const [speed, setSpeed] = useState(waveSurfer?.getPlaybackRate())

const handleSpeedChange = (speed) => {
setSpeed(speed)
waveSurfer?.setPlaybackRate(speed)
}

useEffect(() => {
setSpeed(waveSurfer?.getPlaybackRate())
}, [waveSurfer])

return (
<Popover
overlayClassName="contacto-player-speed"
title="Playback Speed"
content={() => (
<div>
{speeds.map((speed) => (
<Button
key={speed}
className="contacto-player-speed"
type={speed === waveSurfer?.getPlaybackRate() ? 'secondary' : 'table-action-link'}
onClick={() => handleSpeedChange(speed)}
>
{speed}x
</Button>
))}
</div>
)}
>
<Button className="contacto-player-speed-trigger" type="secondary">
{speed}x
</Button>
</Popover>
)
}
83 changes: 83 additions & 0 deletions src/components/AudioPlayer/helpers/useWaveSurfer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { useLayoutEffect, useRef, useState } from 'react'

import WaveSurfer from 'wavesurfer.js'
import { generateId } from './utils'

const defaultPlayerConfig = (playerId) => ({
id: playerId,
isPlaying: false,
loading: true,
})
const defaultDurationConfig = () => ({
totalDuration: 0,
currentDuration: 0,
})
export default function useWaveSurfer(url) {
const playerId = useRef(generateId('contacto-player-wave-')).current
const [playerConfig, setPlayerConfig] = useState(defaultPlayerConfig(playerId))
const [durationConfig, setDurationConfig] = useState(defaultDurationConfig())
const [waveSurfer, setWaveSurfer] = useState(null)

useLayoutEffect(() => {
let wave = null
if (url) {
wave = WaveSurfer.create({
container: `#${playerId}`,
url: url,
waveColor: '#C3D2FF',
progressColor: '#0040E4',
cursorColor: '#0040E4',
responsive: true,
height: 24,
barHeight: 3,
barMinHeight: 1,
barWidth: 1,
barGap: 4,
hideScrollbar: true,
closeAudioContext: true,
partialRender: true,
})
wave.on('load', () => {
setPlayerConfig((prevConfig) => ({ ...prevConfig, loading: true }))
})
wave.on('ready', () => {
setDurationConfig((prevConfig) => ({
...prevConfig,
totalDuration: parseInt(wave.getDuration()),
}))
setPlayerConfig((prevConfig) => ({ ...prevConfig, loading: false }))
})
wave.on('audioprocess', () => {
const duration = parseInt(wave.getCurrentTime())
if (durationConfig.currentDuration !== duration) {
setDurationConfig((prevConfig) => ({
...prevConfig,
currentDuration: duration,
}))
}
})
wave.on('play', () => {
setPlayerConfig((prevConfig) => ({ ...prevConfig, isPlaying: true }))
})
wave.on('pause', () => {
setPlayerConfig((prevConfig) => ({ ...prevConfig, isPlaying: false }))
})
wave.on('finish', () => {
setPlayerConfig((prevConfig) => ({ ...prevConfig, isPlaying: false }))
})

setWaveSurfer(wave)
}

return () => {
wave?.stop()
wave?.destroy()
setWaveSurfer(null)
setPlayerConfig(defaultPlayerConfig(playerId))
setDurationConfig(defaultDurationConfig())
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [playerId, url])

return { waveSurfer, playerConfig, durationConfig }
}
12 changes: 12 additions & 0 deletions src/components/AudioPlayer/helpers/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export const getDisplayTime = (seconds) => {
const int = +seconds / 60
let minutes = parseInt(int)
seconds = seconds % 60
minutes = (minutes < 10 ? '0' : '') + minutes
seconds = (seconds < 10 ? '0' : '') + seconds
return minutes + ':' + seconds
}

export const generateId = (prefix) => {
return `${prefix}${Math.random().toString(36).slice(2)}`
}
45 changes: 45 additions & 0 deletions src/components/AudioPlayer/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import React from 'react'
import Text from 'antd/lib/typography/Text'
import { forwardRef } from 'react'
import PlayPauseIcon from './components/PlayPauseIcon'
import useWaveSurfer from './helpers/useWaveSurfer'
import PlaybackSpeed from './components/PlaybackSpeed'
import { Button } from '../Button/index'
import { Icon } from '../Icon/index'
import { getDisplayTime } from './helpers/utils'
import './styles.scss'

const AudioPlayer = forwardRef((props, ref) => {
const { className, url } = props
const { waveSurfer, playerConfig, durationConfig } = useWaveSurfer(url)

const { isPlaying, loading } = playerConfig
const { totalDuration, currentDuration } = durationConfig

return (
<div
className={`contacto-audio-player ${className ?? ''} ${loading ? 'loading' : ''}`}
ref={ref}
>
<div className="audio-controls">
<Button
className="audio-controls-play-pause"
type="default"
onClick={() => !loading && waveSurfer?.playPause()}
icon={loading ? <Icon.Loading size={30} /> : <PlayPauseIcon isPlaying={isPlaying} />}
/>
<div className="audio-controls-time left">
<Text type="caption">{getDisplayTime(currentDuration)}</Text>
</div>
<div id={playerConfig.id} className="audio-controls-wave-bar" />
<div className="audio-controls-time right">
<Text type="caption">{getDisplayTime(totalDuration)}</Text>
</div>
<PlaybackSpeed waveSurfer={waveSurfer} />
</div>
</div>
)
})

AudioPlayer.displayName = 'AudioPlayer'
export { AudioPlayer }
14 changes: 14 additions & 0 deletions src/components/AudioPlayer/index.stories.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import React from 'react'
import { AudioPlayer } from './'

export default {
title: 'Components/Audio Player',
component: AudioPlayer,
}

const Template = (args) => <AudioPlayer {...args} />

export const Default = Template.bind({})
Default.args = {
url: 'https://wavesurfer-js.org/wavesurfer-code/examples/audio/mono.mp3',
}
Loading

0 comments on commit 2fbaa51

Please sign in to comment.