diff --git a/Userland/Libraries/LibWeb/HTML/AudioTrack.cpp b/Userland/Libraries/LibWeb/HTML/AudioTrack.cpp
index 9f6db1f22f..59aa4fb9f0 100644
--- a/Userland/Libraries/LibWeb/HTML/AudioTrack.cpp
+++ b/Userland/Libraries/LibWeb/HTML/AudioTrack.cpp
@@ -100,6 +100,11 @@ void AudioTrack::seek(double position, MediaSeekMode seek_mode)
m_loader->seek(position).release_value_but_fixme_should_propagate_errors();
}
+void AudioTrack::update_volume()
+{
+ m_audio_plugin->set_volume(m_media_element->effective_media_volume());
+}
+
void AudioTrack::visit_edges(Cell::Visitor& visitor)
{
Base::visit_edges(visitor);
diff --git a/Userland/Libraries/LibWeb/HTML/AudioTrack.h b/Userland/Libraries/LibWeb/HTML/AudioTrack.h
index 844b579ad1..424e52cbe9 100644
--- a/Userland/Libraries/LibWeb/HTML/AudioTrack.h
+++ b/Userland/Libraries/LibWeb/HTML/AudioTrack.h
@@ -28,6 +28,8 @@ public:
Duration duration() const;
void seek(double, MediaSeekMode);
+ void update_volume();
+
String const& id() const { return m_id; }
String const& kind() const { return m_kind; }
String const& label() const { return m_label; }
diff --git a/Userland/Libraries/LibWeb/HTML/HTMLAudioElement.cpp b/Userland/Libraries/LibWeb/HTML/HTMLAudioElement.cpp
index b2bb0ba0a2..f006b07f27 100644
--- a/Userland/Libraries/LibWeb/HTML/HTMLAudioElement.cpp
+++ b/Userland/Libraries/LibWeb/HTML/HTMLAudioElement.cpp
@@ -63,4 +63,11 @@ void HTMLAudioElement::on_seek(double position, MediaSeekMode seek_mode)
});
}
+void HTMLAudioElement::on_volume_change()
+{
+ audio_tracks()->for_each_enabled_track([&](auto& audio_track) {
+ audio_track.update_volume();
+ });
+}
+
}
diff --git a/Userland/Libraries/LibWeb/HTML/HTMLAudioElement.h b/Userland/Libraries/LibWeb/HTML/HTMLAudioElement.h
index 08d0c92153..e08554afd4 100644
--- a/Userland/Libraries/LibWeb/HTML/HTMLAudioElement.h
+++ b/Userland/Libraries/LibWeb/HTML/HTMLAudioElement.h
@@ -29,6 +29,7 @@ private:
virtual void on_playing() override;
virtual void on_paused() override;
virtual void on_seek(double, MediaSeekMode) override;
+ virtual void on_volume_change() override;
};
}
diff --git a/Userland/Libraries/LibWeb/HTML/HTMLMediaElement.cpp b/Userland/Libraries/LibWeb/HTML/HTMLMediaElement.cpp
index 2e9e5b3534..b8168dc04f 100644
--- a/Userland/Libraries/LibWeb/HTML/HTMLMediaElement.cpp
+++ b/Userland/Libraries/LibWeb/HTML/HTMLMediaElement.cpp
@@ -92,6 +92,8 @@ void HTMLMediaElement::parse_attribute(DeprecatedFlyString const& name, Deprecat
load_element().release_value_but_fixme_should_propagate_errors();
else if (name == HTML::AttributeNames::crossorigin)
m_crossorigin = cors_setting_attribute_from_keyword(String::from_deprecated_string(value).release_value_but_fixme_should_propagate_errors());
+ else if (name == HTML::AttributeNames::muted)
+ set_muted(true);
}
void HTMLMediaElement::did_remove_attribute(DeprecatedFlyString const& name)
@@ -344,6 +346,69 @@ WebIDL::ExceptionOr HTMLMediaElement::pause()
return {};
}
+// https://html.spec.whatwg.org/multipage/media.html#dom-media-volume
+WebIDL::ExceptionOr HTMLMediaElement::set_volume(double volume)
+{
+ if (m_volume == volume)
+ return {};
+
+ // On setting, if the new value is in the range 0.0 to 1.0 inclusive, the media element's playback volume must be
+ // set to the new value. If the new value is outside the range 0.0 to 1.0 inclusive, then, on setting, an
+ // "IndexSizeError" DOMException must be thrown instead.
+ if (volume < 0.0 || volume > 1.0)
+ return WebIDL::IndexSizeError::create(realm(), "Volume must be in the range 0.0 to 1.0, inclusive"sv);
+
+ m_volume = volume;
+ volume_or_muted_attribute_changed();
+
+ return {};
+}
+
+// https://html.spec.whatwg.org/multipage/media.html#dom-media-muted
+void HTMLMediaElement::set_muted(bool muted)
+{
+ if (m_muted == muted)
+ return;
+
+ m_muted = muted;
+ volume_or_muted_attribute_changed();
+}
+
+// https://html.spec.whatwg.org/multipage/media.html#user-interface:dom-media-volume-3
+void HTMLMediaElement::volume_or_muted_attribute_changed()
+{
+ // Whenever either of the values that would be returned by the volume and muted IDL attributes change, the user
+ // agent must queue a media element task given the media element to fire an event named volumechange at the media
+ // element.
+ queue_a_media_element_task([this] {
+ dispatch_event(DOM::Event::create(realm(), HTML::EventNames::volumechange).release_value_but_fixme_should_propagate_errors());
+ });
+
+ // FIXME: Then, if the media element is not allowed to play, the user agent must run the internal pause steps for the media element.
+
+ on_volume_change();
+}
+
+// https://html.spec.whatwg.org/multipage/media.html#effective-media-volume
+double HTMLMediaElement::effective_media_volume() const
+{
+ // FIXME 1. If the user has indicated that the user agent is to override the volume of the element, then return the
+ // volume desired by the user.
+
+ // 2. If the element's audio output is muted, then return zero.
+ if (m_muted)
+ return 0.0;
+
+ // 3. Let volume be the playback volume of the audio portions of the media element, in range 0.0 (silent) to
+ // 1.0 (loudest).
+ auto volume = clamp(m_volume, 0.0, 1.0);
+
+ // 4. Return volume, interpreted relative to the range 0.0 to 1.0, with 0.0 being silent, and 1.0 being the loudest
+ // setting, values in between increasing in loudness. The range need not be linear. The loudest setting may be
+ // lower than the system's loudest possible setting; for example the user could have set a maximum volume.
+ return volume;
+}
+
// https://html.spec.whatwg.org/multipage/media.html#media-element-load-algorithm
WebIDL::ExceptionOr HTMLMediaElement::load_element()
{
diff --git a/Userland/Libraries/LibWeb/HTML/HTMLMediaElement.h b/Userland/Libraries/LibWeb/HTML/HTMLMediaElement.h
index 6ecf072bd6..74ec7c65fb 100644
--- a/Userland/Libraries/LibWeb/HTML/HTMLMediaElement.h
+++ b/Userland/Libraries/LibWeb/HTML/HTMLMediaElement.h
@@ -85,6 +85,14 @@ public:
WebIDL::ExceptionOr> play();
WebIDL::ExceptionOr pause();
+ double volume() const { return m_volume; }
+ WebIDL::ExceptionOr set_volume(double);
+
+ bool muted() const { return m_muted; }
+ void set_muted(bool);
+
+ double effective_media_volume() const;
+
JS::NonnullGCPtr audio_tracks() const { return *m_audio_tracks; }
JS::NonnullGCPtr video_tracks() const { return *m_video_tracks; }
@@ -118,6 +126,8 @@ protected:
// subclasses must invoke set_current_playback_position() to unblock the user agent.
virtual void on_seek(double, MediaSeekMode) { m_seek_in_progress = false; }
+ virtual void on_volume_change() { }
+
private:
friend SourceElementSelector;
@@ -142,6 +152,8 @@ private:
void set_paused(bool);
void set_duration(double);
+ void volume_or_muted_attribute_changed();
+
bool blocked() const;
bool is_eligible_for_autoplay() const;
bool has_ended_playback() const;
@@ -212,6 +224,12 @@ private:
// https://html.spec.whatwg.org/multipage/media.html#dom-media-paused
bool m_paused { true };
+ // https://html.spec.whatwg.org/multipage/media.html#dom-media-volume
+ double m_volume { 1.0 };
+
+ // https://html.spec.whatwg.org/multipage/media.html#dom-media-muted
+ bool m_muted { false };
+
// https://html.spec.whatwg.org/multipage/media.html#dom-media-audiotracks
JS::GCPtr m_audio_tracks;
diff --git a/Userland/Libraries/LibWeb/HTML/HTMLMediaElement.idl b/Userland/Libraries/LibWeb/HTML/HTMLMediaElement.idl
index 6f6806ab67..8f3a869c11 100644
--- a/Userland/Libraries/LibWeb/HTML/HTMLMediaElement.idl
+++ b/Userland/Libraries/LibWeb/HTML/HTMLMediaElement.idl
@@ -52,6 +52,9 @@ interface HTMLMediaElement : HTMLElement {
// controls
[Reflect, CEReactions] attribute boolean controls;
+ attribute double volume;
+ attribute boolean muted;
+ [Reflect=muted, CEReactions] attribute boolean defaultMuted;
// tracks
[SameObject] readonly attribute AudioTrackList audioTracks;