From 7739c8f627fd8b2c0e6b0d15051739ac7dd890b7 Mon Sep 17 00:00:00 2001 From: emeric Date: Wed, 18 Feb 2026 23:56:39 +0100 Subject: [PATCH] Init audio context only on first jukebox use --- .../services/jukebox/impl/JukeboxService.cpp | 119 +++++++++++++----- .../services/jukebox/impl/JukeboxService.hpp | 8 ++ .../services/jukebox/IJukeboxService.hpp | 12 ++ src/libs/subsonic/impl/endpoints/Jukebox.cpp | 40 +++++- 4 files changed, 145 insertions(+), 34 deletions(-) diff --git a/src/libs/services/jukebox/impl/JukeboxService.cpp b/src/libs/services/jukebox/impl/JukeboxService.cpp index ac053b44..d664b972 100644 --- a/src/libs/services/jukebox/impl/JukeboxService.cpp +++ b/src/libs/services/jukebox/impl/JukeboxService.cpp @@ -27,6 +27,7 @@ #include #include +#include "core/Exception.hpp" #include "core/ILogger.hpp" #include "core/Random.hpp" @@ -45,14 +46,12 @@ namespace lms::jukebox } JukeboxService::JukeboxService(db::IDb& db, audio::AudioOutputBackend backend) - : _ioContextRunner{ _ioContext, 1, "Jukebox" } + : _backend{ backend } + , _state{ ServiceState::Uninitialized } + , _ioContextRunner{ _ioContext, 1, "Jukebox" } , _db{ db } - , _outputContext{ audio::createAudioOutputContext(_ioContext, "LMS-Jukebox", backend) } { LMS_LOG(JUKEBOX, INFO, "Starting service..."); - - // TODO create a context and an output stream only if a song is actually played - _outputContext->asyncWaitReady([this] { onContextReady(); }); } JukeboxService::~JukeboxService() @@ -65,12 +64,44 @@ namespace lms::jukebox LMS_LOG(JUKEBOX, INFO, "Service stopped!"); } + void JukeboxService::startInit() + { + try + { + std::unique_lock lock{ _mutex }; + + checkState(ServiceState::Uninitialized); + + LMS_LOG(JUKEBOX, INFO, "Starting audio initialization..."); + + _outputContext = audio::createAudioOutputContext(_ioContext, "LMS-Jukebox", _backend); + _state = ServiceState::Initializing; + + // TODO create a context and an output stream only if a song is actually played + _outputContext->asyncWaitReady([this] { onContextReady(); }); + } + catch (const audio::Exception& e) + { + LMS_LOG(JUKEBOX, ERROR, "Cannot create audio context: " << e.what()); + _state = ServiceState::Failed; + } + } + + ServiceState JukeboxService::getState() const + { + std::unique_lock lock{ _mutex }; + + return _state; + } + void JukeboxService::play(std::size_t trackIndex, std::chrono::microseconds offset) { - LMS_LOG(JUKEBOX, INFO, "Playing track index " << trackIndex << " at offset " << std::format("{:%T}", offset)); + LMS_LOG(JUKEBOX, DEBUG, "Playing track index " << trackIndex << " at offset " << std::format("{:%T}", offset)); std::unique_lock lock{ _mutex }; + checkState(ServiceState::Ready); + if (trackIndex >= _tracks.size()) { LMS_LOG(JUKEBOX, INFO, "Requested track index out of bound: stopping"); @@ -78,9 +109,6 @@ namespace lms::jukebox return; } - if (!_outputStream) - return; - abortDecoder(); if (startDecoder(trackIndex, offset)) { @@ -96,24 +124,25 @@ namespace lms::jukebox { std::unique_lock lock{ _mutex }; - if (_outputStream) - _outputStream->pause(); + checkState(ServiceState::Ready); + + _outputStream->pause(); } void JukeboxService::resume() { std::unique_lock lock{ _mutex }; - if (_outputStream) - _outputStream->resume(); + checkState(ServiceState::Ready); + + _outputStream->resume(); } bool JukeboxService::isPaused() const { std::shared_lock lock{ _mutex }; - if (!_outputStream) - return true; + checkState(ServiceState::Ready); return _outputStream->isPaused(); } @@ -122,6 +151,8 @@ namespace lms::jukebox { std::unique_lock lock{ _mutex }; + checkState(ServiceState::Ready); + _outputStream->setVolume(volume); } @@ -129,6 +160,8 @@ namespace lms::jukebox { std::shared_lock lock{ _mutex }; + checkState(ServiceState::Ready); + return _outputStream->getVolume(); } @@ -136,6 +169,8 @@ namespace lms::jukebox { std::shared_lock lock{ _mutex }; + checkState(ServiceState::Ready); + return _currentTrackIndex; } @@ -143,8 +178,7 @@ namespace lms::jukebox { std::shared_lock lock{ _mutex }; - if (!_outputStream) - return {}; + checkState(ServiceState::Ready); const auto playbackTime{ _outputStream->getPlaybackTime() }; @@ -161,6 +195,8 @@ namespace lms::jukebox { std::unique_lock lock{ _mutex }; + checkState(ServiceState::Ready); + _tracks.clear(); _currentTrackIndex.reset(); } @@ -169,6 +205,8 @@ namespace lms::jukebox { std::unique_lock lock{ _mutex }; + checkState(ServiceState::Ready); + if (index >= _tracks.size()) return; @@ -185,10 +223,10 @@ namespace lms::jukebox void JukeboxService::appendTracks(std::span tracks) { - LMS_LOG(JUKEBOX, INFO, "Appending " << tracks.size() << " tracks"); - std::unique_lock lock{ _mutex }; + checkState(ServiceState::Ready); + _tracks.insert(std::end(_tracks), std::cbegin(tracks), std::cend(tracks)); } @@ -196,6 +234,8 @@ namespace lms::jukebox { std::unique_lock lock{ _mutex }; + checkState(ServiceState::Ready); + core::random::shuffleContainer(_tracks); // can't really determine the new pos if the song has been enqueued several times @@ -206,31 +246,51 @@ namespace lms::jukebox { std::shared_lock lock{ _mutex }; + checkState(ServiceState::Ready); + return _tracks; } + void JukeboxService::checkState(ServiceState state) const + { + if (_state != state) + throw core::LmsException{ "Unexpected jukebox state!" }; + } + void JukeboxService::onContextReady() { - _outputStream = _outputContext->createOutputStream("LMS-jukebox", _pcmParams); - _outputStream->asyncWaitReady([this] { onStreamReady(); }); + std::unique_lock lock{ _mutex }; + + try + { + _outputStream = _outputContext->createOutputStream("LMS-jukebox", _pcmParams); + _outputStream->asyncWaitReady([this] { onStreamReady(); }); + } + catch (const audio::Exception& e) + { + LMS_LOG(JUKEBOX, ERROR, "Cannot create audio context: " << e.what()); + _state = ServiceState::Failed; + } } void JukeboxService::onStreamReady() { + LMS_LOG(JUKEBOX, INFO, "Audio initialization complete!"); + audio::utils::PcmDecodeStreamerParameters params{ .outputStream = *_outputStream, .bufferCount = 3, .bufferDuration = std::chrono::milliseconds{ 100 }, }; + std::unique_lock lock{ _mutex }; + _decoder = audio::utils::createPcmDecodeStreamer(_ioContext, params); + _state = ServiceState::Ready; } bool JukeboxService::startDecoder(std::size_t trackIndex, std::chrono::microseconds offset) { - if (!_decoder) - return false; - std::filesystem::path trackPath; { auto& session{ _db.getTLSSession() }; @@ -264,14 +324,11 @@ namespace lms::jukebox void JukeboxService::abortDecoder() { // Must not be called from within owned io_context - if (_decoder) - { - _decoder->abort(); + _decoder->abort(); - // Should be hopefully short since flushing/aborting - while (!_decoder->isComplete()) - std::this_thread::yield(); // TODO: execute some io_context stuff? - } + // Should be hopefully short since flushing/aborting + while (!_decoder->isComplete()) + std::this_thread::yield(); // TODO: execute some io_context stuff? } void JukeboxService::onDecodeFinished(bool aborted) diff --git a/src/libs/services/jukebox/impl/JukeboxService.hpp b/src/libs/services/jukebox/impl/JukeboxService.hpp index ca7db66c..2a6958a8 100644 --- a/src/libs/services/jukebox/impl/JukeboxService.hpp +++ b/src/libs/services/jukebox/impl/JukeboxService.hpp @@ -45,6 +45,9 @@ namespace lms::jukebox JukeboxService& operator=(const JukeboxService&) = delete; private: + void startInit() override; + ServiceState getState() const override; + void play(std::size_t trackIndex, std::chrono::microseconds offset) override; void pause() override; @@ -64,6 +67,8 @@ namespace lms::jukebox void shuffleTracks() override; std::vector getTracks() const override; + void checkState(ServiceState state) const; + void onContextReady(); void onStreamReady(); @@ -82,6 +87,9 @@ namespace lms::jukebox mutable std::shared_mutex _mutex; + const audio::AudioOutputBackend _backend; + ServiceState _state; + std::vector _tracks; // protected by mutex std::optional _currentTrackIndex; // protected by mutex std::chrono::microseconds _currentTrackPlaybackTimeOffset{}; diff --git a/src/libs/services/jukebox/include/services/jukebox/IJukeboxService.hpp b/src/libs/services/jukebox/include/services/jukebox/IJukeboxService.hpp index a24badee..d502154f 100644 --- a/src/libs/services/jukebox/include/services/jukebox/IJukeboxService.hpp +++ b/src/libs/services/jukebox/include/services/jukebox/IJukeboxService.hpp @@ -37,11 +37,23 @@ namespace lms namespace lms::jukebox { + enum class ServiceState + { + Uninitialized, // init not attempted + Initializing, // init in progress + Ready, // init done + Failed, // unrecoverable + }; + class IJukeboxService { public: virtual ~IJukeboxService() = default; + virtual void startInit() = 0; // can be called only if state is Uninitialized + virtual ServiceState getState() const = 0; + + // Can call all methods below only if ready! virtual void play(std::size_t trackIndex, std::chrono::microseconds offset) = 0; virtual void pause() = 0; diff --git a/src/libs/subsonic/impl/endpoints/Jukebox.cpp b/src/libs/subsonic/impl/endpoints/Jukebox.cpp index 6f641273..410de2cc 100644 --- a/src/libs/subsonic/impl/endpoints/Jukebox.cpp +++ b/src/libs/subsonic/impl/endpoints/Jukebox.cpp @@ -20,23 +20,43 @@ #include "Jukebox.hpp" #include +#include #include "core/Service.hpp" #include "database/Session.hpp" #include "database/objects/Track.hpp" #include "database/objects/User.hpp" -#include "responses/Song.hpp" -#include "services/jukebox/IJukeboxService.hpp" #include "ParameterParsing.hpp" #include "SubsonicId.hpp" #include "SubsonicResponse.hpp" +#include "responses/Song.hpp" +#include "services/jukebox/IJukeboxService.hpp" namespace lms::api::subsonic { namespace detail { + void initJukeboxIfNeeded(jukebox::IJukeboxService& jukeboxService) + { + switch (jukeboxService.getState()) + { + case jukebox::ServiceState::Uninitialized: + jukeboxService.startInit(); + [[fallthrough]]; + + case jukebox::ServiceState::Initializing: + while (jukeboxService.getState() == jukebox::ServiceState::Initializing) + std::this_thread::yield(); // should be hopefully quite fast + break; + + case jukebox::ServiceState::Failed: + case jukebox::ServiceState::Ready: + break; + } + } + Response::Node createJukeboxStatusNode(const jukebox::IJukeboxService& jukeboxService) { Response::Node statusNode; @@ -194,11 +214,25 @@ namespace lms::api::subsonic jukebox::IJukeboxService* jukeboxService{ core::Service::get() }; if (!jukeboxService) - throw InternalErrorGenericError{ "Jukebox not available!" }; + throw InternalErrorGenericError{ "Jukebox service disabled" }; if (!context.getUser()->isAdmin()) throw UserNotAuthorizedError{}; + detail::initJukeboxIfNeeded(*jukeboxService); + + switch (jukeboxService->getState()) + { + case jukebox::ServiceState::Failed: + throw InternalErrorGenericError{ "Jukebox service failed" }; + + case jukebox::ServiceState::Ready: + break; + + default: + throw InternalErrorGenericError{ "Bad jukebox state" }; + } + auto itActionHandler{ detail::actionHandlers.find(action) }; if (itActionHandler == std::end(detail::actionHandlers)) throw BadParameterGenericError{ "action" };