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
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

View File

@ -211,6 +211,7 @@ private:
void set_show_poster(bool);
void set_paused(bool);
void set_duration(double);
void set_ended(bool);
void volume_or_muted_attribute_changed();
void update_volume();
@ -224,6 +225,7 @@ private:
PlaybackDirection direction_of_playback() const;
bool has_ended_playback() const;
void upon_has_ended_playback_possibly_changed();
void reached_end_of_media_playback();
void dispatch_time_update_event();
@ -291,6 +293,9 @@ 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-ended
bool m_ended { false };
// https://html.spec.whatwg.org/multipage/media.html#dom-media-defaultplaybackrate
double m_default_playback_rate { 1.0 };
@ -334,6 +339,8 @@ private:
GC::Ptr<VideoTrack> m_selected_video_track;
RefPtr<Media::DisplayingVideoSink> m_selected_video_track_sink;
bool m_loop_was_specified_when_reaching_end_of_media_resource { false };
// Cached state for layout.
Optional<MediaComponent> m_mouse_tracking_component;
Optional<MediaComponent> m_hovered_component;