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:
parent
f33702d4f8
commit
289908968d
6 changed files with 219 additions and 12 deletions
|
|
@ -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()) {
|
||||
|
|
|
|||
|
|
@ -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&);
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue