video_widget.cpp 30.8 KB
Newer Older
1
/*
Sébastien Blin's avatar
Sébastien Blin committed
2
 *  Copyright (C) 2015-2019 Savoir-faire Linux Inc.
3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
 *  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"

22 23 24 25 26 27
// std
#include <atomic>
#include <mutex>
#include <string>

// gtk
28
#include <glib/gi18n.h>
29 30
#include <clutter/clutter.h>
#include <clutter-gtk/clutter-gtk.h>
31 32 33 34 35
#include <glib/gi18n.h>

// LRC
#include <api/avmodel.h>
#include <smartinfohub.h>
36
#include <QSize>
37 38

// gnome client
39
#include "../defines.h"
Stepan Salenikovich's avatar
Stepan Salenikovich committed
40
#include "xrectsel.h"
41

42 43
static constexpr int VIDEO_LOCAL_SIZE            = 150;
static constexpr int VIDEO_LOCAL_OPACITY_DEFAULT = 255; /* out of 255 */
44
static constexpr const char* JOIN_CALL_KEY = "call_data";
Stepan Salenikovich's avatar
Stepan Salenikovich committed
45

46 47 48
/* 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 */
49
static constexpr int FRAME_RATE_PERIOD           = 30;
50

51 52 53 54 55 56
enum SnapshotStatus {
    NOTHING,
    HAS_TO_TAKE_ONE,
    HAS_A_NEW_ONE
};

57
struct _VideoWidgetClass {
58
    GtkClutterEmbedClass parent_class;
59 60 61
};

struct _VideoWidget {
62
    GtkClutterEmbed parent;
63 64 65 66
};

typedef struct _VideoWidgetPrivate VideoWidgetPrivate;

Stepan Salenikovich's avatar
Stepan Salenikovich committed
67 68
typedef struct _VideoWidgetRenderer VideoWidgetRenderer;

69 70 71 72
struct _VideoWidgetPrivate {
    ClutterActor            *video_container;

    /* remote peer data */
Stepan Salenikovich's avatar
Stepan Salenikovich committed
73
    VideoWidgetRenderer     *remote;
74 75

    /* local peer data */
Stepan Salenikovich's avatar
Stepan Salenikovich committed
76
    VideoWidgetRenderer     *local;
77
    bool show_preview {true};
78 79

    guint                    frame_timeout_source;
80 81 82 83 84 85 86 87

    /* 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;
88 89

    GtkWidget               *popup_menu;
90 91

    lrc::api::AVModel* avModel_;
Stepan Salenikovich's avatar
Stepan Salenikovich committed
92 93 94
};

struct _VideoWidgetRenderer {
95
    VideoRendererType        type;
Stepan Salenikovich's avatar
Stepan Salenikovich committed
96
    ClutterActor            *actor;
97
    ClutterAction           *drag_action;
98
    const lrc::api::video::Renderer* v_renderer;
99
    GdkPixbuf               *snapshot;
100 101
    std::mutex               run_mutex;
    bool                     running;
102
    SnapshotStatus           snapshot_status;
103 104

    /* show_black_frame is used to request the actor to render a black image;
105 106
     * this will take over 'running', ie: a black frame will be rendered even if
     * the Video::Renderer is not running;
107 108 109
     * this will be set back to false once the black frame is rendered
     */
    std::atomic_bool         show_black_frame;
Stepan Salenikovich's avatar
Stepan Salenikovich committed
110 111
    QMetaObject::Connection  render_stop;
    QMetaObject::Connection  render_start;
112 113
};

114
G_DEFINE_TYPE_WITH_PRIVATE(VideoWidget, video_widget, GTK_CLUTTER_TYPE_EMBED);
115 116 117

#define VIDEO_WIDGET_GET_PRIVATE(obj) (G_TYPE_INSTANCE_GET_PRIVATE ((obj), VIDEO_WIDGET_TYPE, VideoWidgetPrivate))

118
/* static prototypes */
119 120 121 122 123 124
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 *);
125

126 127 128 129 130 131 132 133 134
/* signals */
enum {
    SNAPSHOT_SIGNAL,
    LAST_SIGNAL
};

static guint video_widget_signals[LAST_SIGNAL] = { 0 };


135 136 137 138 139 140
/*
 * video_widget_dispose()
 *
 * The dispose function for the video_widget class.
 */
static void
Stepan Salenikovich's avatar
Stepan Salenikovich committed
141
video_widget_dispose(GObject *object)
142
{
Stepan Salenikovich's avatar
Stepan Salenikovich committed
143
    VideoWidget *self = VIDEO_WIDGET(object);
144 145
    VideoWidgetPrivate *priv = VIDEO_WIDGET_GET_PRIVATE(self);

146 147 148 149 150 151 152
    /* 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;
    }

153 154 155 156 157 158 159 160 161 162
    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;
    }

163 164
    gtk_widget_destroy(priv->popup_menu);

Stepan Salenikovich's avatar
Stepan Salenikovich committed
165
    G_OBJECT_CLASS(video_widget_parent_class)->dispose(object);
166 167 168 169 170 171 172 173 174 175 176
}


/*
 * video_widget_finalize()
 *
 * The finalize function for the video_widget class.
 */
static void
video_widget_finalize(GObject *object)
{
Stepan Salenikovich's avatar
Stepan Salenikovich committed
177 178 179
    VideoWidget *self = VIDEO_WIDGET(object);
    VideoWidgetPrivate *priv = VIDEO_WIDGET_GET_PRIVATE(self);

180 181
    free_video_widget_renderer(priv->local);
    free_video_widget_renderer(priv->remote);
Stepan Salenikovich's avatar
Stepan Salenikovich committed
182

183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198
    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;
199 200 201 202 203 204 205 206 207 208 209

    /* 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);

210 211
}

212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289
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);
}

290 291 292 293 294 295 296 297 298 299 300 301 302

/*
 * 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);

303
    auto stage = gtk_clutter_embed_get_stage(GTK_CLUTTER_EMBED(self));
304 305 306

    /* layout manager is used to arrange children in space, here we ask clutter
     * to align children to fill the space when resizing the window */
307
    clutter_actor_set_layout_manager(stage,
308 309 310 311 312
        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);
313
    clutter_actor_add_child(stage, priv->video_container);
314

Stepan Salenikovich's avatar
Stepan Salenikovich committed
315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331
    /* 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);
332 333 334
    /* set position constraint to right cornder;
     * this constraint will be removed once the user tries to move the position
     * of the action */
Stepan Salenikovich's avatar
Stepan Salenikovich committed
335 336 337 338 339 340
    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);

341 342 343 344 345 346
    /* 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);
347
    g_signal_connect_after(priv->local->drag_action, "drag-end", G_CALLBACK(on_drag_end), stage);
348 349

    /* make sure the actor stays within the bounds of the stage */
350
    g_signal_connect(stage, "notify::allocation", G_CALLBACK(on_allocation_changed), self);
351

352 353 354 355 356 357 358 359 360 361
    /* 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);
362

363
    /* init new renderer queue */
364
    priv->new_renderer_queue = g_async_queue_new_full((GDestroyNotify)free_video_widget_renderer);
365 366 367 368 369 370 371 372
    /* 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);

373

374
    /* drag & drop files as video sources */
375 376
    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));
377 378

    priv->popup_menu = gtk_menu_new();
379 380 381
}

/*
382
 * video_widget_on_drag_data_received()
383 384 385 386
 *
 * Handle dragged data in the video widget window.
 * Dropping an image causes the client to switch the video input to that image.
 */
387 388 389 390 391 392 393
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,
394
                                        G_GNUC_UNUSED gpointer data)
395
{
396 397
    auto* priv = VIDEO_WIDGET_GET_PRIVATE(self);
    g_return_if_fail(priv);
398
    gchar **uris = gtk_selection_data_get_uris(selection_data);
399 400
    if (uris && *uris && priv->avModel_) {
        priv->avModel_->setInputFile(*uris);
401
    }
402
    g_strfreev(uris);
403 404
}

405
static void
406
switch_video_input(GtkWidget *widget, GtkWidget *parent)
407
{
408 409
    auto* priv = VIDEO_WIDGET_GET_PRIVATE(parent);
    g_return_if_fail(priv);
410

411 412 413 414 415
    auto* label = gtk_menu_item_get_label(GTK_MENU_ITEM(widget));
    g_return_if_fail(label);

    if (priv->avModel_)
        priv->avModel_->switchInputTo(label);
416 417
}

Stepan Salenikovich's avatar
Stepan Salenikovich committed
418
static void
419
switch_video_input_screen_area(G_GNUC_UNUSED GtkWidget *item, GtkWidget *parent)
Stepan Salenikovich's avatar
Stepan Salenikovich committed
420
{
421
    auto* priv = VIDEO_WIDGET_GET_PRIVATE(parent);
Stepan Salenikovich's avatar
Stepan Salenikovich committed
422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447
    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();
    }

448 449
    if (priv->avModel_)
        priv->avModel_->setDisplay(display, x, y, width, height);
Stepan Salenikovich's avatar
Stepan Salenikovich committed
450 451
}

452
static void
453
switch_video_input_monitor(G_GNUC_UNUSED GtkWidget *item, GtkWidget *parent)
454
{
455
    auto* priv = VIDEO_WIDGET_GET_PRIVATE(parent);
456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475
    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();

476 477
    if (priv->avModel_)
        priv->avModel_->setDisplay(display, x, y, width, height);
478 479 480
}


481
static void
482
switch_video_input_file(G_GNUC_UNUSED GtkWidget *item, GtkWidget *parent)
483
{
484
    auto* priv = VIDEO_WIDGET_GET_PRIVATE(parent);
485
    if (parent && GTK_IS_WIDGET(parent)) {
486
        // get parent window
487 488 489 490 491 492 493 494 495 496 497 498
        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) {
499
        gchar *uri = gtk_file_chooser_get_uri(GTK_FILE_CHOOSER(dialog));
500

501 502
        if (uri && priv->avModel_) {
            priv->avModel_->setInputFile(uri);
503 504
            g_free(uri);
        }
505 506 507 508 509
    }

    gtk_widget_destroy(dialog);
}

510
/*
511
 * video_widget_on_button_press_in_screen_event()
512 513 514
 *
 * Handle button event in the video screen.
 */
515
gboolean
516
video_widget_on_button_press_in_screen_event(VideoWidget *self,  GdkEventButton *event, G_GNUC_UNUSED gpointer)
517
{
518
    // check for right click
519 520 521
    if (event->button != BUTTON_RIGHT_CLICK || event->type != GDK_BUTTON_PRESS)
        return FALSE;

522
    // update menu with available video sources
523
    auto priv = VIDEO_WIDGET_GET_PRIVATE(self);
524
    g_return_val_if_fail(priv->remote->v_renderer, false);
525 526
    auto menu = priv->popup_menu;

527 528
    gtk_container_forall(GTK_CONTAINER(menu), (GtkCallback)gtk_widget_destroy,
        nullptr);
529

530 531 532 533
    // 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());
534

535 536
    for (auto device : device_list) {
        GtkWidget *item = gtk_check_menu_item_new_with_mnemonic(device.c_str());
537
        gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
538 539 540 541 542
        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);
543 544
    }

545 546 547
    /* add separator */
    gtk_menu_shell_append(GTK_MENU_SHELL(menu), gtk_separator_menu_item_new());

548
    // add screen area as an input
549 550
    GtkWidget *item = gtk_check_menu_item_new_with_mnemonic(_("Share _screen area"));
    gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
551 552 553 554
    // 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);
555

556
    // add screen area as an input
557
    item = gtk_check_menu_item_new_with_mnemonic(_("Share _monitor"));
Stepan Salenikovich's avatar
Stepan Salenikovich committed
558
    gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
559 560 561
    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);
Stepan Salenikovich's avatar
Stepan Salenikovich committed
562

563
    // add file as an input
564
    item = gtk_check_menu_item_new_with_mnemonic(_("Stream _file"));
565
    gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
566 567 568
    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);
569

570
    // add separator
Olivier Gregoire's avatar
Olivier Gregoire committed
571 572
    gtk_menu_shell_append(GTK_MENU_SHELL(menu), gtk_separator_menu_item_new());

573
    // add SmartInfo
Olivier Gregoire's avatar
Olivier Gregoire committed
574 575 576 577 578
    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()));

579
    // show menu
580 581 582
    gtk_widget_show_all(menu);
    gtk_menu_popup(GTK_MENU(menu), NULL, NULL, NULL, NULL, event->button, event->time);

583
    return TRUE; // event has been fully handled
584 585
}

586 587 588 589 590 591
static void
free_pixels(guchar *pixels, gpointer)
{
    g_free(pixels);
}

Guillaume Roguez's avatar
Guillaume Roguez committed
592 593
static void
clutter_render_image(VideoWidgetRenderer* wg_renderer)
594
{
595 596 597 598 599 600 601
    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
         */
602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637
        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);
            }
        }
638 639 640 641
        wg_renderer->show_black_frame = false;
        return;
    }

642 643 644 645 646 647 648 649 650 651
    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;

652
        if (!wg_renderer->v_renderer)
653 654
            return;

655 656 657 658
        auto v_renderer = wg_renderer->v_renderer;
        if (!v_renderer)
            return;
        auto frame_data = v_renderer->currentFrame().ptr;
659
        if (!frame_data)
660 661 662 663 664
            return;

        image_new = clutter_image_new();
        g_return_if_fail(image_new);

665
        const auto& res = v_renderer->size();
666 667
        gint BPP = 4; /* BGRA */
        gint ROW_STRIDE = BPP * res.width();
668 669 670 671

        GError *error = nullptr;
        clutter_image_set_data(
            CLUTTER_IMAGE(image_new),
672
            frame_data,
673 674 675 676 677 678 679
            COGL_PIXEL_FORMAT_BGRA_8888,
            res.width(),
            res.height(),
            ROW_STRIDE,
            &error);
        if (error) {
            g_warning("error rendering image to clutter: %s", error->message);
680
            g_clear_error(&error);
681 682 683
            g_object_unref (image_new);
            return;
        }
684 685

        if (wg_renderer->snapshot_status == HAS_TO_TAKE_ONE) {
686
            guchar *pixbuf_frame_data = (guchar *)g_malloc(res.width() * res.height() * 3);
687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702

            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;
            }

703
            wg_renderer->snapshot = gdk_pixbuf_new_from_data(pixbuf_frame_data,
704 705
                                                             GDK_COLORSPACE_RGB, FALSE, 8,
                                                             res.width(), res.height(),
706
                                                             ROW_STRIDE, free_pixels, NULL);
707 708 709 710

            wg_renderer->snapshot_status = HAS_A_NEW_ONE;

        }
711 712
    }

Guillaume Roguez's avatar
Guillaume Roguez committed
713
    clutter_actor_set_content(actor, image_new);
714
    g_object_unref (image_new);
Guillaume Roguez's avatar
Guillaume Roguez committed
715

716 717 718
    /* note: we must set the content gravity be "resize aspect" after setting the image data to make sure
     * that the aspect ratio is correct
     */
Guillaume Roguez's avatar
Guillaume Roguez committed
719
    clutter_actor_set_content_gravity(actor, CLUTTER_CONTENT_GRAVITY_RESIZE_ASPECT);
720 721 722 723 724 725 726 727
}

static gboolean
check_frame_queue(VideoWidget *self)
{
    g_return_val_if_fail(IS_VIDEO_WIDGET(self), FALSE);
    VideoWidgetPrivate *priv = VIDEO_WIDGET_GET_PRIVATE(self);

Guillaume Roguez's avatar
Guillaume Roguez committed
728
    /* display renderer's frames */
729 730
    if (priv->show_preview)
        clutter_render_image(priv->local);
Guillaume Roguez's avatar
Guillaume Roguez committed
731
    clutter_render_image(priv->remote);
732 733 734 735
    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);
    }
736 737

    return TRUE; /* keep going */
738 739
}

Stepan Salenikovich's avatar
Stepan Salenikovich committed
740 741
static void
renderer_stop(VideoWidgetRenderer *renderer)
742
{
743 744 745 746 747 748
    {
        /* 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;
    }
749 750
    /* ask to show a black frame */
    renderer->show_black_frame = true;
751 752
}

Stepan Salenikovich's avatar
Stepan Salenikovich committed
753 754
static void
renderer_start(VideoWidgetRenderer *renderer)
755
{
756 757 758 759
    {
        std::lock_guard<std::mutex> lock(renderer->run_mutex);
        renderer->running = true;
    }
760
    renderer->show_black_frame = false;
761 762
}

Stepan Salenikovich's avatar
Stepan Salenikovich committed
763
static void
764
free_video_widget_renderer(VideoWidgetRenderer *renderer)
Stepan Salenikovich's avatar
Stepan Salenikovich committed
765 766 767
{
    QObject::disconnect(renderer->render_stop);
    QObject::disconnect(renderer->render_start);
768 769
    if (renderer->snapshot)
        g_object_unref(renderer->snapshot);
770
    g_free(renderer);
Stepan Salenikovich's avatar
Stepan Salenikovich committed
771
}
772

773
static void
774
video_widget_add_renderer(VideoWidget *self, VideoWidgetRenderer *new_video_renderer)
775 776
{
    g_return_if_fail(IS_VIDEO_WIDGET(self));
777
    g_return_if_fail(new_video_renderer);
778
    g_return_if_fail(new_video_renderer->v_renderer);
779 780 781 782

    VideoWidgetPrivate *priv = VIDEO_WIDGET_GET_PRIVATE(self);

    /* update the renderer */
783
    switch(new_video_renderer->type) {
784
        case VIDEO_RENDERER_REMOTE:
785 786 787 788 789 790 791 792
            /* 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);
793 794
            break;
        case VIDEO_RENDERER_LOCAL:
795 796
            /* swap the remote renderer */
            new_video_renderer->actor = priv->local->actor;
797
            new_video_renderer->drag_action = priv->local->drag_action;
798 799 800 801 802 803
            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);
804 805 806 807
            break;
        case VIDEO_RENDERER_COUNT:
            break;
    }
808
}
809 810 811 812 813 814 815 816

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 */
817
    VideoWidgetRenderer *new_video_renderer = (VideoWidgetRenderer *)g_async_queue_try_pop(priv->new_renderer_queue);
818
    while (new_video_renderer) {
819 820
        video_widget_add_renderer(self, new_video_renderer);
        new_video_renderer = (VideoWidgetRenderer *)g_async_queue_try_pop(priv->new_renderer_queue);
821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838
    }

    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
839 840
video_widget_add_new_renderer(VideoWidget* self, lrc::api::AVModel* avModel,
    const lrc::api::video::Renderer* renderer, VideoRendererType type)
841 842 843
{
    g_return_if_fail(IS_VIDEO_WIDGET(self));
    VideoWidgetPrivate *priv = VIDEO_WIDGET_GET_PRIVATE(self);
844
    priv->avModel_ = avModel;
845

846 847 848 849
    /* if the renderer is nullptr, there is nothing to be done */
    if (!renderer) return;

    VideoWidgetRenderer *new_video_renderer = g_new0(VideoWidgetRenderer, 1);
850
    new_video_renderer->v_renderer = renderer;
851 852
    new_video_renderer->type = type;

853
    if (renderer->isRendering())
854 855 856
        renderer_start(new_video_renderer);

    new_video_renderer->render_stop = QObject::connect(
857 858
        &*avModel,
        &lrc::api::AVModel::rendererStopped,
859 860 861
        [=](const std::string& id) {
            if (renderer->getId() == id)
                renderer_stop(new_video_renderer);
862
        });
863 864

    new_video_renderer->render_start = QObject::connect(
865 866
        &*avModel,
        &lrc::api::AVModel::rendererStarted,
867 868 869
        [=](const std::string& id) {
            if (renderer->getId() == id)
                renderer_start(new_video_renderer);
870
        });
871

872 873
    g_async_queue_push(priv->new_renderer_queue, new_video_renderer);
}
874

875 876 877
void
video_widget_take_snapshot(VideoWidget *self)
{
878
    g_return_if_fail(IS_VIDEO_WIDGET(self));
879 880 881 882 883 884 885 886
    VideoWidgetPrivate *priv = VIDEO_WIDGET_GET_PRIVATE(self);

    priv->remote->snapshot_status = HAS_TO_TAKE_ONE;
}

GdkPixbuf*
video_widget_get_snapshot(VideoWidget *self)
{
887
    g_return_val_if_fail(IS_VIDEO_WIDGET(self), nullptr);
888 889 890 891
    VideoWidgetPrivate *priv = VIDEO_WIDGET_GET_PRIVATE(self);

    return priv->remote->snapshot;
}
892 893 894 895 896 897 898 899 900 901

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;
    }
}