diff --git a/__tests__/tests/player.test.js b/__tests__/tests/player.test.js
index 6678caaf..4e876338 100644
--- a/__tests__/tests/player.test.js
+++ b/__tests__/tests/player.test.js
@@ -1593,11 +1593,12 @@ describe('', () => {
)
wrapper.find('.play-btn').simulate('click')
+ await sleep(300)
wrapper.find('.play-btn').simulate('click')
expect(wrapper.state().soundValue).toEqual(1)
expect(wrapper.state().currentAudioVolume).toEqual(1)
- await sleep(200)
+ await sleep(300)
expect(onAudioVolumeChange).not.toHaveBeenCalled()
expect(wrapper.state().soundValue).toEqual(1)
@@ -1609,8 +1610,16 @@ describe('', () => {
volume: 1,
}
const fn = jest.fn()
- adjustVolume(audio, 0, { duration: 0 }).then(fn)
+ const { fadeInterval, updateIntervalEndVolume } = adjustVolume(
+ audio,
+ 1,
+ 0,
+ { duration: 0 },
+ fn,
+ )
await sleep(100)
+ expect(fadeInterval).toBeUndefined()
+ expect(updateIntervalEndVolume).toBeUndefined()
expect(audio.volume).toStrictEqual(0)
expect(fn).toHaveBeenCalledTimes(1)
})
@@ -1620,8 +1629,16 @@ describe('', () => {
volume: 1,
}
const fn = jest.fn()
- adjustVolume(audio, 1, { duration: 100 }).then(fn)
+ const { fadeInterval, updateIntervalEndVolume } = adjustVolume(
+ audio,
+ 1,
+ 1,
+ { duration: 100 },
+ fn,
+ )
await sleep(100)
+ expect(fadeInterval).toBeUndefined()
+ expect(updateIntervalEndVolume).toBeUndefined()
expect(audio.volume).toStrictEqual(1)
expect(fn).toHaveBeenCalledTimes(1)
})
@@ -1631,7 +1648,15 @@ describe('', () => {
volume: 1,
}
const fn = jest.fn()
- adjustVolume(audio, 0, { duration: 200 }).then(fn)
+ const { fadeInterval, updateIntervalEndVolume } = adjustVolume(
+ audio,
+ audio.volume,
+ 0,
+ { duration: 200 },
+ fn,
+ )
+ expect(fadeInterval).not.toBeUndefined()
+ expect(updateIntervalEndVolume).not.toBeUndefined()
expect(audio.volume).toStrictEqual(1)
expect(fn).not.toHaveBeenCalled()
@@ -1640,4 +1665,28 @@ describe('', () => {
expect(Math.floor(audio.volume)).toStrictEqual(0)
expect(fn).toHaveBeenCalledTimes(1)
})
+
+ it('should respond to end volume changes for fade-ins', async () => {
+ const audio = {
+ volume: 0,
+ }
+ const fn = jest.fn()
+ const { fadeInterval, updateIntervalEndVolume } = adjustVolume(
+ audio,
+ audio.volume,
+ 1,
+ { duration: 200 },
+ fn,
+ )
+ expect(fadeInterval).not.toBeUndefined()
+ expect(updateIntervalEndVolume).not.toBeUndefined()
+ updateIntervalEndVolume(0.5)
+ expect(audio.volume).toStrictEqual(0)
+ expect(fn).not.toHaveBeenCalled()
+
+ await sleep(500)
+
+ expect(audio.volume).toStrictEqual(0.5)
+ expect(fn).toHaveBeenCalledTimes(1)
+ })
})
diff --git a/src/config/volumeFade.js b/src/config/volumeFade.js
new file mode 100644
index 00000000..13ccba27
--- /dev/null
+++ b/src/config/volumeFade.js
@@ -0,0 +1,5 @@
+export const VOLUME_FADE = {
+ IN: 'in',
+ OUT: 'out',
+ NONE: 'none',
+}
diff --git a/src/index.js b/src/index.js
index 8b6a5658..73476ad8 100644
--- a/src/index.js
+++ b/src/index.js
@@ -49,6 +49,7 @@ import PLAY_MODE from './config/playMode'
import PROP_TYPES from './config/propTypes'
import { sliderBaseOptions } from './config/slider'
import { THEME } from './config/theme'
+import { VOLUME_FADE } from './config/volumeFade'
import LOCALE_CONFIG from './locale'
import Lyric from './lyric'
import {
@@ -123,7 +124,9 @@ export default class ReactJkMusicPlayer extends PureComponent {
isAutoPlayWhenUserClicked: false,
playIndex: this.props.playIndex || this.props.defaultPlayIndex || 0,
canPlay: false,
- isVolumeFadeChang: false,
+ currentVolumeFade: VOLUME_FADE.NONE,
+ currentVolumeFadeInterval: undefined,
+ updateIntervalEndVolume: undefined,
}
static defaultProps = {
@@ -449,6 +452,20 @@ export default class ReactJkMusicPlayer extends PureComponent {
return null
}
+ let actionButtonIcon
+
+ if (playing) {
+ if (this.state.currentVolumeFade === VOLUME_FADE.OUT) {
+ actionButtonIcon = this.iconMap.play
+ } else {
+ actionButtonIcon = this.iconMap.pause
+ }
+ } else if (this.state.currentVolumeFade === VOLUME_FADE.IN) {
+ actionButtonIcon = this.iconMap.pause
+ } else {
+ actionButtonIcon = this.iconMap.play
+ }
+
return createPortal(
- {playing ? this.iconMap.pause : this.iconMap.play}
+ {actionButtonIcon}
)}
{
@@ -1183,21 +1205,92 @@ export default class ReactJkMusicPlayer extends PureComponent {
onTogglePlay = () => {
if (this.state.audioLists.length >= 1) {
const { fadeIn, fadeOut } = this.props.volumeFade || {}
- this.setState({ isVolumeFadeChang: fadeIn || fadeOut })
-
- if (this.state.playing) {
- adjustVolume(this.audio, 0, {
- duration: fadeOut,
- }).then(() => {
- this.audio.pause()
+ const { currentVolumeFade, currentVolumeFadeInterval } = this.state
+ const isCurrentlyFading =
+ currentVolumeFade === VOLUME_FADE.IN ||
+ currentVolumeFade === VOLUME_FADE.OUT
+
+ /**
+ * Currently in middle of fading in/out, so need to cancel the current interval and do the opposite action.
+ * E.g. if currently fading out, then we need to cancel the fade-out and do a fade-in starting at current volume.
+ */
+ if (isCurrentlyFading) {
+ // Clear current fade-in/out
+ clearInterval(currentVolumeFadeInterval)
+ this.setState({
+ currentVolumeFadeInterval: undefined,
+ updateIntervalEndVolume: undefined,
})
- return
}
- adjustVolume(this.audio, this.state.currentAudioVolume, {
- duration: fadeIn,
- })
- this.setState({ isAutoPlayWhenUserClicked: true }, this.loadAndPlayAudio)
+ // Currently playing track or in the middle of fading in
+ if (
+ (!isCurrentlyFading && this.state.playing) ||
+ currentVolumeFade === VOLUME_FADE.IN
+ ) {
+ this.setState({ currentVolumeFade: VOLUME_FADE.OUT })
+ // Fade in from current volume to 0
+ const {
+ fadeInterval: fadeOutInterval,
+ updateIntervalEndVolume,
+ } = adjustVolume(
+ this.audio,
+ this.audio.volume,
+ 0,
+ {
+ duration: fadeOut,
+ },
+ () => {
+ this.audio.pause()
+ this.setState({
+ currentVolumeFade: VOLUME_FADE.NONE,
+ currentVolumeFadeInterval: undefined,
+ updateIntervalEndVolume: undefined,
+ })
+ // It's possible that the volume level in the UI has changed since beginning of fade
+ this.audio.volume = this.state.soundValue
+ },
+ )
+
+ this.setState({
+ currentVolumeFadeInterval: fadeOutInterval,
+ updateIntervalEndVolume,
+ })
+ } else {
+ this.setState({ currentVolumeFade: VOLUME_FADE.IN })
+ const startVolume = isCurrentlyFading ? this.audio.volume : 0
+ const endVolume = this.state.soundValue
+ // Always fade in from 0 to current volume
+ const {
+ fadeInterval: fadeInInterval,
+ updateIntervalEndVolume,
+ } = adjustVolume(
+ this.audio,
+ startVolume,
+ endVolume,
+ {
+ // If starting track from beginning, start immediately without fade-in
+ duration: fadeIn,
+ },
+ () => {
+ this.setState({
+ currentVolumeFade: VOLUME_FADE.NONE,
+ currentVolumeFadeInterval: undefined,
+ updateIntervalEndVolume: undefined,
+ })
+ this.audio.volume = this.state.soundValue
+ },
+ )
+
+ this.setState(
+ {
+ currentVolumeFadeInterval: fadeInInterval,
+ updateIntervalEndVolume,
+ isAutoPlayWhenUserClicked: true,
+ },
+ this.loadAndPlayAudio,
+ )
+ }
}
}
@@ -1436,8 +1529,8 @@ export default class ReactJkMusicPlayer extends PureComponent {
onAudioVolumeChange = () => {
const { volume } = this.audio
- const { isVolumeFadeChang } = this.state
- if (isVolumeFadeChang) {
+ const { currentVolumeFade } = this.state
+ if (currentVolumeFade !== VOLUME_FADE.NONE) {
return
}
this.setState({
diff --git a/src/utils.js b/src/utils.js
index a9be88bf..dc4f9120 100644
--- a/src/utils.js
+++ b/src/utils.js
@@ -54,24 +54,42 @@ export function swing(p) {
export function adjustVolume(
element,
- newVolume,
+ startVolume,
+ endVolume,
{ duration = 1000, easing = swing, interval = 13 } = {},
+ callback,
) {
- const originalVolume = element.volume
- const delta = newVolume - originalVolume
+ let delta = endVolume - startVolume
if (!delta || !duration || !easing || !interval) {
- element.volume = newVolume
- return Promise.resolve()
+ element.volume = endVolume
+ callback()
+ return { fadeInterval: undefined, updateIntervalEndVolume: undefined }
}
+
const ticks = Math.floor(duration / interval)
let tick = 1
- return new Promise((resolve) => {
- const timer = setInterval(() => {
- element.volume = originalVolume + easing(tick / ticks) * delta
- if (++tick === ticks) {
- clearInterval(timer)
- resolve()
- }
- }, interval)
- })
+
+ const updateIntervalEndVolume = (newVolume) => {
+ endVolume = newVolume
+ }
+
+ const timer = setInterval(() => {
+ // End volume may have changed in middle of fading
+ const newDelta = endVolume - startVolume
+ if (newDelta !== delta) {
+ delta = newDelta
+ }
+
+ element.volume = startVolume + easing(tick / ticks) * delta
+ if (++tick >= ticks) {
+ element.volume = endVolume
+ clearInterval(timer)
+ callback()
+ }
+ }, interval)
+
+ return {
+ fadeInterval: timer,
+ updateIntervalEndVolume,
+ }
}