Merge branch 'async-transcoder' into develop
This commit is contained in:
@@ -16,7 +16,6 @@ addons:
|
||||
- libavformat-dev
|
||||
- ffmpeg
|
||||
- libstb-dev
|
||||
- libpstreams-dev
|
||||
- libtag1-dev
|
||||
- libpam0g-dev
|
||||
- libgraphicsmagick++1-dev
|
||||
|
||||
@@ -11,7 +11,6 @@ include(CTest)
|
||||
find_package(Filesystem REQUIRED)
|
||||
find_package(FFMPEGAV REQUIRED)
|
||||
find_package(Boost REQUIRED COMPONENTS system program_options)
|
||||
find_package(PStreams REQUIRED)
|
||||
find_package(Wt REQUIRED COMPONENTS Wt Dbo DboSqlite3 HTTP)
|
||||
find_package(PAM)
|
||||
find_package(STB)
|
||||
|
||||
+4
-1
@@ -43,7 +43,7 @@ __Notes__:
|
||||
* a C++17 compiler is needed
|
||||
* ffmpeg version 4 minimum is required
|
||||
```sh
|
||||
apt-get install g++ cmake libboost-system-dev libavutil-dev libavformat-dev libstb-dev libconfig++-dev libpstreams-dev ffmpeg libtag1-dev libpam0g-dev
|
||||
apt-get install g++ cmake libboost-system-dev libavutil-dev libavformat-dev libstb-dev libconfig++-dev ffmpeg libtag1-dev libpam0g-dev
|
||||
```
|
||||
__Notes__:
|
||||
* libpam0g-dev is optional (only for using PAM authentication)
|
||||
@@ -157,6 +157,9 @@ server {
|
||||
proxy_request_buffering off;
|
||||
proxy_buffering off;
|
||||
proxy_buffer_size 4k;
|
||||
proxy_read_timeout 10m;
|
||||
proxy_send_timeout 10m;
|
||||
keepalive_timeout 10m;
|
||||
|
||||
location / {
|
||||
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
# If already in cache, be silent
|
||||
if(PSTREAMS_INCLUDE_DIRS)
|
||||
set (PSTREAMS_FIND_QUIETLY TRUE)
|
||||
endif()
|
||||
|
||||
FIND_PATH(PSTREAMS_INCLUDE_DIR NAMES pstream.h
|
||||
PATH_SUFFIXES pstreams
|
||||
HINTS ${PSTREAMS_ROOT}/include $ENV{PSTREAMS_ROOT})
|
||||
|
||||
set(PSTREAMS_INCLUDE_DIRS ${PSTREAMS_INCLUDE_DIR})
|
||||
|
||||
# Handle the QUIETLY and REQUIRED arguments and set PSTREAMS_FOUND to TRUE if
|
||||
# all listed variables are TRUE.
|
||||
INCLUDE(FindPackageHandleStandardArgs)
|
||||
FIND_PACKAGE_HANDLE_STANDARD_ARGS(Pstreams DEFAULT_MSG PSTREAMS_INCLUDE_DIRS)
|
||||
|
||||
MARK_AS_ADVANCED(PSTREAMS_INCLUDE_DIRS)
|
||||
+1
-9
@@ -4,9 +4,8 @@ WORKDIR /tmp/workdir
|
||||
|
||||
ARG MAKEFLAGS="-j2"
|
||||
ARG FFMPEG_VERSION=4.1.4
|
||||
ARG WT_VERSION=4.2.0
|
||||
ARG WT_VERSION=4.5.0
|
||||
ARG STB_VERSION=b42009b3b9d4ca35bc703f5310eedc74f584be58
|
||||
ARG PSTREAMS_VERSION=1.0.1
|
||||
ARG LMS_VERSION=v3.6.3
|
||||
|
||||
ARG PREFIX="/tmp/install"
|
||||
@@ -102,13 +101,6 @@ RUN \
|
||||
make && \
|
||||
make install
|
||||
|
||||
# libpstreams
|
||||
RUN \
|
||||
DIR=/tmp/libpstreams && mkdir -p ${DIR} && cd ${DIR} && \
|
||||
curl -sLO https://sourceforge.net/projects/pstreams/files/pstreams/Release%201.0/pstreams-${PSTREAMS_VERSION}.tar.gz && \
|
||||
tar -x --strip-components=1 -f pstreams-${PSTREAMS_VERSION}.tar.gz && \
|
||||
prefix=${PREFIX} make install prefix=/ DESTDIR=${PREFIX}
|
||||
|
||||
# LMS
|
||||
RUN \
|
||||
DIR=/tmp/lms && mkdir -p ${DIR} && cd ${DIR} && \
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#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<IAudioFile>
|
||||
parseAudioFile(const std::filesystem::path& p)
|
||||
{
|
||||
return std::make_unique<AudioFile>(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<std::string, std::string>& res)
|
||||
getMetaDataFromDictionnary(AVDictionary* dictionnary, AudioFile::MetadataMap& res)
|
||||
{
|
||||
if (!dictionnary)
|
||||
return;
|
||||
@@ -102,10 +110,10 @@ getMetaDataFromDictionnary(AVDictionary* dictionnary, std::map<std::string, std:
|
||||
}
|
||||
}
|
||||
|
||||
std::map<std::string, std::string>
|
||||
MediaFile::getMetaData(void)
|
||||
AudioFile::MetadataMap
|
||||
AudioFile::getMetaData() const
|
||||
{
|
||||
std::map<std::string, std::string> 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<StreamInfo>
|
||||
MediaFile::getStreamInfo() const
|
||||
AudioFile::getStreamInfo() const
|
||||
{
|
||||
std::vector<StreamInfo> res;
|
||||
|
||||
@@ -154,7 +162,7 @@ MediaFile::getStreamInfo() const
|
||||
}
|
||||
|
||||
std::optional<std::size_t>
|
||||
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<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_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)
|
||||
{
|
||||
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();
|
||||
|
||||
@@ -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/>.
|
||||
*/
|
||||
|
||||
/* 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
|
||||
|
||||
+20
-12
@@ -17,7 +17,7 @@
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#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<unsigned char> buffer;
|
||||
|
||||
_transcoder.process(buffer, _chunkSize);
|
||||
response.out().write(reinterpret_cast<const char *>(&buffer[0]), buffer.size());
|
||||
response.out().write(reinterpret_cast<const char *>(&_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 {};
|
||||
}
|
||||
}
|
||||
|
||||
+8
-5
@@ -19,9 +19,12 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <filesystem>
|
||||
#include "av/AvTranscoder.hpp"
|
||||
|
||||
#include "av/TranscodeParameters.hpp"
|
||||
#include "utils/IResourceHandler.hpp"
|
||||
#include "Transcoder.hpp"
|
||||
|
||||
namespace Av
|
||||
{
|
||||
@@ -32,11 +35,11 @@ namespace Av
|
||||
TranscodeResourceHandler(const std::filesystem::path& trackPath, const TranscodeParameters& parameters);
|
||||
|
||||
private:
|
||||
Wt::Http::ResponseContinuation* processRequest(const Wt::Http::Request& request, Wt::Http::Response& reponse) override;
|
||||
|
||||
void 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;
|
||||
Transcoder _transcoder;
|
||||
};
|
||||
@@ -17,12 +17,12 @@
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "av/AvTranscoder.hpp"
|
||||
#include "Transcoder.hpp"
|
||||
|
||||
#include <atomic>
|
||||
#include <mutex>
|
||||
#include <iomanip>
|
||||
|
||||
#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<size_t> globalId {};
|
||||
static std::atomic<size_t> globalId {};
|
||||
static std::filesystem::path ffmpegPath;
|
||||
|
||||
void
|
||||
@@ -40,39 +40,40 @@ Transcoder::init()
|
||||
{
|
||||
ffmpegPath = Service<IConfig>::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 +83,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 +125,7 @@ Transcoder::start()
|
||||
args.emplace_back("-b:a");
|
||||
args.emplace_back(std::to_string(_parameters.bitrate));
|
||||
|
||||
|
||||
// Codecs and formats
|
||||
switch (_parameters.format)
|
||||
{
|
||||
@@ -157,7 +163,6 @@ Transcoder::start()
|
||||
break;
|
||||
|
||||
default:
|
||||
_isComplete = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -169,84 +174,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<std::mutex> lock {transcoderMutex};
|
||||
|
||||
_child = std::make_shared<redi::ipstream>();
|
||||
|
||||
// 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<IChildProcessManager>::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<unsigned char>& 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<char*>(&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
|
||||
@@ -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,23 +17,25 @@
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#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"};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
-1
@@ -29,6 +29,5 @@ namespace Av
|
||||
struct TranscodeParameters;
|
||||
|
||||
std::unique_ptr<IResourceHandler> createTranscodeResourceHandler(const std::filesystem::path& trackPath, const TranscodeParameters& parameters);
|
||||
|
||||
}
|
||||
|
||||
@@ -19,29 +19,28 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
#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);
|
||||
}
|
||||
|
||||
@@ -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<IEncodedImage>
|
||||
Grabber::getFromAvMediaFile(const Av::MediaFile& input, ImageSize width) const
|
||||
Grabber::getFromAvMediaFile(const Av::IAudioFile& input, ImageSize width) const
|
||||
{
|
||||
std::unique_ptr<IEncodedImage> 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();
|
||||
}
|
||||
|
||||
@@ -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<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> getFromTrack(const std::filesystem::path& path, ImageSize width) const;
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
#include <algorithm>
|
||||
#include <iostream>
|
||||
|
||||
#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<std::string, std::string>;
|
||||
|
||||
template <typename 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; });
|
||||
if (it == std::cend(metadataMap))
|
||||
@@ -45,7 +43,7 @@ findFirstValueOfAs(const MetadataMap& metadataMap, std::initializer_list<std::st
|
||||
|
||||
template <>
|
||||
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)};
|
||||
if (!str)
|
||||
@@ -69,7 +67,7 @@ findFirstValueOfAs(const MetadataMap& metadataMap, std::initializer_list<std::st
|
||||
|
||||
static
|
||||
std::optional<Album>
|
||||
getAlbum(const MetadataMap& metadataMap)
|
||||
getAlbum(const Av::IAudioFile::MetadataMap& metadataMap)
|
||||
{
|
||||
std::optional<Album> res;
|
||||
|
||||
@@ -84,7 +82,7 @@ getAlbum(const MetadataMap& metadataMap)
|
||||
|
||||
static
|
||||
std::vector<Artist>
|
||||
getAlbumArtists(const MetadataMap& metadataMap)
|
||||
getAlbumArtists(const Av::IAudioFile::MetadataMap& metadataMap)
|
||||
{
|
||||
std::vector<Artist> res;
|
||||
|
||||
@@ -99,7 +97,7 @@ getAlbumArtists(const MetadataMap& metadataMap)
|
||||
|
||||
static
|
||||
std::vector<Artist>
|
||||
getArtists(const MetadataMap& metadataMap)
|
||||
getArtists(const Av::IAudioFile::MetadataMap& metadataMap)
|
||||
{
|
||||
std::vector<Artist> 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<AudioStream> audioStreams;
|
||||
|
||||
for (auto stream : mediaFile.getStreamInfo())
|
||||
for (auto stream : mediaFile->getStreamInfo())
|
||||
{
|
||||
MetaData::AudioStream audioStream {static_cast<unsigned>(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<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)
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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<IResourceHandler> 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<std::shared_ptr<IResourceHandler>>(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<IResourceHandler> 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<std::shared_ptr<IResourceHandler>>(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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ using namespace Database;
|
||||
static const std::string genreClusterName {"GENRE"};
|
||||
static const std::string reportedStarredDate {"2000-01-01T00:00:00"};
|
||||
static const std::string reportedDummyDate {"2000-01-01T00:00:00"};
|
||||
static const unsigned long reportedDummyDateULong {946684800000}; // 2000-01-01T00:00:00 UTC
|
||||
static const unsigned long long reportedDummyDateULong {946684800000ULL}; // 2000-01-01T00:00:00 UTC
|
||||
|
||||
namespace API::Subsonic
|
||||
{
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
|
||||
add_library(lmsutils SHARED
|
||||
impl/ChildProcess.cpp
|
||||
impl/ChildProcessManager.cpp
|
||||
impl/Config.cpp
|
||||
impl/FileResourceHandler.cpp
|
||||
impl/Logger.cpp
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
#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)
|
||||
{
|
||||
if (error == boost::asio::error::operation_aborted)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
{
|
||||
boost::system::error_code closeError;
|
||||
_childStdout.close(closeError);
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -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,33 @@
|
||||
#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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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<std::istream::pos_type>(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 {};
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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 {};
|
||||
|
||||
@@ -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 "";
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -22,13 +22,12 @@
|
||||
#include <Wt/Http/Request.h>
|
||||
#include <Wt/Http/Response.h>
|
||||
|
||||
// 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;
|
||||
};
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ enum class Module
|
||||
API_SUBSONIC,
|
||||
AUTH,
|
||||
AV,
|
||||
CHILDPROCESS,
|
||||
COVER,
|
||||
DB,
|
||||
DBUPDATER,
|
||||
|
||||
+2
-6
@@ -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"
|
||||
@@ -180,7 +179,6 @@ int main(int argc, char* argv[])
|
||||
|
||||
try
|
||||
{
|
||||
// Make pstream work with ffmpeg
|
||||
close(STDIN_FILENO);
|
||||
|
||||
Service<IConfig> config {createConfig(configFilePath)};
|
||||
@@ -203,9 +201,6 @@ int main(int argc, char* argv[])
|
||||
Wt::WServer server {argv[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
|
||||
Database::Db database {config->getPath("working-dir") / "lms.db"};
|
||||
{
|
||||
@@ -217,6 +212,7 @@ int main(int argc, char* argv[])
|
||||
UserInterface::LmsApplicationGroupContainer appGroups;
|
||||
|
||||
// Service initialization order is important
|
||||
Service<IChildProcessManager> childProcessManagerService {createChildProcessManager()};
|
||||
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<CoverArt::IGrabber> coverArtService {CoverArt::createGrabber(argv[0],
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
#include <fstream>
|
||||
#include <Wt/Http/Response.h>
|
||||
|
||||
#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<std::shared_ptr<IResourceHandler>>(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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -19,9 +19,12 @@
|
||||
|
||||
#include "AudioTranscodeResource.hpp"
|
||||
|
||||
#include <optional>
|
||||
#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/Track.hpp"
|
||||
#include "database/User.hpp"
|
||||
@@ -66,20 +69,13 @@ namespace StringUtils
|
||||
}
|
||||
}
|
||||
|
||||
namespace UserInterface {
|
||||
|
||||
AudioTranscodeResource:: ~AudioTranscodeResource()
|
||||
{
|
||||
beingDeleted();
|
||||
}
|
||||
|
||||
static
|
||||
std::optional<Av::Format>
|
||||
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<TranscodeParameters>
|
||||
readTranscodeParameters(const Wt::Http::Request& request)
|
||||
{
|
||||
TranscodeParameters parameters;
|
||||
|
||||
// mandatory parameters
|
||||
auto trackId {readParameterAs<Database::IdType>(request, "trackid")};
|
||||
auto format {readParameterAs<Database::AudioFormat>(request, "format")};
|
||||
auto bitrate {readParameterAs<Database::Bitrate>(request, "bitrate")};
|
||||
|
||||
if (!trackId || !format || !bitrate)
|
||||
return std::nullopt;
|
||||
|
||||
const std::optional<Av::Format> avFormat {AudioFormatToAvFormat(*format)};
|
||||
if (!avFormat)
|
||||
return std::nullopt;
|
||||
|
||||
// optional parameter
|
||||
std::size_t offset {readParameterAs<std::size_t>(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<Av::Transcoder> transcoder;
|
||||
std::shared_ptr<IResourceHandler> 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<std::shared_ptr<Av::Transcoder>>(continuation->data());
|
||||
const std::optional<TranscodeParameters>& parameters {readTranscodeParameters(request)};
|
||||
if (parameters)
|
||||
resourceHandler = Av::createTranscodeResourceHandler(parameters->file, parameters->transcodeParameters);
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG(DEBUG) << "First request: creating transcoder";
|
||||
|
||||
// mandatory parameters
|
||||
auto trackId {readParameterAs<Database::IdType>(request, "trackid")};
|
||||
auto format {readParameterAs<Database::AudioFormat>(request, "format")};
|
||||
auto bitrate {readParameterAs<Database::Bitrate>(request, "bitrate")};
|
||||
|
||||
if (!trackId || !format || !bitrate)
|
||||
return;
|
||||
|
||||
auto avFormat {AudioFormatToAvFormat(*format)};
|
||||
if (!avFormat)
|
||||
return;
|
||||
|
||||
// optional parameter
|
||||
auto offset {readParameterAs<std::size_t>(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<Av::Transcoder>(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<std::shared_ptr<IResourceHandler>>(continuation->data());
|
||||
}
|
||||
|
||||
if (!transcoder->isComplete())
|
||||
if (resourceHandler)
|
||||
{
|
||||
std::vector<unsigned char> data;
|
||||
data.reserve(_chunkSize);
|
||||
|
||||
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!";
|
||||
}
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user