/* purple * * Purple is the legal property of its developers, whose names are too numerous * to list here. Please refer to the COPYRIGHT file distributed with this * source distribution. * * 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 Street, Fifth Floor, Boston, MA 02111-1301 USA */ #include #include "account.h" #include "debug.h" #include "glibcompat.h" #include "media.h" #include "mediamanager.h" #include "purplepath.h" #include "media-gst.h" #include typedef struct _PurpleMediaOutputWindow PurpleMediaOutputWindow; typedef struct _PurpleMediaElementInfoPrivate PurpleMediaElementInfoPrivate; struct _PurpleMediaOutputWindow { gulong id; PurpleMedia *media; gchar *session_id; gchar *participant; GstElement *sink; }; typedef struct { GstElement *pipeline; PurpleMediaCaps ui_caps; GList *medias; GList *private_medias; GList *elements; GList *output_windows; gulong next_output_window_id; GType backend_type; GstCaps *video_caps; PurpleMediaElementInfo *video_src; PurpleMediaElementInfo *video_sink; PurpleMediaElementInfo *audio_src; PurpleMediaElementInfo *audio_sink; GstDeviceMonitor *device_monitor; /* Application data streams */ GList *appdata_info; /* holds PurpleMediaAppDataInfo */ GMutex appdata_mutex; guint appdata_cb_token; /* last used read/write callback token */ } PurpleMediaManagerPrivate; /** * PurpleMediaManager: * * The media manager's data. */ struct _PurpleMediaManager { GObject parent; /*< private >*/ PurpleMediaManagerPrivate *priv; }; typedef struct { PurpleMedia *media; GWeakRef media_ref; gchar *session_id; gchar *participant; PurpleMediaAppDataCallbacks callbacks; gpointer user_data; GDestroyNotify notify; GstAppSrc *appsrc; GstAppSink *appsink; gint num_samples; GstSample *current_sample; guint sample_offset; gboolean writable; gboolean connected; guint writable_cb_token; guint readable_cb_token; guint writable_timer_id; guint readable_timer_id; GCond readable_cond; } PurpleMediaAppDataInfo; static void purple_media_manager_finalize (GObject *object); static void free_appdata_info_locked (PurpleMediaAppDataInfo *info); static void purple_media_manager_init_device_monitor(PurpleMediaManager *manager); static void purple_media_manager_register_static_elements(PurpleMediaManager *manager); enum { INIT_MEDIA, INIT_PRIVATE_MEDIA, UI_CAPS_CHANGED, ELEMENTS_CHANGED, LAST_SIGNAL }; static guint purple_media_manager_signals[LAST_SIGNAL] = {0}; G_DEFINE_TYPE_WITH_PRIVATE(PurpleMediaManager, purple_media_manager, G_TYPE_OBJECT); static void purple_media_manager_class_init (PurpleMediaManagerClass *klass) { GObjectClass *gobject_class = (GObjectClass*)klass; gobject_class->finalize = purple_media_manager_finalize; purple_media_manager_signals[INIT_MEDIA] = g_signal_new ("init-media", G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL, G_TYPE_BOOLEAN, 3, PURPLE_TYPE_MEDIA, G_TYPE_POINTER, G_TYPE_STRING); purple_media_manager_signals[INIT_PRIVATE_MEDIA] = g_signal_new ("init-private-media", G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL, G_TYPE_BOOLEAN, 3, PURPLE_TYPE_MEDIA, G_TYPE_POINTER, G_TYPE_STRING); purple_media_manager_signals[UI_CAPS_CHANGED] = g_signal_new ("ui-caps-changed", G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL, G_TYPE_NONE, 2, PURPLE_MEDIA_TYPE_CAPS, PURPLE_MEDIA_TYPE_CAPS); purple_media_manager_signals[ELEMENTS_CHANGED] = g_signal_new("elements-changed", G_TYPE_FROM_CLASS(klass), G_SIGNAL_RUN_LAST | G_SIGNAL_DETAILED, 0, NULL, NULL, NULL, G_TYPE_NONE, 0); } static void purple_media_manager_init (PurpleMediaManager *media) { GError *error = NULL; media->priv = purple_media_manager_get_instance_private(media); media->priv->medias = NULL; media->priv->private_medias = NULL; media->priv->next_output_window_id = 1; media->priv->appdata_info = NULL; g_mutex_init (&media->priv->appdata_mutex); if (gst_init_check(NULL, NULL, &error)) { purple_media_manager_register_static_elements(media); purple_media_manager_init_device_monitor(media); } else { purple_debug_error("mediamanager", "GStreamer failed to initialize: %s.", error ? error->message : ""); g_clear_error(&error); } purple_prefs_add_none("/purple/media"); purple_prefs_add_none("/purple/media/audio"); purple_prefs_add_int("/purple/media/audio/silence_threshold", 5); purple_prefs_add_none("/purple/media/audio/volume"); purple_prefs_add_int("/purple/media/audio/volume/input", 10); purple_prefs_add_int("/purple/media/audio/volume/output", 10); } static void purple_media_manager_finalize (GObject *media) { PurpleMediaManagerPrivate *priv = purple_media_manager_get_instance_private( PURPLE_MEDIA_MANAGER(media)); g_list_free_full(priv->medias, g_object_unref); g_list_free_full(priv->private_medias, g_object_unref); g_list_free_full(priv->elements, g_object_unref); g_clear_pointer(&priv->video_caps, gst_caps_unref); g_clear_list(&priv->appdata_info, (GDestroyNotify)free_appdata_info_locked); g_mutex_clear (&priv->appdata_mutex); if (priv->device_monitor) { gst_device_monitor_stop(priv->device_monitor); g_object_unref(priv->device_monitor); } G_OBJECT_CLASS(purple_media_manager_parent_class)->finalize(media); } PurpleMediaManager * purple_media_manager_get(void) { static PurpleMediaManager *manager = NULL; if (manager == NULL) { manager = PURPLE_MEDIA_MANAGER(g_object_new(purple_media_manager_get_type(), NULL)); } return manager; } static gboolean pipeline_bus_call(G_GNUC_UNUSED GstBus *bus, GstMessage *msg, G_GNUC_UNUSED PurpleMediaManager *manager) { switch(GST_MESSAGE_TYPE(msg)) { case GST_MESSAGE_EOS: purple_debug_info("mediamanager", "End of Stream"); break; case GST_MESSAGE_ERROR: { gchar *debug = NULL; GError *err = NULL; gst_message_parse_error(msg, &err, &debug); purple_debug_error("mediamanager", "gst pipeline error: %s", err->message); g_error_free(err); if (debug) { purple_debug_error("mediamanager", "Debug details: %s", debug); g_free(debug); } break; } default: break; } return TRUE; } GstElement * purple_media_manager_get_pipeline(PurpleMediaManager *manager) { g_return_val_if_fail(PURPLE_IS_MEDIA_MANAGER(manager), NULL); if (manager->priv->pipeline == NULL) { GstBus *bus; manager->priv->pipeline = gst_pipeline_new(NULL); bus = gst_pipeline_get_bus( GST_PIPELINE(manager->priv->pipeline)); gst_bus_add_signal_watch(GST_BUS(bus)); g_signal_connect(G_OBJECT(bus), "message", G_CALLBACK(pipeline_bus_call), manager); gst_bus_set_sync_handler(bus, gst_bus_sync_signal_handler, NULL, NULL); gst_object_unref(bus); gst_element_set_state(manager->priv->pipeline, GST_STATE_PLAYING); } return manager->priv->pipeline; } static PurpleMedia * create_media(PurpleMediaManager *manager, PurpleAccount *account, const char *conference_type, const char *remote_user, gboolean initiator, gboolean private) { PurpleMedia *media; guint signal_id; media = PURPLE_MEDIA(g_object_new(purple_media_get_type(), "manager", manager, "account", account, "conference-type", conference_type, "initiator", initiator, NULL)); signal_id = private ? purple_media_manager_signals[INIT_PRIVATE_MEDIA] : purple_media_manager_signals[INIT_MEDIA]; if (g_signal_has_handler_pending(manager, signal_id, 0, FALSE)) { gboolean signal_ret; g_signal_emit(manager, signal_id, 0, media, account, remote_user, &signal_ret); if (signal_ret == FALSE) { g_object_unref(media); return NULL; } } if (private) { manager->priv->private_medias = g_list_append( manager->priv->private_medias, media); } else { manager->priv->medias = g_list_append(manager->priv->medias, media); } return media; } static GList * get_media(PurpleMediaManager *manager, gboolean private) { if (private) { return manager->priv->private_medias; } else { return manager->priv->medias; } } static GList * get_media_by_account(PurpleMediaManager *manager, PurpleAccount *account, gboolean private) { GList *media = NULL; GList *iter; PurpleAccount *media_account; g_return_val_if_fail(PURPLE_IS_MEDIA_MANAGER(manager), NULL); if (private) { iter = manager->priv->private_medias; } else { iter = manager->priv->medias; } for (; iter; iter = g_list_next(iter)) { media_account = purple_media_get_account(iter->data); if (media_account == account) { media = g_list_prepend(media, iter->data); } g_object_unref (media_account); } return media; } void purple_media_manager_remove_media(PurpleMediaManager *manager, PurpleMedia *media) { GList *list; GList **medias = NULL; g_return_if_fail(manager != NULL); if ((list = g_list_find(manager->priv->medias, media))) { medias = &manager->priv->medias; } else if ((list = g_list_find(manager->priv->private_medias, media))) { medias = &manager->priv->private_medias; } if (list) { *medias = g_list_delete_link(*medias, list); g_mutex_lock (&manager->priv->appdata_mutex); list = manager->priv->appdata_info; while (list) { PurpleMediaAppDataInfo *info = list->data; GList *next = list->next; if (info->media == media) { manager->priv->appdata_info = g_list_delete_link ( manager->priv->appdata_info, list); free_appdata_info_locked (info); } list = next; } g_mutex_unlock (&manager->priv->appdata_mutex); } } PurpleMedia * purple_media_manager_create_media(PurpleMediaManager *manager, PurpleAccount *account, const char *conference_type, const char *remote_user, gboolean initiator) { return create_media (manager, account, conference_type, remote_user, initiator, FALSE); } GList * purple_media_manager_get_media(PurpleMediaManager *manager) { return get_media (manager, FALSE); } GList * purple_media_manager_get_media_by_account(PurpleMediaManager *manager, PurpleAccount *account) { return get_media_by_account (manager, account, FALSE); } PurpleMedia * purple_media_manager_create_private_media(PurpleMediaManager *manager, PurpleAccount *account, const char *conference_type, const char *remote_user, gboolean initiator) { return create_media (manager, account, conference_type, remote_user, initiator, TRUE); } GList * purple_media_manager_get_private_media(PurpleMediaManager *manager) { return get_media (manager, TRUE); } GList * purple_media_manager_get_private_media_by_account(PurpleMediaManager *manager, PurpleAccount *account) { return get_media_by_account (manager, account, TRUE); } static void free_appdata_info_locked (PurpleMediaAppDataInfo *info) { GstAppSrcCallbacks null_src_cb = { .need_data = NULL, .enough_data = NULL, .seek_data = NULL, }; GstAppSinkCallbacks null_sink_cb = { .eos = NULL, .new_preroll = NULL, .new_sample = NULL, }; if (info->notify) { info->notify(info->user_data); } info->media = NULL; if (info->appsrc) { /* Will call appsrc_destroyed. */ gst_app_src_set_callbacks (info->appsrc, &null_src_cb, NULL, NULL); } if (info->appsink) { /* Will call appsink_destroyed. */ gst_app_sink_set_callbacks (info->appsink, &null_sink_cb, NULL, NULL); } /* Make sure no other thread is using the structure */ g_free (info->session_id); g_free (info->participant); /* This lets the potential read or write callbacks waiting for appdata_mutex * know the info structure has been destroyed. */ info->readable_cb_token = 0; info->writable_cb_token = 0; g_clear_handle_id(&info->readable_timer_id, g_source_remove); g_clear_handle_id(&info->writable_timer_id, g_source_remove); g_clear_pointer(&info->current_sample, gst_sample_unref); /* Unblock any reading thread before destroying the GCond */ g_cond_broadcast (&info->readable_cond); g_cond_clear (&info->readable_cond); g_slice_free (PurpleMediaAppDataInfo, info); } /* * Get an app data info struct associated with a session and lock the mutex * We don't want to return an info struct and unlock then it gets destroyed * so we need to return it with the lock still taken */ static PurpleMediaAppDataInfo * get_app_data_info_and_lock (PurpleMediaManager *manager, PurpleMedia *media, const gchar *session_id, const gchar *participant) { GList *i; g_mutex_lock (&manager->priv->appdata_mutex); for (i = manager->priv->appdata_info; i; i = i->next) { PurpleMediaAppDataInfo *info = i->data; if (info->media == media && purple_strequal (info->session_id, session_id) && (participant == NULL || purple_strequal (info->participant, participant))) { return info; } } return NULL; } /* * Get an app data info struct associated with a session and lock the mutex * if it doesn't exist, we create it. */ static PurpleMediaAppDataInfo * ensure_app_data_info_and_lock (PurpleMediaManager *manager, PurpleMedia *media, const gchar *session_id, const gchar *participant) { PurpleMediaAppDataInfo * info = get_app_data_info_and_lock (manager, media, session_id, participant); if (info == NULL) { info = g_slice_new0 (PurpleMediaAppDataInfo); info->media = media; g_weak_ref_init (&info->media_ref, media); info->session_id = g_strdup (session_id); info->participant = g_strdup (participant); g_cond_init (&info->readable_cond); manager->priv->appdata_info = g_list_prepend ( manager->priv->appdata_info, info); } return info; } static void request_pad_unlinked_cb(GstPad *pad, G_GNUC_UNUSED GstPad *peer, G_GNUC_UNUSED gpointer user_data) { GstElement *parent = GST_ELEMENT_PARENT(pad); GstIterator *iter; GValue tmp = G_VALUE_INIT; GstIteratorResult result; gst_element_release_request_pad(parent, pad); iter = gst_element_iterate_src_pads(parent); result = gst_iterator_next(iter, &tmp); if (result == GST_ITERATOR_DONE) { gst_element_set_locked_state(parent, TRUE); gst_element_set_state(parent, GST_STATE_NULL); gst_bin_remove(GST_BIN(GST_ELEMENT_PARENT(parent)), parent); } else if (result == GST_ITERATOR_OK) { g_value_reset(&tmp); } gst_iterator_free(iter); } static void nonunique_src_unlinked_cb(GstPad *pad, G_GNUC_UNUSED GstPad *peer, G_GNUC_UNUSED gpointer user_data) { GstElement *element = GST_ELEMENT_PARENT(pad); gst_element_set_locked_state(element, TRUE); gst_element_set_state(element, GST_STATE_NULL); gst_bin_remove(GST_BIN(GST_ELEMENT_PARENT(element)), element); } void purple_media_manager_set_video_caps(PurpleMediaManager *manager, GstCaps *caps) { g_clear_pointer(&manager->priv->video_caps, gst_caps_unref); manager->priv->video_caps = caps; if (manager->priv->pipeline && manager->priv->video_src) { gchar *id = purple_media_element_info_get_id(manager->priv->video_src); GstElement *src = gst_bin_get_by_name(GST_BIN(manager->priv->pipeline), id); if (src) { GstElement *capsfilter = gst_bin_get_by_name(GST_BIN(src), "protocol_video_caps"); if (capsfilter) { g_object_set(G_OBJECT(capsfilter), "caps", caps, NULL); gst_object_unref (capsfilter); } gst_object_unref (src); } g_free(id); } } GstCaps * purple_media_manager_get_video_caps(PurpleMediaManager *manager) { if (manager->priv->video_caps == NULL) { manager->priv->video_caps = gst_caps_from_string("video/x-raw," "width=[250,352], height=[200,288], framerate=[1/1,20/1]"); } return manager->priv->video_caps; } /* * Calls the appdata writable callback from the main thread. * This needs to grab the appdata lock and make sure it didn't get destroyed * before calling the callback. */ static gboolean appsrc_writable (gpointer user_data) { PurpleMediaManager *manager = purple_media_manager_get (); PurpleMediaAppDataInfo *info = user_data; void (*writable_cb) (PurpleMediaManager *manager, PurpleMedia *media, const gchar *session_id, const gchar *participant, gboolean writable, gpointer user_data); PurpleMedia *media; gchar *session_id; gchar *participant; gboolean writable; gpointer cb_data; guint *cb_token_ptr = &info->writable_cb_token; guint cb_token = *cb_token_ptr; g_mutex_lock (&manager->priv->appdata_mutex); if (cb_token == 0 || cb_token != *cb_token_ptr) { /* In case info was freed while we were waiting for the mutex to unlock * we still have a pointer to the cb_token which should still be * accessible since it's in the Glib slice allocator. It gets set to 0 * just after the timeout is canceled which happens also before the * AppDataInfo is freed, so even if that memory slice gets reused, the * cb_token would be different from its previous value (unless * extremely unlucky). So checking if the value for the cb_token changed * should be enough to prevent any kind of race condition in which the * media/AppDataInfo gets destroyed in one thread while the timeout was * triggered and is waiting on the mutex to get unlocked in this thread */ g_mutex_unlock (&manager->priv->appdata_mutex); return FALSE; } writable_cb = info->callbacks.writable; media = g_weak_ref_get (&info->media_ref); session_id = g_strdup (info->session_id); participant = g_strdup (info->participant); writable = info->writable && info->connected; cb_data = info->user_data; info->writable_cb_token = 0; g_mutex_unlock (&manager->priv->appdata_mutex); if (writable_cb && media) { writable_cb (manager, media, session_id, participant, writable, cb_data); } g_object_unref (media); g_free (session_id); g_free (participant); return FALSE; } /* * Schedule a writable callback to be called from the main thread. * We need to do this because need-data/enough-data signals from appsrc * will come from the streaming thread and we need to create * a source that we attach to the main context but we can't use * g_main_context_invoke since we need to be able to cancel the source if the * media gets destroyed. * We use a timeout source instead of idle source, so the callback gets a higher * priority */ static void call_appsrc_writable_locked (PurpleMediaAppDataInfo *info) { PurpleMediaManager *manager = purple_media_manager_get (); /* We already have a writable callback scheduled, don't create another one */ if (info->writable_cb_token || info->callbacks.writable == NULL) { return; } /* We can't use writable_timer_id as a token, because the timeout is added * into libpurple's main event loop, which runs in a different thread than * from where call_appsrc_writable_locked() was called. Consequently, the * callback may run even before g_timeout_add() returns the timer ID * to us. */ info->writable_cb_token = ++manager->priv->appdata_cb_token; info->writable_timer_id = g_timeout_add (0, appsrc_writable, info); } static void appsrc_need_data(G_GNUC_UNUSED GstAppSrc *appsrc, G_GNUC_UNUSED guint length, gpointer user_data) { PurpleMediaAppDataInfo *info = user_data; PurpleMediaManager *manager = purple_media_manager_get (); g_mutex_lock (&manager->priv->appdata_mutex); if (!info->writable) { info->writable = TRUE; /* Only signal writable if we also established a connection */ if (info->connected) { call_appsrc_writable_locked (info); } } g_mutex_unlock (&manager->priv->appdata_mutex); } static void appsrc_enough_data(G_GNUC_UNUSED GstAppSrc *appsrc, gpointer user_data) { PurpleMediaAppDataInfo *info = user_data; PurpleMediaManager *manager = purple_media_manager_get (); g_mutex_lock (&manager->priv->appdata_mutex); if (info->writable) { info->writable = FALSE; call_appsrc_writable_locked (info); } g_mutex_unlock (&manager->priv->appdata_mutex); } static gboolean appsrc_seek_data(G_GNUC_UNUSED GstAppSrc *appsrc, G_GNUC_UNUSED guint64 offset, G_GNUC_UNUSED gpointer user_data) { return FALSE; } static void appsrc_destroyed (PurpleMediaAppDataInfo *info) { PurpleMediaManager *manager; if (!info->media) { /* PurpleMediaAppDataInfo is being freed. Return at once. */ return; } manager = purple_media_manager_get (); g_mutex_lock (&manager->priv->appdata_mutex); info->appsrc = NULL; if (info->writable) { info->writable = FALSE; call_appsrc_writable_locked (info); } g_mutex_unlock (&manager->priv->appdata_mutex); } static void media_established_cb(G_GNUC_UNUSED PurpleMedia *media, G_GNUC_UNUSED const char *session_id, G_GNUC_UNUSED const char *participant, G_GNUC_UNUSED PurpleMediaCandidate *local_candidate, G_GNUC_UNUSED PurpleMediaCandidate *remote_candidate, PurpleMediaAppDataInfo *info) { PurpleMediaManager *manager = purple_media_manager_get (); g_mutex_lock (&manager->priv->appdata_mutex); info->connected = TRUE; /* We established the connection, if we were writable, then we need to * signal it now */ if (info->writable) { call_appsrc_writable_locked (info); } g_mutex_unlock (&manager->priv->appdata_mutex); } static GstElement * create_send_appsrc(G_GNUC_UNUSED PurpleMediaElementInfo *element_info, PurpleMedia *media, const char *session_id, const char *participant) { PurpleMediaManager *manager = purple_media_manager_get (); PurpleMediaAppDataInfo * info = ensure_app_data_info_and_lock (manager, media, session_id, participant); GstElement *appsrc = (GstElement *)info->appsrc; if (appsrc == NULL) { GstAppSrcCallbacks callbacks = { .need_data = appsrc_need_data, .enough_data = appsrc_enough_data, .seek_data = appsrc_seek_data, }; GstCaps *caps = gst_caps_new_empty_simple ("application/octet-stream"); appsrc = gst_element_factory_make("appsrc", NULL); info->appsrc = (GstAppSrc *)appsrc; gst_app_src_set_caps (info->appsrc, caps); gst_app_src_set_callbacks (info->appsrc, &callbacks, info, (GDestroyNotify) appsrc_destroyed); g_signal_connect (media, "candidate-pair-established", (GCallback) media_established_cb, info); gst_caps_unref (caps); } g_mutex_unlock (&manager->priv->appdata_mutex); return appsrc; } static void appsink_eos(G_GNUC_UNUSED GstAppSink *appsink, G_GNUC_UNUSED gpointer user_data) { } static GstFlowReturn appsink_new_preroll(G_GNUC_UNUSED GstAppSink *appsink, G_GNUC_UNUSED gpointer user_data) { return GST_FLOW_OK; } static gboolean appsink_readable (gpointer user_data) { PurpleMediaManager *manager = purple_media_manager_get (); PurpleMediaAppDataInfo *info = user_data; void (*readable_cb) (PurpleMediaManager *manager, PurpleMedia *media, const gchar *session_id, const gchar *participant, gpointer user_data); PurpleMedia *media; gchar *session_id; gchar *participant; gpointer cb_data; guint *cb_token_ptr = &info->readable_cb_token; guint cb_token = *cb_token_ptr; gboolean run_again = FALSE; g_mutex_lock (&manager->priv->appdata_mutex); if (cb_token == 0 || cb_token != *cb_token_ptr) { /* Avoided a race condition (see writable callback) */ g_mutex_unlock (&manager->priv->appdata_mutex); return FALSE; } if (info->callbacks.readable && (info->num_samples > 0 || info->current_sample != NULL)) { readable_cb = info->callbacks.readable; media = g_weak_ref_get (&info->media_ref); session_id = g_strdup (info->session_id); participant = g_strdup (info->participant); cb_data = info->user_data; g_mutex_unlock (&manager->priv->appdata_mutex); readable_cb(manager, media, session_id, participant, cb_data); g_mutex_lock (&manager->priv->appdata_mutex); g_object_unref (media); g_free (session_id); g_free (participant); if (cb_token != *cb_token_ptr) { /* We got cancelled */ g_mutex_unlock (&manager->priv->appdata_mutex); return FALSE; } } /* Do we still have samples? Schedule appsink_readable again. We break here * so that other events get a chance to be processed too. */ if (info->num_samples > 0 || info->current_sample != NULL) { run_again = TRUE; } else { info->readable_cb_token = 0; } g_mutex_unlock (&manager->priv->appdata_mutex); return run_again; } static void call_appsink_readable_locked (PurpleMediaAppDataInfo *info) { PurpleMediaManager *manager = purple_media_manager_get (); /* We must signal that a new sample has arrived to release blocking reads */ g_cond_broadcast (&info->readable_cond); /* We already have a writable callback scheduled, don't create another one */ if (info->readable_cb_token || info->callbacks.readable == NULL) { return; } info->readable_cb_token = ++manager->priv->appdata_cb_token; info->readable_timer_id = g_timeout_add (0, appsink_readable, info); } static GstFlowReturn appsink_new_sample(G_GNUC_UNUSED GstAppSink *appsink, gpointer user_data) { PurpleMediaManager *manager = purple_media_manager_get (); PurpleMediaAppDataInfo *info = user_data; g_mutex_lock (&manager->priv->appdata_mutex); info->num_samples++; call_appsink_readable_locked (info); g_mutex_unlock (&manager->priv->appdata_mutex); return GST_FLOW_OK; } static void appsink_destroyed (PurpleMediaAppDataInfo *info) { PurpleMediaManager *manager; if (!info->media) { /* PurpleMediaAppDataInfo is being freed. Return at once. */ return; } manager = purple_media_manager_get (); g_mutex_lock (&manager->priv->appdata_mutex); info->appsink = NULL; info->num_samples = 0; g_mutex_unlock (&manager->priv->appdata_mutex); } static GstElement * create_recv_appsink(G_GNUC_UNUSED PurpleMediaElementInfo *element_info, PurpleMedia *media, const char *session_id, const char *participant) { PurpleMediaManager *manager = purple_media_manager_get (); PurpleMediaAppDataInfo * info = ensure_app_data_info_and_lock (manager, media, session_id, participant); GstElement *appsink = (GstElement *)info->appsink; if (appsink == NULL) { GstAppSinkCallbacks callbacks = { .eos = appsink_eos, .new_preroll = appsink_new_preroll, .new_sample = appsink_new_sample, }; GstCaps *caps = gst_caps_new_empty_simple ("application/octet-stream"); appsink = gst_element_factory_make("appsink", NULL); info->appsink = (GstAppSink *)appsink; gst_app_sink_set_caps (info->appsink, caps); gst_app_sink_set_callbacks (info->appsink, &callbacks, info, (GDestroyNotify) appsink_destroyed); gst_caps_unref (caps); } g_mutex_unlock (&manager->priv->appdata_mutex); return appsink; } static PurpleMediaElementInfo * get_send_application_element_info(void) { static PurpleMediaElementInfo *info = NULL; if (info == NULL) { info = g_object_new(PURPLE_TYPE_MEDIA_ELEMENT_INFO, "id", "pidginappsrc", "name", "Pidgin Application Source", "type", PURPLE_MEDIA_ELEMENT_APPLICATION | PURPLE_MEDIA_ELEMENT_SRC | PURPLE_MEDIA_ELEMENT_ONE_SRC, "create-cb", create_send_appsrc, NULL); } return info; } static PurpleMediaElementInfo * get_recv_application_element_info(void) { static PurpleMediaElementInfo *info = NULL; if (info == NULL) { info = g_object_new(PURPLE_TYPE_MEDIA_ELEMENT_INFO, "id", "pidginappsink", "name", "Pidgin Application Sink", "type", PURPLE_MEDIA_ELEMENT_APPLICATION | PURPLE_MEDIA_ELEMENT_SINK | PURPLE_MEDIA_ELEMENT_ONE_SINK, "create-cb", create_recv_appsink, NULL); } return info; } GstElement * purple_media_manager_get_element(PurpleMediaManager *manager, PurpleMediaSessionType type, PurpleMedia *media, const gchar *session_id, const gchar *participant) { GstElement *ret = NULL; PurpleMediaElementInfo *info = NULL; PurpleMediaElementType element_type; if (type & PURPLE_MEDIA_SEND_AUDIO) { info = manager->priv->audio_src; } else if (type & PURPLE_MEDIA_RECV_AUDIO) { info = manager->priv->audio_sink; } else if (type & PURPLE_MEDIA_SEND_VIDEO) { info = manager->priv->video_src; } else if (type & PURPLE_MEDIA_RECV_VIDEO) { info = manager->priv->video_sink; } else if (type & PURPLE_MEDIA_SEND_APPLICATION) { info = get_send_application_element_info (); } else if (type & PURPLE_MEDIA_RECV_APPLICATION) { info = get_recv_application_element_info (); } if (info == NULL) { return NULL; } element_type = purple_media_element_info_get_element_type(info); if (element_type & PURPLE_MEDIA_ELEMENT_UNIQUE && element_type & PURPLE_MEDIA_ELEMENT_SRC) { GstElement *tee; GstPad *pad; GstPad *ghost; gchar *id = purple_media_element_info_get_id(info); ret = gst_bin_get_by_name(GST_BIN( purple_media_manager_get_pipeline( manager)), id); if (ret == NULL) { GstElement *bin, *fakesink; ret = purple_media_element_info_call_create(info, media, session_id, participant); bin = gst_bin_new(id); tee = gst_element_factory_make("tee", "tee"); gst_bin_add_many(GST_BIN(bin), ret, tee, NULL); if (type & PURPLE_MEDIA_SEND_VIDEO) { GstElement *videoscale; GstElement *capsfilter; videoscale = gst_element_factory_make("videoscale", NULL); capsfilter = gst_element_factory_make("capsfilter", "protocol_video_caps"); g_object_set(G_OBJECT(capsfilter), "caps", purple_media_manager_get_video_caps(manager), NULL); gst_bin_add_many(GST_BIN(bin), videoscale, capsfilter, NULL); gst_element_link_many(ret, videoscale, capsfilter, tee, NULL); } else { gst_element_link(ret, tee); } /* * This shouldn't be necessary, but it stops it from * giving a not-linked error upon destruction */ fakesink = gst_element_factory_make("fakesink", NULL); g_object_set(fakesink, "async", FALSE, "sync", FALSE, "enable-last-sample", FALSE, NULL); gst_bin_add(GST_BIN(bin), fakesink); gst_element_link(tee, fakesink); ret = bin; gst_object_ref(ret); gst_bin_add(GST_BIN(purple_media_manager_get_pipeline( manager)), ret); } g_free(id); tee = gst_bin_get_by_name(GST_BIN(ret), "tee"); #if GST_CHECK_VERSION(1, 19, 1) pad = gst_element_request_pad_simple(tee, "src_%u"); #else pad = gst_element_get_request_pad(tee, "src_%u"); #endif gst_object_unref(tee); ghost = gst_ghost_pad_new(NULL, pad); gst_object_unref(pad); g_signal_connect(GST_PAD(ghost), "unlinked", G_CALLBACK(request_pad_unlinked_cb), NULL); gst_pad_set_active(ghost, TRUE); gst_element_add_pad(ret, ghost); } else { ret = purple_media_element_info_call_create(info, media, session_id, participant); if (element_type & PURPLE_MEDIA_ELEMENT_SRC) { GstPad *pad = gst_element_get_static_pad(ret, "src"); g_signal_connect(pad, "unlinked", G_CALLBACK(nonunique_src_unlinked_cb), NULL); gst_object_unref(pad); gst_object_ref(ret); gst_bin_add(GST_BIN(purple_media_manager_get_pipeline(manager)), ret); } } if (ret == NULL) { purple_debug_error("media", "Error creating source or sink\n"); } return ret; } PurpleMediaElementInfo * purple_media_manager_get_element_info(PurpleMediaManager *manager, const gchar *id) { GList *iter; g_return_val_if_fail(PURPLE_IS_MEDIA_MANAGER(manager), NULL); g_return_val_if_fail(id != NULL, NULL); iter = manager->priv->elements; for (; iter; iter = g_list_next(iter)) { gchar *element_id = purple_media_element_info_get_id(iter->data); if (purple_strequal(element_id, id)) { g_free(element_id); g_object_ref(iter->data); return iter->data; } g_free(element_id); } return NULL; } static GQuark element_info_to_detail(PurpleMediaElementInfo *info) { PurpleMediaElementType type; type = purple_media_element_info_get_element_type(info); if (type & PURPLE_MEDIA_ELEMENT_AUDIO) { if (type & PURPLE_MEDIA_ELEMENT_SRC) { return g_quark_from_string("audiosrc"); } else if (type & PURPLE_MEDIA_ELEMENT_SINK) { return g_quark_from_string("audiosink"); } } else if (type & PURPLE_MEDIA_ELEMENT_VIDEO) { if (type & PURPLE_MEDIA_ELEMENT_SRC) { return g_quark_from_string("videosrc"); } else if (type & PURPLE_MEDIA_ELEMENT_SINK) { return g_quark_from_string("videosink"); } } return 0; } gboolean purple_media_manager_register_element(PurpleMediaManager *manager, PurpleMediaElementInfo *info) { PurpleMediaElementInfo *info2; gchar *id; GQuark detail; g_return_val_if_fail(PURPLE_IS_MEDIA_MANAGER(manager), FALSE); g_return_val_if_fail(info != NULL, FALSE); id = purple_media_element_info_get_id(info); info2 = purple_media_manager_get_element_info(manager, id); g_free(id); if (info2 != NULL) { g_object_unref(info2); return FALSE; } manager->priv->elements = g_list_prepend(manager->priv->elements, info); detail = element_info_to_detail(info); if (detail != 0) { g_signal_emit(manager, purple_media_manager_signals[ELEMENTS_CHANGED], detail); } return TRUE; } gboolean purple_media_manager_unregister_element(PurpleMediaManager *manager, const gchar *id) { PurpleMediaElementInfo *info; GQuark detail; g_return_val_if_fail(PURPLE_IS_MEDIA_MANAGER(manager), FALSE); info = purple_media_manager_get_element_info(manager, id); if (info == NULL) { g_object_unref(info); return FALSE; } if (manager->priv->audio_src == info) { manager->priv->audio_src = NULL; } if (manager->priv->audio_sink == info) { manager->priv->audio_sink = NULL; } if (manager->priv->video_src == info) { manager->priv->video_src = NULL; } if (manager->priv->video_sink == info) { manager->priv->video_sink = NULL; } detail = element_info_to_detail(info); manager->priv->elements = g_list_remove( manager->priv->elements, info); g_object_unref(info); if (detail != 0) { g_signal_emit(manager, purple_media_manager_signals[ELEMENTS_CHANGED], detail); } return TRUE; } gboolean purple_media_manager_set_active_element(PurpleMediaManager *manager, PurpleMediaElementInfo *info) { PurpleMediaElementInfo *info2; PurpleMediaElementType type; gboolean ret = FALSE; gchar *id; g_return_val_if_fail(PURPLE_IS_MEDIA_MANAGER(manager), FALSE); g_return_val_if_fail(info != NULL, FALSE); id = purple_media_element_info_get_id(info); info2 = purple_media_manager_get_element_info(manager, id); g_free(id); if (info2 == NULL) { purple_media_manager_register_element(manager, info); } else { g_object_unref(info2); } type = purple_media_element_info_get_element_type(info); if (type & PURPLE_MEDIA_ELEMENT_SRC) { if (type & PURPLE_MEDIA_ELEMENT_AUDIO) { manager->priv->audio_src = info; ret = TRUE; } if (type & PURPLE_MEDIA_ELEMENT_VIDEO) { manager->priv->video_src = info; ret = TRUE; } } if (type & PURPLE_MEDIA_ELEMENT_SINK) { if (type & PURPLE_MEDIA_ELEMENT_AUDIO) { manager->priv->audio_sink = info; ret = TRUE; } if (type & PURPLE_MEDIA_ELEMENT_VIDEO) { manager->priv->video_sink = info; ret = TRUE; } } return ret; } PurpleMediaElementInfo * purple_media_manager_get_active_element(PurpleMediaManager *manager, PurpleMediaElementType type) { g_return_val_if_fail(PURPLE_IS_MEDIA_MANAGER(manager), NULL); if (type & PURPLE_MEDIA_ELEMENT_SRC) { if (type & PURPLE_MEDIA_ELEMENT_AUDIO) { return manager->priv->audio_src; } else if (type & PURPLE_MEDIA_ELEMENT_VIDEO) { return manager->priv->video_src; } else if (type & PURPLE_MEDIA_ELEMENT_APPLICATION) { return get_send_application_element_info (); } } else if (type & PURPLE_MEDIA_ELEMENT_SINK) { if (type & PURPLE_MEDIA_ELEMENT_AUDIO) { return manager->priv->audio_sink; } else if (type & PURPLE_MEDIA_ELEMENT_VIDEO) { return manager->priv->video_sink; } else if (type & PURPLE_MEDIA_ELEMENT_APPLICATION) { return get_recv_application_element_info (); } } return NULL; } gboolean purple_media_manager_create_output_window(PurpleMediaManager *manager, PurpleMedia *media, const gchar *session_id, const gchar *participant) { GList *iter; g_return_val_if_fail(PURPLE_IS_MEDIA(media), FALSE); iter = manager->priv->output_windows; for(; iter; iter = g_list_next(iter)) { PurpleMediaOutputWindow *ow = iter->data; if (ow->sink == NULL && ow->media == media && purple_strequal(participant, ow->participant) && purple_strequal(session_id, ow->session_id)) { GstElement *queue, *convert, *scale; GstElement *tee = purple_media_get_tee(media, session_id, participant); if (tee == NULL) { continue; } queue = gst_element_factory_make("queue", NULL); convert = gst_element_factory_make("videoconvert", NULL); scale = gst_element_factory_make("videoscale", NULL); ow->sink = purple_media_manager_get_element( manager, PURPLE_MEDIA_RECV_VIDEO, ow->media, ow->session_id, ow->participant); if (participant == NULL) { /* aka this is a preview sink */ GObjectClass *klass = G_OBJECT_GET_CLASS(ow->sink); if (g_object_class_find_property(klass, "sync")) { g_object_set(G_OBJECT(ow->sink), "sync", FALSE, NULL); } if (g_object_class_find_property(klass, "async")) { g_object_set(G_OBJECT(ow->sink), "async", FALSE, NULL); } } gst_bin_add_many(GST_BIN(GST_ELEMENT_PARENT(tee)), queue, convert, scale, ow->sink, NULL); gst_element_set_state(ow->sink, GST_STATE_PLAYING); gst_element_set_state(scale, GST_STATE_PLAYING); gst_element_set_state(convert, GST_STATE_PLAYING); gst_element_set_state(queue, GST_STATE_PLAYING); gst_element_link(scale, ow->sink); gst_element_link(convert, scale); gst_element_link(queue, convert); gst_element_link(tee, queue); } } return TRUE; } gulong purple_media_manager_set_output_window(PurpleMediaManager *manager, PurpleMedia *media, const gchar *session_id, const gchar *participant) { PurpleMediaOutputWindow *output_window; g_return_val_if_fail(PURPLE_IS_MEDIA_MANAGER(manager), FALSE); g_return_val_if_fail(PURPLE_IS_MEDIA(media), FALSE); output_window = g_new0(PurpleMediaOutputWindow, 1); output_window->id = manager->priv->next_output_window_id++; output_window->media = media; output_window->session_id = g_strdup(session_id); output_window->participant = g_strdup(participant); manager->priv->output_windows = g_list_prepend( manager->priv->output_windows, output_window); if (purple_media_get_tee(media, session_id, participant) != NULL) { purple_media_manager_create_output_window(manager, media, session_id, participant); } return output_window->id; } gboolean purple_media_manager_remove_output_window(PurpleMediaManager *manager, gulong output_window_id) { PurpleMediaOutputWindow *output_window = NULL; GList *iter; g_return_val_if_fail(PURPLE_IS_MEDIA_MANAGER(manager), FALSE); iter = manager->priv->output_windows; for (; iter; iter = g_list_next(iter)) { PurpleMediaOutputWindow *ow = iter->data; if (ow->id == output_window_id) { manager->priv->output_windows = g_list_delete_link( manager->priv->output_windows, iter); output_window = ow; break; } } if (output_window == NULL) { return FALSE; } if (output_window->sink != NULL) { GstElement *element = output_window->sink; GstPad *teepad = NULL; GSList *to_remove = NULL; /* Find the tee element this output is connected to. */ while (!teepad) { GstPad *pad; GstPad *peer; GstElementFactory *factory; const gchar *factory_name; to_remove = g_slist_append(to_remove, element); pad = gst_element_get_static_pad(element, "sink"); peer = gst_pad_get_peer(pad); if (!peer) { /* Output is disconnected from the pipeline. */ gst_object_unref(pad); break; } factory = gst_element_get_factory(GST_PAD_PARENT(peer)); factory_name = gst_plugin_feature_get_name(factory); if (purple_strequal(factory_name, "tee")) { teepad = peer; } element = GST_PAD_PARENT(peer); gst_object_unref(pad); gst_object_unref(peer); } if (teepad) { gst_element_release_request_pad(GST_PAD_PARENT(teepad), teepad); } while (to_remove) { GstElement *element = to_remove->data; gst_element_set_locked_state(element, TRUE); gst_element_set_state(element, GST_STATE_NULL); gst_bin_remove(GST_BIN(GST_ELEMENT_PARENT(element)), element); to_remove = g_slist_delete_link(to_remove, to_remove); } } g_free(output_window->session_id); g_free(output_window->participant); g_free(output_window); return TRUE; } void purple_media_manager_remove_output_windows(PurpleMediaManager *manager, PurpleMedia *media, const gchar *session_id, const gchar *participant) { GList *iter; g_return_if_fail(PURPLE_IS_MEDIA(media)); iter = manager->priv->output_windows; for (; iter;) { PurpleMediaOutputWindow *ow = iter->data; iter = g_list_next(iter); if (media == ow->media && purple_strequal(session_id, ow->session_id) && purple_strequal(participant, ow->participant)) { purple_media_manager_remove_output_window( manager, ow->id); } } } void purple_media_manager_set_ui_caps(PurpleMediaManager *manager, PurpleMediaCaps caps) { PurpleMediaCaps oldcaps; g_return_if_fail(PURPLE_IS_MEDIA_MANAGER(manager)); oldcaps = manager->priv->ui_caps; manager->priv->ui_caps = caps; if (caps != oldcaps) { g_signal_emit(manager, purple_media_manager_signals[UI_CAPS_CHANGED], 0, caps, oldcaps); } } PurpleMediaCaps purple_media_manager_get_ui_caps(PurpleMediaManager *manager) { g_return_val_if_fail(PURPLE_IS_MEDIA_MANAGER(manager), PURPLE_MEDIA_CAPS_NONE); return manager->priv->ui_caps; } void purple_media_manager_set_backend_type(PurpleMediaManager *manager, GType backend_type) { g_return_if_fail(PURPLE_IS_MEDIA_MANAGER(manager)); manager->priv->backend_type = backend_type; } GType purple_media_manager_get_backend_type(PurpleMediaManager *manager) { g_return_val_if_fail(PURPLE_IS_MEDIA_MANAGER(manager), PURPLE_MEDIA_CAPS_NONE); return manager->priv->backend_type; } void purple_media_manager_set_application_data_callbacks(PurpleMediaManager *manager, PurpleMedia *media, const gchar *session_id, const gchar *participant, PurpleMediaAppDataCallbacks *callbacks, gpointer user_data, GDestroyNotify notify) { PurpleMediaAppDataInfo * info = ensure_app_data_info_and_lock (manager, media, session_id, participant); if (info->notify) { info->notify (info->user_data); } g_clear_handle_id(&info->readable_cb_token, g_source_remove); g_clear_handle_id(&info->writable_cb_token, g_source_remove); if (callbacks) { info->callbacks = *callbacks; } else { info->callbacks.writable = NULL; info->callbacks.readable = NULL; } info->user_data = user_data; info->notify = notify; call_appsrc_writable_locked (info); if (info->num_samples > 0 || info->current_sample != NULL) { call_appsink_readable_locked (info); } g_mutex_unlock (&manager->priv->appdata_mutex); } gint purple_media_manager_send_application_data ( PurpleMediaManager *manager, PurpleMedia *media, const gchar *session_id, const gchar *participant, gpointer buffer, guint size, gboolean blocking) { PurpleMediaAppDataInfo * info = get_app_data_info_and_lock (manager, media, session_id, participant); if (info && info->appsrc && info->connected) { GstBuffer *gstbuffer = gst_buffer_new_wrapped (g_memdup2(buffer, size), size); GstAppSrc *appsrc = gst_object_ref (info->appsrc); g_mutex_unlock (&manager->priv->appdata_mutex); if (gst_app_src_push_buffer (appsrc, gstbuffer) == GST_FLOW_OK) { if (blocking) { GstPad *srcpad; srcpad = gst_element_get_static_pad (GST_ELEMENT (appsrc), "src"); if (srcpad) { gst_pad_peer_query (srcpad, gst_query_new_drain ()); gst_object_unref (srcpad); } } gst_object_unref (appsrc); return size; } else { gst_object_unref (appsrc); return -1; } } g_mutex_unlock (&manager->priv->appdata_mutex); return -1; } gint purple_media_manager_receive_application_data ( PurpleMediaManager *manager, PurpleMedia *media, const gchar *session_id, const gchar *participant, gpointer buffer, guint max_size, gboolean blocking) { PurpleMediaAppDataInfo * info = get_app_data_info_and_lock (manager, media, session_id, participant); guint bytes_read = 0; if (info) { /* If we are in a blocking read, we need to loop until max_size data * is read into the buffer, if we're not, then we need to read as much * data as possible */ do { if (!info->current_sample && info->appsink && info->num_samples > 0) { info->current_sample = gst_app_sink_pull_sample (info->appsink); info->sample_offset = 0; if (info->current_sample) { info->num_samples--; } } if (info->current_sample) { GstBuffer *gstbuffer = gst_sample_get_buffer ( info->current_sample); if (gstbuffer) { GstMapInfo mapinfo; guint bytes_to_copy; gst_buffer_map (gstbuffer, &mapinfo, GST_MAP_READ); /* We must copy only the data remaining in the buffer without * overflowing the buffer */ bytes_to_copy = MIN(max_size - bytes_read, mapinfo.size - info->sample_offset); memcpy ((guint8 *)buffer + bytes_read, mapinfo.data + info->sample_offset, bytes_to_copy); gst_buffer_unmap (gstbuffer, &mapinfo); info->sample_offset += bytes_to_copy; bytes_read += bytes_to_copy; if (info->sample_offset == mapinfo.size) { gst_sample_unref (info->current_sample); info->current_sample = NULL; info->sample_offset = 0; } } else { /* In case there's no buffer in the sample (should never * happen), we need to at least unref it */ gst_sample_unref (info->current_sample); info->current_sample = NULL; info->sample_offset = 0; } } /* If blocking, wait until there's an available sample */ while (bytes_read < max_size && blocking && info->current_sample == NULL && info->num_samples == 0) { g_cond_wait (&info->readable_cond, &manager->priv->appdata_mutex); /* We've been signaled, we need to unlock and regrab the info * struct to make sure nothing changed */ g_mutex_unlock (&manager->priv->appdata_mutex); info = get_app_data_info_and_lock (manager, media, session_id, participant); if (info == NULL || info->appsink == NULL) { /* The session was destroyed while we were waiting, we * should return here */ g_mutex_unlock (&manager->priv->appdata_mutex); return bytes_read; } } } while (bytes_read < max_size && (blocking || info->num_samples > 0)); g_mutex_unlock (&manager->priv->appdata_mutex); return bytes_read; } g_mutex_unlock (&manager->priv->appdata_mutex); return -1; } static void videosink_disable_last_sample(GstElement *sink) { GObjectClass *klass = G_OBJECT_GET_CLASS(sink); if (g_object_class_find_property(klass, "enable-last-sample")) { g_object_set(sink, "enable-last-sample", FALSE, NULL); } } static PurpleMediaElementType gst_class_to_purple_element_type(const gchar *device_class) { if (purple_strequal(device_class, "Audio/Source")) { return PURPLE_MEDIA_ELEMENT_AUDIO | PURPLE_MEDIA_ELEMENT_SRC | PURPLE_MEDIA_ELEMENT_ONE_SRC | PURPLE_MEDIA_ELEMENT_UNIQUE; } else if (purple_strequal(device_class, "Audio/Sink")) { return PURPLE_MEDIA_ELEMENT_AUDIO | PURPLE_MEDIA_ELEMENT_SINK | PURPLE_MEDIA_ELEMENT_ONE_SINK; } else if (purple_strequal(device_class, "Video/Source")) { return PURPLE_MEDIA_ELEMENT_VIDEO | PURPLE_MEDIA_ELEMENT_SRC | PURPLE_MEDIA_ELEMENT_ONE_SRC | PURPLE_MEDIA_ELEMENT_UNIQUE; } else if (purple_strequal(device_class, "Video/Sink")) { return PURPLE_MEDIA_ELEMENT_VIDEO | PURPLE_MEDIA_ELEMENT_SINK | PURPLE_MEDIA_ELEMENT_ONE_SINK; } return PURPLE_MEDIA_ELEMENT_NONE; } static GstElement * gst_device_create_cb(PurpleMediaElementInfo *info, G_GNUC_UNUSED PurpleMedia *media, G_GNUC_UNUSED const char *session_id, G_GNUC_UNUSED const char *participant) { GstDevice *device; GstElement *result; PurpleMediaElementType type; device = g_object_get_data(G_OBJECT(info), "gst-device"); if (!device) { return NULL; } result = gst_device_create_element(device, NULL); if (!result) { return NULL; } type = purple_media_element_info_get_element_type(info); if ((type & PURPLE_MEDIA_ELEMENT_VIDEO) && (type & PURPLE_MEDIA_ELEMENT_SINK)) { videosink_disable_last_sample(result); } return result; } static gboolean device_is_ignored(GstDevice *device) { gboolean result = FALSE; gchar *device_class; g_return_val_if_fail(device, TRUE); device_class = gst_device_get_device_class(device); /* Ignore PulseAudio monitor audio sources since they have little use * in the context of telephony.*/ if (purple_strequal(device_class, "Audio/Source")) { GstStructure *properties; const gchar *pa_class; properties = gst_device_get_properties(device); pa_class = gst_structure_get_string(properties, "device.class"); if (purple_strequal(pa_class, "monitor")) { result = TRUE; } gst_structure_free(properties); } g_free(device_class); return result; } static void purple_media_manager_register_gst_device(PurpleMediaManager *manager, GstDevice *device) { PurpleMediaElementInfo *info; PurpleMediaElementType type; gchar *name; gchar *device_class; gchar *id; if (device_is_ignored(device)) { return; } name = gst_device_get_display_name(device); device_class = gst_device_get_device_class(device); id = g_strdup_printf("%s %s", device_class, name); type = gst_class_to_purple_element_type(device_class); info = g_object_new(PURPLE_TYPE_MEDIA_ELEMENT_INFO, "id", id, "name", name, "type", type, "create-cb", gst_device_create_cb, NULL); g_object_set_data(G_OBJECT(info), "gst-device", device); purple_media_manager_register_element(manager, info); purple_debug_info("mediamanager", "Registered %s device %s", device_class, name); g_free(name); g_free(device_class); g_free(id); } static void purple_media_manager_unregister_gst_device(PurpleMediaManager *manager, GstDevice *device) { GList *i; gchar *name; gchar *device_class; gboolean done = FALSE; name = gst_device_get_display_name(device); device_class = gst_device_get_device_class(device); for (i = manager->priv->elements; i && !done; i = i->next) { PurpleMediaElementInfo *info = i->data; GstDevice *device2; device2 = g_object_get_data(G_OBJECT(info), "gst-device"); if (device2) { gchar *name2; gchar *device_class2; name2 = gst_device_get_display_name(device2); device_class2 = gst_device_get_device_class(device2); if (purple_strequal(name, name2) && purple_strequal(device_class, device_class2)) { gchar *id; id = purple_media_element_info_get_id(info); purple_media_manager_unregister_element(manager, id); purple_debug_info("mediamanager", "Unregistered %s device %s", device_class, name); g_free(id); done = TRUE; } g_free(name2); g_free(device_class2); } } g_free(name); g_free(device_class); } static gboolean device_monitor_bus_cb(G_GNUC_UNUSED GstBus *bus, GstMessage *message, gpointer user_data) { PurpleMediaManager *manager = user_data; GstMessageType message_type; GstDevice *device; message_type = GST_MESSAGE_TYPE(message); if (message_type == GST_MESSAGE_DEVICE_ADDED) { gst_message_parse_device_added(message, &device); purple_media_manager_register_gst_device(manager, device); } else if (message_type == GST_MESSAGE_DEVICE_REMOVED) { gst_message_parse_device_removed (message, &device); purple_media_manager_unregister_gst_device(manager, device); } return G_SOURCE_CONTINUE; } static void purple_media_manager_init_device_monitor(PurpleMediaManager *manager) { GstBus *bus; GList *i; manager->priv->device_monitor = gst_device_monitor_new(); bus = gst_device_monitor_get_bus(manager->priv->device_monitor); gst_bus_add_watch (bus, device_monitor_bus_cb, manager); gst_object_unref (bus); /* This avoids warning in GStreamer logs about no filters set */ gst_device_monitor_add_filter(manager->priv->device_monitor, NULL, NULL); gst_device_monitor_start(manager->priv->device_monitor); i = gst_device_monitor_get_devices(manager->priv->device_monitor); for (; i; i = g_list_delete_link(i, i)) { GstDevice *device = i->data; purple_media_manager_register_gst_device(manager, device); gst_object_unref(device); } } GList * purple_media_manager_enumerate_elements(PurpleMediaManager *manager, PurpleMediaElementType type) { GList *result = NULL; GList *i; for (i = manager->priv->elements; i; i = i->next) { PurpleMediaElementInfo *info = i->data; PurpleMediaElementType type2; type2 = purple_media_element_info_get_element_type(info); if ((type2 & type) == type) { g_object_ref(info); result = g_list_prepend(result, info); } } return result; } static GstElement * gst_factory_make_cb(PurpleMediaElementInfo *info, G_GNUC_UNUSED PurpleMedia *media, G_GNUC_UNUSED const char *session_id, G_GNUC_UNUSED const char *participant) { gchar *id; GstElement *element; id = purple_media_element_info_get_id(info); element = gst_element_factory_make(id, NULL); g_free(id); return element; } static void autovideosink_child_added_cb(G_GNUC_UNUSED GstChildProxy *child_proxy, GObject *object, G_GNUC_UNUSED gchar *name, G_GNUC_UNUSED gpointer user_data) { videosink_disable_last_sample(GST_ELEMENT(object)); } static GstElement * default_video_sink_create_cb(G_GNUC_UNUSED PurpleMediaElementInfo *info, G_GNUC_UNUSED PurpleMedia *media, G_GNUC_UNUSED const char *session_id, G_GNUC_UNUSED const char *participant) { GstElement *videosink = gst_element_factory_make("autovideosink", NULL); g_signal_connect(videosink, "child-added", G_CALLBACK(autovideosink_child_added_cb), NULL); return videosink; } static GstElement * disabled_video_create_cb(G_GNUC_UNUSED PurpleMediaElementInfo *info, G_GNUC_UNUSED PurpleMedia *media, G_GNUC_UNUSED const char *session_id, G_GNUC_UNUSED const char *participant) { GstElement *src = gst_element_factory_make("videotestsrc", NULL); /* GST_VIDEO_TEST_SRC_BLACK */ g_object_set(src, "pattern", 2, NULL); return src; } static GstElement * test_video_create_cb(G_GNUC_UNUSED PurpleMediaElementInfo *info, G_GNUC_UNUSED PurpleMedia *media, G_GNUC_UNUSED const char *session_id, G_GNUC_UNUSED const char *participant) { GstElement *src = gst_element_factory_make("videotestsrc", NULL); g_object_set(src, "is-live", TRUE, NULL); return src; } static void purple_media_manager_register_static_elements(PurpleMediaManager *manager) { static const gchar *VIDEO_SINK_PLUGINS[] = { "gtksink", "GTK", "gtkglsink", "GTK OpenGL", /* "aasink", "AALib", Didn't work for me */ NULL }; const gchar **sinks = VIDEO_SINK_PLUGINS; /* Default auto* elements. */ purple_media_manager_register_element(manager, g_object_new(PURPLE_TYPE_MEDIA_ELEMENT_INFO, "id", "autoaudiosrc", "name", N_("Default"), "type", PURPLE_MEDIA_ELEMENT_AUDIO | PURPLE_MEDIA_ELEMENT_SRC | PURPLE_MEDIA_ELEMENT_ONE_SRC | PURPLE_MEDIA_ELEMENT_UNIQUE, "create-cb", gst_factory_make_cb, NULL)); purple_media_manager_register_element(manager, g_object_new(PURPLE_TYPE_MEDIA_ELEMENT_INFO, "id", "autoaudiosink", "name", N_("Default"), "type", PURPLE_MEDIA_ELEMENT_AUDIO | PURPLE_MEDIA_ELEMENT_SINK | PURPLE_MEDIA_ELEMENT_ONE_SINK, "create-cb", gst_factory_make_cb, NULL)); purple_media_manager_register_element(manager, g_object_new(PURPLE_TYPE_MEDIA_ELEMENT_INFO, "id", "autovideosrc", "name", N_("Default"), "type", PURPLE_MEDIA_ELEMENT_VIDEO | PURPLE_MEDIA_ELEMENT_SRC | PURPLE_MEDIA_ELEMENT_ONE_SRC | PURPLE_MEDIA_ELEMENT_UNIQUE, "create-cb", gst_factory_make_cb, NULL)); purple_media_manager_register_element(manager, g_object_new(PURPLE_TYPE_MEDIA_ELEMENT_INFO, "id", "autovideosink", "name", N_("Default"), "type", PURPLE_MEDIA_ELEMENT_VIDEO | PURPLE_MEDIA_ELEMENT_SINK | PURPLE_MEDIA_ELEMENT_ONE_SINK, "create-cb", default_video_sink_create_cb, NULL)); /* Special elements */ purple_media_manager_register_element(manager, g_object_new(PURPLE_TYPE_MEDIA_ELEMENT_INFO, "id", "audiotestsrc", /* Translators: This is a noun that refers to one * possible audio input device. The device can help the * user to check if her speakers or headphones have been * set up correctly for voice calling. */ "name", N_("Test Sound"), "type", PURPLE_MEDIA_ELEMENT_AUDIO | PURPLE_MEDIA_ELEMENT_SRC | PURPLE_MEDIA_ELEMENT_ONE_SRC, "create-cb", gst_factory_make_cb, NULL)); purple_media_manager_register_element(manager, g_object_new(PURPLE_TYPE_MEDIA_ELEMENT_INFO, "id", "disabledvideosrc", "name", N_("Disabled"), "type", PURPLE_MEDIA_ELEMENT_VIDEO | PURPLE_MEDIA_ELEMENT_SRC | PURPLE_MEDIA_ELEMENT_ONE_SINK, "create-cb", disabled_video_create_cb, NULL)); purple_media_manager_register_element(manager, g_object_new(PURPLE_TYPE_MEDIA_ELEMENT_INFO, "id", "videotestsrc", /* Translators: This is a noun that refers to one * possible video input device. The device produces * a test "monoscope" image that can help the user check * the video output has been set up correctly without * needing a webcam connected to the computer. */ "name", N_("Test Pattern"), "type", PURPLE_MEDIA_ELEMENT_VIDEO | PURPLE_MEDIA_ELEMENT_SRC | PURPLE_MEDIA_ELEMENT_ONE_SRC, "create-cb", test_video_create_cb, NULL)); for (sinks = VIDEO_SINK_PLUGINS; sinks[0]; sinks += 2) { GstElementFactory *factory; factory = gst_element_factory_find(sinks[0]); if (!factory) { continue; } purple_media_manager_register_element(manager, g_object_new(PURPLE_TYPE_MEDIA_ELEMENT_INFO, "id", sinks[0], "name", sinks[1], "type", PURPLE_MEDIA_ELEMENT_VIDEO | PURPLE_MEDIA_ELEMENT_SINK | PURPLE_MEDIA_ELEMENT_ONE_SINK, "create-cb", gst_factory_make_cb, NULL)); gst_object_unref(factory); } } /* * PurpleMediaElementType */ GType purple_media_element_type_get_type(void) { static GType type = 0; if (type == 0) { static const GFlagsValue values[] = { { PURPLE_MEDIA_ELEMENT_NONE, "PURPLE_MEDIA_ELEMENT_NONE", "none" }, { PURPLE_MEDIA_ELEMENT_AUDIO, "PURPLE_MEDIA_ELEMENT_AUDIO", "audio" }, { PURPLE_MEDIA_ELEMENT_VIDEO, "PURPLE_MEDIA_ELEMENT_VIDEO", "video" }, { PURPLE_MEDIA_ELEMENT_AUDIO_VIDEO, "PURPLE_MEDIA_ELEMENT_AUDIO_VIDEO", "audio-video" }, { PURPLE_MEDIA_ELEMENT_NO_SRCS, "PURPLE_MEDIA_ELEMENT_NO_SRCS", "no-srcs" }, { PURPLE_MEDIA_ELEMENT_ONE_SRC, "PURPLE_MEDIA_ELEMENT_ONE_SRC", "one-src" }, { PURPLE_MEDIA_ELEMENT_MULTI_SRC, "PURPLE_MEDIA_ELEMENT_MULTI_SRC", "multi-src" }, { PURPLE_MEDIA_ELEMENT_REQUEST_SRC, "PURPLE_MEDIA_ELEMENT_REQUEST_SRC", "request-src" }, { PURPLE_MEDIA_ELEMENT_NO_SINKS, "PURPLE_MEDIA_ELEMENT_NO_SINKS", "no-sinks" }, { PURPLE_MEDIA_ELEMENT_ONE_SINK, "PURPLE_MEDIA_ELEMENT_ONE_SINK", "one-sink" }, { PURPLE_MEDIA_ELEMENT_MULTI_SINK, "PURPLE_MEDIA_ELEMENT_MULTI_SINK", "multi-sink" }, { PURPLE_MEDIA_ELEMENT_REQUEST_SINK, "PURPLE_MEDIA_ELEMENT_REQUEST_SINK", "request-sink" }, { PURPLE_MEDIA_ELEMENT_UNIQUE, "PURPLE_MEDIA_ELEMENT_UNIQUE", "unique" }, { PURPLE_MEDIA_ELEMENT_SRC, "PURPLE_MEDIA_ELEMENT_SRC", "src" }, { PURPLE_MEDIA_ELEMENT_SINK, "PURPLE_MEDIA_ELEMENT_SINK", "sink" }, { PURPLE_MEDIA_ELEMENT_APPLICATION, "PURPLE_MEDIA_ELEMENT_APPLICATION", "application" }, { 0, NULL, NULL } }; type = g_flags_register_static( "PurpleMediaElementType", values); } return type; } /* * PurpleMediaElementInfo */ struct _PurpleMediaElementInfoClass { GObjectClass parent_class; }; struct _PurpleMediaElementInfo { GObject parent; }; struct _PurpleMediaElementInfoPrivate { gchar *id; gchar *name; PurpleMediaElementType type; PurpleMediaElementCreateCallback create; }; enum { PROP_0, PROP_ID, PROP_NAME, PROP_TYPE, PROP_CREATE_CB, }; G_DEFINE_TYPE_WITH_PRIVATE(PurpleMediaElementInfo, purple_media_element_info, G_TYPE_OBJECT); static void purple_media_element_info_init(PurpleMediaElementInfo *info) { PurpleMediaElementInfoPrivate *priv = purple_media_element_info_get_instance_private(info); priv->id = NULL; priv->name = NULL; priv->type = PURPLE_MEDIA_ELEMENT_NONE; priv->create = NULL; } static void purple_media_element_info_finalize(GObject *info) { PurpleMediaElementInfoPrivate *priv = purple_media_element_info_get_instance_private( PURPLE_MEDIA_ELEMENT_INFO(info)); g_free(priv->id); g_free(priv->name); G_OBJECT_CLASS(purple_media_element_info_parent_class)->finalize(info); } static void purple_media_element_info_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) { PurpleMediaElementInfoPrivate *priv; g_return_if_fail(PURPLE_IS_MEDIA_ELEMENT_INFO(object)); priv = purple_media_element_info_get_instance_private( PURPLE_MEDIA_ELEMENT_INFO(object)); switch (prop_id) { case PROP_ID: g_free(priv->id); priv->id = g_value_dup_string(value); break; case PROP_NAME: g_free(priv->name); priv->name = g_value_dup_string(value); break; case PROP_TYPE: { priv->type = g_value_get_flags(value); break; } case PROP_CREATE_CB: priv->create = g_value_get_pointer(value); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); break; } } static void purple_media_element_info_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) { PurpleMediaElementInfoPrivate *priv; g_return_if_fail(PURPLE_IS_MEDIA_ELEMENT_INFO(object)); priv = purple_media_element_info_get_instance_private( PURPLE_MEDIA_ELEMENT_INFO(object)); switch (prop_id) { case PROP_ID: g_value_set_string(value, priv->id); break; case PROP_NAME: g_value_set_string(value, priv->name); break; case PROP_TYPE: g_value_set_flags(value, priv->type); break; case PROP_CREATE_CB: g_value_set_pointer(value, priv->create); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); break; } } static void purple_media_element_info_class_init(PurpleMediaElementInfoClass *klass) { GObjectClass *gobject_class = (GObjectClass*)klass; gobject_class->finalize = purple_media_element_info_finalize; gobject_class->set_property = purple_media_element_info_set_property; gobject_class->get_property = purple_media_element_info_get_property; g_object_class_install_property(gobject_class, PROP_ID, g_param_spec_string("id", "ID", "The unique identifier of the element.", NULL, G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); g_object_class_install_property(gobject_class, PROP_NAME, g_param_spec_string("name", "Name", "The friendly/display name of this element.", NULL, G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); g_object_class_install_property(gobject_class, PROP_TYPE, g_param_spec_flags("type", "Element Type", "The type of element this is.", PURPLE_TYPE_MEDIA_ELEMENT_TYPE, PURPLE_MEDIA_ELEMENT_NONE, G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); g_object_class_install_property(gobject_class, PROP_CREATE_CB, g_param_spec_pointer("create-cb", "Create Callback", "The function called to create this element.", G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); } gchar * purple_media_element_info_get_id(PurpleMediaElementInfo *info) { gchar *id; g_return_val_if_fail(PURPLE_IS_MEDIA_ELEMENT_INFO(info), NULL); g_object_get(info, "id", &id, NULL); return id; } gchar * purple_media_element_info_get_name(PurpleMediaElementInfo *info) { gchar *name; g_return_val_if_fail(PURPLE_IS_MEDIA_ELEMENT_INFO(info), NULL); g_object_get(info, "name", &name, NULL); return name; } PurpleMediaElementType purple_media_element_info_get_element_type(PurpleMediaElementInfo *info) { PurpleMediaElementType type; g_return_val_if_fail(PURPLE_IS_MEDIA_ELEMENT_INFO(info), PURPLE_MEDIA_ELEMENT_NONE); g_object_get(info, "type", &type, NULL); return type; } GstElement * purple_media_element_info_call_create(PurpleMediaElementInfo *info, PurpleMedia *media, const gchar *session_id, const gchar *participant) { PurpleMediaElementCreateCallback create; g_return_val_if_fail(PURPLE_IS_MEDIA_ELEMENT_INFO(info), NULL); g_object_get(info, "create-cb", &create, NULL); if (create) { return create(info, media, session_id, participant); } return NULL; }