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, +};