/* -*- Mode: C; c-basic-offset: 4; indent-tabs-mode: nil -*- */ /* Copyright (C) 2010 Red Hat, Inc. 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, see . */ #ifdef HAVE_CONFIG_H #include "config.h" #endif #include #include #include #include #include "spice-gstaudio.h" #include "spice-common.h" #include "spice-session.h" #define SPICE_GSTAUDIO_GET_PRIVATE(obj) \ (G_TYPE_INSTANCE_GET_PRIVATE((obj), SPICE_TYPE_GSTAUDIO, spice_gstaudio)) G_DEFINE_TYPE(SpiceGstAudio, spice_gstaudio, SPICE_TYPE_AUDIO) struct stream { GstElement *pipe; GstElement *src; GstElement *sink; guint rate; guint channels; }; struct spice_gstaudio { SpiceSession *session; SpiceChannel *pchannel; SpiceChannel *rchannel; struct stream playback; struct stream record; }; static void channel_event(SpiceChannel *channel, SpiceChannelEvent event, gpointer data); static void spice_gstaudio_finalize(GObject *obj) { spice_gstaudio *p; p = SPICE_GSTAUDIO_GET_PRIVATE(obj); G_OBJECT_CLASS(spice_gstaudio_parent_class)->finalize(obj); } void stream_dispose(struct stream *s) { if (s->pipe) { gst_element_set_state(s->pipe, GST_STATE_NULL); gst_object_unref(s->pipe); s->pipe = NULL; } if (s->src) { gst_object_unref(s->src); s->src = NULL; } if (s->sink) { gst_object_unref(s->sink); s->sink = NULL; } } static void spice_gstaudio_dispose(GObject *obj) { spice_gstaudio *p; SPICE_DEBUG("%s", __FUNCTION__); p = SPICE_GSTAUDIO_GET_PRIVATE(obj); stream_dispose(&p->playback); stream_dispose(&p->record); if (p->pchannel != NULL) { g_signal_handlers_disconnect_by_func(p->pchannel, channel_event, obj); g_object_unref(p->pchannel); p->pchannel = NULL; } if (p->rchannel != NULL) { g_signal_handlers_disconnect_by_func(p->rchannel, channel_event, obj); g_object_unref(p->rchannel); p->rchannel = NULL; } } static void spice_gstaudio_init(SpiceGstAudio *pulse) { spice_gstaudio *p; p = pulse->priv = SPICE_GSTAUDIO_GET_PRIVATE(pulse); memset(p, 0, sizeof(*p)); } static void spice_gstaudio_class_init(SpiceGstAudioClass *klass) { GObjectClass *gobject_class = G_OBJECT_CLASS(klass); gobject_class->finalize = spice_gstaudio_finalize; gobject_class->dispose = spice_gstaudio_dispose; g_type_class_add_private(klass, sizeof(spice_gstaudio)); } static void record_new_buffer(GstAppSink *appsink, gpointer data) { SpiceGstAudio *gstaudio = data; spice_gstaudio *p = gstaudio->priv; GstMessage *msg; g_return_if_fail(p != NULL); msg = gst_message_new_application(GST_OBJECT(p->record.pipe), NULL); gst_element_post_message(p->record.pipe, msg); } static void record_stop(SpiceRecordChannel *channel, gpointer data) { SpiceGstAudio *gstaudio = data; spice_gstaudio *p = gstaudio->priv; SPICE_DEBUG("%s", __FUNCTION__); if (p->record.pipe) gst_element_set_state(p->record.pipe, GST_STATE_READY); } static gboolean record_bus_cb(GstBus *bus, GstMessage *msg, gpointer data) { SpiceGstAudio *gstaudio = data; spice_gstaudio *p = gstaudio->priv; g_return_val_if_fail(p != NULL, FALSE); switch (GST_MESSAGE_TYPE(msg)) { case GST_MESSAGE_APPLICATION: { GstBuffer *b; b = gst_app_sink_pull_buffer(GST_APP_SINK(p->record.sink)); if (!b) { if (!gst_app_sink_is_eos(GST_APP_SINK(p->record.sink))) g_warning("eos not reached, but can't pull new buffer"); return TRUE; } spice_record_send_data(SPICE_RECORD_CHANNEL(p->rchannel), /* FIXME: server side doesn't care about ts? what is the unit? ms apparently */ GST_BUFFER_DATA(b), GST_BUFFER_SIZE(b), 0); break; } default: break; } return TRUE; } static void record_start(SpiceRecordChannel *channel, gint format, gint channels, gint frequency, gpointer data) { SpiceGstAudio *gstaudio = data; spice_gstaudio *p = gstaudio->priv; g_return_if_fail(p != NULL); g_return_if_fail(format == SPICE_AUDIO_FMT_S16); if (p->record.pipe && (p->record.rate != frequency || p->record.channels != channels)) { record_stop(channel, data); gst_object_unref(p->record.pipe); p->record.pipe = NULL; } if (!p->record.pipe) { GError *error = NULL; GstBus *bus; gchar *audio_caps = g_strdup_printf("audio/x-raw-int,channels=%d,rate=%d,signed=(boolean)true," "width=16,depth=16,endianness=1234", channels, frequency); gchar *pipeline = g_strdup_printf("autoaudiosrc name=audiosrc ! queue ! audioconvert ! audioresample ! " "appsink caps=\"%s\" name=appsink", audio_caps); p->record.pipe = gst_parse_launch(pipeline, &error); if (p->record.pipe == NULL) { g_warning("Failed to create pipeline: %s", error->message); goto lerr; } bus = gst_pipeline_get_bus(GST_PIPELINE(p->record.pipe)); gst_bus_add_watch(bus, record_bus_cb, data); gst_object_unref(GST_OBJECT(bus)); p->record.src = gst_bin_get_by_name(GST_BIN(p->record.pipe), "audiosrc"); p->record.sink = gst_bin_get_by_name(GST_BIN(p->record.pipe), "appsink"); p->record.rate = frequency; p->record.channels = channels; gst_app_sink_set_emit_signals(GST_APP_SINK(p->record.sink), TRUE); g_signal_connect(p->record.sink, "new-buffer", G_CALLBACK(record_new_buffer), data); lerr: g_clear_error(&error); g_free(audio_caps); g_free(pipeline); } if (p->record.pipe) gst_element_set_state(p->record.pipe, GST_STATE_PLAYING); } static void channel_event(SpiceChannel *channel, SpiceChannelEvent event, gpointer data) { SpiceGstAudio *gstaudio = data; spice_gstaudio *p = gstaudio->priv; switch (event) { case SPICE_CHANNEL_OPENED: break; case SPICE_CHANNEL_CLOSED: if (channel == p->pchannel) { p->pchannel = NULL; g_object_unref(channel); } else if (channel == p->rchannel) { record_stop(SPICE_RECORD_CHANNEL(channel), gstaudio); p->rchannel = NULL; g_object_unref(channel); } else /* if (p->pchannel || p->rchannel) */ g_warn_if_reached(); break; default: break; } } static void playback_stop(SpicePlaybackChannel *channel, gpointer data) { SpiceGstAudio *gstaudio = data; spice_gstaudio *p = SPICE_GSTAUDIO_GET_PRIVATE(gstaudio); if (p->playback.pipe) gst_element_set_state(p->playback.pipe, GST_STATE_READY); } static void playback_start(SpicePlaybackChannel *channel, gint format, gint channels, gint frequency, gpointer data) { SpiceGstAudio *gstaudio = data; spice_gstaudio *p = SPICE_GSTAUDIO_GET_PRIVATE(gstaudio); g_return_if_fail(p != NULL); g_return_if_fail(format == SPICE_AUDIO_FMT_S16); if (p->playback.pipe && (p->playback.rate != frequency || p->playback.channels != channels)) { playback_stop(channel, data); gst_object_unref(p->playback.pipe); p->playback.pipe = NULL; } if (!p->playback.pipe) { GError *error = NULL; gchar *audio_caps = g_strdup_printf("audio/x-raw-int,channels=%d,rate=%d,signed=(boolean)true," "width=16,depth=16,endianness=1234", channels, frequency); gchar *pipeline = g_strdup_printf("appsrc is-live=1 do-timestamp=1 caps=\"%s\" name=\"appsrc\" ! queue ! " "audioconvert ! audioresample ! autoaudiosink name=\"audiosink\"", audio_caps); p->playback.pipe = gst_parse_launch(pipeline, &error); if (p->playback.pipe == NULL) { g_warning("Failed to create pipeline: %s", error->message); goto lerr; } p->playback.src = gst_bin_get_by_name(GST_BIN(p->playback.pipe), "appsrc"); p->playback.sink = gst_bin_get_by_name(GST_BIN(p->playback.pipe), "audiosink"); p->playback.rate = frequency; p->playback.channels = channels; lerr: g_clear_error(&error); g_free(audio_caps); g_free(pipeline); } if (p->playback.pipe) gst_element_set_state(p->playback.pipe, GST_STATE_PLAYING); } static void playback_data(SpicePlaybackChannel *channel, gpointer *audio, gint size, gpointer data) { SpiceGstAudio *gstaudio = data; spice_gstaudio *p = SPICE_GSTAUDIO_GET_PRIVATE(gstaudio); GstBuffer *buf; g_return_if_fail(p != NULL); audio = g_memdup(audio, size); /* TODO: try to avoid memory copy */ buf = gst_app_buffer_new(audio, size, g_free, audio); gst_app_src_push_buffer(GST_APP_SRC(p->playback.src), buf); } static void channel_new(SpiceSession *s, SpiceChannel *channel, gpointer data) { SpiceGstAudio *gstaudio = data; spice_gstaudio *p = gstaudio->priv; if (SPICE_IS_PLAYBACK_CHANNEL(channel)) { g_return_if_fail(p->pchannel == NULL); p->pchannel = g_object_ref(channel); g_signal_connect(channel, "playback-start", G_CALLBACK(playback_start), gstaudio); g_signal_connect(channel, "playback-data", G_CALLBACK(playback_data), gstaudio); g_signal_connect(channel, "playback-stop", G_CALLBACK(playback_stop), gstaudio); g_signal_connect(channel, "channel-event", G_CALLBACK(channel_event), gstaudio); spice_channel_connect(channel); } if (SPICE_IS_RECORD_CHANNEL(channel)) { g_return_if_fail(p->rchannel == NULL); p->rchannel = g_object_ref(channel); g_signal_connect(channel, "record-start", G_CALLBACK(record_start), gstaudio); g_signal_connect(channel, "record-stop", G_CALLBACK(record_stop), gstaudio); g_signal_connect(channel, "channel-event", G_CALLBACK(channel_event), gstaudio); spice_channel_connect(channel); } } SpiceGstAudio *spice_gstaudio_new(SpiceSession *session, GMainContext *context, const char *name) { SpiceGstAudio *gstaudio; spice_gstaudio *p; GList *list; gst_init(NULL, NULL); gstaudio = g_object_new(SPICE_TYPE_GSTAUDIO, NULL); p = SPICE_GSTAUDIO_GET_PRIVATE(gstaudio); p->session = g_object_ref(session); g_signal_connect(session, "channel-new", G_CALLBACK(channel_new), gstaudio); list = spice_session_get_channels(session); for (list = g_list_first(list); list != NULL; list = g_list_next(list)) { channel_new(session, list->data, (gpointer)gstaudio); } g_list_free(list); return gstaudio; }