mirror of
https://gitlab.freedesktop.org/gstreamer/gstreamer.git
synced 2024-11-03 16:09:39 +00:00
1100 lines
32 KiB
C
1100 lines
32 KiB
C
/*
|
|
* Copyright (C) 2008 Ole André Vadla Ravnås <ole.andre.ravnas@tandberg.com>
|
|
* 2009 Andres Colubri <andres.colubri@gmail.com>
|
|
*
|
|
* This library is free software; you can redistribute it and/or
|
|
* modify it under the terms of the GNU Library General Public
|
|
* License as published by the Free Software Foundation; either
|
|
* version 2 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
|
|
* Library General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Library General Public
|
|
* License along with this library; if not, write to the
|
|
* Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
|
|
* Boston, MA 02110-1301, USA.
|
|
*/
|
|
|
|
/**
|
|
* SECTION:element-ksvideosrc
|
|
*
|
|
* Provides low-latency video capture from WDM cameras on Windows.
|
|
*
|
|
* <refsect2>
|
|
* <title>Example pipelines</title>
|
|
* |[
|
|
* gst-launch -v ksvideosrc do-stats=TRUE ! ffmpegcolorspace ! dshowvideosink
|
|
* ]| Capture from a camera and render using dshowvideosink.
|
|
* |[
|
|
* gst-launch -v ksvideosrc do-stats=TRUE ! image/jpeg, width=640, height=480
|
|
* ! jpegdec ! ffmpegcolorspace ! dshowvideosink
|
|
* ]| Capture from an MJPEG camera and render using dshowvideosink.
|
|
* </refsect2>
|
|
*/
|
|
|
|
#ifdef HAVE_CONFIG_H
|
|
# include <config.h>
|
|
#endif
|
|
|
|
#include "gstksvideosrc.h"
|
|
|
|
#include "gstksclock.h"
|
|
#include "gstksvideodevice.h"
|
|
#include "kshelpers.h"
|
|
#include "ksvideohelpers.h"
|
|
#include "ksdeviceprovider.h"
|
|
|
|
#define DEFAULT_DEVICE_PATH NULL
|
|
#define DEFAULT_DEVICE_NAME NULL
|
|
#define DEFAULT_DEVICE_INDEX -1
|
|
#define DEFAULT_DO_STATS FALSE
|
|
#define DEFAULT_ENABLE_QUIRKS TRUE
|
|
|
|
enum
|
|
{
|
|
PROP_0,
|
|
PROP_DEVICE_PATH,
|
|
PROP_DEVICE_NAME,
|
|
PROP_DEVICE_INDEX,
|
|
PROP_DO_STATS,
|
|
PROP_FPS,
|
|
PROP_ENABLE_QUIRKS,
|
|
};
|
|
|
|
GST_DEBUG_CATEGORY (gst_ks_debug);
|
|
#define GST_CAT_DEFAULT gst_ks_debug
|
|
|
|
#define KS_WORKER_LOCK(priv) g_mutex_lock (&priv->worker_lock)
|
|
#define KS_WORKER_UNLOCK(priv) g_mutex_unlock (&priv->worker_lock)
|
|
#define KS_WORKER_WAIT(priv) \
|
|
g_cond_wait (&priv->worker_notify_cond, &priv->worker_lock)
|
|
#define KS_WORKER_NOTIFY(priv) g_cond_signal (&priv->worker_notify_cond)
|
|
#define KS_WORKER_WAIT_FOR_RESULT(priv) \
|
|
g_cond_wait (&priv->worker_result_cond, &priv->worker_lock)
|
|
#define KS_WORKER_NOTIFY_RESULT(priv) \
|
|
g_cond_signal (&priv->worker_result_cond)
|
|
|
|
typedef enum
|
|
{
|
|
KS_WORKER_STATE_STARTING,
|
|
KS_WORKER_STATE_READY,
|
|
KS_WORKER_STATE_STOPPING,
|
|
KS_WORKER_STATE_ERROR,
|
|
} KsWorkerState;
|
|
|
|
struct _GstKsVideoSrcPrivate
|
|
{
|
|
/* Properties */
|
|
gchar *device_path;
|
|
gchar *device_name;
|
|
gint device_index;
|
|
gboolean do_stats;
|
|
gboolean enable_quirks;
|
|
|
|
/* State */
|
|
GstKsClock *ksclock;
|
|
GstKsVideoDevice *device;
|
|
|
|
guint64 offset;
|
|
GstClockTime prev_ts;
|
|
gboolean running;
|
|
|
|
/* Worker thread */
|
|
GThread *worker_thread;
|
|
GMutex worker_lock;
|
|
GCond worker_notify_cond;
|
|
GCond worker_result_cond;
|
|
KsWorkerState worker_state;
|
|
|
|
GstCaps *worker_pending_caps;
|
|
gboolean worker_setcaps_result;
|
|
|
|
gboolean worker_pending_run;
|
|
gboolean worker_run_result;
|
|
|
|
gulong worker_error_code;
|
|
|
|
/* Statistics */
|
|
GstClockTime last_sampling;
|
|
guint count;
|
|
guint fps;
|
|
};
|
|
|
|
#define GST_KS_VIDEO_SRC_GET_PRIVATE(o) ((o)->priv)
|
|
|
|
static void gst_ks_video_src_finalize (GObject * object);
|
|
static void gst_ks_video_src_get_property (GObject * object, guint prop_id,
|
|
GValue * value, GParamSpec * pspec);
|
|
static void gst_ks_video_src_set_property (GObject * object, guint prop_id,
|
|
const GValue * value, GParamSpec * pspec);
|
|
|
|
G_GNUC_UNUSED static GArray
|
|
* gst_ks_video_src_get_device_name_values (GstKsVideoSrc * self);
|
|
static void gst_ks_video_src_reset (GstKsVideoSrc * self);
|
|
|
|
static GstStateChangeReturn gst_ks_video_src_change_state (GstElement * element,
|
|
GstStateChange transition);
|
|
static gboolean gst_ks_video_src_set_clock (GstElement * element,
|
|
GstClock * clock);
|
|
|
|
static GstCaps *gst_ks_video_src_get_caps (GstBaseSrc * basesrc,
|
|
GstCaps * filter);
|
|
static gboolean gst_ks_video_src_set_caps (GstBaseSrc * basesrc,
|
|
GstCaps * caps);
|
|
static GstCaps *gst_ks_video_src_fixate (GstBaseSrc * basesrc, GstCaps * caps);
|
|
static gboolean gst_ks_video_src_query (GstBaseSrc * basesrc, GstQuery * query);
|
|
static gboolean gst_ks_video_src_unlock (GstBaseSrc * basesrc);
|
|
static gboolean gst_ks_video_src_unlock_stop (GstBaseSrc * basesrc);
|
|
|
|
static GstFlowReturn gst_ks_video_src_create (GstPushSrc * pushsrc,
|
|
GstBuffer ** buffer);
|
|
static GstBuffer *gst_ks_video_src_alloc_buffer (guint size, guint alignment,
|
|
gpointer user_data);
|
|
|
|
G_DEFINE_TYPE (GstKsVideoSrc, gst_ks_video_src, GST_TYPE_PUSH_SRC);
|
|
|
|
static GstKsVideoSrcClass *parent_class = NULL;
|
|
|
|
static void
|
|
gst_ks_video_src_class_init (GstKsVideoSrcClass * klass)
|
|
{
|
|
GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
|
|
GstElementClass *gstelement_class = GST_ELEMENT_CLASS (klass);
|
|
GstBaseSrcClass *gstbasesrc_class = GST_BASE_SRC_CLASS (klass);
|
|
GstPushSrcClass *gstpushsrc_class = GST_PUSH_SRC_CLASS (klass);
|
|
|
|
g_type_class_add_private (klass, sizeof (GstKsVideoSrcPrivate));
|
|
|
|
parent_class = g_type_class_peek_parent (klass);
|
|
|
|
gst_element_class_set_static_metadata (gstelement_class, "KsVideoSrc",
|
|
"Source/Video",
|
|
"Stream data from a video capture device through Windows kernel streaming",
|
|
"Ole André Vadla Ravnås <ole.andre.ravnas@tandberg.com>\n"
|
|
"Haakon Sporsheim <hakon.sporsheim@tandberg.com>\n"
|
|
"Andres Colubri <andres.colubri@gmail.com>");
|
|
|
|
gst_element_class_add_pad_template (gstelement_class,
|
|
gst_pad_template_new ("src", GST_PAD_SRC, GST_PAD_ALWAYS,
|
|
ks_video_get_all_caps ()));
|
|
|
|
gobject_class->finalize = gst_ks_video_src_finalize;
|
|
gobject_class->get_property = gst_ks_video_src_get_property;
|
|
gobject_class->set_property = gst_ks_video_src_set_property;
|
|
|
|
gstelement_class->change_state =
|
|
GST_DEBUG_FUNCPTR (gst_ks_video_src_change_state);
|
|
gstelement_class->set_clock = GST_DEBUG_FUNCPTR (gst_ks_video_src_set_clock);
|
|
|
|
gstbasesrc_class->get_caps = GST_DEBUG_FUNCPTR (gst_ks_video_src_get_caps);
|
|
gstbasesrc_class->set_caps = GST_DEBUG_FUNCPTR (gst_ks_video_src_set_caps);
|
|
gstbasesrc_class->fixate = GST_DEBUG_FUNCPTR (gst_ks_video_src_fixate);
|
|
gstbasesrc_class->query = GST_DEBUG_FUNCPTR (gst_ks_video_src_query);
|
|
gstbasesrc_class->unlock = GST_DEBUG_FUNCPTR (gst_ks_video_src_unlock);
|
|
gstbasesrc_class->unlock_stop =
|
|
GST_DEBUG_FUNCPTR (gst_ks_video_src_unlock_stop);
|
|
|
|
gstpushsrc_class->create = GST_DEBUG_FUNCPTR (gst_ks_video_src_create);
|
|
|
|
g_object_class_install_property (gobject_class, PROP_DEVICE_PATH,
|
|
g_param_spec_string ("device-path", "Device Path",
|
|
"The device path", DEFAULT_DEVICE_PATH,
|
|
G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
|
|
g_object_class_install_property (gobject_class, PROP_DEVICE_NAME,
|
|
g_param_spec_string ("device-name", "Device Name",
|
|
"The human-readable device name", DEFAULT_DEVICE_NAME,
|
|
G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
|
|
g_object_class_install_property (gobject_class, PROP_DEVICE_INDEX,
|
|
g_param_spec_int ("device-index", "Device Index",
|
|
"The zero-based device index", -1, G_MAXINT, DEFAULT_DEVICE_INDEX,
|
|
G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
|
|
g_object_class_install_property (gobject_class, PROP_DO_STATS,
|
|
g_param_spec_boolean ("do-stats", "Enable statistics",
|
|
"Enable logging of statistics", DEFAULT_DO_STATS,
|
|
G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
|
|
g_object_class_install_property (gobject_class, PROP_FPS,
|
|
g_param_spec_int ("fps", "Frames per second",
|
|
"Last measured framerate, if statistics are enabled",
|
|
-1, G_MAXINT, -1, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
|
|
g_object_class_install_property (gobject_class, PROP_ENABLE_QUIRKS,
|
|
g_param_spec_boolean ("enable-quirks", "Enable quirks",
|
|
"Enable driver-specific quirks", DEFAULT_ENABLE_QUIRKS,
|
|
G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
|
|
}
|
|
|
|
static void
|
|
gst_ks_video_src_init (GstKsVideoSrc * self)
|
|
{
|
|
GstBaseSrc *basesrc = GST_BASE_SRC (self);
|
|
GstKsVideoSrcPrivate *priv;
|
|
|
|
self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self, GST_TYPE_KS_VIDEO_SRC,
|
|
GstKsVideoSrcPrivate);
|
|
priv = GST_KS_VIDEO_SRC_GET_PRIVATE (self);
|
|
|
|
gst_base_src_set_live (basesrc, TRUE);
|
|
gst_base_src_set_format (basesrc, GST_FORMAT_TIME);
|
|
|
|
gst_ks_video_src_reset (self);
|
|
|
|
priv->device_path = DEFAULT_DEVICE_PATH;
|
|
priv->device_name = DEFAULT_DEVICE_NAME;
|
|
priv->device_index = DEFAULT_DEVICE_INDEX;
|
|
priv->do_stats = DEFAULT_DO_STATS;
|
|
priv->enable_quirks = DEFAULT_ENABLE_QUIRKS;
|
|
}
|
|
|
|
static void
|
|
gst_ks_video_src_finalize (GObject * object)
|
|
{
|
|
GstKsVideoSrc *self = GST_KS_VIDEO_SRC (object);
|
|
GstKsVideoSrcPrivate *priv = GST_KS_VIDEO_SRC_GET_PRIVATE (self);
|
|
|
|
g_free (priv->device_name);
|
|
g_free (priv->device_path);
|
|
|
|
G_OBJECT_CLASS (parent_class)->finalize (object);
|
|
}
|
|
|
|
static void
|
|
gst_ks_video_src_get_property (GObject * object, guint prop_id,
|
|
GValue * value, GParamSpec * pspec)
|
|
{
|
|
GstKsVideoSrc *self = GST_KS_VIDEO_SRC (object);
|
|
GstKsVideoSrcPrivate *priv = GST_KS_VIDEO_SRC_GET_PRIVATE (self);
|
|
|
|
switch (prop_id) {
|
|
case PROP_DEVICE_PATH:
|
|
g_value_set_string (value, priv->device_path);
|
|
break;
|
|
case PROP_DEVICE_NAME:
|
|
g_value_set_string (value, priv->device_name);
|
|
break;
|
|
case PROP_DEVICE_INDEX:
|
|
g_value_set_int (value, priv->device_index);
|
|
break;
|
|
case PROP_DO_STATS:
|
|
GST_OBJECT_LOCK (object);
|
|
g_value_set_boolean (value, priv->do_stats);
|
|
GST_OBJECT_UNLOCK (object);
|
|
break;
|
|
case PROP_FPS:
|
|
GST_OBJECT_LOCK (object);
|
|
g_value_set_int (value, priv->fps);
|
|
GST_OBJECT_UNLOCK (object);
|
|
break;
|
|
case PROP_ENABLE_QUIRKS:
|
|
g_value_set_boolean (value, priv->enable_quirks);
|
|
break;
|
|
default:
|
|
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
|
|
break;
|
|
}
|
|
}
|
|
|
|
static void
|
|
gst_ks_video_src_set_property (GObject * object, guint prop_id,
|
|
const GValue * value, GParamSpec * pspec)
|
|
{
|
|
GstKsVideoSrc *self = GST_KS_VIDEO_SRC (object);
|
|
GstKsVideoSrcPrivate *priv = GST_KS_VIDEO_SRC_GET_PRIVATE (self);
|
|
|
|
switch (prop_id) {
|
|
case PROP_DEVICE_PATH:
|
|
g_free (priv->device_path);
|
|
priv->device_path = g_value_dup_string (value);
|
|
break;
|
|
case PROP_DEVICE_NAME:
|
|
{
|
|
const gchar *device_name = g_value_get_string (value);
|
|
g_free (priv->device_name);
|
|
priv->device_name = NULL;
|
|
if (device_name && strlen (device_name) != 0)
|
|
priv->device_name = g_strdup (device_name);
|
|
}
|
|
break;
|
|
case PROP_DEVICE_INDEX:
|
|
priv->device_index = g_value_get_int (value);
|
|
break;
|
|
case PROP_DO_STATS:
|
|
GST_OBJECT_LOCK (object);
|
|
priv->do_stats = g_value_get_boolean (value);
|
|
GST_OBJECT_UNLOCK (object);
|
|
break;
|
|
case PROP_ENABLE_QUIRKS:
|
|
priv->enable_quirks = g_value_get_boolean (value);
|
|
break;
|
|
default:
|
|
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
|
|
break;
|
|
}
|
|
}
|
|
|
|
static void
|
|
gst_ks_video_src_reset (GstKsVideoSrc * self)
|
|
{
|
|
GstKsVideoSrcPrivate *priv = GST_KS_VIDEO_SRC_GET_PRIVATE (self);
|
|
|
|
/* Reset statistics */
|
|
priv->last_sampling = GST_CLOCK_TIME_NONE;
|
|
priv->count = 0;
|
|
priv->fps = -1;
|
|
|
|
/* Reset timestamping state */
|
|
priv->offset = 0;
|
|
priv->prev_ts = GST_CLOCK_TIME_NONE;
|
|
|
|
priv->running = FALSE;
|
|
}
|
|
|
|
static void
|
|
gst_ks_video_src_apply_driver_quirks (GstKsVideoSrc * self)
|
|
{
|
|
HMODULE mod;
|
|
|
|
/*
|
|
* Logitech's driver software injects the following DLL into all processes
|
|
* spawned. This DLL does some nasty tricks, sitting in between the
|
|
* application and the low-level ntdll API (NtCreateFile, NtClose,
|
|
* NtDeviceIoControlFile, NtDuplicateObject, etc.), making all sorts
|
|
* of assumptions.
|
|
*
|
|
* The only regression that this quirk causes is that the video effects
|
|
* feature doesn't work.
|
|
*/
|
|
mod = GetModuleHandle ("LVPrcInj.dll");
|
|
if (mod != NULL) {
|
|
GST_DEBUG_OBJECT (self, "Logitech DLL detected, neutralizing it");
|
|
|
|
/*
|
|
* We know that no-one's actually keeping this handle around to decrement
|
|
* its reference count, so we'll take care of that job. The DLL's DllMain
|
|
* implementation takes care of rolling back changes when it gets unloaded,
|
|
* so this seems to be the cleanest and most future-proof way that we can
|
|
* get rid of it...
|
|
*/
|
|
FreeLibrary (mod);
|
|
|
|
/* Paranoia: verify that it's no longer there */
|
|
mod = GetModuleHandle ("LVPrcInj.dll");
|
|
if (mod != NULL)
|
|
GST_WARNING_OBJECT (self, "failed to neutralize Logitech DLL");
|
|
}
|
|
}
|
|
|
|
/*FIXME: when we have a devices API replacement */
|
|
G_GNUC_UNUSED static GArray *
|
|
gst_ks_video_src_get_device_name_values (GstKsVideoSrc * self)
|
|
{
|
|
GList *devices, *cur;
|
|
GArray *array = g_array_new (TRUE, TRUE, sizeof (GValue));
|
|
|
|
devices = ks_enumerate_devices (&KSCATEGORY_VIDEO, &KSCATEGORY_CAPTURE);
|
|
if (devices == NULL)
|
|
return array;
|
|
|
|
devices = ks_video_device_list_sort_cameras_first (devices);
|
|
|
|
for (cur = devices; cur != NULL; cur = cur->next) {
|
|
GValue value = { 0, };
|
|
KsDeviceEntry *entry = cur->data;
|
|
|
|
g_value_init (&value, G_TYPE_STRING);
|
|
g_value_set_string (&value, entry->name);
|
|
g_array_append_val (array, value);
|
|
g_value_unset (&value);
|
|
|
|
ks_device_entry_free (entry);
|
|
}
|
|
|
|
g_list_free (devices);
|
|
return array;
|
|
}
|
|
|
|
static gboolean
|
|
gst_ks_video_src_open_device (GstKsVideoSrc * self)
|
|
{
|
|
GstKsVideoSrcPrivate *priv = GST_KS_VIDEO_SRC_GET_PRIVATE (self);
|
|
GstKsVideoDevice *device = NULL;
|
|
GList *devices, *cur;
|
|
|
|
g_assert (priv->device == NULL);
|
|
|
|
devices = ks_enumerate_devices (&KSCATEGORY_VIDEO, &KSCATEGORY_CAPTURE);
|
|
if (devices == NULL)
|
|
goto error_no_devices;
|
|
|
|
devices = ks_video_device_list_sort_cameras_first (devices);
|
|
|
|
for (cur = devices; cur != NULL; cur = cur->next) {
|
|
KsDeviceEntry *entry = cur->data;
|
|
|
|
GST_DEBUG_OBJECT (self, "device %d: name='%s' path='%s'",
|
|
entry->index, entry->name, entry->path);
|
|
}
|
|
|
|
for (cur = devices; cur != NULL; cur = cur->next) {
|
|
KsDeviceEntry *entry = cur->data;
|
|
gboolean match;
|
|
|
|
if (device != NULL) {
|
|
ks_device_entry_free (entry);
|
|
continue;
|
|
}
|
|
if (priv->device_path != NULL) {
|
|
match = g_ascii_strcasecmp (entry->path, priv->device_path) == 0;
|
|
} else if (priv->device_name != NULL) {
|
|
match = g_ascii_strcasecmp (entry->name, priv->device_name) == 0;
|
|
} else if (priv->device_index >= 0) {
|
|
match = entry->index == priv->device_index;
|
|
} else {
|
|
match = TRUE; /* pick the first entry */
|
|
}
|
|
|
|
if (match) {
|
|
priv->ksclock = g_object_new (GST_TYPE_KS_CLOCK, NULL);
|
|
if (priv->ksclock != NULL && gst_ks_clock_open (priv->ksclock)) {
|
|
GstClock *clock = GST_ELEMENT_CLOCK (self);
|
|
if (clock != NULL)
|
|
gst_ks_clock_provide_master_clock (priv->ksclock, clock);
|
|
} else {
|
|
GST_WARNING_OBJECT (self, "failed to create/open KsClock");
|
|
g_object_unref (priv->ksclock);
|
|
priv->ksclock = NULL;
|
|
}
|
|
|
|
device = gst_ks_video_device_new (entry->path, priv->ksclock,
|
|
gst_ks_video_src_alloc_buffer, self);
|
|
}
|
|
|
|
ks_device_entry_free (entry);
|
|
}
|
|
|
|
g_list_free (devices);
|
|
|
|
if (device == NULL)
|
|
goto error_no_match;
|
|
|
|
if (!gst_ks_video_device_open (device))
|
|
goto error_open;
|
|
|
|
priv->device = device;
|
|
|
|
return TRUE;
|
|
|
|
/* ERRORS */
|
|
error_no_devices:
|
|
{
|
|
GST_ELEMENT_ERROR (self, RESOURCE, NOT_FOUND,
|
|
("No video capture devices found"), (NULL));
|
|
return FALSE;
|
|
}
|
|
error_no_match:
|
|
{
|
|
if (priv->device_path != NULL) {
|
|
GST_ELEMENT_ERROR (self, RESOURCE, NOT_FOUND,
|
|
("Specified video capture device with path '%s' not found",
|
|
priv->device_path), (NULL));
|
|
} else if (priv->device_name != NULL) {
|
|
GST_ELEMENT_ERROR (self, RESOURCE, NOT_FOUND,
|
|
("Specified video capture device with name '%s' not found",
|
|
priv->device_name), (NULL));
|
|
} else {
|
|
GST_ELEMENT_ERROR (self, RESOURCE, NOT_FOUND,
|
|
("Specified video capture device with index %d not found",
|
|
priv->device_index), (NULL));
|
|
}
|
|
return FALSE;
|
|
}
|
|
error_open:
|
|
{
|
|
GST_ELEMENT_ERROR (self, RESOURCE, OPEN_READ,
|
|
("Failed to open device"), (NULL));
|
|
g_object_unref (device);
|
|
return FALSE;
|
|
}
|
|
}
|
|
|
|
static void
|
|
gst_ks_video_src_close_device (GstKsVideoSrc * self)
|
|
{
|
|
GstKsVideoSrcPrivate *priv = GST_KS_VIDEO_SRC_GET_PRIVATE (self);
|
|
|
|
g_assert (priv->device != NULL);
|
|
|
|
gst_ks_video_device_close (priv->device);
|
|
g_object_unref (priv->device);
|
|
priv->device = NULL;
|
|
|
|
if (priv->ksclock != NULL) {
|
|
gst_ks_clock_close (priv->ksclock);
|
|
g_object_unref (priv->ksclock);
|
|
priv->ksclock = NULL;
|
|
}
|
|
|
|
gst_ks_video_src_reset (self);
|
|
}
|
|
|
|
/*
|
|
* Worker thread that takes care of starting, configuring and stopping things.
|
|
*
|
|
* This is needed because Logitech's driver software injects a DLL that
|
|
* intercepts API functions like NtCreateFile, NtClose, NtDeviceIoControlFile
|
|
* and NtDuplicateObject so that they can provide in-place video effects to
|
|
* existing applications. Their assumption is that at least one thread tainted
|
|
* by their code stays around for the lifetime of the capture.
|
|
*/
|
|
static gpointer
|
|
gst_ks_video_src_worker_func (gpointer data)
|
|
{
|
|
GstKsVideoSrc *self = data;
|
|
GstKsVideoSrcPrivate *priv = GST_KS_VIDEO_SRC_GET_PRIVATE (self);
|
|
|
|
if (!gst_ks_video_src_open_device (self))
|
|
goto open_failed;
|
|
|
|
KS_WORKER_LOCK (priv);
|
|
priv->worker_state = KS_WORKER_STATE_READY;
|
|
KS_WORKER_NOTIFY_RESULT (priv);
|
|
|
|
while (priv->worker_state != KS_WORKER_STATE_STOPPING) {
|
|
KS_WORKER_WAIT (priv);
|
|
|
|
if (priv->worker_pending_caps != NULL) {
|
|
priv->worker_setcaps_result =
|
|
gst_ks_video_device_set_caps (priv->device,
|
|
priv->worker_pending_caps);
|
|
|
|
priv->worker_pending_caps = NULL;
|
|
KS_WORKER_NOTIFY_RESULT (priv);
|
|
} else if (priv->worker_pending_run) {
|
|
if (priv->ksclock != NULL)
|
|
gst_ks_clock_start (priv->ksclock);
|
|
priv->worker_run_result = gst_ks_video_device_set_state (priv->device,
|
|
KSSTATE_RUN, &priv->worker_error_code);
|
|
|
|
priv->worker_pending_run = FALSE;
|
|
KS_WORKER_NOTIFY_RESULT (priv);
|
|
}
|
|
}
|
|
|
|
KS_WORKER_UNLOCK (priv);
|
|
|
|
gst_ks_video_src_close_device (self);
|
|
|
|
return NULL;
|
|
|
|
/* ERRORS */
|
|
open_failed:
|
|
{
|
|
KS_WORKER_LOCK (priv);
|
|
priv->worker_state = KS_WORKER_STATE_ERROR;
|
|
KS_WORKER_NOTIFY_RESULT (priv);
|
|
KS_WORKER_UNLOCK (priv);
|
|
|
|
return NULL;
|
|
}
|
|
}
|
|
|
|
static gboolean
|
|
gst_ks_video_src_start_worker (GstKsVideoSrc * self)
|
|
{
|
|
GstKsVideoSrcPrivate *priv = GST_KS_VIDEO_SRC_GET_PRIVATE (self);
|
|
gboolean result;
|
|
|
|
g_mutex_init (&priv->worker_lock);
|
|
g_cond_init (&priv->worker_notify_cond);
|
|
g_cond_init (&priv->worker_result_cond);
|
|
|
|
priv->worker_pending_caps = NULL;
|
|
priv->worker_pending_run = FALSE;
|
|
|
|
priv->worker_state = KS_WORKER_STATE_STARTING;
|
|
priv->worker_thread =
|
|
g_thread_new ("ks-worker", gst_ks_video_src_worker_func, self);
|
|
|
|
KS_WORKER_LOCK (priv);
|
|
while (priv->worker_state < KS_WORKER_STATE_READY)
|
|
KS_WORKER_WAIT_FOR_RESULT (priv);
|
|
result = priv->worker_state == KS_WORKER_STATE_READY;
|
|
KS_WORKER_UNLOCK (priv);
|
|
|
|
return result;
|
|
}
|
|
|
|
static void
|
|
gst_ks_video_src_stop_worker (GstKsVideoSrc * self)
|
|
{
|
|
GstKsVideoSrcPrivate *priv = GST_KS_VIDEO_SRC_GET_PRIVATE (self);
|
|
|
|
KS_WORKER_LOCK (priv);
|
|
priv->worker_state = KS_WORKER_STATE_STOPPING;
|
|
KS_WORKER_NOTIFY (priv);
|
|
KS_WORKER_UNLOCK (priv);
|
|
|
|
g_thread_join (priv->worker_thread);
|
|
priv->worker_thread = NULL;
|
|
|
|
g_cond_clear (&priv->worker_result_cond);
|
|
g_cond_clear (&priv->worker_notify_cond);
|
|
g_mutex_clear (&priv->worker_lock);
|
|
}
|
|
|
|
static GstStateChangeReturn
|
|
gst_ks_video_src_change_state (GstElement * element, GstStateChange transition)
|
|
{
|
|
GstKsVideoSrc *self = GST_KS_VIDEO_SRC (element);
|
|
GstKsVideoSrcPrivate *priv = GST_KS_VIDEO_SRC_GET_PRIVATE (self);
|
|
GstStateChangeReturn ret;
|
|
|
|
switch (transition) {
|
|
case GST_STATE_CHANGE_NULL_TO_READY:
|
|
if (priv->enable_quirks)
|
|
gst_ks_video_src_apply_driver_quirks (self);
|
|
if (!gst_ks_video_src_start_worker (self))
|
|
goto open_failed;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
ret = GST_ELEMENT_CLASS (parent_class)->change_state (element, transition);
|
|
|
|
switch (transition) {
|
|
case GST_STATE_CHANGE_READY_TO_NULL:
|
|
gst_ks_video_src_stop_worker (self);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
return ret;
|
|
|
|
/* ERRORS */
|
|
open_failed:
|
|
{
|
|
gst_ks_video_src_stop_worker (self);
|
|
return GST_STATE_CHANGE_FAILURE;
|
|
}
|
|
}
|
|
|
|
static gboolean
|
|
gst_ks_video_src_set_clock (GstElement * element, GstClock * clock)
|
|
{
|
|
GstKsVideoSrc *self = GST_KS_VIDEO_SRC (element);
|
|
GstKsVideoSrcPrivate *priv = GST_KS_VIDEO_SRC_GET_PRIVATE (self);
|
|
|
|
GST_OBJECT_LOCK (element);
|
|
if (clock != NULL && priv->ksclock != NULL)
|
|
gst_ks_clock_provide_master_clock (priv->ksclock, clock);
|
|
GST_OBJECT_UNLOCK (element);
|
|
|
|
return GST_ELEMENT_CLASS (parent_class)->set_clock (element, clock);
|
|
}
|
|
|
|
static GstCaps *
|
|
gst_ks_video_src_get_caps (GstBaseSrc * basesrc, GstCaps * filter)
|
|
{
|
|
GstKsVideoSrc *self = GST_KS_VIDEO_SRC (basesrc);
|
|
GstKsVideoSrcPrivate *priv = GST_KS_VIDEO_SRC_GET_PRIVATE (self);
|
|
|
|
if (priv->device != NULL)
|
|
return gst_ks_video_device_get_available_caps (priv->device);
|
|
else
|
|
return NULL; /* BaseSrc will return template caps */
|
|
}
|
|
|
|
static gboolean
|
|
gst_ks_video_src_set_caps (GstBaseSrc * basesrc, GstCaps * caps)
|
|
{
|
|
GstKsVideoSrc *self = GST_KS_VIDEO_SRC (basesrc);
|
|
GstKsVideoSrcPrivate *priv = GST_KS_VIDEO_SRC_GET_PRIVATE (self);
|
|
|
|
if (priv->device == NULL)
|
|
return FALSE;
|
|
|
|
KS_WORKER_LOCK (priv);
|
|
priv->worker_pending_caps = caps;
|
|
KS_WORKER_NOTIFY (priv);
|
|
while (priv->worker_pending_caps == caps)
|
|
KS_WORKER_WAIT_FOR_RESULT (priv);
|
|
KS_WORKER_UNLOCK (priv);
|
|
|
|
GST_DEBUG ("Result is %d", priv->worker_setcaps_result);
|
|
return priv->worker_setcaps_result;
|
|
}
|
|
|
|
static GstCaps *
|
|
gst_ks_video_src_fixate (GstBaseSrc * basesrc, GstCaps * caps)
|
|
{
|
|
GstStructure *structure;
|
|
GstCaps *fixated_caps;
|
|
gint i;
|
|
|
|
fixated_caps = gst_caps_make_writable (caps);
|
|
|
|
for (i = 0; i < gst_caps_get_size (fixated_caps); ++i) {
|
|
structure = gst_caps_get_structure (fixated_caps, i);
|
|
gst_structure_fixate_field_nearest_int (structure, "width", G_MAXINT);
|
|
gst_structure_fixate_field_nearest_int (structure, "height", G_MAXINT);
|
|
gst_structure_fixate_field_nearest_fraction (structure, "framerate",
|
|
G_MAXINT, 1);
|
|
}
|
|
|
|
return gst_caps_fixate (fixated_caps);
|
|
}
|
|
|
|
static gboolean
|
|
gst_ks_video_src_query (GstBaseSrc * basesrc, GstQuery * query)
|
|
{
|
|
GstKsVideoSrc *self = GST_KS_VIDEO_SRC (basesrc);
|
|
GstKsVideoSrcPrivate *priv = GST_KS_VIDEO_SRC_GET_PRIVATE (self);
|
|
gboolean result = FALSE;
|
|
|
|
switch (GST_QUERY_TYPE (query)) {
|
|
case GST_QUERY_LATENCY:{
|
|
GstClockTime min_latency, max_latency;
|
|
|
|
if (priv->device == NULL)
|
|
goto beach;
|
|
|
|
result = gst_ks_video_device_get_latency (priv->device, &min_latency,
|
|
&max_latency);
|
|
if (!result)
|
|
goto beach;
|
|
|
|
GST_DEBUG_OBJECT (self, "reporting latency of min %" GST_TIME_FORMAT
|
|
" max %" GST_TIME_FORMAT,
|
|
GST_TIME_ARGS (min_latency), GST_TIME_ARGS (max_latency));
|
|
|
|
gst_query_set_latency (query, TRUE, min_latency, max_latency);
|
|
break;
|
|
}
|
|
default:
|
|
result = GST_BASE_SRC_CLASS (parent_class)->query (basesrc, query);
|
|
break;
|
|
}
|
|
|
|
beach:
|
|
return result;
|
|
}
|
|
|
|
static gboolean
|
|
gst_ks_video_src_unlock (GstBaseSrc * basesrc)
|
|
{
|
|
GstKsVideoSrc *self = GST_KS_VIDEO_SRC (basesrc);
|
|
GstKsVideoSrcPrivate *priv = GST_KS_VIDEO_SRC_GET_PRIVATE (self);
|
|
|
|
GST_DEBUG_OBJECT (self, "%s", G_STRFUNC);
|
|
|
|
gst_ks_video_device_cancel (priv->device);
|
|
return TRUE;
|
|
}
|
|
|
|
static gboolean
|
|
gst_ks_video_src_unlock_stop (GstBaseSrc * basesrc)
|
|
{
|
|
GstKsVideoSrc *self = GST_KS_VIDEO_SRC (basesrc);
|
|
GstKsVideoSrcPrivate *priv = GST_KS_VIDEO_SRC_GET_PRIVATE (self);
|
|
|
|
GST_DEBUG_OBJECT (self, "%s", G_STRFUNC);
|
|
|
|
gst_ks_video_device_cancel_stop (priv->device);
|
|
return TRUE;
|
|
}
|
|
|
|
static gboolean
|
|
gst_ks_video_src_timestamp_buffer (GstKsVideoSrc * self, GstBuffer * buf,
|
|
GstClockTime presentation_time)
|
|
{
|
|
GstKsVideoSrcPrivate *priv = GST_KS_VIDEO_SRC_GET_PRIVATE (self);
|
|
GstClockTime duration;
|
|
GstClock *clock;
|
|
GstClockTime timestamp;
|
|
|
|
/* Don't timestamp muxed streams */
|
|
if (gst_ks_video_device_stream_is_muxed (priv->device)) {
|
|
duration = timestamp = GST_CLOCK_TIME_NONE;
|
|
priv->offset++;
|
|
goto timestamp;
|
|
}
|
|
|
|
duration = gst_ks_video_device_get_duration (priv->device);
|
|
|
|
GST_OBJECT_LOCK (self);
|
|
clock = GST_ELEMENT_CLOCK (self);
|
|
if (clock != NULL) {
|
|
gst_object_ref (clock);
|
|
timestamp = GST_ELEMENT (self)->base_time;
|
|
|
|
if (GST_CLOCK_TIME_IS_VALID (presentation_time)) {
|
|
if (presentation_time > GST_ELEMENT (self)->base_time)
|
|
presentation_time -= GST_ELEMENT (self)->base_time;
|
|
else
|
|
presentation_time = 0;
|
|
}
|
|
} else {
|
|
timestamp = GST_CLOCK_TIME_NONE;
|
|
}
|
|
GST_OBJECT_UNLOCK (self);
|
|
|
|
if (clock != NULL) {
|
|
|
|
/* The time according to the current clock */
|
|
timestamp = gst_clock_get_time (clock) - timestamp;
|
|
if (timestamp > duration)
|
|
timestamp -= duration;
|
|
else
|
|
timestamp = 0;
|
|
|
|
if (GST_CLOCK_TIME_IS_VALID (presentation_time)) {
|
|
/*
|
|
* We don't use this for anything yet, need to ponder how to deal
|
|
* with pins that use an internal clock and timestamp from 0.
|
|
*/
|
|
GstClockTimeDiff diff = GST_CLOCK_DIFF (presentation_time, timestamp);
|
|
GST_DEBUG_OBJECT (self, "diff between gst and driver timestamp: %"
|
|
G_GINT64_FORMAT, diff);
|
|
}
|
|
|
|
gst_object_unref (clock);
|
|
clock = NULL;
|
|
|
|
/* Unless it's the first frame, align the current timestamp on a multiple
|
|
* of duration since the previous */
|
|
if (GST_CLOCK_TIME_IS_VALID (priv->prev_ts)) {
|
|
GstClockTime delta;
|
|
guint delta_remainder, delta_offset;
|
|
|
|
/* REVISIT: I've seen this happen with the GstSystemClock on Windows,
|
|
* scary... */
|
|
if (timestamp < priv->prev_ts) {
|
|
GST_INFO_OBJECT (self, "clock is ticking backwards");
|
|
return FALSE;
|
|
}
|
|
|
|
/* Round to a duration boundary */
|
|
delta = timestamp - priv->prev_ts;
|
|
delta_remainder = delta % duration;
|
|
|
|
if (delta_remainder < duration / 3)
|
|
timestamp -= delta_remainder;
|
|
else
|
|
timestamp += duration - delta_remainder;
|
|
|
|
/* How many frames are we off then? */
|
|
delta = timestamp - priv->prev_ts;
|
|
delta_offset = delta / duration;
|
|
|
|
if (delta_offset == 1) /* perfect */
|
|
GST_BUFFER_FLAG_UNSET (buf, GST_BUFFER_FLAG_DISCONT);
|
|
else if (delta_offset > 1) {
|
|
guint lost = delta_offset - 1;
|
|
GST_INFO_OBJECT (self, "lost %d frame%s, setting discont flag",
|
|
lost, (lost > 1) ? "s" : "");
|
|
GST_BUFFER_FLAG_SET (buf, GST_BUFFER_FLAG_DISCONT);
|
|
} else if (delta_offset == 0) { /* overproduction, skip this frame */
|
|
GST_INFO_OBJECT (self, "skipping frame");
|
|
return FALSE;
|
|
}
|
|
|
|
priv->offset += delta_offset;
|
|
}
|
|
|
|
priv->prev_ts = timestamp;
|
|
}
|
|
|
|
timestamp:
|
|
GST_BUFFER_OFFSET (buf) = priv->offset;
|
|
GST_BUFFER_OFFSET_END (buf) = GST_BUFFER_OFFSET (buf) + 1;
|
|
GST_BUFFER_PTS (buf) = timestamp;
|
|
GST_BUFFER_DTS (buf) = GST_CLOCK_TIME_NONE;
|
|
GST_BUFFER_DURATION (buf) = duration;
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
static void
|
|
gst_ks_video_src_update_statistics (GstKsVideoSrc * self)
|
|
{
|
|
GstKsVideoSrcPrivate *priv = GST_KS_VIDEO_SRC_GET_PRIVATE (self);
|
|
GstClock *clock;
|
|
|
|
GST_OBJECT_LOCK (self);
|
|
clock = GST_ELEMENT_CLOCK (self);
|
|
if (clock != NULL)
|
|
gst_object_ref (clock);
|
|
GST_OBJECT_UNLOCK (self);
|
|
|
|
if (clock != NULL) {
|
|
GstClockTime now = gst_clock_get_time (clock);
|
|
gst_object_unref (clock);
|
|
|
|
priv->count++;
|
|
|
|
if (GST_CLOCK_TIME_IS_VALID (priv->last_sampling)) {
|
|
if (now - priv->last_sampling >= GST_SECOND) {
|
|
GST_OBJECT_LOCK (self);
|
|
priv->fps = priv->count;
|
|
GST_OBJECT_UNLOCK (self);
|
|
|
|
g_object_notify (G_OBJECT (self), "fps");
|
|
|
|
priv->last_sampling = now;
|
|
priv->count = 0;
|
|
}
|
|
} else {
|
|
priv->last_sampling = now;
|
|
}
|
|
}
|
|
}
|
|
|
|
static GstFlowReturn
|
|
gst_ks_video_src_create (GstPushSrc * pushsrc, GstBuffer ** buf)
|
|
{
|
|
GstKsVideoSrc *self = GST_KS_VIDEO_SRC (pushsrc);
|
|
GstKsVideoSrcPrivate *priv = GST_KS_VIDEO_SRC_GET_PRIVATE (self);
|
|
GstFlowReturn result;
|
|
GstClockTime presentation_time;
|
|
gulong error_code;
|
|
gchar *error_str;
|
|
|
|
g_assert (priv->device != NULL);
|
|
|
|
if (!gst_ks_video_device_has_caps (priv->device))
|
|
goto error_no_caps;
|
|
|
|
if (G_UNLIKELY (!priv->running)) {
|
|
KS_WORKER_LOCK (priv);
|
|
priv->worker_pending_run = TRUE;
|
|
KS_WORKER_NOTIFY (priv);
|
|
while (priv->worker_pending_run)
|
|
KS_WORKER_WAIT_FOR_RESULT (priv);
|
|
priv->running = priv->worker_run_result;
|
|
error_code = priv->worker_error_code;
|
|
KS_WORKER_UNLOCK (priv);
|
|
|
|
if (!priv->running)
|
|
goto error_start_capture;
|
|
}
|
|
|
|
do {
|
|
if (*buf != NULL) {
|
|
gst_buffer_unref (*buf);
|
|
*buf = NULL;
|
|
}
|
|
|
|
result = gst_ks_video_device_read_frame (priv->device, buf,
|
|
&presentation_time, &error_code, &error_str);
|
|
if (G_UNLIKELY (result != GST_FLOW_OK))
|
|
goto error_read_frame;
|
|
}
|
|
while (!gst_ks_video_src_timestamp_buffer (self, *buf, presentation_time));
|
|
|
|
if (G_UNLIKELY (priv->do_stats))
|
|
gst_ks_video_src_update_statistics (self);
|
|
|
|
if (!gst_ks_video_device_postprocess_frame (priv->device, *buf)) {
|
|
GST_ELEMENT_ERROR (self, RESOURCE, FAILED, ("Postprocessing failed"),
|
|
("Postprocessing failed"));
|
|
return GST_FLOW_ERROR;
|
|
}
|
|
|
|
return GST_FLOW_OK;
|
|
|
|
/* ERRORS */
|
|
error_no_caps:
|
|
{
|
|
GST_ELEMENT_ERROR (self, CORE, NEGOTIATION,
|
|
("not negotiated"), ("maybe setcaps failed?"));
|
|
|
|
return GST_FLOW_ERROR;
|
|
}
|
|
error_start_capture:
|
|
{
|
|
const gchar *debug_str = "failed to change pin state to KSSTATE_RUN";
|
|
|
|
switch (error_code) {
|
|
case ERROR_FILE_NOT_FOUND:
|
|
GST_ELEMENT_ERROR (self, RESOURCE, NOT_FOUND,
|
|
("failed to start capture (device unplugged)"), ("%s", debug_str));
|
|
break;
|
|
case ERROR_NO_SYSTEM_RESOURCES:
|
|
GST_ELEMENT_ERROR (self, RESOURCE, BUSY,
|
|
("failed to start capture (device already in use)"), ("%s",
|
|
debug_str));
|
|
break;
|
|
default:
|
|
GST_ELEMENT_ERROR (self, RESOURCE, FAILED,
|
|
("failed to start capture (0x%08x)", (guint) error_code), ("%s",
|
|
debug_str));
|
|
break;
|
|
}
|
|
|
|
return GST_FLOW_ERROR;
|
|
}
|
|
error_read_frame:
|
|
{
|
|
if (result == GST_FLOW_ERROR) {
|
|
if (error_str != NULL) {
|
|
GST_ELEMENT_ERROR (self, RESOURCE, READ,
|
|
("read failed: %s [0x%08x]", error_str, (guint) error_code),
|
|
("gst_ks_video_device_read_frame failed"));
|
|
}
|
|
} else if (result == GST_FLOW_CUSTOM_ERROR) {
|
|
GST_ELEMENT_ERROR (self, RESOURCE, READ,
|
|
("read failed"), ("gst_ks_video_device_read_frame failed"));
|
|
}
|
|
|
|
g_free (error_str);
|
|
|
|
return result;
|
|
}
|
|
}
|
|
|
|
static GstBuffer *
|
|
gst_ks_video_src_alloc_buffer (guint size, guint alignment, gpointer user_data)
|
|
{
|
|
GstKsVideoSrc *self = GST_KS_VIDEO_SRC (user_data);
|
|
GstBuffer *buf;
|
|
GstAllocationParams params = { 0, alignment - 1, 0, 0, };
|
|
|
|
buf = gst_buffer_new_allocate (NULL, size, ¶ms);
|
|
if (buf == NULL)
|
|
goto error_alloc_buffer;
|
|
|
|
return buf;
|
|
|
|
error_alloc_buffer:
|
|
{
|
|
GST_ELEMENT_ERROR (self, CORE, PAD, ("alloc_buffer failed"), (NULL));
|
|
|
|
return NULL;
|
|
}
|
|
}
|
|
|
|
static gboolean
|
|
plugin_init (GstPlugin * plugin)
|
|
{
|
|
GST_DEBUG_CATEGORY_INIT (gst_ks_debug, "ksvideosrc",
|
|
0, "Kernel streaming video source");
|
|
|
|
if (!gst_element_register (plugin, "ksvideosrc",
|
|
GST_RANK_NONE, GST_TYPE_KS_VIDEO_SRC))
|
|
return FALSE;
|
|
|
|
if (!gst_device_provider_register (plugin, "ksdeviceprovider",
|
|
GST_RANK_PRIMARY, GST_TYPE_KS_DEVICE_PROVIDER))
|
|
return FALSE;
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
GST_PLUGIN_DEFINE (GST_VERSION_MAJOR,
|
|
GST_VERSION_MINOR,
|
|
winks,
|
|
"Windows kernel streaming plugin",
|
|
plugin_init, VERSION, "LGPL", "GStreamer", "http://gstreamer.net/")
|