Reputation: 313
I am creating an audio player from an audio collection on an HTML page:
...
<div class="tr track" id="01">
<div class="td">
<button class="play" onclick="Play(this)">▶</button>
<button class="play" onclick="Pause(this)">❚❚</button>
<span class="title">A Corda da Liberdade</span>
</div>
<div class="td">
<audio preload="metadata" src="discografia/le-gauche-gnosis/01-a-corda-da-liberdade.ogg"></audio>
</div>
<div class="td">
<span class="duracao"></span>
</div>
</div>
...
I want the <span class="duracao"></span>
element to show the duration of the audio it is related to:
// This function format the audio duration number in the way I want to present
function secToStr(sec_num) {
sec_num = Math.floor( sec_num );
var horas = Math.floor(sec_num / 3600);
var minutos = Math.floor((sec_num - (horas * 3600)) / 60);
var segundos = sec_num - (horas * 3600) - (minutos * 60);
if (horas < 10) {horas = "0"+horas;}
if (minutos < 10) {minutos = "0"+minutos;}
if (segundos < 10) {segundos = "0"+segundos;}
var tempo = minutos+':'+segundos;
return tempo;
}
var i;
var audios = document.getElementsByTagName('audio'); // get all audios elements of the player
for (i = 0; i < audios.length; i++) { // looping through audios
var audio = audios[i]; // get actual audio
var duracao = audio.parentNode.nextElementSibling.getElementsByClassName('duracao')[0] // get actual audio 'duration <span>'
audio.onloadedmetadata = function() {
duracao.innerHTML = secToStr(audio.duration);
}
}
The for
loop is supposed to do the job but is just adding the duration of the last audio element to the last <span class="duracao"></span>
element:
Any help?
Upvotes: 2
Views: 587
Reputation: 53538
This is an excellent place to learn about, and then use Array.reduce()
instead of a for-loop.
The concept of reduce
is that you start with some starting value (which can be anything, not just a number), and you then walk through the array in a way that, at ever step, lets you run some code to update that value. So:
const total = [1,2,3,4,5].reduce( (sofar, value) => sofar + value, 0)
will run through the array, with start value 0, and at every step it runs (sofar, value) => sofar + value
, where the first argument is always "whatever the original start value is at this point". This function assumes that value
is a number (or a string) and adds (or concatenates) it to the start value. So at each step we get:
start = 0
first element: add 1 to this value: 0 + 1 = 1
second element: add 2 to this value: 1 + 2 = 3
third element: add 3 to this value: 3 + 3 = 6
fourth element: add 4 to this value: 6 + 4 = 10
fifth element: add 5 to this value: 10 + 5 = 15
We can apply the same to your audio elements: once they're all done loading in, you can tally their total duration with a single reduce
call:
const total = audios.reduce((sofar, audioElement) => {
sofar += audioElement.duration;
}, 0); // this "0" is the starting value for the reduce-function's first argument
console.log(`Total number of seconds of play: ${total}`);
And then you can convert total
into whatever format you need.
Alternatively, you can keep a global tally, but making each audo element update the total length themselves, simply by finishing loading:
let total = 0;
function updateTotal(increment) {
total += increment;
// and then update whatever HTML element shows that value on the page,
// in whatever format you need.
}
document.querySelectorAll('audio').forEach(element => {
element.onload = (evt) => {
updateTotal(element.duration);
})
});
Upvotes: 1
Reputation: 136598
The general approach with asynchronous loops would be to promisify the async action and then wait for a Promise.all(all_promises)
.
However, in this particular case, it might not be that easy:
Some browsers (Chrome to not tell their name) have a limit on the maximum number of parallel network requests a page can make for Media.
From there, you won't be able to get the duration of more than six different media at the same time...
So we need to load them one after the other.
The async/await syntax introduced in ES6 can help here:
const urls = [
'1cdwpm3gca9mlo0/kick.mp3',
'h8pvqqol3ovyle8/tom.mp3',
'agepbh2agnduknz/camera.mp3',
'should-break.mp3',
'/hjx4xlxyx39uzv7/18660_1464810669.mp3',
'kbgd2jm7ezk3u3x/hihat.mp3',
'h2j6vm17r07jf03/snare.mp3'
]
.map(url => 'https://dl.dropboxusercontent.com/s/' + url);
getAllDurations(urls)
.then(console.log)
.catch(console.error);
async function getAllDurations(urls) {
const loader = generateMediaLoader();
let total = 0;
for(let i = 0; i < urls.length; i++) {
total += await loader.getDuration(urls[i]);
}
return total;
}
// use a single MediaElement to load our media
// this is a bit verbose but can be reused for other purposes where you need a preloaded MediaElement
function generateMediaLoader() {
const elem = new Audio();
let active = false; // so we wait for previous requests
return {
getDuration,
load
};
// returns the duration of the media at url or 0
function getDuration(url) {
return load(url)
.then((res) => res && res.duration || 0)
.catch((_) => 0);
}
// return the MediaElement when the metadata has loaded
function load(url) {
if(active) {
return active.then((_) => load(url));
}
return (active = new Promise((res, rej) => {
elem.onloadedmetadata = e => {
active = false;
res(elem);
};
elem.onerror = e => {
active = false;
rej();
};
elem.src = url;
}));
}
}
But it's also very possible to make it ES5 style.
var urls = [
'1cdwpm3gca9mlo0/kick.mp3',
'h8pvqqol3ovyle8/tom.mp3',
'agepbh2agnduknz/camera.mp3',
'should-break.mp3',
'/hjx4xlxyx39uzv7/18660_1464810669.mp3',
'kbgd2jm7ezk3u3x/hihat.mp3',
'h2j6vm17r07jf03/snare.mp3'
]
.map(function(url) {
return 'https://dl.dropboxusercontent.com/s/' + url;
});
getAllDurations(urls, console.log);
function getAllDurations(urls, callback) {
var loader = new Audio();
var loaded = 0;
var total = 0;
loader.onloadedmetadata = function(e) {
total += loader.duration;
loadNext();
};
loader.onerror = loadNext;
loadNext();
function loadNext() {
if(loaded >= urls.length) {
return callback(total);
}
loader.src = urls[loaded++];
}
}
Upvotes: 1