Now delevering the raw file when transcoding is not enabled (no more remux done via fmmpeg). Fixes #28, Ref #36

This commit is contained in:
emeric
2020-03-07 17:50:03 +01:00
parent ffbb07f295
commit 6ce0d155f6
13 changed files with 637 additions and 244 deletions
+23 -4
View File
@@ -2,6 +2,12 @@
var LMS = LMS || {};
const Mode = {
Transcode: 1,
File: 2,
}
Object.freeze(Mode);
LMS.mediaplayer = function () {
var _root = {};
@@ -9,6 +15,7 @@ LMS.mediaplayer = function () {
var _offset = 0;
var _duration = 0;
var _audioSrc;
var _mode = Mode.File;
var _updateControls = function() {
if (_elems.audio.paused) {
@@ -125,11 +132,22 @@ LMS.mediaplayer = function () {
if (!_elems.audio.hasAttribute("src"))
return;
_offset = parseInt(_elems.seek.value, 10);
_elems.audio.src = _audioSrc + "&offset=" + _offset;
_elems.audio.load();
_playTrack();
let selectedOffset = parseInt(_elems.seek.value, 10);
switch (_mode) {
case Mode.Transcode:
_offset = selectedOffset;
_elems.audio.src = _audioSrc + "&offset=" + _offset;
_elems.audio.load();
_elems.audio.currentTime = 0;
_playTrack();
break;
case Mode.File:
_elems.audio.currentTime = selectedOffset;
_playTrack();
break;
}
});
_elems.audio.addEventListener("play", _updateControls);
@@ -175,6 +193,7 @@ LMS.mediaplayer = function () {
_offset = 0;
_duration = params.duration;
_audioSrc = params.resource;
_mode = params.mode;
_elems.seek.max = _duration;
_elems.audio.src = _audioSrc;
+10
View File
@@ -283,6 +283,16 @@ User::getStarredTracks() const
return std::vector<Wt::Dbo::ptr<Track>>(_starredTracks.begin(), _starredTracks.end());
}
bool
User::checkBitrate(Database::Bitrate bitrate) const
{
if (audioTranscodeAllowedBitrates.find(bitrate) == std::cend(audioTranscodeAllowedBitrates))
return false;
return static_cast<Bitrate>(_maxAudioTranscodeBitrate) >= bitrate;
}
} // namespace Database
@@ -170,6 +170,8 @@ class User : public Wt::Dbo::Dbo<User>
bool hasStarredTrack(Wt::Dbo::ptr<Track> track) const;
std::vector<Wt::Dbo::ptr<Track>> getStarredTracks() const;
bool checkBitrate(Bitrate bitrate) const;
template<class Action>
void persist(Action& a)
{
+2 -1
View File
@@ -29,8 +29,9 @@ add_executable(lms
ui/explore/ReleaseView.cpp
ui/explore/TracksInfoView.cpp
ui/explore/TracksView.cpp
ui/resource/AudioFileResource.cpp
ui/resource/AudioTranscodeResource.cpp
ui/resource/ImageResource.cpp
ui/resource/AudioResource.cpp
)
target_include_directories(lms PRIVATE
+4 -2
View File
@@ -44,8 +44,9 @@
#include "admin/DatabaseSettingsView.hpp"
#include "admin/UserView.hpp"
#include "admin/UsersView.hpp"
#include "resource/AudioFileResource.hpp"
#include "resource/AudioTranscodeResource.hpp"
#include "resource/ImageResource.hpp"
#include "resource/AudioResource.hpp"
#include "Auth.hpp"
#include "LmsApplicationException.hpp"
#include "MediaPlayer.hpp"
@@ -400,7 +401,8 @@ void
LmsApplication::createHome()
{
_imageResource = std::make_shared<ImageResource>();
_audioResource = std::make_shared<AudioResource>();
_audioTranscodeResource = std::make_shared<AudioTranscodeResource>();
_audioFileResource = std::make_shared<AudioFileResource>();
setConfirmCloseMessage(Wt::WString::tr("Lms.quit-confirm"));
+6 -3
View File
@@ -39,7 +39,8 @@ namespace Database {
namespace UserInterface {
class AudioResource;
class AudioTranscodeResource;
class AudioFileResource;
class Auth;
class ImageResource;
class LmsApplicationException;
@@ -82,7 +83,8 @@ class LmsApplication : public Wt::WApplication
// Session application data
std::shared_ptr<ImageResource> getImageResource() { return _imageResource; }
std::shared_ptr<AudioResource> getAudioResource() { return _audioResource; }
std::shared_ptr<AudioTranscodeResource> getAudioTranscodeResource() { return _audioTranscodeResource; }
std::shared_ptr<AudioFileResource> getAudioFileResource() { return _audioFileResource; }
Database::Session& getDbSession() { return _dbSession;}
Wt::Dbo::ptr<Database::User> getUser();
@@ -128,8 +130,9 @@ class LmsApplication : public Wt::WApplication
Events _events;
std::optional<Database::IdType> _userId;
std::optional<bool> _userAuthStrong;
std::shared_ptr<AudioTranscodeResource> _audioTranscodeResource;
std::shared_ptr<AudioFileResource> _audioFileResource;
std::shared_ptr<ImageResource> _imageResource;
std::shared_ptr<AudioResource> _audioResource;
};
+63 -58
View File
@@ -19,15 +19,16 @@
#include "MediaPlayer.hpp"
#include "av/AvInfo.hpp"
#include "utils/Logger.hpp"
#include "database/Artist.hpp"
#include "database/Release.hpp"
#include "database/Track.hpp"
#include "database/User.hpp"
#include "resource/ImageResource.hpp"
#include "resource/AudioResource.hpp"
#include "resource/AudioTranscodeResource.hpp"
#include "resource/AudioFileResource.hpp"
#include "utils/String.hpp"
@@ -57,68 +58,72 @@ MediaPlayer::loadTrack(Database::IdType trackId, bool play)
LMS_LOG(UI, DEBUG) << "Playing track ID = " << trackId;
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
const auto track {Database::Track::getById(LmsApp->getDbSession(), trackId)};
const std::string imgResourceMimeType {LmsApp->getImageResource()->getMimeType()};
try
std::string resource;
bool transcode;
if (LmsApp->getUser()->getAudioTranscodeEnable())
{
const Av::MediaFile mediaFile {track->getPath()};
const std::string resource {LmsApp->getAudioResource()->getUrl(trackId)};
const std::string imgResourceMimeType {LmsApp->getImageResource()->getMimeType()};
const auto artists {track->getArtists()};
std::ostringstream oss;
oss
<< "var params = {"
<< " resource: \"" << resource << "\","
<< " duration: " << std::chrono::duration_cast<std::chrono::seconds>(track->getDuration()).count() << ","
<< " title: \"" << StringUtils::jsEscape(track->getName()) << "\","
<< " artist: \"" << (!artists.empty() ? StringUtils::jsEscape(artists.front()->getName()) : "") << "\","
<< " release: \"" << (track->getRelease() ? StringUtils::jsEscape(track->getRelease()->getName()) : "") << "\","
<< " artwork: ["
<< " { src: \"" << LmsApp->getImageResource()->getTrackUrl(trackId, 96) << "\", sizes: \"96x96\", type: \"" << imgResourceMimeType << "\" },"
<< " { src: \"" << LmsApp->getImageResource()->getTrackUrl(trackId, 256) << "\", sizes: \"256x256\", type: \"" << imgResourceMimeType << "\" },"
<< " { src: \"" << LmsApp->getImageResource()->getTrackUrl(trackId, 512) << "\", sizes: \"512x512\", type: \"" << imgResourceMimeType << "\" },"
<< " ]"
<< "};";
oss << "LMS.mediaplayer.loadTrack(params, " << (play ? "true" : "false") << ")"; // true to autoplay
LMS_LOG(UI, DEBUG) << "Running js = '" << oss.str() << "'";
_title->setTextFormat(Wt::TextFormat::Plain);
_title->setText(Wt::WString::fromUTF8(track->getName()));
if (!artists.empty())
{
_artist->setTextFormat(Wt::TextFormat::Plain);
_artist->setText(Wt::WString::fromUTF8(artists.front()->getName()));
_artist->setLink(LmsApp->createArtistLink(artists.front()));
}
else
{
_artist->setText("");
_artist->setLink({});
}
if (track->getRelease())
{
_release->setTextFormat(Wt::TextFormat::Plain);
_release->setText(Wt::WString::fromUTF8(track->getRelease()->getName()));
_release->setLink(LmsApp->createReleaseLink(track->getRelease()));
}
else
{
_release->setText("");
_release->setLink({});
}
wApp->doJavaScript(oss.str());
resource = LmsApp->getAudioTranscodeResource()->getUrlForUser(trackId, LmsApp->getUser());
transcode = true;
}
catch (Av::MediaFileException& e)
else
{
LMS_LOG(UI, ERROR) << "MediaFileException: " << e.what();
resource = LmsApp->getAudioFileResource()->getUrl(trackId);
transcode = false;
}
const auto artists {track->getArtists()};
std::ostringstream oss;
oss
<< "var params = {"
<< " mode: " << (transcode ? "Mode.Transcode" : "Mode.File") << ","
<< " resource: \"" << resource << "\","
<< " duration: " << std::chrono::duration_cast<std::chrono::seconds>(track->getDuration()).count() << ","
<< " title: \"" << StringUtils::jsEscape(track->getName()) << "\","
<< " artist: \"" << (!artists.empty() ? StringUtils::jsEscape(artists.front()->getName()) : "") << "\","
<< " release: \"" << (track->getRelease() ? StringUtils::jsEscape(track->getRelease()->getName()) : "") << "\","
<< " artwork: ["
<< " { src: \"" << LmsApp->getImageResource()->getTrackUrl(trackId, 96) << "\", sizes: \"96x96\", type: \"" << imgResourceMimeType << "\" },"
<< " { src: \"" << LmsApp->getImageResource()->getTrackUrl(trackId, 256) << "\", sizes: \"256x256\", type: \"" << imgResourceMimeType << "\" },"
<< " { src: \"" << LmsApp->getImageResource()->getTrackUrl(trackId, 512) << "\", sizes: \"512x512\", type: \"" << imgResourceMimeType << "\" },"
<< " ]"
<< "};";
oss << "LMS.mediaplayer.loadTrack(params, " << (play ? "true" : "false") << ")"; // true to autoplay
LMS_LOG(UI, DEBUG) << "Running js = '" << oss.str() << "'";
_title->setTextFormat(Wt::TextFormat::Plain);
_title->setText(Wt::WString::fromUTF8(track->getName()));
if (!artists.empty())
{
_artist->setTextFormat(Wt::TextFormat::Plain);
_artist->setText(Wt::WString::fromUTF8(artists.front()->getName()));
_artist->setLink(LmsApp->createArtistLink(artists.front()));
}
else
{
_artist->setText("");
_artist->setLink({});
}
if (track->getRelease())
{
_release->setTextFormat(Wt::TextFormat::Plain);
_release->setText(Wt::WString::fromUTF8(track->getRelease()->getName()));
_release->setLink(LmsApp->createReleaseLink(track->getRelease()));
}
else
{
_release->setText("");
_release->setLink({});
}
wApp->doJavaScript(oss.str());
}
void
+213
View File
@@ -0,0 +1,213 @@
/*
* 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 "AudioFileResource.hpp"
#include <fstream>
#include <Wt/Http/Response.h>
#include "av/AvInfo.hpp"
#include "database/Track.hpp"
#include "utils/Logger.hpp"
#include "utils/String.hpp"
#include "LmsApplication.hpp"
namespace UserInterface {
#define LOG(level) LMS_LOG(UI, level) << "Audio file resource: "
AudioFileResource:: ~AudioFileResource()
{
beingDeleted();
}
std::string
AudioFileResource::getUrl(Database::IdType trackId) const
{
return url()+ "&trackid=" + std::to_string(trackId);
}
static
std::optional<std::filesystem::path>
getTrackPathFromTrackId(Database::IdType trackId)
{
// DbSession are not thread safe
Wt::WApplication::UpdateLock lock {LmsApp};
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
const Database::Track::pointer track {Database::Track::getById(LmsApp->getDbSession(), trackId)};
if (!track)
{
LOG(ERROR) << "Missing track";
return std::nullopt;
}
return track->getPath();
}
static
std::optional<std::filesystem::path>
getTrackPathFromURLArgs(const Wt::Http::Request& request)
{
const std::string* trackIdParameter {request.getParameter("trackid")};
if (!trackIdParameter)
{
LOG(ERROR) << "Missing trackid URL parameter!";
return std::nullopt;
}
const auto trackId {StringUtils::readAs<Database::IdType>(*trackIdParameter)};
if (!trackId)
{
LOG(ERROR) << "Bad trackid URL parameter!";
return std::nullopt;
}
return getTrackPathFromTrackId(*trackId);
}
void
AudioFileResource::handleRequest(const Wt::Http::Request& request,
Wt::Http::Response& response)
{
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 << "'";
}
else
{
ContinuationData continuationData {Wt::cpp17::any_cast<ContinuationData>(request.continuation()->data())};
handleRequestPiecewise(request, response, continuationData);
}
}
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 (!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<std::istream::pos_type>(startByte));
std::vector<char> 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);
}
}
} // namespace UserInterface
+62
View File
@@ -0,0 +1,62 @@
/*
* 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/>.
*/
#pragma once
#include <filesystem>
#include <Wt/WResource.h>
#include "database/Types.hpp"
namespace UserInterface {
class AudioFileResource : public Wt::WResource
{
public:
~AudioFileResource();
std::string getUrl(Database::IdType trackId) const;
private:
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);
};
} // namespace UserInterface
-170
View File
@@ -1,170 +0,0 @@
/*
* Copyright (C) 2015 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 "AudioResource.hpp"
#include <Wt/Http/Response.h>
#include "utils/Logger.hpp"
#include "database/Track.hpp"
#include "database/User.hpp"
#include "LmsApplication.hpp"
namespace UserInterface {
AudioResource:: ~AudioResource()
{
beingDeleted();
}
std::string
AudioResource::getUrl(Database::IdType trackId) const
{
return url()+ "&trackid=" + std::to_string(trackId);
}
void
AudioResource::handleRequest(const Wt::Http::Request& request,
Wt::Http::Response& response)
{
std::shared_ptr<Av::Transcoder> transcoder;
// First, see if this request is for a continuation
Wt::Http::ResponseContinuation *continuation = request.continuation();
if (continuation)
{
LMS_LOG(UI, DEBUG) << "Continuation! " << continuation ;
transcoder = Wt::cpp17::any_cast<std::shared_ptr<Av::Transcoder>>(continuation->data());
}
else
{
Database::IdType trackId;
Av::TranscodeParameters parameters {};
parameters.stripMetadata = true;
LMS_LOG(UI, DEBUG) << "First request: creating transcoder";
try
{
auto trackIdStr = request.getParameter("trackid");
if (!trackIdStr)
{
LMS_LOG(UI, ERROR) << "Missing trackid transcode parameter!";
return;
}
trackId = std::stol(*request.getParameter("trackid"));
auto offsetStr = request.getParameter("offset");
parameters.offset = std::chrono::seconds(offsetStr ? std::stol(*offsetStr) : 0);
}
catch (std::exception &e)
{
LMS_LOG(UI, ERROR) << "Exception while handling URL parameters: " << e.what();
return;
}
// DbSession are not thread safe
{
Wt::WApplication::UpdateLock lock(LmsApp);
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
const Database::Track::pointer track {Database::Track::getById(LmsApp->getDbSession(), trackId)};
if (!track)
{
LMS_LOG(UI, ERROR) << "Missing track";
return;
}
if (LmsApp->getUser()->getAudioTranscodeEnable())
{
parameters.bitrate = LmsApp->getUser()->getAudioTranscodeBitrate();
switch (LmsApp->getUser()->getAudioTranscodeFormat())
{
case Database::AudioFormat::MP3:
parameters.encoding = Av::Encoding::MP3;
break;
case Database::AudioFormat::OGG_OPUS:
parameters.encoding = Av::Encoding::OGG_OPUS;
break;
case Database::AudioFormat::MATROSKA_OPUS:
parameters.encoding = Av::Encoding::MATROSKA_OPUS;
break;
case Database::AudioFormat::OGG_VORBIS:
parameters.encoding = Av::Encoding::OGG_VORBIS;
break;
case Database::AudioFormat::WEBM_VORBIS:
parameters.encoding = Av::Encoding::WEBM_VORBIS;
break;
default:
parameters.encoding = Av::Encoding::OGG_OPUS;
break;
}
}
else
parameters.bitrate = 0;
transcoder = std::make_shared<Av::Transcoder>(track->getPath(), parameters);
}
if (!transcoder->start())
{
LMS_LOG(UI, ERROR) << "Cannot start transcoder";
return;
}
LMS_LOG(UI, DEBUG) << "Transcoder started";
std::string mimeType {transcoder->getOutputMimeType()};
response.setMimeType(mimeType);
LMS_LOG(UI, DEBUG) << "Mime type set to '" << mimeType << "'";
}
if (!transcoder->isComplete())
{
std::vector<unsigned char> data;
data.reserve(_chunkSize);
transcoder->process(data, _chunkSize);
response.out().write(reinterpret_cast<char*>(&data[0]), data.size());
LMS_LOG(UI, DEBUG) << "Written " << data.size() << " bytes! complete = " << (transcoder->isComplete() ? "true" : "false");
if (!response.out())
{
LMS_LOG(UI, ERROR) << "Write failed!";
}
}
if (!transcoder->isComplete() && response.out())
{
continuation = response.createContinuation();
continuation->setData(transcoder);
}
else
LMS_LOG(UI, DEBUG) << "No more data!";
}
} // namespace UserInterface
@@ -0,0 +1,224 @@
/*
* Copyright (C) 2015 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 "AudioTranscodeResource.hpp"
#include <Wt/Http/Response.h>
#include "av/AvTranscoder.hpp"
#include "database/Track.hpp"
#include "database/User.hpp"
#include "utils/Logger.hpp"
#include "utils/String.hpp"
#include "LmsApplication.hpp"
#define LOG(level) LMS_LOG(UI, level) << "Audio transcode resource: "
namespace StringUtils
{
template <>
std::optional<Database::AudioFormat>
readAs(const std::string& str)
{
auto encodedFormat {readAs<int>(str)};
if (!encodedFormat)
return std::nullopt;
Database::AudioFormat format {static_cast<Database::AudioFormat>(*encodedFormat)};
// check
switch (static_cast<Database::AudioFormat>(*encodedFormat))
{
case Database::AudioFormat::MP3:
[[fallthrough]];
case Database::AudioFormat::OGG_OPUS:
[[fallthrough]];
case Database::AudioFormat::MATROSKA_OPUS:
[[fallthrough]];
case Database::AudioFormat::OGG_VORBIS:
[[fallthrough]];
case Database::AudioFormat::WEBM_VORBIS:
return format;
}
LOG(ERROR) << "Cannot determine audio format from value '" << str << "'";
return std::nullopt;
}
}
namespace UserInterface {
AudioTranscodeResource:: ~AudioTranscodeResource()
{
beingDeleted();
}
static
std::optional<Av::Encoding>
AudioFormatToAvEncoding(Database::AudioFormat format)
{
switch (format)
{
case Database::AudioFormat::MP3: return Av::Encoding::MP3;
case Database::AudioFormat::OGG_OPUS: return Av::Encoding::OGG_OPUS;
case Database::AudioFormat::MATROSKA_OPUS: return Av::Encoding::MATROSKA_OPUS;
case Database::AudioFormat::OGG_VORBIS: return Av::Encoding::OGG_VORBIS;
case Database::AudioFormat::WEBM_VORBIS: return Av::Encoding::WEBM_VORBIS;
}
LOG(ERROR) << "Cannot convert from audio format to encoding";
return std::nullopt;
}
std::string
AudioTranscodeResource::getUrlForUser(Database::IdType trackId, Database::User::pointer user) const
{
std::string computedUrl {url() + "&trackid=" + std::to_string(trackId)};
computedUrl += "&format=" + std::to_string(static_cast<std::size_t>(user->getAudioTranscodeFormat()));
computedUrl += "&bitrate=" + std::to_string(LmsApp->getUser()->getAudioTranscodeBitrate());
return computedUrl;
}
template<typename T>
std::optional<T>
readParameterAs(const Wt::Http::Request& request, const std::string& parameterName)
{
auto paramStr {request.getParameter(parameterName)};
if (!paramStr)
{
LOG(ERROR) << "Missing parameter '" << parameterName << "'";
return std::nullopt;
}
auto res {StringUtils::readAs<T>(*paramStr)};
if (!res)
LOG(ERROR) << "Cannot parse parameter '" << parameterName << "' from value '" << *paramStr << "'";
return res;
}
void
AudioTranscodeResource::handleRequest(const Wt::Http::Request& request,
Wt::Http::Response& response)
{
std::shared_ptr<Av::Transcoder> transcoder;
// First, see if this request is for a continuation
Wt::Http::ResponseContinuation *continuation = request.continuation();
if (continuation)
{
LOG(DEBUG) << "Continuation! " << continuation ;
transcoder = Wt::cpp17::any_cast<std::shared_ptr<Av::Transcoder>>(continuation->data());
}
else
{
LOG(DEBUG) << "First request: creating transcoder";
// mandatory parameters
auto trackId {readParameterAs<Database::IdType>(request, "trackid")};
auto format {readParameterAs<Database::AudioFormat>(request, "format")};
auto bitrate {readParameterAs<Database::Bitrate>(request, "bitrate")};
if (!trackId || !format || !bitrate)
return;
auto encoding {AudioFormatToAvEncoding(*format)};
if (!encoding)
return;
// optional parameter
auto offset {readParameterAs<std::size_t>(request, "offset")};
std::filesystem::path trackPath;
{
// DbSession are not thread safe
Wt::WApplication::UpdateLock lock(LmsApp);
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
const Database::Track::pointer track {Database::Track::getById(LmsApp->getDbSession(), *trackId)};
if (!track)
{
LOG(ERROR) << "Missing track";
return;
}
trackPath = track->getPath();
if (!LmsApp->getUser()->checkBitrate(*bitrate))
{
LOG(ERROR) << "Bitrate '" << *bitrate << "' is not allowed";
return;
}
}
Av::TranscodeParameters parameters {};
parameters.stripMetadata = true;
parameters.encoding = *encoding;
parameters.bitrate = *bitrate;
parameters.offset = std::chrono::seconds {offset ? *offset : 0};
transcoder = std::make_shared<Av::Transcoder>(trackPath, parameters);
if (!transcoder->start())
{
LOG(ERROR) << "Cannot start transcoder";
return;
}
LOG(DEBUG) << "Transcoder started";
std::string mimeType {transcoder->getOutputMimeType()};
response.setMimeType(mimeType);
LOG(DEBUG) << "Mime type set to '" << mimeType << "'";
}
if (!transcoder->isComplete())
{
std::vector<unsigned char> data;
data.reserve(_chunkSize);
transcoder->process(data, _chunkSize);
response.out().write(reinterpret_cast<char*>(&data[0]), data.size());
LOG(DEBUG) << "Written " << data.size() << " bytes! complete = " << (transcoder->isComplete() ? "true" : "false");
if (!response.out())
{
LOG(ERROR) << "Write failed!";
}
}
if (!transcoder->isComplete() && response.out())
{
continuation = response.createContinuation();
continuation->setData(transcoder);
}
else
LOG(DEBUG) << "No more data!";
}
} // namespace UserInterface
@@ -19,28 +19,32 @@
#pragma once
#include <Wt/Dbo/ptr.h>
#include <Wt/WResource.h>
#include "av/AvTranscoder.hpp"
#include "database/Types.hpp"
namespace Database
{
class User;
}
namespace UserInterface {
class AudioResource : public Wt::WResource
class AudioTranscodeResource : public Wt::WResource
{
public:
~AudioResource();
~AudioTranscodeResource();
std::string getUrl(Database::IdType trackId) const;
// Url depends on the user since settings are used in parameters
std::string getUrlForUser(Database::IdType trackId, Wt::Dbo::ptr<Database::User> user) const;
void handleRequest(const Wt::Http::Request& request,
Wt::Http::Response& response);
private:
static const std::size_t _chunkSize = 65536*4;
static constexpr std::size_t _chunkSize {262144};
};
} // namespace UserInterface
+18
View File
@@ -31,6 +31,8 @@
#include "LmsApplication.hpp"
#define LOG(level) LMS_LOG(UI, level) << "Image resource: "
namespace UserInterface {
ImageResource::~ImageResource()
@@ -60,19 +62,30 @@ ImageResource::handleRequest(const Wt::Http::Request& request, Wt::Http::Respons
// Mandatory parameter size
if (!sizeStr)
{
LOG(DEBUG) << "no size provided!";
return;
}
const auto size {StringUtils::readAs<std::size_t>(*sizeStr)};
if (!size || *size > maxSize)
{
LOG(DEBUG) << "invalid size provided!";
return;
}
std::vector<uint8_t> cover;
if (trackIdStr)
{
LOG(DEBUG) << "Requested cover for track " << *trackIdStr << ", size = " << *size;
const auto trackId {StringUtils::readAs<Database::IdType>(*trackIdStr)};
if (!trackId)
{
LOG(DEBUG) << "track not found";
return;
}
// DbSession are not thread safe
{
@@ -82,6 +95,8 @@ ImageResource::handleRequest(const Wt::Http::Request& request, Wt::Http::Respons
}
else if (releaseIdStr)
{
LOG(DEBUG) << "Requested cover for release " << *releaseIdStr << ", size = " << *size;
const auto releaseId {StringUtils::readAs<Database::IdType>(*releaseIdStr)};
if (!releaseId)
return;
@@ -93,7 +108,10 @@ ImageResource::handleRequest(const Wt::Http::Request& request, Wt::Http::Respons
}
}
else
{
LOG(DEBUG) << "No track or release provided";
return;
}
response.setMimeType(getMimeType());