diff --git a/src/libs/av/impl/TranscodeResourceHandler.cpp b/src/libs/av/impl/TranscodeResourceHandler.cpp index 6554999c..54064a8a 100644 --- a/src/libs/av/impl/TranscodeResourceHandler.cpp +++ b/src/libs/av/impl/TranscodeResourceHandler.cpp @@ -18,32 +18,48 @@ */ #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 * std::chrono::duration_cast(inputFileParameters.duration).count() / 1000}; + return estimatedContentLength; + } + } std::unique_ptr - createTranscodeResourceHandler(const std::filesystem::path& trackPath, const TranscodeParameters& parameters) + createTranscodeResourceHandler(const InputFileParameters& inputFileParameters, const TranscodeParameters& transcodeParameters, bool estimateContentLength) { - return std::make_unique(trackPath, parameters); + return std::make_unique(inputFileParameters, transcodeParameters, estimateContentLength); } // TODO set some nice HTTP return code - TranscodeResourceHandler::TranscodeResourceHandler(const std::filesystem::path& trackPath, const TranscodeParameters& parameters) - : _transcoder {trackPath, parameters} + 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; } Wt::Http::ResponseContinuation* TranscodeResourceHandler::processRequest(const Wt::Http::Request& /*request*/, Wt::Http::Response& response) { + if (_estimatedContentLength) + response.setContentLength(*_estimatedContentLength); response.setMimeType(_transcoder.getOutputMimeType()); - if (_nbBytesReady > 0) + if (_bytesReadyCount > 0) { - response.out().write(reinterpret_cast(&_buffer[0]), _nbBytesReady); - _nbBytesReady = 0; + response.out().write(reinterpret_cast(&_buffer[0]), _bytesReadyCount); + _bytesReadyCount = 0; + _totalServedByteCount += _bytesReadyCount; } if (!_transcoder.finished()) @@ -52,13 +68,30 @@ namespace Av continuation->waitForMoreData(); _transcoder.asyncRead(_buffer.data(), _buffer.size(), [=](std::size_t nbBytesRead) { - assert(_nbBytesReady == 0); - _nbBytesReady = nbBytesRead; + 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 {}; } diff --git a/src/libs/av/impl/TranscodeResourceHandler.hpp b/src/libs/av/impl/TranscodeResourceHandler.hpp index 2395a6cc..47d76f36 100644 --- a/src/libs/av/impl/TranscodeResourceHandler.hpp +++ b/src/libs/av/impl/TranscodeResourceHandler.hpp @@ -21,6 +21,7 @@ #include #include +#include #include "av/TranscodeParameters.hpp" #include "utils/IResourceHandler.hpp" @@ -28,19 +29,19 @@ namespace Av { - class TranscodeResourceHandler final : public IResourceHandler { public: - TranscodeResourceHandler(const std::filesystem::path& trackPath, const TranscodeParameters& parameters); + TranscodeResourceHandler(const InputFileParameters& inputFileParameters, const TranscodeParameters& parameters, bool estimateContentLength); private: Wt::Http::ResponseContinuation* processRequest(const Wt::Http::Request& request, Wt::Http::Response& reponse) override; static constexpr std::size_t _chunkSize {32768}; + std::optional _estimatedContentLength; std::array _buffer; - std::size_t _nbBytesReady {}; - const std::filesystem::path _trackPath; + std::size_t _bytesReadyCount {}; + std::size_t _totalServedByteCount {}; Transcoder _transcoder; }; } diff --git a/src/libs/av/impl/Transcoder.cpp b/src/libs/av/impl/Transcoder.cpp index 55b00d1b..4b828d5a 100644 --- a/src/libs/av/impl/Transcoder.cpp +++ b/src/libs/av/impl/Transcoder.cpp @@ -30,7 +30,7 @@ namespace Av { -#define LOG(sev) LMS_LOG(TRANSCODE, sev) << "[" << _id << "] - " +#define LOG(sev) LMS_LOG(TRANSCODE, sev) << "[" << _debugId << "] - " static std::atomic globalId {}; static std::filesystem::path ffmpegPath; @@ -43,10 +43,10 @@ Transcoder::init() throw Exception {"File '" + ffmpegPath.string() + "' does not exist!"}; } -Transcoder::Transcoder(const std::filesystem::path& filePath, const TranscodeParameters& parameters) -: _id {globalId++} -, _filePath {filePath} -, _parameters {parameters} +Transcoder::Transcoder(const InputFileParameters& inputFileParameters, const TranscodeParameters& transcodeParameters) +: _debugId {globalId++} +, _inputFileParameters {inputFileParameters} +, _transcodeParameters {transcodeParameters} { start(); } @@ -61,17 +61,17 @@ Transcoder::start() try { - if (!std::filesystem::exists(_filePath)) - throw Exception {"File '" + _filePath.string() + "' does not exist!"}; - else if (!std::filesystem::is_regular_file( _filePath) ) - throw Exception {"File '" + _filePath.string() + "' is not regular!"}; + 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 '" + _filePath.string() + "': " + e.what()}; + throw Exception {"File error '" + _inputFileParameters.trackPath.string() + "': " + e.what()}; } - LOG(INFO) << "Transcoding file '" << _filePath.string() << "'"; + LOG(INFO) << "Transcoding file '" << _inputFileParameters.trackPath.string() << "'"; std::vector args; @@ -90,22 +90,22 @@ Transcoder::start() args.emplace_back("-ss"); std::ostringstream oss; - oss << std::fixed << std::showpoint << std::setprecision(3) << (_parameters.offset.count() / float {1000}); + oss << std::fixed << std::showpoint << std::setprecision(3) << (_transcodeParameters.offset.count() / float {1000}); args.emplace_back(oss.str()); } // Input file args.emplace_back("-i"); - args.emplace_back(_filePath.string()); + args.emplace_back(_inputFileParameters.trackPath.string()); // Stream mapping, if set - if (_parameters.stream) + if (_transcodeParameters.stream) { args.emplace_back("-map"); - args.emplace_back("0:" + std::to_string(*_parameters.stream)); + args.emplace_back("0:" + std::to_string(*_transcodeParameters.stream)); } - if (_parameters.stripMetadata) + if (_transcodeParameters.stripMetadata) { // Strip metadata args.emplace_back("-map_metadata"); @@ -117,10 +117,10 @@ Transcoder::start() // Output bitrates args.emplace_back("-b:a"); - args.emplace_back(std::to_string(_parameters.bitrate)); + args.emplace_back(std::to_string(_transcodeParameters.bitrate)); // Codecs and formats - switch (_parameters.format) + switch (_transcodeParameters.format) { case Format::MP3: args.emplace_back("-f"); @@ -156,10 +156,10 @@ Transcoder::start() break; default: - throw Exception {"Unhandled format (" + std::to_string(static_cast(_parameters.format)) + ")"}; + throw Exception {"Unhandled format (" + std::to_string(static_cast(_transcodeParameters.format)) + ")"}; } - _outputMimeType = formatToMimetype(_parameters.format); + _outputMimeType = formatToMimetype(_transcodeParameters.format); args.emplace_back("pipe:1"); diff --git a/src/libs/av/impl/Transcoder.hpp b/src/libs/av/impl/Transcoder.hpp index 54ebe5e5..a4de2976 100644 --- a/src/libs/av/impl/Transcoder.hpp +++ b/src/libs/av/impl/Transcoder.hpp @@ -32,7 +32,7 @@ namespace Av class Transcoder { public: - Transcoder(const std::filesystem::path& file, const TranscodeParameters& parameters); + Transcoder(const InputFileParameters& inputFileParameters, const TranscodeParameters& transcodeParameters); ~Transcoder(); Transcoder(const Transcoder&) = delete; @@ -49,7 +49,7 @@ namespace Av std::size_t readSome(std::byte* buffer, std::size_t bufferSize); const std::string& getOutputMimeType() const { return _outputMimeType; } - const TranscodeParameters& getParameters() const { return _parameters; } + const TranscodeParameters& getParameters() const { return _transcodeParameters; } bool finished() const; @@ -58,9 +58,9 @@ namespace Av void start(); - const std::size_t _id {}; - const std::filesystem::path _filePath; - const TranscodeParameters _parameters; + const std::size_t _debugId {}; + const InputFileParameters _inputFileParameters; + const TranscodeParameters _transcodeParameters; std::unique_ptr _childProcess; diff --git a/src/libs/av/include/av/TranscodeParameters.hpp b/src/libs/av/include/av/TranscodeParameters.hpp index c155f6e7..f709fef8 100644 --- a/src/libs/av/include/av/TranscodeParameters.hpp +++ b/src/libs/av/include/av/TranscodeParameters.hpp @@ -20,12 +20,19 @@ #pragma once #include +#include #include #include "Types.hpp" namespace Av { + struct InputFileParameters + { + std::filesystem::path trackPath; + std::chrono::milliseconds duration; + }; + struct TranscodeParameters { Format format; diff --git a/src/libs/av/include/av/TranscodeResourceHandlerCreator.hpp b/src/libs/av/include/av/TranscodeResourceHandlerCreator.hpp index 0255cfc4..8b232d8a 100644 --- a/src/libs/av/include/av/TranscodeResourceHandlerCreator.hpp +++ b/src/libs/av/include/av/TranscodeResourceHandlerCreator.hpp @@ -19,15 +19,15 @@ #pragma once -#include #include #include "utils/IResourceHandler.hpp" namespace Av { + struct InputFileParameters; struct TranscodeParameters; - std::unique_ptr createTranscodeResourceHandler(const std::filesystem::path& trackPath, const TranscodeParameters& parameters); + std::unique_ptr createTranscodeResourceHandler(const InputFileParameters& inputFileParameters, const TranscodeParameters& parameters, bool estimateContentLength); } diff --git a/src/libs/subsonic/impl/Stream.cpp b/src/libs/subsonic/impl/Stream.cpp index 8089779c..91c13eaf 100644 --- a/src/libs/subsonic/impl/Stream.cpp +++ b/src/libs/subsonic/impl/Stream.cpp @@ -54,8 +54,9 @@ userTranscodeFormatToAvFormat(AudioFormat format) struct StreamParameters { - std::filesystem::path trackPath; + Av::InputFileParameters inputFileParameters; std::optional transcodeParameters; + bool estimateContentLength {}; }; static @@ -68,9 +69,12 @@ getStreamParameters(RequestContext& context) // Optional params std::optional maxBitRate {getParameterAs(context.parameters, "maxBitRate")}; std::optional format {getParameterAs(context.parameters, "format")}; + bool estimateContentLength {getParameterAs(context.parameters, "estimateContentLength").value_or(false)}; StreamParameters parameters; + parameters.estimateContentLength = estimateContentLength; + auto transaction {context.dbSession.createSharedTransaction()}; { @@ -78,7 +82,8 @@ getStreamParameters(RequestContext& context) if (!track) throw RequestedDataNotFoundError {}; - parameters.trackPath = track->getPath(); + parameters.inputFileParameters.trackPath = track->getPath(); + parameters.inputFileParameters.duration = track->getDuration(); } { @@ -155,9 +160,9 @@ handleStream(RequestContext& context, const Wt::Http::Request& request, Wt::Http { StreamParameters streamParameters {getStreamParameters(context)}; if (streamParameters.transcodeParameters) - resourceHandler = Av::createTranscodeResourceHandler(streamParameters.trackPath, *streamParameters.transcodeParameters); + resourceHandler = Av::createTranscodeResourceHandler(streamParameters.inputFileParameters, *streamParameters.transcodeParameters, streamParameters.estimateContentLength); else - resourceHandler = createFileResourceHandler(streamParameters.trackPath); + resourceHandler = createFileResourceHandler(streamParameters.inputFileParameters.trackPath); } else { diff --git a/src/libs/subsonic/impl/SubsonicId.cpp b/src/libs/subsonic/impl/SubsonicId.cpp index edd052f1..1252a7b8 100644 --- a/src/libs/subsonic/impl/SubsonicId.cpp +++ b/src/libs/subsonic/impl/SubsonicId.cpp @@ -136,17 +136,4 @@ namespace StringUtils return std::nullopt; } - - template<> - std::optional - readAs(std::string_view str) - { - if (str == "true") - return true; - else if (str == "false") - return false; - - return {}; - } } - diff --git a/src/libs/subsonic/impl/SubsonicId.hpp b/src/libs/subsonic/impl/SubsonicId.hpp index 32b45715..96db3e79 100644 --- a/src/libs/subsonic/impl/SubsonicId.hpp +++ b/src/libs/subsonic/impl/SubsonicId.hpp @@ -58,9 +58,5 @@ namespace StringUtils template<> std::optional readAs(std::string_view str); - - template<> - std::optional - readAs(std::string_view str); } diff --git a/src/libs/utils/impl/String.cpp b/src/libs/utils/impl/String.cpp index 09c55dd3..47adbd85 100644 --- a/src/libs/utils/impl/String.cpp +++ b/src/libs/utils/impl/String.cpp @@ -63,6 +63,18 @@ readAs(std::string_view str) return std::string {str}; } +template<> +std::optional +readAs(std::string_view str) +{ + if (str == "1" || str == "true") + return true; + else if (str == "0" || str == "false") + return false; + + return std::nullopt; +} + std::vector splitStringCopy(std::string_view string, std::string_view separators) { diff --git a/src/libs/utils/include/utils/String.hpp b/src/libs/utils/include/utils/String.hpp index 8ff743c5..9dfe8742 100644 --- a/src/libs/utils/include/utils/String.hpp +++ b/src/libs/utils/include/utils/String.hpp @@ -85,6 +85,11 @@ template<> std::optional readAs(std::string_view str); +template<> +[[nodiscard]] +std::optional +readAs(std::string_view str); + [[nodiscard]] std::string replaceInString(std::string_view str, const std::string& from, const std::string& to); diff --git a/src/libs/utils/test/String.cpp b/src/libs/utils/test/String.cpp index 3935f6c7..87d67ee3 100644 --- a/src/libs/utils/test/String.cpp +++ b/src/libs/utils/test/String.cpp @@ -109,3 +109,12 @@ TEST(StringUtils, escapeString) EXPECT_EQ(StringUtils::escapeString("**||", "*|", '_'), "_*_*_|_|"); } +TEST(StringUtils, readAs) +{ + EXPECT_EQ(StringUtils::readAs("true"), true); + EXPECT_EQ(StringUtils::readAs("1"), true); + EXPECT_EQ(StringUtils::readAs("false"), false); + EXPECT_EQ(StringUtils::readAs("0"), false); + EXPECT_EQ(StringUtils::readAs("foo"), std::nullopt); + EXPECT_EQ(StringUtils::readAs(""), std::nullopt); +} diff --git a/src/lms/ui/resource/AudioTranscodeResource.cpp b/src/lms/ui/resource/AudioTranscodeResource.cpp index c8982fbd..b7797724 100644 --- a/src/lms/ui/resource/AudioTranscodeResource.cpp +++ b/src/lms/ui/resource/AudioTranscodeResource.cpp @@ -118,59 +118,62 @@ readParameterAs(const Wt::Http::Request& request, const std::string& parameterNa return res; } -struct TranscodeParameters +namespace { - std::filesystem::path file; - Av::TranscodeParameters transcodeParameters; -}; - -static -std::optional -readTranscodeParameters(const Wt::Http::Request& request) -{ - TranscodeParameters parameters; - - // mandatory parameters - const std::optional trackId {readParameterAs(request, "trackid")}; - const auto format {readParameterAs(request, "format")}; - const auto bitrate {readParameterAs(request, "bitrate")}; - - if (!trackId || !format || !bitrate) - return std::nullopt; - - const std::optional avFormat {AudioFormatToAvFormat(*format)}; - if (!avFormat) - return std::nullopt; - - // optional parameter - std::size_t offset {readParameterAs(request, "offset").value_or(0)}; - - std::filesystem::path trackPath; + struct TranscodeParameters { - auto transaction {LmsApp->getDbSession().createSharedTransaction()}; + Av::InputFileParameters inputFileParameters; + Av::TranscodeParameters transcodeParameters; + }; - const Database::Track::pointer track {Database::Track::find(LmsApp->getDbSession(), *trackId)}; - if (!track) - { - LOG(ERROR) << "Missing track"; + std::optional + readTranscodeParameters(const Wt::Http::Request& request) + { + TranscodeParameters parameters; + + // mandatory parameters + const std::optional trackId {readParameterAs(request, "trackid")}; + const auto format {readParameterAs(request, "format")}; + const auto bitrate {readParameterAs(request, "bitrate")}; + + if (!trackId || !format || !bitrate) return std::nullopt; - } - - parameters.file = track->getPath(); if (!Database::isAudioBitrateAllowed(*bitrate)) { LOG(ERROR) << "Bitrate '" << *bitrate << "' is not allowed"; return std::nullopt; } + + const std::optional avFormat {AudioFormatToAvFormat(*format)}; + if (!avFormat) + return std::nullopt; + + // optional parameter + std::size_t offset {readParameterAs(request, "offset").value_or(0)}; + + std::filesystem::path trackPath; + { + auto transaction {LmsApp->getDbSession().createSharedTransaction()}; + + const Database::Track::pointer track {Database::Track::find(LmsApp->getDbSession(), *trackId)}; + if (!track) + { + LOG(ERROR) << "Missing track"; + return std::nullopt; + } + + parameters.inputFileParameters.trackPath = track->getPath(); + parameters.inputFileParameters.duration = track->getDuration(); + } + + parameters.transcodeParameters.stripMetadata = true; + parameters.transcodeParameters.format = *avFormat; + parameters.transcodeParameters.bitrate = *bitrate; + parameters.transcodeParameters.offset = std::chrono::seconds {offset}; + + return parameters; } - - parameters.transcodeParameters.stripMetadata = true; - parameters.transcodeParameters.format = *avFormat; - parameters.transcodeParameters.bitrate = *bitrate; - parameters.transcodeParameters.offset = std::chrono::seconds {offset}; - - return parameters; } void @@ -186,7 +189,7 @@ AudioTranscodeResource::handleRequest(const Wt::Http::Request& request, { const std::optional& parameters {readTranscodeParameters(request)}; if (parameters) - resourceHandler = Av::createTranscodeResourceHandler(parameters->file, parameters->transcodeParameters); + resourceHandler = Av::createTranscodeResourceHandler(parameters->inputFileParameters, parameters->transcodeParameters, false /* estimate content length */); } else {