Skip to content
Snippets Groups Projects
Select Git revision
  • e69dbf02cd6064cf8f27753669043526995eb2ab
  • master default protected
  • release/202106
  • release/202104
  • release/202101
  • release/202012
  • release/202005
  • release/202001
  • release/201912
  • release/201911
  • release/windows-test/201910
  • release/201908
  • release/201906
  • release/201905
  • release/201904
  • release/201903
  • release/201902
  • release/201901
  • release/201812
  • release/201811
  • release/201808
  • 1.0.0
  • 0.3.0
  • 0.2.1
  • 0.2.0
  • 0.1.0
26 results

video_widget.cpp

Blame
  • Code owners
    Assign users and groups as approvers for specific file changes. Learn more.
    video_widget.cpp 31.26 KiB
    /*
     *  Copyright (C) 2015-2019 Savoir-faire Linux Inc.
     *  Author: Stepan Salenikovich <stepan.salenikovich@savoirfairelinux.com>
     *
     *  This program is free software; you can redistribute it and/or modify
     *  it under the terms of the GNU General Public License as published by
     *  the Free Software Foundation; either version 3 of the License, or
     *  (at your option) any later version.
     *
     *  This program is distributed in the hope that it will be useful,
     *  but WITHOUT ANY WARRANTY; without even the implied warranty of
     *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     *  GNU General Public License for more details.
     *
     *  You should have received a copy of the GNU General Public License
     *  along with this program; if not, write to the Free Software
     *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301 USA.
     */
    
    #include "video_widget.h"
    
    // std
    #include <atomic>
    #include <mutex>
    #include <string>
    
    // gtk
    #include <glib/gi18n.h>
    #include <clutter/clutter.h>
    #include <clutter-gtk/clutter-gtk.h>
    #include <glib/gi18n.h>
    
    // LRC
    #include <api/avmodel.h>
    #include <smartinfohub.h>
    #include <QSize>
    
    // gnome client
    #include "../defines.h"
    #include "xrectsel.h"
    
    static constexpr int VIDEO_LOCAL_SIZE            = 150;
    static constexpr int VIDEO_LOCAL_OPACITY_DEFAULT = 255; /* out of 255 */
    static constexpr const char* JOIN_CALL_KEY = "call_data";
    
    /* check video frame queues at this rate;
     * use 30 ms (about 30 fps) since we don't expect to
     * receive video frames faster than that */
    static constexpr int FRAME_RATE_PERIOD           = 30;
    
    enum SnapshotStatus {
        NOTHING,
        HAS_TO_TAKE_ONE,
        HAS_A_NEW_ONE
    };
    
    struct _VideoWidgetClass {
        GtkClutterEmbedClass parent_class;
    };
    
    struct _VideoWidget {
        GtkClutterEmbed parent;
    };
    
    typedef struct _VideoWidgetPrivate VideoWidgetPrivate;
    
    typedef struct _VideoWidgetRenderer VideoWidgetRenderer;
    
    struct _VideoWidgetPrivate {
        ClutterActor            *video_container;
    
        /* remote peer data */
        VideoWidgetRenderer     *remote;
    
        /* local peer data */
        VideoWidgetRenderer     *local;
        bool show_preview {true};
    
        guint                    frame_timeout_source;
    
        /* new renderers should be put into the queue for processing by a g_timeout
         * function whose id should be saved into renderer_timeout_source;
         * this way when the VideoWidget object is destroyed, we do not try
         * to process any new renderers by stoping the g_timeout function.
         */
        guint                    renderer_timeout_source;
        GAsyncQueue             *new_renderer_queue;
    
        GtkWidget               *popup_menu;
    
        lrc::api::AVModel* avModel_;
    };
    
    struct _VideoWidgetRenderer {
        VideoRendererType        type;
        ClutterActor            *actor;
        ClutterAction           *drag_action;
        const lrc::api::video::Renderer* v_renderer;
        GdkPixbuf               *snapshot;
        std::mutex               run_mutex;
        bool                     running;
        SnapshotStatus           snapshot_status;
    
        /* show_black_frame is used to request the actor to render a black image;
         * this will take over 'running', ie: a black frame will be rendered even if
         * the Video::Renderer is not running;
         * this will be set back to false once the black frame is rendered
         */
        std::atomic_bool         show_black_frame;
        QMetaObject::Connection  render_stop;
        QMetaObject::Connection  render_start;
    };
    
    G_DEFINE_TYPE_WITH_PRIVATE(VideoWidget, video_widget, GTK_CLUTTER_TYPE_EMBED);
    
    #define VIDEO_WIDGET_GET_PRIVATE(obj) (G_TYPE_INSTANCE_GET_PRIVATE ((obj), VIDEO_WIDGET_TYPE, VideoWidgetPrivate))
    
    /* static prototypes */
    static gboolean check_frame_queue              (VideoWidget *);
    static void     renderer_stop                  (VideoWidgetRenderer *);
    static void     renderer_start                 (VideoWidgetRenderer *);
    static gboolean check_renderer_queue           (VideoWidget *);
    static void     free_video_widget_renderer     (VideoWidgetRenderer *);
    static void     video_widget_add_renderer      (VideoWidget *, VideoWidgetRenderer *);
    
    /* signals */
    enum {
        SNAPSHOT_SIGNAL,
        LAST_SIGNAL
    };
    
    static guint video_widget_signals[LAST_SIGNAL] = { 0 };
    
    
    /*
     * video_widget_dispose()
     *
     * The dispose function for the video_widget class.
     */
    static void
    video_widget_dispose(GObject *object)
    {
        VideoWidget *self = VIDEO_WIDGET(object);
        VideoWidgetPrivate *priv = VIDEO_WIDGET_GET_PRIVATE(self);
    
        /* dispose may be called multiple times, make sure
         * not to call g_source_remove more than once */
        if (priv->frame_timeout_source) {
            g_source_remove(priv->frame_timeout_source);
            priv->frame_timeout_source = 0;
        }
    
        if (priv->renderer_timeout_source) {
            g_source_remove(priv->renderer_timeout_source);
            priv->renderer_timeout_source = 0;
        }
    
        if (priv->new_renderer_queue) {
            g_async_queue_unref(priv->new_renderer_queue);
            priv->new_renderer_queue = NULL;
        }
    
        gtk_widget_destroy(priv->popup_menu);
    
        G_OBJECT_CLASS(video_widget_parent_class)->dispose(object);
    }
    
    
    /*
     * video_widget_finalize()
     *
     * The finalize function for the video_widget class.
     */
    static void
    video_widget_finalize(GObject *object)
    {
        VideoWidget *self = VIDEO_WIDGET(object);
        VideoWidgetPrivate *priv = VIDEO_WIDGET_GET_PRIVATE(self);
    
        free_video_widget_renderer(priv->local);
        free_video_widget_renderer(priv->remote);
    
        G_OBJECT_CLASS(video_widget_parent_class)->finalize(object);
    }
    
    /*
     * video_widget_class_init()
     *
     * This function init the video_widget_class.
     */
    static void
    video_widget_class_init(VideoWidgetClass *klass)
    {
        GObjectClass *object_class = G_OBJECT_CLASS(klass);
    
        /* override method */
        object_class->dispose = video_widget_dispose;
        object_class->finalize = video_widget_finalize;
    
        /* add snapshot signal */
        video_widget_signals[SNAPSHOT_SIGNAL] = g_signal_new("snapshot-taken",
                     G_TYPE_FROM_CLASS(klass),
                     (GSignalFlags) (G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION),
                     0,
                     nullptr,
                     nullptr,
                     g_cclosure_marshal_VOID__VOID,
                     G_TYPE_NONE, 0);
    
    }
    
    static void
    on_allocation_changed(ClutterActor *video_area, G_GNUC_UNUSED GParamSpec *pspec, VideoWidget *self)
    {
        g_return_if_fail(IS_VIDEO_WIDGET(self));
        VideoWidgetPrivate *priv = VIDEO_WIDGET_GET_PRIVATE(self);
    
        auto actor = priv->local->actor;
        auto drag_action = priv->local->drag_action;
    
        ClutterActorBox actor_box;
        clutter_actor_get_allocation_box(actor, &actor_box);
        gfloat actor_w = clutter_actor_box_get_width(&actor_box);
        gfloat actor_h = clutter_actor_box_get_height(&actor_box);
    
        ClutterActorBox area_box;
        clutter_actor_get_allocation_box(video_area, &area_box);
        gfloat area_w = clutter_actor_box_get_width(&area_box);
        gfloat area_h = clutter_actor_box_get_height(&area_box);
    
        /* make sure drag area stays within the bounds of the stage */
        ClutterRect *rect = clutter_rect_init (
            clutter_rect_alloc(),
            0, 0,
            area_w - actor_w,
            area_h - actor_h);
        clutter_drag_action_set_drag_area(CLUTTER_DRAG_ACTION(drag_action), rect);
        clutter_rect_free(rect);
    }
    
    static void
    on_drag_begin(G_GNUC_UNUSED ClutterDragAction   *action,
                                ClutterActor        *actor,
                  G_GNUC_UNUSED gfloat               event_x,
                  G_GNUC_UNUSED gfloat               event_y,
                  G_GNUC_UNUSED ClutterModifierType  modifiers,
                  G_GNUC_UNUSED gpointer             user_data)
    {
        /* clear the align constraint when starting to move the preview, otherwise
         * it won't move; save and set its position, to what it was before the
         * constraint was cleared, or else it might jump around */
        gfloat actor_x, actor_y;
        clutter_actor_get_position(actor, &actor_x, &actor_y);
        clutter_actor_clear_constraints(actor);
        clutter_actor_set_position(actor, actor_x, actor_y);
    }
    
    static void
    on_drag_end(G_GNUC_UNUSED ClutterDragAction   *action,
                              ClutterActor        *actor,
                G_GNUC_UNUSED gfloat               event_x,
                G_GNUC_UNUSED gfloat               event_y,
                G_GNUC_UNUSED ClutterModifierType  modifiers,
                              ClutterActor        *video_area)
    {
        ClutterActorBox area_box;
        clutter_actor_get_allocation_box(video_area, &area_box);
        gfloat area_w = clutter_actor_box_get_width(&area_box);
        gfloat area_h = clutter_actor_box_get_height(&area_box);
    
        gfloat actor_x, actor_y;
        clutter_actor_get_position(actor, &actor_x, &actor_y);
        gfloat actor_w, actor_h;
        clutter_actor_get_size(actor, &actor_w, &actor_h);
    
        area_w -= actor_w;
        area_h -= actor_h;
    
        /* add new constraints to make sure the preview stays in about the same location
         * relative to the rest of the video when resizing */
        ClutterConstraint *constraint_x = clutter_align_constraint_new(video_area,
            CLUTTER_ALIGN_X_AXIS, actor_x/area_w);
        clutter_actor_add_constraint(actor, constraint_x);
    
        ClutterConstraint *constraint_y = clutter_align_constraint_new(video_area,
            CLUTTER_ALIGN_Y_AXIS, actor_y/area_h);
        clutter_actor_add_constraint(actor, constraint_y);
    }
    
    
    /*
     * video_widget_init()
     *
     * This function init the video_widget.
     * - init clutter
     * - init all the widget members
     */
    static void
    video_widget_init(VideoWidget *self)
    {
        VideoWidgetPrivate *priv = VIDEO_WIDGET_GET_PRIVATE(self);
    
        auto stage = gtk_clutter_embed_get_stage(GTK_CLUTTER_EMBED(self));
    
        /* layout manager is used to arrange children in space, here we ask clutter
         * to align children to fill the space when resizing the window */
        clutter_actor_set_layout_manager(stage,
            clutter_bin_layout_new(CLUTTER_BIN_ALIGNMENT_FILL, CLUTTER_BIN_ALIGNMENT_FILL));
    
        /* add a scene container where we can add and remove our actors */
        priv->video_container = clutter_actor_new();
        clutter_actor_set_background_color(priv->video_container, CLUTTER_COLOR_Black);
        clutter_actor_add_child(stage, priv->video_container);
    
        /* init the remote and local structs */
        priv->remote = g_new0(VideoWidgetRenderer, 1);
        priv->local = g_new0(VideoWidgetRenderer, 1);
    
        /* arrange remote actors */
        priv->remote->actor = clutter_actor_new();
        clutter_actor_insert_child_below(priv->video_container, priv->remote->actor, NULL);
        /* the remote camera must always fill the container size */
        ClutterConstraint *constraint = clutter_bind_constraint_new(priv->video_container,
                                                                    CLUTTER_BIND_SIZE, 0);
        clutter_actor_add_constraint(priv->remote->actor, constraint);
    
        /* arrange local actor */
        priv->local->actor = clutter_actor_new();
        clutter_actor_insert_child_above(priv->video_container, priv->local->actor, NULL);
        /* set size to square, but it will stay the aspect ratio when the image is rendered */
        clutter_actor_set_size(priv->local->actor, VIDEO_LOCAL_SIZE, VIDEO_LOCAL_SIZE);
        /* set position constraint to right cornder;
         * this constraint will be removed once the user tries to move the position
         * of the action */
        constraint = clutter_align_constraint_new(priv->video_container,
                                                  CLUTTER_ALIGN_BOTH, 0.99);
        clutter_actor_add_constraint(priv->local->actor, constraint);
        clutter_actor_set_opacity(priv->local->actor,
                                  VIDEO_LOCAL_OPACITY_DEFAULT);
    
        /* add ability for actor to be moved */
        clutter_actor_set_reactive(priv->local->actor, TRUE);
        priv->local->drag_action = clutter_drag_action_new();
        clutter_actor_add_action(priv->local->actor, priv->local->drag_action);
    
        g_signal_connect(priv->local->drag_action, "drag-begin", G_CALLBACK(on_drag_begin), NULL);
        g_signal_connect_after(priv->local->drag_action, "drag-end", G_CALLBACK(on_drag_end), stage);
    
        /* make sure the actor stays within the bounds of the stage */
        g_signal_connect(stage, "notify::allocation", G_CALLBACK(on_allocation_changed), self);
    
        /* Init the timeout source which will check the for new frames.
         * The priority must be lower than GTK drawing events
         * (G_PRIORITY_HIGH_IDLE + 20) so that this timeout source doesn't choke
         * the main loop on slower machines.
         */
        priv->frame_timeout_source = g_timeout_add_full(G_PRIORITY_DEFAULT_IDLE,
                                                        FRAME_RATE_PERIOD,
                                                        (GSourceFunc)check_frame_queue,
                                                        self,
                                                        NULL);
    
        /* init new renderer queue */
        priv->new_renderer_queue = g_async_queue_new_full((GDestroyNotify)free_video_widget_renderer);
        /* check new render every 30 ms (30ms is "fast enough");
         * we don't use an idle function so it doesn't consume cpu needlessly */
        priv->renderer_timeout_source= g_timeout_add_full(G_PRIORITY_DEFAULT_IDLE,
                                                          30,
                                                          (GSourceFunc)check_renderer_queue,
                                                          self,
                                                          NULL);
    
    
        /* drag & drop files as video sources */
        gtk_drag_dest_set(GTK_WIDGET(self), GTK_DEST_DEFAULT_ALL, NULL, 0, (GdkDragAction)(GDK_ACTION_COPY | GDK_ACTION_PRIVATE));
        gtk_drag_dest_add_uri_targets(GTK_WIDGET(self));
    
        priv->popup_menu = gtk_menu_new();
    }
    
    /*
     * video_widget_on_drag_data_received()
     *
     * Handle dragged data in the video widget window.
     * Dropping an image causes the client to switch the video input to that image.
     */
    void video_widget_on_drag_data_received(G_GNUC_UNUSED GtkWidget *self,
                                            G_GNUC_UNUSED GdkDragContext *context,
                                            G_GNUC_UNUSED gint x,
                                            G_GNUC_UNUSED gint y,
                                                          GtkSelectionData *selection_data,
                                            G_GNUC_UNUSED guint info,
                                            G_GNUC_UNUSED guint32 time,
                                            G_GNUC_UNUSED gpointer data)
    {
        auto* priv = VIDEO_WIDGET_GET_PRIVATE(self);
        g_return_if_fail(priv);
        gchar **uris = gtk_selection_data_get_uris(selection_data);
        if (uris && *uris && priv->avModel_) {
            priv->avModel_->setInputFile(*uris);
        }
        g_strfreev(uris);
    }
    
    static void
    switch_video_input(GtkWidget *widget, GtkWidget *parent)
    {
        auto* priv = VIDEO_WIDGET_GET_PRIVATE(parent);
        g_return_if_fail(priv);
    
        auto* label = gtk_menu_item_get_label(GTK_MENU_ITEM(widget));
        g_return_if_fail(label);
    
        if (priv->avModel_) {
            auto device_id = priv->avModel_->getDeviceIdFromName(label);
            if (device_id.empty()) {
                g_warning("switch_video_input couldn't find device: %s", label);
                return;
            }
            priv->avModel_->switchInputTo(device_id, priv->remote->v_renderer->getId());
        }
    }
    
    static void
    switch_video_input_screen_area(G_GNUC_UNUSED GtkWidget *item, GtkWidget *parent)
    {
        auto* priv = VIDEO_WIDGET_GET_PRIVATE(parent);
        unsigned x, y;
        unsigned width, height;
    
        /* try to get the dispaly or default to 0 */
        QString display_env{getenv("DISPLAY")};
        int display = 0;
    
        if (!display_env.isEmpty()) {
            auto list = display_env.split(":", QString::SkipEmptyParts);
            /* should only be one display, so get the first one */
            if (list.size() > 0) {
                display = list.at(0).toInt();
                g_debug("sharing screen from DISPLAY %d", display);
            }
        }
    
        x = y = width = height = 0;
    
        xrectsel(&x, &y, &width, &height);
    
        if (!width || !height) {
            x = y = 0;
            width = gdk_screen_width();
            height = gdk_screen_height();
        }
    
        if (priv->avModel_)
            priv->avModel_->setDisplay(display, x, y, width, height, priv->remote->v_renderer->getId());
    }
    
    static void
    switch_video_input_monitor(G_GNUC_UNUSED GtkWidget *item, GtkWidget *parent)
    {
        auto* priv = VIDEO_WIDGET_GET_PRIVATE(parent);
        unsigned x, y;
        unsigned width, height;
    
        /* try to get the dispaly or default to 0 */
        QString display_env{getenv("DISPLAY")};
        int display = 0;
    
        if (!display_env.isEmpty()) {
            auto list = display_env.split(":", QString::SkipEmptyParts);
            /* should only be one display, so get the first one */
            if (list.size() > 0) {
                display = list.at(0).toInt();
                g_debug("sharing screen from DISPLAY %d", display);
            }
        }
    
        x = y = 0;
        width = gdk_screen_width();
        height = gdk_screen_height();
    
        if (priv->avModel_)
            priv->avModel_->setDisplay(display, x, y, width, height, priv->remote->v_renderer->getId());
    }
    
    
    static void
    switch_video_input_file(G_GNUC_UNUSED GtkWidget *item, GtkWidget *parent)
    {
        auto* priv = VIDEO_WIDGET_GET_PRIVATE(parent);
        if (parent && GTK_IS_WIDGET(parent)) {
            // get parent window
            parent = gtk_widget_get_toplevel(GTK_WIDGET(parent));
        }
    
        GtkWidget *dialog = gtk_file_chooser_dialog_new(
                "Choose File",
                GTK_WINDOW(parent),
                GTK_FILE_CHOOSER_ACTION_OPEN,
                "_Cancel", GTK_RESPONSE_CANCEL,
                "_Open", GTK_RESPONSE_ACCEPT,
                NULL);
    
        if (gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_ACCEPT) {
            gchar *uri = gtk_file_chooser_get_uri(GTK_FILE_CHOOSER(dialog));
    
            if (uri && priv->avModel_) {
                priv->avModel_->setInputFile(uri, priv->remote->v_renderer->getId());
                g_free(uri);
            }
        }
    
        gtk_widget_destroy(dialog);
    }
    
    /*
     * video_widget_on_button_press_in_screen_event()
     *
     * Handle button event in the video screen.
     */
    gboolean
    video_widget_on_button_press_in_screen_event(VideoWidget *self,  GdkEventButton *event, G_GNUC_UNUSED gpointer)
    {
        // check for right click
        if (event->button != BUTTON_RIGHT_CLICK || event->type != GDK_BUTTON_PRESS)
            return FALSE;
    
        // update menu with available video sources
        auto priv = VIDEO_WIDGET_GET_PRIVATE(self);
        g_return_val_if_fail(priv->remote->v_renderer, false);
        auto menu = priv->popup_menu;
    
        gtk_container_forall(GTK_CONTAINER(menu), (GtkCallback)gtk_widget_destroy,
            nullptr);
    
        // list available devices and check off the active device
        auto device_list = priv->avModel_->getDevices();
        auto active_device = priv->avModel_->getCurrentRenderedDevice(
            priv->remote->v_renderer->getId());
    
        g_debug("active_device.name: %s", active_device.name.c_str());
    
        for (auto device : device_list) {
            auto settings = priv->avModel_->getDeviceSettings(device);
            GtkWidget *item = gtk_check_menu_item_new_with_mnemonic(settings.name.c_str());
            gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
            gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(item),
                device == active_device.name
                && active_device.type == lrc::api::video::DeviceType::CAMERA);
            g_signal_connect(item, "activate", G_CALLBACK(switch_video_input),
                self);
        }
    
        /* add separator */
        gtk_menu_shell_append(GTK_MENU_SHELL(menu), gtk_separator_menu_item_new());
    
        // add screen area as an input
        GtkWidget *item = gtk_check_menu_item_new_with_mnemonic(_("Share _screen area"));
        gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
        // TODO(sblin) only set active if fullscreen
        gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(item),
            active_device.type == lrc::api::video::DeviceType::DISPLAY);
        g_signal_connect(item, "activate", G_CALLBACK(switch_video_input_screen_area), self);
    
        // add screen area as an input
        item = gtk_check_menu_item_new_with_mnemonic(_("Share _monitor"));
        gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
        gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(item),
            active_device.type == lrc::api::video::DeviceType::DISPLAY);
        g_signal_connect(item, "activate", G_CALLBACK(switch_video_input_monitor), self);
    
        // add file as an input
        item = gtk_check_menu_item_new_with_mnemonic(_("Stream _file"));
        gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
        gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(item),
            active_device.type == lrc::api::video::DeviceType::FILE);
        g_signal_connect(item, "activate", G_CALLBACK(switch_video_input_file), self);
    
        // add separator
        gtk_menu_shell_append(GTK_MENU_SHELL(menu), gtk_separator_menu_item_new());
    
        // add SmartInfo
        item = gtk_check_menu_item_new_with_mnemonic(_("Show advanced information"));
        gtk_actionable_set_action_name(GTK_ACTIONABLE(item), "app.display-smartinfo");
        gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
        gtk_widget_insert_action_group(menu, "app", G_ACTION_GROUP(g_application_get_default()));
    
        // show menu
        gtk_widget_show_all(menu);
        gtk_menu_popup(GTK_MENU(menu), NULL, NULL, NULL, NULL, event->button, event->time);
    
        return TRUE; // event has been fully handled
    }
    
    static void
    free_pixels(guchar *pixels, gpointer)
    {
        g_free(pixels);
    }
    
    static void
    clutter_render_image(VideoWidgetRenderer* wg_renderer)
    {
        auto actor = wg_renderer->actor;
        g_return_if_fail(CLUTTER_IS_ACTOR(actor));
    
        if (wg_renderer->show_black_frame) {
            /* render a black frame set the bool back to false, this is likely done
             * when the renderer is stopped so we ignore whether or not it is running
             */
            if (auto image_old = clutter_actor_get_content(actor)) {
                gfloat width;
                gfloat height;
                if (clutter_content_get_preferred_size(image_old, &width, &height)) {
                    /* NOTE: this is a workaround for #72531, a crash which occurs
                     * in cogl < 1.18. We allocate a black frame of the same size
                     * as the previous image, instead of simply setting an empty or
                     * a NULL ClutterImage.
                     */
                    auto image_empty = clutter_image_new();
                    if (auto empty_data = (guint8 *)g_try_malloc0((gsize)width * height * 4)) {
                        GError* error = NULL;
                        clutter_image_set_data(
                                CLUTTER_IMAGE(image_empty),
                                empty_data,
                                COGL_PIXEL_FORMAT_BGRA_8888,
                                (guint)width,
                                (guint)height,
                                (guint)width*4,
                                &error);
                        if (error) {
                            g_warning("error rendering empty image to clutter: %s", error->message);
                            g_clear_error(&error);
                            g_object_unref(image_empty);
                            return;
                        }
                        clutter_actor_set_content(actor, image_empty);
                        g_object_unref(image_empty);
                        g_free(empty_data);
                    } else {
                        clutter_actor_set_content(actor, NULL);
                    }
                } else {
                    clutter_actor_set_content(actor, NULL);
                }
            }
            wg_renderer->show_black_frame = false;
            return;
        }
    
        ClutterContent *image_new = nullptr;
    
        {
            /* the following must be done under lock in case a 'stopped' signal is
             * received during rendering; otherwise the mem could become invalid */
            std::lock_guard<std::mutex> lock(wg_renderer->run_mutex);
    
            if (!wg_renderer->running)
                return;
    
            if (!wg_renderer->v_renderer)
                return;
    
            auto v_renderer = wg_renderer->v_renderer;
            if (!v_renderer)
                return;
            auto frame_data = v_renderer->currentFrame().ptr;
            if (!frame_data)
                return;
    
            image_new = clutter_image_new();
            g_return_if_fail(image_new);
    
            const auto& res = v_renderer->size();
            gint BPP = 4; /* BGRA */
            gint ROW_STRIDE = BPP * res.width();
    
            GError *error = nullptr;
            clutter_image_set_data(
                CLUTTER_IMAGE(image_new),
                frame_data,
                COGL_PIXEL_FORMAT_BGRA_8888,
                res.width(),
                res.height(),
                ROW_STRIDE,
                &error);
            if (error) {
                g_warning("error rendering image to clutter: %s", error->message);
                g_clear_error(&error);
                g_object_unref (image_new);
                return;
            }
    
            if (wg_renderer->snapshot_status == HAS_TO_TAKE_ONE) {
                guchar *pixbuf_frame_data = (guchar *)g_malloc(res.width() * res.height() * 3);
    
                BPP = 3; /* RGB */
                gint ROW_STRIDE = BPP * res.width();
    
                /* conversion from BGRA to RGB */
                for(int i = 0, j = 0 ; i < res.width() * res.height() * 4 ; i += 4, j += 3 ) {
                    pixbuf_frame_data[j + 0] = frame_data[i + 2];
                    pixbuf_frame_data[j + 1] = frame_data[i + 1];
                    pixbuf_frame_data[j + 2] = frame_data[i + 0];
                }
    
                if (wg_renderer->snapshot) {
                    g_object_unref(wg_renderer->snapshot);
                    wg_renderer->snapshot = nullptr;
                }
    
                wg_renderer->snapshot = gdk_pixbuf_new_from_data(pixbuf_frame_data,
                                                                 GDK_COLORSPACE_RGB, FALSE, 8,
                                                                 res.width(), res.height(),
                                                                 ROW_STRIDE, free_pixels, NULL);
    
                wg_renderer->snapshot_status = HAS_A_NEW_ONE;
    
            }
        }
    
        clutter_actor_set_content(actor, image_new);
        g_object_unref (image_new);
    
        /* note: we must set the content gravity be "resize aspect" after setting the image data to make sure
         * that the aspect ratio is correct
         */
        clutter_actor_set_content_gravity(actor, CLUTTER_CONTENT_GRAVITY_RESIZE_ASPECT);
    }
    
    static gboolean
    check_frame_queue(VideoWidget *self)
    {
        g_return_val_if_fail(IS_VIDEO_WIDGET(self), FALSE);
        VideoWidgetPrivate *priv = VIDEO_WIDGET_GET_PRIVATE(self);
    
        /* display renderer's frames */
        if (priv->show_preview)
            clutter_render_image(priv->local);
        clutter_render_image(priv->remote);
        if (priv->remote->snapshot_status == HAS_A_NEW_ONE) {
            priv->remote->snapshot_status = NOTHING;
            g_signal_emit(G_OBJECT(self), video_widget_signals[SNAPSHOT_SIGNAL], 0);
        }
    
        return TRUE; /* keep going */
    }
    
    static void
    renderer_stop(VideoWidgetRenderer *renderer)
    {
        {
            /* must do this under lock, in case the rendering is taking place when
             * this signal is received */
            std::lock_guard<std::mutex> lock(renderer->run_mutex);
            renderer->running = false;
        }
        /* ask to show a black frame */
        renderer->show_black_frame = true;
    }
    
    static void
    renderer_start(VideoWidgetRenderer *renderer)
    {
        {
            std::lock_guard<std::mutex> lock(renderer->run_mutex);
            renderer->running = true;
        }
        renderer->show_black_frame = false;
    }
    
    static void
    free_video_widget_renderer(VideoWidgetRenderer *renderer)
    {
        QObject::disconnect(renderer->render_stop);
        QObject::disconnect(renderer->render_start);
        if (renderer->snapshot)
            g_object_unref(renderer->snapshot);
        g_free(renderer);
    }
    
    static void
    video_widget_add_renderer(VideoWidget *self, VideoWidgetRenderer *new_video_renderer)
    {
        g_return_if_fail(IS_VIDEO_WIDGET(self));
        g_return_if_fail(new_video_renderer);
        g_return_if_fail(new_video_renderer->v_renderer);
    
        VideoWidgetPrivate *priv = VIDEO_WIDGET_GET_PRIVATE(self);
    
        /* update the renderer */
        switch(new_video_renderer->type) {
            case VIDEO_RENDERER_REMOTE:
                /* swap the remote renderer */
                new_video_renderer->actor = priv->remote->actor;
                free_video_widget_renderer(priv->remote);
                priv->remote = new_video_renderer;
                /* reset the content gravity so that the aspect ratio gets properly
                 * reset if it chagnes */
                clutter_actor_set_content_gravity(priv->remote->actor,
                                                  CLUTTER_CONTENT_GRAVITY_RESIZE_FILL);
                break;
            case VIDEO_RENDERER_LOCAL:
                /* swap the remote renderer */
                new_video_renderer->actor = priv->local->actor;
                new_video_renderer->drag_action = priv->local->drag_action;
                free_video_widget_renderer(priv->local);
                priv->local = new_video_renderer;
                /* reset the content gravity so that the aspect ratio gets properly
                 * reset if it chagnes */
                clutter_actor_set_content_gravity(priv->local->actor,
                                                  CLUTTER_CONTENT_GRAVITY_RESIZE_FILL);
                break;
            case VIDEO_RENDERER_COUNT:
                break;
        }
    }
    
    static gboolean
    check_renderer_queue(VideoWidget *self)
    {
        g_return_val_if_fail(IS_VIDEO_WIDGET(self), G_SOURCE_REMOVE);
        VideoWidgetPrivate *priv = VIDEO_WIDGET_GET_PRIVATE(self);
    
        /* get all the renderers in the queue */
        VideoWidgetRenderer *new_video_renderer = (VideoWidgetRenderer *)g_async_queue_try_pop(priv->new_renderer_queue);
        while (new_video_renderer) {
            video_widget_add_renderer(self, new_video_renderer);
            new_video_renderer = (VideoWidgetRenderer *)g_async_queue_try_pop(priv->new_renderer_queue);
        }
    
        return G_SOURCE_CONTINUE;
    }
    
    /*
     * video_widget_new()
     *
     * The function use to create a new video_widget
     */
    GtkWidget*
    video_widget_new(void)
    {
        GtkWidget *self = (GtkWidget *)g_object_new(VIDEO_WIDGET_TYPE, NULL);
        return self;
    }
    
    void
    video_widget_add_new_renderer(VideoWidget* self, lrc::api::AVModel* avModel,
        const lrc::api::video::Renderer* renderer, VideoRendererType type)
    {
        g_return_if_fail(IS_VIDEO_WIDGET(self));
        VideoWidgetPrivate *priv = VIDEO_WIDGET_GET_PRIVATE(self);
        priv->avModel_ = avModel;
    
        /* if the renderer is nullptr, there is nothing to be done */
        if (!renderer) return;
    
        VideoWidgetRenderer *new_video_renderer = g_new0(VideoWidgetRenderer, 1);
        new_video_renderer->v_renderer = renderer;
        new_video_renderer->type = type;
    
        if (renderer->isRendering())
            renderer_start(new_video_renderer);
    
        new_video_renderer->render_stop = QObject::connect(
            &*avModel,
            &lrc::api::AVModel::rendererStopped,
            [=](const std::string& id) {
                if (renderer->getId() == id)
                    renderer_stop(new_video_renderer);
            });
    
        new_video_renderer->render_start = QObject::connect(
            &*avModel,
            &lrc::api::AVModel::rendererStarted,
            [=](const std::string& id) {
                if (renderer->getId() == id)
                    renderer_start(new_video_renderer);
            });
    
        g_async_queue_push(priv->new_renderer_queue, new_video_renderer);
    }
    
    void
    video_widget_take_snapshot(VideoWidget *self)
    {
        g_return_if_fail(IS_VIDEO_WIDGET(self));
        VideoWidgetPrivate *priv = VIDEO_WIDGET_GET_PRIVATE(self);
    
        priv->remote->snapshot_status = HAS_TO_TAKE_ONE;
    }
    
    GdkPixbuf*
    video_widget_get_snapshot(VideoWidget *self)
    {
        g_return_val_if_fail(IS_VIDEO_WIDGET(self), nullptr);
        VideoWidgetPrivate *priv = VIDEO_WIDGET_GET_PRIVATE(self);
    
        return priv->remote->snapshot;
    }
    
    void
    video_widget_set_preview_visible(VideoWidget *self, bool show)
    {
        g_return_if_fail(IS_VIDEO_WIDGET(self));
        VideoWidgetPrivate *priv = VIDEO_WIDGET_GET_PRIVATE(self);
        if (priv) {
            priv->show_preview = show;
        }
    }