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.
This commit is contained in:
Zaggy1024
2026-03-20 02:51:02 -05:00
committed by Gregory Bertilson
parent 6b45a11716
commit d6f821f22d
Notes: github-actions[bot] 2026-04-01 07:58:50 +00:00
3 changed files with 84 additions and 1 deletions

View File

@@ -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<i64>(context.default_duration)));
}
static DecoderErrorOr<void> 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<int> {
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<Block> Reader::parse_simple_block(Streamer& streamer, AK::Duration cluster_timestamp, u64 segment_timestamp_scale, TrackBlockContexts const& contexts)
{
Block block;
@@ -810,6 +860,8 @@ DecoderErrorOr<Block> 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<Block> 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<Block> Reader::parse_block_group(Streamer& streamer, AK::Duration
if (context.timestamp_scale != 1)
duration_nanoseconds = AK::clamp_to<i64>(static_cast<double>(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));
}
}

View File

@@ -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<IterationDecision> {
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);
}
}

Binary file not shown.