(2021.3.14) My music looping code doesn't work right in WebGL

I have a class called MusicPlayer2 (attached) that is used to play music in my game. It works by using two audio sources that “take turns.” One plays an audio clip while the other waits to play the next one. This system uses AudioSettings.dspTime and AudioSource.PlayScheduled for timing.

The reason that it does this instead of having one audio source set to loop the clip is that one level uses a music track that dynamically changes its loop points based on the player’s progress through the level, so the music player won’t always just be playing the same clip in a loop.

This system usually works, but it breaks down in WebGL builds. Web browsers won’t let an HTML element play any audio until A) the user clicks on it, or B) the browser has decided the user always wants the audio to play based on past activity on the website. If the browser isn’t letting audio play, AudioSettings.dspTime will still advance and Unity will still try to play audio clips at the scheduled times, but the browser will force playback of those sounds to pause until the user has clicked on the game.

This results in a scenario like this:

  1. User clicks on the game view and begins playing a level.
  2. User clicks outside of the game view, causing playback of the current music clip to pause.
  3. AudioSettings.dspTime continues to advance while the music is paused.
  4. User clicks back inside the game view. Because music playback was paused but dspTime was still advancing, the first music clip will now finish playing late, and part of the two music clips will overlap.
  5. There will be a gap of silence after the next clip finishes. I’m not really sure what’s going on with that.

If I could detect when the browser wasn’t allowing audio to play, I would be able to do something to fix this. Is there a way to do that? If not, what could I do to resolve the issue?

9137920–1269178–MusicPlayer2.cs (11.7 KB)

I am not quite certain of the origin story of that restriction (requiring interaction before enabling audio) but I’ve been told that it was because of security restrictions on the browsers side. “Why does Youtube or Spotify or such web apps don’t show this behavior?” - I could only speculate…

I think you could write some code on the JavaScript side (by adding a .jslib in your Plugin folder) to detect when the browser window is active and control a state that you could monitor to detect to fix your problem.

#if UNITY_WEBGL && !UNITY_EDITOR
[DllImport("__Internal")]
private static extern int hasUserInteracted();
#endif

void Update() {
#if UNITY_WEBGL && !UNITY_EDITOR
    bool hasInteracted = hasUserInteracted() == 1;
    if (hasInteracted) {
        // Proceed with music playback
    } else {
        // Handle non-focused state
    }
#endif
    // ...
}

Thanks. This is the sort of thing I was looking for.

I was successfully able to get the JavaScript and C# code talking to each other, but now I’ve run into another problem. I can’t figure out how to detect when audio is allowed to play. I thought that I’d be able to use the “focus” and “blur” events, but these events never seem to fire for a canvas element.

mergeInto(LibraryManager.library,
{
    InitializeJS : function()
    {
        this.inFocus = 0;

        var element = document.getElementById("unity-canvas");
        if (!element)
        {
            console.log("Failed to find element.");
            return;
        }
        else
        {
            // This message appears in the browser console when the function is called by C# code.
            console.log("Element was found and the event listeners just aren't working.");
        }
        element.addEventListener("focus", function()
        {
            // This message, as well as the next one, never appears in the console.
            console.log("Game window gained focus.");
            this.inFocus = 1;
        });
        element.addEventListener("blur", function()
        {
            console.log("Game window lost focus.");
            this.inFocus = 0;
        });
    },

    GameIsFocused : function()
    {
        // return this.inFocus;
        return 1;
    }
});

Do you know of a reliable method for detection?

Edit: I figured it out. The focus and blur events normally don’t fire for a canvas element, but if you set the “tabindex” attribute on the element, they will fire.

var element = document.getElementById("unity-canvas");
element.setAttribute("tabindex", 1);

Edit 2: I actually don’t understand what the audio sources are doing. I have removed all my code that was written to try to fix the looping issue. It turns out that:

  1. AudioSettings.dspTime actually does not advance while audio is not allowed to play, but audio sources still seem to be reaching their scheduled play times somehow. Do they use some sort of internal timer instead of checking dspTime?
  2. The timers on the audio sources also do not advance, but the clip playback doesn’t actually seem to pause. In fact, I think it might be going faster than when audio is allowed. I could have sworn that earlier, they were pausing. I haven’t changed Unity versions or anything.

It appears that the browser is doing something screwy to stop sound from playing that Unity doesn’t handle correctly. No method I have tried has fixed the audio issue. I tried:

  1. Muting the audio sources when out of focus so they don’t produce sound.
  2. Muting the audio listener when out of focus so it doesn’t pick up sound.
  3. Pausing the audio listener when out of focus.
  4. Setting time scale to zero and pausing the audio listener when out of focus.

None of these prevented music from looping incorrectly when focus is lost. I am at my wit’s end with this problem. Surely, there must be some way to address it. I have played several WebGL games that were made in Unity and never encountered this issue before.