LibMedia: Implement buffered range scanning for Ogg

Unfortunately, this necessarily involves parsing codec frames to get
their durations. Ogg's granule positions indicate the last sample of
the last complete frame of the page, so the navigator has to determine
the durations of every packet in the page to offset it back to its
start.

Vorbis is an especially involved codec to determine that for, since it
has initial data defining a mapping of indices to big or small block
sizes that has to be held onto, and that mapping is preceded by a bunch
of conditionally parsed bits. Also, a frame's block size calculation
involves the block size of the previous frame for an overlap add. To
avoid bringing that complexity directly into LibMedia, we delegate the
parsing/calculation to FFmpeg's av_vorbis_parse functions.
This commit is contained in:
Zaggy1024 2026-05-14 20:51:21 -05:00 committed by Gregory Bertilson
parent 76a5c2b91e
commit c901896972
8 changed files with 639 additions and 2 deletions

View file

@ -4,10 +4,12 @@ set(SOURCES
Audio/AudioDevices.cpp
Codecs/FLAC.cpp
Codecs/Opus.cpp
Codecs/Vorbis.cpp
Containers/ConstantBitrateContainerNavigator.cpp
Containers/FLACNavigator.cpp
Containers/IndexedContainerNavigator.cpp
Containers/MP3Navigator.cpp
Containers/OggNavigator.cpp
Containers/Matroska/MatroskaDemuxer.cpp
Containers/Matroska/Reader.cpp
Containers/Matroska/SampleIterator.cpp

View file

@ -9,7 +9,7 @@
namespace Media::Codecs {
DecoderErrorOr<AK::Duration> Opus::parse_frame_duration(MediaStreamCursor& cursor, size_t frame_size)
DecoderErrorOr<u32> Opus::parse_frame_duration_in_samples(MediaStreamCursor& cursor, size_t frame_size)
{
if (frame_size == 0)
return DecoderError::corrupted("Opus frame is too small"sv);
@ -53,7 +53,12 @@ DecoderErrorOr<AK::Duration> Opus::parse_frame_duration(MediaStreamCursor& curso
}
}());
return AK::Duration::from_microseconds(packet_duration);
return packet_duration * 48 / 1000;
}
DecoderErrorOr<AK::Duration> Opus::parse_frame_duration(MediaStreamCursor& cursor, size_t frame_size)
{
return AK::Duration::from_time_units(TRY(parse_frame_duration_in_samples(cursor, frame_size)), 1, 48000);
}
}

View file

@ -15,6 +15,7 @@ namespace Media::Codecs {
class Opus {
public:
static DecoderErrorOr<AK::Duration> parse_frame_duration(MediaStreamCursor&, size_t frame_size);
static DecoderErrorOr<u32> parse_frame_duration_in_samples(MediaStreamCursor&, size_t frame_size);
};
}

View file

@ -0,0 +1,77 @@
/*
* Copyright (c) 2026-present, the Ladybird developers.
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/Math.h>
#include <LibMedia/Codecs/Vorbis.h>
extern "C" {
#include <libavcodec/vorbis_parser.h>
}
namespace Media::Codecs {
Optional<Vorbis::Parser> Vorbis::Parser::create(ReadonlyBytes codec_initialization_data)
{
if (codec_initialization_data.is_empty())
return {};
auto* parser = av_vorbis_parse_init(codec_initialization_data.data(), AK::clamp_to<int>(codec_initialization_data.size()));
if (!parser)
return {};
return Parser { parser };
}
Vorbis::Parser::Parser(AVVorbisParseContext* context)
: m_context(context)
{
VERIFY(m_context);
}
Vorbis::Parser::Parser(Parser&& other)
: m_context(exchange(other.m_context, nullptr))
{
}
Vorbis::Parser& Vorbis::Parser::operator=(Parser&& other)
{
if (this == &other)
return *this;
if (m_context)
av_vorbis_parse_free(&m_context);
m_context = exchange(other.m_context, nullptr);
return *this;
}
Vorbis::Parser::~Parser()
{
if (m_context)
av_vorbis_parse_free(&m_context);
}
void Vorbis::Parser::reset() const
{
av_vorbis_parse_reset(m_context);
}
Optional<u32> Vorbis::Parser::parse_packet_duration_in_samples(ReadonlyBytes packet) const
{
int flags = 0;
auto duration = av_vorbis_parse_frame_flags(m_context, packet.data(), AK::clamp_to<int>(packet.size()), &flags);
if (duration < 0)
return {};
return duration;
}
Optional<AK::Duration> Vorbis::Parser::parse_packet_duration(ReadonlyBytes packet, u32 sample_rate) const
{
auto duration = parse_packet_duration_in_samples(packet);
if (!duration.has_value())
return {};
return AK::Duration::from_time_units(duration.value(), 1, sample_rate);
}
}

View file

@ -0,0 +1,41 @@
/*
* Copyright (c) 2026-present, the Ladybird developers.
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Noncopyable.h>
#include <AK/Optional.h>
#include <AK/Span.h>
#include <AK/Time.h>
struct AVVorbisParseContext;
namespace Media::Codecs {
class Vorbis {
public:
class Parser {
AK_MAKE_NONCOPYABLE(Parser);
public:
static Optional<Parser> create(ReadonlyBytes codec_initialization_data);
Parser(Parser&&);
Parser& operator=(Parser&&);
~Parser();
void reset() const;
Optional<u32> parse_packet_duration_in_samples(ReadonlyBytes packet) const;
Optional<AK::Duration> parse_packet_duration(ReadonlyBytes packet, u32 sample_rate) const;
private:
explicit Parser(AVVorbisParseContext*);
AVVorbisParseContext* m_context { nullptr };
};
};
}

View file

@ -0,0 +1,401 @@
/*
* Copyright (c) 2026-present, the Ladybird developers.
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include "OggNavigator.h"
#include <AK/BitCast.h>
#include <AK/Math.h>
#include <LibMedia/Codecs/Opus.h>
#include <LibMedia/ReadonlyBytesCursor.h>
namespace Media {
static constexpr StringView CAPTURE_PATTERN = "OggS"sv;
static constexpr size_t CAPTURE_PATTERN_SIZE = CAPTURE_PATTERN.length();
static constexpr size_t PAGE_HEADER_SIZE = 27;
static constexpr size_t CHUNK_SIZE = 4096;
static constexpr u8 CONTINUED_PACKET_FLAG = 1 << 0;
static constexpr i64 UNSET_GRANULE_POSITION = -1;
OggNavigator::OggNavigator(NonnullRefPtr<MediaStreamCursor> cursor, CodecData codec_data)
: m_cursor(move(cursor))
, m_codec_data(move(codec_data))
{
}
OwnPtr<OggNavigator> OggNavigator::create(ReadonlyBytes first_packet, NonnullRefPtr<MediaStreamCursor> cursor, CodecID codec_id, u32 time_base_numerator, u32 time_base_denominator, u32 sample_rate, ReadonlyBytes codec_initialization_data)
{
VERIFY(time_base_numerator > 0);
VERIFY(time_base_denominator > 0);
switch (codec_id) {
case CodecID::Opus:
return adopt_own(*new (nothrow) OggNavigator(move(cursor), Opus { time_base_numerator, time_base_denominator }));
case CodecID::FLAC: {
if (sample_rate == 0 || first_packet.size() < 2)
return {};
u16 sync_code = (static_cast<u16>(first_packet[0]) << 8) | first_packet[1];
if (!Codecs::FLAC::is_sync_code(sync_code))
return {};
auto frame_cursor = make_ref_counted<ReadonlyBytesCursor>(first_packet);
auto frame_info = Codecs::FLAC::parse_frame_header(frame_cursor, sync_code, 0);
if (!frame_info.has_value())
return {};
u16 fixed_block_size = 0;
if ((sync_code & 1) == 0)
fixed_block_size = frame_info->block_size;
return adopt_own(*new (nothrow) OggNavigator(move(cursor), FLAC { sync_code, fixed_block_size, sample_rate }));
}
case CodecID::Vorbis: {
if (sample_rate == 0)
return {};
if (codec_initialization_data.is_empty())
return {};
auto parser = Codecs::Vorbis::Parser::create(codec_initialization_data);
if (!parser.has_value())
return {};
return adopt_own(*new (nothrow) OggNavigator(move(cursor), Vorbis { parser.release_value(), sample_rate }));
}
default:
return {};
}
}
AK::Duration OggNavigator::granule_to_time(i64 granule_position) const
{
return m_codec_data.visit(
[&](Opus const& codec) {
return AK::Duration::from_time_units(max<i64>(0, granule_position), codec.time_base_numerator, codec.time_base_denominator);
},
[&](FLAC const& codec) {
return AK::Duration::from_time_units(max<i64>(0, granule_position), 1, codec.sample_rate);
},
[&](Vorbis const& codec) {
return AK::Duration::from_time_units(max<i64>(0, granule_position), 1, codec.sample_rate);
});
}
void OggNavigator::reset_packet_parser_state() const
{
m_codec_data.visit(
[](Opus const&) {},
[](FLAC const&) {},
[](Vorbis const& codec) {
codec.parser.reset();
});
}
static bool is_capture_pattern(ReadonlyBytes bytes)
{
return bytes.starts_with(CAPTURE_PATTERN.bytes());
}
static bool append_bytes(ByteBuffer& buffer, ReadonlyBytes bytes)
{
auto original_size = buffer.size();
if (buffer.try_resize(original_size + bytes.size()).is_error())
return false;
bytes.copy_to(buffer.span().slice(original_size));
return true;
}
static bool verify_page_crc(ReadonlyBytes page)
{
constexpr size_t checksum_start = 22;
constexpr size_t checksum_end = checksum_start + 4;
VERIFY(page.size() >= checksum_end);
auto expected_crc = static_cast<u32>(page[checksum_start])
| (static_cast<u32>(page[checksum_start + 1]) << 8)
| (static_cast<u32>(page[checksum_start + 2]) << 16)
| (static_cast<u32>(page[checksum_start + 3]) << 24);
constexpr auto lookup_table = [] {
Array<u32, 256> result;
for (size_t i = 0; i < result.size(); i++) {
u32 value = i << 24;
for (auto bit = 0; bit < 8; bit++)
value = (value << 1) ^ (value & 0x80000000 ? 0x04C11DB7 : 0);
result[i] = value;
}
return result;
}();
u32 actual_crc = 0;
for (size_t i = 0; i < page.size(); i++) {
u8 byte = 0;
if (i < checksum_start || i >= checksum_end)
byte = page[i];
actual_crc = (actual_crc << 8) ^ lookup_table[((actual_crc >> 24) & 0xFF) ^ byte];
}
return expected_crc == actual_crc;
}
Optional<u64> OggNavigator::read_packet_duration_in_granules(ReadonlyBytes packet) const
{
auto packet_cursor = make_ref_counted<ReadonlyBytesCursor>(packet);
return m_codec_data.visit(
[&](Opus const&) -> Optional<u64> {
auto parsed_duration = Codecs::Opus::parse_frame_duration_in_samples(packet_cursor, packet.size());
if (parsed_duration.is_error())
return {};
return parsed_duration.release_value();
},
[&](FLAC const& codec) -> Optional<u64> {
auto frame = Codecs::FLAC::parse_frame_header(packet_cursor, codec.sync_code, codec.fixed_block_size);
if (!frame.has_value())
return {};
return frame->block_size;
},
[&](Vorbis const& codec) -> Optional<u64> {
auto packet_duration = codec.parser.parse_packet_duration_in_samples(packet);
if (!packet_duration.has_value())
return {};
return packet_duration.value();
});
}
Optional<OggNavigator::PageScanResult> OggNavigator::parse_page_at(size_t page_start, size_t search_end, ByteBuffer* continued_packet) const
{
if (page_start + PAGE_HEADER_SIZE > search_end)
return {};
if (m_cursor->seek_to_position(page_start).is_error())
return {};
auto resize_page_buffer = [&](size_t size) {
m_page_buffer.ensure_capacity(size);
m_page_buffer.set_size(size);
};
resize_page_buffer(PAGE_HEADER_SIZE);
if (m_cursor->read_until_filled(m_page_buffer.span()).is_error())
return {};
if (!is_capture_pattern(m_page_buffer))
return {};
auto version = m_page_buffer[4];
if (version != 0)
return {};
auto header_type_flag = m_page_buffer[5];
bool begins_with_continued_packet = (header_type_flag & CONTINUED_PACKET_FLAG) != 0;
u64 granule_position_bits { 0 };
for (size_t i = 0; i < sizeof(granule_position_bits); i++)
granule_position_bits |= static_cast<u64>(m_page_buffer[6 + i]) << (8 * i);
auto granule_position = bit_cast<i64>(granule_position_bits);
auto page_segments = m_page_buffer[26];
if (page_start + PAGE_HEADER_SIZE + page_segments > search_end)
return {};
resize_page_buffer(PAGE_HEADER_SIZE + page_segments);
auto segment_table = m_page_buffer.span().slice(PAGE_HEADER_SIZE);
if (m_cursor->read_until_filled(segment_table).is_error())
return {};
size_t payload_size = 0;
for (auto segment_size : segment_table)
payload_size += segment_size;
auto payload_start = page_start + PAGE_HEADER_SIZE + page_segments;
auto payload_end = payload_start + payload_size;
if (payload_end > search_end)
return {};
auto page_size = PAGE_HEADER_SIZE + page_segments + payload_size;
resize_page_buffer(page_size);
segment_table = m_page_buffer.span().slice(PAGE_HEADER_SIZE, page_segments);
auto payload = m_page_buffer.span().slice(PAGE_HEADER_SIZE + page_segments);
if (m_cursor->read_until_filled(payload).is_error())
return {};
if (!verify_page_crc(m_page_buffer))
return {};
u64 parsed_packet_duration_in_granules { 0 };
size_t packet_size = 0;
size_t packet_start = 0;
bool packet_started_before_page = begins_with_continued_packet;
for (auto segment_size : segment_table) {
packet_size += segment_size;
if (segment_size == 255)
continue;
auto packet = payload.slice(packet_start, packet_size);
if (packet_started_before_page) {
if (continued_packet && !continued_packet->is_empty()) {
if (!append_bytes(*continued_packet, packet))
return {};
auto packet_duration = read_packet_duration_in_granules(continued_packet->span());
continued_packet->clear();
if (!packet_duration.has_value())
return {};
parsed_packet_duration_in_granules += packet_duration.value();
}
packet_started_before_page = false;
packet_start += packet_size;
packet_size = 0;
continue;
}
auto packet_duration = read_packet_duration_in_granules(packet);
if (!packet_duration.has_value())
return {};
parsed_packet_duration_in_granules += packet_duration.value();
packet_start += packet_size;
packet_size = 0;
}
if (continued_packet && packet_size > 0 && (!packet_started_before_page || !continued_packet->is_empty())) {
if (!append_bytes(*continued_packet, payload.slice(packet_start, packet_size)))
return {};
}
if (granule_position == UNSET_GRANULE_POSITION)
return PageScanResult { {}, parsed_packet_duration_in_granules, page_start, payload_end };
return PageScanResult { granule_position, parsed_packet_duration_in_granules, page_start, payload_end };
}
Optional<AK::Duration> OggNavigator::find_start_timestamp(size_t search_start, size_t search_end) const
{
VERIFY(search_start <= search_end);
Array<u8, CHUNK_SIZE> chunk;
auto chunk_start = search_start;
while (chunk_start + CAPTURE_PATTERN_SIZE <= search_end) {
auto chunk_size = min(chunk.size(), search_end - chunk_start);
if (m_cursor->seek_to_position(chunk_start).is_error())
return {};
auto read_result = m_cursor->read_into(chunk.span().trim(chunk_size));
if (read_result.is_error())
return {};
auto bytes_read = read_result.value();
if (bytes_read < CAPTURE_PATTERN_SIZE)
return {};
for (size_t i = 0; i + CAPTURE_PATTERN_SIZE <= bytes_read; i++) {
if (!is_capture_pattern(chunk.span().slice(i)))
continue;
reset_packet_parser_state();
auto page = parse_page_at(chunk_start + i, search_end);
if (!page.has_value())
continue;
if (!page->granule_position.has_value())
continue;
auto parsed_packet_duration_in_granules = static_cast<i64>(page->parsed_packet_duration_in_granules);
if (page->granule_position.value() <= parsed_packet_duration_in_granules)
return AK::Duration::zero();
return granule_to_time(page->granule_position.value() - parsed_packet_duration_in_granules);
}
chunk_start += bytes_read - (CAPTURE_PATTERN_SIZE - 1);
}
return {};
}
Optional<OggNavigator::PageScanResult> OggNavigator::find_last_page_with_valid_granule_position(size_t search_start, size_t search_end) const
{
VERIFY(search_start <= search_end);
auto chunk_end = search_end;
Array<u8, CHUNK_SIZE> chunk;
while (chunk_end >= search_start + CAPTURE_PATTERN_SIZE) {
auto chunk_start = chunk_end > chunk.size() ? chunk_end - chunk.size() : 0;
chunk_start = max(chunk_start, search_start);
auto chunk_size = chunk_end - chunk_start;
if (m_cursor->seek_to_position(chunk_start).is_error())
return {};
if (m_cursor->read_until_filled(chunk.span().trim(chunk_size)).is_error())
return {};
for (size_t i = chunk_size - CAPTURE_PATTERN_SIZE + 1; i-- > 0;) {
if (!is_capture_pattern(chunk.span().slice(i)))
continue;
reset_packet_parser_state();
auto page = parse_page_at(chunk_start + i, search_end);
if (page.has_value() && page->granule_position.has_value())
return page;
}
if (chunk_start == search_start)
break;
chunk_end = chunk_start + CAPTURE_PATTERN_SIZE - 1;
}
return {};
}
Optional<AK::Duration> OggNavigator::find_end_timestamp(size_t search_start, size_t search_end) const
{
auto last_complete_page = find_last_page_with_valid_granule_position(search_start, search_end);
if (!last_complete_page.has_value())
return {};
ByteBuffer continued_packet;
reset_packet_parser_state();
auto page = parse_page_at(last_complete_page->byte_start, search_end, &continued_packet);
if (!page.has_value() || !page->granule_position.has_value())
return granule_to_time(last_complete_page->granule_position.value());
auto end_granule_position = page->granule_position.value();
auto page_start = page->byte_end;
while (page_start + CAPTURE_PATTERN_SIZE <= search_end) {
page = parse_page_at(page_start, search_end, &continued_packet);
if (!page.has_value())
break;
if (page->granule_position.has_value())
end_granule_position = page->granule_position.value();
else
end_granule_position += static_cast<i64>(page->parsed_packet_duration_in_granules);
if (page->byte_end >= search_end)
break;
page_start = page->byte_end;
}
return granule_to_time(end_granule_position);
}
void OggNavigator::on_cached_range_changed(OggCachedRange& cached, CachedRangeChange change) const
{
bool rescan_start = has_flag(change, CachedRangeChange::Start);
bool rescan_end = has_flag(change, CachedRangeChange::End);
if (rescan_start && !cached.time_end.has_value())
rescan_end = true;
if (rescan_end && !cached.time_start.has_value())
rescan_start = true;
if (rescan_start)
cached.time_start = find_start_timestamp(cached.byte_start, cached.byte_end);
if (rescan_end)
cached.time_end = find_end_timestamp(cached.byte_start, cached.byte_end);
}
void OggNavigator::append_time_range(OggCachedRange const& cached_range, TimeRanges& to)
{
if (!cached_range.time_start.has_value() || !cached_range.time_end.has_value())
return;
auto time_start = max(cached_range.time_start.value(), AK::Duration::zero());
auto time_end = cached_range.time_end.value();
if (time_start >= time_end)
return;
to.add_range(time_start, time_end);
}
}

View file

@ -0,0 +1,78 @@
/*
* Copyright (c) 2026-present, the Ladybird developers.
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/ByteBuffer.h>
#include <AK/NonnullRefPtr.h>
#include <AK/Optional.h>
#include <AK/OwnPtr.h>
#include <AK/Variant.h>
#include <LibMedia/CodecID.h>
#include <LibMedia/Codecs/FLAC.h>
#include <LibMedia/Codecs/Vorbis.h>
#include "ScanningContainerNavigator.h"
namespace Media {
struct OggCachedRange : CachedByteRange {
using CachedByteRange::CachedByteRange;
Optional<AK::Duration> time_start;
Optional<AK::Duration> time_end;
};
class OggNavigator final : public ScanningContainerNavigator<OggNavigator, OggCachedRange> {
public:
static OwnPtr<OggNavigator> create(ReadonlyBytes first_packet, NonnullRefPtr<MediaStreamCursor>, CodecID, u32 time_base_numerator, u32 time_base_denominator, u32 sample_rate, ReadonlyBytes codec_initialization_data);
void on_cached_range_changed(OggCachedRange& cached_range, CachedRangeChange change) const;
static void append_time_range(OggCachedRange const& cached_range, TimeRanges& to);
private:
struct Opus {
u32 time_base_numerator { 1 };
u32 time_base_denominator { 1 };
};
struct FLAC {
u16 sync_code { 0 };
u16 fixed_block_size { 0 };
u32 sample_rate { 1 };
};
struct Vorbis {
Codecs::Vorbis::Parser parser;
u32 sample_rate { 1 };
};
using CodecData = Variant<Opus, FLAC, Vorbis>;
struct PageScanResult {
Optional<i64> granule_position;
u64 parsed_packet_duration_in_granules;
size_t byte_start;
size_t byte_end;
};
OggNavigator(NonnullRefPtr<MediaStreamCursor>, CodecData);
Optional<AK::Duration> find_start_timestamp(size_t search_start, size_t search_end) const;
Optional<PageScanResult> find_last_page_with_valid_granule_position(size_t search_start, size_t search_end) const;
Optional<AK::Duration> find_end_timestamp(size_t search_start, size_t search_end) const;
Optional<PageScanResult> parse_page_at(size_t page_start, size_t search_end, ByteBuffer* continued_packet = nullptr) const;
Optional<u64> read_packet_duration_in_granules(ReadonlyBytes) const;
void reset_packet_parser_state() const;
AK::Duration granule_to_time(i64 granule_position) const;
NonnullRefPtr<MediaStreamCursor> m_cursor;
CodecData m_codec_data;
mutable ByteBuffer m_page_buffer;
};
}

View file

@ -13,6 +13,7 @@
#include <LibMedia/Containers/FLACNavigator.h>
#include <LibMedia/Containers/IndexedContainerNavigator.h>
#include <LibMedia/Containers/MP3Navigator.h>
#include <LibMedia/Containers/OggNavigator.h>
#include <LibMedia/FFmpeg/FFmpegDemuxer.h>
#include <LibMedia/FFmpeg/FFmpegHelpers.h>
#include <LibMedia/MediaStream.h>
@ -241,6 +242,37 @@ OwnPtr<ContainerNavigator> FFmpegDemuxer::create_container_navigator(AVFormatCon
return make<MP3Navigator>(stream, static_cast<size_t>(packet->pos), total_duration);
}
if (format_name == "ogg"sv) {
if (context.nb_streams != 1)
return nullptr;
auto& av_stream = *context.streams[0];
if (av_stream.time_base.num <= 0 || av_stream.time_base.den <= 0)
return nullptr;
auto cursor = stream->create_cursor();
cursor->set_is_blocking(false);
auto codec_id = FFmpeg::media_codec_id_from_ffmpeg_codec_id(av_stream.codecpar->codec_id);
ReadonlyBytes first_packet;
AVPacket* packet = nullptr;
ScopeGuard free_packet = [&] { av_packet_free(&packet); };
if (codec_id == CodecID::FLAC) {
if (av_stream.codecpar->sample_rate <= 0)
return nullptr;
packet = av_packet_alloc();
VERIFY(packet);
if (av_read_frame(&context, packet) >= 0 && packet->size >= 0)
first_packet = { packet->data, static_cast<size_t>(packet->size) };
}
auto sample_rate = av_stream.codecpar->sample_rate > 0 ? static_cast<u32>(av_stream.codecpar->sample_rate) : 0;
auto codec_initialization_data = ReadonlyBytes { av_stream.codecpar->extradata, static_cast<size_t>(av_stream.codecpar->extradata_size) };
return OggNavigator::create(first_packet, move(cursor), codec_id, static_cast<u32>(av_stream.time_base.num), static_cast<u32>(av_stream.time_base.den), sample_rate, codec_initialization_data);
}
return create_container_navigator_from_index(context);
}