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.
132 lines
4.2 KiB
C++
132 lines
4.2 KiB
C++
/*
|
|
* Copyright (c) 2025, Aliaksandr Kalenik <kalenik.aliaksandr@gmail.com>
|
|
*
|
|
* SPDX-License-Identifier: BSD-2-Clause
|
|
*/
|
|
|
|
#pragma once
|
|
|
|
#include <AK/Atomic.h>
|
|
#include <AK/AtomicRefCounted.h>
|
|
#include <AK/Forward.h>
|
|
#include <AK/Function.h>
|
|
#include <AK/RedBlackTree.h>
|
|
#include <AK/RefPtr.h>
|
|
#include <AK/Time.h>
|
|
#include <AK/Vector.h>
|
|
#include <LibCore/Forward.h>
|
|
#include <LibMedia/DecoderError.h>
|
|
#include <LibMedia/Export.h>
|
|
#include <LibMedia/MediaStream.h>
|
|
#include <LibSync/ConditionVariable.h>
|
|
#include <LibSync/Mutex.h>
|
|
|
|
namespace Media {
|
|
|
|
class MEDIA_API IncrementallyPopulatedStream : public MediaStream {
|
|
public:
|
|
static NonnullRefPtr<IncrementallyPopulatedStream> create_empty();
|
|
static NonnullRefPtr<IncrementallyPopulatedStream> create_from_data(ReadonlyBytes);
|
|
static NonnullRefPtr<IncrementallyPopulatedStream> create_from_buffer(ByteBuffer const&);
|
|
|
|
~IncrementallyPopulatedStream();
|
|
|
|
virtual NonnullRefPtr<MediaStreamCursor> create_cursor() override;
|
|
|
|
// Callback invoked when data at a specific offset is needed but not available.
|
|
// The callback receives the desired offset position and is invoked on the provided event loop.
|
|
using DataRequestCallback = Function<void(u64 offset)>;
|
|
void set_data_request_callback(DataRequestCallback);
|
|
|
|
void add_chunk_at(u64 offset, ReadonlyBytes);
|
|
void remove_byte_range(u64 start, u64 end);
|
|
u64 next_chunk_start() const { return m_last_chunk_end; }
|
|
|
|
void close();
|
|
|
|
virtual Vector<ByteRange> available_byte_ranges() const override;
|
|
|
|
u64 size();
|
|
void set_expected_size(u64);
|
|
virtual Optional<u64> expected_size() const override;
|
|
|
|
class MEDIA_API Cursor : public MediaStreamCursor {
|
|
public:
|
|
~Cursor();
|
|
|
|
virtual void set_is_blocking(bool) override;
|
|
|
|
virtual DecoderErrorOr<void> seek(i64 offset, AK::SeekMode mode) override;
|
|
virtual DecoderErrorOr<size_t> read_into(Bytes bytes) override;
|
|
|
|
virtual size_t position() const override { return m_position; }
|
|
virtual size_t size() const override { return m_stream->size(); }
|
|
|
|
virtual void abort() override;
|
|
virtual void reset_abort() override { m_aborted = false; }
|
|
virtual bool is_aborted() const override { return m_aborted; }
|
|
|
|
virtual bool is_blocked() const override { return m_blocked; }
|
|
|
|
private:
|
|
friend class IncrementallyPopulatedStream;
|
|
|
|
Cursor(NonnullRefPtr<IncrementallyPopulatedStream> const& stream);
|
|
|
|
NonnullRefPtr<IncrementallyPopulatedStream> m_stream;
|
|
bool m_is_blocking { true };
|
|
size_t m_position { 0 };
|
|
bool m_aborted { false };
|
|
Atomic<bool> m_blocked { false };
|
|
MonotonicTime m_active_timeout { MonotonicTime::now_coarse() };
|
|
};
|
|
|
|
private:
|
|
class DataChunk {
|
|
public:
|
|
DataChunk(u64 offset, ByteBuffer&& data)
|
|
: m_offset(offset)
|
|
, m_data(move(data))
|
|
{
|
|
}
|
|
|
|
u64 offset() const { return m_offset; }
|
|
u64 size() const { return m_data.size(); }
|
|
u64 end() const { return offset() + size(); }
|
|
ByteBuffer& data() { return m_data; }
|
|
ByteBuffer const& data() const { return m_data; }
|
|
bool contains(u64 position) const { return position >= m_offset && position < end(); }
|
|
bool overlaps(DataChunk const& chunk) const { return offset() < chunk.end() && chunk.offset() < end(); }
|
|
|
|
private:
|
|
size_t m_offset { 0 };
|
|
ByteBuffer m_data;
|
|
};
|
|
|
|
IncrementallyPopulatedStream();
|
|
|
|
friend class Cursor;
|
|
|
|
using Chunks = AK::RedBlackTree<u64, DataChunk>;
|
|
|
|
DecoderErrorOr<size_t> read_at(Cursor&, size_t position, Bytes&);
|
|
|
|
void begin_new_request_while_locked(u64 position);
|
|
bool check_if_data_is_available_or_begin_request_while_locked(Cursor&, u64 position, u64 length);
|
|
size_t read_from_chunks_while_locked(u64 position, Bytes& bytes) const;
|
|
|
|
mutable Sync::Mutex m_mutex;
|
|
Vector<Cursor&> m_cursors;
|
|
Sync::ConditionVariable m_state_changed { m_mutex };
|
|
|
|
Chunks m_chunks;
|
|
Optional<u64> m_expected_size;
|
|
bool m_closed { false };
|
|
|
|
RefPtr<Core::WeakEventLoopReference> m_callback_event_loop;
|
|
DataRequestCallback m_data_request_callback;
|
|
u64 m_currently_requested_position { 0 };
|
|
u64 m_last_chunk_end { 0 };
|
|
};
|
|
|
|
}
|