[gnome-subtitles: 1/2] GStreamer media playback rewrite




commit efedf76beb1986ea7462bfb3c18bb1b1673af1b5
Author: Pedro Castro <pedro gnomesubtitles org>
Date:   Sat May 15 22:23:27 2021 +0100

    GStreamer media playback rewrite
    
    Closes #177. Key motivations are:
    - it was based on code from the Fuse media project and contained unused
      code, and other bits which weren't totally aligned with what's needed
      in Gnome Subtitles
    - it was hard to maintain
    - fix a number of bugs
    - implement an abstraction to facilitate adding support for other media
      backends (mpv?) in the future

 .gitignore                                         |  15 +-
 configure.ac                                       |   8 +-
 gnome-subtitles.csproj                             |  12 +-
 src/External/GStreamerPlaybin/Engine.cs            | 603 -----------------
 src/External/GStreamerPlaybin/Events.cs            | 107 ----
 src/External/GStreamerPlaybin/main.c               | 713 ---------------------
 src/External/GstBackend/.vscode/settings.json      |   4 +
 src/External/GstBackend/GstBackend.cs              | 210 ++++++
 src/External/GstBackend/GstMediaInfo.cs            |  76 +++
 src/External/GstBackend/gst-backend.c              | 421 ++++++++++++
 .../Core/Command/InsertSubtitleCommand.cs          |   4 +-
 src/GnomeSubtitles/Core/EventHandlers.cs           |  14 +-
 src/GnomeSubtitles/Dialog/AboutDialog.cs           |   2 +-
 src/GnomeSubtitles/Dialog/BaseDialog.cs            |   4 +-
 .../Message/DialogUtil.cs}                         |  36 +-
 .../Dialog/Message/FileOpenErrorDialog.cs          |  75 ---
 .../Dialog/Message/SubtitleFileOpenErrorDialog.cs  |  47 +-
 ...oErrorDialog.cs => VideoFileOpenErrorDialog.cs} |  48 +-
 src/GnomeSubtitles/Dialog/VideoSeekToDialog.cs     |   8 +-
 src/GnomeSubtitles/Execution/AssemblyInfo.cs.in    |   4 +-
 ...itiateEngineException.cs => PlayerException.cs} |  12 +-
 src/GnomeSubtitles/Ui/VideoPreview/MediaBackend.cs |  99 +++
 src/GnomeSubtitles/Ui/VideoPreview/Player.cs       | 310 +++++----
 .../Ui/VideoPreview/PlayerPositionWatcher.cs       |  21 +-
 src/GnomeSubtitles/Ui/VideoPreview/Video.cs        | 168 +++--
 .../Ui/VideoPreview/VideoPosition.cs               |  25 +-
 src/Makefile.am                                    |  14 +-
 27 files changed, 1203 insertions(+), 1857 deletions(-)
---
diff --git a/.gitignore b/.gitignore
index 1fed8a1..6a189da 100644
--- a/.gitignore
+++ b/.gitignore
@@ -25,8 +25,6 @@
 /src/GnomeSubtitles/Execution/ConfigureDefines.cs
 /src/GnomeSubtitles/Execution/AssemblyInfo.cs
 /src/GnomeSubtitles/Execution/gnome-subtitles
-/src/libgstreamer_playbin.so
-/src/main.o
 /src/Makefile
 /src/Makefile.in
 /build/*
@@ -49,9 +47,6 @@
 /po/gnome-subtitles.pot
 /config.sub
 /config.guess
-/src/.libs/*
-/src/libgstreamer_playbin.la
-/src/main.lo
 /m4/libtool.m4
 /m4/lt~obsolete.m4
 /m4/ltoptions.m4
@@ -59,11 +54,13 @@
 /m4/ltversion.m4
 /ltmain.sh
 /libtool
-/src/External/GStreamerPlaybin/.deps/*
-/src/External/GStreamerPlaybin/.libs/*
-/src/External/GStreamerPlaybin/.dirstamp
-/src/External/GStreamerPlaybin/libgstreamer_playbin_la-main.lo
 /po/missing
 /.vs/gnome-subtitles/xs/UserPrefs.xml
 /obj/*
 compile
+/src/.libs
+/src/External/GstBackend/.deps
+/src/External/GstBackend/.dirstamp
+/src/External/GstBackend/.libs
+/src/External/GstBackend/libgst_backend_la-gst-backend.lo
+/src/libgst_backend.la
diff --git a/configure.ac b/configure.ac
index 37f58fa..c35f3f2 100644
--- a/configure.ac
+++ b/configure.ac
@@ -74,8 +74,12 @@ GSTREAMER_REQUIRED_VERSION=1.0
 PKG_CHECK_MODULES(MONO, [mono >= $MONO_REQUIRED_VERSION])
 PKG_CHECK_MODULES(GTKSHARP, [gtk-sharp-3.0 >= $GTKSHARP_REQUIRED_VERSION])
 PKG_CHECK_MODULES(GTK, [gtk+-3.0 >= $GTK_REQUIRED_VERSION])
-PKG_CHECK_MODULES(gstreamer, [gstreamer-video-1.0 >= $GSTREAMER_REQUIRED_VERSION])
-PKG_CHECK_MODULES(gstreamer_plugins_base, [gstreamer-plugins-base-1.0 >= $GSTREAMER_REQUIRED_VERSION])
+PKG_CHECK_MODULES(gstreamer, [
+       gstreamer-1.0 >= $GSTREAMER_REQUIRED_VERSION
+       gstreamer-video-1.0 >= $GSTREAMER_REQUIRED_VERSION
+       gstreamer-plugins-base-1.0 >= $GSTREAMER_REQUIRED_VERSION
+       gstreamer-pbutils-1.0 >= $GSTREAMER_REQUIRED_VERSION
+])
 AC_SUBST(gstreamer_CFLAGS)
 AC_SUBST(gstreamer_LIBS)
 
diff --git a/gnome-subtitles.csproj b/gnome-subtitles.csproj
index e0cc290..4d55860 100644
--- a/gnome-subtitles.csproj
+++ b/gnome-subtitles.csproj
@@ -64,8 +64,6 @@
     </EmbeddedResource>
   </ItemGroup>
   <ItemGroup>
-    <Compile Include="src\External\GStreamerPlaybin\Engine.cs" />
-    <Compile Include="src\External\GStreamerPlaybin\Events.cs" />
     <Compile Include="src\External\NCharDet\Big5Statistics.cs" />
     <Compile Include="src\External\NCharDet\EUCJPStatistics.cs" />
     <Compile Include="src\External\NCharDet\EUCKRStatistics.cs" />
@@ -151,11 +149,10 @@
     <Compile Include="src\GnomeSubtitles\Dialog\VideoSeekToDialog.cs" />
     <Compile Include="src\GnomeSubtitles\Dialog\Message\BasicErrorDialog.cs" />
     <Compile Include="src\GnomeSubtitles\Dialog\Message\ErrorDialog.cs" />
-    <Compile Include="src\GnomeSubtitles\Dialog\Message\FileOpenErrorDialog.cs" />
     <Compile Include="src\GnomeSubtitles\Dialog\Message\FileSaveErrorDialog.cs" />
     <Compile Include="src\GnomeSubtitles\Dialog\Message\SaveConfirmationDialog.cs" />
     <Compile Include="src\GnomeSubtitles\Dialog\Message\SubtitleFileOpenErrorDialog.cs" />
-    <Compile Include="src\GnomeSubtitles\Dialog\Message\VideoErrorDialog.cs" />
+    <Compile Include="src\GnomeSubtitles\Dialog\Message\VideoFileOpenErrorDialog.cs" />
     <Compile Include="src\GnomeSubtitles\Dialog\Message\WarningDialog.cs" />
     <Compile Include="src\GnomeSubtitles\Execution\AssemblyInfo.cs" />
     <Compile Include="src\GnomeSubtitles\Execution\BugReporter.cs" />
@@ -182,8 +179,7 @@
     <Compile Include="src\GnomeSubtitles\Ui\VideoPreview\Video.cs" />
     <Compile Include="src\GnomeSubtitles\Ui\VideoPreview\VideoFiles.cs" />
     <Compile Include="src\GnomeSubtitles\Ui\VideoPreview\VideoPosition.cs" />
-    <Compile 
Include="src\GnomeSubtitles\Ui\VideoPreview\Exceptions\PlayerCouldNotInitiateEngineException.cs" />
-    <Compile Include="src\GnomeSubtitles\Ui\VideoPreview\Exceptions\PlayerEngineException.cs" />
+    <Compile Include="src\GnomeSubtitles\Ui\VideoPreview\Exceptions\PlayerException.cs" />
     <Compile Include="src\GnomeSubtitles\Ui\View\CellRendererCenteredText.cs" />
     <Compile Include="src\GnomeSubtitles\Ui\View\SelectionIntended.cs" />
     <Compile Include="src\GnomeSubtitles\Ui\View\SelectionType.cs" />
@@ -279,6 +275,10 @@
     <Compile Include="src\GnomeSubtitles\Ui\WindowState.cs" />
     <Compile Include="src\External\Enchant\Enchant.cs" />
     <Compile Include="src\External\Interop\Interop.cs" />
+    <Compile Include="src\External\GstBackend\GstBackend.cs" />
+    <Compile Include="src\External\GstBackend\GstMediaInfo.cs" />
+    <Compile Include="src\GnomeSubtitles\Ui\VideoPreview\MediaBackend.cs" />
+    <Compile Include="src\GnomeSubtitles\Dialog\Message\DialogUtil.cs" />
   </ItemGroup>
   <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
   <ItemGroup>
diff --git a/src/External/GstBackend/.vscode/settings.json b/src/External/GstBackend/.vscode/settings.json
new file mode 100644
index 0000000..99e9c3f
--- /dev/null
+++ b/src/External/GstBackend/.vscode/settings.json
@@ -0,0 +1,4 @@
+{
+       "editor.insertSpaces": false,
+       "editor.detectIndentation": false
+}
\ No newline at end of file
diff --git a/src/External/GstBackend/GstBackend.cs b/src/External/GstBackend/GstBackend.cs
new file mode 100644
index 0000000..2184adb
--- /dev/null
+++ b/src/External/GstBackend/GstBackend.cs
@@ -0,0 +1,210 @@
+/*
+ * This file is part of Gnome Subtitles.
+ * Copyright (C) 2021 Pedro Castro
+ *
+ * Gnome Subtitles is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * Gnome Subtitles 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ */
+
+using GnomeSubtitles.Core;
+using GnomeSubtitles.Ui.VideoPreview;
+using Gtk;
+using Mono.Unix;
+using SubLib.Util;
+using System;
+using System.Runtime.InteropServices;
+
+/*
+ * There are many ways to integrate gstreamer into an application. This implementation uses a gstreamer
+ * playbin (which is a gstreamer pipeline with a number of additional features) with a video sink set to a 
+ * gtksink (instead of the default "auto" video sink). Once initialized, this video sink allows returning
+ * its GtkWidget, which is then added to the application UI. Most of the hard work is done in the auxiliary
+ * gst_backend C library, whereas here we use bindings to that library.
+ * 
+ * Previously, this implementation relied on gst_video_overlay_set_window_handle which received the X11 id
+ * of the Gdk.Window to which we wanted gstreamer to attach to, however this implementation presented many
+ * glitches which weren't simple to solve (ex: screen went black when switching between apps, and also when
+ * updating the current subtitle while the video was paused).
+ *
+ * Reference to other ways to implement gstreamer into a GTK application:
+ *   - https://github.com/GNOME/clutter-gst (does a lot more than a simple integration; used in Totem)
+ *   - https://developer.gnome.org/totem/stable/BaconVideoWidget.html (Totem bacon video widget)
+ */
+
+namespace External.GStreamer {
+
+       /* Enums */
+       
+       public class GstBackend : MediaBackend {
+               private HandleRef backend;
+               private GstMediaInfo mediaInfo = null;
+               private float speed = 1f; //Keeping it here as we need to pass it to every 'seek' call
+
+               /* Delegates */
+               private delegate void ErrorFoundHandler(ErrorType type, string errorMessage, string 
debugMessage);
+               private delegate void LoadCompleteHandler(GstMediaInfo mediaInfo);
+
+               /* Enums */
+               private enum ErrorType { GStreamer, NoMediaInfo, NoVideoOrAudio};
+
+               /* Error messages */
+               private readonly string ErrorMessageNoMediaInfo = Catalog.GetString("Unable to obtain the 
complete media information.");
+               private readonly string ErrorMessageNoVideoOrAudio = Catalog.GetString("The file contains no 
video or audio.");
+               
+               /* Public properties */
+       
+               public override string Name {
+                       get { return "GStreamer"; }
+               }
+               
+               public override long CurrentPosition {
+                       get { return gst_backend_get_position(backend); }
+               }
+
+               public override long Duration {
+                       get { return mediaInfo.Duration; }
+               }
+
+               public override bool HasVideo {
+                       get { return mediaInfo.HasVideo; }
+               }
+
+               public override float AspectRatio {
+                       get { return mediaInfo.AspectRatio; }
+               }
+
+               public override float FrameRate {
+                       get { return mediaInfo.FrameRate; }
+               }
+
+               public override bool HasAudio {
+                       get { return mediaInfo.HasAudio; }
+               }
+
+
+               /* Public methods */
+
+               public override void Initialize() {
+                       IntPtr ptr = gst_backend_init(OnErrorFound, OnLoadComplete, OnEndOfStreamReached);
+                       if (ptr == IntPtr.Zero) {
+                               throw new Exception("Unable to initialize gstreamer: gst_backend_init 
returned no pointer.");
+                       }
+                       
+                       backend = new HandleRef(this, ptr);
+               }
+               
+               public override bool Load(string uri) {
+                       SetStatus(MediaStatus.Loading);
+       
+                       return gst_backend_load(backend, uri);
+               }
+               public override void Unload() {
+                       gst_backend_unload(backend);
+                       SetStatus(MediaStatus.Unloaded);
+                                               
+                       mediaInfo = null;
+                       speed = 1f;
+               }
+
+               public override void Dispose() {
+                       //Do nothing
+               }
+               
+               public override void Play() {
+                       gst_backend_play(backend);
+                       SetStatus(MediaStatus.Playing);
+               }
+       
+               public override void Pause() {
+                       gst_backend_pause(backend);
+                       SetStatus(MediaStatus.Paused);
+               }
+               
+               public override void Seek(long time, bool isAbsolute) {
+                       if ((Status == MediaStatus.Unloaded) || (Status == MediaStatus.Loading)) {
+                               return;
+                       }
+                       gst_backend_seek(backend, time, isAbsolute, speed);
+               }
+
+               public override void SetSpeed (float speed) {
+                       if ((Status == MediaStatus.Unloaded) || (Status == MediaStatus.Loading)) {
+                               return;
+                       }
+                       
+                       this.speed = speed;
+                       gst_backend_set_speed(backend, speed);
+               }
+
+               public override Widget CreateVideoWidget() {
+                       return new Widget(gst_backend_get_video_widget(backend));
+               }
+       
+       
+               /* Event handlers */
+       
+               private void OnLoadComplete(GstMediaInfo mediaInfo) {
+                       this.mediaInfo = mediaInfo;
+                       SetStatus(MediaStatus.Loaded);
+               }
+               
+               private void OnErrorFound(ErrorType code, string userMessage, string debugMessage) {
+                       //If we have a debug message, output it to the logs
+                       if (!String.IsNullOrEmpty(debugMessage)) {
+                               Logger.Error("[GstBackend] OnErrorFound debug message: {0}", debugMessage);
+                       }
+
+                       //If we have a user message, use it. Otherwise, show an error message according to 
the specified code.
+                       string errorMessage = userMessage;
+                       if (String.IsNullOrEmpty(userMessage)) {
+                               if (code == ErrorType.NoMediaInfo) {
+                                       errorMessage = ErrorMessageNoMediaInfo;
+                               } else if (code == ErrorType.NoVideoOrAudio) {
+                                       errorMessage = ErrorMessageNoVideoOrAudio;
+                               }
+                       }
+                       if (String.IsNullOrEmpty(errorMessage)) {
+                               Logger.Error("[GstBackend] Got an empty error message for code '{0}'", code);
+                       }
+                       TriggerErrorFound(errorMessage);
+               }
+               
+               private void OnEndOfStreamReached() {
+                       TriggerEndOfStreamReached();
+               }
+
+
+               /* Imports */
+               
+               [DllImport("gst_backend")]
+               private static extern IntPtr gst_backend_init(ErrorFoundHandler onErrorFound, 
LoadCompleteHandler onLoadComplete, BasicEventHandler onEndOfStreamReached);
+               [DllImport("gst_backend")]
+               private static extern bool gst_backend_load(HandleRef backend, string uri);
+               [DllImport("gst_backend")]
+               private static extern void gst_backend_unload(HandleRef backend);
+               [DllImport("gst_backend")]
+               private static extern void gst_backend_play(HandleRef backend);
+               [DllImport("gst_backend")]
+               private static extern void gst_backend_pause(HandleRef backend);
+               [DllImport("gst_backend")]
+               private static extern long gst_backend_get_position(HandleRef backend);
+               [DllImport("gst_backend")]
+               private static extern void gst_backend_seek(HandleRef backend, long time, bool isAbsolute, 
float speed);
+               [DllImport("gst_backend")]
+               private static extern void gst_backend_set_speed(HandleRef backend, float speed);
+               [DllImport("gst_backend")]
+               private static extern IntPtr gst_backend_get_video_widget(HandleRef backend);
+       }
+
+}
diff --git a/src/External/GstBackend/GstMediaInfo.cs b/src/External/GstBackend/GstMediaInfo.cs
new file mode 100644
index 0000000..e7022a1
--- /dev/null
+++ b/src/External/GstBackend/GstMediaInfo.cs
@@ -0,0 +1,76 @@
+/*
+ * This file is part of Gnome Subtitles.
+ * Copyright (C) 2021 Pedro Castro
+ *
+ * Gnome Subtitles is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * Gnome Subtitles 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ */
+
+using System;
+using System.Runtime.InteropServices;
+
+namespace External.GStreamer {
+
+[StructLayout(LayoutKind.Sequential)]
+public class GstMediaInfo {
+       long duration;
+       
+       bool has_video;
+       int width;
+       int height;
+       float aspect_ratio;
+       float frame_rate;
+       
+       bool has_audio;
+
+       public GstMediaInfo(IntPtr ptr) {
+               if (ptr != IntPtr.Zero) {
+                       Marshal.PtrToStructure(ptr, this);
+               }
+       }
+       
+       
+       /* Public properties */
+       
+       public long Duration {
+               get { return duration; }
+       }
+       
+       public bool HasVideo {
+               get { return has_video; }
+       }
+       
+       public int Width {
+               get { return width; }
+       }
+       
+       public int Height
+               { get { return height; }
+       }
+       
+       public float AspectRatio {
+               get { return aspect_ratio; }
+       }
+       
+       public float FrameRate {
+               get { return frame_rate; }
+       }
+       
+       public bool HasAudio {
+               get { return has_audio; }
+       }
+
+}
+
+}
diff --git a/src/External/GstBackend/gst-backend.c b/src/External/GstBackend/gst-backend.c
new file mode 100644
index 0000000..b166f26
--- /dev/null
+++ b/src/External/GstBackend/gst-backend.c
@@ -0,0 +1,421 @@
+/*
+ * This file is part of Gnome Subtitles.
+ * Copyright (C) 2021 Pedro Castro
+ *
+ * Gnome Subtitles is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * Gnome Subtitles 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA   02110-1301      USA
+ */
+
+#include <gst/gst.h>
+#include <gst/pbutils/gstdiscoverer.h>
+
+
+/* Typedefs */
+
+typedef struct GstBackendMediaInfo GstBackendMediaInfo;
+typedef struct GstBackend GstBackend;
+
+
+/* Enums */
+
+enum GstBackendErrorType { GST_BACKEND_ERR_GSTREAMER, GST_BACKEND_ERR_NO_MEDIA_INFO, 
GST_BACKEND_ERR_NO_VIDEO_OR_AUDIO };
+
+
+/* Callbacks */
+
+typedef void (*ErrorCallback)(enum GstBackendErrorType type, const gchar *user_message, const gchar 
*debug_message);
+typedef void (*BasicCallback)();
+typedef void (*LoadCompleteCallback)(GstBackendMediaInfo *media_info);
+
+
+/* Structures */
+
+struct GstBackendMediaInfo { 
+       /* video and audio */
+       gint64 duration;
+
+       /* video */
+       gboolean has_video;
+       gint width;
+       gint height;
+       gfloat aspect_ratio;
+       gfloat frame_rate;
+
+       /* audio */
+       gboolean has_audio;
+};
+
+struct GstBackend {
+       GstElement *element; //playbin
+       GstElement *video_element; //gtksink
+
+       GstBackendMediaInfo *media_info;
+
+       ErrorCallback error_callback;
+       LoadCompleteCallback load_complete_callback;
+       BasicCallback eos_reached_callback;
+};
+
+
+/* Static Functions */
+
+static GstBackend *gst_backend_new() {
+       GstBackend *backend = g_new0(GstBackend, 1);
+       return backend;
+}
+
+static GstBackendMediaInfo *gst_backend_media_info_new() {
+       GstBackendMediaInfo *media_info = g_new0(GstBackendMediaInfo, 1);
+
+       /* video and audio */
+       media_info->duration = -1;
+
+       /* video */
+       media_info->has_video = FALSE;
+       media_info->width = -1;
+       media_info->height = -1;
+       media_info->aspect_ratio = -1;
+       media_info->frame_rate = -1;
+
+       /* audio */
+       media_info->has_audio = FALSE;
+
+       return media_info;
+}
+
+static void throw_error(GstBackend *backend, enum GstBackendErrorType type, const gchar *user_message, const 
gchar *debug_message) {
+       if (backend->error_callback != NULL) {
+               backend->error_callback(type, user_message, debug_message);
+       }
+}
+
+/*
+ * Blocks until a pending state change (if applicable) completes.
+ */
+static void wait_for_state_change_to_complete(GstBackend *backend) {
+       gst_element_get_state(backend->element, NULL, NULL, GST_CLOCK_TIME_NONE);
+}
+
+static gboolean media_has_video(GstBackend *backend) {
+       gint current_video;
+       g_object_get(backend->element, "current-video", &current_video, NULL);
+       return current_video != -1;
+}
+
+static gboolean media_has_audio(GstBackend *backend) {
+       gint current_audio;
+       g_object_get(backend->element, "current-audio", &current_audio, NULL);
+       return current_audio != -1;
+}
+
+static void load_media_info_video(GstBackend *backend) {
+       GstElement *video_sink;
+       g_object_get(G_OBJECT(backend->element), "video-sink", &video_sink, NULL);
+
+       if (video_sink == NULL) {
+               throw_error(backend, GST_BACKEND_ERR_NO_MEDIA_INFO, NULL, "Unable to obtain the video sink");
+               return;
+       }
+
+       GstPad *video_pad = gst_element_get_static_pad(GST_ELEMENT(video_sink), "sink");
+       if (video_pad == NULL) {
+               throw_error(backend, GST_BACKEND_ERR_NO_MEDIA_INFO, NULL, "Unable to obtain the video pad");
+               return;
+       }
+
+       GstCaps *caps =  gst_pad_get_current_caps(video_pad);
+       if (caps == NULL) {
+               throw_error(backend, GST_BACKEND_ERR_NO_MEDIA_INFO, NULL, "Unable to obtain the video caps");
+               return;
+       }
+
+       guint caps_count = gst_caps_get_size(caps);
+       gboolean got_width = FALSE, got_height = FALSE, got_frame_rate = FALSE;
+       for (guint i = 0; i < caps_count; i++) {
+               GstStructure *structure = gst_caps_get_structure(caps, i);
+
+               /* Ignore if not video */
+               const gchar *name = gst_structure_get_name(structure);
+               if (!g_str_has_prefix(name, "video")) {
+                       continue;
+               }
+
+               /* Get the values */
+               if (!got_width) {
+                       got_width = gst_structure_get_int(structure, "width", &backend->media_info->width);
+               }
+               if (!got_height) {
+                       got_height = gst_structure_get_int(structure, "height", &backend->media_info->height);
+               }
+               if (!got_frame_rate) {
+                       gint numerator, denominator;
+                       got_frame_rate = gst_structure_get_fraction(structure, "framerate", &numerator, 
&denominator);
+                       if (got_frame_rate) {
+                               backend->media_info->frame_rate = (float)numerator / (float)denominator;
+                       }
+               }
+               
+               if (got_width && got_height && got_frame_rate) {
+                       break;
+               }
+       }
+
+       gst_caps_unref(caps);
+
+       if (!got_width) {
+               throw_error(backend, GST_BACKEND_ERR_NO_MEDIA_INFO, NULL, "Unable to obtain the video width");
+               return;
+       }
+
+       if (!got_height) {
+               throw_error(backend, GST_BACKEND_ERR_NO_MEDIA_INFO, NULL, "Unable to obtain the video 
height");
+               return;
+       }
+
+       if (!got_frame_rate) {
+               throw_error(backend, GST_BACKEND_ERR_NO_MEDIA_INFO, NULL, "Unable to obtain the video frame 
rate");
+               return;
+       }
+
+       backend->media_info->aspect_ratio = (float)backend->media_info->width / 
(float)backend->media_info->height;
+}
+
+static void on_discovered(GstDiscoverer *discoverer, GstDiscovererInfo *info, GError *error, gpointer data) {
+       GstBackend *backend = (GstBackend *)data;
+       if (backend == NULL) {
+               g_debug("on_discovered user_data is NULL. Unable to continue."); //can't call 
backend->error_callback because backend is null
+               return;
+       }
+
+       GstClockTime duration = gst_discoverer_info_get_duration(info);
+       backend->media_info->duration = duration / GST_MSECOND;
+
+       backend->load_complete_callback(backend->media_info);
+}
+
+static void load_media_info_duration(GstBackend *backend) {
+
+       gint64 duration;
+       if (gst_element_query_duration(backend->element, GST_FORMAT_TIME, &duration)) {
+               backend->media_info->duration = duration / GST_MSECOND;
+               backend->load_complete_callback(backend->media_info);
+               return;
+       }
+
+       /* Usually we can query the duration above. However, there are
+        * some files where this doesn't happen (for example, some audio files
+        * where the duration is only computed when starting to play the file).
+        * In those cases, the GstDiscoverer is used below.
+        */
+       g_debug("Unable to query the media duration. Using the discoverer.");
+       gchararray uri;
+       g_object_get(G_OBJECT(backend->element), "current-uri", &uri, NULL);
+
+       GstDiscoverer *discoverer = gst_discoverer_new(GST_SECOND, NULL);
+       if (discoverer == NULL) {
+               throw_error(backend, GST_BACKEND_ERR_NO_MEDIA_INFO, NULL, "Unable to get the gstreamer 
discoverer");
+               return;
+       }
+
+       g_signal_connect(discoverer, "discovered", G_CALLBACK(on_discovered), backend);
+       gst_discoverer_start(discoverer);
+       
+       if (!gst_discoverer_discover_uri_async(discoverer, uri)) {
+               throw_error(backend, GST_BACKEND_ERR_NO_MEDIA_INFO, NULL, "Failed to start the discoverer");
+               return;
+       }
+}
+
+static void load_media_info(GstBackend *backend) {
+
+       backend->media_info = gst_backend_media_info_new();
+
+       /* Check for video and audio */
+       backend->media_info->has_video = media_has_video(backend);
+       backend->media_info->has_audio = media_has_audio(backend);
+
+       if (!backend->media_info->has_video && !backend->media_info->has_audio) {
+               throw_error(backend, GST_BACKEND_ERR_NO_VIDEO_OR_AUDIO, NULL, NULL);
+               return;
+       }
+
+       if (backend->media_info->has_video) {
+               load_media_info_video(backend);
+       }
+
+       load_media_info_duration(backend);
+}
+
+static gboolean on_gst_message(GstBus *bus, GstMessage *message, gpointer data) {
+
+       GstBackend *backend = (GstBackend *)data;
+       if (backend == NULL) {
+               g_debug("on_gst_message user_data is NULL. Unable to continue."); //can't call 
backend->error_callback because backend is null
+               return FALSE;
+       }
+
+       switch (GST_MESSAGE_TYPE(message)) {
+
+               /* ASYNC_DONE can be emitted many times (example: when returning back from
+                * pause to play). If the info is already loaded, don't do anything.
+                */
+               case GST_MESSAGE_ASYNC_DONE: {
+                       if (!backend->media_info) {
+                               load_media_info(backend);
+                       }
+                       break;
+               }
+
+               /*case GST_MESSAGE_STATE_CHANGED: {
+                       GstState old_state, new_state, pending_state;
+                       gst_message_parse_state_changed(message, &old_state, &new_state, &pending_state);
+                       g_print("on_gst_message: STATE CHANGED: old=%s, new(current)=%s, 
pending(target)=%s\n",
+                               gst_element_state_get_name(old_state), gst_element_state_get_name(new_state), 
gst_element_state_get_name(pending_state));
+                       break;
+               }*/
+
+               case GST_MESSAGE_ERROR: {
+                       if (backend->error_callback != NULL) {
+                               GError *error;
+                               gchar *debug;
+                               gst_message_parse_error(message, &error, &debug);
+
+                               throw_error(backend, GST_BACKEND_ERR_GSTREAMER, error->message, debug);
+
+                               g_error_free(error);
+                               g_free(debug);
+                       }
+                       break;
+               }
+
+               // the media file finished playing
+               case GST_MESSAGE_EOS: {
+                       if (backend->eos_reached_callback != NULL) {
+                               backend->eos_reached_callback();
+                       }
+                       break;
+               }
+
+               default:
+                       break;
+       }
+       return TRUE;
+}
+
+
+/* 'Public' functions */
+
+GstBackend *gst_backend_init(ErrorCallback error_callback, LoadCompleteCallback load_complete_callback,
+               BasicCallback eos_reached_callback) {
+
+       GstBackend *backend = gst_backend_new();
+
+       backend->error_callback = error_callback;
+       backend->load_complete_callback = load_complete_callback;
+       backend->eos_reached_callback = eos_reached_callback;
+
+       gst_init(NULL, NULL);
+
+       backend->element = gst_element_factory_make("playbin", "play");
+       if (backend->element == NULL) {
+               g_debug("gst_element_factory_make returned a null playbin element");
+               return NULL;
+       }
+
+       backend->video_element = gst_element_factory_make("gtksink", "gtksink");
+       if (backend->video_element == NULL) {
+               g_debug("gst_element_factory_make returned a null gtksink element");
+               return NULL;
+       }
+
+       g_object_set(G_OBJECT(backend->element), "video-sink", backend->video_element, NULL);
+
+       gst_bus_add_watch(gst_pipeline_get_bus(GST_PIPELINE(backend->element)), on_gst_message, backend);
+
+       return backend;
+}
+
+gboolean gst_backend_load(GstBackend *backend, char *uri) {
+       g_object_set(G_OBJECT(backend->element), "uri", uri, NULL);
+       return (gst_element_set_state(backend->element, GST_STATE_PAUSED) != GST_STATE_CHANGE_FAILURE);
+}
+
+void gst_backend_play(GstBackend *backend) {
+       gst_element_set_state(backend->element, GST_STATE_PLAYING);
+}
+
+void gst_backend_pause(GstBackend *backend) {
+       gst_element_set_state(backend->element, GST_STATE_PAUSED);
+}
+
+void gst_backend_unload(GstBackend *backend) {
+       gst_element_set_state(backend->element, GST_STATE_NULL);
+       
+       if (backend->element != NULL) {
+               gst_object_unref(GST_OBJECT(backend->element));
+               backend->element = NULL;
+       }
+
+       g_free(backend->media_info);
+       backend->media_info = NULL;
+
+       g_free(backend);
+       backend = NULL;
+}
+
+gint64 gst_backend_get_position(GstBackend *backend) {
+       gint64 position;
+       if (gst_element_query_position(backend->element, GST_FORMAT_TIME, &position)) {
+               return position / GST_MSECOND;
+       }
+
+       /* Query position wasn't successful. This is usually due to a pending state change,
+        * which happens for example after seeking, so we wait for the state change to complete
+        * and try again.
+        */
+       wait_for_state_change_to_complete(backend);
+       if (gst_element_query_position(backend->element, GST_FORMAT_TIME, &position)) {
+               return position / GST_MSECOND;
+       }
+
+       /* If we're here, we're still unable to return the position after waiting for the state
+        * change to complete. Return -1. */
+       g_debug("gst_backend_get_position unable to query the current position");
+       return -1;
+}
+
+/*
+ * time: in ms
+ */
+void gst_backend_seek(GstBackend *backend, gint64 time, gboolean is_absolute, float speed) {
+       gint64 new_position = (is_absolute ? time : gst_backend_get_position(backend) + time);
+
+       /* Note: gst_element_seek_simple can't be used here because it always resets speed to 1, 
+        * and we need to keep the current speed.
+        */
+       gst_element_seek(backend->element, speed, GST_FORMAT_TIME, GST_SEEK_FLAG_FLUSH,
+               GST_SEEK_TYPE_SET, new_position * GST_MSECOND, GST_SEEK_TYPE_NONE, GST_CLOCK_TIME_NONE);
+}
+
+void gst_backend_set_speed(GstBackend *backend, float speed) {
+       gst_element_seek(backend->element, speed, GST_FORMAT_TIME, GST_SEEK_FLAG_FLUSH,
+               GST_SEEK_TYPE_SET, gst_backend_get_position(backend) * GST_MSECOND, GST_SEEK_TYPE_NONE, 
GST_CLOCK_TIME_NONE);
+}
+
+/* Gets the GtkWidget inside the gtksink video element */
+void *gst_backend_get_video_widget(GstBackend *backend) {
+       void *widget;
+       g_object_get(backend->video_element, "widget", &widget, NULL);
+       return widget;
+}
diff --git a/src/GnomeSubtitles/Core/Command/InsertSubtitleCommand.cs 
b/src/GnomeSubtitles/Core/Command/InsertSubtitleCommand.cs
index 64261a4..cca3be2 100644
--- a/src/GnomeSubtitles/Core/Command/InsertSubtitleCommand.cs
+++ b/src/GnomeSubtitles/Core/Command/InsertSubtitleCommand.cs
@@ -1,6 +1,6 @@
 /*
  * This file is part of Gnome Subtitles.
- * Copyright (C) 2006-2019 Pedro Castro
+ * Copyright (C) 2006-2021 Pedro Castro
  *
  * Gnome Subtitles is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -131,7 +131,7 @@ public class InsertSubtitleAtVideoPositionCommand : InsertSubtitleCommand {
 
        protected override TreePath GetNewPath () {
                subtitleTime = Base.Ui.Video.Position.CurrentTime;
-               if (Base.Ui.Video.IsStatePlaying && Base.Config.VideoApplyReactionDelay) {
+               if (Base.Ui.Video.IsStatusPlaying && Base.Config.VideoApplyReactionDelay) {
                        subtitleTime -= TimeSpan.FromMilliseconds(Base.Config.VideoReactionDelay);
                }
 
diff --git a/src/GnomeSubtitles/Core/EventHandlers.cs b/src/GnomeSubtitles/Core/EventHandlers.cs
index 99186b3..8811e4c 100644
--- a/src/GnomeSubtitles/Core/EventHandlers.cs
+++ b/src/GnomeSubtitles/Core/EventHandlers.cs
@@ -1,6 +1,6 @@
 /*
  * This file is part of Gnome Subtitles.
- * Copyright (C) 2006-2019 Pedro Castro
+ * Copyright (C) 2006-2021 Pedro Castro
  *
  * Gnome Subtitles is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -307,14 +307,14 @@ public class EventHandlers {
        public void OnVideoSetSubtitleStart (object o, EventArgs args) {
                if (Base.TimingMode == TimingMode.Times) {
                        TimeSpan time = Base.Ui.Video.Position.CurrentTime;
-                       if (Base.Ui.Video.IsStatePlaying && Base.Config.VideoApplyReactionDelay) {
+                       if (Base.Ui.Video.IsStatusPlaying && Base.Config.VideoApplyReactionDelay) {
                                time -= TimeSpan.FromMilliseconds(Base.Config.VideoReactionDelay);
                        }
                        Base.CommandManager.Execute(new VideoSetSubtitleStartCommand(time));
                }
                else {
                        int frames = Base.Ui.Video.Position.CurrentFrames;
-                       if (Base.Ui.Video.IsStatePlaying && Base.Config.VideoApplyReactionDelay) {
+                       if (Base.Ui.Video.IsStatusPlaying && Base.Config.VideoApplyReactionDelay) {
                                frames -= 
(int)TimingUtil.TimeMillisecondsToFrames(Base.Config.VideoReactionDelay, Base.Ui.Video.FrameRate);
                        }
                        Base.CommandManager.Execute(new VideoSetSubtitleStartCommand(frames));
@@ -324,14 +324,14 @@ public class EventHandlers {
        public void OnVideoSetSubtitleEnd (object o, EventArgs args) {
                if (Base.TimingMode == TimingMode.Times) {
                        TimeSpan time = Base.Ui.Video.Position.CurrentTime;
-                       if (Base.Ui.Video.IsStatePlaying && Base.Config.VideoApplyReactionDelay) {
+                       if (Base.Ui.Video.IsStatusPlaying && Base.Config.VideoApplyReactionDelay) {
                                time -= TimeSpan.FromMilliseconds(Base.Config.VideoReactionDelay);
                        }
                        Base.CommandManager.Execute(new VideoSetSubtitleEndCommand(time));
                }
                else {
                        int frames = Base.Ui.Video.Position.CurrentFrames;
-                       if (Base.Ui.Video.IsStatePlaying && Base.Config.VideoApplyReactionDelay) {
+                       if (Base.Ui.Video.IsStatusPlaying && Base.Config.VideoApplyReactionDelay) {
                                frames -= 
(int)TimingUtil.TimeMillisecondsToFrames(Base.Config.VideoReactionDelay, Base.Ui.Video.FrameRate);
                        }
                        Base.CommandManager.Execute(new VideoSetSubtitleEndCommand(frames));
@@ -375,7 +375,7 @@ public class EventHandlers {
        
                if (Base.TimingMode == TimingMode.Times) {
                        TimeSpan time = Base.Ui.Video.Position.CurrentTime;
-                       if (Base.Ui.Video.IsStatePlaying && Base.Config.VideoApplyReactionDelay) {
+                       if (Base.Ui.Video.IsStatusPlaying && Base.Config.VideoApplyReactionDelay) {
                                time -= TimeSpan.FromMilliseconds(Base.Config.VideoReactionDelay);
                        }
                        Base.CommandManager.Execute(new VideoSetSubtitleEndCommand(time));
@@ -383,7 +383,7 @@ public class EventHandlers {
                }
                else {
                        int frames = Base.Ui.Video.Position.CurrentFrames;
-                       if (Base.Ui.Video.IsStatePlaying && Base.Config.VideoApplyReactionDelay) {
+                       if (Base.Ui.Video.IsStatusPlaying && Base.Config.VideoApplyReactionDelay) {
                                frames -= 
(int)TimingUtil.TimeMillisecondsToFrames(Base.Config.VideoReactionDelay, Base.Ui.Video.FrameRate);
                        }
                        Base.CommandManager.Execute(new VideoSetSubtitleEndCommand(frames));
diff --git a/src/GnomeSubtitles/Dialog/AboutDialog.cs b/src/GnomeSubtitles/Dialog/AboutDialog.cs
index dab9a27..369f2ab 100644
--- a/src/GnomeSubtitles/Dialog/AboutDialog.cs
+++ b/src/GnomeSubtitles/Dialog/AboutDialog.cs
@@ -45,7 +45,7 @@ public class AboutDialog : BaseDialog {
                dialog.Website = "https://gnomesubtitles.org";;
                dialog.WebsiteLabel = dialog.Website;
                dialog.Comments = Catalog.GetString("Video subtitling for the GNOME desktop");
-               dialog.Copyright = "Copyright © 2006-2020 Pedro Castro";
+               dialog.Copyright = "Copyright © 2006-2021 Pedro Castro";
                dialog.LicenseType = License.Gpl20;
 
                dialog.Authors = new string[]{
diff --git a/src/GnomeSubtitles/Dialog/BaseDialog.cs b/src/GnomeSubtitles/Dialog/BaseDialog.cs
index 394ce2b..e27770b 100644
--- a/src/GnomeSubtitles/Dialog/BaseDialog.cs
+++ b/src/GnomeSubtitles/Dialog/BaseDialog.cs
@@ -1,6 +1,6 @@
 /*
  * This file is part of Gnome Subtitles.
- * Copyright (C) 2009-2019 Pedro Castro
+ * Copyright (C) 2009-2021 Pedro Castro
  *
  * Gnome Subtitles is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -99,7 +99,7 @@ public abstract class BaseDialog {
                if (dialog.TransientFor == null) {
                        dialog.TransientFor = Base.Ui.Window;
                }
-
+               
                Dialog.Response += OnResponse;
                Dialog.DeleteEvent += OnDeleteEvent;
        }
diff --git a/src/GnomeSubtitles/Ui/VideoPreview/Exceptions/PlayerEngineException.cs 
b/src/GnomeSubtitles/Dialog/Message/DialogUtil.cs
similarity index 57%
rename from src/GnomeSubtitles/Ui/VideoPreview/Exceptions/PlayerEngineException.cs
rename to src/GnomeSubtitles/Dialog/Message/DialogUtil.cs
index 8fe108a..4dfe75e 100644
--- a/src/GnomeSubtitles/Ui/VideoPreview/Exceptions/PlayerEngineException.cs
+++ b/src/GnomeSubtitles/Dialog/Message/DialogUtil.cs
@@ -1,6 +1,6 @@
 /*
  * This file is part of Gnome Subtitles.
- * Copyright (C) 2008 Pedro Castro
+ * Copyright (C) 2021 Pedro Castro
  *
  * Gnome Subtitles is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -17,34 +17,14 @@
  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
  */
 
-using System;
+namespace GnomeSubtitles.Dialog.Message {
 
-namespace GnomeSubtitles.Ui.VideoPreview.Exceptions {
+       public static class DialogUtil {
+       
+               public static void ShowError(string primary, string secondary) {
+                       new BasicErrorDialog(primary, secondary).Show();
+               }
 
-public class PlayerEngineException : ApplicationException {
-       private string error = String.Empty;
-       private string debug = String.Empty;
-
-       public PlayerEngineException (string error, string debug) {
-               this.error = error;
-               this.debug = debug;
-       }
-
-       /* Properties */
-
-       public string Error {
-               get { return error; }
-       }
-
-       public string Debug {
-               get { return debug; }
-       }
-
-       /* Public methods */
-
-       public override string ToString () {
-               return this.error + "; " + this.debug;
        }
-}
 
-}
+}
\ No newline at end of file
diff --git a/src/GnomeSubtitles/Dialog/Message/SubtitleFileOpenErrorDialog.cs 
b/src/GnomeSubtitles/Dialog/Message/SubtitleFileOpenErrorDialog.cs
index 36aeda6..a79b93e 100644
--- a/src/GnomeSubtitles/Dialog/Message/SubtitleFileOpenErrorDialog.cs
+++ b/src/GnomeSubtitles/Dialog/Message/SubtitleFileOpenErrorDialog.cs
@@ -1,6 +1,6 @@
 /*
  * This file is part of Gnome Subtitles.
- * Copyright (C) 2006-2019 Pedro Castro
+ * Copyright (C) 2006-2021 Pedro Castro
  *
  * Gnome Subtitles is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -17,22 +17,57 @@
  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
  */
 
+using Gtk;
 using Mono.Unix;
 using SubLib.Exceptions;
+using SubLib.Util;
 using System;
 using System.IO;
 using System.Security;
 
 namespace GnomeSubtitles.Dialog.Message {
 
-public class SubtitleFileOpenErrorDialog : FileOpenErrorDialog {
+public class SubtitleFileOpenErrorDialog : ErrorDialog {
+       
+       private string primaryTextStart = Catalog.GetString("Could not open the file {0}.");
+       private string actionLabel = Catalog.GetString("Open another file");
 
-       public SubtitleFileOpenErrorDialog (string filename, Exception exception) : base(filename, exception) 
{
+       public SubtitleFileOpenErrorDialog (string filename, Exception exception) {
+               Logger.Error(exception, "Subtitle file open error");
+
+               string primaryText = GetPrimaryText(filename);
+               string secondaryText = GetSecondaryText(exception);
+               SetText(primaryText, secondaryText);
        }
 
        /* Overridden members */
+       
+       protected override void AddButtons () {
+               Button actionButton = dialog.AddButton(actionLabel, ResponseType.Accept) as Button;
+               actionButton.Image = new Image(Stock.Open, IconSize.Button);
+               dialog.AddButton(Stock.Ok, ResponseType.Ok);
+               
+               dialog.DefaultResponse = ResponseType.Accept;
+       }
+       
+       
+       /* Private members */
+
+       private string GetPrimaryText (string filename) {
+               return string.Format(primaryTextStart, filename);
+       }
+
+       private string GetSecondaryText (Exception exception) {
+               string text = GetSecondaryTextFromException(exception);
+               if (text != null) {
+                       return text;
+               } 
+
+               return GetGeneralExceptionErrorMessage(exception);
+       }
+
 
-       protected override string SecondaryTextFromException (Exception exception) {
+       private string GetSecondaryTextFromException (Exception exception) {
                if (exception is UnknownSubtitleFormatException)
                        return Catalog.GetString("Unable to detect the subtitle format. Please check that the 
file type is supported.");
                else if (exception is EncodingNotSupportedException)
@@ -49,8 +84,10 @@ public class SubtitleFileOpenErrorDialog : FileOpenErrorDialog {
                        return Catalog.GetString("The file could not be found.");
                else if (exception is FileTooLargeException)
                        return Catalog.GetString("The file appears to be too large for a text-based subtitle 
file.");
+               else if (exception is UriFormatException)
+                       return Catalog.GetString("The file path appears to be invalid.");
                else
-                       return String.Empty;
+                       return null;
        }
 
 }
diff --git a/src/GnomeSubtitles/Dialog/Message/VideoErrorDialog.cs 
b/src/GnomeSubtitles/Dialog/Message/VideoFileOpenErrorDialog.cs
similarity index 51%
rename from src/GnomeSubtitles/Dialog/Message/VideoErrorDialog.cs
rename to src/GnomeSubtitles/Dialog/Message/VideoFileOpenErrorDialog.cs
index 583c743..f9e2cb1 100644
--- a/src/GnomeSubtitles/Dialog/Message/VideoErrorDialog.cs
+++ b/src/GnomeSubtitles/Dialog/Message/VideoFileOpenErrorDialog.cs
@@ -1,6 +1,6 @@
 /*
  * This file is part of Gnome Subtitles.
- * Copyright (C) 2007-2019 Pedro Castro
+ * Copyright (C) 2007-2021 Pedro Castro
  *
  * Gnome Subtitles is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -17,35 +17,31 @@
  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
  */
 
-using GnomeSubtitles.Ui.VideoPreview.Exceptions;
 using Mono.Unix;
-using System;
-using SubLib.Util;
+using Gtk;
 
 namespace GnomeSubtitles.Dialog.Message {
 
-public class VideoErrorDialog : FileOpenErrorDialog {
+       public class VideoFileOpenErrorDialog : ErrorDialog {
+
+               private string primaryTextStart = Catalog.GetString("Could not play the file '{0}'.");
+               private string actionLabel = Catalog.GetString("Open another file");
+
+               public VideoFileOpenErrorDialog(string filename, string error) {
+                       string primaryText = string.Format(primaryTextStart, filename);
+                       string secondaryText = error;
+                       SetText(primaryText, secondaryText);
+               }
+               
+               /* Overridden members */
+       
+               protected override void AddButtons() {
+                       Button actionButton = dialog.AddButton(actionLabel, ResponseType.Accept) as Button;
+                       actionButton.Image = new Image(Stock.Open, IconSize.Button);
+                       dialog.AddButton(Stock.Ok, ResponseType.Ok);
+                       
+                       dialog.DefaultResponse = ResponseType.Accept;
+               }
 
-       /* Strings */
-       private string primaryTextStart = Catalog.GetString("Could not play the file");
-
-       public VideoErrorDialog (Uri uri, Exception exception) : base(uri, exception) {
-               Logger.Error(exception, "Video error");
        }
-
-       /* Overridden members */
-
-       protected override string GetPrimaryText (string filename) {
-               return primaryTextStart + " " + filename + ".";
-       }
-
-       protected override string SecondaryTextFromException (Exception exception) {
-               if (exception is PlayerEngineException)
-                       return (exception as PlayerEngineException).Error;
-               else
-                       return String.Empty;
-       }
-
-}
-
 }
diff --git a/src/GnomeSubtitles/Dialog/VideoSeekToDialog.cs b/src/GnomeSubtitles/Dialog/VideoSeekToDialog.cs
index adbdd33..9b8ee10 100644
--- a/src/GnomeSubtitles/Dialog/VideoSeekToDialog.cs
+++ b/src/GnomeSubtitles/Dialog/VideoSeekToDialog.cs
@@ -1,6 +1,6 @@
 /*
  * This file is part of Gnome Subtitles.
- * Copyright (C) 2008-2018 Pedro Castro
+ * Copyright (C) 2008-2021 Pedro Castro
  *
  * Gnome Subtitles is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -98,14 +98,10 @@ public class VideoSeekToDialog : BaseDialog {
                spinButton.Value = newValue;
        }
 
-       //private void OnClear (object o, EventArgs args) {
-       //      SetSpinButtonValue(0);
-       //}
-
        protected override bool ProcessResponse (ResponseType response) {
                if (response == ResponseType.Ok) {
                        if (timingMode == TimingMode.Times) {
-                               TimeSpan position = TimeSpan.FromMilliseconds(spinButton.Value); //TODO move 
to Util
+                               TimeSpan position = TimeSpan.FromMilliseconds(spinButton.Value);
                                Base.Ui.Video.Seek(position);
                        }
                        else {
diff --git a/src/GnomeSubtitles/Execution/AssemblyInfo.cs.in b/src/GnomeSubtitles/Execution/AssemblyInfo.cs.in
index 73b39a5..18af87f 100644
--- a/src/GnomeSubtitles/Execution/AssemblyInfo.cs.in
+++ b/src/GnomeSubtitles/Execution/AssemblyInfo.cs.in
@@ -1,6 +1,6 @@
 /*
  * This file is part of Gnome Subtitles.
- * Copyright (C) 2006-2020 Pedro Castro
+ * Copyright (C) 2006-2021 Pedro Castro
  *
  * Gnome Subtitles is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -22,5 +22,5 @@ using System.Reflection;
 [assembly: AssemblyVersion("@VERSION@")]
 [assembly: AssemblyTitle ("Gnome Subtitles")]
 [assembly: AssemblyDescription ("Video subtitling for the GNOME desktop")]
-[assembly: AssemblyCopyright ("Copyright (c) 2006-2020 Pedro Castro")]
+[assembly: AssemblyCopyright ("Copyright (c) 2006-2021 Pedro Castro")]
 
diff --git a/src/GnomeSubtitles/Ui/VideoPreview/Exceptions/PlayerCouldNotInitiateEngineException.cs 
b/src/GnomeSubtitles/Ui/VideoPreview/Exceptions/PlayerException.cs
similarity index 80%
rename from src/GnomeSubtitles/Ui/VideoPreview/Exceptions/PlayerCouldNotInitiateEngineException.cs
rename to src/GnomeSubtitles/Ui/VideoPreview/Exceptions/PlayerException.cs
index 8e5bd87..a8d8719 100644
--- a/src/GnomeSubtitles/Ui/VideoPreview/Exceptions/PlayerCouldNotInitiateEngineException.cs
+++ b/src/GnomeSubtitles/Ui/VideoPreview/Exceptions/PlayerException.cs
@@ -1,6 +1,6 @@
 /*
  * This file is part of Gnome Subtitles.
- * Copyright (C) 2008-2018 Pedro Castro
+ * Copyright (C) 2021 Pedro Castro
  *
  * Gnome Subtitles is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -21,11 +21,11 @@ using System;
 
 namespace GnomeSubtitles.Ui.VideoPreview.Exceptions {
 
-public class PlayerCouldNotInitiateEngineException : ApplicationException {
+       public class PlayerException : ApplicationException {
+       
+               public PlayerException (string error) : base(error) {
+               }
 
-       public PlayerCouldNotInitiateEngineException (string message) : base(message) {
        }
 
-}
-
-}
+}
\ No newline at end of file
diff --git a/src/GnomeSubtitles/Ui/VideoPreview/MediaBackend.cs 
b/src/GnomeSubtitles/Ui/VideoPreview/MediaBackend.cs
new file mode 100644
index 0000000..e9c44b1
--- /dev/null
+++ b/src/GnomeSubtitles/Ui/VideoPreview/MediaBackend.cs
@@ -0,0 +1,99 @@
+/*
+ * This file is part of Gnome Subtitles.
+ * Copyright (C) 2021 Pedro Castro
+ *
+ * Gnome Subtitles is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * Gnome Subtitles 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ */
+
+using GnomeSubtitles.Core;
+using Gtk;
+
+namespace GnomeSubtitles.Ui.VideoPreview {
+
+       /* Enums */
+       public enum MediaStatus { Unloaded, Loading, Loaded, Paused, Playing };
+
+       /* Delegates */
+       public delegate void ErrorFoundHandler(string errorMessage);
+       public delegate void StatusChangedHandler(MediaStatus newStatus);
+       
+       public abstract class MediaBackend {
+       
+               private MediaStatus status = MediaStatus.Unloaded;
+               
+       
+               /* Events */
+               public event ErrorFoundHandler ErrorFound;
+               public event StatusChangedHandler StatusChanged;
+               public event BasicEventHandler EndOfStreamReached; 
+       
+       
+               /* Public properties */
+       
+               public MediaStatus Status {
+                       get { return status; }
+               }
+
+               public abstract string Name { get; }
+               
+               ///Returns the current media position in ms, or -1 if unable to get the value                 
                          
+               public abstract long CurrentPosition { get; }
+               public abstract long Duration { get; }
+               public abstract bool HasVideo { get; }
+               public abstract float AspectRatio { get; }
+               public abstract float FrameRate { get; }
+               public abstract bool HasAudio { get; }
+               
+
+               /* Public methods */
+               
+               public abstract void Initialize();
+               public abstract bool Load(string uri);
+               
+               public abstract void Unload();
+               public abstract void Dispose();
+               
+               public abstract void Play();
+               public abstract void Pause();
+               public abstract void SetSpeed(float speed);
+               public abstract void Seek(long time, bool isAbsolute);
+               public abstract Widget CreateVideoWidget();
+       
+       
+               /* Protected members */
+               
+               protected void SetStatus(MediaStatus status) {
+                       this.status = status;
+                       
+                       if (StatusChanged != null) {
+                               StatusChanged(status);
+                       }
+               }
+               
+               protected void TriggerErrorFound(string error) {
+                       if (ErrorFound != null) {
+                               ErrorFound(error);
+                       }
+               }
+               
+               protected void TriggerEndOfStreamReached() {
+                       if (EndOfStreamReached != null) {
+                               EndOfStreamReached();
+                       }
+               }
+
+       }
+
+}
\ No newline at end of file
diff --git a/src/GnomeSubtitles/Ui/VideoPreview/Player.cs b/src/GnomeSubtitles/Ui/VideoPreview/Player.cs
index d3611f2..08d3e29 100644
--- a/src/GnomeSubtitles/Ui/VideoPreview/Player.cs
+++ b/src/GnomeSubtitles/Ui/VideoPreview/Player.cs
@@ -17,269 +17,245 @@
  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
  */
 
-using GnomeSubtitles.Ui.VideoPreview.Exceptions;
-using GStreamer;
+using External.GStreamer;
+using GnomeSubtitles.Core;
 using Gtk;
-using SubLib.Core.Domain;
 using SubLib.Util;
 using System;
 
 namespace GnomeSubtitles.Ui.VideoPreview {
 
 /* Delegates */
-public delegate void PlayerErrorEventHandler (Uri videoUri, Exception e);
-public delegate void VideoDurationEventHandler (TimeSpan duration);
+public delegate void PlayerErrorHandler(string error);
 
 public class Player {
 
-       private AspectFrame frame = null;
-       private Playbin playbin = null;
+       private AspectFrame frame = null; //the backend video widget is added as a child of this frame
+       private MediaBackend backend = null;
        private PlayerPositionWatcher position = null;
-       private bool hasFoundDuration = false;
-       private Uri videoUri = null;
-       private VideoInfo videoInfo = null;
-       private float speed = 1;
-
+       private MediaStatus status = MediaStatus.Unloaded; //We don't always have a backend, but we always 
should have a status, so that's why it's here
+       private int speed = DefaultSpeed; //Speed is divided by 10 to get the actual speed (example: 10->1). 
Storing as int to avoid floating rounding errors.
+       
        /* Constants */
-       public const float DefaultAspectRatio = 1.67f;
-       public const float DefaultMinSpeed = 0.1f;
-       public const float DefaultSpeedStep = 0.1f;
-       public const float DefaultMaxSpeed = 2;
-
-       public Player (AspectFrame aspectFrame) {
-               this.frame = aspectFrame;
+       private const int DefaultSpeed = 100;
+       
 
+       public Player (AspectFrame frame) {
+               this.frame = frame;
                InitializePositionWatcher();
        }
        
 
        /* Events */
-       public event PlayerErrorEventHandler Error;
-       public event EndOfStreamEventHandler EndOfStream;
-       public event StateEventHandler StateChanged;
+       public event PlayerErrorHandler ErrorFound; //Note: all errors are considered fatal, meaning the 
backend must be disposed immediately
+       public event BasicEventHandler EndOfStreamReached;
+       public event StatusChangedHandler StatusChanged;
        public event PositionPulseEventHandler PositionPulse;
-       public event VideoInfoEventHandler FoundVideoInfo;
-       public event VideoDurationEventHandler FoundDuration;
+
 
        /* Properties */
 
-       public MediaStatus State {
-               get { return playbin.CurrentStatus; }
+       public MediaStatus Status {
+               get { return status; }
+       }
+       
+       public long Duration {
+               get { return backend.Duration; }
        }
 
-       public TimeSpan Duration {
-               get { return playbin.Duration; }
+       public bool HasVideo {
+               get { return backend.HasVideo; }
        }
 
        public float AspectRatio {
-               get { return videoInfo.AspectRatio; }
+               get { return backend.AspectRatio; }
        }
 
        public float FrameRate {
-               get { return videoInfo.FrameRate; }
+               get { return backend.FrameRate; }
        }
 
        public bool HasAudio {
-               get { return videoInfo.HasAudio; }
-       }
-
-       public bool HasVideo {
-               get { return videoInfo.HasVideo; }
+               get { return backend.HasAudio; }
        }
 
-       public Uri VideoUri {
-               get { return videoUri; }
-       }
-
-       public float Speed {
+       public int Speed {
                get { return speed; }
        }
        
-       public bool IsLoadComplete {
-               get { return (playbin.CurrentStatus != MediaStatus.Unloaded) && HasVideoInfo() && 
HasDuration(); }
-       }
-       
 
        /* Public methods */
 
-       public void Open (Uri videoUri) {
-               this.videoUri = videoUri;
-
-               InitializePlaybin();
-               playbin.Load(videoUri.AbsoluteUri);
+       public void Open(Uri mediaUri) {
+               if (status != MediaStatus.Unloaded) {
+                       throw new Exception("Trying to load a player which is not unloaded.");
+               }
+               
+               backend = InitializeBackend();
+       
+               if (!backend.Load(mediaUri.AbsoluteUri)) {
+                       throw new Exception("Unable to load the media file.");
+               }
        }
 
-       public void Close () {
+       public void Close() {
                position.Stop();
                
-               playbin.Unload();
-               DisposePlaybin();
-
-               videoUri = null;
-               hasFoundDuration = false;
-               videoInfo = null;
-               speed = 1;
+               //Unload
+               if ((status != MediaStatus.Unloaded) && (status != MediaStatus.Loading)) {
+                       backend.Unload();
+               }
+               status = MediaStatus.Unloaded;
+               
+               //Dispose
+               DisposeBackend();
+               
+               speed = DefaultSpeed;
        }
 
-       public void Play () {
-               playbin.Play();
+       public void Play() {
+               if (status == MediaStatus.Playing) {
+                       return;
+               }
+               
+               if ((status != MediaStatus.Loaded) && (status != MediaStatus.Paused)) {
+                       throw new Exception(string.Format("Trying to play but the player status is {0}", 
status));
+               }
+       
+               backend.Play();
        }
 
        public void Pause () {
-               playbin.Pause();
-       }
-
-    public void SpeedUp () {
-        if (this.speed >= DefaultMaxSpeed)
-               return;
-
-               this.speed += DefaultSpeedStep;
-               ChangeSpeed(this.speed);
-       }
-
-       public void SpeedDown () {
-           if (this.speed <= DefaultMinSpeed)
-               return;
+               if (status == MediaStatus.Paused) {
+                       return;
+               }
+               
+               if (status != MediaStatus.Playing) {
+                       throw new Exception(string.Format("Trying to pause but the player status is {0}", 
status));
+               }
 
-           this.speed -= DefaultSpeedStep;
-               ChangeSpeed(this.speed);
+               backend.Pause();
        }
 
-       public void SpeedReset () {
-               this.speed = 1;
-               ChangeSpeed(this.speed);
+       public void SetSpeed(int speed) {
+               this.speed = speed;
+               backend.SetSpeed(((float)speed)/100);
        }
-
-       public void Rewind (TimeSpan dec) {
-               Seek(playbin.CurrentPosition - dec);
+       
+       public void ResetSpeed() {
+               SetSpeed(DefaultSpeed);
        }
-
-       public void Forward (TimeSpan inc) {
-               Seek(playbin.CurrentPosition + inc);
+       
+       /// <summary>
+       /// Rewind the specified time.
+       /// </summary>
+       /// <param name="time">Time in ms.</param>
+       public void Rewind(long time) {
+               backend.Seek(-time, false);
        }
 
-       public void Seek (TimeSpan newPosition) {
-               playbin.Seek(newPosition, speed);
+       /// <summary>
+       /// Forward the specified time.
+       /// </summary>
+       /// <param name="time">Time in ms.</param>
+       public void Forward(long time) {
+               backend.Seek(time, false);
        }
 
-       public void Seek (double newPosition) {
-               playbin.Seek(newPosition, speed); // newPosition in milliseconds
+       /// <summary>
+       /// Seek to the specified position.
+       /// </summary>
+       /// <param name="position">Position in ms.</param>
+       public void Seek(long position) {
+               backend.Seek(position, true);
        }
 
 
        /* Private members */
 
-       private bool HasDuration() {
-               return playbin.Duration != TimeSpan.Zero;
-       }
-       
-       public bool HasVideoInfo() {
-               return videoInfo != null;
-       }
-
-       private void InitializePlaybin () {
-               playbin = new Playbin();
-
-               if (!playbin.Initiate()) {
-                       throw new PlayerCouldNotInitiateEngineException("Unable to initiate the playbin 
engine");
-               }
+       private MediaBackend InitializeBackend() {
+               MediaBackend backend = new GstBackend();
+               backend.Initialize();
                
-               Widget videoWidget = playbin.GetVideoWidget();
+               Widget videoWidget = backend.CreateVideoWidget();
                if (videoWidget == null) {
-                       throw new PlayerCouldNotInitiateEngineException("Unable to get the video widget from 
the playbin engine");
+                       throw new Exception("Unable to create the video widget");
                }
 
                frame.Child = videoWidget;
                videoWidget.Realize();
                videoWidget.Show();
 
-               playbin.Error += OnPlaybinError;
-               playbin.EndOfStream += OnPlaybinEndOfStream;
-               playbin.StateChanged += OnPlaybinStateChanged;
-               playbin.FoundVideoInfo += OnPlaybinFoundVideoInfo;
-               playbin.FoundTag += OnPlaybinFoundTag;
+               backend.ErrorFound += OnBackendErrorFound;
+               backend.StatusChanged += OnBackendStatusChanged;
+               backend.EndOfStreamReached += OnBackendEndOfStreamReached;
+               
+               return backend;
        }
        
-       private void DisposePlaybin () {
-               playbin.Error -= OnPlaybinError;
-               playbin.EndOfStream -= OnPlaybinEndOfStream;
-               playbin.StateChanged -= OnPlaybinStateChanged;
-               playbin.FoundVideoInfo -= OnPlaybinFoundVideoInfo;
-               playbin.FoundTag -= OnPlaybinFoundTag;
+       private void DisposeBackend() {
+               backend.ErrorFound -= OnBackendErrorFound;
+               backend.StatusChanged -= OnBackendStatusChanged;
+               backend.EndOfStreamReached -= OnBackendEndOfStreamReached;
                
-               Widget videoWidget = playbin.GetVideoWidget();
-               frame.Remove(videoWidget);
+               frame.Remove(frame.Child);
 
-               playbin.Dispose();
-               playbin = null;
+               backend.Dispose();
+               backend = null;
        }
        
-       private void InitializePositionWatcher () {
+       private void InitializePositionWatcher() {
                position = new PlayerPositionWatcher(GetPosition);
                position.PositionPulse += OnPositionWatcherPulse;
        }
 
-       /// <summary>Gets the current player position.</summary>
-       private TimeSpan GetPosition () {
-               return playbin.CurrentPosition;
-       }
-
-       private void ChangeSpeed (float newSpeed) {
-           playbin.Seek(playbin.CurrentPosition, newSpeed);
+       private long GetPosition() {
+               return backend.CurrentPosition;
        }
 
 
        /* Event members */
 
-       private void OnPlaybinError (ErrorEventArgs args) {
-               if (Error != null)
-                       Error(videoUri, new PlayerEngineException(args.Error, args.Debug));
+       private void OnPositionWatcherPulse (long time) {
+               if (PositionPulse != null)
+                       PositionPulse(time);
        }
 
-       private void OnPlaybinEndOfStream () {
-               position.Stop();
-               if (EndOfStream != null)
-                       EndOfStream();
+       private void OnBackendErrorFound(string error) {
+               if (ErrorFound != null) {
+                       ErrorFound(error);
+               }
        }
-
-       private void OnPlaybinStateChanged (StateEventArgs args) {
-               if (args.State == MediaStatus.Unloaded)
+       
+       private void OnBackendStatusChanged(MediaStatus newStatus) {
+               this.status = newStatus;
+               
+               //Handle position watcher
+               if (newStatus == MediaStatus.Unloaded) {
                        position.Stop();
-               else
+               } else if (newStatus != MediaStatus.Loading) {
                        position.Start();
-
-               if (StateChanged != null)
-                       StateChanged(args);
-       }
-
-       private void OnPositionWatcherPulse (TimeSpan time) {
-               if (PositionPulse != null)
-                       PositionPulse(time);
-       }
-
-       private void OnPlaybinFoundVideoInfo (VideoInfoEventArgs args) {
-               Logger.Info("[Player] Media info: {0}", args.VideoInfo.ToString());
-               this.videoInfo = args.VideoInfo;
-
-               /* Set defaults if there is no video */
-               if (!videoInfo.HasVideo) {
-                       videoInfo.FrameRate = SubtitleConstants.DefaultFrameRate;
-                       videoInfo.AspectRatio = DefaultAspectRatio;
+               }
+               
+               //Print info when loaded
+               if (newStatus == MediaStatus.Loaded) {
+                       Logger.Info("[Player] Media Loaded: Backend={0}, Duration={1}ms, HasVideo={2}, "
+                               + "AspectRatio={3}, FrameRate={4}, HasAudio={5}",
+                               backend.Name, backend.Duration, backend.HasVideo, backend.AspectRatio,
+                               backend.FrameRate, backend.HasAudio);
                }
 
-               frame.Ratio = videoInfo.AspectRatio;
+               if (StatusChanged != null) {
+                       StatusChanged(newStatus);
+               }
 
-               if (FoundVideoInfo != null)
-                       FoundVideoInfo(args);
        }
-
-       private void OnPlaybinFoundTag (TagEventArgs args) {
-               if ((!hasFoundDuration) && (FoundDuration != null) && (playbin.Duration != TimeSpan.Zero)) {
-                       TimeSpan duration = playbin.Duration;
-                       Logger.Info("[Player] Media duration: {0}", duration);
-
-                       hasFoundDuration = true;
-                       FoundDuration(duration);
+       
+       private void OnBackendEndOfStreamReached() {
+               position.Stop();
+               
+               if (EndOfStreamReached != null) {
+                       EndOfStreamReached();
                }
        }
 
diff --git a/src/GnomeSubtitles/Ui/VideoPreview/PlayerPositionWatcher.cs 
b/src/GnomeSubtitles/Ui/VideoPreview/PlayerPositionWatcher.cs
index 33d1dbe..f369d52 100644
--- a/src/GnomeSubtitles/Ui/VideoPreview/PlayerPositionWatcher.cs
+++ b/src/GnomeSubtitles/Ui/VideoPreview/PlayerPositionWatcher.cs
@@ -1,6 +1,6 @@
 /*
  * This file is part of Gnome Subtitles.
- * Copyright (C) 2006-2019 Pedro Castro
+ * Copyright (C) 2006-2021 Pedro Castro
  *
  * Gnome Subtitles is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -17,18 +17,16 @@
  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
  */
 
-using System;
-
 namespace GnomeSubtitles.Ui.VideoPreview {
 
 /* Delegates */
 
-//Represents a function that gets a time from the player
-public delegate TimeSpan PlayerGetTimeFunc ();
+//Represents a function that gets the time from the player
+public delegate long PlayerGetTimeFunc ();
 
 //Represents a function that handles a frequent pulse in the player position. This means the pulse is
 //emitted every 'x' ms and not only when the position changes.
-public delegate void PositionPulseEventHandler (TimeSpan position);
+public delegate void PositionPulseEventHandler (long position);
 
 public class PlayerPositionWatcher {
        private uint timeoutId = 0;
@@ -74,12 +72,17 @@ public class PlayerPositionWatcher {
        }
 
        private bool CheckPosition () {
-               TimeSpan position = PlayerGetPosition();
-               EmitPositionPulse(position);
+               long position = PlayerGetPosition();
+               
+               //-1 means we couln't get the position value atm, so ignore it
+               if (position != -1) {
+                       EmitPositionPulse(position);
+               }
+
                return true;
        }
 
-       private void EmitPositionPulse (TimeSpan position) {
+       private void EmitPositionPulse (long position) {
                if (PositionPulse != null) {
                        PositionPulse(position);
                }
diff --git a/src/GnomeSubtitles/Ui/VideoPreview/Video.cs b/src/GnomeSubtitles/Ui/VideoPreview/Video.cs
index 9d3f057..b222c48 100644
--- a/src/GnomeSubtitles/Ui/VideoPreview/Video.cs
+++ b/src/GnomeSubtitles/Ui/VideoPreview/Video.cs
@@ -20,11 +20,14 @@
 using Gdk;
 using GnomeSubtitles.Core;
 using GnomeSubtitles.Dialog.Message;
+using GnomeSubtitles.Ui.VideoPreview.Exceptions;
 using Gtk;
-using GStreamer;
+using Mono.Unix;
 using SubLib.Core.Domain;
 using SubLib.Core.Timing;
+using SubLib.Util;
 using System;
+using System.IO;
 
 namespace GnomeSubtitles.Ui.VideoPreview {
 
@@ -32,6 +35,7 @@ public class Video {
        private Box videoArea = null;
        private AspectFrame frame = null;
 
+       private Uri mediaUri = null;
        private Player player = null;
        private VideoPosition position = null;
        private SubtitleOverlay overlay = null;
@@ -40,6 +44,15 @@ public class Video {
        private bool isLoaded = false;
        private bool playPauseToggleIsSilent = false; //Used to indicate whether toggling the button should 
not issue the toggled signal
 
+       /* Constants */
+       private const float DefaultAspectRatio = 1.67f;
+       private const int MinSpeed = 10;
+       private const int SpeedStep = 10;
+       private const int MaxSpeed = 200;
+
+       private readonly string PlayerErrorPrimaryMessage = Catalog.GetString("Media Player Error");
+       private readonly string PlayerUnexpectedErrorSecondaryMessage = Catalog.GetString("An unexpected 
error has occurred: '{0}'. See the log for additional information.");
+       
 
        public Video () {
                videoArea = Base.GetWidget(WidgetNames.VideoAreaHBox) as Box;
@@ -72,16 +85,16 @@ public class Video {
                get { return isLoaded; }
        }
 
-       public bool IsStatePlaying {
-               get { return isLoaded && player.State == MediaStatus.Playing; }
+       public bool IsStatusPlaying {
+               get { return isLoaded && player.Status == MediaStatus.Playing; }
        }
 
        public float FrameRate {
-               get { return player.FrameRate; }
+               get { return player.HasVideo ? player.FrameRate : SubtitleConstants.DefaultFrameRate; }
        }
 
        public TimeSpan Duration {
-               get { return player.Duration; }
+               get { return TimeSpan.FromMilliseconds(player.Duration); }
        }
 
        public bool HasAudio {
@@ -104,34 +117,52 @@ public class Video {
        }
 
        /// <summary>Opens a video file.</summary>
-       /// <exception cref="PlayerCouldNotOpenVideoException">Thrown if the player could not open the 
video.</exception>
-       public void Open (Uri videoUri) {
-               Close();
+       public void Open(Uri mediaUri) {
+               this.mediaUri = mediaUri;
                
-               frame.Show();
+               try {
+                       player.Open(mediaUri);
+               } catch(Exception e) {
+                       HandlePlayerError(e);
+                       return;
+               }
 
-               player.Open(videoUri);
+               frame.Show();
        }
-
-       public void Close () {
-               if (!isLoaded)
+       
+       public void Close() {
+               if (!isLoaded) {
                        return;
+               }
 
                isLoaded = false;
 
+               mediaUri = null;
                player.Close();
                position.Disable();
                tracker.Close();
                overlay.Close();
 
                /* Update the frame */
-               frame.Ratio = Player.DefaultAspectRatio;
-               frame.Hide(); //To keep the frame from showing the last video image that was being played
+               frame.Ratio = DefaultAspectRatio;
+               frame.Hide();
                
                SilentDisablePlayPauseButton();
-               UpdateSpeedControls(1);
+               UpdateSpeedControls(player.Speed);
                SetControlsSensitivity(false);
        }
+       
+
+       //Things we need to do if there was an error while opening a file
+       private void CloseWhenOpeningHasFailed() {
+               mediaUri = null;
+               
+               try {
+                       player.Close();
+               } catch(Exception e) {
+                       Logger.Error(e, "Player error while forcing it to close after a fatal error has 
occurred when opening a file. Should be ok.");
+               }
+       }
 
        public void Quit () {
                if (isLoaded) {
@@ -155,17 +186,27 @@ public class Video {
        }
 
        public void SpeedUp () {
-           player.SpeedUp();
-           UpdateSpeedControls(player.Speed);
+               int newSpeed = player.Speed + SpeedStep;
+               if (newSpeed > MaxSpeed) {
+                       return;
+               }
+
+               player.SetSpeed(newSpeed);
+           UpdateSpeedControls(newSpeed);
        }
 
        public void SpeedDown () {
-           player.SpeedDown();
-           UpdateSpeedControls(player.Speed);
+           int newSpeed = player.Speed - SpeedStep;
+               if (newSpeed < MinSpeed) {
+                       return;
+               }
+
+               player.SetSpeed(newSpeed);
+           UpdateSpeedControls(newSpeed);
        }
 
        public void SpeedReset () {
-           player.SpeedReset();
+           player.ResetSpeed();
            UpdateSpeedControls(player.Speed);
        }
 
@@ -175,7 +216,7 @@ public class Video {
                if (!isLoaded)
                        return;
 
-               player.Seek(time);
+               player.Seek((long)time.TotalMilliseconds);
        }
 
        public void Seek (int frames) {
@@ -222,11 +263,12 @@ public class Video {
                player.Pause();
        }
 
-       private void UpdateSpeedControls (float speed) {
-               (Base.GetWidget(WidgetNames.VideoSpeedButton) as Button).Label = String.Format("{0:0.0}x", 
speed);
+       private void UpdateSpeedControls (int speed) {
+               float speedFraction = ((float)speed) / 100;
+               (Base.GetWidget(WidgetNames.VideoSpeedButton) as Button).Label = String.Format("{0:0.0}x", 
speedFraction);
 
-               Base.GetWidget(WidgetNames.VideoSpeedDownButton).Sensitive = (speed > Player.DefaultMinSpeed);
-               Base.GetWidget(WidgetNames.VideoSpeedUpButton).Sensitive = (speed < Player.DefaultMaxSpeed);
+               Base.GetWidget(WidgetNames.VideoSpeedDownButton).Sensitive = (speed > MinSpeed);
+               Base.GetWidget(WidgetNames.VideoSpeedUpButton).Sensitive = (speed < MaxSpeed);
        }
 
        private void InitializeVideoFrame () {
@@ -249,22 +291,40 @@ public class Video {
                bin.Add(videoFrameEventBox);
                bin.ShowAll();
        }
+       
+       private void HandlePlayerError(Exception e) {
+               Logger.Error(e, "Player error (status {0})", player.Status);
+
+               string secondaryMessage = (e is PlayerException ? e.Message : 
string.Format(PlayerUnexpectedErrorSecondaryMessage, e.Message));
+
+               /* All player errors are fatal, so we need to close it.
+                * If we get a player error and we're not loaded yet, it means we got the error
+                * while loading the file, so we need to do some cleanup.
+                * Otherwise, we just close it in the normal fashion.
+                */
+               if (!isLoaded) {
+                       string filename = Path.GetFileName(mediaUri.LocalPath);
+                       CloseWhenOpeningHasFailed();
+                       ShowVideoFileOpenErrorDialog(filename, secondaryMessage);
+               } else {
+                       Base.CloseVideo();
+                       DialogUtil.ShowError(PlayerErrorPrimaryMessage, secondaryMessage);
+               }
+       }
 
        private void InitializePlayer () {
                player = new Player(frame);
 
-               player.FoundVideoInfo += OnPlayerFoundVideoInfo;
-               player.StateChanged += OnPlayerStateChanged;
-               player.FoundDuration += OnPlayerFoundDuration;
-               player.EndOfStream += OnPlayerEndOfStream;
-               player.Error += OnPlayerError;
+               player.StatusChanged += OnPlayerStatusChanged;
+               player.EndOfStreamReached += OnPlayerEndOfStreamReached;
+               player.ErrorFound += OnPlayerErrorFound;
        }
 
        private void SetControlsSensitivity (bool sensitivity) {
                Base.GetWidget(WidgetNames.VideoTimingsVBox).Sensitive = sensitivity;
                Base.GetWidget(WidgetNames.VideoPlaybackHBox).Sensitive = sensitivity;
 
-               if ((Core.Base.Ui.View.Selection.Count == 1) && sensitivity)
+               if ((Base.Ui.View.Selection.Count == 1) && sensitivity)
                        SetSelectionDependentControlsSensitivity(true);
                else
                        SetSelectionDependentControlsSensitivity(false);
@@ -284,19 +344,6 @@ public class Video {
                }
        }
 
-       private void HandlePlayerLoading () {
-               if (isLoaded || (!IsPlayerLoadComplete()))
-                       return;
-
-               isLoaded = true;
-               SetControlsSensitivity(true);
-               Base.UpdateFromVideoLoaded(player.VideoUri);
-       }
-
-       private bool IsPlayerLoadComplete () {
-               return (player != null) && player.IsLoadComplete;
-       }
-
 
        /* Event members */
 
@@ -319,31 +366,30 @@ public class Video {
                        Pause();
        }
 
-       private void OnPlayerFoundVideoInfo (VideoInfoEventArgs args) {
-               HandlePlayerLoading();
-       }
-
-       private void OnPlayerStateChanged (StateEventArgs args) {
-               if (args.State == MediaStatus.Loaded) {
-                       HandlePlayerLoading();
+       private void OnPlayerStatusChanged(MediaStatus newStatus) {
+               if (newStatus == MediaStatus.Loaded) {
+                       isLoaded = true;
+                       frame.Ratio = player.HasVideo ? player.AspectRatio : DefaultAspectRatio;
+                       SetControlsSensitivity(true);
+                       Base.UpdateFromVideoLoaded(mediaUri);
                }
        }
 
-       private void OnPlayerFoundDuration (TimeSpan duration) {
-               HandlePlayerLoading();
-       }
-
-       private void OnPlayerEndOfStream () {
+       private void OnPlayerEndOfStreamReached() {
                ToggleButton playPauseButton = Base.GetWidget(WidgetNames.VideoPlayPauseButton) as 
ToggleButton;
                playPauseButton.Active = false;
        }
 
-       private void OnPlayerError (Uri videoUri, Exception e) {
-               Close();
-               VideoErrorDialog dialog = new VideoErrorDialog(videoUri, e);
+       private void OnPlayerErrorFound(string error) {
+               HandlePlayerError(new PlayerException(error));
+       }
+       
+       private void ShowVideoFileOpenErrorDialog(string filename, string error) {
+               VideoFileOpenErrorDialog dialog = new VideoFileOpenErrorDialog(filename, error);
                bool toOpenAnother = dialog.WaitForResponse();
-               if (toOpenAnother)
+               if (toOpenAnother) {
                        Base.Ui.OpenVideo();
+               }
        }
 
        private void OnSubtitleSelectionChanged (TreePath[] paths, Subtitle subtitle) {
diff --git a/src/GnomeSubtitles/Ui/VideoPreview/VideoPosition.cs 
b/src/GnomeSubtitles/Ui/VideoPreview/VideoPosition.cs
index cc8b496..7a1694f 100644
--- a/src/GnomeSubtitles/Ui/VideoPreview/VideoPosition.cs
+++ b/src/GnomeSubtitles/Ui/VideoPreview/VideoPosition.cs
@@ -1,6 +1,6 @@
 /*
  * This file is part of Gnome Subtitles.
- * Copyright (C) 2006-2019 Pedro Castro
+ * Copyright (C) 2006-2021 Pedro Castro
  *
  * Gnome Subtitles is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -39,8 +39,8 @@ public class VideoPosition {
        private bool isPlayerUpdate = false;
 
        /* Constants */
-       private const int userUpdateTimeout = 100; //Milliseconds
-       private TimeSpan seekIncrement = TimeSpan.FromMilliseconds(500);
+       private const int userUpdateTimeout = 100; //ms
+       private long seekIncrement = 500; //ms
 
        /* Delegates */
        public delegate void VideoPositionPulseHandler (TimeSpan position);
@@ -62,11 +62,10 @@ public class VideoPosition {
 
        /* Public properties */
 
-       public TimeSpan SeekIncrement {
+       public long SeekIncrement {
                get { return seekIncrement; }
        }
 
-       /// <summary>The current position, in seconds.</summary>
        public TimeSpan CurrentTime {
                get { return position; }
        }
@@ -76,7 +75,7 @@ public class VideoPosition {
        }
 
        public TimeSpan Duration {
-               get { return player.Duration; }
+               get { return TimeSpan.FromMilliseconds(player.Duration); }
        }
 
        public int DurationInFrames {
@@ -109,14 +108,14 @@ public class VideoPosition {
        }
 
        /// <summary>Handles changes in the player position.</summary>
-       private void OnPlayerPositionPulse (TimeSpan newPosition) {
-               position = newPosition;
+       private void OnPlayerPositionPulse (long newPosition) {
+               position = TimeSpan.FromMilliseconds(newPosition);
 
                if (userUpdateTimeoutId == 0)  //There is not a manual positioning going on
-                       UpdateSlider(newPosition);
+                       UpdateSlider(position);
 
-               UpdatePositionValueLabel(newPosition);
-               EmitVideoPositionPulse(newPosition);
+               UpdatePositionValueLabel(position);
+               EmitVideoPositionPulse(position);
        }
 
        private void OnBaseVideoLoaded (Uri videoUri) {
@@ -127,7 +126,7 @@ public class VideoPosition {
 
        private bool UpdatePlayerPosition () {
                userUpdateTimeoutId = 0;
-               player.Seek(slider.Value);
+               player.Seek((long)slider.Value);
                return false;
        }
 
@@ -165,7 +164,7 @@ public class VideoPosition {
        private void OnBaseTimingModeChanged (TimingMode timingMode) {
                UpdatePositionLabel(timingMode);
                UpdatePositionValueLabel(position);
-               TimeSpan length = player.Duration;
+               TimeSpan length = TimeSpan.FromMilliseconds(player.Duration);
                UpdateLengthLabel(timingMode, length);
        }
 
diff --git a/src/Makefile.am b/src/Makefile.am
index fd73c52..77e37e7 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -12,16 +12,16 @@ ASSEMBLY_CONFIG = $(ASSEMBLY).config
 ASSEMBLY_CONFIG_SRC = $(srcdir)/GnomeSubtitles/Execution/gnome-subtitles.exe.config
 
 AM_CFLAGS = $(gstreamer_CFLAGS) -Wall -g -fPIC
-gnomesubtitles_LTLIBRARIES = libgstreamer_playbin.la
-libgstreamer_playbin_la_SOURCES = External/GStreamerPlaybin/main.c
-libgstreamer_playbin_la_LIBADD = $(gstreamer_LIBS)
-libgstreamer_playbin_la_LDFLAGS = -module -avoid-version
-libgstreamer_playbin_la_LIBTOOLFLAGS = --tag=disable-static 
+gnomesubtitles_LTLIBRARIES = libgst_backend.la
+libgst_backend_la_SOURCES = External/GstBackend/gst-backend.c
+libgst_backend_la_LIBADD = $(gstreamer_LIBS)
+libgst_backend_la_LDFLAGS = -module -avoid-version
+libgst_backend_la_LIBTOOLFLAGS = --tag=disable-static 
 
 GSSOURCES = \
        $(srcdir)/External/Enchant/*.cs \
        $(srcdir)/External/GtkSpell/*.cs \
-       $(srcdir)/External/GStreamerPlaybin/*.cs \
+       $(srcdir)/External/GstBackend/*.cs \
        $(srcdir)/External/Interop/*.cs \
        $(srcdir)/External/NCharDet/*.cs \
        $(srcdir)/GnomeSubtitles/Core/*.cs \
@@ -59,7 +59,7 @@ $(ASSEMBLY): $(GSSOURCES) $(GS_RESOURCES)
 
 $(ASSEMBLY_CONFIG): $(gnomesubtitles_LTLIBRARIES)
        cp -f $(ASSEMBLY_CONFIG_SRC) $(GS_BUILDDIR)
-       cp -f $(srcdir)/.libs/libgstreamer_playbin.so $(GS_BUILDDIR)
+       cp -f $(srcdir)/.libs/libgst_backend.so $(GS_BUILDDIR)
 
 bin_SCRIPTS = $(srcdir)/GnomeSubtitles/Execution/gnome-subtitles
 


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