Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

The AudioContext was not allowed to start #22

Closed
feross opened this issue May 21, 2020 · 8 comments
Closed

The AudioContext was not allowed to start #22

feross opened this issue May 21, 2020 · 8 comments

Comments

@feross
Copy link

feross commented May 21, 2020

I get a warning on page load even when I haven't actually called play() yet. It seems like this is an issue in howler because they call new AudioContext() immediately.

Is it possible to somehow workaround this in use-sound?

@joshwcomeau
Copy link
Owner

Hm that's a bummer :/

For context: initially (before this repo existed) I created a useSound hook that did everything from scratch, and I couldn't get rid of that warning. I needed to create an AudioContext to be able to pre-emptively load the sound, otherwise there would be a delay when play was called. The warning was a small price to pay for no-latency.

When I switched to Howler, the warning disappeared, so I figured they knew something I didn't 🤷🏻‍♂️

Unfortunately I don't see much of a workaround (though I'd accept PRs if anyone has ideas that don't compromise the user experience!)

@techieeliot
Copy link

+1 I'm also getting this error @joshwcomeau. I'm having issues playing the audio correctly, also.

@ericblade
Copy link

ericblade commented Aug 23, 2020

Yeah this seems to be an issue -- Howler defaults to attempting to auto-unlock the audio but that doesn't seem to be working. I'm trying to implement a use-sound use in a web-based application here, but pretty much everything i try just fails to get it working correctly -- The first attempt to play back any sound inside a specific React component fails. Not the first time in the application, but the first for each independent component. It fails without error, usually, but sometimes with complaints about the AudioContext not being allowed.

I've got my main application, which has a button that takes you to a menu. On that menu are a bunch of on/off switches. The intent is to have the on/off switches click nicely.

The switches only work after the first time. So, I threw in a click when you press the menu button. In that case, the click only occurs the second time you click the menu button, and still only the second time you click the on/off switch.

It's very weird.

Perhaps this snippet from the Howler docs would help, but I can't see any obvious way of implementing it here:

var sound = new Howl({
  src: ['sound.webm', 'sound.mp3'],
  onplayerror: function() {
    sound.once('unlock', function() {
      sound.play();
    });
  }
});

sound.play();

Perhaps later today or tomorrow I can actually try sticking that entire snippet into my app, instead of using use-sound and see if that works.

@andrewmclagan
Copy link

andrewmclagan commented Dec 3, 2020

We ended up rolling our own Howler hooks due to this issue. We came up with an elegant solution: Simply waiting for user interaction as the error states.

First need a hook that tracks first user interaction events according to the specs:

import { useEffect, useState } from 'react';

const events = ['mousedown', 'touchstart'];

export default function useInteraction() {
  const [ready, setReady] = useState(false);

  const listener = () => {
    if (ready === false) {
      setReady(true);
    }
  };

  useEffect(() => {
    events.forEach((event) => {
      document.addEventListener(event, listener);
    });

    return () => {
      events.forEach((event) => {
        document.removeEventListener(event, listener);
      });   
    };
  }, []);

  return ready;
}

Then we wrapped howler in a simple async dynamic import hook that only loads it after the first user interaction.

import { useState, useEffect } from 'react';
import useInteraction from '../useInteraction';

export default function useAudio(options) {
  const [audio, setAudio] = useState();

  const interacted = useInteraction();

  useEffect(() => {
    async function createAudoContext() {
      const { Howl } = await import('howler');
      setAudio(new Howl(options));
    }

    if (interacted) {
      createAudoContext();
    }

    return () => {
      if (audio) {
        audio.unload();
      }
    };
  }, [options]);

  const ready = Boolean(interacted && audio);

  return { audio, ready };
}

All this in only a few Kb of js. Simply use it like this:

const UsersList = ({ users }) => {
  const { audio, ready } = useAudio({ src: "https://g.com/ding.mp3" });

  useEffect(() => {
    if (ready) {
      audio.play()
    }
  }, [users.length, ready]);

  return <div>{users}</div>;
};

(pseudo code)

@LandonSchropp
Copy link

LandonSchropp commented Mar 25, 2021

@andrewmclagan I really like your guys' solution, but I ran into some issues with it. For some reason, using the hook was causing infinite looping for me. I also found that I didn't need to load Howler asynchronously—I didn't see the AudioContext warning when importing it directly. Finally, I changed the API a bit to suit my personal use case.

Here's my modified version of your useAudio hook.

import { Howl } from "howler";
import { useEffect, useRef } from "react";

import useInteraction from "./use-interaction";

export default function useAudio(soundPath) {
  const hasInteracted = useInteraction();
  const audioRef = useRef();

  useEffect(() => {
    if (!hasInteracted) {
      return;
    }

    let audio = new Howl({ src: soundPath });
    audioRef.current = audio;

    return () => audio.unload();
  }, [ hasInteracted, soundPath ]);

  return () => audioRef.current?.play();
}

Thanks for posting this! It really helped out my project a lot.

@joshwcomeau
Copy link
Owner

Hm, so it seems like Howler / the audio file are only loaded after the user interacts with the page?

I'd rather pre-load everything so that the sound can be triggered the moment the user interacts with the page.

As annoying as the console warning is, I think that's the better trade-off here

@ryan-sirka
Copy link

.current?.play()

hello sir, can i see full of your code to play audio?

@PAXANDDOS
Copy link

I just bumped into this issue too, and I only needed some background music...

Your solutions @andrewmclagan @LandonSchropp worked just great! But then I went to the Howler API docs and found an even better way. Howler has its own "unlock" feature and can listen to the errors, this can be used to help us.

See Howler's README

And finally, my hook looks like that:

import { Howl } from 'howler'
import { useEffect, useState } from 'react'

import clown from '@assets/music/clown.mp3'

export const useMusic = () => {
    const [audio, setAudio] = useState<Howl | null>(null)

    useEffect(() => {
        const howl = new Howl({
            src: clown,
            onplayerror: (e, d) => {
                console.log(e, d)
                howl.once('unlock', () => {
                    howl.play()
                })
            },
            loop: true,
            volume: 0.25,
            autoplay: true,
        })
        setAudio(howl)

        return () => {
            howl.unload()
        }
    }, [])

    return [audio] as const
}

This still will create a bunch of warnings in the console, but works!

Note that I have howler required in my package.json so might need to do this too (plus @types/howler for typescript)

Linked issues:
goldfire/howler.js#1532
goldfire/howler.js#1287

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

8 participants