Merge branch 'upnp_server' into 'master'

UPnP Server module implementation

See merge request videolan/vlc!269
This commit is contained in:
Alaric Senat 2024-04-28 07:11:08 +00:00
commit e143bc9e49
31 changed files with 3245 additions and 38 deletions

View File

@ -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;
}

View File

@ -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 */

View File

@ -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 */

View File

@ -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

View File

@ -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 */

View File

@ -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

View File

@ -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 */

View File

@ -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 */

View File

@ -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 */

View File

@ -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 */

View File

@ -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

View File

@ -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 */

View File

@ -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, &params, id.value(), &res);
else if (CountQuery == VLC_ML_COUNT_ARTISTS)
status = vlc_ml_list(ml.handle, CountQuery, &params, (int)false, &res);
else if (CountQuery == VLC_ML_COUNT_ENTRY_POINTS)
status = vlc_ml_list(ml.handle, CountQuery, &params, (int)false, &res);
else if (CountQuery == VLC_ML_COUNT_PLAYLISTS)
status = vlc_ml_list(ml.handle, CountQuery, &params, VLC_ML_PLAYLIST_TYPE_ALL, &res);
else
status = vlc_ml_list(ml.handle, CountQuery, &params, &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 */

View File

@ -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

View File

@ -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 */

View File

@ -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

View File

@ -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 */

View File

@ -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 */

View File

@ -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

View File

@ -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]
}

View File

@ -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()
/*

View File

@ -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>

View File

@ -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) {

View File

@ -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)
{

View File

@ -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,

View File

@ -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>

View File

@ -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>

View File

@ -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>

BIN
share/upnp_server/vlc.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 KiB

View File

@ -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") );