LibWeb: Implement SourceBuffer capacity and pre-append eviction

Set a limit on SourceBuffer size to 150MiB for video and 12MiB for
audio, matching Chromium's limits.
This commit is contained in:
Zaggy1024 2026-05-23 06:30:40 -05:00 committed by Gregory Bertilson
parent f33702d4f8
commit 289908968d
6 changed files with 219 additions and 12 deletions

View file

@ -271,7 +271,7 @@ WebIDL::ExceptionOr<void> SourceBuffer::set_mode(Bindings::AppendMode mode)
}
// https://w3c.github.io/media-source/#sourcebuffer-prepare-append
WebIDL::ExceptionOr<void> SourceBuffer::prepare_append()
WebIDL::ExceptionOr<void> SourceBuffer::prepare_append(size_t new_data_size, AK::Duration current_time)
{
// FIXME: Support MediaSourceExtensions in workers.
if (!m_media_source->media_element_assigned_to())
@ -314,7 +314,7 @@ WebIDL::ExceptionOr<void> SourceBuffer::prepare_append()
}
// 6. Run the coded frame eviction algorithm.
m_processor->run_coded_frame_eviction();
m_processor->run_coded_frame_eviction(new_data_size, current_time);
// 7. If the [[buffer full flag]] equals true, then throw a QuotaExceededError exception and abort these steps.
if (m_processor->is_buffer_full())
@ -327,7 +327,7 @@ WebIDL::ExceptionOr<void> SourceBuffer::prepare_append()
WebIDL::ExceptionOr<void> SourceBuffer::append_buffer(GC::Root<WebIDL::BufferSource> const& data)
{
// 1. Run the prepare append algorithm.
TRY(prepare_append());
TRY(prepare_append(data->byte_length(), m_media_source->media_element_assigned_to()->playback_manager().current_time()));
// 2. Add data to the end of the [[input buffer]].
if (auto array_buffer = data->viewed_array_buffer(); array_buffer && !array_buffer->is_detached()) {

View file

@ -70,7 +70,7 @@ protected:
virtual void visit_edges(Cell::Visitor&) override;
private:
WebIDL::ExceptionOr<void> prepare_append();
WebIDL::ExceptionOr<void> prepare_append(size_t new_data_size, AK::Duration current_time);
void run_buffer_append_algorithm();
void run_append_error_algorithm();
void on_first_initialization_segment_processed(InitializationSegmentData const&);

View file

@ -52,6 +52,40 @@ bool SourceBufferProcessor::is_buffer_full() const
return m_buffer_full_flag;
}
static constexpr size_t AUDIO_TRACK_BYTE_CAPACITY = 12 * MiB;
static constexpr size_t VIDEO_TRACK_BYTE_CAPACITY = 150 * MiB;
static constexpr size_t TEXT_TRACK_BYTE_CAPACITY = 1 * MiB;
size_t SourceBufferProcessor::total_buffered_bytes() const
{
size_t total = 0;
for (auto const& [track_id, track_buffer] : m_track_buffers)
total += track_buffer->demuxer().total_bytes();
return total;
}
size_t SourceBufferProcessor::capacity_in_bytes() const
{
VERIFY(!m_track_buffers.is_empty());
size_t total = 0;
for (auto const& [track_id, track_buffer] : m_track_buffers) {
switch (track_buffer->demuxer().track().type()) {
case Media::TrackType::Audio:
total += AUDIO_TRACK_BYTE_CAPACITY;
break;
case Media::TrackType::Video:
total += VIDEO_TRACK_BYTE_CAPACITY;
break;
case Media::TrackType::Subtitles:
total += TEXT_TRACK_BYTE_CAPACITY;
break;
case Media::TrackType::Unknown:
break;
}
}
return total;
}
void SourceBufferProcessor::set_mode(AppendMode mode)
{
m_mode = mode;
@ -524,14 +558,111 @@ void SourceBufferProcessor::run_coded_frame_processing(Vector<DemuxedCodedFrame>
}
// https://w3c.github.io/media-source/#sourcebuffer-coded-frame-eviction
void SourceBufferProcessor::run_coded_frame_eviction()
void SourceBufferProcessor::run_coded_frame_eviction(size_t new_data_size, AK::Duration current_time)
{
// FIXME: 1. Let new data equal the data that is about to be appended to this SourceBuffer.
// 2. If the [[buffer full flag]] equals false, then abort these steps.
// 3. Let removal ranges equal a list of presentation time ranges that can be evicted from the presentation
// to make room for the new data.
// 4. For each range in removal ranges, run the coded frame removal algorithm with start and end equal to
// the removal range start and end timestamp respectively.
// https://w3c.github.io/media-source/#dfn-coded-frame-removal
// AD-HOC: We'll run some of the final steps from the coded frame removal algorithm below.
// We explicitly do not remove dependencies here (step 4), since that may evict data ahead of what the
// demuxer needs still for its clients.
// We also skip the ready state change here (step 5), since we explicitly avoid evicting current data.
// NB: Before the first initialization segment has been parsed, no track buffers exist and there is nothing to
// evict.
// AD-HOC: Set the buffer full flag based on the projected size of the buffer after appending the new data.
// If we don't do so here, we will not trigger removal until after an append throws a QuotaExceededError.
// https://github.com/w3c/media-source/issues/289
if (m_track_buffers.is_empty())
return;
auto current_bytes = total_buffered_bytes();
auto current_capacity_in_bytes = capacity_in_bytes();
auto consumed_bytes_after_append = current_bytes + new_data_size;
m_buffer_full_flag = consumed_bytes_after_append > current_capacity_in_bytes;
// 1. Let new data equal the data that is about to be appended to this SourceBuffer.
// 2. If the [[buffer full flag]] equals false, then abort these steps.
if (!m_buffer_full_flag)
return;
// 3. Let removal ranges equal a list of presentation time ranges that can be evicted from the presentation to make
// room for the new data.
// 4. For each range in removal ranges, run the coded frame removal algorithm with start and end equal to the
// removal range start and end timestamp respectively.
// AD-HOC: Steps 3 and 4 are completed together by removing frames relative to the current playback position. We
// first evict frames strictly before current_time across all tracks (oldest first), and if more room is
// needed, evict frames beyond the buffered segment containing current_time (latest first). The per-track
// demuxer enforces the "don't break the currently-playing segment" boundary so seeks naturally preserve
// data around the new position.
VERIFY(consumed_bytes_after_append > current_capacity_in_bytes);
auto bytes_to_evict = consumed_bytes_after_append - current_capacity_in_bytes;
size_t bytes_evicted = 0;
while (bytes_evicted < bytes_to_evict) {
TrackBuffer* oldest_track_buffer = nullptr;
AK::Duration oldest_timestamp;
for (auto& [track_id, track_buffer] : m_track_buffers) {
auto candidate = track_buffer->demuxer().earliest_evictable_frame_timestamp(current_time);
if (!candidate.has_value())
continue;
if (!oldest_track_buffer || candidate.value() < oldest_timestamp) {
oldest_track_buffer = track_buffer.ptr();
oldest_timestamp = candidate.value();
}
}
if (!oldest_track_buffer)
break;
bytes_evicted += oldest_track_buffer->demuxer().take_earliest_frame();
}
while (bytes_evicted < bytes_to_evict) {
TrackBuffer* latest_track_buffer = nullptr;
AK::Duration latest_timestamp;
for (auto& [track_id, track_buffer] : m_track_buffers) {
auto candidate = track_buffer->demuxer().latest_evictable_frame_timestamp(current_time);
if (!candidate.has_value())
continue;
if (!latest_track_buffer || candidate.value() > latest_timestamp) {
latest_track_buffer = track_buffer.ptr();
latest_timestamp = candidate.value();
}
}
if (!latest_track_buffer)
break;
auto last_decode_timestamp = latest_track_buffer->last_decode_timestamp();
bytes_evicted += latest_track_buffer->demuxer().take_latest_frame();
// https://w3c.github.io/media-source/#dfn-coded-frame-removal
// AD-HOC: Steps starting from 3.3.1 are implemented here.
// 1. For each removed frame, if the frame has a decode timestamp equal to the last decode timestamp for the
// frame's track, run the following steps:
// AD-HOC: The spec doesn't nest steps 2-5 below under step 1's if statement, but clearing the last decode
// timestamp upon every removal here would potentially force a RAP unexpectedly.
// https://github.com/w3c/media-source/issues/290
if (last_decode_timestamp.has_value() && latest_timestamp == last_decode_timestamp.value()) {
// -> If mode equals "segments":
if (m_mode == AppendMode::Segments) {
// Set [[group end timestamp]] to presentation timestamp.
m_group_end_timestamp = latest_timestamp;
}
// -> If mode equals "sequence":
else if (m_mode == AppendMode::Sequence) {
// Set [[group start timestamp]] equal to the [[group end timestamp]].
m_group_start_timestamp = m_group_end_timestamp;
}
// 2. Unset the last decode timestamp on all track buffers.
// 3. Unset the last frame duration on all track buffers.
// 4. Unset the highest end timestamp on all track buffers.
unset_all_track_buffer_timestamps();
// 5. Set the need random access point flag on all track buffers to true.
set_need_random_access_point_flag_on_all_track_buffers(true);
}
}
// https://w3c.github.io/media-source/#dfn-coded-frame-removal
// 4. If the [[buffer full flag]] equals true and this object is ready to accept more bytes, then set the
// [[buffer full flag]] to false.
if (total_buffered_bytes() + new_data_size < current_capacity_in_bytes)
m_buffer_full_flag = false;
}
void SourceBufferProcessor::drop_consumed_bytes_from_input_buffer()

View file

@ -67,6 +67,9 @@ public:
AK::Duration group_end_timestamp() const;
bool is_buffer_full() const;
size_t total_buffered_bytes() const;
size_t capacity_in_bytes() const;
void set_mode(AppendMode);
void set_generate_timestamps_flag(bool);
void set_group_start_timestamp(Optional<AK::Duration>);
@ -90,7 +93,7 @@ public:
void run_segment_parser_loop();
void reset_parser_state();
void run_coded_frame_eviction();
void run_coded_frame_eviction(size_t new_data_size, AK::Duration current_time);
void set_reached_end_of_stream();
void clear_reached_end_of_stream();

View file

@ -50,6 +50,7 @@ void TrackBufferDemuxer::add_coded_frame(Media::CodedFrame frame)
if (insert_index < m_coded_frames.size() && m_coded_frames[insert_index].timestamp() < timestamp)
insert_index++;
m_total_bytes += frame.data().size();
m_coded_frames.insert(insert_index, move(frame));
if (insert_index <= m_read_position && (!m_last_returned_timestamp.has_value() || m_last_returned_timestamp.value() > timestamp))
@ -88,6 +89,7 @@ void TrackBufferDemuxer::remove_coded_frames_and_dependants_in_range(AK::Duratio
auto removed_start = removed_frame.timestamp();
auto removed_end = (removed_frame.timestamp() + removed_frame.duration());
m_track_buffer_ranges.remove_range(removed_start, removed_end);
m_total_bytes -= removed_frame.data().size();
}
m_coded_frames.remove(remove_start, remove_end - remove_start);
@ -105,6 +107,67 @@ void TrackBufferDemuxer::remove_coded_frames_and_dependants_in_range(AK::Duratio
}
}
size_t TrackBufferDemuxer::total_bytes() const
{
Sync::MutexLocker locker { m_mutex };
return m_total_bytes;
}
bool TrackBufferDemuxer::is_frame_evictable_while_locked(Media::CodedFrame const& frame, AK::Duration current_time) const
{
auto time_range_start = current_time;
auto time_range_end = current_time;
if (m_last_returned_timestamp.has_value()) {
time_range_start = min(time_range_start, m_last_returned_timestamp.value());
time_range_end = max(time_range_end, m_last_returned_timestamp.value());
}
return frame.timestamp() < time_range_start || frame.timestamp() > time_range_end;
}
Optional<AK::Duration> TrackBufferDemuxer::earliest_evictable_frame_timestamp(AK::Duration current_time) const
{
Sync::MutexLocker locker { m_mutex };
if (m_coded_frames.is_empty())
return {};
auto const& frame = m_coded_frames[0];
if (!is_frame_evictable_while_locked(frame, current_time))
return {};
return frame.timestamp();
}
size_t TrackBufferDemuxer::take_earliest_frame()
{
Sync::MutexLocker locker { m_mutex };
auto frame = m_coded_frames.take_first();
m_track_buffer_ranges.remove_range(frame.timestamp(), frame.timestamp() + frame.duration());
auto bytes = frame.data().size();
m_total_bytes -= bytes;
if (m_read_position > 0)
m_read_position--;
return bytes;
}
Optional<AK::Duration> TrackBufferDemuxer::latest_evictable_frame_timestamp(AK::Duration current_time) const
{
Sync::MutexLocker locker { m_mutex };
if (m_coded_frames.is_empty())
return {};
auto const& frame = m_coded_frames.last();
if (!is_frame_evictable_while_locked(frame, current_time))
return {};
return frame.timestamp();
}
size_t TrackBufferDemuxer::take_latest_frame()
{
Sync::MutexLocker locker { m_mutex };
auto frame = m_coded_frames.take_last();
m_track_buffer_ranges.remove_range(frame.timestamp(), frame.timestamp() + frame.duration());
auto bytes = frame.data().size();
m_total_bytes -= bytes;
return bytes;
}
void TrackBufferDemuxer::set_reached_end_of_stream()
{
Sync::MutexLocker locker { m_mutex };

View file

@ -33,6 +33,14 @@ public:
void add_coded_frame(Media::CodedFrame);
void remove_coded_frames_and_dependants_in_range(AK::Duration start, AK::Duration end);
size_t total_bytes() const;
Optional<AK::Duration> earliest_evictable_frame_timestamp(AK::Duration current_time) const;
size_t take_earliest_frame();
Optional<AK::Duration> latest_evictable_frame_timestamp(AK::Duration current_time) const;
size_t take_latest_frame();
void set_reached_end_of_stream();
void clear_reached_end_of_stream();
@ -56,6 +64,7 @@ public:
private:
AK::Duration maximum_time_range_gap() const;
bool next_frame_is_in_gap_while_locked() const;
bool is_frame_evictable_while_locked(Media::CodedFrame const&, AK::Duration current_time) const;
Media::Track m_track;
Media::CodecID m_codec_id;
@ -72,6 +81,7 @@ private:
Media::TimeRanges m_track_buffer_ranges;
AK::Duration m_last_frame_duration;
size_t m_total_bytes { 0 };
Atomic<bool> m_aborted { false };
};