[Spice-devel] [spice-gtk PATCH v5 2/4] audio: spice-pulse implement async volume-info
Victor Toso
victortoso at redhat.com
Fri Apr 3 06:53:55 PDT 2015
Using ext-stream-restore we can get the last stream data of the
application. This is helpful to spice-pulse in order to provide
this values in case of volume-sync between client and guest.
As pulseaudio don't store volume-changes immediately, we call
pa_ext_stream_restore_write when the volume in the guest changes.
Related: https://bugzilla.redhat.com/show_bug.cgi?id=1012868
---
gtk/spice-pulse.c | 301 ++++++++++++++++++++++++++++++++++++++++++++++++++++--
1 file changed, 292 insertions(+), 9 deletions(-)
diff --git a/gtk/spice-pulse.c b/gtk/spice-pulse.c
index c583032..b12d30c 100644
--- a/gtk/spice-pulse.c
+++ b/gtk/spice-pulse.c
@@ -25,18 +25,29 @@
#include <pulse/glib-mainloop.h>
#include <pulse/pulseaudio.h>
+#include <pulse/ext-stream-restore.h>
#define SPICE_PULSE_GET_PRIVATE(obj) \
(G_TYPE_INSTANCE_GET_PRIVATE((obj), SPICE_TYPE_PULSE, SpicePulsePrivate))
+struct async_task {
+ GSimpleAsyncResult *res;
+ GAsyncReadyCallback callback;
+ gpointer user_data;
+ gboolean is_playback;
+};
+
struct stream {
- pa_sample_spec spec;
- pa_stream *stream;
- int state;
- pa_operation *uncork_op;
- pa_operation *cork_op;
- gboolean started;
- guint num_underflow;
+ pa_sample_spec spec;
+ pa_stream *stream;
+ int state;
+ pa_operation *uncork_op;
+ pa_operation *cork_op;
+ gboolean started;
+ guint num_underflow;
+ gboolean changed;
+ gchar *name;
+ pa_ext_stream_restore_info info;
};
struct _SpicePulsePrivate {
@@ -50,6 +61,8 @@ struct _SpicePulsePrivate {
struct stream record;
guint last_delay;
guint target_delay;
+ gboolean restore_stream_info;
+ GList *results;
};
G_DEFINE_TYPE(SpicePulse, spice_pulse, SPICE_TYPE_AUDIO)
@@ -77,6 +90,17 @@ static const char *context_state_names[] = {
static void stream_stop(SpicePulse *pulse, struct stream *s);
static gboolean connect_channel(SpiceAudio *audio, SpiceChannel *channel);
static void channel_weak_notified(gpointer data, GObject *where_the_object_was);
+static void pulse_stream_restore_info_async_complete(SpicePulse *pulse, const gchar *err_msg);
+static void spice_pulse_get_playback_volume_info_async(SpiceAudio *audio,
+ GAsyncReadyCallback callback, gpointer user_data);
+static gboolean spice_pulse_get_playback_volume_info_finish(SpiceAudio *audio, GAsyncResult *res,
+ gboolean *mute, guint8 *nchannels, guint16 **volume, GError **error);
+static void spice_pulse_get_record_volume_info_async(SpiceAudio *audio,
+ GAsyncReadyCallback callback, gpointer user_data);
+static gboolean spice_pulse_get_record_volume_info_finish(SpiceAudio *audio,GAsyncResult *res,
+ gboolean *mute, guint8 *nchannels, guint16 **volume, GError **error);
+static void stream_restore_read_cb(pa_context *context,
+ const pa_ext_stream_restore_info *info, int eol, void *userdata);
static void spice_pulse_finalize(GObject *obj)
{
@@ -118,6 +142,13 @@ static void spice_pulse_dispose(GObject *obj)
pa_operation_unref(p->record.cork_op);
p->record.cork_op = NULL;
+ if (p->results != NULL) {
+ pulse_stream_restore_info_async_complete(pulse, "PulseAudio is being dispose");
+ }
+
+ g_clear_pointer(&p->playback.name, g_free);
+ g_clear_pointer(&p->record.name, g_free);
+
if (p->pchannel)
g_object_weak_unref(G_OBJECT(p->pchannel), channel_weak_notified, pulse);
p->pchannel = NULL;
@@ -140,6 +171,10 @@ static void spice_pulse_class_init(SpicePulseClass *klass)
SpiceAudioClass *audio_class = SPICE_AUDIO_CLASS(klass);
audio_class->connect_channel = connect_channel;
+ audio_class->get_playback_volume_info_async = spice_pulse_get_playback_volume_info_async;
+ audio_class->get_playback_volume_info_finish = spice_pulse_get_playback_volume_info_finish;
+ audio_class->get_record_volume_info_async = spice_pulse_get_record_volume_info_async;
+ audio_class->get_record_volume_info_finish = spice_pulse_get_record_volume_info_finish;
gobject_class->finalize = spice_pulse_finalize;
gobject_class->dispose = spice_pulse_dispose;
@@ -613,6 +648,20 @@ static void playback_volume_changed(GObject *object, GParamSpec *pspec, gpointer
pa_strerror(pa_context_errno(p->context)));
else
pa_operation_unref(op);
+
+ if (p->playback.info.name != NULL) {
+ /* Store new volume values to keep db up-to-date if we need them */
+ p->playback.info.volume.channels = nchannels;
+ memcpy(p->playback.info.volume.values, v.values, sizeof(pa_volume_t) * nchannels);
+ op = pa_ext_stream_restore_write(p->context, PA_UPDATE_SET,
+ &p->playback.info, 1, 1, NULL, NULL);
+ if (!op) {
+ g_warning("pa_ext_stream_restore_write() failed: %s",
+ pa_strerror(pa_context_errno(p->context)));
+ } else {
+ pa_operation_unref(op);
+ }
+ }
}
static void playback_mute_changed(GObject *object, GParamSpec *pspec, gpointer data)
@@ -726,6 +775,20 @@ static void record_volume_changed(GObject *object, GParamSpec *pspec, gpointer d
pa_strerror(pa_context_errno(p->context)));
else
pa_operation_unref(op);
+
+ if (p->record.info.name != NULL) {
+ /* Store new volume values to keep db up-to-date if we need them */
+ p->record.info.volume.channels = nchannels;
+ memcpy(p->record.info.volume.values, v.values, sizeof(pa_volume_t) * nchannels);
+ op = pa_ext_stream_restore_write(p->context, PA_UPDATE_SET,
+ &p->record.info, 1, 1, NULL, NULL);
+ if (!op) {
+ g_warning("pa_ext_stream_restore_write() failed: %s",
+ pa_strerror(pa_context_errno(p->context)));
+ } else {
+ pa_operation_unref(op);
+ }
+ }
}
static void
@@ -812,18 +875,39 @@ static void context_state_callback(pa_context *c, void *userdata)
if (!p->playback.stream && p->playback.started)
create_playback(SPICE_PULSE(userdata));
+
+ if (p->restore_stream_info == TRUE) {
+ pa_operation *op = pa_ext_stream_restore_read(p->context,
+ stream_restore_read_cb,
+ pulse);
+ if (!op) {
+ pulse_stream_restore_info_async_complete(pulse,
+ pa_strerror(pa_context_errno(p->context)));
+ } else {
+ pa_operation_unref(op);
+ }
+ }
break;
}
case PA_CONTEXT_FAILED:
g_warning("PulseAudio context failed %s",
pa_strerror(pa_context_errno(p->context)));
- break;
+ goto context_fail;
case PA_CONTEXT_TERMINATED:
default:
SPICE_DEBUG("PulseAudio context terminated");
- break;
+ goto context_fail;
+ }
+
+ return;
+
+context_fail:
+ if (p->restore_stream_info == TRUE) {
+ const gchar *errmsg = pa_strerror(pa_context_errno(p->context));
+ errmsg = (errmsg != NULL) ? errmsg : "PulseAudio context terminated";
+ pulse_stream_restore_info_async_complete(pulse, errmsg);
}
}
@@ -849,9 +933,208 @@ SpicePulse *spice_pulse_new(SpiceSession *session, GMainContext *context,
goto error;
}
+ p->playback.name = g_strconcat("sink-input-by-application-name:",
+ g_get_application_name(), NULL);
+ p->record.name = g_strconcat("source-output-by-application-name:",
+ g_get_application_name(), NULL);
return pulse;
error:
g_object_unref(pulse);
return NULL;
}
+
+static void pulse_stream_restore_info_async_complete(SpicePulse *pulse, const gchar *err_msg)
+{
+ SpicePulsePrivate *p = pulse->priv;
+ GList *it;
+ for(it = p->results; it != NULL; it = it->next) {
+ struct async_task *task = it->data;
+ /* If we do have any err_msg, we failed it all */
+ if (err_msg != NULL) {
+ g_simple_async_result_set_op_res_gboolean(task->res, FALSE);
+ g_simple_async_result_set_error(task->res,
+ SPICE_CLIENT_ERROR,
+ SPICE_CLIENT_ERROR_FAILED,
+ "restore-info failed due %s",
+ err_msg);
+
+ /* Volume-info does not change if stream is not found */
+ } else if ((task->is_playback == TRUE && p->playback.changed == FALSE) ||
+ (task->is_playback == FALSE && p->record.changed == FALSE)) {
+ g_simple_async_result_set_op_res_gboolean(task->res, FALSE);
+ g_simple_async_result_set_error(task->res,
+ SPICE_CLIENT_ERROR,
+ SPICE_CLIENT_ERROR_FAILED,
+ "Stream not found by pulse");
+ } else {
+ g_simple_async_result_set_op_res_gboolean(task->res, TRUE);
+ }
+ g_simple_async_result_complete_in_idle(task->res);
+ g_object_unref(task->res);
+ g_free(task);
+ }
+ g_clear_pointer(&p->results, g_list_free);
+ p->restore_stream_info = FALSE;
+}
+
+static void stream_restore_read_cb(pa_context *context,
+ const pa_ext_stream_restore_info *info,
+ int eol,
+ void *userdata)
+{
+ SpicePulsePrivate *p = SPICE_PULSE(userdata)->priv;
+ struct stream *pstream = NULL;
+
+ if (eol) {
+ pulse_stream_restore_info_async_complete(SPICE_PULSE(userdata), NULL);
+ return;
+ }
+
+ if (g_strcmp0(info->name, p->playback.name) == 0) {
+ pstream = &p->playback;
+ } else if (g_strcmp0(info->name, p->record.name) == 0) {
+ pstream = &p->record;
+ } else {
+ /* This is not the stream you are looking for. */
+ return;
+ }
+
+ if (info->channel_map.channels == 0) {
+ SPICE_DEBUG("%s - Number of channels stored is zero. Ignore.", __func__);
+ return;
+ }
+
+ pstream->changed = TRUE;
+ pstream->info.name = pstream->name;
+ pstream->info.mute = info->mute;
+ memcpy(&pstream->info.channel_map, &info->channel_map.channels, sizeof(pa_channel_map));
+ memcpy(&pstream->info.volume, &info->volume, sizeof(pa_cvolume));
+}
+
+/* to avoid code duplication */
+static void pulse_stream_restore_info_async(gboolean is_playback,
+ SpiceAudio *audio,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ SpicePulsePrivate *p = SPICE_PULSE(audio)->priv;
+ GSimpleAsyncResult *simple;
+ struct async_task *task = g_malloc(sizeof(struct async_task));
+
+ simple = g_simple_async_result_new(G_OBJECT(audio),
+ callback,
+ user_data,
+ pulse_stream_restore_info_async);
+ task->res = simple;
+ task->callback = callback;
+ task->user_data = user_data;
+ task->is_playback = is_playback;
+ if (p->results == NULL) {
+ p->playback.changed = FALSE;
+ p->record.changed = FALSE;
+
+ if (p->playback.info.name != NULL ||
+ p->record.info.name != NULL ||
+ pa_context_get_state(p->context) == PA_CONTEXT_READY) {
+ pa_operation *op = pa_ext_stream_restore_read(p->context,
+ stream_restore_read_cb,
+ audio);
+ if (!op) {
+ g_simple_async_report_error_in_idle(G_OBJECT(audio),
+ callback,
+ user_data,
+ SPICE_CLIENT_ERROR,
+ SPICE_CLIENT_ERROR_FAILED,
+ "Restoring stream data failed: %s",
+ pa_strerror(pa_context_errno(p->context)));
+ g_free(task);
+ return;
+ }
+ pa_operation_unref(op);
+ } else {
+ /* It is possible that we want to get volume-info before the
+ * context is in READY state. In this case, we wait for the
+ * context state change to READY. */
+ p->restore_stream_info = TRUE;
+ }
+ }
+
+ p->results = g_list_append(p->results, task);
+ SPICE_DEBUG ("%s Number of async task is %d", __func__, g_list_length(p->results));
+}
+
+/* to avoid code duplication */
+static gboolean pulse_stream_restore_info_finish(gboolean is_playback,
+ SpiceAudio *audio,
+ GAsyncResult *res,
+ gboolean *mute,
+ guint8 *nchannels,
+ guint16 **volume,
+ GError **error)
+{
+ SpicePulsePrivate *p = SPICE_PULSE(audio)->priv;
+ struct stream *pstream = (is_playback) ? &p->playback : &p->record;
+ GSimpleAsyncResult *simple = (GSimpleAsyncResult *) res;
+
+ g_return_val_if_fail(g_simple_async_result_is_valid(res,
+ G_OBJECT(audio), pulse_stream_restore_info_async), FALSE);
+
+ if (g_simple_async_result_propagate_error(simple, error)) {
+ return FALSE;
+ }
+
+ if (mute != NULL) {
+ *mute = (pstream->info.mute) ? TRUE : FALSE;
+ }
+
+ if (nchannels != NULL) {
+ *nchannels = pstream->info.channel_map.channels;
+ }
+
+ if (volume != NULL) {
+ gint i;
+ *volume = g_new(guint16, pstream->info.channel_map.channels);
+ for (i = 0; i < pstream->info.channel_map.channels; i++) {
+ (*volume)[i] = (guint16) pstream->info.volume.values[i];
+ }
+ }
+
+ return g_simple_async_result_get_op_res_gboolean(simple);
+}
+
+static void spice_pulse_get_playback_volume_info_async(SpiceAudio *audio,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ pulse_stream_restore_info_async(TRUE, audio, callback, user_data);
+}
+
+static gboolean spice_pulse_get_playback_volume_info_finish(SpiceAudio *audio,
+ GAsyncResult *res,
+ gboolean *mute,
+ guint8 *nchannels,
+ guint16 **volume,
+ GError **error)
+{
+ return pulse_stream_restore_info_finish(TRUE, audio, res, mute,
+ nchannels, volume, error);
+}
+
+static void spice_pulse_get_record_volume_info_async(SpiceAudio *audio,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ pulse_stream_restore_info_async(FALSE, audio, callback, user_data);
+}
+
+static gboolean spice_pulse_get_record_volume_info_finish(SpiceAudio *audio,
+ GAsyncResult *res,
+ gboolean *mute,
+ guint8 *nchannels,
+ guint16 **volume,
+ GError **error)
+{
+ return pulse_stream_restore_info_finish(FALSE, audio, res, mute,
+ nchannels, volume, error);
+}
--
2.1.0
More information about the Spice-devel
mailing list