Thanks, please see below. Again, runs in one window on Linux, two on macOS. Compiles and runs this way - thanks for any help:
gcc main.c -o gst_gtk4_player $(pkg-config --cflags --libs gtk4 gstreamer-1.0 gstreamer-video-1.0 gstreamer-pbutils-1.0 gstgtk4)
./gst_gtk4_player ../my-file.mp4
CODE:
#include <stdio.h>
#include <stdlib.h>
#include <gtk/gtk.h>
#include <gst/gst.h>
#include <gst/video/video.h> // For video caps check
#include <gst/audio/audio.h> // For audio elements/caps check
#include <gst/pbutils/pbutils.h> // For gst_filename_to_uri
// Structure to hold application data
typedef struct {
GstElement *pipeline;
GstElement *uridecodebin;
// Video Branch
GstElement *videoconvert;
GstElement *videosink; // gtk4paintablesink
// Audio Branch
GstElement *audioconvert;
GstElement *audioresample;
GstElement *audiosink; // autoaudiosink
GtkApplication *app;
GtkWindow *window;
GtkPicture *picture;
gchar *video_uri;
guint bus_watch_id;
gboolean cleanup_done;
gint activate_call_count; // Keep for debugging window issue later
} AppData;
// Forward declarations
static void activate_cb(GtkApplication *app, gpointer user_data);
static void on_destroy_cb(GtkWidget *widget, gpointer user_data);
static gboolean bus_message_cb(GstBus *bus, GstMessage *message, gpointer user_data);
static void uridecodebin_pad_added_cb(GstElement *uridecodebin, GstPad *new_pad, gpointer user_data);
static gboolean create_pipeline(AppData *data);
static void cleanup_gstreamer(AppData *data);
int main(int argc, char *argv[]) {
if (argc != 2) {
fprintf(stderr, "Usage: %s <video_file.mp4>\n", argv[0]);
return -1;
}
gst_init(&argc, &argv);
AppData *data = g_new0(AppData, 1);
data->cleanup_done = FALSE;
data->activate_call_count = 0;
data->video_uri = gst_filename_to_uri(argv[1], NULL);
if (!data->video_uri) {
fprintf(stderr, "Error: Could not convert filename '%s' to URI.\n", argv[1]);
g_free(data);
return -1;
}
printf("Using URI: %s\n", data->video_uri);
data->app = gtk_application_new("org.gstreamer.gtk4.paintable.audio", G_APPLICATION_DEFAULT_FLAGS);
g_signal_connect(data->app, "activate", G_CALLBACK(activate_cb), data);
// No separate shutdown handler needed just to free data
printf("Running GtkApplication...\n");
int status = g_application_run(G_APPLICATION(data->app), 0, NULL);
printf("GtkApplication finished running (status: %d).\n", status);
printf("Performing final cleanup check in main...\n");
cleanup_gstreamer(data); // Safe due to the cleanup_done flag
printf("Unreffing GtkApplication...\n");
g_object_unref(data->app);
printf("GtkApplication unreffed.\n");
printf("Freeing AppData in main...\n");
g_free(data);
printf("AppData freed in main.\n");
printf("Exiting application with status %d.\n", status);
return status;
}
static void activate_cb(GtkApplication *app, gpointer user_data) {
AppData *data = (AppData *)user_data;
GdkPaintable *paintable = NULL;
g_assert(data != NULL);
data->activate_call_count++;
printf("--- activate_cb called (Count: %d) ---\n", data->activate_call_count);
// --- Create GTK Widgets (if first activation) ---
if (!data->window) {
printf(" Creating window and picture widget...\n");
data->window = GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(app)));
if (!data->window) {
fprintf(stderr, " FATAL: Failed to create application window!\n");
return;
}
gtk_window_set_title(data->window, "GStreamer GTK4 Player (Audio/Video)");
gtk_window_set_default_size(data->window, 640, 480);
g_signal_connect(data->window, "destroy", G_CALLBACK(on_destroy_cb), data);
data->picture = GTK_PICTURE(gtk_picture_new());
if (!data->picture) {
fprintf(stderr, " FATAL: Failed to create GtkPicture!\n");
gtk_window_destroy(data->window);
return;
}
gtk_picture_set_can_shrink(data->picture, TRUE);
gtk_window_set_child(data->window, GTK_WIDGET(data->picture));
printf(" Window and picture widget created.\n");
} else {
printf(" Window already exists, skipping creation.\n");
}
// --- Create GStreamer Pipeline (if it doesn't exist) ---
if (!data->pipeline) {
printf(" Creating GStreamer pipeline...\n");
if (!create_pipeline(data)) {
fprintf(stderr, " Pipeline creation failed.\n");
if (data->window) {
gtk_window_destroy(data->window);
}
return;
}
printf(" GStreamer pipeline created.\n");
// --- Get Paintable from Sink ---
printf(" Getting paintable from sink...\n");
g_object_get(data->videosink, "paintable", &paintable, NULL);
if (!paintable) {
fprintf(stderr, "Error: Could not get paintable from gtk4paintablesink.\n");
if (data->window) {
gtk_window_destroy(data->window);
}
return;
}
printf(" Setting paintable on picture widget...\n");
if (data->picture) {
gtk_picture_set_paintable(data->picture, paintable);
} else {
fprintf(stderr, "Error: GtkPicture is NULL when trying to set paintable.\n");
}
g_object_unref(paintable);
// --- Setup GStreamer Bus Watch ---
printf(" Setting up bus watch...\n");
GstBus *bus = gst_element_get_bus(data->pipeline);
if (!bus) {
fprintf(stderr, "Error: Failed to get pipeline bus!\n");
if (data->window) {
gtk_window_destroy(data->window);
}
return;
}
data->bus_watch_id = gst_bus_add_watch(bus, bus_message_cb, data);
gst_object_unref(bus);
if (data->bus_watch_id == 0) {
fprintf(stderr, "Error: Failed to add bus watch!\n");
if (data->window) {
gtk_window_destroy(data->window);
}
return;
}
printf(" Bus watch setup complete (ID: %u).\n", data->bus_watch_id);
} else {
printf(" Pipeline already exists, skipping creation.\n");
}
// --- Start Playback ---
GstState current_state = GST_STATE_NULL;
if (data->pipeline) {
gst_element_get_state(data->pipeline, ¤t_state, NULL, 0);
} else {
fprintf(stderr, " Error: Pipeline is NULL before trying to play!\n");
return;
}
printf(" Attempting to set pipeline to PLAYING (current state: %s)...\n", gst_element_state_get_name(current_state));
if (current_state < GST_STATE_PAUSED) {
GstStateChangeReturn ret = gst_element_set_state(data->pipeline, GST_STATE_PLAYING);
if (ret == GST_STATE_CHANGE_FAILURE) {
fprintf(stderr, " Error: gst_element_set_state returned GST_STATE_CHANGE_FAILURE.\n");
fprintf(stderr, " Check GStreamer debug log or bus messages for element errors.\n");
// Let bus message handle window destruction on error
} else if (ret == GST_STATE_CHANGE_ASYNC) {
printf(" Pipeline state change is ASYNC (will complete later)...\n");
} else {
printf(" Pipeline state set to PLAYING or PAUSED successfully (Return code: %d -> %s).\n", ret, gst_element_state_change_return_get_name(ret));
}
printf(" Pipeline potentially playing or prerolling...\n");
} else {
printf(" Pipeline already playing or paused, not changing state.\n");
}
// --- Show Window ---
if (data->window) {
printf(" Showing/Presenting window...\n");
gtk_window_present(GTK_WINDOW(data->window));
}
printf("--- activate_cb finished ---\n");
}
// Creates the GStreamer pipeline with Audio and Video branches
static gboolean create_pipeline(AppData *data) {
g_assert(data != NULL);
// --- Create Elements ---
data->uridecodebin = gst_element_factory_make("uridecodebin3", "uri-decoder");
if (!data->uridecodebin) {
printf(" uridecodebin3 not found, trying uridecodebin...\n");
data->uridecodebin = gst_element_factory_make("uridecodebin", "uri-decoder");
}
// Video elements
data->videoconvert = gst_element_factory_make("videoconvert", "video-converter");
data->videosink = gst_element_factory_make("gtk4paintablesink", "video-sink");
// Audio elements
data->audioconvert = gst_element_factory_make("audioconvert", "audio-converter");
data->audioresample = gst_element_factory_make("audioresample", "audio-resampler");
data->audiosink = gst_element_factory_make("autoaudiosink", "audio-sink");
if (!data->uridecodebin || !data->videoconvert || !data->videosink ||
!data->audioconvert || !data->audioresample || !data->audiosink ) {
fprintf(stderr, "Error: Not all GStreamer elements could be created:\n");
fprintf(stderr, " uridecodebin: %s\n", data->uridecodebin ? "OK" : "FAIL");
fprintf(stderr, " videoconvert: %s\n", data->videoconvert ? "OK" : "FAIL");
fprintf(stderr, " videosink: %s\n", data->videosink ? "OK" : "FAIL");
fprintf(stderr, " audioconvert: %s\n", data->audioconvert ? "OK" : "FAIL");
fprintf(stderr, " audioresample:%s\n", data->audioresample ? "OK" : "FAIL");
fprintf(stderr, " audiosink: %s\n", data->audiosink ? "OK" : "FAIL");
// Manual unref of elements created so far
if(data->uridecodebin) gst_object_unref(data->uridecodebin);
if(data->videoconvert) gst_object_unref(data->videoconvert);
if(data->videosink) gst_object_unref(data->videosink);
if(data->audioconvert) gst_object_unref(data->audioconvert);
if(data->audioresample) gst_object_unref(data->audioresample);
if(data->audiosink) gst_object_unref(data->audiosink);
// Clear all pointers in data struct
data->uridecodebin = NULL; data->videoconvert = NULL; data->videosink = NULL;
data->audioconvert = NULL; data->audioresample = NULL; data->audiosink = NULL;
return FALSE;
}
printf(" GStreamer elements created successfully (using %s).\n", data->uridecodebin ? GST_ELEMENT_NAME(data->uridecodebin) : "FAILED");
// --- Create Pipeline ---
data->pipeline = gst_pipeline_new("video-pipeline");
if (!data->pipeline) {
fprintf(stderr, "Error: Could not create GStreamer pipeline.\n");
gst_object_unref(data->uridecodebin); gst_object_unref(data->videoconvert); gst_object_unref(data->videosink);
gst_object_unref(data->audioconvert); gst_object_unref(data->audioresample); gst_object_unref(data->audiosink);
data->uridecodebin = NULL; data->videoconvert = NULL; data->videosink = NULL;
data->audioconvert = NULL; data->audioresample = NULL; data->audiosink = NULL;
return FALSE;
}
// --- Build Pipeline ---
if (!data->video_uri) {
fprintf(stderr, "Error: video_uri is NULL when creating pipeline.\n");
gst_object_unref(data->pipeline); // Pipeline exists, unref it
// Elements were not added yet, but unref them to be safe
gst_object_unref(data->uridecodebin); gst_object_unref(data->videoconvert); gst_object_unref(data->videosink);
gst_object_unref(data->audioconvert); gst_object_unref(data->audioresample); gst_object_unref(data->audiosink);
data->pipeline = NULL; data->uridecodebin = NULL; data->videoconvert = NULL; data->videosink = NULL;
data->audioconvert = NULL; data->audioresample = NULL; data->audiosink = NULL;
return FALSE;
}
g_object_set(data->uridecodebin, "uri", data->video_uri, NULL);
// Add all elements to the pipeline bin
gst_bin_add_many(GST_BIN(data->pipeline),
data->uridecodebin,
data->videoconvert, data->videosink,
data->audioconvert, data->audioresample, data->audiosink,
NULL);
// Link static parts of the pipeline (Audio branch)
if (!gst_element_link_many(data->audioconvert, data->audioresample, data->audiosink, NULL)) {
fprintf(stderr, "Error: Could not link audio branch (audioconvert -> audioresample -> audiosink).\n");
gst_object_unref(data->pipeline); data->pipeline = NULL;
// Pointers will be cleared by cleanup
return FALSE;
}
// Link static parts of the pipeline (Video branch)
if (!gst_element_link(data->videoconvert, data->videosink)) {
fprintf(stderr, "Error: Could not link video branch (videoconvert -> videosink).\n");
gst_object_unref(data->pipeline); data->pipeline = NULL;
// Pointers will be cleared by cleanup
return FALSE;
}
// Connect the "pad-added" signal from uridecodebin for dynamic linking
g_signal_connect(data->uridecodebin, "pad-added", G_CALLBACK(uridecodebin_pad_added_cb), data);
printf(" Pipeline constructed with Audio/Video branches (dynamic linking pending).\n");
return TRUE;
}
// Callback for uridecodebin's "pad-added" signal (Handles Audio and Video)
static void uridecodebin_pad_added_cb(GstElement *uridecodebin, GstPad *new_pad, gpointer user_data) {
AppData *data = (AppData *)user_data;
GstCaps *new_pad_caps = NULL;
GstStructure *new_pad_struct = NULL;
const gchar *new_pad_type = NULL;
GstPad *sink_pad = NULL;
GstPadLinkReturn ret;
g_assert(data != NULL);
printf("Received new pad '%s' from '%s'\n", GST_PAD_NAME(new_pad), GST_ELEMENT_NAME(uridecodebin));
// Try to get the caps first to determine media type
new_pad_caps = gst_pad_get_current_caps(new_pad);
if (!new_pad_caps) {
// Fallback: check pad name if caps are not available yet
const gchar *pad_name = GST_PAD_NAME(new_pad);
printf(" Warning: Could not get caps for new pad '%s' immediately.\n", pad_name);
if (g_str_has_prefix(pad_name, "video") || g_str_has_prefix(pad_name, "src_v")) {
new_pad_type = "video"; // Assume video based on name
printf(" Pad name suggests video, proceeding.\n");
} else if (g_str_has_prefix(pad_name, "audio") || g_str_has_prefix(pad_name, "src_a")) {
new_pad_type = "audio"; // Assume audio based on name
printf(" Pad name suggests audio, proceeding.\n");
} else {
printf(" Pad name '%s' doesn't suggest audio or video, ignoring.\n", pad_name);
goto exit; // No cleanup needed beyond caps/sink_pad if obtained
}
} else {
new_pad_struct = gst_caps_get_structure(new_pad_caps, 0);
new_pad_type = gst_structure_get_name(new_pad_struct);
}
// Check if it's video
if (g_str_has_prefix(new_pad_type, "video")) {
printf(" Pad is video type.\n");
if (!data->videoconvert) {
fprintf(stderr, " Error: data->videoconvert is NULL!\n");
goto exit;
}
sink_pad = gst_element_get_static_pad(data->videoconvert, "sink");
if (!sink_pad) {
fprintf(stderr, " Error: Could not get sink pad from videoconvert.\n");
goto exit;
}
if (gst_pad_is_linked(sink_pad)) {
printf(" Videoconvert sink already linked.\n");
goto exit;
}
printf(" Attempting to link VIDEO pad '%s:%s' to '%s:%s'...\n",
GST_ELEMENT_NAME(uridecodebin), GST_PAD_NAME(new_pad),
GST_ELEMENT_NAME(data->videoconvert), GST_PAD_NAME(sink_pad));
ret = gst_pad_link(new_pad, sink_pad);
}
// Check if it's audio
else if (g_str_has_prefix(new_pad_type, "audio")) {
printf(" Pad is audio type.\n");
if (!data->audioconvert) {
fprintf(stderr, " Error: data->audioconvert is NULL!\n");
goto exit;
}
sink_pad = gst_element_get_static_pad(data->audioconvert, "sink");
if (!sink_pad) {
fprintf(stderr, " Error: Could not get sink pad from audioconvert.\n");
goto exit;
}
if (gst_pad_is_linked(sink_pad)) {
printf(" Audioconvert sink already linked.\n");
goto exit;
}
printf(" Attempting to link AUDIO pad '%s:%s' to '%s:%s'...\n",
GST_ELEMENT_NAME(uridecodebin), GST_PAD_NAME(new_pad),
GST_ELEMENT_NAME(data->audioconvert), GST_PAD_NAME(sink_pad));
ret = gst_pad_link(new_pad, sink_pad);
}
// Unknown type
else {
printf(" Ignoring pad with type '%s'.\n", new_pad_type);
goto exit; // Not audio or video
}
// Check linking result
if (GST_PAD_LINK_FAILED(ret)) {
fprintf(stderr, " Error: Failed to link pads. Reason: %s (%d)\n",
gst_pad_link_get_name(ret), ret);
} else {
printf(" Successfully linked pads.\n");
}
exit:
if (new_pad_caps != NULL) gst_caps_unref(new_pad_caps);
if (sink_pad != NULL) gst_object_unref(sink_pad);
}
// Callback for GStreamer bus messages (Unchanged)
static gboolean bus_message_cb(GstBus *bus, GstMessage *message, gpointer user_data) {
AppData *data = (AppData *)user_data;
GError *err = NULL;
gchar *debug_info = NULL;
g_assert (data != NULL);
if (data->cleanup_done) {
//printf("BUS_MESSAGE: Cleanup already done, ignoring message (%s)\n", GST_MESSAGE_TYPE_NAME(message));
return TRUE;
}
switch (GST_MESSAGE_TYPE(message)) {
case GST_MESSAGE_ERROR:
gst_message_parse_error(message, &err, &debug_info);
fprintf(stderr, "BUS_MESSAGE: ERROR from element %s: %s\n",
GST_OBJECT_NAME(message->src), err ? err->message : "(null error)");
fprintf(stderr, "BUS_MESSAGE: Debugging info: %s\n", (debug_info) ? debug_info : "none");
if (data->window) {
printf("BUS_MESSAGE: Destroying window due to error.\n");
gtk_window_destroy(GTK_WINDOW(data->window));
} else {
printf("BUS_MESSAGE: Window already NULL on error, calling cleanup directly.\n");
cleanup_gstreamer(data);
}
g_clear_error(&err);
g_free(debug_info);
break;
case GST_MESSAGE_EOS:
printf("BUS_MESSAGE: Reached end-of-stream.\n");
if (data->window) {
printf("BUS_MESSAGE: Destroying window on EOS.\n");
gtk_window_destroy(GTK_WINDOW(data->window));
} else {
printf("BUS_MESSAGE: Window already NULL on EOS, calling cleanup directly.\n");
cleanup_gstreamer(data);
}
break;
case GST_MESSAGE_STATE_CHANGED:
if (GST_MESSAGE_SRC(message) == GST_OBJECT(data->pipeline)) {
GstState old_state, new_state, pending_state;
gst_message_parse_state_changed(message, &old_state, &new_state, &pending_state);
printf("BUS_MESSAGE: Pipeline state changed from %s to %s (pending: %s)\n",
gst_element_state_get_name(old_state),
gst_element_state_get_name(new_state),
gst_element_state_get_name(pending_state));
}
break;
case GST_MESSAGE_ASYNC_DONE:
printf("BUS_MESSAGE: Async operation finished.\n");
break;
default:
break;
}
return TRUE;
}
// Centralized GStreamer cleanup
static void cleanup_gstreamer(AppData *data) {
if (!data || data->cleanup_done) {
return;
}
printf("Cleanup: Starting GStreamer resource cleanup...\n");
data->cleanup_done = TRUE;
if (data->bus_watch_id > 0) {
GSource *source = g_main_context_find_source_by_id(NULL, data->bus_watch_id);
if (source && !g_source_is_destroyed(source)) {
g_source_remove(data->bus_watch_id);
printf(" Cleanup: Bus watch removed (ID: %u).\n", data->bus_watch_id);
} else { /* Source already gone or destroyed */ }
data->bus_watch_id = 0;
} else {
printf(" Cleanup: Bus watch ID was 0.\n");
}
if (data->pipeline) {
printf(" Cleanup: Setting pipeline to NULL state...\n");
gst_element_set_state(data->pipeline, GST_STATE_NULL);
printf(" Cleanup: Pipeline state set to NULL.\n");
printf(" Cleanup: Unreffing pipeline...\n");
gst_object_unref(data->pipeline);
// Clear all pipeline-related pointers
data->pipeline = NULL;
data->uridecodebin = NULL;
data->videosink = NULL;
data->videoconvert = NULL;
data->audiosink = NULL;
data->audioconvert = NULL;
data->audioresample = NULL;
printf(" Cleanup: Pipeline unreffed.\n");
} else {
printf(" Cleanup: Pipeline already NULL.\n");
}
if (data->video_uri) {
g_free(data->video_uri);
data->video_uri = NULL;
printf(" Cleanup: Video URI freed.\n");
}
printf("Cleanup: GStreamer cleanup complete.\n");
}
// Callback when the GTK window is destroyed
static void on_destroy_cb(GtkWidget *widget, gpointer user_data) {
AppData *data = (AppData *)user_data;
g_assert (data != NULL);
printf("Window destroy callback triggered (widget: %p).\n", widget);
cleanup_gstreamer(data); // Ensure GStreamer stops when window closes
if (data->window == GTK_WINDOW(widget)) {
printf(" Destroyed widget is our main window, clearing pointers.\n");
data->window = NULL;
data->picture = NULL;
} else if (data->window != NULL) {
printf(" Warning: Destroyed widget (%p) is not our main window (%p)?\n", widget, data->window);
} else {
printf(" Main window pointer was already NULL during destroy callback.\n");
}
}