javascripttailwind-css

Issues playing audio in website on iOS devices


I've build a basic webpage with a countdown timer that plays 3 short sounds at different points through the countdown, once the countdown is complete, a stopwatch timer appears and waits for the user to press stop.

The .play() function doesn't seem to work on my iPhone for both Chrome and Safari. None of the three sounds are audible at all.

It plays fine for desktops and there are no issues at all.

Is there a fix for this? The whole idea of the training tool is that it is supposed to work on mobiles so competitors can train their reaction times at home.

<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
<style>
    #processed-content {
        background-image: url('https://www.slslnc.org.au/wp-content/uploads/timerbackground.jpg');
        background-position: center;
        background-size: cover;
        min-height: 96vh;
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
        padding: 16px;
        text-align: center;
    }

    #processed-content h1,
    #processed-content p {
        color: black;
        text-shadow: 0 0 15px white;
        font-size: 2rem;
        margin:50px 0;
    }
    
    p#timerDisplay.instructions {
    font-size: 18px !important;
}

    #processed-content button {
        color: black;
        font-size: 2rem;
        margin:20px 0;
    }
    .start-button {
        background-color:#ffd520;
        border:2px solid black;
    }
    .stop-button {
        background-color:#ed1c2e;
        border:2px solid black;
    }
    .reset-button {
        background-color:#00cc36;
        border:2px solid black;
    }
    .save-button {
        background-color:#0066db;
        border:2px solid black;
    }
    
    .hidden {
        display: none;
    }
    
    @media screen and (max-width:768px) {
    p#timerDisplay.instructions {
    font-size: 18px !important;
    }
}
</style>

<div id="processed-content">
    <h1 class="text-2xl font-bold mb-4">Beach Flags Reaction Time Trainer</h1>
    <button id="startButton" class="bg-gold hover:bg-gray-200 text-black font-bold py-4 px-10 rounded mb-4 start-button">START</button>
    <p id="timerDisplay" class="text-lg mb-4 instructions">When you press START, you will have up to 15 seconds to get into position and be ready for the audio prompts...</p>
    <button id="stopButton" class="hidden bg-white hover:bg-gray-200 text-black font-bold py-4 px-10 rounded mb-4 stop-button">STOP</button>
    <button id="resetButton" class="hidden bg-white hover:bg-gray-200 text-black font-bold py-2 px-4 rounded reset-button">RESET</button>
    <button id="screenshotButton" class="hidden bg-white hover:bg-gray-200 text-black font-bold py-2 px-4 rounded mt-4 save-button">SAVE SCREENSHOT</button>
</div>

<audio id="readyAudio" src="https://www.slslnc.org.au/wp-content/uploads/competitorsready.mp3"></audio>
<audio id="headsDownAudio" src="https://www.slslnc.org.au/wp-content/uploads/headsdown.mp3"></audio>
<audio id="whistleAudio" src="https://www.slslnc.org.au/wp-content/uploads/whistle.mp3"></audio>

<script>
    const startButton = document.getElementById("startButton");
    const stopButton = document.getElementById("stopButton");
    const resetButton = document.getElementById("resetButton");
    const screenshotButton = document.getElementById("screenshotButton");
    const timerDisplay = document.getElementById("timerDisplay");

    const readyAudio = document.getElementById("readyAudio");
    const headsDownAudio = document.getElementById("headsDownAudio");
    const whistleAudio = document.getElementById("whistleAudio");

    let countdownTimeout, headsDownTimeout, whistleTimeout, stopwatchInterval;
    let startTime;

    startButton.addEventListener("click", function() {
        startButton.classList.add("hidden");
        timerDisplay.textContent = "Get ready...";

        const randomCountdown = Math.floor(Math.random() * 6) + 10;

        countdownTimeout = setTimeout(() => {
            readyAudio.play();
            
            const randomReadyTime = Math.floor(Math.random() * 2) + 3;

            headsDownTimeout = setTimeout(() => {
                headsDownAudio.play();
                
                const randomWhistleTime = Math.floor(Math.random() * 3) + 2;

                whistleTimeout = setTimeout(() => {
                    whistleAudio.play();
                    startStopwatch();
                }, randomWhistleTime * 1000);
            }, randomReadyTime * 1000);
        }, randomCountdown * 1000);
    });

    function startStopwatch() {
        startTime = Date.now();
        stopButton.classList.remove("hidden");
        
        stopwatchInterval = setInterval(() => {
            const elapsedTime = ((Date.now() - startTime) / 1000).toFixed(2);
            timerDisplay.textContent = `Time: ${elapsedTime} s`;
        }, 10);
    }

    stopButton.addEventListener("click", function() {
        clearInterval(stopwatchInterval);
        stopButton.classList.add("hidden");
        resetButton.classList.remove("hidden");
        screenshotButton.classList.remove("hidden");
    });

    resetButton.addEventListener("click", function() {
        clearTimeout(countdownTimeout);
        clearTimeout(headsDownTimeout);
        clearTimeout(whistleTimeout);
        timerDisplay.textContent = "Get ready...";
        resetButton.classList.add("hidden");
        screenshotButton.classList.add("hidden");
        startButton.classList.remove("hidden");
    });

    screenshotButton.addEventListener("click", function() {
        html2canvas(document.querySelector("#processed-content")).then(canvas => {
            const link = document.createElement('a');
            link.href = canvas.toDataURL();
            link.download = 'reaction_time.png';
            link.click();
        });
    });

    function loadHtml2Canvas() {
        const script = document.createElement("script");
        script.src = "https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js";
        document.body.appendChild(script);
    }

    loadHtml2Canvas();
</script>


Solution

  • This problem has solutions already on Stackoverflow, so I have voted to close. But as you have said you are not a developer and didn't know how to boil the code down, I've inserted a workaround into your code here just to get you started.

    For each audio this snippet plays then immediately pauses it on the START click. This makes it playable (resume play really) within the setTimeouts on mobile.

    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
    <style>
        #processed-content {
            background-image: url('https://www.slslnc.org.au/wp-content/uploads/timerbackground.jpg');
            background-position: center;
            background-size: cover;
            min-height: 96vh;
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            padding: 16px;
            text-align: center;
        }
    
        #processed-content h1,
        #processed-content p {
            color: black;
            text-shadow: 0 0 15px white;
            font-size: 2rem;
            margin:50px 0;
        }
        
        p#timerDisplay.instructions {
        font-size: 18px !important;
    }
    
        #processed-content button {
            color: black;
            font-size: 2rem;
            margin:20px 0;
        }
        .start-button {
            background-color:#ffd520;
            border:2px solid black;
        }
        .stop-button {
            background-color:#ed1c2e;
            border:2px solid black;
        }
        .reset-button {
            background-color:#00cc36;
            border:2px solid black;
        }
        .save-button {
            background-color:#0066db;
            border:2px solid black;
        }
        
        .hidden {
            display: none;
        }
        
        @media screen and (max-width:768px) {
        p#timerDisplay.instructions {
        font-size: 18px !important;
        }
    }
    </style>
    
    <div id="processed-content">
        <h1 class="text-2xl font-bold mb-4">Beach Flags Reaction Time Trainer</h1>
        <button id="startButton" class="bg-gold hover:bg-gray-200 text-black font-bold py-4 px-10 rounded mb-4 start-button">START</button>
        <p id="timerDisplay" class="text-lg mb-4 instructions">When you press START, you will have up to 15 seconds to get into position and be ready for the audio prompts...</p>
        <button id="stopButton" class="hidden bg-white hover:bg-gray-200 text-black font-bold py-4 px-10 rounded mb-4 stop-button">STOP</button>
        <button id="resetButton" class="hidden bg-white hover:bg-gray-200 text-black font-bold py-2 px-4 rounded reset-button">RESET</button>
        <button id="screenshotButton" class="hidden bg-white hover:bg-gray-200 text-black font-bold py-2 px-4 rounded mt-4 save-button">SAVE SCREENSHOT</button>
    </div>
    
    <audio id="readyAudio" src="https://www.slslnc.org.au/wp-content/uploads/competitorsready.mp3"></audio>
    <audio id="headsDownAudio" src="https://www.slslnc.org.au/wp-content/uploads/headsdown.mp3"></audio>
    <audio id="whistleAudio" src="https://www.slslnc.org.au/wp-content/uploads/whistle.mp3"></audio>
    
    <script>
        const startButton = document.getElementById("startButton");
        const stopButton = document.getElementById("stopButton");
        const resetButton = document.getElementById("resetButton");
        const screenshotButton = document.getElementById("screenshotButton");
        const timerDisplay = document.getElementById("timerDisplay");
    
        const readyAudio = document.getElementById("readyAudio");
        const headsDownAudio = document.getElementById("headsDownAudio");
        const whistleAudio = document.getElementById("whistleAudio");
    
        let countdownTimeout, headsDownTimeout, whistleTimeout, stopwatchInterval;
        let startTime;
    
        startButton.addEventListener("click", function() {
        
        //ADDED for mobile devices. Audio needs to be started directly on a click otherwise it wont play in the timeout
        readyAudio.play();
        readyAudio.pause();
        headsDownAudio.play();
        headsDownAudio.pause();
        whistleAudio.play();
        whistleAudio.pause();
            startButton.classList.add("hidden");
            timerDisplay.textContent = "Get ready...";
    
            const randomCountdown = Math.floor(Math.random() * 6) + 10;
    
            countdownTimeout = setTimeout(() => {
                readyAudio.play();
                
                const randomReadyTime = Math.floor(Math.random() * 2) + 3;
    
                headsDownTimeout = setTimeout(() => {
                    headsDownAudio.play();
                    
                    const randomWhistleTime = Math.floor(Math.random() * 3) + 2;
    
                    whistleTimeout = setTimeout(() => {
                        whistleAudio.play();
                        startStopwatch();
                    }, randomWhistleTime * 1000);
                }, randomReadyTime * 1000);
            }, randomCountdown * 1000);
        });
    
        function startStopwatch() {
            startTime = Date.now();
            stopButton.classList.remove("hidden");
            
            stopwatchInterval = setInterval(() => {
                const elapsedTime = ((Date.now() - startTime) / 1000).toFixed(2);
                timerDisplay.textContent = `Time: ${elapsedTime} s`;
            }, 10);
        }
    
        stopButton.addEventListener("click", function() {
            clearInterval(stopwatchInterval);
            stopButton.classList.add("hidden");
            resetButton.classList.remove("hidden");
            screenshotButton.classList.remove("hidden");
        });
    
        resetButton.addEventListener("click", function() {
            clearTimeout(countdownTimeout);
            clearTimeout(headsDownTimeout);
            clearTimeout(whistleTimeout);
            timerDisplay.textContent = "Get ready...";
            resetButton.classList.add("hidden");
            screenshotButton.classList.add("hidden");
            startButton.classList.remove("hidden");
        });
    
        screenshotButton.addEventListener("click", function() {
            html2canvas(document.querySelector("#processed-content")).then(canvas => {
                const link = document.createElement('a');
                link.href = canvas.toDataURL();
                link.download = 'reaction_time.png';
                link.click();
            });
        });
    
        function loadHtml2Canvas() {
            const script = document.createElement("script");
            script.src = "https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js";
            document.body.appendChild(script);
        }
    
        loadHtml2Canvas();
    </script>