Why isn't clicking on an audio track immediately triggering play? - javascript

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.)

Related

Good-willed Back Button Redirect Script?

I am trying to put the Back Button Redirect Script function to good use. I have a plugin which plays background (user-initiated) music on my site. As long as the user clicks forward, the music streams continuously and nearly uninterrupted from page to page, without restarting. If the user clicks the back button (or refreshes), the music stops and they must manually press play to restart the stream. The author says they have no way to resolve it. I'm not giving up just yet.
My thought is, why not use JavaScript to record the browser's previous page URL, then capture the back button trigger and send the user "forward" to that URL, thus keeping the music stream intact while honoring the user's desire to go back a page?
Conceptually, being a supernoob at JavaScript, I patched this together from different sources on here and codingbeautydev...
$(window).bind("onpopstate", function (e) {
const previousPage = document.getElementById("previous-page");
previousPage.textContent = document.referrer;
window.history.pushState({ page: 1 }, "", "");
window.onpopstate = function (event) {
if (event) {
window.location.href = previousPage;
}
};
});
My first thought is there are surely some syntex errors in there at my doing and potentially much more that need be modified, but I'm hoping someone can easily touch up my rough sketch. Additionally, beyond making this work, I see the limits of this allowing only 1-page of history, and I'm curious if there's a way to nest it into a stack of a few pages to which could be visited in reverse order, all the while moving "forward". First things first though, then on to bigger and better.
Thanks guys! 😀
Mark
You cannot change the default behavior of the browsers's back or forward button unless your app uses URL hashes to navigate, but from my understanding of your question the user actually goes from say .com/paper to .com/boxes and not .com/paper#page1 to .com/paper#page2.
One possible option you could try is using the following (from here):
window.addEventListener('pageshow', function (event) {
if (event.persisted || performance.getEntriesByType("navigation")[0].type === 'back_forward') {
// User got here from using the Back or Forward button
}
});
This will trigger when the user got on the page this code runs on using the back or forward window button, also if the user goes from /boxes back to /paper.
You can try to save the current state of the music playing on the background (which song, timestamp, audio level, etc) in local storage (at max) every second or so, and get the stored values inside the function above to continue the music the user was last listening to when he left the previous page. Not the most elegant solution, but all I think of right now that might actually work.
Edit:
The code you requested. Chrome & Safari will block/ignore it due to history manipulation, except when an user interacts with the page first. It's not how history should be used. Don't use it in production, but play with it all you want. Also, here's an simple example how history can be used.
window.history.pushState({}, '', window.location.pathname);
let previousPage = document.referrer;
window.addEventListener('popstate', (e) => {
window.location.assign(previousPage)
});

How do I get audio to loop more smoothly in React?

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.

The audio did not play after function called with preload.js of createjs' series

I worked on a HTML5 game. I used createjs to preload images. When the images finish preloading, a BGM need to play automatically.
I known that chrome new audio policies. And I am very confused that I use function handleComplete to trigger the audio. Why does it still not work? How should I make it work?
Here is my code:
function Sound (id_str){ this.id = id_str; }
Sound.prototype.play = function () {
//there are other logics, but can simply like below
let audio = document.getElementById(this.id);
audio.play().catch(e =>console.log(e));
};
var audio_bg = new Sound('bgm_id');
windows.onload = function preload (handleFileProgress,handleComplete){
let manifest = [...];
loader = new createjs.LoadQueue(false);
loader.setMaxConnections(100);
loader.maintainScriptOrder=true;
loader.installPlugin(createjs.Sound);
loader.loadManifest(manifest);
loader.addEventListener('progress', handleFileProgress);
loader.addEventListener('complete', handleComplete);
};
function handleFileProgress(){...}
function handleComplete(){
//...
audio_bg.play();
}
The error I caught was:
NotAllowedError: play() failed because the user didn't interact with the document first.
The error you are seeing is due to changes in security in the browser, where you can't play audio without a user action to kick it off. SoundJS already listens for a browser input event (mouse/keyboard/touch) to unlock audio, but if you try to play audio before this happens, then it will fail.
To show this, quickly click in the browser before the file is loaded, and it will likely play.
The recommendation is to put audio behind a user event, like "click to get started" or something similar. It is an unfortunate side-effect of the security improvements in browsers :)

How to use RTCPeerConnection.removeTrack() to remove video or audio or both?

I'm studying WebRTC and try to figure how it works.
I modified this sample on WebRTC.github.io to make getUserMedia source of leftVideo and streaming to rightVideo.It works.
And I want to add some feature, like when I press pause on leftVideo(My browser is Chrome 69)
I change apart of Call()
...
stream.getTracks().forEach(track => {
pc1Senders.push(pc1.addTrack(track, stream));
});
...
And add function on leftVideo
leftVideo.onpause = () => {
pc1Senders.map(sender => pc1.removeTrack(sender));
}
I don't want to close the connection, I just want to turn off only video or audio.
But after I pause leftVideo, the rightVideo still gets track.
Am I doing wrong here, or maybe other place?
Thanks for your helping.
First, you need to get the stream of the peer. You can mute/hide the stream using the enabled attribute of the MediaStreamTrack. Use the below code snippet toggle media.
/* stream: MediaStream, type:trackType('audio'/'video') */
toggleTrack(stream,type) {
stream.getTracks().forEach((track) => {
if (track.kind === type) {
track.enabled = !track.enabled;
}
});
}
const senders = pc.getSenders();
senders.forEach((sender) => pc.removeTrack(sender));
newTracks.forEach((tr) => pc.addTrack(tr));
Get all the senders;
Loop Through and remove each sending track;
Add new tracks (if so desired);
Edit: or, if you won't need renegotiation (conditions listed below), use replaceTrack (https://developer.mozilla.org/en-US/docs/Web/API/RTCRtpSender/replaceTrack).
Not all track replacements require renegotiation. In fact, even
changes that seem huge can be done without requiring negotation. Here
are the changes that can trigger negotiaton:
The new track has a resolution which is outside the bounds of the
bounds of the current track; that is, the new track is either wider or
taller than the current one.
The new track's frame rate is high enough
to cause the codec's block rate to be exceeded. The new track is a
video track and its raw or pre-encoded state differs from that of the
original track.
The new track is an audio track with a different
number of channels from the original.
Media sources that have built-in
encoders — such as hardware encoders — may not be able to provide the
negotiated codec. Software sources may not implement the negotiated
codec.
async switchMicrophone(on) {
if (on) {
console.log("Turning on microphone");
const stream = await navigator.mediaDevices.getUserMedia({audio: true});
this.localAudioTrack = stream.getAudioTracks()[0];
const audioSender = this.peerConnection.getSenders().find(e => e.track?.kind === 'audio');
if (audioSender == null) {
console.log("Initiating audio sender");
this.peerConnection.addTrack(this.localAudioTrack); // will create sender, streamless track must be handled on another side here
} else {
console.log("Updating audio sender");
await audioSender.replaceTrack(this.localAudioTrack); // replaceTrack will do it gently, no new negotiation will be triggered
}
} else {
console.log("Turning off microphone");
this.localAudioTrack.stop(); // this will turn off mic and make sure you don't have active air-on indicator
}
}
This is simplified code. Solves most of the issues described in this topic.

Pause HTML5 audio whilst element still visible from AJAX return

I have a HTML page that polls a PHP script connected to a DB to see if any new items were received (new items are marked NEW) if there is, then an element #audio is sent to the browser so a sound can be played:
$(document).ajaxComplete(function() {
if ($('#audio').length) {
$('#sound').get(0).play();
}
});
The #sound is in the page's HTML so if #audio is returned with the items marked as NEW then the sound is played.
The polling returns all items within a certain time frame so OLD and NEW items can be returned which is why if there is one marked NEW I attach an #audio to make a sound.
Once the sound is played the user presses a button to update the NEW status to OLD in the DB using update.php and at the same time pause the sound.
$('body').on('click','.acc',function() {
$('#sound').get(0).pause();
var itemID = $(this).data("id");
{
$.post("update.php",
{
id: itemID
},);
}
});
Now the problem I am having is that although this works, sometimes the delay in updating in the server (from NEW to OLD) causes the sound to play because the #audio still exists.
I have tried to hide the element on click as well but that still has the same effect i.e. #audio is still returned whilst the updating is in process.
What do I need to do, can I use a different technique to stop the sound whilst the server is still processing?
Hope this makes sense if not let me know and I'll explain further and thanks in advance.

Categories