diff --git a/src/Makefile.am b/src/Makefile.am index 822a80f5..821e214d 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -114,6 +114,7 @@ forked_daapd_SOURCES = main.c \ transcode.c transcode.h \ pipe.c pipe.h \ artwork.c artwork.h \ + icy.h icy.c \ misc.c misc.h \ rng.c rng.h \ rsp_query.c rsp_query.h \ diff --git a/src/artwork.c b/src/artwork.c index de8b4110..de96151e 100644 --- a/src/artwork.c +++ b/src/artwork.c @@ -38,6 +38,7 @@ #include "logger.h" #include "conffile.h" #include "cache.h" +#include "player.h" #if LIBAVFORMAT_VERSION_MAJOR >= 53 # include "avio_evbuffer.h" @@ -725,6 +726,127 @@ artwork_get(char *path, int max_w, int max_h, struct evbuffer *evbuf) return ret; } +static int +artwork_get_player_image(char *path, int max_w, int max_h, struct evbuffer *evbuf) +{ + AVFormatContext *src_ctx; + AVPacket pkt; + char *url; + int len; + int s; + int target_w; + int target_h; + int format_ok; + int ret; + + DPRINTF(E_DBG, L_ART, "Trying internet stream artwork in %s\n", path); + + player_icy_artwork_url(&url, path); + if (!url) + return 0; + + len = strlen(url); + if ((len < 14) || (len > PATH_MAX)) // Can't be shorter than http://a/1.jpg + return 0; + + src_ctx = NULL; + ret = avformat_open_input(&src_ctx, url, NULL, NULL); + if (ret < 0) + { + DPRINTF(E_WARN, L_ART, "Cannot open artwork file '%s': %s\n", url, strerror(AVUNERROR(ret))); + + return 0; + } + free(url); + + format_ok = 0; + for (s = 0; s < src_ctx->nb_streams; s++) + { + if (src_ctx->streams[s]->codec->codec_id == AV_CODEC_ID_PNG) + { + format_ok = ART_FMT_PNG; + break; + } + else if (src_ctx->streams[s]->codec->codec_id == AV_CODEC_ID_MJPEG) + { + format_ok = ART_FMT_JPEG; + break; + } + } + + if (s == src_ctx->nb_streams) + { + DPRINTF(E_LOG, L_ART, "Artwork file '%s' not a PNG or JPEG file\n", path); + + avformat_close_input(&src_ctx); + return 0; + } + + ret = 0; + while (av_read_frame(src_ctx, &pkt) == 0) + { + if (pkt.stream_index != s) + { + av_free_packet(&pkt); + continue; + } + + ret = 1; + break; + } + + if (!ret) + { + DPRINTF(E_LOG, L_ART, "Could not read artwork: '%s'\n", path); + + avformat_close_input(&src_ctx); + return 0; + } + + ret = rescale_needed(src_ctx->streams[s]->codec, max_w, max_h, &target_w, &target_h); + + /* Fastpath */ + if (!ret && format_ok) + { + DPRINTF(E_DBG, L_ART, "Artwork not too large, using original image\n"); + + ret = evbuffer_expand(evbuf, pkt.size); + if (ret < 0) + { + DPRINTF(E_LOG, L_ART, "Out of memory for artwork\n"); + + av_free_packet(&pkt); + avformat_close_input(&src_ctx); + return -1; + } + + ret = evbuffer_add(evbuf, pkt.data, pkt.size); + if (ret < 0) + { + DPRINTF(E_LOG, L_ART, "Could not add image to event buffer\n"); + } + else + ret = format_ok; + } + else + { + DPRINTF(E_DBG, L_ART, "Artwork too large, rescaling image\n"); + + ret = artwork_rescale(src_ctx, s, target_w, target_h, evbuf); + } + + av_free_packet(&pkt); + avformat_close_input(&src_ctx); + + if (ret < 0) + { + if (evbuffer_get_length(evbuf) > 0) + evbuffer_drain(evbuf, evbuffer_get_length(evbuf)); + } + + return ret; +} + #if LIBAVFORMAT_VERSION_MAJOR >= 55 || (LIBAVFORMAT_VERSION_MAJOR == 54 && LIBAVFORMAT_VERSION_MINOR >= 6) static int artwork_get_embedded_image(char *path, int max_w, int max_h, struct evbuffer *evbuf) @@ -786,7 +908,7 @@ artwork_get_embedded_image(char *path, int max_w, int max_h, struct evbuffer *ev if (s == src_ctx->nb_streams) { - DPRINTF(E_SPAM, L_ART, "Did not find embedded artwork in '%s'\n", path); + DPRINTF(E_DBG, L_ART, "Did not find embedded artwork in '%s'\n", path); avformat_close_input(&src_ctx); return -1; @@ -1062,6 +1184,9 @@ artwork_get_item_path(char *in_path, int artwork, int max_w, int max_h, char *ou ret = artwork_get_embedded_image(in_path, max_w, max_h, evbuf); break; #endif + case ARTWORK_HTTP: + ret = artwork_get_player_image(in_path, max_w, max_h, evbuf); + break; } return ret; diff --git a/src/db.c b/src/db.c index 7a976eb4..5c4e296f 100644 --- a/src/db.c +++ b/src/db.c @@ -62,6 +62,11 @@ struct db_unlock { pthread_mutex_t lck; }; +struct async_query { + char *query; + int delay; +}; + #define DB_TYPE_CHAR 1 #define DB_TYPE_INT 2 #define DB_TYPE_INT64 3 @@ -620,7 +625,7 @@ db_exec(const char *query, char **errmsg) static void * db_exec_thread(void *arg) { - char *query = arg; + struct async_query *async = arg; char *errmsg; time_t start, end; int ret; @@ -628,7 +633,7 @@ db_exec_thread(void *arg) // When switching tracks we update playcount and select the next track's // metadata. We want the update to run after the selects so it won't lock // the database. - sleep(3); + sleep(async->delay); ret = db_perthread_init(); if (ret < 0) @@ -637,19 +642,20 @@ db_exec_thread(void *arg) return NULL; } - DPRINTF(E_DBG, L_DB, "Running delayed query '%s'\n", query); + DPRINTF(E_DBG, L_DB, "Running delayed query '%s'\n", async->query); time(&start); - ret = db_exec(query, &errmsg); + ret = db_exec(async->query, &errmsg); if (ret != SQLITE_OK) - DPRINTF(E_LOG, L_DB, "Error running query '%s': %s\n", query, errmsg); + DPRINTF(E_LOG, L_DB, "Error running query '%s': %s\n", async->query, errmsg); time(&end); if (end - start > 1) - DPRINTF(E_LOG, L_DB, "Warning: Slow query detected '%s' - database performance problems?\n", query); + DPRINTF(E_LOG, L_DB, "Warning: Slow query detected '%s' - database performance problems?\n", async->query); sqlite3_free(errmsg); - sqlite3_free(query); + sqlite3_free(async->query); + free(async); db_perthread_deinit(); @@ -658,8 +664,9 @@ db_exec_thread(void *arg) // Creates a one-off thread to run a delayed, fire-and-forget, non-blocking query static void -db_exec_nonblock(char *query) +db_exec_nonblock(char *query, int delay) { + struct async_query *async; pthread_t tid; pthread_attr_t attr; int ret; @@ -671,8 +678,17 @@ db_exec_nonblock(char *query) return; } + async = malloc(sizeof(struct async_query)); + if (!async) + { + DPRINTF(E_LOG, L_DB, "Out of memory\n"); + return; + } + async->query = query; + async->delay = delay; + pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); - ret = pthread_create(&tid, &attr, db_exec_thread, query); + ret = pthread_create(&tid, &attr, db_exec_thread, async); if (ret != 0) { DPRINTF(E_LOG, L_DB, "Error in db_exec_nonblock: Could not create thread\n"); @@ -2020,7 +2036,7 @@ db_file_inc_playcount(int id) } // Run the query non-blocking so we don't block playback if the update is slow - db_exec_nonblock(query); + db_exec_nonblock(query, 5); #undef Q_TMPL } @@ -2666,6 +2682,27 @@ db_file_update(struct media_file_info *mfi) #undef Q_TMPL } +void +db_file_update_icy(int id, char *artist, char *album) +{ +#define Q_TMPL "UPDATE files 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_exec_nonblock(query, 0); +#undef Q_TMPL +} + void db_file_delete_bypath(char *path) { diff --git a/src/db.h b/src/db.h index 82f5e9e9..7a8fc1d7 100644 --- a/src/db.h +++ b/src/db.h @@ -49,6 +49,7 @@ enum query_type { #define ARTWORK_DIR 4 #define ARTWORK_PARENTDIR 5 #define ARTWORK_SPOTIFY 6 +#define ARTWORK_HTTP 7 enum filelistitem_type { F_PLAYLIST = 1, @@ -425,6 +426,9 @@ db_file_add(struct media_file_info *mfi); int db_file_update(struct media_file_info *mfi); +void +db_file_update_icy(int id, char *artist, char *album); + void db_file_delete_bypath(char *path); diff --git a/src/filescanner_ffmpeg.c b/src/filescanner_ffmpeg.c index c70a2530..06560bfd 100644 --- a/src/filescanner_ffmpeg.c +++ b/src/filescanner_ffmpeg.c @@ -37,6 +37,7 @@ #include "logger.h" #include "filescanner.h" #include "misc.h" +#include "icy.h" /* Legacy format-specific scanners */ @@ -314,88 +315,13 @@ extract_metadata(struct media_file_info *mfi, AVFormatContext *ctx, AVStream *au return mdcount; } -#if LIBAVFORMAT_VERSION_MAJOR >= 56 || (LIBAVFORMAT_VERSION_MAJOR == 55 && LIBAVFORMAT_VERSION_MINOR >= 13) -/* Extracts ICY metadata (requires libav 10) */ -static void -extract_metadata_icy(struct media_file_info *mfi, AVFormatContext *ctx) -{ - uint8_t *icy_meta; - char *icy_token; - char *icy_str; - char *ptr; - - icy_meta = NULL; - // TODO Also get icy_metadata_packet to show current track - av_opt_get(ctx, "icy_metadata_headers", AV_OPT_SEARCH_CHILDREN, &icy_meta); - - if (!icy_meta) - return; - - icy_str = strdup((char *)icy_meta); - icy_token = strtok(icy_str, "\r\n"); - - while (icy_token != NULL) - { - ptr = strchr(icy_token, ':'); - if (!ptr || (strlen(ptr) < 4)) - { - icy_token = strtok(NULL, "\r\n"); - continue; - } - - ptr++; - if (ptr[0] == ' ') - ptr++; - - if (strstr(icy_token, "icy-name")) - { - DPRINTF(E_DBG, L_SCAN, "Libav/ffmpeg found ICY metadata, name is '%s'\n", ptr); - - if (mfi->title) - free(mfi->title); - if (mfi->artist) - free(mfi->artist); - if (mfi->album_artist) - free(mfi->album_artist); - - mfi->title = strdup(ptr); - mfi->artist = strdup(ptr); - mfi->album_artist = strdup(ptr); - } - - if (strstr(icy_token, "icy-description")) - { - DPRINTF(E_DBG, L_SCAN, "Libav/ffmpeg found ICY metadata, description is '%s'\n", ptr); - - if (mfi->album) - free(mfi->album); - - mfi->album = strdup(ptr); - } - - if (strstr(icy_token, "icy-genre")) - { - DPRINTF(E_DBG, L_SCAN, "Libav/ffmpeg found ICY metadata, genre is '%s'\n", ptr); - - if (mfi->genre) - free(mfi->genre); - - mfi->genre = strdup(ptr); - } - - icy_token = strtok(NULL, "\r\n"); - } - av_free(icy_meta); - free(icy_str); -} -#endif - int scan_metadata_ffmpeg(char *file, struct media_file_info *mfi) { AVFormatContext *ctx; AVDictionary *options; const struct metadata_map *extra_md_map; + struct icy_metadata *icy_metadata; #if LIBAVCODEC_VERSION_MAJOR >= 55 || (LIBAVCODEC_VERSION_MAJOR == 54 && LIBAVCODEC_VERSION_MINOR >= 35) enum AVCodecID codec_id; enum AVCodecID video_codec_id; @@ -425,7 +351,10 @@ scan_metadata_ffmpeg(char *file, struct media_file_info *mfi) # endif if (mfi->data_kind == 1) - av_dict_set(&options, "icy", "1", 0); + { + av_dict_set(&options, "icy", "1", 0); + mfi->artwork = ARTWORK_HTTP; + } ret = avformat_open_input(&ctx, file, NULL, &options); #else @@ -553,11 +482,46 @@ scan_metadata_ffmpeg(char *file, struct media_file_info *mfi) DPRINTF(E_DBG, L_SCAN, "Duration %d ms, bitrate %d kbps\n", mfi->song_length, mfi->bitrate); -#if LIBAVFORMAT_VERSION_MAJOR >= 56 || (LIBAVFORMAT_VERSION_MAJOR == 55 && LIBAVFORMAT_VERSION_MINOR >= 13) /* Try to extract ICY metadata if url/stream */ if (mfi->data_kind == 1) - extract_metadata_icy(mfi, ctx); -#endif + { + icy_metadata = icy_metadata_get(ctx, 0); + if (icy_metadata && icy_metadata->name) + { + DPRINTF(E_DBG, L_SCAN, "libav/ffmpeg found ICY metadata, name is '%s'\n", icy_metadata->name); + + if (mfi->title) + free(mfi->title); + if (mfi->artist) + free(mfi->artist); + if (mfi->album_artist) + free(mfi->album_artist); + + mfi->title = strdup(icy_metadata->name); + mfi->artist = strdup(icy_metadata->name); + mfi->album_artist = strdup(icy_metadata->name); + } + if (icy_metadata && icy_metadata->description) + { + DPRINTF(E_DBG, L_SCAN, "libav/ffmpeg found ICY metadata, description is '%s'\n", icy_metadata->description); + + if (mfi->album) + free(mfi->album); + + mfi->album = strdup(icy_metadata->description); + } + if (icy_metadata && icy_metadata->genre) + { + DPRINTF(E_DBG, L_SCAN, "libav/ffmpeg found ICY metadata, genre is '%s'\n", icy_metadata->genre); + + if (mfi->genre) + free(mfi->genre); + + mfi->genre = strdup(icy_metadata->genre); + } + if (icy_metadata) + icy_metadata_free(icy_metadata); + } /* Get some more information on the audio stream */ if (audio_stream) diff --git a/src/icy.c b/src/icy.c new file mode 100644 index 00000000..81792e3a --- /dev/null +++ b/src/icy.c @@ -0,0 +1,217 @@ +/* + * Copyright (C) 2015 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 "icy.h" +#include "logger.h" +#include "misc.h" + +#include + +static int +metadata_packet_get(struct icy_metadata *metadata, AVFormatContext *fmtctx) +{ + uint8_t *buffer; + char *icy_token; + char *ptr; + char *end; + + av_opt_get(fmtctx, "icy_metadata_packet", AV_OPT_SEARCH_CHILDREN, &buffer); + if (!buffer) + return -1; + + icy_token = strtok((char *)buffer, ";"); + while (icy_token != NULL) + { + ptr = strchr(icy_token, '='); + if (!ptr || (ptr[1] == '\0')) + { + icy_token = strtok(NULL, ";"); + continue; + } + + ptr++; + if (ptr[0] == '\'') + ptr++; + + end = strrchr(ptr, '\''); + if (end) + *end = '\0'; + + if (strncmp(icy_token, "StreamTitle", strlen("StreamTitle")) == 0) + { + metadata->title = ptr; + + /* Dash separates artist from title, if no dash assume all is title */ + ptr = strstr(ptr, " - "); + if (ptr) + { + *ptr = '\0'; + metadata->title = strdup(metadata->title); + *ptr = ' '; + + metadata->artist = strdup(ptr + 3); + } + else + metadata->title = strdup(metadata->title); + } + else if (strncmp(icy_token, "StreamUrl", strlen("StreamUrl")) == 0) + metadata->artwork_url = strdup(ptr); + + if (end) + *end = '\''; + + icy_token = strtok(NULL, ";"); + } + av_free(buffer); + + if (metadata->title) + metadata->hash = djb_hash(metadata->title, strlen(metadata->title)); + + return 0; +} + +static int +metadata_header_get(struct icy_metadata *metadata, AVFormatContext *fmtctx) +{ + uint8_t *buffer; + char *icy_token; + char *ptr; + + av_opt_get(fmtctx, "icy_metadata_headers", AV_OPT_SEARCH_CHILDREN, &buffer); + if (!buffer) + return -1; + + icy_token = strtok((char *)buffer, "\r\n"); + while (icy_token != NULL) + { + ptr = strchr(icy_token, ':'); + if (!ptr || (ptr[1] == '\0')) + { + icy_token = strtok(NULL, "\r\n"); + continue; + } + + ptr++; + if (ptr[0] == ' ') + ptr++; + + if (strncmp(icy_token, "icy-name", strlen("icy-name")) == 0) + metadata->name = strdup(ptr); + else if (strncmp(icy_token, "icy-description", strlen("icy-description")) == 0) + metadata->description = strdup(ptr); + else if (strncmp(icy_token, "icy-genre", strlen("icy-genre")) == 0) + metadata->genre = strdup(ptr); + + icy_token = strtok(NULL, "\r\n"); + } + av_free(buffer); + + return 0; +} + +void +icy_metadata_free(struct icy_metadata *metadata) +{ + if (metadata->name) + free(metadata->name); + + if (metadata->description) + free(metadata->description); + + if (metadata->genre) + free(metadata->genre); + + if (metadata->title) + free(metadata->title); + + if (metadata->artist) + free(metadata->artist); + + if (metadata->artwork_url) + free(metadata->artwork_url); + + free(metadata); +} + +#if LIBAVFORMAT_VERSION_MAJOR >= 56 || (LIBAVFORMAT_VERSION_MAJOR == 55 && LIBAVFORMAT_VERSION_MINOR >= 13) +/* Extracts ICY header and packet metadata (requires libav 10) + * + * example header metadata (standard http header format): + * icy-name: Rock On Radio + * example packet metadata (track currently being played): + * StreamTitle='Robert Miles - Black Rubber';StreamUrl=''; + * + * The extraction is straight from the stream and done in the player thread, so + * it must not produce significant delay. + */ +struct icy_metadata * +icy_metadata_get(AVFormatContext *fmtctx, int packet_only) +{ + struct icy_metadata *metadata; + int got_packet; + int got_header; + + metadata = malloc(sizeof(struct icy_metadata)); + if (!metadata) + return NULL; + memset(metadata, 0, sizeof(struct icy_metadata)); + + got_packet = (metadata_packet_get(metadata, fmtctx) == 0); + got_header = (!packet_only) && (metadata_header_get(metadata, fmtctx) == 0); + + if (!got_packet && !got_header) + { + free(metadata); + return NULL; + } + +/* DPRINTF(E_DBG, L_MISC, "Found ICY: N %s, D %s, G %s, T %s, A %s, U %s, I %" PRIu32 "\n", + metadata->name, + metadata->description, + metadata->genre, + metadata->title, + metadata->artist, + metadata->artwork_url, + metadata->hash + ); +*/ + return metadata; +} +#else +struct icy_metadata * +icy_metadata_get(AVFormatContext *fmtctx, int packet_only) +{ + return NULL; +} +#endif + diff --git a/src/icy.h b/src/icy.h new file mode 100644 index 00000000..f909bf17 --- /dev/null +++ b/src/icy.h @@ -0,0 +1,28 @@ + +#ifndef __ICY_H__ +#define __ICY_H__ + +#include + +struct icy_metadata +{ + /* Static stream metadata from icy_metadata_headers */ + char *name; + char *description; + char *genre; + + /* Track specific, comes from icy_metadata_packet */ + char *title; + char *artist; + char *artwork_url; + + uint32_t hash; +}; + +void +icy_metadata_free(struct icy_metadata *metadata); + +struct icy_metadata * +icy_metadata_get(AVFormatContext *fmtctx, int packet_only); + +#endif /* !__ICY_H__ */ diff --git a/src/player.c b/src/player.c index 448e937a..694a2640 100644 --- a/src/player.c +++ b/src/player.c @@ -65,6 +65,7 @@ /* These handle getting the media data */ #include "transcode.h" #include "pipe.h" +#include "icy.h" #ifdef HAVE_SPOTIFY_H # include "spotify.h" #endif @@ -77,6 +78,9 @@ #define MAX(a, b) ((a > b) ? a : b) #endif +/* Interval between ICY metadata polls for streams, in seconds */ +#define METADATA_ICY_POLL 5 + enum player_sync_source { PLAYER_SYNC_CLOCK, @@ -128,6 +132,12 @@ struct item_range uint32_t *id_ptr; }; +struct icy_artwork +{ + char *stream_url; + char *artwork_url; +}; + struct player_command { pthread_mutex_t lck; @@ -153,6 +163,7 @@ struct player_command int intval; int ps_pos[2]; struct item_range item_range; + struct icy_artwork icy_artwork; } arg; int ret; @@ -180,6 +191,7 @@ static int cmd_pipe[2]; static int player_exit; static struct event *exitev; static struct event *cmdev; +static struct event *metaev; static pthread_t tid_player; /* Player status */ @@ -661,6 +673,59 @@ metadata_send(struct player_source *ps, int startup) raop_metadata_send(ps->id, rtptime, offset, startup); } +static void +metadata_icy_poll_cb(int fd, short what, void *arg) +{ + struct timeval tv = { METADATA_ICY_POLL, 0 }; + struct icy_metadata *metadata; + int changed; + + /* Playback of stream has stopped, so stop polling */ + if (!cur_streaming || cur_streaming->type != SOURCE_HTTP || !cur_streaming->ctx) + { + if (metaev) + event_free(metaev); + + metaev = NULL; + return; + } + + transcode_metadata(cur_streaming->ctx, &metadata, &changed); + if (!metadata) + goto no_metadata; + + if (!changed) + goto no_update; + + /* Update db (async) and send status update to clients */ + db_file_update_icy(cur_streaming->id, metadata->artist, metadata->title); + status_update(player_state); + metadata_send(cur_streaming, 0); + + no_update: + icy_metadata_free(metadata); + + no_metadata: + evtimer_add(metaev, &tv); +} + +static void +metadata_icy_poll_start(void) +{ + struct timeval tv = { METADATA_ICY_POLL, 0 }; + + DPRINTF(E_DBG, L_PLAYER, "Starting ICY polling\n"); + + if (metaev) + return; + + metaev = evtimer_new(evbase_player, metadata_icy_poll_cb, NULL); + if (!metaev) + return; + + evtimer_add(metaev, &tv); +} + /* Audio sources */ /* Thread: httpd (DACP) */ static struct player_source * @@ -1059,7 +1124,8 @@ source_free(struct player_source *ps) { switch (ps->type) { - case SOURCE_FFMPEG: + case SOURCE_FILE: + case SOURCE_HTTP: if (ps->ctx) transcode_cleanup(ps->ctx); break; @@ -1087,7 +1153,8 @@ source_stop(struct player_source *ps) { switch (ps->type) { - case SOURCE_FFMPEG: + case SOURCE_FILE: + case SOURCE_HTTP: if (ps->ctx) { transcode_cleanup(ps->ctx); @@ -1274,6 +1341,13 @@ source_open(struct player_source *ps, int no_md) // Setup the source type responsible for getting the audio switch (mfi->data_kind) { + case 1: + ps->type = SOURCE_HTTP; + ret = transcode_setup(&ps->ctx, mfi, NULL, 0); + if (ret >= 0) + metadata_icy_poll_start(); + break; + case 2: ps->type = SOURCE_SPOTIFY; #ifdef HAVE_SPOTIFY_H @@ -1289,7 +1363,7 @@ source_open(struct player_source *ps, int no_md) break; default: - ps->type = SOURCE_FFMPEG; + ps->type = SOURCE_FILE; ret = transcode_setup(&ps->ctx, mfi, NULL, 0); } @@ -1347,7 +1421,7 @@ source_next(int force) if (!cur_streaming) break; - if ((cur_streaming->type == SOURCE_FFMPEG) && cur_streaming->ctx) + if ((cur_streaming->type == SOURCE_FILE) && cur_streaming->ctx) { ret = transcode_seek(cur_streaming->ctx, 0); @@ -1585,7 +1659,7 @@ source_check(void) if (ps->setup_done) { - if ((ps->type == SOURCE_FFMPEG) && ps->ctx) + if ((ps->type == SOURCE_FILE) && ps->ctx) { transcode_cleanup(ps->ctx); ps->ctx = NULL; @@ -1639,7 +1713,7 @@ source_check(void) if (ps->setup_done) { - if ((ps->type == SOURCE_FFMPEG) && ps->ctx) + if ((ps->type == SOURCE_FILE) && ps->ctx) { transcode_cleanup(ps->ctx); ps->ctx = NULL; @@ -1726,7 +1800,8 @@ source_read(uint8_t *buf, int len, uint64_t rtptime) { switch (cur_streaming->type) { - case SOURCE_FFMPEG: + case SOURCE_FILE: + case SOURCE_HTTP: ret = transcode(cur_streaming->ctx, audio_buf, len - nbytes); break; @@ -2457,6 +2532,22 @@ now_playing(struct player_command *cmd) return 0; } +static int +artwork_url_get(struct player_command *cmd) +{ + DPRINTF(E_DBG, L_PLAYER, "ICY artwork url call\n"); + + cmd->arg.icy_artwork.artwork_url = NULL; + + /* Not playing a stream */ + if (!cur_playing || cur_playing->type != SOURCE_HTTP || !cur_playing->ctx) + return -1; + + transcode_metadata_artwork_url(cur_playing->ctx, &cmd->arg.icy_artwork.artwork_url, cmd->arg.icy_artwork.stream_url); + + return 0; +} + static int playback_stop(struct player_command *cmd) { @@ -2907,7 +2998,7 @@ playback_seek_bh(struct player_command *cmd) /* Seek to commanded position */ switch (ps->type) { - case SOURCE_FFMPEG: + case SOURCE_FILE: ret = transcode_seek(ps->ctx, ms); break; #ifdef HAVE_SPOTIFY_H @@ -2916,8 +3007,10 @@ playback_seek_bh(struct player_command *cmd) break; #endif case SOURCE_PIPE: + case SOURCE_HTTP: ret = 1; break; + default: ret = -1; } @@ -2964,7 +3057,7 @@ playback_pause_bh(struct player_command *cmd) switch (ps->type) { - case SOURCE_FFMPEG: + case SOURCE_FILE: ret = transcode_seek(ps->ctx, ms); break; #ifdef HAVE_SPOTIFY_H @@ -4110,6 +4203,30 @@ player_now_playing(uint32_t *id) return ret; } +int +player_icy_artwork_url(char **artwork_url, char *stream_url) +{ + struct player_command cmd; + int ret; + + command_init(&cmd); + + cmd.func = artwork_url_get; + cmd.func_bh = NULL; + cmd.arg.icy_artwork.stream_url = stream_url; + + if (pthread_self() != tid_player) + ret = sync_command(&cmd); + else + ret = artwork_url_get(&cmd); + + *artwork_url = cmd.arg.icy_artwork.artwork_url; + + command_deinit(&cmd); + + return ret; +} + /* * Starts/resumes playback * diff --git a/src/player.h b/src/player.h index 93ade6d1..c313efe5 100644 --- a/src/player.h +++ b/src/player.h @@ -32,9 +32,10 @@ enum repeat_mode { }; enum source_type { - SOURCE_FFMPEG = 0, + SOURCE_FILE = 0, SOURCE_SPOTIFY, SOURCE_PIPE, + SOURCE_HTTP, }; struct spk_flags { @@ -132,6 +133,9 @@ player_get_status(struct player_status *status); int player_now_playing(uint32_t *id); +int +player_icy_artwork_url(char **artwork_url, char *stream_url); + void player_speaker_enumerate(spk_enum_cb cb, void *arg); diff --git a/src/raop.c b/src/raop.c index 2309eb71..745cf98b 100644 --- a/src/raop.c +++ b/src/raop.c @@ -61,6 +61,7 @@ #include "evrtsp/evrtsp.h" #include +#include #include "conffile.h" #include "logger.h" @@ -157,6 +158,14 @@ struct raop_metadata struct raop_metadata *next; }; +struct raop_metadata_send_ctx +{ + int id; + uint64_t rtptime; + uint64_t offset; + int startup; +}; + struct raop_service { int fd; @@ -2162,17 +2171,25 @@ raop_metadata_startup_send(struct raop_session *rs) } } -void -raop_metadata_send(int id, uint64_t rtptime, uint64_t offset, int startup) +static void * +raop_metadata_send_thread(void *arg) { + struct raop_metadata_send_ctx *ctx = arg; struct raop_session *rs; struct raop_metadata *rmd; uint32_t delay; int ret; - rmd = raop_metadata_prepare(id, rtptime); + ret = db_perthread_init(); + if (ret < 0) + { + DPRINTF(E_LOG, L_DB, "Error in raop_metadata_send_thread: Could not init thread\n"); + return NULL; + } + + rmd = raop_metadata_prepare(ctx->id, ctx->rtptime); if (!rmd) - return; + goto no_metadata; for (rs = sessions; rs; rs = rs->next) { @@ -2182,18 +2199,59 @@ raop_metadata_send(int id, uint64_t rtptime, uint64_t offset, int startup) if (!rs->wants_metadata) continue; - delay = (startup) ? RAOP_MD_DELAY_STARTUP : RAOP_MD_DELAY_SWITCH; + delay = (ctx->startup) ? RAOP_MD_DELAY_STARTUP : RAOP_MD_DELAY_SWITCH; - ret = raop_metadata_send_internal(rs, rmd, offset, delay); + ret = raop_metadata_send_internal(rs, rmd, ctx->offset, delay); if (ret < 0) { raop_session_failure(rs); - continue; } } + + no_metadata: + db_perthread_deinit(); + free(ctx); + + return NULL; } +void +raop_metadata_send(int id, uint64_t rtptime, uint64_t offset, int startup) +{ + struct raop_metadata_send_ctx *ctx; + pthread_t tid; + pthread_attr_t attr; + int ret; + + ctx = malloc(sizeof(struct raop_metadata_send_ctx)); + if (!ctx) + { + DPRINTF(E_LOG, L_RAOP, "Out of memory\n"); + return; + } + + ctx->id = id; + ctx->rtptime = rtptime; + ctx->offset = offset; + ctx->startup = startup; + + ret = pthread_attr_init(&attr); + if (ret != 0) + { + DPRINTF(E_LOG, L_DB, "Error in raop_metadata_send: Could not init attributes\n"); + return; + } + + pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); + ret = pthread_create(&tid, &attr, raop_metadata_send_thread, ctx); + if (ret != 0) + { + DPRINTF(E_LOG, L_DB, "Error in raop_metadata_send: Could not create thread\n"); + } + + pthread_attr_destroy(&attr); +} /* Volume handling */ static float diff --git a/src/transcode.c b/src/transcode.c index a1269c0b..678d5893 100644 --- a/src/transcode.c +++ b/src/transcode.c @@ -90,6 +90,7 @@ struct transcode_ctx { uint32_t duration; uint64_t samples; + uint32_t icy_hash; /* WAV header */ int wavhdr; @@ -894,3 +895,41 @@ transcode_needed(const char *user_agent, const char *client_codecs, char *file_c return 1; } + +void +transcode_metadata(struct transcode_ctx *ctx, struct icy_metadata **metadata, int *changed) +{ + struct icy_metadata *m; + + *metadata = NULL; + + if (!ctx->fmtctx) + return; + + m = icy_metadata_get(ctx->fmtctx, 1); + + *changed = (m->hash != ctx->icy_hash); + + ctx->icy_hash = m->hash; + + *metadata = m; +} + +void +transcode_metadata_artwork_url(struct transcode_ctx *ctx, char **artwork_url, char *stream_url) +{ + struct icy_metadata *m; + + *artwork_url = NULL; + + if (!ctx->fmtctx || !ctx->fmtctx->filename) + return; + + if (strcmp(ctx->fmtctx->filename, stream_url) != 0) + return; + + m = icy_metadata_get(ctx->fmtctx, 1); + + if (m->artwork_url) + *artwork_url = strdup(m->artwork_url); +} diff --git a/src/transcode.h b/src/transcode.h index b266e9fb..e781802c 100644 --- a/src/transcode.h +++ b/src/transcode.h @@ -7,6 +7,7 @@ #else # include #endif +#include "icy.h" struct transcode_ctx; @@ -25,4 +26,10 @@ transcode_cleanup(struct transcode_ctx *ctx); int transcode_needed(const char *user_agent, const char *client_codecs, char *file_codectype); +void +transcode_metadata(struct transcode_ctx *ctx, struct icy_metadata **metadata, int *changed); + +void +transcode_metadata_artwork_url(struct transcode_ctx *ctx, char **artwork_url, char *stream_url); + #endif /* !__TRANSCODE_H__ */