Skip to content

Commit 4f9b3cd

Browse files
authored
Merge pull request #33 from music-assistant/16-support-pin-based-pairing
Implementation of PIN-based pairing
2 parents e41fc08 + e9e788c commit 4f9b3cd

File tree

7 files changed

+94
-82
lines changed

7 files changed

+94
-82
lines changed

src/cliap2.c

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -147,12 +147,13 @@ usage(char *program)
147147
printf(" --loglevel <number> Log level (0-5)\n");
148148
printf(" --logfile <filename> Log filename. Not supplying this argument will result in logging to stderr only.\n");
149149
printf(" --logdomains <dom,dom..> Log domains\n");
150-
printf(" --config <file> Use <file> as the configuration file\n");
150+
printf(" --config <file> Use <file> for the configuration file.\n");
151151
printf(" --name <name> Name of the airplay 2 device. Mandatory in absence of --ntpstart.\n");
152152
printf(" --hostname <hostname> Hostname of AirPlay 2 device. Mandatory in absence of --ntpstart.\n");
153153
printf(" --address <address> IP address to bind to for AirPlay 2 service. Mandatory in absence of --ntpstart.\n");
154154
printf(" --port <port> Port number to bind to for AirPlay 2 service. Mandatory in absence of --ntpstart.\n");
155155
printf(" --txt <txt> txt keyvals returned in mDNS for AirPlay 2 service. Mandatory in absence of --ntpstart.\n");
156+
printf(" --auth <auth_key> Authorization key.\n");
156157
printf(" --pipe <audio_filename> filename of named pipe to read streamed audio. Mandatory in absence of --ntpstart.\n");
157158
printf(" --command_pipe <command_filename> filename of named pipe to read commands and metadata. Defaults to <audio_filename>.metadata\n");
158159
printf(" --ntp Print current NTP time and exit.\n");
@@ -524,6 +525,7 @@ main(int argc, char **argv)
524525
{ "testrun", 0, NULL, 514 }, // Used for CI, not documented to user
525526
{ "pipe", 1, NULL, 515 },
526527
{ "command_pipe", 1, NULL, 517 },
528+
{ "auth", 1, NULL, 518 },
527529

528530
{ NULL, 0, NULL, 0 }
529531
};
@@ -629,6 +631,10 @@ main(int argc, char **argv)
629631
mass_named_pipes.metadata_pipe = optarg;
630632
break;
631633

634+
case 518: // authorization key
635+
ap2_device_info.auth_key = optarg;
636+
break;
637+
632638
default:
633639
case '?':
634640
usage(argv[0]);
@@ -683,7 +689,6 @@ main(int argc, char **argv)
683689
conffile_unload();
684690
return EXIT_FAILURE;
685691
}
686-
// logger_detach(); // Eliminate logging to stderr
687692

688693
if (testrun) {
689694
ret = create_pipes(TESTRUN_PIPE);
@@ -694,7 +699,7 @@ main(int argc, char **argv)
694699
mass_named_pipes.audio_pipe = TESTRUN_PIPE;
695700
}
696701
else {
697-
// Check that named pipe exists for audio streaming. Metadata one too?
702+
// Check that named pipes exist for audio streaming and metadata
698703
ret = check_pipe(mass_named_pipes.audio_pipe);
699704
if (ret < 0) {
700705
return EXIT_FAILURE;

src/cliap2.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ typedef struct ap2_device_info
1010
const char *address;
1111
int port;
1212
struct keyval *txt;
13+
const char *auth_key;
1314
uint64_t ntpstart;
1415
uint32_t wait;
1516
struct timespec start_ts;

src/conffile.c

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ static cfg_opt_t sec_general[] =
5252
CFG_STR_LIST("trusted_networks", "{lan}", CFGF_NONE),
5353
CFG_BOOL("ipv6", cfg_false, CFGF_NONE),
5454
CFG_STR("bind_address", NULL, CFGF_NONE),
55-
CFG_BOOL("speaker_autoselect", cfg_false, CFGF_NONE),
55+
CFG_BOOL("speaker_autoselect", cfg_true, CFGF_NONE),
5656
#if defined(__FreeBSD__) || defined(__FreeBSD_kernel__)
5757
CFG_BOOL("high_resolution_clock", cfg_false, CFGF_NONE),
5858
#else
@@ -70,7 +70,7 @@ static cfg_opt_t sec_general[] =
7070
/* library section structure */
7171
static cfg_opt_t sec_library[] =
7272
{
73-
CFG_STR("name", "My Music on %h", CFGF_NONE),
73+
CFG_STR("name", "Music Assistant", CFGF_NONE),
7474
CFG_INT("port", 3689, CFGF_NONE),
7575
CFG_STR("password", NULL, CFGF_NONE),
7676
CFG_STR_LIST("directories", NULL, CFGF_NONE),

src/mass.c

Lines changed: 37 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -81,17 +81,17 @@
8181
#include "mass.h"
8282
#include "wrappers.h"
8383

84-
#define MASS_UPDATE_INTERVAL_SEC 1 // every second
85-
#define MASS_MS_TILL_EXIT 5000 // exit after 5 seconds of pause
86-
#define MASS_METADATA_KEYVAL_SEP "=" // Key-value separator in metadata
84+
#define MASS_UPDATE_INTERVAL_SEC 1 // every second
85+
#define MASS_METADATA_KEYVAL_SEP "=" // Key-value separator in metadata
8786
#define MASS_METADATA_PROGRESS_KEY "PROGRESS"
8887
#define MASS_METADATA_VOLUME_KEY "VOLUME"
8988
#define MASS_METADATA_ARTWORK_KEY "ARTWORK"
9089
#define MASS_METADATA_ALBUM_KEY "ALBUM"
9190
#define MASS_METADATA_TITLE_KEY "TITLE"
9291
#define MASS_METADATA_ARTIST_KEY "ARTIST"
93-
#define MASS_METADATA_DURATION_KEY "DURATION"
94-
#define MASS_METADATA_ACTION_KEY "ACTION"
92+
#define MASS_METADATA_DURATION_KEY "DURATION"
93+
#define MASS_METADATA_ACTION_KEY "ACTION"
94+
#define MASS_METADATA_PIN_KEY "PIN"
9595

9696
/* from cliap2.c */
9797
extern mass_named_pipes_t mass_named_pipes;
@@ -137,6 +137,7 @@ enum pipe_metadata_msg
137137
PIPE_METADATA_MSG_STOP = (1 << 6),
138138
PIPE_METADATA_MSG_PAUSE = (1 << 7),
139139
PIPE_METADATA_MSG_PLAY = (1 << 8),
140+
PIPE_METADATA_MSG_PIN = (1 << 9),
140141
};
141142

142143
struct pipe
@@ -164,6 +165,8 @@ struct pipe_metadata_prepared
164165
char pict_tmpfile_path[sizeof(PIPE_TMPFILE_TEMPLATE)];
165166
// Volume
166167
int volume;
168+
// PIN
169+
char *pin; // 4 digit PIN
167170
// Mutex to share the prepared metadata
168171
pthread_mutex_t lock;
169172
};
@@ -835,7 +838,6 @@ void extract_key_value(const char *input_string, char **key, char **value) {
835838
}
836839

837840
// Parse one metadata item from Music Assistant
838-
// TODO: @bradkeifer - check memory management to ensure key and value are freed properly
839841
static int
840842
parse_mass_item(enum pipe_metadata_msg *out_msg, struct pipe_metadata_prepared *prepared, const char *item)
841843
{
@@ -924,6 +926,21 @@ parse_mass_item(enum pipe_metadata_msg *out_msg, struct pipe_metadata_prepared *
924926
free(value);
925927
DPRINTF(E_DBG, L_PLAYER, "%s:Parsed Music Assistant volume: %d\n", __func__, prepared->volume);
926928
}
929+
else if (!strncmp(key,MASS_METADATA_PIN_KEY, strlen(MASS_METADATA_PIN_KEY))) {
930+
message = PIPE_METADATA_MSG_PIN;
931+
uint32_t pin;
932+
ret = safe_atou32(value, &pin);
933+
if (ret < 0 || ret > 9999) { // PIN's limited to 4 digits
934+
DPRINTF(E_LOG, L_PLAYER, "%s:Invalid PIN value in Music Assistant metadata: '%s'\n", __func__, value);
935+
free(key);
936+
free(value);
937+
return -1;
938+
}
939+
asprintf(&prepared->pin, "%0.4u", pin);
940+
free(key);
941+
free(value);
942+
DPRINTF(E_DBG, L_PLAYER, "%s:Parsed Music Assistant PIN: %0.4s\n", __func__, prepared->pin);
943+
}
927944
else if (!strncmp(key,MASS_METADATA_ACTION_KEY, strlen(MASS_METADATA_ACTION_KEY))) {
928945
if (strncmp(value, "SENDMETA", strlen("SENDMETA")) == 0) {
929946
message = PIPE_METADATA_MSG_METADATA;
@@ -962,75 +979,6 @@ parse_mass_item(enum pipe_metadata_msg *out_msg, struct pipe_metadata_prepared *
962979
return 0;
963980
}
964981

965-
static int
966-
parse_item(enum pipe_metadata_msg *out_msg, struct pipe_metadata_prepared *prepared, const char *item)
967-
{
968-
struct input_metadata *m = &prepared->input_metadata;
969-
uint32_t type;
970-
uint32_t code;
971-
uint8_t *data;
972-
int data_len;
973-
char **dstptr;
974-
enum pipe_metadata_msg message;
975-
int ret;
976-
977-
ret = parse_item_xml(&type, &code, &data, &data_len, item);
978-
if (ret < 0)
979-
return -1;
980-
981-
dstptr = NULL;
982-
message = PIPE_METADATA_MSG_METADATA;
983-
984-
if (code == dmap_str2val("asal"))
985-
dstptr = &m->album;
986-
else if (code == dmap_str2val("asar"))
987-
dstptr = &m->artist;
988-
else if (code == dmap_str2val("minm"))
989-
dstptr = &m->title;
990-
else if (code == dmap_str2val("asgn"))
991-
dstptr = &m->genre;
992-
else if (code == dmap_str2val("prgr"))
993-
message = PIPE_METADATA_MSG_PROGRESS;
994-
else if (code == dmap_str2val("pvol"))
995-
message = PIPE_METADATA_MSG_VOLUME;
996-
else if (code == dmap_str2val("PICT"))
997-
message = PIPE_METADATA_MSG_PICTURE;
998-
else if (code == dmap_str2val("pfls"))
999-
message = PIPE_METADATA_MSG_FLUSH;
1000-
else
1001-
goto ignore;
1002-
1003-
if (message != PIPE_METADATA_MSG_FLUSH && (!data || data_len == 0))
1004-
{
1005-
log_incoming(E_DBG, "Missing or pending Shairport metadata payload", type, code, data_len);
1006-
goto ignore;
1007-
}
1008-
1009-
ret = 0;
1010-
if (message == PIPE_METADATA_MSG_PROGRESS)
1011-
ret = parse_progress(prepared, (char *)data);
1012-
else if (message == PIPE_METADATA_MSG_VOLUME)
1013-
ret = parse_volume(prepared, (char *)data);
1014-
else if (message == PIPE_METADATA_MSG_PICTURE)
1015-
ret= parse_picture(prepared, data, data_len);
1016-
else if (dstptr)
1017-
swap_pointers(dstptr, (char **)&data);
1018-
1019-
if (ret < 0)
1020-
goto ignore;
1021-
1022-
log_incoming(E_DBG, "Applying Shairport metadata", type, code, data_len);
1023-
1024-
*out_msg = message;
1025-
free(data);
1026-
return 0;
1027-
1028-
ignore:
1029-
*out_msg = 0;
1030-
free(data);
1031-
return 0;
1032-
}
1033-
1034982
// Music Assistant terminates commands/metadata with a newline. This extracts
1035983
// one item from the evbuffer, or NULL if there is no complete item.
1036984
static char *
@@ -1092,6 +1040,7 @@ pipe_read_cb(evutil_socket_t fd, short event, void *arg)
10921040
{
10931041
struct pipe *pipe = arg;
10941042
struct player_status status;
1043+
struct player_speaker_info speaker_info;
10951044
int ret;
10961045

10971046
ret = player_get_status(&status);
@@ -1287,6 +1236,14 @@ pipe_metadata_read_cb(evutil_socket_t fd, short event, void *arg)
12871236
DPRINTF(E_DBG, L_PLAYER, "%s:Setting volume from metadata pipe to %d\n", __func__, pipe_metadata.prepared.volume);
12881237
player_volume_set(pipe_metadata.prepared.volume);
12891238
}
1239+
if (message & PIPE_METADATA_MSG_PIN) {
1240+
DPRINTF(E_DBG, L_PLAYER, "%s:Setting PIN from metadata pipe to %s\n", __func__, pipe_metadata.prepared.pin);
1241+
// We only support AirPlay2 at the moment. The below code will need to be changed if we add support
1242+
// for RAOP.
1243+
player_verification_kickoff(&pipe_metadata.prepared.pin, OUTPUT_TYPE_AIRPLAY);
1244+
free(pipe_metadata.prepared.pin);
1245+
1246+
}
12901247
if (message & PIPE_METADATA_MSG_FLUSH) {
12911248
DPRINTF(E_DBG, L_PLAYER, "%s:Flushing playback from metadata pipe command\n", __func__);
12921249
player_playback_flush();
@@ -1635,6 +1592,9 @@ mass_timer_cb(int fd, short what, void *arg)
16351592
int
16361593
mass_init(void)
16371594
{
1595+
// Maybe we can add a call to player_device_add(device) in here somewhere to initiate device connection before
1596+
// audio is streamed to the named pipe. Currently, device connection is initiatied on receipt of data on the
1597+
// audio named pipe.
16381598
DPRINTF(E_DBG, L_PLAYER, "mass_init()\n");
16391599

16401600
CHECK_ERR(L_PLAYER, mutex_init(&pipe_metadata.prepared.lock));
@@ -1644,6 +1604,8 @@ mass_init(void)
16441604
if (!tid_pipe) {
16451605
// main thread
16461606
// Create a persistent event timer in the main event loop to monitor and report playback status
1607+
// Using the main thread may not be necessary anymore - test moving this into the pipe thread at some time
1608+
// to understand the implications.
16471609
mass_timer_event = event_new(evbase_main, -1, EV_PERSIST | EV_TIMEOUT, mass_timer_cb, NULL);
16481610
DPRINTF(E_DBG, L_PLAYER,
16491611
"%s:Activating persistent event timer with timeval %d sec, %d usec\n,",

src/player.c

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1645,6 +1645,7 @@ device_activate_cb(struct output_device *device, enum output_device_state status
16451645
if (status == OUTPUT_STATE_PASSWORD)
16461646
{
16471647
DPRINTF(E_LOG, L_PLAYER, "The %s device '%s' requires a valid PIN or password\n", device->type_name, device->name);
1648+
// We need to figure out how to prevent playback starting until such time as PIN or password has been supplied??
16481649

16491650
outputs_device_deselect(device);
16501651

@@ -2114,6 +2115,10 @@ playback_start_item(void *arg, int *retval)
21142115
// Start sessions on selected devices. We shouldn't see any callbacks to
21152116
// device_shutdown_cb, since the unselected devices shouldn't have sessions.
21162117
*retval = outputs_start(device_activate_cb, device_shutdown_cb, false);
2118+
DPRINTF(E_DBG, L_PLAYER,
2119+
"%s:outputs_start(device_activate_cb, device_shutdown_cb, false) returned %d\n",
2120+
__func__, *retval
2121+
);
21172122
if (*retval < 0)
21182123
DPRINTF(E_WARN, L_PLAYER, "All selected speakers failed to start\n");
21192124

@@ -2129,6 +2134,13 @@ playback_start_item(void *arg, int *retval)
21292134
continue;
21302135

21312136
*retval = outputs_device_start(device, device_activate_cb, false);
2137+
DPRINTF(E_DBG, L_PLAYER,
2138+
"%s:retval %d, Device %s, state: %d, requires auth:%s, prevent playback:%s, busy:%s\n",
2139+
__func__, *retval, device->name, device->state,
2140+
device->requires_auth ? "true" : "false",
2141+
device->prevent_playback ? "true" : "false",
2142+
device->busy ? "true" : "false"
2143+
);
21322144
if (*retval < 0)
21332145
continue;
21342146

@@ -3861,6 +3873,25 @@ player_raop_verification_kickoff(char **arglist)
38613873

38623874
}
38633875

3876+
void
3877+
player_verification_kickoff(char **arglist, enum output_types type)
3878+
{
3879+
union player_arg *cmdarg;
3880+
3881+
cmdarg = calloc(1, sizeof(union player_arg));
3882+
if (!cmdarg)
3883+
{
3884+
DPRINTF(E_LOG, L_PLAYER, "Could not allocate player_command\n");
3885+
return;
3886+
}
3887+
3888+
cmdarg->auth.type = type;
3889+
memcpy(cmdarg->auth.pin, arglist[0], 4);
3890+
3891+
commands_exec_async(cmdbase, device_auth_kickoff, cmdarg);
3892+
3893+
}
3894+
38643895

38653896
/* ---------------------------- Thread: player ------------------------------ */
38663897

src/player.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,9 @@ player_device_remove(void *device);
203203
void
204204
player_raop_verification_kickoff(char **arglist);
205205

206+
void
207+
player_verification_kickoff(char **arglist, enum output_types type);
208+
206209
const char *
207210
player_pmap(void *p);
208211

src/wrappers.c

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -387,7 +387,11 @@ db_perthread_deinit(void)
387387
int
388388
db_speaker_save(struct output_device *device)
389389
{
390-
// It has been tested that it is ok to do nothing
390+
if (device->auth_key) {
391+
DPRINTF(E_DBG, L_DB, "%s:Device %s has authorization key '%s'\n",
392+
__func__, device->name, device->auth_key
393+
);
394+
}
391395
return 0;
392396
}
393397

@@ -397,7 +401,7 @@ db_speaker_get(struct output_device *device, uint64_t id)
397401
device->id = id;
398402
device->selected = 1;
399403
device->volume = ap2_device_info.volume;
400-
//device->auth_key = ?? // might need to pass this as command line argument for some devices
404+
device->auth_key = (char *)ap2_device_info.auth_key; // lose the const qualifier
401405
device->selected_format = MEDIA_FORMAT_ALAC;
402406

403407
return 0;
@@ -472,6 +476,12 @@ db_escape_string(const char *str)
472476
/*
473477
* Wrappers for mdns.c
474478
*/
479+
480+
// Immediately call the mdns_browse_cb with the information about the
481+
// airplay device we want to stream to.
482+
// TODO: @bradkeifer - see if we can trigger device connection after
483+
// the callback is complete. At the moment, device connection is triggered
484+
// on receipt of first audio streaming data on the named pipe.
475485
int
476486
mdns_browse(char *type, mdns_browse_cb cb, enum mdns_options flags)
477487
{

0 commit comments

Comments
 (0)