Gtk4paintablesink opens two windows on macOS not Linux

I was able to get gtk4paintablesink working on my macOS system (Apple Silicon, macOS Sonoma) and wrote a small C program that opens a .mp4 file from the commandline. It works, but on macOS it opens two windows, slightly offset, one with the title, the other without a title. Video plays in both & I’m getting audio and both close correctly on file end with no errors.

This same code works perfectly on Linux - only 1 window opens and closes on file end.

Feedback I’ve gotten from various sources suggests this is due to: “On macOS, GTK applications can have their “activate” signal triggered multiple times during startup.”

Is this true? If so how can I fix? I can post my code if that will help. Please advise as I’m trying to get this to work on both macOS and Linux. Thanks.

Please provide code that shows this problem

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, &current_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");
     }
}

This code is supposed to create only a single window, yes. You’ll want to report this to Sign in · GitLab