2018-03-01 03:52:27 +00:00
|
|
|
/*
|
|
|
|
* 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 "gtkvideo.h"
|
|
|
|
|
2019-09-21 12:27:43 +00:00
|
|
|
#include "gtkbinlayout.h"
|
2018-03-01 03:52:27 +00:00
|
|
|
#include "gtkeventcontrollermotion.h"
|
|
|
|
#include "gtkimage.h"
|
|
|
|
#include "gtkintl.h"
|
|
|
|
#include "gtkmediacontrols.h"
|
|
|
|
#include "gtkmediafile.h"
|
2019-05-20 00:38:08 +00:00
|
|
|
#include "gtknative.h"
|
2018-06-09 20:54:04 +00:00
|
|
|
#include "gtkpicture.h"
|
2018-03-01 03:52:27 +00:00
|
|
|
#include "gtkrevealer.h"
|
2020-04-08 11:43:28 +00:00
|
|
|
#include "gtkwidgetprivate.h"
|
2018-03-01 03:52:27 +00:00
|
|
|
|
|
|
|
/**
|
2021-02-25 02:58:45 +00:00
|
|
|
* GtkVideo:
|
2018-03-01 03:52:27 +00:00
|
|
|
*
|
2021-02-20 04:33:24 +00:00
|
|
|
* `GtkVideo` is a widget to show a `GtkMediaStream` with media controls.
|
2018-05-06 19:37:13 +00:00
|
|
|
*
|
2021-02-20 04:33:24 +00:00
|
|
|
* ![An example GtkVideo](video.png)
|
|
|
|
*
|
2021-02-25 02:58:45 +00:00
|
|
|
* The controls are available separately as [class@Gtk.MediaControls].
|
2021-02-20 04:33:24 +00:00
|
|
|
* If you just want to display a video without controls, you can treat it
|
|
|
|
* like any other paintable and for example put it into a [class@Gtk.Picture].
|
|
|
|
*
|
|
|
|
* `GtkVideo` aims to cover use cases such as previews, embedded animations,
|
2021-01-02 16:17:26 +00:00
|
|
|
* etc. It supports autoplay, looping, and simple media controls. It does
|
|
|
|
* not have support for video overlays, multichannel audio, device
|
|
|
|
* selection, or input. If you are writing a full-fledged video player,
|
2021-02-20 04:33:24 +00:00
|
|
|
* you may want to use the [class@Gdk.Paintable] API and a media framework
|
|
|
|
* such as Gstreamer directly.
|
2018-03-01 03:52:27 +00:00
|
|
|
*/
|
|
|
|
|
|
|
|
struct _GtkVideo
|
|
|
|
{
|
|
|
|
GtkWidget parent_instance;
|
|
|
|
|
|
|
|
GFile *file;
|
|
|
|
GtkMediaStream *media_stream;
|
|
|
|
|
|
|
|
GtkWidget *box;
|
2018-06-09 20:54:04 +00:00
|
|
|
GtkWidget *video_picture;
|
2018-03-01 03:52:27 +00:00
|
|
|
GtkWidget *overlay_icon;
|
|
|
|
GtkWidget *controls_revealer;
|
|
|
|
GtkWidget *controls;
|
|
|
|
guint controls_hide_source;
|
2018-04-05 22:41:44 +00:00
|
|
|
|
|
|
|
guint autoplay : 1;
|
|
|
|
guint loop : 1;
|
2020-04-28 19:11:26 +00:00
|
|
|
guint grabbed : 1;
|
2018-03-01 03:52:27 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
enum
|
|
|
|
{
|
|
|
|
PROP_0,
|
2018-04-05 22:41:44 +00:00
|
|
|
PROP_AUTOPLAY,
|
2018-03-01 03:52:27 +00:00
|
|
|
PROP_FILE,
|
2018-04-05 22:41:44 +00:00
|
|
|
PROP_LOOP,
|
2018-03-01 03:52:27 +00:00
|
|
|
PROP_MEDIA_STREAM,
|
|
|
|
|
|
|
|
N_PROPS
|
|
|
|
};
|
|
|
|
|
|
|
|
G_DEFINE_TYPE (GtkVideo, gtk_video, GTK_TYPE_WIDGET)
|
|
|
|
|
|
|
|
static GParamSpec *properties[N_PROPS] = { NULL, };
|
|
|
|
|
2018-03-10 17:45:23 +00:00
|
|
|
static gboolean
|
|
|
|
gtk_video_hide_controls (gpointer data)
|
|
|
|
{
|
|
|
|
GtkVideo *self = data;
|
|
|
|
|
2020-04-28 19:11:26 +00:00
|
|
|
if (self->grabbed)
|
|
|
|
return G_SOURCE_CONTINUE;
|
|
|
|
|
2018-03-10 17:45:23 +00:00
|
|
|
gtk_revealer_set_reveal_child (GTK_REVEALER (self->controls_revealer), FALSE);
|
|
|
|
|
|
|
|
self->controls_hide_source = 0;
|
|
|
|
|
|
|
|
return G_SOURCE_REMOVE;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
2021-06-08 10:58:44 +00:00
|
|
|
gtk_video_reveal_controls (GtkVideo *self)
|
2018-03-10 17:45:23 +00:00
|
|
|
{
|
|
|
|
gtk_revealer_set_reveal_child (GTK_REVEALER (self->controls_revealer), TRUE);
|
|
|
|
if (self->controls_hide_source)
|
|
|
|
g_source_remove (self->controls_hide_source);
|
|
|
|
self->controls_hide_source = g_timeout_add (5 * 1000,
|
|
|
|
gtk_video_hide_controls,
|
|
|
|
self);
|
|
|
|
}
|
|
|
|
|
2021-06-08 10:58:44 +00:00
|
|
|
static void
|
|
|
|
gtk_video_motion (GtkEventControllerMotion *motion,
|
|
|
|
double x,
|
|
|
|
double y,
|
|
|
|
GtkVideo *self)
|
|
|
|
{
|
|
|
|
gtk_video_reveal_controls (self);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
gtk_video_pressed (GtkVideo *self)
|
|
|
|
{
|
|
|
|
gtk_video_reveal_controls (self);
|
|
|
|
}
|
|
|
|
|
2018-03-18 02:20:02 +00:00
|
|
|
static void
|
|
|
|
gtk_video_realize (GtkWidget *widget)
|
|
|
|
{
|
|
|
|
GtkVideo *self = GTK_VIDEO (widget);
|
|
|
|
|
|
|
|
GTK_WIDGET_CLASS (gtk_video_parent_class)->realize (widget);
|
|
|
|
|
|
|
|
if (self->media_stream)
|
2019-05-20 00:38:08 +00:00
|
|
|
{
|
|
|
|
GdkSurface *surface;
|
|
|
|
|
|
|
|
surface = gtk_native_get_surface (gtk_widget_get_native (widget));
|
|
|
|
gtk_media_stream_realize (self->media_stream, surface);
|
|
|
|
}
|
2018-03-18 02:20:02 +00:00
|
|
|
|
|
|
|
if (self->file)
|
|
|
|
gtk_media_file_set_file (GTK_MEDIA_FILE (self->media_stream), self->file);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
gtk_video_unrealize (GtkWidget *widget)
|
|
|
|
{
|
|
|
|
GtkVideo *self = GTK_VIDEO (widget);
|
|
|
|
|
2021-01-01 20:07:25 +00:00
|
|
|
if (self->autoplay && self->media_stream)
|
|
|
|
gtk_media_stream_pause (self->media_stream);
|
|
|
|
|
2018-03-18 02:20:02 +00:00
|
|
|
if (self->media_stream)
|
2019-05-20 00:38:08 +00:00
|
|
|
{
|
|
|
|
GdkSurface *surface;
|
|
|
|
|
|
|
|
surface = gtk_native_get_surface (gtk_widget_get_native (widget));
|
|
|
|
gtk_media_stream_unrealize (self->media_stream, surface);
|
|
|
|
}
|
2018-03-18 02:20:02 +00:00
|
|
|
|
|
|
|
GTK_WIDGET_CLASS (gtk_video_parent_class)->unrealize (widget);
|
|
|
|
}
|
|
|
|
|
2018-04-05 22:41:44 +00:00
|
|
|
static void
|
|
|
|
gtk_video_map (GtkWidget *widget)
|
|
|
|
{
|
|
|
|
GtkVideo *self = GTK_VIDEO (widget);
|
|
|
|
|
|
|
|
GTK_WIDGET_CLASS (gtk_video_parent_class)->map (widget);
|
|
|
|
|
2021-01-01 21:25:54 +00:00
|
|
|
if (self->autoplay &&
|
|
|
|
self->media_stream &&
|
|
|
|
gtk_media_stream_is_prepared (self->media_stream))
|
2018-04-05 22:41:44 +00:00
|
|
|
gtk_media_stream_play (self->media_stream);
|
|
|
|
}
|
|
|
|
|
2018-03-01 03:52:27 +00:00
|
|
|
static void
|
|
|
|
gtk_video_unmap (GtkWidget *widget)
|
|
|
|
{
|
|
|
|
GtkVideo *self = GTK_VIDEO (widget);
|
|
|
|
|
|
|
|
if (self->controls_hide_source)
|
|
|
|
{
|
|
|
|
g_source_remove (self->controls_hide_source);
|
|
|
|
self->controls_hide_source = 0;
|
|
|
|
gtk_revealer_set_reveal_child (GTK_REVEALER (self->controls_revealer), FALSE);
|
|
|
|
}
|
|
|
|
|
|
|
|
GTK_WIDGET_CLASS (gtk_video_parent_class)->unmap (widget);
|
|
|
|
}
|
|
|
|
|
2021-01-01 20:07:25 +00:00
|
|
|
static void
|
|
|
|
gtk_video_hide (GtkWidget *widget)
|
|
|
|
{
|
|
|
|
GtkVideo *self = GTK_VIDEO (widget);
|
|
|
|
|
|
|
|
if (self->autoplay && self->media_stream)
|
|
|
|
gtk_media_stream_pause (self->media_stream);
|
|
|
|
|
|
|
|
GTK_WIDGET_CLASS (gtk_video_parent_class)->hide (widget);
|
|
|
|
}
|
|
|
|
|
2020-04-28 19:11:26 +00:00
|
|
|
static void
|
2020-06-24 13:35:16 +00:00
|
|
|
gtk_video_set_focus_child (GtkWidget *widget,
|
|
|
|
GtkWidget *child)
|
2020-04-28 19:11:26 +00:00
|
|
|
{
|
|
|
|
GtkVideo *self = GTK_VIDEO (widget);
|
|
|
|
|
2020-06-24 13:35:16 +00:00
|
|
|
self->grabbed = child != NULL;
|
2020-04-28 19:11:26 +00:00
|
|
|
|
2020-06-24 13:35:16 +00:00
|
|
|
GTK_WIDGET_CLASS (gtk_video_parent_class)->set_focus_child (widget, child);
|
2020-04-28 19:11:26 +00:00
|
|
|
}
|
|
|
|
|
2018-03-01 03:52:27 +00:00
|
|
|
static void
|
|
|
|
gtk_video_dispose (GObject *object)
|
|
|
|
{
|
|
|
|
GtkVideo *self = GTK_VIDEO (object);
|
|
|
|
|
|
|
|
gtk_video_set_media_stream (self, NULL);
|
|
|
|
|
|
|
|
g_clear_pointer (&self->box, gtk_widget_unparent);
|
2018-05-13 02:53:53 +00:00
|
|
|
g_clear_object (&self->file);
|
2018-03-01 03:52:27 +00:00
|
|
|
|
|
|
|
G_OBJECT_CLASS (gtk_video_parent_class)->dispose (object);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
gtk_video_get_property (GObject *object,
|
|
|
|
guint property_id,
|
|
|
|
GValue *value,
|
|
|
|
GParamSpec *pspec)
|
|
|
|
{
|
|
|
|
GtkVideo *self = GTK_VIDEO (object);
|
|
|
|
|
|
|
|
switch (property_id)
|
|
|
|
{
|
2018-04-05 22:41:44 +00:00
|
|
|
case PROP_AUTOPLAY:
|
|
|
|
g_value_set_boolean (value, self->autoplay);
|
|
|
|
break;
|
|
|
|
|
2018-03-01 03:52:27 +00:00
|
|
|
case PROP_FILE:
|
|
|
|
g_value_set_object (value, self->file);
|
|
|
|
break;
|
|
|
|
|
2018-04-05 22:41:44 +00:00
|
|
|
case PROP_LOOP:
|
|
|
|
g_value_set_boolean (value, self->loop);
|
|
|
|
break;
|
|
|
|
|
2018-03-01 03:52:27 +00:00
|
|
|
case PROP_MEDIA_STREAM:
|
|
|
|
g_value_set_object (value, self->media_stream);
|
|
|
|
break;
|
|
|
|
|
|
|
|
default:
|
|
|
|
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
gtk_video_set_property (GObject *object,
|
|
|
|
guint property_id,
|
|
|
|
const GValue *value,
|
|
|
|
GParamSpec *pspec)
|
|
|
|
{
|
|
|
|
GtkVideo *self = GTK_VIDEO (object);
|
|
|
|
|
|
|
|
switch (property_id)
|
|
|
|
{
|
2018-04-05 22:41:44 +00:00
|
|
|
case PROP_AUTOPLAY:
|
|
|
|
gtk_video_set_autoplay (self, g_value_get_boolean (value));
|
|
|
|
break;
|
|
|
|
|
2018-03-01 03:52:27 +00:00
|
|
|
case PROP_FILE:
|
|
|
|
gtk_video_set_file (self, g_value_get_object (value));
|
|
|
|
break;
|
|
|
|
|
2018-04-05 22:41:44 +00:00
|
|
|
case PROP_LOOP:
|
|
|
|
gtk_video_set_loop (self, g_value_get_boolean (value));
|
|
|
|
break;
|
|
|
|
|
2018-03-01 03:52:27 +00:00
|
|
|
case PROP_MEDIA_STREAM:
|
|
|
|
gtk_video_set_media_stream (self, g_value_get_object (value));
|
|
|
|
break;
|
|
|
|
|
|
|
|
default:
|
|
|
|
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
gtk_video_class_init (GtkVideoClass *klass)
|
|
|
|
{
|
|
|
|
GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
|
|
|
|
GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
|
|
|
|
|
2018-03-18 02:20:02 +00:00
|
|
|
widget_class->realize = gtk_video_realize;
|
|
|
|
widget_class->unrealize = gtk_video_unrealize;
|
2018-04-05 22:41:44 +00:00
|
|
|
widget_class->map = gtk_video_map;
|
2018-03-01 03:52:27 +00:00
|
|
|
widget_class->unmap = gtk_video_unmap;
|
2021-01-01 20:07:25 +00:00
|
|
|
widget_class->hide = gtk_video_hide;
|
2020-06-24 13:35:16 +00:00
|
|
|
widget_class->set_focus_child = gtk_video_set_focus_child;
|
2018-03-01 03:52:27 +00:00
|
|
|
|
|
|
|
gobject_class->dispose = gtk_video_dispose;
|
|
|
|
gobject_class->get_property = gtk_video_get_property;
|
|
|
|
gobject_class->set_property = gtk_video_set_property;
|
|
|
|
|
2018-04-05 22:41:44 +00:00
|
|
|
/**
|
2021-02-25 02:58:45 +00:00
|
|
|
* GtkVideo:autoplay: (attributes org.gtk.Property.get=gtk_video_get_autoplay org.gtk.Property.set=gtk_video_set_autoplay)
|
2018-04-05 22:41:44 +00:00
|
|
|
*
|
|
|
|
* If the video should automatically begin playing.
|
|
|
|
*/
|
|
|
|
properties[PROP_AUTOPLAY] =
|
|
|
|
g_param_spec_boolean ("autoplay",
|
|
|
|
P_("Autoplay"),
|
|
|
|
P_("If playback should begin automatically"),
|
|
|
|
FALSE,
|
|
|
|
G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
|
|
|
|
|
2018-03-01 03:52:27 +00:00
|
|
|
/**
|
2021-02-25 02:58:45 +00:00
|
|
|
* GtkVideo:file: (attributes org.gtk.Property.get=gtk_video_get_file org.gtk.Property.set=gtk_video_set_file)
|
2018-03-01 03:52:27 +00:00
|
|
|
*
|
|
|
|
* The file played by this video if the video is playing a file.
|
|
|
|
*/
|
|
|
|
properties[PROP_FILE] =
|
|
|
|
g_param_spec_object ("file",
|
|
|
|
P_("File"),
|
|
|
|
P_("The video file played back"),
|
|
|
|
G_TYPE_FILE,
|
|
|
|
G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
|
|
|
|
|
2018-04-05 22:41:44 +00:00
|
|
|
/**
|
2021-02-25 02:58:45 +00:00
|
|
|
* GtkVideo:loop: (attributes org.gtk.Property.get=gtk_video_get_loop org.gtk.Property.set=gtk_video_set_loop)
|
2018-04-05 22:41:44 +00:00
|
|
|
*
|
|
|
|
* If new media files should be set to loop.
|
|
|
|
*/
|
|
|
|
properties[PROP_LOOP] =
|
|
|
|
g_param_spec_boolean ("loop",
|
|
|
|
P_("Loop"),
|
|
|
|
P_("If new media streams should be set to loop"),
|
|
|
|
FALSE,
|
|
|
|
G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
|
|
|
|
|
2018-03-01 03:52:27 +00:00
|
|
|
/**
|
2021-02-25 02:58:45 +00:00
|
|
|
* GtkVideo:media-stream: (attributes org.gtk.Property.get=gtk_video_get_media_stream org.gtk.Property.set=gtk_video_set_media_stream)
|
2018-03-01 03:52:27 +00:00
|
|
|
*
|
|
|
|
* The media-stream played
|
|
|
|
*/
|
|
|
|
properties[PROP_MEDIA_STREAM] =
|
|
|
|
g_param_spec_object ("media-stream",
|
|
|
|
P_("Media Stream"),
|
|
|
|
P_("The media stream played"),
|
|
|
|
GTK_TYPE_MEDIA_STREAM,
|
|
|
|
G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
|
|
|
|
|
|
|
|
g_object_class_install_properties (gobject_class, N_PROPS, properties);
|
|
|
|
|
|
|
|
gtk_widget_class_set_template_from_resource (widget_class, "/org/gtk/libgtk/ui/gtkvideo.ui");
|
|
|
|
gtk_widget_class_bind_template_child (widget_class, GtkVideo, box);
|
2018-06-09 20:54:04 +00:00
|
|
|
gtk_widget_class_bind_template_child (widget_class, GtkVideo, video_picture);
|
2018-03-01 03:52:27 +00:00
|
|
|
gtk_widget_class_bind_template_child (widget_class, GtkVideo, overlay_icon);
|
|
|
|
gtk_widget_class_bind_template_child (widget_class, GtkVideo, controls);
|
|
|
|
gtk_widget_class_bind_template_child (widget_class, GtkVideo, controls_revealer);
|
2018-03-10 17:45:23 +00:00
|
|
|
gtk_widget_class_bind_template_callback (widget_class, gtk_video_motion);
|
2021-06-08 10:58:44 +00:00
|
|
|
gtk_widget_class_bind_template_callback (widget_class, gtk_video_pressed);
|
2018-03-01 03:52:27 +00:00
|
|
|
|
2020-10-21 11:59:56 +00:00
|
|
|
gtk_widget_class_set_layout_manager_type (widget_class, GTK_TYPE_BIN_LAYOUT);
|
2018-03-01 03:52:27 +00:00
|
|
|
gtk_widget_class_set_css_name (widget_class, I_("video"));
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
gtk_video_init (GtkVideo *self)
|
|
|
|
{
|
|
|
|
gtk_widget_init_template (GTK_WIDGET (self));
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* gtk_video_new:
|
|
|
|
*
|
2021-02-20 04:33:24 +00:00
|
|
|
* Creates a new empty `GtkVideo`.
|
2018-03-01 03:52:27 +00:00
|
|
|
*
|
2021-02-20 04:33:24 +00:00
|
|
|
* Returns: a new `GtkVideo`
|
|
|
|
*/
|
2018-03-01 03:52:27 +00:00
|
|
|
GtkWidget *
|
|
|
|
gtk_video_new (void)
|
|
|
|
{
|
|
|
|
return g_object_new (GTK_TYPE_VIDEO, NULL);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* gtk_video_new_for_media_stream:
|
2021-05-19 11:24:34 +00:00
|
|
|
* @stream: (nullable): a `GtkMediaStream`
|
2018-03-01 03:52:27 +00:00
|
|
|
*
|
2021-02-20 04:33:24 +00:00
|
|
|
* Creates a `GtkVideo` to play back the given @stream.
|
2018-03-01 03:52:27 +00:00
|
|
|
*
|
2021-02-20 04:33:24 +00:00
|
|
|
* Returns: a new `GtkVideo`
|
|
|
|
*/
|
2018-03-01 03:52:27 +00:00
|
|
|
GtkWidget *
|
|
|
|
gtk_video_new_for_media_stream (GtkMediaStream *stream)
|
|
|
|
{
|
|
|
|
g_return_val_if_fail (stream == NULL || GTK_IS_MEDIA_STREAM (stream), NULL);
|
|
|
|
|
|
|
|
return g_object_new (GTK_TYPE_VIDEO,
|
|
|
|
"media-stream", stream,
|
|
|
|
NULL);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* gtk_video_new_for_file:
|
2021-05-19 11:24:34 +00:00
|
|
|
* @file: (nullable): a `GFile`
|
2018-03-01 03:52:27 +00:00
|
|
|
*
|
2021-02-20 04:33:24 +00:00
|
|
|
* Creates a `GtkVideo` to play back the given @file.
|
2018-03-01 03:52:27 +00:00
|
|
|
*
|
2021-02-20 04:33:24 +00:00
|
|
|
* Returns: a new `GtkVideo`
|
|
|
|
*/
|
2018-03-01 03:52:27 +00:00
|
|
|
GtkWidget *
|
|
|
|
gtk_video_new_for_file (GFile *file)
|
|
|
|
{
|
|
|
|
g_return_val_if_fail (file == NULL || G_IS_FILE (file), NULL);
|
|
|
|
|
|
|
|
return g_object_new (GTK_TYPE_VIDEO,
|
|
|
|
"file", file,
|
|
|
|
NULL);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* gtk_video_new_for_filename:
|
2021-05-19 11:24:34 +00:00
|
|
|
* @filename: (nullable) (type filename): filename to play back
|
2018-03-01 03:52:27 +00:00
|
|
|
*
|
2021-02-20 04:33:24 +00:00
|
|
|
* Creates a `GtkVideo` to play back the given @filename.
|
2018-03-01 03:52:27 +00:00
|
|
|
*
|
2021-02-20 04:33:24 +00:00
|
|
|
* This is a utility function that calls [ctor@Gtk.Video.new_for_file],
|
2018-06-09 20:48:06 +00:00
|
|
|
* See that function for details.
|
2018-03-01 03:52:27 +00:00
|
|
|
*
|
2021-02-20 04:33:24 +00:00
|
|
|
* Returns: a new `GtkVideo`
|
|
|
|
*/
|
2018-03-01 03:52:27 +00:00
|
|
|
GtkWidget *
|
|
|
|
gtk_video_new_for_filename (const char *filename)
|
|
|
|
{
|
|
|
|
GtkWidget *result;
|
|
|
|
GFile *file;
|
|
|
|
|
|
|
|
if (filename)
|
|
|
|
file = g_file_new_for_path (filename);
|
|
|
|
else
|
|
|
|
file = NULL;
|
|
|
|
|
|
|
|
result = gtk_video_new_for_file (file);
|
|
|
|
|
|
|
|
if (file)
|
|
|
|
g_object_unref (file);
|
|
|
|
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* gtk_video_new_for_resource:
|
2021-05-19 11:24:34 +00:00
|
|
|
* @resource_path: (nullable): resource path to play back
|
2018-03-01 03:52:27 +00:00
|
|
|
*
|
2021-02-20 04:33:24 +00:00
|
|
|
* Creates a `GtkVideo` to play back the resource at the
|
2018-03-01 03:52:27 +00:00
|
|
|
* given @resource_path.
|
|
|
|
*
|
2021-02-20 04:33:24 +00:00
|
|
|
* This is a utility function that calls [ctor@Gtk.Video.new_for_file].
|
2018-03-01 03:52:27 +00:00
|
|
|
*
|
2021-02-20 04:33:24 +00:00
|
|
|
* Returns: a new `GtkVideo`
|
|
|
|
*/
|
2018-03-01 03:52:27 +00:00
|
|
|
GtkWidget *
|
|
|
|
gtk_video_new_for_resource (const char *resource_path)
|
|
|
|
{
|
|
|
|
GtkWidget *result;
|
|
|
|
GFile *file;
|
|
|
|
|
|
|
|
if (resource_path)
|
|
|
|
{
|
|
|
|
char *uri, *escaped;
|
|
|
|
|
|
|
|
escaped = g_uri_escape_string (resource_path,
|
|
|
|
G_URI_RESERVED_CHARS_ALLOWED_IN_PATH, FALSE);
|
|
|
|
uri = g_strconcat ("resource://", escaped, NULL);
|
|
|
|
g_free (escaped);
|
|
|
|
|
|
|
|
file = g_file_new_for_uri (uri);
|
|
|
|
g_free (uri);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
file = NULL;
|
|
|
|
}
|
|
|
|
|
|
|
|
result = gtk_video_new_for_file (file);
|
|
|
|
|
|
|
|
if (file)
|
|
|
|
g_object_unref (file);
|
|
|
|
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2021-02-25 02:58:45 +00:00
|
|
|
* gtk_video_get_media_stream: (attributes org.gtk.Method.get_property=media-stream)
|
2021-02-20 04:33:24 +00:00
|
|
|
* @self: a `GtkVideo`
|
2018-03-01 03:52:27 +00:00
|
|
|
*
|
|
|
|
* Gets the media stream managed by @self or %NULL if none.
|
|
|
|
*
|
2018-03-18 20:47:10 +00:00
|
|
|
* Returns: (nullable) (transfer none): The media stream managed by @self
|
2021-02-20 04:33:24 +00:00
|
|
|
*/
|
2018-03-01 03:52:27 +00:00
|
|
|
GtkMediaStream *
|
|
|
|
gtk_video_get_media_stream (GtkVideo *self)
|
|
|
|
{
|
|
|
|
g_return_val_if_fail (GTK_IS_VIDEO (self), NULL);
|
|
|
|
|
|
|
|
return self->media_stream;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
gtk_video_update_overlay_icon (GtkVideo *self)
|
|
|
|
{
|
|
|
|
const char *icon_name;
|
|
|
|
const GError *error = NULL;
|
|
|
|
|
|
|
|
if (self->media_stream == NULL)
|
|
|
|
icon_name = "media-eject-symbolic";
|
|
|
|
else if ((error = gtk_media_stream_get_error (self->media_stream)))
|
|
|
|
icon_name = "dialog-error-symbolic";
|
|
|
|
else if (gtk_media_stream_get_ended (self->media_stream))
|
|
|
|
icon_name = "media-playlist-repeat-symbolic";
|
|
|
|
else
|
|
|
|
icon_name = "media-playback-start-symbolic";
|
|
|
|
|
|
|
|
gtk_image_set_from_icon_name (GTK_IMAGE (self->overlay_icon), icon_name);
|
|
|
|
if (error)
|
|
|
|
gtk_widget_set_tooltip_text (self->overlay_icon, error->message);
|
|
|
|
else
|
|
|
|
gtk_widget_set_tooltip_text (self->overlay_icon, NULL);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
gtk_video_update_ended (GtkVideo *self)
|
|
|
|
{
|
|
|
|
gtk_video_update_overlay_icon (self);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
gtk_video_update_error (GtkVideo *self)
|
|
|
|
{
|
|
|
|
gtk_video_update_overlay_icon (self);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
gtk_video_update_playing (GtkVideo *self)
|
|
|
|
{
|
|
|
|
gboolean playing;
|
|
|
|
|
|
|
|
if (self->media_stream != NULL)
|
|
|
|
playing = gtk_media_stream_get_playing (self->media_stream);
|
|
|
|
else
|
|
|
|
playing = FALSE;
|
|
|
|
|
|
|
|
gtk_widget_set_visible (self->overlay_icon, !playing);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
gtk_video_update_all (GtkVideo *self)
|
|
|
|
{
|
|
|
|
gtk_video_update_ended (self);
|
|
|
|
gtk_video_update_error (self);
|
|
|
|
gtk_video_update_playing (self);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
gtk_video_notify_cb (GtkMediaStream *stream,
|
|
|
|
GParamSpec *pspec,
|
|
|
|
GtkVideo *self)
|
|
|
|
{
|
|
|
|
if (g_str_equal (pspec->name, "ended"))
|
|
|
|
gtk_video_update_ended (self);
|
|
|
|
if (g_str_equal (pspec->name, "error"))
|
|
|
|
gtk_video_update_error (self);
|
|
|
|
if (g_str_equal (pspec->name, "playing"))
|
|
|
|
gtk_video_update_playing (self);
|
2020-12-16 01:22:26 +00:00
|
|
|
if (g_str_equal (pspec->name, "prepared"))
|
|
|
|
{
|
2021-01-01 21:25:54 +00:00
|
|
|
if (self->autoplay &&
|
|
|
|
gtk_media_stream_is_prepared (stream) &&
|
|
|
|
gtk_widget_get_mapped (GTK_WIDGET (self)))
|
2020-12-16 01:22:26 +00:00
|
|
|
gtk_media_stream_play (stream);
|
|
|
|
}
|
2018-03-01 03:52:27 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2021-02-25 02:58:45 +00:00
|
|
|
* gtk_video_set_media_stream: (attributes org.gtk.Method.set_property=media-stream)
|
2021-02-20 04:33:24 +00:00
|
|
|
* @self: a `GtkVideo`
|
2021-05-19 11:24:34 +00:00
|
|
|
* @stream: (nullable): The media stream to play or %NULL to unset
|
2018-03-01 03:52:27 +00:00
|
|
|
*
|
2021-02-20 04:33:24 +00:00
|
|
|
* Sets the media stream to be played back.
|
2018-03-01 03:52:27 +00:00
|
|
|
*
|
2021-02-20 04:33:24 +00:00
|
|
|
* @self will take full control of managing the media stream. If you
|
|
|
|
* want to manage a media stream yourself, consider using a
|
|
|
|
* [class@Gtk.Picture] for display.
|
|
|
|
*
|
|
|
|
* If you want to display a file, consider using [method@Gtk.Video.set_file]
|
2018-03-01 03:52:27 +00:00
|
|
|
* instead.
|
2021-02-20 04:33:24 +00:00
|
|
|
*/
|
2018-03-01 03:52:27 +00:00
|
|
|
void
|
|
|
|
gtk_video_set_media_stream (GtkVideo *self,
|
|
|
|
GtkMediaStream *stream)
|
|
|
|
{
|
|
|
|
g_return_if_fail (GTK_IS_VIDEO (self));
|
|
|
|
g_return_if_fail (stream == NULL || GTK_IS_MEDIA_STREAM (stream));
|
|
|
|
|
|
|
|
if (self->media_stream == stream)
|
|
|
|
return;
|
|
|
|
|
|
|
|
if (self->media_stream)
|
|
|
|
{
|
2021-01-02 05:06:41 +00:00
|
|
|
if (self->autoplay)
|
|
|
|
gtk_media_stream_pause (self->media_stream);
|
2018-03-01 03:52:27 +00:00
|
|
|
g_signal_handlers_disconnect_by_func (self->media_stream,
|
|
|
|
gtk_video_notify_cb,
|
|
|
|
self);
|
2018-03-18 02:20:02 +00:00
|
|
|
if (gtk_widget_get_realized (GTK_WIDGET (self)))
|
2019-05-20 00:38:08 +00:00
|
|
|
{
|
|
|
|
GdkSurface *surface;
|
|
|
|
|
|
|
|
surface = gtk_native_get_surface (gtk_widget_get_native (GTK_WIDGET (self)));
|
|
|
|
gtk_media_stream_unrealize (self->media_stream, surface);
|
|
|
|
}
|
2018-03-01 03:52:27 +00:00
|
|
|
g_object_unref (self->media_stream);
|
|
|
|
self->media_stream = NULL;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (stream)
|
|
|
|
{
|
|
|
|
self->media_stream = g_object_ref (stream);
|
2018-04-05 22:41:44 +00:00
|
|
|
gtk_media_stream_set_loop (stream, self->loop);
|
2018-03-18 02:20:02 +00:00
|
|
|
if (gtk_widget_get_realized (GTK_WIDGET (self)))
|
2019-05-20 00:38:08 +00:00
|
|
|
{
|
|
|
|
GdkSurface *surface;
|
|
|
|
|
|
|
|
surface = gtk_native_get_surface (gtk_widget_get_native (GTK_WIDGET (self)));
|
|
|
|
gtk_media_stream_realize (stream, surface);
|
|
|
|
}
|
2018-03-01 03:52:27 +00:00
|
|
|
g_signal_connect (self->media_stream,
|
|
|
|
"notify",
|
|
|
|
G_CALLBACK (gtk_video_notify_cb),
|
|
|
|
self);
|
2021-01-01 21:25:54 +00:00
|
|
|
if (self->autoplay &&
|
|
|
|
gtk_media_stream_is_prepared (stream) &&
|
|
|
|
gtk_widget_get_mapped (GTK_WIDGET (self)))
|
2018-04-05 22:41:44 +00:00
|
|
|
gtk_media_stream_play (stream);
|
2018-03-01 03:52:27 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
gtk_media_controls_set_media_stream (GTK_MEDIA_CONTROLS (self->controls), stream);
|
2018-06-09 20:54:04 +00:00
|
|
|
gtk_picture_set_paintable (GTK_PICTURE (self->video_picture), GDK_PAINTABLE (stream));
|
2018-03-01 03:52:27 +00:00
|
|
|
|
|
|
|
gtk_video_update_all (self);
|
|
|
|
|
|
|
|
g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_MEDIA_STREAM]);
|
|
|
|
}
|
|
|
|
|
2018-03-18 23:25:17 +00:00
|
|
|
/**
|
2021-02-25 02:58:45 +00:00
|
|
|
* gtk_video_get_file: (attributes org.gtk.Method.get_propert=file)
|
2021-02-20 04:33:24 +00:00
|
|
|
* @self: a `GtkVideo`
|
2018-03-18 23:25:17 +00:00
|
|
|
*
|
|
|
|
* Gets the file played by @self or %NULL if not playing back
|
|
|
|
* a file.
|
|
|
|
*
|
|
|
|
* Returns: (nullable) (transfer none): The file played by @self
|
2021-02-20 04:33:24 +00:00
|
|
|
*/
|
2018-03-18 23:25:17 +00:00
|
|
|
GFile *
|
|
|
|
gtk_video_get_file (GtkVideo *self)
|
|
|
|
{
|
|
|
|
g_return_val_if_fail (GTK_IS_VIDEO (self), NULL);
|
|
|
|
|
|
|
|
return self->file;
|
|
|
|
}
|
|
|
|
|
2018-03-01 03:52:27 +00:00
|
|
|
/**
|
2021-02-25 02:58:45 +00:00
|
|
|
* gtk_video_set_file: (attributes org.gtk.Method.set_property=file)
|
2021-02-20 04:33:24 +00:00
|
|
|
* @self: a `GtkVideo`
|
2021-05-19 11:24:34 +00:00
|
|
|
* @file: (nullable): the file to play
|
2018-03-01 03:52:27 +00:00
|
|
|
*
|
|
|
|
* Makes @self play the given @file.
|
2021-02-20 04:33:24 +00:00
|
|
|
*/
|
2018-03-01 03:52:27 +00:00
|
|
|
void
|
|
|
|
gtk_video_set_file (GtkVideo *self,
|
|
|
|
GFile *file)
|
|
|
|
{
|
|
|
|
g_return_if_fail (GTK_IS_VIDEO (self));
|
|
|
|
g_return_if_fail (file == NULL || G_IS_FILE (file));
|
|
|
|
|
|
|
|
if (!g_set_object (&self->file, file))
|
|
|
|
return;
|
|
|
|
|
|
|
|
g_object_freeze_notify (G_OBJECT (self));
|
|
|
|
|
|
|
|
if (file)
|
2018-03-18 02:20:02 +00:00
|
|
|
{
|
|
|
|
GtkMediaStream *stream;
|
2018-03-01 03:52:27 +00:00
|
|
|
|
2018-03-18 02:20:02 +00:00
|
|
|
stream = gtk_media_file_new ();
|
2018-03-01 03:52:27 +00:00
|
|
|
|
2018-03-18 02:20:02 +00:00
|
|
|
if (gtk_widget_get_realized (GTK_WIDGET (self)))
|
2020-12-29 20:55:28 +00:00
|
|
|
{
|
|
|
|
GdkSurface *surface;
|
|
|
|
|
|
|
|
surface = gtk_native_get_surface (gtk_widget_get_native (GTK_WIDGET (self)));
|
|
|
|
gtk_media_stream_realize (stream, surface);
|
|
|
|
gtk_media_file_set_file (GTK_MEDIA_FILE (stream), file);
|
|
|
|
}
|
2020-09-13 05:56:56 +00:00
|
|
|
gtk_video_set_media_stream (self, stream);
|
2018-03-18 02:20:02 +00:00
|
|
|
|
|
|
|
g_object_unref (stream);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
gtk_video_set_media_stream (self, NULL);
|
|
|
|
}
|
2018-03-01 03:52:27 +00:00
|
|
|
|
2018-04-05 22:41:44 +00:00
|
|
|
g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_FILE]);
|
|
|
|
|
2018-03-01 03:52:27 +00:00
|
|
|
g_object_thaw_notify (G_OBJECT (self));
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2018-03-18 20:47:10 +00:00
|
|
|
* gtk_video_set_filename:
|
2021-02-20 04:33:24 +00:00
|
|
|
* @self: a `GtkVideo`
|
2021-06-12 15:12:06 +00:00
|
|
|
* @filename: (type filename) (nullable): the filename to play
|
2018-03-01 03:52:27 +00:00
|
|
|
*
|
|
|
|
* Makes @self play the given @filename.
|
|
|
|
*
|
|
|
|
* This is a utility function that calls gtk_video_set_file(),
|
2021-02-20 04:33:24 +00:00
|
|
|
*/
|
2018-03-01 03:52:27 +00:00
|
|
|
void
|
|
|
|
gtk_video_set_filename (GtkVideo *self,
|
|
|
|
const char *filename)
|
|
|
|
{
|
|
|
|
GFile *file;
|
|
|
|
|
|
|
|
g_return_if_fail (GTK_IS_VIDEO (self));
|
|
|
|
|
|
|
|
if (filename)
|
|
|
|
file = g_file_new_for_path (filename);
|
|
|
|
else
|
|
|
|
file = NULL;
|
|
|
|
|
|
|
|
gtk_video_set_file (self, file);
|
|
|
|
|
|
|
|
if (file)
|
|
|
|
g_object_unref (file);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* gtk_video_set_resource:
|
2021-02-20 04:33:24 +00:00
|
|
|
* @self: a `GtkVideo`
|
2021-05-19 11:24:34 +00:00
|
|
|
* @resource_path: (nullable): the resource to set
|
2018-03-01 03:52:27 +00:00
|
|
|
*
|
|
|
|
* Makes @self play the resource at the given @resource_path.
|
|
|
|
*
|
2021-02-20 04:33:24 +00:00
|
|
|
* This is a utility function that calls [method@Gtk.Video.set_file].
|
|
|
|
*/
|
2018-03-01 03:52:27 +00:00
|
|
|
void
|
|
|
|
gtk_video_set_resource (GtkVideo *self,
|
|
|
|
const char *resource_path)
|
|
|
|
{
|
|
|
|
GFile *file;
|
|
|
|
|
|
|
|
g_return_if_fail (GTK_IS_VIDEO (self));
|
|
|
|
|
|
|
|
if (resource_path)
|
|
|
|
{
|
|
|
|
char *uri, *escaped;
|
|
|
|
|
|
|
|
escaped = g_uri_escape_string (resource_path,
|
|
|
|
G_URI_RESERVED_CHARS_ALLOWED_IN_PATH, FALSE);
|
|
|
|
uri = g_strconcat ("resource://", escaped, NULL);
|
|
|
|
g_free (escaped);
|
|
|
|
|
|
|
|
file = g_file_new_for_uri (uri);
|
|
|
|
g_free (uri);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
file = NULL;
|
|
|
|
}
|
|
|
|
|
|
|
|
gtk_video_set_file (self, file);
|
|
|
|
|
|
|
|
if (file)
|
|
|
|
g_object_unref (file);
|
|
|
|
}
|
|
|
|
|
2018-04-05 22:41:44 +00:00
|
|
|
/**
|
2021-02-25 02:58:45 +00:00
|
|
|
* gtk_video_get_autoplay: (attributes org.gtk.Method.get_property=autoplay)
|
2021-02-20 04:33:24 +00:00
|
|
|
* @self: a `GtkVideo`
|
2018-04-05 22:41:44 +00:00
|
|
|
*
|
2021-02-20 04:33:24 +00:00
|
|
|
* Returns %TRUE if videos have been set to loop.
|
2018-04-05 22:41:44 +00:00
|
|
|
*
|
|
|
|
* Returns: %TRUE if streams should autoplay
|
2021-02-20 04:33:24 +00:00
|
|
|
*/
|
2018-04-05 22:41:44 +00:00
|
|
|
gboolean
|
|
|
|
gtk_video_get_autoplay (GtkVideo *self)
|
|
|
|
{
|
|
|
|
g_return_val_if_fail (GTK_IS_VIDEO (self), FALSE);
|
|
|
|
|
|
|
|
return self->autoplay;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2021-02-25 02:58:45 +00:00
|
|
|
* gtk_video_set_autoplay: (attributes org.gtk.Method.set_property=autoplay)
|
2021-02-20 04:33:24 +00:00
|
|
|
* @self: a `GtkVideo`
|
2018-04-05 22:41:44 +00:00
|
|
|
* @autoplay: whether media streams should autoplay
|
|
|
|
*
|
2021-02-20 04:33:24 +00:00
|
|
|
* Sets whether @self automatically starts playback when it
|
|
|
|
* becomes visible or when a new file gets loaded.
|
|
|
|
*/
|
2018-04-05 22:41:44 +00:00
|
|
|
void
|
|
|
|
gtk_video_set_autoplay (GtkVideo *self,
|
|
|
|
gboolean autoplay)
|
|
|
|
{
|
|
|
|
g_return_if_fail (GTK_IS_VIDEO (self));
|
|
|
|
|
|
|
|
if (self->autoplay == autoplay)
|
|
|
|
return;
|
|
|
|
|
|
|
|
self->autoplay = autoplay;
|
|
|
|
|
|
|
|
g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_AUTOPLAY]);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2021-02-25 02:58:45 +00:00
|
|
|
* gtk_video_get_loop: (attributes org.gtk.Method.get_property=loop)
|
2021-02-20 04:33:24 +00:00
|
|
|
* @self: a `GtkVideo`
|
2018-04-05 22:41:44 +00:00
|
|
|
*
|
2021-02-20 04:33:24 +00:00
|
|
|
* Returns %TRUE if videos have been set to loop.
|
2018-04-05 22:41:44 +00:00
|
|
|
*
|
|
|
|
* Returns: %TRUE if streams should loop
|
2021-02-20 04:33:24 +00:00
|
|
|
*/
|
2018-04-05 22:41:44 +00:00
|
|
|
gboolean
|
|
|
|
gtk_video_get_loop (GtkVideo *self)
|
|
|
|
{
|
|
|
|
g_return_val_if_fail (GTK_IS_VIDEO (self), FALSE);
|
|
|
|
|
|
|
|
return self->loop;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2021-02-25 02:58:45 +00:00
|
|
|
* gtk_video_set_loop: (attributes org.gtk.Method.set_property=loop)
|
2021-02-20 04:33:24 +00:00
|
|
|
* @self: a `GtkVideo`
|
2018-04-05 22:41:44 +00:00
|
|
|
* @loop: whether media streams should loop
|
|
|
|
*
|
|
|
|
* Sets whether new files loaded by @self should be set to loop.
|
2021-02-20 04:33:24 +00:00
|
|
|
*/
|
2018-04-05 22:41:44 +00:00
|
|
|
void
|
|
|
|
gtk_video_set_loop (GtkVideo *self,
|
|
|
|
gboolean loop)
|
|
|
|
{
|
|
|
|
g_return_if_fail (GTK_IS_VIDEO (self));
|
|
|
|
|
|
|
|
if (self->loop == loop)
|
|
|
|
return;
|
|
|
|
|
|
|
|
self->loop = loop;
|
|
|
|
|
|
|
|
g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_LOOP]);
|
|
|
|
}
|