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