--- a
+++ b/upsend_src/upsend.cpp
@@ -0,0 +1,323 @@
+/* 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.
+ *
+ * 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 <stdlib.h>
+#include <unistd.h>
+#include <getopt.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <strings.h>
+
+#include <string>
+#include <iostream>
+
+#include "libupnpp/upnpplib.hxx"
+#include "libupnpp/log.hxx"
+#include "libupnpp/soaphelp.hxx"
+#include "libupnpp/upnpputils.hxx"
+#include "libupnpp/control/mediarenderer.hxx"
+#include "libupnpp/control/avtransport.hxx"
+#include "libupnpp/control/discovery.hxx"
+
+#include "streamer.h"
+#include "wav.h"
+
+using namespace std;
+using namespace UPnPClient;
+using namespace UPnPP;
+
+WorkQueue<AudioMessage*> audioqueue("audioqueue", 4);
+
+// @param name can be uuid or friendly name, we try both. The chance that a
+// device would have a uuid which would be the friendly name of
+// another is small...
+MRDH getRenderer(const string& name)
+{
+ static UPnPDeviceDirectory *superdir;
+ if (superdir == 0) {
+ superdir = UPnPDeviceDirectory::getTheDir();
+ if (superdir == 0) {
+ cerr << "Discovery init failed\n";
+ return MRDH();
+ }
+ }
+
+ UPnPDeviceDesc ddesc;
+ if (superdir->getDevByFName(name, ddesc)) {
+ return MRDH(new MediaRenderer(ddesc));
+ } else if (superdir->getDevByUDN(name, ddesc)) {
+ return MRDH(new MediaRenderer(ddesc));
+ }
+ cerr << "Can't connect to " << name << endl;
+ return MRDH();
+}
+
+static string path_suffix(const string& s)
+{
+ string::size_type dotp = s.rfind('.');
+ if (dotp == string::npos) {
+ return string();
+ }
+ return s.substr(dotp + 1);
+}
+
+static bool whatfile(const string& audiofile, AudioSink::Context *ctxt)
+{
+ if (access(audiofile.c_str(), R_OK) != 0) {
+ cerr << "No read access " << audiofile << " errno " << errno << endl;
+ return false;
+ }
+ struct stat st;
+ if (stat(audiofile.c_str(), &st)) {
+ cerr << "Can't stat " << audiofile << " errno " << errno << endl;
+ return false;
+ }
+
+ string ext = path_suffix(audiofile);
+ ctxt->filename = audiofile;
+ ctxt->filesize = st.st_size;
+ ctxt->ext = ext;
+ const char* cext = ext.c_str();
+ if (!strcasecmp("flac", cext)) {
+ ctxt->content_type = "audio/flac";
+ } else if (!strcasecmp("mp3", cext)) {
+ ctxt->content_type = "audio/mpeg";
+ } else if (!strcasecmp("wav", cext)) {
+ ctxt->content_type = "audio/wav";
+ } else {
+ cerr << "Unknown extension " << ext << endl;
+ return false;
+ }
+ return true;
+}
+
+void *readworker(void *a)
+{
+ AudioSink::Context *ctxt = (AudioSink::Context *)a;
+
+ int fd = 0;
+ if (ctxt->filename.compare("stdin")) {
+ if ((fd = open(ctxt->filename.c_str(), O_RDONLY)) < 0) {
+ cerr << "readWorker: can't open " << ctxt->filename <<
+ " for reading, errno " << errno << endl;
+ exit(1);
+ }
+ }
+
+ for (;;) {
+ unsigned int allocbytes = 4096;
+ char *buf = (char *)malloc(allocbytes);
+ if (buf == 0) {
+ cerr << "readWorker: can't allocate " << allocbytes << " bytes\n";
+ exit(1);
+ }
+ ssize_t readbytes = read(fd, buf, allocbytes);
+ //cerr << "readworker: got " << readbytes << "bytes\n";
+ if (readbytes < 0) {
+ cerr << "readWorker: read error on " << ctxt->filename <<
+ " errno " << errno << endl;
+ exit(1);
+ } else if (readbytes == 0) {
+ audioqueue.waitIdle();
+ audioqueue.setTerminateAndWait();
+ return nullptr;
+ }
+ AudioMessage *ap = new AudioMessage(buf, readbytes, allocbytes);
+ if (!audioqueue.put(ap, false)) {
+ cerr << "readWorker: queue dead: exiting\n";
+ exit(1);
+ }
+ }
+}
+
+string didlmake(const string& uri, const string& mime)
+{
+ ostringstream ss;
+ ss << "<?xml version=\"1.0\" encoding=\"utf-8\"?>"
+ "<DIDL-Lite xmlns:dc=\"http://purl.org/dc/elements/1.1/\" "
+ "xmlns:upnp=\"urn:schemas-upnp-org:metadata-1-0/upnp/\" "
+ "xmlns=\"urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/\" "
+ "xmlns:dlna=\"urn:schemas-dlna-org:metadata-1-0/\">"
+ << "<item restricted=\"1\">";
+
+ ss << "<dc:title>" << SoapHelp::xmlQuote("Streaming") << "</dc:title>";
+ ss << "<upnp:class>object.item.audioItem.musicTrack</upnp:class>";
+
+#warning "problem with resource values!"
+ ss << "<res " << "duration=\"" << upnpduration(30)
+ << "\" "
+ << "sampleFrequency=\"44100\" audioChannels=\"2\" "
+ << "protocolInfo=\"http-get:*:" << mime << ":*\""
+ << ">"
+ << SoapHelp::xmlQuote(uri)
+ << "</res>"
+ << "</item></DIDL-Lite>";
+ return ss.str();
+}
+
+static char *thisprog;
+static char usage [] =
+"<audiofile> <renderer> : play audio on given renderer\n"
+ ;
+static void Usage(void)
+{
+ fprintf(stderr, "%s: usage:\n%s", thisprog, usage);
+ exit(1);
+}
+static int op_flags;
+#define OPT_h 0x1
+#define OPT_p 0x2
+static struct option long_options[] = {
+ {"host", required_argument, 0, 'h'},
+ {"port", required_argument, 0, 'p'},
+ {0, 0, 0, 0}
+};
+
+
+int main(int argc, char *argv[])
+{
+ thisprog = argv[0];
+ string host = "localhost";
+ int port = 8869;
+
+ int option_index = 0;
+ int ret;
+ while ((ret = getopt_long(argc, argv, "h:p:",
+ long_options, &option_index)) != -1) {
+ cerr << "ret is " << ret << endl;
+ switch (ret) {
+ case 'h': op_flags |= OPT_h; host = optarg; break;
+ case 'p': op_flags |= OPT_h; port = atoi(optarg); break;
+ default: Usage();
+ }
+ }
+
+ if (optind != argc - 2)
+ Usage();
+ string audiofile = argv[optind++];
+ string renderer = argv[optind++];
+
+ if (Logger::getTheLog("stderr") == 0) {
+ cerr << "Can't initialize log" << endl;
+ return 1;
+ }
+ Logger::getTheLog("")->setLogLevel(Logger::LLDEB1);
+
+ string hwa;
+ LibUPnP *mylib = LibUPnP::getLibUPnP(false, &hwa);
+ if (!mylib) {
+ cerr << "Can't get LibUPnP" << endl;
+ return 1;
+ }
+
+ if (!(op_flags & OPT_h)) {
+#if FUTURE
+ host = mylib->host();
+ if (host.empty()) {
+ cerr << "Can't retrieve IP address\n";
+ return 1;
+ }
+#else
+ char hostname[1024];
+ if (gethostname(hostname, 1024)) {
+ perror("gethostname failed. use -h:");
+ return 1;
+ }
+ host = hostname;
+#endif
+ }
+ if (!mylib->ok()) {
+ cerr << "Lib init failed: " <<
+ mylib->errAsString("main", mylib->getInitError()) << endl;
+ return 1;
+ }
+ //mylib->setLogFileName("/tmp/libupnp.log", LibUPnP::LogLevelDebug);
+
+ MRDH rdr = getRenderer(renderer);
+ if (!rdr) {
+ cerr << "Can't connect to renderer\n";
+ return 1;
+ }
+ AVTH avth = rdr->avt();
+ if (!avth) {
+ cerr << "Device has no AVTransport service" << endl;
+ return 1;
+ }
+
+
+ // Identify file
+ AudioSink::Context *ctxt = new AudioSink::Context(&audioqueue);
+ bool makewav = false;
+ if (!audiofile.compare("stdin")) {
+ ctxt->filename = audiofile;
+ ctxt->ext = "wav";
+ ctxt->content_type = "audio/wav";
+ makewav = true;
+ } else {
+ if (!whatfile(audiofile, ctxt)) {
+ cerr << "Can't identify file " << audiofile << endl;
+ return 1;
+ }
+ }
+
+ unordered_map<string,string> c{{"httpport", SoapHelp::i2s(port)},
+ {"httphost", host}};
+ ctxt->config = c;
+
+ // Start the http thread
+ audioqueue.start(1, (void *(*)(void *))(httpAudioSink.worker), ctxt);
+
+ if (makewav) {
+ unsigned int allocbytes = 512;
+ char *buf = (char *)malloc(allocbytes);
+ if (buf == 0) {
+ cerr << "Can't allocate " << allocbytes << " bytes\n";
+ exit(1);
+ }
+ int freq = 44100;
+ int bits = 16;
+ int chans = 2;
+ int databytes = 2 * 1000 * 1000 * 1000;
+ // Using buf+bytes in case we ever insert icy before the audio
+ int sz = makewavheader(buf, allocbytes, freq, bits, chans, databytes);
+ AudioMessage *ap = new AudioMessage(buf, sz, allocbytes);
+ audioqueue.put(ap, false);
+ }
+
+ // Start the reading thread
+ std::thread readthread(readworker, ctxt);
+
+ string uri("http://" + host + ":" + SoapHelp::i2s(port) + "/stream." +
+ ctxt->ext);
+
+ // We'd need a few options here to decide what to do if already playing:
+ // wait (would allow to queue multiple songs), or interrupt.
+
+ // Start the renderer
+ if (avth->setAVTransportURI(uri, didlmake(uri, ctxt->content_type)) != 0) {
+ cerr << "setAVTransportURI failed\n";
+ return 1;
+ }
+
+ if (avth->play() != 0) {
+ cerr << "play failed\n";
+ return 1;
+ }
+ readthread.join();
+ return 0;
+}