src/control/#mediaserver.hxx# to libupnpp/control/mediaserver.hxx
--- a/src/control/#mediaserver.hxx# +++ b/libupnpp/control/mediaserver.hxx @@ -41,11 +41,9 @@ const std::string& friendlyName = ""); static bool isMSDevice(const std::string& devicetype); - enum ServerKind {MSKIND_UNKNOWN, MSKIND_MINIM}: - protected: CDSH m_cds; - KnownType m_serverKindknownType; + static const std::string DType; };
src/control/avlastchg.cxx to libupnpp/control/avlastchg.cxx
File was renamed.
src/control/avlastchg.hxx to libupnpp/control/avlastchg.hxx
File was renamed.
src/control/avtransport.cxx to libupnpp/control/avtransport.cxx
File was renamed.
src/control/avtransport.hxx to libupnpp/control/avtransport.hxx
File was renamed.
src/control/cdircontent.cxx to libupnpp/control/cdircontent.cxx
File was renamed.
src/control/cdircontent.hxx to libupnpp/device/device.hxx
--- a/src/control/cdircontent.hxx +++ b/libupnpp/device/device.hxx @@ -1,218 +1,189 @@ -/* Copyright (C) 2013 J.F.Dockes - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. +/* Copyright (C) 2014 J.F.Dockes + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the - * Free Software Foundation, Inc., - * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the + * Free Software Foundation, Inc., + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ -#ifndef _UPNPDIRCONTENT_H_X_INCLUDED_ -#define _UPNPDIRCONTENT_H_X_INCLUDED_ +#ifndef _DEVICE_H_X_INCLUDED_ +#define _DEVICE_H_X_INCLUDED_ -#include <map> // for map, etc -#include <sstream> // for operator<<, basic_ostream, etc -#include <string> // for string, char_traits, etc -#include <utility> // for pair +#include <pthread.h> // for pthread_cond_t +#include <upnp/upnp.h> // for Upnp_EventType, etc + +#include <functional> // for function +#include <string> // for string +#include <unordered_map> // for unordered_map, etc #include <vector> // for vector -#include "libupnpp/upnpavutils.hxx" // for upnpdurationtos +#include "libupnpp/ptmutex.hxx" // for PTMutexInit +#include "libupnpp/soaphelp.hxx" // for SoapArgs, SoapData -namespace UPnPClient { +namespace UPnPP { class LibUPnP; } +namespace UPnPProvider { class UpnpService; } -/** - * UPnP resource. A resource describes one of the entities associated with - * a directory entry. This would be typically the audio file URI, and - * its characteristics (sample rate etc.) as attributes, but there can - * be several resources associated to one entry, for example for - * multiple encoding formats. +namespace UPnPProvider { + +typedef std::function<int (const UPnPP::SoapArgs&, UPnPP::SoapData&)> soapfun; + +// Definition of a virtual directory entry: data and mime type +struct VDirContent { + VDirContent(const std::string& ct, const std::string& mt) + : content(ct), mimetype(mt) {} + std::string content; + std::string mimetype; +}; + +/** Define a virtual interface to link libupnp operations to a device + * implementation */ -class UPnPResource { +class UpnpDevice { public: - // Value - std::string m_uri; - // Attributes - std::map<std::string, std::string> m_props; + /** Construct device object + * @param deviceId uuid for device: "uuid:UUIDvalue" + * @param files list of name/content pairs to be added to the + * virtual directory root. The file names must match the SCDPURL + * values for the services in the description.xml document. Note that + * this will have to be changed if we ever want to really + * support multiple devices (will need to use subdirectories or + * find another way to avoid name conflicts). + */ + UpnpDevice(const std::string& deviceId, + const std::unordered_map<std::string, VDirContent>& files); + ~UpnpDevice(); + + void addService(UpnpService *, const std::string& serviceId); + + /** + * Add mapping from service+action-name to handler function. + */ + void addActionMapping(const UpnpService*, + const std::string& actName, soapfun); + + /** + * Main routine. To be called by main() when done with initialization. + * + * This loop mostly polls getEventData and generates an UPnP event if + * there is anything to broadcast. The UPnP action calls happen in + * other threads with which we synchronize, currently using a global lock. + */ + void eventloop(); + + /** + * To be called from a service action callback to wake up the + * event loop early if something needs to be broadcast without + * waiting for the normal delay. + * + * Will only do something if the previous event is not too recent. + */ + void loopWakeup(); // To trigger an early event + + /** + * To be called to get the event loop to return + */ + void shouldExit(); + + /** Check status */ + bool ok() {return m_lib != 0;} + +private: + const std::string& serviceType(const std::string& serviceId); + + UPnPP::LibUPnP *m_lib; + std::string m_deviceId; + // We keep the services in a map for easy access from id and in a + // vector for ordered walking while fetching status. Order is + // determine by addService() call sequence. + std::unordered_map<std::string, UpnpService*> m_servicemap; + std::vector<std::string> m_serviceids; + std::unordered_map<std::string, soapfun> m_calls; + std::unordered_map<std::string, UpnpService*>::const_iterator findService(const std::string& serviceid); + + bool m_needExit; + /* My device handle */ + UpnpDevice_Handle m_dvh; + + /* Lock for device operations. Held during a service callback + Must not be held when using m_dvh to call into libupnp */ + UPnPP::PTMutexInit m_lock; + + pthread_cond_t m_evloopcond; + UPnPP::PTMutexInit m_evlooplock; + + /* Gets called when something needs doing */ + int callBack(Upnp_EventType et, void* evp); + + /** + * Generate event. + * + * Called by the device event loop, which polls the services. + * Use loopwakeup() to expedite things. + */ + void notifyEvent(const std::string& serviceId, + const std::vector<std::string>& names, + const std::vector<std::string>& values); + + + /** Static array of devices for dispatching */ + static std::unordered_map<std::string, UpnpDevice *> o_devices; + + /* Static callback for libupnp. This looks up the appropriate + * device using the device ID (UDN), the calls its callback + * method */ + static int sCallBack(Upnp_EventType et, void* evp, void*); }; /** - * UPnP Media Server directory entry, converted from XML data. - * - * This is a dumb data holder class, a struct with helpers. + * Upnp service implementation class. This does not do much useful beyond + * encapsulating the service actions and event callback. In most cases, the + * services will need full access to the device state anyway. */ -class UPnPDirObject { +class UpnpService { public: - enum ObjType {item, container}; - // There are actually several kinds of containers: - // object.container.storageFolder, object.container.person, - // object.container.playlistContainer etc., but they all seem to - // behave the same as far as we're concerned. Otoh, musicTrack - // items are special to us, and so should playlists, but I've not - // seen one of the latter yet (servers seem to use containers for - // playlists). - enum ItemClass {audioItem_musicTrack, audioItem_playlist}; - - std::string m_id; // ObjectId - std::string m_pid; // Parent ObjectId - std::string m_title; // dc:title. Directory name for a container. - ObjType m_type; // item or container - ItemClass m_iclass; - // Properties as gathered from the XML document (album, artist, etc.), - // except for title which has a proper field. - // The map keys are the XML tag names - std::map<std::string, std::string> m_props; - - // Resources: there may be several, for example for different - // audio formats of the same track, each with an URI and descriptor fields - std::vector<UPnPResource> m_resources; - - /** Get named property - * @param property name (e.g. upnp:artist, upnp:album, - * upnp:originalTrackNumber, upnp:genre). Use m_title instead - * for dc:title. - * @param[out] value - * @return true if found. - */ - bool getprop(const std::string& name, std::string& value) const - { - std::map<std::string, std::string>::const_iterator it = - m_props.find(name); - if (it == m_props.end()) - return false; - value = it->second; - return true; - } - - /** Get named property for resource - * Field names: "bitrate", "duration" (H:mm:ss.ms), "nrAudioChannels", - * "protocolInfo", "sampleFrequency" (Hz), "size" (bytes) - */ - bool getrprop(unsigned int ridx, const std::string& nm, std::string& val) - const - { - if (ridx >= m_resources.size()) - return false; - std::map<std::string, std::string>::const_iterator it = - m_resources[ridx].m_props.find(nm); - if (it == m_resources[ridx].m_props.end()) - return false; - val = it->second; - return true; - - } - - /** Simplified interface to retrieving values: we don't distinguish - * between non-existing and empty, and we only use the first ressource - */ - std::string f2s(const std::string& nm, bool isresfield) { - std::string val; - if (isresfield) { - getrprop(0, nm, val); - } else { - getprop(nm, val); + UpnpService(const std::string& stp,const std::string& sid, UpnpDevice *dev) + : m_serviceType(stp), m_serviceId(sid) + { + dev->addService(this, sid); } - return val; - } - - int getDurationSeconds(unsigned ridx = 0) const - { - std::string sdur; - if (!getrprop(ridx, "duration", sdur)) { - //?? Avoid returning 0, who knows... - return 1; - } - return UPnPP::upnpdurationtos(sdur); - } + virtual ~UpnpService() {} /** - * Get a DIDL document suitable for sending to a mediaserver. Only - * works for items, not containers. The idea is that we may have - * missed useful stuff while parsing the data from the content - * directory, so we send the original if we can. + * Poll to retrieve evented data changed since last call. + * + * To be implemented by the derived class. + * Called by the library when a control point subscribes, to + * retrieve eventable data. + * Return name/value pairs for changed variables in the data arrays. */ - std::string getdidl() const; + virtual bool getEventData(bool all, std::vector<std::string>& names, + std::vector<std::string>& values) + { + return true; + } - void clear() - { - m_id.clear(); - m_pid.clear(); - m_title.clear(); - m_type = (ObjType)-1; - m_iclass = (ItemClass)-1; - m_props.clear(); - m_resources.clear(); - m_didlfrag.clear(); - } + virtual const std::string& getServiceType() const + { + return m_serviceType; + } + virtual const std::string& getServiceId() const + { + return m_serviceId; + } - std::string dump() const - { - std::ostringstream os; - os << "UPnPDirObject: " << (m_type == item ? "item" : "container") << - " id [" << m_id << "] pid [" << m_pid << - "] title [" << m_title << "]" << std::endl; - os << "Properties: " << std::endl; - for (auto it = m_props.begin(); it != m_props.end(); it++) { - os << "[" << it->first << "]->[" << it->second << "] " - << std::endl; - } - os << "Resources:" << std::endl; - for (auto it = m_resources.begin(); it != m_resources.end(); it++) { - os << " Uri [" << it->m_uri << "]" << std::endl; - os << " Resource attributes:" << std::endl; - for (auto it1 = it->m_props.begin(); - it1 != it->m_props.end(); it1++) { - os << " [" << it1->first << "]->[" << it1->second << "] " - << std::endl; - } - } - os << std::endl; - return os.str(); - } - -private: - friend class UPnPDirParser; - // didl text for element, sans header - std::string m_didlfrag; +protected: + const std::string m_serviceType; + const std::string m_serviceId; }; -/** - * Image of a MediaServer Directory Service container (directory), - * possibly containing items and subordinate containers. - */ -class UPnPDirContent { -public: - std::vector<UPnPDirObject> m_containers; - std::vector<UPnPDirObject> m_items; +} // End namespace UPnPProvider - void clear() - { - m_containers.clear(); - m_items.clear(); - } - - /** - * Parse from DIDL-Lite XML data. - * - * Normally only used by ContentDirectory::readDir() - * This is cumulative: in general, the XML data is obtained in - * several documents corresponding to (offset,count) slices of the - * directory (container). parse() can be called repeatedly with - * the successive XML documents and will accumulate entries in the item - * and container vectors. This makes more sense if the different - * chunks are from the same container, but given that UPnP Ids are - * actually global, nothing really bad will happen if you mix - * up... - */ - bool parse(const std::string& didltext); -}; - -} // namespace - -#endif /* _UPNPDIRCONTENT_H_X_INCLUDED_ */ +#endif /* _DEVICE_H_X_INCLUDED_ */
src/control/cdirectory.cxx to libupnpp/control/ohplaylist.cxx
--- a/src/control/cdirectory.cxx +++ b/libupnpp/control/ohplaylist.cxx @@ -1,381 +1,413 @@ -/* Copyright (C) 2013 J.F.Dockes - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. +/* Copyright (C) 2014 J.F.Dockes + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the - * Free Software Foundation, Inc., - * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the + * Free Software Foundation, Inc., + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ -#include "config.h" - -#include "libupnpp/control/cdirectory.hxx" - -#include <sys/types.h> -#include <regex.h> - -#include <upnp/upnp.h> // for UPNP_E_SUCCESS, etc -#include <upnp/upnptools.h> // for UpnpGetErrorMessage - -#include <functional> // for _Bind, bind, _1, _2 -#include <iostream> // for operator<<, basic_ostream, etc -#include <set> // for set -#include <string> // for string, operator<<, etc +#include "libupnpp/control/ohplaylist.hxx" + +#include <expat_external.h> // for XML_Char +#include <netinet/in.h> // for ntohl +#include <stdlib.h> // for atoi +#include <string.h> // for strcmp +#include <upnp/upnp.h> // for UPNP_E_BAD_RESPONSE, etc + +#include <functional> // for _Bind, bind, _1 +#include <ostream> // for endl, basic_ostream, etc +#include <string> // for string, basic_string, etc +#include <utility> // for pair #include <vector> // for vector -#include "libupnpp/control/cdircontent.hxx" // for UPnPDirContent -#include "libupnpp/control/description.hxx" // for UPnPDeviceDesc, etc -#include "libupnpp/control/discovery.hxx" // for UPnPDeviceDirectory, etc -#include "libupnpp/log.hxx" // for LOGDEB, LOGINF, LOGERR -#include "libupnpp/soaphelp.hxx" // for SoapEncodeInput, SoapArgs, etc -#include "libupnpp/upnpp_p.hxx" // for csvToStrings +#include "libupnpp/base64.hxx" // for base64_decode +#include "libupnpp/control/cdircontent.hxx" // for UPnPDirContent, etc +#include "libupnpp/control/service.hxx" // for VarEventReporter, Service +#include "libupnpp/expatmm.hxx" // for inputRefXMLParser +#include "libupnpp/log.hxx" // for LOGERR, LOGDEB1, LOGINF +#include "libupnpp/soaphelp.hxx" // for SoapDecodeOutput, etc +#include "libupnpp/upnpp_p.hxx" // for stringToBool using namespace std; using namespace std::placeholders; +using namespace UPnPP; namespace UPnPClient { -// The service type string for Content Directories: -const string ContentDirectory::SType("urn:schemas-upnp-org:service:ContentDirectory:1"); - -class SimpleRegexp { -public: - SimpleRegexp(const string& exp, int flags) : m_ok(false) { - if (regcomp(&m_expr, exp.c_str(), flags) == 0) { - m_ok = true; - } - } - ~SimpleRegexp() { - regfree(&m_expr); - } - bool simpleMatch(const string& val) const { - if (!ok()) - return false; - if (regexec(&m_expr, val.c_str(), 0, 0, 0) == 0) { - return true; - } else { - return false; - } - } - bool operator() (const string& val) const { - return simpleMatch(val); - } - - bool ok() const {return m_ok;} -private: - bool m_ok; - regex_t m_expr; -}; - -/* - manufacturer: Bubblesoft model BubbleUPnP Media Server - manufacturer: Justin Maggard model Windows Media Connect compatible (MiniDLNA) - manufacturer: minimserver.com model MinimServer - manufacturer: PacketVideo model TwonkyMedia Server - manufacturer: ? model MediaTomb -*/ -static const SimpleRegexp bubble_rx("bubble", REG_ICASE|REG_NOSUB); -static const SimpleRegexp mediatomb_rx("mediatomb", REG_ICASE|REG_NOSUB); -static const SimpleRegexp minidlna_rx("minidlna", REG_ICASE|REG_NOSUB); -static const SimpleRegexp minim_rx("minim", REG_ICASE|REG_NOSUB); -static const SimpleRegexp twonky_rx("twonky", REG_ICASE|REG_NOSUB); - -ContentDirectory::ContentDirectory(const UPnPDeviceDesc& device, - const UPnPServiceDesc& service) - : Service(device, service), m_rdreqcnt(200), m_serviceKind(CDSKIND_UNKNOWN) -{ - LOGERR("ContentDirectory::ContentDirectory: manufacturer: " << - m_manufacturer << " model " << m_modelName << endl); - - if (bubble_rx(m_modelName)) { - m_serviceKind = CDSKIND_BUBBLE; - LOGDEB1("ContentDirectory::ContentDirectory: BUBBLE" << endl); - } else if (mediatomb_rx(m_modelName)) { - // Readdir by 200 entries is good for most, but MediaTomb likes - // them really big. Actually 1000 is better but I don't dare - m_rdreqcnt = 500; - m_serviceKind = CDSKIND_MEDIATOMB; - LOGDEB1("ContentDirectory::ContentDirectory: MEDIATOMB" << endl); - } else if (minidlna_rx(m_modelName)) { - m_serviceKind = CDSKIND_MINIDLNA; - LOGDEB1("ContentDirectory::ContentDirectory: MINIDLNA" << endl); - } else if (minim_rx(m_modelName)) { - m_serviceKind = CDSKIND_MINIM; - LOGDEB1("ContentDirectory::ContentDirectory: MINIM" << endl); - } else if (twonky_rx(m_modelName)) { - m_serviceKind = CDSKIND_TWONKY; - LOGDEB1("ContentDirectory::ContentDirectory: TWONKY" << endl); - } - registerCallback(); -} - -// We don't include a version in comparisons, as we are satisfied with -// version 1 -bool ContentDirectory::isCDService(const string& st) +const string OHPlaylist::SType("urn:av-openhome-org:service:Playlist:1"); + +// Check serviceType string (while walking the descriptions. We don't +// include a version in comparisons, as we are satisfied with version1 +bool OHPlaylist::isOHPlService(const string& st) { const string::size_type sz(SType.size()-2); return !SType.compare(0, sz, st, 0, sz); } -static bool DSAccum(vector<CDSH>* out, - const UPnPDeviceDesc& device, - const UPnPServiceDesc& service) -{ - if (ContentDirectory::isCDService(service.serviceType)) { - out->push_back(CDSH(new ContentDirectory(device, service))); - } - return true; -} - -bool ContentDirectory::getServices(vector<CDSH>& vds) -{ - //LOGDEB("UPnPDeviceDirectory::getDirServices" << endl); - UPnPDeviceDirectory::Visitor visitor = bind(DSAccum, &vds, _1, _2); - UPnPDeviceDirectory::getTheDir()->traverse(visitor); - return !vds.empty(); -} - -// Get server by friendly name. -bool ContentDirectory::getServerByName(const string& fname, CDSH& server) -{ - UPnPDeviceDesc ddesc; - bool found = UPnPDeviceDirectory::getTheDir()->getDevByFName(fname, ddesc); - if (!found) - return false; - - found = false; - for (auto it = ddesc.services.begin(); it != ddesc.services.end(); it++) { - if (isCDService(it->serviceType)) { - server = CDSH(new ContentDirectory(ddesc, *it)); - found = true; - break; +static int stringToTpState(const string& value, OHPlaylist::TPState *tpp) +{ + if (!value.compare("Buffering")) { + *tpp = OHPlaylist::TPS_Buffering; + return 0; + } else if (!value.compare("Paused")) { + *tpp = OHPlaylist::TPS_Paused; + return 0; + } else if (!value.compare("Playing")) { + *tpp = OHPlaylist::TPS_Playing; + return 0; + } else if (!value.compare("Stopped")) { + *tpp = OHPlaylist::TPS_Stopped; + return 0; + } + *tpp = OHPlaylist::TPS_Unknown; + return UPNP_E_BAD_RESPONSE; +} + +// Translate IdArray: base64-encoded array of binary msb 32bits integers +static void idArrayToVec(const string& _data, vector<int> *ids) +{ + string data = base64_decode(_data); + const char *cp = data.c_str(); + while (cp - data.c_str() <= int(data.size()) - 4) { + unsigned int *ip = (unsigned int *)cp; + ids->push_back(ntohl(*ip)); + cp += 4; + } +} + +void OHPlaylist::evtCallback( + const std::unordered_map<std::string, std::string>& props) +{ + LOGDEB1("OHPlaylist::evtCallback: m_reporter: " << m_reporter << endl); + for (auto it = props.begin(); it != props.end(); it++) { + if (!m_reporter) { + LOGDEB1("OHPlaylist::evtCallback: " << it->first << " -> " + << it->second << endl); + continue; } - } - return found; -} - -#if 0 -static int asyncReaddirCB(Upnp_EventType et, void *vev, void *cookie) -{ - LOGDEB("asyncReaddirCB: " << LibUPnP::evTypeAsString(et) << endl); - struct Upnp_Action_Complete *act = (struct Upnp_Action_Complete*)vev; - - LOGDEB("asyncReaddirCB: errcode " << act->ErrCode << - " cturl " << UpnpString_get_String(act->CtrlUrl) << - " actionrequest " << endl << - ixmlwPrintDoc(act->ActionRequest) << endl << - " actionresult " << ixmlwPrintDoc(act->ActionResult) << endl); - return 0; -} - int ret = - UpnpSendActionAsync(hdl, m_actionURL.c_str(), m_serviceType.c_str(), - 0 /*devUDN*/, request, asyncReaddirCB, 0); - sleep(10); - return -1; -#endif - -void ContentDirectory::evtCallback(const unordered_map<string, string>&) -{ -} - -void ContentDirectory::registerCallback() -{ - LOGDEB("ContentDirectory::registerCallback"<< endl); - Service::registerCallback(bind(&ContentDirectory::evtCallback, - this, _1)); -} - -int ContentDirectory::readDirSlice(const string& objectId, int offset, - int count, UPnPDirContent& dirbuf, - int *didreadp, int *totalp) -{ - LOGDEB("CDService::readDirSlice: objId [" << objectId << "] offset " << - offset << " count " << count << endl); - - // Create request - // Some devices require an empty SortCriteria, else bad params - SoapData args(m_serviceType, "Browse"); - args("ObjectID", objectId) - ("BrowseFlag", "BrowseDirectChildren") - ("Filter", "*") - ("SortCriteria", "") - ("StartingIndex", SoapHelp::i2s(offset)) - ("RequestedCount", SoapHelp::i2s(count)); - - SoapArgs data; - int ret = runAction(args, data); - if (ret != UPNP_E_SUCCESS) { - return ret; - } - int didread; - string tbuf; - if (!data.getInt("NumberReturned", &didread) || - !data.getInt("TotalMatches", totalp) || - !data.getString("Result", &tbuf)) { - LOGERR("CDService::readDir: missing elts in response" << endl); - return UPNP_E_BAD_RESPONSE; - } - - if (didread <= 0) { - LOGINF("CDService::readDir: got -1 or 0 entries" << endl); - return UPNP_E_BAD_RESPONSE; - } - -#if 0 - cerr << "CDService::readDirSlice: count " << count << - " offset " << offset << - " total " << *totalp << endl; - cerr << " result " << tbuf << endl; -#endif - - dirbuf.parse(tbuf); - *didreadp = didread; - - return UPNP_E_SUCCESS; -} - -int ContentDirectory::readDir(const string& objectId, - UPnPDirContent& dirbuf) -{ - LOGDEB("CDService::readDir: url [" << m_actionURL << "] type [" << - m_serviceType << "] udn [" << m_deviceId << "] objId [" << - objectId << endl); - - int offset = 0; - int total = 1000;// Updated on first read. - - while (offset < total) { - int count; - int error = readDirSlice(objectId, offset, m_rdreqcnt, dirbuf, - &count, &total); - if (error != UPNP_E_SUCCESS) - return error; - - offset += count; - } - - return UPNP_E_SUCCESS; -} - -int ContentDirectory::search(const string& objectId, - const string& ss, - UPnPDirContent& dirbuf) -{ - LOGDEB("CDService::search: url [" << m_actionURL << "] type [" << - m_serviceType << "] udn [" << m_deviceId << "] objid [" << - objectId << "] search [" << ss << "]" << endl); - - int offset = 0; - int total = 1000;// Updated on first read. - - while (offset < total) { - // Create request - SoapData args(m_serviceType, "Search"); - args("ContainerID", objectId) - ("SearchCriteria", ss) - ("Filter", "*") - ("SortCriteria", "") - ("StartingIndex", SoapHelp::i2s(offset)) - ("RequestedCount", "10"); - - SoapArgs data; - int ret = runAction(args, data); - - if (ret != UPNP_E_SUCCESS) { - LOGINF("CDService::search: UpnpSendAction failed: " << - UpnpGetErrorMessage(ret) << endl); - return ret; + + if (!it->first.compare("TransportState")) { + TPState tp; + stringToTpState(it->second, &tp); + m_reporter->changed(it->first.c_str(), int(tp)); + + } else if (!it->first.compare("ProtocolInfo")) { + m_reporter->changed(it->first.c_str(), + it->second.c_str()); + + } else if (!it->first.compare("Repeat") || + !it->first.compare("Shuffle")) { + bool val = false; + stringToBool(it->second, &val); + m_reporter->changed(it->first.c_str(), val ? 1 : 0); + + } else if (!it->first.compare("Id") || + !it->first.compare("TracksMax")) { + m_reporter->changed(it->first.c_str(), + atoi(it->second.c_str())); + + } else if (!it->first.compare("IdArray")) { + // Decode IdArray. See how we call the client + vector<int> v; + idArrayToVec(it->second, &v); + m_reporter->changed(it->first.c_str(), v); + + } else { + LOGERR("OHPlaylist event: unknown variable: name [" << + it->first << "] value [" << it->second << endl); + m_reporter->changed(it->first.c_str(), it->second.c_str()); } - - int count = -1; - string tbuf; - if (!data.getInt("NumberReturned", &count) || - !data.getInt("TotalMatches", &total) || - !data.getString("Result", &tbuf)) { - LOGERR("CDService::search: missing elts in response" << endl); - return UPNP_E_BAD_RESPONSE; + } +} + +void OHPlaylist::registerCallback() +{ + Service::registerCallback(bind(&OHPlaylist::evtCallback, this, _1)); +} + +int OHPlaylist::play() +{ + return runTrivialAction("Play"); +} +int OHPlaylist::pause() +{ + return runTrivialAction("Pause"); +} +int OHPlaylist::stop() +{ + return runTrivialAction("Stop"); +} +int OHPlaylist::next() +{ + return runTrivialAction("Next"); +} +int OHPlaylist::previous() +{ + return runTrivialAction("Previous"); +} +int OHPlaylist::setRepeat(bool onoff) +{ + return runSimpleAction("SetRepeat", "Value", onoff); +} +int OHPlaylist::repeat(bool *on) +{ + return runSimpleGet("Repeat", "Value", on); +} +int OHPlaylist::setShuffle(bool onoff) +{ + return runSimpleAction("SetShuffle", "Value", onoff); +} +int OHPlaylist::shuffle(bool *on) +{ + return runSimpleGet("Shuffle", "Value", on); +} +int OHPlaylist::seekSecondAbsolute(int value) +{ + return runSimpleAction("SeekSecondAbsolute", "Value", value); +} +int OHPlaylist::seekSecondRelative(int value) +{ + return runSimpleAction("SeekSecondRelative", "Value", value); +} +int OHPlaylist::seekId(int value) +{ + return runSimpleAction("SeekId", "Value", value); +} +int OHPlaylist::seekIndex(int value) +{ + return runSimpleAction("SeekIndex", "Value", value); +} + +int OHPlaylist::transportState(TPState* tpp) +{ + string value; + int ret; + + if ((ret = runSimpleGet("TransportState", "Value", &value))) + return ret; + + return stringToTpState(value, tpp); +} + +int OHPlaylist::id(int *value) +{ + return runSimpleGet("Id", "Value", value); +} + +int OHPlaylist::read(int id, std::string* urip, UPnPDirObject *dirent) +{ + SoapEncodeInput args(m_serviceType, "Read"); + args("Id", SoapHelp::i2s(id)); + SoapDecodeOutput data; + int ret = runAction(args, data); + if (ret != UPNP_E_SUCCESS) { + return ret; + } + if (!data.get("Uri", urip)) { + LOGERR("OHPlaylist::Read: missing Uri in response" << endl); + return UPNP_E_BAD_RESPONSE; + } + string didl; + if (!data.get("Metadata", &didl)) { + LOGERR("OHPlaylist::Read: missing Uri in response" << endl); + return UPNP_E_BAD_RESPONSE; + } + didl = SoapHelp::xmlUnquote(didl); + + UPnPDirContent dir; + if (!dir.parse(didl)) { + LOGERR("OHPlaylist::Read: didl parse failed: " << didl << endl); + return UPNP_E_BAD_RESPONSE; + } + if (dir.m_items.size() != 1) { + LOGERR("OHPlaylist::Read: " << dir.m_items.size() << " in response!" << + endl); + return UPNP_E_BAD_RESPONSE; + } + *dirent = dir.m_items[0]; + return 0; +} + +// Tracklist format +// <TrackList> +// <Entry> +// <Id>10</Id> +// <Uri>http://blabla</Uri> +// <Metadata>(xmlencoded didl)</Metadata> +// </Entry> +// </TrackList> + + +class OHTrackListParser : public inputRefXMLParser { +public: + OHTrackListParser(const string& input, + vector<OHPlaylist::TrackListEntry>* vp) + : inputRefXMLParser(input), m_v(vp) + { + //LOGDEB("OHTrackListParser: input: " << input << endl); } - if (count <= 0) { - LOGINF("CDService::search: got -1 or 0 entries" << endl); - return count < 0 ? UPNP_E_BAD_RESPONSE : UPNP_E_SUCCESS; + +protected: + virtual void StartElement(const XML_Char *name, const XML_Char **) { + m_path.push_back(name); + } + virtual void EndElement(const XML_Char *name) { + if (!strcmp(name, "Entry")) { + UPnPDirContent dir; + if (!dir.parse(m_tdidl)) { + LOGERR("OHPlaylist::ReadList: didl parse failed: " + << m_tdidl << endl); + return; + } + if (dir.m_items.size() != 1) { + LOGERR("OHPlaylist::ReadList: " << dir.m_items.size() + << " in response!" << endl); + return; + } + m_tt.dirent = dir.m_items[0]; + m_v->push_back(m_tt); + m_tt.clear(); + m_tdidl.clear(); } - offset += count; - - dirbuf.parse(tbuf); - } - - return UPNP_E_SUCCESS; -} - -int ContentDirectory::getSearchCapabilities(set<string>& result) -{ - LOGDEB("CDService::getSearchCapabilities:" << endl); - - SoapData args(m_serviceType, "GetSearchCapabilities"); - SoapArgs data; - int ret = runAction(args, data); - if (ret != UPNP_E_SUCCESS) { - LOGINF("CDService::getSearchCapa: UpnpSendAction failed: " << - UpnpGetErrorMessage(ret) << endl); - return ret; - } - string tbuf; - if (!data.getString("SearchCaps", &tbuf)) { - LOGERR("CDService::getSearchCaps: missing Result in response" << endl); - cerr << tbuf << endl; - return UPNP_E_BAD_RESPONSE; - } - - result.clear(); - if (!tbuf.compare("*")) { - result.insert(result.end(), "*"); - } else if (!tbuf.empty()) { - if (!csvToStrings(tbuf, result)) { - return UPNP_E_BAD_RESPONSE; - } - } - - return UPNP_E_SUCCESS; -} - -int ContentDirectory::getMetadata(const string& objectId, - UPnPDirContent& dirbuf) -{ - LOGDEB("CDService::getMetadata: url [" << m_actionURL << "] type [" << - m_serviceType << "] udn [" << m_deviceId << "] objId [" << - objectId << "]" << endl); - - SoapData args(m_serviceType, "Browse"); - SoapArgs data; - args("ObjectID", objectId) - ("BrowseFlag", "BrowseMetadata") - ("Filter", "*") - ("SortCriteria", "") - ("StartingIndex", "0") - ("RequestedCount", "1"); - int ret = runAction(args, data); - if (ret != UPNP_E_SUCCESS) { - LOGINF("CDService::getmetadata: UpnpSendAction failed: " << - UpnpGetErrorMessage(ret) << endl); - return ret; - } - string tbuf; - if (!data.getString("Result", &tbuf)) { - LOGERR("CDService::getmetadata: missing Result in response" << endl); - return UPNP_E_BAD_RESPONSE; - } - - if (dirbuf.parse(tbuf)) - return UPNP_E_SUCCESS; - else - return UPNP_E_BAD_RESPONSE; -} - -} // namespace UPnPClient + m_path.pop_back(); + } + virtual void CharacterData(const XML_Char *s, int len) { + if (s == 0 || *s == 0) + return; + string str(s, len); + if (!m_path.back().compare("Id")) + m_tt.id = atoi(str.c_str()); + else if (!m_path.back().compare("Uri")) + m_tt.url = str; + else if (!m_path.back().compare("Metadata")) + m_tdidl += str; + } + +private: + vector<OHPlaylist::TrackListEntry>* m_v; + std::vector<std::string> m_path; + OHPlaylist::TrackListEntry m_tt; + string m_tdidl; +}; + +int OHPlaylist::readList(const std::vector<int>& ids, + vector<TrackListEntry>* entsp) +{ + string idsparam; + for (auto it = ids.begin(); it != ids.end(); it++) { + idsparam += SoapHelp::i2s(*it) + " "; + } + entsp->clear(); + + SoapEncodeInput args(m_serviceType, "ReadList"); + args("IdList", idsparam); + SoapDecodeOutput data; + int ret = runAction(args, data); + if (ret != UPNP_E_SUCCESS) { + return ret; + } + string xml; + if (!data.get("TrackList", &xml)) { + LOGERR("OHPlaylist::readlist: missing TrackList in response" << endl); + return UPNP_E_BAD_RESPONSE; + } + OHTrackListParser mparser(xml, entsp); + if (!mparser.Parse()) + return UPNP_E_BAD_RESPONSE; + return 0; +} + +int OHPlaylist::insert(int afterid, const string& uri, const string& didl, + int *nid) +{ + SoapEncodeInput args(m_serviceType, "Insert"); + args("AfterId", SoapHelp::i2s(afterid)) + ("Uri", uri) + ("Metadata", didl); + SoapDecodeOutput data; + int ret = runAction(args, data); + if (ret != UPNP_E_SUCCESS) { + return ret; + } + if (!data.get("NewId", nid)) { + LOGERR("OHPlaylist::insert: missing Newid in response" << endl); + return UPNP_E_BAD_RESPONSE; + } + return 0; +} + + +int OHPlaylist::deleteId(int value) +{ + return runSimpleAction("DeleteId", "Value", value); +} +int OHPlaylist::deleteAll() +{ + return runTrivialAction("DeleteAll"); +} +int OHPlaylist::tracksMax(int *valuep) +{ + return runSimpleGet("TracksMax", "Value", valuep); +} + +int OHPlaylist::idArray(vector<int> *ids, int *tokp) +{ + SoapEncodeInput args(m_serviceType, "IdArray"); + SoapDecodeOutput data; + int ret = runAction(args, data); + if (ret != UPNP_E_SUCCESS) { + return ret; + } + if (!data.get("Token", tokp)) { + LOGERR("OHPlaylist::idArray: missing Token in response" << endl); + return UPNP_E_BAD_RESPONSE; + } + string arraydata; + if (!data.get("Array", &arraydata)) { + LOGINF("OHPlaylist::idArray: missing Array in response" << endl); + // We get this for an empty array ? This would need to be investigated + } + idArrayToVec(arraydata, ids); + return 0; +} + +int OHPlaylist::idArrayChanged(int token, bool *changed) +{ + SoapEncodeInput args(m_serviceType, "IdArrayChanged"); + args("Token", SoapHelp::i2s(token)); + SoapDecodeOutput data; + int ret = runAction(args, data); + if (ret != UPNP_E_SUCCESS) { + return ret; + } + if (!data.get("Value", changed)) { + LOGERR("OHPlaylist::idArrayChanged: missing Value in response" << endl); + return UPNP_E_BAD_RESPONSE; + } + return 0; +} + +int OHPlaylist::protocolInfo(std::string *proto) +{ + SoapEncodeInput args(m_serviceType, "ProtocolInfo"); + SoapDecodeOutput data; + int ret = runAction(args, data); + if (ret != UPNP_E_SUCCESS) { + return ret; + } + if (!data.get("Value", proto)) { + LOGERR("OHPlaylist::protocolInfo: missing Value in response" << endl); + return UPNP_E_BAD_RESPONSE; + } + return 0; +} + +} // End namespace UPnPClient
src/control/cdirectory.hxx to libupnpp/control/service.hxx
--- a/src/control/cdirectory.hxx +++ b/libupnpp/control/service.hxx @@ -1,4 +1,4 @@ -/* Copyright (C) 2013 J.F.Dockes +/* Copyright (C) 2014 J.F.Dockes * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or @@ -14,140 +14,156 @@ * Free Software Foundation, Inc., * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ -#ifndef _UPNPDIR_HXX_INCLUDED_ -#define _UPNPDIR_HXX_INCLUDED_ +#ifndef _SERVICE_H_X_INCLUDED_ +#define _SERVICE_H_X_INCLUDED_ -#include <memory> // for shared_ptr -#include <set> // for set -#include <string> // for string +#include <upnp/upnp.h> // for UPNP_E_BAD_RESPONSE, etc + +#include <functional> // for function +#include <iostream> // for basic_ostream, operator<<, etc +#include <string> // for string, operator<<, etc #include <unordered_map> // for unordered_map #include <vector> // for vector -#include "libupnpp/control/service.hxx" // for Service +#include "libupnpp/control/cdircontent.hxx" // for UPnPDirObject +#include "libupnpp/log.hxx" // for LOGERR +#include "libupnpp/soaphelp.hxx" // for SoapDecodeOutput, etc -namespace UPnPClient { class ContentDirectory; } // lines 30-30 namespace UPnPClient { class UPnPDeviceDesc; } -namespace UPnPClient { class UPnPDirContent; } namespace UPnPClient { class UPnPServiceDesc; } + +using namespace UPnPP; namespace UPnPClient { -typedef std::shared_ptr<ContentDirectory> CDSH; +/** To be implemented by upper-level client code for event + * reporting. Runs in an event thread. This could for example be + * implemented by a Qt Object to generate events for the GUI. + */ +class VarEventReporter { +public: + virtual ~VarEventReporter() {}; + // Using char * to avoid any issue with strings and concurrency + virtual void changed(const char *nm, int val) = 0; + virtual void changed(const char *nm, const char *val) = 0; + // Used for track metadata (parsed as content directory entry). Not always + // needed. + virtual void changed(const char */*nm*/, UPnPDirObject /*meta*/) {}; + // Used by ohplaylist. Not always needed + virtual void changed(const char */*nm*/, std::vector<int> /*ids*/) {}; +}; -/** - * Content Directory Service client class. - * - * This stores identity data from a directory service - * and the device it belongs to, and has methods to query - * the directory, using libupnp for handling the UPnP protocols. - * - * Note: m_rdreqcnt: number of entries requested per directory read. - * 0 means all entries. The device can still return less entries than - * requested, depending on its own limits. In general it's not optimal - * becauses it triggers issues, and is sometimes actually slower, e.g. on - * a D-Link NAS 327 - * - * The value chosen may affect by the UpnpSetMaxContentLength - * (2000*1024) done during initialization, but this should be ample - */ -class ContentDirectory : public Service { +typedef +std::function<void (const std::unordered_map<std::string, std::string>&)> +evtCBFunc; + +class Service { public: - - /** Construct by copying data from device and service objects. */ - ContentDirectory(const UPnPDeviceDesc& device, - const UPnPServiceDesc& service); + /** Construct by copying data from device and service objects. + */ + Service(const UPnPDeviceDesc& device, + const UPnPServiceDesc& service, bool doSubscribe = true); /** An empty one */ - ContentDirectory() : m_rdreqcnt(200), m_serviceKind(CDSKIND_UNKNOWN) {} + Service() : m_reporter(0) {} - enum ServiceKind {CDSKIND_UNKNOWN, CDSKIND_BUBBLE, CDSKIND_MEDIATOMB, - CDSKIND_MINIDLNA, CDSKIND_MINIM, CDSKIND_TWONKY}; + virtual ~Service(); - ServiceKind getKind() {return m_serviceKind;} + /** Retrieve my root device "friendly name". */ + std::string getFriendlyName() const {return m_friendlyName;} - /** Test service type from discovery message */ - static bool isCDService(const std::string& st); + /** Return my root device id */ + std::string getDeviceId() const {return m_deviceId;} - /** Retrieve the directory services currently seen on the network */ - static bool getServices(std::vector<CDSH>&); + virtual int runAction(const SoapEncodeInput& args, SoapDecodeOutput& data); - /** Retrieve specific service designated by its friendlyName */ - static bool getServerByName(const std::string& friendlyName, - CDSH& server); - - /** Read a full container's children list - * - * @param objectId the UPnP object Id for the container. Root has Id "0" - * @param[out] dirbuf stores the entries we read. - * @return UPNP_E_SUCCESS for success, else libupnp error code. - */ - int readDir(const std::string& objectId, UPnPDirContent& dirbuf); - - /** Read a partial slice of a container's children list - * - * The entries read are concatenated to the input dirbuf. - * - * @param objectId the UPnP object Id for the container. Root has Id "0" - * @param offset the offset of the first entry to read - * @param count the maximum number of entries to read - * @param[out] dirbuf a place to store the entries we read. They are - * appended to the existing ones. - * @param[out] didread number of entries actually read. - * @param[out] total total number of children. - * @return UPNP_E_SUCCESS for success, else libupnp error code. - */ - int readDirSlice(const std::string& objectId, int offset, - int count, UPnPDirContent& dirbuf, - int *didread, int *total); - - int goodSliceSize() + virtual VarEventReporter *getReporter() { - return m_rdreqcnt; + return m_reporter; } - /** Search the content directory service. - * - * @param objectId the UPnP object Id under which the search - * should be done. Not all servers actually support this below - * root. Root has Id "0" - * @param searchstring an UPnP searchcriteria string. Check the - * UPnP document: UPnP-av-ContentDirectory-v1-Service-20020625.pdf - * section 2.5.5. Maybe we'll provide an easier way some day... - * @param[out] dirbuf stores the entries we read. - * @return UPNP_E_SUCCESS for success, else libupnp error code. - */ - int search(const std::string& objectId, const std::string& searchstring, - UPnPDirContent& dirbuf); + virtual void installReporter(VarEventReporter* reporter) + { + m_reporter = reporter; + } - /** Read metadata for a given node. - * - * @param objectId the UPnP object Id. Root has Id "0" - * @param[out] dirbuf stores the entries we read. At most one entry will be - * returned. - * @return UPNP_E_SUCCESS for success, else libupnp error code. - */ - int getMetadata(const std::string& objectId, UPnPDirContent& dirbuf); - - /** Retrieve search capabilities - * - * @param[out] result an empty vector: no search, or a single '*' element: - * any tag can be used in a search, or a list of usable tag names. - * @return UPNP_E_SUCCESS for success, else libupnp error code. - */ - int getSearchCapabilities(std::set<std::string>& result); + // Can't copy these because this does not make sense for the + // member function callback. + Service(Service const&) = delete; + Service& operator=(Service const&) = delete; protected: - /* My service type string */ - static const std::string SType; + + /** Registered callbacks for the service objects. The map is + * indexed by m_SID, the subscription id which was obtained by + * each object when subscribing to receive the events for its + * device. The map allows the static function registered with + * libupnp to call the appropriate object method when it receives + * an event. */ + static std::unordered_map<std::string, evtCBFunc> o_calls; + + /** Used by a derived class to register its callback method. This + * creates an entry in the static map, using m_SID, which was + * obtained by subscribe() during construction + */ + void registerCallback(evtCBFunc c); + + /** Upper level client code event callbacks */ + VarEventReporter *m_reporter; + + std::string m_actionURL; + std::string m_eventURL; + std::string m_serviceType; + std::string m_deviceId; + std::string m_friendlyName; + std::string m_manufacturer; + std::string m_modelName; + + int runTrivialAction(const std::string& nm) { + SoapEncodeInput args(m_serviceType, nm); + SoapDecodeOutput data; + return runAction(args, data); + } + + template <class T> int runSimpleGet(const std::string& actnm, + const std::string& valnm, + T *valuep) { + SoapEncodeInput args(m_serviceType, actnm); + SoapDecodeOutput data; + int ret = runAction(args, data); + if (ret != UPNP_E_SUCCESS) { + return ret; + } + if (!data.get(valnm.c_str(), valuep)) { + LOGERR("Service::runSimpleAction: " << actnm << + " missing " << valnm << " in response" << std::endl); + return UPNP_E_BAD_RESPONSE; + } + return 0; + } + template <class T> int runSimpleAction(const std::string& actnm, + const std::string& valnm, + T value) { + SoapEncodeInput args(m_serviceType, actnm); + args(valnm, SoapHelp::val2s(value)); + SoapDecodeOutput data; + return runAction(args, data); + } private: - int m_rdreqcnt; // Slice size to use when reading - ServiceKind m_serviceKind; + /** Only actually does something on the first call, to register our + * (static) library callback */ + static bool initEvents(); + /** The static event callback given to libupnp */ + static int srvCB(Upnp_EventType et, void* vevp, void*); + /* Tell the UPnP device (through libupnp) that we want to receive + its events. This is called during construction and sets m_SID */ + virtual bool subscribe(); + virtual bool unSubscribe(); - void evtCallback(const std::unordered_map<std::string, std::string>&); - void registerCallback(); + Upnp_SID m_SID; /* Subscription Id */ }; -} +} // namespace UPnPClient -#endif /* _UPNPDIR_HXX_INCLUDED_ */ +#endif /* _SERVICE_H_X_INCLUDED_ */
src/control/description.cxx to libupnpp/control/description.cxx
File was renamed.
src/control/description.hxx to libupnpp/control/description.hxx
File was renamed.
src/control/device.hxx to libupnpp/control/device.hxx
File was renamed.
src/control/discovery.cxx to libupnpp/device/device.cxx
--- a/src/control/discovery.cxx +++ b/libupnpp/device/device.cxx @@ -1,479 +1,450 @@ -/* Copyright (C) 2013 J.F.Dockes - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. +/* Copyright (C) 2014 J.F.Dockes + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the - * Free Software Foundation, Inc., - * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the + * Free Software Foundation, Inc., + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ #include "config.h" -#include "discovery.hxx" - -#include <pthread.h> // for pthread_cond_broadcast, etc -#include <sched.h> // for sched_yield -#include <stdlib.h> // for free +#include "device.hxx" + +#include <errno.h> // for ETIMEDOUT, errno #include <sys/time.h> // for CLOCK_REALTIME -#include <unistd.h> // for sleep -#include <upnp/upnp.h> // for Upnp_Discovery, etc - -#include <functional> // for _Bind, bind, function, _1, etc -#include <iostream> // for operator<<, basic_ostream, etc -#include <map> // for _Rb_tree_iterator, map, etc +#include <time.h> // for timespec, clock_gettime + +#include <iostream> // for endl, operator<<, etc #include <utility> // for pair -#include <vector> // for vector -#include <unordered_set> - -#include "description.hxx" // for UPnPDeviceDesc, etc - -#include "libupnpp/log.hxx" // for LOGDEB1, LOGERR, LOGDEB -#include "libupnpp/ptmutex.hxx" // for PTMutexLocker, PTMutexInit + +#include "libupnpp/log.hxx" // for LOGERR, LOGFAT, LOGDEB, etc +#include "libupnpp/ixmlwrap.hxx" #include "libupnpp/upnpplib.hxx" // for LibUPnP #include "libupnpp/upnpputils.hxx" // for timespec_addnanos -#include "libupnpp/workqueue.hxx" // for WorkQueue -#include "libupnpp/control/httpdownload.hxx" + +#include "vdir.hxx" // for VirtualDir using namespace std; -using namespace std::placeholders; using namespace UPnPP; -namespace UPnPClient { - -static UPnPDeviceDirectory *theDevDir; - -//#undef LOCAL_LOGINC -//#define LOCAL_LOGINC 3 - -static string cluDiscoveryToStr(const struct Upnp_Discovery *disco) -{ - stringstream ss; - ss << "ErrCode: " << disco->ErrCode << endl; - ss << "Expires: " << disco->Expires << endl; - ss << "DeviceId: " << disco->DeviceId << endl; - ss << "DeviceType: " << disco->DeviceType << endl; - ss << "ServiceType: " << disco->ServiceType << endl; - ss << "ServiceVer: " << disco->ServiceVer << endl; - ss << "Location: " << disco->Location << endl; - ss << "Os: " << disco->Os << endl; - ss << "Date: " << disco->Date << endl; - ss << "Ext: " << disco->Ext << endl; - - /** The host address of the device responding to the search. */ - // struct sockaddr_storage DestAddr; - return ss.str(); -} - -// Each appropriate discovery event (executing in a libupnp thread -// context) queues the following task object for processing by the -// discovery thread. -class DiscoveredTask { -public: - DiscoveredTask(bool _alive, const struct Upnp_Discovery *disco) - : alive(_alive), url(disco->Location), deviceId(disco->DeviceId), - expires(disco->Expires) - {} - - bool alive; - string url; - string description; - string deviceId; - int expires; // Seconds valid -}; - -// The workqueue on which callbacks from libupnp (cluCallBack()) queue -// discovered object descriptors for processing by our dedicated -// thread. -static WorkQueue<DiscoveredTask*> discoveredQueue("DiscoveredQueue"); -static unordered_set<string> o_downloading; -static PTMutexInit o_downloading_mutex; - -// This gets called in a libupnp thread context for all asynchronous -// events which we asked for. -// Example: ContentDirectories appearing and disappearing from the network -// We queue a task for our worker thread(s) -// We can get called by several threads. -static int cluCallBack(Upnp_EventType et, void* evp, void*) -{ - LOGDEB1("discovery:cluCallBack: " << LibUPnP::evTypeAsString(et) << endl); - +namespace UPnPProvider { + +unordered_map<std::string, UpnpDevice *> UpnpDevice::o_devices; +PTMutexInit o_devices_lock; + +static bool vectorstoargslists(const vector<string>& names, + const vector<string>& values, + vector<string>& qvalues, + vector<const char *>& cnames, + vector<const char *>& cvalues) +{ + if (names.size() != values.size()) { + LOGERR("vectorstoargslists: bad sizes" << endl); + return false; + } + + cnames.reserve(names.size()); + qvalues.clear(); + qvalues.reserve(values.size()); + cvalues.reserve(values.size()); + for (unsigned int i = 0; i < values.size(); i++) { + cnames.push_back(names[i].c_str()); + qvalues.push_back(SoapHelp::xmlQuote(values[i])); + cvalues.push_back(qvalues[i].c_str()); + //LOGDEB("Edata: " << cnames[i] << " -> " << cvalues[i] << endl); + } + return true; +} + +static const int expiretime = 3600; + +UpnpDevice::UpnpDevice(const string& deviceId, + const unordered_map<string, VDirContent>& files) + : m_deviceId(deviceId), m_needExit(false), m_evloopcond(PTHREAD_COND_INITIALIZER) +{ + //LOGDEB("UpnpDevice::UpnpDevice(" << m_deviceId << ")" << endl); + + m_lib = LibUPnP::getLibUPnP(true); + if (!m_lib) { + LOGFAT(" Can't get LibUPnP" << endl); + return; + } + if (!m_lib->ok()) { + LOGFAT("Lib init failed: " << + m_lib->errAsString("main", m_lib->getInitError()) << endl); + m_lib = 0; + return; + } + + { + PTMutexLocker lock(o_devices_lock); + if (o_devices.empty()) { + // First call: init callbacks + m_lib->registerHandler(UPNP_CONTROL_ACTION_REQUEST, sCallBack, this); + m_lib->registerHandler(UPNP_CONTROL_GET_VAR_REQUEST, sCallBack, this); + m_lib->registerHandler(UPNP_EVENT_SUBSCRIPTION_REQUEST, sCallBack,this); + } + o_devices[m_deviceId] = this; + } + + VirtualDir* theVD = VirtualDir::getVirtualDir(); + if (theVD == 0) { + LOGFAT("UpnpDevice::UpnpDevice: can't get VirtualDir" << endl); + return; + } + + unordered_map<string, VDirContent>::const_iterator it = + files.find("description.xml"); + if (it == files.end()) { + LOGFAT("UpnpDevice::UpnpDevice: no description.xml found in xmlfiles" + << endl); + return; + } + + const string& description = it->second.content; + + for (it = files.begin(); it != files.end(); it++) { + theVD->addFile("/", it->first, it->second.content, it->second.mimetype); + } + + // Start up the web server for sending out description files + int ret; + if ((ret = m_lib->setupWebServer(description, &m_dvh)) != 0) { + LOGFAT("UpnpDevice: libupnp can't start service. Err " << ret << endl); + } + + if ((ret = UpnpSendAdvertisement(m_dvh, expiretime)) != 0) { + LOGERR(m_lib->errAsString("UpnpDevice: UpnpSendAdvertisement", ret) + << endl); + } +} + +UpnpDevice::~UpnpDevice() +{ + UpnpUnRegisterRootDevice(m_dvh); + + PTMutexLocker lock(o_devices_lock); + unordered_map<std::string, UpnpDevice *>::iterator it = o_devices.find(m_deviceId); + if (it != o_devices.end()) + o_devices.erase(it); +} + +// Main libupnp callback: use the device id and call the right device +int UpnpDevice::sCallBack(Upnp_EventType et, void* evp, void* tok) +{ + //LOGDEB("UpnpDevice::sCallBack" << endl); + + string deviceid; switch (et) { - case UPNP_DISCOVERY_SEARCH_RESULT: - case UPNP_DISCOVERY_ADVERTISEMENT_ALIVE: + case UPNP_CONTROL_ACTION_REQUEST: + deviceid = ((struct Upnp_Action_Request *)evp)->DevUDN; + break; + + case UPNP_CONTROL_GET_VAR_REQUEST: + deviceid = ((struct Upnp_State_Var_Request *)evp)->DevUDN; + break; + + case UPNP_EVENT_SUBSCRIPTION_REQUEST: + deviceid = ((struct Upnp_Subscription_Request*)evp)->UDN; + break; + + default: + LOGERR("UpnpDevice::sCallBack: unknown event " << et << endl); + return UPNP_E_INVALID_PARAM; + } + // LOGDEB("UpnpDevice::sCallBack: deviceid[" << deviceid << "]" << endl); + + unordered_map<std::string, UpnpDevice *>::iterator it; { - struct Upnp_Discovery *disco = (struct Upnp_Discovery *)evp; - // Devices send multiple messages for themselves, their subdevices and - // services. AFAIK they all point to the same description.xml document, - // which has all the interesting data. So let's try to only process - // one message per device: the one which probably correspond to the - // upnp "root device" message and has empty service and device types: - if (!disco->DeviceType[0] && !disco->ServiceType[0]) { - LOGDEB1("discovery:cllb:ALIVE: " << cluDiscoveryToStr(disco) - << endl); - // Device signals its existence and well-being. Perform the - // UPnP "description" phase by downloading and decoding the - // description document. - - DiscoveredTask *tp = new DiscoveredTask(1, disco); - + PTMutexLocker lock(o_devices_lock); + + it = o_devices.find(deviceid); + + if (it == o_devices.end()) { + LOGERR("UpnpDevice::sCallBack: Device not found: [" << + deviceid << "]" << endl); + return UPNP_E_INVALID_PARAM; + } + } + + // LOGDEB("UpnpDevice::sCallBack: device found: [" << it->second + // << "]" << endl); + return (it->second)->callBack(et, evp); +} + +unordered_map<string, UpnpService*>::const_iterator UpnpDevice::findService(const string& serviceid) +{ + PTMutexLocker lock(m_lock); + auto servit = m_servicemap.find(serviceid); + if (servit == m_servicemap.end()) { + LOGERR("UpnpDevice: Bad serviceID: " << serviceid << endl); + } + return servit; +} + +int UpnpDevice::callBack(Upnp_EventType et, void* evp) +{ + switch (et) { + case UPNP_CONTROL_ACTION_REQUEST: + { + struct Upnp_Action_Request *act = (struct Upnp_Action_Request *)evp; + + LOGDEB("UPNP_CONTROL_ACTION_REQUEST: " << act->ActionName << + ". Params: " << ixmlwPrintDoc(act->ActionRequest) << endl); + + auto servit = findService(act->ServiceID); + if (servit == m_servicemap.end()) { + return UPNP_E_INVALID_PARAM; + } + + SoapData dt; + { + PTMutexLocker lock(m_lock); + const string& servicetype = servit->second->getServiceType(); + auto callit = m_calls.find(string(act->ActionName) + string(act->ServiceID)); + if (callit == m_calls.end()) { + LOGINF("UpnpDevice: No such action: " << act->ActionName << endl); + return UPNP_E_INVALID_PARAM; + } + + SoapArgs sc; + if (!decodeSoapBody(act->ActionName, act->ActionRequest, &sc)) { + LOGERR("Error decoding Action call arguments" << endl); + return UPNP_E_INVALID_PARAM; + } + dt.name = act->ActionName; + dt.serviceType = servicetype; + + // Call the action routine + int ret = callit->second(sc, dt); + if (ret != UPNP_E_SUCCESS) { + LOGERR("UpnpDevice: Action failed: " << sc.name << endl); + return ret; + } + } + + // Encode result data + act->ActionResult = buildSoapBody(dt); + + //LOGDEB("Response data: " << ixmlwPrintDoc(act->ActionResult) << endl); + + return UPNP_E_SUCCESS; + } + break; + + case UPNP_CONTROL_GET_VAR_REQUEST: + // Note that the "Control: query for variable" action is + // deprecated (upnp arch v1), and we should never get these. + { + struct Upnp_State_Var_Request *act = + (struct Upnp_State_Var_Request *)evp; + LOGDEB("UPNP_CONTROL_GET_VAR__REQUEST?: " << act->StateVarName << endl); + } + break; + + case UPNP_EVENT_SUBSCRIPTION_REQUEST: + { + struct Upnp_Subscription_Request *act = + (struct Upnp_Subscription_Request*)evp; + LOGDEB("UPNP_EVENT_SUBSCRIPTION_REQUEST: " << act->ServiceId << endl); + + auto servit = findService(act->ServiceId); + if (servit == m_servicemap.end()) { + return UPNP_E_INVALID_PARAM; + } + + vector<string> names, values, qvalues; + { + PTMutexLocker lock(m_lock); + if (!servit->second->getEventData(true, names, values)) { + break; + } + } + + vector<const char *> cnames, cvalues; + vectorstoargslists(names, values, qvalues, cnames, cvalues); + int ret = + UpnpAcceptSubscription(m_dvh, act->UDN, act->ServiceId, + &cnames[0], &cvalues[0], + int(cnames.size()), act->Sid); + if (ret != UPNP_E_SUCCESS) { + LOGERR(m_lib->errAsString( + "UpnpDevice::callBack: UpnpAcceptSubscription", ret) << + endl); + } + + return ret; + } + break; + + default: + LOGINF("UpnpDevice::callBack: unknown libupnp event type: " << + LibUPnP::evTypeAsString(et).c_str() << endl); + return UPNP_E_INVALID_PARAM; + } + return UPNP_E_INVALID_PARAM; +} + +void UpnpDevice::addService(UpnpService *serv, const std::string& serviceId) +{ + PTMutexLocker lock(m_lock); + m_servicemap[serviceId] = serv; + m_serviceids.push_back(serviceId); +} + +void UpnpDevice::addActionMapping(const UpnpService* serv, + const std::string& actName, + soapfun fun) +{ + PTMutexLocker lock(m_lock); + // LOGDEB("UpnpDevice::addActionMapping:" << actName << endl); + m_calls[actName + serv->getServiceId()] = fun; +} + +void UpnpDevice::notifyEvent(const string& serviceId, + const vector<string>& names, + const vector<string>& values) +{ + LOGDEB1("UpnpDevice::notifyEvent " << serviceId << " " << + (names.empty()?"Empty names??":names[0]) << endl); + if (names.empty()) + return; + vector<const char *> cnames, cvalues; + vector<string> qvalues; + vectorstoargslists(names, values, qvalues, cnames, cvalues); + + int ret = UpnpNotify(m_dvh, m_deviceId.c_str(), + serviceId.c_str(), &cnames[0], &cvalues[0], + int(cnames.size())); + if (ret != UPNP_E_SUCCESS) { + LOGERR(m_lib->errAsString("UpnpDevice::notifyEvent", ret) << endl); + } +} + +int timespec_diffms(const struct timespec& old, const struct timespec& recent) +{ + return (recent.tv_sec - old.tv_sec) * 1000 + + (recent.tv_nsec - old.tv_nsec) / (1000 * 1000); +} + +#ifndef CLOCK_REALTIME +// Mac OS X for one does not have clock_gettime. Gettimeofday is more than +// enough for our needs. +#define CLOCK_REALTIME 0 +int clock_gettime(int /*clk_id*/, struct timespec* t) { + struct timeval now; + int rv = gettimeofday(&now, NULL); + if (rv) return rv; + t->tv_sec = now.tv_sec; + t->tv_nsec = now.tv_usec * 1000; + return 0; +} +#endif // ! CLOCK_REALTIME + +// Loop on services, and poll each for changed data. Generate event +// only if changed data exists. Every now and then, we generate an +// artificial event with all the current state. This is normally run by the main thread. +void UpnpDevice::eventloop() +{ + int count = 0; + const int loopwait_ms = 1000; // Polling the services every 1 S + const int nloopstofull = 10; // Full state every 10 S + struct timespec wkuptime, earlytime; + bool didearly = false; + + for (;;) { + clock_gettime(CLOCK_REALTIME, &wkuptime); + + timespec_addnanos(&wkuptime, loopwait_ms * 1000 * 1000); + + //LOGDEB("eventloop: now " << time(0) << " wkup at "<< + // wkuptime.tv_sec << " S " << wkuptime.tv_nsec << " ns" << endl); + + PTMutexLocker lock(m_evlooplock); + int err = pthread_cond_timedwait(&m_evloopcond, lock.getMutex(), + &wkuptime); + if (m_needExit) { + break; + } else if (err && err != ETIMEDOUT) { + LOGINF("UpnpDevice:eventloop: wait errno " << errno << endl); + break; + } else if (err == 0) { + // Early wakeup. Only does something if it did not already + // happen recently + if (didearly) { + int millis = timespec_diffms(earlytime, wkuptime); + if (millis < loopwait_ms) { + // Do nothing. didearly stays true + // LOGDEB("eventloop: early, previous too close "<<endl); + continue; + } else { + // had an early wakeup previously, but it was a + // long time ago. Update state and wakeup + // LOGDEB("eventloop: early, previous is old "<<endl); + earlytime = wkuptime; + } + } else { + // Early wakeup, previous one was normal. Remember. + // LOGDEB("eventloop: early, no previous" << endl); + didearly = true; + earlytime = wkuptime; + } + } else { + // Normal wakeup + // LOGDEB("eventloop: normal wakeup" << endl); + didearly = false; + } + + count++; + bool all = count && ((count % nloopstofull) == 0); + //LOGDEB("UpnpDevice::eventloop count "<<count<<" all "<<all<<endl); + + // We can't lock m_lock around the loop because we don't want + // to hold id when calling notifyEvent() (which calls + // libupnp). This means that we should have a separate lock + // for the services arrays. This would only be useful during + // startup, while we add services, but the event loop is the + // last call the main program will make after adding the + // services, so locking does not seem necessary + for (auto it = m_serviceids.begin(); it != m_serviceids.end(); it++) { + vector<string> names, values; { - // Note that this does not prevent multiple successive - // downloads of a normal url, just multiple - // simultaneous downloads of a slow one, to avoid - // tying up threads. - PTMutexLocker lock(o_downloading_mutex); - auto res = o_downloading.insert(tp->url); - if (!res.second) { - LOGDEB("discovery:cllb: already downloading " << - tp->url << endl); - return UPNP_E_SUCCESS; + PTMutexLocker lock(m_lock); + UpnpService* serv = m_servicemap[*it]; + if (!serv->getEventData(all, names, values) || names.empty()) { + continue; } } - - LOGDEB("discoExplorer: downloading " << tp->url << endl); - string sdesc; - if (!downloadUrlWithCurl(tp->url, tp->description, 5)) { - LOGERR("discovery:cllb: downloadUrlWithCurl error for: " << - tp->url << endl); - {PTMutexLocker lock(o_downloading_mutex); - o_downloading.erase(tp->url); - } - delete tp; - return UPNP_E_SUCCESS; - } - LOGDEB1("discovery:cllb: downloaded description document of " << - tp->description.size() << " bytes" << endl); - - {PTMutexLocker lock(o_downloading_mutex); - o_downloading.erase(tp->url); - } - - if (discoveredQueue.put(tp)) { - return UPNP_E_FINISH; - } - } - break; - } - case UPNP_DISCOVERY_ADVERTISEMENT_BYEBYE: - { - struct Upnp_Discovery *disco = (struct Upnp_Discovery *)evp; - //LOGDEB("discovery:cllB:BYEBYE: " << cluDiscoveryToStr(disco) << endl); - DiscoveredTask *tp = new DiscoveredTask(0, disco); - if (discoveredQueue.put(tp)) { - return UPNP_E_FINISH; - } - break; - } - default: - // Ignore other events for now - LOGDEB("discovery:cluCallBack: unprocessed evt type: [" << - LibUPnP::evTypeAsString(et) << "]" << endl); - break; - } - - return UPNP_E_SUCCESS; -} - -// Our client can set up functions to be called when we process a new device. -// This is used during startup, when the pool is not yet complete, to enable -// finding and listing devices as soon as they appear. -static vector<UPnPDeviceDirectory::Visitor> o_callbacks; -static PTMutexInit o_callbacks_mutex; - -unsigned int UPnPDeviceDirectory::addCallback(UPnPDeviceDirectory::Visitor v) -{ - PTMutexLocker lock(o_callbacks_mutex); - o_callbacks.push_back(v); - return o_callbacks.size() - 1; -} - -void UPnPDeviceDirectory::delCallback(unsigned int idx) -{ - PTMutexLocker lock(o_callbacks_mutex); - if (idx >= o_callbacks.size()) - return; - o_callbacks.erase(o_callbacks.begin() + idx); -} - -// Descriptor kept in the device pool for each device found on the network. -class DeviceDescriptor { -public: - DeviceDescriptor(const string& url, const string& description, - time_t last, int exp) - : device(url, description), last_seen(last), expires(exp+20) - {} - DeviceDescriptor() - {} - UPnPDeviceDesc device; - time_t last_seen; - int expires; // seconds valid -}; - -// A DevicePool holds the characteristics of the devices -// currently on the network. -// The map is referenced by deviceId (==UDN) -// The class is instanciated as a static (unenforced) singleton. -class DevicePool { -public: - PTMutexInit m_mutex; - map<string, DeviceDescriptor> m_devices; -}; -static DevicePool o_pool; -typedef map<string, DeviceDescriptor>::iterator DevPoolIt; - -// Worker routine for the discovery queue. Get messages about devices -// appearing and disappearing, and update the directory pool -// accordingly. -void *UPnPDeviceDirectory::discoExplorer(void *) -{ - for (;;) { - DiscoveredTask *tsk = 0; - size_t qsz; - if (!discoveredQueue.take(&tsk, &qsz)) { - discoveredQueue.workerExit(); - return (void*)1; - } - LOGDEB1("discoExplorer: got task: alive " << tsk->alive << " deviceId [" - << tsk->deviceId << " URL [" << tsk->url << "]" << endl); - - if (!tsk->alive) { - // Device signals it is going off. - PTMutexLocker lock(o_pool.m_mutex); - DevPoolIt it = o_pool.m_devices.find(tsk->deviceId); - if (it != o_pool.m_devices.end()) { - o_pool.m_devices.erase(it); - //LOGDEB("discoExplorer: delete " << tsk->deviceId.c_str() << - // endl); - } - } else { - // Update or insert the device - DeviceDescriptor d(tsk->url, tsk->description, time(0), tsk->expires); - if (!d.device.ok) { - LOGERR("discoExplorer: description parse failed for " << - tsk->deviceId << endl); - delete tsk; - continue; - } - LOGDEB1("discoExplorer: found id [" << tsk->deviceId << "]" - << " name " << d.device.friendlyName - << " devtype " << d.device.deviceType << endl); - { - PTMutexLocker lock(o_pool.m_mutex); - //LOGDEB1("discoExplorer: inserting device id "<< tsk->deviceId - // << " description: " << endl << d.device.dump() << endl); - o_pool.m_devices[tsk->deviceId] = d; - } - { - PTMutexLocker lock(o_callbacks_mutex); - for (auto cbp = o_callbacks.begin(); - cbp != o_callbacks.end(); cbp++) { - (*cbp)(d.device, UPnPServiceDesc()); - } - } - } - delete tsk; - } -} - -// Look at the devices and get rid of those which have not been seen -// for too long. We do this when listing the top directory -void UPnPDeviceDirectory::expireDevices() -{ - LOGDEB1("discovery: expireDevices:" << endl); - PTMutexLocker lock(o_pool.m_mutex); - time_t now = time(0); - bool didsomething = false; - - for (DevPoolIt it = o_pool.m_devices.begin(); - it != o_pool.m_devices.end();) { - LOGDEB1("Dev in pool: type: " << it->second.device.deviceType << - " friendlyName " << it->second.device.friendlyName << endl); - if (now - it->second.last_seen > it->second.expires) { - //LOGDEB("expireDevices: deleting " << it->first.c_str() << " " << - // it->second.device.friendlyName.c_str() << endl); - o_pool.m_devices.erase(it++); - didsomething = true; - } else { - it++; - } - } - if (didsomething) - search(); -} - -// m_searchTimeout is the UPnP device search timeout, which should -// actually be called delay because it's the base of a random delay -// that the devices apply to avoid responding all at the same time. -// This means that you have to wait for the specified period before -// the results are complete. -UPnPDeviceDirectory::UPnPDeviceDirectory(time_t search_window) - : m_ok(false), m_searchTimeout(search_window), m_lastSearch(0) -{ - addCallback(std::bind(&UPnPDeviceDirectory::deviceFound, this, _1, _2)); - - if (!discoveredQueue.start(1, discoExplorer, 0)) { - m_reason = "Discover work queue start failed"; - return; - } - sched_yield(); - LibUPnP *lib = LibUPnP::getLibUPnP(); - if (lib == 0) { - m_reason = "Can't get lib"; - return; - } - lib->registerHandler(UPNP_DISCOVERY_SEARCH_RESULT, cluCallBack, this); - lib->registerHandler(UPNP_DISCOVERY_ADVERTISEMENT_ALIVE, - cluCallBack, this); - lib->registerHandler(UPNP_DISCOVERY_ADVERTISEMENT_BYEBYE, - cluCallBack, this); - - m_ok = search(); -} - -bool UPnPDeviceDirectory::search() -{ - LOGDEB1("UPnPDeviceDirectory::search" << endl); - if (time(0) - m_lastSearch < 10) - return true; - - LibUPnP *lib = LibUPnP::getLibUPnP(); - if (lib == 0) { - m_reason = "Can't get lib"; - return false; - } - - LOGDEB1("UPnPDeviceDirectory::search: calling upnpsearchasync"<<endl); - //const char *cp = "ssdp:all"; - const char *cp = "upnp:rootdevice"; - int code1 = UpnpSearchAsync(lib->getclh(), m_searchTimeout, cp, lib); - if (code1 != UPNP_E_SUCCESS) { - m_reason = LibUPnP::errAsString("UpnpSearchAsync", code1); - LOGERR("UPnPDeviceDirectory::search: UpnpSearchAsync failed: " << - m_reason << endl); - } - m_lastSearch = time(0); - return true; -} - -UPnPDeviceDirectory *UPnPDeviceDirectory::getTheDir(time_t search_window) -{ - if (theDevDir == 0) - theDevDir = new UPnPDeviceDirectory(search_window); - if (theDevDir && !theDevDir->ok()) - return 0; - return theDevDir; -} - -void UPnPDeviceDirectory::terminate() -{ - discoveredQueue.setTerminateAndWait(); -} - -time_t UPnPDeviceDirectory::getRemainingDelay() -{ - time_t now = time(0); - if (now - m_lastSearch >= m_searchTimeout) - return 0; - return m_searchTimeout - (now - m_lastSearch); -} - -bool UPnPDeviceDirectory::traverse(UPnPDeviceDirectory::Visitor visit) -{ - //LOGDEB("UPnPDeviceDirectory::traverse" << endl); - if (m_ok == false) - return false; - int secs = getRemainingDelay(); - if (secs > 0) - sleep(secs); - - // Has locking, do it before our own lock - expireDevices(); - - PTMutexLocker lock(o_pool.m_mutex); - - for (auto it = o_pool.m_devices.begin(); - it != o_pool.m_devices.end(); it++) { - for (auto it1 = it->second.device.services.begin(); - it1 != it->second.device.services.end(); it1++) { - if (!visit(it->second.device, *it1)) - return false; - } - } - return true; -} - -static PTMutexInit devWaitLock; -static pthread_cond_t devWaitCond = PTHREAD_COND_INITIALIZER; - -bool UPnPDeviceDirectory::deviceFound(const UPnPDeviceDesc&, - const UPnPServiceDesc&) -{ - PTMutexLocker lock(devWaitLock); - pthread_cond_broadcast(&devWaitCond); - return true; -} - -bool UPnPDeviceDirectory::getDevBySelector(bool cmp(const UPnPDeviceDesc& ddesc, - const string&), - const string& value, - UPnPDeviceDesc& ddesc) -{ - // Has locking, do it before our own lock - expireDevices(); - - struct timespec wkuptime; - long long nanos = getRemainingDelay() * 1000*1000*1000; - clock_gettime(CLOCK_REALTIME, &wkuptime); - UPnPP::timespec_addnanos(&wkuptime, nanos); - do { - PTMutexLocker lock(devWaitLock); - { - PTMutexLocker lock(o_pool.m_mutex); - for (auto it = o_pool.m_devices.begin(); - it != o_pool.m_devices.end(); it++) { - if (!cmp(it->second.device, value)) { - ddesc = it->second.device; - return true; - } - } - } - - if (nanos > 0) { - pthread_cond_timedwait(&devWaitCond, lock.getMutex(), &wkuptime); - } - } while (getRemainingDelay() > 0); - return false; -} - -static bool cmpFName(const UPnPDeviceDesc& ddesc, const string& fname) -{ - return ddesc.friendlyName.compare(fname); -} - -bool UPnPDeviceDirectory::getDevByFName(const string& fname, - UPnPDeviceDesc& ddesc) -{ - return getDevBySelector(cmpFName, fname, ddesc); -} - -static bool cmpUDN(const UPnPDeviceDesc& ddesc, const string& value) -{ - return ddesc.UDN.compare(value); -} - -bool UPnPDeviceDirectory::getDevByUDN(const string& value, - UPnPDeviceDesc& ddesc) -{ - return getDevBySelector(cmpUDN, value, ddesc); -} - - - -} // namespace UPnPClient + notifyEvent(*it, names, values); + } + } +} + +// Can't take the loop lock here. We're called from the service and +// hold the device lock. The locks would be taken in opposite order, +// causing a potential deadlock: +// - device action takes device lock +// - loop wakes up, takes loop lock +// - blocks on device lock before calling getevent +// - device calls loopwakeup which blocks on loop lock +// -> deadlock +void UpnpDevice::loopWakeup() +{ + pthread_cond_broadcast(&m_evloopcond); +} + +void UpnpDevice::shouldExit() +{ + m_needExit = true; + pthread_cond_broadcast(&m_evloopcond); +} + +}// End namespace UPnPProvider
src/control/discovery.hxx to libupnpp/control/discovery.hxx
File was renamed.
src/control/httpdownload.cxx to libupnpp/control/httpdownload.cxx
File was renamed.
src/control/mediarenderer.cxx to libupnpp/control/mediarenderer.cxx
File was renamed.
src/control/mediarenderer.hxx to libupnpp/control/mediarenderer.hxx
File was renamed.
src/control/mediaserver.cxx to libupnpp/control/ohplaylist.hxx
--- a/src/control/mediaserver.cxx +++ b/libupnpp/control/ohplaylist.hxx @@ -14,80 +14,88 @@ * Free Software Foundation, Inc., * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ +#ifndef _OHPLAYLIST_HXX_INCLUDED_ +#define _OHPLAYLIST_HXX_INCLUDED_ -#include "libupnpp/control/mediaserver.hxx" - -#include <functional> // for _Bind, bind, _1, _2 -#include <ostream> // for endl +#include <memory> // for shared_ptr #include <string> // for string -#include <unordered_map> // for unordered_map, etc -#include <utility> // for pair +#include <unordered_map> // for unordered_map #include <vector> // for vector -#include "libupnpp/control/cdirectory.hxx" // for ContentDirectory, CDSH -#include "libupnpp/control/description.hxx" // for UPnPDeviceDesc, etc -#include "libupnpp/control/discovery.hxx" // for UPnPDeviceDirectory, etc -#include "libupnpp/log.hxx" // for LOGERR +#include "cdircontent.hxx" // for UPnPDirObject +#include "service.hxx" // for Service -using namespace std; -using namespace std::placeholders; +namespace UPnPClient { class OHPlaylist; } +namespace UPnPClient { class UPnPDeviceDesc; } +namespace UPnPClient { class UPnPServiceDesc; } namespace UPnPClient { -const string -MediaServer::DType("urn:schemas-upnp-org:device:MediaServer:1"); +typedef std::shared_ptr<OHPlaylist> OHPLH; -// We don't include a version in comparisons, as we are satisfied with -// version 1 -bool MediaServer::isMSDevice(const string& st) -{ - const string::size_type sz(DType.size()-2); - return !DType.compare(0, sz, st, 0, sz); -} +/** + * OHPlaylist Service client class. + * + */ +class OHPlaylist : public Service { +public: -static bool MDAccum(unordered_map<string, UPnPDeviceDesc>* out, - const string& friendlyName, - const UPnPDeviceDesc& device, - const UPnPServiceDesc& service) -{ - //LOGDEB("MDAccum: friendlyname: " << friendlyName << - // " dev friendlyName " << device.friendlyName << endl); - if (ContentDirectory::isCDService(service.serviceType) && - (friendlyName.empty() ? true : - !friendlyName.compare(device.friendlyName))) { - //LOGDEB("MDAccum setting " << device.UDN << endl); - (*out)[device.UDN] = device; + OHPlaylist(const UPnPDeviceDesc& device, const UPnPServiceDesc& service) + : Service(device, service, true) { + registerCallback(); } - return true; -} -bool MediaServer::getDeviceDescs(vector<UPnPDeviceDesc>& devices, - const string& friendlyName) -{ - unordered_map<string, UPnPDeviceDesc> mydevs; + OHPlaylist() {} - UPnPDeviceDirectory::Visitor visitor = bind(MDAccum, &mydevs, friendlyName, - _1, _2); - UPnPDeviceDirectory::getTheDir()->traverse(visitor); - for (auto it = mydevs.begin(); it != mydevs.end(); it++) - devices.push_back(it->second); - return !devices.empty(); -} + /** Test service type from discovery message */ + static bool isOHPlService(const std::string& st); -MediaServer::MediaServer(const UPnPDeviceDesc& desc) -{ - bool found = false; - for (auto it = desc.services.begin(); it != desc.services.end(); it++) { - if (ContentDirectory::isCDService(it->serviceType)) { - m_cds = CDSH(new ContentDirectory(desc, *it)); - found = true; - break; - } - } - if (!found) { - LOGERR("MediaServer::MediaServer: ContentDirectory service not " - "found in device" << endl); - } -} + int play(); + int pause(); + int stop(); + int next(); + int previous(); + int setRepeat(bool onoff); + int repeat(bool *on); + int setShuffle(bool onoff); + int shuffle(bool *on); + int seekSecondAbsolute(int value); + int seekSecondRelative(int value); + int seekId(int value); + int seekIndex(int value); + enum TPState {TPS_Unknown, TPS_Buffering, TPS_Paused, TPS_Playing, + TPS_Stopped}; + int transportState(TPState *tps); + int id(int *value); + int read(int id, std::string* uri, UPnPDirObject *dirent); -} + struct TrackListEntry { + int id; + std::string url; + UPnPDirObject dirent; + void clear() {id = -1; url.clear(); dirent.clear();} + }; + int readList(const std::vector<int>& ids, + std::vector<TrackListEntry>* entsp); + + int insert(int afterid, const std::string& uri, const std::string& didl, + int *nid); + int deleteId(int id); + int deleteAll(); + int tracksMax(int *); + int idArray(std::vector<int> *ids, int *tokp); + int idArrayChanged(int token, bool *changed); + int protocolInfo(std::string *proto); + +protected: + /* My service type string */ + static const std::string SType; + +private: + void evtCallback(const std::unordered_map<std::string, std::string>&); + void registerCallback(); +}; + +} // namespace UPnPClient + +#endif /* _OHPLAYLIST_HXX_INCLUDED_ */
src/control/mediaserver.hxx to libupnpp/ixmlwrap.hxx
--- a/src/control/mediaserver.hxx +++ b/libupnpp/ixmlwrap.hxx @@ -1,52 +1,40 @@ -/* Copyright (C) 2014 J.F.Dockes - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. +/* Copyright (C) 2013 J.F.Dockes + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the - * Free Software Foundation, Inc., - * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the + * Free Software Foundation, Inc., + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ -#ifndef _MEDIASERVER_HXX_INCLUDED_ -#define _MEDIASERVER_HXX_INCLUDED_ +#ifndef _IXMLWRAP_H_INCLUDED_ +#define _IXMLWRAP_H_INCLUDED_ -#include <memory> // for shared_ptr +#include <upnp/ixml.h> // for IXML_Document + #include <string> // for string -#include <vector> // for vector -#include "libupnpp/control/cdirectory.hxx" // for CDSH -#include "libupnpp/control/device.hxx" // for Device +namespace UPnPP { -namespace UPnPClient { +#if notUsedAnyMore + /** Retrieve the text content for the first element of given name. + * Returns an empty string if the element does not contain a text node */ + std::string getFirstElementValue(IXML_Document *doc, + const std::string& name); * +#endif -class MediaServer; -class UPnPDeviceDesc; - -typedef std::shared_ptr<MediaServer> MSRH; - -class MediaServer : public Device { -public: - MediaServer(const UPnPDeviceDesc& desc); - - CDSH cds() {return m_cds;} - - static bool getDeviceDescs(std::vector<UPnPDeviceDesc>& devices, - const std::string& friendlyName = ""); - static bool isMSDevice(const std::string& devicetype); - -protected: - CDSH m_cds; - - static const std::string DType; -}; + /** Return the result of ixmlPrintDocument as a string and take + * care of freeing the memory. This is inefficient of course (one + * more alloc+copy), and destined to debug statements */ + std::string ixmlwPrintDoc(IXML_Document*); } -#endif /* _MEDIASERVER_HXX_INCLUDED_ */ +#endif /* _IXMLWRAP_H_INCLUDED_ */
src/control/ohplaylist.cxx to libupnpp/upnpplib.cxx
--- a/src/control/ohplaylist.cxx +++ b/libupnpp/upnpplib.cxx @@ -1,413 +1,478 @@ -/* Copyright (C) 2014 J.F.Dockes - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. +/* Copyright (C) 2013 J.F.Dockes + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the - * Free Software Foundation, Inc., - * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the + * Free Software Foundation, Inc., + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ -#include "libupnpp/control/ohplaylist.hxx" - -#include <expat_external.h> // for XML_Char -#include <netinet/in.h> // for ntohl -#include <stdlib.h> // for atoi -#include <string.h> // for strcmp -#include <upnp/upnp.h> // for UPNP_E_BAD_RESPONSE, etc - -#include <functional> // for _Bind, bind, _1 -#include <ostream> // for endl, basic_ostream, etc +#include "config.h" + +#include "upnpplib.hxx" + +#include <ctype.h> // for toupper +#include <stdio.h> // for sprintf +#include <string.h> // for strncpy +#include <time.h> // for timespec + +#include <upnp/ixml.h> // for ixmlRelaxParser +#include <upnp/upnptools.h> // for UpnpGetErrorMessage +#include <upnp/upnpdebug.h> + +#include <iostream> // for operator<<, basic_ostream, etc +#include <map> // for map, _Rb_tree_iterator, etc +#include <set> // for set +#include <sstream> // for ostringstream #include <string> // for string, basic_string, etc #include <utility> // for pair #include <vector> // for vector -#include "libupnpp/base64.hxx" // for base64_decode -#include "libupnpp/control/cdircontent.hxx" // for UPnPDirContent, etc -#include "libupnpp/control/service.hxx" // for VarEventReporter, Service -#include "libupnpp/expatmm.hxx" // for inputRefXMLParser -#include "libupnpp/log.hxx" // for LOGERR, LOGDEB1, LOGINF -#include "libupnpp/soaphelp.hxx" // for SoapDecodeOutput, etc -#include "libupnpp/upnpp_p.hxx" // for stringToBool +#include "getsyshwaddr.h" // for getsyshwaddr + +#include "libupnpp/ptmutex.hxx" // for PTMutexLocker + +#include "log.hxx" // for LOGERR, LOGDEB1, LOGDEB, etc +#include "md5.hxx" // for MD5String using namespace std; -using namespace std::placeholders; -using namespace UPnPP; - -namespace UPnPClient { - -const string OHPlaylist::SType("urn:av-openhome-org:service:Playlist:1"); - -// Check serviceType string (while walking the descriptions. We don't -// include a version in comparisons, as we are satisfied with version1 -bool OHPlaylist::isOHPlService(const string& st) -{ - const string::size_type sz(SType.size()-2); - return !SType.compare(0, sz, st, 0, sz); -} - -static int stringToTpState(const string& value, OHPlaylist::TPState *tpp) -{ - if (!value.compare("Buffering")) { - *tpp = OHPlaylist::TPS_Buffering; + +namespace UPnPP { + +static LibUPnP *theLib; + +LibUPnP *LibUPnP::getLibUPnP(bool serveronly, string* hwaddr, + const string ifname, const string ip, + unsigned short port) +{ + if (theLib == 0) + theLib = new LibUPnP(serveronly, hwaddr, ifname, ip, port); + if (theLib && !theLib->ok()) { + delete theLib; + theLib = 0; return 0; - } else if (!value.compare("Paused")) { - *tpp = OHPlaylist::TPS_Paused; - return 0; - } else if (!value.compare("Playing")) { - *tpp = OHPlaylist::TPS_Playing; - return 0; - } else if (!value.compare("Stopped")) { - *tpp = OHPlaylist::TPS_Stopped; - return 0; - } - *tpp = OHPlaylist::TPS_Unknown; - return UPNP_E_BAD_RESPONSE; -} - -// Translate IdArray: base64-encoded array of binary msb 32bits integers -static void idArrayToVec(const string& _data, vector<int> *ids) -{ - string data = base64_decode(_data); - const char *cp = data.c_str(); - while (cp - data.c_str() <= int(data.size()) - 4) { - unsigned int *ip = (unsigned int *)cp; - ids->push_back(ntohl(*ip)); - cp += 4; - } -} - -void OHPlaylist::evtCallback( - const std::unordered_map<std::string, std::string>& props) -{ - LOGDEB1("OHPlaylist::evtCallback: m_reporter: " << m_reporter << endl); - for (auto it = props.begin(); it != props.end(); it++) { - if (!m_reporter) { - LOGDEB1("OHPlaylist::evtCallback: " << it->first << " -> " - << it->second << endl); - continue; + } + return theLib; +} + + +LibUPnP::LibUPnP(bool serveronly, string* hwaddr, + const string ifname, const string inip, unsigned short port) + : m_ok(false) +{ + LOGDEB1("LibUPnP: serveronly " << serveronly << " &hwaddr " << hwaddr << + " ifname [" << ifname << "] inip [" << inip << "] port " << port + << endl); + + // If our caller wants to retrieve an ethernet address (typically + // for uuid purposes), or has specified an interface we have to + // look at the network config. + const int ipalen(100); + char ip_address[ipalen]; + ip_address[0] = 0; + if (hwaddr || !ifname.empty()) { + char mac[20]; + if (getsyshwaddr(ifname.c_str(), ip_address, ipalen, mac, 13) < 0) { + LOGERR("LibUPnP::LibUPnP: failed retrieving addr" << endl); + return; } - - if (!it->first.compare("TransportState")) { - TPState tp; - stringToTpState(it->second, &tp); - m_reporter->changed(it->first.c_str(), int(tp)); - - } else if (!it->first.compare("ProtocolInfo")) { - m_reporter->changed(it->first.c_str(), - it->second.c_str()); - - } else if (!it->first.compare("Repeat") || - !it->first.compare("Shuffle")) { - bool val = false; - stringToBool(it->second, &val); - m_reporter->changed(it->first.c_str(), val ? 1 : 0); - - } else if (!it->first.compare("Id") || - !it->first.compare("TracksMax")) { - m_reporter->changed(it->first.c_str(), - atoi(it->second.c_str())); - - } else if (!it->first.compare("IdArray")) { - // Decode IdArray. See how we call the client - vector<int> v; - idArrayToVec(it->second, &v); - m_reporter->changed(it->first.c_str(), v); - + if (hwaddr) + *hwaddr = string(mac); + } + + // If the interface name was not specified, we possibly use the + // supplied IP address. + if (ifname.empty()) + strncpy(ip_address, inip.c_str(), ipalen); + + m_init_error = UpnpInit(ip_address[0] ? ip_address : 0, port); + + if (m_init_error != UPNP_E_SUCCESS) { + LOGERR(errAsString("UpnpInit", m_init_error) << endl); + return; + } + setMaxContentLength(2000*1024); + + LOGDEB("LibUPnP: Using IP " << UpnpGetServerIpAddress() << " port " << + UpnpGetServerPort() << endl); + +#if defined(HAVE_UPNPSETLOGLEVEL) + UpnpCloseLog(); +#endif + + // Client initialization is simple, just do it. Defer device + // initialization because it's more complicated. + if (serveronly) { + m_ok = true; + } else { + m_init_error = UpnpRegisterClient(o_callback, (void *)this, &m_clh); + + if (m_init_error == UPNP_E_SUCCESS) { + m_ok = true; } else { - LOGERR("OHPlaylist event: unknown variable: name [" << - it->first << "] value [" << it->second << endl); - m_reporter->changed(it->first.c_str(), it->second.c_str()); + LOGERR(errAsString("UpnpRegisterClient", m_init_error) << endl); } } -} - -void OHPlaylist::registerCallback() -{ - Service::registerCallback(bind(&OHPlaylist::evtCallback, this, _1)); -} - -int OHPlaylist::play() -{ - return runTrivialAction("Play"); -} -int OHPlaylist::pause() -{ - return runTrivialAction("Pause"); -} -int OHPlaylist::stop() -{ - return runTrivialAction("Stop"); -} -int OHPlaylist::next() -{ - return runTrivialAction("Next"); -} -int OHPlaylist::previous() -{ - return runTrivialAction("Previous"); -} -int OHPlaylist::setRepeat(bool onoff) -{ - return runSimpleAction("SetRepeat", "Value", onoff); -} -int OHPlaylist::repeat(bool *on) -{ - return runSimpleGet("Repeat", "Value", on); -} -int OHPlaylist::setShuffle(bool onoff) -{ - return runSimpleAction("SetShuffle", "Value", onoff); -} -int OHPlaylist::shuffle(bool *on) -{ - return runSimpleGet("Shuffle", "Value", on); -} -int OHPlaylist::seekSecondAbsolute(int value) -{ - return runSimpleAction("SeekSecondAbsolute", "Value", value); -} -int OHPlaylist::seekSecondRelative(int value) -{ - return runSimpleAction("SeekSecondRelative", "Value", value); -} -int OHPlaylist::seekId(int value) -{ - return runSimpleAction("SeekId", "Value", value); -} -int OHPlaylist::seekIndex(int value) -{ - return runSimpleAction("SeekIndex", "Value", value); -} - -int OHPlaylist::transportState(TPState* tpp) -{ - string value; - int ret; - - if ((ret = runSimpleGet("TransportState", "Value", &value))) - return ret; - - return stringToTpState(value, tpp); -} - -int OHPlaylist::id(int *value) -{ - return runSimpleGet("Id", "Value", value); -} - -int OHPlaylist::read(int id, std::string* urip, UPnPDirObject *dirent) -{ - SoapEncodeInput args(m_serviceType, "Read"); - args("Id", SoapHelp::i2s(id)); - SoapDecodeOutput data; - int ret = runAction(args, data); - if (ret != UPNP_E_SUCCESS) { - return ret; - } - if (!data.get("Uri", urip)) { - LOGERR("OHPlaylist::Read: missing Uri in response" << endl); - return UPNP_E_BAD_RESPONSE; - } - string didl; - if (!data.get("Metadata", &didl)) { - LOGERR("OHPlaylist::Read: missing Uri in response" << endl); - return UPNP_E_BAD_RESPONSE; - } - didl = SoapHelp::xmlUnquote(didl); - - UPnPDirContent dir; - if (!dir.parse(didl)) { - LOGERR("OHPlaylist::Read: didl parse failed: " << didl << endl); - return UPNP_E_BAD_RESPONSE; - } - if (dir.m_items.size() != 1) { - LOGERR("OHPlaylist::Read: " << dir.m_items.size() << " in response!" << - endl); - return UPNP_E_BAD_RESPONSE; - } - *dirent = dir.m_items[0]; - return 0; -} - -// Tracklist format -// <TrackList> -// <Entry> -// <Id>10</Id> -// <Uri>http://blabla</Uri> -// <Metadata>(xmlencoded didl)</Metadata> -// </Entry> -// </TrackList> - - -class OHTrackListParser : public inputRefXMLParser { -public: - OHTrackListParser(const string& input, - vector<OHPlaylist::TrackListEntry>* vp) - : inputRefXMLParser(input), m_v(vp) - { - //LOGDEB("OHTrackListParser: input: " << input << endl); + + // Servers sometimes make errors (e.g.: minidlna returns bad utf-8). + ixmlRelaxParser(1); +} + +int LibUPnP::setupWebServer(const string& description, UpnpDevice_Handle *dvh) +{ + int res = UpnpRegisterRootDevice2( + UPNPREG_BUF_DESC, + description.c_str(), + description.size(), /* Desc filename len, ignored */ + 1, /* config_baseURL */ + o_callback, (void *)this, dvh); + + if (res != UPNP_E_SUCCESS) { + LOGERR(errAsString("UpnpRegisterRootDevice2", res) << " description " << + description << endl); + } + return res; +} + +void LibUPnP::setMaxContentLength(int bytes) +{ + UpnpSetMaxContentLength(bytes); +} + +bool LibUPnP::setLogFileName(const std::string& fn, LogLevel level) +{ + PTMutexLocker lock(m_mutex); + if (fn.empty() || level == LogLevelNone) { +#if defined(HAVE_UPNPSETLOGLEVEL) + UpnpCloseLog(); +#endif + } else { +#if defined(HAVE_UPNPSETLOGLEVEL) + setLogLevel(level); + UpnpSetLogFileNames(fn.c_str(), fn.c_str()); + int code = UpnpInitLog(); + if (code != UPNP_E_SUCCESS) { + LOGERR(errAsString("UpnpInitLog", code) << endl); + return false; } - -protected: - virtual void StartElement(const XML_Char *name, const XML_Char **) { - m_path.push_back(name); - } - virtual void EndElement(const XML_Char *name) { - if (!strcmp(name, "Entry")) { - UPnPDirContent dir; - if (!dir.parse(m_tdidl)) { - LOGERR("OHPlaylist::ReadList: didl parse failed: " - << m_tdidl << endl); - return; +#endif + } + return true; +} + +bool LibUPnP::setLogLevel(LogLevel level) +{ +#if defined(HAVE_UPNPSETLOGLEVEL) + switch (level) { + case LogLevelNone: + setLogFileName("", LogLevelNone); + break; + case LogLevelError: + UpnpSetLogLevel(UPNP_CRITICAL); + break; + case LogLevelDebug: + UpnpSetLogLevel(UPNP_ALL); + break; + } +#endif + return true; +} + +void LibUPnP::registerHandler(Upnp_EventType et, Upnp_FunPtr handler, + void *cookie) +{ + PTMutexLocker lock(m_mutex); + if (handler == 0) { + m_handlers.erase(et); + } else { + Handler h(handler, cookie); + m_handlers[et] = h; + } +} + +std::string LibUPnP::errAsString(const std::string& who, int code) +{ + std::ostringstream os; + os << who << " :" << code << ": " << UpnpGetErrorMessage(code); + return os.str(); +} + +int LibUPnP::o_callback(Upnp_EventType et, void* evp, void* cookie) +{ + LibUPnP *ulib = (LibUPnP *)cookie; + if (ulib == 0) { + // Because the asyncsearch calls uses a null cookie. + //cerr << "o_callback: NULL ulib!" << endl; + ulib = theLib; + } + LOGDEB1("LibUPnP::o_callback: event type: " << evTypeAsString(et) << endl); + + map<Upnp_EventType, Handler>::iterator it = ulib->m_handlers.find(et); + if (it != ulib->m_handlers.end()) { + (it->second.handler)(et, evp, it->second.cookie); + } + return UPNP_E_SUCCESS; +} + +LibUPnP::~LibUPnP() +{ + int error = UpnpFinish(); + if (error != UPNP_E_SUCCESS) { + LOGINF("LibUPnP::~LibUPnP: " << errAsString("UpnpFinish", error) + << endl); + } + LOGDEB1("LibUPnP: done" << endl); +} + +string LibUPnP::makeDevUUID(const std::string& name, const std::string& hw) +{ + string digest; + MD5String(name, digest); + // digest has 16 bytes of binary data. UUID is like: + // f81d4fae-7dec-11d0-a765-00a0c91e6bf6 + // Where the last 12 chars are provided by the hw addr + + char uuid[100]; + sprintf(uuid, "%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%s", + digest[0]&0xff, digest[1]&0xff, digest[2]&0xff, digest[3]&0xff, + digest[4]&0xff, digest[5]&0xff, digest[6]&0xff, digest[7]&0xff, + digest[8]&0xff, digest[9]&0xff, hw.c_str()); + return uuid; +} + + +string LibUPnP::evTypeAsString(Upnp_EventType et) +{ + switch (et) { + case UPNP_CONTROL_ACTION_REQUEST: return "UPNP_CONTROL_ACTION_REQUEST"; + case UPNP_CONTROL_ACTION_COMPLETE: return "UPNP_CONTROL_ACTION_COMPLETE"; + case UPNP_CONTROL_GET_VAR_REQUEST: return "UPNP_CONTROL_GET_VAR_REQUEST"; + case UPNP_CONTROL_GET_VAR_COMPLETE: return "UPNP_CONTROL_GET_VAR_COMPLETE"; + case UPNP_DISCOVERY_ADVERTISEMENT_ALIVE: + return "UPNP_DISCOVERY_ADVERTISEMENT_ALIVE"; + case UPNP_DISCOVERY_ADVERTISEMENT_BYEBYE: + return "UPNP_DISCOVERY_ADVERTISEMENT_BYEBYE"; + case UPNP_DISCOVERY_SEARCH_RESULT: return "UPNP_DISCOVERY_SEARCH_RESULT"; + case UPNP_DISCOVERY_SEARCH_TIMEOUT: return "UPNP_DISCOVERY_SEARCH_TIMEOUT"; + case UPNP_EVENT_SUBSCRIPTION_REQUEST: + return "UPNP_EVENT_SUBSCRIPTION_REQUEST"; + case UPNP_EVENT_RECEIVED: return "UPNP_EVENT_RECEIVED"; + case UPNP_EVENT_RENEWAL_COMPLETE: return "UPNP_EVENT_RENEWAL_COMPLETE"; + case UPNP_EVENT_SUBSCRIBE_COMPLETE: return "UPNP_EVENT_SUBSCRIBE_COMPLETE"; + case UPNP_EVENT_UNSUBSCRIBE_COMPLETE: + return "UPNP_EVENT_UNSUBSCRIBE_COMPLETE"; + case UPNP_EVENT_AUTORENEWAL_FAILED: return "UPNP_EVENT_AUTORENEWAL_FAILED"; + case UPNP_EVENT_SUBSCRIPTION_EXPIRED: + return "UPNP_EVENT_SUBSCRIPTION_EXPIRED"; + default: return "UPNP UNKNOWN EVENT"; + } +} + +/////////////////////// Small global helpers + +/** Get rid of white space at both ends */ +void trimstring(string &s, const char *ws) +{ + string::size_type pos = s.find_first_not_of(ws); + if (pos == string::npos) { + s.clear(); + return; + } + s.replace(0, pos, string()); + + pos = s.find_last_not_of(ws); + if (pos != string::npos && pos != s.length()-1) + s.replace(pos+1, string::npos, string()); +} + +string caturl(const string& s1, const string& s2) +{ + string out(s1); + if (out[out.size()-1] == '/') { + if (s2[0] == '/') + out.erase(out.size()-1); + } else { + if (s2[0] != '/') + out.push_back('/'); + } + out += s2; + return out; +} + +string baseurl(const string& url) +{ + string::size_type pos = url.find("://"); + if (pos == string::npos) + return url; + + pos = url.find_first_of("/", pos + 3); + if (pos == string::npos) { + return url; + } else { + return url.substr(0, pos + 1); + } +} + +static void path_catslash(string &s) { + if (s.empty() || s[s.length() - 1] != '/') + s += '/'; +} +string path_getfather(const string &s) +{ + string father = s; + + // ?? + if (father.empty()) + return "./"; + + if (father[father.length() - 1] == '/') { + // Input ends with /. Strip it, handle special case for root + if (father.length() == 1) + return father; + father.erase(father.length()-1); + } + + string::size_type slp = father.rfind('/'); + if (slp == string::npos) + return "./"; + + father.erase(slp); + path_catslash(father); + return father; +} +string path_getsimple(const string &s) { + string simple = s; + + if (simple.empty()) + return simple; + + string::size_type slp = simple.rfind('/'); + if (slp == string::npos) + return simple; + + simple.erase(0, slp+1); + return simple; +} + +template <class T> bool csvToStrings(const string &s, T &tokens) +{ + string current; + tokens.clear(); + enum states {TOKEN, ESCAPE}; + states state = TOKEN; + for (unsigned int i = 0; i < s.length(); i++) { + switch (s[i]) { + case ',': + switch(state) { + case TOKEN: + tokens.insert(tokens.end(), current); + current.clear(); + continue; + case ESCAPE: + current += ','; + state = TOKEN; + continue; } - if (dir.m_items.size() != 1) { - LOGERR("OHPlaylist::ReadList: " << dir.m_items.size() - << " in response!" << endl); - return; + break; + case '\\': + switch(state) { + case TOKEN: + state=ESCAPE; + continue; + case ESCAPE: + current += '\\'; + state = TOKEN; + continue; } - m_tt.dirent = dir.m_items[0]; - m_v->push_back(m_tt); - m_tt.clear(); - m_tdidl.clear(); + break; + + default: + switch(state) { + case ESCAPE: + state = TOKEN; + break; + case TOKEN: + break; + } + current += s[i]; } - m_path.pop_back(); - } - virtual void CharacterData(const XML_Char *s, int len) { - if (s == 0 || *s == 0) - return; - string str(s, len); - if (!m_path.back().compare("Id")) - m_tt.id = atoi(str.c_str()); - else if (!m_path.back().compare("Uri")) - m_tt.url = str; - else if (!m_path.back().compare("Metadata")) - m_tdidl += str; - } - -private: - vector<OHPlaylist::TrackListEntry>* m_v; - std::vector<std::string> m_path; - OHPlaylist::TrackListEntry m_tt; - string m_tdidl; -}; - -int OHPlaylist::readList(const std::vector<int>& ids, - vector<TrackListEntry>* entsp) -{ - string idsparam; - for (auto it = ids.begin(); it != ids.end(); it++) { - idsparam += SoapHelp::i2s(*it) + " "; - } - entsp->clear(); - - SoapEncodeInput args(m_serviceType, "ReadList"); - args("IdList", idsparam); - SoapDecodeOutput data; - int ret = runAction(args, data); - if (ret != UPNP_E_SUCCESS) { - return ret; - } - string xml; - if (!data.get("TrackList", &xml)) { - LOGERR("OHPlaylist::readlist: missing TrackList in response" << endl); - return UPNP_E_BAD_RESPONSE; - } - OHTrackListParser mparser(xml, entsp); - if (!mparser.Parse()) - return UPNP_E_BAD_RESPONSE; - return 0; -} - -int OHPlaylist::insert(int afterid, const string& uri, const string& didl, - int *nid) -{ - SoapEncodeInput args(m_serviceType, "Insert"); - args("AfterId", SoapHelp::i2s(afterid)) - ("Uri", uri) - ("Metadata", didl); - SoapDecodeOutput data; - int ret = runAction(args, data); - if (ret != UPNP_E_SUCCESS) { - return ret; - } - if (!data.get("NewId", nid)) { - LOGERR("OHPlaylist::insert: missing Newid in response" << endl); - return UPNP_E_BAD_RESPONSE; - } - return 0; -} - - -int OHPlaylist::deleteId(int value) -{ - return runSimpleAction("DeleteId", "Value", value); -} -int OHPlaylist::deleteAll() -{ - return runTrivialAction("DeleteAll"); -} -int OHPlaylist::tracksMax(int *valuep) -{ - return runSimpleGet("TracksMax", "Value", valuep); -} - -int OHPlaylist::idArray(vector<int> *ids, int *tokp) -{ - SoapEncodeInput args(m_serviceType, "IdArray"); - SoapDecodeOutput data; - int ret = runAction(args, data); - if (ret != UPNP_E_SUCCESS) { - return ret; - } - if (!data.get("Token", tokp)) { - LOGERR("OHPlaylist::idArray: missing Token in response" << endl); - return UPNP_E_BAD_RESPONSE; - } - string arraydata; - if (!data.get("Array", &arraydata)) { - LOGINF("OHPlaylist::idArray: missing Array in response" << endl); - // We get this for an empty array ? This would need to be investigated - } - idArrayToVec(arraydata, ids); - return 0; -} - -int OHPlaylist::idArrayChanged(int token, bool *changed) -{ - SoapEncodeInput args(m_serviceType, "IdArrayChanged"); - args("Token", SoapHelp::i2s(token)); - SoapDecodeOutput data; - int ret = runAction(args, data); - if (ret != UPNP_E_SUCCESS) { - return ret; - } - if (!data.get("Value", changed)) { - LOGERR("OHPlaylist::idArrayChanged: missing Value in response" << endl); - return UPNP_E_BAD_RESPONSE; - } - return 0; -} - -int OHPlaylist::protocolInfo(std::string *proto) -{ - SoapEncodeInput args(m_serviceType, "ProtocolInfo"); - SoapDecodeOutput data; - int ret = runAction(args, data); - if (ret != UPNP_E_SUCCESS) { - return ret; - } - if (!data.get("Value", proto)) { - LOGERR("OHPlaylist::protocolInfo: missing Value in response" << endl); - return UPNP_E_BAD_RESPONSE; - } - return 0; -} - -} // End namespace UPnPClient + } + switch(state) { + case TOKEN: + tokens.insert(tokens.end(), current); + break; + case ESCAPE: + return false; + } + return true; +} + +//template bool csvToStrings<list<string> >(const string &, list<string> &); +template bool csvToStrings<vector<string> >(const string &, vector<string> &); +template bool csvToStrings<set<string> >(const string &, set<string> &); + + +bool stringToBool(const string& s, bool *value) +{ + if (s[0] == 'F' ||s[0] == 'f' ||s[0] == 'N' || s[0] == 'n' ||s[0] == '0') { + *value = false; + } else if (s[0] == 'T'|| s[0] == 't' ||s[0] == 'Y' ||s[0] == 'y' || + s[0] == '1') { + *value = true; + } else { + return false; + } + return true; +} + +// s1 is already uppercase +int stringuppercmp(const string & s1, const string& s2) +{ + string::const_iterator it1 = s1.begin(); + string::const_iterator it2 = s2.begin(); + int size1 = s1.length(), size2 = s2.length(); + char c2; + + if (size1 > size2) { + while (it1 != s1.end()) { + c2 = ::toupper(*it2); + if (*it1 != c2) { + return *it1 > c2 ? 1 : -1; + } + ++it1; ++it2; + } + return size1 == size2 ? 0 : 1; + } else { + while (it2 != s2.end()) { + c2 = ::toupper(*it2); + if (*it1 != c2) { + return *it1 > c2 ? 1 : -1; + } + ++it1; ++it2; + } + return size1 == size2 ? 0 : -1; + } +} + +static const long long BILLION = 1000 * 1000 * 1000; + +void timespec_addnanos(struct timespec *ts, long long nanos) +{ + nanos = nanos + ts->tv_nsec; + int secs = 0; + if (nanos > BILLION) { + secs = nanos / BILLION; + nanos -= secs * BILLION; + } + ts->tv_sec += secs; + ts->tv_nsec = nanos; +} + +}
src/control/ohplaylist.hxx to libupnpp/control/renderingcontrol.hxx
--- a/src/control/ohplaylist.hxx +++ b/libupnpp/control/renderingcontrol.hxx @@ -14,88 +14,68 @@ * Free Software Foundation, Inc., * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ -#ifndef _OHPLAYLIST_HXX_INCLUDED_ -#define _OHPLAYLIST_HXX_INCLUDED_ +#ifndef _RENDERINGCONTROL_HXX_INCLUDED_ +#define _RENDERINGCONTROL_HXX_INCLUDED_ #include <memory> // for shared_ptr #include <string> // for string #include <unordered_map> // for unordered_map -#include <vector> // for vector -#include "cdircontent.hxx" // for UPnPDirObject #include "service.hxx" // for Service -namespace UPnPClient { class OHPlaylist; } +namespace UPnPClient { class RenderingControl; } namespace UPnPClient { class UPnPDeviceDesc; } namespace UPnPClient { class UPnPServiceDesc; } namespace UPnPClient { -typedef std::shared_ptr<OHPlaylist> OHPLH; +typedef std::shared_ptr<RenderingControl> RDCH; /** - * OHPlaylist Service client class. + * RenderingControl Service client class. * */ -class OHPlaylist : public Service { +class RenderingControl : public Service { public: - OHPlaylist(const UPnPDeviceDesc& device, const UPnPServiceDesc& service) - : Service(device, service, true) { - registerCallback(); - } + /** Construct by copying data from device and service objects. + * + */ + RenderingControl(const UPnPDeviceDesc& device, + const UPnPServiceDesc& service); - OHPlaylist() {} + RenderingControl() {} /** Test service type from discovery message */ - static bool isOHPlService(const std::string& st); + static bool isRDCService(const std::string& st); - int play(); - int pause(); - int stop(); - int next(); - int previous(); - int setRepeat(bool onoff); - int repeat(bool *on); - int setShuffle(bool onoff); - int shuffle(bool *on); - int seekSecondAbsolute(int value); - int seekSecondRelative(int value); - int seekId(int value); - int seekIndex(int value); - enum TPState {TPS_Unknown, TPS_Buffering, TPS_Paused, TPS_Playing, - TPS_Stopped}; - int transportState(TPState *tps); - int id(int *value); - int read(int id, std::string* uri, UPnPDirObject *dirent); - - struct TrackListEntry { - int id; - std::string url; - UPnPDirObject dirent; - void clear() {id = -1; url.clear(); dirent.clear();} - }; - int readList(const std::vector<int>& ids, - std::vector<TrackListEntry>* entsp); - - int insert(int afterid, const std::string& uri, const std::string& didl, - int *nid); - int deleteId(int id); - int deleteAll(); - int tracksMax(int *); - int idArray(std::vector<int> *ids, int *tokp); - int idArrayChanged(int token, bool *changed); - int protocolInfo(std::string *proto); + /** @ret 0 for success, upnp error else */ + int setVolume(int volume, const std::string& channel = "Master"); + int getVolume(const std::string& channel = "Master"); + int setMute(bool mute, const std::string& channel = "Master"); + bool getMute(const std::string& channel = "Master"); protected: /* My service type string */ static const std::string SType; + /* Volume settings params */ + int m_volmin; + int m_volmax; + int m_volstep; + private: void evtCallback(const std::unordered_map<std::string, std::string>&); void registerCallback(); + /** Set volume parameters from service state variable table values */ + void setVolParams(int min, int max, int step) { + m_volmin = min >= 0 ? min : 0; + m_volmax = max > 0 ? max : 100; + m_volstep = step > 0 ? step : 1; + } + int devVolTo0100(int); }; } // namespace UPnPClient -#endif /* _OHPLAYLIST_HXX_INCLUDED_ */ +#endif /* _RENDERINGCONTROL_HXX_INCLUDED_ */
src/control/ohproduct.cxx to libupnpp/control/ohproduct.cxx
File was renamed.
src/control/ohproduct.hxx to libupnpp/control/ohproduct.hxx
File was renamed.
src/control/renderingcontrol.cxx to libupnpp/control/renderingcontrol.cxx
File was renamed.
src/control/renderingcontrol.hxx to libupnpp/ixmlwrap.cxx
--- a/src/control/renderingcontrol.hxx +++ b/libupnpp/ixmlwrap.cxx @@ -1,4 +1,4 @@ -/* Copyright (C) 2014 J.F.Dockes +/* Copyright (C) 2013 J.F.Dockes * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or @@ -14,68 +14,53 @@ * Free Software Foundation, Inc., * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ -#ifndef _RENDERINGCONTROL_HXX_INCLUDED_ -#define _RENDERINGCONTROL_HXX_INCLUDED_ +#include "config.h" -#include <memory> // for shared_ptr +#include "libupnpp/ixmlwrap.hxx" + +#include <upnp/ixml.h> // for IXML_Node, etc + #include <string> // for string -#include <unordered_map> // for unordered_map -#include "service.hxx" // for Service +using std::string; -namespace UPnPClient { class RenderingControl; } -namespace UPnPClient { class UPnPDeviceDesc; } -namespace UPnPClient { class UPnPServiceDesc; } +namespace UPnPP { -namespace UPnPClient { +#if notUsedAnyMore +// Get the value for the first element in the document with the given name. +// There should be only one such element for this to make any sense. +string getFirstElementValue(IXML_Document *doc, const string& name) +{ + string ret; + IXML_NodeList *nodes = + ixmlDocument_getElementsByTagName(doc, name.c_str()); -typedef std::shared_ptr<RenderingControl> RDCH; + if (nodes) { + IXML_Node *first = ixmlNodeList_item(nodes, 0); + if (first) { + IXML_Node *dnode = ixmlNode_getFirstChild(first); + if (dnode) { + ret = ixmlNode_getNodeValue(dnode); + } + } + } -/** - * RenderingControl Service client class. - * - */ -class RenderingControl : public Service { -public: + if(nodes) + ixmlNodeList_free(nodes); + return ret; +} +#endif - /** Construct by copying data from device and service objects. - * - */ - RenderingControl(const UPnPDeviceDesc& device, - const UPnPServiceDesc& service); +string ixmlwPrintDoc(IXML_Document* doc) +{ + DOMString s = ixmlPrintDocument(doc); + if (s) { + string cpps(s); + ixmlFreeDOMString(s); + return cpps; + } else { + return string(); + } +} - RenderingControl() {} - - /** Test service type from discovery message */ - static bool isRDCService(const std::string& st); - - /** @ret 0 for success, upnp error else */ - int setVolume(int volume, const std::string& channel = "Master"); - int getVolume(const std::string& channel = "Master"); - int setMute(bool mute, const std::string& channel = "Master"); - bool getMute(const std::string& channel = "Master"); - -protected: - /* My service type string */ - static const std::string SType; - - /* Volume settings params */ - int m_volmin; - int m_volmax; - int m_volstep; - -private: - void evtCallback(const std::unordered_map<std::string, std::string>&); - void registerCallback(); - /** Set volume parameters from service state variable table values */ - void setVolParams(int min, int max, int step) { - m_volmin = min >= 0 ? min : 0; - m_volmax = max > 0 ? max : 100; - m_volstep = step > 0 ? step : 1; - } - int devVolTo0100(int); -}; - -} // namespace UPnPClient - -#endif /* _RENDERINGCONTROL_HXX_INCLUDED_ */ +}