Subonsic API: added estimateContentLength parameter support for stream endpoint. fixes #244

This commit is contained in:
emeric
2022-08-20 14:56:16 +02:00
parent 49f0d40515
commit 1fb838c8d0
13 changed files with 162 additions and 104 deletions
+42 -9
View File
@@ -18,32 +18,48 @@
*/ */
#include "TranscodeResourceHandler.hpp" #include "TranscodeResourceHandler.hpp"
#include "utils/Logger.hpp"
namespace Av 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<std::chrono::milliseconds>(inputFileParameters.duration).count() / 1000};
return estimatedContentLength;
}
}
std::unique_ptr<IResourceHandler> std::unique_ptr<IResourceHandler>
createTranscodeResourceHandler(const std::filesystem::path& trackPath, const TranscodeParameters& parameters) createTranscodeResourceHandler(const InputFileParameters& inputFileParameters, const TranscodeParameters& transcodeParameters, bool estimateContentLength)
{ {
return std::make_unique<TranscodeResourceHandler>(trackPath, parameters); return std::make_unique<TranscodeResourceHandler>(inputFileParameters, transcodeParameters, estimateContentLength);
} }
// TODO set some nice HTTP return code // TODO set some nice HTTP return code
TranscodeResourceHandler::TranscodeResourceHandler(const std::filesystem::path& trackPath, const TranscodeParameters& parameters) TranscodeResourceHandler::TranscodeResourceHandler(const InputFileParameters& inputFileParameters, const TranscodeParameters& transcodeParameters, bool estimateContentLength)
: _transcoder {trackPath, parameters} : _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* Wt::Http::ResponseContinuation*
TranscodeResourceHandler::processRequest(const Wt::Http::Request& /*request*/, Wt::Http::Response& response) TranscodeResourceHandler::processRequest(const Wt::Http::Request& /*request*/, Wt::Http::Response& response)
{ {
if (_estimatedContentLength)
response.setContentLength(*_estimatedContentLength);
response.setMimeType(_transcoder.getOutputMimeType()); response.setMimeType(_transcoder.getOutputMimeType());
if (_nbBytesReady > 0) if (_bytesReadyCount > 0)
{ {
response.out().write(reinterpret_cast<const char *>(&_buffer[0]), _nbBytesReady); response.out().write(reinterpret_cast<const char *>(&_buffer[0]), _bytesReadyCount);
_nbBytesReady = 0; _bytesReadyCount = 0;
_totalServedByteCount += _bytesReadyCount;
} }
if (!_transcoder.finished()) if (!_transcoder.finished())
@@ -52,13 +68,30 @@ namespace Av
continuation->waitForMoreData(); continuation->waitForMoreData();
_transcoder.asyncRead(_buffer.data(), _buffer.size(), [=](std::size_t nbBytesRead) _transcoder.asyncRead(_buffer.data(), _buffer.size(), [=](std::size_t nbBytesRead)
{ {
assert(_nbBytesReady == 0); assert(_bytesReadyCount == 0);
_nbBytesReady = nbBytesRead; _bytesReadyCount = nbBytesRead;
continuation->haveMoreData(); continuation->haveMoreData();
}); });
return continuation; 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 {}; return {};
} }
@@ -21,6 +21,7 @@
#include <array> #include <array>
#include <filesystem> #include <filesystem>
#include <optional>
#include "av/TranscodeParameters.hpp" #include "av/TranscodeParameters.hpp"
#include "utils/IResourceHandler.hpp" #include "utils/IResourceHandler.hpp"
@@ -28,19 +29,19 @@
namespace Av namespace Av
{ {
class TranscodeResourceHandler final : public IResourceHandler class TranscodeResourceHandler final : public IResourceHandler
{ {
public: public:
TranscodeResourceHandler(const std::filesystem::path& trackPath, const TranscodeParameters& parameters); TranscodeResourceHandler(const InputFileParameters& inputFileParameters, const TranscodeParameters& parameters, 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;
static constexpr std::size_t _chunkSize {32768}; static constexpr std::size_t _chunkSize {32768};
std::optional<std::size_t> _estimatedContentLength;
std::array<std::byte, _chunkSize> _buffer; std::array<std::byte, _chunkSize> _buffer;
std::size_t _nbBytesReady {}; std::size_t _bytesReadyCount {};
const std::filesystem::path _trackPath; std::size_t _totalServedByteCount {};
Transcoder _transcoder; Transcoder _transcoder;
}; };
} }
+20 -20
View File
@@ -30,7 +30,7 @@
namespace Av { namespace Av {
#define LOG(sev) LMS_LOG(TRANSCODE, sev) << "[" << _id << "] - " #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;
@@ -43,10 +43,10 @@ Transcoder::init()
throw Exception {"File '" + ffmpegPath.string() + "' does not exist!"}; throw Exception {"File '" + ffmpegPath.string() + "' does not exist!"};
} }
Transcoder::Transcoder(const std::filesystem::path& filePath, const TranscodeParameters& parameters) Transcoder::Transcoder(const InputFileParameters& inputFileParameters, const TranscodeParameters& transcodeParameters)
: _id {globalId++} : _debugId {globalId++}
, _filePath {filePath} , _inputFileParameters {inputFileParameters}
, _parameters {parameters} , _transcodeParameters {transcodeParameters}
{ {
start(); start();
} }
@@ -61,17 +61,17 @@ Transcoder::start()
try try
{ {
if (!std::filesystem::exists(_filePath)) if (!std::filesystem::exists(_inputFileParameters.trackPath))
throw Exception {"File '" + _filePath.string() + "' does not exist!"}; throw Exception {"File '" + _inputFileParameters.trackPath.string() + "' does not exist!"};
else if (!std::filesystem::is_regular_file( _filePath) ) else if (!std::filesystem::is_regular_file( _inputFileParameters.trackPath) )
throw Exception {"File '" + _filePath.string() + "' is not regular!"}; throw Exception {"File '" + _inputFileParameters.trackPath.string() + "' is not regular!"};
} }
catch (const std::filesystem::filesystem_error& e) 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<std::string> args; std::vector<std::string> args;
@@ -90,22 +90,22 @@ Transcoder::start()
args.emplace_back("-ss"); args.emplace_back("-ss");
std::ostringstream oss; 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()); args.emplace_back(oss.str());
} }
// Input file // Input file
args.emplace_back("-i"); args.emplace_back("-i");
args.emplace_back(_filePath.string()); args.emplace_back(_inputFileParameters.trackPath.string());
// Stream mapping, if set // Stream mapping, if set
if (_parameters.stream) if (_transcodeParameters.stream)
{ {
args.emplace_back("-map"); 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 // Strip metadata
args.emplace_back("-map_metadata"); args.emplace_back("-map_metadata");
@@ -117,10 +117,10 @@ Transcoder::start()
// Output bitrates // Output bitrates
args.emplace_back("-b:a"); args.emplace_back("-b:a");
args.emplace_back(std::to_string(_parameters.bitrate)); args.emplace_back(std::to_string(_transcodeParameters.bitrate));
// Codecs and formats // Codecs and formats
switch (_parameters.format) switch (_transcodeParameters.format)
{ {
case Format::MP3: case Format::MP3:
args.emplace_back("-f"); args.emplace_back("-f");
@@ -156,10 +156,10 @@ Transcoder::start()
break; break;
default: default:
throw Exception {"Unhandled format (" + std::to_string(static_cast<int>(_parameters.format)) + ")"}; throw Exception {"Unhandled format (" + std::to_string(static_cast<int>(_transcodeParameters.format)) + ")"};
} }
_outputMimeType = formatToMimetype(_parameters.format); _outputMimeType = formatToMimetype(_transcodeParameters.format);
args.emplace_back("pipe:1"); args.emplace_back("pipe:1");
+5 -5
View File
@@ -32,7 +32,7 @@ namespace Av
class Transcoder class Transcoder
{ {
public: public:
Transcoder(const std::filesystem::path& file, const TranscodeParameters& parameters); Transcoder(const InputFileParameters& inputFileParameters, const TranscodeParameters& transcodeParameters);
~Transcoder(); ~Transcoder();
Transcoder(const Transcoder&) = delete; Transcoder(const Transcoder&) = delete;
@@ -49,7 +49,7 @@ namespace Av
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 _parameters; } const TranscodeParameters& getParameters() const { return _transcodeParameters; }
bool finished() const; bool finished() const;
@@ -58,9 +58,9 @@ namespace Av
void start(); void start();
const std::size_t _id {}; const std::size_t _debugId {};
const std::filesystem::path _filePath; const InputFileParameters _inputFileParameters;
const TranscodeParameters _parameters; const TranscodeParameters _transcodeParameters;
std::unique_ptr<IChildProcess> _childProcess; std::unique_ptr<IChildProcess> _childProcess;
@@ -20,12 +20,19 @@
#pragma once #pragma once
#include <chrono> #include <chrono>
#include <filesystem>
#include <optional> #include <optional>
#include "Types.hpp" #include "Types.hpp"
namespace Av namespace Av
{ {
struct InputFileParameters
{
std::filesystem::path trackPath;
std::chrono::milliseconds duration;
};
struct TranscodeParameters struct TranscodeParameters
{ {
Format format; Format format;
@@ -19,15 +19,15 @@
#pragma once #pragma once
#include <filesystem>
#include <memory> #include <memory>
#include "utils/IResourceHandler.hpp" #include "utils/IResourceHandler.hpp"
namespace Av namespace Av
{ {
struct InputFileParameters;
struct TranscodeParameters; struct TranscodeParameters;
std::unique_ptr<IResourceHandler> createTranscodeResourceHandler(const std::filesystem::path& trackPath, const TranscodeParameters& parameters); std::unique_ptr<IResourceHandler> createTranscodeResourceHandler(const InputFileParameters& inputFileParameters, const TranscodeParameters& parameters, bool estimateContentLength);
} }
+9 -4
View File
@@ -54,8 +54,9 @@ userTranscodeFormatToAvFormat(AudioFormat format)
struct StreamParameters struct StreamParameters
{ {
std::filesystem::path trackPath; Av::InputFileParameters inputFileParameters;
std::optional<Av::TranscodeParameters> transcodeParameters; std::optional<Av::TranscodeParameters> transcodeParameters;
bool estimateContentLength {};
}; };
static static
@@ -68,9 +69,12 @@ getStreamParameters(RequestContext& context)
// Optional params // Optional params
std::optional<std::size_t> maxBitRate {getParameterAs<std::size_t>(context.parameters, "maxBitRate")}; std::optional<std::size_t> maxBitRate {getParameterAs<std::size_t>(context.parameters, "maxBitRate")};
std::optional<std::string> format {getParameterAs<std::string>(context.parameters, "format")}; std::optional<std::string> format {getParameterAs<std::string>(context.parameters, "format")};
bool estimateContentLength {getParameterAs<bool>(context.parameters, "estimateContentLength").value_or(false)};
StreamParameters parameters; StreamParameters parameters;
parameters.estimateContentLength = estimateContentLength;
auto transaction {context.dbSession.createSharedTransaction()}; auto transaction {context.dbSession.createSharedTransaction()};
{ {
@@ -78,7 +82,8 @@ getStreamParameters(RequestContext& context)
if (!track) if (!track)
throw RequestedDataNotFoundError {}; 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)}; StreamParameters streamParameters {getStreamParameters(context)};
if (streamParameters.transcodeParameters) if (streamParameters.transcodeParameters)
resourceHandler = Av::createTranscodeResourceHandler(streamParameters.trackPath, *streamParameters.transcodeParameters); resourceHandler = Av::createTranscodeResourceHandler(streamParameters.inputFileParameters, *streamParameters.transcodeParameters, streamParameters.estimateContentLength);
else else
resourceHandler = createFileResourceHandler(streamParameters.trackPath); resourceHandler = createFileResourceHandler(streamParameters.inputFileParameters.trackPath);
} }
else else
{ {
-13
View File
@@ -136,17 +136,4 @@ namespace StringUtils
return std::nullopt; return std::nullopt;
} }
template<>
std::optional<bool>
readAs(std::string_view str)
{
if (str == "true")
return true;
else if (str == "false")
return false;
return {};
}
} }
-4
View File
@@ -58,9 +58,5 @@ namespace StringUtils
template<> template<>
std::optional<Database::TrackListId> std::optional<Database::TrackListId>
readAs(std::string_view str); readAs(std::string_view str);
template<>
std::optional<bool>
readAs(std::string_view str);
} }
+12
View File
@@ -63,6 +63,18 @@ readAs(std::string_view str)
return std::string {str}; return std::string {str};
} }
template<>
std::optional<bool>
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<std::string> std::vector<std::string>
splitStringCopy(std::string_view string, std::string_view separators) splitStringCopy(std::string_view string, std::string_view separators)
{ {
+5
View File
@@ -85,6 +85,11 @@ template<>
std::optional<std::string> std::optional<std::string>
readAs(std::string_view str); readAs(std::string_view str);
template<>
[[nodiscard]]
std::optional<bool>
readAs(std::string_view str);
[[nodiscard]] [[nodiscard]]
std::string std::string
replaceInString(std::string_view str, const std::string& from, const std::string& to); replaceInString(std::string_view str, const std::string& from, const std::string& to);
+9
View File
@@ -109,3 +109,12 @@ TEST(StringUtils, escapeString)
EXPECT_EQ(StringUtils::escapeString("**||", "*|", '_'), "_*_*_|_|"); EXPECT_EQ(StringUtils::escapeString("**||", "*|", '_'), "_*_*_|_|");
} }
TEST(StringUtils, readAs)
{
EXPECT_EQ(StringUtils::readAs<bool>("true"), true);
EXPECT_EQ(StringUtils::readAs<bool>("1"), true);
EXPECT_EQ(StringUtils::readAs<bool>("false"), false);
EXPECT_EQ(StringUtils::readAs<bool>("0"), false);
EXPECT_EQ(StringUtils::readAs<bool>("foo"), std::nullopt);
EXPECT_EQ(StringUtils::readAs<bool>(""), std::nullopt);
}
+46 -43
View File
@@ -118,59 +118,62 @@ readParameterAs(const Wt::Http::Request& request, const std::string& parameterNa
return res; return res;
} }
struct TranscodeParameters namespace
{ {
std::filesystem::path file; struct TranscodeParameters
Av::TranscodeParameters transcodeParameters;
};
static
std::optional<TranscodeParameters>
readTranscodeParameters(const Wt::Http::Request& request)
{
TranscodeParameters parameters;
// mandatory parameters
const std::optional<Database::TrackId> trackId {readParameterAs<Database::TrackId::ValueType>(request, "trackid")};
const auto format {readParameterAs<Database::AudioFormat>(request, "format")};
const auto bitrate {readParameterAs<Database::Bitrate>(request, "bitrate")};
if (!trackId || !format || !bitrate)
return std::nullopt;
const std::optional<Av::Format> avFormat {AudioFormatToAvFormat(*format)};
if (!avFormat)
return std::nullopt;
// optional parameter
std::size_t offset {readParameterAs<std::size_t>(request, "offset").value_or(0)};
std::filesystem::path trackPath;
{ {
auto transaction {LmsApp->getDbSession().createSharedTransaction()}; Av::InputFileParameters inputFileParameters;
Av::TranscodeParameters transcodeParameters;
};
const Database::Track::pointer track {Database::Track::find(LmsApp->getDbSession(), *trackId)}; std::optional<TranscodeParameters>
if (!track) readTranscodeParameters(const Wt::Http::Request& request)
{ {
LOG(ERROR) << "Missing track"; TranscodeParameters parameters;
// mandatory parameters
const std::optional<Database::TrackId> trackId {readParameterAs<Database::TrackId::ValueType>(request, "trackid")};
const auto format {readParameterAs<Database::AudioFormat>(request, "format")};
const auto bitrate {readParameterAs<Database::Bitrate>(request, "bitrate")};
if (!trackId || !format || !bitrate)
return std::nullopt; return std::nullopt;
}
parameters.file = track->getPath();
if (!Database::isAudioBitrateAllowed(*bitrate)) if (!Database::isAudioBitrateAllowed(*bitrate))
{ {
LOG(ERROR) << "Bitrate '" << *bitrate << "' is not allowed"; LOG(ERROR) << "Bitrate '" << *bitrate << "' is not allowed";
return std::nullopt; return std::nullopt;
} }
const std::optional<Av::Format> avFormat {AudioFormatToAvFormat(*format)};
if (!avFormat)
return std::nullopt;
// optional parameter
std::size_t offset {readParameterAs<std::size_t>(request, "offset").value_or(0)};
std::filesystem::path trackPath;
{
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
const Database::Track::pointer track {Database::Track::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 void
@@ -186,7 +189,7 @@ AudioTranscodeResource::handleRequest(const Wt::Http::Request& request,
{ {
const std::optional<TranscodeParameters>& parameters {readTranscodeParameters(request)}; const std::optional<TranscodeParameters>& parameters {readTranscodeParameters(request)};
if (parameters) if (parameters)
resourceHandler = Av::createTranscodeResourceHandler(parameters->file, parameters->transcodeParameters); resourceHandler = Av::createTranscodeResourceHandler(parameters->inputFileParameters, parameters->transcodeParameters, false /* estimate content length */);
} }
else else
{ {