mirror of https://code.videolan.org/videolan/vlc
Merge branch 'upnp_server' into 'master'
UPnP Server module implementation See merge request videolan/vlc!269
This commit is contained in:
commit
e143bc9e49
|
@ -0,0 +1,262 @@
|
|||
/*****************************************************************************
|
||||
* FileHandler.cpp
|
||||
*****************************************************************************
|
||||
* Copyright © 2024 VLC authors and VideoLAN
|
||||
*
|
||||
* Authors: Alaric Senat <alaric@videolabs.io>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation; either version 2.1 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with this program; if not, write to the Free Software Foundation,
|
||||
* Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
|
||||
*****************************************************************************/
|
||||
#ifdef HAVE_CONFIG_H
|
||||
#include "config.h"
|
||||
#endif
|
||||
|
||||
#include <ctime>
|
||||
#include <sstream>
|
||||
|
||||
#include <vlc_common.h>
|
||||
|
||||
#include <vlc_addons.h>
|
||||
#include <vlc_cxx_helpers.hpp>
|
||||
#include <vlc_fourcc.h>
|
||||
#include <vlc_interface.h>
|
||||
#include <vlc_player.h>
|
||||
#include <vlc_rand.h>
|
||||
#include <vlc_stream.h>
|
||||
#include <vlc_stream_extractor.h>
|
||||
#include <vlc_url.h>
|
||||
|
||||
#include "FileHandler.hpp"
|
||||
#include "ml.hpp"
|
||||
#include "utils.hpp"
|
||||
|
||||
// static constexpr char DLNA_TRANSFER_MODE[] = "transfermode.dlna.org";
|
||||
static constexpr char DLNA_CONTENT_FEATURE[] = "contentfeatures.dlna.org";
|
||||
static constexpr char DLNA_TIME_SEEK_RANGE[] = "timeseekrange.dlna.org";
|
||||
|
||||
/// Convenient C++ replacement of vlc_ml_file_t to not have to deal with allocations
|
||||
struct MLFile
|
||||
{
|
||||
std::string mrl;
|
||||
int64_t size;
|
||||
time_t last_modification;
|
||||
};
|
||||
|
||||
/// Usual filesystem FileHandler implementation, this is the most commonly used FileHandler, for
|
||||
/// non-transcoded local medias, thumbnails and subs.
|
||||
class MLFileHandler : public FileHandler
|
||||
{
|
||||
public:
|
||||
MLFile file;
|
||||
utils::MimeType mime_type;
|
||||
|
||||
std::unique_ptr<stream_t, decltype(&vlc_stream_Delete)> stream = {nullptr, &vlc_stream_Delete};
|
||||
|
||||
MLFileHandler(MLFile &&file, utils::MimeType &&mime_type) :
|
||||
file(std::move(file)),
|
||||
mime_type(std::move(mime_type))
|
||||
{}
|
||||
|
||||
bool get_info(UpnpFileInfo &info) final
|
||||
{
|
||||
UpnpFileInfo_set_ContentType(&info, mime_type.combine().c_str());
|
||||
UpnpFileInfo_set_FileLength(&info, file.size);
|
||||
UpnpFileInfo_set_LastModified(&info, file.last_modification);
|
||||
|
||||
// const_cast is expected by the design of the upnp api as it only serves const list heads
|
||||
// FIXME: see if there's no way to patch that in libupnp, we shouldn't have to break const
|
||||
// to do something so usual.
|
||||
auto *head = const_cast<UpnpListHead *>(UpnpFileInfo_get_ExtraHeadersList(&info));
|
||||
utils::http::add_response_hdr(head, {DLNA_CONTENT_FEATURE, "DLNA.ORG_OP=01"});
|
||||
return true;
|
||||
}
|
||||
|
||||
bool open(vlc_object_t *parent) final
|
||||
{
|
||||
stream = vlc::wrap_cptr(vlc_stream_NewMRL(parent, file.mrl.c_str()), &vlc_stream_Delete);
|
||||
return stream != nullptr;
|
||||
}
|
||||
|
||||
size_t read(uint8_t buffer[], size_t buffer_len) noexcept final
|
||||
{
|
||||
return vlc_stream_Read(stream.get(), buffer, buffer_len);
|
||||
}
|
||||
|
||||
bool seek(SeekType type, off_t offset) noexcept final
|
||||
{
|
||||
uint64_t real_offset;
|
||||
switch (type)
|
||||
{
|
||||
case SeekType::Current:
|
||||
real_offset = vlc_stream_Tell(stream.get()) + offset;
|
||||
break;
|
||||
case SeekType::End:
|
||||
if (vlc_stream_GetSize(stream.get(), &real_offset) != VLC_SUCCESS)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
real_offset += offset;
|
||||
break;
|
||||
case SeekType::Set:
|
||||
real_offset = offset;
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
return vlc_stream_Seek(stream.get(), real_offset) == 0;
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Url parsing and FileHandler Factory
|
||||
//
|
||||
|
||||
template <typename MLHelper>
|
||||
auto get_ml_object(const std::string &token,
|
||||
std::string &extension,
|
||||
const ml::MediaLibraryContext &ml)
|
||||
{
|
||||
const auto extension_idx = token.find('.');
|
||||
extension = token.substr(extension_idx + 1);
|
||||
|
||||
try
|
||||
{
|
||||
const int64_t ml_id = std::stoll(token.substr(0, extension_idx));
|
||||
return MLHelper::get(ml, ml_id);
|
||||
}
|
||||
catch (const std::exception &)
|
||||
{
|
||||
return typename MLHelper::Ptr{nullptr, nullptr};
|
||||
}
|
||||
}
|
||||
|
||||
static std::unique_ptr<FileHandler> parse_media_url(std::stringstream &ss,
|
||||
const ml::MediaLibraryContext &ml)
|
||||
{
|
||||
std::string token;
|
||||
std::getline(ss, token, '/');
|
||||
|
||||
if (token != "native")
|
||||
{
|
||||
// TODO Select a transcode profile
|
||||
}
|
||||
|
||||
std::getline(ss, token);
|
||||
|
||||
std::string extension;
|
||||
const auto media = get_ml_object<ml::Media>(token, extension, ml);
|
||||
if (media == nullptr)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const auto main_files = utils::get_media_files(*media, VLC_ML_FILE_TYPE_MAIN);
|
||||
if (main_files.empty())
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
const vlc_ml_file_t &main_file = main_files.front();
|
||||
|
||||
auto mime_type = utils::get_mimetype(media->i_type, extension);
|
||||
auto ret = std::make_unique<MLFileHandler>(
|
||||
MLFile{main_file.psz_mrl, main_file.i_size, main_file.i_last_modification_date},
|
||||
std::move(mime_type));
|
||||
return ret;
|
||||
}
|
||||
|
||||
static std::unique_ptr<FileHandler> parse_thumbnail_url(std::stringstream &ss,
|
||||
const ml::MediaLibraryContext &ml)
|
||||
{
|
||||
std::string token;
|
||||
std::getline(ss, token, '/');
|
||||
vlc_ml_thumbnail_size_t size;
|
||||
if (token == "small")
|
||||
size = VLC_ML_THUMBNAIL_SMALL;
|
||||
else if (token == "banner")
|
||||
size = VLC_ML_THUMBNAIL_BANNER;
|
||||
else
|
||||
return nullptr;
|
||||
|
||||
std::getline(ss, token, '/');
|
||||
std::string extension;
|
||||
std::string mrl;
|
||||
if (token == "media")
|
||||
{
|
||||
std::getline(ss, token);
|
||||
const auto media = get_ml_object<ml::Media>(token, extension, ml);
|
||||
if (media && media->thumbnails[size].i_status == VLC_ML_THUMBNAIL_STATUS_AVAILABLE)
|
||||
mrl = media->thumbnails[size].psz_mrl;
|
||||
}
|
||||
else if (token == "album")
|
||||
{
|
||||
std::getline(ss, token);
|
||||
const auto album = get_ml_object<ml::Album>(token, extension, ml);
|
||||
if (album && album->thumbnails[size].i_status == VLC_ML_THUMBNAIL_STATUS_AVAILABLE)
|
||||
mrl = album->thumbnails[size].psz_mrl;
|
||||
}
|
||||
|
||||
if (mrl.empty())
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return std::make_unique<MLFileHandler>(MLFile{mrl, -1, 0}, utils::MimeType{"image", extension});
|
||||
}
|
||||
|
||||
static std::unique_ptr<FileHandler> parse_subtitle_url(std::stringstream &ss,
|
||||
const ml::MediaLibraryContext &ml)
|
||||
{
|
||||
std::string token;
|
||||
std::string extension;
|
||||
std::string mrl;
|
||||
|
||||
std::getline(ss, token);
|
||||
const auto media = get_ml_object<ml::Media>(token, extension, ml);
|
||||
if (media == nullptr)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const auto subtitles = utils::get_media_files(*media, VLC_ML_FILE_TYPE_SUBTITLE);
|
||||
if (subtitles.empty())
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const vlc_ml_file_t &sub = subtitles.front();
|
||||
return std::make_unique<MLFileHandler>(
|
||||
MLFile{sub.psz_mrl, sub.i_size, sub.i_last_modification_date},
|
||||
utils::MimeType{"text", extension});
|
||||
}
|
||||
|
||||
std::unique_ptr<FileHandler> parse_url(const char *url, const ml::MediaLibraryContext &ml)
|
||||
{
|
||||
std::stringstream ss(url);
|
||||
|
||||
std::string token;
|
||||
std::getline(ss, token, '/');
|
||||
if (!token.empty())
|
||||
return nullptr;
|
||||
|
||||
std::getline(ss, token, '/');
|
||||
if (token == "media")
|
||||
return parse_media_url(ss, ml);
|
||||
else if (token == "thumbnail")
|
||||
return parse_thumbnail_url(ss, ml);
|
||||
else if (token == "subtitle")
|
||||
return parse_subtitle_url(ss, ml);
|
||||
return nullptr;
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
/*****************************************************************************
|
||||
* FileHandler.hpp : UPnP server module header
|
||||
*****************************************************************************
|
||||
* Copyright © 2021 VLC authors and VideoLAN
|
||||
*
|
||||
* Authors: Alaric Senat <alaric@videolabs.io>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation; either version 2.1 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with this program; if not, write to the Free Software Foundation,
|
||||
* Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
|
||||
*****************************************************************************/
|
||||
#ifndef FILEHANDLER_HPP
|
||||
#define FILEHANDLER_HPP
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include <upnp.h>
|
||||
#if UPNP_VERSION >= 11400
|
||||
#include <upnp/UpnpFileInfo.h>
|
||||
#else
|
||||
#include <upnp/FileInfo.h>
|
||||
#endif
|
||||
|
||||
struct vlc_object_t;
|
||||
|
||||
namespace ml
|
||||
{
|
||||
struct MediaLibraryContext;
|
||||
};
|
||||
|
||||
/// This interface reflects the common behaviour of upnp's file handlers.
|
||||
/// The FileHandler is used to serve the content of a named file to the http server.
|
||||
/// The file can be whatever: present on the fs, live streamed, etc.
|
||||
class FileHandler
|
||||
{
|
||||
public:
|
||||
virtual bool get_info(UpnpFileInfo &info) = 0;
|
||||
virtual bool open(vlc_object_t *) = 0;
|
||||
virtual size_t read(uint8_t[], size_t) noexcept = 0;
|
||||
|
||||
enum class SeekType : int
|
||||
{
|
||||
Set = SEEK_SET,
|
||||
Current = SEEK_CUR,
|
||||
End = SEEK_END
|
||||
};
|
||||
virtual bool seek(SeekType, off_t) noexcept = 0;
|
||||
|
||||
virtual ~FileHandler() = default;
|
||||
};
|
||||
|
||||
/// Parses the url and return the needed FileHandler implementation
|
||||
/// All the informations about what FileHandler implementation is to be chosen is locatied either in
|
||||
/// the url
|
||||
std::unique_ptr<FileHandler> parse_url(const char *url, const ml::MediaLibraryContext &);
|
||||
|
||||
#endif /* FILEHANDLER_HPP */
|
|
@ -0,0 +1,66 @@
|
|||
/*****************************************************************************
|
||||
* Container.hpp : CDS Container interface
|
||||
*****************************************************************************
|
||||
* Copyright © 2024 VLC authors and VideoLAN
|
||||
*
|
||||
* Authors: Alaric Senat <alaric@videolabs.io>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation; either version 2.1 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with this program; if not, write to the Free Software Foundation,
|
||||
* Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
|
||||
*****************************************************************************/
|
||||
#ifndef CONTAINER_HPP
|
||||
#define CONTAINER_HPP
|
||||
|
||||
#include "Object.hpp"
|
||||
|
||||
#include <vlc_media_library.h>
|
||||
namespace cds
|
||||
{
|
||||
/// Opaque CDS container type
|
||||
/// Specs: http://www.upnp.org/specs/av/UPnP-av-ContentDirectory-v3-Service-20080930.pdf
|
||||
/// "2.2.8 - Container"
|
||||
class Container : public Object
|
||||
{
|
||||
public:
|
||||
struct BrowseParams
|
||||
{
|
||||
uint32_t offset;
|
||||
uint32_t requested;
|
||||
|
||||
vlc_ml_query_params_t to_query_params() const {
|
||||
vlc_ml_query_params_t query_params = vlc_ml_query_params_create();
|
||||
query_params.i_nbResults = requested;
|
||||
query_params.i_offset = offset;
|
||||
return query_params;
|
||||
}
|
||||
};
|
||||
|
||||
struct BrowseStats
|
||||
{
|
||||
size_t result_count;
|
||||
size_t total_matches;
|
||||
};
|
||||
|
||||
Container(const int64_t id,
|
||||
const int64_t parent_id,
|
||||
const char *name) noexcept :
|
||||
Object(id, parent_id, name, Object::Type::Container) {}
|
||||
|
||||
/// Go through all the container children and dump them to the given xml element
|
||||
virtual BrowseStats
|
||||
browse_direct_children(xml::Element &, BrowseParams, const Object::ExtraId &) const = 0;
|
||||
};
|
||||
} // namespace cds
|
||||
|
||||
#endif /* CONTAINER_HPP */
|
|
@ -0,0 +1,78 @@
|
|||
/*****************************************************************************
|
||||
* FixedContainer.cpp
|
||||
*****************************************************************************
|
||||
* Copyright © 2024 VLC authors and VideoLAN
|
||||
*
|
||||
* Authors: Alaric Senat <alaric@videolabs.io>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation; either version 2.1 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with this program; if not, write to the Free Software Foundation,
|
||||
* Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
|
||||
*****************************************************************************/
|
||||
#ifdef HAVE_CONFIG_H
|
||||
#include "config.h"
|
||||
#endif
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include "FixedContainer.hpp"
|
||||
|
||||
namespace cds
|
||||
{
|
||||
|
||||
FixedContainer::FixedContainer(const int64_t id,
|
||||
const int64_t parent_id,
|
||||
const char *name,
|
||||
std::initializer_list<ObjRef> children) :
|
||||
Container(id, parent_id, name),
|
||||
children(children)
|
||||
{}
|
||||
|
||||
FixedContainer::FixedContainer(const int64_t id, const int64_t parent_id, const char *name)
|
||||
:
|
||||
Container(id, parent_id, name)
|
||||
{}
|
||||
|
||||
Container::BrowseStats FixedContainer::browse_direct_children(xml::Element &dest,
|
||||
BrowseParams params,
|
||||
const Object::ExtraId &extra) const
|
||||
{
|
||||
params.requested =
|
||||
std::min(static_cast<size_t>(params.offset) + params.requested, children.size());
|
||||
|
||||
unsigned i = 0;
|
||||
for (; i + params.offset < params.requested; ++i)
|
||||
{
|
||||
const Object &child = children.at(i + params.offset);
|
||||
dest.add_child(child.browse_metadata(dest.owner, extra));
|
||||
}
|
||||
return {i, children.size()};
|
||||
}
|
||||
|
||||
void FixedContainer::dump_metadata(xml::Element &dest, const Object::ExtraId &) const
|
||||
{
|
||||
dest.set_attribute("childCount", std::to_string(children.size()).c_str());
|
||||
|
||||
xml::Document &doc = dest.owner;
|
||||
dest.add_child(doc.create_element("upnp:class", doc.create_text_node("object.container")));
|
||||
}
|
||||
|
||||
void FixedContainer::add_children(std::initializer_list<ObjRef> l)
|
||||
{
|
||||
for (Object &child : l)
|
||||
{
|
||||
child.parent_id = id;
|
||||
}
|
||||
children.insert(children.end(), l.begin(), l.end());
|
||||
}
|
||||
} // namespace cds
|
|
@ -0,0 +1,53 @@
|
|||
/*****************************************************************************
|
||||
* FixedContainer.hpp : Simple Container implementation
|
||||
*****************************************************************************
|
||||
* Copyright © 2024 VLC authors and VideoLAN
|
||||
*
|
||||
* Authors: Alaric Senat <alaric@videolabs.io>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation; either version 2.1 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with this program; if not, write to the Free Software Foundation,
|
||||
* Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
|
||||
*****************************************************************************/
|
||||
#ifndef FIXEDCONTAINER_HPP
|
||||
#define FIXEDCONTAINER_HPP
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "Container.hpp"
|
||||
|
||||
namespace cds
|
||||
{
|
||||
/// Simplest Container implementation, it is fixed in the object hierarchy and simply list other
|
||||
/// Objects
|
||||
struct FixedContainer : public Container
|
||||
{
|
||||
using ObjRef = std::reference_wrapper<Object>;
|
||||
FixedContainer(const int64_t id,
|
||||
const int64_t parent_id,
|
||||
const char *name,
|
||||
std::initializer_list<ObjRef>);
|
||||
FixedContainer(const int64_t id, const int64_t parent_id, const char *name);
|
||||
|
||||
BrowseStats
|
||||
browse_direct_children(xml::Element &, BrowseParams, const Object::ExtraId &) const final;
|
||||
|
||||
void dump_metadata(xml::Element &dest, const Object::ExtraId &) const final;
|
||||
void add_children(std::initializer_list<ObjRef> l);
|
||||
|
||||
private:
|
||||
std::vector<ObjRef> children;
|
||||
};
|
||||
} // namespace cds
|
||||
|
||||
#endif /* FIXEDCONTAINER_HPP */
|
|
@ -0,0 +1,200 @@
|
|||
/*****************************************************************************
|
||||
* Item.cpp
|
||||
*****************************************************************************
|
||||
* Copyright © 2024 VLC authors and VideoLAN
|
||||
*
|
||||
* Authors: Alaric Senat <alaric@videolabs.io>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation; either version 2.1 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with this program; if not, write to the Free Software Foundation,
|
||||
* Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
|
||||
*****************************************************************************/
|
||||
#ifdef HAVE_CONFIG_H
|
||||
#include "config.h"
|
||||
#endif
|
||||
|
||||
#include <chrono>
|
||||
#include <sstream>
|
||||
|
||||
#include "../utils.hpp"
|
||||
#include "Item.hpp"
|
||||
|
||||
namespace cds
|
||||
{
|
||||
|
||||
Item::Item(const int64_t id, const ml::MediaLibraryContext &ml) noexcept :
|
||||
Object(id, -1, nullptr, Object::Type::Item),
|
||||
medialib_(ml)
|
||||
{}
|
||||
|
||||
template <typename Rep, typename Pediod = std::ratio<1>>
|
||||
static std::string duration_to_string(const char *fmt, std::chrono::duration<Rep, Pediod> duration)
|
||||
{
|
||||
char ret[32] = {0};
|
||||
using namespace std::chrono;
|
||||
// Substract 1 hour because std::localtime starts at 1 AM
|
||||
const time_t sec = duration_cast<seconds>(duration - 1h).count();
|
||||
const size_t size = std::strftime(ret, sizeof(ret), fmt, std::localtime(&sec));
|
||||
return std::string{ret, size};
|
||||
}
|
||||
|
||||
static xml::Element make_resource(xml::Document &doc,
|
||||
const vlc_ml_media_t &media,
|
||||
const std::vector<utils::MediaTrackRef> v_tracks,
|
||||
const std::vector<utils::MediaFileRef> main_files,
|
||||
const std::string &file_extension)
|
||||
{
|
||||
const auto &profile_name = "native";
|
||||
const char *mux = file_extension.c_str();
|
||||
|
||||
const std::string url_base = utils::get_server_url();
|
||||
const std::string url =
|
||||
url_base + "media/" + profile_name + "/" + std::to_string(media.i_id) + "." + mux;
|
||||
|
||||
auto elem = doc.create_element("res", doc.create_text_node(url.c_str()));
|
||||
const auto media_duration =
|
||||
duration_to_string("%H:%M:%S", std::chrono::milliseconds(media.i_duration));
|
||||
elem.set_attribute("duration", media_duration.c_str());
|
||||
|
||||
if (media.i_type == VLC_ML_MEDIA_TYPE_VIDEO)
|
||||
{
|
||||
std::stringstream resolution;
|
||||
if (v_tracks.size() >= 1)
|
||||
{
|
||||
const vlc_ml_media_track_t &vtrack = v_tracks[0];
|
||||
|
||||
resolution << vtrack.v.i_width << 'x' << vtrack.v.i_height;
|
||||
}
|
||||
elem.set_attribute("resolution", resolution.str().c_str());
|
||||
}
|
||||
|
||||
const auto mime_type = utils::get_mimetype(media.i_type, mux);
|
||||
const auto protocol_info = utils::http::get_dlna_extra_protocol_info(mime_type);
|
||||
|
||||
elem.set_attribute("protocolInfo", protocol_info.c_str());
|
||||
|
||||
if (main_files.size() >= 1)
|
||||
{
|
||||
elem.set_attribute("size", std::to_string(main_files[0].get().i_size).c_str());
|
||||
}
|
||||
|
||||
return elem;
|
||||
};
|
||||
|
||||
static void
|
||||
dump_resources(xml::Element &dest, const vlc_ml_media_t &media, const std::string &file_extension)
|
||||
{
|
||||
xml::Document &doc = dest.owner;
|
||||
|
||||
const auto v_tracks = utils::get_media_tracks(media, VLC_ML_TRACK_TYPE_VIDEO);
|
||||
const auto main_files = utils::get_media_files(media, VLC_ML_FILE_TYPE_MAIN);
|
||||
|
||||
dest.add_child(make_resource(doc, media, v_tracks, main_files, file_extension));
|
||||
|
||||
// Thumbnails
|
||||
for (int i = 0; i < VLC_ML_THUMBNAIL_SIZE_COUNT; ++i)
|
||||
{
|
||||
const auto &thumbnail = media.thumbnails[i];
|
||||
if (thumbnail.i_status != VLC_ML_THUMBNAIL_STATUS_AVAILABLE)
|
||||
continue;
|
||||
const auto thumbnail_extension = utils::file_extension(std::string(thumbnail.psz_mrl));
|
||||
const auto url = utils::thumbnail_url(media, static_cast<vlc_ml_thumbnail_size_t>(i));
|
||||
auto elem = doc.create_element("res", doc.create_text_node(url.c_str()));
|
||||
|
||||
const utils::MimeType mime{"image", "jpeg"};
|
||||
const auto protocol_info = utils::http::get_dlna_extra_protocol_info(mime);
|
||||
elem.set_attribute("protocolInfo", protocol_info.c_str());
|
||||
|
||||
dest.add_child(std::move(elem));
|
||||
}
|
||||
|
||||
// Subtitles, for now we only share the first available subtitle file.
|
||||
const auto subtitles = utils::get_media_files(media, VLC_ML_FILE_TYPE_SUBTITLE);
|
||||
if (!subtitles.empty())
|
||||
{
|
||||
const vlc_ml_file_t &sub = subtitles.front();
|
||||
const auto file_extension = utils::file_extension(sub.psz_mrl);
|
||||
const std::string url = utils::get_server_url() + "subtitle/" + std::to_string(media.i_id) +
|
||||
"." + file_extension;
|
||||
|
||||
auto res = doc.create_element("res", doc.create_text_node(url.c_str()));
|
||||
res.set_attribute("protocolInfo", ("http-get:*:text/" + file_extension + ":*").c_str());
|
||||
dest.add_child(std::move(res));
|
||||
}
|
||||
}
|
||||
|
||||
void Item::dump_mlobject_metadata(xml::Element &dest,
|
||||
const vlc_ml_media_t &media,
|
||||
const ml::MediaLibraryContext &ml)
|
||||
{
|
||||
|
||||
if (media.p_files->i_nb_items == 0)
|
||||
return;
|
||||
|
||||
const vlc_ml_file_t &file = media.p_files->p_items[0];
|
||||
|
||||
const std::string file_extension = utils::file_extension(file.psz_mrl);
|
||||
|
||||
const char *object_class = nullptr;
|
||||
switch (media.i_type)
|
||||
{
|
||||
case VLC_ML_MEDIA_TYPE_AUDIO:
|
||||
object_class = "object.item.audioItem";
|
||||
break;
|
||||
case VLC_ML_MEDIA_TYPE_VIDEO:
|
||||
object_class = "object.item.videoItem";
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
const std::string date = std::to_string(media.i_year) + "-01-01";
|
||||
|
||||
xml::Document &doc = dest.owner;
|
||||
|
||||
dest.add_children(doc.create_element("upnp:class", doc.create_text_node(object_class)),
|
||||
doc.create_element("dc:title", doc.create_text_node(media.psz_title)),
|
||||
doc.create_element("dc:date", doc.create_text_node(date.c_str())));
|
||||
|
||||
switch (media.i_subtype)
|
||||
{
|
||||
case VLC_ML_MEDIA_SUBTYPE_ALBUMTRACK:
|
||||
{
|
||||
const auto album = ml::Album::get(ml, media.album_track.i_album_id);
|
||||
if (album != nullptr)
|
||||
{
|
||||
const auto album_thumbnail_url = utils::album_thumbnail_url(*album);
|
||||
dest.add_children(
|
||||
doc.create_element("upnp:album", doc.create_text_node(album->psz_title)),
|
||||
doc.create_element("upnp:artist", doc.create_text_node(album->psz_artist)),
|
||||
doc.create_element("upnp:albumArtURI",
|
||||
doc.create_text_node(album_thumbnail_url.c_str())));
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
dump_resources(dest, media, file_extension);
|
||||
}
|
||||
|
||||
void Item::dump_metadata(xml::Element &dest, const Object::ExtraId &extra_id) const
|
||||
{
|
||||
assert(extra_id.has_value());
|
||||
const auto media = ml::Media::get(medialib_, extra_id->ml_id);
|
||||
|
||||
dump_mlobject_metadata(dest, *media.get(), medialib_);
|
||||
}
|
||||
|
||||
} // namespace cds
|
|
@ -0,0 +1,54 @@
|
|||
/*****************************************************************************
|
||||
* Item.hpp : CDS Item interface
|
||||
*****************************************************************************
|
||||
* Copyright © 2024 VLC authors and VideoLAN
|
||||
*
|
||||
* Authors: Alaric Senat <alaric@videolabs.io>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation; either version 2.1 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with this program; if not, write to the Free Software Foundation,
|
||||
* Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
|
||||
*****************************************************************************/
|
||||
#ifndef ITEM_HPP
|
||||
#define ITEM_HPP
|
||||
|
||||
#ifdef HAVE_CONFIG_H
|
||||
#include "config.h"
|
||||
#endif
|
||||
|
||||
#include <vlc_common.h>
|
||||
|
||||
#include "../ml.hpp"
|
||||
#include "Object.hpp"
|
||||
|
||||
namespace cds
|
||||
{
|
||||
/// This is a dynamic object representing a medialibrary media
|
||||
/// It expect to receive the medialibrary id of the media it should represent in its Extra ID, with
|
||||
/// that, a single instance of Item can effectively represent all medialibrary medias
|
||||
class Item : public Object
|
||||
{
|
||||
public:
|
||||
Item(const int64_t id, const ml::MediaLibraryContext &) noexcept;
|
||||
void dump_metadata(xml::Element &, const Object::ExtraId &) const final;
|
||||
|
||||
static void dump_mlobject_metadata(xml::Element &dest,
|
||||
const vlc_ml_media_t &media,
|
||||
const ml::MediaLibraryContext &ml);
|
||||
|
||||
private:
|
||||
const ml::MediaLibraryContext &medialib_;
|
||||
};
|
||||
} // namespace cds
|
||||
|
||||
#endif /* ITEM_HPP */
|
|
@ -0,0 +1,174 @@
|
|||
/*****************************************************************************
|
||||
* MLContainer.hpp : CDS MediaLibrary container implementation
|
||||
*****************************************************************************
|
||||
* Copyright © 2024 VLC authors and VideoLAN
|
||||
*
|
||||
* Authors: Alaric Senat <alaric@videolabs.io>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation; either version 2.1 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with this program; if not, write to the Free Software Foundation,
|
||||
* Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
|
||||
*****************************************************************************/
|
||||
#pragma once
|
||||
#ifndef MLCONTAINER_HPP
|
||||
#define MLCONTAINER_HPP
|
||||
|
||||
#include <vlc_cxx_helpers.hpp>
|
||||
#include <vlc_url.h>
|
||||
|
||||
#include "../ml.hpp"
|
||||
#include "Container.hpp"
|
||||
#include "Item.hpp"
|
||||
#include "../utils.hpp"
|
||||
|
||||
namespace cds
|
||||
{
|
||||
|
||||
/// MLContainer is a dynamic object, it must have a ml id in its extra id.
|
||||
/// MLContainer is a very versatile Container that basically list all the medialibrary objects such
|
||||
/// as Albums, Playlists, etc.
|
||||
/// MLHelpers can be found in "../ml.hpp"
|
||||
template <typename MLHelper> class MLContainer : public Container
|
||||
{
|
||||
public:
|
||||
MLContainer(int64_t id,
|
||||
int64_t parent_id,
|
||||
const char *name,
|
||||
const ml::MediaLibraryContext &ml,
|
||||
const Object &child) :
|
||||
Container(id, parent_id, name),
|
||||
ml_(ml),
|
||||
child_(child)
|
||||
{}
|
||||
|
||||
void dump_metadata(xml::Element &dest, const Object::ExtraId &extra) const final
|
||||
{
|
||||
if (extra.has_value())
|
||||
{
|
||||
const auto &ml_object = MLHelper::get(ml_, extra.value().ml_id);
|
||||
dump_mlobject_metadata(dest, *ml_object.get());
|
||||
}
|
||||
else
|
||||
{
|
||||
const size_t child_count = MLHelper::count(ml_, std::nullopt);
|
||||
dest.set_attribute("childCount", std::to_string(child_count).c_str());
|
||||
|
||||
xml::Document &doc = dest.owner;
|
||||
dest.add_child(doc.create_element("upnp:class", doc.create_text_node("object.container")));
|
||||
}
|
||||
}
|
||||
|
||||
BrowseStats browse_direct_children(xml::Element &dest,
|
||||
const BrowseParams params,
|
||||
const Object::ExtraId &extra) const final
|
||||
{
|
||||
const vlc_ml_query_params_t query_params = params.to_query_params();
|
||||
std::optional<int64_t> ml_id;
|
||||
if (extra.has_value())
|
||||
ml_id = static_cast<int64_t>(extra->ml_id);
|
||||
const auto list = MLHelper::list(ml_, &query_params, ml_id);
|
||||
|
||||
xml::Document &doc = dest.owner;
|
||||
for (unsigned i = 0; i < list->i_nb_items; ++i)
|
||||
{
|
||||
const auto &item = list->p_items[i];
|
||||
|
||||
auto elem =
|
||||
child_.create_object_element(doc, ExtraIdData{item.i_id, get_dynamic_id(extra)});
|
||||
dump_mlobject_metadata(elem, item);
|
||||
dest.add_child(std::move(elem));
|
||||
}
|
||||
return {list->i_nb_items, MLHelper::count(ml_, ml_id)};
|
||||
}
|
||||
|
||||
private:
|
||||
void dump_mlobject_metadata(xml::Element &dest, const vlc_ml_media_t &media) const
|
||||
{
|
||||
Item::dump_mlobject_metadata(dest, media, ml_);
|
||||
}
|
||||
|
||||
void dump_mlobject_metadata(xml::Element &dest, const vlc_ml_playlist_t &playlist) const
|
||||
{
|
||||
xml::Document &doc = dest.owner;
|
||||
|
||||
dest.set_attribute("childCount", std::to_string(playlist.i_nb_present_media).c_str());
|
||||
|
||||
dest.add_children(
|
||||
doc.create_element("upnp:class",
|
||||
doc.create_text_node("object.container.playlistContainer")),
|
||||
doc.create_element("dc:title", doc.create_text_node(playlist.psz_name)));
|
||||
}
|
||||
|
||||
void dump_mlobject_metadata(xml::Element &dest, const vlc_ml_album_t &album) const
|
||||
{
|
||||
xml::Document &doc = dest.owner;
|
||||
|
||||
dest.set_attribute("childCount", std::to_string(album.i_nb_tracks).c_str());
|
||||
|
||||
const auto album_thumbnail_url = utils::album_thumbnail_url(album);
|
||||
dest.add_children(
|
||||
doc.create_element("upnp:artist", doc.create_text_node(album.psz_artist)),
|
||||
doc.create_element("upnp:class",
|
||||
doc.create_text_node("object.container.album.musicAlbum")),
|
||||
doc.create_element("dc:title", doc.create_text_node(album.psz_title)),
|
||||
doc.create_element("dc:description", doc.create_text_node(album.psz_summary)),
|
||||
doc.create_element("upnp:albumArtURI",
|
||||
doc.create_text_node(album_thumbnail_url.c_str())));
|
||||
}
|
||||
|
||||
void dump_mlobject_metadata(xml::Element &dest, const vlc_ml_artist_t &artist) const
|
||||
{
|
||||
xml::Document &doc = dest.owner;
|
||||
|
||||
dest.set_attribute("childCount", std::to_string(artist.i_nb_album).c_str());
|
||||
|
||||
dest.add_children(
|
||||
doc.create_element("upnp:class",
|
||||
doc.create_text_node("object.container.person.musicArtist")),
|
||||
doc.create_element("dc:title", doc.create_text_node(artist.psz_name)));
|
||||
}
|
||||
|
||||
void dump_mlobject_metadata(xml::Element &dest, const vlc_ml_genre_t &genre) const
|
||||
{
|
||||
xml::Document &doc = dest.owner;
|
||||
|
||||
dest.set_attribute("childCount", std::to_string(genre.i_nb_tracks).c_str());
|
||||
|
||||
dest.add_children(
|
||||
doc.create_element("upnp:class",
|
||||
doc.create_text_node("object.container.genre.musicGenre")),
|
||||
doc.create_element("dc:title", doc.create_text_node(genre.psz_name)));
|
||||
}
|
||||
|
||||
void dump_mlobject_metadata(xml::Element &dest, const vlc_ml_folder_t &folder) const
|
||||
{
|
||||
xml::Document &doc = dest.owner;
|
||||
|
||||
assert(!folder.b_banned);
|
||||
|
||||
const auto path = vlc::wrap_cptr(vlc_uri2path(folder.psz_mrl), &free);
|
||||
dest.add_children(
|
||||
doc.create_element("upnp:class", doc.create_text_node("object.container")),
|
||||
doc.create_element("dc:title", doc.create_text_node(path.get())));
|
||||
}
|
||||
|
||||
private:
|
||||
const ml::MediaLibraryContext &ml_;
|
||||
/// We take another dynamic object as member, this will be the dynamic child of the
|
||||
/// MLContainer, for example a MLContainer representing an album will have an Item
|
||||
/// ("./Item.hpp") as child
|
||||
const Object &child_;
|
||||
};
|
||||
} // namespace cds
|
||||
|
||||
#endif /* MLCONTAINER_HPP */
|
|
@ -0,0 +1,149 @@
|
|||
/*****************************************************************************
|
||||
* MLFolderContainer.hpp : MediaLibrary IFolder container
|
||||
*****************************************************************************
|
||||
* Copyright © 2024 VLC authors and VideoLAN
|
||||
*
|
||||
* Authors: Alaric Senat <alaric@videolabs.io>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation; either version 2.1 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with this program; if not, write to the Free Software Foundation,
|
||||
* Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
|
||||
*****************************************************************************/
|
||||
#ifndef MLFOLDERCONTAINER_HPP
|
||||
#define MLFOLDERCONTAINER_HPP
|
||||
|
||||
#include <vlc_cxx_helpers.hpp>
|
||||
#include <vlc_media_library.h>
|
||||
#include <vlc_url.h>
|
||||
|
||||
#include "../ml.hpp"
|
||||
#include "Container.hpp"
|
||||
#include "Item.hpp"
|
||||
|
||||
namespace cds
|
||||
{
|
||||
class MLFolderContainer : public Container
|
||||
{
|
||||
static void dump_folder_metadata(xml::Element &dest, const vlc_ml_folder_t &folder)
|
||||
{
|
||||
xml::Document &doc = dest.owner;
|
||||
auto path = vlc::wrap_cptr(vlc_uri2path(folder.psz_mrl), &free);
|
||||
|
||||
// Only keep the last folder from the path
|
||||
const char *folder_name = nullptr;
|
||||
if (path != nullptr && strlen(path.get()) > 0)
|
||||
{
|
||||
#ifdef _WIN32
|
||||
const char sep = '\\';
|
||||
#else
|
||||
const char sep = '/';
|
||||
#endif
|
||||
|
||||
for (auto i = strlen(path.get()) - 1; i > 0; --i)
|
||||
{
|
||||
if (path.get()[i] == sep)
|
||||
path.get()[i] = '\0';
|
||||
else
|
||||
break;
|
||||
}
|
||||
folder_name = strrchr(path.get(), '/') + 1;
|
||||
}
|
||||
|
||||
dest.add_children(
|
||||
doc.create_element("dc:title",
|
||||
doc.create_text_node(folder_name ? folder_name : path.get())),
|
||||
doc.create_element("upnp:class", doc.create_text_node("object.container")));
|
||||
}
|
||||
|
||||
public:
|
||||
MLFolderContainer(int64_t id,
|
||||
int64_t parent_id,
|
||||
const char *name,
|
||||
const ml::MediaLibraryContext &ml,
|
||||
const Item &child) :
|
||||
Container(id, parent_id, name),
|
||||
ml_(ml),
|
||||
child_(child)
|
||||
{}
|
||||
|
||||
void dump_metadata(xml::Element &dest, const Object::ExtraId &extra) const final
|
||||
{
|
||||
if (!extra.has_value())
|
||||
{
|
||||
dest.set_attribute("childCount", "0");
|
||||
|
||||
xml::Document &doc = dest.owner;
|
||||
dest.add_child(
|
||||
doc.create_element("upnp:class", doc.create_text_node("object.container")));
|
||||
return;
|
||||
}
|
||||
const auto folder = ml::Folder::get(ml_, extra->ml_id);
|
||||
|
||||
if (folder != nullptr)
|
||||
{
|
||||
dump_folder_metadata(dest, *folder);
|
||||
}
|
||||
}
|
||||
|
||||
BrowseStats browse_direct_children(xml::Element &dest,
|
||||
const BrowseParams params,
|
||||
const Object::ExtraId &extra) const final
|
||||
{
|
||||
const vlc_ml_query_params_t query_params = params.to_query_params();
|
||||
assert(extra.has_value());
|
||||
|
||||
const auto folder = ml::Folder::get(ml_, extra->ml_id);
|
||||
assert(folder != nullptr);
|
||||
|
||||
xml::Document &doc = dest.owner;
|
||||
|
||||
const auto subfolder_list = ml::SubfoldersList::list(ml_, &query_params, extra->ml_id);
|
||||
if (subfolder_list)
|
||||
{
|
||||
for (auto i = 0u; i < subfolder_list->i_nb_items; ++i)
|
||||
{
|
||||
const auto &folder = subfolder_list->p_items[i];
|
||||
auto elem =
|
||||
create_object_element(doc, ExtraIdData{folder.i_id, get_dynamic_id(extra)});
|
||||
dump_folder_metadata(elem, folder);
|
||||
dest.add_child(std::move(elem));
|
||||
}
|
||||
}
|
||||
|
||||
const auto media_list = ml::MediaFolderList::list(ml_, &query_params, extra->ml_id);
|
||||
if (media_list)
|
||||
{
|
||||
for (auto i = 0u; i < media_list->i_nb_items; ++i)
|
||||
{
|
||||
const auto &media = media_list->p_items[i];
|
||||
auto elem = child_.create_object_element(
|
||||
doc, ExtraIdData{child_.id, get_dynamic_id(extra)});
|
||||
Item::dump_mlobject_metadata(elem, media, ml_);
|
||||
dest.add_child(std::move(elem));
|
||||
}
|
||||
}
|
||||
|
||||
BrowseStats stats;
|
||||
stats.result_count = subfolder_list->i_nb_items + media_list->i_nb_items;
|
||||
stats.total_matches = ml::SubfoldersList::count(ml_, extra->ml_id) +
|
||||
ml::MediaFolderList::count(ml_, extra->ml_id);
|
||||
return stats;
|
||||
}
|
||||
|
||||
public:
|
||||
const ml::MediaLibraryContext &ml_;
|
||||
const Item &child_;
|
||||
};
|
||||
} // namespace cds
|
||||
|
||||
#endif /* MLFOLDERCONTAINER_HPP */
|
|
@ -0,0 +1,119 @@
|
|||
/*****************************************************************************
|
||||
* Object.hpp : CDS Object interface implementation
|
||||
*****************************************************************************
|
||||
* Copyright © 2024 VLC authors and VideoLAN
|
||||
*
|
||||
* Authors: Alaric Senat <alaric@videolabs.io>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation; either version 2.1 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with this program; if not, write to the Free Software Foundation,
|
||||
* Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
|
||||
*****************************************************************************/
|
||||
#ifndef OBJECT_HPP
|
||||
#define OBJECT_HPP
|
||||
|
||||
#include "../xml_wrapper.hpp"
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
namespace cds
|
||||
{
|
||||
/// Opaque CDS object type
|
||||
/// Specs: http://www.upnp.org/specs/av/UPnP-av-ContentDirectory-v3-Service-20080930.pdf
|
||||
/// "2.2.2 - Object"
|
||||
struct Object
|
||||
{
|
||||
enum class Type
|
||||
{
|
||||
Item,
|
||||
Container
|
||||
};
|
||||
|
||||
struct ExtraIdData
|
||||
{
|
||||
int64_t ml_id;
|
||||
std::string parent;
|
||||
};
|
||||
|
||||
using ExtraId = std::optional<ExtraIdData>;
|
||||
|
||||
int64_t id;
|
||||
int64_t parent_id;
|
||||
const char *name;
|
||||
const Type type;
|
||||
|
||||
Object(int64_t id, int64_t parent_id, const char *name, const Type type) noexcept :
|
||||
id(id),
|
||||
parent_id(parent_id),
|
||||
name(name),
|
||||
type(type)
|
||||
{}
|
||||
|
||||
virtual ~Object() = default;
|
||||
|
||||
/// Create an xml element describing the object.
|
||||
xml::Element browse_metadata(xml::Document &doc, const ExtraId &extra_id) const
|
||||
{
|
||||
auto ret = create_object_element(doc, extra_id);
|
||||
|
||||
dump_metadata(ret, extra_id);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
/// Utility function to create the xml common representation of an object.
|
||||
xml::Element create_object_element(xml::Document &doc, const ExtraId &extra_id) const
|
||||
{
|
||||
auto ret = doc.create_element(type == Type::Item ? "item" : "container");
|
||||
|
||||
if (extra_id.has_value())
|
||||
{
|
||||
ret.set_attribute("id", get_dynamic_id(extra_id).c_str());
|
||||
ret.set_attribute("parentID", extra_id->parent.c_str());
|
||||
}
|
||||
else
|
||||
{
|
||||
ret.set_attribute("id", std::to_string(id).c_str());
|
||||
ret.set_attribute("parentID", std::to_string(parent_id).c_str());
|
||||
}
|
||||
|
||||
if (name)
|
||||
{
|
||||
ret.add_child(doc.create_element("dc:title", doc.create_text_node(name)));
|
||||
}
|
||||
ret.set_attribute("restricted", "1");
|
||||
return ret;
|
||||
}
|
||||
|
||||
protected:
|
||||
/// Build an Object id based on the extra id provided,
|
||||
/// Some Objects can have a changing id based on the medialib id they expose, for example,
|
||||
/// "1:3" or "1:43" are both valid, they just expose different contents through the same object
|
||||
/// tied to the id "1".
|
||||
std::string get_dynamic_id(const ExtraId &extra_id) const
|
||||
{
|
||||
if (!extra_id.has_value())
|
||||
return std::to_string(id);
|
||||
const std::string ml_id_str = std::to_string(extra_id->ml_id);
|
||||
if (!extra_id->parent.empty())
|
||||
return std::to_string(id) + ':' + ml_id_str + '(' + extra_id->parent + ')';
|
||||
return std::to_string(id) + ':' + ml_id_str;
|
||||
}
|
||||
|
||||
/// Dump Object specialization specific informations in the fiven xml element
|
||||
virtual void dump_metadata(xml::Element &dest, const ExtraId &extra_id) const = 0;
|
||||
};
|
||||
} // namespace cds
|
||||
|
||||
#endif /* OBJECT_HPP */
|
|
@ -0,0 +1,158 @@
|
|||
/*****************************************************************************
|
||||
* cds.cpp
|
||||
*****************************************************************************
|
||||
* Copyright © 2024 VLC authors and VideoLAN
|
||||
*
|
||||
* Authors: Alaric Senat <alaric@videolabs.io>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation; either version 2.1 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with this program; if not, write to the Free Software Foundation,
|
||||
* Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
|
||||
*****************************************************************************/
|
||||
#ifdef HAVE_CONFIG_H
|
||||
#include "config.h"
|
||||
#endif
|
||||
|
||||
#include "FixedContainer.hpp"
|
||||
#include "Item.hpp"
|
||||
#include "MLContainer.hpp"
|
||||
#include "MLFolderContainer.hpp"
|
||||
#include "cds.hpp"
|
||||
|
||||
#include <sstream>
|
||||
|
||||
namespace cds
|
||||
{
|
||||
|
||||
template <typename T> std::optional<T> next_value(std::stringstream &ss, const char delim)
|
||||
{
|
||||
std::string token;
|
||||
if (!std::getline(ss, token, delim))
|
||||
return std::nullopt;
|
||||
try
|
||||
{
|
||||
return static_cast<T>(std::stoull(token));
|
||||
}
|
||||
catch (const std::invalid_argument &)
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
};
|
||||
|
||||
std::tuple<unsigned, Object::ExtraId> parse_id(const std::string &id)
|
||||
{
|
||||
std::stringstream ss(id);
|
||||
const auto parsed_id = next_value<unsigned>(ss, ':');
|
||||
if (!parsed_id.has_value())
|
||||
throw std::invalid_argument("Invalid id");
|
||||
const auto parsed_ml_id = next_value<int64_t>(ss, '(');
|
||||
|
||||
std::optional<std::string> parent = std::nullopt;
|
||||
{
|
||||
std::string token;
|
||||
if (std::getline(ss, token) && !token.empty() && token.back() == ')')
|
||||
parent = token.substr(0, token.size() - 1);
|
||||
}
|
||||
|
||||
if (parsed_ml_id.has_value())
|
||||
return {parsed_id.value(), {{parsed_ml_id.value(), parent.value_or("")}}};
|
||||
return {parsed_id.value(), std::nullopt};
|
||||
}
|
||||
|
||||
std::vector<std::unique_ptr<Object>> init_hierarchy(const ml::MediaLibraryContext &ml)
|
||||
{
|
||||
std::vector<std::unique_ptr<Object>> hierarchy;
|
||||
|
||||
const auto add_fixed_container =
|
||||
[&](const char *name, std::initializer_list<FixedContainer::ObjRef> children) -> Object & {
|
||||
const int64_t id = hierarchy.size();
|
||||
for (Object &child : children)
|
||||
child.parent_id = id;
|
||||
auto up = std::make_unique<FixedContainer>(id, -1, name, children);
|
||||
hierarchy.emplace_back(std::move(up));
|
||||
return *hierarchy.back();
|
||||
};
|
||||
|
||||
const auto add_ml_container = [&](auto MLHelper, const char *name, Object &child) -> Object & {
|
||||
const int64_t id = hierarchy.size();
|
||||
child.parent_id = id;
|
||||
hierarchy.push_back(
|
||||
std::make_unique<MLContainer<decltype(MLHelper)>>(id, -1, name, ml, child));
|
||||
return static_cast<Object &>(*hierarchy.back());
|
||||
};
|
||||
|
||||
hierarchy.push_back(std::make_unique<FixedContainer>(0, -1, "Home"));
|
||||
hierarchy.push_back(std::make_unique<Item>(1, ml));
|
||||
|
||||
const auto &item = static_cast<const Item &>(*hierarchy[1]);
|
||||
|
||||
const auto add_ml_folder_container = [&]() -> Object & {
|
||||
const int64_t id = hierarchy.size();
|
||||
hierarchy.push_back(std::make_unique<MLFolderContainer>(id, -1, nullptr, ml, item));
|
||||
return static_cast<Object &>(*hierarchy.back());
|
||||
};
|
||||
|
||||
const auto add_ml_media_container = [&](auto MLHelper, const char *name) -> Object & {
|
||||
const int64_t id = hierarchy.size();
|
||||
hierarchy.push_back(
|
||||
std::make_unique<MLContainer<decltype(MLHelper)>>(id, -1, name, ml, item));
|
||||
return static_cast<Object &>(*hierarchy.back());
|
||||
};
|
||||
|
||||
static_cast<FixedContainer &>(*hierarchy[0])
|
||||
.add_children({
|
||||
add_fixed_container(
|
||||
"Video",
|
||||
{
|
||||
add_ml_media_container(ml::AllVideos{}, "All Video"),
|
||||
}),
|
||||
add_fixed_container(
|
||||
"Music",
|
||||
{
|
||||
add_fixed_container("Tracks", {
|
||||
add_ml_media_container(ml::AllAudio{}, "All"),
|
||||
add_ml_container(ml::AllArtistsList{},
|
||||
"By Artist",
|
||||
add_ml_media_container(ml::ArtistTracksList{}, nullptr)),
|
||||
add_ml_container(ml::AllGenresList{},
|
||||
"By Genre",
|
||||
add_ml_media_container(ml::GenreTracksList{}, nullptr)),
|
||||
}),
|
||||
|
||||
add_fixed_container("Albums", {
|
||||
add_ml_container(ml::AllAlbums{},
|
||||
"All",
|
||||
add_ml_media_container(ml::AlbumTracksList{}, nullptr)),
|
||||
add_ml_container(ml::AllArtistsList{},
|
||||
"By Artist",
|
||||
add_ml_container(ml::ArtistAlbumList{},
|
||||
nullptr,
|
||||
add_ml_media_container(
|
||||
ml::AlbumTracksList{}, nullptr))),
|
||||
add_ml_container(ml::AllGenresList{},
|
||||
"By Genre",
|
||||
add_ml_container(ml::GenreAlbumList{},
|
||||
nullptr,
|
||||
add_ml_media_container(
|
||||
ml::AlbumTracksList{}, nullptr))),
|
||||
}),
|
||||
}),
|
||||
add_ml_container(ml::PlaylistsList{},
|
||||
"Playlists",
|
||||
add_ml_media_container(ml::PlaylistMediaList{}, nullptr)),
|
||||
add_ml_container(ml::AllEntryPoints{}, "Folders", add_ml_folder_container()),
|
||||
});
|
||||
|
||||
return hierarchy;
|
||||
}
|
||||
} // namespace cds
|
|
@ -0,0 +1,55 @@
|
|||
/*****************************************************************************
|
||||
* cds.hpp : UPNP ContentDirectory Service entry point
|
||||
*****************************************************************************
|
||||
* Copyright © 2024 VLC authors and VideoLAN
|
||||
*
|
||||
* Authors: Alaric Senat <alaric@videolabs.io>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation; either version 2.1 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with this program; if not, write to the Free Software Foundation,
|
||||
* Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
|
||||
*****************************************************************************/
|
||||
#ifndef CDS_HPP
|
||||
#define CDS_HPP
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "../ml.hpp"
|
||||
#include "Object.hpp"
|
||||
|
||||
/// CDS is the short for ContentDirectory Service, its the services that clients should use to
|
||||
/// browse the server file hierarchy
|
||||
///
|
||||
/// Specs: http://www.upnp.org/specs/av/UPnP-av-ContentDirectory-v3-Service-20080930.pdf
|
||||
namespace cds
|
||||
{
|
||||
|
||||
/// Split the given string and return the cds::Object id its refers to along with extra
|
||||
/// informations on the object (for instance a medialibrary id).
|
||||
/// A string id must follow this pattern:
|
||||
/// "OBJ_ID:ML_ID(PARENT_ID)" where:
|
||||
/// - OBJ_ID is the cds object index.
|
||||
/// - ML_ID (optional) is a medialibrary id (A media, an album, whatever)
|
||||
/// - PARENT_ID (optional) is the cds parent object, it's needed in case the parent has a ML_ID
|
||||
/// bound to it
|
||||
/// This pattern allows us to create "dynamics" object that reflects the structure of the
|
||||
/// medialibrary database without actually duplicating it.
|
||||
std::tuple<unsigned, Object::ExtraId> parse_id(const std::string &id);
|
||||
|
||||
/// Initialize the Upnp server objects hierarchy.
|
||||
/// This needs to be called once at the startup of the server.
|
||||
std::vector<std::unique_ptr<Object>> init_hierarchy(const ml::MediaLibraryContext &ml);
|
||||
|
||||
} // namespace cds
|
||||
|
||||
#endif /* CDS_HPP */
|
|
@ -0,0 +1,206 @@
|
|||
/*****************************************************************************
|
||||
* ml.hpp : C++ media library API wrapper
|
||||
*****************************************************************************
|
||||
* Copyright © 2021 VLC authors and VideoLAN
|
||||
*
|
||||
* Authors: Alaric Senat <alaric@videolabs.io>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation; either version 2.1 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with this program; if not, write to the Free Software Foundation,
|
||||
* Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
|
||||
*****************************************************************************/
|
||||
#ifndef ML_HPP
|
||||
#define ML_HPP
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
|
||||
#include <vlc_media_library.h>
|
||||
|
||||
namespace ml
|
||||
{
|
||||
|
||||
struct MediaLibraryContext {
|
||||
vlc_medialibrary_t *handle;
|
||||
bool share_private_media;
|
||||
};
|
||||
|
||||
static const vlc_ml_query_params_t PUBLIC_ONLY_QP = [] {
|
||||
vlc_ml_query_params_t params = vlc_ml_query_params_create();
|
||||
params.b_public_only = true;
|
||||
return params;
|
||||
}();
|
||||
|
||||
static inline bool is_ml_object_private(const MediaLibraryContext &, vlc_ml_media_t *media)
|
||||
{
|
||||
return !media->b_is_public;
|
||||
}
|
||||
|
||||
static inline bool is_ml_object_private(const MediaLibraryContext &ml, vlc_ml_album_t *album)
|
||||
{
|
||||
const size_t count = vlc_ml_count_album_tracks(ml.handle, &PUBLIC_ONLY_QP, album->i_id);
|
||||
return count == 0;
|
||||
}
|
||||
|
||||
static inline bool is_ml_object_private(const MediaLibraryContext &ml, vlc_ml_playlist_t *playlist)
|
||||
{
|
||||
const size_t count = vlc_ml_count_playlist_media(ml.handle, &PUBLIC_ONLY_QP, playlist->i_id);
|
||||
return count == 0;
|
||||
}
|
||||
|
||||
static inline bool is_ml_object_private(const MediaLibraryContext &ml, vlc_ml_artist_t *artist)
|
||||
{
|
||||
const size_t count = vlc_ml_count_artist_tracks(ml.handle, &PUBLIC_ONLY_QP, artist->i_id);
|
||||
return count == 0;
|
||||
}
|
||||
|
||||
static inline bool is_ml_object_private(const MediaLibraryContext &ml, vlc_ml_genre_t *genre)
|
||||
{
|
||||
const size_t count = vlc_ml_count_genre_tracks(ml.handle, &PUBLIC_ONLY_QP, genre->i_id);
|
||||
return count == 0;
|
||||
}
|
||||
|
||||
static inline bool is_ml_object_private(const MediaLibraryContext &ml, vlc_ml_folder_t *folder)
|
||||
{
|
||||
const size_t count = vlc_ml_count_folder_media(ml.handle, &PUBLIC_ONLY_QP, folder->i_id);
|
||||
return count == 0;
|
||||
}
|
||||
|
||||
namespace errors {
|
||||
struct ForbiddenAccess : public std::exception {
|
||||
virtual ~ForbiddenAccess() = default;
|
||||
virtual const char *what() const noexcept override {
|
||||
return "Private element";
|
||||
}
|
||||
};
|
||||
|
||||
struct UnknownObject : public std::exception {
|
||||
virtual ~UnknownObject() = default;
|
||||
virtual const char *what() const noexcept override {
|
||||
return "Unknown element";
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
template <typename MLObject, vlc_ml_get_queries GetQuery> struct Object
|
||||
{
|
||||
using Ptr = std::unique_ptr<MLObject, std::function<void(MLObject *)>>;
|
||||
|
||||
static Ptr get(const MediaLibraryContext &ml, const int64_t id)
|
||||
{
|
||||
MLObject *obj = static_cast<MLObject *>(vlc_ml_get(ml.handle, GetQuery, id));
|
||||
if (obj == nullptr)
|
||||
throw errors::UnknownObject();
|
||||
|
||||
Ptr ptr{obj, static_cast<void (*)(MLObject *)>(&vlc_ml_release)};
|
||||
if (!ml.share_private_media && is_ml_object_private(ml, ptr.get()))
|
||||
throw errors::ForbiddenAccess();
|
||||
return ptr;
|
||||
}
|
||||
};
|
||||
|
||||
using Media = Object<vlc_ml_media_t, VLC_ML_GET_MEDIA>;
|
||||
using Album = Object<vlc_ml_album_t, VLC_ML_GET_ALBUM>;
|
||||
using Playlist = Object<vlc_ml_playlist_t, VLC_ML_GET_PLAYLIST>;
|
||||
using Artist = Object<vlc_ml_artist_t, VLC_ML_GET_ARTIST>;
|
||||
using Genre = Object<vlc_ml_genre_t, VLC_ML_GET_GENRE>;
|
||||
using Folder = Object<vlc_ml_folder_t, VLC_ML_GET_FOLDER>;
|
||||
|
||||
template <typename ListType,
|
||||
vlc_ml_list_queries ListQuery,
|
||||
vlc_ml_list_queries CountQuery,
|
||||
typename Object>
|
||||
struct List : Object
|
||||
{
|
||||
static size_t count(const MediaLibraryContext &ml, std::optional<int64_t> id) noexcept
|
||||
{
|
||||
size_t res;
|
||||
int status;
|
||||
|
||||
vlc_ml_query_params_t params = vlc_ml_query_params_create();
|
||||
params.b_public_only = !ml.share_private_media;
|
||||
|
||||
if (id.has_value())
|
||||
status = vlc_ml_list(ml.handle, CountQuery, ¶ms, id.value(), &res);
|
||||
else if (CountQuery == VLC_ML_COUNT_ARTISTS)
|
||||
status = vlc_ml_list(ml.handle, CountQuery, ¶ms, (int)false, &res);
|
||||
else if (CountQuery == VLC_ML_COUNT_ENTRY_POINTS)
|
||||
status = vlc_ml_list(ml.handle, CountQuery, ¶ms, (int)false, &res);
|
||||
else if (CountQuery == VLC_ML_COUNT_PLAYLISTS)
|
||||
status = vlc_ml_list(ml.handle, CountQuery, ¶ms, VLC_ML_PLAYLIST_TYPE_ALL, &res);
|
||||
else
|
||||
status = vlc_ml_list(ml.handle, CountQuery, ¶ms, &res);
|
||||
return status == VLC_SUCCESS ? res : 0;
|
||||
}
|
||||
|
||||
using Ptr = std::unique_ptr<ListType, std::function<void(ListType *)>>;
|
||||
|
||||
static Ptr list(const MediaLibraryContext &ml,
|
||||
const vlc_ml_query_params_t *params,
|
||||
const std::optional<int64_t> id) noexcept
|
||||
{
|
||||
ListType *res;
|
||||
int status;
|
||||
|
||||
vlc_ml_query_params_t extra_params = *params;
|
||||
extra_params.b_public_only = !ml.share_private_media;
|
||||
|
||||
if (id.has_value())
|
||||
status = vlc_ml_list(ml.handle, ListQuery, &extra_params, id.value(), &res);
|
||||
else if (ListQuery == VLC_ML_LIST_ARTISTS)
|
||||
status = vlc_ml_list(ml.handle, ListQuery, &extra_params, (int)false, &res);
|
||||
else if (ListQuery == VLC_ML_LIST_ENTRY_POINTS)
|
||||
status = vlc_ml_list(ml.handle, ListQuery, &extra_params, (int)false, &res);
|
||||
else if (ListQuery == VLC_ML_LIST_PLAYLISTS)
|
||||
status = vlc_ml_list(ml.handle, ListQuery, &extra_params, VLC_ML_PLAYLIST_TYPE_ALL, &res);
|
||||
else
|
||||
status = vlc_ml_list(ml.handle, ListQuery, &extra_params, &res);
|
||||
return {status == VLC_SUCCESS ? res : nullptr,
|
||||
static_cast<void (*)(ListType *)>(&vlc_ml_release)};
|
||||
}
|
||||
};
|
||||
|
||||
using AllAudio = List<vlc_ml_media_list_t, VLC_ML_LIST_AUDIOS, VLC_ML_COUNT_AUDIOS, Media>;
|
||||
using AllVideos = List<vlc_ml_media_list_t, VLC_ML_LIST_VIDEOS, VLC_ML_COUNT_VIDEOS, Media>;
|
||||
using AllAlbums = List<vlc_ml_album_list_t, VLC_ML_LIST_ALBUMS, VLC_ML_COUNT_ALBUMS, Media>;
|
||||
using AllEntryPoints =
|
||||
List<vlc_ml_folder_list_t, VLC_ML_LIST_ENTRY_POINTS, VLC_ML_COUNT_ENTRY_POINTS, Folder>;
|
||||
|
||||
using AllArtistsList = List<vlc_ml_artist_list_t, VLC_ML_LIST_ARTISTS, VLC_ML_COUNT_ARTISTS, Media>;
|
||||
using ArtistAlbumList =
|
||||
List<vlc_ml_album_list_t, VLC_ML_LIST_ARTIST_ALBUMS, VLC_ML_COUNT_ARTIST_ALBUMS, Album>;
|
||||
using ArtistTracksList =
|
||||
List<vlc_ml_media_list_t, VLC_ML_LIST_ARTIST_TRACKS, VLC_ML_COUNT_ARTIST_TRACKS, Media>;
|
||||
using AlbumTracksList =
|
||||
List<vlc_ml_media_list_t, VLC_ML_LIST_ALBUM_TRACKS, VLC_ML_COUNT_ALBUM_TRACKS, Album>;
|
||||
|
||||
using AllGenresList = List<vlc_ml_genre_list_t, VLC_ML_LIST_GENRES, VLC_ML_COUNT_GENRES, Media>;
|
||||
using GenreAlbumList =
|
||||
List<vlc_ml_album_list_t, VLC_ML_LIST_GENRE_ALBUMS, VLC_ML_COUNT_GENRE_ALBUMS, Album>;
|
||||
using GenreTracksList =
|
||||
List<vlc_ml_media_list_t, VLC_ML_LIST_GENRE_TRACKS, VLC_ML_COUNT_GENRE_TRACKS, Media>;
|
||||
|
||||
using PlaylistsList =
|
||||
List<vlc_ml_playlist_list_t, VLC_ML_LIST_PLAYLISTS, VLC_ML_COUNT_PLAYLISTS, Playlist>;
|
||||
using PlaylistMediaList =
|
||||
List<vlc_ml_media_list_t, VLC_ML_LIST_PLAYLIST_MEDIA, VLC_ML_COUNT_PLAYLIST_MEDIA, Media>;
|
||||
|
||||
using MediaFolderList =
|
||||
List<vlc_ml_media_list_t, VLC_ML_LIST_FOLDER_MEDIA, VLC_ML_COUNT_FOLDER_MEDIA, Media>;
|
||||
using SubfoldersList =
|
||||
List<vlc_ml_folder_list_t, VLC_ML_LIST_SUBFOLDERS, VLC_ML_COUNT_SUBFOLDERS, Folder>;
|
||||
|
||||
} // namespace ml
|
||||
|
||||
#endif /* ML_HPP */
|
|
@ -0,0 +1,603 @@
|
|||
/*****************************************************************************
|
||||
* upnp_server.cpp : UPnP server module
|
||||
*****************************************************************************
|
||||
* Copyright © 2024 VLC authors and VideoLAN
|
||||
*
|
||||
* Authors: Hamza Parnica <hparnica@gmail.com>
|
||||
* Alaric Senat <alaric@videolabs.io>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation; either version 2.1 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with this program; if not, write to the Free Software Foundation,
|
||||
* Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
|
||||
*****************************************************************************/
|
||||
#ifdef HAVE_CONFIG_H
|
||||
#include "config.h"
|
||||
#endif
|
||||
|
||||
#include <atomic>
|
||||
#include <cstring>
|
||||
#include <services_discovery/upnp-wrapper.hpp>
|
||||
|
||||
#include <vlc_common.h>
|
||||
|
||||
#include <vlc_addons.h>
|
||||
#include <vlc_interface.h>
|
||||
#include <vlc_rand.h>
|
||||
|
||||
#include "FileHandler.hpp"
|
||||
#include "cds/Container.hpp"
|
||||
#include "cds/cds.hpp"
|
||||
#include "ml.hpp"
|
||||
#include "upnp_server.hpp"
|
||||
#include "utils.hpp"
|
||||
#include "xml_wrapper.hpp"
|
||||
|
||||
#define CDS_ID "urn:upnp-org:serviceId:ContentDirectory"
|
||||
#define CMS_ID "urn:upnp-org:serviceId:ConnectionManager"
|
||||
|
||||
#define UPNP_SERVICE_TYPE(service) "urn:schemas-upnp-org:service:" service ":1"
|
||||
|
||||
struct intf_sys_t
|
||||
{
|
||||
ml::MediaLibraryContext p_ml;
|
||||
std::unique_ptr<vlc_ml_event_callback_t, std::function<void(vlc_ml_event_callback_t *)>>
|
||||
ml_callback_handle;
|
||||
|
||||
std::unique_ptr<char, std::function<void(void *)>> uuid;
|
||||
|
||||
UpnpDevice_Handle p_device_handle;
|
||||
std::unique_ptr<UpnpInstanceWrapper, std::function<void(UpnpInstanceWrapper *)>> upnp;
|
||||
|
||||
// This integer is atomically incremented at each medialib modification. It will be sent in each
|
||||
// response. If the client notice that the update id has been incremented since the last
|
||||
// request, he knows that the server state has changed and hence can refetch the exposed
|
||||
// hierarchy.
|
||||
std::atomic<unsigned int> upnp_update_id;
|
||||
|
||||
std::vector<std::unique_ptr<cds::Object>> obj_hierarchy;
|
||||
|
||||
bool extra_verbose;
|
||||
};
|
||||
|
||||
static void medialibrary_event_callback(void *p_data, const struct vlc_ml_event_t *p_event)
|
||||
{
|
||||
intf_thread_t *p_intf = (intf_thread_t *)p_data;
|
||||
intf_sys_t *p_sys = p_intf->p_sys;
|
||||
|
||||
switch (p_event->i_type)
|
||||
{
|
||||
case VLC_ML_EVENT_MEDIA_ADDED:
|
||||
case VLC_ML_EVENT_MEDIA_UPDATED:
|
||||
case VLC_ML_EVENT_MEDIA_DELETED:
|
||||
case VLC_ML_EVENT_ARTIST_ADDED:
|
||||
case VLC_ML_EVENT_ARTIST_UPDATED:
|
||||
case VLC_ML_EVENT_ARTIST_DELETED:
|
||||
case VLC_ML_EVENT_ALBUM_ADDED:
|
||||
case VLC_ML_EVENT_ALBUM_UPDATED:
|
||||
case VLC_ML_EVENT_ALBUM_DELETED:
|
||||
case VLC_ML_EVENT_PLAYLIST_ADDED:
|
||||
case VLC_ML_EVENT_PLAYLIST_UPDATED:
|
||||
case VLC_ML_EVENT_PLAYLIST_DELETED:
|
||||
case VLC_ML_EVENT_GENRE_ADDED:
|
||||
case VLC_ML_EVENT_GENRE_UPDATED:
|
||||
case VLC_ML_EVENT_GENRE_DELETED:
|
||||
p_sys->upnp_update_id++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static bool cds_browse(UpnpActionRequest *request, intf_thread_t *intf)
|
||||
{
|
||||
intf_sys_t *sys = intf->p_sys;
|
||||
|
||||
auto *action_rq =
|
||||
reinterpret_cast<IXML_Element *>(UpnpActionRequest_get_ActionRequest(request));
|
||||
|
||||
const char *object_id = xml_getChildElementValue(action_rq, "ObjectID");
|
||||
if (object_id == nullptr)
|
||||
return false;
|
||||
|
||||
const char *browse_flag = xml_getChildElementValue(action_rq, "BrowseFlag");
|
||||
const char *starting_index = xml_getChildElementValue(action_rq, "StartingIndex");
|
||||
const char *requested_count = xml_getChildElementValue(action_rq, "RequestedCount");
|
||||
|
||||
if (browse_flag == nullptr || starting_index == nullptr || requested_count == nullptr)
|
||||
return false;
|
||||
|
||||
cds::Container::BrowseParams browse_params{
|
||||
static_cast<uint32_t>(strtoul(starting_index, nullptr, 10)),
|
||||
static_cast<uint32_t>(strtoul(requested_count, nullptr, 10)),
|
||||
};
|
||||
|
||||
xml::Document result;
|
||||
xml::Element didl_lite = result.create_element("DIDL-Lite");
|
||||
|
||||
// Standard upnp attributes
|
||||
didl_lite.set_attribute("xmlns", "urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/");
|
||||
didl_lite.set_attribute("xmlns:dc", "http://purl.org/dc/elements/1.1/");
|
||||
didl_lite.set_attribute("xmlns:upnp", "urn:schemas-upnp-org:metadata-1-0/upnp/");
|
||||
|
||||
const std::string id = object_id;
|
||||
unsigned obj_idx;
|
||||
cds::Object::ExtraId extra_id;
|
||||
|
||||
try
|
||||
{
|
||||
std::tie(obj_idx, extra_id) = cds::parse_id(id);
|
||||
}
|
||||
catch (const std::invalid_argument &e)
|
||||
{
|
||||
UpnpActionRequest_set_ErrCode(request, 500);
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string str_nb_returned;
|
||||
std::string str_total_matches;
|
||||
const cds::Object &obj = *sys->obj_hierarchy[obj_idx];
|
||||
try
|
||||
{
|
||||
if (strcmp(browse_flag, "BrowseDirectChildren") == 0 &&
|
||||
obj.type == cds::Object::Type::Container)
|
||||
{
|
||||
const auto browse_stats =
|
||||
static_cast<const cds::Container &>(obj).browse_direct_children(
|
||||
didl_lite, browse_params, extra_id);
|
||||
str_nb_returned = std::to_string(browse_stats.result_count);
|
||||
str_total_matches = std::to_string(browse_stats.total_matches);
|
||||
}
|
||||
else if (strcmp(browse_flag, "BrowseMetadata") == 0)
|
||||
{
|
||||
didl_lite.add_child(obj.browse_metadata(result, extra_id));
|
||||
str_nb_returned = "1";
|
||||
str_total_matches = "1";
|
||||
}
|
||||
} catch (const ml::errors::ForbiddenAccess&)
|
||||
{
|
||||
msg_Warn(intf, "Client tried to browse a private medialibrary element.");
|
||||
UpnpActionRequest_set_ErrCode(request, 404);
|
||||
return false;
|
||||
} catch (const ml::errors::UnknownObject&)
|
||||
{
|
||||
UpnpActionRequest_set_ErrCode(request, 404);
|
||||
return false;
|
||||
}
|
||||
|
||||
result.set_entry(std::move(didl_lite));
|
||||
|
||||
const char *action_name = UpnpActionRequest_get_ActionName_cstr(request);
|
||||
|
||||
const auto up_reponse_str = result.to_wrapped_cstr();
|
||||
const auto str_update_id = std::to_string(sys->upnp_update_id.load());
|
||||
|
||||
auto *p_answer = UpnpActionRequest_get_ActionResult(request);
|
||||
|
||||
static constexpr char service_type[] = UPNP_SERVICE_TYPE("ContentDirectory");
|
||||
UpnpAddToActionResponse(&p_answer, action_name, service_type, "Result", up_reponse_str.get());
|
||||
UpnpAddToActionResponse(
|
||||
&p_answer, action_name, service_type, "NumberReturned", str_nb_returned.c_str());
|
||||
UpnpAddToActionResponse(
|
||||
&p_answer, action_name, service_type, "TotalMatches", str_total_matches.c_str());
|
||||
UpnpAddToActionResponse(
|
||||
&p_answer, action_name, service_type, "UpdateID", str_update_id.c_str());
|
||||
|
||||
UpnpActionRequest_set_ActionResult(request, p_answer);
|
||||
if (sys->extra_verbose)
|
||||
msg_Dbg(intf, "Sending response to client: \n%s", up_reponse_str.get());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static void handle_action_request(UpnpActionRequest *p_request, intf_thread_t *p_intf)
|
||||
{
|
||||
intf_sys_t *sys = p_intf->p_sys;
|
||||
|
||||
IXML_Document *action_result = nullptr;
|
||||
const char *service_id = UpnpActionRequest_get_ServiceID_cstr(p_request);
|
||||
const char *action_name = UpnpActionRequest_get_ActionName_cstr(p_request);
|
||||
const auto client_addr = utils::addr_to_string(UpnpActionRequest_get_CtrlPtIPAddr(p_request));
|
||||
msg_Dbg(p_intf,
|
||||
"Received action request \"%s\" for service \"%s\" from %s",
|
||||
action_name,
|
||||
service_id,
|
||||
client_addr.c_str());
|
||||
if (strcmp(service_id, CMS_ID) == 0)
|
||||
{
|
||||
static constexpr char cms_type[] = UPNP_SERVICE_TYPE("ConnectionManager");
|
||||
if (strcmp(action_name, "GetProtocolInfo") == 0)
|
||||
{
|
||||
UpnpAddToActionResponse(
|
||||
&action_result, action_name, cms_type, "Source", "http-get:*:*:*");
|
||||
UpnpAddToActionResponse(&action_result, action_name, cms_type, "Sink", "");
|
||||
UpnpActionRequest_set_ActionResult(p_request, action_result);
|
||||
}
|
||||
else if (strcmp(action_name, "GetCurrentConnectionIDs") == 0)
|
||||
{
|
||||
UpnpAddToActionResponse(&action_result, action_name, cms_type, "ConnectionIDs", "");
|
||||
UpnpActionRequest_set_ActionResult(p_request, action_result);
|
||||
}
|
||||
}
|
||||
|
||||
else if (strcmp(service_id, CDS_ID) == 0)
|
||||
{
|
||||
static constexpr char cds_type[] = UPNP_SERVICE_TYPE("ContentDirectory");
|
||||
|
||||
if (strcmp(action_name, "Browse") == 0)
|
||||
{
|
||||
if (!cds_browse(p_request, p_intf))
|
||||
msg_Err(p_intf, "Failed to respond to browse action request");
|
||||
}
|
||||
else if (strcmp(action_name, "GetSearchCapabilities") == 0)
|
||||
{
|
||||
UpnpAddToActionResponse(&action_result, action_name, cds_type, "SearchCaps", "");
|
||||
UpnpActionRequest_set_ActionResult(p_request, action_result);
|
||||
}
|
||||
else if (strcmp(action_name, "GetSortCapabilities") == 0)
|
||||
{
|
||||
UpnpAddToActionResponse(&action_result, action_name, cds_type, "SortCaps", "");
|
||||
UpnpActionRequest_set_ActionResult(p_request, action_result);
|
||||
}
|
||||
else if (strcmp(action_name, "GetSystemUpdateID") == 0)
|
||||
{
|
||||
char *psz_update_id;
|
||||
if (asprintf(&psz_update_id, "%d", sys->upnp_update_id.load()) == -1)
|
||||
return;
|
||||
auto up_update_id = vlc::wrap_cptr(psz_update_id);
|
||||
UpnpAddToActionResponse(&action_result, action_name, cds_type, "Id", psz_update_id);
|
||||
UpnpActionRequest_set_ActionResult(p_request, action_result);
|
||||
}
|
||||
}
|
||||
else if (strcmp(service_id, "urn:microsoft.com:serviceId:X_MS_MediaReceiverRegistrar") == 0)
|
||||
{
|
||||
// We need a mockup of this service to support Microsoft devices and clients.
|
||||
if (strcmp(action_name, "IsAuthorized") == 0)
|
||||
{
|
||||
UpnpAddToActionResponse(&action_result, action_name, CDS_ID, "Result", "1");
|
||||
UpnpActionRequest_set_ActionResult(p_request, action_result);
|
||||
}
|
||||
else if (strcmp(action_name, "IsValidated") == 0)
|
||||
{
|
||||
UpnpAddToActionResponse(&action_result, action_name, CDS_ID, "Result", "1");
|
||||
UpnpActionRequest_set_ActionResult(p_request, action_result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static int Callback(Upnp_EventType event_type, const void *event, void *cookie)
|
||||
{
|
||||
auto *intf = static_cast<intf_thread_t *>(cookie);
|
||||
|
||||
switch (event_type)
|
||||
{
|
||||
case UPNP_CONTROL_ACTION_REQUEST:
|
||||
{
|
||||
msg_Dbg(intf, "Action request");
|
||||
// We need to const_cast here because the upnp callback has to take a const void* for
|
||||
// the event data even if the sdk also expect us to modify it sometimes (in the case of
|
||||
// a upnp response for example)
|
||||
auto *rq =
|
||||
const_cast<UpnpActionRequest *>(static_cast<const UpnpActionRequest *>(event));
|
||||
handle_action_request(rq, intf);
|
||||
}
|
||||
break;
|
||||
case UPNP_CONTROL_GET_VAR_REQUEST:
|
||||
msg_Dbg(intf, "Var request");
|
||||
break;
|
||||
|
||||
case UPNP_EVENT_SUBSCRIPTION_REQUEST:
|
||||
msg_Dbg(intf, "Sub request");
|
||||
break;
|
||||
|
||||
default:
|
||||
msg_Err(intf, "Unhandled event: %d", event_type);
|
||||
return UPNP_E_INVALID_ACTION;
|
||||
}
|
||||
|
||||
return UPNP_E_SUCCESS;
|
||||
}
|
||||
|
||||
// UPNP Callbacks
|
||||
|
||||
static int
|
||||
getinfo_cb(const char *url, UpnpFileInfo *info, intf_thread_t *intf, FileHandler **fhandler)
|
||||
{
|
||||
const char *user_agent = UpnpFileInfo_get_Os_cstr(info);
|
||||
if (user_agent == nullptr)
|
||||
return UPNP_E_BAD_REQUEST;
|
||||
|
||||
msg_Dbg(intf, "GetInfo callback on: \"%s\" from: \"%s\"", url, user_agent);
|
||||
|
||||
UpnpFileInfo_set_IsReadable(info, true);
|
||||
UpnpFileInfo_set_IsDirectory(info, false);
|
||||
|
||||
auto file_handler = parse_url(url, intf->p_sys->p_ml);
|
||||
|
||||
if (file_handler == nullptr)
|
||||
return UPNP_E_FILE_NOT_FOUND;
|
||||
|
||||
file_handler->get_info(*info);
|
||||
|
||||
// Pass the filehandler ownership to the open callback to avoid reparsing the url
|
||||
*fhandler = file_handler.release();
|
||||
|
||||
return UPNP_E_SUCCESS;
|
||||
}
|
||||
|
||||
static UpnpWebFileHandle
|
||||
open_cb(const char *url, enum UpnpOpenFileMode, intf_thread_t *intf, FileHandler *file_handler)
|
||||
{
|
||||
msg_Dbg(intf, "Opening: %s", url);
|
||||
|
||||
FileHandler *ret = file_handler;
|
||||
if (!ret)
|
||||
ret = parse_url(url, intf->p_sys->p_ml).release();
|
||||
|
||||
if (ret == nullptr)
|
||||
return nullptr;
|
||||
|
||||
if (!ret->open(VLC_OBJECT(intf)))
|
||||
{
|
||||
msg_Err(intf, "Failed to open %s", url);
|
||||
delete ret;
|
||||
return nullptr;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
static int
|
||||
read_cb(UpnpWebFileHandle fileHnd, uint8_t buf[], size_t buflen, intf_thread_t *intf, const void *)
|
||||
{
|
||||
assert(fileHnd);
|
||||
|
||||
auto *impl = static_cast<FileHandler *>(fileHnd);
|
||||
const size_t bytes_read = impl->read(buf, buflen);
|
||||
|
||||
msg_Dbg(intf, "http read callback, %zub requested %zub returned", buflen, bytes_read);
|
||||
|
||||
return bytes_read;
|
||||
}
|
||||
|
||||
static int
|
||||
seek_cb(UpnpWebFileHandle fileHnd, off_t offset, int origin, intf_thread_t *intf, const void *)
|
||||
{
|
||||
assert(fileHnd);
|
||||
msg_Dbg(
|
||||
intf, "http seek callback offset: %jd origin: %d", static_cast<intmax_t>(offset), origin);
|
||||
|
||||
auto *impl = static_cast<FileHandler *>(fileHnd);
|
||||
const auto seek_type = static_cast<FileHandler::SeekType>(origin);
|
||||
const bool success = impl->seek(seek_type, offset);
|
||||
|
||||
return success == true ? 0 : -1;
|
||||
}
|
||||
|
||||
static int close_cb(UpnpWebFileHandle fileHnd, intf_thread_t *intf, const void *)
|
||||
{
|
||||
assert(fileHnd);
|
||||
msg_Dbg(intf, "http close callback");
|
||||
delete static_cast<FileHandler *>(fileHnd);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static xml::Document make_server_identity(const char *uuid, const char *server_name)
|
||||
{
|
||||
xml::Document ret;
|
||||
|
||||
const auto icon_elem =
|
||||
[&ret](const char *url, const char *width, const char *height) -> xml::Element {
|
||||
return ret.create_element("icon",
|
||||
ret.create_element("mimetype", ret.create_text_node("image/png")),
|
||||
ret.create_element("width", ret.create_text_node(width)),
|
||||
ret.create_element("height", ret.create_text_node(height)),
|
||||
ret.create_element("depth", ret.create_text_node("8")),
|
||||
ret.create_element("url", ret.create_text_node(url)));
|
||||
};
|
||||
|
||||
const auto service_elem = [&ret](const char *service) -> xml::Element {
|
||||
const auto type = std::string("urn:schemas-upnp-org:service:") + service + ":1";
|
||||
const auto id = std::string("urn:upnp-org:serviceId:") + service;
|
||||
const auto scpd_url = std::string("/") + service + ".xml";
|
||||
const auto control_url = std::string("/") + service + "/Control";
|
||||
const auto event_url = std::string("/") + service + "/Event";
|
||||
return ret.create_element(
|
||||
"service",
|
||||
ret.create_element("serviceType", ret.create_text_node(type.c_str())),
|
||||
ret.create_element("serviceId", ret.create_text_node(id.c_str())),
|
||||
ret.create_element("SCPDURL", ret.create_text_node(scpd_url.c_str())),
|
||||
ret.create_element("controlURL", ret.create_text_node(control_url.c_str())),
|
||||
ret.create_element("eventSubURL", ret.create_text_node(event_url.c_str())));
|
||||
};
|
||||
|
||||
const std::string url = utils::get_server_url();
|
||||
|
||||
const auto uuid_attr = std::string("uuid:") + uuid;
|
||||
|
||||
xml::Element dlna_doc = ret.create_element("dlna:X_DLNADOC", ret.create_text_node("DMS-1.50"));
|
||||
dlna_doc.set_attribute("xmlns:dlna", "urn:schemas-dlna-org:device-1-0");
|
||||
|
||||
xml::Element root = ret.create_element(
|
||||
"root",
|
||||
ret.create_element("specVersion",
|
||||
ret.create_element("major", ret.create_text_node("1")),
|
||||
ret.create_element("minor", ret.create_text_node("0"))),
|
||||
ret.create_element(
|
||||
"device",
|
||||
std::move(dlna_doc),
|
||||
|
||||
ret.create_element("deviceType",
|
||||
ret.create_text_node("urn:schemas-upnp-org:device:MediaServer:1")),
|
||||
ret.create_element("presentationUrl", ret.create_text_node(url.c_str())),
|
||||
ret.create_element("friendlyName", ret.create_text_node(server_name)),
|
||||
ret.create_element("manufacturer", ret.create_text_node("VideoLAN")),
|
||||
ret.create_element("manufacturerURL", ret.create_text_node("https://videolan.org")),
|
||||
ret.create_element("modelDescription", ret.create_text_node("VLC UPNP Media Server")),
|
||||
ret.create_element("modelName", ret.create_text_node("VLC")),
|
||||
ret.create_element("modelNumber", ret.create_text_node(PACKAGE_VERSION)),
|
||||
ret.create_element("modelURL", ret.create_text_node("https://videolan.org/vlc/")),
|
||||
ret.create_element("serialNumber", ret.create_text_node("1")),
|
||||
ret.create_element("UDN", ret.create_text_node(uuid_attr.c_str())),
|
||||
ret.create_element("iconList",
|
||||
icon_elem("/vlc.png", "32", "32"),
|
||||
icon_elem("/vlc512x512.png", "512", "512")),
|
||||
ret.create_element(
|
||||
"serviceList",
|
||||
service_elem("ConnectionManager"),
|
||||
service_elem("ContentDirectory"),
|
||||
|
||||
ret.create_element(
|
||||
"service",
|
||||
ret.create_element(
|
||||
"serviceType",
|
||||
ret.create_text_node(
|
||||
"urn:microsoft.com:service:X_MS_MediaReceiverRegistrar:1")),
|
||||
ret.create_element(
|
||||
"serviceId",
|
||||
ret.create_text_node(
|
||||
"urn:microsoft.com:serviceId:X_MS_MediaReceiverRegistrar")),
|
||||
ret.create_element("SCPDURL",
|
||||
ret.create_text_node("/X_MS_MediaReceiverRegistrar.xml")),
|
||||
ret.create_element(
|
||||
"controlURL", ret.create_text_node("/X_MS_MediaReceiverRegistrar/Control")),
|
||||
ret.create_element(
|
||||
"eventSubURL",
|
||||
ret.create_text_node("/X_MS_MediaReceiverRegistrar/Event"))))));
|
||||
root.set_attribute("xmlns", "urn:schemas-upnp-org:device-1-0");
|
||||
|
||||
ret.set_entry(std::move(root));
|
||||
return ret;
|
||||
}
|
||||
|
||||
static bool init_upnp(intf_thread_t *intf)
|
||||
{
|
||||
intf_sys_t *sys = intf->p_sys;
|
||||
|
||||
addon_uuid_t uuid;
|
||||
vlc_rand_bytes(uuid, sizeof(uuid));
|
||||
sys->uuid = vlc::wrap_cptr(addons_uuid_to_psz(&uuid), &free);
|
||||
|
||||
int res = UpnpEnableWebserver(true);
|
||||
if (res != UPNP_E_SUCCESS)
|
||||
{
|
||||
msg_Err(intf, "Enabling libupnp webserver failed: %s", UpnpGetErrorMessage(res));
|
||||
return false;
|
||||
}
|
||||
|
||||
msg_Info(intf, "Upnp server enabled on %s:%d", UpnpGetServerIpAddress(), UpnpGetServerPort());
|
||||
|
||||
const auto str_rootdir = utils::get_root_dir();
|
||||
|
||||
res = UpnpSetWebServerRootDir(str_rootdir.c_str());
|
||||
msg_Dbg(intf, "Webserver root dir set to: \"%s\"", str_rootdir.c_str());
|
||||
if (res != UPNP_E_SUCCESS)
|
||||
{
|
||||
msg_Err(intf, "Setting webserver root dir failed: %s", UpnpGetErrorMessage(res));
|
||||
UpnpEnableWebserver(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto server_name = vlc::wrap_cptr(var_InheritString(intf, SERVER_PREFIX "name"), &free);
|
||||
assert(server_name);
|
||||
const auto presentation_doc = make_server_identity(sys->uuid.get(), server_name.get());
|
||||
const auto up_presentation_str = presentation_doc.to_wrapped_cstr();
|
||||
|
||||
if (sys->extra_verbose)
|
||||
msg_Dbg(intf, "%s", up_presentation_str.get());
|
||||
|
||||
res = UpnpRegisterRootDevice2(UPNPREG_BUF_DESC,
|
||||
up_presentation_str.get(),
|
||||
strlen(up_presentation_str.get()),
|
||||
1,
|
||||
Callback,
|
||||
intf,
|
||||
&sys->p_device_handle);
|
||||
if (res != UPNP_E_SUCCESS)
|
||||
{
|
||||
msg_Err(intf, "Registration failed: %s", UpnpGetErrorMessage(res));
|
||||
UpnpEnableWebserver(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
UpnpVirtualDir_set_GetInfoCallback(reinterpret_cast<VDCallback_GetInfo>(getinfo_cb));
|
||||
UpnpVirtualDir_set_OpenCallback(reinterpret_cast<VDCallback_Open>(open_cb));
|
||||
UpnpVirtualDir_set_ReadCallback(reinterpret_cast<VDCallback_Read>(read_cb));
|
||||
UpnpVirtualDir_set_SeekCallback(reinterpret_cast<VDCallback_Seek>(seek_cb));
|
||||
UpnpVirtualDir_set_CloseCallback(reinterpret_cast<VDCallback_Close>(close_cb));
|
||||
|
||||
UpnpAddVirtualDir("/media", intf, nullptr);
|
||||
UpnpAddVirtualDir("/thumbnail", intf, nullptr);
|
||||
UpnpAddVirtualDir("/subtitle", intf, nullptr);
|
||||
|
||||
res = UpnpSendAdvertisement(sys->p_device_handle, 1800);
|
||||
if (res != UPNP_E_SUCCESS)
|
||||
{
|
||||
msg_Dbg(intf, "Advertisement failed: %s", UpnpGetErrorMessage(res));
|
||||
UpnpUnRegisterRootDevice(sys->p_device_handle);
|
||||
UpnpEnableWebserver(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
sys->upnp_update_id = 0;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
namespace Server
|
||||
{
|
||||
|
||||
int open(vlc_object_t *p_this)
|
||||
{
|
||||
intf_thread_t *intf = container_of(p_this, intf_thread_t, obj);
|
||||
intf_sys_t *sys = new (std::nothrow) intf_sys_t;
|
||||
if (unlikely(sys == nullptr))
|
||||
return VLC_ENOMEM;
|
||||
intf->p_sys = sys;
|
||||
|
||||
const bool share_private_media = var_InheritBool(p_this, SERVER_PREFIX "share-private-media");
|
||||
sys->p_ml = ml::MediaLibraryContext{vlc_ml_instance_get(p_this), share_private_media};
|
||||
if (!sys->p_ml.handle)
|
||||
{
|
||||
msg_Err(intf, "Medialibrary not initialized");
|
||||
delete sys;
|
||||
return VLC_EGENERIC;
|
||||
}
|
||||
|
||||
vlc_ml_event_callback_t *cb =
|
||||
vlc_ml_event_register_callback(sys->p_ml.handle, medialibrary_event_callback, intf);
|
||||
const auto release_cb = [sys](vlc_ml_event_callback_t *cb) {
|
||||
vlc_ml_event_unregister_callback(sys->p_ml.handle, cb);
|
||||
};
|
||||
sys->ml_callback_handle = {cb, release_cb};
|
||||
|
||||
sys->upnp = vlc::wrap_cptr(UpnpInstanceWrapper::get(p_this),
|
||||
[](UpnpInstanceWrapper *p_upnp) { p_upnp->release(); });
|
||||
|
||||
sys->obj_hierarchy = cds::init_hierarchy(sys->p_ml);
|
||||
sys->extra_verbose = var_InheritInteger(p_this, "verbose") >= 4;
|
||||
|
||||
if (!init_upnp(intf))
|
||||
{
|
||||
delete sys;
|
||||
return VLC_EGENERIC;
|
||||
}
|
||||
|
||||
return VLC_SUCCESS;
|
||||
}
|
||||
|
||||
void close(vlc_object_t *p_this)
|
||||
{
|
||||
intf_thread_t *intf = container_of(p_this, intf_thread_t, obj);
|
||||
intf_sys_t *sys = intf->p_sys;
|
||||
UpnpUnRegisterRootDevice(sys->p_device_handle);
|
||||
UpnpEnableWebserver(false);
|
||||
delete sys;
|
||||
}
|
||||
} // namespace Server
|
|
@ -0,0 +1,45 @@
|
|||
/*****************************************************************************
|
||||
* upnp_server.hpp : UPnP server module header
|
||||
*****************************************************************************
|
||||
* Copyright © 2024 VLC authors and VideoLAN
|
||||
*
|
||||
* Authors: Hamza Parnica <hparnica@gmail.com>
|
||||
* Alaric Senat <alaric@videolabs.io>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation; either version 2.1 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with this program; if not, write to the Free Software Foundation,
|
||||
* Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
|
||||
*****************************************************************************/
|
||||
#ifndef UPNP_SERVER_HPP
|
||||
#define UPNP_SERVER_HPP
|
||||
|
||||
#define SERVER_PREFIX "upnp-server-"
|
||||
|
||||
#define SERVER_DEFAULT_NAME N_("VLC Media Server")
|
||||
#define SERVER_NAME_DESC N_("Upnp server name")
|
||||
#define SERVER_NAME_LONGTEXT N_("The client exposed upnp server name")
|
||||
#define SERVER_SHARE_PRIVATE_MEDIA_TEXT N_("Share private media")
|
||||
#define SERVER_SHARE_PRIVATE_MEDIA_LONGTEXT \
|
||||
N_("Every media indexed by the media library will be exposed by the UPNP server regardless " \
|
||||
"of their public/private status. This option needs to be explicitely set at each startup " \
|
||||
"of VLC to avoid unnoticed private media leaks on the network.")
|
||||
|
||||
struct vlc_object_t;
|
||||
|
||||
namespace Server
|
||||
{
|
||||
int open(vlc_object_t *p_this);
|
||||
void close(vlc_object_t *p_this);
|
||||
} // namespace Server
|
||||
|
||||
#endif /* UPNP_SERVER_HPP */
|
|
@ -0,0 +1,233 @@
|
|||
/*****************************************************************************
|
||||
* Clients.cpp
|
||||
*****************************************************************************
|
||||
* Copyright © 2024 VLC authors and VideoLAN
|
||||
*
|
||||
* Authors: Alaric Senat <alaric@videolabs.io>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation; either version 2.1 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with this program; if not, write to the Free Software Foundation,
|
||||
* Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
|
||||
*****************************************************************************/
|
||||
#ifdef HAVE_CONFIG_H
|
||||
#include "config.h"
|
||||
#endif
|
||||
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
#include <sstream>
|
||||
#include <stream_out/dlna/dlna.hpp>
|
||||
#include <string>
|
||||
#include <upnp/upnp.h>
|
||||
#if UPNP_VERSION >= 11400
|
||||
#include <upnp/UpnpExtraHeaders.h>
|
||||
#else
|
||||
#include <upnp/ExtraHeaders.h>
|
||||
#endif
|
||||
|
||||
#if defined(_WIN32)
|
||||
#include <winsock2.h>
|
||||
#elif defined(HAVE_SYS_SOCKET_H)
|
||||
#include <sys/socket.h>
|
||||
#endif
|
||||
|
||||
#include <vlc_common.h>
|
||||
|
||||
#include <vlc_configuration.h>
|
||||
|
||||
#include "utils.hpp"
|
||||
|
||||
namespace utils
|
||||
{
|
||||
MimeType get_mimetype(vlc_ml_media_type_t type, const std::string &file_extension) noexcept
|
||||
{
|
||||
const char *mime_end = file_extension.c_str();
|
||||
// special case for the transcode muxer to be widely accepted by a majority of players.
|
||||
if (file_extension == "ts")
|
||||
mime_end = "mpeg";
|
||||
else if (file_extension == "mp3")
|
||||
mime_end = "mpeg";
|
||||
switch (type)
|
||||
{
|
||||
case VLC_ML_MEDIA_TYPE_AUDIO:
|
||||
return {"audio", mime_end};
|
||||
case VLC_ML_MEDIA_TYPE_UNKNOWN: // Intended pass through
|
||||
case VLC_ML_MEDIA_TYPE_VIDEO:
|
||||
return {"video", mime_end};
|
||||
default:
|
||||
vlc_assert_unreachable();
|
||||
}
|
||||
}
|
||||
|
||||
std::string file_extension(const std::string &file)
|
||||
{
|
||||
auto pos = file.find_last_of('.');
|
||||
if (pos == std::string::npos)
|
||||
return {};
|
||||
return file.substr(pos + 1);
|
||||
}
|
||||
|
||||
std::string get_server_url()
|
||||
{
|
||||
// TODO support ipv6
|
||||
const std::string addr = UpnpGetServerIpAddress();
|
||||
const std::string port = std::to_string(UpnpGetServerPort());
|
||||
return "http://" + addr + ':' + port + '/';
|
||||
}
|
||||
|
||||
std::string get_root_dir()
|
||||
{
|
||||
std::stringstream ret;
|
||||
|
||||
char *path = config_GetSysPath(VLC_PKG_DATA_DIR, NULL);
|
||||
assert(path);
|
||||
|
||||
ret << path << "/upnp_server/";
|
||||
|
||||
free(path);
|
||||
return ret.str();
|
||||
}
|
||||
|
||||
std::string addr_to_string(const sockaddr_storage *addr)
|
||||
{
|
||||
const void *ip;
|
||||
if (addr->ss_family == AF_INET6)
|
||||
{
|
||||
ip = &reinterpret_cast<const struct sockaddr_in6 *>(addr)->sin6_addr;
|
||||
}
|
||||
else
|
||||
{
|
||||
ip = &reinterpret_cast<const struct sockaddr_in *>(addr)->sin_addr;
|
||||
}
|
||||
|
||||
char buff[INET6_ADDRSTRLEN];
|
||||
inet_ntop(addr->ss_family, ip, buff, sizeof(buff));
|
||||
return buff;
|
||||
}
|
||||
|
||||
std::vector<MediaTrackRef> get_media_tracks(const vlc_ml_media_t &media, vlc_ml_track_type_t type)
|
||||
{
|
||||
std::vector<MediaTrackRef> ret;
|
||||
|
||||
if (media.p_tracks == nullptr)
|
||||
return ret;
|
||||
for (unsigned i = 0; i < media.p_tracks->i_nb_items; ++i)
|
||||
{
|
||||
const auto &track = media.p_tracks->p_items[i];
|
||||
if (track.i_type == type)
|
||||
ret.emplace_back(track);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
std::vector<MediaFileRef> get_media_files(const vlc_ml_media_t &media, vlc_ml_file_type_t type)
|
||||
{
|
||||
std::vector<MediaFileRef> ret;
|
||||
|
||||
if (media.p_files == nullptr)
|
||||
return ret;
|
||||
for (unsigned i = 0; i < media.p_files->i_nb_items; ++i)
|
||||
{
|
||||
const auto &file = media.p_files->p_items[i];
|
||||
if (file.i_type == type)
|
||||
ret.emplace_back(file);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
std::string album_thumbnail_url(const vlc_ml_album_t &album)
|
||||
{
|
||||
const auto &thumbnail = album.thumbnails[VLC_ML_THUMBNAIL_SMALL];
|
||||
if (thumbnail.i_status != VLC_ML_THUMBNAIL_STATUS_AVAILABLE)
|
||||
return "";
|
||||
const auto thumbnail_extension = file_extension(std::string(thumbnail.psz_mrl));
|
||||
return get_server_url() + "thumbnail/small/album/" + std::to_string(album.i_id) + "." +
|
||||
thumbnail_extension;
|
||||
}
|
||||
|
||||
std::string thumbnail_url(const vlc_ml_media_t &media, vlc_ml_thumbnail_size_t size)
|
||||
{
|
||||
std::stringstream ss;
|
||||
const std::string s_size = size == VLC_ML_THUMBNAIL_SMALL ? "small" : "banner";
|
||||
const auto &thumbnail = media.thumbnails[size];
|
||||
|
||||
const auto thumbnail_extension = file_extension(std::string(thumbnail.psz_mrl));
|
||||
ss << get_server_url() << "thumbnail/" << s_size << "/media/" << media.i_id << '.'
|
||||
<< thumbnail_extension;
|
||||
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
namespace http
|
||||
{
|
||||
|
||||
static UpnpExtraHeaders *get_hdr(UpnpListHead *list, const std::string name)
|
||||
{
|
||||
for (auto *it = UpnpListBegin(list); it != UpnpListEnd(list); it = UpnpListNext(list, it))
|
||||
{
|
||||
UpnpExtraHeaders *hd = reinterpret_cast<UpnpExtraHeaders *>(it);
|
||||
std::string hdr_name = UpnpExtraHeaders_get_name_cstr(hd);
|
||||
// std::cerr << hdr_name << ": " << UpnpExtraHeaders_get_value_cstr( hd ) << "\n";
|
||||
std::transform(std::begin(hdr_name), std::end(hdr_name), std::begin(hdr_name), ::tolower);
|
||||
if (hdr_name == name)
|
||||
{
|
||||
return hd;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void add_response_hdr(UpnpListHead *list, const std::pair<std::string, std::string> resp)
|
||||
{
|
||||
|
||||
auto *hdr = get_hdr(list, resp.first);
|
||||
const auto resp_str = resp.first + ": " + resp.second;
|
||||
if (!hdr)
|
||||
{
|
||||
hdr = UpnpExtraHeaders_new();
|
||||
}
|
||||
UpnpExtraHeaders_set_resp(hdr, resp_str.c_str());
|
||||
UpnpListInsert(
|
||||
list, UpnpListEnd(list), const_cast<UpnpListHead *>(UpnpExtraHeaders_get_node(hdr)));
|
||||
}
|
||||
|
||||
std::string get_dlna_extra_protocol_info(const MimeType &mime)
|
||||
{
|
||||
// TODO We should change that to a better profile selection using profiles in dlna.hpp
|
||||
// as soon as more info on media tracks are available in the medialibrary
|
||||
dlna_profile_t profile;
|
||||
if (mime.media_type == "audio")
|
||||
profile = default_audio_profile;
|
||||
else if (mime.media_type == "image")
|
||||
profile = default_image_profile;
|
||||
else
|
||||
profile = default_video_profile;
|
||||
profile.mime = mime.combine();
|
||||
|
||||
const protocol_info_t info{
|
||||
DLNA_TRANSPORT_PROTOCOL_HTTP,
|
||||
DLNA_ORG_CONVERSION_NONE,
|
||||
profile,
|
||||
};
|
||||
|
||||
const dlna_org_flags_t flags = DLNA_ORG_FLAG_STREAMING_TRANSFER_MODE |
|
||||
DLNA_ORG_FLAG_BACKGROUND_TRANSFERT_MODE |
|
||||
DLNA_ORG_FLAG_CONNECTION_STALL | DLNA_ORG_FLAG_DLNA_V15;
|
||||
const dlna_org_operation_t op = DLNA_ORG_OPERATION_RANGE;
|
||||
|
||||
return dlna_write_protocol_info(info, flags, op);
|
||||
}
|
||||
|
||||
} // namespace http
|
||||
|
||||
} // namespace utils
|
|
@ -0,0 +1,70 @@
|
|||
/*****************************************************************************
|
||||
* utils.hpp : UPnP server utils
|
||||
*****************************************************************************
|
||||
* Copyright © 2021 VLC authors and VideoLAN
|
||||
*
|
||||
* Authors: Alaric Senat <alaric@videolabs.io>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation; either version 2.1 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with this program; if not, write to the Free Software Foundation,
|
||||
* Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
|
||||
*****************************************************************************/
|
||||
#ifndef UTILS_HPP
|
||||
#define UTILS_HPP
|
||||
|
||||
#include <string>
|
||||
#include <upnp/list.h>
|
||||
#include <vector>
|
||||
|
||||
#include <vlc_media_library.h>
|
||||
|
||||
struct sockaddr_storage;
|
||||
|
||||
namespace utils
|
||||
{
|
||||
struct MimeType
|
||||
{
|
||||
std::string media_type;
|
||||
std::string file_type;
|
||||
|
||||
std::string combine() const { return media_type + '/' + file_type; }
|
||||
};
|
||||
|
||||
MimeType get_mimetype(vlc_ml_media_type_t type, const std::string &file_extension) noexcept;
|
||||
|
||||
std::string file_extension(const std::string &file);
|
||||
|
||||
std::string get_server_url();
|
||||
std::string get_root_dir();
|
||||
|
||||
std::string addr_to_string(const sockaddr_storage *addr);
|
||||
|
||||
template <typename T> using ConstRef = std::reference_wrapper<const T>;
|
||||
|
||||
using MediaTrackRef = ConstRef<vlc_ml_media_track_t>;
|
||||
std::vector<MediaTrackRef> get_media_tracks(const vlc_ml_media_t &media, vlc_ml_track_type_t type);
|
||||
using MediaFileRef = ConstRef<vlc_ml_file_t>;
|
||||
std::vector<MediaFileRef> get_media_files(const vlc_ml_media_t &media, vlc_ml_file_type_t);
|
||||
|
||||
std::string album_thumbnail_url(const vlc_ml_album_t &);
|
||||
std::string thumbnail_url(const vlc_ml_media_t &media, vlc_ml_thumbnail_size_t size);
|
||||
|
||||
namespace http
|
||||
{
|
||||
void add_response_hdr(UpnpListHead *list, const std::pair<std::string, std::string> resp);
|
||||
std::string get_dlna_extra_protocol_info(const MimeType &dlna_profile);
|
||||
} // namespace http
|
||||
|
||||
} // namespace utils
|
||||
|
||||
#endif /* UTILS_HPP */
|
|
@ -0,0 +1,114 @@
|
|||
/*****************************************************************************
|
||||
* xml_wrapper.hpp : Modern C++ ixml wrapper
|
||||
*****************************************************************************
|
||||
* Copyright © 2021 VLC authors and VideoLAN
|
||||
*
|
||||
* Authors: Alaric Senat <alaric@videolabs.io>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation; either version 2.1 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with this program; if not, write to the Free Software Foundation,
|
||||
* Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
|
||||
*****************************************************************************/
|
||||
#ifndef XML_WRAPPER_HPP
|
||||
#define XML_WRAPPER_HPP
|
||||
|
||||
#include <cassert>
|
||||
#include <ixml.h>
|
||||
#include <memory>
|
||||
|
||||
/// Simple C++ wrapper around libixml xml library.
|
||||
namespace xml
|
||||
{
|
||||
|
||||
struct Document;
|
||||
|
||||
struct Node
|
||||
{
|
||||
using Ptr = std::unique_ptr<IXML_Node, decltype(&ixmlNode_free)>;
|
||||
Ptr ptr;
|
||||
};
|
||||
|
||||
struct Element
|
||||
{
|
||||
using Ptr = std::unique_ptr<IXML_Element, decltype(&ixmlElement_free)>;
|
||||
|
||||
Ptr ptr;
|
||||
Document &owner;
|
||||
|
||||
void set_attribute(const char *name, const char *value)
|
||||
{
|
||||
assert(ptr != nullptr);
|
||||
ixmlElement_setAttribute(ptr.get(), name, value);
|
||||
}
|
||||
|
||||
void add_child(Node&& child)
|
||||
{
|
||||
assert(ptr != nullptr);
|
||||
if (child.ptr != nullptr)
|
||||
ixmlNode_appendChild(&ptr->n, child.ptr.release());
|
||||
}
|
||||
|
||||
void add_child(Element&& child)
|
||||
{
|
||||
assert(ptr != nullptr);
|
||||
if (child.ptr != nullptr)
|
||||
ixmlNode_appendChild(&ptr->n, &child.ptr.release()->n);
|
||||
}
|
||||
|
||||
template <typename... Child> void add_children(Child &&...child)
|
||||
{
|
||||
(add_child(std::move(child)), ...);
|
||||
}
|
||||
};
|
||||
|
||||
struct Document
|
||||
{
|
||||
using Ptr = std::unique_ptr<IXML_Document, decltype(&ixmlDocument_free)>;
|
||||
Ptr ptr;
|
||||
|
||||
Document() : ptr{ixmlDocument_createDocument(), &ixmlDocument_free} {}
|
||||
|
||||
Document(Ptr) = delete;
|
||||
Document(Ptr &&) = delete;
|
||||
|
||||
Node create_text_node(const char *text)
|
||||
{
|
||||
return Node{Node::Ptr{ixmlDocument_createTextNode(ptr.get(), text), ixmlNode_free}};
|
||||
}
|
||||
|
||||
Element create_element(const char *name)
|
||||
{
|
||||
return Element{Element::Ptr{ixmlDocument_createElement(ptr.get(), name), &ixmlElement_free},
|
||||
*this};
|
||||
}
|
||||
|
||||
template <typename... Children> Element create_element(const char *name, Children &&...children)
|
||||
{
|
||||
Element ret = create_element(name);
|
||||
ret.add_children(children...);
|
||||
return ret;
|
||||
}
|
||||
|
||||
void set_entry(Element &&entry) { ixmlNode_appendChild(&ptr->n, &entry.ptr.release()->n); }
|
||||
|
||||
using WrappedDOMString = std::unique_ptr<char, decltype(&ixmlFreeDOMString)>;
|
||||
|
||||
WrappedDOMString to_wrapped_cstr() const
|
||||
{
|
||||
return WrappedDOMString{ixmlDocumenttoString(ptr.get()), &ixmlFreeDOMString};
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace xml
|
||||
|
||||
#endif /* XML_WRAPPER_HPP */
|
|
@ -24,17 +24,42 @@ EXTRA_LTLIBRARIES += libmtp_plugin.la
|
|||
sd_LTLIBRARIES += $(LTLIBmtp)
|
||||
|
||||
libupnp_plugin_la_SOURCES = services_discovery/upnp.cpp services_discovery/upnp.hpp \
|
||||
services_discovery/upnp-wrapper.hpp \
|
||||
services_discovery/upnp-wrapper.cpp \
|
||||
stream_out/renderer_common.hpp \
|
||||
stream_out/renderer_common.cpp \
|
||||
stream_out/dlna/profile_names.hpp \
|
||||
stream_out/dlna/dlna_common.hpp \
|
||||
stream_out/dlna/dlna.hpp \
|
||||
stream_out/dlna/dlna.cpp
|
||||
services_discovery/upnp-wrapper.hpp \
|
||||
services_discovery/upnp-wrapper.cpp \
|
||||
stream_out/renderer_common.hpp \
|
||||
stream_out/renderer_common.cpp \
|
||||
stream_out/dlna/profile_names.hpp \
|
||||
stream_out/dlna/dlna_common.hpp \
|
||||
stream_out/dlna/dlna.hpp \
|
||||
stream_out/dlna/dlna.cpp \
|
||||
control/upnp_server/cds/Object.hpp \
|
||||
control/upnp_server/cds/Container.hpp \
|
||||
control/upnp_server/cds/FixedContainer.cpp \
|
||||
control/upnp_server/cds/FixedContainer.hpp \
|
||||
control/upnp_server/cds/Item.cpp \
|
||||
control/upnp_server/cds/Item.hpp \
|
||||
control/upnp_server/cds/MLContainer.hpp \
|
||||
control/upnp_server/cds/MLFolderContainer.hpp \
|
||||
control/upnp_server/cds/cds.cpp \
|
||||
control/upnp_server/cds/cds.hpp \
|
||||
control/upnp_server/ml.hpp \
|
||||
control/upnp_server/xml_wrapper.hpp \
|
||||
control/upnp_server/upnp_server.hpp \
|
||||
control/upnp_server/utils.cpp \
|
||||
control/upnp_server/utils.hpp \
|
||||
control/upnp_server/FileHandler.cpp \
|
||||
control/upnp_server/FileHandler.hpp \
|
||||
control/upnp_server/upnp_server.cpp
|
||||
libupnp_plugin_la_CXXFLAGS = $(AM_CXXFLAGS) $(UPNP_CFLAGS)
|
||||
libupnp_plugin_la_LDFLAGS = $(AM_LDFLAGS) -rpath '$(sddir)'
|
||||
libupnp_plugin_la_LIBADD = $(UPNP_LIBS)
|
||||
libupnp_plugin_la_RES = ../share/upnp_server/ContentDirectory.xml \
|
||||
../share/upnp_server/ConnectionManager.xml \
|
||||
../share/upnp_server/X_MS_MediaReceiverRegistrar.xml \
|
||||
../share/upnp_server/vlc.png \
|
||||
../share/upnp_server/vlc512x512.png
|
||||
dist_libupnp_plugin_la_DATA = $(libupnp_plugin_la_RES)
|
||||
libupnp_plugin_ladir = $(prefix)/share/vlc/upnp_server
|
||||
EXTRA_LTLIBRARIES += libupnp_plugin.la
|
||||
sd_LTLIBRARIES += $(LTLIBupnp)
|
||||
if HAVE_OSX
|
||||
|
|
|
@ -49,6 +49,24 @@ if upnp_dep.found()
|
|||
'upnp-wrapper.cpp',
|
||||
'../stream_out/renderer_common.cpp',
|
||||
'../stream_out/dlna/dlna.cpp',
|
||||
'../control/upnp_server/cds/Object.hpp',
|
||||
'../control/upnp_server/cds/Container.hpp',
|
||||
'../control/upnp_server/cds/FixedContainer.cpp',
|
||||
'../control/upnp_server/cds/FixedContainer.hpp',
|
||||
'../control/upnp_server/cds/Item.cpp',
|
||||
'../control/upnp_server/cds/Item.hpp',
|
||||
'../control/upnp_server/cds/MLContainer.hpp',
|
||||
'../control/upnp_server/cds/MLFolderContainer.hpp',
|
||||
'../control/upnp_server/cds/cds.cpp',
|
||||
'../control/upnp_server/cds/cds.hpp',
|
||||
'../control/upnp_server/ml.hpp',
|
||||
'../control/upnp_server/xml_wrapper.hpp',
|
||||
'../control/upnp_server/upnp_server.hpp',
|
||||
'../control/upnp_server/utils.cpp',
|
||||
'../control/upnp_server/utils.hpp',
|
||||
'../control/upnp_server/FileHandler.cpp',
|
||||
'../control/upnp_server/FileHandler.hpp',
|
||||
'../control/upnp_server/upnp_server.cpp',
|
||||
),
|
||||
'dependencies' : [upnp_dep, upnp_darwin_deps]
|
||||
}
|
||||
|
|
|
@ -180,6 +180,18 @@ vlc_module_begin()
|
|||
add_string(SOUT_CFG_PREFIX "base_url", NULL, BASE_URL_TEXT, BASE_URL_LONGTEXT)
|
||||
add_string(SOUT_CFG_PREFIX "url", NULL, URL_TEXT, URL_LONGTEXT)
|
||||
add_renderer_opts(SOUT_CFG_PREFIX)
|
||||
|
||||
add_submodule()
|
||||
set_shortname("UPnP Server");
|
||||
set_description(N_("Universal Plug'n'Play Server"));
|
||||
set_subcategory(SUBCAT_INTERFACE_MAIN);
|
||||
set_capability("interface", 0);
|
||||
set_callbacks(Server::open, Server::close);
|
||||
|
||||
add_string(SERVER_PREFIX "name", SERVER_DEFAULT_NAME, SERVER_NAME_DESC, SERVER_NAME_LONGTEXT)
|
||||
add_bool(SERVER_PREFIX "share-private-media", false, SERVER_SHARE_PRIVATE_MEDIA_TEXT, SERVER_SHARE_PRIVATE_MEDIA_LONGTEXT);
|
||||
change_volatile();
|
||||
|
||||
vlc_module_end()
|
||||
|
||||
/*
|
||||
|
|
|
@ -29,6 +29,7 @@
|
|||
|
||||
#include "upnp-wrapper.hpp"
|
||||
#include "../stream_out/dlna/dlna_common.hpp"
|
||||
#include "../control/upnp_server/upnp_server.hpp"
|
||||
|
||||
#include <vlc_url.h>
|
||||
#include <vlc_interrupt.h>
|
||||
|
|
|
@ -30,7 +30,6 @@
|
|||
#include <vector>
|
||||
#include <string>
|
||||
#include <sstream>
|
||||
#include <iomanip>
|
||||
|
||||
#include <vlc_configuration.h>
|
||||
#include <vlc_cxx_helpers.hpp>
|
||||
|
@ -108,34 +107,11 @@ static char *getServerIPAddress() {
|
|||
|
||||
std::string dlna_write_protocol_info (const protocol_info_t info)
|
||||
{
|
||||
std::ostringstream protocol;
|
||||
|
||||
if (info.transport == DLNA_TRANSPORT_PROTOCOL_HTTP)
|
||||
protocol << "http-get:*:";
|
||||
|
||||
protocol << info.profile.mime;
|
||||
protocol << ":";
|
||||
|
||||
if (info.profile.name != "*")
|
||||
protocol << "DLNA.ORG_PN=" << info.profile.name.c_str() << ";";
|
||||
|
||||
dlna_org_flags_t flags = DLNA_ORG_FLAG_STREAMING_TRANSFER_MODE |
|
||||
DLNA_ORG_FLAG_BACKGROUND_TRANSFERT_MODE |
|
||||
DLNA_ORG_FLAG_CONNECTION_STALL |
|
||||
DLNA_ORG_FLAG_DLNA_V15;
|
||||
|
||||
protocol << std::setfill('0')
|
||||
<< "DLNA.ORG_OP="
|
||||
<< std::hex << std::setw(2)
|
||||
<< DLNA_ORG_OPERATION_RANGE
|
||||
<< ";DLNA.ORG_CI="
|
||||
<< std::dec << info.ci
|
||||
<< ";DLNA.ORG_FLAGS="
|
||||
<< std::hex
|
||||
<< std::setw(8) << flags
|
||||
<< std::setw(24) << 0;
|
||||
|
||||
return protocol.str();
|
||||
const dlna_org_flags_t flags = DLNA_ORG_FLAG_STREAMING_TRANSFER_MODE |
|
||||
DLNA_ORG_FLAG_BACKGROUND_TRANSFERT_MODE |
|
||||
DLNA_ORG_FLAG_CONNECTION_STALL |
|
||||
DLNA_ORG_FLAG_DLNA_V15;
|
||||
return dlna_write_protocol_info(info, flags, DLNA_ORG_OPERATION_RANGE);
|
||||
}
|
||||
|
||||
std::vector<std::string> split(const std::string &s, char delim) {
|
||||
|
|
|
@ -28,6 +28,9 @@
|
|||
#include "dlna_common.hpp"
|
||||
#include "profile_names.hpp"
|
||||
|
||||
#include <sstream>
|
||||
#include <iomanip>
|
||||
|
||||
struct protocol_info_t {
|
||||
protocol_info_t() = default;
|
||||
protocol_info_t(const protocol_info_t&) = default;
|
||||
|
@ -47,6 +50,34 @@ struct protocol_info_t {
|
|||
dlna_profile_t profile;
|
||||
};
|
||||
|
||||
static inline std::string dlna_write_protocol_info(const protocol_info_t info,
|
||||
const dlna_org_flags_t flags,
|
||||
const dlna_org_operation_t op)
|
||||
{
|
||||
std::ostringstream protocol;
|
||||
|
||||
if (info.transport == DLNA_TRANSPORT_PROTOCOL_HTTP)
|
||||
protocol << "http-get:*:";
|
||||
|
||||
protocol << info.profile.mime;
|
||||
protocol << ":";
|
||||
|
||||
if (info.profile.name != "*")
|
||||
protocol << "DLNA.ORG_PN=" << info.profile.name.c_str() << ";";
|
||||
|
||||
protocol << std::setfill('0')
|
||||
<< "DLNA.ORG_OP="
|
||||
<< std::hex << std::setw(2) << op
|
||||
<< ";DLNA.ORG_CI="
|
||||
<< std::dec << info.ci
|
||||
<< ";DLNA.ORG_FLAGS="
|
||||
<< std::hex
|
||||
<< std::setw(8) << flags
|
||||
<< std::setw(24) << 0;
|
||||
|
||||
return protocol.str();
|
||||
}
|
||||
|
||||
using ProtocolPtr = std::unique_ptr<protocol_info_t>;
|
||||
static inline ProtocolPtr make_protocol(protocol_info_t a)
|
||||
{
|
||||
|
|
|
@ -167,7 +167,16 @@ const dlna_profile_t default_video_profile = {
|
|||
VLC_CODEC_MP4A,
|
||||
};
|
||||
|
||||
std::vector<dlna_profile_t> dlna_profile_list = {
|
||||
const dlna_profile_t default_image_profile = {
|
||||
"JPEG_MED",
|
||||
"jpg",
|
||||
"image/jpeg",
|
||||
DLNA_CLASS_IMAGE,
|
||||
VLC_CODEC_JPEG,
|
||||
VLC_CODEC_NONE,
|
||||
};
|
||||
|
||||
static const std::vector<dlna_profile_t> dlna_profile_list = {
|
||||
|
||||
default_audio_profile,
|
||||
default_video_profile,
|
||||
|
|
|
@ -0,0 +1,132 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<scpd xmlns="urn:schemas-upnp-org:service-1-0">
|
||||
<specVersion>
|
||||
<major>1</major>
|
||||
<minor>0</minor>
|
||||
</specVersion>
|
||||
<actionList>
|
||||
<action>
|
||||
<name>GetCurrentConnectionIDs</name>
|
||||
<argumentList>
|
||||
<argument>
|
||||
<name>ConnectionIDs</name>
|
||||
<direction>out</direction>
|
||||
<relatedStateVariable>CurrentConnectionIDs</relatedStateVariable>
|
||||
</argument>
|
||||
</argumentList>
|
||||
</action>
|
||||
<action>
|
||||
<name>GetCurrentConnectionInfo</name>
|
||||
<argumentList>
|
||||
<argument>
|
||||
<name>ConnectionID</name>
|
||||
<direction>in</direction>
|
||||
<relatedStateVariable>A_ARG_TYPE_ConnectionID</relatedStateVariable>
|
||||
</argument>
|
||||
<argument>
|
||||
<name>RcsID</name>
|
||||
<direction>out</direction>
|
||||
<relatedStateVariable>A_ARG_TYPE_RcsID</relatedStateVariable>
|
||||
</argument>
|
||||
<argument>
|
||||
<name>AVTransportID</name>
|
||||
<direction>out</direction>
|
||||
<relatedStateVariable>A_ARG_TYPE_AVTransportID</relatedStateVariable>
|
||||
</argument>
|
||||
<argument>
|
||||
<name>ProtocolInfo</name>
|
||||
<direction>out</direction>
|
||||
<relatedStateVariable>A_ARG_TYPE_ProtocolInfo</relatedStateVariable>
|
||||
</argument>
|
||||
<argument>
|
||||
<name>PeerConnectionManager</name>
|
||||
<direction>out</direction>
|
||||
<relatedStateVariable>A_ARG_TYPE_ConnectionManager</relatedStateVariable>
|
||||
</argument>
|
||||
<argument>
|
||||
<name>PeerConnectionID</name>
|
||||
<direction>out</direction>
|
||||
<relatedStateVariable>A_ARG_TYPE_ConnectionID</relatedStateVariable>
|
||||
</argument>
|
||||
<argument>
|
||||
<name>Direction</name>
|
||||
<direction>out</direction>
|
||||
<relatedStateVariable>A_ARG_TYPE_Direction</relatedStateVariable>
|
||||
</argument>
|
||||
<argument>
|
||||
<name>Status</name>
|
||||
<direction>out</direction>
|
||||
<relatedStateVariable>A_ARG_TYPE_ConnectionStatus</relatedStateVariable>
|
||||
</argument>
|
||||
</argumentList>
|
||||
</action>
|
||||
<action>
|
||||
<name>GetProtocolInfo</name>
|
||||
<argumentList>
|
||||
<argument>
|
||||
<name>Source</name>
|
||||
<direction>out</direction>
|
||||
<relatedStateVariable>SourceProtocolInfo</relatedStateVariable>
|
||||
</argument>
|
||||
<argument>
|
||||
<name>Sink</name>
|
||||
<direction>out</direction>
|
||||
<relatedStateVariable>SinkProtocolInfo</relatedStateVariable>
|
||||
</argument>
|
||||
</argumentList>
|
||||
</action>
|
||||
</actionList>
|
||||
<serviceStateTable>
|
||||
<stateVariable sendEvents="no">
|
||||
<name>A_ARG_TYPE_ProtocolInfo</name>
|
||||
<dataType>string</dataType>
|
||||
</stateVariable>
|
||||
<stateVariable sendEvents="no">
|
||||
<name>A_ARG_TYPE_ConnectionStatus</name>
|
||||
<dataType>string</dataType>
|
||||
<allowedValueList>
|
||||
<allowedValue>OK</allowedValue>
|
||||
<allowedValue>ContentFormatMismatch</allowedValue>
|
||||
<allowedValue>InsufficientBandwidth</allowedValue>
|
||||
<allowedValue>UnreliableChannel</allowedValue>
|
||||
<allowedValue>Unknown</allowedValue>
|
||||
</allowedValueList>
|
||||
</stateVariable>
|
||||
<stateVariable sendEvents="no">
|
||||
<name>A_ARG_TYPE_AVTransportID</name>
|
||||
<dataType>i4</dataType>
|
||||
</stateVariable>
|
||||
<stateVariable sendEvents="no">
|
||||
<name>A_ARG_TYPE_RcsID</name>
|
||||
<dataType>i4</dataType>
|
||||
</stateVariable>
|
||||
<stateVariable sendEvents="no">
|
||||
<name>A_ARG_TYPE_ConnectionID</name>
|
||||
<dataType>i4</dataType>
|
||||
</stateVariable>
|
||||
<stateVariable sendEvents="no">
|
||||
<name>A_ARG_TYPE_ConnectionManager</name>
|
||||
<dataType>string</dataType>
|
||||
</stateVariable>
|
||||
<stateVariable sendEvents="yes">
|
||||
<name>SourceProtocolInfo</name>
|
||||
<dataType>string</dataType>
|
||||
</stateVariable>
|
||||
<stateVariable sendEvents="yes">
|
||||
<name>SinkProtocolInfo</name>
|
||||
<dataType>string</dataType>
|
||||
</stateVariable>
|
||||
<stateVariable sendEvents="no">
|
||||
<name>A_ARG_TYPE_Direction</name>
|
||||
<dataType>string</dataType>
|
||||
<allowedValueList>
|
||||
<allowedValue>Input</allowedValue>
|
||||
<allowedValue>Output</allowedValue>
|
||||
</allowedValueList>
|
||||
</stateVariable>
|
||||
<stateVariable sendEvents="yes">
|
||||
<name>CurrentConnectionIDs</name>
|
||||
<dataType>string</dataType>
|
||||
</stateVariable>
|
||||
</serviceStateTable>
|
||||
</scpd>
|
|
@ -0,0 +1,207 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<scpd xmlns="urn:schemas-upnp-org:service-1-0">
|
||||
<specVersion>
|
||||
<major>1</major>
|
||||
<minor>0</minor>
|
||||
</specVersion>
|
||||
<actionList>
|
||||
<action>
|
||||
<name>Browse</name>
|
||||
<argumentList>
|
||||
<argument>
|
||||
<name>ObjectID</name>
|
||||
<direction>in</direction>
|
||||
<relatedStateVariable>A_ARG_TYPE_ObjectID</relatedStateVariable>
|
||||
</argument>
|
||||
<argument>
|
||||
<name>BrowseFlag</name>
|
||||
<direction>in</direction>
|
||||
<relatedStateVariable>A_ARG_TYPE_BrowseFlag</relatedStateVariable>
|
||||
</argument>
|
||||
<argument>
|
||||
<name>Filter</name>
|
||||
<direction>in</direction>
|
||||
<relatedStateVariable>A_ARG_TYPE_Filter</relatedStateVariable>
|
||||
</argument>
|
||||
<argument>
|
||||
<name>StartingIndex</name>
|
||||
<direction>in</direction>
|
||||
<relatedStateVariable>A_ARG_TYPE_Index</relatedStateVariable>
|
||||
</argument>
|
||||
<argument>
|
||||
<name>RequestedCount</name>
|
||||
<direction>in</direction>
|
||||
<relatedStateVariable>A_ARG_TYPE_Count</relatedStateVariable>
|
||||
</argument>
|
||||
<argument>
|
||||
<name>SortCriteria</name>
|
||||
<direction>in</direction>
|
||||
<relatedStateVariable>A_ARG_TYPE_SortCriteria</relatedStateVariable>
|
||||
</argument>
|
||||
<argument>
|
||||
<name>Result</name>
|
||||
<direction>out</direction>
|
||||
<relatedStateVariable>A_ARG_TYPE_Result</relatedStateVariable>
|
||||
</argument>
|
||||
<argument>
|
||||
<name>NumberReturned</name>
|
||||
<direction>out</direction>
|
||||
<relatedStateVariable>A_ARG_TYPE_Count</relatedStateVariable>
|
||||
</argument>
|
||||
<argument>
|
||||
<name>TotalMatches</name>
|
||||
<direction>out</direction>
|
||||
<relatedStateVariable>A_ARG_TYPE_Count</relatedStateVariable>
|
||||
</argument>
|
||||
<argument>
|
||||
<name>UpdateID</name>
|
||||
<direction>out</direction>
|
||||
<relatedStateVariable>A_ARG_TYPE_UpdateID</relatedStateVariable>
|
||||
</argument>
|
||||
</argumentList>
|
||||
</action>
|
||||
<action>
|
||||
<name>Search</name>
|
||||
<argumentList>
|
||||
<argument>
|
||||
<name>ContainerID</name>
|
||||
<direction>in</direction>
|
||||
<relatedStateVariable>A_ARG_TYPE_ObjectID</relatedStateVariable>
|
||||
</argument>
|
||||
<argument>
|
||||
<name>SearchCriteria</name>
|
||||
<direction>in</direction>
|
||||
<relatedStateVariable>A_ARG_TYPE_SearchCriteria</relatedStateVariable>
|
||||
</argument>
|
||||
<argument>
|
||||
<name>Filter</name>
|
||||
<direction>in</direction>
|
||||
<relatedStateVariable>A_ARG_TYPE_Filter</relatedStateVariable>
|
||||
</argument>
|
||||
<argument>
|
||||
<name>StartingIndex</name>
|
||||
<direction>in</direction>
|
||||
<relatedStateVariable>A_ARG_TYPE_Index</relatedStateVariable>
|
||||
</argument>
|
||||
<argument>
|
||||
<name>RequestedCount</name>
|
||||
<direction>in</direction>
|
||||
<relatedStateVariable>A_ARG_TYPE_Count</relatedStateVariable>
|
||||
</argument>
|
||||
<argument>
|
||||
<name>SortCriteria</name>
|
||||
<direction>in</direction>
|
||||
<relatedStateVariable>A_ARG_TYPE_SortCriteria</relatedStateVariable>
|
||||
</argument>
|
||||
<argument>
|
||||
<name>Result</name>
|
||||
<direction>out</direction>
|
||||
<relatedStateVariable>A_ARG_TYPE_Result</relatedStateVariable>
|
||||
</argument>
|
||||
<argument>
|
||||
<name>NumberReturned</name>
|
||||
<direction>out</direction>
|
||||
<relatedStateVariable>A_ARG_TYPE_Count</relatedStateVariable>
|
||||
</argument>
|
||||
<argument>
|
||||
<name>TotalMatches</name>
|
||||
<direction>out</direction>
|
||||
<relatedStateVariable>A_ARG_TYPE_Count</relatedStateVariable>
|
||||
</argument>
|
||||
<argument>
|
||||
<name>UpdateID</name>
|
||||
<direction>out</direction>
|
||||
<relatedStateVariable>A_ARG_TYPE_UpdateID</relatedStateVariable>
|
||||
</argument>
|
||||
</argumentList>
|
||||
</action>
|
||||
<action>
|
||||
<name>GetSearchCapabilities</name>
|
||||
<argumentList>
|
||||
<argument>
|
||||
<name>SearchCaps</name>
|
||||
<direction>out</direction>
|
||||
<relatedStateVariable>SearchCapabilities</relatedStateVariable>
|
||||
</argument>
|
||||
</argumentList>
|
||||
</action>
|
||||
<action>
|
||||
<name>GetSortCapabilities</name>
|
||||
<argumentList>
|
||||
<argument>
|
||||
<name>SortCaps</name>
|
||||
<direction>out</direction>
|
||||
<relatedStateVariable>SortCapabilities</relatedStateVariable>
|
||||
</argument>
|
||||
</argumentList>
|
||||
</action>
|
||||
<action>
|
||||
<name>GetSystemUpdateID</name>
|
||||
<argumentList>
|
||||
<argument>
|
||||
<name>Id</name>
|
||||
<direction>out</direction>
|
||||
<relatedStateVariable>SystemUpdateID</relatedStateVariable>
|
||||
</argument>
|
||||
</argumentList>
|
||||
</action>
|
||||
</actionList>
|
||||
<serviceStateTable>
|
||||
<stateVariable sendEvents="no">
|
||||
<name>A_ARG_TYPE_BrowseFlag</name>
|
||||
<dataType>string</dataType>
|
||||
<allowedValueList>
|
||||
<allowedValue>BrowseMetadata</allowedValue>
|
||||
<allowedValue>BrowseDirectChildren</allowedValue>
|
||||
</allowedValueList>
|
||||
</stateVariable>
|
||||
<stateVariable sendEvents="no">
|
||||
<name>A_ARG_TYPE_SearchCriteria</name>
|
||||
<dataType>string</dataType>
|
||||
</stateVariable>
|
||||
<stateVariable sendEvents="no">
|
||||
<name>SystemUpdateID</name>
|
||||
<dataType>ui4</dataType>
|
||||
</stateVariable>
|
||||
<stateVariable sendEvents="no">
|
||||
<name>ContainerUpdateIDs</name>
|
||||
<dataType>string</dataType>
|
||||
</stateVariable>
|
||||
<stateVariable sendEvents="no">
|
||||
<name>A_ARG_TYPE_Count</name>
|
||||
<dataType>ui4</dataType>
|
||||
</stateVariable>
|
||||
<stateVariable sendEvents="no">
|
||||
<name>A_ARG_TYPE_SortCriteria</name>
|
||||
<dataType>string</dataType>
|
||||
</stateVariable>
|
||||
<stateVariable sendEvents="no">
|
||||
<name>SortCapabilities</name>
|
||||
<dataType>string</dataType>
|
||||
</stateVariable>
|
||||
<stateVariable sendEvents="no">
|
||||
<name>A_ARG_TYPE_Index</name>
|
||||
<dataType>ui4</dataType>
|
||||
</stateVariable>
|
||||
<stateVariable sendEvents="no">
|
||||
<name>A_ARG_TYPE_ObjectID</name>
|
||||
<dataType>string</dataType>
|
||||
</stateVariable>
|
||||
<stateVariable sendEvents="no">
|
||||
<name>A_ARG_TYPE_UpdateID</name>
|
||||
<dataType>ui4</dataType>
|
||||
</stateVariable>
|
||||
<stateVariable sendEvents="no">
|
||||
<name>A_ARG_TYPE_Result</name>
|
||||
<dataType>string</dataType>
|
||||
</stateVariable>
|
||||
<stateVariable sendEvents="no">
|
||||
<name>SearchCapabilities</name>
|
||||
<dataType>string</dataType>
|
||||
</stateVariable>
|
||||
<stateVariable sendEvents="no">
|
||||
<name>A_ARG_TYPE_Filter</name>
|
||||
<dataType>string</dataType>
|
||||
</stateVariable>
|
||||
</serviceStateTable>
|
||||
</scpd>
|
|
@ -0,0 +1,88 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<scpd xmlns="urn:schemas-upnp-org:service-1-0">
|
||||
<specVersion>
|
||||
<major>1</major>
|
||||
<minor>0</minor>
|
||||
</specVersion>
|
||||
<actionList>
|
||||
<action>
|
||||
<name>IsAuthorized</name>
|
||||
<argumentList>
|
||||
<argument>
|
||||
<name>DeviceID</name>
|
||||
<direction>in</direction>
|
||||
<relatedStateVariable>A_ARG_TYPE_DeviceID</relatedStateVariable>
|
||||
</argument>
|
||||
<argument>
|
||||
<name>Result</name>
|
||||
<direction>out</direction>
|
||||
<relatedStateVariable>A_ARG_TYPE_Result</relatedStateVariable>
|
||||
</argument>
|
||||
</argumentList>
|
||||
</action>
|
||||
<action>
|
||||
<name>RegisterDevice</name>
|
||||
<argumentList>
|
||||
<argument>
|
||||
<name>RegistrationReqMsg</name>
|
||||
<direction>in</direction>
|
||||
<relatedStateVariable>A_ARG_TYPE_RegistrationReqMsg</relatedStateVariable>
|
||||
</argument>
|
||||
<argument>
|
||||
<name>RegistrationRespMsg</name>
|
||||
<direction>out</direction>
|
||||
<relatedStateVariable>A_ARG_TYPE_RegistrationRespMsg</relatedStateVariable>
|
||||
</argument>
|
||||
</argumentList>
|
||||
</action>
|
||||
<action>
|
||||
<name>IsValidated</name>
|
||||
<argumentList>
|
||||
<argument>
|
||||
<name>DeviceID</name>
|
||||
<direction>in</direction>
|
||||
<relatedStateVariable>A_ARG_TYPE_DeviceID</relatedStateVariable>
|
||||
</argument>
|
||||
<argument>
|
||||
<name>Result</name>
|
||||
<direction>out</direction>
|
||||
<relatedStateVariable>A_ARG_TYPE_Result</relatedStateVariable>
|
||||
</argument>
|
||||
</argumentList>
|
||||
</action>
|
||||
</actionList>
|
||||
<serviceStateTable>
|
||||
<stateVariable sendEvents="no">
|
||||
<name>A_ARG_TYPE_DeviceID</name>
|
||||
<dataType>string</dataType>
|
||||
</stateVariable>
|
||||
<stateVariable sendEvents="no">
|
||||
<name>A_ARG_TYPE_Result</name>
|
||||
<dataType>int</dataType>
|
||||
</stateVariable>
|
||||
<stateVariable sendEvents="no">
|
||||
<name>A_ARG_TYPE_RegistrationReqMsg</name>
|
||||
<dataType>bin.base64</dataType>
|
||||
</stateVariable>
|
||||
<stateVariable sendEvents="no">
|
||||
<name>A_ARG_TYPE_RegistrationRespMsg</name>
|
||||
<dataType>bin.base64</dataType>
|
||||
</stateVariable>
|
||||
<stateVariable sendEvents="yes">
|
||||
<name>AuthorizationGrantedUpdateID</name>
|
||||
<dataType>ui4</dataType>
|
||||
</stateVariable>
|
||||
<stateVariable sendEvents="yes">
|
||||
<name>AuthorizationDeniedUpdateID</name>
|
||||
<dataType>ui4</dataType>
|
||||
</stateVariable>
|
||||
<stateVariable sendEvents="yes">
|
||||
<name>ValidationSucceededUpdateID</name>
|
||||
<dataType>ui4</dataType>
|
||||
</stateVariable>
|
||||
<stateVariable sendEvents="yes">
|
||||
<name>ValidationRevokedUpdateID</name>
|
||||
<dataType>ui4</dataType>
|
||||
</stateVariable>
|
||||
</serviceStateTable>
|
||||
</scpd>
|
Binary file not shown.
After Width: | Height: | Size: 1.5 KiB |
Binary file not shown.
After Width: | Height: | Size: 205 KiB |
|
@ -159,6 +159,8 @@ int intf_Create( libvlc_int_t *libvlc, const char *chain )
|
|||
var_Change( p_intf, "intf-add", VLC_VAR_ADDCHOICE, val, _("Telnet") );
|
||||
val.psz_string = (char *)"http,none";
|
||||
var_Change( p_intf, "intf-add", VLC_VAR_ADDCHOICE, val, _("Web") );
|
||||
val.psz_string = (char *)"upnp,none";
|
||||
var_Change( p_intf, "intf-add", VLC_VAR_ADDCHOICE, val, _("Upnp") );
|
||||
val.psz_string = (char *)"gestures,none";
|
||||
var_Change( p_intf, "intf-add", VLC_VAR_ADDCHOICE, val,
|
||||
_("Mouse Gestures") );
|
||||
|
|
Loading…
Reference in New Issue