Commit aba967ca authored by Jean-Francois Dockes's avatar Jean-Francois Dockes
Browse files

Merge branch 'multidevice': localize knowledge of XML data to the services

+ other changes
parents 164e34f7 38e70d76
......@@ -69,8 +69,6 @@ upmpdcli_SOURCES = \
src/conman.hxx \
src/execmd-fixed.cpp \
src/execmd.h \
src/httpfs.cxx \
src/httpfs.hxx \
src/main.cxx \
src/main.hxx \
src/mediaserver/cdplugins/abuffer.h \
......@@ -154,6 +152,7 @@ dist_pkgdata_DATA = src/description.xml src/AVTransport.xml \
src/OHTime.xml \
src/OHVolume.xml src/OHPlaylist.xml src/OHReceiver.xml \
src/OHRadio.xml src/mediaserver/ContentDirectory.xml \
src/mediaserver/MS-description.xml \
src/protocolinfo.txt src/presentation.html \
src/upmpdcli.conf-dist
......
......@@ -44,7 +44,8 @@ static const string sIdTransport("urn:upnp-org:serviceId:AVTransport");
static const string sTpTransport("urn:schemas-upnp-org:service:AVTransport:1");
UpMpdAVTransport::UpMpdAVTransport(UpMpd *dev, bool noev)
: UpnpService(sTpTransport, sIdTransport, dev, noev), m_dev(dev), m_ohp(0)
: UpnpService(sTpTransport, sIdTransport, "AVTransport.xml", dev, noev),
m_dev(dev), m_ohp(0)
{
m_dev->addActionMapping(this,"SetAVTransportURI",
bind(&UpMpdAVTransport::setAVTransportURI,
......
......@@ -39,7 +39,7 @@ static const string sTpCM("urn:schemas-upnp-org:service:ConnectionManager:1");
static const string sIdCM("urn:upnp-org:serviceId:ConnectionManager");
UpMpdConMan::UpMpdConMan(UpnpDevice *dev)
: UpnpService(sTpCM, sIdCM, dev)
: UpnpService(sTpCM, sIdCM, "ConnectionManager.xml", dev)
{
dev->addActionMapping(this,"GetCurrentConnectionIDs",
bind(&UpMpdConMan::getCurrentConnectionIDs,
......
<?xml version="1.0" encoding="utf-8"?>
<root xmlns="urn:schemas-upnp-org:device-1-0">
<specVersion>
<major>1</major>
<minor>0</minor>
</specVersion>
<device>
<deviceType>urn:schemas-upnp-org:device:MediaRenderer:1</deviceType>
<friendlyName>@FRIENDLYNAME@</friendlyName>
<manufacturer>JF Light Industries</manufacturer>
......@@ -14,16 +7,3 @@
<modelNumber>1.0</modelNumber>
<modelURL>https://opensourceprojects.eu/p/upmpdcli/code/</modelURL>
<serialNumber>42</serialNumber>
<UDN>uuid:@UUID@</UDN>
@ICONLIST@
<serviceList>
@UPNPAV@
@OPENHOME@
</serviceList>
@PRESENTATION@
</device>
<devicelist>
@MEDIASERVER@
</devicelist>
</root>
/* 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.
*
* 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 <unordered_map>
#include <string>
#include "libupnpp/log.hxx"
#include "libupnpp/upnpplib.hxx"
#include "libupnpp/upnpavutils.hxx"
#include "libupnpp/device/device.hxx"
#include "main.hxx"
#include "upmpdutils.hxx"
#include "smallut.h"
#include "pathut.h"
#include "readfile.h"
#include "httpfs.hxx"
#include "conftree.h"
using namespace std;
using namespace UPnPP;
using namespace UPnPProvider;
// UPnP AV services. We can disable this to help pure OpenHome
// renderers which having both UPnP AV and OpenHome gets in trouble
// (Kinsky)
static string upnpAVDesc(
"<service>"
" <serviceType>urn:schemas-upnp-org:service:RenderingControl:1</serviceType>"
" <serviceId>urn:upnp-org:serviceId:RenderingControl</serviceId>"
" <SCPDURL>/upmpd/RenderingControl.xml</SCPDURL>"
" <controlURL>/ctl/RenderingControl</controlURL>"
" <eventSubURL>/evt/RenderingControl</eventSubURL>"
"</service>"
"<service>"
" <serviceType>urn:schemas-upnp-org:service:AVTransport:1</serviceType>"
" <serviceId>urn:upnp-org:serviceId:AVTransport</serviceId>"
" <SCPDURL>/upmpd/AVTransport.xml</SCPDURL>"
" <controlURL>/ctl/AVTransport</controlURL>"
" <eventSubURL>/evt/AVTransport</eventSubURL>"
"</service>"
"<service>"
" <serviceType>urn:schemas-upnp-org:service:ConnectionManager:1</serviceType>"
" <serviceId>urn:upnp-org:serviceId:ConnectionManager</serviceId>"
" <SCPDURL>/upmpd/ConnectionManager.xml</SCPDURL>"
" <controlURL>/ctl/ConnectionManager</controlURL>"
" <eventSubURL>/evt/ConnectionManager</eventSubURL>"
"</service>"
);
// The description XML document is the first thing downloaded by
// clients and tells them what services we export, and where to find
// them. The base data is in /usr/shared/upmpdcli/description.xml, it
// has a number of substitutable fields for optional data, which we
// conditionally substitute into the file data (e.g. description of
// OpenHome or other optional services)
// Description of OpenHome services
static string ohDesc(
"<service>"
" <serviceType>urn:av-openhome-org:service:Product:2</serviceType>"
" <serviceId>urn:av-openhome-org:serviceId:Product</serviceId>"
" <SCPDURL>/upmpd/OHProduct.xml</SCPDURL>"
" <controlURL>/ctl/OHProduct</controlURL>"
" <eventSubURL>/evt/OHProduct</eventSubURL>"
"</service>"
"<service>"
" <serviceType>urn:av-openhome-org:service:Info:1</serviceType>"
" <serviceId>urn:av-openhome-org:serviceId:Info</serviceId>"
" <SCPDURL>/upmpd/OHInfo.xml</SCPDURL>"
" <controlURL>/ctl/OHInfo</controlURL>"
" <eventSubURL>/evt/OHInfo</eventSubURL>"
"</service>"
"<service>"
" <serviceType>urn:av-openhome-org:service:Time:1</serviceType>"
" <serviceId>urn:av-openhome-org:serviceId:Time</serviceId>"
" <SCPDURL>/upmpd/OHTime.xml</SCPDURL>"
" <controlURL>/ctl/OHTime</controlURL>"
" <eventSubURL>/evt/OHTime</eventSubURL>"
"</service>"
"<service>"
" <serviceType>urn:av-openhome-org:service:Volume:1</serviceType>"
" <serviceId>urn:av-openhome-org:serviceId:Volume</serviceId>"
" <SCPDURL>/upmpd/OHVolume.xml</SCPDURL>"
" <controlURL>/ctl/OHVolume</controlURL>"
" <eventSubURL>/evt/OHVolume</eventSubURL>"
"</service>"
"<service>"
" <serviceType>urn:av-openhome-org:service:Playlist:1</serviceType>"
" <serviceId>urn:av-openhome-org:serviceId:Playlist</serviceId>"
" <SCPDURL>/upmpd/OHPlaylist.xml</SCPDURL>"
" <controlURL>/ctl/OHPlaylist</controlURL>"
" <eventSubURL>/evt/OHPlaylist</eventSubURL>"
"</service>"
"<service>"
" <serviceType>urn:av-openhome-org:service:Radio:1</serviceType>"
" <serviceId>urn:av-openhome-org:serviceId:Radio</serviceId>"
" <SCPDURL>/upmpd/OHRadio.xml</SCPDURL>"
" <controlURL>/ctl/OHRadio</controlURL>"
" <eventSubURL>/evt/OHRadio</eventSubURL>"
"</service>"
);
// We only advertise the Openhome Receiver service if the sc2mpd
// songcast-to-mpd gateway command is available
static string ohDescReceive(
"<service>"
" <serviceType>urn:av-openhome-org:service:Receiver:1</serviceType>"
" <serviceId>urn:av-openhome-org:serviceId:Receiver</serviceId>"
" <SCPDURL>/upmpd/OHReceiver.xml</SCPDURL>"
" <controlURL>/ctl/OHReceiver</controlURL>"
" <eventSubURL>/evt/OHReceiver</eventSubURL>"
"</service>"
);
// We want to be able to hide the credentials service because it
// breaks Lumin for some reason.
static string ohDescCreds(
"<service>"
" <serviceType>urn:av-openhome-org:service:Credentials:1</serviceType>"
" <serviceId>urn:av-openhome-org:serviceId:Credentials</serviceId>"
" <SCPDURL>/upmpd/OHCredentials.xml</SCPDURL>"
" <controlURL>/ctl/OHCredentials</controlURL>"
" <eventSubURL>/evt/OHCredentials</eventSubURL>"
"</service>"
);
static const string iconDesc(
"<iconList>"
" <icon>"
" <mimetype>image/png</mimetype>"
" <width>64</width>"
" <height>64</height>"
" <depth>32</depth>"
" <url>/upmpd/icon.png</url>"
" </icon>"
"</iconList>"
);
static const string presDesc(
"<presentationURL>/upmpd/presentation.html</presentationURL>"
);
// description file fragment for Media Server as embedded device
static const string embedms_desc(
"<device>"
"<deviceType>urn:schemas-upnp-org:device:MediaServer:1</deviceType>"
"<friendlyName>@FRIENDLYNAMEMEDIA@</friendlyName>"
"<UDN>uuid:@UUIDMEDIA@</UDN>"
"<serviceList>"
"<service>"
"<serviceType>urn:schemas-upnp-org:service:ConnectionManager:1</serviceType>"
"<serviceId>urn:upnp-org:serviceId:ConnectionManager</serviceId>"
"<SCPDURL>/upmpd/ConnectionManager.xml</SCPDURL>"
"<controlURL>/ctl1/ConnectionManager</controlURL>"
"<eventSubURL>/evt1/ConnectionManager</eventSubURL>"
"</service>"
"<service>"
"<serviceType>urn:schemas-upnp-org:service:ContentDirectory:1</serviceType>"
"<serviceId>urn:upnp-org:serviceId:ContentDirectory</serviceId>"
"<SCPDURL>/upmpd/ContentDirectory.xml</SCPDURL>"
"<controlURL>/ctl1/ContentDirectory</controlURL>"
"<eventSubURL>/evt1/ContentDirectory</eventSubURL>"
"</service>"
"</serviceList>"
"</device>"
);
// Description file for Media Server as root device
static const string msonly_desc(
"<?xml version=\"1.0\"?>"
"<root xmlns=\"urn:schemas-upnp-org:device-1-0\">"
"<specVersion><major>1</major><minor>0</minor></specVersion>"
"<device>"
"<deviceType>urn:schemas-upnp-org:device:MediaServer:1</deviceType>"
"<friendlyName>@FRIENDLYNAMEMEDIA@</friendlyName>"
"<UDN>uuid:@UUIDMEDIA@</UDN>"
"@ICONLIST@"
"<serviceList><service>"
"<serviceType>urn:schemas-upnp-org:service:ConnectionManager:1</serviceType>"
"<serviceId>urn:upnp-org:serviceId:ConnectionManager</serviceId>"
"<SCPDURL>/upmpd/ConnectionManager.xml</SCPDURL>"
"<controlURL>/ctl1/ConnectionManager</controlURL>"
"<eventSubURL>/evt1/ConnectionManager</eventSubURL>"
"</service><service>"
"<serviceType>urn:schemas-upnp-org:service:ContentDirectory:1</serviceType>"
"<serviceId>urn:upnp-org:serviceId:ContentDirectory</serviceId>"
"<SCPDURL>/upmpd/ContentDirectory.xml</SCPDURL>"
"<controlURL>/ctl1/ContentDirectory</controlURL>"
"<eventSubURL>/evt1/ContentDirectory</eventSubURL>"
"</service></serviceList>"
"</device>"
"</root>"
);
// The base XML description files. !Keep description.xml first!
static vector<const char *> xmlfilenames =
{
/* keep first */ "description.xml", /* keep first */
"RenderingControl.xml", "AVTransport.xml", "ConnectionManager.xml",
"ContentDirectory.xml"
};
// Optional OpenHome service description files
static vector<const char *> ohxmlfilenames =
{
"OHProduct.xml", "OHInfo.xml", "OHTime.xml", "OHVolume.xml",
"OHPlaylist.xml", "OHRadio.xml", "OHCredentials.xml"
};
// Read and setup our (mostly XML) data to make it available from the
// virtual directory
bool initHttpFs(unordered_map<string, VDirContent>& files,
const string& datadir,
const UDevIds& ids,
bool enableAV, bool enableOH, bool enableReceiver,
bool enableMediaServer, bool msonly,
const string& iconpath, const string& presentationhtml)
{
if (msonly) {
enableAV=enableOH=enableReceiver=false;
enableMediaServer = true;
}
if (enableOH) {
if (enableReceiver) {
ohxmlfilenames.push_back("OHReceiver.xml");
}
xmlfilenames.insert(xmlfilenames.end(), ohxmlfilenames.begin(),
ohxmlfilenames.end());
}
string reason;
string icondata;
if (!iconpath.empty()) {
if (!file_to_string(iconpath, icondata, &reason)) {
if (iconpath.compare("/usr/share/upmpdcli/icon.png")) {
LOGERR("Failed reading " << iconpath << " : " << reason << endl);
} else {
LOGDEB("Failed reading " << iconpath << " : " << reason << endl);
}
}
}
string presentationdata;
if (!presentationhtml.empty()) {
if (!file_to_string(presentationhtml, presentationdata, &reason)) {
LOGERR("Failed reading " << presentationhtml << " : " << reason <<
endl);
}
}
string dir("/upmpd/");
for (unsigned int i = 0; i < xmlfilenames.size(); i++) {
string filename = path_cat(datadir, xmlfilenames[i]);
string data;
if (!file_to_string(filename, data, &reason)) {
LOGFAT("Failed reading " << filename << " : " << reason << endl);
return false;
}
if (i == 0) {
// Description
if (!msonly) {
// Set UUID and friendlyname for renderer
data = regsub1("@UUID@", data, ids.uuid);
data = regsub1("@FRIENDLYNAME@", data, ids.fname);
}
if (enableMediaServer && !msonly) {
// Edit embedded media server description and
// subsitute it in main description
string msdesc = regsub1("@UUIDMEDIA@", embedms_desc, ids.uuidMS);
msdesc = regsub1("@FRIENDLYNAMEMEDIA@", msdesc, ids.fnameMS);
data = regsub1("@MEDIASERVER@", data, msdesc);
} else if (msonly) {
// Substitute values in msonly description
data = regsub1("@UUIDMEDIA@", msonly_desc, ids.uuidMS);
data = regsub1("@FRIENDLYNAMEMEDIA@", data, ids.fnameMS);
} else {
// No media server: erase the section
data = regsub1("@MEDIASERVER@", data, "");
}
if (enableAV) {
data = regsub1("@UPNPAV@", data, upnpAVDesc);
} else {
data = regsub1("@UPNPAV@", data, "");
}
if (enableOH) {
if (enableReceiver) {
ohDesc += ohDescReceive;
}
if (!g_lumincompat) {
ohDesc += ohDescCreds;
}
data = regsub1("@OPENHOME@", data, ohDesc);
// See comment about ohproduct version in upmpd.cxx
if (g_lumincompat) {
data = regsub1("Product:2", data, "Product:1");
}
} else {
data = regsub1("@OPENHOME@", data, "");
}
if (!icondata.empty())
data = regsub1("@ICONLIST@", data, iconDesc);
else
data = regsub1("@ICONLIST@", data, "");
if (!presentationdata.empty())
data = regsub1("@PRESENTATION@", data, presDesc);
else
data = regsub1("@PRESENTATION@", data, "");
} // End description file editing
files.insert(pair<string, VDirContent>
(dir + xmlfilenames[i],
VDirContent(data, "application/xml")));
}
if (!icondata.empty()) {
files.insert(pair<string, VDirContent>
(dir + "icon.png",
VDirContent(icondata, "image/png")));
}
if (!presentationdata.empty()) {
files.insert(pair<string, VDirContent>
(dir + "presentation.html",
VDirContent(presentationdata, "text/html")));
}
return true;
}
#ifndef _HTTPFS_H_X_INCLUDED_
#define _HTTPFS_H_X_INCLUDED_
/* 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.
*
* 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 <string>
#include <unordered_map>
#include "libupnpp/device/device.hxx"
// Ad-hoc struct to hold the input identification parameters for the device(s)
struct UDevIds {
std::string fname;
std::string uuid;
std::string fnameMS;
std::string uuidMS;
};
/**
* Initialize the files which will be served by libupnp HTTP server.
*
* The output files are the device and service description xml
* files. Some of the content is constant, some depends on data
* elements and what services are actually enabled. The source XML files which
* we include or modify are found in @param datadir.
* @param[output] files "file system" content as required by a
* UPnPProvider::UpnpDevice constructor.
* @param datadir The location for the source files.
* @param ids the friendly names and uuids for the devices, for embedding
* in desc files
*/
extern bool initHttpFs(
std::unordered_map<std::string, UPnPProvider::VDirContent>& files,
const std::string& datadir, const UDevIds& ids,
bool enableAV, bool enableOH, bool enableReceiver,
bool enableMediaServer, bool msonly,
const std::string& iconpath, const std::string& presentationhtml
);
#endif /* _HTTPFS_H_X_INCLUDED_ */
......@@ -43,9 +43,9 @@
#include "upmpd.hxx"
#include "mediaserver/mediaserver.hxx"
#include "mediaserver/contentdirectory.hxx"
#include "httpfs.hxx"
#include "upmpdutils.hxx"
#include "pathut.h"
#include "readfile.h"
using namespace std;
using namespace UPnPP;
......@@ -81,8 +81,8 @@ static const char usage[] =
"-P upport \t specify port number to be used for UPnP\n"
"-O 0|1\t decide if we run and export the OpenHome services\n"
"-v \tprint version info\n"
"-m <0|1|2|3> media server mode "
"(default, forked|only renderer|only media|combined)\n"
"-m <0|1|2|3|4> media server mode "
"(default, forked|only renderer|only media|combined/embedded|combined/multidev)\n"
"\n"
;
......@@ -95,18 +95,20 @@ static const char usage[] =
// control points are confused by embedded devices.
//
// - -m 0, default, Forked: this is for the main process, which will
// implement a Media Renderer device, and, if needed, fork/exec the
// Media Server (with option -m 2)
// implement a Media Renderer device, and, if needed, fork/exec the
// Media Server (with option -m 2)
// - -m 1, RdrOnly: for the main instance: be a Renderer, do not start the
// Media Server even if the configuration indicates it is needed
// (this is not used in normal situations, just edit the config
// instead!)
// Media Server even if the configuration indicates it is needed
// (this is not used in normal situations, just edit the config
// instead!)
// - -m 2, MSOnly Media Server only, this is for the process forked/execed
// by a main Renderer process, or a standalone Media Server.
// by a main Renderer process, or a standalone Media Server.
// - -m 3, Combined: for the main process: implement the Media Server
// as an embedded device. This works just fine with, for example,
// upplay, but confuses most of the other Control Points.
enum MSMode {Forked, RdrOnly, MSOnly, Combined};
// as an embedded device. This works just fine with, for example,
// upplay, but confuses most of the other Control Points.
// - -m 4, Combined: for the main process: implement the Media Server
// as a separate root device. Only works with a modified libupnp.
enum MSMode {Forked, RdrOnly, MSOnly, CombinedEmbedded, CombinedMultiDev};
static void
versionInfo(FILE *fp)
......@@ -150,8 +152,16 @@ ohProductDesc_t ohProductDesc = {
}
};
// Ad-hoc struct to hold the input identification parameters for the device(s)
struct UDevIds {
std::string fname;
std::string uuid;
std::string fnameMS;
std::string uuidMS;
};
// Static for cleanup in sig handler.
static UpnpDevice *dev;
static vector<UpnpDevice *> devs;
string g_datadir(DATADIR "/");
string g_cachedir("/var/cache/upmpdcli");
......@@ -165,7 +175,9 @@ bool g_lumincompat = false;
static void onsig(int)
{
LOGDEB("Got sig" << endl);
dev->shouldExit();
for (auto& dev : devs) {
dev->shouldExit();
}
}
static const int catchedSigs[] = {SIGINT, SIGQUIT, SIGTERM};
......@@ -247,8 +259,9 @@ int main(int argc, char *argv[])
unsigned short upport = 0;
string upnpip;
int msm = 0;
bool inprocessms = false;
bool msonly = false;
bool inprocessms{false};
bool msonly{false};
bool msroot{false};
const char *cp;
if ((cp = getenv("UPMPD_HOST")))
......@@ -308,7 +321,7 @@ int main(int argc, char *argv[])
b1: argc--; argv++;
}
if (argc != 0 || msm < 0 || msm > 3) {
if (argc != 0 || msm < 0 || msm > 4) {
Usage();
}
MSMode arg_msmode = MSMode(msm);
......@@ -442,9 +455,15 @@ int main(int argc, char *argv[])
inprocessms = true;
msonly = true;
break;
case Combined:
case CombinedEmbedded:
inprocessms = true;
msonly = false;
msroot = false;
break;
case CombinedMultiDev:
inprocessms = true;
msonly = false;
msroot = true;
break;
case RdrOnly:
case Forked:
......@@ -690,15 +709,8 @@ int main(int argc, char *argv[])
pidfilename = pidfilename + "-ms";
}
// Initialize the data we serve through HTTP (device and service
// descriptions, icons, presentation page, etc.)
unordered_map<string, VDirContent> files;
if (!initHttpFs(files, g_datadir, ids, enableAV, enableOH,
!senderpath.empty(), inprocessms, msonly,
iconpath, presentationhtml)) {
exit(1);
}
opts.iconpath = iconpath;
opts.presentationhtml = presentationhtml;
if (ownqueue)
opts.options |= UpMpd::upmpdOwnQueue;
if (enableOH)
......@@ -734,48 +746,49 @@ int main(int argc, char *argv[])
if (!enableAV)
opts.options |= UpMpd::upmpdNoAV;
setupsigs();
UpMpd *mediarenderer{nullptr};
if (!msonly) {
mediarenderer = new UpMpd(string("uuid:") + ids.uuid, ids.fname,
ohProductDesc, files, mpdclip, opts);
ohProductDesc, mpdclip, opts);
devs.push_back(mediarenderer);
}
MediaServer *mediaserver = nullptr;
unordered_map<string, VDirContent> emptyfiles =
unordered_map<string, VDirContent>();
unordered_map<string, VDirContent> *msfiles = &emptyfiles;
MediaServer *mediaserver{nullptr};
if (inprocessms) {
if (msonly) {
msfiles = &files;
}
// Create the Media Server embedded device object. There needs
// be no reference to the root object because there can be
// only one (libupnp restriction)
mediaserver = new MediaServer(string("uuid:") + ids.uuidMS, ids.fnameMS,
enableMediaServer, *msfiles);
// Create the Media Server device. If msonly is set, both
// branches do the same thing and create a root device
// (mediarenderer is null).
// The multidev modified libupnp is needed for using 2 root devices
mediaserver = new MediaServer(msroot?nullptr:mediarenderer, string("uuid:") +
ids.uuidMS,ids.fnameMS, enableMediaServer);