LibMedia: Implement buffered time range scanning for Matroska

This commit is contained in:
Zaggy1024 2026-03-29 03:04:14 -05:00 committed by Gregory Bertilson
parent be089ce24c
commit 7d3dd2d641
7 changed files with 378 additions and 9 deletions

View file

@ -25,8 +25,10 @@ DecoderErrorOr<NonnullRefPtr<MatroskaDemuxer>> MatroskaDemuxer::from_stream(Nonn
MatroskaDemuxer::MatroskaDemuxer(NonnullRefPtr<MediaStream> const& stream, Reader&& reader)
: m_stream(stream)
, m_buffered_scan_cursor(stream->create_cursor())
, m_reader(move(reader))
{
m_buffered_scan_cursor->set_is_blocking(false);
}
MatroskaDemuxer::~MatroskaDemuxer() = default;
@ -177,12 +179,10 @@ DecoderErrorOr<AK::Duration> MatroskaDemuxer::total_duration()
TimeRanges MatroskaDemuxer::buffered_time_ranges() const
{
// FIXME: Scan the stream for buffered ranges.
TimeRanges ranges;
auto duration = m_reader.duration();
if (duration.has_value())
ranges.add_range(AK::Duration::zero(), duration.value());
return ranges;
auto byte_ranges = m_stream->available_byte_ranges();
if (byte_ranges.is_empty())
return {};
return m_reader.buffered_time_ranges(m_buffered_scan_cursor, byte_ranges);
}
DecoderErrorOr<AK::Duration> MatroskaDemuxer::duration_of_track(Track const&)

View file

@ -63,6 +63,7 @@ private:
TrackStatus& get_track_status(Track const&);
NonnullRefPtr<MediaStream> m_stream;
NonnullRefPtr<MediaStreamCursor> m_buffered_scan_cursor;
Reader m_reader;
mutable Sync::Mutex m_track_statuses_mutex;

View file

@ -866,7 +866,33 @@ DecoderErrorOr<Block> Reader::parse_block_group(Streamer& streamer, AK::Duration
DecoderErrorOr<SampleIterator> Reader::create_sample_iterator(NonnullRefPtr<MediaStreamCursor> const& cursor, Optional<u64> track_number) const
{
dbgln_if(MATROSKA_DEBUG, "Creating sample iterator starting at {} relative to segment at {}", m_first_cluster_position, m_segment_contents_position);
return create_sample_iterator_at_byte_position(cursor, 0, track_number);
}
DecoderErrorOr<SampleIterator> Reader::create_sample_iterator_at_byte_position(NonnullRefPtr<MediaStreamCursor> const& cursor, size_t position, Optional<u64> track_number) const
{
Optional<size_t> cluster_position;
if (m_first_cluster_position >= position) {
cluster_position = m_first_cluster_position;
} else {
for (auto const& [track_number_with_cues, cue_points] : m_cues) {
if (track_number.has_value() && track_number != track_number_with_cues)
continue;
for (auto const& cue_point : cue_points) {
auto cue_cluster_position = m_segment_contents_position + cue_point.position.cluster_position();
if (cue_cluster_position < position)
continue;
if (!cluster_position.has_value() || cue_cluster_position < cluster_position.value())
cluster_position = cue_cluster_position;
}
}
}
if (!cluster_position.has_value())
return DecoderError::format(DecoderErrorCategory::EndOfStream, "Could not find a Cluster element after {}", position);
dbgln_if(MATROSKA_DEBUG, "Creating sample iterator starting at {} relative to segment at {}", cluster_position, m_segment_contents_position);
TrackBlockContexts track_contexts;
if (track_number.has_value()) {
auto track = TRY(track_for_track_number(track_number.value()));
@ -875,7 +901,7 @@ DecoderErrorOr<SampleIterator> Reader::create_sample_iterator(NonnullRefPtr<Medi
for (auto const& [number, track_entry] : m_tracks)
track_contexts.set(number, TrackBlockContext::from_track_entry(*track_entry));
}
return SampleIterator(cursor, track_number, move(track_contexts), m_segment_information.timestamp_scale(), m_segment_contents_position, m_first_cluster_position);
return SampleIterator(cursor, track_number, move(track_contexts), m_segment_information.timestamp_scale(), m_segment_contents_position, cluster_position.value());
}
static DecoderErrorOr<CueTrackPosition> parse_cue_track_position(Streamer& streamer)
@ -1121,4 +1147,131 @@ Optional<Vector<TrackCuePoint> const&> Reader::cue_points_for_track(u64 track_nu
return m_cues.get(track_number);
}
TimeRanges Reader::buffered_time_ranges(NonnullRefPtr<MediaStreamCursor> const& cursor, Vector<MediaStream::ByteRange> const& byte_ranges) const
{
auto create_iterator = [&](size_t position) -> Optional<SampleIterator> {
auto iterator = create_sample_iterator_at_byte_position(cursor, position);
if (iterator.is_error())
return {};
return iterator.release_value();
};
size_t cached_range_index = 0;
size_t byte_range_index = 0;
while (byte_range_index < byte_ranges.size()) {
auto cached_range = m_buffered_ranges.get(cached_range_index);
auto const& byte_range = byte_ranges[byte_range_index];
VERIFY(byte_range.start < byte_range.end);
auto previous_byte_range = byte_ranges.get(byte_range_index - 1);
if (previous_byte_range.has_value())
VERIFY(previous_byte_range->end < byte_range.start);
// If the current byte range precedes the current cached range, insert a new one for that byte range.
// Restart the loop with the same cached range.
if (!cached_range.has_value() || byte_range.start < cached_range->start) {
auto new_cached_range = BufferedRange {
.start = byte_range.start,
.end = byte_range.end,
.iterator = create_iterator(byte_range.start),
};
m_buffered_ranges.insert(cached_range_index, move(new_cached_range));
cached_range_index++;
byte_range_index++;
continue;
}
VERIFY(cached_range.has_value());
// If the current range is an exact match to the byte range, we can just update the end byte and advance.
if (byte_range.start == cached_range->start) {
cached_range->end = byte_range.end;
cached_range_index++;
byte_range_index++;
continue;
}
auto previous_cached_range = m_buffered_ranges.get(cached_range_index - 1);
if (previous_cached_range.has_value()) {
VERIFY(previous_cached_range->start < cached_range->start);
// If the current cached range is entirely encompassed by the previous one, then remove that range. We'll
// need to rescan its contents.
if (previous_cached_range->end >= cached_range->end) {
m_buffered_ranges.remove(cached_range_index);
continue;
}
}
// The range has shifted forward. We'll need to re-read from the new start position.
auto new_iterator = create_iterator(byte_range.start);
// If the cached range's last read is still contained in the new byte range, we can keep using its end time.
// Just grab the first frame at the new byte range's start and update the cached range's start from it.
auto& cached_iterator = cached_range->iterator;
if (cached_iterator.has_value() && new_iterator.has_value()) {
auto last_cached_position = cached_iterator->position();
if (byte_range.start <= last_cached_position && last_cached_position <= byte_range.end) {
auto first_block = new_iterator->next_block();
if (!first_block.is_error() && first_block.value().timestamp().has_value()) {
cached_range->start = byte_range.start;
cached_range->end = byte_range.end;
cached_range->time_start = first_block.value().timestamp().value();
cached_range_index++;
byte_range_index++;
continue;
}
}
}
// Otherwise, we have to reset everything for this range.
*cached_range = {
.start = byte_range.start,
.end = byte_range.end,
.iterator = move(new_iterator),
};
cached_range_index++;
byte_range_index++;
}
// Remove any leftover ranges. We should be left with only the exact ranges provided to us.
m_buffered_ranges.remove(cached_range_index, m_buffered_ranges.size() - cached_range_index);
// All previously known buffered ranges are now matched up or discarded. Iterate the blocks to update the ranges'
// end times and append the ranges.
VERIFY(m_buffered_ranges.size() == byte_ranges.size());
TimeRanges result;
for (size_t i = 0; i < byte_ranges.size(); i++) {
auto& cached_range = m_buffered_ranges[i];
auto const& byte_range = byte_ranges[i];
VERIFY(cached_range.start == byte_range.start);
VERIFY(cached_range.end == byte_range.end);
if (cached_range.iterator.has_value() && cached_range.iterator->position() < cached_range.end) {
auto& iterator = cached_range.iterator;
while (iterator->position() < byte_range.end) {
auto block_or_error = iterator->next_block();
if (block_or_error.is_error())
break;
auto block = block_or_error.release_value();
if (block.timestamp().has_value() && block.duration().has_value()) {
if (!cached_range.time_start.has_value())
cached_range.time_start = block.timestamp().value();
auto block_end = block.timestamp().value() + block.duration().value();
cached_range.time_end = block_end;
}
}
}
if (cached_range.time_start.has_value())
result.add_range(max(AK::Duration::zero(), cached_range.time_start.value()), cached_range.time_end);
}
return result;
}
}

View file

@ -13,6 +13,8 @@
#include <LibMedia/DecoderError.h>
#include <LibMedia/Export.h>
#include <LibMedia/Forward.h>
#include <LibMedia/MediaStream.h>
#include <LibMedia/TimeRanges.h>
#include "Document.h"
#include "SampleIterator.h"
@ -51,12 +53,15 @@ public:
DecoderErrorOr<size_t> track_count() const;
DecoderErrorOr<SampleIterator> create_sample_iterator(NonnullRefPtr<MediaStreamCursor> const& cursor, Optional<u64> track_number = {}) const;
DecoderErrorOr<SampleIterator> create_sample_iterator_at_byte_position(NonnullRefPtr<MediaStreamCursor> const& cursor, size_t position, Optional<u64> track_number = {}) const;
DecoderErrorOr<SampleIterator> seek_to_random_access_point(SampleIterator, AK::Duration) const;
Optional<Vector<TrackCuePoint> const&> cue_points_for_track(u64 track_number) const;
static size_t find_cue_point_index_at_or_before(Vector<TrackCuePoint> const&, Optional<AK::Duration> total_duration, AK::Duration target);
TimeRanges buffered_time_ranges(NonnullRefPtr<MediaStreamCursor> const&, Vector<MediaStream::ByteRange> const& byte_ranges) const;
private:
Reader() = default;
@ -90,6 +95,15 @@ private:
// The vectors must be sorted by timestamp at all times.
HashMap<u64, Vector<TrackCuePoint>> m_cues;
struct BufferedRange {
size_t start { 0 };
size_t end { 0 };
Optional<SampleIterator> iterator;
Optional<AK::Duration> time_start { OptionalNone() };
AK::Duration time_end { AK::Duration::zero() };
};
mutable Vector<BufferedRange> m_buffered_ranges;
};
}

View file

@ -1,4 +1,4 @@
## vp9_clamp_reference_mvs.webm & master_elements_containing_crc32.mkv
## vp9_clamp_reference_mvs.webm, master_elements_containing_crc32.mkv & big_buck_bunny_5s.webm
Licensed under [Creative Commons Attribution 3.0](http://creativecommons.org/licenses/by/3.0/)\
(c) copyright 2008, Blender Foundation / [www.bigbuckbunny.org](https://www.bigbuckbunny.org)

View file

@ -483,3 +483,204 @@ TEST_CASE(opus_frame_duration)
EXPECT_EQ(block.duration().value(), expected_duration);
}
}
static ByteBuffer load_test_file_data(StringView path)
{
auto file = MUST(Core::File::open(path, Core::File::OpenMode::Read));
return MUST(file->read_until_eof());
}
static constexpr size_t CUES_START = 298382;
static auto create_incremental_demuxer(ByteBuffer const& file_data, NonnullRefPtr<Media::IncrementallyPopulatedStream>& stream, size_t initial_end)
{
stream = Media::IncrementallyPopulatedStream::create_empty();
stream->add_chunk_at(0, file_data.bytes().slice(0, initial_end));
stream->add_chunk_at(CUES_START, file_data.bytes().slice(CUES_START));
return MUST(Media::Matroska::MatroskaDemuxer::from_stream(stream));
}
TEST_CASE(buffered_time_ranges_full_file)
{
auto file_data = load_test_file_data("./vp9_in_webm.webm"sv);
auto stream = Media::IncrementallyPopulatedStream::create_from_buffer(file_data);
auto demuxer = MUST(Media::Matroska::MatroskaDemuxer::from_stream(stream));
auto ranges = demuxer->buffered_time_ranges();
EXPECT_EQ(ranges.size(), 1u);
EXPECT_EQ(ranges[0].start, AK::Duration::from_microseconds(500));
EXPECT_EQ(ranges[0].end, AK::Duration::from_microseconds(1021500));
}
TEST_CASE(buffered_time_ranges_incremental_thirds)
{
auto file_data = load_test_file_data("./vp9_in_webm.webm"sv);
auto start = AK::Duration::from_microseconds(500);
size_t one_third = file_data.size() / 3;
size_t two_thirds = one_third * 2;
NonnullRefPtr<Media::IncrementallyPopulatedStream> stream = Media::IncrementallyPopulatedStream::create_empty();
auto demuxer = create_incremental_demuxer(file_data, stream, one_third);
// Stage 1: first third.
auto ranges_1 = demuxer->buffered_time_ranges();
EXPECT_EQ(ranges_1.size(), 1u);
EXPECT_EQ(ranges_1[0].start, start);
EXPECT_EQ(ranges_1[0].end, AK::Duration::from_microseconds(91000));
// Stage 2: extend to two thirds.
stream->add_chunk_at(one_third, file_data.bytes().slice(one_third, two_thirds - one_third));
auto ranges_2 = demuxer->buffered_time_ranges();
EXPECT_EQ(ranges_2.size(), 1u);
EXPECT_EQ(ranges_2[0].start, start);
EXPECT(ranges_2[0].end > ranges_1[0].end);
EXPECT_EQ(ranges_2[0].end, AK::Duration::from_microseconds(571000));
// Stage 3: complete the file.
stream->add_chunk_at(two_thirds, file_data.bytes().slice(two_thirds, CUES_START - two_thirds));
stream->close();
auto ranges_3 = demuxer->buffered_time_ranges();
EXPECT_EQ(ranges_3.size(), 1u);
EXPECT_EQ(ranges_3[0].start, start);
EXPECT(ranges_3[0].end > ranges_2[0].end);
EXPECT_EQ(ranges_3[0].end, AK::Duration::from_microseconds(1021500));
}
// big_buck_bunny_5s.webm cluster layout:
// Cluster 0 (0ms): byte 482
// Cluster 1 (500ms): byte 6128
// Cluster 2 (1000ms): byte 13177
// Cluster 3 (1500ms): byte 21628
// Cluster 4 (2000ms): byte 31913
// Cluster 5 (2500ms): byte 49246
// Cluster 6 (3000ms): byte 59860
// Cluster 7 (3500ms): byte 71977
// Cluster 8 (4000ms): byte 91687
// Cluster 9 (4500ms): byte 102297
// Cues: byte 113303
// File size: 113491
static constexpr size_t BBB_CUES_START = 113303;
TEST_CASE(buffered_time_ranges_gap_then_fill)
{
auto file_data = load_test_file_data("./big_buck_bunny_5s.webm"sv);
// Buffer clusters 0-3 (0-2s) and clusters 7-9 (3.5-5s), leaving a gap at clusters 4-6 (2-3.5s).
constexpr size_t first_chunk_end = 31913;
constexpr size_t second_chunk_start = 71977;
auto stream = Media::IncrementallyPopulatedStream::create_empty();
stream->add_chunk_at(0, file_data.bytes().slice(0, first_chunk_end));
stream->add_chunk_at(BBB_CUES_START, file_data.bytes().slice(BBB_CUES_START));
stream->close();
stream->add_chunk_at(second_chunk_start, file_data.bytes().slice(second_chunk_start, BBB_CUES_START - second_chunk_start));
auto demuxer = MUST(Media::Matroska::MatroskaDemuxer::from_stream(stream));
auto ranges_gap = demuxer->buffered_time_ranges();
EXPECT_EQ(ranges_gap.size(), 2u);
EXPECT_EQ(ranges_gap[0].start, AK::Duration::zero());
EXPECT_EQ(ranges_gap[1].start, AK::Duration::from_milliseconds(3500));
// Fill the gap with clusters 4-6.
stream->add_chunk_at(first_chunk_end, file_data.bytes().slice(first_chunk_end, second_chunk_start - first_chunk_end));
auto ranges_filled = demuxer->buffered_time_ranges();
EXPECT_EQ(ranges_filled.size(), 1u);
EXPECT_EQ(ranges_filled[0].start, AK::Duration::zero());
EXPECT_EQ(ranges_filled[0].end, AK::Duration::from_nanoseconds(4999666666));
}
TEST_CASE(buffered_time_ranges_reverse_order_chunks)
{
auto file_data = load_test_file_data("./big_buck_bunny_5s.webm"sv);
// Buffer init + clusters 0-2 (0-1.5s) and clusters 7-9 (3.5-5s), then fill the middle.
constexpr size_t first_chunk_end = 21628;
constexpr size_t second_chunk_start = 71977;
NonnullRefPtr<Media::IncrementallyPopulatedStream> stream = Media::IncrementallyPopulatedStream::create_empty();
stream = Media::IncrementallyPopulatedStream::create_empty();
stream->add_chunk_at(0, file_data.bytes().slice(0, first_chunk_end));
stream->add_chunk_at(second_chunk_start, file_data.bytes().slice(second_chunk_start));
stream->close();
auto demuxer = MUST(Media::Matroska::MatroskaDemuxer::from_stream(stream));
auto ranges_1 = demuxer->buffered_time_ranges();
EXPECT_EQ(ranges_1.size(), 2u);
EXPECT_EQ(ranges_1[0].start, AK::Duration::zero());
EXPECT_EQ(ranges_1[1].start, AK::Duration::from_milliseconds(3500));
// Fill the gap with clusters 3-6.
stream->add_chunk_at(first_chunk_end, file_data.bytes().slice(first_chunk_end, second_chunk_start - first_chunk_end));
auto ranges_2 = demuxer->buffered_time_ranges();
EXPECT_EQ(ranges_2.size(), 1u);
EXPECT_EQ(ranges_2[0].start, AK::Duration::zero());
EXPECT(ranges_2[0].end > AK::Duration::from_milliseconds(4900));
}
TEST_CASE(buffered_time_ranges_repeated_query)
{
auto file_data = load_test_file_data("./vp9_in_webm.webm"sv);
auto stream = Media::IncrementallyPopulatedStream::create_from_buffer(file_data);
auto demuxer = MUST(Media::Matroska::MatroskaDemuxer::from_stream(stream));
// Query multiple times — results should be identical and stable.
auto ranges_1 = demuxer->buffered_time_ranges();
auto ranges_2 = demuxer->buffered_time_ranges();
auto ranges_3 = demuxer->buffered_time_ranges();
EXPECT_EQ(ranges_1, ranges_2);
EXPECT_EQ(ranges_2, ranges_3);
}
TEST_CASE(buffered_time_ranges_evicted_start)
{
// Simulate data eviction by calling buffered_time_ranges with the full file first,
// then with a byte range whose start is later (as if the beginning was evicted).
auto file_data = load_test_file_data("./big_buck_bunny_5s.webm"sv);
auto stream = Media::IncrementallyPopulatedStream::create_from_buffer(file_data);
auto reader = MUST(Media::Matroska::Reader::from_stream(stream->create_cursor()));
auto cursor = stream->create_cursor();
// Get buffered ranges for the full file.
Vector<Media::MediaStream::ByteRange> byte_ranges;
byte_ranges.append({ 0, file_data.size() });
auto time_ranges = reader.buffered_time_ranges(cursor, byte_ranges);
EXPECT_EQ(time_ranges.size(), 1u);
EXPECT_EQ(time_ranges[0].start, AK::Duration::zero());
EXPECT_EQ(time_ranges[0].end, AK::Duration::from_nanoseconds(4999666666));
// Simulate eviction of the first four clusters.
byte_ranges[0] = { 31913, file_data.size() };
time_ranges = reader.buffered_time_ranges(cursor, byte_ranges);
EXPECT_EQ(time_ranges.size(), 1u);
EXPECT_EQ(time_ranges[0].start, AK::Duration::from_milliseconds(2000));
EXPECT_EQ(time_ranges[0].end, AK::Duration::from_nanoseconds(4999666666));
}
TEST_CASE(buffered_time_ranges_evicted_start_appended_end)
{
// Simulate data eviction by calling buffered_time_ranges with an early portion of the file,
// then again with a new range that does not overlap.
auto file_data = load_test_file_data("./big_buck_bunny_5s.webm"sv);
auto stream = Media::IncrementallyPopulatedStream::create_from_buffer(file_data);
auto reader = MUST(Media::Matroska::Reader::from_stream(stream->create_cursor()));
auto cursor = stream->create_cursor();
// Get buffered ranges with only the first two clusters available.
Vector<Media::MediaStream::ByteRange> byte_ranges;
byte_ranges.append({ 0, 21628 });
auto time_ranges = reader.buffered_time_ranges(cursor, byte_ranges);
EXPECT_EQ(time_ranges.size(), 1u);
EXPECT_EQ(time_ranges[0].start, AK::Duration::zero());
EXPECT_EQ(time_ranges[0].end, AK::Duration::from_nanoseconds(1499666666));
// Get buffered ranges with only clusters 8 and 9 available.
byte_ranges[0] = { 91687, 113303 };
time_ranges = reader.buffered_time_ranges(cursor, byte_ranges);
EXPECT_EQ(time_ranges.size(), 1u);
EXPECT_EQ(time_ranges[0].start, AK::Duration::from_milliseconds(4000));
EXPECT_EQ(time_ranges[0].end, AK::Duration::from_nanoseconds(4999666666));
// Get buffered ranges with a byte range containing no clusters.
byte_ranges[0] = { 113303, file_data.size() };
time_ranges = reader.buffered_time_ranges(cursor, byte_ranges);
EXPECT_EQ(time_ranges.size(), 0u);
}

Binary file not shown.