Skip to content

Make muted content attribute affect muted IDL attribute only once - #12473

Open
foolip wants to merge 13 commits into
mainfrom
foolip/muted-once
Open

foolip wants to merge 13 commits into
mainfrom
foolip/muted-once

Conversation

@foolip

@foolip foolip commented May 21, 2026 •

Copy link
Copy Markdown
Member

The purpose of this is to limit the cases where the muted IDL attribute
can change without a volumechange event firing.

The notion that user agents may remember volume and muted is removed as
it would require additional muted states to handle correctly, but could
be reinstated if there is implementer interest.

  • At least two implementers are interested (and none opposed):
  • Tests are written and can be reviewed and commented upon at:
  • Implementation bugs are filed:
    • Chromium: …
    • Gecko: …
    • WebKit: …
    • Deno (only for timers, structured clone, base64 utils, channel messaging, module resolution, web workers, and web storage): …
    • Node.js (only for timers, structured clone, base64 utils, channel messaging, and module resolution): …
  • Corresponding HTML AAM & ARIA in HTML issues & PRs:
  • MDN issue is filed: …
  • The top of this comment includes a clear commit message to use.

(See WHATWG Working Mode: Changes for more details.)


/media.html ( diff )

@annevk

annevk commented May 21, 2026

Copy link
Copy Markdown
Member

What do implementations do today? I'm pretty sure WebKit doesn't have a "set once" special case.

@foolip

foolip commented May 21, 2026

Copy link
Copy Markdown
Member Author

Chromium reacts to muted content attribute changes but only for AttributeModificationReason::kByParser which can only happen once in HTMLMediaElement::ParseAttribute().

Gecko seems to look at the muted content attribute in two places: in HTMLMediaElement::DoneCreatingElement() and in HTMLMediaElement::CopyInnerTo(), which maybe is for cloning, not sure.

WebKit has a tri-state optional bool m_implicitlyMuted and a boolean m_explicitlyMuted. I'm not sure about m_implicitlyMuted, but m_explicitlyMuted is used as a once-only state bit when setting m_muted from the attribute in two places: in MediaElement::postConnectionSteps() and in MediaElement::loadResource().

@foolip

foolip commented May 21, 2026 •

Copy link
Copy Markdown
Member Author

For Gecko, the MutedReasons is also interesting and confirms that playbackRate can mute in Gecko. Seems like that doesn't fire a "volumechange" event.

@foolip

foolip commented May 21, 2026

Copy link
Copy Markdown
Member Author

cc @whatwg/media for opinions on this. In short, the topic is how/when the muted content attribute affects the muted IDL attribute, and which changes in the muted IDL attribute should fire a volumechange event.

Recent changes here are #12389 and #12428.

@annevk

annevk commented May 24, 2026 •

Copy link
Copy Markdown
Member

I see, perhaps observing the muted content attribute in the post-connection steps is a good alternative to the fictional "creation time". As that is a well-defined point in time.

That would require changing the :muted tests again I suspect.

@foolip
foolip force-pushed the foolip/muted-once branch from 249e5dc to f6b7b0e Compare May 25, 2026 21:16
The purpose of this is to limit the cases where the muted IDL attribute
can change without a volumechange event firing.

The notion that user agents may remember volume and muted is removed as
it would require additional muted states to handle correctly, but could
be reinstated if there is implementer interest.
@foolip
foolip force-pushed the foolip/muted-once branch from f6b7b0e to 295bb37 Compare May 25, 2026 21:19
@foolip

foolip commented May 25, 2026

Copy link
Copy Markdown
Member Author

@annevk that works for me, I've updated this PR and tests in https://crrev.com/c/7857861 to match.

@foolip foolip added the agenda+ To be discussed at a triage meeting label May 26, 2026

@annevk annevk left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about the WebKit case of taking the muted content attribute into account when loading a new media resource?

Comment thread source Resolved
Comment thread source Resolved
Comment thread source Resolved
Comment thread source Resolved
Comment thread source Resolved
@foolip
foolip force-pushed the foolip/muted-once branch from 6cccc77 to 025c4cf Compare June 4, 2026 08:14
Comment thread source Resolved
Comment thread source Resolved
Comment thread source Resolved
Comment thread source Resolved
@jstenback jstenback removed the agenda+ To be discussed at a triage meeting label Jun 24, 2026
webkit-commit-queue pushed a commit to jernoble/WebKit that referenced this pull request Aug 17, 2026
rdar://184148295
https://bugs.webkit.org/show_bug.cgi?id=321113

Reviewed by Jean-Yves Avenard.

The "Test :muted matches .muted for content attribute" sub-test tries to validate that setting
the "muted" content attribute correctly reflects to the .muted DOM property as well as the :muted
pseudo selector. However, the behavior of the "muted" content attribute is such that it does not
universally reflect to the DOM and CSS; specifically, all three UAs will collapse the "default"
mute state to an explicit value at different points. There is a proposal in whatwg/html/pull/12473
to make this collapse point explicit: either during the load algorithm or during the post connection
steps.

Modify the sound-state.html test to validate that the :muted CSS pseudo selector matches the .muted
IDL attribute by splitting the existing content attribute test into three separate tests. This-
ensures that the desired functionality (that the IDL and CSS match) is equally well tested both before
the proposed post-connection change is merged and after.

* LayoutTests/imported/w3c/web-platform-tests/css/selectors/media/sound-state-expected.txt:
* LayoutTests/imported/w3c/web-platform-tests/css/selectors/media/sound-state.html:

Canonical link: https://commits.webkit.org/319299@main
@zcorpan

zcorpan commented Aug 27, 2026

Copy link
Copy Markdown
Member

Summary of Opus + Codex review:

1. Attribute order in the markup becomes observable

The parser appends a token's attributes one at a time, and each src append reaches the resource selection algorithm synchronously via the src attribute change steps and the media element load algorithm. The new RSA step therefore resolves the muted state before muted has been appended:

<video muted src=x></video>   <!-- RSA sees muted   → muted state true -->
<video src=x muted></video>   <!-- RSA runs first   → muted state false -->

The post-connection steps do not rescue the second case: by the time the element is inserted the state is already false, so the hook does nothing. The same applies to script:

const v = document.createElement("video");
v.src = x;                    // resource selection algorithm runs here, no muted attribute yet
v.setAttribute("muted", "");  // too late
document.body.append(v);      // post-connection steps: state is already false
v.muted;                      // false

<video src="…" autoplay muted> is the more common authoring order, and on main neither order matters, because the old bullet was evaluated at read time. This is the same shape as #12854.

2. play() consults the state before the resource selection algorithm can resolve it

play() step 1 rejects if the element is not allowed to play. The resource selection algorithm is only invoked later, by internal play steps step 1. Since the spec's own example ties the autoplay-policy exception to a muted element:

const v = document.createElement("video");
v.setAttribute("muted", "");
await v.play();   // step 1: muted state is still "initial", so the element is not muted
                  // and an autoplay policy can reject before RSA resolves it

3. Detached, never-loaded elements stay "initial"

Nothing triggers a resource selection, so the content attribute never takes effect, and defaultMuted and muted disagree:

const v = document.createElement("video");
v.defaultMuted = true;
v.muted;              // false
v.matches(":muted");  // false
const d = document.createElement("div");
d.innerHTML = "<video muted></video>";  // no src, so no resource selection
d.firstChild.muted;                     // false

Also <template><video muted></video></template>, DOMParser output, and detached clones. This is @smaug----'s earlier question; the RSA read only closes it when something starts a resource selection.

4. The post-connection hook does not cover the case its note describes

The note on the new post-connection steps says:

This is to avoid firing the event for parser-inserted media elements with the muted content attribute

...and for a parser-inserted media element that has src, those steps never do any work, because the RSA step already resolved the state during attribute append, before the element was inserted:

<!-- resolved by the RSA step during attribute append, before insertion;
     the post-connection steps then find the state already resolved -->
<video muted src=x></video>

<!-- no src at attribute-append time, so the post-connection steps are what resolve it -->
<video muted><source src=x></video>
<video muted></video>

So the hook only does work for media elements whose resource comes from source children, or that have no resource at all. If that note is the justification for having the hook, it should describe those cases instead.

On the fix

Moving the RSA step to after "await a stable state" fixes 1, because every case there is intra-task: the parser appends all of the token's attributes and inserts the element before the event loop reaches a stable state, and so do innerHTML and v.src = x; v.setAttribute("muted", ""). It would need to be the first ⌛ step, since the "no media provider object, no src, no source child" branch ends the synchronous section and returns.

But it makes 2 worse rather than better, since play() step 1 runs in the current task, and it introduces an eventless flip: muted and :muted would read false immediately after v.src = x and become true at the stable state, with no volumechange.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Development

Successfully merging this pull request may close these issues.

5 participants