diff --git a/demos/gtk-demo/demo.gresource.xml b/demos/gtk-demo/demo.gresource.xml index 81519f1c37..ccc17fe9ad 100644 --- a/demos/gtk-demo/demo.gresource.xml +++ b/demos/gtk-demo/demo.gresource.xml @@ -184,6 +184,7 @@ pagesetup.c paintable.c paintable_animated.c + paintable_mediastream.c panes.c pickers.c pixbufs.c diff --git a/demos/gtk-demo/meson.build b/demos/gtk-demo/meson.build index 5d3a318bd1..0ad157bf8d 100644 --- a/demos/gtk-demo/meson.build +++ b/demos/gtk-demo/meson.build @@ -47,6 +47,7 @@ demos = files([ 'overlay2.c', 'paintable.c', 'paintable_animated.c', + 'paintable_mediastream.c', 'panes.c', 'pickers.c', 'pixbufs.c', diff --git a/demos/gtk-demo/paintable.h b/demos/gtk-demo/paintable.h index 0f561b2713..33bc4dbc8b 100644 --- a/demos/gtk-demo/paintable.h +++ b/demos/gtk-demo/paintable.h @@ -10,5 +10,6 @@ void gtk_nuclear_snapshot (GtkSnapshot *snapshot, GdkPaintable * gtk_nuclear_icon_new (double rotation); GdkPaintable * gtk_nuclear_animation_new (void); +GtkMediaStream *gtk_nuclear_media_stream_new (void); #endif /* __PAINTABLE_H__ */ diff --git a/demos/gtk-demo/paintable_mediastream.c b/demos/gtk-demo/paintable_mediastream.c new file mode 100644 index 0000000000..b06cf11565 --- /dev/null +++ b/demos/gtk-demo/paintable_mediastream.c @@ -0,0 +1,312 @@ +/* Paintable/A media stream + * + * GdkPaintable is also used by the GtkMediaStream class. + * + * This demo code turns the nuclear media_stream into the object + * GTK uses for videos. This allows treating the icon like a + * regular video, so we can for example attach controls to it. + * + * After all, what good is an media_stream if one cannot pause + * it. + */ + +#include + +#include "paintable.h" + +static GtkWidget *window = NULL; + +/* First, add the boilerplate for the object itself. + * This part would normally go in the header. + */ +#define GTK_TYPE_NUCLEAR_MEDIA_STREAM (gtk_nuclear_media_stream_get_type ()) +G_DECLARE_FINAL_TYPE (GtkNuclearMediaStream, gtk_nuclear_media_stream, GTK, NUCLEAR_MEDIA_STREAM, GtkMediaStream) + +/* Do a full rotation in 5 seconds. + * + * We do not save steps here but real timestamps. + * GtkMediaStream uses microseconds, so we will do so, too. + */ +#define DURATION (5 * G_USEC_PER_SEC) + +/* Declare the struct. */ +struct _GtkNuclearMediaStream +{ + /* We now inherit from the media stream object. */ + GtkMediaStream parent_instance; + + /* This variable stores the progress of our video. + */ + gint64 progress; + + /* This variable stores the timestamp of the last + * time we updated the progress variable when the + * video is currently playing. + * This is so that we can always accurately compute the + * progress we've had, even if the timeout does not + * exactly work. + */ + gint64 last_time; + + /* This variable again holds the ID of the timer that + * updates our progress variable. Nothing changes about + * how this works compared to the previous example. + */ + guint source_id; +}; + +struct _GtkNuclearMediaStreamClass +{ + GObjectClass parent_class; +}; + +/* GtkMediaStream is a GdkPaintable. So when we want to display video, + * we have to implement the interface, just like in the animation example. + */ +static void +gtk_nuclear_media_stream_snapshot (GdkPaintable *paintable, + GdkSnapshot *snapshot, + double width, + double height) +{ + GtkNuclearMediaStream *nuclear = GTK_NUCLEAR_MEDIA_STREAM (paintable); + + /* We call the function from the previous example here. */ + gtk_nuclear_snapshot (snapshot, + width, height, + 2 * G_PI * nuclear->progress / DURATION); +} + +static GdkPaintable * +gtk_nuclear_media_stream_get_current_image (GdkPaintable *paintable) +{ + GtkNuclearMediaStream *nuclear = GTK_NUCLEAR_MEDIA_STREAM (paintable); + + /* Same thing as with the animation */ + return gtk_nuclear_icon_new (2 * G_PI * nuclear->progress / DURATION); +} + +static GdkPaintableFlags +gtk_nuclear_media_stream_get_flags (GdkPaintable *paintable) +{ + /* And same thing as with the animation over here, too. */ + return GDK_PAINTABLE_STATIC_SIZE; +} + +static void +gtk_nuclear_media_stream_paintable_init (GdkPaintableInterface *iface) +{ + iface->snapshot = gtk_nuclear_media_stream_snapshot; + iface->get_current_image = gtk_nuclear_media_stream_get_current_image; + iface->get_flags = gtk_nuclear_media_stream_get_flags; +} + +/* This time, we inherit from GTK_TYPE_MEDIA_STREAM */ +G_DEFINE_TYPE_WITH_CODE (GtkNuclearMediaStream, gtk_nuclear_media_stream, GTK_TYPE_MEDIA_STREAM, + G_IMPLEMENT_INTERFACE (GDK_TYPE_PAINTABLE, + gtk_nuclear_media_stream_paintable_init)) + +static gboolean +gtk_nuclear_media_stream_step (gpointer data) +{ + GtkNuclearMediaStream *nuclear = data; + gint64 current_time; + + /* Compute the time that has elapsed since the last time we were called + * and add it to our current progress. + */ + current_time = g_source_get_time (g_main_current_source ()); + nuclear->progress += current_time - nuclear->last_time; + + /* Check if we've ended */ + if (nuclear->progress > DURATION) + { + if (gtk_media_stream_get_loop (GTK_MEDIA_STREAM (nuclear))) + { + /* We're looping. So make the progress loop using modulo */ + nuclear->progress %= DURATION; + } + else + { + /* Just make sure we don't overflow */ + nuclear->progress = DURATION; + } + } + + /* Update the last time to the current timestamp. */ + nuclear->last_time = current_time; + + /* Update the timestamp of the media stream */ + gtk_media_stream_update (GTK_MEDIA_STREAM (nuclear), nuclear->progress); + + /* We also need to invalidate our contents again. + * After all, we are a video and not just an audio stream. + */ + gdk_paintable_invalidate_contents (GDK_PAINTABLE (nuclear)); + + /* Now check if we have finished playing and if so, + * tell the media stream. The media stream will then + * call our pause function to pause the stream. + */ + if (nuclear->progress >= DURATION) + gtk_media_stream_ended (GTK_MEDIA_STREAM (nuclear)); + + /* The timeout function is removed by the pause function, + * so we can just always return this value. + */ + return G_SOURCE_CONTINUE; +} + +static gboolean +gtk_nuclear_media_stream_play (GtkMediaStream *stream) +{ + GtkNuclearMediaStream *nuclear = GTK_NUCLEAR_MEDIA_STREAM (stream); + + /* If we're already at the end of the stream, we don't want + * to start playing and exit early. + */ + if (nuclear->progress >= DURATION) + return FALSE; + + /* This time, we add the source only when we start playing. + */ + nuclear->source_id = g_timeout_add (10, + gtk_nuclear_media_stream_step, + nuclear); + + /* We also want to initialize our time, so that we can + * do accurate updates. + */ + nuclear->last_time = g_get_monotonic_time (); + + /* We successfully started playing, so we return TRUE here. */ + return TRUE; +} + +static void +gtk_nuclear_media_stream_pause (GtkMediaStream *stream) +{ + GtkNuclearMediaStream *nuclear = GTK_NUCLEAR_MEDIA_STREAM (stream); + + /* This function will be called when a playing stream + * gets paused. + * So we remove the updating source here and set it + * back to 0 so that the finalize function doesn't try + * to remove it again. + */ + g_source_remove (nuclear->source_id); + nuclear->source_id = 0; + nuclear->last_time = 0; +} + +static void +gtk_nuclear_media_stream_seek (GtkMediaStream *stream, + gint64 timestamp) +{ + GtkNuclearMediaStream *nuclear = GTK_NUCLEAR_MEDIA_STREAM (stream); + + /* This is optional functionality for media streams, + * but not being able to seek is kinda boring. + * And it's trivial to implement, so let's go for it. + */ + nuclear->progress = timestamp; + + /* Media streams are asynchronous, so seeking can take a while. + * We however don't need that functionality, so we can just + * report success. + */ + gtk_media_stream_seek_success (stream); + + /* We also have to update our timestamp and tell the + * paintable interface abbout the seek + */ + gtk_media_stream_update (stream, nuclear->progress); + gdk_paintable_invalidate_contents (GDK_PAINTABLE (nuclear)); +} + +/* Again, we need to implement the finalize function. + */ +static void +gtk_nuclear_media_stream_finalize (GObject *object) +{ + GtkNuclearMediaStream *nuclear = GTK_NUCLEAR_MEDIA_STREAM (object); + + /* This time, we need to check if the source exists before + * removing it as it only exists while we are playing. + */ + if (nuclear->source_id > 0) + g_source_remove (nuclear->source_id); + + /* Don't forget to chain up to the parent class' implementation + * of the finalize function. + */ + G_OBJECT_CLASS (gtk_nuclear_media_stream_parent_class)->finalize (object); +} + +/* In the class declaration, we need to implement the media stream */ +static void +gtk_nuclear_media_stream_class_init (GtkNuclearMediaStreamClass *klass) +{ + GtkMediaStreamClass *stream_class = GTK_MEDIA_STREAM_CLASS (klass); + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + + stream_class->play = gtk_nuclear_media_stream_play; + stream_class->pause = gtk_nuclear_media_stream_pause; + stream_class->seek = gtk_nuclear_media_stream_seek; + + gobject_class->finalize = gtk_nuclear_media_stream_finalize; +} + +static void +gtk_nuclear_media_stream_init (GtkNuclearMediaStream *nuclear) +{ + /* This time, we don't have to add a timer here, because media + * streams start paused. + * + * However, media streams need to tell GTK once they are intialized, + * so we do that here. + */ + gtk_media_stream_prepared (GTK_MEDIA_STREAM (nuclear), + FALSE, + TRUE, + TRUE, + DURATION); +} + +/* And finally, we add the simple constructor we declared in the header. */ +GtkMediaStream * +gtk_nuclear_media_stream_new (void) +{ + return g_object_new (GTK_TYPE_NUCLEAR_MEDIA_STREAM, NULL); +} + +GtkWidget * +do_paintable_mediastream (GtkWidget *do_widget) +{ + GtkMediaStream *nuclear; + GtkWidget *video; + + if (!window) + { + window = gtk_window_new (GTK_WINDOW_TOPLEVEL); + gtk_window_set_display (GTK_WINDOW (window), + gtk_widget_get_display (do_widget)); + gtk_window_set_title (GTK_WINDOW (window), "Nuclear MediaStream"); + gtk_window_set_default_size (GTK_WINDOW (window), 300, 200); + + nuclear = gtk_nuclear_media_stream_new (); + gtk_media_stream_set_loop (GTK_MEDIA_STREAM (nuclear), TRUE); + + video = gtk_video_new_for_media_stream (nuclear); + gtk_container_add (GTK_CONTAINER (window), video); + + g_object_unref (nuclear); + } + + if (!gtk_widget_get_visible (window)) + gtk_widget_show (window); + else + gtk_widget_destroy (window); + + return window; +}