mirror of
https://github.com/owntone/owntone-server.git
synced 2025-01-14 08:15:02 -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 \
|
||||
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 \
|
||||
|
127
src/artwork.c
127
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;
|
||||
|
57
src/db.c
57
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)
|
||||
{
|
||||
|
4
src/db.h
4
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);
|
||||
|
||||
|
@ -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)
|
||||
|
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 */
|
||||
#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
|
||||
*
|
||||
|
@ -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);
|
||||
|
||||
|
72
src/raop.c
72
src/raop.c
@ -61,6 +61,7 @@
|
||||
#include "evrtsp/evrtsp.h"
|
||||
|
||||
#include <gcrypt.h>
|
||||
#include <pthread.h>
|
||||
|
||||
#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
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -7,6 +7,7 @@
|
||||
#else
|
||||
# include <event.h>
|
||||
#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__ */
|
||||
|
Loading…
Reference in New Issue
Block a user