Renamed some transcoding stuff + now retrieve decoder codecs
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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 {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
+4
-4
@@ -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;
|
||||||
@@ -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"};
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|||||||
+26
-15
@@ -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
|
||||||
|
|
||||||
+4
-5
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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>() };
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user