gtk: Add ffmpeg implementation of GtkMediaFile
authorBenjamin Otte <otte@redhat.com>
Tue, 27 Feb 2018 22:19:17 +0000 (23:19 +0100)
committerBenjamin Otte <otte@redhat.com>
Sun, 18 Mar 2018 20:01:23 +0000 (21:01 +0100)
This adds a module using ffmpeg to implement the GtkMediaFile interface.

config.h.meson
modules/media/gtkffmediafile.c [new file with mode: 0644]
modules/media/gtkffmediafileprivate.h [new file with mode: 0644]
modules/media/meson.build

index 54dec7fa82dca6112bb857a86e5b9afc7d9f2ce7..054831bbfa7fcccdc5a74f7be3541ab9cffa547b 100644 (file)
@@ -41,6 +41,9 @@
 /* Define to 1 if you have the <dlfcn.h> header file. */
 #mesondefine HAVE_DLFCN_H
 
+/* Have the ffmpeg library */
+#mesondefine HAVE_FFMPEG
+
 /* Define to 1 if you have the <ftw.h> header file. */
 #mesondefine HAVE_FTW_H
 
diff --git a/modules/media/gtkffmediafile.c b/modules/media/gtkffmediafile.c
new file mode 100644 (file)
index 0000000..46011e0
--- /dev/null
@@ -0,0 +1,756 @@
+/*
+ * Copyright © 2018 Benjamin Otte
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Benjamin Otte <otte@gnome.org>
+ */
+
+#include "config.h"
+
+#include "gtkffmediafileprivate.h"
+
+#include "gtkintl.h"
+
+#include "gdk/gdkmemorytextureprivate.h"
+
+#include <libavcodec/avcodec.h>
+#include <libavformat/avformat.h>
+#include <libavutil/pixdesc.h>
+#include <libswscale/swscale.h>
+
+typedef struct _GtkVideoFrameFFMpeg GtkVideoFrameFFMpeg;
+
+struct _GtkVideoFrameFFMpeg
+{
+  GdkTexture *texture;
+  gint64 timestamp;
+};
+
+struct _GtkFfMediaFile
+{
+  GtkMediaFile parent_instance;
+
+  GFile *file;
+  GInputStream *input_stream;
+
+  AVFormatContext *format_ctx;
+  AVCodecContext *codec_ctx;
+  int stream_id;
+  struct SwsContext *sws_ctx;
+  enum AVPixelFormat sws_pix_fmt;
+  GdkMemoryFormat memory_format;
+
+  GtkVideoFrameFFMpeg current_frame;
+  GtkVideoFrameFFMpeg next_frame;
+
+  gint64 start_time; /* monotonic time when we displayed the last frame */
+  guint next_frame_cb; /* Source ID of next frame callback */
+};
+
+struct _GtkFfMediaFileClass
+{
+  GtkMediaFileClass parent_class;
+};
+
+static void
+gtk_video_frame_ffmpeg_init (GtkVideoFrameFFMpeg *frame,
+                             GdkTexture          *texture,
+                             gint64               timestamp)
+{
+  frame->texture = texture;
+  frame->timestamp = timestamp;
+}
+
+static void
+gtk_video_frame_ffmpeg_clear (GtkVideoFrameFFMpeg *frame)
+{
+  g_clear_object (&frame->texture);
+  frame->timestamp = 0;
+}
+
+static gboolean
+gtk_video_frame_ffmpeg_is_empty (GtkVideoFrameFFMpeg *frame)
+{
+  return frame->texture == NULL;
+}
+
+static void
+gtk_video_frame_ffmpeg_move (GtkVideoFrameFFMpeg *dest,
+                             GtkVideoFrameFFMpeg *src)
+{
+  *dest = *src;
+  src->texture = NULL;
+  src->timestamp = 0;
+}
+
+static void
+gtk_ff_media_file_paintable_snapshot (GdkPaintable *paintable,
+                                      GdkSnapshot  *snapshot,
+                                      double        width,
+                                      double        height)
+{
+  GtkFfMediaFile *video = GTK_FF_MEDIA_FILE (paintable);
+
+  if (!gtk_video_frame_ffmpeg_is_empty (&video->current_frame))
+    {
+      gdk_paintable_snapshot (GDK_PAINTABLE (video->current_frame.texture), snapshot, width, height);
+    }
+}
+
+static GdkPaintable *
+gtk_ff_media_file_paintable_get_current_image (GdkPaintable *paintable)
+{
+  GtkFfMediaFile *video = GTK_FF_MEDIA_FILE (paintable);
+
+  return GDK_PAINTABLE (g_object_ref (video->current_frame.texture));
+}
+
+static int
+gtk_ff_media_file_paintable_get_intrinsic_width (GdkPaintable *paintable)
+{
+  GtkFfMediaFile *video = GTK_FF_MEDIA_FILE (paintable);
+
+  if (video->codec_ctx)
+    return video->codec_ctx->width;
+
+  return 0;
+}
+
+static int
+gtk_ff_media_file_paintable_get_intrinsic_height (GdkPaintable *paintable)
+{
+  GtkFfMediaFile *video = GTK_FF_MEDIA_FILE (paintable);
+
+  if (video->codec_ctx)
+    return video->codec_ctx->height;
+
+  return 0;
+}
+
+static double gtk_ff_media_file_paintable_get_intrinsic_aspect_ratio (GdkPaintable *paintable)
+{
+  GtkFfMediaFile *video = GTK_FF_MEDIA_FILE (paintable);
+
+  if (video->codec_ctx)
+    return (double) video->codec_ctx->width / video->codec_ctx->height;
+
+  return 0.0;
+};
+
+static void
+gtk_ff_media_file_paintable_init (GdkPaintableInterface *iface)
+{
+  iface->snapshot = gtk_ff_media_file_paintable_snapshot;
+  iface->get_current_image = gtk_ff_media_file_paintable_get_current_image;
+  iface->get_intrinsic_width = gtk_ff_media_file_paintable_get_intrinsic_width;
+  iface->get_intrinsic_height = gtk_ff_media_file_paintable_get_intrinsic_height;
+  iface->get_intrinsic_aspect_ratio = gtk_ff_media_file_paintable_get_intrinsic_aspect_ratio;
+}
+
+G_DEFINE_TYPE_EXTENDED (GtkFfMediaFile, gtk_ff_media_file, GTK_TYPE_MEDIA_FILE, 0,
+                        G_IMPLEMENT_INTERFACE (GDK_TYPE_PAINTABLE,
+                                               gtk_ff_media_file_paintable_init))
+
+void
+g_io_module_load (GIOModule *module)
+{
+  g_type_module_use (G_TYPE_MODULE (module));
+
+  av_register_all ();
+
+  g_io_extension_point_implement (GTK_MEDIA_FILE_EXTENSION_POINT_NAME,
+                                  GTK_TYPE_FF_MEDIA_FILE,
+                                  "ffmpeg",
+                                  0);
+}
+
+void
+g_io_module_unload (GIOModule *module)
+{
+  g_assert_not_reached ();
+}
+
+char **
+g_io_module_query (void)
+{
+  char *eps[] = {
+    GTK_MEDIA_FILE_EXTENSION_POINT_NAME,
+    NULL
+  };
+
+  return g_strdupv (eps);
+}
+
+static void
+gtk_ff_media_file_set_ffmpeg_error (GtkFfMediaFile *video,
+                                    int           av_errnum)
+{
+  char s[AV_ERROR_MAX_STRING_SIZE];
+
+  if (gtk_media_stream_get_error (GTK_MEDIA_STREAM (video)))
+    return;
+
+  if (av_strerror (av_errnum, s, sizeof (s) != 0))
+    snprintf (s, sizeof (s), _("Unspecified error decoding video"));
+
+  gtk_media_stream_error (GTK_MEDIA_STREAM (video),
+                          G_IO_ERROR,
+                          G_IO_ERROR_FAILED,
+                          "%s",
+                          s);
+}
+
+static int
+gtk_ff_media_file_read_packet_cb (void    *data,
+                                  uint8_t *buf,
+                                  int      buf_size)
+{
+  GtkFfMediaFile *video = data;
+  GError *error = NULL;
+  gssize n_read;
+
+  n_read = g_input_stream_read (video->input_stream,
+                                buf,
+                                buf_size,
+                                NULL,
+                                &error);
+  if (n_read < 0)
+    {
+      gtk_media_stream_gerror (GTK_MEDIA_STREAM (video), error);
+    }
+  else if (n_read == 0)
+    {
+      n_read = AVERROR_EOF;
+    }
+
+  return n_read;
+}
+
+static GdkMemoryFormat
+memory_format_from_pix_fmt (enum AVPixelFormat pix_fmt)
+{
+  switch ((int) pix_fmt)
+    {
+    case AV_PIX_FMT_RGBA:
+      return GDK_MEMORY_R8G8B8A8;
+    case AV_PIX_FMT_RGB24:
+      return GDK_MEMORY_R8G8B8;
+    default:
+      g_assert_not_reached ();
+      return GDK_MEMORY_R8G8B8A8;
+    }
+}
+
+static gboolean
+gtk_ff_media_file_decode_frame (GtkFfMediaFile      *video,
+                                GtkVideoFrameFFMpeg *result)
+{
+  GdkTexture *texture;
+  AVPacket packet;
+  AVFrame *frame;
+  int errnum;
+  GBytes *bytes;
+  guchar *data;
+
+  frame = av_frame_alloc ();
+
+  for (errnum = av_read_frame (video->format_ctx, &packet);
+       errnum >= 0;
+       errnum = av_read_frame (video->format_ctx, &packet))
+    {
+      if (packet.stream_index == video->stream_id)
+        {
+          errnum = avcodec_send_packet (video->codec_ctx, &packet);
+              if (errnum < 0)
+                G_BREAKPOINT();
+          if (errnum >= 0)
+            {
+              errnum = avcodec_receive_frame (video->codec_ctx, frame);
+              if (errnum < 0)
+                G_BREAKPOINT();
+              if (errnum >= 0)
+                {
+                  av_packet_unref (&packet);
+                  break;
+                }
+            }
+        }
+
+      av_packet_unref (&packet);
+    }
+
+  if (errnum < 0)
+    {
+      if (errnum != AVERROR_EOF)
+        gtk_ff_media_file_set_ffmpeg_error (video, errnum);
+      av_frame_free (&frame);
+      return FALSE;
+    }
+
+  data = g_try_malloc0 (video->codec_ctx->width * video->codec_ctx->height * 4);
+  if (data == NULL)
+    {
+      gtk_media_stream_error (GTK_MEDIA_STREAM (video),
+                              G_IO_ERROR,
+                              G_IO_ERROR_FAILED,
+                              _("Not enough memory"));
+      av_frame_free (&frame);
+      return FALSE;
+    }
+
+  if (video->sws_ctx == NULL ||
+      video->sws_pix_fmt != frame->format)
+    {
+      const AVPixFmtDescriptor *desc;
+      enum AVPixelFormat gdk_pix_fmt;
+
+      g_clear_pointer (&video->sws_ctx, sws_freeContext);
+      video->sws_pix_fmt = frame->format;
+      desc = av_pix_fmt_desc_get (video->sws_pix_fmt);
+      /* Use gdk-pixbuf formats because ffmpeg can't premultiply */
+      if (desc != NULL && (desc->flags & AV_PIX_FMT_FLAG_ALPHA))
+        gdk_pix_fmt = AV_PIX_FMT_RGBA;
+      else
+        gdk_pix_fmt = AV_PIX_FMT_RGB24;
+
+      video->sws_ctx = sws_getContext (video->codec_ctx->width,
+                                       video->codec_ctx->height,
+                                       frame->format,
+                                       video->codec_ctx->width,
+                                       video->codec_ctx->height,
+                                       gdk_pix_fmt,
+                                       0,
+                                       NULL,
+                                       NULL,
+                                       NULL);
+
+      video->memory_format = memory_format_from_pix_fmt (gdk_pix_fmt);
+    }
+
+  sws_scale(video->sws_ctx,
+            (const uint8_t * const *) frame->data, frame->linesize,
+            0, video->codec_ctx->height,
+            (uint8_t *[1]) { data }, (int[1]) { video->codec_ctx->width * 4 });
+
+  bytes = g_bytes_new_take (data, video->codec_ctx->width * video->codec_ctx->height * 4);
+  texture = gdk_memory_texture_new (video->codec_ctx->width,
+                                    video->codec_ctx->height,
+                                    video->memory_format,
+                                    bytes,
+                                    video->codec_ctx->width * 4);
+
+  g_bytes_unref (bytes);
+
+  gtk_video_frame_ffmpeg_init (result,
+                               texture,
+                               av_rescale_q (av_frame_get_best_effort_timestamp (frame),
+                                             video->format_ctx->streams[video->stream_id]->time_base,
+                                             (AVRational) { 1, G_USEC_PER_SEC }));
+
+  av_frame_free (&frame);
+
+  return TRUE;
+}
+
+static int64_t
+gtk_ff_media_file_seek_cb (void    *data,
+                           int64_t  offset,
+                           int      whence)
+{
+  GtkFfMediaFile *video = data;
+  GSeekType seek_type;
+  gboolean result;
+
+  switch (whence)
+    {
+    case SEEK_SET:
+      seek_type = G_SEEK_SET;
+      break;
+
+    case SEEK_CUR:
+      seek_type = G_SEEK_CUR;
+      break;
+
+    case SEEK_END:
+      seek_type = G_SEEK_END;
+      break;
+
+    case AVSEEK_SIZE:
+      /* FIXME: Handle size querying */
+      return -1;
+
+    default:
+      g_assert_not_reached ();
+      return -1;
+    }
+
+  result = g_seekable_seek (G_SEEKABLE (video->input_stream),
+                            offset,
+                            seek_type,
+                            NULL,
+                            NULL);
+  if (!result)
+    return -1;
+
+  return g_seekable_tell (G_SEEKABLE (video->input_stream));
+}
+
+static gboolean
+gtk_ff_media_file_create_input_stream (GtkFfMediaFile *video)
+{
+  GError *error = NULL;
+  GFile *file;
+
+  file = gtk_media_file_get_file (GTK_MEDIA_FILE (video));
+  if (file)
+    {
+      video->input_stream = G_INPUT_STREAM (g_file_read (file, NULL, &error));
+      if (video->input_stream == NULL)
+        {
+          gtk_media_stream_gerror (GTK_MEDIA_STREAM (video), error);
+          g_error_free (error);
+          return FALSE;
+        }
+    }
+  else
+    {
+      video->input_stream = g_object_ref (gtk_media_file_get_input_stream (GTK_MEDIA_FILE (video)));
+    }
+
+  return TRUE;
+}
+
+static AVIOContext *
+gtk_ff_media_file_create_io_context (GtkFfMediaFile *video)
+{
+  AVIOContext *result;
+  int buffer_size = 4096; /* it's what everybody else uses... */
+  unsigned char *buffer;
+
+  if (!gtk_ff_media_file_create_input_stream (video))
+    return NULL;
+
+  buffer = av_malloc (buffer_size);
+  if (buffer == NULL)
+    return NULL;
+
+  result = avio_alloc_context (buffer,
+                               buffer_size,
+                               AVIO_FLAG_READ,
+                               video,
+                               gtk_ff_media_file_read_packet_cb,
+                               NULL,
+                               G_IS_SEEKABLE (video->input_stream)
+                               ? gtk_ff_media_file_seek_cb
+                               : NULL);
+
+  result->buf_ptr = result->buf_end;
+  result->write_flag = 0;
+
+  return result;
+}
+
+static gboolean gtk_ff_media_file_play (GtkMediaStream *stream);
+
+static void
+gtk_ff_media_file_open (GtkMediaFile *file)
+{
+  GtkFfMediaFile *video = GTK_FF_MEDIA_FILE (file);
+  AVStream *stream;
+  AVCodec *codec;
+  int errnum;
+
+  video->format_ctx = avformat_alloc_context ();
+  video->format_ctx->pb = gtk_ff_media_file_create_io_context (video);
+  if (video->format_ctx->pb == NULL)
+    {
+      gtk_media_stream_error (GTK_MEDIA_STREAM (video),
+                              G_IO_ERROR,
+                              G_IO_ERROR_FAILED,
+                              _("Not enough memory"));
+      return;
+    }
+  errnum = avformat_open_input (&video->format_ctx, NULL, NULL, NULL);
+  if (errnum != 0)
+    {
+      gtk_ff_media_file_set_ffmpeg_error (video, errnum);
+      return;
+    }
+
+  errnum = avformat_find_stream_info (video->format_ctx, NULL);
+  if (errnum < 0)
+    {
+      gtk_ff_media_file_set_ffmpeg_error (video, errnum);
+      return;
+    }
+
+  video->stream_id = av_find_best_stream (video->format_ctx, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0);
+  if (video->stream_id < 0)
+    {
+      gtk_media_stream_error (GTK_MEDIA_STREAM (video),
+                              G_IO_ERROR,
+                              G_IO_ERROR_INVALID_DATA,
+                              _("Not a video file"));
+      return;
+    }
+
+  stream = video->format_ctx->streams[video->stream_id];
+  /* alpha transparency requires the libvpx codecs, not the ffmpeg builtin ones */
+  if (stream->codecpar->codec_id == AV_CODEC_ID_VP8)
+    codec = avcodec_find_decoder_by_name ("libvpx");
+  else if (stream->codecpar->codec_id == AV_CODEC_ID_VP9)
+    codec = avcodec_find_decoder_by_name ("libvpx-vp9");
+  else
+    codec = NULL;
+  if (codec == NULL)
+    codec = avcodec_find_decoder (stream->codecpar->codec_id);
+  if (codec == NULL)
+    {
+      gtk_media_stream_error (GTK_MEDIA_STREAM (video),
+                              G_IO_ERROR,
+                              G_IO_ERROR_NOT_SUPPORTED,
+                              _("Unsupported video codec"));
+      return;
+    }
+
+  video->codec_ctx = avcodec_alloc_context3 (codec);
+  errnum = avcodec_parameters_to_context (video->codec_ctx, stream->codecpar);
+  if (errnum < 0)
+    {
+      gtk_ff_media_file_set_ffmpeg_error (video, errnum);
+      return;
+    }
+  errnum = avcodec_open2 (video->codec_ctx, codec, &stream->metadata);
+  if (errnum < 0)
+    {
+      gtk_ff_media_file_set_ffmpeg_error (video, errnum);
+      return;
+    }
+
+  gtk_media_stream_prepared (GTK_MEDIA_STREAM (video),
+                             FALSE,
+                             video->codec_ctx != NULL,
+                             TRUE,
+                             video->format_ctx->duration != AV_NOPTS_VALUE
+                             ? av_rescale (video->format_ctx->duration, G_USEC_PER_SEC, AV_TIME_BASE)
+                             : 0);
+
+  gdk_paintable_invalidate_size (GDK_PAINTABLE (video));
+
+  if (gtk_ff_media_file_decode_frame (video, &video->current_frame))
+    gdk_paintable_invalidate_contents (GDK_PAINTABLE (video));
+
+  if (gtk_media_stream_get_playing (GTK_MEDIA_STREAM (video)))
+    gtk_ff_media_file_play (GTK_MEDIA_STREAM (video));
+}
+
+static void
+gtk_ff_media_file_close (GtkMediaFile *file)
+{
+  GtkFfMediaFile *video = GTK_FF_MEDIA_FILE (file);
+
+  g_clear_object (&video->input_stream);
+
+  g_clear_pointer (&video->sws_ctx, sws_freeContext);
+  g_clear_pointer (&video->codec_ctx, avcodec_close);
+  avformat_close_input (&video->format_ctx);
+  video->stream_id = -1;
+  gtk_video_frame_ffmpeg_clear (&video->next_frame);
+  gtk_video_frame_ffmpeg_clear (&video->current_frame);
+
+  gdk_paintable_invalidate_size (GDK_PAINTABLE (video));
+  gdk_paintable_invalidate_contents (GDK_PAINTABLE (video));
+}
+
+static gboolean
+gtk_ff_media_file_next_frame_cb (gpointer data);
+static void
+gtk_ff_media_file_queue_frame (GtkFfMediaFile *video)
+{
+  gint64 time, frame_time;
+  guint delay;
+
+  time = g_get_monotonic_time ();
+  frame_time = video->start_time + video->next_frame.timestamp;
+  delay = time > frame_time ? 0 : (frame_time - time) / 1000;
+
+  video->next_frame_cb = g_timeout_add (delay, gtk_ff_media_file_next_frame_cb, video);
+}
+
+static gboolean
+gtk_ff_media_file_restart (GtkFfMediaFile *video)
+{
+  if (av_seek_frame (video->format_ctx,
+                     video->stream_id,
+                     av_rescale_q (0,
+                                   (AVRational) { 1, G_USEC_PER_SEC },
+                                   video->format_ctx->streams[video->stream_id]->time_base),
+                     AVSEEK_FLAG_BACKWARD) < 0)
+    return FALSE;
+
+  if (!gtk_ff_media_file_decode_frame (video, &video->next_frame))
+    return FALSE;
+
+  return TRUE;
+}
+
+static gboolean
+gtk_ff_media_file_next_frame_cb (gpointer data)
+{
+  GtkFfMediaFile *video = data;
+
+  video->next_frame_cb = 0;
+
+  if (gtk_video_frame_ffmpeg_is_empty (&video->next_frame))
+    {
+      if (!gtk_media_stream_get_loop (GTK_MEDIA_STREAM (video)) ||
+          !gtk_ff_media_file_restart (video))
+        {
+          gtk_media_stream_ended (GTK_MEDIA_STREAM (video));
+          return G_SOURCE_REMOVE;
+        }
+
+      video->start_time += video->current_frame.timestamp - video->next_frame.timestamp;
+    }
+
+  gtk_video_frame_ffmpeg_clear (&video->current_frame);
+  gtk_video_frame_ffmpeg_move (&video->current_frame,
+                               &video->next_frame);
+
+  gtk_media_stream_update (GTK_MEDIA_STREAM (video),
+                           video->current_frame.timestamp);
+  gdk_paintable_invalidate_contents (GDK_PAINTABLE (video));
+
+  /* ignore failure here, we'll handle the empty frame case above
+   * the next time we're called. */
+  gtk_ff_media_file_decode_frame (video, &video->next_frame);
+  gtk_ff_media_file_queue_frame (video);
+
+  return G_SOURCE_REMOVE;
+}
+
+static gboolean
+gtk_ff_media_file_play (GtkMediaStream *stream)
+{
+  GtkFfMediaFile *video = GTK_FF_MEDIA_FILE (stream);
+
+  if (!gtk_media_stream_is_prepared (stream))
+    return TRUE;
+
+  if (gtk_video_frame_ffmpeg_is_empty (&video->next_frame) &&
+      !gtk_ff_media_file_decode_frame (video, &video->next_frame))
+    {
+      if (gtk_ff_media_file_restart (video))
+        {
+          video->start_time = g_get_monotonic_time () - video->next_frame.timestamp;
+        }
+      else
+        {
+          return FALSE;
+        }
+    }
+  else
+    {
+      video->start_time = g_get_monotonic_time () - video->current_frame.timestamp;
+    }
+
+  gtk_ff_media_file_queue_frame (video);
+
+  return TRUE;
+}
+
+static void
+gtk_ff_media_file_pause (GtkMediaStream *stream)
+{
+  GtkFfMediaFile *video = GTK_FF_MEDIA_FILE (stream);
+
+  if (video->next_frame_cb)
+    {
+      g_source_remove (video->next_frame_cb);
+      video->next_frame_cb = 0;
+    }
+
+  video->start_time = 0;
+}
+
+static void
+gtk_ff_media_file_seek (GtkMediaStream *stream,
+                        gint64          timestamp)
+{
+  GtkFfMediaFile *video = GTK_FF_MEDIA_FILE (stream);
+  int errnum;
+
+  errnum = av_seek_frame (video->format_ctx,
+                          video->stream_id,
+                          av_rescale_q (timestamp,
+                                        (AVRational) { 1, G_USEC_PER_SEC },
+                                        video->format_ctx->streams[video->stream_id]->time_base),
+                                        AVSEEK_FLAG_BACKWARD);
+  if (errnum < 0)
+    {
+      gtk_media_stream_seek_failed (stream);
+      return;
+    }
+
+  gtk_media_stream_seek_success (stream);
+
+  gtk_video_frame_ffmpeg_clear (&video->next_frame);
+  gtk_video_frame_ffmpeg_clear (&video->current_frame);
+  if (gtk_ff_media_file_decode_frame (video, &video->current_frame))
+    gtk_media_stream_update (stream, video->current_frame.timestamp);
+  gdk_paintable_invalidate_contents (GDK_PAINTABLE (video));
+
+  if (gtk_media_stream_get_playing (stream))
+    {
+      gtk_ff_media_file_pause (stream);
+      if (!gtk_ff_media_file_play (stream))
+        gtk_media_stream_ended (stream);
+    }
+}
+
+static void
+gtk_ff_media_file_dispose (GObject *object)
+{
+  GtkFfMediaFile *video = GTK_FF_MEDIA_FILE (object);
+
+  gtk_ff_media_file_pause (GTK_MEDIA_STREAM (video));
+  gtk_ff_media_file_close (GTK_MEDIA_FILE (video));
+
+  G_OBJECT_CLASS (gtk_ff_media_file_parent_class)->dispose (object);
+}
+
+static void
+gtk_ff_media_file_class_init (GtkFfMediaFileClass *klass)
+{
+  GtkMediaFileClass *file_class = GTK_MEDIA_FILE_CLASS (klass);
+  GtkMediaStreamClass *stream_class = GTK_MEDIA_STREAM_CLASS (klass);
+  GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+
+  file_class->open = gtk_ff_media_file_open;
+  file_class->close = gtk_ff_media_file_close;
+  stream_class->play = gtk_ff_media_file_play;
+  stream_class->pause = gtk_ff_media_file_pause;
+  stream_class->seek = gtk_ff_media_file_seek;
+
+  gobject_class->dispose = gtk_ff_media_file_dispose;
+}
+
+static void
+gtk_ff_media_file_init (GtkFfMediaFile *video)
+{
+  video->stream_id = -1;
+}
+
+
diff --git a/modules/media/gtkffmediafileprivate.h b/modules/media/gtkffmediafileprivate.h
new file mode 100644 (file)
index 0000000..c8366ef
--- /dev/null
@@ -0,0 +1,33 @@
+/*
+ * Copyright © 2018 Benjamin Otte
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Benjamin Otte <otte@gnome.org>
+ */
+
+#ifndef __GTK_FF_MEDIA_FILE_H__
+#define __GTK_FF_MEDIA_FILE_H__
+
+#include <gtk/gtkmediafile.h>
+
+G_BEGIN_DECLS
+
+#define GTK_TYPE_FF_MEDIA_FILE (gtk_ff_media_file_get_type ())
+
+G_DECLARE_FINAL_TYPE (GtkFfMediaFile, gtk_ff_media_file, GTK, FF_MEDIA_FILE, GtkMediaFile)
+
+G_END_DECLS
+
+#endif /* __GTK_FF_MEDIA_FILE_H__ */
index efab5a3b3eaf73f119e5be6ce6c26b63300c7eb5..3ca8a8adacc041b0cac7277cd5a90e64f2f276b0 100644 (file)
@@ -1,4 +1,5 @@
 all_media_backends = [
+  'ffmpeg'
 ]
 
 enabled_media_backends = get_option('media').split(',')
@@ -21,3 +22,22 @@ endif
 media_subdir = 'gtk-4.0/@0@/media'.format(gtk_binary_version)
 media_install_dir = join_paths(get_option('libdir'), media_subdir)
 
+if media_backends.contains('ffmpeg')
+  libavfilter_dep = dependency('libavfilter', version: '>= 6.47.100', required: true)
+  libavformat_dep = dependency('libavformat', version: '>= 57.41.100', required: true)
+  libavcodec_dep = dependency('libavcodec', version: '>= 57.48.101', required: true)
+  libavutil_dep = dependency('libavutil', version: '>= 55.28.100', required: true)
+  libswscale_dep = dependency('libswscale', version: '>= 4.6.100', required: true)
+  ffmpeg_deps = [libavfilter_dep, libavformat_dep, libavcodec_dep, libavutil_dep, libswscale_dep]
+  cdata.set('HAVE_FFMPEG', 1)
+
+  shared_module('media-ffmpeg',
+                'gtkffmediafile.c',
+                c_args: [
+                  '-DGTK_COMPILATION'
+                ],
+                dependencies: [ libgtk_dep, ffmpeg_deps ],
+                install_dir: media_install_dir,
+                install : true)
+endif
+