On 8 September 2014 14:45, Wim Taymans <wim.taymans at gmail.com> wrote: > Add a simple native headset backend that implements support for the > blutooth HSP profile. > This allows pulseaudio to output audio to a Headset using the HSP profile. > > Make the native backend the default. Thanks for taking this up. tbh, I still think this should be in BlueZ, but since that seems to be a dead end, and creating a new daemon is probably more work that it's worth, let's go with this approach. Luiz, would you also have the time to take a look over this? > --- > configure.ac | 6 +- > src/modules/bluetooth/backend-native.c | 474 +++++++++++++++++++++++++++++++++ > 2 files changed, 477 insertions(+), 3 deletions(-) > create mode 100644 src/modules/bluetooth/backend-native.c > > diff --git a/configure.ac b/configure.ac > index e1e2572..a91639f 100644 > --- a/configure.ac > +++ b/configure.ac > @@ -1030,14 +1030,14 @@ AM_CONDITIONAL([HAVE_BLUEZ], [test "x$HAVE_BLUEZ" = x1]) > ## Bluetooth Headset profiles backend ## > > AC_ARG_WITH(bluetooth_headset_backend, > - AS_HELP_STRING([--with-bluetooth-headset-backend=<ofono|null>],[Backend for Bluetooth headset profiles (ofono)])) > + AS_HELP_STRING([--with-bluetooth-headset-backend=<ofono|native|null>],[Backend for Bluetooth headset profiles (native)])) > if test -z "$with_bluetooth_headset_backend" ; then > - BLUETOOTH_HEADSET_BACKEND=ofono > + BLUETOOTH_HEADSET_BACKEND=native > else > BLUETOOTH_HEADSET_BACKEND=$with_bluetooth_headset_backend > fi > > -AS_IF([test "x$BLUETOOTH_HEADSET_BACKEND" != "xofono" && test "x$BLUETOOTH_HEADSET_BACKEND" != "xnull"], > +AS_IF([test "x$BLUETOOTH_HEADSET_BACKEND" != "xofono" && test "x$BLUETOOTH_HEADSET_BACKEND" != "xnull" && test "x$BLUETOOTH_HEADSET_BACKEND" != "xnative"], > [AC_MSG_ERROR([*** Invalid Bluetooth Headset backend])]) > > AC_SUBST(BLUETOOTH_HEADSET_BACKEND) > diff --git a/src/modules/bluetooth/backend-native.c b/src/modules/bluetooth/backend-native.c > new file mode 100644 > index 0000000..8af610c > --- /dev/null > +++ b/src/modules/bluetooth/backend-native.c > @@ -0,0 +1,474 @@ > +/*** > + This file is part of PulseAudio. > + > + Copyright 2014 Wim Taymans <wim.taymans at gmail.com> > + > + PulseAudio is free software; you can redistribute it and/or modify > + it under the terms of the GNU Lesser General Public License as > + published by the Free Software Foundation; either version 2.1 of the > + License, or (at your option) any later version. > + > + PulseAudio is distributed in the hope that it will be useful, but > + WITHOUT ANY WARRANTY; without even the implied warranty of > + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU > + General Public License for more details. > + > + You should have received a copy of the GNU Lesser General Public > + License along with PulseAudio; if not, write to the Free Software > + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 > + USA. > +***/ > + > +#ifdef HAVE_CONFIG_H > +#include <config.h> > +#endif > + > +#include <pulsecore/shared.h> > +#include <pulsecore/core-error.h> > +#include <pulsecore/core-util.h> > +#include <pulsecore/dbus-shared.h> > +#include <pulsecore/log.h> > + > +#include <errno.h> > +#include <sys/types.h> > +#include <sys/socket.h> > + > +#include <bluetooth/bluetooth.h> > +#include <bluetooth/sco.h> > + > +#include "bluez5-util.h" > + > +struct pa_bluetooth_backend { > + pa_core *core; > + pa_dbus_connection *connection; > + pa_bluetooth_discovery *discovery; > + > + PA_LLIST_HEAD(pa_dbus_pending, pending); > +}; > + > +struct transport_rfcomm { > + int rfcomm_fd; > + pa_io_event *rfcomm_io; > + pa_bluetooth_backend *backend; > +}; > + > +#define BLUEZ_SERVICE "org.bluez" > +#define BLUEZ_MEDIA_TRANSPORT_INTERFACE BLUEZ_SERVICE ".MediaTransport1" > + > +#define BLUEZ_ERROR_NOT_SUPPORTED "org.bluez.Error.NotSupported" > + > +#define BLUEZ_PROFILE_MANAGER_INTERFACE BLUEZ_SERVICE ".ProfileManager1" > +#define BLUEZ_PROFILE_INTERFACE BLUEZ_SERVICE ".Profile1" > + > +#define HSP_AG_PROFILE "/Profile/HSPAGProfile" > + > +#define PROFILE_INTROSPECT_XML \ > + DBUS_INTROSPECT_1_0_XML_DOCTYPE_DECL_NODE \ > + "<node>" \ > + " <interface name=\"" BLUEZ_PROFILE_INTERFACE "\">" \ > + " <method name=\"Release\">" \ > + " </method>" \ > + " <method name=\"Cancel\">" \ > + " </method>" \ > + " <method name=\"RequestDisconnection\">" \ > + " <arg name=\"device\" direction=\"in\" type=\"o\"/>" \ > + " </method>" \ > + " <method name=\"NewConnection\">" \ > + " <arg name=\"device\" direction=\"in\" type=\"o\"/>" \ > + " <arg name=\"fd\" direction=\"in\" type=\"h\"/>" \ > + " <arg name=\"opts\" direction=\"in\" type=\"a{sv}\"/>" \ > + " </method>" \ > + " </interface>" \ > + " <interface name=\"org.freedesktop.DBus.Introspectable\">" \ > + " <method name=\"Introspect\">" \ > + " <arg name=\"data\" type=\"s\" direction=\"out\"/>" \ > + " </method>" \ > + " </interface>" \ > + "</node>" > + > +static pa_dbus_pending* send_and_add_to_pending(pa_bluetooth_backend *backend, DBusMessage *m, > + DBusPendingCallNotifyFunction func, void *call_data) { > + pa_dbus_pending *p; > + DBusPendingCall *call; > + > + pa_assert(backend); > + pa_assert(m); > + > + pa_assert_se(dbus_connection_send_with_reply(pa_dbus_connection_get(backend->connection), m, &call, -1)); > + > + p = pa_dbus_pending_new(pa_dbus_connection_get(backend->connection), m, call, backend, call_data); > + PA_LLIST_PREPEND(pa_dbus_pending, backend->pending, p); > + dbus_pending_call_set_notify(call, func, p, NULL); > + > + return p; > +} > + > +static int bluez5_sco_acquire_cb(pa_bluetooth_transport *t, bool optional, size_t *imtu, size_t *omtu) { > + pa_bluetooth_device *d = t->device; > + struct sockaddr_sco addr; > + int err, i; > + int sock; > + bdaddr_t src; > + bdaddr_t dst; > + const char *src_addr, *dst_addr; > + > + src_addr = d->adapter->address; > + dst_addr = d->address; > + > + for (i = 5; i >= 0; i--, src_addr += 3) > + src.b[i] = strtol(src_addr, NULL, 16); > + for (i = 5; i >= 0; i--, dst_addr += 3) > + dst.b[i] = strtol(dst_addr, NULL, 16); > + > + sock = socket(PF_BLUETOOTH, SOCK_SEQPACKET, BTPROTO_SCO); > + if (sock < 0) { > + pa_log_error("socket(SEQPACKET, SCO) %s", pa_cstrerror(errno)); > + return -1; > + } > + > + memset(&addr, 0, sizeof(addr)); > + addr.sco_family = AF_BLUETOOTH; > + bacpy(&addr.sco_bdaddr, &src); > + > + if (bind(sock, (struct sockaddr *) &addr, sizeof(addr)) < 0) { > + pa_log_error("bind(): %s", pa_cstrerror(errno)); > + goto fail_close; > + } > + > + memset(&addr, 0, sizeof(addr)); > + addr.sco_family = AF_BLUETOOTH; > + bacpy(&addr.sco_bdaddr, &dst); > + > + pa_log_info ("doing connect\n"); > + err = connect(sock, (struct sockaddr *) &addr, sizeof(addr)); > + if (err < 0 && !(errno == EAGAIN || errno == EINPROGRESS)) { > + pa_log_error("connect(): %s", pa_cstrerror(errno)); > + goto fail_close; > + } > + > + if (imtu) > + *imtu = 48; > + > + if (omtu) > + *omtu = 48; Out of curiosity, are these values fixed in the spec? > + > + return sock; > + > +fail_close: > + close(sock); > + return -1; > +} > + > +static void bluez5_sco_release_cb(pa_bluetooth_transport *t) { > + pa_log_info("Transport %s released", t->path); > +} > + > +static void register_profile_reply(DBusPendingCall *pending, void *userdata) { > + DBusMessage *r; > + pa_dbus_pending *p; > + pa_bluetooth_backend *b; > + char *profile; > + > + pa_assert(pending); > + pa_assert_se(p = userdata); > + pa_assert_se(b = p->context_data); > + pa_assert_se(profile = p->call_data); > + pa_assert_se(r = dbus_pending_call_steal_reply(pending)); > + > + if (dbus_message_is_error(r, BLUEZ_ERROR_NOT_SUPPORTED)) { > + pa_log_info("Couldn't register profile %s because it is disabled in BlueZ", profile); > + goto finish; > + } > + > + if (dbus_message_get_type(r) == DBUS_MESSAGE_TYPE_ERROR) { > + pa_log_error(BLUEZ_PROFILE_MANAGER_INTERFACE ".RegisterProfile() failed: %s: %s", dbus_message_get_error_name(r), > + pa_dbus_get_error_message(r)); > + goto finish; > + } > + > +finish: > + dbus_message_unref(r); > + > + PA_LLIST_REMOVE(pa_dbus_pending, b->pending, p); > + pa_dbus_pending_free(p); > + > + pa_xfree(profile); > +} > + > +static void register_profile(pa_bluetooth_backend *b, const char *profile, const char *uuid) { > + DBusMessage *m; > + DBusMessageIter i, d; > + > + pa_log_debug("Registering Profile %s", profile); > + > + pa_assert_se(m = dbus_message_new_method_call(BLUEZ_SERVICE, "/org/bluez", BLUEZ_PROFILE_MANAGER_INTERFACE, "RegisterProfile")); > + > + dbus_message_iter_init_append(m, &i); > + dbus_message_iter_append_basic(&i, DBUS_TYPE_OBJECT_PATH, &profile); > + dbus_message_iter_append_basic(&i, DBUS_TYPE_STRING, &uuid); > + dbus_message_iter_open_container(&i, DBUS_TYPE_ARRAY, DBUS_DICT_ENTRY_BEGIN_CHAR_AS_STRING DBUS_TYPE_STRING_AS_STRING > + DBUS_TYPE_VARIANT_AS_STRING DBUS_DICT_ENTRY_END_CHAR_AS_STRING, &d); > + dbus_message_iter_close_container(&i, &d); > + > + send_and_add_to_pending(b, m, register_profile_reply, pa_xstrdup(profile)); > +} > + > +static void rfcomm_io_callback(pa_mainloop_api *io, pa_io_event *e, int fd, pa_io_event_flags_t events, void*userdata) { > + pa_bluetooth_transport *t = userdata; > + > + pa_assert(io); > + pa_assert(t); > + > + if (events & (PA_IO_EVENT_HANGUP|PA_IO_EVENT_ERROR)) { > + pa_log("Lost RFCOMM connection."); > + goto fail; > + } > + > + if (events & PA_IO_EVENT_INPUT) { > + char buf[512]; > + ssize_t len; > + > + len = read (fd, buf, 511); > + buf[len] = 0; > + pa_log("RFCOMM << %s", buf); > + > + pa_log("RFCOMM >> OK"); > + len = write (fd, "\r\nOK\r\n", 5); > + if (len < 0) > + pa_log_error("RFCOMM write error: %s", pa_cstrerror(errno)); Should we go to fail if this happens? > + } > + return; > + > +fail: > + pa_bluetooth_transport_free(t); > + return; > +} > + > +static void transport_dispose(pa_bluetooth_transport *t) { > + struct transport_rfcomm *trfc = t->userdata; > + > + trfc->backend->core->mainloop->io_free(trfc->rfcomm_io); > + shutdown (trfc->rfcomm_fd, SHUT_RDWR); > + close (trfc->rfcomm_fd); > + > + pa_xfree(trfc); > +} > + > + > +static DBusMessage *profile_new_connection(DBusConnection *conn, DBusMessage *m, void *userdata) { > + pa_bluetooth_backend *b = userdata; > + pa_bluetooth_device *d; > + pa_bluetooth_transport *t; > + pa_bluetooth_profile_t p; > + DBusMessage *r; > + int fd; > + const char *sender, *path, *handler; > + DBusMessageIter arg_i, element_i; > + char *pathfd; > + struct transport_rfcomm *trfc; > + > + if (!dbus_message_iter_init(m, &arg_i) || !pa_streq(dbus_message_get_signature(m), "oha{sv}")) { > + pa_log_error("Invalid signature found in NewConnection"); > + goto fail; > + } > + > + handler = dbus_message_get_path(m); > + > + pa_assert(dbus_message_iter_get_arg_type(&arg_i) == DBUS_TYPE_OBJECT_PATH); > + dbus_message_iter_get_basic(&arg_i, &path); > + > + d = pa_bluetooth_discovery_get_device_by_path(b->discovery, path); > + if (d == NULL) { > + pa_log_error("Device doesnt exist for %s", path); > + goto fail; > + } > + > + if (pa_streq(handler, HSP_AG_PROFILE)) { > + p = PA_BLUETOOTH_PROFILE_HEADSET_HEAD_UNIT; > + } else > + goto refused; Is the else possible at all? > + > + pa_assert_se(dbus_message_iter_next(&arg_i)); > + > + pa_assert(dbus_message_iter_get_arg_type(&arg_i) == DBUS_TYPE_UNIX_FD); > + dbus_message_iter_get_basic(&arg_i, &fd); > + > + pa_log_debug("dbus: NewConnection path=%s, fd=%d", path, fd); > + > + pa_assert_se(dbus_message_iter_next(&arg_i)); > + pa_assert(dbus_message_iter_get_arg_type(&arg_i) == DBUS_TYPE_ARRAY); > + > + dbus_message_iter_recurse(&arg_i, &element_i); > + > + while (dbus_message_iter_get_arg_type(&element_i) == DBUS_TYPE_DICT_ENTRY) { > + DBusMessageIter dict_i; > + const char *key; > + > + dbus_message_iter_recurse(&element_i, &dict_i); > + > + if (key == NULL) > + break; > + if (dbus_message_iter_get_arg_type(&dict_i) != DBUS_TYPE_STRING) > + break; > + > + dbus_message_iter_get_basic(&dict_i, &key); > + > + if (!dbus_message_iter_next(&dict_i)) > + break; > + > + if (dbus_message_iter_get_arg_type(&dict_i) != DBUS_TYPE_VARIANT) > + break; > + > + pa_log_debug("key=%s", key); > + > + dbus_message_iter_next(&element_i); > + } If we're not doing anything with the keys, maybe we don't need to parse any of this? > + > + sender = dbus_message_get_sender(m); > + > + pathfd = pa_sprintf_malloc ("%s/fd%d", path, fd); > + d->transports[p] = t = pa_bluetooth_transport_new(d, sender, pathfd, p, NULL, 0); > + pa_xfree(pathfd); > + > + t->acquire = bluez5_sco_acquire_cb; > + t->release = bluez5_sco_release_cb; > + t->dispose = transport_dispose; > + > + trfc = pa_xnew0(struct transport_rfcomm, 1); > + trfc->rfcomm_fd = fd; > + trfc->rfcomm_io = b->core->mainloop->io_new(b->core->mainloop, fd, PA_IO_EVENT_INPUT|PA_IO_EVENT_HANGUP, > + rfcomm_io_callback, t); Can reading/writing on the RFCOMM fd block the mainloop for long periods? > + trfc->backend = b; Can the backend be destroyed before the transport? Maybe we should only reference the mainloop instead of the backend here. > + t->userdata = trfc; > + > + pa_bluetooth_transport_put(t); > + > + pa_log_debug("Transport %s available for profile %s", t->path, pa_bluetooth_profile_to_string(t->profile)); > + > + pa_assert_se(r = dbus_message_new_method_return(m)); > + > + return r; > + > +fail: > + pa_assert_se(r = dbus_message_new_error(m, "org.bluez.Error.InvalidArguments", "Unable to handle new connection")); > + return r; > +refused: > + pa_log_debug("dbus: NewConnection path=%s rejected", path); > + pa_assert_se(r = dbus_message_new_error(m, "org.bluez.Error.Rejected", "New connection rejected")); > + return r; > +} > + > +static DBusMessage *profile_request_disconnection(DBusConnection *conn, DBusMessage *m, void *userdata) { > + DBusMessage *r; > + > + pa_assert_se(r = dbus_message_new_method_return(m)); How is the transport torn down in this case? > + > + return r; > +} > + > +static DBusHandlerResult profile_handler(DBusConnection *c, DBusMessage *m, void *userdata) { > + pa_bluetooth_backend *b = userdata; > + DBusMessage *r = NULL; > + const char *path, *interface, *member; > + > + pa_assert(b); > + > + path = dbus_message_get_path(m); > + interface = dbus_message_get_interface(m); > + member = dbus_message_get_member(m); > + > + pa_log_debug("dbus: path=%s, interface=%s, member=%s", path, interface, member); > + > + if (!pa_streq(path, HSP_AG_PROFILE)) > + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; > + > + if (dbus_message_is_method_call(m, "org.freedesktop.DBus.Introspectable", "Introspect")) { > + const char *xml = PROFILE_INTROSPECT_XML; > + > + pa_assert_se(r = dbus_message_new_method_return(m)); > + pa_assert_se(dbus_message_append_args(r, DBUS_TYPE_STRING, &xml, DBUS_TYPE_INVALID)); > + > + } else if (dbus_message_is_method_call(m, BLUEZ_PROFILE_INTERFACE, "Release")) { > + } else if (dbus_message_is_method_call(m, BLUEZ_PROFILE_INTERFACE, "Cancel")) { There doesn't actually seem to be a 'Cancel' method. > + } else if (dbus_message_is_method_call(m, BLUEZ_PROFILE_INTERFACE, "RequestDisconnection")) { > + r = profile_request_disconnection(c, m, userdata); > + } else if (dbus_message_is_method_call(m, BLUEZ_PROFILE_INTERFACE, "NewConnection")) > + r = profile_new_connection(c, m, userdata); > + else > + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; > + > + if (r) { > + pa_assert_se(dbus_connection_send(pa_dbus_connection_get(b->connection), r, NULL)); > + dbus_message_unref(r); > + } > + > + return DBUS_HANDLER_RESULT_HANDLED; > +} > + > +static void profile_init(pa_bluetooth_backend *b, pa_bluetooth_profile_t profile) { > + static const DBusObjectPathVTable vtable_profile = { > + .message_function = profile_handler, > + }; > + const char *object_name; > + const char *uuid; > + > + pa_assert(b); > + > + switch(profile) { > + case PA_BLUETOOTH_PROFILE_HEADSET_HEAD_UNIT: > + object_name = HSP_AG_PROFILE; > + uuid = PA_BLUETOOTH_UUID_HSP_AG; > + break; > + default: > + pa_assert_not_reached(); > + break; > + } > + pa_assert_se(dbus_connection_register_object_path(pa_dbus_connection_get(b->connection), > + object_name, &vtable_profile, b)); > + register_profile (b, object_name, uuid); There doesn't seem to be a way to handle failure when registering the profile. Should we perhaps be unregistering the object path? > +} > + > +static void profile_done(pa_bluetooth_backend *b, pa_bluetooth_profile_t profile) { > + pa_assert(b); > + > + switch(profile) { > + case PA_BLUETOOTH_PROFILE_HEADSET_HEAD_UNIT: > + dbus_connection_unregister_object_path(pa_dbus_connection_get(b->connection), HSP_AG_PROFILE); > + break; > + default: > + pa_assert_not_reached(); > + break; > + } > +} > + > +pa_bluetooth_backend *pa_bluetooth_backend_new(pa_core *c) { > + pa_bluetooth_backend *backend; > + DBusError err; > + > + pa_log_debug("Bluetooth Headset Backend API support using the NULL backend"); > + > + backend = pa_xnew0(pa_bluetooth_backend, 1); > + backend->core = c; > + > + dbus_error_init(&err); > + if (!(backend->connection = pa_dbus_bus_get(c, DBUS_BUS_SYSTEM, &err))) { > + pa_log("Failed to get D-Bus connection: %s", err.message); > + dbus_error_free(&err); > + pa_xfree(backend); > + return NULL; > + } > + > + backend->discovery = pa_shared_get(c, "bluetooth-discovery"); > + > + profile_init(backend, PA_BLUETOOTH_PROFILE_HEADSET_HEAD_UNIT); > + > + return backend; > +} > + > +void pa_bluetooth_backend_free(pa_bluetooth_backend *backend) { > + pa_assert(backend); > + > + profile_done(backend, PA_BLUETOOTH_PROFILE_HEADSET_HEAD_UNIT); backend->connection needs to be unref'ed here. > + > + pa_xfree(backend); > +} > -- > 1.9.3 Cheers, Arun