diff --git a/src/libs/auth/impl/PasswordService.cpp b/src/libs/auth/impl/PasswordService.cpp index 3ee4d932..0c58b410 100644 --- a/src/libs/auth/impl/PasswordService.cpp +++ b/src/libs/auth/impl/PasswordService.cpp @@ -82,7 +82,7 @@ checkUserPassword(Database::Session& session, const std::string& loginName, cons case Database::User::AuthMode::Internal: { LMS_LOG(AUTH, DEBUG) << "Checking internal password for user '" << loginName << "'"; - const Wt::Auth::BCryptHashFunction hashFunc {6}; // TODO parametrize this + const Wt::Auth::BCryptHashFunction hashFunc {7}; // TODO parametrize this return hashFunc.verify(password, passwordHash.salt, passwordHash.hash); } diff --git a/src/libs/av/CMakeLists.txt b/src/libs/av/CMakeLists.txt index a5c24167..c42826bf 100644 --- a/src/libs/av/CMakeLists.txt +++ b/src/libs/av/CMakeLists.txt @@ -1,9 +1,9 @@ add_library(lmsav SHARED - impl/AvInfo.cpp - impl/AvTranscoder.cpp - impl/AvTranscodeResourceHandler.cpp - impl/AvTypes.cpp + impl/AudioFile.cpp + impl/Transcoder.cpp + impl/TranscodeResourceHandler.cpp + impl/Types.cpp ) target_include_directories(lmsav INTERFACE diff --git a/src/libs/av/impl/AvInfo.cpp b/src/libs/av/impl/AudioFile.cpp similarity index 80% rename from src/libs/av/impl/AvInfo.cpp rename to src/libs/av/impl/AudioFile.cpp index fa893871..b9b51524 100644 --- a/src/libs/av/impl/AvInfo.cpp +++ b/src/libs/av/impl/AudioFile.cpp @@ -17,7 +17,7 @@ * along with LMS. If not, see . */ -#include "av/AvInfo.hpp" +#include "AudioFile.hpp" extern "C" { @@ -44,20 +44,28 @@ static std::string averror_to_string(int error) return "Unknown error"; } -MediaFileException::MediaFileException(int avError) -: AvException {"MediaFileException: " + averror_to_string(avError)} +class AudioFileException : public Av::Exception { + public: + AudioFileException(int avError) + : Av::Exception {"AudioFileException: " + averror_to_string(avError)} + {} +}; + +std::unique_ptr +parseAudioFile(const std::filesystem::path& p) +{ + return std::make_unique(p); } - -MediaFile::MediaFile(const std::filesystem::path& p) +AudioFile::AudioFile(const std::filesystem::path& p) : _p {p} { - int error = avformat_open_input(&_context, _p.string().c_str(), nullptr, nullptr); + int error {avformat_open_input(&_context, _p.string().c_str(), nullptr, nullptr)}; if (error < 0) { LMS_LOG(AV, ERROR) << "Cannot open " << _p.string() << ": " << averror_to_string(error); - throw MediaFileException(error); + throw AudioFileException {error}; } error = avformat_find_stream_info(_context, nullptr); @@ -65,32 +73,32 @@ MediaFile::MediaFile(const std::filesystem::path& p) { LMS_LOG(AV, ERROR) << "Cannot find stream information on " << _p.string() << ": " << averror_to_string(error); avformat_close_input(&_context); - throw MediaFileException(error); + throw AudioFileException {error}; } } -MediaFile::~MediaFile() +AudioFile::~AudioFile() { avformat_close_input(&_context); } -std::string -MediaFile::getFormatName() const +const std::filesystem::path& +AudioFile::getPath() const { - return _context->iformat->name; + return _p; } std::chrono::milliseconds -MediaFile::getDuration() const +AudioFile::getDuration() const { if (_context->duration == AV_NOPTS_VALUE) - return std::chrono::milliseconds(0); // TODO estimate + return std::chrono::milliseconds {0}; // TODO estimate - return std::chrono::milliseconds(_context->duration / AV_TIME_BASE * 1000); + return std::chrono::milliseconds {_context->duration / AV_TIME_BASE * 1000}; } void -getMetaDataFromDictionnary(AVDictionary* dictionnary, std::map& res) +getMetaDataFromDictionnary(AVDictionary* dictionnary, AudioFile::MetadataMap& res) { if (!dictionnary) return; @@ -102,10 +110,10 @@ getMetaDataFromDictionnary(AVDictionary* dictionnary, std::map -MediaFile::getMetaData(void) +AudioFile::MetadataMap +AudioFile::getMetaData() const { - std::map res; + MetadataMap res; getMetaDataFromDictionnary(_context->metadata, res); @@ -113,7 +121,7 @@ MediaFile::getMetaData(void) // If we did not find tags, search metadata in streams if (res.empty()) { - for (std::size_t i = 0; i < _context->nb_streams; ++i) + for (std::size_t i {}; i < _context->nb_streams; ++i) { getMetaDataFromDictionnary(_context->streams[i]->metadata, res); @@ -126,7 +134,7 @@ MediaFile::getMetaData(void) } std::vector -MediaFile::getStreamInfo() const +AudioFile::getStreamInfo() const { std::vector res; @@ -154,7 +162,7 @@ MediaFile::getStreamInfo() const } std::optional -MediaFile::getBestStream() const +AudioFile::getBestStream() const { int res = av_find_best_stream(_context, AVMEDIA_TYPE_AUDIO, @@ -170,7 +178,7 @@ MediaFile::getBestStream() const } bool -MediaFile::hasAttachedPictures(void) const +AudioFile::hasAttachedPictures(void) const { for (std::size_t i = 0; i < _context->nb_streams; ++i) { @@ -182,9 +190,9 @@ MediaFile::hasAttachedPictures(void) const } void -MediaFile::visitAttachedPictures(std::function func) const +AudioFile::visitAttachedPictures(std::function func) const { - static const std::map codecMimeMap = + static const std::unordered_map codecMimeMap = { { AV_CODEC_ID_BMP, "image/x-bmp" }, { AV_CODEC_ID_GIF, "image/gif" }, @@ -230,7 +238,7 @@ MediaFile::visitAttachedPictures(std::function func) const } } -std::optional +std::optional guessMediaFileFormat(const std::filesystem::path& file) { AVOutputFormat* format {av_guess_format(NULL,file.string().c_str(),NULL)}; @@ -252,7 +260,7 @@ guessMediaFileFormat(const std::filesystem::path& file) else if (mimeTypes.size() > 1) LMS_LOG(AV, INFO) << "File '" << file.string() << "' reported several mime types: '" << format->mime_type << "'"; - MediaFileFormat res; + AudioFileFormat res; res.format = formats.front(); res.mimeType = mimeTypes.empty() ? "application/octet-stream" : mimeTypes.front(); diff --git a/src/libs/av/impl/AudioFile.hpp b/src/libs/av/impl/AudioFile.hpp new file mode 100644 index 00000000..82f1dfb5 --- /dev/null +++ b/src/libs/av/impl/AudioFile.hpp @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2020 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +/* This file contains some classes in order to get info from file using the libavconv */ + +#pragma once + +#include "av/IAudioFile.hpp" + +struct AVFormatContext; + +namespace Av +{ + + class AudioFile final : public IAudioFile + { + public: + AudioFile(const std::filesystem::path& p); + ~AudioFile(); + + AudioFile(const AudioFile&) = delete; + AudioFile(AudioFile&&) = delete; + AudioFile& operator=(const AudioFile&) = delete; + AudioFile& operator=(AudioFile&&) = delete; + + const std::filesystem::path& getPath() const override; + std::chrono::milliseconds getDuration() const override; + MetadataMap getMetaData() const override; + std::vector getStreamInfo() const override; + std::optional getBestStream() const override; + bool hasAttachedPictures() const override; + void visitAttachedPictures(std::function func) const override; + + private: + + const std::filesystem::path _p; + AVFormatContext* _context {}; + }; + +} // namespace Av + diff --git a/src/libs/av/impl/AvTranscodeResourceHandler.cpp b/src/libs/av/impl/TranscodeResourceHandler.cpp similarity index 69% rename from src/libs/av/impl/AvTranscodeResourceHandler.cpp rename to src/libs/av/impl/TranscodeResourceHandler.cpp index fbea1f2f..7cbd99c6 100644 --- a/src/libs/av/impl/AvTranscodeResourceHandler.cpp +++ b/src/libs/av/impl/TranscodeResourceHandler.cpp @@ -17,7 +17,7 @@ * along with LMS. If not, see . */ -#include "AvTranscodeResourceHandler.hpp" +#include "TranscodeResourceHandler.hpp" namespace Av { @@ -36,24 +36,32 @@ namespace Av _transcoder.start(); } - void + Wt::Http::ResponseContinuation* TranscodeResourceHandler::processRequest(const Wt::Http::Request& /*request*/, Wt::Http::Response& response) { response.setMimeType(_transcoder.getOutputMimeType()); - if (!_transcoder.isComplete()) + if (_nbBytesReady > 0) { - std::vector buffer; - - _transcoder.process(buffer, _chunkSize); - response.out().write(reinterpret_cast(&buffer[0]), buffer.size()); + response.out().write(reinterpret_cast(&_buffer[0]), _nbBytesReady); + _nbBytesReady = 0; } - } - bool - TranscodeResourceHandler::isFinished() const - { - return _transcoder.isComplete(); + if (!_transcoder.finished()) + { + Wt::Http::ResponseContinuation *continuation {response.createContinuation()}; + continuation->waitForMoreData(); + _transcoder.asyncRead(_buffer.data(), _buffer.size(), [=](std::size_t nbBytesRead) + { + assert(_nbBytesReady == 0); + _nbBytesReady = nbBytesRead; + continuation->haveMoreData(); + }); + + return continuation; + } + + return {}; } } diff --git a/src/libs/av/impl/AvTranscodeResourceHandler.hpp b/src/libs/av/impl/TranscodeResourceHandler.hpp similarity index 76% rename from src/libs/av/impl/AvTranscodeResourceHandler.hpp rename to src/libs/av/impl/TranscodeResourceHandler.hpp index 543ca74b..87a6f7e0 100644 --- a/src/libs/av/impl/AvTranscodeResourceHandler.hpp +++ b/src/libs/av/impl/TranscodeResourceHandler.hpp @@ -19,9 +19,12 @@ #pragma once +#include #include -#include "av/AvTranscoder.hpp" + +#include "av/TranscodeParameters.hpp" #include "utils/IResourceHandler.hpp" +#include "Transcoder.hpp" namespace Av { @@ -33,10 +36,11 @@ namespace Av private: - void processRequest(const Wt::Http::Request& request, Wt::Http::Response& reponse) override; - bool isFinished() const override; + Wt::Http::ResponseContinuation* processRequest(const Wt::Http::Request& request, Wt::Http::Response& reponse) override; - static constexpr std::size_t _chunkSize {262144}; + static constexpr std::size_t _chunkSize {32768}; + std::array _buffer; + std::size_t _nbBytesReady {}; const std::filesystem::path _trackPath; Transcoder _transcoder; }; diff --git a/src/libs/av/impl/AvTranscoder.cpp b/src/libs/av/impl/Transcoder.cpp similarity index 65% rename from src/libs/av/impl/AvTranscoder.cpp rename to src/libs/av/impl/Transcoder.cpp index 574f8299..bbbec080 100644 --- a/src/libs/av/impl/AvTranscoder.cpp +++ b/src/libs/av/impl/Transcoder.cpp @@ -17,12 +17,12 @@ * along with LMS. If not, see . */ -#include "av/AvTranscoder.hpp" +#include "Transcoder.hpp" #include -#include +#include -#include "av/AvInfo.hpp" +#include "utils/IChildProcessManager.hpp" #include "utils/IConfig.hpp" #include "utils/Path.hpp" #include "utils/Logger.hpp" @@ -32,7 +32,7 @@ namespace Av { #define LOG(sev) LMS_LOG(TRANSCODE, sev) << "[" << _id << "] - " -static std::atomic globalId {}; +static std::atomic globalId {}; static std::filesystem::path ffmpegPath; void @@ -40,39 +40,41 @@ Transcoder::init() { ffmpegPath = Service::get()->getPath("ffmpeg-file", "/usr/bin/ffmpeg"); if (!std::filesystem::exists(ffmpegPath)) - throw LmsException {"File '" + ffmpegPath.string() + "' does not exist!"}; + throw Exception {"File '" + ffmpegPath.string() + "' does not exist!"}; } Transcoder::Transcoder(const std::filesystem::path& filePath, const TranscodeParameters& parameters) -: _filePath {filePath}, - _parameters {parameters}, - _id {globalId++} +: _id {globalId++} +, _filePath {filePath} +, _parameters {parameters} { } +Transcoder::~Transcoder() = default; + bool Transcoder::start() { + if (ffmpegPath.empty()) + init(); + try { if (!std::filesystem::exists(_filePath)) { LOG(ERROR) << "File '" << _filePath << "' does not exist!"; - _isComplete = true; return false; } else if (!std::filesystem::is_regular_file( _filePath) ) { LOG(ERROR) << "File '" << _filePath << "' is not regular!"; - _isComplete = true; return false; } } catch (const std::filesystem::filesystem_error& e) { LOG(ERROR) << "File error on '" << _filePath.string() << "': " << e.what(); - _isComplete = true; return false; } @@ -82,17 +84,21 @@ Transcoder::start() args.emplace_back(ffmpegPath.string()); - // Make sure we do not produce anything in the stderr output + // Make sure: + // - we do not produce anything in the stderr output + // - we do not rely on input // in order not to block the whole forked process args.emplace_back("-loglevel"); args.emplace_back("quiet"); args.emplace_back("-nostdin"); // input Offset - if (_parameters.offset) { args.emplace_back("-ss"); - args.emplace_back(std::to_string((*_parameters.offset).count())); + + std::ostringstream oss; + oss << std::fixed << std::showpoint << std::setprecision(3) << (_parameters.offset.count() / float {1000}); + args.emplace_back(oss.str()); } // Input file @@ -120,6 +126,7 @@ Transcoder::start() args.emplace_back("-b:a"); args.emplace_back(std::to_string(_parameters.bitrate)); + // Codecs and formats switch (_parameters.format) { @@ -157,7 +164,6 @@ Transcoder::start() break; default: - _isComplete = true; return false; } @@ -169,84 +175,56 @@ Transcoder::start() for (const std::string& arg : args) LOG(DEBUG) << "Arg = '" << arg << "'"; - // make sure only one thread is executing this part of code + // Caution: stdin must have been closed before + try { - static std::mutex transcoderMutex; - - std::lock_guard lock {transcoderMutex}; - - _child = std::make_shared(); - - // Caution: stdin must have been closed before - _child->open(ffmpegPath.string(), args); - if (!_child->is_open()) - { - LOG(DEBUG) << "Exec failed!"; - _isComplete = true; - return false; - } - - if (_child->out().eof()) - { - LOG(DEBUG) << "Early end of file!"; - _isComplete = true; - return false; - } - + _childProcess = Service::get()->spawnChildProcess(ffmpegPath, args); + } + catch (ChildProcessException& exception) + { + LOG(ERROR) << "Cannot execute '" << ffmpegPath << "': " << exception.what(); + return false; } - LOG(DEBUG) << "Stream opened!"; return true; } void -Transcoder::process(std::vector& output, std::size_t maxSize) +Transcoder::asyncWaitForData(WaitCallback cb) { - if (!_child || _isComplete) - return; + assert(_childProcess); - output.resize(maxSize); + LOG(DEBUG) << "Want to wait for data"; - //Read on the output stream - _child->out().read(reinterpret_cast(&output[0]), maxSize); - output.resize(_child->out().gcount()); - - if (_child->out().fail()) + _childProcess->asyncWaitForData([cb = std::move(cb)] { - LOG(DEBUG) << "Stdout FAILED"; - _isComplete = true; - } - - if (_child->out().eof()) - { - LOG(DEBUG) << "Stdout EOF!"; - _isComplete = true; - } - - if (_isComplete) - { - LOG(DEBUG) << "Transcode complete!"; - _child->clear(); - _child.reset(); - } - - _total += output.size(); - - LOG(DEBUG) << "nb bytes = " << output.size() << ", total = " << _total; + cb(); + }); } -Transcoder::~Transcoder() +void +Transcoder::asyncRead(std::byte* buffer, std::size_t bufferSize, ReadCallback readCallback) { - LOG(DEBUG) << ", ~Transcoder called! Total produced bytes = " << _total; + assert(_childProcess); - if (_child) + return _childProcess->asyncRead(buffer, bufferSize, [readCallback {std::move(readCallback)}](IChildProcess::ReadResult /*res*/, std::size_t nbBytesRead) { - LOG(DEBUG) << "Child still here!"; - _child->rdbuf()->kill(SIGKILL); - LOG(DEBUG) << "Closing..."; - _child->rdbuf()->close(); - LOG(DEBUG) << "Closing DONE"; - } + readCallback(nbBytesRead); + }); +} + +std::size_t +Transcoder::readSome(std::byte* buffer, std::size_t bufferSize) +{ + assert(_childProcess); + + return _childProcess->readSome(buffer, bufferSize); +} + +bool +Transcoder::finished() const +{ + return _childProcess->finished(); } } // namespace Transcode diff --git a/src/libs/av/impl/Transcoder.hpp b/src/libs/av/impl/Transcoder.hpp new file mode 100644 index 00000000..aa9ace31 --- /dev/null +++ b/src/libs/av/impl/Transcoder.hpp @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2015 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#pragma once + +#include +#include + +#include "av/TranscodeParameters.hpp" +#include "av/Types.hpp" + +class IChildProcess; + +namespace Av +{ + class Transcoder + { + public: + Transcoder(const std::filesystem::path& file, const TranscodeParameters& parameters); + ~Transcoder(); + + Transcoder(const Transcoder&) = delete; + Transcoder& operator=(const Transcoder&) = delete; + Transcoder(Transcoder&&) = delete; + Transcoder& operator=(Transcoder&&) = delete; + + bool start(); + + using WaitCallback = std::function; + void asyncWaitForData(WaitCallback cb); + + // non blocking calls + using ReadCallback = std::function; + void asyncRead(std::byte* buffer, std::size_t bufferSize, ReadCallback); + std::size_t readSome(std::byte* buffer, std::size_t bufferSize); + + const std::string& getOutputMimeType() const { return _outputMimeType; } + const TranscodeParameters& getParameters() const { return _parameters; } + + bool finished() const; + + private: + static void init(); + + const std::size_t _id {}; + const std::filesystem::path _filePath; + const TranscodeParameters _parameters; + + std::unique_ptr _childProcess; + + bool _finished {}; + std::string _outputMimeType; + }; + +} // namespace Av + diff --git a/src/libs/av/impl/AvTypes.cpp b/src/libs/av/impl/Types.cpp similarity index 63% rename from src/libs/av/impl/AvTypes.cpp rename to src/libs/av/impl/Types.cpp index ca83978f..713491bc 100644 --- a/src/libs/av/impl/AvTypes.cpp +++ b/src/libs/av/impl/Types.cpp @@ -17,23 +17,25 @@ * along with LMS. If not, see . */ -#include "av/AvTypes.hpp" +#include "av/Types.hpp" -namespace Av { - -const char* formatToMimetype(Format format) +namespace Av { - switch (format) + + std::string_view + formatToMimetype(Format format) { - case Format::MP3: return "audio/mpeg"; - case Format::OGG_OPUS: return "audio/opus"; - case Format::MATROSKA_OPUS: return "audio/x-matroska"; - case Format::OGG_VORBIS: return "audio/ogg"; - case Format::WEBM_VORBIS: return "audio/webm"; + switch (format) + { + case Format::MP3: return "audio/mpeg"; + case Format::OGG_OPUS: return "audio/opus"; + case Format::MATROSKA_OPUS: return "audio/x-matroska"; + case Format::OGG_VORBIS: return "audio/ogg"; + case Format::WEBM_VORBIS: return "audio/webm"; + } + + throw Exception {"Invalid encoding"}; } - throw AvException {"Invalid encoding"}; -} - } diff --git a/src/libs/av/include/av/AvInfo.hpp b/src/libs/av/include/av/AvInfo.hpp deleted file mode 100644 index 60fe0844..00000000 --- a/src/libs/av/include/av/AvInfo.hpp +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright (C) 2015 Emeric Poupon - * - * This file is part of LMS. - * - * LMS is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LMS is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LMS. If not, see . - */ - -/* This file contains some classes in order to get info from file using the libavconv */ - -#pragma once - -#include -#include -#include -#include -#include -#include -#include - -#include "AvTypes.hpp" - -struct AVFormatContext; - -namespace Av -{ - -void AvInit(); - -struct Picture -{ - std::string mimeType; - const std::byte* data {}; - std::size_t dataSize; -}; - -struct StreamInfo -{ - size_t id; - std::size_t bitrate; -}; - -class MediaFileException : public AvException -{ - public: - MediaFileException(int avError); -}; - -class MediaFile -{ - public: - MediaFile(const std::filesystem::path& p); - ~MediaFile(); - - MediaFile(const MediaFile&) = delete; - MediaFile& operator=(const MediaFile&) = delete; - MediaFile(MediaFile&&) = delete; - MediaFile& operator=(MediaFile&&) = delete; - - std::string getFormatName() const; - - const std::filesystem::path& getPath() const {return _p;}; - - std::chrono::milliseconds getDuration() const; - std::map getMetaData(void); - - std::vector getStreamInfo() const; - std::optional getBestStream() const; // none if failure/unknown - bool hasAttachedPictures(void) const; - void visitAttachedPictures(std::function func) const; - - private: - - const std::filesystem::path _p; - AVFormatContext* _context {}; -}; - - -struct MediaFileFormat -{ - std::string mimeType; - std::string format; -}; - -std::optional guessMediaFileFormat(const std::filesystem::path& file); - -} // namespace Av - diff --git a/src/libs/av/include/av/AvTranscoder.hpp b/src/libs/av/include/av/AvTranscoder.hpp deleted file mode 100644 index 4dfff1bb..00000000 --- a/src/libs/av/include/av/AvTranscoder.hpp +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright (C) 2015 Emeric Poupon - * - * This file is part of LMS. - * - * LMS is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LMS is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LMS. If not, see . - */ - -#pragma once - -#include -#include -#include - -#include - -#include "AvTypes.hpp" - -namespace Av { - - - -struct TranscodeParameters -{ - Format format; - std::size_t bitrate {128000}; - std::optional stream; // Id of the stream to be transcoded (auto detect by default) - std::optional offset; - bool stripMetadata {true}; -}; - -class Transcoder -{ - public: - static void init(); - - Transcoder(const std::filesystem::path& file, const TranscodeParameters& parameters); - ~Transcoder(); - - Transcoder(const Transcoder&) = delete; - Transcoder& operator=(const Transcoder&) = delete; - Transcoder(Transcoder&&) = delete; - Transcoder& operator=(Transcoder&&) = delete; - - bool start(); - const std::string& getOutputMimeType() const { return _outputMimeType; } - void process(std::vector& output, std::size_t maxSize); - bool isComplete(void) const { return _isComplete; } - - const TranscodeParameters& getParameters() const { return _parameters; } - - private: - const std::filesystem::path _filePath; - const TranscodeParameters _parameters; - - std::shared_ptr _child; - - bool _isComplete {}; - std::size_t _total {}; - const std::size_t _id {}; - std::string _outputMimeType; -}; - -} // namespace Av - diff --git a/src/libs/av/include/av/IAudioFile.hpp b/src/libs/av/include/av/IAudioFile.hpp new file mode 100644 index 00000000..3a5b9a9c --- /dev/null +++ b/src/libs/av/include/av/IAudioFile.hpp @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2015 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +/* This file contains some classes in order to get info from file using the libavconv */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include "Types.hpp" + +namespace Av +{ + struct Picture + { + std::string mimeType; + const std::byte* data {}; + std::size_t dataSize; + }; + + struct StreamInfo + { + size_t id; + std::size_t bitrate; + }; + + class IAudioFile + { + public: + virtual ~IAudioFile() = default; + + using MetadataMap = std::unordered_map; + + virtual const std::filesystem::path& getPath() const = 0; + virtual std::chrono::milliseconds getDuration() const = 0; + virtual MetadataMap getMetaData() const = 0; + virtual std::vector getStreamInfo() const = 0; + virtual std::optional getBestStream() const = 0; // none if failure/unknown + virtual bool hasAttachedPictures() const = 0; + virtual void visitAttachedPictures(std::function func) const = 0; + }; + + std::unique_ptr parseAudioFile(const std::filesystem::path& p); + + struct AudioFileFormat + { + std::string mimeType; + std::string format; + }; + + std::optional guessAudioFileFormat(const std::filesystem::path& file); + +} // namespace Av + diff --git a/src/libs/av/include/av/TranscodeParameters.hpp b/src/libs/av/include/av/TranscodeParameters.hpp new file mode 100644 index 00000000..7449e2d9 --- /dev/null +++ b/src/libs/av/include/av/TranscodeParameters.hpp @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2015 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#pragma once + +#include +#include + +#include "Types.hpp" + +namespace Av { + +struct TranscodeParameters +{ + Format format; + std::size_t bitrate {128000}; + std::optional stream; // Id of the stream to be transcoded (auto detect by default) + std::chrono::milliseconds offset {0}; + bool stripMetadata {true}; +}; + +} // namespace Av + diff --git a/src/libs/av/include/av/AvTranscodeResourceHandlerCreator.hpp b/src/libs/av/include/av/TranscodeResourceHandlerCreator.hpp similarity index 99% rename from src/libs/av/include/av/AvTranscodeResourceHandlerCreator.hpp rename to src/libs/av/include/av/TranscodeResourceHandlerCreator.hpp index 041ebc91..0255cfc4 100644 --- a/src/libs/av/include/av/AvTranscodeResourceHandlerCreator.hpp +++ b/src/libs/av/include/av/TranscodeResourceHandlerCreator.hpp @@ -29,6 +29,5 @@ namespace Av struct TranscodeParameters; std::unique_ptr createTranscodeResourceHandler(const std::filesystem::path& trackPath, const TranscodeParameters& parameters); - } diff --git a/src/libs/av/include/av/AvTypes.hpp b/src/libs/av/include/av/Types.hpp similarity index 67% rename from src/libs/av/include/av/AvTypes.hpp rename to src/libs/av/include/av/Types.hpp index f2c942a9..d1cc4cb5 100644 --- a/src/libs/av/include/av/AvTypes.hpp +++ b/src/libs/av/include/av/Types.hpp @@ -19,29 +19,28 @@ #pragma once -#include +#include #include "utils/Exception.hpp" namespace Av { -class AvException : public LmsException -{ - public: - AvException(const std::string& msg) : LmsException(msg) {} -}; + class Exception : public LmsException + { + public: + using LmsException::LmsException; + }; -enum class Format -{ - // Values are important and must not be changed - MP3 = 0, - OGG_OPUS = 1, - MATROSKA_OPUS = 2, - OGG_VORBIS = 3, - WEBM_VORBIS = 4, -}; - -const char* formatToMimetype(Format encoding); + enum class Format + { + // Values are important and must not be changed (stored in the UI's localstorage) + MP3 = 0, + OGG_OPUS = 1, + MATROSKA_OPUS = 2, + OGG_VORBIS = 3, + WEBM_VORBIS = 4, + }; + std::string_view formatToMimetype(Format format); } diff --git a/src/libs/cover/impl/CoverArtGrabber.cpp b/src/libs/cover/impl/CoverArtGrabber.cpp index 8644ef4b..1227fcd6 100644 --- a/src/libs/cover/impl/CoverArtGrabber.cpp +++ b/src/libs/cover/impl/CoverArtGrabber.cpp @@ -19,7 +19,7 @@ #include "CoverArtGrabber.hpp" -#include "av/AvInfo.hpp" +#include "av/IAudioFile.hpp" #include "database/Release.hpp" #include "database/Session.hpp" @@ -125,7 +125,7 @@ Grabber::Grabber(const std::filesystem::path& execPath, } std::unique_ptr -Grabber::getFromAvMediaFile(const Av::MediaFile& input, ImageSize width) const +Grabber::getFromAvMediaFile(const Av::IAudioFile& input, ImageSize width) const { std::unique_ptr image; @@ -303,10 +303,9 @@ Grabber::getFromTrack(const std::filesystem::path& p, ImageSize width) const try { - const Av::MediaFile input {p}; - image = getFromAvMediaFile(input, width); + image = getFromAvMediaFile(*Av::parseAudioFile(p), width); } - catch (Av::AvException& e) + catch (Av::Exception& e) { LMS_LOG(COVER, ERROR) << "Cannot get covers from track " << p.string() << ": " << e.what(); } diff --git a/src/libs/cover/impl/CoverArtGrabber.hpp b/src/libs/cover/impl/CoverArtGrabber.hpp index 3ba39a41..abe02fbc 100644 --- a/src/libs/cover/impl/CoverArtGrabber.hpp +++ b/src/libs/cover/impl/CoverArtGrabber.hpp @@ -39,7 +39,7 @@ namespace Database namespace Av { - class MediaFile; + class IAudioFile; } namespace CoverArt @@ -106,7 +106,7 @@ namespace CoverArt void flushCache() override; std::shared_ptr getFromTrack(Database::Session& dbSession, Database::IdType trackId, ImageSize width, bool allowReleaseFallback); - std::unique_ptr getFromAvMediaFile(const Av::MediaFile& input, ImageSize width) const; + std::unique_ptr getFromAvMediaFile(const Av::IAudioFile& input, ImageSize width) const; std::unique_ptr getFromCoverFile(const std::filesystem::path& p, ImageSize width) const; std::unique_ptr getFromTrack(const std::filesystem::path& path, ImageSize width) const; diff --git a/src/libs/metadata/impl/AvFormatParser.cpp b/src/libs/metadata/impl/AvFormatParser.cpp index b55eed4d..3bde19ab 100644 --- a/src/libs/metadata/impl/AvFormatParser.cpp +++ b/src/libs/metadata/impl/AvFormatParser.cpp @@ -22,7 +22,7 @@ #include #include -#include "av/AvInfo.hpp" +#include "av/IAudioFile.hpp" #include "utils/Logger.hpp" #include "utils/String.hpp" @@ -30,11 +30,9 @@ namespace MetaData { -using MetadataMap = std::map; - template std::optional -findFirstValueOfAs(const MetadataMap& metadataMap, std::initializer_list tags) +findFirstValueOfAs(const Av::IAudioFile::MetadataMap& metadataMap, std::initializer_list tags) { auto it = std::find_first_of(std::cbegin(metadataMap), std::cend(metadataMap), std::cbegin(tags), std::cend(tags), [](const auto& it, const auto& str) { return it.first == str; }); if (it == std::cend(metadataMap)) @@ -45,7 +43,7 @@ findFirstValueOfAs(const MetadataMap& metadataMap, std::initializer_list std::optional> -findFirstValueOfAs(const MetadataMap& metadataMap, std::initializer_list tags) +findFirstValueOfAs(const Av::IAudioFile::MetadataMap& metadataMap, std::initializer_list tags) { std::optional str {findFirstValueOfAs(metadataMap, tags)}; if (!str) @@ -69,7 +67,7 @@ findFirstValueOfAs(const MetadataMap& metadataMap, std::initializer_list -getAlbum(const MetadataMap& metadataMap) +getAlbum(const Av::IAudioFile::MetadataMap& metadataMap) { std::optional res; @@ -84,7 +82,7 @@ getAlbum(const MetadataMap& metadataMap) static std::vector -getAlbumArtists(const MetadataMap& metadataMap) +getAlbumArtists(const Av::IAudioFile::MetadataMap& metadataMap) { std::vector res; @@ -99,7 +97,7 @@ getAlbumArtists(const MetadataMap& metadataMap) static std::vector -getArtists(const MetadataMap& metadataMap) +getArtists(const Av::IAudioFile::MetadataMap& metadataMap) { std::vector artists; @@ -133,31 +131,28 @@ AvFormatParser::parse(const std::filesystem::path& p, bool debug) try { - Av::MediaFile mediaFile {p}; + const auto mediaFile {Av::parseAudioFile(p)}; // Stream info { std::vector audioStreams; - for (auto stream : mediaFile.getStreamInfo()) + for (auto stream : mediaFile->getStreamInfo()) { MetaData::AudioStream audioStream {static_cast(stream.bitrate)}; track.audioStreams.emplace_back(audioStream); } } - track.duration = mediaFile.getDuration(); - track.hasCover = mediaFile.hasAttachedPictures(); + track.duration = mediaFile->getDuration(); + track.hasCover = mediaFile->hasAttachedPictures(); MetaData::Clusters clusters; - const std::map metadataMap {mediaFile.getMetaData()}; + const Av::IAudioFile::MetadataMap metadataMap {mediaFile->getMetaData()}; - for (const auto& metadata : metadataMap) + for (const auto& [tag, value] : metadataMap) { - const std::string& tag {metadata.first}; - const std::string& value {metadata.second}; - if (debug) std::cout << "TAG = " << tag << ", VAL = " << value << std::endl; @@ -231,7 +226,7 @@ AvFormatParser::parse(const std::filesystem::path& p, bool debug) track.album = getAlbum(metadataMap); track.albumArtists = getAlbumArtists(metadataMap); } - catch(Av::MediaFileException& e) + catch(Av::Exception& e) { return std::nullopt; } diff --git a/src/libs/subsonic/impl/Stream.cpp b/src/libs/subsonic/impl/Stream.cpp index 62503463..625224e5 100644 --- a/src/libs/subsonic/impl/Stream.cpp +++ b/src/libs/subsonic/impl/Stream.cpp @@ -19,9 +19,9 @@ #include "Stream.hpp" -#include "av/AvTranscoder.hpp" -#include "av/AvTranscodeResourceHandlerCreator.hpp" -#include "av/AvTypes.hpp" +#include "av/TranscodeParameters.hpp" +#include "av/TranscodeResourceHandlerCreator.hpp" +#include "av/Types.hpp" #include "database/Session.hpp" #include "database/Track.hpp" #include "database/User.hpp" @@ -113,7 +113,7 @@ handleDownload(RequestContext& context, const Wt::Http::Request& request, Wt::Ht { std::shared_ptr resourceHandler; - Wt::Http::ResponseContinuation *continuation = request.continuation(); + Wt::Http::ResponseContinuation* continuation {request.continuation()}; if (!continuation) { // Mandatory params @@ -137,12 +137,9 @@ handleDownload(RequestContext& context, const Wt::Http::Request& request, Wt::Ht resourceHandler = Wt::cpp17::any_cast>(continuation->data()); } - resourceHandler->processRequest(request, response); - if (!resourceHandler->isFinished()) - { - Wt::Http::ResponseContinuation *continuation = response.createContinuation(); + continuation = resourceHandler->processRequest(request, response); + if (continuation) continuation->setData(resourceHandler); - } } void @@ -150,7 +147,7 @@ handleStream(RequestContext& context, const Wt::Http::Request& request, Wt::Http { std::shared_ptr resourceHandler; - Wt::Http::ResponseContinuation *continuation = request.continuation(); + Wt::Http::ResponseContinuation* continuation = request.continuation(); if (!continuation) { StreamParameters streamParameters {getStreamParameters(context)}; @@ -164,12 +161,9 @@ handleStream(RequestContext& context, const Wt::Http::Request& request, Wt::Http resourceHandler = Wt::cpp17::any_cast>(continuation->data()); } - resourceHandler->processRequest(request, response); - if (!resourceHandler->isFinished()) - { - Wt::Http::ResponseContinuation *continuation = response.createContinuation(); + continuation = resourceHandler->processRequest(request, response); + if (continuation) continuation->setData(resourceHandler); - } } } diff --git a/src/libs/utils/CMakeLists.txt b/src/libs/utils/CMakeLists.txt index e78383d5..2917cbb1 100644 --- a/src/libs/utils/CMakeLists.txt +++ b/src/libs/utils/CMakeLists.txt @@ -1,5 +1,7 @@ add_library(lmsutils SHARED + impl/ChildProcess.cpp + impl/ChildProcessManager.cpp impl/Config.cpp impl/FileResourceHandler.cpp impl/Logger.cpp diff --git a/src/libs/utils/impl/ChildProcess.cpp b/src/libs/utils/impl/ChildProcess.cpp new file mode 100644 index 00000000..9cc0b243 --- /dev/null +++ b/src/libs/utils/impl/ChildProcess.cpp @@ -0,0 +1,201 @@ +#include "ChildProcess.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include + +#include "utils/Exception.hpp" +#include "utils/Logger.hpp" + +namespace +{ + class SystemException : public ChildProcessException + { + public: + SystemException(int err, const std::string& errMsg) + : ChildProcessException {errMsg + ": " + strerror(err)} + {} + }; +} + +ChildProcess::ChildProcess(boost::asio::io_context& ioContext, const std::filesystem::path& path, const Args& args) +: _ioContext {ioContext} +, _childStdout {_ioContext} + +{ + // make sure only one thread is executing this part of code + static std::mutex mutex; + std::unique_lock lock {mutex}; + + int pipe[2]; + + int res {pipe2(pipe, O_NONBLOCK | O_CLOEXEC)}; + if (res < 0) + throw SystemException {errno, "pipe2 failed!"}; + + { + const std::size_t pipeSize {65536*8}; + + if (fcntl(pipe[0], F_SETPIPE_SZ, pipeSize) == -1) + throw SystemException {errno, "fcntl failed!"}; + + if (fcntl(pipe[1], F_SETPIPE_SZ, pipeSize) == -1) + throw SystemException {errno, "fcntl failed!"}; + } + + res = fork(); + if (res == -1) + throw SystemException {errno, "fork failed!"}; + + if (res == 0) // CHILD + { + close(pipe[0]); + close(STDIN_FILENO); + close(STDERR_FILENO); + + // Replace stdout with pipe write + if (dup2(pipe[1], STDOUT_FILENO) == -1) + exit(-1); + + std::vector execArgs; + std::transform(std::cbegin(args), std::cend(args), std::back_inserter(execArgs), [](const std::string& arg) { return arg.c_str(); }); + execArgs.push_back(nullptr); + + res = execv(path.string().c_str(), (char *const*)&execArgs[0]); + if (res == -1) + exit(-1); + } + else // PARENT + { + close(pipe[1]); + _childStdout.assign(pipe[0]); + _childPID = res; + } +} + +ChildProcess::~ChildProcess() +{ + if (!_waited) + { + close(_childStdout.native_handle()); + kill(); + wait(true); + } +} + +void +ChildProcess::drain() +{ + char buf[128]; + + while (boost::asio::read(_childStdout, boost::asio::buffer(buf)) > 0) + LMS_LOG(CHILDPROCESS, DEBUG) << "drained some bytes" << std::endl; +} + +void +ChildProcess::kill() +{ + ::kill(_childPID, SIGKILL); +} + +bool +ChildProcess::wait(bool block) +{ + int wstatus {}; + + pid_t pid {waitpid(_childPID, &wstatus, block ? 0 : WNOHANG)}; + + if (pid == -1) + throw SystemException {errno, "waitpid failed!"}; + else if (pid == 0) + return false; + + if (WIFEXITED(wstatus)) + _exitCode = WEXITSTATUS(wstatus); + + _waited = true; + return true; +} + + +void +ChildProcess::asyncRead(std::byte* data, std::size_t bufferSize, ReadCallback callback) +{ + LMS_LOG(CHILDPROCESS, DEBUG) << "ASYNC READ"; + + boost::asio::async_read(_childStdout, boost::asio::buffer(data, bufferSize), + [this, callback {std::move(callback)}](const boost::system::error_code& error, std::size_t bytesTransferred) + { + LMS_LOG(CHILDPROCESS, DEBUG) << "ASYNC READ CB - error = '" << error.message() << "', bytesTransferred = " << bytesTransferred; + + if (error) + { + { + boost::system::error_code closeError; + _childStdout.close(closeError); + } + + if (error == boost::asio::error::operation_aborted) + { + return; + } + if (error == boost::asio::error::eof) + { + callback(ReadResult::EndOfFile, bytesTransferred); + return; + } + else + { + callback(ReadResult::Error, bytesTransferred); + return; + } + } + + callback(ReadResult::Success, bytesTransferred); + }); +} + +void +ChildProcess::asyncWaitForData(WaitCallback cb) +{ + LMS_LOG(CHILDPROCESS, DEBUG) << "Async wait requested"; + + _childStdout.async_wait(boost::asio::posix::stream_descriptor::wait_read, + [cb {std::move(cb)}](const boost::system::error_code& ec) + { + LMS_LOG(CHILDPROCESS, DEBUG) << "Wait CB, error = " << ec.message(); + if (!ec) + cb(); + }); +} + +std::size_t +ChildProcess::readSome(std::byte* data, std::size_t bufferSize) +{ + boost::system::error_code ec; + const std::size_t res {_childStdout.read_some(boost::asio::buffer(data, bufferSize), ec)}; + LMS_LOG(CHILDPROCESS, DEBUG) << "read some " << res << " bytes, ec = " << ec.message(); + if (ec) + _childStdout.close(ec); + + return res; +} + +bool +ChildProcess::finished() +{ + return !_childStdout.is_open(); +} + diff --git a/src/libs/utils/impl/ChildProcess.hpp b/src/libs/utils/impl/ChildProcess.hpp new file mode 100644 index 00000000..f051eae7 --- /dev/null +++ b/src/libs/utils/impl/ChildProcess.hpp @@ -0,0 +1,37 @@ +#pragma once + +#include +#include + +#include + +#include +#include + +#include "utils/IChildProcess.hpp" + + +class ChildProcess : public IChildProcess +{ + public: + ~ChildProcess(); + ChildProcess(boost::asio::io_context& ioContext, const std::filesystem::path& path, const Args& args); + + private: + void asyncRead(std::byte* data, std::size_t bufferSize, ReadCallback callback) override; + void asyncWaitForData(WaitCallback cb) override; + std::size_t readSome(std::byte* data, std::size_t bufferSize) override; + bool finished() override; + + void kill(); + void drain(); + bool wait(bool block); // return true if waited + + using FileDescriptor = boost::asio::posix::stream_descriptor; + + boost::asio::io_context& _ioContext; + FileDescriptor _childStdout; + ::pid_t _childPID {}; + bool _waited {}; + std::optional _exitCode; +}; diff --git a/src/libs/utils/impl/ChildProcessManager.cpp b/src/libs/utils/impl/ChildProcessManager.cpp new file mode 100644 index 00000000..bad7ebfa --- /dev/null +++ b/src/libs/utils/impl/ChildProcessManager.cpp @@ -0,0 +1,54 @@ + +#include "ChildProcessManager.hpp" + +#include "utils/Logger.hpp" + +#include "ChildProcess.hpp" + + +std::unique_ptr +createChildProcessManager() +{ + return std::make_unique(); +} + +ChildProcessManager::ChildProcessManager() +: _work {boost::asio::make_work_guard(_ioContext)} +{ + start(); +} + +ChildProcessManager::~ChildProcessManager() +{ + stop(); +} + +void +ChildProcessManager::start() +{ + LMS_LOG(CHILDPROCESS, INFO) << "Starting child process manager..."; + + _thread = std::make_unique([&]() + { + _ioContext.run(); + }); + + LMS_LOG(CHILDPROCESS, INFO) << "Child process manager started!"; +} + +void +ChildProcessManager::stop() +{ + LMS_LOG(CHILDPROCESS, INFO) << "Stopping child process manager"; + _work.reset(); + _thread->join(); + LMS_LOG(CHILDPROCESS, INFO) << "Stopped child process manager"; +} + +std::unique_ptr +ChildProcessManager::spawnChildProcess(const std::filesystem::path& path, const IChildProcess::Args& args) +{ + return std::make_unique(_ioContext, path, args); +} + + diff --git a/src/libs/utils/impl/ChildProcessManager.hpp b/src/libs/utils/impl/ChildProcessManager.hpp new file mode 100644 index 00000000..da3760b7 --- /dev/null +++ b/src/libs/utils/impl/ChildProcessManager.hpp @@ -0,0 +1,34 @@ +#pragma once + +#include +#include + +#include +#include + +#include "utils/IChildProcessManager.hpp" + +class ChildProcessManager : public IChildProcessManager +{ + public: + + ChildProcessManager(); + ~ChildProcessManager(); + + ChildProcessManager(const ChildProcessManager&) = delete; + ChildProcessManager(ChildProcessManager&&) = delete; + ChildProcessManager& operator=(const ChildProcessManager&) = delete; + ChildProcessManager& operator=(ChildProcessManager&&) = delete; + + private: + std::unique_ptr spawnChildProcess(const std::filesystem::path& path, const IChildProcess::Args& args) override; + + void start(); + void stop(); + + boost::asio::io_context _ioContext; + std::unique_ptr _thread; + boost::asio::executor_work_guard _work; +}; + + diff --git a/src/libs/utils/impl/FileResourceHandler.cpp b/src/libs/utils/impl/FileResourceHandler.cpp index f3eb1291..c9105630 100644 --- a/src/libs/utils/impl/FileResourceHandler.cpp +++ b/src/libs/utils/impl/FileResourceHandler.cpp @@ -36,7 +36,7 @@ FileResourceHandler::FileResourceHandler(const std::filesystem::path& path) } -void +Wt::Http::ResponseContinuation* FileResourceHandler::processRequest(const Wt::Http::Request& request, Wt::Http::Response& response) { ::uint64_t startByte {_offset}; @@ -49,7 +49,7 @@ FileResourceHandler::processRequest(const Wt::Http::Request& request, Wt::Http:: LMS_LOG(UTILS, ERROR) << "Cannot open file stream for '" << _path.string() << "'"; response.setStatus(404); _isFinished = true; - return; + return {}; } else { @@ -72,7 +72,7 @@ FileResourceHandler::processRequest(const Wt::Http::Request& request, Wt::Http:: LMS_LOG(UTILS, DEBUG) << "Range not satisfiable"; _isFinished = true; - return; + return {}; } if (ranges.size() == 1) @@ -102,7 +102,7 @@ FileResourceHandler::processRequest(const Wt::Http::Request& request, Wt::Http:: { LMS_LOG(UTILS, ERROR) << "Cannot reopen file stream for '" << _path.string() << "'"; _isFinished = true; - return; + return {}; } ifs.seekg(static_cast(startByte)); @@ -123,20 +123,16 @@ FileResourceHandler::processRequest(const Wt::Http::Request& request, Wt::Http:: if (ifs.good() && actualPieceSize < restSize) { _offset = startByte + actualPieceSize; - LMS_LOG(UTILS, DEBUG) << "Job not complete! Next chunk offset = " << _offset; - } - else - { - _isFinished = true; - LMS_LOG(UTILS, DEBUG) << "Job complete!"; - } -} -bool -FileResourceHandler::isFinished() const -{ - return _isFinished; + return response.createContinuation(); + } + + _isFinished = true; + LMS_LOG(UTILS, DEBUG) << "Job complete!"; + + return {}; } + diff --git a/src/libs/utils/impl/FileResourceHandler.hpp b/src/libs/utils/impl/FileResourceHandler.hpp index befd3bdd..8baca6e4 100644 --- a/src/libs/utils/impl/FileResourceHandler.hpp +++ b/src/libs/utils/impl/FileResourceHandler.hpp @@ -29,10 +29,9 @@ class FileResourceHandler final : public IResourceHandler private: - void processRequest(const Wt::Http::Request& request, Wt::Http::Response& response) override; - bool isFinished() const override; + Wt::Http::ResponseContinuation* processRequest(const Wt::Http::Request& request, Wt::Http::Response& response) override; - static constexpr std::size_t _chunkSize {262144}; + static constexpr std::size_t _chunkSize {65536}; std::filesystem::path _path; ::uint64_t _beyondLastByte {}; diff --git a/src/libs/utils/impl/Logger.cpp b/src/libs/utils/impl/Logger.cpp index d19f79b3..1f3dbca4 100644 --- a/src/libs/utils/impl/Logger.cpp +++ b/src/libs/utils/impl/Logger.cpp @@ -24,20 +24,21 @@ const char* getModuleName(Module mod) switch (mod) { case Module::API_SUBSONIC: return "API_SUBSONIC"; - case Module::AUTH: return "AUTH"; - case Module::AV: return "AV"; - case Module::COVER: return "COVER"; - case Module::DB: return "DB"; + case Module::AUTH: return "AUTH"; + case Module::AV: return "AV"; + case Module::CHILDPROCESS: return "CHILDPROC"; + case Module::COVER: return "COVER"; + case Module::DB: return "DB"; case Module::DBUPDATER: return "DB UPDATER"; case Module::FEATURE: return "FEATURE"; - case Module::MAIN: return "MAIN"; + case Module::MAIN: return "MAIN"; case Module::METADATA: return "METADATA"; case Module::REMOTE: return "REMOTE"; case Module::SERVICE: return "SERVICE"; case Module::RECOMMENDATION: return "RECOMMENDATION"; case Module::TRANSCODE: return "TRANSCODE"; - case Module::UI: return "UI"; - case Module::UTILS: return "UTILS"; + case Module::UI: return "UI"; + case Module::UTILS: return "UTILS"; } return ""; } diff --git a/src/libs/utils/include/utils/IChildProcess.hpp b/src/libs/utils/include/utils/IChildProcess.hpp new file mode 100644 index 00000000..7de5731a --- /dev/null +++ b/src/libs/utils/include/utils/IChildProcess.hpp @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2020 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ +#pragma once + +#include +#include +#include +#include + +#include "utils/Exception.hpp" + +class ChildProcessException : public LmsException +{ + public: + using LmsException::LmsException; +}; + +class IChildProcess +{ + public: + using Args = std::vector; + + virtual ~IChildProcess() = default; + + enum class ReadResult + { + Success, + Error, + EndOfFile, + }; + + using ReadCallback = std::function; + virtual void asyncRead(std::byte* data, std::size_t bufferSize, ReadCallback callback) = 0; + + using WaitCallback = std::function; + virtual void asyncWaitForData(WaitCallback cb) = 0; + virtual std::size_t readSome(std::byte* data, std::size_t bufferSize) = 0; + virtual bool finished() = 0; +}; + diff --git a/src/libs/utils/include/utils/IChildProcessManager.hpp b/src/libs/utils/include/utils/IChildProcessManager.hpp new file mode 100644 index 00000000..fd9f8e08 --- /dev/null +++ b/src/libs/utils/include/utils/IChildProcessManager.hpp @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2020 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ +#pragma once + +#include +#include +#pragma once + +#include +#include + +#include "IChildProcess.hpp" + +class IChildProcessManager +{ + public: + virtual ~IChildProcessManager() = default; + + virtual std::unique_ptr spawnChildProcess(const std::filesystem::path& path, const IChildProcess::Args& args) = 0; +}; + +std::unique_ptr createChildProcessManager(); + + diff --git a/src/libs/utils/include/utils/IResourceHandler.hpp b/src/libs/utils/include/utils/IResourceHandler.hpp index b3fb1a5e..6518a810 100644 --- a/src/libs/utils/include/utils/IResourceHandler.hpp +++ b/src/libs/utils/include/utils/IResourceHandler.hpp @@ -22,13 +22,12 @@ #include #include -// Helper class to serve a resource (must be saved as continuation data if not complete) +// Helper class to serve a resource (must be saved as continuation data if not complete) class IResourceHandler { public: virtual ~IResourceHandler() = default; - virtual void processRequest(const Wt::Http::Request& request, Wt::Http::Response& response) = 0; - virtual bool isFinished() const = 0; + [[nodiscard]] virtual Wt::Http::ResponseContinuation* processRequest(const Wt::Http::Request& request, Wt::Http::Response& response) = 0; }; diff --git a/src/libs/utils/include/utils/Logger.hpp b/src/libs/utils/include/utils/Logger.hpp index 430beb39..8e9be839 100644 --- a/src/libs/utils/include/utils/Logger.hpp +++ b/src/libs/utils/include/utils/Logger.hpp @@ -38,6 +38,7 @@ enum class Module API_SUBSONIC, AUTH, AV, + CHILDPROCESS, COVER, DB, DBUPDATER, diff --git a/src/lms/main.cpp b/src/lms/main.cpp index f190371f..7d566c05 100644 --- a/src/lms/main.cpp +++ b/src/lms/main.cpp @@ -26,8 +26,6 @@ #include "auth/IAuthTokenService.hpp" #include "auth/IPasswordService.hpp" -#include "av/AvInfo.hpp" -#include "av/AvTranscoder.hpp" #include "cover/ICoverArtGrabber.hpp" #include "database/Db.hpp" #include "database/Session.hpp" @@ -35,6 +33,7 @@ #include "recommendation/IEngine.hpp" #include "subsonic/SubsonicResource.hpp" #include "ui/LmsApplication.hpp" +#include "utils/IChildProcessManager.hpp" #include "utils/IConfig.hpp" #include "utils/Service.hpp" #include "utils/WtLogger.hpp" @@ -203,9 +202,6 @@ int main(int argc, char* argv[]) Wt::WServer server {argv[0]}; server.setServerConfiguration(wtServerArgs.size(), const_cast(&wtArgv[0])); - // lib init - Av::Transcoder::init(); - // Initializing a connection pool to the database that will be shared along services Database::Db database {config->getPath("working-dir") / "lms.db"}; { @@ -217,6 +213,7 @@ int main(int argc, char* argv[]) UserInterface::LmsApplicationGroupContainer appGroups; // Service initialization order is important + Service childProcessManagerService {createChildProcessManager()}; Service authTokenService {Auth::createAuthTokenService(config->getULong("login-throttler-max-entriees", 10000))}; Service passwordService {Auth::createPasswordService(config->getULong("login-throttler-max-entriees", 10000))}; Service coverArtService {CoverArt::createGrabber(argv[0], diff --git a/src/lms/ui/resource/AudioFileResource.cpp b/src/lms/ui/resource/AudioFileResource.cpp index 7aaac02f..2a2b5ed4 100644 --- a/src/lms/ui/resource/AudioFileResource.cpp +++ b/src/lms/ui/resource/AudioFileResource.cpp @@ -22,7 +22,7 @@ #include #include -#include "av/AvInfo.hpp" +#include "av/IAudioFile.hpp" #include "database/Session.hpp" #include "database/Track.hpp" #include "utils/FileResourceHandlerCreator.hpp" @@ -101,13 +101,9 @@ AudioFileResource::handleRequest(const Wt::Http::Request& request, fileResourceHandler = Wt::cpp17::any_cast>(request.continuation()->data()); } - fileResourceHandler->processRequest(request, response); - - if (!fileResourceHandler->isFinished()) - { - auto* continuation {response.createContinuation()}; + auto* continuation {fileResourceHandler->processRequest(request, response)}; + if (continuation) continuation->setData(fileResourceHandler); - } } diff --git a/src/lms/ui/resource/AudioTranscodeResource.cpp b/src/lms/ui/resource/AudioTranscodeResource.cpp index 16ffbf14..818eb67a 100644 --- a/src/lms/ui/resource/AudioTranscodeResource.cpp +++ b/src/lms/ui/resource/AudioTranscodeResource.cpp @@ -19,9 +19,12 @@ #include "AudioTranscodeResource.hpp" +#include #include -#include "av/AvTranscoder.hpp" +#include "av/TranscodeParameters.hpp" +#include "av/TranscodeResourceHandlerCreator.hpp" +#include "av/Types.hpp" #include "database/Session.hpp" #include "database/Track.hpp" #include "database/User.hpp" @@ -66,20 +69,13 @@ namespace StringUtils } } -namespace UserInterface { - -AudioTranscodeResource:: ~AudioTranscodeResource() -{ - beingDeleted(); -} - static std::optional AudioFormatToAvFormat(Database::AudioFormat format) { switch (format) { - case Database::AudioFormat::MP3: return Av::Format::MP3; + case Database::AudioFormat::MP3: return Av::Format::MP3; case Database::AudioFormat::OGG_OPUS: return Av::Format::OGG_OPUS; case Database::AudioFormat::MATROSKA_OPUS: return Av::Format::MATROSKA_OPUS; case Database::AudioFormat::OGG_VORBIS: return Av::Format::OGG_VORBIS; @@ -91,6 +87,14 @@ AudioFormatToAvFormat(Database::AudioFormat format) return std::nullopt; } + +namespace UserInterface { + +AudioTranscodeResource:: ~AudioTranscodeResource() +{ + beingDeleted(); +} + std::string AudioTranscodeResource::getUrl(Database::IdType trackId) const { @@ -115,102 +119,85 @@ readParameterAs(const Wt::Http::Request& request, const std::string& parameterNa return res; } +struct TranscodeParameters +{ + std::filesystem::path file; + Av::TranscodeParameters transcodeParameters; +}; + +static +std::optional +readTranscodeParameters(const Wt::Http::Request& request) +{ + TranscodeParameters parameters; + + // mandatory parameters + auto trackId {readParameterAs(request, "trackid")}; + auto format {readParameterAs(request, "format")}; + auto bitrate {readParameterAs(request, "bitrate")}; + + if (!trackId || !format || !bitrate) + return std::nullopt; + + const std::optional avFormat {AudioFormatToAvFormat(*format)}; + if (!avFormat) + return std::nullopt; + + // optional parameter + std::size_t offset {readParameterAs(request, "offset").value_or(0)}; + + std::filesystem::path trackPath; + { + auto transaction {LmsApp->getDbSession().createSharedTransaction()}; + + const Database::Track::pointer track {Database::Track::getById(LmsApp->getDbSession(), *trackId)}; + if (!track) + { + LOG(ERROR) << "Missing track"; + return std::nullopt; + } + + parameters.file = track->getPath(); + + if (Database::User::audioTranscodeAllowedBitrates.find(*bitrate) == std::cend(Database::User::audioTranscodeAllowedBitrates)) + { + LOG(ERROR) << "Bitrate '" << *bitrate << "' is not allowed"; + return std::nullopt; + } + } + + parameters.transcodeParameters.stripMetadata = true; + parameters.transcodeParameters.format = *avFormat; + parameters.transcodeParameters.bitrate = *bitrate; + parameters.transcodeParameters.offset = std::chrono::seconds {offset}; + + return parameters; +} + void AudioTranscodeResource::handleRequest(const Wt::Http::Request& request, Wt::Http::Response& response) { - std::shared_ptr transcoder; + std::shared_ptr resourceHandler; - // First, see if this request is for a continuation - Wt::Http::ResponseContinuation *continuation = request.continuation(); - if (continuation) + Wt::Http::ResponseContinuation* continuation {request.continuation()}; + if (!continuation) { - LOG(DEBUG) << "Continuation! " << continuation ; - transcoder = Wt::cpp17::any_cast>(continuation->data()); + const std::optional& parameters {readTranscodeParameters(request)}; + if (parameters) + resourceHandler = Av::createTranscodeResourceHandler(parameters->file, parameters->transcodeParameters); } else { - LOG(DEBUG) << "First request: creating transcoder"; - - // mandatory parameters - auto trackId {readParameterAs(request, "trackid")}; - auto format {readParameterAs(request, "format")}; - auto bitrate {readParameterAs(request, "bitrate")}; - - if (!trackId || !format || !bitrate) - return; - - auto avFormat {AudioFormatToAvFormat(*format)}; - if (!avFormat) - return; - - // optional parameter - auto offset {readParameterAs(request, "offset")}; - - std::filesystem::path trackPath; - { - auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - - const Database::Track::pointer track {Database::Track::getById(LmsApp->getDbSession(), *trackId)}; - if (!track) - { - LOG(ERROR) << "Missing track"; - return; - } - - trackPath = track->getPath(); - - if (Database::User::audioTranscodeAllowedBitrates.find(*bitrate) == std::cend(Database::User::audioTranscodeAllowedBitrates)) - { - LOG(ERROR) << "Bitrate '" << *bitrate << "' is not allowed"; - return; - } - } - - Av::TranscodeParameters parameters {}; - parameters.stripMetadata = true; - parameters.format = *avFormat; - parameters.bitrate = *bitrate; - parameters.offset = std::chrono::seconds {offset ? *offset : 0}; - - transcoder = std::make_shared(trackPath, parameters); - if (!transcoder->start()) - { - LOG(ERROR) << "Cannot start transcoder"; - return; - } - - LOG(DEBUG) << "Transcoder started"; - - std::string mimeType {transcoder->getOutputMimeType()}; - response.setMimeType(mimeType); - LOG(DEBUG) << "Mime type set to '" << mimeType << "'"; - + resourceHandler = Wt::cpp17::any_cast>(continuation->data()); } - if (!transcoder->isComplete()) + if (resourceHandler) { - std::vector data; - data.reserve(_chunkSize); - - transcoder->process(data, _chunkSize); - - response.out().write(reinterpret_cast(&data[0]), data.size()); - LOG(DEBUG) << "Written " << data.size() << " bytes! complete = " << (transcoder->isComplete() ? "true" : "false"); - - if (!response.out()) - { - LOG(ERROR) << "Write failed!"; - } + continuation = resourceHandler->processRequest(request, response); + if (continuation) + continuation->setData(resourceHandler); } - - if (!transcoder->isComplete() && response.out()) - { - continuation = response.createContinuation(); - continuation->setData(transcoder); - } - else - LOG(DEBUG) << "No more data!"; } } // namespace UserInterface