diff --git a/Libraries/LibMedia/CMakeLists.txt b/Libraries/LibMedia/CMakeLists.txt index 163266547a..8c0b2b52c1 100644 --- a/Libraries/LibMedia/CMakeLists.txt +++ b/Libraries/LibMedia/CMakeLists.txt @@ -16,6 +16,7 @@ set(SOURCES Sinks/AudioMixingSink.cpp Sinks/DisplayingVideoSink.cpp TimedImage.cpp + TimeRanges.cpp VideoFrame.cpp ) diff --git a/Libraries/LibMedia/TimeRanges.cpp b/Libraries/LibMedia/TimeRanges.cpp new file mode 100644 index 0000000000..aa70c183d3 --- /dev/null +++ b/Libraries/LibMedia/TimeRanges.cpp @@ -0,0 +1,139 @@ +/* + * Copyright (c) 2026-present, the Ladybird developers. + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include + +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 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_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]; +} + +} diff --git a/Libraries/LibMedia/TimeRanges.h b/Libraries/LibMedia/TimeRanges.h new file mode 100644 index 0000000000..40397bb1cf --- /dev/null +++ b/Libraries/LibMedia/TimeRanges.h @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2026-present, the Ladybird developers. + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include +#include +#include + +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 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_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 m_ranges; +}; + +} + +template<> +struct AK::Formatter : StandardFormatter { + ErrorOr format(FormatBuilder& builder, Media::TimeRanges::Range const& range) + { + Formatter 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 : StandardFormatter { + ErrorOr 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 value_formatter; + TRY(value_formatter.format(builder, ranges[i])); + } + return builder.put_literal("]"sv); + } +}; diff --git a/Tests/LibMedia/CMakeLists.txt b/Tests/LibMedia/CMakeLists.txt index 46332593de..e6d6b117d9 100644 --- a/Tests/LibMedia/CMakeLists.txt +++ b/Tests/LibMedia/CMakeLists.txt @@ -12,6 +12,7 @@ set(TEST_SOURCES TestParseMatroska.cpp TestPlaybackStream.cpp TestVorbisDecode.cpp + TestTimeRanges.cpp TestVP9Decode.cpp TestWav.cpp ) diff --git a/Tests/LibMedia/TestTimeRanges.cpp b/Tests/LibMedia/TestTimeRanges.cpp new file mode 100644 index 0000000000..3feca77708 --- /dev/null +++ b/Tests/LibMedia/TestTimeRanges.cpp @@ -0,0 +1,327 @@ +/* + * Copyright (c) 2026-present, the Ladybird developers. + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include + +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) })); +}