# Gtk4paintablesink opens two windows on macOS not Linux

**URL:** https://discourse.gstreamer.org/t/gtk4paintablesink-opens-two-windows-on-macos-not-linux/4606
**Category:** General Discussion
**Tags:** macos
**Created:** [May 13, 2025, 7:57pm UTC](https://discourse.gstreamer.org/t/gtk4paintablesink-opens-two-windows-on-macos-not-linux/4606 "2025-05-13T19:57:50Z")
**Posts on this page:** 5
**Page:** 1

<div class="post-metadata">

### Author: ![GR99](https://discourse.gstreamer.org/letter_avatar_proxy/v4/letter/g/e79b87/32.png) [@GR99](https://discourse.gstreamer.org/u/GR99)
#### Post date: [May 13, 2025, 7:57pm UTC](https://discourse.gstreamer.org/t/gtk4paintablesink-opens-two-windows-on-macos-not-linux/4606/1 "2025-05-13T19:57:50Z")

</div>

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.

---

<div class="post-metadata">

### Author: ![slomo](https://discourse.gstreamer.org/user_avatar/discourse.gstreamer.org/slomo/32/4_2.png) [@slomo](https://discourse.gstreamer.org/u/slomo)
#### Post date: [May 20, 2025, 1:05pm UTC](https://discourse.gstreamer.org/t/gtk4paintablesink-opens-two-windows-on-macos-not-linux/4606/2 "2025-05-20T13:05:57Z")

</div>

Please provide code that shows this problem

---

<div class="post-metadata">

### Author: ![GR99](https://discourse.gstreamer.org/letter_avatar_proxy/v4/letter/g/e79b87/32.png) [@GR99](https://discourse.gstreamer.org/u/GR99)
#### Post date: [May 20, 2025, 11:14pm UTC](https://discourse.gstreamer.org/t/gtk4paintablesink-opens-two-windows-on-macos-not-linux/4606/3 "2025-05-20T23:14:39Z")

</div>

Thanks, please see below. Again, runs in one window on Linux, two on macOS. Compiles and runs this way - thanks for any help:

```shell
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:

```cpp
#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");
     }
}

```

---

<div class="post-metadata">

### Author: ![slomo](https://discourse.gstreamer.org/user_avatar/discourse.gstreamer.org/slomo/32/4_2.png) [@slomo](https://discourse.gstreamer.org/u/slomo)
#### Post date: [May 21, 2025, 9:59am UTC](https://discourse.gstreamer.org/t/gtk4paintablesink-opens-two-windows-on-macos-not-linux/4606/4 "2025-05-21T09:59:40Z")

</div>

This code is supposed to create only a single window, yes. You’ll want to report this to [Sign in · GitLab](https://gitlab.gnome.org/GNOME/gtk/-/issues/new)

---

<div class="post-metadata">

### Author: ![GR99](https://discourse.gstreamer.org/letter_avatar_proxy/v4/letter/g/e79b87/32.png) [@GR99](https://discourse.gstreamer.org/u/GR99)
#### Post date: [June 5, 2025, 8:33pm UTC](https://discourse.gstreamer.org/t/gtk4paintablesink-opens-two-windows-on-macos-not-linux/4606/5 "2025-06-05T20:33:43Z")

</div>

Thanks, I’ve posted the issue here:

> **[Gtk4paintablesink opens two windows on macOS not Linux (#7579) · Issues ·...](https://gitlab.gnome.org/GNOME/gtk/-/issues/7579)**
>
> I was able to get gtk4paintablesink working on my macOS system (Apple Silicon, macOS Sonoma & Sequoia) and wrote a small C program that opens a .mp4 file...
