mirror of
https://github.com/owntone/owntone-server.git
synced 2024-12-27 15:45:56 -05:00
[player/outputs] New metadata handling (wip)
This commit is contained in:
parent
ab0a6055b9
commit
4fb45e84f2
147
src/outputs.c
147
src/outputs.c
@ -37,6 +37,7 @@
|
||||
#include "listener.h"
|
||||
#include "db.h"
|
||||
#include "player.h" //TODO remove me when player_pmap is removed again
|
||||
#include "worker.h"
|
||||
#include "outputs.h"
|
||||
|
||||
extern struct output_definition output_raop;
|
||||
@ -418,6 +419,60 @@ device_list_sort(void)
|
||||
while (swaps > 0);
|
||||
}
|
||||
|
||||
static void
|
||||
metadata_cb_send(int fd, short what, void *arg)
|
||||
{
|
||||
struct output_metadata *metadata = arg;
|
||||
int ret;
|
||||
|
||||
event_free(metadata->ev);
|
||||
metadata->ev = NULL;
|
||||
|
||||
ret = metadata->finalize_cb(metadata);
|
||||
if (ret < 0)
|
||||
return;
|
||||
|
||||
outputs[metadata->type]->metadata_send(metadata);
|
||||
}
|
||||
|
||||
// *** Worker thread ***
|
||||
static void
|
||||
metadata_cb_prepare(void *arg)
|
||||
{
|
||||
struct output_metadata *metadata = *((struct output_metadata **)arg);
|
||||
|
||||
metadata->priv = outputs[metadata->type]->metadata_prepare(metadata);
|
||||
if (!metadata->priv)
|
||||
{
|
||||
event_free(metadata->ev);
|
||||
free(metadata);
|
||||
return;
|
||||
}
|
||||
|
||||
// Metadata is prepared, let the player thread do the actual sending
|
||||
event_active(metadata->ev, 0, 0);
|
||||
}
|
||||
|
||||
static void
|
||||
metadata_send(enum output_types type, uint32_t item_id, bool startup, output_metadata_finalize_cb cb)
|
||||
{
|
||||
struct output_metadata *metadata;
|
||||
|
||||
CHECK_NULL(L_PLAYER, metadata = calloc(1, sizeof(struct output_metadata)));
|
||||
|
||||
metadata->type = type;
|
||||
metadata->item_id = item_id;
|
||||
metadata->startup = startup;
|
||||
metadata->finalize_cb = cb;
|
||||
|
||||
metadata->ev = event_new(evbase_player, -1, 0, metadata_cb_send, metadata);
|
||||
|
||||
if (outputs[type]->metadata_prepare)
|
||||
worker_execute(metadata_cb_prepare, &metadata, sizeof(struct output_metadata *), 0);
|
||||
else
|
||||
outputs[type]->metadata_send(metadata);
|
||||
}
|
||||
|
||||
|
||||
/* ----------------------------------- API ---------------------------------- */
|
||||
|
||||
@ -729,6 +784,12 @@ outputs_device_stop_delayed(struct output_device *device, output_status_cb cb)
|
||||
if (outputs[device->type]->disabled || !outputs[device->type]->device_stop)
|
||||
return -1;
|
||||
|
||||
if (!device->session)
|
||||
{
|
||||
DPRINTF(E_LOG, L_PLAYER, "Bug! outputs_device_stop_delayed() called for a device that has no session\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
outputs[device->type]->device_cb_set(device, callback_add(device, cb));
|
||||
|
||||
event_add(device->stop_timer, &outputs_stop_timeout);
|
||||
@ -899,64 +960,17 @@ outputs_write(void *buf, size_t bufsize, int nsamples, struct media_quality *qua
|
||||
buffer_drain(&output_buffer);
|
||||
}
|
||||
|
||||
struct output_metadata *
|
||||
outputs_metadata_prepare(int id)
|
||||
{
|
||||
struct output_metadata *omd;
|
||||
struct output_metadata *new;
|
||||
void *metadata;
|
||||
int i;
|
||||
|
||||
omd = NULL;
|
||||
for (i = 0; outputs[i]; i++)
|
||||
{
|
||||
if (outputs[i]->disabled)
|
||||
continue;
|
||||
|
||||
if (!outputs[i]->metadata_prepare)
|
||||
continue;
|
||||
|
||||
metadata = outputs[i]->metadata_prepare(id);
|
||||
if (!metadata)
|
||||
continue;
|
||||
|
||||
new = calloc(1, sizeof(struct output_metadata));
|
||||
if (!new)
|
||||
return omd;
|
||||
|
||||
if (omd)
|
||||
new->next = omd;
|
||||
omd = new;
|
||||
omd->type = i;
|
||||
omd->metadata = metadata;
|
||||
}
|
||||
|
||||
return omd;
|
||||
}
|
||||
|
||||
void
|
||||
outputs_metadata_send(struct output_metadata *omd, uint64_t rtptime, uint64_t offset, int startup)
|
||||
outputs_metadata_send(uint32_t item_id, bool startup, output_metadata_finalize_cb cb)
|
||||
{
|
||||
struct output_metadata *ptr;
|
||||
int i;
|
||||
|
||||
for (i = 0; outputs[i]; i++)
|
||||
{
|
||||
if (outputs[i]->disabled)
|
||||
if (outputs[i]->disabled || !outputs[i]->metadata_send)
|
||||
continue;
|
||||
|
||||
if (!outputs[i]->metadata_send)
|
||||
continue;
|
||||
|
||||
// Go through linked list to find appropriate metadata for type
|
||||
for (ptr = omd; ptr; ptr = ptr->next)
|
||||
if (ptr->type == i)
|
||||
break;
|
||||
|
||||
if (!ptr)
|
||||
continue;
|
||||
|
||||
outputs[i]->metadata_send(ptr->metadata, rtptime, offset, startup);
|
||||
metadata_send(i, item_id, startup, cb);
|
||||
}
|
||||
}
|
||||
|
||||
@ -967,44 +981,13 @@ outputs_metadata_purge(void)
|
||||
|
||||
for (i = 0; outputs[i]; i++)
|
||||
{
|
||||
if (outputs[i]->disabled)
|
||||
if (outputs[i]->disabled || !outputs[i]->metadata_purge)
|
||||
continue;
|
||||
|
||||
if (outputs[i]->metadata_purge)
|
||||
outputs[i]->metadata_purge();
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
outputs_metadata_prune(uint64_t rtptime)
|
||||
{
|
||||
int i;
|
||||
|
||||
for (i = 0; outputs[i]; i++)
|
||||
{
|
||||
if (outputs[i]->disabled)
|
||||
continue;
|
||||
|
||||
if (outputs[i]->metadata_prune)
|
||||
outputs[i]->metadata_prune(rtptime);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
outputs_metadata_free(struct output_metadata *omd)
|
||||
{
|
||||
struct output_metadata *ptr;
|
||||
|
||||
if (!omd)
|
||||
return;
|
||||
|
||||
for (ptr = omd; omd; ptr = omd)
|
||||
{
|
||||
omd = ptr->next;
|
||||
free(ptr);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
outputs_authorize(enum output_types type, const char *pin)
|
||||
{
|
||||
|
@ -2,6 +2,7 @@
|
||||
#ifndef __OUTPUTS_H__
|
||||
#define __OUTPUTS_H__
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <time.h>
|
||||
#include <event2/event.h>
|
||||
#include <event2/buffer.h>
|
||||
@ -62,6 +63,14 @@
|
||||
// different values can only do so within a limited range (maybe max 3 secs)
|
||||
#define OUTPUTS_BUFFER_DURATION 2
|
||||
|
||||
// Forward declarations
|
||||
struct output_device;
|
||||
struct output_metadata;
|
||||
enum output_device_state;
|
||||
|
||||
typedef void (*output_status_cb)(struct output_device *device, enum output_device_state status);
|
||||
typedef int (*output_metadata_finalize_cb)(struct output_metadata *metadata);
|
||||
|
||||
// Must be in sync with outputs[] in outputs.c
|
||||
enum output_types
|
||||
{
|
||||
@ -147,13 +156,24 @@ struct output_device
|
||||
struct output_device *next;
|
||||
};
|
||||
|
||||
// Linked list of metadata prepared by each output backend
|
||||
struct output_metadata
|
||||
{
|
||||
enum output_types type;
|
||||
void *metadata;
|
||||
uint32_t item_id;
|
||||
|
||||
struct output_metadata *next;
|
||||
// Progress data, filled out by finalize_cb()
|
||||
uint32_t pos_ms;
|
||||
uint32_t len_ms;
|
||||
struct timespec pts;
|
||||
bool startup;
|
||||
|
||||
// Private output data made by the metadata_prepare()
|
||||
void *priv;
|
||||
|
||||
struct event *ev;
|
||||
|
||||
// Finalize before right before sending, e.g. set playback position
|
||||
output_metadata_finalize_cb finalize_cb;
|
||||
};
|
||||
|
||||
struct output_data
|
||||
@ -172,8 +192,6 @@ struct output_buffer
|
||||
struct output_data data[OUTPUTS_MAX_QUALITY_SUBSCRIPTIONS + 1];
|
||||
} output_buffer;
|
||||
|
||||
typedef void (*output_status_cb)(struct output_device *device, enum output_device_state status);
|
||||
|
||||
struct output_definition
|
||||
{
|
||||
// Name of the output
|
||||
@ -229,11 +247,16 @@ struct output_definition
|
||||
// Authorize an output with a pin-code (probably coming from the filescanner)
|
||||
void (*authorize)(const char *pin);
|
||||
|
||||
// Metadata
|
||||
void *(*metadata_prepare)(int id);
|
||||
void (*metadata_send)(void *metadata, uint64_t rtptime, uint64_t offset, int startup);
|
||||
// Called from worker thread for async preparation of metadata (e.g. getting
|
||||
// artwork, which might involce downloading image data). The prepared data is
|
||||
// saved to metadata->data, which metadata_send() can use.
|
||||
void *(*metadata_prepare)(struct output_metadata *metadata);
|
||||
|
||||
// Send metadata to outputs. Ownership of *metadata is transferred.
|
||||
void (*metadata_send)(struct output_metadata *metadata);
|
||||
|
||||
// Output will cleanup all metadata (so basically like flush but for metadata)
|
||||
void (*metadata_purge)(void);
|
||||
void (*metadata_prune)(uint64_t rtptime);
|
||||
};
|
||||
|
||||
// Our main list of devices, not for use by backend modules
|
||||
@ -317,21 +340,12 @@ outputs_stop_delayed_cancel(void);
|
||||
void
|
||||
outputs_write(void *buf, size_t bufsize, int nsamples, struct media_quality *quality, struct timespec *pts);
|
||||
|
||||
struct output_metadata *
|
||||
outputs_metadata_prepare(int id);
|
||||
|
||||
void
|
||||
outputs_metadata_send(struct output_metadata *omd, uint64_t rtptime, uint64_t offset, int startup);
|
||||
outputs_metadata_send(uint32_t item_id, bool startup, output_metadata_finalize_cb cb);
|
||||
|
||||
void
|
||||
outputs_metadata_purge(void);
|
||||
|
||||
void
|
||||
outputs_metadata_prune(uint64_t rtptime);
|
||||
|
||||
void
|
||||
outputs_metadata_free(struct output_metadata *omd);
|
||||
|
||||
void
|
||||
outputs_authorize(enum output_types type, const char *pin);
|
||||
|
||||
|
@ -89,7 +89,6 @@
|
||||
// AirTunes v2 number of samples per packet
|
||||
// Probably using this value because 44100/352 and 48000/352 has good 32 byte
|
||||
// alignment, which improves performance of some encoders
|
||||
// TODO Should probably not be fixed, but vary with quality
|
||||
#define RAOP_SAMPLES_PER_PACKET 352
|
||||
|
||||
// How many RTP packets keep in a buffer for retransmission
|
||||
@ -98,7 +97,7 @@
|
||||
#define RAOP_MD_DELAY_STARTUP 15360
|
||||
#define RAOP_MD_DELAY_SWITCH (RAOP_MD_DELAY_STARTUP * 2)
|
||||
|
||||
/* This is an arbitrary value which just needs to be kept in sync with the config */
|
||||
// This is an arbitrary value which just needs to be kept in sync with the config
|
||||
#define RAOP_CONFIG_MAX_VOLUME 11
|
||||
|
||||
union sockaddr_all
|
||||
@ -239,15 +238,8 @@ struct raop_session
|
||||
struct raop_metadata
|
||||
{
|
||||
struct evbuffer *metadata;
|
||||
|
||||
struct evbuffer *artwork;
|
||||
int artwork_fmt;
|
||||
|
||||
/* Progress data */
|
||||
uint32_t start;
|
||||
uint32_t end;
|
||||
|
||||
struct raop_metadata *next;
|
||||
};
|
||||
|
||||
struct raop_service
|
||||
@ -339,8 +331,7 @@ static struct raop_service control_4svc;
|
||||
static struct raop_service control_6svc;
|
||||
|
||||
/* Metadata */
|
||||
static struct raop_metadata *metadata_head;
|
||||
static struct raop_metadata *metadata_tail;
|
||||
static struct output_metadata *raop_cur_metadata;
|
||||
|
||||
/* Keep-alive timer - hack for ATV's with tvOS 10 */
|
||||
static struct event *keep_alive_timer;
|
||||
@ -2164,138 +2155,70 @@ session_make(struct output_device *rd, int family, int callback_id, bool only_pr
|
||||
static void
|
||||
raop_metadata_free(struct raop_metadata *rmd)
|
||||
{
|
||||
if (!rmd)
|
||||
return;
|
||||
|
||||
if (rmd->metadata)
|
||||
evbuffer_free(rmd->metadata);
|
||||
if (rmd->artwork)
|
||||
evbuffer_free(rmd->artwork);
|
||||
|
||||
free(rmd);
|
||||
}
|
||||
|
||||
static void
|
||||
raop_metadata_purge(void)
|
||||
{
|
||||
struct raop_metadata *rmd;
|
||||
if (!raop_cur_metadata)
|
||||
return;
|
||||
|
||||
for (rmd = metadata_head; rmd; rmd = metadata_head)
|
||||
{
|
||||
metadata_head = rmd->next;
|
||||
|
||||
raop_metadata_free(rmd);
|
||||
}
|
||||
|
||||
metadata_tail = NULL;
|
||||
raop_metadata_free(raop_cur_metadata->priv);
|
||||
free(raop_cur_metadata);
|
||||
raop_cur_metadata = NULL;
|
||||
}
|
||||
|
||||
static void
|
||||
raop_metadata_prune(uint64_t rtptime)
|
||||
{
|
||||
struct raop_metadata *rmd;
|
||||
|
||||
for (rmd = metadata_head; rmd; rmd = metadata_head)
|
||||
{
|
||||
if (rmd->end >= rtptime)
|
||||
break;
|
||||
|
||||
if (metadata_tail == metadata_head)
|
||||
metadata_tail = rmd->next;
|
||||
|
||||
metadata_head = rmd->next;
|
||||
|
||||
raop_metadata_free(rmd);
|
||||
}
|
||||
}
|
||||
|
||||
/* Thread: worker */
|
||||
// *** Thread: worker ***
|
||||
static void *
|
||||
raop_metadata_prepare(int id)
|
||||
raop_metadata_prepare(struct output_metadata *metadata)
|
||||
{
|
||||
struct db_queue_item *queue_item;
|
||||
struct raop_metadata *rmd;
|
||||
struct evbuffer *tmp;
|
||||
int ret;
|
||||
|
||||
rmd = (struct raop_metadata *)malloc(sizeof(struct raop_metadata));
|
||||
if (!rmd)
|
||||
queue_item = db_queue_fetch_byitemid(metadata->item_id);
|
||||
if (!queue_item)
|
||||
{
|
||||
DPRINTF(E_LOG, L_RAOP, "Out of memory for RAOP metadata\n");
|
||||
|
||||
DPRINTF(E_LOG, L_RAOP, "Could not fetch queue item\n");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
memset(rmd, 0, sizeof(struct raop_metadata));
|
||||
|
||||
queue_item = db_queue_fetch_byitemid(id);
|
||||
if (!queue_item)
|
||||
{
|
||||
DPRINTF(E_LOG, L_RAOP, "Out of memory for queue item\n");
|
||||
|
||||
goto out_rmd;
|
||||
}
|
||||
|
||||
/* Get artwork */
|
||||
rmd->artwork = evbuffer_new();
|
||||
if (!rmd->artwork)
|
||||
{
|
||||
DPRINTF(E_LOG, L_RAOP, "Out of memory for artwork evbuffer; no artwork will be sent\n");
|
||||
|
||||
goto skip_artwork;
|
||||
}
|
||||
CHECK_NULL(L_RAOP, rmd = calloc(1, sizeof(struct raop_metadata)));
|
||||
CHECK_NULL(L_RAOP, rmd->artwork = evbuffer_new());
|
||||
CHECK_NULL(L_RAOP, rmd->metadata = evbuffer_new());
|
||||
CHECK_NULL(L_RAOP, tmp = evbuffer_new());
|
||||
|
||||
ret = artwork_get_item(rmd->artwork, queue_item->file_id, ART_DEFAULT_WIDTH, ART_DEFAULT_HEIGHT);
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_INFO, L_RAOP, "Failed to retrieve artwork for file id %d; no artwork will be sent\n", id);
|
||||
|
||||
DPRINTF(E_INFO, L_RAOP, "Failed to retrieve artwork for file '%s'; no artwork will be sent\n", queue_item->path);
|
||||
evbuffer_free(rmd->artwork);
|
||||
rmd->artwork = NULL;
|
||||
}
|
||||
|
||||
rmd->artwork_fmt = ret;
|
||||
|
||||
skip_artwork:
|
||||
|
||||
/* Turn it into DAAP metadata */
|
||||
tmp = evbuffer_new();
|
||||
if (!tmp)
|
||||
{
|
||||
DPRINTF(E_LOG, L_RAOP, "Out of memory for temporary metadata evbuffer; metadata will not be sent\n");
|
||||
|
||||
goto out_qi;
|
||||
}
|
||||
|
||||
rmd->metadata = evbuffer_new();
|
||||
if (!rmd->metadata)
|
||||
{
|
||||
DPRINTF(E_LOG, L_RAOP, "Out of memory for metadata evbuffer; metadata will not be sent\n");
|
||||
|
||||
evbuffer_free(tmp);
|
||||
goto out_qi;
|
||||
}
|
||||
|
||||
ret = dmap_encode_queue_metadata(rmd->metadata, tmp, queue_item);
|
||||
evbuffer_free(tmp);
|
||||
free_queue_item(queue_item, 0);
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_RAOP, "Could not encode file metadata; metadata will not be sent\n");
|
||||
|
||||
goto out_metadata;
|
||||
raop_metadata_free(rmd);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Progress - raop_metadata_send() will add rtptime to these */
|
||||
rmd->start = 0;
|
||||
rmd->end = ((uint64_t)queue_item->song_length * 44100UL) / 1000UL;
|
||||
|
||||
free_queue_item(queue_item, 0);
|
||||
|
||||
return rmd;
|
||||
|
||||
out_metadata:
|
||||
evbuffer_free(rmd->metadata);
|
||||
out_qi:
|
||||
free_queue_item(queue_item, 0);
|
||||
out_rmd:
|
||||
free(rmd);
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void
|
||||
@ -2329,37 +2252,62 @@ raop_cb_metadata(struct evrtsp_request *req, void *arg)
|
||||
session_failure(rs);
|
||||
}
|
||||
|
||||
static int
|
||||
raop_metadata_send_progress(struct raop_session *rs, struct evbuffer *evbuf, struct raop_metadata *rmd, uint32_t offset, uint32_t delay)
|
||||
static void
|
||||
raop_metadata_rtptimes_get(uint32_t *start, uint32_t *display, uint32_t *pos, uint32_t *end, struct raop_master_session *rms, struct output_metadata *metadata)
|
||||
{
|
||||
uint32_t display;
|
||||
int ret;
|
||||
struct rtp_session *rtp_session = rms->rtp_session;
|
||||
uint64_t sample_rate;
|
||||
uint32_t elapsed_ms;
|
||||
int delay;
|
||||
|
||||
/* Here's the deal with progress values:
|
||||
* - first value, called display, is always start minus a delay
|
||||
* -> delay x1 if streaming is starting for this device (joining or not)
|
||||
* -> delay x2 if stream is switching to a new song
|
||||
* - second value, called start, is the RTP time of the first sample for this
|
||||
* - second value, called pos, is the RTP time of the first sample for this
|
||||
* song for this device
|
||||
* -> start of song
|
||||
* -> start of song + offset if device is joining in the middle of a song,
|
||||
* or getting out of a pause or seeking
|
||||
* - third value, called end, is the RTP time of the last sample for this song
|
||||
*/
|
||||
sample_rate = rtp_session->quality.sample_rate;
|
||||
|
||||
display = RAOP_RTPTIME(rmd->start - delay);
|
||||
/* First calculate the rtptime that streaming of this item started:
|
||||
* - at time metadata->pts the elapsed time was metadata->pos_ms
|
||||
* - the time is now rtp_session->pts and the position is rtp_session->pos
|
||||
* -> time since item started is elapsed = metadata->pos_ms + (rtp_session->pts - metadata->pts)
|
||||
* -> start must then be start = rtp_session->pos - elapsed * sample_rate;
|
||||
*/
|
||||
elapsed_ms = metadata->pos_ms;
|
||||
|
||||
ret = evbuffer_add_printf(evbuf, "progress: %u/%u/%u\r\n", display, RAOP_RTPTIME(rmd->start + offset), RAOP_RTPTIME(rmd->end));
|
||||
*start = rtp_session->pos - sample_rate * elapsed_ms / 1000;
|
||||
|
||||
if (metadata->startup)
|
||||
delay = RAOP_MD_DELAY_STARTUP;
|
||||
else
|
||||
delay = RAOP_MD_DELAY_SWITCH;
|
||||
|
||||
*display = *start - delay;
|
||||
*pos = MAX(rtp_session->pos, *start); // TODO is this calculation correct? It is not in line with the description above
|
||||
*end = *start + sample_rate * metadata->len_ms / 1000;
|
||||
|
||||
DPRINTF(E_DBG, L_RAOP, "Metadata sr=%lu, pos_ms=%u, len_ms=%u, start=%u, display=%u, pos=%u, end=%u, rtptime=%u\n",
|
||||
sample_rate, metadata->pos_ms, metadata->len_ms, *start, *display, *pos, *end, rtp_session->pos);
|
||||
}
|
||||
|
||||
static int
|
||||
raop_metadata_send_progress(struct raop_session *rs, struct evbuffer *evbuf, struct raop_metadata *rmd, uint32_t display, uint32_t pos, uint32_t end)
|
||||
{
|
||||
int ret;
|
||||
|
||||
ret = evbuffer_add_printf(evbuf, "progress: %u/%u/%u\r\n", display, pos, end);
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_RAOP, "Could not build progress string for sending\n");
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
DPRINTF(E_DBG, L_PLAYER, "Metadata send is start_time=%" PRIu32 ", start=%" PRIu32 ", display=%" PRIu32 ", current=%" PRIu32 ", end=%" PRIu32 "\n",
|
||||
rs->start_rtptime, rmd->start, rmd->start - delay, rmd->start + offset, rmd->end);
|
||||
|
||||
ret = raop_send_req_set_parameter(rs, evbuf, "text/parameters", NULL, raop_cb_metadata, "send_progress");
|
||||
if (ret < 0)
|
||||
DPRINTF(E_LOG, L_RAOP, "Could not send SET_PARAMETER progress request to '%s'\n", rs->devname);
|
||||
@ -2435,25 +2383,25 @@ raop_metadata_send_metadata(struct raop_session *rs, struct evbuffer *evbuf, str
|
||||
}
|
||||
|
||||
static int
|
||||
raop_metadata_send_internal(struct raop_session *rs, struct raop_metadata *rmd, uint32_t offset, uint32_t delay)
|
||||
raop_metadata_send_generic(struct raop_session *rs, struct output_metadata *metadata)
|
||||
{
|
||||
char rtptime[32];
|
||||
struct raop_metadata *rmd = metadata->priv;
|
||||
struct evbuffer *evbuf;
|
||||
uint32_t start;
|
||||
uint32_t display;
|
||||
uint32_t pos;
|
||||
uint32_t end;
|
||||
char rtptime[32];
|
||||
int ret;
|
||||
|
||||
evbuf = evbuffer_new();
|
||||
if (!evbuf)
|
||||
{
|
||||
DPRINTF(E_LOG, L_RAOP, "Could not allocate temp evbuffer for metadata processing\n");
|
||||
raop_metadata_rtptimes_get(&start, &display, &pos, &end, rs->master_session, metadata);
|
||||
|
||||
return -1;
|
||||
}
|
||||
CHECK_NULL(L_RAOP, evbuf = evbuffer_new());
|
||||
|
||||
ret = snprintf(rtptime, sizeof(rtptime), "rtptime=%u", RAOP_RTPTIME(rmd->start));
|
||||
ret = snprintf(rtptime, sizeof(rtptime), "rtptime=%u", start);
|
||||
if ((ret < 0) || (ret >= sizeof(rtptime)))
|
||||
{
|
||||
DPRINTF(E_LOG, L_RAOP, "RTP-Info too big for buffer while sending metadata\n");
|
||||
|
||||
ret = -1;
|
||||
goto out;
|
||||
}
|
||||
@ -2462,29 +2410,25 @@ raop_metadata_send_internal(struct raop_session *rs, struct raop_metadata *rmd,
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_RAOP, "Could not send metadata to '%s'\n", rs->devname);
|
||||
|
||||
ret = -1;
|
||||
goto out;
|
||||
}
|
||||
|
||||
if (!rmd->artwork)
|
||||
goto skip_artwork;
|
||||
|
||||
if (rmd->artwork)
|
||||
{
|
||||
ret = raop_metadata_send_artwork(rs, evbuf, rmd, rtptime);
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_RAOP, "Could not send artwork to '%s'\n", rs->devname);
|
||||
|
||||
ret = -1;
|
||||
goto out;
|
||||
}
|
||||
}
|
||||
|
||||
skip_artwork:
|
||||
ret = raop_metadata_send_progress(rs, evbuf, rmd, offset, delay);
|
||||
ret = raop_metadata_send_progress(rs, evbuf, rmd, display, pos, end);
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_RAOP, "Could not send progress to '%s'\n", rs->devname);
|
||||
|
||||
ret = -1;
|
||||
goto out;
|
||||
}
|
||||
@ -2495,90 +2439,44 @@ raop_metadata_send_internal(struct raop_session *rs, struct raop_metadata *rmd,
|
||||
return ret;
|
||||
}
|
||||
|
||||
static void
|
||||
static int
|
||||
raop_metadata_startup_send(struct raop_session *rs)
|
||||
{
|
||||
struct raop_metadata *rmd;
|
||||
uint32_t offset;
|
||||
int sent;
|
||||
int ret;
|
||||
if (!rs->wants_metadata || !raop_cur_metadata)
|
||||
return 0;
|
||||
|
||||
if (!rs->wants_metadata)
|
||||
return;
|
||||
// We don't need to preserve the previous value, this function is the only one
|
||||
// using raop_cur_metadata
|
||||
raop_cur_metadata->startup = true;
|
||||
|
||||
sent = 0;
|
||||
for (rmd = metadata_head; rmd; rmd = rmd->next)
|
||||
{
|
||||
// Current song, rmd->start >= rmd->end if endless stream
|
||||
if ((rs->start_rtptime >= rmd->start) && ( (rs->start_rtptime < rmd->end) || (rmd->start >= rmd->end) ))
|
||||
{
|
||||
offset = rs->start_rtptime - rmd->start;
|
||||
|
||||
ret = raop_metadata_send_internal(rs, rmd, offset, RAOP_MD_DELAY_STARTUP);
|
||||
if (ret < 0)
|
||||
{
|
||||
session_failure(rs);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
sent = 1;
|
||||
}
|
||||
// Next song(s)
|
||||
else if (sent && (rs->start_rtptime < rmd->start))
|
||||
{
|
||||
ret = raop_metadata_send_internal(rs, rmd, 0, RAOP_MD_DELAY_SWITCH);
|
||||
if (ret < 0)
|
||||
{
|
||||
session_failure(rs);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
return raop_metadata_send_generic(rs, raop_cur_metadata);
|
||||
}
|
||||
|
||||
static void
|
||||
raop_metadata_send(void *metadata, uint64_t rtptime, uint64_t offset, int startup)
|
||||
raop_metadata_send(struct output_metadata *metadata)
|
||||
{
|
||||
struct raop_metadata *rmd;
|
||||
struct raop_session *rs;
|
||||
struct raop_session *next;
|
||||
uint32_t delay;
|
||||
int ret;
|
||||
|
||||
rmd = metadata;
|
||||
rmd->start += rtptime;
|
||||
rmd->end += rtptime;
|
||||
|
||||
/* Add the rmd to the metadata list */
|
||||
if (metadata_tail)
|
||||
metadata_tail->next = rmd;
|
||||
else
|
||||
{
|
||||
metadata_head = rmd;
|
||||
metadata_tail = rmd;
|
||||
}
|
||||
|
||||
for (rs = raop_sessions; rs; rs = next)
|
||||
{
|
||||
next = rs->next;
|
||||
|
||||
if (!(rs->state & RAOP_STATE_F_CONNECTED))
|
||||
if (!(rs->state & RAOP_STATE_F_CONNECTED) || !rs->wants_metadata)
|
||||
continue;
|
||||
|
||||
if (!rs->wants_metadata)
|
||||
continue;
|
||||
|
||||
delay = (startup) ? RAOP_MD_DELAY_STARTUP : RAOP_MD_DELAY_SWITCH;
|
||||
|
||||
ret = raop_metadata_send_internal(rs, rmd, offset, delay);
|
||||
ret = raop_metadata_send_generic(rs, metadata);
|
||||
if (ret < 0)
|
||||
{
|
||||
session_failure(rs);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Replace current metadata with the new stuff
|
||||
raop_metadata_purge();
|
||||
raop_cur_metadata = metadata;
|
||||
}
|
||||
|
||||
|
||||
@ -3718,15 +3616,13 @@ raop_cb_startup_volume(struct evrtsp_request *req, void *arg)
|
||||
if (ret < 0)
|
||||
goto cleanup;
|
||||
|
||||
raop_metadata_startup_send(rs);
|
||||
ret = raop_metadata_startup_send(rs);
|
||||
if (ret < 0)
|
||||
goto cleanup;
|
||||
|
||||
ret = raop_v2_stream_open(rs);
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_RAOP, "Could not open streaming socket\n");
|
||||
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
/* Session startup and setup is done, tell our user */
|
||||
raop_status(rs);
|
||||
@ -5068,7 +4964,6 @@ struct output_definition output_raop =
|
||||
.metadata_prepare = raop_metadata_prepare,
|
||||
.metadata_send = raop_metadata_send,
|
||||
.metadata_purge = raop_metadata_purge,
|
||||
.metadata_prune = raop_metadata_prune,
|
||||
#ifdef RAOP_VERIFICATION
|
||||
.authorize = raop_verification_setup,
|
||||
#endif
|
||||
|
225
src/player.c
225
src/player.c
@ -33,22 +33,6 @@
|
||||
* not always obeyed, for instance some outputs do their setup in ways that
|
||||
* could block.
|
||||
*
|
||||
*
|
||||
* About metadata
|
||||
* --------------
|
||||
* The player gets metadata from library + inputs and passes it to the outputs
|
||||
* and other clients (e.g. Remotes).
|
||||
*
|
||||
* 1. On playback start, metadata from the library is loaded into the queue
|
||||
* items, and these items are then the source of metadata for clients.
|
||||
* 2. During playback, the input may signal new metadata by making a
|
||||
* input_write() with the INPUT_FLAG_METADATA flag. When the player read
|
||||
* reaches that data, the player will request the metadata from the input
|
||||
* with input_metadata_get(). This metadata is then saved to the currently
|
||||
* playing queue item, and the clients are told to update metadata.
|
||||
* 3. Artwork works differently than textual metadata. The artwork module will
|
||||
* look for artwork in the library, and addition also check the artwork_url
|
||||
* of the queue_item.
|
||||
*/
|
||||
|
||||
#ifdef HAVE_CONFIG_H
|
||||
@ -162,12 +146,6 @@ struct speaker_get_param
|
||||
struct player_speaker_info *spk_info;
|
||||
};
|
||||
|
||||
struct metadata_param
|
||||
{
|
||||
struct input_metadata *input;
|
||||
struct output_metadata *output;
|
||||
};
|
||||
|
||||
struct speaker_auth_param
|
||||
{
|
||||
enum output_types type;
|
||||
@ -184,16 +162,16 @@ union player_arg
|
||||
|
||||
struct player_source
|
||||
{
|
||||
/* Id of the file/item in the files database */
|
||||
// Id of the file/item in the files database
|
||||
uint32_t id;
|
||||
|
||||
/* Item-Id of the file/item in the queue */
|
||||
// Item-Id of the file/item in the queue
|
||||
uint32_t item_id;
|
||||
|
||||
/* Length of the file/item in milliseconds */
|
||||
// Length of the file/item in milliseconds
|
||||
uint32_t len_ms;
|
||||
|
||||
/* Quality of the source (sample rate etc.) */
|
||||
// Quality of the source (sample rate etc.)
|
||||
struct media_quality quality;
|
||||
|
||||
enum data_kind data_kind;
|
||||
@ -292,7 +270,7 @@ static struct timespec player_tick_interval;
|
||||
// Timer resolution
|
||||
static struct timespec player_timer_res;
|
||||
|
||||
static struct timeval player_pause_timeout = { PLAYER_PAUSE_TIMEOUT, 0 };
|
||||
//static struct timeval player_pause_timeout = { PLAYER_PAUSE_TIMEOUT, 0 };
|
||||
|
||||
// PLAYER_WRITE_BEHIND_MAX converted to clock ticks
|
||||
static int pb_write_deficit_max;
|
||||
@ -448,87 +426,24 @@ pause_timer_cb(int fd, short what, void *arg)
|
||||
pb_abort();
|
||||
}
|
||||
|
||||
// Callback from the worker thread. Here the heavy lifting is done: updating the
|
||||
// db_queue_item, retrieving artwork (through outputs_metadata_prepare) and
|
||||
// when done, telling the player to send the metadata to the clients
|
||||
static void
|
||||
metadata_update_cb(void *arg)
|
||||
static int
|
||||
metadata_finalize_cb(struct output_metadata *metadata)
|
||||
{
|
||||
struct input_metadata *metadata = arg;
|
||||
struct output_metadata *o_metadata;
|
||||
struct db_queue_item *queue_item;
|
||||
int ret;
|
||||
|
||||
ret = input_metadata_get(metadata);
|
||||
if (ret < 0)
|
||||
if (metadata->item_id != pb_session.playing_now->item_id)
|
||||
{
|
||||
goto out_free_metadata;
|
||||
DPRINTF(E_WARN, L_PLAYER, "Aborting metadata_send(), item_id changed during metadata preparation (%" PRIu32 " -> %" PRIu32 ")\n",
|
||||
metadata->item_id, pb_session.playing_now->item_id);
|
||||
return -1;
|
||||
}
|
||||
|
||||
queue_item = db_queue_fetch_byitemid(metadata->item_id);
|
||||
if (!queue_item)
|
||||
{
|
||||
DPRINTF(E_LOG, L_PLAYER, "Bug! Input metadata item_id does not match anything in queue\n");
|
||||
goto out_free_metadata;
|
||||
}
|
||||
if (!metadata->pos_ms)
|
||||
metadata->pos_ms = pb_session.playing_now->pos_ms;
|
||||
if (!metadata->len_ms)
|
||||
metadata->len_ms = pb_session.playing_now->len_ms;
|
||||
if (!metadata->pts.tv_sec)
|
||||
metadata->pts = pb_session.pts;
|
||||
|
||||
// Update queue item if metadata changed
|
||||
if (metadata->artist || metadata->title || metadata->album || metadata->genre || metadata->artwork_url || metadata->song_length)
|
||||
{
|
||||
// Since we won't be using the metadata struct values for anything else than
|
||||
// this we just swap pointers
|
||||
if (metadata->artist)
|
||||
swap_pointers(&queue_item->artist, &metadata->artist);
|
||||
if (metadata->title)
|
||||
swap_pointers(&queue_item->title, &metadata->title);
|
||||
if (metadata->album)
|
||||
swap_pointers(&queue_item->album, &metadata->album);
|
||||
if (metadata->genre)
|
||||
swap_pointers(&queue_item->genre, &metadata->genre);
|
||||
if (metadata->artwork_url)
|
||||
swap_pointers(&queue_item->artwork_url, &metadata->artwork_url);
|
||||
if (metadata->song_length)
|
||||
queue_item->song_length = metadata->song_length;
|
||||
|
||||
ret = db_queue_update_item(queue_item);
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_PLAYER, "Database error while updating queue with new metadata\n");
|
||||
goto out_free_queueitem;
|
||||
}
|
||||
}
|
||||
|
||||
o_metadata = outputs_metadata_prepare(metadata->item_id);
|
||||
|
||||
// Actual sending must be done by player, since the worker does not own the outputs
|
||||
player_metadata_send(metadata, o_metadata);
|
||||
|
||||
outputs_metadata_free(o_metadata);
|
||||
|
||||
out_free_queueitem:
|
||||
free_queue_item(queue_item, 0);
|
||||
|
||||
out_free_metadata:
|
||||
input_metadata_free(metadata, 1);
|
||||
}
|
||||
|
||||
// Gets the metadata, but since the actual update requires db writes and
|
||||
// possibly retrieving artwork we let the worker do the next step
|
||||
static void
|
||||
metadata_trigger(int startup)
|
||||
{
|
||||
struct input_metadata metadata;
|
||||
|
||||
memset(&metadata, 0, sizeof(struct input_metadata));
|
||||
|
||||
metadata.item_id = pb_session.playing_now->item_id;
|
||||
|
||||
metadata.startup = startup;
|
||||
metadata.start = pb_session.playing_now->read_start;
|
||||
metadata.offset = pb_session.playing_now->play_start - pb_session.playing_now->read_start;
|
||||
metadata.rtptime = pb_session.pos;
|
||||
|
||||
worker_execute(metadata_update_cb, &metadata, sizeof(metadata), 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
@ -877,9 +792,10 @@ session_update_read_quality(struct media_quality *quality)
|
||||
int samples_per_read;
|
||||
|
||||
if (quality_is_equal(quality, &pb_session.reading_now->quality))
|
||||
return;
|
||||
goto out;
|
||||
|
||||
pb_session.reading_now->quality = *quality;
|
||||
|
||||
samples_per_read = ((uint64_t)quality->sample_rate * (player_tick_interval.tv_nsec / 1000000)) / 1000;
|
||||
pb_session.reading_now->output_buffer_samples = OUTPUTS_BUFFER_DURATION * quality->sample_rate;
|
||||
|
||||
@ -897,6 +813,9 @@ session_update_read_quality(struct media_quality *quality)
|
||||
CHECK_NULL(L_PLAYER, pb_session.buffer);
|
||||
|
||||
pb_session.reading_now->play_start = pb_session.reading_now->read_start + pb_session.reading_now->output_buffer_samples;
|
||||
|
||||
out:
|
||||
free(quality);
|
||||
}
|
||||
|
||||
static void
|
||||
@ -926,15 +845,11 @@ session_start(struct player_source *ps, uint32_t seek_ms)
|
||||
/* ------------------------- Playback event handlers ------------------------ */
|
||||
|
||||
static void
|
||||
event_read_quality()
|
||||
event_read_quality(struct media_quality *quality)
|
||||
{
|
||||
DPRINTF(E_DBG, L_PLAYER, "event_read_quality()\n");
|
||||
|
||||
struct media_quality quality;
|
||||
|
||||
input_quality_get(&quality);
|
||||
|
||||
session_update_read_quality(&quality);
|
||||
session_update_read_quality(quality);
|
||||
}
|
||||
|
||||
// Stuff to do when read of current track ends
|
||||
@ -969,11 +884,13 @@ event_read_start_next()
|
||||
}
|
||||
|
||||
static void
|
||||
event_metadata_new()
|
||||
event_read_metadata(struct input_metadata *metadata)
|
||||
{
|
||||
DPRINTF(E_DBG, L_PLAYER, "event_metadata_new()\n");
|
||||
DPRINTF(E_DBG, L_PLAYER, "event_read_metadata()\n");
|
||||
|
||||
metadata_trigger(0);
|
||||
outputs_metadata_send(pb_session.playing_now->item_id, false, metadata_finalize_cb);
|
||||
|
||||
status_update(player_state);
|
||||
}
|
||||
|
||||
static void
|
||||
@ -1004,7 +921,10 @@ event_play_eof()
|
||||
if (consume)
|
||||
db_queue_delete_byitemid(pb_session.playing_now->item_id);
|
||||
|
||||
outputs_metadata_prune(pb_session.pos);
|
||||
// outputs_metadata_prune(pb_session.pos);
|
||||
|
||||
if (pb_session.reading_next)
|
||||
outputs_metadata_send(pb_session.reading_next->item_id, false, metadata_finalize_cb);
|
||||
|
||||
session_update_play_eof();
|
||||
}
|
||||
@ -1014,8 +934,6 @@ event_play_start()
|
||||
{
|
||||
DPRINTF(E_DBG, L_PLAYER, "event_play_start()\n");
|
||||
|
||||
event_metadata_new();
|
||||
|
||||
status_update(PLAY_PLAYING);
|
||||
|
||||
session_update_play_start();
|
||||
@ -1055,7 +973,8 @@ event_read(int nsamples)
|
||||
static inline int
|
||||
source_read(int *nbytes, int *nsamples, struct media_quality *quality, uint8_t *buf, int len)
|
||||
{
|
||||
short flags;
|
||||
short flag;
|
||||
void *flagdata;
|
||||
|
||||
// Nothing to read
|
||||
if (!pb_session.reading_now)
|
||||
@ -1080,28 +999,28 @@ source_read(int *nbytes, int *nsamples, struct media_quality *quality, uint8_t *
|
||||
|
||||
*quality = pb_session.reading_now->quality;
|
||||
*nsamples = 0;
|
||||
*nbytes = input_read(buf, len, &flags);
|
||||
if ((*nbytes < 0) || (flags & INPUT_FLAG_ERROR))
|
||||
*nbytes = input_read(buf, len, &flag, &flagdata);
|
||||
if ((*nbytes < 0) || (flag == INPUT_FLAG_ERROR))
|
||||
{
|
||||
DPRINTF(E_LOG, L_PLAYER, "Error reading source '%s' (id=%d)\n", pb_session.reading_now->path, pb_session.reading_now->id);
|
||||
event_read_error();
|
||||
return -1;
|
||||
}
|
||||
else if (flags & INPUT_FLAG_START_NEXT)
|
||||
else if (flag == INPUT_FLAG_START_NEXT)
|
||||
{
|
||||
event_read_start_next();
|
||||
}
|
||||
else if (flags & INPUT_FLAG_EOF)
|
||||
else if (flag == INPUT_FLAG_EOF)
|
||||
{
|
||||
event_read_eof();
|
||||
}
|
||||
else if (flags & INPUT_FLAG_METADATA)
|
||||
else if (flag == INPUT_FLAG_METADATA)
|
||||
{
|
||||
event_metadata_new();
|
||||
event_read_metadata((struct input_metadata *)flagdata);
|
||||
}
|
||||
else if (flags & INPUT_FLAG_QUALITY || quality->channels == 0)
|
||||
else if (flag == INPUT_FLAG_QUALITY)
|
||||
{
|
||||
event_read_quality();
|
||||
event_read_quality((struct media_quality *)flagdata);
|
||||
}
|
||||
|
||||
if (*nbytes == 0 || quality->channels == 0)
|
||||
@ -1321,25 +1240,6 @@ device_auth_kickoff(void *arg, int *retval)
|
||||
}
|
||||
|
||||
|
||||
static enum command_state
|
||||
device_metadata_send(void *arg, int *retval)
|
||||
{
|
||||
struct metadata_param *metadata_param = arg;
|
||||
struct input_metadata *imd;
|
||||
struct output_metadata *omd;
|
||||
|
||||
imd = metadata_param->input;
|
||||
omd = metadata_param->output;
|
||||
|
||||
outputs_metadata_send(omd, imd->rtptime, imd->offset, imd->startup);
|
||||
|
||||
status_update(player_state);
|
||||
|
||||
*retval = 0;
|
||||
return COMMAND_END;
|
||||
}
|
||||
|
||||
|
||||
/* -------- Output device callbacks executed in the player thread ----------- */
|
||||
|
||||
static void
|
||||
@ -1609,7 +1509,7 @@ pb_timer_start(void)
|
||||
|
||||
// The stop timers will be active if we have recently paused, but now that the
|
||||
// playback loop has been kicked off, we deactivate them
|
||||
event_del(player_pause_timeout_ev);
|
||||
// event_del(player_pause_timeout_ev);
|
||||
outputs_stop_delayed_cancel();
|
||||
|
||||
ret = event_add(pb_timer_ev, NULL);
|
||||
@ -1689,21 +1589,30 @@ pb_session_start(struct db_queue_item *queue_item, uint32_t seek_ms)
|
||||
|
||||
// The input source is now open and ready, but we might actually be paused. So
|
||||
// we activate the below event in case the user never starts us again
|
||||
event_add(player_pause_timeout_ev, &player_pause_timeout);
|
||||
// event_add(player_pause_timeout_ev, &player_pause_timeout);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
// Stops input source and stops read loop
|
||||
static void
|
||||
pb_session_pause(void)
|
||||
{
|
||||
pb_timer_stop();
|
||||
|
||||
source_stop();
|
||||
}
|
||||
|
||||
// Stops input source and deallocates pb_session content
|
||||
static void
|
||||
pb_session_stop(void)
|
||||
{
|
||||
pb_timer_stop();
|
||||
|
||||
source_stop();
|
||||
|
||||
session_stop();
|
||||
|
||||
pb_timer_stop();
|
||||
|
||||
status_update(PLAY_STOPPED);
|
||||
}
|
||||
|
||||
@ -1867,6 +1776,8 @@ playback_start_bh(void *arg, int *retval)
|
||||
if (ret < 0)
|
||||
goto error;
|
||||
|
||||
outputs_metadata_send(pb_session.playing_now->item_id, true, metadata_finalize_cb);
|
||||
|
||||
status_update(PLAY_PLAYING);
|
||||
|
||||
*retval = 0;
|
||||
@ -1943,8 +1854,6 @@ playback_start_item(void *arg, int *retval)
|
||||
}
|
||||
}
|
||||
|
||||
metadata_trigger(1);
|
||||
|
||||
// Start sessions on selected devices
|
||||
*retval = 0;
|
||||
|
||||
@ -2246,7 +2155,7 @@ playback_pause(void *arg, int *retval)
|
||||
return COMMAND_END;
|
||||
}
|
||||
|
||||
pb_timer_stop();
|
||||
pb_session_pause();
|
||||
|
||||
*retval = outputs_flush(device_flush_cb);
|
||||
outputs_metadata_purge();
|
||||
@ -3184,20 +3093,6 @@ player_raop_verification_kickoff(char **arglist)
|
||||
}
|
||||
|
||||
|
||||
/* ---------------------------- Thread: worker ------------------------------ */
|
||||
|
||||
void
|
||||
player_metadata_send(void *imd, void *omd)
|
||||
{
|
||||
struct metadata_param metadata_param;
|
||||
|
||||
metadata_param.input = imd;
|
||||
metadata_param.output = omd;
|
||||
|
||||
commands_exec_sync(cmdbase, device_metadata_send, NULL, &metadata_param);
|
||||
}
|
||||
|
||||
|
||||
/* ---------------------------- Thread: player ------------------------------ */
|
||||
|
||||
static void *
|
||||
|
@ -138,7 +138,6 @@ player_shuffle_set(int enable);
|
||||
int
|
||||
player_consume_set(int enable);
|
||||
|
||||
|
||||
void
|
||||
player_queue_clear_history(void);
|
||||
|
||||
@ -157,9 +156,6 @@ player_device_remove(void *device);
|
||||
void
|
||||
player_raop_verification_kickoff(char **arglist);
|
||||
|
||||
void
|
||||
player_metadata_send(void *imd, void *omd);
|
||||
|
||||
const char *
|
||||
player_pmap(void *p);
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user