chiark / gitweb /
disobedience can stop/start a background rtp player now
authorRichard Kettlewell <rjk@greenend.org.uk>
Sat, 20 Oct 2007 10:27:00 +0000 (11:27 +0100)
committerRichard Kettlewell <rjk@greenend.org.uk>
Sat, 20 Oct 2007 10:27:00 +0000 (11:27 +0100)
13 files changed:
clients/playrtp.c
disobedience/Makefile.am
disobedience/control.c
disobedience/disobedience.c
disobedience/disobedience.h
disobedience/rtp.c [new file with mode: 0644]
images/Makefile.am
images/speaker.png [new file with mode: 0644]
images/speaker.xcf [new file with mode: 0644]
images/speakercross.png [new file with mode: 0644]
lib/eclient.c
lib/eclient.h
lib/inputline.c

index 6e428aa..f92d872 100644 (file)
@@ -67,6 +67,7 @@
 #include <errno.h>
 #include <netinet/in.h>
 #include <sys/time.h>
+#include <sys/un.h>
 
 #include "log.h"
 #include "mem.h"
@@ -80,6 +81,7 @@
 #include "timeval.h"
 #include "client.h"
 #include "playrtp.h"
+#include "inputline.h"
 
 #define readahead linux_headers_are_borked
 
@@ -184,6 +186,9 @@ static void (*backend)(void) = &DEFAULT_BACKEND;
 
 HEAP_DEFINE(pheap, struct packet *, lt_packet);
 
+/** @brief Control socket or NULL */
+const char *control_socket;
+
 static const struct option options[] = {
   { "help", no_argument, 0, 'h' },
   { "version", no_argument, 0, 'V' },
@@ -203,10 +208,77 @@ static const struct option options[] = {
 #if HAVE_COREAUDIO_AUDIOHARDWARE_H
   { "core-audio", no_argument, 0, 'c' },
 #endif
+  { "socket", required_argument, 0, 's' },
   { "config", required_argument, 0, 'C' },
   { 0, 0, 0, 0 }
 };
 
+/** @brief Control thread
+ *
+ * This thread is responsible for accepting control commands from Disobedience
+ * (or other controllers) over an AF_UNIX stream socket with a path specified
+ * by the @c --socket option.  The protocol uses simple string commands and
+ * replies:
+ *
+ * - @c stop will shut the player down
+ * - @c query will send back the reply @c running
+ * - anything else is ignored
+ *
+ * Commands and response strings terminated by shutting down the connection or
+ * by a newline.  No attempt is made to multiplex multiple clients so it is
+ * important that the command be sent as soon as the connection is made - it is
+ * assumed that both parties to the protocol are entirely cooperating with one
+ * another.
+ */
+static void *control_thread(void attribute((unused)) *arg) {
+  struct sockaddr_un sa;
+  int sfd, cfd;
+  char *line;
+  socklen_t salen;
+  FILE *fp;
+
+  assert(control_socket);
+  unlink(control_socket);
+  memset(&sa, 0, sizeof sa);
+  sa.sun_family = AF_UNIX;
+  strcpy(sa.sun_path, control_socket);
+  sfd = xsocket(PF_UNIX, SOCK_STREAM, 0);
+  if(bind(sfd, (const struct sockaddr *)&sa, sizeof sa) < 0)
+    fatal(errno, "error binding to %s", control_socket);
+  if(listen(sfd, 128) < 0)
+    fatal(errno, "error calling listen on %s", control_socket);
+  info("listening on %s", control_socket);
+  for(;;) {
+    salen = sizeof sa;
+    cfd = accept(sfd, (struct sockaddr *)&sa, &salen);
+    if(cfd < 0) {
+      switch(errno) {
+      case EINTR:
+      case EAGAIN:
+        break;
+      default:
+        fatal(errno, "error calling accept on %s", control_socket);
+      }
+    }
+    if(!(fp = fdopen(cfd, "r+"))) {
+      error(errno, "error calling fdopen for %s connection", control_socket);
+      close(cfd);
+      continue;
+    }
+    if(!inputline(control_socket, fp, &line, '\n')) {
+      if(!strcmp(line, "stop")) {
+        info("stopped via %s", control_socket);
+        exit(0);                          /* terminate immediately */
+      }
+      if(!strcmp(line, "query"))
+        fprintf(fp, "running");
+      xfree(line);
+    }
+    if(fclose(fp) < 0)
+      error(errno, "error closing %s connection", control_socket);
+  }
+}
+
 /** @brief Drop the first packet
  *
  * Assumes that @ref lock is held. 
@@ -396,11 +468,14 @@ struct packet *playrtp_next_packet(void) {
  */
 static void play_rtp(void) {
   pthread_t ltid;
+  int err;
 
   /* We receive and convert audio data in a background thread */
-  pthread_create(&ltid, 0, listen_thread, 0);
+  if((err = pthread_create(&ltid, 0, listen_thread, 0)))
+    fatal(err, "pthread_create listen_thread");
   /* We have a second thread to add received packets to the queue */
-  pthread_create(&ltid, 0, queue_thread, 0);
+  if((err = pthread_create(&ltid, 0, queue_thread, 0)))
+    fatal(err, "pthread_create queue_thread");
   /* The rest of the work is backend-specific */
   backend();
 }
@@ -441,7 +516,7 @@ static void version(void) {
 }
 
 int main(int argc, char **argv) {
-  int n;
+  int n, err;
   struct addrinfo *res;
   struct stringlist sl;
   char *sockname;
@@ -488,6 +563,7 @@ int main(int argc, char **argv) {
     case 'c': backend = playrtp_coreaudio; break;
 #endif
     case 'C': configfile = optarg; break;
+    case 's': control_socket = optarg; break;
     default: fatal(0, "invalid option");
     }
   }
@@ -561,6 +637,12 @@ int main(int argc, char **argv) {
     info("default socket receive buffer %d", rcvbuf);
   if(logfp)
     info("WARNING: -L option can impact performance");
+  if(control_socket) {
+    pthread_t tid;
+
+    if((err = pthread_create(&tid, 0, control_thread, 0)))
+      fatal(err, "pthread_create control_thread");
+  }
   play_rtp();
   return 0;
 }
index 1f9bd88..86d1102 100644 (file)
@@ -25,7 +25,7 @@ AM_CFLAGS=$(GLIB_CFLAGS) $(GTK_CFLAGS)
 
 disobedience_SOURCES=disobedience.h disobedience.c client.c queue.c    \
                  choose.c misc.c style.h control.c properties.c menu.c \
-                 log.c progress.c login.c \
+                 log.c progress.c login.c rtp.c \
                  ../lib/memgc.c
 disobedience_LDADD=../lib/libdisorder.a $(LIBPCRE) $(LIBGC) $(LIBGCRYPT)
 disobedience_LDFLAGS=$(GTK_LIBS)
index f4541f7..39bced1 100644 (file)
@@ -42,8 +42,17 @@ static void update_random_enable(const struct icon *);
 static void update_random_disable(const struct icon *);
 static void update_enable(const struct icon *);
 static void update_disable(const struct icon *);
+static void update_rtp(const struct icon *);
+static void update_nortp(const struct icon *);
 static void clicked_icon(GtkButton *, gpointer);
 
+static int enable_rtp(disorder_eclient *c,
+                      disorder_eclient_no_response *completed,
+                      void *v);
+static int disable_rtp(disorder_eclient *c,
+                       disorder_eclient_no_response *completed,
+                       void *v);
+
 static double left(double v, double b);
 static double right(double v, double b);
 static double volume(double l, double r);
@@ -101,6 +110,10 @@ static struct icon icons[] = {
     disorder_eclient_enable, 0 },
   { "notescross.png", "Disable play", clicked_icon, update_disable,
     disorder_eclient_disable, 0 },
+  { "speaker.png", "Play network stream", clicked_icon, update_rtp,
+    enable_rtp, 0 },
+  { "speakercross.png", "Stop playing network stream", clicked_icon, update_nortp,
+    disable_rtp, 0 },
 };
 
 /** @brief Count of icons */
@@ -110,7 +123,7 @@ static GtkAdjustment *volume_adj;
 static GtkAdjustment *balance_adj;
 
 /** @brief Called whenever last_state changes in any way */
-static void control_monitor(void attribute((unused)) *u) {
+void control_monitor(void attribute((unused)) *u) {
   int n;
 
   D(("control_monitor"));
@@ -265,6 +278,18 @@ static void update_disable(const struct icon *icon) {
   update_icon(icon, visible, usable);
 }
 
+static void update_rtp(const struct icon *icon) {
+  const int visible = !rtp_is_running;
+  const int usable = rtp_supported;
+  update_icon(icon, visible, usable);
+}
+
+static void update_nortp(const struct icon *icon) {
+  const int visible = rtp_is_running;
+  const int usable = rtp_supported;
+  update_icon(icon, visible, usable);
+}
+
 static void clicked_icon(GtkButton attribute((unused)) *button,
                          gpointer userdata) {
   const struct icon *icon = userdata;
@@ -377,6 +402,30 @@ static double balance(double l, double r) {
     return 0;
 }
 
+/** @brief Called to enable RTP play
+ *
+ * Rather odd signature is to fit in with the other icons which all call @ref
+ * lib/eclient.h functions.
+ */
+static int enable_rtp(disorder_eclient attribute((unused)) *c,
+                      disorder_eclient_no_response attribute((unused)) *completed,
+                      void attribute((unused)) *v) {
+  start_rtp();
+  return 0;
+}
+
+/** @brief Called to disable RTP play
+ *
+ * Rather odd signature is to fit in with the other icons which all call @ref
+ * lib/eclient.h functions.
+ */
+static int disable_rtp(disorder_eclient attribute((unused)) *c,
+                       disorder_eclient_no_response attribute((unused)) *completed,
+                       void attribute((unused)) *v) {
+  stop_rtp();
+  return 0;
+}
+
 /*
 Local Variables:
 c-basic-offset:2
index 6271560..d99ecef 100644 (file)
@@ -75,9 +75,21 @@ double goesupto = 10;                   /* volume upper bound */
 /** @brief True if a NOP is in flight */
 static int nop_in_flight;
 
+/** @brief True if an rtp-address command is in flight */
+static int rtp_address_in_flight;
+
 /** @brief Global tooltip group */
 GtkTooltips *tips;
 
+/** @brief True if RTP play is available
+ *
+ * This is a bit of a bodge...
+ */
+int rtp_supported;
+
+/** @brief True if RTP play is enabled */
+int rtp_is_running;
+
 /** @brief Linked list of functions to call when we reset login parameters */
 static struct reset_callback_node {
   struct reset_callback_node *next;
@@ -316,9 +328,44 @@ static gboolean maybe_send_nop(gpointer attribute((unused)) data) {
     nop_in_flight = 1;
     disorder_eclient_nop(client, nop_completed, 0);
   }
+  if(rtp_supported) {
+    const int old_state = rtp_is_running;
+    rtp_is_running = rtp_running();
+    if(old_state != rtp_is_running)
+      control_monitor(0);
+  }
   return TRUE;                          /* keep call me please */
 }
 
+/** @brief Called when a rtp-address command succeeds */
+static void got_rtp_address(void attribute((unused)) *v,
+                            int attribute((unused)) nvec,
+                            char attribute((unused)) **vec) {
+  rtp_address_in_flight = 0;
+  rtp_supported = 1;
+  rtp_is_running = rtp_running();
+  control_monitor(0);
+}
+
+/** @brief Called when a rtp-address command fails */
+static void no_rtp_address(struct callbackdata attribute((unused)) *cbd,
+                           int attribute((unused)) code,
+                           const char attribute((unused)) *msg) {
+  rtp_address_in_flight = 0;
+  rtp_supported = 0;
+  rtp_is_running = 0;
+  control_monitor(0);
+}
+
+/** @brief Called to check whether RTP play is available */
+static void check_rtp_address(void) {
+  if(!rtp_address_in_flight) {
+    struct callbackdata *const cbd = xmalloc(sizeof *cbd);
+    cbd->onerror = no_rtp_address;
+    disorder_eclient_rtp_address(client, got_rtp_address, cbd);
+  }
+}
+
 /* main -------------------------------------------------------------------- */
 
 static const struct option options[] = {
@@ -361,8 +408,11 @@ void reset(void) {
   /* reset the clients */
   disorder_eclient_close(client);
   disorder_eclient_close(logclient);
+  rtp_supported = 0;
   for(r = resets; r; r = r->next)
     r->callback();
+  /* Might be a new server so re-check */
+  check_rtp_address();
 }
 
 /** @brief Register a reset callback */
@@ -432,6 +482,8 @@ int main(int argc, char **argv) {
   register_reset(properties_reset);
   /* Start monitoring the log */
   disorder_eclient_log(logclient, &log_callbacks, 0);
+  /* See if RTP play supported */
+  check_rtp_address();
   D(("enter main loop"));
   MTAG("misc");
   g_main_loop_run(mainloop);
index 70eee10..6614d8b 100644 (file)
@@ -111,6 +111,9 @@ extern int volume_l, volume_r;          /* current volume */
 extern double goesupto;                 /* volume upper bound */
 extern int choosealpha;                 /* break up choose by letter */
 extern GtkTooltips *tips;
+extern int rtp_supported;
+extern int rtp_is_running;
+
 
 extern const disorder_eclient_log_callbacks log_callbacks;
 
@@ -189,6 +192,8 @@ GtkWidget *control_widget(void);
 void volume_update(void);
 /* Called whenever we think the volume control has changed */
 
+void control_monitor(void *u);
+
 /* Queue/Recent/Added */
 
 GtkWidget *queue_widget(void);
@@ -226,8 +231,16 @@ GtkWidget *choose_widget(void);
 void choose_update(void);
 /* Called when we think the choose tree might need updating */
 
+/* Login details */
+
 void login_box(void);
 
+/* RTP */
+
+int rtp_running(void);
+void start_rtp(void);
+void stop_rtp(void);
+
 /* Widget leakage debugging rubbish ---------------------------------------- */
 
 #if MDEBUG
diff --git a/disobedience/rtp.c b/disobedience/rtp.c
new file mode 100644 (file)
index 0000000..a4efed5
--- /dev/null
@@ -0,0 +1,156 @@
+/*
+ * This file is part of Disobedience
+ * Copyright (C) 2007 Richard Kettlewell
+ *
+ * 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
+ */
+/** @file disobedience/rtp.c
+ * @brief RTP player support for Disobedience
+ */
+
+#include "disobedience.h"
+#include <sys/stat.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <sys/un.h>
+
+/** @brief Path to RTP player's control socket */
+static char *rtp_socket;
+
+/** @brief Path to RTP player's logfile */
+static char *rtp_log;
+
+/** @brief Initialize @ref rtp_socket and @ref rtp_log if necessary */
+static void rtp_init(void) {
+  if(!rtp_socket) {
+    const char *home = getenv("HOME");
+    char *dir;
+
+    byte_xasprintf(&dir, "%s/.disorder", home);
+    mkdir(dir, 02700);
+    byte_xasprintf(&rtp_socket, "%s/rtp", dir);
+    byte_xasprintf(&rtp_log, "%s/rtp.log", dir);
+  }
+}
+
+/** @brief Return a connection to the RTP player's control socket */
+static FILE *rtp_connect(void) {
+  struct sockaddr_un un;
+  int fd;
+  FILE *fp;
+
+  rtp_init();
+  memset(&un, 0, sizeof un);
+  un.sun_family = AF_UNIX;
+  strcpy(un.sun_path, rtp_socket);
+  fd = xsocket(PF_UNIX, SOCK_STREAM, 0);
+  if(connect(fd, (const struct sockaddr *)&un, sizeof un) < 0) {
+    /* Connection refused just means that the player quit without deleting its
+     * socket */
+    switch(errno) {
+    case ECONNREFUSED:
+    case ENOENT:
+      break;
+    default:
+      /* Anything else may be a problem */
+      fpopup_msg(GTK_MESSAGE_ERROR, "connecting to %s: %s",
+                 rtp_socket, strerror(errno));
+      break;
+    }
+    close(fd);
+    return 0;
+  }
+  if(!(fp = fdopen(fd, "r+"))) {
+    fpopup_msg(GTK_MESSAGE_ERROR, "error calling fdopen: %s", strerror(errno));
+    close(fd);
+    return 0;
+  }
+  return fp;
+}
+
+/** @brief Return non-0 iff the RTP player is running */
+int rtp_running(void) {
+  FILE *fp = rtp_connect();
+
+  if(!fp)
+    return 0;                           /* no connection -> not running */
+  fclose(fp);                           /* connection -> running */
+  return 1;
+}
+
+/** @brief Activate the RTP player if it is not running */
+void start_rtp(void) {
+  pid_t pid;
+  int w, fd;
+
+  if(rtp_running())
+    return;                             /* already running */
+  /* double-fork so we don't have to wait() later */
+  if(!(pid = xfork())) {
+    if(setsid() < 0)
+      fatal(errno, "error calling setsid");
+    if(!(pid = xfork())) {
+      /* grandchild */
+      exitfn = _exit;
+      /* log errors and output somewhere reasonably sane.  rtp_running()
+       * will have made sure the directory exists. */
+      if((fd = open(rtp_log, O_WRONLY|O_CREAT|O_TRUNC, 0600)) < 0)
+       fatal(errno, "creating %s", rtp_log);
+      if(dup2(fd, 1) < 0
+        || dup2(fd, 2) < 0)
+       fatal(errno, "dup2");
+      if(close(fd) < 0)
+       fatal(errno, "close");
+      /* We don't want to hang onto whatever stdin was */
+      if((fd = open("/dev/null", O_RDONLY)) < 0)
+        fatal(errno, "opening /dev/null");
+      if(dup2(fd, 0) < 0)
+        fatal(errno, "dup2");
+      if(close(fd) < 0)
+       fatal(errno, "close");
+      /* execute the player */
+      execlp("disorder-playrtp",
+            "disorder-playrtp", "--socket", rtp_socket, (char *)0);
+      fatal(errno, "disorder-playrtp");
+    } else {
+      /* child */
+      _exit(0);
+    }
+  } else {
+    /* parent */
+    while(waitpid(pid, &w, 0) < 0 && errno == EINTR)
+      ;
+  }
+}
+
+/** @brief Stop the RTP player if it is running */
+void stop_rtp(void) {
+  FILE *fp = rtp_connect();
+
+  if(!fp)
+    return;                             /* already stopped */
+  fprintf(fp, "stop\n");
+  fclose(fp);
+}
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+indent-tabs-mode:nil
+End:
+*/
index 90f3a22..a8e4ddf 100644 (file)
@@ -21,7 +21,7 @@
 static_DATA=cross.png down.png downdown.png edit.png nocross.png       \
 nodown.png nodowndown.png noup.png noupup.png tick.png up.png upup.png  \
 notes.png play.png pause.png random.png randomcross.png notescross.png  \
-propagate.png
+propagate.png speaker.png speakercross.png
 
 staticdir=${pkgdatadir}/static
 
diff --git a/images/speaker.png b/images/speaker.png
new file mode 100644 (file)
index 0000000..c042397
Binary files /dev/null and b/images/speaker.png differ
diff --git a/images/speaker.xcf b/images/speaker.xcf
new file mode 100644 (file)
index 0000000..3eb1d52
Binary files /dev/null and b/images/speaker.xcf differ
diff --git a/images/speakercross.png b/images/speakercross.png
new file mode 100644 (file)
index 0000000..86c77f6
Binary files /dev/null and b/images/speakercross.png differ
index e175c22..fe03c99 100644 (file)
@@ -1177,6 +1177,35 @@ int disorder_eclient_new_tracks(disorder_eclient *c,
                 "new", limit, (char *)0);
 }
 
+static void rtp_response_opcallback(disorder_eclient *c,
+                                    struct operation *op) {
+  D(("rtp_response_opcallback"));
+  if(c->rc / 100 == 2) {
+    if(op->completed) {
+      int nvec;
+      char **vec = split(c->line + 4, &nvec, SPLIT_QUOTES, 0, 0);
+
+      ((disorder_eclient_list_response *)op->completed)(op->v, nvec, vec);
+    }
+  } else
+    protocol_error(c, op, c->rc, "%s: %s", c->ident, c->line);
+}
+
+/** @brief Determine the RTP target address
+ * @param c Client
+ * @param completed Called with address details
+ * @param v Passed to @p completed
+ *
+ * The address details will be two elements, the first being the hostname and
+ * the second the service (port).
+ */
+int disorder_eclient_rtp_address(disorder_eclient *c,
+                                 disorder_eclient_list_response *completed,
+                                 void *v) {
+  return simple(c, rtp_response_opcallback, (void (*)())completed, v,
+                "rtp-address", (char *)0);
+}
+
 /* Log clients ***************************************************************/
 
 /** @brief Monitor the server log
index 0ced5de..37ec9cb 100644 (file)
@@ -321,6 +321,11 @@ int disorder_eclient_new_tracks(disorder_eclient *c,
                                 disorder_eclient_list_response *completed,
                                 int max,
                                 void *v);
+
+int disorder_eclient_rtp_address(disorder_eclient *c,
+                                 disorder_eclient_list_response *completed,
+                                 void *v);
+
 #endif
 
 /*
index 416162c..3202eff 100644 (file)
@@ -17,6 +17,9 @@
  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
  * USA
  */
+/** @file lib/inputline.c
+ * @brief Line input
+ */
 
 #include <config.h>
 #include "types.h"
 #include "charset.h"
 #include "inputline.h"
 
+/** @brief Read a line from @p fp
+ * @param tag Used in error messages
+ * @param fp Stream to read from
+ * @param lp Where to store newly allocated string
+ * @param newline Newline character
+ * @return 0 on success, -1 on error or eof.
+ *
+ * The newline is not included in the string.  If the last line of a
+ * stream does not have a newline then that line is still returned.
+ *
+ * @p *lp is only set if the return value was 0.
+ */
 int inputline(const char *tag, FILE *fp, char **lp, int newline) {
   struct dynstr d;
   int ch;