goldfire / howler.js

Javascript audio library for the modern web.
https://howlerjs.com
MIT License
23.29k stars 2.21k forks source link

Chrome autoplay policy changes #939

Open robbue opened 6 years ago

robbue commented 6 years ago

Chrome 66 have changed its autoplay policy:

An AudioContext must be created or resumed after the document received a user gesture to enable audio playback. https://developers.google.com/web/updates/2017/09/autoplay-policy-changes#webaudio

mobileAutoEnable could be updated to adjust for Chrome (desktop and mobile) as well.

inear commented 6 years ago

Maybe a subject of its own, but could we somehow register the start time of a play call and use that to seek to the offset position when the AudioContext is resumed? This to avoid having all created instances fire of at the same time when resumed. Sounds are very often timed to visual events which is so frustrating with the new policy. This would be a very good reason for more devs to use this library.

TxAg12 commented 6 years ago

Maybe we could organize to unbreak the internet? I cannot even imagine the economic cost of everyone--most working for free--spending time to hack around these content restrictions.

sammcgrail commented 6 years ago

Hoping for a fix in 2.1.0 ! Gosh darn it chrome you broke my little toy app!

goldfire commented 6 years ago

FYI, the mobileAutoEnable system should still be working on desktop Chrome as well. I just marked it for 2.1.0 as it now makes sense to update the API naming since this isn't a mobile-only issue anymore. If anyone is seeing issues with this not working with Chrome desktop then that would be a more urgent matter.

dangrima90 commented 6 years ago

@goldfire I'm having issues due to this Chrome update. In the application I'm working on every time a sound is loaded I'm seeing the warning mentioned above, which results in the sounds not playing.

Since in Google's autoplay policy (https://developers.google.com/web/updates/2017/09/autoplay-policy-changes#webaudio) it is mentioned that muted autoplay is allowed I've tried to load the application sounds with the mute setting set to true - but so far I didn't see any difference. I don't know if there's any workaround at the moment, or maybe I'm doing something wrong.

Since you mentioned mobileAutoEnable, am I correct in assuming that the mobileAutoEnable is enabled by default? And if so, sounds are played once there's the first user interaction?

goldfire commented 6 years ago

There is currently an open Chromium report about this that is picking up a lot of support. If we can at least get them to add a way to detect this it would make it much easier to handle within the library: https://bugs.chromium.org/p/chromium/issues/detail?id=840866.

dangrima90 commented 6 years ago

Thank you for your reply, let's hope we'll have a way forward soon. I'm a bit disappointed with this change but that's the life of a developer I guess haha.

inear commented 6 years ago

It's not just the problem with resuming the AudioContext, it's also about what should happen when it does. Right now all the sounds that I've tried playing before it's resumed (showing the warning) is played in the same time when resumed.

jmo84 commented 6 years ago

Everybody go to the bug report and vote for it. https://bugs.chromium.org/p/chromium/issues/detail?id=840866

Sieabah commented 6 years ago

@goldfire I believe there is a way to test it by checking the state of the AudioContext.

From their docs there is this:

To detect whether browser will require user interaction to play audio, you can check the state of the AudioContext after you've created it. If you are allowed to play, it should immediately switch to running. Otherwise it will be suspended. If you listen to the statechange event, you can detect changes asynchronously.

At least that way people using Howler will be able to inspect the state. I've been looking through how howl.state() works and if we had access to the raw state value from the audio context it'd allow us to check if interaction is required.

Edit: As it currently stands, state() only returns if it's loaded essentially and playing() will be true despite it not actually playing.

Somewhat related

goldfire commented 6 years ago

@Sieabah Unfortunately, this behavior is not consistent across browsers, which is why howler tries to resume the audio context on play. However, if the audio is still locked, the promise on resume neither resolves nor rejects, essentially leaving the state in limbo. I'm sure we can find a way to work around this, but I'm hoping we hear something soon on what/if anything is going to be changed.

Sieabah commented 6 years ago

@goldfire Right, but you could detect this issue by detecting (for chrome) whether the state is suspended or not immediately after play? I'm all for the universal solution, but currently there is no way to use howler is a reactive way.

I'm against google making this huge change just because they're a huge browser, but I don't believe they're going to back down on this. Is there any reason why you use resume() and not play() to check if audio is locked?

Off the wall solution, would it be possible to play a tiny sound file that is dead silent at init to see if audio is locked?

goldfire commented 6 years ago

I just added this Chromium bug relating to the issue with AudioContext.resume: https://bugs.chromium.org/p/chromium/issues/detail?id=842921

goldfire commented 6 years ago

Chrome has reverted this change on Web Audio (not on HTML5 Audio) and has said it will come back in Chrome 70, due for release in October. The AudioContext issue is also fixed in Chrome 68.

Sieabah commented 6 years ago

@goldfire Great news!

robbue commented 6 years ago

Good news, but only delays the massive side-effects of this change until October. But at least we (and Chrome) have some time to smooth things out.

goldfire commented 5 years ago

Commit https://github.com/goldfire/howler.js/commit/310736b33da50bb8f075eab2eb8a34f66fbc857b updates mobileAutoEnable to work on Chrome as well. I'm going to keep this open though as the naming really needs to updated as a breaking change since this will no longer only apply to mobile.

Sieabah commented 5 years ago

@goldfire Where does howler stand in supporting the new chrome autoplay api changes?

abelsonlive commented 5 years ago

I'm using react-howler, but I had some luck detecting instances when autoplay is blocked by checking if window.Howler.state == 'suspended'

Sieabah commented 5 years ago

@abelsonlive Seems messy to check the window object directly.

dangrima90 commented 5 years ago

Any way forward on this? At the moment the following warning is being shown in the console:

The Web Audio autoplay policy will be re-enabled in Chrome 70 (October 2018). Please check that your website is compatible with it. https://goo.gl/7K7WLu

I'm not understanding what I should do, will this be handled by howler or must the application find a solution for this?

sajjanbalar commented 5 years ago

Hi,

I was able to catch this error and handle it using the onplayerror handler. There is not much you can do to stop this from happening, but handling it and showing a play button (instead of a pause) and stuff like that is easy.

goldfire commented 5 years ago

Sorry about the delay, I've been swamped lately. I'm working on some changes to address this though.

goldfire commented 5 years ago

Okay, so I'm considering two different approaches here:

  1. If the audio is locked, howler just throws a playerror and discards the playback.
  2. As suggested above by @inear, the time the playback was started is tracked and then when audio is unlocked, howler either seeks to the position the sound would have been at or discards the playback if it would have already concluded.

Option 2 I think would be the most beneficial for most applications, but does anyone have any scenarios where this might be best left to the application to handle (and thus going with option 1)?

Sieabah commented 5 years ago

The issue with Option 2 is you wouldn't know that the play failed unless you kept checking either after playing or in a loop elsewhere. Which creates an awkward loop of logic whereas if you errored on play you could create some popup asking for user input.

goldfire commented 5 years ago

@Sieabah The library would still be detecting that the audio is locked either way, that would be able to be checked at a global level.

jmo84 commented 5 years ago

Option 2 sounds like the best option but please keep emitting the playerror event described in Option 1. The only thing I can think of that would be a problem with Option 2 is if it adds too much code to the library.

inear commented 5 years ago

I would appreciate the functionality in option 2 to be available if possible, but could live with using a option-parameter to activate sync to intended global time for sounds that is part of a visual timeline. And use option 1 as default. With that per-sound-parameter we could use it in places where we don't know of context is created, like in cinematic intros, or after the first load. Also, we then don't magically solve the issue, but encourage devs to think twice about their solution and learn to design for the autoplay policy, because it seems like it will stick around. The option 2 is a way out for existing sites where we don't have control over the flow. Or if we just can't have an interaction point at the beginning of the experience.

Sieabah commented 5 years ago

@goldfire So the intended solution is to have code which checks every x seconds or so to see if it's locked and provide a prompt? How would you trigger all the sounds to play again or would Howler automatically do that?

It just seems messy to have check at the global scope.

goldfire commented 5 years ago

As I've played around with this more, I've started leaning towards option 1. I'm seeing the potential for a lot of edge cases and I'm also starting to think option 2 makes more sense as a possible plugin rather than in the core library. I'm working on putting this together in a way that will be easy to detect and handle in advance of Chrome 70 dropping (I was hoping to already have this done, but I've been out of town a lot over the last month).

Sieabah commented 5 years ago

@goldfire I think throwing something like a playerror or something specific to chrome autoplay would be good enough for most cases. How have you been reliably testing the autoplay? Whenever I modify the chrome config to restrict 100% of the autoplay it still just plays.

goldfire commented 5 years ago

Since my previous bug report was closed, I had assumed the issue with AudioContext.resume not rejecting when audio was locked was fixed. Not surprisingly, that is not the case, so I've created a new issue: https://bugs.chromium.org/p/chromium/issues/detail?id=884337.

That was the plan to detect if the audio was locked, so now I'm trying to find an alternate solution. I'm all ears if anyone has any ideas.

aaclayton commented 5 years ago

I'm quite new to the howler library, so I don't have any suggestions for you on how best to work around the autoplay policy changes, but I just wanted to provide my encouragement for your efforts. You have a great library that is doing a big service to a lot of developers and if you are able to provide a sensible way of handling the Chrome audio restrictions it would be hugely valuable. Thanks for your efforts @goldfire!

damrbaby commented 5 years ago

Safari 12, which was released today, has a similar issue where if "Auto-Play" is not allowed in settings for the website (which seems to be the default), web audio will not play (tested with latest version of Howler.js). I'm also noticing that in Safari 12 the AudioContext.resume promise does not resolve or reject similar to the chromium issue. The "Spatial Audio" demo on the https://howlerjs.com homepage does not work on Safari 12 unless Auto-Play is allowed, but it does work on Safari 11 even without Auto-Play being allowed (I tested both).

Sieabah commented 5 years ago

Google is poorly handling this clusterfuck

goldfire commented 5 years ago

I just pushed a155a1933b99dfb7be3e8ce553ece850f231a2e5 to master, which fixes playing() not showing the correct value when audio is locked.

mike7a commented 5 years ago

Thanks for coding a workaround into the new version of Howler, Gold!

goldfire commented 5 years ago

FYI, the autoplay change has again been pushed back to Chrome 71 in December.

daniloacim commented 5 years ago

Does it mean that we can't enable autoplay on Chrome until Chrome 71 version ?

goldfire commented 5 years ago

Commit https://github.com/goldfire/howler.js/commit/8cfb3978ec5aa5f2a4028424f5aab9c24a8be9b0 has changed the name of this to autoUnlock since it applies to more than just mobile. There is still no way to directly test for the audio being locked, though there are some proposals in the works. Chrome is going ahead with the change anyway, which means you need to make sure your first audio happens in a user interaction for it to work. I'll leave this open as this continues to evolve.

aaclayton commented 5 years ago

Hey goldfire, thanks for the update. Any chance you could provide a high level summary of the recommended usage pattern given the changes, I've been following along. but not super closely.

Any chance of a small example code illustrating how to ensure that the first audio playback is triggered by a "user interaction" and how using the autoUnlock flag would factor in?

I can probably figure this out through some trial and error plus searching about, but if there's any existing documentation or example code which explains how to comply with the policy changes I know it would help me out and I suspect others as well who may be discovering this issue.

Either way, thanks for your work to continue improving your great library!

Sieabah commented 5 years ago

@aaclayton The general rule is do not play audio unless it's after a direct user gesture, only then can you play.

Essentially this change makes "autoplay" unreliable until your site has a high usage score, of which you still can't rely on.

aaclayton commented 5 years ago

Hey @Sieabah, thanks for your clarification, but I'm afraid this was the portion of the change that I did already understand.

What isn't clear to me (and I likely need to just spend some time googling about) is what the best practices are to associate the audio playback with a user gesture. For example, my app has a click listener, when the button is pressed I get Howl to begin playback of some audio. Does Howler expose a convenient API to determine whether or not the audio context is available to be used?

EDIT: Nevermind, my example usage does in fact work properly. So I was mixing something up which caused confusion on the root cause of the error.

For example.... simple usage:

$("#mybutton").click(ev => {
   new Howl({src: "path", autoplay: true});
});

Currently exposes a Chrome warning for me, although clearly this audio is played as the result of a user gesture. What's the right way to do this?

goldfire commented 5 years ago

@aaclayton Create the new Howl outside of the click listener and then call play() inside the click.

Sieabah commented 5 years ago

@aaclayton You never explicitly played it inside. It was an audio tag created async from the click event handler but it's "autoplaying" which is unreliable.

It should work if you create the howler instance and call play in the same click handler. Alternatively the approach of having the howler created outside and playing it inside also works. I wouldn't create howler instances unless they self destruct after sometime.

aaclayton commented 5 years ago

Thanks for the feedback, both of you. Appreciate the follow-up!

shanemielke commented 5 years ago

Hey all. Sorry for the long winded buildup just giving context. I have an interactive comic book for a client that I started well over a year ago (before all of the autoplay stuff). The comic is spread out across 17 chapters (which are on 17 individual html pages) and contains multiple sound objects starting/playing/stopping in sync to animations on a page and soon even a surround sound version. Basically, there's a lot going on once you enter each chapter of the comic. Unfortunately, It's too late to try and combine all of this work into a single page experience with a single enter button. Because of all the autoplay issues across the various browsers & mobile I currently preload all of the imagery/audio and after the preloading is complete I animate in an "ENTER" button for ALL pages regardless of browser/desktop/mobile. That is not a great experience but it works. I would like to see if I can reduce the need for the ENTER button on every page if possible.

I've read about the autoplay policies like Media Engagement Index (MEI) and I'm seeing that working on the dev version of the comic site. On chrome 71 the audio now plays when I change up the code on the pages to bypass the enter button after the preloader. Audio now plays even if I have have not interacted with the page. I am assuming this is because I've refreshed these pages hundreds of times the past year and or just in the last couple of hours tonight and Chrome knows it. This is great for me but it makes me nervous because it's now a little harder to test the site under typical user situations of a first time or unfrequent visitor.

Given my above situation, I'm looking for current best practices to try and know whether or not the audio has been unlocked. What I'm reading here is that my only strategies with howler is to try and play a small mp3 file (without any loud audio) when any page in the site is preloading and then listen for the "onplayerror". Then based upon if there's an error show the button? Does that sound like a safe solution? Or am I missing some variable that I can check that would give me this? Thanks in advance for any help and taking the time to read this.

Sieabah commented 5 years ago

@shanemielke So unfortunately because it's based on metrics you can't get around the autoplay per-page. That's the entire point of why they did it this way I assume. You're going to have to make it a SPA app if you don't want to have to have an enter page per page.

The only reason why it's playing while you're developing because you're engaging with the url enough times (I've experienced this locally). You have to start chrome from the CLI with some extra flags to be able to test from the perspective of a user first coming to the page. I haven't found a better way to do this.

I think the best approach, is to have an enter per page, it's not a great experience but it's the experience chrome wants for the future. For some stupid reason.

ellisio commented 5 years ago

We use Howler to play pops when our chat application receives a new message to be read, using Vue. Below is a solution to eliminate the warning, and to only play pops on unreads once our users actually engage with the page (by activating a conversation, for example).

mixins/popper.js:

import { Howl } from 'howler';

let Howler = null;
let canPlayPop = true;

export default {
    methods: {
        pusherMessageReceived() {
            // This is called by our pusher.listen() config elsewhere in the app.
            this.playPop();
        },

        playPop() {
            // This will bail out until Howler has been initialized, or if we're currently playing a pop sound.
            if (!Howler || !canPlayPop) {
                return;
            }

            canPlayPop = false;
            Howler.play();
        },

        waitForClickForHowler() {
            // Remove the event listener right away so we don't call this more than once for a click happy user.
            document.removeEventListener('click', this.waitForClickForHowler);

            // Initialize our Howl instance.
            Howler = new Howl({
                volume: 0.4,
                src: ['/sounds/notification.mp3'],
                preload: true,
                onend: () => {
                    canPlayPop = true;
                },
            });
       },
    },

    mounted() {
        document.addEventListener('click', this.waitForClickForHowler);
    },
};

This mixin is imported into our primary App.vue. Once a user clicks anywhere on the document, we initialize Howler, and then when a new message comes in we can play a pop. No warning, functional, and adheres to the Chrome reasoning for this behavior.

Sieabah commented 5 years ago

@ellisio I'm curious what your Howler global variable is set to after the click event occurs.

I still think from a usability point where you load the page (and are logged in) that page should be able to play the pops. What about the use case where they don't click anything but just load the page and switch tabs, waiting for a chat?