[outputs] Add nickname option for Airplay/Chromecast speakers (issue #1145)

This commit is contained in:
ejurgensen 2020-12-14 23:47:21 +01:00
parent 76cd982201
commit 14756c0d59
5 changed files with 71 additions and 69 deletions

View File

@ -261,8 +261,7 @@ audio {
# README about ALSA for details. Note that these settings will override the ALSA # README about ALSA for details. Note that these settings will override the ALSA
# settings in the "audio" section above. # settings in the "audio" section above.
#alsa "card name" { #alsa "card name" {
# Name - used in the speaker list in Remote # Name used in the speaker list. If not set, the card name will be used.
# If not set, the card name will be used
# nickname = "Computer" # nickname = "Computer"
# Mixer channel to use for volume control # Mixer channel to use for volume control
@ -312,6 +311,9 @@ audio {
# AirPlay password # AirPlay password
# password = "s1kr3t" # password = "s1kr3t"
# Name used in the speaker list, overrides name from the device
# nickname = "My speaker name"
#} #}
# Chromecast settings # Chromecast settings
@ -324,6 +326,9 @@ audio {
# Enable this option to exclude a particular device from the speaker # Enable this option to exclude a particular device from the speaker
# list # list
# exclude = false # exclude = false
# Name used in the speaker list, overrides name from the device
# nickname = "My speaker name"
#} #}
# Spotify settings (only have effect if Spotify enabled - see README/INSTALL) # Spotify settings (only have effect if Spotify enabled - see README/INSTALL)

View File

@ -160,6 +160,7 @@ static cfg_opt_t sec_airplay[] =
CFG_BOOL("permanent", cfg_false, CFGF_NONE), CFG_BOOL("permanent", cfg_false, CFGF_NONE),
CFG_BOOL("reconnect", cfg_false, CFGF_NODEFAULT), CFG_BOOL("reconnect", cfg_false, CFGF_NODEFAULT),
CFG_STR("password", NULL, CFGF_NONE), CFG_STR("password", NULL, CFGF_NONE),
CFG_STR("nickname", NULL, CFGF_NONE),
CFG_END() CFG_END()
}; };
@ -169,6 +170,7 @@ static cfg_opt_t sec_chromecast[] =
CFG_INT("max_volume", 11, CFGF_NONE), CFG_INT("max_volume", 11, CFGF_NONE),
CFG_BOOL("exclude", cfg_false, CFGF_NONE), CFG_BOOL("exclude", cfg_false, CFGF_NONE),
CFG_INT("offset_ms", 0, CFGF_NONE), CFG_INT("offset_ms", 0, CFGF_NONE),
CFG_STR("nickname", NULL, CFGF_NONE),
CFG_END() CFG_END()
}; };

View File

@ -128,6 +128,7 @@ struct output_device
// Device volume // Device volume
int volume; int volume;
int relvol; int relvol;
int max_volume;
// Quality of audio output // Quality of audio output
struct media_quality quality; struct media_quality quality;

View File

@ -1822,7 +1822,7 @@ cast_device_cb(const char *name, const char *type, const char *domain, const cha
{ {
struct output_device *device; struct output_device *device;
const char *friendly_name; const char *friendly_name;
cfg_t *chromecast; cfg_t *devcfg;
uint32_t id; uint32_t id;
id = djb_hash(name, strlen(name)); id = djb_hash(name, strlen(name));
@ -1838,12 +1838,16 @@ cast_device_cb(const char *name, const char *type, const char *domain, const cha
DPRINTF(E_DBG, L_CAST, "Event for Chromecast device '%s' (port %d, id %" PRIu32 ")\n", name, port, id); DPRINTF(E_DBG, L_CAST, "Event for Chromecast device '%s' (port %d, id %" PRIu32 ")\n", name, port, id);
chromecast = cfg_gettsec(cfg, "chromecast", name); devcfg = cfg_gettsec(cfg, "chromecast", name);
if (chromecast && cfg_getbool(chromecast, "exclude")) if (devcfg && cfg_getbool(devcfg, "exclude"))
{ {
DPRINTF(E_LOG, L_CAST, "Excluding Chromecast device '%s' as set in config\n", name); DPRINTF(E_LOG, L_CAST, "Excluding Chromecast device '%s' as set in config\n", name);
return; return;
} }
if (devcfg && cfg_getstr(devcfg, "nickname"))
{
name = cfg_getstr(devcfg, "nickname");
}
device = calloc(1, sizeof(struct output_device)); device = calloc(1, sizeof(struct output_device));
if (!device) if (!device)
@ -1876,6 +1880,14 @@ cast_device_cb(const char *name, const char *type, const char *domain, const cha
return; return;
} }
// Max volume
device->max_volume = devcfg ? cfg_getint(devcfg, "max_volume") : CAST_CONFIG_MAX_VOLUME;
if ((device->max_volume < 1) || (device->max_volume > CAST_CONFIG_MAX_VOLUME))
{
DPRINTF(E_LOG, L_CAST, "Config has bad max_volume (%d) for device '%s', using default instead\n", device->max_volume, name);
device->max_volume = CAST_CONFIG_MAX_VOLUME;
}
DPRINTF(E_INFO, L_CAST, "Adding Chromecast device '%s'\n", name); DPRINTF(E_INFO, L_CAST, "Adding Chromecast device '%s'\n", name);
device->advertised = 1; device->advertised = 1;
@ -2261,22 +2273,12 @@ static int
cast_device_volume_set(struct output_device *device, int callback_id) cast_device_volume_set(struct output_device *device, int callback_id)
{ {
struct cast_session *cs = device->session; struct cast_session *cs = device->session;
cfg_t *cast_cfg;
int max_volume;
int ret; int ret;
if (!cs || !(cs->state & CAST_STATE_F_APP_READY)) if (!cs || !(cs->state & CAST_STATE_F_APP_READY))
return 0; return 0;
cast_cfg = cfg_gettsec(cfg, "chromecast", device->name); cs->volume = ((float)device->max_volume * (float)device->volume * 1.0) / (100.0 * CAST_CONFIG_MAX_VOLUME);
max_volume = cast_cfg ? cfg_getint(cast_cfg, "max_volume") : CAST_CONFIG_MAX_VOLUME;
if ((max_volume < 1) || (max_volume > CAST_CONFIG_MAX_VOLUME))
{
DPRINTF(E_LOG, L_CAST, "Config has bad max_volume (%d) for device '%s', using default instead\n", max_volume, device->name);
max_volume = CAST_CONFIG_MAX_VOLUME;
}
cs->volume = ((float)max_volume * (float)device->volume * 1.0) / (100.0 * CAST_CONFIG_MAX_VOLUME);
ret = cast_msg_send(cs, SET_VOLUME, cast_cb_volume); ret = cast_msg_send(cs, SET_VOLUME, cast_cb_volume);
if (ret < 0) if (ret < 0)

View File

@ -2488,31 +2488,16 @@ raop_metadata_send(struct output_metadata *metadata)
/* ------------------------------ Volume handling --------------------------- */ /* ------------------------------ Volume handling --------------------------- */
static float static float
raop_volume_from_pct(int volume, char *name) raop_volume_from_pct(int volume, struct output_device *device)
{ {
float raop_volume; float raop_volume;
cfg_t *airplay;
int max_volume;
max_volume = RAOP_CONFIG_MAX_VOLUME;
airplay = cfg_gettsec(cfg, "airplay", name);
if (airplay)
max_volume = cfg_getint(airplay, "max_volume");
if ((max_volume < 1) || (max_volume > RAOP_CONFIG_MAX_VOLUME))
{
DPRINTF(E_LOG, L_RAOP, "Config has bad max_volume (%d) for device '%s', using default instead\n", max_volume, name);
max_volume = RAOP_CONFIG_MAX_VOLUME;
}
/* RAOP volume /* RAOP volume
* -144.0 is off * -144.0 is off
* 0 - 100 maps to -30.0 - 0 * 0 - 100 maps to -30.0 - 0
*/ */
if (volume > 0 && volume <= 100) if (volume > 0 && volume <= 100)
raop_volume = -30.0 + ((float)max_volume * (float)volume * 30.0) / (100.0 * RAOP_CONFIG_MAX_VOLUME); raop_volume = -30.0 + ((float)device->max_volume * (float)volume * 30.0) / (100.0 * RAOP_CONFIG_MAX_VOLUME);
else else
raop_volume = -144.0; raop_volume = -144.0;
@ -2520,11 +2505,9 @@ raop_volume_from_pct(int volume, char *name)
} }
static int static int
raop_volume_to_pct(struct output_device *rd, const char *volume) raop_volume_to_pct(struct output_device *device, const char *volume)
{ {
float raop_volume; float raop_volume;
cfg_t *airplay;
int max_volume;
raop_volume = atof(volume); raop_volume = atof(volume);
@ -2535,21 +2518,9 @@ raop_volume_to_pct(struct output_device *rd, const char *volume)
return -1; return -1;
} }
max_volume = RAOP_CONFIG_MAX_VOLUME;
airplay = cfg_gettsec(cfg, "airplay", rd->name);
if (airplay)
max_volume = cfg_getint(airplay, "max_volume");
if ((max_volume < 1) || (max_volume > RAOP_CONFIG_MAX_VOLUME))
{
DPRINTF(E_LOG, L_RAOP, "Config has bad max_volume (%d) for device '%s', using default instead\n", max_volume, rd->name);
max_volume = RAOP_CONFIG_MAX_VOLUME;
}
// RAOP volume: -144.0 is off, -30.0 - 0 scaled by max_volume maps to 0 - 100 // RAOP volume: -144.0 is off, -30.0 - 0 scaled by max_volume maps to 0 - 100
if (raop_volume > -30.0 && raop_volume <= 0.0) if (raop_volume > -30.0 && raop_volume <= 0.0)
return (int)(100.0 * (raop_volume / 30.0 + 1.0) * RAOP_CONFIG_MAX_VOLUME / (float)max_volume); return (int)(100.0 * (raop_volume / 30.0 + 1.0) * RAOP_CONFIG_MAX_VOLUME / (float)device->max_volume);
else else
return 0; return 0;
} }
@ -2557,10 +2528,19 @@ raop_volume_to_pct(struct output_device *rd, const char *volume)
static int static int
raop_set_volume_internal(struct raop_session *rs, int volume, evrtsp_req_cb cb) raop_set_volume_internal(struct raop_session *rs, int volume, evrtsp_req_cb cb)
{ {
struct output_device *device;
struct evbuffer *evbuf; struct evbuffer *evbuf;
float raop_volume; float raop_volume;
int ret; int ret;
device = outputs_device_get(rs->device_id);
if (!device)
{
DPRINTF(E_LOG, L_RAOP, "Could not set volume, device with id %" PRIu64 " not in out list\n", rs->device_id);
return -1;
}
evbuf = evbuffer_new(); evbuf = evbuffer_new();
if (!evbuf) if (!evbuf)
{ {
@ -2569,7 +2549,7 @@ raop_set_volume_internal(struct raop_session *rs, int volume, evrtsp_req_cb cb)
return -1; return -1;
} }
raop_volume = raop_volume_from_pct(volume, rs->devname); raop_volume = raop_volume_from_pct(volume, device);
/* Don't let locales get in the way here */ /* Don't let locales get in the way here */
/* We use -%d and -(int)raop_volume so -0.3 won't become 0.3 */ /* We use -%d and -(int)raop_volume so -0.3 won't become 0.3 */
@ -4438,7 +4418,7 @@ raop_device_cb(const char *name, const char *type, const char *domain, const cha
cfg_t *devcfg; cfg_t *devcfg;
cfg_opt_t *cfgopt; cfg_opt_t *cfgopt;
const char *p; const char *p;
char *at_name; const char *device_name;
char *password; char *password;
char *s; char *s;
char *token; char *token;
@ -4455,36 +4435,40 @@ raop_device_cb(const char *name, const char *type, const char *domain, const cha
return; return;
} }
at_name = strchr(name, '@'); device_name = strchr(name, '@');
if (!at_name) if (!device_name)
{ {
DPRINTF(E_LOG, L_RAOP, "Could not extract AirPlay device name ('%s')\n", name); DPRINTF(E_LOG, L_RAOP, "Could not extract AirPlay device name ('%s')\n", name);
return; return;
} }
at_name++; device_name++;
DPRINTF(E_DBG, L_RAOP, "Event for AirPlay device '%s' (port %d, id %" PRIx64 ")\n", at_name, port, id); DPRINTF(E_DBG, L_RAOP, "Event for AirPlay device '%s' (port %d, id %" PRIx64 ")\n", device_name, port, id);
devcfg = cfg_gettsec(cfg, "airplay", at_name); devcfg = cfg_gettsec(cfg, "airplay", device_name);
if (devcfg && cfg_getbool(devcfg, "exclude")) if (devcfg && cfg_getbool(devcfg, "exclude"))
{ {
DPRINTF(E_LOG, L_RAOP, "Excluding AirPlay device '%s' as set in config\n", at_name); DPRINTF(E_LOG, L_RAOP, "Excluding AirPlay device '%s' as set in config\n", device_name);
return; return;
} }
if (devcfg && cfg_getbool(devcfg, "permanent") && (port < 0)) if (devcfg && cfg_getbool(devcfg, "permanent") && (port < 0))
{ {
DPRINTF(E_INFO, L_RAOP, "AirPlay device '%s' disappeared, but set as permanent in config\n", at_name); DPRINTF(E_INFO, L_RAOP, "AirPlay device '%s' disappeared, but set as permanent in config\n", device_name);
return; return;
} }
if (devcfg && cfg_getstr(devcfg, "nickname"))
{
device_name = cfg_getstr(devcfg, "nickname");
}
CHECK_NULL(L_RAOP, rd = calloc(1, sizeof(struct output_device))); CHECK_NULL(L_RAOP, rd = calloc(1, sizeof(struct output_device)));
CHECK_NULL(L_RAOP, re = calloc(1, sizeof(struct raop_extra))); CHECK_NULL(L_RAOP, re = calloc(1, sizeof(struct raop_extra)));
rd->id = id; rd->id = id;
rd->name = strdup(at_name); rd->name = strdup(device_name);
rd->type = OUTPUT_TYPE_RAOP; rd->type = OUTPUT_TYPE_RAOP;
rd->type_name = outputs_name(rd->type); rd->type_name = outputs_name(rd->type);
rd->extra_device_info = re; rd->extra_device_info = re;
@ -4514,21 +4498,21 @@ raop_device_cb(const char *name, const char *type, const char *domain, const cha
p = keyval_get(txt, "tp"); p = keyval_get(txt, "tp");
if (!p) if (!p)
{ {
DPRINTF(E_LOG, L_RAOP, "AirPlay '%s': no tp field in TXT record!\n", at_name); DPRINTF(E_LOG, L_RAOP, "AirPlay '%s': no tp field in TXT record!\n", device_name);
goto free_rd; goto free_rd;
} }
if (*p == '\0') if (*p == '\0')
{ {
DPRINTF(E_LOG, L_RAOP, "AirPlay '%s': tp has no value\n", at_name); DPRINTF(E_LOG, L_RAOP, "AirPlay '%s': tp has no value\n", device_name);
goto free_rd; goto free_rd;
} }
if (!strstr(p, "UDP")) if (!strstr(p, "UDP"))
{ {
DPRINTF(E_LOG, L_RAOP, "AirPlay '%s': device does not support AirTunes v2 (tp=%s), discarding\n", at_name, p); DPRINTF(E_LOG, L_RAOP, "AirPlay '%s': device does not support AirTunes v2 (tp=%s), discarding\n", device_name, p);
goto free_rd; goto free_rd;
} }
@ -4542,7 +4526,7 @@ raop_device_cb(const char *name, const char *type, const char *domain, const cha
} }
else if (*p == '\0') else if (*p == '\0')
{ {
DPRINTF(E_LOG, L_RAOP, "AirPlay '%s': pw has no value\n", at_name); DPRINTF(E_LOG, L_RAOP, "AirPlay '%s': pw has no value\n", device_name);
goto free_rd; goto free_rd;
} }
@ -4553,13 +4537,13 @@ raop_device_cb(const char *name, const char *type, const char *domain, const cha
if (rd->has_password) if (rd->has_password)
{ {
DPRINTF(E_LOG, L_RAOP, "AirPlay device '%s' is password-protected\n", at_name); DPRINTF(E_LOG, L_RAOP, "AirPlay device '%s' is password-protected\n", device_name);
if (devcfg) if (devcfg)
password = cfg_getstr(devcfg, "password"); password = cfg_getstr(devcfg, "password");
if (!password) if (!password)
DPRINTF(E_LOG, L_RAOP, "No password given in config for AirPlay device '%s'\n", at_name); DPRINTF(E_LOG, L_RAOP, "No password given in config for AirPlay device '%s'\n", device_name);
} }
rd->password = password; rd->password = password;
@ -4591,6 +4575,14 @@ raop_device_cb(const char *name, const char *type, const char *domain, const cha
if (!quality_is_equal(&rd->quality, &raop_quality_default)) if (!quality_is_equal(&rd->quality, &raop_quality_default))
DPRINTF(E_LOG, L_RAOP, "Device '%s' requested non-default audio quality (%d/%d/%d)\n", rd->name, rd->quality.sample_rate, rd->quality.bits_per_sample, rd->quality.channels); DPRINTF(E_LOG, L_RAOP, "Device '%s' requested non-default audio quality (%d/%d/%d)\n", rd->name, rd->quality.sample_rate, rd->quality.bits_per_sample, rd->quality.channels);
// Max volume
rd->max_volume = devcfg ? cfg_getint(devcfg, "max_volume") : RAOP_CONFIG_MAX_VOLUME;
if ((rd->max_volume < 1) || (rd->max_volume > RAOP_CONFIG_MAX_VOLUME))
{
DPRINTF(E_LOG, L_RAOP, "Config has bad max_volume (%d) for device '%s', using default instead\n", rd->max_volume, device_name);
rd->max_volume = RAOP_CONFIG_MAX_VOLUME;
}
// Device type // Device type
re->devtype = RAOP_DEV_OTHER; re->devtype = RAOP_DEV_OTHER;
p = keyval_get(txt, "am"); p = keyval_get(txt, "am");
@ -4608,7 +4600,7 @@ raop_device_cb(const char *name, const char *type, const char *domain, const cha
else if (strncmp(p, "AudioAccessory", strlen("AudioAccessory")) == 0) else if (strncmp(p, "AudioAccessory", strlen("AudioAccessory")) == 0)
re->devtype = RAOP_DEV_HOMEPOD; re->devtype = RAOP_DEV_HOMEPOD;
else if (*p == '\0') else if (*p == '\0')
DPRINTF(E_LOG, L_RAOP, "AirPlay device '%s': am has no value\n", at_name); DPRINTF(E_LOG, L_RAOP, "AirPlay device '%s': am has no value\n", device_name);
// If the user didn't set any reconnect setting we enable for Apple TV and // If the user didn't set any reconnect setting we enable for Apple TV and
// HomePods due to https://github.com/ejurgensen/forked-daapd/issues/734 // HomePods due to https://github.com/ejurgensen/forked-daapd/issues/734
@ -4665,18 +4657,18 @@ raop_device_cb(const char *name, const char *type, const char *domain, const cha
rd->v4_address = strdup(address); rd->v4_address = strdup(address);
rd->v4_port = port; rd->v4_port = port;
DPRINTF(E_INFO, L_RAOP, "Adding AirPlay device '%s': password: %u, verification: %u, encrypt: %u, authsetup: %u, metadata: %u, type %s, address %s:%d\n", DPRINTF(E_INFO, L_RAOP, "Adding AirPlay device '%s': password: %u, verification: %u, encrypt: %u, authsetup: %u, metadata: %u, type %s, address %s:%d\n",
at_name, rd->has_password, rd->requires_auth, re->encrypt, re->supports_auth_setup, re->wanted_metadata, raop_devtype[re->devtype], address, port); device_name, rd->has_password, rd->requires_auth, re->encrypt, re->supports_auth_setup, re->wanted_metadata, raop_devtype[re->devtype], address, port);
break; break;
case AF_INET6: case AF_INET6:
rd->v6_address = strdup(address); rd->v6_address = strdup(address);
rd->v6_port = port; rd->v6_port = port;
DPRINTF(E_INFO, L_RAOP, "Adding AirPlay device '%s': password: %u, verification: %u, encrypt: %u, authsetup: %u, metadata: %u, type %s, address [%s]:%d\n", DPRINTF(E_INFO, L_RAOP, "Adding AirPlay device '%s': password: %u, verification: %u, encrypt: %u, authsetup: %u, metadata: %u, type %s, address [%s]:%d\n",
at_name, rd->has_password, rd->requires_auth, re->encrypt, re->supports_auth_setup, re->wanted_metadata, raop_devtype[re->devtype], address, port); device_name, rd->has_password, rd->requires_auth, re->encrypt, re->supports_auth_setup, re->wanted_metadata, raop_devtype[re->devtype], address, port);
break; break;
default: default:
DPRINTF(E_LOG, L_RAOP, "Error: AirPlay device '%s' has neither ipv4 og ipv6 address\n", at_name); DPRINTF(E_LOG, L_RAOP, "Error: AirPlay device '%s' has neither ipv4 og ipv6 address\n", device_name);
goto free_rd; goto free_rd;
} }