From c1da3c53973e7a45f1814329aea965dc43bb8f69 Mon Sep 17 00:00:00 2001 From: emeric Date: Sat, 7 Mar 2020 21:49:28 +0100 Subject: [PATCH] Isolate Audio file resource request handler to ease reuse. ref #36 --- src/libs/av/impl/AvTranscoder.cpp | 133 ++++++--------- src/libs/av/include/av/AvTranscoder.hpp | 2 +- src/libs/subsonic/impl/SubsonicResource.cpp | 1 - src/libs/utils/CMakeLists.txt | 1 + src/libs/utils/impl/FileResourceHandler.cpp | 157 ++++++++++++++++++ src/libs/utils/impl/Logger.cpp | 1 + .../include/utils/FileResourceHandler.hpp | 40 +++++ src/libs/utils/include/utils/Logger.hpp | 1 + src/lms/ui/resource/AudioFileResource.cpp | 113 +------------ src/lms/ui/resource/AudioFileResource.hpp | 14 +- 10 files changed, 263 insertions(+), 200 deletions(-) create mode 100644 src/libs/utils/impl/FileResourceHandler.cpp create mode 100644 src/libs/utils/include/utils/FileResourceHandler.hpp diff --git a/src/libs/av/impl/AvTranscoder.cpp b/src/libs/av/impl/AvTranscoder.cpp index b9f82ad3..fd6576f2 100644 --- a/src/libs/av/impl/AvTranscoder.cpp +++ b/src/libs/av/impl/AvTranscoder.cpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2015 Emeric Poupon + * * This file is part of LMS. * @@ -30,7 +30,7 @@ namespace Av { -#define LMS_LOG_TRANSCODE(sev) LMS_LOG(TRANSCODE, sev) << "[" << _id << "] - " +#define LOG(sev) LMS_LOG(TRANSCODE, sev) << "[" << _id << "] - " static std::atomic globalId {}; static std::filesystem::path ffmpegPath; @@ -59,7 +59,7 @@ Transcoder::start() else if (!std::filesystem::is_regular_file( _filePath) ) return false; - LMS_LOG_TRANSCODE(INFO) << "Transcoding file '" << _filePath.string() << "'"; + LOG(INFO) << "Transcoding file '" << _filePath.string() << "'"; std::vector args; @@ -99,84 +99,57 @@ Transcoder::start() // Skip video flows (including covers) args.emplace_back("-vn"); + // Output bitrates + args.emplace_back("-b:a"); + args.emplace_back(std::to_string(_parameters.bitrate)); + // Codecs and formats - if (_parameters.encoding) + switch (_parameters.encoding) { - // Output bitrates - args.emplace_back("-b:a"); - args.emplace_back(std::to_string(_parameters.bitrate)); + case Encoding::MP3: + args.emplace_back("-f"); + args.emplace_back("mp3"); + break; - switch (*_parameters.encoding) - { - case Encoding::MP3: - args.emplace_back("-f"); - args.emplace_back("mp3"); - break; + case Encoding::OGG_OPUS: + args.emplace_back("-acodec"); + args.emplace_back("libopus"); + args.emplace_back("-f"); + args.emplace_back("ogg"); + break; - case Encoding::OGG_OPUS: - args.emplace_back("-acodec"); - args.emplace_back("libopus"); - args.emplace_back("-f"); - args.emplace_back("ogg"); - break; + case Encoding::MATROSKA_OPUS: + args.emplace_back("-acodec"); + args.emplace_back("libopus"); + args.emplace_back("-f"); + args.emplace_back("matroska"); + break; - case Encoding::MATROSKA_OPUS: - args.emplace_back("-acodec"); - args.emplace_back("libopus"); - args.emplace_back("-f"); - args.emplace_back("matroska"); - break; + case Encoding::OGG_VORBIS: + args.emplace_back("-acodec"); + args.emplace_back("libvorbis"); + args.emplace_back("-f"); + args.emplace_back("ogg"); + break; - case Encoding::OGG_VORBIS: - args.emplace_back("-acodec"); - args.emplace_back("libvorbis"); - args.emplace_back("-f"); - args.emplace_back("ogg"); - break; + case Encoding::WEBM_VORBIS: + args.emplace_back("-acodec"); + args.emplace_back("libvorbis"); + args.emplace_back("-f"); + args.emplace_back("webm"); + break; - case Encoding::WEBM_VORBIS: - args.emplace_back("-acodec"); - args.emplace_back("libvorbis"); - args.emplace_back("-f"); - args.emplace_back("webm"); - break; - - default: - return false; - } - - _outputMimeType = encodingToMimetype(*_parameters.encoding); - } - else - { - auto mediaFileFormat {guessMediaFileFormat(_filePath)}; - - if (!mediaFileFormat) - { - LMS_LOG(AV, ERROR) << "Cannot guess media file format for '" << _filePath.string() << "'"; + default: return false; - } - - args.emplace_back("-acodec"); - args.emplace_back("copy"); - args.emplace_back("-f"); - args.emplace_back(mediaFileFormat->format); - - // Workaround for ipod streams: make it compatible with non seekable output - if (mediaFileFormat->format == "ipod") - { - args.emplace_back("-movflags"); - args.emplace_back("frag_keyframe"); - } - - _outputMimeType = mediaFileFormat->mimeType; } + _outputMimeType = encodingToMimetype(_parameters.encoding); + args.emplace_back("pipe:1"); - LMS_LOG_TRANSCODE(DEBUG) << "Dumping args (" << args.size() << ")"; + LOG(DEBUG) << "Dumping args (" << args.size() << ")"; for (const std::string& arg : args) - LMS_LOG_TRANSCODE(DEBUG) << "Arg = '" << arg << "'"; + LOG(DEBUG) << "Arg = '" << arg << "'"; // make sure only one thread is executing this part of code { @@ -190,18 +163,18 @@ Transcoder::start() _child->open(ffmpegPath.string(), args); if (!_child->is_open()) { - LMS_LOG_TRANSCODE(DEBUG) << "Exec failed!"; + LOG(DEBUG) << "Exec failed!"; return false; } if (_child->out().eof()) { - LMS_LOG_TRANSCODE(DEBUG) << "Early end of file!"; + LOG(DEBUG) << "Early end of file!"; return false; } } - LMS_LOG_TRANSCODE(DEBUG) << "Stream opened!"; + LOG(DEBUG) << "Stream opened!"; return true; } @@ -214,12 +187,12 @@ Transcoder::process(std::vector& output, std::size_t maxSize) if (_child->out().fail()) { - LMS_LOG_TRANSCODE(DEBUG) << "Stdout FAILED 2"; + LOG(DEBUG) << "Stdout FAILED 2"; } if (_child->out().eof()) { - LMS_LOG_TRANSCODE(DEBUG) << "Stdout ENDED 2"; + LOG(DEBUG) << "Stdout ENDED 2"; } output.resize(maxSize); @@ -230,12 +203,12 @@ Transcoder::process(std::vector& output, std::size_t maxSize) if (_child->out().fail()) { - LMS_LOG_TRANSCODE(DEBUG) << "Stdout FAILED"; + LOG(DEBUG) << "Stdout FAILED"; } if (_child->out().eof()) { - LMS_LOG_TRANSCODE(DEBUG) << "Stdout EOF!"; + LOG(DEBUG) << "Stdout EOF!"; _child->clear(); _isComplete = true; @@ -244,20 +217,20 @@ Transcoder::process(std::vector& output, std::size_t maxSize) _total += output.size(); - LMS_LOG_TRANSCODE(DEBUG) << "nb bytes = " << output.size() << ", total = " << _total; + LOG(DEBUG) << "nb bytes = " << output.size() << ", total = " << _total; } Transcoder::~Transcoder() { - LMS_LOG_TRANSCODE(DEBUG) << ", ~Transcoder called! Total produced bytes = " << _total; + LOG(DEBUG) << ", ~Transcoder called! Total produced bytes = " << _total; if (_child) { - LMS_LOG_TRANSCODE(DEBUG) << "Child still here!"; + LOG(DEBUG) << "Child still here!"; _child->rdbuf()->kill(SIGKILL); - LMS_LOG_TRANSCODE(DEBUG) << "Closing..."; + LOG(DEBUG) << "Closing..."; _child->rdbuf()->close(); - LMS_LOG_TRANSCODE(DEBUG) << "Closing DONE"; + LOG(DEBUG) << "Closing DONE"; } } diff --git a/src/libs/av/include/av/AvTranscoder.hpp b/src/libs/av/include/av/AvTranscoder.hpp index efe02f9b..60093ebb 100644 --- a/src/libs/av/include/av/AvTranscoder.hpp +++ b/src/libs/av/include/av/AvTranscoder.hpp @@ -33,7 +33,7 @@ namespace Av { struct TranscodeParameters { - std::optional encoding; // If not set, no transcoding is performed + Encoding encoding; std::size_t bitrate {128000}; std::optional stream; // Id of the stream to be transcoded (auto detect by default) std::optional offset; diff --git a/src/libs/subsonic/impl/SubsonicResource.cpp b/src/libs/subsonic/impl/SubsonicResource.cpp index a1f78af0..c538d1eb 100644 --- a/src/libs/subsonic/impl/SubsonicResource.cpp +++ b/src/libs/subsonic/impl/SubsonicResource.cpp @@ -1845,7 +1845,6 @@ handleStream(RequestContext& context, Wt::Http::ResponseContinuation* continuati { MediaRetrievalResult res; - // TODO store only weak ptrs and use a ring container to store shared_ptr? std::shared_ptr transcoder; if (!continuation) diff --git a/src/libs/utils/CMakeLists.txt b/src/libs/utils/CMakeLists.txt index c468be2f..e3cd2e9e 100644 --- a/src/libs/utils/CMakeLists.txt +++ b/src/libs/utils/CMakeLists.txt @@ -1,6 +1,7 @@ add_library(lmsutils SHARED impl/Config.cpp + impl/FileResourceHandler.cpp impl/Logger.cpp impl/NetAddress.cpp impl/Path.cpp diff --git a/src/libs/utils/impl/FileResourceHandler.cpp b/src/libs/utils/impl/FileResourceHandler.cpp new file mode 100644 index 00000000..01628a07 --- /dev/null +++ b/src/libs/utils/impl/FileResourceHandler.cpp @@ -0,0 +1,157 @@ +/* + * 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 . + */ + +#include "utils/FileResourceHandler.hpp" + +#include + +#include "utils/Logger.hpp" + +namespace FileResourceHandler +{ + +static constexpr std::size_t _chunkSize {262144}; + +static +std::optional +handleRequestPiecewise(const Wt::Http::Request& request, + Wt::Http::Response& response, + ContinuationData continuationData) +{ + ::uint64_t startByte {continuationData.offset}; + std::ifstream ifs {continuationData.path.string().c_str(), std::ios::in | std::ios::binary}; + + LMS_LOG(UTILS, DEBUG) << "startByte = " << startByte; + + if (startByte == 0) + { + if (!ifs) + { + LMS_LOG(UTILS, ERROR) << "Cannot open file stream for '" << continuationData.path.string() << "'"; + response.setStatus(404); + return std::nullopt; + } + else + { + response.setStatus(200); + } + + ifs.seekg(0, std::ios::end); + const ::uint64_t fileSize {static_cast<::uint64_t>(ifs.tellg())}; + ifs.seekg(0, std::ios::beg); + + LMS_LOG(UTILS, DEBUG) << "fileSize = " << fileSize; + + const Wt::Http::Request::ByteRangeSpecifier ranges {request.getRanges(fileSize)}; + if (!ranges.isSatisfiable()) + { + std::ostringstream contentRange; + contentRange << "bytes */" << fileSize; + response.setStatus(416); // Requested range not satisfiable + response.addHeader("Content-Range", contentRange.str()); + + LMS_LOG(UTILS, DEBUG) << "Range not satisfiable"; + return std::nullopt; + } + + if (ranges.size() == 1) + { + LMS_LOG(UTILS, DEBUG) << "Range requested = " << ranges[0].firstByte() << "/" << ranges[0].lastByte(); + + response.setStatus(206); + startByte = ranges[0].firstByte(); + continuationData.beyondLastByte = ranges[0].lastByte() + 1; + + std::ostringstream contentRange; + contentRange << "bytes " << startByte << "-" + << continuationData.beyondLastByte - 1 << "/" << fileSize; + + response.addHeader("Content-Range", contentRange.str()); + response.setContentLength(continuationData.beyondLastByte - startByte); + } + else + { + LMS_LOG(UTILS, DEBUG) << "No range requested"; + + continuationData.beyondLastByte = fileSize; + response.setContentLength(continuationData.beyondLastByte); + } + } + + ifs.seekg(static_cast(startByte)); + + std::vector buf; + buf.resize(_chunkSize); + + ::uint64_t restSize = continuationData.beyondLastByte - startByte; + ::uint64_t pieceSize = buf.size() > restSize ? restSize : buf.size(); + + ifs.read(&buf[0], pieceSize); + const ::uint64_t actualPieceSize {static_cast<::uint64_t>(ifs.gcount())}; + response.out().write(&buf[0], actualPieceSize); + + LMS_LOG(UTILS, DEBUG) << "Written " << actualPieceSize << " bytes"; + + LMS_LOG(UTILS, DEBUG) << "Progress: " << actualPieceSize << "/" << restSize; + if (ifs.good() && actualPieceSize < restSize) + { + ContinuationData newContinuationData {continuationData}; + newContinuationData.offset = startByte + actualPieceSize; + + LMS_LOG(UTILS, DEBUG) << "Job not complete! Next chunk offset = " << newContinuationData.offset; + + return newContinuationData; + } + else + { + LMS_LOG(UTILS, DEBUG) << "Job complete!"; + } + + return std::nullopt; +} + + +std::optional +handleInitialRequest(const Wt::Http::Request& request, + Wt::Http::Response& response, + const std::filesystem::path& path) +{ + ContinuationData continuationData; + continuationData.path = path; + continuationData.offset = 0; + continuationData.beyondLastByte = 0; + + LMS_LOG(UTILS, DEBUG) << "Initial request for file '" << path << "'"; + + return handleRequestPiecewise(request, response, continuationData); +} + +std::optional +handleContinuationRequest(const Wt::Http::Request& request, + Wt::Http::Response& response, + const ContinuationData& continuationData) +{ + LMS_LOG(UTILS, DEBUG) << "Continuation request for file '" << continuationData.path << "', offset = " << continuationData.offset; + return handleRequestPiecewise(request, response, continuationData); +} + +} // ns FileResourceHandler + + + diff --git a/src/libs/utils/impl/Logger.cpp b/src/libs/utils/impl/Logger.cpp index 602fdbc3..d19f79b3 100644 --- a/src/libs/utils/impl/Logger.cpp +++ b/src/libs/utils/impl/Logger.cpp @@ -37,6 +37,7 @@ const char* getModuleName(Module mod) case Module::RECOMMENDATION: return "RECOMMENDATION"; case Module::TRANSCODE: return "TRANSCODE"; case Module::UI: return "UI"; + case Module::UTILS: return "UTILS"; } return ""; } diff --git a/src/libs/utils/include/utils/FileResourceHandler.hpp b/src/libs/utils/include/utils/FileResourceHandler.hpp new file mode 100644 index 00000000..fb663032 --- /dev/null +++ b/src/libs/utils/include/utils/FileResourceHandler.hpp @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2020 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#pragma once + +#include +#include +#include + + +// Helper used to deliver file contents from a WResource +namespace FileResourceHandler +{ + struct ContinuationData + { + std::filesystem::path path; + ::uint64_t beyondLastByte; + ::uint64_t offset; + }; + + std::optional handleInitialRequest(const Wt::Http::Request& request, Wt::Http::Response& response, const std::filesystem::path& path); + std::optional handleContinuationRequest(const Wt::Http::Request& request, Wt::Http::Response& response, const ContinuationData& continuationData); +} + diff --git a/src/libs/utils/include/utils/Logger.hpp b/src/libs/utils/include/utils/Logger.hpp index 3b734a59..11789b17 100644 --- a/src/libs/utils/include/utils/Logger.hpp +++ b/src/libs/utils/include/utils/Logger.hpp @@ -49,6 +49,7 @@ enum class Module RECOMMENDATION, TRANSCODE, UI, + UTILS, }; const char* getModuleName(Module mod); diff --git a/src/lms/ui/resource/AudioFileResource.cpp b/src/lms/ui/resource/AudioFileResource.cpp index 88b8c2e0..c2986cec 100644 --- a/src/lms/ui/resource/AudioFileResource.cpp +++ b/src/lms/ui/resource/AudioFileResource.cpp @@ -24,6 +24,7 @@ #include "av/AvInfo.hpp" #include "database/Track.hpp" +#include "utils/FileResourceHandler.hpp" #include "utils/Logger.hpp" #include "utils/String.hpp" #include "LmsApplication.hpp" @@ -87,126 +88,28 @@ void AudioFileResource::handleRequest(const Wt::Http::Request& request, Wt::Http::Response& response) { + std::optional continuationData; if (!request.continuation()) { - LOG(DEBUG) << "Initial request"; - auto trackPath {getTrackPathFromURLArgs(request)}; if (!trackPath) return; - ContinuationData continuationData; - continuationData.path = *trackPath; - continuationData.offset = 0; - continuationData.beyondLastByte = 0; - - { - std::error_code ec; - continuationData.fileSize = std::filesystem::file_size(*trackPath, ec); - - if (ec) - { - LOG(ERROR) << "Cannot get file size for '" << *trackPath << "': " << ec.message(); - return; - } - } - - LOG(DEBUG) << "Initial request. File = '" << continuationData.path << "', size = " << continuationData.fileSize; - - handleRequestPiecewise(request, response, continuationData); - - const auto fileFormat {Av::guessMediaFileFormat(*trackPath)}; - const std::string mimeType {fileFormat ? fileFormat->mimeType : "application/octet-stream"}; - response.setMimeType(mimeType); - - LOG(DEBUG) << "Mime type set to '" << mimeType << "'"; + continuationData = FileResourceHandler::handleInitialRequest(request, response, *trackPath); } else { - ContinuationData continuationData {Wt::cpp17::any_cast(request.continuation()->data())}; - handleRequestPiecewise(request, response, continuationData); + auto currentContinuationData {Wt::cpp17::any_cast(request.continuation()->data())}; + continuationData = FileResourceHandler::handleContinuationRequest(request, response, currentContinuationData); } -} - - -void -AudioFileResource::handleRequestPiecewise(const Wt::Http::Request& request, - Wt::Http::Response& response, - ContinuationData continuationData) -{ - LOG(DEBUG) << "Handling request. File = '" << continuationData.path << "', size = " << continuationData.fileSize << ", offset = " << continuationData.offset << ", beyondLastByte = " << continuationData.beyondLastByte; - - ::uint64_t startByte {continuationData.offset}; - std::ifstream ifs {continuationData.path.string().c_str(), std::ios::in | std::ios::binary}; - - if (startByte == 0) + if (continuationData) { - if (!ifs) - { - response.setStatus(404); - return; - } - else - { - response.setStatus(200); - } - - const Wt::Http::Request::ByteRangeSpecifier ranges {request.getRanges(continuationData.fileSize)}; - if (!ranges.isSatisfiable()) - { - std::ostringstream contentRange; - contentRange << "bytes */" << continuationData.fileSize; - response.setStatus(416); // Requested range not satisfiable - response.addHeader("Content-Range", contentRange.str()); - return; - } - - if (ranges.size() == 1) - { - response.setStatus(206); - startByte = ranges[0].firstByte(); - continuationData.beyondLastByte = ranges[0].lastByte() + 1; - - std::ostringstream contentRange; - contentRange << "bytes " << startByte << "-" - << continuationData.beyondLastByte - 1 << "/" << continuationData.fileSize; - - response.addHeader("Content-Range", contentRange.str()); - response.setContentLength(continuationData.beyondLastByte - startByte); - } - else - { - continuationData.beyondLastByte = continuationData.fileSize; - response.setContentLength(continuationData.beyondLastByte); - } - } - - ifs.seekg(static_cast(startByte)); - - std::vector buf; - buf.resize(_chunkSize); - - ::uint64_t restSize = continuationData.beyondLastByte - startByte; - ::uint64_t pieceSize = buf.size() > restSize ? restSize : buf.size(); - - ifs.read(&buf[0], pieceSize); - const ::uint64_t actualPieceSize {static_cast<::uint64_t>(ifs.gcount())}; - response.out().write(&buf[0], actualPieceSize); - - LOG(DEBUG) << "Written " << actualPieceSize << " bytes!"; - - if (ifs.good() && actualPieceSize < restSize) - { - LOG(DEBUG) << "Still more to do"; - auto* continuation {response.createContinuation()}; - ContinuationData newContinuationData {continuationData}; - newContinuationData.offset = startByte + actualPieceSize; - continuation->setData(newContinuationData); + continuation->setData(*continuationData); } -} +} } // namespace UserInterface diff --git a/src/lms/ui/resource/AudioFileResource.hpp b/src/lms/ui/resource/AudioFileResource.hpp index 0ba77397..9e52a077 100644 --- a/src/lms/ui/resource/AudioFileResource.hpp +++ b/src/lms/ui/resource/AudioFileResource.hpp @@ -38,20 +38,8 @@ class AudioFileResource : public Wt::WResource static constexpr std::size_t _chunkSize {262144}; - struct ContinuationData - { - std::filesystem::path path; - ::uint64_t beyondLastByte; - ::uint64_t fileSize; - ::uint64_t offset; - }; - void handleRequest(const Wt::Http::Request& request, - Wt::Http::Response& response); - - void handleRequestPiecewise(const Wt::Http::Request& request, - Wt::Http::Response& response, - ContinuationData continuationData); + Wt::Http::Response& response) override; };