1
0
Эх сурвалжийг харах

Add sndio support (#3715)

Add sndio support
Vadim Zhukov 5 жил өмнө
parent
commit
12719816fc

+ 1 - 0
.github/workflows/main.yml

@@ -358,6 +358,7 @@ jobs:
            libluajit-5.1-dev \
            libpulse-dev \
            libqt5x11extras5-dev \
+           libsndio-dev \
            libspeexdsp-dev \
            libswresample-dev \
            libswscale-dev \

+ 71 - 0
cmake/Modules/FindSndio.cmake

@@ -0,0 +1,71 @@
+# Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
+# file Copyright.txt or https://cmake.org/licensing for details.
+
+#[=======================================================================[.rst:
+FindSndio
+-------
+
+Finds the Sndio library.
+
+Imported Targets
+^^^^^^^^^^^^^^^^
+
+This module provides the following imported targets, if found:
+
+``Sndio::Sndio``
+  The Sndio library
+
+Result Variables
+^^^^^^^^^^^^^^^^
+
+This will define the following variables:
+
+``Sndio_FOUND``
+  True if the system has the Sndio library.
+``Sndio_VERSION``
+  The version of the Sndio library which was found.
+``Sndio_INCLUDE_DIRS``
+  Include directories needed to use Sndio.
+``Sndio_LIBRARIES``
+  Libraries needed to link to Sndio.
+
+Cache Variables
+^^^^^^^^^^^^^^^
+
+The following cache variables may also be set:
+
+``Sndio_INCLUDE_DIR``
+  The directory containing ``sndio.h``.
+``Sndio_LIBRARY``
+  The path to the Sndio library.
+
+#]=======================================================================]
+
+find_path(Sndio_INCLUDE_DIR sndio.h)
+find_library(Sndio_LIBRARY sndio)
+
+include(FindPackageHandleStandardArgs)
+find_package_handle_standard_args(Sndio
+  FOUND_VAR Sndio_FOUND
+  REQUIRED_VARS
+    Sndio_LIBRARY
+    Sndio_INCLUDE_DIR
+)
+
+if(Sndio_FOUND)
+  set(Sndio_LIBRARIES ${Sndio_LIBRARY})
+  set(Sndio_INCLUDE_DIRS ${Sndio_INCLUDE_DIR})
+endif()
+
+if(Sndio_FOUND AND NOT TARGET Sndio::Sndio)
+  add_library(Sndio::Sndio UNKNOWN IMPORTED)
+  set_target_properties(Sndio::Sndio PROPERTIES
+    IMPORTED_LOCATION "${Sndio_LIBRARY}"
+    INTERFACE_INCLUDE_DIRECTORIES "${Sndio_INCLUDE_DIR}"
+  )
+endif()
+
+mark_as_advanced(
+  Sndio_INCLUDE_DIR
+  Sndio_LIBRARY
+)

+ 5 - 0
plugins/CMakeLists.txt

@@ -42,6 +42,7 @@ elseif("${CMAKE_SYSTEM_NAME}" MATCHES "Linux")
 	add_subdirectory(linux-alsa)
 	add_subdirectory(decklink/linux)
 	add_subdirectory(vlc-video)
+	add_subdirectory(sndio)
 elseif("${CMAKE_SYSTEM_NAME}" MATCHES "FreeBSD")
 	add_subdirectory(linux-capture)
 	add_subdirectory(linux-pulseaudio)
@@ -50,6 +51,10 @@ elseif("${CMAKE_SYSTEM_NAME}" MATCHES "FreeBSD")
 	add_subdirectory(linux-alsa)
 	add_subdirectory(vlc-video)
 	add_subdirectory(oss-audio)
+	add_subdirectory(sndio)
+elseif("${CMAKE_SYSTEM_NAME}" MATCHES "OpenBSD")
+	add_subdirectory(linux-capture)
+	add_subdirectory(sndio)
 endif()
 
 option(BUILD_BROWSER "Build browser plugin" OFF)

+ 35 - 0
plugins/sndio/CMakeLists.txt

@@ -0,0 +1,35 @@
+project(sndio)
+
+if(DISABLE_SNDIO)
+	message(STATUS "Sndio support disabled")
+	return()
+endif()
+
+find_package(Sndio)
+if(NOT Sndio_FOUND AND ENABLE_SNDIO)
+	message(FATAL_ERROR "Sndio not found but set as enabled")
+elseif(NOT Sndio_FOUND)
+	message(STATUS "Sndio not found, disabling Sndio plugin")
+	return()
+endif()
+
+include_directories(
+	SYSTEM "${CMAKE_SOURCE_DIR}/libobs" SYSTEM "${CMAKE_SOURCE_DIR}/../../libobs"
+	${Sndio_INCLUDE_DIRS}
+)
+
+set(sndio_SOURCES
+	sndio.c
+	sndio-input.c
+)
+
+add_library(sndio MODULE
+	${sndio_SOURCES}
+)
+target_link_libraries(sndio
+	libobs
+	${Sndio_LIBRARIES}
+)
+set_target_properties(sndio PROPERTIES FOLDER "plugins")
+
+install_obs_plugin_with_data(sndio data)

+ 6 - 0
plugins/sndio/data/locale/en-US.ini

@@ -0,0 +1,6 @@
+Channels="Number of channels"
+Device="Device"
+BitsPerSample="Bits per sample"
+Rate="Rate"
+SndioInput="Sndio input client"
+

+ 418 - 0
plugins/sndio/sndio-input.c

@@ -0,0 +1,418 @@
+/*
+Copyright (C) 2020 by Vadim Zhukov <[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 <sys/types.h>
+#include <sys/socket.h>
+#include <errno.h>
+#include <poll.h>
+#include <pthread.h>
+#include <sndio.h>
+#include <string.h>
+#include <unistd.h>
+
+#include <util/bmem.h>
+#include <util/platform.h>
+#include <util/threading.h>
+#include <util/util_uint64.h>
+#include <obs.h>
+#include <obs-module.h>
+
+#include "sndio-input.h"
+
+#define blog(level, msg, ...) \
+	blog(level, "sndio-input: %s: " msg, __func__, ##__VA_ARGS__);
+
+#define berr(level, msg, ...)                                   \
+	do {                                                    \
+		const char *errstr = strerror_l(errno, NULL);   \
+		blog(level, msg ": %s", ##__VA_ARGS__, errstr); \
+	} while (0)
+
+#define NSEC_PER_SEC 1000000000LL
+
+/**
+ * Returns the name of the plugin
+ */
+static const char *sndio_input_getname(void *unused)
+{
+	UNUSED_PARAMETER(unused);
+	return obs_module_text("SndioInput");
+}
+
+static enum speaker_layout sndio_channels_to_obs_speakers(int channels)
+{
+	switch (channels) {
+	case 1:
+		return SPEAKERS_MONO;
+	case 2:
+		return SPEAKERS_STEREO;
+	case 3:
+		return SPEAKERS_2POINT1;
+	case 4:
+		return SPEAKERS_4POINT0;
+	case 5:
+		return SPEAKERS_4POINT1;
+	case 6:
+		return SPEAKERS_5POINT1;
+	case 8:
+		return SPEAKERS_7POINT1;
+	}
+	return SPEAKERS_UNKNOWN;
+}
+
+static enum audio_format sndio_to_obs_audio_format(const struct sio_par *par)
+{
+	switch (par->bits) {
+	case 8:
+		return AUDIO_FORMAT_U8BIT;
+	case 16:
+		return AUDIO_FORMAT_16BIT;
+	case 32:
+		return AUDIO_FORMAT_32BIT;
+	}
+	return AUDIO_FORMAT_UNKNOWN;
+}
+
+/**
+ * Runs sndio tasks on a pre-opened audio device.
+ * Whenever user chooses another device, this thread gets signalled to exit,
+ * and another one starts immediately, without waiting for the previous.
+ */
+static void *sndio_thread(void *attr)
+{
+	struct sndio_thr_data *thrdata = attr;
+	size_t msgread = 0; // msg bytes read from socket
+	size_t nsiofds = sio_nfds(thrdata->hdl);
+	struct pollfd pfd[1 + nsiofds];
+	struct sio_par par;
+	uint64_t ts;
+	ssize_t nread;
+	int pollres;
+	uint8_t *buf;
+	size_t bufsz;
+
+	ts = os_gettime_ns();
+
+	bufsz = thrdata->par.appbufsz * thrdata->par.bps * 2;
+	if ((buf = bmalloc(bufsz * 2)) == NULL) {
+		blog(LOG_ERROR, "could not allocate record buffer of %zu bytes",
+		     bufsz);
+		goto finish;
+	}
+
+	memset(pfd, 0, sizeof(pfd));
+	pfd[0].fd = thrdata->sock;
+
+	for (;;) {
+		for (size_t i = 0; i <= nsiofds; i++)
+			pfd[i].revents = 0;
+		pfd[0].events = POLLIN;
+		sio_pollfd(thrdata->hdl, pfd + 1, POLLIN);
+
+		if (poll(pfd, 1 + nsiofds, /*INFTIM*/ -1) == -1) {
+			if (errno == EINTR)
+				continue;
+			berr(LOG_ERROR, "exiting due to poll error");
+			goto finish;
+		}
+
+		if ((pfd[0].revents & POLLHUP) == POLLHUP) {
+			blog(LOG_INFO,
+			     "exiting upon receiving EOF at IPC socket");
+			goto finish;
+		}
+		if ((pfd[0].revents & POLLIN) == POLLIN) {
+			nread = read(pfd[0].fd, ((uint8_t *)&par) + msgread,
+				     sizeof(par) - msgread);
+			switch (nread) {
+			case -1:
+				if (errno == EAGAIN)
+					goto proceed_sio;
+				berr(LOG_ERROR,
+				     "reading from IPC socket failed");
+				goto finish;
+
+			case 0:
+				blog(LOG_INFO,
+				     "exiting upon receiving EOF at IPC socket");
+				goto finish;
+
+			default:
+				msgread += (size_t)nread;
+				if (msgread == sizeof(struct sio_par)) {
+					size_t tbufsz;
+					uint8_t *tbuf;
+
+					msgread = 0;
+					sio_stop(thrdata->hdl);
+					if (!sio_setpar(thrdata->hdl, &par)) {
+						blog(LOG_WARNING,
+						     "sio_setpar failed, keeping old params");
+					}
+					blog(LOG_INFO,
+					     "after sio_setpar(): appbufsz=%u bps=%u",
+					     par.appbufsz, par.bps);
+					memcpy(&thrdata->par, &par,
+					       sizeof(struct sio_par));
+
+					tbufsz = thrdata->par.appbufsz *
+						 thrdata->par.bps * 2;
+					if ((tbuf = brealloc(buf, tbufsz)) ==
+					    NULL) {
+						blog(LOG_ERROR,
+						     "could not reallocate record buffer of %zu bytes",
+						     tbufsz);
+						goto finish;
+					}
+					buf = tbuf;
+					bufsz = tbufsz;
+
+					if (!sio_start(thrdata->hdl)) {
+						blog(LOG_ERROR,
+						     "sio_start failed, exiting");
+						goto finish;
+					}
+					ts = os_gettime_ns();
+					// Since we restarted recording,
+					// do not try to handle events we lost.
+					continue;
+				}
+			}
+		}
+
+	proceed_sio:
+		pollres = sio_revents(thrdata->hdl, pfd + 1);
+		if ((pollres & POLLHUP) == POLLHUP) {
+			blog(LOG_ERROR, "sndio device error happened, exiting");
+			goto finish;
+		}
+		if ((pollres & POLLIN) == POLLIN) {
+			struct obs_source_audio out;
+			unsigned int nframes;
+
+			nread = (ssize_t)sio_read(thrdata->hdl, buf, bufsz);
+			if (nread == 0) {
+				if (sio_eof(thrdata->hdl)) {
+					blog(LOG_ERROR,
+					     "sndio device EOF happened, exiting");
+					goto finish;
+				}
+				continue;
+			}
+			nframes = (unsigned int)nread / thrdata->par.bps;
+			//blog(LOG_INFO, "sio_read returned %u, nframes = %u", nread, nframes);
+
+			memset(&out, 0, sizeof(struct obs_source_audio));
+			out.data[0] = buf;
+			out.frames = nframes;
+			out.format = sndio_to_obs_audio_format(&thrdata->par);
+			out.speakers = sndio_channels_to_obs_speakers(
+				thrdata->par.rchan);
+			out.samples_per_sec = thrdata->par.rate;
+			out.timestamp = ts;
+
+			ts += util_mul_div64(nframes, NSEC_PER_SEC,
+					     thrdata->par.rate);
+
+			obs_source_output_audio(thrdata->source, &out);
+		}
+	}
+
+finish:
+	sio_close(thrdata->hdl);
+	close(thrdata->sock);
+	bfree(buf);
+	bfree(thrdata);
+	return NULL;
+}
+
+/**
+ * Destroy the plugin object and free all memory
+ */
+static void sndio_destroy(void *vptr)
+{
+	struct sndio_data *data = vptr;
+
+	if (!data)
+		return;
+	close(data->sock);
+	data->sock = -1;
+	pthread_attr_destroy(&data->attr);
+	bfree(data);
+}
+
+/**
+ * Tries to apply the input settings.
+ * Must be called on stopped device.
+ */
+static void sndio_apply(struct sndio_data *data, obs_data_t *settings)
+{
+	struct sndio_thr_data *thrdata;
+	pthread_t thread;
+	int ec;
+	int socks[2] = {-1, -1};
+	const char *devname = obs_data_get_string(settings, "device");
+
+	if ((thrdata = bzalloc(sizeof(struct sndio_thr_data))) == NULL) {
+		blog(LOG_ERROR, "malloc");
+		return;
+	}
+	if (socketpair(AF_UNIX, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0,
+		       socks) == -1) {
+		berr(LOG_ERROR, "socketpair");
+		goto error;
+	}
+
+	if (data->sock != -1)
+		close(data->sock);
+	data->sock = socks[0];
+	thrdata->sock = socks[1];
+	thrdata->source = data->source;
+
+	thrdata->hdl = sio_open(devname, SIO_REC, 0);
+	if (!thrdata->hdl) {
+		berr(LOG_ERROR, "could not open %s sndio device", devname);
+		goto error;
+	}
+
+	sio_initpar(&thrdata->par);
+	thrdata->par.bits = obs_data_get_int(settings, "bits");
+	thrdata->par.bps = SIO_BPS(thrdata->par.bits);
+	thrdata->par.sig = thrdata->par.bits > 8;
+	thrdata->par.le = 1;
+	thrdata->par.rate = obs_data_get_int(settings, "rate");
+	thrdata->par.rchan = obs_data_get_int(settings, "channels");
+	thrdata->par.xrun = SIO_SYNC; // makes timestamping easy
+	if (!sio_setpar(thrdata->hdl, &thrdata->par)) {
+		berr(LOG_ERROR, "could not set parameters for %s sndio device",
+		     devname);
+		goto error;
+	}
+	blog(LOG_INFO, "after initial sio_setpar(): appbufsz=%u bps=%u",
+	     thrdata->par.appbufsz, thrdata->par.bps);
+
+	if (!sio_start(thrdata->hdl)) {
+		berr(LOG_ERROR, "could not start recording on %s sndio device",
+		     devname);
+		goto error;
+	}
+
+	ec = pthread_create(&thread, &data->attr, sndio_thread, thrdata);
+	if (ec != 0) {
+		blog(LOG_ERROR, "could not start thread");
+		goto error;
+	}
+
+	return;
+
+error:
+	if (thrdata->hdl != NULL)
+		sio_close(thrdata->hdl);
+	close(socks[0]);
+	close(socks[1]);
+	bfree(thrdata);
+}
+
+/**
+ * Update the input settings.
+ */
+static void sndio_update(void *vptr, obs_data_t *settings)
+{
+	struct sndio_data *data = vptr;
+	sndio_apply(data, settings);
+}
+
+/**
+ * Create the plugin object
+ */
+static void *sndio_create(obs_data_t *settings, obs_source_t *source)
+{
+	struct sndio_data *data = bzalloc(sizeof(struct sndio_data));
+	pthread_attr_init(&data->attr);
+	pthread_attr_setdetachstate(&data->attr, PTHREAD_CREATE_DETACHED);
+	data->source = source;
+	data->sock = -1;
+	sndio_apply(data, settings);
+	return data;
+}
+
+/**
+ * Get plugin defaults
+ */
+static void sndio_input_defaults(obs_data_t *settings)
+{
+	obs_data_set_default_string(settings, "device", SIO_DEVANY);
+	obs_data_set_default_int(settings, "rate", 48000);
+	obs_data_set_default_int(settings, "bits", 16);
+	obs_data_set_default_int(settings, "channels", 2);
+}
+
+/**
+ * Get plugin properties
+ */
+static obs_properties_t *sndio_input_properties(void *unused)
+{
+	obs_property_t *rate, *bits;
+
+	UNUSED_PARAMETER(unused);
+
+	obs_properties_t *props = obs_properties_create();
+
+	obs_properties_add_text(props, "device", obs_module_text("Device"),
+				OBS_TEXT_DEFAULT);
+
+	rate = obs_properties_add_list(props, "rate", obs_module_text("Rate"),
+				       OBS_COMBO_TYPE_LIST,
+				       OBS_COMBO_FORMAT_INT);
+	obs_property_list_add_int(rate, "11025 Hz", 11025);
+	obs_property_list_add_int(rate, "22050 Hz", 22050);
+	obs_property_list_add_int(rate, "32000 Hz", 32000);
+	obs_property_list_add_int(rate, "44100 Hz", 44100);
+	obs_property_list_add_int(rate, "48000 Hz", 48000);
+	obs_property_list_add_int(rate, "96000 Hz", 96000);
+	obs_property_list_add_int(rate, "192000 Hz", 192000);
+
+	bits = obs_properties_add_list(props, "bits",
+				       obs_module_text("BitsPerSample"),
+				       OBS_COMBO_TYPE_LIST,
+				       OBS_COMBO_FORMAT_INT);
+	obs_property_list_add_int(bits, "8", 8);
+	obs_property_list_add_int(bits, "16", 16);
+	obs_property_list_add_int(bits, "32", 32);
+
+	obs_properties_add_int(props, "channels", obs_module_text("Channels"),
+			       1, 8, 1);
+
+	return props;
+}
+
+struct obs_source_info sndio_output_capture = {
+	.id = "sndio_output_capture",
+	.type = OBS_SOURCE_TYPE_INPUT,
+	.output_flags = OBS_SOURCE_AUDIO,
+	.get_name = sndio_input_getname,
+	.create = sndio_create,
+	.destroy = sndio_destroy,
+#if SHUTDOWN_ON_DEACTIVATE
+	.activate = sndio_activate,
+	.deactivate = sndio_deactivate,
+#endif
+	.update = sndio_update,
+	.get_defaults = sndio_input_defaults,
+	.get_properties = sndio_input_properties,
+	.icon_type = OBS_ICON_TYPE_AUDIO_OUTPUT,
+};

+ 18 - 0
plugins/sndio/sndio-input.h

@@ -0,0 +1,18 @@
+#ifndef OBS_SNDIO_INPUT_H
+#define OBS_SNDIO_INPUT_H
+
+struct sndio_thr_data {
+	obs_source_t *source;
+	struct obs_source_audio out;
+	struct sio_hdl *hdl;
+	struct sio_par par;
+	int sock;
+};
+
+struct sndio_data {
+	obs_source_t *source;
+	pthread_attr_t attr;
+	int sock;
+};
+
+#endif // OBS_SNDIO_INPUT_H

+ 32 - 0
plugins/sndio/sndio.c

@@ -0,0 +1,32 @@
+/*
+Copyright (C) 2020 by Vadim Zhukov <[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()
+OBS_MODULE_USE_DEFAULT_LOCALE("sndio", "en-US")
+MODULE_EXPORT const char *obs_module_description(void)
+{
+	return "sndio output capture";
+}
+
+extern struct obs_source_info sndio_output_capture;
+
+bool obs_module_load(void)
+{
+	obs_register_source(&sndio_output_capture);
+	return true;
+}