[sushi/wip/cosimoc/no-clutter: 22/66] media-bin: import SushiMediaBin for gstreamer playback
- From: Cosimo Cecchi <cosimoc src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [sushi/wip/cosimoc/no-clutter: 22/66] media-bin: import SushiMediaBin for gstreamer playback
- Date: Sat, 8 Jun 2019 18:42:00 +0000 (UTC)
commit 9494cb0f53a4c952fef4dc1f78040d0288dd9779
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 1330791..7007bc1 100644
--- a/meson.build
+++ b/meson.build
@@ -5,6 +5,7 @@ project(
meson_version: '>=0.42.0'
)
+epoxy_dep = dependency('epoxy')
evince_document_dep = dependency('evince-document-3.0')
evince_view_dep = dependency('evince-view-3.0')
freetype_dep = dependency('freetype2')
@@ -12,9 +13,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 93e76b6..bfa6a7e 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 4f37331..10a25ca 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -5,6 +5,7 @@ sushi_sources = [
]
deps = [
+ epoxy_dep,
evince_document_dep,
evince_view_dep,
freetype_dep,
@@ -12,8 +13,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]