rehowl
Version:
Opinionated React wrapper for Howler.js
292 lines (291 loc) • 10 kB
JavaScript
import { useEffect, useRef, useState } from 'react';
/**
* Plays and controls sounds from a Howl.
*
* The Howl instance, provided by the `howl` prop, can come from `useHowl`,
* `<Rehowl />`, or provided from your own use of howler.js
*
* You can render **multiple `<Play />` components** for a single
* Howl instance in order to play multiple sounds or sprites at once.
*
* Event handlers fire only for the `<Play />` that they correspond to,
* and not for every sound playing off of the Howl instance like in howler.js
*/
export function Play(props) {
var howl = props.howl, pause = props.pause, sprite = props.sprite, mute = props.mute, _a = props.volume, volume = _a === void 0 ? 1 : _a, seek = props.seek, fade = props.fade, stop = props.stop, rate = props.rate, loop = props.loop, children = props.children;
var shouldPlay = !pause && !stop;
// Don't attempt to play the Howl until both pause and stop are false.
var _b = useState(shouldPlay), initialized = _b[0], setInitialized = _b[1];
var _c = useState(null), playId = _c[0], setPlayId = _c[1];
var _d = useState(true), playing = _d[0], setPlaying = _d[1];
var _e = useState(false), stopped = _e[0], setStopped = _e[1];
var _f = useState(false), unlocked = _f[0], setUnlocked = _f[1];
var _g = useState(false), seeking = _g[0], setSeeking = _g[1];
// We use refs for the callbacks so that they can be dynamic.
var onPlay = useRef(null);
var onPlayError = useRef(null);
var onEnd = useRef(null);
var onPause = useRef(null);
var onStop = useRef(null);
var onMute = useRef(null);
var onVolume = useRef(null);
var onSeek = useRef(null);
var onFade = useRef(null);
var onRate = useRef(null);
useEffect(function () {
onPlay.current = props.onPlay || null;
}, [props.onPlay]);
useEffect(function () {
onPlayError.current = props.onPlayError || null;
}, [props.onPlayError]);
useEffect(function () {
onEnd.current = props.onEnd || null;
}, [props.onEnd]);
useEffect(function () {
onPause.current = props.onPause || null;
}, [props.onPause]);
useEffect(function () {
onStop.current = props.onStop || null;
}, [props.onStop]);
useEffect(function () {
onMute.current = props.onMute || null;
}, [props.onMute]);
useEffect(function () {
onVolume.current = props.onVolume || null;
}, [props.onVolume]);
useEffect(function () {
onSeek.current = props.onSeek || null;
}, [props.onSeek]);
useEffect(function () {
onFade.current = props.onFade || null;
}, [props.onFade]);
useEffect(function () {
onRate.current = props.onRate || null;
}, [props.onRate]);
// Only load the rest of the component once the user wants to play.
useEffect(function () {
if (initialized)
return;
if (!shouldPlay)
return;
setInitialized(true);
}, [initialized, shouldPlay]);
useEffect(function () {
if (!howl || !shouldPlay || !initialized)
return;
// Play the sound and get its ID.
var currentPlayId = howl.play(sprite);
setPlayId(currentPlayId);
// Initialize with the right settings.
howl.volume(volume, currentPlayId);
if (mute !== undefined) {
howl.mute(mute, currentPlayId);
}
if (rate !== undefined) {
howl.rate(rate, currentPlayId);
}
// Update children on initial play.
howl.once('play', function (id) {
if (id !== currentPlayId)
return;
setUnlocked(true);
});
// Set up event listeners, filtered to this play ID.
howl.on('play', function (id) {
if (id !== currentPlayId)
return;
if (onPlay.current) {
onPlay.current();
}
});
howl.on('playerror', function (id) {
if (id !== currentPlayId)
return;
if (onPlayError.current) {
onPlayError.current();
}
});
howl.on('pause', function (id) {
if (id !== currentPlayId)
return;
if (onPause.current) {
onPause.current();
}
});
howl.on('end', function (id) {
if (id !== currentPlayId)
return;
if (onEnd.current) {
onEnd.current();
}
});
howl.on('stop', function (id) {
if (id !== currentPlayId)
return;
if (onStop.current) {
onStop.current();
}
});
howl.on('mute', function (id) {
if (id !== currentPlayId)
return;
if (onMute.current) {
onMute.current();
}
});
howl.on('volume', function (id) {
if (id !== currentPlayId)
return;
if (onVolume.current) {
onVolume.current();
}
});
howl.on('rate', function (id) {
if (id !== currentPlayId)
return;
if (onRate.current) {
onRate.current();
}
});
howl.on('seek', function (id) {
if (id !== currentPlayId)
return;
setSeeking(false);
if (onSeek.current) {
onSeek.current();
}
});
howl.on('fade', function (id) {
if (id !== currentPlayId)
return;
if (onFade.current) {
onFade.current();
}
});
return function () {
howl.stop(currentPlayId);
setInitialized(false);
setUnlocked(false);
howl.off('play', undefined, currentPlayId);
howl.off('playerror', undefined, currentPlayId);
howl.off('pause', undefined, currentPlayId);
howl.off('end', undefined, currentPlayId);
howl.off('stop', undefined, currentPlayId);
howl.off('mute', undefined, currentPlayId);
howl.off('volume', undefined, currentPlayId);
howl.off('rate', undefined, currentPlayId);
howl.off('seek', undefined, currentPlayId);
howl.off('fade', undefined, currentPlayId);
setPlayId(null);
};
}, [initialized, howl, sprite]);
useEffect(function () {
/**
* Use playing in state because queued events (like in the above useEffect)
* will not apply immediately, so it's possible for us to attempt playing
* twice when the sound is initialized. This causes some issues with Howler.
*/
if (!howl || !playId || !unlocked)
return;
if (stop) {
if (!stopped) {
howl.stop(playId);
setStopped(true);
setPlaying(false);
}
return;
}
if (playing && pause) {
howl.pause(playId);
setStopped(false);
setPlaying(false);
}
else if (!playing && !pause) {
howl.play(playId);
setStopped(false);
setPlaying(true);
}
}, [howl, playId, stopped, unlocked, playing, pause, stop]);
useEffect(function () {
if (!howl || !playId || !unlocked)
return;
if (mute === undefined)
return;
howl.mute(mute, playId);
}, [howl, playId, unlocked, mute]);
useEffect(function () {
if (!howl || !playId || !unlocked)
return;
if (volume === undefined)
return;
howl.volume(volume, playId);
}, [howl, playId, unlocked, volume]);
useEffect(function () {
if (!howl || !playId || !unlocked)
return;
if (seek === undefined)
return;
setSeeking(true);
howl.seek(seek, playId);
}, [howl, playId, unlocked, seek]);
useEffect(function () {
if (!howl || !playId || !unlocked)
return;
if (!fade)
return;
var from = fade[0], to = fade[1], duration = fade[2];
howl.fade(from, to, duration, playId);
}, [howl, playId, unlocked, JSON.stringify(fade)]);
useEffect(function () {
if (!howl || !playId || !unlocked)
return;
if (rate === undefined)
return;
howl.rate(rate, playId);
}, [howl, playId, unlocked, rate]);
useEffect(function () {
if (!howl || !playId || !unlocked)
return;
if (loop === undefined)
return;
howl.loop(loop, playId);
}, [howl, playId, unlocked, loop]);
if (!children || !howl)
return null;
return children({
duration: function () {
if (!howl || !playId)
return 0;
if (sprite)
return howl.duration(playId);
return howl.duration();
},
playing: function () {
if (!howl || !playId)
return false;
return howl.playing(playId);
},
seek: function () {
if (!howl || !playId)
return 0;
// Get seek
if (seeking && seek !== undefined)
return seek;
var position = howl.seek(playId);
if (typeof position !== 'number') {
return 0;
}
return position;
},
volume: function () {
var propsVolume = volume === undefined ? 1 : volume;
if (!howl || !playId)
return propsVolume;
var currentVolume = howl.volume(playId);
if (typeof currentVolume !== 'number') {
return 0;
}
return currentVolume;
},
});
}