From 171a75375be9b27e6a38361ef3235cb6333e8a16 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Mon, 26 Dec 2016 19:27:37 +0100 Subject: [PATCH 01/37] [outputs] Fix for (unlikely) situation where all outputs have no init() --- src/outputs.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/outputs.c b/src/outputs.c index 42db1b5a..871798fd 100644 --- a/src/outputs.c +++ b/src/outputs.c @@ -342,7 +342,10 @@ outputs_init(void) } if (!outputs[i]->init) - continue; + { + no_output = 0; + continue; + } ret = outputs[i]->init(); if (ret < 0) From c50b038397ae76e00735cde1d1f8c39af09be340 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Mon, 26 Dec 2016 19:29:47 +0100 Subject: [PATCH 02/37] [misc] Move STOB and BTOS macros to misc.h --- src/misc.h | 5 +++++ src/outputs/alsa.c | 1 + src/outputs/fifo.c | 1 + 3 files changed, 7 insertions(+) diff --git a/src/misc.h b/src/misc.h index 2a3172b5..c358dde9 100644 --- a/src/misc.h +++ b/src/misc.h @@ -10,6 +10,11 @@ #include #include +/* Samples to bytes, bytes to samples */ +#define STOB(s) ((s) * 4) +#define BTOS(b) ((b) / 4) + + struct onekeyval { char *name; char *value; diff --git a/src/outputs/alsa.c b/src/outputs/alsa.c index 9e54c158..20a902c4 100644 --- a/src/outputs/alsa.c +++ b/src/outputs/alsa.c @@ -32,6 +32,7 @@ #include #include +#include "misc.h" #include "conffile.h" #include "logger.h" #include "player.h" diff --git a/src/outputs/fifo.c b/src/outputs/fifo.c index 9199d276..dd44ceb8 100644 --- a/src/outputs/fifo.c +++ b/src/outputs/fifo.c @@ -33,6 +33,7 @@ #include +#include "misc.h" #include "conffile.h" #include "logger.h" #include "player.h" From 3e24f857fad41787a58bc564fb060b308f1255fe Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Mon, 26 Dec 2016 19:30:29 +0100 Subject: [PATCH 03/37] [input] Add input interface to player - WIP --- src/Makefile.am | 2 + src/input.c | 470 +++++++++++++++++++++++++++++++++++++++++ src/input.h | 176 +++++++++++++++ src/inputs/file_http.c | 125 +++++++++++ src/player.c | 375 +++++--------------------------- src/player.h | 6 - 6 files changed, 824 insertions(+), 330 deletions(-) create mode 100644 src/input.c create mode 100644 src/input.h create mode 100644 src/inputs/file_http.c diff --git a/src/Makefile.am b/src/Makefile.am index dd7f40fe..faa62afc 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -108,6 +108,8 @@ forked_daapd_SOURCES = main.c \ daap_query.c daap_query.h \ player.c player.h \ worker.c worker.h \ + input.h input.c \ + inputs/file_http.c \ outputs.h outputs.c \ outputs/raop.c outputs/streaming.c outputs/dummy.c outputs/fifo.c \ $(ALSA_SRC) $(PULSEAUDIO_SRC) $(CHROMECAST_SRC) \ diff --git a/src/input.c b/src/input.c new file mode 100644 index 00000000..828bdc92 --- /dev/null +++ b/src/input.c @@ -0,0 +1,470 @@ +/* + * Copyright (C) 2017 Espen Jürgensen + * + * 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 + */ + +#ifdef HAVE_CONFIG_H +# include +#endif + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#ifdef HAVE_PTHREAD_NP_H +# include +#endif + +#include "misc.h" +#include "logger.h" +#include "input.h" + +// Disallow further writes to the buffer when its size is larger than this threshold +#define INPUT_BUFFER_THRESHOLD STOB(44100) + +#define DEBUG 1 //TODO disable + +extern struct input_definition input_file; +extern struct input_definition input_http; + +// Must be in sync with enum input_types +static struct input_definition *inputs[] = { + &input_file, + &input_http, + NULL +}; + +struct input_buffer +{ + // Raw pcm stream data + struct evbuffer *evbuf; + + // If non-zero, remaining length of buffer until EOF + size_t eof; + // If non-zero, remaining length of buffer until (possible) new metadata + size_t metadata; + + // Locks for sharing the buffer between input and player thread + pthread_mutex_t mutex; + pthread_cond_t cond; +}; + +/* --- Globals --- */ +// Input thread +static pthread_t tid_input; + +// Input buffer +static struct input_buffer input_buffer; + +#ifdef DEBUG +static size_t debug_elapsed; +#endif + + +/* ------------------------------ MISC HELPERS ---------------------------- */ + +static short +flags_set(size_t len) +{ + short flags = 0; + + if (input_buffer.eof) + { + if (len >= input_buffer.eof) + { + flags |= INPUT_FLAG_EOF; + input_buffer.eof = 0; + } + else + input_buffer.eof -= len; + } + + if (input_buffer.metadata) + { + if (len >= input_buffer.metadata) + { + flags |= INPUT_FLAG_METADATA; + input_buffer.metadata = 0; + } + else + input_buffer.metadata -= len; + } + + return flags; +} + +static int +map_data_kind(int data_kind) +{ + switch (data_kind) + { + case DATA_KIND_FILE: + return INPUT_TYPE_FILE; + + case DATA_KIND_HTTP: + return INPUT_TYPE_HTTP; + + default: + return -1; + } +} + +static int +source_check_and_map(struct player_source *ps, const char *action, char check_setup) +{ + int type; + +#ifdef DEBUG + DPRINTF(E_DBG, L_PLAYER, "Action is %s\n", action); +#endif + + if (!ps) + { + DPRINTF(E_LOG, L_PLAYER, "Stream %s called with invalid player source\n", action); + return -1; + } + + if (check_setup && !ps->setup_done) + { + DPRINTF(E_LOG, L_PLAYER, "Given player source not setup, %s not possible\n", action); + return -1; + } + + type = map_data_kind(ps->data_kind); + if (type < 0) + { + DPRINTF(E_LOG, L_PLAYER, "Unsupported input type, %s not possible\n", action); + return -1; + } + + return type; +} + +/* ----------------------------- PLAYBACK LOOP ---------------------------- */ +/* Thread: input */ + +// TODO Thread safety of ps? Do we need the return of the loop? +static void * +playback(void *arg) +{ + struct player_source *ps = arg; + int type; + + type = source_check_and_map(ps, "start", 1); + if ((type < 0) || (inputs[type]->disabled)) + goto thread_exit; + + // Loops until input_loop_break is set or no more input, e.g. EOF + inputs[type]->start(ps); + +#ifdef DEBUG + DPRINTF(E_DBG, L_PLAYER, "Playback loop stopped (break is %d)\n", input_loop_break); +#endif + + thread_exit: + pthread_exit(NULL); +} + +// Called by input modules from within the playback loop +int +input_write(struct evbuffer *evbuf, short flags) +{ + int ret; + + pthread_mutex_lock(&input_buffer.mutex); + + while ( (!input_loop_break) && (evbuffer_get_length(input_buffer.evbuf) > INPUT_BUFFER_THRESHOLD) ) + { + if (flags & INPUT_FLAG_NONBLOCK) + { + pthread_mutex_unlock(&input_buffer.mutex); + return EAGAIN; + } + + pthread_cond_wait(&input_buffer.cond, &input_buffer.mutex); + + // TODO protect against infinite looping and waiting? + } + + if (!input_loop_break) + { + ret = evbuffer_add_buffer(input_buffer.evbuf, evbuf); + if (ret < 0) + DPRINTF(E_LOG, L_PLAYER, "Error adding stream data to input buffer\n"); + + if (!input_buffer.eof && (flags & INPUT_FLAG_EOF)) + input_buffer.eof = evbuffer_get_length(input_buffer.evbuf); + if (!input_buffer.metadata && (flags & INPUT_FLAG_METADATA)) + input_buffer.metadata = evbuffer_get_length(input_buffer.evbuf); + } + else + ret = 0; + + pthread_mutex_unlock(&input_buffer.mutex); + + return ret; +} + + +/* -------------------- Interface towards player thread ------------------- */ +/* Thread: player */ + +int +input_read(struct evbuffer *evbuf, size_t want, short *flags) +{ + int len; + + *flags = 0; + + pthread_mutex_lock(&input_buffer.mutex); + +#ifdef DEBUG + debug_elapsed += want; + if (debug_elapsed > STOB(441000)) // 10 sec + { + DPRINTF(E_DBG, L_PLAYER, "Input buffer has %zu bytes\n", evbuffer_get_length(input_buffer.evbuf)); + debug_elapsed = 0; + } +#endif + + len = evbuffer_remove_buffer(input_buffer.evbuf, evbuf, want); + if (len < 0) + { + DPRINTF(E_LOG, L_PLAYER, "Error reading stream data from input buffer\n"); + goto out_unlock; + } + + *flags = flags_set(len); + + out_unlock: + pthread_cond_signal(&input_buffer.cond); + pthread_mutex_unlock(&input_buffer.mutex); + + return len; +} + +int +input_setup(struct player_source *ps) +{ + int type; + + type = source_check_and_map(ps, "setup", 0); + if ((type < 0) || (inputs[type]->disabled)) + return -1; + + if (!inputs[type]->setup) + return 0; + + return inputs[type]->setup(ps); +} + +int +input_start(struct player_source *ps) +{ + int ret; + + if (tid_input) + { + DPRINTF(E_LOG, L_PLAYER, "Bug! Input start called, but playback already running\n"); + input_pause(ps); + } + + input_loop_break = 0; + + ret = pthread_create(&tid_input, NULL, playback, ps); + if (ret < 0) + { + DPRINTF(E_LOG, L_PLAYER, "Could not spawn input thread: %s\n", strerror(errno)); + return -1; + } + +#if defined(HAVE_PTHREAD_SETNAME_NP) + pthread_setname_np(tid_input, "input"); +#elif defined(HAVE_PTHREAD_SET_NAME_NP) + pthread_set_name_np(tid_input, "input"); +#endif + + return 0; +} + +int +input_pause(struct player_source *ps) +{ + short flags; + int ret; + +#ifdef DEBUG + DPRINTF(E_DBG, L_PLAYER, "Pause called, stopping playback loop\n"); +#endif + + if (!tid_input) + return -1; + + pthread_mutex_lock(&input_buffer.mutex); + + input_loop_break = 1; + + pthread_cond_signal(&input_buffer.cond); + pthread_mutex_unlock(&input_buffer.mutex); + + ret = pthread_join(tid_input, NULL); + if (ret != 0) + { + DPRINTF(E_LOG, L_PLAYER, "Could not join input thread: %s\n", strerror(errno)); + return -1; + } + + tid_input = 0; + + input_flush(&flags); + + return 0; +} + +int +input_stop(struct player_source *ps) +{ + int type; + + if (tid_input) + input_pause(ps); + + if (!ps) + return 0; + + type = source_check_and_map(ps, "stop", 1); + if ((type < 0) || (inputs[type]->disabled)) + return -1; + + if (!inputs[type]->stop) + return 0; + + return inputs[type]->stop(ps); +} + +int +input_seek(struct player_source *ps, int seek_ms) +{ + int type; + + type = source_check_and_map(ps, "seek", 1); + if ((type < 0) || (inputs[type]->disabled)) + return -1; + + if (!inputs[type]->seek) + return 0; + + if (tid_input) + input_pause(ps); + + return inputs[type]->seek(ps, seek_ms); +} + +void +input_flush(short *flags) +{ + size_t len; + + pthread_mutex_lock(&input_buffer.mutex); + + len = evbuffer_get_length(input_buffer.evbuf); + + evbuffer_drain(input_buffer.evbuf, len); + + *flags = flags_set(len); + + input_buffer.eof = 0; + input_buffer.metadata = 0; + + pthread_mutex_unlock(&input_buffer.mutex); + +#ifdef DEBUG + DPRINTF(E_DBG, L_PLAYER, "Flush with flags %d\n", *flags); +#endif +} + +int +input_init(void) +{ + int no_input; + int ret; + int i; + + // Prepare input buffer + pthread_mutex_init(&input_buffer.mutex, NULL); + pthread_cond_init(&input_buffer.cond, NULL); + + input_buffer.evbuf = evbuffer_new(); + if (!input_buffer.evbuf) + { + DPRINTF(E_LOG, L_PLAYER, "Out of memory for input buffer\n"); + return -1; + } + + no_input = 1; + for (i = 0; inputs[i]; i++) + { + if (inputs[i]->type != i) + { + DPRINTF(E_FATAL, L_PLAYER, "BUG! Input definitions are misaligned with input enum\n"); + return -1; + } + + if (!inputs[i]->init) + { + no_input = 0; + continue; + } + + ret = inputs[i]->init(); + if (ret < 0) + inputs[i]->disabled = 1; + else + no_input = 0; + } + + if (no_input) + return -1; + + return 0; +} + +void +input_deinit(void) +{ + int i; + + input_stop(NULL); + + for (i = 0; inputs[i]; i++) + { + if (inputs[i]->disabled) + continue; + + if (inputs[i]->deinit) + inputs[i]->deinit(); + } + + evbuffer_free(input_buffer.evbuf); +} + diff --git a/src/input.h b/src/input.h new file mode 100644 index 00000000..4b6f3a57 --- /dev/null +++ b/src/input.h @@ -0,0 +1,176 @@ + +#ifndef __INPUT_H__ +#define __INPUT_H__ + +#include +#include "transcode.h" + +// Must be in sync with inputs[] in input.c +enum input_types +{ + INPUT_TYPE_FILE, + INPUT_TYPE_HTTP, +}; + +enum input_flags +{ + // Write to input buffer must not block + INPUT_FLAG_NONBLOCK = (1 << 0), + // Flags end of file + INPUT_FLAG_EOF = (1 << 1), + // Flags possible new stream metadata + INPUT_FLAG_METADATA = (1 << 2), +}; + +struct player_source +{ + /* Id of the file/item in the files database */ + uint32_t id; + + /* Item-Id of the file/item in the queue */ + uint32_t item_id; + + /* Length of the file/item in milliseconds */ + uint32_t len_ms; + + enum data_kind data_kind; + enum media_kind media_kind; + char *path; + + /* Start time of the media item as rtp-time + The stream-start is the rtp-time the media item did or would have + started playing (after seek or pause), therefor the elapsed time of the + media item is always: + elapsed time = current rtptime - stream-start */ + uint64_t stream_start; + + /* Output start time of the media item as rtp-time + The output start time is the rtp-time of the first audio packet send + to the audio outputs. + It differs from stream-start especially after a seek, where the first audio + packet has the next rtp-time as output start and stream start becomes the + rtp-time the media item would have been started playing if the seek did + not happen. */ + uint64_t output_start; + + /* End time of media item as rtp-time + The end time is set if the reading (source_read) of the media item reached + end of file, until then it is 0. */ + uint64_t end; + + struct transcode_ctx *xcode; + int setup_done; + + struct player_source *play_next; +}; + +struct input_definition +{ + // Name of the input + const char *name; + + // Type of input + enum input_types type; + + // Set to 1 if the input initialization failed + char disabled; + + // Prepare a playback session + int (*setup)(struct player_source *ps); + + // Starts playback loop (must be defined) + int (*start)(struct player_source *ps); + + // Cleans up when playback loop has ended + int (*stop)(struct player_source *ps); + + // Changes the playback position + int (*seek)(struct player_source *ps, int seek_ms); + + // Initialization function called during startup + int (*init)(void); + + // Deinitialization function called at shutdown + void (*deinit)(void); + +}; + +/* + * Input modules use this to test if playback should be stopped or seeked + */ +int input_loop_break; + +/* + * Transfer stream data to the player's input buffer. The input evbuf will be + * drained on succesful write. This is to avoid copying memory. If the player's + * input buffer is full the function will block until the write can be made + * (unless INPUT_FILE_NONBLOCK is set). + * + * @in evbuf Raw audio data to write + * @in flags One or more INPUT_FLAG_* + * @return 0 on success, EAGAIN if buffer was full (and _NONBLOCK is set), + * -1 on error + */ +int +input_write(struct evbuffer *evbuf, short flags); + +/* + * Move a chunk of stream data from the player's input buffer to an output + * buffer. Should only be called by the player thread. Will not block. + * + * @in evbuf Output buffer + * @in want How much data to move to the output buffer + * @out flags Flags INPUT_FLAG_EOF or INPUT_FLAG_METADATA + * @return Number of bytes moved + */ +int +input_read(struct evbuffer *evbuf, size_t want, short *flags); + +/* + * Initializes the given player source for playback + */ +int +input_setup(struct player_source *ps); + +/* + * Tells the input to start or resume playback, i.e. after calling this function + * the input buffer will begin to fill up, and should be read periodically with + * input_read(). Before calling this input_setup() must have been called. + */ +int +input_start(struct player_source *ps); + +/* + * Pauses playback of the given player source (stops playback loop) and flushes + * the input buffer + */ +int +input_pause(struct player_source *ps); + +/* + * Stops playback loop (if running), flushes input buffer and cleans up the + * player source + */ +int +input_stop(struct player_source *ps); + +/* + * Seeks playback position to seek_ms. Returns actual seek position, 0 on + * unseekable, -1 on error. May block. + */ +int +input_seek(struct player_source *ps, int seek_ms); + +/* + * Flush input buffer. Output flags will be the same as input_read(). + */ +void +input_flush(short *flags); + +int +input_init(void); + +void +input_deinit(void); + +#endif /* !__INPUT_H__ */ diff --git a/src/inputs/file_http.c b/src/inputs/file_http.c new file mode 100644 index 00000000..956982b5 --- /dev/null +++ b/src/inputs/file_http.c @@ -0,0 +1,125 @@ +/* + * 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 + */ + +#include +#include +#include +#include + +#include + +#include "transcode.h" +#include "http.h" +#include "input.h" + +static int +setup(struct player_source *ps) +{ + ps->xcode = transcode_setup(ps->data_kind, ps->path, ps->len_ms, XCODE_PCM16_NOHEADER, NULL); + if (!ps->xcode) + return -1; + + ps->setup_done = 1; + + return 0; +} + +static int +http_setup(struct player_source *ps) +{ + char *url; + + if (http_stream_setup(&url, ps->path) < 0) + return -1; + + free(ps->path); + ps->path = url; + + return setup(ps); +} + +static int +start(struct player_source *ps) +{ + struct evbuffer *evbuf; + short flags; + int ret; + int icy_timer; + + evbuf = evbuffer_new(); + + ret = -1; + flags = 0; + while (!input_loop_break && !(flags & INPUT_FLAG_EOF)) + { + // We set "wanted" to 1 because the read size doesn't matter to us + // TODO optimize? + ret = transcode(evbuf, 1, ps->xcode, &icy_timer); + if (ret < 0) + break; + + flags = ((ret == 0) ? INPUT_FLAG_EOF : 0) | + (icy_timer ? INPUT_FLAG_METADATA : 0); + + ret = input_write(evbuf, flags); + if (ret < 0) + break; + } + + evbuffer_free(evbuf); + + return ret; +} + +static int +stop(struct player_source *ps) +{ + transcode_cleanup(ps->xcode); + + ps->xcode = NULL; + ps->setup_done = 0; + + return 0; +} + +static int +seek(struct player_source *ps, int seek_ms) +{ + return transcode_seek(ps->xcode, seek_ms); +} + +struct input_definition input_file = +{ + .name = "file", + .type = INPUT_TYPE_FILE, + .disabled = 0, + .setup = setup, + .start = start, + .stop = stop, + .seek = seek, +}; + +struct input_definition input_http = +{ + .name = "http", + .type = INPUT_TYPE_HTTP, + .disabled = 0, + .setup = http_setup, + .start = start, + .stop = stop, +}; diff --git a/src/player.c b/src/player.c index 32225ca2..87478dc4 100644 --- a/src/player.c +++ b/src/player.c @@ -1,6 +1,6 @@ /* * Copyright (C) 2010-2011 Julien BLACHE - * Copyright (C) 2016 Espen Jürgensen + * Copyright (C) 2016-2017 Espen Jürgensen * * 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 @@ -15,6 +15,22 @@ * 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 + + + * About player.c + * -------------- + * The main tasks of the player are the following: + * - handle playback commands, status checks and events from other threads + * - receive audio from the input thread and to own the playback buffer + * - feed the outputs at the appropriate rate (controlled by the playback timer) + * - output device handling (partly outsourced to outputs.c) + * - notify about playback status changes + * - maintain the playback queue + * + * The player thread should *never* be making operations that may block, since + * that could block callers requesting status (effectively making forked-daapd + * unresponsive) and it could also starve the outputs. + * */ #ifdef HAVE_CONFIG_H @@ -55,18 +71,13 @@ #include "listener.h" #include "commands.h" -/* Audio outputs */ +/* Audio and metadata outputs */ #include "outputs.h" -/* Audio inputs */ -#include "transcode.h" -#include "pipe.h" -#ifdef HAVE_SPOTIFY_H -# include "spotify.h" -#endif +/* Audio and metadata input */ +#include "input.h" -/* Metadata input/output */ -#include "http.h" +/* Scrobbling */ #ifdef LASTFM # include "lastfm.h" #endif @@ -84,48 +95,6 @@ // Used to keep the player from getting ahead of a rate limited source (see below) #define PLAYER_TICKS_MAX_OVERRUN 2 -struct player_source -{ - /* Id of the file/item in the files database */ - uint32_t id; - - /* Item-Id of the file/item in the queue */ - uint32_t item_id; - - /* Length of the file/item in milliseconds */ - uint32_t len_ms; - - enum data_kind data_kind; - enum media_kind media_kind; - char *path; - - /* Start time of the media item as rtp-time - The stream-start is the rtp-time the media item did or would have - started playing (after seek or pause), therefor the elapsed time of the - media item is always: - elapsed time = current rtptime - stream-start */ - uint64_t stream_start; - - /* Output start time of the media item as rtp-time - The output start time is the rtp-time of the first audio packet send - to the audio outputs. - It differs from stream-start especially after a seek, where the first audio - packet has the next rtp-time as output start and stream start becomes the - rtp-time the media item would have been started playing if the seek did - not happen. */ - uint64_t output_start; - - /* End time of media item as rtp-time - The end time is set if the reading (source_read) of the media item reached - end of file, until then it is 0. */ - uint64_t end; - - struct transcode_ctx *xcode; - int setup_done; - - struct player_source *play_next; -}; - struct volume_param { int volume; uint64_t spk_id; @@ -594,125 +563,11 @@ history_add(uint32_t id, uint32_t item_id) /* Audio sources */ -/* - * Initializes the given player source for playback - */ -static int -stream_setup(struct player_source *ps) -{ - char *url; - int ret; - - if (!ps) - { - DPRINTF(E_LOG, L_PLAYER, "No player source given to stream_setup\n"); - return -1; - } - - if (ps->setup_done) - { - DPRINTF(E_LOG, L_PLAYER, "Given player source already setup (id = %d)\n", ps->id); - return -1; - } - - // Setup depending on data kind - switch (ps->data_kind) - { - case DATA_KIND_FILE: - ps->xcode = transcode_setup(ps->data_kind, ps->path, ps->len_ms, XCODE_PCM16_NOHEADER, NULL); - ret = ps->xcode ? 0 : -1; - break; - - case DATA_KIND_HTTP: - ret = http_stream_setup(&url, ps->path); - if (ret < 0) - break; - - free(ps->path); - ps->path = url; - - ps->xcode = transcode_setup(ps->data_kind, ps->path, ps->len_ms, XCODE_PCM16_NOHEADER, NULL); - ret = ps->xcode ? 0 : -1; - break; - - case DATA_KIND_SPOTIFY: -#ifdef HAVE_SPOTIFY_H - ret = spotify_playback_setup(ps->path); -#else - DPRINTF(E_LOG, L_PLAYER, "Player source has data kind 'spotify' (%d), but forked-daapd is compiled without spotify support - cannot setup source '%s'\n", - ps->data_kind, ps->path); - ret = -1; -#endif - break; - - case DATA_KIND_PIPE: - ret = pipe_setup(ps->path); - break; - - default: - DPRINTF(E_LOG, L_PLAYER, "Unknown data kind (%d) for player source - cannot setup source '%s'\n", - ps->data_kind, ps->path); - ret = -1; - } - - if (ret == 0) - ps->setup_done = 1; - else - DPRINTF(E_LOG, L_PLAYER, "Failed to setup player source (id = %d)\n", ps->id); - - return ret; -} - -/* - * Starts or resumes plaback for the given player source - */ -static int -stream_play(struct player_source *ps) -{ - int ret; - - if (!ps) - { - DPRINTF(E_LOG, L_PLAYER, "Stream play called with no active streaming player source\n"); - return -1; - } - - if (!ps->setup_done) - { - DPRINTF(E_LOG, L_PLAYER, "Given player source not setup, play not possible\n"); - return -1; - } - - // Start/resume playback depending on data kind - switch (ps->data_kind) - { - case DATA_KIND_HTTP: - case DATA_KIND_FILE: - ret = 0; - break; - -#ifdef HAVE_SPOTIFY_H - case DATA_KIND_SPOTIFY: - ret = spotify_playback_play(); - break; -#endif - - case DATA_KIND_PIPE: - ret = 0; - break; - - default: - ret = -1; - } - - return ret; -} - /* * Read up to "len" data from the given player source and returns * the actual amount of data read. */ -static int +/*static int stream_read(struct player_source *ps, int len) { int icy_timer; @@ -759,149 +614,7 @@ stream_read(struct player_source *ps, int len) } return ret; -} - -/* - * Pauses playback of the given player source - */ -static int -stream_pause(struct player_source *ps) -{ - int ret; - - if (!ps) - { - DPRINTF(E_LOG, L_PLAYER, "Stream pause called with no active streaming player source\n"); - return -1; - } - - if (!ps->setup_done) - { - DPRINTF(E_LOG, L_PLAYER, "Given player source not setup, pause not possible\n"); - return -1; - } - - // Pause playback depending on data kind - switch (ps->data_kind) - { - case DATA_KIND_HTTP: - ret = 0; - break; - - case DATA_KIND_FILE: - ret = 0; - break; - -#ifdef HAVE_SPOTIFY_H - case DATA_KIND_SPOTIFY: - ret = spotify_playback_pause(); - break; -#endif - - case DATA_KIND_PIPE: - ret = 0; - break; - - default: - ret = -1; - } - - return ret; -} - -/* - * Seeks to the given position in milliseconds of the given player source - */ -static int -stream_seek(struct player_source *ps, int seek_ms) -{ - int ret; - - if (!ps) - { - DPRINTF(E_LOG, L_PLAYER, "Stream seek called with no active streaming player source\n"); - return -1; - } - - if (!ps->setup_done) - { - DPRINTF(E_LOG, L_PLAYER, "Given player source not setup, seek not possible\n"); - return -1; - } - - // Seek depending on data kind - switch (ps->data_kind) - { - case DATA_KIND_HTTP: - ret = 0; - break; - - case DATA_KIND_FILE: - ret = transcode_seek(ps->xcode, seek_ms); - break; - -#ifdef HAVE_SPOTIFY_H - case DATA_KIND_SPOTIFY: - ret = spotify_playback_seek(seek_ms); - break; -#endif - - case DATA_KIND_PIPE: - ret = 0; - break; - - default: - ret = -1; - } - - return ret; -} - -/* - * Stops playback and cleanup for the given player source - */ -static int -stream_stop(struct player_source *ps) -{ - if (!ps) - { - DPRINTF(E_LOG, L_PLAYER, "Stream cleanup called with no active streaming player source\n"); - return -1; - } - - if (!ps->setup_done) - { - DPRINTF(E_LOG, L_PLAYER, "Given player source not setup, cleanup not possible\n"); - return -1; - } - - switch (ps->data_kind) - { - case DATA_KIND_FILE: - case DATA_KIND_HTTP: - if (ps->xcode) - { - transcode_cleanup(ps->xcode); - ps->xcode = NULL; - } - break; - - case DATA_KIND_SPOTIFY: -#ifdef HAVE_SPOTIFY_H - spotify_playback_stop(); -#endif - break; - - case DATA_KIND_PIPE: - pipe_cleanup(); - break; - } - - ps->setup_done = 0; - - return 0; -} - +}*/ static struct player_source * source_now_playing() @@ -959,7 +672,7 @@ source_stop() struct player_source *ps_temp; if (cur_streaming) - stream_stop(cur_streaming); + input_stop(cur_streaming); ps_playing = source_now_playing(); @@ -997,20 +710,20 @@ source_pause(uint64_t pos) if (!ps_playing) return -1; - if (cur_streaming) + if (cur_streaming && (cur_streaming == ps_playing)) { if (ps_playing != cur_streaming) { DPRINTF(E_DBG, L_PLAYER, "Pause called on playing source (id=%d) and streaming source already " "switched to the next item (id=%d)\n", ps_playing->id, cur_streaming->id); - ret = stream_stop(cur_streaming); + ret = input_stop(cur_streaming); if (ret < 0) return -1; } else { - ret = stream_pause(cur_streaming); + ret = input_pause(cur_streaming); if (ret < 0) return -1; } @@ -1034,7 +747,7 @@ source_pause(uint64_t pos) { DPRINTF(E_INFO, L_PLAYER, "Opening '%s'\n", cur_streaming->path); - ret = stream_setup(cur_streaming); + ret = input_setup(cur_streaming); if (ret < 0) { DPRINTF(E_LOG, L_PLAYER, "Failed to open '%s'\n", cur_streaming->path); @@ -1045,7 +758,9 @@ source_pause(uint64_t pos) /* Seek back to the pause position */ seek_frames = (pos - cur_streaming->stream_start); seek_ms = (int)((seek_frames * 1000) / 44100); - ret = stream_seek(cur_streaming, seek_ms); + ret = input_seek(cur_streaming, seek_ms); + +// TODO what if ret < 0? /* Adjust start_pos to take into account the pause and seek back */ cur_streaming->stream_start = last_rtptime + AIRTUNES_V2_PACKET_SAMPLES - ((uint64_t)ret * 44100) / 1000; @@ -1067,7 +782,7 @@ source_seek(int seek_ms) { int ret; - ret = stream_seek(cur_streaming, seek_ms); + ret = input_seek(cur_streaming, seek_ms); if (ret < 0) return -1; @@ -1086,7 +801,7 @@ source_play() { int ret; - ret = stream_play(cur_streaming); + ret = input_start(cur_streaming); ticks_skip = 0; memset(rawbuf, 0, sizeof(rawbuf)); @@ -1116,7 +831,7 @@ source_open(struct player_source *ps, uint64_t start_pos, int seek_ms) return -1; } - ret = stream_setup(ps); + ret = input_setup(ps); if (ret < 0) { DPRINTF(E_LOG, L_PLAYER, "Failed to open '%s' (id=%d, item-id=%d)\n", ps->path, ps->id, ps->item_id); @@ -1151,7 +866,7 @@ source_open(struct player_source *ps, uint64_t start_pos, int seek_ms) static int source_close(uint64_t end_pos) { - stream_stop(cur_streaming); + input_stop(cur_streaming); cur_streaming->end = end_pos; @@ -1339,6 +1054,7 @@ source_read(uint8_t *buf, int len, uint64_t rtptime) { int ret; int nbytes; + short flags; char *silence_buf; struct player_source *ps; @@ -1350,9 +1066,10 @@ source_read(uint8_t *buf, int len, uint64_t rtptime) { if (evbuffer_get_length(audio_buf) == 0) { + flags = 0; if (cur_streaming) { - ret = stream_read(cur_streaming, len - nbytes); + ret = input_read(audio_buf, len - nbytes, &flags); } else if (cur_playing) { @@ -1369,9 +1086,10 @@ source_read(uint8_t *buf, int len, uint64_t rtptime) return -1; } - if (ret <= 0) +// if (ret == 0) +//TODO Underrun -> pause + if ((ret < 0) || (flags & INPUT_FLAG_EOF)) { - /* EOF or error */ source_close(rtptime + BTOS(nbytes) - 1); DPRINTF(E_DBG, L_PLAYER, "New file\n"); @@ -3224,7 +2942,6 @@ player_playback_prev(void) return ret; } - void player_speaker_enumerate(spk_enum_cb cb, void *arg) { @@ -3539,6 +3256,13 @@ player_init(void) goto outputs_fail; } + ret = input_init(); + if (ret < 0) + { + DPRINTF(E_FATAL, L_PLAYER, "Input initiation failed\n"); + goto input_fail; + } + ret = pthread_create(&tid_player, NULL, player, NULL); if (ret < 0) { @@ -3554,6 +3278,8 @@ player_init(void) return 0; thread_fail: + input_deinit(); + input_fail: outputs_deinit(); outputs_fail: commands_base_free(cmdbase); @@ -3599,6 +3325,7 @@ player_deinit(void) evbuffer_free(audio_buf); + input_deinit(); outputs_deinit(); event_base_free(evbase_player); diff --git a/src/player.h b/src/player.h index 000d3c77..a22f1820 100644 --- a/src/player.h +++ b/src/player.h @@ -13,11 +13,6 @@ /* AirTunes v2 number of samples per packet */ #define AIRTUNES_V2_PACKET_SAMPLES 352 - -/* Samples to bytes, bytes to samples */ -#define STOB(s) ((s) * 4) -#define BTOS(b) ((b) / 4) - /* Maximum number of previously played songs that are remembered */ #define MAX_HISTORY_COUNT 20 @@ -115,7 +110,6 @@ player_playback_next(void); int player_playback_prev(void); - int player_volume_set(int vol); From c92ebf9dfb2aa3e1dc885a3ded0283ee5fa6edf8 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Tue, 27 Dec 2016 22:11:11 +0100 Subject: [PATCH 04/37] [player] Fix problem where player_playback_cb triggers after playback stop --- src/input.c | 6 ++++++ src/player.c | 13 +++++++++++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/input.c b/src/input.c index 828bdc92..c7e3cea6 100644 --- a/src/input.c +++ b/src/input.c @@ -236,6 +236,12 @@ input_read(struct evbuffer *evbuf, size_t want, short *flags) *flags = 0; + if (!tid_input) + { + DPRINTF(E_LOG, L_PLAYER, "Bug! Read called, but playback not running\n"); + return -1; + } + pthread_mutex_lock(&input_buffer.mutex); #ifdef DEBUG diff --git a/src/player.c b/src/player.c index 87478dc4..cda99bc5 100644 --- a/src/player.c +++ b/src/player.c @@ -355,6 +355,15 @@ pb_timer_start(void) struct itimerspec tick; int ret; + ret = event_add(pb_timer_ev, NULL); + if (ret < 0) + { + DPRINTF(E_LOG, L_PLAYER, "Could not add playback timer\n"); + + return -1; + } + + tick.it_interval = tick_interval; tick.it_value = tick_interval; @@ -379,6 +388,8 @@ pb_timer_stop(void) struct itimerspec tick; int ret; + event_del(pb_timer_ev); + memset(&tick, 0, sizeof(struct itimerspec)); #ifdef HAVE_TIMERFD @@ -3245,8 +3256,6 @@ player_init(void) goto evnew_fail; } - event_add(pb_timer_ev, NULL); - cmdbase = commands_base_new(evbase_player, NULL); ret = outputs_init(); From 79639c73ed87cee83b7f1bed268a443fe0153259 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Thu, 29 Dec 2016 00:39:23 +0100 Subject: [PATCH 05/37] [input] Add Spotify input module --- src/Makefile.am | 2 +- src/input.c | 21 +++++ src/input.h | 15 ++- src/inputs/spotify.c | 96 +++++++++++++++++++ src/player.c | 6 +- src/spotify.c | 220 +++++-------------------------------------- src/spotify.h | 3 - 7 files changed, 161 insertions(+), 202 deletions(-) create mode 100644 src/inputs/spotify.c diff --git a/src/Makefile.am b/src/Makefile.am index faa62afc..ef5b1498 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -6,7 +6,7 @@ ITUNES_SRC=library/filescanner_itunes.c endif if COND_SPOTIFY -SPOTIFY_SRC=spotify.c spotify.h spotify_webapi.c spotify_webapi.h +SPOTIFY_SRC=spotify.c spotify.h spotify_webapi.c spotify_webapi.h inputs/spotify.c endif if COND_LASTFM diff --git a/src/input.c b/src/input.c index c7e3cea6..f037ccce 100644 --- a/src/input.c +++ b/src/input.c @@ -46,11 +46,17 @@ extern struct input_definition input_file; extern struct input_definition input_http; +#ifdef HAVE_SPOTIFY_H +extern struct input_definition input_spotify; +#endif // Must be in sync with enum input_types static struct input_definition *inputs[] = { &input_file, &input_http, +#ifdef HAVE_SPOTIFY_H + &input_spotify, +#endif NULL }; @@ -124,6 +130,11 @@ map_data_kind(int data_kind) case DATA_KIND_HTTP: return INPUT_TYPE_HTTP; +#ifdef HAVE_SPOTIFY_H + case DATA_KIND_SPOTIFY: + return INPUT_TYPE_SPOTIFY; +#endif + default: return -1; } @@ -185,6 +196,15 @@ playback(void *arg) pthread_exit(NULL); } +void +input_wait(void) +{ + pthread_mutex_lock(&input_buffer.mutex); + pthread_cond_wait(&input_buffer.cond, &input_buffer.mutex); + + pthread_mutex_unlock(&input_buffer.mutex); +} + // Called by input modules from within the playback loop int input_write(struct evbuffer *evbuf, short flags) @@ -333,6 +353,7 @@ input_pause(struct player_source *ps) pthread_cond_signal(&input_buffer.cond); pthread_mutex_unlock(&input_buffer.mutex); + // TODO What if input thread is hanging waiting for source? Kill thread? ret = pthread_join(tid_input, NULL); if (ret != 0) { diff --git a/src/input.h b/src/input.h index 4b6f3a57..3190d864 100644 --- a/src/input.h +++ b/src/input.h @@ -2,6 +2,9 @@ #ifndef __INPUT_H__ #define __INPUT_H__ +#ifdef HAVE_CONFIG_H +# include +#endif #include #include "transcode.h" @@ -10,6 +13,9 @@ enum input_types { INPUT_TYPE_FILE, INPUT_TYPE_HTTP, +#ifdef HAVE_SPOTIFY_H + INPUT_TYPE_SPOTIFY, +#endif }; enum input_flags @@ -96,7 +102,7 @@ struct input_definition }; /* - * Input modules use this to test if playback should be stopped or seeked + * Input modules should use this to test if playback should end */ int input_loop_break; @@ -114,6 +120,13 @@ int input_loop_break; int input_write(struct evbuffer *evbuf, short flags); +/* + * Input modules can use this to wait in the playback loop (like input_write() + * would have done) + */ +void +input_wait(void); + /* * Move a chunk of stream data from the player's input buffer to an output * buffer. Should only be called by the player thread. Will not block. diff --git a/src/inputs/spotify.c b/src/inputs/spotify.c new file mode 100644 index 00000000..f4d5cc8a --- /dev/null +++ b/src/inputs/spotify.c @@ -0,0 +1,96 @@ +/* + * 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 + */ + +#include +#include +#include +#include + +#include "input.h" +#include "spotify.h" + +static int +setup(struct player_source *ps) +{ + int ret; + + ret = spotify_playback_setup(ps->path); + if (ret < 0) + return -1; + + ps->setup_done = 1; + + return 0; +} + +static int +start(struct player_source *ps) +{ + int ret; + + ret = spotify_playback_play(); + if (ret < 0) + return -1; + + while (!input_loop_break) + { + input_wait(); + } + + ret = spotify_playback_pause(); + + return ret; +} + +static int +stop(struct player_source *ps) +{ + int ret; + + ret = spotify_playback_stop(); + if (ret < 0) + return -1; + + ps->setup_done = 0; + + return 0; +} + +static int +seek(struct player_source *ps, int seek_ms) +{ + int ret; + + ret = spotify_playback_seek(seek_ms); + if (ret < 0) + return -1; + + return ret; +} + +struct input_definition input_spotify = +{ + .name = "Spotify", + .type = INPUT_TYPE_SPOTIFY, + .disabled = 0, + .setup = setup, + .start = start, + .stop = stop, + .seek = seek, +}; + diff --git a/src/player.c b/src/player.c index cda99bc5..19f44304 100644 --- a/src/player.c +++ b/src/player.c @@ -1097,9 +1097,9 @@ source_read(uint8_t *buf, int len, uint64_t rtptime) return -1; } -// if (ret == 0) -//TODO Underrun -> pause - if ((ret < 0) || (flags & INPUT_FLAG_EOF)) + if (ret == 0) + sleep(1); // TODO Underrun -> proper pause + else if ((ret < 0) || (flags & INPUT_FLAG_EOF)) { source_close(rtptime + BTOS(nbytes) - 1); diff --git a/src/spotify.c b/src/spotify.c index ce3eba70..8e967380 100644 --- a/src/spotify.c +++ b/src/spotify.c @@ -55,6 +55,7 @@ #include "cache.h" #include "commands.h" #include "library.h" +#include "input.h" /* TODO for the web api: * - UI should be prettier @@ -88,22 +89,6 @@ #define SPOTIFY_WEB_REQUESTS_MAX 20 /* --- Types --- */ -typedef struct audio_fifo_data -{ - TAILQ_ENTRY(audio_fifo_data) link; - int nsamples; - int16_t samples[0]; -} audio_fifo_data_t; - -typedef struct audio_fifo -{ - TAILQ_HEAD(, audio_fifo_data) q; - int qlen; - int fullcount; - pthread_mutex_t mutex; - pthread_cond_t cond; -} audio_fifo_t; - enum spotify_state { SPOTIFY_STATE_INACTIVE, @@ -114,12 +99,6 @@ enum spotify_state SPOTIFY_STATE_STOPPED, }; -struct audio_get_param -{ - struct evbuffer *evbuf; - int wanted; -}; - struct artwork_get_param { struct evbuffer *evbuf; @@ -164,8 +143,8 @@ static int spotify_saved_plid; // Flag to avoid triggering playlist change events while the (re)scan is running static bool scanning; -// Audio fifo -static audio_fifo_t *g_audio_fifo; +// Audio buffer +static struct evbuffer *spotify_audio_buffer; /** * The application key is specific to forked-daapd, and allows Spotify @@ -1180,25 +1159,6 @@ mk_reltime(struct timespec *ts, time_t sec) ts->tv_sec += sec; } -static void -audio_fifo_flush(void) -{ - audio_fifo_data_t *afd; - - DPRINTF(E_DBG, L_SPOTIFY, "Flushing audio fifo\n"); - - CHECK_ERR(L_SPOTIFY, pthread_mutex_lock(&g_audio_fifo->mutex)); - - while((afd = TAILQ_FIRST(&g_audio_fifo->q))) { - TAILQ_REMOVE(&g_audio_fifo->q, afd, link); - free(afd); - } - - g_audio_fifo->qlen = 0; - g_audio_fifo->fullcount = 0; - CHECK_ERR(L_SPOTIFY, pthread_mutex_unlock(&g_audio_fifo->mutex)); -} - static enum command_state playback_setup(void *arg, int *retval) { @@ -1240,8 +1200,6 @@ playback_setup(void *arg, int *retval) return COMMAND_END; } - audio_fifo_flush(); - *retval = 0; return COMMAND_END; } @@ -1330,8 +1288,6 @@ playback_seek(void *arg, int *retval) return COMMAND_END; } - audio_fifo_flush(); - *retval = 0; return COMMAND_END; } @@ -1353,92 +1309,14 @@ playback_eot(void *arg, int *retval) g_state = SPOTIFY_STATE_STOPPING; + // TODO 1) This will block for a while, but perhaps ok? + // 2) spotify_audio_buffer not entirely thread safe here (but unlikely risk...) + input_write(spotify_audio_buffer, INPUT_FLAG_EOF); + *retval = 0; return COMMAND_END; } -static enum command_state -audio_get(void *arg, int *retval) -{ - struct audio_get_param *audio; - struct timespec ts; - audio_fifo_data_t *afd; - int processed; - int timeout; - int ret; - int err; - int s; - - audio = (struct audio_get_param *) arg; - afd = NULL; - processed = 0; - - // If spotify was paused begin by resuming playback - if (g_state == SPOTIFY_STATE_PAUSED) - playback_play(NULL, retval); - - CHECK_ERR(L_SPOTIFY, pthread_mutex_lock(&g_audio_fifo->mutex)); - - while ((processed < audio->wanted) && (g_state != SPOTIFY_STATE_STOPPED)) - { - // If track has ended and buffer is empty - if ((g_state == SPOTIFY_STATE_STOPPING) && (g_audio_fifo->qlen <= 0)) - { - DPRINTF(E_DBG, L_SPOTIFY, "Track finished\n"); - g_state = SPOTIFY_STATE_STOPPED; - break; - } - - // If buffer is empty, wait for audio, but use timed wait so we don't - // risk waiting forever (maybe the player stopped while we were waiting) - timeout = 0; - while ( !(afd = TAILQ_FIRST(&g_audio_fifo->q)) && - (g_state != SPOTIFY_STATE_STOPPED) && - (g_state != SPOTIFY_STATE_STOPPING) && - (timeout < SPOTIFY_TIMEOUT) ) - { - DPRINTF(E_DBG, L_SPOTIFY, "Waiting for audio\n"); - timeout += 5; - mk_reltime(&ts, 5); - CHECK_ERR_EXCEPT(L_SPOTIFY, pthread_cond_timedwait(&g_audio_fifo->cond, &g_audio_fifo->mutex, &ts), err, ETIMEDOUT); - } - - if ((!afd) && (timeout >= SPOTIFY_TIMEOUT)) - { - DPRINTF(E_LOG, L_SPOTIFY, "Timeout waiting for audio (waited %d sec)\n", timeout); - - spotify_playback_stop_nonblock(); - } - - if (!afd) - break; - - TAILQ_REMOVE(&g_audio_fifo->q, afd, link); - g_audio_fifo->qlen -= afd->nsamples; - - s = afd->nsamples * sizeof(int16_t) * 2; - - ret = evbuffer_add(audio->evbuf, afd->samples, s); - free(afd); - afd = NULL; - if (ret < 0) - { - DPRINTF(E_LOG, L_SPOTIFY, "Out of memory for evbuffer (tried to add %d bytes)\n", s); - CHECK_ERR(L_SPOTIFY, pthread_mutex_unlock(&g_audio_fifo->mutex)); - *retval = -1; - return COMMAND_END; - } - - processed += s; - } - - CHECK_ERR(L_SPOTIFY, pthread_mutex_unlock(&g_audio_fifo->mutex)); - - - *retval = processed; - return COMMAND_END; -} - static void artwork_loaded_cb(sp_image *image, void *userdata) { @@ -1681,8 +1559,8 @@ logged_out(sp_session *sess) static int music_delivery(sp_session *sess, const sp_audioformat *format, const void *frames, int num_frames) { - audio_fifo_data_t *afd; - size_t s; + size_t size; + int ret; /* No support for resampling right now */ if ((format->sample_rate != 44100) || (format->channels != 2)) @@ -1692,44 +1570,26 @@ static int music_delivery(sp_session *sess, const sp_audioformat *format, return num_frames; } + // Audio discontinuity, e.g. seek if (num_frames == 0) - return 0; // Audio discontinuity, do nothing - - CHECK_ERR(L_SPOTIFY, pthread_mutex_lock(&g_audio_fifo->mutex)); - - /* Buffer three seconds of audio */ - if (g_audio_fifo->qlen > (3 * format->sample_rate)) { - // If the buffer has been full the last 300 times (~about a minute) we - // assume the player thread paused/died without telling us, so we signal pause - if (g_audio_fifo->fullcount < 300) - g_audio_fifo->fullcount++; - else - { - DPRINTF(E_WARN, L_SPOTIFY, "Buffer full more than 300 times, pausing\n"); - spotify_playback_pause_nonblock(); - g_audio_fifo->fullcount = 0; - } - - CHECK_ERR(L_SPOTIFY, pthread_mutex_unlock(&g_audio_fifo->mutex)); - + evbuffer_drain(spotify_audio_buffer, evbuffer_get_length(spotify_audio_buffer)); return 0; } - else - g_audio_fifo->fullcount = 0; - s = num_frames * sizeof(int16_t) * format->channels; + size = num_frames * sizeof(int16_t) * format->channels; - afd = malloc(sizeof(*afd) + s); - memcpy(afd->samples, frames, s); + ret = evbuffer_add(spotify_audio_buffer, frames, size); + if (ret < 0) + { + DPRINTF(E_LOG, L_SPOTIFY, "Out of memory adding audio to buffer\n"); + return num_frames; + } - afd->nsamples = num_frames; - - TAILQ_INSERT_TAIL(&g_audio_fifo->q, afd, link); - g_audio_fifo->qlen += num_frames; - - CHECK_ERR(L_SPOTIFY, pthread_cond_signal(&g_audio_fifo->cond)); - CHECK_ERR(L_SPOTIFY, pthread_mutex_unlock(&g_audio_fifo->mutex)); + // The input buffer only accepts writing when it is approaching depletion, and + // because we use NONBLOCK it will just return if this is not the case. So in + // most cases no actual write is made and spotify_audio_buffer will just grow. + input_write(spotify_audio_buffer, INPUT_FLAG_NONBLOCK); return num_frames; } @@ -1975,18 +1835,6 @@ spotify_playback_seek(int ms) return -1; } -/* Thread: player */ -int -spotify_audio_get(struct evbuffer *evbuf, int wanted) -{ - struct audio_get_param audio; - - audio.evbuf = evbuf; - audio.wanted = wanted; - - return commands_exec_sync(cmdbase, audio_get, NULL, &audio); -} - /* Thread: httpd (artwork) and worker */ int spotify_artwork_get(struct evbuffer *evbuf, char *path, int max_w, int max_h) @@ -2613,7 +2461,6 @@ spotify_init(void) event_add(g_notifyev, NULL); - cmdbase = commands_base_new(evbase_spotify, exit_cb); if (!cmdbase) { @@ -2651,17 +2498,7 @@ spotify_init(void) break; } - /* Prepare audio buffer */ - g_audio_fifo = (audio_fifo_t *)malloc(sizeof(audio_fifo_t)); - if (!g_audio_fifo) - { - DPRINTF(E_LOG, L_SPOTIFY, "Out of memory for audio buffer\n"); - goto audio_fifo_fail; - } - TAILQ_INIT(&g_audio_fifo->q); - g_audio_fifo->qlen = 0; - CHECK_ERR(L_SPOTIFY, mutex_init(&g_audio_fifo->mutex)); - CHECK_ERR(L_SPOTIFY, pthread_cond_init(&g_audio_fifo->cond, NULL)); + spotify_audio_buffer = evbuffer_new(); CHECK_ERR(L_SPOTIFY, mutex_init(&login_lck)); CHECK_ERR(L_SPOTIFY, pthread_cond_init(&login_cond, NULL)); @@ -2687,11 +2524,8 @@ spotify_init(void) CHECK_ERR(L_SPOTIFY, pthread_cond_destroy(&login_cond)); CHECK_ERR(L_SPOTIFY, pthread_mutex_destroy(&login_lck)); - CHECK_ERR(L_SPOTIFY, pthread_cond_destroy(&g_audio_fifo->cond)); - CHECK_ERR(L_SPOTIFY, pthread_mutex_destroy(&g_audio_fifo->mutex)); - free(g_audio_fifo); + evbuffer_free(spotify_audio_buffer); - audio_fifo_fail: fptr_sp_session_release(g_sess); g_sess = NULL; @@ -2751,10 +2585,8 @@ spotify_deinit(void) CHECK_ERR(L_SPOTIFY, pthread_cond_destroy(&login_cond)); CHECK_ERR(L_SPOTIFY, pthread_mutex_destroy(&login_lck)); - /* Clear audio fifo */ - CHECK_ERR(L_SPOTIFY, pthread_cond_destroy(&g_audio_fifo->cond)); - CHECK_ERR(L_SPOTIFY, pthread_mutex_destroy(&g_audio_fifo->mutex)); - free(g_audio_fifo); + /* Free audio buffer */ + evbuffer_free(spotify_audio_buffer); /* Release libspotify handle */ dlclose(g_libhandle); diff --git a/src/spotify.h b/src/spotify.h index 7b4b6f9d..ca028e14 100644 --- a/src/spotify.h +++ b/src/spotify.h @@ -27,9 +27,6 @@ spotify_playback_stop_nonblock(void); int spotify_playback_seek(int ms); -int -spotify_audio_get(struct evbuffer *evbuf, int wanted); - int spotify_artwork_get(struct evbuffer *evbuf, char *path, int max_w, int max_h); From 60daf03f66490d1629bb43d58a2889a89b91e369 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Sun, 8 Jan 2017 00:21:47 +0100 Subject: [PATCH 06/37] [cache/scan] Let notifications from db.c about library updates go through filescanner/listener instead of directly to the cache --- src/cache.c | 116 ++++++++++++++------------------------ src/cache.h | 3 - src/db.c | 22 +++++--- src/library/filescanner.c | 50 +++++++++++++++- src/library/filescanner.h | 3 + 5 files changed, 107 insertions(+), 87 deletions(-) diff --git a/src/cache.c b/src/cache.c index 66bdd0ce..1aeb9096 100644 --- a/src/cache.c +++ b/src/cache.c @@ -42,6 +42,7 @@ #include "httpd_daap.h" #include "db.h" #include "cache.h" +#include "listener.h" #include "commands.h" @@ -73,8 +74,8 @@ static pthread_t tid_cache; // Event base, pipes and events struct event_base *evbase_cache; -static struct event *g_cacheev; static struct commands_base *cmdbase; +static struct event *cache_daap_updateev; static int g_initialized; @@ -91,8 +92,6 @@ struct stash uint8_t *data; } g_stash; -// After being triggered wait 60 seconds before rebuilding cache -static struct timeval g_wait = { 60, 0 }; static int g_suspended; // The user may configure a threshold (in msec), and queries slower than @@ -608,6 +607,7 @@ cache_daap_query_add(void *arg, int *retval) #define Q_TMPL "INSERT OR REPLACE INTO queries (user_agent, query, msec, timestamp) VALUES ('%q', '%q', %d, %" PRIi64 ");" #define Q_CLEANUP "DELETE FROM queries WHERE id NOT IN (SELECT id FROM queries ORDER BY timestamp DESC LIMIT 20);" struct cache_arg *cmdarg; + struct timeval delay = { 60, 0 }; char *query; char *errmsg; int ret; @@ -663,7 +663,9 @@ cache_daap_query_add(void *arg, int *retval) return COMMAND_END; } - cache_daap_trigger(); + // Will set of cache regeneration after waiting a bit (so there is less risk + // of disturbing the user) + evtimer_add(cache_daap_updateev, &delay); *retval = 0; return COMMAND_END; @@ -790,7 +792,13 @@ cache_daap_update_cb(int fd, short what, void *arg) char *query; int ret; - DPRINTF(E_INFO, L_CACHE, "Timeout reached, time to update DAAP cache\n"); + if (g_suspended) + { + DPRINTF(E_DBG, L_CACHE, "Got a request to update DAAP cache while suspended\n"); + return; + } + + DPRINTF(E_LOG, L_CACHE, "Beginning DAAP cache update\n"); ret = sqlite3_exec(g_db_hdl, "DELETE FROM replies;", NULL, NULL, &errmsg); if (ret != SQLITE_OK) @@ -845,63 +853,28 @@ cache_daap_update_cb(int fd, short what, void *arg) sqlite3_finalize(stmt); - DPRINTF(E_INFO, L_CACHE, "DAAP cache updated\n"); + DPRINTF(E_LOG, L_CACHE, "DAAP cache updated\n"); } -/* This function will just set a timer, which when it times out will trigger - * the actual cache update. The purpose is to avoid avoid cache updates when - * the database is busy, eg during a library scan. +/* Sets off an update by activating the event. The delay is because we are low + * priority compared to other listeners of database updates. */ static enum command_state -cache_daap_update_timer(void *arg, int *ret) +cache_daap_update(void *arg, int *retval) { - if (!g_cacheev) - { - *ret = -1; - return COMMAND_END; - } - - evtimer_add(g_cacheev, &g_wait); - - *ret = 0; + struct timeval delay = { 10, 0 }; + *retval = event_add(cache_daap_updateev, &delay); return COMMAND_END; } -static enum command_state -cache_daap_suspend_timer(void *arg, int *ret) +/* Callback from filescanner thread */ +static void +cache_daap_listener_cb(enum listener_event_type type) { - if (!g_cacheev) - { - *ret = -1; - return COMMAND_END; - } - - g_suspended = evtimer_pending(g_cacheev, NULL); - if (g_suspended) - evtimer_del(g_cacheev); - - *ret = 0; - - return COMMAND_END; + commands_exec_async(cmdbase, cache_daap_update, NULL); } -static enum command_state -cache_daap_resume_timer(void *arg, int *ret) -{ - if (!g_cacheev) - { - *ret = -1; - return COMMAND_END; - } - - if (g_suspended) - evtimer_add(g_cacheev, &g_wait); - - *ret = 0; - - return COMMAND_END; -} /* * Updates cached timestamps to current time for all cache entries for the given path, if the file was not modfied @@ -1328,31 +1301,16 @@ cache(void *arg) * */ -void -cache_daap_trigger(void) -{ - if (!g_initialized) - return; - - commands_exec_async(cmdbase, cache_daap_update_timer, NULL); -} - void cache_daap_suspend(void) { - if (!g_initialized) - return; - - commands_exec_async(cmdbase, cache_daap_suspend_timer, NULL); + g_suspended = 1; } void cache_daap_resume(void) { - if (!g_initialized) - return; - - commands_exec_async(cmdbase, cache_daap_resume_timer, NULL); + g_suspended = 0; } int @@ -1377,15 +1335,13 @@ cache_daap_add(const char *query, const char *ua, int msec) if (!g_initialized) return; - cmdarg = (struct cache_arg *)malloc(sizeof(struct cache_arg)); + cmdarg = calloc(1, sizeof(struct cache_arg)); if (!cmdarg) { DPRINTF(E_LOG, L_CACHE, "Could not allocate cache_arg\n"); return; } - memset(cmdarg, 0, sizeof(struct cache_arg)); - cmdarg->query = strdup(query); cmdarg->ua = strdup(ua); cmdarg->msec = msec; @@ -1422,15 +1378,13 @@ cache_artwork_ping(const char *path, time_t mtime, int del) if (!g_initialized) return; - cmdarg = (struct cache_arg *)malloc(sizeof(struct cache_arg)); + cmdarg = calloc(1, sizeof(struct cache_arg)); if (!cmdarg) { DPRINTF(E_LOG, L_CACHE, "Could not allocate cache_arg\n"); return; } - memset(cmdarg, 0, sizeof(struct cache_arg)); - cmdarg->path = strdup(path); cmdarg->mtime = mtime; cmdarg->del = del; @@ -1629,8 +1583,8 @@ cache_init(void) goto evbase_fail; } - g_cacheev = evtimer_new(evbase_cache, cache_daap_update_cb, NULL); - if (!g_cacheev) + cache_daap_updateev = evtimer_new(evbase_cache, cache_daap_update_cb, NULL); + if (!cache_daap_updateev) { DPRINTF(E_LOG, L_CACHE, "Could not create cache event\n"); goto evnew_fail; @@ -1638,6 +1592,13 @@ cache_init(void) cmdbase = commands_base_new(evbase_cache, NULL); + ret = listener_add(cache_daap_listener_cb, LISTENER_DATABASE); + if (ret < 0) + { + DPRINTF(E_LOG, L_CACHE, "Could not create listener event\n"); + goto listener_fail; + } + DPRINTF(E_INFO, L_CACHE, "cache thread init\n"); ret = pthread_create(&tid_cache, NULL, cache, NULL); @@ -1657,6 +1618,8 @@ cache_init(void) return 0; thread_fail: + listener_remove(cache_daap_listener_cb); + listener_fail: commands_base_free(cmdbase); evnew_fail: event_base_free(evbase_cache); @@ -1675,6 +1638,9 @@ cache_deinit(void) return; g_initialized = 0; + + listener_remove(cache_daap_listener_cb); + commands_base_destroy(cmdbase); ret = pthread_join(tid_cache, NULL); diff --git a/src/cache.h b/src/cache.h index 7e4e112f..69acf4d0 100644 --- a/src/cache.h +++ b/src/cache.h @@ -6,9 +6,6 @@ /* ---------------------------- DAAP cache API --------------------------- */ -void -cache_daap_trigger(void); - void cache_daap_suspend(void); diff --git a/src/db.c b/src/db.c index 336d362c..7ce9db3f 100644 --- a/src/db.c +++ b/src/db.c @@ -41,6 +41,7 @@ #include "logger.h" #include "cache.h" #include "listener.h" +#include "filescanner.h" #include "misc.h" #include "db.h" #include "db_init.h" @@ -1446,8 +1447,15 @@ db_query_end(struct query_params *qp) qp->stmt = NULL; } +/* + * Utility function for running write queries (INSERT, UPDATE, DELETE). If you + * set free to non-zero, the function will free the query. If you set + * library_update to non-zero it means that the update was not just of some + * internal value (like a timestamp), but of something that requires clients + * to update their cache of the library (and of course also of our own cache). + */ static int -db_query_run(char *query, int free, int cache_update) +db_query_run(char *query, int free, int library_update) { char *errmsg; int ret; @@ -1473,10 +1481,10 @@ db_query_run(char *query, int free, int cache_update) if (free) sqlite3_free(query); - if (cache_update) - cache_daap_trigger(); - else - cache_daap_resume(); + cache_daap_resume(); + + if (library_update) + library_update_trigger(); return ((ret != SQLITE_OK) ? -1 : 0); } @@ -2404,7 +2412,7 @@ db_file_add(struct media_file_info *mfi) sqlite3_free(query); - cache_daap_trigger(); + library_update_trigger(); return 0; @@ -2483,7 +2491,7 @@ db_file_update(struct media_file_info *mfi) sqlite3_free(query); - cache_daap_trigger(); + library_update_trigger(); return 0; diff --git a/src/library/filescanner.c b/src/library/filescanner.c index f3e5e030..465345f1 100644 --- a/src/library/filescanner.c +++ b/src/library/filescanner.c @@ -61,6 +61,7 @@ #include "player.h" #include "cache.h" #include "artwork.h" +#include "listener.h" #include "commands.h" #include "library.h" @@ -107,13 +108,19 @@ struct stacked_dir { static int inofd; static struct event *inoev; +static struct event *updateev; static struct deferred_pl *playlists; static struct stacked_dir *dirstack; +// After being told by db that the library was updated through update_trigger(), +// wait 60 seconds before notifying listeners of LISTENER_DATABASE. This is to +// avoid bombarding the listeners while there are many db updates, and to make +// sure they only get a single update (useful for the cache). +static struct timeval library_update_wait = { 60, 0 }; + /* From library.c */ extern struct event_base *evbase_lib; - #ifndef __linux__ struct deferred_file { @@ -841,6 +848,8 @@ bulk_scan(int flags) { DPRINTF(E_LOG, L_SCAN, "Bulk library scan completed in %.f sec\n", difftime(end, start)); } + + listener_notify(LISTENER_DATABASE); // TODO Move to library.c } static int @@ -1469,6 +1478,21 @@ filescanner_initscan() return 0; } +static void +update_trigger_cb(int fd, short what, void *arg) +{ + listener_notify(LISTENER_DATABASE); +} + +static enum command_state +update_trigger(void *arg, int *retval) +{ + evtimer_add(updateev, &library_update_wait); + + *retval = 0; + return COMMAND_END; +} + static int filescanner_rescan() { @@ -1494,6 +1518,16 @@ filescanner_fullrescan() return 0; } +// TODO Move to abstraction +void +library_update_trigger(void) +{ + if (scanning) + return; + + commands_exec_async(cmdbase, update_trigger, NULL); +} + /* Thread: main */ static int filescanner_init(void) @@ -1501,8 +1535,20 @@ filescanner_init(void) int ret; ret = inofd_event_set(); + if (ret < 0) + { + return -1; + } - return ret; + updateev = evtimer_new(evbase_lib, update_trigger_cb, NULL); + if (!updateev) + { + DPRINTF(E_FATAL, L_SCAN, "Could not create library update event\n"); + close(inofd); + return -1; + } + + return 0; } /* Thread: main */ diff --git a/src/library/filescanner.h b/src/library/filescanner.h index 1acdf61e..c15c655c 100644 --- a/src/library/filescanner.h +++ b/src/library/filescanner.h @@ -31,4 +31,7 @@ void scan_itunes_itml(char *file); #endif +void +library_update_trigger(void); // TODO Move to library abstraction + #endif /* !__FILESCANNER_H__ */ From 12584812029fa633b362400bc558f4b73296c822 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Sun, 8 Jan 2017 00:24:40 +0100 Subject: [PATCH 07/37] [listener] Rename LISTENER_PLAYLIST to LISTENER_QUEUE --- src/db.c | 4 ++-- src/listener.h | 4 ++-- src/mpd.c | 8 ++++---- src/player.c | 21 ++++++++++++++++++--- 4 files changed, 26 insertions(+), 11 deletions(-) diff --git a/src/db.c b/src/db.c index 7ce9db3f..e747ff02 100644 --- a/src/db.c +++ b/src/db.c @@ -4006,7 +4006,7 @@ db_queue_get_version() } /* - * Increments the version of the queue in the admin table and notifies listener of LISTENER_PLAYLIST + * Increments the version of the queue in the admin table and notifies listener of LISTENER_QUEUE * about the change. * * This function must be called after successfully modifying the queue table in order to send @@ -4044,7 +4044,7 @@ queue_inc_version_and_notify() db_transaction_end(); - listener_notify(LISTENER_PLAYLIST); + listener_notify(LISTENER_QUEUE); } void diff --git a/src/listener.h b/src/listener.h index 8b493552..61421077 100644 --- a/src/listener.h +++ b/src/listener.h @@ -6,8 +6,8 @@ enum listener_event_type { /* The player has been started, stopped or seeked */ LISTENER_PLAYER = (1 << 0), - /* The current playlist has been modified */ - LISTENER_PLAYLIST = (1 << 1), + /* The current playback queue has been modified */ + LISTENER_QUEUE = (1 << 1), /* The volume has been changed */ LISTENER_VOLUME = (1 << 2), /* A speaker has been enabled or disabled */ diff --git a/src/mpd.c b/src/mpd.c index ad6242d8..0e8c11bd 100644 --- a/src/mpd.c +++ b/src/mpd.c @@ -596,7 +596,7 @@ mpd_command_idle(struct evbuffer *evbuf, int argc, char **argv, char **errmsg) } else if (0 == strcmp(argv[i], "playlist")) { - client->events |= LISTENER_PLAYLIST; + client->events |= LISTENER_QUEUE; } else if (0 == strcmp(argv[i], "mixer")) { @@ -617,7 +617,7 @@ mpd_command_idle(struct evbuffer *evbuf, int argc, char **argv, char **errmsg) } } else - client->events = LISTENER_PLAYER | LISTENER_PLAYLIST | LISTENER_VOLUME | LISTENER_SPEAKER | LISTENER_OPTIONS; + client->events = LISTENER_PLAYER | LISTENER_QUEUE | LISTENER_VOLUME | LISTENER_SPEAKER | LISTENER_OPTIONS; idle_clients = client; @@ -4544,7 +4544,7 @@ mpd_notify_idle_client(struct idle_client *client, enum listener_event_type type evbuffer_add(client->evbuffer, "changed: player\n", 16); break; - case LISTENER_PLAYLIST: + case LISTENER_QUEUE: evbuffer_add(client->evbuffer, "changed: playlist\n", 18); break; @@ -4871,7 +4871,7 @@ int mpd_init(void) #endif idle_clients = NULL; - listener_add(mpd_listener_cb, LISTENER_PLAYER | LISTENER_PLAYLIST | LISTENER_VOLUME | LISTENER_SPEAKER | LISTENER_OPTIONS); + listener_add(mpd_listener_cb, LISTENER_PLAYER | LISTENER_QUEUE | LISTENER_VOLUME | LISTENER_SPEAKER | LISTENER_OPTIONS); return 0; diff --git a/src/player.c b/src/player.c index 19f44304..c8827fdb 100644 --- a/src/player.c +++ b/src/player.c @@ -27,10 +27,25 @@ * - notify about playback status changes * - maintain the playback queue * - * The player thread should *never* be making operations that may block, since + * The player thread should never be making operations that may block, since * that could block callers requesting status (effectively making forked-daapd - * unresponsive) and it could also starve the outputs. + * unresponsive) and it could also starve the outputs. In practice this rule is + * not always obeyed, for instance some outputs do their setup in ways that + * could block. * + * + * About metadata + * -------------- + * The player gets metadata from library + inputs and passes it to the outputs + * and other clients (e.g. Remotes). Text metadata is handled differently than + * artwork. Here's how text works: + * + * 1. On playback start, the player will TODO + * 2. During playback, the input may signal new metadata by making a + * input_write() with the INPUT_FLAG_METADATA flag. When the player read + * reaches that data, the player will request the metadata from the input + * with input_metadata_get(). + * 3. If the new metadata is different than the TODO */ #ifdef HAVE_CONFIG_H @@ -2796,7 +2811,7 @@ playerqueue_clear_history(void *arg, int *retval) cur_plversion++; // TODO [db_queue] need to update db queue version - listener_notify(LISTENER_PLAYLIST); + listener_notify(LISTENER_QUEUE); *retval = 0; return COMMAND_END; From 938e197fa45cd20d6b889fac8632c44990f2d7f0 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Sat, 14 Jan 2017 00:43:03 +0100 Subject: [PATCH 08/37] [player] Refactor read/write - remove read skip which is obsolete when input has own thread and cannot block - simplify code - fix while loop that could loop infinitely --- src/input.c | 9 +- src/input.h | 14 ++- src/player.c | 322 +++++++++++++++++---------------------------------- 3 files changed, 122 insertions(+), 223 deletions(-) diff --git a/src/input.c b/src/input.c index f037ccce..41a2e5d3 100644 --- a/src/input.c +++ b/src/input.c @@ -250,7 +250,7 @@ input_write(struct evbuffer *evbuf, short flags) /* Thread: player */ int -input_read(struct evbuffer *evbuf, size_t want, short *flags) +input_read(void *data, size_t size, short *flags) { int len; @@ -265,7 +265,7 @@ input_read(struct evbuffer *evbuf, size_t want, short *flags) pthread_mutex_lock(&input_buffer.mutex); #ifdef DEBUG - debug_elapsed += want; + debug_elapsed += size; if (debug_elapsed > STOB(441000)) // 10 sec { DPRINTF(E_DBG, L_PLAYER, "Input buffer has %zu bytes\n", evbuffer_get_length(input_buffer.evbuf)); @@ -273,7 +273,7 @@ input_read(struct evbuffer *evbuf, size_t want, short *flags) } #endif - len = evbuffer_remove_buffer(input_buffer.evbuf, evbuf, want); + len = evbuffer_remove(input_buffer.evbuf, data, size); if (len < 0) { DPRINTF(E_LOG, L_PLAYER, "Error reading stream data from input buffer\n"); @@ -492,6 +492,9 @@ input_deinit(void) inputs[i]->deinit(); } + pthread_cond_destroy(&input_buffer.cond); + pthread_mutex_destroy(&input_buffer.mutex); + evbuffer_free(input_buffer.evbuf); } diff --git a/src/input.h b/src/input.h index 3190d864..b63f7596 100644 --- a/src/input.h +++ b/src/input.h @@ -131,13 +131,13 @@ input_wait(void); * Move a chunk of stream data from the player's input buffer to an output * buffer. Should only be called by the player thread. Will not block. * - * @in evbuf Output buffer - * @in want How much data to move to the output buffer + * @in data Output buffer + * @in size How much data to move to the output buffer * @out flags Flags INPUT_FLAG_EOF or INPUT_FLAG_METADATA - * @return Number of bytes moved + * @return Number of bytes moved, -1 on error */ int -input_read(struct evbuffer *evbuf, size_t want, short *flags); +input_read(void *data, size_t size, short *flags); /* * Initializes the given player source for playback @@ -180,9 +180,15 @@ input_seek(struct player_source *ps, int seek_ms); void input_flush(short *flags); +/* + * Called by player_init (so will run in main thread) + */ int input_init(void); +/* + * Called by player_deinit (so will run in main thread) + */ void input_deinit(void); diff --git a/src/player.c b/src/player.c index c8827fdb..11ad681e 100644 --- a/src/player.c +++ b/src/player.c @@ -107,8 +107,10 @@ // Default volume (must be from 0 - 100) #define PLAYER_DEFAULT_VOLUME 50 -// Used to keep the player from getting ahead of a rate limited source (see below) -#define PLAYER_TICKS_MAX_OVERRUN 2 +// For every tick, we will read a packet from the input buffer and write it to +// the outputs. If the input is empty, we will try to catch up next tick. We +// will continue trying that up to this max, after which we abort playback. +#define PLAYER_WRITES_PENDING_MAX 126 struct volume_param { int volume; @@ -192,8 +194,9 @@ static struct timespec tick_interval; static struct timespec timer_res; // Time between two packets static struct timespec packet_time = { 0, AIRTUNES_V2_STREAM_PERIOD }; -// Will be positive if we need to skip some source reads (see below) -static int ticks_skip; + +// How many writes we owe the output (when the input is underrunning) +static int pb_writes_pending; /* Sync values */ static struct timespec pb_pos_stamp; @@ -217,9 +220,9 @@ static struct player_source *cur_streaming; static uint32_t cur_plid; static uint32_t cur_plversion; -static struct evbuffer *audio_buf; -static uint8_t rawbuf[STOB(AIRTUNES_V2_PACKET_SAMPLES)]; - +/* Player buffer (holds one packet) */ +static uint8_t pb_buffer[STOB(AIRTUNES_V2_PACKET_SAMPLES)]; +static size_t pb_buffer_offset; /* Play history */ static struct player_history *history; @@ -589,59 +592,6 @@ history_add(uint32_t id, uint32_t item_id) /* Audio sources */ -/* - * Read up to "len" data from the given player source and returns - * the actual amount of data read. - */ -/*static int -stream_read(struct player_source *ps, int len) -{ - int icy_timer; - int ret; - - if (!ps) - { - DPRINTF(E_LOG, L_PLAYER, "Stream read called with no active streaming player source\n"); - return -1; - } - - if (!ps->setup_done) - { - DPRINTF(E_LOG, L_PLAYER, "Given player source not setup for reading data\n"); - return -1; - } - - // Read up to len data depending on data kind - switch (ps->data_kind) - { - case DATA_KIND_HTTP: - ret = transcode(audio_buf, len, ps->xcode, &icy_timer); - - if (icy_timer) - metadata_check_icy(); - break; - - case DATA_KIND_FILE: - ret = transcode(audio_buf, len, ps->xcode, &icy_timer); - break; - -#ifdef HAVE_SPOTIFY_H - case DATA_KIND_SPOTIFY: - ret = spotify_audio_get(audio_buf, len); - break; -#endif - - case DATA_KIND_PIPE: - ret = pipe_audio_get(audio_buf, len); - break; - - default: - ret = -1; - } - - return ret; -}*/ - static struct player_source * source_now_playing() { @@ -829,9 +779,6 @@ source_play() ret = input_start(cur_streaming); - ticks_skip = 0; - memset(rawbuf, 0, sizeof(rawbuf)); - return ret; } @@ -1076,91 +1023,80 @@ source_prev() } static int -source_read(uint8_t *buf, int len, uint64_t rtptime) +source_switch(int nbytes) { - int ret; - int nbytes; - short flags; - char *silence_buf; struct player_source *ps; + int ret; + + DPRINTF(E_DBG, L_PLAYER, "Switching track\n"); + + source_close(last_rtptime + AIRTUNES_V2_PACKET_SAMPLES + BTOS(nbytes) - 1); + + ps = source_next(); + if (!ps) + { + cur_streaming = NULL; + return 0; // End of queue + } + + ret = source_open(ps, cur_streaming->end + 1, 0); + if (ret < 0) + return -1; + + ret = source_play(); + if (ret < 0) + return -1; + + metadata_trigger(0); + + return 0; +} + +// Returns -1 on error (caller should abort playback), or bytes read (possibly 0) +static int +source_read(uint8_t *buf, int len) +{ + int nbytes; + int ret; + short flags; if (!cur_streaming) - return 0; + return -1; - nbytes = 0; - while (nbytes < len) + nbytes = input_read(buf, len, &flags); + if (nbytes < 0) { - if (evbuffer_get_length(audio_buf) == 0) - { - flags = 0; - if (cur_streaming) - { - ret = input_read(audio_buf, len - nbytes, &flags); - } - else if (cur_playing) - { - // Reached end of playlist (cur_playing is NULL) send silence and source_check will abort playback if the last item was played - DPRINTF(E_SPAM, L_PLAYER, "End of playlist reached, stream silence until playback of last item ends\n"); - silence_buf = (char *)calloc((len - nbytes), sizeof(char)); - evbuffer_add(audio_buf, silence_buf, (len - nbytes)); - free(silence_buf); - ret = len - nbytes; - } - else - { - // If cur_streaming and cur_playing are NULL, source_read for all queue items failed. Playback will be aborted in the calling function - return -1; - } + DPRINTF(E_LOG, L_PLAYER, "Error reading source %d\n", cur_streaming->id); - if (ret == 0) - sleep(1); // TODO Underrun -> proper pause - else if ((ret < 0) || (flags & INPUT_FLAG_EOF)) - { - source_close(rtptime + BTOS(nbytes) - 1); + nbytes = 0; + ret = source_switch(0); + db_queue_delete_byitemid(cur_streaming->item_id); + if (ret < 0) + return -1; + } + else if (flags & INPUT_FLAG_EOF) + { + ret = source_switch(nbytes); + if (ret < 0) + return -1; + } - DPRINTF(E_DBG, L_PLAYER, "New file\n"); - - ps = source_next(); - - if (ret < 0) - { - DPRINTF(E_LOG, L_PLAYER, "Error reading source %d\n", cur_streaming->id); - db_queue_delete_byitemid(cur_streaming->item_id); - } - - if (ps) - { - ret = source_open(ps, cur_streaming->end + 1, 0); - if (ret < 0) - { - source_free(ps); - return -1; - } - - ret = source_play(); - if (ret < 0) - return -1; - - metadata_trigger(0); - } - else - { - cur_streaming = NULL; - } - continue; - } - } - - nbytes += evbuffer_remove(audio_buf, buf + nbytes, len - nbytes); + // We pad the output buffer with silence if we don't have enough data for a + // full packet and there is no more data coming up (no more tracks in queue) + if ((nbytes < len) && (!cur_streaming)) + { + memset(buf + nbytes, 0, len - nbytes); + nbytes = len; } return nbytes; } static void -playback_write(int read_skip) +playback_write(void) { - int ret; + int want; + int got; source_check(); @@ -1168,23 +1104,31 @@ playback_write(int read_skip) if (player_state == PLAY_STOPPED) return; - last_rtptime += AIRTUNES_V2_PACKET_SAMPLES; - - if (!read_skip) + pb_writes_pending++; + while (pb_writes_pending) { - ret = source_read(rawbuf, sizeof(rawbuf), last_rtptime); - if (ret < 0) + want = sizeof(pb_buffer) - pb_buffer_offset; + got = source_read(pb_buffer + pb_buffer_offset, want); + if (got == want) { - DPRINTF(E_DBG, L_PLAYER, "Error reading from source, aborting playback\n"); - + last_rtptime += AIRTUNES_V2_PACKET_SAMPLES; + outputs_write(pb_buffer, last_rtptime); + pb_buffer_offset = 0; + pb_writes_pending--; + } + else if ((got < 0) || (pb_writes_pending > PLAYER_WRITES_PENDING_MAX)) + { + DPRINTF(E_LOG, L_PLAYER, "Error reading from source, aborting playback (%d)\n", pb_writes_pending); playback_abort(); return; } + else + { + DPRINTF(E_DBG, L_PLAYER, "Partial read (offset=%zu, pending=%d)\n", pb_buffer_offset, pb_writes_pending); + pb_buffer_offset += got; + return; + } } - else - DPRINTF(E_SPAM, L_PLAYER, "Skipping read\n"); - - outputs_write(rawbuf, last_rtptime); } static void @@ -1193,8 +1137,6 @@ player_playback_cb(int fd, short what, void *arg) struct timespec next_tick; uint64_t overrun; int ret; - int skip; - int skip_first; // Check if we missed any timer expirations overrun = 0; @@ -1212,55 +1154,16 @@ player_playback_cb(int fd, short what, void *arg) overrun = ret; #endif /* HAVE_TIMERFD */ - // The reason we get behind the playback timer may be that we are playing a - // network stream OR that the source is slow to open OR some interruption. - // For streams, we might be consuming faster than the stream delivers, so - // when ffmpeg's buffer empties (might take a few hours) our av_read_frame() - // in transcode.c will begin to block, because ffmpeg has to wait for new data - // from the stream server. - // - // Our strategy to catch up with the timer depends on the source: - // - streams: We will skip reading data every second until we have countered - // the overrun by skipping reads for a number of ticks that is - // 3 times the overrun. That should make the source catch up. To - // keep the output happy we resend the previous rawbuf when we - // have skipped a read. - // - files: Just read and write like crazy until we have caught up. - - skip_first = 0; - if (overrun > PLAYER_TICKS_MAX_OVERRUN) - { - DPRINTF(E_WARN, L_PLAYER, "Behind the playback timer with %" PRIu64 " ticks\n", overrun); - - if (cur_streaming && (cur_streaming->data_kind == DATA_KIND_HTTP || cur_streaming->data_kind == DATA_KIND_PIPE)) - { - ticks_skip = 3 * overrun; - - DPRINTF(E_WARN, L_PLAYER, "Will skip reading for a total of %d ticks to catch up\n", ticks_skip); - - // We always skip after a timer overrun, since another read will - // probably just give another time overrun - skip_first = 1; - } - else - ticks_skip = 0; - } - - // Decide how many packets to send + // If there was an overrun, we will try to read/write a corresponding number + // of times so we catch up. The read from the input is non-blocking, so it + // should not bring us further behind, even if there is no data. next_tick = timespec_add(pb_timer_last, tick_interval); for (; overrun > 0; overrun--) next_tick = timespec_add(next_tick, tick_interval); do { - skip = skip_first || ((ticks_skip > 0) && ((last_rtptime / AIRTUNES_V2_PACKET_SAMPLES) % 126 == 0)); - - playback_write(skip); - - skip_first = 0; - if (skip) - ticks_skip--; - + playback_write(); packet_timer_last = timespec_add(packet_timer_last, packet_time); } while ((timespec_cmp(packet_timer_last, next_tick) < 0) && (player_state == PLAY_PLAYING)); @@ -1789,8 +1692,6 @@ playback_abort(void) source_stop(); - evbuffer_drain(audio_buf, evbuffer_get_length(audio_buf)); - if (!clear_queue_on_stop_disabled) db_queue_clear(); @@ -1963,8 +1864,6 @@ playback_stop(void *arg, int *retval) source_stop(); - evbuffer_drain(audio_buf, evbuffer_get_length(audio_buf)); - status_update(PLAY_STOPPED); metadata_purge(); @@ -2009,6 +1908,9 @@ playback_start_bh(void *arg, int *retval) pb_timer_last.tv_sec = pb_pos_stamp.tv_sec; pb_timer_last.tv_nsec = pb_pos_stamp.tv_nsec; + pb_buffer_offset = 0; + pb_writes_pending = 0; + ret = pb_timer_start(); if (ret < 0) goto out_fail; @@ -2406,8 +2308,6 @@ playback_pause(void *arg, int *retval) source_pause(pos); - evbuffer_drain(audio_buf, evbuffer_get_length(audio_buf)); - metadata_purge(); /* We're async if we need to flush devices */ @@ -3244,14 +3144,6 @@ player_init(void) gcry_randomize(&rnd, sizeof(rnd), GCRY_STRONG_RANDOM); last_rtptime = ((uint64_t)1 << 32) | rnd; - audio_buf = evbuffer_new(); - if (!audio_buf) - { - DPRINTF(E_LOG, L_PLAYER, "Could not allocate evbuffer for audio buffer\n"); - - goto audio_fail; - } - evbase_player = event_base_new(); if (!evbase_player) { @@ -3310,8 +3202,6 @@ player_init(void) evnew_fail: event_base_free(evbase_player); evbase_fail: - evbuffer_free(audio_buf); - audio_fail: #ifdef HAVE_TIMERFD close(pb_timer_fd); #else @@ -3327,6 +3217,14 @@ player_deinit(void) { int ret; + player_playback_stop(); + +#ifdef HAVE_TIMERFD + close(pb_timer_fd); +#else + timer_delete(pb_timer); +#endif + player_exit = 1; commands_base_destroy(cmdbase); @@ -3338,19 +3236,11 @@ player_deinit(void) return; } - free(history); - - pb_timer_stop(); -#ifdef HAVE_TIMERFD - close(pb_timer_fd); -#else - timer_delete(pb_timer); -#endif - - evbuffer_free(audio_buf); - input_deinit(); + outputs_deinit(); + free(history); + event_base_free(evbase_player); } From 9aede45a122568135aa2d35d87b4667c621220f4 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Sat, 14 Jan 2017 23:35:19 +0100 Subject: [PATCH 09/37] [pipe] Add a pipe input with autostart capabilities --- forked-daapd.conf.in | 10 +- src/Makefile.am | 3 +- src/conffile.c | 3 +- src/input.c | 5 + src/input.h | 1 + src/inputs/pipe.c | 467 +++++++++++++++++++++++++++++++++++++++++++ src/pipe.c | 129 ------------ src/pipe.h | 16 -- src/player.c | 12 +- 9 files changed, 493 insertions(+), 153 deletions(-) create mode 100644 src/inputs/pipe.c delete mode 100644 src/pipe.c delete mode 100644 src/pipe.h diff --git a/forked-daapd.conf.in b/forked-daapd.conf.in index ca4ee9b3..d3bb93e6 100644 --- a/forked-daapd.conf.in +++ b/forked-daapd.conf.in @@ -116,8 +116,9 @@ library { # File types the scanner should ignore # Non-audio files will never be added to the database, but here you # can prevent the scanner from even probing them. This might improve - # scan time. By default .db, .ini, .db-journal and .pdf are ignored. -# filetypes_ignore = { ".db", ".ini", ".db-journal", ".pdf" } + # scan time. By default .db, .ini, .db-journal, .pdf and .metadata are + # ignored. +# filetypes_ignore = { ".db", ".ini", ".db-journal", ".pdf", ".metadata" } # File paths the scanner should ignore # If you want to exclude files on a more advanced basis you can enter @@ -153,6 +154,11 @@ library { # no_decode = { "format", "format" } # Formats that should always be decoded # force_decode = { "format", "format" } + + # Watch named pipes in the library for data and autostart playback when + # there is data to be read. To exclude specific pipes from watching, + # consider using the above _ignore options. +# pipe_autostart = true } # Local audio output diff --git a/src/Makefile.am b/src/Makefile.am index ef5b1498..1906b50f 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -100,7 +100,6 @@ forked_daapd_SOURCES = main.c \ http.c http.h \ dmap_common.c dmap_common.h \ transcode.c transcode.h \ - pipe.c pipe.h \ artwork.c artwork.h \ misc.c misc.h \ rng.c rng.h \ @@ -109,7 +108,7 @@ forked_daapd_SOURCES = main.c \ player.c player.h \ worker.c worker.h \ input.h input.c \ - inputs/file_http.c \ + inputs/file_http.c inputs/pipe.c \ outputs.h outputs.c \ outputs/raop.c outputs/streaming.c outputs/dummy.c outputs/fifo.c \ $(ALSA_SRC) $(PULSEAUDIO_SRC) $(CHROMECAST_SRC) \ diff --git a/src/conffile.c b/src/conffile.c index 8d01de03..cd46b632 100644 --- a/src/conffile.c +++ b/src/conffile.c @@ -82,13 +82,14 @@ static cfg_opt_t sec_library[] = CFG_STR("name_radio", "Radio", CFGF_NONE), CFG_STR_LIST("artwork_basenames", "{artwork,cover,Folder}", CFGF_NONE), CFG_BOOL("artwork_individual", cfg_false, CFGF_NONE), - CFG_STR_LIST("filetypes_ignore", "{.db,.ini,.db-journal,.pdf}", CFGF_NONE), + CFG_STR_LIST("filetypes_ignore", "{.db,.ini,.db-journal,.pdf,.metadata}", CFGF_NONE), CFG_STR_LIST("filepath_ignore", NULL, CFGF_NONE), CFG_BOOL("filescan_disable", cfg_false, CFGF_NONE), CFG_BOOL("itunes_overrides", cfg_false, CFGF_NONE), CFG_BOOL("itunes_smartpl", cfg_false, CFGF_NONE), CFG_STR_LIST("no_decode", NULL, CFGF_NONE), CFG_STR_LIST("force_decode", NULL, CFGF_NONE), + CFG_BOOL("pipe_autostart", cfg_true, CFGF_NONE), CFG_END() }; diff --git a/src/input.c b/src/input.c index 41a2e5d3..0aeaeb4f 100644 --- a/src/input.c +++ b/src/input.c @@ -46,6 +46,7 @@ extern struct input_definition input_file; extern struct input_definition input_http; +extern struct input_definition input_pipe; #ifdef HAVE_SPOTIFY_H extern struct input_definition input_spotify; #endif @@ -54,6 +55,7 @@ extern struct input_definition input_spotify; static struct input_definition *inputs[] = { &input_file, &input_http, + &input_pipe, #ifdef HAVE_SPOTIFY_H &input_spotify, #endif @@ -130,6 +132,9 @@ map_data_kind(int data_kind) case DATA_KIND_HTTP: return INPUT_TYPE_HTTP; + case DATA_KIND_PIPE: + return INPUT_TYPE_PIPE; + #ifdef HAVE_SPOTIFY_H case DATA_KIND_SPOTIFY: return INPUT_TYPE_SPOTIFY; diff --git a/src/input.h b/src/input.h index b63f7596..7646d78e 100644 --- a/src/input.h +++ b/src/input.h @@ -13,6 +13,7 @@ enum input_types { INPUT_TYPE_FILE, INPUT_TYPE_HTTP, + INPUT_TYPE_PIPE, #ifdef HAVE_SPOTIFY_H INPUT_TYPE_SPOTIFY, #endif diff --git a/src/inputs/pipe.c b/src/inputs/pipe.c new file mode 100644 index 00000000..a2c8c90f --- /dev/null +++ b/src/inputs/pipe.c @@ -0,0 +1,467 @@ +/* + * 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 + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "misc.h" +#include "logger.h" +#include "db.h" +#include "conffile.h" +#include "listener.h" +#include "player.h" +#include "input.h" + +// Maximum number of pipes to watch for data +#define PIPE_MAX_OPEN 4 +// Max number of bytes to read from a pipe at a time ( +#define PIPE_READ_MAX 65536 + +// filescanner event base, from filescanner.c +// TODO don't use filescanner thread/base +extern struct event_base *evbase_scan; + +enum pipestate +{ + PIPE_NEW = (1 << 0), + PIPE_DEL = (1 << 1), + PIPE_OPEN = (1 << 2), +}; + +struct pipe +{ + int id; + int fd; + char *path; + enum pipestate state; + struct event *ev; + // 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; + + +/* -------------------------------- HELPERS ------------------------------- */ + +static struct pipe * +pipe_new(const char *path, int id) +{ + struct pipe *pipe; + + pipe = calloc(1, sizeof(struct pipe)); + pipe->path = strdup(path); + pipe->id = id; + pipe->fd = -1; + pipe->state = PIPE_NEW; + pipe->next = pipelist; + pipelist = pipe; + + return pipe; +} + +static void +pipe_free(struct pipe *pipe) +{ + free(pipe->path); + free(pipe); +} + +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; +} + + +/* ----------------------------- PIPE WATCHING ---------------------------- */ +/* Thread: filescanner */ + +// 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; + } + + pipe = pipe_new(dbmfi.path, id); + } + + db_query_end(&qp); + + return count; +} + +// 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; + + DPRINTF(E_DBG, L_PLAYER, "Autostarting pipe %d, %d\n", (int) fd, (int)event); + + ret = player_get_status(&status); + if ((ret < 0) || (status.status == PLAY_PLAYING)) + return; + + 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); + + free_queue_item(queue_item, 0); +} + +/* Opens a pipe and starts watching it for data */ +static int +pipe_open(struct pipe *pipe) +{ + struct stat sb; + + DPRINTF(E_DBG, L_PLAYER, "(Re)opening pipe: '%s'\n", pipe->path); + + if (lstat(pipe->path, &sb) < 0) + { + DPRINTF(E_LOG, L_PLAYER, "Could not lstat() '%s': %s\n", pipe->path, strerror(errno)); + return -1; + } + + if (!S_ISFIFO(sb.st_mode)) + { + DPRINTF(E_LOG, L_PLAYER, "Source type is pipe, but path is not a fifo: %s\n", pipe->path); + return -1; + } + + pipe->fd = open(pipe->path, O_RDONLY | O_NONBLOCK); + if (pipe->fd < 0) + { + DPRINTF(E_LOG, L_PLAYER, "Could not open pipe for reading '%s': %s\n", pipe->path, strerror(errno)); + return -1; + } + + pipe->state = PIPE_OPEN; + + if (!pipe_autostart) + return 0; // All done + + pipe->ev = event_new(evbase_scan, pipe->fd, EV_READ, pipe_read_cb, pipe); + if (!pipe->ev) + { + DPRINTF(E_LOG, L_PLAYER, "Could not watch pipe for new data '%s'\n", pipe->path); + return -1; + } + + event_add(pipe->ev, NULL); + + return 0; +} + +static void +pipe_close(struct pipe *pipe) +{ + if (pipe->fd < 0) + return; + + if (pipe->ev) + event_free(pipe->ev); + + close(pipe->fd); + + pipe->fd = -1; +} + +static void +pipe_remove(struct pipe *pipe) +{ + struct pipe *p; + + pipe_close(pipe); + + if (pipe == pipelist) + pipelist = pipe->next; + else + { + for (p = pipelist; p && (p->next != pipe); p = p->next) + ; /* EMPTY */ + + if (!p) + { + DPRINTF(E_LOG, L_REMOTE, "WARNING: pipe not found in list; BUG!\n"); + return; + } + + p->next = pipe->next; + } + + pipe_free(pipe); +} + +static void +pipe_listener_cb(enum listener_event_type type) +{ + struct pipe *pipe; + struct pipe *next; + int count; + + count = pipe_enum(); // Count does not include pipes with state PIPE_DEL + if (count < 0) + return; + + for (pipe = pipelist; pipe; pipe = next) + { + next = pipe->next; + + DPRINTF(E_DBG, L_PLAYER, "Processing pipe '%s', state is %d\n", pipe->path, pipe->state); + + if ((pipe->state == PIPE_NEW) && (count > PIPE_MAX_OPEN)) + DPRINTF(E_LOG, L_PLAYER, "Max open pipes reached, will not watch %s\n", pipe->path); + else if (pipe->state == PIPE_NEW) + pipe_open(pipe); + else if (pipe->state == PIPE_DEL) + pipe_remove(pipe); // Note: Will free pipe + } +} + + +/* -------------------------- PIPE INPUT INTERFACE ------------------------ */ +/* Thread: player/input */ + +static int +pipe_metadata_open(struct pipe *pipe) +{ +// char md_pipe_path[PATH_MAX]; //TODO PATH_MAX - limits.h? +/* snprintf(md_pipe_path, sizeof(md_pipe_path), "%s.metadata", pipe->path); + + fd = pipe_open(md_pipe_path); // TODO avoid lstat error + if (fd < 0) +*/ + return 0; +} + +static int +setup(struct player_source *ps) +{ + struct pipe *pipe; + int ret; + + if (pipe_autostart) + { + pipe = pipe_find(ps->id); + if (!pipe) + { + DPRINTF(E_LOG, L_PLAYER, "Unknown pipe '%s'\n", ps->path); + return -1; + } + } + else + pipe = pipe_new(ps->path, ps->id); + + if (pipe->state != PIPE_OPEN) + { + ret = pipe_open(pipe); + if (ret < 0) + return -1; + } + + pipe_metadata_open(pipe); + + if (pipe->ev) + event_del(pipe->ev); // Avoids autostarting pipe if manually started by user + + ps->setup_done = 1; + + return 0; +} + +static int +start(struct player_source *ps) +{ + struct pipe *pipe; + struct evbuffer *evbuf; + 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); + if ( (ret == 0) || ((ret < 0) && (errno == EAGAIN)) ) + { + input_wait(); + continue; + } + else if (ret < 0) + { + DPRINTF(E_LOG, L_PLAYER, "Could not read from pipe: %s\n", strerror(errno)); + break; + } + + ret = input_write(evbuf, 0); + 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); + if (pipe_autostart) + { + // Reopen pipe since if I just readd the event it instantly makes the + // callback (probably something I have missed...) + pipe_close(pipe); + pipe_open(pipe); + } + else + { + pipe_remove(pipe); + } + + ps->setup_done = 0; + + return 0; +} + +// 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; + pipe_remove(pipe); + } + + 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, + .init = init, + .deinit = deinit, +}; + diff --git a/src/pipe.c b/src/pipe.c deleted file mode 100644 index 3107d349..00000000 --- a/src/pipe.c +++ /dev/null @@ -1,129 +0,0 @@ -/* - * Copyright (C) 2014 Espen Jürgensen - * - * 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 - */ - -#ifdef HAVE_CONFIG_H -# include -#endif - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "pipe.h" -#include "logger.h" - -#define PIPE_BUFFER_SIZE 8192 - -static int g_fd = -1; -static uint16_t *g_buf = NULL; - -int -pipe_setup(const char *path) -{ - struct stat sb; - - if (!path) - { - DPRINTF(E_LOG, L_PLAYER, "Path to pipe is NULL\n"); - return -1; - } - - DPRINTF(E_DBG, L_PLAYER, "Setting up pipe: %s\n", path); - - if (lstat(path, &sb) < 0) - { - DPRINTF(E_LOG, L_PLAYER, "Could not lstat() '%s': %s\n", path, strerror(errno)); - return -1; - } - - if (!S_ISFIFO(sb.st_mode)) - { - DPRINTF(E_LOG, L_PLAYER, "Source type is pipe, but path is not a fifo: %s\n", path); - return -1; - } - - pipe_cleanup(); - - g_fd = open(path, O_RDONLY | O_NONBLOCK); - if (g_fd < 0) - { - DPRINTF(E_LOG, L_PLAYER, "Could not open pipe for reading '%s': %s\n", path, strerror(errno)); - return -1; - } - - g_buf = (uint16_t *)malloc(PIPE_BUFFER_SIZE); - if (!g_buf) - { - DPRINTF(E_LOG, L_PLAYER, "Out of memory for buffer\n"); - return -1; - } - - return 0; -} - -void -pipe_cleanup(void) -{ - if (g_fd >= 0) - close(g_fd); - g_fd = -1; - - if (g_buf) - free(g_buf); - g_buf = NULL; - - return; -} - -int -pipe_audio_get(struct evbuffer *evbuf, int wanted) -{ - int got; - - if (wanted > PIPE_BUFFER_SIZE) - wanted = PIPE_BUFFER_SIZE; - - got = read(g_fd, g_buf, wanted); - - if ((got < 0) && (errno != EAGAIN)) - { - DPRINTF(E_LOG, L_PLAYER, "Could not read from pipe: %s\n", strerror(errno)); - return -1; - } - - // If the other end of the pipe is not writing or the read was blocked, - // we just return silence - if (got <= 0) - { - memset(g_buf, 0, wanted); - got = wanted; - } - - evbuffer_add(evbuf, g_buf, got); - - return got; -} - diff --git a/src/pipe.h b/src/pipe.h deleted file mode 100644 index 637a2072..00000000 --- a/src/pipe.h +++ /dev/null @@ -1,16 +0,0 @@ - -#ifndef __PIPE_H__ -#define __PIPE_H__ - -#include - -int -pipe_setup(const char *path); - -void -pipe_cleanup(void); - -int -pipe_audio_get(struct evbuffer *evbuf, int wanted); - -#endif /* !__PIPE_H__ */ diff --git a/src/player.c b/src/player.c index 11ad681e..dd3f4129 100644 --- a/src/player.c +++ b/src/player.c @@ -1116,15 +1116,21 @@ playback_write(void) pb_buffer_offset = 0; pb_writes_pending--; } - else if ((got < 0) || (pb_writes_pending > PLAYER_WRITES_PENDING_MAX)) + else if (got < 0) { - DPRINTF(E_LOG, L_PLAYER, "Error reading from source, aborting playback (%d)\n", pb_writes_pending); + DPRINTF(E_LOG, L_PLAYER, "Error reading from source, aborting playback\n"); playback_abort(); return; } + else if (pb_writes_pending > PLAYER_WRITES_PENDING_MAX) + { + DPRINTF(E_LOG, L_PLAYER, "Source is not providing sufficient data, temporarily pausing playback\n"); + playback_abort(); // TODO Actually pause input + implement a resume event + return; + } else { - DPRINTF(E_DBG, L_PLAYER, "Partial read (offset=%zu, pending=%d)\n", pb_buffer_offset, pb_writes_pending); + DPRINTF(E_SPAM, L_PLAYER, "Partial read (offset=%zu, pending=%d)\n", pb_buffer_offset, pb_writes_pending); pb_buffer_offset += got; return; } From 6db4e40119dd7c3b5fed3018a81e425e822929e9 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Sun, 15 Jan 2017 23:19:57 +0100 Subject: [PATCH 10/37] [misc] Add function to add a relative time to current clock --- src/misc.c | 16 ++++++++++++++++ src/misc.h | 3 +++ 2 files changed, 19 insertions(+) diff --git a/src/misc.c b/src/misc.c index f624bd4a..769b15e2 100644 --- a/src/misc.c +++ b/src/misc.c @@ -27,6 +27,7 @@ #endif #include +#include #include #include #include @@ -928,6 +929,21 @@ timespec_cmp(struct timespec time1, struct timespec time2) return 0; } +struct timespec +timespec_reltoabs(struct timespec relative) +{ + struct timespec absolute; + +#if _POSIX_TIMERS > 0 + clock_gettime(CLOCK_REALTIME, &absolute); +#else + struct timeval tv; + gettimeofday(&tv, NULL); + TIMEVAL_TO_TIMESPEC(&tv, &absolute); +#endif + return timespec_add(absolute, relative); +} + #if defined(HAVE_MACH_CLOCK) || defined(HAVE_MACH_TIMER) #include /* mach_absolute_time */ diff --git a/src/misc.h b/src/misc.h index c358dde9..de1a089e 100644 --- a/src/misc.h +++ b/src/misc.h @@ -145,6 +145,9 @@ timespec_add(struct timespec time1, struct timespec time2); int timespec_cmp(struct timespec time1, struct timespec time2); +struct timespec +timespec_reltoabs(struct timespec relative); + /* initialize mutex with error checking (not default on all platforms) */ int mutex_init(pthread_mutex_t *mutex); From 6af8fa07b4581525fd8131d52d91379739adba0b Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Sun, 15 Jan 2017 23:20:53 +0100 Subject: [PATCH 11/37] [spotify] Use timespec_reltoabs instead of internal mk_reltime --- src/spotify.c | 21 ++++----------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/src/spotify.c b/src/spotify.c index 8e967380..0030a671 100644 --- a/src/spotify.c +++ b/src/spotify.c @@ -36,7 +36,6 @@ #include #include #include -#include #include #ifdef HAVE_PTHREAD_NP_H # include @@ -80,8 +79,6 @@ * then use our normal cleanup of stray files to tidy db and cache. */ -// How long to wait for audio (in sec) before giving up -#define SPOTIFY_TIMEOUT 20 // How long to wait for artwork (in sec) before giving up #define SPOTIFY_ARTWORK_TIMEOUT 3 // An upper limit on sequential requests to Spotify's web api @@ -143,6 +140,9 @@ static int spotify_saved_plid; // Flag to avoid triggering playlist change events while the (re)scan is running static bool scanning; +// Timeout timespec +static struct timespec spotify_artwork_timeout = { SPOTIFY_ARTWORK_TIMEOUT, 0 }; + // Audio buffer static struct evbuffer *spotify_audio_buffer; @@ -1146,19 +1146,6 @@ static sp_playlistcontainer_callbacks pc_callbacks = { /* --------------------- INTERNAL PLAYBACK AND AUDIO ----------------------- */ /* Should only be called from within the spotify thread */ -static void -mk_reltime(struct timespec *ts, time_t sec) -{ -#if _POSIX_TIMERS > 0 - clock_gettime(CLOCK_REALTIME, ts); -#else - struct timeval tv; - gettimeofday(&tv, NULL); - TIMEVAL_TO_TIMESPEC(&tv, ts); -#endif - ts->tv_sec += sec; -} - static enum command_state playback_setup(void *arg, int *retval) { @@ -1858,7 +1845,7 @@ spotify_artwork_get(struct evbuffer *evbuf, char *path, int max_w, int max_h) if (ret == 0) { CHECK_ERR(L_SPOTIFY, pthread_mutex_lock(&artwork.mutex)); - mk_reltime(&ts, SPOTIFY_ARTWORK_TIMEOUT); + ts = timespec_reltoabs(spotify_artwork_timeout); if (!artwork.is_loaded) CHECK_ERR_EXCEPT(L_SPOTIFY, pthread_cond_timedwait(&artwork.cond, &artwork.mutex, &ts), err, ETIMEDOUT); CHECK_ERR(L_SPOTIFY, pthread_mutex_unlock(&artwork.mutex)); From 18918ce4890d622debf08d3726ef4ceb02240da3 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Sun, 15 Jan 2017 23:22:06 +0100 Subject: [PATCH 12/37] [db] Rename db_file_save_seek -> db_file_seek_update --- src/db.c | 2 +- src/db.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/db.c b/src/db.c index e747ff02..b0c808a3 100644 --- a/src/db.c +++ b/src/db.c @@ -2499,7 +2499,7 @@ db_file_update(struct media_file_info *mfi) } void -db_file_save_seek(int id, uint32_t seek) +db_file_seek_update(int id, uint32_t seek) { #define Q_TMPL "UPDATE files SET seek = %d WHERE id = %d;" char *query; diff --git a/src/db.h b/src/db.h index 079e1044..324b32c4 100644 --- a/src/db.h +++ b/src/db.h @@ -542,7 +542,7 @@ int db_file_update(struct media_file_info *mfi); void -db_file_save_seek(int id, uint32_t seek); +db_file_seek_update(int id, uint32_t seek); void db_file_delete_bypath(char *path); From aa8edeead408f120c5e4f5f1cfeea6f998de6463 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Sun, 15 Jan 2017 23:23:36 +0100 Subject: [PATCH 13/37] [player] Add suspend/resume on underrun + misc fixing up --- src/player.c | 65 ++++++++++++++++++++++++++++++++++------------------ src/player.h | 2 +- 2 files changed, 44 insertions(+), 23 deletions(-) diff --git a/src/player.c b/src/player.c index dd3f4129..de3f4d8e 100644 --- a/src/player.c +++ b/src/player.c @@ -323,6 +323,20 @@ speaker_deselect_output(struct output_device *device) volume_master_find(); } +static void +seek_save(void) +{ + int seek; + + if (!cur_streaming) + return; + + if (cur_streaming->media_kind & (MEDIA_KIND_MOVIE | MEDIA_KIND_PODCAST | MEDIA_KIND_AUDIOBOOK | MEDIA_KIND_TVSHOW)) + { + seek = (cur_streaming->output_start - cur_streaming->stream_start) / 44100 * 1000; + db_file_seek_update(cur_streaming->id, seek); + } +} int player_get_current_pos(uint64_t *pos, struct timespec *ts, int commit) @@ -429,6 +443,9 @@ pb_timer_stop(void) static void playback_abort(void); +static void +playback_suspend(void); + static void player_metadata_send(struct player_metadata *pmd); @@ -1124,8 +1141,8 @@ playback_write(void) } else if (pb_writes_pending > PLAYER_WRITES_PENDING_MAX) { - DPRINTF(E_LOG, L_PLAYER, "Source is not providing sufficient data, temporarily pausing playback\n"); - playback_abort(); // TODO Actually pause input + implement a resume event + DPRINTF(E_LOG, L_PLAYER, "Source is not providing sufficient data, temporarily suspending playback\n"); + playback_suspend(); return; } else @@ -1706,6 +1723,21 @@ playback_abort(void) metadata_purge(); } +/* Internal routine for waiting when input buffer underruns */ +static void +playback_suspend(void) +{ + outputs_playback_stop(); + + pb_timer_stop(); + + status_update(PLAY_PAUSED); + + seek_save(); + + input_buffer_full_cb(player_playback_start); +} + /* Actual commands, executed in the player thread */ static enum command_state get_status(void *arg, int *retval) @@ -2011,7 +2043,6 @@ playback_start_item(void *arg, int *retval) return COMMAND_END; } - metadata_trigger(1); /* Start sessions on selected devices */ @@ -2080,14 +2111,14 @@ playback_start(void *arg, int *retval) enum command_state cmd_state; if (player_state == PLAY_STOPPED) -{ + { // Start playback of first item in queue queue_item = db_queue_fetch_bypos(0, shuffle); if (!queue_item) -{ + { *retval = -1; return COMMAND_END; -} + } } cmd_state = playback_start_item(queue_item, retval); @@ -2263,8 +2294,6 @@ playback_seek_bh(void *arg, int *retval) static enum command_state playback_pause_bh(void *arg, int *retval) { - int ret; - if (cur_streaming->data_kind == DATA_KIND_HTTP || cur_streaming->data_kind == DATA_KIND_PIPE) { @@ -2276,11 +2305,7 @@ playback_pause_bh(void *arg, int *retval) } status_update(PLAY_PAUSED); - if (cur_streaming->media_kind & (MEDIA_KIND_MOVIE | MEDIA_KIND_PODCAST | MEDIA_KIND_AUDIOBOOK | MEDIA_KIND_TVSHOW)) - { - ret = (cur_streaming->output_start - cur_streaming->stream_start) / 44100 * 1000; - db_file_save_seek(cur_streaming->id, ret); - } + seek_save(); *retval = 0; return COMMAND_END; @@ -2787,18 +2812,15 @@ player_get_icy_artwork_url(uint32_t id) /* * Starts/resumes playback * - * Depending on the player state, this will either resume playing the current item (player is paused) - * or begin playing the queue from the beginning. + * Depending on the player state, this will either resume playing the current + * item (player is paused) or begin playing the queue from the beginning. * * If shuffle is set, the queue is reshuffled prior to starting playback. * - * If a pointer is given as argument "itemid", its value will be set to the playing item dbmfi-id. - * - * @param *id if not NULL, will be set to the playing item dbmfi-id * @return 0 if successful, -1 if an error occurred */ int -player_playback_start() +player_playback_start(void) { int ret; @@ -2807,14 +2829,13 @@ player_playback_start() } /* - * Starts playback with the media item at the given index of the play-queue. + * Starts/resumes playback of the given queue_item * * If shuffle is set, the queue is reshuffled prior to starting playback. * * If a pointer is given as argument "itemid", its value will be set to the playing item id. * - * @param index the index of the item in the play-queue - * @param *id if not NULL, will be set to the playing item id + * @param queue_item to start playing * @return 0 if successful, -1 if an error occurred */ int diff --git a/src/player.h b/src/player.h index a22f1820..ad1c4951 100644 --- a/src/player.h +++ b/src/player.h @@ -90,7 +90,7 @@ int player_speaker_set(uint64_t *ids); int -player_playback_start(); +player_playback_start(void); int player_playback_start_byitem(struct db_queue_item *queue_item); From acc67338a1b8de286da6e73340f47718b94e212f Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Sun, 15 Jan 2017 23:25:00 +0100 Subject: [PATCH 14/37] [input] Add a buffer full callback for the player --- src/input.c | 40 ++++++++++++++++++++++++++++++++++------ src/input.h | 11 +++++++++++ 2 files changed, 45 insertions(+), 6 deletions(-) diff --git a/src/input.c b/src/input.c index 0aeaeb4f..f7812c23 100644 --- a/src/input.c +++ b/src/input.c @@ -41,6 +41,8 @@ // Disallow further writes to the buffer when its size is larger than this threshold #define INPUT_BUFFER_THRESHOLD STOB(44100) +// How long (in sec) to wait for player read before looping in playback thread +#define INPUT_LOOP_TIMEOUT 1 #define DEBUG 1 //TODO disable @@ -72,6 +74,9 @@ struct input_buffer // If non-zero, remaining length of buffer until (possible) new metadata size_t metadata; + // Optional callback to player if buffer is full + input_cb full_cb; + // Locks for sharing the buffer between input and player thread pthread_mutex_t mutex; pthread_cond_t cond; @@ -84,6 +89,9 @@ static pthread_t tid_input; // Input buffer static struct input_buffer input_buffer; +// Timeout waiting in playback loop +static struct timespec input_loop_timeout = { INPUT_LOOP_TIMEOUT, 0 }; + #ifdef DEBUG static size_t debug_elapsed; #endif @@ -204,8 +212,12 @@ playback(void *arg) void input_wait(void) { + struct timespec ts; + pthread_mutex_lock(&input_buffer.mutex); - pthread_cond_wait(&input_buffer.cond, &input_buffer.mutex); + + ts = timespec_reltoabs(input_loop_timeout); + pthread_cond_timedwait(&input_buffer.cond, &input_buffer.mutex, &ts); pthread_mutex_unlock(&input_buffer.mutex); } @@ -214,21 +226,27 @@ input_wait(void) int input_write(struct evbuffer *evbuf, short flags) { + struct timespec ts; int ret; pthread_mutex_lock(&input_buffer.mutex); while ( (!input_loop_break) && (evbuffer_get_length(input_buffer.evbuf) > INPUT_BUFFER_THRESHOLD) ) { + if (input_buffer.full_cb) + { + input_buffer.full_cb(); + input_buffer.full_cb = NULL; + } + if (flags & INPUT_FLAG_NONBLOCK) { pthread_mutex_unlock(&input_buffer.mutex); return EAGAIN; } - pthread_cond_wait(&input_buffer.cond, &input_buffer.mutex); - - // TODO protect against infinite looping and waiting? + ts = timespec_reltoabs(input_loop_timeout); + pthread_cond_timedwait(&input_buffer.cond, &input_buffer.mutex, &ts); } if (!input_loop_break) @@ -294,6 +312,15 @@ input_read(void *data, size_t size, short *flags) return len; } +void +input_buffer_full_cb(input_cb cb) +{ + pthread_mutex_lock(&input_buffer.mutex); + input_buffer.full_cb = cb; + + pthread_mutex_unlock(&input_buffer.mutex); +} + int input_setup(struct player_source *ps) { @@ -316,8 +343,8 @@ input_start(struct player_source *ps) if (tid_input) { - DPRINTF(E_LOG, L_PLAYER, "Bug! Input start called, but playback already running\n"); - input_pause(ps); + DPRINTF(E_WARN, L_PLAYER, "Input start called, but playback already running\n"); + return 0; } input_loop_break = 0; @@ -427,6 +454,7 @@ input_flush(short *flags) input_buffer.eof = 0; input_buffer.metadata = 0; + input_buffer.full_cb = NULL; pthread_mutex_unlock(&input_buffer.mutex); diff --git a/src/input.h b/src/input.h index 7646d78e..c0c826a3 100644 --- a/src/input.h +++ b/src/input.h @@ -71,6 +71,8 @@ struct player_source struct player_source *play_next; }; +typedef int (*input_cb)(void); + struct input_definition { // Name of the input @@ -140,6 +142,15 @@ input_wait(void); int input_read(void *data, size_t size, short *flags); +/* + * Player can set this to get a callback from the input when the input buffer + * is full. The player may use this to resume playback after an underrun. + * + * @in cb The callback + */ +void +input_buffer_full_cb(input_cb cb); + /* * Initializes the given player source for playback */ From 7f7207bb8722d1393c578cc01feaf70407e5973f Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Sun, 15 Jan 2017 23:26:11 +0100 Subject: [PATCH 15/37] [pipe] Pipe input interface (wip) --- src/inputs/pipe.c | 179 +++++++++++++++++++++++++++------------------- 1 file changed, 105 insertions(+), 74 deletions(-) diff --git a/src/inputs/pipe.c b/src/inputs/pipe.c index a2c8c90f..33afdd85 100644 --- a/src/inputs/pipe.c +++ b/src/inputs/pipe.c @@ -23,6 +23,7 @@ #include #include #include +#include #include #include #include @@ -39,7 +40,7 @@ #include "input.h" // Maximum number of pipes to watch for data -#define PIPE_MAX_OPEN 4 +#define PIPE_MAX_WATCH 4 // Max number of bytes to read from a pipe at a time ( #define PIPE_READ_MAX 65536 @@ -99,6 +100,30 @@ pipe_free(struct pipe *pipe) free(pipe); } +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; + } + } +} + static struct pipe * pipe_find(int id) { @@ -114,7 +139,7 @@ pipe_find(int id) } -/* ----------------------------- PIPE WATCHING ---------------------------- */ +/* -------------------------------- PIPE I/O ------------------------------ */ /* Thread: filescanner */ // Updates pipelist with pipe items from the db. Pipes that are no longer in @@ -197,38 +222,52 @@ pipe_read_cb(evutil_socket_t fd, short event, void *arg) free_queue_item(queue_item, 0); } -/* Opens a pipe and starts watching it for data */ +// Opens a pipe and starts watching it for data if autostart is configured static int -pipe_open(struct pipe *pipe) +pipe_open(const char *path) { struct stat sb; + int fd; - DPRINTF(E_DBG, L_PLAYER, "(Re)opening pipe: '%s'\n", pipe->path); + DPRINTF(E_DBG, L_PLAYER, "(Re)opening pipe: '%s'\n", path); - if (lstat(pipe->path, &sb) < 0) + if (lstat(path, &sb) < 0) { - DPRINTF(E_LOG, L_PLAYER, "Could not lstat() '%s': %s\n", pipe->path, strerror(errno)); + DPRINTF(E_LOG, L_PLAYER, "Could not lstat() '%s': %s\n", path, strerror(errno)); return -1; } if (!S_ISFIFO(sb.st_mode)) { - DPRINTF(E_LOG, L_PLAYER, "Source type is pipe, but path is not a fifo: %s\n", pipe->path); + DPRINTF(E_LOG, L_PLAYER, "Source type is pipe, but path is not a fifo: %s\n", path); return -1; } - pipe->fd = open(pipe->path, O_RDONLY | O_NONBLOCK); - if (pipe->fd < 0) + fd = open(path, O_RDONLY | O_NONBLOCK); + if (fd < 0) { - DPRINTF(E_LOG, L_PLAYER, "Could not open pipe for reading '%s': %s\n", pipe->path, strerror(errno)); + DPRINTF(E_LOG, L_PLAYER, "Could not open pipe for reading '%s': %s\n", path, strerror(errno)); return -1; } + return fd; +} + +static void +pipe_close(int fd) +{ + close(fd); +} + +static int +pipe_watch_add(struct pipe *pipe) +{ + pipe->fd = pipe_open(pipe->path); + if (pipe->fd < 0) + return -1; + pipe->state = PIPE_OPEN; - if (!pipe_autostart) - return 0; // All done - pipe->ev = event_new(evbase_scan, pipe->fd, EV_READ, pipe_read_cb, pipe); if (!pipe->ev) { @@ -242,113 +281,96 @@ pipe_open(struct pipe *pipe) } static void -pipe_close(struct pipe *pipe) +pipe_watch_del(struct pipe *pipe) { - if (pipe->fd < 0) - return; - if (pipe->ev) event_free(pipe->ev); - close(pipe->fd); + pipe_close(pipe->fd); pipe->fd = -1; } -static void -pipe_remove(struct pipe *pipe) +/* +static int +pipe_metadata_watch_add(struct pipe *pipe) { - struct pipe *p; + struct pipe *md_pipe; + char md_pipe_path[PATH_MAX]; + int ret; - pipe_close(pipe); + ret = snprintf(md_pipe_path, sizeof(md_pipe_path), "%s.metadata", pipe->path); + if ((ret < 0) || (ret > sizeof(md_pipe_path))) + return -1; - if (pipe == pipelist) - pipelist = pipe->next; - else - { - for (p = pipelist; p && (p->next != pipe); p = p->next) - ; /* EMPTY */ + md_pipe = pipe_new(md_pipe_path, -1); +xxxx; - if (!p) - { - DPRINTF(E_LOG, L_REMOTE, "WARNING: pipe not found in list; BUG!\n"); - return; - } + md_pipe->fd = pipe_open(md_pipe_path); + if (md_pipe->fd < 0) + return -1; - p->next = pipe->next; - } - - pipe_free(pipe); + return 0; } +static void +pipe_metadata_watch_del(struct pipe *pipe) +{ +}*/ + static void pipe_listener_cb(enum listener_event_type type) { struct pipe *pipe; - struct pipe *next; int count; count = pipe_enum(); // Count does not include pipes with state PIPE_DEL if (count < 0) return; - for (pipe = pipelist; pipe; pipe = next) + for (pipe = pipelist; pipe; pipe = pipe->next) { - next = pipe->next; - DPRINTF(E_DBG, L_PLAYER, "Processing pipe '%s', state is %d\n", pipe->path, pipe->state); - if ((pipe->state == PIPE_NEW) && (count > PIPE_MAX_OPEN)) + if ((pipe->state == PIPE_NEW) && (count > PIPE_MAX_WATCH)) DPRINTF(E_LOG, L_PLAYER, "Max open pipes reached, will not watch %s\n", pipe->path); else if (pipe->state == PIPE_NEW) - pipe_open(pipe); + pipe_watch_add(pipe); else if (pipe->state == PIPE_DEL) - pipe_remove(pipe); // Note: Will free pipe + pipe_watch_del(pipe); } + + pipelist_prune(); } /* -------------------------- PIPE INPUT INTERFACE ------------------------ */ /* Thread: player/input */ -static int -pipe_metadata_open(struct pipe *pipe) -{ -// char md_pipe_path[PATH_MAX]; //TODO PATH_MAX - limits.h? -/* snprintf(md_pipe_path, sizeof(md_pipe_path), "%s.metadata", pipe->path); - - fd = pipe_open(md_pipe_path); // TODO avoid lstat error - if (fd < 0) -*/ - return 0; -} - static int setup(struct player_source *ps) { struct pipe *pipe; - int ret; - if (pipe_autostart) + if (!pipe_autostart) + pipelist = pipe_new(ps->path, ps->id); + + pipe = pipe_find(ps->id); + if (!pipe) { - pipe = pipe_find(ps->id); - if (!pipe) - { - DPRINTF(E_LOG, L_PLAYER, "Unknown pipe '%s'\n", ps->path); - return -1; - } + DPRINTF(E_LOG, L_PLAYER, "Unknown pipe '%s'\n", ps->path); + return -1; } - else - pipe = pipe_new(ps->path, ps->id); if (pipe->state != PIPE_OPEN) { - ret = pipe_open(pipe); - if (ret < 0) + pipe->fd = pipe_open(pipe->path); + if (pipe->fd < 0) return -1; + pipe->state = PIPE_OPEN; } - pipe_metadata_open(pipe); +// pipe_metadata_open(pipe); if (pipe->ev) event_del(pipe->ev); // Avoids autostarting pipe if manually started by user @@ -409,16 +431,24 @@ stop(struct player_source *ps) DPRINTF(E_DBG, L_PLAYER, "Stopping pipe\n"); pipe = pipe_find(ps->id); + if (!pipe) + { + DPRINTF(E_LOG, L_PLAYER, "Unknown pipe '%s'\n", ps->path); + return -1; + } + if (pipe_autostart) { // Reopen pipe since if I just readd the event it instantly makes the // callback (probably something I have missed...) - pipe_close(pipe); - pipe_open(pipe); + pipe_watch_del(pipe); + pipe_watch_add(pipe); } else { - pipe_remove(pipe); + pipe_close(pipe->fd); + pipe->state = PIPE_DEL; + pipelist_prune(); } ps->setup_done = 0; @@ -446,7 +476,8 @@ deinit(void) for (pipe = pipelist; pipelist; pipe = pipelist) { pipelist = pipe->next; - pipe_remove(pipe); + pipe_watch_del(pipe); + pipe_free(pipe); } if (pipe_autostart) From 061beaf27250b4e500672f06abeb32b17bfded1f Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Mon, 16 Jan 2017 21:44:51 +0100 Subject: [PATCH 16/37] [worker] Make sure worker accepts NULL-arguments --- src/worker.c | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/worker.c b/src/worker.c index 07007982..20a7146b 100644 --- a/src/worker.c +++ b/src/worker.c @@ -149,15 +149,20 @@ worker_execute(void (*cb)(void *), void *cb_arg, size_t arg_size, int delay) return; } - argcpy = malloc(arg_size); - if (!argcpy) + if (arg_size > 0) { - DPRINTF(E_LOG, L_MAIN, "Out of memory\n"); - free(cmdarg); - return; - } + argcpy = malloc(arg_size); + if (!argcpy) + { + DPRINTF(E_LOG, L_MAIN, "Out of memory\n"); + free(cmdarg); + return; + } - memcpy(argcpy, cb_arg, arg_size); + memcpy(argcpy, cb_arg, arg_size); + } + else + argcpy = NULL; cmdarg->cb = cb; cmdarg->cb_arg = argcpy; From 9fb62441d20fd6b7ebb5cb823d4eacff87f6214f Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Mon, 16 Jan 2017 22:08:57 +0100 Subject: [PATCH 17/37] [pipe] Use worker thread instead of filescanner for watching pipes --- src/inputs/pipe.c | 89 ++++++++++++++++++++++++++--------------------- 1 file changed, 50 insertions(+), 39 deletions(-) diff --git a/src/inputs/pipe.c b/src/inputs/pipe.c index 33afdd85..e99a6112 100644 --- a/src/inputs/pipe.c +++ b/src/inputs/pipe.c @@ -37,6 +37,7 @@ #include "conffile.h" #include "listener.h" #include "player.h" +#include "worker.h" #include "input.h" // Maximum number of pipes to watch for data @@ -44,9 +45,7 @@ // Max number of bytes to read from a pipe at a time ( #define PIPE_READ_MAX 65536 -// filescanner event base, from filescanner.c -// TODO don't use filescanner thread/base -extern struct event_base *evbase_scan; +extern struct event_base *evbase_worker; enum pipestate { @@ -140,7 +139,7 @@ pipe_find(int id) /* -------------------------------- PIPE I/O ------------------------------ */ -/* Thread: filescanner */ +/* Thread: worker */ // 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 @@ -192,36 +191,6 @@ pipe_enum(void) return count; } -// 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; - - DPRINTF(E_DBG, L_PLAYER, "Autostarting pipe %d, %d\n", (int) fd, (int)event); - - ret = player_get_status(&status); - if ((ret < 0) || (status.status == PLAY_PLAYING)) - return; - - 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); - - free_queue_item(queue_item, 0); -} - // Opens a pipe and starts watching it for data if autostart is configured static int pipe_open(const char *path) @@ -259,6 +228,40 @@ pipe_close(int fd) close(fd); } +// 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 we are busy\n", pipe->path); + // FIXME Now the event won't activate any more - even if we are not busy + 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); + + free_queue_item(queue_item, 0); +} + static int pipe_watch_add(struct pipe *pipe) { @@ -268,7 +271,7 @@ pipe_watch_add(struct pipe *pipe) pipe->state = PIPE_OPEN; - pipe->ev = event_new(evbase_scan, pipe->fd, EV_READ, pipe_read_cb, pipe); + pipe->ev = event_new(evbase_worker, pipe->fd, EV_READ, pipe_read_cb, pipe); if (!pipe->ev) { DPRINTF(E_LOG, L_PLAYER, "Could not watch pipe for new data '%s'\n", pipe->path); @@ -292,9 +295,10 @@ pipe_watch_del(struct pipe *pipe) } /* -static int -pipe_metadata_watch_add(struct pipe *pipe) +static void +pipe_metadata_watch_add(void *arg) { + struct pipe *pipe = arg; struct pipe *md_pipe; char md_pipe_path[PATH_MAX]; int ret; @@ -319,7 +323,7 @@ pipe_metadata_watch_del(struct pipe *pipe) }*/ static void -pipe_listener_cb(enum listener_event_type type) +pipe_update(void *arg) { struct pipe *pipe; int count; @@ -343,6 +347,13 @@ pipe_listener_cb(enum listener_event_type type) pipelist_prune(); } +// Thread: filescanner +static void +pipe_listener_cb(enum listener_event_type type) +{ + worker_execute(pipe_update, NULL, 0, 0); +} + /* -------------------------- PIPE INPUT INTERFACE ------------------------ */ /* Thread: player/input */ @@ -370,7 +381,7 @@ setup(struct player_source *ps) pipe->state = PIPE_OPEN; } -// pipe_metadata_open(pipe); +// worker_execute(pipe_metadata_watch_add, pipe, sizeof(struct pipe), 0); if (pipe->ev) event_del(pipe->ev); // Avoids autostarting pipe if manually started by user From 90f37b75ce760a5e342ccf4d30c3b8218ce42125 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Wed, 18 Jan 2017 23:37:26 +0100 Subject: [PATCH 18/37] [player] Fix it so source_read() returns silence and not an error until source_check stops playback --- src/player.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/player.c b/src/player.c index de3f4d8e..24ae595f 100644 --- a/src/player.c +++ b/src/player.c @@ -1077,8 +1077,12 @@ source_read(uint8_t *buf, int len) int ret; short flags; + // Nothing to read, stream silence until source_check() stops playback if (!cur_streaming) - return -1; + { + memset(buf, 0, len); + return len; + } nbytes = input_read(buf, len, &flags); if (nbytes < 0) From ee32b9cb7085400dfebbf5058e81cc21c93db16f Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Wed, 18 Jan 2017 23:42:52 +0100 Subject: [PATCH 19/37] [pipe] Reset pipes when required + prepare for metadata pipes --- src/inputs/pipe.c | 292 +++++++++++++++++++++++++++++++--------------- 1 file changed, 198 insertions(+), 94 deletions(-) diff --git a/src/inputs/pipe.c b/src/inputs/pipe.c index e99a6112..e636f9f1 100644 --- a/src/inputs/pipe.c +++ b/src/inputs/pipe.c @@ -14,10 +14,25 @@ * 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 + * + * + * 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. + * */ #include #include +#include #include #include #include @@ -49,18 +64,27 @@ extern struct event_base *evbase_worker; enum pipestate { - PIPE_NEW = (1 << 0), - PIPE_DEL = (1 << 1), - PIPE_OPEN = (1 << 2), + PIPE_NEW, + PIPE_DEL, + PIPE_OPEN, +}; + +enum pipetype +{ + PIPE_PCM, + PIPE_METADATA, }; struct pipe { - int id; - int fd; - char *path; - enum pipestate state; - struct event *ev; + 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 // TODO mutex struct pipe *next; @@ -73,11 +97,19 @@ static int pipe_autostart; // will just point to the currently playing pipe (if any). static struct pipe *pipelist; +// Single pipe that we start watching for metadata after playback starts +static struct pipe *pipe_metadata; + +/* ------------------------------- FORWARDS ------------------------------- */ + +static void +pipe_watch_reset(struct pipe *pipe); + /* -------------------------------- HELPERS ------------------------------- */ static struct pipe * -pipe_new(const char *path, int id) +pipe_new(const char *path, int id, enum pipetype type, event_callback_fn cb) { struct pipe *pipe; @@ -86,8 +118,14 @@ pipe_new(const char *path, int id) pipe->id = id; pipe->fd = -1; pipe->state = PIPE_NEW; - pipe->next = pipelist; - pipelist = pipe; + pipe->type = type; + pipe->cb = cb; + + if (type == PIPE_PCM) + { + pipe->next = pipelist; + pipelist = pipe; + } return pipe; } @@ -138,9 +176,45 @@ pipe_find(int id) } -/* -------------------------------- PIPE I/O ------------------------------ */ +/* ---------------------------- GENERAL PIPE I/O -------------------------- */ /* Thread: worker */ +// 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); +} + // 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) @@ -183,7 +257,7 @@ pipe_enum(void) continue; } - pipe = pipe_new(dbmfi.path, id); + pipe = pipe_new(dbmfi.path, id, PIPE_PCM, pipe_read_cb); } db_query_end(&qp); @@ -193,7 +267,7 @@ pipe_enum(void) // Opens a pipe and starts watching it for data if autostart is configured static int -pipe_open(const char *path) +pipe_open(const char *path, bool silent) { struct stat sb; int fd; @@ -202,7 +276,8 @@ pipe_open(const char *path) if (lstat(path, &sb) < 0) { - DPRINTF(E_LOG, L_PLAYER, "Could not lstat() '%s': %s\n", path, strerror(errno)); + if (!silent) + DPRINTF(E_LOG, L_PLAYER, "Could not lstat() '%s': %s\n", path, strerror(errno)); return -1; } @@ -225,61 +300,32 @@ pipe_open(const char *path) static void pipe_close(int fd) { - close(fd); -} - -// 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 we are busy\n", pipe->path); - // FIXME Now the event won't activate any more - even if we are not busy - 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); - - free_queue_item(queue_item, 0); + if (fd >= 0) + close(fd); } static int pipe_watch_add(struct pipe *pipe) { - pipe->fd = pipe_open(pipe->path); + bool silent; + + silent = (pipe->type == PIPE_METADATA); + pipe->fd = pipe_open(pipe->path, silent); if (pipe->fd < 0) return -1; - pipe->state = PIPE_OPEN; - - pipe->ev = event_new(evbase_worker, pipe->fd, EV_READ, pipe_read_cb, pipe); + pipe->ev = event_new(evbase_worker, pipe->fd, EV_READ, pipe->cb, pipe); if (!pipe->ev) { DPRINTF(E_LOG, L_PLAYER, "Could not watch pipe for new data '%s'\n", pipe->path); + pipe_close(pipe->fd); return -1; } event_add(pipe->ev, NULL); + pipe->state = PIPE_OPEN; + return 0; } @@ -294,36 +340,17 @@ pipe_watch_del(struct pipe *pipe) pipe->fd = -1; } -/* +// 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. static void -pipe_metadata_watch_add(void *arg) +pipe_watch_reset(struct pipe *pipe) { - struct pipe *pipe = arg; - struct pipe *md_pipe; - char md_pipe_path[PATH_MAX]; - int ret; - - ret = snprintf(md_pipe_path, sizeof(md_pipe_path), "%s.metadata", pipe->path); - if ((ret < 0) || (ret > sizeof(md_pipe_path))) - return -1; - - md_pipe = pipe_new(md_pipe_path, -1); -xxxx; - - md_pipe->fd = pipe_open(md_pipe_path); - if (md_pipe->fd < 0) - return -1; - - return 0; + pipe_watch_del(pipe); + pipe_watch_add(pipe); } static void -pipe_metadata_watch_del(struct pipe *pipe) -{ -}*/ - -static void -pipe_update(void *arg) +pipe_watch_update(void *arg) { struct pipe *pipe; int count; @@ -351,7 +378,75 @@ pipe_update(void *arg) static void pipe_listener_cb(enum listener_event_type type) { - worker_execute(pipe_update, NULL, 0, 0); + 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) +{ + struct evbuffer *evbuf; + int ret; + + DPRINTF(E_DBG, L_PLAYER, "BANG\n"); + + evbuf = evbuffer_new(); + ret = evbuffer_read(evbuf, pipe_metadata->fd, PIPE_READ_MAX); + evbuffer_free(evbuf); + + event_add(pipe_metadata->ev, NULL); +/* + else if (ret < 0) + give up + goto out; + if (ret == 0) + pipe_reset(pipe_metadata); + evbuffer_add_printf(evbuf, "\n"); + DPRINTF(E_DBG, L_PLAYER, "Got some pipe metadata\n%s", (char *)evbuffer_pullup(evbuf, -1)); + + event_add(pipe_metadata->ev, NULL); + + out: + evbuffer_free(evbuf); + pipe_reset(pipe_metadata);*/ +} + +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; + + ret = pipe_watch_add(pipe_metadata); + if (ret < 0) + { + pipe_free(pipe_metadata); + pipe_metadata = NULL; + return; + } +} + +static void +pipe_metadata_watch_del(void *arg) +{ + if (!pipe_metadata) + return; + + pipe_watch_del(pipe_metadata); + pipe_free(pipe_metadata); + pipe_metadata = NULL; } @@ -363,8 +458,9 @@ setup(struct player_source *ps) { struct pipe *pipe; + // If autostart is disabled then this is the first time we encounter the pipe if (!pipe_autostart) - pipelist = pipe_new(ps->path, ps->id); + pipe_new(ps->path, ps->id, PIPE_PCM, NULL); pipe = pipe_find(ps->id); if (!pipe) @@ -372,20 +468,20 @@ setup(struct player_source *ps) DPRINTF(E_LOG, L_PLAYER, "Unknown pipe '%s'\n", ps->path); return -1; } - +// TODO pipe mutex here if (pipe->state != PIPE_OPEN) { - pipe->fd = pipe_open(pipe->path); + pipe->fd = pipe_open(pipe->path, 0); if (pipe->fd < 0) return -1; pipe->state = PIPE_OPEN; } -// worker_execute(pipe_metadata_watch_add, pipe, sizeof(struct pipe), 0); - if (pipe->ev) event_del(pipe->ev); // Avoids autostarting pipe if manually started by user + worker_execute(pipe_metadata_watch_add, pipe->path, strlen(pipe->path) + 1, 0); + ps->setup_done = 1; return 0; @@ -413,7 +509,12 @@ start(struct player_source *ps) while (!input_loop_break) { ret = evbuffer_read(evbuf, pipe->fd, PIPE_READ_MAX); - if ( (ret == 0) || ((ret < 0) && (errno == EAGAIN)) ) + if ((ret == 0) && (pipe->is_autostarted)) + { + input_write(evbuf, INPUT_FLAG_EOF); // Autostop + break; + } + else if ((ret == 0) || ((ret < 0) && (errno == EAGAIN))) { input_wait(); continue; @@ -448,19 +549,22 @@ stop(struct player_source *ps) return -1; } - if (pipe_autostart) - { - // Reopen pipe since if I just readd the event it instantly makes the - // callback (probably something I have missed...) - pipe_watch_del(pipe); - pipe_watch_add(pipe); - } - else + if (!pipe_autostart) { + // Since autostart is disabled we are now done with the pipe pipe_close(pipe->fd); pipe->state = PIPE_DEL; pipelist_prune(); } + 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); ps->setup_done = 0; From 40d50d693b7049f0d3c357487c947ec86db28934 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Sun, 22 Jan 2017 23:06:13 +0100 Subject: [PATCH 20/37] [misc] Add small function to swap pointers --- src/misc.c | 8 ++++++++ src/misc.h | 3 +++ 2 files changed, 11 insertions(+) diff --git a/src/misc.c b/src/misc.c index 769b15e2..101c6472 100644 --- a/src/misc.c +++ b/src/misc.c @@ -569,6 +569,14 @@ trimwhitespace(const char *str) return out; } +void +swap_pointers(char **a, char **b) +{ + char *t = *a; + *a = *b; + *b = t; +} + uint32_t djb_hash(const void *data, size_t len) { diff --git a/src/misc.h b/src/misc.h index de1a089e..685a3e14 100644 --- a/src/misc.h +++ b/src/misc.h @@ -82,6 +82,9 @@ unicode_fixup_string(char *str, const char *fromcode); char * trimwhitespace(const char *str); +void +swap_pointers(char **a, char **b); + uint32_t djb_hash(const void *data, size_t len); From e92152cadb7a9c6ce1a35ee7cc7ab520e8a51ce5 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Sun, 22 Jan 2017 23:08:26 +0100 Subject: [PATCH 21/37] [db] Upgrade db to 19.03: Add artwork_url column to queue table --- src/db_init.c | 2 ++ src/db_init.h | 2 +- src/db_upgrade.c | 23 +++++++++++++++++++++++ 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/db_init.c b/src/db_init.c index d3017841..82d93a57 100644 --- a/src/db_init.c +++ b/src/db_init.c @@ -1,3 +1,4 @@ + /* * Copyright (C) 2009-2011 Julien BLACHE * Copyright (C) 2010 Kai Elwert @@ -182,6 +183,7 @@ " year INTEGER DEFAULT 0," \ " track INTEGER DEFAULT 0," \ " disc INTEGER DEFAULT 0" \ + " artwork_url VARCHAR(4096) DEFAULT NULL," \ ");" #define TRG_GROUPS_INSERT_FILES \ diff --git a/src/db_init.h b/src/db_init.h index 4ed6b0df..84237d8a 100644 --- a/src/db_init.h +++ b/src/db_init.h @@ -26,7 +26,7 @@ * is a major upgrade. In other words minor version upgrades permit downgrading * forked-daapd after the database was upgraded. */ #define SCHEMA_VERSION_MAJOR 19 -#define SCHEMA_VERSION_MINOR 02 +#define SCHEMA_VERSION_MINOR 03 int db_init_indices(sqlite3 *hdl); diff --git a/src/db_upgrade.c b/src/db_upgrade.c index f25c0e00..8b7c787d 100644 --- a/src/db_upgrade.c +++ b/src/db_upgrade.c @@ -1529,6 +1529,24 @@ static const struct db_upgrade_query db_upgrade_v1902_queries[] = { U_V1902_SCVER_MINOR, "set schema_version_minor to 02" }, }; + +#define U_V1903_ALTER_QUEUE_ADD_ARTWORKURL \ + "ALTER TABLE queue ADD COLUMN artwork_url VARCHAR(4096) DEFAULT NULL;" + +#define U_V1903_SCVER_MAJOR \ + "UPDATE admin SET value = '19' WHERE key = 'schema_version_major';" +#define U_V1903_SCVER_MINOR \ + "UPDATE admin SET value = '03' WHERE key = 'schema_version_minor';" + +static const struct db_upgrade_query db_upgrade_v1903_queries[] = + { + { U_V1903_ALTER_QUEUE_ADD_ARTWORKURL, "alter table queue add column artwork_url" }, + + { U_V1903_SCVER_MAJOR, "set schema_version_major to 19" }, + { U_V1903_SCVER_MINOR, "set schema_version_minor to 03" }, + }; + + int db_upgrade(sqlite3 *hdl, int db_ver) { @@ -1651,6 +1669,11 @@ db_upgrade(sqlite3 *hdl, int db_ver) if (ret < 0) return -1; + case 1902: + ret = db_generic_upgrade(hdl, db_upgrade_v1903_queries, sizeof(db_upgrade_v1903_queries) / sizeof(db_upgrade_v1903_queries[0])); + if (ret < 0) + return -1; + break; default: From 4d4a4ffd706949a638b8d28820defdddf08478a0 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Sun, 22 Jan 2017 23:10:15 +0100 Subject: [PATCH 22/37] [db] Remove special icy update function, add db_queue_update_item() - we will use the latter generalised form from now on, even though it is perhaps a tad slower --- src/db.c | 221 ++++++++++++++++++++++--------------------------------- src/db.h | 6 +- 2 files changed, 92 insertions(+), 135 deletions(-) diff --git a/src/db.c b/src/db.c index b0c808a3..e72d4f60 100644 --- a/src/db.c +++ b/src/db.c @@ -356,14 +356,12 @@ db_escape_string(const char *str) void free_pi(struct pairing_info *pi, int content_only) { - if (pi->remote_id) - free(pi->remote_id); + if (!pi) + return; - if (pi->name) - free(pi->name); - - if (pi->guid) - free(pi->guid); + free(pi->remote_id); + free(pi->name); + free(pi->guid); if (!content_only) free(pi); @@ -374,77 +372,33 @@ free_pi(struct pairing_info *pi, int content_only) void free_mfi(struct media_file_info *mfi, int content_only) { - if (mfi->path) - free(mfi->path); + if (!mfi) + return; - if (mfi->fname) - free(mfi->fname); - - if (mfi->title) - free(mfi->title); - - if (mfi->artist) - free(mfi->artist); - - if (mfi->album) - free(mfi->album); - - if (mfi->genre) - free(mfi->genre); - - if (mfi->comment) - free(mfi->comment); - - if (mfi->type) - free(mfi->type); - - if (mfi->composer) - free(mfi->composer); - - if (mfi->orchestra) - free(mfi->orchestra); - - if (mfi->conductor) - free(mfi->conductor); - - if (mfi->grouping) - free(mfi->grouping); - - if (mfi->description) - free(mfi->description); - - if (mfi->codectype) - free(mfi->codectype); - - if (mfi->album_artist) - free(mfi->album_artist); - - if (mfi->tv_series_name) - free(mfi->tv_series_name); - - if (mfi->tv_episode_num_str) - free(mfi->tv_episode_num_str); - - if (mfi->tv_network_name) - free(mfi->tv_network_name); - - if (mfi->title_sort) - free(mfi->title_sort); - - if (mfi->artist_sort) - free(mfi->artist_sort); - - if (mfi->album_sort) - free(mfi->album_sort); - - if (mfi->composer_sort) - free(mfi->composer_sort); - - if (mfi->album_artist_sort) - free(mfi->album_artist_sort); - - if (mfi->virtual_path) - free(mfi->virtual_path); + free(mfi->path); + free(mfi->fname); + free(mfi->title); + free(mfi->artist); + free(mfi->album); + free(mfi->genre); + free(mfi->comment); + free(mfi->type); + free(mfi->composer); + free(mfi->orchestra); + free(mfi->conductor); + free(mfi->grouping); + free(mfi->description); + free(mfi->codectype); + free(mfi->album_artist); + free(mfi->tv_series_name); + free(mfi->tv_episode_num_str); + free(mfi->tv_network_name); + free(mfi->title_sort); + free(mfi->artist_sort); + free(mfi->album_sort); + free(mfi->composer_sort); + free(mfi->album_artist_sort); + free(mfi->virtual_path); if (!content_only) free(mfi); @@ -489,17 +443,13 @@ unicode_fixup_mfi(struct media_file_info *mfi) void free_pli(struct playlist_info *pli, int content_only) { - if (pli->title) - free(pli->title); + if (!pli) + return; - if (pli->query) - free(pli->query); - - if (pli->path) - free(pli->path); - - if (pli->virtual_path) - free(pli->virtual_path); + free(pli->title); + free(pli->query); + free(pli->path); + free(pli->virtual_path); if (!content_only) free(pli); @@ -510,8 +460,10 @@ free_pli(struct playlist_info *pli, int content_only) void free_di(struct directory_info *di, int content_only) { - if (di->virtual_path) - free(di->virtual_path); + if (!di) + return; + + free(di->virtual_path); if (!content_only) free(di); @@ -3952,29 +3904,25 @@ db_speaker_clear_all(void) void free_queue_item(struct db_queue_item *queue_item, int content_only) { - if (queue_item->path) - free(queue_item->path); - if (queue_item->virtual_path) - free(queue_item->virtual_path); - if (queue_item->title) - free(queue_item->title); - if (queue_item->artist) - free(queue_item->artist); - if (queue_item->album_artist) - free(queue_item->album_artist); - if (queue_item->album) - free(queue_item->album); - if (queue_item->genre) - free(queue_item->genre); - if (queue_item->artist_sort) - free(queue_item->artist_sort); - if (queue_item->album_sort) - free(queue_item->album_sort); - if (queue_item->album_artist_sort) - free(queue_item->album_artist_sort); + if (!queue_item) + return; - if (!content_only && queue_item) + free(queue_item->path); + free(queue_item->virtual_path); + free(queue_item->title); + free(queue_item->artist); + free(queue_item->album_artist); + free(queue_item->album); + free(queue_item->genre); + free(queue_item->artist_sort); + free(queue_item->album_sort); + free(queue_item->album_artist_sort); + free(queue_item->artwork_url); + + if (!content_only) free(queue_item); + else + memset(queue_item, 0, sizeof(struct db_queue_item)); } /* @@ -4047,29 +3995,6 @@ queue_inc_version_and_notify() listener_notify(LISTENER_QUEUE); } -void -db_queue_update_icymetadata(int id, char *artist, char *album) -{ -#define Q_TMPL "UPDATE queue SET artist = TRIM(%Q), album = TRIM(%Q) WHERE id = %d;" - char *query; - - if (id == 0) - return; - - query = sqlite3_mprintf(Q_TMPL, artist, album, id); - if (!query) - { - DPRINTF(E_LOG, L_DB, "Out of memory for query string\n"); - - return; - } - - db_query_run(query, 1, 0); - queue_inc_version_and_notify(); - -#undef Q_TMPL -} - static int queue_add_file(struct db_media_file_info *dbmfi, int pos, int shuffle_pos) { @@ -4102,6 +4027,35 @@ queue_add_file(struct db_media_file_info *dbmfi, int pos, int shuffle_pos) #undef Q_TMPL } +int +db_queue_update_item(struct db_queue_item *qi) +{ +#define Q_TMPL "UPDATE queue SET " \ + "file_id = %d, song_length = %d, data_kind = %d, media_kind = %d, " \ + "pos = %d, shuffle_pos = %d, path = '%q', virtual_path = %Q, " \ + "title = %Q, artist = %Q, album_artist = %Q, album = %Q, " \ + "genre = %Q, songalbumid = %" PRIi64 ", time_modified = %d, " \ + "artist_sort = %Q, album_sort = %Q, album_artist_sort = %Q, " \ + "year = %d, track = %d, disc = %d, artwork_url = %Q " \ + "WHERE id = %d;" + char *query; + int ret; + + query = sqlite3_mprintf(Q_TMPL, + qi->file_id, qi->song_length, qi->data_kind, qi->media_kind, + qi->pos, qi->shuffle_pos, qi->path, qi->virtual_path, + qi->title, qi->artist, qi->album_artist, qi->album, + qi->genre, qi->songalbumid, qi->time_modified, + qi->artist_sort, qi->album_sort, qi->album_artist_sort, + qi->year, qi->track, qi->disc, qi->artwork_url, + qi->id); + + ret = db_query_run(query, 1, 0); + + return ret; +#undef Q_TMPL +} + /* * Adds the files matching the given query to the queue after the item with the given item id * @@ -4434,6 +4388,7 @@ queue_enum_fetch(struct query_params *query_params, struct db_queue_item *queue_ queue_item->year = sqlite3_column_int(query_params->stmt, 19); queue_item->track = sqlite3_column_int(query_params->stmt, 20); queue_item->disc = sqlite3_column_int(query_params->stmt, 21); + queue_item->artwork_url = strdup_if((char *)sqlite3_column_text(query_params->stmt, 22), keep_item); return 0; } diff --git a/src/db.h b/src/db.h index 324b32c4..108198ed 100644 --- a/src/db.h +++ b/src/db.h @@ -418,6 +418,8 @@ struct db_queue_item uint32_t year; uint32_t track; uint32_t disc; + + char *artwork_url; }; char * @@ -687,8 +689,8 @@ db_speaker_clear_all(void); int db_queue_get_version(); -void -db_queue_update_icymetadata(int id, char *artist, char *album); +int +db_queue_update_item(struct db_queue_item *queue_item); int db_queue_add_by_queryafteritemid(struct query_params *qp, uint32_t item_id); From 2696b279727d913a7af5ea2cc4d9868181130862 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Sun, 22 Jan 2017 23:13:13 +0100 Subject: [PATCH 23/37] [artwork] Let the artwork handler for streams use queue->artwork_url instead of calling the player, which was messy --- src/artwork.c | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/artwork.c b/src/artwork.c index a17a6519..a967fbc9 100644 --- a/src/artwork.c +++ b/src/artwork.c @@ -40,7 +40,6 @@ #include "logger.h" #include "conffile.h" #include "cache.h" -#include "player.h" #include "http.h" #include "avio_evbuffer.h" @@ -1105,6 +1104,7 @@ static int source_item_stream_get(struct artwork_ctx *ctx) { struct http_client_ctx client; + struct db_queue_item *queue_item; struct keyval *kv; const char *content_type; char *url; @@ -1116,9 +1116,15 @@ source_item_stream_get(struct artwork_ctx *ctx) ret = ART_E_NONE; - url = player_get_icy_artwork_url(ctx->id); - if (!url) - return ART_E_NONE; + queue_item = db_queue_fetch_byfileid(ctx->id); + if (!queue_item || !queue_item->artwork_url) + { + free_queue_item(queue_item, 0); + return ART_E_NONE; + } + + url = strdup(queue_item->artwork_url); + free_queue_item(queue_item, 0); len = strlen(url); if ((len < 14) || (len > PATH_MAX)) // Can't be shorter than http://a/1.jpg From 7b6a7b65b31d9637367492b2061e42dc7044b456 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Sun, 22 Jan 2017 23:16:15 +0100 Subject: [PATCH 24/37] [input] Add interface for getting metadata from input modules --- src/input.c | 42 ++++++++++++++++++++++++++++++++++++++++++ src/input.h | 30 ++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+) diff --git a/src/input.c b/src/input.c index f7812c23..dee17b33 100644 --- a/src/input.c +++ b/src/input.c @@ -463,6 +463,48 @@ input_flush(short *flags) #endif } +int +input_metadata_get(struct input_metadata *metadata, struct player_source *ps, int startup) +{ + int type; + + if (!metadata || !ps || !ps->stream_start || !ps->output_start) + { + DPRINTF(E_LOG, L_PLAYER, "Bug! Unhandled case in input_metadata_get()\n"); + return -1; + } + + memset(metadata, 0, sizeof(struct input_metadata)); + + metadata->item_id = ps->item_id; + + metadata->startup = startup; + metadata->offset = ps->output_start - ps->stream_start; + metadata->rtptime = ps->stream_start; + + // Note that the source may overwrite the above progress metadata + type = source_check_and_map(ps, "metadata_get", 1); + if ((type < 0) || (inputs[type]->disabled)) + return -1; + + if (!inputs[type]->metadata_get) + return 0; + + return inputs[type]->metadata_get(metadata, ps); +} + +void +input_metadata_free(struct input_metadata *metadata, int content_only) +{ + free(metadata->artist); + free(metadata->title); + free(metadata->album); + free(metadata->artwork_url); + + if (!content_only) + free(metadata); +} + int input_init(void) { diff --git a/src/input.h b/src/input.h index c0c826a3..b828a03a 100644 --- a/src/input.h +++ b/src/input.h @@ -73,6 +73,21 @@ struct player_source typedef int (*input_cb)(void); +struct input_metadata +{ + uint32_t item_id; + + int startup; + + uint64_t rtptime; + uint64_t offset; + + char *artist; + char *title; + char *album; + char *artwork_url; +}; + struct input_definition { // Name of the input @@ -96,6 +111,9 @@ struct input_definition // Changes the playback position int (*seek)(struct player_source *ps, int seek_ms); + // Return metadata + int (*metadata_get)(struct input_metadata *metadata, struct player_source *ps); + // Initialization function called during startup int (*init)(void); @@ -192,6 +210,18 @@ input_seek(struct player_source *ps, int seek_ms); void input_flush(short *flags); +/* + * Gets metadata from the input, returns 0 if metadata is set, otherwise -1 + */ +int +input_metadata_get(struct input_metadata *metadata, struct player_source *ps, int startup); + +/* + * Free the entire struct + */ +void +input_metadata_free(struct input_metadata *metadata, int content_only); + /* * Called by player_init (so will run in main thread) */ From 0b9b008a1a89166a40c372a4692b4f9c83e95587 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Sun, 22 Jan 2017 23:21:48 +0100 Subject: [PATCH 25/37] [player] Try to consolidate metadata handling + use input interface --- src/player.c | 253 ++++++++++++++++----------------------------------- src/player.h | 3 - 2 files changed, 80 insertions(+), 176 deletions(-) diff --git a/src/player.c b/src/player.c index 24ae595f..c3dee8f5 100644 --- a/src/player.c +++ b/src/player.c @@ -123,28 +123,18 @@ struct spk_enum void *arg; }; -struct icy_artwork -{ - uint32_t id; - char *artwork_url; -}; - -struct player_metadata -{ - int id; - uint64_t rtptime; - uint64_t offset; - int startup; - - struct output_metadata *omd; -}; - struct speaker_set_param { uint64_t *device_ids; int intval; }; +struct metadata_param +{ + struct input_metadata *input; + struct output_metadata *output; +}; + union player_arg { struct volume_param vol_param; @@ -153,13 +143,12 @@ union player_arg struct output_device *device; struct player_status *status; struct player_source *ps; - struct player_metadata *pmd; + struct metadata_param metadata_param; uint32_t *id_ptr; struct speaker_set_param speaker_set_param; enum repeat_mode mode; uint32_t id; int intval; - struct icy_artwork icy; }; struct event_base *evbase_player; @@ -447,9 +436,10 @@ static void playback_suspend(void); static void -player_metadata_send(struct player_metadata *pmd); +player_metadata_send(struct input_metadata *imd, struct output_metadata *omd); -/* Callback from the worker thread (async operation as it may block) */ + +// Callback from the worker thread (async operation as it may block) static void playcount_inc_cb(void *arg) { @@ -459,7 +449,7 @@ playcount_inc_cb(void *arg) } #ifdef LASTFM -/* Callback from the worker thread (async operation as it may block) */ +// Callback from the worker thread (async operation as it may block) static void scrobble_cb(void *arg) { @@ -469,108 +459,72 @@ scrobble_cb(void *arg) } #endif -/* Callback from the worker thread - * This prepares metadata in the worker thread, since especially the artwork - * retrieval may take some time. outputs_metadata_prepare() must be thread safe. - * The sending must be done in the player thread. - */ +// Callback from the worker thread. Here the heavy lifting is done: updating the +// db_queue_item, retrieving artwork (through outputs_metadata_prepare) and +// when done, telling the player to send the metadata to the clients static void -metadata_prepare_cb(void *arg) +metadata_update_cb(void *arg) { - struct player_metadata *pmd = arg; + struct input_metadata *metadata = arg; + struct output_metadata *o_metadata; + struct db_queue_item *queue_item; + int ret; - pmd->omd = outputs_metadata_prepare(pmd->id); + queue_item = db_queue_fetch_byitemid(metadata->item_id); + if (!queue_item) + { + DPRINTF(E_LOG, L_PLAYER, "Bug! Input metadata item_id does not match anything in queue\n"); + goto out_free_metadata; + } - if (pmd->omd) - player_metadata_send(pmd); + // Since we won't be using the metadata struct values for anything else than + // this we just swap pointers + if (metadata->artist) + swap_pointers(&queue_item->artist, &metadata->artist); + if (metadata->title) + swap_pointers(&queue_item->title, &metadata->title); + if (metadata->album) + swap_pointers(&queue_item->album, &metadata->album); + if (metadata->artwork_url) + swap_pointers(&queue_item->artwork_url, &metadata->artwork_url); - outputs_metadata_free(pmd->omd); + ret = db_queue_update_item(queue_item); + if (ret < 0) + { + DPRINTF(E_LOG, L_PLAYER, "Database error while updating queue with new metadata\n"); + goto out_free_queueitem; + } + + o_metadata = outputs_metadata_prepare(metadata->item_id); + + // Actual sending must be done by player, since the worker does not own the outputs + player_metadata_send(metadata, o_metadata); + + outputs_metadata_free(o_metadata); + + out_free_queueitem: + free_queue_item(queue_item, 0); + + out_free_metadata: + input_metadata_free(metadata, 1); } -/* Callback from the worker thread (async operation as it may block) */ -static void -update_icy_cb(void *arg) -{ - struct http_icy_metadata *metadata = arg; - - db_queue_update_icymetadata(metadata->id, metadata->artist, metadata->title); - - http_icy_metadata_free(metadata, 1); -} - -/* Metadata */ -static void -metadata_prune(uint64_t pos) -{ - outputs_metadata_prune(pos); -} - -static void -metadata_purge(void) -{ - outputs_metadata_purge(); -} - -static void +// Gets the metadata, but since the actual update requires db writes and +// possibly retrieving artwork we let the worker do the next step +void metadata_trigger(int startup) { - struct player_metadata pmd; + struct input_metadata metadata; + int ret; - memset(&pmd, 0, sizeof(struct player_metadata)); - - pmd.id = cur_streaming->item_id; - pmd.startup = startup; - - if (cur_streaming->stream_start && cur_streaming->output_start) - { - pmd.offset = cur_streaming->output_start - cur_streaming->stream_start; - pmd.rtptime = cur_streaming->stream_start; - } - else - { - DPRINTF(E_LOG, L_PLAYER, "PTOH! Unhandled song boundary case in metadata_trigger()\n"); - } - - /* Defer the actual work of preparing the metadata to the worker thread */ - worker_execute(metadata_prepare_cb, &pmd, sizeof(struct player_metadata), 0); -} - -/* Checks if there is new HTTP ICY metadata, and if so sends updates to clients */ -void -metadata_check_icy(void) -{ - struct http_icy_metadata *metadata; - int changed; - - metadata = transcode_metadata(cur_streaming->xcode, &changed); - if (!metadata) + ret = input_metadata_get(&metadata, cur_streaming, startup); + if (ret < 0) return; - if (!changed || !metadata->title) - goto no_update; - - if (metadata->title[0] == '\0') - goto no_update; - - metadata->id = cur_streaming->item_id; - - /* Defer the database update to the worker thread */ - worker_execute(update_icy_cb, metadata, sizeof(struct http_icy_metadata), 0); - - /* Triggers preparing and sending output metadata */ - metadata_trigger(0); - - /* Only free the struct, the content must be preserved for update_icy_cb */ - free(metadata); - - status_update(player_state); - - return; - - no_update: - http_icy_metadata_free(metadata, 0); + worker_execute(metadata_update_cb, &metadata, sizeof(metadata), 0); } + struct player_history * player_history_get(void) { @@ -950,7 +904,7 @@ source_check(void) status_update(PLAY_PLAYING); - metadata_prune(pos); + outputs_metadata_prune(pos); } return pos; @@ -1101,6 +1055,10 @@ source_read(uint8_t *buf, int len) if (ret < 0) return -1; } + else if (flags & INPUT_FLAG_METADATA) + { + metadata_trigger(0); + } // We pad the output buffer with silence if we don't have enough data for a // full packet and there is no more data coming up (no more tracks in queue) @@ -1429,18 +1387,16 @@ static enum command_state metadata_send(void *arg, int *retval) { union player_arg *cmdarg; - struct player_metadata *pmd; + struct input_metadata *imd; + struct output_metadata *omd; cmdarg = arg; - pmd = cmdarg->pmd; + imd = cmdarg->metadata_param.input; + omd = cmdarg->metadata_param.output; - /* Do the setting of rtptime which was deferred in metadata_trigger because we - * wanted to wait until we had the actual last_rtptime - */ - if ((pmd->rtptime == 0) && (pmd->startup)) - pmd->rtptime = last_rtptime + AIRTUNES_V2_PACKET_SAMPLES; + outputs_metadata_send(omd, imd->rtptime, imd->offset, imd->startup); - outputs_metadata_send(pmd->omd, pmd->rtptime, pmd->offset, pmd->startup); + status_update(player_state); *retval = 0; return COMMAND_END; @@ -1724,7 +1680,7 @@ playback_abort(void) status_update(PLAY_STOPPED); - metadata_purge(); + outputs_metadata_purge(); } /* Internal routine for waiting when input buffer underruns */ @@ -1854,37 +1810,6 @@ now_playing(void *arg, int *retval) return COMMAND_END; } -static enum command_state -artwork_url_get(void *arg, int *retval) -{ - union player_arg *cmdarg = arg; - struct player_source *ps; - - cmdarg->icy.artwork_url = NULL; - - if (cur_playing) - ps = cur_playing; - else if (cur_streaming) - ps = cur_streaming; - else - { - *retval = -1; - return COMMAND_END; - } - - /* Check that we are playing a viable stream, and that it has the requested id */ - if (!ps->xcode || ps->data_kind != DATA_KIND_HTTP || ps->id != cmdarg->icy.id) - { - *retval = -1; - return COMMAND_END; - } - - cmdarg->icy.artwork_url = transcode_metadata_artwork_url(ps->xcode); - - *retval = 0; - return COMMAND_END; -} - static enum command_state playback_stop(void *arg, int *retval) { @@ -1908,7 +1833,7 @@ playback_stop(void *arg, int *retval) status_update(PLAY_STOPPED); - metadata_purge(); + outputs_metadata_purge(); /* We're async if we need to flush devices */ if (*retval > 0) @@ -2343,7 +2268,7 @@ playback_pause(void *arg, int *retval) source_pause(pos); - metadata_purge(); + outputs_metadata_purge(); /* We're async if we need to flush devices */ if (*retval > 0) @@ -2794,25 +2719,6 @@ player_now_playing(uint32_t *id) return ret; } -char * -player_get_icy_artwork_url(uint32_t id) -{ - union player_arg cmdarg; - int ret; - - cmdarg.icy.id = id; - - if (pthread_self() != tid_player) - ret = commands_exec_sync(cmdbase, artwork_url_get, NULL, &cmdarg); - else - artwork_url_get(&cmdarg, &ret); - - if (ret < 0) - return NULL; - else - return cmdarg.icy.artwork_url; -} - /* * Starts/resumes playback * @@ -3057,11 +2963,12 @@ player_device_remove(void *device) /* Thread: worker */ static void -player_metadata_send(struct player_metadata *pmd) +player_metadata_send(struct input_metadata *imd, struct output_metadata *omd) { union player_arg cmdarg; - cmdarg.pmd = pmd; + cmdarg.metadata_param.input = imd; + cmdarg.metadata_param.output = omd; commands_exec_sync(cmdbase, metadata_send, NULL, &cmdarg); } diff --git a/src/player.h b/src/player.h index ad1c4951..e1d8e2cf 100644 --- a/src/player.h +++ b/src/player.h @@ -80,9 +80,6 @@ player_get_status(struct player_status *status); int player_now_playing(uint32_t *id); -char * -player_get_icy_artwork_url(uint32_t id); - void player_speaker_enumerate(spk_enum_cb cb, void *arg); From 8b5cac053855fea1f518474d9328aee5f762bd53 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Sun, 22 Jan 2017 23:23:18 +0100 Subject: [PATCH 26/37] [file/http/input] Implement metadata handling through input interface --- src/inputs/file_http.c | 37 ++++++++++++++++++++++++++++++++-- src/transcode.c | 45 +++++++++++++++--------------------------- src/transcode.h | 3 --- 3 files changed, 51 insertions(+), 34 deletions(-) diff --git a/src/inputs/file_http.c b/src/inputs/file_http.c index 956982b5..5546508d 100644 --- a/src/inputs/file_http.c +++ b/src/inputs/file_http.c @@ -40,7 +40,7 @@ setup(struct player_source *ps) } static int -http_setup(struct player_source *ps) +setup_http(struct player_source *ps) { char *url; @@ -103,6 +103,38 @@ seek(struct player_source *ps, int seek_ms) return transcode_seek(ps->xcode, seek_ms); } +static int +metadata_get_http(struct input_metadata *metadata, struct player_source *ps) +{ + struct http_icy_metadata *m; + int changed; + + m = transcode_metadata(ps->xcode, &changed); + if (!m) + return -1; + + if (!changed) + { + http_icy_metadata_free(m, 0); + return -1; // TODO Perhaps a problem since this prohibits the player updating metadata + } + + if (m->artist) + metadata->artist = m->artist; + // Note we map title to album, because clients should show stream name as titel + if (m->title) + metadata->album = m->title; + if (m->artwork_url) + metadata->artwork_url = m->artwork_url; + + m->artist = NULL; + m->title = NULL; + m->artwork_url = NULL; + + http_icy_metadata_free(m, 0); + return 0; +} + struct input_definition input_file = { .name = "file", @@ -119,7 +151,8 @@ struct input_definition input_http = .name = "http", .type = INPUT_TYPE_HTTP, .disabled = 0, - .setup = http_setup, + .setup = setup_http, .start = start, .stop = stop, + .metadata_get = metadata_get_http, }; diff --git a/src/transcode.c b/src/transcode.c index 6be2447d..8b08e3cc 100644 --- a/src/transcode.c +++ b/src/transcode.c @@ -82,6 +82,9 @@ struct decode_ctx { // Duration (used to make wav header) uint32_t duration; + // Data kind (used to determine if ICY metadata is relevant to look for) + enum data_kind data_kind; + // Contains the most recent packet from av_read_frame // Used for resuming after seek and for freeing correctly // in transcode_decode() @@ -583,7 +586,7 @@ flush_encoder(struct encode_ctx *ctx, unsigned int stream_index) /* --------------------------- INPUT/OUTPUT INIT --------------------------- */ static int -open_input(struct decode_ctx *ctx, enum data_kind data_kind, const char *path, int decode_video) +open_input(struct decode_ctx *ctx, const char *path, int decode_video) { AVDictionary *options; AVCodec *decoder; @@ -600,10 +603,10 @@ open_input(struct decode_ctx *ctx, enum data_kind data_kind, const char *path, i # ifndef HAVE_FFMPEG // Without this, libav is slow to probe some internet streams, which leads to RAOP timeouts - if (data_kind == DATA_KIND_HTTP) + if (ctx->data_kind == DATA_KIND_HTTP) ctx->ifmt_ctx->probesize = 64000; # endif - if (data_kind == DATA_KIND_HTTP) + if (ctx->data_kind == DATA_KIND_HTTP) av_dict_set(&options, "icy", "1", 0); // TODO Newest versions of ffmpeg have timeout and reconnect options we should use @@ -1245,14 +1248,15 @@ transcode_decode_setup(enum data_kind data_kind, const char *path, uint32_t song return NULL; } - if (open_input(ctx, data_kind, path, decode_video) < 0) + ctx->duration = song_length; + ctx->data_kind = data_kind; + + if (open_input(ctx, path, decode_video) < 0) { free(ctx); return NULL; } - ctx->duration = song_length; - av_init_packet(&ctx->packet); return ctx; @@ -1283,7 +1287,8 @@ transcode_encode_setup(struct decode_ctx *src_ctx, enum transcode_profile profil return NULL; } - ctx->icy_interval = METADATA_ICY_INTERVAL * ctx->channels * ctx->byte_depth * ctx->sample_rate; + if (src_ctx->data_kind == DATA_KIND_HTTP) + ctx->icy_interval = METADATA_ICY_INTERVAL * ctx->channels * ctx->byte_depth * ctx->sample_rate; if (profile == XCODE_PCM16_HEADER) { @@ -1637,6 +1642,8 @@ transcode(struct evbuffer *evbuf, int wanted, struct transcode_ctx *ctx, int *ic int processed; int ret; + *icy_timer = 0; + processed = 0; while (processed < wanted) { @@ -1653,7 +1660,8 @@ transcode(struct evbuffer *evbuf, int wanted, struct transcode_ctx *ctx, int *ic } ctx->encode_ctx->total_bytes += processed; - *icy_timer = (ctx->encode_ctx->total_bytes % ctx->encode_ctx->icy_interval < processed); + if (ctx->encode_ctx->icy_interval) + *icy_timer = (ctx->encode_ctx->total_bytes % ctx->encode_ctx->icy_interval < processed); return processed; } @@ -1822,24 +1830,3 @@ transcode_metadata(struct transcode_ctx *ctx, int *changed) return m; } -char * -transcode_metadata_artwork_url(struct transcode_ctx *ctx) -{ - struct http_icy_metadata *m; - char *artwork_url; - - if (!ctx->decode_ctx->ifmt_ctx || !ctx->decode_ctx->ifmt_ctx->filename) - return NULL; - - artwork_url = NULL; - - m = http_icy_metadata_get(ctx->decode_ctx->ifmt_ctx, 1); - if (m && m->artwork_url) - artwork_url = strdup(m->artwork_url); - - if (m) - http_icy_metadata_free(m, 0); - - return artwork_url; -} - diff --git a/src/transcode.h b/src/transcode.h index e103fc96..3a8614f0 100644 --- a/src/transcode.h +++ b/src/transcode.h @@ -100,7 +100,4 @@ transcode_seek(struct transcode_ctx *ctx, int ms); struct http_icy_metadata * transcode_metadata(struct transcode_ctx *ctx, int *changed); -char * -transcode_metadata_artwork_url(struct transcode_ctx *ctx); - #endif /* !__TRANSCODE_H__ */ From ae1f2d75d3040fc842266826f1429deea23fa61c Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Sun, 22 Jan 2017 23:24:47 +0100 Subject: [PATCH 27/37] [pipe] Prepare reading and parsing Shairport metadata pipes --- src/inputs/pipe.c | 96 +++++++++++++++++++++++++++++++++++++---------- 1 file changed, 77 insertions(+), 19 deletions(-) diff --git a/src/inputs/pipe.c b/src/inputs/pipe.c index e636f9f1..94dc9fe2 100644 --- a/src/inputs/pipe.c +++ b/src/inputs/pipe.c @@ -57,8 +57,10 @@ // Maximum number of pipes to watch for data #define PIPE_MAX_WATCH 4 -// Max number of bytes to read from a pipe at a time ( +// Max number of bytes to read from a pipe at a time #define PIPE_READ_MAX 65536 +// Max number of bytes to buffer from metadata pipes +#define PIPE_METADATA_BUFLEN_MAX 262144 extern struct event_base *evbase_worker; @@ -99,12 +101,18 @@ static struct pipe *pipelist; // Single pipe that we start watching for metadata after playback starts static struct pipe *pipe_metadata; +// We read metadata into this evbuffer +static struct evbuffer *pipe_metadata_buf; +// True if there is new metadata to push to the player +static bool pipe_metadata_is_new; /* ------------------------------- FORWARDS ------------------------------- */ static void pipe_watch_reset(struct pipe *pipe); +static void +pipe_metadata_watch_del(void *arg); /* -------------------------------- HELPERS ------------------------------- */ @@ -175,6 +183,23 @@ pipe_find(int id) return NULL; } +static int +pipe_metadata_parse(struct evbuffer *evbuf) +{ + char *line; + size_t size; + + while ( (line = evbuffer_readln(evbuf, &size, EVBUFFER_EOL_CRLF)) ) + { + DPRINTF(E_DBG, L_PLAYER, "Parsing %s\n", line); + + free(line); + } + + evbuffer_drain(evbuf, evbuffer_get_length(evbuf)); + + return 0; +} /* ---------------------------- GENERAL PIPE I/O -------------------------- */ /* Thread: worker */ @@ -388,30 +413,55 @@ pipe_listener_cb(enum listener_event_type type) static void pipe_metadata_read_cb(evutil_socket_t fd, short event, void *arg) { - struct evbuffer *evbuf; + struct evbuffer_ptr evptr; int ret; DPRINTF(E_DBG, L_PLAYER, "BANG\n"); - evbuf = evbuffer_new(); - ret = evbuffer_read(evbuf, pipe_metadata->fd, PIPE_READ_MAX); - evbuffer_free(evbuf); + 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; + } - event_add(pipe_metadata->ev, NULL); -/* - else if (ret < 0) - give up - goto out; - if (ret == 0) - pipe_reset(pipe_metadata); - evbuffer_add_printf(evbuf, "\n"); - DPRINTF(E_DBG, L_PLAYER, "Got some pipe metadata\n%s", (char *)evbuffer_pullup(evbuf, -1)); + 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; + } - event_add(pipe_metadata->ev, NULL); + // Did we get the end tag? If not return to wait for more data + evptr = evbuffer_search(pipe_metadata_buf, "", strlen(""), NULL); + if (evptr.pos < 0) + { + DPRINTF(E_DBG, L_PLAYER, "Incomplete\n"); + goto readd; + } - out: - evbuffer_free(evbuf); - pipe_reset(pipe_metadata);*/ + // NULL-terminate the buffer + evbuffer_add(pipe_metadata_buf, "", 1); + + ret = pipe_metadata_parse(pipe_metadata_buf); + if (ret < 0) + { + pipe_metadata_watch_del(NULL); + return; + } + + // Trigger notification in playback loop + pipe_metadata_is_new = 1; + + readd: + if (pipe_metadata && pipe_metadata->ev) + event_add(pipe_metadata->ev, NULL); } static void @@ -429,9 +479,12 @@ pipe_metadata_watch_add(void *arg) if (!pipe_metadata) return; + pipe_metadata_buf = evbuffer_new(); + ret = pipe_watch_add(pipe_metadata); if (ret < 0) { + evbuffer_free(pipe_metadata_buf); pipe_free(pipe_metadata); pipe_metadata = NULL; return; @@ -444,6 +497,7 @@ pipe_metadata_watch_del(void *arg) if (!pipe_metadata) return; + evbuffer_free(pipe_metadata_buf); pipe_watch_del(pipe_metadata); pipe_free(pipe_metadata); pipe_metadata = NULL; @@ -492,6 +546,7 @@ start(struct player_source *ps) { struct pipe *pipe; struct evbuffer *evbuf; + short flags; int ret; pipe = pipe_find(ps->id); @@ -525,7 +580,10 @@ start(struct player_source *ps) break; } - ret = input_write(evbuf, 0); + flags = (pipe_metadata_is_new ? INPUT_FLAG_METADATA : 0); + pipe_metadata_is_new = 0; + + ret = input_write(evbuf, flags); if (ret < 0) break; } From ab06a9fd7d5a83934fa8541419d181b491b85d06 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Wed, 25 Jan 2017 22:25:30 +0100 Subject: [PATCH 28/37] [player] Update info about player.c --- src/player.c | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/player.c b/src/player.c index c3dee8f5..d3e6f3ec 100644 --- a/src/player.c +++ b/src/player.c @@ -37,15 +37,18 @@ * About metadata * -------------- * The player gets metadata from library + inputs and passes it to the outputs - * and other clients (e.g. Remotes). Text metadata is handled differently than - * artwork. Here's how text works: + * and other clients (e.g. Remotes). * - * 1. On playback start, the player will TODO + * 1. On playback start, metadata from the library is loaded into the queue + * items, and these items are then the source of metadata for clients. * 2. During playback, the input may signal new metadata by making a * input_write() with the INPUT_FLAG_METADATA flag. When the player read * reaches that data, the player will request the metadata from the input - * with input_metadata_get(). - * 3. If the new metadata is different than the TODO + * with input_metadata_get(). This metadata is then saved to the currently + * playing queue item, and the clients are told to update metadata. + * 3. Artwork works differently than textual metadata. The artwork module will + * look for artwork in the library, and addition also check the artwork_url + * of the queue_item. */ #ifdef HAVE_CONFIG_H From c975cf44748be9cba358f90c84174ed7e0fa403a Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Fri, 27 Jan 2017 10:59:34 +0100 Subject: [PATCH 29/37] [misc] Set output of safe_xxx to 0 also in error cases (for safety) --- src/misc.c | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/misc.c b/src/misc.c index 101c6472..94d9f509 100644 --- a/src/misc.c +++ b/src/misc.c @@ -48,6 +48,8 @@ safe_atoi32(const char *str, int32_t *val) char *end; long intval; + *val = 0; + errno = 0; intval = strtol(str, &end, 10); @@ -84,6 +86,8 @@ safe_atou32(const char *str, uint32_t *val) char *end; unsigned long intval; + *val = 0; + errno = 0; intval = strtoul(str, &end, 10); @@ -120,6 +124,8 @@ safe_hextou32(const char *str, uint32_t *val) char *end; unsigned long intval; + *val = 0; + /* A hex shall begin with 0x */ if (strncmp(str, "0x", 2) != 0) return safe_atou32(str, val); @@ -160,6 +166,8 @@ safe_atoi64(const char *str, int64_t *val) char *end; long long intval; + *val = 0; + errno = 0; intval = strtoll(str, &end, 10); @@ -196,6 +204,8 @@ safe_atou64(const char *str, uint64_t *val) char *end; unsigned long long intval; + *val = 0; + errno = 0; intval = strtoull(str, &end, 10); @@ -232,6 +242,8 @@ safe_hextou64(const char *str, uint64_t *val) char *end; unsigned long long intval; + *val = 0; + errno = 0; intval = strtoull(str, &end, 16); From 67d05047000c9d8f65504006c5c8ba166458310c Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Fri, 27 Jan 2017 11:00:29 +0100 Subject: [PATCH 30/37] [input] Let input_metadata_free zero metadata like the other free functions --- src/input.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/input.c b/src/input.c index dee17b33..362d8da0 100644 --- a/src/input.c +++ b/src/input.c @@ -503,6 +503,8 @@ input_metadata_free(struct input_metadata *metadata, int content_only) if (!content_only) free(metadata); + else + memset(metadata, 0, sizeof(struct input_metadata)); } int From ea874154b2a946451de872fe1fc52ea006bcdb52 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Fri, 27 Jan 2017 11:02:10 +0100 Subject: [PATCH 31/37] [input/file_http] Use swap_pointers to transfer ownership of metadata --- src/inputs/file_http.c | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/inputs/file_http.c b/src/inputs/file_http.c index 5546508d..f4494e04 100644 --- a/src/inputs/file_http.c +++ b/src/inputs/file_http.c @@ -25,6 +25,7 @@ #include "transcode.h" #include "http.h" +#include "misc.h" #include "input.h" static int @@ -120,16 +121,12 @@ metadata_get_http(struct input_metadata *metadata, struct player_source *ps) } if (m->artist) - metadata->artist = m->artist; + swap_pointers(&metadata->artist, &m->artist); // Note we map title to album, because clients should show stream name as titel if (m->title) - metadata->album = m->title; + swap_pointers(&metadata->album, &m->title); if (m->artwork_url) - metadata->artwork_url = m->artwork_url; - - m->artist = NULL; - m->title = NULL; - m->artwork_url = NULL; + swap_pointers(&metadata->artwork_url, &m->artwork_url); http_icy_metadata_free(m, 0); return 0; From dc842943488dbd54ef292d0f1ee92ea87a417f0c Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Fri, 27 Jan 2017 11:05:24 +0100 Subject: [PATCH 32/37] [input/pipe] Parse basic Shairport metadata using mxml --- src/input.c | 5 +- src/input.h | 8 +- src/inputs/file_http.c | 2 +- src/inputs/pipe.c | 182 +++++++++++++++++++++++++++++++++++++---- src/player.c | 6 +- 5 files changed, 182 insertions(+), 21 deletions(-) diff --git a/src/input.c b/src/input.c index 362d8da0..4bd5aead 100644 --- a/src/input.c +++ b/src/input.c @@ -464,7 +464,7 @@ input_flush(short *flags) } int -input_metadata_get(struct input_metadata *metadata, struct player_source *ps, int startup) +input_metadata_get(struct input_metadata *metadata, struct player_source *ps, int startup, uint64_t rtptime) { int type; @@ -490,7 +490,7 @@ input_metadata_get(struct input_metadata *metadata, struct player_source *ps, in if (!inputs[type]->metadata_get) return 0; - return inputs[type]->metadata_get(metadata, ps); + return inputs[type]->metadata_get(metadata, ps, rtptime); } void @@ -499,6 +499,7 @@ input_metadata_free(struct input_metadata *metadata, int content_only) free(metadata->artist); free(metadata->title); free(metadata->album); + free(metadata->genre); free(metadata->artwork_url); if (!content_only) diff --git a/src/input.h b/src/input.h index b828a03a..1e81b058 100644 --- a/src/input.h +++ b/src/input.h @@ -82,9 +82,13 @@ struct input_metadata uint64_t rtptime; uint64_t offset; + // The player will update queue_item with the below + uint32_t song_length; + char *artist; char *title; char *album; + char *genre; char *artwork_url; }; @@ -112,7 +116,7 @@ struct input_definition int (*seek)(struct player_source *ps, int seek_ms); // Return metadata - int (*metadata_get)(struct input_metadata *metadata, struct player_source *ps); + int (*metadata_get)(struct input_metadata *metadata, struct player_source *ps, uint64_t rtptime); // Initialization function called during startup int (*init)(void); @@ -214,7 +218,7 @@ input_flush(short *flags); * Gets metadata from the input, returns 0 if metadata is set, otherwise -1 */ int -input_metadata_get(struct input_metadata *metadata, struct player_source *ps, int startup); +input_metadata_get(struct input_metadata *metadata, struct player_source *ps, int startup, uint64_t rtptime); /* * Free the entire struct diff --git a/src/inputs/file_http.c b/src/inputs/file_http.c index f4494e04..5bda057d 100644 --- a/src/inputs/file_http.c +++ b/src/inputs/file_http.c @@ -105,7 +105,7 @@ seek(struct player_source *ps, int seek_ms) } static int -metadata_get_http(struct input_metadata *metadata, struct player_source *ps) +metadata_get_http(struct input_metadata *metadata, struct player_source *ps, uint64_t rtptime) { struct http_icy_metadata *m; int changed; diff --git a/src/inputs/pipe.c b/src/inputs/pipe.c index 94dc9fe2..8707fdf4 100644 --- a/src/inputs/pipe.c +++ b/src/inputs/pipe.c @@ -45,6 +45,7 @@ #include #include +#include #include "misc.h" #include "logger.h" @@ -103,6 +104,8 @@ static struct pipe *pipelist; static struct pipe *pipe_metadata; // We read metadata into this evbuffer static struct evbuffer *pipe_metadata_buf; +// Parsed metadata goes here +static struct input_metadata pipe_metadata_parsed; // True if there is new metadata to push to the player static bool pipe_metadata_is_new; @@ -183,22 +186,143 @@ pipe_find(int id) return NULL; } -static int -pipe_metadata_parse(struct evbuffer *evbuf) +// Convert to macro? +static inline uint32_t +dmapval(const char s[4]) { - char *line; - size_t size; + return ((s[0] << 24) | (s[1] << 16) | (s[2] << 8) | (s[3] << 0)); +} - while ( (line = evbuffer_readln(evbuf, &size, EVBUFFER_EOL_CRLF)) ) +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 +static int +parse_item(struct input_metadata *m, mxml_node_t *item) +{ + 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_DBG, L_PLAYER, "Parsing %s\n", line); + DPRINTF(E_WARN, L_PLAYER, "No type (%d) or code (%d) in pipe metadata\n", type, code); + return -1; + } - free(line); + 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; + + if ( (needle = mxmlFindElement(item, item, "data", NULL, NULL, MXML_DESCEND)) && + (s = mxmlGetText(needle, NULL)) ) + { + 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; + + 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; } evbuffer_drain(evbuf, evbuffer_get_length(evbuf)); - return 0; +// 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; } /* ---------------------------- GENERAL PIPE I/O -------------------------- */ @@ -416,8 +540,6 @@ pipe_metadata_read_cb(evutil_socket_t fd, short event, void *arg) struct evbuffer_ptr evptr; int ret; - DPRINTF(E_DBG, L_PLAYER, "BANG\n"); - ret = evbuffer_read(pipe_metadata_buf, pipe_metadata->fd, PIPE_READ_MAX); if (ret < 0) { @@ -442,22 +564,23 @@ pipe_metadata_read_cb(evutil_socket_t fd, short event, void *arg) evptr = evbuffer_search(pipe_metadata_buf, "", strlen(""), NULL); if (evptr.pos < 0) { - DPRINTF(E_DBG, L_PLAYER, "Incomplete\n"); goto readd; } // NULL-terminate the buffer evbuffer_add(pipe_metadata_buf, "", 1); - ret = pipe_metadata_parse(pipe_metadata_buf); + ret = pipe_metadata_parse(&pipe_metadata_parsed, pipe_metadata_buf); if (ret < 0) { pipe_metadata_watch_del(NULL); return; } - - // Trigger notification in playback loop - pipe_metadata_is_new = 1; + else if (ret > 0) + { + // Trigger notification in playback loop + pipe_metadata_is_new = 1; + } readd: if (pipe_metadata && pipe_metadata->ev) @@ -629,6 +752,34 @@ stop(struct player_source *ps) return 0; } +// 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; +} + // Thread: main static int init(void) @@ -665,6 +816,7 @@ struct input_definition input_pipe = .setup = setup, .start = start, .stop = stop, + .metadata_get = metadata_get, .init = init, .deinit = deinit, }; diff --git a/src/player.c b/src/player.c index d3e6f3ec..4dac3739 100644 --- a/src/player.c +++ b/src/player.c @@ -488,8 +488,12 @@ metadata_update_cb(void *arg) swap_pointers(&queue_item->title, &metadata->title); if (metadata->album) swap_pointers(&queue_item->album, &metadata->album); + if (metadata->genre) + swap_pointers(&queue_item->genre, &metadata->genre); if (metadata->artwork_url) swap_pointers(&queue_item->artwork_url, &metadata->artwork_url); + if (metadata->song_length) + queue_item->song_length = metadata->song_length; ret = db_queue_update_item(queue_item); if (ret < 0) @@ -520,7 +524,7 @@ metadata_trigger(int startup) struct input_metadata metadata; int ret; - ret = input_metadata_get(&metadata, cur_streaming, startup); + ret = input_metadata_get(&metadata, cur_streaming, startup, last_rtptime + AIRTUNES_V2_PACKET_SAMPLES); if (ret < 0) return; From 97aa54494521c3b1d6fc97a8fda84db0da7d1427 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Fri, 27 Jan 2017 23:23:25 +0100 Subject: [PATCH 33/37] [library] Move library update trigger to new library module --- src/db.c | 2 +- src/library.c | 80 ++++++++++++++++++++++++--------------- src/library.h | 3 ++ src/library/filescanner.c | 43 --------------------- src/library/filescanner.h | 3 -- 5 files changed, 54 insertions(+), 77 deletions(-) diff --git a/src/db.c b/src/db.c index e72d4f60..a810cd83 100644 --- a/src/db.c +++ b/src/db.c @@ -41,7 +41,7 @@ #include "logger.h" #include "cache.h" #include "listener.h" -#include "filescanner.h" +#include "library.h" #include "misc.h" #include "db.h" #include "db_init.h" diff --git a/src/library.c b/src/library.c index a536b7f5..5f686772 100644 --- a/src/library.c +++ b/src/library.c @@ -21,10 +21,7 @@ # include #endif -#include "library.h" - #include -#include #include #include #include @@ -38,6 +35,9 @@ #include #include +#include + +#include "library.h" #include "cache.h" #include "commands.h" #include "conffile.h" @@ -45,20 +45,14 @@ #include "library/filescanner.h" #include "logger.h" #include "misc.h" +#include "listener.h" #include "player.h" - static struct commands_base *cmdbase; static pthread_t tid_library; struct event_base *evbase_lib; -/* Flag for aborting scan on exit */ -static bool scan_exit; - -/* Flag for scan in progress */ -static bool scanning; - extern struct library_source filescanner; #ifdef HAVE_SPOTIFY_H extern struct library_source spotifyscanner; @@ -72,6 +66,19 @@ static struct library_source *sources[] = { NULL }; +/* Flag for aborting scan on exit */ +static bool scan_exit; + +/* Flag for scan in progress */ +static bool scanning; + +// After being told by db that the library was updated through update_trigger(), +// wait 60 seconds before notifying listeners of LISTENER_DATABASE. This is to +// avoid bombarding the listeners while there are many db updates, and to make +// sure they only get a single update (useful for the cache). +static struct timeval library_update_wait = { 60, 0 }; +static struct event *updateev; + static void sort_tag_create(char **sort_tag, char *src_tag) @@ -630,6 +637,24 @@ fullrescan(void *arg, int *ret) return COMMAND_END; } +static void +update_trigger_cb(int fd, short what, void *arg) +{ + listener_notify(LISTENER_DATABASE); +} + +static enum command_state +update_trigger(void *arg, int *retval) +{ + evtimer_add(updateev, &library_update_wait); + + *retval = 0; + return COMMAND_END; +} + + +/* --------------------------- LIBRARY INTERFACE -------------------------- */ + void library_rescan() { @@ -692,6 +717,8 @@ initscan() DPRINTF(E_LOG, L_LIB, "Library init scan completed in %.f sec\n", difftime(endtime, starttime)); scanning = false; + + listener_notify(LISTENER_DATABASE); } /* @@ -721,6 +748,15 @@ library_is_exiting() return scan_exit; } +void +library_update_trigger(void) +{ + if (scanning) + return; + + commands_exec_async(cmdbase, update_trigger, NULL); +} + /* * Execute the function 'func' with the given argument 'arg' in the library thread. * @@ -786,13 +822,8 @@ library_init(void) scan_exit = false; scanning = false; - evbase_lib = event_base_new(); - if (!evbase_lib) - { - DPRINTF(E_FATAL, L_LIB, "Could not create an event base\n"); - - return -1; - } + CHECK_NULL(L_LIB, evbase_lib = event_base_new()); + CHECK_NULL(L_LIB, updateev = evtimer_new(evbase_lib, update_trigger_cb, NULL)); for (i = 0; sources[i]; i++) { @@ -804,15 +835,9 @@ library_init(void) sources[i]->disabled = 1; } - cmdbase = commands_base_new(evbase_lib, NULL); + CHECK_NULL(L_LIB, cmdbase = commands_base_new(evbase_lib, NULL)); - ret = pthread_create(&tid_library, NULL, library, NULL); - if (ret != 0) - { - DPRINTF(E_FATAL, L_LIB, "Could not spawn library thread: %s\n", strerror(errno)); - - goto thread_fail; - } + CHECK_ERR(L_LIB, pthread_create(&tid_library, NULL, library, NULL)); #if defined(HAVE_PTHREAD_SETNAME_NP) pthread_setname_np(tid_library, "library"); @@ -821,11 +846,6 @@ library_init(void) #endif return 0; - - thread_fail: - event_base_free(evbase_lib); - - return -1; } /* Thread: main */ diff --git a/src/library.h b/src/library.h index e137f520..740c7540 100644 --- a/src/library.h +++ b/src/library.h @@ -86,6 +86,9 @@ library_set_scanning(bool is_scanning); bool library_is_exiting(); +void +library_update_trigger(void); + int library_exec_async(command_function func, void *arg); diff --git a/src/library/filescanner.c b/src/library/filescanner.c index 465345f1..e8a7a5bb 100644 --- a/src/library/filescanner.c +++ b/src/library/filescanner.c @@ -61,7 +61,6 @@ #include "player.h" #include "cache.h" #include "artwork.h" -#include "listener.h" #include "commands.h" #include "library.h" @@ -108,16 +107,9 @@ struct stacked_dir { static int inofd; static struct event *inoev; -static struct event *updateev; static struct deferred_pl *playlists; static struct stacked_dir *dirstack; -// After being told by db that the library was updated through update_trigger(), -// wait 60 seconds before notifying listeners of LISTENER_DATABASE. This is to -// avoid bombarding the listeners while there are many db updates, and to make -// sure they only get a single update (useful for the cache). -static struct timeval library_update_wait = { 60, 0 }; - /* From library.c */ extern struct event_base *evbase_lib; @@ -848,8 +840,6 @@ bulk_scan(int flags) { DPRINTF(E_LOG, L_SCAN, "Bulk library scan completed in %.f sec\n", difftime(end, start)); } - - listener_notify(LISTENER_DATABASE); // TODO Move to library.c } static int @@ -1478,21 +1468,6 @@ filescanner_initscan() return 0; } -static void -update_trigger_cb(int fd, short what, void *arg) -{ - listener_notify(LISTENER_DATABASE); -} - -static enum command_state -update_trigger(void *arg, int *retval) -{ - evtimer_add(updateev, &library_update_wait); - - *retval = 0; - return COMMAND_END; -} - static int filescanner_rescan() { @@ -1518,16 +1493,6 @@ filescanner_fullrescan() return 0; } -// TODO Move to abstraction -void -library_update_trigger(void) -{ - if (scanning) - return; - - commands_exec_async(cmdbase, update_trigger, NULL); -} - /* Thread: main */ static int filescanner_init(void) @@ -1540,14 +1505,6 @@ filescanner_init(void) return -1; } - updateev = evtimer_new(evbase_lib, update_trigger_cb, NULL); - if (!updateev) - { - DPRINTF(E_FATAL, L_SCAN, "Could not create library update event\n"); - close(inofd); - return -1; - } - return 0; } diff --git a/src/library/filescanner.h b/src/library/filescanner.h index c15c655c..1acdf61e 100644 --- a/src/library/filescanner.h +++ b/src/library/filescanner.h @@ -31,7 +31,4 @@ void scan_itunes_itml(char *file); #endif -void -library_update_trigger(void); // TODO Move to library abstraction - #endif /* !__FILESCANNER_H__ */ From b6f969d96ef094398cd5f0e6de9e0389a64b0d0c Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Sat, 28 Jan 2017 00:16:33 +0100 Subject: [PATCH 34/37] [pipe/mxml] Add compability with older versions of mxml --- configure.ac | 2 +- src/Makefile.am | 2 +- src/inputs/pipe.c | 3 ++- src/lastfm.c | 20 ++-------------- src/mxml-compat.h | 58 +++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 64 insertions(+), 21 deletions(-) create mode 100644 src/mxml-compat.h diff --git a/configure.ac b/configure.ac index 12014472..ecb6000e 100644 --- a/configure.ac +++ b/configure.ac @@ -110,7 +110,7 @@ FORK_FUNC_REQUIRE([COMMON], [GNU libunistring], [LIBUNISTRING], [unistring], FORK_MODULES_CHECK([FORKED], [ZLIB], [zlib], [deflate], [zlib.h]) FORK_MODULES_CHECK([FORKED], [CONFUSE], [libconfuse], [cfg_init], [confuse.h]) FORK_MODULES_CHECK([FORKED], [MINIXML], [mxml], [mxmlNewElement], [mxml.h], - [AC_CHECK_FUNCS([mxmlGetOpaque])]) + [AC_CHECK_FUNCS([mxmlGetOpaque] [mxmlGetText] [mxmlGetType] [mxmlGetFirstChild])]) dnl SQLite3 requires extra checks FORK_MODULES_CHECK([COMMON], [SQLITE3], [sqlite3 >= 3.5.0], diff --git a/src/Makefile.am b/src/Makefile.am index 1906b50f..a9eb2e4b 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -118,7 +118,7 @@ forked_daapd_SOURCES = main.c \ $(MPD_SRC) \ listener.c listener.h \ commands.c commands.h \ - ffmpeg-compat.h \ + ffmpeg-compat.h mxml-compat.h \ $(GPERF_SRC) \ $(ANTLR_SRC) diff --git a/src/inputs/pipe.c b/src/inputs/pipe.c index 8707fdf4..f180fe11 100644 --- a/src/inputs/pipe.c +++ b/src/inputs/pipe.c @@ -47,6 +47,7 @@ #include #include +#include "input.h" #include "misc.h" #include "logger.h" #include "db.h" @@ -54,7 +55,7 @@ #include "listener.h" #include "player.h" #include "worker.h" -#include "input.h" +#include "mxml-compat.h" // Maximum number of pipes to watch for data #define PIPE_MAX_WATCH 4 diff --git a/src/lastfm.c b/src/lastfm.c index 1d81cd39..cc8fc077 100644 --- a/src/lastfm.c +++ b/src/lastfm.c @@ -35,6 +35,8 @@ #include #include +#include "mxml-compat.h" + #include "db.h" #include "lastfm.h" #include "logger.h" @@ -215,24 +217,6 @@ param_sign(struct keyval *kv) return ret; } -/* For compability with mxml 2.6 */ -#ifndef HAVE_MXMLGETOPAQUE -const char * /* O - Opaque string or NULL */ -mxmlGetOpaque(mxml_node_t *node) /* I - Node to get */ -{ - if (!node) - return (NULL); - - if (node->type == MXML_OPAQUE) - return (node->value.opaque); - else if (node->type == MXML_ELEMENT && - node->child && - node->child->type == MXML_OPAQUE) - return (node->child->value.opaque); - else - return (NULL); -} -#endif /* --------------------------------- MAIN --------------------------------- */ diff --git a/src/mxml-compat.h b/src/mxml-compat.h new file mode 100644 index 00000000..e9e4e9e9 --- /dev/null +++ b/src/mxml-compat.h @@ -0,0 +1,58 @@ +#ifndef __MXML_COMPAT_H__ +#define __MXML_COMPAT_H__ + +/* For compability with mxml 2.6 */ +#ifndef HAVE_MXMLGETTEXT +static const char * /* O - Text string or NULL */ +mxmlGetText(mxml_node_t *node, /* I - Node to get */ + int *whitespace) /* O - 1 if string is preceded by whitespace, 0 otherwise */ +{ + if (node->type == MXML_TEXT) + return (node->value.text.string); + else if (node->type == MXML_ELEMENT && + node->child && + node->child->type == MXML_TEXT) + return (node->child->value.text.string); + else + return (NULL); +} +#endif + +#ifndef HAVE_MXMLGETOPAQUE +const char * /* O - Opaque string or NULL */ +mxmlGetOpaque(mxml_node_t *node) /* I - Node to get */ +{ + if (!node) + return (NULL); + + if (node->type == MXML_OPAQUE) + return (node->value.opaque); + else if (node->type == MXML_ELEMENT && + node->child && + node->child->type == MXML_OPAQUE) + return (node->child->value.opaque); + else + return (NULL); +} +#endif + +#ifndef HAVE_MXMLGETFIRSTCHILD +static mxml_node_t * /* O - First child or NULL */ +mxmlGetFirstChild(mxml_node_t *node) /* I - Node to get */ +{ + if (!node || node->type != MXML_ELEMENT) + return (NULL); + + return (node->child); +} +#endif + +#ifndef HAVE_MXMLGETTYPE +static mxml_type_t /* O - Type of node */ +mxmlGetType(mxml_node_t *node) /* I - Node to get */ +{ + return (node->type); +} +#endif + +#endif /* !__MXML_COMPAT_H__ */ From 41c5ef1474e5691f94adba61fe4868b3c793e86b Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Sat, 28 Jan 2017 00:45:44 +0100 Subject: [PATCH 35/37] [compat] Suppress warnings about unused functions --- src/ffmpeg-compat.h | 4 ++-- src/mxml-compat.h | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/ffmpeg-compat.h b/src/ffmpeg-compat.h index e262f39a..3df85ca0 100644 --- a/src/ffmpeg-compat.h +++ b/src/ffmpeg-compat.h @@ -31,7 +31,7 @@ #endif #ifndef HAVE_AV_PACKET_RESCALE_TS -static void +__attribute__((unused)) static void av_packet_rescale_ts(AVPacket *pkt, AVRational src_tb, AVRational dst_tb) { if (pkt->pts != AV_NOPTS_VALUE) @@ -48,7 +48,7 @@ av_packet_rescale_ts(AVPacket *pkt, AVRational src_tb, AVRational dst_tb) #ifndef HAVE_AVFORMAT_ALLOC_OUTPUT_CONTEXT2 # include -static int +__attribute__((unused)) static int avformat_alloc_output_context2(AVFormatContext **avctx, AVOutputFormat *oformat, const char *format, const char *filename) { AVFormatContext *s = avformat_alloc_context(); diff --git a/src/mxml-compat.h b/src/mxml-compat.h index e9e4e9e9..1437ad89 100644 --- a/src/mxml-compat.h +++ b/src/mxml-compat.h @@ -3,7 +3,7 @@ /* For compability with mxml 2.6 */ #ifndef HAVE_MXMLGETTEXT -static const char * /* O - Text string or NULL */ +__attribute__((unused)) static const char * /* O - Text string or NULL */ mxmlGetText(mxml_node_t *node, /* I - Node to get */ int *whitespace) /* O - 1 if string is preceded by whitespace, 0 otherwise */ { @@ -19,7 +19,7 @@ mxmlGetText(mxml_node_t *node, /* I - Node to get */ #endif #ifndef HAVE_MXMLGETOPAQUE -const char * /* O - Opaque string or NULL */ +__attribute__((unused)) static const char * /* O - Opaque string or NULL */ mxmlGetOpaque(mxml_node_t *node) /* I - Node to get */ { if (!node) @@ -37,7 +37,7 @@ mxmlGetOpaque(mxml_node_t *node) /* I - Node to get */ #endif #ifndef HAVE_MXMLGETFIRSTCHILD -static mxml_node_t * /* O - First child or NULL */ +__attribute__((unused)) static mxml_node_t * /* O - First child or NULL */ mxmlGetFirstChild(mxml_node_t *node) /* I - Node to get */ { if (!node || node->type != MXML_ELEMENT) @@ -48,7 +48,7 @@ mxmlGetFirstChild(mxml_node_t *node) /* I - Node to get */ #endif #ifndef HAVE_MXMLGETTYPE -static mxml_type_t /* O - Type of node */ +__attribute__((unused)) static mxml_type_t /* O - Type of node */ mxmlGetType(mxml_node_t *node) /* I - Node to get */ { return (node->type); From 12567d8e93c292c8448c797b3282a17feb8164fc Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Sat, 28 Jan 2017 21:47:30 +0100 Subject: [PATCH 36/37] [pipe] Make Shairport metadata parser work with incomplete reads from pipe --- src/inputs/pipe.c | 126 ++++++++++++++++++++++++++++------------------ 1 file changed, 77 insertions(+), 49 deletions(-) diff --git a/src/inputs/pipe.c b/src/inputs/pipe.c index f180fe11..52e636a0 100644 --- a/src/inputs/pipe.c +++ b/src/inputs/pipe.c @@ -107,6 +107,8 @@ static struct pipe *pipe_metadata; static struct evbuffer *pipe_metadata_buf; // Parsed metadata goes here static struct input_metadata pipe_metadata_parsed; +// Mutex to share the parsed metadata +static pthread_mutex_t pipe_metadata_lock; // True if there is new metadata to push to the player static bool pipe_metadata_is_new; @@ -223,31 +225,48 @@ parse_progress(struct input_metadata *m, char *progress) m->song_length = (end - start) * 10 / 441; // Convert to ms based on 44100 } -// returns 0 on metadata found, otherwise -1 +// returns 1 on metadata found, 0 on nothing, -1 on error static int -parse_item(struct input_metadata *m, mxml_node_t *item) +parse_item(struct input_metadata *m, const char *item) { + mxml_node_t *xml; + mxml_node_t *haystack; mxml_node_t *needle; const char *s; uint32_t type; uint32_t code; char *progress; char **data; + int ret; + + ret = 0; + xml = mxmlNewXML("1.0"); + if (!xml) + return -1; + +// DPRINTF(E_DBG, L_PLAYER, "Parsing %s\n", item); + + haystack = mxmlLoadString(xml, item, MXML_NO_CALLBACK); + if (!haystack) + { + DPRINTF(E_LOG, L_PLAYER, "Could not parse pipe metadata\n"); + goto out_error; + } type = 0; - if ( (needle = mxmlFindElement(item, item, "type", NULL, NULL, MXML_DESCEND)) && + if ( (needle = mxmlFindElement(haystack, haystack, "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)) && + if ( (needle = mxmlFindElement(haystack, haystack, "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; + DPRINTF(E_LOG, L_PLAYER, "No type (%d) or code (%d) in pipe metadata, aborting\n", type, code); + goto out_error; } if (code == dmapval("asal")) @@ -261,11 +280,13 @@ parse_item(struct input_metadata *m, mxml_node_t *item) else if (code == dmapval("prgr")) data = &progress; else - return -1; + goto out_nothing; - if ( (needle = mxmlFindElement(item, item, "data", NULL, NULL, MXML_DESCEND)) && + if ( (needle = mxmlFindElement(haystack, haystack, "data", NULL, NULL, MXML_DESCEND)) && (s = mxmlGetText(needle, NULL)) ) { + pthread_mutex_lock(&pipe_metadata_lock); + if (data != &progress) free(*data); @@ -279,53 +300,64 @@ parse_item(struct input_metadata *m, mxml_node_t *item) free(*data); } - return 0; + pthread_mutex_unlock(&pipe_metadata_lock); + + ret = 1; } + out_nothing: + mxmlDelete(xml); + return ret; + + out_error: + mxmlDelete(xml); return -1; } +static char * +extract_item(struct evbuffer *evbuf) +{ + struct evbuffer_ptr evptr; + size_t size; + char *item; + + evptr = evbuffer_search(evbuf, "", strlen(""), NULL); + if (evptr.pos < 0) + return NULL; + + size = evptr.pos + strlen("") + 1; + item = malloc(size); + if (!item) + return NULL; + + evbuffer_remove(evbuf, item, size - 1); + item[size - 1] = '\0'; + + return item; +} + 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; + char *item; int found; - - xml = mxmlNewXML("1.0"); - if (!xml) - return -1; - - 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; - } - - evbuffer_drain(evbuf, evbuffer_get_length(evbuf)); - -// DPRINTF(E_DBG, L_PLAYER, "Parsing %s\n", s); + int ret; found = 0; - for (item = mxmlGetFirstChild(haystack); item; item = mxmlWalkNext(item, haystack, MXML_NO_DESCEND)) + while ((item = extract_item(evbuf))) { - if (mxmlGetType(item) != 0) - continue; - - if (parse_item(m, item) == 0) + ret = parse_item(m, item); + free(item); + if (ret < 0) + return -1; + if (ret > 0) found = 1; } - mxmlDelete(xml); return found; } + /* ---------------------------- GENERAL PIPE I/O -------------------------- */ /* Thread: worker */ @@ -538,7 +570,6 @@ pipe_listener_cb(enum listener_event_type type) static void pipe_metadata_read_cb(evutil_socket_t fd, short event, void *arg) { - struct evbuffer_ptr evptr; int ret; ret = evbuffer_read(pipe_metadata_buf, pipe_metadata->fd, PIPE_READ_MAX); @@ -561,16 +592,6 @@ pipe_metadata_read_cb(evutil_socket_t fd, short event, void *arg) return; } - // Did we get the end tag? If not return to wait for more data - evptr = evbuffer_search(pipe_metadata_buf, "", strlen(""), NULL); - if (evptr.pos < 0) - { - goto readd; - } - - // NULL-terminate the buffer - evbuffer_add(pipe_metadata_buf, "", 1); - ret = pipe_metadata_parse(&pipe_metadata_parsed, pipe_metadata_buf); if (ret < 0) { @@ -753,10 +774,11 @@ stop(struct player_source *ps) return 0; } -// FIXME Thread safety of pipe_metadata_parsed static int metadata_get(struct input_metadata *metadata, struct player_source *ps, uint64_t rtptime) { + pthread_mutex_lock(&pipe_metadata_lock); + if (pipe_metadata_parsed.artist) swap_pointers(&metadata->artist, &pipe_metadata_parsed.artist); if (pipe_metadata_parsed.title) @@ -778,6 +800,8 @@ metadata_get(struct input_metadata *metadata, struct player_source *ps, uint64_t input_metadata_free(&pipe_metadata_parsed, 1); + pthread_mutex_unlock(&pipe_metadata_lock); + return 0; } @@ -787,6 +811,8 @@ init(void) { pipe_autostart = cfg_getbool(cfg_getsec(cfg, "library"), "pipe_autostart"); + CHECK_ERR(L_PLAYER, mutex_init(&pipe_metadata_lock)); + if (pipe_autostart) return listener_add(pipe_listener_cb, LISTENER_DATABASE); else @@ -805,6 +831,8 @@ deinit(void) pipe_free(pipe); } + CHECK_ERR(L_PLAYER, pthread_mutex_destroy(&pipe_metadata_lock)); + if (pipe_autostart) listener_remove(pipe_listener_cb); } From b44ae55c1d6e3460c05bed2a6dcd1713a9bba727 Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Sat, 28 Jan 2017 22:37:56 +0100 Subject: [PATCH 37/37] Some scan-build fixing up --- src/inputs/pipe.c | 2 +- src/player.c | 4 +++- src/transcode.c | 1 - 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/inputs/pipe.c b/src/inputs/pipe.c index 52e636a0..e5d90fde 100644 --- a/src/inputs/pipe.c +++ b/src/inputs/pipe.c @@ -439,7 +439,7 @@ pipe_enum(void) continue; } - pipe = pipe_new(dbmfi.path, id, PIPE_PCM, pipe_read_cb); + pipe_new(dbmfi.path, id, PIPE_PCM, pipe_read_cb); } db_query_end(&qp); diff --git a/src/player.c b/src/player.c index 4dac3739..7ec14dad 100644 --- a/src/player.c +++ b/src/player.c @@ -1035,6 +1035,7 @@ static int source_read(uint8_t *buf, int len) { int nbytes; + uint32_t item_id; int ret; short flags; @@ -1051,8 +1052,9 @@ source_read(uint8_t *buf, int len) DPRINTF(E_LOG, L_PLAYER, "Error reading source %d\n", cur_streaming->id); nbytes = 0; + item_id = cur_streaming->item_id; ret = source_switch(0); - db_queue_delete_byitemid(cur_streaming->item_id); + db_queue_delete_byitemid(item_id); if (ret < 0) return -1; } diff --git a/src/transcode.c b/src/transcode.c index 8b08e3cc..27e50d7c 100644 --- a/src/transcode.c +++ b/src/transcode.c @@ -1096,7 +1096,6 @@ open_filter(struct filter_ctx *filter_ctx, AVCodecContext *dec_ctx, AVCodecConte if (!buffersrc || !format || !buffersink) { DPRINTF(E_LOG, L_XCODE, "Filtering source, format or sink element not found\n"); - ret = AVERROR_UNKNOWN; goto out_fail; }