2017-01-14 17:35:19 -05:00
|
|
|
/*
|
|
|
|
* Copyright (C) 2017 Espen Jurgensen
|
|
|
|
*
|
|
|
|
* 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
|
2017-01-18 17:42:52 -05:00
|
|
|
*
|
|
|
|
*
|
|
|
|
* About pipe.c
|
|
|
|
* --------------
|
|
|
|
* This module will read a PCM16 stream from a named pipe and write it to the
|
|
|
|
* input buffer. The user may start/stop playback from a pipe by selecting it
|
|
|
|
* through a client. If the user has configured pipe_autostart, then pipes in
|
|
|
|
* the library will also be watched for data, and playback will start/stop
|
|
|
|
* automatically.
|
|
|
|
*
|
|
|
|
* The module will also look for pipes with a .metadata suffix, and if found,
|
|
|
|
* the metadata will be parsed and fed to the player. The metadata must be in
|
|
|
|
* the format Shairport uses for this purpose.
|
|
|
|
*
|
2017-01-14 17:35:19 -05:00
|
|
|
*/
|
|
|
|
|
|
|
|
#include <stdio.h>
|
|
|
|
#include <stdlib.h>
|
2017-01-18 17:42:52 -05:00
|
|
|
#include <stdbool.h>
|
2017-01-14 17:35:19 -05:00
|
|
|
#include <unistd.h>
|
|
|
|
#include <stdint.h>
|
|
|
|
#include <fcntl.h>
|
|
|
|
#include <string.h>
|
|
|
|
#include <errno.h>
|
2017-01-15 17:26:11 -05:00
|
|
|
#include <limits.h>
|
2017-01-14 17:35:19 -05:00
|
|
|
#include <sys/types.h>
|
|
|
|
#include <sys/stat.h>
|
|
|
|
#include <sys/ioctl.h>
|
|
|
|
|
|
|
|
#include <event2/event.h>
|
|
|
|
#include <event2/buffer.h>
|
2017-01-27 05:05:24 -05:00
|
|
|
#include <mxml.h>
|
2017-01-14 17:35:19 -05:00
|
|
|
|
|
|
|
#include "misc.h"
|
|
|
|
#include "logger.h"
|
|
|
|
#include "db.h"
|
|
|
|
#include "conffile.h"
|
|
|
|
#include "listener.h"
|
|
|
|
#include "player.h"
|
2017-01-16 16:08:57 -05:00
|
|
|
#include "worker.h"
|
2017-01-14 17:35:19 -05:00
|
|
|
#include "input.h"
|
|
|
|
|
|
|
|
// Maximum number of pipes to watch for data
|
2017-01-15 17:26:11 -05:00
|
|
|
#define PIPE_MAX_WATCH 4
|
2017-01-22 17:24:47 -05:00
|
|
|
// Max number of bytes to read from a pipe at a time
|
2017-01-14 17:35:19 -05:00
|
|
|
#define PIPE_READ_MAX 65536
|
2017-01-22 17:24:47 -05:00
|
|
|
// Max number of bytes to buffer from metadata pipes
|
|
|
|
#define PIPE_METADATA_BUFLEN_MAX 262144
|
2017-01-14 17:35:19 -05:00
|
|
|
|
2017-01-16 16:08:57 -05:00
|
|
|
extern struct event_base *evbase_worker;
|
2017-01-14 17:35:19 -05:00
|
|
|
|
|
|
|
enum pipestate
|
|
|
|
{
|
2017-01-18 17:42:52 -05:00
|
|
|
PIPE_NEW,
|
|
|
|
PIPE_DEL,
|
|
|
|
PIPE_OPEN,
|
|
|
|
};
|
|
|
|
|
|
|
|
enum pipetype
|
|
|
|
{
|
|
|
|
PIPE_PCM,
|
|
|
|
PIPE_METADATA,
|
2017-01-14 17:35:19 -05:00
|
|
|
};
|
|
|
|
|
|
|
|
struct pipe
|
|
|
|
{
|
2017-01-18 17:42:52 -05:00
|
|
|
int id; // The mfi id of the pipe
|
|
|
|
int fd; // File descriptor
|
|
|
|
bool is_autostarted; // We autostarted the pipe (and we will autostop)
|
|
|
|
char *path; // Path
|
|
|
|
enum pipestate state; // Newly appeared, marked for deletion, open/ready
|
|
|
|
enum pipetype type; // PCM (audio) or metadata
|
|
|
|
event_callback_fn cb; // Callback when there is data to read
|
|
|
|
struct event *ev; // Event for the callback
|
2017-01-14 17:35:19 -05:00
|
|
|
// TODO mutex
|
|
|
|
|
|
|
|
struct pipe *next;
|
|
|
|
};
|
|
|
|
|
|
|
|
// From config - should we watch library pipes for data or only start on request
|
|
|
|
static int pipe_autostart;
|
|
|
|
|
|
|
|
// Global list of pipes we are watching. If watching/autostart is disabled this
|
|
|
|
// will just point to the currently playing pipe (if any).
|
|
|
|
static struct pipe *pipelist;
|
|
|
|
|
2017-01-18 17:42:52 -05:00
|
|
|
// Single pipe that we start watching for metadata after playback starts
|
|
|
|
static struct pipe *pipe_metadata;
|
2017-01-22 17:24:47 -05:00
|
|
|
// We read metadata into this evbuffer
|
|
|
|
static struct evbuffer *pipe_metadata_buf;
|
2017-01-27 05:05:24 -05:00
|
|
|
// Parsed metadata goes here
|
|
|
|
static struct input_metadata pipe_metadata_parsed;
|
2017-01-22 17:24:47 -05:00
|
|
|
// True if there is new metadata to push to the player
|
|
|
|
static bool pipe_metadata_is_new;
|
2017-01-18 17:42:52 -05:00
|
|
|
|
|
|
|
/* ------------------------------- FORWARDS ------------------------------- */
|
|
|
|
|
|
|
|
static void
|
|
|
|
pipe_watch_reset(struct pipe *pipe);
|
|
|
|
|
2017-01-22 17:24:47 -05:00
|
|
|
static void
|
|
|
|
pipe_metadata_watch_del(void *arg);
|
2017-01-14 17:35:19 -05:00
|
|
|
|
|
|
|
/* -------------------------------- HELPERS ------------------------------- */
|
|
|
|
|
|
|
|
static struct pipe *
|
2017-01-18 17:42:52 -05:00
|
|
|
pipe_new(const char *path, int id, enum pipetype type, event_callback_fn cb)
|
2017-01-14 17:35:19 -05:00
|
|
|
{
|
|
|
|
struct pipe *pipe;
|
|
|
|
|
|
|
|
pipe = calloc(1, sizeof(struct pipe));
|
|
|
|
pipe->path = strdup(path);
|
|
|
|
pipe->id = id;
|
|
|
|
pipe->fd = -1;
|
|
|
|
pipe->state = PIPE_NEW;
|
2017-01-18 17:42:52 -05:00
|
|
|
pipe->type = type;
|
|
|
|
pipe->cb = cb;
|
|
|
|
|
|
|
|
if (type == PIPE_PCM)
|
|
|
|
{
|
|
|
|
pipe->next = pipelist;
|
|
|
|
pipelist = pipe;
|
|
|
|
}
|
2017-01-14 17:35:19 -05:00
|
|
|
|
|
|
|
return pipe;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
pipe_free(struct pipe *pipe)
|
|
|
|
{
|
|
|
|
free(pipe->path);
|
|
|
|
free(pipe);
|
|
|
|
}
|
|
|
|
|
2017-01-15 17:26:11 -05:00
|
|
|
static void
|
|
|
|
pipelist_prune(void)
|
|
|
|
{
|
|
|
|
struct pipe *pipe;
|
|
|
|
struct pipe *next;
|
|
|
|
|
|
|
|
for (pipe = pipelist; pipe; pipe = next)
|
|
|
|
{
|
|
|
|
next = pipe->next;
|
|
|
|
|
|
|
|
if (pipelist->state == PIPE_DEL)
|
|
|
|
{
|
|
|
|
pipe_free(pipelist);
|
|
|
|
pipelist = next;
|
|
|
|
}
|
|
|
|
else if (next && (next->state == PIPE_DEL))
|
|
|
|
{
|
|
|
|
pipe->next = next->next;
|
|
|
|
pipe_free(next);
|
|
|
|
next = pipe->next;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-01-14 17:35:19 -05:00
|
|
|
static struct pipe *
|
|
|
|
pipe_find(int id)
|
|
|
|
{
|
|
|
|
struct pipe *pipe;
|
|
|
|
|
|
|
|
for (pipe = pipelist; pipe; pipe = pipe->next)
|
|
|
|
{
|
|
|
|
if (id == pipe->id)
|
|
|
|
return pipe;
|
|
|
|
}
|
|
|
|
|
|
|
|
return NULL;
|
|
|
|
}
|
|
|
|
|
2017-01-27 05:05:24 -05:00
|
|
|
// Convert to macro?
|
|
|
|
static inline uint32_t
|
|
|
|
dmapval(const char s[4])
|
|
|
|
{
|
|
|
|
return ((s[0] << 24) | (s[1] << 16) | (s[2] << 8) | (s[3] << 0));
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
parse_progress(struct input_metadata *m, char *progress)
|
|
|
|
{
|
|
|
|
char *s;
|
|
|
|
char *ptr;
|
|
|
|
uint64_t start;
|
|
|
|
uint64_t pos;
|
|
|
|
uint64_t end;
|
|
|
|
|
|
|
|
if (!(s = strtok_r(progress, "/", &ptr)))
|
|
|
|
return;
|
|
|
|
safe_atou64(s, &start);
|
|
|
|
|
|
|
|
if (!(s = strtok_r(NULL, "/", &ptr)))
|
|
|
|
return;
|
|
|
|
safe_atou64(s, &pos);
|
|
|
|
|
|
|
|
if (!(s = strtok_r(NULL, "/", &ptr)))
|
|
|
|
return;
|
|
|
|
safe_atou64(s, &end);
|
|
|
|
|
|
|
|
if (!start || !pos || !end)
|
|
|
|
return;
|
|
|
|
|
|
|
|
m->rtptime = start; // Not actually used - we have our own rtptime
|
|
|
|
m->offset = (pos > start) ? (pos - start) : 0;
|
|
|
|
m->song_length = (end - start) * 10 / 441; // Convert to ms based on 44100
|
|
|
|
}
|
|
|
|
|
|
|
|
// returns 0 on metadata found, otherwise -1
|
2017-01-22 17:24:47 -05:00
|
|
|
static int
|
2017-01-27 05:05:24 -05:00
|
|
|
parse_item(struct input_metadata *m, mxml_node_t *item)
|
2017-01-22 17:24:47 -05:00
|
|
|
{
|
2017-01-27 05:05:24 -05:00
|
|
|
mxml_node_t *needle;
|
|
|
|
const char *s;
|
|
|
|
uint32_t type;
|
|
|
|
uint32_t code;
|
|
|
|
char *progress;
|
|
|
|
char **data;
|
|
|
|
|
|
|
|
type = 0;
|
|
|
|
if ( (needle = mxmlFindElement(item, item, "type", NULL, NULL, MXML_DESCEND)) &&
|
|
|
|
(s = mxmlGetText(needle, NULL)) )
|
|
|
|
sscanf(s, "%8x", &type);
|
|
|
|
|
|
|
|
code = 0;
|
|
|
|
if ( (needle = mxmlFindElement(item, item, "code", NULL, NULL, MXML_DESCEND)) &&
|
|
|
|
(s = mxmlGetText(needle, NULL)) )
|
|
|
|
sscanf(s, "%8x", &code);
|
|
|
|
|
|
|
|
if (!type || !code)
|
|
|
|
{
|
|
|
|
DPRINTF(E_WARN, L_PLAYER, "No type (%d) or code (%d) in pipe metadata\n", type, code);
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (code == dmapval("asal"))
|
|
|
|
data = &m->album;
|
|
|
|
else if (code == dmapval("asar"))
|
|
|
|
data = &m->artist;
|
|
|
|
else if (code == dmapval("minm"))
|
|
|
|
data = &m->title;
|
|
|
|
else if (code == dmapval("asgn"))
|
|
|
|
data = &m->genre;
|
|
|
|
else if (code == dmapval("prgr"))
|
|
|
|
data = &progress;
|
|
|
|
else
|
|
|
|
return -1;
|
2017-01-22 17:24:47 -05:00
|
|
|
|
2017-01-27 05:05:24 -05:00
|
|
|
if ( (needle = mxmlFindElement(item, item, "data", NULL, NULL, MXML_DESCEND)) &&
|
|
|
|
(s = mxmlGetText(needle, NULL)) )
|
2017-01-22 17:24:47 -05:00
|
|
|
{
|
2017-01-27 05:05:24 -05:00
|
|
|
if (data != &progress)
|
|
|
|
free(*data);
|
|
|
|
|
|
|
|
*data = b64_decode(s);
|
|
|
|
|
|
|
|
DPRINTF(E_DBG, L_PLAYER, "Read Shairport metadata (type=%8x, code=%8x): '%s'\n", type, code, *data);
|
|
|
|
|
|
|
|
if (data == &progress)
|
|
|
|
{
|
|
|
|
parse_progress(m, progress);
|
|
|
|
free(*data);
|
|
|
|
}
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
static int
|
|
|
|
pipe_metadata_parse(struct input_metadata *m, struct evbuffer *evbuf)
|
|
|
|
{
|
|
|
|
mxml_node_t *xml;
|
|
|
|
mxml_node_t *haystack;
|
|
|
|
mxml_node_t *item;
|
|
|
|
const char *s;
|
|
|
|
int found;
|
|
|
|
|
|
|
|
xml = mxmlNewXML("1.0");
|
|
|
|
if (!xml)
|
|
|
|
return -1;
|
2017-01-22 17:24:47 -05:00
|
|
|
|
2017-01-27 05:05:24 -05:00
|
|
|
s = (char *)evbuffer_pullup(evbuf, -1);
|
|
|
|
haystack = mxmlLoadString(xml, s, MXML_NO_CALLBACK);
|
|
|
|
if (!haystack)
|
|
|
|
{
|
|
|
|
DPRINTF(E_LOG, L_PLAYER, "Could not parse pipe metadata\n");
|
|
|
|
mxmlDelete(xml);
|
|
|
|
evbuffer_drain(evbuf, evbuffer_get_length(evbuf));
|
|
|
|
return -1;
|
2017-01-22 17:24:47 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
evbuffer_drain(evbuf, evbuffer_get_length(evbuf));
|
|
|
|
|
2017-01-27 05:05:24 -05:00
|
|
|
// DPRINTF(E_DBG, L_PLAYER, "Parsing %s\n", s);
|
|
|
|
|
|
|
|
found = 0;
|
|
|
|
for (item = mxmlGetFirstChild(haystack); item; item = mxmlWalkNext(item, haystack, MXML_NO_DESCEND))
|
|
|
|
{
|
|
|
|
if (mxmlGetType(item) != 0)
|
|
|
|
continue;
|
|
|
|
|
|
|
|
if (parse_item(m, item) == 0)
|
|
|
|
found = 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
mxmlDelete(xml);
|
|
|
|
return found;
|
2017-01-22 17:24:47 -05:00
|
|
|
}
|
2017-01-14 17:35:19 -05:00
|
|
|
|
2017-01-18 17:42:52 -05:00
|
|
|
/* ---------------------------- GENERAL PIPE I/O -------------------------- */
|
2017-01-16 16:08:57 -05:00
|
|
|
/* Thread: worker */
|
2017-01-14 17:35:19 -05:00
|
|
|
|
2017-01-18 17:42:52 -05:00
|
|
|
// Some data arrived on a pipe we watch - let's autostart playback
|
|
|
|
static void
|
|
|
|
pipe_read_cb(evutil_socket_t fd, short event, void *arg)
|
|
|
|
{
|
|
|
|
struct pipe *pipe = arg;
|
|
|
|
struct player_status status;
|
|
|
|
struct db_queue_item *queue_item;
|
|
|
|
int ret;
|
|
|
|
|
|
|
|
ret = player_get_status(&status);
|
|
|
|
if ((ret < 0) || (status.status == PLAY_PLAYING))
|
|
|
|
{
|
|
|
|
DPRINTF(E_LOG, L_PLAYER, "Data arrived on pipe '%s', but player is busy\n", pipe->path);
|
|
|
|
pipe_watch_reset(pipe);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
DPRINTF(E_INFO, L_PLAYER, "Autostarting pipe '%s' (fd %d)\n", pipe->path, fd);
|
|
|
|
|
|
|
|
db_queue_clear();
|
|
|
|
|
|
|
|
ret = db_queue_add_by_fileid(pipe->id, 0, 0);
|
|
|
|
if (ret < 0)
|
|
|
|
return;
|
|
|
|
|
|
|
|
queue_item = db_queue_fetch_byfileid(pipe->id);
|
|
|
|
if (!queue_item)
|
|
|
|
return;
|
|
|
|
|
|
|
|
player_playback_start_byitem(queue_item);
|
|
|
|
|
|
|
|
pipe->is_autostarted = 1;
|
|
|
|
|
|
|
|
free_queue_item(queue_item, 0);
|
|
|
|
}
|
|
|
|
|
2017-01-14 17:35:19 -05:00
|
|
|
// Updates pipelist with pipe items from the db. Pipes that are no longer in
|
|
|
|
// the db get marked with PIPE_DEL. Returns count of pipes that should be
|
|
|
|
// watched (may or may not equal length of pipelist)
|
|
|
|
static int
|
|
|
|
pipe_enum(void)
|
|
|
|
{
|
|
|
|
struct query_params qp;
|
|
|
|
struct db_media_file_info dbmfi;
|
|
|
|
struct pipe *pipe;
|
|
|
|
char filter[32];
|
|
|
|
int count;
|
|
|
|
int id;
|
|
|
|
int ret;
|
|
|
|
|
|
|
|
memset(&qp, 0, sizeof(struct query_params));
|
|
|
|
qp.type = Q_ITEMS;
|
|
|
|
qp.filter = filter;
|
|
|
|
|
|
|
|
snprintf(filter, sizeof(filter), "f.data_kind = %d", DATA_KIND_PIPE);
|
|
|
|
|
|
|
|
ret = db_query_start(&qp);
|
|
|
|
if (ret < 0)
|
|
|
|
return -1;
|
|
|
|
|
|
|
|
for (pipe = pipelist; pipe; pipe = pipe->next)
|
|
|
|
pipe->state = PIPE_DEL;
|
|
|
|
|
|
|
|
count = 0;
|
|
|
|
while (((ret = db_query_fetch_file(&qp, &dbmfi)) == 0) && (dbmfi.id))
|
|
|
|
{
|
|
|
|
ret = safe_atoi32(dbmfi.id, &id);
|
|
|
|
if (ret < 0)
|
|
|
|
continue;
|
|
|
|
|
|
|
|
count++;
|
|
|
|
|
|
|
|
if ((pipe = pipe_find(id)))
|
|
|
|
{
|
|
|
|
pipe->state = PIPE_OPEN;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2017-01-18 17:42:52 -05:00
|
|
|
pipe = pipe_new(dbmfi.path, id, PIPE_PCM, pipe_read_cb);
|
2017-01-14 17:35:19 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
db_query_end(&qp);
|
|
|
|
|
|
|
|
return count;
|
|
|
|
}
|
|
|
|
|
2017-01-15 17:26:11 -05:00
|
|
|
// Opens a pipe and starts watching it for data if autostart is configured
|
2017-01-14 17:35:19 -05:00
|
|
|
static int
|
2017-01-18 17:42:52 -05:00
|
|
|
pipe_open(const char *path, bool silent)
|
2017-01-14 17:35:19 -05:00
|
|
|
{
|
|
|
|
struct stat sb;
|
2017-01-15 17:26:11 -05:00
|
|
|
int fd;
|
2017-01-14 17:35:19 -05:00
|
|
|
|
2017-01-15 17:26:11 -05:00
|
|
|
DPRINTF(E_DBG, L_PLAYER, "(Re)opening pipe: '%s'\n", path);
|
2017-01-14 17:35:19 -05:00
|
|
|
|
2017-01-15 17:26:11 -05:00
|
|
|
if (lstat(path, &sb) < 0)
|
2017-01-14 17:35:19 -05:00
|
|
|
{
|
2017-01-18 17:42:52 -05:00
|
|
|
if (!silent)
|
|
|
|
DPRINTF(E_LOG, L_PLAYER, "Could not lstat() '%s': %s\n", path, strerror(errno));
|
2017-01-14 17:35:19 -05:00
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!S_ISFIFO(sb.st_mode))
|
|
|
|
{
|
2017-01-15 17:26:11 -05:00
|
|
|
DPRINTF(E_LOG, L_PLAYER, "Source type is pipe, but path is not a fifo: %s\n", path);
|
2017-01-14 17:35:19 -05:00
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
2017-01-15 17:26:11 -05:00
|
|
|
fd = open(path, O_RDONLY | O_NONBLOCK);
|
|
|
|
if (fd < 0)
|
2017-01-14 17:35:19 -05:00
|
|
|
{
|
2017-01-15 17:26:11 -05:00
|
|
|
DPRINTF(E_LOG, L_PLAYER, "Could not open pipe for reading '%s': %s\n", path, strerror(errno));
|
2017-01-14 17:35:19 -05:00
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
2017-01-15 17:26:11 -05:00
|
|
|
return fd;
|
|
|
|
}
|
2017-01-14 17:35:19 -05:00
|
|
|
|
2017-01-15 17:26:11 -05:00
|
|
|
static void
|
|
|
|
pipe_close(int fd)
|
|
|
|
{
|
2017-01-18 17:42:52 -05:00
|
|
|
if (fd >= 0)
|
|
|
|
close(fd);
|
2017-01-16 16:08:57 -05:00
|
|
|
}
|
|
|
|
|
2017-01-15 17:26:11 -05:00
|
|
|
static int
|
|
|
|
pipe_watch_add(struct pipe *pipe)
|
|
|
|
{
|
2017-01-18 17:42:52 -05:00
|
|
|
bool silent;
|
|
|
|
|
|
|
|
silent = (pipe->type == PIPE_METADATA);
|
|
|
|
pipe->fd = pipe_open(pipe->path, silent);
|
2017-01-15 17:26:11 -05:00
|
|
|
if (pipe->fd < 0)
|
|
|
|
return -1;
|
|
|
|
|
2017-01-18 17:42:52 -05:00
|
|
|
pipe->ev = event_new(evbase_worker, pipe->fd, EV_READ, pipe->cb, pipe);
|
2017-01-14 17:35:19 -05:00
|
|
|
if (!pipe->ev)
|
|
|
|
{
|
|
|
|
DPRINTF(E_LOG, L_PLAYER, "Could not watch pipe for new data '%s'\n", pipe->path);
|
2017-01-18 17:42:52 -05:00
|
|
|
pipe_close(pipe->fd);
|
2017-01-14 17:35:19 -05:00
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
event_add(pipe->ev, NULL);
|
|
|
|
|
2017-01-18 17:42:52 -05:00
|
|
|
pipe->state = PIPE_OPEN;
|
|
|
|
|
2017-01-14 17:35:19 -05:00
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
2017-01-15 17:26:11 -05:00
|
|
|
pipe_watch_del(struct pipe *pipe)
|
2017-01-14 17:35:19 -05:00
|
|
|
{
|
|
|
|
if (pipe->ev)
|
|
|
|
event_free(pipe->ev);
|
|
|
|
|
2017-01-15 17:26:11 -05:00
|
|
|
pipe_close(pipe->fd);
|
2017-01-14 17:35:19 -05:00
|
|
|
|
|
|
|
pipe->fd = -1;
|
|
|
|
}
|
|
|
|
|
2017-01-18 17:42:52 -05:00
|
|
|
// If a read on pipe returns 0 it is an EOF, and we must close it and reopen it
|
|
|
|
// for renewed watching. The event will be freed and reallocated by this.
|
2017-01-16 16:08:57 -05:00
|
|
|
static void
|
2017-01-18 17:42:52 -05:00
|
|
|
pipe_watch_reset(struct pipe *pipe)
|
2017-01-14 17:35:19 -05:00
|
|
|
{
|
2017-01-18 17:42:52 -05:00
|
|
|
pipe_watch_del(pipe);
|
|
|
|
pipe_watch_add(pipe);
|
2017-01-14 17:35:19 -05:00
|
|
|
}
|
|
|
|
|
2017-01-15 17:26:11 -05:00
|
|
|
static void
|
2017-01-18 17:42:52 -05:00
|
|
|
pipe_watch_update(void *arg)
|
2017-01-14 17:35:19 -05:00
|
|
|
{
|
|
|
|
struct pipe *pipe;
|
|
|
|
int count;
|
|
|
|
|
|
|
|
count = pipe_enum(); // Count does not include pipes with state PIPE_DEL
|
|
|
|
if (count < 0)
|
|
|
|
return;
|
|
|
|
|
2017-01-15 17:26:11 -05:00
|
|
|
for (pipe = pipelist; pipe; pipe = pipe->next)
|
2017-01-14 17:35:19 -05:00
|
|
|
{
|
|
|
|
DPRINTF(E_DBG, L_PLAYER, "Processing pipe '%s', state is %d\n", pipe->path, pipe->state);
|
|
|
|
|
2017-01-15 17:26:11 -05:00
|
|
|
if ((pipe->state == PIPE_NEW) && (count > PIPE_MAX_WATCH))
|
2017-01-14 17:35:19 -05:00
|
|
|
DPRINTF(E_LOG, L_PLAYER, "Max open pipes reached, will not watch %s\n", pipe->path);
|
|
|
|
else if (pipe->state == PIPE_NEW)
|
2017-01-15 17:26:11 -05:00
|
|
|
pipe_watch_add(pipe);
|
2017-01-14 17:35:19 -05:00
|
|
|
else if (pipe->state == PIPE_DEL)
|
2017-01-15 17:26:11 -05:00
|
|
|
pipe_watch_del(pipe);
|
2017-01-14 17:35:19 -05:00
|
|
|
}
|
2017-01-15 17:26:11 -05:00
|
|
|
|
|
|
|
pipelist_prune();
|
2017-01-14 17:35:19 -05:00
|
|
|
}
|
|
|
|
|
2017-01-16 16:08:57 -05:00
|
|
|
// Thread: filescanner
|
|
|
|
static void
|
|
|
|
pipe_listener_cb(enum listener_event_type type)
|
|
|
|
{
|
2017-01-18 17:42:52 -05:00
|
|
|
worker_execute(pipe_watch_update, NULL, 0, 0);
|
|
|
|
}
|
|
|
|
|
|
|
|
/* -------------------------- METADATA PIPE HANDLING ---------------------- */
|
|
|
|
/* Thread: worker */
|
|
|
|
|
|
|
|
// Some metadata arrived on a pipe we watch
|
|
|
|
static void
|
|
|
|
pipe_metadata_read_cb(evutil_socket_t fd, short event, void *arg)
|
|
|
|
{
|
2017-01-22 17:24:47 -05:00
|
|
|
struct evbuffer_ptr evptr;
|
2017-01-18 17:42:52 -05:00
|
|
|
int ret;
|
|
|
|
|
2017-01-22 17:24:47 -05:00
|
|
|
ret = evbuffer_read(pipe_metadata_buf, pipe_metadata->fd, PIPE_READ_MAX);
|
|
|
|
if (ret < 0)
|
|
|
|
{
|
|
|
|
if (errno != EAGAIN)
|
|
|
|
pipe_metadata_watch_del(NULL);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
else if (ret == 0)
|
|
|
|
{
|
|
|
|
pipe_watch_reset(pipe_metadata);
|
|
|
|
goto readd;
|
|
|
|
}
|
2017-01-18 17:42:52 -05:00
|
|
|
|
2017-01-22 17:24:47 -05:00
|
|
|
if (evbuffer_get_length(pipe_metadata_buf) > PIPE_METADATA_BUFLEN_MAX)
|
|
|
|
{
|
|
|
|
DPRINTF(E_LOG, L_PLAYER, "Can't process data from metadata pipe, reading will stop\n");
|
|
|
|
pipe_metadata_watch_del(NULL);
|
|
|
|
return;
|
|
|
|
}
|
2017-01-18 17:42:52 -05:00
|
|
|
|
2017-01-22 17:24:47 -05:00
|
|
|
// Did we get the end tag? If not return to wait for more data
|
|
|
|
evptr = evbuffer_search(pipe_metadata_buf, "</item>", strlen("</item>"), NULL);
|
|
|
|
if (evptr.pos < 0)
|
|
|
|
{
|
|
|
|
goto readd;
|
|
|
|
}
|
2017-01-18 17:42:52 -05:00
|
|
|
|
2017-01-22 17:24:47 -05:00
|
|
|
// NULL-terminate the buffer
|
|
|
|
evbuffer_add(pipe_metadata_buf, "", 1);
|
|
|
|
|
2017-01-27 05:05:24 -05:00
|
|
|
ret = pipe_metadata_parse(&pipe_metadata_parsed, pipe_metadata_buf);
|
2017-01-22 17:24:47 -05:00
|
|
|
if (ret < 0)
|
|
|
|
{
|
|
|
|
pipe_metadata_watch_del(NULL);
|
|
|
|
return;
|
|
|
|
}
|
2017-01-27 05:05:24 -05:00
|
|
|
else if (ret > 0)
|
|
|
|
{
|
|
|
|
// Trigger notification in playback loop
|
|
|
|
pipe_metadata_is_new = 1;
|
|
|
|
}
|
2017-01-22 17:24:47 -05:00
|
|
|
|
|
|
|
readd:
|
|
|
|
if (pipe_metadata && pipe_metadata->ev)
|
|
|
|
event_add(pipe_metadata->ev, NULL);
|
2017-01-18 17:42:52 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
pipe_metadata_watch_add(void *arg)
|
|
|
|
{
|
|
|
|
char *base_path = arg;
|
|
|
|
char path[PATH_MAX];
|
|
|
|
int ret;
|
|
|
|
|
|
|
|
ret = snprintf(path, sizeof(path), "%s.metadata", base_path);
|
|
|
|
if ((ret < 0) || (ret > sizeof(path)))
|
|
|
|
return;
|
|
|
|
|
|
|
|
pipe_metadata = pipe_new(path, 0, PIPE_METADATA, pipe_metadata_read_cb);
|
|
|
|
if (!pipe_metadata)
|
|
|
|
return;
|
|
|
|
|
2017-01-22 17:24:47 -05:00
|
|
|
pipe_metadata_buf = evbuffer_new();
|
|
|
|
|
2017-01-18 17:42:52 -05:00
|
|
|
ret = pipe_watch_add(pipe_metadata);
|
|
|
|
if (ret < 0)
|
|
|
|
{
|
2017-01-22 17:24:47 -05:00
|
|
|
evbuffer_free(pipe_metadata_buf);
|
2017-01-18 17:42:52 -05:00
|
|
|
pipe_free(pipe_metadata);
|
|
|
|
pipe_metadata = NULL;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
pipe_metadata_watch_del(void *arg)
|
|
|
|
{
|
|
|
|
if (!pipe_metadata)
|
|
|
|
return;
|
|
|
|
|
2017-01-22 17:24:47 -05:00
|
|
|
evbuffer_free(pipe_metadata_buf);
|
2017-01-18 17:42:52 -05:00
|
|
|
pipe_watch_del(pipe_metadata);
|
|
|
|
pipe_free(pipe_metadata);
|
|
|
|
pipe_metadata = NULL;
|
2017-01-16 16:08:57 -05:00
|
|
|
}
|
|
|
|
|
2017-01-14 17:35:19 -05:00
|
|
|
|
|
|
|
/* -------------------------- PIPE INPUT INTERFACE ------------------------ */
|
|
|
|
/* Thread: player/input */
|
|
|
|
|
|
|
|
static int
|
|
|
|
setup(struct player_source *ps)
|
|
|
|
{
|
|
|
|
struct pipe *pipe;
|
|
|
|
|
2017-01-18 17:42:52 -05:00
|
|
|
// If autostart is disabled then this is the first time we encounter the pipe
|
2017-01-15 17:26:11 -05:00
|
|
|
if (!pipe_autostart)
|
2017-01-18 17:42:52 -05:00
|
|
|
pipe_new(ps->path, ps->id, PIPE_PCM, NULL);
|
2017-01-15 17:26:11 -05:00
|
|
|
|
|
|
|
pipe = pipe_find(ps->id);
|
|
|
|
if (!pipe)
|
2017-01-14 17:35:19 -05:00
|
|
|
{
|
2017-01-15 17:26:11 -05:00
|
|
|
DPRINTF(E_LOG, L_PLAYER, "Unknown pipe '%s'\n", ps->path);
|
|
|
|
return -1;
|
2017-01-14 17:35:19 -05:00
|
|
|
}
|
2017-01-18 17:42:52 -05:00
|
|
|
// TODO pipe mutex here
|
2017-01-14 17:35:19 -05:00
|
|
|
if (pipe->state != PIPE_OPEN)
|
|
|
|
{
|
2017-01-18 17:42:52 -05:00
|
|
|
pipe->fd = pipe_open(pipe->path, 0);
|
2017-01-15 17:26:11 -05:00
|
|
|
if (pipe->fd < 0)
|
2017-01-14 17:35:19 -05:00
|
|
|
return -1;
|
2017-01-15 17:26:11 -05:00
|
|
|
pipe->state = PIPE_OPEN;
|
2017-01-14 17:35:19 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
if (pipe->ev)
|
|
|
|
event_del(pipe->ev); // Avoids autostarting pipe if manually started by user
|
|
|
|
|
2017-01-18 17:42:52 -05:00
|
|
|
worker_execute(pipe_metadata_watch_add, pipe->path, strlen(pipe->path) + 1, 0);
|
|
|
|
|
2017-01-14 17:35:19 -05:00
|
|
|
ps->setup_done = 1;
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
static int
|
|
|
|
start(struct player_source *ps)
|
|
|
|
{
|
|
|
|
struct pipe *pipe;
|
|
|
|
struct evbuffer *evbuf;
|
2017-01-22 17:24:47 -05:00
|
|
|
short flags;
|
2017-01-14 17:35:19 -05:00
|
|
|
int ret;
|
|
|
|
|
|
|
|
pipe = pipe_find(ps->id);
|
|
|
|
if (!pipe)
|
|
|
|
return -1;
|
|
|
|
|
|
|
|
evbuf = evbuffer_new();
|
|
|
|
if (!evbuf)
|
|
|
|
{
|
|
|
|
DPRINTF(E_LOG, L_PLAYER, "Out of memory for pipe evbuf\n");
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
ret = -1;
|
|
|
|
while (!input_loop_break)
|
|
|
|
{
|
|
|
|
ret = evbuffer_read(evbuf, pipe->fd, PIPE_READ_MAX);
|
2017-01-18 17:42:52 -05:00
|
|
|
if ((ret == 0) && (pipe->is_autostarted))
|
|
|
|
{
|
|
|
|
input_write(evbuf, INPUT_FLAG_EOF); // Autostop
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
else if ((ret == 0) || ((ret < 0) && (errno == EAGAIN)))
|
2017-01-14 17:35:19 -05:00
|
|
|
{
|
|
|
|
input_wait();
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
else if (ret < 0)
|
|
|
|
{
|
|
|
|
DPRINTF(E_LOG, L_PLAYER, "Could not read from pipe: %s\n", strerror(errno));
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
2017-01-22 17:24:47 -05:00
|
|
|
flags = (pipe_metadata_is_new ? INPUT_FLAG_METADATA : 0);
|
|
|
|
pipe_metadata_is_new = 0;
|
|
|
|
|
|
|
|
ret = input_write(evbuf, flags);
|
2017-01-14 17:35:19 -05:00
|
|
|
if (ret < 0)
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
evbuffer_free(evbuf);
|
|
|
|
|
|
|
|
return ret;
|
|
|
|
}
|
|
|
|
|
|
|
|
static int
|
|
|
|
stop(struct player_source *ps)
|
|
|
|
{
|
|
|
|
struct pipe *pipe;
|
|
|
|
|
|
|
|
DPRINTF(E_DBG, L_PLAYER, "Stopping pipe\n");
|
|
|
|
|
|
|
|
pipe = pipe_find(ps->id);
|
2017-01-15 17:26:11 -05:00
|
|
|
if (!pipe)
|
|
|
|
{
|
|
|
|
DPRINTF(E_LOG, L_PLAYER, "Unknown pipe '%s'\n", ps->path);
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
2017-01-18 17:42:52 -05:00
|
|
|
if (!pipe_autostart)
|
2017-01-14 17:35:19 -05:00
|
|
|
{
|
2017-01-18 17:42:52 -05:00
|
|
|
// Since autostart is disabled we are now done with the pipe
|
2017-01-15 17:26:11 -05:00
|
|
|
pipe_close(pipe->fd);
|
|
|
|
pipe->state = PIPE_DEL;
|
|
|
|
pipelist_prune();
|
2017-01-14 17:35:19 -05:00
|
|
|
}
|
2017-01-18 17:42:52 -05:00
|
|
|
else
|
|
|
|
{
|
|
|
|
// Reset the pipe and start watching it again for new data
|
|
|
|
pipe->is_autostarted = 0;
|
|
|
|
pipe_watch_reset(pipe);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (pipe_metadata)
|
|
|
|
worker_execute(pipe_metadata_watch_del, NULL, 0, 0);
|
2017-01-14 17:35:19 -05:00
|
|
|
|
|
|
|
ps->setup_done = 0;
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2017-01-27 05:05:24 -05:00
|
|
|
// FIXME Thread safety of pipe_metadata_parsed
|
|
|
|
static int
|
|
|
|
metadata_get(struct input_metadata *metadata, struct player_source *ps, uint64_t rtptime)
|
|
|
|
{
|
|
|
|
if (pipe_metadata_parsed.artist)
|
|
|
|
swap_pointers(&metadata->artist, &pipe_metadata_parsed.artist);
|
|
|
|
if (pipe_metadata_parsed.title)
|
|
|
|
swap_pointers(&metadata->title, &pipe_metadata_parsed.title);
|
|
|
|
if (pipe_metadata_parsed.album)
|
|
|
|
swap_pointers(&metadata->album, &pipe_metadata_parsed.album);
|
|
|
|
if (pipe_metadata_parsed.genre)
|
|
|
|
swap_pointers(&metadata->genre, &pipe_metadata_parsed.genre);
|
|
|
|
if (pipe_metadata_parsed.artwork_url)
|
|
|
|
swap_pointers(&metadata->artwork_url, &pipe_metadata_parsed.artwork_url);
|
|
|
|
|
|
|
|
if (pipe_metadata_parsed.song_length)
|
|
|
|
{
|
|
|
|
if (rtptime > ps->stream_start)
|
|
|
|
metadata->rtptime = rtptime - pipe_metadata_parsed.offset;
|
|
|
|
metadata->offset = pipe_metadata_parsed.offset;
|
|
|
|
metadata->song_length = pipe_metadata_parsed.song_length;
|
|
|
|
}
|
|
|
|
|
|
|
|
input_metadata_free(&pipe_metadata_parsed, 1);
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2017-01-14 17:35:19 -05:00
|
|
|
// Thread: main
|
|
|
|
static int
|
|
|
|
init(void)
|
|
|
|
{
|
|
|
|
pipe_autostart = cfg_getbool(cfg_getsec(cfg, "library"), "pipe_autostart");
|
|
|
|
|
|
|
|
if (pipe_autostart)
|
|
|
|
return listener_add(pipe_listener_cb, LISTENER_DATABASE);
|
|
|
|
else
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
deinit(void)
|
|
|
|
{
|
|
|
|
struct pipe *pipe;
|
|
|
|
|
|
|
|
for (pipe = pipelist; pipelist; pipe = pipelist)
|
|
|
|
{
|
|
|
|
pipelist = pipe->next;
|
2017-01-15 17:26:11 -05:00
|
|
|
pipe_watch_del(pipe);
|
|
|
|
pipe_free(pipe);
|
2017-01-14 17:35:19 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
if (pipe_autostart)
|
|
|
|
listener_remove(pipe_listener_cb);
|
|
|
|
}
|
|
|
|
|
|
|
|
struct input_definition input_pipe =
|
|
|
|
{
|
|
|
|
.name = "pipe",
|
|
|
|
.type = INPUT_TYPE_PIPE,
|
|
|
|
.disabled = 0,
|
|
|
|
.setup = setup,
|
|
|
|
.start = start,
|
|
|
|
.stop = stop,
|
2017-01-27 05:05:24 -05:00
|
|
|
.metadata_get = metadata_get,
|
2017-01-14 17:35:19 -05:00
|
|
|
.init = init,
|
|
|
|
.deinit = deinit,
|
|
|
|
};
|
|
|
|
|