]>
Commit | Line | Data |
---|---|---|
3198ec42 MB |
1 | /* |
2 | * DACP protocol handler. This file is part of Shairport Sync. | |
a6acb606 | 3 | * Copyright (c) Mike Brady 2017 -- 2020 |
3198ec42 MB |
4 | * All rights reserved. |
5 | * | |
6 | * Permission is hereby granted, free of charge, to any person | |
7 | * obtaining a copy of this software and associated documentation | |
8 | * files (the "Software"), to deal in the Software without | |
9 | * restriction, including without limitation the rights to use, | |
10 | * copy, modify, merge, publish, distribute, sublicense, and/or | |
11 | * sell copies of the Software, and to permit persons to whom the | |
12 | * Software is furnished to do so, subject to the following conditions: | |
3198ec42 MB |
13 | * The above copyright notice and this permission notice shall be |
14 | * included in all copies or substantial portions of the Software. | |
15 | * | |
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, | |
17 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES | |
18 | * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND | |
19 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT | |
20 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, | |
21 | * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING | |
22 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR | |
23 | * OTHER DEALINGS IN THE SOFTWARE. | |
24 | */ | |
25 | ||
6af24f7c MB |
26 | // Information about the four-character codes is from many sources, with thanks, including |
27 | // https://github.com/melloware/dacp-net/blob/master/Melloware.DACP/ | |
28 | ||
3198ec42 MB |
29 | #include "dacp.h" |
30 | #include "common.h" | |
31 | #include "config.h" | |
32 | ||
33 | #include <arpa/inet.h> | |
34 | #include <errno.h> | |
c36a7822 | 35 | #include <inttypes.h> |
3198ec42 MB |
36 | #include <math.h> |
37 | #include <memory.h> | |
38 | #include <netdb.h> | |
39 | #include <netinet/in.h> | |
fe3b70b4 | 40 | #include <pthread.h> |
3198ec42 | 41 | #include <stdlib.h> |
413df6d0 | 42 | #include <sys/time.h> |
c8b0be30 | 43 | #include <time.h> |
3198ec42 MB |
44 | #include <unistd.h> |
45 | ||
0801290a | 46 | #include "metadata_hub.h" |
1637a79d MB |
47 | #include "tinyhttp/http.h" |
48 | ||
0f68ff0c | 49 | typedef struct { |
d692fab9 MB |
50 | int players_connection_thread_index; // the connection thread index when a player thread is |
51 | // associated with this, zero otherwise | |
a6acb606 | 52 | int scan_enable; // set to 1 if scanning should be considered |
32c7a535 | 53 | char dacp_id[256]; // the DACP ID string |
d692fab9 MB |
54 | uint16_t port; // zero if no port discovered |
55 | short connection_family; // AF_INET6 or AF_INET | |
27e42a66 | 56 | int always_use_revision_number_1; // for dealing with forked-daapd; |
d692fab9 MB |
57 | uint32_t scope_id; // if it's an ipv6 connection, this will be its scope id |
58 | char ip_string[INET6_ADDRSTRLEN]; // the ip string pointing to the client | |
6857d06b | 59 | char *active_remote_id; // send this when you want to send remote control commands |
32c7a535 | 60 | void *port_monitor_private_storage; |
88c55066 MB |
61 | } dacp_server_record; |
62 | ||
79e22063 | 63 | int dacp_monitor_initialised = 0; |
fe3b70b4 | 64 | pthread_t dacp_monitor_thread; |
88c55066 | 65 | dacp_server_record dacp_server; |
72079ada | 66 | void *mdns_dacp_monitor_private_storage_pointer; |
fe3b70b4 | 67 | |
1637a79d MB |
68 | // HTTP Response data/funcs (See the tinyhttp example.cpp file for more on this.) |
69 | struct HttpResponse { | |
70 | void *body; // this will be a malloc'ed pointer | |
71 | ssize_t malloced_size; // this will be its allocated size | |
72 | ssize_t size; // the current size of the content | |
73 | int code; | |
74 | }; | |
75 | ||
69b3b5d7 MB |
76 | void *response_realloc(__attribute__((unused)) void *opaque, void *ptr, int size) { |
77 | void *t = realloc(ptr, size); | |
78 | if ((t == NULL) && (size != 0)) | |
79 | debug(1, "Response realloc of size %d failed!", size); | |
80 | return t; | |
8991f342 | 81 | } |
1637a79d | 82 | |
69b3b5d7 | 83 | void response_body(void *opaque, const char *data, int size) { |
1637a79d MB |
84 | struct HttpResponse *response = (struct HttpResponse *)opaque; |
85 | ||
6857d06b | 86 | ssize_t space_available = response->malloced_size - response->size; |
1637a79d | 87 | if (space_available < size) { |
6857d06b MB |
88 | // debug(1,"Getting more space for the response -- need %d bytes but only %ld bytes left.\n", |
89 | // size, | |
90 | // size - space_available); | |
91 | ssize_t size_requested = size - space_available + response->malloced_size + 16384; | |
1637a79d MB |
92 | void *t = realloc(response->body, size_requested); |
93 | response->malloced_size = size_requested; | |
6857d06b | 94 | if (t) |
1637a79d | 95 | response->body = t; |
6857d06b | 96 | else { |
57bcea52 | 97 | die("dacp: can't allocate any more space for parser."); |
1637a79d MB |
98 | } |
99 | } | |
100 | memcpy(response->body + response->size, data, size); | |
101 | response->size += size; | |
102 | } | |
103 | ||
8991f342 MB |
104 | static void |
105 | response_header(__attribute__((unused)) void *opaque, __attribute__((unused)) const char *ckey, | |
106 | __attribute__((unused)) int nkey, __attribute__((unused)) const char *cvalue, | |
107 | __attribute__((unused)) int nvalue) { /* example doesn't care about headers */ | |
1637a79d MB |
108 | } |
109 | ||
110 | static void response_code(void *opaque, int code) { | |
111 | struct HttpResponse *response = (struct HttpResponse *)opaque; | |
112 | response->code = code; | |
113 | } | |
114 | ||
115 | static const struct http_funcs responseFuncs = { | |
86b6d7bf MB |
116 | response_realloc, |
117 | response_body, | |
118 | response_header, | |
119 | response_code, | |
1637a79d MB |
120 | }; |
121 | ||
7fb89135 MB |
122 | // static pthread_mutex_t dacp_conversation_lock = PTHREAD_MUTEX_INITIALIZER; |
123 | // static pthread_mutex_t dacp_server_information_lock = PTHREAD_MUTEX_INITIALIZER; | |
124 | static pthread_mutex_t dacp_conversation_lock; | |
125 | static pthread_mutex_t dacp_server_information_lock; | |
a6acb606 | 126 | static pthread_cond_t dacp_server_information_cv; |
fe3b70b4 | 127 | |
5f750ba9 | 128 | void addrinfo_cleanup(void *arg) { |
703ab8e5 | 129 | // debug(1, "addrinfo cleanup called."); |
5f750ba9 MB |
130 | struct addrinfo **info = (struct addrinfo **)arg; |
131 | freeaddrinfo(*info); | |
132 | } | |
133 | ||
134 | void mutex_lock_cleanup(void *arg) { | |
5f750ba9 | 135 | pthread_mutex_t *m = (pthread_mutex_t *)arg; |
8e72574a | 136 | if (pthread_mutex_unlock(m)) |
8f20e580 | 137 | debug(1, "Error releasing mutex."); |
5f750ba9 MB |
138 | } |
139 | ||
140 | void connect_cleanup(void *arg) { | |
5f750ba9 | 141 | int *fd = (int *)arg; |
668f7806 | 142 | // debug(2, "dacp_send_command: close socket %d.",*fd); |
5f750ba9 MB |
143 | close(*fd); |
144 | } | |
145 | ||
146 | void http_cleanup(void *arg) { | |
703ab8e5 | 147 | // debug(1, "http cleanup called."); |
5f750ba9 MB |
148 | struct http_roundtripper *rt = (struct http_roundtripper *)arg; |
149 | http_free(rt); | |
150 | } | |
151 | ||
3e2c3e83 | 152 | int dacp_send_command(const char *command, char **body, ssize_t *bodysize) { |
daab7a63 | 153 | int result; |
85b17cda MB |
154 | // debug(1,"dacp_send_command: command is: \"%s\".",command); |
155 | ||
156 | if (dacp_server.port == 0) { | |
77a5cf1e | 157 | // debug(3, "No DACP port specified yet"); |
85b17cda MB |
158 | result = 490; // no port specified |
159 | } else { | |
daab7a63 MB |
160 | |
161 | // will malloc space for the body or set it to NULL -- the caller should free it. | |
162 | ||
163 | // Using some custom HTTP-like return codes | |
164 | // 498 Bad Address information for the DACP server | |
165 | // 497 Can't establish a socket to the DACP server | |
166 | // 496 Can't connect to the DACP server | |
167 | // 495 Error receiving response | |
168 | // 494 This client is already busy | |
169 | // 493 Client failed to send a message | |
170 | // 492 Argument out of range | |
171 | // 491 Client refused connection | |
a6acb606 | 172 | // 490 No port specified |
daab7a63 MB |
173 | |
174 | struct addrinfo hints, *res; | |
175 | int sockfd; | |
176 | ||
177 | struct HttpResponse response; | |
178 | response.body = NULL; | |
179 | response.malloced_size = 0; | |
180 | response.size = 0; | |
181 | response.code = 0; | |
182 | ||
183 | char portstring[10], server[1024], message[1024]; | |
184 | memset(&portstring, 0, sizeof(portstring)); | |
185 | if (dacp_server.connection_family == AF_INET6) { | |
186 | snprintf(server, sizeof(server), "%s%%%u", dacp_server.ip_string, dacp_server.scope_id); | |
187 | } else { | |
188 | strcpy(server, dacp_server.ip_string); | |
189 | } | |
190 | snprintf(portstring, sizeof(portstring), "%u", dacp_server.port); | |
1637a79d | 191 | |
daab7a63 | 192 | // first, load up address structs with getaddrinfo(): |
0f68ff0c | 193 | |
daab7a63 MB |
194 | memset(&hints, 0, sizeof(hints)); |
195 | hints.ai_family = AF_UNSPEC; | |
196 | hints.ai_socktype = SOCK_STREAM; | |
0f68ff0c | 197 | |
daab7a63 | 198 | // debug(1, "DACP port string is \"%s:%s\".", server, portstring); |
0f68ff0c | 199 | |
daab7a63 MB |
200 | int ires = getaddrinfo(server, portstring, &hints, &res); |
201 | if (ires) { | |
202 | // debug(1,"Error %d \"%s\" at getaddrinfo.",ires,gai_strerror(ires)); | |
203 | response.code = 498; // Bad Address information for the DACP server | |
204 | } else { | |
67e9b1b6 | 205 | uint64_t start_time = get_absolute_time_in_ns(); |
daab7a63 MB |
206 | pthread_cleanup_push(addrinfo_cleanup, (void *)&res); |
207 | // only do this one at a time -- not sure it is necessary, but better safe than sorry | |
0f68ff0c | 208 | |
fd880056 MB |
209 | // int mutex_reply = sps_pthread_mutex_timedlock(&dacp_conversation_lock, 2000000, command, |
210 | // 1); | |
2dc6af2d | 211 | int mutex_reply = debug_mutex_lock(&dacp_conversation_lock, 2000000, 1); |
daab7a63 MB |
212 | // int mutex_reply = pthread_mutex_lock(&dacp_conversation_lock); |
213 | if (mutex_reply == 0) { | |
214 | pthread_cleanup_push(mutex_lock_cleanup, (void *)&dacp_conversation_lock); | |
0f68ff0c | 215 | |
daab7a63 MB |
216 | // make a socket: |
217 | sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol); | |
0f68ff0c | 218 | |
daab7a63 | 219 | if (sockfd == -1) { |
85b17cda MB |
220 | // debug(1, "DACP socket could not be created -- error %d: |
221 | // \"%s\".",errno,strerror(errno)); | |
daab7a63 | 222 | response.code = 497; // Can't establish a socket to the DACP server |
1637a79d | 223 | } else { |
daab7a63 MB |
224 | pthread_cleanup_push(connect_cleanup, (void *)&sockfd); |
225 | // debug(2, "dacp_send_command: open socket %d.",sockfd); | |
226 | ||
85b17cda MB |
227 | // This is for limiting the time to be spent waiting for a response. |
228 | ||
daab7a63 MB |
229 | struct timeval tv; |
230 | tv.tv_sec = 0; | |
6857d06b | 231 | tv.tv_usec = 500000; |
27e42a66 | 232 | if (setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, (const char *)&tv, sizeof tv) == -1) |
54d761ff | 233 | debug(1, "dacp_send_command: error %d setting receive timeout.", errno); |
daab7a63 MB |
234 | if (setsockopt(sockfd, SOL_SOCKET, SO_SNDTIMEO, (const char *)&tv, sizeof tv) == -1) |
235 | debug(1, "dacp_send_command: error %d setting send timeout.", errno); | |
236 | ||
237 | // connect! | |
238 | // debug(1, "DACP socket created."); | |
239 | if (connect(sockfd, res->ai_addr, res->ai_addrlen) < 0) { | |
240 | // debug(1, "dacp_send_command: connect failed with errno %d.", errno); | |
241 | if (errno == ECONNREFUSED) | |
242 | response.code = 491; // DACP server doesn't want to talk anymore... | |
243 | else | |
244 | response.code = 496; // Can't connect to the DACP server | |
6af24f7c | 245 | } else { |
daab7a63 MB |
246 | // debug(1,"DACP connect succeeded."); |
247 | ||
248 | snprintf(message, sizeof(message), | |
6857d06b | 249 | "GET /ctrl-int/1/%s HTTP/1.1\r\nHost: %s:%u\r\nActive-Remote: %s\r\n\r\n", |
85b17cda MB |
250 | command, dacp_server.ip_string, dacp_server.port, |
251 | dacp_server.active_remote_id); | |
daab7a63 MB |
252 | |
253 | // Send command | |
254 | debug(3, "dacp_send_command: \"%s\".", command); | |
255 | ssize_t wresp = send(sockfd, message, strlen(message), 0); | |
256 | if (wresp == -1) { | |
257 | char errorstring[1024]; | |
258 | strerror_r(errno, (char *)errorstring, sizeof(errorstring)); | |
259 | debug(2, "dacp_send_command: write error %d: \"%s\".", errno, (char *)errorstring); | |
260 | struct linger so_linger; | |
261 | so_linger.l_onoff = 1; // "true" | |
262 | so_linger.l_linger = 0; | |
263 | int err = setsockopt(sockfd, SOL_SOCKET, SO_LINGER, &so_linger, sizeof so_linger); | |
264 | if (err) | |
265 | debug(1, "Could not set the dacp socket to abort due to a write error on closing."); | |
266 | } | |
267 | if (wresp != (ssize_t)strlen(message)) { | |
268 | // debug(1, "dacp_send_command: send failed."); | |
269 | response.code = 493; // Client failed to send a message | |
6af24f7c | 270 | |
daab7a63 MB |
271 | } else { |
272 | ||
6857d06b MB |
273 | response.body = malloc(2048); // it can resize this if necessary |
274 | response.malloced_size = 2048; | |
daab7a63 MB |
275 | pthread_cleanup_push(malloc_cleanup, response.body); |
276 | ||
277 | struct http_roundtripper rt; | |
278 | http_init(&rt, responseFuncs, &response); | |
279 | pthread_cleanup_push(http_cleanup, &rt); | |
280 | ||
281 | int needmore = 1; | |
282 | int looperror = 0; | |
283 | char buffer[8192]; | |
284 | memset(buffer, 0, sizeof(buffer)); | |
285 | while (needmore && !looperror) { | |
286 | const char *data = buffer; | |
27e42a66 MB |
287 | if (setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, (const char *)&tv, sizeof tv) == -1) |
288 | debug(1, "dacp_send_command: error %d setting receive timeout.", errno); | |
daab7a63 MB |
289 | ssize_t ndata = recv(sockfd, buffer, sizeof(buffer), 0); |
290 | // debug(3, "Received %d bytes: \"%s\".", ndata, buffer); | |
291 | if (ndata <= 0) { | |
292 | if (ndata == -1) { | |
293 | char errorstring[1024]; | |
294 | strerror_r(errno, (char *)errorstring, sizeof(errorstring)); | |
295 | debug(2, "dacp_send_command: receiving error %d: \"%s\".", errno, | |
296 | (char *)errorstring); | |
297 | struct linger so_linger; | |
298 | so_linger.l_onoff = 1; // "true" | |
299 | so_linger.l_linger = 0; | |
85b17cda MB |
300 | int err = |
301 | setsockopt(sockfd, SOL_SOCKET, SO_LINGER, &so_linger, sizeof so_linger); | |
daab7a63 | 302 | if (err) |
85b17cda MB |
303 | debug( |
304 | 1, | |
305 | "Could not set the dacp socket to abort due to a read error on closing."); | |
daab7a63 MB |
306 | } |
307 | ||
308 | free(response.body); | |
309 | response.body = NULL; | |
310 | response.malloced_size = 0; | |
311 | response.size = 0; | |
312 | response.code = 495; // Error receiving response | |
313 | looperror = 1; | |
8f20e580 MB |
314 | } |
315 | ||
daab7a63 MB |
316 | while (needmore && ndata && !looperror) { |
317 | int read; | |
318 | needmore = http_data(&rt, data, ndata, &read); | |
319 | ndata -= read; | |
320 | data += read; | |
321 | } | |
322 | } | |
323 | ||
324 | if (http_iserror(&rt)) { | |
325 | debug(3, "dacp_send_command: error parsing data."); | |
6af24f7c MB |
326 | free(response.body); |
327 | response.body = NULL; | |
328 | response.malloced_size = 0; | |
329 | response.size = 0; | |
6af24f7c | 330 | } |
daab7a63 MB |
331 | // debug(1,"Size of response body is %d",response.size); |
332 | pthread_cleanup_pop(1); // this should call http_cleanup | |
333 | // http_free(&rt); | |
334 | pthread_cleanup_pop( | |
335 | 0); // this should *not* free the malloced buffer -- just pop the malloc cleanup | |
6af24f7c | 336 | } |
1637a79d | 337 | } |
daab7a63 MB |
338 | pthread_cleanup_pop(1); // this should close the socket |
339 | // close(sockfd); | |
340 | // debug(1,"DACP socket closed."); | |
6af24f7c | 341 | } |
daab7a63 MB |
342 | pthread_cleanup_pop(1); // this should unlock the dacp_conversation_lock); |
343 | // pthread_mutex_unlock(&dacp_conversation_lock); | |
344 | // debug(1,"Sent command\"%s\" with a response body of size %d.",command,response.size); | |
345 | // debug(1,"dacp_conversation_lock released."); | |
346 | } else { | |
347 | debug(3, | |
348 | "dacp_send_command: could not acquire a lock on the dacp transmit/receive section " | |
349 | "when attempting to " | |
350 | "send the command \"%s\". Possible timeout?", | |
351 | command); | |
352 | response.code = 494; // This client is already busy | |
0f68ff0c | 353 | } |
daab7a63 MB |
354 | pthread_cleanup_pop(1); // this should free the addrinfo |
355 | // freeaddrinfo(res); | |
67e9b1b6 | 356 | uint64_t et = get_absolute_time_in_ns() - start_time; // this will be in nanoseconds |
daab7a63 | 357 | debug(3, "dacp_send_command: %f seconds, response code %d, command \"%s\".", |
67e9b1b6 | 358 | (1.0 * et) / 1000000000, response.code, command); |
0f68ff0c | 359 | } |
daab7a63 MB |
360 | *body = response.body; |
361 | *bodysize = response.size; | |
362 | result = response.code; | |
71f62a2f | 363 | } |
daab7a63 | 364 | return result; |
0f68ff0c MB |
365 | } |
366 | ||
1637a79d MB |
367 | int send_simple_dacp_command(const char *command) { |
368 | int reply = 0; | |
369 | char *server_reply = NULL; | |
d734b7e7 | 370 | debug(2, "send_simple_dacp_command: sending command \"%s\".", command); |
1637a79d MB |
371 | ssize_t reply_size = 0; |
372 | reply = dacp_send_command(command, &server_reply, &reply_size); | |
373 | if (server_reply) { | |
374 | free(server_reply); | |
375 | server_reply = NULL; | |
376 | } | |
377 | return reply; | |
378 | } | |
0f68ff0c | 379 | |
c02e53ea | 380 | void relinquish_dacp_server_information(rtsp_conn_info *conn) { |
d692fab9 MB |
381 | // this will set the dacp_server.players_connection_thread_index to zero iff it has the same value |
382 | // as the conn's connection number | |
383 | // this is to signify that the player has stopped, but only if another thread (with a different | |
384 | // index) hasn't already taken over the dacp service | |
2eaaa9cd | 385 | debug_mutex_lock(&dacp_server_information_lock, 500000, 2); |
c02e53ea MB |
386 | if (dacp_server.players_connection_thread_index == conn->connection_number) |
387 | dacp_server.players_connection_thread_index = 0; | |
2eaaa9cd | 388 | debug_mutex_unlock(&dacp_server_information_lock, 3); |
c02e53ea MB |
389 | } |
390 | ||
fe3b70b4 | 391 | // this will be running on the thread of its caller, not of the conversation thread... |
32c7a535 MB |
392 | // tell the DACP conversation thread what DACP server we are listening to |
393 | // if the active_remote_id is the same we don't change anything apart from | |
394 | // the conversation number | |
395 | // Thus, we can keep the DACP port that might have previously been discovered | |
396 | void set_dacp_server_information(rtsp_conn_info *conn) { | |
a9d6e0a2 | 397 | // debug(1, "set_dacp_server_information"); |
2eaaa9cd | 398 | debug_mutex_lock(&dacp_server_information_lock, 500000, 2); |
c02e53ea | 399 | dacp_server.players_connection_thread_index = conn->connection_number; |
9e944e40 MB |
400 | |
401 | if ((conn->dacp_id == NULL) || (strcmp(conn->dacp_id, dacp_server.dacp_id) != 0)) { | |
9e944e40 | 402 | if (conn->dacp_id) |
c8b0be30 | 403 | strncpy(dacp_server.dacp_id, conn->dacp_id, sizeof(dacp_server.dacp_id) - 1); |
9e944e40 MB |
404 | else |
405 | dacp_server.dacp_id[0] = '\0'; | |
32c7a535 | 406 | dacp_server.port = 0; |
c36a7822 | 407 | dacp_server.scan_enable = 0; |
32c7a535 MB |
408 | dacp_server.connection_family = conn->connection_ip_family; |
409 | dacp_server.scope_id = conn->self_scope_id; | |
410 | strncpy(dacp_server.ip_string, conn->client_ip_string, INET6_ADDRSTRLEN); | |
27e42a66 | 411 | debug(2, "set_dacp_server_information set IP to \"%s\" and DACP id to \"%s\".", |
4aab0a6f | 412 | dacp_server.ip_string, dacp_server.dacp_id); |
32c7a535 | 413 | |
b7d9c22d | 414 | /* |
6acd73da | 415 | |
b7d9c22d MB |
416 | "long polling" is not implemented by Shairport Sync, whereby by sending the client the |
417 | last-received revision number, the link will hang until a change occurs. | |
6acd73da | 418 | |
b7d9c22d | 419 | Instead, at present, Shairport Sync always uses a revision number of 1. |
6acd73da | 420 | |
b7d9c22d | 421 | */ |
b29e92b8 MB |
422 | |
423 | // always use revision number 1 | |
424 | dacp_server.always_use_revision_number_1 = 1; | |
27e42a66 | 425 | |
32c7a535 MB |
426 | metadata_hub_modify_prolog(); |
427 | int ch = metadata_store.dacp_server_active != dacp_server.scan_enable; | |
428 | metadata_store.dacp_server_active = dacp_server.scan_enable; | |
429 | if ((metadata_store.client_ip == NULL) || | |
430 | (strncmp(metadata_store.client_ip, conn->client_ip_string, INET6_ADDRSTRLEN) != 0)) { | |
431 | if (metadata_store.client_ip) | |
432 | free(metadata_store.client_ip); | |
433 | metadata_store.client_ip = strndup(conn->client_ip_string, INET6_ADDRSTRLEN); | |
434 | debug(3, "MH Client IP set to: \"%s\"", metadata_store.client_ip); | |
435 | ch = 1; | |
436 | } | |
437 | metadata_hub_modify_epilog(ch); | |
00664871 MB |
438 | } else { |
439 | if (dacp_server.port) { | |
a02b9759 | 440 | // debug(1, "Re-enable scanning."); |
00664871 | 441 | dacp_server.scan_enable = 1; |
4aab0a6f MB |
442 | // metadata_hub_modify_prolog(); |
443 | // int ch = metadata_store.dacp_server_active != dacp_server.scan_enable; | |
444 | // metadata_store.dacp_server_active = dacp_server.scan_enable; | |
445 | // metadata_hub_modify_epilog(ch); | |
00664871 | 446 | } |
69b3b5d7 | 447 | } |
6857d06b | 448 | if (dacp_server.active_remote_id) |
3fc95704 | 449 | free(dacp_server.active_remote_id); |
c27fb1c9 | 450 | if (conn->dacp_active_remote) |
18c9e13a MB |
451 | dacp_server.active_remote_id = |
452 | strdup(conn->dacp_active_remote); // even if the dacp_id remains the same, | |
453 | // the active remote will change. | |
c27fb1c9 | 454 | else |
18c9e13a | 455 | dacp_server.active_remote_id = NULL; |
c27fb1c9 | 456 | |
3fc95704 | 457 | debug(3, "set_dacp_server_information set active-remote id to %s.", dacp_server.active_remote_id); |
fe3b70b4 | 458 | pthread_cond_signal(&dacp_server_information_cv); |
2eaaa9cd | 459 | debug_mutex_unlock(&dacp_server_information_lock, 3); |
fe3b70b4 MB |
460 | } |
461 | ||
32c7a535 | 462 | void dacp_monitor_port_update_callback(char *dacp_id, uint16_t port) { |
2eaaa9cd | 463 | debug_mutex_lock(&dacp_server_information_lock, 500000, 2); |
daab7a63 | 464 | debug(2, |
86b6d7bf MB |
465 | "dacp_monitor_port_update_callback with Remote ID \"%s\", target ID \"%s\" and port " |
466 | "number %d.", | |
72079ada | 467 | dacp_id, dacp_server.dacp_id, port); |
e925e196 | 468 | if ((dacp_id == NULL) || (dacp_server.dacp_id[0] == '\0')) { |
3fc95704 | 469 | warn("dacp_id or dacp_server.dacp_id NULL detected"); |
32c7a535 | 470 | } else { |
3fc95704 MB |
471 | if (strcmp(dacp_id, dacp_server.dacp_id) == 0) { |
472 | dacp_server.port = port; | |
473 | if (port == 0) | |
474 | dacp_server.scan_enable = 0; | |
475 | else { | |
476 | dacp_server.scan_enable = 1; | |
477 | // debug(2, "dacp_monitor_port_update_callback enables scan"); | |
478 | } | |
479 | // metadata_hub_modify_prolog(); | |
480 | // int ch = metadata_store.dacp_server_active != dacp_server.scan_enable; | |
481 | // metadata_store.dacp_server_active = dacp_server.scan_enable; | |
482 | // metadata_hub_modify_epilog(ch); | |
483 | } else { | |
dbebdca8 | 484 | debug(2, "dacp port monitor reporting on an out-of-use remote."); |
3fc95704 | 485 | } |
32c7a535 | 486 | } |
4aab0a6f | 487 | pthread_cond_signal(&dacp_server_information_cv); |
2eaaa9cd | 488 | debug_mutex_unlock(&dacp_server_information_lock, 3); |
32c7a535 | 489 | } |
f1d45034 | 490 | |
8991f342 | 491 | void *dacp_monitor_thread_code(__attribute__((unused)) void *na) { |
88c55066 | 492 | int scan_index = 0; |
27e42a66 | 493 | int always_use_revision_number_1 = 0; |
73bf006c | 494 | // char server_reply[10000]; |
bef287d9 | 495 | // debug(1, "DACP monitor thread started."); |
fe3b70b4 | 496 | // wait until we get a valid port number to begin monitoring it |
1637a79d | 497 | int32_t revision_number = 1; |
f9845d01 MB |
498 | int bad_result_count = 0; |
499 | int idle_scan_count = 0; | |
fe3b70b4 | 500 | while (1) { |
f9845d01 | 501 | int result = 0; |
f1d45034 | 502 | int32_t the_volume; |
2dc6af2d | 503 | pthread_cleanup_debug_mutex_lock(&dacp_server_information_lock, 500000, 2); |
fb3f9f51 MB |
504 | if (dacp_server.scan_enable == 0) { |
505 | metadata_hub_modify_prolog(); | |
506 | int ch = (metadata_store.dacp_server_active != 0) || | |
507 | (metadata_store.advanced_dacp_server_active != 0); | |
508 | metadata_store.dacp_server_active = 0; | |
509 | metadata_store.advanced_dacp_server_active = 0; | |
77a5cf1e | 510 | /* |
54d761ff | 511 | debug(3, |
85b17cda MB |
512 | "setting metadata_store.dacp_server_active and " |
513 | "metadata_store.advanced_dacp_server_active to 0 with an update " | |
86b6d7bf | 514 | "flag value of %d", |
fb3f9f51 | 515 | ch); |
77a5cf1e | 516 | */ |
fb3f9f51 | 517 | metadata_hub_modify_epilog(ch); |
a6acb606 | 518 | |
54d761ff MB |
519 | uint64_t time_to_wait_for_wakeup_ns = |
520 | (uint64_t)(1000000000 * config.missing_port_dacp_scan_interval_seconds); | |
a6acb606 MB |
521 | |
522 | #ifdef COMPILE_FOR_LINUX_AND_FREEBSD_AND_CYGWIN_AND_OPENBSD | |
56bef8e7 | 523 | uint64_t time_of_wakeup_ns = get_realtime_in_ns() + time_to_wait_for_wakeup_ns; |
a6acb606 MB |
524 | uint64_t sec = time_of_wakeup_ns / 1000000000; |
525 | uint64_t nsec = time_of_wakeup_ns % 1000000000; | |
526 | #endif | |
527 | #ifdef COMPILE_FOR_OSX | |
528 | uint64_t sec = time_to_wait_for_wakeup_ns / 1000000000; | |
529 | uint64_t nsec = time_to_wait_for_wakeup_ns % 1000000000; | |
530 | #endif | |
531 | ||
532 | struct timespec time_to_wait; | |
533 | time_to_wait.tv_sec = sec; | |
534 | time_to_wait.tv_nsec = nsec; | |
535 | ||
536 | while ((dacp_server.scan_enable == 0) && (result != ETIMEDOUT)) { | |
537 | // debug(1, "dacp_monitor_thread_code wait for an event to possibly enable scan"); | |
538 | ||
539 | #ifdef COMPILE_FOR_LINUX_AND_FREEBSD_AND_CYGWIN_AND_OPENBSD | |
540 | result = pthread_cond_timedwait(&dacp_server_information_cv, &dacp_server_information_lock, | |
541 | &time_to_wait); // this is a pthread cancellation point | |
54d761ff MB |
542 | // debug(1, "result is %d, dacp_server.scan_enable is %d, time_to_wait_for_wakeup_ns is %" |
543 | // PRId64 ".", result, dacp_server.scan_enable, time_to_wait_for_wakeup_ns); | |
a6acb606 MB |
544 | |
545 | #endif | |
546 | #ifdef COMPILE_FOR_OSX | |
54d761ff MB |
547 | result = pthread_cond_timedwait_relative_np(&dacp_server_information_cv, |
548 | &dacp_server_information_lock, &time_to_wait); | |
a6acb606 MB |
549 | #endif |
550 | } | |
551 | if (dacp_server.scan_enable == 1) { | |
552 | bad_result_count = 0; | |
553 | idle_scan_count = 0; | |
fb3f9f51 | 554 | } |
88c55066 | 555 | } |
85b17cda | 556 | |
54d761ff MB |
557 | always_use_revision_number_1 = |
558 | dacp_server.always_use_revision_number_1; // set this while access is locked | |
27e42a66 | 559 | |
cbbe84d8 | 560 | result = dacp_get_volume(&the_volume); // just want the http code |
daab7a63 | 561 | pthread_cleanup_pop(1); |
4aab0a6f | 562 | |
54d761ff | 563 | if (result == 490) { // 490 means no port was specified |
ea4eafbf | 564 | if (strlen(dacp_server.dacp_id) != 0) { |
a6acb606 MB |
565 | // debug(1,"mdns_dacp_monitor_set_id"); |
566 | mdns_dacp_monitor_set_id(dacp_server.dacp_id); | |
567 | } | |
daab7a63 | 568 | } else { |
a6acb606 MB |
569 | scan_index++; |
570 | // debug(1,"DACP Scan Result: %d.", result); | |
85b17cda | 571 | |
a6acb606 MB |
572 | if ((result == 200) || (result == 400)) { |
573 | bad_result_count = 0; | |
574 | } else { | |
575 | if (bad_result_count < config.scan_max_bad_response_count) // limit to some reasonable value | |
576 | bad_result_count++; | |
577 | } | |
85b17cda | 578 | |
a6acb606 MB |
579 | // here, do the debouncing calculations to see |
580 | // if the dacp server and the | |
581 | // advanced dacp server are available | |
582 | ||
583 | // -1 means we don't know because some bad statuses have been reported | |
584 | // 0 means definitely no | |
585 | // +1 means definitely yes | |
586 | ||
587 | int dacp_server_status_now = -1; | |
588 | int advanced_dacp_server_status_now = -1; | |
589 | ||
590 | if (bad_result_count == 0) { | |
591 | dacp_server_status_now = 1; | |
592 | if (result == 200) | |
593 | advanced_dacp_server_status_now = 1; | |
594 | else if (result == 400) | |
595 | advanced_dacp_server_status_now = 0; | |
596 | } else if (bad_result_count == | |
597 | config.scan_max_bad_response_count) { // if a sequence of bad return codes occurs, | |
598 | // then it's gone | |
599 | dacp_server_status_now = 0; | |
2b980ff2 | 600 | advanced_dacp_server_status_now = 0; |
a6acb606 | 601 | } |
4aab0a6f | 602 | |
a6acb606 MB |
603 | if (metadata_store.player_thread_active == 0) |
604 | idle_scan_count++; | |
605 | else | |
606 | idle_scan_count = 0; | |
f9845d01 | 607 | |
54d761ff MB |
608 | debug(3, "Scan Result: %d, Bad Scan Count: %d, Idle Scan Count: %d.", result, |
609 | bad_result_count, idle_scan_count); | |
4aab0a6f | 610 | |
a6acb606 MB |
611 | /* not used |
612 | // decide if idle for too long | |
613 | if (idle_scan_count == config.scan_max_inactive_count) { | |
614 | debug(2, "DACP server status scanning stopped."); | |
615 | dacp_server.scan_enable = 0; | |
616 | } | |
617 | */ | |
85b17cda | 618 | |
a6acb606 MB |
619 | int update_needed = 0; |
620 | metadata_hub_modify_prolog(); | |
621 | if (dacp_server_status_now != -1) { // if dacp_server_status_now is actually known... | |
622 | if (metadata_store.dacp_server_active != dacp_server_status_now) { | |
623 | debug(2, "metadata_store.dacp_server_active set to %d.", dacp_server_status_now); | |
624 | metadata_store.dacp_server_active = dacp_server_status_now; | |
625 | update_needed = 1; | |
626 | } | |
daab7a63 | 627 | } |
a6acb606 MB |
628 | if (advanced_dacp_server_status_now != |
629 | -1) { // if advanced_dacp_server_status_now is actually known... | |
630 | if (metadata_store.advanced_dacp_server_active != advanced_dacp_server_status_now) { | |
631 | debug(2, "metadata_store.advanced_dacp_server_active set to %d.", dacp_server_status_now); | |
632 | metadata_store.advanced_dacp_server_active = advanced_dacp_server_status_now; | |
633 | update_needed = 1; | |
634 | } | |
daab7a63 | 635 | } |
f1d45034 | 636 | |
a6acb606 | 637 | metadata_hub_modify_epilog(update_needed); |
85b17cda | 638 | |
a6acb606 MB |
639 | // pthread_mutex_unlock(&dacp_server_information_lock); |
640 | // debug(1, "DACP Server ID \"%u\" at \"%s:%u\", scan %d.", dacp_server.active_remote_id, | |
641 | // dacp_server.ip_string, dacp_server.port, scan_index); | |
6dc30c6c | 642 | |
cbbe84d8 | 643 | if (result == 200) { |
a6acb606 MB |
644 | metadata_hub_modify_prolog(); |
645 | int diff = metadata_store.speaker_volume != the_volume; | |
646 | if (diff) | |
647 | metadata_store.speaker_volume = the_volume; | |
648 | metadata_hub_modify_epilog(diff); | |
649 | ||
650 | ssize_t le; | |
651 | char *response = NULL; | |
652 | int32_t item_size; | |
653 | char command[1024] = ""; | |
6acd73da | 654 | if (always_use_revision_number_1 != 0) // see the "long polling" note above |
a6acb606 MB |
655 | revision_number = 1; |
656 | snprintf(command, sizeof(command) - 1, "playstatusupdate?revision-number=%d", | |
657 | revision_number); | |
658 | // debug(1,"dacp_monitor_thread_code: command: \"%s\"",command); | |
659 | result = dacp_send_command(command, &response, &le); | |
660 | // debug(1,"Response to \"%s\" is %d.",command,result); | |
661 | // remember: unless the revision_number you pass in is 1, | |
662 | // response will be 200 only if there's something new to report. | |
663 | if (result == 200) { | |
664 | // if (0) { | |
665 | char *sp = response; | |
666 | if (le >= 8) { | |
667 | // here start looking for the contents of the status update | |
668 | if (dacp_tlv_crawl(&sp, &item_size) == 'cmst') { // status | |
669 | // here, we know that we are receiving playerstatusupdates, so set a flag | |
670 | metadata_hub_modify_prolog(); | |
671 | // debug(1, "playstatusupdate release track metadata"); | |
672 | // metadata_hub_reset_track_metadata(); | |
673 | // metadata_store.playerstatusupdates_are_received = 1; | |
674 | sp -= item_size; // drop down into the array -- don't skip over it | |
675 | le -= 8; | |
676 | // char typestring[5]; | |
677 | // we need to acquire the metadata data structure and possibly update it | |
678 | while (le >= 8) { | |
679 | uint32_t type = dacp_tlv_crawl(&sp, &item_size); | |
680 | le -= item_size + 8; | |
681 | char *t; | |
682 | // char u; | |
683 | // char *st; | |
684 | int32_t r; | |
685 | uint32_t ui; | |
686 | // uint64_t v; | |
687 | // int i; | |
688 | ||
689 | switch (type) { | |
690 | case 'cmsr': // revision number | |
691 | t = sp - item_size; | |
692 | revision_number = ntohl(*(uint32_t *)(t)); | |
693 | // debug(1,"New revision number received: %d", revision_number); | |
3a4e29c5 | 694 | break; |
a6acb606 MB |
695 | case 'caps': // play status |
696 | t = sp - item_size; | |
697 | r = *(unsigned char *)(t); | |
698 | switch (r) { | |
699 | case 2: | |
700 | if (metadata_store.play_status != PS_STOPPED) { | |
701 | metadata_store.play_status = PS_STOPPED; | |
702 | debug(2, "Play status is \"stopped\"."); | |
703 | } | |
704 | break; | |
705 | case 3: | |
706 | if (metadata_store.play_status != PS_PAUSED) { | |
707 | metadata_store.play_status = PS_PAUSED; | |
708 | debug(2, "Play status is \"paused\"."); | |
709 | } | |
710 | break; | |
711 | case 4: | |
712 | if (metadata_store.play_status != PS_PLAYING) { | |
713 | metadata_store.play_status = PS_PLAYING; | |
714 | debug(2, "Play status changed to \"playing\"."); | |
715 | } | |
716 | break; | |
717 | default: | |
718 | debug(1, "Unrecognised play status %d received.", r); | |
719 | break; | |
3a4e29c5 MB |
720 | } |
721 | break; | |
a6acb606 MB |
722 | case 'cash': // shuffle status |
723 | t = sp - item_size; | |
724 | r = *(unsigned char *)(t); | |
725 | switch (r) { | |
726 | case 0: | |
727 | if (metadata_store.shuffle_status != SS_OFF) { | |
728 | metadata_store.shuffle_status = SS_OFF; | |
729 | debug(2, "Shuffle status is \"off\"."); | |
730 | } | |
731 | break; | |
732 | case 1: | |
733 | if (metadata_store.shuffle_status != SS_ON) { | |
734 | metadata_store.shuffle_status = SS_ON; | |
735 | debug(2, "Shuffle status is \"on\"."); | |
736 | } | |
737 | break; | |
738 | default: | |
739 | debug(1, "Unrecognised shuffle status %d received.", r); | |
740 | break; | |
3a4e29c5 MB |
741 | } |
742 | break; | |
a6acb606 MB |
743 | case 'carp': // repeat status |
744 | t = sp - item_size; | |
745 | r = *(unsigned char *)(t); | |
746 | switch (r) { | |
747 | case 0: | |
748 | if (metadata_store.repeat_status != RS_OFF) { | |
749 | metadata_store.repeat_status = RS_OFF; | |
750 | debug(2, "Repeat status is \"none\"."); | |
751 | } | |
752 | break; | |
753 | case 1: | |
754 | if (metadata_store.repeat_status != RS_ONE) { | |
755 | metadata_store.repeat_status = RS_ONE; | |
756 | debug(2, "Repeat status is \"one\"."); | |
757 | } | |
758 | break; | |
759 | case 2: | |
760 | if (metadata_store.repeat_status != RS_ALL) { | |
761 | metadata_store.repeat_status = RS_ALL; | |
762 | debug(2, "Repeat status is \"all\"."); | |
763 | } | |
764 | break; | |
765 | default: | |
766 | debug(1, "Unrecognised repeat status %d received.", r); | |
767 | break; | |
768 | } | |
3a4e29c5 | 769 | break; |
a6acb606 MB |
770 | case 'cann': // track name |
771 | debug(2, "DACP Track Name seen"); | |
772 | if (string_update_with_size(&metadata_store.track_name, | |
773 | &metadata_store.track_name_changed, sp - item_size, | |
774 | item_size)) { | |
775 | debug(2, "DACP Track Name set to: \"%s\"", metadata_store.track_name); | |
4aab0a6f | 776 | } |
3a4e29c5 | 777 | break; |
a6acb606 MB |
778 | case 'cana': // artist name |
779 | debug(2, "DACP Artist Name seen"); | |
780 | if (string_update_with_size(&metadata_store.artist_name, | |
781 | &metadata_store.artist_name_changed, sp - item_size, | |
782 | item_size)) { | |
783 | debug(2, "DACP Artist Name set to: \"%s\"", metadata_store.artist_name); | |
3a4e29c5 MB |
784 | } |
785 | break; | |
a6acb606 MB |
786 | case 'canl': // album name |
787 | debug(2, "DACP Album Name seen"); | |
788 | if (string_update_with_size(&metadata_store.album_name, | |
789 | &metadata_store.album_name_changed, sp - item_size, | |
790 | item_size)) { | |
791 | debug(2, "DACP Album Name set to: \"%s\"", metadata_store.album_name); | |
792 | } | |
85b17cda | 793 | break; |
a6acb606 MB |
794 | case 'cang': // genre |
795 | debug(2, "DACP Genre seen"); | |
796 | if (string_update_with_size(&metadata_store.genre, &metadata_store.genre_changed, | |
797 | sp - item_size, item_size)) { | |
798 | debug(2, "DACP Genre set to: \"%s\"", metadata_store.genre); | |
3a4e29c5 MB |
799 | } |
800 | break; | |
a6acb606 MB |
801 | case 'canp': // nowplaying 4 ids: dbid, plid, playlistItem, itemid (from mellowware |
802 | // see reference above) | |
803 | debug(2, "DACP Composite ID seen"); | |
e8b8c6ed MB |
804 | if ((metadata_store.item_composite_id_is_valid == 0) || |
805 | (memcmp(metadata_store.item_composite_id, sp - item_size, | |
806 | sizeof(metadata_store.item_composite_id)) != 0)) { | |
a6acb606 MB |
807 | memcpy(metadata_store.item_composite_id, sp - item_size, |
808 | sizeof(metadata_store.item_composite_id)); | |
809 | char st[33]; | |
810 | char *pt = st; | |
811 | int it; | |
812 | for (it = 0; it < 16; it++) { | |
813 | snprintf(pt, 3, "%02X", metadata_store.item_composite_id[it]); | |
814 | pt += 2; | |
815 | } | |
816 | *pt = 0; | |
817 | debug(2, "Item composite ID changed to 0x%s.", st); | |
818 | metadata_store.item_composite_id_changed = 1; | |
d67909b8 | 819 | metadata_store.item_composite_id_is_valid = 1; |
3a4e29c5 MB |
820 | } |
821 | break; | |
a6acb606 MB |
822 | case 'astm': |
823 | t = sp - item_size; | |
824 | ui = ntohl(*(uint32_t *)(t)); | |
825 | debug(2, "DACP Song Time seen: \"%u\" of length %u.", ui, item_size); | |
826 | if (ui != metadata_store.songtime_in_milliseconds) { | |
827 | metadata_store.songtime_in_milliseconds = ui; | |
828 | metadata_store.songtime_in_milliseconds_changed = 1; | |
d67909b8 | 829 | metadata_store.songtime_in_milliseconds_is_valid = 1; |
a6acb606 MB |
830 | debug(2, "DACP Song Time set to: \"%u\"", |
831 | metadata_store.songtime_in_milliseconds); | |
86b6d7bf | 832 | } |
3a4e29c5 | 833 | break; |
a6acb606 MB |
834 | |
835 | /* | |
836 | case 'mstt': | |
837 | case 'cant': | |
838 | case 'cast': | |
839 | case 'cmmk': | |
840 | case 'caas': | |
841 | case 'caar': | |
842 | t = sp - item_size; | |
843 | r = ntohl(*(uint32_t *)(t)); | |
844 | printf(" %d", r); | |
845 | printf(" (0x"); | |
846 | t = sp - item_size; | |
847 | for (i = 0; i < item_size; i++) { | |
848 | printf("%02x", *t & 0xff); | |
849 | t++; | |
850 | } | |
851 | printf(")"); | |
852 | break; | |
853 | case 'asai': | |
854 | t = sp - item_size; | |
855 | s = ntohl(*(uint32_t *)(t)); | |
856 | s = s << 32; | |
857 | t += 4; | |
858 | v = (ntohl(*(uint32_t *)(t))) & 0xffffffff; | |
859 | s += v; | |
860 | printf(" %lu", s); | |
861 | printf(" (0x"); | |
862 | t = sp - item_size; | |
863 | for (i = 0; i < item_size; i++) { | |
864 | printf("%02x", *t & 0xff); | |
865 | t++; | |
866 | } | |
867 | printf(")"); | |
868 | break; | |
869 | */ | |
4aab0a6f | 870 | default: |
a6acb606 MB |
871 | /* |
872 | printf(" 0x"); | |
873 | t = sp - item_size; | |
874 | for (i = 0; i < item_size; i++) { | |
875 | printf("%02x", *t & 0xff); | |
876 | t++; | |
877 | } | |
878 | */ | |
4aab0a6f MB |
879 | break; |
880 | } | |
a6acb606 | 881 | // printf("\n"); |
3a4e29c5 | 882 | } |
85b17cda | 883 | |
a6acb606 MB |
884 | // finished possibly writing to the metadata hub |
885 | metadata_hub_modify_epilog( | |
886 | 1); // should really see if this can be made responsive to changes | |
887 | } else { | |
888 | debug(1, "Status Update not found.\n"); | |
889 | } | |
3a4e29c5 | 890 | } else { |
a6acb606 | 891 | debug(1, "Can't find any content in playerstatusupdate request"); |
3a4e29c5 | 892 | } |
a6acb606 MB |
893 | } /* else { |
894 | if (result != 403) | |
895 | debug(1, "Unexpected response %d to playerstatusupdate request", result); | |
896 | } */ | |
897 | if (response) { | |
898 | free(response); | |
899 | response = NULL; | |
900 | }; | |
901 | }; | |
902 | /* | |
903 | strcpy(command,"nowplayingartwork?mw=320&mh=320"); | |
904 | debug(1,"Command: \"%s\", result is %d",command, dacp_send_command(command, &response, &le)); | |
4aab0a6f MB |
905 | if (response) { |
906 | free(response); | |
907 | response = NULL; | |
a6acb606 MB |
908 | } |
909 | strcpy(command,"getproperty?properties=dmcp.volume"); | |
910 | debug(1,"Command: \"%s\", result is %d",command, dacp_send_command(command, &response, &le)); | |
911 | if (response) { | |
912 | free(response); | |
913 | response = NULL; | |
914 | } | |
915 | strcpy(command,"setproperty?dmcp.volume=100.000000"); | |
916 | debug(1,"Command: \"%s\", result is %d",command, dacp_send_command(command, &response, &le)); | |
917 | if (response) { | |
918 | free(response); | |
919 | response = NULL; | |
920 | } | |
921 | */ | |
922 | if (metadata_store.player_thread_active) | |
923 | sleep(config.scan_interval_when_active); | |
924 | else | |
925 | sleep(config.scan_interval_when_inactive); | |
85b17cda | 926 | } |
fe3b70b4 | 927 | } |
f1d45034 | 928 | debug(1, "DACP monitor thread exiting -- should never happen."); |
fe3b70b4 MB |
929 | pthread_exit(NULL); |
930 | } | |
931 | ||
932 | void dacp_monitor_start() { | |
7fb89135 | 933 | int rc; |
a6acb606 | 934 | |
b3cec5c6 | 935 | rc = pthread_cond_init(&dacp_server_information_cv, NULL); |
a6acb606 MB |
936 | if (rc) |
937 | debug(1, "Error initialising the DACP Server Information Condition Variable"); | |
a6acb606 | 938 | |
7fb89135 MB |
939 | pthread_mutexattr_t mta; |
940 | ||
941 | rc = pthread_mutexattr_init(&mta); | |
942 | if (rc) | |
943 | debug(1, "Error creating the DACP Conversation Lock Mutex Att Init"); | |
944 | ||
945 | rc = pthread_mutexattr_settype(&mta, PTHREAD_MUTEX_ERRORCHECK); | |
946 | if (rc) | |
947 | debug(1, "Error creating the DACP Conversation Lock Mutex Errorcheck"); | |
948 | ||
949 | // rc = pthread_mutexattr_setname_np(&mta, "DACP Conversation Lock"); | |
950 | // if (rc) | |
951 | // debug(1,"Error creating the DACP Conversation Lock Mutex Set Name"); | |
952 | ||
953 | rc = pthread_mutex_init(&dacp_conversation_lock, &mta); | |
954 | if (rc) | |
955 | debug(1, "Error creating the DACP Conversation Lock Mutex Init"); | |
f6d6700d MB |
956 | // else |
957 | // debug(1, "DACP Conversation Lock Mutex Init"); | |
7fb89135 MB |
958 | |
959 | rc = pthread_mutexattr_destroy(&mta); | |
960 | if (rc) | |
961 | debug(1, "Error creating the DACP Conversation Lock Attr Destroy"); | |
962 | ||
963 | rc = pthread_mutexattr_init(&mta); | |
964 | if (rc) | |
965 | debug(1, "Error creating the DACP Server Information Lock Mutex Att Init"); | |
966 | ||
967 | rc = pthread_mutexattr_settype(&mta, PTHREAD_MUTEX_ERRORCHECK); | |
968 | if (rc) | |
969 | debug(1, "Error creating the DACP Server Information Lock Mutex Errorcheck"); | |
970 | ||
971 | // rc = pthread_mutexattr_setname_np(&mta, "DACP Conversation Lock"); | |
972 | // if (rc) | |
973 | // debug(1,"Error creating the DACP Server Information Lock Mutex Set Name"); | |
974 | ||
975 | rc = pthread_mutex_init(&dacp_server_information_lock, &mta); | |
976 | if (rc) | |
977 | debug(1, "Error creating the DACP Server Information Lock Mutex Init"); | |
978 | ||
979 | rc = pthread_mutexattr_destroy(&mta); | |
980 | if (rc) | |
981 | debug(1, "Error creating the DACP Server Information Lock Attr Destroy"); | |
982 | ||
0f68ff0c | 983 | memset(&dacp_server, 0, sizeof(dacp_server_record)); |
72079ada | 984 | |
fe3b70b4 | 985 | pthread_create(&dacp_monitor_thread, NULL, dacp_monitor_thread_code, NULL); |
79e22063 | 986 | dacp_monitor_initialised = 1; |
fe3b70b4 MB |
987 | } |
988 | ||
f1d45034 | 989 | void dacp_monitor_stop() { |
79e22063 | 990 | if (dacp_monitor_initialised) { // only if it's been started and initialised |
405a028f | 991 | debug(2, "dacp_monitor_stop"); |
79e22063 MB |
992 | pthread_cancel(dacp_monitor_thread); |
993 | pthread_join(dacp_monitor_thread, NULL); | |
994 | pthread_mutex_destroy(&dacp_server_information_lock); | |
405a028f | 995 | debug(3, "DACP Conversation Lock Mutex Destroyed"); |
79e22063 | 996 | pthread_mutex_destroy(&dacp_conversation_lock); |
a6acb606 | 997 | pthread_cond_destroy(&dacp_server_information_cv); |
4e1f74db | 998 | debug(3, "DACP Server Information Condition Variable destroyed."); |
6857d06b | 999 | if (dacp_server.active_remote_id) { |
3fc95704 MB |
1000 | free(dacp_server.active_remote_id); |
1001 | dacp_server.active_remote_id = NULL; | |
6857d06b | 1002 | } |
79e22063 | 1003 | } |
f1d45034 MB |
1004 | } |
1005 | ||
3198ec42 MB |
1006 | uint32_t dacp_tlv_crawl(char **p, int32_t *length) { |
1007 | char typecode[5]; | |
1008 | memcpy(typecode, *p, 4); | |
1009 | typecode[4] = '\0'; | |
1010 | uint32_t type = ntohl(*(uint32_t *)*p); | |
1011 | *p += 4; | |
419fe95a | 1012 | *length = ntohl(*(uint32_t *)*p); |
3198ec42 MB |
1013 | *p += 4 + *length; |
1014 | // debug(1,"Type seen: '%s' of length %d",typecode,*length); | |
1015 | return type; | |
1016 | } | |
1017 | ||
3a4e29c5 | 1018 | int dacp_get_client_volume(int32_t *result) { |
07e46565 | 1019 | // debug(1,"dacp_get_client_volume"); |
1637a79d | 1020 | char *server_reply = NULL; |
3198ec42 | 1021 | int32_t overall_volume = -1; |
1637a79d | 1022 | ssize_t reply_size; |
daab7a63 | 1023 | // debug(1,"dacp_get_client_volume: dacp_send_command"); |
1637a79d MB |
1024 | int response = |
1025 | dacp_send_command("getproperty?properties=dmcp.volume", &server_reply, &reply_size); | |
1026 | if (response == 200) { // if we get an okay | |
3e2c3e83 MB |
1027 | char *sp = server_reply; |
1028 | int32_t item_size; | |
1029 | if (reply_size >= 8) { | |
1030 | if (dacp_tlv_crawl(&sp, &item_size) == 'cmgt') { | |
3e2c3e83 MB |
1031 | sp -= item_size; // drop down into the array -- don't skip over it |
1032 | reply_size -= 8; | |
1033 | while (reply_size >= 8) { | |
1034 | uint32_t type = dacp_tlv_crawl(&sp, &item_size); | |
1035 | reply_size -= item_size + 8; | |
1036 | if (type == 'cmvo') { // drop down into the dictionary -- don't skip over it | |
1037 | char *t = sp - item_size; | |
419fe95a | 1038 | overall_volume = ntohl(*(uint32_t *)(t)); |
3e2c3e83 MB |
1039 | } |
1040 | } | |
1041 | } else { | |
e0aa75a8 | 1042 | debug(1, "Unexpected payload response from getproperty?properties=dmcp.volume"); |
3e2c3e83 MB |
1043 | } |
1044 | } else { | |
e0aa75a8 | 1045 | debug(1, "Too short a response from getproperty?properties=dmcp.volume"); |
3e2c3e83 | 1046 | } |
fa6cafd0 | 1047 | // debug(1, "Overall Volume is %d.", overall_volume); |
668f7806 | 1048 | } |
ad6c721d | 1049 | |
668f7806 MB |
1050 | if (server_reply) { |
1051 | // debug(1, "Freeing response memory."); | |
1637a79d | 1052 | free(server_reply); |
668f7806 MB |
1053 | server_reply = NULL; |
1054 | } | |
1055 | ||
73bf006c | 1056 | if (result) { |
c36a7822 | 1057 | *result = overall_volume; |
07e46565 | 1058 | // debug(1,"dacp_get_client_volume returns: %" PRId32 ".",overall_volume); |
73bf006c | 1059 | } |
3a4e29c5 | 1060 | return response; |
3198ec42 MB |
1061 | } |
1062 | ||
3e2c3e83 | 1063 | int dacp_set_include_speaker_volume(int64_t machine_number, int32_t vo) { |
39f159bf | 1064 | debug(2, "dacp_set_include_speaker_volume to %" PRId32 ".", vo); |
3198ec42 MB |
1065 | char message[1000]; |
1066 | memset(message, 0, sizeof(message)); | |
7fb89135 MB |
1067 | snprintf(message, sizeof(message), |
1068 | "setproperty?include-speaker-id=%" PRId64 "&dmcp.volume=%" PRId32 "", machine_number, | |
1069 | vo); | |
27e42a66 | 1070 | debug(2, "sending \"%s\"", message); |
1637a79d MB |
1071 | return send_simple_dacp_command(message); |
1072 | // should return 204 | |
3198ec42 MB |
1073 | } |
1074 | ||
3e2c3e83 | 1075 | int dacp_set_speaker_volume(int64_t machine_number, int32_t vo) { |
3198ec42 MB |
1076 | char message[1000]; |
1077 | memset(message, 0, sizeof(message)); | |
7fb89135 MB |
1078 | snprintf(message, sizeof(message), "setproperty?speaker-id=%" PRId64 "&dmcp.volume=%" PRId32 "", |
1079 | machine_number, vo); | |
27e42a66 | 1080 | debug(2, "sending \"%s\"", message); |
1637a79d MB |
1081 | return send_simple_dacp_command(message); |
1082 | // should return 204 | |
3198ec42 MB |
1083 | } |
1084 | ||
c36a7822 MB |
1085 | int dacp_get_speaker_list(dacp_spkr_stuff *speaker_info, int max_size_of_array, |
1086 | int *actual_speaker_count) { | |
73bf006c | 1087 | // char typestring[5]; |
1637a79d | 1088 | char *server_reply = NULL; |
3198ec42 | 1089 | int speaker_index = -1; // will be incremented before use |
c36a7822 | 1090 | int speaker_count = -1; // will be fixed if there is no problem |
1637a79d MB |
1091 | ssize_t le; |
1092 | ||
daab7a63 | 1093 | // debug(1,"dacp_speaker_list: dacp_send_command"); |
1637a79d MB |
1094 | int response = dacp_send_command("getspeakers", &server_reply, &le); |
1095 | if (response == 200) { | |
1096 | char *sp = server_reply; | |
1097 | int32_t item_size; | |
1098 | if (le >= 8) { | |
1099 | if (dacp_tlv_crawl(&sp, &item_size) == 'casp') { | |
1100 | // debug(1,"Speakers:",item_size); | |
1101 | sp -= item_size; // drop down into the array -- don't skip over it | |
1102 | le -= 8; | |
1103 | while (le >= 8) { | |
1104 | uint32_t type = dacp_tlv_crawl(&sp, &item_size); | |
1105 | if (type == 'mdcl') { // drop down into the dictionary -- don't skip over it | |
1106 | // debug(1,">>>> Dictionary:"); | |
1107 | sp -= item_size; | |
1108 | le -= 8; | |
1109 | speaker_index++; | |
5f750ba9 | 1110 | if (speaker_index == max_size_of_array) { |
c36a7822 | 1111 | return 413; // Payload Too Large -- too many speakers |
5f750ba9 | 1112 | } |
1637a79d MB |
1113 | speaker_info[speaker_index].active = 0; |
1114 | speaker_info[speaker_index].speaker_number = 0; | |
1115 | speaker_info[speaker_index].volume = 0; | |
5f750ba9 | 1116 | speaker_info[speaker_index].name[0] = '\0'; |
1637a79d MB |
1117 | } else { |
1118 | le -= item_size + 8; | |
1119 | char *t; | |
73bf006c | 1120 | // char u; |
1637a79d MB |
1121 | int32_t r; |
1122 | int64_t s, v; | |
1123 | switch (type) { | |
1124 | case 'minm': | |
1125 | t = sp - item_size; | |
5f750ba9 MB |
1126 | strncpy((char *)&speaker_info[speaker_index].name, t, |
1127 | sizeof(speaker_info[speaker_index].name)); | |
1128 | speaker_info[speaker_index].name[sizeof(speaker_info[speaker_index].name) - 1] = | |
1129 | '\0'; // just in case | |
1637a79d | 1130 | break; |
1637a79d MB |
1131 | case 'cmvo': |
1132 | t = sp - item_size; | |
419fe95a | 1133 | r = ntohl(*(uint32_t *)(t)); |
1637a79d | 1134 | speaker_info[speaker_index].volume = r; |
c36a7822 MB |
1135 | // debug(1,"The individual volume of speaker \"%s\" is |
1136 | // \"%d\".",speaker_info[speaker_index].name,r); | |
1637a79d MB |
1137 | break; |
1138 | case 'msma': | |
1139 | t = sp - item_size; | |
1140 | s = ntohl(*(uint32_t *)(t)); | |
1141 | s = s << 32; | |
1142 | t += 4; | |
1143 | v = (ntohl(*(uint32_t *)(t))) & 0xffffffff; | |
1144 | s += v; | |
1145 | speaker_info[speaker_index].speaker_number = s; | |
1146 | // debug(1,"Speaker machine number: %ld",s); | |
1147 | break; | |
1148 | ||
1149 | case 'caia': | |
1150 | speaker_info[speaker_index].active = 1; | |
1151 | break; | |
1152 | /* | |
1153 | case 'caip': | |
1154 | case 'cavd': | |
1155 | case 'caiv': | |
3a4e29c5 | 1156 | case 'cads': |
c36a7822 | 1157 | |
3a4e29c5 MB |
1158 | *(uint32_t *)typestring = htonl(type); |
1159 | typestring[4] = 0; | |
1160 | ||
c36a7822 MB |
1161 | |
1162 | ||
1637a79d MB |
1163 | t = sp-item_size; |
1164 | u = *t; | |
3a4e29c5 | 1165 | debug(1,"Type: '%s' Value: \"%d\".",typestring,u); |
1637a79d MB |
1166 | break; |
1167 | */ | |
1168 | default: | |
1169 | break; | |
3198ec42 MB |
1170 | } |
1171 | } | |
3198ec42 | 1172 | } |
1637a79d | 1173 | // debug(1,"Total of %d speakers found. Here are the active ones:",speaker_index+1); |
3a4e29c5 | 1174 | speaker_count = speaker_index + 1; // number of speaker entries in the array |
3198ec42 | 1175 | } else { |
1637a79d | 1176 | debug(1, "Speaker array not found."); |
3198ec42 | 1177 | } |
1637a79d MB |
1178 | /* |
1179 | int i; | |
1180 | for (i=0;i<le;i++) { | |
1181 | if (*sp < ' ') | |
1182 | debug(1,"%d %02x", i, *sp); | |
1183 | else | |
1184 | debug(1,"%d %02x '%c'", i, *sp,*sp); | |
1185 | sp++; | |
1186 | } | |
1187 | */ | |
3198ec42 | 1188 | } else { |
1637a79d | 1189 | debug(1, "Can't find any content in dacp speakers request"); |
3198ec42 | 1190 | } |
1637a79d MB |
1191 | free(server_reply); |
1192 | server_reply = NULL; | |
3198ec42 | 1193 | } else { |
668f7806 MB |
1194 | // debug(1, "Unexpected response %d to dacp speakers request", response); |
1195 | if (server_reply) { | |
1196 | debug(1, "Freeing response memory."); | |
1197 | free(server_reply); | |
1198 | server_reply = NULL; | |
1199 | } | |
3198ec42 | 1200 | } |
3a4e29c5 MB |
1201 | if (actual_speaker_count) |
1202 | *actual_speaker_count = speaker_count; | |
1203 | return response; | |
3198ec42 | 1204 | } |
3e2c3e83 | 1205 | |
3a4e29c5 | 1206 | int dacp_get_volume(int32_t *the_actual_volume) { |
3e2c3e83 MB |
1207 | // get the speaker volume information from the DACP source and store it in the metadata_hub |
1208 | // A volume command has been sent from the client | |
1209 | // let's get the master volume from the DACP remote control | |
1210 | struct dacp_speaker_stuff speaker_info[50]; | |
1211 | // we need the overall volume and the speakers information to get this device's relative volume to | |
1212 | // calculate the real volume | |
1213 | ||
3a4e29c5 MB |
1214 | int32_t overall_volume = 0; |
1215 | int32_t actual_volume = 0; | |
1216 | int http_response = dacp_get_client_volume(&overall_volume); | |
c36a7822 | 1217 | if (http_response == 200) { |
07e46565 | 1218 | // debug(1,"Overall volume is: %u.",overall_volume); |
3a4e29c5 | 1219 | int speaker_count = 0; |
c36a7822 MB |
1220 | http_response = dacp_get_speaker_list((dacp_spkr_stuff *)&speaker_info, 50, &speaker_count); |
1221 | if (http_response == 200) { | |
3a4e29c5 | 1222 | // get our machine number |
03d291f4 MB |
1223 | uint16_t *hn = (uint16_t *)config.ap1_prefix; |
1224 | uint32_t *ln = (uint32_t *)(config.ap1_prefix + 2); | |
3a4e29c5 MB |
1225 | uint64_t t1 = ntohs(*hn); |
1226 | uint64_t t2 = ntohl(*ln); | |
1227 | int64_t machine_number = (t1 << 32) + t2; // this form is useful | |
1228 | ||
1229 | // Let's find our own speaker in the array and pick up its relative volume | |
1230 | int i; | |
1231 | int32_t relative_volume = 0; | |
1232 | for (i = 0; i < speaker_count; i++) { | |
1233 | if (speaker_info[i].speaker_number == machine_number) { | |
3a4e29c5 | 1234 | relative_volume = speaker_info[i].volume; |
07e46565 | 1235 | /* |
73bf006c | 1236 | debug(1,"Our speaker was found with a relative volume of: %u.",relative_volume); |
c36a7822 | 1237 | |
3a4e29c5 MB |
1238 | if (speaker_info[i].active) |
1239 | debug(1,"Our speaker is active."); | |
1240 | else | |
1241 | debug(1,"Our speaker is inactive."); | |
07e46565 | 1242 | */ |
3a4e29c5 MB |
1243 | } |
1244 | } | |
1245 | actual_volume = (overall_volume * relative_volume + 50) / 100; | |
c36a7822 MB |
1246 | // debug(1,"Overall volume: %d, relative volume: %d%, actual volume: |
1247 | // %d.",overall_volume,relative_volume,actual_volume); | |
3a4e29c5 | 1248 | // debug(1,"Our actual speaker volume is %d.",actual_volume); |
c36a7822 MB |
1249 | // metadata_hub_modify_prolog(); |
1250 | // metadata_store.speaker_volume = actual_volume; | |
1251 | // metadata_hub_modify_epilog(1); | |
3a4e29c5 | 1252 | } else { |
d3365f8b | 1253 | debug(2, "Unexpected return code %d from dacp_get_speaker_list.", http_response); |
3e2c3e83 | 1254 | } |
77a5cf1e | 1255 | } else if ((http_response != 400) && (http_response != 490)) { |
c02e53ea | 1256 | debug(3, "Unexpected return code %d from dacp_get_client_volume.", http_response); |
c36a7822 | 1257 | } |
73bf006c | 1258 | if (the_actual_volume) { |
07e46565 | 1259 | // debug(1,"dacp_get_volume returns %d.",actual_volume); |
3a4e29c5 | 1260 | *the_actual_volume = actual_volume; |
73bf006c MB |
1261 | } |
1262 | return http_response; | |
3e2c3e83 | 1263 | } |
39f159bf MB |
1264 | |
1265 | int dacp_set_volume(int32_t vo) { | |
1266 | int http_response = 492; // argument out of range | |
1267 | if ((vo >= 0) && (vo <= 100)) { | |
1268 | // get the information we need -- the absolute volume, the speaker list, our ID | |
1269 | struct dacp_speaker_stuff speaker_info[50]; | |
1270 | int32_t overall_volume; | |
1271 | http_response = dacp_get_client_volume(&overall_volume); | |
1272 | if (http_response == 200) { | |
1273 | int speaker_count; | |
1274 | http_response = dacp_get_speaker_list((dacp_spkr_stuff *)&speaker_info, 50, &speaker_count); | |
1275 | if (http_response == 200) { | |
1276 | // get our machine number | |
03d291f4 MB |
1277 | uint16_t *hn = (uint16_t *)config.ap1_prefix; |
1278 | uint32_t *ln = (uint32_t *)(config.ap1_prefix + 2); | |
39f159bf MB |
1279 | uint64_t t1 = ntohs(*hn); |
1280 | uint64_t t2 = ntohl(*ln); | |
1281 | int64_t machine_number = (t1 << 32) + t2; // this form is useful | |
1282 | ||
1283 | // Let's find our own speaker in the array and pick up its relative volume | |
1284 | int i; | |
1285 | int32_t active_speakers = 0; | |
1286 | for (i = 0; i < speaker_count; i++) { | |
1287 | if (speaker_info[i].speaker_number == machine_number) { | |
1288 | debug(2, "Our speaker number found: %ld with relative volume.", machine_number, | |
1289 | speaker_info[i].volume); | |
1290 | } | |
1291 | if (speaker_info[i].active == 1) { | |
1292 | active_speakers++; | |
1293 | } | |
1294 | } | |
1295 | ||
1296 | if (active_speakers == 1) { | |
1297 | // must be just this speaker | |
1298 | debug(2, "Remote-setting volume to %d on just one speaker.", vo); | |
1299 | http_response = dacp_set_include_speaker_volume(machine_number, vo); | |
1300 | } else if (active_speakers == 0) { | |
1301 | debug(2, "No speakers!"); | |
1302 | } else { | |
1303 | debug(2, "Speakers: %d, active: %d", speaker_count, active_speakers); | |
1304 | if (vo >= overall_volume) { | |
1305 | debug(2, "Multiple speakers active, but desired new volume is highest"); | |
1306 | http_response = dacp_set_include_speaker_volume(machine_number, vo); | |
1307 | } else { | |
1308 | // the desired volume is less than the current overall volume and there is more than | |
1309 | // one | |
1310 | // speaker | |
1311 | // we must find out the highest other speaker volume. | |
1312 | // If the desired volume is less than it, we must set the current_overall volume to | |
1313 | // that | |
1314 | // highest volume | |
1315 | // and set our volume relative to it. | |
1316 | // If the desired volume is greater than the highest current volume, then we can just | |
1317 | // go | |
1318 | // ahead | |
1319 | // with dacp_set_include_speaker_volume, setting the new current overall volume to the | |
1320 | // desired new level | |
1321 | // with the speaker at 100% | |
1322 | ||
1323 | int32_t highest_other_volume = 0; | |
1324 | for (i = 0; i < speaker_count; i++) { | |
1325 | if ((speaker_info[i].speaker_number != machine_number) && | |
1326 | (speaker_info[i].active == 1) && | |
1327 | (speaker_info[i].volume > highest_other_volume)) { | |
1328 | highest_other_volume = speaker_info[i].volume; | |
1329 | } | |
1330 | } | |
1331 | highest_other_volume = (highest_other_volume * overall_volume + 50) / 100; | |
1332 | if (highest_other_volume <= vo) { | |
1333 | debug(2, | |
1334 | "Highest other volume %d is less than or equal to the desired new volume %d.", | |
1335 | highest_other_volume, vo); | |
1336 | http_response = dacp_set_include_speaker_volume(machine_number, vo); | |
1337 | } else { | |
1338 | debug(2, "Highest other volume %d is greater than the desired new volume %d.", | |
1339 | highest_other_volume, vo); | |
1340 | // if the present overall volume is higher than the highest other volume at present, | |
1341 | // then bring it down to it. | |
1342 | if (overall_volume > highest_other_volume) { | |
1343 | debug(2, "Lower overall volume to new highest volume."); | |
1344 | http_response = dacp_set_include_speaker_volume( | |
1345 | machine_number, | |
1346 | highest_other_volume); // set the overall volume to the highest one | |
1347 | } | |
14bfba27 MB |
1348 | if (highest_other_volume != 0) { |
1349 | int32_t desired_relative_volume = | |
1350 | (vo * 100 + (highest_other_volume / 2)) / highest_other_volume; | |
1351 | debug(2, "Set our speaker volume relative to the highest volume."); | |
1352 | http_response = dacp_set_speaker_volume( | |
1353 | machine_number, | |
1354 | desired_relative_volume); // set the overall volume to the highest one | |
1355 | } | |
39f159bf MB |
1356 | } |
1357 | } | |
1358 | } | |
1359 | } else { | |
1360 | debug(2, "Can't get speakers list"); | |
1361 | } | |
1362 | } else { | |
1363 | debug(2, "Can't get client volume"); | |
1364 | } | |
1365 | ||
1366 | } else { | |
1367 | debug(2, "Invalid volume: %d -- ignored.", vo); | |
1368 | } | |
1369 | return http_response; | |
1370 | } |