From f61248104f33912122647bf6b80b57797cab9b48 Mon Sep 17 00:00:00 2001 From: Marek Malek Date: Mon, 11 May 2026 12:57:18 +0200 Subject: [PATCH 1/4] feat: add HLSDecoder structure --- .../cpp/audioapi/core/utils/HLSDecoder.cpp | 11 + .../cpp/audioapi/core/utils/HLSDecoder.h | 193 ++++++++++++++++++ 2 files changed, 204 insertions(+) create mode 100644 packages/react-native-audio-api/common/cpp/audioapi/core/utils/HLSDecoder.cpp create mode 100644 packages/react-native-audio-api/common/cpp/audioapi/core/utils/HLSDecoder.h diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/HLSDecoder.cpp b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/HLSDecoder.cpp new file mode 100644 index 000000000..758b7f01e --- /dev/null +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/HLSDecoder.cpp @@ -0,0 +1,11 @@ +#include + +namespace audioapi { + +bool HLSDecoder::openFile(int outputSampleRate, const std::string &path) { + if (avformat_open_input(&fmtCtx_, path, nullptr, nullptr) < 0) { + return false; + } + return avformat_find_stream_info(fmtCtx_, nullptr) >= 0; +} +}; // namespace audioapi \ No newline at end of file diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/HLSDecoder.h b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/HLSDecoder.h new file mode 100644 index 000000000..a7a3ee4f8 --- /dev/null +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/HLSDecoder.h @@ -0,0 +1,193 @@ +#pragma once + +#pragma once + +#include +#include +#include +#include +#include +#include + +#if !RN_AUDIO_API_FFMPEG_DISABLED +extern "C" { +#include +#include +#include +#include +#include +#include +} +#endif // RN_AUDIO_API_FFMPEG_DISABLED + +#include +#include +#include +#include +#include +#include +#include + +// TODO: change names, prob move to constants.h (?) +inline constexpr auto STREAMER_NODE_SPSC_OVERFLOW_STRATEGY = + audioapi::channels::spsc::OverflowStrategy::WAIT_ON_FULL; +inline constexpr auto STREAMER_NODE_SPSC_WAIT_STRATEGY = + audioapi::channels::spsc::WaitStrategy::ATOMIC_WAIT; + +inline constexpr auto VERBOSE = false; +inline constexpr auto CHANNEL_CAPACITY = 32; + +// TODO: check if copy constructors are needed +struct StreamingData { + audioapi::AudioBuffer buffer; + size_t size{0}; + + StreamingData() = default; + ~StreamingData() = default; + StreamingData(audioapi::AudioBuffer b, size_t s) : buffer(std::move(b)), size(s) {} + StreamingData(const StreamingData &data) = default; + StreamingData(StreamingData &&data) noexcept : buffer(std::move(data.buffer)), size(data.size) {} + StreamingData &operator=(StreamingData &&other) = default; + StreamingData &operator=(const StreamingData &data) { + if (this == &data) { + return *this; + } + buffer = data.buffer; + size = data.size; + return *this; + } +}; + +namespace audioapi { + +class HLSDecoder : public decoding::IIncrementalAudioDecoder { + /// @brief Opens a file for decoding. + /// @param outputSampleRate The output sample rate. + /// @param path The path to the file. + /// @return True if the file was opened successfully, false otherwise. + [[nodiscard]] bool openFile(int outputSampleRate, const std::string &path) override; + + /// @brief Opens a memory block for decoding. + /// @param outputSampleRate The output sample rate. + /// @param data The data to decode. + /// @param size The size of the data. + /// @return True if the memory block was opened successfully, false otherwise. + [[nodiscard]] bool openMemory(int outputSampleRate, const void *data, size_t size) override; + + /// @brief Reads PCM frames from the decoder. + /// @param outInterleaved The output buffer for the decoded frames. + /// @param frameCount The number of frames to read. + /// @return The number of frames read. + [[nodiscard]] size_t readPcmFrames(float *outInterleaved, size_t frameCount) override; + + /// @brief Closes the decoder. + void close() override; + + /// @brief Checks if the decoder is open. + /// @return True if the decoder is open, false otherwise. + [[nodiscard]] bool isOpen() const override; + + /// @brief Gets the number of output channels. + /// @return The number of output channels. + [[nodiscard]] int outputChannels() const override; + + /// @brief Gets the output sample rate. + /// @return The output sample rate. + [[nodiscard]] int outputSampleRate() const override; + + /// @brief Gets the duration of the audio in seconds. + /// @return The duration of the audio in seconds. + [[nodiscard]] float getDurationInSeconds() const override; + + /// @brief Gets the current position of the audio in seconds. + /// @return The current position of the audio in seconds. + [[nodiscard]] float getCurrentPositionInSeconds() const override; + + /// @brief Seeks to a specific time in the audio. + /// @param seconds The time to seek to in seconds. + /// @return True if the seek was successful, false otherwise. + [[nodiscard]] bool seekToTime(double seconds) override; + +#if !RN_AUDIO_API_FFMPEG_DISABLED + AVFormatContext *fmtCtx_; + AVCodecContext *codecCtx_; + const AVCodec *decoder_; + AVCodecParameters *codecpar_; + AVPacket *pkt_; + AVFrame *frame_; // Frame that is currently being processed + SwrContext *swrCtx_; + + // --resampling-- + AudioBuffer resamplerInputBuffer_; + AudioBuffer resamplerOutputBuffer_; + StreamingData bufferedAudioData_; // audio data for buffering hls frames + bool hasBufferedAudioData_; + int audio_stream_index_; // index of the audio stream channel in the input + int maxResampledSamples_; + size_t processedSamples_; + + std::thread streamingThread_; + std::atomic isNodeFinished_; // Flag to control the streaming thread + static constexpr int INITIAL_MAX_RESAMPLED_SAMPLES = 8192; // Initial size for resampled data + channels::spsc:: + Sender + sender_; + channels::spsc::Receiver< + StreamingData, + STREAMER_NODE_SPSC_OVERFLOW_STRATEGY, + STREAMER_NODE_SPSC_WAIT_STRATEGY> + receiver_; + + /// @brief Initialize the StreamerNode by opening the input stream, + /// finding the audio stream, setting up the decoder, and starting the streaming thread. + /// @param inputUrl The URL of the input stream + /// @return true if initialization was successful, false otherwise + bool initialize(const std::string &inputUrl); + + /** + * @brief Setting up the resampler + * @param outSampleRate Sample rate for the output audio + * @return true if successful, false otherwise + */ + bool setupResampler(float outSampleRate); + + /** + * @brief Resample the audio frame, change its sample format and channel layout + * @param frame The AVFrame to resample + * @param context The context + */ + void processFrameWithResampler(AVFrame *frame, const std::shared_ptr &context); + + /** + * @brief Thread function to continuously read and process audio frames + * @details This function runs in a separate thread to avoid blocking the main audio processing thread + * @note It will read frames from the input stream, resample them, and store them in the buffered buffer + * @note The thread will stop when streamFlag is set to false + */ + void streamAudio(); + + /** @brief Clean up resources */ + void cleanup(); + + /** + * @brief Open the input stream + * @param inputUrl The URL of the input stream + * @return true if successful, false otherwise + * @note This function initializes the FFmpeg libraries and opens the input stream + */ + bool openInput(const std::string &inputUrl); + + /** + * @brief Find the audio stream channel in the input + * @return true if audio stream was found, false otherwise + */ + bool findAudioStream(); + + /** + * @brief Set up the decoder for the audio stream + * @return true if successful, false otherwise + */ + bool setupDecoder(); +#endif // RN_AUDIO_API_FFMPEG_DISABLED +}; +} // namespace audioapi \ No newline at end of file From a201b8da8c1dac497c67e0164bbf433ed92e9500 Mon Sep 17 00:00:00 2001 From: Marek Malek Date: Thu, 14 May 2026 16:45:43 +0200 Subject: [PATCH 2/4] chore: changes --- .../sources/AudioFileSourceNodeHostObject.cpp | 2 +- .../cpp/audioapi/core/utils/HLSDecoder.cpp | 10 +- .../cpp/audioapi/core/utils/HLSDecoder.h | 115 +++++------------- .../libs/decoding/IncrementalAudioDecoder.h | 15 +-- .../audioapi/libs/ffmpeg/FFmpegDecoding.cpp | 60 ++++----- .../cpp/audioapi/libs/ffmpeg/FFmpegDecoding.h | 19 +-- .../audioapi/utils/BoundedPriorityQueue.hpp | 2 +- 7 files changed, 78 insertions(+), 145 deletions(-) diff --git a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/sources/AudioFileSourceNodeHostObject.cpp b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/sources/AudioFileSourceNodeHostObject.cpp index 0d4f884a9..f0b9fbbd6 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/sources/AudioFileSourceNodeHostObject.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/sources/AudioFileSourceNodeHostObject.cpp @@ -1,11 +1,11 @@ #include +#include #include #include #include #include #include -#include "audioapi/HostObjects/sources/AudioScheduledSourceNodeHostObject.h" namespace audioapi { diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/HLSDecoder.cpp b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/HLSDecoder.cpp index 758b7f01e..f16a27975 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/HLSDecoder.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/HLSDecoder.cpp @@ -1,11 +1,3 @@ #include -namespace audioapi { - -bool HLSDecoder::openFile(int outputSampleRate, const std::string &path) { - if (avformat_open_input(&fmtCtx_, path, nullptr, nullptr) < 0) { - return false; - } - return avformat_find_stream_info(fmtCtx_, nullptr) >= 0; -} -}; // namespace audioapi \ No newline at end of file +namespace audioapi {}; // namespace audioapi \ No newline at end of file diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/HLSDecoder.h b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/HLSDecoder.h index a7a3ee4f8..dda066eeb 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/HLSDecoder.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/HLSDecoder.h @@ -1,7 +1,5 @@ #pragma once -#pragma once - #include #include #include @@ -28,10 +26,9 @@ extern "C" { #include #include -// TODO: change names, prob move to constants.h (?) -inline constexpr auto STREAMER_NODE_SPSC_OVERFLOW_STRATEGY = +inline constexpr auto HLS_DECODER_SPSC_OVERFLOW_STRATEGY = audioapi::channels::spsc::OverflowStrategy::WAIT_ON_FULL; -inline constexpr auto STREAMER_NODE_SPSC_WAIT_STRATEGY = +inline constexpr auto HLS_DECODER_SPSC_WAIT_STRATEGY = audioapi::channels::spsc::WaitStrategy::ATOMIC_WAIT; inline constexpr auto VERBOSE = false; @@ -59,84 +56,8 @@ struct StreamingData { }; namespace audioapi { - -class HLSDecoder : public decoding::IIncrementalAudioDecoder { - /// @brief Opens a file for decoding. - /// @param outputSampleRate The output sample rate. - /// @param path The path to the file. - /// @return True if the file was opened successfully, false otherwise. - [[nodiscard]] bool openFile(int outputSampleRate, const std::string &path) override; - - /// @brief Opens a memory block for decoding. - /// @param outputSampleRate The output sample rate. - /// @param data The data to decode. - /// @param size The size of the data. - /// @return True if the memory block was opened successfully, false otherwise. - [[nodiscard]] bool openMemory(int outputSampleRate, const void *data, size_t size) override; - - /// @brief Reads PCM frames from the decoder. - /// @param outInterleaved The output buffer for the decoded frames. - /// @param frameCount The number of frames to read. - /// @return The number of frames read. - [[nodiscard]] size_t readPcmFrames(float *outInterleaved, size_t frameCount) override; - - /// @brief Closes the decoder. - void close() override; - - /// @brief Checks if the decoder is open. - /// @return True if the decoder is open, false otherwise. - [[nodiscard]] bool isOpen() const override; - - /// @brief Gets the number of output channels. - /// @return The number of output channels. - [[nodiscard]] int outputChannels() const override; - - /// @brief Gets the output sample rate. - /// @return The output sample rate. - [[nodiscard]] int outputSampleRate() const override; - - /// @brief Gets the duration of the audio in seconds. - /// @return The duration of the audio in seconds. - [[nodiscard]] float getDurationInSeconds() const override; - - /// @brief Gets the current position of the audio in seconds. - /// @return The current position of the audio in seconds. - [[nodiscard]] float getCurrentPositionInSeconds() const override; - - /// @brief Seeks to a specific time in the audio. - /// @param seconds The time to seek to in seconds. - /// @return True if the seek was successful, false otherwise. - [[nodiscard]] bool seekToTime(double seconds) override; - +class HLSDecoder { #if !RN_AUDIO_API_FFMPEG_DISABLED - AVFormatContext *fmtCtx_; - AVCodecContext *codecCtx_; - const AVCodec *decoder_; - AVCodecParameters *codecpar_; - AVPacket *pkt_; - AVFrame *frame_; // Frame that is currently being processed - SwrContext *swrCtx_; - - // --resampling-- - AudioBuffer resamplerInputBuffer_; - AudioBuffer resamplerOutputBuffer_; - StreamingData bufferedAudioData_; // audio data for buffering hls frames - bool hasBufferedAudioData_; - int audio_stream_index_; // index of the audio stream channel in the input - int maxResampledSamples_; - size_t processedSamples_; - - std::thread streamingThread_; - std::atomic isNodeFinished_; // Flag to control the streaming thread - static constexpr int INITIAL_MAX_RESAMPLED_SAMPLES = 8192; // Initial size for resampled data - channels::spsc:: - Sender - sender_; - channels::spsc::Receiver< - StreamingData, - STREAMER_NODE_SPSC_OVERFLOW_STRATEGY, - STREAMER_NODE_SPSC_WAIT_STRATEGY> - receiver_; /// @brief Initialize the StreamerNode by opening the input stream, /// finding the audio stream, setting up the decoder, and starting the streaming thread. @@ -188,6 +109,36 @@ class HLSDecoder : public decoding::IIncrementalAudioDecoder { * @return true if successful, false otherwise */ bool setupDecoder(); + + // + AVFormatContext *fmtCtx_; + AVCodecContext *codecCtx_; + const AVCodec *decoder_; + AVCodecParameters *codecpar_; + AVPacket *pkt_; + AVFrame *frame_; // Frame that is currently being processed + SwrContext *swrCtx_; + + // --resampling-- + AudioBuffer resamplerInputBuffer_; + AudioBuffer resamplerOutputBuffer_; + StreamingData bufferedAudioData_; // audio data for buffering hls frames + bool hasBufferedAudioData_; + int audio_stream_index_; // index of the audio stream channel in the input + int maxResampledSamples_; + size_t processedSamples_; + + std::thread streamingThread_; + std::atomic isNodeFinished_; // Flag to control the streaming thread + + // spsc + static constexpr int INITIAL_MAX_RESAMPLED_SAMPLES = 8192; // Initial size for resampled data + channels::spsc:: + Sender + sender_; + channels::spsc:: + Receiver + receiver_; #endif // RN_AUDIO_API_FFMPEG_DISABLED }; } // namespace audioapi \ No newline at end of file diff --git a/packages/react-native-audio-api/common/cpp/audioapi/libs/decoding/IncrementalAudioDecoder.h b/packages/react-native-audio-api/common/cpp/audioapi/libs/decoding/IncrementalAudioDecoder.h index cf4550646..552f0f300 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/libs/decoding/IncrementalAudioDecoder.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/libs/decoding/IncrementalAudioDecoder.h @@ -1,8 +1,9 @@ #pragma once + +#include +#include #include #include -#include -#include namespace audioapi::decoding { using DecoderResult = Result; @@ -21,19 +22,15 @@ class IncrementalAudioDecoder { /// @param outputSampleRate The output sample rate. /// @param path The path to the file. /// @return Ok(None) on success or Err(message) on failure. - [[nodiscard]] virtual DecoderResult openFile( - int outputSampleRate, - const std::string &path) = 0; + [[nodiscard]] virtual DecoderResult openFile(int outputSampleRate, const std::string &path) = 0; /// @brief Opens a memory block for decoding. /// @param outputSampleRate The output sample rate. /// @param data The data to decode. /// @param size The size of the data. /// @return Ok(None) on success or Err(message) on failure. - [[nodiscard]] virtual DecoderResult openMemory( - int outputSampleRate, - const void *data, - size_t size) = 0; + [[nodiscard]] virtual DecoderResult + openMemory(int outputSampleRate, const void *data, size_t size) = 0; /// @brief Reads PCM frames from the decoder. /// @param outInterleaved The output buffer for the decoded frames. diff --git a/packages/react-native-audio-api/common/cpp/audioapi/libs/ffmpeg/FFmpegDecoding.cpp b/packages/react-native-audio-api/common/cpp/audioapi/libs/ffmpeg/FFmpegDecoding.cpp index 57c366cf1..560b22ccd 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/libs/ffmpeg/FFmpegDecoding.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/libs/ffmpeg/FFmpegDecoding.cpp @@ -79,10 +79,8 @@ int findAudioStreamIndex(AVFormatContext *fmt_ctx) { return -1; } -decoding::DecoderResult openCodec( - AVFormatContext *fmt_ctx, - int &audio_stream_index, - AVCodecContext **out_codec) { +decoding::DecoderResult +openCodec(AVFormatContext *fmt_ctx, int &audio_stream_index, AVCodecContext **out_codec) { audio_stream_index = findAudioStreamIndex(fmt_ctx); if (audio_stream_index < 0) { return Err("FFmpegDecoder::openCodec failed: no audio stream found"); @@ -106,7 +104,8 @@ decoding::DecoderResult openCodec( const int openResult = avcodec_open2(ctx, codec, nullptr); if (openResult < 0) { avcodec_free_context(&ctx); - return Err("FFmpegDecoder::openCodec failed: avcodec_open2 failed: " + parseFFmpegError(openResult)); + return Err( + "FFmpegDecoder::openCodec failed: avcodec_open2 failed: " + parseFFmpegError(openResult)); } *out_codec = ctx; return Ok(None); @@ -214,8 +213,7 @@ decoding::DecoderResult FFmpegDecoder::openFile(int outputSampleRate, const std: return codecResult; } output_channels_ = codec_ctx_->ch_layout.nb_channels; - output_sample_rate_ = - (outputSampleRate > 0) ? outputSampleRate : codec_ctx_->sample_rate; + output_sample_rate_ = (outputSampleRate > 0) ? outputSampleRate : codec_ctx_->sample_rate; packet_ = av_packet_alloc(); frame_ = av_frame_alloc(); @@ -236,10 +234,8 @@ decoding::DecoderResult FFmpegDecoder::openFile(int outputSampleRate, const std: return Ok(None); } -decoding::DecoderResult FFmpegDecoder::openMemory( - int outputSampleRate, - const void *data, - size_t size) { +decoding::DecoderResult +FFmpegDecoder::openMemory(int outputSampleRate, const void *data, size_t size) { close(); if (data == nullptr || size == 0) { return Err("FFmpegDecoder::openMemory failed: input data is empty"); @@ -249,20 +245,19 @@ decoding::DecoderResult FFmpegDecoder::openMemory( mem_io_->size = size; mem_io_->pos = 0; - auto* io_buf = - static_cast(av_malloc(decoding::IncrementalAudioDecoder::CHUNK_SIZE)); + auto *io_buf = static_cast(av_malloc(decoding::IncrementalAudioDecoder::CHUNK_SIZE)); if (io_buf == nullptr) { close(); return Err("FFmpegDecoder::openMemory failed: av_malloc returned null"); } avio_ctx_ = avio_alloc_context( - io_buf, - static_cast(decoding::IncrementalAudioDecoder::CHUNK_SIZE), - 0, - mem_io_.get(), - read_packet, - nullptr, - seek_packet); + io_buf, + static_cast(decoding::IncrementalAudioDecoder::CHUNK_SIZE), + 0, + mem_io_.get(), + read_packet, + nullptr, + seek_packet); if (avio_ctx_ == nullptr) { av_free(io_buf); mem_io_.reset(); @@ -296,8 +291,7 @@ decoding::DecoderResult FFmpegDecoder::openMemory( return codecResult; } output_channels_ = codec_ctx_->ch_layout.nb_channels; - output_sample_rate_ = - (outputSampleRate > 0) ? outputSampleRate : codec_ctx_->sample_rate; + output_sample_rate_ = (outputSampleRate > 0) ? outputSampleRate : codec_ctx_->sample_rate; packet_ = av_packet_alloc(); frame_ = av_frame_alloc(); @@ -388,8 +382,7 @@ decoding::DecoderResult FFmpegDecoder::feedPipeline() { av_packet_unref(packet_); if (r < 0) { return Err( - "FFmpegDecoder::feedPipeline failed: avcodec_send_packet failed: " + - parseFFmpegError(r)); + "FFmpegDecoder::feedPipeline failed: avcodec_send_packet failed: " + parseFFmpegError(r)); } } } @@ -403,7 +396,9 @@ float FFmpegDecoder::getDurationInSeconds() const { return 0; } - auto validSeconds = [](double s) -> bool { return s > 0 && std::isfinite(s); }; + auto validSeconds = [](double s) -> bool { + return s > 0 && std::isfinite(s); + }; // Prefer per-stream duration (e.g. MP4 mdhd) — often exact vs container-level // guesses that trigger AAC “bitrate duration” warnings. @@ -457,8 +452,8 @@ decoding::DecoderResult FFmpegDecoder::seekToTime(double seconds) { avcodec_flush_buffers(codec_ctx_); leftover_.clear(); leftover_offset_ = 0; - total_output_frames_ = static_cast( - std::llround(seconds * static_cast(output_sample_rate_))); + total_output_frames_ = + static_cast(std::llround(seconds * static_cast(output_sample_rate_))); return Ok(None); } @@ -471,9 +466,8 @@ size_t FFmpegDecoder::readPcmFrames(float *outInterleaved, size_t frameCount) { while (delivered < frameCount) { size_t need = frameCount - delivered; - size_t available_samples = leftover_.size() > leftover_offset_ - ? leftover_.size() - leftover_offset_ - : 0; + size_t available_samples = + leftover_.size() > leftover_offset_ ? leftover_.size() - leftover_offset_ : 0; size_t leftover_frames = available_samples / ch; if (leftover_frames > 0) { size_t take = std::min(need, leftover_frames); @@ -496,10 +490,8 @@ size_t FFmpegDecoder::readPcmFrames(float *outInterleaved, size_t frameCount) { return delivered; } -static std::shared_ptr buildAudioBufferFromInterleaved( - std::vector &interleaved, - int channels, - int sample_rate) { +static std::shared_ptr +buildAudioBufferFromInterleaved(std::vector &interleaved, int channels, int sample_rate) { if (interleaved.empty() || channels <= 0) { return nullptr; } diff --git a/packages/react-native-audio-api/common/cpp/audioapi/libs/ffmpeg/FFmpegDecoding.h b/packages/react-native-audio-api/common/cpp/audioapi/libs/ffmpeg/FFmpegDecoding.h index 4863c9b1c..d22a11b3d 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/libs/ffmpeg/FFmpegDecoding.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/libs/ffmpeg/FFmpegDecoding.h @@ -48,14 +48,11 @@ class FFmpegDecoder : public decoding::IncrementalAudioDecoder { FFmpegDecoder &operator=(FFmpegDecoder &&other) = delete; ~FFmpegDecoder() override; - [[nodiscard]] decoding::DecoderResult openFile( - int outputSampleRate, - const std::string &path) override; + [[nodiscard]] decoding::DecoderResult openFile(int outputSampleRate, const std::string &path) + override; - [[nodiscard]] decoding::DecoderResult openMemory( - int outputSampleRate, - const void *data, - size_t size) override; + [[nodiscard]] decoding::DecoderResult + openMemory(int outputSampleRate, const void *data, size_t size) override; [[nodiscard]] size_t readPcmFrames(float *outInterleaved, size_t frameCount) override; @@ -64,8 +61,12 @@ class FFmpegDecoder : public decoding::IncrementalAudioDecoder { [[nodiscard]] bool isOpen() const override { return fmt_ctx_ != nullptr && codec_ctx_ != nullptr; } - [[nodiscard]] int outputChannels() const override { return output_channels_; } - [[nodiscard]] int outputSampleRate() const override { return output_sample_rate_; } + [[nodiscard]] int outputChannels() const override { + return output_channels_; + } + [[nodiscard]] int outputSampleRate() const override { + return output_sample_rate_; + } [[nodiscard]] float getDurationInSeconds() const override; diff --git a/packages/react-native-audio-api/common/cpp/audioapi/utils/BoundedPriorityQueue.hpp b/packages/react-native-audio-api/common/cpp/audioapi/utils/BoundedPriorityQueue.hpp index 63125bd9e..95412e5fe 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/utils/BoundedPriorityQueue.hpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/utils/BoundedPriorityQueue.hpp @@ -13,7 +13,7 @@ namespace audioapi { /// @brief A bounded priority queue with fixed capacity backed by a static pool allocator. /// Elements are kept in ascending sorted order (smallest element at front). /// All operations avoid heap allocation. Uses std::multiset under the hood -// to maintain sorted order and provide efficient insertion and removal. +/// to maintain sorted order and provide efficient insertion and removal. /// @tparam T The type of elements stored. Must be move-constructible. /// @tparam Capacity The maximum number of elements. /// @tparam Compare Comparator type. Defaults to std::less (smallest element at front). From d2df37a21a9666bbefd84ec188ef4178e618cbb3 Mon Sep 17 00:00:00 2001 From: Marek Malek Date: Wed, 27 May 2026 15:42:46 +0200 Subject: [PATCH 3/4] feat: add seekdecoder daemon to AFSN --- .../src/examples/AudioTag/AudioTag.tsx | 3 +- .../src/examples/Streaming/Streaming.tsx | 4 +- apps/fabric-example/ios/Podfile.lock | 8 +- .../HostObjects/utils/NodeOptionsParser.h | 6 +- .../core/sources/AudioFileSourceNode.cpp | 223 +++++++----------- .../core/sources/AudioFileSourceNode.h | 74 +++--- .../cpp/audioapi/core/types/AudioFormat.h | 2 +- .../cpp/audioapi/core/utils/HLSDecoder.cpp | 3 - .../cpp/audioapi/core/utils/HLSDecoder.h | 144 ----------- .../core/utils/decoding/SeekDecoderDaemon.cpp | 116 +++++++++ .../core/utils/decoding/SeekDecoderDaemon.h | 83 +++++++ 11 files changed, 332 insertions(+), 334 deletions(-) delete mode 100644 packages/react-native-audio-api/common/cpp/audioapi/core/utils/HLSDecoder.cpp delete mode 100644 packages/react-native-audio-api/common/cpp/audioapi/core/utils/HLSDecoder.h create mode 100644 packages/react-native-audio-api/common/cpp/audioapi/core/utils/decoding/SeekDecoderDaemon.cpp create mode 100644 packages/react-native-audio-api/common/cpp/audioapi/core/utils/decoding/SeekDecoderDaemon.h diff --git a/apps/common-app/src/examples/AudioTag/AudioTag.tsx b/apps/common-app/src/examples/AudioTag/AudioTag.tsx index 7ff67d93c..d912648c4 100644 --- a/apps/common-app/src/examples/AudioTag/AudioTag.tsx +++ b/apps/common-app/src/examples/AudioTag/AudioTag.tsx @@ -8,7 +8,8 @@ import { import { Container } from '../../components'; // const DEMO_AUDIO_URL = 'https://filesamples.com/samples/audio/m4a/sample4.m4a'; -const DEMO_AUDIO_URL = 'https://filesamples.com/samples/audio/mp3/sample4.mp3'; +const DEMO_AUDIO_URL = 'https://liveradio.timesa.pl/2980-1.aac/playlist.m3u8'; +// const DEMO_AUDIO_URL = 'https://filesamples.com/samples/audio/mp3/sample4.mp3'; const AudioTag: React.FC = () => { const audioRef = useRef(null); diff --git a/apps/common-app/src/examples/Streaming/Streaming.tsx b/apps/common-app/src/examples/Streaming/Streaming.tsx index ab0393004..97c8de173 100644 --- a/apps/common-app/src/examples/Streaming/Streaming.tsx +++ b/apps/common-app/src/examples/Streaming/Streaming.tsx @@ -30,7 +30,9 @@ const Streaming: FC = () => { console.error('StreamerNode is already initialized'); return; } - streamerRef.current = aCtxRef.current.createStreamer('https://liveradio.timesa.pl/2980-1.aac/playlist.m3u8'); + streamerRef.current = aCtxRef.current.createStreamer( + 'https://liveradio.timesa.pl/2980-1.aac/playlist.m3u8' + ); streamerRef.current.connect(gainRef.current); gainRef.current.connect(aCtxRef.current.destination); diff --git a/apps/fabric-example/ios/Podfile.lock b/apps/fabric-example/ios/Podfile.lock index 79db070cf..45288eaca 100644 --- a/apps/fabric-example/ios/Podfile.lock +++ b/apps/fabric-example/ios/Podfile.lock @@ -2476,7 +2476,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: FBLazyVector: c00c20551d40126351a6783c47ce75f5b374851b - hermes-engine: 91023181d4bc5948b457de5314623fbfe4f8604e + hermes-engine: 146211e12d60a1951d9eb0287be07211e86cf5d5 RCTDeprecation: 3bb167081b134461cfeb875ff7ae1945f8635257 RCTRequired: 74839f55d5058a133a0bc4569b0afec750957f64 RCTSwiftUI: 87a316382f3eab4dd13d2a0d0fd2adcce917361a @@ -2485,7 +2485,7 @@ SPEC CHECKSUMS: React: 1b1536b9099195944034e65b1830f463caaa8390 React-callinvoker: 6dff6d17d1d6cc8fdf85468a649bafed473c65f5 React-Core: 00faa4d038298089a1d5a5b21dde8660c4f0820d - React-Core-prebuilt: a6d614de037caff7898424dfc22915ec792de921 + React-Core-prebuilt: ef40616103ee11f8c2517697c3aa4f48ce790549 React-CoreModules: a17807f849bfd86045b0b9a75ec8c19373b482f6 React-cxxreact: c7b53ace5827be54048288bce5c55f337c41e95f React-debug: e1f00fcd2cef58a2897471a6d76a4ef5f5f90c74 @@ -2549,8 +2549,8 @@ SPEC CHECKSUMS: ReactAppDependencyProvider: 5787b37b8e2e51dfeab697ec031cc7c4080dcea2 ReactCodegen: d07ee3c8db75b43d1cbe479ae6affebf9925c733 ReactCommon: fe2a3af8975e63efa60f95fca8c34dc85deee360 - ReactNativeDependencies: 4d5ce2683b6d74f7c686bf90a88c7d381295cf3c - RNAudioAPI: 764858df27270ed9a55803bb4c9c0ccb5bb14e9a + ReactNativeDependencies: 54189f1570b1308686cb21564e755e1daa77ea03 + RNAudioAPI: 6668f71bdd9850005984acf39a3daef4935cec02 RNGestureHandler: 187c5c7936abf427bc4d22d6c3b1ac80ad1f63c0 RNReanimated: 64f4b3b33b48b19e0ba76a352571b52b1e931981 RNScreens: 01b065ded2dfe7987bcce770ff3a196be417ff41 diff --git a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/utils/NodeOptionsParser.h b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/utils/NodeOptionsParser.h index 4e0212d5e..dea70be35 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/utils/NodeOptionsParser.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/utils/NodeOptionsParser.h @@ -309,7 +309,7 @@ inline AudioFileSourceOptions parseAudioFileSourceOptions( if (sourceValue.isString()) { options.filePath = sourceValue.asString(runtime).utf8(runtime); options.requiresFFmpeg = - audiodecoder::pathHasExtension(options.filePath, {".mp4", ".m4a", ".aac"}); + audiodecoder::pathHasExtension(options.filePath, {".mp4", ".m4a", ".aac", ".m3u8"}); } else if (sourceValue.isObject()) { auto sourceObj = sourceValue.asObject(runtime); if (sourceObj.isArrayBuffer(runtime)) { @@ -317,8 +317,8 @@ inline AudioFileSourceOptions parseAudioFileSourceOptions( auto *data = arrayBuffer.data(runtime); auto size = arrayBuffer.size(runtime); auto format = audiodecoder::detectAudioFormat(data, size); - options.requiresFFmpeg = - format == AudioFormat::MP4 || format == AudioFormat::M4A || format == AudioFormat::AAC; + options.requiresFFmpeg = format == AudioFormat::MP4 || format == AudioFormat::M4A || + format == AudioFormat::AAC || format == AudioFormat::M3U8; options.data = std::vector(data, data + size); } } diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioFileSourceNode.cpp b/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioFileSourceNode.cpp index 6ae7b44e5..8a6f8b515 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioFileSourceNode.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioFileSourceNode.cpp @@ -7,9 +7,14 @@ #include +#include +#include #include +#include #include +#include #include +#include #if !RN_AUDIO_API_FFMPEG_DISABLED #include @@ -21,35 +26,52 @@ AudioFileSourceNode::AudioFileSourceNode( const std::shared_ptr &context, const AudioFileSourceOptions &options) : AudioScheduledSourceNode(context, options), + decoderState_(std::make_shared()), volume_(options.volume), - requiresFFmpeg_(options.requiresFFmpeg), loop_(options.loop), onPositionChangedInterval_( static_cast(context->getSampleRate() * ON_POSITION_CHANGED_INTERVAL)) { const bool useFilePath = !options.filePath.empty(); const bool useData = !options.data.empty(); - if (useFilePath || useData) { - auto state = std::make_shared(); - if (useData) { - state->memoryData = options.data; - } - if (useFilePath) { - state->filePath = options.filePath; - } - initDecoders(useFilePath, context, state); - } + // todo: possibly check for file format, if hls and ffmpeg is not available, then fail to initialize + // ->> add to options parsing validation - if (decoderState_ == nullptr) { - assert(false && "cannot initialize decoder"); + if (!useFilePath && !useData) { + assert(false && "AudioFileSourceNode requires either a file path or memory data to initialize"); return; } - seekOffloader_ = std::make_unique>( - SEEK_OFFLOADER_WORKER_COUNT, [this](OffloadedSeekRequest req) { runOffloadedSeekTask(req); }); + // todo: possibly move to an initialization function, and handle failure more gracefully + auto [frameSender, frameReceiver] = + channels::spsc::channel( + FRAME_SPSC_CHANNEL_CAPACITY); + frameReceiver_ = std::move(frameReceiver); + + auto [commandSender, commandReceiver] = channels::spsc:: + channel( + COMMAND_SPSC_CHANNEL_CAPACITY); + commandSender_ = std::move(commandSender); + + SeekDecoderDaemonOptions daemonOptions{ + .requiresFFmpeg = options.requiresFFmpeg, + .filePath = options.filePath, + .memoryData = options.data, + .contextSampleRate = context->getSampleRate(), + .loop = options.loop}; + + seekDecoderDaemon_ = std::make_unique( + daemonOptions, decoderState_, std::move(commandReceiver), std::move(frameSender)); + + // check if all this is needed or some may be accessed directly from the daemon thread + channelCount_ = decoderState_->channelCount; + sampleRate_ = decoderState_->sampleRate; + duration_ = decoderState_->duration; + + isInitialized_.store(true, std::memory_order_release); + + audioBuffer_ = std::make_shared( + static_cast(RENDER_QUANTUM_SIZE), channelCount_, context->getSampleRate()); isInitialized_.store(true, std::memory_order_release); } @@ -79,44 +101,14 @@ void AudioFileSourceNode::sendOnPositionChangedEvent(int framesPlayed) { onPositionChangedTime_ += framesPlayed; } -void AudioFileSourceNode::initDecoders( - bool useFilePath, - const std::shared_ptr &context, - const std::shared_ptr &state) { - decoding::DecoderResult openResult = Ok(None); - if (requiresFFmpeg_) { -#if !RN_AUDIO_API_FFMPEG_DISABLED - decoder_ = std::make_unique(); -#endif // RN_AUDIO_API_FFMPEG_DISABLED - } else { - decoder_ = std::make_unique(); - } - if (useFilePath) { - openResult = decoder_->openFile(static_cast(context->getSampleRate()), state->filePath); - } else { - openResult = decoder_->openMemory( - static_cast(context->getSampleRate()), - state->memoryData.data(), - state->memoryData.size()); - } - if (openResult.is_ok()) { - state->channels = decoder_->outputChannels(); - state->sampleRate = static_cast(decoder_->outputSampleRate()); - duration_ = static_cast(decoder_->getDurationInSeconds()); - } else { - decoder_->close(); - } - state->interleavedBuffer.resize(static_cast(RENDER_QUANTUM_SIZE) * state->channels); - decoderState_ = state; - channelCount_ = decoderState_->channels; - sampleRate_ = decoderState_->sampleRate; - audioBuffer_ = std::make_shared( - static_cast(RENDER_QUANTUM_SIZE), channelCount_, context->getSampleRate()); -} - void AudioFileSourceNode::start(double when) { AudioScheduledSourceNode::start(when); filePaused_ = false; + + if (seekDecoderDaemon_) { + seekDecoderThread_ = std::thread(std::move(*seekDecoderDaemon_)); + seekDecoderDaemon_.reset(); + } } void AudioFileSourceNode::pause() { @@ -124,23 +116,15 @@ void AudioFileSourceNode::pause() { } void AudioFileSourceNode::disable() { - seekOffloader_.reset(); - filePaused_ = false; - if (decoder_ != nullptr) { - decoder_->close(); + decoderState_->isDaemonRunning.store(false, std::memory_order_release); + commandSender_.send( + SeekRequest{0}); // send a dummy command to unblock the daemon thread if it's waiting + if (seekDecoderThread_.joinable()) { + seekDecoderThread_.join(); } - AudioScheduledSourceNode::disable(); -} - -size_t AudioFileSourceNode::readFrames(float *buf, size_t frameCount) { - if (pendingOffloadedSeeks_.load(std::memory_order_acquire) > 0) { - return 0; - } - return decoder_->readPcmFrames(buf, frameCount); -} + filePaused_ = false; -bool AudioFileSourceNode::seekDecoderToTime(double seconds) { - return decoder_->seekToTime(seconds).is_ok(); + AudioScheduledSourceNode::disable(); } void AudioFileSourceNode::applyPlaybackStateAfterSuccessfulSeek(double seconds) { @@ -148,17 +132,6 @@ void AudioFileSourceNode::applyPlaybackStateAfterSuccessfulSeek(double seconds) onPositionChangedFlush_.store(true, std::memory_order_release); } -void AudioFileSourceNode::runOffloadedSeekTask(OffloadedSeekRequest req) { - if (decoderState_ == nullptr) { - pendingOffloadedSeeks_.fetch_sub(1, std::memory_order_acq_rel); - return; - } - if (seekDecoderToTime(req.seconds)) { - applyPlaybackStateAfterSuccessfulSeek(req.seconds); - } - pendingOffloadedSeeks_.fetch_sub(1, std::memory_order_acq_rel); -} - void AudioFileSourceNode::seekToTime(double seconds) { if (decoderState_ == nullptr) { return; @@ -169,54 +142,42 @@ void AudioFileSourceNode::seekToTime(double seconds) { } else { seconds = std::max(0.0, seconds); } - pendingOffloadedSeeks_.fetch_add(1, std::memory_order_acq_rel); - seekOffloader_->getSender()->send(OffloadedSeekRequest{seconds}); + decoderState_->pendingOffloadedSeeks.fetch_add(1, std::memory_order_acq_rel); + commandSender_.send(SeekRequest{seconds}); } -void AudioFileSourceNode::writeInterleavedToBufferAtOffset( - const std::shared_ptr &processingBuffer, - const AudioFileDecoderState &state, - size_t destFrameOffset, - size_t frameCount) const { - if (frameCount == 0 || volume_ == 0.0f) { - return; - } - processingBuffer->deinterleaveFrom(state.interleavedBuffer.data(), frameCount); - processingBuffer->scale(volume_); -} +size_t AudioFileSourceNode::readInterleavedFrames( + const std::shared_ptr &destBuffer, + size_t framesToRead) { -size_t AudioFileSourceNode::handleEof( - const std::shared_ptr &processingBuffer, - size_t regionFrames, - size_t framesRead, - size_t destFrameOffset) { - if (!loop_) { - return framesRead; - } - - if (!seekDecoderToTime(0)) { - return framesRead; + // 1. Sync Gate: If a seek is active, continuously drain the pipe and return silence + if (decoderState_->pendingOffloadedSeeks.load(std::memory_order_acquire) > 0) { + DecoderData drop; + while (frameReceiver_.try_receive(drop) == ResponseStatus::SUCCESS) {} + return 0; } - const size_t toFill = regionFrames - framesRead; - if (toFill == 0) { - return framesRead; - } + // 2. Direct 1:1 Read: Try to grab exactly one chunk from the queue + DecoderData incoming; + if (frameReceiver_.try_receive(incoming) == ResponseStatus::SUCCESS) { + size_t framesToCopy = std::min(framesToRead, incoming.size); + destBuffer->deinterleaveFrom(incoming.interleavedBuffer.data(), framesToCopy); - auto &state = *decoderState_; - const size_t extra = readFrames(state.interleavedBuffer.data(), toFill); + if (volume_ != 1.0f && framesToCopy > 0) { + destBuffer->scale(volume_); + } - if (volume_ != 0.0f) { - writeInterleavedToBufferAtOffset(processingBuffer, state, destFrameOffset + framesRead, extra); + return framesToCopy; } - return framesRead + extra; + return 0; // Queue empty (connection starved or buffered behind network) } std::shared_ptr AudioFileSourceNode::processNode( const std::shared_ptr &processingBuffer, int framesToProcess) { - if (decoderState_ == nullptr || decoder_ == nullptr || !decoder_->isOpen()) { + + if (decoderState_ == nullptr) { processingBuffer->zero(); return processingBuffer; } @@ -227,16 +188,13 @@ std::shared_ptr AudioFileSourceNode::processNode( return processingBuffer; } - if (pendingOffloadedSeeks_.load(std::memory_order_acquire) > 0) { - processingBuffer->zero(); - return processingBuffer; - } - - if (filePaused_) { + // Handle running sync gate blocks or user pause interactions instantly + if (decoderState_->pendingOffloadedSeeks.load(std::memory_order_acquire) > 0 || filePaused_) { processingBuffer->zero(); return processingBuffer; } + // Web Audio Timeline calculations size_t startOffset = 0; size_t offsetLength = 0; updatePlaybackInfo( @@ -252,30 +210,29 @@ std::shared_ptr AudioFileSourceNode::processNode( return processingBuffer; } - auto &state = *decoderState_; + // Zero out optional leading sub-quantum scheduling padding space + if (startOffset > 0) { + processingBuffer->zero(0, startOffset); + } - size_t framesRead = readFrames(state.interleavedBuffer.data(), offsetLength); + // Consume, deinterleave, and copy chunks safely across the thread line + size_t framesRead = readInterleavedFrames(processingBuffer, offsetLength); sendOnPositionChangedEvent(static_cast(framesRead)); - if (volume_ != 0.0f && framesRead > 0) { - writeInterleavedToBufferAtOffset(processingBuffer, state, startOffset, framesRead); - } - + // Handle End of Stream / End of File boundaries if (framesRead < offsetLength) { - if (!loop_) { + // Because the Daemon loop rewinds internally, a true short-read here means looping is off and song ended + if (!decoderState_->loop.load(std::memory_order_acquire)) { currentTime_.store(duration_, std::memory_order_release); onPositionChangedFlush_.store(true, std::memory_order_release); sendOnPositionChangedEvent(static_cast(offsetLength - framesRead)); + filePaused_ = true; playbackState_ = PlaybackState::STOP_SCHEDULED; - processingBuffer->zero(startOffset + framesRead, offsetLength - framesRead); - } else { - const size_t totalFilled = handleEof(processingBuffer, offsetLength, framesRead, startOffset); - onPositionChangedFlush_.store(true, std::memory_order_release); - currentTime_.store(0, std::memory_order_release); - sendOnPositionChangedEvent(static_cast(totalFilled)); - processingBuffer->zero(startOffset + totalFilled, offsetLength - totalFilled); } + + // Isolate hardware drivers completely from trailing garbage stack noise + processingBuffer->zero(startOffset + framesRead, offsetLength - framesRead); } if (isStopScheduled()) { diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioFileSourceNode.h b/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioFileSourceNode.h index 42db20ad7..eaaeea31f 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioFileSourceNode.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioFileSourceNode.h @@ -2,17 +2,17 @@ #include #include +#include #include +#include +#include #if !RN_AUDIO_API_FFMPEG_DISABLED #include #endif // RN_AUDIO_API_FFMPEG_DISABLED #include -#include #include #include -#include -#include using namespace audioapi::channels; @@ -20,19 +20,19 @@ namespace audioapi { struct AudioFileSourceOptions; -struct OffloadedSeekRequest { - double seconds = 0; - OffloadedSeekRequest() = default; - explicit OffloadedSeekRequest(double t) : seconds(t) {} -}; +inline constexpr auto FRAME_SPSC_OVERFLOW_STRATEGY = + audioapi::channels::spsc::OverflowStrategy::WAIT_ON_FULL; +inline constexpr auto FRAME_SPSC_WAIT_STRATEGY = + audioapi::channels::spsc::WaitStrategy::ATOMIC_WAIT; +inline constexpr auto FRAME_SPSC_CHANNEL_CAPACITY = 32; -struct AudioFileDecoderState { - std::vector memoryData; - std::string filePath; - std::vector interleavedBuffer; - int channels = 0; - float sampleRate = 0; -}; +inline constexpr auto COMMAND_SPSC_OVERFLOW_STRATEGY = + audioapi::channels::spsc::OverflowStrategy::OVERWRITE_ON_FULL; +inline constexpr auto COMMAND_SPSC_WAIT_STRATEGY = + audioapi::channels::spsc::WaitStrategy::ATOMIC_WAIT; +inline constexpr auto COMMAND_SPSC_CHANNEL_CAPACITY = 16; + +inline constexpr auto ON_POSITION_CHANGED_INTERVAL = 0.25f; class AudioFileSourceNode : public AudioScheduledSourceNode { public: @@ -75,54 +75,40 @@ class AudioFileSourceNode : public AudioScheduledSourceNode { int framesToProcess) override; private: - void initDecoders( - bool useFilePath, - const std::shared_ptr &context, - const std::shared_ptr &state); - std::shared_ptr decoderState_; - std::unique_ptr decoder_; float volume_; - bool requiresFFmpeg_; bool filePaused_{false}; bool loop_{false}; double duration_{0}; - std::atomic currentTime_{0}; double sampleRate_{0}; - static constexpr double ON_POSITION_CHANGED_INTERVAL = 0.25f; - static constexpr int SEEK_OFFLOADER_WORKER_COUNT = 16; + std::atomic currentTime_{0}; + + size_t readInterleavedFrames( + const std::shared_ptr &destBuffer, + size_t framesToRead); - size_t readFrames(float *buf, size_t frameCount); bool seekDecoderToTime(double seconds); - void writeInterleavedToBufferAtOffset( - const std::shared_ptr &processingBuffer, - const AudioFileDecoderState &state, - size_t destFrameOffset, - size_t frameCount) const; - size_t handleEof( - const std::shared_ptr &processingBuffer, - size_t regionFrames, - size_t framesRead, - size_t destFrameOffset); void sendOnPositionChangedEvent(int framesPlayed); void applyPlaybackStateAfterSuccessfulSeek(double seconds); - void runOffloadedSeekTask(OffloadedSeekRequest req); + + // Daemon thread for decoding and seeking + std::unique_ptr seekDecoderDaemon_; + std::thread seekDecoderThread_; uint64_t onPositionChangedCallbackId_ = 0; int onPositionChangedInterval_; int onPositionChangedTime_ = 0; std::atomic onPositionChangedFlush_{true}; - /// Pending offloaded seeks; while > 0 the audio thread must not read the decoder (outputs silence). - std::atomic pendingOffloadedSeeks_{0}; + /// SPSC for JS -> Daemon thread communication (seek event) + channels::spsc::Sender + commandSender_; - std::unique_ptr> - seekOffloader_; + /// SPSC for Daemon thread -> Audio thread communication (decoded frames) + channels::spsc::Receiver + frameReceiver_; }; } // namespace audioapi diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/types/AudioFormat.h b/packages/react-native-audio-api/common/cpp/audioapi/core/types/AudioFormat.h index 74fd4f9d1..f13a26c8d 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/types/AudioFormat.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/types/AudioFormat.h @@ -4,5 +4,5 @@ namespace audioapi { -enum class AudioFormat : uint8_t { UNKNOWN, WAV, OGG, FLAC, AAC, MP3, M4A, MP4, MOV }; +enum class AudioFormat : uint8_t { UNKNOWN, WAV, OGG, FLAC, AAC, MP3, M4A, MP4, MOV, M3U8 }; } // namespace audioapi diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/HLSDecoder.cpp b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/HLSDecoder.cpp deleted file mode 100644 index f16a27975..000000000 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/HLSDecoder.cpp +++ /dev/null @@ -1,3 +0,0 @@ -#include - -namespace audioapi {}; // namespace audioapi \ No newline at end of file diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/HLSDecoder.h b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/HLSDecoder.h deleted file mode 100644 index dda066eeb..000000000 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/HLSDecoder.h +++ /dev/null @@ -1,144 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include - -#if !RN_AUDIO_API_FFMPEG_DISABLED -extern "C" { -#include -#include -#include -#include -#include -#include -} -#endif // RN_AUDIO_API_FFMPEG_DISABLED - -#include -#include -#include -#include -#include -#include -#include - -inline constexpr auto HLS_DECODER_SPSC_OVERFLOW_STRATEGY = - audioapi::channels::spsc::OverflowStrategy::WAIT_ON_FULL; -inline constexpr auto HLS_DECODER_SPSC_WAIT_STRATEGY = - audioapi::channels::spsc::WaitStrategy::ATOMIC_WAIT; - -inline constexpr auto VERBOSE = false; -inline constexpr auto CHANNEL_CAPACITY = 32; - -// TODO: check if copy constructors are needed -struct StreamingData { - audioapi::AudioBuffer buffer; - size_t size{0}; - - StreamingData() = default; - ~StreamingData() = default; - StreamingData(audioapi::AudioBuffer b, size_t s) : buffer(std::move(b)), size(s) {} - StreamingData(const StreamingData &data) = default; - StreamingData(StreamingData &&data) noexcept : buffer(std::move(data.buffer)), size(data.size) {} - StreamingData &operator=(StreamingData &&other) = default; - StreamingData &operator=(const StreamingData &data) { - if (this == &data) { - return *this; - } - buffer = data.buffer; - size = data.size; - return *this; - } -}; - -namespace audioapi { -class HLSDecoder { -#if !RN_AUDIO_API_FFMPEG_DISABLED - - /// @brief Initialize the StreamerNode by opening the input stream, - /// finding the audio stream, setting up the decoder, and starting the streaming thread. - /// @param inputUrl The URL of the input stream - /// @return true if initialization was successful, false otherwise - bool initialize(const std::string &inputUrl); - - /** - * @brief Setting up the resampler - * @param outSampleRate Sample rate for the output audio - * @return true if successful, false otherwise - */ - bool setupResampler(float outSampleRate); - - /** - * @brief Resample the audio frame, change its sample format and channel layout - * @param frame The AVFrame to resample - * @param context The context - */ - void processFrameWithResampler(AVFrame *frame, const std::shared_ptr &context); - - /** - * @brief Thread function to continuously read and process audio frames - * @details This function runs in a separate thread to avoid blocking the main audio processing thread - * @note It will read frames from the input stream, resample them, and store them in the buffered buffer - * @note The thread will stop when streamFlag is set to false - */ - void streamAudio(); - - /** @brief Clean up resources */ - void cleanup(); - - /** - * @brief Open the input stream - * @param inputUrl The URL of the input stream - * @return true if successful, false otherwise - * @note This function initializes the FFmpeg libraries and opens the input stream - */ - bool openInput(const std::string &inputUrl); - - /** - * @brief Find the audio stream channel in the input - * @return true if audio stream was found, false otherwise - */ - bool findAudioStream(); - - /** - * @brief Set up the decoder for the audio stream - * @return true if successful, false otherwise - */ - bool setupDecoder(); - - // - AVFormatContext *fmtCtx_; - AVCodecContext *codecCtx_; - const AVCodec *decoder_; - AVCodecParameters *codecpar_; - AVPacket *pkt_; - AVFrame *frame_; // Frame that is currently being processed - SwrContext *swrCtx_; - - // --resampling-- - AudioBuffer resamplerInputBuffer_; - AudioBuffer resamplerOutputBuffer_; - StreamingData bufferedAudioData_; // audio data for buffering hls frames - bool hasBufferedAudioData_; - int audio_stream_index_; // index of the audio stream channel in the input - int maxResampledSamples_; - size_t processedSamples_; - - std::thread streamingThread_; - std::atomic isNodeFinished_; // Flag to control the streaming thread - - // spsc - static constexpr int INITIAL_MAX_RESAMPLED_SAMPLES = 8192; // Initial size for resampled data - channels::spsc:: - Sender - sender_; - channels::spsc:: - Receiver - receiver_; -#endif // RN_AUDIO_API_FFMPEG_DISABLED -}; -} // namespace audioapi \ No newline at end of file diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/decoding/SeekDecoderDaemon.cpp b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/decoding/SeekDecoderDaemon.cpp new file mode 100644 index 000000000..852419afd --- /dev/null +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/decoding/SeekDecoderDaemon.cpp @@ -0,0 +1,116 @@ +#include +#include +#include +namespace audioapi { + +SeekDecoderDaemon::SeekDecoderDaemon( + SeekDecoderDaemonOptions options, + std::shared_ptr sharedState, + channels::spsc::Receiver< + SeekRequest, + channels::spsc::OverflowStrategy::OVERWRITE_ON_FULL, + channels::spsc::WaitStrategy::ATOMIC_WAIT> commandReceiver, + channels::spsc::Sender< + DecoderData, + channels::spsc::OverflowStrategy::WAIT_ON_FULL, + channels::spsc::WaitStrategy::ATOMIC_WAIT> frameSender) + : sharedState_(std::move(sharedState)), + commandReceiver_(std::move(commandReceiver)), + frameSender_(std::move(frameSender)) { + if (options.requiresFFmpeg) { +#if !RN_AUDIO_API_FFMPEG_DISABLED + decoder_ = std::make_unique(); +#endif + } else { + decoder_ = std::make_unique(); + } + + decoding::DecoderResult openResult = Ok(None); + if (!options.filePath.empty()) { + openResult = decoder_->openFile(options.contextSampleRate, options.filePath); + } else if (!options.memoryData.empty()) { + openResult = decoder_->openMemory( + options.contextSampleRate, options.memoryData.data(), options.memoryData.size()); + } + + if (!openResult.is_ok()) { + decoder_->close(); + sharedState_->isDaemonRunning.store(false, std::memory_order_release); + return; + } + + sharedState_->channelCount.store(decoder_->outputChannels(), std::memory_order_release); + sharedState_->sampleRate.store( + static_cast(decoder_->outputSampleRate()), std::memory_order_release); + sharedState_->duration.store(decoder_->getDurationInSeconds(), std::memory_order_release); + sharedState_->isReady.store(true, std::memory_order_release); +} + +void SeekDecoderDaemon::operator()() { + const size_t chunkSize = RENDER_QUANTUM_SIZE; + const size_t chCount = static_cast(sharedState_->channelCount); + + DecoderData localData; + localData.interleavedBuffer.resize(chunkSize * chCount); + bool hasPendingChunk = false; + + while (sharedState_->isDaemonRunning.load(std::memory_order_acquire)) { + SeekRequest seekReq; + bool seekHappened = false; + + while (commandReceiver_.try_receive(seekReq) == ResponseStatus::SUCCESS) { + if (decoder_ && decoder_->isOpen()) { + if (decoder_->seekToTime(seekReq.seconds).is_ok()) { + sharedState_->currentTime.store(seekReq.seconds, std::memory_order_release); + sharedState_->onPositionChangedFlush.store(true, std::memory_order_release); + } + } + seekHappened = true; + } + + if (seekHappened) { + hasPendingChunk = false; // Dump old timeline data + sharedState_->pendingOffloadedSeeks.fetch_sub(1, std::memory_order_release); + continue; + } + + if (!hasPendingChunk) { + if (!decoder_ || !decoder_->isOpen()) { + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + continue; + } + + size_t framesRead = decoder_->readPcmFrames(localData.interleavedBuffer.data(), chunkSize); + + if (framesRead == 0) { + if (sharedState_->loop.load(std::memory_order_acquire)) { + if (decoder_->seekToTime(0).is_ok()) { + sharedState_->currentTime.store(0.0, std::memory_order_release); + sharedState_->onPositionChangedFlush.store(true, std::memory_order_release); + continue; // Loop back immediately to start + } + } + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + continue; + } + + localData.size = framesRead; + hasPendingChunk = true; + } + + if (frameSender_.try_send(std::move(localData)) == ResponseStatus::SUCCESS) { + hasPendingChunk = false; + localData = DecoderData{}; + localData.interleavedBuffer.resize(chunkSize * chCount); + } else { + // Audio thread queue is packed. Yield current time slice. + std::this_thread::yield(); + } + } + + if (decoder_) { + decoder_->close(); + } +} + +} // namespace audioapi \ No newline at end of file diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/decoding/SeekDecoderDaemon.h b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/decoding/SeekDecoderDaemon.h new file mode 100644 index 000000000..036dd9377 --- /dev/null +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/decoding/SeekDecoderDaemon.h @@ -0,0 +1,83 @@ +#pragma once + +#include +#if !RN_AUDIO_API_FFMPEG_DISABLED +#include +#endif // RN_AUDIO_API_FFMPEG_DISABLED +#include +#include +#include +#include +#include + +using namespace audioapi::channels::spsc; + +struct SeekDecoderDaemonOptions { + bool requiresFFmpeg; + // source + std::string filePath; + std::vector memoryData; + // playback + float contextSampleRate; + bool loop; +}; + +struct AudioFileDecoderState { + // Lifecycle and Sync + std::atomic isDaemonRunning{true}; + std::atomic isReady{false}; // True once the decoder opens the file/URL + std::atomic pendingOffloadedSeeks{0}; + + // Metadata + std::atomic channelCount{0}; + std::atomic sampleRate{0.0f}; + std::atomic duration{0.0}; + + std::atomic onPositionChangedFlush{false}; + + // Playback state + std::atomic currentTime{0.0}; + std::atomic loop{false}; +}; + +struct SeekRequest { + double seconds = 0; + SeekRequest() = default; + explicit SeekRequest(double t) : seconds(t) {} +}; + +struct DecoderData { + std::vector interleavedBuffer; + size_t size{}; +}; + +namespace audioapi { + +using CommandReceiver = + Receiver; +using FrameSender = Sender; + +/// @brief SeekDecoderDaemon is a dedicated thread worker that manages an audio decoder instance (FFmpeg or MiniAudio). +/// It listens for seek commands from the audio thread, performs seeks on the decoder, +/// decodes audio frames, and sends decoded planar audio data back to the audio thread via a lock-free SPSC channel. +class SeekDecoderDaemon { + public: + SeekDecoderDaemon( + SeekDecoderDaemonOptions options, + std::shared_ptr sharedState, + CommandReceiver commandReceiver, + FrameSender frameSender); + + /// @brief Main loop of the daemon thread. Listens for seek commands, + /// decodes audio frames, and sends decoded data back to the audio thread + /// via the frameSender SPSC channel. + void operator()(); + + private: + std::shared_ptr sharedState_; + std::unique_ptr decoder_; + CommandReceiver commandReceiver_; + FrameSender frameSender_; +}; + +} // namespace audioapi \ No newline at end of file From a9d5542ab277e2fd304edb7917f9e4ab3269ca3e Mon Sep 17 00:00:00 2001 From: Marek Malek Date: Thu, 28 May 2026 18:22:56 +0200 Subject: [PATCH 4/4] feat: ensure seamless transition between hls chunks --- .../HostObjects/utils/NodeOptionsParser.h | 3 +- .../core/sources/AudioFileSourceNode.cpp | 35 ++++++++++++------- .../core/sources/AudioFileSourceNode.h | 14 ++++---- .../core/utils/decoding/SeekDecoderDaemon.cpp | 15 ++++---- .../core/utils/decoding/SeekDecoderDaemon.h | 4 ++- .../react/Audio/useAudioSourceLoader.ts | 2 +- 6 files changed, 46 insertions(+), 27 deletions(-) diff --git a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/utils/NodeOptionsParser.h b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/utils/NodeOptionsParser.h index 706a18cbe..2f0f0df77 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/utils/NodeOptionsParser.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/HostObjects/utils/NodeOptionsParser.h @@ -3,6 +3,7 @@ #include #include #include +#include #include #include @@ -297,7 +298,7 @@ inline AudioFileSourceOptions parseAudioFileSourceOptions( auto loopValue = optionsObject.getProperty(runtime, "loop"); if (loopValue.isBool()) { - options.loop = static_cast(loopValue.getBool()); + options.loop = loopValue.getBool(); } auto volumeValue = optionsObject.getProperty(runtime, "volume"); diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioFileSourceNode.cpp b/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioFileSourceNode.cpp index 3714f0dd3..98c5d974c 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioFileSourceNode.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioFileSourceNode.cpp @@ -36,7 +36,7 @@ AudioFileSourceNode::AudioFileSourceNode( const bool useFilePath = !options.filePath.empty(); const bool useData = !options.data.empty(); - // todo: possibly check for file format, if hls and ffmpeg is not available, then fail to initialize + // TODO: possibly check for file format, if hls and ffmpeg is not available, then fail to initialize // ->> add to options parsing validation if (!useFilePath && !useData) { @@ -44,7 +44,7 @@ AudioFileSourceNode::AudioFileSourceNode( return; } - // todo: possibly move to an initialization function, and handle failure more gracefully + // TODO: possibly move to an initialization function, and handle failure more gracefully auto [frameSender, frameReceiver] = channels::spsc::channel( FRAME_SPSC_CHANNEL_CAPACITY); @@ -65,7 +65,7 @@ AudioFileSourceNode::AudioFileSourceNode( seekDecoderDaemon_ = std::make_unique( daemonOptions, decoderState_, std::move(commandReceiver), std::move(frameSender)); - // check if all this is needed or some may be accessed directly from the daemon thread + // TODO: check if all this is needed or some may be accessed directly from the daemon thread channelCount_ = decoderState_->channelCount; sampleRate_ = decoderState_->sampleRate; duration_ = decoderState_->duration; @@ -89,7 +89,7 @@ void AudioFileSourceNode::unregisterOnPositionChangedCallback(uint64_t callbackI void AudioFileSourceNode::sendOnPositionChangedEvent(int framesPlayed) { currentTime_.fetch_add(framesPlayed / sampleRate_); if (onPositionChangedCallbackId_ != 0 && - (onPositionChangedFlush_.load(std::memory_order_acquire) || + (decoderState_->onPositionChangedFlush.load(std::memory_order_acquire) || onPositionChangedTime_ > onPositionChangedInterval_)) { audioEventHandlerRegistry_->dispatchEvent( AudioEvent::POSITION_CHANGED, @@ -97,7 +97,7 @@ void AudioFileSourceNode::sendOnPositionChangedEvent(int framesPlayed) { DoubleValuePayload{.value = getCurrentTime()}); onPositionChangedTime_ = 0; - onPositionChangedFlush_.store(false, std::memory_order_release); + decoderState_->onPositionChangedFlush.store(false, std::memory_order_release); } onPositionChangedTime_ += framesPlayed; @@ -177,7 +177,7 @@ void AudioFileSourceNode::disable() { void AudioFileSourceNode::applyPlaybackStateAfterSuccessfulSeek(double seconds) { currentTime_.store(seconds, std::memory_order_release); - onPositionChangedFlush_.store(true, std::memory_order_release); + decoderState_->onPositionChangedFlush.store(true, std::memory_order_release); } void AudioFileSourceNode::seekToTime(double seconds) { @@ -194,18 +194,23 @@ void AudioFileSourceNode::seekToTime(double seconds) { commandSender_.send(SeekRequest{seconds}); } +// TODO: `DecoderData` may be initialized once and then reused as size is stable (128 samples, the render quantum size), +// to avoid dynamic memory allocations on the audio thread. size_t AudioFileSourceNode::readInterleavedFrames( const std::shared_ptr &destBuffer, size_t framesToRead) { - // 1. Sync Gate: If a seek is active, continuously drain the pipe and return silence + // TODO: if the decoder daemon shares frameReceiver_ directly, it can flush the queue when a seek happens, + // seekFlag will guard the audio thread from reading stale data during an active seek + + // If a seek is active, continuously drain the pipe and return silence if (decoderState_->pendingOffloadedSeeks.load(std::memory_order_acquire) > 0) { DecoderData drop; while (frameReceiver_.try_receive(drop) == ResponseStatus::SUCCESS) {} return 0; } - // 2. Direct 1:1 Read: Try to grab exactly one chunk from the queue + // Read from the decoder daemon thread via the SPSC channel. DecoderData incoming; if (frameReceiver_.try_receive(incoming) == ResponseStatus::SUCCESS) { size_t framesToCopy = std::min(framesToRead, incoming.size); @@ -218,7 +223,7 @@ size_t AudioFileSourceNode::readInterleavedFrames( return framesToCopy; } - return 0; // Queue empty (connection starved or buffered behind network) + return 0; } std::shared_ptr AudioFileSourceNode::processNode( @@ -245,6 +250,9 @@ std::shared_ptr AudioFileSourceNode::processDecodedOutput( return processingBuffer; } + // TODO: either handle seek here on in reading frames, no need to duplicate the logic, + // remove pause as it is already handled above + // Handle running sync gate blocks or user pause interactions instantly if (decoderState_->pendingOffloadedSeeks.load(std::memory_order_acquire) > 0 || filePaused_) { processingBuffer->zero(); @@ -276,12 +284,15 @@ std::shared_ptr AudioFileSourceNode::processDecodedOutput( size_t framesRead = readInterleavedFrames(processingBuffer, offsetLength); sendOnPositionChangedEvent(static_cast(framesRead)); + // TODO: make eof/eos more explicit + // Handle End of Stream / End of File boundaries if (framesRead < offsetLength) { - // Because the Daemon loop rewinds internally, a true short-read here means looping is off and song ended - if (!decoderState_->loop.load(std::memory_order_acquire)) { + if (decoderState_->isEof.load(std::memory_order_acquire) && + !decoderState_->loop.load(std::memory_order_acquire)) { + currentTime_.store(duration_, std::memory_order_release); - onPositionChangedFlush_.store(true, std::memory_order_release); + decoderState_->onPositionChangedFlush.store(true, std::memory_order_release); sendOnPositionChangedEvent(static_cast(offsetLength - framesRead)); filePaused_ = true; diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioFileSourceNode.h b/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioFileSourceNode.h index 0c3e01f07..c776813f6 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioFileSourceNode.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/sources/AudioFileSourceNode.h @@ -26,7 +26,7 @@ inline constexpr auto FRAME_SPSC_OVERFLOW_STRATEGY = audioapi::channels::spsc::OverflowStrategy::WAIT_ON_FULL; inline constexpr auto FRAME_SPSC_WAIT_STRATEGY = audioapi::channels::spsc::WaitStrategy::ATOMIC_WAIT; -inline constexpr auto FRAME_SPSC_CHANNEL_CAPACITY = 32; +inline constexpr auto FRAME_SPSC_CHANNEL_CAPACITY = 64; inline constexpr auto COMMAND_SPSC_OVERFLOW_STRATEGY = audioapi::channels::spsc::OverflowStrategy::OVERWRITE_ON_FULL; @@ -38,6 +38,8 @@ inline constexpr auto ON_POSITION_CHANGED_INTERVAL = 0.25f; /// @brief Decodes a file or in-memory buffer and plays it as a scheduled source. /// @note When routed through MediaElementAudioSourceNode, this node outputs silence and the media node pulls decoded audio. +/// @note Seek commands are executed from the JS thread and delegated to @ref SeekDecoderDaemon, which performs the seek and decoding on a worker thread, +// then sends decoded frames back to the audio thread via SPSC channels. class AudioFileSourceNode : public AudioScheduledSourceNode { friend class MediaElementAudioSourceNode; @@ -138,11 +140,12 @@ class AudioFileSourceNode : public AudioScheduledSourceNode { /// @brief Updates playback clock after a successful offloaded seek. void applyPlaybackStateAfterSuccessfulSeek(double seconds); - size_t readInterleavedFrames( + /// @brief Reads decoded interleaved frames from the SPSC channel, deinterleaves them into @p destBuffer, and applies volume. + [[nodiscard]] size_t readInterleavedFrames( const std::shared_ptr &destBuffer, size_t framesToRead); - // Daemon thread for decoding and seeking + /// @brief Daemon thread for decoding and seeking std::unique_ptr seekDecoderDaemon_; std::thread seekDecoderThread_; @@ -153,13 +156,12 @@ class AudioFileSourceNode : public AudioScheduledSourceNode { uint64_t onPositionChangedCallbackId_ = 0; int onPositionChangedInterval_; int onPositionChangedTime_ = 0; - std::atomic onPositionChangedFlush_{true}; - /// SPSC for JS -> Daemon thread communication (seek event) + /// @brief SPSC for JS -> Daemon thread communication (seek event) channels::spsc::Sender commandSender_; - /// SPSC for Daemon thread -> Audio thread communication (decoded frames) + /// @brief SPSC for Daemon thread -> Audio thread communication (decoded frames) channels::spsc::Receiver frameReceiver_; }; diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/decoding/SeekDecoderDaemon.cpp b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/decoding/SeekDecoderDaemon.cpp index 852419afd..1c96dc5af 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/decoding/SeekDecoderDaemon.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/decoding/SeekDecoderDaemon.cpp @@ -19,7 +19,7 @@ SeekDecoderDaemon::SeekDecoderDaemon( frameSender_(std::move(frameSender)) { if (options.requiresFFmpeg) { #if !RN_AUDIO_API_FFMPEG_DISABLED - decoder_ = std::make_unique(); + decoder_ = std::make_unique(); #endif } else { decoder_ = std::make_unique(); @@ -48,7 +48,7 @@ SeekDecoderDaemon::SeekDecoderDaemon( void SeekDecoderDaemon::operator()() { const size_t chunkSize = RENDER_QUANTUM_SIZE; - const size_t chCount = static_cast(sharedState_->channelCount); + const auto chCount = static_cast(sharedState_->channelCount); DecoderData localData; localData.interleavedBuffer.resize(chunkSize * chCount); @@ -76,7 +76,6 @@ void SeekDecoderDaemon::operator()() { if (!hasPendingChunk) { if (!decoder_ || !decoder_->isOpen()) { - std::this_thread::sleep_for(std::chrono::milliseconds(5)); continue; } @@ -87,11 +86,15 @@ void SeekDecoderDaemon::operator()() { if (decoder_->seekToTime(0).is_ok()) { sharedState_->currentTime.store(0.0, std::memory_order_release); sharedState_->onPositionChangedFlush.store(true, std::memory_order_release); - continue; // Loop back immediately to start + continue; } + } else { + sharedState_->isEof.store(true, std::memory_order_release); + continue; } - std::this_thread::sleep_for(std::chrono::milliseconds(10)); - continue; + } else { + // If we read actual frames, ensure the EOF flag remains false + sharedState_->isEof.store(false, std::memory_order_release); } localData.size = framesRead; diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/decoding/SeekDecoderDaemon.h b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/decoding/SeekDecoderDaemon.h index 036dd9377..9b7f0337b 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/decoding/SeekDecoderDaemon.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/decoding/SeekDecoderDaemon.h @@ -22,11 +22,13 @@ struct SeekDecoderDaemonOptions { bool loop; }; +// TODO: check if all those fields are necessary or even used struct AudioFileDecoderState { // Lifecycle and Sync std::atomic isDaemonRunning{true}; std::atomic isReady{false}; // True once the decoder opens the file/URL std::atomic pendingOffloadedSeeks{0}; + std::atomic isEof{false}; // Metadata std::atomic channelCount{0}; @@ -58,7 +60,7 @@ using CommandReceiver = using FrameSender = Sender; /// @brief SeekDecoderDaemon is a dedicated thread worker that manages an audio decoder instance (FFmpeg or MiniAudio). -/// It listens for seek commands from the audio thread, performs seeks on the decoder, +/// It listens for seek commands from the JS thread, performs seeks on the decoder, /// decodes audio frames, and sends decoded planar audio data back to the audio thread via a lock-free SPSC channel. class SeekDecoderDaemon { public: diff --git a/packages/react-native-audio-api/src/development/react/Audio/useAudioSourceLoader.ts b/packages/react-native-audio-api/src/development/react/Audio/useAudioSourceLoader.ts index 086ad67ef..777eb8cd0 100644 --- a/packages/react-native-audio-api/src/development/react/Audio/useAudioSourceLoader.ts +++ b/packages/react-native-audio-api/src/development/react/Audio/useAudioSourceLoader.ts @@ -135,7 +135,7 @@ export function useAudioSourceLoader({ const headers = getSourceHeaders(source); try { - if (path.startsWith('http')) { + if (path.startsWith('http') && !path.endsWith('.m3u8')) { const arrayBuffer = await fetch(path, { headers }).then((response) => response.arrayBuffer() );