From 16f5a3405f03deaf5f39f39df41af395837c259e Mon Sep 17 00:00:00 2001 From: Zaggy1024 Date: Tue, 12 May 2026 15:31:34 -0500 Subject: [PATCH] LibMedia: Implement buffered ranges for MP3 A new MP3Navigator class is added, which determines timestamps for byte positions by resyncing to a frame and then interpolating between known points on either side. The known points start out as the first frame's position in the file at timestamp 0, and EOF at the timestamp for FFmpeg's file duration estimate. New buffered ranges are interpolated between those two points, but also between the end of a prior range and the start of the next. Since MP3 can have variable bitrate without declaring it in the file header, we have to allow buffered ranges to shift forward as new data arrives to make room for underestimated durations. This is done for all ranges following the first that has been appended to, keeping the start of the current range consistent, so that subsequent seeks within that range remain consistent. Seeking is also implemented within the navigator to ensure that the byte<->timestamp mapping is consistent and the buffered ranges begin exactly where the seek landed. --- Libraries/LibMedia/CMakeLists.txt | 1 + .../LibMedia/Containers/MP3Navigator.cpp | 630 ++++++++++++++++++ Libraries/LibMedia/Containers/MP3Navigator.h | 47 ++ Libraries/LibMedia/FFmpeg/FFmpegDemuxer.cpp | 15 +- Libraries/LibMedia/FFmpeg/FFmpegDemuxer.h | 2 +- .../LibMedia/IncrementallyPopulatedStream.h | 2 +- Libraries/LibMedia/MediaStream.h | 2 + 7 files changed, 694 insertions(+), 5 deletions(-) create mode 100644 Libraries/LibMedia/Containers/MP3Navigator.cpp create mode 100644 Libraries/LibMedia/Containers/MP3Navigator.h diff --git a/Libraries/LibMedia/CMakeLists.txt b/Libraries/LibMedia/CMakeLists.txt index 9c1e87da3a..b44de61334 100644 --- a/Libraries/LibMedia/CMakeLists.txt +++ b/Libraries/LibMedia/CMakeLists.txt @@ -6,6 +6,7 @@ set(SOURCES Containers/ConstantBitrateContainerNavigator.cpp Containers/FLACNavigator.cpp Containers/IndexedContainerNavigator.cpp + Containers/MP3Navigator.cpp Containers/Matroska/MatroskaDemuxer.cpp Containers/Matroska/Reader.cpp Containers/Matroska/SampleIterator.cpp diff --git a/Libraries/LibMedia/Containers/MP3Navigator.cpp b/Libraries/LibMedia/Containers/MP3Navigator.cpp new file mode 100644 index 0000000000..ef08cd37dc --- /dev/null +++ b/Libraries/LibMedia/Containers/MP3Navigator.cpp @@ -0,0 +1,630 @@ +/* + * Copyright (c) 2026-present, the Ladybird developers. + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include "MP3Navigator.h" + +#include +#include +#include +#include + +namespace Media { + +static bool read_exact(MediaStreamCursor& cursor, Bytes buffer) +{ + auto result = cursor.read_into(buffer); + return !result.is_error() && result.value() == buffer.size(); +} + +template +static bool read(MediaStreamCursor& cursor, V& value) +{ + T read_value = 0; + bool result = read_exact(cursor, { &read_value, sizeof(read_value) }); + value = AK::convert_between_host_and_big_endian(read_value); + return result; +} + +template +static bool seek(MediaStreamCursor& cursor, T position) +{ + if (position > NumericLimits::max()) + return false; + auto result = cursor.seek(static_cast(position), SeekMode::SetPosition); + return !result.is_error(); +} + +static constexpr i16 BITRATES[2][3][16] = { + // Version 1 + { + { 0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, -1 }, // Layer III + { 0, 32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384, -1 }, // Layer II + { 0, 32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448, -1 }, // Layer I + }, + // Version 2/2.5 + { + { 0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, -1 }, // Layer III + { 0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, -1 }, // Layer II + { 0, 32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256, -1 }, // Layer I + } +}; + +static constexpr u16 SAMPLES_PER_FRAME[2][3] = { + // Version 1 + { + 1152, // Layer III + 1152, // Layer II + 384, // Layer I + }, + // Version 2/2.5 + { + 576, // Layer III + 1152, // Layer II + 384, // Layer I + }, +}; + +static constexpr u16 SAMPLING_RATES[4][4] = { + { 11025, 12000, 8000, 0 }, // Version 2.5 + { 0, 0, 0, 0 }, // Reserved + { 22050, 24000, 16000, 0 }, // Version 2 + { 44100, 48000, 32000, 0 }, // Version 1 +}; + +template +static bool has_sync_code(T value) +{ + constexpr auto all_bits = static_cast(-1); + constexpr auto shift = NumericLimits::digits() - 11; + constexpr auto sync_code = static_cast(all_bits << shift); + return (value & sync_code) == sync_code; +} + +static bool has_sync_code(ReadonlyBytes bytes, size_t start) +{ + if (bytes.size() < start + 2) + return false; + auto value = static_cast((bytes[start] << 8) | bytes[start + 1]); + return has_sync_code(value); +} + +template +static u8 read_field(T value, u8 offset, u8 size) +{ + constexpr auto bit_count = NumericLimits::digits(); + auto mask = (static_cast(1) << size) - 1; + return (value >> (bit_count - offset - size)) & mask; +} + +static constexpr u64 EXACT_MPEG_AUDIO_DURATION_TIMEBASE = 14'112'000; + +struct FrameInfo { + u16 frame_byte_size { 0 }; + u32 duration_in_ticks { 0 }; +}; + +static bool parse_frame_header(MediaStreamCursor& cursor, FrameInfo& frame_info) +{ + u32 frame_header_data = 0; + if (!read(cursor, frame_header_data)) + return false; + + if (!has_sync_code(frame_header_data)) + return false; + + u8 mpeg_version = read_field(frame_header_data, 11, 2); + if (mpeg_version == 0b01) + return false; + auto is_mpeg_version_2 = mpeg_version != 0b11; + + u8 layer_description = read_field(frame_header_data, 13, 2); + if (layer_description == 0b00) + return false; + auto is_layer_i = layer_description == 0b11; + + auto layer_description_index = layer_description - 1; + u16 frame_samples = SAMPLES_PER_FRAME[is_mpeg_version_2][layer_description_index]; + + u8 bitrate_description = read_field(frame_header_data, 16, 4); + i16 bitrate = BITRATES[is_mpeg_version_2][layer_description_index][bitrate_description]; + if (bitrate <= 0) + return false; + + u8 sampling_frequency_index = read_field(frame_header_data, 20, 2); + u16 sampling_frequency = SAMPLING_RATES[mpeg_version][sampling_frequency_index]; + if (sampling_frequency == 0) + return false; + + u8 padding_bit = read_field(frame_header_data, 22, 1); + + constexpr size_t bytes_per_kb = 1000 / 8; + size_t slot_size = is_layer_i ? 4 : 1; + auto slot_count = static_cast(frame_samples) * static_cast(bitrate) * bytes_per_kb / (sampling_frequency * slot_size); + frame_info.frame_byte_size = static_cast((slot_count + padding_bit) * slot_size); + + frame_info.duration_in_ticks = static_cast(frame_samples) * EXACT_MPEG_AUDIO_DURATION_TIMEBASE / sampling_frequency; + return true; +} + +static AK::Duration duration_from_ticks(u64 ticks) +{ + return AK::Duration::from_time_units(static_cast(ticks), 1, EXACT_MPEG_AUDIO_DURATION_TIMEBASE); +} + +static AK::Duration scale_duration_by_byte_ratio(AK::Duration duration, size_t numerator, size_t denominator) +{ + VERIFY(numerator <= denominator); + VERIFY(denominator > 0); + + auto denominator_bit_width = AK::log2(denominator) + 1; + if (denominator_bit_width > NumericLimits::digits()) { + auto shift = denominator_bit_width - NumericLimits::digits(); + numerator >>= shift; + denominator >>= shift; + } + + return duration.scaled_by(static_cast(numerator), static_cast(denominator)); +} + +struct ByteTimePoint { + size_t byte; + AK::Duration time; +}; + +struct EnclosingCachedRanges { + MP3Navigator::CachedRange before; + Optional after; +}; + +struct FrameScanResult { + ByteTimePoint scanned_end; + ByteTimePoint last_parsed_frame; + Optional frame_spanning_target; +}; + +static int compare_byte_range_end_to_position(MediaStream::ByteRange const& range, size_t position) +{ + return range.end <= position ? -1 : 1; +} + +static AK::Duration interpolate_timestamp_at_byte(size_t byte, ByteTimePoint const& left, ByteTimePoint const& right) +{ + if (byte <= left.byte) + return left.time; + if (byte >= right.byte) + return right.time; + if (right.time <= left.time) + return left.time; + + return left.time + scale_duration_by_byte_ratio(right.time - left.time, byte - left.byte, right.byte - left.byte); +} + +static size_t estimate_byte_for_timestamp(AK::Duration timestamp, ByteTimePoint const& left, ByteTimePoint const& right) +{ + auto estimated_byte = left.byte; + if (right.time > left.time && right.byte > left.byte) { + auto numerator = (timestamp - left.time).to_time_units(1, EXACT_MPEG_AUDIO_DURATION_TIMEBASE); + auto denominator = (right.time - left.time).to_time_units(1, EXACT_MPEG_AUDIO_DURATION_TIMEBASE); + if (denominator > 0) { + auto byte_span = static_cast(right.byte - left.byte); + if (Checked::multiplication_would_overflow(numerator, byte_span)) { + estimated_byte = right.byte; + } else { + estimated_byte = left.byte + static_cast(numerator * byte_span / denominator); + } + } + } + return estimated_byte; +} + +static constexpr int FRAME_VALIDATION_COUNT = 3; + +static bool has_valid_frame_sequence_at(MediaStreamCursor& cursor, size_t candidate, size_t upper_bound) +{ + auto position = candidate; + for (int i = 0; i < FRAME_VALIDATION_COUNT; i++) { + if (position + 4 > upper_bound) + return false; + if (!seek(cursor, position)) + return false; + FrameInfo frame_info; + if (!parse_frame_header(cursor, frame_info)) + return false; + position += frame_info.frame_byte_size; + } + return true; +} + +static constexpr size_t RESYNC_CHUNK_SIZE = 4096; + +static Optional find_frame_boundary_at_or_before(MediaStreamCursor& cursor, size_t target_byte, size_t lower_bound, size_t upper_bound) +{ + VERIFY(target_byte > lower_bound); + VERIFY(target_byte <= upper_bound); + + auto chunk_end = min(target_byte + 2, upper_bound); + auto chunk_start = chunk_end > RESYNC_CHUNK_SIZE ? chunk_end - RESYNC_CHUNK_SIZE : 0; + chunk_start = max(chunk_start, lower_bound); + auto chunk_size = chunk_end - chunk_start; + if (chunk_size < 2) + return {}; + + Array buffer; + if (!seek(cursor, chunk_start)) + return {}; + if (!read_exact(cursor, buffer.span().trim(chunk_size))) + return {}; + + auto bytes = buffer.span().trim(chunk_size); + for (size_t i = bytes.size(); i-- > 0;) { + if (!has_sync_code(bytes, i)) + continue; + auto candidate = chunk_start + i; + if (candidate > target_byte) + continue; + if (has_valid_frame_sequence_at(cursor, candidate, upper_bound)) + return candidate; + } + return {}; +} + +static Optional find_frame_boundary_at_or_after(MediaStreamCursor& cursor, size_t start_byte, size_t upper_bound) +{ + VERIFY(start_byte < upper_bound); + auto chunk_start = start_byte; + auto chunk_size = min(RESYNC_CHUNK_SIZE, upper_bound - chunk_start); + if (chunk_size < 2) + return {}; + + Array buffer; + if (!seek(cursor, chunk_start)) + return {}; + if (!read_exact(cursor, buffer.span().trim(chunk_size))) + return {}; + + auto bytes = buffer.span().trim(chunk_size); + for (size_t i = 0; i < bytes.size(); i++) { + if (!has_sync_code(bytes, i)) + continue; + auto candidate = chunk_start + i; + if (has_valid_frame_sequence_at(cursor, candidate, upper_bound)) + return candidate; + } + return {}; +} + +static Optional find_containing_byte_range(Vector const& byte_ranges, size_t byte_position) +{ + auto index = lower_bound_index(byte_ranges, byte_position, compare_byte_range_end_to_position); + if (index >= byte_ranges.size() || byte_position < byte_ranges[index].start) + return {}; + return byte_ranges[index]; +} + +static void scan_available_frames_for_cached_range(MediaStreamCursor& cursor, MP3Navigator::CachedRange& range, MediaStream::ByteRange const& byte_range) +{ + auto position = max(range.last_scanned_byte, range.byte_start); + auto upper_bound = byte_range.end; + + while (position + 4 <= upper_bound) { + if (!seek(cursor, position)) + return; + FrameInfo frame_info; + if (!parse_frame_header(cursor, frame_info)) + return; + if (position + frame_info.frame_byte_size > upper_bound) + return; + position += frame_info.frame_byte_size; + range.duration_in_ticks += frame_info.duration_in_ticks; + range.last_scanned_byte = position; + } +} + +static ByteTimePoint scanned_endpoint(MP3Navigator::CachedRange const& range) +{ + return { + max(range.last_scanned_byte, range.byte_start), + range.time_start + duration_from_ticks(range.duration_in_ticks), + }; +} + +static Optional file_endpoint_after(ByteTimePoint const& left, Optional file_size, AK::Duration total_duration) +{ + if (!file_size.has_value()) + return {}; + if (*file_size <= left.byte) + return {}; + if (total_duration <= left.time) + return {}; + return ByteTimePoint { static_cast(*file_size), total_duration }; +} + +static EnclosingCachedRanges find_enclosing_cached_ranges_for_timestamp(Vector const& cached_ranges, size_t first_frame_position, AK::Duration timestamp) +{ + EnclosingCachedRanges ranges { + MP3Navigator::CachedRange { + .byte_start = first_frame_position, + .last_scanned_byte = first_frame_position, + }, + {}, + }; + for (auto const& range : cached_ranges) { + if (range.time_start <= timestamp) { + ranges.before = range; + continue; + } + ranges.after = range; + break; + } + return ranges; +} + +static AK::Duration estimate_time_for_cached_range_start(Vector const& cached_ranges, size_t byte_start, size_t first_frame_position, Optional file_size, AK::Duration total_duration) +{ + ByteTimePoint left { first_frame_position, AK::Duration::zero() }; + for (auto const& range : cached_ranges) { + if (range.byte_start >= byte_start) + break; + left = scanned_endpoint(range); + } + + auto right = file_endpoint_after(left, file_size, total_duration); + if (!right.has_value()) + return left.time; + return interpolate_timestamp_at_byte(byte_start, left, *right); +} + +static bool insert_cached_range_sorted(Vector& cached_ranges, MP3Navigator::CachedRange new_range) +{ + size_t insert_index = cached_ranges.size(); + for (size_t i = 0; i < cached_ranges.size(); i++) { + if (cached_ranges[i].byte_start == new_range.byte_start) + return false; + if (cached_ranges[i].byte_start > new_range.byte_start) { + insert_index = i; + break; + } + } + + VERIFY(insert_index == 0 || cached_ranges[insert_index - 1].byte_start < new_range.byte_start); + VERIFY(insert_index == cached_ranges.size() || new_range.byte_start < cached_ranges[insert_index].byte_start); + cached_ranges.insert(insert_index, new_range); + return true; +} + +static FrameScanResult scan_available_seek_frames(MediaStreamCursor& cursor, MP3Navigator::CachedRange const& range, Optional const& next_range, AK::Duration target) +{ + VERIFY(range.time_start <= target); + + auto target_duration_in_ticks = (target - range.time_start).to_time_units(1, EXACT_MPEG_AUDIO_DURATION_TIMEBASE); + auto scan_limit_byte = NumericLimits::max(); + if (next_range.has_value()) { + VERIFY(range.byte_start < next_range->byte_start); + VERIFY(range.time_start <= next_range->time_start); + + scan_limit_byte = next_range->byte_start; + } + + auto position = range.byte_start; + auto last_frame_position = position; + auto last_frame_duration_in_ticks = 0ull; + u64 duration_in_ticks = 0; + while (position + 4 <= scan_limit_byte) { + if (!seek(cursor, position)) + break; + FrameInfo frame_info; + if (!parse_frame_header(cursor, frame_info)) + break; + if (static_cast(duration_in_ticks + frame_info.duration_in_ticks) > target_duration_in_ticks) { + auto frame_time = range.time_start + duration_from_ticks(duration_in_ticks); + return { + { position, frame_time }, + { last_frame_position, range.time_start + duration_from_ticks(last_frame_duration_in_ticks) }, + SeekedPosition { static_cast(position), frame_time }, + }; + } + last_frame_position = position; + last_frame_duration_in_ticks = duration_in_ticks; + position += frame_info.frame_byte_size; + duration_in_ticks += frame_info.duration_in_ticks; + } + + return { + { position, range.time_start + duration_from_ticks(duration_in_ticks) }, + { last_frame_position, range.time_start + duration_from_ticks(last_frame_duration_in_ticks) }, + {}, + }; +} + +static void scan_cached_range(MediaStreamCursor& cursor, MP3Navigator::CachedRange& range, MediaStream::ByteRange const& byte_range) +{ + if (range.last_scanned_byte > byte_range.end) { + range.last_scanned_byte = range.byte_start; + range.duration_in_ticks = 0; + } else if (range.last_scanned_byte == byte_range.end) { + return; + } + scan_available_frames_for_cached_range(cursor, range, byte_range); +} + +static bool insert_cached_range_for_byte_range(MediaStreamCursor& cursor, Vector& ranges, MediaStream::ByteRange const& byte_range, size_t first_frame_position, Optional file_size, AK::Duration total_duration) +{ + if (byte_range.end <= first_frame_position) + return false; + + auto search_start = max(byte_range.start, first_frame_position); + if (search_start >= byte_range.end) + return false; + + auto frame_boundary = find_frame_boundary_at_or_after(cursor, search_start, byte_range.end); + if (!frame_boundary.has_value()) + return false; + + MP3Navigator::CachedRange range { + .byte_start = *frame_boundary, + .time_start = estimate_time_for_cached_range_start(ranges, *frame_boundary, first_frame_position, file_size, total_duration), + .last_scanned_byte = *frame_boundary, + .duration_in_ticks = 0, + }; + scan_available_frames_for_cached_range(cursor, range, byte_range); + + return insert_cached_range_sorted(ranges, range); +} + +MP3Navigator::MP3Navigator(NonnullRefPtr stream, size_t first_frame_position, AK::Duration total_duration) + : m_stream(move(stream)) + , m_first_frame_position(first_frame_position) + , m_total_duration(total_duration) + , m_buffered_range_scanning_cursor(m_stream->create_cursor()) + , m_seek_range_scanning_cursor(m_stream->create_cursor()) + , m_seek_cursor(m_stream->create_cursor()) +{ + m_buffered_range_scanning_cursor->set_is_blocking(false); + m_seek_range_scanning_cursor->set_is_blocking(false); + m_seek_cursor->set_is_blocking(true); + + m_cached_ranges.append({ + .byte_start = m_first_frame_position, + .time_start = AK::Duration::zero(), + .last_scanned_byte = m_first_frame_position, + .duration_in_ticks = 0, + }); +} + +TimeRanges MP3Navigator::buffered_time_ranges(Vector const& byte_ranges) const +{ + Sync::MutexLocker locker { m_mutex }; + update_cached_ranges(byte_ranges, *m_buffered_range_scanning_cursor); + + TimeRanges result; + auto file_size = m_stream->expected_size(); + for (auto const& range : m_cached_ranges) { + auto byte_range = find_containing_byte_range(byte_ranges, range.byte_start); + if (!byte_range.has_value()) + continue; + auto time_start = max(range.time_start, AK::Duration::zero()); + auto time_end = range.time_start + duration_from_ticks(range.duration_in_ticks); + if (file_size.has_value() && byte_range->end == *file_size) + time_end = AK::Duration::max(); + result.add_range(time_start, time_end); + } + return result; +} + +DecoderErrorOr MP3Navigator::seek_to_timestamp(AK::Duration timestamp) const +{ + if (m_total_duration <= AK::Duration::zero()) + return SeekSkipped {}; + + timestamp = max(timestamp, AK::Duration::zero()); + + EnclosingCachedRanges enclosing_cached_ranges; + FrameScanResult scan_result; + { + Sync::MutexLocker locker { m_mutex }; + update_cached_ranges(m_stream->available_byte_ranges(), *m_seek_range_scanning_cursor); + enclosing_cached_ranges = find_enclosing_cached_ranges_for_timestamp(m_cached_ranges, m_first_frame_position, timestamp); + scan_result = scan_available_seek_frames(*m_seek_range_scanning_cursor, enclosing_cached_ranges.before, enclosing_cached_ranges.after, timestamp); + if (scan_result.frame_spanning_target.has_value()) + return *scan_result.frame_spanning_target; + } + + auto file_size = m_stream->expected_size(); + Optional boundary_after_target; + if (enclosing_cached_ranges.after.has_value()) + boundary_after_target = ByteTimePoint { enclosing_cached_ranges.after->byte_start, enclosing_cached_ranges.after->time_start }; + else + boundary_after_target = file_endpoint_after(scan_result.scanned_end, file_size, m_total_duration); + if (!boundary_after_target.has_value()) + return SeekedPosition { static_cast(scan_result.last_parsed_frame.byte), scan_result.last_parsed_frame.time }; + + auto estimated_byte = estimate_byte_for_timestamp(timestamp, scan_result.scanned_end, *boundary_after_target); + + auto resynced_frame_byte = find_frame_boundary_at_or_before(*m_seek_cursor, estimated_byte, m_first_frame_position, boundary_after_target->byte); + if (!resynced_frame_byte.has_value() && estimated_byte < boundary_after_target->byte) + resynced_frame_byte = find_frame_boundary_at_or_after(*m_seek_cursor, estimated_byte, boundary_after_target->byte); + if (!resynced_frame_byte.has_value()) + return DecoderError::format(DecoderErrorCategory::Corrupted, "MP3 resync failed"); + + if (*resynced_frame_byte <= scan_result.scanned_end.byte) + return SeekedPosition { static_cast(scan_result.last_parsed_frame.byte), scan_result.last_parsed_frame.time }; + + auto resolved_time = interpolate_timestamp_at_byte(*resynced_frame_byte, scan_result.scanned_end, *boundary_after_target); + return SeekedPosition { static_cast(*resynced_frame_byte), resolved_time }; +} + +void MP3Navigator::update_cached_ranges(Vector const& byte_ranges, MediaStreamCursor& cursor) const +{ + if (byte_ranges.is_empty()) { + m_cached_ranges.clear(); + m_cached_ranges.append({ + .byte_start = m_first_frame_position, + .time_start = AK::Duration::zero(), + .last_scanned_byte = m_first_frame_position, + .duration_in_ticks = 0, + }); + return; + } + + auto file_size = m_stream->expected_size(); + size_t cached_index = 0; + size_t byte_index = 0; + + while (byte_index < byte_ranges.size()) { + auto const& byte_range = byte_ranges[byte_index]; + VERIFY(byte_range.start < byte_range.end); + + if (cached_index < m_cached_ranges.size() && m_cached_ranges[cached_index].byte_start < byte_range.start) { + m_cached_ranges.remove(cached_index); + continue; + } + + if (cached_index >= m_cached_ranges.size() || m_cached_ranges[cached_index].byte_start >= byte_range.end) { + if (insert_cached_range_for_byte_range(cursor, m_cached_ranges, byte_range, m_first_frame_position, file_size, m_total_duration)) + cached_index++; + byte_index++; + continue; + } + + auto& cached_range = m_cached_ranges[cached_index]; + VERIFY(byte_range.start <= cached_range.byte_start); + VERIFY(cached_range.byte_start < byte_range.end); + + scan_cached_range(cursor, cached_range, byte_range); + + while (cached_index + 1 < m_cached_ranges.size() && m_cached_ranges[cached_index + 1].byte_start < byte_range.end) + m_cached_ranges.remove(cached_index + 1); + + cached_index++; + byte_index++; + } + + if (cached_index < m_cached_ranges.size()) + m_cached_ranges.remove(cached_index, m_cached_ranges.size() - cached_index); + + reproject_cached_range_times(); +} + +void MP3Navigator::reproject_cached_range_times() const +{ + if (m_cached_ranges.size() < 2) + return; + + auto file_size = m_stream->expected_size(); + for (size_t i = 1; i < m_cached_ranges.size(); i++) { + auto left = scanned_endpoint(m_cached_ranges[i - 1]); + auto right = file_endpoint_after(left, file_size, m_total_duration); + if (!right.has_value()) { + m_cached_ranges[i].time_start = left.time; + continue; + } + + auto projected_time = interpolate_timestamp_at_byte(m_cached_ranges[i].byte_start, left, *right); + m_cached_ranges[i].time_start = projected_time; + } +} + +} diff --git a/Libraries/LibMedia/Containers/MP3Navigator.h b/Libraries/LibMedia/Containers/MP3Navigator.h new file mode 100644 index 0000000000..5ab0485003 --- /dev/null +++ b/Libraries/LibMedia/Containers/MP3Navigator.h @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2026-present, the Ladybird developers. + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include +#include +#include + +namespace Media { + +class MP3Navigator final : public ContainerNavigator { +public: + MP3Navigator(NonnullRefPtr stream, size_t first_frame_position, AK::Duration total_duration); + + struct CachedRange { + size_t byte_start { 0 }; + AK::Duration time_start { AK::Duration::zero() }; + size_t last_scanned_byte { 0 }; + u64 duration_in_ticks { 0 }; + }; + + TimeRanges buffered_time_ranges(Vector const& byte_ranges) const override; + DecoderErrorOr seek_to_timestamp(AK::Duration timestamp) const override; + +private: + void update_cached_ranges(Vector const& byte_ranges, MediaStreamCursor&) const; + void reproject_cached_range_times() const; + + NonnullRefPtr m_stream; + size_t m_first_frame_position; + AK::Duration m_total_duration; + + NonnullRefPtr m_buffered_range_scanning_cursor; + NonnullRefPtr m_seek_range_scanning_cursor; + NonnullRefPtr m_seek_cursor; + + mutable Sync::Mutex m_mutex; + mutable Vector m_cached_ranges; +}; + +} diff --git a/Libraries/LibMedia/FFmpeg/FFmpegDemuxer.cpp b/Libraries/LibMedia/FFmpeg/FFmpegDemuxer.cpp index fa37f4839a..71455121fa 100644 --- a/Libraries/LibMedia/FFmpeg/FFmpegDemuxer.cpp +++ b/Libraries/LibMedia/FFmpeg/FFmpegDemuxer.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -175,7 +176,7 @@ DecoderErrorOr> FFmpegDemuxer::from_stream(NonnullR demuxer->m_preferred_track_for_type[type_index] = static_cast(i); } - demuxer->m_container_navigator = create_container_navigator(*format_context, stream); + demuxer->m_container_navigator = create_container_navigator(*format_context, demuxer->m_total_duration, stream); avformat_close_input(&format_context); return demuxer; @@ -195,7 +196,7 @@ static inline i64 duration_to_time_units(AK::Duration duration, AVRational const return duration.to_time_units(time_base.num, time_base.den); } -OwnPtr FFmpegDemuxer::create_container_navigator(AVFormatContext& context, NonnullRefPtr const& stream) +OwnPtr FFmpegDemuxer::create_container_navigator(AVFormatContext& context, AK::Duration total_duration, NonnullRefPtr const& stream) { auto format_name = StringView(context.iformat->name, strlen(context.iformat->name)); @@ -232,6 +233,14 @@ OwnPtr FFmpegDemuxer::create_container_navigator(AVFormatCon return make(data_offset, bytes_per_second, codec_par->block_align); } + if (format_name == "mp3"sv && context.nb_streams == 1) { + AVPacket* packet = av_packet_alloc(); + ScopeGuard free_packet = [&] { av_packet_free(&packet); }; + + if (av_read_frame(&context, packet) >= 0 && packet->pos >= 0) + return make(stream, static_cast(packet->pos), total_duration); + } + return create_container_navigator_from_index(context); } @@ -390,7 +399,7 @@ DecoderErrorOr FFmpegDemuxer::seek_to_most_recent_keyframe(Tr return DemuxerSeekResult::KeptCurrentPosition; } if (auto const* seeked = seek_result.get_pointer()) { - if (av_seek_frame(&format_context, stream.index, seeked->byte_position, AVSEEK_FLAG_BYTE | AVSEEK_FLAG_BACKWARD) >= 0) { + if (av_seek_frame(&format_context, stream.index, seeked->byte_position, AVSEEK_FLAG_BYTE) >= 0) { seek_succeeded = true; track_context.pending_timestamp_offset = seeked->timestamp; track_context.timestamp_offset = AK::Duration::zero(); diff --git a/Libraries/LibMedia/FFmpeg/FFmpegDemuxer.h b/Libraries/LibMedia/FFmpeg/FFmpegDemuxer.h index 370f42fedd..2d70398e44 100644 --- a/Libraries/LibMedia/FFmpeg/FFmpegDemuxer.h +++ b/Libraries/LibMedia/FFmpeg/FFmpegDemuxer.h @@ -82,7 +82,7 @@ private: FFmpegDemuxer(NonnullRefPtr const&); - static OwnPtr create_container_navigator(AVFormatContext&, NonnullRefPtr const&); + static OwnPtr create_container_navigator(AVFormatContext&, AK::Duration, NonnullRefPtr const&); static OwnPtr create_container_navigator_from_index(AVFormatContext&); StreamInfo const& get_track_info(Track const&) const; diff --git a/Libraries/LibMedia/IncrementallyPopulatedStream.h b/Libraries/LibMedia/IncrementallyPopulatedStream.h index 1b09703bed..2480e0a736 100644 --- a/Libraries/LibMedia/IncrementallyPopulatedStream.h +++ b/Libraries/LibMedia/IncrementallyPopulatedStream.h @@ -48,7 +48,7 @@ public: u64 size(); void set_expected_size(u64); - Optional expected_size() const; + virtual Optional expected_size() const override; class MEDIA_API Cursor : public MediaStreamCursor { public: diff --git a/Libraries/LibMedia/MediaStream.h b/Libraries/LibMedia/MediaStream.h index 9437ea10b6..1ab532e9ba 100644 --- a/Libraries/LibMedia/MediaStream.h +++ b/Libraries/LibMedia/MediaStream.h @@ -79,6 +79,8 @@ public: virtual NonnullRefPtr create_cursor() = 0; virtual Vector available_byte_ranges() const = 0; + + virtual Optional expected_size() const = 0; }; }