diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index f22342349b72f1d8d6b6a70ce687e8e2fb77f0d7..55541b787fbe16556e86f762b4af1c1d0c2d60f0 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -391,7 +391,9 @@ jobs:
qtbase5-dev \
libqt5svg5-dev \
swig \
- libcmocka-dev
+ libcmocka-dev \
+ linux-generic \
+ v4l2loopback-dkms
- name: 'Restore Chromium Embedded Framework from cache'
id: cef-cache
uses: actions/cache@v2.1.2
diff --git a/CI/install-dependencies-linux.sh b/CI/install-dependencies-linux.sh
index 0d92fbf97b0a6cb818495c7597c65925eedcc54a..dd6bb61ea6d292efb6b16076bef86d83c55a5438 100755
--- a/CI/install-dependencies-linux.sh
+++ b/CI/install-dependencies-linux.sh
@@ -43,7 +43,9 @@ sudo apt-get install -y \
python3-dev \
qtbase5-dev \
libqt5svg5-dev \
- swig
+ swig \
+ linux-generic \
+ v4l2loopback-dkms
# build cef
wget --quiet --retry-connrefused --waitretry=1 https://cdn-fastly.obsproject.com/downloads/cef_binary_${CEF_BUILD_VERSION}_linux64.tar.bz2
diff --git a/plugins/linux-v4l2/CMakeLists.txt b/plugins/linux-v4l2/CMakeLists.txt
index 89882cc958363854a059269bf8f5a15824066321..51047ec592e74b43fb80714840bae02cd53daa88 100644
--- a/plugins/linux-v4l2/CMakeLists.txt
+++ b/plugins/linux-v4l2/CMakeLists.txt
@@ -39,6 +39,7 @@ set(linux-v4l2_SOURCES
v4l2-controls.c
v4l2-input.c
v4l2-helpers.c
+ v4l2-output.c
${linux-v4l2-udev_SOURCES}
)
diff --git a/plugins/linux-v4l2/linux-v4l2.c b/plugins/linux-v4l2/linux-v4l2.c
index b57deaa3498ad8541d055d60c3ff97a2320f626b..3de38d5acff3ceba716600f88a6cbef4c2b52b6b 100644
--- a/plugins/linux-v4l2/linux-v4l2.c
+++ b/plugins/linux-v4l2/linux-v4l2.c
@@ -15,18 +15,47 @@ 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";
+ return "Video4Linux2(V4L2) sources/virtual camera";
}
extern struct obs_source_info v4l2_input;
+extern struct obs_output_info virtualcam_info;
+
+static bool v4l2loopback_installed()
+{
+ bool loaded = false;
+
+ int ret = system("modinfo v4l2loopback");
+
+ if (ret == 0)
+ loaded = true;
+
+ return loaded;
+}
bool obs_module_load(void)
{
obs_register_source(&v4l2_input);
+
+ obs_data_t *obs_settings = obs_data_create();
+
+ if (v4l2loopback_installed()) {
+ 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-v4l2/v4l2-output.c b/plugins/linux-v4l2/v4l2-output.c
new file mode 100644
index 0000000000000000000000000000000000000000..e2c9a647ce430edd4013795202494271ed8a9d12
--- /dev/null
+++ b/plugins/linux-v4l2/v4l2-output.c
@@ -0,0 +1,179 @@
+#include
+#include
+#include
+#include
+#include
+#include
+
+#define MAX_DEVICES 64
+
+struct virtualcam_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 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;
+ }
+ }
+
+ if (fp)
+ fclose(fp);
+
+ return loaded;
+}
+
+static int loopback_module_load()
+{
+ return system(
+ "pkexec modprobe v4l2loopback exclusive_caps=1 card_label='OBS Virtual Camera' && sleep 0.5");
+}
+
+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 bool try_connect(void *data, int device)
+{
+ 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;
+
+ char new_device[16];
+ sprintf(new_device, "/dev/video%d", device);
+
+ vcam->device = open(new_device, O_RDWR);
+
+ if (vcam->device < 0)
+ return false;
+
+ if (ioctl(vcam->device, VIDIOC_QUERYCAP, &capability) < 0)
+ return false;
+
+ format.type = V4L2_BUF_TYPE_VIDEO_OUTPUT;
+
+ if (ioctl(vcam->device, VIDIOC_G_FMT, &format) < 0)
+ return false;
+
+ struct obs_video_info ovi;
+ obs_get_video_info(&ovi);
+
+ 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;
+ parm.parm.output.outputmode = 0;
+ parm.parm.output.writebuffers = 0;
+ parm.parm.output.extendedmode = 0;
+ parm.parm.output.reserved[4] = 0;
+
+ if (ioctl(vcam->device, VIDIOC_S_PARM, &parm) < 0)
+ return false;
+
+ format.fmt.pix.width = width;
+ format.fmt.pix.height = height;
+ format.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV;
+ format.fmt.pix.sizeimage = vcam->frame_size;
+
+ if (ioctl(vcam->device, VIDIOC_S_FMT, &format) < 0)
+ return false;
+
+ struct video_scale_info vsi = {0};
+ vsi.format = VIDEO_FORMAT_YUY2;
+ vsi.width = width;
+ vsi.height = height;
+ obs_output_set_video_conversion(vcam->output, &vsi);
+
+ blog(LOG_INFO, "Virtual camera started");
+ obs_output_begin_data_capture(vcam->output, 0);
+
+ return true;
+}
+
+static bool virtualcam_start(void *data)
+{
+ struct virtualcam_data *vcam = (struct virtualcam_data *)data;
+
+ if (!loopback_module_loaded()) {
+ if (loopback_module_load() != 0)
+ return false;
+ }
+
+ for (int i = 0; i < MAX_DEVICES; i++) {
+ if (!try_connect(vcam, i))
+ continue;
+ else
+ return true;
+ }
+
+ blog(LOG_WARNING, "Failed to start virtual camera");
+ 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);
+ close(vcam->device);
+
+ blog(LOG_INFO, "Virtual camera stopped");
+
+ UNUSED_PARAMETER(ts);
+}
+
+static void virtual_video(void *param, struct video_data *frame)
+{
+ struct virtualcam_data *vcam = (struct virtualcam_data *)param;
+ write(vcam->device, frame->data[0], vcam->frame_size);
+}
+
+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,
+};