Background
I have a button that plays the audio connected to a nearby audio
element when a click
event is detected. The button
element has a nested img
element whose src
I change to point to various .svg files depending on on whether audio is
- playing (pause icon)
- paused (play icon)
- about to play but hasn’t started playing yet (rotating buffering icon)
Problem
The switch to the pause or play icon happens with no problems at all, but the switch to the buffering icon happens so late it isn’t really useful (i.e., user clicks the play button, user waits for however long, and the buffering icon flashes for a few ms right before the audio starts playing).
On desktop, this is all very snappy and the buffering icon wouldn’t even be needed, but on mobile (iOS) there can be a one to four second delay from when the user clicks the play button and the audio actually starts playing (which is why I want a buffering icon for that wait time).
Code
HTML structure
There are a whole slew of these li
elements. The real focus here is on the button.playOrPauseBtn
element:
<li>
<audio
id="TR-21"
class="dialogueAudio"
src="/static/audio/en/es/end/tr-21.mp3"
preload="auto"
>
</audio>
<div>
<button type="button" class="svgBtn playOrPauseBtn" >
<img class="playIcon" src="/static/svg/play.svg" height="22px" width="22px" />
</button>
</div>
</li>
javascript
The focus here would be the event listener at the bottom, which I would expect to immediately change the UI of the play/pause button to my buffer icon right when the button is clicked, but on mobile (iOS; haven’t tested android), it actually waits and then only flashes the buffer icon right before the audio starts playing (1 to 4 second wait, depending on audio).
If it seems weird that everything is separated into lots of mini functions, it’s because these are extremely simplified versions of the actual functions (I’m trying to only focus on the problem at hand). They do a lot more with the UI, such as changing title and alt text for various elements. Let me know if you think it would be helpful to see the JS code and HTML markup in their original, unsimplified form.
/* ----------------------------------------------
------------------- FUNCTIONS -------------------
---------------------------------------------- */
const changeAudioControlBtn = function(btn, svg, disabled=false, spinny=false) {
// get other elements
let btnImg = btn.querySelector("img");
// change UI of btn
btn.disabled = disabled;
// change UI of img
btnImg.src = svg;
if (spinny === false) {
btnImg.classList.remove("spinny");
} else {
btnImg.classList.add("spinny");
}
}
/* Function to pause all audios */
const pauseAll = async function() {
let allYeAudios = document.querySelectorAll("audio");
allYeAudios.forEach((item, i) => {
item.pause();
});
};
/* Function for playing or pausing audio associated with play/pause button */
const playOrPause = async function(soundByte) {
if (soundByte.paused == true) {
// pause other audios before playing
await pauseAll();
soundByte.play();
} else {
soundByte.pause();
}
};
/* Function to change play/pause button icon to play or pause symbol */
const changePlayOrPauseGraphics = function() {
let playPauseBtn = this.parentElement.querySelector(".playOrPauseBtn");
if (this.paused === true) {
// set properties for function to change UI of play/pause btn
let btn = playPauseBtn;
let svg = "/static/svg/play.svg";
changeAudioControlBtn(btn, svg);
} else {
// set properties for function to change UI of play/pause btn
let btn = playPauseBtn;
let svg = "/static/svg/pause.svg";
changeAudioControlBtn(btn, svg);
}
};
/* ----------------------------------------------
---------------- EVENT LISTENERS ----------------
---------------------------------------------- */
const allDialogueAudios = document.querySelectorAll(".dialogueAudio");
// Change visuals if playing
allDialogueAudios.forEach((item, i) => {
item.addEventListener("playing", changePlayOrPauseGraphics);
});
// change visuals when audio pauses too
allDialogueAudios.forEach((item, i) => {
item.addEventListener("pause", changePlayOrPauseGraphics);
});
const allPlayButtons = document.querySelectorAll(".playOrPauseBtn");
// Play buttons
allPlayButtons.forEach((item, i) => {
item.addEventListener("click", () => {
// set properties for UI function (to change btn to spinny buffer icon)
let btn = item;
let svg = "/static/svg/loady_spinner.svg";
let disabled = true;
let spinny = true;
changeAudioControlBtn(btn, title, svg, alt, disabled, spinny);
let soundByte = item.parentElement.parentElement.querySelector(".dialogueAudio");
playOrPause(soundByte);
});
});
What I’ve tried
I used to have the code that calls the changeAudioControlBtn()
function in order to change the UI of the playOrPauseBtn
to a buffer icon inside the playOrPause()
function, and the event listener handler for the play/pause buttons was much simpler (it just called the playOrPause()
function, but I thought moving the code for the buffer icon change to the event listener handler would be more immediate. But that didn’t have any effect.
Other details
The issue does not seem to be Safari related because it does not happen on Safari desktop, and the same issue happens on my iOS device on Chrome, Safari, and Firefox browsers.
2
Answers
Try using the
canplaythrough
event, which is fired when the user agent can play the media, and estimates that if playback were to be started now, the media could be rendered at the current playback rate all the way to its end without having to stop for further buffering.https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/canplaythrough_event
Try this approach:
In this code, the
playOrPause
function is called only when thecanplaythrough
event is fired, ensuring that the audio is ready to play. This should help to display the buffering icon immediately after the button is clicked and keep it visible until the audio starts playing.Also, it is better to test your code on multiple platforms to ensure it wirks as expected.
I thing Your are adding the spinning on a wrong moment. change your function logic may help.