Reputation: 21
I am trying to create a music app over an API, where I am rendering a Song component for each song having its own Play, Pause and Stop button, however when I play another song while a song is playing, I want that the previous one will stop playing. Just like other music app.
This is the component to handle play, pause and stop
const useAudio = (song_url) => {
const audio = useRef(new Audio(song_url));
audio.current.preload = "metadata";
const [isPlaying, setPlaying] = useState(false);
const toggleAudio = () => {
setPlaying(!isPlaying);
};
const handleStop = () => {
audio.current.pause();
audio.current.currentTime = 0;
setPlaying(false);
};
useEffect(() => {
isPlaying ? audio.current.play() : audio.current.pause();
}, [isPlaying]);
useEffect(() => {
audio.current.addEventListener("ended", () => setPlaying(false));
return () => {
audio.current.removeEventListener("ended", () => setPlaying(false));
};
}, []);
return [isPlaying, toggleAudio, handleStop];
};
Upvotes: 1
Views: 2296
Reputation: 602
const {useState, useRef} = React;
const AudioPlayer = ({ recording, id, activeId, setActiveId, audioRefs }) => {
const audioRef = useRef(null);
const handlePlay = () => {
if (activeId === id) {
audioRef.current.play();
} else {
// Clicked on a different recording, pause the current one (if any) and play the new one
if (activeId !== null) {
audioRefs[activeId].current.pause();
}
audioRef.current.play();
setActiveId(id);
}
};
const handlePause = () => {
if (activeId === id) {
audioRef.current.pause();
}
}
audioRefs[id] = audioRef;
return (
<div key={id}>
<audio ref={audioRef} controls={true} onPlay={handlePlay} onPause={handlePause}>
<source src={recording} type="audio/mpeg" />
</audio>
</div>
);
};
const RecordingList = () => {
const recordings = [
"https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3",
"https://www.soundhelix.com/examples/mp3/SoundHelix-Song-2.mp3",
"https://www.soundhelix.com/examples/mp3/SoundHelix-Song-3.mp3"
];
const [activeId, setActiveId] = useState(null);
const audioRefs = useRef({});
return (
<div>
{recordings.map((recording, index) => (
<AudioPlayer
key={index}
recording={recording}
id={index}
activeId={activeId}
setActiveId={setActiveId}
audioRefs={audioRefs.current}
/>
))}
</div>
);
};
ReactDOM.createRoot(
document.getElementById("root")
).render(
<RecordingList />
);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.development.js"></script>
Here's a demo where I utilize the audio tag and ref to enable playing only one audio at a time.
Upvotes: 1
Reputation: 21
Well thanks for the suggestions, I really appreciate them, but this works absolutely fine for my case
// creating two states:
// (1) toggle Play-Pause
// (2) storing currently playing song ID
const [isPlaying, setPlaying] = useState(false);
const [currentSong, setCurrentSong] = useState(null);
const audio = useRef(null);
const togglePlay = (e) => {
const song = e.target.id;
if (currentSong === song) {
isPlaying ? audio.current.pause() : audio.current.play();
setPlaying(!isPlaying);
} else {
if (audio.current) {
audio.current.pause();
}
setCurrentSong(song);
setPlaying(true);
audio.current = new Audio(song);
audio.current.play();
}
};
Upvotes: 1
Reputation: 13588
Each time you call useAudio you are creating a new instance of the hook with it's independent state, hence you are unable to control from one hook to another.
In order to control all the songs with one hook, you should probably create a store.
This is a brief example of what I would be doing. Please make necessary changes to suit your needs.
//We first create a store
export const AudioContext = createContext();
export const useAudio = () => {
const context = useContext(AudioContext);
if (!context && typeof window !== 'undefined') {
throw new Error(`useAudio must be used within a AudioContext `);
}
return context;
};
//Then we create the provider
export const AudioProvider = ({ children }) => {
const [ song, _setSong ] = useState()
const setSong = (url) => {
song.pause();
const newSong = new Audio(url)
newSong.play()
setSong(newSong)
}
const pauseSong = () => song.pause()
return <AudioContext.Provider value={{ setSong, pauseSong }}>{children}</AudioContext.Provider>
}
You should then wrap your app with <AudioProvider>
Usage:
const { setSong, pauseSong } = useAudio()
const songSelected = (url) => setSong(url)
setSong
will first pause the original song, then create a new Audio object with the new url, and then play.
Only one song can be played at a time.
Upvotes: 2
Reputation: 2132
My suggestions is write 2 hooks and consider the lifecycle of song plays:
in code:
const [songIdToPlay, setSongIdToPlay] = useState();
Also if there were x songs - the code need to ability to identify which songs to play, so hook can take songId or something like identifier(or url) instead boolean.
In this idea there is probable need for 2 refs.
const audio = useRef(new Audio(song_url));
After triggering the play function(also try to simplify and use only 1 useEffect for better readability) :
useEffect(() => {
setSongIdToPlay(song_url)
}, [isPlaying]);
const toggleAudio = () => {
setPlaying(songToPlay);
};
Upvotes: 0