LibMedia: Add a TimeRanges class to be used for buffered attributes

This will be used by PlaybackManager and Demuxer implementations to
communicate the buffered time up to HTMLMediaElement and SourceBuffer.
This commit is contained in:
Zaggy1024 2026-03-25 16:21:37 -05:00 committed by Gregory Bertilson
parent 5083a96c90
commit 2a75c0db62
5 changed files with 563 additions and 0 deletions

View file

@ -16,6 +16,7 @@ set(SOURCES
Sinks/AudioMixingSink.cpp
Sinks/DisplayingVideoSink.cpp
TimedImage.cpp
TimeRanges.cpp
VideoFrame.cpp
)

View file

@ -0,0 +1,139 @@
/*
* Copyright (c) 2026-present, the Ladybird developers.
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/BinarySearch.h>
#include <LibMedia/TimeRanges.h>
namespace Media {
void TimeRanges::add_range(AK::Duration start, AK::Duration end)
{
if (start >= end)
return;
if (m_ranges.is_empty()) {
m_ranges.append({ start, end });
return;
}
// Find the index we should insert at. This index would point to either the range overlapping the start of the new
// range, or the range following it if there is no overlap.
size_t insert_index = 0;
binary_search(m_ranges, start, &insert_index, [](AK::Duration needle, Range const& range) -> int {
return needle <=> range.start;
});
if (m_ranges[insert_index].end < start)
insert_index++;
// Determine the range of ranges that will be made contiguous by this new range.
AK::Duration merged_start = start;
AK::Duration merged_end = end;
size_t merge_end_index = insert_index;
if (merge_end_index < m_ranges.size()) {
merged_start = min(merged_start, m_ranges[merge_end_index].start);
while (merge_end_index < m_ranges.size() && m_ranges[merge_end_index].start <= merged_end) {
merged_end = max(merged_end, m_ranges[merge_end_index].end);
merge_end_index++;
}
}
// Replace the found range with our new merged range if applicable.
if (merge_end_index > insert_index) {
m_ranges[insert_index] = { merged_start, merged_end };
m_ranges.remove(insert_index + 1, merge_end_index - insert_index - 1);
} else {
m_ranges.insert(insert_index, { merged_start, merged_end });
}
}
void TimeRanges::remove_range(AK::Duration start, AK::Duration end)
{
if (start >= end)
return;
Vector<Range> new_ranges;
for (auto const& range : m_ranges) {
if (range.end <= start || range.start >= end) {
new_ranges.append(range);
continue;
}
if (range.start < start)
new_ranges.append({ range.start, start });
if (range.end > end)
new_ranges.append({ end, range.end });
}
m_ranges = move(new_ranges);
}
AK::Duration TimeRanges::highest_end_time() const
{
if (m_ranges.is_empty())
return AK::Duration::zero();
return m_ranges.last().end;
}
TimeRanges TimeRanges::coalesced(AK::Duration threshold) const
{
TimeRanges result;
if (m_ranges.is_empty())
return result;
result.m_ranges.append(m_ranges[0]);
for (size_t i = 1; i < m_ranges.size(); i++) {
auto& last = result.m_ranges.last();
if (m_ranges[i].start - last.end <= threshold) {
last.end = max(last.end, m_ranges[i].end);
} else {
result.m_ranges.append(m_ranges[i]);
}
}
return result;
}
TimeRanges TimeRanges::intersection(TimeRanges const& other) const
{
TimeRanges result;
size_t i = 0;
size_t j = 0;
while (i < m_ranges.size() && j < other.m_ranges.size()) {
auto const& a = m_ranges[i];
auto const& b = other.m_ranges[j];
auto inter_start = max(a.start, b.start);
auto inter_end = min(a.end, b.end);
if (inter_start < inter_end)
result.m_ranges.append({ inter_start, inter_end });
// Advance the range that ends first.
if (a.end < b.end)
i++;
else
j++;
}
return result;
}
Optional<TimeRanges::Range> TimeRanges::range_at_or_after(AK::Duration point) const
{
size_t index = 0;
binary_search(m_ranges, point, &index, [](AK::Duration needle, Range const& range) -> int {
return needle <=> range.start;
});
if (index >= m_ranges.size())
return {};
if (m_ranges[index].end <= point)
index++;
if (index >= m_ranges.size())
return {};
return m_ranges[index];
}
}

View file

@ -0,0 +1,95 @@
/*
* Copyright (c) 2026-present, the Ladybird developers.
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Format.h>
#include <AK/StringBuilder.h>
#include <AK/Time.h>
#include <AK/Vector.h>
#include <LibMedia/Export.h>
namespace Media {
// A sorted, non-overlapping collection of time ranges. Ranges are automatically merged
// when they overlap or are adjacent. This is the backing store for track buffer ranges
// in the Media Source Extensions spec.
class MEDIA_API TimeRanges {
public:
struct Range {
AK::Duration start;
AK::Duration end;
bool operator==(Range const&) const = default;
};
TimeRanges() = default;
TimeRanges(std::initializer_list<Range> ranges)
{
for (auto const& range : ranges)
add_range(range.start, range.end);
}
void add_range(AK::Duration start, AK::Duration end);
void remove_range(AK::Duration start, AK::Duration end);
size_t size() const { return m_ranges.size(); }
bool is_empty() const { return m_ranges.is_empty(); }
Range const& operator[](size_t index) const { return m_ranges[index]; }
AK::Duration highest_end_time() const;
// Returns a copy with ranges separated by gaps smaller than the given threshold merged.
[[nodiscard]] TimeRanges coalesced(AK::Duration threshold) const;
// Returns the intersection of this set of ranges with another.
TimeRanges intersection(TimeRanges const& other) const;
// Returns the range containing the given point, or the nearest range after it.
Optional<Range> range_at_or_after(AK::Duration point) const;
bool operator==(TimeRanges const& other) const
{
return m_ranges == other.m_ranges;
}
auto begin() const { return m_ranges.begin(); }
auto end() const { return m_ranges.end(); }
private:
// Sorted by start time, non-overlapping.
Vector<Range> m_ranges;
};
}
template<>
struct AK::Formatter<Media::TimeRanges::Range> : StandardFormatter {
ErrorOr<void> format(FormatBuilder& builder, Media::TimeRanges::Range const& range)
{
Formatter<AK::Duration> value_formatter;
TRY(value_formatter.format(builder, range.start));
TRY(builder.put_literal("-"sv));
TRY(value_formatter.format(builder, range.end));
return {};
}
};
template<>
struct AK::Formatter<Media::TimeRanges> : StandardFormatter {
ErrorOr<void> format(FormatBuilder& builder, Media::TimeRanges const& ranges)
{
TRY(builder.put_literal("["sv));
for (size_t i = 0; i < ranges.size(); i++) {
if (i > 0)
TRY(builder.put_literal(", "sv));
Formatter<Media::TimeRanges::Range> value_formatter;
TRY(value_formatter.format(builder, ranges[i]));
}
return builder.put_literal("]"sv);
}
};

View file

@ -12,6 +12,7 @@ set(TEST_SOURCES
TestParseMatroska.cpp
TestPlaybackStream.cpp
TestVorbisDecode.cpp
TestTimeRanges.cpp
TestVP9Decode.cpp
TestWav.cpp
)

View file

@ -0,0 +1,327 @@
/*
* Copyright (c) 2026-present, the Ladybird developers.
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibMedia/TimeRanges.h>
#include <LibTest/TestCase.h>
using Media::TimeRanges;
static AK::Duration ms(i64 milliseconds)
{
return AK::Duration::from_milliseconds(milliseconds);
}
// add_range()
TEST_CASE(add_range_to_empty)
{
TimeRanges ranges;
ranges.add_range(ms(100), ms(200));
EXPECT_EQ(ranges, (TimeRanges { { ms(100), ms(200) } }));
}
TEST_CASE(add_range_invalid_is_no_op)
{
TimeRanges ranges;
ranges.add_range(ms(200), ms(100));
EXPECT(ranges.is_empty());
ranges.add_range(ms(100), ms(100));
EXPECT(ranges.is_empty());
}
TEST_CASE(add_range_before_existing)
{
TimeRanges ranges;
ranges.add_range(ms(500), ms(1000));
ranges.add_range(ms(100), ms(300));
EXPECT_EQ(ranges, (TimeRanges { { ms(100), ms(300) }, { ms(500), ms(1000) } }));
}
TEST_CASE(add_range_after_existing)
{
TimeRanges ranges;
ranges.add_range(ms(100), ms(300));
ranges.add_range(ms(500), ms(1000));
EXPECT_EQ(ranges, (TimeRanges { { ms(100), ms(300) }, { ms(500), ms(1000) } }));
}
TEST_CASE(add_range_adjacent_extends_end)
{
TimeRanges ranges;
ranges.add_range(ms(0), ms(500));
ranges.add_range(ms(500), ms(600));
EXPECT_EQ(ranges, (TimeRanges { { ms(0), ms(600) } }));
}
TEST_CASE(add_range_adjacent_extends_start)
{
TimeRanges ranges;
ranges.add_range(ms(500), ms(1000));
ranges.add_range(ms(300), ms(500));
EXPECT_EQ(ranges, (TimeRanges { { ms(300), ms(1000) } }));
}
TEST_CASE(add_range_overlapping_extends_end)
{
TimeRanges ranges;
ranges.add_range(ms(0), ms(500));
ranges.add_range(ms(300), ms(700));
EXPECT_EQ(ranges, (TimeRanges { { ms(0), ms(700) } }));
}
TEST_CASE(add_range_overlapping_extends_start)
{
TimeRanges ranges;
ranges.add_range(ms(500), ms(1000));
ranges.add_range(ms(300), ms(700));
EXPECT_EQ(ranges, (TimeRanges { { ms(300), ms(1000) } }));
}
TEST_CASE(add_range_inside_existing_is_no_op)
{
TimeRanges ranges;
ranges.add_range(ms(0), ms(1000));
ranges.add_range(ms(300), ms(700));
EXPECT_EQ(ranges, (TimeRanges { { ms(0), ms(1000) } }));
}
TEST_CASE(add_range_enclosing_existing)
{
TimeRanges ranges;
ranges.add_range(ms(300), ms(700));
ranges.add_range(ms(0), ms(1000));
EXPECT_EQ(ranges, (TimeRanges { { ms(0), ms(1000) } }));
}
TEST_CASE(add_range_bridges_two_ranges)
{
TimeRanges ranges;
ranges.add_range(ms(0), ms(500));
ranges.add_range(ms(700), ms(1000));
ranges.add_range(ms(400), ms(800));
EXPECT_EQ(ranges, (TimeRanges { { ms(0), ms(1000) } }));
}
TEST_CASE(add_range_bridges_three_ranges)
{
TimeRanges ranges;
ranges.add_range(ms(0), ms(200));
ranges.add_range(ms(400), ms(600));
ranges.add_range(ms(800), ms(1000));
ranges.add_range(ms(100), ms(900));
EXPECT_EQ(ranges, (TimeRanges { { ms(0), ms(1000) } }));
}
TEST_CASE(add_range_between_existing_no_overlap)
{
TimeRanges ranges;
ranges.add_range(ms(0), ms(200));
ranges.add_range(ms(800), ms(1000));
ranges.add_range(ms(400), ms(600));
EXPECT_EQ(ranges, (TimeRanges { { ms(0), ms(200) }, { ms(400), ms(600) }, { ms(800), ms(1000) } }));
}
TEST_CASE(add_range_sequential_frames)
{
TimeRanges ranges;
for (i64 i = 0; i < 100; i++)
ranges.add_range(ms(i * 33), ms((i + 1) * 33));
EXPECT_EQ(ranges, (TimeRanges { { ms(0), ms(3300) } }));
}
// remove_range()
TEST_CASE(remove_range_from_empty)
{
TimeRanges ranges;
ranges.remove_range(ms(0), ms(100));
EXPECT(ranges.is_empty());
}
TEST_CASE(remove_range_no_overlap)
{
TimeRanges ranges;
ranges.add_range(ms(0), ms(500));
ranges.remove_range(ms(600), ms(700));
EXPECT_EQ(ranges, (TimeRanges { { ms(0), ms(500) } }));
}
TEST_CASE(remove_range_entire_range)
{
TimeRanges ranges;
ranges.add_range(ms(0), ms(500));
ranges.remove_range(ms(0), ms(500));
EXPECT(ranges.is_empty());
}
TEST_CASE(remove_range_splits_range)
{
TimeRanges ranges;
ranges.add_range(ms(0), ms(1000));
ranges.remove_range(ms(300), ms(700));
EXPECT_EQ(ranges, (TimeRanges { { ms(0), ms(300) }, { ms(700), ms(1000) } }));
}
TEST_CASE(remove_range_trims_start)
{
TimeRanges ranges;
ranges.add_range(ms(0), ms(1000));
ranges.remove_range(ms(0), ms(500));
EXPECT_EQ(ranges, (TimeRanges { { ms(500), ms(1000) } }));
}
TEST_CASE(remove_range_trims_end)
{
TimeRanges ranges;
ranges.add_range(ms(0), ms(1000));
ranges.remove_range(ms(500), ms(1000));
EXPECT_EQ(ranges, (TimeRanges { { ms(0), ms(500) } }));
}
TEST_CASE(remove_range_across_multiple_ranges)
{
TimeRanges ranges;
ranges.add_range(ms(0), ms(200));
ranges.add_range(ms(400), ms(600));
ranges.add_range(ms(800), ms(1000));
ranges.remove_range(ms(100), ms(900));
EXPECT_EQ(ranges, (TimeRanges { { ms(0), ms(100) }, { ms(900), ms(1000) } }));
}
// highest_end_time()
TEST_CASE(highest_end_time_empty)
{
TimeRanges ranges;
EXPECT_EQ(ranges.highest_end_time(), AK::Duration::zero());
}
TEST_CASE(highest_end_time)
{
TimeRanges ranges;
ranges.add_range(ms(0), ms(500));
ranges.add_range(ms(700), ms(1000));
EXPECT_EQ(ranges.highest_end_time(), ms(1000));
}
// coalesced()
TEST_CASE(coalesced_no_change)
{
TimeRanges ranges;
ranges.add_range(ms(0), ms(500));
ranges.add_range(ms(1000), ms(1500));
EXPECT_EQ(ranges.coalesced(ms(100)), (TimeRanges { { ms(0), ms(500) }, { ms(1000), ms(1500) } }));
}
TEST_CASE(coalesced_merges_small_gap)
{
TimeRanges ranges;
ranges.add_range(ms(0), ms(500));
ranges.add_range(ms(550), ms(1000));
EXPECT_EQ(ranges.coalesced(ms(100)), (TimeRanges { { ms(0), ms(1000) } }));
}
TEST_CASE(coalesced_merges_exact_threshold)
{
TimeRanges ranges;
ranges.add_range(ms(0), ms(500));
ranges.add_range(ms(600), ms(1000));
EXPECT_EQ(ranges.coalesced(ms(100)), (TimeRanges { { ms(0), ms(1000) } }));
}
// intersection()
TEST_CASE(intersection_no_overlap)
{
TimeRanges a;
TimeRanges b;
a.add_range(ms(0), ms(500));
b.add_range(ms(600), ms(1000));
EXPECT(a.intersection(b).is_empty());
}
TEST_CASE(intersection_partial_overlap)
{
TimeRanges a;
TimeRanges b;
a.add_range(ms(0), ms(700));
b.add_range(ms(300), ms(1000));
EXPECT_EQ(a.intersection(b), (TimeRanges { { ms(300), ms(700) } }));
}
TEST_CASE(intersection_one_inside_other)
{
TimeRanges a;
TimeRanges b;
a.add_range(ms(0), ms(1000));
b.add_range(ms(300), ms(700));
EXPECT_EQ(a.intersection(b), (TimeRanges { { ms(300), ms(700) } }));
}
TEST_CASE(intersection_multiple_ranges)
{
TimeRanges a;
TimeRanges b;
a.add_range(ms(0), ms(500));
a.add_range(ms(700), ms(1000));
b.add_range(ms(300), ms(800));
EXPECT_EQ(a.intersection(b), (TimeRanges { { ms(300), ms(500) }, { ms(700), ms(800) } }));
}
TEST_CASE(intersection_with_empty)
{
TimeRanges a;
TimeRanges b;
a.add_range(ms(0), ms(1000));
EXPECT(a.intersection(b).is_empty());
}
// range_at_or_after()
TEST_CASE(range_at_or_after_empty)
{
TimeRanges ranges;
EXPECT(!ranges.range_at_or_after(ms(0)).has_value());
}
TEST_CASE(range_at_or_after_inside_range)
{
TimeRanges ranges;
ranges.add_range(ms(100), ms(500));
EXPECT_EQ(ranges.range_at_or_after(ms(300)), (TimeRanges::Range { ms(100), ms(500) }));
}
TEST_CASE(range_at_or_after_before_range)
{
TimeRanges ranges;
ranges.add_range(ms(100), ms(500));
EXPECT_EQ(ranges.range_at_or_after(ms(0)), (TimeRanges::Range { ms(100), ms(500) }));
}
TEST_CASE(range_at_or_after_after_all_ranges)
{
TimeRanges ranges;
ranges.add_range(ms(100), ms(500));
EXPECT(!ranges.range_at_or_after(ms(600)).has_value());
}
TEST_CASE(range_at_or_after_in_gap_returns_next)
{
TimeRanges ranges;
ranges.add_range(ms(0), ms(200));
ranges.add_range(ms(500), ms(700));
EXPECT_EQ(ranges.range_at_or_after(ms(300)), (TimeRanges::Range { ms(500), ms(700) }));
}
TEST_CASE(range_at_or_after_at_range_boundary)
{
TimeRanges ranges;
ranges.add_range(ms(0), ms(500));
ranges.add_range(ms(700), ms(1000));
EXPECT_EQ(ranges.range_at_or_after(ms(0)), (TimeRanges::Range { ms(0), ms(500) }));
EXPECT_EQ(ranges.range_at_or_after(ms(500)), (TimeRanges::Range { ms(700), ms(1000) }));
}