html5-audioweb-audio-api

Problems with Javascript audio controls for multiple audio files on one page


Web dev turned voice actor here! I'm working on my personal site for the latter, and have been running into a lot of issues around setting up custom controls for audio files. I'm loading my audio files using the following code…

<section id="demos widthWrap">
        <?php
            $aniURL = get_field('animation');
            $comURL = get_field('commercial');

            $demos = array(
                "aniDemo" => array(
                    "Kind" => "Animation",
                    "URL" => $aniURL
                ),
                "comDemo" => array(
                    "Kind" => "Commercial",
                    "URL" => $comURL
                )
            );

            foreach ($demos as $demo){
                ?>
                <figure class="demoBox">
                    <audio class="demo" controls preload="metadata">
                        <source src="<?php echo $demo["URL"];?>" type="audio/mp3">
            <!-- Put a small fallback message for old browsers -->
                        <p>ERROR! Your browser does not support audio playback!</p>
                    </audio>
                    <ul class="demoControls">
                        <li class="playButton"><button class="playPause"><i class="fa-solid fa-play"></i></button></li>
                        <li class="progress">
                            <progress class="progressBar" value="0" min="0"></progress>
                        </li>
                    </ul>
                </figure>
                <?php
            }
        ?>
</section>

… and those are loading properly. The problems arise with the Javascript intended for the custom controls. Here it is.

const supportsAudio = !!document.createElement("audio").canPlayType;
if (supportsAudio) {
    const audioContaner = document.getElementsByClassName("demoBox");
    const audio = document.getElementsByClassName("demo");
    const audioControls = document.getElementsByClassName("demoControls");

    // Hide the default controls
    audio.controls = false;

    // Display the user defined audio controls
    audioControls.style.display = "block";

    const playPause = document.getElementsByClassName("playPause");
    const progress = document.getElementsByClassName("progressBar");

    playPause.addEventListener("click", (e) => {
        if (audio.paused || audio.ended){
            audio.play();
            //switch to pause icon
        } else {
            audio.pause();
            //switch to play icon
        }
    });
    playPause.addEventListener(ended, (e)=> {
        console.log("Audio has ended");
        //switch to repeat icon
    });

    audio.addEventListener("loadedmetadata", () => {
        progress.setAttribute("max", audio.duration)
    });
    audio.addEventListener("timeupdate", () => {
        if (!progress.getAttribute("max"))
            progress.setAttribute("max", audio.duration);
        progress.value = audio.currentTime;
    });
}

I based most of it on this MDN article, cherry picking just the parts I need and adjusting things for audio. I also tried altering things to work for multiple instances of audio files, and I'm pretty sure that's where things have gone sideways.

I get an "Uncaught TypeError: Cannot set properties of undefined (setting 'display')" when setting the audioControls style to display:block, but the issues start well before that. audio.controls = false; doesn't get rid of the default playback controls, indicating that the issues are probably with my declarations. Everything I've declared using getElementsByClassName comes out as an HTMLCollection (which I only just learned is a thing), and attempts to iterate through any of them gives nothing. I tried a few different for…of loops, and tried converting the declaration for the highest relevant DOM element to an array and tried a forEach on that, and got noting back from my console.log attempts.

I need to have all my demos on one page as a professional requirement (agents don't want to go to multiple pages to listen to demos), and really don't want to write all the Javascript for every instance of an audio file. What should I do?


Solution

  • ...and attempts to iterate through any of them gives nothing.

    It is where the problem lies. You're trying set the properties of a HTMLCollection, which is basically a list of elements found by getElementsByClassName.

    The solution here is to select all the demoBox elements and loop through them. Then with each demoBox, select all the elements inside and set your event listeners and settings.

    Personally I prefer using querySelector() and querySelectorAll() over getElementsByClassName() as they give more control over what you're looking for and give predictable results. And they can be used on element level, which is what we need here.

    const audioContainers = document.querySelectorAll(".demoBox");
    
    // Loop over each .demoBox element.
    for (const audioContainer of audioContainers) {
      const audio = audioContainer.querySelector(".demo");
      const audioControls = audioContainer.querySelector(".demoControls");
    
      // Hide the default controls
      audio.controls = false;
    
      // Display the user defined audio controls
      audioControls.style.display = "block";
    
      const playPause = audioControls.querySelector(".playPause");
      const progress = audioControls.querySelector(".progressBar");
    
      playPause.addEventListener("click", (e) => {
        if (audio.paused || audio.ended) {
          audio.play();
          //switch to pause icon
        } else {
          audio.pause();
          //switch to play icon
        }
      });
    
      playPause.addEventListener(ended, (e) => {
        console.log("Audio has ended");
        //switch to repeat icon
      });
    
      audio.addEventListener("loadedmetadata", () => {
        progress.setAttribute("max", audio.duration);
      });
    
      audio.addEventListener("timeupdate", () => {
        if (!progress.getAttribute("max")) {
          progress.setAttribute("max", audio.duration);
        }
    
        progress.value = audio.currentTime;
      });
    }
    

    I don't think you need to check if a device supports audio, since it's part of the HTML5 spec, which should be widely supported. But if you're expecting to run in obscure or outdated browsers, then it couldn't hurt to check.