11521 sujets

JavaScript, DOM et API Web HTML5

Pages :
niuxe a écrit :
Sinon, la solution que je t'ai soumise sur le codepen fonctionne. As-tu bien vidé le cache ?

Je t'assure que, chez moi en tout cas, ça ne marche pas dans tous les cas de figure.

De toute façon il faut que je reprenne tout depuis le début. En effet, j'ai tendance à m'appuyer sur mon lecteur généré en javascript, par exemple je teste l'état play/pause de mon bouton addhoc pour savoir où en est le lecteur plutôt que l'état play/pause du media audio. Cette façon de procéder commence à me jouer des tours à mesure que j'avance dans le développement.

Par exemple, je ne peu pas ajouter telle quelle la solution de Bongota car l'état de mes boutons ne suit pas :
document.addEventListener('play', e => { // Un seul lecteur actif sur la page
  [...document.querySelectorAll('audio, video')].forEach((media) => media !== e.target && media.pause())
}, true)

Cela ne devrait pas être, la solution devrait fonctionner telle quelle, il faut que je revoie tout donc.
Modifié par Olivier C (17 Apr 2023 - 13:28)
Modérateur
Bonjour,

Utiliser un setTimeout() n'est pas une option ! Smiley cligne

Il me semble que :

1) Il faut utiliser impérativement un audio.addEventListener('loadedmetadata',function(){audioDuration(audio, output);}), sinon on aura de temps en temps des NaN.

2) Dans les codes de test fournis par Olivier, cette ligne semble faite à un moment où l'évènement 'loadedmetadata' peut avoir déjà été émis. Ceci peut expliquer pourquoi Olivier observe qu'il ne marche pas de temps en temps.

3) La ligne audio.addEventListener('loadedmetadata', audioDuration(audio, output)) que j'ai aperçu dans l'un des codes d'Olivier n'est pas bonne. Car ainsi écrite, audioDuration(audio, output) s'exécute au moment où on fait le audio.addEventListener() et non au moment où l'évènement 'loadedmetadata' est émis.

4) Il faudrait faire un audio.addEventListener('loadedmetadata',function(){audioDuration(audio, output);}) suivi d'un output.value = secondsToTime(audio.duration);

En faisant ça, soit le 'loadedmetadata' est émis après l'exécution de la ligne audio.addEventListener('loadedmetadata',function(){audioDuration(audio, output);}) et dans ce cas la valeur de output.value sera calculée par la fonction audioDuration(audio, output) lorsque l'évènement sera émis, soit il est émis avant, et dans ce cas la valeur de output.value sera donnée par la ligne output.value = secondsToTime(audio.duration); c'est à dire celle qui se trouve après le audio.addEventListener('loadedmetadata',function(){audioDuration(audio, output);}) et non celle qui se trouve dans la fonction audioDuration().

À vérifier !

EDIT: une alternative est de n'affecter la valeur de audio.src qu'après avoir exécuté le audio.addEventListener('loadedmetadata',function(){audioDuration(audio, output);}), auquel cas on pourra se passer de la ligne output.value = secondsToTime(audio.duration);

Amicalement,
Modifié par parsimonhi (17 Apr 2023 - 13:46)
Meilleure solution
parsimonhi a écrit :
4) Il faudrait faire un audio.addEventListener('loadedmetadata',function(){audioDuration(audio, output);}) suivi d'un output.value = secondsToTime(audio.duration);

En faisant ça, soit le 'loadedmetadata' est émis après l'exécution de la ligne audio.addEventListener('loadedmetadata',function(){audioDuration(audio, output);}) et dans ce cas la valeur de output.value sera calculée par la fonction audioDuration(audio, output) lorsque l’événement sera émis, soit il est émis avant, et dans ce cas la valeur de output.value sera donnée par la ligne output.value = secondsToTime(audio.duration); c'est à dire celle qui se trouve après le audio.addEventListener('loadedmetadata',function(){audioDuration(audio, output);}) et non celle qui se trouve dans la fonction audioDuration().

Bah mince alors, ça marche ! Entre temps j'ai un peu factorisé le code et renommé des variables :
const mediaDuration = (media) => {
  const output = media.nextElementSibling.querySelector('.audio-player-duration')
  media.addEventListener('loadedmetadata',() => output.value = secondsToTime(media.duration))
  output.value = secondsToTime(media.duration)
}

Mais j'ai du mal à comprendre pourquoi il faut répéter l'instruction : il faut que je médite un peu sur tes explications. La version à jour, incluant une suggestion de Bongota fonctionnelle avec mon code (pensez à vider votre cache) : Media players.

J'attends un peu avant de mettre (à nouveau) "meilleure solution" et "résolu", mais a priori c'est bien cela.
Modifié par Olivier C (17 Apr 2023 - 14:15)
Modérateur
parsimonhi a écrit :

Utiliser un setTimeout() n'est pas une option ! Smiley cligne

+1


J'ai repris le code en local :
le html

<?php 
    $sources = [
        'https://scriptura.github.io/medias/audios/BagdadCafe.mp3',
        'https://scriptura.github.io/medias/audios/abeilles.mp3',
        'https://scriptura.github.io/medias/audios/merle.mp3',
        'https://scriptura.github.io/medias/audios/lion.mp3',
        'https://scriptura.github.io/medias/audios/grillons.mp3',
    ];
?>
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <link rel="stylesheet" href="https://scriptura.github.io/styles/main.css">
    <style>
        main .grid2 > div{
            display:grid;
            gap:1em;
        }
        .audio{
            visibility: hidden;
        }
        .message-highlight{
            background: #6fa939;
        }
    </style>
</head>
<body>
    <main>
        <div class="grid2 s-grid1 gap-block gap">
            <div>
                <?php foreach($sources as $source): ?>
                <audio class="audio" controls preload="metadata" style="display:block !important">
                    <source src="<?= $source ?>" />
                </audio>
                <?php endforeach ?>
            </div>
        </div>
    </main>
    <template id="tpl-audio-player">
        <div class="audio-player">
            <button class="audio-play-pause">
                <svg focusable="false">
                    <use href="/svgs/compiled/svgs.20ea15c6.svg#arrow-right-mini"></use>
                </svg>
                <svg focusable="false">
                    <use href="/svgs/compiled/svgs.20ea15c6.svg#x"></use>
                </svg>
            </button>
            <div>
                <output class="audio-player-current-time">0:00</output>&nbsp;/&nbsp; <output class="audio-player-duration">0:00</output>
            </div>
            <div class="progress"></div>
            <button class="audio-volume">
                <svg focusable="false">
                    <use href="/svgs/compiled/svgs.20ea15c6.svg#ellipse"></use>
                </svg>
                <svg focusable="false">
                    <use href="/svgs/compiled/svgs.20ea15c6.svg#x"></use>
                </svg>
            </button>
            <button class="audio-menu">
                <svg focusable="false">
                    <use href="/svgs/compiled/svgs.20ea15c6.svg#ellipse"></use>
                </svg>
            </button>
        </div>
    </template>
    <script src="app.js"></script>
</body>
</html>




le js

(()=>{
    let secondsToTime = e => { 
        let hh = Math.floor(e / 3600).toString().padStart(2, '0'),
            mm = Math.floor(e % 3600 / 60).toString().padStart(2, '0'),
            ss = Math.floor(e % 60).toString().padStart(2, '0')
        
        if (hh == '00'){
            hh = null
        } 
        
        return [hh, mm, ss].filter(Boolean).join(':')
    }

    function play(player) {
        const audio = player.previousElementSibling
        audio.paused ? audio.play() : audio.pause()
    }

    function mute(player) {
        const audio = player.previousElementSibling
        audio.volume === 0 ? audio.volume = 1 : audio.volume = 0
        //audio.loop = true
    }

    function buttonState(button) {
        if (button.classList.contains('active')) button.classList.remove('active')
        else button.classList.add('active')
    }

    function init(player) {
        const buttonPlay = player.querySelector('.audio-play-pause')
        buttonPlay.addEventListener('click', () => {
            play(player)
            buttonState(buttonPlay)
        })
        const buttonVolume = player.querySelector('.audio-volume')
        buttonVolume.addEventListener('click', () => {
            mute(player)
            buttonState(buttonVolume)
        })
        player.previousElementSibling.addEventListener('ended', () => buttonState(buttonPlay)) // Si fin de la lecture.
    }

    window.addEventListener('DOMContentLoaded', ()=>{
        document.querySelectorAll('.audio').forEach(($audio, i) =>{
            $audio.addEventListener('loadedmetadata', e =>{
                if(e.target.readyState > 2){
                    $audio.id = `audio-player-${i}`
                    $audio.insertAdjacentHTML('afterend', document.getElementById('tpl-audio-player').innerHTML)
                    let output = $audio.nextElementSibling.querySelector('.audio-player-duration')
                    output.value = secondsToTime($audio.duration)
                    init($audio.nextElementSibling)
                }
            })
        })
    })

})()



Ça fonctionne très bien sur Chrome. Par contre, il ne comprend pas l'event 'loadedmetadata' sur Firefox. Bizarre Smiley hum . Au passage, j'ai mis audio en display block (c'est source d'erreur js le display none ==> undefined/null). Ce genre de truc peut être fait à l'objet très facilement voir même en web component.
Modifié par niuxe (17 Apr 2023 - 14:32)
Pour que vous ayez quelque chose d'exploitable sous les yeux j'ai mis à jour le pen : CodePen updated.

Tu verras que je n'ai plus de problème lié au chargement.

Maintenant il faut que je continue le développement. Mais pas avant ce soir (normalement je dois peindre la maison là !).
Modérateur
Bonjour,

Olivier C a écrit :
Mais j'ai du mal à comprendre pourquoi il faut répéter l'instruction


Il faut répéter l'instruction parce qu'il y a 2 cas :

1) soit l'évènement 'loadedmetadata' est émis (par le navigateur) avant que ton code javascript soit exécuté, auquel cas l'instruction dans le handler de 'loadedmetadata' ne sera jamais exécutée, puisqu'on ne verra jamais passer l'évènement. Mais dans ce cas, ça veut dire que l'audio est déjà chargée et qu'on peut directement récupérer la durée et la mettre dans output.value,

2) soit l'évènement 'loadedmetadata' est émis (par le navigateur) après que ton code javascript soit exécuté, auquel cas au moment où ton code est exécuté, la vraie durée n'est pas encore connue, le output.value va recevoir provisoirement un NaN comme valeur, et ce n'est que plus tard lorsque le handler de 'loadedmetadata' sera exécuté (car dans ce cas il sera effectivement exécuté contrairement au cas 1), que output.value va recevoir la bonne durée. Eventuellement, on peut tester si audio.duration vaut NaN, et si c'est le cas, on n'exécute que le audio.addEventListener('loadedmetadata', ...), et si audio.duration n'est pas NaN, on n'exécute que output.value = secondsToTime($audio.duration).

Amicalement,
Modifié par parsimonhi (17 Apr 2023 - 15:37)
Modérateur
Bonjour,

niuxe a écrit :
Ça fonctionne très bien sur Chrome. Par contre, il ne comprend pas l'event 'loadedmetadata' sur Firefox. Bizarre Smiley hum .


Vérifie si l'évènement 'loadedmetadata' n'aurait pas été émis avant que ton code ne s'exécute !

Amicalement,
Après quelques tests je confirme que je n'ai plus de bugs. Sujet résolu.

Et aussi j'ai réussi à faire la fonction currentTime ! (j'avais pas de la peinture à faire ? je suis un vrai gosse...) :
const currentTime = () => {
  for (const media of medias) {
    const player = media.nextElementSibling
    const output = player.querySelector('.audio-player-current-time')
    setInterval(frame, 200)
    function frame() {
      output.value = secondsToTime(media.currentTime)
    }
  }
}

Magique, on voit enfin une valeur de retour à l'écran. Prochaine étape : animer la barre de progression.

Edit : c'est fait :
let width = media.currentTime / media.duration * 100
progress.style.width = width + '%'

Ce qui donne dans le contexte :
const currentTime = () => {
  for (const media of medias) {
    const player = media.nextElementSibling
    const output = player.querySelector('.audio-player-current-time')
    const progress = player.querySelector('.audio-progress > div')
    setInterval(frame, 200)
    function frame() {
      output.value = secondsToTime(media.currentTime)
      let width = media.currentTime / media.duration * 100
      progress.style.width = width + '%'
    }
  }
}

Et au passage, la solution proposée sur MDN n'était pas top (avec calcul du parent via .clientWidth, donc pas responsive). C'est pour ça qu'une solution perso c'est top.
Modifié par Olivier C (17 Apr 2023 - 16:56)
Bongota a écrit :
Oui, que ce soit de la vidéo ou de l'audio, j'ai aussi remarqué que le lecteur audio html 5 est quasiment similaire. Simplement pour l'image, il faut donner les dimensions de la vidéo, éventuellement utiliser "poster" afin de choisir l'image d'accueil qui sera affichée, sinon ce sera la première de la vidéo.

Et ! Mais ça marche du tonnère ! J'ai testé sur une vidéo, il n'y aura pratiquement rien à retoucher :

Page de dev' pour les lecteurs audios et vidéos

Du coup je renomme toutes les classes ".audio-*" en ".media-*" (je l'avais déjà fait pour les variables). Un même code pour tous les médias HTML5... ils ont pensé à tout. Génial.

Edit : allez hop, dixit les classes .audio et .video sur les players ; je mets désormais une même classe d'appel pour tout le monde, quelque soit la nature du média : ".media".
Modifié par Olivier C (18 Apr 2023 - 09:42)
Modérateur
Dans mon script, j'avais implémenté (ci-dessous) et je faisais référence à HTMLMediaElement: readyState property


if(e.target.readyState > 2)


Je pense que je l'avais mal implémenté. Depuis, je me suis amusé à le faire en composant web et à le rendre plus concis. Pour les curieux ?
Modifié par niuxe (18 Apr 2023 - 15:35)
Merci pour le partage.

J'ai fait un petit test par curiosité mais je ne suis pas doué, je ne suis pas arrivé à le mettre en oeuvre correctement :
media.addEventListener('loadeddata', () => {
  if (media.readyState >= 2) output.value = secondsToTime(media.duration); // Nop (et le point-virgule c'est pour Parsimonhi)
})

Modifié par Olivier C (18 Apr 2023 - 16:05)
Modérateur
Bonjour,

Ça parait pas mal en effet d'utiliser media.readyState. Mais j'aurais plutôt codé cette affaire comme ci-dessous :

EDIT: mea culpa, j'avais dans un premier temps utiliser l'évènement 'loadeddata'. Il fallait utiliser 'loadedmetadata'. Le code modifié est donc :
        if (media.readyState >= 1)
        	output.value = secondsToTime(media.duration);
        else
        	media.addEventListener('loadedmetadata', () => {
					output.value = secondsToTime(media.duration);
				});
Explications :
- il suffit que media.readyState soit supérieur ou égal à 1 (media.duration étant me semble-t-il une metadata, tester par rapport à 1 suffit),
- et encore et toujours, le problème est le moment de la survenue de l'évènement 'loadedmetadata'. Si media.duration est supérieur ou égal à 1, ça ne sert à rien de faire un media.addEventListener('loadedmetadata',...) car l'évènement 'loadedmetadata' a déjà été émis par le navigateur. Et inversement, au moment où l'évènement 'loadedmetadata' est émis, media.readyState devrait être supérieur ou égal à 1, ce qui rend inutile son test dans le handler de l'évènement.


Amicalement,
Modifié par parsimonhi (18 Apr 2023 - 20:06)
Alors effectivement ça fonctionne, mais pourquoi ne pas utiliser "loadedmetadata" plutôt que "loadeddata" ?
MDN a écrit :
loadeddata est déclenché lorsque l'image à la position de lecture actuelle du média a fini de se charger ; souvent la première image.
loadedmetadata est déclenché lorsque les métadonnées ont été chargées.

On est bien d'accord que ce n'est pas une critique, c'est juste que je navigue à vue et que je cherche à comprendre.

En théorie "loadedmetadata" ce devrait être plus rapide non ?
media.readyState >= 1 ? output.value = secondsToTime(media.duration) : media.addEventListener('loadedmetadata', () => output.value = secondsToTime(media.duration)); // oui oui, le point virgule [cligne]

Modifié par Olivier C (18 Apr 2023 - 20:03)
Modérateur
Bonjour,

On utilise loadedmetadata parce que ce qu'on veut, c'est utiliser media.duration qui est une metadata. On n'a pas besoin d'attendre que toutes les datas soient chargées.

EDIT: je viens de voir que j'avais utilisé 'loadeddata' dans mon post. C'est une erreur.

Amicalement,
Modifié par parsimonhi (18 Apr 2023 - 20:02)
Super merci, au moins je suis arrivé à suivre.
Modifié par Olivier C (18 Apr 2023 - 21:11)
Modérateur
Bonjour,

Olivier C a écrit :
Super merci, on moins je suis arrivé à suivre.
Oui, bien vu ! Smiley smile

Amicalement,
Alors juste une réflexion au passage me concernant, pour me donner le bâton pour me faire battre...
Smiley smash

Depuis deux jours je me dis : "HTMLMediaElement doit avoir tous ses états accessibles et moi je passe par le html de mon player (l'état actif ou non de mes boutons) pour les récupérer, c'est c...". Je comprenais bien que ma solution serait plus robuste en tapant directement à la source, mais j'étais incapable de trouver une solution. Ça fonctionnait, mais ce n'était pas un truc de fifou :
const test = player.querySelector('.media-replay').classList.contains('active')
test ? media.loop = true : media.loop = false

Et là, tout d'un coup, bingo :
media.loop = !media.loop

Ah là là ! Un jour il faudra vraiment que j'accepte de perdre du temps pour apprendre correctement les bases du JavaScript...
Modifié par Olivier C (19 Apr 2023 - 20:43)
Tiens : ! voilà l'exemple parfait du truc bancal que je voulais souligner :
buttonPlayPause.addEventListener('click', function() {
  togglePlayPause(media)
  toggleActiveClass(this) // <- le blem'
  currentTime()
})

Ce code fera parfaitement son office au clique du bouton. Mais que se passera-t-il si l'action est produite par ailleurs ? Via un gestionnaire d'événement tel que l'avait suggéré Bongota par exemple ? :
document.addEventListener('play', e => { // Si un lecteur actif sur la page, alors les autres se mettent en pause.
  [...document.querySelectorAll('.media')].forEach((media) => {
    if (media !== e.target) media.pause()
  })
}, true)

Dans ce cas il faudra compenser, tout le temps, partout. Penser à tous les cas de figure. Bonjour la maintenabilité du code :
document.addEventListener('play', e => { // Si un lecteur actif sur la page, alors les autres se mettent en pause.
  [...document.querySelectorAll('.media')].forEach((media) => {
    if (media !== e.target) {
      media.pause()
      media.nextElementSibling.querySelector('.media-play-pause').classList.remove('active') // <- ici
    }
  })
}, true)

J'ai donc réussi à faire mon renversement copernicien : ce n'est plus le player html généré qui cherche à comprendre où en est le média élément, c'est le média élément qui dicte directement au player quel doit être l'état de ses boutons :
media.paused ? button.classList.remove('active') : button.classList.add('active')

On en apprend tous les jours...
Modifié par Olivier C (19 Apr 2023 - 22:50)
Bonsoir,

Je continue, toujours en dilettante, le développement de mon player HTML5. Si je versionnais mon code on pourrait dire que les objectifs de départ sont atteints, à savoir : coder un player dont les fonctionnalités de base sont au moins équivalentes aux players HTML5 par défaut des navigateurs.

Au cours du dev' j'ai de toute façon rapidement dépassé cet objectif, notamment pour ce qui est de sa gestion des erreurs (inexistante sur les players par défaut). J'ai ajouté d'autres fonctionnalités basiques, comme des boutons replay, stop, saut en avant et en arrière... Rien de bien compliqué.

Et puis il arrive que - tout autiste que je suis - j'arrive à entendre une suggestion posée en commentaire d'un forum tel que celui-ci : au final j'ai trouvé la piste de Bongota intéressante, pas au niveau d'une liste préétablie, mais par rapport à cette idée de passer d'un player à un autre sur une page, je l'ai implémenté et ça fonctionne :

Media Player HTML5, page de démonstration

Mais je l'ai implémenté à ma manière afin de rester modulable : tous les players de la page n'ont pas cette option, il faut que le player soit intégré au sein d'un élément parent comportant la classe ".media-relationship", ce qui permet de contrôler les players que l'on souhaite ou non passer en liste de lecture ; précisons aussi que les players ne sont pas obligés d'être des éléments frères. Du coup on peut avoir plusieurs listes de lecture indépendantes sur une même page. Là encore, les fichiers corrompus sont filtrés pour ne pas faire échouer la lecture de la playlist. Pour activer la lecture entre players faut activer l'option "next reading mode", le bouton est disponible à partir du menu de chaque player.

Je ferais peut-être critiquer ce player dans la section dédiée du forum, mais dore et déjà je suis preneur pour des retours...