LibMedia+LibWeb: End media element playback based on the pipeline EOS

Instead of comparing the current time to the duration, the playback
manager now has an explicit Ended state that jumps to the duration. The
element simply reacts to that to trigger the ended event and attribute,
along with all the other steps involved.

This moves the ended event to fire after the seeked event, which
matches other browsers' behavior. The spec doesn't explicitly say which
order they should fire in.
This commit is contained in:
Zaggy1024 2026-06-09 17:44:01 -05:00 committed by Gregory Bertilson
parent af9ab3b3fa
commit 9e2a820884
9 changed files with 88 additions and 10 deletions

View file

@ -7,11 +7,17 @@
#include "BufferingStateHandler.h"
#include <LibMedia/PlaybackManager.h>
#include <LibMedia/PlaybackStates/EndedStateHandler.h>
namespace Media {
void BufferingStateHandler::on_pipeline_status_changed(PipelineStatus status)
{
if (status == PipelineStatus::EndOfStream) {
manager().replace_state_handler<EndedStateHandler>();
return;
}
if (status != PipelineStatus::Blocked)
resume();
}

View file

@ -0,0 +1,51 @@
/*
* Copyright (c) 2026-present, the Ladybird developers.
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <LibMedia/PlaybackManager.h>
namespace Media {
class EndedStateHandler final : public PlaybackStateHandler {
public:
EndedStateHandler(PlaybackManager& manager)
: PlaybackStateHandler(manager)
{
}
virtual ~EndedStateHandler() override = default;
virtual void on_enter() override
{
manager().m_time_provider->pause();
}
virtual void on_exit() override { }
virtual AK::Duration current_time() const override
{
return manager().duration();
}
virtual void play() override { }
virtual void pause() override { }
virtual bool is_playing() override
{
return false;
}
virtual PlaybackState state() override
{
return PlaybackState::Ended;
}
virtual AvailableData available_data() override
{
return AvailableData::Current;
}
virtual void on_pipeline_status_changed(PipelineStatus) override { }
};
}

View file

@ -15,7 +15,8 @@
X(PlayingStateHandler) \
X(PausedStateHandler) \
X(ResumingStateHandler) \
X(SeekingStateHandler)
X(SeekingStateHandler) \
X(EndedStateHandler)
namespace Media {

View file

@ -17,6 +17,7 @@ enum class PlaybackState : u8 {
Playing,
Paused,
Seeking,
Ended,
};
constexpr StringView playback_state_to_string(PlaybackState state)
@ -32,6 +33,8 @@ constexpr StringView playback_state_to_string(PlaybackState state)
return "Paused"sv;
case PlaybackState::Seeking:
return "Seeking"sv;
case PlaybackState::Ended:
return "Ended"sv;
}
return "Invalid"sv;
}

View file

@ -5,6 +5,7 @@
*/
#include <LibMedia/PlaybackManager.h>
#include <LibMedia/PlaybackStates/EndedStateHandler.h>
#include <LibMedia/PlaybackStates/SeekingStateHandler.h>
#include "PlaybackStateHandler.h"
@ -23,7 +24,8 @@ void PlaybackStateHandler::seek(AK::Duration timestamp, SeekMode mode)
void PlaybackStateHandler::on_pipeline_status_changed(PipelineStatus status)
{
(void)status;
if (status == PipelineStatus::EndOfStream)
manager().replace_state_handler<EndedStateHandler>();
}
}

View file

@ -8,6 +8,7 @@
#include <LibMedia/PlaybackManager.h>
#include <LibMedia/PlaybackStates/BufferingStateHandler.h>
#include <LibMedia/PlaybackStates/EndedStateHandler.h>
#include <LibMedia/PlaybackStates/PausedStateHandler.h>
namespace Media {
@ -19,8 +20,13 @@ void PlayingStateHandler::pause()
void PlayingStateHandler::on_pipeline_status_changed(PipelineStatus status)
{
if (status == PipelineStatus::Blocked)
if (status == PipelineStatus::Blocked) {
manager().replace_state_handler<BufferingStateHandler>(true);
return;
}
if (status == PipelineStatus::EndOfStream)
manager().replace_state_handler<EndedStateHandler>();
}
}

View file

@ -60,6 +60,11 @@ public:
if (!resolves_seek(status))
return;
if (status == PipelineStatus::EndOfStream) {
PlaybackStateHandler::on_pipeline_status_changed(status);
return;
}
resume();
}

View file

@ -459,11 +459,6 @@ void HTMLMediaElement::set_current_playback_position(double playback_position)
time_marches_on();
// NOTE: Invoking the following steps is not listed in the spec. Rather, the spec just describes the scenario in
// 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();
update_natural_dimensions();
@ -2043,6 +2038,8 @@ void HTMLMediaElement::forget_media_resource_specific_tracks()
// of text tracks all the media-resource-specific text tracks, then empty the media element's audioTracks attribute's AudioTrackList object, then
// empty the media element's videoTracks attribute's VideoTrackList object. No events (in particular, no removetrack events) are fired as part of
// this; the error and emptied events, fired by the algorithms that invoke this one, can be used instead.
if (m_playback_manager)
m_playback_manager->on_playback_state_change = nullptr;
m_audio_tracks->remove_all_tracks();
m_video_tracks->remove_all_tracks();
m_playback_manager.clear();
@ -2269,6 +2266,10 @@ void HTMLMediaElement::on_playback_manager_state_change()
auto state = m_playback_manager->state();
if (seeking() && state != Media::PlaybackState::Seeking)
finish_seeking_element();
if (state == Media::PlaybackState::Ended && !m_error) {
set_current_playback_position(m_duration);
reached_end_of_media_playback();
}
// NB: Queue the readyState update as a task so that it will never run before the durationchange and loadedmetadata
// events are fired. This ensures that readyState has a deterministic value in those events.
@ -2699,10 +2700,13 @@ bool HTMLMediaElement::has_ended_playback() const
if (m_ready_state < ReadyState::HaveMetadata)
return false;
VERIFY(m_playback_manager != nullptr);
// Either:
if (
// The current playback position is the end of the media resource, and
m_current_playback_position == m_duration &&
// NB: This is represented by the playback manager's Ended state, which is only entered once the pipeline has
// consumed all real media data.
m_playback_manager->state() == Media::PlaybackState::Ended &&
// The direction of playback is forwards, and
direction_of_playback() == PlaybackDirection::Forwards &&

View file

@ -8,5 +8,5 @@ resize: videoWidth=320 videoHeight=240
seeked: videoWidth=320 videoHeight=240
--- seek to end ---
resize: videoWidth=640 videoHeight=480
ended: videoWidth=640 videoHeight=480
seeked: videoWidth=640 videoHeight=480
ended: videoWidth=640 videoHeight=480