AudioContext async state transitions [chromium/src : main]

0 views
Skip to first unread message

Hongchan Choi (Gerrit)

unread,
Jan 9, 2026, 7:05:15 PM (11 days ago) Jan 9
to Felipe Erias, Chromium Metrics Reviews, srirama chandra sekhar, AyeAye, Chromium LUCI CQ, chromium...@chromium.org, asvitkine...@chromium.org, blink-re...@chromium.org, feature-me...@chromium.org, eric.c...@apple.com, blink-revie...@chromium.org, blink-...@chromium.org, jmedle...@chromium.org, kinuko...@chromium.org
Attention needed from Felipe Erias

Hongchan Choi added 6 comments

Patchset-level comments
File-level comment, Patchset 11 (Latest):
Hongchan Choi . resolved

Thanks for your contribution!

The code review on the autoplay part will take some time because the WebAudio owners will need to understand the actual implementation.

It generally looks good, but I am curious about your evaluation plan on the rollout other than using usage counters.

File third_party/blink/renderer/modules/webaudio/audio_context.cc
Line 953, Patchset 11 (Latest): "Cannot suspend a closed AudioContext."));
Hongchan Choi . unresolved

Perhaps we can expand this error message to be more descriptive?

File third_party/blink/web_tests/http/tests/media/autoplay/document-user-activation-navigation-nogesture-expected.txt
Line 1, Patchset 11 (Latest):CONSOLE WARNING: The AudioContext was not allowed to start. It must be resumed (or created) after a user gesture on the page. https://developer.chrome.com/blog/autoplay/#web_audio
Hongchan Choi . unresolved

Can you explain why this CONSOLE WARNING is shown here?

File third_party/blink/web_tests/http/tests/media/autoplay/resources/autoplay-test.js
Line 72, Patchset 11 (Latest): const timeout = new Promise(resolve => setTimeout(resolve, 500));
Hongchan Choi . unresolved

I am not entirely sure what test is verifying. (The autoplay code is not implemented by the WebAudio team)

If the intention is to get the state reliably, I think we can use the `onstatechange` event listener within the promise.

File third_party/blink/web_tests/webaudio/internals/audiocontext-close.html
Line 161, Patchset 11 (Latest): await context3.resume();
Hongchan Choi . unresolved

Is this necessary to pass the test?

Line 187, Patchset 11 (Latest): await context.resume();
Hongchan Choi . unresolved

Ditto.

Open in Gerrit

Related details

Attention is currently required from:
  • Felipe Erias
Submit Requirements:
  • requirement satisfiedCode-Coverage
  • requirement is not satisfiedCode-Owners
  • requirement is not satisfiedCode-Review
  • requirement is not satisfiedNo-Unresolved-Comments
  • requirement is not satisfiedReview-Enforcement
Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. DiffyGerrit
Gerrit-MessageType: comment
Gerrit-Project: chromium/src
Gerrit-Branch: main
Gerrit-Change-Id: I79d1bb94f809b460a750b9c37aa2b0d9ab0289c1
Gerrit-Change-Number: 7266845
Gerrit-PatchSet: 11
Gerrit-Owner: Felipe Erias <felip...@igalia.com>
Gerrit-Reviewer: Felipe Erias <felip...@igalia.com>
Gerrit-CC: Chromium Metrics Reviews <chromium-met...@google.com>
Gerrit-CC: Hongchan Choi <hong...@chromium.org>
Gerrit-CC: Michael Wilson <mjwi...@chromium.org>
Gerrit-CC: srirama chandra sekhar <srir...@samsung.com>
Gerrit-Attention: Felipe Erias <felip...@igalia.com>
Gerrit-Comment-Date: Sat, 10 Jan 2026 00:05:06 +0000
Gerrit-HasComments: Yes
Gerrit-Has-Labels: No
satisfied_requirement
unsatisfied_requirement
open
diffy

Felipe Erias (Gerrit)

unread,
Jan 13, 2026, 12:28:16 AM (8 days ago) Jan 13
to Chromium Metrics Reviews, srirama chandra sekhar, AyeAye, Chromium LUCI CQ, chromium...@chromium.org, Hongchan Choi, asvitkine...@chromium.org, blink-re...@chromium.org, feature-me...@chromium.org, eric.c...@apple.com, blink-revie...@chromium.org, blink-...@chromium.org, jmedle...@chromium.org, kinuko...@chromium.org
Attention needed from Hongchan Choi

Felipe Erias added 6 comments

Patchset-level comments
File-level comment, Patchset 11:
Felipe Erias . resolved

Thank you very much for your feedback.

---

The way I thought about the usage counters was that we could use `kAudioContextAsyncStateTransitions` to count the number of page loads with this feature enabled.

From those, `kAudioContextAsyncTransitionToRunningStateRead` and `kAudioContextAsyncTransitionToSuspendedStateRead` will count those that are at risk of introducing a bug because they are reading the context state before it has finished transitioning.

Combining these, we would be able to measure the fraction of page loads which might be at risk.

I do not know whether it would be possible to automatically identify the specific websites where those happen.

---

Regarding the code changes:

There were some unnecessary calls to `resume()` in the tests which I have now removed. I've also reworked the comments as suggested. Please see below.

File third_party/blink/renderer/modules/webaudio/audio_context.cc
Line 953, Patchset 11: "Cannot suspend a closed AudioContext."));
Hongchan Choi . unresolved

Perhaps we can expand this error message to be more descriptive?

Felipe Erias

I've replaced it with

    "AudioContext was closed before a pending suspend() could complete."
File third_party/blink/web_tests/http/tests/media/autoplay/document-user-activation-navigation-nogesture-expected.txt
Line 1, Patchset 11:CONSOLE WARNING: The AudioContext was not allowed to start. It must be resumed (or created) after a user gesture on the page. https://developer.chrome.com/blog/autoplay/#web_audio
Hongchan Choi . unresolved

Can you explain why this CONSOLE WARNING is shown here?

Felipe Erias

This was caused by calling `resume()` inside `test-autoplay.html` `runWebAudioTests()`, which is not needed.

File third_party/blink/web_tests/http/tests/media/autoplay/resources/autoplay-test.js
Line 72, Patchset 11: const timeout = new Promise(resolve => setTimeout(resolve, 500));
Hongchan Choi . unresolved

I am not entirely sure what test is verifying. (The autoplay code is not implemented by the WebAudio team)

If the intention is to get the state reliably, I think we can use the `onstatechange` event listener within the promise.

Felipe Erias

Agree, we don't need to call `resume()` here.

File third_party/blink/web_tests/webaudio/internals/audiocontext-close.html
Line 161, Patchset 11: await context3.resume();
Hongchan Choi . unresolved

Is this necessary to pass the test?

Felipe Erias

No, it is not necessary. I have removed it.

However, this test is now checking a behaviour that is a bit different from the original:

  • before this CL, this test was calling `suspend()` on a context in the "running" state
  • here, `suspend()` is called on a context that is still in the "suspended" state after creation, and will transition to "running" async.
Line 187, Patchset 11: await context.resume();
Hongchan Choi . unresolved

Ditto.

Felipe Erias

Same as above.

Open in Gerrit

Related details

Attention is currently required from:
  • Hongchan Choi
Submit Requirements:
  • requirement satisfiedCode-Coverage
  • requirement is not satisfiedCode-Owners
  • requirement is not satisfiedCode-Review
  • requirement is not satisfiedNo-Unresolved-Comments
  • requirement is not satisfiedReview-Enforcement
Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. DiffyGerrit
Gerrit-MessageType: comment
Gerrit-Project: chromium/src
Gerrit-Branch: main
Gerrit-Change-Id: I79d1bb94f809b460a750b9c37aa2b0d9ab0289c1
Gerrit-Change-Number: 7266845
Gerrit-PatchSet: 12
Gerrit-Owner: Felipe Erias <felip...@igalia.com>
Gerrit-Reviewer: Felipe Erias <felip...@igalia.com>
Gerrit-CC: Chromium Metrics Reviews <chromium-met...@google.com>
Gerrit-CC: Hongchan Choi <hong...@chromium.org>
Gerrit-CC: Michael Wilson <mjwi...@chromium.org>
Gerrit-CC: srirama chandra sekhar <srir...@samsung.com>
Gerrit-Attention: Hongchan Choi <hong...@chromium.org>
Gerrit-Comment-Date: Tue, 13 Jan 2026 05:27:40 +0000
Gerrit-HasComments: Yes
Gerrit-Has-Labels: No
Comment-In-Reply-To: Hongchan Choi <hong...@chromium.org>
satisfied_requirement
unsatisfied_requirement
open
diffy

Hongchan Choi (Gerrit)

unread,
Jan 13, 2026, 2:25:09 PM (7 days ago) Jan 13
to Felipe Erias, Chromium Metrics Reviews, srirama chandra sekhar, AyeAye, Chromium LUCI CQ, chromium...@chromium.org, asvitkine...@chromium.org, blink-re...@chromium.org, feature-me...@chromium.org, eric.c...@apple.com, blink-revie...@chromium.org, blink-...@chromium.org, jmedle...@chromium.org, kinuko...@chromium.org
Attention needed from Felipe Erias and Michael Wilson

Hongchan Choi added 5 comments

Patchset-level comments
File-level comment, Patchset 12 (Latest):
Hongchan Choi . resolved

Looking good! I’m adding mjwilson@ as an additional reviewer since this is a significant change.

I also think this is worth a PSA to blink-dev to give other developers a heads-up.

Thank you so much for this contribution!

File third_party/blink/renderer/modules/webaudio/audio_context.cc
Line 953, Patchset 11: "Cannot suspend a closed AudioContext."));
Hongchan Choi . resolved

Perhaps we can expand this error message to be more descriptive?

Felipe Erias

I've replaced it with

    "AudioContext was closed before a pending suspend() could complete."
Hongchan Choi

Acknowledged

File third_party/blink/web_tests/http/tests/media/autoplay/resources/autoplay-test.js
Line 72, Patchset 11: const timeout = new Promise(resolve => setTimeout(resolve, 500));
Hongchan Choi . resolved

I am not entirely sure what test is verifying. (The autoplay code is not implemented by the WebAudio team)

If the intention is to get the state reliably, I think we can use the `onstatechange` event listener within the promise.

Felipe Erias

Agree, we don't need to call `resume()` here.

Hongchan Choi

Acknowledged

File third_party/blink/web_tests/webaudio/internals/audiocontext-close.html
Line 161, Patchset 11: await context3.resume();
Hongchan Choi . resolved

Is this necessary to pass the test?

Felipe Erias

No, it is not necessary. I have removed it.

However, this test is now checking a behaviour that is a bit different from the original:

  • before this CL, this test was calling `suspend()` on a context in the "running" state
  • here, `suspend()` is called on a context that is still in the "suspended" state after creation, and will transition to "running" async.
Hongchan Choi

Acknowledged

Line 187, Patchset 11: await context.resume();
Hongchan Choi . resolved

Ditto.

Felipe Erias

Same as above.

Hongchan Choi

Acknowledged

Open in Gerrit

Related details

Attention is currently required from:
  • Felipe Erias
  • Michael Wilson
Submit Requirements:
  • requirement satisfiedCode-Coverage
  • requirement is not satisfiedCode-Owners
  • requirement is not satisfiedCode-Review
  • requirement is not satisfiedNo-Unresolved-Comments
  • requirement is not satisfiedReview-Enforcement
Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. DiffyGerrit
Gerrit-MessageType: comment
Gerrit-Project: chromium/src
Gerrit-Branch: main
Gerrit-Change-Id: I79d1bb94f809b460a750b9c37aa2b0d9ab0289c1
Gerrit-Change-Number: 7266845
Gerrit-PatchSet: 12
Gerrit-Owner: Felipe Erias <felip...@igalia.com>
Gerrit-Reviewer: Felipe Erias <felip...@igalia.com>
Gerrit-Reviewer: Michael Wilson <mjwi...@chromium.org>
Gerrit-CC: Chromium Metrics Reviews <chromium-met...@google.com>
Gerrit-CC: Hongchan Choi <hong...@chromium.org>
Gerrit-CC: srirama chandra sekhar <srir...@samsung.com>
Gerrit-Attention: Michael Wilson <mjwi...@chromium.org>
Gerrit-Attention: Felipe Erias <felip...@igalia.com>
Gerrit-Comment-Date: Tue, 13 Jan 2026 19:24:57 +0000
Gerrit-HasComments: Yes
Gerrit-Has-Labels: No
Comment-In-Reply-To: Hongchan Choi <hong...@chromium.org>
Comment-In-Reply-To: Felipe Erias <felip...@igalia.com>
satisfied_requirement
unsatisfied_requirement
open
diffy

Michael Wilson (Gerrit)

unread,
Jan 13, 2026, 6:39:27 PM (7 days ago) Jan 13
to Felipe Erias, Chromium Metrics Reviews, srirama chandra sekhar, AyeAye, Chromium LUCI CQ, chromium...@chromium.org, Hongchan Choi, asvitkine...@chromium.org, blink-re...@chromium.org, feature-me...@chromium.org, eric.c...@apple.com, blink-revie...@chromium.org, blink-...@chromium.org, jmedle...@chromium.org, kinuko...@chromium.org
Attention needed from Felipe Erias and Michael Wilson

Michael Wilson added 8 comments

Patchset-level comments
Michael Wilson . resolved

Thank you for writing this! I still need to review the tests, but want to give some initial feedback now.

File third_party/blink/renderer/modules/webaudio/audio_context.h
Line 554, Patchset 12 (Latest): pending_suspend_resolvers_;
Michael Wilson . unresolved

According to the spec, suspend promises are supposed to go into "pending promises" on the BaseAudioContext (https://webaudio.github.io/web-audio-api/#dom-baseaudiocontext-pending-promises-slot) and resume promises into both this queue and "pending resume promises" (https://webaudio.github.io/web-audio-api/#dom-audiocontext-pending-resume-promises-slot) on the AudioContext, but we only have `pending_promises_resolves_` on the current Chromium BaseAudioContext.

Furthermore, suspend and resume should both use the same "control message queue" (https://webaudio.github.io/web-audio-api/#queuing) but we don't have that concept implemented directly in Chromium.

In terms of the promise queues, from my reading of the spec we should actually do the opposite here: make a queue in AudioContext for the resume promises and store the suspend promises in the BaseAudioContext queue. However, this may have unintended side-effects if we try to implement it with the current Chromium state.

What do you think? If it would be simple to align the promise queues with the spec I would prefer that. However, the suspend transition bug is a big interop problem so if it would significantly delay landing I'm fine with looking at it in a follow-up.

Line 316, Patchset 12 (Latest): void ScheduleTransitionToRunning();
void PerformTransitionToRunning();
Michael Wilson . unresolved
It looks like these functions are only used for the initial transition to running, not for subsequent calls to resume.  Let's rename them to make that more clear.
```suggestion
void ScheduleInitialTransitionToRunning();
void PerformInitialTransitionToRunning();
```
File third_party/blink/renderer/modules/webaudio/audio_context.cc
Line 765, Patchset 12 (Latest):
// If resume() is called before the async transition to "suspended" runs,
// it will clear this flag to make that task a no-op.
suspended_by_user_ = true;
Michael Wilson . unresolved

Minor nit: the spec says this should happen after the promise is appended (https://webaudio.github.io/web-audio-api/#dom-audiocontext-suspend).

Line 830, Patchset 12 (Latest):
// Clear this flag to cancel any pending async transition to "suspended".
suspended_by_user_ = false;
Michael Wilson . unresolved

Referencing https://webaudio.github.io/web-audio-api/#dom-audiocontext-resume, it looks like this should actually happen even earlier (after the closed check, but before the interrupted checks).

Line 949, Patchset 12 (Latest): // Reject all pending suspend promises.
for (auto& resolver : pending_suspend_resolvers_) {
resolver->Reject(MakeGarbageCollected<DOMException>(
DOMExceptionCode::kInvalidStateError,
"AudioContext was closed before a pending suspend() could complete."));
}
pending_suspend_resolvers_.clear();
Michael Wilson . unresolved

Should we guard access to `pending_suspend_resolvers_` here by the graph lock (DeferredTaskHandler::GraphAutoLocker locker(this);)? If not, can we add a comment explaining why?

File third_party/blink/renderer/modules/webaudio/audio_context_test.cc
Line 1622, Patchset 12 (Latest): // The state transition is scheduled to happen asynchronouslly
Michael Wilson . unresolved
```suggestion
// The state transition is scheduled to happen asynchronously
```
File third_party/blink/renderer/modules/webaudio/base_audio_context.h
Line 122, Patchset 12 (Latest): virtual V8AudioContextState state() const;
Michael Wilson . unresolved

Let's add a comment here too that this is `virtual` only to allow adding use counters to the derived AudioContext version of `state()`.

Gerrit-Comment-Date: Tue, 13 Jan 2026 23:39:13 +0000
Gerrit-HasComments: Yes
Gerrit-Has-Labels: No
satisfied_requirement
unsatisfied_requirement
open
diffy

Felipe Erias (Gerrit)

unread,
Jan 13, 2026, 10:06:33 PM (7 days ago) Jan 13
to Chromium Metrics Reviews, srirama chandra sekhar, AyeAye, Chromium LUCI CQ, chromium...@chromium.org, Hongchan Choi, asvitkine...@chromium.org, blink-re...@chromium.org, feature-me...@chromium.org, eric.c...@apple.com, blink-revie...@chromium.org, blink-...@chromium.org, jmedle...@chromium.org, kinuko...@chromium.org
Attention needed from Michael Wilson

Felipe Erias added 8 comments

Patchset-level comments
Felipe Erias . resolved

Thank you very much for your feedback.

File third_party/blink/renderer/modules/webaudio/audio_context.h
Line 554, Patchset 12: pending_suspend_resolvers_;
Michael Wilson . unresolved

According to the spec, suspend promises are supposed to go into "pending promises" on the BaseAudioContext (https://webaudio.github.io/web-audio-api/#dom-baseaudiocontext-pending-promises-slot) and resume promises into both this queue and "pending resume promises" (https://webaudio.github.io/web-audio-api/#dom-audiocontext-pending-resume-promises-slot) on the AudioContext, but we only have `pending_promises_resolves_` on the current Chromium BaseAudioContext.

Furthermore, suspend and resume should both use the same "control message queue" (https://webaudio.github.io/web-audio-api/#queuing) but we don't have that concept implemented directly in Chromium.

In terms of the promise queues, from my reading of the spec we should actually do the opposite here: make a queue in AudioContext for the resume promises and store the suspend promises in the BaseAudioContext queue. However, this may have unintended side-effects if we try to implement it with the current Chromium state.

What do you think? If it would be simple to align the promise queues with the spec I would prefer that. However, the suspend transition bug is a big interop problem so if it would significantly delay landing I'm fine with looking at it in a follow-up.

Felipe Erias

You are correct, but unfortunately the change does not look simple.

Chromium currently uses separate promise queues for different actions, for example:

  • `BaseAudioContext::pending_promises_resolvers_` for resume promises
  • `BaseAudioContext::decode_audio_resolvers_` for decodeAudioData promises

This CL adds another one:

  • `AudioContext::pending_suspend_resolvers_` for suspend promises

(It is in AudioContext because OfflineAudioContext implements suspend in its own way)

However, the spec describes the opposite arrangement:

  • `BaseAudioContext::[[pending promises]]` should store all promises
  • `AudioContext::[[pending resume promises]]` stores only resume promises

Implementing the spec would not be straightforward because much of the existing code expects the current structure, so I would rather tackle it as a follow-up.

Line 316, Patchset 12: void ScheduleTransitionToRunning();
void PerformTransitionToRunning();
Michael Wilson . resolved
It looks like these functions are only used for the initial transition to running, not for subsequent calls to resume.  Let's rename them to make that more clear.
```suggestion
void ScheduleInitialTransitionToRunning();
void PerformInitialTransitionToRunning();
```
Felipe Erias

Done

File third_party/blink/renderer/modules/webaudio/audio_context.cc

// If resume() is called before the async transition to "suspended" runs,
// it will clear this flag to make that task a no-op.
suspended_by_user_ = true;
Michael Wilson . unresolved

Minor nit: the spec says this should happen after the promise is appended (https://webaudio.github.io/web-audio-api/#dom-audiocontext-suspend).

Felipe Erias

Done. I've moved it after the promise is appended.


// Clear this flag to cancel any pending async transition to "suspended".
suspended_by_user_ = false;
Michael Wilson . unresolved

Referencing https://webaudio.github.io/web-audio-api/#dom-audiocontext-resume, it looks like this should actually happen even earlier (after the closed check, but before the interrupted checks).

Felipe Erias

Done. I've moved to the beginning of the method, after the close check.

Line 949, Patchset 12: // Reject all pending suspend promises.

for (auto& resolver : pending_suspend_resolvers_) {
resolver->Reject(MakeGarbageCollected<DOMException>(
DOMExceptionCode::kInvalidStateError,
"AudioContext was closed before a pending suspend() could complete."));
}
pending_suspend_resolvers_.clear();
Michael Wilson . unresolved

Should we guard access to `pending_suspend_resolvers_` here by the graph lock (DeferredTaskHandler::GraphAutoLocker locker(this);)? If not, can we add a comment explaining why?

Felipe Erias

Done. Other accesses to the same field use a lock so it makes sense to do it here as well. I've implemented that change.

File third_party/blink/renderer/modules/webaudio/audio_context_test.cc
Line 1622, Patchset 12: // The state transition is scheduled to happen asynchronouslly
Michael Wilson . resolved
```suggestion
// The state transition is scheduled to happen asynchronously
```
Felipe Erias

Done

File third_party/blink/renderer/modules/webaudio/base_audio_context.h
Line 122, Patchset 12: virtual V8AudioContextState state() const;
Michael Wilson . unresolved

Let's add a comment here too that this is `virtual` only to allow adding use counters to the derived AudioContext version of `state()`.

Felipe Erias

Done.

```cpp
// Virtual so AudioContext::state() can add UseCounters.
virtual V8AudioContextState state() const;
```
Open in Gerrit

Related details

Attention is currently required from:
  • Michael Wilson
Submit Requirements:
  • requirement satisfiedCode-Coverage
  • requirement is not satisfiedCode-Owners
  • requirement is not satisfiedCode-Review
  • requirement is not satisfiedNo-Unresolved-Comments
  • requirement is not satisfiedReview-Enforcement
Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. DiffyGerrit
Gerrit-MessageType: comment
Gerrit-Project: chromium/src
Gerrit-Branch: main
Gerrit-Change-Id: I79d1bb94f809b460a750b9c37aa2b0d9ab0289c1
Gerrit-Change-Number: 7266845
Gerrit-PatchSet: 13
Gerrit-Owner: Felipe Erias <felip...@igalia.com>
Gerrit-Reviewer: Felipe Erias <felip...@igalia.com>
Gerrit-Reviewer: Michael Wilson <mjwi...@chromium.org>
Gerrit-CC: Chromium Metrics Reviews <chromium-met...@google.com>
Gerrit-CC: Hongchan Choi <hong...@chromium.org>
Gerrit-CC: srirama chandra sekhar <srir...@samsung.com>
Gerrit-Attention: Michael Wilson <mjwi...@chromium.org>
Gerrit-Comment-Date: Wed, 14 Jan 2026 03:06:02 +0000
Gerrit-HasComments: Yes
Gerrit-Has-Labels: No
Comment-In-Reply-To: Michael Wilson <mjwi...@chromium.org>
satisfied_requirement
unsatisfied_requirement
open
diffy

Michael Wilson (Gerrit)

unread,
Jan 15, 2026, 6:59:55 PM (5 days ago) Jan 15
to Felipe Erias, Chromium Metrics Reviews, srirama chandra sekhar, AyeAye, Chromium LUCI CQ, chromium...@chromium.org, Hongchan Choi, asvitkine...@chromium.org, blink-re...@chromium.org, feature-me...@chromium.org, eric.c...@apple.com, blink-revie...@chromium.org, blink-...@chromium.org, jmedle...@chromium.org, kinuko...@chromium.org
Attention needed from Felipe Erias

Michael Wilson voted and added 9 comments

Votes added by Michael Wilson

Code-Review+1

9 comments

Patchset-level comments
File-level comment, Patchset 13 (Latest):
Michael Wilson . unresolved

LGTM overall, with a nit and some concern about potential test flakiness.

File third_party/blink/renderer/modules/webaudio/audio_context.h
Line 554, Patchset 12: pending_suspend_resolvers_;
Michael Wilson . resolved

According to the spec, suspend promises are supposed to go into "pending promises" on the BaseAudioContext (https://webaudio.github.io/web-audio-api/#dom-baseaudiocontext-pending-promises-slot) and resume promises into both this queue and "pending resume promises" (https://webaudio.github.io/web-audio-api/#dom-audiocontext-pending-resume-promises-slot) on the AudioContext, but we only have `pending_promises_resolves_` on the current Chromium BaseAudioContext.

Furthermore, suspend and resume should both use the same "control message queue" (https://webaudio.github.io/web-audio-api/#queuing) but we don't have that concept implemented directly in Chromium.

In terms of the promise queues, from my reading of the spec we should actually do the opposite here: make a queue in AudioContext for the resume promises and store the suspend promises in the BaseAudioContext queue. However, this may have unintended side-effects if we try to implement it with the current Chromium state.

What do you think? If it would be simple to align the promise queues with the spec I would prefer that. However, the suspend transition bug is a big interop problem so if it would significantly delay landing I'm fine with looking at it in a follow-up.

Felipe Erias

You are correct, but unfortunately the change does not look simple.

Chromium currently uses separate promise queues for different actions, for example:

  • `BaseAudioContext::pending_promises_resolvers_` for resume promises
  • `BaseAudioContext::decode_audio_resolvers_` for decodeAudioData promises

This CL adds another one:

  • `AudioContext::pending_suspend_resolvers_` for suspend promises

(It is in AudioContext because OfflineAudioContext implements suspend in its own way)

However, the spec describes the opposite arrangement:

  • `BaseAudioContext::[[pending promises]]` should store all promises
  • `AudioContext::[[pending resume promises]]` stores only resume promises

Implementing the spec would not be straightforward because much of the existing code expects the current structure, so I would rather tackle it as a follow-up.

Michael Wilson

Thank you for considering it, and I agree with your assessment. Let's land this change first.

File third_party/blink/renderer/modules/webaudio/audio_context.cc
Line 765, Patchset 12:
// If resume() is called before the async transition to "suspended" runs,
// it will clear this flag to make that task a no-op.
suspended_by_user_ = true;
Michael Wilson . resolved

Minor nit: the spec says this should happen after the promise is appended (https://webaudio.github.io/web-audio-api/#dom-audiocontext-suspend).

Felipe Erias

Done. I've moved it after the promise is appended.

Michael Wilson

Acknowledged

Line 830, Patchset 12:
// Clear this flag to cancel any pending async transition to "suspended".
suspended_by_user_ = false;
Michael Wilson . resolved

Referencing https://webaudio.github.io/web-audio-api/#dom-audiocontext-resume, it looks like this should actually happen even earlier (after the closed check, but before the interrupted checks).

Felipe Erias

Done. I've moved to the beginning of the method, after the close check.

Michael Wilson

Acknowledged

Line 949, Patchset 12: // Reject all pending suspend promises.
for (auto& resolver : pending_suspend_resolvers_) {
resolver->Reject(MakeGarbageCollected<DOMException>(
DOMExceptionCode::kInvalidStateError,
"AudioContext was closed before a pending suspend() could complete."));
}
pending_suspend_resolvers_.clear();
Michael Wilson . resolved

Should we guard access to `pending_suspend_resolvers_` here by the graph lock (DeferredTaskHandler::GraphAutoLocker locker(this);)? If not, can we add a comment explaining why?

Felipe Erias

Done. Other accesses to the same field use a lock so it makes sense to do it here as well. I've implemented that change.

Michael Wilson

Acknowledged

Line 1042, Patchset 13 (Latest): // Flag will be cleared if resume() was called after this task was scheduled.
if (!suspended_by_user_) {
for (auto& resolver : resolvers) {
resolver->Resolve();
}
return;
}

// Stop rendering now.
if (destination()) {
SuspendRendering();
}

for (auto& resolver : resolvers) {
resolver->Resolve();
}
Michael Wilson . unresolved
Nit: we could combine the conditions, although it may be less clear:
```suggestion
if (suspended_by_user_ && destination()) {
SuspendRendering();
}
  for (auto& resolver : resolvers) {
resolver->Resolve();
}
```
File third_party/blink/renderer/modules/webaudio/base_audio_context.h
Line 122, Patchset 12: virtual V8AudioContextState state() const;
Michael Wilson . resolved

Let's add a comment here too that this is `virtual` only to allow adding use counters to the derived AudioContext version of `state()`.

Felipe Erias

Done.

```cpp
// Virtual so AudioContext::state() can add UseCounters.
virtual V8AudioContextState state() const;
```
Michael Wilson

Acknowledged

File third_party/blink/web_tests/http/tests/media/autoplay/resources/autoplay-test.js
Line 79, Patchset 13 (Latest): await Promise.race([stateChange, timeout]);
Michael Wilson . unresolved

This works logically, but I'm worried that using this pattern here and in test-autoplay.html will result in flakiness for the tests.

Gemini suggested that we could refactor the tests to take an expected result state, and await the state change if it is running. It suggested we could look at the result from runMediaTests to see if we should be blocked or not.

But, I'm not sure how robust that is. Do we have a strong reason for selected 500 as the timeout?

File third_party/blink/web_tests/http/tests/media/autoplay/resources/test-autoplay.html
Line 38, Patchset 13 (Latest): await Promise.race([stateChange, timeout]);
Michael Wilson . unresolved

See other comment.

Open in Gerrit

Related details

Attention is currently required from:
  • Felipe Erias
Submit Requirements:
  • requirement satisfiedCode-Coverage
  • requirement is not satisfiedCode-Owners
  • requirement is not satisfiedCode-Review
  • requirement is not satisfiedNo-Unresolved-Comments
  • requirement is not satisfiedReview-Enforcement
Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. DiffyGerrit
Gerrit-MessageType: comment
Gerrit-Project: chromium/src
Gerrit-Branch: main
Gerrit-Change-Id: I79d1bb94f809b460a750b9c37aa2b0d9ab0289c1
Gerrit-Change-Number: 7266845
Gerrit-PatchSet: 13
Gerrit-Owner: Felipe Erias <felip...@igalia.com>
Gerrit-Reviewer: Felipe Erias <felip...@igalia.com>
Gerrit-Reviewer: Michael Wilson <mjwi...@chromium.org>
Gerrit-CC: Chromium Metrics Reviews <chromium-met...@google.com>
Gerrit-CC: Hongchan Choi <hong...@chromium.org>
Gerrit-CC: srirama chandra sekhar <srir...@samsung.com>
Gerrit-Attention: Felipe Erias <felip...@igalia.com>
Gerrit-Comment-Date: Thu, 15 Jan 2026 23:59:40 +0000
Gerrit-HasComments: Yes
Gerrit-Has-Labels: Yes
Comment-In-Reply-To: Michael Wilson <mjwi...@chromium.org>
Comment-In-Reply-To: Felipe Erias <felip...@igalia.com>
satisfied_requirement
unsatisfied_requirement
open
diffy

Felipe Erias (Gerrit)

unread,
Jan 18, 2026, 9:07:42 PM (2 days ago) Jan 18
to Hongchan Choi, Chromium Metrics Reviews, srirama chandra sekhar, AyeAye, Chromium LUCI CQ, chromium...@chromium.org, asvitkine...@chromium.org, blink-re...@chromium.org, feature-me...@chromium.org, eric.c...@apple.com, blink-revie...@chromium.org, blink-...@chromium.org, jmedle...@chromium.org, kinuko...@chromium.org
Attention needed from Hongchan Choi and Michael Wilson

Felipe Erias added 4 comments

Patchset-level comments
File-level comment, Patchset 14 (Latest):
Felipe Erias . resolved

Thank you for your feedback.

File third_party/blink/renderer/modules/webaudio/audio_context.cc
Line 1042, Patchset 13: // Flag will be cleared if resume() was called after this task was scheduled.

if (!suspended_by_user_) {
for (auto& resolver : resolvers) {
resolver->Resolve();
}
return;
}

// Stop rendering now.
if (destination()) {
SuspendRendering();
}

for (auto& resolver : resolvers) {
resolver->Resolve();
}
Michael Wilson . unresolved
Nit: we could combine the conditions, although it may be less clear:
```suggestion
if (suspended_by_user_ && destination()) {
SuspendRendering();
}
  for (auto& resolver : resolvers) {
resolver->Resolve();
}
```
Felipe Erias

Good suggestion. I have updated the code like this:

```cpp
// Stop rendering unless resume() was called after this task was scheduled.

if (suspended_by_user_ && destination()) {
SuspendRendering();
}
  for (auto& resolver : resolvers) {
resolver->Resolve();
}
```
File third_party/blink/web_tests/http/tests/media/autoplay/resources/autoplay-test.js
Line 79, Patchset 13: await Promise.race([stateChange, timeout]);
Michael Wilson . unresolved

This works logically, but I'm worried that using this pattern here and in test-autoplay.html will result in flakiness for the tests.

Gemini suggested that we could refactor the tests to take an expected result state, and await the state change if it is running. It suggested we could look at the result from runMediaTests to see if we should be blocked or not.

But, I'm not sure how robust that is. Do we have a strong reason for selected 500 as the timeout?

Felipe Erias

I have updated the test to await the state change (if one is forthcoming) so we don't rely on a fixed timeout:

```js
async function runWebAudioTest() {
const audioContext = new AudioContext();
  if (audioContext.state === 'running') {
results.webAudio = audioContext.state;
return;
}
  // When autoplay is allowed, we wait for the state transition to "running".
// When autoplay is blocked, no "statechange" event will be fired.
if (results.media?.name !== 'NotAllowedError') {
await new Promise(resolve => {
audioContext.onstatechange = resolve;
});
}
results.webAudio = audioContext.state;
}
```
File third_party/blink/web_tests/http/tests/media/autoplay/resources/test-autoplay.html
Line 38, Patchset 13: await Promise.race([stateChange, timeout]);
Michael Wilson . unresolved

See other comment.

Felipe Erias

Same.

Open in Gerrit

Related details

Attention is currently required from:
  • Hongchan Choi
  • Michael Wilson
Submit Requirements:
  • requirement satisfiedCode-Coverage
  • requirement is not satisfiedCode-Owners
  • requirement is not satisfiedCode-Review
  • requirement is not satisfiedNo-Unresolved-Comments
  • requirement is not satisfiedReview-Enforcement
Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. DiffyGerrit
Gerrit-MessageType: comment
Gerrit-Project: chromium/src
Gerrit-Branch: main
Gerrit-Change-Id: I79d1bb94f809b460a750b9c37aa2b0d9ab0289c1
Gerrit-Change-Number: 7266845
Gerrit-PatchSet: 14
Gerrit-Owner: Felipe Erias <felip...@igalia.com>
Gerrit-Reviewer: Felipe Erias <felip...@igalia.com>
Gerrit-Reviewer: Hongchan Choi <hong...@chromium.org>
Gerrit-Reviewer: Michael Wilson <mjwi...@chromium.org>
Gerrit-CC: Chromium Metrics Reviews <chromium-met...@google.com>
Gerrit-CC: srirama chandra sekhar <srir...@samsung.com>
Gerrit-Attention: Michael Wilson <mjwi...@chromium.org>
Gerrit-Attention: Hongchan Choi <hong...@chromium.org>
Gerrit-Comment-Date: Mon, 19 Jan 2026 02:07:10 +0000
satisfied_requirement
unsatisfied_requirement
open
diffy

Michael Wilson (Gerrit)

unread,
Jan 20, 2026, 3:18:34 PM (11 hours ago) Jan 20
to Felipe Erias, Hongchan Choi, Chromium Metrics Reviews, srirama chandra sekhar, AyeAye, Chromium LUCI CQ, chromium...@chromium.org, asvitkine...@chromium.org, blink-re...@chromium.org, feature-me...@chromium.org, eric.c...@apple.com, blink-revie...@chromium.org, blink-...@chromium.org, jmedle...@chromium.org, kinuko...@chromium.org
Attention needed from Felipe Erias and Hongchan Choi

Michael Wilson voted and added 4 comments

Votes added by Michael Wilson

Code-Review+1

4 comments

Patchset-level comments
File-level comment, Patchset 13:
Michael Wilson . resolved

LGTM overall, with a nit and some concern about potential test flakiness.

Michael Wilson

Everything LGTM now, thank you!

File third_party/blink/renderer/modules/webaudio/audio_context.cc
Line 1042, Patchset 13: // Flag will be cleared if resume() was called after this task was scheduled.
if (!suspended_by_user_) {
for (auto& resolver : resolvers) {
resolver->Resolve();
}
return;
}

// Stop rendering now.
if (destination()) {
SuspendRendering();
}

for (auto& resolver : resolvers) {
resolver->Resolve();
}
Michael Wilson . resolved
Nit: we could combine the conditions, although it may be less clear:
```suggestion
if (suspended_by_user_ && destination()) {
SuspendRendering();
}
  for (auto& resolver : resolvers) {
resolver->Resolve();
}
```
Felipe Erias

Good suggestion. I have updated the code like this:

```cpp
// Stop rendering unless resume() was called after this task was scheduled.
if (suspended_by_user_ && destination()) {
SuspendRendering();
}
  for (auto& resolver : resolvers) {
resolver->Resolve();
}
```
Michael Wilson

Done

File third_party/blink/web_tests/http/tests/media/autoplay/resources/autoplay-test.js
Line 79, Patchset 13: await Promise.race([stateChange, timeout]);
Michael Wilson . resolved

This works logically, but I'm worried that using this pattern here and in test-autoplay.html will result in flakiness for the tests.

Gemini suggested that we could refactor the tests to take an expected result state, and await the state change if it is running. It suggested we could look at the result from runMediaTests to see if we should be blocked or not.

But, I'm not sure how robust that is. Do we have a strong reason for selected 500 as the timeout?

Felipe Erias

I have updated the test to await the state change (if one is forthcoming) so we don't rely on a fixed timeout:

```js
async function runWebAudioTest() {
const audioContext = new AudioContext();
  if (audioContext.state === 'running') {
results.webAudio = audioContext.state;
return;
}
  // When autoplay is allowed, we wait for the state transition to "running".
// When autoplay is blocked, no "statechange" event will be fired.
if (results.media?.name !== 'NotAllowedError') {
await new Promise(resolve => {
audioContext.onstatechange = resolve;
});
}
results.webAudio = audioContext.state;
}
```
Michael Wilson

Done

File third_party/blink/web_tests/http/tests/media/autoplay/resources/test-autoplay.html
Line 38, Patchset 13: await Promise.race([stateChange, timeout]);
Michael Wilson . resolved

See other comment.

Felipe Erias

Same.

Michael Wilson

Done

Open in Gerrit

Related details

Attention is currently required from:
  • Felipe Erias
  • Hongchan Choi
Submit Requirements:
  • requirement satisfiedCode-Coverage
  • requirement is not satisfiedCode-Owners
  • requirement is not satisfiedCode-Review
  • requirement is not satisfiedNo-Unresolved-Comments
  • requirement is not satisfiedReview-Enforcement
Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. DiffyGerrit
Gerrit-MessageType: comment
Gerrit-Project: chromium/src
Gerrit-Branch: main
Gerrit-Change-Id: I79d1bb94f809b460a750b9c37aa2b0d9ab0289c1
Gerrit-Change-Number: 7266845
Gerrit-PatchSet: 14
Gerrit-Owner: Felipe Erias <felip...@igalia.com>
Gerrit-Reviewer: Felipe Erias <felip...@igalia.com>
Gerrit-Reviewer: Hongchan Choi <hong...@chromium.org>
Gerrit-Reviewer: Michael Wilson <mjwi...@chromium.org>
Gerrit-CC: Chromium Metrics Reviews <chromium-met...@google.com>
Gerrit-CC: srirama chandra sekhar <srir...@samsung.com>
Gerrit-Attention: Hongchan Choi <hong...@chromium.org>
Gerrit-Attention: Felipe Erias <felip...@igalia.com>
Gerrit-Comment-Date: Tue, 20 Jan 2026 20:18:19 +0000
satisfied_requirement
unsatisfied_requirement
open
diffy

Hongchan Choi (Gerrit)

unread,
Jan 20, 2026, 4:04:47 PM (10 hours ago) Jan 20
to Felipe Erias, Chromium Metrics Reviews, srirama chandra sekhar, AyeAye, Chromium LUCI CQ, chromium...@chromium.org, asvitkine...@chromium.org, blink-re...@chromium.org, feature-me...@chromium.org, eric.c...@apple.com, blink-revie...@chromium.org, blink-...@chromium.org, jmedle...@chromium.org, kinuko...@chromium.org
Attention needed from Felipe Erias

Hongchan Choi voted and added 1 comment

Votes added by Hongchan Choi

Code-Review+1

1 comment

Patchset-level comments
File-level comment, Patchset 14 (Latest):
Hongchan Choi . resolved

LGTM

Since this is landing with experimental status, it’s good to know it won't impact general users yet. Before we move toward a stable launch, we should send out a PSA to gather feedback and ensure developers are aware of the transition.

This was a complex set of async state changes. Thanks for your contribution!

Open in Gerrit

Related details

Attention is currently required from:
  • Felipe Erias
Submit Requirements:
    • requirement satisfiedCode-Coverage
    • requirement is not satisfiedCode-Owners
    • requirement satisfiedCode-Review
    • requirement is not satisfiedNo-Unresolved-Comments
    • requirement satisfiedReview-Enforcement
    Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. DiffyGerrit
    Gerrit-MessageType: comment
    Gerrit-Project: chromium/src
    Gerrit-Branch: main
    Gerrit-Change-Id: I79d1bb94f809b460a750b9c37aa2b0d9ab0289c1
    Gerrit-Change-Number: 7266845
    Gerrit-PatchSet: 14
    Gerrit-Owner: Felipe Erias <felip...@igalia.com>
    Gerrit-Reviewer: Felipe Erias <felip...@igalia.com>
    Gerrit-Reviewer: Hongchan Choi <hong...@chromium.org>
    Gerrit-Reviewer: Michael Wilson <mjwi...@chromium.org>
    Gerrit-CC: Chromium Metrics Reviews <chromium-met...@google.com>
    Gerrit-CC: srirama chandra sekhar <srir...@samsung.com>
    Gerrit-Attention: Felipe Erias <felip...@igalia.com>
    Gerrit-Comment-Date: Tue, 20 Jan 2026 21:04:37 +0000
    Gerrit-HasComments: Yes
    Gerrit-Has-Labels: Yes
    satisfied_requirement
    unsatisfied_requirement
    open
    diffy

    Felipe Erias (Gerrit)

    unread,
    Jan 20, 2026, 7:30:57 PM (7 hours ago) Jan 20
    to Rick Byers, Hongchan Choi, Chromium Metrics Reviews, srirama chandra sekhar, AyeAye, Chromium LUCI CQ, chromium...@chromium.org, asvitkine...@chromium.org, blink-re...@chromium.org, feature-me...@chromium.org, eric.c...@apple.com, blink-revie...@chromium.org, blink-...@chromium.org, jmedle...@chromium.org, kinuko...@chromium.org
    Attention needed from Rick Byers

    Felipe Erias added 2 comments

    Patchset-level comments
    Felipe Erias . resolved

    Thank you very much for help to get this CL ready.

    Adding Rick for approval of the changes to `runtime_enabled_features.json5`. Thanks!

    File third_party/blink/web_tests/http/tests/media/autoplay/document-user-activation-navigation-nogesture-expected.txt
    Line 1, Patchset 11:CONSOLE WARNING: The AudioContext was not allowed to start. It must be resumed (or created) after a user gesture on the page. https://developer.chrome.com/blog/autoplay/#web_audio
    Hongchan Choi . resolved

    Can you explain why this CONSOLE WARNING is shown here?

    Felipe Erias

    This was caused by calling `resume()` inside `test-autoplay.html` `runWebAudioTests()`, which is not needed.

    Felipe Erias

    Done

    Open in Gerrit

    Related details

    Attention is currently required from:
    • Rick Byers
    Submit Requirements:
      • requirement satisfiedCode-Coverage
      • requirement is not satisfiedCode-Owners
      • requirement satisfiedCode-Review
      • requirement satisfiedReview-Enforcement
      Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. DiffyGerrit
      Gerrit-MessageType: comment
      Gerrit-Project: chromium/src
      Gerrit-Branch: main
      Gerrit-Change-Id: I79d1bb94f809b460a750b9c37aa2b0d9ab0289c1
      Gerrit-Change-Number: 7266845
      Gerrit-PatchSet: 14
      Gerrit-Owner: Felipe Erias <felip...@igalia.com>
      Gerrit-Reviewer: Felipe Erias <felip...@igalia.com>
      Gerrit-Reviewer: Hongchan Choi <hong...@chromium.org>
      Gerrit-Reviewer: Michael Wilson <mjwi...@chromium.org>
      Gerrit-Reviewer: Rick Byers <rby...@chromium.org>
      Gerrit-CC: Chromium Metrics Reviews <chromium-met...@google.com>
      Gerrit-CC: srirama chandra sekhar <srir...@samsung.com>
      Gerrit-Attention: Rick Byers <rby...@chromium.org>
      Gerrit-Comment-Date: Wed, 21 Jan 2026 00:30:24 +0000
      Gerrit-HasComments: Yes
      Gerrit-Has-Labels: No
      satisfied_requirement
      unsatisfied_requirement
      open
      diffy
      Reply all
      Reply to author
      Forward
      0 new messages