mirror of
https://github.com/owntone/owntone-server.git
synced 2025-01-14 16:25:03 -05:00
Support for live ICY metadata for streams (incl. artwork)
This commit is contained in:
parent
34d815a130
commit
6221e24f1b
@ -114,6 +114,7 @@ forked_daapd_SOURCES = main.c \
|
|||||||
transcode.c transcode.h \
|
transcode.c transcode.h \
|
||||||
pipe.c pipe.h \
|
pipe.c pipe.h \
|
||||||
artwork.c artwork.h \
|
artwork.c artwork.h \
|
||||||
|
icy.h icy.c \
|
||||||
misc.c misc.h \
|
misc.c misc.h \
|
||||||
rng.c rng.h \
|
rng.c rng.h \
|
||||||
rsp_query.c rsp_query.h \
|
rsp_query.c rsp_query.h \
|
||||||
|
127
src/artwork.c
127
src/artwork.c
@ -38,6 +38,7 @@
|
|||||||
#include "logger.h"
|
#include "logger.h"
|
||||||
#include "conffile.h"
|
#include "conffile.h"
|
||||||
#include "cache.h"
|
#include "cache.h"
|
||||||
|
#include "player.h"
|
||||||
|
|
||||||
#if LIBAVFORMAT_VERSION_MAJOR >= 53
|
#if LIBAVFORMAT_VERSION_MAJOR >= 53
|
||||||
# include "avio_evbuffer.h"
|
# include "avio_evbuffer.h"
|
||||||
@ -725,6 +726,127 @@ artwork_get(char *path, int max_w, int max_h, struct evbuffer *evbuf)
|
|||||||
return ret;
|
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)
|
#if LIBAVFORMAT_VERSION_MAJOR >= 55 || (LIBAVFORMAT_VERSION_MAJOR == 54 && LIBAVFORMAT_VERSION_MINOR >= 6)
|
||||||
static int
|
static int
|
||||||
artwork_get_embedded_image(char *path, int max_w, int max_h, struct evbuffer *evbuf)
|
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)
|
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);
|
avformat_close_input(&src_ctx);
|
||||||
return -1;
|
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);
|
ret = artwork_get_embedded_image(in_path, max_w, max_h, evbuf);
|
||||||
break;
|
break;
|
||||||
#endif
|
#endif
|
||||||
|
case ARTWORK_HTTP:
|
||||||
|
ret = artwork_get_player_image(in_path, max_w, max_h, evbuf);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
|
57
src/db.c
57
src/db.c
@ -62,6 +62,11 @@ struct db_unlock {
|
|||||||
pthread_mutex_t lck;
|
pthread_mutex_t lck;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct async_query {
|
||||||
|
char *query;
|
||||||
|
int delay;
|
||||||
|
};
|
||||||
|
|
||||||
#define DB_TYPE_CHAR 1
|
#define DB_TYPE_CHAR 1
|
||||||
#define DB_TYPE_INT 2
|
#define DB_TYPE_INT 2
|
||||||
#define DB_TYPE_INT64 3
|
#define DB_TYPE_INT64 3
|
||||||
@ -620,7 +625,7 @@ db_exec(const char *query, char **errmsg)
|
|||||||
static void *
|
static void *
|
||||||
db_exec_thread(void *arg)
|
db_exec_thread(void *arg)
|
||||||
{
|
{
|
||||||
char *query = arg;
|
struct async_query *async = arg;
|
||||||
char *errmsg;
|
char *errmsg;
|
||||||
time_t start, end;
|
time_t start, end;
|
||||||
int ret;
|
int ret;
|
||||||
@ -628,7 +633,7 @@ db_exec_thread(void *arg)
|
|||||||
// When switching tracks we update playcount and select the next track's
|
// 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
|
// metadata. We want the update to run after the selects so it won't lock
|
||||||
// the database.
|
// the database.
|
||||||
sleep(3);
|
sleep(async->delay);
|
||||||
|
|
||||||
ret = db_perthread_init();
|
ret = db_perthread_init();
|
||||||
if (ret < 0)
|
if (ret < 0)
|
||||||
@ -637,19 +642,20 @@ db_exec_thread(void *arg)
|
|||||||
return NULL;
|
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);
|
time(&start);
|
||||||
ret = db_exec(query, &errmsg);
|
ret = db_exec(async->query, &errmsg);
|
||||||
if (ret != SQLITE_OK)
|
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);
|
time(&end);
|
||||||
if (end - start > 1)
|
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(errmsg);
|
||||||
sqlite3_free(query);
|
sqlite3_free(async->query);
|
||||||
|
free(async);
|
||||||
|
|
||||||
db_perthread_deinit();
|
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
|
// Creates a one-off thread to run a delayed, fire-and-forget, non-blocking query
|
||||||
static void
|
static void
|
||||||
db_exec_nonblock(char *query)
|
db_exec_nonblock(char *query, int delay)
|
||||||
{
|
{
|
||||||
|
struct async_query *async;
|
||||||
pthread_t tid;
|
pthread_t tid;
|
||||||
pthread_attr_t attr;
|
pthread_attr_t attr;
|
||||||
int ret;
|
int ret;
|
||||||
@ -671,8 +678,17 @@ db_exec_nonblock(char *query)
|
|||||||
return;
|
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);
|
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)
|
if (ret != 0)
|
||||||
{
|
{
|
||||||
DPRINTF(E_LOG, L_DB, "Error in db_exec_nonblock: Could not create thread\n");
|
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
|
// 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
|
#undef Q_TMPL
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2666,6 +2682,27 @@ db_file_update(struct media_file_info *mfi)
|
|||||||
#undef Q_TMPL
|
#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
|
void
|
||||||
db_file_delete_bypath(char *path)
|
db_file_delete_bypath(char *path)
|
||||||
{
|
{
|
||||||
|
4
src/db.h
4
src/db.h
@ -49,6 +49,7 @@ enum query_type {
|
|||||||
#define ARTWORK_DIR 4
|
#define ARTWORK_DIR 4
|
||||||
#define ARTWORK_PARENTDIR 5
|
#define ARTWORK_PARENTDIR 5
|
||||||
#define ARTWORK_SPOTIFY 6
|
#define ARTWORK_SPOTIFY 6
|
||||||
|
#define ARTWORK_HTTP 7
|
||||||
|
|
||||||
enum filelistitem_type {
|
enum filelistitem_type {
|
||||||
F_PLAYLIST = 1,
|
F_PLAYLIST = 1,
|
||||||
@ -425,6 +426,9 @@ db_file_add(struct media_file_info *mfi);
|
|||||||
int
|
int
|
||||||
db_file_update(struct media_file_info *mfi);
|
db_file_update(struct media_file_info *mfi);
|
||||||
|
|
||||||
|
void
|
||||||
|
db_file_update_icy(int id, char *artist, char *album);
|
||||||
|
|
||||||
void
|
void
|
||||||
db_file_delete_bypath(char *path);
|
db_file_delete_bypath(char *path);
|
||||||
|
|
||||||
|
@ -37,6 +37,7 @@
|
|||||||
#include "logger.h"
|
#include "logger.h"
|
||||||
#include "filescanner.h"
|
#include "filescanner.h"
|
||||||
#include "misc.h"
|
#include "misc.h"
|
||||||
|
#include "icy.h"
|
||||||
|
|
||||||
|
|
||||||
/* Legacy format-specific scanners */
|
/* Legacy format-specific scanners */
|
||||||
@ -314,88 +315,13 @@ extract_metadata(struct media_file_info *mfi, AVFormatContext *ctx, AVStream *au
|
|||||||
return mdcount;
|
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
|
int
|
||||||
scan_metadata_ffmpeg(char *file, struct media_file_info *mfi)
|
scan_metadata_ffmpeg(char *file, struct media_file_info *mfi)
|
||||||
{
|
{
|
||||||
AVFormatContext *ctx;
|
AVFormatContext *ctx;
|
||||||
AVDictionary *options;
|
AVDictionary *options;
|
||||||
const struct metadata_map *extra_md_map;
|
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)
|
#if LIBAVCODEC_VERSION_MAJOR >= 55 || (LIBAVCODEC_VERSION_MAJOR == 54 && LIBAVCODEC_VERSION_MINOR >= 35)
|
||||||
enum AVCodecID codec_id;
|
enum AVCodecID codec_id;
|
||||||
enum AVCodecID video_codec_id;
|
enum AVCodecID video_codec_id;
|
||||||
@ -425,7 +351,10 @@ scan_metadata_ffmpeg(char *file, struct media_file_info *mfi)
|
|||||||
# endif
|
# endif
|
||||||
|
|
||||||
if (mfi->data_kind == 1)
|
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);
|
ret = avformat_open_input(&ctx, file, NULL, &options);
|
||||||
#else
|
#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);
|
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 */
|
/* Try to extract ICY metadata if url/stream */
|
||||||
if (mfi->data_kind == 1)
|
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 */
|
/* Get some more information on the audio stream */
|
||||||
if (audio_stream)
|
if (audio_stream)
|
||||||
|
217
src/icy.c
Normal file
217
src/icy.c
Normal file
@ -0,0 +1,217 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2015 Espen Jürgensen <espenjurgensen@gmail.com>
|
||||||
|
*
|
||||||
|
* 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 <config.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <ctype.h>
|
||||||
|
#include <limits.h>
|
||||||
|
#include <sys/param.h>
|
||||||
|
#include <sys/types.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <errno.h>
|
||||||
|
|
||||||
|
#include "icy.h"
|
||||||
|
#include "logger.h"
|
||||||
|
#include "misc.h"
|
||||||
|
|
||||||
|
#include <libavutil/opt.h>
|
||||||
|
|
||||||
|
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
|
||||||
|
|
28
src/icy.h
Normal file
28
src/icy.h
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
|
||||||
|
#ifndef __ICY_H__
|
||||||
|
#define __ICY_H__
|
||||||
|
|
||||||
|
#include <libavformat/avformat.h>
|
||||||
|
|
||||||
|
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__ */
|
135
src/player.c
135
src/player.c
@ -65,6 +65,7 @@
|
|||||||
/* These handle getting the media data */
|
/* These handle getting the media data */
|
||||||
#include "transcode.h"
|
#include "transcode.h"
|
||||||
#include "pipe.h"
|
#include "pipe.h"
|
||||||
|
#include "icy.h"
|
||||||
#ifdef HAVE_SPOTIFY_H
|
#ifdef HAVE_SPOTIFY_H
|
||||||
# include "spotify.h"
|
# include "spotify.h"
|
||||||
#endif
|
#endif
|
||||||
@ -77,6 +78,9 @@
|
|||||||
#define MAX(a, b) ((a > b) ? a : b)
|
#define MAX(a, b) ((a > b) ? a : b)
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
/* Interval between ICY metadata polls for streams, in seconds */
|
||||||
|
#define METADATA_ICY_POLL 5
|
||||||
|
|
||||||
enum player_sync_source
|
enum player_sync_source
|
||||||
{
|
{
|
||||||
PLAYER_SYNC_CLOCK,
|
PLAYER_SYNC_CLOCK,
|
||||||
@ -128,6 +132,12 @@ struct item_range
|
|||||||
uint32_t *id_ptr;
|
uint32_t *id_ptr;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct icy_artwork
|
||||||
|
{
|
||||||
|
char *stream_url;
|
||||||
|
char *artwork_url;
|
||||||
|
};
|
||||||
|
|
||||||
struct player_command
|
struct player_command
|
||||||
{
|
{
|
||||||
pthread_mutex_t lck;
|
pthread_mutex_t lck;
|
||||||
@ -153,6 +163,7 @@ struct player_command
|
|||||||
int intval;
|
int intval;
|
||||||
int ps_pos[2];
|
int ps_pos[2];
|
||||||
struct item_range item_range;
|
struct item_range item_range;
|
||||||
|
struct icy_artwork icy_artwork;
|
||||||
} arg;
|
} arg;
|
||||||
|
|
||||||
int ret;
|
int ret;
|
||||||
@ -180,6 +191,7 @@ static int cmd_pipe[2];
|
|||||||
static int player_exit;
|
static int player_exit;
|
||||||
static struct event *exitev;
|
static struct event *exitev;
|
||||||
static struct event *cmdev;
|
static struct event *cmdev;
|
||||||
|
static struct event *metaev;
|
||||||
static pthread_t tid_player;
|
static pthread_t tid_player;
|
||||||
|
|
||||||
/* Player status */
|
/* Player status */
|
||||||
@ -661,6 +673,59 @@ metadata_send(struct player_source *ps, int startup)
|
|||||||
raop_metadata_send(ps->id, rtptime, offset, 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 */
|
/* Audio sources */
|
||||||
/* Thread: httpd (DACP) */
|
/* Thread: httpd (DACP) */
|
||||||
static struct player_source *
|
static struct player_source *
|
||||||
@ -1059,7 +1124,8 @@ source_free(struct player_source *ps)
|
|||||||
{
|
{
|
||||||
switch (ps->type)
|
switch (ps->type)
|
||||||
{
|
{
|
||||||
case SOURCE_FFMPEG:
|
case SOURCE_FILE:
|
||||||
|
case SOURCE_HTTP:
|
||||||
if (ps->ctx)
|
if (ps->ctx)
|
||||||
transcode_cleanup(ps->ctx);
|
transcode_cleanup(ps->ctx);
|
||||||
break;
|
break;
|
||||||
@ -1087,7 +1153,8 @@ source_stop(struct player_source *ps)
|
|||||||
{
|
{
|
||||||
switch (ps->type)
|
switch (ps->type)
|
||||||
{
|
{
|
||||||
case SOURCE_FFMPEG:
|
case SOURCE_FILE:
|
||||||
|
case SOURCE_HTTP:
|
||||||
if (ps->ctx)
|
if (ps->ctx)
|
||||||
{
|
{
|
||||||
transcode_cleanup(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
|
// Setup the source type responsible for getting the audio
|
||||||
switch (mfi->data_kind)
|
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:
|
case 2:
|
||||||
ps->type = SOURCE_SPOTIFY;
|
ps->type = SOURCE_SPOTIFY;
|
||||||
#ifdef HAVE_SPOTIFY_H
|
#ifdef HAVE_SPOTIFY_H
|
||||||
@ -1289,7 +1363,7 @@ source_open(struct player_source *ps, int no_md)
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
ps->type = SOURCE_FFMPEG;
|
ps->type = SOURCE_FILE;
|
||||||
ret = transcode_setup(&ps->ctx, mfi, NULL, 0);
|
ret = transcode_setup(&ps->ctx, mfi, NULL, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1347,7 +1421,7 @@ source_next(int force)
|
|||||||
if (!cur_streaming)
|
if (!cur_streaming)
|
||||||
break;
|
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);
|
ret = transcode_seek(cur_streaming->ctx, 0);
|
||||||
|
|
||||||
@ -1585,7 +1659,7 @@ source_check(void)
|
|||||||
|
|
||||||
if (ps->setup_done)
|
if (ps->setup_done)
|
||||||
{
|
{
|
||||||
if ((ps->type == SOURCE_FFMPEG) && ps->ctx)
|
if ((ps->type == SOURCE_FILE) && ps->ctx)
|
||||||
{
|
{
|
||||||
transcode_cleanup(ps->ctx);
|
transcode_cleanup(ps->ctx);
|
||||||
ps->ctx = NULL;
|
ps->ctx = NULL;
|
||||||
@ -1639,7 +1713,7 @@ source_check(void)
|
|||||||
|
|
||||||
if (ps->setup_done)
|
if (ps->setup_done)
|
||||||
{
|
{
|
||||||
if ((ps->type == SOURCE_FFMPEG) && ps->ctx)
|
if ((ps->type == SOURCE_FILE) && ps->ctx)
|
||||||
{
|
{
|
||||||
transcode_cleanup(ps->ctx);
|
transcode_cleanup(ps->ctx);
|
||||||
ps->ctx = NULL;
|
ps->ctx = NULL;
|
||||||
@ -1726,7 +1800,8 @@ source_read(uint8_t *buf, int len, uint64_t rtptime)
|
|||||||
{
|
{
|
||||||
switch (cur_streaming->type)
|
switch (cur_streaming->type)
|
||||||
{
|
{
|
||||||
case SOURCE_FFMPEG:
|
case SOURCE_FILE:
|
||||||
|
case SOURCE_HTTP:
|
||||||
ret = transcode(cur_streaming->ctx, audio_buf, len - nbytes);
|
ret = transcode(cur_streaming->ctx, audio_buf, len - nbytes);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@ -2457,6 +2532,22 @@ now_playing(struct player_command *cmd)
|
|||||||
return 0;
|
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
|
static int
|
||||||
playback_stop(struct player_command *cmd)
|
playback_stop(struct player_command *cmd)
|
||||||
{
|
{
|
||||||
@ -2907,7 +2998,7 @@ playback_seek_bh(struct player_command *cmd)
|
|||||||
/* Seek to commanded position */
|
/* Seek to commanded position */
|
||||||
switch (ps->type)
|
switch (ps->type)
|
||||||
{
|
{
|
||||||
case SOURCE_FFMPEG:
|
case SOURCE_FILE:
|
||||||
ret = transcode_seek(ps->ctx, ms);
|
ret = transcode_seek(ps->ctx, ms);
|
||||||
break;
|
break;
|
||||||
#ifdef HAVE_SPOTIFY_H
|
#ifdef HAVE_SPOTIFY_H
|
||||||
@ -2916,8 +3007,10 @@ playback_seek_bh(struct player_command *cmd)
|
|||||||
break;
|
break;
|
||||||
#endif
|
#endif
|
||||||
case SOURCE_PIPE:
|
case SOURCE_PIPE:
|
||||||
|
case SOURCE_HTTP:
|
||||||
ret = 1;
|
ret = 1;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
ret = -1;
|
ret = -1;
|
||||||
}
|
}
|
||||||
@ -2964,7 +3057,7 @@ playback_pause_bh(struct player_command *cmd)
|
|||||||
|
|
||||||
switch (ps->type)
|
switch (ps->type)
|
||||||
{
|
{
|
||||||
case SOURCE_FFMPEG:
|
case SOURCE_FILE:
|
||||||
ret = transcode_seek(ps->ctx, ms);
|
ret = transcode_seek(ps->ctx, ms);
|
||||||
break;
|
break;
|
||||||
#ifdef HAVE_SPOTIFY_H
|
#ifdef HAVE_SPOTIFY_H
|
||||||
@ -4110,6 +4203,30 @@ player_now_playing(uint32_t *id)
|
|||||||
return ret;
|
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
|
* Starts/resumes playback
|
||||||
*
|
*
|
||||||
|
@ -32,9 +32,10 @@ enum repeat_mode {
|
|||||||
};
|
};
|
||||||
|
|
||||||
enum source_type {
|
enum source_type {
|
||||||
SOURCE_FFMPEG = 0,
|
SOURCE_FILE = 0,
|
||||||
SOURCE_SPOTIFY,
|
SOURCE_SPOTIFY,
|
||||||
SOURCE_PIPE,
|
SOURCE_PIPE,
|
||||||
|
SOURCE_HTTP,
|
||||||
};
|
};
|
||||||
|
|
||||||
struct spk_flags {
|
struct spk_flags {
|
||||||
@ -132,6 +133,9 @@ player_get_status(struct player_status *status);
|
|||||||
int
|
int
|
||||||
player_now_playing(uint32_t *id);
|
player_now_playing(uint32_t *id);
|
||||||
|
|
||||||
|
int
|
||||||
|
player_icy_artwork_url(char **artwork_url, char *stream_url);
|
||||||
|
|
||||||
void
|
void
|
||||||
player_speaker_enumerate(spk_enum_cb cb, void *arg);
|
player_speaker_enumerate(spk_enum_cb cb, void *arg);
|
||||||
|
|
||||||
|
72
src/raop.c
72
src/raop.c
@ -61,6 +61,7 @@
|
|||||||
#include "evrtsp/evrtsp.h"
|
#include "evrtsp/evrtsp.h"
|
||||||
|
|
||||||
#include <gcrypt.h>
|
#include <gcrypt.h>
|
||||||
|
#include <pthread.h>
|
||||||
|
|
||||||
#include "conffile.h"
|
#include "conffile.h"
|
||||||
#include "logger.h"
|
#include "logger.h"
|
||||||
@ -157,6 +158,14 @@ struct raop_metadata
|
|||||||
struct raop_metadata *next;
|
struct raop_metadata *next;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct raop_metadata_send_ctx
|
||||||
|
{
|
||||||
|
int id;
|
||||||
|
uint64_t rtptime;
|
||||||
|
uint64_t offset;
|
||||||
|
int startup;
|
||||||
|
};
|
||||||
|
|
||||||
struct raop_service
|
struct raop_service
|
||||||
{
|
{
|
||||||
int fd;
|
int fd;
|
||||||
@ -2162,17 +2171,25 @@ raop_metadata_startup_send(struct raop_session *rs)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
static void *
|
||||||
raop_metadata_send(int id, uint64_t rtptime, uint64_t offset, int startup)
|
raop_metadata_send_thread(void *arg)
|
||||||
{
|
{
|
||||||
|
struct raop_metadata_send_ctx *ctx = arg;
|
||||||
struct raop_session *rs;
|
struct raop_session *rs;
|
||||||
struct raop_metadata *rmd;
|
struct raop_metadata *rmd;
|
||||||
uint32_t delay;
|
uint32_t delay;
|
||||||
int ret;
|
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)
|
if (!rmd)
|
||||||
return;
|
goto no_metadata;
|
||||||
|
|
||||||
for (rs = sessions; rs; rs = rs->next)
|
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)
|
if (!rs->wants_metadata)
|
||||||
continue;
|
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)
|
if (ret < 0)
|
||||||
{
|
{
|
||||||
raop_session_failure(rs);
|
raop_session_failure(rs);
|
||||||
|
|
||||||
continue;
|
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 */
|
/* Volume handling */
|
||||||
static float
|
static float
|
||||||
|
@ -90,6 +90,7 @@ struct transcode_ctx {
|
|||||||
|
|
||||||
uint32_t duration;
|
uint32_t duration;
|
||||||
uint64_t samples;
|
uint64_t samples;
|
||||||
|
uint32_t icy_hash;
|
||||||
|
|
||||||
/* WAV header */
|
/* WAV header */
|
||||||
int wavhdr;
|
int wavhdr;
|
||||||
@ -894,3 +895,41 @@ transcode_needed(const char *user_agent, const char *client_codecs, char *file_c
|
|||||||
|
|
||||||
return 1;
|
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);
|
||||||
|
}
|
||||||
|
@ -7,6 +7,7 @@
|
|||||||
#else
|
#else
|
||||||
# include <event.h>
|
# include <event.h>
|
||||||
#endif
|
#endif
|
||||||
|
#include "icy.h"
|
||||||
|
|
||||||
struct transcode_ctx;
|
struct transcode_ctx;
|
||||||
|
|
||||||
@ -25,4 +26,10 @@ transcode_cleanup(struct transcode_ctx *ctx);
|
|||||||
int
|
int
|
||||||
transcode_needed(const char *user_agent, const char *client_codecs, char *file_codectype);
|
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__ */
|
#endif /* !__TRANSCODE_H__ */
|
||||||
|
Loading…
Reference in New Issue
Block a user