Merge GStreamer with GTK4

Hello,
I am using GStreamer to play recorded video . I need to add UI Controls , for that I am using GTK4 platform . I created a sample application to merge gstreamer with gtk4 . Here I am attaching the code snippet regarding to it.

#include <gst/gst.h>
#include <gtk/gtk.h>
#include <string.h>
#include <gdk/gdk.h>

int play_flag = 0;

/* Structure to contain all our information, so we can pass it around */
typedef struct _CustomData {
    GstElement *pipeline;          /* Our pipeline */
    GtkWidget *sink_widget;        /* Widget where our video will be displayed */
    GtkWidget *slider;             /* Slider widget to keep track of current position */
    GtkWidget *streams_list; 
    gulong slider_update_signal_id; /* Signal ID for the slider update signal */
    GstState state;                /* Current state of the pipeline */
    gint64 duration;               /* Duration of the clip, in nanoseconds */
} CustomData;

GstElement *source;
GstElement *decode;
GstElement *video_queue, *v4l2convert, *videosink, *audio_queue, *audioconvert, *audioresample, *audiosink;

/* This function is called when the PLAY button is clicked */
static void play_cb(GtkButton *button, CustomData *data) {
    if (play_flag == 1) {
        gst_element_set_state(data->pipeline, GST_STATE_READY);
        play_flag = 0;
    }
    gst_element_set_state(data->pipeline, GST_STATE_PLAYING);
}

/* This function is called when the PAUSE button is clicked */
static void pause_cb(GtkButton *button, CustomData *data) {
    gst_element_set_state(data->pipeline, GST_STATE_PAUSED);
}

/* This function is called when the STOP button is clicked */
static void stop_cb(GtkButton *button, CustomData *data) {
    gst_element_set_state(data->pipeline, GST_STATE_READY);
}

/* This function is called when the main window is closed */
static void delete_event_cb(GtkWidget *widget, GdkEvent *event, CustomData *data) {
    stop_cb(NULL, data);
}

/* This function is called when the slider changes its position. We perform a seek to the new position here. */
static void slider_cb(GtkRange *range, CustomData *data) {
    gdouble value = gtk_range_get_value(GTK_RANGE(data->slider));
    gst_element_seek_simple(data->pipeline, GST_FORMAT_TIME, GST_SEEK_FLAG_FLUSH | GST_SEEK_FLAG_KEY_UNIT,
                            (gint64)(value * GST_SECOND));
}

/* Dynamic pad handler to link decodebin pads */
static void pad_added_handler(GstElement *src, GstPad *new_pad, CustomData *data) {
    GstPad *sink_pad = NULL;
    GstCaps *new_pad_caps = NULL;
    GstStructure *new_pad_struct = NULL;
    const gchar *new_pad_type = NULL;

    new_pad_caps = gst_pad_get_current_caps(new_pad);
    new_pad_struct = gst_caps_get_structure(new_pad_caps, 0);
    new_pad_type = gst_structure_get_name(new_pad_struct);

    if (g_str_has_prefix(new_pad_type, "video/x-raw")) {
        sink_pad = gst_element_get_static_pad(video_queue, "sink");
    } else if (g_str_has_prefix(new_pad_type, "audio/x-raw")) {
        sink_pad = gst_element_get_static_pad(audio_queue, "sink");
    }

    if (gst_pad_is_linked(sink_pad)) {
        g_object_unref(sink_pad);
        if (new_pad_caps != NULL)
            gst_caps_unref(new_pad_caps);
        return;
    }

    if (gst_pad_link(new_pad, sink_pad) != GST_PAD_LINK_OK) {
        g_printerr("Type is '%s' but link failed.\n", new_pad_type);
    } else {
        g_print("Link succeeded (type '%s').\n", new_pad_type);
    }

    if (new_pad_caps != NULL)
        gst_caps_unref(new_pad_caps);
    gst_object_unref(sink_pad);
}

/* This function is called periodically to refresh the GUI */
static gboolean refresh_ui(CustomData *data) {
    gint64 current = -1;

    /* We do not want to update anything unless we are in the PAUSED or PLAYING states */
    if (data->state < GST_STATE_PAUSED)
        return TRUE;

    /* If we didn't know it yet, query the stream duration */
    if (!GST_CLOCK_TIME_IS_VALID(data->duration)) {
        if (!gst_element_query_duration(data->pipeline, GST_FORMAT_TIME, &data->duration)) {
            g_printerr("Could not query current duration.\n");
        } else {
            /* Set the range of the slider to the clip duration, in SECONDS */
            gtk_range_set_range(GTK_RANGE(data->slider), 0, (gdouble)data->duration / GST_SECOND);
        }
    }

    if (gst_element_query_position(data->pipeline, GST_FORMAT_TIME, &current)) {
        /* Block the "value-changed" signal, so the slider_cb function is not called
         * (which would trigger a seek the user has not requested) */
        g_signal_handler_block(data->slider, data->slider_update_signal_id);
        /* Set the position of the slider to the current pipeline position, in SECONDS */
        gtk_range_set_value(GTK_RANGE(data->slider), (gdouble)current / GST_SECOND);
        /* Re-enable the signal */
        g_signal_handler_unblock(data->slider, data->slider_update_signal_id);
    }
    return TRUE;
}

/* This function is called when an error message is posted on the bus */
static void error_cb(GstBus *bus, GstMessage *msg, CustomData *data) {
    GError *err;
    gchar *debug_info;

    /* Print error details on the screen */
    gst_message_parse_error(msg, &err, &debug_info);
    g_printerr("Error received from element %s: %s\n", GST_OBJECT_NAME(msg->src), err->message);
    g_printerr("Debugging information: %s\n", debug_info ? debug_info : "none");
    g_clear_error(&err);
    g_free(debug_info);

    /* Set the pipeline to READY (which stops playback) */
    gst_element_set_state(data->pipeline, GST_STATE_READY);
}

/* This function is called when an End-Of-Stream message is posted on the bus.
 * We just set the pipeline to READY (which stops playback) */
static void eos_cb(GstBus *bus, GstMessage *msg, CustomData *data) {
    g_print("End-Of-Stream reached.\n");
    play_flag = 1;
}

/* This function is called when the pipeline changes states. We use it to
 * keep track of the current state. */
static void state_changed_cb(GstBus *bus, GstMessage *msg, CustomData *data) {
    GstState old_state, new_state, pending_state;
    gst_message_parse_state_changed(msg, &old_state, &new_state, &pending_state);
    if (GST_MESSAGE_SRC(msg) == GST_OBJECT(data->pipeline)) {
        data->state = new_state;
        g_print("State set to %s\n", gst_element_state_get_name(new_state));
        if (old_state == GST_STATE_READY && new_state == GST_STATE_PAUSED) {
            /* For extra responsiveness, we refresh the GUI as soon as we reach the PAUSED state */
            refresh_ui(data);
        }
    }
}

static void activate(GtkApplication *app, CustomData *data) {
    GtkWidget *window;
    GtkWidget *box;
    GtkWidget *button_play;
    GtkWidget *button_pause;
    GtkWidget *button_stop;
    GstBus *bus;
    GstElement *gtkglsink;

    window = gtk_application_window_new(app);
    gtk_window_set_title(GTK_WINDOW(window), "Video Player");
    gtk_window_set_default_size(GTK_WINDOW(window), 800, 600);

    box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 5);
    gtk_window_set_child(GTK_WINDOW(window), box);

    /* Initialize our data structure */
    memset(data, 0, sizeof(CustomData));
    data->duration = GST_CLOCK_TIME_NONE;

    // Create GStreamer pipeline
    data->pipeline = gst_pipeline_new("video-audio-player");

    source = gst_element_factory_make("filesrc", "file-source");
    decode = gst_element_factory_make("decodebin", "decoder");
    video_queue = gst_element_factory_make("queue", "video-queue");
    v4l2convert = gst_element_factory_make("videoconvert", "video-convert");
    videosink = gst_element_factory_make("gtksink", "video-output"); //autovideosink
    audio_queue = gst_element_factory_make("queue", "audio-queue");
    audioconvert = gst_element_factory_make("audioconvert", "audio-convert");
    audioresample = gst_element_factory_make("audioresample", "audio-resample");
    audiosink = gst_element_factory_make("autoaudiosink", "audio-output");

    if (!source || !decode || !video_queue || !v4l2convert || !videosink ||
        !audio_queue || !audioconvert || !audioresample || !audiosink) {
        g_printerr("Not all elements could be created.\n");
        return;
    }

    g_object_set(G_OBJECT(source), "location", "my_video-4.mp4", NULL);

    /* Build the pipeline. We add all elements into the pipeline */
    gst_bin_add_many(GST_BIN(data->pipeline), source, decode, video_queue, v4l2convert, videosink,
                     audio_queue, audioconvert, audioresample, audiosink, NULL);

    /* Link the file source to the decoder */
    if (!gst_element_link(source, decode)) {
        g_printerr("Failed to link filesrc and decodebin.\n");
        gst_object_unref(data->pipeline);
        return;
    }

    // Link decodebin dynamically
    g_signal_connect(decode, "pad-added", G_CALLBACK(pad_added_handler), data);

    /* Link video elements */
    if (!gst_element_link_many(video_queue, v4l2convert, videosink, NULL)) {
        g_printerr("Video elements could not be linked.\n");
        gst_object_unref(data->pipeline);
        return;
    }

    /* Link audio elements */
    if (!gst_element_link_many(audio_queue, audioconvert, audioresample, audiosink, NULL)) {
        g_printerr("Audio elements could not be linked.\n");
        gst_object_unref(data->pipeline);
        return;
    }

    // Add the video sink widget to the UI only if the sink is a gtksink or gtkglsink
    if (g_strcmp0(gst_element_get_name(videosink), "video-output") == 0) {
    	printf("videooutput yes\n");
        g_object_get(videosink, "widget", &data->sink_widget, NULL);
    }

    if (data->sink_widget) {
        gtk_box_append(GTK_BOX(box), data->sink_widget);
    } else {
        g_printerr("No video widget could be retrieved from the videosink.\n");
    }

    // Continue setting up the rest of the UI and pipeline handling...

    button_play = gtk_button_new_with_label("Play");
    button_pause = gtk_button_new_with_label("Pause");
    button_stop = gtk_button_new_with_label("Stop");

    data->slider = gtk_scale_new_with_range(GTK_ORIENTATION_HORIZONTAL, 0, 100, 1);
    gtk_scale_set_draw_value(GTK_SCALE(data->slider), 0);
    data->slider_update_signal_id = g_signal_connect(G_OBJECT(data->slider), "value-changed", G_CALLBACK(slider_cb), data);

    gtk_box_append(GTK_BOX(box), button_play);
    gtk_box_append(GTK_BOX(box), button_pause);
    gtk_box_append(GTK_BOX(box), button_stop);
    gtk_box_append(GTK_BOX(box), data->slider);

    g_signal_connect(button_play, "clicked", G_CALLBACK(play_cb), data);
    g_signal_connect(button_pause, "clicked", G_CALLBACK(pause_cb), data);
    g_signal_connect(button_stop, "clicked", G_CALLBACK(stop_cb), data);

    gtk_widget_show(window);

    bus = gst_element_get_bus(data->pipeline);
    gst_bus_add_signal_watch(bus);
    g_signal_connect(bus, "message::error", G_CALLBACK(error_cb), data);
    g_signal_connect(bus, "message::eos", G_CALLBACK(eos_cb), data);
    g_signal_connect(bus, "message::state-changed", G_CALLBACK(state_changed_cb), data);

    // Start playing
    gst_element_set_state(data->pipeline, GST_STATE_PLAYING);
	
	    /* Register a function that GLib will call every second */
  //  g_timeout_add(1000, (GSourceFunc)refresh_ui, &data);
  	    g_timeout_add_seconds(1, (GSourceFunc)refresh_ui, data);
	
    /* Free resources */
    gst_object_unref(bus);
}

int main(int argc, char *argv[]) {
    GtkApplication *app;
    int status;
    CustomData data;
	 gst_init(&argc, &argv);
    app = gtk_application_new("org.gtk.example", G_APPLICATION_FLAGS_NONE);
    g_signal_connect(app, "activate", G_CALLBACK(activate), &data);
    status = g_application_run(G_APPLICATION(app), argc, argv);
    g_object_unref(app);

    return status;
}

While running the application , I am facing some critical warnings and no window is displayed. Here I am attaching the critical warnings which I have receiving.

(video:512357): GLib-GObject-WARNING **: 21:24:17.400: cannot register existing type 'GtkWidget'

(video:512357): GLib-GObject-WARNING **: 21:24:17.400: cannot add class private field to invalid type '<invalid>'

(video:512357): GLib-GObject-WARNING **: 21:24:17.400: cannot add private field to invalid (non-instantiatable) type '<invalid>'

(video:512357): GLib-GObject-CRITICAL **: 21:24:17.400: g_type_add_interface_static: assertion 'G_TYPE_IS_INSTANTIATABLE (instance_type)' failed

(video:512357): GLib-GObject-WARNING **: 21:24:17.400: cannot register existing type 'GtkBuildable'

(video:512357): GLib-GObject-CRITICAL **: 21:24:17.400: g_type_interface_add_prerequisite: assertion 'G_TYPE_IS_INTERFACE (interface_type)' failed

(video:512357): GLib-CRITICAL **: 21:24:17.400: g_once_init_leave: assertion 'result != 0' failed

(video:512357): GLib-GObject-CRITICAL **: 21:24:17.400: g_type_add_interface_static: assertion 'G_TYPE_IS_INSTANTIATABLE (instance_type)' failed

(video:512357): Gtk-CRITICAL **: 21:24:17.400: gtk_widget_get_events: assertion 'GTK_IS_WIDGET (widget)' failed

(video:512357): GLib-GObject-WARNING **: 21:24:17.400: cannot register existing type 'GtkWidget'

(video:512357): GLib-GObject-WARNING **: 21:24:17.400: cannot add class private field to invalid type '<invalid>'

(video:512357): GLib-GObject-WARNING **: 21:24:17.400: cannot add private field to invalid (non-instantiatable) type '<invalid>'

(video:512357): GLib-GObject-CRITICAL **: 21:24:17.400: g_type_add_interface_static: assertion 'G_TYPE_IS_INSTANTIATABLE (instance_type)' failed

Can anyone suggest how to resolve this issue or any alternative methods are highly appreciable

gtksink and gtkglsink from the gtk plugin are for GTK3. You’re currently mixing GTK3 and GTK4 in the same process, and that’s not allowed in general because of type conflicts (the critical warnings you get), and also not going to work because GTK3 and GTK4 have a completely different rendering architecture.

You’ll have to use gtk4paintablesink with GTK4. Check the examples in the repository, or e.g. what showtime or livi are doing.

Thank you for your reply . I tried with gtk4paintablesink with GTK4 , but it shows no gtk4paintablesink plugin is available in my system . Here I am attcahing log for it .

gst-inspect-1.0 gtk4paintablesink
No such element or plugin ‘gtk4paintablesink’

Can you guide me how to add this plugin and can you give any examples to play video using gstreamer embedded with GTK4 Window in C language .

Thanks & Regards,
Teena

livi is a C application making use of that plugin.

I tried with gtk4paintablesink with GTK4 , but it shows no gtk4paintablesink plugin is available in my system

Without more details about your system and deployment process I can’t really tell you more. In the worst case, you can build the plugin from the repository. The README.md contains instructions for how to do that as part of a Flatpak application as well as generally via cargo.

My system is using wayland protocol and I am using GTK Version - 4.6.9 and my GStreamer version is 1.20.3

And you build GStreamer and GTK yourself?

Yes, I am building GStreamer and GTK application by myself . And I am installing GTK using sudo apt install libgtk-4-dev and GStreamer by using sudo apt install gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly gstreamer1.0-libav

So you’re using some kind of Debian-based Linux distribution. For Debian a package is available in testing/unstable, but not in any release yet. For Ubuntu a package is available in the upcoming 24.10 release (the 24.04 version didn’t ship the actual plugin :upside_down_face: ). If you’re using Debian/stable or Ubuntu 24.04 or earlier, you’ll have to build it yourself.

For comparison, in Fedora a package is available since F39.