diff --git a/src/Makefile.am b/src/Makefile.am index b91edc49..5ef6390f 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -119,7 +119,8 @@ forked_daapd_SOURCES = main.c \ scan-wma.c \ $(SPOTIFY_SRC) $(LASTFM_SRC) \ $(MPD_SRC) \ - $(FLAC_SRC) $(MUSEPACK_SRC) + $(FLAC_SRC) $(MUSEPACK_SRC) \ + listener.c listener.h nodist_forked_daapd_SOURCES = \ $(ANTLR_SOURCES) diff --git a/src/httpd_dacp.c b/src/httpd_dacp.c index d52396c3..446436de 100644 --- a/src/httpd_dacp.c +++ b/src/httpd_dacp.c @@ -46,6 +46,7 @@ #include "dmap_common.h" #include "db.h" #include "player.h" +#include "listener.h" /* httpd event base, from httpd.c */ extern struct event_base *evbase_httpd; @@ -342,10 +343,14 @@ playstatusupdate_cb(int fd, short what, void *arg) /* Thread: player */ static void -dacp_playstatus_update_handler(void) +dacp_playstatus_update_handler(enum listener_event_type type) { int ret; + // Only send status update on player change events + if (type != LISTENER_PLAYER) + return; + #ifdef USE_EVENTFD ret = eventfd_write(update_efd, 1); if (ret < 0) @@ -2423,7 +2428,7 @@ dacp_init(void) event_base_set(evbase_httpd, &updateev); event_add(&updateev, NULL); - player_set_update_handler(dacp_playstatus_update_handler); + listener_add(dacp_playstatus_update_handler, LISTENER_PLAYER); return 0; @@ -2444,7 +2449,7 @@ dacp_deinit(void) struct evhttp_connection *evcon; int i; - player_set_update_handler(NULL); + listener_remove(dacp_playstatus_update_handler); for (i = 0; dacp_handlers[i].handler; i++) regfree(&dacp_handlers[i].preg); diff --git a/src/listener.c b/src/listener.c new file mode 100644 index 00000000..92b575ba --- /dev/null +++ b/src/listener.c @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2015 Christian Meffert + * + * 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 "listener.h" + +struct listener +{ + notify notify_cb; + short events; + struct listener *next; +}; + +struct listener *listener_list = NULL; + +int +listener_add(notify notify_cb, short events) +{ + struct listener *listener; + + listener = (struct listener*)malloc(sizeof(struct listener)); + if (!listener) + { + return -1; + } + listener->notify_cb = notify_cb; + listener->events = events; + listener->next = listener_list; + listener_list = listener; + + return 0; +} + +int +listener_remove(notify notify_cb) +{ + struct listener *listener; + struct listener *prev; + + prev = NULL; + for (listener = listener_list; listener; listener = listener->next) + { + if (listener->notify_cb == notify_cb) + break; + + prev = listener; + } + + if (!listener) + { + return -1; + } + + if (prev) + prev->next = listener->next; + else + listener_list = NULL; + + free(listener); + return 0; +} + +void +listener_notify(enum listener_event_type type) +{ + struct listener *listener; + + listener = listener_list; + while (listener) + { + if (type & listener->events) + listener->notify_cb(type); + listener = listener->next; + } +} diff --git a/src/listener.h b/src/listener.h new file mode 100644 index 00000000..8b493552 --- /dev/null +++ b/src/listener.h @@ -0,0 +1,54 @@ + +#ifndef __LISTENER_H__ +#define __LISTENER_H__ + +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 volume has been changed */ + LISTENER_VOLUME = (1 << 2), + /* A speaker has been enabled or disabled */ + LISTENER_SPEAKER = (1 << 3), + /* Options like repeat, random has been changed */ + LISTENER_OPTIONS = (1 << 4), + /* The library has been modified */ + LISTENER_DATABASE = (1 << 5), +}; + +typedef void (*notify)(enum listener_event_type type); + +/* + * Registers the given callback function to the given event types. + * This function is not thread safe. Listeners must be added once at startup. + * + * @param notify_cb Callback function + * @param events Event mask, one or more of LISTENER_* + * @return 0 on success, -1 on failure + */ +int +listener_add(notify notify_cb, short events); + +/* + * Removes the given callback function + * This function is not thread safe. Listeners must be removed once at shutdown. + * + * @param notify_cb Callback function + * @return 0 on success, -1 if the callback was not registered + */ +int +listener_remove(notify notify_cb); + +/* + * Calls the callback function of the registered listeners listening for the + * given type of event. + * + * @param type The event type, on of the LISTENER_* values + * + */ +void +listener_notify(enum listener_event_type type); + +#endif /* !__LISTENER_H__ */ diff --git a/src/mpd.c b/src/mpd.c index 4c25a2d1..bbac03b5 100644 --- a/src/mpd.c +++ b/src/mpd.c @@ -34,6 +34,11 @@ #include #include +# include +# include +# include +# include + #if defined(HAVE_SYS_EVENTFD_H) && defined(HAVE_EVENTFD) # define USE_EVENTFD # include @@ -47,7 +52,7 @@ #include "db.h" #include "conffile.h" #include "misc.h" -#include "mpd.h" +#include "listener.h" #include "player.h" #include "filescanner.h" @@ -59,6 +64,26 @@ struct event_base *evbase_mpd; static int g_exit_pipe[2]; static struct event *g_exitev; +static int g_cmd_pipe[2]; +static struct event *g_cmdev; + +struct mpd_command; + +typedef int (*cmd_func)(struct mpd_command *cmd); + +struct mpd_command +{ + pthread_mutex_t lck; + pthread_cond_t cond; + + cmd_func func; + + enum listener_event_type arg_evtype; + int nonblock; + + int ret; +}; + #define COMMAND_ARGV_MAX 37 /* MPD error codes (taken from ack.h) */ @@ -121,6 +146,51 @@ free_outputs(struct output *outputs) } } +struct idle_client +{ + struct evbuffer *evbuffer; + short events; + + struct idle_client *next; +}; + +struct idle_client *idle_clients; + +/* ---------------------------- COMMAND EXECUTION -------------------------- */ + +static int +send_command(struct mpd_command *cmd) +{ + int ret; + + if (!cmd->func) + { + DPRINTF(E_LOG, L_MPD, "BUG: cmd->func is NULL!\n"); + return -1; + } + + ret = write(g_cmd_pipe[1], &cmd, sizeof(cmd)); + if (ret != sizeof(cmd)) + { + DPRINTF(E_LOG, L_MPD, "Could not send command: %s\n", strerror(errno)); + return -1; + } + + return 0; +} + +static int +nonblock_command(struct mpd_command *cmd) +{ + int ret; + + ret = send_command(cmd); + if (ret < 0) + return -1; + + return 0; +} + static void thread_exit(void) { @@ -553,13 +623,95 @@ mpd_command_currentsong(struct evbuffer *evbuf, int argc, char **argv, char **er static int mpd_command_idle(struct evbuffer *evbuf, int argc, char **argv, char **errmsg) { - DPRINTF(E_WARN, L_MPD, "Idle command is not supported by forked-daapd, there will be no notifications about changes\n"); + struct idle_client *client; + int i; + + client = (struct idle_client*)malloc(sizeof(struct idle_client)); + if (!client) + { + DPRINTF(E_LOG, L_MPD, "Out of memory for idle_client\n"); + return ACK_ERROR_UNKNOWN; + } + + client->evbuffer = evbuf; + client->events = 0; + client->next = idle_clients; + + if (argc > 1) + { + for (i = 1; i < argc; i++) + { + if (0 == strcmp(argv[i], "database")) + { + client->events |= LISTENER_DATABASE; + } + else if (0 == strcmp(argv[i], "player")) + { + client->events |= LISTENER_PLAYER; + } + else if (0 == strcmp(argv[i], "playlist")) + { + client->events |= LISTENER_PLAYLIST; + } + else if (0 == strcmp(argv[i], "mixer")) + { + client->events |= LISTENER_VOLUME; + } + else if (0 == strcmp(argv[i], "output")) + { + client->events |= LISTENER_SPEAKER; + } + else if (0 == strcmp(argv[i], "options")) + { + client->events |= LISTENER_OPTIONS; + } + else + { + DPRINTF(E_DBG, L_MPD, "Idle command for '%s' not supported\n", argv[i]); + } + } + } + else + client->events = LISTENER_PLAYER | LISTENER_PLAYLIST | LISTENER_VOLUME | LISTENER_SPEAKER | LISTENER_OPTIONS; + + idle_clients = client; + return 0; } +static void +mpd_remove_idle_client(struct evbuffer *evbuf) +{ + struct idle_client *client; + struct idle_client *prev; + + client = idle_clients; + prev = NULL; + + while (client) + { + if (client->evbuffer == evbuf) + { + DPRINTF(E_DBG, L_MPD, "Removing idle client for evbuffer\n"); + + if (prev) + prev->next = client->next; + else + idle_clients = client->next; + + free(client); + break; + } + + prev = client; + client = client->next; + } +} + static int mpd_command_noidle(struct evbuffer *evbuf, int argc, char **argv, char **errmsg) { + mpd_remove_idle_client(evbuf); return 0; } @@ -623,7 +775,7 @@ mpd_command_status(struct evbuffer *evbuf, int argc, char **argv, char **errmsg) status.shuffle, (status.repeat == REPEAT_SONG ? 1 : 0), 0 /* consume: not supported by forked-daapd, always return 'off' */, - status.plid, + status.plversion, status.playlistlength, state); @@ -1648,6 +1800,7 @@ mpd_command_playlistinfo(struct evbuffer *evbuf, int argc, char **argv, char **e static int mpd_command_plchanges(struct evbuffer *evbuf, int argc, char **argv, char **errmsg) { + DPRINTF(E_WARN, L_MPD, "Ignore command %s\n", argv[0]); return 0; } @@ -3697,11 +3850,20 @@ mpd_read_cb(struct bufferevent *bev, void *ctx) static void mpd_event_cb(struct bufferevent *bev, short events, void *ctx) { + struct evbuffer *evbuf; + if (events & BEV_EVENT_ERROR) - DPRINTF(E_LOG, L_MPD, "Error from buffer event\n"); + { + DPRINTF(E_LOG, L_MPD, "Error from bufferevent: %s\n", + evutil_socket_error_to_string(EVUTIL_SOCKET_ERROR())); + } if (events & (BEV_EVENT_EOF | BEV_EVENT_ERROR)) + { + evbuf = bufferevent_get_output(bev); + mpd_remove_idle_client(evbuf); bufferevent_free(bev); + } } /* @@ -3807,6 +3969,147 @@ mpd_accept_error_cb(struct evconnlistener *listener, void *ctx) DPRINTF(E_LOG, L_MPD, "Error occured %d (%s) on the listener.\n", err, evutil_socket_error_to_string(err)); } +static int +mpd_notify_idle_client(struct idle_client *client, enum listener_event_type type) +{ + if (!(client->events & type)) + { + DPRINTF(E_DBG, L_MPD, "Client not listening for event: %d\n", type); + return 1; + } + + switch (type) + { + case LISTENER_PLAYER: + evbuffer_add(client->evbuffer, "changed: player\n", 16); + break; + + case LISTENER_PLAYLIST: + evbuffer_add(client->evbuffer, "changed: playlist\n", 18); + break; + + case LISTENER_VOLUME: + evbuffer_add(client->evbuffer, "changed: mixer\n", 15); + break; + + case LISTENER_SPEAKER: + evbuffer_add(client->evbuffer, "changed: output\n", 16); + break; + + case LISTENER_OPTIONS: + evbuffer_add(client->evbuffer, "changed: options\n", 17); + break; + + default: + DPRINTF(E_WARN, L_MPD, "Unsupported event type (%d) in notify idle clients.\n", type); + return -1; + } + + evbuffer_add(client->evbuffer, "OK\n", 3); + + return 0; +} + +static int +mpd_notify_idle(struct mpd_command *cmd) +{ + struct idle_client *client; + struct idle_client *prev; + struct idle_client *next; + int i; + int ret; + + DPRINTF(E_DBG, L_MPD, "Notify clients waiting for idle results: %d\n", cmd->arg_evtype); + + prev = NULL; + next = NULL; + i = 0; + client = idle_clients; + while (client) + { + DPRINTF(E_DBG, L_MPD, "Notify client #%d\n", i); + + next = client->next; + + ret = mpd_notify_idle_client(client, cmd->arg_evtype); + + if (ret == 0) + { + if (prev) + prev->next = next; + else + idle_clients = next; + + free(client); + } + else + { + prev = client; + } + + client = next; + i++; + } + + return 0; +} + +static void +mpd_listener_cb(enum listener_event_type type) +{ + DPRINTF(E_DBG, L_MPD, "Listener callback called with event type %d.\n", type); + struct mpd_command *cmd; + + cmd = (struct mpd_command *)malloc(sizeof(struct mpd_command)); + if (!cmd) + { + DPRINTF(E_LOG, L_MPD, "Could not allocate cache_command\n"); + return; + } + + memset(cmd, 0, sizeof(struct mpd_command)); + + cmd->nonblock = 1; + + cmd->func = mpd_notify_idle; + cmd->arg_evtype = type; + + nonblock_command(cmd); +} + +static void +command_cb(int fd, short what, void *arg) +{ + struct mpd_command *cmd; + int ret; + + ret = read(g_cmd_pipe[0], &cmd, sizeof(cmd)); + if (ret != sizeof(cmd)) + { + DPRINTF(E_LOG, L_MPD, "Could not read command! (read %d): %s\n", ret, (ret < 0) ? strerror(errno) : "-no error-"); + goto readd; + } + + if (cmd->nonblock) + { + cmd->func(cmd); + + free(cmd); + goto readd; + } + + pthread_mutex_lock(&cmd->lck); + + ret = cmd->func(cmd); + cmd->ret = ret; + + pthread_cond_signal(&cmd->cond); + pthread_mutex_unlock(&cmd->lck); + + readd: + event_add(g_cmdev, NULL); +} + /* Thread: main */ int mpd_init(void) @@ -3841,6 +4144,17 @@ int mpd_init(void) goto exit_fail; } +# if defined(__linux__) + ret = pipe2(g_cmd_pipe, O_CLOEXEC); +# else + ret = pipe(g_cmd_pipe); +# endif + if (ret < 0) + { + DPRINTF(E_LOG, L_MPD, "Could not create command pipe: %s\n", strerror(errno)); + goto cmd_fail; + } + evbase_mpd = event_base_new(); if (!evbase_mpd) { @@ -3857,6 +4171,16 @@ int mpd_init(void) event_add(g_exitev, NULL); + + g_cmdev = event_new(evbase_mpd, g_cmd_pipe[0], EV_READ, command_cb, NULL); + if (!g_cmdev) + { + DPRINTF(E_LOG, L_MPD, "Could not create cmd event\n"); + goto evnew_fail; + } + + event_add(g_cmdev, NULL); + if (v6enabled) { saddr_length = sizeof(sin6); @@ -3902,6 +4226,9 @@ int mpd_init(void) goto thread_fail; } + idle_clients = NULL; + listener_add(mpd_listener_cb, LISTENER_PLAYER | LISTENER_PLAYLIST | LISTENER_VOLUME | LISTENER_SPEAKER | LISTENER_OPTIONS); + return 0; @@ -3912,6 +4239,10 @@ int mpd_init(void) evbase_mpd = NULL; evbase_fail: + close(g_cmd_pipe[0]); + close(g_cmd_pipe[1]); + + cmd_fail: close(g_exit_pipe[0]); close(g_exit_pipe[1]); @@ -3922,6 +4253,7 @@ int mpd_init(void) /* Thread: main */ void mpd_deinit(void) { + struct idle_client *temp; unsigned short port; int ret; @@ -3941,6 +4273,15 @@ void mpd_deinit(void) return; } + listener_remove(mpd_listener_cb); + + while (idle_clients) + { + temp = idle_clients; + idle_clients = idle_clients->next; + free(temp); + } + // Free event base (should free events too) event_base_free(evbase_mpd); diff --git a/src/mpd.h b/src/mpd.h index c5430a40..5df5917a 100644 --- a/src/mpd.h +++ b/src/mpd.h @@ -2,14 +2,6 @@ #ifndef __MPD_H__ #define __MPD_H__ -#ifdef HAVE_LIBEVENT2 -# include -# include -# include -# include -#else -# include -#endif int mpd_init(void); diff --git a/src/player.c b/src/player.c index e4f64f40..f204d069 100644 --- a/src/player.c +++ b/src/player.c @@ -58,6 +58,7 @@ #include "raop.h" #include "laudio.h" #include "worker.h" +#include "listener.h" #ifdef LASTFM # include "lastfm.h" @@ -164,7 +165,6 @@ struct player_command struct player_status *status; struct player_source *ps; struct player_metadata *pmd; - player_status_handler status_handler; uint32_t *id_ptr; uint64_t *raop_ids; enum repeat_mode mode; @@ -207,9 +207,6 @@ static enum play_status player_state; static enum repeat_mode repeat; static char shuffle; -/* Status updates (for DACP) */ -static player_status_handler update_handler; - /* Playback timer */ #if defined(__linux__) static int pb_timer_fd; @@ -259,6 +256,7 @@ static struct player_source *shuffle_head; static struct player_source *cur_playing; static struct player_source *cur_streaming; static uint32_t cur_plid; +static uint32_t cur_plversion; static struct evbuffer *audio_buf; /* Play history */ @@ -299,8 +297,7 @@ status_update(enum play_status status) { player_state = status; - if (update_handler) - update_handler(); + listener_notify(LISTENER_PLAYER); if (status == PLAY_PLAYING) dev_autoselect = 0; @@ -1642,6 +1639,10 @@ source_count() return ret; } +/* + * Updates cur_playing and notifies remotes and raop devices about + * changes. + */ static uint64_t source_check(void) { @@ -1665,6 +1666,7 @@ source_check(void) return 0; } + /* If cur_playing is NULL, we are still in the first two seconds after starting the stream */ if (!cur_playing) { if (pos >= cur_streaming->output_start) @@ -1678,9 +1680,13 @@ source_check(void) return pos; } + /* Check if we are still in the middle of the current playing song */ if ((cur_playing->end == 0) || (pos < cur_playing->end)) return pos; + /* We have reached the end of the current playing song, update cur_playing to the next song in the queue + and initialize stream_start and output_start values. */ + r_mode = repeat; /* Playlist has only one file, treat REPEAT_ALL as REPEAT_SONG */ if ((r_mode == REPEAT_ALL) && (source_head == source_head->pl_next)) @@ -2509,6 +2515,7 @@ get_status(struct player_command *cmd) status->volume = master_volume; status->plid = cur_plid; + status->plversion = cur_plversion; switch (player_state) { @@ -3510,6 +3517,8 @@ speaker_set(struct player_command *cmd) } } + listener_notify(LISTENER_SPEAKER); + if (cmd->raop_pending > 0) return 1; /* async */ @@ -3556,6 +3565,8 @@ volume_set(struct player_command *cmd) cmd->raop_pending += raop_set_volume_one(rd->session, rd->volume, device_command_cb); } + listener_notify(LISTENER_VOLUME); + if (cmd->raop_pending > 0) return 1; /* async */ @@ -3606,6 +3617,8 @@ volume_setrel_speaker(struct player_command *cmd) } } + listener_notify(LISTENER_VOLUME); + if (cmd->raop_pending > 0) return 1; /* async */ @@ -3665,6 +3678,8 @@ volume_setabs_speaker(struct player_command *cmd) } } + listener_notify(LISTENER_VOLUME); + if (cmd->raop_pending > 0) return 1; /* async */ @@ -3674,6 +3689,9 @@ volume_setabs_speaker(struct player_command *cmd) static int repeat_set(struct player_command *cmd) { + if (cmd->arg.mode == repeat) + return 0; + switch (cmd->arg.mode) { case REPEAT_OFF: @@ -3687,6 +3705,8 @@ repeat_set(struct player_command *cmd) return -1; } + listener_notify(LISTENER_OPTIONS); + return 0; } @@ -3708,6 +3728,8 @@ shuffle_set(struct player_command *cmd) return -1; } + listener_notify(LISTENER_OPTIONS); + return 0; } @@ -3851,6 +3873,9 @@ queue_add(struct player_command *cmd) if (cur_plid != 0) cur_plid = 0; + cur_plversion++; + + listener_notify(LISTENER_PLAYLIST); return 0; } @@ -3892,6 +3917,9 @@ queue_add_next(struct player_command *cmd) if (cur_plid != 0) cur_plid = 0; + cur_plversion++; + + listener_notify(LISTENER_PLAYLIST); return 0; } @@ -3960,6 +3988,10 @@ queue_move(struct player_command *cmd) ps_dst->pl_next = ps_src; } + cur_plversion++; + + listener_notify(LISTENER_PLAYLIST); + return 0; } @@ -4025,6 +4057,10 @@ queue_remove(struct player_command *cmd) source_free(ps); + cur_plversion++; + + listener_notify(LISTENER_PLAYLIST); + return 0; } @@ -4050,6 +4086,9 @@ queue_clear(struct player_command *cmd) } cur_plid = 0; + cur_plversion++; + + listener_notify(LISTENER_PLAYLIST); return 0; } @@ -4100,6 +4139,10 @@ queue_empty(struct player_command *cmd) source_head->shuffle_prev = source_head; } + cur_plversion++; + + listener_notify(LISTENER_PLAYLIST); + return 0; } @@ -4114,14 +4157,6 @@ queue_plid(struct player_command *cmd) return 0; } -static int -set_update_handler(struct player_command *cmd) -{ - update_handler = cmd->arg.status_handler; - - return 0; -} - /* Command processing */ /* Thread: player */ static void @@ -4810,22 +4845,6 @@ player_queue_plid(uint32_t plid) command_deinit(&cmd); } -void -player_set_update_handler(player_status_handler handler) -{ - struct player_command cmd; - - command_init(&cmd); - - cmd.func = set_update_handler; - cmd.func_bh = NULL; - cmd.arg.status_handler = handler; - - sync_command(&cmd); - - command_deinit(&cmd); -} - /* Non-blocking commands used by mDNS */ static void player_device_add(struct raop_device *rd) @@ -5182,13 +5201,12 @@ player_init(void) cur_playing = NULL; cur_streaming = NULL; cur_plid = 0; + cur_plversion = 0; player_state = PLAY_STOPPED; repeat = REPEAT_OFF; shuffle = 0; - update_handler = NULL; - history = (struct player_history *)calloc(1, sizeof(struct player_history)); /* diff --git a/src/player.h b/src/player.h index 0f14f6b0..eb127484 100644 --- a/src/player.h +++ b/src/player.h @@ -56,6 +56,11 @@ struct player_status { /* Playlist id */ uint32_t plid; + /* Playlist version + After startup plversion is 0 and gets incremented after each change of the playlist + (e. g. after adding/moving/removing items). It is used by mpd clients to recognize if + they need to update the current playlist. */ + uint32_t plversion; /* Playlist length */ uint32_t playlistlength; /* Playing song id*/ @@ -73,7 +78,6 @@ struct player_status { }; typedef void (*spk_enum_cb)(uint64_t id, const char *name, int relvol, struct spk_flags flags, void *arg); -typedef void (*player_status_handler)(void); struct player_source { @@ -229,9 +233,6 @@ player_queue_plid(uint32_t plid); struct player_history * player_history_get(void); -void -player_set_update_handler(player_status_handler handler); - int player_init(void);