From c901896972083b03eea712084fb26cb25725e739 Mon Sep 17 00:00:00 2001 From: Zaggy1024 Date: Thu, 14 May 2026 20:51:21 -0500 Subject: [PATCH] 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. --- Libraries/LibMedia/CMakeLists.txt | 2 + Libraries/LibMedia/Codecs/Opus.cpp | 9 +- Libraries/LibMedia/Codecs/Opus.h | 1 + Libraries/LibMedia/Codecs/Vorbis.cpp | 77 ++++ Libraries/LibMedia/Codecs/Vorbis.h | 41 ++ .../LibMedia/Containers/OggNavigator.cpp | 401 ++++++++++++++++++ Libraries/LibMedia/Containers/OggNavigator.h | 78 ++++ Libraries/LibMedia/FFmpeg/FFmpegDemuxer.cpp | 32 ++ 8 files changed, 639 insertions(+), 2 deletions(-) create mode 100644 Libraries/LibMedia/Codecs/Vorbis.cpp create mode 100644 Libraries/LibMedia/Codecs/Vorbis.h create mode 100644 Libraries/LibMedia/Containers/OggNavigator.cpp create mode 100644 Libraries/LibMedia/Containers/OggNavigator.h diff --git a/Libraries/LibMedia/CMakeLists.txt b/Libraries/LibMedia/CMakeLists.txt index b1e2ab953b..e779e064ff 100644 --- a/Libraries/LibMedia/CMakeLists.txt +++ b/Libraries/LibMedia/CMakeLists.txt @@ -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 diff --git a/Libraries/LibMedia/Codecs/Opus.cpp b/Libraries/LibMedia/Codecs/Opus.cpp index 9d8c5d83c8..96d47c74fc 100644 --- a/Libraries/LibMedia/Codecs/Opus.cpp +++ b/Libraries/LibMedia/Codecs/Opus.cpp @@ -9,7 +9,7 @@ namespace Media::Codecs { -DecoderErrorOr Opus::parse_frame_duration(MediaStreamCursor& cursor, size_t frame_size) +DecoderErrorOr 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 Opus::parse_frame_duration(MediaStreamCursor& curso } }()); - return AK::Duration::from_microseconds(packet_duration); + return packet_duration * 48 / 1000; +} + +DecoderErrorOr 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); } } diff --git a/Libraries/LibMedia/Codecs/Opus.h b/Libraries/LibMedia/Codecs/Opus.h index 5ae365d542..93ac4f607a 100644 --- a/Libraries/LibMedia/Codecs/Opus.h +++ b/Libraries/LibMedia/Codecs/Opus.h @@ -15,6 +15,7 @@ namespace Media::Codecs { class Opus { public: static DecoderErrorOr parse_frame_duration(MediaStreamCursor&, size_t frame_size); + static DecoderErrorOr parse_frame_duration_in_samples(MediaStreamCursor&, size_t frame_size); }; } diff --git a/Libraries/LibMedia/Codecs/Vorbis.cpp b/Libraries/LibMedia/Codecs/Vorbis.cpp new file mode 100644 index 0000000000..cfc8d0835f --- /dev/null +++ b/Libraries/LibMedia/Codecs/Vorbis.cpp @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2026-present, the Ladybird developers. + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include + +extern "C" { +#include +} + +namespace Media::Codecs { + +Optional 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(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 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(packet.size()), &flags); + if (duration < 0) + return {}; + return duration; +} + +Optional 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); +} + +} diff --git a/Libraries/LibMedia/Codecs/Vorbis.h b/Libraries/LibMedia/Codecs/Vorbis.h new file mode 100644 index 0000000000..f5e81a662d --- /dev/null +++ b/Libraries/LibMedia/Codecs/Vorbis.h @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2026-present, the Ladybird developers. + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include +#include + +struct AVVorbisParseContext; + +namespace Media::Codecs { + +class Vorbis { +public: + class Parser { + AK_MAKE_NONCOPYABLE(Parser); + + public: + static Optional create(ReadonlyBytes codec_initialization_data); + + Parser(Parser&&); + Parser& operator=(Parser&&); + ~Parser(); + + void reset() const; + Optional parse_packet_duration_in_samples(ReadonlyBytes packet) const; + Optional parse_packet_duration(ReadonlyBytes packet, u32 sample_rate) const; + + private: + explicit Parser(AVVorbisParseContext*); + + AVVorbisParseContext* m_context { nullptr }; + }; +}; + +} diff --git a/Libraries/LibMedia/Containers/OggNavigator.cpp b/Libraries/LibMedia/Containers/OggNavigator.cpp new file mode 100644 index 0000000000..cfd7e22a63 --- /dev/null +++ b/Libraries/LibMedia/Containers/OggNavigator.cpp @@ -0,0 +1,401 @@ +/* + * Copyright (c) 2026-present, the Ladybird developers. + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include "OggNavigator.h" + +#include +#include +#include +#include + +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 cursor, CodecData codec_data) + : m_cursor(move(cursor)) + , m_codec_data(move(codec_data)) +{ +} + +OwnPtr OggNavigator::create(ReadonlyBytes first_packet, NonnullRefPtr 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(first_packet[0]) << 8) | first_packet[1]; + if (!Codecs::FLAC::is_sync_code(sync_code)) + return {}; + + auto frame_cursor = make_ref_counted(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(0, granule_position), codec.time_base_numerator, codec.time_base_denominator); + }, + [&](FLAC const& codec) { + return AK::Duration::from_time_units(max(0, granule_position), 1, codec.sample_rate); + }, + [&](Vorbis const& codec) { + return AK::Duration::from_time_units(max(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(page[checksum_start]) + | (static_cast(page[checksum_start + 1]) << 8) + | (static_cast(page[checksum_start + 2]) << 16) + | (static_cast(page[checksum_start + 3]) << 24); + + constexpr auto lookup_table = [] { + Array 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 OggNavigator::read_packet_duration_in_granules(ReadonlyBytes packet) const +{ + auto packet_cursor = make_ref_counted(packet); + return m_codec_data.visit( + [&](Opus const&) -> Optional { + 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 { + 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 { + auto packet_duration = codec.parser.parse_packet_duration_in_samples(packet); + if (!packet_duration.has_value()) + return {}; + return packet_duration.value(); + }); +} + +Optional 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(m_page_buffer[6 + i]) << (8 * i); + auto granule_position = bit_cast(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 OggNavigator::find_start_timestamp(size_t search_start, size_t search_end) const +{ + VERIFY(search_start <= search_end); + + Array 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(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::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 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 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(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); +} + +} diff --git a/Libraries/LibMedia/Containers/OggNavigator.h b/Libraries/LibMedia/Containers/OggNavigator.h new file mode 100644 index 0000000000..4409aa92aa --- /dev/null +++ b/Libraries/LibMedia/Containers/OggNavigator.h @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2026-present, the Ladybird developers. + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "ScanningContainerNavigator.h" + +namespace Media { + +struct OggCachedRange : CachedByteRange { + using CachedByteRange::CachedByteRange; + + Optional time_start; + Optional time_end; +}; + +class OggNavigator final : public ScanningContainerNavigator { +public: + static OwnPtr create(ReadonlyBytes first_packet, NonnullRefPtr, 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; + + struct PageScanResult { + Optional granule_position; + u64 parsed_packet_duration_in_granules; + size_t byte_start; + size_t byte_end; + }; + + OggNavigator(NonnullRefPtr, CodecData); + + Optional find_start_timestamp(size_t search_start, size_t search_end) const; + Optional find_last_page_with_valid_granule_position(size_t search_start, size_t search_end) const; + Optional find_end_timestamp(size_t search_start, size_t search_end) const; + Optional parse_page_at(size_t page_start, size_t search_end, ByteBuffer* continued_packet = nullptr) const; + Optional read_packet_duration_in_granules(ReadonlyBytes) const; + void reset_packet_parser_state() const; + + AK::Duration granule_to_time(i64 granule_position) const; + + NonnullRefPtr m_cursor; + CodecData m_codec_data; + mutable ByteBuffer m_page_buffer; +}; + +} diff --git a/Libraries/LibMedia/FFmpeg/FFmpegDemuxer.cpp b/Libraries/LibMedia/FFmpeg/FFmpegDemuxer.cpp index 71455121fa..fa9632d3d6 100644 --- a/Libraries/LibMedia/FFmpeg/FFmpegDemuxer.cpp +++ b/Libraries/LibMedia/FFmpeg/FFmpegDemuxer.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -241,6 +242,37 @@ OwnPtr FFmpegDemuxer::create_container_navigator(AVFormatCon return make(stream, static_cast(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(packet->size) }; + } + + auto sample_rate = av_stream.codecpar->sample_rate > 0 ? static_cast(av_stream.codecpar->sample_rate) : 0; + auto codec_initialization_data = ReadonlyBytes { av_stream.codecpar->extradata, static_cast(av_stream.codecpar->extradata_size) }; + return OggNavigator::create(first_packet, move(cursor), codec_id, static_cast(av_stream.time_base.num), static_cast(av_stream.time_base.den), sample_rate, codec_initialization_data); + } + return create_container_navigator_from_index(context); }