Bug 217606 - Web Audio broken on iOS 14 after switching app
Summary: Web Audio broken on iOS 14 after switching app
Status: RESOLVED FIXED
Alias: None
Product: WebKit
Classification: Unclassified
Component: Web Audio (show other bugs)
Version: Safari 13
Hardware: iPhone / iPad Unspecified
: P2 Major
Assignee: Chris Dumez
URL:
Keywords: InRadar
Depends on: 218210 218235 218251
Blocks:
  Show dependency treegraph
 
Reported: 2020-10-12 06:15 PDT by Ashley Gullen
Modified: 2020-10-28 19:40 PDT (History)
11 users (show)

See Also:


Attachments
Patch (19.58 KB, patch)
2020-10-26 14:43 PDT, Chris Dumez
ews-feeder: commit-queue-
Details | Formatted Diff | Diff
Patch (6.91 KB, patch)
2020-10-26 16:34 PDT, Chris Dumez
darin: review+
Details | Formatted Diff | Diff
Patch (9.39 KB, patch)
2020-10-28 11:51 PDT, Chris Dumez
ews-feeder: commit-queue-
Details | Formatted Diff | Diff
Patch (9.40 KB, patch)
2020-10-28 11:57 PDT, Chris Dumez
no flags Details | Formatted Diff | Diff
Patch (10.60 KB, patch)
2020-10-28 13:27 PDT, Chris Dumez
no flags Details | Formatted Diff | Diff
Patch (12.24 KB, patch)
2020-10-28 15:24 PDT, Chris Dumez
no flags Details | Formatted Diff | Diff
Patch (10.60 KB, patch)
2020-10-28 16:06 PDT, Chris Dumez
no flags Details | Formatted Diff | Diff
Patch (3.86 KB, patch)
2020-10-28 17:47 PDT, Chris Dumez
no flags Details | Formatted Diff | Diff
Patch (3.87 KB, patch)
2020-10-28 18:19 PDT, Chris Dumez
no flags Details | Formatted Diff | Diff

Note You need to log in before you can comment on or make changes to this bug.
Description Ashley Gullen 2020-10-12 06:15:41 PDT
Demo URL: https://downloads.scirra.com/labs/ios14audio/standard/index.html

Steps to reproduce:
1. Press button and verify a sound can be heard
2. Go to the home screen
3. Switch back to Safari so the page is showing again
4. Press the button again

Observed result:
Audio no longer plays.

Expected result:
Audio to continue playing.

It looks like all our JavaScript code is running as expected, and the AudioContext state is still "running" after switching back to the app, but no audio playback happens. So it appears to be a regression in iOS 14. This affects all web content made in Construct 3 (www.construct.net).
Comment 1 Ashley Gullen 2020-10-12 06:17:40 PDT
Note it also works correctly in Safari 14 on macOS, and in Chrome on Android, so it looks like this only affects iOS.
Comment 2 Radar WebKit Bug Importer 2020-10-12 17:39:39 PDT
<rdar://problem/70231769>
Comment 3 Chris Dumez 2020-10-26 09:57:53 PDT
I see an exception showing in the console on homing out:
Unhandled Promise Rejection: undefined

		_SetSuspended(e)
		{
			const isSuspended = e["isSuspended"];

			console.log("[Construct 3] Setting audio suspended: ", isSuspended);
			
			// Upon resume: first resume the whole context
			if (!isSuspended && this._audioContext["resume"])
				this._audioContext["resume"]();
			
			// Suspend or resume all audio instances
			for (const ai of this._audioInstances)
				ai.SetSuspended(isSuspended);
				
			// After suspend: also suspend the whole context
			if (isSuspended && this._audioContext["suspend"])
				this._audioContext["suspend"](); // On this line.
		}

Looks like the script is not dealing with this promise rejection.
Comment 4 Ashley Gullen 2020-10-26 10:18:03 PDT
Why would that promise reject? It didn't before, so this seems to be new.
Comment 5 Chris Dumez 2020-10-26 10:48:30 PDT
(In reply to Ashley Gullen from comment #4)
> Why would that promise reject? It didn't before, so this seems to be new.

I will investigate. It is possible it is new behavior since we've been working on aligning our WebAudio implementation with the W3C specification.
Comment 6 Chris Dumez 2020-10-26 10:48:53 PDT
Still reproduces on with WebKit trunk.
Comment 7 Chris Dumez 2020-10-26 10:57:22 PDT
suspend() throws because the audio context's state is "interrupted", likely because the view is not longer visible and we interrupt audio in this case.

It seems wrong to reject the promise in this case. Seems like we should set the "suspendedByUser" flag and resolve the promise in this case.
Comment 8 Ashley Gullen 2020-10-26 12:12:16 PDT
This may be off topic but the spec says the only valid AudioContext states are "running", "suspended" and "closed". Since "interrupted" isn't a specified state we haven't written any code to try to handle that. Separately, this has caused us problems in the past in WKWebView, since some things (in our case showing an AdMob ad) seemed to cause the AudioContext to end up in this non-standard state, causing audio playback to fail in that case as well.
Comment 9 Chris Dumez 2020-10-26 12:16:45 PDT
(In reply to Ashley Gullen from comment #8)
> This may be off topic but the spec says the only valid AudioContext states
> are "running", "suspended" and "closed". Since "interrupted" isn't a
> specified state we haven't written any code to try to handle that.
> Separately, this has caused us problems in the past in WKWebView, since some
> things (in our case showing an AdMob ad) seemed to cause the AudioContext to
> end up in this non-standard state, causing audio playback to fail in that
> case as well.

Yes, "interrupted" is not a standard state and I agree we should stop exposing to the Web this internal implementation detail.
Comment 10 Chris Dumez 2020-10-26 14:25:16 PDT
Even after I stop rejecting the suspend() promise and get rid of the "interrupted" state, clicking the button still does not play audio sometimes (it is flaky). I am not sure why (The JS is pretty complicated) and there is no longer any exception/error in the console.

When the button does not cause audio playback, I see that the AudioContext's state is "running", as expected.
Comment 11 Chris Dumez 2020-10-26 14:38:00 PDT
(In reply to Chris Dumez from comment #10)
> Even after I stop rejecting the suspend() promise and get rid of the
> "interrupted" state, clicking the button still does not play audio sometimes
> (it is flaky). I am not sure why (The JS is pretty complicated) and there is
> no longer any exception/error in the console.
> 
> When the button does not cause audio playback, I see that the AudioContext's
> state is "running", as expected.

Could you point me to the code that actually plays the audio when I click the button? I tried to follow in Web Inspector but could not easily find the logic.
Comment 12 Chris Dumez 2020-10-26 14:43:47 PDT
Created attachment 412355 [details]
Patch
Comment 13 Ashley Gullen 2020-10-26 14:54:39 PDT
If you beautify the source and search for createBufferSource, there is one in a method named Play. That is where the actual playback is done and the call to start() made.
Comment 14 Chris Dumez 2020-10-26 15:23:19 PDT
(In reply to Ashley Gullen from comment #13)
> If you beautify the source and search for createBufferSource, there is one
> in a method named Play. That is where the actual playback is done and the
> call to start() made.

Thanks, will debug further.
Comment 15 Chris Dumez 2020-10-26 16:34:38 PDT
Created attachment 412364 [details]
Patch
Comment 16 Chris Dumez 2020-10-27 08:31:05 PDT
I landed this first patch via Bug 218235 and am keeping this bug open because it is still very flaky. There seems to be a remaining issue where when going back to Safari, we try to resume the AudioContext and fail due to a permission error. The call to AudioOutputUnitStart() fails with an AVAudioSessionErrorCodeCannotStartPlaying error code and we still move the AudioContext's state to "running" and resolve the resume() promise. There are several issues here:
1. If the attempt to start the AudioDestination fails, we should probably reject the resume() promise and not set the state to "running".
2. Why are we getting a AVAudioSessionErrorCodeCannotStartPlaying error when trying to start the AudioDestination when going back to Safari. This is flaky so I assume we attempt to start the destination slightly before AVFoundation realizes we are allowed to play.
Comment 17 Chris Dumez 2020-10-27 15:01:12 PDT
After the fixes from Bug 218235 & Bug 218251, this test page provided actually works reliably how. That said, it only works reliably because the test page keeps trying to resume the context until context.state becomes "running".

As a result, I am keeping this bug open for now. We still have a race when coming back to Safari causing us to fail to resume rendering in some cases. Without the retry logic on the JS side, it would fail which is bad. I am looking into why this race is happening and if we can resolve it. If not, I guess we could implement a retry logic on WebKit side to avoid relying on the page's JS to do so.
Comment 18 Chris Dumez 2020-10-28 11:51:50 PDT
Created attachment 412556 [details]
Patch
Comment 19 Chris Dumez 2020-10-28 11:57:55 PDT
Created attachment 412558 [details]
Patch
Comment 20 Chris Dumez 2020-10-28 13:27:08 PDT
Created attachment 412567 [details]
Patch
Comment 21 Eric Carlson 2020-10-28 14:28:47 PDT
Comment on attachment 412567 [details]
Patch

View in context: https://bugs.webkit.org/attachment.cgi?id=412567&action=review

r=me once the bots are happy

> Source/WebCore/platform/audio/cocoa/AudioDestinationCocoa.cpp:108
> +            weakThis->setIsPlaying(true);

Why not call `setIsPlaying` when success is false?
Comment 22 Chris Dumez 2020-10-28 15:02:26 PDT
Comment on attachment 412567 [details]
Patch

View in context: https://bugs.webkit.org/attachment.cgi?id=412567&action=review

>> Source/WebCore/platform/audio/cocoa/AudioDestinationCocoa.cpp:108
>> +            weakThis->setIsPlaying(true);
> 
> Why not call `setIsPlaying` when success is false?

I am maintaining pre-existing behavior here.

If you try to start and starting fails, presumably there is nothing to do since you did not start playing. isPlaying should already be false?
Either way, seems out of scope of this patch no? I am not changing behavior here AFAICT.
Comment 23 Chris Dumez 2020-10-28 15:24:47 PDT
Created attachment 412577 [details]
Patch
Comment 24 Chris Dumez 2020-10-28 16:06:13 PDT
Created attachment 412583 [details]
Patch
Comment 25 Chris Dumez 2020-10-28 17:47:10 PDT
Created attachment 412595 [details]
Patch
Comment 26 Eric Carlson 2020-10-28 18:12:04 PDT
Comment on attachment 412595 [details]
Patch

r=me once the bots can deal with it
Comment 27 Chris Dumez 2020-10-28 18:19:51 PDT
Created attachment 412599 [details]
Patch
Comment 28 EWS 2020-10-28 19:40:36 PDT
Committed r269134: <https://trac.webkit.org/changeset/269134>

All reviewed patches have been landed. Closing bug and clearing flags on attachment 412599 [details].