gtk: Add GtkVideo
authorBenjamin Otte <otte@redhat.com>
Thu, 1 Mar 2018 03:52:27 +0000 (04:52 +0100)
committerBenjamin Otte <otte@redhat.com>
Sun, 18 Mar 2018 20:01:23 +0000 (21:01 +0100)
GtkVideo is a simple video player widget. It probably needs some more
configurability, but it does its job.

gtk/gtk.h
gtk/gtkvideo.c [new file with mode: 0644]
gtk/gtkvideo.h [new file with mode: 0644]
gtk/meson.build
gtk/theme/Adwaita/_common.scss
gtk/ui/gtkvideo.ui [new file with mode: 0644]

index afe849c292de174bd81a023d83707ef1dfbee4b8..b6f73e61083b7a2294c159cffdfcca91a5e085c3 100644 (file)
--- a/gtk/gtk.h
+++ b/gtk/gtk.h
 #include <gtk/gtktypebuiltins.h>
 #include <gtk/gtktypes.h>
 #include <gtk/gtkversion.h>
+#include <gtk/gtkvideo.h>
 #include <gtk/gtkviewport.h>
 #include <gtk/gtkvolumebutton.h>
 #include <gtk/gtkwidget.h>
diff --git a/gtk/gtkvideo.c b/gtk/gtkvideo.c
new file mode 100644 (file)
index 0000000..bf51a23
--- /dev/null
@@ -0,0 +1,615 @@
+/*
+ * Copyright © 2018 Benjamin Otte
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Benjamin Otte <otte@gnome.org>
+ */
+
+#include "config.h"
+
+#include "gtkvideo.h"
+
+#include "gtkeventcontrollermotion.h"
+#include "gtkimage.h"
+#include "gtkintl.h"
+#include "gtkmediacontrols.h"
+#include "gtkmediafile.h"
+#include "gtkrevealer.h"
+
+/**
+ * SECTION:gtkvideo
+ * @title: GtkVideo
+ * @short_description: A widget for displaying video
+ *
+ * GtkVideo is a widget to show a #GtkMediaStream.
+ */
+
+struct _GtkVideo
+{
+  GtkWidget parent_instance;
+
+  GFile *file;
+  GtkMediaStream *media_stream;
+
+  GtkEventController *motion_controller;
+
+  GtkWidget *box;
+  GtkWidget *video_image;
+  GtkWidget *overlay_icon;
+  GtkWidget *controls_revealer;
+  GtkWidget *controls;
+  guint controls_hide_source;
+};
+
+enum
+{
+  PROP_0,
+  PROP_FILE,
+  PROP_MEDIA_STREAM,
+
+  N_PROPS
+};
+
+G_DEFINE_TYPE (GtkVideo, gtk_video, GTK_TYPE_WIDGET)
+
+static GParamSpec *properties[N_PROPS] = { NULL, };
+
+static void
+gtk_video_measure (GtkWidget      *widget,
+                   GtkOrientation  orientation,
+                   int             for_size,
+                   int            *minimum,
+                   int            *natural,
+                   int            *minimum_baseline,
+                   int            *natural_baseline)
+{
+  GtkVideo *self = GTK_VIDEO (widget);
+
+  gtk_widget_measure (self->box,
+                      orientation,
+                      for_size,
+                      minimum, natural,
+                      minimum_baseline, natural_baseline);
+}
+
+static void
+gtk_video_size_allocate (GtkWidget           *widget,
+                         const GtkAllocation *allocation,
+                         int                  baseline,
+                         GtkAllocation       *out_clip)
+{
+  GtkVideo *self = GTK_VIDEO (widget);
+
+  gtk_widget_size_allocate (self->box, allocation, baseline, out_clip);
+}
+
+static void
+gtk_video_unmap (GtkWidget *widget)
+{
+  GtkVideo *self = GTK_VIDEO (widget);
+
+  if (self->controls_hide_source)
+    {
+      g_source_remove (self->controls_hide_source);
+      self->controls_hide_source = 0;
+      gtk_revealer_set_reveal_child (GTK_REVEALER (self->controls_revealer), FALSE);
+    }
+
+  /* XXX: pause video here? */
+
+  GTK_WIDGET_CLASS (gtk_video_parent_class)->unmap (widget);
+}
+
+static void
+gtk_video_dispose (GObject *object)
+{
+  GtkVideo *self = GTK_VIDEO (object);
+
+  gtk_video_set_media_stream (self, NULL);
+
+  g_clear_object (&self->motion_controller);
+  g_clear_pointer (&self->box, gtk_widget_unparent);
+
+  G_OBJECT_CLASS (gtk_video_parent_class)->dispose (object);
+}
+
+static void
+gtk_video_get_property (GObject    *object,
+                        guint       property_id,
+                        GValue     *value,
+                        GParamSpec *pspec)
+{
+  GtkVideo *self = GTK_VIDEO (object);
+
+  switch (property_id)
+    {
+    case PROP_FILE:
+      g_value_set_object (value, self->file);
+      break;
+
+    case PROP_MEDIA_STREAM:
+      g_value_set_object (value, self->media_stream);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+      break;
+    }
+}
+
+static void
+gtk_video_set_property (GObject      *object,
+                        guint         property_id,
+                        const GValue *value,
+                        GParamSpec   *pspec)
+{
+  GtkVideo *self = GTK_VIDEO (object);
+
+  switch (property_id)
+    {
+    case PROP_FILE:
+      gtk_video_set_file (self, g_value_get_object (value));
+      break;
+
+    case PROP_MEDIA_STREAM:
+      gtk_video_set_media_stream (self, g_value_get_object (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+      break;
+    }
+}
+
+static void
+gtk_video_class_init (GtkVideoClass *klass)
+{
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+  GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+
+  widget_class->measure = gtk_video_measure;
+  widget_class->size_allocate = gtk_video_size_allocate;
+  widget_class->unmap = gtk_video_unmap;
+
+  gobject_class->dispose = gtk_video_dispose;
+  gobject_class->get_property = gtk_video_get_property;
+  gobject_class->set_property = gtk_video_set_property;
+
+  /**
+   * GtkVideo:file:
+   *
+   * The file played by this video if the video is playing a file.
+   */
+  properties[PROP_FILE] =
+    g_param_spec_object ("file",
+                         P_("File"),
+                         P_("The video file played back"),
+                         G_TYPE_FILE,
+                         G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
+
+  /**
+   * GtkVideo:media-stream:
+   *
+   * The media-stream played
+   */
+  properties[PROP_MEDIA_STREAM] =
+    g_param_spec_object ("media-stream",
+                         P_("Media Stream"),
+                         P_("The media stream played"),
+                         GTK_TYPE_MEDIA_STREAM,
+                         G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
+
+  g_object_class_install_properties (gobject_class, N_PROPS, properties);
+
+  gtk_widget_class_set_template_from_resource (widget_class, "/org/gtk/libgtk/ui/gtkvideo.ui");
+  gtk_widget_class_bind_template_child (widget_class, GtkVideo, box);
+  gtk_widget_class_bind_template_child (widget_class, GtkVideo, video_image);
+  gtk_widget_class_bind_template_child (widget_class, GtkVideo, overlay_icon);
+  gtk_widget_class_bind_template_child (widget_class, GtkVideo, controls);
+  gtk_widget_class_bind_template_child (widget_class, GtkVideo, controls_revealer);
+
+  gtk_widget_class_set_css_name (widget_class, I_("video"));
+}
+
+static gboolean
+gtk_video_hide_controls (gpointer data)
+{
+  GtkVideo *self = data;
+
+  gtk_revealer_set_reveal_child (GTK_REVEALER (self->controls_revealer), FALSE);
+
+  self->controls_hide_source = 0;
+
+  return G_SOURCE_REMOVE;
+}
+
+static void
+gtk_video_motion (GtkEventControllerMotion *motion,
+                  double                    x,
+                  double                    y,
+                  GtkVideo                 *self)
+{
+  gtk_revealer_set_reveal_child (GTK_REVEALER (self->controls_revealer), TRUE);
+  if (self->controls_hide_source)
+    g_source_remove (self->controls_hide_source);
+  self->controls_hide_source = g_timeout_add (5 * 1000,
+                                              gtk_video_hide_controls,
+                                              self);
+}
+
+static void
+gtk_video_init (GtkVideo *self)
+{
+  gtk_widget_init_template (GTK_WIDGET (self));
+  gtk_widget_set_has_window (GTK_WIDGET (self), FALSE);
+
+  self->motion_controller = gtk_event_controller_motion_new (GTK_WIDGET (self));
+  g_signal_connect (self->motion_controller, "motion", G_CALLBACK (gtk_video_motion), self);
+}
+
+/**
+ * gtk_video_new:
+ *
+ * Creates a new empty #GtkVideo.
+ *
+ * Returns: a new #GtkVideo
+ **/
+GtkWidget *
+gtk_video_new (void)
+{
+  return g_object_new (GTK_TYPE_VIDEO, NULL);
+}
+
+/**
+ * gtk_video_new_for_media_stream:
+ * @stream: (allow-none): a #GtkMediaStream
+ *
+ * Creates a #GtkVideo to play back the given @stream.
+ *
+ * Returns: a new #GtkVideo
+ **/
+GtkWidget *
+gtk_video_new_for_media_stream (GtkMediaStream *stream)
+{
+  g_return_val_if_fail (stream == NULL || GTK_IS_MEDIA_STREAM (stream), NULL);
+
+  return g_object_new (GTK_TYPE_VIDEO,
+                       "media-stream", stream,
+                       NULL);
+}
+
+/**
+ * gtk_video_new_for_file:
+ * @file: (allow-none): a #GFile
+ *
+ * Creates a #GtkVideo to play back the given @file.
+ *
+ * Returns: a new #GtkVideo
+ **/
+GtkWidget *
+gtk_video_new_for_file (GFile *file)
+{
+  g_return_val_if_fail (file == NULL || G_IS_FILE (file), NULL);
+
+  return g_object_new (GTK_TYPE_VIDEO,
+                       "file", file,
+                       NULL);
+}
+
+/**
+ * gtk_video_new_for_filename:
+ * @filename: (allow-none): filename to play back
+ *
+ * Creates a #GtkVideo to play back the given @filename.
+ *
+ * This is a utility function that calls gtk_video_new_for_file(),
+ *
+ * Returns: a new #GtkVideo
+ **/
+GtkWidget *
+gtk_video_new_for_filename (const char *filename)
+{
+  GtkWidget *result;
+  GFile *file;
+
+  if (filename)
+    file = g_file_new_for_path (filename);
+  else
+    file = NULL;
+
+  result = gtk_video_new_for_file (file);
+
+  if (file)
+    g_object_unref (file);
+
+  return result;
+}
+
+/**
+ * gtk_video_new_for_resource:
+ * @filename: (allow-none): resource path to play back
+ *
+ * Creates a #GtkVideo to play back the resource at the
+ * given @resource_path.
+ *
+ * This is a utility function that calls gtk_video_new_for_file(),
+ *
+ * Returns: a new #GtkVideo
+ **/
+GtkWidget *
+gtk_video_new_for_resource (const char *resource_path)
+{
+  GtkWidget *result;
+  GFile *file;
+
+  if (resource_path)
+    {
+      char *uri, *escaped;
+
+      escaped = g_uri_escape_string (resource_path,
+                                     G_URI_RESERVED_CHARS_ALLOWED_IN_PATH, FALSE);
+      uri = g_strconcat ("resource://", escaped, NULL);
+      g_free (escaped);
+
+      file = g_file_new_for_uri (uri);
+      g_free (uri);
+    }
+  else
+    {
+      file = NULL;
+    }
+
+  result = gtk_video_new_for_file (file);
+
+  if (file)
+    g_object_unref (file);
+
+  return result;
+}
+
+/**
+ * gtk_video_get_media_stream:
+ * @self: a #GtkVideo
+ *
+ * Gets the media stream managed by @self or %NULL if none.
+ *
+ * Returns: (nullable): The media stream managed by @self
+ **/
+GtkMediaStream *
+gtk_video_get_media_stream (GtkVideo *self)
+{
+  g_return_val_if_fail (GTK_IS_VIDEO (self), NULL);
+
+  return self->media_stream;
+}
+
+static void
+gtk_video_update_overlay_icon (GtkVideo *self)
+{
+  const char *icon_name;
+  const GError *error = NULL;
+
+  if (self->media_stream == NULL)
+    icon_name = "media-eject-symbolic";
+  else if ((error = gtk_media_stream_get_error (self->media_stream)))
+    icon_name = "dialog-error-symbolic";
+  else if (gtk_media_stream_get_ended (self->media_stream))
+    icon_name = "media-playlist-repeat-symbolic";
+  else
+    icon_name = "media-playback-start-symbolic";
+
+  gtk_image_set_from_icon_name (GTK_IMAGE (self->overlay_icon), icon_name);
+  if (error)
+    gtk_widget_set_tooltip_text (self->overlay_icon, error->message);
+  else
+    gtk_widget_set_tooltip_text (self->overlay_icon, NULL);
+}
+
+static void
+gtk_video_update_ended (GtkVideo *self)
+{
+  gtk_video_update_overlay_icon (self);
+}
+
+static void
+gtk_video_update_error (GtkVideo *self)
+{
+  gtk_video_update_overlay_icon (self);
+}
+
+static void
+gtk_video_update_playing (GtkVideo *self)
+{
+  gboolean playing;
+
+  if (self->media_stream != NULL)
+    playing = gtk_media_stream_get_playing (self->media_stream);
+  else
+    playing = FALSE;
+
+  gtk_widget_set_visible (self->overlay_icon, !playing);
+}
+
+static void
+gtk_video_update_all (GtkVideo *self)
+{
+  gtk_video_update_ended (self);
+  gtk_video_update_error (self);
+  gtk_video_update_playing (self);
+}
+
+static void
+gtk_video_notify_cb (GtkMediaStream *stream,
+                     GParamSpec     *pspec,
+                     GtkVideo       *self)
+{
+  if (g_str_equal (pspec->name, "ended"))
+    gtk_video_update_ended (self);
+  if (g_str_equal (pspec->name, "error"))
+    gtk_video_update_error (self);
+  if (g_str_equal (pspec->name, "playing"))
+    gtk_video_update_playing (self);
+}
+
+/**
+ * gtk_video_set_media_stream:
+ * @self: a #GtkVideo
+ * @stream: (allow-none): The media stream to play or %NULL to unset
+ *
+ * Sets the media stream to be played back. @self will take full control
+ * of managing the media stream. If you want to manage a media stream
+ * yourself, consider using a #GtkImage for display.
+ *
+ * If you want to display a file, consider using gtk_video_set_file()
+ * instead.
+ **/
+void
+gtk_video_set_media_stream (GtkVideo       *self,
+                            GtkMediaStream *stream)
+{
+  g_return_if_fail (GTK_IS_VIDEO (self));
+  g_return_if_fail (stream == NULL || GTK_IS_MEDIA_STREAM (stream));
+
+  if (self->media_stream == stream)
+    return;
+
+  if (self->media_stream)
+    {
+      g_signal_handlers_disconnect_by_func (self->media_stream,
+                                            gtk_video_notify_cb,
+                                            self);
+      g_object_unref (self->media_stream);
+      self->media_stream = NULL;
+    }
+
+  if (stream)
+    {
+      self->media_stream = g_object_ref (stream);
+      g_signal_connect (self->media_stream,
+                        "notify",
+                        G_CALLBACK (gtk_video_notify_cb),
+                        self);
+    }
+
+  gtk_media_controls_set_media_stream (GTK_MEDIA_CONTROLS (self->controls), stream);
+  gtk_image_set_from_paintable (GTK_IMAGE (self->video_image), GDK_PAINTABLE (stream));
+
+  gtk_video_update_all (self);
+
+  g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_MEDIA_STREAM]);
+}
+
+/**
+ * gtk_video_set_file:
+ * @self: a #GtkVideo
+ * @file: (allow-none): the file to play
+ *
+ * Makes @self play the given @file.
+ **/
+void
+gtk_video_set_file (GtkVideo *self,
+                    GFile    *file)
+{
+  GtkMediaStream *stream;
+
+  g_return_if_fail (GTK_IS_VIDEO (self));
+  g_return_if_fail (file == NULL || G_IS_FILE (file));
+
+  if (!g_set_object (&self->file, file))
+    return;
+
+  g_object_freeze_notify (G_OBJECT (self));
+
+  if (file)
+    stream = gtk_media_file_new_for_file (file);
+  else
+    stream = NULL;
+
+  gtk_video_set_media_stream (self, stream);
+
+  if (stream)
+    g_object_unref (stream);
+
+  g_object_thaw_notify (G_OBJECT (self));
+}
+
+/**
+ * gtk_video_set_resource:
+ * @self: a #GtkVideo
+ * @filename: (allow-none): the filename to play
+ *
+ * Makes @self play the given @filename.
+ *
+ * This is a utility function that calls gtk_video_set_file(),
+ **/
+void
+gtk_video_set_filename (GtkVideo   *self,
+                        const char *filename)
+{
+  GFile *file;
+
+  g_return_if_fail (GTK_IS_VIDEO (self));
+
+  if (filename)
+    file = g_file_new_for_path (filename);
+  else
+    file = NULL;
+
+  gtk_video_set_file (self, file);
+
+  if (file)
+    g_object_unref (file);
+}
+
+/**
+ * gtk_video_set_resource:
+ * @self: a #GtkVideo
+ * @resource_path: (allow-none): the resource to set
+ *
+ * Makes @self play the resource at the given @resource_path.
+ *
+ * This is a utility function that calls gtk_video_set_file(),
+ **/
+void
+gtk_video_set_resource (GtkVideo   *self,
+                        const char *resource_path)
+{
+  GFile *file;
+
+  g_return_if_fail (GTK_IS_VIDEO (self));
+
+  if (resource_path)
+    {
+      char *uri, *escaped;
+
+      escaped = g_uri_escape_string (resource_path,
+                                     G_URI_RESERVED_CHARS_ALLOWED_IN_PATH, FALSE);
+      uri = g_strconcat ("resource://", escaped, NULL);
+      g_free (escaped);
+
+      file = g_file_new_for_uri (uri);
+      g_free (uri);
+    }
+  else
+    {
+      file = NULL;
+    }
+
+  gtk_video_set_file (self, file);
+
+  if (file)
+    g_object_unref (file);
+}
+
diff --git a/gtk/gtkvideo.h b/gtk/gtkvideo.h
new file mode 100644 (file)
index 0000000..16ffbbf
--- /dev/null
@@ -0,0 +1,64 @@
+/*
+ * Copyright © 2018 Benjamin Otte
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Benjamin Otte <otte@gnome.org>
+ */
+
+#ifndef __GTK_VIDEO_H__
+#define __GTK_VIDEO_H__
+
+#include <gtk/gtkmediastream.h>
+#include <gtk/gtkwidget.h>
+
+G_BEGIN_DECLS
+
+#define GTK_TYPE_VIDEO         (gtk_video_get_type ())
+
+GDK_AVAILABLE_IN_ALL
+G_DECLARE_FINAL_TYPE (GtkVideo, gtk_video, GTK, VIDEO, GtkWidget)
+
+GDK_AVAILABLE_IN_ALL
+GtkWidget *     gtk_video_new                           (void);
+GDK_AVAILABLE_IN_ALL
+GtkWidget *     gtk_video_new_for_media_stream          (GtkMediaStream         *stream);
+GDK_AVAILABLE_IN_ALL
+GtkWidget *     gtk_video_new_for_file                  (GFile                  *file);
+GDK_AVAILABLE_IN_ALL
+GtkWidget *     gtk_video_new_for_filename              (const char             *filename);
+GDK_AVAILABLE_IN_ALL
+GtkWidget *     gtk_video_new_for_resource              (const char             *resource_path);
+
+GDK_AVAILABLE_IN_ALL
+GtkMediaStream *gtk_video_get_media_stream              (GtkVideo               *self);
+GDK_AVAILABLE_IN_ALL
+void            gtk_video_set_media_stream              (GtkVideo               *self,
+                                                         GtkMediaStream         *stream);
+GDK_AVAILABLE_IN_ALL
+GFile *         gtk_video_get_file                      (GtkVideo               *self);
+GDK_AVAILABLE_IN_ALL
+void            gtk_video_set_file                      (GtkVideo               *self,
+                                                         GFile                  *file);
+GDK_AVAILABLE_IN_ALL
+void            gtk_video_set_filename                  (GtkVideo               *self,
+                                                         const char             *filename);
+GDK_AVAILABLE_IN_ALL
+void            gtk_video_set_resource                  (GtkVideo               *self,
+                                                         const char             *resource_path);
+
+
+G_END_DECLS
+
+#endif  /* __GTK_VIDEO_H__ */
index 22d849f2e9426d175de083cfa5aa97e33b5939a9..c06b5aa07545f861747de49f71d1b358fe285d19 100644 (file)
@@ -358,6 +358,7 @@ gtk_public_sources = files([
   'gtktreeview.c',
   'gtktreeviewcolumn.c',
   'gtkutils.c',
+  'gtkvideo.c',
   'gtkviewport.c',
   'gtkvolumebutton.c',
   'gtkwidget.c',
@@ -573,6 +574,7 @@ gtk_public_headers = files([
   'gtktreeview.h',
   'gtktreeviewcolumn.h',
   'gtktypes.h',
+  'gtkvideo.h',
   'gtkviewport.h',
   'gtkvolumebutton.h',
   'gtkwidget.h',
index 3e28edd0f3400316521b2e56d9213d2fd8b7e900..783df4fbf64bc2332fb53f8779cd1cebde15898d 100644 (file)
@@ -3998,6 +3998,19 @@ paned {
 }
 
 
+/**************
+ * GtkVideo *
+ **************/
+
+video {
+  & image.osd {
+    min-width: 64px;
+    min-height: 64px;
+    border-radius: 32px;
+  }
+  background: black;
+}
+
 /**************
  * GtkInfoBar *
  **************/
diff --git a/gtk/ui/gtkvideo.ui b/gtk/ui/gtkvideo.ui
new file mode 100644 (file)
index 0000000..8acd989
--- /dev/null
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface domain="gtk40">
+  <!-- interface-requires gtk+ 3.6 -->
+  <template class="GtkVideo" parent="GtkWidget">
+    <child>
+      <object class="GtkOverlay" id="box">
+        <child>
+          <object class="GtkImage" id="video_image">
+            <property name="can-shrink">1</property>
+          </object>
+        </child>
+        <child type="overlay">
+          <object class="GtkImage" id="overlay_icon">
+            <style>
+              <class name="osd"/>
+              <class name="circular"/>
+            </style>
+            <property name="halign">center</property>
+            <property name="valign">center</property>
+            <property name="icon-name">media-playback-start-symbolic</property>
+            <property name="icon-size">large</property>
+          </object>
+          <packing>
+            <property name="measure">1</property>
+          </packing>
+        </child>
+        <child type="overlay">
+          <object class="GtkRevealer" id="controls_revealer">
+            <property name="reveal-child">0</property>
+            <property name="valign">end</property>
+            <child>
+              <object class="GtkMediaControls" id="controls">
+                <style>
+                  <class name="osd"/>
+                  <class name="bottom"/>
+                </style>
+              </object>
+            </child>
+          </object>
+          <packing>
+            <property name="measure">1</property>
+          </packing>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>