[raop] Add support for Apple TV device verification, required by tvOS 10.2 (fix for issue #377)

- also change how speakers are saved/retrieved from the db
- add generic authorization methods in outputs.c and player.c
- let filescanner read *.verification files (containing PIN)
- configure options to enable and disable, since libsodium is required
This commit is contained in:
ejurgensen 2017-06-19 21:52:01 +02:00
parent 736979a9a2
commit f63d103753
15 changed files with 2090 additions and 288 deletions

View File

@ -273,6 +273,14 @@ dnl Build with libcurl
FORK_ARG_WITH_CHECK([FORKED_OPTS], [libcurl support], [libcurl], [LIBCURL], FORK_ARG_WITH_CHECK([FORKED_OPTS], [libcurl support], [libcurl], [LIBCURL],
[libcurl], [curl_global_init], [curl/curl.h]) [libcurl], [curl_global_init], [curl/curl.h])
dnl Build with libsodium
FORK_ARG_WITH_CHECK([FORKED_OPTS], [libsodium support], [libsodium], [LIBSODIUM],
[libsodium], [sodium_init], [sodium.h])
dnl Build with libplist
FORK_ARG_WITH_CHECK([FORKED_OPTS], [libplist support], [libplist], [LIBPLIST],
[libplist >= 0.16], [plist_dict_get_item], [plist/plist.h])
dnl Build with libevent_pthreads dnl Build with libevent_pthreads
FORK_ARG_WITH_CHECK([FORKED_OPTS], [libevent_pthreads support], FORK_ARG_WITH_CHECK([FORKED_OPTS], [libevent_pthreads support],
[libevent_pthreads], [LIBEVENT_PTHREADS], [libevent_pthreads], [libevent_pthreads], [LIBEVENT_PTHREADS], [libevent_pthreads],
@ -300,12 +308,6 @@ AS_IF([[test "x$with_avahi" = "xno"]],
[AC_MSG_ERROR([[Avahi client or Bonjour DNS_SD required, please install one.]])])]) [AC_MSG_ERROR([[Avahi client or Bonjour DNS_SD required, please install one.]])])])
AM_CONDITIONAL([COND_AVAHI], [[test "x$with_avahi" = "xyes"]]) AM_CONDITIONAL([COND_AVAHI], [[test "x$with_avahi" = "xyes"]])
dnl iTunes playlists with libplist
FORK_ARG_ENABLE([iTunes Music Library XML support], [itunes], [ITUNES],
[FORK_MODULES_CHECK([FORKED_OPTS], [LIBPLIST], [libplist >= 0.16],
[plist_dict_get_item], [plist/plist.h])])
AM_CONDITIONAL([COND_ITUNES], [[test "x$enable_itunes" = "xyes"]])
dnl Spotify with dynamic linking to libspotify dnl Spotify with dynamic linking to libspotify
FORK_ARG_ENABLE([Spotify support], [spotify], [SPOTIFY], FORK_ARG_ENABLE([Spotify support], [spotify], [SPOTIFY],
[AS_IF([[test "x$with_json" = "xno"]], [AS_IF([[test "x$with_json" = "xno"]],
@ -352,10 +354,27 @@ FORK_ARG_ENABLE([Chromecast support], [chromecast], [CHROMECAST],
AM_CONDITIONAL([COND_CHROMECAST], [[test "x$enable_chromecast" = "xyes"]]) AM_CONDITIONAL([COND_CHROMECAST], [[test "x$enable_chromecast" = "xyes"]])
AM_CONDITIONAL([COND_PROTOBUF_OLD], [[test "x$protobuf_old" = "xyes"]]) AM_CONDITIONAL([COND_PROTOBUF_OLD], [[test "x$protobuf_old" = "xyes"]])
dnl iTunes playlists with libplist
FORK_ARG_DISABLE([iTunes Music Library XML support], [itunes], [ITUNES],
[AS_IF([[test "x$with_libplist" = "xno"]],
[AC_MSG_ERROR([[iTunes Music Library XML support requires libplist]])])
])
AM_CONDITIONAL([COND_ITUNES], [[test "x$enable_itunes" = "xyes"]])
dnl MPD support dnl MPD support
FORK_ARG_DISABLE([MPD client protocol support], [mpd], [MPD]) FORK_ARG_DISABLE([MPD client protocol support], [mpd], [MPD])
AM_CONDITIONAL([COND_MPD], [[test "x$enable_mpd" = "xyes"]]) AM_CONDITIONAL([COND_MPD], [[test "x$enable_mpd" = "xyes"]])
dnl Apple device verification
FORK_ARG_DISABLE([Apple TV device verification], [verification], [RAOP_VERIFICATION],
[
AS_IF([[test "x$with_libsodium" = "xno"]],
[AC_MSG_ERROR([[Apple TV device verification requires libsodium]])])
AS_IF([[test "x$with_libplist" = "xno"]],
[AC_MSG_ERROR([[Apple TV device verification requires libplist]])])
])
AM_CONDITIONAL([COND_RAOP_VERIFICATION], [[test "x$enable_verification" = "xyes"]])
dnl Defining users and groups dnl Defining users and groups
AC_ARG_WITH([daapd_user], AC_ARG_WITH([daapd_user],
[AS_HELP_STRING([--with-daapd-user=USER], [AS_HELP_STRING([--with-daapd-user=USER],

View File

@ -25,6 +25,10 @@ if COND_MPD
MPD_SRC=mpd.c mpd.h MPD_SRC=mpd.c mpd.h
endif endif
if COND_RAOP_VERIFICATION
RAOP_VERIFICATION_SRC=outputs/raop_verification.c outputs/raop_verification.h
endif
if COND_ALSA if COND_ALSA
ALSA_SRC=outputs/alsa.c ALSA_SRC=outputs/alsa.c
endif endif
@ -112,7 +116,8 @@ forked_daapd_SOURCES = main.c \
input.h input.c \ input.h input.c \
inputs/file_http.c inputs/pipe.c \ inputs/file_http.c inputs/pipe.c \
outputs.h outputs.c \ outputs.h outputs.c \
outputs/raop.c outputs/streaming.c outputs/dummy.c outputs/fifo.c \ outputs/raop.c $(RAOP_VERIFICATION_SRC) \
outputs/streaming.c outputs/dummy.c outputs/fifo.c \
$(ALSA_SRC) $(PULSEAUDIO_SRC) $(CHROMECAST_SRC) \ $(ALSA_SRC) $(PULSEAUDIO_SRC) $(CHROMECAST_SRC) \
evrtsp/rtsp.c evrtsp/evrtsp.h evrtsp/rtsp-internal.h evrtsp/log.h \ evrtsp/rtsp.c evrtsp/evrtsp.h evrtsp/rtsp-internal.h evrtsp/log.h \
$(SPOTIFY_SRC) \ $(SPOTIFY_SRC) \

View File

@ -3824,21 +3824,21 @@ db_admin_delete(const char *key)
/* Speakers */ /* Speakers */
int int
db_speaker_save(uint64_t id, int selected, int volume, const char *name) db_speaker_save(struct output_device *device)
{ {
#define Q_TMPL "INSERT OR REPLACE INTO speakers (id, selected, volume, name) VALUES (%" PRIi64 ", %d, %d, '%q');" #define Q_TMPL "INSERT OR REPLACE INTO speakers (id, selected, volume, name, auth_key) VALUES (%" PRIi64 ", %d, %d, %Q, %Q);"
char *query; char *query;
query = sqlite3_mprintf(Q_TMPL, id, selected, volume, name); query = sqlite3_mprintf(Q_TMPL, device->id, device->selected, device->volume, device->name, device->auth_key);
return db_query_run(query, 1, 0); return db_query_run(query, 1, 0);
#undef Q_TMPL #undef Q_TMPL
} }
int int
db_speaker_get(uint64_t id, int *selected, int *volume) db_speaker_get(struct output_device *device, uint64_t id)
{ {
#define Q_TMPL "SELECT s.selected, s.volume FROM speakers s WHERE s.id = %" PRIi64 ";" #define Q_TMPL "SELECT s.selected, s.volume, s.name, s.auth_key FROM speakers s WHERE s.id = %" PRIi64 ";"
sqlite3_stmt *stmt; sqlite3_stmt *stmt;
char *query; char *query;
int ret; int ret;
@ -3847,7 +3847,6 @@ db_speaker_get(uint64_t id, int *selected, int *volume)
if (!query) if (!query)
{ {
DPRINTF(E_LOG, L_DB, "Out of memory for query string\n"); DPRINTF(E_LOG, L_DB, "Out of memory for query string\n");
return -1; return -1;
} }
@ -3857,7 +3856,6 @@ db_speaker_get(uint64_t id, int *selected, int *volume)
if (ret != SQLITE_OK) if (ret != SQLITE_OK)
{ {
DPRINTF(E_LOG, L_DB, "Could not prepare statement: %s\n", sqlite3_errmsg(hdl)); DPRINTF(E_LOG, L_DB, "Could not prepare statement: %s\n", sqlite3_errmsg(hdl));
ret = -1; ret = -1;
goto out; goto out;
} }
@ -3867,15 +3865,20 @@ db_speaker_get(uint64_t id, int *selected, int *volume)
{ {
if (ret != SQLITE_DONE) if (ret != SQLITE_DONE)
DPRINTF(E_LOG, L_DB, "Could not step: %s\n", sqlite3_errmsg(hdl)); DPRINTF(E_LOG, L_DB, "Could not step: %s\n", sqlite3_errmsg(hdl));
sqlite3_finalize(stmt); sqlite3_finalize(stmt);
ret = -1; ret = -1;
goto out; goto out;
} }
*selected = sqlite3_column_int(stmt, 0); device->id = id;
*volume = sqlite3_column_int(stmt, 1); device->selected = sqlite3_column_int(stmt, 0);
device->volume = sqlite3_column_int(stmt, 1);
free(device->name);
device->name = safe_strdup((char *)sqlite3_column_text(stmt, 2));
free(device->auth_key);
device->auth_key = safe_strdup((char *)sqlite3_column_text(stmt, 3));
#ifdef DB_PROFILE #ifdef DB_PROFILE
while (db_blocking_step(stmt) == SQLITE_ROW) while (db_blocking_step(stmt) == SQLITE_ROW)
@ -3893,73 +3896,6 @@ db_speaker_get(uint64_t id, int *selected, int *volume)
#undef Q_TMPL #undef Q_TMPL
} }
int
db_speaker_auth_save(uint64_t id, const char *authkey)
{
#define Q_TMPL "UPDATE speakers SET authkey = '%q' WHERE id = %" PRIi64 ";"
char *query;
query = sqlite3_mprintf(Q_TMPL, authkey, id);
return db_query_run(query, 1, 0);
#undef Q_TMPL
}
char *
db_speaker_auth_get(uint64_t id)
{
#define Q_TMPL "SELECT authkey FROM speakers WHERE id = %" PRIi64 ";"
sqlite3_stmt *stmt;
char *query;
char *out;
int ret;
out = NULL;
query = sqlite3_mprintf(Q_TMPL, id);
if (!query)
{
DPRINTF(E_LOG, L_DB, "Out of memory for query string\n");
return NULL;
}
DPRINTF(E_DBG, L_DB, "Running query '%s'\n", query);
ret = db_blocking_prepare_v2(query, -1, &stmt, NULL);
if (ret != SQLITE_OK)
{
DPRINTF(E_LOG, L_DB, "Could not prepare statement: %s\n", sqlite3_errmsg(hdl));
goto out;
}
ret = db_blocking_step(stmt);
if (ret != SQLITE_ROW)
{
if (ret != SQLITE_DONE)
DPRINTF(E_LOG, L_DB, "Could not step: %s\n", sqlite3_errmsg(hdl));
sqlite3_finalize(stmt);
goto out;
}
out = (char *)sqlite3_column_text(stmt, 0);
if (out)
out = strdup(out);
#ifdef DB_PROFILE
while (db_blocking_step(stmt) == SQLITE_ROW)
; /* EMPTY */
#endif
sqlite3_finalize(stmt);
out:
sqlite3_free(query);
return out;
#undef Q_TMPL
}
void void
db_speaker_clear_all(void) db_speaker_clear_all(void)
{ {

View File

@ -8,6 +8,7 @@
#include <sqlite3.h> #include <sqlite3.h>
#include "outputs.h"
enum index_type { enum index_type {
I_NONE, I_NONE,
@ -675,18 +676,12 @@ db_admin_get(const char *key);
int int
db_admin_delete(const char *key); db_admin_delete(const char *key);
/* Speakers */ /* Speakers/outputs */
int int
db_speaker_save(uint64_t id, int selected, int volume, const char *name); db_speaker_save(struct output_device *device);
int int
db_speaker_get(uint64_t id, int *selected, int *volume); db_speaker_get(struct output_device *device, uint64_t id);
int
db_speaker_auth_save(uint64_t id, const char *authkey);
char *
db_speaker_auth_get(uint64_t id);
void void
db_speaker_clear_all(void); db_speaker_clear_all(void);

View File

@ -141,7 +141,7 @@
" selected INTEGER NOT NULL," \ " selected INTEGER NOT NULL," \
" volume INTEGER NOT NULL," \ " volume INTEGER NOT NULL," \
" name VARCHAR(255) DEFAULT NULL," \ " name VARCHAR(255) DEFAULT NULL," \
" authkey VARCHAR(2048) DEFAULT NULL" \ " auth_key VARCHAR(2048) DEFAULT NULL" \
");" ");"
#define T_INOTIFY \ #define T_INOTIFY \

View File

@ -1548,7 +1548,7 @@ static const struct db_upgrade_query db_upgrade_v1903_queries[] =
#define U_V1904_ALTER_SPEAKERS_ADD_AUTHKEY \ #define U_V1904_ALTER_SPEAKERS_ADD_AUTHKEY \
"ALTER TABLE speakers ADD COLUMN authkey VARCHAR(2048) DEFAULT NULL;" "ALTER TABLE speakers ADD COLUMN auth_key VARCHAR(2048) DEFAULT NULL;"
#define U_V1904_SCVER_MAJOR \ #define U_V1904_SCVER_MAJOR \
"UPDATE admin SET value = '19' WHERE key = 'schema_version_major';" "UPDATE admin SET value = '19' WHERE key = 'schema_version_major';"
@ -1557,7 +1557,7 @@ static const struct db_upgrade_query db_upgrade_v1903_queries[] =
static const struct db_upgrade_query db_upgrade_v1904_queries[] = static const struct db_upgrade_query db_upgrade_v1904_queries[] =
{ {
{ U_V1904_ALTER_SPEAKERS_ADD_AUTHKEY, "alter table speakers add column authkey" }, { U_V1904_ALTER_SPEAKERS_ADD_AUTHKEY, "alter table speakers add column auth_key" },
{ U_V1904_SCVER_MAJOR, "set schema_version_major to 19" }, { U_V1904_SCVER_MAJOR, "set schema_version_major to 19" },
{ U_V1904_SCVER_MINOR, "set schema_version_minor to 04" }, { U_V1904_SCVER_MINOR, "set schema_version_minor to 04" },

View File

@ -551,8 +551,8 @@ process_file(char *file, struct stat *sb, int type, int flags, int dir_id)
case FILE_CTRL_RAOP_VERIFICATION: case FILE_CTRL_RAOP_VERIFICATION:
if (flags & F_SCAN_BULK) if (flags & F_SCAN_BULK)
DPRINTF(E_LOG, L_SCAN, "Bulk scan will ignore '%s' (to process, add it after startup)\n", file); DPRINTF(E_LOG, L_SCAN, "Bulk scan will ignore '%s' (to process, add it after startup)\n", file);
// else else
// kickoff(player_raop_verification_kickoff, file, 1); kickoff(player_raop_verification_kickoff, file, 1);
break; break;
case FILE_CTRL_LASTFM: case FILE_CTRL_LASTFM:

View File

@ -606,6 +606,9 @@ main(int argc, char **argv)
#ifdef MPD #ifdef MPD
strcat(buildopts, " --enable-mpd"); strcat(buildopts, " --enable-mpd");
#endif #endif
#ifdef RAOP_VERIFICATION
strcat(buildopts, " --enable-verification");
#endif
#ifdef HAVE_ALSA #ifdef HAVE_ALSA
strcat(buildopts, " --with-alsa"); strcat(buildopts, " --with-alsa");
#endif #endif

View File

@ -100,23 +100,22 @@ outputs_device_probe(struct output_device *device, output_status_cb cb)
void void
outputs_device_free(struct output_device *device) outputs_device_free(struct output_device *device)
{ {
if (!device)
return;
if (outputs[device->type]->disabled) if (outputs[device->type]->disabled)
DPRINTF(E_LOG, L_PLAYER, "BUG! Freeing device from a disabled output?\n"); DPRINTF(E_LOG, L_PLAYER, "BUG! Freeing device from a disabled output?\n");
if (device->session)
DPRINTF(E_LOG, L_PLAYER, "BUG! Freeing device with active session?\n");
if (outputs[device->type]->device_free_extra) if (outputs[device->type]->device_free_extra)
outputs[device->type]->device_free_extra(device); outputs[device->type]->device_free_extra(device);
if (device->name) free(device->name);
free(device->name); free(device->auth_key);
free(device->v4_address);
if (device->v4_address) free(device->v6_address);
free(device->v4_address);
if (device->v6_address)
free(device->v6_address);
if (device->session)
DPRINTF(E_LOG, L_PLAYER, "BUG! Freeing device with active session?\n");
free(device); free(device);
} }
@ -313,6 +312,16 @@ outputs_metadata_free(struct output_metadata *omd)
} }
} }
void
outputs_authorize(enum output_types type, const char *pin)
{
if (outputs[type]->disabled)
return;
if (outputs[type]->authorize)
outputs[type]->authorize(pin);
}
int int
outputs_priority(struct output_device *device) outputs_priority(struct output_device *device)
{ {

View File

@ -102,9 +102,11 @@ struct output_device
unsigned advertised:1; unsigned advertised:1;
unsigned has_password:1; unsigned has_password:1;
unsigned has_video:1; unsigned has_video:1;
unsigned requires_auth:1;
// Password if relevant // Credentials if relevant
const char *password; const char *password;
char *auth_key;
// Device volume // Device volume
int volume; int volume;
@ -187,6 +189,9 @@ struct output_definition
// Flush all sessions, the return must be number of sessions pending the flush // Flush all sessions, the return must be number of sessions pending the flush
int (*flush)(output_status_cb cb, uint64_t rtptime); int (*flush)(output_status_cb cb, uint64_t rtptime);
// Authorize an output with a pin-code (probably coming from the filescanner)
void (*authorize)(const char *pin);
// Change the call back associated with a session // Change the call back associated with a session
void (*status_cb)(struct output_session *session, output_status_cb cb); void (*status_cb)(struct output_session *session, output_status_cb cb);
@ -242,6 +247,9 @@ outputs_metadata_prune(uint64_t rtptime);
void void
outputs_metadata_free(struct output_metadata *omd); outputs_metadata_free(struct output_metadata *omd);
void
outputs_authorize(enum output_types type, const char *pin);
int int
outputs_priority(struct output_device *device); outputs_priority(struct output_device *device);

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,66 @@
#ifndef __VERIFICATION_H__
#define __VERIFICATION_H__
#include <stdint.h>
struct verification_setup_context;
struct verification_verify_context;
/* When you have the pin-code (must be 4 bytes), create a new context with this
* function and then call verification_setup_request1()
*/
struct verification_setup_context *
verification_setup_new(const char *pin);
void
verification_setup_free(struct verification_setup_context *sctx);
/* Returns last error message
*/
const char *
verification_setup_errmsg(struct verification_setup_context *sctx);
uint8_t *
verification_setup_request1(uint32_t *len, struct verification_setup_context *sctx);
uint8_t *
verification_setup_request2(uint32_t *len, struct verification_setup_context *sctx);
uint8_t *
verification_setup_request3(uint32_t *len, struct verification_setup_context *sctx);
int
verification_setup_response1(struct verification_setup_context *sctx, const uint8_t *data, uint32_t data_len);
int
verification_setup_response2(struct verification_setup_context *sctx, const uint8_t *data, uint32_t data_len);
int
verification_setup_response3(struct verification_setup_context *sctx, const uint8_t *data, uint32_t data_len);
/* Returns a 0-terminated string that is the authorisation key. The caller
* should save it and use it later to initialize verification_verify_new().
* Note that the pointer becomes invalid when you free sctx.
*/
int
verification_setup_result(const char **authorisation_key, struct verification_setup_context *sctx);
/* When you have completed the setup you can extract a key with
* verification_setup_result(). Give the string as input to this function to
* create a verification context and then call verification_verify_request1()
*/
struct verification_verify_context *
verification_verify_new(const char *authorisation_key);
void
verification_verify_free(struct verification_verify_context *vctx);
/* Returns last error message
*/
const char *
verification_verify_errmsg(struct verification_verify_context *vctx);
uint8_t *
verification_verify_request1(uint32_t *len, struct verification_verify_context *vctx);
uint8_t *
verification_verify_request2(uint32_t *len, struct verification_verify_context *vctx);
int
verification_verify_response1(struct verification_verify_context *vctx, const uint8_t *data, uint32_t data_len);
#endif /* !__VERIFICATION_H__ */

View File

@ -138,6 +138,12 @@ struct metadata_param
struct output_metadata *output; struct output_metadata *output;
}; };
struct speaker_auth_param
{
enum output_types type;
char pin[5];
};
union player_arg union player_arg
{ {
struct volume_param vol_param; struct volume_param vol_param;
@ -150,6 +156,7 @@ union player_arg
uint32_t *id_ptr; uint32_t *id_ptr;
struct speaker_set_param speaker_set_param; struct speaker_set_param speaker_set_param;
enum repeat_mode mode; enum repeat_mode mode;
struct speaker_auth_param auth;
uint32_t id; uint32_t id;
int intval; int intval;
}; };
@ -1235,7 +1242,7 @@ device_remove(struct output_device *remove)
return; return;
/* Save device volume */ /* Save device volume */
ret = db_speaker_save(remove->id, remove->selected, remove->volume, remove->name); ret = db_speaker_save(remove);
if (ret < 0) if (ret < 0)
DPRINTF(E_LOG, L_PLAYER, "Could not save state for %s device '%s'\n", remove->type_name, remove->name); DPRINTF(E_LOG, L_PLAYER, "Could not save state for %s device '%s'\n", remove->type_name, remove->name);
@ -1273,7 +1280,7 @@ device_add(void *arg, int *retval)
union player_arg *cmdarg; union player_arg *cmdarg;
struct output_device *add; struct output_device *add;
struct output_device *device; struct output_device *device;
int selected; char *keep_name;
int ret; int ret;
cmdarg = arg; cmdarg = arg;
@ -1290,15 +1297,21 @@ device_add(void *arg, int *retval)
{ {
device = add; device = add;
ret = db_speaker_get(device->id, &selected, &device->volume); keep_name = strdup(device->name);
ret = db_speaker_get(device, device->id);
if (ret < 0) if (ret < 0)
{ {
selected = 0; device->selected = 0;
device->volume = (master_volume >= 0) ? master_volume : PLAYER_DEFAULT_VOLUME; device->volume = (master_volume >= 0) ? master_volume : PLAYER_DEFAULT_VOLUME;
} }
if (selected && (player_state != PLAY_PLAYING)) free(device->name);
device->name = keep_name;
if (device->selected && (player_state != PLAY_PLAYING))
speaker_select_output(device); speaker_select_output(device);
else
device->selected = 0;
device->next = dev_list; device->next = dev_list;
dev_list = device; dev_list = device;
@ -1403,6 +1416,18 @@ device_remove_family(void *arg, int *retval)
return COMMAND_END; return COMMAND_END;
} }
static enum command_state
device_auth_kickoff(void *arg, int *retval)
{
union player_arg *cmdarg = arg;
outputs_authorize(cmdarg->auth.type, cmdarg->auth.pin);
*retval = 0;
return COMMAND_END;
}
static enum command_state static enum command_state
metadata_send(void *arg, int *retval) metadata_send(void *arg, int *retval)
{ {
@ -3020,6 +3045,31 @@ player_device_remove(void *device)
return ret; return ret;
} }
static void
player_device_auth_kickoff(enum output_types type, char **arglist)
{
union player_arg *cmdarg;
cmdarg = calloc(1, sizeof(union player_arg));
if (!cmdarg)
{
DPRINTF(E_LOG, L_PLAYER, "Could not allocate player_command\n");
return;
}
cmdarg->auth.type = type;
memcpy(cmdarg->auth.pin, arglist[0], 4);
commands_exec_async(cmdbase, device_auth_kickoff, cmdarg);
}
/* Thread: filescanner */
void
player_raop_verification_kickoff(char **arglist)
{
player_device_auth_kickoff(OUTPUT_TYPE_RAOP, arglist);
}
/* Thread: worker */ /* Thread: worker */
static void static void
player_metadata_send(struct input_metadata *imd, struct output_metadata *omd) player_metadata_send(struct input_metadata *imd, struct output_metadata *omd)
@ -3057,7 +3107,7 @@ player(void *arg)
for (device = dev_list; device; device = device->next) for (device = dev_list; device; device = device->next)
{ {
ret = db_speaker_save(device->id, device->selected, device->volume, device->name); ret = db_speaker_save(device);
if (ret < 0) if (ret < 0)
DPRINTF(E_LOG, L_PLAYER, "Could not save state for %s device '%s'\n", device->type_name, device->name); DPRINTF(E_LOG, L_PLAYER, "Could not save state for %s device '%s'\n", device->type_name, device->name);
} }

View File

@ -141,6 +141,9 @@ player_device_add(void *device);
int int
player_device_remove(void *device); player_device_remove(void *device);
void
player_raop_verification_kickoff(char **arglist);
struct player_history * struct player_history *
player_history_get(void); player_history_get(void);