diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt
index d20bce142fe43f..60958ad400a0e0 100644
--- a/plugins/CMakeLists.txt
+++ b/plugins/CMakeLists.txt
@@ -56,6 +56,7 @@ elseif(OS_LINUX)
add_subdirectory(linux-jack)
add_subdirectory(linux-alsa)
add_subdirectory(linux-pipewire)
+ add_subdirectory(linux-virtualcam)
add_subdirectory(decklink)
add_subdirectory(vlc-video)
add_subdirectory(sndio)
diff --git a/plugins/linux-v4l2/CMakeLists.txt b/plugins/linux-v4l2/CMakeLists.txt
index c26357663c5d59..9b0db86163b556 100644
--- a/plugins/linux-v4l2/CMakeLists.txt
+++ b/plugins/linux-v4l2/CMakeLists.txt
@@ -15,7 +15,7 @@ add_library(linux-v4l2 MODULE)
add_library(OBS::v4l2 ALIAS linux-v4l2)
target_sources(linux-v4l2 PRIVATE linux-v4l2.c v4l2-controls.c v4l2-input.c
- v4l2-helpers.c v4l2-output.c v4l2-decoder.c)
+ v4l2-helpers.c v4l2-decoder.c)
target_link_libraries(
linux-v4l2 PRIVATE OBS::libobs LIB4L2::LIB4L2 FFmpeg::avcodec
diff --git a/plugins/linux-v4l2/linux-v4l2.c b/plugins/linux-v4l2/linux-v4l2.c
index 94f8346658007a..c1d44a2f7229cc 100644
--- a/plugins/linux-v4l2/linux-v4l2.c
+++ b/plugins/linux-v4l2/linux-v4l2.c
@@ -15,36 +15,19 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
#include
-#include
OBS_DECLARE_MODULE()
OBS_MODULE_USE_DEFAULT_LOCALE("linux-v4l2", "en-US")
MODULE_EXPORT const char *obs_module_description(void)
{
- return "Video4Linux2(V4L2) sources/virtual camera";
+ return "Video4Linux2(V4L2) sources";
}
extern struct obs_source_info v4l2_input;
-extern struct obs_output_info virtualcam_info;
-extern bool loopback_module_available();
bool obs_module_load(void)
{
obs_register_source(&v4l2_input);
- obs_data_t *obs_settings = obs_data_create();
-
- if (loopback_module_available()) {
- obs_register_output(&virtualcam_info);
- obs_data_set_bool(obs_settings, "vcamEnabled", true);
- } else {
- obs_data_set_bool(obs_settings, "vcamEnabled", false);
- blog(LOG_WARNING,
- "v4l2loopback not installed, virtual camera disabled");
- }
-
- obs_apply_private_data(obs_settings);
- obs_data_release(obs_settings);
-
return true;
}
diff --git a/plugins/linux-virtualcam/CMakeLists.txt b/plugins/linux-virtualcam/CMakeLists.txt
new file mode 100644
index 00000000000000..2a08616a2b50f8
--- /dev/null
+++ b/plugins/linux-virtualcam/CMakeLists.txt
@@ -0,0 +1,24 @@
+project(linux-virtualcam)
+
+option(ENABLE_VIRTUALCAM "Build OBS Virtualcam (Linux)" ON)
+
+if(NOT ENABLE_VIRTUALCAM)
+ obs_status(DISABLED "linux-virtualcam")
+ return()
+endif()
+
+find_package(ALSA REQUIRED)
+find_package(Libv4l2 REQUIRED)
+
+add_library(linux-virtualcam MODULE)
+add_library(OBS::virtualcam ALIAS linux-virtualcam)
+
+target_sources(linux-virtualcam PRIVATE alsa-output.c v4l2-output.c
+ linux-virtualcam.c)
+
+target_link_libraries(linux-virtualcam PRIVATE OBS::libobs LIB4L2::LIB4L2
+ ALSA::ALSA)
+
+set_target_properties(linux-virtualcam PROPERTIES FOLDER "plugins")
+
+setup_plugin_target(linux-virtualcam)
diff --git a/plugins/linux-virtualcam/alsa-output.c b/plugins/linux-virtualcam/alsa-output.c
new file mode 100644
index 00000000000000..9ad397fc58343c
--- /dev/null
+++ b/plugins/linux-virtualcam/alsa-output.c
@@ -0,0 +1,361 @@
+/*
+Copyright (C) 2022 DEV47APPS, github.com/dev47apps
+
+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 .
+*/
+#define _GNU_SOURCE
+#include
+#include
+
+#include "linux-virtualcam.h"
+
+struct alsa_output_data {
+ snd_pcm_t *handle;
+ snd_pcm_hw_params_t *hwparams;
+ snd_pcm_sw_params_t *swparams;
+ snd_pcm_sframes_t period_size;
+ snd_pcm_sframes_t buffer_size;
+};
+
+static bool loopback_module_loaded()
+{
+ return module_loaded("snd_aloop");
+}
+
+bool audio_possible()
+{
+ return access("/sys/module/snd_aloop", F_OK) == 0;
+}
+
+static int loopback_module_load()
+{
+ return run_command("pkexec modprobe snd_aloop && sleep 0.5");
+}
+
+static inline snd_pcm_format_t to_alsa_format(enum audio_format format)
+{
+ switch (format) {
+ case AUDIO_FORMAT_U8BIT:
+ return SND_PCM_FORMAT_U8;
+ case AUDIO_FORMAT_16BIT:
+ return SND_PCM_FORMAT_S16;
+ case AUDIO_FORMAT_32BIT:
+ return SND_PCM_FORMAT_S32;
+ case AUDIO_FORMAT_FLOAT:
+ return SND_PCM_FORMAT_FLOAT;
+ default:
+ return SND_PCM_FORMAT_UNKNOWN;
+ }
+}
+
+static inline enum audio_format to_obs_format(snd_pcm_format_t format)
+{
+ switch (format) {
+ case SND_PCM_FORMAT_U8:
+ return AUDIO_FORMAT_U8BIT;
+ case SND_PCM_FORMAT_S16:
+ return AUDIO_FORMAT_16BIT;
+ case SND_PCM_FORMAT_S32:
+ return AUDIO_FORMAT_32BIT;
+ case SND_PCM_FORMAT_FLOAT:
+ return AUDIO_FORMAT_FLOAT;
+ default:
+ return AUDIO_FORMAT_UNKNOWN;
+ }
+}
+
+static int set_hwparams(snd_pcm_t *handle, snd_pcm_hw_params_t *params,
+ unsigned channels, unsigned sample_rate,
+ snd_pcm_format_t format, snd_pcm_access_t access,
+ snd_pcm_sframes_t *period_size,
+ snd_pcm_sframes_t *buffer_size)
+{
+ int rc, dir;
+ snd_pcm_uframes_t size;
+
+ /* choose all parameters */
+ rc = snd_pcm_hw_params_any(handle, params);
+ if (rc < 0) {
+ blog(LOG_WARNING, "snd configuration not available");
+ return rc;
+ }
+
+ /* set hardware resampling */
+ int resample = 1;
+ rc = snd_pcm_hw_params_set_rate_resample(handle, params, resample);
+ if (rc < 0) {
+ blog(LOG_WARNING, "resample setup failed");
+ return rc;
+ }
+
+ /* set the interleaved read/write format */
+ rc = snd_pcm_hw_params_set_access(handle, params, access);
+ if (rc < 0) {
+ blog(LOG_WARNING, "access mode not available");
+ return rc;
+ }
+
+ /* set the sample format */
+ rc = snd_pcm_hw_params_set_format(handle, params, format);
+ if (rc < 0) {
+ blog(LOG_WARNING, "sample format not available");
+ return rc;
+ }
+
+ /* set the channels */
+ rc = snd_pcm_hw_params_set_channels(handle, params, channels);
+ if (rc < 0) {
+ blog(LOG_WARNING, "Channels count (%u) not available",
+ channels);
+ return rc;
+ }
+
+ /* set the sampling rate */
+ unsigned int rate = sample_rate;
+ rc = snd_pcm_hw_params_set_rate_near(handle, params, &rate, 0);
+ if (rc < 0) {
+ blog(LOG_WARNING, "Rate %uHz not available", sample_rate);
+ return rc;
+ }
+
+ if (sample_rate != rate) {
+ blog(LOG_WARNING, "Rate doesn't match, want:%uHz, got %uHz",
+ sample_rate, rate);
+ return -EINVAL;
+ }
+
+ /* set the period time in us */
+ unsigned period_time = 1000 * 1000 * AUDIO_OUTPUT_FRAMES / sample_rate;
+ rc = snd_pcm_hw_params_set_period_time_near(handle, params,
+ &period_time, &dir);
+ if (rc < 0) {
+ blog(LOG_WARNING, "Unable to set period time %u", period_time);
+ return rc;
+ }
+
+ rc = snd_pcm_hw_params_get_period_size(params, &size, &dir);
+ if (rc < 0) {
+ blog(LOG_WARNING, "Unable to get period size");
+ return rc;
+ }
+ *period_size = size;
+
+ blog(LOG_DEBUG, "period_size=%ld", size);
+
+ /* ring buffer length in us */
+ unsigned buffer_time = period_time * 4;
+ rc = snd_pcm_hw_params_set_buffer_time_near(handle, params,
+ &buffer_time, &dir);
+ if (rc < 0) {
+ blog(LOG_WARNING, "Unable to set buffer time");
+ return rc;
+ }
+
+ rc = snd_pcm_hw_params_get_buffer_size(params, &size);
+ if (rc < 0) {
+ blog(LOG_WARNING, "Unable to get buffer size");
+ return rc;
+ }
+ *buffer_size = size;
+
+ blog(LOG_DEBUG, "buffer_size=%ld", size);
+
+ /* write the parameters to device */
+ rc = snd_pcm_hw_params(handle, params);
+ if (rc < 0) {
+ blog(LOG_WARNING, "Unable to set hwparams");
+ return rc;
+ }
+
+ return 0;
+}
+
+static int set_swparams(snd_pcm_t *handle, snd_pcm_sw_params_t *swparams,
+ snd_pcm_sframes_t period_size,
+ snd_pcm_sframes_t buffer_size)
+{
+ int rc;
+
+ UNUSED_PARAMETER(period_size);
+ UNUSED_PARAMETER(buffer_size);
+
+ rc = snd_pcm_sw_params_current(handle, swparams);
+ if (rc < 0) {
+ blog(LOG_WARNING, "Unable to determine current swparams");
+ return rc;
+ }
+
+ rc = snd_pcm_sw_params_set_start_threshold(handle, swparams,
+ period_size * 2);
+ if (rc < 0) {
+ blog(LOG_WARNING, "Unable to set start threshold");
+ return rc;
+ }
+
+ rc = snd_pcm_sw_params_set_avail_min(handle, swparams, period_size);
+ if (rc < 0) {
+ blog(LOG_WARNING, "Unable to set avail min");
+ return rc;
+ }
+
+ rc = snd_pcm_sw_params(handle, swparams);
+ if (rc < 0) {
+ blog(LOG_WARNING, "Unable to set swparams");
+ return rc;
+ }
+
+ return 0;
+}
+
+void *audio_start(obs_output_t *output)
+{
+ if (!loopback_module_loaded()) {
+ /* clang-format off */
+
+ // Should we load snd_aloop automatically?
+ // Given how finicky Linux audio is, lets not.
+ // Avoid messing with peoples audio.
+
+ #if 0
+ if (loopback_module_load() != 0)
+ return NULL;
+ #else
+ blog(LOG_INFO, "ALSA Loopback module is not loaded");
+ return NULL;
+ #endif
+
+ /* clang-format on */
+ }
+
+ audio_t *audio = obs_output_audio(output);
+ const struct audio_output_info *aoi = audio_output_get_info(audio);
+ struct audio_convert_info aci = {0};
+
+ size_t channels = audio_output_get_channels(audio);
+ uint32_t sample_rate = (int)audio_output_get_sample_rate(audio);
+ snd_pcm_format_t alsa_fromat = to_alsa_format(aoi->format);
+
+ if (alsa_fromat == SND_PCM_FORMAT_UNKNOWN) {
+ alsa_fromat = to_alsa_format(AUDIO_FORMAT_16BIT);
+ aci.format = AUDIO_FORMAT_16BIT;
+ aci.samples_per_sec = sample_rate;
+ aci.speakers = aoi->speakers;
+ }
+
+ snd_pcm_sframes_t period_size;
+ snd_pcm_sframes_t buffer_size;
+ snd_pcm_hw_params_t *hwparams;
+ snd_pcm_sw_params_t *swparams;
+
+ snd_pcm_hw_params_alloca(&hwparams);
+ snd_pcm_sw_params_alloca(&swparams);
+
+ for (int i = 0; i < 8; i++) {
+ int rc;
+ bool success = true;
+ char device[32];
+
+ snprintf(device, sizeof(device), "hw:Loopback,0,%d", i);
+ blog(LOG_DEBUG, "alsa-output: trying device: '%s'", device);
+
+ snd_pcm_t *handle = NULL;
+ rc = snd_pcm_open(&handle, device, SND_PCM_STREAM_PLAYBACK,
+ SND_PCM_NONBLOCK);
+
+ if (rc < 0 || !handle) {
+ blog(LOG_ERROR, "snd_pcm_open failed: %s",
+ snd_strerror(rc));
+ continue;
+ }
+
+ if ((rc = set_hwparams(handle, hwparams, channels, sample_rate,
+ alsa_fromat,
+ SND_PCM_ACCESS_MMAP_INTERLEAVED,
+ &period_size, &buffer_size)) < 0) {
+
+ blog(LOG_ERROR, "alsa-output: hwparams failed: %s",
+ snd_strerror(rc));
+
+ success = false;
+ }
+
+ else if ((rc = set_swparams(handle, swparams, period_size,
+ buffer_size)) < 0) {
+
+ blog(LOG_ERROR, "alsa-output: swparams failed: %s",
+ snd_strerror(rc));
+
+ success = false;
+ }
+
+ if (success) {
+ struct alsa_output_data *vcam =
+ (struct alsa_output_data *)bzalloc(
+ sizeof(*vcam));
+
+ vcam->handle = handle;
+ vcam->hwparams = hwparams;
+ vcam->swparams = swparams;
+ vcam->period_size = period_size;
+ vcam->buffer_size = buffer_size;
+
+ if (aci.format != 0)
+ obs_output_set_audio_conversion(output, &aci);
+
+ snprintf(device, sizeof(device), "hw:Loopback,1,%d", i);
+ blog(LOG_INFO, "ALSA output: %s", device);
+ return vcam;
+ }
+
+ snd_pcm_close(handle);
+ }
+
+ blog(LOG_WARNING, "Failed to start ALSA Loopback output");
+ return NULL;
+}
+
+void audio_stop(void *data)
+{
+ if (!data)
+ return;
+
+ struct alsa_output_data *vcam = (struct alsa_output_data *)data;
+ snd_pcm_close(vcam->handle);
+
+ bfree(data);
+}
+
+void virtual_audio(void *data, struct audio_data *frame)
+{
+ struct alsa_output_data *vcam = (struct alsa_output_data *)data;
+ int rc = snd_pcm_mmap_writei(vcam->handle, frame->data[0],
+ frame->frames);
+
+ if (rc == -EPIPE) { /* under-run */
+ rc = snd_pcm_prepare(vcam->handle);
+ UNUSED_PARAMETER(rc);
+ return;
+ }
+
+ if (rc == -ESTRPIPE) {
+ rc = snd_pcm_resume(vcam->handle);
+ if (rc == -EAGAIN)
+ return; /* wait until the suspend flag is released */
+
+ if (rc < 0) {
+ rc = snd_pcm_prepare(vcam->handle);
+ UNUSED_PARAMETER(rc);
+ }
+ }
+}
diff --git a/plugins/linux-virtualcam/data/locale/en-US.ini b/plugins/linux-virtualcam/data/locale/en-US.ini
new file mode 100644
index 00000000000000..e69de29bb2d1d6
diff --git a/plugins/linux-virtualcam/linux-virtualcam.c b/plugins/linux-virtualcam/linux-virtualcam.c
new file mode 100644
index 00000000000000..9a1f1da28801f4
--- /dev/null
+++ b/plugins/linux-virtualcam/linux-virtualcam.c
@@ -0,0 +1,194 @@
+/*
+Copyright (C) 2022 DEV47APPS, github.com/dev47apps
+
+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 .
+*/
+#include
+#include
+#include
+#include
+
+#include "linux-virtualcam.h"
+
+struct virtualcam_data {
+ obs_output_t *output;
+ void *audio_data;
+ void *video_data;
+};
+
+static bool is_flatpak_sandbox(void)
+{
+ return access("/.flatpak-info", F_OK) == 0;
+}
+
+int run_command(const char *command)
+{
+ struct dstr str;
+ int result;
+
+ dstr_init_copy(&str, "PATH=\"$PATH:/sbin\" ");
+
+ if (is_flatpak_sandbox())
+ dstr_cat(&str, "flatpak-spawn --host ");
+
+ dstr_cat(&str, command);
+ result = system(str.array);
+ dstr_free(&str);
+ return result;
+}
+
+bool module_loaded(const char *module)
+{
+ bool loaded = false;
+ char temp[512];
+
+ FILE *fp = fopen("/proc/modules", "r");
+ if (!fp)
+ return false;
+
+ while (fgets(temp, sizeof(temp), fp)) {
+ if (strstr(temp, module)) {
+ loaded = true;
+ break;
+ }
+ }
+
+ fclose(fp);
+
+ return loaded;
+}
+
+static void *virtualcam_create(obs_data_t *settings, obs_output_t *output)
+{
+ struct virtualcam_data *vcam =
+ (struct virtualcam_data *)bzalloc(sizeof(*vcam));
+ vcam->output = output;
+
+ UNUSED_PARAMETER(settings);
+ return vcam;
+}
+
+static void virtualcam_destroy(void *data)
+{
+ struct virtualcam_data *vcam = (struct virtualcam_data *)data;
+
+ if (vcam->audio_data)
+ audio_stop(vcam->audio_data);
+
+ if (vcam->video_data)
+ video_stop(vcam->video_data);
+
+ bfree(data);
+}
+
+static bool virtualcam_start(void *data)
+{
+ struct virtualcam_data *vcam = (struct virtualcam_data *)data;
+
+ vcam->video_data = video_start(vcam->output);
+ vcam->audio_data = audio_start(vcam->output);
+
+ if (vcam->video_data || vcam->audio_data) {
+ blog(LOG_INFO, "Virtual camera starting: audio=%d video=%d",
+ (vcam->audio_data != NULL), (vcam->video_data != NULL));
+
+ obs_output_begin_data_capture(vcam->output, 0);
+ return true;
+ }
+
+ return false;
+}
+
+static void virtualcam_stop(void *data, uint64_t ts)
+{
+ struct virtualcam_data *vcam = (struct virtualcam_data *)data;
+ obs_output_end_data_capture(vcam->output);
+
+ if (vcam->audio_data) {
+ audio_stop(vcam->audio_data);
+ vcam->audio_data = NULL;
+ }
+
+ if (vcam->video_data) {
+ video_stop(vcam->video_data);
+ vcam->video_data = NULL;
+ }
+
+ blog(LOG_INFO, "Virtual camera stopped");
+
+ UNUSED_PARAMETER(ts);
+}
+
+static const char *virtualcam_name(void *unused)
+{
+ UNUSED_PARAMETER(unused);
+ return "Virtual Camera Output";
+}
+
+extern void virtual_video(void *data, struct video_data *frame);
+extern void virtual_audio(void *data, struct audio_data *frame);
+
+void raw_audio(void *data, struct audio_data *frame)
+{
+ struct virtualcam_data *vcam = (struct virtualcam_data *)data;
+ if (vcam->audio_data) {
+ virtual_audio(vcam->audio_data, frame);
+ }
+}
+
+void raw_video(void *data, struct video_data *frame)
+{
+ struct virtualcam_data *vcam = (struct virtualcam_data *)data;
+ if (vcam->video_data) {
+ virtual_video(vcam->video_data, frame);
+ }
+}
+
+OBS_DECLARE_MODULE()
+OBS_MODULE_USE_DEFAULT_LOCALE("linux-virtualcam", "en-US")
+MODULE_EXPORT const char *obs_module_description(void)
+{
+ return "Linux virtual audio/video output";
+}
+
+struct obs_output_info virtualcam_info = {
+ .id = "virtualcam_output",
+ .flags = OBS_OUTPUT_AV,
+ .get_name = virtualcam_name,
+ .create = virtualcam_create,
+ .destroy = virtualcam_destroy,
+ .start = virtualcam_start,
+ .stop = virtualcam_stop,
+ .raw_audio = raw_audio,
+ .raw_video = raw_video,
+};
+
+bool obs_module_load(void)
+{
+ obs_data_t *obs_settings = obs_data_create();
+
+ if (video_possible()) {
+ obs_register_output(&virtualcam_info);
+ obs_data_set_bool(obs_settings, "vcamEnabled", true);
+ } else {
+ obs_data_set_bool(obs_settings, "vcamEnabled", false);
+ blog(LOG_WARNING,
+ "v4l2loopback not installed, virtual camera disabled");
+ }
+
+ obs_apply_private_data(obs_settings);
+ obs_data_release(obs_settings);
+
+ return true;
+}
diff --git a/plugins/linux-virtualcam/linux-virtualcam.h b/plugins/linux-virtualcam/linux-virtualcam.h
new file mode 100644
index 00000000000000..09b50e1e3dbef0
--- /dev/null
+++ b/plugins/linux-virtualcam/linux-virtualcam.h
@@ -0,0 +1,13 @@
+// Copyright (C) 2022 DEV47APPS, github.com/dev47apps
+#pragma once
+
+int run_command(const char *command);
+bool module_loaded(const char *module);
+
+bool audio_possible();
+void audio_stop(void *data);
+void *audio_start(obs_output_t *output);
+
+bool video_possible();
+void video_stop(void *data);
+void *video_start(obs_output_t *output);
diff --git a/plugins/linux-v4l2/v4l2-output.c b/plugins/linux-virtualcam/v4l2-output.c
similarity index 50%
rename from plugins/linux-v4l2/v4l2-output.c
rename to plugins/linux-virtualcam/v4l2-output.c
index 0d07d0d8a8c68b..065adae1089e05 100644
--- a/plugins/linux-v4l2/v4l2-output.c
+++ b/plugins/linux-virtualcam/v4l2-output.c
@@ -2,7 +2,6 @@
#include
#include
-#include
#include
#include
#include
@@ -11,125 +10,68 @@
#include
#include
-struct virtualcam_data {
+#include "linux-virtualcam.h"
+
+#define OBS_V4L2_CARD_LABEL "OBS Virtual Camera"
+
+struct v4l2_output_data {
obs_output_t *output;
int device;
uint32_t frame_size;
};
-static const char *virtualcam_name(void *unused)
-{
- UNUSED_PARAMETER(unused);
- return "Virtual Camera Output";
-}
-
-static void virtualcam_destroy(void *data)
-{
- struct virtualcam_data *vcam = (struct virtualcam_data *)data;
- close(vcam->device);
- bfree(data);
-}
-
-static bool is_flatpak_sandbox(void)
-{
- static bool flatpak_info_exists = false;
- static bool initialized = false;
-
- if (!initialized) {
- flatpak_info_exists = access("/.flatpak-info", F_OK) == 0;
- initialized = true;
- }
-
- return flatpak_info_exists;
-}
-
-static int run_command(const char *command)
-{
- struct dstr str;
- int result;
-
- dstr_init_copy(&str, "PATH=\"$PATH:/sbin\" ");
-
- if (is_flatpak_sandbox())
- dstr_cat(&str, "flatpak-spawn --host ");
-
- dstr_cat(&str, command);
- result = system(str.array);
- dstr_free(&str);
- return result;
-}
-
static bool loopback_module_loaded()
{
- bool loaded = false;
-
- char temp[512];
-
- FILE *fp = fopen("/proc/modules", "r");
-
- if (!fp)
- return false;
-
- while (fgets(temp, sizeof(temp), fp)) {
- if (strstr(temp, "v4l2loopback")) {
- loaded = true;
- break;
- }
- }
-
- fclose(fp);
-
- return loaded;
+ return module_loaded("v4l2loopback");
}
-bool loopback_module_available()
+bool video_possible()
{
if (loopback_module_loaded()) {
return true;
}
- if (run_command("modinfo v4l2loopback >/dev/null 2>&1") == 0) {
- return true;
- }
-
- return false;
+ return access("/sys/module/v4l2loopback", F_OK) == 0;
}
static int loopback_module_load()
{
return run_command(
- "pkexec modprobe v4l2loopback exclusive_caps=1 card_label='OBS Virtual Camera' && sleep 0.5");
+ "pkexec modprobe v4l2loopback exclusive_caps=1 card_label='" OBS_V4L2_CARD_LABEL
+ "' && sleep 0.5");
}
-static void *virtualcam_create(obs_data_t *settings, obs_output_t *output)
+static int loopback_module_add_card()
{
- struct virtualcam_data *vcam =
- (struct virtualcam_data *)bzalloc(sizeof(*vcam));
- vcam->output = output;
-
- UNUSED_PARAMETER(settings);
- return vcam;
+ return run_command(
+ "pkexec v4l2loopback-ctl add -n '" OBS_V4L2_CARD_LABEL
+ "' && sleep 0.5");
}
-static bool try_connect(void *data, const char *device)
+static bool try_connect(void *data, const char *device, const char *name)
{
- struct virtualcam_data *vcam = (struct virtualcam_data *)data;
- struct v4l2_format format;
- struct v4l2_capability capability;
- struct v4l2_streamparm parm;
-
- uint32_t width = obs_output_get_width(vcam->output);
- uint32_t height = obs_output_get_height(vcam->output);
-
- vcam->frame_size = width * height * 2;
+ struct v4l2_output_data *vcam = (struct v4l2_output_data *)data;
+ struct v4l2_format format = {0};
+ struct v4l2_capability capability = {0};
+ struct v4l2_streamparm parm = {0};
vcam->device = open(device, O_RDWR);
if (vcam->device < 0)
return false;
- if (ioctl(vcam->device, VIDIOC_QUERYCAP, &capability) < 0)
+ if (ioctl(vcam->device, VIDIOC_QUERYCAP, &capability) < 0) {
+ blog(LOG_WARNING,
+ "v4l2-output: VIDIOC_QUERYCAP failed: device:%s (%s)",
+ device, strerror(errno));
+ goto fail_close_device;
+ }
+
+ blog(LOG_DEBUG, "v4l2-output: found device: '%s'", capability.card);
+ if (name &&
+ strncmp((const char *)capability.card, name, strlen(name)) != 0) {
goto fail_close_device;
+ }
format.type = V4L2_BUF_TYPE_VIDEO_OUTPUT;
@@ -139,9 +81,7 @@ static bool try_connect(void *data, const char *device)
struct obs_video_info ovi;
obs_get_video_info(&ovi);
- memset(&parm, 0, sizeof(parm));
parm.type = V4L2_BUF_TYPE_VIDEO_OUTPUT;
-
parm.parm.output.capability = V4L2_CAP_TIMEPERFRAME;
parm.parm.output.timeperframe.numerator = ovi.fps_den;
parm.parm.output.timeperframe.denominator = ovi.fps_num;
@@ -149,8 +89,13 @@ static bool try_connect(void *data, const char *device)
if (ioctl(vcam->device, VIDIOC_S_PARM, &parm) < 0)
goto fail_close_device;
+ uint32_t width = obs_output_get_width(vcam->output);
+ uint32_t height = obs_output_get_height(vcam->output);
+ vcam->frame_size = width * height * 2;
+
format.fmt.pix.width = width;
format.fmt.pix.height = height;
+ format.fmt.pix.field = V4L2_FIELD_NONE;
format.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV;
format.fmt.pix.sizeimage = vcam->frame_size;
@@ -172,13 +117,14 @@ static bool try_connect(void *data, const char *device)
goto fail_close_device;
}
- blog(LOG_INFO, "Virtual camera started");
- obs_output_begin_data_capture(vcam->output, 0);
+ blog(LOG_INFO, "v4l2-output: Using device '%s' at '%s'",
+ capability.card, device);
return true;
fail_close_device:
close(vcam->device);
+ vcam->device = 0;
return false;
}
@@ -187,18 +133,12 @@ static int scanfilter(const struct dirent *entry)
return !astrcmp_n(entry->d_name, "video", 5);
}
-static bool virtualcam_start(void *data)
+static bool loopback_card_open(void *data, const char *name)
{
- struct virtualcam_data *vcam = (struct virtualcam_data *)data;
struct dirent **list;
bool success = false;
int n;
- if (!loopback_module_loaded()) {
- if (loopback_module_load() != 0)
- return false;
- }
-
n = scandir("/dev", &list, scanfilter,
#if defined(__linux__)
versionsort
@@ -220,7 +160,7 @@ static bool virtualcam_start(void *data)
"v4l2-output: A format truncation may have occurred."
" This can be ignored since it is quite improbable.");
- if (try_connect(vcam, device)) {
+ if (try_connect(data, device, name)) {
success = true;
break;
}
@@ -230,35 +170,61 @@ static bool virtualcam_start(void *data)
free(list[n]);
free(list);
- if (!success)
- blog(LOG_WARNING, "Failed to start virtual camera");
-
return success;
}
-static void virtualcam_stop(void *data, uint64_t ts)
+void *video_start(obs_output_t *output)
{
- struct virtualcam_data *vcam = (struct virtualcam_data *)data;
- obs_output_end_data_capture(vcam->output);
+ if (!loopback_module_loaded()) {
+ if (loopback_module_load() != 0) {
+ return NULL;
+ }
+ }
- struct v4l2_streamparm parm = {0};
- parm.type = V4L2_BUF_TYPE_VIDEO_OUTPUT;
+ struct v4l2_output_data data = {
+ .output = output,
+ .device = -1,
+ .frame_size = 0,
+ };
- if (ioctl(vcam->device, VIDIOC_STREAMOFF, &parm) < 0) {
- blog(LOG_WARNING,
- "Failed to stop streaming on video device %d (%s)",
- vcam->device, strerror(errno));
+ bool success = loopback_card_open(&data, OBS_V4L2_CARD_LABEL);
+
+ if (!success) {
+ // TODO: Parse the output of add command and connect directly
+ loopback_module_add_card();
+ success = loopback_card_open(&data, OBS_V4L2_CARD_LABEL);
}
- close(vcam->device);
- blog(LOG_INFO, "Virtual camera stopped");
+ if (!success) {
+ success = loopback_card_open(&data, NULL);
+ }
+
+ if (success) {
+ struct v4l2_output_data *vcam =
+ (struct v4l2_output_data *)bzalloc(sizeof(*vcam));
+
+ memcpy(vcam, &data, sizeof(*vcam));
+ return vcam;
+ }
+
+ blog(LOG_WARNING, "Failed to start v4l2 output");
+ return NULL;
+}
+
+void video_stop(void *data)
+{
+ if (!data)
+ return;
- UNUSED_PARAMETER(ts);
+ struct v4l2_output_data *vcam = (struct v4l2_output_data *)data;
+
+ close(vcam->device);
+ bfree(data);
}
-static void virtual_video(void *param, struct video_data *frame)
+void virtual_video(void *data, struct video_data *frame)
{
- struct virtualcam_data *vcam = (struct virtualcam_data *)param;
+ struct v4l2_output_data *vcam = (struct v4l2_output_data *)data;
uint32_t frame_size = vcam->frame_size;
while (frame_size > 0) {
ssize_t written =
@@ -268,14 +234,3 @@ static void virtual_video(void *param, struct video_data *frame)
frame_size -= written;
}
}
-
-struct obs_output_info virtualcam_info = {
- .id = "virtualcam_output",
- .flags = OBS_OUTPUT_VIDEO,
- .get_name = virtualcam_name,
- .create = virtualcam_create,
- .destroy = virtualcam_destroy,
- .start = virtualcam_start,
- .stop = virtualcam_stop,
- .raw_video = virtual_video,
-};