1 /* Spa Bluez5 AVRCP Player
2 *
3 * Copyright © 2021 Pauli Virtanen
4 *
5 * Permission is hereby granted, free of charge, to any person obtaining a
6 * copy of this software and associated documentation files (the "Software"),
7 * to deal in the Software without restriction, including without limitation
8 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
9 * and/or sell copies of the Software, and to permit persons to whom the
10 * Software is furnished to do so, subject to the following conditions:
11 *
12 * The above copyright notice and this permission notice (including the next
13 * paragraph) shall be included in all copies or substantial portions of the
14 * Software.
15 *
16 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
19 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
22 * DEALINGS IN THE SOFTWARE.
23 */
24
25 #include <errno.h>
26 #include <stdbool.h>
27 #include <dbus/dbus.h>
28
29 #include <spa/utils/string.h>
30
31 #include "defs.h"
32 #include "player.h"
33
34 #define PLAYER_OBJECT_PATH_BASE "/media_player"
35
36 #define PLAYER_INTERFACE "org.mpris.MediaPlayer2.Player"
37
38 #define PLAYER_INTROSPECT_XML \
39 DBUS_INTROSPECT_1_0_XML_DOCTYPE_DECL_NODE \
40 "<node>" \
41 " <interface name='" PLAYER_INTERFACE "'>" \
42 " <property name='PlaybackStatus' type='s' access='read'/>" \
43 " </interface>" \
44 " <interface name='" DBUS_INTERFACE_PROPERTIES "'>" \
45 " <method name='Get'>" \
46 " <arg name='interface' type='s' direction='in' />" \
47 " <arg name='name' type='s' direction='in' />" \
48 " <arg name='value' type='v' direction='out' />" \
49 " </method>" \
50 " <method name='Set'>" \
51 " <arg name='interface' type='s' direction='in' />" \
52 " <arg name='name' type='s' direction='in' />" \
53 " <arg name='value' type='v' direction='in' />" \
54 " </method>" \
55 " <method name='GetAll'>" \
56 " <arg name='interface' type='s' direction='in' />" \
57 " <arg name='properties' type='a{sv}' direction='out' />" \
58 " </method>" \
59 " <signal name='PropertiesChanged'>" \
60 " <arg name='interface' type='s' />" \
61 " <arg name='changed_properties' type='a{sv}' />" \
62 " <arg name='invalidated_properties' type='as' />" \
63 " </signal>" \
64 " </interface>" \
65 " <interface name='" DBUS_INTERFACE_INTROSPECTABLE "'>" \
66 " <method name='Introspect'>" \
67 " <arg name='xml' type='s' direction='out'/>" \
68 " </method>" \
69 " </interface>" \
70 "</node>"
71
72 static struct spa_log_topic log_topic = SPA_LOG_TOPIC(0, "spa.bluez5.player");
73 #undef SPA_LOG_TOPIC_DEFAULT
74 #define SPA_LOG_TOPIC_DEFAULT &log_topic
75
76 #define MAX_PROPERTIES 1
77
78 struct impl {
79 struct spa_bt_player this;
80 DBusConnection *conn;
81 char *path;
82 struct spa_log *log;
83 struct spa_dict_item properties_items[MAX_PROPERTIES];
84 struct spa_dict properties;
85 unsigned int playing_count;
86 };
87
88 static size_t instance_counter = 0;
89
properties_get(struct impl * impl,DBusMessage * m)90 static DBusMessage *properties_get(struct impl *impl, DBusMessage *m)
91 {
92 const char *iface, *name;
93 size_t j;
94
95 if (!dbus_message_get_args(m, NULL,
96 DBUS_TYPE_STRING, &iface,
97 DBUS_TYPE_STRING, &name,
98 DBUS_TYPE_INVALID))
99 return NULL;
100
101 if (!spa_streq(iface, PLAYER_INTERFACE))
102 return dbus_message_new_error(m, DBUS_ERROR_INVALID_ARGS,
103 "No such interface");
104
105 for (j = 0; j < impl->properties.n_items; ++j) {
106 const struct spa_dict_item *item = &impl->properties.items[j];
107 if (spa_streq(item->key, name)) {
108 DBusMessage *r;
109 DBusMessageIter i, v;
110
111 r = dbus_message_new_method_return(m);
112 if (r == NULL)
113 return NULL;
114
115 dbus_message_iter_init_append(r, &i);
116 dbus_message_iter_open_container(&i, DBUS_TYPE_VARIANT,
117 "s", &v);
118 dbus_message_iter_append_basic(&v, DBUS_TYPE_STRING,
119 &item->value);
120 dbus_message_iter_close_container(&i, &v);
121 return r;
122 }
123 }
124
125 return dbus_message_new_error(m, DBUS_ERROR_INVALID_ARGS,
126 "No such property");
127 }
128
append_properties(struct impl * impl,DBusMessageIter * i)129 static void append_properties(struct impl *impl, DBusMessageIter *i)
130 {
131 DBusMessageIter d, e, v;
132 size_t j;
133
134 dbus_message_iter_open_container(i, DBUS_TYPE_ARRAY,
135 DBUS_DICT_ENTRY_BEGIN_CHAR_AS_STRING
136 DBUS_TYPE_STRING_AS_STRING DBUS_TYPE_VARIANT_AS_STRING
137 DBUS_DICT_ENTRY_END_CHAR_AS_STRING, &d);
138
139 for (j = 0; j < impl->properties.n_items; ++j) {
140 const struct spa_dict_item *item = &impl->properties.items[j];
141
142 spa_log_debug(impl->log, "player %s: %s=%s", impl->path,
143 item->key, item->value);
144
145 dbus_message_iter_open_container(&d, DBUS_TYPE_DICT_ENTRY, NULL, &e);
146 dbus_message_iter_append_basic(&e, DBUS_TYPE_STRING, &item->key);
147 dbus_message_iter_open_container(&e, DBUS_TYPE_VARIANT, "s", &v);
148 dbus_message_iter_append_basic(&v, DBUS_TYPE_STRING, &item->value);
149 dbus_message_iter_close_container(&e, &v);
150 dbus_message_iter_close_container(&d, &e);
151 }
152
153 dbus_message_iter_close_container(i, &d);
154 }
155
properties_get_all(struct impl * impl,DBusMessage * m)156 static DBusMessage *properties_get_all(struct impl *impl, DBusMessage *m)
157 {
158 const char *iface, *name;
159 DBusMessage *r;
160 DBusMessageIter i;
161
162 if (!dbus_message_get_args(m, NULL,
163 DBUS_TYPE_STRING, &iface,
164 DBUS_TYPE_STRING, &name,
165 DBUS_TYPE_INVALID))
166 return NULL;
167
168 if (!spa_streq(iface, PLAYER_INTERFACE))
169 return dbus_message_new_error(m, DBUS_ERROR_INVALID_ARGS,
170 "No such interface");
171
172 r = dbus_message_new_method_return(m);
173 if (r == NULL)
174 return NULL;
175
176 dbus_message_iter_init_append(r, &i);
177 append_properties(impl, &i);
178 return r;
179 }
180
properties_set(struct impl * impl,DBusMessage * m)181 static DBusMessage *properties_set(struct impl *impl, DBusMessage *m)
182 {
183 return dbus_message_new_error(m, DBUS_ERROR_PROPERTY_READ_ONLY,
184 "Property not writable");
185 }
186
introspect(struct impl * impl,DBusMessage * m)187 static DBusMessage *introspect(struct impl *impl, DBusMessage *m)
188 {
189 const char *xml = PLAYER_INTROSPECT_XML;
190 DBusMessage *r;
191 if ((r = dbus_message_new_method_return(m)) == NULL)
192 return NULL;
193 if (!dbus_message_append_args(r, DBUS_TYPE_STRING, &xml, DBUS_TYPE_INVALID))
194 return NULL;
195 return r;
196 }
197
player_handler(DBusConnection * c,DBusMessage * m,void * userdata)198 static DBusHandlerResult player_handler(DBusConnection *c, DBusMessage *m, void *userdata)
199 {
200 struct impl *impl = userdata;
201 DBusMessage *r;
202
203 if (dbus_message_is_method_call(m, DBUS_INTERFACE_INTROSPECTABLE, "Introspect")) {
204 r = introspect(impl, m);
205 } else if (dbus_message_is_method_call(m, DBUS_INTERFACE_PROPERTIES, "Get")) {
206 r = properties_get(impl, m);
207 } else if (dbus_message_is_method_call(m, DBUS_INTERFACE_PROPERTIES, "GetAll")) {
208 r = properties_get_all(impl, m);
209 } else if (dbus_message_is_method_call(m, DBUS_INTERFACE_PROPERTIES, "Set")) {
210 r = properties_set(impl, m);
211 } else {
212 return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
213 }
214
215 if (r == NULL)
216 return DBUS_HANDLER_RESULT_NEED_MEMORY;
217 if (!dbus_connection_send(impl->conn, r, NULL)) {
218 dbus_message_unref(r);
219 return DBUS_HANDLER_RESULT_NEED_MEMORY;
220 }
221 dbus_message_unref(r);
222 return DBUS_HANDLER_RESULT_HANDLED;
223 }
224
send_update_signal(struct impl * impl)225 static int send_update_signal(struct impl *impl)
226 {
227 DBusMessage *m;
228 const char *iface = PLAYER_INTERFACE;
229 DBusMessageIter i, a;
230 int res = 0;
231
232 m = dbus_message_new_signal(impl->path, DBUS_INTERFACE_PROPERTIES, "PropertiesChanged");
233 if (m == NULL)
234 return -ENOMEM;
235
236 dbus_message_iter_init_append(m, &i);
237 dbus_message_iter_append_basic(&i, DBUS_TYPE_STRING, &iface);
238
239 append_properties(impl, &i);
240
241 dbus_message_iter_open_container(&i, DBUS_TYPE_ARRAY,
242 DBUS_TYPE_STRING_AS_STRING, &a);
243 dbus_message_iter_close_container(&i, &a);
244
245 if (!dbus_connection_send(impl->conn, m, NULL))
246 res = -EIO;
247
248 dbus_message_unref(m);
249
250 return res;
251 }
252
update_properties(struct impl * impl,bool send_signal)253 static void update_properties(struct impl *impl, bool send_signal)
254 {
255 int nitems = 0;
256
257 switch (impl->this.state) {
258 case SPA_BT_PLAYER_PLAYING:
259 impl->properties_items[nitems++] = SPA_DICT_ITEM_INIT("PlaybackStatus", "Playing");
260 break;
261 case SPA_BT_PLAYER_STOPPED:
262 impl->properties_items[nitems++] = SPA_DICT_ITEM_INIT("PlaybackStatus", "Stopped");
263 break;
264 }
265 impl->properties = SPA_DICT_INIT(impl->properties_items, nitems);
266
267 if (!send_signal)
268 return;
269
270 send_update_signal(impl);
271 }
272
spa_bt_player_new(void * dbus_connection,struct spa_log * log)273 struct spa_bt_player *spa_bt_player_new(void *dbus_connection, struct spa_log *log)
274 {
275 struct impl *impl;
276 const DBusObjectPathVTable vtable = {
277 .message_function = player_handler,
278 };
279
280 spa_log_topic_init(log, &log_topic);
281
282 impl = calloc(1, sizeof(struct impl));
283 if (impl == NULL)
284 return NULL;
285
286 impl->this.state = SPA_BT_PLAYER_STOPPED;
287 impl->conn = dbus_connection;
288 impl->log = log;
289 impl->path = spa_aprintf("%s%zu", PLAYER_OBJECT_PATH_BASE, instance_counter++);
290 if (impl->path == NULL) {
291 free(impl);
292 return NULL;
293 }
294
295 dbus_connection_ref(impl->conn);
296
297 update_properties(impl, false);
298
299 if (!dbus_connection_register_object_path(impl->conn, impl->path, &vtable, impl)) {
300 spa_bt_player_destroy(&impl->this);
301 errno = EIO;
302 return NULL;
303 }
304
305 return &impl->this;
306 }
307
spa_bt_player_destroy(struct spa_bt_player * player)308 void spa_bt_player_destroy(struct spa_bt_player *player)
309 {
310 struct impl *impl = SPA_CONTAINER_OF(player, struct impl, this);
311
312 /*
313 * We unregister only the object path, but don't unregister it from
314 * BlueZ, to avoid hanging on BlueZ DBus activation. The assumption is
315 * that the DBus connection is terminated immediately after.
316 */
317 dbus_connection_unregister_object_path(impl->conn, impl->path);
318
319 dbus_connection_unref(impl->conn);
320 free(impl->path);
321 free(impl);
322 }
323
spa_bt_player_set_state(struct spa_bt_player * player,enum spa_bt_player_state state)324 int spa_bt_player_set_state(struct spa_bt_player *player, enum spa_bt_player_state state)
325 {
326 struct impl *impl = SPA_CONTAINER_OF(player, struct impl, this);
327
328 switch (state) {
329 case SPA_BT_PLAYER_PLAYING:
330 if (impl->playing_count++ > 0)
331 return 0;
332 break;
333 case SPA_BT_PLAYER_STOPPED:
334 if (impl->playing_count == 0)
335 return -EINVAL;
336 if (--impl->playing_count > 0)
337 return 0;
338 break;
339 default:
340 return -EINVAL;
341 }
342
343 impl->this.state = state;
344 update_properties(impl, true);
345
346 return 0;
347 }
348
spa_bt_player_register(struct spa_bt_player * player,const char * adapter_path)349 int spa_bt_player_register(struct spa_bt_player *player, const char *adapter_path)
350 {
351 struct impl *impl = SPA_CONTAINER_OF(player, struct impl, this);
352
353 DBusError err;
354 DBusMessageIter i;
355 DBusMessage *m, *r;
356 int res = 0;
357
358 spa_log_debug(impl->log, "RegisterPlayer() for dummy AVRCP player %s for %s",
359 impl->path, adapter_path);
360
361 m = dbus_message_new_method_call(BLUEZ_SERVICE, adapter_path,
362 BLUEZ_MEDIA_INTERFACE, "RegisterPlayer");
363 if (m == NULL)
364 return -EIO;
365
366 dbus_message_iter_init_append(m, &i);
367 dbus_message_iter_append_basic(&i, DBUS_TYPE_OBJECT_PATH, &impl->path);
368 append_properties(impl, &i);
369
370 dbus_error_init(&err);
371 r = dbus_connection_send_with_reply_and_block(impl->conn, m, -1, &err);
372 dbus_message_unref(m);
373
374 if (r == NULL) {
375 spa_log_error(impl->log, "RegisterPlayer() failed (%s)", err.message);
376 dbus_error_free(&err);
377 return -EIO;
378 }
379
380 if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) {
381 spa_log_error(impl->log, "RegisterPlayer() failed");
382 res = -EIO;
383 }
384
385 dbus_message_unref(r);
386
387 return res;
388 }
389
spa_bt_player_unregister(struct spa_bt_player * player,const char * adapter_path)390 int spa_bt_player_unregister(struct spa_bt_player *player, const char *adapter_path)
391 {
392 struct impl *impl = SPA_CONTAINER_OF(player, struct impl, this);
393
394 DBusError err;
395 DBusMessageIter i;
396 DBusMessage *m, *r;
397 int res = 0;
398
399 spa_log_debug(impl->log, "UnregisterPlayer() for dummy AVRCP player %s for %s",
400 impl->path, adapter_path);
401
402 m = dbus_message_new_method_call(BLUEZ_SERVICE, adapter_path,
403 BLUEZ_MEDIA_INTERFACE, "UnregisterPlayer");
404 if (m == NULL)
405 return -EIO;
406
407 dbus_message_iter_init_append(m, &i);
408 dbus_message_iter_append_basic(&i, DBUS_TYPE_OBJECT_PATH, &impl->path);
409
410 dbus_error_init(&err);
411 r = dbus_connection_send_with_reply_and_block(impl->conn, m, -1, &err);
412 dbus_message_unref(m);
413
414 if (r == NULL) {
415 spa_log_error(impl->log, "UnregisterPlayer() failed (%s)", err.message);
416 dbus_error_free(&err);
417 return -EIO;
418 }
419
420 if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) {
421 spa_log_error(impl->log, "UnregisterPlayer() failed");
422 res = -EIO;
423 }
424
425 dbus_message_unref(r);
426
427 return res;
428 }
429