diff options
Diffstat (limited to 'audio/gsta2dpsink.c')
-rw-r--r-- | audio/gsta2dpsink.c | 701 |
1 files changed, 701 insertions, 0 deletions
diff --git a/audio/gsta2dpsink.c b/audio/gsta2dpsink.c new file mode 100644 index 00000000..d25cbded --- /dev/null +++ b/audio/gsta2dpsink.c @@ -0,0 +1,701 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2004-2008 Marcel Holtmann <marcel@holtmann.org> + * + * + * This library 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. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; 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 <unistd.h> +#include <pthread.h> + +#include "gsta2dpsink.h" + +GST_DEBUG_CATEGORY_STATIC(gst_a2dp_sink_debug); +#define GST_CAT_DEFAULT gst_a2dp_sink_debug + +#define A2DP_SBC_RTP_PAYLOAD_TYPE 1 +#define TEMPLATE_MAX_BITPOOL_STR "64" + +#define DEFAULT_AUTOCONNECT TRUE + +enum { + PROP_0, + PROP_DEVICE, + PROP_AUTOCONNECT +}; + +GST_BOILERPLATE(GstA2dpSink, gst_a2dp_sink, GstBin, GST_TYPE_BIN); + +static const GstElementDetails gst_a2dp_sink_details = + GST_ELEMENT_DETAILS("Bluetooth A2DP sink", + "Sink/Audio", + "Plays audio to an A2DP device", + "Marcel Holtmann <marcel@holtmann.org>"); + +static GstStaticPadTemplate gst_a2dp_sink_factory = + GST_STATIC_PAD_TEMPLATE("sink", GST_PAD_SINK, GST_PAD_ALWAYS, + GST_STATIC_CAPS("audio/x-sbc, " + "rate = (int) { 16000, 32000, 44100, 48000 }, " + "channels = (int) [ 1, 2 ], " + "mode = (string) { \"mono\", \"dual\", \"stereo\", \"joint\" }, " + "blocks = (int) { 4, 8, 12, 16 }, " + "subbands = (int) { 4, 8 }, " + "allocation = (string) { \"snr\", \"loudness\" }, " + "bitpool = (int) [ 2, " + TEMPLATE_MAX_BITPOOL_STR " ]; " + "audio/mpeg" + )); + +static gboolean gst_a2dp_sink_handle_event(GstPad *pad, GstEvent *event); +static gboolean gst_a2dp_sink_set_caps(GstPad *pad, GstCaps *caps); +static GstCaps *gst_a2dp_sink_get_caps(GstPad *pad); +static gboolean gst_a2dp_sink_init_caps_filter(GstA2dpSink *self); +static gboolean gst_a2dp_sink_init_fakesink(GstA2dpSink *self); +static gboolean gst_a2dp_sink_remove_fakesink(GstA2dpSink *self); + +static void gst_a2dp_sink_finalize(GObject *obj) +{ + GstA2dpSink *self = GST_A2DP_SINK(obj); + + g_mutex_free(self->cb_mutex); + + G_OBJECT_CLASS (parent_class)->finalize (obj); +} + +static GstState gst_a2dp_sink_get_state(GstA2dpSink *self) +{ + GstState current, pending; + + gst_element_get_state(GST_ELEMENT(self), ¤t, &pending, 0); + if (pending == GST_STATE_VOID_PENDING) + return current; + + return pending; +} + +/* + * Helper function to create elements, add to the bin and link it + * to another element. + */ +static GstElement* gst_a2dp_sink_init_element(GstA2dpSink *self, + const gchar* elementname, const gchar* name, + GstElement *link_to) +{ + GstElement *element; + GstState state; + + GST_LOG_OBJECT(self, "Initializing %s", elementname); + + element = gst_element_factory_make(elementname, name); + if (element == NULL) { + GST_DEBUG_OBJECT(self, "Couldn't create %s", elementname); + return NULL; + } + + if (!gst_bin_add(GST_BIN(self), element)) { + GST_DEBUG_OBJECT(self, "failed to add %s to the bin", + elementname); + goto cleanup_and_fail; + } + + state = gst_a2dp_sink_get_state(self); + if (gst_element_set_state(element, state) == + GST_STATE_CHANGE_FAILURE) { + GST_DEBUG_OBJECT(self, "%s failed to go to playing", + elementname); + goto remove_element_and_fail; + } + + if (link_to != NULL) + if (!gst_element_link(link_to, element)) { + GST_DEBUG_OBJECT(self, "couldn't link %s", + elementname); + goto remove_element_and_fail; + } + + return element; + +remove_element_and_fail: + gst_element_set_state(element, GST_STATE_NULL); + gst_bin_remove(GST_BIN(self), element); + return NULL; + +cleanup_and_fail: + if (element != NULL) + g_object_unref(G_OBJECT(element)); + + return NULL; +} + +static void gst_a2dp_sink_base_init(gpointer g_class) +{ + GstElementClass *element_class = GST_ELEMENT_CLASS(g_class); + + gst_element_class_set_details(element_class, + &gst_a2dp_sink_details); + gst_element_class_add_pad_template(element_class, + gst_static_pad_template_get(&gst_a2dp_sink_factory)); +} + +static void gst_a2dp_sink_set_property(GObject *object, guint prop_id, + const GValue *value, GParamSpec *pspec) +{ + GstA2dpSink *self = GST_A2DP_SINK(object); + + switch (prop_id) { + case PROP_DEVICE: + if (self->sink != NULL) + gst_avdtp_sink_set_device(self->sink, + g_value_get_string(value)); + + if (self->device != NULL) + g_free(self->device); + self->device = g_value_dup_string(value); + break; + + case PROP_AUTOCONNECT: + self->autoconnect = g_value_get_boolean(value); + + if (self->sink != NULL) + g_object_set(G_OBJECT(self->sink), "auto-connect", + self->autoconnect, NULL); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); + break; + } +} + +static void gst_a2dp_sink_get_property(GObject *object, guint prop_id, + GValue *value, GParamSpec *pspec) +{ + GstA2dpSink *self = GST_A2DP_SINK(object); + gchar *device; + + switch (prop_id) { + case PROP_DEVICE: + if (self->sink != NULL) { + device = gst_avdtp_sink_get_device(self->sink); + if (device != NULL) + g_value_take_string(value, device); + } + break; + case PROP_AUTOCONNECT: + if (self->sink != NULL) + g_object_get(G_OBJECT(self->sink), "auto-connect", + &self->autoconnect, NULL); + + g_value_set_boolean(value, self->autoconnect); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); + break; + } +} + +static gboolean gst_a2dp_sink_init_ghost_pad(GstA2dpSink *self) +{ + GstPad *capsfilter_pad; + + /* we search for the capsfilter sinkpad */ + capsfilter_pad = gst_element_get_static_pad(self->capsfilter, "sink"); + + /* now we add a ghostpad */ + self->ghostpad = GST_GHOST_PAD(gst_ghost_pad_new("sink", + capsfilter_pad)); + g_object_unref(capsfilter_pad); + + /* the getcaps of our ghostpad must reflect the device caps */ + gst_pad_set_getcaps_function(GST_PAD(self->ghostpad), + gst_a2dp_sink_get_caps); + self->ghostpad_setcapsfunc = GST_PAD_SETCAPSFUNC(self->ghostpad); + gst_pad_set_setcaps_function(GST_PAD(self->ghostpad), + GST_DEBUG_FUNCPTR(gst_a2dp_sink_set_caps)); + + /* we need to handle events on our own and we also need the eventfunc + * of the ghostpad for forwarding calls */ + self->ghostpad_eventfunc = GST_PAD_EVENTFUNC(GST_PAD(self->ghostpad)); + gst_pad_set_event_function(GST_PAD(self->ghostpad), + gst_a2dp_sink_handle_event); + + if (!gst_element_add_pad(GST_ELEMENT(self), GST_PAD(self->ghostpad))) + GST_ERROR_OBJECT(self, "failed to add ghostpad"); + + return TRUE; +} + +static void gst_a2dp_sink_remove_dynamic_elements(GstA2dpSink *self) +{ + if (self->rtp) { + GST_LOG_OBJECT(self, "removing rtp element from the bin"); + if (!gst_bin_remove(GST_BIN(self), GST_ELEMENT(self->rtp))) + GST_WARNING_OBJECT(self, "failed to remove rtp " + "element from bin"); + else + self->rtp = NULL; + } +} + +static GstStateChangeReturn gst_a2dp_sink_change_state(GstElement *element, + GstStateChange transition) +{ + GstStateChangeReturn ret = GST_STATE_CHANGE_SUCCESS; + GstA2dpSink *self = GST_A2DP_SINK(element); + + switch (transition) { + case GST_STATE_CHANGE_READY_TO_PAUSED: + self->taglist = gst_tag_list_new(); + + gst_a2dp_sink_init_fakesink(self); + break; + + case GST_STATE_CHANGE_NULL_TO_READY: + self->sink_is_in_bin = FALSE; + self->sink = GST_AVDTP_SINK(gst_element_factory_make( + "avdtpsink", "avdtpsink")); + if (self->sink == NULL) { + GST_WARNING_OBJECT(self, "failed to create avdtpsink"); + return GST_STATE_CHANGE_FAILURE; + } + + if (self->device != NULL) + gst_avdtp_sink_set_device(self->sink, + self->device); + + g_object_set(G_OBJECT(self->sink), "auto-connect", + self->autoconnect, NULL); + + ret = gst_element_set_state(GST_ELEMENT(self->sink), + GST_STATE_READY); + break; + default: + break; + } + + if (ret == GST_STATE_CHANGE_FAILURE) + return ret; + + ret = GST_ELEMENT_CLASS(parent_class)->change_state(element, + transition); + + switch (transition) { + case GST_STATE_CHANGE_PAUSED_TO_READY: + if (self->taglist) { + gst_tag_list_free(self->taglist); + self->taglist = NULL; + } + if (self->newseg_event != NULL) { + gst_event_unref(self->newseg_event); + self->newseg_event = NULL; + } + gst_a2dp_sink_remove_fakesink(self); + break; + + case GST_STATE_CHANGE_READY_TO_NULL: + if (self->sink_is_in_bin) { + if (!gst_bin_remove(GST_BIN(self), + GST_ELEMENT(self->sink))) + GST_WARNING_OBJECT(self, "Failed to remove " + "avdtpsink from bin"); + } else if (self->sink != NULL) { + gst_element_set_state(GST_ELEMENT(self->sink), + GST_STATE_NULL); + g_object_unref(G_OBJECT(self->sink)); + } + + self->sink = NULL; + + gst_a2dp_sink_remove_dynamic_elements(self); + break; + default: + break; + } + + return ret; +} + +static void gst_a2dp_sink_class_init(GstA2dpSinkClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS(klass); + GstElementClass *element_class = GST_ELEMENT_CLASS(klass); + + parent_class = g_type_class_peek_parent(klass); + + object_class->set_property = GST_DEBUG_FUNCPTR( + gst_a2dp_sink_set_property); + object_class->get_property = GST_DEBUG_FUNCPTR( + gst_a2dp_sink_get_property); + + object_class->finalize = GST_DEBUG_FUNCPTR( + gst_a2dp_sink_finalize); + + element_class->change_state = GST_DEBUG_FUNCPTR( + gst_a2dp_sink_change_state); + + g_object_class_install_property(object_class, PROP_DEVICE, + g_param_spec_string("device", "Device", + "Bluetooth remote device address", + NULL, G_PARAM_READWRITE)); + + g_object_class_install_property(object_class, PROP_AUTOCONNECT, + g_param_spec_boolean("auto-connect", "Auto-connect", + "Automatically attempt to connect to device", + DEFAULT_AUTOCONNECT, G_PARAM_READWRITE)); + + GST_DEBUG_CATEGORY_INIT(gst_a2dp_sink_debug, "a2dpsink", 0, + "A2DP sink element"); +} + +GstCaps *gst_a2dp_sink_get_device_caps(GstA2dpSink *self) +{ + return gst_avdtp_sink_get_device_caps(self->sink); +} + +static GstCaps *gst_a2dp_sink_get_caps(GstPad *pad) +{ + GstCaps *caps; + GstCaps *caps_aux; + GstA2dpSink *self = GST_A2DP_SINK(GST_PAD_PARENT(pad)); + + if (self->sink == NULL) { + GST_DEBUG_OBJECT(self, "a2dpsink isn't initialized " + "returning template caps"); + caps = gst_static_pad_template_get_caps( + &gst_a2dp_sink_factory); + } else { + GST_LOG_OBJECT(self, "Getting device caps"); + caps = gst_a2dp_sink_get_device_caps(self); + if (caps == NULL) + caps = gst_static_pad_template_get_caps( + &gst_a2dp_sink_factory); + } + caps_aux = gst_caps_copy(caps); + g_object_set(self->capsfilter, "caps", caps_aux, NULL); + gst_caps_unref(caps_aux); + return caps; +} + +static gboolean gst_a2dp_sink_init_avdtp_sink(GstA2dpSink *self) +{ + GstElement *sink; + + /* check if we don't need a new sink */ + if (self->sink_is_in_bin) + return TRUE; + + if (self->sink == NULL) + sink = gst_element_factory_make("avdtpsink", "avdtpsink"); + else + sink = GST_ELEMENT(self->sink); + + if (sink == NULL) { + GST_ERROR_OBJECT(self, "Couldn't create avdtpsink"); + return FALSE; + } + + if (!gst_bin_add(GST_BIN(self), sink)) { + GST_ERROR_OBJECT(self, "failed to add avdtpsink " + "to the bin"); + goto cleanup_and_fail; + } + + if (gst_element_set_state(sink, GST_STATE_READY) == + GST_STATE_CHANGE_FAILURE) { + GST_ERROR_OBJECT(self, "avdtpsink failed to go to ready"); + goto remove_element_and_fail; + } + + if (!gst_element_link(GST_ELEMENT(self->rtp), sink)) { + GST_ERROR_OBJECT(self, "couldn't link rtpsbcpay " + "to avdtpsink"); + goto remove_element_and_fail; + } + + self->sink = GST_AVDTP_SINK(sink); + self->sink_is_in_bin = TRUE; + g_object_set(G_OBJECT(self->sink), "device", self->device, NULL); + + gst_element_set_state(sink, GST_STATE_PAUSED); + + return TRUE; + +remove_element_and_fail: + gst_element_set_state (sink, GST_STATE_NULL); + gst_bin_remove(GST_BIN(self), sink); + return FALSE; + +cleanup_and_fail: + if (sink != NULL) + g_object_unref(G_OBJECT(sink)); + + return FALSE; +} + +static gboolean gst_a2dp_sink_init_rtp_sbc_element(GstA2dpSink *self) +{ + GstElement *rtppay; + + /* if we already have a rtp, we don't need a new one */ + if (self->rtp != NULL) + return TRUE; + + rtppay = gst_a2dp_sink_init_element(self, "rtpsbcpay", "rtp", + self->capsfilter); + if (rtppay == NULL) + return FALSE; + + self->rtp = GST_BASE_RTP_PAYLOAD(rtppay); + g_object_set(G_OBJECT(self->rtp), "min-frames", -1, NULL); + + gst_element_set_state(rtppay, GST_STATE_PAUSED); + + return TRUE; +} + +static gboolean gst_a2dp_sink_init_rtp_mpeg_element(GstA2dpSink *self) +{ + GstElement *rtppay; + + /* check if we don't need a new rtp */ + if (self->rtp) + return TRUE; + + GST_LOG_OBJECT(self, "Initializing rtp mpeg element"); + /* if capsfilter is not created then we can't have our rtp element */ + if (self->capsfilter == NULL) + return FALSE; + + rtppay = gst_a2dp_sink_init_element(self, "rtpmpapay", "rtp", + self->capsfilter); + if (rtppay == NULL) + return FALSE; + + self->rtp = GST_BASE_RTP_PAYLOAD(rtppay); + + gst_element_set_state(rtppay, GST_STATE_PAUSED); + + return TRUE; +} + +static gboolean gst_a2dp_sink_init_dynamic_elements(GstA2dpSink *self, + GstCaps *caps) +{ + GstStructure *structure; + GstEvent *event; + GstPad *capsfilterpad; + gboolean crc; + gchar *mode = NULL; + + structure = gst_caps_get_structure(caps, 0); + + /* before everything we need to remove fakesink */ + gst_a2dp_sink_remove_fakesink(self); + + /* first, we need to create our rtp payloader */ + if (gst_structure_has_name(structure, "audio/x-sbc")) { + GST_LOG_OBJECT(self, "sbc media received"); + if (!gst_a2dp_sink_init_rtp_sbc_element(self)) + return FALSE; + } else if (gst_structure_has_name(structure, "audio/mpeg")) { + GST_LOG_OBJECT(self, "mp3 media received"); + if (!gst_a2dp_sink_init_rtp_mpeg_element(self)) + return FALSE; + } else { + GST_ERROR_OBJECT(self, "Unexpected media type"); + return FALSE; + } + + if (!gst_a2dp_sink_init_avdtp_sink(self)) + return FALSE; + + /* check if we should push the taglist FIXME should we push this? + * we can send the tags directly if needed */ + if (self->taglist != NULL && + gst_structure_has_name(structure, "audio/mpeg")) { + + event = gst_event_new_tag(self->taglist); + + /* send directly the crc */ + if (gst_tag_list_get_boolean(self->taglist, "has-crc", &crc)) + gst_avdtp_sink_set_crc(self->sink, crc); + + if (gst_tag_list_get_string(self->taglist, "channel-mode", + &mode)) + gst_avdtp_sink_set_channel_mode(self->sink, mode); + + capsfilterpad = gst_ghost_pad_get_target(self->ghostpad); + gst_pad_send_event(capsfilterpad, event); + self->taglist = NULL; + g_free(mode); + } + + if (!gst_avdtp_sink_set_device_caps(self->sink, caps)) + return FALSE; + + g_object_set(G_OBJECT(self->rtp), "mtu", + gst_avdtp_sink_get_link_mtu(self->sink), NULL); + + /* we forward our new segment here if we have one */ + if (self->newseg_event) { + gst_pad_send_event(GST_BASE_RTP_PAYLOAD_SINKPAD(self->rtp), + self->newseg_event); + self->newseg_event = NULL; + } + + return TRUE; +} + +static gboolean gst_a2dp_sink_set_caps(GstPad *pad, GstCaps *caps) +{ + GstA2dpSink *self; + + self = GST_A2DP_SINK(GST_PAD_PARENT(pad)); + GST_INFO_OBJECT(self, "setting caps"); + + /* now we know the caps */ + gst_a2dp_sink_init_dynamic_elements(self, caps); + + return self->ghostpad_setcapsfunc(GST_PAD(self->ghostpad), caps); +} + +/* used for catching newsegment events while we don't have a sink, for + * later forwarding it to the sink */ +static gboolean gst_a2dp_sink_handle_event(GstPad *pad, GstEvent *event) +{ + GstA2dpSink *self; + GstTagList *taglist = NULL; + GstObject *parent; + + self = GST_A2DP_SINK(GST_PAD_PARENT(pad)); + parent = gst_element_get_parent(GST_ELEMENT(self->sink)); + + if (GST_EVENT_TYPE(event) == GST_EVENT_NEWSEGMENT && + parent != GST_OBJECT_CAST(self)) { + if (self->newseg_event != NULL) + gst_event_unref(self->newseg_event); + self->newseg_event = gst_event_ref(event); + + } else if (GST_EVENT_TYPE(event) == GST_EVENT_TAG && + parent != GST_OBJECT_CAST(self)) { + if (self->taglist == NULL) + gst_event_parse_tag(event, &self->taglist); + else { + gst_event_parse_tag(event, &taglist); + gst_tag_list_insert(self->taglist, taglist, + GST_TAG_MERGE_REPLACE); + } + } + + if (parent != NULL) + gst_object_unref(GST_OBJECT(parent)); + + return self->ghostpad_eventfunc(GST_PAD(self->ghostpad), event); +} + +static gboolean gst_a2dp_sink_init_caps_filter(GstA2dpSink *self) +{ + GstElement *element; + + element = gst_element_factory_make("capsfilter", "filter"); + if (element == NULL) + goto failed; + + if (!gst_bin_add(GST_BIN(self), element)) + goto failed; + + self->capsfilter = element; + return TRUE; + +failed: + GST_ERROR_OBJECT(self, "Failed to initialize caps filter"); + return FALSE; +} + +static gboolean gst_a2dp_sink_init_fakesink(GstA2dpSink *self) +{ + if (self->fakesink != NULL) + return TRUE; + + g_mutex_lock (self->cb_mutex); + self->fakesink = gst_a2dp_sink_init_element(self, "fakesink", + "fakesink", self->capsfilter); + g_mutex_unlock (self->cb_mutex); + + if (!self->fakesink) + return FALSE; + + return TRUE; +} + +static gboolean gst_a2dp_sink_remove_fakesink(GstA2dpSink *self) +{ + g_mutex_lock(self->cb_mutex); + + if (self->fakesink != NULL) { + gst_element_set_locked_state(self->fakesink, TRUE); + gst_element_set_state(self->fakesink, GST_STATE_NULL); + + gst_bin_remove(GST_BIN(self), self->fakesink); + self->fakesink = NULL; + } + + g_mutex_unlock(self->cb_mutex); + + return TRUE; +} + +static void gst_a2dp_sink_init(GstA2dpSink *self, + GstA2dpSinkClass *klass) +{ + self->sink = NULL; + self->fakesink = NULL; + self->rtp = NULL; + self->device = NULL; + self->autoconnect = DEFAULT_AUTOCONNECT; + self->capsfilter = NULL; + self->newseg_event = NULL; + self->taglist = NULL; + self->ghostpad = NULL; + self->sink_is_in_bin = FALSE; + + self->cb_mutex = g_mutex_new(); + + /* we initialize our capsfilter */ + gst_a2dp_sink_init_caps_filter(self); + g_object_set(self->capsfilter, "caps", + gst_static_pad_template_get_caps(&gst_a2dp_sink_factory), + NULL); + + gst_a2dp_sink_init_fakesink(self); + + gst_a2dp_sink_init_ghost_pad(self); + +} + +gboolean gst_a2dp_sink_plugin_init (GstPlugin * plugin) +{ + return gst_element_register (plugin, "a2dpsink", + GST_RANK_PRIMARY, GST_TYPE_A2DP_SINK); +} + |