Renamed some transcoding stuff + now retrieve decoder codecs

This commit is contained in:
emeric
2023-11-12 20:51:42 +01:00
parent 53943c4118
commit 87d70465e9
22 changed files with 711 additions and 712 deletions
+1 -2
View File
@@ -3,8 +3,7 @@ add_library(lmsav SHARED
impl/AudioFile.cpp impl/AudioFile.cpp
impl/RawResourceHandlerCreator.cpp impl/RawResourceHandlerCreator.cpp
impl/Transcoder.cpp impl/Transcoder.cpp
impl/TranscodeResourceHandler.cpp impl/TranscodingResourceHandler.cpp
impl/Types.cpp
) )
target_include_directories(lmsav INTERFACE target_include_directories(lmsav INTERFACE
+30 -2
View File
@@ -65,6 +65,31 @@ namespace Av
res[StringUtils::stringToUpper(tag->key)] = tag->value; res[StringUtils::stringToUpper(tag->key)] = tag->value;
} }
} }
DecodingCodec avcodecToDecodingCodec(AVCodecID codec)
{
switch (codec)
{
case AV_CODEC_ID_MP3: return DecodingCodec::MP3;
case AV_CODEC_ID_AAC: return DecodingCodec::AAC;
case AV_CODEC_ID_AC3: return DecodingCodec::AC3;
case AV_CODEC_ID_VORBIS: return DecodingCodec::VORBIS;
case AV_CODEC_ID_WMAV1: return DecodingCodec::WMAV1;
case AV_CODEC_ID_WMAV2: return DecodingCodec::WMAV2;
case AV_CODEC_ID_FLAC: return DecodingCodec::FLAC;
case AV_CODEC_ID_ALAC: return DecodingCodec::ALAC;
case AV_CODEC_ID_WAVPACK: return DecodingCodec::WAVPACK;
case AV_CODEC_ID_MUSEPACK7: return DecodingCodec::MUSEPACK7;
case AV_CODEC_ID_MUSEPACK8: return DecodingCodec::MUSEPACK8;
case AV_CODEC_ID_APE: return DecodingCodec::APE;
case AV_CODEC_ID_EAC3: return DecodingCodec::EAC3;
case AV_CODEC_ID_MP4ALS: return DecodingCodec::MP4ALS;
case AV_CODEC_ID_OPUS: return DecodingCodec::OPUS;
case AV_CODEC_ID_SHORTEN: return DecodingCodec::SHORTEN;
default:
return DecodingCodec::UNKNOWN;
}
}
} }
std::unique_ptr<IAudioFile> parseAudioFile(const std::filesystem::path& p) std::unique_ptr<IAudioFile> parseAudioFile(const std::filesystem::path& p)
@@ -254,14 +279,17 @@ namespace Av
res.emplace(); res.emplace();
res->index = streamIndex; res->index = streamIndex;
res->bitrate = static_cast<std::size_t>(avstream->codecpar->bit_rate); res->bitrate = static_cast<std::size_t>(avstream->codecpar->bit_rate);
res->codec = ::avcodec_get_name(avstream->codecpar->codec_id); res->codec = avcodecToDecodingCodec(avstream->codecpar->codec_id);
assert(!res->codec.empty()); res->codecName = ::avcodec_get_name(avstream->codecpar->codec_id);
assert(!res->codecName.empty()); // doc says it is never NULL
return res; return res;
} }
std::string_view getMimeType(const std::filesystem::path& fileExtension) std::string_view getMimeType(const std::filesystem::path& fileExtension)
{ {
// List should be sync with the demuxers shipped in the lms's docker version
// + the _audioFileExtensions in ScanSettings
static const std::unordered_map<std::filesystem::path, std::string_view> entries static const std::unordered_map<std::filesystem::path, std::string_view> entries
{ {
{".mp3", "audio/mpeg"}, {".mp3", "audio/mpeg"},
@@ -1,106 +0,0 @@
/*
* 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/>.
*/
#include "TranscodeResourceHandler.hpp"
#include "utils/Logger.hpp"
namespace Av
{
namespace
{
std::size_t
doEstimateContentLength(const InputFileParameters& inputFileParameters, const TranscodeParameters& transcodeParameters)
{
const std::size_t estimatedContentLength {transcodeParameters.bitrate / 8 * static_cast<std::size_t>(std::chrono::duration_cast<std::chrono::milliseconds>(inputFileParameters.duration).count()) / 1000};
return estimatedContentLength;
}
}
std::unique_ptr<IResourceHandler>
createTranscodeResourceHandler(const InputFileParameters& inputFileParameters, const TranscodeParameters& transcodeParameters, bool estimateContentLength)
{
return std::make_unique<TranscodeResourceHandler>(inputFileParameters, transcodeParameters, estimateContentLength);
}
// TODO set some nice HTTP return code
TranscodeResourceHandler::TranscodeResourceHandler(const InputFileParameters& inputFileParameters, const TranscodeParameters& transcodeParameters, bool estimateContentLength)
: _estimatedContentLength {estimateContentLength ? std::make_optional(doEstimateContentLength(inputFileParameters, transcodeParameters)) : std::nullopt}
, _transcoder {inputFileParameters, transcodeParameters}
{
if (_estimatedContentLength)
LMS_LOG(TRANSCODE, DEBUG) << "Estimated content length = " << *_estimatedContentLength;
else
LMS_LOG(TRANSCODE, DEBUG) << "Not using estimated content length";
}
Wt::Http::ResponseContinuation*
TranscodeResourceHandler::processRequest(const Wt::Http::Request& /*request*/, Wt::Http::Response& response)
{
if (_estimatedContentLength)
response.setContentLength(*_estimatedContentLength);
response.setMimeType(_transcoder.getOutputMimeType());
LMS_LOG(TRANSCODE, DEBUG) << "Transcoder finished = " << _transcoder.finished() << ", total served bytes = " << _totalServedByteCount << ", mime type = " << _transcoder.getOutputMimeType();
if (_bytesReadyCount > 0)
{
LMS_LOG(TRANSCODE, DEBUG) << "Writing " << _bytesReadyCount << " bytes back to client";
response.out().write(reinterpret_cast<const char *>(&_buffer[0]), _bytesReadyCount);
_totalServedByteCount += _bytesReadyCount;
_bytesReadyCount = 0;
}
if (!_transcoder.finished())
{
Wt::Http::ResponseContinuation *continuation {response.createContinuation()};
continuation->waitForMoreData();
_transcoder.asyncRead(_buffer.data(), _buffer.size(), [=](std::size_t nbBytesRead)
{
LMS_LOG(TRANSCODE, DEBUG) << "Have " << nbBytesRead << " more bytes to send back";
assert(_bytesReadyCount == 0);
_bytesReadyCount = nbBytesRead;
continuation->haveMoreData();
});
return continuation;
}
else
{
// pad with 0 if necessary as duration may not be accurate
if (_estimatedContentLength && *_estimatedContentLength > _totalServedByteCount)
{
const std::size_t padSize {*_estimatedContentLength - _totalServedByteCount};
LMS_LOG(TRANSCODE, DEBUG) << "Adding " << padSize << " padding bytes";
for (std::size_t i {}; i < padSize; ++i)
response.out().put(0);
_totalServedByteCount += padSize;
}
LMS_LOG(TRANSCODE, DEBUG) << "Transcoding finished. Total served byte count = " << _totalServedByteCount;
}
return {};
}
}
+152 -142
View File
@@ -28,181 +28,191 @@
#include "utils/Logger.hpp" #include "utils/Logger.hpp"
#include "utils/Service.hpp" #include "utils/Service.hpp"
namespace Av { namespace Av::Transcoding
{
#define LOG(sev) LMS_LOG(TRANSCODE, sev) << "[" << _debugId << "] - " #define LOG(sev) LMS_LOG(TRANSCODE, sev) << "[" << _debugId << "] - "
static std::atomic<size_t> globalId {}; static std::atomic<size_t> globalId{};
static std::filesystem::path ffmpegPath; static std::filesystem::path ffmpegPath;
void std::string_view formatToMimetype(OutputFormat format)
Transcoder::init() {
{ switch (format)
ffmpegPath = Service<IConfig>::get()->getPath("ffmpeg-file", "/usr/bin/ffmpeg"); {
if (!std::filesystem::exists(ffmpegPath)) case OutputFormat::MP3: return "audio/mpeg";
throw Exception {"File '" + ffmpegPath.string() + "' does not exist!"}; case OutputFormat::OGG_OPUS: return "audio/opus";
} case OutputFormat::MATROSKA_OPUS: return "audio/x-matroska";
case OutputFormat::OGG_VORBIS: return "audio/ogg";
case OutputFormat::WEBM_VORBIS: return "audio/webm";
}
Transcoder::Transcoder(const InputFileParameters& inputFileParameters, const TranscodeParameters& transcodeParameters) throw Exception{ "Invalid encoding" };
: _debugId {globalId++} }
, _inputFileParameters {inputFileParameters}
, _transcodeParameters {transcodeParameters}
{
start();
}
Transcoder::~Transcoder() = default; void Transcoder::init()
{
ffmpegPath = Service<IConfig>::get()->getPath("ffmpeg-file", "/usr/bin/ffmpeg");
if (!std::filesystem::exists(ffmpegPath))
throw Exception{ "File '" + ffmpegPath.string() + "' does not exist!" };
}
void Transcoder::Transcoder(const InputParameters& inputParameters, const OutputParameters& outputParameters)
Transcoder::start() : _debugId{ globalId++ }
{ , _inputParameters{ inputParameters }
if (ffmpegPath.empty()) , _outputParameters{ outputParameters }
init(); {
start();
}
try Transcoder::~Transcoder() = default;
{
if (!std::filesystem::exists(_inputFileParameters.trackPath))
throw Exception {"File '" + _inputFileParameters.trackPath.string() + "' does not exist!"};
else if (!std::filesystem::is_regular_file( _inputFileParameters.trackPath) )
throw Exception {"File '" + _inputFileParameters.trackPath.string() + "' is not regular!"};
}
catch (const std::filesystem::filesystem_error& e)
{
throw Exception {"File error '" + _inputFileParameters.trackPath.string() + "': " + e.what()};
}
LOG(INFO) << "Transcoding file '" << _inputFileParameters.trackPath.string() << "'"; void Transcoder::start()
{
if (ffmpegPath.empty())
init();
std::vector<std::string> args; try
{
if (!std::filesystem::exists(_inputParameters.trackPath))
throw Exception{ "File '" + _inputParameters.trackPath.string() + "' does not exist!" };
else if (!std::filesystem::is_regular_file(_inputParameters.trackPath))
throw Exception{ "File '" + _inputParameters.trackPath.string() + "' is not regular!" };
}
catch (const std::filesystem::filesystem_error& e)
{
throw Exception{ "File error '" + _inputParameters.trackPath.string() + "': " + e.what() };
}
args.emplace_back(ffmpegPath.string()); LOG(INFO) << "Transcoding file '" << _inputParameters.trackPath.string() << "'";
// Make sure: std::vector<std::string> args;
// - 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 args.emplace_back(ffmpegPath.string());
{
args.emplace_back("-ss");
std::ostringstream oss; // Make sure:
oss << std::fixed << std::showpoint << std::setprecision(3) << (_transcodeParameters.offset.count() / float {1000}); // - we do not produce anything in the stderr output
args.emplace_back(oss.str()); // - 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 file // input Offset
args.emplace_back("-i"); {
args.emplace_back(_inputFileParameters.trackPath.string()); args.emplace_back("-ss");
// Stream mapping, if set std::ostringstream oss;
if (_transcodeParameters.stream) oss << std::fixed << std::showpoint << std::setprecision(3) << (_outputParameters.offset.count() / float{ 1000 });
{ args.emplace_back(oss.str());
args.emplace_back("-map"); }
args.emplace_back("0:" + std::to_string(*_transcodeParameters.stream));
}
if (_transcodeParameters.stripMetadata) // Input file
{ args.emplace_back("-i");
// Strip metadata args.emplace_back(_inputParameters.trackPath.string());
args.emplace_back("-map_metadata");
args.emplace_back("-1");
}
// Skip video flows (including covers) // Stream mapping, if set
args.emplace_back("-vn"); if (_outputParameters.stream)
{
args.emplace_back("-map");
args.emplace_back("0:" + std::to_string(*_outputParameters.stream));
}
// Output bitrates if (_outputParameters.stripMetadata)
args.emplace_back("-b:a"); {
args.emplace_back(std::to_string(_transcodeParameters.bitrate)); // Strip metadata
args.emplace_back("-map_metadata");
args.emplace_back("-1");
}
// Codecs and formats // Skip video flows (including covers)
switch (_transcodeParameters.format) args.emplace_back("-vn");
{
case Format::MP3:
args.emplace_back("-f");
args.emplace_back("mp3");
break;
case Format::OGG_OPUS: // Output bitrates
args.emplace_back("-acodec"); args.emplace_back("-b:a");
args.emplace_back("libopus"); args.emplace_back(std::to_string(_outputParameters.bitrate));
args.emplace_back("-f");
args.emplace_back("ogg");
break;
case Format::MATROSKA_OPUS: // Codecs and formats
args.emplace_back("-acodec"); switch (_outputParameters.format)
args.emplace_back("libopus"); {
args.emplace_back("-f"); case OutputFormat::MP3:
args.emplace_back("matroska"); args.emplace_back("-f");
break; args.emplace_back("mp3");
break;
case Format::OGG_VORBIS: case OutputFormat::OGG_OPUS:
args.emplace_back("-acodec"); args.emplace_back("-acodec");
args.emplace_back("libvorbis"); args.emplace_back("libopus");
args.emplace_back("-f"); args.emplace_back("-f");
args.emplace_back("ogg"); args.emplace_back("ogg");
break; break;
case Format::WEBM_VORBIS: case OutputFormat::MATROSKA_OPUS:
args.emplace_back("-acodec"); args.emplace_back("-acodec");
args.emplace_back("libvorbis"); args.emplace_back("libopus");
args.emplace_back("-f"); args.emplace_back("-f");
args.emplace_back("webm"); args.emplace_back("matroska");
break; break;
default: case OutputFormat::OGG_VORBIS:
throw Exception {"Unhandled format (" + std::to_string(static_cast<int>(_transcodeParameters.format)) + ")"}; args.emplace_back("-acodec");
} args.emplace_back("libvorbis");
args.emplace_back("-f");
args.emplace_back("ogg");
break;
_outputMimeType = formatToMimetype(_transcodeParameters.format); case OutputFormat::WEBM_VORBIS:
args.emplace_back("-acodec");
args.emplace_back("libvorbis");
args.emplace_back("-f");
args.emplace_back("webm");
break;
args.emplace_back("pipe:1"); default:
throw Exception{ "Unhandled format (" + std::to_string(static_cast<int>(_outputParameters.format)) + ")" };
}
LOG(DEBUG) << "Dumping args (" << args.size() << ")"; _outputMimeType = formatToMimetype(_outputParameters.format);
for (const std::string& arg : args)
LOG(DEBUG) << "Arg = '" << arg << "'";
// Caution: stdin must have been closed before args.emplace_back("pipe:1");
try
{
_childProcess = Service<IChildProcessManager>::get()->spawnChildProcess(ffmpegPath, args);
}
catch (ChildProcessException& exception)
{
throw Exception {"Cannot execute '" + ffmpegPath.string() + "': " + exception.what()};
}
}
void LOG(DEBUG) << "Dumping args (" << args.size() << ")";
Transcoder::asyncRead(std::byte* buffer, std::size_t bufferSize, ReadCallback readCallback) for (const std::string& arg : args)
{ LOG(DEBUG) << "Arg = '" << arg << "'";
assert(_childProcess);
return _childProcess->asyncRead(buffer, bufferSize, [readCallback {std::move(readCallback)}](IChildProcess::ReadResult /*res*/, std::size_t nbBytesRead) // Caution: stdin must have been closed before
{ try
readCallback(nbBytesRead); {
}); _childProcess = Service<IChildProcessManager>::get()->spawnChildProcess(ffmpegPath, args);
} }
catch (ChildProcessException& exception)
{
throw Exception{ "Cannot execute '" + ffmpegPath.string() + "': " + exception.what() };
}
}
std::size_t void Transcoder::asyncRead(std::byte* buffer, std::size_t bufferSize, ReadCallback readCallback)
Transcoder::readSome(std::byte* buffer, std::size_t bufferSize) {
{ assert(_childProcess);
assert(_childProcess);
return _childProcess->readSome(buffer, bufferSize); return _childProcess->asyncRead(buffer, bufferSize, [readCallback{ std::move(readCallback) }](IChildProcess::ReadResult /*res*/, std::size_t nbBytesRead)
} {
readCallback(nbBytesRead);
});
}
bool std::size_t Transcoder::readSome(std::byte* buffer, std::size_t bufferSize)
Transcoder::finished() const {
{ assert(_childProcess);
assert(_childProcess);
return _childProcess->finished(); return _childProcess->readSome(buffer, bufferSize);
} }
} // namespace Transcode bool Transcoder::finished() const
{
assert(_childProcess);
return _childProcess->finished();
}
} // namespace Av::Transcoding
+28 -30
View File
@@ -22,47 +22,45 @@
#include <filesystem> #include <filesystem>
#include <functional> #include <functional>
#include "av/TranscodeParameters.hpp" #include "av/TranscodingParameters.hpp"
#include "av/Types.hpp" #include "av/Types.hpp"
class IChildProcess; class IChildProcess;
namespace Av namespace Av::Transcoding
{ {
class Transcoder class Transcoder
{ {
public: public:
Transcoder(const InputFileParameters& inputFileParameters, const TranscodeParameters& transcodeParameters); Transcoder(const InputParameters& inputParameters, const OutputParameters& outputParameters);
~Transcoder(); ~Transcoder();
Transcoder(const Transcoder&) = delete; Transcoder(const Transcoder&) = delete;
Transcoder& operator=(const Transcoder&) = delete; Transcoder& operator=(const Transcoder&) = delete;
Transcoder(Transcoder&&) = delete; Transcoder(Transcoder&&) = delete;
Transcoder& operator=(Transcoder&&) = delete; Transcoder& operator=(Transcoder&&) = delete;
// non blocking calls // non blocking calls
using ReadCallback = std::function<void(std::size_t nbReadBytes)>; using ReadCallback = std::function<void(std::size_t nbReadBytes)>;
void asyncRead(std::byte* buffer, std::size_t bufferSize, ReadCallback); void asyncRead(std::byte* buffer, std::size_t bufferSize, ReadCallback);
std::size_t readSome(std::byte* buffer, std::size_t bufferSize); std::size_t readSome(std::byte* buffer, std::size_t bufferSize);
const std::string& getOutputMimeType() const { return _outputMimeType; } const std::string& getOutputMimeType() const { return _outputMimeType; }
const TranscodeParameters& getParameters() const { return _transcodeParameters; } const OutputParameters& getOutputParameters() const { return _outputParameters; }
bool finished() const; bool finished() const;
private: private:
static void init(); static void init();
void start(); void start();
const std::size_t _debugId {}; const std::size_t _debugId{};
const InputFileParameters _inputFileParameters; const InputParameters _inputParameters;
const TranscodeParameters _transcodeParameters; const OutputParameters _outputParameters;
std::string _outputMimeType;
std::unique_ptr<IChildProcess> _childProcess; std::unique_ptr<IChildProcess> _childProcess;
};
std::string _outputMimeType;
};
} // namespace Av
} // namespace Av::Transcoding
@@ -0,0 +1,103 @@
/*
* 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/>.
*/
#include "TranscodingResourceHandler.hpp"
#include "utils/Logger.hpp"
namespace Av::Transcoding
{
namespace
{
std::size_t doEstimateContentLength(const InputParameters& inputParameters, const OutputParameters& outputParameters)
{
const std::size_t estimatedContentLength{ outputParameters.bitrate / 8 * static_cast<std::size_t>(std::chrono::duration_cast<std::chrono::milliseconds>(inputParameters.duration).count()) / 1000 };
return estimatedContentLength;
}
}
std::unique_ptr<IResourceHandler> createResourceHandler(const InputParameters& inputParameters, const OutputParameters& outputParameters, bool estimateContentLength)
{
return std::make_unique<TranscodingResourceHandler>(inputParameters, outputParameters, estimateContentLength);
}
// TODO set some nice HTTP return code
TranscodingResourceHandler::TranscodingResourceHandler(const InputParameters& inputParameters, const OutputParameters& outputParameters, bool estimateContentLength)
: _estimatedContentLength{ estimateContentLength ? std::make_optional(doEstimateContentLength(inputParameters, outputParameters)) : std::nullopt }
, _transcoder{ inputParameters, outputParameters }
{
if (_estimatedContentLength)
LMS_LOG(TRANSCODE, DEBUG) << "Estimated content length = " << *_estimatedContentLength;
else
LMS_LOG(TRANSCODE, DEBUG) << "Not using estimated content length";
}
Wt::Http::ResponseContinuation* TranscodingResourceHandler::processRequest(const Wt::Http::Request& /*request*/, Wt::Http::Response& response)
{
if (_estimatedContentLength)
response.setContentLength(*_estimatedContentLength);
response.setMimeType(_transcoder.getOutputMimeType());
LMS_LOG(TRANSCODE, DEBUG) << "Transcoder finished = " << _transcoder.finished() << ", total served bytes = " << _totalServedByteCount << ", mime type = " << _transcoder.getOutputMimeType();
if (_bytesReadyCount > 0)
{
LMS_LOG(TRANSCODE, DEBUG) << "Writing " << _bytesReadyCount << " bytes back to client";
response.out().write(reinterpret_cast<const char*>(&_buffer[0]), _bytesReadyCount);
_totalServedByteCount += _bytesReadyCount;
_bytesReadyCount = 0;
}
if (!_transcoder.finished())
{
Wt::Http::ResponseContinuation* continuation{ response.createContinuation() };
continuation->waitForMoreData();
_transcoder.asyncRead(_buffer.data(), _buffer.size(), [=](std::size_t nbBytesRead)
{
LMS_LOG(TRANSCODE, DEBUG) << "Have " << nbBytesRead << " more bytes to send back";
assert(_bytesReadyCount == 0);
_bytesReadyCount = nbBytesRead;
continuation->haveMoreData();
});
return continuation;
}
else
{
// pad with 0 if necessary as duration may not be accurate
if (_estimatedContentLength && *_estimatedContentLength > _totalServedByteCount)
{
const std::size_t padSize{ *_estimatedContentLength - _totalServedByteCount };
LMS_LOG(TRANSCODE, DEBUG) << "Adding " << padSize << " padding bytes";
for (std::size_t i{}; i < padSize; ++i)
response.out().put(0);
_totalServedByteCount += padSize;
}
LMS_LOG(TRANSCODE, DEBUG) << "Transcoding finished. Total served byte count = " << _totalServedByteCount;
}
return {};
}
}
@@ -23,16 +23,16 @@
#include <filesystem> #include <filesystem>
#include <optional> #include <optional>
#include "av/TranscodeParameters.hpp" #include "av/TranscodingParameters.hpp"
#include "utils/IResourceHandler.hpp" #include "utils/IResourceHandler.hpp"
#include "Transcoder.hpp" #include "Transcoder.hpp"
namespace Av namespace Av::Transcoding
{ {
class TranscodeResourceHandler final : public IResourceHandler class TranscodingResourceHandler final : public IResourceHandler
{ {
public: public:
TranscodeResourceHandler(const InputFileParameters& inputFileParameters, const TranscodeParameters& parameters, bool estimateContentLength); TranscodingResourceHandler(const InputParameters& inputParameters, const OutputParameters& outputParameters, bool estimateContentLength);
private: private:
Wt::Http::ResponseContinuation* 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;
-41
View File
@@ -1,41 +0,0 @@
/*
* Copyright (C) 2019 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/>.
*/
#include "av/Types.hpp"
namespace Av
{
std::string_view
formatToMimetype(Format format)
{
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"};
}
}
+25 -1
View File
@@ -34,6 +34,29 @@
namespace Av namespace Av
{ {
// List should be sync with the codecs shipped in the lms's docker version
enum class DecodingCodec
{
UNKNOWN,
MP3,
AAC,
AC3,
VORBIS,
WMAV1,
WMAV2,
FLAC, // Flac
ALAC, // Apple Lossless Audio Codec (ALAC)
WAVPACK, // WavPack
MUSEPACK7, // Musepack
MUSEPACK8,
APE, // // Monkey's Audio
EAC3, // Enhanced AC-3
MP4ALS, // MPEG-4 Audio Lossless Coding
OPUS, // Opus
SHORTEN, // Shorten (shn)
// TODO add PCM codecs
};
struct Picture struct Picture
{ {
std::string mimeType; std::string mimeType;
@@ -52,7 +75,8 @@ namespace Av
{ {
size_t index{}; size_t index{};
std::size_t bitrate{}; std::size_t bitrate{};
std::string codec; DecodingCodec codec;
std::string codecName;
}; };
class IAudioFile class IAudioFile
@@ -25,21 +25,32 @@
#include "Types.hpp" #include "Types.hpp"
namespace Av namespace Av::Transcoding
{ {
struct InputFileParameters struct InputParameters
{ {
std::filesystem::path trackPath; std::filesystem::path trackPath;
std::chrono::milliseconds duration; std::chrono::milliseconds duration; // used to estimate content length
}; };
struct TranscodeParameters enum class OutputFormat
{ {
Format format; MP3,
std::size_t bitrate {128000}; OGG_OPUS,
std::optional<std::size_t> stream; // Id of the stream to be transcoded (auto detect by default) MATROSKA_OPUS,
std::chrono::milliseconds offset {0}; OGG_VORBIS,
bool stripMetadata {true}; WEBM_VORBIS,
}; };
} // namespace Av
std::string_view toMimetype(OutputFormat format);
struct OutputParameters
{
OutputFormat 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::Transcoding
@@ -23,11 +23,10 @@
#include "utils/IResourceHandler.hpp" #include "utils/IResourceHandler.hpp"
namespace Av namespace Av::Transcoding
{ {
struct InputFileParameters; struct InputParameters;
struct TranscodeParameters; struct OutputParameters;
std::unique_ptr<IResourceHandler> createTranscodeResourceHandler(const InputFileParameters& inputFileParameters, const TranscodeParameters& parameters, bool estimateContentLength); std::unique_ptr<IResourceHandler> createResourceHandler(const InputParameters& inputParameters, const OutputParameters& outputParameters, bool estimateContentLength);
} }
+7 -21
View File
@@ -19,27 +19,13 @@
#pragma once #pragma once
#include <string_view>
#include "utils/Exception.hpp" #include "utils/Exception.hpp"
namespace Av { namespace Av
{
class Exception : public LmsException class Exception : public LmsException
{ {
public: public:
using LmsException::LmsException; using LmsException::LmsException;
}; };
enum class Format
{
MP3,
OGG_OPUS,
MATROSKA_OPUS,
OGG_VORBIS,
WEBM_VORBIS,
};
std::string_view formatToMimetype(Format format);
} }
+2 -2
View File
@@ -80,10 +80,10 @@ namespace Database {
.resultValue(); .resultValue();
} }
void User::setSubsonicDefaultTranscodeBitrate(Bitrate bitrate) void User::setSubsonicDefaultTranscodingOutputBitrate(Bitrate bitrate)
{ {
assert(isAudioBitrateAllowed(bitrate)); assert(isAudioBitrateAllowed(bitrate));
_subsonicDefaultTranscodeBitrate = bitrate; _subsonicDefaultTranscodingOutputBitrate = bitrate;
} }
void User::clearAuthTokens() void User::clearAuthTokens()
@@ -161,8 +161,8 @@ namespace Database
Writer = 10, Writer = 10,
}; };
// User selectable audio file formats // User selectable transcoding output formats
enum class AudioFormat enum class TranscodingOutputFormat
{ {
MP3 = 1, MP3 = 1,
OGG_OPUS = 2, OGG_OPUS = 2,
@@ -58,8 +58,8 @@ namespace Database {
static inline constexpr std::size_t MinNameLength{ 3 }; static inline constexpr std::size_t MinNameLength{ 3 };
static inline constexpr std::size_t MaxNameLength{ 15 }; static inline constexpr std::size_t MaxNameLength{ 15 };
static inline constexpr AudioFormat defaultSubsonicTranscodeFormat{ AudioFormat::OGG_OPUS }; static inline constexpr TranscodingOutputFormat defaultSubsonicTranscodingOutputFormat{ TranscodingOutputFormat::OGG_OPUS };
static inline constexpr Bitrate defaultSubsonicTranscodeBitrate{ 128000 }; static inline constexpr Bitrate defaultSubsonicTranscodingOutputBitrate{ 128000 };
static inline constexpr UITheme defaultUITheme{ UITheme::Dark }; static inline constexpr UITheme defaultUITheme{ UITheme::Dark };
static inline constexpr SubsonicArtistListMode defaultSubsonicArtistListMode{ SubsonicArtistListMode::AllArtists }; static inline constexpr SubsonicArtistListMode defaultSubsonicArtistListMode{ SubsonicArtistListMode::AllArtists };
static inline constexpr ScrobblingBackend defaultScrobblingBackend{ ScrobblingBackend::Internal }; static inline constexpr ScrobblingBackend defaultScrobblingBackend{ ScrobblingBackend::Internal };
@@ -83,8 +83,8 @@ namespace Database {
void setLastLogin(const Wt::WDateTime& dateTime) { _lastLogin = dateTime; } void setLastLogin(const Wt::WDateTime& dateTime) { _lastLogin = dateTime; }
void setPasswordHash(const PasswordHash& passwordHash) { _passwordSalt = passwordHash.salt; _passwordHash = passwordHash.hash; } void setPasswordHash(const PasswordHash& passwordHash) { _passwordSalt = passwordHash.salt; _passwordHash = passwordHash.hash; }
void setType(UserType type) { _type = type; } void setType(UserType type) { _type = type; }
void setSubsonicDefaultTranscodeFormat(AudioFormat encoding) { _subsonicDefaultTranscodeFormat = encoding; } void setSubsonicDefaultTranscodintOutputFormat(TranscodingOutputFormat encoding) { _subsonicDefaultTranscodingOutputFormat = encoding; }
void setSubsonicDefaultTranscodeBitrate(Bitrate bitrate); void setSubsonicDefaultTranscodingOutputBitrate(Bitrate bitrate);
void setCurPlayingTrackPos(std::size_t pos) { _curPlayingTrackPos = pos; } void setCurPlayingTrackPos(std::size_t pos) { _curPlayingTrackPos = pos; }
void setRadio(bool val) { _radio = val; } void setRadio(bool val) { _radio = val; }
void setRepeatAll(bool val) { _repeatAll = val; } void setRepeatAll(bool val) { _repeatAll = val; }
@@ -99,8 +99,8 @@ namespace Database {
bool isAdmin() const { return _type == UserType::ADMIN; } bool isAdmin() const { return _type == UserType::ADMIN; }
bool isDemo() const { return _type == UserType::DEMO; } bool isDemo() const { return _type == UserType::DEMO; }
UserType getType() const { return _type; } UserType getType() const { return _type; }
AudioFormat getSubsonicDefaultTranscodeFormat() const { return _subsonicDefaultTranscodeFormat; } TranscodingOutputFormat getSubsonicDefaultTranscodingOutputFormat() const { return _subsonicDefaultTranscodingOutputFormat; }
Bitrate getSubsonicDefaultTranscodeBitrate() const { return _subsonicDefaultTranscodeBitrate; } Bitrate getSubsonicDefaultTranscodingOutputBitrate() const { return _subsonicDefaultTranscodingOutputBitrate; }
std::size_t getCurPlayingTrackPos() const { return _curPlayingTrackPos; } std::size_t getCurPlayingTrackPos() const { return _curPlayingTrackPos; }
bool isRepeatAllSet() const { return _repeatAll; } bool isRepeatAllSet() const { return _repeatAll; }
bool isRadioSet() const { return _radio; } bool isRadioSet() const { return _radio; }
@@ -118,8 +118,8 @@ namespace Database {
Wt::Dbo::field(a, _passwordSalt, "password_salt"); Wt::Dbo::field(a, _passwordSalt, "password_salt");
Wt::Dbo::field(a, _passwordHash, "password_hash"); Wt::Dbo::field(a, _passwordHash, "password_hash");
Wt::Dbo::field(a, _lastLogin, "last_login"); Wt::Dbo::field(a, _lastLogin, "last_login");
Wt::Dbo::field(a, _subsonicDefaultTranscodeFormat, "subsonic_default_transcode_format"); Wt::Dbo::field(a, _subsonicDefaultTranscodingOutputFormat, "subsonic_default_transcode_format");
Wt::Dbo::field(a, _subsonicDefaultTranscodeBitrate, "subsonic_default_transcode_bitrate"); Wt::Dbo::field(a, _subsonicDefaultTranscodingOutputBitrate, "subsonic_default_transcode_bitrate");
Wt::Dbo::field(a, _subsonicArtistListMode, "subsonic_artist_list_mode"); Wt::Dbo::field(a, _subsonicArtistListMode, "subsonic_artist_list_mode");
Wt::Dbo::field(a, _uiTheme, "ui_theme"); Wt::Dbo::field(a, _uiTheme, "ui_theme");
Wt::Dbo::field(a, _feedbackBackend, "feedback_backend"); Wt::Dbo::field(a, _feedbackBackend, "feedback_backend");
@@ -153,8 +153,8 @@ namespace Database {
// User defined settings // User defined settings
SubsonicArtistListMode _subsonicArtistListMode{ defaultSubsonicArtistListMode }; SubsonicArtistListMode _subsonicArtistListMode{ defaultSubsonicArtistListMode };
AudioFormat _subsonicDefaultTranscodeFormat{ defaultSubsonicTranscodeFormat }; TranscodingOutputFormat _subsonicDefaultTranscodingOutputFormat{ defaultSubsonicTranscodingOutputFormat };
int _subsonicDefaultTranscodeBitrate{ defaultSubsonicTranscodeBitrate }; int _subsonicDefaultTranscodingOutputBitrate{ defaultSubsonicTranscodingOutputBitrate };
// User's dynamic data (UI) // User's dynamic data (UI)
int _curPlayingTrackPos{}; // Current track position in queue int _curPlayingTrackPos{}; // Current track position in queue
@@ -21,8 +21,8 @@
#include "av/IAudioFile.hpp" #include "av/IAudioFile.hpp"
#include "av/RawResourceHandlerCreator.hpp" #include "av/RawResourceHandlerCreator.hpp"
#include "av/TranscodeParameters.hpp" #include "av/TranscodingParameters.hpp"
#include "av/TranscodeResourceHandlerCreator.hpp" #include "av/TranscodingResourceHandlerCreator.hpp"
#include "av/Types.hpp" #include "av/Types.hpp"
#include "services/cover/ICoverService.hpp" #include "services/cover/ICoverService.hpp"
#include "services/database/Session.hpp" #include "services/database/Session.hpp"
@@ -41,37 +41,37 @@ namespace API::Subsonic
using namespace Database; using namespace Database;
namespace { namespace {
std::optional<Av::Format> subsonicStreamFormatToAvFormat(std::string_view format) std::optional<Av::Transcoding::OutputFormat> subsonicStreamFormatToAvFormat(std::string_view format)
{ {
for (const auto& [str, avFormat] : std::initializer_list<std::pair<std::string_view, Av::Format>>{ for (const auto& [str, avFormat] : std::initializer_list<std::pair<std::string_view, Av::Transcoding::OutputFormat>>{
{"mp3", Av::Format::MP3}, {"mp3", Av::Transcoding::OutputFormat::MP3},
{"opus", Av::Format::OGG_OPUS}, {"opus", Av::Transcoding::OutputFormat::OGG_OPUS},
{"vorbis", Av::Format::OGG_VORBIS}, {"vorbis", Av::Transcoding::OutputFormat::OGG_VORBIS},
}) })
{ {
if (StringUtils::stringCaseInsensitiveEqual("str", format)) if (StringUtils::stringCaseInsensitiveEqual(str, format))
return avFormat; return avFormat;
} }
return std::nullopt; return std::nullopt;
} }
Av::Format userTranscodeFormatToAvFormat(AudioFormat format) Av::Transcoding::OutputFormat userTranscodeFormatToAvFormat(Database::TranscodingOutputFormat format)
{ {
switch (format) switch (format)
{ {
case Database::AudioFormat::MP3: return Av::Format::MP3; case Database::TranscodingOutputFormat::MP3: return Av::Transcoding::OutputFormat::MP3;
case Database::AudioFormat::OGG_OPUS: return Av::Format::OGG_OPUS; case Database::TranscodingOutputFormat::OGG_OPUS: return Av::Transcoding::OutputFormat::OGG_OPUS;
case Database::AudioFormat::MATROSKA_OPUS: return Av::Format::MATROSKA_OPUS; case Database::TranscodingOutputFormat::MATROSKA_OPUS: return Av::Transcoding::OutputFormat::MATROSKA_OPUS;
case Database::AudioFormat::OGG_VORBIS: return Av::Format::OGG_VORBIS; case Database::TranscodingOutputFormat::OGG_VORBIS: return Av::Transcoding::OutputFormat::OGG_VORBIS;
case Database::AudioFormat::WEBM_VORBIS: return Av::Format::WEBM_VORBIS; case Database::TranscodingOutputFormat::WEBM_VORBIS: return Av::Transcoding::OutputFormat::WEBM_VORBIS;
} }
return Av::Format::OGG_OPUS; return Av::Transcoding::OutputFormat::OGG_OPUS;
} }
struct StreamParameters struct StreamParameters
{ {
Av::InputFileParameters inputFileParameters; Av::Transcoding::InputParameters inputParameters;
std::optional<Av::TranscodeParameters> transcodeParameters; std::optional<Av::Transcoding::OutputParameters> outputParameters;
bool estimateContentLength{}; bool estimateContentLength{};
}; };
@@ -98,22 +98,22 @@ namespace API::Subsonic
if (!track) if (!track)
throw RequestedDataNotFoundError{}; throw RequestedDataNotFoundError{};
parameters.inputFileParameters.trackPath = track->getPath(); parameters.inputParameters.trackPath = track->getPath();
parameters.inputFileParameters.duration = track->getDuration(); parameters.inputParameters.duration = track->getDuration();
bitrate = track->getBitrate() / 1000; bitrate = track->getBitrate() / 1000;
} }
if (format == "raw") // raw => no transcode if (format == "raw") // raw => no transcode
return parameters; return parameters;
const auto audioFile{ Av::parseAudioFile(parameters.inputFileParameters.trackPath) }; const auto audioFile{ Av::parseAudioFile(parameters.inputParameters.trackPath) };
// check if transcode is really needed or not // check if transcode is really needed or not
// same format as requested, bitrate is lower than requested => no need to transcode // same format as requested, bitrate is lower than requested => no need to transcode
if (const auto streamInfo{ audioFile->getBestStreamInfo() }) if (const auto streamInfo{ audioFile->getBestStreamInfo() })
{ {
// assume reported codec is "mp3", "opus", "vorbis", etc. // assume reported codec is "mp3", "opus", "vorbis", etc.
if (StringUtils::stringCaseInsensitiveEqual(streamInfo->codec, format) && (maxBitRate == 0 || (bitrate != 0 && bitrate <= maxBitRate))) if (StringUtils::stringCaseInsensitiveEqual(streamInfo->codecName, format) && (maxBitRate == 0 || (bitrate != 0 && bitrate <= maxBitRate)))
{ {
LMS_LOG(API_SUBSONIC, DEBUG) << "stream parameters are compatible with actual file: no transcode"; LMS_LOG(API_SUBSONIC, DEBUG) << "stream parameters are compatible with actual file: no transcode";
return parameters; return parameters;
@@ -126,19 +126,19 @@ namespace API::Subsonic
if (!user) if (!user)
throw UserNotAuthorizedError{}; throw UserNotAuthorizedError{};
Av::TranscodeParameters& transcodeParameters{ parameters.transcodeParameters.emplace() }; Av::Transcoding::OutputParameters& outputParameters{ parameters.outputParameters.emplace() };
transcodeParameters.stripMetadata = false; // We want clients to use metadata (offline use, replay gain, etc.) outputParameters.stripMetadata = false; // We want clients to use metadata (offline use, replay gain, etc.)
transcodeParameters.offset = std::chrono::seconds{ timeOffset }; outputParameters.offset = std::chrono::seconds{ timeOffset };
if (std::optional<Av::Format> requestedFormat{ subsonicStreamFormatToAvFormat(format) }) if (std::optional<Av::Transcoding::OutputFormat> requestedFormat{ subsonicStreamFormatToAvFormat(format) })
transcodeParameters.format = *requestedFormat; outputParameters.format = *requestedFormat;
else else
transcodeParameters.format = userTranscodeFormatToAvFormat(user->getSubsonicDefaultTranscodeFormat()); outputParameters.format = userTranscodeFormatToAvFormat(user->getSubsonicDefaultTranscodingOutputFormat());
transcodeParameters.bitrate = user->getSubsonicDefaultTranscodeBitrate(); outputParameters.bitrate = user->getSubsonicDefaultTranscodingOutputBitrate();
if (maxBitRate != 0) if (maxBitRate != 0)
transcodeParameters.bitrate = Utils::clamp(transcodeParameters.bitrate, std::size_t{ 48000 }, maxBitRate * 1000); outputParameters.bitrate = Utils::clamp(outputParameters.bitrate, std::size_t{ 48000 }, maxBitRate * 1000);
return parameters; return parameters;
} }
@@ -187,10 +187,10 @@ namespace API::Subsonic
if (!continuation) if (!continuation)
{ {
StreamParameters streamParameters{ getStreamParameters(context) }; StreamParameters streamParameters{ getStreamParameters(context) };
if (streamParameters.transcodeParameters) if (streamParameters.outputParameters)
resourceHandler = Av::createTranscodeResourceHandler(streamParameters.inputFileParameters, *streamParameters.transcodeParameters, streamParameters.estimateContentLength); resourceHandler = Av::Transcoding::createResourceHandler(streamParameters.inputParameters, *streamParameters.outputParameters, streamParameters.estimateContentLength);
else else
resourceHandler = Av::createRawResourceHandler(streamParameters.inputFileParameters.trackPath); resourceHandler = Av::createRawResourceHandler(streamParameters.inputParameters.trackPath);
} }
else else
{ {
+7 -7
View File
@@ -46,15 +46,15 @@ namespace API::Subsonic
namespace namespace
{ {
std::string_view formatToSuffix(AudioFormat format) std::string_view formatToSuffix(TranscodingOutputFormat format)
{ {
switch (format) switch (format)
{ {
case AudioFormat::MP3: return "mp3"; case TranscodingOutputFormat::MP3: return "mp3";
case AudioFormat::OGG_OPUS: return "opus"; case TranscodingOutputFormat::OGG_OPUS: return "opus";
case AudioFormat::MATROSKA_OPUS: return "mka"; case TranscodingOutputFormat::MATROSKA_OPUS: return "mka";
case AudioFormat::OGG_VORBIS: return "ogg"; case TranscodingOutputFormat::OGG_VORBIS: return "ogg";
case AudioFormat::WEBM_VORBIS: return "webm"; case TranscodingOutputFormat::WEBM_VORBIS: return "webm";
} }
return ""; return "";
@@ -125,7 +125,7 @@ namespace API::Subsonic
} }
{ {
const std::string fileSuffix{ formatToSuffix(user->getSubsonicDefaultTranscodeFormat()) }; const std::string fileSuffix{ formatToSuffix(user->getSubsonicDefaultTranscodingOutputFormat()) };
trackResponse.setAttribute("transcodedSuffix", fileSuffix); trackResponse.setAttribute("transcodedSuffix", fileSuffix);
trackResponse.setAttribute("transcodedContentType", Av::getMimeType(std::filesystem::path{ "." + fileSuffix })); trackResponse.setAttribute("transcodedContentType", Av::getMimeType(std::filesystem::path{ "." + fileSuffix }));
} }
+81 -81
View File
@@ -19,118 +19,118 @@
#pragma once #pragma once
#include <chrono>
#include <optional> #include <optional>
#include <Wt/WAnchor.h> #include <Wt/WAnchor.h>
#include <Wt/WJavaScript.h> #include <Wt/WJavaScript.h>
#include <Wt/WPushButton.h>
#include <Wt/WTemplate.h> #include <Wt/WTemplate.h>
#include <Wt/WText.h> #include <Wt/WText.h>
#include "services/database/TrackId.hpp" #include "services/database/TrackId.hpp"
#include "services/database/Types.hpp" #include "services/database/Types.hpp"
namespace UserInterface { namespace UserInterface
class AudioFileResource;
class AudioTranscodeResource;
class MediaPlayer : public Wt::WTemplate
{ {
public: class AudioFileResource;
using Bitrate = Database::Bitrate; class AudioTranscodeResource;
using Format = Database::AudioFormat;
using Gain = float;
// Do not change enum values as they may be stored locally in browser class MediaPlayer : public Wt::WTemplate
// Keep it sync with LMS.mediaplayer js {
public:
using Bitrate = Database::Bitrate;
using Format = Database::TranscodingOutputFormat;
using Gain = float;
struct Settings // Do not change enum values as they may be stored locally in browser
{ // Keep it sync with LMS.mediaplayer js
struct Transcode
{
enum class Mode
{
Never = 0,
Always = 1,
IfFormatNotSupported = 2,
};
static inline constexpr Mode defaultMode {Mode::IfFormatNotSupported};
static inline constexpr Format defaultFormat {Format::OGG_OPUS};
static inline constexpr Bitrate defaultBitrate {128000};
Mode mode {defaultMode}; struct Settings
Format format {defaultFormat}; {
Bitrate bitrate {defaultBitrate}; struct Transcode
}; {
enum class Mode
{
Never = 0,
Always = 1,
IfFormatNotSupported = 2,
};
static inline constexpr Mode defaultMode{ Mode::IfFormatNotSupported };
static inline constexpr Format defaultFormat{ Format::OGG_OPUS };
static inline constexpr Bitrate defaultBitrate{ 128000 };
struct ReplayGain Mode mode{ defaultMode };
{ Format format{ defaultFormat };
enum class Mode Bitrate bitrate{ defaultBitrate };
{ };
None = 0,
Auto = 1,
Track = 2,
Release = 3,
};
static inline constexpr Mode defaultMode {Mode::None}; struct ReplayGain
static inline constexpr Gain defaultPreAmpGain {}; {
static inline constexpr Gain minPreAmpGain {-15}; enum class Mode
static inline constexpr Gain maxPreAmpGain {15}; {
None = 0,
Auto = 1,
Track = 2,
Release = 3,
};
Mode mode {defaultMode}; static inline constexpr Mode defaultMode{ Mode::None };
Gain preAmpGain {defaultPreAmpGain}; static inline constexpr Gain defaultPreAmpGain{};
Gain preAmpGainIfNoInfo {defaultPreAmpGain}; static inline constexpr Gain minPreAmpGain{ -15 };
}; static inline constexpr Gain maxPreAmpGain{ 15 };
Transcode transcode; Mode mode{ defaultMode };
ReplayGain replayGain; Gain preAmpGain{ defaultPreAmpGain };
}; Gain preAmpGainIfNoInfo{ defaultPreAmpGain };
};
MediaPlayer(); Transcode transcode;
ReplayGain replayGain;
};
MediaPlayer(const MediaPlayer&) = delete; MediaPlayer();
MediaPlayer(MediaPlayer&&) = delete;
MediaPlayer& operator=(const MediaPlayer&) = delete;
MediaPlayer& operator=(MediaPlayer&&) = delete;
std::optional<Database::TrackId> getTrackLoaded() const { return _trackIdLoaded; } MediaPlayer(const MediaPlayer&) = delete;
MediaPlayer(MediaPlayer&&) = delete;
MediaPlayer& operator=(const MediaPlayer&) = delete;
MediaPlayer& operator=(MediaPlayer&&) = delete;
void loadTrack(Database::TrackId trackId, bool play, float replayGain); std::optional<Database::TrackId> getTrackLoaded() const { return _trackIdLoaded; }
void stop();
std::optional<Settings> getSettings() const { return _settings; } void loadTrack(Database::TrackId trackId, bool play, float replayGain);
void setSettings(const Settings& settings); void stop();
void onPlayQueueUpdated(std::size_t trackCount); std::optional<Settings> getSettings() const { return _settings; }
void setSettings(const Settings& settings);
// Signals void onPlayQueueUpdated(std::size_t trackCount);
Wt::JSignal<> playPrevious;
Wt::JSignal<> playNext;
Wt::Signal<Database::TrackId> trackLoaded;
Wt::Signal<> settingsLoaded;
Wt::JSignal<Database::TrackId::ValueType> scrobbleListenNow; // Signals
Wt::JSignal<Database::TrackId::ValueType, unsigned /* ms */> scrobbleListenFinished; Wt::JSignal<> playPrevious;
Wt::JSignal<> playNext;
Wt::Signal<Database::TrackId> trackLoaded;
Wt::Signal<> settingsLoaded;
Wt::JSignal<> playbackEnded; Wt::JSignal<Database::TrackId::ValueType> scrobbleListenNow;
Wt::JSignal<Database::TrackId::ValueType, unsigned /* ms */> scrobbleListenFinished;
private: Wt::JSignal<> playbackEnded;
std::unique_ptr<AudioFileResource> _audioFileResource;
std::unique_ptr<AudioTranscodeResource> _audioTranscodeResource;
std::optional<Database::TrackId> _trackIdLoaded; private:
std::optional<Settings> _settings; std::unique_ptr<AudioFileResource> _audioFileResource;
std::unique_ptr<AudioTranscodeResource> _audioTranscodeResource;
Wt::JSignal<std::string> _settingsLoaded; std::optional<Database::TrackId> _trackIdLoaded;
std::optional<Settings> _settings;
Wt::WText* _title {}; Wt::JSignal<std::string> _settingsLoaded;
Wt::WAnchor* _release {};
Wt::WText* _separator {}; Wt::WText* _title{};
Wt::WAnchor* _artist {}; Wt::WAnchor* _release{};
Wt::WPushButton* _playQueue {}; Wt::WText* _separator{};
}; Wt::WAnchor* _artist{};
Wt::WPushButton* _playQueue{};
};
} // namespace UserInterface } // namespace UserInterface
+60 -60
View File
@@ -59,8 +59,8 @@ namespace UserInterface {
static inline const Field ReplayGainPreAmpGainField{ "replaygain-preamp" }; static inline const Field ReplayGainPreAmpGainField{ "replaygain-preamp" };
static inline const Field ReplayGainPreAmpGainIfNoInfoField{ "replaygain-preamp-no-rg-info" }; static inline const Field ReplayGainPreAmpGainIfNoInfoField{ "replaygain-preamp-no-rg-info" };
static inline const Field SubsonicArtistListModeField{ "subsonic-artist-list-mode" }; static inline const Field SubsonicArtistListModeField{ "subsonic-artist-list-mode" };
static inline const Field SubsonicTranscodeFormatField{ "subsonic-transcode-format" }; static inline const Field SubsonicTranscodingOutputFormatField{ "subsonic-transcode-format" };
static inline const Field SubsonicTranscodeBitrateField{ "subsonic-transcode-bitrate" }; static inline const Field SubsonicTranscodingOutputBitrateField{ "subsonic-transcode-bitrate" };
static inline const Field FeedbackBackendField{ "feedback-backend" }; static inline const Field FeedbackBackendField{ "feedback-backend" };
static inline const Field ScrobblingBackendField{ "scrobbling-backend" }; static inline const Field ScrobblingBackendField{ "scrobbling-backend" };
static inline const Field ListenBrainzTokenField{ "listenbrainz-token" }; static inline const Field ListenBrainzTokenField{ "listenbrainz-token" };
@@ -68,7 +68,7 @@ namespace UserInterface {
static inline const Field PasswordField{ "password" }; static inline const Field PasswordField{ "password" };
static inline const Field PasswordConfirmField{ "password-confirm" }; static inline const Field PasswordConfirmField{ "password-confirm" };
using TranscodeModeModel = ValueStringModel<MediaPlayer::Settings::Transcode::Mode>; using TranscodingModeModel = ValueStringModel<MediaPlayer::Settings::Transcode::Mode>;
using ReplayGainModeModel = ValueStringModel<MediaPlayer::Settings::ReplayGain::Mode>; using ReplayGainModeModel = ValueStringModel<MediaPlayer::Settings::ReplayGain::Mode>;
using FeedbackBackendModel = ValueStringModel<FeedbackBackend>; using FeedbackBackendModel = ValueStringModel<FeedbackBackend>;
using ScrobblingBackendModel = ValueStringModel<ScrobblingBackend>; using ScrobblingBackendModel = ValueStringModel<ScrobblingBackend>;
@@ -85,8 +85,8 @@ namespace UserInterface {
addField(ReplayGainModeField); addField(ReplayGainModeField);
addField(ReplayGainPreAmpGainField); addField(ReplayGainPreAmpGainField);
addField(ReplayGainPreAmpGainIfNoInfoField); addField(ReplayGainPreAmpGainIfNoInfoField);
addField(SubsonicTranscodeBitrateField); addField(SubsonicTranscodingOutputBitrateField);
addField(SubsonicTranscodeFormatField); addField(SubsonicTranscodingOutputFormatField);
addField(FeedbackBackendField); addField(FeedbackBackendField);
addField(ScrobblingBackendField); addField(ScrobblingBackendField);
addField(ListenBrainzTokenField); addField(ListenBrainzTokenField);
@@ -116,15 +116,15 @@ namespace UserInterface {
setValidator(ReplayGainPreAmpGainField, createPreAmpValidator()); setValidator(ReplayGainPreAmpGainField, createPreAmpValidator());
setValidator(ReplayGainPreAmpGainIfNoInfoField, createPreAmpValidator()); setValidator(ReplayGainPreAmpGainIfNoInfoField, createPreAmpValidator());
setValidator(SubsonicTranscodeBitrateField, createMandatoryValidator()); setValidator(SubsonicTranscodingOutputBitrateField, createMandatoryValidator());
setValidator(SubsonicTranscodeFormatField, createMandatoryValidator()); setValidator(SubsonicTranscodingOutputFormatField, createMandatoryValidator());
loadData(); loadData();
} }
std::shared_ptr<TranscodeModeModel> getTranscodeModeModel() { return _transcodeModeModel; } std::shared_ptr<TranscodingModeModel> getTranscodingModeModel() { return _transcodingModeModeModel; }
std::shared_ptr<Wt::WAbstractItemModel> getTranscodeBitrateModel() { return _transcodeBitrateModel; } std::shared_ptr<Wt::WAbstractItemModel> getTranscodingOutputBitrateModel() { return _transcodingOutputBitrateModel; }
std::shared_ptr<Wt::WAbstractItemModel> getTranscodeFormatModel() { return _transcodeFormatModel; } std::shared_ptr<Wt::WAbstractItemModel> getTranscodeFormatModel() { return _transcodingOutputFormatModel; }
std::shared_ptr<ReplayGainModeModel> getReplayGainModeModel() { return _replayGainModeModel; } std::shared_ptr<ReplayGainModeModel> getReplayGainModeModel() { return _replayGainModeModel; }
std::shared_ptr<Wt::WAbstractItemModel> getSubsonicArtistListModeModel() { return _subsonicArtistListModeModel; } std::shared_ptr<Wt::WAbstractItemModel> getSubsonicArtistListModeModel() { return _subsonicArtistListModeModel; }
std::shared_ptr<FeedbackBackendModel> getFeedbackBackendModel() { return _feedbackBackendModel; } std::shared_ptr<FeedbackBackendModel> getFeedbackBackendModel() { return _feedbackBackendModel; }
@@ -139,17 +139,17 @@ namespace UserInterface {
{ {
MediaPlayer::Settings settings; MediaPlayer::Settings settings;
auto transcodeModeRow{ _transcodeModeModel->getRowFromString(valueText(TranscodeModeField)) }; auto transcodeModeRow{ _transcodingModeModeModel->getRowFromString(valueText(TranscodeModeField)) };
if (transcodeModeRow) if (transcodeModeRow)
settings.transcode.mode = _transcodeModeModel->getValue(*transcodeModeRow); settings.transcode.mode = _transcodingModeModeModel->getValue(*transcodeModeRow);
auto transcodeFormatRow{ _transcodeFormatModel->getRowFromString(valueText(TranscodeFormatField)) }; auto transcodeFormatRow{ _transcodingOutputFormatModel->getRowFromString(valueText(TranscodeFormatField)) };
if (transcodeFormatRow) if (transcodeFormatRow)
settings.transcode.format = _transcodeFormatModel->getValue(*transcodeFormatRow); settings.transcode.format = _transcodingOutputFormatModel->getValue(*transcodeFormatRow);
auto transcodeBitrateRow{ _transcodeBitrateModel->getRowFromString(valueText(TranscodeBitrateField)) }; auto transcodeBitrateRow{ _transcodingOutputBitrateModel->getRowFromString(valueText(TranscodeBitrateField)) };
if (transcodeBitrateRow) if (transcodeBitrateRow)
settings.transcode.bitrate = _transcodeBitrateModel->getValue(*transcodeBitrateRow); settings.transcode.bitrate = _transcodingOutputBitrateModel->getValue(*transcodeBitrateRow);
auto replayGainModeRow{ _replayGainModeModel->getRowFromString(valueText(ReplayGainModeField)) }; auto replayGainModeRow{ _replayGainModeModel->getRowFromString(valueText(ReplayGainModeField)) };
if (replayGainModeRow) if (replayGainModeRow)
@@ -162,13 +162,13 @@ namespace UserInterface {
} }
{ {
auto subsonicTranscodeBitrateRow{ _transcodeBitrateModel->getRowFromString(valueText(SubsonicTranscodeBitrateField)) }; auto subsonicTranscodingOutputBitrateRow{ _transcodingOutputBitrateModel->getRowFromString(valueText(SubsonicTranscodingOutputBitrateField)) };
if (subsonicTranscodeBitrateRow) if (subsonicTranscodingOutputBitrateRow)
user.modify()->setSubsonicDefaultTranscodeBitrate(_transcodeBitrateModel->getValue(*subsonicTranscodeBitrateRow)); user.modify()->setSubsonicDefaultTranscodingOutputBitrate(_transcodingOutputBitrateModel->getValue(*subsonicTranscodingOutputBitrateRow));
auto subsonicTranscodeFormatRow{ _transcodeFormatModel->getRowFromString(valueText(SubsonicTranscodeFormatField)) }; auto subsonicTranscodingOutputFormatRow{ _transcodingOutputFormatModel->getRowFromString(valueText(SubsonicTranscodingOutputFormatField)) };
if (subsonicTranscodeFormatRow) if (subsonicTranscodingOutputFormatRow)
user.modify()->setSubsonicDefaultTranscodeFormat(_transcodeFormatModel->getValue(*subsonicTranscodeFormatRow)); user.modify()->setSubsonicDefaultTranscodintOutputFormat(_transcodingOutputFormatModel->getValue(*subsonicTranscodingOutputFormatRow));
auto subsonicArtistListModeRow{ _subsonicArtistListModeModel->getRowFromString(valueText(SubsonicArtistListModeField)) }; auto subsonicArtistListModeRow{ _subsonicArtistListModeModel->getRowFromString(valueText(SubsonicArtistListModeField)) };
if (subsonicArtistListModeRow) if (subsonicArtistListModeRow)
@@ -205,17 +205,17 @@ namespace UserInterface {
{ {
const auto settings{ *LmsApp->getMediaPlayer().getSettings() }; const auto settings{ *LmsApp->getMediaPlayer().getSettings() };
auto transcodeModeRow{ _transcodeModeModel->getRowFromValue(settings.transcode.mode) }; auto transcodeModeRow{ _transcodingModeModeModel->getRowFromValue(settings.transcode.mode) };
if (transcodeModeRow) if (transcodeModeRow)
setValue(TranscodeModeField, _transcodeModeModel->getString(*transcodeModeRow)); setValue(TranscodeModeField, _transcodingModeModeModel->getString(*transcodeModeRow));
auto transcodeFormatRow{ _transcodeFormatModel->getRowFromValue(settings.transcode.format) }; auto transcodeFormatRow{ _transcodingOutputFormatModel->getRowFromValue(settings.transcode.format) };
if (transcodeFormatRow) if (transcodeFormatRow)
setValue(TranscodeFormatField, _transcodeFormatModel->getString(*transcodeFormatRow)); setValue(TranscodeFormatField, _transcodingOutputFormatModel->getString(*transcodeFormatRow));
auto transcodeBitrateRow{ _transcodeBitrateModel->getRowFromValue(settings.transcode.bitrate) }; auto transcodeBitrateRow{ _transcodingOutputBitrateModel->getRowFromValue(settings.transcode.bitrate) };
if (transcodeBitrateRow) if (transcodeBitrateRow)
setValue(TranscodeBitrateField, _transcodeBitrateModel->getString(*transcodeBitrateRow)); setValue(TranscodeBitrateField, _transcodingOutputBitrateModel->getString(*transcodeBitrateRow));
{ {
const bool usesTranscode{ settings.transcode.mode != MediaPlayer::Settings::Transcode::Mode::Never }; const bool usesTranscode{ settings.transcode.mode != MediaPlayer::Settings::Transcode::Mode::Never };
@@ -232,13 +232,13 @@ namespace UserInterface {
} }
{ {
auto subsonicTranscodeBitrateRow{ _transcodeBitrateModel->getRowFromValue(user->getSubsonicDefaultTranscodeBitrate()) }; auto subsonicTranscodingOutputBitrateRow{ _transcodingOutputBitrateModel->getRowFromValue(user->getSubsonicDefaultTranscodingOutputBitrate()) };
if (subsonicTranscodeBitrateRow) if (subsonicTranscodingOutputBitrateRow)
setValue(SubsonicTranscodeBitrateField, _transcodeBitrateModel->getString(*subsonicTranscodeBitrateRow)); setValue(SubsonicTranscodingOutputBitrateField, _transcodingOutputBitrateModel->getString(*subsonicTranscodingOutputBitrateRow));
auto subsonicTranscodeFormatRow{ _transcodeFormatModel->getRowFromValue(user->getSubsonicDefaultTranscodeFormat()) }; auto subsonicTranscodingOutputFormatRow{ _transcodingOutputFormatModel->getRowFromValue(user->getSubsonicDefaultTranscodingOutputFormat()) };
if (subsonicTranscodeFormatRow) if (subsonicTranscodingOutputFormatRow)
setValue(SubsonicTranscodeFormatField, _transcodeFormatModel->getString(*subsonicTranscodeFormatRow)); setValue(SubsonicTranscodingOutputFormatField, _transcodingOutputFormatModel->getString(*subsonicTranscodingOutputFormatRow));
auto subsonicArtistListModeRow{ _subsonicArtistListModeModel->getRowFromValue(user->getSubsonicArtistListMode()) }; auto subsonicArtistListModeRow{ _subsonicArtistListModeModel->getRowFromValue(user->getSubsonicArtistListMode()) };
if (subsonicArtistListModeRow) if (subsonicArtistListModeRow)
@@ -259,7 +259,7 @@ namespace UserInterface {
} }
{ {
const bool usesListenBrainz{ user->getScrobblingBackend() == ScrobblingBackend::ListenBrainz || user->getFeedbackBackend() == FeedbackBackend::ListenBrainz}; const bool usesListenBrainz{ user->getScrobblingBackend() == ScrobblingBackend::ListenBrainz || user->getFeedbackBackend() == FeedbackBackend::ListenBrainz };
setReadOnly(SettingsModel::ListenBrainzTokenField, !usesListenBrainz); setReadOnly(SettingsModel::ListenBrainzTokenField, !usesListenBrainz);
validator(SettingsModel::ListenBrainzTokenField)->setMandatory(usesListenBrainz); validator(SettingsModel::ListenBrainzTokenField)->setMandatory(usesListenBrainz);
} }
@@ -315,23 +315,23 @@ namespace UserInterface {
void initializeModels() void initializeModels()
{ {
_transcodeModeModel = std::make_shared<TranscodeModeModel>(); _transcodingModeModeModel = std::make_shared<TranscodingModeModel>();
_transcodeModeModel->add(Wt::WString::tr("Lms.Settings.transcode-mode.always"), MediaPlayer::Settings::Transcode::Mode::Always); _transcodingModeModeModel->add(Wt::WString::tr("Lms.Settings.transcode-mode.always"), MediaPlayer::Settings::Transcode::Mode::Always);
_transcodeModeModel->add(Wt::WString::tr("Lms.Settings.transcode-mode.never"), MediaPlayer::Settings::Transcode::Mode::Never); _transcodingModeModeModel->add(Wt::WString::tr("Lms.Settings.transcode-mode.never"), MediaPlayer::Settings::Transcode::Mode::Never);
_transcodeModeModel->add(Wt::WString::tr("Lms.Settings.transcode-mode.if-format-not-supported"), MediaPlayer::Settings::Transcode::Mode::IfFormatNotSupported); _transcodingModeModeModel->add(Wt::WString::tr("Lms.Settings.transcode-mode.if-format-not-supported"), MediaPlayer::Settings::Transcode::Mode::IfFormatNotSupported);
_transcodeBitrateModel = std::make_shared<ValueStringModel<Bitrate>>(); _transcodingOutputBitrateModel = std::make_shared<ValueStringModel<Bitrate>>();
visitAllowedAudioBitrates([&](const Bitrate bitrate) visitAllowedAudioBitrates([&](const Bitrate bitrate)
{ {
_transcodeBitrateModel->add(Wt::WString::fromUTF8(std::to_string(bitrate / 1000)), bitrate); _transcodingOutputBitrateModel->add(Wt::WString::fromUTF8(std::to_string(bitrate / 1000)), bitrate);
}); });
_transcodeFormatModel = std::make_shared<ValueStringModel<AudioFormat>>(); _transcodingOutputFormatModel = std::make_shared<ValueStringModel<TranscodingOutputFormat>>();
_transcodeFormatModel->add(Wt::WString::tr("Lms.Settings.transcode-format.mp3"), AudioFormat::MP3); _transcodingOutputFormatModel->add(Wt::WString::tr("Lms.Settings.transcode-format.mp3"), TranscodingOutputFormat::MP3);
_transcodeFormatModel->add(Wt::WString::tr("Lms.Settings.transcode-format.ogg_opus"), AudioFormat::OGG_OPUS); _transcodingOutputFormatModel->add(Wt::WString::tr("Lms.Settings.transcode-format.ogg_opus"), TranscodingOutputFormat::OGG_OPUS);
_transcodeFormatModel->add(Wt::WString::tr("Lms.Settings.transcode-format.matroska_opus"), AudioFormat::MATROSKA_OPUS); _transcodingOutputFormatModel->add(Wt::WString::tr("Lms.Settings.transcode-format.matroska_opus"), TranscodingOutputFormat::MATROSKA_OPUS);
_transcodeFormatModel->add(Wt::WString::tr("Lms.Settings.transcode-format.ogg_vorbis"), AudioFormat::OGG_VORBIS); _transcodingOutputFormatModel->add(Wt::WString::tr("Lms.Settings.transcode-format.ogg_vorbis"), TranscodingOutputFormat::OGG_VORBIS);
_transcodeFormatModel->add(Wt::WString::tr("Lms.Settings.transcode-format.webm_vorbis"), AudioFormat::WEBM_VORBIS); _transcodingOutputFormatModel->add(Wt::WString::tr("Lms.Settings.transcode-format.webm_vorbis"), TranscodingOutputFormat::WEBM_VORBIS);
_replayGainModeModel = std::make_shared<ReplayGainModeModel>(); _replayGainModeModel = std::make_shared<ReplayGainModeModel>();
_replayGainModeModel->add(Wt::WString::tr("Lms.Settings.replaygain-mode.none"), MediaPlayer::Settings::ReplayGain::Mode::None); _replayGainModeModel->add(Wt::WString::tr("Lms.Settings.replaygain-mode.none"), MediaPlayer::Settings::ReplayGain::Mode::None);
@@ -356,13 +356,13 @@ namespace UserInterface {
::Auth::IPasswordService* _authPasswordService{}; ::Auth::IPasswordService* _authPasswordService{};
bool _withOldPassword{}; bool _withOldPassword{};
std::shared_ptr<TranscodeModeModel> _transcodeModeModel; std::shared_ptr<TranscodingModeModel> _transcodingModeModeModel;
std::shared_ptr<ValueStringModel<Bitrate>> _transcodeBitrateModel; std::shared_ptr<ValueStringModel<Bitrate>> _transcodingOutputBitrateModel;
std::shared_ptr<ValueStringModel<AudioFormat>> _transcodeFormatModel; std::shared_ptr<ValueStringModel<TranscodingOutputFormat>> _transcodingOutputFormatModel;
std::shared_ptr<ReplayGainModeModel> _replayGainModeModel; std::shared_ptr<ReplayGainModeModel> _replayGainModeModel;
std::shared_ptr<ValueStringModel<SubsonicArtistListMode>> _subsonicArtistListModeModel; std::shared_ptr<ValueStringModel<SubsonicArtistListMode>> _subsonicArtistListModeModel;
std::shared_ptr<FeedbackBackendModel> _feedbackBackendModel; std::shared_ptr<FeedbackBackendModel> _feedbackBackendModel;
std::shared_ptr<ScrobblingBackendModel> _scrobblingBackendModel; std::shared_ptr<ScrobblingBackendModel> _scrobblingBackendModel;
}; };
SettingsView::SettingsView() SettingsView::SettingsView()
@@ -433,7 +433,7 @@ namespace UserInterface {
// Transcode // Transcode
auto transcodeMode{ std::make_unique<Wt::WComboBox>() }; auto transcodeMode{ std::make_unique<Wt::WComboBox>() };
auto* transcodeModeRaw{ transcodeMode.get() }; auto* transcodeModeRaw{ transcodeMode.get() };
transcodeMode->setModel(model->getTranscodeModeModel()); transcodeMode->setModel(model->getTranscodingModeModel());
t->setFormWidget(SettingsModel::TranscodeModeField, std::move(transcodeMode)); t->setFormWidget(SettingsModel::TranscodeModeField, std::move(transcodeMode));
// Format // Format
@@ -443,12 +443,12 @@ namespace UserInterface {
// Bitrate // Bitrate
auto transcodeBitrate{ std::make_unique<Wt::WComboBox>() }; auto transcodeBitrate{ std::make_unique<Wt::WComboBox>() };
transcodeBitrate->setModel(model->getTranscodeBitrateModel()); transcodeBitrate->setModel(model->getTranscodingOutputBitrateModel());
t->setFormWidget(SettingsModel::TranscodeBitrateField, std::move(transcodeBitrate)); t->setFormWidget(SettingsModel::TranscodeBitrateField, std::move(transcodeBitrate));
transcodeModeRaw->activated().connect([=](int row) transcodeModeRaw->activated().connect([=](int row)
{ {
const bool enable{ model->getTranscodeModeModel()->getValue(row) != MediaPlayer::Settings::Transcode::Mode::Never }; const bool enable{ model->getTranscodingModeModel()->getValue(row) != MediaPlayer::Settings::Transcode::Mode::Never };
model->setReadOnly(SettingsModel::TranscodeFormatField, !enable); model->setReadOnly(SettingsModel::TranscodeFormatField, !enable);
model->setReadOnly(SettingsModel::TranscodeBitrateField, !enable); model->setReadOnly(SettingsModel::TranscodeBitrateField, !enable);
t->updateModel(model.get()); t->updateModel(model.get());
@@ -493,12 +493,12 @@ namespace UserInterface {
// Format // Format
auto transcodeFormat{ std::make_unique<Wt::WComboBox>() }; auto transcodeFormat{ std::make_unique<Wt::WComboBox>() };
transcodeFormat->setModel(model->getTranscodeFormatModel()); transcodeFormat->setModel(model->getTranscodeFormatModel());
t->setFormWidget(SettingsModel::SubsonicTranscodeFormatField, std::move(transcodeFormat)); t->setFormWidget(SettingsModel::SubsonicTranscodingOutputFormatField, std::move(transcodeFormat));
// Bitrate // Bitrate
auto transcodeBitrate{ std::make_unique<Wt::WComboBox>() }; auto transcodeBitrate{ std::make_unique<Wt::WComboBox>() };
transcodeBitrate->setModel(model->getTranscodeBitrateModel()); transcodeBitrate->setModel(model->getTranscodingOutputBitrateModel());
t->setFormWidget(SettingsModel::SubsonicTranscodeBitrateField, std::move(transcodeBitrate)); t->setFormWidget(SettingsModel::SubsonicTranscodingOutputBitrateField, std::move(transcodeBitrate));
// Artist list mode // Artist list mode
auto artistListMode{ std::make_unique<Wt::WComboBox>() }; auto artistListMode{ std::make_unique<Wt::WComboBox>() };
+1 -1
View File
@@ -146,7 +146,7 @@ namespace UserInterface
if (audioStream) if (audioStream)
{ {
releaseInfo->setCondition("if-has-codec", true); releaseInfo->setCondition("if-has-codec", true);
releaseInfo->bindString("codec", audioStream->codec); releaseInfo->bindString("codec", audioStream->codecName);
break; break;
} }
} }
+1 -1
View File
@@ -130,7 +130,7 @@ namespace UserInterface::TrackListHelpers
if (audioStream) if (audioStream)
{ {
trackInfo->setCondition("if-has-codec", true); trackInfo->setCondition("if-has-codec", true);
trackInfo->bindString("codec", audioStream->codec); trackInfo->bindString("codec", audioStream->codecName);
} }
} }
+134 -146
View File
@@ -22,8 +22,8 @@
#include <optional> #include <optional>
#include <Wt/Http/Response.h> #include <Wt/Http/Response.h>
#include "av/TranscodeParameters.hpp" #include "av/TranscodingParameters.hpp"
#include "av/TranscodeResourceHandlerCreator.hpp" #include "av/TranscodingResourceHandlerCreator.hpp"
#include "av/Types.hpp" #include "av/Types.hpp"
#include "services/database/Session.hpp" #include "services/database/Session.hpp"
#include "services/database/Track.hpp" #include "services/database/Track.hpp"
@@ -37,178 +37,166 @@
namespace StringUtils namespace StringUtils
{ {
template <> template <>
std::optional<Database::AudioFormat> std::optional<Database::TranscodingOutputFormat> readAs(std::string_view str)
readAs(std::string_view str) {
{ auto encodedFormat{ readAs<int>(str) };
auto encodedFormat {readAs<int>(str)}; if (!encodedFormat)
if (!encodedFormat) return std::nullopt;
return std::nullopt;
Database::AudioFormat format {static_cast<Database::AudioFormat>(*encodedFormat)}; Database::TranscodingOutputFormat format{ static_cast<Database::TranscodingOutputFormat>(*encodedFormat) };
// check // check
switch (static_cast<Database::AudioFormat>(*encodedFormat)) switch (static_cast<Database::TranscodingOutputFormat>(*encodedFormat))
{ {
case Database::AudioFormat::MP3: case Database::TranscodingOutputFormat::MP3:
[[fallthrough]]; [[fallthrough]];
case Database::AudioFormat::OGG_OPUS: case Database::TranscodingOutputFormat::OGG_OPUS:
[[fallthrough]]; [[fallthrough]];
case Database::AudioFormat::MATROSKA_OPUS: case Database::TranscodingOutputFormat::MATROSKA_OPUS:
[[fallthrough]]; [[fallthrough]];
case Database::AudioFormat::OGG_VORBIS: case Database::TranscodingOutputFormat::OGG_VORBIS:
[[fallthrough]]; [[fallthrough]];
case Database::AudioFormat::WEBM_VORBIS: case Database::TranscodingOutputFormat::WEBM_VORBIS:
return format; return format;
} }
LOG(ERROR) << "Cannot determine audio format from value '" << str << "'"; LOG(ERROR) << "Cannot determine audio format from value '" << str << "'";
return std::nullopt; return std::nullopt;
} }
} }
static namespace UserInterface
std::optional<Av::Format>
AudioFormatToAvFormat(Database::AudioFormat format)
{ {
switch (format) namespace
{ {
case Database::AudioFormat::MP3: return Av::Format::MP3; std::optional<Av::Transcoding::OutputFormat> AudioFormatToAvFormat(Database::TranscodingOutputFormat format)
case Database::AudioFormat::OGG_OPUS: return Av::Format::OGG_OPUS; {
case Database::AudioFormat::MATROSKA_OPUS: return Av::Format::MATROSKA_OPUS; switch (format)
case Database::AudioFormat::OGG_VORBIS: return Av::Format::OGG_VORBIS; {
case Database::AudioFormat::WEBM_VORBIS: return Av::Format::WEBM_VORBIS; case Database::TranscodingOutputFormat::MP3: return Av::Transcoding::OutputFormat::MP3;
} case Database::TranscodingOutputFormat::OGG_OPUS: return Av::Transcoding::OutputFormat::OGG_OPUS;
case Database::TranscodingOutputFormat::MATROSKA_OPUS: return Av::Transcoding::OutputFormat::MATROSKA_OPUS;
case Database::TranscodingOutputFormat::OGG_VORBIS: return Av::Transcoding::OutputFormat::OGG_VORBIS;
case Database::TranscodingOutputFormat::WEBM_VORBIS: return Av::Transcoding::OutputFormat::WEBM_VORBIS;
}
LOG(ERROR) << "Cannot convert from audio format to AV format"; LOG(ERROR) << "Cannot convert from audio format to AV format";
return std::nullopt; return std::nullopt;
} }
template<typename T>
std::optional<T> readParameterAs(const Wt::Http::Request& request, const std::string& parameterName)
{
auto paramStr{ request.getParameter(parameterName) };
if (!paramStr)
{
LOG(DEBUG) << "Missing parameter '" << parameterName << "'";
return std::nullopt;
}
namespace UserInterface { auto res{ StringUtils::readAs<T>(*paramStr) };
if (!res)
LOG(ERROR) << "Cannot parse parameter '" << parameterName << "' from value '" << *paramStr << "'";
AudioTranscodeResource:: ~AudioTranscodeResource() return res;
{ }
beingDeleted();
}
std::string struct TranscodingParameters
AudioTranscodeResource::getUrl(Database::TrackId trackId) const {
{ Av::Transcoding::InputParameters inputParameters;
return url() + "&trackid=" + trackId.toString(); Av::Transcoding::OutputParameters outputParameters;
} };
template<typename T> std::optional<TranscodingParameters> readTranscodingParameters(const Wt::Http::Request& request)
std::optional<T> {
readParameterAs(const Wt::Http::Request& request, const std::string& parameterName) TranscodingParameters parameters;
{
auto paramStr {request.getParameter(parameterName)};
if (!paramStr)
{
LOG(DEBUG) << "Missing parameter '" << parameterName << "'";
return std::nullopt;
}
auto res {StringUtils::readAs<T>(*paramStr)}; // mandatory parameters
if (!res) const std::optional<Database::TrackId> trackId{ readParameterAs<Database::TrackId::ValueType>(request, "trackid") };
LOG(ERROR) << "Cannot parse parameter '" << parameterName << "' from value '" << *paramStr << "'"; const auto format{ readParameterAs<Database::TranscodingOutputFormat>(request, "format") };
const auto bitrate{ readParameterAs<Database::Bitrate>(request, "bitrate") };
return res; if (!trackId || !format || !bitrate)
} return std::nullopt;
namespace if (!Database::isAudioBitrateAllowed(*bitrate))
{ {
struct TranscodeParameters LOG(ERROR) << "Bitrate '" << *bitrate << "' is not allowed";
{ return std::nullopt;
Av::InputFileParameters inputFileParameters; }
Av::TranscodeParameters transcodeParameters;
};
std::optional<TranscodeParameters> const std::optional<Av::Transcoding::OutputFormat> avFormat{ AudioFormatToAvFormat(*format) };
readTranscodeParameters(const Wt::Http::Request& request) if (!avFormat)
{ return std::nullopt;
TranscodeParameters parameters;
// mandatory parameters // optional parameter
const std::optional<Database::TrackId> trackId {readParameterAs<Database::TrackId::ValueType>(request, "trackid")}; std::size_t offset{ readParameterAs<std::size_t>(request, "offset").value_or(0) };
const auto format {readParameterAs<Database::AudioFormat>(request, "format")};
const auto bitrate {readParameterAs<Database::Bitrate>(request, "bitrate")};
if (!trackId || !format || !bitrate) std::filesystem::path trackPath;
return std::nullopt; {
auto transaction{ LmsApp->getDbSession().createSharedTransaction() };
if (!Database::isAudioBitrateAllowed(*bitrate)) const Database::Track::pointer track{ Database::Track::find(LmsApp->getDbSession(), *trackId) };
{ if (!track)
LOG(ERROR) << "Bitrate '" << *bitrate << "' is not allowed"; {
return std::nullopt; LOG(ERROR) << "Missing track";
} return std::nullopt;
}
const std::optional<Av::Format> avFormat {AudioFormatToAvFormat(*format)}; parameters.inputParameters.trackPath = track->getPath();
if (!avFormat) parameters.inputParameters.duration = track->getDuration();
return std::nullopt; }
// optional parameter parameters.outputParameters.stripMetadata = true;
std::size_t offset {readParameterAs<std::size_t>(request, "offset").value_or(0)}; parameters.outputParameters.format = *avFormat;
parameters.outputParameters.bitrate = *bitrate;
parameters.outputParameters.offset = std::chrono::seconds{ offset };
std::filesystem::path trackPath; return parameters;
{ }
auto transaction {LmsApp->getDbSession().createSharedTransaction()}; }
const Database::Track::pointer track {Database::Track::find(LmsApp->getDbSession(), *trackId)}; AudioTranscodeResource:: ~AudioTranscodeResource()
if (!track) {
{ beingDeleted();
LOG(ERROR) << "Missing track"; }
return std::nullopt;
}
parameters.inputFileParameters.trackPath = track->getPath(); std::string AudioTranscodeResource::getUrl(Database::TrackId trackId) const
parameters.inputFileParameters.duration = track->getDuration(); {
} return url() + "&trackid=" + trackId.toString();
}
parameters.transcodeParameters.stripMetadata = true; void AudioTranscodeResource::handleRequest(const Wt::Http::Request& request, Wt::Http::Response& response)
parameters.transcodeParameters.format = *avFormat; {
parameters.transcodeParameters.bitrate = *bitrate; std::shared_ptr<IResourceHandler> resourceHandler;
parameters.transcodeParameters.offset = std::chrono::seconds {offset};
return parameters; try
} {
} Wt::Http::ResponseContinuation* continuation{ request.continuation() };
if (!continuation)
{
if (const auto& parameters{ readTranscodingParameters(request) })
resourceHandler = Av::Transcoding::createResourceHandler(parameters->inputParameters, parameters->outputParameters, false /* estimate content length */);
}
else
{
resourceHandler = Wt::cpp17::any_cast<std::shared_ptr<IResourceHandler>>(continuation->data());
}
void if (resourceHandler)
AudioTranscodeResource::handleRequest(const Wt::Http::Request& request, {
Wt::Http::Response& response) continuation = resourceHandler->processRequest(request, response);
{ if (continuation)
std::shared_ptr<IResourceHandler> resourceHandler; continuation->setData(resourceHandler);
}
try }
{ catch (const Av::Exception& e)
Wt::Http::ResponseContinuation* continuation {request.continuation()}; {
if (!continuation) LOG(ERROR) << "Caught Av exception: " << e.what();
{ }
const std::optional<TranscodeParameters>& parameters {readTranscodeParameters(request)}; }
if (parameters)
resourceHandler = Av::createTranscodeResourceHandler(parameters->inputFileParameters, parameters->transcodeParameters, false /* estimate content length */);
}
else
{
resourceHandler = Wt::cpp17::any_cast<std::shared_ptr<IResourceHandler>>(continuation->data());
}
if (resourceHandler)
{
continuation = resourceHandler->processRequest(request, response);
if (continuation)
continuation->setData(resourceHandler);
}
}
catch (const Av::Exception& e)
{
LOG(ERROR) << "Caught Av exception: " << e.what();
}
}
} // namespace UserInterface } // namespace UserInterface