blob: d7fda70692068d53bc7911f7ce542f1ec6bfeeeb [file] [log] [blame]
Stepan Salenikovich36c025c2015-03-03 19:06:44 -05001/*
Stepan Salenikovichbe87d2c2016-01-25 14:14:34 -05002 * Copyright (C) 2015-2016 Savoir-faire Linux Inc.
Stepan Salenikovich36c025c2015-03-03 19:06:44 -05003 * Author: Stepan Salenikovich <stepan.salenikovich@savoirfairelinux.com>
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 3 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program; if not, write to the Free Software
17 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
Stepan Salenikovich36c025c2015-03-03 19:06:44 -050018 */
19
20#include "video_widget.h"
21
Julien Grossholtz58cfc152015-10-22 15:43:43 -040022#include <glib/gi18n.h>
Stepan Salenikovich36c025c2015-03-03 19:06:44 -050023#include <clutter/clutter.h>
24#include <clutter-gtk/clutter-gtk.h>
25#include <video/renderer.h>
Stepan Salenikovich033dc832015-03-23 15:56:47 -040026#include <video/sourcemodel.h>
Julien Grossholtza0d4f102015-10-22 14:24:17 -040027#include <media/video.h>
Stepan Salenikovich50c989b2015-03-21 18:32:46 -040028#include <video/devicemodel.h>
29#include <QtCore/QUrl>
30#include "../defines.h"
Stepan Salenikovich8bc51e52015-03-21 20:17:29 -040031#include <stdlib.h>
Guillaume Roguez24834e02015-04-09 12:45:40 -040032#include <atomic>
Stepan Salenikovichaea6c042015-04-29 15:09:16 -040033#include <mutex>
Julien Grossholtza0d4f102015-10-22 14:24:17 -040034#include <call.h>
Stepan Salenikovich8bc51e52015-03-21 20:17:29 -040035#include "xrectsel.h"
Stepan Salenikovich36c025c2015-03-03 19:06:44 -050036
Stepan Salenikovich9ffad5e2015-09-25 13:16:50 -040037static constexpr int VIDEO_LOCAL_SIZE = 150;
38static constexpr int VIDEO_LOCAL_OPACITY_DEFAULT = 255; /* out of 255 */
Julien Grossholtza0d4f102015-10-22 14:24:17 -040039static constexpr const char* JOIN_CALL_KEY = "call_data";
Stepan Salenikovich4ac89f12015-03-10 16:48:47 -040040
Stepan Salenikovich5e6a0b72015-03-12 14:55:22 -040041/* check video frame queues at this rate;
42 * use 30 ms (about 30 fps) since we don't expect to
43 * receive video frames faster than that */
Stepan Salenikovich9ffad5e2015-09-25 13:16:50 -040044static constexpr int FRAME_RATE_PERIOD = 30;
Stepan Salenikovich5e6a0b72015-03-12 14:55:22 -040045
Nicolas Jager4bb29542016-05-12 10:21:47 -040046enum SnapshotStatus {
47 NOTHING,
48 HAS_TO_TAKE_ONE,
49 HAS_A_NEW_ONE
50};
51
Stepan Salenikovich36c025c2015-03-03 19:06:44 -050052struct _VideoWidgetClass {
Stepan Salenikovich36ef3942015-11-05 18:26:57 -050053 GtkClutterEmbedClass parent_class;
Stepan Salenikovich36c025c2015-03-03 19:06:44 -050054};
55
56struct _VideoWidget {
Stepan Salenikovich36ef3942015-11-05 18:26:57 -050057 GtkClutterEmbed parent;
Stepan Salenikovich36c025c2015-03-03 19:06:44 -050058};
59
60typedef struct _VideoWidgetPrivate VideoWidgetPrivate;
61
Stepan Salenikovich4ac89f12015-03-10 16:48:47 -040062typedef struct _VideoWidgetRenderer VideoWidgetRenderer;
63
Stepan Salenikovich36c025c2015-03-03 19:06:44 -050064struct _VideoWidgetPrivate {
Stepan Salenikovich36c025c2015-03-03 19:06:44 -050065 ClutterActor *video_container;
66
67 /* remote peer data */
Stepan Salenikovich4ac89f12015-03-10 16:48:47 -040068 VideoWidgetRenderer *remote;
Stepan Salenikovich36c025c2015-03-03 19:06:44 -050069
70 /* local peer data */
Stepan Salenikovich4ac89f12015-03-10 16:48:47 -040071 VideoWidgetRenderer *local;
Stepan Salenikovich5e6a0b72015-03-12 14:55:22 -040072
73 guint frame_timeout_source;
Stepan Salenikovich0f693232015-04-22 10:45:08 -040074
75 /* new renderers should be put into the queue for processing by a g_timeout
76 * function whose id should be saved into renderer_timeout_source;
77 * this way when the VideoWidget object is destroyed, we do not try
78 * to process any new renderers by stoping the g_timeout function.
79 */
80 guint renderer_timeout_source;
81 GAsyncQueue *new_renderer_queue;
Stepan Salenikovich4ac89f12015-03-10 16:48:47 -040082};
83
84struct _VideoWidgetRenderer {
Stepan Salenikovich3bcb0b22015-04-21 18:44:55 -040085 VideoRendererType type;
Stepan Salenikovich4ac89f12015-03-10 16:48:47 -040086 ClutterActor *actor;
Stepan Salenikovich223b2fd2015-06-22 12:52:21 -040087 ClutterAction *drag_action;
Stepan Salenikovich4ac89f12015-03-10 16:48:47 -040088 Video::Renderer *renderer;
Nicolas Jager4bb29542016-05-12 10:21:47 -040089 GdkPixbuf *snapshot;
Stepan Salenikovichaea6c042015-04-29 15:09:16 -040090 std::mutex run_mutex;
91 bool running;
Nicolas Jager4bb29542016-05-12 10:21:47 -040092 SnapshotStatus snapshot_status;
Stepan Salenikovichbf118ac2015-04-22 14:06:50 -040093
94 /* show_black_frame is used to request the actor to render a black image;
Stepan Salenikovichd6a4ef32015-11-12 10:59:02 -050095 * this will take over 'running', ie: a black frame will be rendered even if
96 * the Video::Renderer is not running;
Stepan Salenikovichbf118ac2015-04-22 14:06:50 -040097 * this will be set back to false once the black frame is rendered
98 */
99 std::atomic_bool show_black_frame;
Stepan Salenikovichb94873c2015-06-02 16:53:18 -0400100 std::atomic_bool pause_rendering;
Stepan Salenikovich4ac89f12015-03-10 16:48:47 -0400101 QMetaObject::Connection render_stop;
102 QMetaObject::Connection render_start;
Stepan Salenikovich36c025c2015-03-03 19:06:44 -0500103};
104
Stepan Salenikovich36ef3942015-11-05 18:26:57 -0500105G_DEFINE_TYPE_WITH_PRIVATE(VideoWidget, video_widget, GTK_CLUTTER_TYPE_EMBED);
Stepan Salenikovich36c025c2015-03-03 19:06:44 -0500106
107#define VIDEO_WIDGET_GET_PRIVATE(obj) (G_TYPE_INSTANCE_GET_PRIVATE ((obj), VIDEO_WIDGET_TYPE, VideoWidgetPrivate))
108
Stepan Salenikovich5e6a0b72015-03-12 14:55:22 -0400109/* static prototypes */
Stepan Salenikovich3bcb0b22015-04-21 18:44:55 -0400110static gboolean check_frame_queue (VideoWidget *);
111static void renderer_stop (VideoWidgetRenderer *);
112static void renderer_start (VideoWidgetRenderer *);
Stepan Salenikovich3bcb0b22015-04-21 18:44:55 -0400113static gboolean check_renderer_queue (VideoWidget *);
114static void free_video_widget_renderer (VideoWidgetRenderer *);
115static void video_widget_add_renderer (VideoWidget *, VideoWidgetRenderer *);
Stepan Salenikovich5e6a0b72015-03-12 14:55:22 -0400116
Nicolas Jager4bb29542016-05-12 10:21:47 -0400117/* signals */
118enum {
119 SNAPSHOT_SIGNAL,
120 LAST_SIGNAL
121};
122
123static guint video_widget_signals[LAST_SIGNAL] = { 0 };
124
125
Stepan Salenikovich36c025c2015-03-03 19:06:44 -0500126/*
127 * video_widget_dispose()
128 *
129 * The dispose function for the video_widget class.
130 */
131static void
Stepan Salenikovich4ac89f12015-03-10 16:48:47 -0400132video_widget_dispose(GObject *object)
Stepan Salenikovich36c025c2015-03-03 19:06:44 -0500133{
Stepan Salenikovich4ac89f12015-03-10 16:48:47 -0400134 VideoWidget *self = VIDEO_WIDGET(object);
Stepan Salenikovich36c025c2015-03-03 19:06:44 -0500135 VideoWidgetPrivate *priv = VIDEO_WIDGET_GET_PRIVATE(self);
136
Stepan Salenikovichbf74af82015-03-13 11:35:58 -0400137 /* dispose may be called multiple times, make sure
138 * not to call g_source_remove more than once */
139 if (priv->frame_timeout_source) {
140 g_source_remove(priv->frame_timeout_source);
141 priv->frame_timeout_source = 0;
142 }
Stepan Salenikovich5e6a0b72015-03-12 14:55:22 -0400143
Stepan Salenikovich0f693232015-04-22 10:45:08 -0400144 if (priv->renderer_timeout_source) {
145 g_source_remove(priv->renderer_timeout_source);
146 priv->renderer_timeout_source = 0;
147 }
148
149 if (priv->new_renderer_queue) {
150 g_async_queue_unref(priv->new_renderer_queue);
151 priv->new_renderer_queue = NULL;
152 }
153
Stepan Salenikovich4ac89f12015-03-10 16:48:47 -0400154 G_OBJECT_CLASS(video_widget_parent_class)->dispose(object);
Stepan Salenikovich36c025c2015-03-03 19:06:44 -0500155}
156
157
158/*
159 * video_widget_finalize()
160 *
161 * The finalize function for the video_widget class.
162 */
163static void
164video_widget_finalize(GObject *object)
165{
Stepan Salenikovich4ac89f12015-03-10 16:48:47 -0400166 VideoWidget *self = VIDEO_WIDGET(object);
167 VideoWidgetPrivate *priv = VIDEO_WIDGET_GET_PRIVATE(self);
168
Stepan Salenikovich3bcb0b22015-04-21 18:44:55 -0400169 free_video_widget_renderer(priv->local);
170 free_video_widget_renderer(priv->remote);
Stepan Salenikovich4ac89f12015-03-10 16:48:47 -0400171
Stepan Salenikovich36c025c2015-03-03 19:06:44 -0500172 G_OBJECT_CLASS(video_widget_parent_class)->finalize(object);
173}
174
175
176/*
177 * video_widget_class_init()
178 *
179 * This function init the video_widget_class.
180 */
181static void
182video_widget_class_init(VideoWidgetClass *klass)
183{
184 GObjectClass *object_class = G_OBJECT_CLASS(klass);
185
186 /* override method */
187 object_class->dispose = video_widget_dispose;
188 object_class->finalize = video_widget_finalize;
Nicolas Jager4bb29542016-05-12 10:21:47 -0400189
190 /* add snapshot signal */
191 video_widget_signals[SNAPSHOT_SIGNAL] = g_signal_new("snapshot-taken",
192 G_TYPE_FROM_CLASS(klass),
193 (GSignalFlags) (G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION),
194 0,
195 nullptr,
196 nullptr,
197 g_cclosure_marshal_VOID__VOID,
198 G_TYPE_NONE, 0);
199
Stepan Salenikovich36c025c2015-03-03 19:06:44 -0500200}
201
Stepan Salenikovich223b2fd2015-06-22 12:52:21 -0400202static void
203on_allocation_changed(ClutterActor *video_area, G_GNUC_UNUSED GParamSpec *pspec, VideoWidget *self)
204{
205 g_return_if_fail(IS_VIDEO_WIDGET(self));
206 VideoWidgetPrivate *priv = VIDEO_WIDGET_GET_PRIVATE(self);
207
208 auto actor = priv->local->actor;
209 auto drag_action = priv->local->drag_action;
210
211 ClutterActorBox actor_box;
212 clutter_actor_get_allocation_box(actor, &actor_box);
213 gfloat actor_w = clutter_actor_box_get_width(&actor_box);
214 gfloat actor_h = clutter_actor_box_get_height(&actor_box);
215
216 ClutterActorBox area_box;
217 clutter_actor_get_allocation_box(video_area, &area_box);
218 gfloat area_w = clutter_actor_box_get_width(&area_box);
219 gfloat area_h = clutter_actor_box_get_height(&area_box);
220
221 /* make sure drag area stays within the bounds of the stage */
222 ClutterRect *rect = clutter_rect_init (
223 clutter_rect_alloc(),
224 0, 0,
225 area_w - actor_w,
226 area_h - actor_h);
227 clutter_drag_action_set_drag_area(CLUTTER_DRAG_ACTION(drag_action), rect);
228 clutter_rect_free(rect);
229}
230
231static void
232on_drag_begin(G_GNUC_UNUSED ClutterDragAction *action,
233 ClutterActor *actor,
234 G_GNUC_UNUSED gfloat event_x,
235 G_GNUC_UNUSED gfloat event_y,
236 G_GNUC_UNUSED ClutterModifierType modifiers,
237 G_GNUC_UNUSED gpointer user_data)
238{
239 /* clear the align constraint when starting to move the preview, otherwise
240 * it won't move; save and set its position, to what it was before the
241 * constraint was cleared, or else it might jump around */
242 gfloat actor_x, actor_y;
243 clutter_actor_get_position(actor, &actor_x, &actor_y);
244 clutter_actor_clear_constraints(actor);
245 clutter_actor_set_position(actor, actor_x, actor_y);
246}
247
248static void
249on_drag_end(G_GNUC_UNUSED ClutterDragAction *action,
250 ClutterActor *actor,
251 G_GNUC_UNUSED gfloat event_x,
252 G_GNUC_UNUSED gfloat event_y,
253 G_GNUC_UNUSED ClutterModifierType modifiers,
254 ClutterActor *video_area)
255{
256 ClutterActorBox area_box;
257 clutter_actor_get_allocation_box(video_area, &area_box);
258 gfloat area_w = clutter_actor_box_get_width(&area_box);
259 gfloat area_h = clutter_actor_box_get_height(&area_box);
260
261 gfloat actor_x, actor_y;
262 clutter_actor_get_position(actor, &actor_x, &actor_y);
263 gfloat actor_w, actor_h;
264 clutter_actor_get_size(actor, &actor_w, &actor_h);
265
266 area_w -= actor_w;
267 area_h -= actor_h;
268
269 /* add new constraints to make sure the preview stays in about the same location
270 * relative to the rest of the video when resizing */
271 ClutterConstraint *constraint_x = clutter_align_constraint_new(video_area,
272 CLUTTER_ALIGN_X_AXIS, actor_x/area_w);
273 clutter_actor_add_constraint(actor, constraint_x);
274
275 ClutterConstraint *constraint_y = clutter_align_constraint_new(video_area,
276 CLUTTER_ALIGN_Y_AXIS, actor_y/area_h);
277 clutter_actor_add_constraint(actor, constraint_y);
278}
279
Stepan Salenikovich36c025c2015-03-03 19:06:44 -0500280
281/*
282 * video_widget_init()
283 *
284 * This function init the video_widget.
285 * - init clutter
286 * - init all the widget members
287 */
288static void
289video_widget_init(VideoWidget *self)
290{
291 VideoWidgetPrivate *priv = VIDEO_WIDGET_GET_PRIVATE(self);
292
Stepan Salenikovich36ef3942015-11-05 18:26:57 -0500293 auto stage = gtk_clutter_embed_get_stage(GTK_CLUTTER_EMBED(self));
Stepan Salenikovich36c025c2015-03-03 19:06:44 -0500294
295 /* layout manager is used to arrange children in space, here we ask clutter
296 * to align children to fill the space when resizing the window */
Stepan Salenikovich36ef3942015-11-05 18:26:57 -0500297 clutter_actor_set_layout_manager(stage,
Stepan Salenikovich36c025c2015-03-03 19:06:44 -0500298 clutter_bin_layout_new(CLUTTER_BIN_ALIGNMENT_FILL, CLUTTER_BIN_ALIGNMENT_FILL));
299
300 /* add a scene container where we can add and remove our actors */
301 priv->video_container = clutter_actor_new();
302 clutter_actor_set_background_color(priv->video_container, CLUTTER_COLOR_Black);
Stepan Salenikovich36ef3942015-11-05 18:26:57 -0500303 clutter_actor_add_child(stage, priv->video_container);
Stepan Salenikovich36c025c2015-03-03 19:06:44 -0500304
Stepan Salenikovich4ac89f12015-03-10 16:48:47 -0400305 /* init the remote and local structs */
306 priv->remote = g_new0(VideoWidgetRenderer, 1);
307 priv->local = g_new0(VideoWidgetRenderer, 1);
308
309 /* arrange remote actors */
310 priv->remote->actor = clutter_actor_new();
311 clutter_actor_insert_child_below(priv->video_container, priv->remote->actor, NULL);
312 /* the remote camera must always fill the container size */
313 ClutterConstraint *constraint = clutter_bind_constraint_new(priv->video_container,
314 CLUTTER_BIND_SIZE, 0);
315 clutter_actor_add_constraint(priv->remote->actor, constraint);
316
317 /* arrange local actor */
318 priv->local->actor = clutter_actor_new();
319 clutter_actor_insert_child_above(priv->video_container, priv->local->actor, NULL);
320 /* set size to square, but it will stay the aspect ratio when the image is rendered */
321 clutter_actor_set_size(priv->local->actor, VIDEO_LOCAL_SIZE, VIDEO_LOCAL_SIZE);
Stepan Salenikovich223b2fd2015-06-22 12:52:21 -0400322 /* set position constraint to right cornder;
323 * this constraint will be removed once the user tries to move the position
324 * of the action */
Stepan Salenikovich4ac89f12015-03-10 16:48:47 -0400325 constraint = clutter_align_constraint_new(priv->video_container,
326 CLUTTER_ALIGN_BOTH, 0.99);
327 clutter_actor_add_constraint(priv->local->actor, constraint);
328 clutter_actor_set_opacity(priv->local->actor,
329 VIDEO_LOCAL_OPACITY_DEFAULT);
330
Stepan Salenikovich223b2fd2015-06-22 12:52:21 -0400331 /* add ability for actor to be moved */
332 clutter_actor_set_reactive(priv->local->actor, TRUE);
333 priv->local->drag_action = clutter_drag_action_new();
334 clutter_actor_add_action(priv->local->actor, priv->local->drag_action);
335
336 g_signal_connect(priv->local->drag_action, "drag-begin", G_CALLBACK(on_drag_begin), NULL);
Stepan Salenikovich36ef3942015-11-05 18:26:57 -0500337 g_signal_connect_after(priv->local->drag_action, "drag-end", G_CALLBACK(on_drag_end), stage);
Stepan Salenikovich223b2fd2015-06-22 12:52:21 -0400338
339 /* make sure the actor stays within the bounds of the stage */
Stepan Salenikovich36ef3942015-11-05 18:26:57 -0500340 g_signal_connect(stage, "notify::allocation", G_CALLBACK(on_allocation_changed), self);
Stepan Salenikovich223b2fd2015-06-22 12:52:21 -0400341
Stepan Salenikovich8934e842015-04-20 15:16:13 -0400342 /* Init the timeout source which will check the for new frames.
343 * The priority must be lower than GTK drawing events
344 * (G_PRIORITY_HIGH_IDLE + 20) so that this timeout source doesn't choke
345 * the main loop on slower machines.
346 */
347 priv->frame_timeout_source = g_timeout_add_full(G_PRIORITY_DEFAULT_IDLE,
348 FRAME_RATE_PERIOD,
349 (GSourceFunc)check_frame_queue,
350 self,
351 NULL);
Stepan Salenikovich5e6a0b72015-03-12 14:55:22 -0400352
Stepan Salenikovich0f693232015-04-22 10:45:08 -0400353 /* init new renderer queue */
Stepan Salenikovich3bcb0b22015-04-21 18:44:55 -0400354 priv->new_renderer_queue = g_async_queue_new_full((GDestroyNotify)free_video_widget_renderer);
Stepan Salenikovich0f693232015-04-22 10:45:08 -0400355 /* check new render every 30 ms (30ms is "fast enough");
356 * we don't use an idle function so it doesn't consume cpu needlessly */
357 priv->renderer_timeout_source= g_timeout_add_full(G_PRIORITY_DEFAULT_IDLE,
358 30,
359 (GSourceFunc)check_renderer_queue,
360 self,
361 NULL);
362
Stepan Salenikovich36c025c2015-03-03 19:06:44 -0500363
Stepan Salenikovich50c989b2015-03-21 18:32:46 -0400364 /* drag & drop files as video sources */
Stepan Salenikovichcb6aa462015-03-21 15:13:37 -0400365 gtk_drag_dest_set(GTK_WIDGET(self), GTK_DEST_DEFAULT_ALL, NULL, 0, (GdkDragAction)(GDK_ACTION_COPY | GDK_ACTION_PRIVATE));
366 gtk_drag_dest_add_uri_targets(GTK_WIDGET(self));
Stepan Salenikovichcb6aa462015-03-21 15:13:37 -0400367}
368
369/*
Julien Grossholtza0d4f102015-10-22 14:24:17 -0400370 * video_widget_on_drag_data_received()
Stepan Salenikovichcb6aa462015-03-21 15:13:37 -0400371 *
372 * Handle dragged data in the video widget window.
373 * Dropping an image causes the client to switch the video input to that image.
374 */
Julien Grossholtza0d4f102015-10-22 14:24:17 -0400375void video_widget_on_drag_data_received(G_GNUC_UNUSED GtkWidget *self,
376 G_GNUC_UNUSED GdkDragContext *context,
377 G_GNUC_UNUSED gint x,
378 G_GNUC_UNUSED gint y,
379 GtkSelectionData *selection_data,
380 G_GNUC_UNUSED guint info,
381 G_GNUC_UNUSED guint32 time,
382 Call *call)
Stepan Salenikovichcb6aa462015-03-21 15:13:37 -0400383{
Julien Grossholtza0d4f102015-10-22 14:24:17 -0400384 g_return_if_fail(call);
Stepan Salenikovichcb6aa462015-03-21 15:13:37 -0400385 gchar **uris = gtk_selection_data_get_uris(selection_data);
386
387 /* only play the first selection */
Julien Grossholtza0d4f102015-10-22 14:24:17 -0400388 if (uris && *uris){
389 if (auto out_media = call->firstMedia<Media::Video>(Media::Media::Direction::OUT))
390 out_media->sourceModel()->setFile(QUrl(*uris));
391 }
Stepan Salenikovichcb6aa462015-03-21 15:13:37 -0400392
393 g_strfreev(uris);
Stepan Salenikovich36c025c2015-03-03 19:06:44 -0500394}
395
Stepan Salenikovich50c989b2015-03-21 18:32:46 -0400396static void
Julien Grossholtza0d4f102015-10-22 14:24:17 -0400397switch_video_input(GtkWidget *widget, Video::Device *device)
Stepan Salenikovich50c989b2015-03-21 18:32:46 -0400398{
Julien Grossholtza0d4f102015-10-22 14:24:17 -0400399 gpointer data = g_object_get_data(G_OBJECT(widget),JOIN_CALL_KEY );
400 g_return_if_fail(data);
401 Call *call = (Call*)data;
402
403 if (auto out_media = call->firstMedia<Media::Video>(Media::Media::Direction::OUT))
404 out_media->sourceModel()->switchTo(device);
Stepan Salenikovich50c989b2015-03-21 18:32:46 -0400405}
406
Stepan Salenikovich8bc51e52015-03-21 20:17:29 -0400407static void
Julien Grossholtza0d4f102015-10-22 14:24:17 -0400408switch_video_input_screen(G_GNUC_UNUSED GtkWidget *item, Call* call)
Stepan Salenikovich8bc51e52015-03-21 20:17:29 -0400409{
410 unsigned x, y;
411 unsigned width, height;
412
413 /* try to get the dispaly or default to 0 */
414 QString display_env{getenv("DISPLAY")};
415 int display = 0;
416
417 if (!display_env.isEmpty()) {
418 auto list = display_env.split(":", QString::SkipEmptyParts);
419 /* should only be one display, so get the first one */
420 if (list.size() > 0) {
421 display = list.at(0).toInt();
422 g_debug("sharing screen from DISPLAY %d", display);
423 }
424 }
425
426 x = y = width = height = 0;
427
428 xrectsel(&x, &y, &width, &height);
429
430 if (!width || !height) {
431 x = y = 0;
432 width = gdk_screen_width();
433 height = gdk_screen_height();
434 }
435
Julien Grossholtza0d4f102015-10-22 14:24:17 -0400436 if (auto out_media = call->firstMedia<Media::Video>(Media::Media::Direction::OUT))
437 out_media->sourceModel()->setDisplay(display, QRect(x,y,width,height));
Stepan Salenikovich8bc51e52015-03-21 20:17:29 -0400438}
439
Stepan Salenikovich763c25a2015-03-26 13:51:31 -0400440static void
Julien Grossholtza0d4f102015-10-22 14:24:17 -0400441switch_video_input_file(GtkWidget *item, GtkWidget *parent)
Stepan Salenikovich763c25a2015-03-26 13:51:31 -0400442{
443 if (parent && GTK_IS_WIDGET(parent)) {
444 /* get parent window */
445 parent = gtk_widget_get_toplevel(GTK_WIDGET(parent));
446 }
447
448 gchar *uri = NULL;
449 GtkWidget *dialog = gtk_file_chooser_dialog_new(
450 "Choose File",
451 GTK_WINDOW(parent),
452 GTK_FILE_CHOOSER_ACTION_OPEN,
453 "_Cancel", GTK_RESPONSE_CANCEL,
454 "_Open", GTK_RESPONSE_ACCEPT,
455 NULL);
456
457 if (gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_ACCEPT) {
Julien Grossholtza0d4f102015-10-22 14:24:17 -0400458 Call *call;
459 gpointer data = g_object_get_data(G_OBJECT(item),JOIN_CALL_KEY );
460 g_return_if_fail(data);
461 call = (Call*)data;
462
Stepan Salenikovich763c25a2015-03-26 13:51:31 -0400463 uri = gtk_file_chooser_get_uri(GTK_FILE_CHOOSER(dialog));
Julien Grossholtza0d4f102015-10-22 14:24:17 -0400464
465 if (auto out_media = call->firstMedia<Media::Video>(Media::Media::Direction::OUT))
466 out_media->sourceModel()->setFile(QUrl(uri));
Stepan Salenikovich763c25a2015-03-26 13:51:31 -0400467 }
468
469 gtk_widget_destroy(dialog);
Stepan Salenikovich763c25a2015-03-26 13:51:31 -0400470 g_free(uri);
471}
472
Stepan Salenikovich50c989b2015-03-21 18:32:46 -0400473/*
Julien Grossholtza0d4f102015-10-22 14:24:17 -0400474 * video_widget_on_button_press_in_screen_event()
Stepan Salenikovich50c989b2015-03-21 18:32:46 -0400475 *
476 * Handle button event in the video screen.
477 */
Julien Grossholtza0d4f102015-10-22 14:24:17 -0400478gboolean
479video_widget_on_button_press_in_screen_event(GtkWidget *parent, GdkEventButton *event, Call* call)
Stepan Salenikovich50c989b2015-03-21 18:32:46 -0400480{
481 /* check for right click */
482 if (event->button != BUTTON_RIGHT_CLICK || event->type != GDK_BUTTON_PRESS)
483 return FALSE;
484
485 /* create menu with available video sources */
486 GtkWidget *menu = gtk_menu_new();
487
Julien Grossholtza0d4f102015-10-22 14:24:17 -0400488 Video::SourceModel *sourcemodel = nullptr;
489 if (auto out_media = call->firstMedia<Media::Video>(Media::Media::Direction::OUT))
490 sourcemodel = out_media->sourceModel();
491
492 if(!sourcemodel)
493 return FALSE;
494
495 auto active = sourcemodel->activeIndex();
Stepan Salenikovichcb1c2952015-08-13 14:28:20 -0400496
Stepan Salenikovich50c989b2015-03-21 18:32:46 -0400497 /* list available devices and check off the active device */
Guillaume Roguez5d1514b2015-10-22 15:55:31 -0400498 auto device_list = Video::DeviceModel::instance().devices();
Stepan Salenikovich50c989b2015-03-21 18:32:46 -0400499
500 for( auto device: device_list) {
501 GtkWidget *item = gtk_check_menu_item_new_with_mnemonic(device->name().toLocal8Bit().constData());
502 gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
Julien Grossholtza0d4f102015-10-22 14:24:17 -0400503 auto device_idx = sourcemodel->getDeviceIndex(device);
Stepan Salenikovichcb1c2952015-08-13 14:28:20 -0400504 gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(item), device_idx == active);
Julien Grossholtza0d4f102015-10-22 14:24:17 -0400505 g_object_set_data(G_OBJECT(item), JOIN_CALL_KEY,call);
Stepan Salenikovich50c989b2015-03-21 18:32:46 -0400506 g_signal_connect(item, "activate", G_CALLBACK(switch_video_input), device);
507 }
508
Stepan Salenikovichbb382632015-03-26 12:40:46 -0400509 /* add separator */
510 gtk_menu_shell_append(GTK_MENU_SHELL(menu), gtk_separator_menu_item_new());
511
Stepan Salenikovich8bc51e52015-03-21 20:17:29 -0400512 /* add screen area as an input */
Julien Grossholtz58cfc152015-10-22 15:43:43 -0400513 GtkWidget *item = gtk_check_menu_item_new_with_mnemonic(_("Share screen area"));
Stepan Salenikovich8bc51e52015-03-21 20:17:29 -0400514 gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
Stepan Salenikovichcb1c2952015-08-13 14:28:20 -0400515 gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(item), Video::SourceModel::ExtendedDeviceList::SCREEN == active);
Julien Grossholtza0d4f102015-10-22 14:24:17 -0400516 g_signal_connect(item, "activate", G_CALLBACK(switch_video_input_screen), call);
Stepan Salenikovich8bc51e52015-03-21 20:17:29 -0400517
Stepan Salenikovich763c25a2015-03-26 13:51:31 -0400518 /* add file as an input */
Julien Grossholtz58cfc152015-10-22 15:43:43 -0400519 item = gtk_check_menu_item_new_with_mnemonic(_("Share file"));
Stepan Salenikovich763c25a2015-03-26 13:51:31 -0400520 gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
Stepan Salenikovichcb1c2952015-08-13 14:28:20 -0400521 gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(item), Video::SourceModel::ExtendedDeviceList::FILE == active);
Julien Grossholtza0d4f102015-10-22 14:24:17 -0400522 g_object_set_data(G_OBJECT(item), JOIN_CALL_KEY, call);
Stepan Salenikovich763c25a2015-03-26 13:51:31 -0400523 g_signal_connect(item, "activate", G_CALLBACK(switch_video_input_file), parent);
524
Stepan Salenikovich50c989b2015-03-21 18:32:46 -0400525 /* show menu */
526 gtk_widget_show_all(menu);
527 gtk_menu_popup(GTK_MENU(menu), NULL, NULL, NULL, NULL, event->button, event->time);
528
529 return TRUE; /* event has been fully handled */
530}
531
Stepan Salenikovich36c025c2015-03-03 19:06:44 -0500532static void
Guillaume Roguez24834e02015-04-09 12:45:40 -0400533clutter_render_image(VideoWidgetRenderer* wg_renderer)
Stepan Salenikovich36c025c2015-03-03 19:06:44 -0500534{
Stepan Salenikovichbf118ac2015-04-22 14:06:50 -0400535 auto actor = wg_renderer->actor;
536 g_return_if_fail(CLUTTER_IS_ACTOR(actor));
537
Stepan Salenikovichb94873c2015-06-02 16:53:18 -0400538 if (wg_renderer->pause_rendering)
539 return;
540
Stepan Salenikovichbf118ac2015-04-22 14:06:50 -0400541 if (wg_renderer->show_black_frame) {
542 /* render a black frame set the bool back to false, this is likely done
543 * when the renderer is stopped so we ignore whether or not it is running
544 */
Stepan Salenikovich6e7b0922015-05-05 17:51:30 -0400545 if (auto image_old = clutter_actor_get_content(actor)) {
546 gfloat width;
547 gfloat height;
548 if (clutter_content_get_preferred_size(image_old, &width, &height)) {
549 /* NOTE: this is a workaround for #72531, a crash which occurs
550 * in cogl < 1.18. We allocate a black frame of the same size
551 * as the previous image, instead of simply setting an empty or
552 * a NULL ClutterImage.
553 */
554 auto image_empty = clutter_image_new();
555 if (auto empty_data = (guint8 *)g_try_malloc0((gsize)width * height * 4)) {
556 GError* error = NULL;
557 clutter_image_set_data(
558 CLUTTER_IMAGE(image_empty),
559 empty_data,
560 COGL_PIXEL_FORMAT_BGRA_8888,
561 (guint)width,
562 (guint)height,
563 (guint)width*4,
564 &error);
565 if (error) {
566 g_warning("error rendering empty image to clutter: %s", error->message);
567 g_clear_error(&error);
568 g_object_unref(image_empty);
569 return;
570 }
571 clutter_actor_set_content(actor, image_empty);
572 g_object_unref(image_empty);
573 g_free(empty_data);
574 } else {
575 clutter_actor_set_content(actor, NULL);
576 }
577 } else {
578 clutter_actor_set_content(actor, NULL);
579 }
580 }
Stepan Salenikovichbf118ac2015-04-22 14:06:50 -0400581 wg_renderer->show_black_frame = false;
582 return;
583 }
584
Stepan Salenikovichaea6c042015-04-29 15:09:16 -0400585 ClutterContent *image_new = nullptr;
Stepan Salenikovich36c025c2015-03-03 19:06:44 -0500586
Stepan Salenikovichaea6c042015-04-29 15:09:16 -0400587 {
588 /* the following must be done under lock in case a 'stopped' signal is
589 * received during rendering; otherwise the mem could become invalid */
590 std::lock_guard<std::mutex> lock(wg_renderer->run_mutex);
Stepan Salenikovich36c025c2015-03-03 19:06:44 -0500591
Stepan Salenikovichaea6c042015-04-29 15:09:16 -0400592 if (!wg_renderer->running)
593 return;
Guillaume Roguez24834e02015-04-09 12:45:40 -0400594
Stepan Salenikovichaea6c042015-04-29 15:09:16 -0400595 auto renderer = wg_renderer->renderer;
596 if (renderer == nullptr)
597 return;
Guillaume Roguez24834e02015-04-09 12:45:40 -0400598
Guillaume Rogueza8860ea2015-10-13 18:04:39 -0400599 auto frame_ptr = renderer->currentFrame();
600 auto frame_data = frame_ptr.ptr;
Stepan Salenikovichd6a4ef32015-11-12 10:59:02 -0500601 if (!frame_data)
Stepan Salenikovichaea6c042015-04-29 15:09:16 -0400602 return;
Guillaume Roguez24834e02015-04-09 12:45:40 -0400603
Stepan Salenikovichaea6c042015-04-29 15:09:16 -0400604 image_new = clutter_image_new();
605 g_return_if_fail(image_new);
606
607 const auto& res = renderer->size();
Nicolas Jager4bb29542016-05-12 10:21:47 -0400608 gint BPP = 4; /* BGRA */
609 gint ROW_STRIDE = BPP * res.width();
Stepan Salenikovichaea6c042015-04-29 15:09:16 -0400610
611 GError *error = nullptr;
612 clutter_image_set_data(
613 CLUTTER_IMAGE(image_new),
Guillaume Rogueza8860ea2015-10-13 18:04:39 -0400614 frame_data,
Stepan Salenikovichaea6c042015-04-29 15:09:16 -0400615 COGL_PIXEL_FORMAT_BGRA_8888,
616 res.width(),
617 res.height(),
618 ROW_STRIDE,
619 &error);
620 if (error) {
621 g_warning("error rendering image to clutter: %s", error->message);
Stepan Salenikovich8a287fc2015-05-01 16:53:20 -0400622 g_clear_error(&error);
Stepan Salenikovichaea6c042015-04-29 15:09:16 -0400623 g_object_unref (image_new);
624 return;
625 }
Nicolas Jager4bb29542016-05-12 10:21:47 -0400626
627 if (wg_renderer->snapshot_status == HAS_TO_TAKE_ONE) {
628 guchar pixbuf_frame_data[res.width() * res.height() * 3];
629
630 BPP = 3; /* RGB */
631 gint ROW_STRIDE = BPP * res.width();
632
633 /* conversion from BGRA to RGB */
634 for(int i = 0, j = 0 ; i < res.width() * res.height() * 4 ; i += 4, j += 3 ) {
635 pixbuf_frame_data[j + 0] = frame_data[i + 2];
636 pixbuf_frame_data[j + 1] = frame_data[i + 1];
637 pixbuf_frame_data[j + 2] = frame_data[i + 0];
638 }
639
640 if (wg_renderer->snapshot) {
641 g_object_unref(wg_renderer->snapshot);
642 wg_renderer->snapshot = nullptr;
643 }
644
645 wg_renderer->snapshot = gdk_pixbuf_new_from_data(&pixbuf_frame_data[0],
646 GDK_COLORSPACE_RGB, FALSE, 8,
647 res.width(), res.height(),
648 ROW_STRIDE, NULL, NULL);
649
650 wg_renderer->snapshot_status = HAS_A_NEW_ONE;
651
652 }
Stepan Salenikovich36c025c2015-03-03 19:06:44 -0500653 }
654
Guillaume Roguez24834e02015-04-09 12:45:40 -0400655 clutter_actor_set_content(actor, image_new);
Stepan Salenikovichcd1bf632015-03-09 16:24:08 -0400656 g_object_unref (image_new);
Guillaume Roguez24834e02015-04-09 12:45:40 -0400657
Stepan Salenikovichcd1bf632015-03-09 16:24:08 -0400658 /* note: we must set the content gravity be "resize aspect" after setting the image data to make sure
659 * that the aspect ratio is correct
660 */
Guillaume Roguez24834e02015-04-09 12:45:40 -0400661 clutter_actor_set_content_gravity(actor, CLUTTER_CONTENT_GRAVITY_RESIZE_ASPECT);
Stepan Salenikovich5e6a0b72015-03-12 14:55:22 -0400662}
Stepan Salenikovich36c025c2015-03-03 19:06:44 -0500663
Stepan Salenikovich5e6a0b72015-03-12 14:55:22 -0400664static gboolean
665check_frame_queue(VideoWidget *self)
666{
667 g_return_val_if_fail(IS_VIDEO_WIDGET(self), FALSE);
668 VideoWidgetPrivate *priv = VIDEO_WIDGET_GET_PRIVATE(self);
669
Guillaume Roguez24834e02015-04-09 12:45:40 -0400670 /* display renderer's frames */
671 clutter_render_image(priv->local);
672 clutter_render_image(priv->remote);
Nicolas Jager4bb29542016-05-12 10:21:47 -0400673 if (priv->remote->snapshot_status == HAS_A_NEW_ONE) {
674 priv->remote->snapshot_status = NOTHING;
675 g_signal_emit(G_OBJECT(self), video_widget_signals[SNAPSHOT_SIGNAL], 0);
676 }
Stepan Salenikovich5e6a0b72015-03-12 14:55:22 -0400677
678 return TRUE; /* keep going */
Stepan Salenikovich36c025c2015-03-03 19:06:44 -0500679}
680
Stepan Salenikovich4ac89f12015-03-10 16:48:47 -0400681static void
682renderer_stop(VideoWidgetRenderer *renderer)
Stepan Salenikovich36c025c2015-03-03 19:06:44 -0500683{
Stepan Salenikovichaea6c042015-04-29 15:09:16 -0400684 {
685 /* must do this under lock, in case the rendering is taking place when
686 * this signal is received */
687 std::lock_guard<std::mutex> lock(renderer->run_mutex);
688 renderer->running = false;
689 }
Stepan Salenikovichbf118ac2015-04-22 14:06:50 -0400690 /* ask to show a black frame */
691 renderer->show_black_frame = true;
Stepan Salenikovich36c025c2015-03-03 19:06:44 -0500692}
693
Stepan Salenikovich4ac89f12015-03-10 16:48:47 -0400694static void
695renderer_start(VideoWidgetRenderer *renderer)
Stepan Salenikovich36c025c2015-03-03 19:06:44 -0500696{
Stepan Salenikovichaea6c042015-04-29 15:09:16 -0400697 {
698 std::lock_guard<std::mutex> lock(renderer->run_mutex);
699 renderer->running = true;
700 }
Stepan Salenikovichd6a4ef32015-11-12 10:59:02 -0500701 renderer->show_black_frame = false;
Stepan Salenikovich36c025c2015-03-03 19:06:44 -0500702}
703
Stepan Salenikovich4ac89f12015-03-10 16:48:47 -0400704static void
Stepan Salenikovich3bcb0b22015-04-21 18:44:55 -0400705free_video_widget_renderer(VideoWidgetRenderer *renderer)
Stepan Salenikovich4ac89f12015-03-10 16:48:47 -0400706{
Stepan Salenikovich4ac89f12015-03-10 16:48:47 -0400707 QObject::disconnect(renderer->render_stop);
708 QObject::disconnect(renderer->render_start);
Stepan Salenikovich3bcb0b22015-04-21 18:44:55 -0400709 g_free(renderer);
Nicolas Jager4bb29542016-05-12 10:21:47 -0400710 if (renderer->snapshot)
711 g_object_unref(renderer->snapshot);
Stepan Salenikovich4ac89f12015-03-10 16:48:47 -0400712}
Stepan Salenikovich36c025c2015-03-03 19:06:44 -0500713
Stepan Salenikovich0f693232015-04-22 10:45:08 -0400714static void
Stepan Salenikovich3bcb0b22015-04-21 18:44:55 -0400715video_widget_add_renderer(VideoWidget *self, VideoWidgetRenderer *new_video_renderer)
Stepan Salenikovich36c025c2015-03-03 19:06:44 -0500716{
717 g_return_if_fail(IS_VIDEO_WIDGET(self));
Stepan Salenikovich3bcb0b22015-04-21 18:44:55 -0400718 g_return_if_fail(new_video_renderer);
719 g_return_if_fail(new_video_renderer->renderer);
Stepan Salenikovich36c025c2015-03-03 19:06:44 -0500720
721 VideoWidgetPrivate *priv = VIDEO_WIDGET_GET_PRIVATE(self);
722
723 /* update the renderer */
Stepan Salenikovich3bcb0b22015-04-21 18:44:55 -0400724 switch(new_video_renderer->type) {
Stepan Salenikovichc5f08152015-03-19 00:53:23 -0400725 case VIDEO_RENDERER_REMOTE:
Stepan Salenikovich3bcb0b22015-04-21 18:44:55 -0400726 /* swap the remote renderer */
727 new_video_renderer->actor = priv->remote->actor;
728 free_video_widget_renderer(priv->remote);
729 priv->remote = new_video_renderer;
730 /* reset the content gravity so that the aspect ratio gets properly
731 * reset if it chagnes */
732 clutter_actor_set_content_gravity(priv->remote->actor,
733 CLUTTER_CONTENT_GRAVITY_RESIZE_FILL);
Stepan Salenikovichc5f08152015-03-19 00:53:23 -0400734 break;
735 case VIDEO_RENDERER_LOCAL:
Stepan Salenikovich3bcb0b22015-04-21 18:44:55 -0400736 /* swap the remote renderer */
737 new_video_renderer->actor = priv->local->actor;
Stepan Salenikovich223b2fd2015-06-22 12:52:21 -0400738 new_video_renderer->drag_action = priv->local->drag_action;
Stepan Salenikovich3bcb0b22015-04-21 18:44:55 -0400739 free_video_widget_renderer(priv->local);
740 priv->local = new_video_renderer;
741 /* reset the content gravity so that the aspect ratio gets properly
742 * reset if it chagnes */
743 clutter_actor_set_content_gravity(priv->local->actor,
744 CLUTTER_CONTENT_GRAVITY_RESIZE_FILL);
Stepan Salenikovichc5f08152015-03-19 00:53:23 -0400745 break;
746 case VIDEO_RENDERER_COUNT:
747 break;
748 }
Stepan Salenikovich36c025c2015-03-03 19:06:44 -0500749}
Stepan Salenikovich0f693232015-04-22 10:45:08 -0400750
751static gboolean
752check_renderer_queue(VideoWidget *self)
753{
754 g_return_val_if_fail(IS_VIDEO_WIDGET(self), G_SOURCE_REMOVE);
755 VideoWidgetPrivate *priv = VIDEO_WIDGET_GET_PRIVATE(self);
756
757 /* get all the renderers in the queue */
Stepan Salenikovich3bcb0b22015-04-21 18:44:55 -0400758 VideoWidgetRenderer *new_video_renderer = (VideoWidgetRenderer *)g_async_queue_try_pop(priv->new_renderer_queue);
Stepan Salenikovich0f693232015-04-22 10:45:08 -0400759 while (new_video_renderer) {
Stepan Salenikovich3bcb0b22015-04-21 18:44:55 -0400760 video_widget_add_renderer(self, new_video_renderer);
761 new_video_renderer = (VideoWidgetRenderer *)g_async_queue_try_pop(priv->new_renderer_queue);
Stepan Salenikovich0f693232015-04-22 10:45:08 -0400762 }
763
764 return G_SOURCE_CONTINUE;
765}
766
767/*
768 * video_widget_new()
769 *
770 * The function use to create a new video_widget
771 */
772GtkWidget*
773video_widget_new(void)
774{
775 GtkWidget *self = (GtkWidget *)g_object_new(VIDEO_WIDGET_TYPE, NULL);
776 return self;
777}
778
779/**
780 * video_widget_push_new_renderer()
781 *
782 * This function is used add a new Video::Renderer to the VideoWidget in a
783 * thread-safe manner.
784 */
785void
786video_widget_push_new_renderer(VideoWidget *self, Video::Renderer *renderer, VideoRendererType type)
787{
788 g_return_if_fail(IS_VIDEO_WIDGET(self));
789 VideoWidgetPrivate *priv = VIDEO_WIDGET_GET_PRIVATE(self);
790
Stepan Salenikovich3bcb0b22015-04-21 18:44:55 -0400791 /* if the renderer is nullptr, there is nothing to be done */
792 if (!renderer) return;
793
794 VideoWidgetRenderer *new_video_renderer = g_new0(VideoWidgetRenderer, 1);
Stepan Salenikovich0f693232015-04-22 10:45:08 -0400795 new_video_renderer->renderer = renderer;
796 new_video_renderer->type = type;
797
Stepan Salenikovich3bcb0b22015-04-21 18:44:55 -0400798 if (new_video_renderer->renderer->isRendering())
799 renderer_start(new_video_renderer);
800
801 new_video_renderer->render_stop = QObject::connect(
802 new_video_renderer->renderer,
803 &Video::Renderer::stopped,
804 [=]() {
805 renderer_stop(new_video_renderer);
806 }
807 );
808
809 new_video_renderer->render_start = QObject::connect(
810 new_video_renderer->renderer,
811 &Video::Renderer::started,
812 [=]() {
813 renderer_start(new_video_renderer);
814 }
815 );
816
Stepan Salenikovich0f693232015-04-22 10:45:08 -0400817 g_async_queue_push(priv->new_renderer_queue, new_video_renderer);
818}
Stepan Salenikovichb94873c2015-06-02 16:53:18 -0400819
820void
821video_widget_pause_rendering(VideoWidget *self, gboolean pause)
822{
823 g_return_if_fail(IS_VIDEO_WIDGET(self));
824 VideoWidgetPrivate *priv = VIDEO_WIDGET_GET_PRIVATE(self);
825
826 priv->local->pause_rendering = pause;
827 priv->remote->pause_rendering = pause;
828}
Nicolas Jager4bb29542016-05-12 10:21:47 -0400829
830void
831video_widget_take_snapshot(VideoWidget *self)
832{
833 VideoWidgetPrivate *priv = VIDEO_WIDGET_GET_PRIVATE(self);
834
835 priv->remote->snapshot_status = HAS_TO_TAKE_ONE;
836}
837
838GdkPixbuf*
839video_widget_get_snapshot(VideoWidget *self)
840{
841 VideoWidgetPrivate *priv = VIDEO_WIDGET_GET_PRIVATE(self);
842
843 return priv->remote->snapshot;
844}