1 /*
2 * GStreamer
3 * Copyright (C) 2015 Matthew Waters <matthew@centricular.com>
4 * Copyright (C) 2020 Rafał Dzięgiel <rafostar.github@gmail.com>
5 *
6 * This library is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU Library General Public
8 * License as published by the Free Software Foundation; either
9 * version 2 of the License, or (at your option) any later version.
10 *
11 * This library is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 * Library General Public License for more details.
15 *
16 * You should have received a copy of the GNU Library General Public
17 * License along with this library; if not, write to the
18 * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
19 * Boston, MA 02110-1301, USA.
20 */
21
22 #ifdef HAVE_CONFIG_H
23 #include "config.h"
24 #endif
25
26 #include <stdio.h>
27
28 #include "gtkgstbasewidget.h"
29
30 GST_DEBUG_CATEGORY (gst_debug_gtk_base_widget);
31 #define GST_CAT_DEFAULT gst_debug_gtk_base_widget
32
33 #define DEFAULT_FORCE_ASPECT_RATIO TRUE
34 #define DEFAULT_PAR_N 0
35 #define DEFAULT_PAR_D 1
36 #define DEFAULT_IGNORE_ALPHA TRUE
37
38 enum
39 {
40 PROP_0,
41 PROP_FORCE_ASPECT_RATIO,
42 PROP_PIXEL_ASPECT_RATIO,
43 PROP_IGNORE_ALPHA,
44 };
45
46 static void
gtk_gst_base_widget_get_preferred_width(GtkWidget * widget,gint * min,gint * natural)47 gtk_gst_base_widget_get_preferred_width (GtkWidget * widget, gint * min,
48 gint * natural)
49 {
50 GtkGstBaseWidget *gst_widget = (GtkGstBaseWidget *) widget;
51 gint video_width = gst_widget->display_width;
52
53 if (!gst_widget->negotiated)
54 video_width = 10;
55
56 if (min)
57 *min = 1;
58 if (natural)
59 *natural = video_width;
60 }
61
62 static void
gtk_gst_base_widget_get_preferred_height(GtkWidget * widget,gint * min,gint * natural)63 gtk_gst_base_widget_get_preferred_height (GtkWidget * widget, gint * min,
64 gint * natural)
65 {
66 GtkGstBaseWidget *gst_widget = (GtkGstBaseWidget *) widget;
67 gint video_height = gst_widget->display_height;
68
69 if (!gst_widget->negotiated)
70 video_height = 10;
71
72 if (min)
73 *min = 1;
74 if (natural)
75 *natural = video_height;
76 }
77
78 #if defined(BUILD_FOR_GTK4)
79 static void
gtk_gst_base_widget_measure(GtkWidget * widget,GtkOrientation orientation,gint for_size,gint * min,gint * natural,gint * minimum_baseline,gint * natural_baseline)80 gtk_gst_base_widget_measure (GtkWidget * widget, GtkOrientation orientation,
81 gint for_size, gint * min, gint * natural,
82 gint * minimum_baseline, gint * natural_baseline)
83 {
84 if (orientation == GTK_ORIENTATION_HORIZONTAL)
85 gtk_gst_base_widget_get_preferred_width (widget, min, natural);
86 else
87 gtk_gst_base_widget_get_preferred_height (widget, min, natural);
88
89 *minimum_baseline = -1;
90 *natural_baseline = -1;
91 }
92 #endif
93
94 static void
gtk_gst_base_widget_set_property(GObject * object,guint prop_id,const GValue * value,GParamSpec * pspec)95 gtk_gst_base_widget_set_property (GObject * object, guint prop_id,
96 const GValue * value, GParamSpec * pspec)
97 {
98 GtkGstBaseWidget *gtk_widget = GTK_GST_BASE_WIDGET (object);
99
100 switch (prop_id) {
101 case PROP_FORCE_ASPECT_RATIO:
102 gtk_widget->force_aspect_ratio = g_value_get_boolean (value);
103 break;
104 case PROP_PIXEL_ASPECT_RATIO:
105 gtk_widget->par_n = gst_value_get_fraction_numerator (value);
106 gtk_widget->par_d = gst_value_get_fraction_denominator (value);
107 break;
108 case PROP_IGNORE_ALPHA:
109 gtk_widget->ignore_alpha = g_value_get_boolean (value);
110 break;
111 default:
112 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
113 break;
114 }
115 }
116
117 static void
gtk_gst_base_widget_get_property(GObject * object,guint prop_id,GValue * value,GParamSpec * pspec)118 gtk_gst_base_widget_get_property (GObject * object, guint prop_id,
119 GValue * value, GParamSpec * pspec)
120 {
121 GtkGstBaseWidget *gtk_widget = GTK_GST_BASE_WIDGET (object);
122
123 switch (prop_id) {
124 case PROP_FORCE_ASPECT_RATIO:
125 g_value_set_boolean (value, gtk_widget->force_aspect_ratio);
126 break;
127 case PROP_PIXEL_ASPECT_RATIO:
128 gst_value_set_fraction (value, gtk_widget->par_n, gtk_widget->par_d);
129 break;
130 case PROP_IGNORE_ALPHA:
131 g_value_set_boolean (value, gtk_widget->ignore_alpha);
132 break;
133 default:
134 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
135 break;
136 }
137 }
138
139 static gboolean
_calculate_par(GtkGstBaseWidget * widget,GstVideoInfo * info)140 _calculate_par (GtkGstBaseWidget * widget, GstVideoInfo * info)
141 {
142 gboolean ok;
143 gint width, height;
144 gint par_n, par_d;
145 gint display_par_n, display_par_d;
146
147 width = GST_VIDEO_INFO_WIDTH (info);
148 height = GST_VIDEO_INFO_HEIGHT (info);
149
150 par_n = GST_VIDEO_INFO_PAR_N (info);
151 par_d = GST_VIDEO_INFO_PAR_D (info);
152
153 if (!par_n)
154 par_n = 1;
155
156 /* get display's PAR */
157 if (widget->par_n != 0 && widget->par_d != 0) {
158 display_par_n = widget->par_n;
159 display_par_d = widget->par_d;
160 } else {
161 display_par_n = 1;
162 display_par_d = 1;
163 }
164
165
166 ok = gst_video_calculate_display_ratio (&widget->display_ratio_num,
167 &widget->display_ratio_den, width, height, par_n, par_d, display_par_n,
168 display_par_d);
169
170 if (ok) {
171 GST_LOG ("PAR: %u/%u DAR:%u/%u", par_n, par_d, display_par_n,
172 display_par_d);
173 return TRUE;
174 }
175
176 return FALSE;
177 }
178
179 static void
_apply_par(GtkGstBaseWidget * widget)180 _apply_par (GtkGstBaseWidget * widget)
181 {
182 guint display_ratio_num, display_ratio_den;
183 gint width, height;
184
185 width = GST_VIDEO_INFO_WIDTH (&widget->v_info);
186 height = GST_VIDEO_INFO_HEIGHT (&widget->v_info);
187
188 display_ratio_num = widget->display_ratio_num;
189 display_ratio_den = widget->display_ratio_den;
190
191 if (height % display_ratio_den == 0) {
192 GST_DEBUG ("keeping video height");
193 widget->display_width = (guint)
194 gst_util_uint64_scale_int (height, display_ratio_num,
195 display_ratio_den);
196 widget->display_height = height;
197 } else if (width % display_ratio_num == 0) {
198 GST_DEBUG ("keeping video width");
199 widget->display_width = width;
200 widget->display_height = (guint)
201 gst_util_uint64_scale_int (width, display_ratio_den, display_ratio_num);
202 } else {
203 GST_DEBUG ("approximating while keeping video height");
204 widget->display_width = (guint)
205 gst_util_uint64_scale_int (height, display_ratio_num,
206 display_ratio_den);
207 widget->display_height = height;
208 }
209
210 GST_DEBUG ("scaling to %dx%d", widget->display_width, widget->display_height);
211 }
212
213 static gboolean
_queue_draw(GtkGstBaseWidget * widget)214 _queue_draw (GtkGstBaseWidget * widget)
215 {
216 GTK_GST_BASE_WIDGET_LOCK (widget);
217 widget->draw_id = 0;
218
219 if (widget->pending_resize) {
220 widget->pending_resize = FALSE;
221
222 widget->v_info = widget->pending_v_info;
223 widget->negotiated = TRUE;
224
225 _apply_par (widget);
226
227 gtk_widget_queue_resize (GTK_WIDGET (widget));
228 } else {
229 gtk_widget_queue_draw (GTK_WIDGET (widget));
230 }
231
232 GTK_GST_BASE_WIDGET_UNLOCK (widget);
233
234 return G_SOURCE_REMOVE;
235 }
236
237 static const gchar *
_gdk_key_to_navigation_string(guint keyval)238 _gdk_key_to_navigation_string (guint keyval)
239 {
240 /* TODO: expand */
241 switch (keyval) {
242 #define KEY(key) case GDK_KEY_ ## key: return G_STRINGIFY(key)
243 KEY (Up);
244 KEY (Down);
245 KEY (Left);
246 KEY (Right);
247 KEY (Home);
248 KEY (End);
249 #undef KEY
250 default:
251 return NULL;
252 }
253 }
254
255 static GdkEvent *
_get_current_event(GtkEventController * controller)256 _get_current_event (GtkEventController * controller)
257 {
258 #if defined(BUILD_FOR_GTK4)
259 return gtk_event_controller_get_current_event (controller);
260 #else
261 return gtk_get_current_event ();
262 #endif
263 }
264
265 static void
_gdk_event_free(GdkEvent * event)266 _gdk_event_free (GdkEvent * event)
267 {
268 #if !defined(BUILD_FOR_GTK4)
269 if (event)
270 gdk_event_free (event);
271 #endif
272 }
273
274 static gboolean
gtk_gst_base_widget_key_event(GtkEventControllerKey * key_controller,guint keyval,guint keycode,GdkModifierType state)275 gtk_gst_base_widget_key_event (GtkEventControllerKey * key_controller,
276 guint keyval, guint keycode, GdkModifierType state)
277 {
278 GtkEventController *controller = GTK_EVENT_CONTROLLER (key_controller);
279 GtkWidget *widget = gtk_event_controller_get_widget (controller);
280 GtkGstBaseWidget *base_widget = GTK_GST_BASE_WIDGET (widget);
281 GstElement *element;
282
283 if ((element = g_weak_ref_get (&base_widget->element))) {
284 if (GST_IS_NAVIGATION (element)) {
285 GdkEvent *event = _get_current_event (controller);
286 const gchar *str = _gdk_key_to_navigation_string (keyval);
287
288 if (str) {
289 const gchar *key_type =
290 gdk_event_get_event_type (event) ==
291 GDK_KEY_PRESS ? "key-press" : "key-release";
292 gst_navigation_send_key_event (GST_NAVIGATION (element), key_type, str);
293 }
294 _gdk_event_free (event);
295 }
296 g_object_unref (element);
297 }
298
299 return FALSE;
300 }
301
302 static void
_fit_stream_to_allocated_size(GtkGstBaseWidget * base_widget,GtkAllocation * allocation,GstVideoRectangle * result)303 _fit_stream_to_allocated_size (GtkGstBaseWidget * base_widget,
304 GtkAllocation * allocation, GstVideoRectangle * result)
305 {
306 if (base_widget->force_aspect_ratio) {
307 GstVideoRectangle src, dst;
308
309 src.x = 0;
310 src.y = 0;
311 src.w = base_widget->display_width;
312 src.h = base_widget->display_height;
313
314 dst.x = 0;
315 dst.y = 0;
316 dst.w = allocation->width;
317 dst.h = allocation->height;
318
319 gst_video_sink_center_rect (src, dst, result, TRUE);
320 } else {
321 result->x = 0;
322 result->y = 0;
323 result->w = allocation->width;
324 result->h = allocation->height;
325 }
326 }
327
328 static void
_display_size_to_stream_size(GtkGstBaseWidget * base_widget,gdouble x,gdouble y,gdouble * stream_x,gdouble * stream_y)329 _display_size_to_stream_size (GtkGstBaseWidget * base_widget, gdouble x,
330 gdouble y, gdouble * stream_x, gdouble * stream_y)
331 {
332 gdouble stream_width, stream_height;
333 GtkAllocation allocation;
334 GstVideoRectangle result;
335
336 gtk_widget_get_allocation (GTK_WIDGET (base_widget), &allocation);
337 _fit_stream_to_allocated_size (base_widget, &allocation, &result);
338
339 stream_width = (gdouble) GST_VIDEO_INFO_WIDTH (&base_widget->v_info);
340 stream_height = (gdouble) GST_VIDEO_INFO_HEIGHT (&base_widget->v_info);
341
342 /* from display coordinates to stream coordinates */
343 if (result.w > 0)
344 *stream_x = (x - result.x) / result.w * stream_width;
345 else
346 *stream_x = 0.;
347
348 /* clip to stream size */
349 if (*stream_x < 0.)
350 *stream_x = 0.;
351 if (*stream_x > GST_VIDEO_INFO_WIDTH (&base_widget->v_info))
352 *stream_x = GST_VIDEO_INFO_WIDTH (&base_widget->v_info);
353
354 /* same for y-axis */
355 if (result.h > 0)
356 *stream_y = (y - result.y) / result.h * stream_height;
357 else
358 *stream_y = 0.;
359
360 if (*stream_y < 0.)
361 *stream_y = 0.;
362 if (*stream_y > GST_VIDEO_INFO_HEIGHT (&base_widget->v_info))
363 *stream_y = GST_VIDEO_INFO_HEIGHT (&base_widget->v_info);
364
365 GST_TRACE ("transform %fx%f into %fx%f", x, y, *stream_x, *stream_y);
366 }
367
368 static gboolean
gtk_gst_base_widget_button_event(GtkGestureClick * gesture,gint n_press,gdouble x,gdouble y)369 gtk_gst_base_widget_button_event (
370 #if defined(BUILD_FOR_GTK4)
371 GtkGestureClick * gesture,
372 #else
373 GtkGestureMultiPress * gesture,
374 #endif
375 gint n_press, gdouble x, gdouble y)
376 {
377 GtkEventController *controller = GTK_EVENT_CONTROLLER (gesture);
378 GtkWidget *widget = gtk_event_controller_get_widget (controller);
379 GtkGstBaseWidget *base_widget = GTK_GST_BASE_WIDGET (widget);
380 GstElement *element;
381
382 if ((element = g_weak_ref_get (&base_widget->element))) {
383 if (GST_IS_NAVIGATION (element)) {
384 GdkEvent *event = _get_current_event (controller);
385 const gchar *key_type =
386 gdk_event_get_event_type (event) == GDK_BUTTON_PRESS
387 ? "mouse-button-press" : "mouse-button-release";
388 gdouble stream_x, stream_y;
389 #if !defined(BUILD_FOR_GTK4)
390 guint button;
391 gdk_event_get_button (event, &button);
392 #endif
393
394 _display_size_to_stream_size (base_widget, x, y, &stream_x, &stream_y);
395
396 gst_navigation_send_mouse_event (GST_NAVIGATION (element), key_type,
397 #if defined(BUILD_FOR_GTK4)
398 /* Gesture is set to ignore other buttons so we do not have to check */
399 GDK_BUTTON_PRIMARY,
400 #else
401 button,
402 #endif
403 stream_x, stream_y);
404
405 _gdk_event_free (event);
406 }
407 g_object_unref (element);
408 }
409
410 return FALSE;
411 }
412
413 static gboolean
gtk_gst_base_widget_motion_event(GtkEventControllerMotion * motion_controller,gdouble x,gdouble y)414 gtk_gst_base_widget_motion_event (GtkEventControllerMotion * motion_controller,
415 gdouble x, gdouble y)
416 {
417 GtkEventController *controller = GTK_EVENT_CONTROLLER (motion_controller);
418 GtkWidget *widget = gtk_event_controller_get_widget (controller);
419 GtkGstBaseWidget *base_widget = GTK_GST_BASE_WIDGET (widget);
420 GstElement *element;
421
422 if ((element = g_weak_ref_get (&base_widget->element))) {
423 if (GST_IS_NAVIGATION (element)) {
424 gdouble stream_x, stream_y;
425
426 _display_size_to_stream_size (base_widget, x, y, &stream_x, &stream_y);
427
428 gst_navigation_send_mouse_event (GST_NAVIGATION (element), "mouse-move",
429 0, stream_x, stream_y);
430 }
431 g_object_unref (element);
432 }
433
434 return FALSE;
435 }
436
437 void
gtk_gst_base_widget_class_init(GtkGstBaseWidgetClass * klass)438 gtk_gst_base_widget_class_init (GtkGstBaseWidgetClass * klass)
439 {
440 GObjectClass *gobject_klass = (GObjectClass *) klass;
441 GtkWidgetClass *widget_klass = (GtkWidgetClass *) klass;
442
443 gobject_klass->set_property = gtk_gst_base_widget_set_property;
444 gobject_klass->get_property = gtk_gst_base_widget_get_property;
445
446 g_object_class_install_property (gobject_klass, PROP_FORCE_ASPECT_RATIO,
447 g_param_spec_boolean ("force-aspect-ratio",
448 "Force aspect ratio",
449 "When enabled, scaling will respect original aspect ratio",
450 DEFAULT_FORCE_ASPECT_RATIO,
451 G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
452
453 g_object_class_install_property (gobject_klass, PROP_PIXEL_ASPECT_RATIO,
454 gst_param_spec_fraction ("pixel-aspect-ratio", "Pixel Aspect Ratio",
455 "The pixel aspect ratio of the device", DEFAULT_PAR_N, DEFAULT_PAR_D,
456 G_MAXINT, 1, 1, 1, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
457
458 g_object_class_install_property (gobject_klass, PROP_IGNORE_ALPHA,
459 g_param_spec_boolean ("ignore-alpha", "Ignore Alpha",
460 "When enabled, alpha will be ignored and converted to black",
461 DEFAULT_IGNORE_ALPHA, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
462
463 #if defined(BUILD_FOR_GTK4)
464 widget_klass->measure = gtk_gst_base_widget_measure;
465 #else
466 widget_klass->get_preferred_width = gtk_gst_base_widget_get_preferred_width;
467 widget_klass->get_preferred_height = gtk_gst_base_widget_get_preferred_height;
468 #endif
469
470 GST_DEBUG_CATEGORY_INIT (gst_debug_gtk_base_widget, "gtkbasewidget", 0,
471 "GTK Video Base Widget");
472 }
473
474 void
gtk_gst_base_widget_init(GtkGstBaseWidget * widget)475 gtk_gst_base_widget_init (GtkGstBaseWidget * widget)
476 {
477 widget->force_aspect_ratio = DEFAULT_FORCE_ASPECT_RATIO;
478 widget->par_n = DEFAULT_PAR_N;
479 widget->par_d = DEFAULT_PAR_D;
480 widget->ignore_alpha = DEFAULT_IGNORE_ALPHA;
481
482 gst_video_info_init (&widget->v_info);
483 gst_video_info_init (&widget->pending_v_info);
484
485 g_weak_ref_init (&widget->element, NULL);
486 g_mutex_init (&widget->lock);
487
488 widget->key_controller = gtk_event_controller_key_new (
489 #if !defined(BUILD_FOR_GTK4)
490 GTK_WIDGET (widget)
491 #endif
492 );
493 g_signal_connect (widget->key_controller, "key-pressed",
494 G_CALLBACK (gtk_gst_base_widget_key_event), NULL);
495 g_signal_connect (widget->key_controller, "key-released",
496 G_CALLBACK (gtk_gst_base_widget_key_event), NULL);
497
498 widget->motion_controller = gtk_event_controller_motion_new (
499 #if !defined(BUILD_FOR_GTK4)
500 GTK_WIDGET (widget)
501 #endif
502 );
503 g_signal_connect (widget->motion_controller, "motion",
504 G_CALLBACK (gtk_gst_base_widget_motion_event), NULL);
505
506 widget->click_gesture =
507 #if defined(BUILD_FOR_GTK4)
508 gtk_gesture_click_new ();
509 #else
510 gtk_gesture_multi_press_new (GTK_WIDGET (widget));
511 #endif
512 g_signal_connect (widget->click_gesture, "pressed",
513 G_CALLBACK (gtk_gst_base_widget_button_event), NULL);
514 g_signal_connect (widget->click_gesture, "released",
515 G_CALLBACK (gtk_gst_base_widget_button_event), NULL);
516
517 #if defined(BUILD_FOR_GTK4)
518 /* Otherwise widget in grid will appear as a 1x1px
519 * video which might be misleading for users */
520 gtk_widget_set_hexpand (GTK_WIDGET (widget), TRUE);
521 gtk_widget_set_vexpand (GTK_WIDGET (widget), TRUE);
522
523 gtk_widget_set_focusable (GTK_WIDGET (widget), TRUE);
524 gtk_gesture_single_set_button (GTK_GESTURE_SINGLE (widget->click_gesture),
525 GDK_BUTTON_PRIMARY);
526
527 gtk_widget_add_controller (GTK_WIDGET (widget), widget->key_controller);
528 gtk_widget_add_controller (GTK_WIDGET (widget), widget->motion_controller);
529 gtk_widget_add_controller (GTK_WIDGET (widget),
530 GTK_EVENT_CONTROLLER (widget->click_gesture));
531 #endif
532
533 gtk_widget_set_can_focus (GTK_WIDGET (widget), TRUE);
534 }
535
536 void
gtk_gst_base_widget_finalize(GObject * object)537 gtk_gst_base_widget_finalize (GObject * object)
538 {
539 GtkGstBaseWidget *widget = GTK_GST_BASE_WIDGET (object);
540
541 /* GTK4 takes ownership of EventControllers
542 * while GTK3 still needs manual unref */
543 #if !defined(BUILD_FOR_GTK4)
544 g_object_unref (widget->key_controller);
545 g_object_unref (widget->motion_controller);
546 g_object_unref (widget->click_gesture);
547 #endif
548
549 gst_buffer_replace (&widget->pending_buffer, NULL);
550 gst_buffer_replace (&widget->buffer, NULL);
551 g_mutex_clear (&widget->lock);
552 g_weak_ref_clear (&widget->element);
553
554 if (widget->draw_id)
555 g_source_remove (widget->draw_id);
556 }
557
558 void
gtk_gst_base_widget_set_element(GtkGstBaseWidget * widget,GstElement * element)559 gtk_gst_base_widget_set_element (GtkGstBaseWidget * widget,
560 GstElement * element)
561 {
562 g_weak_ref_set (&widget->element, element);
563 }
564
565 gboolean
gtk_gst_base_widget_set_format(GtkGstBaseWidget * widget,GstVideoInfo * v_info)566 gtk_gst_base_widget_set_format (GtkGstBaseWidget * widget,
567 GstVideoInfo * v_info)
568 {
569 GTK_GST_BASE_WIDGET_LOCK (widget);
570
571 if (gst_video_info_is_equal (&widget->pending_v_info, v_info)) {
572 GTK_GST_BASE_WIDGET_UNLOCK (widget);
573 return TRUE;
574 }
575
576 if (!_calculate_par (widget, v_info)) {
577 GTK_GST_BASE_WIDGET_UNLOCK (widget);
578 return FALSE;
579 }
580
581 widget->pending_resize = TRUE;
582 widget->pending_v_info = *v_info;
583
584 GTK_GST_BASE_WIDGET_UNLOCK (widget);
585
586 return TRUE;
587 }
588
589 void
gtk_gst_base_widget_set_buffer(GtkGstBaseWidget * widget,GstBuffer * buffer)590 gtk_gst_base_widget_set_buffer (GtkGstBaseWidget * widget, GstBuffer * buffer)
591 {
592 /* As we have no type, this is better then no check */
593 g_return_if_fail (GTK_IS_WIDGET (widget));
594
595 GTK_GST_BASE_WIDGET_LOCK (widget);
596
597 gst_buffer_replace (&widget->pending_buffer, buffer);
598
599 if (!widget->draw_id) {
600 widget->draw_id = g_idle_add_full (G_PRIORITY_DEFAULT,
601 (GSourceFunc) _queue_draw, widget, NULL);
602 }
603
604 GTK_GST_BASE_WIDGET_UNLOCK (widget);
605 }
606