2 * UPnP Support for XBMC
3 * Copyright (c) 2006 c0diq (Sylvain Rebaud)
4 * Portions Copyright (c) by the authors of libPlatinum
5 * http://www.plutinosoft.com/blog/category/platinum/
6 * Copyright (C) 2006-2013 Team XBMC
9 * This Program is free software; you can redistribute it and/or modify
10 * it under the terms of the GNU General Public License as published by
11 * the Free Software Foundation; either version 2, or (at your option)
14 * This Program is distributed in the hope that it will be useful,
15 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 * GNU General Public License for more details.
19 * You should have received a copy of the GNU General Public License
20 * along with XBMC; see the file COPYING. If not, see
21 * <http://www.gnu.org/licenses/>.
27 #include "threads/SystemClock.h"
29 #include "UPnPInternal.h"
30 #include "UPnPRenderer.h"
31 #include "UPnPServer.h"
32 #include "UPnPSettings.h"
33 #include "utils/URIUtils.h"
34 #include "Application.h"
35 #include "ApplicationMessenger.h"
36 #include "network/Network.h"
37 #include "utils/log.h"
40 #include "profiles/ProfilesManager.h"
41 #include "settings/Settings.h"
42 #include "GUIUserMessages.h"
44 #include "guilib/GUIWindowManager.h"
45 #include "GUIInfoManager.h"
46 #include "utils/TimeUtils.h"
47 #include "video/VideoInfoTag.h"
48 #include "guilib/Key.h"
54 NPT_SET_LOCAL_LOGGER("xbmc.upnp")
56 #define UPNP_DEFAULT_MAX_RETURNED_ITEMS 200
57 #define UPNP_DEFAULT_MIN_RETURNED_ITEMS 30
63 DLNA_ORG_PS = 'DLNA.ORG_PS'
66 # Convertion Indicator
69 DLNA_ORG_CI = 'DLNA.ORG_CI'
73 # 00 not time seek range, not range
75 # 10 time seek range supported
77 DLNA_ORG_OP = 'DLNA.ORG_OP'
78 DLNA_ORG_OP_VAL = '01'
81 # senderPaced 80000000 31
82 # lsopTimeBasedSeekSupported 40000000 30
83 # lsopByteBasedSeekSupported 20000000 29
84 # playcontainerSupported 10000000 28
85 # s0IncreasingSupported 08000000 27
86 # sNIncreasingSupported 04000000 26
87 # rtspPauseSupported 02000000 25
88 # streamingTransferModeSupported 01000000 24
89 # interactiveTransferModeSupported 00800000 23
90 # backgroundTransferModeSupported 00400000 22
91 # connectionStallingSupported 00200000 21
92 # dlnaVersion15Supported 00100000 20
93 DLNA_ORG_FLAGS = 'DLNA.ORG_FLAGS'
94 DLNA_ORG_FLAGS_VAL = '01500000000000000000000000000000'
97 /*----------------------------------------------------------------------
99 +---------------------------------------------------------------------*/
101 NPT_Console::Output(const char* message)
103 CLog::Log(LOGDEBUG, "%s", message);
109 /*----------------------------------------------------------------------
111 +---------------------------------------------------------------------*/
112 CUPnP* CUPnP::upnp = NULL;
113 static NPT_List<void*> g_UserData;
114 static NPT_Mutex g_UserDataLock;
116 /*----------------------------------------------------------------------
117 | CDeviceHostReferenceHolder class
118 +---------------------------------------------------------------------*/
119 class CDeviceHostReferenceHolder
122 PLT_DeviceHostReference m_Device;
125 /*----------------------------------------------------------------------
126 | CCtrlPointReferenceHolder class
127 +---------------------------------------------------------------------*/
128 class CCtrlPointReferenceHolder
131 PLT_CtrlPointReference m_CtrlPoint;
134 /*----------------------------------------------------------------------
136 +---------------------------------------------------------------------*/
137 class CUPnPCleaner : public NPT_Thread
140 CUPnPCleaner(CUPnP* upnp) : NPT_Thread(true), m_UPnP(upnp) {}
148 /*----------------------------------------------------------------------
149 | CMediaBrowser class
150 +---------------------------------------------------------------------*/
151 class CMediaBrowser : public PLT_SyncMediaBrowser,
152 public PLT_MediaContainerChangesListener
155 CMediaBrowser(PLT_CtrlPointReference& ctrlPoint)
156 : PLT_SyncMediaBrowser(ctrlPoint, true)
158 SetContainerListener(this);
161 // PLT_MediaBrowser methods
162 virtual bool OnMSAdded(PLT_DeviceDataReference& device)
164 CGUIMessage message(GUI_MSG_NOTIFY_ALL, 0, 0, GUI_MSG_UPDATE_PATH);
165 message.SetStringParam("upnp://");
166 g_windowManager.SendThreadMessage(message);
168 return PLT_SyncMediaBrowser::OnMSAdded(device);
170 virtual void OnMSRemoved(PLT_DeviceDataReference& device)
172 PLT_SyncMediaBrowser::OnMSRemoved(device);
174 CGUIMessage message(GUI_MSG_NOTIFY_ALL, 0, 0, GUI_MSG_UPDATE_PATH);
175 message.SetStringParam("upnp://");
176 g_windowManager.SendThreadMessage(message);
178 PLT_SyncMediaBrowser::OnMSRemoved(device);
181 // PLT_MediaContainerChangesListener methods
182 virtual void OnContainerChanged(PLT_DeviceDataReference& device,
184 const char* update_id)
186 NPT_String path = "upnp://"+device->GetUUID()+"/";
187 if (!NPT_StringsEqual(item_id, "0")) {
188 CStdString id(CURL::Encode(item_id));
189 URIUtils::AddSlashAtEnd(id);
193 CLog::Log(LOGDEBUG, "UPNP: notfified container update %s", (const char*)path);
194 CGUIMessage message(GUI_MSG_NOTIFY_ALL, 0, 0, GUI_MSG_UPDATE_PATH);
195 message.SetStringParam(path.GetChars());
196 g_windowManager.SendThreadMessage(message);
199 bool MarkWatched(const CFileItem& item, const bool watched)
202 CFileItem temp(item);
203 temp.SetProperty("original_listitem_url", item.GetPath());
204 return SaveFileState(temp, CBookmark(), watched);
207 CLog::Log(LOGDEBUG, "UPNP: Marking video item %s as watched", item.GetPath().c_str());
208 return InvokeUpdateObject(item.GetPath().c_str(), "<upnp:playCount>1</upnp:playCount>", "<upnp:playCount>0</upnp:playCount>");
212 bool SaveFileState(const CFileItem& item, const CBookmark& bookmark, const bool updatePlayCount)
214 string path = item.GetProperty("original_listitem_url").asString();
215 if (!item.HasVideoInfoTag() || path.empty()) {
219 NPT_String curr_value;
220 NPT_String new_value;
222 if (item.GetVideoInfoTag()->m_resumePoint.timeInSeconds != bookmark.timeInSeconds) {
223 CLog::Log(LOGDEBUG, "UPNP: Updating resume point for item %s", path.c_str());
224 long time = (long)bookmark.timeInSeconds;
225 if (time < 0) time = 0;
226 curr_value.Append(NPT_String::Format("<upnp:lastPlaybackPosition>%ld</upnp:lastPlaybackPosition>",
227 (long)item.GetVideoInfoTag()->m_resumePoint.timeInSeconds));
228 new_value.Append(NPT_String::Format("<upnp:lastPlaybackPosition>%ld</upnp:lastPlaybackPosition>", time));
230 if (updatePlayCount) {
231 CLog::Log(LOGDEBUG, "UPNP: Marking video item %s as watched", path.c_str());
232 if (!curr_value.IsEmpty()) curr_value.Append(",");
233 if (!new_value.IsEmpty()) new_value.Append(",");
234 curr_value.Append("<upnp:playCount>0</upnp:playCount>");
235 new_value.Append("<upnp:playCount>1</upnp:playCount>");
238 return InvokeUpdateObject(path.c_str(), (const char*)curr_value, (const char*)new_value);
241 bool InvokeUpdateObject(const char* id, const char* curr_value, const char* new_value)
244 PLT_DeviceDataReference device;
246 PLT_ActionReference action;
248 CLog::Log(LOGDEBUG, "UPNP: attempting to invoke UpdateObject for %s", id);
250 // check this server supports UpdateObject action
251 NPT_CHECK_LABEL(FindServer(url.GetHostName().c_str(), device),failed);
252 NPT_CHECK_LABEL(device->FindServiceById("urn:upnp-org:serviceId:ContentDirectory", cds),failed);
254 NPT_CHECK_SEVERE(m_CtrlPoint->CreateAction(
256 "urn:schemas-upnp-org:service:ContentDirectory:1",
260 NPT_CHECK_LABEL(action->SetArgumentValue("ObjectID", url.GetFileName().c_str()), failed);
261 NPT_CHECK_LABEL(action->SetArgumentValue("CurrentTagValue", curr_value), failed);
262 NPT_CHECK_LABEL(action->SetArgumentValue("NewTagValue", new_value), failed);
264 NPT_CHECK_LABEL(m_CtrlPoint->InvokeAction(action, NULL),failed);
266 CLog::Log(LOGDEBUG, "UPNP: invoked UpdateObject successfully");
270 CLog::Log(LOGINFO, "UPNP: invoking UpdateObject failed");
276 /*----------------------------------------------------------------------
277 | CMediaController class
278 +---------------------------------------------------------------------*/
279 class CMediaController
280 : public PLT_MediaControllerDelegate
281 , public PLT_MediaController
284 CMediaController(PLT_CtrlPointReference& ctrl_point)
285 : PLT_MediaController(ctrl_point)
287 PLT_MediaController::SetDelegate(this);
292 for (std::set<std::string>::const_iterator itRenderer = m_registeredRenderers.begin(); itRenderer != m_registeredRenderers.end(); ++itRenderer)
293 unregisterRenderer(*itRenderer);
294 m_registeredRenderers.clear();
297 #define CHECK_USERDATA_RETURN(userdata) do { \
298 if (!g_UserData.Contains(userdata)) \
302 virtual void OnStopResult(NPT_Result res, PLT_DeviceDataReference& device, void* userdata)
303 { CHECK_USERDATA_RETURN(userdata);
304 static_cast<PLT_MediaControllerDelegate*>(userdata)->OnStopResult(res, device, userdata);
307 virtual void OnSetPlayModeResult(NPT_Result res, PLT_DeviceDataReference& device, void* userdata)
308 { CHECK_USERDATA_RETURN(userdata);
309 static_cast<PLT_MediaControllerDelegate*>(userdata)->OnSetPlayModeResult(res, device, userdata);
312 virtual void OnSetAVTransportURIResult(NPT_Result res, PLT_DeviceDataReference& device, void* userdata)
313 { CHECK_USERDATA_RETURN(userdata);
314 static_cast<PLT_MediaControllerDelegate*>(userdata)->OnSetAVTransportURIResult(res, device, userdata);
317 virtual void OnSeekResult(NPT_Result res, PLT_DeviceDataReference& device, void* userdata)
318 { CHECK_USERDATA_RETURN(userdata);
319 static_cast<PLT_MediaControllerDelegate*>(userdata)->OnSeekResult(res, device, userdata);
322 virtual void OnPreviousResult(NPT_Result res, PLT_DeviceDataReference& device, void* userdata)
323 { CHECK_USERDATA_RETURN(userdata);
324 static_cast<PLT_MediaControllerDelegate*>(userdata)->OnPreviousResult(res, device, userdata);
327 virtual void OnPlayResult(NPT_Result res, PLT_DeviceDataReference& device, void* userdata)
328 { CHECK_USERDATA_RETURN(userdata);
329 static_cast<PLT_MediaControllerDelegate*>(userdata)->OnPlayResult(res, device, userdata);
332 virtual void OnPauseResult(NPT_Result res, PLT_DeviceDataReference& device, void* userdata)
333 { CHECK_USERDATA_RETURN(userdata);
334 static_cast<PLT_MediaControllerDelegate*>(userdata)->OnPauseResult(res, device, userdata);
337 virtual void OnNextResult(NPT_Result res, PLT_DeviceDataReference& device, void* userdata)
338 { CHECK_USERDATA_RETURN(userdata);
339 static_cast<PLT_MediaControllerDelegate*>(userdata)->OnNextResult(res, device, userdata);
342 virtual void OnGetMediaInfoResult(NPT_Result res, PLT_DeviceDataReference& device, PLT_MediaInfo* info, void* userdata)
343 { CHECK_USERDATA_RETURN(userdata);
344 static_cast<PLT_MediaControllerDelegate*>(userdata)->OnGetMediaInfoResult(res, device, info, userdata);
347 virtual void OnGetPositionInfoResult(NPT_Result res, PLT_DeviceDataReference& device, PLT_PositionInfo* info, void* userdata)
348 { CHECK_USERDATA_RETURN(userdata);
349 static_cast<PLT_MediaControllerDelegate*>(userdata)->OnGetPositionInfoResult(res, device, info, userdata);
352 virtual void OnGetTransportInfoResult(NPT_Result res, PLT_DeviceDataReference& device, PLT_TransportInfo* info, void* userdata)
353 { CHECK_USERDATA_RETURN(userdata);
354 static_cast<PLT_MediaControllerDelegate*>(userdata)->OnGetTransportInfoResult(res, device, info, userdata);
357 virtual bool OnMRAdded(PLT_DeviceDataReference& device )
359 if (device->GetUUID().IsEmpty() || device->GetUUID().GetChars() == NULL)
362 CPlayerCoreFactory::Get().OnPlayerDiscovered((const char*)device->GetUUID()
363 ,(const char*)device->GetFriendlyName()
365 m_registeredRenderers.insert(std::string(device->GetUUID().GetChars()));
369 virtual void OnMRRemoved(PLT_DeviceDataReference& device )
371 if (device->GetUUID().IsEmpty() || device->GetUUID().GetChars() == NULL)
374 std::string uuid(device->GetUUID().GetChars());
375 unregisterRenderer(uuid);
376 m_registeredRenderers.erase(uuid);
380 void unregisterRenderer(const std::string &deviceUUID)
382 CPlayerCoreFactory::Get().OnPlayerRemoved(deviceUUID);
385 std::set<std::string> m_registeredRenderers;
388 /*----------------------------------------------------------------------
390 +---------------------------------------------------------------------*/
392 m_MediaBrowser(NULL),
393 m_MediaController(NULL),
394 m_ServerHolder(new CDeviceHostReferenceHolder()),
395 m_RendererHolder(new CRendererReferenceHolder()),
396 m_CtrlPointHolder(new CCtrlPointReferenceHolder())
398 // initialize upnp context
399 m_UPnP = new PLT_UPnP();
401 // keep main IP around
402 if (g_application.getNetwork().GetFirstConnectedInterface()) {
403 m_IP = g_application.getNetwork().GetFirstConnectedInterface()->GetCurrentIPAddress().c_str();
405 NPT_List<NPT_IpAddress> list;
406 if (NPT_SUCCEEDED(PLT_UPnPMessageHelper::GetIPAddresses(list)) && list.GetItemCount()) {
407 m_IP = (*(list.GetFirstItem())).ToString();
409 else if(m_IP.empty())
412 // start upnp monitoring
416 /*----------------------------------------------------------------------
418 +---------------------------------------------------------------------*/
426 delete m_ServerHolder;
427 delete m_RendererHolder;
428 delete m_CtrlPointHolder;
431 /*----------------------------------------------------------------------
433 +---------------------------------------------------------------------*/
444 /*----------------------------------------------------------------------
445 | CUPnP::ReleaseInstance
446 +---------------------------------------------------------------------*/
448 CUPnP::ReleaseInstance(bool bWait)
457 // since it takes a while to clean up
458 // starts a detached thread to do this
459 CUPnPCleaner* cleaner = new CUPnPCleaner(_upnp);
465 /*----------------------------------------------------------------------
467 +---------------------------------------------------------------------*/
468 CUPnPServer* CUPnP::GetServer()
471 return (CUPnPServer*)upnp->m_ServerHolder->m_Device.AsPointer();
475 /*----------------------------------------------------------------------
477 +---------------------------------------------------------------------*/
479 CUPnP::MarkWatched(const CFileItem& item, const bool watched)
481 if (upnp && upnp->m_MediaBrowser) {
482 // dynamic_cast is safe here, avoids polluting CUPnP.h header file
483 CMediaBrowser* browser = dynamic_cast<CMediaBrowser*>(upnp->m_MediaBrowser);
484 return browser->MarkWatched(item, watched);
489 /*----------------------------------------------------------------------
490 | CUPnP::SaveFileState
491 +---------------------------------------------------------------------*/
493 CUPnP::SaveFileState(const CFileItem& item, const CBookmark& bookmark, const bool updatePlayCount)
495 if (upnp && upnp->m_MediaBrowser) {
496 // dynamic_cast is safe here, avoids polluting CUPnP.h header file
497 CMediaBrowser* browser = dynamic_cast<CMediaBrowser*>(upnp->m_MediaBrowser);
498 return browser->SaveFileState(item, bookmark, updatePlayCount);
503 /*----------------------------------------------------------------------
505 +---------------------------------------------------------------------*/
509 if (!m_CtrlPointHolder->m_CtrlPoint.IsNull()) return;
511 // create controlpoint
512 m_CtrlPointHolder->m_CtrlPoint = new PLT_CtrlPoint();
515 m_UPnP->AddCtrlPoint(m_CtrlPointHolder->m_CtrlPoint);
518 m_MediaBrowser = new CMediaBrowser(m_CtrlPointHolder->m_CtrlPoint);
521 if (CSettings::Get().GetBool("services.upnpcontroller") &&
522 CSettings::Get().GetBool("services.upnpserver")) {
523 m_MediaController = new CMediaController(m_CtrlPointHolder->m_CtrlPoint);
527 /*----------------------------------------------------------------------
529 +---------------------------------------------------------------------*/
533 if (m_CtrlPointHolder->m_CtrlPoint.IsNull()) return;
535 m_UPnP->RemoveCtrlPoint(m_CtrlPointHolder->m_CtrlPoint);
536 m_CtrlPointHolder->m_CtrlPoint = NULL;
538 delete m_MediaBrowser;
539 m_MediaBrowser = NULL;
540 delete m_MediaController;
541 m_MediaController = NULL;
544 /*----------------------------------------------------------------------
545 | CUPnP::CreateServer
546 +---------------------------------------------------------------------*/
548 CUPnP::CreateServer(int port /* = 0 */)
550 CUPnPServer* device =
551 new CUPnPServer(g_infoManager.GetLabel(SYSTEM_FRIENDLY_NAME),
552 CUPnPSettings::Get().GetServerUUID().length() ? CUPnPSettings::Get().GetServerUUID().c_str() : NULL,
555 // trying to set optional upnp values for XP UPnP UI Icons to detect us
556 // but it doesn't work anyways as it requires multicast for XP to detect us
557 device->m_PresentationURL =
559 CSettings::Get().GetInt("services.webserverport"),
562 device->m_ModelName = "XBMC Media Center";
563 device->m_ModelNumber = g_infoManager.GetVersion().c_str();
564 device->m_ModelDescription = "XBMC Media Center - Media Server";
565 device->m_ModelURL = "http://xbmc.org/";
566 device->m_Manufacturer = "Team XBMC";
567 device->m_ManufacturerURL = "http://xbmc.org/";
569 device->SetDelegate(device);
573 /*----------------------------------------------------------------------
575 +---------------------------------------------------------------------*/
579 if (!m_ServerHolder->m_Device.IsNull()) return false;
581 // load upnpserver.xml
582 CStdString filename = URIUtils::AddFileToFolder(CProfilesManager::Get().GetUserDataFolder(), "upnpserver.xml");
583 CUPnPSettings::Get().Load(filename);
585 // create the server with a XBox compatible friendlyname and UUID from upnpserver.xml if found
586 m_ServerHolder->m_Device = CreateServer(CUPnPSettings::Get().GetServerPort());
589 NPT_Result res = m_UPnP->AddDevice(m_ServerHolder->m_Device);
590 if (NPT_FAILED(res)) {
591 // if the upnp device port was not 0, it could have failed because
592 // of port being in used, so restart with a random port
593 if (CUPnPSettings::Get().GetServerPort() > 0) m_ServerHolder->m_Device = CreateServer(0);
595 res = m_UPnP->AddDevice(m_ServerHolder->m_Device);
598 // save port but don't overwrite saved settings if port was random
599 if (NPT_SUCCEEDED(res)) {
600 if (CUPnPSettings::Get().GetServerPort() == 0) {
601 CUPnPSettings::Get().SetServerPort(m_ServerHolder->m_Device->GetPort());
603 CUPnPServer::m_MaxReturnedItems = UPNP_DEFAULT_MAX_RETURNED_ITEMS;
604 if (CUPnPSettings::Get().GetMaximumReturnedItems() > 0) {
605 // must be > UPNP_DEFAULT_MIN_RETURNED_ITEMS
606 CUPnPServer::m_MaxReturnedItems = max(UPNP_DEFAULT_MIN_RETURNED_ITEMS, CUPnPSettings::Get().GetMaximumReturnedItems());
608 CUPnPSettings::Get().SetMaximumReturnedItems(CUPnPServer::m_MaxReturnedItems);
612 CUPnPSettings::Get().SetServerUUID(m_ServerHolder->m_Device->GetUUID().GetChars());
613 return CUPnPSettings::Get().Save(filename);
616 /*----------------------------------------------------------------------
618 +---------------------------------------------------------------------*/
622 if (m_ServerHolder->m_Device.IsNull()) return;
624 m_UPnP->RemoveDevice(m_ServerHolder->m_Device);
625 m_ServerHolder->m_Device = NULL;
628 /*----------------------------------------------------------------------
629 | CUPnP::CreateRenderer
630 +---------------------------------------------------------------------*/
632 CUPnP::CreateRenderer(int port /* = 0 */)
634 CUPnPRenderer* device =
635 new CUPnPRenderer(g_infoManager.GetLabel(SYSTEM_FRIENDLY_NAME),
637 (CUPnPSettings::Get().GetRendererUUID().length() ? CUPnPSettings::Get().GetRendererUUID().c_str() : NULL),
640 device->m_PresentationURL =
642 CSettings::Get().GetInt("services.webserverport"),
644 device->m_ModelName = "XBMC Media Center";
645 device->m_ModelNumber = g_infoManager.GetVersion().c_str();
646 device->m_ModelDescription = "XBMC Media Center - Media Renderer";
647 device->m_ModelURL = "http://xbmc.org/";
648 device->m_Manufacturer = "Team XBMC";
649 device->m_ManufacturerURL = "http://xbmc.org/";
654 /*----------------------------------------------------------------------
655 | CUPnP::StartRenderer
656 +---------------------------------------------------------------------*/
657 bool CUPnP::StartRenderer()
659 if (!m_RendererHolder->m_Device.IsNull()) return false;
661 CStdString filename = URIUtils::AddFileToFolder(CProfilesManager::Get().GetUserDataFolder(), "upnpserver.xml");
662 CUPnPSettings::Get().Load(filename);
664 m_RendererHolder->m_Device = CreateRenderer(CUPnPSettings::Get().GetRendererPort());
666 NPT_Result res = m_UPnP->AddDevice(m_RendererHolder->m_Device);
668 // failed most likely because port is in use, try again with random port now
669 if (NPT_FAILED(res) && CUPnPSettings::Get().GetRendererPort() != 0) {
670 m_RendererHolder->m_Device = CreateRenderer(0);
672 res = m_UPnP->AddDevice(m_RendererHolder->m_Device);
675 // save port but don't overwrite saved settings if random
676 if (NPT_SUCCEEDED(res) && CUPnPSettings::Get().GetRendererPort() == 0) {
677 CUPnPSettings::Get().SetRendererPort(m_RendererHolder->m_Device->GetPort());
681 CUPnPSettings::Get().SetRendererUUID(m_RendererHolder->m_Device->GetUUID().GetChars());
682 return CUPnPSettings::Get().Save(filename);
685 /*----------------------------------------------------------------------
686 | CUPnP::StopRenderer
687 +---------------------------------------------------------------------*/
688 void CUPnP::StopRenderer()
690 if (m_RendererHolder->m_Device.IsNull()) return;
692 m_UPnP->RemoveDevice(m_RendererHolder->m_Device);
693 m_RendererHolder->m_Device = NULL;
696 /*----------------------------------------------------------------------
698 +---------------------------------------------------------------------*/
699 void CUPnP::UpdateState()
701 if (!m_RendererHolder->m_Device.IsNull())
702 ((CUPnPRenderer*)m_RendererHolder->m_Device.AsPointer())->UpdateState();
705 void CUPnP::RegisterUserdata(void* ptr)
707 NPT_AutoLock lock(g_UserDataLock);
711 void CUPnP::UnregisterUserdata(void* ptr)
713 NPT_AutoLock lock(g_UserDataLock);
714 g_UserData.Remove(ptr);
717 } /* namespace UPNP */