[sushi/wip/cosimoc/no-clutter: 13/50] media-bin: import SushiMediaBin for gstreamer playback



commit e954524d1e1eeef5ecc9a465c632d63b3de1d6c3
Author: Cosimo Cecchi <cosimoc gnome org>
Date:   Sun Apr 2 12:16:05 2017 -0700

    media-bin: import SushiMediaBin for gstreamer playback
    
    Imported from eos-knowledge-lib and changed namespace to use in Sushi.

 meson.build                                   |    3 +
 src/libsushi/SushiMediaBin.ui                 |  409 +++++
 src/libsushi/meson.build                      |    9 +
 src/libsushi/org.gnome.Libsushi.gresource.xml |    7 +
 src/libsushi/sushi-media-bin.c                | 2212 +++++++++++++++++++++++++
 src/libsushi/sushi-media-bin.css              |  220 +++
 src/libsushi/sushi-media-bin.h                |   75 +
 src/meson.build                               |    3 +
 8 files changed, 2938 insertions(+)
---
diff --git a/meson.build b/meson.build
index 538dc4b..6a94156 100644
--- a/meson.build
+++ b/meson.build
@@ -8,6 +8,7 @@ project(
 clutter_dep = dependency('clutter-1.0', version: '>=1.11.4')
 clutter_gst_dep = dependency('clutter-gst-3.0')
 clutter_gtk_dep = dependency('clutter-gtk-1.0', version: '>=1.0.1')
+epoxy_dep = dependency('epoxy')
 evince_document_dep = dependency('evince-document-3.0')
 evince_view_dep = dependency('evince-view-3.0')
 freetype_dep = dependency('freetype2')
@@ -15,9 +16,11 @@ gdk_pixbuf_dep = dependency('gdk-pixbuf-2.0', version: '>=2.23.0')
 gjs_dep = dependency('gjs-1.0', version: '>=1.38.0')
 glib_dep = dependency('glib-2.0', version: '>=2.29.14')
 gstreamer_dep = dependency('gstreamer-1.0')
+gstreamer_audio_dep = dependency('gstreamer-audio-1.0')
 gstreamer_base_dep = dependency('gstreamer-base-1.0')
 gstreamer_pbutils_dep = dependency('gstreamer-pbutils-1.0')
 gstreamer_tag_dep = dependency('gstreamer-tag-1.0')
+gstreamer_video_dep = dependency('gstreamer-video-1.0')
 gtk_dep = dependency('gtk+-3.0', version: '>=3.13.2')
 gtksourceview_dep = dependency('gtksourceview-4', version: '>=4.0.3')
 harfbuzz_dep = dependency('harfbuzz', version: '>=0.9.9')
diff --git a/src/libsushi/SushiMediaBin.ui b/src/libsushi/SushiMediaBin.ui
new file mode 100644
index 0000000..2933878
--- /dev/null
+++ b/src/libsushi/SushiMediaBin.ui
@@ -0,0 +1,409 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.20.0 -->
+<interface>
+  <requires lib="gtk+" version="3.20"/>
+  <object class="GtkImage" id="audio_playback_image">
+    <property name="visible">True</property>
+    <property name="can_focus">False</property>
+    <property name="icon_name">media-playback-start-symbolic</property>
+  </object>
+  <object class="GtkImage" id="fullscreen_image">
+    <property name="visible">True</property>
+    <property name="can_focus">False</property>
+    <property name="pixel_size">18</property>
+    <property name="icon_name">view-fullscreen-symbolic</property>
+  </object>
+  <object class="GtkAdjustment" id="playback_adjustment">
+    <property name="upper">128</property>
+    <property name="step_increment">60</property>
+    <property name="page_increment">300</property>
+    <signal name="value-changed" handler="on_playback_adjustment_value_changed" swapped="no"/>
+  </object>
+  <object class="GtkImage" id="playback_image">
+    <property name="visible">True</property>
+    <property name="can_focus">False</property>
+    <property name="pixel_size">18</property>
+    <property name="icon_name">media-playback-start-symbolic</property>
+  </object>
+  <object class="GtkAdjustment" id="volume_adjustment">
+    <property name="upper">1</property>
+    <property name="value">1</property>
+    <property name="step_increment">0.040000000000000001</property>
+    <property name="page_increment">0.10000000000000001</property>
+  </object>
+  <template class="SushiMediaBin" parent="GtkBox">
+    <property name="visible">True</property>
+    <property name="can_focus">True</property>
+    <property name="orientation">vertical</property>
+    <signal name="realize" handler="on_sushi_media_bin_realize" swapped="no"/>
+    <child>
+      <object class="GtkStack" id="stack">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="hexpand">True</property>
+        <property name="vexpand">True</property>
+        <property name="hhomogeneous">False</property>
+        <property name="vhomogeneous">False</property>
+        <child>
+          <object class="GtkOverlay" id="overlay">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="events">GDK_POINTER_MOTION_MASK | GDK_BUTTON_PRESS_MASK | 
GDK_BUTTON_RELEASE_MASK | GDK_KEY_PRESS_MASK | GDK_KEY_RELEASE_MASK | GDK_STRUCTURE_MASK</property>
+            <property name="no_show_all">True</property>
+            <signal name="button-press-event" handler="on_overlay_button_press_event" swapped="no"/>
+            <signal name="button-release-event" handler="on_overlay_button_release_event" swapped="no"/>
+            <signal name="motion-notify-event" handler="on_overlay_motion_notify_event" swapped="no"/>
+            <child>
+              <placeholder/>
+            </child>
+            <child type="overlay">
+              <object class="GtkRevealer" id="top_revealer">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="events">GDK_POINTER_MOTION_MASK | GDK_LEAVE_NOTIFY_MASK | 
GDK_STRUCTURE_MASK</property>
+                <property name="valign">start</property>
+                <signal name="leave-notify-event" handler="on_revealer_leave_notify_event" swapped="no"/>
+                <signal name="motion-notify-event" handler="on_revealer_motion_notify_event" swapped="no"/>
+                <child>
+                  <object class="GtkBox">
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                    <property name="orientation">vertical</property>
+                    <child>
+                      <object class="GtkLabel" id="title_label">
+                        <property name="can_focus">False</property>
+                        <property name="halign">start</property>
+                        <style>
+                          <class name="title"/>
+                        </style>
+                      </object>
+                      <packing>
+                        <property name="expand">False</property>
+                        <property name="fill">True</property>
+                        <property name="position">0</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <object class="GtkBox" id="info_box">
+                        <property name="can_focus">False</property>
+                        <child>
+                          <placeholder/>
+                        </child>
+                      </object>
+                      <packing>
+                        <property name="expand">False</property>
+                        <property name="fill">True</property>
+                        <property name="position">1</property>
+                      </packing>
+                    </child>
+                    <style>
+                      <class name="overlay-bar"/>
+                      <class name="top"/>
+                    </style>
+                  </object>
+                </child>
+              </object>
+              <packing>
+                <property name="index">1</property>
+              </packing>
+            </child>
+            <child type="overlay">
+              <object class="GtkRevealer" id="bottom_revealer">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="events">GDK_POINTER_MOTION_MASK | GDK_LEAVE_NOTIFY_MASK | 
GDK_STRUCTURE_MASK</property>
+                <property name="valign">end</property>
+                <property name="transition_type">slide-up</property>
+                <signal name="leave-notify-event" handler="on_revealer_leave_notify_event" swapped="no"/>
+                <signal name="motion-notify-event" handler="on_revealer_motion_notify_event" swapped="no"/>
+                <child>
+                  <object class="GtkBox" id="bottom_box">
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                    <property name="valign">end</property>
+                    <property name="orientation">vertical</property>
+                    <child>
+                      <object class="GtkScale" id="progress_scale">
+                        <property name="visible">True</property>
+                        <property name="can_focus">True</property>
+                        <property name="adjustment">playback_adjustment</property>
+                        <property name="round_digits">2</property>
+                        <signal name="format-value" handler="on_progress_scale_format_value" swapped="no"/>
+                      </object>
+                      <packing>
+                        <property name="expand">False</property>
+                        <property name="fill">True</property>
+                        <property name="position">0</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <object class="GtkBox">
+                        <property name="visible">True</property>
+                        <property name="can_focus">False</property>
+                        <property name="spacing">2</property>
+                        <child>
+                          <object class="GtkButton" id="playback_button">
+                            <property name="visible">True</property>
+                            <property name="can_focus">True</property>
+                            <property name="receives_default">False</property>
+                            <property name="image">playback_image</property>
+                            <property name="relief">none</property>
+                            <signal name="clicked" handler="sushi_media_bin_toggle_playback" swapped="yes"/>
+                          </object>
+                          <packing>
+                            <property name="expand">False</property>
+                            <property name="fill">True</property>
+                            <property name="position">0</property>
+                          </packing>
+                        </child>
+                        <child>
+                          <object class="GtkButton" id="fullscreen_button">
+                            <property name="visible">True</property>
+                            <property name="can_focus">True</property>
+                            <property name="receives_default">False</property>
+                            <property name="image">fullscreen_image</property>
+                            <property name="relief">none</property>
+                            <signal name="clicked" handler="sushi_media_bin_toggle_fullscreen" 
swapped="yes"/>
+                          </object>
+                          <packing>
+                            <property name="expand">False</property>
+                            <property name="fill">True</property>
+                            <property name="pack_type">end</property>
+                            <property name="position">1</property>
+                          </packing>
+                        </child>
+                        <child>
+                          <object class="GtkVolumeButton" id="volume_button">
+                            <property name="visible">True</property>
+                            <property name="can_focus">True</property>
+                            <property name="focus_on_click">False</property>
+                            <property name="receives_default">False</property>
+                            <property name="relief">none</property>
+                            <property name="orientation">vertical</property>
+                            <property name="value">1</property>
+                            <property name="size">large-toolbar</property>
+                            <property name="adjustment">volume_adjustment</property>
+                            <property name="icons">audio-volume-muted-symbolic
+audio-volume-high-symbolic
+audio-volume-low-symbolic
+audio-volume-medium-symbolic</property>
+                            <property name="use_symbolic">False</property>
+                            <child internal-child="plus_button">
+                              <object class="GtkButton">
+                                <property name="can_focus">True</property>
+                                <property name="receives_default">True</property>
+                                <property name="halign">center</property>
+                                <property name="valign">center</property>
+                                <property name="relief">none</property>
+                              </object>
+                            </child>
+                            <child internal-child="minus_button">
+                              <object class="GtkButton">
+                                <property name="can_focus">True</property>
+                                <property name="receives_default">True</property>
+                                <property name="halign">center</property>
+                                <property name="valign">center</property>
+                                <property name="relief">none</property>
+                              </object>
+                            </child>
+                          </object>
+                          <packing>
+                            <property name="expand">False</property>
+                            <property name="fill">True</property>
+                            <property name="pack_type">end</property>
+                            <property name="position">2</property>
+                          </packing>
+                        </child>
+                        <child>
+                          <placeholder/>
+                        </child>
+                        <style>
+                          <class name="overlay-bar"/>
+                          <class name="bottom"/>
+                        </style>
+                      </object>
+                      <packing>
+                        <property name="expand">False</property>
+                        <property name="fill">True</property>
+                        <property name="position">1</property>
+                      </packing>
+                    </child>
+                  </object>
+                </child>
+              </object>
+              <packing>
+                <property name="index">2</property>
+              </packing>
+            </child>
+            <child type="overlay">
+              <object class="GtkBox" id="play_box">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="halign">center</property>
+                <property name="valign">center</property>
+                <child>
+                  <object class="GtkImage" id="play_image">
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                    <property name="pixel_size">32</property>
+                    <property name="icon_name">media-playback-start-symbolic</property>
+                  </object>
+                  <packing>
+                    <property name="expand">False</property>
+                    <property name="fill">True</property>
+                    <property name="position">0</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkLabel" id="duration_label">
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                    <property name="halign">start</property>
+                  </object>
+                  <packing>
+                    <property name="expand">True</property>
+                    <property name="fill">True</property>
+                    <property name="position">1</property>
+                  </packing>
+                </child>
+              </object>
+              <packing>
+                <property name="pass_through">True</property>
+                <property name="index">2</property>
+              </packing>
+            </child>
+          </object>
+        </child>
+        <child>
+          <object class="GtkBox" id="audio_box">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="valign">center</property>
+            <property name="orientation">vertical</property>
+            <child>
+              <object class="GtkScale" id="audio_progress_scale">
+                <property name="visible">True</property>
+                <property name="can_focus">True</property>
+                <property name="adjustment">playback_adjustment</property>
+                <property name="draw_value">False</property>
+              </object>
+              <packing>
+                <property name="expand">False</property>
+                <property name="fill">True</property>
+                <property name="position">0</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkBox">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="spacing">2</property>
+                <child>
+                  <object class="GtkButton" id="audio_playback_button">
+                    <property name="visible">True</property>
+                    <property name="can_focus">True</property>
+                    <property name="receives_default">False</property>
+                    <property name="image">audio_playback_image</property>
+                    <property name="relief">none</property>
+                    <signal name="clicked" handler="sushi_media_bin_toggle_playback" swapped="yes"/>
+                  </object>
+                  <packing>
+                    <property name="expand">False</property>
+                    <property name="fill">True</property>
+                    <property name="position">0</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkLabel">
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                    <property name="width_chars">12</property>
+                  </object>
+                  <packing>
+                    <property name="expand">False</property>
+                    <property name="fill">True</property>
+                    <property name="position">1</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkVolumeButton" id="audio_volume_button">
+                    <property name="visible">True</property>
+                    <property name="can_focus">True</property>
+                    <property name="focus_on_click">False</property>
+                    <property name="receives_default">False</property>
+                    <property name="relief">none</property>
+                    <property name="orientation">vertical</property>
+                    <property name="value">1</property>
+                    <property name="size">large-toolbar</property>
+                    <property name="adjustment">volume_adjustment</property>
+                    <property name="icons">audio-volume-muted-symbolic
+audio-volume-high-symbolic
+audio-volume-low-symbolic
+audio-volume-medium-symbolic</property>
+                    <property name="use_symbolic">False</property>
+                    <child internal-child="plus_button">
+                      <object class="GtkButton">
+                        <property name="can_focus">True</property>
+                        <property name="receives_default">True</property>
+                        <property name="halign">center</property>
+                        <property name="valign">center</property>
+                        <property name="relief">none</property>
+                      </object>
+                    </child>
+                    <child internal-child="minus_button">
+                      <object class="GtkButton">
+                        <property name="can_focus">True</property>
+                        <property name="receives_default">True</property>
+                        <property name="halign">center</property>
+                        <property name="valign">center</property>
+                        <property name="relief">none</property>
+                      </object>
+                    </child>
+                  </object>
+                  <packing>
+                    <property name="expand">False</property>
+                    <property name="fill">True</property>
+                    <property name="pack_type">end</property>
+                    <property name="position">2</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkLabel" id="audio_position_label">
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                    <property name="width_chars">4</property>
+                  </object>
+                  <packing>
+                    <property name="expand">False</property>
+                    <property name="fill">True</property>
+                    <property name="pack_type">end</property>
+                    <property name="position">3</property>
+                  </packing>
+                </child>
+                <style>
+                  <class name="bottom"/>
+                </style>
+              </object>
+              <packing>
+                <property name="expand">False</property>
+                <property name="fill">True</property>
+                <property name="position">1</property>
+              </packing>
+            </child>
+            <style>
+              <class name="audio"/>
+            </style>
+          </object>
+          <packing>
+            <property name="position">1</property>
+          </packing>
+        </child>
+      </object>
+      <packing>
+        <property name="expand">False</property>
+        <property name="fill">True</property>
+        <property name="position">0</property>
+      </packing>
+    </child>
+  </template>
+</interface>
diff --git a/src/libsushi/meson.build b/src/libsushi/meson.build
index 66ef646..970cf05 100644
--- a/src/libsushi/meson.build
+++ b/src/libsushi/meson.build
@@ -3,6 +3,7 @@ libsushi_c = [
   'sushi-file-loader.c',
   'sushi-font-loader.c',
   'sushi-font-widget.c',
+  'sushi-media-bin.c',
   'sushi-pdf-loader.c',
   'sushi-sound-player.c',
   'sushi-text-loader.c',
@@ -14,6 +15,7 @@ libsushi_headers = [
   'sushi-file-loader.h',
   'sushi-font-loader.h',
   'sushi-font-widget.h',
+  'sushi-media-bin.h',
   'sushi-pdf-loader.h',
   'sushi-sound-player.h',
   'sushi-text-loader.h',
@@ -27,6 +29,12 @@ libsushi_enum = gnome.mkenums(
   sources: libsushi_headers,
 )
 
+libsushi_resource = gnome.compile_resources(
+  'sushi-lib-resources',
+  'org.gnome.Libsushi.gresource.xml',
+  c_name: 'sushi'
+)
+
 libsushi = shared_library(
   'sushi-1.0',
   dependencies: deps,
@@ -34,6 +42,7 @@ libsushi = shared_library(
     libsushi_c,
     libsushi_enum,
     libsushi_headers,
+    libsushi_resource
   ],
   install: true,
   install_dir: join_paths(libdir, 'sushi')
diff --git a/src/libsushi/org.gnome.Libsushi.gresource.xml b/src/libsushi/org.gnome.Libsushi.gresource.xml
new file mode 100644
index 0000000..ddbb1c1
--- /dev/null
+++ b/src/libsushi/org.gnome.Libsushi.gresource.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+  <gresource prefix="/org/gnome/Sushi/libsushi">
+    <file>SushiMediaBin.ui</file>
+    <file>sushi-media-bin.css</file>
+  </gresource>
+</gresources>
diff --git a/src/libsushi/sushi-media-bin.c b/src/libsushi/sushi-media-bin.c
new file mode 100644
index 0000000..4bce95d
--- /dev/null
+++ b/src/libsushi/sushi-media-bin.c
@@ -0,0 +1,2212 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
+/*
+ * sushi-media-bin.c
+ * Based on ekn-media-bin.c from:
+ * https://github.com/endlessm/eos-knowledge-lib/tree/master/lib/eosknowledgeprivate
+ *
+ * Copyright (C) 2016 Endless Mobile, Inc.
+ *
+ * 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, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ * Author: Juan Pablo Ugarte <ugarte endlessm com>
+ *
+ */
+
+#include "sushi-media-bin.h"
+#include <gst/gst.h>
+#include <gst/video/gstvideosink.h>
+#include <gst/audio/gstaudiobasesink.h>
+#include <epoxy/gl.h>
+
+#ifdef DEBUG
+
+#include <unistd.h>
+#include <sys/types.h>
+#include <sys/syscall.h>
+#define WARN_IF_NOT_MAIN_THREAD if (getpid () != syscall (SYS_gettid)) g_warning ("%s %d not in main 
thread", __func__, __LINE__);
+
+#endif
+
+#define AUTOHIDE_TIMEOUT_DEFAULT 2  /* Controls autohide timeout in seconds */
+
+#define INFO_N_COLUMNS           6  /* Number of info columns labels */
+
+#define FPS_WINDOW_SIZE          2  /* Window size in seconds to calculate fps */
+
+#define SMB_ICON_SIZE            GTK_ICON_SIZE_BUTTON
+
+#define SMB_ICON_NAME_PLAY       "media-playback-start-symbolic"
+#define SMB_ICON_NAME_PAUSE      "media-playback-pause-symbolic"
+#define SMB_ICON_NAME_FULLSCREEN "view-fullscreen-symbolic"
+#define SMB_ICON_NAME_RESTORE    "view-restore-symbolic"
+
+#define SMB_INITIAL_STATE        GST_STATE_PAUSED
+
+GST_DEBUG_CATEGORY_STATIC (sushi_media_bin_debug);
+#define GST_CAT_DEFAULT sushi_media_bin_debug
+
+struct _SushiMediaBin
+{
+  GtkBox parent;
+};
+
+typedef struct
+{
+  /* Properties */
+  gchar   *uri;
+  gint     autohide_timeout;
+  gchar   *title;
+  gchar   *description;
+
+  /* Boolean properties */
+  gboolean fullscreen:1;
+  gboolean show_stream_info:1;
+  gboolean audio_mode:1;
+
+  /* We place extra flags here so the get squashed with the boolean properties */
+  gboolean title_user_set:1;            /* True if the user set title property */
+  gboolean description_user_set:1;      /* True if the user set description property */
+  gboolean dump_dot_file:1;             /* True if GST_DEBUG_DUMP_DOT_DIR is set */
+  gboolean ignore_adjustment_changes:1;
+
+  /* Internal Widgets */
+  GtkStack      *stack;
+  GtkImage      *playback_image;
+  GtkImage      *fullscreen_image;
+  GtkAdjustment *playback_adjustment;
+  GtkAdjustment *volume_adjustment;
+
+  /* Internal Video Widgets */
+  GtkWidget      *overlay;
+  GtkWidget      *play_box;
+  GtkScaleButton *volume_button;
+  GtkWidget      *info_box;
+
+  GtkLabel *title_label;
+  GtkLabel *info_column_label[INFO_N_COLUMNS];
+  GtkLabel *duration_label;
+
+  /* Thanks to GSK all the blitting will be done in GL */
+  GtkRevealer *top_revealer;
+  GtkRevealer *bottom_revealer;
+
+  /* Internal Audio Widgets */
+  GtkWidget      *audio_box;
+  GtkScaleButton *audio_volume_button;
+  GtkLabel       *audio_position_label;
+  GtkImage       *audio_playback_image;
+
+  /* Support Objects */
+  GtkWidget *video_widget;      /* Created at runtime from sink */
+  GtkWindow *fullscreen_window;
+  GdkCursor *blank_cursor;
+  GtkWidget *tmp_image;      /* FIXME: remove this once we can derive from GtkBin in Glade */
+
+  /* Internal variables */
+  guint timeout_id;          /* Autohide timeout source id */
+  gint  timeout_count;       /* Autohide timeout count since last move event */
+
+  guint  tick_id;           /* Widget frame clock tick callback (used to update UI) */
+  gint64 tick_start;
+  gint64 frames_window_start;
+  guint  frames_window;     /* Frames "rendered" in the last FPS_WINDOW_SIZE seconds window */
+  guint  frames_rendered;   /* Total frames "rendered" */
+  GdkEventType pressed_button_type;
+
+  gint video_width;
+  gint video_height;
+
+  /* Gst support */
+  GstElement *play;          /* playbin element */
+  GstElement *video_sink;    /* The video sink element used (glsinkbin or gtksink) */
+  GstElement *vis_plugin;    /* The visualization plugin */
+  GstBus     *bus;           /* playbin bus */
+  GstBuffer  *last_buffer;
+
+  GstTagList *audio_tags;
+  GstTagList *video_tags;
+  GstTagList *text_tags;
+
+  GstQuery *position_query;  /* Used to query position more quicker */
+
+  GstState state;            /* The desired state of the pipeline */
+  gint64   duration;         /* Stream duration */
+  guint    position;         /* Stream position in seconds */
+} SushiMediaBinPrivate;
+
+enum
+{
+  PROP_0,
+
+  PROP_URI,
+  PROP_VOLUME,
+  PROP_AUTOHIDE_TIMEOUT,
+  PROP_FULLSCREEN,
+  PROP_SHOW_STREAM_INFO,
+  PROP_AUDIO_MODE,
+  PROP_TITLE,
+  PROP_DESCRIPTION,
+  N_PROPERTIES
+};
+
+enum
+{
+  ERROR,
+  LAST_SIGNAL
+};
+
+static GParamSpec *properties[N_PROPERTIES];
+static guint sushi_media_bin_signals[LAST_SIGNAL] = { 0 };
+
+G_DEFINE_TYPE_WITH_PRIVATE (SushiMediaBin, sushi_media_bin, GTK_TYPE_BOX);
+
+#define SMB_PRIVATE(d) ((SushiMediaBinPrivate *) sushi_media_bin_get_instance_private(d))
+
+static void         sushi_media_bin_init_playbin (SushiMediaBin *self);
+static void         sushi_media_bin_set_tick_enabled (SushiMediaBin *self,
+                                                      gboolean enabled);
+static GtkWindow   *sushi_media_bin_window_new (SushiMediaBin *self);
+static const gchar *format_time (gint time);
+
+static inline gint64
+sushi_media_bin_get_position (SushiMediaBin *self)
+{
+  SushiMediaBinPrivate *priv = SMB_PRIVATE (self);
+  gint64 position;
+
+  if (!priv->play || !gst_element_query (priv->play, priv->position_query))
+    return 0;
+
+  gst_query_parse_position (priv->position_query, NULL, &position);
+
+  return position;
+}
+
+static GstStateChangeReturn
+sushi_media_bin_set_state (SushiMediaBin *self, GstState state)
+{
+  SushiMediaBinPrivate *priv = SMB_PRIVATE (self);
+
+  priv->state = state;
+  return gst_element_set_state (priv->play, state);
+}
+
+/* Action handlers */
+static void
+sushi_media_bin_toggle_playback (SushiMediaBin *self)
+{
+  if (SMB_PRIVATE (self)->state == GST_STATE_PLAYING)
+    sushi_media_bin_pause (self);
+  else
+    sushi_media_bin_play (self);
+}
+
+static void
+sushi_media_bin_toggle_fullscreen (SushiMediaBin *self)
+{
+  SushiMediaBinPrivate *priv = SMB_PRIVATE (self);
+
+  /* Do nothing in audio mode */
+  if (priv->audio_mode)
+    return;
+
+  sushi_media_bin_set_fullscreen (self, !priv->fullscreen);
+}
+
+static void
+sushi_media_bin_reveal_controls (SushiMediaBin *self)
+{
+  SushiMediaBinPrivate *priv = SMB_PRIVATE (self);
+
+  gdk_window_set_cursor (gtk_widget_get_window (priv->overlay), NULL);
+
+  /* We only show the top bar if there is something in the info labels */
+  if (!g_str_equal (gtk_label_get_label (priv->title_label), "") ||
+      !g_str_equal (gtk_label_get_label (priv->info_column_label[0]), "") ||
+      !g_str_equal (gtk_label_get_label (priv->info_column_label[2]), "") ||
+      !g_str_equal (gtk_label_get_label (priv->info_column_label[4]), ""))
+    gtk_revealer_set_reveal_child (priv->top_revealer, TRUE);
+
+  gtk_revealer_set_reveal_child (priv->bottom_revealer, TRUE);
+}
+
+static gboolean
+revealer_timeout (gpointer data)
+{
+  SushiMediaBinPrivate *priv = SMB_PRIVATE (data);
+  GdkWindow *window;
+
+  if (++priv->timeout_count < priv->autohide_timeout)
+    return G_SOURCE_CONTINUE;
+  
+  window = gtk_widget_get_window (priv->overlay);
+  
+  if (window != NULL)
+    gdk_window_set_cursor (window, priv->blank_cursor);
+
+  gtk_revealer_set_reveal_child (priv->top_revealer, FALSE);
+  gtk_revealer_set_reveal_child (priv->bottom_revealer, FALSE);
+
+  priv->timeout_id = 0;
+
+  return G_SOURCE_REMOVE;
+}
+
+static inline void
+ensure_no_timeout(SushiMediaBinPrivate *priv)
+{
+  if (!priv->timeout_id)
+    return;
+
+  g_source_remove (priv->timeout_id);
+  priv->timeout_id = 0;
+}
+
+static void
+sushi_media_bin_revealer_timeout (SushiMediaBin *self, gboolean activate)
+{
+  SushiMediaBinPrivate *priv = SMB_PRIVATE (self);
+
+  if (activate)
+    {
+      /* Reset counter */
+      priv->timeout_count = 0;
+
+      if (!priv->timeout_id)
+        priv->timeout_id = g_timeout_add_seconds (1, revealer_timeout, self);
+    }
+  else
+   {
+      GdkWindow *window = gtk_widget_get_window (priv->overlay);
+
+      ensure_no_timeout (priv);
+
+      if (window)
+        gdk_window_set_cursor (window, NULL);
+   }
+}
+
+static void
+sushi_media_bin_action_toggle (SushiMediaBin *self, const gchar *action)
+{
+  SushiMediaBinPrivate *priv = SMB_PRIVATE (self);
+
+  g_return_if_fail (action != NULL);
+
+  if (g_str_equal (action, "playback"))
+    sushi_media_bin_toggle_playback (self);
+  else if (g_str_equal (action, "fullscreen"))
+    sushi_media_bin_toggle_fullscreen (self);
+  else if (g_str_equal (action, "show-stream-info"))
+    {
+      sushi_media_bin_set_show_stream_info (self, !priv->show_stream_info);
+      sushi_media_bin_reveal_controls (self);
+    }
+  else
+    g_warning ("Ignoring unknown toggle action %s", action);
+}
+
+static void
+sushi_media_bin_action_seek (SushiMediaBin *self, gint seconds)
+{
+  SushiMediaBinPrivate *priv = SMB_PRIVATE (self);
+  gint64 position = sushi_media_bin_get_position (self) + (seconds * GST_SECOND);
+
+  gst_element_seek_simple (priv->play, GST_FORMAT_TIME,
+                           GST_SEEK_FLAG_FLUSH |
+                           GST_SEEK_FLAG_ACCURATE,
+                           seconds ? CLAMP (position, 0, priv->duration) : 0);
+}
+
+/* Signals handlers */
+static gboolean
+on_overlay_button_press_event (GtkWidget   *widget,
+                               GdkEvent    *event,
+                               SushiMediaBin *self)
+{
+  SushiMediaBinPrivate *priv = SMB_PRIVATE (self);
+
+  if (event->button.button != GDK_BUTTON_PRIMARY)
+    return FALSE;
+
+  priv->pressed_button_type = event->type;
+  return TRUE;
+}
+
+static gboolean
+on_overlay_button_release_event (GtkWidget   *widget,
+                                 GdkEvent    *event,
+                                 SushiMediaBin *self)
+{
+  SushiMediaBinPrivate *priv = SMB_PRIVATE (self);
+
+  if (event->button.button != GDK_BUTTON_PRIMARY)
+    return FALSE;
+
+  if (priv->pressed_button_type == GDK_BUTTON_PRESS)
+    {
+      sushi_media_bin_toggle_playback (self);
+    }
+  else if (priv->pressed_button_type == GDK_2BUTTON_PRESS)
+    {
+      sushi_media_bin_toggle_fullscreen (self);
+      sushi_media_bin_toggle_playback (self);
+    }
+
+  /* Reset state, since some widgets like GtkButton do not consume
+   * the last button press release event
+   */
+  priv->pressed_button_type = GDK_NOTHING;
+
+  return TRUE;
+}
+
+static gboolean
+on_revealer_leave_notify_event (GtkWidget   *widget,
+                                GdkEvent    *event,
+                                SushiMediaBin *self)
+{
+  sushi_media_bin_revealer_timeout (self, TRUE);
+  return FALSE;
+}
+
+static gboolean
+on_revealer_motion_notify_event (GtkWidget   *widget,
+                                 GdkEvent    *event,
+                                 SushiMediaBin *self)
+{
+  /* Do not hide controls and restore pointer */
+  sushi_media_bin_revealer_timeout (self, FALSE);
+
+  return TRUE;
+}
+
+static gboolean
+on_overlay_motion_notify_event (GtkWidget   *widget,
+                                GdkEvent    *event,
+                                SushiMediaBin *self)
+{
+  sushi_media_bin_reveal_controls (self);
+  sushi_media_bin_revealer_timeout (self, TRUE);
+  return FALSE;
+}
+
+static void
+on_playback_adjustment_value_changed (GtkAdjustment *adjustment,
+                                      SushiMediaBin   *self)
+{
+  SushiMediaBinPrivate *priv = SMB_PRIVATE (self);
+
+  if (priv->ignore_adjustment_changes)
+    return;
+
+  priv->position = gtk_adjustment_get_value (adjustment);
+
+  gst_element_seek_simple (priv->play,
+                           GST_FORMAT_TIME,
+                           GST_SEEK_FLAG_ACCURATE |
+                           GST_SEEK_FLAG_FLUSH,
+                           priv->position * GST_SECOND);
+}
+
+static gchar *
+on_progress_scale_format_value (GtkScale    *scale,
+                                gdouble      value,
+                                SushiMediaBin *self)
+{
+  /* FIXME: CSS padding does not work as expected, add some padding here */
+  return g_strdup_printf ("  %s  ", format_time (value));
+}
+
+static void
+on_volume_popup_show (GtkWidget *popup, SushiMediaBin *self)
+{
+  /* Do not hide controls */
+  sushi_media_bin_revealer_timeout (self, FALSE);
+}
+
+static void
+on_volume_popup_hide (GtkWidget *popup, SushiMediaBin *self)
+{
+  sushi_media_bin_revealer_timeout (self, TRUE);
+}
+
+static inline void
+sushi_media_bin_update_state (SushiMediaBin *self)
+{
+  SushiMediaBinPrivate *priv = SMB_PRIVATE (self);
+
+  if (priv->uri && priv->video_sink)
+    {
+      g_object_set (priv->play, "uri", priv->uri, NULL);
+      gst_element_set_state (priv->play, priv->state);
+    }
+}
+
+static GdkPixbuf *
+sushi_media_bin_video_pixbuf_new (SushiMediaBin *self)
+{
+  SushiMediaBinPrivate *priv = SMB_PRIVATE (self);
+  gint width, height, video_width, video_height, dx, dy;
+  cairo_surface_t *surface;
+  gdouble scale = 1.0;
+  GdkPixbuf *pixbuf;
+  cairo_t *cr;
+
+  width = gtk_widget_get_allocated_width (GTK_WIDGET (self));
+  height = gtk_widget_get_allocated_height (GTK_WIDGET (self));
+
+  video_width = gtk_widget_get_allocated_width (priv->video_widget);
+  video_height = gtk_widget_get_allocated_height (priv->video_widget);
+
+  if ((width != video_width || height != video_height) &&
+      priv->video_width && priv->video_height)
+    {
+      scale = MIN (width/(gdouble)priv->video_width, height/(gdouble)priv->video_height);
+
+      dx = ABS (video_width - priv->video_width) * scale;
+      dy = ABS (video_height - priv->video_height) * scale;
+      width = video_width * scale;
+      height = video_height * scale;
+    }
+  else
+    dx = dy = 0;
+
+  surface = cairo_image_surface_create (CAIRO_FORMAT_RGB24, width, height);
+  cr = cairo_create (surface);
+
+  if (scale != 1.0)
+    cairo_scale (cr, scale, scale);
+
+  gtk_widget_draw (priv->video_widget, cr);
+
+  pixbuf = gdk_pixbuf_get_from_surface (surface, dx/2, dy/2, width-dx, height-dy);
+
+  cairo_destroy (cr);
+  cairo_surface_destroy (surface);
+
+  return pixbuf;
+}
+
+static inline gboolean
+sushi_media_bin_gl_check (GtkWidget *widget)
+{
+  static gsize gl_works = 0;
+
+  if (g_once_init_enter (&gl_works))
+    {
+      GError *error = NULL;
+      gsize works = 1;
+      GdkGLContext *context;
+      GdkWindow *window;
+
+      if ((window  = gtk_widget_get_window (widget)) &&
+           (context = gdk_window_create_gl_context (window, &error)))
+        {
+          const gchar *vendor, *renderer;
+
+          gdk_gl_context_make_current (context);
+
+          vendor   = (const gchar *) glGetString (GL_VENDOR);
+          renderer = (const gchar *) glGetString (GL_RENDERER);
+
+          GST_INFO ("GL Vendor: %s, renderer: %s", vendor, renderer);
+
+          if (g_str_equal (vendor, "nouveau"))
+            GST_WARNING ("nouveau is blacklisted, since sharing gl contexts in "
+                         "multiple threads is not supported "
+                         "and will eventually make it crash.");
+          else if (g_strstr_len (renderer, -1, "Gallium") &&
+                   g_strstr_len (renderer, -1, "llvmpipe"))
+            GST_INFO ("Detected software GL rasterizer, falling back to gtksink");
+          else
+            works = 2;
+
+          gdk_gl_context_clear_current ();
+        }
+
+        if (error)
+          {
+            GST_WARNING ("Could not window to create GL context, %s", error->message);
+            g_error_free (error);
+          }
+
+      g_once_init_leave (&gl_works, works);
+    }
+
+  return (gl_works > 1);
+}
+
+static inline void
+sushi_media_bin_init_video_sink (SushiMediaBin *self)
+{
+  SushiMediaBinPrivate *priv = SMB_PRIVATE (self);
+  GtkWidget *video_widget = NULL;
+  GstElement *video_sink = NULL;
+
+  if (priv->video_sink)
+    return;
+
+  if (priv->audio_mode)
+    {
+      video_sink = gst_element_factory_make ("fakesink", "SushiMediaBinNullSink");
+      g_object_set (video_sink, "sync", TRUE, NULL);
+      g_object_set (priv->play, "video-sink", video_sink, NULL);
+      priv->video_sink = gst_object_ref_sink (video_sink);
+      return;
+    }
+
+  if (sushi_media_bin_gl_check (GTK_WIDGET (self)))
+    {
+      video_sink = gst_element_factory_make ("glsinkbin", "SushiMediaBinGLVideoSink");
+
+      if (video_sink)
+        {
+          GstElement *gtkglsink = gst_element_factory_make ("gtkglsink", NULL);
+
+          if (gtkglsink)
+            {
+              GST_INFO ("Using gtkglsink");
+              g_object_set (video_sink, "sink", gtkglsink, NULL);
+              g_object_get (gtkglsink, "widget", &video_widget, NULL);
+            }
+          else
+            {
+              GST_WARNING ("Could not create gtkglsink");
+              gst_object_replace ((GstObject**)&video_sink, NULL);
+            }
+        }
+      else
+        {
+          GST_WARNING ("Could not create glsinkbin");
+        }
+    }
+
+  /* Fallback to gtksink */
+  if (!video_sink)
+    {
+      GST_INFO ("Falling back to gtksink");
+      video_sink = gst_element_factory_make ("gtksink", NULL);
+      g_object_get (video_sink, "widget", &video_widget, NULL);
+    }
+
+  /* We use a null sink as a last resort */
+  if (video_sink && video_widget)
+    {
+      g_object_set (video_widget, "expand", TRUE, NULL);
+
+      /* And pack it where we want the video to show up */
+      gtk_container_add (GTK_CONTAINER (priv->overlay), video_widget);
+      gtk_widget_show (video_widget);
+
+      /* g_object_get() returns a new reference */
+      priv->video_widget = video_widget;
+    }
+  else
+    {
+      GtkWidget *img = gtk_image_new_from_icon_name ("image-missing",
+                                                     GTK_ICON_SIZE_DIALOG);
+
+      GST_WARNING ("Could not get video widget from gtkglsink/gtksink, falling back to fakesink");
+
+      g_object_unref (video_widget);
+      gst_object_unref (video_sink);
+      video_sink = gst_element_factory_make ("fakesink", "SushiMediaBinFakeSink");
+      g_object_set (video_sink, "sync", TRUE, NULL);
+
+      gtk_container_add (GTK_CONTAINER (priv->overlay), img);
+      gtk_widget_show (img);
+
+      /* FIXME: the overlay does not get motion and press events with this code path */
+    }
+
+  /* Setup playbin video sink */
+  if (video_sink)
+    {
+      g_object_set (priv->play, "video-sink", video_sink, NULL);
+      priv->video_sink = gst_object_ref_sink (video_sink);
+    }
+}
+
+static inline void
+sushi_media_bin_deinit_video_sink (SushiMediaBin *self)
+{
+  SushiMediaBinPrivate *priv = SMB_PRIVATE (self);
+
+  /* Stop Playback to give gst a chance to cleanup its mess */
+  if (priv->play)
+    gst_element_set_state (priv->play, GST_STATE_NULL);
+
+  /* Stop bus watch */
+  if (priv->bus)
+    {
+      gst_bus_set_flushing (priv->bus, TRUE);
+      gst_bus_remove_watch (priv->bus);
+      gst_object_replace ((GstObject**)&priv->bus, NULL);
+    }
+
+  /* Unref video sink */
+  gst_object_replace ((GstObject**)&priv->video_sink, NULL);
+
+  /* Unref video widget */
+  g_clear_object (&priv->video_widget);
+
+  /* Unref playbin */
+  gst_object_replace ((GstObject**)&priv->play, NULL);
+}
+
+static void
+sushi_media_bin_fullscreen_apply (SushiMediaBin *self, gboolean fullscreen)
+{
+  SushiMediaBinPrivate *priv = SMB_PRIVATE (self);
+  gint64 position = -1;
+
+  if ((fullscreen && priv->fullscreen_window) ||
+      (!fullscreen && !priv->fullscreen_window))
+    return;
+
+  /*
+   * To avoid flickering, this will make the widget pack an image with the last
+   * frame in the container before reparenting the video widget in the
+   * fullscreen window
+   */
+  if (!priv->tmp_image)
+    {
+      GdkPixbuf *pixbuf = sushi_media_bin_video_pixbuf_new (self);
+      priv->tmp_image = gtk_image_new_from_pixbuf (pixbuf);
+      g_object_set (priv->tmp_image, "expand", TRUE, NULL);
+      g_object_unref (pixbuf);
+    }
+
+  /*
+   * FIXME: GtkGstGLWidget does not support reparenting to a different toplevel
+   * because the gl context is different and the pipeline does not know it
+   * changes, so as a temporary workaround we simply reconstruct the whole
+   * pipeline.
+   *
+   * See bug https://bugzilla.gnome.org/show_bug.cgi?id=775045
+   */
+  if ((priv->state == GST_STATE_PAUSED || priv->state == GST_STATE_PLAYING) &&
+      g_strcmp0 (G_OBJECT_TYPE_NAME (priv->video_sink), "GstGLSinkBin") == 0)
+    {
+      /* NOTE: here we could set tmp_image to the content of the current sample
+       * but it wont be updated until the main window is show at which point
+       * we will see the old frame anyways.
+       */
+      position = sushi_media_bin_get_position (self);
+
+      gtk_container_remove (GTK_CONTAINER (priv->overlay), priv->video_widget);
+      sushi_media_bin_deinit_video_sink (self);
+    }
+
+  g_object_ref (priv->overlay);
+
+  if (fullscreen)
+    {
+      priv->fullscreen_window = g_object_ref (sushi_media_bin_window_new (self));
+
+      /* Reparent video widget in a fullscreen window */
+      gtk_container_remove (GTK_CONTAINER (priv->stack), priv->overlay);
+
+      /* Pack an image with the last frame inside the bin */
+      gtk_container_add (GTK_CONTAINER (priv->stack), priv->tmp_image);
+      gtk_widget_show (priv->tmp_image);
+      gtk_stack_set_visible_child (GTK_STACK (priv->stack), priv->tmp_image);
+
+      /* Pack video in the fullscreen window */
+      gtk_container_add (GTK_CONTAINER (priv->fullscreen_window), priv->overlay);
+
+      gtk_window_fullscreen (priv->fullscreen_window);
+      gtk_window_present (priv->fullscreen_window);
+
+      /* Hide cursor if controls are hidden */
+      if (!gtk_revealer_get_reveal_child (priv->bottom_revealer))
+        gdk_window_set_cursor (gtk_widget_get_window (GTK_WIDGET (priv->fullscreen_window)),
+                               priv->blank_cursor);
+
+      gtk_image_set_from_icon_name (priv->fullscreen_image, SMB_ICON_NAME_RESTORE, SMB_ICON_SIZE);
+    }
+  else
+    {
+      gtk_container_remove (GTK_CONTAINER (priv->stack), priv->tmp_image);
+      priv->tmp_image = NULL;
+
+      /* Reparent video widget back into ourselves */
+      gtk_container_remove (GTK_CONTAINER (priv->fullscreen_window), priv->overlay);
+      gtk_container_add (GTK_CONTAINER (priv->stack), priv->overlay);
+      gtk_stack_set_visible_child (GTK_STACK (priv->stack), priv->overlay);
+
+      gtk_widget_destroy (GTK_WIDGET (priv->fullscreen_window));
+      g_clear_object (&priv->fullscreen_window);
+
+      gtk_image_set_from_icon_name (priv->fullscreen_image, SMB_ICON_NAME_FULLSCREEN, SMB_ICON_SIZE);
+
+      gtk_widget_grab_focus (GTK_WIDGET (self));
+    }
+
+  /*
+   * FIXME: See bug https://bugzilla.gnome.org/show_bug.cgi?id=775045
+   */
+  if (priv->play == NULL)
+    {
+      sushi_media_bin_init_playbin (self);
+      sushi_media_bin_init_video_sink (self);
+
+      g_object_set (priv->play, "uri", priv->uri, NULL);
+
+      /* Init new pipeline */
+      gst_element_set_state (priv->play, GST_STATE_PAUSED);
+      gst_element_get_state (priv->play, NULL, NULL, GST_CLOCK_TIME_NONE);
+
+      /* Seek to position */
+      gst_element_seek_simple (priv->play, GST_FORMAT_TIME,
+                               GST_SEEK_FLAG_ACCURATE | GST_SEEK_FLAG_FLUSH,
+                               position);
+      gst_message_unref (gst_bus_pop_filtered (priv->bus, GST_MESSAGE_ASYNC_DONE));
+
+      /* Resume playback */
+      if (priv->state == GST_STATE_PLAYING)
+        {
+          gst_element_set_state (priv->play, GST_STATE_PLAYING);
+          gst_element_get_state (priv->play, NULL, NULL, GST_CLOCK_TIME_NONE);
+        }
+    }
+
+  g_object_unref (priv->overlay);
+}
+
+static void
+on_sushi_media_bin_realize (GtkWidget *widget, SushiMediaBin *self)
+{
+  SushiMediaBinPrivate *priv = SMB_PRIVATE (self);
+
+  /* Create a blank_cursor */
+  priv->blank_cursor = gdk_cursor_new_from_name (gtk_widget_get_display (widget),
+                                                 "none");
+
+  /* Create video sink */
+  sushi_media_bin_init_video_sink (self);
+
+  if (priv->fullscreen)
+    sushi_media_bin_fullscreen_apply (self, TRUE);
+
+  /* Make playbin show the first video frame if there is an URI */
+  sushi_media_bin_update_state (self);
+
+  /* Disconnect after initialization */
+  g_signal_handlers_disconnect_by_func (widget, on_sushi_media_bin_realize, self);
+}
+
+static void
+on_sushi_media_bin_unrealize (GtkWidget *widget, SushiMediaBin *self)
+{
+  SushiMediaBinPrivate *priv = SMB_PRIVATE (self);
+
+  /* Remove controls timeout */
+  ensure_no_timeout (priv);
+
+  /* Disconnect after completion */
+  g_signal_handlers_disconnect_by_func (widget, on_sushi_media_bin_unrealize, self);
+}
+
+static gboolean
+sushi_media_bin_error (SushiMediaBin *self, GError *error)
+{
+  /* TODO: properly present errors to the user */
+  g_warning ("%s", error->message);
+  return TRUE;
+}
+
+static void
+sushi_media_bin_init_volume_button (SushiMediaBin    *self,
+                                    GtkScaleButton *button,
+                                    gboolean        stop_timeout)
+{
+  GtkWidget *popup = gtk_scale_button_get_popup (button);
+
+  if (stop_timeout)
+    {
+      g_signal_connect (popup, "show", G_CALLBACK (on_volume_popup_show), self);
+      g_signal_connect (popup, "hide", G_CALLBACK (on_volume_popup_hide), self);
+    }
+
+  gtk_style_context_add_class (gtk_widget_get_style_context (popup), "sushi-media-bin");
+
+  /* Hide volume popup buttons */
+  gtk_widget_hide (gtk_scale_button_get_plus_button (button));
+  gtk_widget_hide (gtk_scale_button_get_minus_button (button));
+}
+
+static void
+sushi_media_bin_init (SushiMediaBin *self)
+{
+  SushiMediaBinPrivate *priv = SMB_PRIVATE (self);
+  gint i;
+
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  priv->state = SMB_INITIAL_STATE;
+  priv->autohide_timeout = AUTOHIDE_TIMEOUT_DEFAULT;
+  priv->pressed_button_type = GDK_NOTHING;
+  priv->dump_dot_file = (g_getenv ("GST_DEBUG_DUMP_DOT_DIR") != NULL);
+
+  sushi_media_bin_init_playbin (self);
+
+  /* Create info box column labels */
+  for (i = 0; i < INFO_N_COLUMNS; i++)
+    {
+      GtkWidget *label = gtk_label_new ("");
+      priv->info_column_label[i] = GTK_LABEL (label);
+      gtk_container_add (GTK_CONTAINER (priv->info_box), label);
+      gtk_widget_set_valign (label, GTK_ALIGN_START);
+      gtk_widget_show (label);
+    }
+
+  /* Cache position query */
+  priv->position_query = gst_query_new_position (GST_FORMAT_TIME);
+
+  /* Make both buttons look the same */
+  g_object_bind_property (priv->playback_image, "icon-name",
+                          priv->audio_playback_image, "icon-name",
+                          G_BINDING_SYNC_CREATE);
+
+  sushi_media_bin_init_volume_button (self, priv->volume_button, TRUE);
+  sushi_media_bin_init_volume_button (self, priv->audio_volume_button, FALSE);
+}
+
+static void
+sushi_media_bin_dispose (GObject *object)
+{
+  SushiMediaBin *self = SUSHI_MEDIA_BIN (object);
+  SushiMediaBinPrivate *priv = SMB_PRIVATE (self);
+
+  /* Remove controls timeout */
+  ensure_no_timeout (priv);
+
+  /* Finalize gstreamer related objects */
+  sushi_media_bin_deinit_video_sink (self);
+
+  /* Destroy fullscreen window */
+  if (priv->fullscreen_window)
+    {
+      gtk_widget_destroy (GTK_WIDGET (priv->fullscreen_window));
+      g_clear_object (&priv->fullscreen_window);
+    }
+
+  /* Unref cursor */
+  g_clear_object (&priv->blank_cursor);
+
+  G_OBJECT_CLASS (sushi_media_bin_parent_class)->dispose (object);
+}
+
+static void
+sushi_media_bin_finalize (GObject *object)
+{
+  SushiMediaBin *self = SUSHI_MEDIA_BIN (object);
+  SushiMediaBinPrivate *priv = SMB_PRIVATE (self);
+
+  ensure_no_timeout(priv);
+
+  /* Clear position query */
+  g_clear_pointer (&priv->position_query, gst_query_unref);
+
+  /* Remove frame clock tick callback */
+  sushi_media_bin_set_tick_enabled (self, FALSE);
+
+  /* Clear tag lists */
+  g_clear_pointer (&priv->audio_tags, gst_tag_list_unref);
+  g_clear_pointer (&priv->video_tags, gst_tag_list_unref);
+  g_clear_pointer (&priv->text_tags, gst_tag_list_unref);
+
+  /* Free properties */
+  g_clear_pointer (&priv->uri, g_free);
+  g_clear_pointer (&priv->title, g_free);
+  g_clear_pointer (&priv->description, g_free);
+
+  G_OBJECT_CLASS (sushi_media_bin_parent_class)->finalize (object);
+}
+
+static inline void
+sushi_media_bin_set_audio_mode (SushiMediaBin *self, gboolean audio_mode)
+{
+  SushiMediaBinPrivate *priv = SMB_PRIVATE (self);
+
+  priv->audio_mode = audio_mode;
+
+  if (audio_mode)
+    gtk_stack_set_visible_child (GTK_STACK (priv->stack), priv->audio_box);
+}
+
+static void
+sushi_media_bin_set_property (GObject      *object,
+                              guint         prop_id,
+                              const GValue *value,
+                              GParamSpec   *pspec)
+{
+  g_return_if_fail (SUSHI_IS_MEDIA_BIN (object));
+
+  switch (prop_id)
+    {
+    case PROP_URI:
+      sushi_media_bin_set_uri (SUSHI_MEDIA_BIN (object),
+                               g_value_get_string (value));
+      break;
+    case PROP_VOLUME:
+      sushi_media_bin_set_volume (SUSHI_MEDIA_BIN (object),
+                                  g_value_get_double (value));
+      break;
+    case PROP_AUTOHIDE_TIMEOUT:
+      sushi_media_bin_set_autohide_timeout (SUSHI_MEDIA_BIN (object),
+                                            g_value_get_int (value));
+      break;
+    case PROP_FULLSCREEN:
+      sushi_media_bin_set_fullscreen (SUSHI_MEDIA_BIN (object),
+                                      g_value_get_boolean (value));
+      break;
+    case PROP_SHOW_STREAM_INFO:
+      sushi_media_bin_set_show_stream_info (SUSHI_MEDIA_BIN (object),
+                                            g_value_get_boolean (value));
+      break;
+    case PROP_AUDIO_MODE:
+      sushi_media_bin_set_audio_mode (SUSHI_MEDIA_BIN (object),
+                                      g_value_get_boolean (value));
+      break;
+    case PROP_TITLE:
+      sushi_media_bin_set_title (SUSHI_MEDIA_BIN (object),
+                                 g_value_get_string (value));
+      break;
+    case PROP_DESCRIPTION:
+      sushi_media_bin_set_description (SUSHI_MEDIA_BIN (object),
+                                       g_value_get_string (value));
+      break;
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+      break;
+    }
+}
+
+static void
+sushi_media_bin_get_property (GObject    *object,
+                              guint       prop_id,
+                              GValue     *value,
+                              GParamSpec *pspec)
+{
+  SushiMediaBinPrivate *priv;
+
+  g_return_if_fail (SUSHI_IS_MEDIA_BIN (object));
+  priv = SMB_PRIVATE (SUSHI_MEDIA_BIN (object));
+
+  switch (prop_id)
+    {
+    case PROP_URI:
+      g_value_set_string (value, priv->uri);
+      break;
+    case PROP_VOLUME:
+      g_value_set_double (value, gtk_adjustment_get_value (priv->volume_adjustment));
+      break;
+    case PROP_AUTOHIDE_TIMEOUT:
+      g_value_set_int (value, priv->autohide_timeout);
+      break;
+    case PROP_FULLSCREEN:
+      g_value_set_boolean (value, priv->fullscreen);
+      break;
+    case PROP_SHOW_STREAM_INFO:
+      g_value_set_boolean (value, priv->show_stream_info);
+      break;
+    case PROP_AUDIO_MODE:
+      g_value_set_boolean (value, priv->audio_mode);
+      break;
+    case PROP_TITLE:
+      g_value_set_string (value, priv->title);
+      break;
+    case PROP_DESCRIPTION:
+      g_value_set_string (value, priv->description);
+      break;
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+      break;
+    }
+}
+
+static GtkSizeRequestMode
+sushi_media_bin_get_request_mode (GtkWidget *self)
+{
+  return GTK_SIZE_REQUEST_CONSTANT_SIZE;
+}
+
+
+static void
+sushi_media_bin_get_preferred_width (GtkWidget *self,
+                                     gint      *minimum_width,
+                                     gint      *natural_width)
+{
+  SushiMediaBinPrivate *priv = SMB_PRIVATE (SUSHI_MEDIA_BIN (self));
+
+  if (priv->audio_mode)
+    {
+      GTK_WIDGET_CLASS (sushi_media_bin_parent_class)->get_preferred_width
+        (self, minimum_width, natural_width);
+    }
+  else
+    {
+      *minimum_width = 320;
+      *natural_width = priv->video_width ? priv->video_width : 640;
+    }
+}
+
+static void
+sushi_media_bin_get_preferred_height (GtkWidget *self,
+                                      gint      *minimum_height,
+                                      gint      *natural_height)
+{
+  SushiMediaBinPrivate *priv = SMB_PRIVATE (SUSHI_MEDIA_BIN (self));
+
+  if (priv->audio_mode)
+    {
+      GTK_WIDGET_CLASS (sushi_media_bin_parent_class)->get_preferred_height
+        (self, minimum_height, natural_height);
+    }
+  else
+    {
+      *minimum_height = 240;
+      *natural_height = priv->video_height ? priv->video_height : 480;
+    }
+}
+
+#define SMB_DEFINE_ACTION_SIGNAL(klass, name, handler,...) \
+  g_signal_new_class_handler (name, \
+                              G_TYPE_FROM_CLASS (klass), \
+                              G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, \
+                              G_CALLBACK (handler), \
+                              NULL, NULL, NULL, \
+                              G_TYPE_NONE, __VA_ARGS__)
+
+static void
+sushi_media_bin_class_init (SushiMediaBinClass *klass)
+{
+  GObjectClass   *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->dispose = sushi_media_bin_dispose;
+  object_class->finalize = sushi_media_bin_finalize;
+  object_class->set_property = sushi_media_bin_set_property;
+  object_class->get_property = sushi_media_bin_get_property;
+
+  widget_class->get_request_mode = sushi_media_bin_get_request_mode;
+  widget_class->get_preferred_width = sushi_media_bin_get_preferred_width;
+  widget_class->get_preferred_height = sushi_media_bin_get_preferred_height;
+
+  /* Properties */
+  properties[PROP_URI] =
+    g_param_spec_string ("uri",
+                         "URI",
+                         "The Media URI to playback",
+                         NULL,
+                         G_PARAM_READWRITE);
+
+  properties[PROP_VOLUME] =
+    g_param_spec_double ("volume",
+                         "Volume",
+                         "Stream volume",
+                         0.0, 1.0, 1.0,
+                         G_PARAM_READWRITE);
+
+  properties[PROP_AUTOHIDE_TIMEOUT] =
+    g_param_spec_int ("autohide-timeout",
+                      "Auto hide timeout",
+                      "Controls auto hide timeout in seconds",
+                      0, G_MAXINT,
+                      AUTOHIDE_TIMEOUT_DEFAULT,
+                      G_PARAM_READWRITE);
+
+  properties[PROP_FULLSCREEN] =
+    g_param_spec_boolean ("fullscreen",
+                          "Fullscreen",
+                          "Whether to show the video in fullscreen or not",
+                          FALSE,
+                          G_PARAM_READWRITE);
+
+  properties[PROP_SHOW_STREAM_INFO] =
+    g_param_spec_boolean ("show-stream-info",
+                          "Show stream info",
+                          "Whether to show stream information or not",
+                          FALSE,
+                          G_PARAM_READWRITE);
+
+  properties[PROP_AUDIO_MODE] =
+    g_param_spec_boolean ("audio-mode",
+                          "Audio Mode",
+                          "Wheter to show controls suitable for audio files only",
+                          FALSE,
+                          G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY);
+
+  properties[PROP_TITLE] =
+    g_param_spec_string ("title",
+                         "Title",
+                         "The title to display",
+                         NULL,
+                         G_PARAM_READWRITE);
+
+  properties[PROP_DESCRIPTION] =
+    g_param_spec_string ("description",
+                         "Description",
+                         "Audio/Video description",
+                         NULL,
+                         G_PARAM_READWRITE);
+
+  g_object_class_install_properties (object_class, N_PROPERTIES, properties);
+
+  /**
+   * SushiMediaBin::error:
+   * @self: the #SushiMediaBin which received the signal.
+   * @error: the #GError
+   */
+  sushi_media_bin_signals[ERROR] =
+      g_signal_new_class_handler ("error",
+                                  G_TYPE_FROM_CLASS (object_class),
+                                  G_SIGNAL_RUN_LAST,
+                                  G_CALLBACK (sushi_media_bin_error),
+                                  g_signal_accumulator_true_handled, NULL,
+                                  NULL,
+                                  G_TYPE_BOOLEAN, 1, G_TYPE_ERROR);
+
+  /* Action signals for key bindings */
+  SMB_DEFINE_ACTION_SIGNAL (object_class, "toggle", sushi_media_bin_action_toggle, 1, G_TYPE_STRING);
+  SMB_DEFINE_ACTION_SIGNAL (object_class, "seek", sushi_media_bin_action_seek, 1, G_TYPE_INT);
+
+  /* Template */
+  gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/Sushi/libsushi/SushiMediaBin.ui");
+
+  gtk_widget_class_bind_template_child_private (widget_class, SushiMediaBin, stack);
+  gtk_widget_class_bind_template_child_private (widget_class, SushiMediaBin, playback_adjustment);
+  gtk_widget_class_bind_template_child_private (widget_class, SushiMediaBin, volume_adjustment);
+  gtk_widget_class_bind_template_child_private (widget_class, SushiMediaBin, playback_image);
+  gtk_widget_class_bind_template_child_private (widget_class, SushiMediaBin, fullscreen_image);
+
+  gtk_widget_class_bind_template_child_private (widget_class, SushiMediaBin, overlay);
+  gtk_widget_class_bind_template_child_private (widget_class, SushiMediaBin, play_box);
+  gtk_widget_class_bind_template_child_private (widget_class, SushiMediaBin, volume_button);
+  gtk_widget_class_bind_template_child_private (widget_class, SushiMediaBin, title_label);
+  gtk_widget_class_bind_template_child_private (widget_class, SushiMediaBin, info_box);
+  gtk_widget_class_bind_template_child_private (widget_class, SushiMediaBin, duration_label);
+  gtk_widget_class_bind_template_child_private (widget_class, SushiMediaBin, top_revealer);
+  gtk_widget_class_bind_template_child_private (widget_class, SushiMediaBin, bottom_revealer);
+
+  gtk_widget_class_bind_template_child_private (widget_class, SushiMediaBin, audio_box);
+  gtk_widget_class_bind_template_child_private (widget_class, SushiMediaBin, audio_volume_button);
+  gtk_widget_class_bind_template_child_private (widget_class, SushiMediaBin, audio_position_label);
+  gtk_widget_class_bind_template_child_private (widget_class, SushiMediaBin, audio_playback_image);
+
+  gtk_widget_class_bind_template_callback (widget_class, on_sushi_media_bin_realize);
+  gtk_widget_class_bind_template_callback (widget_class, on_sushi_media_bin_unrealize);
+
+  gtk_widget_class_bind_template_callback (widget_class, on_overlay_motion_notify_event);
+  gtk_widget_class_bind_template_callback (widget_class, on_overlay_button_press_event);
+  gtk_widget_class_bind_template_callback (widget_class, on_overlay_button_release_event);
+
+  gtk_widget_class_bind_template_callback (widget_class, on_revealer_motion_notify_event);
+  gtk_widget_class_bind_template_callback (widget_class, on_revealer_leave_notify_event);
+
+  gtk_widget_class_bind_template_callback (widget_class, on_progress_scale_format_value);
+  gtk_widget_class_bind_template_callback (widget_class, on_playback_adjustment_value_changed);
+
+  gtk_widget_class_bind_template_callback (widget_class, sushi_media_bin_toggle_playback);
+  gtk_widget_class_bind_template_callback (widget_class, sushi_media_bin_toggle_fullscreen);
+
+  /* Setup CSS */
+  gtk_widget_class_set_css_name (widget_class, "sushi-media-bin");
+
+  if (gdk_screen_get_default ())
+    {
+      GtkCssProvider *css_provider = gtk_css_provider_new ();
+
+      gtk_css_provider_load_from_resource (css_provider, "/org/gnome/Sushi/libsushi/sushi-media-bin.css");
+      gtk_style_context_add_provider_for_screen (gdk_screen_get_default (),
+                                                 GTK_STYLE_PROVIDER (css_provider),
+                                                 GTK_STYLE_PROVIDER_PRIORITY_APPLICATION-10);
+      g_object_unref (css_provider);
+    }
+
+  /* Init GStreamer */
+  gst_init_check (NULL, NULL, NULL);
+  GST_DEBUG_CATEGORY_INIT (sushi_media_bin_debug, "SushiMediaBin", 0, "SushiMediaBin audio/video widget");
+}
+
+/*************************** Fullscreen Window Type ***************************/
+
+G_DECLARE_FINAL_TYPE (SushiMediaBinWindow, sushi_media_bin_window, SUSHI, MEDIA_BIN_WINDOW, GtkWindow)
+
+struct _SushiMediaBinWindow
+{
+  GtkWindow parent;
+};
+
+G_DEFINE_TYPE (SushiMediaBinWindow, sushi_media_bin_window, GTK_TYPE_WINDOW);
+
+static void
+sushi_media_bin_window_init (SushiMediaBinWindow *self)
+{
+  gtk_window_set_decorated (GTK_WINDOW (self), FALSE);
+}
+
+static void
+sushi_media_bin_window_class_init (SushiMediaBinWindowClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  gtk_widget_class_set_css_name (GTK_WIDGET_CLASS (klass), "sushi-media-bin");
+
+  SMB_DEFINE_ACTION_SIGNAL (object_class, "toggle", NULL, 1, G_TYPE_STRING);
+  SMB_DEFINE_ACTION_SIGNAL (object_class, "seek", NULL, 1, G_TYPE_INT);
+}
+
+static GtkWindow *
+sushi_media_bin_window_new (SushiMediaBin *bin)
+{
+  GObject *window = g_object_new (sushi_media_bin_window_get_type (), NULL);
+
+  g_signal_connect_swapped (window, "delete-event", G_CALLBACK (sushi_media_bin_toggle_fullscreen), bin);
+  g_signal_connect_swapped (window, "toggle", G_CALLBACK (sushi_media_bin_action_toggle), bin);
+  g_signal_connect_swapped (window, "seek", G_CALLBACK (sushi_media_bin_action_seek), bin);
+
+  return (GtkWindow *) window;
+}
+
+/*********************************** Utils ************************************/
+
+#define TIME_HOURS(t)   (t / 3600)
+#define TIME_MINUTES(t) ((t % 3600) / 60)
+#define TIME_SECONDS(t) (t % 60)
+
+static const gchar *
+format_time (gint time)
+{
+  static gchar buffer[16];
+  gint hours = TIME_HOURS (time);
+
+  if (hours)
+    g_snprintf (buffer,
+                sizeof (buffer),
+                "%d:%02d:%02d",
+                hours,
+                TIME_MINUTES (time),
+                TIME_SECONDS (time));
+  else
+    g_snprintf (buffer,
+                sizeof (buffer),
+                "%d:%02d",
+                TIME_MINUTES (time),
+                TIME_SECONDS (time));
+
+  return (const gchar *) buffer;
+}
+
+static void
+on_widget_style_updated (GtkWidget *widget, gpointer data)
+{
+  gboolean visible = GPOINTER_TO_INT (data);
+  gdouble opacity;
+
+  gtk_style_context_get (gtk_widget_get_style_context (widget),
+                         gtk_widget_get_state_flags (widget),
+                         "opacity", &opacity, NULL);
+
+  if ((visible && opacity >= 1.0) || (!visible && opacity == 0.0))
+    {
+      gtk_widget_set_visible (widget, visible);
+      g_signal_handlers_disconnect_by_func (widget, on_widget_style_updated, data);
+    }
+}
+
+static void
+widget_set_visible (GtkWidget *widget, gboolean visible)
+{
+  GtkStyleContext *context = gtk_widget_get_style_context (widget);
+
+  g_signal_handlers_disconnect_by_func (widget, on_widget_style_updated, GINT_TO_POINTER (TRUE));
+  g_signal_handlers_disconnect_by_func (widget, on_widget_style_updated, GINT_TO_POINTER (FALSE));
+
+  gtk_style_context_remove_class (context, visible ? "hide" : "show");
+  gtk_style_context_add_class (context, visible ? "show" : "hide");
+
+  if (visible)
+    gtk_widget_show (widget);
+
+  g_signal_connect (widget, "style-updated",
+                    G_CALLBACK (on_widget_style_updated),
+                    GINT_TO_POINTER (visible));
+}
+
+/* The following macros are used to define generic getters and setters */
+
+#define SMB_DEFINE_GETTER_FULL(type, prop, retval, retstmt) \
+type \
+sushi_media_bin_get_##prop (SushiMediaBin *self) \
+{ \
+  g_return_val_if_fail (SUSHI_IS_MEDIA_BIN (self), retval); \
+  retstmt \
+}
+
+#define SMB_DEFINE_GETTER(type, prop, retval) \
+  SMB_DEFINE_GETTER_FULL (type, prop, retval, return SMB_PRIVATE (self)->prop;)
+
+#define SMB_DEFINE_SETTER_FULL(type, prop, PROP, setup, cmp, assign, code) \
+void \
+sushi_media_bin_set_##prop (SushiMediaBin *self, type prop) \
+{ \
+  SushiMediaBinPrivate *priv; \
+  g_return_if_fail (SUSHI_IS_MEDIA_BIN (self)); \
+  priv = SMB_PRIVATE (self); \
+  setup; \
+  if (cmp) \
+    { \
+      assign; \
+      code; \
+      g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_##PROP]); \
+    } \
+}
+
+/* The last argument is for custom code that will be added just before calling
+ * g_object_notify_by_pspec()
+ */
+#define SMB_DEFINE_SETTER(type, prop, PROP, code) \
+  SMB_DEFINE_SETTER_FULL(type, prop, PROP, \
+    , \
+    priv->prop != prop, \
+    priv->prop = prop, \
+    code)
+
+#define SMB_DEFINE_SETTER_BOOLEAN(prop, PROP, code) \
+  SMB_DEFINE_SETTER_FULL(gboolean, prop, PROP, \
+    prop = (prop) ? TRUE : FALSE, \
+    priv->prop != prop, \
+    priv->prop = prop, \
+    code)
+
+#define SMB_DEFINE_SETTER_STRING(prop, PROP, code) \
+  SMB_DEFINE_SETTER_FULL(const gchar *, prop, PROP, \
+    , \
+    g_strcmp0 (priv->prop, prop), \
+    g_free (priv->prop); priv->prop = g_strdup (prop), \
+    code)
+
+
+/******************************** GST Support *********************************/
+static inline gboolean
+sushi_media_bin_handle_msg_error (SushiMediaBin *self, GstMessage *msg)
+{
+  SushiMediaBinPrivate *priv = SMB_PRIVATE (self);
+  GError *error = NULL;
+  gboolean handled;
+
+  gst_message_parse_error (msg, &error, NULL);
+
+  if (priv->play)
+    gst_element_set_state (priv->play, GST_STATE_NULL);
+
+  g_signal_emit (self, sushi_media_bin_signals[ERROR], 0, error, &handled);
+
+  g_error_free (error);
+
+  return handled;
+}
+
+static inline void
+sushi_media_bin_update_duration (SushiMediaBin *self)
+{
+  SushiMediaBinPrivate *priv = SMB_PRIVATE (self);
+  gint64 duration;
+
+  if (!gst_element_query_duration (priv->play, GST_FORMAT_TIME, &duration)
+      || priv->duration == duration)
+    return;
+
+  priv->duration = duration;
+
+  duration = GST_TIME_AS_SECONDS (duration);
+  gtk_label_set_label (priv->duration_label, format_time (duration));
+  gtk_adjustment_set_upper (priv->playback_adjustment, duration);
+}
+
+static inline void
+sushi_media_bin_update_position (SushiMediaBin *self)
+{
+  SushiMediaBinPrivate *priv = SMB_PRIVATE (self);
+  gint position = GST_TIME_AS_SECONDS (sushi_media_bin_get_position (self));
+
+  if (priv->position == position)
+    return;
+
+  priv->position = position;
+
+  priv->ignore_adjustment_changes = TRUE;
+  gtk_adjustment_set_value (priv->playback_adjustment, position);
+  priv->ignore_adjustment_changes = FALSE;
+
+  gtk_label_set_label (priv->audio_position_label, format_time (position));
+}
+
+static inline void
+log_fps (SushiMediaBin *self, GdkFrameClock *frame_clock)
+{
+  SushiMediaBinPrivate *priv = SMB_PRIVATE (self);
+  gint64 frame_time, time;
+  GstSample *sample;
+
+  /* Get current buffer and return if its the same as last tick */
+  g_object_get (priv->play, "sample", &sample, NULL);
+  if (sample)
+    {
+      GstBuffer *buffer = gst_sample_get_buffer (sample);
+      gst_sample_unref (sample);
+      
+      if (priv->last_buffer == buffer)
+        return;
+
+      priv->last_buffer = buffer;
+    }
+  else
+    return;
+
+  frame_time = gdk_frame_clock_get_frame_time (frame_clock);
+
+  /* Initialize state variables */
+  if (priv->tick_start == 0)
+    {
+      priv->tick_start = frame_time;
+      priv->frames_window_start = frame_time;
+      priv->frames_window = 0;
+      priv->frames_rendered = 0;
+    }
+  else if (priv->frames_window == 0)
+    priv->frames_window_start = frame_time;
+
+  priv->frames_window++;
+
+  /* We only print FPS once every FPS_WINDOW_SIZE seconds */
+  time = frame_time - priv->frames_window_start;
+  if (time < FPS_WINDOW_SIZE * 1000000)
+    return;
+
+  priv->frames_rendered += priv->frames_window;
+
+  GST_INFO ("FPS: %lf average: %lf",
+            priv->frames_window / (time / 1000000.0),
+            priv->frames_rendered / ((frame_time - priv->tick_start) / 1000000.0));
+
+  priv->frames_window = 0;
+}
+
+static gboolean
+sushi_media_bin_tick_callback (GtkWidget     *widget,
+                               GdkFrameClock *frame_clock,
+                               gpointer       user_data)
+{
+  static GstDebugLevel level;
+
+  sushi_media_bin_update_position ((SushiMediaBin *)widget);
+
+  if (level == 0)
+    level = gst_debug_category_get_threshold (sushi_media_bin_debug);
+
+  if (level >= GST_LEVEL_INFO)
+    log_fps ((SushiMediaBin *)widget, frame_clock);
+
+  return G_SOURCE_CONTINUE;
+}
+
+static void
+sushi_media_bin_set_tick_enabled (SushiMediaBin *self, gboolean enabled)
+{
+  SushiMediaBinPrivate *priv = SMB_PRIVATE (self);
+
+  if (priv->tick_id)
+    {
+      gtk_widget_remove_tick_callback (GTK_WIDGET (self), priv->tick_id);
+      priv->tick_id = priv->tick_start = 0;
+    }
+
+  if (enabled)
+    priv->tick_id = gtk_widget_add_tick_callback (GTK_WIDGET (self),
+                                                  sushi_media_bin_tick_callback,
+                                                  NULL, NULL);
+}
+
+static inline void
+sushi_media_bin_dump_dot (SushiMediaBin *self, GstState old, GstState new)
+{
+  SushiMediaBinPrivate *priv = SMB_PRIVATE (self);
+  gchar *filename;
+
+  filename = g_strdup_printf ("%s_%s_%s", g_get_prgname (),
+                              gst_element_state_get_name (old),
+                              gst_element_state_get_name (new));
+  gst_debug_bin_to_dot_file_with_ts (GST_BIN (priv->play),
+                                     GST_DEBUG_GRAPH_SHOW_ALL,
+                                     filename);
+  g_free (filename);
+}
+
+static inline void
+sushi_media_bin_handle_msg_state_changed (SushiMediaBin *self, GstMessage *msg)
+{
+  SushiMediaBinPrivate *priv = SMB_PRIVATE (self);
+  GstState old_state, new_state;
+
+  gst_message_parse_state_changed (msg, &old_state, &new_state, NULL);
+
+  if (old_state == new_state ||
+      GST_MESSAGE_SRC (msg) != GST_OBJECT (priv->play))
+    return;
+
+  GST_DEBUG ("State changed from %s to %s",
+             gst_element_state_get_name (old_state),
+             gst_element_state_get_name (new_state));
+
+  if (priv->dump_dot_file)
+    sushi_media_bin_dump_dot (self, old_state, new_state);
+
+  /* Update UI */
+  if (old_state == GST_STATE_READY && new_state == GST_STATE_PAUSED)
+    {
+      gtk_image_set_from_icon_name (priv->playback_image, SMB_ICON_NAME_PLAY, SMB_ICON_SIZE);
+      widget_set_visible (priv->play_box, TRUE);
+      sushi_media_bin_update_duration (self);
+    }
+  else if (new_state == GST_STATE_PLAYING)
+    {
+      widget_set_visible (priv->play_box, FALSE);
+      gtk_image_set_from_icon_name (priv->playback_image, SMB_ICON_NAME_PAUSE, SMB_ICON_SIZE);
+      sushi_media_bin_set_tick_enabled (self, TRUE);
+    }
+  else
+    {
+      gtk_image_set_from_icon_name (priv->playback_image, SMB_ICON_NAME_PLAY, SMB_ICON_SIZE);
+      widget_set_visible (priv->play_box, TRUE);
+      priv->position = 0;
+      sushi_media_bin_set_tick_enabled (self, FALSE);
+    }
+}
+
+typedef struct {
+  GString *tag;
+  GString *val;
+} MetaDataStrings;
+
+static void
+print_tag (const GstTagList *list, const gchar *tag, gpointer data)
+{
+  MetaDataStrings *metadata = data;
+  gint i, n;
+
+  for (i = 0, n = gst_tag_list_get_tag_size (list, tag); i < n; ++i)
+    {
+      const GValue *val = gst_tag_list_get_value_index (list, tag, i);
+      GValue str = {0, };
+
+      g_value_init (&str, G_TYPE_STRING);
+      g_value_transform (val, &str);
+
+      g_string_append_printf (metadata->tag, "\n    %s", tag);
+      g_string_append_printf (metadata->val, "\n: %s", g_value_get_string (&str));
+
+      g_value_unset (&str);
+    }
+}
+
+static inline void
+meta_data_strings_set_title (MetaDataStrings *metadata, const gchar *title)
+{
+  g_string_assign (metadata->tag, title);
+  g_string_assign (metadata->val, "");
+}
+
+static inline void
+meta_data_strings_set_info (MetaDataStrings *metadata,
+                            GtkLabel        *left,
+                            GtkLabel        *right,
+                            GstTagList      *tags)
+{
+  if (tags)
+    {
+      gst_tag_list_foreach (tags, print_tag, metadata);
+
+      gtk_label_set_label (left, metadata->tag->str);
+      gtk_label_set_label (right, metadata->val->str);
+    }
+  else
+    {
+      gtk_label_set_label (left, "");
+      gtk_label_set_label (right, "");
+    }
+}
+
+static inline void
+sushi_media_bin_update_stream_info (SushiMediaBin *self)
+{
+  SushiMediaBinPrivate *priv = SMB_PRIVATE (self);
+  MetaDataStrings metadata = { g_string_new (""), g_string_new ("") };
+
+  meta_data_strings_set_title (&metadata, "Audio:");
+  meta_data_strings_set_info (&metadata,
+                              priv->info_column_label[0],
+                              priv->info_column_label[1],
+                              priv->audio_tags);
+
+  meta_data_strings_set_title (&metadata, "Video:");
+  if (priv->video_width && priv->video_height)
+    {
+      g_string_append_printf (metadata.tag, "\n    video-resolution");
+      g_string_append_printf (metadata.val, "\n: %dx%d", priv->video_width, priv->video_height);
+    }
+  meta_data_strings_set_info (&metadata,
+                              priv->info_column_label[2],
+                              priv->info_column_label[3],
+                              priv->video_tags);
+
+  meta_data_strings_set_title (&metadata, "Text:");
+  meta_data_strings_set_info (&metadata,
+                              priv->info_column_label[4],
+                              priv->info_column_label[5],
+                              priv->text_tags);
+
+  g_string_free (metadata.tag, TRUE);
+  g_string_free (metadata.val, TRUE);
+}
+
+static inline void
+sushi_media_bin_handle_msg_application (SushiMediaBin *self, GstMessage *msg)
+{
+  SushiMediaBinPrivate *priv = SMB_PRIVATE (self);
+  const GstStructure *structure;
+  const gchar *name;
+
+  structure = gst_message_get_structure (msg);
+  name = gst_structure_get_name (structure);
+  g_return_if_fail (name != NULL);
+
+  if (priv->show_stream_info)
+    sushi_media_bin_update_stream_info (self);
+
+  /* TODO: handle audio and text tags */
+  if (g_str_equal (name, "video-tags-changed"))
+    {
+      gchar *value = NULL;
+
+      if (!priv->title_user_set)
+        {
+          if (priv->video_tags)
+            gst_tag_list_get_string_index (priv->video_tags, GST_TAG_TITLE, 0, &value);
+
+          sushi_media_bin_set_title (self, value);
+          priv->title_user_set = FALSE;
+          g_clear_pointer (&value, g_free);
+        }
+
+      if (!priv->description_user_set)
+        {
+          /* Get description from comment or description tags */
+          if (priv->video_tags)
+            {
+              /* We try comment tag first and then description */
+              if (!gst_tag_list_get_string_index (priv->video_tags, GST_TAG_COMMENT, 0, &value))
+                gst_tag_list_get_string_index (priv->video_tags, GST_TAG_DESCRIPTION, 0, &value);
+            }
+
+          sushi_media_bin_set_description (self, value);
+          priv->description_user_set = FALSE;
+          g_clear_pointer (&value, g_free);
+        }
+    }
+}
+
+static inline void
+sushi_media_bin_handle_msg_eos (SushiMediaBin *self, GstMessage *msg)
+{
+  SushiMediaBinPrivate *priv = SMB_PRIVATE (self);
+
+  GST_DEBUG ("Got EOS");
+
+  gst_element_set_state (priv->play, GST_STATE_NULL);
+  sushi_media_bin_set_state (self, SMB_INITIAL_STATE);
+  sushi_media_bin_update_position (self);
+}
+
+static inline void
+sushi_media_bin_post_message_application (SushiMediaBin *self, const gchar *name)
+{
+  SushiMediaBinPrivate *priv = SMB_PRIVATE (self);
+  GstStructure *data = gst_structure_new (name, NULL, NULL);
+
+  /* Post message on the bus for the main thread to pick it up */
+  gst_element_post_message (priv->play,
+                            gst_message_new_application (GST_OBJECT (priv->play),
+                                                         data));
+}
+
+static inline void
+sushi_media_bin_handle_msg_tag (SushiMediaBin *self, GstMessage *msg)
+{
+  SushiMediaBinPrivate *priv = SMB_PRIVATE (self);
+  GstObject *src = GST_MESSAGE_SRC (msg);
+  GstTagList *tags = NULL, *old_tags = NULL;
+  const gchar *type = NULL;
+
+  gst_message_parse_tag (msg, &tags);
+
+  if (g_type_is_a (G_OBJECT_TYPE (src), GST_TYPE_VIDEO_SINK))
+    {
+      type = "video-tags-changed";
+      old_tags = priv->video_tags;
+      priv->video_tags = gst_tag_list_merge (old_tags, tags, GST_TAG_MERGE_REPLACE);
+    }
+  else if (g_type_is_a (G_OBJECT_TYPE (src), GST_TYPE_AUDIO_BASE_SINK))
+    {
+      type = "audio-tags-changed";
+      old_tags = priv->audio_tags;
+      priv->audio_tags = gst_tag_list_merge (old_tags, tags, GST_TAG_MERGE_REPLACE);
+    }
+
+  /* Post message on the bus for the main thread to pick it up */
+  if (type)
+    sushi_media_bin_post_message_application (self, type);
+
+  gst_tag_list_unref (tags);
+  g_clear_pointer (&old_tags, gst_tag_list_unref);
+}
+
+static inline void
+sushi_media_bin_handle_streams_selected (SushiMediaBin *self, GstMessage *msg)
+{
+  SushiMediaBinPrivate *priv = SMB_PRIVATE (self);
+  GstStreamCollection *collection = NULL;
+  GstStructure *caps_struct;
+  GstStream *stream;
+  GstCaps *caps;
+  gint i, n, w, h;
+
+  gst_message_parse_streams_selected (msg, &collection);
+  n = gst_stream_collection_get_size (collection);
+
+  for (i = 0; i < n; i++)
+    {
+      stream = gst_stream_collection_get_stream (collection, i);
+
+      if (gst_stream_get_stream_type (stream) == GST_STREAM_TYPE_VIDEO)
+        break;
+    }
+
+  caps = gst_stream_get_caps (stream);
+  caps_struct = gst_caps_get_structure (caps, 0);
+
+  if (gst_structure_get_int (caps_struct, "width", &w) &&
+      gst_structure_get_int (caps_struct, "height", &h))
+    {
+      if (priv->video_width != w || priv->video_height != h)
+        {
+          priv->video_width = w;
+          priv->video_height = h;
+          gtk_widget_queue_resize (GTK_WIDGET (self));
+        }
+    }
+  else
+    priv->video_width = priv->video_height = 0;
+
+  gst_caps_unref (caps);
+  gst_object_unref (collection);
+}
+
+static gboolean
+sushi_media_bin_bus_watch (GstBus *bus, GstMessage *msg, gpointer data)
+{
+  SushiMediaBin *self = data;
+
+  switch (GST_MESSAGE_TYPE (msg))
+    {
+    case GST_MESSAGE_APPLICATION:
+      sushi_media_bin_handle_msg_application (self, msg);
+      break;
+    case GST_MESSAGE_DURATION_CHANGED:
+      sushi_media_bin_update_duration (self);
+      break;
+    case GST_MESSAGE_EOS:
+      sushi_media_bin_handle_msg_eos (self, msg);
+      break;
+    case GST_MESSAGE_ERROR:
+      return sushi_media_bin_handle_msg_error (self, msg);
+    case GST_MESSAGE_STATE_CHANGED:
+      sushi_media_bin_handle_msg_state_changed (self, msg);
+      break;
+    case GST_MESSAGE_STREAMS_SELECTED:
+      sushi_media_bin_handle_streams_selected (self, msg);
+      break;
+    case GST_MESSAGE_TAG:
+      sushi_media_bin_handle_msg_tag (self, msg);
+      break;
+    default:
+      break;
+    }
+
+  return G_SOURCE_CONTINUE;
+}
+
+static void
+sushi_media_bin_init_playbin (SushiMediaBin *self)
+{
+  SushiMediaBinPrivate *priv = SMB_PRIVATE (self);
+
+  priv->play = gst_element_factory_make ("playbin3", "SushiMediaBinPlayBin");
+  gst_object_ref_sink (priv->play);
+
+  /* Setup volume */
+  /* NOTE: Bidirectional binding makes the app crash on X11 */
+  g_object_bind_property (priv->volume_adjustment, "value",
+                          priv->play, "volume",
+                          G_BINDING_SYNC_CREATE);
+
+  /* Watch bus */
+  priv->bus = gst_pipeline_get_bus (GST_PIPELINE (priv->play));
+  gst_bus_add_watch (priv->bus, sushi_media_bin_bus_watch, self);
+}
+
+/********************************* Public API *********************************/
+
+/**
+ * sushi_media_bin_new:
+ * @audio_mode:
+ *
+ * Returns a new #SushiMediaBin
+ *
+ */
+GtkWidget *
+sushi_media_bin_new (gboolean audio_mode)
+{
+  return (GtkWidget*) g_object_new (SUSHI_TYPE_MEDIA_BIN,
+                                    "audio-mode", audio_mode,
+                                    NULL);
+}
+
+/**
+ * sushi_media_bin_get_uri:
+ * @self: a #SushiMediaBin
+ *
+ * Return the media URI
+ */
+SMB_DEFINE_GETTER (const gchar *, uri, NULL)
+
+/**
+ * sushi_media_bin_set_uri:
+ * @self: a #SushiMediaBin
+ * @uri:
+ *
+ * Sets the media URI to play
+ */
+SMB_DEFINE_SETTER_STRING (uri, URI,
+  /* Make playbin show the first video frame if there is an URI
+   * and the widget is realized.
+   */
+  sushi_media_bin_update_state (self);
+
+  /* Clear tag lists */
+  if (priv->audio_tags)
+    {
+      g_clear_pointer (&priv->audio_tags, gst_tag_list_unref);
+      sushi_media_bin_post_message_application (self, "audio-tags-changed");
+    }
+
+  if (priv->video_tags)
+    {
+      g_clear_pointer (&priv->video_tags, gst_tag_list_unref);
+      sushi_media_bin_post_message_application (self, "video-tags-changed");
+    }
+
+  if (priv->text_tags)
+    {
+      g_clear_pointer (&priv->text_tags, gst_tag_list_unref);
+      sushi_media_bin_post_message_application (self, "text-tags-changed");
+    }
+)
+
+/**
+ * sushi_media_bin_get_autohide_timeout:
+ * @self: a #SushiMediaBin
+ *
+ * Returns control's auto hide timeout in seconds.
+ */
+SMB_DEFINE_GETTER (gint, autohide_timeout, 0)
+
+/**
+ * sushi_media_bin_set_autohide_timeout:
+ * @self: a #SushiMediaBin
+ * @autohide_timeout: A timeout in seconds
+ *
+ * Sets the timeout to auto hide controls
+ */
+SMB_DEFINE_SETTER (gint, autohide_timeout, AUTOHIDE_TIMEOUT,)
+
+/**
+ * sushi_media_bin_get_fullscreen:
+ * @self: a #SushiMediaBin
+ *
+ * Returns whether video is fullscreen or not
+ */
+SMB_DEFINE_GETTER (gboolean, fullscreen, FALSE)
+
+/**
+ * sushi_media_bin_set_fullscreen:
+ * @self: a #SushiMediaBin
+ * @fullscreen:
+ *
+ * Sets whether to show the video in fullscreen mode or not
+ */
+SMB_DEFINE_SETTER_BOOLEAN (fullscreen, FULLSCREEN,
+  /* If there is no video sink, delay fullscreen until realize event */
+  if (priv->video_sink)
+    sushi_media_bin_fullscreen_apply (self, fullscreen);
+)
+
+/**
+ * sushi_media_bin_get_show_stream_info:
+ * @self: a #SushiMediaBin
+ *
+ * Returns whether streams information are show or not
+ */
+SMB_DEFINE_GETTER (gboolean, show_stream_info, FALSE)
+
+/**
+ * sushi_media_bin_set_show_stream_info:
+ * @self: a #SushiMediaBin
+ * @show_stream_info:
+ *
+ * Sets whether to show stream information or not
+ */
+SMB_DEFINE_SETTER_BOOLEAN (show_stream_info, SHOW_STREAM_INFO,
+
+  if (show_stream_info)
+    {
+      sushi_media_bin_update_stream_info (self);
+      gtk_widget_show (priv->info_box);
+    }
+  else
+    {
+      gint i;
+
+      gtk_widget_hide (priv->info_box);
+
+      for (i = 0; i < INFO_N_COLUMNS; i++)
+        gtk_label_set_label (priv->info_column_label[i], "");
+    }
+)
+
+/**
+ * sushi_media_bin_get_title:
+ * @self: a #SushiMediaBin
+ *
+ * Returns the media title if any
+ */
+SMB_DEFINE_GETTER (const gchar *, title, NULL)
+
+/**
+ * sushi_media_bin_set_title:
+ * @self: a #SushiMediaBin
+ * @title:
+ *
+ * Sets the media title.
+ * By default SushiMediaBin will use the title from the media metadata
+ */
+SMB_DEFINE_SETTER_STRING (title, TITLE,
+  gtk_label_set_label (priv->title_label, title);
+  gtk_widget_set_visible (GTK_WIDGET (priv->title_label), title != NULL);
+  priv->title_user_set = TRUE;
+)
+
+/**
+ * sushi_media_bin_get_description:
+ * @self: a #SushiMediaBin
+ *
+ * Returns the media description if any
+ */
+SMB_DEFINE_GETTER (const gchar *, description, NULL)
+
+/**
+ * sushi_media_bin_set_description:
+ * @self: a #SushiMediaBin
+ * @description:
+ *
+ * Sets the media description.
+ * By default SushiMediaBin will use the description from the media metadata
+ */
+SMB_DEFINE_SETTER_STRING (description, DESCRIPTION,
+  priv->description_user_set = TRUE;
+)
+
+/**
+ * sushi_media_bin_get_volume:
+ * @self: a #SushiMediaBin
+ *
+ * Returns audio volume from 0.0 to 1.0
+ */
+SMB_DEFINE_GETTER_FULL (gdouble, volume, 1.0,
+  return gtk_adjustment_get_value (SMB_PRIVATE (self)->volume_adjustment);
+)
+
+/**
+ * sushi_media_bin_set_volume:
+ * @self: a #SushiMediaBin
+ * @volume: from 0.0 to 1.0
+ *
+ * Sets the audio volume
+ */
+SMB_DEFINE_SETTER_FULL (gdouble, volume, VOLUME,
+  volume = CLAMP (volume, 0.0, 1.0),
+  gtk_adjustment_get_value (priv->volume_adjustment) != volume,
+  gtk_adjustment_set_value (priv->volume_adjustment, volume),
+)
+
+/**
+ * sushi_media_bin_play:
+ * @self: a #SushiMediaBin
+ *
+ * Start media playback
+ */
+void
+sushi_media_bin_play (SushiMediaBin *self)
+{
+  SushiMediaBinPrivate *priv;
+
+  g_return_if_fail (SUSHI_IS_MEDIA_BIN (self));
+  priv = SMB_PRIVATE (self);
+
+  g_object_set (priv->play, "uri", priv->uri, NULL);
+
+  sushi_media_bin_set_state (self, GST_STATE_PLAYING);
+}
+
+/**
+ * sushi_media_bin_pause:
+ * @self: a #SushiMediaBin
+ *
+ * Pause media playback
+ */
+void
+sushi_media_bin_pause (SushiMediaBin *self)
+{
+  g_return_if_fail (SUSHI_IS_MEDIA_BIN (self));
+  sushi_media_bin_set_state (self, GST_STATE_PAUSED);
+}
+
+/**
+ * sushi_media_bin_stop:
+ * @self: a #SushiMediaBin
+ *
+ * Stop media playback
+ */
+void
+sushi_media_bin_stop (SushiMediaBin *self)
+{
+  g_return_if_fail (SUSHI_IS_MEDIA_BIN (self));
+  sushi_media_bin_set_state (self, GST_STATE_NULL);
+}
+
+static void
+sushi_media_bin_free_pixbuf (guchar *pixels, gpointer data)
+{
+  gst_sample_unref (GST_SAMPLE (data));
+}
+
+/**
+ * sushi_media_bin_screenshot:
+ * @self: a #SushiMediaBin
+ * @width: desired screenshot width or -1 for original size
+ * @height: desired screenshot height or -1 for original size
+ *
+ * Takes a screenshot of the current frame.
+ *
+ * Returns: (transfer full): a new #GdkPixbuf
+ */
+GdkPixbuf *
+sushi_media_bin_screenshot (SushiMediaBin *self, gint width, gint height)
+{
+  SushiMediaBinPrivate *priv;
+  GdkPixbuf *retval = NULL;
+  GstSample *sample;
+  GstCaps   *caps;
+  GstBuffer *buffer;
+  GstMemory *memory = NULL;
+  GstMapInfo info;
+
+  g_return_val_if_fail (SUSHI_IS_MEDIA_BIN (self), NULL);
+  priv = SMB_PRIVATE (self);
+
+  /* Create a caps object with the desired format */
+  caps = gst_caps_new_simple ("video/x-raw",
+                              "format", G_TYPE_STRING, "RGB",
+                              "pixel-aspect-ratio", GST_TYPE_FRACTION, 1, 1,
+                              NULL);
+
+  if (width >= 0 && width >= 0)
+    gst_caps_set_simple (caps,
+                         "width", G_TYPE_INT, width,
+                         "height", G_TYPE_INT, height,
+                         NULL);
+
+  /* Get current sample in RGB */
+  g_signal_emit_by_name (priv->play, "convert-sample", caps, &sample);
+  gst_caps_unref (caps);
+
+  if (sample)
+    {
+      GstStructure *caps_struct;
+
+      if (!(caps = gst_sample_get_caps (sample)))
+        return NULL;
+
+      caps_struct = gst_caps_get_structure (caps, 0);
+
+      if (!(gst_structure_get_int (caps_struct, "width", &width) &&
+            gst_structure_get_int (caps_struct, "height", &height)))
+        return NULL;
+    }
+  else
+    {
+      /* FIXME: gst does not suport converting from video/x-raw(memory:GLMemory) */
+      g_warning ("Could not get video sample");
+      return NULL;
+    }
+
+  /* The buffer remains valid as long as sample is valid */
+  if ((buffer = gst_sample_get_buffer (sample)) && 
+      (memory = gst_buffer_get_memory (buffer, 0)) &&
+      gst_memory_map (memory, &info, GST_MAP_READ))
+    {
+      /* Create pixbuf from data with custom destroy function to free sample */
+      retval = gdk_pixbuf_new_from_data (info.data,
+                                         GDK_COLORSPACE_RGB, FALSE, 8,
+                                         width, height,
+                                         GST_ROUND_UP_4 (width * 3),
+                                         sushi_media_bin_free_pixbuf,
+                                         sample);
+      gst_memory_unmap (memory, &info);
+    }
+  else
+    {
+      g_warning ("Could not map memory from sample");
+      gst_sample_unref (sample);
+    }
+
+  gst_memory_unref (memory);
+
+  return retval;
+}
diff --git a/src/libsushi/sushi-media-bin.css b/src/libsushi/sushi-media-bin.css
new file mode 100644
index 0000000..6cd6450
--- /dev/null
+++ b/src/libsushi/sushi-media-bin.css
@@ -0,0 +1,220 @@
+/*
+ * sushi-media-bin.css
+ * Based on ekn-media-bin.css from:
+ * https://github.com/endlessm/eos-knowledge-lib/tree/master/lib/eosknowledgeprivate
+ *
+ * Copyright (C) 2016 Endless Mobile, Inc.
+ *
+ * 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, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ * Author: Juan Pablo Ugarte <ugarte endlessm com>
+ *
+ */
+
+@define-color transparent-light rgba (1,1,1,.1);
+@define-color transparent-dark  rgba (.11,.11,.11,.8);
+@define-color highlight-color   #ff6835;
+@define-color audio-bg-color    #4c4c4c;
+
+/* hiden/shown  */
+sushi-media-bin *.hide {
+  transition: opacity .32s;
+  opacity: 0;
+}
+
+sushi-media-bin *.show {
+  transition: opacity .32s;
+  opacity: 1;
+}
+
+sushi-media-bin {
+  background: black;
+}
+
+sushi-media-bin label,
+sushi-media-bin scale value {
+  color: white;
+}
+
+sushi-media-bin label:backdrop,
+sushi-media-bin scale value:backdrop {
+  color: darker(white);
+}
+
+sushi-media-bin box.overlay-bar {
+  background: @transparent-dark;
+}
+
+sushi-media-bin box.overlay-bar.top {
+  padding: 12px;
+}
+
+sushi-media-bin label.title {
+  font: 18px Sans;
+}
+
+sushi-media-bin box.bottom button {
+  border: 0px;
+  border-radius: 0px;
+  box-shadow: none;
+  outline: 0px;
+  background: none;
+  padding: 8px;
+}
+
+sushi-media-bin box.bottom button:hover {
+  box-shadow: none;
+  background: rgba (0,0,0,.6);
+}
+
+/* Main play button */
+sushi-media-bin overlay > box {
+  font-size: 18px;
+  border: 0px;
+  border-radius: 2em;
+  padding: .7em 1em;
+  box-shadow: none;
+  background: @transparent-dark;
+}
+
+sushi-media-bin overlay > box > label {
+  padding-left: .64em;
+}
+
+/* Style media playback scale */
+sushi-media-bin scale {
+  margin-top: .5em;
+  padding: 3px 0px;
+  background-image: linear-gradient(to top, @transparent-dark 3px, transparent 3px);
+}
+
+sushi-media-bin scale trough {
+  border: 0;
+  border-radius: 0px;
+  padding: 1px;
+  background: @transparent-light;
+}
+
+sushi-media-bin scale slider {
+  min-height: 12px;
+  min-width: 12px;
+  margin: -4px;
+  border: 0px;
+  border-radius: 6px;
+  box-shadow: none;
+  background: none;
+}
+
+sushi-media-bin scale highlight {
+  border: 0px;
+  padding: 0px;
+  border-radius: 0px 4px 4px 0px;
+  background: @highlight-color;
+}
+
+sushi-media-bin scale value {
+  padding: 2px 0px;
+  border-radius: 1em;
+  background: @transparent-dark;
+}
+
+sushi-media-bin scale:hover slider {
+  background: white;
+}
+
+/* Volume popover */
+popover.sushi-media-bin {
+  border: 0px;
+  border-radius: 0px;
+  box-shadow: none;
+  background: none;
+}
+
+popover.sushi-media-bin scale {
+  border-radius: 18px;
+  padding: 6px;
+  background: @transparent-dark;
+}
+
+popover.sushi-media-bin scale trough {
+  outline: 0px;
+  border: 0px;
+  border-radius: 3px;
+  padding: 1px;
+  background: @transparent-light;
+}
+
+popover.sushi-media-bin scale highlight {
+  border: 0px;
+  padding: 0px;
+  border-radius: 4px;
+  background: @highlight-color;
+}
+
+popover.sushi-media-bin scale slider {
+  min-height: 12px;
+  min-width: 12px;
+  margin: -3px;
+  border: 0px;
+  border-radius: 6px;
+  box-shadow: none;
+  background: none;
+}
+
+popover.sushi-media-bin scale:hover slider {
+  background: white;
+}
+
+/* Audio nodes */
+sushi-media-bin box.audio > box {
+  background: @audio-bg-color;
+}
+
+sushi-media-bin box.audio > scale {
+  margin: 0;
+  padding: 1px 0px;
+  background-image: linear-gradient(to top, @audio-bg-color 3px, transparent 3px);
+}
+
+sushi-media-bin box.audio > scale trough {
+  padding: 1px 0px;
+  background: #707070;
+}
+
+/* Setup key bindings */
+@binding-set smb-binding-set {
+  bind "space"     { "toggle" ("playback") };
+  bind "f"         { "toggle" ("fullscreen") };
+  bind "i"         { "toggle" ("show-stream-info") };
+  bind "Right"     { "seek" (5) };
+  bind "Left"      { "seek" (-5) };
+  bind "Up"        { "seek" (60) };
+  bind "Down"      { "seek" (-60) };
+  bind "Page_Up"   { "seek" (300) };
+  bind "Page_Down" { "seek" (-300) };
+  bind "Home"      { "seek" (0) };
+}
+
+@binding-set smb-fullscreen-binding-set {
+  bind "Escape" { "toggle" ("fullscreen") };
+}
+
+sushi-media-bin {
+  -gtk-key-bindings: smb-binding-set;
+}
+
+sushi-media-bin.fullscreen {
+  -gtk-key-bindings: smb-binding-set, smb-fullscreen-binding-set;
+}
diff --git a/src/libsushi/sushi-media-bin.h b/src/libsushi/sushi-media-bin.h
new file mode 100644
index 0000000..ca15533
--- /dev/null
+++ b/src/libsushi/sushi-media-bin.h
@@ -0,0 +1,75 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
+/*
+ * sushi-media-bin.h
+ * Based on ekn-media-bin.h from:
+ * https://github.com/endlessm/eos-knowledge-lib/tree/master/lib/eosknowledgeprivate
+ *
+ * Copyright (C) 2016 Endless Mobile, Inc.
+ *
+ * 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, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ * Author: Juan Pablo Ugarte <ugarte endlessm com>
+ *
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define SUSHI_TYPE_MEDIA_BIN (sushi_media_bin_get_type ())
+G_DECLARE_FINAL_TYPE (SushiMediaBin, sushi_media_bin, SUSHI, MEDIA_BIN, GtkBox)
+
+GtkWidget     *sushi_media_bin_new                  (gboolean audio_mode);
+
+const gchar   *sushi_media_bin_get_uri              (SushiMediaBin *self);
+void           sushi_media_bin_set_uri              (SushiMediaBin *self,
+                                                     const gchar *uri);
+
+gdouble        sushi_media_bin_get_volume           (SushiMediaBin *self);
+void           sushi_media_bin_set_volume           (SushiMediaBin *self,
+                                                     gdouble      volume);
+
+gint           sushi_media_bin_get_autohide_timeout (SushiMediaBin *self);
+void           sushi_media_bin_set_autohide_timeout (SushiMediaBin *self,
+                                                     gint         autohide_timeout);
+
+gboolean       sushi_media_bin_get_fullscreen       (SushiMediaBin *self);
+void           sushi_media_bin_set_fullscreen       (SushiMediaBin *self,
+                                                     gboolean     fullscreen);
+
+gboolean       sushi_media_bin_get_show_stream_info (SushiMediaBin *self);
+void           sushi_media_bin_set_show_stream_info (SushiMediaBin *self,
+                                                     gboolean     show_stream_info);
+
+const gchar   *sushi_media_bin_get_title            (SushiMediaBin *self);
+void           sushi_media_bin_set_title            (SushiMediaBin *self,
+                                                     const gchar *title);
+
+const gchar   *sushi_media_bin_get_description      (SushiMediaBin *self);
+void           sushi_media_bin_set_description      (SushiMediaBin *self,
+                                                     const gchar *description);
+
+
+void           sushi_media_bin_play                 (SushiMediaBin *self);
+void           sushi_media_bin_pause                (SushiMediaBin *self);
+void           sushi_media_bin_stop                 (SushiMediaBin *self);
+
+GdkPixbuf     *sushi_media_bin_screenshot           (SushiMediaBin *self,
+                                                     gint         width,
+                                                     gint         height);
+
+G_END_DECLS
diff --git a/src/meson.build b/src/meson.build
index ff63a50..9400566 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -8,6 +8,7 @@ deps = [
   clutter_dep,
   clutter_gst_dep,
   clutter_gtk_dep,
+  epoxy_dep,
   evince_document_dep,
   evince_view_dep,
   freetype_dep,
@@ -15,8 +16,10 @@ deps = [
   gjs_dep,
   glib_dep,
   gstreamer_dep,
+  gstreamer_audio_dep,
   gstreamer_pbutils_dep,
   gstreamer_tag_dep,
+  gstreamer_video_dep,
   gtk_dep,
   gtksourceview_dep,
   harfbuzz_dep,


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]