From d6f821f22d5587652feb7dfc3a481cb34d6f397c Mon Sep 17 00:00:00 2001 From: Zaggy1024 Date: Fri, 20 Mar 2026 02:51:02 -0500 Subject: [PATCH] LibMedia: Parse Opus frame durations in Matroska::Reader Most WebM files don't have their default duration defined, so we need to parse the Opus frame header to determine the duration. This is needed for buffered range calculation. --- .../LibMedia/Containers/Matroska/Reader.cpp | 60 +++++++++++++++++- Tests/LibMedia/TestParseMatroska.cpp | 25 ++++++++ Tests/LibMedia/vp9_in_webm.webm | Bin 288681 -> 298404 bytes 3 files changed, 84 insertions(+), 1 deletion(-) diff --git a/Libraries/LibMedia/Containers/Matroska/Reader.cpp b/Libraries/LibMedia/Containers/Matroska/Reader.cpp index 292e1ba059..eff6d1786a 100644 --- a/Libraries/LibMedia/Containers/Matroska/Reader.cpp +++ b/Libraries/LibMedia/Containers/Matroska/Reader.cpp @@ -784,6 +784,56 @@ static void set_block_duration_to_default(Block& block, TrackBlockContext const& block.set_duration(AK::Duration::from_nanoseconds(AK::clamp_to(context.default_duration))); } +static DecoderErrorOr maybe_parse_opus_frame_duration(Streamer& streamer, Block& block, TrackBlockContext const& context) +{ + if (block.lacing() != Block::Lacing::None) + return {}; + if (codec_id_from_matroska_id_string(context.codec_id) != CodecID::Opus) + return {}; + if (block.data_size() == 0) + return DecoderError::corrupted("Opus frame is too small"sv); + + // https://datatracker.ietf.org/doc/html/rfc6716#section-3.1 + auto toc_byte = TRY(streamer.read_octet()); + auto configuration_number = (toc_byte >> 3) & 0b1'1111; + auto packet_code = toc_byte & 0b11; + // clang-format off + constexpr Array frame_durations = { + 10000, 20000, 40000, 60000, // SILK-only NB + 10000, 20000, 40000, 60000, // SILK-only MB + 10000, 20000, 40000, 60000, // SILK-only WB + 10000, 20000, // Hybrid SWB + 10000, 20000, // Hybrid FB + 2500, 5000, 10000, 20000, // CELT-only NB + 2500, 5000, 10000, 20000, // CELT-only WB + 2500, 5000, 10000, 20000, // CELT-only SWB + 2500, 5000, 10000, 20000, // CELT-only FB + }; + // clang-format on + + auto frame_duration = frame_durations[configuration_number]; + auto block_duration = TRY([&] -> DecoderErrorOr { + switch (packet_code) { + case 0: + return frame_duration; + case 1: + case 2: + return frame_duration * 2; + case 3: { + if (block.data_size() == 1) + return DecoderError::corrupted("Opus frame is too small"sv); + auto frame_count_byte = TRY(streamer.read_octet()); + auto frame_count = frame_count_byte & 0b11'1111; + return frame_duration * frame_count; + } + default: + VERIFY_NOT_REACHED(); + } + }()); + block.set_duration(AK::Duration::from_microseconds(block_duration)); + return {}; +} + DecoderErrorOr Reader::parse_simple_block(Streamer& streamer, AK::Duration cluster_timestamp, u64 segment_timestamp_scale, TrackBlockContexts const& contexts) { Block block; @@ -810,6 +860,8 @@ DecoderErrorOr Reader::parse_simple_block(Streamer& streamer, AK::Duratio auto const& context = maybe_context.value(); block.set_timestamp(block_timestamp_to_duration(cluster_timestamp, segment_timestamp_scale, context, timestamp_offset)); + TRY(maybe_parse_opus_frame_duration(streamer, block, context)); + set_block_duration_to_default(block, context); } @@ -843,6 +895,7 @@ DecoderErrorOr Reader::parse_block_group(Streamer& streamer, AK::Duration auto data_position = streamer.position(); auto data_size = content_end - data_position; block.set_data(data_position, data_size); + TRY(streamer.seek_to_position(content_end)); parsed_a_block = true; break; @@ -869,8 +922,13 @@ DecoderErrorOr Reader::parse_block_group(Streamer& streamer, AK::Duration if (context.timestamp_scale != 1) duration_nanoseconds = AK::clamp_to(static_cast(duration_nanoseconds) * context.timestamp_scale); block.set_duration(AK::Duration::from_nanoseconds(duration_nanoseconds)); - } else { + } else if (context.default_duration != 0) { set_block_duration_to_default(block, context); + } else { + auto position_after_block_group = streamer.position(); + TRY(streamer.seek_to_position(block.data_position())); + TRY(maybe_parse_opus_frame_duration(streamer, block, context)); + TRY(streamer.seek_to_position(position_after_block_group)); } } diff --git a/Tests/LibMedia/TestParseMatroska.cpp b/Tests/LibMedia/TestParseMatroska.cpp index 1c417a1e65..b74b430352 100644 --- a/Tests/LibMedia/TestParseMatroska.cpp +++ b/Tests/LibMedia/TestParseMatroska.cpp @@ -458,3 +458,28 @@ TEST_CASE(seeking) EXPECT(block_exact_100.only_keyframes()); } } + +TEST_CASE(opus_frame_duration) +{ + auto file = MUST(Core::File::open("./vp9_in_webm.webm"sv, Core::File::OpenMode::Read)); + auto stream = Media::IncrementallyPopulatedStream::create_from_buffer(MUST(file->read_until_eof())); + auto matroska_reader = MUST(Media::Matroska::Reader::from_stream(stream->create_cursor())); + + u64 audio_track = 0; + MUST(matroska_reader.for_each_track_of_type(Media::Matroska::TrackEntry::TrackType::Audio, [&](Media::Matroska::TrackEntry const& track_entry) -> Media::DecoderErrorOr { + audio_track = track_entry.track_number(); + return IterationDecision::Break; + })); + EXPECT_NE(audio_track, 0u); + + auto iterator = MUST(matroska_reader.create_sample_iterator(stream->create_cursor(), audio_track)); + + // WebM files typically don't specify a default frame duration. However, we parse the Opus frame header + // to get the duration, so the reader should return durations of 20ms. + auto expected_duration = AK::Duration::from_milliseconds(20); + for (int i = 0; i < 5; i++) { + auto block = MUST(iterator.next_block()); + VERIFY(block.duration().has_value()); + EXPECT_EQ(block.duration().value(), expected_duration); + } +} diff --git a/Tests/LibMedia/vp9_in_webm.webm b/Tests/LibMedia/vp9_in_webm.webm index 7db562a22ef12d818b491ad3ea0c380d331f8ed8..ab2d04e22a2964736b5cd1eeaaf321befc014f6e 100644 GIT binary patch delta 10350 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

GQoGY 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