summaryrefslogtreecommitdiff
path: root/profiles/audio/sink.c
diff options
context:
space:
mode:
Diffstat (limited to 'profiles/audio/sink.c')
-rw-r--r--profiles/audio/sink.c454
1 files changed, 454 insertions, 0 deletions
diff --git a/profiles/audio/sink.c b/profiles/audio/sink.c
new file mode 100644
index 00000000..400af061
--- /dev/null
+++ b/profiles/audio/sink.c
@@ -0,0 +1,454 @@
+/*
+ *
+ * BlueZ - Bluetooth protocol stack for Linux
+ *
+ * Copyright (C) 2006-2010 Nokia Corporation
+ * Copyright (C) 2004-2010 Marcel Holtmann <marcel@holtmann.org>
+ *
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdint.h>
+#include <stdbool.h>
+#include <errno.h>
+
+#include <bluetooth/bluetooth.h>
+#include <bluetooth/sdp.h>
+
+#include <glib.h>
+#include <dbus/dbus.h>
+#include <gdbus/gdbus.h>
+
+#include "log.h"
+
+#include "src/adapter.h"
+#include "src/device.h"
+#include "src/service.h"
+
+#include "avdtp.h"
+#include "media.h"
+#include "a2dp.h"
+#include "error.h"
+#include "sink.h"
+#include "dbus-common.h"
+
+#define STREAM_SETUP_RETRY_TIMER 2
+
+struct sink {
+ struct btd_service *service;
+ struct avdtp *session;
+ struct avdtp_stream *stream;
+ unsigned int cb_id;
+ avdtp_session_state_t session_state;
+ avdtp_state_t stream_state;
+ sink_state_t state;
+ unsigned int connect_id;
+ unsigned int disconnect_id;
+ unsigned int avdtp_callback_id;
+};
+
+struct sink_state_callback {
+ sink_state_cb cb;
+ struct btd_service *service;
+ void *user_data;
+ unsigned int id;
+};
+
+static GSList *sink_callbacks = NULL;
+
+static char *str_state[] = {
+ "SINK_STATE_DISCONNECTED",
+ "SINK_STATE_CONNECTING",
+ "SINK_STATE_CONNECTED",
+ "SINK_STATE_PLAYING",
+};
+
+static void sink_set_state(struct sink *sink, sink_state_t new_state)
+{
+ struct btd_service *service = sink->service;
+ struct btd_device *dev = btd_service_get_device(service);
+ sink_state_t old_state = sink->state;
+ GSList *l;
+
+ sink->state = new_state;
+
+ DBG("State changed %s: %s -> %s", device_get_path(dev),
+ str_state[old_state], str_state[new_state]);
+
+ for (l = sink_callbacks; l != NULL; l = l->next) {
+ struct sink_state_callback *cb = l->data;
+
+ if (cb->service != service)
+ continue;
+
+ cb->cb(service, old_state, new_state, cb->user_data);
+ }
+
+ if (new_state != SINK_STATE_DISCONNECTED)
+ return;
+
+ if (sink->session) {
+ avdtp_unref(sink->session);
+ sink->session = NULL;
+ }
+}
+
+static void avdtp_state_callback(struct btd_device *dev,
+ struct avdtp *session,
+ avdtp_session_state_t old_state,
+ avdtp_session_state_t new_state,
+ void *user_data)
+{
+ struct sink *sink = user_data;
+
+ switch (new_state) {
+ case AVDTP_SESSION_STATE_DISCONNECTED:
+ sink_set_state(sink, SINK_STATE_DISCONNECTED);
+ break;
+ case AVDTP_SESSION_STATE_CONNECTING:
+ sink_set_state(sink, SINK_STATE_CONNECTING);
+ break;
+ case AVDTP_SESSION_STATE_CONNECTED:
+ break;
+ }
+
+ sink->session_state = new_state;
+}
+
+static void stream_state_changed(struct avdtp_stream *stream,
+ avdtp_state_t old_state,
+ avdtp_state_t new_state,
+ struct avdtp_error *err,
+ void *user_data)
+{
+ struct btd_service *service = user_data;
+ struct sink *sink = btd_service_get_user_data(service);
+
+ if (err)
+ return;
+
+ switch (new_state) {
+ case AVDTP_STATE_IDLE:
+ btd_service_disconnecting_complete(sink->service, 0);
+
+ if (sink->disconnect_id > 0) {
+ a2dp_cancel(sink->disconnect_id);
+ sink->disconnect_id = 0;
+ }
+
+ if (sink->session) {
+ avdtp_unref(sink->session);
+ sink->session = NULL;
+ }
+ sink->stream = NULL;
+ sink->cb_id = 0;
+ break;
+ case AVDTP_STATE_OPEN:
+ btd_service_connecting_complete(sink->service, 0);
+ sink_set_state(sink, SINK_STATE_CONNECTED);
+ break;
+ case AVDTP_STATE_STREAMING:
+ sink_set_state(sink, SINK_STATE_PLAYING);
+ break;
+ case AVDTP_STATE_CONFIGURED:
+ case AVDTP_STATE_CLOSING:
+ case AVDTP_STATE_ABORTING:
+ default:
+ break;
+ }
+
+ sink->stream_state = new_state;
+}
+
+static void stream_setup_complete(struct avdtp *session, struct a2dp_sep *sep,
+ struct avdtp_stream *stream,
+ struct avdtp_error *err, void *user_data)
+{
+ struct sink *sink = user_data;
+
+ sink->connect_id = 0;
+
+ if (stream)
+ return;
+
+ avdtp_unref(sink->session);
+ sink->session = NULL;
+ if (avdtp_error_category(err) == AVDTP_ERRNO
+ && avdtp_error_posix_errno(err) != EHOSTDOWN)
+ btd_service_connecting_complete(sink->service, -EAGAIN);
+ else
+ btd_service_connecting_complete(sink->service, -EIO);
+}
+
+static void select_complete(struct avdtp *session, struct a2dp_sep *sep,
+ GSList *caps, void *user_data)
+{
+ struct sink *sink = user_data;
+ int id;
+
+ sink->connect_id = 0;
+
+ id = a2dp_config(session, sep, stream_setup_complete, caps, sink);
+ if (id == 0)
+ goto failed;
+
+ sink->connect_id = id;
+ return;
+
+failed:
+ btd_service_connecting_complete(sink->service, -EIO);
+
+ avdtp_unref(sink->session);
+ sink->session = NULL;
+}
+
+static void discovery_complete(struct avdtp *session, GSList *seps, struct avdtp_error *err,
+ void *user_data)
+{
+ struct sink *sink = user_data;
+ int id, perr;
+
+ if (err) {
+ avdtp_unref(sink->session);
+ sink->session = NULL;
+ if (avdtp_error_category(err) == AVDTP_ERRNO
+ && avdtp_error_posix_errno(err) != EHOSTDOWN) {
+ perr = -EAGAIN;
+ } else
+ perr = -EIO;
+ goto failed;
+ }
+
+ DBG("Discovery complete");
+
+ id = a2dp_select_capabilities(sink->session, AVDTP_SEP_TYPE_SINK, NULL,
+ select_complete, sink);
+ if (id == 0) {
+ perr = -EIO;
+ goto failed;
+ }
+
+ sink->connect_id = id;
+ return;
+
+failed:
+ btd_service_connecting_complete(sink->service, perr);
+ avdtp_unref(sink->session);
+ sink->session = NULL;
+}
+
+gboolean sink_setup_stream(struct btd_service *service, struct avdtp *session)
+{
+ struct sink *sink = btd_service_get_user_data(service);
+
+ if (sink->connect_id > 0 || sink->disconnect_id > 0)
+ return FALSE;
+
+ if (session && !sink->session)
+ sink->session = avdtp_ref(session);
+
+ if (!sink->session)
+ return FALSE;
+
+ if (avdtp_discover(sink->session, discovery_complete, sink) < 0)
+ return FALSE;
+
+ return TRUE;
+}
+
+int sink_connect(struct btd_service *service)
+{
+ struct sink *sink = btd_service_get_user_data(service);
+
+ if (!sink->session)
+ sink->session = avdtp_get(btd_service_get_device(service));
+
+ if (!sink->session) {
+ DBG("Unable to get a session");
+ return -EIO;
+ }
+
+ if (sink->connect_id > 0 || sink->disconnect_id > 0)
+ return -EBUSY;
+
+ if (sink->stream_state >= AVDTP_STATE_OPEN)
+ return -EALREADY;
+
+ if (!sink_setup_stream(service, NULL)) {
+ DBG("Failed to create a stream");
+ return -EIO;
+ }
+
+ DBG("stream creation in progress");
+
+ return 0;
+}
+
+static void sink_free(struct btd_service *service)
+{
+ struct sink *sink = btd_service_get_user_data(service);
+
+ if (sink->cb_id)
+ avdtp_stream_remove_cb(sink->session, sink->stream,
+ sink->cb_id);
+
+ if (sink->session)
+ avdtp_unref(sink->session);
+
+ if (sink->connect_id > 0) {
+ btd_service_connecting_complete(sink->service, -ECANCELED);
+ a2dp_cancel(sink->connect_id);
+ sink->connect_id = 0;
+ }
+
+ if (sink->disconnect_id > 0) {
+ btd_service_disconnecting_complete(sink->service, -ECANCELED);
+ a2dp_cancel(sink->disconnect_id);
+ sink->disconnect_id = 0;
+ }
+
+ avdtp_remove_state_cb(sink->avdtp_callback_id);
+ btd_service_unref(sink->service);
+
+ g_free(sink);
+}
+
+void sink_unregister(struct btd_service *service)
+{
+ struct btd_device *dev = btd_service_get_device(service);
+
+ DBG("%s", device_get_path(dev));
+
+ sink_free(service);
+}
+
+int sink_init(struct btd_service *service)
+{
+ struct btd_device *dev = btd_service_get_device(service);
+ struct sink *sink;
+
+ DBG("%s", device_get_path(dev));
+
+ sink = g_new0(struct sink, 1);
+
+ sink->service = btd_service_ref(service);
+
+ sink->avdtp_callback_id = avdtp_add_state_cb(dev, avdtp_state_callback,
+ sink);
+
+ btd_service_set_user_data(service, sink);
+
+ return 0;
+}
+
+gboolean sink_is_active(struct btd_service *service)
+{
+ struct sink *sink = btd_service_get_user_data(service);
+
+ if (sink->session)
+ return TRUE;
+
+ return FALSE;
+}
+
+gboolean sink_new_stream(struct btd_service *service, struct avdtp *session,
+ struct avdtp_stream *stream)
+{
+ struct sink *sink = btd_service_get_user_data(service);
+
+ if (sink->stream)
+ return FALSE;
+
+ if (!sink->session)
+ sink->session = avdtp_ref(session);
+
+ sink->stream = stream;
+
+ sink->cb_id = avdtp_stream_add_cb(session, stream,
+ stream_state_changed, service);
+
+ return TRUE;
+}
+
+int sink_disconnect(struct btd_service *service, gboolean shutdown)
+{
+ struct sink *sink = btd_service_get_user_data(service);
+
+ if (!sink->session)
+ return -ENOTCONN;
+
+ if (shutdown)
+ avdtp_set_device_disconnect(sink->session, TRUE);
+
+ /* cancel pending connect */
+ if (sink->connect_id > 0) {
+ a2dp_cancel(sink->connect_id);
+ sink->connect_id = 0;
+ btd_service_connecting_complete(sink->service, -ECANCELED);
+
+ avdtp_unref(sink->session);
+ sink->session = NULL;
+
+ return 0;
+ }
+
+ /* disconnect already ongoing */
+ if (sink->disconnect_id > 0)
+ return -EBUSY;
+
+ if (!sink->stream)
+ return -ENOTCONN;
+
+ return avdtp_close(sink->session, sink->stream, FALSE);
+}
+
+unsigned int sink_add_state_cb(struct btd_service *service, sink_state_cb cb,
+ void *user_data)
+{
+ struct sink_state_callback *state_cb;
+ static unsigned int id = 0;
+
+ state_cb = g_new(struct sink_state_callback, 1);
+ state_cb->cb = cb;
+ state_cb->service = service;
+ state_cb->user_data = user_data;
+ state_cb->id = ++id;
+
+ sink_callbacks = g_slist_append(sink_callbacks, state_cb);
+
+ return state_cb->id;
+}
+
+gboolean sink_remove_state_cb(unsigned int id)
+{
+ GSList *l;
+
+ for (l = sink_callbacks; l != NULL; l = l->next) {
+ struct sink_state_callback *cb = l->data;
+ if (cb && cb->id == id) {
+ sink_callbacks = g_slist_remove(sink_callbacks, cb);
+ g_free(cb);
+ return TRUE;
+ }
+ }
+
+ return FALSE;
+}