/* playlist-streamer, (c) 2000-2007 Jamie Zawinski * * Usage: playlist-streamer bits-per-second metadata-interval filenames... * * Copy the given files to stdout, one after another, and throttle * the rate of output to be approximately (but not less than) the * given bit rate. If the metadata-interval is non-0, insert the * file name as Shoutcast-style metadata every N bytes. * * Permission to use, copy, modify, distribute, and sell this software and its * documentation for any purpose is hereby granted without fee, provided that * the above copyright notice appear in all copies and that both that * copyright notice and this permission notice appear in supporting * documentation. No representations are made about the suitability of this * software for any purpose. It is provided "as is" without express or * implied warranty. * * Created: 10-Jun-2000 * Modified: 20-Oct-2007 */ #include #include #include #include #include #include #include #include #include #include char *progname; int verbose_p = 0; int debug_p = 0; static void my_usleep (unsigned long usecs) { struct timeval tv; tv.tv_sec = usecs / 1000000L; tv.tv_usec = usecs % 1000000L; (void) select (0, 0, 0, 0, &tv); } static void write_all (int fd, const char *buf, size_t count) { while (count > 0) { int n = write (fd, buf, count); if (n < 0) { char buf2[1024]; if (errno == EINTR || errno == EAGAIN) continue; sprintf (buf2, "%.255s: write:", progname); perror (buf2); exit (1); } count -= n; buf += n; } } typedef struct { char *metadata, *prev_metadata; int out_fd; int bps; char *data; int size; int fp; int batch_written; /* for bps delay computation */ int total_written; /* for debug statistics */ int content_length; /* actual number of bytes sent to the stream */ time_t start, batch_start, last_stats; } slowcat_data; static void print_stats (slowcat_data *scd) { int secs, rate; double off; time_t now = time ((time_t *) 0); secs = now - scd->start; if (secs == 0) { fprintf (stderr, "%s: wrote %d bytes in one chunk.\n", progname, scd->total_written); } else { rate = scd->total_written * 8 / secs; off = (double) rate / (double) (scd->bps * 8); fprintf (stderr, "%s: actual bits per second: %d (%d bytes in %ds)\n", progname, rate, scd->total_written, secs); if (off < 1.0) fprintf (stderr, "%s: undershot by %.1f%%\n", progname, (1.0 - off) * 100); else if (off > 1.0) fprintf (stderr, "%s: overshot by %.1f%%\n", progname, -(1.0 - off) * 100); } } static void slowcat_buffer (slowcat_data *scd, char *in_buf, int in_bufsiz) { while (in_bufsiz > 0) { int n = in_bufsiz; int left = scd->size - scd->fp; if (n > left) n = left; memcpy (scd->data + scd->fp, in_buf, n); scd->fp += n; in_buf += n; in_bufsiz -= n; if (scd->fp > scd->size) abort(); if (scd->fp == scd->size) { /* Buffer is full: time to write it and the current metadata. */ int wrote = scd->fp; write_all (scd->out_fd, scd->data, scd->fp); scd->batch_written += scd->fp; scd->total_written += scd->fp; scd->content_length += scd->fp; scd->fp = 0; if (scd->metadata) { int L; char *md = (char *) calloc (1, strlen(scd->metadata) + 200); if (*scd->metadata) { sprintf (md, "\001StreamTitle='%s';", scd->metadata); L = strlen(md+1) + 2; L = 16 * ((L + 16) / 16); /* round to 16 */ md[0] = (char) (L / 16); /* mod16 in byte 0 */ if (verbose_p) fprintf (stderr, "%s: metadata [%d] %s\n", progname, L, md+1); } else { L = 0; /* no-op metadata */ *md = 0; } write_all (scd->out_fd, md, L+1); /* Don't count these when computing bitrate: scd->batch_written += L+1; scd->total_written += L+1; */ scd->content_length += L+1; wrote += L+1; free (md); /* Nuke this metadata string so that it is only written once. */ *scd->metadata = 0; } if (verbose_p) fprintf (stderr, "%s: wrote %d bytes\n", progname, wrote); } /* If we've written a bunch of bytes, maybe it's time to sleep. */ if (!debug_p && scd->batch_written >= scd->bps) { /* how many seconds the batch we just wrote should have taken. */ int secs = scd->batch_written / scd->bps; time_t now = time ((time_t *) 0); /* Wait for the second to tick. */ while (now < scd->batch_start + secs) { if (now < scd->batch_start - 1) sleep (now - scd->batch_start - 1); /* N-1 seconds */ else my_usleep (20000L); /* 1/50th second */ now = time ((time_t *) 0); } /* Second has ticked, restart counter and start writing again. */ scd->batch_start = now; scd->batch_written = 0; if (verbose_p) { if (scd->last_stats + 10 <= now) { print_stats (scd); scd->last_stats = now; } } } } } void slowcat_one (slowcat_data *scd, int in_fd) { static char buf[100 * 1024]; int bufsiz = sizeof(buf)-1; int n; while ((n = read (in_fd, buf, bufsiz)) != 0) { if (n < 0) { if (errno == EINTR || errno == EAGAIN) continue; sprintf (buf, "%.255s: read", progname); perror (buf); exit (1); } slowcat_buffer (scd, buf, n); } } char * make_metadata (const char *filename) { char *metadata = malloc (strlen(filename) * 2 + 1); char *s, *out = metadata; s = strrchr (filename, '/'); /* lose directory */ if (s) filename = s+1; while (*filename >= '0' && *filename <= '9') /* skip leading numbers */ filename++; while (*filename == ' ') /* skip leading spaces */ filename++; while (*filename) { char c = *filename++; /* if (c == '\'' || c == '\\') *out++ = '\\'; */ /* if (c == '\'') *out++ = '\''; */ *out++ = c; } s = strrchr (metadata, '.'); if (s) *s = 0; /* truncate before ".mp3" */ return metadata; } int main (int argc, char **argv) { char *s, c; int metaint, bps, i; slowcat_data SCD; slowcat_data *scd = &SCD; progname = argv[0]; s = strrchr (progname, '/'); if (s) progname = s+1; while (argc > 1 && argv[1][0] == '-') { if (!strcmp(argv[1], "-v")) verbose_p = 1; else if (!strcmp(argv[1], "-d")) debug_p = 1; else goto USAGE; argc--; argv++; } for (i = 1; i < argc; i++) if (argv[i][0] == '-') goto USAGE; if (argc < 4) { USAGE: fprintf (stderr, "usage: %s [-v] [-d] bits-per-second meta-int filenames...\n", progname); exit (1); } if (1 != sscanf (argv[1], "%d%c", &bps, &c)) { if (1 != sscanf (argv[1], "%dk%c", &bps, &c) && 1 != sscanf (argv[1], "%dK%c", &bps, &c)) goto USAGE; else bps *= 1024; } if (bps < 8 || bps > (1024 * 1024 * 1024)) { fprintf (stderr, "%s: how about a sane bitrate?\n", progname); goto USAGE; } if (1 != sscanf (argv[2], "%d%c", &metaint, &c)) goto USAGE; memset (scd, 0, sizeof(*scd)); scd->out_fd = fileno(stdout); scd->bps = bps / 8; /* bytes per second, not bits per second. */ scd->size = (metaint ? metaint : 100 * 1024); scd->data = (char *) calloc (1, scd->size); scd->start = time ((time_t *) 0); scd->last_stats = scd->start; for (i = 3; i < argc; i++) { const char *file = argv[i]; int fd = open (file, O_RDONLY); if (scd->metadata) free (scd->metadata); scd->metadata = (metaint ? make_metadata (file) : 0); if (fd < 0) { char buf[1024]; sprintf (buf, "%.255s: %.255s", progname, file); perror (buf); exit (1); } if (verbose_p) fprintf (stderr, "%s: streaming file %d/%d: %s\n", progname, i-2, argc-3, scd->metadata); slowcat_one (scd, fd); close (fd); } if (scd->fp > 0) /* Flush the last buffer */ { write_all (scd->out_fd, scd->data, scd->fp); if (verbose_p) fprintf (stderr, "%s: wrote %d bytes [EOF]\n", progname, scd->fp); scd->content_length += scd->fp; } if (verbose_p) fprintf (stderr, "%s: actual content length: %d\n", progname, scd->content_length); return 0; }