Browse Source

Merge pull request #49 from fryshorts/linux-input

linux input plugins for desktop and audio capture
Jim 11 years ago
parent
commit
88e4e7f1be

+ 2 - 2
obs/obs-app.cpp

@@ -188,8 +188,8 @@ void OBSApp::OBSInit()
 #define INPUT_AUDIO_SOURCE  "wasapi_input_capture"
 #define OUTPUT_AUDIO_SOURCE "wasapi_output_capture"
 #else
-#define INPUT_AUDIO_SOURCE  ""
-#define OUTPUT_AUDIO_SOURCE ""
+#define INPUT_AUDIO_SOURCE  "pulse_input_capture"
+#define OUTPUT_AUDIO_SOURCE "pulse_output_capture"
 #endif
 
 const char *OBSApp::InputAudioSource() const

+ 3 - 0
obs/window-basic-main.cpp

@@ -172,6 +172,9 @@ void OBSBasic::OBSInit()
 #elif _WIN32
 	obs_load_module("win-wasapi");
 	obs_load_module("win-capture");
+#else
+	obs_load_module("linux-xshm");
+	obs_load_module("linux-pulseaudio");
 #endif
 
 	ResetAudioDevices();

+ 3 - 0
plugins/CMakeLists.txt

@@ -6,6 +6,9 @@ if(WIN32)
 	add_subdirectory(win-capture)
 elseif(APPLE)
 	add_subdirectory(mac-capture)
+elseif("${CMAKE_SYSTEM_NAME}" MATCHES "Linux")
+	add_subdirectory(linux-xshm)
+	add_subdirectory(linux-pulseaudio)
 endif()
 
 add_subdirectory(obs-ffmpeg)

+ 18 - 0
plugins/linux-pulseaudio/CMakeLists.txt

@@ -0,0 +1,18 @@
+find_package(PulseAudio REQUIRED)
+
+include_directories(SYSTEM "${CMAKE_SOURCE_DIR}/libobs")
+
+set(linux-pulseaudio_SOURCES
+	linux-pulseaudio.c
+	pulse-input.c
+)
+    
+add_library(linux-pulseaudio MODULE
+	${linux-pulseaudio_SOURCES}
+)
+target_link_libraries(linux-pulseaudio
+	libobs
+	${PULSEAUDIO_LIBRARY}
+)
+
+install_obs_plugin(linux-pulseaudio)

+ 30 - 0
plugins/linux-pulseaudio/linux-pulseaudio.c

@@ -0,0 +1,30 @@
+/*
+Copyright (C) 2014 by Leonhard Oelke <[email protected]>
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 2 of the License, or
+(at your option) any later version.
+
+This program 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 General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+#include <obs-module.h>
+
+OBS_DECLARE_MODULE()
+
+extern struct obs_source_info pulse_input_capture;
+extern struct obs_source_info pulse_output_capture;
+
+bool obs_module_load(uint32_t obs_version)
+{
+	UNUSED_PARAMETER(obs_version);
+	obs_register_source(&pulse_input_capture);
+	obs_register_source(&pulse_output_capture);
+	return true;
+}

+ 682 - 0
plugins/linux-pulseaudio/pulse-input.c

@@ -0,0 +1,682 @@
+/*
+Copyright (C) 2014 by Leonhard Oelke <[email protected]>
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 2 of the License, or
+(at your option) any later version.
+
+This program 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 General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+#include <util/bmem.h>
+#include <util/threading.h>
+#include <util/platform.h>
+
+#include <pulse/thread-mainloop.h>
+#include <pulse/mainloop.h>
+#include <pulse/context.h>
+#include <pulse/introspect.h>
+#include <pulse/stream.h>
+#include <pulse/error.h>
+
+#include <obs.h>
+
+#define PULSE_DATA(voidptr) struct pulse_data *data = voidptr;
+
+/*
+ * delay in usecs before starting to record, this eliminates problems with
+ * pulse audio sending weird data/timestamps when the stream is connected
+ * 
+ * for more information see:
+ * github.com/MaartenBaert/ssr/blob/master/src/AV/Input/PulseAudioInput.cpp
+ */
+const uint64_t pulse_start_delay = 100000;
+
+struct pulse_data {
+	pthread_t thread;
+	os_event_t event;
+	obs_source_t source;
+	
+	enum speaker_layout speakers;
+	pa_sample_format_t format;
+	uint_fast32_t samples_per_sec;
+	uint_fast8_t channels;
+	
+	uint_fast32_t bytes_per_frame;
+	
+	pa_mainloop *mainloop;
+	pa_context *context;
+	pa_stream *stream;
+	pa_proplist *props;
+};
+
+struct pulse_context_change {
+	pa_threaded_mainloop *mainloop;
+	pa_context_state_t state;
+};
+
+struct pulse_enumerate {
+	pa_threaded_mainloop *mainloop;
+	obs_property_t devices;
+	bool input;
+};
+
+/*
+ * get obs from pulse audio format
+ */
+static enum audio_format pulse_to_obs_audio_format(
+	pa_sample_format_t format)
+{
+	switch (format) {
+		case PA_SAMPLE_U8:
+			return AUDIO_FORMAT_U8BIT;
+		case PA_SAMPLE_S16LE:
+			return AUDIO_FORMAT_16BIT;
+		case PA_SAMPLE_S24_32LE:
+			return AUDIO_FORMAT_32BIT;
+		case PA_SAMPLE_FLOAT32LE:
+			return AUDIO_FORMAT_FLOAT;
+		default:
+			return AUDIO_FORMAT_UNKNOWN;
+	}
+	
+	return AUDIO_FORMAT_UNKNOWN;
+}
+
+/*
+ * get the number of frames from bytes and current format
+ */
+static uint_fast32_t frames_to_bytes(struct pulse_data *data, size_t bytes)
+{
+	return (bytes / data->bytes_per_frame);
+}
+
+/*
+ * get the buffer size needed for length msec with current settings
+ */
+static uint_fast32_t get_buffer_size(struct pulse_data *data,
+	uint_fast32_t length)
+{
+	return (length * data->samples_per_sec * data->bytes_per_frame) / 1000;
+}
+
+/*
+ * Get latency for a pulse audio stream
+ */
+static int pulse_get_stream_latency(pa_stream *stream, int64_t *latency)
+{
+	int ret;
+	int sign;
+	pa_usec_t abs;
+	
+	ret = pa_stream_get_latency(stream, &abs, &sign);
+	*latency = (sign) ? -(int64_t) abs : (int64_t) abs;
+	return ret;
+}
+
+/*
+ * Iterate the mainloop
+ * 
+ * The custom implementation gives better performance than the function
+ * provided by pulse audio, maybe due to the timeout set in prepare ?
+ */
+static void pulse_iterate(struct pulse_data *data)
+{
+	if (pa_mainloop_prepare(data->mainloop, 1000) < 0) {
+		blog(LOG_ERROR, "Unable to prepare main loop");
+		return;
+	}
+	if (pa_mainloop_poll(data->mainloop) < 0) {
+		blog(LOG_ERROR, "Unable to poll main loop");
+		return;
+	}
+	if (pa_mainloop_dispatch(data->mainloop) < 0)
+		blog(LOG_ERROR, "Unable to dispatch main loop");
+}
+
+/*
+ * Server info callback, this is called from pa_mainloop_dispatch
+ * TODO: how to free the server info struct ?
+ */
+static void pulse_get_server_info_cb(pa_context *c, const pa_server_info *i,
+	void *userdata)
+{
+	UNUSED_PARAMETER(c);
+	PULSE_DATA(userdata);
+	
+	const pa_sample_spec *spec = &i->sample_spec;
+	data->format = spec->format;
+	data->samples_per_sec = spec->rate;
+	
+	blog(LOG_DEBUG, "pulse-input: Default format: %s, %u Hz, %u channels",
+		pa_sample_format_to_string(spec->format),
+		spec->rate,
+		spec->channels);
+}
+
+/*
+ * Request pulse audio server info
+ * TODO: handle failures ?
+ */
+static int pulse_get_server_info(struct pulse_data *data)
+{
+	pa_server_info_cb_t cb = pulse_get_server_info_cb;
+	pa_operation *op = pa_context_get_server_info(data->context, cb, data);
+	
+	for(;;) {
+		pulse_iterate(data);
+		pa_operation_state_t state = pa_operation_get_state(op);
+		if (state == PA_OPERATION_DONE) {
+			pa_operation_unref(op);
+			break;
+		}
+	}
+	
+	return 0;
+}
+
+/*
+ * Create a new pulse audio main loop and connect to the server
+ * 
+ * Returns a negative value on error
+ */
+static int pulse_connect(struct pulse_data *data)
+{
+	data->mainloop = pa_mainloop_new();
+	if (!data->mainloop) {
+		blog(LOG_ERROR, "pulse-input: Unable to create main loop");
+		return -1;
+	}
+	
+	data->context = pa_context_new_with_proplist(
+		pa_mainloop_get_api(data->mainloop), "OBS Studio", data->props);
+	if (!data->context) {
+		blog(LOG_ERROR, "pulse-input: Unable to create context");
+		return -1;
+	}
+	
+	int status = pa_context_connect(
+		data->context, NULL, PA_CONTEXT_NOAUTOSPAWN, NULL);
+	if (status < 0) {
+		blog(LOG_ERROR, "pulse-input: Unable to connect! Status: %d",
+		     status);
+		return -1;
+	}
+	
+	// wait until connected
+	for (;;) {
+		pulse_iterate(data);
+		pa_context_state_t state = pa_context_get_state(data->context);
+		if (state == PA_CONTEXT_READY) {
+			blog(LOG_DEBUG, "pulse-input: Context ready");
+			break;
+		}
+		if (!PA_CONTEXT_IS_GOOD(state)) {
+			blog(LOG_ERROR, "pulse-input: Context connect failed");
+			return -1;
+		}
+	}
+	
+	return 0;
+}
+
+/*
+ * Disconnect from the pulse audio server and destroy the main loop
+ */
+static void pulse_disconnect(struct pulse_data *data)
+{
+	if (data->context) {
+		pa_context_disconnect(data->context);
+		pa_context_unref(data->context);
+	}
+	
+	if (data->mainloop)
+		pa_mainloop_free(data->mainloop);
+}
+
+/*
+ * Create a new pulse audio stream and connect to it
+ * 
+ * Return a negative value on error
+ */
+static int pulse_connect_stream(struct pulse_data *data)
+{
+	pa_sample_spec spec;
+	spec.format = data->format;
+	spec.rate = data->samples_per_sec;
+	spec.channels = get_audio_channels(data->speakers);
+	
+	if (!pa_sample_spec_valid(&spec)) {
+		blog(LOG_ERROR, "pulse-input: Sample spec is not valid");
+		return -1;
+	}
+	
+	data->bytes_per_frame = pa_frame_size(&spec);
+	blog(LOG_DEBUG, "pulse-input: %u bytes per frame",
+	     (unsigned int) data->bytes_per_frame);
+	
+	pa_buffer_attr attr;
+	attr.fragsize = get_buffer_size(data, 250);
+	attr.maxlength = (uint32_t) -1;
+	attr.minreq = (uint32_t) -1;
+	attr.prebuf = (uint32_t) -1;
+	attr.tlength = (uint32_t) -1;
+	
+	data->stream = pa_stream_new_with_proplist(data->context,
+		obs_source_getname(data->source), &spec, NULL, data->props);
+	if (!data->stream) {
+		blog(LOG_ERROR, "pulse-input: Unable to create stream");
+		return -1;
+	}
+	pa_stream_flags_t flags =
+		PA_STREAM_INTERPOLATE_TIMING
+		| PA_STREAM_AUTO_TIMING_UPDATE
+		| PA_STREAM_ADJUST_LATENCY;
+	if (pa_stream_connect_record(data->stream, NULL, &attr, flags) < 0) {
+		blog(LOG_ERROR, "pulse-input: Unable to connect to stream");
+		return -1;
+	}
+	
+	for (;;) {
+		pulse_iterate(data);
+		pa_stream_state_t state = pa_stream_get_state(data->stream);
+		if (state == PA_STREAM_READY) {
+			blog(LOG_DEBUG, "pulse-input: Stream ready");
+			break;
+		}
+		if (!PA_STREAM_IS_GOOD(state)) {
+			blog(LOG_ERROR, "pulse-input: Stream connect failed");
+			return -1;
+		}
+	}
+	
+	return 0;
+}
+
+/*
+ * Disconnect from the pulse audio stream
+ */
+static void pulse_diconnect_stream(struct pulse_data *data)
+{
+	if (data->stream) {
+		pa_stream_disconnect(data->stream);
+		pa_stream_unref(data->stream);
+	}
+}
+
+/*
+ * Loop to skip the first few samples of a stream
+ */
+static int pulse_skip(struct pulse_data *data)
+{
+	uint64_t skip = 1;
+	const void *frames;
+	size_t bytes;
+	uint64_t pa_time;
+	
+	while (os_event_try(data->event) == EAGAIN) {
+		pulse_iterate(data);
+		pa_stream_peek(data->stream, &frames, &bytes);
+		
+		if (!bytes)
+			continue;
+		if (!frames || pa_stream_get_time(data->stream, &pa_time) < 0) {
+			pa_stream_drop(data->stream);
+			continue;
+		}
+		
+		if (skip == 1 && pa_time)
+			skip = pa_time;
+		if (skip + pulse_start_delay < pa_time)
+			return 0;
+		
+		pa_stream_drop(data->stream);
+	}
+	
+	return -1;
+}
+
+/*
+ * Worker thread to get audio data
+ * 
+ * Will run until signaled
+ */
+static void *pulse_thread(void *vptr)
+{
+	PULSE_DATA(vptr);
+	
+	if (pulse_connect(data) < 0)
+		return NULL;
+	if (pulse_get_server_info(data) < 0)
+		return NULL;
+	if (pulse_connect_stream(data) < 0)
+		return NULL;
+	
+	if (pulse_skip(data) < 0)
+		return NULL;
+	
+	blog(LOG_DEBUG, "pulse-input: Start recording");
+	
+	const void *frames;
+	size_t bytes;
+	uint64_t pa_time;
+	int64_t pa_latency;
+	
+	struct source_audio out;
+	out.speakers = data->speakers;
+	out.samples_per_sec = data->samples_per_sec;
+	out.format = pulse_to_obs_audio_format(data->format);
+	
+	while (os_event_try(data->event) == EAGAIN) {
+		pulse_iterate(data);
+		
+		pa_stream_peek(data->stream, &frames, &bytes);
+		
+		// check if we got data
+		if (!bytes)
+			continue;
+		if (!frames) {
+			blog(LOG_DEBUG,
+				"pulse-input: Got audio hole of %u bytes",
+				(unsigned int) bytes);
+			pa_stream_drop(data->stream);
+			continue;
+		}
+		
+		if (pa_stream_get_time(data->stream, &pa_time) < 0) {
+			blog(LOG_ERROR,
+				"pulse-input: Failed to get timing info !");
+			pa_stream_drop(data->stream);
+			continue;
+		}
+		
+		pulse_get_stream_latency(data->stream, &pa_latency);
+		
+		out.data[0] = (uint8_t *) frames;
+		out.frames = frames_to_bytes(data, bytes);
+		out.timestamp = (pa_time - pa_latency) * 1000;
+		obs_source_output_audio(data->source, &out);
+		
+		pa_stream_drop(data->stream);
+	}
+	
+	pulse_diconnect_stream(data);
+	pulse_disconnect(data);
+	
+	return NULL;
+}
+
+/*
+ * Create a new pulseaudio context
+ */
+static pa_context *pulse_context_create(pa_threaded_mainloop *m)
+{
+	pa_context *c;
+	pa_proplist *p;
+	
+	p = pa_proplist_new();
+	pa_proplist_sets(p, PA_PROP_APPLICATION_NAME, "OBS Studio");
+	pa_proplist_sets(p, PA_PROP_APPLICATION_ICON_NAME, "application-exit");
+	pa_proplist_sets(p, PA_PROP_MEDIA_ROLE, "production");
+	
+	pa_threaded_mainloop_lock(m);
+	c = pa_context_new_with_proplist(pa_threaded_mainloop_get_api(m),
+		"OBS Studio", p);
+	pa_threaded_mainloop_unlock(m);
+	
+	pa_proplist_free(p);
+	
+	return c;
+}
+
+/**
+ * Context state callback
+ */
+static void pulse_context_state_changed(pa_context *c, void *userdata)
+{
+	struct pulse_context_change *ctx =
+		(struct pulse_context_change *) userdata;
+	ctx->state = pa_context_get_state(c);
+	
+	pa_threaded_mainloop_signal(ctx->mainloop, 0);
+}
+
+/*
+ * Connect context
+ */
+static int pulse_context_connect(pa_threaded_mainloop *m, pa_context *c)
+{
+	int status = 0;
+	struct pulse_context_change ctx;
+	ctx.mainloop = m;
+	ctx.state = PA_CONTEXT_UNCONNECTED;
+	
+	pa_threaded_mainloop_lock(m);
+	pa_context_set_state_callback(c, pulse_context_state_changed,
+		(void *) &ctx);
+	
+	status = pa_context_connect(c, NULL, PA_CONTEXT_NOAUTOSPAWN, NULL);
+	if (status < 0) {
+		blog(LOG_ERROR, "pulse-input: Unable to connect! Status: %d",
+		     status);
+	}
+	else {
+		for (;;) {
+			if (ctx.state == PA_CONTEXT_READY) {
+				blog(LOG_DEBUG, "pulse-input: Context Ready");
+				break;
+			}
+			if (!PA_CONTEXT_IS_GOOD(ctx.state)) {
+				blog(LOG_ERROR,
+				     "pulse-input: Context connect failed !");
+				status = -1;
+				break;
+			}
+			pa_threaded_mainloop_wait(m);
+		}
+	}
+	
+	pa_threaded_mainloop_unlock(m);
+	return status;
+}
+
+/*
+ * Source properties callback
+ */
+static void pulse_source_info(pa_context *c, const pa_source_info *i, int eol,
+	void *userdata)
+{
+	UNUSED_PARAMETER(c);
+	
+	if (eol != 0)
+		return;
+	
+	struct pulse_enumerate *e = (struct pulse_enumerate *) userdata;
+	
+	if ((e->input) ^ (i->monitor_of_sink == PA_INVALID_INDEX))
+		return;
+	
+	blog(LOG_DEBUG, "pulse-input: Got source #%u '%s'",
+	     i->index, i->description);
+	
+	obs_property_list_add_item(e->devices, i->description, i->name);
+	
+	pa_threaded_mainloop_signal(e->mainloop, 0);
+}
+
+/*
+ * enumerate input/output devices
+ */
+static void pulse_enumerate_devices(obs_properties_t props, bool input)
+{
+	pa_context *c;
+	pa_operation *op;
+	pa_threaded_mainloop *m = pa_threaded_mainloop_new();
+	struct pulse_enumerate e;
+	
+	e.mainloop = m;
+	e.devices = obs_properties_add_list(props, "device_id", "Device",
+		OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_STRING);
+	e.input = input;
+	
+	pa_threaded_mainloop_start(m);
+	c = pulse_context_create(m);
+	
+	if (pulse_context_connect(m, c) < 0)
+		goto fail;
+	
+	pa_threaded_mainloop_lock(m);
+	
+	op = pa_context_get_source_info_list(c, pulse_source_info, (void *) &e);
+	while (pa_operation_get_state(op) == PA_OPERATION_RUNNING)
+		pa_threaded_mainloop_wait(m);
+	pa_operation_unref(op);
+	
+	pa_threaded_mainloop_unlock(m);
+	
+	pa_context_disconnect(c);
+fail:
+	pa_context_unref(c);
+	pa_threaded_mainloop_stop(m);
+	pa_threaded_mainloop_free(m);
+}
+
+/*
+ * get plugin properties
+ */
+static obs_properties_t pulse_properties(const char *locale, bool input)
+{
+	UNUSED_PARAMETER(locale);
+	
+	blog(LOG_DEBUG, "pulse-input: properties requested !");
+	
+	obs_properties_t props = obs_properties_create();
+	
+	pulse_enumerate_devices(props, input);
+	
+	return props;
+}
+
+static obs_properties_t pulse_input_properties(const char *locale)
+{
+	return pulse_properties(locale, true);
+}
+
+static obs_properties_t pulse_output_properties(const char *locale)
+{
+	return pulse_properties(locale, false);
+}
+
+/*
+ * get plugin defaults
+ */
+static void pulse_defaults(obs_data_t settings)
+{
+	obs_data_set_default_string(settings, "device_id", "default");
+}
+
+/*
+ * Returns the name of the plugin
+ */
+static const char *pulse_input_getname(const char *locale)
+{
+	UNUSED_PARAMETER(locale);
+	return "Pulse Audio Input Capture";
+}
+
+static const char *pulse_output_getname(const char *locale)
+{
+	UNUSED_PARAMETER(locale);
+	return "Pulse Audio Output Capture";
+}
+
+/*
+ * Destroy the plugin object and free all memory
+ */
+static void pulse_destroy(void *vptr)
+{
+	PULSE_DATA(vptr);
+	
+	if (!data)
+		return;
+	
+	if (data->thread) {
+		void *ret;
+		os_event_signal(data->event);
+		pthread_join(data->thread, &ret);
+	}
+	
+	os_event_destroy(data->event);
+	
+	pa_proplist_free(data->props);
+	
+	blog(LOG_DEBUG, "pulse-input: Input destroyed");
+	
+	bfree(data);
+}
+
+/*
+ * Create the plugin object
+ */
+static void *pulse_create(obs_data_t settings, obs_source_t source)
+{
+	UNUSED_PARAMETER(settings);
+	
+	struct pulse_data *data = bmalloc(sizeof(struct pulse_data));
+	memset(data, 0, sizeof(struct pulse_data));
+	
+	data->source = source;
+	data->speakers = SPEAKERS_STEREO;
+	
+	blog(LOG_DEBUG, "pulse-input: obs wants '%s'",
+		obs_data_getstring(settings, "device_id"));
+	
+	/* TODO: use obs-studio icon */
+	data->props = pa_proplist_new();
+	pa_proplist_sets(data->props, PA_PROP_APPLICATION_NAME,
+		"OBS Studio");
+	pa_proplist_sets(data->props, PA_PROP_APPLICATION_ICON_NAME,
+		"application-exit");
+	pa_proplist_sets(data->props, PA_PROP_MEDIA_ROLE,
+		"production");
+	
+	if (os_event_init(&data->event, OS_EVENT_TYPE_MANUAL) != 0)
+		goto fail;
+	if (pthread_create(&data->thread, NULL, pulse_thread, data) != 0)
+		goto fail;
+	
+	return data;
+	
+fail:
+	pulse_destroy(data);
+	return NULL;
+}
+
+struct obs_source_info pulse_input_capture = {
+	.id           = "pulse_input_capture",
+	.type         = OBS_SOURCE_TYPE_INPUT,
+	.output_flags = OBS_SOURCE_AUDIO,
+	.getname      = pulse_input_getname,
+	.create       = pulse_create,
+	.destroy      = pulse_destroy,
+	.defaults     = pulse_defaults,
+	.properties   = pulse_input_properties
+};
+
+struct obs_source_info pulse_output_capture = {
+	.id           = "pulse_output_capture",
+	.type         = OBS_SOURCE_TYPE_INPUT,
+	.output_flags = OBS_SOURCE_AUDIO,
+	.getname      = pulse_output_getname,
+	.create       = pulse_create,
+	.destroy      = pulse_destroy,
+	.defaults     = pulse_defaults,
+	.properties   = pulse_output_properties
+};

+ 25 - 0
plugins/linux-xshm/CMakeLists.txt

@@ -0,0 +1,25 @@
+find_package(X11 REQUIRED)
+
+include_directories(SYSTEM "${CMAKE_SOURCE_DIR}/libobs")
+
+set(linux-xshm_SOURCES
+	linux-xshm.c
+	xcursor.c
+	xshm-input.c
+)
+set(linux-xshm_HEADERS
+	xcursor.h
+)
+    
+add_library(linux-xshm MODULE
+	${linux-xshm_SOURCES}
+	${linux-xshm_HEADERS}
+)
+target_link_libraries(linux-xshm
+	libobs
+	${X11_LIBRARIES}
+	${X11_XShm_LIB}
+	${X11_Xfixes_LIB}
+)
+
+install_obs_plugin(linux-xshm)

+ 28 - 0
plugins/linux-xshm/linux-xshm.c

@@ -0,0 +1,28 @@
+/*
+Copyright (C) 2014 by Leonhard Oelke <[email protected]>
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 2 of the License, or
+(at your option) any later version.
+
+This program 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 General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+#include <obs-module.h>
+
+OBS_DECLARE_MODULE()
+
+extern struct obs_source_info xshm_input;
+
+bool obs_module_load(uint32_t obs_version)
+{
+	UNUSED_PARAMETER(obs_version);
+	obs_register_source(&xshm_input);
+	return true;
+}

+ 112 - 0
plugins/linux-xshm/xcursor.c

@@ -0,0 +1,112 @@
+/*
+Copyright (C) 2014 by Leonhard Oelke <[email protected]>
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 2 of the License, or
+(at your option) any later version.
+
+This program 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 General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+#include <stdint.h>
+#include <X11/extensions/Xfixes.h>
+
+#include <util/bmem.h>
+#include "xcursor.h"
+
+/*
+ * Get pixel data for the cursor
+ * 
+ * XFixes has the data defined as unsigned long, so we can not use memcpy.
+ * Theres a lot of talk about this in other implementation and they tend to
+ * be really complicated, but this naive approach seems to work fine ...
+ */
+static uint32_t *xcursor_pixels(XFixesCursorImage *xc) {
+	uint_fast32_t size = xc->width * xc->height;
+	uint32_t *pixels = bmalloc(size * sizeof(uint32_t));
+	
+	for (uint_fast32_t i = 0; i < size; ++i)
+		pixels[i] = (uint32_t) xc->pixels[i];
+	
+	return pixels;
+}
+
+/*
+ * Create the cursor texture, either by updating if the new cursor has the same
+ * size or by creating a new texture if the size is different
+ */ 
+static void xcursor_create(xcursor_t *data, XFixesCursorImage *xc) {
+	uint32_t *pixels = xcursor_pixels(xc);
+
+	if (data->tex
+	&& data->last_height == xc->width
+	&& data->last_width == xc->height) {
+		texture_setimage(data->tex, (void **) pixels,
+			xc->width * sizeof(uint32_t), False);
+	} else {
+		if (data->tex)
+			texture_destroy(data->tex);
+			
+		data->tex = gs_create_texture(xc->width, xc->height,
+			GS_RGBA, 1, (const void **) &pixels, GS_DYNAMIC);
+	}
+	
+	bfree(pixels);
+	
+	data->last_serial = xc->cursor_serial;
+	data->last_width = xc->width;
+	data->last_height = xc->height;
+}
+
+xcursor_t *xcursor_init(Display *dpy) {
+	xcursor_t *data = bmalloc(sizeof(xcursor_t));
+	memset(data, 0, sizeof(xcursor_t));
+	
+	data->dpy = dpy;
+	xcursor_tick(data);
+	
+	return data;
+}
+
+void xcursor_destroy(xcursor_t *data) {
+	if (data->tex)
+		texture_destroy(data->tex);
+	bfree(data);
+}
+
+void xcursor_tick(xcursor_t *data) {
+	XFixesCursorImage *xc = XFixesGetCursorImage(data->dpy);
+	
+	if (!data->tex || data->last_serial != xc->cursor_serial)
+		xcursor_create(data, xc);
+	data->pos_x = -1.0 * (xc->x - xc->xhot);
+	data->pos_y = -1.0 * (xc->y - xc->yhot);
+	
+	XFree(xc);
+}
+
+void xcursor_render(xcursor_t *data) {
+	/* TODO: why do i need effects ? */
+	effect_t effect  = gs_geteffect();
+	eparam_t image = effect_getparambyname(effect, "image");
+	
+	effect_settexture(effect, image, data->tex);
+	
+	gs_matrix_push();
+	
+	gs_matrix_translate3f(data->pos_x, data->pos_y, 0);
+	
+	gs_enable_blending(True);
+	gs_blendfunction(GS_BLEND_ONE, GS_BLEND_INVSRCALPHA);
+	gs_draw_sprite(data->tex, 0, 0, 0);
+	gs_enable_blending(False);
+	
+	gs_matrix_pop();
+}

+ 64 - 0
plugins/linux-xshm/xcursor.h

@@ -0,0 +1,64 @@
+/*
+Copyright (C) 2014 by Leonhard Oelke <[email protected]>
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 2 of the License, or
+(at your option) any later version.
+
+This program 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 General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+#pragma once
+
+#include <obs.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+typedef struct {
+	Display *dpy;
+	float pos_x;
+	float pos_y;
+	unsigned long last_serial;
+	unsigned short int last_width;
+	unsigned short int last_height;
+	texture_t tex;
+} xcursor_t;
+
+/**
+ * Initializes the xcursor object
+ * 
+ * This needs to be executed within a valid render context
+ */
+xcursor_t *xcursor_init(Display *dpy);
+
+/**
+ * Destroys the xcursor object
+ */
+void xcursor_destroy(xcursor_t *data);
+
+/**
+ * Update the cursor texture
+ * 
+ * This needs to be executed within a valid render context
+ */
+void xcursor_tick(xcursor_t *data);
+
+/**
+ * Draw the cursor
+ * 
+ * This needs to be executed within a valid render context
+ */
+void xcursor_render(xcursor_t *data);
+
+#ifdef __cplusplus
+}
+#endif

+ 206 - 0
plugins/linux-xshm/xshm-input.c

@@ -0,0 +1,206 @@
+/*
+Copyright (C) 2014 by Leonhard Oelke <[email protected]>
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 2 of the License, or
+(at your option) any later version.
+
+This program 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 General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+#include <stdlib.h>
+#include <stdio.h>
+
+#include <sys/shm.h>
+#include <X11/Xlib.h>
+#include <X11/Xutil.h>
+#include <X11/extensions/XShm.h>
+
+#include <obs.h>
+#include "xcursor.h"
+
+#define XSHM_DATA(voidptr) struct xshm_data *data = voidptr;
+
+struct xshm_data {
+	Display *dpy;
+	Window root_window;
+	uint32_t width, height;
+	int shm_attached;
+	XShmSegmentInfo shm_info;
+	XImage *image;
+	texture_t texture;
+	xcursor_t *cursor;
+};
+
+static const char* xshm_getname(const char* locale)
+{
+	UNUSED_PARAMETER(locale);
+	return "X11 Shared Memory Screen Input";
+}
+
+static void xshm_destroy(void *vptr)
+{
+	XSHM_DATA(vptr);
+	
+	if (!data)
+		return;
+	
+	gs_entercontext(obs_graphics());
+	
+	texture_destroy(data->texture);
+	xcursor_destroy(data->cursor);
+	
+	gs_leavecontext();
+	
+	if (data->shm_attached)
+		XShmDetach(data->dpy, &data->shm_info);
+	
+	if (data->shm_info.shmaddr != (char *) -1) {
+		shmdt(data->shm_info.shmaddr);
+		data->shm_info.shmaddr = (char *) -1;
+	}
+	
+	if (data->shm_info.shmid != -1)
+		shmctl(data->shm_info.shmid, IPC_RMID, NULL);
+	
+	if (data->image)
+		XDestroyImage(data->image);
+	
+	if (data->dpy)
+		XCloseDisplay(data->dpy);
+	
+	bfree(data);
+}
+
+static void *xshm_create(obs_data_t settings, obs_source_t source)
+{
+	UNUSED_PARAMETER(settings);
+	UNUSED_PARAMETER(source);
+	
+	
+	struct xshm_data *data = bmalloc(sizeof(struct xshm_data));
+	memset(data, 0, sizeof(struct xshm_data));
+	
+	data->dpy = XOpenDisplay(NULL);
+	if (!data->dpy)
+		goto fail;
+	
+	Screen *screen = XDefaultScreenOfDisplay(data->dpy);
+	data->width = WidthOfScreen(screen);
+	data->height = HeightOfScreen(screen);
+	data->root_window = XRootWindowOfScreen(screen);
+	Visual *visual = DefaultVisualOfScreen(screen);
+	int depth = DefaultDepthOfScreen(screen);
+	
+	if (!XShmQueryExtension(data->dpy))
+		goto fail;
+	
+	data->image = XShmCreateImage(data->dpy, visual, depth,
+		ZPixmap, NULL, &data->shm_info, data->width, data->height);
+	if (!data->image)
+		goto fail;
+	
+	data->shm_info.shmid = shmget(IPC_PRIVATE,
+		data->image->bytes_per_line * data->image->height,
+		IPC_CREAT | 0700);
+	if (data->shm_info.shmid < 0)
+		goto fail;
+	
+	data->shm_info.shmaddr
+		= data->image->data 
+		= (char *) shmat(data->shm_info.shmid, 0, 0);
+	if (data->shm_info.shmaddr == (char *) -1)
+		goto fail;
+	data->shm_info.readOnly = False;
+	
+	
+	if (!XShmAttach(data->dpy, &data->shm_info))
+		goto fail;
+	data->shm_attached = 1;
+	
+	if (!XShmGetImage(data->dpy, data->root_window, data->image,
+		0, 0, AllPlanes)) {
+		goto fail;
+	}
+	
+	
+	gs_entercontext(obs_graphics());
+	data->texture = gs_create_texture(data->width, data->height,
+		GS_BGRA, 1, (const void**) &data->image->data, GS_DYNAMIC);
+	data->cursor = xcursor_init(data->dpy);
+	gs_leavecontext();
+	
+	if (!data->texture)
+		goto fail;
+	
+	return data;
+	
+fail:
+	xshm_destroy(data);
+	return NULL;
+}
+
+static void xshm_video_tick(void *vptr, float seconds)
+{
+	UNUSED_PARAMETER(seconds);
+	XSHM_DATA(vptr);
+	
+	gs_entercontext(obs_graphics());
+	
+	
+	XShmGetImage(data->dpy, data->root_window, data->image,
+		0, 0, AllPlanes);
+	texture_setimage(data->texture, (void *) data->image->data,
+		data->width * 4, False);
+	
+	xcursor_tick(data->cursor);
+	
+	gs_leavecontext();
+}
+
+static void xshm_video_render(void *vptr, effect_t effect)
+{
+	XSHM_DATA(vptr);
+	
+	eparam_t image = effect_getparambyname(effect, "image");
+	effect_settexture(effect, image, data->texture);
+	
+	gs_enable_blending(False);
+	
+	gs_draw_sprite(data->texture, 0, 0, 0);
+	
+	xcursor_render(data->cursor);
+}
+
+static uint32_t xshm_getwidth(void *vptr)
+{
+	XSHM_DATA(vptr);
+	
+	return texture_getwidth(data->texture);
+}
+
+static uint32_t xshm_getheight(void *vptr)
+{
+	XSHM_DATA(vptr);
+	
+	return texture_getheight(data->texture);
+}
+
+struct obs_source_info xshm_input = {
+    .id           = "xshm_input",
+    .type         = OBS_SOURCE_TYPE_INPUT,
+    .output_flags = OBS_SOURCE_VIDEO,
+    .getname      = xshm_getname,
+    .create       = xshm_create,
+    .destroy      = xshm_destroy,
+    .video_tick   = xshm_video_tick,
+    .video_render = xshm_video_render,
+    .getwidth     = xshm_getwidth,
+    .getheight    = xshm_getheight
+};