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

Timers are unavailable until AudioContexts are allowed to run. #462

Open
patternleaf opened this issue Apr 26, 2023 · 3 comments
Open

Timers are unavailable until AudioContexts are allowed to run. #462

patternleaf opened this issue Apr 26, 2023 · 3 comments

Comments

@patternleaf
Copy link

Per autoplay policy, an AudioContext won't be allowed to run until a user gesture of some sort happens. Calls to audio-context-timers.setTimeout made before this happens won't function as expected.

One solution: expose the audio context used to provide the timer service. Clients can then observe that context to see when it should be safe to use its timers.

Another solution might be to allow clients to provide the AudioContext. This would also allow clients to better manage the number of AudioContexts running around and avoid possibly bumping up against browser limits.

@chrisguttandin
Copy link
Owner

As far as I can tell the autoplay policy gets lifted in any browser as soon as you resume any AudioContext. Once this has been done for one AudioContext every other AudioContext seems to resume, too. I made a little demo to verify this:

https://stackblitz.com/edit/js-vvbsjc?file=index.js,index.html

Anyway, it feels not okay to create an AudioContext only to resume it. What do you think about a new exported function that could be used to set the AudioContext? I think you already proposed above. Is that what you had in mind, too?

import { setContext, setTimeout } from 'audio-context-timers';

const audioContext = new AudioContext();

setContext(audioContext);

document.onclick = () => {
    audioContext.resume();
    setTimeout(() => console.log('yeah'), 1000);
}

@patternleaf
Copy link
Author

@chrisguttandin Thanks. Yes a setContext would help our use case. Maybe consider also adding a getContext?

More info about what we were trying to do:

  • We had a small internal cover over timing services, which itself exported createTimer and clearTimer.
  • Sometimes createTimer needed to be called before any user gesture.
  • We also used AudioContexts for actual audio playback and manipulation in other places.

To service createTimer calls before an audio context was running we had to return vanilla browser timers. The trouble was that createTimer didn't have a clean way to know when it could start using ACT timers.

// timers.ts
import { setTimeout as actSetTimeout } from 'audio-context-timers';

export const createTimer = (callback: Function, delay): TimerId => {
  if (canUseActTimers) {                                                 // <----- how to know this?
    return actSetTimeout(callback, delay);
  }
  return window.setTimeout(callback, delay);
};

To answer this question we made a local AudioContext just for the purpose of attempting to resume() it and check its state. As you say: not great.

A setContext exported from audio-context-timers would let us pass our own AudioContext, which is cool, and we could query that same one. Two downsides I'm thinking of:

  • It's not obvious why our timing service would need to depend on our AudioContext, but ... eh, not a big deal.
  • Depending on the behavior of setContext we might have to do some extra work. For example, let's say our AudioContext isn't initialized immediately on load. Would we need to track whether we've called setContext? Would audio-context-timers have already created its own default AudioContext if we don't call it right away?

Alternatively, if a getContext was provided we could just query that directly:

import { setTimeout as actSetTimeout, getContext as actGetContext } from 'audio-context-timers';

export const createTimer = (callback: Function, delay): TimerId => {
  if (actGetContext().state === 'running') {
    return actSetTimeout(callback, delay);
  }
  return window.setTimeout(callback, delay);
};

Or if we definitely wanted to use our own AudioContext for timers to avoid browser limits, etc, we could use both getContext and setContext, something like:

import {
  setTimeout as actSetTimeout,
  setContext as actSetContext,
  getContext as actGetContext
} from 'audio-context-timers';

import { getGlobalAudioContext } from '../local-audio-service';

export const createTimer = (callback: Function, delay): TimerId => {
  let timerContext = actGetContext();
  const globalContext = getGlobalAudioContext();

  if (!timerContext && globalContext) {
    actSetContext(globalContext);
    timerContext = globalContext; 
  }

  if (timerContext && context.state === 'running') {
    return actSetTimeout(callback, delay);
  }

  return window.setTimeout(callback, delay);
};

So anyway I guess I'd advocate for both a setContext and getContext. But either one would help clean up this use case. Thanks for the library, it's super helpful!

@chrisguttandin
Copy link
Owner

Thanks for telling me more about your use case. It got me thinking. Would it be an option for you to use worker-timers instead?

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

2 participants