Switched from sync transcoder to async transcoder

This commit is contained in:
emeric
2020-12-12 14:43:33 +01:00
parent 0586e93655
commit eea27b86f0
34 changed files with 959 additions and 504 deletions
+1 -1
View File
@@ -82,7 +82,7 @@ checkUserPassword(Database::Session& session, const std::string& loginName, cons
case Database::User::AuthMode::Internal: case Database::User::AuthMode::Internal:
{ {
LMS_LOG(AUTH, DEBUG) << "Checking internal password for user '" << loginName << "'"; 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); return hashFunc.verify(password, passwordHash.salt, passwordHash.hash);
} }
+4 -4
View File
@@ -1,9 +1,9 @@
add_library(lmsav SHARED add_library(lmsav SHARED
impl/AvInfo.cpp impl/AudioFile.cpp
impl/AvTranscoder.cpp impl/Transcoder.cpp
impl/AvTranscodeResourceHandler.cpp impl/TranscodeResourceHandler.cpp
impl/AvTypes.cpp impl/Types.cpp
) )
target_include_directories(lmsav INTERFACE target_include_directories(lmsav INTERFACE
@@ -17,7 +17,7 @@
* along with LMS. If not, see <http://www.gnu.org/licenses/>. * along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/ */
#include "av/AvInfo.hpp" #include "AudioFile.hpp"
extern "C" extern "C"
{ {
@@ -44,20 +44,28 @@ static std::string averror_to_string(int error)
return "Unknown error"; return "Unknown error";
} }
MediaFileException::MediaFileException(int avError) class AudioFileException : public Av::Exception
: AvException {"MediaFileException: " + averror_to_string(avError)}
{ {
public:
AudioFileException(int avError)
: Av::Exception {"AudioFileException: " + averror_to_string(avError)}
{}
};
std::unique_ptr<IAudioFile>
parseAudioFile(const std::filesystem::path& p)
{
return std::make_unique<AudioFile>(p);
} }
AudioFile::AudioFile(const std::filesystem::path& p)
MediaFile::MediaFile(const std::filesystem::path& p)
: _p {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) if (error < 0)
{ {
LMS_LOG(AV, ERROR) << "Cannot open " << _p.string() << ": " << averror_to_string(error); 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); 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); LMS_LOG(AV, ERROR) << "Cannot find stream information on " << _p.string() << ": " << averror_to_string(error);
avformat_close_input(&_context); avformat_close_input(&_context);
throw MediaFileException(error); throw AudioFileException {error};
} }
} }
MediaFile::~MediaFile() AudioFile::~AudioFile()
{ {
avformat_close_input(&_context); avformat_close_input(&_context);
} }
std::string const std::filesystem::path&
MediaFile::getFormatName() const AudioFile::getPath() const
{ {
return _context->iformat->name; return _p;
} }
std::chrono::milliseconds std::chrono::milliseconds
MediaFile::getDuration() const AudioFile::getDuration() const
{ {
if (_context->duration == AV_NOPTS_VALUE) 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 void
getMetaDataFromDictionnary(AVDictionary* dictionnary, std::map<std::string, std::string>& res) getMetaDataFromDictionnary(AVDictionary* dictionnary, AudioFile::MetadataMap& res)
{ {
if (!dictionnary) if (!dictionnary)
return; return;
@@ -102,10 +110,10 @@ getMetaDataFromDictionnary(AVDictionary* dictionnary, std::map<std::string, std:
} }
} }
std::map<std::string, std::string> AudioFile::MetadataMap
MediaFile::getMetaData(void) AudioFile::getMetaData() const
{ {
std::map<std::string, std::string> res; MetadataMap res;
getMetaDataFromDictionnary(_context->metadata, res); getMetaDataFromDictionnary(_context->metadata, res);
@@ -113,7 +121,7 @@ MediaFile::getMetaData(void)
// If we did not find tags, search metadata in streams // If we did not find tags, search metadata in streams
if (res.empty()) 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); getMetaDataFromDictionnary(_context->streams[i]->metadata, res);
@@ -126,7 +134,7 @@ MediaFile::getMetaData(void)
} }
std::vector<StreamInfo> std::vector<StreamInfo>
MediaFile::getStreamInfo() const AudioFile::getStreamInfo() const
{ {
std::vector<StreamInfo> res; std::vector<StreamInfo> res;
@@ -154,7 +162,7 @@ MediaFile::getStreamInfo() const
} }
std::optional<std::size_t> std::optional<std::size_t>
MediaFile::getBestStream() const AudioFile::getBestStream() const
{ {
int res = av_find_best_stream(_context, int res = av_find_best_stream(_context,
AVMEDIA_TYPE_AUDIO, AVMEDIA_TYPE_AUDIO,
@@ -170,7 +178,7 @@ MediaFile::getBestStream() const
} }
bool bool
MediaFile::hasAttachedPictures(void) const AudioFile::hasAttachedPictures(void) const
{ {
for (std::size_t i = 0; i < _context->nb_streams; ++i) for (std::size_t i = 0; i < _context->nb_streams; ++i)
{ {
@@ -182,9 +190,9 @@ MediaFile::hasAttachedPictures(void) const
} }
void void
MediaFile::visitAttachedPictures(std::function<void(const Picture&)> func) const AudioFile::visitAttachedPictures(std::function<void(const Picture&)> func) const
{ {
static const std::map<int, std::string> codecMimeMap = static const std::unordered_map<int, std::string> codecMimeMap =
{ {
{ AV_CODEC_ID_BMP, "image/x-bmp" }, { AV_CODEC_ID_BMP, "image/x-bmp" },
{ AV_CODEC_ID_GIF, "image/gif" }, { AV_CODEC_ID_GIF, "image/gif" },
@@ -230,7 +238,7 @@ MediaFile::visitAttachedPictures(std::function<void(const Picture&)> func) const
} }
} }
std::optional<MediaFileFormat> std::optional<AudioFileFormat>
guessMediaFileFormat(const std::filesystem::path& file) guessMediaFileFormat(const std::filesystem::path& file)
{ {
AVOutputFormat* format {av_guess_format(NULL,file.string().c_str(),NULL)}; 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) else if (mimeTypes.size() > 1)
LMS_LOG(AV, INFO) << "File '" << file.string() << "' reported several mime types: '" << format->mime_type << "'"; LMS_LOG(AV, INFO) << "File '" << file.string() << "' reported several mime types: '" << format->mime_type << "'";
MediaFileFormat res; AudioFileFormat res;
res.format = formats.front(); res.format = formats.front();
res.mimeType = mimeTypes.empty() ? "application/octet-stream" : mimeTypes.front(); res.mimeType = mimeTypes.empty() ? "application/octet-stream" : mimeTypes.front();
+57
View File
@@ -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 <http://www.gnu.org/licenses/>.
*/
/* 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<StreamInfo> getStreamInfo() const override;
std::optional<std::size_t> getBestStream() const override;
bool hasAttachedPictures() const override;
void visitAttachedPictures(std::function<void(const Picture&)> func) const override;
private:
const std::filesystem::path _p;
AVFormatContext* _context {};
};
} // namespace Av
@@ -17,7 +17,7 @@
* along with LMS. If not, see <http://www.gnu.org/licenses/>. * along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/ */
#include "AvTranscodeResourceHandler.hpp" #include "TranscodeResourceHandler.hpp"
namespace Av namespace Av
{ {
@@ -36,24 +36,32 @@ namespace Av
_transcoder.start(); _transcoder.start();
} }
void Wt::Http::ResponseContinuation*
TranscodeResourceHandler::processRequest(const Wt::Http::Request& /*request*/, Wt::Http::Response& response) TranscodeResourceHandler::processRequest(const Wt::Http::Request& /*request*/, Wt::Http::Response& response)
{ {
response.setMimeType(_transcoder.getOutputMimeType()); response.setMimeType(_transcoder.getOutputMimeType());
if (!_transcoder.isComplete()) if (_nbBytesReady > 0)
{ {
std::vector<unsigned char> buffer; response.out().write(reinterpret_cast<const char *>(&_buffer[0]), _nbBytesReady);
_nbBytesReady = 0;
_transcoder.process(buffer, _chunkSize);
response.out().write(reinterpret_cast<const char *>(&buffer[0]), buffer.size());
}
} }
bool if (!_transcoder.finished())
TranscodeResourceHandler::isFinished() const
{ {
return _transcoder.isComplete(); 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 {};
} }
} }
@@ -19,9 +19,12 @@
#pragma once #pragma once
#include <array>
#include <filesystem> #include <filesystem>
#include "av/AvTranscoder.hpp"
#include "av/TranscodeParameters.hpp"
#include "utils/IResourceHandler.hpp" #include "utils/IResourceHandler.hpp"
#include "Transcoder.hpp"
namespace Av namespace Av
{ {
@@ -33,10 +36,11 @@ namespace Av
private: private:
void processRequest(const Wt::Http::Request& request, Wt::Http::Response& reponse) override; Wt::Http::ResponseContinuation* processRequest(const Wt::Http::Request& request, Wt::Http::Response& reponse) override;
bool isFinished() const override;
static constexpr std::size_t _chunkSize {262144}; static constexpr std::size_t _chunkSize {32768};
std::array<std::byte, _chunkSize> _buffer;
std::size_t _nbBytesReady {};
const std::filesystem::path _trackPath; const std::filesystem::path _trackPath;
Transcoder _transcoder; Transcoder _transcoder;
}; };
@@ -17,12 +17,12 @@
* along with LMS. If not, see <http://www.gnu.org/licenses/>. * along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/ */
#include "av/AvTranscoder.hpp" #include "Transcoder.hpp"
#include <atomic> #include <atomic>
#include <mutex> #include <iomanip>
#include "av/AvInfo.hpp" #include "utils/IChildProcessManager.hpp"
#include "utils/IConfig.hpp" #include "utils/IConfig.hpp"
#include "utils/Path.hpp" #include "utils/Path.hpp"
#include "utils/Logger.hpp" #include "utils/Logger.hpp"
@@ -40,39 +40,41 @@ Transcoder::init()
{ {
ffmpegPath = Service<IConfig>::get()->getPath("ffmpeg-file", "/usr/bin/ffmpeg"); ffmpegPath = Service<IConfig>::get()->getPath("ffmpeg-file", "/usr/bin/ffmpeg");
if (!std::filesystem::exists(ffmpegPath)) 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) Transcoder::Transcoder(const std::filesystem::path& filePath, const TranscodeParameters& parameters)
: _filePath {filePath}, : _id {globalId++}
_parameters {parameters}, , _filePath {filePath}
_id {globalId++} , _parameters {parameters}
{ {
} }
Transcoder::~Transcoder() = default;
bool bool
Transcoder::start() Transcoder::start()
{ {
if (ffmpegPath.empty())
init();
try try
{ {
if (!std::filesystem::exists(_filePath)) if (!std::filesystem::exists(_filePath))
{ {
LOG(ERROR) << "File '" << _filePath << "' does not exist!"; LOG(ERROR) << "File '" << _filePath << "' does not exist!";
_isComplete = true;
return false; return false;
} }
else if (!std::filesystem::is_regular_file( _filePath) ) else if (!std::filesystem::is_regular_file( _filePath) )
{ {
LOG(ERROR) << "File '" << _filePath << "' is not regular!"; LOG(ERROR) << "File '" << _filePath << "' is not regular!";
_isComplete = true;
return false; return false;
} }
} }
catch (const std::filesystem::filesystem_error& e) catch (const std::filesystem::filesystem_error& e)
{ {
LOG(ERROR) << "File error on '" << _filePath.string() << "': " << e.what(); LOG(ERROR) << "File error on '" << _filePath.string() << "': " << e.what();
_isComplete = true;
return false; return false;
} }
@@ -82,17 +84,21 @@ Transcoder::start()
args.emplace_back(ffmpegPath.string()); 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 // in order not to block the whole forked process
args.emplace_back("-loglevel"); args.emplace_back("-loglevel");
args.emplace_back("quiet"); args.emplace_back("quiet");
args.emplace_back("-nostdin"); args.emplace_back("-nostdin");
// input Offset // input Offset
if (_parameters.offset)
{ {
args.emplace_back("-ss"); 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 // Input file
@@ -120,6 +126,7 @@ Transcoder::start()
args.emplace_back("-b:a"); args.emplace_back("-b:a");
args.emplace_back(std::to_string(_parameters.bitrate)); args.emplace_back(std::to_string(_parameters.bitrate));
// Codecs and formats // Codecs and formats
switch (_parameters.format) switch (_parameters.format)
{ {
@@ -157,7 +164,6 @@ Transcoder::start()
break; break;
default: default:
_isComplete = true;
return false; return false;
} }
@@ -169,84 +175,56 @@ Transcoder::start()
for (const std::string& arg : args) for (const std::string& arg : args)
LOG(DEBUG) << "Arg = '" << arg << "'"; LOG(DEBUG) << "Arg = '" << arg << "'";
// make sure only one thread is executing this part of code
{
static std::mutex transcoderMutex;
std::lock_guard<std::mutex> lock {transcoderMutex};
_child = std::make_shared<redi::ipstream>();
// Caution: stdin must have been closed before // Caution: stdin must have been closed before
_child->open(ffmpegPath.string(), args); try
if (!_child->is_open())
{ {
LOG(DEBUG) << "Exec failed!"; _childProcess = Service<IChildProcessManager>::get()->spawnChildProcess(ffmpegPath, args);
_isComplete = true; }
catch (ChildProcessException& exception)
{
LOG(ERROR) << "Cannot execute '" << ffmpegPath << "': " << exception.what();
return false; return false;
} }
if (_child->out().eof())
{
LOG(DEBUG) << "Early end of file!";
_isComplete = true;
return false;
}
}
LOG(DEBUG) << "Stream opened!";
return true; return true;
} }
void void
Transcoder::process(std::vector<unsigned char>& output, std::size_t maxSize) Transcoder::asyncWaitForData(WaitCallback cb)
{ {
if (!_child || _isComplete) assert(_childProcess);
return;
output.resize(maxSize); LOG(DEBUG) << "Want to wait for data";
//Read on the output stream _childProcess->asyncWaitForData([cb = std::move(cb)]
_child->out().read(reinterpret_cast<char*>(&output[0]), maxSize);
output.resize(_child->out().gcount());
if (_child->out().fail())
{ {
LOG(DEBUG) << "Stdout FAILED"; cb();
_isComplete = true; });
} }
if (_child->out().eof()) void
Transcoder::asyncRead(std::byte* buffer, std::size_t bufferSize, ReadCallback readCallback)
{ {
LOG(DEBUG) << "Stdout EOF!"; assert(_childProcess);
_isComplete = true;
return _childProcess->asyncRead(buffer, bufferSize, [readCallback {std::move(readCallback)}](IChildProcess::ReadResult /*res*/, std::size_t nbBytesRead)
{
readCallback(nbBytesRead);
});
} }
if (_isComplete) std::size_t
Transcoder::readSome(std::byte* buffer, std::size_t bufferSize)
{ {
LOG(DEBUG) << "Transcode complete!"; assert(_childProcess);
_child->clear();
_child.reset(); return _childProcess->readSome(buffer, bufferSize);
} }
_total += output.size(); bool
Transcoder::finished() const
LOG(DEBUG) << "nb bytes = " << output.size() << ", total = " << _total;
}
Transcoder::~Transcoder()
{ {
LOG(DEBUG) << ", ~Transcoder called! Total produced bytes = " << _total; return _childProcess->finished();
if (_child)
{
LOG(DEBUG) << "Child still here!";
_child->rdbuf()->kill(SIGKILL);
LOG(DEBUG) << "Closing...";
_child->rdbuf()->close();
LOG(DEBUG) << "Closing DONE";
}
} }
} // namespace Transcode } // namespace Transcode
+72
View File
@@ -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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <filesystem>
#include <functional>
#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()>;
void asyncWaitForData(WaitCallback cb);
// non blocking calls
using ReadCallback = std::function<void(std::size_t nbReadBytes)>;
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<IChildProcess> _childProcess;
bool _finished {};
std::string _outputMimeType;
};
} // namespace Av
@@ -17,11 +17,13 @@
* along with LMS. If not, see <http://www.gnu.org/licenses/>. * along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/ */
#include "av/AvTypes.hpp" #include "av/Types.hpp"
namespace Av { namespace Av
{
const char* formatToMimetype(Format format) std::string_view
formatToMimetype(Format format)
{ {
switch (format) switch (format)
{ {
@@ -32,7 +34,7 @@ const char* formatToMimetype(Format format)
case Format::WEBM_VORBIS: return "audio/webm"; case Format::WEBM_VORBIS: return "audio/webm";
} }
throw AvException {"Invalid encoding"}; throw Exception {"Invalid encoding"};
} }
} }
-99
View File
@@ -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 <http://www.gnu.org/licenses/>.
*/
/* This file contains some classes in order to get info from file using the libavconv */
#pragma once
#include <chrono>
#include <filesystem>
#include <functional>
#include <map>
#include <optional>
#include <string>
#include <vector>
#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<std::string, std::string> getMetaData(void);
std::vector<StreamInfo> getStreamInfo() const;
std::optional<std::size_t> getBestStream() const; // none if failure/unknown
bool hasAttachedPictures(void) const;
void visitAttachedPictures(std::function<void(const Picture&)> func) const;
private:
const std::filesystem::path _p;
AVFormatContext* _context {};
};
struct MediaFileFormat
{
std::string mimeType;
std::string format;
};
std::optional<MediaFileFormat> guessMediaFileFormat(const std::filesystem::path& file);
} // namespace Av
-76
View File
@@ -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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <chrono>
#include <filesystem>
#include <optional>
#include <pstreams/pstream.h>
#include "AvTypes.hpp"
namespace Av {
struct TranscodeParameters
{
Format format;
std::size_t bitrate {128000};
std::optional<std::size_t> stream; // Id of the stream to be transcoded (auto detect by default)
std::optional<std::chrono::seconds> 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<unsigned char>& 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<redi::ipstream> _child;
bool _isComplete {};
std::size_t _total {};
const std::size_t _id {};
std::string _outputMimeType;
};
} // namespace Av
+76
View File
@@ -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 <http://www.gnu.org/licenses/>.
*/
/* This file contains some classes in order to get info from file using the libavconv */
#pragma once
#include <chrono>
#include <filesystem>
#include <functional>
#include <unordered_map>
#include <optional>
#include <string>
#include <vector>
#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<std::string, std::string>;
virtual const std::filesystem::path& getPath() const = 0;
virtual std::chrono::milliseconds getDuration() const = 0;
virtual MetadataMap getMetaData() const = 0;
virtual std::vector<StreamInfo> getStreamInfo() const = 0;
virtual std::optional<std::size_t> getBestStream() const = 0; // none if failure/unknown
virtual bool hasAttachedPictures() const = 0;
virtual void visitAttachedPictures(std::function<void(const Picture&)> func) const = 0;
};
std::unique_ptr<IAudioFile> parseAudioFile(const std::filesystem::path& p);
struct AudioFileFormat
{
std::string mimeType;
std::string format;
};
std::optional<AudioFileFormat> guessAudioFileFormat(const std::filesystem::path& file);
} // namespace Av
@@ -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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <chrono>
#include <optional>
#include "Types.hpp"
namespace Av {
struct TranscodeParameters
{
Format format;
std::size_t bitrate {128000};
std::optional<std::size_t> stream; // Id of the stream to be transcoded (auto detect by default)
std::chrono::milliseconds offset {0};
bool stripMetadata {true};
};
} // namespace Av
@@ -29,6 +29,5 @@ namespace Av
struct TranscodeParameters; struct TranscodeParameters;
std::unique_ptr<IResourceHandler> createTranscodeResourceHandler(const std::filesystem::path& trackPath, const TranscodeParameters& parameters); std::unique_ptr<IResourceHandler> createTranscodeResourceHandler(const std::filesystem::path& trackPath, const TranscodeParameters& parameters);
} }
@@ -19,21 +19,21 @@
#pragma once #pragma once
#include <string> #include <string_view>
#include "utils/Exception.hpp" #include "utils/Exception.hpp"
namespace Av { namespace Av {
class AvException : public LmsException class Exception : public LmsException
{ {
public: public:
AvException(const std::string& msg) : LmsException(msg) {} using LmsException::LmsException;
}; };
enum class Format enum class Format
{ {
// Values are important and must not be changed // Values are important and must not be changed (stored in the UI's localstorage)
MP3 = 0, MP3 = 0,
OGG_OPUS = 1, OGG_OPUS = 1,
MATROSKA_OPUS = 2, MATROSKA_OPUS = 2,
@@ -41,7 +41,6 @@ enum class Format
WEBM_VORBIS = 4, WEBM_VORBIS = 4,
}; };
const char* formatToMimetype(Format encoding); std::string_view formatToMimetype(Format format);
} }
+4 -5
View File
@@ -19,7 +19,7 @@
#include "CoverArtGrabber.hpp" #include "CoverArtGrabber.hpp"
#include "av/AvInfo.hpp" #include "av/IAudioFile.hpp"
#include "database/Release.hpp" #include "database/Release.hpp"
#include "database/Session.hpp" #include "database/Session.hpp"
@@ -125,7 +125,7 @@ Grabber::Grabber(const std::filesystem::path& execPath,
} }
std::unique_ptr<IEncodedImage> std::unique_ptr<IEncodedImage>
Grabber::getFromAvMediaFile(const Av::MediaFile& input, ImageSize width) const Grabber::getFromAvMediaFile(const Av::IAudioFile& input, ImageSize width) const
{ {
std::unique_ptr<IEncodedImage> image; std::unique_ptr<IEncodedImage> image;
@@ -303,10 +303,9 @@ Grabber::getFromTrack(const std::filesystem::path& p, ImageSize width) const
try try
{ {
const Av::MediaFile input {p}; image = getFromAvMediaFile(*Av::parseAudioFile(p), width);
image = getFromAvMediaFile(input, width);
} }
catch (Av::AvException& e) catch (Av::Exception& e)
{ {
LMS_LOG(COVER, ERROR) << "Cannot get covers from track " << p.string() << ": " << e.what(); LMS_LOG(COVER, ERROR) << "Cannot get covers from track " << p.string() << ": " << e.what();
} }
+2 -2
View File
@@ -39,7 +39,7 @@ namespace Database
namespace Av namespace Av
{ {
class MediaFile; class IAudioFile;
} }
namespace CoverArt namespace CoverArt
@@ -106,7 +106,7 @@ namespace CoverArt
void flushCache() override; void flushCache() override;
std::shared_ptr<IEncodedImage> getFromTrack(Database::Session& dbSession, Database::IdType trackId, ImageSize width, bool allowReleaseFallback); std::shared_ptr<IEncodedImage> getFromTrack(Database::Session& dbSession, Database::IdType trackId, ImageSize width, bool allowReleaseFallback);
std::unique_ptr<IEncodedImage> getFromAvMediaFile(const Av::MediaFile& input, ImageSize width) const; std::unique_ptr<IEncodedImage> getFromAvMediaFile(const Av::IAudioFile& input, ImageSize width) const;
std::unique_ptr<IEncodedImage> getFromCoverFile(const std::filesystem::path& p, ImageSize width) const; std::unique_ptr<IEncodedImage> getFromCoverFile(const std::filesystem::path& p, ImageSize width) const;
std::unique_ptr<IEncodedImage> getFromTrack(const std::filesystem::path& path, ImageSize width) const; std::unique_ptr<IEncodedImage> getFromTrack(const std::filesystem::path& path, ImageSize width) const;
+13 -18
View File
@@ -22,7 +22,7 @@
#include <algorithm> #include <algorithm>
#include <iostream> #include <iostream>
#include "av/AvInfo.hpp" #include "av/IAudioFile.hpp"
#include "utils/Logger.hpp" #include "utils/Logger.hpp"
#include "utils/String.hpp" #include "utils/String.hpp"
@@ -30,11 +30,9 @@
namespace MetaData namespace MetaData
{ {
using MetadataMap = std::map<std::string, std::string>;
template <typename T> template <typename T>
std::optional<T> std::optional<T>
findFirstValueOfAs(const MetadataMap& metadataMap, std::initializer_list<std::string> tags) findFirstValueOfAs(const Av::IAudioFile::MetadataMap& metadataMap, std::initializer_list<std::string> 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; }); 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)) if (it == std::cend(metadataMap))
@@ -45,7 +43,7 @@ findFirstValueOfAs(const MetadataMap& metadataMap, std::initializer_list<std::st
template <> template <>
std::optional<std::vector<UUID>> std::optional<std::vector<UUID>>
findFirstValueOfAs(const MetadataMap& metadataMap, std::initializer_list<std::string> tags) findFirstValueOfAs(const Av::IAudioFile::MetadataMap& metadataMap, std::initializer_list<std::string> tags)
{ {
std::optional<std::string> str {findFirstValueOfAs<std::string>(metadataMap, tags)}; std::optional<std::string> str {findFirstValueOfAs<std::string>(metadataMap, tags)};
if (!str) if (!str)
@@ -69,7 +67,7 @@ findFirstValueOfAs(const MetadataMap& metadataMap, std::initializer_list<std::st
static static
std::optional<Album> std::optional<Album>
getAlbum(const MetadataMap& metadataMap) getAlbum(const Av::IAudioFile::MetadataMap& metadataMap)
{ {
std::optional<Album> res; std::optional<Album> res;
@@ -84,7 +82,7 @@ getAlbum(const MetadataMap& metadataMap)
static static
std::vector<Artist> std::vector<Artist>
getAlbumArtists(const MetadataMap& metadataMap) getAlbumArtists(const Av::IAudioFile::MetadataMap& metadataMap)
{ {
std::vector<Artist> res; std::vector<Artist> res;
@@ -99,7 +97,7 @@ getAlbumArtists(const MetadataMap& metadataMap)
static static
std::vector<Artist> std::vector<Artist>
getArtists(const MetadataMap& metadataMap) getArtists(const Av::IAudioFile::MetadataMap& metadataMap)
{ {
std::vector<Artist> artists; std::vector<Artist> artists;
@@ -133,31 +131,28 @@ AvFormatParser::parse(const std::filesystem::path& p, bool debug)
try try
{ {
Av::MediaFile mediaFile {p}; const auto mediaFile {Av::parseAudioFile(p)};
// Stream info // Stream info
{ {
std::vector<AudioStream> audioStreams; std::vector<AudioStream> audioStreams;
for (auto stream : mediaFile.getStreamInfo()) for (auto stream : mediaFile->getStreamInfo())
{ {
MetaData::AudioStream audioStream {static_cast<unsigned>(stream.bitrate)}; MetaData::AudioStream audioStream {static_cast<unsigned>(stream.bitrate)};
track.audioStreams.emplace_back(audioStream); track.audioStreams.emplace_back(audioStream);
} }
} }
track.duration = mediaFile.getDuration(); track.duration = mediaFile->getDuration();
track.hasCover = mediaFile.hasAttachedPictures(); track.hasCover = mediaFile->hasAttachedPictures();
MetaData::Clusters clusters; MetaData::Clusters clusters;
const std::map<std::string, std::string> 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) if (debug)
std::cout << "TAG = " << tag << ", VAL = " << value << std::endl; 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.album = getAlbum(metadataMap);
track.albumArtists = getAlbumArtists(metadataMap); track.albumArtists = getAlbumArtists(metadataMap);
} }
catch(Av::MediaFileException& e) catch(Av::Exception& e)
{ {
return std::nullopt; return std::nullopt;
} }
+8 -14
View File
@@ -19,9 +19,9 @@
#include "Stream.hpp" #include "Stream.hpp"
#include "av/AvTranscoder.hpp" #include "av/TranscodeParameters.hpp"
#include "av/AvTranscodeResourceHandlerCreator.hpp" #include "av/TranscodeResourceHandlerCreator.hpp"
#include "av/AvTypes.hpp" #include "av/Types.hpp"
#include "database/Session.hpp" #include "database/Session.hpp"
#include "database/Track.hpp" #include "database/Track.hpp"
#include "database/User.hpp" #include "database/User.hpp"
@@ -113,7 +113,7 @@ handleDownload(RequestContext& context, const Wt::Http::Request& request, Wt::Ht
{ {
std::shared_ptr<IResourceHandler> resourceHandler; std::shared_ptr<IResourceHandler> resourceHandler;
Wt::Http::ResponseContinuation *continuation = request.continuation(); Wt::Http::ResponseContinuation* continuation {request.continuation()};
if (!continuation) if (!continuation)
{ {
// Mandatory params // Mandatory params
@@ -137,13 +137,10 @@ handleDownload(RequestContext& context, const Wt::Http::Request& request, Wt::Ht
resourceHandler = Wt::cpp17::any_cast<std::shared_ptr<IResourceHandler>>(continuation->data()); resourceHandler = Wt::cpp17::any_cast<std::shared_ptr<IResourceHandler>>(continuation->data());
} }
resourceHandler->processRequest(request, response); continuation = resourceHandler->processRequest(request, response);
if (!resourceHandler->isFinished()) if (continuation)
{
Wt::Http::ResponseContinuation *continuation = response.createContinuation();
continuation->setData(resourceHandler); continuation->setData(resourceHandler);
} }
}
void void
handleStream(RequestContext& context, const Wt::Http::Request& request, Wt::Http::Response& response) handleStream(RequestContext& context, const Wt::Http::Request& request, Wt::Http::Response& response)
@@ -164,13 +161,10 @@ handleStream(RequestContext& context, const Wt::Http::Request& request, Wt::Http
resourceHandler = Wt::cpp17::any_cast<std::shared_ptr<IResourceHandler>>(continuation->data()); resourceHandler = Wt::cpp17::any_cast<std::shared_ptr<IResourceHandler>>(continuation->data());
} }
resourceHandler->processRequest(request, response); continuation = resourceHandler->processRequest(request, response);
if (!resourceHandler->isFinished()) if (continuation)
{
Wt::Http::ResponseContinuation *continuation = response.createContinuation();
continuation->setData(resourceHandler); continuation->setData(resourceHandler);
} }
}
} }
+2
View File
@@ -1,5 +1,7 @@
add_library(lmsutils SHARED add_library(lmsutils SHARED
impl/ChildProcess.cpp
impl/ChildProcessManager.cpp
impl/Config.cpp impl/Config.cpp
impl/FileResourceHandler.cpp impl/FileResourceHandler.cpp
impl/Logger.cpp impl/Logger.cpp
+201
View File
@@ -0,0 +1,201 @@
#include "ChildProcess.hpp"
#include <cstring>
#include <cerrno>
#include <fcntl.h>
#include <stdexcept>
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>
#include <unistd.h>
#include <algorithm>
#include <iostream>
#include <mutex>
#include <boost/asio/read.hpp>
#include <boost/asio/buffer.hpp>
#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<std::mutex> 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<const char*> 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();
}
+37
View File
@@ -0,0 +1,37 @@
#pragma once
#include <sys/types.h>
#include <unistd.h>
#include <filesystem>
#include <boost/asio/io_context.hpp>
#include <boost/asio/posix/stream_descriptor.hpp>
#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<int> _exitCode;
};
@@ -0,0 +1,54 @@
#include "ChildProcessManager.hpp"
#include "utils/Logger.hpp"
#include "ChildProcess.hpp"
std::unique_ptr<IChildProcessManager>
createChildProcessManager()
{
return std::make_unique<ChildProcessManager>();
}
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<std::thread>([&]()
{
_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<IChildProcess>
ChildProcessManager::spawnChildProcess(const std::filesystem::path& path, const IChildProcess::Args& args)
{
return std::make_unique<ChildProcess>(_ioContext, path, args);
}
@@ -0,0 +1,34 @@
#pragma once
#include <memory>
#include <thread>
#include <boost/asio/io_context.hpp>
#include <boost/asio/executor_work_guard.hpp>
#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<IChildProcess> spawnChildProcess(const std::filesystem::path& path, const IChildProcess::Args& args) override;
void start();
void stop();
boost::asio::io_context _ioContext;
std::unique_ptr<std::thread> _thread;
boost::asio::executor_work_guard<boost::asio::io_context::executor_type> _work;
};
+9 -13
View File
@@ -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) FileResourceHandler::processRequest(const Wt::Http::Request& request, Wt::Http::Response& response)
{ {
::uint64_t startByte {_offset}; ::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() << "'"; LMS_LOG(UTILS, ERROR) << "Cannot open file stream for '" << _path.string() << "'";
response.setStatus(404); response.setStatus(404);
_isFinished = true; _isFinished = true;
return; return {};
} }
else else
{ {
@@ -72,7 +72,7 @@ FileResourceHandler::processRequest(const Wt::Http::Request& request, Wt::Http::
LMS_LOG(UTILS, DEBUG) << "Range not satisfiable"; LMS_LOG(UTILS, DEBUG) << "Range not satisfiable";
_isFinished = true; _isFinished = true;
return; return {};
} }
if (ranges.size() == 1) 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() << "'"; LMS_LOG(UTILS, ERROR) << "Cannot reopen file stream for '" << _path.string() << "'";
_isFinished = true; _isFinished = true;
return; return {};
} }
ifs.seekg(static_cast<std::istream::pos_type>(startByte)); ifs.seekg(static_cast<std::istream::pos_type>(startByte));
@@ -123,20 +123,16 @@ FileResourceHandler::processRequest(const Wt::Http::Request& request, Wt::Http::
if (ifs.good() && actualPieceSize < restSize) if (ifs.good() && actualPieceSize < restSize)
{ {
_offset = startByte + actualPieceSize; _offset = startByte + actualPieceSize;
LMS_LOG(UTILS, DEBUG) << "Job not complete! Next chunk offset = " << _offset; LMS_LOG(UTILS, DEBUG) << "Job not complete! Next chunk offset = " << _offset;
return response.createContinuation();
} }
else
{
_isFinished = true; _isFinished = true;
LMS_LOG(UTILS, DEBUG) << "Job complete!"; LMS_LOG(UTILS, DEBUG) << "Job complete!";
}
}
bool return {};
FileResourceHandler::isFinished() const
{
return _isFinished;
} }
+2 -3
View File
@@ -29,10 +29,9 @@ class FileResourceHandler final : public IResourceHandler
private: private:
void processRequest(const Wt::Http::Request& request, Wt::Http::Response& response) override; Wt::Http::ResponseContinuation* processRequest(const Wt::Http::Request& request, Wt::Http::Response& response) override;
bool isFinished() const override;
static constexpr std::size_t _chunkSize {262144}; static constexpr std::size_t _chunkSize {65536};
std::filesystem::path _path; std::filesystem::path _path;
::uint64_t _beyondLastByte {}; ::uint64_t _beyondLastByte {};
+1
View File
@@ -26,6 +26,7 @@ const char* getModuleName(Module mod)
case Module::API_SUBSONIC: return "API_SUBSONIC"; case Module::API_SUBSONIC: return "API_SUBSONIC";
case Module::AUTH: return "AUTH"; case Module::AUTH: return "AUTH";
case Module::AV: return "AV"; case Module::AV: return "AV";
case Module::CHILDPROCESS: return "CHILDPROC";
case Module::COVER: return "COVER"; case Module::COVER: return "COVER";
case Module::DB: return "DB"; case Module::DB: return "DB";
case Module::DBUPDATER: return "DB UPDATER"; case Module::DBUPDATER: return "DB UPDATER";
@@ -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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <cstddef>
#include <functional>
#include <string>
#include <vector>
#include "utils/Exception.hpp"
class ChildProcessException : public LmsException
{
public:
using LmsException::LmsException;
};
class IChildProcess
{
public:
using Args = std::vector<std::string>;
virtual ~IChildProcess() = default;
enum class ReadResult
{
Success,
Error,
EndOfFile,
};
using ReadCallback = std::function<void(ReadResult, std::size_t)>;
virtual void asyncRead(std::byte* data, std::size_t bufferSize, ReadCallback callback) = 0;
using WaitCallback = std::function<void(void)>;
virtual void asyncWaitForData(WaitCallback cb) = 0;
virtual std::size_t readSome(std::byte* data, std::size_t bufferSize) = 0;
virtual bool finished() = 0;
};
@@ -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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <filesystem>
#include <memory>
#pragma once
#include <filesystem>
#include <memory>
#include "IChildProcess.hpp"
class IChildProcessManager
{
public:
virtual ~IChildProcessManager() = default;
virtual std::unique_ptr<IChildProcess> spawnChildProcess(const std::filesystem::path& path, const IChildProcess::Args& args) = 0;
};
std::unique_ptr<IChildProcessManager> createChildProcessManager();
@@ -28,7 +28,6 @@ class IResourceHandler
public: public:
virtual ~IResourceHandler() = default; virtual ~IResourceHandler() = default;
virtual void processRequest(const Wt::Http::Request& request, Wt::Http::Response& response) = 0; [[nodiscard]] virtual Wt::Http::ResponseContinuation* processRequest(const Wt::Http::Request& request, Wt::Http::Response& response) = 0;
virtual bool isFinished() const = 0;
}; };
+1
View File
@@ -38,6 +38,7 @@ enum class Module
API_SUBSONIC, API_SUBSONIC,
AUTH, AUTH,
AV, AV,
CHILDPROCESS,
COVER, COVER,
DB, DB,
DBUPDATER, DBUPDATER,
+2 -5
View File
@@ -26,8 +26,6 @@
#include "auth/IAuthTokenService.hpp" #include "auth/IAuthTokenService.hpp"
#include "auth/IPasswordService.hpp" #include "auth/IPasswordService.hpp"
#include "av/AvInfo.hpp"
#include "av/AvTranscoder.hpp"
#include "cover/ICoverArtGrabber.hpp" #include "cover/ICoverArtGrabber.hpp"
#include "database/Db.hpp" #include "database/Db.hpp"
#include "database/Session.hpp" #include "database/Session.hpp"
@@ -35,6 +33,7 @@
#include "recommendation/IEngine.hpp" #include "recommendation/IEngine.hpp"
#include "subsonic/SubsonicResource.hpp" #include "subsonic/SubsonicResource.hpp"
#include "ui/LmsApplication.hpp" #include "ui/LmsApplication.hpp"
#include "utils/IChildProcessManager.hpp"
#include "utils/IConfig.hpp" #include "utils/IConfig.hpp"
#include "utils/Service.hpp" #include "utils/Service.hpp"
#include "utils/WtLogger.hpp" #include "utils/WtLogger.hpp"
@@ -203,9 +202,6 @@ int main(int argc, char* argv[])
Wt::WServer server {argv[0]}; Wt::WServer server {argv[0]};
server.setServerConfiguration(wtServerArgs.size(), const_cast<char**>(&wtArgv[0])); server.setServerConfiguration(wtServerArgs.size(), const_cast<char**>(&wtArgv[0]));
// lib init
Av::Transcoder::init();
// Initializing a connection pool to the database that will be shared along services // Initializing a connection pool to the database that will be shared along services
Database::Db database {config->getPath("working-dir") / "lms.db"}; Database::Db database {config->getPath("working-dir") / "lms.db"};
{ {
@@ -217,6 +213,7 @@ int main(int argc, char* argv[])
UserInterface::LmsApplicationGroupContainer appGroups; UserInterface::LmsApplicationGroupContainer appGroups;
// Service initialization order is important // Service initialization order is important
Service<IChildProcessManager> childProcessManagerService {createChildProcessManager()};
Service<Auth::IAuthTokenService> authTokenService {Auth::createAuthTokenService(config->getULong("login-throttler-max-entriees", 10000))}; Service<Auth::IAuthTokenService> authTokenService {Auth::createAuthTokenService(config->getULong("login-throttler-max-entriees", 10000))};
Service<Auth::IPasswordService> passwordService {Auth::createPasswordService(config->getULong("login-throttler-max-entriees", 10000))}; Service<Auth::IPasswordService> passwordService {Auth::createPasswordService(config->getULong("login-throttler-max-entriees", 10000))};
Service<CoverArt::IGrabber> coverArtService {CoverArt::createGrabber(argv[0], Service<CoverArt::IGrabber> coverArtService {CoverArt::createGrabber(argv[0],
+3 -7
View File
@@ -22,7 +22,7 @@
#include <fstream> #include <fstream>
#include <Wt/Http/Response.h> #include <Wt/Http/Response.h>
#include "av/AvInfo.hpp" #include "av/IAudioFile.hpp"
#include "database/Session.hpp" #include "database/Session.hpp"
#include "database/Track.hpp" #include "database/Track.hpp"
#include "utils/FileResourceHandlerCreator.hpp" #include "utils/FileResourceHandlerCreator.hpp"
@@ -101,14 +101,10 @@ AudioFileResource::handleRequest(const Wt::Http::Request& request,
fileResourceHandler = Wt::cpp17::any_cast<std::shared_ptr<IResourceHandler>>(request.continuation()->data()); fileResourceHandler = Wt::cpp17::any_cast<std::shared_ptr<IResourceHandler>>(request.continuation()->data());
} }
fileResourceHandler->processRequest(request, response); auto* continuation {fileResourceHandler->processRequest(request, response)};
if (continuation)
if (!fileResourceHandler->isFinished())
{
auto* continuation {response.createContinuation()};
continuation->setData(fileResourceHandler); continuation->setData(fileResourceHandler);
} }
}
} // namespace UserInterface } // namespace UserInterface
+53 -66
View File
@@ -19,9 +19,12 @@
#include "AudioTranscodeResource.hpp" #include "AudioTranscodeResource.hpp"
#include <optional>
#include <Wt/Http/Response.h> #include <Wt/Http/Response.h>
#include "av/AvTranscoder.hpp" #include "av/TranscodeParameters.hpp"
#include "av/TranscodeResourceHandlerCreator.hpp"
#include "av/Types.hpp"
#include "database/Session.hpp" #include "database/Session.hpp"
#include "database/Track.hpp" #include "database/Track.hpp"
#include "database/User.hpp" #include "database/User.hpp"
@@ -66,13 +69,6 @@ namespace StringUtils
} }
} }
namespace UserInterface {
AudioTranscodeResource:: ~AudioTranscodeResource()
{
beingDeleted();
}
static static
std::optional<Av::Format> std::optional<Av::Format>
AudioFormatToAvFormat(Database::AudioFormat format) AudioFormatToAvFormat(Database::AudioFormat format)
@@ -91,6 +87,14 @@ AudioFormatToAvFormat(Database::AudioFormat format)
return std::nullopt; return std::nullopt;
} }
namespace UserInterface {
AudioTranscodeResource:: ~AudioTranscodeResource()
{
beingDeleted();
}
std::string std::string
AudioTranscodeResource::getUrl(Database::IdType trackId) const AudioTranscodeResource::getUrl(Database::IdType trackId) const
{ {
@@ -115,22 +119,17 @@ readParameterAs(const Wt::Http::Request& request, const std::string& parameterNa
return res; return res;
} }
void struct TranscodeParameters
AudioTranscodeResource::handleRequest(const Wt::Http::Request& request,
Wt::Http::Response& response)
{ {
std::shared_ptr<Av::Transcoder> transcoder; std::filesystem::path file;
Av::TranscodeParameters transcodeParameters;
};
// First, see if this request is for a continuation static
Wt::Http::ResponseContinuation *continuation = request.continuation(); std::optional<TranscodeParameters>
if (continuation) readTranscodeParameters(const Wt::Http::Request& request)
{ {
LOG(DEBUG) << "Continuation! " << continuation ; TranscodeParameters parameters;
transcoder = Wt::cpp17::any_cast<std::shared_ptr<Av::Transcoder>>(continuation->data());
}
else
{
LOG(DEBUG) << "First request: creating transcoder";
// mandatory parameters // mandatory parameters
auto trackId {readParameterAs<Database::IdType>(request, "trackid")}; auto trackId {readParameterAs<Database::IdType>(request, "trackid")};
@@ -138,14 +137,14 @@ AudioTranscodeResource::handleRequest(const Wt::Http::Request& request,
auto bitrate {readParameterAs<Database::Bitrate>(request, "bitrate")}; auto bitrate {readParameterAs<Database::Bitrate>(request, "bitrate")};
if (!trackId || !format || !bitrate) if (!trackId || !format || !bitrate)
return; return std::nullopt;
auto avFormat {AudioFormatToAvFormat(*format)}; const std::optional<Av::Format> avFormat {AudioFormatToAvFormat(*format)};
if (!avFormat) if (!avFormat)
return; return std::nullopt;
// optional parameter // optional parameter
auto offset {readParameterAs<std::size_t>(request, "offset")}; std::size_t offset {readParameterAs<std::size_t>(request, "offset").value_or(0)};
std::filesystem::path trackPath; std::filesystem::path trackPath;
{ {
@@ -155,62 +154,50 @@ AudioTranscodeResource::handleRequest(const Wt::Http::Request& request,
if (!track) if (!track)
{ {
LOG(ERROR) << "Missing track"; LOG(ERROR) << "Missing track";
return; return std::nullopt;
} }
trackPath = track->getPath(); parameters.file = track->getPath();
if (Database::User::audioTranscodeAllowedBitrates.find(*bitrate) == std::cend(Database::User::audioTranscodeAllowedBitrates)) if (Database::User::audioTranscodeAllowedBitrates.find(*bitrate) == std::cend(Database::User::audioTranscodeAllowedBitrates))
{ {
LOG(ERROR) << "Bitrate '" << *bitrate << "' is not allowed"; LOG(ERROR) << "Bitrate '" << *bitrate << "' is not allowed";
return; return std::nullopt;
} }
} }
Av::TranscodeParameters parameters {}; parameters.transcodeParameters.stripMetadata = true;
parameters.stripMetadata = true; parameters.transcodeParameters.format = *avFormat;
parameters.format = *avFormat; parameters.transcodeParameters.bitrate = *bitrate;
parameters.bitrate = *bitrate; parameters.transcodeParameters.offset = std::chrono::seconds {offset};
parameters.offset = std::chrono::seconds {offset ? *offset : 0};
transcoder = std::make_shared<Av::Transcoder>(trackPath, parameters); return parameters;
if (!transcoder->start()) }
void
AudioTranscodeResource::handleRequest(const Wt::Http::Request& request,
Wt::Http::Response& response)
{ {
LOG(ERROR) << "Cannot start transcoder"; std::shared_ptr<IResourceHandler> resourceHandler;
return;
}
LOG(DEBUG) << "Transcoder started"; Wt::Http::ResponseContinuation* continuation {request.continuation()};
if (!continuation)
std::string mimeType {transcoder->getOutputMimeType()};
response.setMimeType(mimeType);
LOG(DEBUG) << "Mime type set to '" << mimeType << "'";
}
if (!transcoder->isComplete())
{ {
std::vector<unsigned char> data; const std::optional<TranscodeParameters>& parameters {readTranscodeParameters(request)};
data.reserve(_chunkSize); if (parameters)
resourceHandler = Av::createTranscodeResourceHandler(parameters->file, parameters->transcodeParameters);
transcoder->process(data, _chunkSize);
response.out().write(reinterpret_cast<char*>(&data[0]), data.size());
LOG(DEBUG) << "Written " << data.size() << " bytes! complete = " << (transcoder->isComplete() ? "true" : "false");
if (!response.out())
{
LOG(ERROR) << "Write failed!";
}
}
if (!transcoder->isComplete() && response.out())
{
continuation = response.createContinuation();
continuation->setData(transcoder);
} }
else else
LOG(DEBUG) << "No more data!"; {
resourceHandler = Wt::cpp17::any_cast<std::shared_ptr<IResourceHandler>>(continuation->data());
}
if (resourceHandler)
{
continuation = resourceHandler->processRequest(request, response);
if (continuation)
continuation->setData(resourceHandler);
}
} }
} // namespace UserInterface } // namespace UserInterface