1 /*
2 * Copyright (C) 2003-2007 the GStreamer project
3 * Julien Moutte <julien@moutte.net>
4 * Ronald Bultje <rbultje@ronald.bitfreak.net>
5 * Copyright (C) 2003-2014 Bastien Nocera <hadess@hadess.net>
6 * Copyright (C) 2005-2008 Tim-Philipp Müller <tim centricular net>
7 * Copyright (C) 2009 Sebastian Dröge <sebastian.droege@collabora.co.uk>
8 * Copyright © 2009 Christian Persch
9 *
10 * This program is free software; you can redistribute it and/or modify
11 * it under the terms of the GNU General Public License as published by
12 * the Free Software Foundation; either version 2 of the License, or
13 * (at your option) any later version.
14 *
15 * This program is distributed in the hope that it will be useful,
16 * but WITHOUT ANY WARRANTY; without even the implied warranty of
17 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 * GNU General Public License for more details.
19 *
20 * You should have received a copy of the GNU General Public License
21 * along with this program; if not, write to the Free Software
22 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
23 *
24 * The Totem project hereby grant permission for non-gpl compatible GStreamer
25 * plugins to be used and distributed together with GStreamer and Totem. This
26 * permission is above and beyond the permissions granted by the GPL license
27 * Totem is covered by.
28 *
29 * Monday 7th February 2005: Christian Schaller: Add exception clause.
30 * See license_change file for details.
31 *
32 */
33
34 /**
35 * SECTION:bacon-video-widget
36 * @short_description: video playing widget and abstraction
37 * @stability: Unstable
38 * @include: bacon-video-widget.h
39 *
40 * #BaconVideoWidget is a widget to play audio or video streams, with support for visualisations for audio-only streams. It has a GStreamer
41 * backend, and abstracts away the differences to provide a simple interface to the functionality required by Totem. It handles all the low-level
42 * audio and video work for Totem (or passes the work off to the backend).
43 **/
44
45 #include <config.h>
46
47 #define GST_USE_UNSTABLE_API 1
48
49 #include <gst/gst.h>
50
51 /* GStreamer Interfaces */
52 #include <gst/video/navigation.h>
53 #include <gst/video/colorbalance.h>
54 /* for detecting sources of errors */
55 #include <gst/video/gstvideosink.h>
56 #include <gst/video/video.h>
57 #include <gst/audio/audio.h>
58 #include <gst/audio/streamvolume.h>
59
60 /* for missing decoder/demuxer detection */
61 #include <gst/pbutils/pbutils.h>
62
63 /* for the cover metadata info */
64 #include <gst/tag/tag.h>
65
66 #include <clutter-gst/clutter-gst.h>
67 #include "totem-aspect-frame.h"
68
69 /* system */
70 #include <unistd.h>
71 #include <time.h>
72 #include <string.h>
73 #include <stdio.h>
74 #include <math.h>
75
76 /* gtk+/gnome */
77 #include <gtk/gtk.h>
78 #include <glib/gi18n-lib.h>
79 #include <gio/gio.h>
80 #include <gdesktop-enums.h>
81
82 #include "totem-gst-helpers.h"
83 #include "totem-gst-pixbuf-helpers.h"
84 #include "bacon-video-widget.h"
85 #include "bacon-video-widget-gst-missing-plugins.h"
86 #include "bacon-video-controls-actor.h"
87 #include "bacon-video-spinner-actor.h"
88 #include "bacon-video-widget-enums.h"
89
90 #define DEFAULT_USER_AGENT "Videos/"VERSION
91
92 #define DEFAULT_CONTROLS_WIDTH 600 /* In pixels */
93 #define LOGO_SIZE 256 /* Maximum size of the logo */
94 #define REWIND_OR_PREVIOUS 4000
95 #define POPUP_HIDING_TIMEOUT 2
96
97 #define MAX_NETWORK_SPEED 10752
98 #define BUFFERING_LEFT_RATIO 1.1
99
100 /* Helper constants */
101 #define NANOSECS_IN_SEC 1000000000
102 #define SEEK_TIMEOUT NANOSECS_IN_SEC / 10
103 #define FORWARD_RATE 1.0
104 #define REVERSE_RATE -1.0
105 #define DIRECTION_STR (forward == FALSE ? "reverse" : "forward")
106
107 #define is_error(e, d, c) \
108 (e->domain == GST_##d##_ERROR && \
109 e->code == GST_##d##_ERROR_##c)
110
111 #define I_(string) (g_intern_static_string (string))
112
113 static void bacon_video_widget_initable_iface_init (GInitableIface *iface);
114
115 /* Signals */
116 enum
117 {
118 SIGNAL_ERROR,
119 SIGNAL_EOS,
120 SIGNAL_REDIRECT,
121 SIGNAL_CHANNELS_CHANGE,
122 SIGNAL_TICK,
123 SIGNAL_GOT_METADATA,
124 SIGNAL_BUFFERING,
125 SIGNAL_MISSING_PLUGINS,
126 SIGNAL_DOWNLOAD_BUFFERING,
127 SIGNAL_SEEK_REQUESTED,
128 SIGNAL_TRACK_SKIP_REQUESTED,
129 SIGNAL_VOLUME_CHANGE_REQUESTED,
130 LAST_SIGNAL
131 };
132
133 /* Properties */
134 enum
135 {
136 PROP_0,
137 PROP_LOGO_MODE,
138 PROP_POSITION,
139 PROP_CURRENT_TIME,
140 PROP_STREAM_LENGTH,
141 PROP_PLAYING,
142 PROP_REFERRER,
143 PROP_SEEKABLE,
144 PROP_USER_AGENT,
145 PROP_VOLUME,
146 PROP_DOWNLOAD_FILENAME,
147 PROP_DEINTERLACING,
148 PROP_BRIGHTNESS,
149 PROP_CONTRAST,
150 PROP_SATURATION,
151 PROP_HUE,
152 PROP_AUDIO_OUTPUT_TYPE,
153 PROP_AV_OFFSET,
154 PROP_REVEAL_CONTROLS
155 };
156
157 static const gchar *video_props_str[4] = {
158 "brightness",
159 "contrast",
160 "saturation",
161 "hue"
162 };
163
164 struct _BaconVideoWidget
165 {
166 GtkClutterEmbed parent;
167
168 char *user_agent;
169
170 char *referrer;
171 char *mrl;
172 char *subtitle_uri;
173 BvwAspectRatio ratio_type;
174
175 GstElement *play;
176 GstElement *video_sink;
177 GstNavigation *navigation;
178
179 guint update_id;
180 guint fill_id;
181
182 GdkPixbuf *logo_pixbuf;
183 GdkPixbuf *cover_pixbuf; /* stream-specific image */
184
185 gboolean media_has_video;
186 gboolean media_has_audio;
187 gint seekable; /* -1 = don't know, FALSE = no */
188 gint64 stream_length;
189 gint64 current_time;
190 gdouble current_position;
191 gboolean is_live;
192
193 GstTagList *tagcache;
194 GstTagList *audiotags;
195 GstTagList *videotags;
196
197 GAsyncQueue *tag_update_queue;
198 guint tag_update_id;
199
200 gboolean got_redirect;
201
202 ClutterActor *stage;
203 ClutterActor *texture;
204 ClutterActor *frame;
205 ClutterActor *header_controls;
206 ClutterActor *controls;
207 ClutterActor *spinner;
208
209 ClutterActor *logo_frame;
210 ClutterContent *logo;
211
212 GdkCursor *cursor;
213
214 /* Controls */
215 gboolean reveal_controls;
216 guint transition_timeout_id;
217 GHashTable *busy_popup_ht; /* key=reason string, value=gboolean */
218
219 /* Visual effects */
220 GstElement *audio_capsfilter;
221 GstElement *audio_pitchcontrol;
222
223 /* Other stuff */
224 gboolean logo_mode;
225 gboolean cursor_shown;
226 gboolean uses_audio_fakesink;
227 gdouble volume;
228 gboolean is_menu;
229 gboolean has_angles;
230 GList *chapters;
231
232 BvwRotation rotation;
233
234 gint video_width; /* Movie width */
235 gint video_height; /* Movie height */
236 gint movie_par_n; /* Movie pixel aspect ratio numerator */
237 gint movie_par_d; /* Movie pixel aspect ratio denominator */
238 gint video_width_pixels; /* Scaled movie width */
239 gint video_height_pixels; /* Scaled movie height */
240 gint video_fps_n;
241 gint video_fps_d;
242
243 BvwAudioOutputType speakersetup;
244
245 GstBus *bus;
246 gulong sig_bus_async;
247
248 gint eos_id;
249
250 /* When seeking, queue up the seeks if they happen before
251 * the previous one finished */
252 GMutex seek_mutex;
253 GstClock *clock;
254 GstClockTime seek_req_time;
255 gint64 seek_time;
256 /* state we want to be in, as opposed to actual pipeline state
257 * which may change asynchronously or during buffering */
258 GstState target_state;
259 gboolean buffering;
260 gboolean download_buffering;
261 char *download_filename;
262 /* used to compute when the download buffer has gone far
263 * enough to start playback, not "amount of buffering time left
264 * to reach 100% fill-level" */
265 gint64 buffering_left;
266
267 /* for easy codec installation */
268 GList *missing_plugins; /* GList of GstMessages */
269 gboolean plugin_install_in_progress;
270 GCancellable *missing_plugins_cancellable;
271
272 /* for mounting locations if necessary */
273 GCancellable *mount_cancellable;
274 gboolean mount_in_progress;
275
276 /* for auth */
277 GMountOperation *auth_dialog;
278 GMountOperationResult auth_last_result;
279 char *user_id, *user_pw;
280
281 /* for stepping */
282 float rate;
283 };
284
285 G_DEFINE_TYPE_WITH_CODE (BaconVideoWidget, bacon_video_widget, GTK_CLUTTER_TYPE_EMBED,
286 G_IMPLEMENT_INTERFACE (G_TYPE_INITABLE,
287 bacon_video_widget_initable_iface_init))
288
289 static void bacon_video_widget_set_property (GObject * object,
290 guint property_id,
291 const GValue * value,
292 GParamSpec * pspec);
293 static void bacon_video_widget_get_property (GObject * object,
294 guint property_id,
295 GValue * value,
296 GParamSpec * pspec);
297
298 static void bacon_video_widget_finalize (GObject * object);
299
300 static void bvw_reconfigure_fill_timeout (BaconVideoWidget *bvw, guint msecs);
301 static void size_changed_cb (GdkScreen *screen, BaconVideoWidget *bvw);
302 static void bvw_stop_play_pipeline (BaconVideoWidget * bvw);
303 static GError* bvw_error_from_gst_error (BaconVideoWidget *bvw, GstMessage *m);
304 static gboolean bvw_check_for_cover_pixbuf (BaconVideoWidget * bvw);
305 static const GdkPixbuf * bvw_get_logo_pixbuf (BaconVideoWidget * bvw);
306 static gboolean bvw_set_playback_direction (BaconVideoWidget *bvw, gboolean forward);
307 static gboolean bacon_video_widget_seek_time_no_lock (BaconVideoWidget *bvw,
308 gint64 _time,
309 GstSeekFlags flag,
310 GError **error);
311 static void set_controls_visibility (BaconVideoWidget *bvw,
312 gboolean visible,
313 gboolean animate);
314
315 typedef struct {
316 GstTagList *tags;
317 const gchar *type;
318 } UpdateTagsDelayedData;
319
320 static void update_tags_delayed_data_destroy (UpdateTagsDelayedData *data);
321
322 static GtkWidgetClass *parent_class = NULL;
323
324 static int bvw_signals[LAST_SIGNAL] = { 0 };
325
326 GST_DEBUG_CATEGORY (_totem_gst_debug_cat);
327 #define GST_CAT_DEFAULT _totem_gst_debug_cat
328
329 typedef gchar * (* MsgToStrFunc) (GstMessage * msg);
330
331 static const gchar *
get_type_name(GType class_type,int type)332 get_type_name (GType class_type, int type)
333 {
334 GEnumClass *eclass;
335 GEnumValue *value;
336
337 eclass = G_ENUM_CLASS (g_type_class_peek (class_type));
338 value = g_enum_get_value (eclass, type);
339
340 if (value == NULL)
341 return "unknown";
342
343 return value->value_nick;
344 }
345
346 static gchar **
bvw_get_missing_plugins_foo(const GList * missing_plugins,MsgToStrFunc func)347 bvw_get_missing_plugins_foo (const GList * missing_plugins, MsgToStrFunc func)
348 {
349 GPtrArray *arr = g_ptr_array_new ();
350 GHashTable *ht;
351
352 ht = g_hash_table_new (g_str_hash, g_str_equal);
353 while (missing_plugins != NULL) {
354 char *tmp;
355 tmp = func (GST_MESSAGE (missing_plugins->data));
356 if (!g_hash_table_lookup (ht, tmp)) {
357 g_ptr_array_add (arr, tmp);
358 g_hash_table_insert (ht, tmp, GINT_TO_POINTER (1));
359 } else {
360 g_free (tmp);
361 }
362 missing_plugins = missing_plugins->next;
363 }
364 g_ptr_array_add (arr, NULL);
365 g_hash_table_destroy (ht);
366 return (gchar **) g_ptr_array_free (arr, FALSE);
367 }
368
369 static gchar **
bvw_get_missing_plugins_details(const GList * missing_plugins)370 bvw_get_missing_plugins_details (const GList * missing_plugins)
371 {
372 return bvw_get_missing_plugins_foo (missing_plugins,
373 gst_missing_plugin_message_get_installer_detail);
374 }
375
376 static gchar **
bvw_get_missing_plugins_descriptions(const GList * missing_plugins)377 bvw_get_missing_plugins_descriptions (const GList * missing_plugins)
378 {
379 return bvw_get_missing_plugins_foo (missing_plugins,
380 gst_missing_plugin_message_get_description);
381 }
382
383 static void
bvw_clear_missing_plugins_messages(BaconVideoWidget * bvw)384 bvw_clear_missing_plugins_messages (BaconVideoWidget * bvw)
385 {
386 g_list_free_full (bvw->missing_plugins, (GDestroyNotify) gst_mini_object_unref);
387 bvw->missing_plugins = NULL;
388 }
389
390 static void
bvw_check_if_video_decoder_is_missing(BaconVideoWidget * bvw)391 bvw_check_if_video_decoder_is_missing (BaconVideoWidget * bvw)
392 {
393 GList *l;
394
395 if (bvw->media_has_video || bvw->missing_plugins == NULL)
396 return;
397
398 for (l = bvw->missing_plugins; l != NULL; l = l->next) {
399 GstMessage *msg = GST_MESSAGE (l->data);
400 gchar *d, *f;
401
402 if ((d = gst_missing_plugin_message_get_installer_detail (msg))) {
403 if ((f = strstr (d, "|decoder-")) && strstr (f, "video")) {
404 GError *err;
405
406 /* create a fake GStreamer error so we get a nice warning message */
407 err = g_error_new (GST_CORE_ERROR, GST_CORE_ERROR_MISSING_PLUGIN, "x");
408 msg = gst_message_new_error (GST_OBJECT (bvw->play), err, NULL);
409 g_error_free (err);
410 err = bvw_error_from_gst_error (bvw, msg);
411 gst_message_unref (msg);
412 g_signal_emit (bvw, bvw_signals[SIGNAL_ERROR], 0, err->message, FALSE);
413 g_error_free (err);
414 g_free (d);
415 break;
416 }
417 g_free (d);
418 }
419 }
420 }
421
422 static void
set_display_pixel_aspect_ratio(GdkMonitor * monitor,GValue * value)423 set_display_pixel_aspect_ratio (GdkMonitor *monitor,
424 GValue *value)
425 {
426 static const gint par[][2] = {
427 {1, 1}, /* regular screen */
428 {16, 15}, /* PAL TV */
429 {11, 10}, /* 525 line Rec.601 video */
430 {54, 59}, /* 625 line Rec.601 video */
431 {64, 45}, /* 1280x1024 on 16:9 display */
432 {5, 3}, /* 1280x1024 on 4:3 display */
433 {4, 3} /* 800x600 on 16:9 display */
434 };
435 guint i;
436 gint par_index;
437 gdouble ratio;
438 gdouble delta;
439 GdkRectangle rect;
440
441 #define DELTA(idx) (ABS (ratio - ((gdouble) par[idx][0] / par[idx][1])))
442
443 /* first calculate the "real" ratio based on the X values;
444 * which is the "physical" w/h divided by the w/h in pixels of the display */
445 gdk_monitor_get_geometry (monitor, &rect);
446
447 ratio = (gdouble) (gdk_monitor_get_width_mm (monitor) * rect.height) /
448 (gdk_monitor_get_height_mm (monitor) * rect.width);
449
450 GST_DEBUG ("calculated pixel aspect ratio: %f", ratio);
451 /* now find the one from par[][2] with the lowest delta to the real one */
452 delta = DELTA (0);
453 par_index = 0;
454
455 for (i = 1; i < sizeof (par) / (sizeof (gint) * 2); ++i) {
456 gdouble this_delta = DELTA (i);
457
458 if (this_delta < delta) {
459 par_index = i;
460 delta = this_delta;
461 }
462 }
463
464 GST_DEBUG ("Decided on index %d (%d/%d)", par_index,
465 par[par_index][0], par[par_index][1]);
466 gst_value_set_fraction (value, par[par_index][0], par[par_index][1]);
467 }
468
469 static void
get_media_size(BaconVideoWidget * bvw,gint * width,gint * height)470 get_media_size (BaconVideoWidget *bvw, gint *width, gint *height)
471 {
472 if (bvw->logo_mode) {
473 const GdkPixbuf *pixbuf;
474
475 pixbuf = bvw_get_logo_pixbuf (bvw);
476 if (pixbuf) {
477 *width = gdk_pixbuf_get_width (pixbuf);
478 *height = gdk_pixbuf_get_height (pixbuf);
479 if (*width == *height) {
480 /* The icons will be square, so lie so we get a 16:9
481 * ratio */
482 *width = (int) ((float) *height / 9. * 16.);
483 }
484 } else {
485 *width = 0;
486 *height = 0;
487 }
488 } else {
489 if (bvw->media_has_video) {
490 GValue disp_par = {0, };
491 guint movie_par_n, movie_par_d, disp_par_n, disp_par_d, num, den;
492
493 /* Create and init the fraction value */
494 g_value_init (&disp_par, GST_TYPE_FRACTION);
495
496 /* Square pixel is our default */
497 gst_value_set_fraction (&disp_par, 1, 1);
498
499 /* Now try getting display's pixel aspect ratio */
500 if (gtk_widget_get_realized (GTK_WIDGET (bvw))) {
501 GdkDisplay *display;
502 GdkWindow *window;
503 GdkMonitor *monitor;
504
505 display = gtk_widget_get_display (GTK_WIDGET (bvw));
506 window = gtk_widget_get_window (GTK_WIDGET (bvw));
507 if (window)
508 monitor = gdk_display_get_monitor_at_window (display, window);
509 else
510 monitor = gdk_display_get_primary_monitor (display);
511 set_display_pixel_aspect_ratio (monitor, &disp_par);
512 }
513
514 disp_par_n = gst_value_get_fraction_numerator (&disp_par);
515 disp_par_d = gst_value_get_fraction_denominator (&disp_par);
516
517 GST_DEBUG ("display PAR is %d/%d", disp_par_n, disp_par_d);
518
519 /* If movie pixel aspect ratio is enforced, use that */
520 if (bvw->ratio_type != BVW_RATIO_AUTO) {
521 switch (bvw->ratio_type) {
522 case BVW_RATIO_SQUARE:
523 movie_par_n = 1;
524 movie_par_d = 1;
525 break;
526 case BVW_RATIO_FOURBYTHREE:
527 movie_par_n = 4 * bvw->video_height;
528 movie_par_d = 3 * bvw->video_width;
529 break;
530 case BVW_RATIO_ANAMORPHIC:
531 movie_par_n = 16 * bvw->video_height;
532 movie_par_d = 9 * bvw->video_width;
533 break;
534 case BVW_RATIO_DVB:
535 movie_par_n = 20 * bvw->video_height;
536 movie_par_d = 9 * bvw->video_width;
537 break;
538 /* handle these to avoid compiler warnings */
539 case BVW_RATIO_AUTO:
540 default:
541 movie_par_n = 0;
542 movie_par_d = 0;
543 g_assert_not_reached ();
544 }
545 } else {
546 /* Use the movie pixel aspect ratio if any */
547 movie_par_n = bvw->movie_par_n;
548 movie_par_d = bvw->movie_par_d;
549 }
550
551 GST_DEBUG ("movie PAR is %d/%d", movie_par_n, movie_par_d);
552
553 if (bvw->video_width == 0 || bvw->video_height == 0) {
554 GST_DEBUG ("width and/or height 0, assuming 1/1 ratio");
555 num = 1;
556 den = 1;
557 } else if (!gst_video_calculate_display_ratio (&num, &den,
558 bvw->video_width, bvw->video_height,
559 movie_par_n, movie_par_d, disp_par_n, disp_par_d)) {
560 GST_WARNING ("overflow calculating display aspect ratio!");
561 num = 1; /* FIXME: what values to use here? */
562 den = 1;
563 }
564
565 GST_DEBUG ("calculated scaling ratio %d/%d for video %dx%d", num, den,
566 bvw->video_width, bvw->video_height);
567
568 /* now find a width x height that respects this display ratio.
569 * prefer those that have one of w/h the same as the incoming video
570 * using wd / hd = num / den */
571
572 /* start with same height, because of interlaced video */
573 /* check hd / den is an integer scale factor, and scale wd with the PAR */
574 if (bvw->video_height % den == 0) {
575 GST_DEBUG ("keeping video height");
576 bvw->video_width_pixels =
577 (guint) gst_util_uint64_scale (bvw->video_height, num, den);
578 bvw->video_height_pixels = bvw->video_height;
579 } else if (bvw->video_width % num == 0) {
580 GST_DEBUG ("keeping video width");
581 bvw->video_width_pixels = bvw->video_width;
582 bvw->video_height_pixels =
583 (guint) gst_util_uint64_scale (bvw->video_width, den, num);
584 } else {
585 GST_DEBUG ("approximating while keeping video height");
586 bvw->video_width_pixels =
587 (guint) gst_util_uint64_scale (bvw->video_height, num, den);
588 bvw->video_height_pixels = bvw->video_height;
589 }
590 GST_DEBUG ("scaling to %dx%d", bvw->video_width_pixels,
591 bvw->video_height_pixels);
592
593 *width = bvw->video_width_pixels;
594 *height = bvw->video_height_pixels;
595
596 /* Free the PAR fraction */
597 g_value_unset (&disp_par);
598 }
599 else {
600 *width = 0;
601 *height = 0;
602 }
603 }
604 }
605
606 static gboolean
leave_notify_cb(GtkWidget * widget,GdkEventCrossing * event,gpointer user_data)607 leave_notify_cb (GtkWidget *widget,
608 GdkEventCrossing *event,
609 gpointer user_data)
610 {
611 gboolean res = GDK_EVENT_PROPAGATE;
612 BaconVideoWidget *bvw = BACON_VIDEO_WIDGET (user_data);
613 GdkDevice *device;
614
615 if (event->detail != GDK_NOTIFY_NONLINEAR &&
616 event->detail != GDK_NOTIFY_NONLINEAR_VIRTUAL)
617 return res;
618
619 device = gdk_event_get_source_device ((GdkEvent *) event);
620 if (gdk_device_get_source (device) == GDK_SOURCE_TOUCHSCREEN)
621 return res;
622
623 if (bvw->reveal_controls) {
624 gboolean not_busy;
625
626 not_busy = g_hash_table_size (bvw->busy_popup_ht) == 0;
627 if (not_busy) {
628 GST_DEBUG ("will hide because we're not busy and cursor left");
629 set_controls_visibility (bvw, FALSE, TRUE);
630 }
631 }
632
633 return res;
634 }
635
636 static void
bacon_video_widget_realize(GtkWidget * widget)637 bacon_video_widget_realize (GtkWidget * widget)
638 {
639 BaconVideoWidget *bvw = BACON_VIDEO_WIDGET (widget);
640 GtkWidget *toplevel;
641
642 GTK_WIDGET_CLASS (parent_class)->realize (widget);
643
644 gtk_widget_set_realized (widget, TRUE);
645
646 /* get screen size changes */
647 g_signal_connect (G_OBJECT (gtk_widget_get_screen (widget)),
648 "size-changed", G_CALLBACK (size_changed_cb), bvw);
649
650 /* setup the toplevel, ready to be resized */
651 toplevel = gtk_widget_get_toplevel (widget);
652 gtk_window_set_geometry_hints (GTK_WINDOW (toplevel), widget, NULL, 0);
653 g_signal_connect (G_OBJECT (toplevel), "leave-notify-event",
654 G_CALLBACK (leave_notify_cb), bvw);
655
656 bvw->missing_plugins_cancellable = g_cancellable_new ();
657 g_object_set_data_full (G_OBJECT (bvw), "missing-plugins-cancellable",
658 bvw->missing_plugins_cancellable, g_object_unref);
659 bacon_video_widget_gst_missing_plugins_setup (bvw);
660 }
661
662 static void
bacon_video_widget_unrealize(GtkWidget * widget)663 bacon_video_widget_unrealize (GtkWidget *widget)
664 {
665 BaconVideoWidget *bvw = BACON_VIDEO_WIDGET (widget);
666 GtkWidget *toplevel;
667
668 GTK_WIDGET_CLASS (parent_class)->unrealize (widget);
669
670 gtk_widget_set_realized (widget, FALSE);
671
672 g_signal_handlers_disconnect_by_func (G_OBJECT (gtk_widget_get_screen (widget)),
673 size_changed_cb, bvw);
674 toplevel = gtk_widget_get_toplevel (widget);
675 g_signal_handlers_disconnect_by_func (G_OBJECT (toplevel),
676 leave_notify_cb, bvw);
677
678 g_cancellable_cancel (bvw->missing_plugins_cancellable);
679 bvw->missing_plugins_cancellable = NULL;
680 g_object_set_data (G_OBJECT (bvw), "missing-plugins-cancellable", NULL);
681 }
682
683 static void
size_changed_cb(GdkScreen * screen,BaconVideoWidget * bvw)684 size_changed_cb (GdkScreen *screen, BaconVideoWidget *bvw)
685 {
686 bvw_check_for_cover_pixbuf (bvw);
687 }
688
689 static void
set_current_actor(BaconVideoWidget * bvw)690 set_current_actor (BaconVideoWidget *bvw)
691 {
692 gboolean draw_logo;
693
694 if (bvw->stage == NULL)
695 return;
696
697 /* If there's only audio and no visualisation, draw the logo as well.
698 * If we have a cover image to display, we display it regardless of whether we're
699 * doing visualisations. */
700 draw_logo = bvw->media_has_audio && !bvw->media_has_video;
701
702 if (bvw->logo_mode || draw_logo) {
703 const GdkPixbuf *pixbuf;
704
705 pixbuf = bvw_get_logo_pixbuf (bvw);
706 if (pixbuf != NULL) {
707 gboolean ret;
708 GError *err = NULL;
709
710 ret = clutter_image_set_data (CLUTTER_IMAGE (bvw->logo),
711 gdk_pixbuf_get_pixels (pixbuf),
712 gdk_pixbuf_get_has_alpha (pixbuf) ? COGL_PIXEL_FORMAT_RGBA_8888 : COGL_PIXEL_FORMAT_RGB_888,
713 gdk_pixbuf_get_width (pixbuf),
714 gdk_pixbuf_get_height (pixbuf),
715 gdk_pixbuf_get_rowstride (pixbuf),
716 &err);
717 if (ret == FALSE) {
718 g_warning ("clutter_image_set_data() failed %s", err->message);
719 g_error_free (err);
720 } else {
721 clutter_actor_show (CLUTTER_ACTOR (bvw->logo_frame));
722 clutter_actor_hide (CLUTTER_ACTOR (bvw->frame));
723 return;
724 }
725 }
726 }
727
728 clutter_actor_show (CLUTTER_ACTOR (bvw->frame));
729 clutter_actor_hide (CLUTTER_ACTOR (bvw->logo_frame));
730 }
731
732 static void
unschedule_hiding_popup(BaconVideoWidget * bvw)733 unschedule_hiding_popup (BaconVideoWidget *bvw)
734 {
735 if (bvw->transition_timeout_id > 0)
736 g_source_remove (bvw->transition_timeout_id);
737 bvw->transition_timeout_id = 0;
738 }
739
740 static gboolean
hide_popup_timeout_cb(BaconVideoWidget * bvw)741 hide_popup_timeout_cb (BaconVideoWidget *bvw)
742 {
743 set_controls_visibility (bvw, FALSE, TRUE);
744 unschedule_hiding_popup (bvw);
745 return G_SOURCE_REMOVE;
746 }
747
748 static void
schedule_hiding_popup(BaconVideoWidget * bvw)749 schedule_hiding_popup (BaconVideoWidget *bvw)
750 {
751 unschedule_hiding_popup (bvw);
752 bvw->transition_timeout_id = g_timeout_add_seconds (POPUP_HIDING_TIMEOUT, (GSourceFunc) hide_popup_timeout_cb, bvw);
753 g_source_set_name_by_id (bvw->transition_timeout_id, "[totem] hide_popup_timeout_cb");
754 }
755
756 static void
set_show_cursor(BaconVideoWidget * bvw,gboolean show_cursor)757 set_show_cursor (BaconVideoWidget *bvw,
758 gboolean show_cursor)
759 {
760 GdkWindow *window;
761
762 bvw->cursor_shown = show_cursor;
763 window = gtk_widget_get_window (GTK_WIDGET (bvw));
764
765 if (!window)
766 return;
767
768 if (show_cursor == FALSE) {
769 GdkCursor *cursor;
770 GdkDisplay *display;
771
772 display = gdk_window_get_display (window);
773 cursor = gdk_cursor_new_for_display (display, GDK_BLANK_CURSOR);
774 gdk_window_set_cursor (window, cursor);
775 g_object_unref (cursor);
776 } else {
777 gdk_window_set_cursor (window, bvw->cursor);
778 }
779 }
780
781 static void
set_controls_visibility(BaconVideoWidget * bvw,gboolean visible,gboolean animate)782 set_controls_visibility (BaconVideoWidget *bvw,
783 gboolean visible,
784 gboolean animate)
785 {
786 guint8 opacity = visible ? OVERLAY_OPACITY : 0;
787 gint header_controls_height;
788 gfloat header_controls_y;
789 guint duration;
790
791 gtk_widget_get_preferred_height (gtk_clutter_actor_get_widget (GTK_CLUTTER_ACTOR (bvw->header_controls)),
792 NULL,
793 &header_controls_height);
794 header_controls_y = visible ? 0 : -header_controls_height;
795
796 duration = animate ? 250 : 0;
797
798 /* FIXME:
799 * Using a show/hide seems to not trigger the
800 * controls to redraw, so let's change the opacity instead */
801 clutter_actor_set_easing_duration (bvw->controls, duration);
802 clutter_actor_set_easing_duration (bvw->header_controls, duration);
803 clutter_actor_set_opacity (bvw->controls, opacity);
804 clutter_actor_set_y (bvw->header_controls, header_controls_y);
805
806 set_show_cursor (bvw, visible);
807 if (visible && animate)
808 schedule_hiding_popup (bvw);
809
810 bvw->reveal_controls = visible;
811 g_object_notify (G_OBJECT (bvw), "reveal-controls");
812 }
813
814 static void
translate_coords(GtkWidget * widget,GdkWindow * window,int x,int y,int * out_x,int * out_y)815 translate_coords (GtkWidget *widget,
816 GdkWindow *window,
817 int x,
818 int y,
819 int *out_x,
820 int *out_y)
821 {
822 GtkWidget *src;
823
824 gdk_window_get_user_data (window, (gpointer *)&src);
825 if (src && src != widget) {
826 gtk_widget_translate_coordinates (src, widget, x, y, out_x, out_y);
827 } else {
828 *out_x = x;
829 *out_y = y;
830 }
831 }
832
833 static gboolean
ignore_event(BaconVideoWidget * bvw,int x,int y)834 ignore_event (BaconVideoWidget *bvw,
835 int x,
836 int y)
837 {
838 ClutterActor *actor;
839
840 actor = clutter_stage_get_actor_at_pos (CLUTTER_STAGE (bvw->stage), CLUTTER_PICK_REACTIVE, x, y);
841
842 /* Eat the GTK+ event if we're not clicking on the video itself */
843 if (actor == bvw->controls)
844 return TRUE;
845
846 return FALSE;
847 }
848
849 /* need to use gstnavigation interface for these vmethods, to allow for the sink
850 to map screen coordinates to video coordinates in the presence of e.g.
851 hardware scaling */
852
853 static gboolean
bacon_video_widget_motion_notify(GtkWidget * widget,GdkEventMotion * event)854 bacon_video_widget_motion_notify (GtkWidget *widget, GdkEventMotion *event)
855 {
856 gboolean res = GDK_EVENT_PROPAGATE;
857 BaconVideoWidget *bvw = BACON_VIDEO_WIDGET (widget);
858 GdkDevice *device;
859 int x, y;
860
861 g_return_val_if_fail (bvw->play != NULL, FALSE);
862
863 if (bvw->navigation && !bvw->logo_mode)
864 gst_navigation_send_mouse_event (bvw->navigation, "mouse-move", 0, event->x, event->y);
865
866 if (GTK_WIDGET_CLASS (parent_class)->motion_notify_event)
867 res |= GTK_WIDGET_CLASS (parent_class)->motion_notify_event (widget, event);
868
869 device = gdk_event_get_source_device ((GdkEvent *) event);
870 if (gdk_device_get_source (device) == GDK_SOURCE_TOUCHSCREEN)
871 return res;
872
873 if (!bvw->reveal_controls)
874 set_controls_visibility (bvw, TRUE, TRUE);
875
876 translate_coords (widget, event->window, event->x, event->y, &x, &y);
877 if (ignore_event (bvw, x, y)) {
878 /* Is the mouse on the popups? */
879 unschedule_hiding_popup (bvw);
880 } else {
881 schedule_hiding_popup (bvw);
882 }
883
884 return res;
885 }
886
887 static gboolean
bacon_video_widget_button_press_or_release(GtkWidget * widget,GdkEventButton * event)888 bacon_video_widget_button_press_or_release (GtkWidget *widget, GdkEventButton *event)
889 {
890 gboolean res = FALSE;
891 BaconVideoWidget *bvw = BACON_VIDEO_WIDGET (widget);
892 int x, y;
893 GdkDevice *device;
894
895 device = gdk_event_get_source_device ((GdkEvent *) event);
896 if (gdk_device_get_source (device) == GDK_SOURCE_TOUCHSCREEN)
897 return FALSE;
898
899 g_return_val_if_fail (bvw->play != NULL, FALSE);
900
901 translate_coords (widget, event->window, event->x, event->y, &x, &y);
902 if (ignore_event (bvw, x, y))
903 return GDK_EVENT_STOP;
904
905 if (event->type != GDK_BUTTON_PRESS &&
906 event->type != GDK_BUTTON_RELEASE)
907 goto bail;
908
909 if (bvw->navigation &&
910 !bvw->logo_mode &&
911 event->button == 1 &&
912 bvw->is_menu != FALSE) {
913 const char *event_str;
914 event_str = (event->type == GDK_BUTTON_PRESS) ? "mouse-button-press" : "mouse-button-release";
915 gst_navigation_send_mouse_event (bvw->navigation,
916 event_str, event->button, x, y);
917
918 /* FIXME need to check whether the backend will have handled
919 * the button press
920 res = TRUE; */
921 }
922
923 bail:
924 if (event->type == GDK_BUTTON_PRESS && GTK_WIDGET_CLASS (parent_class)->button_press_event)
925 res |= GTK_WIDGET_CLASS (parent_class)->button_press_event (widget, event);
926 if (event->type == GDK_BUTTON_RELEASE && GTK_WIDGET_CLASS (parent_class)->button_release_event)
927 res |= GTK_WIDGET_CLASS (parent_class)->button_release_event (widget, event);
928
929 return res;
930 }
931
932 static gboolean
bacon_video_widget_tap(ClutterTapAction * action,ClutterActor * actor,BaconVideoWidget * bvw)933 bacon_video_widget_tap (ClutterTapAction *action,
934 ClutterActor *actor,
935 BaconVideoWidget *bvw)
936 {
937 ClutterInputDevice *device;
938 const ClutterEvent *event;
939 gboolean value;
940
941 GST_DEBUG ("Tap event received");
942
943 event = clutter_gesture_action_get_last_event (CLUTTER_GESTURE_ACTION (action), 0);
944 if (!event)
945 return CLUTTER_EVENT_PROPAGATE;
946
947 device = clutter_event_get_source_device (event);
948 if (device == NULL ||
949 clutter_input_device_get_device_type (device) != CLUTTER_TOUCHSCREEN_DEVICE)
950 return CLUTTER_EVENT_PROPAGATE;
951
952 value = (clutter_actor_get_opacity (bvw->controls) == 0);
953 set_controls_visibility (bvw, value, FALSE);
954 return CLUTTER_EVENT_STOP;
955 }
956
957 static gboolean
bacon_video_widget_swipe(ClutterSwipeAction * action,ClutterActor * actor,ClutterSwipeDirection direction,BaconVideoWidget * bvw)958 bacon_video_widget_swipe (ClutterSwipeAction *action,
959 ClutterActor *actor,
960 ClutterSwipeDirection direction,
961 BaconVideoWidget *bvw)
962 {
963 GST_DEBUG ("Swipe event received");
964
965 if ((direction & CLUTTER_SWIPE_DIRECTION_UP) ||
966 (direction & CLUTTER_SWIPE_DIRECTION_DOWN)) {
967 if ((direction & CLUTTER_SWIPE_DIRECTION_LEFT) ||
968 (direction & CLUTTER_SWIPE_DIRECTION_RIGHT)) {
969 GST_DEBUG ("Ignoring diagonal swipe 0x%X", direction);
970 return CLUTTER_EVENT_PROPAGATE;
971 }
972 }
973
974 if (direction & CLUTTER_SWIPE_DIRECTION_LEFT)
975 g_signal_emit (G_OBJECT (bvw), bvw_signals[SIGNAL_SEEK_REQUESTED], 0,
976 gtk_widget_get_direction (GTK_WIDGET (bvw)) == GTK_TEXT_DIR_RTL);
977 if (direction & CLUTTER_SWIPE_DIRECTION_RIGHT)
978 g_signal_emit (G_OBJECT (bvw), bvw_signals[SIGNAL_SEEK_REQUESTED], 0,
979 gtk_widget_get_direction (GTK_WIDGET (bvw)) == GTK_TEXT_DIR_LTR);
980
981 return CLUTTER_EVENT_STOP;
982 }
983
984 static gboolean
bacon_video_widget_handle_scroll(GtkWidget * widget,GdkEventScroll * event,BaconVideoWidget * bvw)985 bacon_video_widget_handle_scroll (GtkWidget *widget,
986 GdkEventScroll *event,
987 BaconVideoWidget *bvw)
988 {
989 int x, y;
990 gboolean forward;
991 gdouble delta_y;
992
993 g_return_val_if_fail (bvw->play != NULL, FALSE);
994
995 if (event->direction != GDK_SCROLL_SMOOTH)
996 return GDK_EVENT_PROPAGATE;
997
998 if (widget == (gpointer) bvw) {
999 translate_coords (widget, event->window, event->x, event->y, &x, &y);
1000 if (ignore_event (bvw, x, y))
1001 return GDK_EVENT_STOP;
1002 }
1003
1004 gdk_event_get_scroll_deltas ((GdkEvent *) event, NULL, &delta_y);
1005 if (delta_y == 0.0)
1006 return GDK_EVENT_PROPAGATE;
1007 forward = delta_y >= 0.0 ? FALSE : TRUE;
1008
1009 if (widget == (gpointer) bvw ||
1010 widget == g_object_get_data (G_OBJECT (bvw->controls), "seek_scale")) {
1011 if (bvw->seekable > 0)
1012 g_signal_emit (G_OBJECT (bvw), bvw_signals[SIGNAL_SEEK_REQUESTED], 0, forward);
1013 } else if (widget == g_object_get_data (G_OBJECT (bvw->controls), "volume_button")) {
1014 if (bacon_video_widget_can_set_volume (bvw))
1015 g_signal_emit (G_OBJECT (bvw), bvw_signals[SIGNAL_VOLUME_CHANGE_REQUESTED], 0, forward);
1016 }
1017
1018 return GDK_EVENT_STOP;
1019 }
1020
1021 static gboolean
bacon_video_widget_scroll(GtkWidget * widget,GdkEventScroll * event)1022 bacon_video_widget_scroll (GtkWidget *widget, GdkEventScroll *event)
1023 {
1024 return bacon_video_widget_handle_scroll (widget, event, BACON_VIDEO_WIDGET (widget));
1025 }
1026
1027 static void
bacon_video_widget_get_preferred_width(GtkWidget * widget,gint * minimum,gint * natural)1028 bacon_video_widget_get_preferred_width (GtkWidget *widget,
1029 gint *minimum,
1030 gint *natural)
1031 {
1032 /* We could also make the actor a minimum width, based on its contents */
1033 *minimum = *natural = DEFAULT_CONTROLS_WIDTH;
1034 }
1035
1036 static void
bacon_video_widget_get_preferred_height(GtkWidget * widget,gint * minimum,gint * natural)1037 bacon_video_widget_get_preferred_height (GtkWidget *widget,
1038 gint *minimum,
1039 gint *natural)
1040 {
1041 *minimum = *natural = DEFAULT_CONTROLS_WIDTH / 16 * 9;
1042 }
1043
1044 static gboolean
bvw_boolean_handled_accumulator(GSignalInvocationHint * ihint,GValue * return_accu,const GValue * handler_return,gpointer foobar)1045 bvw_boolean_handled_accumulator (GSignalInvocationHint * ihint,
1046 GValue * return_accu, const GValue * handler_return, gpointer foobar)
1047 {
1048 gboolean continue_emission;
1049 gboolean signal_handled;
1050
1051 signal_handled = g_value_get_boolean (handler_return);
1052 g_value_set_boolean (return_accu, signal_handled);
1053 continue_emission = !signal_handled;
1054
1055 return continue_emission;
1056 }
1057
1058 static void
disable_vaapi(void)1059 disable_vaapi (void)
1060 {
1061 GstRegistry *registry;
1062 GstPlugin *plugin;
1063
1064 registry = gst_registry_get ();
1065 plugin = gst_registry_find_plugin (registry, "vaapi");
1066 if (!plugin)
1067 return;
1068 gst_registry_remove_plugin (registry, plugin);
1069 }
1070
1071 static void
bacon_video_widget_class_init(BaconVideoWidgetClass * klass)1072 bacon_video_widget_class_init (BaconVideoWidgetClass * klass)
1073 {
1074 GObjectClass *object_class;
1075 GtkWidgetClass *widget_class;
1076
1077 disable_vaapi ();
1078 clutter_gst_init (NULL, NULL);
1079
1080 object_class = (GObjectClass *) klass;
1081 widget_class = (GtkWidgetClass *) klass;
1082
1083 parent_class = g_type_class_peek_parent (klass);
1084
1085 /* GtkWidget */
1086 widget_class->get_preferred_width = bacon_video_widget_get_preferred_width;
1087 widget_class->get_preferred_height = bacon_video_widget_get_preferred_height;
1088 widget_class->realize = bacon_video_widget_realize;
1089 widget_class->unrealize = bacon_video_widget_unrealize;
1090
1091 widget_class->motion_notify_event = bacon_video_widget_motion_notify;
1092 widget_class->button_press_event = bacon_video_widget_button_press_or_release;
1093 widget_class->button_release_event = bacon_video_widget_button_press_or_release;
1094 widget_class->scroll_event = bacon_video_widget_scroll;
1095
1096 /* GObject */
1097 object_class->set_property = bacon_video_widget_set_property;
1098 object_class->get_property = bacon_video_widget_get_property;
1099 object_class->finalize = bacon_video_widget_finalize;
1100
1101 /* Properties */
1102 /**
1103 * BaconVideoWidget:logo-mode:
1104 *
1105 * Whether the logo should be displayed when no stream is loaded, or the widget
1106 * should take up no space.
1107 **/
1108 g_object_class_install_property (object_class, PROP_LOGO_MODE,
1109 g_param_spec_boolean ("logo-mode", "Logo mode?",
1110 "Whether the logo should be displayed when no stream is loaded.", FALSE,
1111 G_PARAM_READWRITE |
1112 G_PARAM_STATIC_STRINGS));
1113
1114 /**
1115 * BaconVideoWidget:position:
1116 *
1117 * The current position in the stream, as a percentage between <code class="literal">0</code> and <code class="literal">1</code>.
1118 **/
1119 g_object_class_install_property (object_class, PROP_POSITION,
1120 g_param_spec_double ("position", "Position", "The current position in the stream.",
1121 0, 1.0, 0,
1122 G_PARAM_READABLE |
1123 G_PARAM_STATIC_STRINGS));
1124
1125 /**
1126 * BaconVideoWidget:stream-length:
1127 *
1128 * The length of the current stream, in milliseconds.
1129 **/
1130 g_object_class_install_property (object_class, PROP_STREAM_LENGTH,
1131 g_param_spec_int64 ("stream-length", "Stream length",
1132 "The length of the current stream, in milliseconds.", 0, G_MAXINT64, 0,
1133 G_PARAM_READABLE |
1134 G_PARAM_STATIC_STRINGS));
1135
1136 /**
1137 * BaconVideoWidget:playing:
1138 *
1139 * Whether a stream is currently playing.
1140 **/
1141 g_object_class_install_property (object_class, PROP_PLAYING,
1142 g_param_spec_boolean ("playing", "Playing?",
1143 "Whether a stream is currently playing.", FALSE,
1144 G_PARAM_READABLE |
1145 G_PARAM_STATIC_STRINGS));
1146
1147 /**
1148 * BaconVideoWidget:seekable:
1149 *
1150 * Whether the current stream can be seeked.
1151 **/
1152 g_object_class_install_property (object_class, PROP_SEEKABLE,
1153 g_param_spec_boolean ("seekable", "Seekable?",
1154 "Whether the current stream can be seeked.", FALSE,
1155 G_PARAM_READABLE |
1156 G_PARAM_STATIC_STRINGS));
1157
1158 /**
1159 * BaconVideoWidget:volume:
1160 *
1161 * The current volume level, as a percentage between <code class="literal">0</code> and <code class="literal">1</code>.
1162 **/
1163 g_object_class_install_property (object_class, PROP_VOLUME,
1164 g_param_spec_double ("volume", "Volume", "The current volume level.",
1165 0.0, 1.0, 0.0,
1166 G_PARAM_READWRITE |
1167 G_PARAM_STATIC_STRINGS));
1168
1169 /**
1170 * BaconVideoWidget:referrer:
1171 *
1172 * The HTTP referrer URI.
1173 **/
1174 g_object_class_install_property (object_class, PROP_REFERRER,
1175 g_param_spec_string ("referrer", "Referrer URI", "The HTTP referrer URI.",
1176 NULL,
1177 G_PARAM_READWRITE |
1178 G_PARAM_STATIC_STRINGS));
1179
1180 /**
1181 * BaconVideoWidget:user-agent:
1182 *
1183 * The HTTP user agent string to use.
1184 **/
1185 g_object_class_install_property (object_class, PROP_USER_AGENT,
1186 g_param_spec_string ("user-agent", "User agent", "The HTTP user agent string to use.",
1187 NULL,
1188 G_PARAM_READWRITE |
1189 G_PARAM_STATIC_STRINGS));
1190
1191 /**
1192 * BaconVideoWidget:download-filename:
1193 *
1194 * The filename of the fully downloaded stream when using
1195 * download buffering.
1196 **/
1197 g_object_class_install_property (object_class, PROP_DOWNLOAD_FILENAME,
1198 g_param_spec_string ("download-filename", "Download filename.", "The filename of the fully downloaded stream.",
1199 NULL,
1200 G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
1201
1202 /**
1203 * BaconVideoWidget:deinterlacing:
1204 *
1205 * Whether to automatically deinterlace videos.
1206 **/
1207 g_object_class_install_property (object_class, PROP_DEINTERLACING,
1208 g_param_spec_boolean ("deinterlacing", "Deinterlacing?",
1209 "Whether to automatically deinterlace videos.", FALSE,
1210 G_PARAM_READWRITE |
1211 G_PARAM_STATIC_STRINGS));
1212
1213 /**
1214 * BaconVideoWidget:brightness:
1215 *
1216 * The brightness of the video display.
1217 **/
1218 g_object_class_install_property (object_class, PROP_BRIGHTNESS,
1219 g_param_spec_int ("brightness", "Brightness",
1220 "The brightness of the video display.", 0, 65535, 32768,
1221 G_PARAM_READWRITE |
1222 G_PARAM_STATIC_STRINGS));
1223
1224 /**
1225 * BaconVideoWidget:contrast:
1226 *
1227 * The contrast of the video display.
1228 **/
1229 g_object_class_install_property (object_class, PROP_CONTRAST,
1230 g_param_spec_int ("contrast", "Contrast",
1231 "The contrast of the video display.", 0, 65535, 32768,
1232 G_PARAM_READWRITE |
1233 G_PARAM_STATIC_STRINGS));
1234
1235 /**
1236 * BaconVideoWidget:saturation:
1237 *
1238 * The saturation of the video display.
1239 **/
1240 g_object_class_install_property (object_class, PROP_SATURATION,
1241 g_param_spec_int ("saturation", "Saturation",
1242 "The saturation of the video display.", 0, 65535, 32768,
1243 G_PARAM_READWRITE |
1244 G_PARAM_STATIC_STRINGS));
1245
1246 /**
1247 * BaconVideoWidget:hue:
1248 *
1249 * The hue of the video display.
1250 **/
1251 g_object_class_install_property (object_class, PROP_HUE,
1252 g_param_spec_int ("hue", "Hue",
1253 "The hue of the video display.", 0, 65535, 32768,
1254 G_PARAM_READWRITE |
1255 G_PARAM_STATIC_STRINGS));
1256
1257 /**
1258 * BaconVideoWidget:audio-output-type:
1259 *
1260 * The type of audio output to use (e.g. the number of channels).
1261 **/
1262 g_object_class_install_property (object_class, PROP_AUDIO_OUTPUT_TYPE,
1263 g_param_spec_enum ("audio-output-type", "Audio output type",
1264 "The type of audio output to use.", BVW_TYPE_AUDIO_OUTPUT_TYPE,
1265 BVW_AUDIO_SOUND_STEREO,
1266 G_PARAM_READWRITE |
1267 G_PARAM_STATIC_STRINGS));
1268
1269 /**
1270 * BaconVideoWidget:av-offset:
1271 *
1272 * Control the synchronisation offset between the audio and video streams.
1273 * Positive values make the audio ahead of the video and negative values
1274 * make the audio go behind the video.
1275 **/
1276 g_object_class_install_property (object_class, PROP_AV_OFFSET,
1277 g_param_spec_int64 ("av-offset", "Audio/Video offset",
1278 "The synchronisation offset between audio and video in nanoseconds.",
1279 G_MININT64, G_MAXINT64,
1280 0, G_PARAM_READWRITE |
1281 G_PARAM_STATIC_STRINGS));
1282
1283 /**
1284 * BaconVideoWidget:reveal-controls:
1285 *
1286 * Whether to show or hide the controls.
1287 **/
1288 g_object_class_install_property (object_class, PROP_REVEAL_CONTROLS,
1289 g_param_spec_boolean ("reveal-controls", "Reveal controls",
1290 "Whether to show or hide the controls.", FALSE,
1291 G_PARAM_READABLE |
1292 G_PARAM_STATIC_STRINGS));
1293
1294 /* Signals */
1295 /**
1296 * BaconVideoWidget::error:
1297 * @bvw: the #BaconVideoWidget which received the signal
1298 * @message: the error message
1299 * @playback_stopped: %TRUE if playback has stopped due to the error, %FALSE otherwise
1300 * @fatal: %TRUE if the error was fatal to playback, %FALSE otherwise
1301 *
1302 * Emitted when the backend wishes to asynchronously report an error. If @fatal is %TRUE,
1303 * playback of this stream cannot be restarted.
1304 **/
1305 bvw_signals[SIGNAL_ERROR] =
1306 g_signal_new (I_("error"),
1307 G_TYPE_FROM_CLASS (object_class),
1308 G_SIGNAL_RUN_LAST,
1309 0,
1310 NULL, NULL,
1311 g_cclosure_marshal_generic,
1312 G_TYPE_NONE, 2, G_TYPE_STRING, G_TYPE_BOOLEAN);
1313
1314 /**
1315 * BaconVideoWidget::eos:
1316 * @bvw: the #BaconVideoWidget which received the signal
1317 *
1318 * Emitted when the end of the current stream is reached.
1319 **/
1320 bvw_signals[SIGNAL_EOS] =
1321 g_signal_new (I_("eos"),
1322 G_TYPE_FROM_CLASS (object_class),
1323 G_SIGNAL_RUN_LAST,
1324 0,
1325 NULL, NULL, g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0);
1326
1327 /**
1328 * BaconVideoWidget::got-metadata:
1329 * @bvw: the #BaconVideoWidget which received the signal
1330 *
1331 * Emitted when the widget has updated the metadata of the current stream. This
1332 * will typically happen just after opening a stream.
1333 *
1334 * Call bacon_video_widget_get_metadata() to query the updated metadata.
1335 **/
1336 bvw_signals[SIGNAL_GOT_METADATA] =
1337 g_signal_new (I_("got-metadata"),
1338 G_TYPE_FROM_CLASS (object_class),
1339 G_SIGNAL_RUN_LAST,
1340 0,
1341 NULL, NULL, g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0);
1342
1343 /**
1344 * BaconVideoWidget::got-redirect:
1345 * @bvw: the #BaconVideoWidget which received the signal
1346 * @new_mrl: the new MRL
1347 *
1348 * Emitted when a redirect response is received from a stream's server.
1349 **/
1350 bvw_signals[SIGNAL_REDIRECT] =
1351 g_signal_new (I_("got-redirect"),
1352 G_TYPE_FROM_CLASS (object_class),
1353 G_SIGNAL_RUN_LAST,
1354 0,
1355 NULL, NULL, g_cclosure_marshal_VOID__STRING,
1356 G_TYPE_NONE, 1, G_TYPE_STRING);
1357
1358 /**
1359 * BaconVideoWidget::channels-change:
1360 * @bvw: the #BaconVideoWidget which received the signal
1361 *
1362 * Emitted when the number of audio languages available changes, or when the
1363 * selected audio language is changed.
1364 *
1365 * Query the new list of audio languages with bacon_video_widget_get_languages().
1366 **/
1367 bvw_signals[SIGNAL_CHANNELS_CHANGE] =
1368 g_signal_new (I_("channels-change"),
1369 G_TYPE_FROM_CLASS (object_class),
1370 G_SIGNAL_RUN_LAST,
1371 0,
1372 NULL, NULL, g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0);
1373
1374 /**
1375 * BaconVideoWidget::tick:
1376 * @bvw: the #BaconVideoWidget which received the signal
1377 * @current_time: the current position in the stream, in milliseconds since the beginning of the stream
1378 * @stream_length: the length of the stream, in milliseconds
1379 * @current_position: the current position in the stream, as a percentage between <code class="literal">0</code> and <code class="literal">1</code>
1380 * @seekable: %TRUE if the stream can be seeked, %FALSE otherwise
1381 *
1382 * Emitted every time an important time event happens, or at regular intervals when playing a stream.
1383 **/
1384 bvw_signals[SIGNAL_TICK] =
1385 g_signal_new (I_("tick"),
1386 G_TYPE_FROM_CLASS (object_class),
1387 G_SIGNAL_RUN_LAST,
1388 0,
1389 NULL, NULL,
1390 g_cclosure_marshal_generic,
1391 G_TYPE_NONE, 4, G_TYPE_INT64, G_TYPE_INT64, G_TYPE_DOUBLE,
1392 G_TYPE_BOOLEAN);
1393
1394 /**
1395 * BaconVideoWidget::buffering:
1396 * @bvw: the #BaconVideoWidget which received the signal
1397 * @percentage: the percentage of buffering completed, between <code class="literal">0</code> and <code class="literal">1</code>
1398 *
1399 * Emitted regularly when a network stream is being buffered, to provide status updates on the buffering
1400 * progress.
1401 **/
1402 bvw_signals[SIGNAL_BUFFERING] =
1403 g_signal_new (I_("buffering"),
1404 G_TYPE_FROM_CLASS (object_class),
1405 G_SIGNAL_RUN_LAST,
1406 0,
1407 NULL, NULL,
1408 g_cclosure_marshal_VOID__DOUBLE, G_TYPE_NONE, 1, G_TYPE_DOUBLE);
1409
1410 /**
1411 * BaconVideoWidget::missing-plugins:
1412 * @bvw: the #BaconVideoWidget which received the signal
1413 * @details: a %NULL-terminated array of missing plugin details for use when installing the plugins with libgimme-codec
1414 * @descriptions: a %NULL-terminated array of missing plugin descriptions for display to the user
1415 * @playing: %TRUE if the stream could be played even without these plugins, %FALSE otherwise
1416 *
1417 * Emitted when plugins required to play the current stream are not found. This allows the application
1418 * to request the user install them before proceeding to try and play the stream again.
1419 *
1420 * Note that this signal is only available for the GStreamer backend.
1421 *
1422 * Return value: %TRUE if the signal was handled and some action was taken, %FALSE otherwise
1423 **/
1424 bvw_signals[SIGNAL_MISSING_PLUGINS] =
1425 g_signal_new (I_("missing-plugins"),
1426 G_TYPE_FROM_CLASS (object_class),
1427 G_SIGNAL_RUN_LAST,
1428 0,
1429 bvw_boolean_handled_accumulator, NULL,
1430 g_cclosure_marshal_generic,
1431 G_TYPE_BOOLEAN, 3, G_TYPE_STRV, G_TYPE_STRV, G_TYPE_BOOLEAN);
1432
1433 /**
1434 * BaconVideoWidget::download-buffering:
1435 * @bvw: the #BaconVideoWidget which received the signal
1436 * @percentage: the percentage of download buffering completed, between <code class="literal">0</code> and <code class="literal">1</code>
1437 *
1438 * Emitted regularly when a network stream is being cached on disk, to provide status
1439 * updates on the buffering level of the stream.
1440 **/
1441 bvw_signals[SIGNAL_DOWNLOAD_BUFFERING] =
1442 g_signal_new ("download-buffering",
1443 G_TYPE_FROM_CLASS (object_class),
1444 G_SIGNAL_RUN_LAST,
1445 0,
1446 NULL, NULL,
1447 g_cclosure_marshal_VOID__DOUBLE, G_TYPE_NONE, 1, G_TYPE_DOUBLE);
1448
1449 /**
1450 * BaconVideoWidget::seek-requested:
1451 * @bvw: the #BaconVideoWidget which received the signal
1452 * @forward: whether the seek requested is a forward or backward seek.
1453 *
1454 * Emitted when a gesture, our mouse movement that should seek is made.
1455 **/
1456 bvw_signals[SIGNAL_SEEK_REQUESTED] =
1457 g_signal_new ("seek-requested",
1458 G_TYPE_FROM_CLASS (object_class),
1459 G_SIGNAL_RUN_LAST,
1460 0,
1461 NULL, NULL,
1462 g_cclosure_marshal_generic, G_TYPE_NONE, 1, G_TYPE_BOOLEAN);
1463
1464 /**
1465 * BaconVideoWidget::track-skip-requested:
1466 * @bvw: the #BaconVideoWidget which received the signal
1467 * @forward: whether the track change requested is a forward or backward skip.
1468 *
1469 * Emitted when a gesture, our mouse movement that should seek is made.
1470 **/
1471 bvw_signals[SIGNAL_TRACK_SKIP_REQUESTED] =
1472 g_signal_new ("track-skip-requested",
1473 G_TYPE_FROM_CLASS (object_class),
1474 G_SIGNAL_RUN_LAST,
1475 0,
1476 NULL, NULL,
1477 g_cclosure_marshal_generic, G_TYPE_NONE, 1, G_TYPE_BOOLEAN);
1478
1479 /**
1480 * BaconVideoWidget::volume-change-requested:
1481 * @bvw: the #BaconVideoWidget which received the signal
1482 * @increase: whether the volume change requested is an increase or decrease.
1483 *
1484 * Emitted when a gesture, our mouse movement that should change the volume
1485 * is emitted.
1486 **/
1487 bvw_signals[SIGNAL_VOLUME_CHANGE_REQUESTED] =
1488 g_signal_new ("volume-change-requested",
1489 G_TYPE_FROM_CLASS (object_class),
1490 G_SIGNAL_RUN_LAST,
1491 0,
1492 NULL, NULL,
1493 g_cclosure_marshal_generic, G_TYPE_NONE, 1, G_TYPE_BOOLEAN);
1494 }
1495
1496 static void
bacon_video_widget_init(BaconVideoWidget * bvw)1497 bacon_video_widget_init (BaconVideoWidget * bvw)
1498 {
1499 gtk_widget_set_can_focus (GTK_WIDGET (bvw), TRUE);
1500
1501 g_type_class_ref (BVW_TYPE_METADATA_TYPE);
1502 g_type_class_ref (BVW_TYPE_DVD_EVENT);
1503 g_type_class_ref (BVW_TYPE_ROTATION);
1504
1505 g_object_set (G_OBJECT (bvw), "use-layout-size", TRUE, NULL);
1506
1507 bvw->update_id = 0;
1508 bvw->tagcache = NULL;
1509 bvw->audiotags = NULL;
1510 bvw->videotags = NULL;
1511 bvw->volume = -1.0;
1512 bvw->movie_par_n = bvw->movie_par_d = 1;
1513 bvw->rate = FORWARD_RATE;
1514
1515 bvw->tag_update_queue = g_async_queue_new_full ((GDestroyNotify) update_tags_delayed_data_destroy);
1516 bvw->tag_update_id = 0;
1517
1518 g_mutex_init (&bvw->seek_mutex);
1519 bvw->clock = gst_system_clock_obtain ();
1520 bvw->seek_req_time = GST_CLOCK_TIME_NONE;
1521 bvw->seek_time = -1;
1522
1523 bvw->missing_plugins = NULL;
1524 bvw->plugin_install_in_progress = FALSE;
1525
1526 bvw->mount_cancellable = NULL;
1527 bvw->mount_in_progress = FALSE;
1528 bvw->auth_last_result = G_MOUNT_OPERATION_HANDLED;
1529 bvw->auth_dialog = NULL;
1530
1531 bvw->busy_popup_ht = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
1532
1533 bacon_video_widget_gst_missing_plugins_block ();
1534 }
1535
1536 static gboolean bvw_query_timeout (BaconVideoWidget *bvw);
1537 static gboolean bvw_query_buffering_timeout (BaconVideoWidget *bvw);
1538 static void parse_stream_info (BaconVideoWidget *bvw);
1539
1540 static void
bvw_update_stream_info(BaconVideoWidget * bvw)1541 bvw_update_stream_info (BaconVideoWidget *bvw)
1542 {
1543 parse_stream_info (bvw);
1544
1545 g_signal_emit (bvw, bvw_signals[SIGNAL_GOT_METADATA], 0, NULL);
1546 g_signal_emit (bvw, bvw_signals[SIGNAL_CHANNELS_CHANGE], 0);
1547 }
1548
1549 static void
bvw_handle_application_message(BaconVideoWidget * bvw,GstMessage * msg)1550 bvw_handle_application_message (BaconVideoWidget *bvw, GstMessage *msg)
1551 {
1552 const GstStructure *structure;
1553 const gchar *msg_name;
1554
1555 structure = gst_message_get_structure (msg);
1556 msg_name = gst_structure_get_name (structure);
1557 g_return_if_fail (msg_name != NULL);
1558
1559 GST_DEBUG ("Handling application message: %" GST_PTR_FORMAT, structure);
1560
1561 if (strcmp (msg_name, "stream-changed") == 0) {
1562 bvw_update_stream_info (bvw);
1563 }
1564 else if (strcmp (msg_name, "video-size") == 0) {
1565 int w, h;
1566
1567 g_signal_emit (bvw, bvw_signals[SIGNAL_GOT_METADATA], 0, NULL);
1568
1569 /* This is necessary for the pixel-aspect-ratio of the
1570 * display to be taken into account. */
1571 get_media_size (bvw, &w, &h);
1572 clutter_actor_set_size (bvw->texture, w, h);
1573
1574 set_current_actor (bvw);
1575 } else {
1576 g_debug ("Unhandled application message %s", msg_name);
1577 }
1578 }
1579
1580 static gboolean
bvw_do_navigation_query(BaconVideoWidget * bvw,GstQuery * query)1581 bvw_do_navigation_query (BaconVideoWidget * bvw, GstQuery *query)
1582 {
1583 if (!bvw->navigation)
1584 return FALSE;
1585
1586 return gst_element_query (GST_ELEMENT_CAST (bvw->navigation), query);
1587 }
1588
1589 static void
mount_cb(GObject * obj,GAsyncResult * res,gpointer user_data)1590 mount_cb (GObject *obj, GAsyncResult *res, gpointer user_data)
1591 {
1592 BaconVideoWidget * bvw = user_data;
1593 gboolean ret;
1594 gchar *uri;
1595 GError *error = NULL;
1596 GError *err = NULL;
1597 GstMessage *msg;
1598
1599 ret = g_file_mount_enclosing_volume_finish (G_FILE (obj), res, &error);
1600 if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
1601 return;
1602
1603 g_clear_object (&bvw->mount_cancellable);
1604 bvw->mount_in_progress = FALSE;
1605
1606 uri = g_strdup (bvw->mrl);
1607
1608 if (ret) {
1609 GstState target_state;
1610
1611 GST_DEBUG ("Mounting location '%s' successful", GST_STR_NULL (uri));
1612 /* Save the expected pipeline state */
1613 target_state = bvw->target_state;
1614 bacon_video_widget_open (bvw, uri);
1615 if (target_state == GST_STATE_PLAYING)
1616 bacon_video_widget_play (bvw, NULL);
1617 g_free (uri);
1618 return;
1619 }
1620
1621 if (!ret)
1622 GST_DEBUG ("Mounting location '%s' failed: %s", GST_STR_NULL (uri), error->message);
1623 else
1624 GST_DEBUG ("Failed to set '%s' back to playing: %s", GST_STR_NULL (uri), error->message);
1625
1626 /* create a fake GStreamer error so we get a nice warning message */
1627 err = g_error_new_literal (GST_RESOURCE_ERROR, GST_RESOURCE_ERROR_OPEN_READ, error->message);
1628 msg = gst_message_new_error (GST_OBJECT (bvw->play), err, error->message);
1629 g_error_free (err);
1630 g_error_free (error);
1631 err = bvw_error_from_gst_error (bvw, msg);
1632 gst_message_unref (msg);
1633 g_signal_emit (bvw, bvw_signals[SIGNAL_ERROR], 0, err->message, FALSE);
1634 g_error_free (err);
1635
1636 g_free (uri);
1637 }
1638
1639 static void
bvw_handle_element_message(BaconVideoWidget * bvw,GstMessage * msg)1640 bvw_handle_element_message (BaconVideoWidget *bvw, GstMessage *msg)
1641 {
1642 const GstStructure *structure;
1643 const gchar *type_name = NULL;
1644 gchar *src_name;
1645
1646 src_name = gst_object_get_name (msg->src);
1647
1648 structure = gst_message_get_structure (msg);
1649 if (structure)
1650 type_name = gst_structure_get_name (structure);
1651
1652 GST_DEBUG ("from %s: %" GST_PTR_FORMAT, src_name, structure);
1653
1654 if (type_name == NULL)
1655 goto unhandled;
1656
1657 if (strcmp (type_name, "redirect") == 0) {
1658 const gchar *new_location;
1659
1660 new_location = gst_structure_get_string (structure, "new-location");
1661 GST_DEBUG ("Got redirect to '%s'", GST_STR_NULL (new_location));
1662
1663 if (new_location && *new_location) {
1664 g_signal_emit (bvw, bvw_signals[SIGNAL_REDIRECT], 0, new_location);
1665 goto done;
1666 }
1667 } else if (strcmp (type_name, "progress") == 0) {
1668 /* this is similar to buffering messages, but shouldn't affect pipeline
1669 * state; qtdemux emits those when headers are after movie data and
1670 * it is in streaming mode and has to receive all the movie data first */
1671 if (!bvw->buffering) {
1672 gint percent = 0;
1673
1674 if (gst_structure_get_int (structure, "percent", &percent)) {
1675 gdouble fraction = (gdouble) percent / 100.0;
1676 g_signal_emit (bvw, bvw_signals[SIGNAL_BUFFERING], 0, fraction);
1677 }
1678 }
1679 goto done;
1680 } else if (gst_is_missing_plugin_message (msg)) {
1681 bvw->missing_plugins =
1682 g_list_prepend (bvw->missing_plugins, gst_message_ref (msg));
1683 goto done;
1684 } else if (strcmp (type_name, "not-mounted") == 0) {
1685 const GValue *val;
1686 GFile *file;
1687 GMountOperation *mount_op;
1688 GtkWidget *toplevel;
1689 GstState target_state;
1690 const char *uri;
1691
1692 val = gst_structure_get_value (structure, "uri");
1693 uri = g_value_get_string (val);
1694
1695 if (bvw->mount_in_progress) {
1696 g_cancellable_cancel (bvw->mount_cancellable);
1697 g_clear_object (&bvw->mount_cancellable);
1698 bvw->mount_in_progress = FALSE;
1699 }
1700
1701 GST_DEBUG ("Trying to mount location '%s'", GST_STR_NULL (uri));
1702
1703 toplevel = gtk_widget_get_toplevel (GTK_WIDGET (bvw));
1704 if (toplevel == GTK_WIDGET (bvw) || !GTK_IS_WINDOW (toplevel))
1705 toplevel = NULL;
1706
1707 val = gst_structure_get_value (structure, "file");
1708 if (val == NULL)
1709 goto done;
1710
1711 file = G_FILE (g_value_get_object (val));
1712 if (file == NULL)
1713 goto done;
1714
1715 /* Save and restore the expected pipeline state */
1716 target_state = bvw->target_state;
1717 bacon_video_widget_stop (bvw);
1718 bvw->target_state = target_state;
1719
1720 mount_op = gtk_mount_operation_new (toplevel ? GTK_WINDOW (toplevel) : NULL);
1721 bvw->mount_in_progress = TRUE;
1722 bvw->mount_cancellable = g_cancellable_new ();
1723 g_file_mount_enclosing_volume (file, G_MOUNT_MOUNT_NONE,
1724 mount_op, bvw->mount_cancellable, mount_cb, bvw);
1725
1726 g_object_unref (mount_op);
1727 goto done;
1728 } else if (strcmp (type_name, "GstCacheDownloadComplete") == 0) {
1729 const gchar *location;
1730
1731 /* do query for the last time */
1732 bvw_query_buffering_timeout (bvw);
1733 /* Finished buffering the whole file, so don't run the timeout anymore */
1734 bvw_reconfigure_fill_timeout (bvw, 0);
1735
1736 /* Tell the front-end about the downloaded file */
1737 g_object_notify (G_OBJECT (bvw), "download-filename");
1738
1739 location = gst_structure_get_string (structure, "location");
1740 GST_DEBUG ("Finished download of '%s'", GST_STR_NULL (location));
1741 goto done;
1742 } else {
1743 GstNavigationMessageType nav_msg_type =
1744 gst_navigation_message_get_type (msg);
1745
1746 switch (nav_msg_type) {
1747 case GST_NAVIGATION_MESSAGE_MOUSE_OVER: {
1748 gint active;
1749 GdkWindow *window;
1750 GdkDisplay *display;
1751 if (!gst_navigation_message_parse_mouse_over (msg, &active))
1752 break;
1753 window = gtk_widget_get_window (GTK_WIDGET (bvw));
1754 if (active) {
1755 if (bvw->cursor == NULL) {
1756 display = gdk_window_get_display (window);
1757 bvw->cursor = gdk_cursor_new_for_display (display, GDK_HAND2);
1758 }
1759 } else {
1760 g_clear_object (&bvw->cursor);
1761 }
1762 gdk_window_set_cursor (window, bvw->cursor);
1763 goto done;
1764 }
1765 case GST_NAVIGATION_MESSAGE_COMMANDS_CHANGED: {
1766 GstQuery *cmds_q = gst_navigation_query_new_commands();
1767 gboolean res = bvw_do_navigation_query (bvw, cmds_q);
1768
1769 if (res) {
1770 gboolean is_menu = FALSE;
1771 gboolean has_angles = FALSE;
1772 guint i, n;
1773
1774 if (gst_navigation_query_parse_commands_length (cmds_q, &n)) {
1775 for (i = 0; i < n; i++) {
1776 GstNavigationCommand cmd;
1777 if (!gst_navigation_query_parse_commands_nth (cmds_q, i, &cmd))
1778 break;
1779 is_menu |= (cmd == GST_NAVIGATION_COMMAND_ACTIVATE);
1780 is_menu |= (cmd == GST_NAVIGATION_COMMAND_LEFT);
1781 is_menu |= (cmd == GST_NAVIGATION_COMMAND_RIGHT);
1782 is_menu |= (cmd == GST_NAVIGATION_COMMAND_UP);
1783 is_menu |= (cmd == GST_NAVIGATION_COMMAND_DOWN);
1784
1785 has_angles |= (cmd == GST_NAVIGATION_COMMAND_PREV_ANGLE);
1786 has_angles |= (cmd == GST_NAVIGATION_COMMAND_NEXT_ANGLE);
1787 }
1788 }
1789 /* Are we in a menu now? */
1790 if (bvw->is_menu != is_menu) {
1791 bvw->is_menu = is_menu;
1792 g_object_notify (G_OBJECT (bvw), "seekable");
1793 }
1794 /* Do we have angle switching now? */
1795 if (bvw->has_angles != has_angles) {
1796 bvw->has_angles = has_angles;
1797 g_signal_emit (bvw, bvw_signals[SIGNAL_CHANNELS_CHANGE], 0);
1798 }
1799 }
1800
1801 gst_query_unref (cmds_q);
1802 goto done;
1803 }
1804 case GST_NAVIGATION_MESSAGE_ANGLES_CHANGED:
1805 case GST_NAVIGATION_MESSAGE_INVALID:
1806 goto unhandled;
1807 default:
1808 break;
1809 }
1810 }
1811
1812 unhandled:
1813 GST_WARNING ("Unhandled element message %s from %s: %" GST_PTR_FORMAT,
1814 GST_STR_NULL (type_name), GST_STR_NULL (src_name), msg);
1815
1816 done:
1817 g_free (src_name);
1818 }
1819
1820 /* This is a hack to avoid doing poll_for_state_change() indirectly
1821 * from the bus message callback (via EOS => totem => close => wait for READY)
1822 * and deadlocking there. We need something like a
1823 * gst_bus_set_auto_flushing(bus, FALSE) ... */
1824 static gboolean
bvw_signal_eos_delayed(gpointer user_data)1825 bvw_signal_eos_delayed (gpointer user_data)
1826 {
1827 BaconVideoWidget *bvw = BACON_VIDEO_WIDGET (user_data);
1828
1829 g_signal_emit (bvw, bvw_signals[SIGNAL_EOS], 0, NULL);
1830 bvw->eos_id = 0;
1831 return FALSE;
1832 }
1833
1834 static void
bvw_reconfigure_tick_timeout(BaconVideoWidget * bvw,guint msecs)1835 bvw_reconfigure_tick_timeout (BaconVideoWidget *bvw, guint msecs)
1836 {
1837 if (bvw->update_id != 0) {
1838 GST_DEBUG ("removing tick timeout");
1839 g_source_remove (bvw->update_id);
1840 bvw->update_id = 0;
1841 }
1842 if (msecs > 0) {
1843 GST_DEBUG ("adding tick timeout (at %ums)", msecs);
1844 bvw->update_id =
1845 g_timeout_add (msecs, (GSourceFunc) bvw_query_timeout, bvw);
1846 g_source_set_name_by_id (bvw->update_id, "[totem] bvw_query_timeout");
1847 }
1848 }
1849
1850 static void
bvw_reconfigure_fill_timeout(BaconVideoWidget * bvw,guint msecs)1851 bvw_reconfigure_fill_timeout (BaconVideoWidget *bvw, guint msecs)
1852 {
1853 if (bvw->fill_id != 0) {
1854 GST_DEBUG ("removing fill timeout");
1855 g_source_remove (bvw->fill_id);
1856 bvw->fill_id = 0;
1857 }
1858 if (msecs > 0) {
1859 GST_DEBUG ("adding fill timeout (at %ums)", msecs);
1860 bvw->fill_id =
1861 g_timeout_add (msecs, (GSourceFunc) bvw_query_buffering_timeout, bvw);
1862 g_source_set_name_by_id (bvw->fill_id, "[totem] bvw_query_buffering_timeout");
1863 }
1864 }
1865
1866 /* returns TRUE if the error/signal has been handled and should be ignored */
1867 static gboolean
bvw_emit_missing_plugins_signal(BaconVideoWidget * bvw,gboolean prerolled)1868 bvw_emit_missing_plugins_signal (BaconVideoWidget * bvw, gboolean prerolled)
1869 {
1870 gboolean handled = FALSE;
1871 gchar **descriptions, **details;
1872
1873 details = bvw_get_missing_plugins_details (bvw->missing_plugins);
1874 descriptions = bvw_get_missing_plugins_descriptions (bvw->missing_plugins);
1875
1876 GST_LOG ("emitting missing-plugins signal (prerolled=%d)", prerolled);
1877
1878 g_signal_emit (bvw, bvw_signals[SIGNAL_MISSING_PLUGINS], 0,
1879 details, descriptions, prerolled, &handled);
1880 GST_DEBUG ("missing-plugins signal was %shandled", (handled) ? "" : "not ");
1881
1882 g_strfreev (descriptions);
1883 g_strfreev (details);
1884
1885 if (handled) {
1886 bvw->plugin_install_in_progress = TRUE;
1887 bvw_clear_missing_plugins_messages (bvw);
1888 }
1889
1890 /* if it wasn't handled, we might need the list of missing messages again
1891 * later to create a proper error message with details of what's missing */
1892
1893 return handled;
1894 }
1895
1896 static void
bvw_auth_reply_cb(GMountOperation * op,GMountOperationResult result,BaconVideoWidget * bvw)1897 bvw_auth_reply_cb (GMountOperation *op,
1898 GMountOperationResult result,
1899 BaconVideoWidget *bvw)
1900 {
1901 GST_DEBUG ("Got authentication reply %d", result);
1902 bvw->auth_last_result = result;
1903
1904 if (result == G_MOUNT_OPERATION_HANDLED) {
1905 bvw->user_id = g_strdup (g_mount_operation_get_username (op));
1906 bvw->user_pw = g_strdup (g_mount_operation_get_password (op));
1907 }
1908
1909 g_clear_object (&bvw->auth_dialog);
1910
1911 if (bvw->target_state == GST_STATE_PLAYING) {
1912 GST_DEBUG ("Starting deferred playback after authentication");
1913 bacon_video_widget_play (bvw, NULL);
1914 }
1915 }
1916
1917 static int
bvw_get_http_error_code(GstMessage * err_msg)1918 bvw_get_http_error_code (GstMessage *err_msg)
1919 {
1920 GError *err = NULL;
1921 gchar *dbg = NULL;
1922 int code = -1;
1923
1924 if (g_strcmp0 ("GstRTSPSrc", G_OBJECT_TYPE_NAME (err_msg->src)) != 0 &&
1925 g_strcmp0 ("GstSoupHTTPSrc", G_OBJECT_TYPE_NAME (err_msg->src)) != 0)
1926 return code;
1927
1928 gst_message_parse_error (err_msg, &err, &dbg);
1929
1930 /* Urgh! Check whether this is an auth error */
1931 if (err == NULL || dbg == NULL)
1932 goto done;
1933 if (!is_error (err, RESOURCE, READ) &&
1934 !is_error (err, RESOURCE, OPEN_READ))
1935 goto done;
1936
1937 /* FIXME: Need to find a better way than parsing the plain text */
1938 /* Keep in sync with bvw_error_from_gst_error() */
1939 if (strstr (dbg, "401") != NULL)
1940 code = 401;
1941 else if (strstr (dbg, "404") != NULL)
1942 code = 404;
1943 else if (strstr (dbg, "403") != NULL)
1944 code = 403;
1945 else if (strstr (dbg, "install glib-networking") != NULL)
1946 code = 495;
1947
1948 done:
1949 if (err != NULL)
1950 g_error_free (err);
1951 g_free (dbg);
1952 return code;
1953 }
1954
1955 /* returns TRUE if the error should be ignored */
1956 static gboolean
bvw_check_missing_auth(BaconVideoWidget * bvw,GstMessage * err_msg)1957 bvw_check_missing_auth (BaconVideoWidget * bvw, GstMessage * err_msg)
1958 {
1959 GtkWidget *toplevel;
1960 GMountOperationClass *klass;
1961 int code;
1962
1963 if (gtk_widget_get_realized (GTK_WIDGET (bvw)) == FALSE)
1964 return FALSE;
1965
1966 /* The user already tried, and we aborted */
1967 if (bvw->auth_last_result == G_MOUNT_OPERATION_ABORTED) {
1968 GST_DEBUG ("Not authenticating, the user aborted the last auth attempt");
1969 return FALSE;
1970 }
1971 /* There's already an auth on-going, ignore */
1972 if (bvw->auth_dialog != NULL) {
1973 GST_DEBUG ("Ignoring error, we're doing authentication");
1974 return TRUE;
1975 }
1976
1977 /* RTSP or HTTP source with user-id property ? */
1978 code = bvw_get_http_error_code (err_msg);
1979 if (code != 401)
1980 return FALSE;
1981
1982 if (g_object_class_find_property (G_OBJECT_GET_CLASS (err_msg->src), "user-id") == NULL) {
1983 GST_DEBUG ("HTTP error is 401, but don't have \"user-id\" property, exiting");
1984 return FALSE;
1985 }
1986
1987 GST_DEBUG ("Trying to get auth for location '%s'", GST_STR_NULL (bvw->mrl));
1988
1989 if (bvw->auth_dialog == NULL) {
1990 toplevel = gtk_widget_get_toplevel (GTK_WIDGET (bvw));
1991 bvw->auth_dialog = gtk_mount_operation_new (GTK_WINDOW (toplevel));
1992 g_signal_connect (G_OBJECT (bvw->auth_dialog), "reply",
1993 G_CALLBACK (bvw_auth_reply_cb), bvw);
1994 }
1995
1996 /* And popup the dialogue! */
1997 klass = (GMountOperationClass *) G_OBJECT_GET_CLASS (bvw->auth_dialog);
1998 klass->ask_password (bvw->auth_dialog,
1999 _("Password requested for RTSP server"),
2000 g_get_user_name (),
2001 NULL,
2002 G_ASK_PASSWORD_NEED_PASSWORD | G_ASK_PASSWORD_NEED_USERNAME);
2003 return TRUE;
2004 }
2005
2006 /* returns TRUE if the error has been handled and should be ignored */
2007 static gboolean
bvw_check_missing_plugins_error(BaconVideoWidget * bvw,GstMessage * err_msg)2008 bvw_check_missing_plugins_error (BaconVideoWidget * bvw, GstMessage * err_msg)
2009 {
2010 gboolean error_src_is_playbin;
2011 gboolean ret = FALSE;
2012 GError *err = NULL;
2013
2014 if (bvw->missing_plugins == NULL) {
2015 GST_DEBUG ("no missing-plugin messages");
2016 return FALSE;
2017 }
2018
2019 gst_message_parse_error (err_msg, &err, NULL);
2020
2021 error_src_is_playbin = (err_msg->src == GST_OBJECT_CAST (bvw->play));
2022
2023 /* If we get a WRONG_TYPE error from playbin itself it's most likely because
2024 * there is a subtitle stream we can decode, but no video stream to overlay
2025 * it on. Since there were missing-plugins messages, we'll assume this is
2026 * because we cannot decode the video stream (this should probably be fixed
2027 * in playbin, but for now we'll work around it here) */
2028 if (is_error (err, CORE, MISSING_PLUGIN) ||
2029 is_error (err, STREAM, CODEC_NOT_FOUND) ||
2030 (is_error (err, STREAM, WRONG_TYPE) && error_src_is_playbin)) {
2031 ret = bvw_emit_missing_plugins_signal (bvw, FALSE);
2032 if (ret) {
2033 /* If it was handled, stop playback to make sure we're not processing any
2034 * other error messages that might also be on the bus */
2035 bacon_video_widget_stop (bvw);
2036 }
2037 } else {
2038 GST_DEBUG ("not an error code we are looking for, doing nothing");
2039 }
2040
2041 g_error_free (err);
2042 return ret;
2043 }
2044
2045 static gboolean
bvw_check_mpeg_eos(BaconVideoWidget * bvw,GstMessage * err_msg)2046 bvw_check_mpeg_eos (BaconVideoWidget *bvw, GstMessage *err_msg)
2047 {
2048 gboolean ret = FALSE;
2049 g_autoptr(GError) err = NULL;
2050 g_autofree char *dbg = NULL;
2051
2052 gst_message_parse_error (err_msg, &err, &dbg);
2053
2054 /* Error from gst-libs/gst/video/gstvideodecoder.c
2055 * thrown by mpeg2dec */
2056
2057 if (err != NULL &&
2058 dbg != NULL &&
2059 is_error (err, STREAM, DECODE) &&
2060 strstr (dbg, "no valid frames found")) {
2061 if (bvw->eos_id == 0) {
2062 bvw->eos_id = g_idle_add (bvw_signal_eos_delayed, bvw);
2063 g_source_set_name_by_id (bvw->eos_id, "[totem] bvw_signal_eos_delayed");
2064 GST_DEBUG ("Throwing EOS instead of an error when seeking to the end of an MPEG file");
2065 } else {
2066 GST_DEBUG ("Not throwing EOS instead of an error when seeking to the end of an MPEG file, EOS already planned");
2067 }
2068 ret = TRUE;
2069 }
2070
2071 return ret;
2072 }
2073
2074 /* returns TRUE if the error/signal has been handled and should be ignored */
2075 static gboolean
bvw_check_missing_plugins_on_preroll(BaconVideoWidget * bvw)2076 bvw_check_missing_plugins_on_preroll (BaconVideoWidget * bvw)
2077 {
2078 if (bvw->missing_plugins == NULL) {
2079 GST_DEBUG ("no missing-plugin messages");
2080 return FALSE;
2081 }
2082
2083 return bvw_emit_missing_plugins_signal (bvw, TRUE);
2084 }
2085
2086 static void
update_orientation_from_video(BaconVideoWidget * bvw)2087 update_orientation_from_video (BaconVideoWidget *bvw)
2088 {
2089 BvwRotation rotation = BVW_ROTATION_R_ZERO;
2090 char *orientation_str = NULL;
2091 gboolean ret;
2092 gdouble angle;
2093
2094 /* Don't change the rotation if explicitely set */
2095 if (bvw->rotation != BVW_ROTATION_R_ZERO)
2096 return;
2097
2098 ret = gst_tag_list_get_string_index (bvw->tagcache,
2099 GST_TAG_IMAGE_ORIENTATION, 0, &orientation_str);
2100 if (!ret || !orientation_str || g_str_equal (orientation_str, "rotate-0"))
2101 rotation = BVW_ROTATION_R_ZERO;
2102 else if (g_str_equal (orientation_str, "rotate-90"))
2103 rotation = BVW_ROTATION_R_90R;
2104 else if (g_str_equal (orientation_str, "rotate-180"))
2105 rotation = BVW_ROTATION_R_180;
2106 else if (g_str_equal (orientation_str, "rotate-270"))
2107 rotation = BVW_ROTATION_R_90L;
2108 else
2109 g_warning ("Unhandled orientation value: '%s'", orientation_str);
2110
2111 g_free (orientation_str);
2112
2113 angle = rotation * 90.0;
2114 totem_aspect_frame_set_rotation (TOTEM_ASPECT_FRAME (bvw->frame), angle);
2115 }
2116
2117 static void
bvw_update_tags(BaconVideoWidget * bvw,GstTagList * tag_list,const gchar * type)2118 bvw_update_tags (BaconVideoWidget * bvw, GstTagList *tag_list, const gchar *type)
2119 {
2120 GstTagList **cache = NULL;
2121 GstTagList *result;
2122
2123 /* all tags (replace previous tags, title/artist/etc. might change
2124 * in the middle of a stream, e.g. with radio streams) */
2125 result = gst_tag_list_merge (bvw->tagcache, tag_list,
2126 GST_TAG_MERGE_REPLACE);
2127 if (bvw->tagcache &&
2128 result &&
2129 gst_tag_list_is_equal (result, bvw->tagcache)) {
2130 gst_tag_list_unref (result);
2131 GST_WARNING ("Pipeline sent %s tags update with no changes", type);
2132 return;
2133 }
2134 g_clear_pointer (&bvw->tagcache, gst_tag_list_unref);
2135 bvw->tagcache = result;
2136 GST_DEBUG ("Tags: %" GST_PTR_FORMAT, tag_list);
2137
2138 /* media-type-specific tags */
2139 if (!strcmp (type, "video")) {
2140 cache = &bvw->videotags;
2141 } else if (!strcmp (type, "audio")) {
2142 cache = &bvw->audiotags;
2143 }
2144
2145 if (cache) {
2146 result = gst_tag_list_merge (*cache, tag_list, GST_TAG_MERGE_REPLACE);
2147 if (*cache)
2148 gst_tag_list_unref (*cache);
2149 *cache = result;
2150 }
2151
2152 /* clean up */
2153 if (tag_list)
2154 gst_tag_list_unref (tag_list);
2155
2156 bvw_check_for_cover_pixbuf (bvw);
2157
2158 g_signal_emit (bvw, bvw_signals[SIGNAL_GOT_METADATA], 0);
2159
2160 update_orientation_from_video (bvw);
2161
2162 set_current_actor (bvw);
2163 }
2164
2165 static void
update_tags_delayed_data_destroy(UpdateTagsDelayedData * data)2166 update_tags_delayed_data_destroy (UpdateTagsDelayedData *data)
2167 {
2168 g_slice_free (UpdateTagsDelayedData, data);
2169 }
2170
2171 static gboolean
bvw_update_tags_dispatcher(BaconVideoWidget * bvw)2172 bvw_update_tags_dispatcher (BaconVideoWidget *bvw)
2173 {
2174 UpdateTagsDelayedData *data;
2175
2176 /* If we take the queue's lock for the entire function call, we can use it to protect tag_update_id too */
2177 g_async_queue_lock (bvw->tag_update_queue);
2178
2179 while ((data = g_async_queue_try_pop_unlocked (bvw->tag_update_queue)) != NULL) {
2180 bvw_update_tags (bvw, data->tags, data->type);
2181 update_tags_delayed_data_destroy (data);
2182 }
2183
2184 bvw->tag_update_id = 0;
2185 g_async_queue_unlock (bvw->tag_update_queue);
2186
2187 return FALSE;
2188 }
2189
2190 /* Marshal the changed tags to the main thread for updating the GUI
2191 * and sending the BVW signals */
2192 static void
bvw_update_tags_delayed(BaconVideoWidget * bvw,GstTagList * tags,const gchar * type)2193 bvw_update_tags_delayed (BaconVideoWidget *bvw, GstTagList *tags, const gchar *type) {
2194 UpdateTagsDelayedData *data = g_slice_new0 (UpdateTagsDelayedData);
2195
2196 data->tags = tags;
2197 data->type = type;
2198
2199 g_async_queue_lock (bvw->tag_update_queue);
2200 g_async_queue_push_unlocked (bvw->tag_update_queue, data);
2201
2202 if (bvw->tag_update_id == 0) {
2203 bvw->tag_update_id = g_idle_add ((GSourceFunc) bvw_update_tags_dispatcher, bvw);
2204 g_source_set_name_by_id (bvw->tag_update_id, "[totem] bvw_update_tags_dispatcher");
2205 }
2206
2207 g_async_queue_unlock (bvw->tag_update_queue);
2208 }
2209
2210 static void
video_tags_changed_cb(GstElement * playbin2,gint stream_id,gpointer user_data)2211 video_tags_changed_cb (GstElement *playbin2, gint stream_id, gpointer user_data)
2212 {
2213 BaconVideoWidget *bvw = (BaconVideoWidget *) user_data;
2214 GstTagList *tags = NULL;
2215 gint current_stream_id = 0;
2216
2217 g_object_get (G_OBJECT (bvw->play), "current-video", ¤t_stream_id, NULL);
2218
2219 /* Only get the updated tags if it's for our current stream id */
2220 if (current_stream_id != stream_id)
2221 return;
2222
2223 g_signal_emit_by_name (G_OBJECT (bvw->play), "get-video-tags", stream_id, &tags);
2224
2225 if (tags)
2226 bvw_update_tags_delayed (bvw, tags, "video");
2227 }
2228
2229 static void
audio_tags_changed_cb(GstElement * playbin2,gint stream_id,gpointer user_data)2230 audio_tags_changed_cb (GstElement *playbin2, gint stream_id, gpointer user_data)
2231 {
2232 BaconVideoWidget *bvw = (BaconVideoWidget *) user_data;
2233 GstTagList *tags = NULL;
2234 gint current_stream_id = 0;
2235
2236 g_object_get (G_OBJECT (bvw->play), "current-audio", ¤t_stream_id, NULL);
2237
2238 /* Only get the updated tags if it's for our current stream id */
2239 if (current_stream_id != stream_id)
2240 return;
2241
2242 g_signal_emit_by_name (G_OBJECT (bvw->play), "get-audio-tags", stream_id, &tags);
2243
2244 if (tags)
2245 bvw_update_tags_delayed (bvw, tags, "audio");
2246 }
2247
2248 static void
text_tags_changed_cb(GstElement * playbin2,gint stream_id,gpointer user_data)2249 text_tags_changed_cb (GstElement *playbin2, gint stream_id, gpointer user_data)
2250 {
2251 BaconVideoWidget *bvw = (BaconVideoWidget *) user_data;
2252 GstTagList *tags = NULL;
2253 gint current_stream_id = 0;
2254
2255 g_object_get (G_OBJECT (bvw->play), "current-text", ¤t_stream_id, NULL);
2256
2257 /* Only get the updated tags if it's for our current stream id */
2258 if (current_stream_id != stream_id)
2259 return;
2260
2261 g_signal_emit_by_name (G_OBJECT (bvw->play), "get-text-tags", stream_id, &tags);
2262
2263 if (tags)
2264 bvw_update_tags_delayed (bvw, tags, "text");
2265 }
2266
2267 static gboolean
bvw_download_buffering_done(BaconVideoWidget * bvw)2268 bvw_download_buffering_done (BaconVideoWidget *bvw)
2269 {
2270 /* When we set buffering left to 0, that means it's ready to play */
2271 if (bvw->buffering_left == 0) {
2272 GST_DEBUG ("Buffering left is 0, so buffering done");
2273 return TRUE;
2274 }
2275 if (bvw->stream_length <= 0)
2276 return FALSE;
2277 /* When queue2 doesn't implement buffering-left, always think
2278 * it's ready to go */
2279 if (bvw->buffering_left < 0) {
2280 GST_DEBUG ("Buffering left not implemented, so buffering done");
2281 return TRUE;
2282 }
2283
2284 if (bvw->buffering_left * BUFFERING_LEFT_RATIO < bvw->stream_length - bvw->current_time) {
2285 GST_DEBUG ("Buffering left: %" G_GINT64_FORMAT " * %f, = %f < %" G_GUINT64_FORMAT,
2286 bvw->buffering_left, BUFFERING_LEFT_RATIO,
2287 bvw->buffering_left * BUFFERING_LEFT_RATIO,
2288 bvw->stream_length - bvw->current_time);
2289 return TRUE;
2290 }
2291 return FALSE;
2292 }
2293
2294 static void
bvw_handle_buffering_message(GstMessage * message,BaconVideoWidget * bvw)2295 bvw_handle_buffering_message (GstMessage * message, BaconVideoWidget *bvw)
2296 {
2297 GstBufferingMode mode;
2298 gint percent = 0;
2299
2300 gst_message_parse_buffering_stats (message, &mode, NULL, NULL, NULL);
2301 if (mode == GST_BUFFERING_DOWNLOAD) {
2302 if (bvw->download_buffering == FALSE) {
2303 bvw->download_buffering = TRUE;
2304
2305 /* We're not ready to play yet, so pause the stream */
2306 GST_DEBUG ("Pausing because we're not ready to play the buffer yet");
2307 gst_element_set_state (GST_ELEMENT (bvw->play), GST_STATE_PAUSED);
2308
2309 bvw_reconfigure_fill_timeout (bvw, 200);
2310 }
2311
2312 return;
2313 }
2314
2315 /* We switched from download mode to normal buffering */
2316 if (bvw->download_buffering != FALSE) {
2317 bvw_reconfigure_fill_timeout (bvw, 0);
2318 bvw->download_buffering = FALSE;
2319 g_clear_pointer (&bvw->download_filename, g_free);
2320 }
2321
2322 /* Live, timeshift and stream buffering modes */
2323 gst_message_parse_buffering (message, &percent);
2324 g_signal_emit (bvw, bvw_signals[SIGNAL_BUFFERING], 0, (gdouble) percent / 100.0);
2325
2326 if (percent >= 100) {
2327 clutter_actor_hide (bvw->spinner);
2328 /* Reset */
2329 g_object_set (G_OBJECT (bvw->spinner), "percent", 0.0, NULL);
2330 } else {
2331 clutter_actor_show (bvw->spinner);
2332 g_object_set (G_OBJECT (bvw->spinner), "percent", (float) percent, NULL);
2333 }
2334
2335 if (percent >= 100) {
2336 /* a 100% message means buffering is done */
2337 bvw->buffering = FALSE;
2338 /* if the desired state is playing, go back */
2339 if (bvw->target_state == GST_STATE_PLAYING) {
2340 GST_DEBUG ("Buffering done, setting pipeline back to PLAYING");
2341 bacon_video_widget_play (bvw, NULL);
2342 } else {
2343 GST_DEBUG ("Buffering done, keeping pipeline PAUSED");
2344 }
2345 } else if (bvw->target_state == GST_STATE_PLAYING) {
2346 GstState cur_state;
2347
2348 gst_element_get_state (bvw->play, &cur_state, NULL, 0);
2349 if (cur_state != GST_STATE_PAUSED) {
2350 GST_DEBUG ("Buffering ... temporarily pausing playback %d%%", percent);
2351 gst_element_set_state (bvw->play, GST_STATE_PAUSED);
2352 } else {
2353 GST_LOG ("Buffering (already paused) ... %d%%", percent);
2354 }
2355 bvw->buffering = TRUE;
2356 } else {
2357 GST_LOG ("Buffering ... %d", percent);
2358 bvw->buffering = TRUE;
2359 }
2360 }
2361
2362 static inline void
bvw_get_navigation_if_available(BaconVideoWidget * bvw)2363 bvw_get_navigation_if_available (BaconVideoWidget *bvw)
2364 {
2365 GstElement * nav;
2366 nav = gst_bin_get_by_interface (GST_BIN (bvw->play),
2367 GST_TYPE_NAVIGATION);
2368 g_clear_pointer (&bvw->navigation, gst_object_unref);
2369
2370 if (nav)
2371 bvw->navigation = GST_NAVIGATION (nav);
2372 }
2373
2374 static void
bvw_handle_toc_message(GstMessage * message,BaconVideoWidget * bvw)2375 bvw_handle_toc_message (GstMessage *message,
2376 BaconVideoWidget *bvw)
2377 {
2378 GstToc *toc;
2379 GList *entries, *l;
2380 guint i;
2381
2382 gst_message_parse_toc (message, &toc, NULL);
2383 if (gst_toc_get_scope (toc) != GST_TOC_SCOPE_GLOBAL)
2384 goto out;
2385
2386 entries = gst_toc_get_entries (toc);
2387
2388 parse:
2389 if (entries == NULL)
2390 goto out;
2391 if (gst_toc_entry_get_entry_type (entries->data) != GST_TOC_ENTRY_TYPE_CHAPTER) {
2392 if (g_list_length (entries) == 1) {
2393 entries = gst_toc_entry_get_sub_entries (entries->data);
2394 goto parse;
2395 }
2396 goto out;
2397 }
2398
2399 GST_DEBUG ("Found %d chapters", g_list_length (entries));
2400
2401 if (bvw->chapters)
2402 g_list_free_full (bvw->chapters, (GDestroyNotify) gst_mini_object_unref);
2403
2404 for (l = entries, i = 0; l != NULL; l = l->next, i++) {
2405 GstTocEntry *entry = l->data;
2406 gint64 start, stop;
2407
2408 if (!gst_toc_entry_get_start_stop_times (entry, &start, &stop)) {
2409 GST_DEBUG ("Chapter #%d (couldn't get times)", i);
2410 } else {
2411 GST_DEBUG ("Chapter #%d (start: %" G_GINT64_FORMAT " stop: %" G_GINT64_FORMAT ")", i, start, stop);
2412 }
2413 }
2414
2415 bvw->chapters = g_list_copy_deep (entries, (GCopyFunc) gst_mini_object_ref, NULL);
2416
2417 out:
2418 gst_toc_unref (toc);
2419 }
2420
2421 static void
bvw_bus_message_cb(GstBus * bus,GstMessage * message,BaconVideoWidget * bvw)2422 bvw_bus_message_cb (GstBus * bus, GstMessage * message, BaconVideoWidget *bvw)
2423 {
2424 GstMessageType msg_type;
2425
2426 g_return_if_fail (BACON_IS_VIDEO_WIDGET (bvw));
2427
2428 msg_type = GST_MESSAGE_TYPE (message);
2429
2430 if (msg_type != GST_MESSAGE_STATE_CHANGED) {
2431 gchar *src_name = gst_object_get_name (message->src);
2432 GST_LOG ("Handling %s message from element %s",
2433 gst_message_type_get_name (msg_type), src_name);
2434 g_free (src_name);
2435 }
2436
2437 switch (msg_type) {
2438 case GST_MESSAGE_ERROR: {
2439 totem_gst_message_print (message, bvw->play, "totem-error");
2440
2441 if (!bvw_check_missing_plugins_error (bvw, message) &&
2442 !bvw_check_missing_auth (bvw, message) &&
2443 !bvw_check_mpeg_eos (bvw, message)) {
2444 GError *error;
2445
2446 error = bvw_error_from_gst_error (bvw, message);
2447
2448 bvw->target_state = GST_STATE_NULL;
2449 if (bvw->play)
2450 gst_element_set_state (bvw->play, GST_STATE_NULL);
2451
2452 bvw->buffering = FALSE;
2453
2454 g_signal_emit (bvw, bvw_signals[SIGNAL_ERROR], 0,
2455 error->message, TRUE);
2456
2457 g_error_free (error);
2458 }
2459 break;
2460 }
2461 case GST_MESSAGE_WARNING: {
2462 GST_WARNING ("Warning message: %" GST_PTR_FORMAT, message);
2463 break;
2464 }
2465 case GST_MESSAGE_TAG:
2466 /* Ignore TAG messages, we get updated tags from the
2467 * {audio,video,text}-tags-changed signals of playbin2
2468 */
2469 break;
2470 case GST_MESSAGE_EOS:
2471 GST_DEBUG ("EOS message");
2472 /* update slider one last time */
2473 bvw_query_timeout (bvw);
2474 if (bvw->eos_id == 0) {
2475 bvw->eos_id = g_idle_add (bvw_signal_eos_delayed, bvw);
2476 g_source_set_name_by_id (bvw->eos_id, "[totem] bvw_signal_eos_delayed");
2477 }
2478 break;
2479 case GST_MESSAGE_BUFFERING:
2480 bvw_handle_buffering_message (message, bvw);
2481 break;
2482 case GST_MESSAGE_APPLICATION: {
2483 bvw_handle_application_message (bvw, message);
2484 break;
2485 }
2486 case GST_MESSAGE_STATE_CHANGED: {
2487 GstState old_state, new_state;
2488 gchar *src_name;
2489
2490 gst_message_parse_state_changed (message, &old_state, &new_state, NULL);
2491
2492 if (old_state == new_state)
2493 break;
2494
2495 /* we only care about playbin (pipeline) state changes */
2496 if (GST_MESSAGE_SRC (message) != GST_OBJECT (bvw->play))
2497 break;
2498
2499 src_name = gst_object_get_name (message->src);
2500 GST_DEBUG ("%s changed state from %s to %s", src_name,
2501 gst_element_state_get_name (old_state),
2502 gst_element_state_get_name (new_state));
2503 g_free (src_name);
2504
2505 if (new_state <= GST_STATE_READY) {
2506 if (bvw->navigation)
2507 g_clear_object (&bvw->navigation);
2508 }
2509
2510 /* now do stuff */
2511 if (new_state <= GST_STATE_PAUSED) {
2512 bvw_query_timeout (bvw);
2513 bvw_reconfigure_tick_timeout (bvw, 0);
2514 } else if (new_state > GST_STATE_PAUSED) {
2515 bvw_reconfigure_tick_timeout (bvw, 200);
2516 }
2517
2518 if (old_state == GST_STATE_READY && new_state == GST_STATE_PAUSED) {
2519 GST_DEBUG_BIN_TO_DOT_FILE (GST_BIN_CAST (bvw->play),
2520 GST_DEBUG_GRAPH_SHOW_ALL ^ GST_DEBUG_GRAPH_SHOW_NON_DEFAULT_PARAMS,
2521 "totem-prerolled");
2522 bacon_video_widget_get_stream_length (bvw);
2523 bvw_update_stream_info (bvw);
2524 if (!bvw_check_missing_plugins_on_preroll (bvw)) {
2525 /* show a non-fatal warning message if we can't decode the video */
2526 bvw_check_if_video_decoder_is_missing (bvw);
2527 }
2528 /* Now that we have the length, check whether we wanted
2529 * to pause or to stop the pipeline */
2530 if (bvw->target_state == GST_STATE_PAUSED)
2531 bacon_video_widget_pause (bvw);
2532 } else if (old_state == GST_STATE_PAUSED && new_state == GST_STATE_READY) {
2533 bvw->media_has_video = FALSE;
2534 bvw->media_has_audio = FALSE;
2535
2536 /* clean metadata cache */
2537 g_clear_pointer (&bvw->tagcache, gst_tag_list_unref);
2538 g_clear_pointer (&bvw->audiotags, gst_tag_list_unref);
2539 g_clear_pointer (&bvw->videotags, gst_tag_list_unref);
2540
2541 bvw->video_width = 0;
2542 bvw->video_height = 0;
2543 }
2544 break;
2545 }
2546 case GST_MESSAGE_ELEMENT: {
2547 bvw_handle_element_message (bvw, message);
2548 break;
2549 }
2550
2551 case GST_MESSAGE_DURATION_CHANGED: {
2552 gint64 len = -1;
2553 if (gst_element_query_duration (bvw->play, GST_FORMAT_TIME, &len) && len != -1) {
2554 bvw->stream_length = len / GST_MSECOND;
2555 GST_DEBUG ("got new stream length (through duration message) %" G_GINT64_FORMAT, bvw->stream_length);
2556 }
2557 break;
2558 }
2559
2560 case GST_MESSAGE_ASYNC_DONE: {
2561 gint64 _time;
2562 /* When a seek has finished, set the playing state again */
2563 g_mutex_lock (&bvw->seek_mutex);
2564
2565 bvw->seek_req_time = gst_clock_get_internal_time (bvw->clock);
2566 _time = bvw->seek_time;
2567 bvw->seek_time = -1;
2568
2569 g_mutex_unlock (&bvw->seek_mutex);
2570
2571 if (_time >= 0) {
2572 GST_DEBUG ("Have an old seek to schedule, doing it now");
2573 bacon_video_widget_seek_time_no_lock (bvw, _time, 0, NULL);
2574 } else if (bvw->target_state == GST_STATE_PLAYING) {
2575 GST_DEBUG ("Maybe starting deferred playback after seek");
2576 bacon_video_widget_play (bvw, NULL);
2577 }
2578 bvw_get_navigation_if_available (bvw);
2579 bacon_video_widget_get_stream_length (bvw);
2580 bacon_video_widget_is_seekable (bvw);
2581 break;
2582 }
2583
2584 case GST_MESSAGE_TOC: {
2585 bvw_handle_toc_message (message, bvw);
2586 break;
2587 }
2588
2589 /* FIXME: at some point we might want to handle CLOCK_LOST and set the
2590 * pipeline back to PAUSED and then PLAYING again to select a different
2591 * clock (this seems to trip up rtspsrc though so has to wait until
2592 * rtspsrc gets fixed) */
2593 case GST_MESSAGE_CLOCK_PROVIDE:
2594 case GST_MESSAGE_CLOCK_LOST:
2595 case GST_MESSAGE_NEW_CLOCK:
2596 case GST_MESSAGE_STATE_DIRTY:
2597 case GST_MESSAGE_STREAM_STATUS:
2598 break;
2599
2600 case GST_MESSAGE_UNKNOWN:
2601 case GST_MESSAGE_INFO:
2602 case GST_MESSAGE_STEP_DONE:
2603 case GST_MESSAGE_STRUCTURE_CHANGE:
2604 case GST_MESSAGE_SEGMENT_START:
2605 case GST_MESSAGE_SEGMENT_DONE:
2606 case GST_MESSAGE_LATENCY:
2607 case GST_MESSAGE_ASYNC_START:
2608 case GST_MESSAGE_REQUEST_STATE:
2609 case GST_MESSAGE_STEP_START:
2610 case GST_MESSAGE_QOS:
2611 case GST_MESSAGE_PROGRESS:
2612 case GST_MESSAGE_ANY:
2613 case GST_MESSAGE_RESET_TIME:
2614 case GST_MESSAGE_STREAM_START:
2615 case GST_MESSAGE_NEED_CONTEXT:
2616 case GST_MESSAGE_HAVE_CONTEXT:
2617 default:
2618 GST_LOG ("Unhandled message: %" GST_PTR_FORMAT, message);
2619 break;
2620 }
2621 }
2622
2623 static void
got_time_tick(GstElement * play,gint64 time_nanos,BaconVideoWidget * bvw)2624 got_time_tick (GstElement * play, gint64 time_nanos, BaconVideoWidget * bvw)
2625 {
2626 gboolean seekable;
2627
2628 bvw->current_time = (gint64) time_nanos / GST_MSECOND;
2629
2630 if (bvw->stream_length == 0) {
2631 bvw->current_position = 0;
2632 } else {
2633 bvw->current_position =
2634 (gdouble) bvw->current_time / bvw->stream_length;
2635 }
2636
2637 if (bvw->stream_length == 0) {
2638 seekable = bacon_video_widget_is_seekable (bvw);
2639 } else {
2640 if (bvw->seekable == -1)
2641 g_object_notify (G_OBJECT (bvw), "seekable");
2642 seekable = TRUE;
2643 }
2644
2645 bvw->is_live = (bvw->stream_length == 0);
2646
2647 /*
2648 GST_DEBUG ("current time: %" GST_TIME_FORMAT ", stream length: %" GST_TIME_FORMAT ", seekable: %s",
2649 GST_TIME_ARGS (bvw->current_time * GST_MSECOND),
2650 GST_TIME_ARGS (bvw->stream_length * GST_MSECOND),
2651 (seekable) ? "TRUE" : "FALSE");
2652 */
2653
2654 g_signal_emit (bvw, bvw_signals[SIGNAL_TICK], 0,
2655 bvw->current_time, bvw->stream_length,
2656 bvw->current_position,
2657 seekable);
2658 }
2659
2660 static void
bvw_set_user_agent_on_element(BaconVideoWidget * bvw,GstElement * element)2661 bvw_set_user_agent_on_element (BaconVideoWidget * bvw, GstElement * element)
2662 {
2663 const char *user_agent;
2664
2665 if (g_object_class_find_property (G_OBJECT_GET_CLASS (element), "user-agent") == NULL)
2666 return;
2667
2668 user_agent = bvw->user_agent ? bvw->user_agent : DEFAULT_USER_AGENT;
2669 GST_DEBUG ("Setting HTTP user-agent to '%s'", user_agent);
2670 g_object_set (element, "user-agent", user_agent, NULL);
2671 }
2672
2673 static void
bvw_set_auth_on_element(BaconVideoWidget * bvw,GstElement * element)2674 bvw_set_auth_on_element (BaconVideoWidget * bvw, GstElement * element)
2675 {
2676 if (g_object_class_find_property (G_OBJECT_GET_CLASS (element), "user-id") == NULL)
2677 return;
2678 if (bvw->auth_last_result != G_MOUNT_OPERATION_HANDLED)
2679 return;
2680 if (bvw->user_id == NULL || bvw->user_pw == NULL)
2681 return;
2682
2683 GST_DEBUG ("Setting username and password");
2684 g_object_set (element,
2685 "user-id", bvw->user_id,
2686 "user-pw", bvw->user_pw,
2687 NULL);
2688
2689 g_clear_pointer (&bvw->user_id, g_free);
2690 g_clear_pointer (&bvw->user_pw, g_free);
2691 }
2692
2693 static void
bvw_set_http_proxy_on_element(BaconVideoWidget * bvw,GstElement * element,const char * uri_str)2694 bvw_set_http_proxy_on_element (BaconVideoWidget *bvw,
2695 GstElement *element,
2696 const char *uri_str)
2697 {
2698 GstUri *uri;
2699 char *protocol, *proxy_url;
2700 const char *host, *userinfo;
2701 guint port;
2702 char **user_strv;
2703 g_autofree char *user = NULL;
2704 g_autofree char *password = NULL;
2705
2706 uri = gst_uri_from_string (uri_str);
2707 if (!uri) {
2708 GST_DEBUG ("Failed to parse URI '%s'", uri_str);
2709 return;
2710 }
2711
2712 protocol = gst_uri_get_protocol (uri_str);
2713 host = gst_uri_get_host (uri);
2714 port = gst_uri_get_port (uri);
2715
2716 proxy_url = g_strdup_printf ("%s://%s:%d", protocol, host, port);
2717 g_object_set (element, "proxy", proxy_url, NULL);
2718 g_free (proxy_url);
2719
2720 /* https doesn't handle authentication yet */
2721 if (gst_uri_has_protocol (uri_str, "https"))
2722 goto finish;
2723
2724 userinfo = gst_uri_get_userinfo (uri);
2725 if (userinfo == NULL)
2726 goto finish;
2727
2728 user_strv = g_strsplit (userinfo, ":", 2);
2729 user = g_uri_unescape_string (user_strv[0], NULL);
2730 password = g_uri_unescape_string (user_strv[1], NULL);
2731
2732 g_object_set (element,
2733 "proxy-id", user,
2734 "proxy-pw", password,
2735 NULL);
2736 g_strfreev (user_strv);
2737
2738 finish:
2739 gst_uri_unref (uri);
2740 }
2741
2742 static void
bvw_set_proxy_on_element(BaconVideoWidget * bvw,GstElement * element)2743 bvw_set_proxy_on_element (BaconVideoWidget * bvw, GstElement * element)
2744 {
2745 GError *error = NULL;
2746 char **uris;
2747
2748 if (g_object_class_find_property (G_OBJECT_GET_CLASS (element), "proxy") == NULL)
2749 return;
2750
2751 uris = g_proxy_resolver_lookup (g_proxy_resolver_get_default (),
2752 bvw->mrl,
2753 NULL,
2754 &error);
2755 if (!uris) {
2756 if (error != NULL) {
2757 GST_DEBUG ("Failed to look up proxy for MRL '%s': %s",
2758 bvw->mrl,
2759 error->message);
2760 g_clear_error (&error);
2761 }
2762 return;
2763 }
2764
2765 if (!g_str_equal (uris[0], "direct://"))
2766 bvw_set_http_proxy_on_element (bvw, element, uris[0]);
2767 g_strfreev (uris);
2768 }
2769
2770 static void
bvw_set_referrer_on_element(BaconVideoWidget * bvw,GstElement * element)2771 bvw_set_referrer_on_element (BaconVideoWidget * bvw, GstElement * element)
2772 {
2773 GstStructure *extra_headers = NULL;
2774
2775 if (g_object_class_find_property (G_OBJECT_GET_CLASS (element), "extra-headers") == NULL)
2776 return;
2777
2778 GST_DEBUG ("Setting HTTP referrer to '%s'", bvw->referrer ? bvw->referrer : "none");
2779
2780 g_object_get (element, "extra-headers", &extra_headers, NULL);
2781 if (extra_headers == NULL) {
2782 extra_headers = gst_structure_new_empty ("extra-headers");
2783 }
2784 g_assert (GST_IS_STRUCTURE (extra_headers));
2785
2786 if (bvw->referrer != NULL) {
2787 gst_structure_set (extra_headers,
2788 "Referer" /* not a typo! */,
2789 G_TYPE_STRING,
2790 bvw->referrer,
2791 NULL);
2792 } else {
2793 gst_structure_remove_field (extra_headers,
2794 "Referer" /* not a typo! */);
2795 }
2796
2797 g_object_set (element, "extra-headers", extra_headers, NULL);
2798 gst_structure_free (extra_headers);
2799 }
2800
2801 static void
playbin_source_setup_cb(GstElement * playbin,GstElement * source,BaconVideoWidget * bvw)2802 playbin_source_setup_cb (GstElement *playbin,
2803 GstElement *source,
2804 BaconVideoWidget *bvw)
2805 {
2806 GST_DEBUG ("Got source of type '%s'", G_OBJECT_TYPE_NAME (source));
2807 if (g_strcmp0 (G_OBJECT_TYPE_NAME (source), "GstCurlHttpSrc") == 0)
2808 g_warning ("Download buffering not supported with GstCurlHttpSrc, see https://gitlab.freedesktop.org/gstreamer/gst-plugins-base/issues/551");
2809 bvw_set_user_agent_on_element (bvw, source);
2810 bvw_set_referrer_on_element (bvw, source);
2811 bvw_set_auth_on_element (bvw, source);
2812 bvw_set_proxy_on_element (bvw, source);
2813 }
2814
2815 static void
playbin_element_setup_cb(GstElement * playbin,GstElement * element,BaconVideoWidget * bvw)2816 playbin_element_setup_cb (GstElement *playbin,
2817 GstElement *element,
2818 BaconVideoWidget *bvw)
2819 {
2820 char *template;
2821
2822 if (g_strcmp0 (G_OBJECT_TYPE_NAME (element), "GstDownloadBuffer") != 0)
2823 return;
2824
2825 /* See also bacon_video_widget_initable_init() */
2826 template = g_build_filename (g_get_user_cache_dir (), "totem", "stream-buffer", "XXXXXX", NULL);
2827 g_object_set (element, "temp-template", template, NULL);
2828 GST_DEBUG ("Reconfigured file download template to '%s'", template);
2829 g_free (template);
2830 }
2831
2832 static void
playbin_deep_notify_cb(GstObject * gstobject,GstObject * prop_object,GParamSpec * prop,BaconVideoWidget * bvw)2833 playbin_deep_notify_cb (GstObject *gstobject,
2834 GstObject *prop_object,
2835 GParamSpec *prop,
2836 BaconVideoWidget *bvw)
2837 {
2838 if (g_str_equal (prop->name, "temp-location") == FALSE)
2839 return;
2840
2841 g_clear_pointer (&bvw->download_filename, g_free);
2842 g_object_get (G_OBJECT (prop_object),
2843 "temp-location", &bvw->download_filename,
2844 NULL);
2845 }
2846
2847 static gboolean
bvw_query_timeout(BaconVideoWidget * bvw)2848 bvw_query_timeout (BaconVideoWidget *bvw)
2849 {
2850 gint64 pos = -1;
2851
2852 /* check pos of stream */
2853 if (gst_element_query_position (bvw->play, GST_FORMAT_TIME, &pos)) {
2854 if (pos != -1) {
2855 got_time_tick (GST_ELEMENT (bvw->play), pos, bvw);
2856 }
2857 } else {
2858 GST_DEBUG ("could not get position");
2859 }
2860
2861 return TRUE;
2862 }
2863
2864 static gboolean
bvw_query_buffering_timeout(BaconVideoWidget * bvw)2865 bvw_query_buffering_timeout (BaconVideoWidget *bvw)
2866 {
2867 GstQuery *query;
2868 GstElement *element;
2869
2870 element = bvw->play;
2871
2872 query = gst_query_new_buffering (GST_FORMAT_PERCENT);
2873 if (gst_element_query (element, query)) {
2874 gint64 stop, estimated_total;
2875 gdouble fill;
2876 guint n_ranges, i, pos;
2877
2878 gst_query_parse_buffering_range (query, NULL, NULL, &stop, &estimated_total);
2879
2880 /* stop expresses the last bit of data that we have from the currently downloading
2881 * region and is a good value to use for the fill level if it is after our
2882 * current position. */
2883 pos = bvw->current_position * GST_FORMAT_PERCENT_MAX;
2884 if (stop < pos)
2885 stop = -1;
2886
2887 n_ranges = gst_query_get_n_buffering_ranges (query);
2888
2889 for (i = 0; i < n_ranges; i++) {
2890 gint64 n_start, n_stop;
2891 gst_query_parse_nth_buffering_range (query, i, &n_start, &n_stop);
2892
2893 /* take first stop after current offset if not known */
2894 if (stop == -1 && n_stop > pos)
2895 stop = n_stop;
2896
2897 GST_DEBUG ("%s range %d: start %" G_GINT64_FORMAT " stop %" G_GINT64_FORMAT,
2898 n_stop == stop ? "*" : " ",
2899 i, n_start, n_stop);
2900 }
2901 /* if no fill level, just take the current position */
2902 if (stop == -1)
2903 stop = pos;
2904
2905 /* estimated_total is the amount of time it will take to download the
2906 * remaining part of the file, from the current position to the end. */
2907 bvw->buffering_left = estimated_total;
2908 GST_DEBUG ("stop %" G_GINT64_FORMAT ", buffering left %" G_GINT64_FORMAT,
2909 stop, bvw->buffering_left);
2910
2911 fill = (gdouble) stop / GST_FORMAT_PERCENT_MAX;
2912 GST_DEBUG ("download buffer filled up to %f%% (element: %s)", fill * 100.0,
2913 G_OBJECT_TYPE_NAME (element));
2914
2915 g_signal_emit (bvw, bvw_signals[SIGNAL_DOWNLOAD_BUFFERING], 0, fill);
2916
2917 /* Start playing when we've downloaded enough */
2918 if (bvw_download_buffering_done (bvw) != FALSE &&
2919 bvw->target_state == GST_STATE_PLAYING) {
2920 GST_DEBUG ("Starting playback because the download buffer is filled enough");
2921 bacon_video_widget_play (bvw, NULL);
2922 }
2923 } else {
2924 g_debug ("Failed to query the source element for buffering info in percent");
2925 }
2926 gst_query_unref (query);
2927
2928 return TRUE;
2929 }
2930
2931 static void
caps_set(GObject * obj,GParamSpec * pspec,BaconVideoWidget * bvw)2932 caps_set (GObject * obj,
2933 GParamSpec * pspec, BaconVideoWidget * bvw)
2934 {
2935 GstPad *pad = GST_PAD (obj);
2936 GstStructure *s;
2937 GstCaps *caps;
2938
2939 if (!(caps = gst_pad_get_current_caps (pad)))
2940 return;
2941
2942 /* Get video decoder caps */
2943 s = gst_caps_get_structure (caps, 0);
2944 if (s) {
2945 const GValue *movie_par;
2946
2947 /* We need at least width/height and framerate */
2948 if (!(gst_structure_get_fraction (s, "framerate", &bvw->video_fps_n,
2949 &bvw->video_fps_d) &&
2950 gst_structure_get_int (s, "width", &bvw->video_width) &&
2951 gst_structure_get_int (s, "height", &bvw->video_height)))
2952 return;
2953
2954 /* Get the movie PAR if available */
2955 movie_par = gst_structure_get_value (s, "pixel-aspect-ratio");
2956 if (movie_par) {
2957 bvw->movie_par_n = gst_value_get_fraction_numerator (movie_par);
2958 bvw->movie_par_d = gst_value_get_fraction_denominator (movie_par);
2959 }
2960 else {
2961 /* Square pixels */
2962 bvw->movie_par_n = 1;
2963 bvw->movie_par_d = 1;
2964 }
2965
2966 /* Now set for real */
2967 bacon_video_widget_set_aspect_ratio (bvw, bvw->ratio_type);
2968 }
2969
2970 gst_caps_unref (caps);
2971 }
2972
2973 static void
parse_stream_info(BaconVideoWidget * bvw)2974 parse_stream_info (BaconVideoWidget *bvw)
2975 {
2976 GstPad *videopad = NULL;
2977 gint n_audio, n_video;
2978
2979 g_object_get (G_OBJECT (bvw->play), "n-audio", &n_audio,
2980 "n-video", &n_video, NULL);
2981
2982 bvw_check_for_cover_pixbuf (bvw);
2983
2984 bvw->media_has_video = FALSE;
2985 if (n_video > 0) {
2986 gint i;
2987
2988 bvw->media_has_video = TRUE;
2989
2990 for (i = 0; i < n_video && videopad == NULL; i++)
2991 g_signal_emit_by_name (bvw->play, "get-video-pad", i, &videopad);
2992 }
2993
2994 bvw->media_has_audio = (n_audio > 0);
2995
2996 if (videopad) {
2997 GstCaps *caps;
2998
2999 if ((caps = gst_pad_get_current_caps (videopad))) {
3000 caps_set (G_OBJECT (videopad), NULL, bvw);
3001 gst_caps_unref (caps);
3002 }
3003 g_signal_connect (videopad, "notify::caps",
3004 G_CALLBACK (caps_set), bvw);
3005 gst_object_unref (videopad);
3006 }
3007
3008 set_current_actor (bvw);
3009 }
3010
3011 static void
playbin_stream_changed_cb(GstElement * obj,gpointer data)3012 playbin_stream_changed_cb (GstElement * obj, gpointer data)
3013 {
3014 BaconVideoWidget *bvw = BACON_VIDEO_WIDGET (data);
3015 GstMessage *msg;
3016
3017 /* we're being called from the streaming thread, so don't do anything here */
3018 GST_LOG ("streams have changed");
3019 msg = gst_message_new_application (GST_OBJECT (bvw->play),
3020 gst_structure_new_empty ("stream-changed"));
3021 gst_element_post_message (bvw->play, msg);
3022 }
3023
3024 static void
bacon_video_widget_finalize(GObject * object)3025 bacon_video_widget_finalize (GObject * object)
3026 {
3027 BaconVideoWidget *bvw = (BaconVideoWidget *) object;
3028
3029 GST_DEBUG ("finalizing");
3030
3031 g_type_class_unref (g_type_class_peek (BVW_TYPE_METADATA_TYPE));
3032 g_type_class_unref (g_type_class_peek (BVW_TYPE_DVD_EVENT));
3033 g_type_class_unref (g_type_class_peek (BVW_TYPE_ROTATION));
3034
3035 unschedule_hiding_popup (bvw);
3036
3037 if (bvw->bus) {
3038 /* make bus drop all messages to make sure none of our callbacks is ever
3039 * called again (main loop might be run again to display error dialog) */
3040 gst_bus_set_flushing (bvw->bus, TRUE);
3041
3042 if (bvw->sig_bus_async)
3043 g_signal_handler_disconnect (bvw->bus, bvw->sig_bus_async);
3044
3045 g_clear_pointer (&bvw->bus, gst_object_unref);
3046 }
3047
3048 g_clear_pointer (&bvw->user_agent, g_free);
3049 g_clear_pointer (&bvw->referrer, g_free);
3050 g_clear_pointer (&bvw->mrl, g_free);
3051 g_clear_pointer (&bvw->subtitle_uri, g_free);
3052 g_clear_pointer (&bvw->busy_popup_ht, g_hash_table_destroy);
3053
3054 g_clear_object (&bvw->clock);
3055
3056 if (bvw->play != NULL)
3057 gst_element_set_state (bvw->play, GST_STATE_NULL);
3058
3059 g_clear_object (&bvw->play);
3060
3061 if (bvw->update_id) {
3062 g_source_remove (bvw->update_id);
3063 bvw->update_id = 0;
3064 }
3065
3066 if (bvw->chapters) {
3067 g_list_free_full (bvw->chapters, (GDestroyNotify) gst_mini_object_unref);
3068 bvw->chapters = NULL;
3069 }
3070
3071 g_clear_pointer (&bvw->tagcache, gst_tag_list_unref);
3072 g_clear_pointer (&bvw->audiotags, gst_tag_list_unref);
3073 g_clear_pointer (&bvw->videotags, gst_tag_list_unref);
3074
3075 if (bvw->tag_update_id != 0)
3076 g_source_remove (bvw->tag_update_id);
3077 g_async_queue_unref (bvw->tag_update_queue);
3078
3079 if (bvw->eos_id != 0) {
3080 g_source_remove (bvw->eos_id);
3081 bvw->eos_id = 0;
3082 }
3083
3084 g_clear_object (&bvw->cursor);
3085
3086 if (bvw->mount_cancellable)
3087 g_cancellable_cancel (bvw->mount_cancellable);
3088 g_clear_object (&bvw->mount_cancellable);
3089
3090 g_mutex_clear (&bvw->seek_mutex);
3091
3092 G_OBJECT_CLASS (parent_class)->finalize (object);
3093 }
3094
3095 static void
bacon_video_widget_set_property(GObject * object,guint property_id,const GValue * value,GParamSpec * pspec)3096 bacon_video_widget_set_property (GObject * object, guint property_id,
3097 const GValue * value, GParamSpec * pspec)
3098 {
3099 BaconVideoWidget *bvw;
3100
3101 bvw = BACON_VIDEO_WIDGET (object);
3102
3103 switch (property_id) {
3104 case PROP_LOGO_MODE:
3105 bacon_video_widget_set_logo_mode (bvw, g_value_get_boolean (value));
3106 break;
3107 case PROP_REFERRER:
3108 bacon_video_widget_set_referrer (bvw, g_value_get_string (value));
3109 break;
3110 case PROP_USER_AGENT:
3111 bacon_video_widget_set_user_agent (bvw, g_value_get_string (value));
3112 break;
3113 case PROP_VOLUME:
3114 bacon_video_widget_set_volume (bvw, g_value_get_double (value));
3115 break;
3116 case PROP_DEINTERLACING:
3117 bacon_video_widget_set_deinterlacing (bvw, g_value_get_boolean (value));
3118 break;
3119 case PROP_BRIGHTNESS:
3120 bacon_video_widget_set_video_property (bvw, BVW_VIDEO_BRIGHTNESS, g_value_get_int (value));
3121 break;
3122 case PROP_CONTRAST:
3123 bacon_video_widget_set_video_property (bvw, BVW_VIDEO_CONTRAST, g_value_get_int (value));
3124 break;
3125 case PROP_SATURATION:
3126 bacon_video_widget_set_video_property (bvw, BVW_VIDEO_SATURATION, g_value_get_int (value));
3127 break;
3128 case PROP_HUE:
3129 bacon_video_widget_set_video_property (bvw, BVW_VIDEO_HUE, g_value_get_int (value));
3130 break;
3131 case PROP_AUDIO_OUTPUT_TYPE:
3132 bacon_video_widget_set_audio_output_type (bvw, g_value_get_enum (value));
3133 break;
3134 case PROP_AV_OFFSET:
3135 g_object_set_property (G_OBJECT (bvw->play), "av-offset", value);
3136 break;
3137 default:
3138 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
3139 break;
3140 }
3141 }
3142
3143 static void
bacon_video_widget_get_property(GObject * object,guint property_id,GValue * value,GParamSpec * pspec)3144 bacon_video_widget_get_property (GObject * object, guint property_id,
3145 GValue * value, GParamSpec * pspec)
3146 {
3147 BaconVideoWidget *bvw;
3148
3149 bvw = BACON_VIDEO_WIDGET (object);
3150
3151 switch (property_id) {
3152 case PROP_LOGO_MODE:
3153 g_value_set_boolean (value, bacon_video_widget_get_logo_mode (bvw));
3154 break;
3155 case PROP_POSITION:
3156 g_value_set_double (value, bacon_video_widget_get_position (bvw));
3157 break;
3158 case PROP_STREAM_LENGTH:
3159 g_value_set_int64 (value, bacon_video_widget_get_stream_length (bvw));
3160 break;
3161 case PROP_PLAYING:
3162 g_value_set_boolean (value, bacon_video_widget_is_playing (bvw));
3163 break;
3164 case PROP_REFERRER:
3165 g_value_set_string (value, bvw->referrer);
3166 break;
3167 case PROP_SEEKABLE:
3168 g_value_set_boolean (value, bacon_video_widget_is_seekable (bvw));
3169 break;
3170 case PROP_USER_AGENT:
3171 g_value_set_string (value, bvw->user_agent);
3172 break;
3173 case PROP_VOLUME:
3174 g_value_set_double (value, bvw->volume);
3175 break;
3176 case PROP_DOWNLOAD_FILENAME:
3177 g_value_set_string (value, bvw->download_filename);
3178 break;
3179 case PROP_DEINTERLACING:
3180 g_value_set_boolean (value, bacon_video_widget_get_deinterlacing (bvw));
3181 break;
3182 case PROP_BRIGHTNESS:
3183 g_value_set_int (value, bacon_video_widget_get_video_property (bvw, BVW_VIDEO_BRIGHTNESS));
3184 break;
3185 case PROP_CONTRAST:
3186 g_value_set_int (value, bacon_video_widget_get_video_property (bvw, BVW_VIDEO_CONTRAST));
3187 break;
3188 case PROP_SATURATION:
3189 g_value_set_int (value, bacon_video_widget_get_video_property (bvw, BVW_VIDEO_SATURATION));
3190 break;
3191 case PROP_HUE:
3192 g_value_set_int (value, bacon_video_widget_get_video_property (bvw, BVW_VIDEO_HUE));
3193 break;
3194 case PROP_AUDIO_OUTPUT_TYPE:
3195 g_value_set_enum (value, bacon_video_widget_get_audio_output_type (bvw));
3196 break;
3197 case PROP_AV_OFFSET:
3198 g_object_get_property (G_OBJECT (bvw->play), "av-offset", value);
3199 break;
3200 case PROP_REVEAL_CONTROLS:
3201 g_value_set_boolean (value, bvw->reveal_controls);
3202 break;
3203 default:
3204 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
3205 break;
3206 }
3207 }
3208
3209 /* ============================================================= */
3210 /* */
3211 /* Public Methods */
3212 /* */
3213 /* ============================================================= */
3214
3215 /**
3216 * bacon_video_widget_get_subtitle:
3217 * @bvw: a #BaconVideoWidget
3218 *
3219 * Returns the index of the current subtitles.
3220 *
3221 * If the widget is not playing, <code class="literal">-2</code> will be returned. If no subtitles are
3222 * being used, <code class="literal">-1</code> is returned.
3223 *
3224 * Return value: the subtitle index
3225 **/
3226 int
bacon_video_widget_get_subtitle(BaconVideoWidget * bvw)3227 bacon_video_widget_get_subtitle (BaconVideoWidget * bvw)
3228 {
3229 int subtitle = -1;
3230 gint flags;
3231
3232 g_return_val_if_fail (BACON_IS_VIDEO_WIDGET (bvw), -2);
3233 g_return_val_if_fail (bvw->play != NULL, -2);
3234
3235 g_object_get (bvw->play, "flags", &flags, NULL);
3236
3237 if ((flags & GST_PLAY_FLAG_TEXT) == 0)
3238 return -2;
3239
3240 g_object_get (G_OBJECT (bvw->play), "current-text", &subtitle, NULL);
3241
3242 return subtitle;
3243 }
3244
3245 static gboolean
sublang_is_valid(int sublang,int n_sublang)3246 sublang_is_valid (int sublang,
3247 int n_sublang)
3248 {
3249 if (sublang == -1 ||
3250 sublang == -2)
3251 return TRUE;
3252 if (sublang < 0)
3253 return FALSE;
3254 if (sublang >= n_sublang)
3255 return FALSE;
3256 return TRUE;
3257 }
3258
3259 /**
3260 * bacon_video_widget_set_subtitle:
3261 * @bvw: a #BaconVideoWidget
3262 * @subtitle: a subtitle index
3263 *
3264 * Sets the subtitle index for @bvw. If @subtitle is <code class="literal">-1</code>, no subtitles will
3265 * be used.
3266 **/
3267 void
bacon_video_widget_set_subtitle(BaconVideoWidget * bvw,int subtitle)3268 bacon_video_widget_set_subtitle (BaconVideoWidget * bvw, int subtitle)
3269 {
3270 GstTagList *tags;
3271 gint flags;
3272 gint n_text;
3273
3274 g_return_if_fail (BACON_IS_VIDEO_WIDGET (bvw));
3275 g_return_if_fail (bvw->play != NULL);
3276
3277 g_object_get (bvw->play, "flags", &flags, "n-text", &n_text, NULL);
3278
3279 g_return_if_fail (sublang_is_valid (subtitle, n_text));
3280
3281 if (subtitle == -2) {
3282 flags &= ~GST_PLAY_FLAG_TEXT;
3283 subtitle = -1;
3284 } else {
3285 flags |= GST_PLAY_FLAG_TEXT;
3286 }
3287
3288 g_object_set (bvw->play, "flags", flags, "current-text", subtitle, NULL);
3289
3290 if (flags & GST_PLAY_FLAG_TEXT) {
3291 g_object_get (bvw->play, "current-text", &subtitle, NULL);
3292
3293 g_signal_emit_by_name (G_OBJECT (bvw->play), "get-text-tags", subtitle, &tags);
3294 bvw_update_tags (bvw, tags, "text");
3295 }
3296 }
3297
3298 /**
3299 * bacon_video_widget_set_next_subtitle:
3300 * @bvw: a #BaconVideoWidget
3301 *
3302 * Switch to the next text subtitle index for the current video. See
3303 * bacon_video_widget_set_subtitle().
3304 *
3305 * Since: 3.12
3306 */
3307 void
bacon_video_widget_set_next_subtitle(BaconVideoWidget * bvw)3308 bacon_video_widget_set_next_subtitle (BaconVideoWidget *bvw)
3309 {
3310 int n_text;
3311 int current_text;
3312
3313 g_object_get (bvw->play, "current-text", ¤t_text, "n-text", &n_text, NULL);
3314
3315 current_text++;
3316 if (current_text >= n_text)
3317 current_text = -2;
3318
3319 bacon_video_widget_set_subtitle (bvw, current_text);
3320 }
3321
3322 static gboolean
bvw_chapter_compare_func(GstTocEntry * entry,BaconVideoWidget * bvw)3323 bvw_chapter_compare_func (GstTocEntry *entry,
3324 BaconVideoWidget *bvw)
3325 {
3326 gint64 start, stop;
3327
3328 if (!gst_toc_entry_get_start_stop_times (entry, &start, &stop))
3329 return -1;
3330
3331 if (bvw->current_time >= start / GST_MSECOND &&
3332 bvw->current_time < stop / GST_MSECOND)
3333 return 0;
3334
3335 return -1;
3336 }
3337
3338 static GList *
bvw_get_current_chapter(BaconVideoWidget * bvw)3339 bvw_get_current_chapter (BaconVideoWidget *bvw)
3340 {
3341 return g_list_find_custom (bvw->chapters, bvw, (GCompareFunc) bvw_chapter_compare_func);
3342 }
3343
3344 /**
3345 * bacon_video_widget_has_next_track:
3346 * @bvw: a #BaconVideoWidget
3347 *
3348 * Determines whether there is another track after the current one, typically
3349 * as a chapter on a DVD.
3350 *
3351 * Return value: %TRUE if there is another track, %FALSE otherwise
3352 **/
3353 gboolean
bacon_video_widget_has_next_track(BaconVideoWidget * bvw)3354 bacon_video_widget_has_next_track (BaconVideoWidget *bvw)
3355 {
3356 GList *l;
3357
3358 g_return_val_if_fail (BACON_IS_VIDEO_WIDGET (bvw), FALSE);
3359
3360 if (bvw->mrl == NULL)
3361 return FALSE;
3362
3363 if (g_str_has_prefix (bvw->mrl, "dvd:/"))
3364 return TRUE;
3365
3366 l = bvw_get_current_chapter (bvw);
3367 if (l != NULL && l->next != NULL)
3368 return TRUE;
3369
3370 return FALSE;
3371 }
3372
3373 /**
3374 * bacon_video_widget_has_previous_track:
3375 * @bvw: a #BaconVideoWidget
3376 *
3377 * Determines whether there is another track before the current one, typically
3378 * as a chapter on a DVD.
3379 *
3380 * Return value: %TRUE if there is another track, %FALSE otherwise
3381 **/
3382 gboolean
bacon_video_widget_has_previous_track(BaconVideoWidget * bvw)3383 bacon_video_widget_has_previous_track (BaconVideoWidget *bvw)
3384 {
3385 GstFormat fmt;
3386 gint64 val;
3387 GList *l;
3388
3389 g_return_val_if_fail (BACON_IS_VIDEO_WIDGET (bvw), FALSE);
3390
3391 if (bvw->mrl == NULL)
3392 return FALSE;
3393
3394 if (g_str_has_prefix (bvw->mrl, "dvd:/"))
3395 return TRUE;
3396
3397 /* Look in the chapters first */
3398 l = bvw_get_current_chapter (bvw);
3399 if (l != NULL && l->prev != NULL)
3400 return TRUE;
3401
3402 fmt = gst_format_get_by_nick ("chapter");
3403 /* If chapter isn't registered, then there's no chapters support */
3404 if (fmt == GST_FORMAT_UNDEFINED)
3405 return FALSE;
3406
3407 if (gst_element_query_position (bvw->play, fmt, &val))
3408 return (val > 0);
3409
3410 return FALSE;
3411 }
3412
3413 static GList *
get_lang_list_for_type(BaconVideoWidget * bvw,const gchar * type_name)3414 get_lang_list_for_type (BaconVideoWidget * bvw, const gchar * type_name)
3415 {
3416 GList *ret = NULL;
3417 gint i, n;
3418 const char *prop;
3419 const char *signal;
3420
3421 if (g_str_equal (type_name, "AUDIO")) {
3422 prop = "n-audio";
3423 signal = "get-audio-tags";
3424 } else if (g_str_equal (type_name, "TEXT")) {
3425 prop = "n-text";
3426 signal = "get-text-tags";
3427 } else {
3428 g_critical ("Invalid stream type '%s'", type_name);
3429 return NULL;
3430 }
3431
3432 n = 0;
3433 g_object_get (G_OBJECT (bvw->play), prop, &n, NULL);
3434 if (n == 0)
3435 return NULL;
3436
3437 for (i = 0; i < n; i++) {
3438 GstTagList *tags = NULL;
3439 BvwLangInfo *info;
3440
3441 g_signal_emit_by_name (G_OBJECT (bvw->play), signal, i, &tags);
3442
3443 info = g_new0 (BvwLangInfo, 1);
3444
3445 if (tags) {
3446 gst_tag_list_get_string (tags, GST_TAG_LANGUAGE_CODE, &info->language);
3447 if (g_str_equal (type_name, "AUDIO"))
3448 gst_tag_list_get_string (tags, GST_TAG_AUDIO_CODEC, &info->codec);
3449
3450 gst_tag_list_unref (tags);
3451 }
3452
3453 if (info->language == NULL)
3454 info->language = g_strdup ("und");
3455 ret = g_list_prepend (ret, info);
3456 }
3457
3458 return g_list_reverse (ret);
3459 }
3460
3461 void
bacon_video_widget_lang_info_free(BvwLangInfo * info)3462 bacon_video_widget_lang_info_free (BvwLangInfo *info)
3463 {
3464 if (info == NULL)
3465 return;
3466 g_free (info->language);
3467 g_free (info->codec);
3468 g_free (info);
3469 }
3470
3471 /**
3472 * bacon_video_widget_get_subtitles:
3473 * @bvw: a #BaconVideoWidget
3474 *
3475 * Returns a list of #BvwLangInfo for each subtitle track.
3476 *
3477 * Return value: a #GList of #BvwLangInfo, or %NULL; free each element with bacon_video_widget_lang_info_free() and the list with g_list_free()
3478 **/
3479 GList *
bacon_video_widget_get_subtitles(BaconVideoWidget * bvw)3480 bacon_video_widget_get_subtitles (BaconVideoWidget * bvw)
3481 {
3482 GList *list;
3483
3484 g_return_val_if_fail (BACON_IS_VIDEO_WIDGET (bvw), NULL);
3485 g_return_val_if_fail (bvw->play != NULL, NULL);
3486
3487 list = get_lang_list_for_type (bvw, "TEXT");
3488
3489 return list;
3490 }
3491
3492 /**
3493 * bacon_video_widget_get_languages:
3494 * @bvw: a #BaconVideoWidget
3495 *
3496 * Returns a list of #BvwLangInfo for each audio track.
3497 *
3498 * Return value: a #GList of #BvwLangInfo, or %NULL; free each element with bacon_video_widget_lang_info_free() and the list with g_list_free()
3499 **/
3500 GList *
bacon_video_widget_get_languages(BaconVideoWidget * bvw)3501 bacon_video_widget_get_languages (BaconVideoWidget * bvw)
3502 {
3503 GList *list;
3504
3505 g_return_val_if_fail (BACON_IS_VIDEO_WIDGET (bvw), NULL);
3506 g_return_val_if_fail (bvw->play != NULL, NULL);
3507
3508 list = get_lang_list_for_type (bvw, "AUDIO");
3509
3510 /* When we have only one language, we don't need to show
3511 * any languages, we default to the only track */
3512 if (g_list_length (list) == 1) {
3513 g_list_free_full (list, (GDestroyNotify) bacon_video_widget_lang_info_free);
3514 return NULL;
3515 }
3516
3517 return list;
3518 }
3519
3520 /**
3521 * bacon_video_widget_get_language:
3522 * @bvw: a #BaconVideoWidget
3523 *
3524 * Returns the index of the current audio language.
3525 *
3526 * If the widget is not playing, or the default language is in use, <code class="literal">-1</code> will be returned.
3527 *
3528 * Return value: the audio language index
3529 **/
3530 int
bacon_video_widget_get_language(BaconVideoWidget * bvw)3531 bacon_video_widget_get_language (BaconVideoWidget * bvw)
3532 {
3533 int language = -1;
3534
3535 g_return_val_if_fail (BACON_IS_VIDEO_WIDGET (bvw), -1);
3536 g_return_val_if_fail (bvw->play != NULL, -1);
3537
3538 g_object_get (G_OBJECT (bvw->play), "current-audio", &language, NULL);
3539
3540 return language;
3541 }
3542
3543 /**
3544 * bacon_video_widget_set_language:
3545 * @bvw: a #BaconVideoWidget
3546 * @language: an audio language index
3547 *
3548 * Sets the audio language index for @bvw. If @language is <code class="literal">-1</code>, the default language will
3549 * be used.
3550 **/
3551 void
bacon_video_widget_set_language(BaconVideoWidget * bvw,int language)3552 bacon_video_widget_set_language (BaconVideoWidget * bvw, int language)
3553 {
3554 GstTagList *tags;
3555 int n_lang;
3556
3557 g_return_if_fail (BACON_IS_VIDEO_WIDGET (bvw));
3558 g_return_if_fail (bvw->play != NULL);
3559
3560 g_object_get (bvw->play, "n-audio", &n_lang, NULL);
3561
3562 g_return_if_fail (sublang_is_valid (language, n_lang));
3563
3564 if (language == -1)
3565 language = 0;
3566 else if (language == -2)
3567 language = -1;
3568
3569 GST_DEBUG ("setting language to %d", language);
3570
3571 g_object_set (bvw->play, "current-audio", language, NULL);
3572
3573 g_object_get (bvw->play, "current-audio", &language, NULL);
3574 GST_DEBUG ("current-audio now: %d", language);
3575
3576 g_signal_emit_by_name (G_OBJECT (bvw->play), "get-audio-tags", language, &tags);
3577 bvw_update_tags (bvw, tags, "audio");
3578
3579 /* so it updates its metadata for the newly-selected stream */
3580 g_signal_emit (bvw, bvw_signals[SIGNAL_GOT_METADATA], 0, NULL);
3581 g_signal_emit (bvw, bvw_signals[SIGNAL_CHANNELS_CHANGE], 0);
3582 }
3583
3584 /**
3585 * bacon_video_widget_set_next_language:
3586 * @bvw: a #BaconVideoWidget
3587 *
3588 * Switch to the next audio language index for the current video. See
3589 * bacon_video_widget_set_language().
3590 *
3591 * Since: 3.12
3592 */
3593 void
bacon_video_widget_set_next_language(BaconVideoWidget * bvw)3594 bacon_video_widget_set_next_language (BaconVideoWidget *bvw)
3595 {
3596 int n_audio;
3597 int current_audio;
3598
3599 g_object_get (bvw->play, "current-audio", ¤t_audio, "n-audio", &n_audio, NULL);
3600
3601 current_audio++;
3602 if (current_audio >= n_audio)
3603 current_audio = -2;
3604
3605 bacon_video_widget_set_language (bvw, current_audio);
3606 }
3607
3608 /**
3609 * bacon_video_widget_set_deinterlacing:
3610 * @bvw: a #BaconVideoWidget
3611 * @deinterlace: %TRUE if videos should be automatically deinterlaced, %FALSE otherwise
3612 *
3613 * Sets whether the widget should deinterlace videos.
3614 **/
3615 void
bacon_video_widget_set_deinterlacing(BaconVideoWidget * bvw,gboolean deinterlace)3616 bacon_video_widget_set_deinterlacing (BaconVideoWidget * bvw,
3617 gboolean deinterlace)
3618 {
3619 gint flags;
3620
3621 g_return_if_fail (BACON_IS_VIDEO_WIDGET (bvw));
3622 g_return_if_fail (GST_IS_ELEMENT (bvw->play));
3623
3624 g_object_get (bvw->play, "flags", &flags, NULL);
3625 if (deinterlace)
3626 flags |= GST_PLAY_FLAG_DEINTERLACE;
3627 else
3628 flags &= ~GST_PLAY_FLAG_DEINTERLACE;
3629 g_object_set (bvw->play, "flags", flags, NULL);
3630
3631 g_object_notify (G_OBJECT (bvw), "deinterlacing");
3632 }
3633
3634 /**
3635 * bacon_video_widget_get_deinterlacing:
3636 * @bvw: a #BaconVideoWidget
3637 *
3638 * Returns whether deinterlacing of videos is enabled for this widget.
3639 *
3640 * Return value: %TRUE if automatic deinterlacing is enabled, %FALSE otherwise
3641 **/
3642 gboolean
bacon_video_widget_get_deinterlacing(BaconVideoWidget * bvw)3643 bacon_video_widget_get_deinterlacing (BaconVideoWidget * bvw)
3644 {
3645 gint flags;
3646
3647 g_return_val_if_fail (BACON_IS_VIDEO_WIDGET (bvw), FALSE);
3648 g_return_val_if_fail (GST_IS_ELEMENT (bvw->play), FALSE);
3649
3650 g_object_get (bvw->play, "flags", &flags, NULL);
3651
3652 return !!(flags & GST_PLAY_FLAG_DEINTERLACE);
3653 }
3654
3655 static gint
get_num_audio_channels(BaconVideoWidget * bvw)3656 get_num_audio_channels (BaconVideoWidget * bvw)
3657 {
3658 gint channels;
3659
3660 switch (bvw->speakersetup) {
3661 case BVW_AUDIO_SOUND_STEREO:
3662 channels = 2;
3663 break;
3664 case BVW_AUDIO_SOUND_4CHANNEL:
3665 channels = 4;
3666 break;
3667 case BVW_AUDIO_SOUND_5CHANNEL:
3668 channels = 5;
3669 break;
3670 case BVW_AUDIO_SOUND_41CHANNEL:
3671 /* so alsa has this as 5.1, but empty center speaker. We don't really
3672 * do that yet. ;-). So we'll take the placebo approach. */
3673 case BVW_AUDIO_SOUND_51CHANNEL:
3674 channels = 6;
3675 break;
3676 case BVW_AUDIO_SOUND_AC3PASSTHRU:
3677 default:
3678 g_return_val_if_reached (-1);
3679 }
3680
3681 return channels;
3682 }
3683
3684 static GstCaps *
fixate_to_num(const GstCaps * in_caps,gint channels)3685 fixate_to_num (const GstCaps * in_caps, gint channels)
3686 {
3687 gint n, count;
3688 GstStructure *s;
3689 const GValue *v;
3690 GstCaps *out_caps;
3691
3692 out_caps = gst_caps_copy (in_caps);
3693
3694 count = gst_caps_get_size (out_caps);
3695 for (n = 0; n < count; n++) {
3696 s = gst_caps_get_structure (out_caps, n);
3697 v = gst_structure_get_value (s, "channels");
3698 if (!v)
3699 continue;
3700
3701 /* get channel count (or list of ~) */
3702 gst_structure_fixate_field_nearest_int (s, "channels", channels);
3703 }
3704
3705 return out_caps;
3706 }
3707
3708 static void
set_audio_filter(BaconVideoWidget * bvw)3709 set_audio_filter (BaconVideoWidget *bvw)
3710 {
3711 gint channels;
3712 GstCaps *caps, *res;
3713 GstPad *pad, *peer_pad;
3714
3715 /* reset old */
3716 g_object_set (bvw->audio_capsfilter, "caps", NULL, NULL);
3717
3718 /* construct possible caps to filter down to our chosen caps */
3719 /* Start with what the audio sink supports, but limit the allowed
3720 * channel count to our speaker output configuration */
3721 pad = gst_element_get_static_pad (bvw->audio_capsfilter, "src");
3722 peer_pad = gst_pad_get_peer (pad);
3723 gst_object_unref (pad);
3724
3725 caps = gst_pad_get_current_caps (peer_pad);
3726 gst_object_unref (peer_pad);
3727
3728 if ((channels = get_num_audio_channels (bvw)) == -1)
3729 return;
3730
3731 res = fixate_to_num (caps, channels);
3732 gst_caps_unref (caps);
3733
3734 /* set */
3735 if (res && gst_caps_is_empty (res)) {
3736 gst_caps_unref (res);
3737 res = NULL;
3738 }
3739 g_object_set (bvw->audio_capsfilter, "caps", res, NULL);
3740
3741 if (res) {
3742 gst_caps_unref (res);
3743 }
3744
3745 /* reset */
3746 pad = gst_element_get_static_pad (bvw->audio_capsfilter, "src");
3747 gst_pad_set_caps (pad, NULL);
3748 gst_object_unref (pad);
3749 }
3750
3751 /**
3752 * bacon_video_widget_get_audio_output_type:
3753 * @bvw: a #BaconVideoWidget
3754 *
3755 * Returns the current audio output type (e.g. how many speaker channels)
3756 * from #BvwAudioOutputType.
3757 *
3758 * Return value: the audio output type, or <code class="literal">-1</code>
3759 **/
3760 BvwAudioOutputType
bacon_video_widget_get_audio_output_type(BaconVideoWidget * bvw)3761 bacon_video_widget_get_audio_output_type (BaconVideoWidget *bvw)
3762 {
3763 g_return_val_if_fail (BACON_IS_VIDEO_WIDGET (bvw), -1);
3764
3765 return bvw->speakersetup;
3766 }
3767
3768 /**
3769 * bacon_video_widget_set_audio_output_type:
3770 * @bvw: a #BaconVideoWidget
3771 * @type: the new audio output type
3772 *
3773 * Sets the audio output type (number of speaker channels) in the video widget.
3774 **/
3775 void
bacon_video_widget_set_audio_output_type(BaconVideoWidget * bvw,BvwAudioOutputType type)3776 bacon_video_widget_set_audio_output_type (BaconVideoWidget *bvw,
3777 BvwAudioOutputType type)
3778 {
3779 g_return_if_fail (BACON_IS_VIDEO_WIDGET (bvw));
3780
3781 if (type == bvw->speakersetup)
3782 return;
3783 else if (type == BVW_AUDIO_SOUND_AC3PASSTHRU)
3784 return;
3785
3786 bvw->speakersetup = type;
3787 g_object_notify (G_OBJECT (bvw), "audio-output-type");
3788
3789 set_audio_filter (bvw);
3790 }
3791
3792 /**
3793 * bacon_video_widget_show_popup:
3794 * @bvw: a #BaconVideoWidget
3795 *
3796 * Show the video controls popup, and schedule for it to be hidden again after
3797 * a timeout.
3798 *
3799 * Since: 3.12
3800 */
3801 void
bacon_video_widget_show_popup(BaconVideoWidget * bvw)3802 bacon_video_widget_show_popup (BaconVideoWidget *bvw)
3803 {
3804 g_return_if_fail (BACON_IS_VIDEO_WIDGET (bvw));
3805
3806 set_controls_visibility (bvw, TRUE, FALSE);
3807 schedule_hiding_popup (bvw);
3808 }
3809
3810 /**
3811 * bacon_video_widget_mark_popup_busy:
3812 * @bvw: a #BaconVideoWidget
3813 * @reason: human-readable reason for the controls popup being marked as busy
3814 *
3815 * Mark the video controls popup as busy, for the given @reason. Use
3816 * bacon_video_widget_unmark_popup_busy() to undo.
3817 *
3818 * Since: 3.12
3819 */
3820 void
bacon_video_widget_mark_popup_busy(BaconVideoWidget * bvw,const char * reason)3821 bacon_video_widget_mark_popup_busy (BaconVideoWidget *bvw,
3822 const char *reason)
3823 {
3824 g_return_if_fail (BACON_IS_VIDEO_WIDGET (bvw));
3825
3826 g_hash_table_insert (bvw->busy_popup_ht,
3827 g_strdup (reason),
3828 GINT_TO_POINTER (1));
3829
3830 set_controls_visibility (bvw, TRUE, FALSE);
3831
3832 GST_DEBUG ("Adding popup busy for reason %s", reason);
3833
3834 unschedule_hiding_popup (bvw);
3835 }
3836
3837 /**
3838 * bacon_video_widget_unmark_popup_busy:
3839 * @bvw: a #BaconVideoWidget
3840 * @reason: human-readable reason for the controls popup being unmarked as busy
3841 *
3842 * Unmark the video controls popup as busy, for the given @reason. The popup
3843 * must previously have been marked as busy using
3844 * bacon_video_widget_mark_popup_busy().
3845 *
3846 * Since: 3.12
3847 */
3848 void
bacon_video_widget_unmark_popup_busy(BaconVideoWidget * bvw,const char * reason)3849 bacon_video_widget_unmark_popup_busy (BaconVideoWidget *bvw,
3850 const char *reason)
3851 {
3852 g_return_if_fail (BACON_IS_VIDEO_WIDGET (bvw));
3853
3854 g_hash_table_remove (bvw->busy_popup_ht, reason);
3855
3856 GST_DEBUG ("Removing popup busy for reason %s", reason);
3857
3858 if (g_hash_table_size (bvw->busy_popup_ht) == 0 &&
3859 clutter_actor_get_opacity (bvw->controls) != 0) {
3860 GST_DEBUG ("Will hide popup soon");
3861 schedule_hiding_popup (bvw);
3862 }
3863 }
3864
3865 /**
3866 * bacon_video_widget_get_controls_object:
3867 * @bvw: a #BaconVideoWidget
3868 *
3869 * Get the widget which displays the video controls.
3870 *
3871 * Returns: (transfer none): controls widget
3872 * Since: 3.12
3873 */
3874 GObject *
bacon_video_widget_get_controls_object(BaconVideoWidget * bvw)3875 bacon_video_widget_get_controls_object (BaconVideoWidget *bvw)
3876 {
3877 g_return_val_if_fail (BACON_IS_VIDEO_WIDGET (bvw), NULL);
3878
3879 return G_OBJECT (bvw->controls);
3880 }
3881
3882 /**
3883 * bacon_video_widget_get_header_controls_object:
3884 * @bvw: a #BaconVideoWidget
3885 *
3886 * Get the widget which displays the video header controls.
3887 *
3888 * Returns: (transfer none): header controls widget
3889 * Since: 3.20
3890 */
3891 GObject *
bacon_video_widget_get_header_controls_object(BaconVideoWidget * bvw)3892 bacon_video_widget_get_header_controls_object (BaconVideoWidget *bvw)
3893 {
3894 g_return_val_if_fail (BACON_IS_VIDEO_WIDGET (bvw), NULL);
3895
3896 return G_OBJECT (gtk_clutter_actor_get_widget (GTK_CLUTTER_ACTOR (bvw->header_controls)));
3897 }
3898
3899 /* =========================================== */
3900 /* */
3901 /* Play/Pause, Stop */
3902 /* */
3903 /* =========================================== */
3904
3905 static GError*
bvw_error_from_gst_error(BaconVideoWidget * bvw,GstMessage * err_msg)3906 bvw_error_from_gst_error (BaconVideoWidget *bvw, GstMessage * err_msg)
3907 {
3908 const gchar *src_typename;
3909 GError *ret = NULL;
3910 GError *e = NULL;
3911 char *dbg = NULL;
3912 int http_error_code;
3913
3914 GST_LOG ("resolving %" GST_PTR_FORMAT, err_msg);
3915
3916 src_typename = (err_msg->src) ? G_OBJECT_TYPE_NAME (err_msg->src) : NULL;
3917
3918 gst_message_parse_error (err_msg, &e, &dbg);
3919
3920 /* FIXME:
3921 * Unemitted errors:
3922 * BVW_ERROR_BROKEN_FILE
3923 */
3924
3925 /* Can't open optical disc? */
3926 if (is_error (e, RESOURCE, NOT_FOUND) ||
3927 is_error (e, RESOURCE, OPEN_READ)) {
3928 if (g_str_has_prefix (bvw->mrl, "dvd:")) {
3929 ret = g_error_new_literal (BVW_ERROR, BVW_ERROR_INVALID_DEVICE,
3930 "The DVD device you specified seems to be invalid.");
3931 goto done;
3932 } else if (g_str_has_prefix (bvw->mrl, "vcd:")) {
3933 ret = g_error_new_literal (BVW_ERROR, BVW_ERROR_INVALID_DEVICE,
3934 "The VCD device you specified seems to be invalid.");
3935 goto done;
3936 }
3937 }
3938
3939 /* Check for encrypted DVD */
3940 if (is_error (e, RESOURCE, READ) &&
3941 g_str_has_prefix (bvw->mrl, "dvd:")) {
3942 GModule *module;
3943 gpointer sym;
3944
3945 module = g_module_open ("libdvdcss", 0);
3946 if (module == NULL ||
3947 g_module_symbol (module, "dvdcss_open", &sym)) {
3948 g_clear_pointer (&module, g_module_close);
3949 ret = g_error_new_literal (BVW_ERROR, BVW_ERROR_DVD_ENCRYPTED,
3950 _("The source seems encrypted and can’t be read. Are you trying to play an encrypted DVD without libdvdcss?"));
3951 goto done;
3952 }
3953 g_clear_pointer (&module, g_module_close);
3954 }
3955
3956 /* HTTP error codes */
3957 /* FIXME: bvw_get_http_error_code() calls gst_message_parse_error too */
3958 http_error_code = bvw_get_http_error_code (err_msg);
3959
3960 if (is_error (e, RESOURCE, NOT_FOUND) ||
3961 http_error_code == 404) {
3962 if (strstr (e->message, "Cannot resolve hostname") != NULL) {
3963 ret = g_error_new_literal (BVW_ERROR, BVW_ERROR_UNKNOWN_HOST,
3964 _("The server you are trying to connect to is not known."));
3965 } else if (strstr (e->message, "Cannot connect to destination") != NULL) {
3966 ret = g_error_new_literal (BVW_ERROR, BVW_ERROR_CONNECTION_REFUSED,
3967 _("The connection to this server was refused."));
3968 } else {
3969 ret = g_error_new_literal (BVW_ERROR, BVW_ERROR_FILE_NOT_FOUND,
3970 _("The specified movie could not be found."));
3971 }
3972 goto done;
3973 }
3974
3975 if (http_error_code == 403) {
3976 ret = g_error_new_literal (BVW_ERROR, BVW_ERROR_FILE_PERMISSION,
3977 _("The server refused access to this file or stream."));
3978 goto done;
3979 }
3980
3981 if (http_error_code == 401) {
3982 ret = g_error_new_literal (BVW_ERROR, BVW_ERROR_FILE_PERMISSION,
3983 _("Authentication is required to access this file or stream."));
3984 goto done;
3985 }
3986
3987 if (http_error_code == 495) {
3988 ret = g_error_new_literal (BVW_ERROR, BVW_ERROR_READ_ERROR,
3989 _("SSL/TLS support is missing. Check your installation."));
3990 goto done;
3991 }
3992
3993 if (is_error (e, RESOURCE, OPEN_READ)) {
3994 if (strstr (dbg, g_strerror (EACCES)) != NULL) {
3995 ret = g_error_new_literal (BVW_ERROR, BVW_ERROR_FILE_PERMISSION,
3996 _("You are not allowed to open this file."));
3997 goto done;
3998 }
3999 if (strstr (dbg, "Error parsing URL.") != NULL) {
4000 ret = g_error_new_literal (BVW_ERROR, BVW_ERROR_INVALID_LOCATION,
4001 _("This location is not a valid one."));
4002 goto done;
4003 }
4004 }
4005
4006 if (is_error (e, RESOURCE, OPEN_READ) ||
4007 is_error (e, RESOURCE, READ)) {
4008 ret = g_error_new_literal (BVW_ERROR, BVW_ERROR_READ_ERROR,
4009 _("The movie could not be read."));
4010 goto done;
4011 }
4012
4013 if (is_error (e, STREAM, DECRYPT)) {
4014 ret = g_error_new_literal (BVW_ERROR, BVW_ERROR_FILE_ENCRYPTED,
4015 _("This file is encrypted and cannot be played back."));
4016 goto done;
4017 }
4018
4019 if (is_error (e, STREAM, TYPE_NOT_FOUND)) {
4020 ret = g_error_new_literal (BVW_ERROR, BVW_ERROR_EMPTY_FILE,
4021 _("The file you tried to play is an empty file."));
4022 goto done;
4023 }
4024
4025 if (e->domain == GST_RESOURCE_ERROR) {
4026 ret = g_error_new_literal (BVW_ERROR, BVW_ERROR_FILE_GENERIC,
4027 e->message);
4028 goto done;
4029 }
4030
4031 if (is_error (e, CORE, MISSING_PLUGIN) ||
4032 is_error (e, STREAM, CODEC_NOT_FOUND) ||
4033 is_error (e, STREAM, WRONG_TYPE) ||
4034 is_error (e, STREAM, NOT_IMPLEMENTED) ||
4035 (is_error (e, STREAM, FORMAT) && strstr (dbg, "no video pad or visualizations"))) {
4036 if (bvw->missing_plugins != NULL) {
4037 gchar **descs, *msg = NULL;
4038 guint num;
4039
4040 descs = bvw_get_missing_plugins_descriptions (bvw->missing_plugins);
4041 num = g_list_length (bvw->missing_plugins);
4042
4043 if (is_error (e, CORE, MISSING_PLUGIN)) {
4044 /* should be exactly one missing thing (source or converter) */
4045 msg = g_strdup_printf (_("The playback of this movie requires a %s "
4046 "plugin which is not installed."), descs[0]);
4047 ret = g_error_new_literal (BVW_ERROR, BVW_ERROR_NO_PLUGIN_FOR_FILE, msg);
4048 g_free (msg);
4049 } else {
4050 gchar *desc_list;
4051
4052 desc_list = g_strjoinv ("\n", descs);
4053 msg = g_strdup_printf (g_dngettext (GETTEXT_PACKAGE, "The playback of this movie "
4054 "requires a %s plugin which is not installed.", "The playback "
4055 "of this movie requires the following plugins which are not "
4056 "installed:\n\n%s", num), (num == 1) ? descs[0] : desc_list);
4057 g_free (desc_list);
4058 ret = g_error_new_literal (BVW_ERROR, BVW_ERROR_CODEC_NOT_HANDLED, msg);
4059 g_free (msg);
4060 }
4061 g_strfreev (descs);
4062 } else {
4063 if (g_str_has_prefix (bvw->mrl, "rtsp:")) {
4064 ret = g_error_new_literal (BVW_ERROR, BVW_ERROR_NETWORK_UNREACHABLE,
4065 _("This stream cannot be played. It’s possible that a firewall is blocking it."));
4066 } else {
4067 ret = g_error_new_literal (BVW_ERROR, BVW_ERROR_CODEC_NOT_HANDLED,
4068 _("An audio or video stream is not handled due to missing codecs. "
4069 "You might need to install additional plugins to be able to play some types of movies"));
4070 }
4071 }
4072 goto done;
4073 }
4074
4075 if (is_error (e, STREAM, FAILED) &&
4076 src_typename &&
4077 strncmp (src_typename, "GstTypeFind", 11) == 0) {
4078 ret = g_error_new_literal (BVW_ERROR, BVW_ERROR_READ_ERROR,
4079 _("This file cannot be played over the network. Try downloading it locally first."));
4080 goto done;
4081 }
4082
4083 /* generic error, no code; take message */
4084 ret = g_error_new_literal (BVW_ERROR, BVW_ERROR_GENERIC,
4085 e->message);
4086
4087 done:
4088 g_error_free (e);
4089 g_free (dbg);
4090 bvw_clear_missing_plugins_messages (bvw);
4091
4092 return ret;
4093 }
4094
4095 static char *
get_target_uri(GFile * file)4096 get_target_uri (GFile *file)
4097 {
4098 GFileInfo *info;
4099 char *target;
4100
4101 info = g_file_query_info (file, G_FILE_ATTRIBUTE_STANDARD_TARGET_URI, G_FILE_QUERY_INFO_NONE, NULL, NULL);
4102 if (info == NULL)
4103 return NULL;
4104 target = g_strdup (g_file_info_get_attribute_string (info, G_FILE_ATTRIBUTE_STANDARD_TARGET_URI));
4105 g_object_unref (info);
4106
4107 return target;
4108 }
4109
4110 /**
4111 * bacon_video_widget_open:
4112 * @bvw: a #BaconVideoWidget
4113 * @mrl: an MRL
4114 *
4115 * Opens the given @mrl in @bvw for playing.
4116 *
4117 * The MRL is loaded and waiting to be played with bacon_video_widget_play().
4118 **/
4119 void
bacon_video_widget_open(BaconVideoWidget * bvw,const char * mrl)4120 bacon_video_widget_open (BaconVideoWidget *bvw,
4121 const char *mrl)
4122 {
4123 GFile *file;
4124
4125 g_return_if_fail (mrl != NULL);
4126 g_return_if_fail (BACON_IS_VIDEO_WIDGET (bvw));
4127 g_return_if_fail (bvw->play != NULL);
4128
4129 /* So we aren't closed yet... */
4130 if (bvw->mrl) {
4131 bacon_video_widget_close (bvw);
4132 }
4133
4134 GST_DEBUG ("mrl = %s", GST_STR_NULL (mrl));
4135
4136 /* this allows non-URI type of files in the thumbnailer and so on */
4137 file = g_file_new_for_commandline_arg (mrl);
4138
4139 if (g_file_has_uri_scheme (file, "trash") != FALSE ||
4140 g_file_has_uri_scheme (file, "recent") != FALSE) {
4141 bvw->mrl = get_target_uri (file);
4142 GST_DEBUG ("Found target location '%s' for original MRL '%s'",
4143 GST_STR_NULL (bvw->mrl), mrl);
4144 } else if (g_file_has_uri_scheme (file, "cdda") != FALSE) {
4145 char *path;
4146 path = g_file_get_path (file);
4147 bvw->mrl = g_filename_to_uri (path, NULL, NULL);
4148 g_free (path);
4149 } else {
4150 bvw->mrl = g_strdup (mrl);
4151 }
4152
4153 g_object_unref (file);
4154
4155 bvw->got_redirect = FALSE;
4156 bvw->media_has_video = FALSE;
4157 bvw->media_has_audio = FALSE;
4158
4159 /* Flush the bus to make sure we don't get any messages
4160 * from the previous URI, see bug #607224.
4161 */
4162 gst_bus_set_flushing (bvw->bus, TRUE);
4163 bvw->target_state = GST_STATE_READY;
4164 gst_element_set_state (bvw->play, GST_STATE_READY);
4165 gst_bus_set_flushing (bvw->bus, FALSE);
4166
4167 g_object_set (bvw->play, "uri", bvw->mrl, NULL);
4168
4169 bvw->seekable = -1;
4170 bvw->target_state = GST_STATE_PAUSED;
4171 bvw_clear_missing_plugins_messages (bvw);
4172
4173 bacon_video_widget_mark_popup_busy (bvw, "opening file");
4174
4175 gst_element_set_state (bvw->play, GST_STATE_PAUSED);
4176
4177 g_signal_emit (bvw, bvw_signals[SIGNAL_CHANNELS_CHANGE], 0);
4178 }
4179
4180 /**
4181 * bacon_video_widget_play:
4182 * @bvw: a #BaconVideoWidget
4183 * @error: a #GError, or %NULL
4184 *
4185 * Plays the currently-loaded video in @bvw.
4186 *
4187 * Errors from the GStreamer backend will be returned asynchronously via the
4188 * #BaconVideoWidget::error signal, even if this function returns %TRUE.
4189 *
4190 * Return value: %TRUE on success, %FALSE otherwise
4191 **/
4192 gboolean
bacon_video_widget_play(BaconVideoWidget * bvw,GError ** error)4193 bacon_video_widget_play (BaconVideoWidget * bvw, GError ** error)
4194 {
4195 GstState cur_state;
4196
4197 g_return_val_if_fail (BACON_IS_VIDEO_WIDGET (bvw), FALSE);
4198 g_return_val_if_fail (GST_IS_ELEMENT (bvw->play), FALSE);
4199 g_return_val_if_fail (bvw->mrl != NULL, FALSE);
4200
4201 bvw->target_state = GST_STATE_PLAYING;
4202
4203 /* Don't try to play if we're already doing that */
4204 gst_element_get_state (bvw->play, &cur_state, NULL, 0);
4205 if (cur_state == GST_STATE_PLAYING)
4206 return TRUE;
4207
4208 /* Lie when trying to play a file whilst we're download buffering */
4209 if (bvw->download_buffering != FALSE &&
4210 bvw_download_buffering_done (bvw) == FALSE) {
4211 GST_DEBUG ("download buffering in progress, not playing");
4212 return TRUE;
4213 }
4214
4215 /* Or when we're buffering */
4216 if (bvw->buffering != FALSE) {
4217 GST_DEBUG ("buffering in progress, not playing");
4218 return TRUE;
4219 }
4220
4221 /* just lie and do nothing in this case */
4222 if (bvw->plugin_install_in_progress && cur_state != GST_STATE_PAUSED) {
4223 GST_DEBUG ("plugin install in progress and nothing to play, not playing");
4224 return TRUE;
4225 } else if (bvw->mount_in_progress) {
4226 GST_DEBUG ("Mounting in progress, not playing");
4227 return TRUE;
4228 } else if (bvw->auth_dialog != NULL) {
4229 GST_DEBUG ("Authentication in progress, not playing");
4230 return TRUE;
4231 }
4232
4233 /* Set direction to forward */
4234 if (bvw_set_playback_direction (bvw, TRUE) == FALSE) {
4235 GST_DEBUG ("Failed to reset direction back to forward to play");
4236 return FALSE;
4237 }
4238
4239 bacon_video_widget_unmark_popup_busy (bvw, "opening file");
4240
4241 GST_DEBUG ("play");
4242 gst_element_set_state (bvw->play, GST_STATE_PLAYING);
4243
4244 /* will handle all errors asynchroneously */
4245 return TRUE;
4246 }
4247
4248 /**
4249 * bacon_video_widget_can_direct_seek:
4250 * @bvw: a #BaconVideoWidget
4251 *
4252 * Determines whether direct seeking is possible for the current stream.
4253 *
4254 * Return value: %TRUE if direct seeking is possible, %FALSE otherwise
4255 **/
4256 gboolean
bacon_video_widget_can_direct_seek(BaconVideoWidget * bvw)4257 bacon_video_widget_can_direct_seek (BaconVideoWidget *bvw)
4258 {
4259 g_return_val_if_fail (BACON_IS_VIDEO_WIDGET (bvw), FALSE);
4260 g_return_val_if_fail (GST_IS_ELEMENT (bvw->play), FALSE);
4261
4262 if (bvw->mrl == NULL)
4263 return FALSE;
4264
4265 if (bvw->download_buffering != FALSE)
4266 return TRUE;
4267
4268 /* (instant seeking only make sense with video,
4269 * hence no cdda:// here) */
4270 if (g_str_has_prefix (bvw->mrl, "file://") ||
4271 g_str_has_prefix (bvw->mrl, "dvd:/") ||
4272 g_str_has_prefix (bvw->mrl, "vcd:/") ||
4273 g_str_has_prefix (bvw->mrl, "trash:/"))
4274 return TRUE;
4275
4276 return FALSE;
4277 }
4278
4279 static gboolean
bacon_video_widget_seek_time_no_lock(BaconVideoWidget * bvw,gint64 _time,GstSeekFlags flag,GError ** error)4280 bacon_video_widget_seek_time_no_lock (BaconVideoWidget *bvw,
4281 gint64 _time,
4282 GstSeekFlags flag,
4283 GError **error)
4284 {
4285 if (bvw_set_playback_direction (bvw, TRUE) == FALSE)
4286 return FALSE;
4287
4288 bvw->seek_time = -1;
4289
4290 gst_element_set_state (bvw->play, GST_STATE_PAUSED);
4291
4292 gst_element_seek (bvw->play, bvw->rate,
4293 GST_FORMAT_TIME, GST_SEEK_FLAG_FLUSH | flag,
4294 GST_SEEK_TYPE_SET, _time * GST_MSECOND,
4295 GST_SEEK_TYPE_NONE, GST_CLOCK_TIME_NONE);
4296
4297 return TRUE;
4298 }
4299
4300 /**
4301 * bacon_video_widget_seek_time:
4302 * @bvw: a #BaconVideoWidget
4303 * @_time: the time to which to seek, in milliseconds
4304 * @accurate: whether to use accurate seek, an accurate seek might be slower for some formats (see GStreamer docs)
4305 * @error: a #GError, or %NULL
4306 *
4307 * Seeks the currently-playing stream to the absolute position @time, in milliseconds.
4308 *
4309 * Return value: %TRUE on success, %FALSE otherwise
4310 **/
4311 gboolean
bacon_video_widget_seek_time(BaconVideoWidget * bvw,gint64 _time,gboolean accurate,GError ** error)4312 bacon_video_widget_seek_time (BaconVideoWidget *bvw, gint64 _time, gboolean accurate, GError **error)
4313 {
4314 GstClockTime cur_time;
4315 GstSeekFlags flag;
4316
4317 g_return_val_if_fail (BACON_IS_VIDEO_WIDGET (bvw), FALSE);
4318 g_return_val_if_fail (GST_IS_ELEMENT (bvw->play), FALSE);
4319
4320 GST_LOG ("Seeking to %" GST_TIME_FORMAT, GST_TIME_ARGS (_time * GST_MSECOND));
4321
4322 /* Don't say we'll seek past the end */
4323 _time = MIN (_time, bvw->stream_length);
4324
4325 /* Emit a time tick of where we are going, we are paused */
4326 got_time_tick (bvw->play, _time * GST_MSECOND, bvw);
4327
4328 /* Is there a pending seek? */
4329 g_mutex_lock (&bvw->seek_mutex);
4330
4331 /* If there's no pending seek, or
4332 * it's been too long since the seek,
4333 * or we don't have an accurate seek requested */
4334 cur_time = gst_clock_get_internal_time (bvw->clock);
4335 if (bvw->seek_req_time == GST_CLOCK_TIME_NONE ||
4336 cur_time > bvw->seek_req_time + SEEK_TIMEOUT ||
4337 accurate) {
4338 bvw->seek_time = -1;
4339 bvw->seek_req_time = cur_time;
4340 g_mutex_unlock (&bvw->seek_mutex);
4341 } else {
4342 GST_LOG ("Not long enough since last seek, queuing it");
4343 bvw->seek_time = _time;
4344 g_mutex_unlock (&bvw->seek_mutex);
4345 return TRUE;
4346 }
4347
4348 flag = (accurate ? GST_SEEK_FLAG_ACCURATE : GST_SEEK_FLAG_NONE);
4349 bacon_video_widget_seek_time_no_lock (bvw, _time, flag, error);
4350
4351 return TRUE;
4352 }
4353
4354 /**
4355 * bacon_video_widget_seek:
4356 * @bvw: a #BaconVideoWidget
4357 * @position: the percentage of the way through the stream to which to seek
4358 * @error: a #GError, or %NULL
4359 *
4360 * Seeks the currently-playing stream to @position as a percentage of the total
4361 * stream length.
4362 *
4363 * Return value: %TRUE on success, %FALSE otherwise
4364 **/
4365 gboolean
bacon_video_widget_seek(BaconVideoWidget * bvw,double position,GError ** error)4366 bacon_video_widget_seek (BaconVideoWidget *bvw, double position, GError **error)
4367 {
4368 gint64 seek_time, length_nanos;
4369
4370 g_return_val_if_fail (BACON_IS_VIDEO_WIDGET (bvw), FALSE);
4371 g_return_val_if_fail (GST_IS_ELEMENT (bvw->play), FALSE);
4372
4373 length_nanos = (gint64) (bvw->stream_length * GST_MSECOND);
4374 seek_time = (gint64) (length_nanos * position);
4375
4376 GST_LOG ("Seeking to %3.2f%% %" GST_TIME_FORMAT, position,
4377 GST_TIME_ARGS (seek_time));
4378
4379 return bacon_video_widget_seek_time (bvw, seek_time / GST_MSECOND, FALSE, error);
4380 }
4381
4382 /**
4383 * bacon_video_widget_step:
4384 * @bvw: a #BaconVideoWidget
4385 * @forward: the direction of the frame step
4386 * @error: a #GError, or %NULL
4387 *
4388 * Step one frame forward, if @forward is %TRUE, or backwards, if @forward is %FALSE
4389 *
4390 * Return value: %TRUE on success, %FALSE otherwise
4391 **/
4392 gboolean
bacon_video_widget_step(BaconVideoWidget * bvw,gboolean forward,GError ** error)4393 bacon_video_widget_step (BaconVideoWidget *bvw, gboolean forward, GError **error)
4394 {
4395 GstEvent *event;
4396 gboolean retval;
4397
4398 if (bvw_set_playback_direction (bvw, forward) == FALSE)
4399 return FALSE;
4400
4401 event = gst_event_new_step (GST_FORMAT_BUFFERS, 1, 1.0, TRUE, FALSE);
4402
4403 retval = gst_element_send_event (bvw->play, event);
4404
4405 if (retval != FALSE)
4406 bvw_query_timeout (bvw);
4407 else
4408 GST_WARNING ("Failed to step %s", DIRECTION_STR);
4409
4410 return retval;
4411 }
4412
4413 static void
bvw_stop_play_pipeline(BaconVideoWidget * bvw)4414 bvw_stop_play_pipeline (BaconVideoWidget * bvw)
4415 {
4416 GstState cur_state;
4417
4418 gst_element_get_state (bvw->play, &cur_state, NULL, 0);
4419 if (cur_state > GST_STATE_READY) {
4420 GstMessage *msg;
4421
4422 GST_DEBUG ("stopping");
4423 gst_element_set_state (bvw->play, GST_STATE_READY);
4424
4425 /* process all remaining state-change messages so everything gets
4426 * cleaned up properly (before the state change to NULL flushes them) */
4427 GST_DEBUG ("processing pending state-change messages");
4428 while ((msg = gst_bus_pop_filtered (bvw->bus, GST_MESSAGE_STATE_CHANGED))) {
4429 gst_bus_async_signal_func (bvw->bus, msg, NULL);
4430 gst_message_unref (msg);
4431 }
4432 }
4433
4434 /* and now drop all following messages until we start again. The
4435 * bus is set to flush=false again in bacon_video_widget_open()
4436 */
4437 gst_bus_set_flushing (bvw->bus, TRUE);
4438
4439 /* Now in READY or lower */
4440 bvw->target_state = GST_STATE_READY;
4441
4442 bvw->buffering = FALSE;
4443 bvw->plugin_install_in_progress = FALSE;
4444 bvw->download_buffering = FALSE;
4445 g_clear_pointer (&bvw->download_filename, g_free);
4446 bvw->buffering_left = -1;
4447 bvw_reconfigure_fill_timeout (bvw, 0);
4448 bvw->movie_par_n = bvw->movie_par_d = 1;
4449 g_clear_object (&bvw->cover_pixbuf);
4450 clutter_actor_hide (bvw->spinner);
4451 g_object_set (G_OBJECT (bvw->spinner), "percent", 0.0, NULL);
4452 totem_aspect_frame_set_internal_rotation (TOTEM_ASPECT_FRAME (bvw->frame), 0.0);
4453 GST_DEBUG ("stopped");
4454 }
4455
4456 /**
4457 * bacon_video_widget_stop:
4458 * @bvw: a #BaconVideoWidget
4459 *
4460 * Stops playing the current stream and resets to the first position in the stream.
4461 **/
4462 void
bacon_video_widget_stop(BaconVideoWidget * bvw)4463 bacon_video_widget_stop (BaconVideoWidget * bvw)
4464 {
4465 g_return_if_fail (BACON_IS_VIDEO_WIDGET (bvw));
4466 g_return_if_fail (GST_IS_ELEMENT (bvw->play));
4467
4468 GST_LOG ("Stopping");
4469 bvw_stop_play_pipeline (bvw);
4470
4471 /* Reset position to 0 when stopping */
4472 got_time_tick (GST_ELEMENT (bvw->play), 0, bvw);
4473 }
4474
4475 /**
4476 * bacon_video_widget_close:
4477 * @bvw: a #BaconVideoWidget
4478 *
4479 * Closes the current stream and frees the resources associated with it.
4480 **/
4481 void
bacon_video_widget_close(BaconVideoWidget * bvw)4482 bacon_video_widget_close (BaconVideoWidget * bvw)
4483 {
4484 g_return_if_fail (BACON_IS_VIDEO_WIDGET (bvw));
4485 g_return_if_fail (GST_IS_ELEMENT (bvw->play));
4486
4487 GST_LOG ("Closing");
4488 bvw_stop_play_pipeline (bvw);
4489
4490 g_clear_pointer (&bvw->mrl, g_free);
4491 g_clear_pointer (&bvw->subtitle_uri, g_free);
4492 g_object_set (G_OBJECT (bvw->play), "suburi", NULL, NULL);
4493 g_clear_pointer (&bvw->subtitle_uri, g_free);
4494 g_clear_pointer (&bvw->user_id, g_free);
4495 g_clear_pointer (&bvw->user_pw, g_free);
4496
4497 bvw->is_live = FALSE;
4498 bvw->is_menu = FALSE;
4499 bvw->has_angles = FALSE;
4500 bvw->rate = FORWARD_RATE;
4501
4502 bvw->current_time = 0;
4503 bvw->seek_req_time = GST_CLOCK_TIME_NONE;
4504 bvw->seek_time = -1;
4505 bvw->stream_length = 0;
4506
4507 if (bvw->eos_id != 0)
4508 g_source_remove (bvw->eos_id);
4509
4510 if (bvw->chapters) {
4511 g_list_free_full (bvw->chapters, (GDestroyNotify) gst_mini_object_unref);
4512 bvw->chapters = NULL;
4513 }
4514
4515 g_clear_pointer (&bvw->tagcache, gst_tag_list_unref);
4516 g_clear_pointer (&bvw->audiotags, gst_tag_list_unref);
4517 g_clear_pointer (&bvw->videotags, gst_tag_list_unref);
4518
4519 g_object_notify (G_OBJECT (bvw), "seekable");
4520 g_signal_emit (bvw, bvw_signals[SIGNAL_CHANNELS_CHANGE], 0);
4521 got_time_tick (GST_ELEMENT (bvw->play), 0, bvw);
4522 }
4523
4524 static void
bvw_do_navigation_command(BaconVideoWidget * bvw,GstNavigationCommand command)4525 bvw_do_navigation_command (BaconVideoWidget * bvw, GstNavigationCommand command)
4526 {
4527 if (bvw->navigation)
4528 gst_navigation_send_command (bvw->navigation, command);
4529 }
4530
4531 /**
4532 * bacon_video_widget_set_text_subtitle:
4533 * @bvw: a #BaconVideoWidget
4534 * @subtitle_uri: (allow-none): the URI of a subtitle file, or %NULL
4535 *
4536 * Sets the URI for the text subtitle file to be displayed alongside
4537 * the current video. Use %NULL if you want to unload the current text subtitle
4538 * file being used.
4539 */
4540 void
bacon_video_widget_set_text_subtitle(BaconVideoWidget * bvw,const gchar * subtitle_uri)4541 bacon_video_widget_set_text_subtitle (BaconVideoWidget * bvw,
4542 const gchar * subtitle_uri)
4543 {
4544 GstState cur_state;
4545
4546 g_return_if_fail (BACON_IS_VIDEO_WIDGET (bvw));
4547 g_return_if_fail (GST_IS_ELEMENT (bvw->play));
4548 g_return_if_fail (bvw->mrl != NULL);
4549
4550 GST_LOG ("Setting subtitle as %s", GST_STR_NULL (subtitle_uri));
4551
4552 if (subtitle_uri == NULL &&
4553 bvw->subtitle_uri == NULL)
4554 return;
4555
4556 /* Wait for the previous state change to finish */
4557 gst_element_get_state (bvw->play, NULL, NULL, GST_CLOCK_TIME_NONE);
4558
4559 /* -> READY */
4560 gst_element_get_state (bvw->play, &cur_state, NULL, 0);
4561 if (cur_state > GST_STATE_READY) {
4562 gst_element_set_state (bvw->play, GST_STATE_READY);
4563 /* Block for new state */
4564 gst_element_get_state (bvw->play, NULL, NULL, GST_CLOCK_TIME_NONE);
4565 }
4566
4567 g_free (bvw->subtitle_uri);
4568 bvw->subtitle_uri = g_strdup (subtitle_uri);
4569 g_object_set (G_OBJECT (bvw->play), "suburi", subtitle_uri, NULL);
4570
4571 /* And back to the original state */
4572 if (cur_state > GST_STATE_READY) {
4573 gst_element_set_state (bvw->play, cur_state);
4574 /* Block for new state */
4575 gst_element_get_state (bvw->play, NULL, NULL, GST_CLOCK_TIME_NONE);
4576 }
4577
4578 if (bvw->current_time > 0)
4579 bacon_video_widget_seek_time_no_lock (bvw, bvw->current_time,
4580 GST_SEEK_FLAG_ACCURATE, NULL);
4581 }
4582
4583 static void
handle_dvd_seek(BaconVideoWidget * bvw,int offset,const char * fmt_name)4584 handle_dvd_seek (BaconVideoWidget *bvw,
4585 int offset,
4586 const char *fmt_name)
4587 {
4588 GstFormat fmt;
4589 gint64 val;
4590
4591 fmt = gst_format_get_by_nick (fmt_name);
4592 if (!fmt)
4593 return;
4594
4595 bvw_set_playback_direction (bvw, TRUE);
4596
4597 if (gst_element_query_position (bvw->play, fmt, &val)) {
4598 GST_DEBUG ("current %s is: %" G_GINT64_FORMAT, fmt_name, val);
4599 val += offset;
4600 GST_DEBUG ("seeking to %s: %" G_GINT64_FORMAT, fmt_name, val);
4601 gst_element_seek (bvw->play, FORWARD_RATE, fmt, GST_SEEK_FLAG_FLUSH,
4602 GST_SEEK_TYPE_SET, val, GST_SEEK_TYPE_NONE, G_GINT64_CONSTANT (0));
4603 bvw->rate = FORWARD_RATE;
4604 } else {
4605 GST_DEBUG ("failed to query position (%s)", fmt_name);
4606 }
4607 }
4608
4609 static gboolean
handle_chapters_seek(BaconVideoWidget * bvw,gboolean forward)4610 handle_chapters_seek (BaconVideoWidget *bvw,
4611 gboolean forward)
4612 {
4613 GList *l;
4614 GstTocEntry *entry;
4615 gint64 start;
4616
4617 l = bvw_get_current_chapter (bvw);
4618 if (!l)
4619 return FALSE;
4620
4621 entry = NULL;
4622 if (forward && l->next)
4623 entry = l->next->data;
4624 else if (!forward) {
4625 gint64 current_start;
4626 if (gst_toc_entry_get_start_stop_times (l->data, ¤t_start, NULL)) {
4627 if (bvw->current_time - current_start / GST_MSECOND < REWIND_OR_PREVIOUS &&
4628 bvw->current_time - current_start / GST_MSECOND > 0 &&
4629 l->prev) {
4630 entry = l->prev->data;
4631 } else {
4632 entry = l->data;
4633 }
4634 }
4635 }
4636
4637 if (!entry)
4638 return FALSE;
4639
4640 if (!gst_toc_entry_get_start_stop_times (entry, &start, NULL))
4641 return FALSE;
4642
4643 GST_DEBUG ("Found chapter and seeking to %" G_GINT64_FORMAT, start / GST_MSECOND);
4644
4645 return bacon_video_widget_seek_time (bvw, start / GST_MSECOND, FALSE, NULL);
4646 }
4647
4648 /**
4649 * bacon_video_widget_dvd_event:
4650 * @bvw: a #BaconVideoWidget
4651 * @type: the type of DVD event to issue
4652 *
4653 * Issues a DVD navigation event to the video widget, such as one to skip to the
4654 * next chapter, or navigate to the DVD title menu.
4655 *
4656 * This is a no-op if the current stream is not navigable.
4657 **/
4658 void
bacon_video_widget_dvd_event(BaconVideoWidget * bvw,BvwDVDEvent type)4659 bacon_video_widget_dvd_event (BaconVideoWidget * bvw,
4660 BvwDVDEvent type)
4661 {
4662 g_return_if_fail (BACON_IS_VIDEO_WIDGET (bvw));
4663 g_return_if_fail (GST_IS_ELEMENT (bvw->play));
4664
4665 GST_DEBUG ("Sending event '%s'", get_type_name (BVW_TYPE_DVD_EVENT, type));
4666
4667 switch (type) {
4668 case BVW_DVD_ROOT_MENU:
4669 bvw_do_navigation_command (bvw, GST_NAVIGATION_COMMAND_DVD_MENU);
4670 break;
4671 case BVW_DVD_TITLE_MENU:
4672 bvw_do_navigation_command (bvw, GST_NAVIGATION_COMMAND_DVD_TITLE_MENU);
4673 break;
4674 case BVW_DVD_SUBPICTURE_MENU:
4675 bvw_do_navigation_command (bvw, GST_NAVIGATION_COMMAND_DVD_SUBPICTURE_MENU);
4676 break;
4677 case BVW_DVD_AUDIO_MENU:
4678 bvw_do_navigation_command (bvw, GST_NAVIGATION_COMMAND_DVD_AUDIO_MENU);
4679 break;
4680 case BVW_DVD_ANGLE_MENU:
4681 bvw_do_navigation_command (bvw, GST_NAVIGATION_COMMAND_DVD_ANGLE_MENU);
4682 break;
4683 case BVW_DVD_CHAPTER_MENU:
4684 bvw_do_navigation_command (bvw, GST_NAVIGATION_COMMAND_DVD_CHAPTER_MENU);
4685 break;
4686 case BVW_DVD_ROOT_MENU_UP:
4687 bvw_do_navigation_command (bvw, GST_NAVIGATION_COMMAND_UP);
4688 break;
4689 case BVW_DVD_ROOT_MENU_DOWN:
4690 bvw_do_navigation_command (bvw, GST_NAVIGATION_COMMAND_DOWN);
4691 break;
4692 case BVW_DVD_ROOT_MENU_LEFT:
4693 bvw_do_navigation_command (bvw, GST_NAVIGATION_COMMAND_LEFT);
4694 break;
4695 case BVW_DVD_ROOT_MENU_RIGHT:
4696 bvw_do_navigation_command (bvw, GST_NAVIGATION_COMMAND_RIGHT);
4697 break;
4698 case BVW_DVD_ROOT_MENU_SELECT:
4699 bvw_do_navigation_command (bvw, GST_NAVIGATION_COMMAND_ACTIVATE);
4700 break;
4701 case BVW_DVD_NEXT_CHAPTER:
4702 if (!handle_chapters_seek (bvw, TRUE))
4703 handle_dvd_seek (bvw, 1, "chapter");
4704 break;
4705 case BVW_DVD_PREV_CHAPTER:
4706 if (!handle_chapters_seek (bvw, FALSE))
4707 handle_dvd_seek (bvw, -1, "chapter");
4708 break;
4709 case BVW_DVD_NEXT_TITLE:
4710 handle_dvd_seek (bvw, 1, "title");
4711 break;
4712 case BVW_DVD_PREV_TITLE:
4713 handle_dvd_seek (bvw, -1, "title");
4714 break;
4715 default:
4716 GST_WARNING ("unhandled type %d", type);
4717 break;
4718 }
4719 }
4720
4721 /**
4722 * bacon_video_widget_set_logo:
4723 * @bvw: a #BaconVideoWidget
4724 * @name: the icon name of the logo
4725 *
4726 * Sets the logo displayed on the video widget when no stream is loaded.
4727 **/
4728 void
bacon_video_widget_set_logo(BaconVideoWidget * bvw,const gchar * name)4729 bacon_video_widget_set_logo (BaconVideoWidget *bvw, const gchar *name)
4730 {
4731 GtkIconTheme *theme;
4732 GError *error = NULL;
4733
4734 g_return_if_fail (BACON_IS_VIDEO_WIDGET (bvw));
4735 g_return_if_fail (name != NULL);
4736
4737 if (bvw->logo_pixbuf != NULL)
4738 g_object_unref (bvw->logo_pixbuf);
4739
4740 theme = gtk_icon_theme_get_for_screen (gtk_widget_get_screen (GTK_WIDGET (bvw)));
4741 bvw->logo_pixbuf = gtk_icon_theme_load_icon (theme, name, LOGO_SIZE, 0, &error);
4742
4743 if (error) {
4744 g_warning ("An error occurred trying to open logo %s: %s", name, error->message);
4745 g_error_free (error);
4746 return;
4747 }
4748
4749 set_current_actor (bvw);
4750 }
4751
4752 /**
4753 * bacon_video_widget_set_logo_mode:
4754 * @bvw: a #BaconVideoWidget
4755 * @logo_mode: %TRUE to display the logo, %FALSE otherwise
4756 *
4757 * Sets whether to display a logo set with @bacon_video_widget_set_logo when
4758 * no stream is loaded. If @logo_mode is %FALSE, nothing will be displayed
4759 * and the video widget will take up no space. Otherwise, the logo will be
4760 * displayed and will requisition a corresponding amount of space.
4761 **/
4762 void
bacon_video_widget_set_logo_mode(BaconVideoWidget * bvw,gboolean logo_mode)4763 bacon_video_widget_set_logo_mode (BaconVideoWidget * bvw, gboolean logo_mode)
4764 {
4765 g_return_if_fail (BACON_IS_VIDEO_WIDGET (bvw));
4766
4767 logo_mode = logo_mode != FALSE;
4768
4769 if (bvw->logo_mode != logo_mode) {
4770 bvw->logo_mode = logo_mode;
4771
4772 set_current_actor (bvw);
4773
4774 g_object_notify (G_OBJECT (bvw), "logo-mode");
4775 g_object_notify (G_OBJECT (bvw), "seekable");
4776 }
4777 }
4778
4779 /**
4780 * bacon_video_widget_get_logo_mode
4781 * @bvw: a #BaconVideoWidget
4782 *
4783 * Gets whether the logo is displayed when no stream is loaded.
4784 *
4785 * Return value: %TRUE if the logo is displayed, %FALSE otherwise
4786 **/
4787 gboolean
bacon_video_widget_get_logo_mode(BaconVideoWidget * bvw)4788 bacon_video_widget_get_logo_mode (BaconVideoWidget * bvw)
4789 {
4790 g_return_val_if_fail (BACON_IS_VIDEO_WIDGET (bvw), FALSE);
4791
4792 return bvw->logo_mode;
4793 }
4794
4795 static gboolean
bvw_check_for_cover_pixbuf(BaconVideoWidget * bvw)4796 bvw_check_for_cover_pixbuf (BaconVideoWidget * bvw)
4797 {
4798 GValue value = { 0, };
4799
4800 /* for efficiency reasons (decoding of encoded image into pixbuf) we assume
4801 * that all potential images come in the same taglist, so once we've
4802 * determined the best image/cover, we assume that's really the best one
4803 * for this stream, even if more tag messages come in later (this should
4804 * not be a problem in practice) */
4805 if (bvw->cover_pixbuf)
4806 return TRUE;
4807
4808 bacon_video_widget_get_metadata (bvw, BVW_INFO_COVER, &value);
4809 if (G_VALUE_HOLDS_OBJECT (&value)) {
4810 bvw->cover_pixbuf = g_value_dup_object (&value);
4811 g_value_unset (&value);
4812 }
4813
4814 return (bvw->cover_pixbuf != NULL);
4815 }
4816
4817 static const GdkPixbuf *
bvw_get_logo_pixbuf(BaconVideoWidget * bvw)4818 bvw_get_logo_pixbuf (BaconVideoWidget * bvw)
4819 {
4820 if (bvw_check_for_cover_pixbuf (bvw))
4821 return bvw->cover_pixbuf;
4822 else
4823 return bvw->logo_pixbuf;
4824 }
4825
4826 /**
4827 * bacon_video_widget_pause:
4828 * @bvw: a #BaconVideoWidget
4829 *
4830 * Pauses the current stream in the video widget.
4831 *
4832 * If a live stream is being played, playback is stopped entirely.
4833 **/
4834 void
bacon_video_widget_pause(BaconVideoWidget * bvw)4835 bacon_video_widget_pause (BaconVideoWidget * bvw)
4836 {
4837 GstStateChangeReturn ret;
4838 GstState state;
4839
4840 g_return_if_fail (BACON_IS_VIDEO_WIDGET (bvw));
4841 g_return_if_fail (GST_IS_ELEMENT (bvw->play));
4842 g_return_if_fail (bvw->mrl != NULL);
4843
4844 /* Get the current state */
4845 ret = gst_element_get_state (GST_ELEMENT (bvw->play), &state, NULL, 0);
4846
4847 if (bvw->is_live != FALSE &&
4848 ret != GST_STATE_CHANGE_NO_PREROLL &&
4849 ret != GST_STATE_CHANGE_SUCCESS &&
4850 state > GST_STATE_READY) {
4851 GST_LOG ("Stopping because we have a live stream");
4852 bacon_video_widget_stop (bvw);
4853 return;
4854 }
4855
4856 GST_LOG ("Pausing");
4857 bvw->target_state = GST_STATE_PAUSED;
4858 gst_element_set_state (GST_ELEMENT (bvw->play), GST_STATE_PAUSED);
4859 }
4860
4861 /**
4862 * bacon_video_widget_set_subtitle_font:
4863 * @bvw: a #BaconVideoWidget
4864 * @font: a font description string
4865 *
4866 * Sets the font size and style in which to display subtitles.
4867 *
4868 * @font is a Pango font description string, as understood by
4869 * pango_font_description_from_string().
4870 **/
4871 void
bacon_video_widget_set_subtitle_font(BaconVideoWidget * bvw,const gchar * font)4872 bacon_video_widget_set_subtitle_font (BaconVideoWidget * bvw,
4873 const gchar * font)
4874 {
4875 g_return_if_fail (BACON_IS_VIDEO_WIDGET (bvw));
4876 g_return_if_fail (GST_IS_ELEMENT (bvw->play));
4877
4878 if (!g_object_class_find_property (G_OBJECT_GET_CLASS (bvw->play), "subtitle-font-desc"))
4879 return;
4880 g_object_set (bvw->play, "subtitle-font-desc", font, NULL);
4881 }
4882
4883 /**
4884 * bacon_video_widget_set_subtitle_encoding:
4885 * @bvw: a #BaconVideoWidget
4886 * @encoding: an encoding system
4887 *
4888 * Sets the encoding system for the subtitles, so that they can be decoded
4889 * properly.
4890 **/
4891 void
bacon_video_widget_set_subtitle_encoding(BaconVideoWidget * bvw,const char * encoding)4892 bacon_video_widget_set_subtitle_encoding (BaconVideoWidget *bvw,
4893 const char *encoding)
4894 {
4895 g_return_if_fail (BACON_IS_VIDEO_WIDGET (bvw));
4896 g_return_if_fail (GST_IS_ELEMENT (bvw->play));
4897
4898 if (!g_object_class_find_property (G_OBJECT_GET_CLASS (bvw->play), "subtitle-encoding"))
4899 return;
4900 g_object_set (bvw->play, "subtitle-encoding", encoding, NULL);
4901 }
4902
4903 /**
4904 * bacon_video_widget_set_user_agent:
4905 * @bvw: a #BaconVideoWidget
4906 * @user_agent: a HTTP user agent string, or %NULL to use the default
4907 *
4908 * Sets the HTTP user agent string to use when fetching HTTP ressources.
4909 **/
4910 void
bacon_video_widget_set_user_agent(BaconVideoWidget * bvw,const char * user_agent)4911 bacon_video_widget_set_user_agent (BaconVideoWidget *bvw,
4912 const char *user_agent)
4913 {
4914 g_return_if_fail (BACON_IS_VIDEO_WIDGET (bvw));
4915
4916 if (g_strcmp0 (user_agent, bvw->user_agent) == 0)
4917 return;
4918
4919 g_free (bvw->user_agent);
4920 bvw->user_agent = g_strdup (user_agent);
4921
4922 g_object_notify (G_OBJECT (bvw), "user-agent");
4923 }
4924
4925 /**
4926 * bacon_video_widget_set_referrer:
4927 * @bvw: a #BaconVideoWidget
4928 * @referrer: a HTTP referrer URI, or %NULL
4929 *
4930 * Sets the HTTP referrer URI to use when fetching HTTP ressources.
4931 **/
4932 void
bacon_video_widget_set_referrer(BaconVideoWidget * bvw,const char * referrer)4933 bacon_video_widget_set_referrer (BaconVideoWidget *bvw,
4934 const char *referrer)
4935 {
4936 char *frag;
4937
4938 g_return_if_fail (BACON_IS_VIDEO_WIDGET (bvw));
4939
4940 if (g_strcmp0 (referrer, bvw->referrer) == 0)
4941 return;
4942
4943 g_free (bvw->referrer);
4944 bvw->referrer = g_strdup (referrer);
4945
4946 /* Referrer URIs must not have a fragment */
4947 if ((frag = strchr (bvw->referrer, '#')) != NULL)
4948 *frag = '\0';
4949
4950 g_object_notify (G_OBJECT (bvw), "referrer");
4951 }
4952
4953 /**
4954 * bacon_video_widget_can_set_volume:
4955 * @bvw: a #BaconVideoWidget
4956 *
4957 * Returns whether the volume level can be set, given the current settings.
4958 *
4959 * The volume cannot be set if the audio output type is set to
4960 * %BVW_AUDIO_SOUND_AC3PASSTHRU.
4961 *
4962 * Return value: %TRUE if the volume can be set, %FALSE otherwise
4963 **/
4964 gboolean
bacon_video_widget_can_set_volume(BaconVideoWidget * bvw)4965 bacon_video_widget_can_set_volume (BaconVideoWidget * bvw)
4966 {
4967 g_return_val_if_fail (BACON_IS_VIDEO_WIDGET (bvw), FALSE);
4968 g_return_val_if_fail (GST_IS_ELEMENT (bvw->play), FALSE);
4969
4970 if (bvw->speakersetup == BVW_AUDIO_SOUND_AC3PASSTHRU)
4971 return FALSE;
4972
4973 return !bvw->uses_audio_fakesink;
4974 }
4975
4976 /**
4977 * bacon_video_widget_set_volume:
4978 * @bvw: a #BaconVideoWidget
4979 * @volume: the new volume level, as a percentage between <code class="literal">0</code> and <code class="literal">1</code>
4980 *
4981 * Sets the volume level of the stream as a percentage between <code class="literal">0</code> and <code class="literal">1</code>.
4982 *
4983 * If bacon_video_widget_can_set_volume() returns %FALSE, this is a no-op.
4984 **/
4985 void
bacon_video_widget_set_volume(BaconVideoWidget * bvw,double volume)4986 bacon_video_widget_set_volume (BaconVideoWidget * bvw, double volume)
4987 {
4988 g_return_if_fail (BACON_IS_VIDEO_WIDGET (bvw));
4989 g_return_if_fail (GST_IS_ELEMENT (bvw->play));
4990
4991 if (bacon_video_widget_can_set_volume (bvw) != FALSE) {
4992 volume = CLAMP (volume, 0.0, 1.0);
4993 gst_stream_volume_set_volume (GST_STREAM_VOLUME (bvw->play),
4994 GST_STREAM_VOLUME_FORMAT_CUBIC,
4995 volume);
4996
4997 bvw->volume = volume;
4998 g_object_notify (G_OBJECT (bvw), "volume");
4999 }
5000 }
5001
5002 /**
5003 * bacon_video_widget_get_volume:
5004 * @bvw: a #BaconVideoWidget
5005 *
5006 * Returns the current volume level, as a percentage between <code class="literal">0</code> and <code class="literal">1</code>.
5007 *
5008 * Return value: the volume as a percentage between <code class="literal">0</code> and <code class="literal">1</code>
5009 **/
5010 double
bacon_video_widget_get_volume(BaconVideoWidget * bvw)5011 bacon_video_widget_get_volume (BaconVideoWidget * bvw)
5012 {
5013 g_return_val_if_fail (BACON_IS_VIDEO_WIDGET (bvw), 0.0);
5014 g_return_val_if_fail (GST_IS_ELEMENT (bvw->play), 0.0);
5015
5016 return bvw->volume;
5017 }
5018
5019 /**
5020 * bacon_video_widget_set_aspect_ratio:
5021 * @bvw: a #BaconVideoWidget
5022 * @ratio: the new aspect ratio
5023 *
5024 * Sets the aspect ratio used by the widget, from #BvwAspectRatio.
5025 *
5026 * Changes to this take effect immediately.
5027 **/
5028 void
bacon_video_widget_set_aspect_ratio(BaconVideoWidget * bvw,BvwAspectRatio ratio)5029 bacon_video_widget_set_aspect_ratio (BaconVideoWidget *bvw,
5030 BvwAspectRatio ratio)
5031 {
5032 GstMessage *msg;
5033
5034 g_return_if_fail (BACON_IS_VIDEO_WIDGET (bvw));
5035
5036 bvw->ratio_type = ratio;
5037 msg = gst_message_new_application (GST_OBJECT (bvw->play),
5038 gst_structure_new ("video-size", "width", G_TYPE_INT,
5039 bvw->video_width, "height", G_TYPE_INT,
5040 bvw->video_height, NULL));
5041 gst_element_post_message (bvw->play, msg);
5042 }
5043
5044 /**
5045 * bacon_video_widget_get_aspect_ratio:
5046 * @bvw: a #BaconVideoWidget
5047 *
5048 * Returns the current aspect ratio used by the widget, from
5049 * #BvwAspectRatio.
5050 *
5051 * Return value: the aspect ratio
5052 **/
5053 BvwAspectRatio
bacon_video_widget_get_aspect_ratio(BaconVideoWidget * bvw)5054 bacon_video_widget_get_aspect_ratio (BaconVideoWidget *bvw)
5055 {
5056 g_return_val_if_fail (BACON_IS_VIDEO_WIDGET (bvw), 0);
5057
5058 return bvw->ratio_type;
5059 }
5060
5061 /**
5062 * bacon_video_widget_set_zoom:
5063 * @bvw: a #BaconVideoWidget
5064 * @mode: the #BvwZoomMode
5065 *
5066 * Sets the zoom type applied to the video when it is displayed.
5067 **/
5068 void
bacon_video_widget_set_zoom(BaconVideoWidget * bvw,BvwZoomMode mode)5069 bacon_video_widget_set_zoom (BaconVideoWidget *bvw,
5070 BvwZoomMode mode)
5071 {
5072 g_return_if_fail (BACON_IS_VIDEO_WIDGET (bvw));
5073
5074 if (bvw->frame == NULL)
5075 return;
5076
5077 totem_aspect_frame_set_expand (TOTEM_ASPECT_FRAME (bvw->frame),
5078 (mode == BVW_ZOOM_EXPAND));
5079 }
5080
5081 /**
5082 * bacon_video_widget_get_zoom:
5083 * @bvw: a #BaconVideoWidget
5084 *
5085 * Returns the zoom mode applied to videos displayed by the widget.
5086 *
5087 * Return value: a #BvwZoomMode
5088 **/
5089 BvwZoomMode
bacon_video_widget_get_zoom(BaconVideoWidget * bvw)5090 bacon_video_widget_get_zoom (BaconVideoWidget *bvw)
5091 {
5092 gboolean expand;
5093
5094 g_return_val_if_fail (BACON_IS_VIDEO_WIDGET (bvw), BVW_ZOOM_NONE);
5095
5096 expand = totem_aspect_frame_get_expand (TOTEM_ASPECT_FRAME (bvw->frame));
5097 return expand ? BVW_ZOOM_EXPAND : BVW_ZOOM_NONE;
5098 }
5099
5100 /**
5101 * bacon_video_widget_set_rotation:
5102 * @bvw: a #BaconVideoWidget
5103 * @rotation: the #BvwRotation of the video in degrees
5104 *
5105 * Sets the rotation to be applied to the video when it is displayed.
5106 **/
5107 void
bacon_video_widget_set_rotation(BaconVideoWidget * bvw,BvwRotation rotation)5108 bacon_video_widget_set_rotation (BaconVideoWidget *bvw,
5109 BvwRotation rotation)
5110 {
5111 gfloat angle;
5112
5113 g_return_if_fail (BACON_IS_VIDEO_WIDGET (bvw));
5114
5115 if (bvw->frame == NULL)
5116 return;
5117
5118 GST_DEBUG ("Rotating to %s (%f degrees) from %s",
5119 get_type_name (BVW_TYPE_ROTATION, rotation),
5120 rotation * 90.0,
5121 get_type_name (BVW_TYPE_ROTATION, bvw->rotation));
5122
5123 bvw->rotation = rotation;
5124
5125 angle = rotation * 90.0;
5126 totem_aspect_frame_set_rotation (TOTEM_ASPECT_FRAME (bvw->frame), angle);
5127 }
5128
5129 /**
5130 * bacon_video_widget_get_rotation:
5131 * @bvw: a #BaconVideoWidget
5132 *
5133 * Returns the angle of rotation of the video, in degrees.
5134 *
5135 * Return value: a #BvwRotation.
5136 **/
5137 BvwRotation
bacon_video_widget_get_rotation(BaconVideoWidget * bvw)5138 bacon_video_widget_get_rotation (BaconVideoWidget *bvw)
5139 {
5140 g_return_val_if_fail (BACON_IS_VIDEO_WIDGET (bvw), BVW_ROTATION_R_ZERO);
5141
5142 return bvw->rotation;
5143 }
5144
5145 /* Search for the color balance channel corresponding to type and return it. */
5146 static GstColorBalanceChannel *
bvw_get_color_balance_channel(GstColorBalance * color_balance,BvwVideoProperty type)5147 bvw_get_color_balance_channel (GstColorBalance * color_balance,
5148 BvwVideoProperty type)
5149 {
5150 const GList *channels;
5151
5152 channels = gst_color_balance_list_channels (color_balance);
5153
5154 for (; channels != NULL; channels = channels->next) {
5155 GstColorBalanceChannel *c = channels->data;
5156
5157 if (type == BVW_VIDEO_BRIGHTNESS && g_strrstr (c->label, "BRIGHTNESS"))
5158 return g_object_ref (c);
5159 else if (type == BVW_VIDEO_CONTRAST && g_strrstr (c->label, "CONTRAST"))
5160 return g_object_ref (c);
5161 else if (type == BVW_VIDEO_SATURATION && g_strrstr (c->label, "SATURATION"))
5162 return g_object_ref (c);
5163 else if (type == BVW_VIDEO_HUE && g_strrstr (c->label, "HUE"))
5164 return g_object_ref (c);
5165 }
5166
5167 return NULL;
5168 }
5169
5170 /**
5171 * bacon_video_widget_get_video_property:
5172 * @bvw: a #BaconVideoWidget
5173 * @type: the type of property
5174 *
5175 * Returns the given property of the video display, such as its brightness or saturation.
5176 *
5177 * It is returned as a percentage in the full range of integer values; from <code class="literal">0</code>
5178 * to <code class="literal">65535</code> (inclusive), where <code class="literal">32768</code> is the default.
5179 *
5180 * Return value: the property's value, in the range <code class="literal">0</code> to <code class="literal">65535</code>
5181 **/
5182 int
bacon_video_widget_get_video_property(BaconVideoWidget * bvw,BvwVideoProperty type)5183 bacon_video_widget_get_video_property (BaconVideoWidget *bvw,
5184 BvwVideoProperty type)
5185 {
5186 GstColorBalanceChannel *found_channel = NULL;
5187 int ret, cur;
5188
5189 g_return_val_if_fail (BACON_IS_VIDEO_WIDGET (bvw), 65535/2);
5190 g_return_val_if_fail (bvw->play != NULL, 65535/2);
5191
5192 ret = 0;
5193
5194
5195 found_channel = bvw_get_color_balance_channel (GST_COLOR_BALANCE (bvw->play), type);
5196 cur = gst_color_balance_get_value (GST_COLOR_BALANCE (bvw->play), found_channel);
5197
5198 GST_DEBUG ("channel %s: cur=%d, min=%d, max=%d", found_channel->label,
5199 cur, found_channel->min_value, found_channel->max_value);
5200
5201 ret = floor (0.5 +
5202 ((double) cur - found_channel->min_value) * 65535 /
5203 ((double) found_channel->max_value - found_channel->min_value));
5204
5205 GST_DEBUG ("channel %s: returning value %d", found_channel->label, ret);
5206 g_object_unref (found_channel);
5207 return ret;
5208 }
5209
5210 /**
5211 * bacon_video_widget_has_menus:
5212 * @bvw: a #BaconVideoWidget
5213 *
5214 * Returns whether the widget is currently displaying a menu,
5215 * such as a DVD menu.
5216 *
5217 * Return value: %TRUE if a menu is displayed, %FALSE otherwise
5218 **/
5219 gboolean
bacon_video_widget_has_menus(BaconVideoWidget * bvw)5220 bacon_video_widget_has_menus (BaconVideoWidget *bvw)
5221 {
5222 g_return_val_if_fail (BACON_IS_VIDEO_WIDGET (bvw), FALSE);
5223
5224 if (bacon_video_widget_is_playing (bvw) == FALSE)
5225 return FALSE;
5226
5227 return bvw->is_menu;
5228 }
5229
5230 /**
5231 * bacon_video_widget_has_angles:
5232 * @bvw: a #BaconVideoWidget
5233 *
5234 * Returns whether the widget is currently playing a stream with
5235 * multiple angles.
5236 *
5237 * Return value: %TRUE if the current video stream has multiple
5238 * angles, %FALSE otherwise
5239 **/
5240 gboolean
bacon_video_widget_has_angles(BaconVideoWidget * bvw)5241 bacon_video_widget_has_angles (BaconVideoWidget *bvw)
5242 {
5243 guint n_video;
5244
5245 g_return_val_if_fail (BACON_IS_VIDEO_WIDGET (bvw), FALSE);
5246
5247 if (bacon_video_widget_is_playing (bvw) == FALSE)
5248 return FALSE;
5249
5250 if (bvw->has_angles)
5251 return TRUE;
5252
5253 g_object_get (G_OBJECT (bvw->play), "n-video", &n_video, NULL);
5254
5255 return n_video > 1;
5256 }
5257
5258 /**
5259 * bacon_video_widget_set_next_angle:
5260 * @bvw: a #BaconVideoWidget
5261 *
5262 * Select the next angle, or video track in the playing stream.
5263 **/
5264 void
bacon_video_widget_set_next_angle(BaconVideoWidget * bvw)5265 bacon_video_widget_set_next_angle (BaconVideoWidget *bvw)
5266 {
5267 guint n_video, current_video;
5268
5269 g_return_if_fail (BACON_IS_VIDEO_WIDGET (bvw));
5270
5271 if (bacon_video_widget_is_playing (bvw) == FALSE)
5272 return;
5273
5274 if (bvw->has_angles) {
5275 GST_DEBUG ("Sending event 'next-angle'");
5276 bvw_do_navigation_command (bvw, GST_NAVIGATION_COMMAND_NEXT_ANGLE);
5277 return;
5278 }
5279
5280 g_object_get (G_OBJECT (bvw->play),
5281 "current-video", ¤t_video,
5282 "n-video", &n_video,
5283 NULL);
5284
5285 if (n_video <= 1) {
5286 GST_DEBUG ("Not setting next video stream, we have %d video streams", n_video);
5287 return;
5288 }
5289
5290 current_video++;
5291 if (current_video == n_video)
5292 current_video = 0;
5293
5294 GST_DEBUG ("Setting current-video to %d/%d", current_video, n_video);
5295 g_object_set (G_OBJECT (bvw->play), "current-video", current_video, NULL);
5296 }
5297
5298 static gboolean
notify_volume_idle_cb(BaconVideoWidget * bvw)5299 notify_volume_idle_cb (BaconVideoWidget *bvw)
5300 {
5301 gdouble vol;
5302
5303 vol = gst_stream_volume_get_volume (GST_STREAM_VOLUME (bvw->play),
5304 GST_STREAM_VOLUME_FORMAT_CUBIC);
5305
5306 bvw->volume = vol;
5307
5308 g_object_notify (G_OBJECT (bvw), "volume");
5309
5310 return FALSE;
5311 }
5312
5313 static void
notify_volume_cb(GObject * object,GParamSpec * pspec,BaconVideoWidget * bvw)5314 notify_volume_cb (GObject *object,
5315 GParamSpec *pspec,
5316 BaconVideoWidget *bvw)
5317 {
5318 guint id;
5319
5320 id = g_idle_add ((GSourceFunc) notify_volume_idle_cb, bvw);
5321 g_source_set_name_by_id (id, "[totem] notify_volume_idle_cb");
5322 }
5323
5324 /**
5325 * bacon_video_widget_set_video_property:
5326 * @bvw: a #BaconVideoWidget
5327 * @type: the type of property
5328 * @value: the property's value, in the range <code class="literal">0</code> to <code class="literal">65535</code>
5329 *
5330 * Sets the given property of the video display, such as its brightness or saturation.
5331 *
5332 * It should be given as a percentage in the full range of integer values; from <code class="literal">0</code>
5333 * to <code class="literal">65535</code> (inclusive), where <code class="literal">32768</code> is the default.
5334 **/
5335 void
bacon_video_widget_set_video_property(BaconVideoWidget * bvw,BvwVideoProperty type,int value)5336 bacon_video_widget_set_video_property (BaconVideoWidget *bvw,
5337 BvwVideoProperty type,
5338 int value)
5339 {
5340 GstColorBalanceChannel *found_channel = NULL;
5341 int i_value;
5342
5343 g_return_if_fail (BACON_IS_VIDEO_WIDGET (bvw));
5344 g_return_if_fail (bvw->play != NULL);
5345
5346 GST_DEBUG ("set video property type %d to value %d", type, value);
5347
5348 if ( !(value <= 65535 && value >= 0) )
5349 return;
5350
5351 found_channel = bvw_get_color_balance_channel (GST_COLOR_BALANCE (bvw->play), type);
5352 i_value = floor (0.5 + value * ((double) found_channel->max_value -
5353 found_channel->min_value) / 65535 + found_channel->min_value);
5354
5355 GST_DEBUG ("channel %s: set to %d/65535", found_channel->label, value);
5356
5357 gst_color_balance_set_value (GST_COLOR_BALANCE (bvw->play), found_channel, i_value);
5358
5359 GST_DEBUG ("channel %s: val=%d, min=%d, max=%d", found_channel->label,
5360 i_value, found_channel->min_value, found_channel->max_value);
5361
5362 g_object_unref (found_channel);
5363
5364 /* Notify of the property change */
5365 g_object_notify (G_OBJECT (bvw), video_props_str[type]);
5366
5367 GST_DEBUG ("setting value %d", value);
5368 }
5369
5370 /**
5371 * bacon_video_widget_get_position:
5372 * @bvw: a #BaconVideoWidget
5373 *
5374 * Returns the current position in the stream, as a value between
5375 * <code class="literal">0</code> and <code class="literal">1</code>.
5376 *
5377 * Return value: the current position, or <code class="literal">-1</code>
5378 **/
5379 double
bacon_video_widget_get_position(BaconVideoWidget * bvw)5380 bacon_video_widget_get_position (BaconVideoWidget * bvw)
5381 {
5382 g_return_val_if_fail (BACON_IS_VIDEO_WIDGET (bvw), -1);
5383 return bvw->current_position;
5384 }
5385
5386 /**
5387 * bacon_video_widget_get_current_time:
5388 * @bvw: a #BaconVideoWidget
5389 *
5390 * Returns the current position in the stream, as the time (in milliseconds)
5391 * since the beginning of the stream.
5392 *
5393 * Return value: time since the beginning of the stream, in milliseconds, or <code class="literal">-1</code>
5394 **/
5395 gint64
bacon_video_widget_get_current_time(BaconVideoWidget * bvw)5396 bacon_video_widget_get_current_time (BaconVideoWidget * bvw)
5397 {
5398 g_return_val_if_fail (BACON_IS_VIDEO_WIDGET (bvw), -1);
5399 return bvw->current_time;
5400 }
5401
5402 /**
5403 * bacon_video_widget_get_stream_length:
5404 * @bvw: a #BaconVideoWidget
5405 *
5406 * Returns the total length of the stream, in milliseconds.
5407 *
5408 * Return value: the stream length, in milliseconds, or <code class="literal">-1</code>
5409 **/
5410 gint64
bacon_video_widget_get_stream_length(BaconVideoWidget * bvw)5411 bacon_video_widget_get_stream_length (BaconVideoWidget * bvw)
5412 {
5413 g_return_val_if_fail (BACON_IS_VIDEO_WIDGET (bvw), -1);
5414
5415 if (bvw->stream_length == 0 && bvw->play != NULL) {
5416 gint64 len = -1;
5417
5418 if (gst_element_query_duration (bvw->play, GST_FORMAT_TIME, &len) && len != -1) {
5419 bvw->stream_length = len / GST_MSECOND;
5420 }
5421 }
5422
5423 return bvw->stream_length;
5424 }
5425
5426 /**
5427 * bacon_video_widget_is_playing:
5428 * @bvw: a #BaconVideoWidget
5429 *
5430 * Returns whether the widget is currently playing a stream.
5431 *
5432 * Return value: %TRUE if a stream is playing, %FALSE otherwise
5433 **/
5434 gboolean
bacon_video_widget_is_playing(BaconVideoWidget * bvw)5435 bacon_video_widget_is_playing (BaconVideoWidget * bvw)
5436 {
5437 gboolean ret;
5438
5439 g_return_val_if_fail (BACON_IS_VIDEO_WIDGET (bvw), FALSE);
5440 g_return_val_if_fail (GST_IS_ELEMENT (bvw->play), FALSE);
5441
5442 ret = (bvw->target_state == GST_STATE_PLAYING);
5443 GST_LOG ("%splaying", (ret) ? "" : "not ");
5444
5445 return ret;
5446 }
5447
5448 /**
5449 * bacon_video_widget_is_seekable:
5450 * @bvw: a #BaconVideoWidget
5451 *
5452 * Returns whether seeking is possible in the current stream.
5453 *
5454 * If no stream is loaded, %FALSE is returned.
5455 *
5456 * Return value: %TRUE if the stream is seekable, %FALSE otherwise
5457 **/
5458 gboolean
bacon_video_widget_is_seekable(BaconVideoWidget * bvw)5459 bacon_video_widget_is_seekable (BaconVideoWidget * bvw)
5460 {
5461 gboolean res;
5462 gint old_seekable;
5463
5464 g_return_val_if_fail (BACON_IS_VIDEO_WIDGET (bvw), FALSE);
5465 g_return_val_if_fail (GST_IS_ELEMENT (bvw->play), FALSE);
5466
5467 if (bvw->mrl == NULL)
5468 return FALSE;
5469
5470 old_seekable = bvw->seekable;
5471
5472 if (bvw->is_menu != FALSE)
5473 return FALSE;
5474
5475 if (bvw->seekable == -1) {
5476 GstQuery *query;
5477
5478 query = gst_query_new_seeking (GST_FORMAT_TIME);
5479 if (gst_element_query (bvw->play, query)) {
5480 gst_query_parse_seeking (query, NULL, &res, NULL, NULL);
5481 GST_DEBUG ("seeking query says the stream is%s seekable", (res) ? "" : " not");
5482 bvw->seekable = (res) ? 1 : 0;
5483 } else {
5484 GST_DEBUG ("seeking query failed");
5485 }
5486 gst_query_unref (query);
5487 }
5488
5489 if (bvw->seekable != -1) {
5490 res = (bvw->seekable != 0);
5491 goto done;
5492 }
5493
5494 /* Try to guess from duration. This is very unreliable
5495 * though so don't save it */
5496 if (bvw->stream_length == 0) {
5497 res = (bacon_video_widget_get_stream_length (bvw) > 0);
5498 } else {
5499 res = (bvw->stream_length > 0);
5500 }
5501
5502 done:
5503
5504 if (old_seekable != bvw->seekable)
5505 g_object_notify (G_OBJECT (bvw), "seekable");
5506
5507 GST_DEBUG ("stream is%s seekable", (res) ? "" : " not");
5508 return res;
5509 }
5510
5511 static gint
bvw_get_current_stream_num(BaconVideoWidget * bvw,const gchar * stream_type)5512 bvw_get_current_stream_num (BaconVideoWidget * bvw,
5513 const gchar *stream_type)
5514 {
5515 gchar *lower, *cur_prop_str;
5516 gint stream_num = -1;
5517
5518 if (bvw->play == NULL)
5519 return stream_num;
5520
5521 lower = g_ascii_strdown (stream_type, -1);
5522 cur_prop_str = g_strconcat ("current-", lower, NULL);
5523 g_object_get (bvw->play, cur_prop_str, &stream_num, NULL);
5524 g_free (cur_prop_str);
5525 g_free (lower);
5526
5527 GST_LOG ("current %s stream: %d", stream_type, stream_num);
5528 return stream_num;
5529 }
5530
5531 static GstTagList *
bvw_get_tags_of_current_stream(BaconVideoWidget * bvw,const gchar * stream_type)5532 bvw_get_tags_of_current_stream (BaconVideoWidget * bvw,
5533 const gchar *stream_type)
5534 {
5535 GstTagList *tags = NULL;
5536 gint stream_num = -1;
5537 gchar *lower, *cur_sig_str;
5538
5539 stream_num = bvw_get_current_stream_num (bvw, stream_type);
5540 if (stream_num < 0)
5541 return NULL;
5542
5543 lower = g_ascii_strdown (stream_type, -1);
5544 cur_sig_str = g_strconcat ("get-", lower, "-tags", NULL);
5545 g_signal_emit_by_name (bvw->play, cur_sig_str, stream_num, &tags);
5546 g_free (cur_sig_str);
5547 g_free (lower);
5548
5549 GST_LOG ("current %s stream tags %" GST_PTR_FORMAT, stream_type, tags);
5550 return tags;
5551 }
5552
5553 static GstCaps *
bvw_get_caps_of_current_stream(BaconVideoWidget * bvw,const gchar * stream_type)5554 bvw_get_caps_of_current_stream (BaconVideoWidget * bvw,
5555 const gchar *stream_type)
5556 {
5557 GstCaps *caps = NULL;
5558 gint stream_num = -1;
5559 GstPad *current;
5560 gchar *lower, *cur_sig_str;
5561
5562 stream_num = bvw_get_current_stream_num (bvw, stream_type);
5563 if (stream_num < 0)
5564 return NULL;
5565
5566 lower = g_ascii_strdown (stream_type, -1);
5567 cur_sig_str = g_strconcat ("get-", lower, "-pad", NULL);
5568 g_signal_emit_by_name (bvw->play, cur_sig_str, stream_num, ¤t);
5569 g_free (cur_sig_str);
5570 g_free (lower);
5571
5572 if (current != NULL) {
5573 caps = gst_pad_get_current_caps (current);
5574 gst_object_unref (current);
5575 }
5576 GST_LOG ("current %s stream caps: %" GST_PTR_FORMAT, stream_type, caps);
5577 return caps;
5578 }
5579
5580 static gboolean
audio_caps_have_LFE(GstStructure * s)5581 audio_caps_have_LFE (GstStructure * s)
5582 {
5583 guint64 mask;
5584 int channels;
5585
5586 if (!gst_structure_get_int (s, "channels", &channels) ||
5587 channels == 0)
5588 return FALSE;
5589
5590 if (!gst_structure_get (s, "channel-mask", GST_TYPE_BITMASK, &mask, NULL))
5591 return FALSE;
5592
5593 if (mask & GST_AUDIO_CHANNEL_POSITION_LFE1 ||
5594 mask & GST_AUDIO_CHANNEL_POSITION_LFE2)
5595 return TRUE;
5596
5597 return FALSE;
5598 }
5599
5600 static void
bacon_video_widget_get_metadata_string(BaconVideoWidget * bvw,BvwMetadataType type,GValue * value)5601 bacon_video_widget_get_metadata_string (BaconVideoWidget * bvw,
5602 BvwMetadataType type,
5603 GValue * value)
5604 {
5605 char *string = NULL;
5606 gboolean res = FALSE;
5607
5608 g_value_init (value, G_TYPE_STRING);
5609
5610 if (bvw->play == NULL) {
5611 g_value_set_string (value, NULL);
5612 return;
5613 }
5614
5615 switch (type) {
5616 case BVW_INFO_TITLE:
5617 if (bvw->tagcache != NULL) {
5618 res = gst_tag_list_get_string_index (bvw->tagcache,
5619 GST_TAG_TITLE, 0, &string);
5620 }
5621 break;
5622 case BVW_INFO_ARTIST:
5623 if (bvw->tagcache != NULL) {
5624 res = gst_tag_list_get_string_index (bvw->tagcache,
5625 GST_TAG_ARTIST, 0, &string);
5626 }
5627 break;
5628 case BVW_INFO_YEAR:
5629 if (bvw->tagcache != NULL) {
5630 GDate *date;
5631 GstDateTime *datetime;
5632
5633 if ((res = gst_tag_list_get_date (bvw->tagcache,
5634 GST_TAG_DATE, &date))) {
5635 string = g_strdup_printf ("%d", g_date_get_year (date));
5636 g_date_free (date);
5637 } else if ((res = gst_tag_list_get_date_time (bvw->tagcache,
5638 GST_TAG_DATE_TIME, &datetime))) {
5639 string = g_strdup_printf ("%d", gst_date_time_get_year (datetime));
5640 gst_date_time_unref (datetime);
5641 }
5642 }
5643 break;
5644 case BVW_INFO_COMMENT:
5645 if (bvw->tagcache != NULL) {
5646 res = gst_tag_list_get_string_index (bvw->tagcache,
5647 GST_TAG_COMMENT, 0, &string);
5648
5649 /* Use the Comment; if that fails, use Description as specified by:
5650 * http://xiph.org/vorbis/doc/v-comment.html */
5651 if (!res) {
5652 res = gst_tag_list_get_string_index (bvw->tagcache,
5653 GST_TAG_DESCRIPTION, 0, &string);
5654 }
5655 }
5656 break;
5657 case BVW_INFO_ALBUM:
5658 if (bvw->tagcache != NULL) {
5659 res = gst_tag_list_get_string_index (bvw->tagcache,
5660 GST_TAG_ALBUM, 0, &string);
5661 }
5662 break;
5663 case BVW_INFO_CONTAINER:
5664 if (bvw->tagcache != NULL) {
5665 res = gst_tag_list_get_string_index (bvw->tagcache,
5666 GST_TAG_CONTAINER_FORMAT, 0, &string);
5667 }
5668 break;
5669 case BVW_INFO_VIDEO_CODEC: {
5670 GstTagList *tags;
5671
5672 /* try to get this from the stream info first */
5673 if ((tags = bvw_get_tags_of_current_stream (bvw, "video"))) {
5674 res = gst_tag_list_get_string (tags, GST_TAG_CODEC, &string);
5675 gst_tag_list_unref (tags);
5676 }
5677
5678 /* if that didn't work, try the aggregated tags */
5679 if (!res && bvw->tagcache != NULL) {
5680 res = gst_tag_list_get_string (bvw->tagcache,
5681 GST_TAG_VIDEO_CODEC, &string);
5682 }
5683 break;
5684 }
5685 case BVW_INFO_AUDIO_CODEC: {
5686 GstTagList *tags;
5687
5688 /* try to get this from the stream info first */
5689 if ((tags = bvw_get_tags_of_current_stream (bvw, "audio"))) {
5690 res = gst_tag_list_get_string (tags, GST_TAG_CODEC, &string);
5691 gst_tag_list_unref (tags);
5692 }
5693
5694 /* if that didn't work, try the aggregated tags */
5695 if (!res && bvw->tagcache != NULL) {
5696 res = gst_tag_list_get_string (bvw->tagcache,
5697 GST_TAG_AUDIO_CODEC, &string);
5698 }
5699 break;
5700 }
5701 case BVW_INFO_AUDIO_CHANNELS: {
5702 GstStructure *s;
5703 GstCaps *caps;
5704
5705 caps = bvw_get_caps_of_current_stream (bvw, "audio");
5706 if (caps) {
5707 gint channels = 0;
5708
5709 s = gst_caps_get_structure (caps, 0);
5710 if ((res = gst_structure_get_int (s, "channels", &channels))) {
5711 /* FIXME: do something more sophisticated - but what? */
5712 if (channels > 2 && audio_caps_have_LFE (s)) {
5713 string = g_strdup_printf ("%s %d.1", _("Surround"), channels - 1);
5714 } else if (channels == 1) {
5715 string = g_strdup (_("Mono"));
5716 } else if (channels == 2) {
5717 string = g_strdup (_("Stereo"));
5718 } else {
5719 string = g_strdup_printf ("%d", channels);
5720 }
5721 }
5722 gst_caps_unref (caps);
5723 }
5724 break;
5725 }
5726
5727 case BVW_INFO_DURATION:
5728 case BVW_INFO_TRACK_NUMBER:
5729 case BVW_INFO_COVER:
5730 case BVW_INFO_HAS_VIDEO:
5731 case BVW_INFO_DIMENSION_X:
5732 case BVW_INFO_DIMENSION_Y:
5733 case BVW_INFO_VIDEO_BITRATE:
5734 case BVW_INFO_FPS:
5735 case BVW_INFO_HAS_AUDIO:
5736 case BVW_INFO_AUDIO_BITRATE:
5737 case BVW_INFO_AUDIO_SAMPLE_RATE:
5738 /* Not strings */
5739 default:
5740 g_assert_not_reached ();
5741 }
5742
5743 /* Remove line feeds */
5744 if (string && strstr (string, "\n") != NULL)
5745 g_strdelimit (string, "\n", ' ');
5746 if (string != NULL)
5747 string = g_strstrip (string);
5748
5749 if (res && string && *string != '\0' && g_utf8_validate (string, -1, NULL)) {
5750 g_value_take_string (value, string);
5751 GST_DEBUG ("%s = '%s'", get_type_name (BVW_TYPE_METADATA_TYPE, type), string);
5752 } else {
5753 g_value_set_string (value, NULL);
5754 g_free (string);
5755 }
5756
5757 return;
5758 }
5759
5760 static void
bacon_video_widget_get_metadata_int(BaconVideoWidget * bvw,BvwMetadataType type,GValue * value)5761 bacon_video_widget_get_metadata_int (BaconVideoWidget * bvw,
5762 BvwMetadataType type,
5763 GValue * value)
5764 {
5765 int integer = 0;
5766
5767 g_value_init (value, G_TYPE_INT);
5768
5769 if (bvw->play == NULL) {
5770 g_value_set_int (value, 0);
5771 return;
5772 }
5773
5774 switch (type) {
5775 case BVW_INFO_DURATION:
5776 integer = bacon_video_widget_get_stream_length (bvw) / 1000;
5777 break;
5778 case BVW_INFO_TRACK_NUMBER:
5779 if (bvw->tagcache == NULL)
5780 break;
5781 if (!gst_tag_list_get_uint (bvw->tagcache,
5782 GST_TAG_TRACK_NUMBER, (guint *) &integer))
5783 integer = 0;
5784 break;
5785 case BVW_INFO_DIMENSION_X:
5786 integer = bvw->video_width;
5787 break;
5788 case BVW_INFO_DIMENSION_Y:
5789 integer = bvw->video_height;
5790 break;
5791 case BVW_INFO_AUDIO_BITRATE:
5792 if (bvw->audiotags == NULL)
5793 break;
5794 if (gst_tag_list_get_uint (bvw->audiotags, GST_TAG_BITRATE,
5795 (guint *)&integer) ||
5796 gst_tag_list_get_uint (bvw->audiotags, GST_TAG_NOMINAL_BITRATE,
5797 (guint *)&integer)) {
5798 integer /= 1000;
5799 }
5800 break;
5801 case BVW_INFO_VIDEO_BITRATE:
5802 if (bvw->videotags == NULL)
5803 break;
5804 if (gst_tag_list_get_uint (bvw->videotags, GST_TAG_BITRATE,
5805 (guint *)&integer) ||
5806 gst_tag_list_get_uint (bvw->videotags, GST_TAG_NOMINAL_BITRATE,
5807 (guint *)&integer)) {
5808 integer /= 1000;
5809 }
5810 break;
5811 case BVW_INFO_AUDIO_SAMPLE_RATE: {
5812 GstStructure *s;
5813 GstCaps *caps;
5814
5815 caps = bvw_get_caps_of_current_stream (bvw, "audio");
5816 if (caps) {
5817 s = gst_caps_get_structure (caps, 0);
5818 gst_structure_get_int (s, "rate", &integer);
5819 gst_caps_unref (caps);
5820 }
5821 break;
5822 }
5823
5824 case BVW_INFO_TITLE:
5825 case BVW_INFO_ARTIST:
5826 case BVW_INFO_YEAR:
5827 case BVW_INFO_COMMENT:
5828 case BVW_INFO_ALBUM:
5829 case BVW_INFO_COVER:
5830 case BVW_INFO_CONTAINER:
5831 case BVW_INFO_HAS_VIDEO:
5832 case BVW_INFO_VIDEO_CODEC:
5833 case BVW_INFO_HAS_AUDIO:
5834 case BVW_INFO_AUDIO_CODEC:
5835 case BVW_INFO_AUDIO_CHANNELS:
5836 /* Not ints */
5837 default:
5838 g_assert_not_reached ();
5839 }
5840
5841 g_value_set_int (value, integer);
5842 GST_DEBUG ("%s = %d", get_type_name (BVW_TYPE_METADATA_TYPE, type), integer);
5843
5844 return;
5845 }
5846
5847 static void
bacon_video_widget_get_metadata_bool(BaconVideoWidget * bvw,BvwMetadataType type,GValue * value)5848 bacon_video_widget_get_metadata_bool (BaconVideoWidget * bvw,
5849 BvwMetadataType type,
5850 GValue * value)
5851 {
5852 gboolean boolean = FALSE;
5853
5854 g_value_init (value, G_TYPE_BOOLEAN);
5855
5856 if (bvw->play == NULL) {
5857 g_value_set_boolean (value, FALSE);
5858 return;
5859 }
5860
5861 GST_DEBUG ("tagcache = %" GST_PTR_FORMAT, bvw->tagcache);
5862 GST_DEBUG ("videotags = %" GST_PTR_FORMAT, bvw->videotags);
5863 GST_DEBUG ("audiotags = %" GST_PTR_FORMAT, bvw->audiotags);
5864
5865 switch (type)
5866 {
5867 case BVW_INFO_HAS_VIDEO:
5868 boolean = bvw->media_has_video;
5869 break;
5870 case BVW_INFO_HAS_AUDIO:
5871 boolean = bvw->media_has_audio;
5872 break;
5873
5874 case BVW_INFO_TITLE:
5875 case BVW_INFO_ARTIST:
5876 case BVW_INFO_YEAR:
5877 case BVW_INFO_COMMENT:
5878 case BVW_INFO_ALBUM:
5879 case BVW_INFO_DURATION:
5880 case BVW_INFO_TRACK_NUMBER:
5881 case BVW_INFO_COVER:
5882 case BVW_INFO_CONTAINER:
5883 case BVW_INFO_DIMENSION_X:
5884 case BVW_INFO_DIMENSION_Y:
5885 case BVW_INFO_VIDEO_BITRATE:
5886 case BVW_INFO_VIDEO_CODEC:
5887 case BVW_INFO_FPS:
5888 case BVW_INFO_AUDIO_BITRATE:
5889 case BVW_INFO_AUDIO_CODEC:
5890 case BVW_INFO_AUDIO_SAMPLE_RATE:
5891 case BVW_INFO_AUDIO_CHANNELS:
5892 /* Not bools */
5893 default:
5894 g_assert_not_reached ();
5895 }
5896
5897 g_value_set_boolean (value, boolean);
5898 GST_DEBUG ("%s = %s", get_type_name (BVW_TYPE_METADATA_TYPE, type), (boolean) ? "yes" : "no");
5899
5900 return;
5901 }
5902
5903 /**
5904 * bacon_video_widget_get_metadata:
5905 * @bvw: a #BaconVideoWidget
5906 * @type: the type of metadata to return
5907 * @value: a #GValue
5908 *
5909 * Provides metadata of the given @type about the current stream in @value.
5910 *
5911 * Free the #GValue with g_value_unset().
5912 **/
5913 void
bacon_video_widget_get_metadata(BaconVideoWidget * bvw,BvwMetadataType type,GValue * value)5914 bacon_video_widget_get_metadata (BaconVideoWidget * bvw,
5915 BvwMetadataType type,
5916 GValue * value)
5917 {
5918 g_return_if_fail (BACON_IS_VIDEO_WIDGET (bvw));
5919 g_return_if_fail (GST_IS_ELEMENT (bvw->play));
5920
5921 switch (type)
5922 {
5923 case BVW_INFO_TITLE:
5924 case BVW_INFO_ARTIST:
5925 case BVW_INFO_YEAR:
5926 case BVW_INFO_COMMENT:
5927 case BVW_INFO_ALBUM:
5928 case BVW_INFO_CONTAINER:
5929 case BVW_INFO_VIDEO_CODEC:
5930 case BVW_INFO_AUDIO_CODEC:
5931 case BVW_INFO_AUDIO_CHANNELS:
5932 bacon_video_widget_get_metadata_string (bvw, type, value);
5933 break;
5934 case BVW_INFO_DURATION:
5935 case BVW_INFO_DIMENSION_X:
5936 case BVW_INFO_DIMENSION_Y:
5937 case BVW_INFO_AUDIO_BITRATE:
5938 case BVW_INFO_VIDEO_BITRATE:
5939 case BVW_INFO_TRACK_NUMBER:
5940 case BVW_INFO_AUDIO_SAMPLE_RATE:
5941 bacon_video_widget_get_metadata_int (bvw, type, value);
5942 break;
5943 case BVW_INFO_HAS_VIDEO:
5944 case BVW_INFO_HAS_AUDIO:
5945 bacon_video_widget_get_metadata_bool (bvw, type, value);
5946 break;
5947 case BVW_INFO_COVER:
5948 {
5949 GdkPixbuf *pixbuf;
5950
5951 if (!bvw->tagcache)
5952 break;
5953
5954 pixbuf = totem_gst_tag_list_get_cover (bvw->tagcache);
5955 if (pixbuf) {
5956 g_value_init (value, GDK_TYPE_PIXBUF);
5957 g_value_take_object (value, pixbuf);
5958 }
5959 }
5960 break;
5961 case BVW_INFO_FPS:
5962 {
5963 float fps = 0.0;
5964
5965 if (bvw->video_fps_d > 0)
5966 fps = (float) bvw->video_fps_n / (float) bvw->video_fps_d;
5967
5968 g_value_init (value, G_TYPE_FLOAT);
5969 g_value_set_float (value, fps);
5970 }
5971 break;
5972 default:
5973 g_return_if_reached ();
5974 }
5975
5976 return;
5977 }
5978
5979 /* Screenshot functions */
5980
5981 /**
5982 * bacon_video_widget_can_get_frames:
5983 * @bvw: a #BaconVideoWidget
5984 * @error: a #GError, or %NULL
5985 *
5986 * Determines whether individual frames from the current stream can
5987 * be returned using bacon_video_widget_get_current_frame().
5988 *
5989 * Frames cannot be returned for audio-only streams, unless visualisations
5990 * are enabled.
5991 *
5992 * Return value: %TRUE if frames can be captured, %FALSE otherwise
5993 **/
5994 gboolean
bacon_video_widget_can_get_frames(BaconVideoWidget * bvw,GError ** error)5995 bacon_video_widget_can_get_frames (BaconVideoWidget * bvw, GError ** error)
5996 {
5997 g_return_val_if_fail (BACON_IS_VIDEO_WIDGET (bvw), FALSE);
5998 g_return_val_if_fail (GST_IS_ELEMENT (bvw->play), FALSE);
5999
6000 /* check for video */
6001 if (!bvw->media_has_video) {
6002 g_set_error_literal (error, BVW_ERROR, BVW_ERROR_CANNOT_CAPTURE,
6003 _("Media contains no supported video streams."));
6004 return FALSE;
6005 }
6006
6007 return TRUE;
6008 }
6009
6010 /**
6011 * bacon_video_widget_get_current_frame:
6012 * @bvw: a #BaconVideoWidget
6013 *
6014 * Returns a #GdkPixbuf containing the current frame from the playing
6015 * stream. This will wait for any pending seeks to complete before
6016 * capturing the frame.
6017 *
6018 * Return value: the current frame, or %NULL; unref with g_object_unref()
6019 **/
6020 GdkPixbuf *
bacon_video_widget_get_current_frame(BaconVideoWidget * bvw)6021 bacon_video_widget_get_current_frame (BaconVideoWidget * bvw)
6022 {
6023 g_return_val_if_fail (BACON_IS_VIDEO_WIDGET (bvw), NULL);
6024 g_return_val_if_fail (GST_IS_ELEMENT (bvw->play), NULL);
6025
6026 /* no video info */
6027 if (!bvw->video_width || !bvw->video_height) {
6028 GST_DEBUG ("Could not take screenshot: %s", "no video info");
6029 g_warning ("Could not take screenshot: %s", "no video info");
6030 return NULL;
6031 }
6032
6033 return totem_gst_playbin_get_frame (bvw->play);
6034 }
6035
6036 /* =========================================== */
6037 /* */
6038 /* Widget typing & Creation */
6039 /* */
6040 /* =========================================== */
6041
6042 /**
6043 * bacon_video_widget_get_option_group:
6044 *
6045 * Returns the #GOptionGroup containing command-line options for
6046 * #BaconVideoWidget.
6047 *
6048 * Applications must call either this exactly once.
6049 *
6050 * Return value: a #GOptionGroup giving command-line options for #BaconVideoWidget
6051 **/
6052 GOptionGroup*
bacon_video_widget_get_option_group(void)6053 bacon_video_widget_get_option_group (void)
6054 {
6055 return gst_init_get_option_group ();
6056 }
6057
6058 GQuark
bacon_video_widget_error_quark(void)6059 bacon_video_widget_error_quark (void)
6060 {
6061 static GQuark q; /* 0 */
6062
6063 if (G_UNLIKELY (q == 0)) {
6064 q = g_quark_from_static_string ("bvw-error-quark");
6065 }
6066 return q;
6067 }
6068
6069 static gboolean
bvw_set_playback_direction(BaconVideoWidget * bvw,gboolean forward)6070 bvw_set_playback_direction (BaconVideoWidget *bvw, gboolean forward)
6071 {
6072 gboolean is_forward;
6073 gboolean retval;
6074 float target_rate;
6075 GstEvent *event;
6076 gint64 cur = 0;
6077
6078 is_forward = (bvw->rate > 0.0);
6079 if (forward == is_forward)
6080 return TRUE;
6081
6082 retval = FALSE;
6083 target_rate = (forward ? FORWARD_RATE : REVERSE_RATE);
6084
6085 if (gst_element_query_position (bvw->play, GST_FORMAT_TIME, &cur)) {
6086 GST_DEBUG ("Setting playback direction to %s at %"G_GINT64_FORMAT"", DIRECTION_STR, cur);
6087 event = gst_event_new_seek (target_rate,
6088 GST_FORMAT_TIME, GST_SEEK_FLAG_FLUSH | GST_SEEK_FLAG_ACCURATE,
6089 GST_SEEK_TYPE_SET, forward ? cur : G_GINT64_CONSTANT (0),
6090 GST_SEEK_TYPE_SET, forward ? G_GINT64_CONSTANT (0) : cur);
6091 if (gst_element_send_event (bvw->play, event) == FALSE) {
6092 GST_WARNING ("Failed to set playback direction to %s", DIRECTION_STR);
6093 } else {
6094 gst_element_get_state (bvw->play, NULL, NULL, GST_CLOCK_TIME_NONE);
6095 bvw->rate = target_rate;
6096 retval = TRUE;
6097 }
6098 } else {
6099 GST_LOG ("Failed to query position to set playback to %s", DIRECTION_STR);
6100 }
6101
6102 return retval;
6103 }
6104
6105 static gboolean
navigation_event(ClutterActor * actor,ClutterEvent * event,BaconVideoWidget * bvw)6106 navigation_event (ClutterActor *actor,
6107 ClutterEvent *event,
6108 BaconVideoWidget *bvw)
6109 {
6110 ClutterGstFrame *frame =
6111 clutter_gst_video_sink_get_frame (CLUTTER_GST_VIDEO_SINK (bvw->video_sink));
6112 gfloat actor_width, actor_height;
6113 gfloat x, y;
6114
6115 if (frame == NULL)
6116 return CLUTTER_EVENT_PROPAGATE;
6117
6118 /* Get event coordinates into the actor's coordinates. */
6119 clutter_event_get_coords (event, &x, &y);
6120 clutter_actor_transform_stage_point (actor, x, y, &x, &y);
6121
6122 clutter_actor_get_size (actor, &actor_width, &actor_height);
6123
6124 /* Convert event's coordinates into the frame's coordinates. */
6125 x = x * frame->resolution.width / actor_width;
6126 y = y * frame->resolution.height / actor_height;
6127
6128 if (event->type == CLUTTER_MOTION) {
6129 gst_navigation_send_mouse_event (GST_NAVIGATION (bvw->video_sink),
6130 "mouse-move", 0, x, y);
6131 } else if (event->type == CLUTTER_BUTTON_PRESS ||
6132 event->type == CLUTTER_BUTTON_RELEASE) {
6133 ClutterButtonEvent *bevent = (ClutterButtonEvent *) event;
6134 const char *type = (event->type == CLUTTER_BUTTON_PRESS) ?
6135 "mouse-button-press" : "mouse-button-release";
6136 gst_navigation_send_mouse_event (GST_NAVIGATION (bvw->video_sink), type,
6137 bevent->button, x, y);
6138 }
6139
6140 return CLUTTER_EVENT_PROPAGATE;
6141 }
6142
6143 static void
listen_navigation_events(ClutterActor * actor,BaconVideoWidget * bvw)6144 listen_navigation_events (ClutterActor *actor,
6145 BaconVideoWidget *bvw)
6146 {
6147 const char * const events[] = {
6148 "button-press-event",
6149 "button-release-event",
6150 "motion-event"
6151 };
6152 guint i;
6153
6154 for (i = 0; i < G_N_ELEMENTS (events); i++)
6155 g_signal_connect (actor, events[i], G_CALLBACK (navigation_event), bvw);
6156 }
6157
6158 static GstElement *
element_make_or_warn(const char * plugin,const char * name)6159 element_make_or_warn (const char *plugin,
6160 const char *name)
6161 {
6162 GstElement *element;
6163 element = gst_element_factory_make (plugin, name);
6164 if (!element)
6165 g_warning ("Element '%s' is missing, verify your installation", plugin);
6166 return element;
6167 }
6168
6169 static gboolean
bacon_video_widget_initable_init(GInitable * initable,GCancellable * cancellable,GError ** error)6170 bacon_video_widget_initable_init (GInitable *initable,
6171 GCancellable *cancellable,
6172 GError **error)
6173 {
6174 BaconVideoWidget *bvw;
6175 GstElement *audio_sink = NULL;
6176 gchar *version_str;
6177 GstPlayFlags flags;
6178 ClutterActor *layout;
6179 GstElement *audio_bin;
6180 GstPad *audio_pad;
6181 ClutterAction *action;
6182 GObject *item;
6183 char *template;
6184
6185 bvw = BACON_VIDEO_WIDGET (initable);
6186
6187 #ifndef GST_DISABLE_GST_DEBUG
6188 if (_totem_gst_debug_cat == NULL) {
6189 GST_DEBUG_CATEGORY_INIT (_totem_gst_debug_cat, "totem", 0,
6190 "Totem GStreamer Backend");
6191 }
6192 #endif
6193
6194 version_str = gst_version_string ();
6195 GST_DEBUG ("Initialised %s", version_str);
6196 g_free (version_str);
6197
6198 gst_pb_utils_init ();
6199
6200 /* Instantiate all the fallible plugins */
6201 bvw->play = element_make_or_warn ("playbin", "play");
6202 bvw->audio_pitchcontrol = element_make_or_warn ("scaletempo", "scaletempo");
6203 bvw->video_sink = GST_ELEMENT (clutter_gst_video_sink_new ());
6204 audio_sink = element_make_or_warn ("autoaudiosink", "audio-sink");
6205
6206 if (!bvw->play ||
6207 !bvw->audio_pitchcontrol ||
6208 !bvw->video_sink ||
6209 !audio_sink) {
6210 if (bvw->video_sink)
6211 g_object_ref_sink (bvw->video_sink);
6212 if (audio_sink)
6213 g_object_ref_sink (audio_sink);
6214 g_set_error_literal (error, BVW_ERROR, BVW_ERROR_PLUGIN_LOAD,
6215 _("Some necessary plug-ins are missing. "
6216 "Make sure that the program is correctly installed."));
6217 return FALSE;
6218 }
6219
6220 bvw->bus = gst_element_get_bus (bvw->play);
6221
6222 /* Add the download flag, for streaming buffering,
6223 * and the deinterlace flag, for video only */
6224 g_object_get (bvw->play, "flags", &flags, NULL);
6225 flags |= GST_PLAY_FLAG_DOWNLOAD | GST_PLAY_FLAG_DEINTERLACE;
6226 g_object_set (bvw->play, "flags", flags, NULL);
6227
6228 /* Keep in sync with playbin_element_setup_cb() */
6229 template = g_build_filename (g_get_user_cache_dir (), "totem", "stream-buffer", NULL);
6230 g_mkdir_with_parents (template, 0700);
6231 g_free (template);
6232
6233 gst_bus_add_signal_watch (bvw->bus);
6234
6235 bvw->sig_bus_async =
6236 g_signal_connect (bvw->bus, "message",
6237 G_CALLBACK (bvw_bus_message_cb),
6238 bvw);
6239
6240 bvw->speakersetup = BVW_AUDIO_SOUND_STEREO;
6241 bvw->ratio_type = BVW_RATIO_AUTO;
6242
6243 bvw->cursor_shown = TRUE;
6244 bvw->logo_mode = FALSE;
6245
6246 bvw->stage = gtk_clutter_embed_get_stage (GTK_CLUTTER_EMBED (bvw));
6247 clutter_actor_set_text_direction (bvw->stage,
6248 CLUTTER_TEXT_DIRECTION_LTR);
6249 clutter_actor_set_layout_manager (bvw->stage,
6250 clutter_bin_layout_new (CLUTTER_BIN_ALIGNMENT_FILL, CLUTTER_BIN_ALIGNMENT_FILL));
6251 clutter_actor_set_name (bvw->stage, "stage");
6252 clutter_actor_set_background_color (bvw->stage, CLUTTER_COLOR_Black);
6253
6254 /* Video sink, with aspect frame */
6255 bvw->texture = g_object_new (CLUTTER_TYPE_ACTOR,
6256 "content", g_object_new (CLUTTER_GST_TYPE_CONTENT,
6257 "sink", bvw->video_sink,
6258 NULL),
6259 "name", "texture",
6260 "reactive", TRUE,
6261 NULL);
6262 listen_navigation_events (bvw->texture, bvw);
6263
6264 /* The logo */
6265 bvw->logo_frame = clutter_actor_new ();
6266 clutter_actor_set_name (bvw->logo_frame, "logo-frame");
6267 bvw->logo = clutter_image_new ();
6268 clutter_actor_set_content (bvw->logo_frame, bvw->logo);
6269 clutter_actor_set_content_gravity (bvw->logo_frame, CLUTTER_CONTENT_GRAVITY_RESIZE_ASPECT);
6270 clutter_actor_add_child (bvw->stage, bvw->logo_frame);
6271 clutter_actor_hide (CLUTTER_ACTOR (bvw->logo_frame));
6272
6273 /* The video */
6274 bvw->frame = totem_aspect_frame_new ();
6275 clutter_actor_set_name (bvw->frame, "frame");
6276 totem_aspect_frame_set_child (TOTEM_ASPECT_FRAME (bvw->frame), bvw->texture);
6277
6278 clutter_actor_add_child (bvw->stage, bvw->frame);
6279
6280 clutter_actor_set_child_above_sibling (bvw->stage,
6281 bvw->logo_frame,
6282 bvw->frame);
6283
6284 /* The video's actions */
6285 action = clutter_tap_action_new ();
6286 clutter_actor_add_action (bvw->texture, action);
6287 g_signal_connect (action, "tap",
6288 G_CALLBACK (bacon_video_widget_tap), bvw);
6289
6290 action = clutter_swipe_action_new ();
6291 clutter_gesture_action_set_threshold_trigger_distance (CLUTTER_GESTURE_ACTION (action), 80.0, 80.0);
6292 clutter_actor_add_action (bvw->texture, action);
6293 g_signal_connect (action, "swipe",
6294 G_CALLBACK (bacon_video_widget_swipe), bvw);
6295
6296 /* The spinner */
6297 bvw->spinner = bacon_video_spinner_actor_new ();
6298 clutter_actor_set_name (bvw->spinner, "spinner");
6299 clutter_actor_add_child (bvw->stage, bvw->spinner);
6300 clutter_actor_set_child_above_sibling (bvw->stage,
6301 bvw->spinner,
6302 bvw->frame);
6303 clutter_actor_hide (bvw->spinner);
6304
6305 /* Fullscreen header controls */
6306 bvw->header_controls = gtk_clutter_actor_new ();
6307 clutter_actor_set_opacity (bvw->header_controls, OVERLAY_OPACITY);
6308 clutter_actor_set_name (bvw->header_controls, "header-controls");
6309 clutter_actor_add_constraint (bvw->header_controls,
6310 clutter_bind_constraint_new (bvw->stage,
6311 CLUTTER_BIND_WIDTH,
6312 0));
6313 layout = g_object_new (CLUTTER_TYPE_ACTOR,
6314 "layout-manager", clutter_bin_layout_new (CLUTTER_BIN_ALIGNMENT_CENTER, CLUTTER_BIN_ALIGNMENT_START),
6315 NULL);
6316 clutter_actor_set_name (layout, "layout");
6317 clutter_actor_add_child (layout, bvw->header_controls);
6318 clutter_actor_add_child (bvw->stage, layout);
6319
6320 /* The controls */
6321 bvw->controls = bacon_video_controls_actor_new ();
6322 clutter_actor_set_name (bvw->controls, "controls");
6323 layout = g_object_new (CLUTTER_TYPE_ACTOR,
6324 "layout-manager", clutter_bin_layout_new (CLUTTER_BIN_ALIGNMENT_CENTER, CLUTTER_BIN_ALIGNMENT_END),
6325 NULL);
6326 clutter_actor_set_name (layout, "layout");
6327 clutter_actor_add_child (layout, bvw->controls);
6328
6329 clutter_actor_add_child (bvw->stage, layout);
6330 clutter_actor_set_child_above_sibling (bvw->stage,
6331 layout,
6332 bvw->logo_frame);
6333
6334 clutter_actor_set_opacity (bvw->controls, 0);
6335
6336 item = g_object_get_data (G_OBJECT (bvw->controls), "seek_scale");
6337 g_signal_connect (item, "scroll-event",
6338 G_CALLBACK (bacon_video_widget_handle_scroll), bvw);
6339 item = g_object_get_data (G_OBJECT (bvw->controls), "volume_button");
6340 g_signal_connect (item, "scroll-event",
6341 G_CALLBACK (bacon_video_widget_handle_scroll), bvw);
6342
6343 /* And tell playbin */
6344 g_object_set (bvw->play, "video-sink", bvw->video_sink, NULL);
6345
6346 /* Link the audiopitch element */
6347 bvw->audio_capsfilter =
6348 gst_element_factory_make ("capsfilter", "audiofilter");
6349 audio_bin = gst_bin_new ("audiosinkbin");
6350 gst_bin_add_many (GST_BIN (audio_bin),
6351 bvw->audio_capsfilter,
6352 audio_sink, NULL);
6353 gst_element_link_many (bvw->audio_capsfilter,
6354 audio_sink,
6355 NULL);
6356
6357 audio_pad = gst_element_get_static_pad (bvw->audio_capsfilter, "sink");
6358 gst_element_add_pad (audio_bin, gst_ghost_pad_new ("sink", audio_pad));
6359 gst_object_unref (audio_pad);
6360
6361 /* And tell playbin */
6362 g_object_set (bvw->play, "audio-sink", audio_bin, NULL);
6363 g_object_set (bvw->play, "audio-filter", bvw->audio_pitchcontrol, NULL);
6364
6365 /* Set default connection speed */
6366 /* Cast the value to guint64 to match the type of the 'connection-speed'
6367 * property to avoid problems reading variable arguments on 32-bit systems. */
6368 g_object_set (bvw->play, "connection-speed", (guint64) MAX_NETWORK_SPEED, NULL);
6369
6370 g_signal_connect (G_OBJECT (bvw->play), "notify::volume",
6371 G_CALLBACK (notify_volume_cb), bvw);
6372 g_signal_connect (bvw->play, "source-setup",
6373 G_CALLBACK (playbin_source_setup_cb), bvw);
6374 g_signal_connect (bvw->play, "element-setup",
6375 G_CALLBACK (playbin_element_setup_cb), bvw);
6376 g_signal_connect (bvw->play, "video-changed",
6377 G_CALLBACK (playbin_stream_changed_cb), bvw);
6378 g_signal_connect (bvw->play, "audio-changed",
6379 G_CALLBACK (playbin_stream_changed_cb), bvw);
6380 g_signal_connect (bvw->play, "text-changed",
6381 G_CALLBACK (playbin_stream_changed_cb), bvw);
6382 g_signal_connect (bvw->play, "deep-notify::temp-location",
6383 G_CALLBACK (playbin_deep_notify_cb), bvw);
6384
6385 g_signal_connect (bvw->play, "video-tags-changed",
6386 G_CALLBACK (video_tags_changed_cb), bvw);
6387 g_signal_connect (bvw->play, "audio-tags-changed",
6388 G_CALLBACK (audio_tags_changed_cb), bvw);
6389 g_signal_connect (bvw->play, "text-tags-changed",
6390 G_CALLBACK (text_tags_changed_cb), bvw);
6391
6392 return TRUE;
6393 }
6394
6395 static void
bacon_video_widget_initable_iface_init(GInitableIface * iface)6396 bacon_video_widget_initable_iface_init (GInitableIface *iface)
6397 {
6398 iface->init = bacon_video_widget_initable_init;
6399 }
6400
6401 /**
6402 * bacon_video_widget_new:
6403 * @error: a #GError, or %NULL
6404 *
6405 * Creates a new #BaconVideoWidget.
6406 *
6407 * A #BvwError will be returned on error.
6408 *
6409 * Return value: a new #BaconVideoWidget, or %NULL; destroy with gtk_widget_destroy()
6410 **/
6411 GtkWidget *
bacon_video_widget_new(GError ** error)6412 bacon_video_widget_new (GError ** error)
6413 {
6414 return GTK_WIDGET (g_initable_new (BACON_TYPE_VIDEO_WIDGET, NULL, error, NULL));
6415 }
6416
6417 /**
6418 * bacon_video_widget_get_rate:
6419 * @bvw: a #BaconVideoWidget
6420 *
6421 * Get the current playback rate, with 1.0 being normal rate.
6422 *
6423 * Returns: the current playback rate
6424 **/
6425 gfloat
bacon_video_widget_get_rate(BaconVideoWidget * bvw)6426 bacon_video_widget_get_rate (BaconVideoWidget *bvw)
6427 {
6428 return bvw->rate;
6429 }
6430
6431 /**
6432 * bacon_video_widget_set_rate:
6433 * @bvw: a #BaconVideoWidget
6434 * @new_rate: the new playback rate
6435 *
6436 * Sets the current playback rate.
6437 *
6438 * Returns: %TRUE on success, %FALSE on failure.
6439 **/
6440 gboolean
bacon_video_widget_set_rate(BaconVideoWidget * bvw,gfloat new_rate)6441 bacon_video_widget_set_rate (BaconVideoWidget *bvw,
6442 gfloat new_rate)
6443 {
6444 GstEvent *event;
6445 gboolean retval = FALSE;
6446 gint64 cur;
6447
6448 g_return_val_if_fail (BACON_IS_VIDEO_WIDGET (bvw), FALSE);
6449 g_return_val_if_fail (GST_IS_ELEMENT (bvw->play), FALSE);
6450
6451 if (new_rate == bvw->rate)
6452 return TRUE;
6453
6454 /* set upper and lower limit for rate */
6455 if (new_rate < 0.5)
6456 return retval;
6457 if (new_rate > 2.0)
6458 return retval;
6459
6460 if (gst_element_query_position (bvw->play, GST_FORMAT_TIME, &cur)) {
6461 GST_DEBUG ("Setting new rate at %"G_GINT64_FORMAT"", cur);
6462 event = gst_event_new_seek (new_rate,
6463 GST_FORMAT_TIME, GST_SEEK_FLAG_FLUSH | GST_SEEK_FLAG_ACCURATE,
6464 GST_SEEK_TYPE_SET, cur,
6465 GST_SEEK_TYPE_SET, GST_CLOCK_TIME_NONE);
6466 if (gst_element_send_event (bvw->play, event) == FALSE) {
6467 GST_DEBUG ("Failed to change rate");
6468 } else {
6469 gst_element_get_state (bvw->play, NULL, NULL, GST_CLOCK_TIME_NONE);
6470 bvw->rate = new_rate;
6471 retval = TRUE;
6472 }
6473 } else {
6474 GST_DEBUG ("failed to query position");
6475 }
6476
6477 return retval;
6478 }
6479
6480 /**
6481 * bacon_video_widget_set_fullscreen:
6482 * @bvw: a #BaconVideoWidget
6483 * @fullscreen: the new fullscreen state
6484 *
6485 * Sets the fullscreen state, enabling a toplevel header bar sliding from
6486 * the top of the video widget.
6487 **/
6488 void
bacon_video_widget_set_fullscreen(BaconVideoWidget * bvw,gboolean fullscreen)6489 bacon_video_widget_set_fullscreen (BaconVideoWidget *bvw,
6490 gboolean fullscreen)
6491 {
6492 g_return_if_fail (BACON_IS_VIDEO_WIDGET (bvw));
6493
6494 g_object_set (bvw->header_controls, "visible", fullscreen, NULL);
6495 }
6496
6497 /*
6498 * vim: sw=2 ts=8 cindent noai bs=2
6499 */
6500