[gtk/matthiasc/gl-media-stream: 15/19] media: Add support for OpenGL to GtkGstMediaFile
- From: Matthias Clasen <matthiasc src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [gtk/matthiasc/gl-media-stream: 15/19] media: Add support for OpenGL to GtkGstMediaFile
- Date: Mon, 4 Jan 2021 20:03:53 +0000 (UTC)
commit 7901ab857b689b8777cb8f8aeb431951c9554066
Author: Benjamin Otte <otte redhat com>
Date: Tue Dec 29 14:11:51 2020 -0500
media: Add support for OpenGL to GtkGstMediaFile
modules/media/gtkgstmediafile.c | 20 ++
modules/media/gtkgstpaintable.c | 44 +++++
modules/media/gtkgstpaintableprivate.h | 4 +
modules/media/gtkgstsink.c | 334 +++++++++++++++++++++++++++++----
modules/media/gtkgstsinkprivate.h | 6 +
modules/media/meson.build | 7 +-
6 files changed, 381 insertions(+), 34 deletions(-)
---
diff --git a/modules/media/gtkgstmediafile.c b/modules/media/gtkgstmediafile.c
index 40643b881b..6382f3b10a 100644
--- a/modules/media/gtkgstmediafile.c
+++ b/modules/media/gtkgstmediafile.c
@@ -297,6 +297,24 @@ gtk_gst_media_file_update_audio (GtkMediaStream *stream,
gst_player_set_volume (self->player, volume * volume * volume);
}
+static void
+gtk_gst_media_file_realize (GtkMediaStream *stream,
+ GdkSurface *surface)
+{
+ GtkGstMediaFile *self = GTK_GST_MEDIA_FILE (stream);
+
+ gtk_gst_paintable_realize (GTK_GST_PAINTABLE (self->paintable), surface);
+}
+
+static void
+gtk_gst_media_file_unrealize (GtkMediaStream *stream,
+ GdkSurface *surface)
+{
+ GtkGstMediaFile *self = GTK_GST_MEDIA_FILE (stream);
+
+ gtk_gst_paintable_unrealize (GTK_GST_PAINTABLE (self->paintable), surface);
+}
+
static void
gtk_gst_media_file_dispose (GObject *object)
{
@@ -327,6 +345,8 @@ gtk_gst_media_file_class_init (GtkGstMediaFileClass *klass)
stream_class->pause = gtk_gst_media_file_pause;
stream_class->seek = gtk_gst_media_file_seek;
stream_class->update_audio = gtk_gst_media_file_update_audio;
+ stream_class->realize = gtk_gst_media_file_realize;
+ stream_class->unrealize = gtk_gst_media_file_unrealize;
gobject_class->dispose = gtk_gst_media_file_dispose;
}
diff --git a/modules/media/gtkgstpaintable.c b/modules/media/gtkgstpaintable.c
index 8b9c3e5f43..500997f75b 100644
--- a/modules/media/gtkgstpaintable.c
+++ b/modules/media/gtkgstpaintable.c
@@ -33,6 +33,8 @@ struct _GtkGstPaintable
GdkPaintable *image;
double pixel_aspect_ratio;
+
+ GdkGLContext *context;
};
struct _GtkGstPaintableClass
@@ -116,6 +118,7 @@ gtk_gst_paintable_video_renderer_create_video_sink (GstPlayerVideoRenderer *rend
return g_object_new (GTK_TYPE_GST_SINK,
"paintable", self,
+ "gl-context", self->context,
NULL);
}
@@ -160,6 +163,47 @@ gtk_gst_paintable_new (void)
return g_object_new (GTK_TYPE_GST_PAINTABLE, NULL);
}
+void
+gtk_gst_paintable_realize (GtkGstPaintable *self,
+ GdkSurface *surface)
+{
+ GError *error = NULL;
+
+ if (self->context)
+ return;
+
+ self->context = gdk_surface_create_gl_context (surface, &error);
+ if (self->context == NULL)
+ {
+ GST_INFO ("failed to create GDK GL context: %s", error->message);
+ g_error_free (error);
+ return;
+ }
+
+ if (!gdk_gl_context_realize (self->context, &error))
+ {
+ GST_INFO ("failed to realize GDK GL context: %s", error->message);
+ g_clear_object (&self->context);
+ g_error_free (error);
+ return;
+ }
+}
+
+void
+gtk_gst_paintable_unrealize (GtkGstPaintable *self,
+ GdkSurface *surface)
+{
+ /* XXX: We could be smarter here and:
+ * - track how often we were realized with that surface
+ * - track alternate surfaces
+ */
+ if (self->context == NULL)
+ return;
+
+ if (gdk_gl_context_get_surface (self->context) == surface)
+ g_clear_object (&self->context);
+}
+
static void
gtk_gst_paintable_set_paintable (GtkGstPaintable *self,
GdkPaintable *paintable,
diff --git a/modules/media/gtkgstpaintableprivate.h b/modules/media/gtkgstpaintableprivate.h
index b914346cf3..6691e51c0b 100644
--- a/modules/media/gtkgstpaintableprivate.h
+++ b/modules/media/gtkgstpaintableprivate.h
@@ -30,6 +30,10 @@ G_DECLARE_FINAL_TYPE (GtkGstPaintable, gtk_gst_paintable, GTK, GST_PAINTABLE, GO
GdkPaintable * gtk_gst_paintable_new (void);
+void gtk_gst_paintable_realize (GtkGstPaintable *self,
+ GdkSurface *surface);
+void gtk_gst_paintable_unrealize (GtkGstPaintable *self,
+ GdkSurface *surface);
void gtk_gst_paintable_queue_set_texture (GtkGstPaintable *self,
GdkTexture *texture,
double pixel_aspect_ratio);
diff --git a/modules/media/gtkgstsink.c b/modules/media/gtkgstsink.c
index 8d50ef403f..43a83713e0 100644
--- a/modules/media/gtkgstsink.c
+++ b/modules/media/gtkgstsink.c
@@ -25,9 +25,20 @@
#include "gtkgstpaintableprivate.h"
#include "gtkintl.h"
+#if GST_GL_HAVE_WINDOW_X11 && GST_GL_HAVE_PLATFORM_GLX && defined (GDK_WINDOWING_X11)
+#include <gdk/x11/gdkx.h>
+#include <gst/gl/x11/gstgldisplay_x11.h>
+#endif
+
+#if GST_GL_HAVE_WINDOW_WAYLAND && GST_GL_HAVE_PLATFORM_EGL && defined (GDK_WINDOWING_WAYLAND)
+#include <gdk/wayland/gdkwayland.h>
+#include <gst/gl/wayland/gstgldisplay_wayland.h>
+#endif
+
enum {
PROP_0,
PROP_PAINTABLE,
+ PROP_GL_CONTEXT,
N_PROPS,
};
@@ -37,11 +48,14 @@ GST_DEBUG_CATEGORY (gtk_debug_gst_sink);
#define FORMATS "{ BGRA, ARGB, RGBA, ABGR, RGB, BGR }"
+#define NOGL_CAPS GST_VIDEO_CAPS_MAKE (FORMATS)
+#define GL_CAPS GST_VIDEO_CAPS_MAKE_WITH_FEATURES (GST_CAPS_FEATURE_MEMORY_GL_MEMORY, "RGBA")
+
static GstStaticPadTemplate gtk_gst_sink_template =
GST_STATIC_PAD_TEMPLATE ("sink",
GST_PAD_SINK,
GST_PAD_ALWAYS,
- GST_STATIC_CAPS (GST_VIDEO_CAPS_MAKE (FORMATS))
+ GST_STATIC_CAPS (GL_CAPS "; " NOGL_CAPS)
);
G_DEFINE_TYPE_WITH_CODE (GtkGstSink, gtk_gst_sink,
@@ -53,34 +67,75 @@ static GParamSpec *properties[N_PROPS] = { NULL, };
static void
-gtk_gst_sink_get_times (GstBaseSink * bsink, GstBuffer * buf,
- GstClockTime * start, GstClockTime * end)
+gtk_gst_sink_get_times (GstBaseSink *bsink,
+ GstBuffer *buf,
+ GstClockTime *start,
+ GstClockTime *end)
{
GtkGstSink *gtk_sink;
gtk_sink = GTK_GST_SINK (bsink);
- if (GST_BUFFER_TIMESTAMP_IS_VALID (buf)) {
- *start = GST_BUFFER_TIMESTAMP (buf);
- if (GST_BUFFER_DURATION_IS_VALID (buf))
- *end = *start + GST_BUFFER_DURATION (buf);
- else {
- if (GST_VIDEO_INFO_FPS_N (>k_sink->v_info) > 0) {
- *end = *start +
- gst_util_uint64_scale_int (GST_SECOND,
- GST_VIDEO_INFO_FPS_D (>k_sink->v_info),
- GST_VIDEO_INFO_FPS_N (>k_sink->v_info));
- }
+ if (GST_BUFFER_TIMESTAMP_IS_VALID (buf))
+ {
+ *start = GST_BUFFER_TIMESTAMP (buf);
+ if (GST_BUFFER_DURATION_IS_VALID (buf))
+ *end = *start + GST_BUFFER_DURATION (buf);
+ else
+ {
+ if (GST_VIDEO_INFO_FPS_N (>k_sink->v_info) > 0)
+ {
+ *end = *start +
+ gst_util_uint64_scale_int (GST_SECOND,
+ GST_VIDEO_INFO_FPS_D (>k_sink->v_info),
+ GST_VIDEO_INFO_FPS_N (>k_sink->v_info));
+ }
+ }
}
- }
+}
+
+static GstCaps *
+gtk_gst_sink_get_caps (GstBaseSink *bsink,
+ GstCaps *filter)
+{
+ GtkGstSink *self = GTK_GST_SINK (bsink);
+ GstCaps *tmp;
+ GstCaps *result;
+
+ if (self->gst_context)
+ {
+ tmp = gst_pad_get_pad_template_caps (GST_BASE_SINK_PAD (bsink));
+ }
+ else
+ {
+ tmp = gst_caps_from_string (NOGL_CAPS);
+ }
+ GST_DEBUG_OBJECT (self, "advertising own caps %" GST_PTR_FORMAT, tmp);
+
+ if (filter)
+ {
+ GST_DEBUG_OBJECT (self, "intersecting with filter caps %" GST_PTR_FORMAT, filter);
+
+ result = gst_caps_intersect_full (filter, tmp, GST_CAPS_INTERSECT_FIRST);
+ gst_caps_unref (tmp);
+ }
+ else
+ {
+ result = tmp;
+ }
+
+ GST_DEBUG_OBJECT (self, "returning caps: %" GST_PTR_FORMAT, result);
+
+ return result;
}
static gboolean
-gtk_gst_sink_set_caps (GstBaseSink * bsink, GstCaps * caps)
+gtk_gst_sink_set_caps (GstBaseSink *bsink,
+ GstCaps *caps)
{
GtkGstSink *self = GTK_GST_SINK (bsink);
- GST_DEBUG ("set caps with %" GST_PTR_FORMAT, caps);
+ GST_DEBUG_OBJECT (self, "set caps with %" GST_PTR_FORMAT, caps);
if (!gst_video_info_from_caps (&self->v_info, caps))
return FALSE;
@@ -88,6 +143,82 @@ gtk_gst_sink_set_caps (GstBaseSink * bsink, GstCaps * caps)
return TRUE;
}
+static gboolean
+gtk_gst_sink_query (GstBaseSink *bsink,
+ GstQuery *query)
+{
+ GtkGstSink *self = GTK_GST_SINK (bsink);
+
+ if (GST_QUERY_TYPE (query) == GST_QUERY_CONTEXT &&
+ self->gst_display != NULL &&
+ gst_gl_handle_context_query (GST_ELEMENT (self), query, self->gst_display, self->gst_context,
self->gst_app_context))
+ return TRUE;
+
+ return GST_BASE_SINK_CLASS (gtk_gst_sink_parent_class)->query (bsink, query);
+}
+
+static gboolean
+gtk_gst_sink_propose_allocation (GstBaseSink *bsink,
+ GstQuery *query)
+{
+ GtkGstSink *self = GTK_GST_SINK (bsink);
+ GstBufferPool *pool = NULL;
+ GstStructure *config;
+ GstCaps *caps;
+ guint size;
+ gboolean need_pool;
+
+ if (!self->gst_context)
+ return FALSE;
+
+ gst_query_parse_allocation (query, &caps, &need_pool);
+
+ if (caps == NULL)
+ {
+ GST_DEBUG_OBJECT (bsink, "no caps specified");
+ return FALSE;
+ }
+
+ if (!gst_caps_features_contains (gst_caps_get_features (caps, 0), GST_CAPS_FEATURE_MEMORY_GL_MEMORY))
+ return FALSE;
+
+ if (need_pool)
+ {
+ GstVideoInfo info;
+
+ if (!gst_video_info_from_caps (&info, caps))
+ {
+ GST_DEBUG_OBJECT (self, "invalid caps specified");
+ return FALSE;
+ }
+
+ GST_DEBUG_OBJECT (self, "create new pool");
+ pool = gst_gl_buffer_pool_new (self->gst_context);
+
+ /* the normal size of a frame */
+ size = info.size;
+
+ config = gst_buffer_pool_get_config (pool);
+ gst_buffer_pool_config_set_params (config, caps, size, 0, 0);
+
+ if (!gst_buffer_pool_set_config (pool, config))
+ {
+ GST_DEBUG_OBJECT (bsink, "failed setting config");
+ gst_object_unref (pool);
+ return FALSE;
+ }
+
+ /* we need at least 2 buffer because we hold on to the last one */
+ gst_query_add_allocation_pool (query, pool, size, 2, 0);
+ gst_object_unref (pool);
+ }
+
+ /* we also support various metadata */
+ gst_query_add_allocation_meta (query, GST_VIDEO_META_API_TYPE, 0);
+
+ return TRUE;
+}
+
static GdkMemoryFormat
gtk_gst_memory_format_from_video (GstVideoFormat format)
{
@@ -125,29 +256,50 @@ gtk_gst_sink_texture_from_buffer (GtkGstSink *self,
{
GstVideoFrame frame;
GdkTexture *texture;
- GBytes *bytes;
- if (!gst_video_frame_map (&frame, &self->v_info, buffer, GST_MAP_READ))
- return NULL;
-
- bytes = g_bytes_new_with_free_func (frame.data[0],
- frame.info.width * frame.info.stride[0],
- (GDestroyNotify) video_frame_free,
- g_memdup (&frame, sizeof (frame)));
- texture = gdk_memory_texture_new (frame.info.width,
+ if (self->gdk_context &&
+ gst_video_frame_map (&frame, &self->v_info, buffer, GST_MAP_READ | GST_MAP_GL))
+ {
+ texture = gdk_gl_texture_new (self->gdk_context,
+ *(guint *) frame.data[0],
+ frame.info.width,
frame.info.height,
- gtk_gst_memory_format_from_video (GST_VIDEO_FRAME_FORMAT (&frame)),
- bytes,
- frame.info.stride[0]);
- g_bytes_unref (bytes);
+ (GDestroyNotify) gst_buffer_unref,
+ gst_buffer_ref (buffer));
- *pixel_aspect_ratio = ((double) frame.info.par_n) / ((double) frame.info.par_d);
+ *pixel_aspect_ratio = ((double) frame.info.par_n) / ((double) frame.info.par_d);
+
+ gst_video_frame_unmap (&frame);
+ }
+ else if (gst_video_frame_map (&frame, &self->v_info, buffer, GST_MAP_READ))
+ {
+ GBytes *bytes;
+
+ bytes = g_bytes_new_with_free_func (frame.data[0],
+ frame.info.height * frame.info.stride[0],
+ (GDestroyNotify) video_frame_free,
+ g_memdup (&frame, sizeof (frame)));
+ texture = gdk_memory_texture_new (frame.info.width,
+ frame.info.height,
+ gtk_gst_memory_format_from_video (GST_VIDEO_FRAME_FORMAT (&frame)),
+ bytes,
+ frame.info.stride[0]);
+ g_bytes_unref (bytes);
+
+ *pixel_aspect_ratio = ((double) frame.info.par_n) / ((double) frame.info.par_d);
+ }
+ else
+ {
+ GST_ERROR_OBJECT (self, "Could not convert buffer to texture.");
+ texture = NULL;
+ }
return texture;
}
static GstFlowReturn
-gtk_gst_sink_show_frame (GstVideoSink * vsink, GstBuffer * buf)
+gtk_gst_sink_show_frame (GstVideoSink *vsink,
+ GstBuffer *buf)
{
GtkGstSink *self;
GdkTexture *texture;
@@ -171,6 +323,100 @@ gtk_gst_sink_show_frame (GstVideoSink * vsink, GstBuffer * buf)
return GST_FLOW_OK;
}
+static void
+gtk_gst_sink_initialize_gl (GtkGstSink *self)
+{
+ GdkDisplay *display;
+ GError *error = NULL;
+
+ display = gdk_gl_context_get_display (self->gdk_context);
+
+ gdk_gl_context_make_current (self->gdk_context);
+
+#if GST_GL_HAVE_WINDOW_X11 && GST_GL_HAVE_PLATFORM_GLX && defined (GDK_WINDOWING_X11)
+ if (GDK_IS_X11_DISPLAY (display))
+ {
+ GstGLPlatform platform = GST_GL_PLATFORM_GLX;
+ GstGLAPI gl_api;
+ guintptr gl_handle;
+
+ GST_DEBUG_OBJECT (self, "got GLX on X11!");
+
+ gl_api = gst_gl_context_get_current_gl_api (platform, NULL, NULL);
+ gl_handle = gst_gl_context_get_current_gl_context (platform);
+ if (gl_handle)
+ {
+ self->gst_display = GST_GL_DISPLAY (gst_gl_display_x11_new_with_display
(gdk_x11_display_get_xdisplay (display)));
+ self->gst_app_context = gst_gl_context_new_wrapped (self->gst_display, gl_handle, platform,
gl_api);
+ }
+ else
+ {
+ GST_ERROR_OBJECT (self, "Failed to get handle from GdkGLContext, not using GLX");
+ return;
+ }
+ }
+ else
+#endif
+#if GST_GL_HAVE_WINDOW_WAYLAND && GST_GL_HAVE_PLATFORM_EGL && defined (GDK_WINDOWING_WAYLAND)
+ if (GDK_IS_WAYLAND_DISPLAY (display))
+ {
+ GstGLPlatform platform = GST_GL_PLATFORM_GLX;
+ GstGLAPI gl_api;
+ guintptr gl_handle;
+
+ GST_DEBUG_OBJECT (self, "got EGL on Wayland!");
+
+ platform = GST_GL_PLATFORM_EGL;
+ gl_api = gst_gl_context_get_current_gl_api (platform, NULL, NULL);
+ gl_handle = gst_gl_context_get_current_gl_context (platform);
+
+ if (gl_handle)
+ {
+ struct wl_display *wayland_display;
+
+ wayland_display = gdk_wayland_display_get_wl_display (display);
+ self->gst_display = GST_GL_DISPLAY (gst_gl_display_wayland_new_with_display (wayland_display));
+ self->gst_app_context = gst_gl_context_new_wrapped (self->gst_display, gl_handle, platform,
gl_api);
+ }
+ else
+ {
+ GST_ERROR_OBJECT (self, "Failed to get handle from GdkGLContext, not using Wayland EGL");
+ return;
+ }
+ }
+ else
+#endif
+ {
+ GST_INFO_OBJECT (self, "Unsupported GDK display %s for GL", G_OBJECT_TYPE_NAME (display));
+ return;
+ }
+
+ g_assert (self->gst_app_context != NULL);
+
+ gst_gl_context_activate (self->gst_app_context, TRUE);
+ if (!gst_gl_context_fill_info (self->gst_app_context, &error))
+ {
+ GST_ERROR_OBJECT (self, "failed to retrieve GDK context info: %s", error->message);
+ g_clear_error (&error);
+ g_clear_object (&self->gst_app_context);
+ g_clear_object (&self->gst_display);
+ return;
+ }
+ else
+ {
+ gst_gl_context_activate (self->gst_app_context, FALSE);
+ }
+
+ if (!gst_gl_display_create_context (self->gst_display, self->gst_app_context, &self->gst_context, &error))
+ {
+ GST_ERROR_OBJECT (self, "Couldn't create GL context: %s", error->message);
+ g_error_free (error);
+ g_clear_object (&self->gst_app_context);
+ g_clear_object (&self->gst_display);
+ return;
+ }
+}
+
static void
gtk_gst_sink_set_property (GObject *object,
guint prop_id,
@@ -188,6 +434,12 @@ gtk_gst_sink_set_property (GObject *object,
self->paintable = GTK_GST_PAINTABLE (gtk_gst_paintable_new ());
break;
+ case PROP_GL_CONTEXT:
+ self->gdk_context = g_value_dup_object (value);
+ if (self->gdk_context != NULL)
+ gtk_gst_sink_initialize_gl (self);
+ break;
+
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
@@ -220,6 +472,9 @@ gtk_gst_sink_dispose (GObject *object)
GtkGstSink *self = GTK_GST_SINK (object);
g_clear_object (&self->paintable);
+ g_clear_object (&self->gst_app_context);
+ g_clear_object (&self->gst_display);
+ g_clear_object (&self->gdk_context);
G_OBJECT_CLASS (gtk_gst_sink_parent_class)->dispose (object);
}
@@ -238,6 +493,9 @@ gtk_gst_sink_class_init (GtkGstSinkClass * klass)
gstbasesink_class->set_caps = gtk_gst_sink_set_caps;
gstbasesink_class->get_times = gtk_gst_sink_get_times;
+ gstbasesink_class->query = gtk_gst_sink_query;
+ gstbasesink_class->propose_allocation = gtk_gst_sink_propose_allocation;
+ gstbasesink_class->get_caps = gtk_gst_sink_get_caps;
gstvideosink_class->show_frame = gtk_gst_sink_show_frame;
@@ -253,6 +511,18 @@ gtk_gst_sink_class_init (GtkGstSinkClass * klass)
GTK_TYPE_GST_PAINTABLE,
G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+ /**
+ * GtkGstSink:gl-context:
+ *
+ * The #GdkGLContext to use for GL rendering.
+ */
+ properties[PROP_GL_CONTEXT] =
+ g_param_spec_object ("gl-context",
+ P_("gl-context"),
+ P_("GL context to use for rendering"),
+ GDK_TYPE_GL_CONTEXT,
+ G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+
g_object_class_install_properties (gobject_class, N_PROPS, properties);
gst_element_class_set_metadata (gstelement_class,
diff --git a/modules/media/gtkgstsinkprivate.h b/modules/media/gtkgstsinkprivate.h
index ceb3aa5207..06e9f7f1b3 100644
--- a/modules/media/gtkgstsinkprivate.h
+++ b/modules/media/gtkgstsinkprivate.h
@@ -24,6 +24,8 @@
#include "gtkgstpaintableprivate.h"
#include <gst/gst.h>
+#define GST_USE_UNSTABLE_API
+#include <gst/gl/gl.h>
#include <gst/video/gstvideosink.h>
#include <gst/video/video.h>
@@ -47,6 +49,10 @@ struct _GtkGstSink
GstVideoInfo v_info;
GtkGstPaintable * paintable;
+ GdkGLContext * gdk_context;
+ GstGLDisplay * gst_display;
+ GstGLContext * gst_app_context;
+ GstGLContext * gst_context;
};
struct _GtkGstSinkClass
diff --git a/modules/media/meson.build b/modules/media/meson.build
index 42b3c48e4f..aa5c0280e6 100644
--- a/modules/media/meson.build
+++ b/modules/media/meson.build
@@ -42,7 +42,10 @@ endif
gstplayer_dep = dependency('gstreamer-player-1.0', version: '>= 1.12.3',
required: get_option('media-gstreamer'))
-if gstplayer_dep.found()
+gstgl_dep = dependency('gstreamer-gl-1.0', version: '>= 1.12.3',
+ required: get_option('media-gstreamer'))
+
+if gstplayer_dep.found() and gstgl_dep.found()
media_backends += 'gstreamer'
cdata.set('HAVE_GSTREAMER', 1)
shared_module('media-gstreamer',
@@ -52,7 +55,7 @@ if gstplayer_dep.found()
'gtkgstsink.c',
],
c_args: extra_c_args,
- dependencies: [ libm, libgtk_dep, gstplayer_dep ],
+ dependencies: [ libm, libgtk_dep, gstplayer_dep, gstgl_dep ],
install_dir: media_install_dir,
install: true,
)
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]