summaryrefslogtreecommitdiffstats
path: root/xbmc/network/upnp
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-10 18:07:22 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-10 18:07:22 +0000
commitc04dcc2e7d834218ef2d4194331e383402495ae1 (patch)
tree7333e38d10d75386e60f336b80c2443c1166031d /xbmc/network/upnp
parentInitial commit. (diff)
downloadkodi-c04dcc2e7d834218ef2d4194331e383402495ae1.tar.xz
kodi-c04dcc2e7d834218ef2d4194331e383402495ae1.zip
Adding upstream version 2:20.4+dfsg.upstream/2%20.4+dfsg
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r--xbmc/network/upnp/CMakeLists.txt18
-rw-r--r--xbmc/network/upnp/UPnP.cpp891
-rw-r--r--xbmc/network/upnp/UPnP.h108
-rw-r--r--xbmc/network/upnp/UPnPInternal.cpp1245
-rw-r--r--xbmc/network/upnp/UPnPInternal.h122
-rw-r--r--xbmc/network/upnp/UPnPPlayer.cpp625
-rw-r--r--xbmc/network/upnp/UPnPPlayer.h77
-rw-r--r--xbmc/network/upnp/UPnPRenderer.cpp758
-rw-r--r--xbmc/network/upnp/UPnPRenderer.h73
-rw-r--r--xbmc/network/upnp/UPnPServer.cpp1388
-rw-r--r--xbmc/network/upnp/UPnPServer.h159
-rw-r--r--xbmc/network/upnp/UPnPSettings.cpp110
-rw-r--r--xbmc/network/upnp/UPnPSettings.h56
13 files changed, 5630 insertions, 0 deletions
diff --git a/xbmc/network/upnp/CMakeLists.txt b/xbmc/network/upnp/CMakeLists.txt
new file mode 100644
index 0000000..e558cfc
--- /dev/null
+++ b/xbmc/network/upnp/CMakeLists.txt
@@ -0,0 +1,18 @@
+set(SOURCES UPnP.cpp
+ UPnPInternal.cpp
+ UPnPPlayer.cpp
+ UPnPRenderer.cpp
+ UPnPServer.cpp
+ UPnPSettings.cpp)
+
+set(HEADERS UPnP.h
+ UPnPInternal.h
+ UPnPPlayer.h
+ UPnPRenderer.h
+ UPnPServer.h
+ UPnPSettings.h)
+
+core_add_library(network_upnp)
+if(ENABLE_STATIC_LIBS)
+ target_link_libraries(network_upnp PRIVATE upnp)
+endif()
diff --git a/xbmc/network/upnp/UPnP.cpp b/xbmc/network/upnp/UPnP.cpp
new file mode 100644
index 0000000..0e6b689
--- /dev/null
+++ b/xbmc/network/upnp/UPnP.cpp
@@ -0,0 +1,891 @@
+/*
+ * UPnP Support for XBMC
+ * Copyright (c) 2006 c0diq (Sylvain Rebaud)
+ * Portions Copyright (c) by the authors of libPlatinum
+ * http://www.plutinosoft.com/blog/category/platinum/
+ * Copyright (C) 2006-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "UPnP.h"
+
+#include "FileItem.h"
+#include "GUIUserMessages.h"
+#include "ServiceBroker.h"
+#include "UPnPInternal.h"
+#include "UPnPRenderer.h"
+#include "UPnPServer.h"
+#include "UPnPSettings.h"
+#include "URL.h"
+#include "cores/playercorefactory/PlayerCoreFactory.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "messaging/ApplicationMessenger.h"
+#include "network/Network.h"
+#include "profiles/ProfileManager.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/SystemInfo.h"
+#include "utils/TimeUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/log.h"
+#include "video/VideoInfoTag.h"
+
+#include <memory>
+#include <mutex>
+#include <set>
+
+#include <Platinum/Source/Platinum/Platinum.h>
+
+using namespace UPNP;
+using namespace KODI::MESSAGING;
+
+#define UPNP_DEFAULT_MAX_RETURNED_ITEMS 200
+#define UPNP_DEFAULT_MIN_RETURNED_ITEMS 30
+
+/*
+# Play speed
+# 1 normal
+# 0 invalid
+DLNA_ORG_PS = 'DLNA.ORG_PS'
+DLNA_ORG_PS_VAL = '1'
+
+# Conversion Indicator
+# 1 transcoded
+# 0 not transcoded
+DLNA_ORG_CI = 'DLNA.ORG_CI'
+DLNA_ORG_CI_VAL = '0'
+
+# Operations
+# 00 not time seek range, not range
+# 01 range supported
+# 10 time seek range supported
+# 11 both supported
+DLNA_ORG_OP = 'DLNA.ORG_OP'
+DLNA_ORG_OP_VAL = '01'
+
+# Flags
+# senderPaced 80000000 31
+# lsopTimeBasedSeekSupported 40000000 30
+# lsopByteBasedSeekSupported 20000000 29
+# playcontainerSupported 10000000 28
+# s0IncreasingSupported 08000000 27
+# sNIncreasingSupported 04000000 26
+# rtspPauseSupported 02000000 25
+# streamingTransferModeSupported 01000000 24
+# interactiveTransferModeSupported 00800000 23
+# backgroundTransferModeSupported 00400000 22
+# connectionStallingSupported 00200000 21
+# dlnaVersion15Supported 00100000 20
+DLNA_ORG_FLAGS = 'DLNA.ORG_FLAGS'
+DLNA_ORG_FLAGS_VAL = '01500000000000000000000000000000'
+*/
+
+/*----------------------------------------------------------------------
+| NPT_Console::Output
++---------------------------------------------------------------------*/
+void
+NPT_Console::Output(const char* msg) { }
+
+spdlog::level::level_enum ConvertLogLevel(int nptLogLevel)
+{
+ if (nptLogLevel >= NPT_LOG_LEVEL_FATAL)
+ return spdlog::level::critical;
+ if (nptLogLevel >= NPT_LOG_LEVEL_SEVERE)
+ return spdlog::level::err;
+ if (nptLogLevel >= NPT_LOG_LEVEL_WARNING)
+ return spdlog::level::warn;
+ if (nptLogLevel >= NPT_LOG_LEVEL_FINE)
+ return spdlog::level::info;
+ if (nptLogLevel >= NPT_LOG_LEVEL_FINER)
+ return spdlog::level::debug;
+
+ return spdlog::level::trace;
+}
+
+void
+UPnPLogger(const NPT_LogRecord* record)
+{
+ static Logger logger = CServiceBroker::GetLogging().GetLogger("Platinum");
+ if (CServiceBroker::GetLogging().CanLogComponent(LOGUPNP))
+ logger->log(ConvertLogLevel(record->m_Level), "[{}]: {}", record->m_LoggerName,
+ record->m_Message);
+}
+
+namespace UPNP
+{
+
+/*----------------------------------------------------------------------
+| static
++---------------------------------------------------------------------*/
+CUPnP* CUPnP::upnp = NULL;
+static NPT_List<void*> g_UserData;
+static NPT_Mutex g_UserDataLock;
+
+/*----------------------------------------------------------------------
+| CDeviceHostReferenceHolder class
++---------------------------------------------------------------------*/
+class CDeviceHostReferenceHolder
+{
+public:
+ PLT_DeviceHostReference m_Device;
+};
+
+/*----------------------------------------------------------------------
+| CCtrlPointReferenceHolder class
++---------------------------------------------------------------------*/
+class CCtrlPointReferenceHolder
+{
+public:
+ PLT_CtrlPointReference m_CtrlPoint;
+};
+
+/*----------------------------------------------------------------------
+| CUPnPCleaner class
++---------------------------------------------------------------------*/
+class CUPnPCleaner : public NPT_Thread
+{
+public:
+ explicit CUPnPCleaner(CUPnP* upnp) : NPT_Thread(true), m_UPnP(upnp) {}
+ void Run() override {
+ delete m_UPnP;
+ }
+
+ CUPnP* m_UPnP;
+};
+
+/*----------------------------------------------------------------------
+| CMediaBrowser class
++---------------------------------------------------------------------*/
+class CMediaBrowser : public PLT_SyncMediaBrowser, public PLT_MediaContainerChangesListener
+{
+public:
+ explicit CMediaBrowser(PLT_CtrlPointReference& ctrlPoint)
+ : PLT_SyncMediaBrowser(ctrlPoint, true),
+ m_logger(CServiceBroker::GetLogging().GetLogger("UPNP::CMediaBrowser"))
+ {
+ SetContainerListener(this);
+ }
+
+ // PLT_MediaBrowser methods
+ bool OnMSAdded(PLT_DeviceDataReference& device) override
+ {
+ CGUIMessage message(GUI_MSG_NOTIFY_ALL, 0, 0, GUI_MSG_UPDATE_PATH);
+ message.SetStringParam("upnp://");
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(message);
+
+ return PLT_SyncMediaBrowser::OnMSAdded(device);
+ }
+ void OnMSRemoved(PLT_DeviceDataReference& device) override
+ {
+ PLT_SyncMediaBrowser::OnMSRemoved(device);
+
+ CGUIMessage message(GUI_MSG_NOTIFY_ALL, 0, 0, GUI_MSG_UPDATE_PATH);
+ message.SetStringParam("upnp://");
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(message);
+
+ PLT_SyncMediaBrowser::OnMSRemoved(device);
+ }
+
+ // PLT_MediaContainerChangesListener methods
+ void OnContainerChanged(PLT_DeviceDataReference& device,
+ const char* item_id,
+ const char* update_id) override
+ {
+ NPT_String path = "upnp://"+device->GetUUID()+"/";
+ if (!NPT_StringsEqual(item_id, "0")) {
+ std::string id(CURL::Encode(item_id));
+ URIUtils::AddSlashAtEnd(id);
+ path += id.c_str();
+ }
+
+ m_logger->debug("notified container update {}", (const char*)path);
+ CGUIMessage message(GUI_MSG_NOTIFY_ALL, 0, 0, GUI_MSG_UPDATE_PATH);
+ message.SetStringParam(path.GetChars());
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(message);
+ }
+
+ bool MarkWatched(const CFileItem& item, const bool watched)
+ {
+ if (watched) {
+ CFileItem temp(item);
+ temp.SetProperty("original_listitem_url", item.GetPath());
+ return SaveFileState(temp, CBookmark(), watched);
+ }
+ else {
+ m_logger->debug("Marking video item {} as watched", item.GetPath());
+
+ std::set<std::pair<NPT_String, NPT_String> > values;
+ values.insert(std::make_pair("<upnp:playCount>1</upnp:playCount>",
+ "<upnp:playCount>0</upnp:playCount>"));
+ return InvokeUpdateObject(item.GetPath().c_str(), values);
+ }
+ }
+
+ bool SaveFileState(const CFileItem& item, const CBookmark& bookmark, const bool updatePlayCount)
+ {
+ std::string path = item.GetProperty("original_listitem_url").asString();
+ if (!item.HasVideoInfoTag() || path.empty()) {
+ return false;
+ }
+
+ std::set<std::pair<NPT_String, NPT_String> > values;
+ if (item.GetVideoInfoTag()->GetResumePoint().timeInSeconds != bookmark.timeInSeconds) {
+ m_logger->debug("Updating resume point for item {}", path);
+ long time = (long)bookmark.timeInSeconds;
+ if (time < 0)
+ time = 0;
+
+ values.insert(std::make_pair(
+ NPT_String::Format("<upnp:lastPlaybackPosition>%ld</upnp:lastPlaybackPosition>",
+ (long)item.GetVideoInfoTag()->GetResumePoint().timeInSeconds),
+ NPT_String::Format("<upnp:lastPlaybackPosition>%ld</upnp:lastPlaybackPosition>",
+ time)));
+
+ NPT_String curr_value = "<xbmc:lastPlayerState>";
+ PLT_Didl::AppendXmlEscape(curr_value, item.GetVideoInfoTag()->GetResumePoint().playerState.c_str());
+ curr_value += "</xbmc:lastPlayerState>";
+ NPT_String new_value = "<xbmc:lastPlayerState>";
+ PLT_Didl::AppendXmlEscape(new_value, bookmark.playerState.c_str());
+ new_value += "</xbmc:lastPlayerState>";
+ values.insert(std::make_pair(curr_value, new_value));
+ }
+ if (updatePlayCount) {
+ m_logger->debug("Marking video item {} as watched", path);
+ values.insert(std::make_pair("<upnp:playCount>0</upnp:playCount>",
+ "<upnp:playCount>1</upnp:playCount>"));
+ }
+
+ return InvokeUpdateObject(path.c_str(), values);
+ }
+
+ bool UpdateItem(const std::string& path, const CFileItem& item)
+ {
+ if (path.empty())
+ return false;
+
+ std::set<std::pair<NPT_String, NPT_String> > values;
+ if (item.HasVideoInfoTag())
+ {
+ // handle playcount
+ const CVideoInfoTag *details = item.GetVideoInfoTag();
+ int playcountOld = 0, playcountNew = 0;
+ if (details->GetPlayCount() <= 0)
+ playcountOld = 1;
+ else
+ playcountNew = details->GetPlayCount();
+
+ values.insert(std::make_pair(
+ NPT_String::Format("<upnp:playCount>%d</upnp:playCount>", playcountOld),
+ NPT_String::Format("<upnp:playCount>%d</upnp:playCount>", playcountNew)));
+
+ // handle lastplayed
+ CDateTime lastPlayedOld, lastPlayedNew;
+ if (!details->m_lastPlayed.IsValid())
+ lastPlayedOld = CDateTime::GetCurrentDateTime();
+ else
+ lastPlayedNew = details->m_lastPlayed;
+
+ values.insert(std::make_pair(
+ NPT_String::Format("<upnp:lastPlaybackTime>%s</upnp:lastPlaybackTime>",
+ lastPlayedOld.GetAsW3CDateTime().c_str()),
+ NPT_String::Format("<upnp:lastPlaybackTime>%s</upnp:lastPlaybackTime>",
+ lastPlayedNew.GetAsW3CDateTime().c_str())));
+
+ // handle resume point
+ long resumePointOld = 0L, resumePointNew = 0L;
+ if (details->GetResumePoint().timeInSeconds <= 0)
+ resumePointOld = 1;
+ else
+ resumePointNew = static_cast<long>(details->GetResumePoint().timeInSeconds);
+
+ values.insert(std::make_pair(
+ NPT_String::Format("<upnp:lastPlaybackPosition>%ld</upnp:lastPlaybackPosition>",
+ resumePointOld),
+ NPT_String::Format("<upnp:lastPlaybackPosition>%ld</upnp:lastPlaybackPosition>",
+ resumePointNew)));
+ }
+
+ return InvokeUpdateObject(path.c_str(), values);
+ }
+
+ bool InvokeUpdateObject(const char *id, const std::set<std::pair<NPT_String, NPT_String> >& values)
+ {
+ CURL url(id);
+ PLT_DeviceDataReference device;
+ PLT_Service* cds;
+ PLT_ActionReference action;
+ NPT_String curr_value, new_value;
+
+ m_logger->debug("attempting to invoke UpdateObject for {}", id);
+
+ // check this server supports UpdateObject action
+ NPT_CHECK_LABEL(FindServer(url.GetHostName().c_str(), device),failed);
+ NPT_CHECK_LABEL(device->FindServiceById("urn:upnp-org:serviceId:ContentDirectory", cds),failed);
+
+ NPT_CHECK_LABEL(m_CtrlPoint->CreateAction(
+ device,
+ "urn:schemas-upnp-org:service:ContentDirectory:1",
+ "UpdateObject",
+ action), failed);
+
+ NPT_CHECK_LABEL(action->SetArgumentValue("ObjectID", url.GetFileName().c_str()), failed);
+
+ // put together the current and the new value string
+ for (std::set<std::pair<NPT_String, NPT_String> >::const_iterator value = values.begin(); value != values.end(); ++value)
+ {
+ if (!curr_value.IsEmpty())
+ curr_value.Append(",");
+ if (!new_value.IsEmpty())
+ new_value.Append(",");
+
+ curr_value.Append(value->first);
+ new_value.Append(value->second);
+ }
+ NPT_CHECK_LABEL(action->SetArgumentValue("CurrentTagValue", curr_value), failed);
+ NPT_CHECK_LABEL(action->SetArgumentValue("NewTagValue", new_value), failed);
+
+ NPT_CHECK_LABEL(m_CtrlPoint->InvokeAction(action, NULL),failed);
+
+ m_logger->debug("invoked UpdateObject successfully");
+ return true;
+
+ failed:
+ m_logger->info("invoking UpdateObject failed");
+ return false;
+ }
+
+ private:
+ Logger m_logger;
+};
+
+
+/*----------------------------------------------------------------------
+| CMediaController class
++---------------------------------------------------------------------*/
+class CMediaController
+ : public PLT_MediaControllerDelegate
+ , public PLT_MediaController
+{
+public:
+ explicit CMediaController(PLT_CtrlPointReference& ctrl_point)
+ : PLT_MediaController(ctrl_point)
+ {
+ PLT_MediaController::SetDelegate(this);
+ }
+
+ ~CMediaController() override
+ {
+ for (const auto& itRenderer : m_registeredRenderers)
+ unregisterRenderer(itRenderer);
+ m_registeredRenderers.clear();
+ }
+
+#define CHECK_USERDATA_RETURN(userdata) do { \
+ if (!g_UserData.Contains(userdata)) \
+ return; \
+ } while(0)
+
+ void OnStopResult(NPT_Result res, PLT_DeviceDataReference& device, void* userdata) override
+ { CHECK_USERDATA_RETURN(userdata);
+ static_cast<PLT_MediaControllerDelegate*>(userdata)->OnStopResult(res, device, userdata);
+ }
+
+ void OnSetPlayModeResult(NPT_Result res, PLT_DeviceDataReference& device, void* userdata) override
+ { CHECK_USERDATA_RETURN(userdata);
+ static_cast<PLT_MediaControllerDelegate*>(userdata)->OnSetPlayModeResult(res, device, userdata);
+ }
+
+ void OnSetAVTransportURIResult(NPT_Result res, PLT_DeviceDataReference& device, void* userdata) override
+ { CHECK_USERDATA_RETURN(userdata);
+ static_cast<PLT_MediaControllerDelegate*>(userdata)->OnSetAVTransportURIResult(res, device, userdata);
+ }
+
+ void OnSeekResult(NPT_Result res, PLT_DeviceDataReference& device, void* userdata) override
+ { CHECK_USERDATA_RETURN(userdata);
+ static_cast<PLT_MediaControllerDelegate*>(userdata)->OnSeekResult(res, device, userdata);
+ }
+
+ void OnPreviousResult(NPT_Result res, PLT_DeviceDataReference& device, void* userdata) override
+ { CHECK_USERDATA_RETURN(userdata);
+ static_cast<PLT_MediaControllerDelegate*>(userdata)->OnPreviousResult(res, device, userdata);
+ }
+
+ void OnPlayResult(NPT_Result res, PLT_DeviceDataReference& device, void* userdata) override
+ { CHECK_USERDATA_RETURN(userdata);
+ static_cast<PLT_MediaControllerDelegate*>(userdata)->OnPlayResult(res, device, userdata);
+ }
+
+ void OnPauseResult(NPT_Result res, PLT_DeviceDataReference& device, void* userdata) override
+ { CHECK_USERDATA_RETURN(userdata);
+ static_cast<PLT_MediaControllerDelegate*>(userdata)->OnPauseResult(res, device, userdata);
+ }
+
+ void OnNextResult(NPT_Result res, PLT_DeviceDataReference& device, void* userdata) override
+ { CHECK_USERDATA_RETURN(userdata);
+ static_cast<PLT_MediaControllerDelegate*>(userdata)->OnNextResult(res, device, userdata);
+ }
+
+ void OnGetMediaInfoResult(NPT_Result res, PLT_DeviceDataReference& device, PLT_MediaInfo* info, void* userdata) override
+ { CHECK_USERDATA_RETURN(userdata);
+ static_cast<PLT_MediaControllerDelegate*>(userdata)->OnGetMediaInfoResult(res, device, info, userdata);
+ }
+
+ void OnGetPositionInfoResult(NPT_Result res, PLT_DeviceDataReference& device, PLT_PositionInfo* info, void* userdata) override
+ { CHECK_USERDATA_RETURN(userdata);
+ static_cast<PLT_MediaControllerDelegate*>(userdata)->OnGetPositionInfoResult(res, device, info, userdata);
+ }
+
+ void OnGetTransportInfoResult(NPT_Result res, PLT_DeviceDataReference& device, PLT_TransportInfo* info, void* userdata) override
+ { CHECK_USERDATA_RETURN(userdata);
+ static_cast<PLT_MediaControllerDelegate*>(userdata)->OnGetTransportInfoResult(res, device, info, userdata);
+ }
+
+ bool OnMRAdded(PLT_DeviceDataReference& device ) override
+ {
+ if (device->GetUUID().IsEmpty() || device->GetUUID().GetChars() == NULL)
+ return false;
+
+ CPlayerCoreFactory &playerCoreFactory = CServiceBroker::GetPlayerCoreFactory();
+
+ playerCoreFactory.OnPlayerDiscovered((const char*)device->GetUUID()
+ ,(const char*)device->GetFriendlyName());
+
+ m_registeredRenderers.insert(std::string(device->GetUUID().GetChars()));
+ return true;
+ }
+
+ void OnMRRemoved(PLT_DeviceDataReference& device ) override
+ {
+ if (device->GetUUID().IsEmpty() || device->GetUUID().GetChars() == NULL)
+ return;
+
+ std::string uuid(device->GetUUID().GetChars());
+ unregisterRenderer(uuid);
+ m_registeredRenderers.erase(uuid);
+ }
+
+private:
+ void unregisterRenderer(const std::string &deviceUUID)
+ {
+ CPlayerCoreFactory &playerCoreFactory = CServiceBroker::GetPlayerCoreFactory();
+
+ playerCoreFactory.OnPlayerRemoved(deviceUUID);
+ }
+
+ std::set<std::string> m_registeredRenderers;
+};
+
+/*----------------------------------------------------------------------
+| CUPnP::CUPnP
++---------------------------------------------------------------------*/
+CUPnP::CUPnP() :
+ m_MediaBrowser(NULL),
+ m_MediaController(NULL),
+ m_LogHandler(NULL),
+ m_ServerHolder(new CDeviceHostReferenceHolder()),
+ m_RendererHolder(new CRendererReferenceHolder()),
+ m_CtrlPointHolder(new CCtrlPointReferenceHolder())
+{
+ NPT_LogManager::GetDefault().Configure("plist:.level=FINE;.handlers=CustomHandler;");
+ NPT_LogHandler::Create("xbmc", "CustomHandler", m_LogHandler);
+ m_LogHandler->SetCustomHandlerFunction(&UPnPLogger);
+
+ // initialize upnp context
+ m_UPnP = new PLT_UPnP();
+
+ // keep main IP around
+ if (CServiceBroker::GetNetwork().GetFirstConnectedInterface()) {
+ m_IP = CServiceBroker::GetNetwork().GetFirstConnectedInterface()->GetCurrentIPAddress().c_str();
+ }
+ NPT_List<NPT_IpAddress> list;
+ if (NPT_SUCCEEDED(PLT_UPnPMessageHelper::GetIPAddresses(list)) && list.GetItemCount()) {
+ m_IP = (*(list.GetFirstItem())).ToString();
+ }
+ else if(m_IP.empty())
+ m_IP = "localhost";
+
+ // start upnp monitoring
+ m_UPnP->Start();
+}
+
+/*----------------------------------------------------------------------
+| CUPnP::~CUPnP
++---------------------------------------------------------------------*/
+CUPnP::~CUPnP()
+{
+ m_UPnP->Stop();
+ StopClient();
+ StopController();
+ StopServer();
+
+ delete m_UPnP;
+ delete m_LogHandler;
+ delete m_ServerHolder;
+ delete m_RendererHolder;
+ delete m_CtrlPointHolder;
+}
+
+/*----------------------------------------------------------------------
+| CUPnP::GetInstance
++---------------------------------------------------------------------*/
+CUPnP*
+CUPnP::GetInstance()
+{
+ if (!upnp) {
+ upnp = new CUPnP();
+ }
+
+ return upnp;
+}
+
+/*----------------------------------------------------------------------
+| CUPnP::ReleaseInstance
++---------------------------------------------------------------------*/
+void
+CUPnP::ReleaseInstance(bool bWait)
+{
+ if (upnp) {
+ CUPnP* _upnp = upnp;
+ upnp = NULL;
+
+ if (bWait) {
+ delete _upnp;
+ } else {
+ // since it takes a while to clean up
+ // starts a detached thread to do this
+ CUPnPCleaner* cleaner = new CUPnPCleaner(_upnp);
+ cleaner->Start();
+ }
+ }
+}
+
+/*----------------------------------------------------------------------
+| CUPnP::GetServer
++---------------------------------------------------------------------*/
+CUPnPServer* CUPnP::GetServer()
+{
+ if(upnp)
+ return static_cast<CUPnPServer*>(upnp->m_ServerHolder->m_Device.AsPointer());
+ return NULL;
+}
+
+/*----------------------------------------------------------------------
+| CUPnP::MarkWatched
++---------------------------------------------------------------------*/
+bool
+CUPnP::MarkWatched(const CFileItem& item, const bool watched)
+{
+ if (upnp && upnp->m_MediaBrowser) {
+ // dynamic_cast is safe here, avoids polluting CUPnP.h header file
+ CMediaBrowser* browser = dynamic_cast<CMediaBrowser*>(upnp->m_MediaBrowser);
+ if (browser)
+ return browser->MarkWatched(item, watched);
+ }
+ return false;
+}
+
+/*----------------------------------------------------------------------
+| CUPnP::SaveFileState
++---------------------------------------------------------------------*/
+bool
+CUPnP::SaveFileState(const CFileItem& item, const CBookmark& bookmark, const bool updatePlayCount)
+{
+ if (upnp && upnp->m_MediaBrowser) {
+ // dynamic_cast is safe here, avoids polluting CUPnP.h header file
+ CMediaBrowser* browser = dynamic_cast<CMediaBrowser*>(upnp->m_MediaBrowser);
+ if (browser)
+ return browser->SaveFileState(item, bookmark, updatePlayCount);
+ }
+ return false;
+}
+
+/*----------------------------------------------------------------------
+| CUPnP::CreateControlPoint
++---------------------------------------------------------------------*/
+void
+CUPnP::CreateControlPoint()
+{
+ if (!m_CtrlPointHolder->m_CtrlPoint.IsNull())
+ return;
+
+ // create controlpoint
+ m_CtrlPointHolder->m_CtrlPoint = new PLT_CtrlPoint();
+
+ // start it
+ m_UPnP->AddCtrlPoint(m_CtrlPointHolder->m_CtrlPoint);
+}
+
+/*----------------------------------------------------------------------
+| CUPnP::DestroyControlPoint
++---------------------------------------------------------------------*/
+void
+CUPnP::DestroyControlPoint()
+{
+ if (m_CtrlPointHolder->m_CtrlPoint.IsNull())
+ return;
+
+ m_UPnP->RemoveCtrlPoint(m_CtrlPointHolder->m_CtrlPoint);
+ m_CtrlPointHolder->m_CtrlPoint = NULL;
+}
+
+/*----------------------------------------------------------------------
+| CUPnP::UpdateItem
++---------------------------------------------------------------------*/
+bool
+CUPnP::UpdateItem(const std::string& path, const CFileItem& item)
+{
+ if (upnp && upnp->m_MediaBrowser) {
+ // dynamic_cast is safe here, avoids polluting CUPnP.h header file
+ CMediaBrowser* browser = dynamic_cast<CMediaBrowser*>(upnp->m_MediaBrowser);
+ if (browser)
+ return browser->UpdateItem(path, item);
+ }
+ return false;
+}
+
+/*----------------------------------------------------------------------
+| CUPnP::StartClient
++---------------------------------------------------------------------*/
+void
+CUPnP::StartClient()
+{
+ std::unique_lock<CCriticalSection> lock(m_lockMediaBrowser);
+ if (m_MediaBrowser != NULL)
+ return;
+
+ CreateControlPoint();
+
+ // start browser
+ m_MediaBrowser = new CMediaBrowser(m_CtrlPointHolder->m_CtrlPoint);
+}
+
+/*----------------------------------------------------------------------
+| CUPnP::StopClient
++---------------------------------------------------------------------*/
+void
+CUPnP::StopClient()
+{
+ std::unique_lock<CCriticalSection> lock(m_lockMediaBrowser);
+ if (m_MediaBrowser == NULL)
+ return;
+
+ delete m_MediaBrowser;
+ m_MediaBrowser = NULL;
+
+ if (!IsControllerStarted())
+ DestroyControlPoint();
+}
+
+/*----------------------------------------------------------------------
+| CUPnP::StartController
++---------------------------------------------------------------------*/
+void
+CUPnP::StartController()
+{
+ if (m_MediaController != NULL)
+ return;
+
+ CreateControlPoint();
+
+ m_MediaController = new CMediaController(m_CtrlPointHolder->m_CtrlPoint);
+}
+
+/*----------------------------------------------------------------------
+| CUPnP::StopController
++---------------------------------------------------------------------*/
+void
+CUPnP::StopController()
+{
+ if (m_MediaController == NULL)
+ return;
+
+ delete m_MediaController;
+ m_MediaController = NULL;
+
+ if (!IsClientStarted())
+ DestroyControlPoint();
+}
+
+/*----------------------------------------------------------------------
+| CUPnP::CreateServer
++---------------------------------------------------------------------*/
+CUPnPServer*
+CUPnP::CreateServer(int port /* = 0 */)
+{
+ CUPnPServer* device =
+ new CUPnPServer(CSysInfo::GetDeviceName().c_str(),
+ CUPnPSettings::GetInstance().GetServerUUID().length() ? CUPnPSettings::GetInstance().GetServerUUID().c_str() : NULL,
+ port);
+
+ // trying to set optional upnp values for XP UPnP UI Icons to detect us
+ // but it doesn't work anyways as it requires multicast for XP to detect us
+ device->m_PresentationURL =
+ NPT_HttpUrl(m_IP.c_str(),
+ CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(CSettings::SETTING_SERVICES_WEBSERVERPORT),
+ "/").ToString();
+
+ device->m_ModelName = "Kodi";
+ device->m_ModelNumber = CSysInfo::GetVersion().c_str();
+ device->m_ModelDescription = "Kodi - Media Server";
+ device->m_ModelURL = "http://kodi.tv/";
+ device->m_Manufacturer = "XBMC Foundation";
+ device->m_ManufacturerURL = "http://kodi.tv/";
+
+ device->SetDelegate(device);
+ return device;
+}
+
+/*----------------------------------------------------------------------
+| CUPnP::StartServer
++---------------------------------------------------------------------*/
+bool
+CUPnP::StartServer()
+{
+ if (!m_ServerHolder->m_Device.IsNull()) return false;
+
+ const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager();
+
+ // load upnpserver.xml
+ std::string filename = URIUtils::AddFileToFolder(profileManager->GetUserDataFolder(), "upnpserver.xml");
+ CUPnPSettings::GetInstance().Load(filename);
+
+ // create the server with a XBox compatible friendlyname and UUID from upnpserver.xml if found
+ m_ServerHolder->m_Device = CreateServer(CUPnPSettings::GetInstance().GetServerPort());
+
+ // start server
+ NPT_Result res = m_UPnP->AddDevice(m_ServerHolder->m_Device);
+ if (NPT_FAILED(res)) {
+ // if the upnp device port was not 0, it could have failed because
+ // of port being in used, so restart with a random port
+ if (CUPnPSettings::GetInstance().GetServerPort() > 0) m_ServerHolder->m_Device = CreateServer(0);
+
+ res = m_UPnP->AddDevice(m_ServerHolder->m_Device);
+ }
+
+ // save port but don't overwrite saved settings if port was random
+ if (NPT_SUCCEEDED(res)) {
+ if (CUPnPSettings::GetInstance().GetServerPort() == 0) {
+ CUPnPSettings::GetInstance().SetServerPort(m_ServerHolder->m_Device->GetPort());
+ }
+ CUPnPServer::m_MaxReturnedItems = UPNP_DEFAULT_MAX_RETURNED_ITEMS;
+ if (CUPnPSettings::GetInstance().GetMaximumReturnedItems() > 0) {
+ // must be > UPNP_DEFAULT_MIN_RETURNED_ITEMS
+ CUPnPServer::m_MaxReturnedItems = std::max(UPNP_DEFAULT_MIN_RETURNED_ITEMS, CUPnPSettings::GetInstance().GetMaximumReturnedItems());
+ }
+ CUPnPSettings::GetInstance().SetMaximumReturnedItems(CUPnPServer::m_MaxReturnedItems);
+ }
+
+ // save UUID
+ CUPnPSettings::GetInstance().SetServerUUID(m_ServerHolder->m_Device->GetUUID().GetChars());
+ return CUPnPSettings::GetInstance().Save(filename);
+}
+
+/*----------------------------------------------------------------------
+| CUPnP::StopServer
++---------------------------------------------------------------------*/
+void
+CUPnP::StopServer()
+{
+ if (m_ServerHolder->m_Device.IsNull()) return;
+
+ m_UPnP->RemoveDevice(m_ServerHolder->m_Device);
+ m_ServerHolder->m_Device = NULL;
+}
+
+/*----------------------------------------------------------------------
+| CUPnP::CreateRenderer
++---------------------------------------------------------------------*/
+CUPnPRenderer*
+CUPnP::CreateRenderer(int port /* = 0 */)
+{
+ CUPnPRenderer* device =
+ new CUPnPRenderer(CSysInfo::GetDeviceName().c_str(),
+ false,
+ (CUPnPSettings::GetInstance().GetRendererUUID().length() ? CUPnPSettings::GetInstance().GetRendererUUID().c_str() : NULL),
+ port);
+
+ device->m_PresentationURL =
+ NPT_HttpUrl(m_IP.c_str(),
+ CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(CSettings::SETTING_SERVICES_WEBSERVERPORT),
+ "/").ToString();
+ device->m_ModelName = "Kodi";
+ device->m_ModelNumber = CSysInfo::GetVersion().c_str();
+ device->m_ModelDescription = "Kodi - Media Renderer";
+ device->m_ModelURL = "http://kodi.tv/";
+ device->m_Manufacturer = "XBMC Foundation";
+ device->m_ManufacturerURL = "http://kodi.tv/";
+
+ return device;
+}
+
+/*----------------------------------------------------------------------
+| CUPnP::StartRenderer
++---------------------------------------------------------------------*/
+bool CUPnP::StartRenderer()
+{
+ if (!m_RendererHolder->m_Device.IsNull())
+ return false;
+
+ const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager();
+
+ std::string filename = URIUtils::AddFileToFolder(profileManager->GetUserDataFolder(), "upnpserver.xml");
+ CUPnPSettings::GetInstance().Load(filename);
+
+ m_RendererHolder->m_Device = CreateRenderer(CUPnPSettings::GetInstance().GetRendererPort());
+
+ NPT_Result res = m_UPnP->AddDevice(m_RendererHolder->m_Device);
+
+ // failed most likely because port is in use, try again with random port now
+ if (NPT_FAILED(res) && CUPnPSettings::GetInstance().GetRendererPort() != 0) {
+ m_RendererHolder->m_Device = CreateRenderer(0);
+
+ res = m_UPnP->AddDevice(m_RendererHolder->m_Device);
+ }
+
+ // save port but don't overwrite saved settings if random
+ if (NPT_SUCCEEDED(res) && CUPnPSettings::GetInstance().GetRendererPort() == 0) {
+ CUPnPSettings::GetInstance().SetRendererPort(m_RendererHolder->m_Device->GetPort());
+ }
+
+ // save UUID
+ CUPnPSettings::GetInstance().SetRendererUUID(m_RendererHolder->m_Device->GetUUID().GetChars());
+ return CUPnPSettings::GetInstance().Save(filename);
+}
+
+/*----------------------------------------------------------------------
+| CUPnP::StopRenderer
++---------------------------------------------------------------------*/
+void CUPnP::StopRenderer()
+{
+ if (m_RendererHolder->m_Device.IsNull()) return;
+
+ m_UPnP->RemoveDevice(m_RendererHolder->m_Device);
+ m_RendererHolder->m_Device = NULL;
+}
+
+/*----------------------------------------------------------------------
+| CUPnP::UpdateState
++---------------------------------------------------------------------*/
+void CUPnP::UpdateState()
+{
+ if (!m_RendererHolder->m_Device.IsNull())
+ static_cast<CUPnPRenderer*>(m_RendererHolder->m_Device.AsPointer())->UpdateState();
+}
+
+void CUPnP::RegisterUserdata(void* ptr)
+{
+ NPT_AutoLock lock(g_UserDataLock);
+ g_UserData.Add(ptr);
+}
+
+void CUPnP::UnregisterUserdata(void* ptr)
+{
+ NPT_AutoLock lock(g_UserDataLock);
+ g_UserData.Remove(ptr);
+}
+
+} /* namespace UPNP */
diff --git a/xbmc/network/upnp/UPnP.h b/xbmc/network/upnp/UPnP.h
new file mode 100644
index 0000000..311aa6c
--- /dev/null
+++ b/xbmc/network/upnp/UPnP.h
@@ -0,0 +1,108 @@
+/*
+ * UPnP Support for XBMC
+ * Copyright (c) 2006 c0diq (Sylvain Rebaud)
+ * Portions Copyright (c) by the authors of libPlatinum
+ * http://www.plutinosoft.com/blog/category/platinum/
+ * Copyright (C) 2006-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "threads/CriticalSection.h"
+
+#include <string>
+
+class NPT_LogHandler;
+class PLT_UPnP;
+class PLT_SyncMediaBrowser;
+class PLT_MediaController;
+class PLT_MediaObject;
+class PLT_MediaItemResource;
+class CFileItem;
+class CBookmark;
+
+namespace UPNP
+{
+
+class CDeviceHostReferenceHolder;
+class CCtrlPointReferenceHolder;
+class CRendererReferenceHolder;
+class CUPnPRenderer;
+class CUPnPServer;
+
+class CUPnP
+{
+public:
+ CUPnP();
+ ~CUPnP();
+
+ // server
+ bool StartServer();
+ void StopServer();
+
+ // client
+ void StartClient();
+ void StopClient();
+ bool IsClientStarted() { return (m_MediaBrowser != NULL); }
+
+ // controller
+ void StartController();
+ void StopController();
+ bool IsControllerStarted() { return (m_MediaController != NULL); }
+
+ // renderer
+ bool StartRenderer();
+ void StopRenderer();
+ void UpdateState();
+
+ // class methods
+ static CUPnP* GetInstance();
+ static CUPnPServer* GetServer();
+ static void ReleaseInstance(bool bWait);
+ static bool IsInstantiated() { return upnp != NULL; }
+
+ static bool MarkWatched(const CFileItem& item,
+ const bool watched);
+
+ static bool SaveFileState(const CFileItem& item,
+ const CBookmark& bookmark,
+ const bool updatePlayCount);
+ static bool UpdateItem(const std::string& path,
+ const CFileItem& item);
+
+ static void RegisterUserdata(void* ptr);
+ static void UnregisterUserdata(void* ptr);
+private:
+ CUPnP(const CUPnP&) = delete;
+ CUPnP& operator=(const CUPnP&) = delete;
+
+ void CreateControlPoint();
+ void DestroyControlPoint();
+
+ // methods
+ CUPnPRenderer* CreateRenderer(int port = 0);
+ CUPnPServer* CreateServer(int port = 0);
+
+ CCriticalSection m_lockMediaBrowser;
+
+ public:
+ PLT_SyncMediaBrowser* m_MediaBrowser;
+ PLT_MediaController* m_MediaController;
+
+private:
+ std::string m_IP;
+ PLT_UPnP* m_UPnP;
+ NPT_LogHandler* m_LogHandler;
+ CDeviceHostReferenceHolder* m_ServerHolder;
+ CRendererReferenceHolder* m_RendererHolder;
+ CCtrlPointReferenceHolder* m_CtrlPointHolder;
+
+
+ static CUPnP* upnp;
+};
+
+} /* namespace UPNP */
diff --git a/xbmc/network/upnp/UPnPInternal.cpp b/xbmc/network/upnp/UPnPInternal.cpp
new file mode 100644
index 0000000..4066157
--- /dev/null
+++ b/xbmc/network/upnp/UPnPInternal.cpp
@@ -0,0 +1,1245 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+#include "UPnPInternal.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "TextureDatabase.h"
+#include "ThumbLoader.h"
+#include "UPnPServer.h"
+#include "URL.h"
+#include "Util.h"
+#include "filesystem/MusicDatabaseDirectory.h"
+#include "filesystem/StackDirectory.h"
+#include "filesystem/VideoDatabaseDirectory.h"
+#include "music/tags/MusicInfoTag.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "settings/lib/Setting.h"
+#include "utils/ContentUtils.h"
+#include "utils/LangCodeExpander.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/log.h"
+#include "video/VideoInfoTag.h"
+
+#include <algorithm>
+#include <array>
+#include <string_view>
+
+#include <Platinum/Source/Platinum/Platinum.h>
+
+using namespace MUSIC_INFO;
+using namespace XFILE;
+
+namespace UPNP
+{
+
+// the original version of content type here,eg: text/srt,which was defined 10 years ago (year 2013,commit 56519bec #L1158-L1161 )
+// is not a standard mime type. according to the specs of UPNP
+// http://upnp.org/specs/av/UPnP-av-ConnectionManager-v3-Service.pdf chapter "A.1.1 ProtocolInfo Definition"
+// "The <contentFormat> part for HTTP GET is described by a MIME type RFC https://www.ietf.org/rfc/rfc1341.txt"
+// all the pre-defined "text/*" MIME by IANA is here https://www.iana.org/assignments/media-types/media-types.xhtml#text
+// there is not any subtitle MIME for now (year 2022), we used to use text/srt|ssa|sub|idx, but,
+// kodi support SUP subtitle now, and SUP subtitle is not really a text(see below), to keep it
+// compatible, we suggest only to match the extension
+//
+// main purpose of this array is to share supported real subtitle formats when kodi act as a UPNP
+// server or UPNP/DLNA media render
+constexpr std::array<std::string_view, 9> SupportedSubFormats = {
+ "txt", "srt", "ssa", "ass", "sub", "smi", "vtt",
+ // "sup" subtitle is not a real TEXT,
+ // and there is no real STD subtitle RFC of DLNA,
+ // so we only match the extension of the "fake" content type
+ "sup", "idx"};
+
+// Map defining extensions for mimetypes not available in Platinum mimetype map
+// or that the application wants to override. These definitions take precedence
+// over all other possible mime type definitions.
+constexpr NPT_HttpFileRequestHandler_DefaultFileTypeMapEntry kodiPlatinumMimeTypeExtensions[] = {
+ {"m2ts", "video/vnd.dlna.mpeg-tts"}};
+
+/*----------------------------------------------------------------------
+| GetClientQuirks
++---------------------------------------------------------------------*/
+EClientQuirks GetClientQuirks(const PLT_HttpRequestContext* context)
+{
+ if(context == NULL)
+ return ECLIENTQUIRKS_NONE;
+
+ unsigned int quirks = 0;
+ const NPT_String* user_agent = context->GetRequest().GetHeaders().GetHeaderValue(NPT_HTTP_HEADER_USER_AGENT);
+ const NPT_String* server = context->GetRequest().GetHeaders().GetHeaderValue(NPT_HTTP_HEADER_SERVER);
+
+ if (user_agent) {
+ if (user_agent->Find("XBox", 0, true) >= 0 ||
+ user_agent->Find("Xenon", 0, true) >= 0)
+ quirks |= ECLIENTQUIRKS_ONLYSTORAGEFOLDER | ECLIENTQUIRKS_BASICVIDEOCLASS;
+
+ if (user_agent->Find("Windows-Media-Player", 0, true) >= 0)
+ quirks |= ECLIENTQUIRKS_UNKNOWNSERIES;
+
+ }
+ if (server) {
+ if (server->Find("Xbox", 0, true) >= 0)
+ quirks |= ECLIENTQUIRKS_ONLYSTORAGEFOLDER | ECLIENTQUIRKS_BASICVIDEOCLASS;
+ }
+
+ return (EClientQuirks)quirks;
+}
+
+/*----------------------------------------------------------------------
+| GetMediaControllerQuirks
++---------------------------------------------------------------------*/
+EMediaControllerQuirks GetMediaControllerQuirks(const PLT_DeviceData *device)
+{
+ if (device == NULL)
+ return EMEDIACONTROLLERQUIRKS_NONE;
+
+ unsigned int quirks = 0;
+
+ if (device->m_Manufacturer.Find("Samsung Electronics") >= 0)
+ quirks |= EMEDIACONTROLLERQUIRKS_X_MKV;
+
+ return (EMediaControllerQuirks)quirks;
+}
+
+/*----------------------------------------------------------------------
+| GetMimeType
++---------------------------------------------------------------------*/
+NPT_String
+GetMimeType(const char* filename,
+ const PLT_HttpRequestContext* context /* = NULL */)
+{
+ NPT_String ext = URIUtils::GetExtension(filename).c_str();
+ ext.TrimLeft('.');
+ ext = ext.ToLowercase();
+
+ return PLT_MimeType::GetMimeTypeFromExtension(ext, context);
+}
+
+/*----------------------------------------------------------------------
+| GetMimeType
++---------------------------------------------------------------------*/
+NPT_String
+GetMimeType(const CFileItem& item,
+ const PLT_HttpRequestContext* context /* = NULL */)
+{
+ std::string path = item.GetPath();
+ if (item.HasVideoInfoTag() && !item.GetVideoInfoTag()->GetPath().empty()) {
+ path = item.GetVideoInfoTag()->GetPath();
+ } else if (item.HasMusicInfoTag() && !item.GetMusicInfoTag()->GetURL().empty()) {
+ path = item.GetMusicInfoTag()->GetURL();
+ }
+
+ if (URIUtils::IsStack(path))
+ path = XFILE::CStackDirectory::GetFirstStackedFile(path);
+
+ NPT_String ext = URIUtils::GetExtension(path).c_str();
+ ext.TrimLeft('.');
+ ext = ext.ToLowercase();
+
+ NPT_String mime;
+
+ if (!ext.IsEmpty())
+ {
+ /* We look first to our extensions/overrides of libplatinum mimetypes. If not found, fallback to
+ Platinum definitions.
+ */
+ const auto kodiOverrideMimeType = std::find_if(
+ std::begin(kodiPlatinumMimeTypeExtensions), std::end(kodiPlatinumMimeTypeExtensions),
+ [&](const auto& mimeTypeEntry) { return mimeTypeEntry.extension == ext; });
+ if (kodiOverrideMimeType != std::end(kodiPlatinumMimeTypeExtensions))
+ {
+ mime = kodiOverrideMimeType->mime_type;
+ }
+ else
+ {
+ /* Give priority to Platinum mime types as they are defined to map extension to DLNA compliant mime types
+ or custom types according to context (who asked for it)
+ */
+ mime = PLT_MimeType::GetMimeTypeFromExtension(ext, context);
+ if (mime == "application/octet-stream")
+ {
+ mime = "";
+ }
+ }
+ }
+
+ /* if Platinum couldn't map it, default to Kodi internal mapping */
+ if (mime.IsEmpty()) {
+ NPT_String mime = item.GetMimeType().c_str();
+ if (mime == "application/octet-stream") mime = "";
+ }
+
+ /* fallback to generic mime type if not found */
+ if (mime.IsEmpty()) {
+ if (item.IsVideo() || item.IsVideoDb() )
+ mime = "video/" + ext;
+ else if (item.IsAudio() || item.IsMusicDb() )
+ mime = "audio/" + ext;
+ else if (item.IsPicture() )
+ mime = "image/" + ext;
+ else if (item.IsSubtitle())
+ mime = "text/" + ext;
+ }
+
+ /* nothing we can figure out */
+ if (mime.IsEmpty()) {
+ mime = "application/octet-stream";
+ }
+
+ return mime;
+}
+
+/*----------------------------------------------------------------------
+| GetProtocolInfo
++---------------------------------------------------------------------*/
+const NPT_String
+GetProtocolInfo(const CFileItem& item,
+ const char* protocol,
+ const PLT_HttpRequestContext* context /* = NULL */)
+{
+ NPT_String proto = protocol;
+
+ //! @todo fixup the protocol just in case nothing was passed
+ if (proto.IsEmpty()) {
+ proto = item.GetURL().GetProtocol().c_str();
+ }
+
+ /**
+ * map protocol to right prefix and use xbmc-get for
+ * unsupported UPnP protocols for other xbmc clients
+ * @todo add rtsp ?
+ */
+ if (proto == "http") {
+ proto = "http-get";
+ } else {
+ proto = "xbmc-get";
+ }
+
+ /* we need a valid extension to retrieve the mimetype for the protocol info */
+ NPT_String mime = GetMimeType(item, context);
+ proto += ":*:" + mime + ":" + PLT_ProtocolInfo::GetDlnaExtension(mime, context);
+ return proto;
+}
+
+ /*----------------------------------------------------------------------
+ | CResourceFinder
+ +---------------------------------------------------------------------*/
+CResourceFinder::CResourceFinder(const char* protocol, const char* content)
+ : m_Protocol(protocol)
+ , m_Content(content)
+{
+}
+
+bool CResourceFinder::operator()(const PLT_MediaItemResource& resource) const {
+ if (m_Content.IsEmpty())
+ return (resource.m_ProtocolInfo.GetProtocol().Compare(m_Protocol, true) == 0);
+ else
+ return ((resource.m_ProtocolInfo.GetProtocol().Compare(m_Protocol, true) == 0)
+ && resource.m_ProtocolInfo.GetContentType().StartsWith(m_Content, true));
+}
+
+/*----------------------------------------------------------------------
+| PopulateObjectFromTag
++---------------------------------------------------------------------*/
+NPT_Result
+PopulateObjectFromTag(CMusicInfoTag& tag,
+ PLT_MediaObject& object,
+ NPT_String* file_path,
+ PLT_MediaItemResource* resource,
+ EClientQuirks quirks,
+ UPnPService service /* = UPnPServiceNone */)
+{
+ if (!tag.GetURL().empty() && file_path)
+ *file_path = tag.GetURL().c_str();
+
+ std::vector<std::string> genres = tag.GetGenre();
+ for (unsigned int index = 0; index < genres.size(); index++)
+ object.m_Affiliation.genres.Add(genres.at(index).c_str());
+ object.m_Title = tag.GetTitle().c_str();
+ object.m_Affiliation.album = tag.GetAlbum().c_str();
+ for (unsigned int index = 0; index < tag.GetArtist().size(); index++)
+ {
+ object.m_People.artists.Add(tag.GetArtist().at(index).c_str());
+ object.m_People.artists.Add(tag.GetArtist().at(index).c_str(), "Performer");
+ }
+ object.m_People.artists.Add((!tag.GetAlbumArtistString().empty() ? tag.GetAlbumArtistString() : tag.GetArtistString()).c_str(), "AlbumArtist");
+ if(tag.GetAlbumArtistString().empty())
+ object.m_Creator = tag.GetArtistString().c_str();
+ else
+ object.m_Creator = tag.GetAlbumArtistString().c_str();
+ object.m_MiscInfo.original_track_number = tag.GetTrackNumber();
+ if(tag.GetDatabaseId() >= 0) {
+ object.m_ReferenceID = NPT_String::Format("musicdb://songs/%i%s", tag.GetDatabaseId(), URIUtils::GetExtension(tag.GetURL()).c_str());
+ }
+ if (object.m_ReferenceID == object.m_ObjectID)
+ object.m_ReferenceID = "";
+
+ object.m_MiscInfo.last_time = tag.GetLastPlayed().GetAsW3CDateTime().c_str();
+ object.m_MiscInfo.play_count = tag.GetPlayCount();
+
+ if (resource) resource->m_Duration = tag.GetDuration();
+
+ return NPT_SUCCESS;
+}
+
+/*----------------------------------------------------------------------
+| PopulateObjectFromTag
++---------------------------------------------------------------------*/
+NPT_Result
+PopulateObjectFromTag(CVideoInfoTag& tag,
+ PLT_MediaObject& object,
+ NPT_String* file_path,
+ PLT_MediaItemResource* resource,
+ EClientQuirks quirks,
+ UPnPService service /* = UPnPServiceNone */)
+{
+ if (!tag.m_strFileNameAndPath.empty() && file_path)
+ *file_path = tag.m_strFileNameAndPath.c_str();
+
+ if (tag.m_iDbId != -1 ) {
+ if (tag.m_type == MediaTypeMusicVideo) {
+ object.m_ObjectClass.type = "object.item.videoItem.musicVideoClip";
+ object.m_Creator = StringUtils::Join(tag.m_artist, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoItemSeparator).c_str();
+ for (const auto& itArtist : tag.m_artist)
+ object.m_People.artists.Add(itArtist.c_str());
+ object.m_Affiliation.album = tag.m_strAlbum.c_str();
+ object.m_Title = tag.m_strTitle.c_str();
+ object.m_Date = tag.GetPremiered().GetAsW3CDate().c_str();
+ object.m_ReferenceID = NPT_String::Format("videodb://musicvideos/titles/%i", tag.m_iDbId);
+ } else if (tag.m_type == MediaTypeMovie) {
+ object.m_ObjectClass.type = "object.item.videoItem.movie";
+ object.m_Title = tag.m_strTitle.c_str();
+ object.m_Date = tag.GetPremiered().GetAsW3CDate().c_str();
+ object.m_ReferenceID = NPT_String::Format("videodb://movies/titles/%i", tag.m_iDbId);
+ } else {
+ object.m_Recorded.series_title = tag.m_strShowTitle.c_str();
+
+ if (tag.m_type == MediaTypeTvShow) {
+ object.m_ObjectClass.type = "object.container.album.videoAlbum.videoBroadcastShow";
+ object.m_Title = tag.m_strTitle.c_str();
+ object.m_Recorded.episode_number = tag.m_iEpisode;
+ object.m_Recorded.episode_count = tag.m_iEpisode;
+ if (!tag.m_premiered.IsValid() && tag.GetYear() > 0)
+ object.m_Date = CDateTime(tag.GetYear(), 1, 1, 0, 0, 0).GetAsW3CDate().c_str();
+ else
+ object.m_Date = tag.m_premiered.GetAsW3CDate().c_str();
+ object.m_ReferenceID = NPT_String::Format("videodb://tvshows/titles/%i", tag.m_iDbId);
+ } else if (tag.m_type == MediaTypeSeason) {
+ object.m_ObjectClass.type = "object.container.album.videoAlbum.videoBroadcastSeason";
+ object.m_Title = tag.m_strTitle.c_str();
+ object.m_Recorded.episode_season = tag.m_iSeason;
+ object.m_Recorded.episode_count = tag.m_iEpisode;
+ if (!tag.m_premiered.IsValid() && tag.GetYear() > 0)
+ object.m_Date = CDateTime(tag.GetYear(), 1, 1, 0, 0, 0).GetAsW3CDate().c_str();
+ else
+ object.m_Date = tag.m_premiered.GetAsW3CDate().c_str();
+ object.m_ReferenceID = NPT_String::Format("videodb://tvshows/titles/%i/%i", tag.m_iIdShow, tag.m_iSeason);
+ } else {
+ object.m_ObjectClass.type = "object.item.videoItem.videoBroadcast";
+ object.m_Recorded.program_title = "S" + ("0" + NPT_String::FromInteger(tag.m_iSeason)).Right(2);
+ object.m_Recorded.program_title += "E" + ("0" + NPT_String::FromInteger(tag.m_iEpisode)).Right(2);
+ object.m_Recorded.program_title += (" : " + tag.m_strTitle).c_str();
+ object.m_Recorded.episode_number = tag.m_iEpisode;
+ object.m_Recorded.episode_season = tag.m_iSeason;
+ object.m_Title = object.m_Recorded.series_title + " - " + object.m_Recorded.program_title;
+ object.m_ReferenceID = NPT_String::Format("videodb://tvshows/titles/%i/%i/%i", tag.m_iIdShow, tag.m_iSeason, tag.m_iDbId);
+ object.m_Date = tag.m_firstAired.GetAsW3CDate().c_str();
+ }
+ }
+ }
+
+ if(quirks & ECLIENTQUIRKS_BASICVIDEOCLASS)
+ object.m_ObjectClass.type = "object.item.videoItem";
+
+ if(object.m_ReferenceID == object.m_ObjectID)
+ object.m_ReferenceID = "";
+
+ for (unsigned int index = 0; index < tag.m_studio.size(); index++)
+ object.m_People.publisher.Add(tag.m_studio[index].c_str());
+
+ object.m_XbmcInfo.date_added = tag.m_dateAdded.GetAsW3CDate().c_str();
+ object.m_XbmcInfo.rating = tag.GetRating().rating;
+ object.m_XbmcInfo.votes = tag.GetRating().votes;
+ object.m_XbmcInfo.unique_identifier = tag.GetUniqueID().c_str();
+ for (const auto& country : tag.m_country)
+ object.m_XbmcInfo.countries.Add(country.c_str());
+ object.m_XbmcInfo.user_rating = tag.m_iUserRating;
+
+ for (unsigned int index = 0; index < tag.m_genre.size(); index++)
+ object.m_Affiliation.genres.Add(tag.m_genre.at(index).c_str());
+
+ for (CVideoInfoTag::iCast it = tag.m_cast.begin(); it != tag.m_cast.end(); ++it)
+ {
+ object.m_People.actors.Add(it->strName.c_str(), it->strRole.c_str());
+ }
+
+ for (unsigned int index = 0; index < tag.m_director.size(); index++)
+ object.m_People.directors.Add(tag.m_director[index].c_str());
+
+ for (unsigned int index = 0; index < tag.m_writingCredits.size(); index++)
+ object.m_People.authors.Add(tag.m_writingCredits[index].c_str());
+
+ object.m_Description.description = tag.m_strTagLine.c_str();
+ object.m_Description.long_description = tag.m_strPlot.c_str();
+ object.m_Description.rating = tag.m_strMPAARating.c_str();
+ object.m_MiscInfo.last_position = (NPT_UInt32)tag.GetResumePoint().timeInSeconds;
+ object.m_XbmcInfo.last_playerstate = tag.GetResumePoint().playerState.c_str();
+ object.m_MiscInfo.last_time = tag.m_lastPlayed.GetAsW3CDateTime().c_str();
+ object.m_MiscInfo.play_count = tag.GetPlayCount();
+ if (resource) {
+ resource->m_Duration = tag.GetDuration();
+ if (tag.HasStreamDetails()) {
+ const CStreamDetails &details = tag.m_streamDetails;
+ resource->m_Resolution = NPT_String::FromInteger(details.GetVideoWidth()) + "x" + NPT_String::FromInteger(details.GetVideoHeight());
+ resource->m_NbAudioChannels = details.GetAudioChannels();
+ }
+ }
+
+ return NPT_SUCCESS;
+}
+
+/*----------------------------------------------------------------------
+| BuildObject
++---------------------------------------------------------------------*/
+PLT_MediaObject*
+BuildObject(CFileItem& item,
+ NPT_String& file_path,
+ bool with_count,
+ NPT_Reference<CThumbLoader>& thumb_loader,
+ const PLT_HttpRequestContext* context /* = NULL */,
+ CUPnPServer* upnp_server /* = NULL */,
+ UPnPService upnp_service /* = UPnPServiceNone */)
+{
+ static Logger logger = CServiceBroker::GetLogging().GetLogger("UPNP::BuildObject");
+
+ PLT_MediaItemResource resource;
+ PLT_MediaObject* object = NULL;
+ std::string thumb;
+
+ logger->debug("Building didl for object '{}'", item.GetPath());
+
+ auto settingsComponent = CServiceBroker::GetSettingsComponent();
+ if (!settingsComponent)
+ return nullptr;
+
+ auto settings = settingsComponent->GetSettings();
+ if (!settings)
+ return nullptr;
+
+ EClientQuirks quirks = GetClientQuirks(context);
+
+ // get list of ip addresses
+ NPT_List<NPT_IpAddress> ips;
+ NPT_HttpUrl rooturi;
+ NPT_CHECK_LABEL(PLT_UPnPMessageHelper::GetIPAddresses(ips), failure);
+
+ // if we're passed an interface where we received the request from
+ // move the ip to the top
+ if (context && context->GetLocalAddress().GetIpAddress().ToString() != "0.0.0.0")
+ {
+ rooturi = NPT_HttpUrl(context->GetLocalAddress().GetIpAddress().ToString(),
+ context->GetLocalAddress().GetPort(), "/");
+ ips.Remove(context->GetLocalAddress().GetIpAddress());
+ ips.Insert(ips.GetFirstItem(), context->GetLocalAddress().GetIpAddress());
+ } else if(upnp_server) {
+ rooturi = NPT_HttpUrl("localhost", upnp_server->GetPort(), "/");
+ }
+
+ if (!item.m_bIsFolder) {
+ object = new PLT_MediaItem();
+ object->m_ObjectID = item.GetPath().c_str();
+
+ /* Setup object type */
+ if (item.IsMusicDb() || item.IsAudio()) {
+ object->m_ObjectClass.type = "object.item.audioItem.musicTrack";
+
+ if (item.HasMusicInfoTag()) {
+ CMusicInfoTag *tag = item.GetMusicInfoTag();
+ PopulateObjectFromTag(*tag, *object, &file_path, &resource, quirks, upnp_service);
+ }
+ } else if (item.IsVideoDb() || item.IsVideo()) {
+ object->m_ObjectClass.type = "object.item.videoItem";
+
+ if(quirks & ECLIENTQUIRKS_UNKNOWNSERIES)
+ object->m_Affiliation.album = "[Unknown Series]";
+
+ if (item.HasVideoInfoTag()) {
+ CVideoInfoTag *tag = item.GetVideoInfoTag();
+ PopulateObjectFromTag(*tag, *object, &file_path, &resource, quirks, upnp_service);
+ }
+ } else if (item.IsPicture()) {
+ object->m_ObjectClass.type = "object.item.imageItem.photo";
+ } else {
+ object->m_ObjectClass.type = "object.item";
+ }
+
+ // duration of zero is invalid
+ if (resource.m_Duration == 0) resource.m_Duration = -1;
+
+ // Set the resource file size
+ resource.m_Size = item.m_dwSize;
+ if(resource.m_Size == 0)
+ resource.m_Size = (NPT_LargeSize)-1;
+
+ // set date
+ if (object->m_Date.IsEmpty() && item.m_dateTime.IsValid()) {
+ object->m_Date = item.m_dateTime.GetAsW3CDate().c_str();
+ }
+
+ if (upnp_server) {
+ upnp_server->AddSafeResourceUri(object, rooturi, ips, file_path, GetProtocolInfo(item, "http", context));
+ }
+
+ // if the item is remote, add a direct link to the item
+ if (URIUtils::IsRemote((const char*)file_path)) {
+ resource.m_ProtocolInfo = PLT_ProtocolInfo(GetProtocolInfo(item, item.GetURL().GetProtocol().c_str(), context));
+ resource.m_Uri = file_path;
+
+ // if the direct link can be served directly using http, then push it in front
+ // otherwise keep the xbmc-get resource last and let a compatible client look for it
+ if (resource.m_ProtocolInfo.ToString().StartsWith("xbmc", true)) {
+ object->m_Resources.Add(resource);
+ } else {
+ object->m_Resources.Insert(object->m_Resources.GetFirstItem(), resource);
+ }
+ }
+
+ // copy across the known metadata
+ for(unsigned i=0; i<object->m_Resources.GetItemCount(); i++) {
+ object->m_Resources[i].m_Size = resource.m_Size;
+ object->m_Resources[i].m_Duration = resource.m_Duration;
+ object->m_Resources[i].m_Resolution = resource.m_Resolution;
+ }
+
+ // Some upnp clients expect all audio items to have parent root id 4
+#ifdef WMP_ID_MAPPING
+ object->m_ParentID = "4";
+#endif
+ } else {
+ PLT_MediaContainer* container = new PLT_MediaContainer;
+ object = container;
+
+ /* Assign a title and id for this container */
+ container->m_ObjectID = item.GetPath().c_str();
+ container->m_ObjectClass.type = "object.container";
+ container->m_ChildrenCount = -1;
+
+ /* this might be overkill, but hey */
+ if (item.IsMusicDb()) {
+ MUSICDATABASEDIRECTORY::NODE_TYPE node = CMusicDatabaseDirectory::GetDirectoryType(item.GetPath());
+ switch(node) {
+ case MUSICDATABASEDIRECTORY::NODE_TYPE_ARTIST: {
+ container->m_ObjectClass.type += ".person.musicArtist";
+ CMusicInfoTag *tag = item.GetMusicInfoTag();
+ if (tag) {
+ container->m_People.artists.Add(
+ CorrectAllItemsSortHack(tag->GetArtistString()).c_str(), "Performer");
+ container->m_People.artists.Add(
+ CorrectAllItemsSortHack((!tag->GetAlbumArtistString().empty() ? tag->GetAlbumArtistString() : tag->GetArtistString())).c_str(), "AlbumArtist");
+ }
+#ifdef WMP_ID_MAPPING
+ // Some upnp clients expect all artists to have parent root id 107
+ container->m_ParentID = "107";
+#endif
+ }
+ break;
+ case MUSICDATABASEDIRECTORY::NODE_TYPE_ALBUM:
+ case MUSICDATABASEDIRECTORY::NODE_TYPE_ALBUM_RECENTLY_ADDED: {
+ container->m_ObjectClass.type += ".album.musicAlbum";
+ // for Sonos to be happy
+ CMusicInfoTag *tag = item.GetMusicInfoTag();
+ if (tag) {
+ container->m_People.artists.Add(
+ CorrectAllItemsSortHack(tag->GetArtistString()).c_str(), "Performer");
+ container->m_People.artists.Add(
+ CorrectAllItemsSortHack(!tag->GetAlbumArtistString().empty() ? tag->GetAlbumArtistString() : tag->GetArtistString()).c_str(), "AlbumArtist");
+ container->m_Affiliation.album = CorrectAllItemsSortHack(tag->GetAlbum()).c_str();
+ }
+#ifdef WMP_ID_MAPPING
+ // Some upnp clients expect all albums to have parent root id 7
+ container->m_ParentID = "7";
+#endif
+ }
+ break;
+ case MUSICDATABASEDIRECTORY::NODE_TYPE_GENRE:
+ container->m_ObjectClass.type += ".genre.musicGenre";
+ break;
+ default:
+ break;
+ }
+ } else if (item.IsVideoDb()) {
+ VIDEODATABASEDIRECTORY::NODE_TYPE node = CVideoDatabaseDirectory::GetDirectoryType(item.GetPath());
+ CVideoInfoTag &tag = *item.GetVideoInfoTag();
+ switch(node) {
+ case VIDEODATABASEDIRECTORY::NODE_TYPE_GENRE:
+ container->m_ObjectClass.type += ".genre.movieGenre";
+ break;
+ case VIDEODATABASEDIRECTORY::NODE_TYPE_ACTOR:
+ container->m_ObjectClass.type += ".person.videoArtist";
+ container->m_Creator =
+ StringUtils::Join(
+ tag.m_artist,
+ settingsComponent->GetAdvancedSettings()->m_videoItemSeparator)
+ .c_str();
+ container->m_Title = tag.m_strTitle.c_str();
+ break;
+ case VIDEODATABASEDIRECTORY::NODE_TYPE_SEASONS:
+ container->m_ObjectClass.type += ".album.videoAlbum.videoBroadcastSeason";
+ if (item.HasVideoInfoTag()) {
+ CVideoInfoTag *tag = (CVideoInfoTag*)item.GetVideoInfoTag();
+ PopulateObjectFromTag(*tag, *container, &file_path, &resource, quirks);
+ }
+ break;
+ case VIDEODATABASEDIRECTORY::NODE_TYPE_TITLE_TVSHOWS:
+ container->m_ObjectClass.type += ".album.videoAlbum.videoBroadcastShow";
+ if (item.HasVideoInfoTag()) {
+ CVideoInfoTag *tag = (CVideoInfoTag*)item.GetVideoInfoTag();
+ PopulateObjectFromTag(*tag, *container, &file_path, &resource, quirks);
+ }
+ break;
+ default:
+ container->m_ObjectClass.type += ".storageFolder";
+ break;
+ }
+ } else if (item.IsPlayList() || item.IsSmartPlayList()) {
+ container->m_ObjectClass.type += ".playlistContainer";
+ }
+
+ if(quirks & ECLIENTQUIRKS_ONLYSTORAGEFOLDER) {
+ container->m_ObjectClass.type = "object.container.storageFolder";
+ }
+
+ /* Get the number of children for this container */
+ if (with_count && upnp_server) {
+ if (object->m_ObjectID.StartsWith("virtualpath://")) {
+ NPT_LargeSize count = 0;
+ NPT_CHECK_LABEL(NPT_File::GetSize(file_path, count), failure);
+ container->m_ChildrenCount = (NPT_Int32)count;
+ } else {
+ /* this should be a standard path */
+ //! @todo - get file count of this directory
+ }
+ }
+ }
+
+ // set a title for the object
+ if (object->m_Title.IsEmpty()) {
+ if (!item.GetLabel().empty()) {
+ std::string title = item.GetLabel();
+ if (item.IsPlayList() || !item.m_bIsFolder) URIUtils::RemoveExtension(title);
+ object->m_Title = title.c_str();
+ }
+ }
+
+ if (upnp_server) {
+ // determine the correct artwork for this item
+ if (!thumb_loader.IsNull())
+ thumb_loader->LoadItem(&item);
+
+ // we have to decide the best art type to serve to the client - use ContentUtils
+ // to get it since it depends on the mediatype of the item being served
+ thumb = ContentUtils::GetPreferredArtImage(item);
+
+ if (!thumb.empty()) {
+ PLT_AlbumArtInfo art;
+ // Set DLNA profileID by extension, defaulting to JPEG.
+ if (URIUtils::HasExtension(thumb, ".png"))
+ {
+ art.dlna_profile = "PNG_TN";
+ }
+ else
+ {
+ art.dlna_profile = "JPEG_TN";
+ }
+ // append /thumb to the safe resource uri to avoid clients flagging the item with
+ // the incorrect mimetype (derived from the file extension)
+ art.uri = upnp_server->BuildSafeResourceUri(
+ rooturi, (*ips.GetFirstItem()).ToString(),
+ std::string(CTextureUtils::GetWrappedImageURL(thumb) + "/thumb").c_str());
+ object->m_ExtraInfo.album_arts.Add(art);
+ }
+
+ for (const auto& itArtwork : item.GetArt())
+ {
+ if (!itArtwork.first.empty() && !itArtwork.second.empty())
+ {
+ std::string wrappedUrl = CTextureUtils::GetWrappedImageURL(itArtwork.second);
+ object->m_XbmcInfo.artwork.Add(
+ itArtwork.first.c_str(),
+ upnp_server->BuildSafeResourceUri(rooturi, (*ips.GetFirstItem()).ToString(),
+ wrappedUrl.c_str()));
+ upnp_server->AddSafeResourceUri(object, rooturi, ips, wrappedUrl.c_str(),
+ ("xbmc.org:*:" + itArtwork.first + ":*").c_str());
+ }
+ }
+ }
+
+ // look for and add external subtitle if we are processing a video file and
+ // we are being called by a UPnP player or renderer or the user has chosen
+ // to look for external subtitles
+ if (upnp_server != NULL && item.IsVideo() &&
+ (upnp_service == UPnPPlayer || upnp_service == UPnPRenderer ||
+ settings->GetBool(CSettings::SETTING_SERVICES_UPNPLOOKFOREXTERNALSUBTITLES)))
+ {
+ // find any available external subtitles
+ std::vector<std::string> filenames;
+ std::vector<std::string> subtitles;
+ CUtil::ScanForExternalSubtitles(file_path.GetChars(), filenames);
+
+ std::string ext;
+ for (unsigned int i = 0; i < filenames.size(); i++)
+ {
+ ext = URIUtils::GetExtension(filenames[i]).c_str();
+ ext = ext.substr(1);
+ std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower);
+ /* Hardcoded check for extension is not the best way, but it can't be allowed to pass all
+ subtitle extension (ex. rar or zip). There are the most popular extensions support by UPnP devices.*/
+ for (std::string_view type : SupportedSubFormats)
+ {
+ if (type == ext)
+ {
+ subtitles.push_back(filenames[i]);
+ }
+ }
+ }
+
+ std::string subtitlePath;
+
+ if (subtitles.size() == 1)
+ {
+ subtitlePath = subtitles[0];
+ }
+ else if (!subtitles.empty())
+ {
+ std::string preferredLanguage{"en"};
+
+ /* trying to find subtitle with preferred language settings */
+ auto setting = settings->GetSetting("locale.subtitlelanguage");
+ if (!setting)
+ CLog::Log(LOGERROR, "Failed to load setting for: {}", "locale.subtitlelanguage");
+ else
+ preferredLanguage = setting->ToString();
+
+ std::string preferredLanguageCode;
+ g_LangCodeExpander.ConvertToISO6392B(preferredLanguage, preferredLanguageCode);
+
+ for (unsigned int i = 0; i < subtitles.size(); i++)
+ {
+ ExternalStreamInfo info =
+ CUtil::GetExternalStreamDetailsFromFilename(file_path.GetChars(), subtitles[i]);
+
+ if (preferredLanguageCode == info.language)
+ {
+ subtitlePath = subtitles[i];
+ break;
+ }
+ }
+ /* if not found subtitle with preferred language, get the first one */
+ if (subtitlePath.empty())
+ {
+ subtitlePath = subtitles[0];
+ }
+ }
+
+ if (!subtitlePath.empty())
+ {
+ /* subtitles are added as 2 resources, 2 sec resources and 1 addon to video resource, to be compatible with
+ the most of the devices; all UPnP devices take the last one it could handle,
+ and skip ones it doesn't "understand" */
+ // add subtitle resource with standard protocolInfo
+ NPT_String protocolInfo = GetProtocolInfo(CFileItem(subtitlePath, false), "http", context);
+ upnp_server->AddSafeResourceUri(object, rooturi, ips, NPT_String(subtitlePath.c_str()), protocolInfo);
+ // add subtitle resource with smi/caption protocol info (some devices)
+ PLT_ProtocolInfo protInfo = PLT_ProtocolInfo(protocolInfo);
+ protocolInfo = protInfo.GetProtocol() + ":" + protInfo.GetMask() + ":smi/caption:" + protInfo.GetExtra();
+ upnp_server->AddSafeResourceUri(object, rooturi, ips, NPT_String(subtitlePath.c_str()), protocolInfo);
+
+ ext = URIUtils::GetExtension(subtitlePath).c_str();
+ ext = ext.substr(1);
+ std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower);
+
+ NPT_String subtitle_uri = object->m_Resources[object->m_Resources.GetItemCount() - 1].m_Uri;
+
+ // add subtitle to video resource (the first one) (for some devices)
+ object->m_Resources[0].m_CustomData["xmlns:pv"] = "http://www.pv.com/pvns/";
+ object->m_Resources[0].m_CustomData["pv:subtitleFileUri"] = subtitle_uri;
+ object->m_Resources[0].m_CustomData["pv:subtitleFileType"] = ext.c_str();
+
+ // for samsung devices
+ PLT_SecResource sec_res;
+ sec_res.name = "CaptionInfoEx";
+ sec_res.value = subtitle_uri;
+ sec_res.attributes["type"] = ext.c_str();
+ object->m_SecResources.Add(sec_res);
+ sec_res.name = "CaptionInfo";
+ object->m_SecResources.Add(sec_res);
+
+ // adding subtitle uri for movie md5, for later use in http response
+ NPT_String movie_md5 = object->m_Resources[0].m_Uri;
+ movie_md5 = movie_md5.Right(movie_md5.GetLength() - movie_md5.Find("/%25/") - 5);
+ upnp_server->AddSubtitleUriForSecResponse(movie_md5, subtitle_uri);
+ }
+ }
+
+ return object;
+
+failure:
+ delete object;
+ return NULL;
+}
+
+/*----------------------------------------------------------------------
+| CUPnPServer::CorrectAllItemsSortHack
++---------------------------------------------------------------------*/
+const std::string&
+CorrectAllItemsSortHack(const std::string &item)
+{
+ // This is required as in order for the "* All Albums" etc. items to sort
+ // correctly, they must have fake artist/album etc. information generated.
+ // This looks nasty if we attempt to render it to the GUI, thus this (further)
+ // workaround
+ if ((item.size() == 1 && item[0] == 0x01) || (item.size() > 1 && ((unsigned char) item[1]) == 0xff))
+ return StringUtils::Empty;
+
+ return item;
+}
+
+int
+PopulateTagFromObject(CMusicInfoTag& tag,
+ PLT_MediaObject& object,
+ PLT_MediaItemResource* resource /* = NULL */,
+ UPnPService service /* = UPnPServiceNone */)
+{
+ tag.SetTitle((const char*)object.m_Title);
+ tag.SetArtist((const char*)object.m_Creator);
+ for(PLT_PersonRoles::Iterator it = object.m_People.artists.GetFirstItem(); it; it++) {
+ if (it->role == "") tag.SetArtist((const char*)it->name);
+ else if(it->role == "Performer") tag.SetArtist((const char*)it->name);
+ else if(it->role == "AlbumArtist") tag.SetAlbumArtist((const char*)it->name);
+ }
+ tag.SetTrackNumber(object.m_MiscInfo.original_track_number);
+
+ for (NPT_List<NPT_String>::Iterator it = object.m_Affiliation.genres.GetFirstItem(); it; it++) {
+ // ignore single "Unknown" genre inserted by Platinum
+ if (it == object.m_Affiliation.genres.GetFirstItem() && object.m_Affiliation.genres.GetItemCount() == 1 &&
+ *it == "Unknown")
+ break;
+
+ tag.SetGenre((const char*) *it);
+ }
+
+ tag.SetAlbum((const char*)object.m_Affiliation.album);
+ CDateTime last;
+ last.SetFromW3CDateTime((const char*)object.m_MiscInfo.last_time);
+ tag.SetLastPlayed(last);
+ tag.SetPlayCount(object.m_MiscInfo.play_count);
+ if(resource)
+ tag.SetDuration(resource->m_Duration);
+ tag.SetLoaded();
+ return NPT_SUCCESS;
+}
+
+int
+PopulateTagFromObject(CVideoInfoTag& tag,
+ PLT_MediaObject& object,
+ PLT_MediaItemResource* resource /* = NULL */,
+ UPnPService service /* = UPnPServiceNone */)
+{
+ CDateTime date;
+ date.SetFromW3CDate((const char*)object.m_Date);
+
+ if(!object.m_Recorded.program_title.IsEmpty() || object.m_ObjectClass.type == "object.item.videoItem.videoBroadcast")
+ {
+ tag.m_type = MediaTypeEpisode;
+ tag.m_strShowTitle = object.m_Recorded.series_title;
+ if (date.IsValid())
+ tag.m_firstAired = date;
+
+ int title = object.m_Recorded.program_title.Find(" : ");
+ if (title >= 0)
+ tag.m_strTitle = object.m_Recorded.program_title.SubString(title + 3);
+ else
+ tag.m_strTitle = object.m_Recorded.program_title;
+
+ int episode;
+ int season;
+ if (object.m_Recorded.episode_number > 0 && object.m_Recorded.episode_season < (NPT_UInt32)-1) {
+ tag.m_iEpisode = object.m_Recorded.episode_number;
+ tag.m_iSeason = object.m_Recorded.episode_season;
+ } else if(sscanf(object.m_Recorded.program_title, "S%2dE%2d", &season, &episode) == 2 && title >= 0) {
+ tag.m_iEpisode = episode;
+ tag.m_iSeason = season;
+ } else {
+ tag.m_iSeason = object.m_Recorded.episode_number / 100;
+ tag.m_iEpisode = object.m_Recorded.episode_number % 100;
+ }
+ }
+ else {
+ tag.m_strTitle = object.m_Title;
+ if (date.IsValid())
+ tag.m_premiered = date;
+
+ if (!object.m_Recorded.series_title.IsEmpty()) {
+ if (object.m_ObjectClass.type == "object.container.album.videoAlbum.videoBroadcastSeason") {
+ tag.m_type = MediaTypeSeason;
+ tag.m_iSeason = object.m_Recorded.episode_season;
+ tag.m_strShowTitle = object.m_Recorded.series_title;
+ }
+ else {
+ tag.m_type = MediaTypeTvShow;
+ tag.m_strShowTitle = object.m_Title;
+ }
+
+ if (object.m_Recorded.episode_count > 0)
+ tag.m_iEpisode = object.m_Recorded.episode_count;
+ else
+ tag.m_iEpisode = object.m_Recorded.episode_number;
+ }
+ else if(object.m_ObjectClass.type == "object.item.videoItem.musicVideoClip") {
+ tag.m_type = MediaTypeMusicVideo;
+
+ if (object.m_People.artists.GetItemCount() > 0) {
+ for (unsigned int index = 0; index < object.m_People.artists.GetItemCount(); index++)
+ tag.m_artist.emplace_back(object.m_People.artists.GetItem(index)->name.GetChars());
+ }
+ else if (!object.m_Creator.IsEmpty() && object.m_Creator != "Unknown")
+ tag.m_artist = StringUtils::Split(object.m_Creator.GetChars(), CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoItemSeparator);
+ tag.m_strAlbum = object.m_Affiliation.album;
+ }
+ else
+ tag.m_type = MediaTypeMovie;
+
+ tag.m_strTitle = object.m_Title;
+ if (date.IsValid())
+ tag.SetPremiered(date);
+ }
+
+ for (unsigned int index = 0; index < object.m_People.publisher.GetItemCount(); index++)
+ tag.m_studio.emplace_back(object.m_People.publisher.GetItem(index)->GetChars());
+
+ tag.m_dateAdded.SetFromW3CDate((const char*)object.m_XbmcInfo.date_added);
+ tag.SetRating(object.m_XbmcInfo.rating, object.m_XbmcInfo.votes);
+ tag.SetUniqueID(object.m_XbmcInfo.unique_identifier.GetChars());
+ for (unsigned int index = 0; index < object.m_XbmcInfo.countries.GetItemCount(); index++)
+ tag.m_country.emplace_back(object.m_XbmcInfo.countries.GetItem(index)->GetChars());
+ tag.m_iUserRating = object.m_XbmcInfo.user_rating;
+
+ for (unsigned int index = 0; index < object.m_Affiliation.genres.GetItemCount(); index++)
+ {
+ // ignore single "Unknown" genre inserted by Platinum
+ if (index == 0 && object.m_Affiliation.genres.GetItemCount() == 1 &&
+ *object.m_Affiliation.genres.GetItem(index) == "Unknown")
+ break;
+
+ tag.m_genre.emplace_back(object.m_Affiliation.genres.GetItem(index)->GetChars());
+ }
+ for (unsigned int index = 0; index < object.m_People.directors.GetItemCount(); index++)
+ tag.m_director.emplace_back(object.m_People.directors.GetItem(index)->name.GetChars());
+ for (unsigned int index = 0; index < object.m_People.authors.GetItemCount(); index++)
+ tag.m_writingCredits.emplace_back(object.m_People.authors.GetItem(index)->name.GetChars());
+ for (unsigned int index = 0; index < object.m_People.actors.GetItemCount(); index++)
+ {
+ SActorInfo info;
+ info.strName = object.m_People.actors.GetItem(index)->name;
+ info.strRole = object.m_People.actors.GetItem(index)->role;
+ tag.m_cast.push_back(info);
+ }
+ tag.m_strTagLine = object.m_Description.description;
+ tag.m_strPlot = object.m_Description.long_description;
+ tag.m_strMPAARating = object.m_Description.rating;
+ tag.m_strShowTitle = object.m_Recorded.series_title;
+ tag.m_lastPlayed.SetFromW3CDateTime((const char*)object.m_MiscInfo.last_time);
+ tag.SetPlayCount(object.m_MiscInfo.play_count);
+
+ if(resource)
+ {
+ if (resource->m_Duration)
+ tag.SetDuration(resource->m_Duration);
+ if (object.m_MiscInfo.last_position > 0 )
+ {
+ tag.SetResumePoint(object.m_MiscInfo.last_position,
+ resource->m_Duration,
+ object.m_XbmcInfo.last_playerstate.GetChars());
+ }
+ if (!resource->m_Resolution.IsEmpty())
+ {
+ int width, height;
+ if (sscanf(resource->m_Resolution, "%dx%d", &width, &height) == 2)
+ {
+ CStreamDetailVideo* detail = new CStreamDetailVideo;
+ detail->m_iWidth = width;
+ detail->m_iHeight = height;
+ detail->m_iDuration = tag.GetDuration();
+ tag.m_streamDetails.AddStream(detail);
+ }
+ }
+ if (resource->m_NbAudioChannels > 0)
+ {
+ CStreamDetailAudio* detail = new CStreamDetailAudio;
+ detail->m_iChannels = resource->m_NbAudioChannels;
+ tag.m_streamDetails.AddStream(detail);
+ }
+ }
+ return NPT_SUCCESS;
+}
+
+std::shared_ptr<CFileItem> BuildObject(PLT_MediaObject* entry,
+ UPnPService upnp_service /* = UPnPServiceNone */)
+{
+ NPT_String ObjectClass = entry->m_ObjectClass.type.ToLowercase();
+
+ CFileItemPtr pItem(new CFileItem((const char*)entry->m_Title));
+ pItem->SetLabelPreformatted(true);
+ pItem->m_strTitle = (const char*)entry->m_Title;
+ pItem->m_bIsFolder = entry->IsContainer();
+
+ // if it's a container, format a string as upnp://uuid/object_id
+ if (pItem->m_bIsFolder) {
+
+ // look for metadata
+ if( ObjectClass.StartsWith("object.container.album.videoalbum") ) {
+ pItem->SetLabelPreformatted(false);
+ UPNP::PopulateTagFromObject(*pItem->GetVideoInfoTag(), *entry, NULL, upnp_service);
+
+ } else if( ObjectClass.StartsWith("object.container.album.photoalbum")) {
+ //CPictureInfoTag* tag = pItem->GetPictureInfoTag();
+
+ } else if( ObjectClass.StartsWith("object.container.album") ) {
+ pItem->SetLabelPreformatted(false);
+ UPNP::PopulateTagFromObject(*pItem->GetMusicInfoTag(), *entry, NULL, upnp_service);
+ }
+
+ } else {
+ bool audio = false
+ , image = false
+ , video = false;
+ // set a general content type
+ const char* content = NULL;
+ if (ObjectClass.StartsWith("object.item.videoitem")) {
+ pItem->SetMimeType("video/octet-stream");
+ content = "video";
+ video = true;
+ }
+ else if(ObjectClass.StartsWith("object.item.audioitem")) {
+ pItem->SetMimeType("audio/octet-stream");
+ content = "audio";
+ audio = true;
+ }
+ else if(ObjectClass.StartsWith("object.item.imageitem")) {
+ pItem->SetMimeType("image/octet-stream");
+ content = "image";
+ image = true;
+ }
+
+ // attempt to find a valid resource (may be multiple)
+ PLT_MediaItemResource resource, *res = NULL;
+ if(NPT_SUCCEEDED(NPT_ContainerFind(entry->m_Resources,
+ CResourceFinder("http-get", content), resource))) {
+
+ // set metadata
+ if (resource.m_Size != (NPT_LargeSize)-1) {
+ pItem->m_dwSize = resource.m_Size;
+ }
+ res = &resource;
+ }
+ // look for metadata
+ if(video) {
+ pItem->SetLabelPreformatted(false);
+ UPNP::PopulateTagFromObject(*pItem->GetVideoInfoTag(), *entry, res, upnp_service);
+
+ } else if(audio) {
+ pItem->SetLabelPreformatted(false);
+ UPNP::PopulateTagFromObject(*pItem->GetMusicInfoTag(), *entry, res, upnp_service);
+
+ } else if(image) {
+ //! @todo fill pictureinfotag?
+ GetResource(entry, *pItem);
+ }
+ }
+
+ // look for date?
+ if(entry->m_Description.date.GetLength()) {
+ KODI::TIME::SystemTime time = {};
+ sscanf(entry->m_Description.date, "%hu-%hu-%huT%hu:%hu:%hu", &time.year, &time.month, &time.day,
+ &time.hour, &time.minute, &time.second);
+ pItem->m_dateTime = time;
+ }
+
+ // if there is a thumbnail available set it here
+ if(entry->m_ExtraInfo.album_arts.GetItem(0))
+ // only considers first album art
+ pItem->SetArt("thumb", (const char*) entry->m_ExtraInfo.album_arts.GetItem(0)->uri);
+ else if(entry->m_Description.icon_uri.GetLength())
+ pItem->SetArt("thumb", (const char*) entry->m_Description.icon_uri);
+
+ for (unsigned int index = 0; index < entry->m_XbmcInfo.artwork.GetItemCount(); index++)
+ pItem->SetArt(entry->m_XbmcInfo.artwork.GetItem(index)->type.GetChars(),
+ entry->m_XbmcInfo.artwork.GetItem(index)->url.GetChars());
+
+ // set the watched overlay, as this will not be set later due to
+ // content set on file item list
+ if (pItem->HasVideoInfoTag()) {
+ int episodes = pItem->GetVideoInfoTag()->m_iEpisode;
+ int played = pItem->GetVideoInfoTag()->GetPlayCount();
+ const std::string& type = pItem->GetVideoInfoTag()->m_type;
+ bool watched(false);
+ if (type == MediaTypeTvShow || type == MediaTypeSeason) {
+ pItem->SetProperty("totalepisodes", episodes);
+ pItem->SetProperty("numepisodes", episodes);
+ pItem->SetProperty("watchedepisodes", played);
+ pItem->SetProperty("unwatchedepisodes", episodes - played);
+ pItem->SetProperty("watchedepisodepercent", episodes > 0 ? played * 100 / episodes : 0);
+ watched = (episodes && played >= episodes);
+ pItem->GetVideoInfoTag()->SetPlayCount(watched ? 1 : 0);
+ }
+ else if (type == MediaTypeEpisode || type == MediaTypeMovie)
+ watched = (played > 0);
+ pItem->SetOverlayImage(CGUIListItem::ICON_OVERLAY_UNWATCHED, watched);
+ }
+ return pItem;
+}
+
+struct ResourcePrioritySort
+{
+ explicit ResourcePrioritySort(const PLT_MediaObject* entry)
+ {
+ if (entry->m_ObjectClass.type.StartsWith("object.item.audioItem"))
+ m_content = "audio";
+ else if (entry->m_ObjectClass.type.StartsWith("object.item.imageItem"))
+ m_content = "image";
+ else if (entry->m_ObjectClass.type.StartsWith("object.item.videoItem"))
+ m_content = "video";
+ }
+
+ int GetPriority(const PLT_MediaItemResource& res) const
+ {
+ int prio = 0;
+
+ if (m_content != "" && res.m_ProtocolInfo.GetContentType().StartsWith(m_content))
+ prio += 400;
+
+ NPT_Url url(res.m_Uri);
+ if (URIUtils::IsHostOnLAN((const char*)url.GetHost(), false))
+ prio += 300;
+
+ if (res.m_ProtocolInfo.GetProtocol() == "xbmc-get")
+ prio += 200;
+ else if (res.m_ProtocolInfo.GetProtocol() == "http-get")
+ prio += 100;
+
+ return prio;
+ }
+
+ int operator()(const PLT_MediaItemResource& lh, const PLT_MediaItemResource& rh) const
+ {
+ if(GetPriority(lh) < GetPriority(rh))
+ return 1;
+ else
+ return 0;
+ }
+
+ NPT_String m_content;
+};
+
+bool GetResource(const PLT_MediaObject* entry, CFileItem& item)
+{
+ static Logger logger = CServiceBroker::GetLogging().GetLogger("CUPnPDirectory::GetResource");
+
+ PLT_MediaItemResource resource;
+
+ // store original path so we remember it
+ item.SetProperty("original_listitem_url", item.GetPath());
+ item.SetProperty("original_listitem_mime", item.GetMimeType());
+
+ // get a sorted list based on our preference
+ NPT_List<PLT_MediaItemResource> sorted;
+ for (NPT_Cardinal i = 0; i < entry->m_Resources.GetItemCount(); ++i) {
+ sorted.Add(entry->m_Resources[i]);
+ }
+ sorted.Sort(ResourcePrioritySort(entry));
+
+ if(sorted.GetItemCount() == 0)
+ return false;
+
+ resource = *sorted.GetFirstItem();
+
+ // if it's an item, path is the first url to the item
+ // we hope the server made the first one reachable for us
+ // (it could be a format we dont know how to play however)
+ item.SetDynPath((const char*) resource.m_Uri);
+
+ // look for content type in protocol info
+ if (resource.m_ProtocolInfo.IsValid()) {
+ logger->debug("resource protocol info '{}'", (const char*)(resource.m_ProtocolInfo.ToString()));
+
+ if (resource.m_ProtocolInfo.GetContentType().Compare("application/octet-stream") != 0) {
+ item.SetMimeType((const char*)resource.m_ProtocolInfo.GetContentType());
+ }
+
+ // if this is an image fill the thumb of the item
+ if (StringUtils::StartsWithNoCase(resource.m_ProtocolInfo.GetContentType(), "image"))
+ {
+ item.SetArt("thumb", std::string(resource.m_Uri));
+ }
+ } else {
+ logger->error("invalid protocol info '{}'", (const char*)(resource.m_ProtocolInfo.ToString()));
+ }
+
+ // look for subtitles
+ unsigned subIdx = 0;
+
+ for(unsigned r = 0; r < entry->m_Resources.GetItemCount(); r++)
+ {
+ const PLT_MediaItemResource& res = entry->m_Resources[r];
+ const PLT_ProtocolInfo& info = res.m_ProtocolInfo;
+
+ for (std::string_view type : SupportedSubFormats)
+ {
+ if (type == info.GetContentType().Split("/").GetLastItem()->GetChars())
+ {
+ ++subIdx;
+ logger->info("adding subtitle: #{}, type '{}', URI '{}'", subIdx, type,
+ res.m_Uri.GetChars());
+
+ std::string prop = StringUtils::Format("subtitle:{}", subIdx);
+ item.SetProperty(prop, (const char*)res.m_Uri);
+ }
+ }
+ }
+ return true;
+}
+
+std::shared_ptr<CFileItem> GetFileItem(const NPT_String& uri, const NPT_String& meta)
+{
+ PLT_MediaObjectListReference list;
+ PLT_MediaObject* object = NULL;
+ CFileItemPtr item;
+
+ if (NPT_SUCCEEDED(PLT_Didl::FromDidl(meta, list))) {
+ list->Get(0, object);
+ }
+
+ if (object) {
+ item = BuildObject(object);
+ }
+
+ if (item) {
+ item->SetPath((const char*)uri);
+ GetResource(object, *item);
+ } else {
+ item.reset(new CFileItem((const char*)uri, false));
+ }
+ return item;
+}
+
+} /* namespace UPNP */
+
diff --git a/xbmc/network/upnp/UPnPInternal.h b/xbmc/network/upnp/UPnPInternal.h
new file mode 100644
index 0000000..5723e22
--- /dev/null
+++ b/xbmc/network/upnp/UPnPInternal.h
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include <memory>
+#include <string>
+
+#include <Neptune/Source/Core/NptReferences.h>
+#include <Neptune/Source/Core/NptStrings.h>
+#include <Neptune/Source/Core/NptTypes.h>
+
+class CUPnPServer;
+class CFileItem;
+class CThumbLoader;
+class PLT_DeviceData;
+class PLT_HttpRequestContext;
+class PLT_MediaItemResource;
+class PLT_MediaObject;
+class NPT_String;
+namespace MUSIC_INFO {
+ class CMusicInfoTag;
+}
+class CVideoInfoTag;
+
+namespace UPNP
+{
+ enum UPnPService {
+ UPnPServiceNone = 0,
+ UPnPClient,
+ UPnPContentDirectory,
+ UPnPPlayer,
+ UPnPRenderer
+ };
+
+ class CResourceFinder {
+ public:
+ CResourceFinder(const char* protocol, const char* content = NULL);
+ bool operator()(const PLT_MediaItemResource& resource) const;
+ private:
+ NPT_String m_Protocol;
+ NPT_String m_Content;
+ };
+
+ enum EClientQuirks
+ {
+ ECLIENTQUIRKS_NONE = 0x0
+
+ /* Client requires folder's to be marked as storageFolders as vendor type (360)*/
+ , ECLIENTQUIRKS_ONLYSTORAGEFOLDER = 0x01
+
+ /* Client can't handle subtypes for videoItems (360) */
+ , ECLIENTQUIRKS_BASICVIDEOCLASS = 0x02
+
+ /* Client requires album to be set to [Unknown Series] to show title (WMP) */
+ , ECLIENTQUIRKS_UNKNOWNSERIES = 0x04
+ };
+
+ EClientQuirks GetClientQuirks(const PLT_HttpRequestContext* context);
+
+ enum EMediaControllerQuirks
+ {
+ EMEDIACONTROLLERQUIRKS_NONE = 0x00
+
+ /* Media Controller expects MIME type video/x-mkv instead of video/x-matroska (Samsung) */
+ , EMEDIACONTROLLERQUIRKS_X_MKV = 0x01
+ };
+
+ EMediaControllerQuirks GetMediaControllerQuirks(const PLT_DeviceData *device);
+
+ const char* GetMimeTypeFromExtension(const char* extension, const PLT_HttpRequestContext* context = NULL);
+ NPT_String GetMimeType(const CFileItem& item, const PLT_HttpRequestContext* context = NULL);
+ NPT_String GetMimeType(const char* filename, const PLT_HttpRequestContext* context = NULL);
+ const NPT_String GetProtocolInfo(const CFileItem& item, const char* protocol, const PLT_HttpRequestContext* context = NULL);
+
+
+ const std::string& CorrectAllItemsSortHack(const std::string &item);
+
+ NPT_Result PopulateTagFromObject(MUSIC_INFO::CMusicInfoTag& tag,
+ PLT_MediaObject& object,
+ PLT_MediaItemResource* resource = NULL,
+ UPnPService service = UPnPServiceNone);
+
+ NPT_Result PopulateTagFromObject(CVideoInfoTag& tag,
+ PLT_MediaObject& object,
+ PLT_MediaItemResource* resource = NULL,
+ UPnPService service = UPnPServiceNone);
+
+ NPT_Result PopulateObjectFromTag(MUSIC_INFO::CMusicInfoTag& tag,
+ PLT_MediaObject& object,
+ NPT_String* file_path,
+ PLT_MediaItemResource* resource,
+ EClientQuirks quirks,
+ UPnPService service = UPnPServiceNone);
+
+ NPT_Result PopulateObjectFromTag(CVideoInfoTag& tag,
+ PLT_MediaObject& object,
+ NPT_String* file_path,
+ PLT_MediaItemResource* resource,
+ EClientQuirks quirks,
+ UPnPService service = UPnPServiceNone);
+
+ PLT_MediaObject* BuildObject(CFileItem& item,
+ NPT_String& file_path,
+ bool with_count,
+ NPT_Reference<CThumbLoader>& thumb_loader,
+ const PLT_HttpRequestContext* context = NULL,
+ CUPnPServer* upnp_server = NULL,
+ UPnPService upnp_service = UPnPServiceNone);
+
+ std::shared_ptr<CFileItem> BuildObject(PLT_MediaObject* entry,
+ UPnPService upnp_service = UPnPServiceNone);
+
+ bool GetResource(const PLT_MediaObject* entry, CFileItem& item);
+ std::shared_ptr<CFileItem> GetFileItem(const NPT_String& uri, const NPT_String& meta);
+}
+
diff --git a/xbmc/network/upnp/UPnPPlayer.cpp b/xbmc/network/upnp/UPnPPlayer.cpp
new file mode 100644
index 0000000..0f0d52a
--- /dev/null
+++ b/xbmc/network/upnp/UPnPPlayer.cpp
@@ -0,0 +1,625 @@
+/*
+ * Copyright (c) 2006 elupus (Joakim Plate)
+ * Copyright (C) 2006-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+#include "UPnPPlayer.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "ThumbLoader.h"
+#include "UPnP.h"
+#include "UPnPInternal.h"
+#include "application/Application.h"
+#include "cores/DataCacheCore.h"
+#include "dialogs/GUIDialogBusy.h"
+#include "input/actions/Action.h"
+#include "input/actions/ActionIDs.h"
+#include "messaging/ApplicationMessenger.h"
+#include "messaging/helpers/DialogHelper.h"
+#include "music/MusicThumbLoader.h"
+#include "threads/Event.h"
+#include "utils/StringUtils.h"
+#include "utils/TimeUtils.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+#include "video/VideoThumbLoader.h"
+#include "windowing/WinSystem.h"
+
+#include <mutex>
+
+#include <Platinum/Source/Devices/MediaRenderer/PltMediaController.h>
+#include <Platinum/Source/Devices/MediaServer/PltDidl.h>
+#include <Platinum/Source/Platinum/Platinum.h>
+
+using namespace KODI::MESSAGING;
+
+using KODI::MESSAGING::HELPERS::DialogResponse;
+using namespace std::chrono_literals;
+
+NPT_SET_LOCAL_LOGGER("xbmc.upnp.player")
+
+namespace UPNP
+{
+
+class CUPnPPlayerController : public PLT_MediaControllerDelegate
+{
+public:
+ CUPnPPlayerController(PLT_MediaController* control,
+ PLT_DeviceDataReference& device,
+ IPlayerCallback& callback)
+ : m_control(control),
+ m_transport(NULL),
+ m_device(device),
+ m_instance(0),
+ m_callback(callback),
+ m_postime(0),
+ m_logger(CServiceBroker::GetLogging().GetLogger("CUPnPPlayerController"))
+ {
+ m_posinfo = {};
+ m_device->FindServiceByType("urn:schemas-upnp-org:service:AVTransport:1", m_transport);
+ }
+
+ void OnSetAVTransportURIResult(NPT_Result res, PLT_DeviceDataReference& device, void* userdata) override
+ {
+ if(NPT_FAILED(res))
+ m_logger->error("OnSetAVTransportURIResult failed");
+ m_resstatus = res;
+ m_resevent.Set();
+ }
+
+ void OnPlayResult(NPT_Result res, PLT_DeviceDataReference& device, void* userdata) override
+ {
+ if(NPT_FAILED(res))
+ m_logger->error("OnPlayResult failed");
+ m_resstatus = res;
+ m_resevent.Set();
+ }
+
+ void OnStopResult(NPT_Result res, PLT_DeviceDataReference& device, void* userdata) override
+ {
+ if(NPT_FAILED(res))
+ m_logger->error("OnStopResult failed");
+ m_resstatus = res;
+ m_resevent.Set();
+ }
+
+ void OnGetMediaInfoResult(NPT_Result res, PLT_DeviceDataReference& device, PLT_MediaInfo* info, void* userdata) override
+ {
+ if(NPT_FAILED(res) || info == NULL)
+ m_logger->error("OnGetMediaInfoResult failed");
+ }
+
+ void OnGetTransportInfoResult(NPT_Result res, PLT_DeviceDataReference& device, PLT_TransportInfo* info, void* userdata) override
+ {
+ std::unique_lock<CCriticalSection> lock(m_section);
+
+ if(NPT_FAILED(res))
+ {
+ m_logger->error("OnGetTransportInfoResult failed");
+ m_trainfo.cur_speed = "0";
+ m_trainfo.cur_transport_state = "STOPPED";
+ m_trainfo.cur_transport_status = "ERROR_OCCURED";
+ }
+ else
+ m_trainfo = *info;
+ m_traevnt.Set();
+ }
+
+ void UpdatePositionInfo()
+ {
+ if(m_postime == 0
+ || m_postime > CTimeUtils::GetFrameTime())
+ return;
+
+ m_control->GetTransportInfo(m_device, m_instance, this);
+ m_control->GetPositionInfo(m_device, m_instance, this);
+ m_postime = 0;
+ }
+
+ void OnGetPositionInfoResult(NPT_Result res, PLT_DeviceDataReference& device, PLT_PositionInfo* info, void* userdata) override
+ {
+ std::unique_lock<CCriticalSection> lock(m_section);
+
+ if(NPT_FAILED(res) || info == NULL)
+ {
+ m_logger->error("OnGetMediaInfoResult failed");
+ m_posinfo = PLT_PositionInfo();
+ }
+ else
+ m_posinfo = *info;
+ m_postime = CTimeUtils::GetFrameTime() + 500;
+ m_posevnt.Set();
+ }
+
+
+ ~CUPnPPlayerController() override = default;
+
+ PLT_MediaController* m_control;
+ PLT_Service * m_transport;
+ PLT_DeviceDataReference m_device;
+ NPT_UInt32 m_instance;
+ IPlayerCallback& m_callback;
+
+ NPT_Result m_resstatus;
+ CEvent m_resevent;
+
+ CCriticalSection m_section;
+ unsigned int m_postime;
+
+ CEvent m_posevnt;
+ PLT_PositionInfo m_posinfo;
+
+ CEvent m_traevnt;
+ PLT_TransportInfo m_trainfo;
+
+ Logger m_logger;
+};
+
+CUPnPPlayer::CUPnPPlayer(IPlayerCallback& callback, const char* uuid)
+ : IPlayer(callback),
+ m_control(NULL),
+ m_delegate(NULL),
+ m_started(false),
+ m_stopremote(false),
+ m_logger(CServiceBroker::GetLogging().GetLogger(StringUtils::Format("CUPnPPlayer[{}]", uuid)))
+{
+ m_control = CUPnP::GetInstance()->m_MediaController;
+
+ PLT_DeviceDataReference device;
+ if(NPT_SUCCEEDED(m_control->FindRenderer(uuid, device)))
+ {
+ m_delegate = new CUPnPPlayerController(m_control, device, callback);
+ CUPnP::RegisterUserdata(m_delegate);
+ }
+ else
+ m_logger->error("couldn't find device as {}", uuid);
+
+ CServiceBroker::GetWinSystem()->RegisterRenderLoop(this);
+}
+
+CUPnPPlayer::~CUPnPPlayer()
+{
+ CServiceBroker::GetWinSystem()->UnregisterRenderLoop(this);
+ CloseFile();
+ CUPnP::UnregisterUserdata(m_delegate);
+ delete m_delegate;
+}
+
+static NPT_Result WaitOnEvent(CEvent& event, XbmcThreads::EndTime<>& timeout)
+{
+ if (event.Wait(0ms))
+ return NPT_SUCCESS;
+
+ if (!CGUIDialogBusy::WaitOnEvent(event))
+ return NPT_FAILURE;
+
+ return NPT_SUCCESS;
+}
+
+int CUPnPPlayer::PlayFile(const CFileItem& file,
+ const CPlayerOptions& options,
+ XbmcThreads::EndTime<>& timeout)
+{
+ CFileItem item(file);
+ NPT_Reference<CThumbLoader> thumb_loader;
+ NPT_Reference<PLT_MediaObject> obj;
+ NPT_String path(file.GetPath().c_str());
+ NPT_String tmp, resource;
+ EMediaControllerQuirks quirks = EMEDIACONTROLLERQUIRKS_NONE;
+
+ NPT_CHECK_POINTER_LABEL_SEVERE(m_delegate, failed);
+
+ if (file.IsVideoDb())
+ thumb_loader = NPT_Reference<CThumbLoader>(new CVideoThumbLoader());
+ else if (item.IsMusicDb())
+ thumb_loader = NPT_Reference<CThumbLoader>(new CMusicThumbLoader());
+
+ obj = BuildObject(item, path, false, thumb_loader, NULL, CUPnP::GetServer(), UPnPPlayer);
+ if(obj.IsNull()) goto failed;
+
+ NPT_CHECK_LABEL_SEVERE(PLT_Didl::ToDidl(*obj, "", tmp), failed_todidl);
+ tmp.Insert(didl_header, 0);
+ tmp.Append(didl_footer);
+
+ quirks = GetMediaControllerQuirks(m_delegate->m_device.AsPointer());
+ if (quirks & EMEDIACONTROLLERQUIRKS_X_MKV)
+ {
+ for (NPT_Cardinal i=0; i< obj->m_Resources.GetItemCount(); i++) {
+ if (obj->m_Resources[i].m_ProtocolInfo.GetContentType().Compare("video/x-matroska") == 0) {
+ m_logger->debug("PlayFile({}): applying video/x-mkv quirk", file.GetPath());
+ NPT_String protocolInfo = obj->m_Resources[i].m_ProtocolInfo.ToString();
+ protocolInfo.Replace(":video/x-matroska:", ":video/x-mkv:");
+ obj->m_Resources[i].m_ProtocolInfo = PLT_ProtocolInfo(protocolInfo);
+ }
+ }
+ }
+
+ /* The resource uri's are stored in the Didl. We must choose the best resource
+ * for the playback device */
+ NPT_Cardinal res_index;
+ NPT_CHECK_LABEL_SEVERE(m_control->FindBestResource(m_delegate->m_device, *obj, res_index), failed_findbestresource);
+
+ // get the transport info to evaluate the TransportState to be able to
+ // determine whether we first need to call Stop()
+ timeout.Set(timeout.GetInitialTimeoutValue());
+ NPT_CHECK_LABEL_SEVERE(m_control->GetTransportInfo(m_delegate->m_device
+ , m_delegate->m_instance
+ , m_delegate), failed_gettransportinfo);
+ NPT_CHECK_LABEL_SEVERE(WaitOnEvent(m_delegate->m_traevnt, timeout), failed_gettransportinfo);
+
+ if (m_delegate->m_trainfo.cur_transport_state != "NO_MEDIA_PRESENT" &&
+ m_delegate->m_trainfo.cur_transport_state != "STOPPED")
+ {
+ timeout.Set(timeout.GetInitialTimeoutValue());
+ NPT_CHECK_LABEL_SEVERE(m_control->Stop(m_delegate->m_device
+ , m_delegate->m_instance
+ , m_delegate), failed_stop);
+ NPT_CHECK_LABEL_SEVERE(WaitOnEvent(m_delegate->m_resevent, timeout), failed_stop);
+ NPT_CHECK_LABEL_SEVERE(m_delegate->m_resstatus, failed_stop);
+ }
+
+
+ timeout.Set(timeout.GetInitialTimeoutValue());
+ NPT_CHECK_LABEL_SEVERE(m_control->SetAVTransportURI(m_delegate->m_device
+ , m_delegate->m_instance
+ , obj->m_Resources[res_index].m_Uri
+ , (const char*)tmp
+ , m_delegate), failed_setavtransporturi);
+ NPT_CHECK_LABEL_SEVERE(WaitOnEvent(m_delegate->m_resevent, timeout), failed_setavtransporturi);
+ NPT_CHECK_LABEL_SEVERE(m_delegate->m_resstatus, failed_setavtransporturi);
+
+ timeout.Set(timeout.GetInitialTimeoutValue());
+ NPT_CHECK_LABEL_SEVERE(m_control->Play(m_delegate->m_device
+ , m_delegate->m_instance
+ , "1"
+ , m_delegate), failed_play);
+ NPT_CHECK_LABEL_SEVERE(WaitOnEvent(m_delegate->m_resevent, timeout), failed_play);
+ NPT_CHECK_LABEL_SEVERE(m_delegate->m_resstatus, failed_play);
+
+
+ /* wait for PLAYING state */
+ timeout.Set(timeout.GetInitialTimeoutValue());
+ do {
+ NPT_CHECK_LABEL_SEVERE(m_control->GetTransportInfo(m_delegate->m_device
+ , m_delegate->m_instance
+ , m_delegate), failed_waitplaying);
+
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_delegate->m_section);
+ if(m_delegate->m_trainfo.cur_transport_state == "PLAYING"
+ || m_delegate->m_trainfo.cur_transport_state == "PAUSED_PLAYBACK")
+ break;
+
+ if(m_delegate->m_trainfo.cur_transport_state == "STOPPED"
+ && m_delegate->m_trainfo.cur_transport_status != "OK")
+ {
+ m_logger->error("OpenFile({}): remote player signalled error", file.GetPath());
+ return NPT_FAILURE;
+ }
+ }
+
+ NPT_CHECK_LABEL_SEVERE(WaitOnEvent(m_delegate->m_traevnt, timeout), failed_waitplaying);
+
+ } while(!timeout.IsTimePast());
+
+ if(options.starttime > 0)
+ {
+ /* many upnp units won't load file properly until after play (including xbmc) */
+ NPT_CHECK_LABEL(m_control->Seek(m_delegate->m_device
+ , m_delegate->m_instance
+ , "REL_TIME"
+ , PLT_Didl::FormatTimeStamp((NPT_UInt32)options.starttime)
+ , m_delegate), failed_seek);
+ }
+
+ return NPT_SUCCESS;
+failed_todidl:
+ m_logger->error("PlayFile({}) failed to serialize item into DIDL-Lite", file.GetPath());
+ return NPT_FAILURE;
+failed_findbestresource:
+ m_logger->error("PlayFile({}) failed to find a matching resource", file.GetPath());
+ return NPT_FAILURE;
+failed_gettransportinfo:
+ m_logger->error("PlayFile({}): call to GetTransportInfo failed", file.GetPath());
+ return NPT_FAILURE;
+failed_stop:
+ m_logger->error("PlayFile({}) failed to stop current playback", file.GetPath());
+ return NPT_FAILURE;
+failed_setavtransporturi:
+ m_logger->error("PlayFile({}) failed to set the playback URI", file.GetPath());
+ return NPT_FAILURE;
+failed_play:
+ m_logger->error("PlayFile({}) failed to start playback", file.GetPath());
+ return NPT_FAILURE;
+failed_waitplaying:
+ m_logger->error("PlayFile({}) failed to wait for PLAYING state", file.GetPath());
+ return NPT_FAILURE;
+failed_seek:
+ m_logger->error("PlayFile({}) failed to seek to start offset", file.GetPath());
+ return NPT_FAILURE;
+failed:
+ m_logger->error("PlayFile({}) failed", file.GetPath());
+ return NPT_FAILURE;
+}
+
+bool CUPnPPlayer::OpenFile(const CFileItem& file, const CPlayerOptions& options)
+{
+ XbmcThreads::EndTime<> timeout(10s);
+
+ /* if no path we want to attach to a already playing player */
+ if(file.GetPath() == "")
+ {
+ NPT_CHECK_LABEL_SEVERE(m_control->GetTransportInfo(m_delegate->m_device
+ , m_delegate->m_instance
+ , m_delegate), failed);
+
+ NPT_CHECK_LABEL_SEVERE(WaitOnEvent(m_delegate->m_traevnt, timeout), failed);
+
+ /* make sure the attached player is actually playing */
+ {
+ std::unique_lock<CCriticalSection> lock(m_delegate->m_section);
+ if(m_delegate->m_trainfo.cur_transport_state != "PLAYING"
+ && m_delegate->m_trainfo.cur_transport_state != "PAUSED_PLAYBACK")
+ goto failed;
+ }
+ }
+ else
+ NPT_CHECK_LABEL_SEVERE(PlayFile(file, options, timeout), failed);
+
+ m_stopremote = true;
+ m_started = true;
+ m_callback.OnPlayBackStarted(file);
+ m_callback.OnAVStarted(file);
+ NPT_CHECK_LABEL_SEVERE(m_control->GetPositionInfo(m_delegate->m_device
+ , m_delegate->m_instance
+ , m_delegate), failed);
+ NPT_CHECK_LABEL_SEVERE(m_control->GetMediaInfo(m_delegate->m_device
+ , m_delegate->m_instance
+ , m_delegate), failed);
+
+ m_updateTimer.Set(0ms);
+
+ return true;
+failed:
+ m_logger->error("OpenFile({}) failed to open file", file.GetPath());
+ return false;
+}
+
+bool CUPnPPlayer::QueueNextFile(const CFileItem& file)
+{
+ CFileItem item(file);
+ NPT_Reference<CThumbLoader> thumb_loader;
+ NPT_Reference<PLT_MediaObject> obj;
+ NPT_String path(file.GetPath().c_str());
+ NPT_String tmp;
+
+ if (file.IsVideoDb())
+ thumb_loader = NPT_Reference<CThumbLoader>(new CVideoThumbLoader());
+ else if (item.IsMusicDb())
+ thumb_loader = NPT_Reference<CThumbLoader>(new CMusicThumbLoader());
+
+
+ obj = BuildObject(item, path, false, thumb_loader, NULL, CUPnP::GetServer(), UPnPPlayer);
+ if(!obj.IsNull())
+ {
+ NPT_CHECK_LABEL_SEVERE(PLT_Didl::ToDidl(*obj, "", tmp), failed);
+ tmp.Insert(didl_header, 0);
+ tmp.Append(didl_footer);
+ }
+
+ NPT_CHECK_LABEL_WARNING(m_control->SetNextAVTransportURI(m_delegate->m_device
+ , m_delegate->m_instance
+ , file.GetPath().c_str()
+ , (const char*)tmp
+ , m_delegate), failed);
+ if (!m_delegate->m_resevent.Wait(10000ms))
+ goto failed;
+ NPT_CHECK_LABEL_WARNING(m_delegate->m_resstatus, failed);
+ return true;
+
+failed:
+ m_logger->error("QueueNextFile({}) failed to queue file", file.GetPath());
+ return false;
+}
+
+bool CUPnPPlayer::CloseFile(bool reopen)
+{
+ NPT_CHECK_POINTER_LABEL_SEVERE(m_delegate, failed);
+ if(m_stopremote)
+ {
+ NPT_CHECK_LABEL(m_control->Stop(m_delegate->m_device
+ , m_delegate->m_instance
+ , m_delegate), failed);
+ if (!m_delegate->m_resevent.Wait(10000ms))
+ goto failed;
+ NPT_CHECK_LABEL(m_delegate->m_resstatus, failed);
+ }
+
+ if(m_started)
+ {
+ m_started = false;
+ m_callback.OnPlayBackStopped();
+ }
+
+ return true;
+failed:
+ m_logger->error("CloseFile - unable to stop playback");
+ return false;
+}
+
+void CUPnPPlayer::Pause()
+{
+ if(IsPaused())
+ {
+ NPT_CHECK_LABEL(m_control->Play(m_delegate->m_device
+ , m_delegate->m_instance
+ , "1"
+ , m_delegate), failed);
+ CDataCacheCore::GetInstance().SetSpeed(1.0, 1.0);
+ }
+ else
+ {
+ NPT_CHECK_LABEL(m_control->Pause(m_delegate->m_device
+ , m_delegate->m_instance
+ , m_delegate), failed);
+ CDataCacheCore::GetInstance().SetSpeed(1.0, 0.0);
+ }
+
+ return;
+failed:
+ m_logger->error("CloseFile - unable to pause/unpause playback");
+}
+
+void CUPnPPlayer::SeekTime(int64_t ms)
+{
+ NPT_CHECK_LABEL(m_control->Seek(m_delegate->m_device
+ , m_delegate->m_instance
+ , "REL_TIME", PLT_Didl::FormatTimeStamp((NPT_UInt32)(ms / 1000))
+ , m_delegate), failed);
+
+ CDataCacheCore::GetInstance().SeekFinished(0);
+ return;
+failed:
+ m_logger->error("SeekTime - unable to seek playback");
+}
+
+float CUPnPPlayer::GetPercentage()
+{
+ int64_t tot = GetTotalTime();
+ if(tot)
+ return 100.0f * GetTime() / tot;
+ else
+ return 0.0f;
+}
+
+void CUPnPPlayer::SeekPercentage(float percent)
+{
+ int64_t tot = GetTotalTime();
+ if (tot)
+ SeekTime((int64_t)(tot * percent / 100));
+}
+
+void CUPnPPlayer::Seek(bool bPlus, bool bLargeStep, bool bChapterOverride)
+{
+}
+
+void CUPnPPlayer::DoAudioWork()
+{
+ NPT_String data;
+ NPT_CHECK_POINTER_LABEL_SEVERE(m_delegate, failed);
+ m_delegate->UpdatePositionInfo();
+
+ if(m_started) {
+ NPT_String uri, meta;
+ NPT_CHECK_LABEL(m_delegate->m_transport->GetStateVariableValue("CurrentTrackURI", uri), failed);
+ NPT_CHECK_LABEL(m_delegate->m_transport->GetStateVariableValue("CurrentTrackMetadata", meta), failed);
+
+ if(m_current_uri != (const char*)uri
+ || m_current_meta != (const char*)meta) {
+ m_current_uri = (const char*)uri;
+ m_current_meta = (const char*)meta;
+ CFileItemPtr item = GetFileItem(uri, meta);
+ g_application.CurrentFileItem() = *item;
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_UPDATE_CURRENT_ITEM, 0, -1,
+ static_cast<void*>(new CFileItem(*item)));
+ }
+
+ NPT_CHECK_LABEL(m_delegate->m_transport->GetStateVariableValue("TransportState", data), failed);
+ if(data == "STOPPED")
+ {
+ m_started = false;
+ m_callback.OnPlayBackEnded();
+ }
+ }
+ return;
+failed:
+ return;
+}
+
+bool CUPnPPlayer::IsPlaying() const
+{
+ NPT_String data;
+ NPT_CHECK_POINTER_LABEL_SEVERE(m_delegate, failed);
+ NPT_CHECK_LABEL(m_delegate->m_transport->GetStateVariableValue("TransportState", data), failed);
+ return data != "STOPPED";
+failed:
+ return false;
+}
+
+bool CUPnPPlayer::IsPaused() const
+{
+ NPT_String data;
+ NPT_CHECK_POINTER_LABEL_SEVERE(m_delegate, failed);
+ NPT_CHECK_LABEL(m_delegate->m_transport->GetStateVariableValue("TransportState", data), failed);
+ return data == "PAUSED_PLAYBACK";
+failed:
+ return false;
+}
+
+void CUPnPPlayer::SetVolume(float volume)
+{
+ NPT_CHECK_POINTER_LABEL_SEVERE(m_delegate, failed);
+ NPT_CHECK_LABEL(m_control->SetVolume(m_delegate->m_device
+ , m_delegate->m_instance
+ , "Master", (int)(volume * 100)
+ , m_delegate), failed);
+ return;
+failed:
+ m_logger->error("- unable to set volume");
+}
+
+int64_t CUPnPPlayer::GetTime()
+{
+ NPT_CHECK_POINTER_LABEL_SEVERE(m_delegate, failed);
+ return m_delegate->m_posinfo.rel_time.ToMillis();
+failed:
+ return 0;
+}
+
+int64_t CUPnPPlayer::GetTotalTime()
+{
+ NPT_CHECK_POINTER_LABEL_SEVERE(m_delegate, failed);
+ return m_delegate->m_posinfo.track_duration.ToMillis();
+failed:
+ return 0;
+};
+
+bool CUPnPPlayer::OnAction(const CAction &action)
+{
+ switch (action.GetID())
+ {
+ case ACTION_STOP:
+ if(IsPlaying())
+ {
+ //stop on remote system
+ m_stopremote = HELPERS::ShowYesNoDialogText(CVariant{37022}, CVariant{37023}) ==
+ DialogResponse::CHOICE_YES;
+
+ return false; /* let normal code handle the action */
+ }
+ [[fallthrough]];
+ default:
+ return false;
+ }
+}
+
+void CUPnPPlayer::SetSpeed(float speed)
+{
+
+}
+
+void CUPnPPlayer::FrameMove()
+{
+ if (m_updateTimer.IsTimePast())
+ {
+ CDataCacheCore::GetInstance().SetPlayTimes(0, GetTime(), 0, GetTotalTime());
+ m_updateTimer.Set(500ms);
+ }
+}
+
+} /* namespace UPNP */
diff --git a/xbmc/network/upnp/UPnPPlayer.h b/xbmc/network/upnp/UPnPPlayer.h
new file mode 100644
index 0000000..aafd8c4
--- /dev/null
+++ b/xbmc/network/upnp/UPnPPlayer.h
@@ -0,0 +1,77 @@
+/*
+ * Copyright (c) 2006 elupus (Joakim Plate)
+ * Copyright (C) 2006-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "cores/IPlayer.h"
+#include "guilib/DispResource.h"
+#include "threads/SystemClock.h"
+#include "utils/logtypes.h"
+
+#include <string>
+
+class PLT_MediaController;
+
+namespace UPNP
+{
+
+class CUPnPPlayerController;
+
+class CUPnPPlayer
+ : public IPlayer, public IRenderLoop
+{
+public:
+ CUPnPPlayer(IPlayerCallback& callback, const char* uuid);
+ ~CUPnPPlayer() override;
+
+ bool OpenFile(const CFileItem& file, const CPlayerOptions& options) override;
+ bool QueueNextFile(const CFileItem &file) override;
+ bool CloseFile(bool reopen = false) override;
+ bool IsPlaying() const override;
+ void Pause() override;
+ bool HasVideo() const override { return false; }
+ bool HasAudio() const override { return false; }
+ void Seek(bool bPlus, bool bLargeStep, bool bChapterOverride) override;
+ void SeekPercentage(float fPercent = 0) override;
+ void SetVolume(float volume) override;
+
+ int SeekChapter(int iChapter) override { return -1; }
+
+ void SeekTime(int64_t iTime = 0) override;
+ void SetSpeed(float speed = 0) override;
+
+ bool IsCaching() const override { return false; }
+ int GetCacheLevel() const override { return -1; }
+ void DoAudioWork() override;
+ bool OnAction(const CAction &action) override;
+
+ void FrameMove() override;
+
+ int PlayFile(const CFileItem& file,
+ const CPlayerOptions& options,
+ XbmcThreads::EndTime<>& timeout);
+
+private:
+ bool IsPaused() const;
+ int64_t GetTime();
+ int64_t GetTotalTime();
+ float GetPercentage();
+
+ PLT_MediaController* m_control;
+ CUPnPPlayerController* m_delegate;
+ std::string m_current_uri;
+ std::string m_current_meta;
+ bool m_started;
+ bool m_stopremote;
+ XbmcThreads::EndTime<> m_updateTimer;
+
+ Logger m_logger;
+};
+
+} /* namespace UPNP */
diff --git a/xbmc/network/upnp/UPnPRenderer.cpp b/xbmc/network/upnp/UPnPRenderer.cpp
new file mode 100644
index 0000000..d296471
--- /dev/null
+++ b/xbmc/network/upnp/UPnPRenderer.cpp
@@ -0,0 +1,758 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+#include "UPnPRenderer.h"
+
+#include "FileItem.h"
+#include "GUIInfoManager.h"
+#include "GUIUserMessages.h"
+#include "PlayListPlayer.h"
+#include "ServiceBroker.h"
+#include "TextureDatabase.h"
+#include "ThumbLoader.h"
+#include "UPnP.h"
+#include "UPnPInternal.h"
+#include "URL.h"
+#include "application/Application.h"
+#include "application/ApplicationComponents.h"
+#include "application/ApplicationPlayer.h"
+#include "application/ApplicationVolumeHandling.h"
+#include "filesystem/SpecialProtocol.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/guiinfo/GUIInfoLabels.h"
+#include "input/actions/Action.h"
+#include "input/actions/ActionIDs.h"
+#include "interfaces/AnnouncementManager.h"
+#include "messaging/ApplicationMessenger.h"
+#include "network/Network.h"
+#include "pictures/GUIWindowSlideShow.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/Variant.h"
+#include "xbmc/interfaces/AnnouncementManager.h"
+
+#include <inttypes.h>
+#include <mutex>
+
+#include <Platinum/Source/Platinum/Platinum.h>
+
+NPT_SET_LOCAL_LOGGER("xbmc.upnp.renderer")
+
+namespace UPNP
+{
+
+/*----------------------------------------------------------------------
+| CUPnPRenderer::CUPnPRenderer
++---------------------------------------------------------------------*/
+CUPnPRenderer::CUPnPRenderer(const char* friendly_name, bool show_ip /*= false*/,
+ const char* uuid /*= NULL*/, unsigned int port /*= 0*/)
+ : PLT_MediaRenderer(friendly_name, show_ip, uuid, port)
+{
+ CServiceBroker::GetAnnouncementManager()->AddAnnouncer(this);
+}
+
+/*----------------------------------------------------------------------
+| CUPnPRenderer::~CUPnPRenderer
++---------------------------------------------------------------------*/
+CUPnPRenderer::~CUPnPRenderer()
+{
+ CServiceBroker::GetAnnouncementManager()->RemoveAnnouncer(this);
+}
+
+/*----------------------------------------------------------------------
+| CUPnPRenderer::SetupServices
++---------------------------------------------------------------------*/
+NPT_Result
+CUPnPRenderer::SetupServices()
+{
+ NPT_CHECK(PLT_MediaRenderer::SetupServices());
+
+ // update what we can play
+ PLT_Service* service = NULL;
+ NPT_CHECK_FATAL(FindServiceByType("urn:schemas-upnp-org:service:ConnectionManager:1", service));
+ service->SetStateVariable("SinkProtocolInfo"
+ ,"http-get:*:*:*"
+ ",xbmc-get:*:*:*"
+ ",http-get:*:audio/mkv:*"
+ ",http-get:*:audio/mpegurl:*"
+ ",http-get:*:audio/mpeg:*"
+ ",http-get:*:audio/mpeg3:*"
+ ",http-get:*:audio/mp3:*"
+ ",http-get:*:audio/mp4:*"
+ ",http-get:*:audio/basic:*"
+ ",http-get:*:audio/midi:*"
+ ",http-get:*:audio/ulaw:*"
+ ",http-get:*:audio/ogg:*"
+ ",http-get:*:audio/DVI4:*"
+ ",http-get:*:audio/G722:*"
+ ",http-get:*:audio/G723:*"
+ ",http-get:*:audio/G726-16:*"
+ ",http-get:*:audio/G726-24:*"
+ ",http-get:*:audio/G726-32:*"
+ ",http-get:*:audio/G726-40:*"
+ ",http-get:*:audio/G728:*"
+ ",http-get:*:audio/G729:*"
+ ",http-get:*:audio/G729D:*"
+ ",http-get:*:audio/G729E:*"
+ ",http-get:*:audio/GSM:*"
+ ",http-get:*:audio/GSM-EFR:*"
+ ",http-get:*:audio/L8:*"
+ ",http-get:*:audio/L16:*"
+ ",http-get:*:audio/LPC:*"
+ ",http-get:*:audio/MPA:*"
+ ",http-get:*:audio/PCMA:*"
+ ",http-get:*:audio/PCMU:*"
+ ",http-get:*:audio/QCELP:*"
+ ",http-get:*:audio/RED:*"
+ ",http-get:*:audio/VDVI:*"
+ ",http-get:*:audio/ac3:*"
+ ",http-get:*:audio/webm:*"
+ ",http-get:*:audio/vorbis:*"
+ ",http-get:*:audio/speex:*"
+ ",http-get:*:audio/flac:*"
+ ",http-get:*:audio/x-flac:*"
+ ",http-get:*:audio/x-aiff:*"
+ ",http-get:*:audio/x-pn-realaudio:*"
+ ",http-get:*:audio/x-realaudio:*"
+ ",http-get:*:audio/x-wav:*"
+ ",http-get:*:audio/x-matroska:*"
+ ",http-get:*:audio/x-ms-wma:*"
+ ",http-get:*:audio/x-mpegurl:*"
+ ",http-get:*:application/x-shockwave-flash:*"
+ ",http-get:*:application/ogg:*"
+ ",http-get:*:application/sdp:*"
+ ",http-get:*:image/gif:*"
+ ",http-get:*:image/jpeg:*"
+ ",http-get:*:image/ief:*"
+ ",http-get:*:image/png:*"
+ ",http-get:*:image/tiff:*"
+ ",http-get:*:image/webp:*"
+ ",http-get:*:video/avi:*"
+ ",http-get:*:video/divx:*"
+ ",http-get:*:video/mpeg:*"
+ ",http-get:*:video/fli:*"
+ ",http-get:*:video/flv:*"
+ ",http-get:*:video/quicktime:*"
+ ",http-get:*:video/vnd.vivo:*"
+ ",http-get:*:video/vc1:*"
+ ",http-get:*:video/ogg:*"
+ ",http-get:*:video/mp4:*"
+ ",http-get:*:video/mkv:*"
+ ",http-get:*:video/BT656:*"
+ ",http-get:*:video/CelB:*"
+ ",http-get:*:video/JPEG:*"
+ ",http-get:*:video/H261:*"
+ ",http-get:*:video/H263:*"
+ ",http-get:*:video/H263-1998:*"
+ ",http-get:*:video/H263-2000:*"
+ ",http-get:*:video/MPV:*"
+ ",http-get:*:video/MP2T:*"
+ ",http-get:*:video/MP1S:*"
+ ",http-get:*:video/MP2P:*"
+ ",http-get:*:video/BMPEG:*"
+ ",http-get:*:video/webm:*"
+ ",http-get:*:video/xvid:*"
+ ",http-get:*:video/x-divx:*"
+ ",http-get:*:video/x-matroska:*"
+ ",http-get:*:video/x-mkv:*"
+ ",http-get:*:video/x-ms-wmv:*"
+ ",http-get:*:video/x-ms-avi:*"
+ ",http-get:*:video/x-flv:*"
+ ",http-get:*:video/x-fli:*"
+ ",http-get:*:video/x-ms-asf:*"
+ ",http-get:*:video/x-ms-asx:*"
+ ",http-get:*:video/x-ms-wmx:*"
+ ",http-get:*:video/x-ms-wvx:*"
+ ",http-get:*:video/x-msvideo:*"
+ ",http-get:*:video/x-xvid:*"
+ );
+
+ NPT_CHECK_FATAL(FindServiceByType("urn:schemas-upnp-org:service:AVTransport:1", service));
+ service->SetStateVariable("NextAVTransportURI", "");
+ service->SetStateVariable("NextAVTransportURIMetadata", "");
+
+ return NPT_SUCCESS;
+}
+
+/*----------------------------------------------------------------------
+| CUPnPRenderer::ProcessHttpRequest
++---------------------------------------------------------------------*/
+NPT_Result
+CUPnPRenderer::ProcessHttpGetRequest(NPT_HttpRequest& request,
+ const NPT_HttpRequestContext& context,
+ NPT_HttpResponse& response)
+{
+ // get the address of who sent us some data back
+ NPT_String ip_address = context.GetRemoteAddress().GetIpAddress().ToString();
+ NPT_String method = request.GetMethod();
+ NPT_String protocol = request.GetProtocol();
+ NPT_HttpUrl url = request.GetUrl();
+
+ if (url.GetPath() == "/thumb") {
+ NPT_HttpUrlQuery query(url.GetQuery());
+ NPT_String filepath = query.GetField("path");
+ if (!filepath.IsEmpty()) {
+ NPT_HttpEntity* entity = response.GetEntity();
+ if (entity == NULL) return NPT_ERROR_INVALID_STATE;
+
+ // check the method
+ if (request.GetMethod() != NPT_HTTP_METHOD_GET &&
+ request.GetMethod() != NPT_HTTP_METHOD_HEAD) {
+ response.SetStatus(405, "Method Not Allowed");
+ return NPT_SUCCESS;
+ }
+
+ // prevent hackers from accessing files outside of our root
+ if ((filepath.Find("/..") >= 0) || (filepath.Find("\\..") >=0)) {
+ return NPT_FAILURE;
+ }
+
+ // open the file
+ std::string path (CURL::Decode((const char*) filepath));
+ NPT_File file(path.c_str());
+ NPT_Result result = file.Open(NPT_FILE_OPEN_MODE_READ);
+ if (NPT_FAILED(result)) {
+ response.SetStatus(404, "Not Found");
+ return NPT_SUCCESS;
+ }
+ NPT_InputStreamReference stream;
+ file.GetInputStream(stream);
+ entity->SetContentType(GetMimeType(filepath));
+ entity->SetInputStream(stream, true);
+
+ return NPT_SUCCESS;
+ }
+ }
+
+ return PLT_MediaRenderer::ProcessHttpGetRequest(request, context, response);
+}
+
+/*----------------------------------------------------------------------
+| CUPnPRenderer::Announce
++---------------------------------------------------------------------*/
+void CUPnPRenderer::Announce(ANNOUNCEMENT::AnnouncementFlag flag,
+ const std::string& sender,
+ const std::string& message,
+ const CVariant& data)
+{
+ if (sender != ANNOUNCEMENT::CAnnouncementManager::ANNOUNCEMENT_SENDER)
+ return;
+
+ NPT_AutoLock lock(m_state);
+ PLT_Service *avt, *rct;
+
+ if (flag == ANNOUNCEMENT::Player)
+ {
+ if (NPT_FAILED(FindServiceByType("urn:schemas-upnp-org:service:AVTransport:1", avt)))
+ return;
+
+ if (message == "OnPlay" || message == "OnResume")
+ {
+ avt->SetStateVariable("AVTransportURI", g_application.CurrentFile().c_str());
+ avt->SetStateVariable("CurrentTrackURI", g_application.CurrentFile().c_str());
+
+ NPT_String meta;
+ if (NPT_SUCCEEDED(GetMetadata(meta)))
+ {
+ avt->SetStateVariable("CurrentTrackMetadata", meta);
+ avt->SetStateVariable("AVTransportURIMetaData", meta);
+ }
+
+ avt->SetStateVariable("TransportPlaySpeed",
+ NPT_String::FromInteger(data["player"]["speed"].asInteger()));
+ avt->SetStateVariable("TransportState", "PLAYING");
+
+ /* this could be a transition to next track, so clear next */
+ avt->SetStateVariable("NextAVTransportURI", "");
+ avt->SetStateVariable("NextAVTransportURIMetaData", "");
+ }
+ else if (message == "OnPause")
+ {
+ int64_t speed = data["player"]["speed"].asInteger();
+ avt->SetStateVariable("TransportPlaySpeed", NPT_String::FromInteger(speed != 0 ? speed : 1));
+ avt->SetStateVariable("TransportState", "PAUSED_PLAYBACK");
+ }
+ else if (message == "OnSpeedChanged")
+ {
+ avt->SetStateVariable("TransportPlaySpeed",
+ NPT_String::FromInteger(data["player"]["speed"].asInteger()));
+ }
+ }
+ else if (flag == ANNOUNCEMENT::Application && message == "OnVolumeChanged")
+ {
+ if (NPT_FAILED(FindServiceByType("urn:schemas-upnp-org:service:RenderingControl:1", rct)))
+ return;
+
+ std::string buffer;
+
+ buffer = std::to_string(data["volume"].asInteger());
+ rct->SetStateVariable("Volume", buffer.c_str());
+
+ buffer = std::to_string(256 * (data["volume"].asInteger() * 60 - 60) / 100);
+ rct->SetStateVariable("VolumeDb", buffer.c_str());
+
+ rct->SetStateVariable("Mute", data["muted"].asBoolean() ? "1" : "0");
+ }
+}
+
+/*----------------------------------------------------------------------
+| CUPnPRenderer::UpdateState
++---------------------------------------------------------------------*/
+void
+CUPnPRenderer::UpdateState()
+{
+ NPT_AutoLock lock(m_state);
+
+ PLT_Service *avt;
+
+ if (NPT_FAILED(FindServiceByType("urn:schemas-upnp-org:service:AVTransport:1", avt)))
+ return;
+
+ /* don't update state while transitioning */
+ NPT_String state;
+ avt->GetStateVariableValue("TransportState", state);
+ if(state == "TRANSITIONING")
+ return;
+
+ avt->SetStateVariable("TransportStatus", "OK");
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ if (appPlayer->IsPlaying() || appPlayer->IsPausedPlayback())
+ {
+ avt->SetStateVariable("NumberOfTracks", "1");
+ avt->SetStateVariable("CurrentTrack", "1");
+
+ // get elapsed time
+ std::string buffer = StringUtils::SecondsToTimeString(std::lrint(g_application.GetTime()),
+ TIME_FORMAT_HH_MM_SS);
+ avt->SetStateVariable("RelativeTimePosition", buffer.c_str());
+ avt->SetStateVariable("AbsoluteTimePosition", buffer.c_str());
+
+ // get duration
+ buffer = StringUtils::SecondsToTimeString(std::lrint(g_application.GetTotalTime()),
+ TIME_FORMAT_HH_MM_SS);
+ if (buffer.length() > 0)
+ {
+ avt->SetStateVariable("CurrentTrackDuration", buffer.c_str());
+ avt->SetStateVariable("CurrentMediaDuration", buffer.c_str());
+ }
+ else
+ {
+ avt->SetStateVariable("CurrentTrackDuration", "00:00:00");
+ avt->SetStateVariable("CurrentMediaDuration", "00:00:00");
+ }
+ }
+ else if (CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() == WINDOW_SLIDESHOW)
+ {
+ avt->SetStateVariable("TransportState", "PLAYING");
+
+ const std::string filePath = CServiceBroker::GetGUI()->GetInfoManager().GetLabel(
+ SLIDESHOW_FILE_PATH, INFO::DEFAULT_CONTEXT);
+ avt->SetStateVariable("AVTransportURI", filePath.c_str());
+ avt->SetStateVariable("CurrentTrackURI", filePath.c_str());
+ avt->SetStateVariable("TransportPlaySpeed", "1");
+
+ CGUIWindowSlideShow* slideshow =
+ CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIWindowSlideShow>(
+ WINDOW_SLIDESHOW);
+ if (slideshow)
+ {
+ std::string index;
+ index = std::to_string(slideshow->NumSlides());
+ avt->SetStateVariable("NumberOfTracks", index.c_str());
+ index = std::to_string(slideshow->CurrentSlide());
+ avt->SetStateVariable("CurrentTrack", index.c_str());
+ }
+
+ avt->SetStateVariable("CurrentTrackMetadata", "");
+ avt->SetStateVariable("AVTransportURIMetaData", "");
+ }
+ else
+ {
+ avt->SetStateVariable("TransportState", "STOPPED");
+ avt->SetStateVariable("TransportPlaySpeed", "1");
+ avt->SetStateVariable("NumberOfTracks", "0");
+ avt->SetStateVariable("CurrentTrack", "0");
+ avt->SetStateVariable("RelativeTimePosition", "00:00:00");
+ avt->SetStateVariable("AbsoluteTimePosition", "00:00:00");
+ avt->SetStateVariable("CurrentTrackDuration", "00:00:00");
+ avt->SetStateVariable("CurrentMediaDuration", "00:00:00");
+ avt->SetStateVariable("NextAVTransportURI", "");
+ avt->SetStateVariable("NextAVTransportURIMetaData", "");
+ }
+}
+
+/*----------------------------------------------------------------------
+| CUPnPRenderer::SetupIcons
++---------------------------------------------------------------------*/
+NPT_Result
+CUPnPRenderer::SetupIcons()
+{
+ NPT_String file_root = CSpecialProtocol::TranslatePath("special://xbmc/media/").c_str();
+ AddIcon(
+ PLT_DeviceIcon("image/png", 256, 256, 8, "/icon256x256.png"),
+ file_root);
+ AddIcon(
+ PLT_DeviceIcon("image/png", 120, 120, 8, "/icon120x120.png"),
+ file_root);
+ AddIcon(
+ PLT_DeviceIcon("image/png", 48, 48, 8, "/icon48x48.png"),
+ file_root);
+ AddIcon(
+ PLT_DeviceIcon("image/png", 32, 32, 8, "/icon32x32.png"),
+ file_root);
+ AddIcon(
+ PLT_DeviceIcon("image/png", 16, 16, 8, "/icon16x16.png"),
+ file_root);
+ return NPT_SUCCESS;
+}
+
+/*----------------------------------------------------------------------
+| CUPnPRenderer::GetMetadata
++---------------------------------------------------------------------*/
+NPT_Result
+CUPnPRenderer::GetMetadata(NPT_String& meta)
+{
+ NPT_Result res = NPT_FAILURE;
+ CFileItem item(g_application.CurrentFileItem());
+ NPT_String file_path, tmp;
+
+ // we pass an empty CThumbLoader reference, as it can't be used
+ // without CUPnPServer enabled
+ NPT_Reference<CThumbLoader> thumb_loader;
+ PLT_MediaObject* object = BuildObject(item, file_path, false, thumb_loader, NULL, NULL, UPnPRenderer);
+ if (object) {
+ // fetch the item's artwork
+ std::string thumb;
+ if (object->m_ObjectClass.type == "object.item.audioItem.musicTrack")
+ thumb = CServiceBroker::GetGUI()->GetInfoManager().GetImage(MUSICPLAYER_COVER, -1);
+ else
+ thumb = CServiceBroker::GetGUI()->GetInfoManager().GetImage(VIDEOPLAYER_COVER, -1);
+
+ thumb = CTextureUtils::GetWrappedImageURL(thumb);
+
+ NPT_String ip;
+ if (CServiceBroker::GetNetwork().GetFirstConnectedInterface()) {
+ ip = CServiceBroker::GetNetwork().GetFirstConnectedInterface()->GetCurrentIPAddress().c_str();
+ }
+ // build url, use the internal device http server to serv the image
+ NPT_HttpUrlQuery query;
+ query.AddField("path", thumb.c_str());
+ PLT_AlbumArtInfo art;
+ art.uri = NPT_HttpUrl(
+ ip,
+ m_URLDescription.GetPort(),
+ "/thumb",
+ query.ToString()).ToString();
+ // Set DLNA profileID by extension, defaulting to JPEG.
+ if (URIUtils::HasExtension(item.GetArt("thumb"), ".png")) {
+ art.dlna_profile = "PNG_TN";
+ } else {
+ art.dlna_profile = "JPEG_TN";
+ }
+ object->m_ExtraInfo.album_arts.Add(art);
+
+ res = PLT_Didl::ToDidl(*object, "*", tmp);
+ meta = didl_header + tmp + didl_footer;
+ delete object;
+ }
+ return res;
+}
+
+/*----------------------------------------------------------------------
+| CUPnPRenderer::OnNext
++---------------------------------------------------------------------*/
+NPT_Result
+CUPnPRenderer::OnNext(PLT_ActionReference& action)
+{
+ if (CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() == WINDOW_SLIDESHOW) {
+ CServiceBroker::GetAppMessenger()->SendMsg(
+ TMSG_GUI_ACTION, WINDOW_SLIDESHOW, -1,
+ static_cast<void*>(new CAction(ACTION_NEXT_PICTURE)));
+ } else {
+ CServiceBroker::GetAppMessenger()->SendMsg(TMSG_PLAYLISTPLAYER_NEXT);
+ }
+ return NPT_SUCCESS;
+}
+
+/*----------------------------------------------------------------------
+| CUPnPRenderer::OnPause
++---------------------------------------------------------------------*/
+NPT_Result
+CUPnPRenderer::OnPause(PLT_ActionReference& action)
+{
+ if (CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() == WINDOW_SLIDESHOW) {
+ CServiceBroker::GetAppMessenger()->SendMsg(
+ TMSG_GUI_ACTION, WINDOW_SLIDESHOW, -1,
+ static_cast<void*>(new CAction(ACTION_NEXT_PICTURE)));
+ }
+ else
+ {
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ if (!appPlayer->IsPausedPlayback())
+ CServiceBroker::GetAppMessenger()->SendMsg(TMSG_MEDIA_PAUSE);
+ }
+ return NPT_SUCCESS;
+}
+
+/*----------------------------------------------------------------------
+| CUPnPRenderer::OnPlay
++---------------------------------------------------------------------*/
+NPT_Result
+CUPnPRenderer::OnPlay(PLT_ActionReference& action)
+{
+ if (CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() == WINDOW_SLIDESHOW)
+ return NPT_SUCCESS;
+
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ if (appPlayer->IsPausedPlayback())
+ {
+ CServiceBroker::GetAppMessenger()->SendMsg(TMSG_MEDIA_PAUSE);
+ }
+ else if (appPlayer && !appPlayer->IsPlaying())
+ {
+ NPT_String uri, meta;
+ PLT_Service* service;
+ // look for value set previously by SetAVTransportURI
+ NPT_CHECK_SEVERE(FindServiceByType("urn:schemas-upnp-org:service:AVTransport:1", service));
+ NPT_CHECK_SEVERE(service->GetStateVariableValue("AVTransportURI", uri));
+ NPT_CHECK_SEVERE(service->GetStateVariableValue("AVTransportURIMetaData", meta));
+
+ // if not set, use the current file being played
+ PlayMedia(uri, meta);
+ }
+ return NPT_SUCCESS;
+}
+
+/*----------------------------------------------------------------------
+| CUPnPRenderer::OnPrevious
++---------------------------------------------------------------------*/
+NPT_Result
+CUPnPRenderer::OnPrevious(PLT_ActionReference& action)
+{
+ if (CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() == WINDOW_SLIDESHOW) {
+ CServiceBroker::GetAppMessenger()->SendMsg(
+ TMSG_GUI_ACTION, WINDOW_SLIDESHOW, -1,
+ static_cast<void*>(new CAction(ACTION_PREV_PICTURE)));
+ } else {
+ CServiceBroker::GetAppMessenger()->SendMsg(TMSG_PLAYLISTPLAYER_PREV);
+ }
+ return NPT_SUCCESS;
+}
+
+/*----------------------------------------------------------------------
+| CUPnPRenderer::OnStop
++---------------------------------------------------------------------*/
+NPT_Result
+CUPnPRenderer::OnStop(PLT_ActionReference& action)
+{
+ if (CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() == WINDOW_SLIDESHOW) {
+ CServiceBroker::GetAppMessenger()->SendMsg(
+ TMSG_GUI_ACTION, WINDOW_SLIDESHOW, -1,
+ static_cast<void*>(new CAction(ACTION_NEXT_PICTURE)));
+ } else {
+ CServiceBroker::GetAppMessenger()->SendMsg(TMSG_MEDIA_STOP);
+ }
+ return NPT_SUCCESS;
+}
+
+/*----------------------------------------------------------------------
+| CUPnPRenderer::OnSetAVTransportURI
++---------------------------------------------------------------------*/
+NPT_Result
+CUPnPRenderer::OnSetAVTransportURI(PLT_ActionReference& action)
+{
+ NPT_String uri, meta;
+ PLT_Service* service;
+ NPT_CHECK_SEVERE(FindServiceByType("urn:schemas-upnp-org:service:AVTransport:1", service));
+
+ NPT_CHECK_SEVERE(action->GetArgumentValue("CurrentURI", uri));
+ NPT_CHECK_SEVERE(action->GetArgumentValue("CurrentURIMetaData", meta));
+
+ // if not playing already, just keep around uri & metadata
+ // and wait for play command
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ if (!appPlayer->IsPlaying() &&
+ CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() != WINDOW_SLIDESHOW)
+ {
+ service->SetStateVariable("TransportState", "STOPPED");
+ service->SetStateVariable("TransportStatus", "OK");
+ service->SetStateVariable("TransportPlaySpeed", "1");
+ service->SetStateVariable("AVTransportURI", uri);
+ service->SetStateVariable("AVTransportURIMetaData", meta);
+ service->SetStateVariable("NextAVTransportURI", "");
+ service->SetStateVariable("NextAVTransportURIMetaData", "");
+
+ NPT_CHECK_SEVERE(action->SetArgumentsOutFromStateVariable());
+ return NPT_SUCCESS;
+ }
+
+ return PlayMedia(uri, meta, action.AsPointer());
+}
+
+/*----------------------------------------------------------------------
+ | CUPnPRenderer::OnSetAVTransportURI
+ +---------------------------------------------------------------------*/
+NPT_Result
+CUPnPRenderer::OnSetNextAVTransportURI(PLT_ActionReference& action)
+{
+ NPT_String uri, meta;
+ PLT_Service* service;
+ NPT_CHECK_SEVERE(FindServiceByType("urn:schemas-upnp-org:service:AVTransport:1", service));
+
+ NPT_CHECK_SEVERE(action->GetArgumentValue("NextURI", uri));
+ NPT_CHECK_SEVERE(action->GetArgumentValue("NextURIMetaData", meta));
+
+ CFileItemPtr item = GetFileItem(uri, meta);
+ if (!item) {
+ return NPT_FAILURE;
+ }
+
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ if (appPlayer->IsPlaying())
+ {
+
+ PLAYLIST::Id playlistId = PLAYLIST::TYPE_MUSIC;
+ if (item->IsVideo())
+ playlistId = PLAYLIST::TYPE_VIDEO;
+
+ {
+ std::unique_lock<CCriticalSection> lock(CServiceBroker::GetWinSystem()->GetGfxContext());
+ CServiceBroker::GetPlaylistPlayer().ClearPlaylist(playlistId);
+ CServiceBroker::GetPlaylistPlayer().Add(playlistId, item);
+
+ CServiceBroker::GetPlaylistPlayer().SetCurrentSong(-1);
+ CServiceBroker::GetPlaylistPlayer().SetCurrentPlaylist(playlistId);
+ }
+
+ CGUIMessage msg(GUI_MSG_PLAYLIST_CHANGED, 0, 0);
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg);
+
+
+ service->SetStateVariable("NextAVTransportURI", uri);
+ service->SetStateVariable("NextAVTransportURIMetaData", meta);
+
+ NPT_CHECK_SEVERE(action->SetArgumentsOutFromStateVariable());
+
+ return NPT_SUCCESS;
+ }
+ else if (CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() == WINDOW_SLIDESHOW)
+ {
+ return NPT_FAILURE;
+ }
+ else
+ {
+ return NPT_FAILURE;
+ }
+}
+
+/*----------------------------------------------------------------------
+| CUPnPRenderer::PlayMedia
++---------------------------------------------------------------------*/
+NPT_Result
+CUPnPRenderer::PlayMedia(const NPT_String& uri, const NPT_String& meta, PLT_Action* action)
+{
+ PLT_Service* service;
+ NPT_CHECK_SEVERE(FindServiceByType("urn:schemas-upnp-org:service:AVTransport:1", service));
+
+ { NPT_AutoLock lock(m_state);
+ service->SetStateVariable("TransportState", "TRANSITIONING");
+ service->SetStateVariable("TransportStatus", "OK");
+ }
+
+ CFileItemPtr item = GetFileItem(uri, meta);
+ if (!item) {
+ return NPT_FAILURE;
+ }
+
+ if (item->IsPicture()) {
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_PICTURE_SHOW, -1, -1, nullptr,
+ item->GetPath());
+ } else {
+ CFileItemList *l = new CFileItemList; //don't delete,
+ l->Add(std::make_shared<CFileItem>(*item));
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_MEDIA_PLAY, -1, -1, static_cast<void*>(l));
+ }
+
+ // just return success because the play actions are asynchronous
+ NPT_AutoLock lock(m_state);
+ service->SetStateVariable("TransportState", "PLAYING");
+ service->SetStateVariable("TransportStatus", "OK");
+ service->SetStateVariable("AVTransportURI", uri);
+ service->SetStateVariable("AVTransportURIMetaData", meta);
+
+ service->SetStateVariable("NextAVTransportURI", "");
+ service->SetStateVariable("NextAVTransportURIMetaData", "");
+
+ if (action) {
+ NPT_CHECK_SEVERE(action->SetArgumentsOutFromStateVariable());
+ }
+ return NPT_SUCCESS;
+}
+
+/*----------------------------------------------------------------------
+| CUPnPRenderer::OnSetVolume
++---------------------------------------------------------------------*/
+NPT_Result
+CUPnPRenderer::OnSetVolume(PLT_ActionReference& action)
+{
+ NPT_String volume;
+ NPT_CHECK_SEVERE(action->GetArgumentValue("DesiredVolume", volume));
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appVolume = components.GetComponent<CApplicationVolumeHandling>();
+ appVolume->SetVolume((float)strtod((const char*)volume, NULL));
+ return NPT_SUCCESS;
+}
+
+/*----------------------------------------------------------------------
+| CUPnPRenderer::OnSetMute
++---------------------------------------------------------------------*/
+NPT_Result
+CUPnPRenderer::OnSetMute(PLT_ActionReference& action)
+{
+ NPT_String mute;
+ NPT_CHECK_SEVERE(action->GetArgumentValue("DesiredMute",mute));
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appVolume = components.GetComponent<CApplicationVolumeHandling>();
+ if ((mute == "1") ^ appVolume->IsMuted())
+ appVolume->ToggleMute();
+ return NPT_SUCCESS;
+}
+
+/*----------------------------------------------------------------------
+| CUPnPRenderer::OnSeek
++---------------------------------------------------------------------*/
+NPT_Result
+CUPnPRenderer::OnSeek(PLT_ActionReference& action)
+{
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ if (!appPlayer->IsPlaying())
+ return NPT_ERROR_INVALID_STATE;
+
+ NPT_String unit, target;
+ NPT_CHECK_SEVERE(action->GetArgumentValue("Unit", unit));
+ NPT_CHECK_SEVERE(action->GetArgumentValue("Target", target));
+
+ if (!unit.Compare("REL_TIME"))
+ {
+ // converts target to seconds
+ NPT_UInt32 seconds;
+ NPT_CHECK_SEVERE(PLT_Didl::ParseTimeStamp(target, seconds));
+ g_application.SeekTime(seconds);
+ }
+
+ return NPT_SUCCESS;
+}
+
+} /* namespace UPNP */
+
diff --git a/xbmc/network/upnp/UPnPRenderer.h b/xbmc/network/upnp/UPnPRenderer.h
new file mode 100644
index 0000000..e2ebe4b
--- /dev/null
+++ b/xbmc/network/upnp/UPnPRenderer.h
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "interfaces/IAnnouncer.h"
+
+#include <Platinum/Source/Devices/MediaRenderer/PltMediaRenderer.h>
+
+class CVariant;
+
+namespace UPNP
+{
+
+class CRendererReferenceHolder
+{
+public:
+ PLT_DeviceHostReference m_Device;
+};
+
+class CUPnPRenderer : public PLT_MediaRenderer
+ , public ANNOUNCEMENT::IAnnouncer
+{
+public:
+ CUPnPRenderer(const char* friendly_name,
+ bool show_ip = false,
+ const char* uuid = NULL,
+ unsigned int port = 0);
+
+ ~CUPnPRenderer() override;
+
+ void Announce(ANNOUNCEMENT::AnnouncementFlag flag,
+ const std::string& sender,
+ const std::string& message,
+ const CVariant& data) override;
+ void UpdateState();
+
+ // Http server handler
+ NPT_Result ProcessHttpGetRequest(NPT_HttpRequest& request,
+ const NPT_HttpRequestContext& context,
+ NPT_HttpResponse& response) override;
+
+ // AVTransport methods
+ NPT_Result OnNext(PLT_ActionReference& action) override;
+ NPT_Result OnPause(PLT_ActionReference& action) override;
+ NPT_Result OnPlay(PLT_ActionReference& action) override;
+ NPT_Result OnPrevious(PLT_ActionReference& action) override;
+ NPT_Result OnStop(PLT_ActionReference& action) override;
+ NPT_Result OnSeek(PLT_ActionReference& action) override;
+ NPT_Result OnSetAVTransportURI(PLT_ActionReference& action) override;
+ NPT_Result OnSetNextAVTransportURI(PLT_ActionReference& action) override;
+
+ // RenderingControl methods
+ NPT_Result OnSetVolume(PLT_ActionReference& action) override;
+ NPT_Result OnSetMute(PLT_ActionReference& action) override;
+
+private:
+ NPT_Result SetupServices() override;
+ NPT_Result SetupIcons() override;
+ NPT_Result GetMetadata(NPT_String& meta);
+ NPT_Result PlayMedia(const NPT_String& uri,
+ const NPT_String& meta,
+ PLT_Action* action = NULL);
+ NPT_Mutex m_state;
+};
+
+} /* namespace UPNP */
+
diff --git a/xbmc/network/upnp/UPnPServer.cpp b/xbmc/network/upnp/UPnPServer.cpp
new file mode 100644
index 0000000..8e8432e
--- /dev/null
+++ b/xbmc/network/upnp/UPnPServer.cpp
@@ -0,0 +1,1388 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+#include "UPnPServer.h"
+
+#include "GUIUserMessages.h"
+#include "ServiceBroker.h"
+#include "TextureDatabase.h"
+#include "UPnPInternal.h"
+#include "URL.h"
+#include "Util.h"
+#include "filesystem/Directory.h"
+#include "filesystem/MusicDatabaseDirectory.h"
+#include "filesystem/SpecialProtocol.h"
+#include "filesystem/VideoDatabaseDirectory.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "guilib/WindowIDs.h"
+#include "interfaces/AnnouncementManager.h"
+#include "music/Artist.h"
+#include "music/MusicDatabase.h"
+#include "music/MusicLibraryQueue.h"
+#include "music/MusicThumbLoader.h"
+#include "music/tags/MusicInfoTag.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/Digest.h"
+#include "utils/FileExtensionProvider.h"
+#include "utils/FileUtils.h"
+#include "utils/SortUtils.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+#include "video/VideoDatabase.h"
+#include "video/VideoLibraryQueue.h"
+#include "video/VideoThumbLoader.h"
+#include "view/GUIViewState.h"
+#include "xbmc/interfaces/AnnouncementManager.h"
+
+#include <Platinum/Source/Platinum/Platinum.h>
+
+NPT_SET_LOCAL_LOGGER("xbmc.upnp.server")
+
+using namespace ANNOUNCEMENT;
+using namespace XFILE;
+using KODI::UTILITY::CDigest;
+
+namespace UPNP
+{
+
+NPT_UInt32 CUPnPServer::m_MaxReturnedItems = 0;
+
+const char* audio_containers[] = { "musicdb://genres/", "musicdb://artists/", "musicdb://albums/",
+ "musicdb://songs/", "musicdb://recentlyaddedalbums/", "musicdb://years/",
+ "musicdb://singles/" };
+
+const char* video_containers[] = { "library://video/movies/titles.xml/", "library://video/tvshows/titles.xml/",
+ "videodb://recentlyaddedmovies/", "videodb://recentlyaddedepisodes/" };
+
+/*----------------------------------------------------------------------
+| CUPnPServer::CUPnPServer
++---------------------------------------------------------------------*/
+CUPnPServer::CUPnPServer(const char* friendly_name, const char* uuid /*= NULL*/, int port /*= 0*/)
+ : PLT_MediaConnect(friendly_name, false, uuid, port),
+ PLT_FileMediaConnectDelegate("/", "/"),
+ m_scanning(CMusicLibraryQueue::GetInstance().IsScanningLibrary() ||
+ CVideoLibraryQueue::GetInstance().IsScanningLibrary()),
+ m_logger(CServiceBroker::GetLogging().GetLogger(
+ StringUtils::Format("CUPnPServer[{}]", friendly_name)))
+{
+}
+
+CUPnPServer::~CUPnPServer()
+{
+ CServiceBroker::GetAnnouncementManager()->RemoveAnnouncer(this);
+}
+
+/*----------------------------------------------------------------------
+| CUPnPServer::ProcessGetSCPD
++---------------------------------------------------------------------*/
+NPT_Result
+CUPnPServer::ProcessGetSCPD(PLT_Service* service,
+ NPT_HttpRequest& request,
+ const NPT_HttpRequestContext& context,
+ NPT_HttpResponse& response)
+{
+ // needed because PLT_MediaConnect only allows Xbox360 & WMP to search
+ return PLT_MediaServer::ProcessGetSCPD(service, request, context, response);
+}
+
+/*----------------------------------------------------------------------
+| CUPnPServer::SetupServices
++---------------------------------------------------------------------*/
+NPT_Result
+CUPnPServer::SetupServices()
+{
+ PLT_MediaConnect::SetupServices();
+ PLT_Service* service = NULL;
+ NPT_Result result = FindServiceById("urn:upnp-org:serviceId:ContentDirectory", service);
+ if (service)
+ {
+ service->SetStateVariable("SearchCapabilities", "upnp:class");
+ service->SetStateVariable("SortCapabilities", "res@duration,res@size,res@bitrate,dc:date,dc:title,dc:size,upnp:album,upnp:artist,upnp:albumArtist,upnp:episodeNumber,upnp:genre,upnp:originalTrackNumber,upnp:rating,upnp:episodeCount,upnp:episodeSeason,xbmc:rating,xbmc:dateadded,xbmc:votes");
+ }
+
+ m_scanning = true;
+ OnScanCompleted(AudioLibrary);
+ m_scanning = true;
+ OnScanCompleted(VideoLibrary);
+
+ // now safe to start passing on new notifications
+ CServiceBroker::GetAnnouncementManager()->AddAnnouncer(this);
+
+ return result;
+}
+
+/*----------------------------------------------------------------------
+| CUPnPServer::OnScanCompleted
++---------------------------------------------------------------------*/
+void
+CUPnPServer::OnScanCompleted(int type)
+{
+ if (type == AudioLibrary) {
+ for (const char* const audio_container : audio_containers)
+ UpdateContainer(audio_container);
+ }
+ else if (type == VideoLibrary) {
+ for (const char* const video_container : video_containers)
+ UpdateContainer(video_container);
+ }
+ else
+ return;
+ m_scanning = false;
+ PropagateUpdates();
+}
+
+/*----------------------------------------------------------------------
+| CUPnPServer::UpdateContainer
++---------------------------------------------------------------------*/
+void
+CUPnPServer::UpdateContainer(const std::string& id)
+{
+ std::map<std::string, std::pair<bool, unsigned long> >::iterator itr = m_UpdateIDs.find(id);
+ unsigned long count = 0;
+ if (itr != m_UpdateIDs.end())
+ count = ++itr->second.second;
+ m_UpdateIDs[id] = std::make_pair(true, count);
+ PropagateUpdates();
+}
+
+/*----------------------------------------------------------------------
+| CUPnPServer::PropagateUpdates
++---------------------------------------------------------------------*/
+void
+CUPnPServer::PropagateUpdates()
+{
+ PLT_Service* service = NULL;
+ NPT_String current_ids;
+ std::string buffer;
+ std::map<std::string, std::pair<bool, unsigned long> >::iterator itr;
+
+ if (m_scanning || !CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_SERVICES_UPNPANNOUNCE))
+ return;
+
+ NPT_CHECK_LABEL(FindServiceById("urn:upnp-org:serviceId:ContentDirectory", service), failed);
+
+ // we pause, and we must retain any changes which have not been
+ // broadcast yet
+ NPT_CHECK_LABEL(service->PauseEventing(), failed);
+ NPT_CHECK_LABEL(service->GetStateVariableValue("ContainerUpdateIDs", current_ids), failed);
+ buffer = (const char*)current_ids;
+ if (!buffer.empty())
+ buffer.append(",");
+
+ // only broadcast ids with modified bit set
+ for (itr = m_UpdateIDs.begin(); itr != m_UpdateIDs.end(); ++itr) {
+ if (itr->second.first) {
+ buffer.append(StringUtils::Format("{},{},", itr->first, itr->second.second));
+ itr->second.first = false;
+ }
+ }
+
+ // set the value, Platinum will clear ContainerUpdateIDs after sending
+ NPT_CHECK_LABEL(service->SetStateVariable("ContainerUpdateIDs", buffer.substr(0,buffer.size()-1).c_str(), true), failed);
+ NPT_CHECK_LABEL(service->IncStateVariable("SystemUpdateID"), failed);
+
+ service->PauseEventing(false);
+ return;
+
+failed:
+ // should attempt to start eventing on a failure
+ if (service) service->PauseEventing(false);
+ m_logger->error("Unable to propagate updates");
+}
+
+/*----------------------------------------------------------------------
+| CUPnPServer::SetupIcons
++---------------------------------------------------------------------*/
+NPT_Result
+CUPnPServer::SetupIcons()
+{
+ NPT_String file_root = CSpecialProtocol::TranslatePath("special://xbmc/media/").c_str();
+ AddIcon(
+ PLT_DeviceIcon("image/png", 256, 256, 8, "/icon256x256.png"),
+ file_root);
+ AddIcon(
+ PLT_DeviceIcon("image/png", 120, 120, 8, "/icon120x120.png"),
+ file_root);
+ AddIcon(
+ PLT_DeviceIcon("image/png", 48, 48, 8, "/icon48x48.png"),
+ file_root);
+ AddIcon(
+ PLT_DeviceIcon("image/png", 32, 32, 8, "/icon32x32.png"),
+ file_root);
+ AddIcon(
+ PLT_DeviceIcon("image/png", 16, 16, 8, "/icon16x16.png"),
+ file_root);
+ return NPT_SUCCESS;
+}
+
+/*----------------------------------------------------------------------
+| CUPnPServer::BuildSafeResourceUri
++---------------------------------------------------------------------*/
+NPT_String CUPnPServer::BuildSafeResourceUri(const NPT_HttpUrl &rooturi,
+ const char* host,
+ const char* file_path)
+{
+ CURL url(file_path);
+ std::string md5;
+ std::string mapped_file_path(file_path);
+
+ // determine the filename to provide context to md5'd urls
+ std::string filename;
+ if (url.IsProtocol("image"))
+ {
+ filename = URIUtils::GetFileName(url.GetHostName());
+ // Remove trailing / for Platinum/Neptune to recognize the file extension and use the correct mime type when serving the image
+ URIUtils::RemoveSlashAtEnd(mapped_file_path);
+ }
+ else
+ filename = URIUtils::GetFileName(mapped_file_path);
+
+ filename = CURL::Encode(filename);
+ md5 = CDigest::Calculate(CDigest::Type::MD5, mapped_file_path);
+ md5 += "/" + filename;
+ { NPT_AutoLock lock(m_FileMutex);
+ NPT_CHECK(m_FileMap.Put(md5.c_str(), mapped_file_path.c_str()));
+ }
+ return PLT_FileMediaServer::BuildSafeResourceUri(rooturi, host, md5.c_str());
+}
+
+/*----------------------------------------------------------------------
+| CUPnPServer::Build
++---------------------------------------------------------------------*/
+PLT_MediaObject* CUPnPServer::Build(const std::shared_ptr<CFileItem>& item,
+ bool with_count,
+ const PLT_HttpRequestContext& context,
+ NPT_Reference<CThumbLoader>& thumb_loader,
+ const char* parent_id /* = NULL */)
+{
+ PLT_MediaObject* object = NULL;
+ NPT_String path = item->GetPath().c_str();
+
+ //HACK: temporary disabling count as it thrashes HDD
+ with_count = false;
+
+ m_logger->debug("Preparing upnp object for item '{}'", (const char*)path);
+
+ if (path.StartsWith("virtualpath://upnproot")) {
+ path.TrimRight("/");
+ item->m_bIsFolder = true;
+ if (path.StartsWith("virtualpath://")) {
+ object = new PLT_MediaContainer;
+ object->m_Title = item->GetLabel().c_str();
+ object->m_ObjectClass.type = "object.container";
+ object->m_ObjectID = path;
+
+ // root
+ object->m_ObjectID = "0";
+ object->m_ParentID = "-1";
+ // root has 5 children
+
+ //This is dead code because of the HACK a few lines up setting with_count to false
+ //if (with_count) {
+ // ((PLT_MediaContainer*)object)->m_ChildrenCount = 5;
+ //}
+ } else {
+ goto failure;
+ }
+
+ } else {
+ // db path handling
+ NPT_String file_path, share_name;
+ file_path = item->GetPath().c_str();
+ share_name = "";
+
+ if (path.StartsWith("musicdb://")) {
+ if (path == "musicdb://" ) {
+ item->SetLabel("Music Library");
+ item->SetLabelPreformatted(true);
+ item->m_bIsFolder = true;
+ } else {
+ if (!item->HasMusicInfoTag()) {
+ MUSICDATABASEDIRECTORY::CQueryParams params;
+ MUSICDATABASEDIRECTORY::CDirectoryNode::GetDatabaseInfo((const char*)path, params);
+
+ CMusicDatabase db;
+ if (!db.Open() ) return NULL;
+
+ if (params.GetSongId() >= 0 ) {
+ CSong song;
+ if (db.GetSong(params.GetSongId(), song))
+ item->GetMusicInfoTag()->SetSong(song);
+ }
+ else if (params.GetAlbumId() >= 0 ) {
+ item->m_bIsFolder = true;
+ CAlbum album;
+ if (db.GetAlbum(params.GetAlbumId(), album, false))
+ item->GetMusicInfoTag()->SetAlbum(album);
+ }
+ else if (params.GetArtistId() >= 0 ) {
+ item->m_bIsFolder = true;
+ CArtist artist;
+ if (db.GetArtist(params.GetArtistId(), artist, false))
+ item->GetMusicInfoTag()->SetArtist(artist);
+ }
+ }
+
+ // all items appart from songs (artists, albums, etc) are folders
+ if (!item->HasMusicInfoTag() || item->GetMusicInfoTag()->GetType() != MediaTypeSong)
+ {
+ item->m_bIsFolder = true;
+ }
+
+ if (item->GetLabel().empty()) {
+ /* if no label try to grab it from node type */
+ std::string label;
+ if (CMusicDatabaseDirectory::GetLabel((const char*)path, label)) {
+ item->SetLabel(label);
+ item->SetLabelPreformatted(true);
+ }
+ }
+ }
+ } else if (file_path.StartsWith("library://") || file_path.StartsWith("videodb://")) {
+ if (path == "library://video/" ) {
+ item->SetLabel("Video Library");
+ item->SetLabelPreformatted(true);
+ item->m_bIsFolder = true;
+ } else {
+ if (!item->HasVideoInfoTag()) {
+ VIDEODATABASEDIRECTORY::CQueryParams params;
+ VIDEODATABASEDIRECTORY::CDirectoryNode::GetDatabaseInfo((const char*)path, params);
+
+ CVideoDatabase db;
+ if (!db.Open() ) return NULL;
+
+ if (params.GetMovieId() >= 0 )
+ db.GetMovieInfo((const char*)path, *item->GetVideoInfoTag(), params.GetMovieId());
+ else if (params.GetMVideoId() >= 0 )
+ db.GetMusicVideoInfo((const char*)path, *item->GetVideoInfoTag(), params.GetMVideoId());
+ else if (params.GetEpisodeId() >= 0 )
+ db.GetEpisodeInfo((const char*)path, *item->GetVideoInfoTag(), params.GetEpisodeId());
+ else if (params.GetTvShowId() >= 0)
+ {
+ if (params.GetSeason() >= 0)
+ {
+ int idSeason = db.GetSeasonId(params.GetTvShowId(), params.GetSeason());
+ if (idSeason >= 0)
+ db.GetSeasonInfo(idSeason, *item->GetVideoInfoTag());
+ }
+ else
+ db.GetTvShowInfo((const char*)path, *item->GetVideoInfoTag(), params.GetTvShowId());
+ }
+ }
+
+ if (item->GetVideoInfoTag()->m_type == MediaTypeTvShow || item->GetVideoInfoTag()->m_type == MediaTypeSeason) {
+ // for tvshows and seasons, iEpisode and playCount are
+ // invalid
+ item->m_bIsFolder = true;
+ item->GetVideoInfoTag()->m_iEpisode = (int)item->GetProperty("totalepisodes").asInteger();
+ item->GetVideoInfoTag()->SetPlayCount(static_cast<int>(item->GetProperty("watchedepisodes").asInteger()));
+ }
+ // if this is an item in the library without a playable path it most be a folder
+ else if (item->GetVideoInfoTag()->m_strFileNameAndPath.empty())
+ {
+ item->m_bIsFolder = true;
+ }
+
+ // try to grab title from tag
+ if (item->HasVideoInfoTag() && !item->GetVideoInfoTag()->m_strTitle.empty()) {
+ item->SetLabel(item->GetVideoInfoTag()->m_strTitle);
+ item->SetLabelPreformatted(true);
+ }
+
+ // try to grab it from the folder
+ if (item->GetLabel().empty()) {
+ std::string label;
+ if (CVideoDatabaseDirectory::GetLabel((const char*)path, label)) {
+ item->SetLabel(label);
+ item->SetLabelPreformatted(true);
+ }
+ }
+ }
+ }
+ // playlists are folders
+ else if (item->IsPlayList())
+ {
+ item->m_bIsFolder = true;
+ }
+ // audio and not a playlist -> song, so it's not a folder
+ else if (item->IsAudio())
+ {
+ item->m_bIsFolder = false;
+ }
+ // any other type of item -> delegate to CDirectory
+ else
+ {
+ item->m_bIsFolder = CDirectory::Exists(item->GetPath());
+ }
+
+ // not a virtual path directory, new system
+ object = BuildObject(*item.get(), file_path, with_count, thumb_loader, &context, this, UPnPContentDirectory);
+
+ // set parent id if passed, otherwise it should have been determined
+ if (object && parent_id) {
+ object->m_ParentID = parent_id;
+ }
+ }
+
+ if (object) {
+ // remap Root virtualpath://upnproot/ to id "0"
+ if (object->m_ObjectID == "virtualpath://upnproot/")
+ object->m_ObjectID = "0";
+
+ // remap Parent Root virtualpath://upnproot/ to id "0"
+ if (object->m_ParentID == "virtualpath://upnproot/")
+ object->m_ParentID = "0";
+ }
+
+ return object;
+
+failure:
+ delete object;
+ return NULL;
+}
+
+/*----------------------------------------------------------------------
+| CUPnPServer::Announce
++---------------------------------------------------------------------*/
+void CUPnPServer::Announce(AnnouncementFlag flag,
+ const std::string& sender,
+ const std::string& message,
+ const CVariant& data)
+{
+ NPT_String path;
+ int item_id;
+ std::string item_type;
+
+ if (sender != CAnnouncementManager::ANNOUNCEMENT_SENDER)
+ return;
+
+ if (message != "OnUpdate" && message != "OnRemove" && message != "OnScanStarted" &&
+ message != "OnScanFinished")
+ return;
+
+ if (data.isNull()) {
+ if (message == "OnScanStarted" || message == "OnCleanStarted")
+ {
+ m_scanning = true;
+ }
+ else if (message == "OnScanFinished" || message == "OnCleanFinished")
+ {
+ OnScanCompleted(flag);
+ }
+ }
+ else {
+ // handle both updates & removals
+ if (!data["item"].isNull()) {
+ item_id = (int)data["item"]["id"].asInteger();
+ item_type = data["item"]["type"].asString();
+ }
+ else {
+ item_id = (int)data["id"].asInteger();
+ item_type = data["type"].asString();
+ }
+
+ // we always update 'recently added' nodes along with the specific container,
+ // as we don't differentiate 'updates' from 'adds' in RPC interface
+ if (flag == VideoLibrary) {
+ if(item_type == MediaTypeEpisode) {
+ CVideoDatabase db;
+ if (!db.Open()) return;
+ int show_id = db.GetTvShowForEpisode(item_id);
+ int season_id = db.GetSeasonForEpisode(item_id);
+ UpdateContainer(StringUtils::Format("videodb://tvshows/titles/{}/", show_id));
+ UpdateContainer(StringUtils::Format("videodb://tvshows/titles/{}/{}/?tvshowid={}",
+ show_id, season_id, show_id));
+ UpdateContainer("videodb://recentlyaddedepisodes/");
+ }
+ else if(item_type == MediaTypeTvShow) {
+ UpdateContainer("library://video/tvshows/titles.xml/");
+ UpdateContainer("videodb://recentlyaddedepisodes/");
+ }
+ else if(item_type == MediaTypeMovie) {
+ UpdateContainer("library://video/movies/titles.xml/");
+ UpdateContainer("videodb://recentlyaddedmovies/");
+ }
+ else if(item_type == MediaTypeMusicVideo) {
+ UpdateContainer("library://video/musicvideos/titles.xml/");
+ UpdateContainer("videodb://recentlyaddedmusicvideos/");
+ }
+ }
+ else if (flag == AudioLibrary && item_type == MediaTypeSong) {
+ // we also update the 'songs' container is maybe a performance drop too
+ // high? would need to check if slow clients even cache at all anyway
+ CMusicDatabase db;
+ CAlbum album;
+ if (!db.Open()) return;
+ if (db.GetAlbumFromSong(item_id, album)) {
+ UpdateContainer(StringUtils::Format("musicdb://albums/{}", album.idAlbum));
+ UpdateContainer("musicdb://songs/");
+ UpdateContainer("musicdb://recentlyaddedalbums/");
+ }
+ }
+ }
+}
+
+/*----------------------------------------------------------------------
+| TranslateWMPObjectId
++---------------------------------------------------------------------*/
+static NPT_String TranslateWMPObjectId(NPT_String id, const Logger& logger)
+{
+ if (id == "0") {
+ id = "virtualpath://upnproot/";
+ } else if (id == "15") {
+ // Xbox 360 asking for videos
+ id = "library://video/";
+ } else if (id == "16") {
+ // Xbox 360 asking for photos
+ } else if (id == "107") {
+ // Sonos uses 107 for artists root container id
+ id = "musicdb://artists/";
+ } else if (id == "7") {
+ // Sonos uses 7 for albums root container id
+ id = "musicdb://albums/";
+ } else if (id == "4") {
+ // Sonos uses 4 for tracks root container id
+ id = "musicdb://songs/";
+ }
+
+ logger->debug("Translated id to '{}'", (const char*)id);
+ return id;
+}
+
+NPT_Result
+ObjectIDValidate(const NPT_String& id)
+{
+ if (CFileUtils::RemoteAccessAllowed(id.GetChars()))
+ return NPT_SUCCESS;
+ return NPT_ERROR_NO_SUCH_FILE;
+}
+
+/*----------------------------------------------------------------------
+| CUPnPServer::OnBrowseMetadata
++---------------------------------------------------------------------*/
+NPT_Result
+CUPnPServer::OnBrowseMetadata(PLT_ActionReference& action,
+ const char* object_id,
+ const char* filter,
+ NPT_UInt32 starting_index,
+ NPT_UInt32 requested_count,
+ const char* sort_criteria,
+ const PLT_HttpRequestContext& context)
+{
+ NPT_COMPILER_UNUSED(sort_criteria);
+ NPT_COMPILER_UNUSED(requested_count);
+ NPT_COMPILER_UNUSED(starting_index);
+
+ NPT_String didl;
+ NPT_Reference<PLT_MediaObject> object;
+ NPT_String id = TranslateWMPObjectId(object_id, m_logger);
+ CFileItemPtr item;
+ NPT_Reference<CThumbLoader> thumb_loader;
+
+ m_logger->info("Received UPnP Browse Metadata request for object '{}'", object_id);
+
+ if(NPT_FAILED(ObjectIDValidate(id))) {
+ action->SetError(701, "Incorrect ObjectID.");
+ return NPT_FAILURE;
+ }
+
+ if (id.StartsWith("virtualpath://")) {
+ id.TrimRight("/");
+ if (id == "virtualpath://upnproot") {
+ id += "/";
+ item.reset(new CFileItem((const char*)id, true));
+ item->SetLabel("Root");
+ item->SetLabelPreformatted(true);
+ object = Build(item, true, context, thumb_loader);
+ object->m_ParentID = "-1";
+ } else {
+ return NPT_FAILURE;
+ }
+ } else {
+ item.reset(new CFileItem((const char*)id, false));
+
+ // attempt to determine the parent of this item
+ std::string parent;
+ if (URIUtils::IsVideoDb((const char*)id) || URIUtils::IsMusicDb((const char*)id) || StringUtils::StartsWithNoCase((const char*)id, "library://video/")) {
+ if (!URIUtils::GetParentPath((const char*)id, parent)) {
+ parent = "0";
+ }
+ }
+ else {
+ // non-library objects - playlists / sources
+ //
+ // we could instead store the parents in a hash during every browse
+ // or could handle this in URIUtils::GetParentPath() possibly,
+ // however this is quicker to implement and subsequently purge when a
+ // better solution presents itself
+ std::string child_id((const char*)id);
+ if (StringUtils::StartsWithNoCase(child_id, "special://musicplaylists/")) parent = "musicdb://";
+ else if (StringUtils::StartsWithNoCase(child_id, "special://videoplaylists/")) parent = "library://video/";
+ else if (StringUtils::StartsWithNoCase(child_id, "sources://video/")) parent = "library://video/";
+ else if (StringUtils::StartsWithNoCase(child_id, "special://profile/playlists/music/")) parent = "special://musicplaylists/";
+ else if (StringUtils::StartsWithNoCase(child_id, "special://profile/playlists/video/")) parent = "special://videoplaylists/";
+ else parent = "sources://video/"; // this can only match video sources
+ }
+
+ if (item->IsVideoDb()) {
+ thumb_loader = NPT_Reference<CThumbLoader>(new CVideoThumbLoader());
+ }
+ else if (item->IsMusicDb()) {
+ thumb_loader = NPT_Reference<CThumbLoader>(new CMusicThumbLoader());
+ }
+ if (!thumb_loader.IsNull()) {
+ thumb_loader->OnLoaderStart();
+ }
+ object = Build(item, true, context, thumb_loader, parent.empty()?NULL:parent.c_str());
+ }
+
+ if (object.IsNull()) {
+ /* error */
+ NPT_LOG_WARNING_1("CUPnPServer::OnBrowseMetadata - Object null (%s)", object_id);
+ action->SetError(701, "No Such Object.");
+ return NPT_FAILURE;
+ }
+
+ NPT_String tmp;
+ NPT_CHECK(PLT_Didl::ToDidl(*object.AsPointer(), filter, tmp));
+
+ /* add didl header and footer */
+ didl = didl_header + tmp + didl_footer;
+
+ NPT_CHECK(action->SetArgumentValue("Result", didl));
+ NPT_CHECK(action->SetArgumentValue("NumberReturned", "1"));
+ NPT_CHECK(action->SetArgumentValue("TotalMatches", "1"));
+
+ // update ID may be wrong here, it should be the one of the container?
+ NPT_CHECK(action->SetArgumentValue("UpdateId", "0"));
+
+ //! @todo We need to keep track of the overall SystemUpdateID of the CDS
+
+ return NPT_SUCCESS;
+}
+
+/*----------------------------------------------------------------------
+| CUPnPServer::OnBrowseDirectChildren
++---------------------------------------------------------------------*/
+NPT_Result
+CUPnPServer::OnBrowseDirectChildren(PLT_ActionReference& action,
+ const char* object_id,
+ const char* filter,
+ NPT_UInt32 starting_index,
+ NPT_UInt32 requested_count,
+ const char* sort_criteria,
+ const PLT_HttpRequestContext& context)
+{
+ CFileItemList items;
+ NPT_String parent_id = TranslateWMPObjectId(object_id, m_logger);
+
+ m_logger->info("Received Browse DirectChildren request for object '{}', with sort criteria {}",
+ object_id, sort_criteria);
+
+ if(NPT_FAILED(ObjectIDValidate(parent_id))) {
+ action->SetError(701, "Incorrect ObjectID.");
+ return NPT_FAILURE;
+ }
+
+ items.SetPath(std::string(parent_id));
+
+ // guard against loading while saving to the same cache file
+ // as CArchive currently performs no locking itself
+ bool load;
+ { NPT_AutoLock lock(m_CacheMutex);
+ load = items.Load();
+ }
+
+ if (!load) {
+ // cache anything that takes more than a second to retrieve
+ auto start = std::chrono::steady_clock::now();
+
+ if (parent_id.StartsWith("virtualpath://upnproot")) {
+ CFileItemPtr item;
+
+ // music library
+ item.reset(new CFileItem("musicdb://", true));
+ item->SetLabel("Music Library");
+ item->SetLabelPreformatted(true);
+ items.Add(item);
+
+ // video library
+ item.reset(new CFileItem("library://video/", true));
+ item->SetLabel("Video Library");
+ item->SetLabelPreformatted(true);
+ items.Add(item);
+
+ items.Sort(SortByLabel, SortOrderAscending);
+ } else {
+ // this is the only way to hide unplayable items in the 'files'
+ // view as we cannot tell what context (eg music vs video) the
+ // request came from
+ std::string supported = CServiceBroker::GetFileExtensionProvider().GetPictureExtensions() + "|"
+ + CServiceBroker::GetFileExtensionProvider().GetVideoExtensions() + "|"
+ + CServiceBroker::GetFileExtensionProvider().GetMusicExtensions() + "|"
+ + CServiceBroker::GetFileExtensionProvider().GetPictureExtensions();
+ CDirectory::GetDirectory((const char*)parent_id, items, supported, DIR_FLAG_DEFAULTS);
+ DefaultSortItems(items);
+ }
+
+ auto end = std::chrono::steady_clock::now();
+ auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
+
+ if (items.CacheToDiscAlways() || (items.CacheToDiscIfSlow() && duration.count() > 1000))
+ {
+ NPT_AutoLock lock(m_CacheMutex);
+ items.Save();
+ }
+ }
+
+ // as there's no library://music support, manually add playlists and music
+ // video nodes
+ if (items.GetPath() == "musicdb://") {
+ CFileItemPtr playlists(new CFileItem("special://musicplaylists/", true));
+ playlists->SetLabel(g_localizeStrings.Get(136));
+ items.Add(playlists);
+
+ CVideoDatabase database;
+ database.Open();
+ if (database.HasContent(VideoDbContentType::MUSICVIDEOS))
+ {
+ CFileItemPtr mvideos(new CFileItem("library://video/musicvideos/", true));
+ mvideos->SetLabel(g_localizeStrings.Get(20389));
+ items.Add(mvideos);
+ }
+ }
+
+ // Don't pass parent_id if action is Search not BrowseDirectChildren, as
+ // we want the engine to determine the best parent id, not necessarily the one
+ // passed
+ NPT_String action_name = action->GetActionDesc().GetName();
+ return BuildResponse(
+ action,
+ items,
+ filter,
+ starting_index,
+ requested_count,
+ sort_criteria,
+ context,
+ (action_name.Compare("Search", true)==0)?NULL:parent_id.GetChars());
+}
+
+/*----------------------------------------------------------------------
+| CUPnPServer::BuildResponse
++---------------------------------------------------------------------*/
+NPT_Result
+CUPnPServer::BuildResponse(PLT_ActionReference& action,
+ CFileItemList& items,
+ const char* filter,
+ NPT_UInt32 starting_index,
+ NPT_UInt32 requested_count,
+ const char* sort_criteria,
+ const PLT_HttpRequestContext& context,
+ const char* parent_id /* = NULL */)
+{
+ NPT_COMPILER_UNUSED(sort_criteria);
+
+ m_logger->debug("Building UPnP response with filter '{}', starting @ {} with {} requested",
+ filter, starting_index, requested_count);
+
+ // we will reuse this ThumbLoader for all items
+ NPT_Reference<CThumbLoader> thumb_loader;
+
+ if (URIUtils::IsVideoDb(items.GetPath()) ||
+ StringUtils::StartsWithNoCase(items.GetPath(), "library://video/") ||
+ StringUtils::StartsWithNoCase(items.GetPath(), "special://profile/playlists/video/")) {
+
+ thumb_loader = NPT_Reference<CThumbLoader>(new CVideoThumbLoader());
+ }
+ else if (URIUtils::IsMusicDb(items.GetPath()) ||
+ StringUtils::StartsWithNoCase(items.GetPath(), "special://profile/playlists/music/")) {
+
+ thumb_loader = NPT_Reference<CThumbLoader>(new CMusicThumbLoader());
+ }
+ if (!thumb_loader.IsNull()) {
+ thumb_loader->OnLoaderStart();
+ }
+
+ // this isn't pretty but needed to properly hide the addons node from clients
+ if (StringUtils::StartsWith(items.GetPath(), "library")) {
+ for (int i=0; i<items.Size(); i++) {
+ if (StringUtils::StartsWith(items[i]->GetPath(), "addons") ||
+ StringUtils::EndsWith(items[i]->GetPath(), "/addons.xml/"))
+ items.Remove(i);
+ }
+ }
+
+ // won't return more than UPNP_MAX_RETURNED_ITEMS items at a time to keep things smooth
+ // 0 requested means as many as possible
+ NPT_UInt32 max_count = (requested_count == 0)?m_MaxReturnedItems:std::min((unsigned long)requested_count, (unsigned long)m_MaxReturnedItems);
+ NPT_UInt32 stop_index = std::min((unsigned long)(starting_index + max_count), (unsigned long)items.Size()); // don't return more than we can
+
+ NPT_Cardinal count = 0;
+ NPT_Cardinal total = items.Size();
+ NPT_String didl = didl_header;
+ PLT_MediaObjectReference object;
+ for (unsigned long i=starting_index; i<stop_index; ++i) {
+ object = Build(items[i], true, context, thumb_loader, parent_id);
+ if (object.IsNull()) {
+ // don't tell the client this item ever existed
+ --total;
+ continue;
+ }
+
+ NPT_String tmp;
+ NPT_CHECK(PLT_Didl::ToDidl(*object.AsPointer(), filter, tmp));
+
+ // Neptunes string growing is dead slow for small additions
+ if (didl.GetCapacity() < tmp.GetLength() + didl.GetLength()) {
+ didl.Reserve((tmp.GetLength() + didl.GetLength())*2);
+ }
+ didl += tmp;
+ ++count;
+ }
+
+ didl += didl_footer;
+
+ m_logger->debug("Returning UPnP response with {} items out of {} total matches", count, total);
+
+ NPT_CHECK(action->SetArgumentValue("Result", didl));
+ NPT_CHECK(action->SetArgumentValue("NumberReturned", NPT_String::FromInteger(count)));
+ NPT_CHECK(action->SetArgumentValue("TotalMatches", NPT_String::FromInteger(total)));
+ NPT_CHECK(action->SetArgumentValue("UpdateId", "0"));
+ return NPT_SUCCESS;
+}
+
+/*----------------------------------------------------------------------
+| FindSubCriteria
++---------------------------------------------------------------------*/
+static
+NPT_String
+FindSubCriteria(NPT_String criteria, const char* name)
+{
+ NPT_String result;
+ int search = criteria.Find(name);
+ if (search >= 0) {
+ criteria = criteria.Right(criteria.GetLength() - search - NPT_StringLength(name));
+ criteria.TrimLeft(" ");
+ if (criteria.GetLength()>0 && criteria[0] == '=') {
+ criteria.TrimLeft("= ");
+ if (criteria.GetLength()>0 && criteria[0] == '\"') {
+ search = criteria.Find("\"", 1);
+ if (search > 0) result = criteria.SubString(1, search-1);
+ }
+ }
+ }
+ return result;
+}
+
+/*----------------------------------------------------------------------
+| CUPnPServer::OnSearchContainer
++---------------------------------------------------------------------*/
+NPT_Result
+CUPnPServer::OnSearchContainer(PLT_ActionReference& action,
+ const char* object_id,
+ const char* search_criteria,
+ const char* filter,
+ NPT_UInt32 starting_index,
+ NPT_UInt32 requested_count,
+ const char* sort_criteria,
+ const PLT_HttpRequestContext& context)
+{
+ m_logger->debug("Received Search request for object '{}' with search '{}'", object_id,
+ search_criteria);
+
+ NPT_String id = object_id;
+ NPT_String searchClass = NPT_String(search_criteria);
+ if (id.StartsWith("musicdb://")) {
+ // we browse for all tracks given a genre, artist or album
+ if (searchClass.Find("object.item.audioItem") >= 0) {
+ if (!id.EndsWith("/")) id += "/";
+ NPT_Cardinal count = id.SubString(10).Split("/").GetItemCount();
+ // remove extra empty node count
+ count = count?count-1:0;
+
+ // genre
+ if (id.StartsWith("musicdb://genres/")) {
+ // all tracks of all genres
+ if (count == 1)
+ id += "-1/-1/-1/";
+ // all tracks of a specific genre
+ else if (count == 2)
+ id += "-1/-1/";
+ // all tracks of a specific genre of a specific artist
+ else if (count == 3)
+ id += "-1/";
+ }
+ else if (id.StartsWith("musicdb://artists/")) {
+ // all tracks by all artists
+ if (count == 1)
+ id += "-1/-1/";
+ // all tracks of a specific artist
+ else if (count == 2)
+ id += "-1/";
+ }
+ else if (id.StartsWith("musicdb://albums/")) {
+ // all albums ?
+ if (count == 1) id += "-1/";
+ }
+ }
+ return OnBrowseDirectChildren(action, id, filter, starting_index, requested_count, sort_criteria, context);
+ } else if (searchClass.Find("object.item.audioItem") >= 0) {
+ // look for artist, album & genre filters
+ NPT_String genre = FindSubCriteria(searchClass, "upnp:genre");
+ NPT_String album = FindSubCriteria(searchClass, "upnp:album");
+ NPT_String artist = FindSubCriteria(searchClass, "upnp:artist");
+ // sonos looks for microsoft specific stuff
+ artist = artist.GetLength()?artist:FindSubCriteria(searchClass, "microsoft:artistPerformer");
+ artist = artist.GetLength()?artist:FindSubCriteria(searchClass, "microsoft:artistAlbumArtist");
+ artist = artist.GetLength()?artist:FindSubCriteria(searchClass, "microsoft:authorComposer");
+
+ CMusicDatabase database;
+ database.Open();
+
+ if (genre.GetLength() > 0) {
+ // all tracks by genre filtered by artist and/or album
+ std::string strPath = StringUtils::Format(
+ "musicdb://genres/{}/{}/{}/", database.GetGenreByName((const char*)genre),
+ database.GetArtistByName((const char*)artist), // will return -1 if no artist
+ database.GetAlbumByName((const char*)album)); // will return -1 if no album
+
+ return OnBrowseDirectChildren(action, strPath.c_str(), filter, starting_index, requested_count, sort_criteria, context);
+ } else if (artist.GetLength() > 0) {
+ // all tracks by artist name filtered by album if passed
+ std::string strPath = StringUtils::Format(
+ "musicdb://artists/{}/{}/", database.GetArtistByName((const char*)artist),
+ database.GetAlbumByName((const char*)album)); // will return -1 if no album
+
+ return OnBrowseDirectChildren(action, strPath.c_str(), filter, starting_index, requested_count, sort_criteria, context);
+ } else if (album.GetLength() > 0) {
+ // all tracks by album name
+ std::string strPath = StringUtils::Format("musicdb://albums/{}/",
+ database.GetAlbumByName((const char*)album));
+
+ return OnBrowseDirectChildren(action, strPath.c_str(), filter, starting_index, requested_count, sort_criteria, context);
+ }
+
+ // browse all songs
+ return OnBrowseDirectChildren(action, "musicdb://songs/", filter, starting_index, requested_count, sort_criteria, context);
+ } else if (searchClass.Find("object.container.album.musicAlbum") >= 0) {
+ // sonos filters by genre
+ NPT_String genre = FindSubCriteria(searchClass, "upnp:genre");
+
+ // 360 hack: artist/albums using search
+ NPT_String artist = FindSubCriteria(searchClass, "upnp:artist");
+ // sonos looks for microsoft specific stuff
+ artist = artist.GetLength()?artist:FindSubCriteria(searchClass, "microsoft:artistPerformer");
+ artist = artist.GetLength()?artist:FindSubCriteria(searchClass, "microsoft:artistAlbumArtist");
+ artist = artist.GetLength()?artist:FindSubCriteria(searchClass, "microsoft:authorComposer");
+
+ CMusicDatabase database;
+ database.Open();
+
+ if (genre.GetLength() > 0) {
+ std::string strPath = StringUtils::Format(
+ "musicdb://genres/{}/{}/", database.GetGenreByName((const char*)genre),
+ database.GetArtistByName((const char*)artist)); // no artist should return -1
+ return OnBrowseDirectChildren(action, strPath.c_str(), filter, starting_index,
+ requested_count, sort_criteria, context);
+ } else if (artist.GetLength() > 0) {
+ std::string strPath = StringUtils::Format("musicdb://artists/{}/",
+ database.GetArtistByName((const char*)artist));
+ return OnBrowseDirectChildren(action, strPath.c_str(), filter, starting_index,
+ requested_count, sort_criteria, context);
+ }
+
+ // all albums
+ return OnBrowseDirectChildren(action, "musicdb://albums/", filter, starting_index, requested_count, sort_criteria, context);
+ } else if (searchClass.Find("object.container.person.musicArtist") >= 0) {
+ // Sonos filters by genre
+ NPT_String genre = FindSubCriteria(searchClass, "upnp:genre");
+ if (genre.GetLength() > 0) {
+ CMusicDatabase database;
+ database.Open();
+ std::string strPath = StringUtils::Format("musicdb://genres/{}/",
+ database.GetGenreByName((const char*)genre));
+ return OnBrowseDirectChildren(action, strPath.c_str(), filter, starting_index, requested_count, sort_criteria, context);
+ }
+ return OnBrowseDirectChildren(action, "musicdb://artists/", filter, starting_index, requested_count, sort_criteria, context);
+ } else if (searchClass.Find("object.container.genre.musicGenre") >= 0) {
+ return OnBrowseDirectChildren(action, "musicdb://genres/", filter, starting_index, requested_count, sort_criteria, context);
+ } else if (searchClass.Find("object.container.playlistContainer") >= 0) {
+ return OnBrowseDirectChildren(action, "special://musicplaylists/", filter, starting_index, requested_count, sort_criteria, context);
+ } else if (searchClass.Find("object.container.album.videoAlbum.videoBroadcastShow") >= 0) {
+ CVideoDatabase database;
+ if (!database.Open()) {
+ action->SetError(800, "Internal Error");
+ return NPT_SUCCESS;
+ }
+
+ CFileItemList items;
+ if (!database.GetTvShowsByWhere("videodb://tvshows/titles/?local", CDatabase::Filter(),
+ items, SortDescription(), GetRequiredVideoDbDetails(NPT_String(filter)))) {
+ action->SetError(800, "Internal Error");
+ return NPT_SUCCESS;
+ }
+
+ items.SetPath("videodb://tvshows/titles/");
+ return BuildResponse(action, items, filter, starting_index, requested_count, sort_criteria, context, NULL);
+ } else if (searchClass.Find("object.container.album.videoAlbum.videoBroadcastSeason") >= 0) {
+ CVideoDatabase database;
+ if (!database.Open()) {
+ action->SetError(800, "Internal Error");
+ return NPT_SUCCESS;
+ }
+
+ CFileItemList items;
+ if (!database.GetSeasonsByWhere("videodb://tvshows/titles/-1/?local", CDatabase::Filter(), items, true)) {
+ action->SetError(800, "Internal Error");
+ return NPT_SUCCESS;
+ }
+
+ items.SetPath("videodb://tvshows/titles/-1/");
+ return BuildResponse(action, items, filter, starting_index, requested_count, sort_criteria, context, NULL);
+ } else if (searchClass.Find("object.item.videoItem") >= 0) {
+ CFileItemList items, allItems;
+
+ CVideoDatabase database;
+ if (!database.Open()) {
+ action->SetError(800, "Internal Error");
+ return NPT_SUCCESS;
+ }
+
+ bool allVideoItems = searchClass.Compare("object.item.videoItem") == 0;
+
+ // determine the required videodb details to be retrieved
+ int requiredVideoDbDetails = GetRequiredVideoDbDetails(NPT_String(filter));
+
+ if (allVideoItems || searchClass.Find("object.item.videoItem.movie") >= 0)
+ {
+ if (!database.GetMoviesByWhere("videodb://movies/titles/?local", CDatabase::Filter(), items, SortDescription(), requiredVideoDbDetails)) {
+ action->SetError(800, "Internal Error");
+ return NPT_SUCCESS;
+ }
+
+ allItems.Append(items);
+ items.Clear();
+
+ if (!allVideoItems)
+ allItems.SetPath("videodb://movies/titles/");
+ }
+
+ if (allVideoItems || searchClass.Find("object.item.videoItem.videoBroadcast") >= 0)
+ {
+ if (!database.GetEpisodesByWhere("videodb://tvshows/titles/?local", CDatabase::Filter(), items, true, SortDescription(), requiredVideoDbDetails)) {
+ action->SetError(800, "Internal Error");
+ return NPT_SUCCESS;
+ }
+
+ allItems.Append(items);
+ items.Clear();
+
+ if (!allVideoItems)
+ allItems.SetPath("videodb://tvshows/titles/");
+ }
+
+ if (allVideoItems || searchClass.Find("object.item.videoItem.musicVideoClip") >= 0)
+ {
+ if (!database.GetMusicVideosByWhere("videodb://musicvideos/titles/?local", CDatabase::Filter(), items, true, SortDescription(), requiredVideoDbDetails)) {
+ action->SetError(800, "Internal Error");
+ return NPT_SUCCESS;
+ }
+
+ allItems.Append(items);
+ items.Clear();
+
+ if (!allVideoItems)
+ allItems.SetPath("videodb://musicvideos/titles/");
+ }
+
+ if (allVideoItems)
+ allItems.SetPath("videodb://movies/titles/");
+
+ return BuildResponse(action, allItems, filter, starting_index, requested_count, sort_criteria, context, NULL);
+ } else if (searchClass.Find("object.item.imageItem") >= 0) {
+ CFileItemList items;
+ return BuildResponse(action, items, filter, starting_index, requested_count, sort_criteria, context, NULL);
+ }
+
+ return NPT_FAILURE;
+}
+
+/*----------------------------------------------------------------------
+| CUPnPServer::OnUpdateObject
++---------------------------------------------------------------------*/
+NPT_Result
+CUPnPServer::OnUpdateObject(PLT_ActionReference& action,
+ const char* object_id,
+ NPT_Map<NPT_String,NPT_String>& current_vals,
+ NPT_Map<NPT_String,NPT_String>& new_vals,
+ const PLT_HttpRequestContext& context)
+{
+ std::string path(CURL::Decode(object_id));
+ CFileItem updated;
+ updated.SetPath(path);
+ m_logger->info("OnUpdateObject: {} from {}", path,
+ (const char*)context.GetRemoteAddress().GetIpAddress().ToString());
+
+ NPT_String playCount, position, lastPlayed;
+ int err;
+ const char* msg = NULL;
+ bool updatelisting(false);
+
+ // we pause eventing as multiple announces may happen in this operation
+ PLT_Service* service = NULL;
+ NPT_CHECK_LABEL(FindServiceById("urn:upnp-org:serviceId:ContentDirectory", service), error);
+ NPT_CHECK_LABEL(service->PauseEventing(), error);
+
+ if (updated.IsVideoDb()) {
+ CVideoDatabase db;
+ NPT_CHECK_LABEL(!db.Open(), error);
+
+ // must first determine type of file from object id
+ VIDEODATABASEDIRECTORY::CQueryParams params;
+ VIDEODATABASEDIRECTORY::CDirectoryNode::GetDatabaseInfo(path.c_str(), params);
+
+ int id = -1;
+ VideoDbContentType content_type;
+ if ((id = params.GetMovieId()) >= 0 )
+ content_type = VideoDbContentType::MOVIES;
+ else if ((id = params.GetEpisodeId()) >= 0 )
+ content_type = VideoDbContentType::EPISODES;
+ else if ((id = params.GetMVideoId()) >= 0 )
+ content_type = VideoDbContentType::MUSICVIDEOS;
+ else {
+ err = 701;
+ msg = "No such object";
+ goto failure;
+ }
+
+ std::string file_path;
+ db.GetFilePathById(id, file_path, content_type);
+ CVideoInfoTag tag;
+ db.LoadVideoInfo(file_path, tag);
+ updated.SetFromVideoInfoTag(tag);
+ m_logger->info("Translated to {}", file_path);
+
+ position = new_vals["lastPlaybackPosition"];
+ playCount = new_vals["playCount"];
+ lastPlayed = new_vals["lastPlaybackTime"];
+
+
+ if (!position.IsEmpty()
+ && position.Compare(current_vals["lastPlaybackPosition"]) != 0) {
+ NPT_UInt32 resume;
+ NPT_CHECK_LABEL(position.ToInteger32(resume), args);
+
+ if (resume <= 0)
+ db.ClearBookMarksOfFile(file_path, CBookmark::RESUME);
+ else {
+ CBookmark bookmark;
+ bookmark.timeInSeconds = resume;
+ bookmark.totalTimeInSeconds = resume + 100; // not required to be correct
+ bookmark.playerState = new_vals["lastPlayerState"];
+
+ db.AddBookMarkToFile(file_path, bookmark, CBookmark::RESUME);
+ }
+ if (playCount.IsEmpty()) {
+ CVariant data;
+ data["id"] = updated.GetVideoInfoTag()->m_iDbId;
+ data["type"] = updated.GetVideoInfoTag()->m_type;
+ CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::VideoLibrary,
+ "OnUpdate", data);
+ }
+ updatelisting = true;
+ }
+
+ if (!playCount.IsEmpty()
+ && playCount.Compare(current_vals["playCount"]) != 0) {
+
+ NPT_UInt32 count;
+ NPT_CHECK_LABEL(playCount.ToInteger32(count), args);
+
+ CDateTime lastPlayedObj;
+ if (!lastPlayed.IsEmpty() &&
+ lastPlayed.Compare(current_vals["lastPlaybackTime"]) != 0)
+ lastPlayedObj.SetFromW3CDateTime(lastPlayed.GetChars());
+
+ db.SetPlayCount(updated, count, lastPlayedObj);
+ updatelisting = true;
+ }
+
+ // we must load the changed settings before propagating to local UI
+ if (updatelisting) {
+ db.LoadVideoInfo(file_path, tag);
+ updated.SetFromVideoInfoTag(tag);
+ //! TODO: we should find a way to avoid obtaining the artwork just to
+ // update the playcount or similar properties. Maybe a flag in the GUI
+ // update message to inform we should only update the playback properties
+ // without touching other parts of the item.
+ CVideoThumbLoader().FillLibraryArt(updated);
+ }
+
+ } else if (updated.IsMusicDb()) {
+ //! @todo implement this
+
+ } else {
+ err = 701;
+ msg = "No such object";
+ goto failure;
+ }
+
+ if (updatelisting) {
+ updated.SetPath(path);
+ if (updated.IsVideoDb())
+ CUtil::DeleteVideoDatabaseDirectoryCache();
+ else if (updated.IsMusicDb())
+ CUtil::DeleteMusicDatabaseDirectoryCache();
+
+ CFileItemPtr msgItem(new CFileItem(updated));
+ CGUIMessage message(GUI_MSG_NOTIFY_ALL, CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow(), 0, GUI_MSG_UPDATE_ITEM, GUI_MSG_FLAG_UPDATE_LIST, msgItem);
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(message);
+ }
+
+ NPT_CHECK_LABEL(service->PauseEventing(false), error);
+ return NPT_SUCCESS;
+
+args:
+ err = 402;
+ msg = "Invalid args";
+ goto failure;
+
+error:
+ err = 501;
+ msg = "Internal error";
+
+failure:
+ m_logger->error("OnUpdateObject failed with err {}: {}", err, msg);
+ action->SetError(err, msg);
+ service->PauseEventing(false);
+ return NPT_FAILURE;
+}
+
+/*----------------------------------------------------------------------
+| CUPnPServer::ServeFile
++---------------------------------------------------------------------*/
+NPT_Result
+CUPnPServer::ServeFile(const NPT_HttpRequest& request,
+ const NPT_HttpRequestContext& context,
+ NPT_HttpResponse& response,
+ const NPT_String& md5)
+{
+ // Translate hash to filename
+ NPT_String file_path(md5), *file_path2;
+ { NPT_AutoLock lock(m_FileMutex);
+ if(NPT_SUCCEEDED(m_FileMap.Get(md5, file_path2))) {
+ file_path = *file_path2;
+ m_logger->debug("Received request to serve '{}' = '{}'", (const char*)md5,
+ (const char*)file_path);
+ } else {
+ m_logger->debug("Received request to serve unknown md5 '{}'", (const char*)md5);
+ response.SetStatus(404, "File Not Found");
+ return NPT_SUCCESS;
+ }
+ }
+
+ // File requested
+ NPT_HttpUrl rooturi(context.GetLocalAddress().GetIpAddress().ToString(), context.GetLocalAddress().GetPort(), "/");
+
+ if (file_path.Left(8).Compare("stack://", true) == 0) {
+
+ NPT_List<NPT_String> files = file_path.SubString(8).Split(" , ");
+ if (files.GetItemCount() == 0) {
+ response.SetStatus(404, "File Not Found");
+ return NPT_SUCCESS;
+ }
+
+ NPT_String output;
+ output.Reserve(file_path.GetLength()*2);
+ output += "#EXTM3U\r\n";
+
+ NPT_List<NPT_String>::Iterator url = files.GetFirstItem();
+ for (;url;url++) {
+ output += ("#EXTINF:-1," + URIUtils::GetFileName((const char*)*url)).c_str();
+ output += "\r\n";
+ output += BuildSafeResourceUri(
+ rooturi,
+ context.GetLocalAddress().GetIpAddress().ToString(),
+ *url);
+ output += "\r\n";
+ }
+
+ PLT_HttpHelper::SetBody(response, (const char*)output, output.GetLength());
+ response.GetHeaders().SetHeader("Content-Disposition", "inline; filename=\"stack.m3u\"");
+ return NPT_SUCCESS;
+ }
+
+ if (URIUtils::IsURL(static_cast<const char*>(file_path)))
+ {
+ CURL url(CTextureUtils::UnwrapImageURL(static_cast<const char*>(file_path)));
+ std::string disp = "inline; filename=\"" + URIUtils::GetFileName(url) + "\"";
+ response.GetHeaders().SetHeader("Content-Disposition", disp.c_str());
+ }
+
+ // set getCaptionInfo.sec - sets subtitle uri for Samsung devices
+ const NPT_String* captionInfoHeader = request.GetHeaders().GetHeaderValue("getCaptionInfo.sec");
+ if (captionInfoHeader)
+ {
+ NPT_String *sub_uri, movie;
+ movie = "subtitle://" + md5;
+
+ NPT_AutoLock lock(m_FileMutex);
+ if (NPT_SUCCEEDED(m_FileMap.Get(movie, sub_uri)))
+ {
+ response.GetHeaders().SetHeader("CaptionInfo.sec", sub_uri->GetChars(), false);
+ }
+ }
+
+ return PLT_HttpServer::ServeFile(request,
+ context,
+ response,
+ file_path);
+}
+
+void
+CUPnPServer::DefaultSortItems(CFileItemList& items)
+{
+ CGUIViewState* viewState = CGUIViewState::GetViewState(items.IsVideoDb() ? WINDOW_VIDEO_NAV : -1, items);
+ if (viewState)
+ {
+ SortDescription sorting = viewState->GetSortMethod();
+ items.Sort(sorting.sortBy, sorting.sortOrder, sorting.sortAttributes);
+ delete viewState;
+ }
+}
+
+NPT_Result CUPnPServer::AddSubtitleUriForSecResponse(const NPT_String& movie_md5,
+ const NPT_String& subtitle_uri)
+{
+ /* using existing m_FileMap to store subtitle uri for movie,
+ adding subtitle:// prefix, because there is already entry for movie md5 with movie path */
+ NPT_String movie = "subtitle://" + movie_md5;
+
+ NPT_AutoLock lock(m_FileMutex);
+ NPT_CHECK(m_FileMap.Put(movie, subtitle_uri));
+
+ return NPT_SUCCESS;
+}
+
+int CUPnPServer::GetRequiredVideoDbDetails(const NPT_String& filter)
+{
+ int details = VideoDbDetailsRating;
+ if (filter.Find("res@resolution") >= 0 || filter.Find("res@nrAudioChannels") >= 0)
+ details |= VideoDbDetailsStream;
+ if (filter.Find("upnp:actor") >= 0)
+ details |= VideoDbDetailsCast;
+
+ return details;
+}
+
+} /* namespace UPNP */
+
diff --git a/xbmc/network/upnp/UPnPServer.h b/xbmc/network/upnp/UPnPServer.h
new file mode 100644
index 0000000..1c92e8b
--- /dev/null
+++ b/xbmc/network/upnp/UPnPServer.h
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "interfaces/IAnnouncer.h"
+#include "utils/logtypes.h"
+
+#include <map>
+#include <memory>
+#include <string>
+#include <utility>
+
+#include <Platinum/Source/Devices/MediaConnect/PltMediaConnect.h>
+
+class CFileItem;
+class CFileItemList;
+class CThumbLoader;
+class CVariant;
+class PLT_MediaObject;
+class PLT_HttpRequestContext;
+
+namespace UPNP
+{
+
+class CUPnPServer : public PLT_MediaConnect,
+ public PLT_FileMediaConnectDelegate,
+ public ANNOUNCEMENT::IAnnouncer
+{
+public:
+ CUPnPServer(const char* friendly_name, const char* uuid = NULL, int port = 0);
+ ~CUPnPServer() override;
+ void Announce(ANNOUNCEMENT::AnnouncementFlag flag,
+ const std::string& sender,
+ const std::string& message,
+ const CVariant& data) override;
+
+ // PLT_MediaServer methods
+ NPT_Result OnBrowseMetadata(PLT_ActionReference& action,
+ const char* object_id,
+ const char* filter,
+ NPT_UInt32 starting_index,
+ NPT_UInt32 requested_count,
+ const char* sort_criteria,
+ const PLT_HttpRequestContext& context) override;
+ NPT_Result OnBrowseDirectChildren(PLT_ActionReference& action,
+ const char* object_id,
+ const char* filter,
+ NPT_UInt32 starting_index,
+ NPT_UInt32 requested_count,
+ const char* sort_criteria,
+ const PLT_HttpRequestContext& context) override;
+ NPT_Result OnSearchContainer(PLT_ActionReference& action,
+ const char* container_id,
+ const char* search_criteria,
+ const char* filter,
+ NPT_UInt32 starting_index,
+ NPT_UInt32 requested_count,
+ const char* sort_criteria,
+ const PLT_HttpRequestContext& context) override;
+
+ NPT_Result OnUpdateObject(PLT_ActionReference& action,
+ const char* object_id,
+ NPT_Map<NPT_String,NPT_String>& current_vals,
+ NPT_Map<NPT_String,NPT_String>& new_vals,
+ const PLT_HttpRequestContext& context) override;
+
+ // PLT_FileMediaServer methods
+ NPT_Result ServeFile(const NPT_HttpRequest& request,
+ const NPT_HttpRequestContext& context,
+ NPT_HttpResponse& response,
+ const NPT_String& file_path) override;
+
+ // PLT_DeviceHost methods
+ NPT_Result ProcessGetSCPD(PLT_Service* service,
+ NPT_HttpRequest& request,
+ const NPT_HttpRequestContext& context,
+ NPT_HttpResponse& response) override;
+
+ NPT_Result SetupServices() override;
+ NPT_Result SetupIcons() override;
+ NPT_String BuildSafeResourceUri(const NPT_HttpUrl &rooturi,
+ const char* host,
+ const char* file_path);
+
+ void AddSafeResourceUri(PLT_MediaObject* object,
+ const NPT_HttpUrl& rooturi,
+ const NPT_List<NPT_IpAddress>& ips,
+ const char* file_path,
+ const NPT_String& info)
+ {
+ PLT_MediaItemResource res;
+ for(NPT_List<NPT_IpAddress>::Iterator ip = ips.GetFirstItem(); ip; ++ip) {
+ res.m_ProtocolInfo = PLT_ProtocolInfo(info);
+ res.m_Uri = BuildSafeResourceUri(rooturi, (*ip).ToString(), file_path);
+ object->m_Resources.Add(res);
+ }
+ }
+
+ /* Samsung's devices get subtitles from header in response (for movie file), not from didl.
+ It's a way to store subtitle uri generated when building didl, to use later in http response*/
+ NPT_Result AddSubtitleUriForSecResponse(const NPT_String& movie_md5,
+ const NPT_String& subtitle_uri);
+
+
+ private:
+ void OnScanCompleted(int type);
+ void UpdateContainer(const std::string& id);
+ void PropagateUpdates();
+
+ PLT_MediaObject* Build(const std::shared_ptr<CFileItem>& item,
+ bool with_count,
+ const PLT_HttpRequestContext& context,
+ NPT_Reference<CThumbLoader>& thumbLoader,
+ const char* parent_id = NULL);
+ NPT_Result BuildResponse(PLT_ActionReference& action,
+ CFileItemList& items,
+ const char* filter,
+ NPT_UInt32 starting_index,
+ NPT_UInt32 requested_count,
+ const char* sort_criteria,
+ const PLT_HttpRequestContext& context,
+ const char* parent_id /* = NULL */);
+
+ // class methods
+ static void DefaultSortItems(CFileItemList& items);
+ static NPT_String GetParentFolder(const NPT_String& file_path)
+ {
+ int index = file_path.ReverseFind("\\");
+ if (index == -1)
+ return "";
+
+ return file_path.Left(index);
+ }
+
+ static int GetRequiredVideoDbDetails(const NPT_String& filter);
+
+ NPT_Mutex m_CacheMutex;
+
+ NPT_Mutex m_FileMutex;
+ NPT_Map<NPT_String, NPT_String> m_FileMap;
+
+ std::map<std::string, std::pair<bool, unsigned long> > m_UpdateIDs;
+ bool m_scanning;
+
+ Logger m_logger;
+
+ public:
+ // class members
+ static NPT_UInt32 m_MaxReturnedItems;
+};
+
+} /* namespace UPNP */
+
diff --git a/xbmc/network/upnp/UPnPSettings.cpp b/xbmc/network/upnp/UPnPSettings.cpp
new file mode 100644
index 0000000..30ad1fd
--- /dev/null
+++ b/xbmc/network/upnp/UPnPSettings.cpp
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2013-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "UPnPSettings.h"
+
+#include "ServiceBroker.h"
+#include "utils/FileUtils.h"
+#include "utils/StringUtils.h"
+#include "utils/XBMCTinyXML.h"
+#include "utils/XMLUtils.h"
+#include "utils/log.h"
+
+#include <mutex>
+
+#define XML_UPNP "upnpserver"
+#define XML_SERVER_UUID "UUID"
+#define XML_SERVER_PORT "Port"
+#define XML_MAX_ITEMS "MaxReturnedItems"
+#define XML_RENDERER_UUID "UUIDRenderer"
+#define XML_RENDERER_PORT "PortRenderer"
+
+CUPnPSettings::CUPnPSettings() : m_logger(CServiceBroker::GetLogging().GetLogger("CUPnPSettings"))
+{
+ Clear();
+}
+
+CUPnPSettings::~CUPnPSettings()
+{
+ Clear();
+}
+
+CUPnPSettings& CUPnPSettings::GetInstance()
+{
+ static CUPnPSettings sUPnPSettings;
+ return sUPnPSettings;
+}
+
+void CUPnPSettings::OnSettingsUnloaded()
+{
+ Clear();
+}
+
+bool CUPnPSettings::Load(const std::string &file)
+{
+ std::unique_lock<CCriticalSection> lock(m_critical);
+
+ Clear();
+
+ if (!CFileUtils::Exists(file))
+ return false;
+
+ CXBMCTinyXML doc;
+ if (!doc.LoadFile(file))
+ {
+ m_logger->error("error loading {}, Line {}\n{}", file, doc.ErrorRow(), doc.ErrorDesc());
+ return false;
+ }
+
+ TiXmlElement *pRootElement = doc.RootElement();
+ if (pRootElement == NULL || !StringUtils::EqualsNoCase(pRootElement->Value(), XML_UPNP))
+ {
+ m_logger->error("error loading {}, no <upnpserver> node", file);
+ return false;
+ }
+
+ // load settings
+ XMLUtils::GetString(pRootElement, XML_SERVER_UUID, m_serverUUID);
+ XMLUtils::GetInt(pRootElement, XML_SERVER_PORT, m_serverPort);
+ XMLUtils::GetInt(pRootElement, XML_MAX_ITEMS, m_maxReturnedItems);
+ XMLUtils::GetString(pRootElement, XML_RENDERER_UUID, m_rendererUUID);
+ XMLUtils::GetInt(pRootElement, XML_RENDERER_PORT, m_rendererPort);
+
+ return true;
+}
+
+bool CUPnPSettings::Save(const std::string &file) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critical);
+
+ CXBMCTinyXML doc;
+ TiXmlElement xmlRootElement(XML_UPNP);
+ TiXmlNode *pRoot = doc.InsertEndChild(xmlRootElement);
+ if (pRoot == NULL)
+ return false;
+
+ XMLUtils::SetString(pRoot, XML_SERVER_UUID, m_serverUUID);
+ XMLUtils::SetInt(pRoot, XML_SERVER_PORT, m_serverPort);
+ XMLUtils::SetInt(pRoot, XML_MAX_ITEMS, m_maxReturnedItems);
+ XMLUtils::SetString(pRoot, XML_RENDERER_UUID, m_rendererUUID);
+ XMLUtils::SetInt(pRoot, XML_RENDERER_PORT, m_rendererPort);
+
+ // save the file
+ return doc.SaveFile(file);
+}
+
+void CUPnPSettings::Clear()
+{
+ std::unique_lock<CCriticalSection> lock(m_critical);
+
+ m_serverUUID.clear();
+ m_serverPort = 0;
+ m_maxReturnedItems = 0;
+ m_rendererUUID.clear();
+ m_rendererPort = 0;
+}
diff --git a/xbmc/network/upnp/UPnPSettings.h b/xbmc/network/upnp/UPnPSettings.h
new file mode 100644
index 0000000..7469b06
--- /dev/null
+++ b/xbmc/network/upnp/UPnPSettings.h
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2013-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "settings/lib/ISettingsHandler.h"
+#include "threads/CriticalSection.h"
+#include "utils/logtypes.h"
+
+#include <string>
+
+class CUPnPSettings : public ISettingsHandler
+{
+public:
+ static CUPnPSettings& GetInstance();
+
+ void OnSettingsUnloaded() override;
+
+ bool Load(const std::string &file);
+ bool Save(const std::string &file) const;
+ void Clear();
+
+ const std::string& GetServerUUID() const { return m_serverUUID; }
+ int GetServerPort() const { return m_serverPort; }
+ int GetMaximumReturnedItems() const { return m_maxReturnedItems; }
+ const std::string& GetRendererUUID() const { return m_rendererUUID; }
+ int GetRendererPort() const { return m_rendererPort; }
+
+ void SetServerUUID(const std::string &uuid) { m_serverUUID = uuid; }
+ void SetServerPort(int port) { m_serverPort = port; }
+ void SetMaximumReturnedItems(int maximumReturnedItems) { m_maxReturnedItems = maximumReturnedItems; }
+ void SetRendererUUID(const std::string &uuid) { m_rendererUUID = uuid; }
+ void SetRendererPort(int port) { m_rendererPort = port; }
+
+protected:
+ CUPnPSettings();
+ CUPnPSettings(const CUPnPSettings&) = delete;
+ CUPnPSettings& operator=(CUPnPSettings const&) = delete;
+ ~CUPnPSettings() override;
+
+private:
+ std::string m_serverUUID;
+ int m_serverPort;
+ int m_maxReturnedItems;
+ std::string m_rendererUUID;
+ int m_rendererPort;
+
+ mutable CCriticalSection m_critical;
+
+ Logger m_logger;
+};