Embedding GTK applications via XEmbed - 2

In previous post, we learnt the basics of embedding a GTK application in another application. This works fine when the embedded application supports client server architecture like gVim. Note that in this case the client mode invocations are short lived processes while server process runs throughout the lifetime of the application.

In contrast, an application like VLC provides a remote control interface to run  multiple operations in the same process. In this case, we need to obtain an input channel of the process and write our commands to the same.

Here's an updated version of the code. Remember to update the two bold paths with appropriate video locations. The application starts with one video and then switches to the next one when the button is clicked.

embed.c

#include <gtk/gtkx.h>

gchar *string;
GMutex mutex;

void send_hello(GtkButton *btn)
{
  /* Note the \n at the end */
  string = g_strdup_printf("add /home/anand/Downloads/test.mp4\n");
}

static void
cb_child_watch( GPid      pid,
                gint      status,
                gpointer *data )
{
  /* Close pid */
  g_spawn_close_pid( pid );
}

static gboolean
cb_in_watch( GIOChannel   *channel,
             GIOCondition  cond,
             gpointer     *data )
{
  gsize  size;

  if( cond == G_IO_HUP )
    {
      g_io_channel_unref( channel );
      return( FALSE );
    }

  while(gtk_events_pending()) gtk_main_iteration();
  g_mutex_lock(&mutex);
  if (string) {
    g_io_channel_write_chars ( channel, string, -1, &size, NULL );
    g_io_channel_flush ( channel, NULL);
    g_warning (string);
    string = NULL;
    /* Channel frees the string when it's done */
    /* g_free( string ); */
  }
  g_mutex_unlock(&mutex);

  return( TRUE );
}

static gboolean
cb_out_watch( GIOChannel   *channel,
              GIOCondition  cond,
              gpointer     *data )
{
  gchar *string;
  gsize  size;

  if( cond == G_IO_HUP )
    {
      g_io_channel_unref( channel );
      return( FALSE );
    }

  while(gtk_events_pending()) gtk_main_iteration();
  g_io_channel_read_line( channel, &string, &size, NULL, NULL );
  g_warning (string);
  g_free( string );

  return( TRUE );
}

static gboolean
cb_err_watch( GIOChannel   *channel,
              GIOCondition  cond,
              gpointer         *data )
{
  gchar *string;
  gsize  size;

  while(gtk_events_pending()) gtk_main_iteration();
  if( cond == G_IO_HUP )
    {
      g_io_channel_unref( channel );
      return( FALSE );
    }

  g_io_channel_read_line( channel, &string, &size, NULL, NULL );
  g_error (string);
  g_free( string );

  return( TRUE );
}

void init(GtkWidget *sock)
{

  /* Embed application */
  Window id = gtk_socket_get_id(GTK_SOCKET(sock));
  gchar *command = g_strdup_printf(
      "/usr/bin/cvlc -I rc --drawable-xid %d /home/anand/Downloads/1.mp4", id);

  GPid        pid;
  gchar      **argv;
  /* gchar      *argv[] = { "/bin/ls", NULL }; */
  gint        out, in, err;
  GIOChannel *out_ch, *in_ch, *err_ch;
  gboolean    ret;
  gpointer data;

  /* Spawn child process */
  g_shell_parse_argv(command, NULL, &argv, NULL);
  ret = g_spawn_async_with_pipes( NULL, argv, NULL,
                                  G_SPAWN_DO_NOT_REAP_CHILD, NULL,
                                  NULL, &pid, &in, &out, &err, NULL );
  if( ! ret )
    {
      g_error( "SPAWN FAILED" );
      return;
    }

  /* Add watch function to catch termination of the process. This function
   * will clean any remnants of process. */
  g_child_watch_add( pid, (GChildWatchFunc)cb_child_watch, data );

  /* Create channels that will be used to read data from pipes. */
#ifdef G_OS_WIN32
  in_ch  = g_io_channel_win32_new_fd( in  );
  out_ch = g_io_channel_win32_new_fd( out );
  err_ch = g_io_channel_win32_new_fd( err );
#else
  in_ch  = g_io_channel_unix_new( in  );
  out_ch = g_io_channel_unix_new( out );
  err_ch = g_io_channel_unix_new( err );
#endif

  /* Add watches to channels */
  /* g_io_channel_set_encoding ( in_ch, NULL, NULL); */
  g_io_add_watch(  in_ch, G_IO_OUT| G_IO_HUP, (GIOFunc)cb_in_watch , data );
  /* g_io_add_watch( out_ch, G_IO_IN | G_IO_HUP, (GIOFunc)cb_out_watch, data ); */
  /* g_io_add_watch( err_ch, G_IO_IN | G_IO_HUP, (GIOFunc)cb_err_watch, data ); */

}

gint main(gint argc, gchar **argv)
{
  gtk_init(&argc, &argv);

  /* Create window */
  GtkWidget *win  = gtk_window_new(GTK_WINDOW_TOPLEVEL);
  GtkWidget *vbox = gtk_box_new(FALSE, 0);
  GtkWidget *sock = gtk_socket_new();
  GtkWidget *btn  = gtk_button_new_with_label("Hello, World!");

  g_signal_connect(sock, "plug-removed", gtk_main_quit, NULL);
  g_signal_connect(win,  "delete-event", gtk_main_quit, NULL);
  g_signal_connect(btn, "clicked", G_CALLBACK(send_hello), NULL);

  gtk_widget_set_size_request(sock, 200, 200);
  gtk_box_pack_start(GTK_BOX(vbox), sock, TRUE,  TRUE, 0);
  gtk_box_pack_start(GTK_BOX(vbox), btn,  FALSE, TRUE, 0);
  gtk_container_add(GTK_CONTAINER(win), vbox);
  gtk_widget_show_all(win);
    
  g_thread_new("thread", (gpointer) init, sock);
  /* g_idle_add((gpointer)init, sock);  */

  /* Run */
  gtk_main();
  return 0;
}

 
 
Compile the file using following command to generate executable embed.

 gcc embed.c $(pkg-config --libs --cflags gtk+-3.0) -o embed  

Lessons Learnt

  • Use complete path to executable.
  • Use g_shell_parse_argv() to generate argv array from command string. 
  • Terminate the command sent to the external process via channel with a new line (\n).
  • Follow the write operation with a g_io_channel_flush().
  • If your channel callback is called too frequently, remember to update the UI using while(gtk_events_pending()) gtk_main_iteration(); 
  • Use mutex to avoid race condition - g_mutex_lock(&mutex); & g_mutex_lock(&mutex); 
  • Use g_thread_new() for one time task and g_idle_add() for periodic idle tasks. If the function returns FALSE, the function added via
    g_idle_add() will not be invoked again.
  • Be watchful of orphan processes generated after running the program.
  • Remember to size the window using gtk_window_resize().
  • gtk_widget_set_size_request() works only after widget is visible ie. after gtk_widget_show() call.

Multimedia dashboard in GNU Emacs

Using the above approach, you can generate a multimedia dashboard in GNU Emacs using VLC backend.
 
 
Create the widget and assign to variable w.
     (require 'xwidget)
     (insert " ")
     (forward-char -1)
     (setq w (xwidget-insert (point) 'socket "test" 400 400 '(:init "/usr/bin/cvlc -I rc --drawable-xid %lu")))

 
Send command to widget w to play file /home/anand/Downloads/test.mp4 .
(xwidget-socket-command w "add /home/anand/Downloads/test.mp4")


Code: 
https://gitlab.com/atamariya/emacs/-/blob/dev/src/xwidget.c
https://gitlab.com/atamariya/emacs/-/blob/dev/src/xterm.h
https://gitlab.com/atamariya/emacs/-/blob/dev/src/xterm.c
https://gitlab.com/atamariya/emacs/-/blob/dev/lisp/xwidget.el

gVim in GNU Emacs

You can also embed gVim in GNU Emacs (GTK build with X11 backend).




Create the widget and assign to variable w.
     (require 'xwidget)
     (insert " ")
     (forward-char -1)
     (setq w (xwidget-insert (point) 'socket "test" 400 400 '(:init "/usr/bin/gvim --servername emacs --socketid %lu")))

 
 

xterm in GNU Emacs

You can also embed xterm in GNU Emacs (GTK build with X11 backend).



Create the widget and assign to variable w.
     (require 'xwidget)
     (insert " ")
     (forward-char -1)
     (setq w (xwidget-insert (point) 'socket "test" 400 400 '(:init "/usr/bin/xterm -into %lu")))

 

Surf browser in GNU Emacs

You can also embed Surf browser in GNU Emacs (GTK build with X11 backend).


Create the widget and assign to variable w.
     (require 'xwidget)
     (insert " ")
     (forward-char -1)
     (setq w (xwidget-insert (point) 'socket "test" 400 400 '(:init "/usr/local/bin/surf -e %lu https://surf.suckless.org")))

 

References:

Comments

Popular posts from this blog

GNU Emacs as a Comic Book Reader

Data Visualization with GNU Emacs

Tinylisp and Multi-threaded Emacs