/* * * BlueZ - Bluetooth protocol stack for Linux * * Copyright (C) 2006-2007 Nokia Corporation * Copyright (C) 2004-2009 Marcel Holtmann * Copyright (C) 2009-2010 Motorola Inc. * * 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 #endif #include #include #include #include #include #include #include #include "log.h" #include "device.h" #include "avdtp.h" #include "a2dp.h" #include "error.h" #include "sink.h" #include "dbus-common.h" #include "../src/adapter.h" #include "../src/device.h" #define STREAM_SETUP_RETRY_TIMER 2 struct pending_request { DBusConnection *conn; DBusMessage *msg; unsigned int id; }; struct sink { struct audio_device *dev; struct avdtp *session; struct avdtp_stream *stream; unsigned int cb_id; guint dc_id; guint retry_id; avdtp_session_state_t session_state; avdtp_state_t stream_state; sink_state_t state; struct pending_request *connect; struct pending_request *disconnect; DBusConnection *conn; }; struct sink_state_callback { sink_state_cb cb; void *user_data; unsigned int id; }; static GSList *sink_callbacks = NULL; static unsigned int avdtp_callback_id = 0; static const char *state2str(sink_state_t state) { switch (state) { case SINK_STATE_DISCONNECTED: return "disconnected"; case SINK_STATE_CONNECTING: return "connecting"; case SINK_STATE_CONNECTED: return "connected"; case SINK_STATE_PLAYING: return "playing"; default: error("Invalid sink state %d", state); return NULL; } } static void sink_set_state(struct audio_device *dev, sink_state_t new_state) { struct sink *sink = dev->sink; const char *state_str; sink_state_t old_state = sink->state; GSList *l; sink->state = new_state; state_str = state2str(new_state); if (state_str) emit_property_changed(dev->conn, dev->path, AUDIO_SINK_INTERFACE, "State", DBUS_TYPE_STRING, &state_str); for (l = sink_callbacks; l != NULL; l = l->next) { struct sink_state_callback *cb = l->data; cb->cb(dev, old_state, new_state, cb->user_data); } } static void avdtp_state_callback(struct audio_device *dev, struct avdtp *session, avdtp_session_state_t old_state, avdtp_session_state_t new_state, void *user_data) { struct sink *sink = dev->sink; if (sink == NULL) return; switch (new_state) { case AVDTP_SESSION_STATE_DISCONNECTED: if (sink->state != SINK_STATE_CONNECTING) { gboolean value = FALSE; g_dbus_emit_signal(dev->conn, dev->path, AUDIO_SINK_INTERFACE, "Disconnected", DBUS_TYPE_INVALID); emit_property_changed(dev->conn, dev->path, AUDIO_SINK_INTERFACE, "Connected", DBUS_TYPE_BOOLEAN, &value); if (sink->dc_id) { device_remove_disconnect_watch(dev->btd_dev, sink->dc_id); sink->dc_id = 0; } } sink_set_state(dev, SINK_STATE_DISCONNECTED); break; case AVDTP_SESSION_STATE_CONNECTING: sink_set_state(dev, SINK_STATE_CONNECTING); break; case AVDTP_SESSION_STATE_CONNECTED: break; } sink->session_state = new_state; } static void pending_request_free(struct audio_device *dev, struct pending_request *pending) { if (pending->conn) dbus_connection_unref(pending->conn); if (pending->msg) dbus_message_unref(pending->msg); if (pending->id) a2dp_cancel(dev, pending->id); g_free(pending); } static void disconnect_cb(struct btd_device *btd_dev, gboolean removal, void *user_data) { struct audio_device *device = user_data; struct sink *sink = device->sink; DBG("Sink: disconnect %s", device->path); avdtp_close(sink->session, sink->stream, TRUE); } 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 audio_device *dev = user_data; struct sink *sink = dev->sink; gboolean value; if (err) return; switch (new_state) { case AVDTP_STATE_IDLE: if (sink->disconnect) { DBusMessage *reply; struct pending_request *p; p = sink->disconnect; sink->disconnect = NULL; reply = dbus_message_new_method_return(p->msg); g_dbus_send_message(p->conn, reply); pending_request_free(dev, p); } if (sink->dc_id) { device_remove_disconnect_watch(dev->btd_dev, sink->dc_id); sink->dc_id = 0; } if (sink->session) { avdtp_unref(sink->session); sink->session = NULL; } sink->stream = NULL; sink->cb_id = 0; break; case AVDTP_STATE_OPEN: if (old_state == AVDTP_STATE_CONFIGURED && sink->state == SINK_STATE_CONNECTING) { value = TRUE; g_dbus_emit_signal(dev->conn, dev->path, AUDIO_SINK_INTERFACE, "Connected", DBUS_TYPE_INVALID); emit_property_changed(dev->conn, dev->path, AUDIO_SINK_INTERFACE, "Connected", DBUS_TYPE_BOOLEAN, &value); sink->dc_id = device_add_disconnect_watch(dev->btd_dev, disconnect_cb, dev, NULL); } else if (old_state == AVDTP_STATE_STREAMING) { value = FALSE; g_dbus_emit_signal(dev->conn, dev->path, AUDIO_SINK_INTERFACE, "Stopped", DBUS_TYPE_INVALID); emit_property_changed(dev->conn, dev->path, AUDIO_SINK_INTERFACE, "Playing", DBUS_TYPE_BOOLEAN, &value); } sink_set_state(dev, SINK_STATE_CONNECTED); break; case AVDTP_STATE_STREAMING: value = TRUE; g_dbus_emit_signal(dev->conn, dev->path, AUDIO_SINK_INTERFACE, "Playing", DBUS_TYPE_INVALID); emit_property_changed(dev->conn, dev->path, AUDIO_SINK_INTERFACE, "Playing", DBUS_TYPE_BOOLEAN, &value); sink_set_state(dev, SINK_STATE_PLAYING); break; case AVDTP_STATE_CONFIGURED: case AVDTP_STATE_CLOSING: case AVDTP_STATE_ABORTING: default: break; } sink->stream_state = new_state; } static DBusHandlerResult error_failed(DBusConnection *conn, DBusMessage *msg, const char * desc) { return error_common_reply(conn, msg, ERROR_INTERFACE ".Failed", desc); } static gboolean stream_setup_retry(gpointer user_data) { struct sink *sink = user_data; struct pending_request *pending = sink->connect; sink->retry_id = 0; if (sink->stream_state >= AVDTP_STATE_OPEN) { DBG("Stream successfully created, after XCASE connect:connect"); if (pending->msg) { DBusMessage *reply; reply = dbus_message_new_method_return(pending->msg); g_dbus_send_message(pending->conn, reply); } } else { DBG("Stream setup failed, after XCASE connect:connect"); if (pending->msg) error_failed(pending->conn, pending->msg, "Stream setup failed"); } sink->connect = NULL; pending_request_free(sink->dev, pending); return FALSE; } 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; struct pending_request *pending; pending = sink->connect; pending->id = 0; if (stream) { DBG("Stream successfully created"); if (pending->msg) { DBusMessage *reply; reply = dbus_message_new_method_return(pending->msg); g_dbus_send_message(pending->conn, reply); } sink->connect = NULL; pending_request_free(sink->dev, pending); return; } avdtp_unref(sink->session); sink->session = NULL; if (avdtp_error_type(err) == AVDTP_ERROR_ERRNO && avdtp_error_posix_errno(err) != EHOSTDOWN) { DBG("connect:connect XCASE detected"); sink->retry_id = g_timeout_add_seconds(STREAM_SETUP_RETRY_TIMER, stream_setup_retry, sink); } else { if (pending->msg) error_failed(pending->conn, pending->msg, "Stream setup failed"); sink->connect = NULL; pending_request_free(sink->dev, pending); DBG("Stream setup failed : %s", avdtp_strerror(err)); } } static uint8_t default_bitpool(uint8_t freq, uint8_t mode) { switch (freq) { case SBC_SAMPLING_FREQ_16000: case SBC_SAMPLING_FREQ_32000: return 53; case SBC_SAMPLING_FREQ_44100: switch (mode) { case SBC_CHANNEL_MODE_MONO: case SBC_CHANNEL_MODE_DUAL_CHANNEL: return 31; case SBC_CHANNEL_MODE_STEREO: case SBC_CHANNEL_MODE_JOINT_STEREO: return 53; default: error("Invalid channel mode %u", mode); return 53; } case SBC_SAMPLING_FREQ_48000: switch (mode) { case SBC_CHANNEL_MODE_MONO: case SBC_CHANNEL_MODE_DUAL_CHANNEL: return 29; case SBC_CHANNEL_MODE_STEREO: case SBC_CHANNEL_MODE_JOINT_STEREO: return 51; default: error("Invalid channel mode %u", mode); return 51; } default: error("Invalid sampling freq %u", freq); return 53; } } static gboolean select_sbc_params(struct sbc_codec_cap *cap, struct sbc_codec_cap *supported) { unsigned int max_bitpool, min_bitpool; memset(cap, 0, sizeof(struct sbc_codec_cap)); cap->cap.media_type = AVDTP_MEDIA_TYPE_AUDIO; cap->cap.media_codec_type = A2DP_CODEC_SBC; if (supported->frequency & SBC_SAMPLING_FREQ_44100) cap->frequency = SBC_SAMPLING_FREQ_44100; else if (supported->frequency & SBC_SAMPLING_FREQ_48000) cap->frequency = SBC_SAMPLING_FREQ_48000; else if (supported->frequency & SBC_SAMPLING_FREQ_32000) cap->frequency = SBC_SAMPLING_FREQ_32000; else if (supported->frequency & SBC_SAMPLING_FREQ_16000) cap->frequency = SBC_SAMPLING_FREQ_16000; else { error("No supported frequencies"); return FALSE; } if (supported->channel_mode & SBC_CHANNEL_MODE_JOINT_STEREO) cap->channel_mode = SBC_CHANNEL_MODE_JOINT_STEREO; else if (supported->channel_mode & SBC_CHANNEL_MODE_STEREO) cap->channel_mode = SBC_CHANNEL_MODE_STEREO; else if (supported->channel_mode & SBC_CHANNEL_MODE_DUAL_CHANNEL) cap->channel_mode = SBC_CHANNEL_MODE_DUAL_CHANNEL; else if (supported->channel_mode & SBC_CHANNEL_MODE_MONO) cap->channel_mode = SBC_CHANNEL_MODE_MONO; else { error("No supported channel modes"); return FALSE; } if (supported->block_length & SBC_BLOCK_LENGTH_16) cap->block_length = SBC_BLOCK_LENGTH_16; else if (supported->block_length & SBC_BLOCK_LENGTH_12) cap->block_length = SBC_BLOCK_LENGTH_12; else if (supported->block_length & SBC_BLOCK_LENGTH_8) cap->block_length = SBC_BLOCK_LENGTH_8; else if (supported->block_length & SBC_BLOCK_LENGTH_4) cap->block_length = SBC_BLOCK_LENGTH_4; else { error("No supported block lengths"); return FALSE; } if (supported->subbands & SBC_SUBBANDS_8) cap->subbands = SBC_SUBBANDS_8; else if (supported->subbands & SBC_SUBBANDS_4) cap->subbands = SBC_SUBBANDS_4; else { error("No supported subbands"); return FALSE; } if (supported->allocation_method & SBC_ALLOCATION_LOUDNESS) cap->allocation_method = SBC_ALLOCATION_LOUDNESS; else if (supported->allocation_method & SBC_ALLOCATION_SNR) cap->allocation_method = SBC_ALLOCATION_SNR; min_bitpool = MAX(MIN_BITPOOL, supported->min_bitpool); max_bitpool = MIN(default_bitpool(cap->frequency, cap->channel_mode), supported->max_bitpool); cap->min_bitpool = min_bitpool; cap->max_bitpool = max_bitpool; return TRUE; } static gboolean select_capabilities(struct avdtp *session, struct avdtp_remote_sep *rsep, GSList **caps) { struct avdtp_service_capability *media_transport, *media_codec; struct sbc_codec_cap sbc_cap; media_codec = avdtp_get_codec(rsep); if (!media_codec) return FALSE; select_sbc_params(&sbc_cap, (struct sbc_codec_cap *) media_codec->data); media_transport = avdtp_service_cap_new(AVDTP_MEDIA_TRANSPORT, NULL, 0); *caps = g_slist_append(*caps, media_transport); media_codec = avdtp_service_cap_new(AVDTP_MEDIA_CODEC, &sbc_cap, sizeof(sbc_cap)); *caps = g_slist_append(*caps, media_codec); if (avdtp_get_delay_reporting(rsep)) { struct avdtp_service_capability *delay_reporting; delay_reporting = avdtp_service_cap_new(AVDTP_DELAY_REPORTING, NULL, 0); *caps = g_slist_append(*caps, delay_reporting); } return TRUE; } static void discovery_complete(struct avdtp *session, GSList *seps, struct avdtp_error *err, void *user_data) { struct sink *sink = user_data; struct pending_request *pending; struct avdtp_local_sep *lsep; struct avdtp_remote_sep *rsep; struct a2dp_sep *sep; GSList *caps = NULL; int id; pending = sink->connect; if (err) { avdtp_unref(sink->session); sink->session = NULL; if (avdtp_error_type(err) == AVDTP_ERROR_ERRNO && avdtp_error_posix_errno(err) != EHOSTDOWN) { DBG("connect:connect XCASE detected"); sink->retry_id = g_timeout_add_seconds(STREAM_SETUP_RETRY_TIMER, stream_setup_retry, sink); } else goto failed; return; } DBG("Discovery complete"); if (avdtp_get_seps(session, AVDTP_SEP_TYPE_SINK, AVDTP_MEDIA_TYPE_AUDIO, A2DP_CODEC_SBC, &lsep, &rsep) < 0) { error("No matching ACP and INT SEPs found"); goto failed; } if (!select_capabilities(session, rsep, &caps)) { error("Unable to select remote SEP capabilities"); goto failed; } sep = a2dp_get(session, rsep); if (!sep) { error("Unable to get a local source SEP"); goto failed; } id = a2dp_config(sink->session, sep, stream_setup_complete, caps, sink); if (id == 0) goto failed; pending->id = id; return; failed: if (pending->msg) error_failed(pending->conn, pending->msg, "Stream setup failed"); pending_request_free(sink->dev, pending); sink->connect = NULL; avdtp_unref(sink->session); sink->session = NULL; } gboolean sink_setup_stream(struct sink *sink, struct avdtp *session) { if (sink->connect || sink->disconnect) return FALSE; if (session && !sink->session) sink->session = avdtp_ref(session); if (!sink->session) return FALSE; avdtp_set_auto_disconnect(sink->session, FALSE); if (avdtp_discover(sink->session, discovery_complete, sink) < 0) return FALSE; sink->connect = g_new0(struct pending_request, 1); return TRUE; } static DBusMessage *sink_connect(DBusConnection *conn, DBusMessage *msg, void *data) { struct audio_device *dev = data; struct sink *sink = dev->sink; struct pending_request *pending; if (!sink->session) sink->session = avdtp_get(&dev->src, &dev->dst); if (!sink->session) return g_dbus_create_error(msg, ERROR_INTERFACE ".Failed", "Unable to get a session"); if (sink->connect || sink->disconnect) return g_dbus_create_error(msg, ERROR_INTERFACE ".Failed", "%s", strerror(EBUSY)); if (sink->stream_state >= AVDTP_STATE_OPEN) return g_dbus_create_error(msg, ERROR_INTERFACE ".AlreadyConnected", "Device Already Connected"); if (!sink_setup_stream(sink, NULL)) return g_dbus_create_error(msg, ERROR_INTERFACE ".Failed", "Failed to create a stream"); dev->auto_connect = FALSE; pending = sink->connect; pending->conn = dbus_connection_ref(conn); pending->msg = dbus_message_ref(msg); DBG("stream creation in progress"); return NULL; } static DBusMessage *sink_disconnect(DBusConnection *conn, DBusMessage *msg, void *data) { struct audio_device *device = data; struct sink *sink = device->sink; struct pending_request *pending; int err; if (!sink->session) return g_dbus_create_error(msg, ERROR_INTERFACE ".NotConnected", "Device not Connected"); if (sink->connect || sink->disconnect) return g_dbus_create_error(msg, ERROR_INTERFACE ".Failed", "%s", strerror(EBUSY)); if (sink->stream_state < AVDTP_STATE_OPEN) { DBusMessage *reply = dbus_message_new_method_return(msg); if (!reply) return NULL; avdtp_unref(sink->session); sink->session = NULL; return reply; } err = avdtp_close(sink->session, sink->stream, FALSE); if (err < 0) return g_dbus_create_error(msg, ERROR_INTERFACE ".Failed", "%s", strerror(-err)); pending = g_new0(struct pending_request, 1); pending->conn = dbus_connection_ref(conn); pending->msg = dbus_message_ref(msg); sink->disconnect = pending; return NULL; } static DBusMessage *sink_suspend(DBusConnection *conn, DBusMessage *msg, void *data) { struct audio_device *device = data; struct sink *sink = device->sink; struct pending_request *pending; int err; if (!sink->session) return g_dbus_create_error(msg, ERROR_INTERFACE ".NotConnected", "Device not Connected"); if (sink->connect || sink->disconnect) return g_dbus_create_error(msg, ERROR_INTERFACE ".Failed", "%s", strerror(EBUSY)); if (sink->state < AVDTP_STATE_OPEN) { DBusMessage *reply = dbus_message_new_method_return(msg); if (!reply) return NULL; avdtp_unref(sink->session); sink->session = NULL; return reply; } err = avdtp_suspend(sink->session, sink->stream); if (err < 0) return g_dbus_create_error(msg, ERROR_INTERFACE ".Failed", "%s", strerror(-err)); return NULL; } static DBusMessage *sink_resume(DBusConnection *conn, DBusMessage *msg, void *data) { struct audio_device *device = data; struct sink *sink = device->sink; struct pending_request *pending; int err; if (!sink->session) return g_dbus_create_error(msg, ERROR_INTERFACE ".NotConnected", "Device not Connected"); if (sink->connect || sink->disconnect) return g_dbus_create_error(msg, ERROR_INTERFACE ".Failed", "%s", strerror(EBUSY)); if (sink->state < AVDTP_STATE_OPEN) { DBusMessage *reply = dbus_message_new_method_return(msg); if (!reply) return NULL; avdtp_unref(sink->session); sink->session = NULL; return reply; } err = avdtp_start(sink->session, sink->stream); if (err < 0) return g_dbus_create_error(msg, ERROR_INTERFACE ".Failed", "%s", strerror(-err)); return NULL; } static DBusMessage *sink_is_connected(DBusConnection *conn, DBusMessage *msg, void *data) { struct audio_device *device = data; struct sink *sink = device->sink; DBusMessage *reply; dbus_bool_t connected; reply = dbus_message_new_method_return(msg); if (!reply) return NULL; connected = (sink->stream_state >= AVDTP_STATE_CONFIGURED); dbus_message_append_args(reply, DBUS_TYPE_BOOLEAN, &connected, DBUS_TYPE_INVALID); return reply; } static DBusMessage *sink_get_properties(DBusConnection *conn, DBusMessage *msg, void *data) { struct audio_device *device = data; struct sink *sink = device->sink; DBusMessage *reply; DBusMessageIter iter; DBusMessageIter dict; const char *state; gboolean value; reply = dbus_message_new_method_return(msg); if (!reply) return NULL; dbus_message_iter_init_append(reply, &iter); dbus_message_iter_open_container(&iter, 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, &dict); /* Playing */ value = (sink->stream_state == AVDTP_STATE_STREAMING); dict_append_entry(&dict, "Playing", DBUS_TYPE_BOOLEAN, &value); /* Connected */ value = (sink->stream_state >= AVDTP_STATE_CONFIGURED); dict_append_entry(&dict, "Connected", DBUS_TYPE_BOOLEAN, &value); /* State */ state = state2str(sink->state); if (state) dict_append_entry(&dict, "State", DBUS_TYPE_STRING, &state); dbus_message_iter_close_container(&iter, &dict); return reply; } static GDBusMethodTable sink_methods[] = { { "Connect", "", "", sink_connect, G_DBUS_METHOD_FLAG_ASYNC }, { "Disconnect", "", "", sink_disconnect, G_DBUS_METHOD_FLAG_ASYNC }, { "Suspend", "", "", sink_suspend, G_DBUS_METHOD_FLAG_ASYNC }, { "Resume", "", "", sink_resume, G_DBUS_METHOD_FLAG_ASYNC }, { "IsConnected", "", "b", sink_is_connected, G_DBUS_METHOD_FLAG_DEPRECATED }, { "GetProperties", "", "a{sv}",sink_get_properties }, { NULL, NULL, NULL, NULL } }; static GDBusSignalTable sink_signals[] = { { "Connected", "", G_DBUS_SIGNAL_FLAG_DEPRECATED }, { "Disconnected", "", G_DBUS_SIGNAL_FLAG_DEPRECATED }, { "Playing", "", G_DBUS_SIGNAL_FLAG_DEPRECATED }, { "Stopped", "", G_DBUS_SIGNAL_FLAG_DEPRECATED }, { "PropertyChanged", "sv" }, { NULL, NULL } }; static void sink_free(struct audio_device *dev) { struct sink *sink = dev->sink; if (sink->cb_id) avdtp_stream_remove_cb(sink->session, sink->stream, sink->cb_id); if (sink->dc_id) device_remove_disconnect_watch(dev->btd_dev, sink->dc_id); if (sink->session) avdtp_unref(sink->session); if (sink->connect) pending_request_free(dev, sink->connect); if (sink->disconnect) pending_request_free(dev, sink->disconnect); if (sink->retry_id) g_source_remove(sink->retry_id); g_free(sink); dev->sink = NULL; } static void path_unregister(void *data) { struct audio_device *dev = data; DBG("Unregistered interface %s on path %s", AUDIO_SINK_INTERFACE, dev->path); sink_free(dev); } void sink_unregister(struct audio_device *dev) { g_dbus_unregister_interface(dev->conn, dev->path, AUDIO_SINK_INTERFACE); } struct sink *sink_init(struct audio_device *dev) { struct sink *sink; if (!g_dbus_register_interface(dev->conn, dev->path, AUDIO_SINK_INTERFACE, sink_methods, sink_signals, NULL, dev, path_unregister)) return NULL; DBG("Registered interface %s on path %s", AUDIO_SINK_INTERFACE, dev->path); if (avdtp_callback_id == 0) avdtp_callback_id = avdtp_add_state_cb(avdtp_state_callback, NULL); sink = g_new0(struct sink, 1); sink->dev = dev; return sink; } gboolean sink_is_active(struct audio_device *dev) { struct sink *sink = dev->sink; if (sink->session) return TRUE; return FALSE; } gboolean sink_is_streaming(struct audio_device *dev) { struct sink *sink = dev->sink; if (sink_get_state(dev) == AVDTP_STATE_STREAMING) return TRUE; return FALSE; } avdtp_state_t sink_get_state(struct audio_device *dev) { struct sink *sink = dev->sink; return sink->stream_state; } gboolean sink_new_stream(struct audio_device *dev, struct avdtp *session, struct avdtp_stream *stream) { struct sink *sink = dev->sink; 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, dev); return TRUE; } gboolean sink_shutdown(struct sink *sink) { if (!sink->stream) return FALSE; if (avdtp_close(sink->session, sink->stream, FALSE) < 0) return FALSE; return TRUE; } unsigned int sink_add_state_cb(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->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; }