chiark / gitweb /
clients/playrtp.c: Replace the hairy interface-scanning heuristics.
[disorder] / clients / playrtp.c
index 2ab67906ba5ffff9ab44cee412bb397b7092a6fb..c22998b6f522465b0c03ce9c47630e7795e10e39 100644 (file)
@@ -1,6 +1,6 @@
 /*
  * This file is part of DisOrder.
- * Copyright (C) 2007-2009 Richard Kettlewell
+ * Copyright (C) 2007-2009, 2011, 2013 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
 #include <sys/mman.h>
 #include <fcntl.h>
 #include <math.h>
+#include <arpa/inet.h>
+#include <ifaddrs.h>
+#include <net/if.h>
 
 #include "log.h"
 #include "mem.h"
 #include "configuration.h"
 #include "addr.h"
 #include "syscalls.h"
+#include "printf.h"
 #include "rtp.h"
 #include "defs.h"
 #include "vector.h"
@@ -96,7 +100,7 @@ static FILE *logfp;
 /** @brief Output device */
 
 /** @brief Buffer low watermark in samples */
-unsigned minbuffer = 4 * (2 * 44100) / 10;  /* 0.4 seconds */
+unsigned minbuffer;
 
 /** @brief Maximum buffer size in samples
  *
@@ -207,6 +211,7 @@ static const struct option options[] = {
 #if HAVE_COREAUDIO_AUDIOHARDWARE_H
   { "core-audio", no_argument, 0, 'c' },
 #endif
+  { "api", required_argument, 0, 'A' },
   { "dump", required_argument, 0, 'r' },
   { "command", required_argument, 0, 'e' },
   { "pause-mode", required_argument, 0, 'P' },
@@ -239,6 +244,7 @@ static void *control_thread(void attribute((unused)) *arg) {
   char *line;
   socklen_t salen;
   FILE *fp;
+  int vl, vr;
 
   assert(control_socket);
   unlink(control_socket);
@@ -272,9 +278,21 @@ static void *control_thread(void attribute((unused)) *arg) {
       if(!strcmp(line, "stop")) {
         disorder_info("stopped via %s", control_socket);
         exit(0);                          /* terminate immediately */
-      }
-      if(!strcmp(line, "query"))
+      } else if(!strcmp(line, "query"))
         fprintf(fp, "running");
+      else if(!strcmp(line, "getvol")) {
+        if(backend->get_volume) backend->get_volume(&vl, &vr);
+        else vl = vr = 0;
+        fprintf(fp, "%d %d\n", vl, vr);
+      } else if(!strncmp(line, "setvol ", 7)) {
+        if(!backend->set_volume)
+          vl = vr = 0;
+        else if(sscanf(line + 7, "%d %d", &vl, &vr) == 2)
+          backend->set_volume(&vl, &vr);
+        else
+          backend->get_volume(&vl, &vr);
+        fprintf(fp, "%d %d\n", vl, vr);
+      }
       xfree(line);
     }
     if(fclose(fp) < 0)
@@ -483,7 +501,7 @@ struct packet *playrtp_next_packet(void) {
 }
 
 /* display usage message and terminate */
-static void help(void) {
+static void attribute((noreturn)) help(void) {
   xprintf("Usage:\n"
          "  disorder-playrtp [OPTIONS] [[ADDRESS] PORT]\n"
          "Options:\n"
@@ -492,15 +510,19 @@ static void help(void) {
           "  --max, -x FRAMES        Buffer maximum size\n"
           "  --rcvbuf, -R BYTES      Socket receive buffer size\n"
           "  --config, -C PATH       Set configuration file\n"
-#if HAVE_ALSA_ASOUNDLIB_H
-          "  --alsa, -a              Use ALSA to play audio\n"
-#endif
-#if HAVE_SYS_SOUNDCARD_H || EMPEG_HOST
-          "  --oss, -o               Use OSS to play audio\n"
-#endif
-#if HAVE_COREAUDIO_AUDIOHARDWARE_H
-          "  --core-audio, -c        Use Core Audio to play audio\n"
-#endif
+          "  --api, -A API           Select audio API.  Possibilities:\n"
+          "                            ");
+  int first = 1;
+  for(int n = 0; uaudio_apis[n]; ++n) {
+    if(uaudio_apis[n]->flags & UAUDIO_API_CLIENT) {
+      if(first)
+        first = 0;
+      else
+        xprintf(", ");
+      xprintf("%s", uaudio_apis[n]->name);
+    }
+  }
+  xprintf("\n"
           "  --command, -e COMMAND   Pipe audio to command.\n"
           "  --pause-mode, -P silence  For -e: pauses send silence (default)\n"
           "  --pause-mode, -P suspend  For -e: pauses suspend writes\n"
@@ -618,11 +640,11 @@ int main(int argc, char **argv) {
   struct addrinfo *res;
   struct stringlist sl;
   char *sockname;
-  int rcvbuf, target_rcvbuf = 0;
+  int rcvbuf, target_rcvbuf = -1;
   socklen_t len;
   struct ip_mreq mreq;
   struct ipv6_mreq mreq6;
-  disorder_client *c;
+  disorder_client *c = NULL;
   char *address, *port;
   int is_multicast;
   union any_sockaddr {
@@ -636,7 +658,7 @@ int main(int argc, char **argv) {
   int monitor = 0;
   static const int one = 1;
 
-  static const struct addrinfo prefs = {
+  struct addrinfo prefs = {
     .ai_flags = AI_PASSIVE,
     .ai_family = PF_INET,
     .ai_socktype = SOCK_DGRAM,
@@ -648,8 +670,7 @@ int main(int argc, char **argv) {
   logdate = 1;
   mem_init();
   if(!setlocale(LC_CTYPE, "")) disorder_fatal(errno, "error calling setlocale");
-  backend = uaudio_apis[0];
-  while((n = getopt_long(argc, argv, "hVdD:m:x:L:R:aocC:re:P:M", options, 0)) >= 0) {
+  while((n = getopt_long(argc, argv, "hVdD:m:x:L:R:aocC:re:P:MA:", options, 0)) >= 0) {
     switch(n) {
     case 'h': help();
     case 'V': version("disorder-playrtp");
@@ -660,14 +681,23 @@ int main(int argc, char **argv) {
     case 'L': logfp = fopen(optarg, "w"); break;
     case 'R': target_rcvbuf = atoi(optarg); break;
 #if HAVE_ALSA_ASOUNDLIB_H
-    case 'a': backend = &uaudio_alsa; break;
+    case 'a':
+      disorder_error(0, "deprecated option; use --api alsa instead");
+      backend = &uaudio_alsa; break;
 #endif
 #if HAVE_SYS_SOUNDCARD_H || EMPEG_HOST
-    case 'o': backend = &uaudio_oss; break;
+    case 'o':
+      disorder_error(0, "deprecated option; use --api oss instead");
+      backend = &uaudio_oss; 
+      break;
 #endif
 #if HAVE_COREAUDIO_AUDIOHARDWARE_H      
-    case 'c': backend = &uaudio_coreaudio; break;
+    case 'c':
+      disorder_error(0, "deprecated option; use --api coreaudio instead");
+      backend = &uaudio_coreaudio;
+      break;
 #endif
+    case 'A': backend = uaudio_find(optarg); break;
     case 'C': configfile = optarg; break;
     case 's': control_socket = optarg; break;
     case 'r': dumpfile = optarg; break;
@@ -678,23 +708,60 @@ int main(int argc, char **argv) {
     }
   }
   if(config_read(0, NULL)) disorder_fatal(0, "cannot read configuration");
-  if(!maxbuffer)
-    maxbuffer = 2 * minbuffer;
+  /* Choose a sensible default audio backend */
+  if(!backend) {
+    backend = uaudio_default(uaudio_apis, UAUDIO_API_CLIENT);
+    if(!backend)
+      disorder_fatal(0, "no default uaudio API found");
+    disorder_info("default audio API %s", backend->name);
+  }
+  if(backend == &uaudio_rtp) {
+    /* This means that you have NO local sound output.  This can happen if you
+     * use a non-Apple GCC on a Mac (because it doesn't know how to compile
+     * CoreAudio/AudioHardware.h). */
+    disorder_fatal(0, "cannot play RTP through RTP");
+  }
+  /* Set buffering parameters if not overridden */
+  if(!minbuffer) {
+    minbuffer = config->rtp_minbuffer;
+    if(!minbuffer) minbuffer = (2*44100)*4/10;
+  }
+  if(!maxbuffer) {
+    maxbuffer = config->rtp_maxbuffer;
+    if(!maxbuffer) maxbuffer = 2 * minbuffer;
+  }
+  if(target_rcvbuf < 0) target_rcvbuf = config->rtp_rcvbuf;
   argc -= optind;
   argv += optind;
   switch(argc) {
   case 0:
-    /* Get configuration from server */
-    if(!(c = disorder_new(1))) exit(EXIT_FAILURE);
-    if(disorder_connect(c)) exit(EXIT_FAILURE);
-    if(disorder_rtp_address(c, &address, &port)) exit(EXIT_FAILURE);
-    sl.n = 2;
-    sl.s = xcalloc(2, sizeof *sl.s);
-    sl.s[0] = address;
-    sl.s[1] = port;
+    sl.s = xcalloc(3, sizeof *sl.s);
+    if(config->rtp_always_request) {
+      sl.s[0] = sl.s[1] = (/*unconst*/ char *)"-";
+      sl.n = 2;
+    } else {
+      /* Get configuration from server */
+      if(!(c = disorder_new(1))) exit(EXIT_FAILURE);
+      if(disorder_connect(c)) exit(EXIT_FAILURE);
+      if(disorder_rtp_address(c, &address, &port)) exit(EXIT_FAILURE);
+      sl.s[0] = address;
+      sl.s[1] = port;
+      sl.n = 2;
+    }
+    /* If we're requesting a new stream then apply the local network address
+     * overrides.
+     */
+    if(!strcmp(sl.s[0], "-")) {
+      if(config->rtp_request_address.port)
+        byte_xasprintf(&sl.s[1], "%d", config->rtp_request_address.port);
+      if(config->rtp_request_address.address) {
+        sl.s[2] = sl.s[1];
+        sl.s[1] = config->rtp_request_address.address;
+        sl.n = 3;
+      }
+    }
     break;
-  case 1:
-  case 2:
+  case 1: case 2: case 3:
     /* Use command-line ADDRESS+PORT or just PORT */
     sl.n = argc;
     sl.s = argv;
@@ -702,83 +769,183 @@ int main(int argc, char **argv) {
   default:
     disorder_fatal(0, "usage: disorder-playrtp [OPTIONS] [[ADDRESS] PORT]");
   }
-  /* Look up address and port */
-  if(!(res = get_address(&sl, &prefs, &sockname)))
-    exit(1);
-  /* Create the socket */
-  if((rtpfd = socket(res->ai_family,
-                     res->ai_socktype,
-                     res->ai_protocol)) < 0)
-    disorder_fatal(errno, "error creating socket");
-  /* Allow multiple listeners */
-  xsetsockopt(rtpfd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof one);
-  is_multicast = multicast(res->ai_addr);
-  /* The multicast and unicast/broadcast cases are different enough that they
-   * are totally split.  Trying to find commonality between them causes more
-   * trouble that it's worth. */
-  if(is_multicast) {
-    /* Stash the multicast group address */
-    memcpy(&mgroup, res->ai_addr, res->ai_addrlen);
-    switch(res->ai_addr->sa_family) {
-    case AF_INET:
-      mgroup.in.sin_port = 0;
-      break;
-    case AF_INET6:
-      mgroup.in6.sin6_port = 0;
-      break;
-    default:
-      disorder_fatal(0, "unsupported address family %d",
-                     (int)res->ai_addr->sa_family);
+  disorder_info("version "VERSION" process ID %lu",
+                (unsigned long)getpid());
+  struct sockaddr *addr;
+  socklen_t addr_len;
+  if(!strcmp(sl.s[0], "-")) {
+    /* Syntax: - [[ADDRESS] PORT].  Here, the PORT may be `-' to get the local
+     * kernel to choose.  The ADDRESS may be omitted or `-' to pick something
+     * suitable. */
+    const char *node, *svc;
+    struct sockaddr *sa = 0;
+    switch (sl.n) {
+#define NULLDASH(s) (strcmp((s), "-") ? (s) : 0)
+      case 1: node = 0; svc = 0; break;
+      case 2: node = 0; svc = NULLDASH(sl.s[1]); break;
+      case 3: node = NULLDASH(sl.s[1]); svc = NULLDASH(sl.s[2]); break;
+      default: disorder_fatal(0, "too many listening-address compoennts");
+#undef NULLDASH
     }
-    /* Bind to to the multicast group address */
-    if(bind(rtpfd, res->ai_addr, res->ai_addrlen) < 0)
-      disorder_fatal(errno, "error binding socket to %s",
-                     format_sockaddr(res->ai_addr));
-    /* Add multicast group membership */
-    switch(mgroup.sa.sa_family) {
-    case PF_INET:
-      mreq.imr_multiaddr = mgroup.in.sin_addr;
-      mreq.imr_interface.s_addr = 0;      /* use primary interface */
-      if(setsockopt(rtpfd, IPPROTO_IP, IP_ADD_MEMBERSHIP,
-                    &mreq, sizeof mreq) < 0)
-        disorder_fatal(errno, "error calling setsockopt IP_ADD_MEMBERSHIP");
-      break;
-    case PF_INET6:
-      mreq6.ipv6mr_multiaddr = mgroup.in6.sin6_addr;
-      memset(&mreq6.ipv6mr_interface, 0, sizeof mreq6.ipv6mr_interface);
-      if(setsockopt(rtpfd, IPPROTO_IPV6, IPV6_JOIN_GROUP,
-                    &mreq6, sizeof mreq6) < 0)
-        disorder_fatal(errno, "error calling setsockopt IPV6_JOIN_GROUP");
-      break;
-    default:
-      disorder_fatal(0, "unsupported address family %d", res->ai_family);
+    /* We'll need a connection to request the incoming stream, so open one if
+     * we don't have one already */
+    if(!c) {
+      if(!(c = disorder_new(1))) exit(EXIT_FAILURE);
+      if(disorder_connect(c)) exit(EXIT_FAILURE);
     }
+    /* If no address was given, we need to pick one.  But we already have a
+     * connection to the server, so we can probably use the address from that.
+     */
+    struct sockaddr_storage ss;
+    if(!node) {
+      addr_len = sizeof ss;
+      if(disorder_client_sockname(c, (struct sockaddr *)&ss, &addr_len))
+        exit(EXIT_FAILURE);
+      if(ss.ss_family != AF_INET && ss.ss_family != AF_INET6) {
+        /* We're using a Unix-domain socket, so use a loopback address.  I'm
+         * cowardly using IPv4 here. */
+        struct sockaddr_in *sin = (struct sockaddr_in *)&ss;
+        sin->sin_family = AF_INET;
+        sin->sin_addr.s_addr = htonl(INADDR_LOOPBACK);
+      }
+      sa = (struct sockaddr *)&ss;
+      prefs.ai_family = sa->sa_family;
+    }
+    /* If we have an address or port to resolve then do that now */
+    if (node || svc) {
+      struct addrinfo *ai;
+      char errbuf[1024];
+      int rc;
+      if((rc = getaddrinfo(node, svc, &prefs, &ai)))
+        disorder_fatal(0, "failed to resolve address `%s' and service `%s': %s",
+                       node ? node : "-", svc ? svc : "-",
+                       format_error(ec_getaddrinfo, rc,
+                                    errbuf, sizeof(errbuf)));
+      if(!sa)
+        sa = ai->ai_addr;
+      else {
+        assert(sa->sa_family == ai->ai_addr->sa_family);
+        switch(sa->sa_family) {
+          case AF_INET:
+            ((struct sockaddr_in *)sa)->sin_port =
+              ((struct sockaddr_in *)ai->ai_addr)->sin_port;
+            break;
+          case AF_INET6:
+            ((struct sockaddr_in6 *)sa)->sin6_port =
+              ((struct sockaddr_in6 *)ai->ai_addr)->sin6_port;
+            break;
+          default:
+            assert(!"unexpected address family");
+        }
+      }
+    }
+    if((rtpfd = socket(sa->sa_family, SOCK_DGRAM, IPPROTO_UDP)) < 0)
+      disorder_fatal(errno, "error creating socket (family %d)",
+                     sa->sa_family);
+    /* Bind the address */
+    if(bind(rtpfd, sa,
+            sa->sa_family == AF_INET
+            ? sizeof (struct sockaddr_in) : sizeof (struct sockaddr_in6)) < 0)
+      disorder_fatal(errno, "error binding socket");
+    static struct sockaddr_storage bound_address;
+    addr = (struct sockaddr *)&bound_address;
+    addr_len = sizeof bound_address;
+    if(getsockname(rtpfd, addr, &addr_len) < 0)
+      disorder_fatal(errno, "error getting socket address");
+    /* Convert to string */
+    char addrname[128], portname[32];
+    if(getnameinfo(addr, addr_len,
+                   addrname, sizeof addrname,
+                   portname, sizeof portname,
+                   NI_NUMERICHOST|NI_NUMERICSERV) < 0)
+      disorder_fatal(errno, "getnameinfo");
+    /* Ask for audio data */
+    if(disorder_rtp_request(c, addrname, portname)) exit(EXIT_FAILURE);
     /* Report what we did */
-    disorder_info("listening on %s multicast group %s",
-                  format_sockaddr(res->ai_addr), format_sockaddr(&mgroup.sa));
+    disorder_info("listening on %s (stream requested)",
+                  format_sockaddr(addr));
   } else {
-    /* Bind to 0/port */
-    switch(res->ai_addr->sa_family) {
-    case AF_INET: {
-      struct sockaddr_in *in = (struct sockaddr_in *)res->ai_addr;
+    if(sl.n > 2) disorder_fatal(0, "too many address components");
+    /* Look up address and port */
+    if(!(res = get_address(&sl, &prefs, &sockname)))
+      exit(1);
+    addr = res->ai_addr;
+    addr_len = res->ai_addrlen;
+    /* Create the socket */
+    if((rtpfd = socket(res->ai_family,
+                       res->ai_socktype,
+                       res->ai_protocol)) < 0)
+      disorder_fatal(errno, "error creating socket");
+    /* Allow multiple listeners */
+    xsetsockopt(rtpfd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof one);
+    is_multicast = multicast(addr);
+    /* The multicast and unicast/broadcast cases are different enough that they
+     * are totally split.  Trying to find commonality between them causes more
+     * trouble that it's worth. */
+    if(is_multicast) {
+      /* Stash the multicast group address */
+      memcpy(&mgroup, addr, addr_len);
+      switch(res->ai_addr->sa_family) {
+      case AF_INET:
+        mgroup.in.sin_port = 0;
+        break;
+      case AF_INET6:
+        mgroup.in6.sin6_port = 0;
+        break;
+      default:
+        disorder_fatal(0, "unsupported address family %d",
+                       (int)addr->sa_family);
+      }
+      /* Bind to to the multicast group address */
+      if(bind(rtpfd, addr, addr_len) < 0)
+        disorder_fatal(errno, "error binding socket to %s",
+                       format_sockaddr(addr));
+      /* Add multicast group membership */
+      switch(mgroup.sa.sa_family) {
+      case PF_INET:
+        mreq.imr_multiaddr = mgroup.in.sin_addr;
+        mreq.imr_interface.s_addr = 0;      /* use primary interface */
+        if(setsockopt(rtpfd, IPPROTO_IP, IP_ADD_MEMBERSHIP,
+                      &mreq, sizeof mreq) < 0)
+          disorder_fatal(errno, "error calling setsockopt IP_ADD_MEMBERSHIP");
+        break;
+      case PF_INET6:
+        mreq6.ipv6mr_multiaddr = mgroup.in6.sin6_addr;
+        memset(&mreq6.ipv6mr_interface, 0, sizeof mreq6.ipv6mr_interface);
+        if(setsockopt(rtpfd, IPPROTO_IPV6, IPV6_JOIN_GROUP,
+                      &mreq6, sizeof mreq6) < 0)
+          disorder_fatal(errno, "error calling setsockopt IPV6_JOIN_GROUP");
+        break;
+      default:
+        disorder_fatal(0, "unsupported address family %d", res->ai_family);
+      }
+      /* Report what we did */
+      disorder_info("listening on %s multicast group %s",
+                    format_sockaddr(addr), format_sockaddr(&mgroup.sa));
+    } else {
+      /* Bind to 0/port */
+      switch(addr->sa_family) {
+      case AF_INET: {
+        struct sockaddr_in *in = (struct sockaddr_in *)addr;
       
-      memset(&in->sin_addr, 0, sizeof (struct in_addr));
-      break;
-    }
-    case AF_INET6: {
-      struct sockaddr_in6 *in6 = (struct sockaddr_in6 *)res->ai_addr;
+        memset(&in->sin_addr, 0, sizeof (struct in_addr));
+        break;
+      }
+      case AF_INET6: {
+        struct sockaddr_in6 *in6 = (struct sockaddr_in6 *)addr;
       
-      memset(&in6->sin6_addr, 0, sizeof (struct in6_addr));
-      break;
-    }
-    default:
-      disorder_fatal(0, "unsupported family %d", (int)res->ai_addr->sa_family);
+        memset(&in6->sin6_addr, 0, sizeof (struct in6_addr));
+        break;
+      }
+      default:
+        disorder_fatal(0, "unsupported family %d", (int)addr->sa_family);
+      }
+      if(bind(rtpfd, addr, addr_len) < 0)
+        disorder_fatal(errno, "error binding socket to %s",
+                       format_sockaddr(addr));
+      /* Report what we did */
+      disorder_info("listening on %s", format_sockaddr(addr));
     }
-    if(bind(rtpfd, res->ai_addr, res->ai_addrlen) < 0)
-      disorder_fatal(errno, "error binding socket to %s",
-                     format_sockaddr(res->ai_addr));
-    /* Report what we did */
-    disorder_info("listening on %s", format_sockaddr(res->ai_addr));
   }
   len = sizeof rcvbuf;
   if(getsockopt(rtpfd, SOL_SOCKET, SO_RCVBUF, &rcvbuf, &len) < 0)
@@ -828,7 +995,10 @@ int main(int argc, char **argv) {
    * the format before we know what it is! */
   uaudio_set_format(44100/*Hz*/, 2/*channels*/,
                     16/*bits/channel*/, 1/*signed*/);
+  uaudio_set("application", "disorder-playrtp");
+  backend->configure();
   backend->start(playrtp_callback, NULL);
+  if(backend->open_mixer) backend->open_mixer();
   /* We receive and convert audio data in a background thread */
   if((err = pthread_create(&ltid, 0, listen_thread, 0)))
     disorder_fatal(err, "pthread_create listen_thread");