LibWeb: Consider playback ended when loop is set after ending playback

This allows playback to restart when playing is requested after the end
of playback was reached while loop was disabled, regardless of whether
loop is then subsequently enabled.

This matches other browsers' implementations, but differs from the spec
in how the ended attribute is handled.

See: https://github.com/whatwg/html/issues/11775
This commit is contained in:
Zaggy1024 2025-10-09 16:55:03 -05:00 committed by Jelle Raaijmakers
parent 3be6b957f8
commit 4471e8c0ec
Notes: github-actions[bot] 2025-10-28 00:31:04 +00:00
2 changed files with 37 additions and 4 deletions

View file

@ -314,6 +314,8 @@ void HTMLMediaElement::set_current_playback_position(double playback_position)
// which these steps should be invoked, which is when we've reached the end of the media playback.
if (m_current_playback_position == m_duration)
reached_end_of_media_playback();
upon_has_ended_playback_possibly_changed();
}
// https://html.spec.whatwg.org/multipage/media.html#dom-media-duration
@ -332,8 +334,10 @@ bool HTMLMediaElement::ended() const
{
// The ended attribute must return true if, the last time the event loop reached step 1, the media element had ended
// playback and the direction of playback was forwards, and false otherwise.
// FIXME: Add a hook into EventLoop::process() to be notified when step 1 is reached.
return has_ended_playback() && direction_of_playback() == PlaybackDirection::Forwards;
// NOTE: We queue a task to set this at event loop step 1 whenever something happens that may affect the resulting value.
// Currently, that is when the ready state changes, when the current playback position changes, or the duration
// changes.
return m_ended;
}
// https://html.spec.whatwg.org/multipage/media.html#durationChange
@ -354,6 +358,8 @@ void HTMLMediaElement::set_duration(double duration)
m_duration = duration;
upon_has_ended_playback_possibly_changed();
if (auto* paintable = this->paintable())
paintable->set_needs_display();
}
@ -1456,6 +1462,7 @@ void HTMLMediaElement::set_ready_state(ReadyState ready_state)
{
ScopeGuard guard { [&] {
m_ready_state = ready_state;
upon_has_ended_playback_possibly_changed();
set_needs_style_update(true);
} };
@ -1863,6 +1870,11 @@ void HTMLMediaElement::set_paused(bool paused)
set_needs_style_update(true);
}
void HTMLMediaElement::set_ended(bool ended)
{
m_ended = ended;
}
// https://html.spec.whatwg.org/multipage/media.html#dom-media-defaultplaybackrate
void HTMLMediaElement::set_default_playback_rate(double new_value)
{
@ -1984,7 +1996,11 @@ bool HTMLMediaElement::has_ended_playback() const
direction_of_playback() == PlaybackDirection::Forwards &&
// The media element does not have a loop attribute specified.
!has_attribute(HTML::AttributeNames::loop)) {
// AD-HOC: Use the value of the loop attribute from the last time we reached end of playback.
// Without this change, the ended attribute changes when enabling the loop attribute after
// playback has ended, and playback will not restart when playing the element.
// See https://github.com/whatwg/html/issues/11775
!m_loop_was_specified_when_reaching_end_of_media_resource) {
return true;
}
@ -2001,11 +2017,21 @@ bool HTMLMediaElement::has_ended_playback() const
return false;
}
void HTMLMediaElement::upon_has_ended_playback_possibly_changed()
{
run_when_event_loop_reaches_step_1(GC::Function<void()>::create(heap(), [&] {
// The ended attribute must return true if, the last time the event loop reached step 1, the media element had ended
// playback and the direction of playback was forwards, and false otherwise.
set_ended(has_ended_playback() && direction_of_playback() == PlaybackDirection::Forwards);
}));
}
// https://html.spec.whatwg.org/multipage/media.html#reaches-the-end
void HTMLMediaElement::reached_end_of_media_playback()
{
// 1. If the media element has a loop attribute specified,
if (has_attribute(HTML::AttributeNames::loop)) {
m_loop_was_specified_when_reaching_end_of_media_resource = has_attribute(HTML::AttributeNames::loop);
if (m_loop_was_specified_when_reaching_end_of_media_resource) {
// then seek to the earliest possible position of the media resource and return.
seek_element(0);
// FIXME: Tell PlaybackManager that we're looping to allow data providers to decode frames ahead when looping