diff options
Diffstat (limited to 'xbmc/network/upnp')
-rw-r--r-- | xbmc/network/upnp/CMakeLists.txt | 18 | ||||
-rw-r--r-- | xbmc/network/upnp/UPnP.cpp | 891 | ||||
-rw-r--r-- | xbmc/network/upnp/UPnP.h | 108 | ||||
-rw-r--r-- | xbmc/network/upnp/UPnPInternal.cpp | 1245 | ||||
-rw-r--r-- | xbmc/network/upnp/UPnPInternal.h | 122 | ||||
-rw-r--r-- | xbmc/network/upnp/UPnPPlayer.cpp | 625 | ||||
-rw-r--r-- | xbmc/network/upnp/UPnPPlayer.h | 77 | ||||
-rw-r--r-- | xbmc/network/upnp/UPnPRenderer.cpp | 758 | ||||
-rw-r--r-- | xbmc/network/upnp/UPnPRenderer.h | 73 | ||||
-rw-r--r-- | xbmc/network/upnp/UPnPServer.cpp | 1388 | ||||
-rw-r--r-- | xbmc/network/upnp/UPnPServer.h | 159 | ||||
-rw-r--r-- | xbmc/network/upnp/UPnPSettings.cpp | 110 | ||||
-rw-r--r-- | xbmc/network/upnp/UPnPSettings.h | 56 |
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; +}; |