The problem
I'm trying to loop some audio in a React app created with create-react-app using useEffect. I've got it working but there's a short delay between the audio ending and re-starting. I want to use the audio as backing tracks to play guitar to so I need it to loop perfectly smoothly. I'm confident that the track length is correct, I recorded it myself and exported exactly 8 bars, and it loops fine in iTunes.
Current code
Thanks to the accepted answer in this question, my audio player function works fine, and currently looks like this:
import React, { useState, useEffect } from 'react'
const useAudio = audioPath => {
const [audio] = useState(new Audio(audioPath))
const [playing, setPlaying] = useState(false)
const toggle = () => setPlaying(!playing)
useEffect(() => {
playing ? audio.play() : audio.pause()
},
[playing, audio]
)
useEffect(() => {
audio.addEventListener('ended', () => {
audio.currentTime = 0
audio.play()
setPlaying(true)
})
}, [audio])
return [playing, toggle]
}
const Player = ({ audioPath }) => {
const [playing, toggle] = useAudio(audioPath)
return (
<div>
<button onClick={toggle}>{playing ? 'Pause' : 'Play'}</button>
</div>
)
}
export default Player
The audioPath is passed in and is just a relative path, that loads fine. It plays fine, it pauses fine, it does loop, just with a tiny delay between loops.
What I've tried
As you can see from the code, I've been trying to hijack the audio ended event and setting the audio back to the start of the track but obviously this isn't instant - I'm not really sure how to handle this. I've tried in my first useEffect function checking the time of the audio and if it's within say 500ms of the end of the track setting the time back to 0 but I couldn't get that working, and it seems very hacky and unreliable anyway. Ideally I'm after a proper solution that will work with any tracks as I want to add more.
Demo
Go to the very bottom of the GitHub pages site where this is hosted, expand the very bottom panel and hit play.
Ive been playing around with audio a bit recently as well and found that the react-h5-audio-player npm package was the best option for me.
Its got good storyboard examples which include looping and custom controls etc which you might just be able to hide the display by using CSS if you want to keep the single play/pause button you currently have.
Related
I am not able to destroy embedded youtube video on plyr player. player.destroy() method is called without any error but it does not destroy the player.
For this reason when I try to open another embedded video, it just loads the previous embedded video.
When I checked isEmbed property of the player it returns false. It should be true as I am playing youtube video.
I am using react js.
let video = document.getElementById('player-embed');
var youtubeEmbedId = extractYoutubeEmbedId(options.source.src)
window.player = new Plyr(video, defaultOptions);
useEffect(() => {
return () => {
window.player.destroy();
if (window.hls) {
window.destroyHLS();
}
}
}, [])
return (
<div id="player-embed" data-plyr-provider="youtube" data-plyr-embed-id={youtubeEmbedId}/>
);
I found this codepen example for plyr youtube video example and it is working as expected. isEmbed property returns true and player.destroy() method indeed destroy the player. Don't know why it's not working for me.
It seems like window.player = new Plyr() is called everytime your react component is rerendered, therefore you keep creating new Plyr instances. Try moving Plyr initialization to useState like const [player] = useState(new Plyr()) and then assign it to window in useEffect so it would get called only once.
It still might have some bugs so you would be better off using Plyr official package for react https://github.com/chintan9/plyr-react
I have a StackBlitz minimum code example which illustrates the problem. For brevity, I've also placed the problematic code below.
Whenever the user clicks on a track of a number of tracks, I want the Audio component to immediately play that track, think Spotify etc.
Currently, the source of the audio component is updating, but it does not play the track unless there is a triple click.
I'm not sure why a triple click is needed to create a successful play call.
const changeSource = newSource => {
setSource(newSource);
togglePlayPause();
};
Here is the setSource hook, which is initialised to a default track:
const [source, setSource] = useState(
'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-4.mp3'
);
const togglePlayPause = () => {
const prevValue = isPlaying;
setIsPlaying(!prevValue);
if (!prevValue) {
audioPlayer.current.play();
} else {
audioPlayer.current.pause();
}
};
It'll do that, unless you preload your audio data. The <audio> element would need to go out, bring the data into the browser, process it, then begin to play. There is a way to try to preload the data. By setting <audio preload="auto"> it will "attempt" to preload tracks on page load. But, when dynamically setting source, you can't really get that data until you know what the source is. You can get around this, a little, by using the autoplay attribute, which will cause the browser to automatically begin fetching the source once it is set. (But then, it will also start playing it right away as well.)
I am making game in browser and use sound effects for example shot, explosion and for every generated instance of classes there is also creating new Audio object which is eating memory so much and app is crashing after 2/3 minutes thats mean is getting very slow. Is any better way to do this? Maybe creating new Audio() in another place but just once and call it when need, not every time when generating new enemy, bullet etc.
For example:
class Bullet extends Common {
constructor() {
this.element = document.createElement("div");
this.audio = new Audio("./audio/LaserShot.wav");
}
And in upper class Spaceship I call it every time I shot pressing space:
executeShot() {
const bullet = new Bullet(this.getCurrentPosition(), this.element.offsetTop, this.area);
bullet.init();
this.bullets.push(bullet);
}
Not sure if this works great in all scenario, but you can try the following code, and see if it works.
<button class="btn">Click</button>
class AudioService {
constructor(initialsetup = 1) {
this._audios = [];
for (let i = 0; i < initialsetup; i++) {
this._audios.push(new Audio());
}
}
/**
* use to get available audio
*/
_getAudioElemToPlay() {
const audios = this._audios.filter(audio => {
// if the audio is empty, without a valid url or if the audio is ended
// TODO: not sure if these attributes are more than enough
return !audio.duration || audio.ended;
});
console.log('audios', audios);
if (audios.length == 0) {
const audio = new Audio();
this._audios.push(audio);
return audio;
}
return audios[0];
}
playAudio(url) {
const player = this._getAudioElemToPlay();
player.src = url;
player.load();
player.play();
}
}
const audioService = new AudioService();
let index = 0;
document.querySelector('.btn').addEventListener('click', function() {
index++;
const audioList = new Array(12).fill(0).map((value, index) => {
return `https://www.soundhelix.com/examples/mp3/SoundHelix-Song-${index}.mp3`;
});
audioService.playAudio(audioList[index % audioList.length]);
})
Here is the link to run the above code, https://codepen.io/hphchan/pen/xxqbezb.
You may also change the audio to other audio as you like.
My main idea to solve the issue, is by reusing the audio element created, by having an array to store it, and reuse the element once it finish playing.
Of course, for the demo, I am playing the audio by using a click button. But definitely, you can plug it into your game.
Hope the solution may help you. In case there are any cases not covering, as I have not much exposure to this area, it would be nice if you can post your modified solution here, so we can all learn together.
Have you looked at the Web Audio API? If it works for you, a single AudioBuffer can hold the audio data in memory for a given cue, and you can play it multiple times by spawning AudioBufferSourceNode objects. If you have many different sounds playing, this might not be much help, but if you are reusing sounds continuously (many laser shots), this could a big help. Another benefit is that this way of playing sounds is pretty low latency.
I just used this for the first time, getting it to work yesterday. But I'm loading it with raw PCM data (floats ranging from -1 to 1). There is surely a way to load this or an equivalent in-memory structure with a wav, but I'm too new to the API to know yet how to do this.
Sandbox demo of my problem: https://codesandbox.io/s/jolly-fermat-j23w9
I'm trying to implement a feature on my React site where a user clicks an element, and the audio plays.
On desktop, this is working fine. But on iOS devices, I am encountering an "Unhandled Rejection (NotAllowedError)". This error usually is caused by autoplaying media on iOS devices. However, in this case, the user must click the element to start the audio, so this error should not be happening.
My suspicion is that because the component rerenders on state change, React doesn't know that the user had to interact with the site in order to trigger the audio.
Here is the basic code:
// audio playing function, found on this stackoverflow question:
// https://stackoverflow.com/questions/47686345/playing-sound-in-reactjs
const useAudio = url => {
const [audio] = useState(new Audio(url));
const [playing, setPlaying] = useState(false);
const toggle = () => setPlaying(!playing);
useEffect(() => {
playing ? audio.play() : audio.pause();
}, [playing]);
return [playing, toggle];
};
// url of audio is passed in as prop from App.js
const PlayAudio = ({ url }) => {
const [playing, toggle] = useAudio(url);
return (
<>
<PlayButton onClick={toggle}>{playing ? "Pause" : "Play"}</PlayButton>
</>
);
};
Tested on an iPhone using Safari, Chrome, and Firefox browsers.
Again, a full working demonstration can be found here, and you need to check on an iOS device to see the error: https://codesandbox.io/s/jolly-fermat-j23w9
Any help is appreciated.
Created example with yours code but another approach:
<audio ref....>
https://codesandbox.io/s/elegant-black-c0jr8
This should work: setTimeout(audio.play)
So I'm trying to make a simple Digital audio workstation(D.A.W) kind of thing in react.
To solve this problem I'm using Tonejs Transport to play and sync multiple songs on Transport timeline and is working fine but problem arises when i try to pause it. Eventually it pauses when i click to pause but also it crashes the application saying Error: buffer is either not set or not loaded
I tried to do something like this but this didn't solve the problem.
Tone.Buffer.on('load', () => {
if (this.props.play)
Tone.Transport.start()
if (!this.props.play)
Tone.Transport.stop()
})
This is where I'm setting the player
Player = (src, startTime,volume) => {
Tone.Transport.bpm.value = 108;
Tone.Transport.loop = false;
let buff = new Tone.Buffer({
"url": `/MP3s/${src}`,
});
let play = new Tone.Player(buff).toMaster().sync().start(startTime)
Tone.Buffer.on('load', () => {
if (this.props.play)
Tone.Transport.start()
if (!this.props.play)
Tone.Transport.stop()
})
}
It's showing error on the let play line in code and i just couldn't figure out on my own about the reason of problem and how to resolve it. Also I'm playing these files from the public folder of my react app.