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:
parent
76a5c2b91e
commit
c901896972
8 changed files with 639 additions and 2 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
|||
77
Libraries/LibMedia/Codecs/Vorbis.cpp
Normal file
77
Libraries/LibMedia/Codecs/Vorbis.cpp
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
41
Libraries/LibMedia/Codecs/Vorbis.h
Normal file
41
Libraries/LibMedia/Codecs/Vorbis.h
Normal 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 };
|
||||
};
|
||||
};
|
||||
|
||||
}
|
||||
401
Libraries/LibMedia/Containers/OggNavigator.cpp
Normal file
401
Libraries/LibMedia/Containers/OggNavigator.cpp
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
78
Libraries/LibMedia/Containers/OggNavigator.h
Normal file
78
Libraries/LibMedia/Containers/OggNavigator.h
Normal 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;
|
||||
};
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue