mirror of
https://github.com/owntone/owntone-server.git
synced 2025-04-15 00:35:55 -04:00
Merge branch 'streamurl1'
This commit is contained in:
commit
e132b2fd25
@ -20,6 +20,7 @@ nobase_dist_doc_DATA = \
|
|||||||
README_ALSA.md \
|
README_ALSA.md \
|
||||||
README_SMARTPL.md \
|
README_SMARTPL.md \
|
||||||
README_PLAYER_WEBINTERFACE.md \
|
README_PLAYER_WEBINTERFACE.md \
|
||||||
|
README_RADIO_STREAMS.md \
|
||||||
scripts/pairinghelper.sh
|
scripts/pairinghelper.sh
|
||||||
|
|
||||||
EXTRA_DIST = \
|
EXTRA_DIST = \
|
||||||
|
@ -323,6 +323,10 @@ forked-daapd has support for smart playlists. How to create a smart playlist is
|
|||||||
documented in
|
documented in
|
||||||
[README_SMARTPL.md](https://github.com/ejurgensen/forked-daapd/blob/master/README_SMARTPL.md).
|
[README_SMARTPL.md](https://github.com/ejurgensen/forked-daapd/blob/master/README_SMARTPL.md).
|
||||||
|
|
||||||
|
If you're not satisfied with internet radio metadata that forked-daapd shows,
|
||||||
|
then you can read about tweaking it in
|
||||||
|
[README_RADIO_STREAMS.md](https://github.com/ejurgensen/forked-daapd/blob/master/README_RADIO_STREAMS.md).
|
||||||
|
|
||||||
|
|
||||||
## Artwork
|
## Artwork
|
||||||
|
|
||||||
|
97
README_RADIO_STREAMS.md
Normal file
97
README_RADIO_STREAMS.md
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
# forked-daapd and Radio Stream tweaking
|
||||||
|
|
||||||
|
Radio streams have many different ways in how metadata is sent. Many should
|
||||||
|
just work as expected, but a few may require some tweaking. If you are not
|
||||||
|
seeing expected title, track, artist, artwork in forked-daapd clients or web UI,
|
||||||
|
the following may help.
|
||||||
|
|
||||||
|
First, understand what and how the particular stream is sending information.
|
||||||
|
ffprobe is a command that can be used to interegrate most of the stream
|
||||||
|
information. `ffprobe <http://stream.url>` should give you some useful output,
|
||||||
|
look at the Metadata section, below is an example.
|
||||||
|
|
||||||
|
```
|
||||||
|
Metadata:
|
||||||
|
icy-br : 320
|
||||||
|
icy-description : DJ-mixed blend of modern and classic rock, electronica, world music, and more. Always 100% commercial-free
|
||||||
|
icy-genre : Eclectic
|
||||||
|
icy-name : Radio Paradise (320k aac)
|
||||||
|
icy-pub : 1
|
||||||
|
icy-url : https://radioparadise.com
|
||||||
|
StreamTitle : Depeche Mode - Strangelove
|
||||||
|
StreamUrl : http://img.radioparadise.com/covers/l/B000002LCI.jpg
|
||||||
|
```
|
||||||
|
|
||||||
|
In the example above, all tags are populated with correct information, no
|
||||||
|
modifications to forked-daapd configuration should be needed. Note that
|
||||||
|
StreamUrl points to the artwork image file.
|
||||||
|
|
||||||
|
|
||||||
|
Below is another example that will require some tweaks to forked-daapd, Notice
|
||||||
|
`icy-name` is blank and `StreamUrl` doesn't point to an image.
|
||||||
|
|
||||||
|
```
|
||||||
|
Metadata:
|
||||||
|
icy-br : 127
|
||||||
|
icy-pub : 0
|
||||||
|
icy-description : Unspecified description
|
||||||
|
icy-url :
|
||||||
|
icy-genre : various
|
||||||
|
icy-name :
|
||||||
|
StreamTitle : Pour Some Sugar On Me - Def Leppard
|
||||||
|
StreamUrl : https://radio.stream.domain/api9/eventdata/49790578
|
||||||
|
```
|
||||||
|
|
||||||
|
In the above, first fix is the blank name, second is the image artwork.
|
||||||
|
### 1) Stream Name/Title
|
||||||
|
Set the name with an EXTINF tag in the m3u playlist file:
|
||||||
|
|
||||||
|
```
|
||||||
|
#EXTM3U
|
||||||
|
#EXTINF:-1, - My Radio Stream Name
|
||||||
|
http://radio.stream.domain/stream.url
|
||||||
|
```
|
||||||
|
|
||||||
|
The format is basically `#EXTINF:<length>, <Artist Name> - <Artist Title>`.
|
||||||
|
Length is -1 since it's a stream, `<Artist Name>` was left blank since
|
||||||
|
`StreamTitle` is accurate in the Metadata but `<Artist Title>` was set to
|
||||||
|
`My Radio Stream Name` since `icy-name` was blank.
|
||||||
|
|
||||||
|
### 2) Artwork (and track duration)
|
||||||
|
If `StreamUrl` does not point directly to an artwork file then the link may be
|
||||||
|
to a json file that contains an artwork link. If so, you can make forked-daapd
|
||||||
|
download the file automatically and search for an artwork link, and also track
|
||||||
|
duration.
|
||||||
|
|
||||||
|
Try to download the file, e.g. with `curl "https://radio.stream.domain/api9/eventdata/49790578"`.
|
||||||
|
Let's assume you get something like this:
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"eventId": 49793707,
|
||||||
|
"eventStart": "2020-05-08 16:23:03",
|
||||||
|
"eventFinish": "2020-05-08 16:27:21",
|
||||||
|
"eventDuration": 254,
|
||||||
|
"eventType": "Song",
|
||||||
|
"eventSongTitle": "Pour Some Sugar On Me",
|
||||||
|
"eventSongArtist": "Def Leppard",
|
||||||
|
"eventImageUrl": "https://radio.stream.domain/artist/1-1/320x320/562.jpg?ver=1465083491",
|
||||||
|
"eventImageUrlSmall": "https://radio.stream.domain/artist/1-1/160x160/562.jpg?ver=1465083491",
|
||||||
|
"eventAppleMusicUrl": "https://geo.itunes.apple.com/dk/album/530707298?i=530707313"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
In this case, you would need to tell forked-daapd to look for "eventDuration"
|
||||||
|
and "eventImageUrl" (or just "duration" and "url"). You can do that like this:
|
||||||
|
|
||||||
|
```
|
||||||
|
curl -X PUT "http://localhost:3689/api/settings/misc/streamurl_keywords_length" --data "{\"name\":\"streamurl_keywords_length\",\"value\":\"duration\"}"
|
||||||
|
curl -X PUT "http://localhost:3689/api/settings/misc/streamurl_keywords_artwork_url" --data "{\"name\":\"streamurl_keywords_artwork_url\",\"value\":\"url\"}
|
||||||
|
```
|
||||||
|
|
||||||
|
If you want multiple search phrases then comma separate, e.g. "duration,length".
|
||||||
|
|
||||||
|
|
||||||
|
If your radio station is not returning any artwork links, you can also just make
|
||||||
|
a static artwork by placing a png/jpg in the same directory as the m3u, and with
|
||||||
|
the same name, e.g. `My Radio Stream.jpg` for `My Radio Stream.m3u`.
|
@ -130,7 +130,7 @@ forked_daapd_SOURCES = main.c \
|
|||||||
worker.c worker.h \
|
worker.c worker.h \
|
||||||
settings.c settings.h \
|
settings.c settings.h \
|
||||||
input.h input.c \
|
input.h input.c \
|
||||||
inputs/file_http.c inputs/pipe.c inputs/timer.c \
|
inputs/file.c inputs/http.c inputs/pipe.c inputs/timer.c \
|
||||||
outputs.h outputs.c \
|
outputs.h outputs.c \
|
||||||
outputs/rtp_common.h outputs/rtp_common.c \
|
outputs/rtp_common.h outputs/rtp_common.c \
|
||||||
outputs/raop.c $(RAOP_VERIFICATION_SRC) \
|
outputs/raop.c $(RAOP_VERIFICATION_SRC) \
|
||||||
|
@ -26,6 +26,7 @@
|
|||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
#include <strings.h>
|
||||||
#include <sys/types.h>
|
#include <sys/types.h>
|
||||||
#include <sys/stat.h>
|
#include <sys/stat.h>
|
||||||
#include <fcntl.h>
|
#include <fcntl.h>
|
||||||
@ -193,7 +194,7 @@ static int source_group_dir_get(struct artwork_ctx *ctx);
|
|||||||
static int source_item_cache_get(struct artwork_ctx *ctx);
|
static int source_item_cache_get(struct artwork_ctx *ctx);
|
||||||
static int source_item_embedded_get(struct artwork_ctx *ctx);
|
static int source_item_embedded_get(struct artwork_ctx *ctx);
|
||||||
static int source_item_own_get(struct artwork_ctx *ctx);
|
static int source_item_own_get(struct artwork_ctx *ctx);
|
||||||
static int source_item_stream_get(struct artwork_ctx *ctx);
|
static int source_item_artwork_url_get(struct artwork_ctx *ctx);
|
||||||
static int source_item_pipe_get(struct artwork_ctx *ctx);
|
static int source_item_pipe_get(struct artwork_ctx *ctx);
|
||||||
static int source_item_spotifywebapi_track_get(struct artwork_ctx *ctx);
|
static int source_item_spotifywebapi_track_get(struct artwork_ctx *ctx);
|
||||||
static int source_item_ownpl_get(struct artwork_ctx *ctx);
|
static int source_item_ownpl_get(struct artwork_ctx *ctx);
|
||||||
@ -254,7 +255,7 @@ static struct artwork_source artwork_item_source[] =
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
.name = "stream",
|
.name = "stream",
|
||||||
.handler = source_item_stream_get,
|
.handler = source_item_artwork_url_get,
|
||||||
.data_kinds = (1 << DATA_KIND_HTTP),
|
.data_kinds = (1 << DATA_KIND_HTTP),
|
||||||
.media_kinds = MEDIA_KIND_MUSIC,
|
.media_kinds = MEDIA_KIND_MUSIC,
|
||||||
.cache = STASH,
|
.cache = STASH,
|
||||||
@ -274,11 +275,14 @@ static struct artwork_source artwork_item_source[] =
|
|||||||
.cache = ON_SUCCESS | ON_FAILURE,
|
.cache = ON_SUCCESS | ON_FAILURE,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
// Here we must use STASH because this handler must always just be a
|
||||||
|
// backup when artwork_url_get fails. If we used ON_SUCCESS this image
|
||||||
|
// would go in permanent cache, and artwork_url_get not get called again.
|
||||||
.name = "playlist own",
|
.name = "playlist own",
|
||||||
.handler = source_item_ownpl_get,
|
.handler = source_item_ownpl_get,
|
||||||
.data_kinds = (1 << DATA_KIND_HTTP),
|
.data_kinds = (1 << DATA_KIND_HTTP),
|
||||||
.media_kinds = MEDIA_KIND_ALL,
|
.media_kinds = MEDIA_KIND_ALL,
|
||||||
.cache = ON_SUCCESS | ON_FAILURE,
|
.cache = STASH,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
.name = "Spotify search web api (files)",
|
.name = "Spotify search web api (files)",
|
||||||
@ -1503,25 +1507,15 @@ source_item_own_get(struct artwork_ctx *ctx)
|
|||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Downloads the artwork pointed to by the ICY metadata tag in an internet radio
|
* Downloads the artwork from the location pointed to by queue_item->artwork_url
|
||||||
* stream (the StreamUrl tag). The path will be converted back to the id, which
|
|
||||||
* is given to the player. If the id is currently being played, and there is a
|
|
||||||
* valid ICY metadata artwork URL available, it will be returned to this
|
|
||||||
* function, which will then use the http client to get the artwork.
|
|
||||||
*/
|
*/
|
||||||
static int
|
static int
|
||||||
source_item_stream_get(struct artwork_ctx *ctx)
|
source_item_artwork_url_get(struct artwork_ctx *ctx)
|
||||||
{
|
{
|
||||||
struct db_queue_item *queue_item;
|
struct db_queue_item *queue_item;
|
||||||
char *url;
|
|
||||||
char *ext;
|
|
||||||
int len;
|
|
||||||
int ret;
|
int ret;
|
||||||
|
|
||||||
|
DPRINTF(E_SPAM, L_ART, "Trying artwork url for %s\n", ctx->dbmfi->path);
|
||||||
DPRINTF(E_SPAM, L_ART, "Trying internet stream artwork in %s\n", ctx->dbmfi->path);
|
|
||||||
|
|
||||||
ret = ART_E_NONE;
|
|
||||||
|
|
||||||
queue_item = db_queue_fetch_byfileid(ctx->id);
|
queue_item = db_queue_fetch_byfileid(ctx->id);
|
||||||
if (!queue_item || !queue_item->artwork_url)
|
if (!queue_item || !queue_item->artwork_url)
|
||||||
@ -1530,24 +1524,12 @@ source_item_stream_get(struct artwork_ctx *ctx)
|
|||||||
return ART_E_NONE;
|
return ART_E_NONE;
|
||||||
}
|
}
|
||||||
|
|
||||||
url = strdup(queue_item->artwork_url);
|
ret = artwork_get_byurl(ctx->evbuf, queue_item->artwork_url, ctx->max_w, ctx->max_h);
|
||||||
|
|
||||||
|
snprintf(ctx->path, sizeof(ctx->path), "%s", queue_item->artwork_url);
|
||||||
|
|
||||||
free_queue_item(queue_item, 0);
|
free_queue_item(queue_item, 0);
|
||||||
|
|
||||||
len = strlen(url);
|
|
||||||
if ((len < 14) || (len > PATH_MAX)) // Can't be shorter than http://a/1.jpg
|
|
||||||
goto out_url;
|
|
||||||
|
|
||||||
ext = strrchr(url, '.');
|
|
||||||
if (!ext)
|
|
||||||
goto out_url;
|
|
||||||
if ((strcmp(ext, ".jpg") != 0) && (strcmp(ext, ".png") != 0))
|
|
||||||
goto out_url;
|
|
||||||
|
|
||||||
ret = artwork_get_byurl(ctx->evbuf, url, ctx->max_w, ctx->max_h);
|
|
||||||
|
|
||||||
out_url:
|
|
||||||
free(url);
|
|
||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2024,7 +2006,7 @@ artwork_get_group(struct evbuffer *evbuf, int id, int max_w, int max_h)
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Checks if the file is an artwork file */
|
/* Checks if the file is an artwork file */
|
||||||
int
|
bool
|
||||||
artwork_file_is_artwork(const char *filename)
|
artwork_file_is_artwork(const char *filename)
|
||||||
{
|
{
|
||||||
cfg_t *lib;
|
cfg_t *lib;
|
||||||
@ -2039,7 +2021,7 @@ artwork_file_is_artwork(const char *filename)
|
|||||||
|
|
||||||
for (i = 0; i < n; i++)
|
for (i = 0; i < n; i++)
|
||||||
{
|
{
|
||||||
for (j = 0; j < (sizeof(cover_extension) / sizeof(cover_extension[0])); j++)
|
for (j = 0; j < ARRAY_SIZE(cover_extension); j++)
|
||||||
{
|
{
|
||||||
ret = snprintf(artwork, sizeof(artwork), "%s.%s", cfg_getnstr(lib, "artwork_basenames", i), cover_extension[j]);
|
ret = snprintf(artwork, sizeof(artwork), "%s.%s", cfg_getnstr(lib, "artwork_basenames", i), cover_extension[j]);
|
||||||
if ((ret < 0) || (ret >= sizeof(artwork)))
|
if ((ret < 0) || (ret >= sizeof(artwork)))
|
||||||
@ -2049,12 +2031,42 @@ artwork_file_is_artwork(const char *filename)
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (strcmp(artwork, filename) == 0)
|
if (strcmp(artwork, filename) == 0)
|
||||||
return 1;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (j < (sizeof(cover_extension) / sizeof(cover_extension[0])))
|
if (j < ARRAY_SIZE(cover_extension))
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0;
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
artwork_extension_is_artwork(const char *path)
|
||||||
|
{
|
||||||
|
char *ext;
|
||||||
|
int len;
|
||||||
|
int i;
|
||||||
|
|
||||||
|
ext = strrchr(path, '.');
|
||||||
|
if (!ext)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
ext++;
|
||||||
|
|
||||||
|
for (i = 0; i < ARRAY_SIZE(cover_extension); i++)
|
||||||
|
{
|
||||||
|
len = strlen(cover_extension[i]);
|
||||||
|
|
||||||
|
if (strncasecmp(cover_extension[i], ext, len) != 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Check that after the extension we either have the end or "?"
|
||||||
|
if (ext[len] != '\0' && ext[len] != '?')
|
||||||
|
continue;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,7 @@
|
|||||||
#define ART_DEFAULT_WIDTH 600
|
#define ART_DEFAULT_WIDTH 600
|
||||||
|
|
||||||
#include <event2/buffer.h>
|
#include <event2/buffer.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Get the artwork image for an individual item (track)
|
* Get the artwork image for an individual item (track)
|
||||||
@ -38,9 +39,20 @@ artwork_get_group(struct evbuffer *evbuf, int id, int max_w, int max_h);
|
|||||||
* Checks if the file is an artwork file (based on user config)
|
* Checks if the file is an artwork file (based on user config)
|
||||||
*
|
*
|
||||||
* @in filename Name of the file
|
* @in filename Name of the file
|
||||||
* @return 1 if true, 0 if false
|
* @return true/false
|
||||||
*/
|
*/
|
||||||
int
|
bool
|
||||||
artwork_file_is_artwork(const char *filename);
|
artwork_file_is_artwork(const char *filename);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Checks if the path (or URL) has file extension that is recognized as a
|
||||||
|
* supported file type (e.g. ".jpg"). Also supports URL-encoded paths, e.g.
|
||||||
|
* http://foo.com/bar.jpg?something
|
||||||
|
*
|
||||||
|
* @in path Path to the file (can also be a URL)
|
||||||
|
* @return true/false
|
||||||
|
*/
|
||||||
|
bool
|
||||||
|
artwork_extension_is_artwork(const char *path);
|
||||||
|
|
||||||
#endif /* !__ARTWORK_H__ */
|
#endif /* !__ARTWORK_H__ */
|
||||||
|
58
src/http.c
58
src/http.c
@ -667,9 +667,9 @@ metadata_packet_get(struct http_icy_metadata *metadata, AVFormatContext *fmtctx)
|
|||||||
else
|
else
|
||||||
metadata->title = strdup(metadata->title);
|
metadata->title = strdup(metadata->title);
|
||||||
}
|
}
|
||||||
else if ((strncmp(icy_token, "StreamUrl", strlen("StreamUrl")) == 0) && !metadata->artwork_url && strlen(ptr) > 0)
|
else if ((strncmp(icy_token, "StreamUrl", strlen("StreamUrl")) == 0) && !metadata->url && strlen(ptr) > 0)
|
||||||
{
|
{
|
||||||
metadata->artwork_url = strdup(ptr);
|
metadata->url = strdup(ptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (end)
|
if (end)
|
||||||
@ -720,11 +720,11 @@ metadata_header_get(struct http_icy_metadata *metadata, AVFormatContext *fmtctx)
|
|||||||
if (ptr[0] == ' ')
|
if (ptr[0] == ' ')
|
||||||
ptr++;
|
ptr++;
|
||||||
|
|
||||||
if ((strncmp(icy_token, "icy-name", strlen("icy-name")) == 0) && !metadata->name)
|
if ((strncmp(icy_token, "icy-name", strlen("icy-name")) == 0) && ptr[0] != '\0' && !metadata->name)
|
||||||
metadata->name = strdup(ptr);
|
metadata->name = strdup(ptr);
|
||||||
else if ((strncmp(icy_token, "icy-description", strlen("icy-description")) == 0) && !metadata->description)
|
else if ((strncmp(icy_token, "icy-description", strlen("icy-description")) == 0) && ptr[0] != '\0' && !metadata->description)
|
||||||
metadata->description = strdup(ptr);
|
metadata->description = strdup(ptr);
|
||||||
else if ((strncmp(icy_token, "icy-genre", strlen("icy-genre")) == 0) && !metadata->genre)
|
else if ((strncmp(icy_token, "icy-genre", strlen("icy-genre")) == 0) && ptr[0] != '\0' && !metadata->genre)
|
||||||
metadata->genre = strdup(ptr);
|
metadata->genre = strdup(ptr);
|
||||||
|
|
||||||
icy_token = strtok_r(NULL, "\r\n", &save_pr);
|
icy_token = strtok_r(NULL, "\r\n", &save_pr);
|
||||||
@ -741,10 +741,7 @@ http_icy_metadata_get(AVFormatContext *fmtctx, int packet_only)
|
|||||||
int got_packet;
|
int got_packet;
|
||||||
int got_header;
|
int got_header;
|
||||||
|
|
||||||
metadata = malloc(sizeof(struct http_icy_metadata));
|
CHECK_NULL(L_HTTP, metadata = calloc(1, sizeof(struct http_icy_metadata)));
|
||||||
if (!metadata)
|
|
||||||
return NULL;
|
|
||||||
memset(metadata, 0, sizeof(struct http_icy_metadata));
|
|
||||||
|
|
||||||
got_packet = (metadata_packet_get(metadata, fmtctx) == 0);
|
got_packet = (metadata_packet_get(metadata, fmtctx) == 0);
|
||||||
got_header = (!packet_only) && (metadata_header_get(metadata, fmtctx) == 0);
|
got_header = (!packet_only) && (metadata_header_get(metadata, fmtctx) == 0);
|
||||||
@ -761,7 +758,7 @@ http_icy_metadata_get(AVFormatContext *fmtctx, int packet_only)
|
|||||||
metadata->genre,
|
metadata->genre,
|
||||||
metadata->title,
|
metadata->title,
|
||||||
metadata->artist,
|
metadata->artist,
|
||||||
metadata->artwork_url,
|
metadata->url,
|
||||||
metadata->hash
|
metadata->hash
|
||||||
);
|
);
|
||||||
*/
|
*/
|
||||||
@ -816,23 +813,20 @@ http_icy_metadata_get(AVFormatContext *fmtctx, int packet_only)
|
|||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
metadata = malloc(sizeof(struct http_icy_metadata));
|
CHECK_NULL(L_HTTP, metadata = calloc(1, sizeof(struct http_icy_metadata)));
|
||||||
if (!metadata)
|
|
||||||
return NULL;
|
|
||||||
memset(metadata, 0, sizeof(struct http_icy_metadata));
|
|
||||||
|
|
||||||
got_header = 0;
|
got_header = 0;
|
||||||
if ( (value = keyval_get(ctx.input_headers, "icy-name")) )
|
if ( (value = keyval_get(ctx.input_headers, "icy-name")) && value[0] != '\0' )
|
||||||
{
|
{
|
||||||
metadata->name = strdup(value);
|
metadata->name = strdup(value);
|
||||||
got_header = 1;
|
got_header = 1;
|
||||||
}
|
}
|
||||||
if ( (value = keyval_get(ctx.input_headers, "icy-description")) )
|
if ( (value = keyval_get(ctx.input_headers, "icy-description")) && value[0] != '\0' )
|
||||||
{
|
{
|
||||||
metadata->description = strdup(value);
|
metadata->description = strdup(value);
|
||||||
got_header = 1;
|
got_header = 1;
|
||||||
}
|
}
|
||||||
if ( (value = keyval_get(ctx.input_headers, "icy-genre")) )
|
if ( (value = keyval_get(ctx.input_headers, "icy-genre")) && value[0] != '\0' )
|
||||||
{
|
{
|
||||||
metadata->genre = strdup(value);
|
metadata->genre = strdup(value);
|
||||||
got_header = 1;
|
got_header = 1;
|
||||||
@ -853,7 +847,7 @@ http_icy_metadata_get(AVFormatContext *fmtctx, int packet_only)
|
|||||||
metadata->genre,
|
metadata->genre,
|
||||||
metadata->title,
|
metadata->title,
|
||||||
metadata->artist,
|
metadata->artist,
|
||||||
metadata->artwork_url,
|
metadata->url,
|
||||||
metadata->hash
|
metadata->hash
|
||||||
);*/
|
);*/
|
||||||
|
|
||||||
@ -864,24 +858,14 @@ http_icy_metadata_get(AVFormatContext *fmtctx, int packet_only)
|
|||||||
void
|
void
|
||||||
http_icy_metadata_free(struct http_icy_metadata *metadata, int content_only)
|
http_icy_metadata_free(struct http_icy_metadata *metadata, int content_only)
|
||||||
{
|
{
|
||||||
if (metadata->name)
|
if (!metadata)
|
||||||
free(metadata->name);
|
return;
|
||||||
|
|
||||||
if (metadata->description)
|
free(metadata->name);
|
||||||
free(metadata->description);
|
free(metadata->description);
|
||||||
|
free(metadata->genre);
|
||||||
if (metadata->genre)
|
free(metadata->title);
|
||||||
free(metadata->genre);
|
free(metadata->artist);
|
||||||
|
free(metadata->url);
|
||||||
if (metadata->title)
|
free(metadata);
|
||||||
free(metadata->title);
|
|
||||||
|
|
||||||
if (metadata->artist)
|
|
||||||
free(metadata->artist);
|
|
||||||
|
|
||||||
if (metadata->artwork_url)
|
|
||||||
free(metadata->artwork_url);
|
|
||||||
|
|
||||||
if (!content_only)
|
|
||||||
free(metadata);
|
|
||||||
}
|
}
|
||||||
|
@ -49,7 +49,7 @@ struct http_icy_metadata
|
|||||||
/* Track specific, comes from icy_metadata_packet */
|
/* Track specific, comes from icy_metadata_packet */
|
||||||
char *title;
|
char *title;
|
||||||
char *artist;
|
char *artist;
|
||||||
char *artwork_url;
|
char *url;
|
||||||
|
|
||||||
uint32_t hash;
|
uint32_t hash;
|
||||||
};
|
};
|
||||||
|
@ -133,7 +133,7 @@ struct input_definition
|
|||||||
/*
|
/*
|
||||||
* Transfer stream data to the player's input buffer. Data must be PCM-LE
|
* Transfer stream data to the player's input buffer. Data must be PCM-LE
|
||||||
* samples. The input evbuf will be drained on succesful write. This is to avoid
|
* samples. The input evbuf will be drained on succesful write. This is to avoid
|
||||||
* copying memory.
|
* copying memory. Thread-safe.
|
||||||
*
|
*
|
||||||
* @in evbuf Raw PCM_LE audio data to write
|
* @in evbuf Raw PCM_LE audio data to write
|
||||||
* @in evbuf Quality of the PCM (sample rate etc.)
|
* @in evbuf Quality of the PCM (sample rate etc.)
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2017 Espen Jurgensen
|
* Copyright (C) 2017-2020 Espen Jurgensen
|
||||||
*
|
*
|
||||||
* This program is free software; you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
@ -24,11 +24,15 @@
|
|||||||
#include <event2/buffer.h>
|
#include <event2/buffer.h>
|
||||||
|
|
||||||
#include "transcode.h"
|
#include "transcode.h"
|
||||||
#include "http.h"
|
|
||||||
#include "misc.h"
|
#include "misc.h"
|
||||||
#include "logger.h"
|
#include "logger.h"
|
||||||
#include "input.h"
|
#include "input.h"
|
||||||
|
|
||||||
|
/*---------------------------- Input implementation --------------------------*/
|
||||||
|
|
||||||
|
// Important! If you change any of the below then consider if the change also
|
||||||
|
// should be made in http.c
|
||||||
|
|
||||||
static int
|
static int
|
||||||
setup(struct input_source *source)
|
setup(struct input_source *source)
|
||||||
{
|
{
|
||||||
@ -49,20 +53,6 @@ setup(struct input_source *source)
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
static int
|
|
||||||
setup_http(struct input_source *source)
|
|
||||||
{
|
|
||||||
char *url;
|
|
||||||
|
|
||||||
if (http_stream_setup(&url, source->path) < 0)
|
|
||||||
return -1;
|
|
||||||
|
|
||||||
free(source->path);
|
|
||||||
source->path = url;
|
|
||||||
|
|
||||||
return setup(source);
|
|
||||||
}
|
|
||||||
|
|
||||||
static int
|
static int
|
||||||
stop(struct input_source *source)
|
stop(struct input_source *source)
|
||||||
{
|
{
|
||||||
@ -83,13 +73,11 @@ static int
|
|||||||
play(struct input_source *source)
|
play(struct input_source *source)
|
||||||
{
|
{
|
||||||
struct transcode_ctx *ctx = source->input_ctx;
|
struct transcode_ctx *ctx = source->input_ctx;
|
||||||
int icy_timer;
|
|
||||||
int ret;
|
int ret;
|
||||||
short flags;
|
|
||||||
|
|
||||||
// We set "wanted" to 1 because the read size doesn't matter to us
|
// We set "wanted" to 1 because the read size doesn't matter to us
|
||||||
// TODO optimize?
|
// TODO optimize?
|
||||||
ret = transcode(source->evbuf, &icy_timer, ctx, 1);
|
ret = transcode(source->evbuf, NULL, ctx, 1);
|
||||||
if (ret == 0)
|
if (ret == 0)
|
||||||
{
|
{
|
||||||
input_write(source->evbuf, &source->quality, INPUT_FLAG_EOF);
|
input_write(source->evbuf, &source->quality, INPUT_FLAG_EOF);
|
||||||
@ -103,9 +91,7 @@ play(struct input_source *source)
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
flags = (icy_timer ? INPUT_FLAG_METADATA : 0);
|
input_write(source->evbuf, &source->quality, 0);
|
||||||
|
|
||||||
input_write(source->evbuf, &source->quality, flags);
|
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@ -116,43 +102,6 @@ seek(struct input_source *source, int seek_ms)
|
|||||||
return transcode_seek(source->input_ctx, seek_ms);
|
return transcode_seek(source->input_ctx, seek_ms);
|
||||||
}
|
}
|
||||||
|
|
||||||
static int
|
|
||||||
seek_http(struct input_source *source, int seek_ms)
|
|
||||||
{
|
|
||||||
// Stream is live/unknown length so can't seek. We return 0 anyway, because
|
|
||||||
// it is valid for the input to request a seek, since the input is not
|
|
||||||
// supposed to concern itself about this.
|
|
||||||
if (source->len_ms == 0)
|
|
||||||
return 0;
|
|
||||||
|
|
||||||
return transcode_seek(source->input_ctx, seek_ms);
|
|
||||||
}
|
|
||||||
|
|
||||||
static int
|
|
||||||
metadata_get_http(struct input_metadata *metadata, struct input_source *source)
|
|
||||||
{
|
|
||||||
struct http_icy_metadata *m;
|
|
||||||
int changed;
|
|
||||||
|
|
||||||
m = transcode_metadata(source->input_ctx, &changed);
|
|
||||||
if (!m)
|
|
||||||
return -1;
|
|
||||||
|
|
||||||
if (!changed)
|
|
||||||
{
|
|
||||||
http_icy_metadata_free(m, 0);
|
|
||||||
return -1; // TODO Perhaps a problem since this prohibits the player updating metadata
|
|
||||||
}
|
|
||||||
|
|
||||||
swap_pointers(&metadata->artist, &m->artist);
|
|
||||||
// Note we map title to album, because clients should show stream name as titel
|
|
||||||
swap_pointers(&metadata->album, &m->title);
|
|
||||||
swap_pointers(&metadata->artwork_url, &m->artwork_url);
|
|
||||||
|
|
||||||
http_icy_metadata_free(m, 0);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
struct input_definition input_file =
|
struct input_definition input_file =
|
||||||
{
|
{
|
||||||
.name = "file",
|
.name = "file",
|
||||||
@ -163,15 +112,3 @@ struct input_definition input_file =
|
|||||||
.stop = stop,
|
.stop = stop,
|
||||||
.seek = seek,
|
.seek = seek,
|
||||||
};
|
};
|
||||||
|
|
||||||
struct input_definition input_http =
|
|
||||||
{
|
|
||||||
.name = "http",
|
|
||||||
.type = INPUT_TYPE_HTTP,
|
|
||||||
.disabled = 0,
|
|
||||||
.setup = setup_http,
|
|
||||||
.play = play,
|
|
||||||
.stop = stop,
|
|
||||||
.metadata_get = metadata_get_http,
|
|
||||||
.seek = seek_http
|
|
||||||
};
|
|
421
src/inputs/http.c
Normal file
421
src/inputs/http.c
Normal file
@ -0,0 +1,421 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2017-2020 Espen Jurgensen
|
||||||
|
*
|
||||||
|
* This program is free software; you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation; either version 2 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program; if not, write to the Free Software
|
||||||
|
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <string.h> // strcasestr
|
||||||
|
#include <strings.h> // strcasecmp
|
||||||
|
#include <pthread.h> // mutex
|
||||||
|
|
||||||
|
#include <event2/buffer.h>
|
||||||
|
|
||||||
|
#include "transcode.h"
|
||||||
|
#include "http.h"
|
||||||
|
#include "misc.h"
|
||||||
|
#include "misc_json.h"
|
||||||
|
#include "settings.h"
|
||||||
|
#include "logger.h"
|
||||||
|
#include "artwork.h"
|
||||||
|
#include "worker.h"
|
||||||
|
#include "input.h"
|
||||||
|
|
||||||
|
struct prepared_metadata
|
||||||
|
{
|
||||||
|
// Parsed metadata goes here
|
||||||
|
struct input_metadata parsed;
|
||||||
|
// Mutex to share the parsed metadata
|
||||||
|
pthread_mutex_t lock;
|
||||||
|
} prepared_metadata;
|
||||||
|
|
||||||
|
|
||||||
|
/* ------- Handling/parsing of StreamUrl tags from some http streams ---------*/
|
||||||
|
|
||||||
|
struct streamurl_map
|
||||||
|
{
|
||||||
|
const char *setting;
|
||||||
|
enum json_type jtype;
|
||||||
|
int (*parser)(struct input_metadata *, const char *, json_object *);
|
||||||
|
char *words;
|
||||||
|
};
|
||||||
|
|
||||||
|
static int
|
||||||
|
streamurl_parse_artwork_url(struct input_metadata *metadata, const char *key, json_object *val)
|
||||||
|
{
|
||||||
|
const char *url = json_object_get_string(val);
|
||||||
|
|
||||||
|
if (metadata->artwork_url)
|
||||||
|
return -1; // Already found artwork
|
||||||
|
|
||||||
|
if (!artwork_extension_is_artwork(url))
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
metadata->artwork_url = strdup(url);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
streamurl_parse_length(struct input_metadata *metadata, const char *key, json_object *val)
|
||||||
|
{
|
||||||
|
int len = json_object_get_int(val);
|
||||||
|
|
||||||
|
if (len <= 0 || len > 7200)
|
||||||
|
return -1; // We expect seconds, so if it is longer than 2 hours we are probably wrong
|
||||||
|
|
||||||
|
metadata->len_ms = len * 1000;
|
||||||
|
metadata->pos_is_updated = true;
|
||||||
|
metadata->pos_ms = 0;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lookup is case-insensitive and partial, first occurrence takes precedence
|
||||||
|
static struct streamurl_map streamurl_map[] =
|
||||||
|
{
|
||||||
|
{ "streamurl_keywords_artwork_url", json_type_string, streamurl_parse_artwork_url },
|
||||||
|
{ "streamurl_keywords_length", json_type_int, streamurl_parse_length },
|
||||||
|
};
|
||||||
|
|
||||||
|
static void
|
||||||
|
streamurl_field_parse(struct input_metadata *metadata, struct streamurl_map *map, const char *jkey, json_object *jval)
|
||||||
|
{
|
||||||
|
char *word;
|
||||||
|
char *ptr;
|
||||||
|
|
||||||
|
if (!map->words)
|
||||||
|
return;
|
||||||
|
|
||||||
|
for (word = atrim(strtok_r(map->words, ",", &ptr)); word; free(word), word = atrim(strtok_r(NULL, ",", &ptr)))
|
||||||
|
{
|
||||||
|
if (json_object_get_type(jval) != map->jtype)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (!strcasestr(jkey, word)) // True if e.g. word="duration" and jkey="eventDuration"
|
||||||
|
continue;
|
||||||
|
|
||||||
|
map->parser(metadata, jkey, jval);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
streamurl_json_parse(struct input_metadata *metadata, const char *body)
|
||||||
|
{
|
||||||
|
json_object *jresponse;
|
||||||
|
int i;
|
||||||
|
|
||||||
|
jresponse = json_tokener_parse(body);
|
||||||
|
if (!jresponse)
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
json_object_object_foreach(jresponse, jkey, jval)
|
||||||
|
{
|
||||||
|
for (i = 0; i < ARRAY_SIZE(streamurl_map); i++)
|
||||||
|
{
|
||||||
|
streamurl_field_parse(metadata, &streamurl_map[i], jkey, jval);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
jparse_free(jresponse);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
streamurl_settings_unload(void)
|
||||||
|
{
|
||||||
|
int i;
|
||||||
|
|
||||||
|
for (i = 0; i < ARRAY_SIZE(streamurl_map); i++)
|
||||||
|
{
|
||||||
|
free(streamurl_map[i].words);
|
||||||
|
streamurl_map[i].words = NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
streamurl_settings_load(void)
|
||||||
|
{
|
||||||
|
struct settings_category *category;
|
||||||
|
bool enabled;
|
||||||
|
int i;
|
||||||
|
|
||||||
|
category = settings_category_get("misc");
|
||||||
|
if (!category)
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
for (i = 0, enabled = false; i < ARRAY_SIZE(streamurl_map); i++)
|
||||||
|
{
|
||||||
|
streamurl_map[i].words = settings_option_getstr(settings_option_get(category, streamurl_map[i].setting));
|
||||||
|
if (streamurl_map[i].words)
|
||||||
|
enabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return enabled ? 0 : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
streamurl_process(struct input_metadata *metadata, const char *url)
|
||||||
|
{
|
||||||
|
struct http_client_ctx client = { 0 };
|
||||||
|
struct keyval kv = { 0 };
|
||||||
|
struct evbuffer *evbuf;
|
||||||
|
const char *content_type;
|
||||||
|
char *body;
|
||||||
|
int ret;
|
||||||
|
|
||||||
|
// If the user didn't configure any keywords to look for then we can stop now
|
||||||
|
ret = streamurl_settings_load();
|
||||||
|
if (ret < 0)
|
||||||
|
{
|
||||||
|
DPRINTF(E_DBG, L_PLAYER, "Ignoring StreamUrl resource '%s', no settings\n", url);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
DPRINTF(E_DBG, L_PLAYER, "Downloading StreamUrl resource '%s'\n", url);
|
||||||
|
|
||||||
|
CHECK_NULL(L_PLAYER, evbuf = evbuffer_new());
|
||||||
|
|
||||||
|
client.url = url;
|
||||||
|
client.input_headers = &kv;
|
||||||
|
client.input_body = evbuf;
|
||||||
|
|
||||||
|
ret = http_client_request(&client);
|
||||||
|
if (ret < 0 || client.response_code != HTTP_OK)
|
||||||
|
{
|
||||||
|
DPRINTF(E_WARN, L_PLAYER, "Request for StreamUrl resource '%s' failed, response code %d\n", url, client.response_code);
|
||||||
|
ret = -1;
|
||||||
|
goto out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 0-terminate for safety
|
||||||
|
evbuffer_add(evbuf, "", 1);
|
||||||
|
body = (char *)evbuffer_pullup(evbuf, -1);
|
||||||
|
|
||||||
|
content_type = keyval_get(&kv, "Content-Type");
|
||||||
|
if (content_type && strcasecmp(content_type, "application/json") == 0)
|
||||||
|
{
|
||||||
|
ret = streamurl_json_parse(metadata, body);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
DPRINTF(E_WARN, L_PLAYER, "No handler for StreamUrl resource '%s' with content type '%s'\n", url, content_type);
|
||||||
|
ret = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
out:
|
||||||
|
keyval_clear(&kv);
|
||||||
|
evbuffer_free(evbuf);
|
||||||
|
streamurl_settings_unload();
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Thread: worker
|
||||||
|
static void
|
||||||
|
streamurl_cb(void *arg)
|
||||||
|
{
|
||||||
|
struct input_metadata metadata = { 0 };
|
||||||
|
char *url = arg;
|
||||||
|
int ret;
|
||||||
|
|
||||||
|
ret = streamurl_process(&metadata, url);
|
||||||
|
if (ret < 0) // Only negative on error/unconfigured (not if no metadata)
|
||||||
|
return;
|
||||||
|
|
||||||
|
pthread_mutex_lock(&prepared_metadata.lock);
|
||||||
|
|
||||||
|
swap_pointers(&prepared_metadata.parsed.artwork_url, &metadata.artwork_url);
|
||||||
|
prepared_metadata.parsed.pos_is_updated = metadata.pos_is_updated;
|
||||||
|
prepared_metadata.parsed.pos_ms = metadata.pos_ms;
|
||||||
|
prepared_metadata.parsed.len_ms = metadata.len_ms;
|
||||||
|
|
||||||
|
pthread_mutex_unlock(&prepared_metadata.lock);
|
||||||
|
|
||||||
|
input_metadata_free(&metadata, 1);
|
||||||
|
|
||||||
|
input_write(NULL, NULL, INPUT_FLAG_METADATA);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*-------------------------------- http metadata -----------------------------*/
|
||||||
|
|
||||||
|
// Checks if there is new metadata, which means getting the ICY data plus the
|
||||||
|
// StreamTitle and StreamUrl fields from libav. If StreamUrl is not an artwork
|
||||||
|
// link then we also kick off async downloading of it.
|
||||||
|
static int
|
||||||
|
metadata_prepare(struct input_source *source)
|
||||||
|
{
|
||||||
|
struct http_icy_metadata *m;
|
||||||
|
int changed;
|
||||||
|
|
||||||
|
m = transcode_metadata(source->input_ctx, &changed);
|
||||||
|
if (!m)
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
if (!changed)
|
||||||
|
{
|
||||||
|
http_icy_metadata_free(m, 0);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
swap_pointers(&prepared_metadata.parsed.artist, &m->artist);
|
||||||
|
// Note we map title to album, because clients should show stream name as title
|
||||||
|
swap_pointers(&prepared_metadata.parsed.album, &m->title);
|
||||||
|
|
||||||
|
// In this case we have to go async to download the url and process the content
|
||||||
|
if (m->url && !artwork_extension_is_artwork(m->url))
|
||||||
|
worker_execute(streamurl_cb, m->url, strlen(m->url) + 1, 0);
|
||||||
|
else
|
||||||
|
swap_pointers(&prepared_metadata.parsed.artwork_url, &m->url);
|
||||||
|
|
||||||
|
http_icy_metadata_free(m, 0);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*---------------------------- Input implementation --------------------------*/
|
||||||
|
|
||||||
|
// Important! If you change any of the below then consider if the change also
|
||||||
|
// should be made in file.c
|
||||||
|
|
||||||
|
static int
|
||||||
|
setup(struct input_source *source)
|
||||||
|
{
|
||||||
|
struct transcode_ctx *ctx;
|
||||||
|
char *url;
|
||||||
|
|
||||||
|
if (http_stream_setup(&url, source->path) < 0)
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
free(source->path);
|
||||||
|
source->path = url;
|
||||||
|
|
||||||
|
ctx = transcode_setup(XCODE_PCM_NATIVE, NULL, source->data_kind, source->path, source->len_ms, NULL);
|
||||||
|
if (!ctx)
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
CHECK_NULL(L_PLAYER, source->evbuf = evbuffer_new());
|
||||||
|
|
||||||
|
source->quality.sample_rate = transcode_encode_query(ctx->encode_ctx, "sample_rate");
|
||||||
|
source->quality.bits_per_sample = transcode_encode_query(ctx->encode_ctx, "bits_per_sample");
|
||||||
|
source->quality.channels = transcode_encode_query(ctx->encode_ctx, "channels");
|
||||||
|
|
||||||
|
source->input_ctx = ctx;
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
stop(struct input_source *source)
|
||||||
|
{
|
||||||
|
struct transcode_ctx *ctx = source->input_ctx;
|
||||||
|
|
||||||
|
transcode_cleanup(&ctx);
|
||||||
|
|
||||||
|
if (source->evbuf)
|
||||||
|
evbuffer_free(source->evbuf);
|
||||||
|
|
||||||
|
source->input_ctx = NULL;
|
||||||
|
source->evbuf = NULL;
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
play(struct input_source *source)
|
||||||
|
{
|
||||||
|
struct transcode_ctx *ctx = source->input_ctx;
|
||||||
|
int icy_timer;
|
||||||
|
int ret;
|
||||||
|
short flags;
|
||||||
|
|
||||||
|
// We set "wanted" to 1 because the read size doesn't matter to us
|
||||||
|
// TODO optimize?
|
||||||
|
ret = transcode(source->evbuf, &icy_timer, ctx, 1);
|
||||||
|
if (ret == 0)
|
||||||
|
{
|
||||||
|
input_write(source->evbuf, &source->quality, INPUT_FLAG_EOF);
|
||||||
|
stop(source);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
else if (ret < 0)
|
||||||
|
{
|
||||||
|
input_write(NULL, NULL, INPUT_FLAG_ERROR);
|
||||||
|
stop(source);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
flags = (icy_timer && metadata_prepare(source) == 0) ? INPUT_FLAG_METADATA : 0;
|
||||||
|
|
||||||
|
input_write(source->evbuf, &source->quality, flags);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
seek(struct input_source *source, int seek_ms)
|
||||||
|
{
|
||||||
|
// Stream is live/unknown length so can't seek. We return 0 anyway, because
|
||||||
|
// it is valid for the input to request a seek, since the input is not
|
||||||
|
// supposed to concern itself about this.
|
||||||
|
if (source->len_ms == 0)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
return transcode_seek(source->input_ctx, seek_ms);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
metadata_get(struct input_metadata *metadata, struct input_source *source)
|
||||||
|
{
|
||||||
|
pthread_mutex_lock(&prepared_metadata.lock);
|
||||||
|
|
||||||
|
*metadata = prepared_metadata.parsed;
|
||||||
|
|
||||||
|
// Ownership transferred to caller, null all pointers in the struct
|
||||||
|
memset(&prepared_metadata.parsed, 0, sizeof(struct input_metadata));
|
||||||
|
|
||||||
|
pthread_mutex_unlock(&prepared_metadata.lock);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
init(void)
|
||||||
|
{
|
||||||
|
CHECK_ERR(L_PLAYER, mutex_init(&prepared_metadata.lock));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
deinit(void)
|
||||||
|
{
|
||||||
|
CHECK_ERR(L_PLAYER, pthread_mutex_destroy(&prepared_metadata.lock));
|
||||||
|
}
|
||||||
|
|
||||||
|
struct input_definition input_http =
|
||||||
|
{
|
||||||
|
.name = "http",
|
||||||
|
.type = INPUT_TYPE_HTTP,
|
||||||
|
.disabled = 0,
|
||||||
|
.setup = setup,
|
||||||
|
.play = play,
|
||||||
|
.stop = stop,
|
||||||
|
.metadata_get = metadata_get,
|
||||||
|
.seek = seek,
|
||||||
|
.init = init,
|
||||||
|
.deinit = deinit,
|
||||||
|
};
|
@ -42,10 +42,17 @@ static struct settings_option artwork_options[] =
|
|||||||
{ "use_artwork_source_coverartarchive", SETTINGS_TYPE_BOOL, NULL, artwork_coverartarchive_default_getbool, NULL },
|
{ "use_artwork_source_coverartarchive", SETTINGS_TYPE_BOOL, NULL, artwork_coverartarchive_default_getbool, NULL },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
static struct settings_option misc_options[] =
|
||||||
|
{
|
||||||
|
{ "streamurl_keywords_artwork_url", SETTINGS_TYPE_STR },
|
||||||
|
{ "streamurl_keywords_length", SETTINGS_TYPE_STR },
|
||||||
|
};
|
||||||
|
|
||||||
static struct settings_category categories[] =
|
static struct settings_category categories[] =
|
||||||
{
|
{
|
||||||
{ "webinterface", webinterface_options, ARRAY_SIZE(webinterface_options) },
|
{ "webinterface", webinterface_options, ARRAY_SIZE(webinterface_options) },
|
||||||
{ "artwork", artwork_options, ARRAY_SIZE(artwork_options) },
|
{ "artwork", artwork_options, ARRAY_SIZE(artwork_options) },
|
||||||
|
{ "misc", misc_options, ARRAY_SIZE(misc_options) },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@ -94,7 +101,6 @@ artwork_coverartarchive_default_getbool(struct settings_option *option)
|
|||||||
return artwork_default_getbool(false, "coverartarchive");
|
return artwork_default_getbool(false, "coverartarchive");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* ------------------------------ IMPLEMENTATION -----------------------------*/
|
/* ------------------------------ IMPLEMENTATION -----------------------------*/
|
||||||
|
|
||||||
int
|
int
|
||||||
|
Loading…
x
Reference in New Issue
Block a user