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/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 dd7f40fe..a9eb2e4b 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 @@ -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 \ @@ -108,6 +107,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 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) \ @@ -117,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/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 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/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/db.c b/src/db.c index 336d362c..a810cd83 100644 --- a/src/db.c +++ b/src/db.c @@ -41,6 +41,7 @@ #include "logger.h" #include "cache.h" #include "listener.h" +#include "library.h" #include "misc.h" #include "db.h" #include "db_init.h" @@ -355,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); @@ -373,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); @@ -488,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); @@ -509,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); @@ -1446,8 +1399,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 +1433,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 +2364,7 @@ db_file_add(struct media_file_info *mfi) sqlite3_free(query); - cache_daap_trigger(); + library_update_trigger(); return 0; @@ -2483,7 +2443,7 @@ db_file_update(struct media_file_info *mfi) sqlite3_free(query); - cache_daap_trigger(); + library_update_trigger(); return 0; @@ -2491,7 +2451,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; @@ -3944,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)); } /* @@ -3998,7 +3954,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 @@ -4036,30 +3992,7 @@ queue_inc_version_and_notify() db_transaction_end(); - listener_notify(LISTENER_PLAYLIST); -} - -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 + listener_notify(LISTENER_QUEUE); } static int @@ -4094,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 * @@ -4426,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 079e1044..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 * @@ -542,7 +544,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); @@ -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); 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: 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/input.c b/src/input.c new file mode 100644 index 00000000..4bd5aead --- /dev/null +++ b/src/input.c @@ -0,0 +1,578 @@ +/* + * 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) +// How long (in sec) to wait for player read before looping in playback thread +#define INPUT_LOOP_TIMEOUT 1 + +#define DEBUG 1 //TODO disable + +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 + +// Must be in sync with enum input_types +static struct input_definition *inputs[] = { + &input_file, + &input_http, + &input_pipe, +#ifdef HAVE_SPOTIFY_H + &input_spotify, +#endif + 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; + + // 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; +}; + +/* --- Globals --- */ +// Input thread +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 + + +/* ------------------------------ 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; + + case DATA_KIND_PIPE: + return INPUT_TYPE_PIPE; + +#ifdef HAVE_SPOTIFY_H + case DATA_KIND_SPOTIFY: + return INPUT_TYPE_SPOTIFY; +#endif + + 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); +} + +void +input_wait(void) +{ + struct timespec ts; + + pthread_mutex_lock(&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); +} + +// Called by input modules from within the playback loop +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; + } + + ts = timespec_reltoabs(input_loop_timeout); + pthread_cond_timedwait(&input_buffer.cond, &input_buffer.mutex, &ts); + } + + 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(void *data, size_t size, short *flags) +{ + int len; + + *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 + 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)); + debug_elapsed = 0; + } +#endif + + len = evbuffer_remove(input_buffer.evbuf, data, size); + 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; +} + +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) +{ + 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_WARN, L_PLAYER, "Input start called, but playback already running\n"); + return 0; + } + + 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); + + // TODO What if input thread is hanging waiting for source? Kill thread? + 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; + input_buffer.full_cb = NULL; + + pthread_mutex_unlock(&input_buffer.mutex); + +#ifdef DEBUG + DPRINTF(E_DBG, L_PLAYER, "Flush with flags %d\n", *flags); +#endif +} + +int +input_metadata_get(struct input_metadata *metadata, struct player_source *ps, int startup, uint64_t rtptime) +{ + 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, rtptime); +} + +void +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) + free(metadata); + else + memset(metadata, 0, sizeof(struct input_metadata)); +} + +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(); + } + + 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 new file mode 100644 index 00000000..1e81b058 --- /dev/null +++ b/src/input.h @@ -0,0 +1,241 @@ + +#ifndef __INPUT_H__ +#define __INPUT_H__ + +#ifdef HAVE_CONFIG_H +# include +#endif +#include +#include "transcode.h" + +// Must be in sync with inputs[] in input.c +enum input_types +{ + INPUT_TYPE_FILE, + INPUT_TYPE_HTTP, + INPUT_TYPE_PIPE, +#ifdef HAVE_SPOTIFY_H + INPUT_TYPE_SPOTIFY, +#endif +}; + +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; +}; + +typedef int (*input_cb)(void); + +struct input_metadata +{ + uint32_t item_id; + + int startup; + + 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; +}; + +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); + + // Return metadata + int (*metadata_get)(struct input_metadata *metadata, struct player_source *ps, uint64_t rtptime); + + // Initialization function called during startup + int (*init)(void); + + // Deinitialization function called at shutdown + void (*deinit)(void); + +}; + +/* + * Input modules should use this to test if playback should end + */ +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); + +/* + * 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. + * + * @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, -1 on error + */ +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 + */ +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); + +/* + * 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, uint64_t rtptime); + +/* + * 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) + */ +int +input_init(void); + +/* + * Called by player_deinit (so will run in main thread) + */ +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..5bda057d --- /dev/null +++ b/src/inputs/file_http.c @@ -0,0 +1,155 @@ +/* + * 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 "misc.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 +setup_http(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); +} + +static int +metadata_get_http(struct input_metadata *metadata, struct player_source *ps, uint64_t rtptime) +{ + 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) + swap_pointers(&metadata->artist, &m->artist); + // Note we map title to album, because clients should show stream name as titel + if (m->title) + swap_pointers(&metadata->album, &m->title); + if (m->artwork_url) + swap_pointers(&metadata->artwork_url, &m->artwork_url); + + http_icy_metadata_free(m, 0); + return 0; +} + +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 = setup_http, + .start = start, + .stop = stop, + .metadata_get = metadata_get_http, +}; diff --git a/src/inputs/pipe.c b/src/inputs/pipe.c new file mode 100644 index 00000000..e5d90fde --- /dev/null +++ b/src/inputs/pipe.c @@ -0,0 +1,852 @@ +/* + * 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 + * + * + * 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 +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "input.h" +#include "misc.h" +#include "logger.h" +#include "db.h" +#include "conffile.h" +#include "listener.h" +#include "player.h" +#include "worker.h" +#include "mxml-compat.h" + +// 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 +#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; + +enum pipestate +{ + PIPE_NEW, + PIPE_DEL, + PIPE_OPEN, +}; + +enum pipetype +{ + PIPE_PCM, + PIPE_METADATA, +}; + +struct pipe +{ + 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; +}; + +// 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; + +// 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; +// 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; + +/* ------------------------------- FORWARDS ------------------------------- */ + +static void +pipe_watch_reset(struct pipe *pipe); + +static void +pipe_metadata_watch_del(void *arg); + +/* -------------------------------- HELPERS ------------------------------- */ + +static struct pipe * +pipe_new(const char *path, int id, enum pipetype type, event_callback_fn cb) +{ + struct pipe *pipe; + + pipe = calloc(1, sizeof(struct pipe)); + pipe->path = strdup(path); + pipe->id = id; + pipe->fd = -1; + pipe->state = PIPE_NEW; + pipe->type = type; + pipe->cb = cb; + + if (type == PIPE_PCM) + { + pipe->next = pipelist; + pipelist = pipe; + } + + return pipe; +} + +static void +pipe_free(struct pipe *pipe) +{ + free(pipe->path); + 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) +{ + struct pipe *pipe; + + for (pipe = pipelist; pipe; pipe = pipe->next) + { + if (id == pipe->id) + return pipe; + } + + return NULL; +} + +// Convert to macro? +static inline uint32_t +dmapval(const char s[4]) +{ + return ((s[0] << 24) | (s[1] << 16) | (s[2] << 8) | (s[3] << 0)); +} + +static void +parse_progress(struct input_metadata *m, char *progress) +{ + char *s; + char *ptr; + uint64_t start; + uint64_t pos; + uint64_t end; + + if (!(s = strtok_r(progress, "/", &ptr))) + return; + safe_atou64(s, &start); + + if (!(s = strtok_r(NULL, "/", &ptr))) + return; + safe_atou64(s, &pos); + + if (!(s = strtok_r(NULL, "/", &ptr))) + return; + safe_atou64(s, &end); + + if (!start || !pos || !end) + return; + + m->rtptime = start; // Not actually used - we have our own rtptime + m->offset = (pos > start) ? (pos - start) : 0; + m->song_length = (end - start) * 10 / 441; // Convert to ms based on 44100 +} + +// returns 1 on metadata found, 0 on nothing, -1 on error +static int +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(haystack, haystack, "type", NULL, NULL, MXML_DESCEND)) && + (s = mxmlGetText(needle, NULL)) ) + sscanf(s, "%8x", &type); + + code = 0; + if ( (needle = mxmlFindElement(haystack, haystack, "code", NULL, NULL, MXML_DESCEND)) && + (s = mxmlGetText(needle, NULL)) ) + sscanf(s, "%8x", &code); + + if (!type || !code) + { + 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")) + 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 + goto out_nothing; + + 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); + + *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); + } + + 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) +{ + char *item; + int found; + int ret; + + found = 0; + while ((item = extract_item(evbuf))) + { + ret = parse_item(m, item); + free(item); + if (ret < 0) + return -1; + if (ret > 0) + found = 1; + } + + return found; +} + + +/* ---------------------------- 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) +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_new(dbmfi.path, id, PIPE_PCM, pipe_read_cb); + } + + db_query_end(&qp); + + return count; +} + +// Opens a pipe and starts watching it for data if autostart is configured +static int +pipe_open(const char *path, bool silent) +{ + struct stat sb; + int fd; + + DPRINTF(E_DBG, L_PLAYER, "(Re)opening pipe: '%s'\n", path); + + if (lstat(path, &sb) < 0) + { + if (!silent) + 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; + } + + fd = open(path, O_RDONLY | O_NONBLOCK); + if (fd < 0) + { + 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) +{ + if (fd >= 0) + close(fd); +} + +static int +pipe_watch_add(struct pipe *pipe) +{ + bool silent; + + silent = (pipe->type == PIPE_METADATA); + pipe->fd = pipe_open(pipe->path, silent); + if (pipe->fd < 0) + return -1; + + 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; +} + +static void +pipe_watch_del(struct pipe *pipe) +{ + if (pipe->ev) + event_free(pipe->ev); + + pipe_close(pipe->fd); + + 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_watch_reset(struct pipe *pipe) +{ + pipe_watch_del(pipe); + pipe_watch_add(pipe); +} + +static void +pipe_watch_update(void *arg) +{ + struct pipe *pipe; + int count; + + count = pipe_enum(); // Count does not include pipes with state PIPE_DEL + if (count < 0) + return; + + for (pipe = pipelist; pipe; pipe = 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_WATCH)) + DPRINTF(E_LOG, L_PLAYER, "Max open pipes reached, will not watch %s\n", pipe->path); + else if (pipe->state == PIPE_NEW) + pipe_watch_add(pipe); + else if (pipe->state == PIPE_DEL) + pipe_watch_del(pipe); + } + + pipelist_prune(); +} + +// Thread: filescanner +static void +pipe_listener_cb(enum listener_event_type type) +{ + 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) +{ + int ret; + + 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; + } + + 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; + } + + ret = pipe_metadata_parse(&pipe_metadata_parsed, pipe_metadata_buf); + if (ret < 0) + { + pipe_metadata_watch_del(NULL); + return; + } + else if (ret > 0) + { + // 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 +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; + + 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; + } +} + +static void +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; +} + + +/* -------------------------- PIPE INPUT INTERFACE ------------------------ */ +/* Thread: player/input */ + +static int +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) + pipe_new(ps->path, ps->id, PIPE_PCM, NULL); + + pipe = pipe_find(ps->id); + if (!pipe) + { + 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, 0); + if (pipe->fd < 0) + return -1; + pipe->state = PIPE_OPEN; + } + + 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; +} + +static int +start(struct player_source *ps) +{ + struct pipe *pipe; + struct evbuffer *evbuf; + short flags; + 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) && (pipe->is_autostarted)) + { + input_write(evbuf, INPUT_FLAG_EOF); // Autostop + break; + } + else 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; + } + + flags = (pipe_metadata_is_new ? INPUT_FLAG_METADATA : 0); + pipe_metadata_is_new = 0; + + ret = input_write(evbuf, flags); + 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) + { + DPRINTF(E_LOG, L_PLAYER, "Unknown pipe '%s'\n", ps->path); + return -1; + } + + 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; + + return 0; +} + +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) + 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); + + pthread_mutex_unlock(&pipe_metadata_lock); + + return 0; +} + +// Thread: main +static int +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 + return 0; +} + +static void +deinit(void) +{ + struct pipe *pipe; + + for (pipe = pipelist; pipelist; pipe = pipelist) + { + pipelist = pipe->next; + pipe_watch_del(pipe); + pipe_free(pipe); + } + + CHECK_ERR(L_PLAYER, pthread_mutex_destroy(&pipe_metadata_lock)); + + 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, + .metadata_get = metadata_get, + .init = init, + .deinit = deinit, +}; + 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/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/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 f3e5e030..e8a7a5bb 100644 --- a/src/library/filescanner.c +++ b/src/library/filescanner.c @@ -113,7 +113,6 @@ static struct stacked_dir *dirstack; /* From library.c */ extern struct event_base *evbase_lib; - #ifndef __linux__ struct deferred_file { @@ -1501,8 +1500,12 @@ filescanner_init(void) int ret; ret = inofd_event_set(); + if (ret < 0) + { + return -1; + } - return ret; + return 0; } /* Thread: main */ 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/misc.c b/src/misc.c index f624bd4a..94d9f509 100644 --- a/src/misc.c +++ b/src/misc.c @@ -27,6 +27,7 @@ #endif #include +#include #include #include #include @@ -47,6 +48,8 @@ safe_atoi32(const char *str, int32_t *val) char *end; long intval; + *val = 0; + errno = 0; intval = strtol(str, &end, 10); @@ -83,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); @@ -119,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); @@ -159,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); @@ -195,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); @@ -231,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); @@ -568,6 +581,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) { @@ -928,6 +949,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 2a3172b5..685a3e14 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; @@ -77,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); @@ -140,6 +148,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); 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/mxml-compat.h b/src/mxml-compat.h new file mode 100644 index 00000000..1437ad89 --- /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 +__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 */ +{ + 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 +__attribute__((unused)) static 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 +__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) + return (NULL); + + return (node->child); +} +#endif + +#ifndef HAVE_MXMLGETTYPE +__attribute__((unused)) 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__ */ 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) 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" 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 32225ca2..7ec14dad 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,40 @@ * 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. 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). + * + * 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(). 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 @@ -55,18 +89,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 @@ -81,50 +110,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 - -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; -}; +// 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; @@ -137,28 +126,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; @@ -167,13 +146,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; @@ -208,8 +186,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; @@ -233,9 +212,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; @@ -336,6 +315,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) @@ -386,6 +379,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; @@ -410,6 +412,8 @@ pb_timer_stop(void) struct itimerspec tick; int ret; + event_del(pb_timer_ev); + memset(&tick, 0, sizeof(struct itimerspec)); #ifdef HAVE_TIMERFD @@ -432,9 +436,13 @@ static void playback_abort(void); static void -player_metadata_send(struct player_metadata *pmd); +playback_suspend(void); -/* Callback from the worker thread (async operation as it may block) */ +static void +player_metadata_send(struct input_metadata *imd, struct output_metadata *omd); + + +// Callback from the worker thread (async operation as it may block) static void playcount_inc_cb(void *arg) { @@ -444,7 +452,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) { @@ -454,108 +462,76 @@ 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->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; - 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, last_rtptime + AIRTUNES_V2_PACKET_SAMPLES); + 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) { @@ -594,315 +570,6 @@ 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 -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; -} - -/* - * 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 +626,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 +664,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 +701,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 +712,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 +736,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,10 +755,7 @@ source_play() { int ret; - ret = stream_play(cur_streaming); - - ticks_skip = 0; - memset(rawbuf, 0, sizeof(rawbuf)); + ret = input_start(cur_streaming); return ret; } @@ -1116,7 +782,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 +817,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; @@ -1245,7 +911,7 @@ source_check(void) status_update(PLAY_PLAYING); - metadata_prune(pos); + outputs_metadata_prune(pos); } return pos; @@ -1335,88 +1001,90 @@ source_prev() } static int -source_read(uint8_t *buf, int len, uint64_t rtptime) +source_switch(int nbytes) { - int ret; - int nbytes; - char *silence_buf; struct player_source *ps; + int ret; - if (!cur_streaming) - return 0; + DPRINTF(E_DBG, L_PLAYER, "Switching track\n"); - nbytes = 0; - while (nbytes < len) + source_close(last_rtptime + AIRTUNES_V2_PACKET_SAMPLES + BTOS(nbytes) - 1); + + ps = source_next(); + if (!ps) { - if (evbuffer_get_length(audio_buf) == 0) - { - if (cur_streaming) - { - ret = stream_read(cur_streaming, len - nbytes); - } - 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; - } + cur_streaming = NULL; + return 0; // End of queue + } - if (ret <= 0) - { - /* EOF or error */ - source_close(rtptime + BTOS(nbytes) - 1); + ret = source_open(ps, cur_streaming->end + 1, 0); + if (ret < 0) + return -1; - DPRINTF(E_DBG, L_PLAYER, "New file\n"); + ret = source_play(); + if (ret < 0) + return -1; - ps = source_next(); + metadata_trigger(0); - if (ret < 0) - { - DPRINTF(E_LOG, L_PLAYER, "Error reading source %d\n", cur_streaming->id); - db_queue_delete_byitemid(cur_streaming->item_id); - } + return 0; +} - if (ps) - { - ret = source_open(ps, cur_streaming->end + 1, 0); - if (ret < 0) - { - source_free(ps); - return -1; - } +// Returns -1 on error (caller should abort playback), or bytes read (possibly 0) +static int +source_read(uint8_t *buf, int len) +{ + int nbytes; + uint32_t item_id; + int ret; + short flags; - ret = source_play(); - if (ret < 0) - return -1; + // Nothing to read, stream silence until source_check() stops playback + if (!cur_streaming) + { + memset(buf, 0, len); + return len; + } - metadata_trigger(0); - } - else - { - cur_streaming = NULL; - } - continue; - } - } + nbytes = input_read(buf, len, &flags); + if (nbytes < 0) + { + DPRINTF(E_LOG, L_PLAYER, "Error reading source %d\n", cur_streaming->id); - nbytes += evbuffer_remove(audio_buf, buf + nbytes, len - nbytes); + nbytes = 0; + item_id = cur_streaming->item_id; + ret = source_switch(0); + db_queue_delete_byitemid(item_id); + if (ret < 0) + return -1; + } + else if (flags & INPUT_FLAG_EOF) + { + ret = source_switch(nbytes); + 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) + 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(); @@ -1424,23 +1092,37 @@ 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) + { + 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 suspending playback\n"); + playback_suspend(); + return; + } + else + { + DPRINTF(E_SPAM, 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 @@ -1449,8 +1131,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; @@ -1468,55 +1148,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)); @@ -1755,18 +1396,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; @@ -2045,14 +1684,27 @@ playback_abort(void) source_stop(); - evbuffer_drain(audio_buf, evbuffer_get_length(audio_buf)); - if (!clear_queue_on_stop_disabled) db_queue_clear(); status_update(PLAY_STOPPED); - metadata_purge(); + outputs_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 */ @@ -2167,37 +1819,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) { @@ -2219,11 +1840,9 @@ playback_stop(void *arg, int *retval) source_stop(); - evbuffer_drain(audio_buf, evbuffer_get_length(audio_buf)); - status_update(PLAY_STOPPED); - metadata_purge(); + outputs_metadata_purge(); /* We're async if we need to flush devices */ if (*retval > 0) @@ -2265,6 +1884,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; @@ -2359,7 +1981,6 @@ playback_start_item(void *arg, int *retval) return COMMAND_END; } - metadata_trigger(1); /* Start sessions on selected devices */ @@ -2428,14 +2049,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); @@ -2611,8 +2232,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) { @@ -2624,11 +2243,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; @@ -2662,9 +2277,7 @@ playback_pause(void *arg, int *retval) source_pause(pos); - evbuffer_drain(audio_buf, evbuffer_get_length(audio_buf)); - - metadata_purge(); + outputs_metadata_purge(); /* We're async if we need to flush devices */ if (*retval > 0) @@ -3067,7 +2680,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; @@ -3115,40 +2728,18 @@ 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 * - * 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; @@ -3157,14 +2748,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 @@ -3224,7 +2814,6 @@ player_playback_prev(void) return ret; } - void player_speaker_enumerate(spk_enum_cb cb, void *arg) { @@ -3383,11 +2972,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); } @@ -3501,14 +3091,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) { @@ -3528,8 +3110,6 @@ player_init(void) goto evnew_fail; } - event_add(pb_timer_ev, NULL); - cmdbase = commands_base_new(evbase_player, NULL); ret = outputs_init(); @@ -3539,6 +3119,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,14 +3141,14 @@ player_init(void) return 0; thread_fail: + input_deinit(); + input_fail: outputs_deinit(); outputs_fail: commands_base_free(cmdbase); evnew_fail: event_base_free(evbase_player); evbase_fail: - evbuffer_free(audio_buf); - audio_fail: #ifdef HAVE_TIMERFD close(pb_timer_fd); #else @@ -3577,6 +3164,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); @@ -3588,18 +3183,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); } diff --git a/src/player.h b/src/player.h index 000d3c77..e1d8e2cf 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 @@ -85,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); @@ -95,7 +87,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); @@ -115,7 +107,6 @@ player_playback_next(void); int player_playback_prev(void); - int player_volume_set(int vol); diff --git a/src/spotify.c b/src/spotify.c index ce3eba70..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 @@ -55,6 +54,7 @@ #include "cache.h" #include "commands.h" #include "library.h" +#include "input.h" /* TODO for the web api: * - UI should be prettier @@ -79,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 @@ -88,22 +86,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 +96,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 +140,11 @@ 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; +// Timeout timespec +static struct timespec spotify_artwork_timeout = { SPOTIFY_ARTWORK_TIMEOUT, 0 }; + +// Audio buffer +static struct evbuffer *spotify_audio_buffer; /** * The application key is specific to forked-daapd, and allows Spotify @@ -1167,38 +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 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 +1187,6 @@ playback_setup(void *arg, int *retval) return COMMAND_END; } - audio_fifo_flush(); - *retval = 0; return COMMAND_END; } @@ -1330,8 +1275,6 @@ playback_seek(void *arg, int *retval) return COMMAND_END; } - audio_fifo_flush(); - *retval = 0; return COMMAND_END; } @@ -1353,92 +1296,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 +1546,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 +1557,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 +1822,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) @@ -2010,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)); @@ -2613,7 +2448,6 @@ spotify_init(void) event_add(g_notifyev, NULL); - cmdbase = commands_base_new(evbase_spotify, exit_cb); if (!cmdbase) { @@ -2651,17 +2485,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 +2511,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 +2572,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); diff --git a/src/transcode.c b/src/transcode.c index 6be2447d..27e50d7c 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 @@ -1093,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; } @@ -1245,14 +1247,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 +1286,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 +1641,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 +1659,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 +1829,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__ */ 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;