summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--gtk/meson.build5
-rw-r--r--modules/media/gtkffmediafile.c756
-rw-r--r--modules/media/gtkffmediafileprivate.h33
-rw-r--r--modules/media/meson.build11
4 files changed, 805 insertions, 0 deletions
diff --git a/gtk/meson.build b/gtk/meson.build
index f62afe4c3d..a8731213b2 100644
--- a/gtk/meson.build
+++ b/gtk/meson.build
@@ -867,6 +867,11 @@ if harfbuzz_dep.found() and pangoft_dep.found()
gtk_deps += [ harfbuzz_dep, ]
endif
+if ffmpeg_enabled
+ gtk_sources += [ 'gtkffmediafile.c' ]
+ gtk_deps += [ ffmpeg_deps, ]
+endif
+
if x11_enabled
x11_data_prefix = dependency('x11').get_pkgconfig_variable('prefix')
diff --git a/modules/media/gtkffmediafile.c b/modules/media/gtkffmediafile.c
new file mode 100644
index 0000000000..46011e00d1
--- /dev/null
+++ b/modules/media/gtkffmediafile.c
@@ -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
index 0000000000..c8366eff2f
--- /dev/null
+++ b/modules/media/gtkffmediafileprivate.h
@@ -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__ */
diff --git a/modules/media/meson.build b/modules/media/meson.build
index d337ca6e15..bfa63fb6d7 100644
--- a/modules/media/meson.build
+++ b/modules/media/meson.build
@@ -1,3 +1,14 @@
media_subdir = 'gtk-4.0/@0@/media'.format(gtk_binary_version)
media_install_dir = join_paths(get_option('libdir'), media_subdir)
+if ffmpeg_enabled
+ shared_module('media-ffmpeg',
+ 'gtkffmediafile.c',
+ c_args: [
+ '-DGTK_COMPILATION'
+ ],
+ dependencies: [ libgtk_dep, ffmpeg_deps ],
+ install_dir: media_install_dir,
+ install : true)
+endif
+