messenger-gtk

Gtk+3 graphical user interfaces for GNUnet Messenger
Log | Files | Refs | Submodules | README | LICENSE

commit 8e8f7bf802109d986f7c2c083b1622d25c7f5356
parent 8ac4536fbfe1bbcc6da753e3bea2944d0c937323
Author: TheJackiMonster <thejackimonster@gmail.com>
Date:   Mon, 14 Mar 2022 20:33:10 +0100

Implement sending voice messages with preview functionality

Signed-off-by: TheJackiMonster <thejackimonster@gmail.com>

Diffstat:
MMakefile | 9++++++++-
MREADME.md | 1+
Mresources/ui/chat.ui | 2+-
Msrc/application.c | 6++++++
Msrc/ui/chat.c | 461++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Msrc/ui/chat.h | 20++++++++++++++++++++
6 files changed, 481 insertions(+), 18 deletions(-)

diff --git a/Makefile b/Makefile @@ -39,7 +39,14 @@ RESOURCES = css.gresource.xml\ ui.gresource.xml LIBRARIES = gnunetchat -PACKAGES = gnunetutil libhandy-1 gtk+-3.0 libnotify zbar libqrencode +PACKAGES = gnunetutil\ + gstreamer-1.0\ + gtk+-3.0\ + libhandy-1\ + libnotify\ + libqrencode\ + zbar + INCLUDES = submodules/gnome-characters/lib GNU_CC ?= gcc diff --git a/README.md b/README.md @@ -13,6 +13,7 @@ The following dependencies are required and need to be installed to build the ap - [libnotify](https://gitlab.gnome.org/GNOME/libnotify): For notifications - [qrencode](https://github.com/fukuchi/libqrencode): For generating QR codes to share credentials - [zbar](https://github.com/mchehab/zbar): For scanning QR codes via camera + - [gstreamer](https://gitlab.freedesktop.org/gstreamer): For recording and playing voice messages As additional step you will need to load all required git submodules via `git submodule init` and `git submodule update`. It is also possible to just add the `--recursive` flag while cloning the repository to do that automatically. diff --git a/resources/ui/chat.ui b/resources/ui/chat.ui @@ -449,7 +449,7 @@ Author: Tobias Frisch <object class="GtkLabel" id="recording_label"> <property name="visible">True</property> <property name="can-focus">False</property> - <property name="label" translatable="yes">00:00</property> + <property name="label" translatable="yes">00:00:00</property> </object> <packing> <property name="expand">False</property> diff --git a/src/application.c b/src/application.c @@ -25,6 +25,11 @@ #include "application.h" #include "resources.h" +#include <gstreamer-1.0/gst/gst.h> +#include <gtk-3.0/gtk/gtk.h> +#include <libhandy-1/handy.h> +#include <libnotify/notify.h> + static void _load_ui_stylesheets(MESSENGER_Application *app) { @@ -79,6 +84,7 @@ application_init(MESSENGER_Application *app, app->argc = argc; app->argv = argv; + gst_init (&argc, &argv); gtk_init(&argc, &argv); hdy_init(); diff --git a/src/ui/chat.c b/src/ui/chat.c @@ -25,16 +25,19 @@ #include "chat.h" #include <gdk/gdkkeysyms.h> +#include <stdlib.h> #include "file_load_entry.h" #include "message.h" #include "messenger.h" #include "picker.h" -#include "../application.h" -#include "../contact.h" #include "account_entry.h" #include "delete_messages.h" +#include "../application.h" +#include "../contact.h" +#include "../file.h" + static gboolean _flap_reveal_switch(gpointer user_data) { @@ -389,6 +392,12 @@ _send_text_from_view(MESSENGER_Application *app, static void _drop_any_recording(UI_CHAT_Handle *handle) { + if ((handle->play_pipeline) && (handle->playing)) + { + gst_element_set_state(handle->play_pipeline, GST_STATE_NULL); + handle->playing = FALSE; + } + _update_send_record_symbol( gtk_text_view_get_buffer(handle->send_text_view), handle->send_record_symbol, @@ -397,10 +406,33 @@ _drop_any_recording(UI_CHAT_Handle *handle) gtk_stack_set_visible_child(handle->send_stack, handle->send_text_box); + if (handle->recording_filename[0]) + remove(handle->recording_filename); + + handle->recording_filename[0] = 0; handle->recorded = FALSE; } static void +handle_sending_recording_upload_file(UNUSED void *cls, + const struct GNUNET_CHAT_File *file, + uint64_t completed, + uint64_t size) +{ + UI_FILE_LOAD_ENTRY_Handle *file_load = cls; + + gtk_progress_bar_set_fraction( + file_load->load_progress_bar, + 1.0 * completed / size + ); + + file_update_upload_info(file, completed, size); + + if ((completed >= size) && (file_load->chat)) + ui_chat_remove_file_load(file_load->chat, file_load); +} + +static void handle_send_record_button_click(GtkButton *button, gpointer user_data) { @@ -410,17 +442,42 @@ handle_send_record_button_click(GtkButton *button, g_object_get_qdata(G_OBJECT(button), app->quarks.ui) ); - if ((handle->recorded) && - (!gtk_revealer_get_child_revealed(handle->picker_revealer)) && - (gtk_stack_get_visible_child(handle->send_stack) == - handle->send_recording_box)) + struct GNUNET_CHAT_Context *context = (struct GNUNET_CHAT_Context*) ( + g_object_get_qdata(G_OBJECT(handle->send_text_view), app->quarks.data) + ); + + if ((handle->recorded) && (context) && + (handle->recording_filename[0]) && + (!gtk_revealer_get_child_revealed(handle->picker_revealer))) { - // TODO: send audio as file! + UI_FILE_LOAD_ENTRY_Handle *file_load = ui_file_load_entry_new(app); + + gtk_label_set_text(file_load->file_label, handle->recording_filename); + gtk_progress_bar_set_fraction(file_load->load_progress_bar, 0.0); + + struct GNUNET_CHAT_File *file = GNUNET_CHAT_context_send_file( + context, + handle->recording_filename, + handle_sending_recording_upload_file, + file_load + ); + + if (file) + { + file_create_info(file); + + ui_chat_add_file_load(handle, file_load); + } + else if (file_load) + ui_file_load_entry_delete(file_load); _drop_any_recording(handle); return; } + if (gtk_stack_get_visible_child(handle->send_stack) != handle->send_text_box) + return; + GtkTextView *text_view = GTK_TEXT_VIEW( g_object_get_qdata(G_OBJECT(button), app->quarks.widget) ); @@ -439,11 +496,27 @@ handle_send_record_button_pressed(GtkWidget *widget, g_object_get_qdata(G_OBJECT(widget), app->quarks.ui) ); - if ((handle->recorded) || + if ((handle->recorded) || (!(handle->record_pipeline)) || + (handle->recording_filename[0]) || (gtk_revealer_get_child_revealed(handle->picker_revealer)) || (handle->send_text_box != gtk_stack_get_visible_child(handle->send_stack))) return FALSE; + strcpy(handle->recording_filename, "/tmp/rec_XXXXXX.ogg"); + + int fd = mkstemps(handle->recording_filename, 4); + + if (-1 == fd) + return FALSE; + else + close(fd); + + if ((handle->play_pipeline) && (handle->playing)) + { + gst_element_set_state(handle->play_pipeline, GST_STATE_NULL); + handle->playing = FALSE; + } + gtk_image_set_from_icon_name( handle->play_pause_symbol, "media-playback-start-symbolic", @@ -456,10 +529,21 @@ handle_send_record_button_pressed(GtkWidget *widget, GTK_ICON_SIZE_BUTTON ); - gtk_widget_set_sensitive(GTK_WIDGET(handle->recording_play_button), FALSE); + gtk_label_set_text(handle->recording_label, "00:00:00"); + gtk_progress_bar_set_fraction(handle->recording_progress_bar, 0.0); + gtk_widget_set_sensitive(GTK_WIDGET(handle->recording_play_button), FALSE); gtk_stack_set_visible_child(handle->send_stack, handle->send_recording_box); + g_object_set( + G_OBJECT(handle->record_sink), + "location", + handle->recording_filename, + NULL + ); + + gst_element_set_state(handle->record_pipeline, GST_STATE_PLAYING); + return TRUE; } @@ -474,7 +558,8 @@ handle_send_record_button_released(GtkWidget *widget, g_object_get_qdata(G_OBJECT(widget), app->quarks.ui) ); - if ((handle->recorded) || + if ((handle->recorded) || (!(handle->record_pipeline)) || + (!(handle->recording_filename[0])) || (gtk_revealer_get_child_revealed(handle->picker_revealer)) || (handle->send_recording_box != gtk_stack_get_visible_child( handle->send_stack))) @@ -482,8 +567,7 @@ handle_send_record_button_released(GtkWidget *widget, gtk_widget_set_sensitive(GTK_WIDGET(handle->recording_play_button), TRUE); - gtk_revealer_set_reveal_child(handle->picker_revealer, FALSE); - + gst_element_set_state(handle->record_pipeline, GST_STATE_NULL); handle->recorded = TRUE; gtk_image_set_from_icon_name( @@ -521,6 +605,62 @@ handle_recording_close_button_click(UNUSED GtkButton *button, } static void +_stop_playing_recording(UI_CHAT_Handle *handle, + gboolean reset_bar) +{ + gst_element_set_state(handle->play_pipeline, GST_STATE_NULL); + handle->playing = FALSE; + + gtk_image_set_from_icon_name( + handle->play_pause_symbol, + "media-playback-start-symbolic", + GTK_ICON_SIZE_BUTTON + ); + + gtk_progress_bar_set_fraction( + handle->recording_progress_bar, + reset_bar? 0.0 : 1.0 + ); + + if (handle->play_timer) + { + g_source_remove(handle->play_timer); + handle->play_timer = 0; + } +} + +static void +handle_recording_play_button_click(UNUSED GtkButton *button, + gpointer user_data) +{ + UI_CHAT_Handle *handle = (UI_CHAT_Handle*) user_data; + + if ((!(handle->recorded)) || (!(handle->play_pipeline))) + return; + + if (handle->playing) + _stop_playing_recording(handle, TRUE); + else if (handle->recording_filename[0]) + { + g_object_set( + G_OBJECT(handle->play_source), + "location", + handle->recording_filename, + NULL + ); + + gst_element_set_state(handle->play_pipeline, GST_STATE_PLAYING); + handle->playing = TRUE; + + gtk_image_set_from_icon_name( + handle->play_pause_symbol, + "media-playback-stop-symbolic", + GTK_ICON_SIZE_BUTTON + ); + } +} + +static void handle_picker_button_click(UNUSED GtkButton *button, gpointer user_data) { @@ -537,6 +677,268 @@ handle_picker_button_click(UNUSED GtkButton *button, ); } +static gboolean +_record_timer_func(gpointer user_data) +{ + UI_CHAT_Handle *handle = (UI_CHAT_Handle*) user_data; + + GString *time_string = g_string_new(NULL); + + g_string_printf( + time_string, + "%02u:%02u:%02u", + (handle->record_time / 3600), + (handle->record_time / 60) % 60, + (handle->record_time % 60) + ); + + gtk_label_set_text(handle->recording_label, time_string->str); + g_string_free(time_string, TRUE); + + if (!(handle->recorded)) + { + handle->record_time++; + handle->record_timer = g_timeout_add_seconds( + 1, + _record_timer_func, + handle + ); + } + else + handle->record_timer = 0; + + return FALSE; +} + +static gboolean +_play_timer_func(gpointer user_data) +{ + UI_CHAT_Handle *handle = (UI_CHAT_Handle*) user_data; + + if (handle->play_time < handle->record_time * 100) + gtk_progress_bar_set_fraction( + handle->recording_progress_bar, + 0.01 * handle->play_time / handle->record_time + ); + else + gtk_progress_bar_set_fraction( + handle->recording_progress_bar, + 1.0 + ); + + if (handle->playing) + { + handle->play_time++; + handle->play_timer = g_timeout_add( + 10, + _play_timer_func, + handle + ); + } + else + handle->play_timer = 0; + + return FALSE; +} + +static gboolean +handle_record_bus_watch(UNUSED GstBus *bus, + GstMessage *msg, + gpointer data) +{ + UI_CHAT_Handle *handle = (UI_CHAT_Handle*) data; + GstMessageType type = GST_MESSAGE_TYPE(msg); + + switch (type) + { + case GST_MESSAGE_STREAM_START: + handle->record_time = 0; + handle->record_timer = g_timeout_add_seconds( + 0, + _record_timer_func, + handle + ); + + break; + default: + break; + } + + return TRUE; +} + +static gboolean +handle_play_bus_watch(UNUSED GstBus *bus, + GstMessage *msg, + gpointer data) +{ + UI_CHAT_Handle *handle = (UI_CHAT_Handle*) data; + GstMessageType type = GST_MESSAGE_TYPE(msg); + + switch (type) + { + case GST_MESSAGE_STREAM_START: + handle->play_time = 0; + handle->play_timer = g_timeout_add_seconds( + 0, + _play_timer_func, + handle + ); + + break; + case GST_MESSAGE_EOS: + if (handle->playing) + _stop_playing_recording(handle, FALSE); + break; + default: + break; + } + + return TRUE; +} + +static void +_play_pad_added(UNUSED GstElement *element, + GstPad *pad, + gpointer data) +{ + GstElement *decoder = (GstElement*) data; + + GstPad *sinkpad = gst_element_get_static_pad(decoder, "sink"); + gst_pad_link(pad, sinkpad); + gst_object_unref (sinkpad); +} + +static void +_setup_gst_pipelines(UI_CHAT_Handle *handle) +{ + handle->record_pipeline = gst_pipeline_new("audio-recorder"); + + GstElement *audio_source = gst_element_factory_make( + "autoaudiosrc", + "audio-input" + ); + + GstElement *audio_converter = gst_element_factory_make( + "audioconvert", + "audio-converter" + ); + + GstElement *vorbis_encoder = gst_element_factory_make( + "vorbisenc", + "vorbis-encoder" + ); + + GstElement *ogg_muxer = gst_element_factory_make( + "oggmux", + "ogg-muxer" + ); + + handle->record_sink = gst_element_factory_make( + "filesink", + "file-output" + ); + + gst_bin_add_many( + GST_BIN(handle->record_pipeline), + audio_source, + audio_converter, + vorbis_encoder, + ogg_muxer, + handle->record_sink, + NULL + ); + + gst_element_link_many( + audio_source, + audio_converter, + vorbis_encoder, + ogg_muxer, + handle->record_sink, + NULL + ); + + { + GstBus *bus = gst_pipeline_get_bus(GST_PIPELINE(handle->record_pipeline)); + + handle->record_watch = gst_bus_add_watch( + bus, + handle_record_bus_watch, + handle + ); + + gst_object_unref(bus); + } + + handle->play_pipeline = gst_pipeline_new("audio-previewer"); + + handle->play_source = gst_element_factory_make( + "filesrc", + "file-input" + ); + + GstElement *ogg_demuxer = gst_element_factory_make( + "oggdemux", + "ogg-demuxer" + ); + + GstElement *vorbis_decoder = gst_element_factory_make( + "vorbisdec", + "vorbis-decoder" + ); + + GstElement *audio_play_converter = gst_element_factory_make( + "audioconvert", + "audio-converter" + ); + + GstElement *audio_sink = gst_element_factory_make( + "autoaudiosink", + "audio-output" + ); + + gst_bin_add_many( + GST_BIN(handle->play_pipeline), + handle->play_source, + ogg_demuxer, + vorbis_decoder, + audio_play_converter, + audio_sink, + NULL + ); + + gst_element_link( + handle->play_source, + ogg_demuxer + ); + + gst_element_link_many( + vorbis_decoder, + audio_play_converter, + audio_sink, + NULL + ); + + g_signal_connect( + ogg_demuxer, + "pad-added", + G_CALLBACK(_play_pad_added), + vorbis_decoder + ); + + { + GstBus *bus = gst_pipeline_get_bus(GST_PIPELINE(handle->play_pipeline)); + + handle->play_watch = gst_bus_add_watch( + bus, + handle_play_bus_watch, + handle + ); + + gst_object_unref(bus); + } +} + UI_CHAT_Handle* ui_chat_new(MESSENGER_Application *app) { @@ -545,12 +947,11 @@ ui_chat_new(MESSENGER_Application *app) UI_CHAT_Handle *handle = g_malloc(sizeof(UI_CHAT_Handle)); UI_MESSENGER_Handle *messenger = &(app->ui.messenger); - handle->recorded = FALSE; + memset(handle, 0, sizeof(*handle)); - handle->app = app; + _setup_gst_pipelines(handle); - handle->messages = NULL; - handle->edge_value = 0; + handle->app = app; handle->loads = NULL; @@ -845,6 +1246,13 @@ ui_chat_new(MESSENGER_Application *app) gtk_builder_get_object(handle->builder, "recording_play_button") ); + g_signal_connect( + handle->recording_play_button, + "clicked", + G_CALLBACK(handle_recording_play_button_click), + handle + ); + handle->play_pause_symbol = GTK_IMAGE( gtk_builder_get_object(handle->builder, "play_pause_symbol") ); @@ -1033,6 +1441,27 @@ ui_chat_delete(UI_CHAT_Handle *handle) if (handle->loads) g_list_free_full(handle->loads, (GDestroyNotify) ui_file_load_entry_delete); + if (handle->record_pipeline) + { + gst_element_set_state (handle->record_pipeline, GST_STATE_NULL); + gst_object_unref(GST_OBJECT(handle->record_pipeline)); + } + + if (handle->play_pipeline) + { + gst_element_set_state (handle->play_pipeline, GST_STATE_NULL); + gst_object_unref(GST_OBJECT(handle->play_pipeline)); + } + + if (handle->recording_filename[0]) + remove(handle->recording_filename); + + if (handle->record_timer) + g_source_remove(handle->record_timer); + + if (handle->play_timer) + g_source_remove(handle->play_timer); + g_free(handle); } diff --git a/src/ui/chat.h b/src/ui/chat.h @@ -25,9 +25,11 @@ #ifndef UI_CHAT_H_ #define UI_CHAT_H_ +#include <gstreamer-1.0/gst/gst.h> #include <gtk-3.0/gtk/gtk.h> #include <libhandy-1/handy.h> #include <libnotify/notify.h> +#include <stdio.h> #include <gnunet/gnunet_chat_lib.h> @@ -39,6 +41,24 @@ typedef struct UI_FILE_LOAD_ENTRY_Handle UI_FILE_LOAD_ENTRY_Handle; typedef struct UI_CHAT_Handle { gboolean recorded; + gboolean playing; + + char recording_filename [PATH_MAX]; + + guint record_timer; + guint record_time; + + guint play_timer; + guint play_time; + + GstElement *record_pipeline; + GstElement *record_sink; + + GstElement *play_pipeline; + GstElement *play_source; + + guint record_watch; + guint play_watch; MESSENGER_Application *app;