Skip to content

Commit

Permalink
support camera selection on Linux (#44)
Browse files Browse the repository at this point in the history
  • Loading branch information
letmaik authored Mar 22, 2021
1 parent 778ba52 commit 76de0c9
Show file tree
Hide file tree
Showing 11 changed files with 127 additions and 79 deletions.
10 changes: 1 addition & 9 deletions .github/scripts/build-windows.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,9 @@ function Init-VS {
# setuptools automatically selects the right compiler for building
# the extension module. The following is mostly for building any
# dependencies like libraw.
# FIXME choose matching VC++ compiler, maybe using -vcvars_ver
# -> dependencies should not be built with newer compiler than Python itself
# https://docs.microsoft.com/en-us/cpp/build/building-on-the-command-line
# https://docs.microsoft.com/en-us/cpp/porting/binary-compat-2015-2017

$VS2015_ROOT = "C:\Program Files (x86)\Microsoft Visual Studio 14.0"
$VS2017_ROOT = "C:\Program Files (x86)\Microsoft Visual Studio\2017"
$VS2019_ROOT = "C:\Program Files (x86)\Microsoft Visual Studio\2019"

Expand All @@ -41,12 +38,7 @@ function Init-VS {
if ($PYTHON_VERSION_MINOR -le '4') {
throw ("Python <= 3.4 unsupported: $env:PYTHON_VERSION")
}
if (exists $VS2015_ROOT) {
$VS_VERSION = "2015"
$VS_ROOT = $VS2015_ROOT
$VS_INIT_CMD = "$VS_ROOT\VC\vcvarsall.bat"
$VS_INIT_ARGS = "$VS_ARCH"
} elseif (exists $VS2017_ROOT) {
if (exists $VS2017_ROOT) {
$VS_VERSION = "2017"
if (exists "$VS2017_ROOT\Enterprise") {
$VS_ROOT = "$VS2017_ROOT\Enterprise"
Expand Down
10 changes: 4 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,24 +54,22 @@ jobs:
python-version: '3.9'
numpy-version: '1.19.*'

# Use 2016 image as 2019 does not have VC++ 14.0 compiler.
# https://github.community/t5/GitHub-Actions/Microsoft-Visual-C-14-0-compiler-not-available-on-Windows-2019/m-p/32871
- os-image: windows-2016
- os-image: windows-latest
os-name: windows
python-version: '3.6'
python-arch: '64'
numpy-version: '1.11'
- os-image: windows-2016
- os-image: windows-latest
os-name: windows
python-version: '3.7'
python-arch: '64'
numpy-version: '1.14'
- os-image: windows-2016
- os-image: windows-latest
os-name: windows
python-version: '3.8'
python-arch: '64'
numpy-version: '1.17'
- os-image: windows-2016
- os-image: windows-latest
os-name: windows
python-version: '3.9'
python-arch: '64'
Expand Down
5 changes: 4 additions & 1 deletion pyvirtualcam/camera.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ def __str__(self):
class Camera:
def __init__(self, width: int, height: int, fps: float, *,
fmt: PixelFormat=PixelFormat.RGB,
device: Optional[str]=None,
backend: Optional[str]=None,
print_fps=False,
delay=None,
Expand All @@ -61,7 +62,9 @@ def __init__(self, width: int, height: int, fps: float, *,
for name, clazz in backends:
try:
self._backend = clazz(
width=width, height=height, fps=fps, fourcc=encode_fourcc(fmt.value),
width=width, height=height, fps=fps,
fourcc=encode_fourcc(fmt.value),
device=device,
**kw)
except Exception as e:
errors.append(f"'{name}' backend: {e}")
Expand Down
11 changes: 7 additions & 4 deletions pyvirtualcam/native_linux_v4l2loopback/main.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
#include <stdexcept>
#include <optional>
#include <pybind11/pybind11.h>
#include <pybind11/stl.h>
#include <pybind11/numpy.h>
#include "virtual_output.h"

Expand All @@ -10,8 +12,9 @@ class Camera {
VirtualOutput virtual_output;

public:
Camera(uint32_t width, uint32_t height, [[maybe_unused]] double fps, uint32_t fourcc)
: virtual_output {width, height, fourcc} {
Camera(uint32_t width, uint32_t height, [[maybe_unused]] double fps,
uint32_t fourcc, std::optional<std::string> device_)
: virtual_output {width, height, fourcc, device_} {
}

void close() {
Expand All @@ -34,9 +37,9 @@ class Camera {

PYBIND11_MODULE(_native_linux_v4l2loopback, m) {
py::class_<Camera>(m, "Camera")
.def(py::init<uint32_t, uint32_t, double, uint32_t>(),
.def(py::init<uint32_t, uint32_t, double, uint32_t, std::optional<std::string>>(),
py::arg("width"), py::arg("height"), py::arg("fps"),
py::kw_only(), py::arg("fourcc"))
py::kw_only(), py::arg("fourcc"), py::arg("device"))
.def("close", &Camera::close)
.def("send", &Camera::send)
.def("device", &Camera::device)
Expand Down
92 changes: 60 additions & 32 deletions pyvirtualcam/native_linux_v4l2loopback/virtual_output.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#include <sys/ioctl.h>
#include <linux/videodev2.h>

#include <string>
#include <vector>
#include <set>
#include <stdexcept>
Expand All @@ -21,14 +22,13 @@
// Obviously, this won't help if multiple processes are used
// or if devices are opened by other tools.
// In this case, explicitly specifying the device seems the only solution.
static std::set<size_t> ACTIVE_DEVICES;
static std::set<std::string> ACTIVE_DEVICES;

class VirtualOutput {
private:
bool _output_running = false;
int _camera_fd;
std::string _camera_device;
size_t _camera_device_idx;
uint32_t _frame_fourcc;
uint32_t _native_fourcc;
uint32_t _frame_width;
Expand All @@ -37,7 +37,8 @@ class VirtualOutput {
std::vector<uint8_t> _buffer_output;

public:
VirtualOutput(uint32_t width, uint32_t height, uint32_t fourcc) {
VirtualOutput(uint32_t width, uint32_t height, uint32_t fourcc,
std::optional<std::string> device_) {
_frame_width = width;
_frame_height = height;
_frame_fourcc = libyuv::CanonicalFourCC(fourcc);
Expand Down Expand Up @@ -82,50 +83,78 @@ class VirtualOutput {
throw std::runtime_error("Unsupported image format.");
}

char device_name[14];
int device_idx = -1;

for (size_t i = 0; i < 100; i++) {
if (ACTIVE_DEVICES.count(i)) {
continue;
auto try_open = [&](const std::string& device_name) {
if (ACTIVE_DEVICES.count(device_name)) {
throw std::invalid_argument(
"Device " + device_name + " is already in use."
);
}
sprintf(device_name, "/dev/video%zu", i);
_camera_fd = open(device_name, O_WRONLY | O_SYNC);
_camera_fd = open(device_name.c_str(), O_WRONLY | O_SYNC);
if (_camera_fd == -1) {
if (errno == EACCES) {
throw std::runtime_error(
"Could not access " + std::string(device_name) + " due to missing permissions. "
"Could not access " + device_name + " due to missing permissions. "
"Did you add your user to the 'video' group? "
"Run 'usermod -a -G video myusername' and log out and in again."
);
} else if (errno == ENOENT) {
throw std::invalid_argument(
"Device " + device_name + " does not exist."
);
} else {
throw std::invalid_argument(
"Device " + device_name + " could not be opened: " +
std::string(strerror(errno))
);
}
continue;
}

struct v4l2_capability camera_cap;

if (ioctl(_camera_fd, VIDIOC_QUERYCAP, &camera_cap) == -1) {
continue;
throw std::invalid_argument(
"Device capabilities of " + device_name + " could not be queried."
);
}
if (!(camera_cap.capabilities & V4L2_CAP_VIDEO_OUTPUT)) {
continue;
throw std::invalid_argument(
"Device " + device_name + " is not a video output device."
);
}
if (strcmp((const char*)camera_cap.driver, "v4l2 loopback") != 0) {
continue;
throw std::invalid_argument(
"Device " + device_name + " is not a V4L2 device."
);
}
};

std::string device_name;

if (device_.has_value()) {
device_name = device_.value();
try_open(device_name);
} else {
bool found = false;
for (size_t i = 0; i < 100; i++) {
std::ostringstream device_name_s;
device_name_s << "/dev/video" << i;
device_name = device_name_s.str();
try {
try_open(device_name);
} catch (std::invalid_argument&) {
continue;
}
found = true;
break;
}
if (!found) {
throw std::runtime_error(
"No v4l2 loopback device found at /dev/video[0-99]. "
"Did you run 'modprobe v4l2loopback'? "
"See also pyvirtualcam's documentation.");
}
device_idx = i;
break;
}
if (device_idx == -1) {
throw std::runtime_error(
"No v4l2 loopback device found at /dev/video[0-99]. "
"Did you run 'modprobe v4l2loopback'? "
"See also pyvirtualcam's documentation.");
}

uint32_t half_width = width / 2;
uint32_t half_height = height / 2;

v4l2_format v4l2_fmt;
memset(&v4l2_fmt, 0, sizeof(v4l2_fmt));
v4l2_fmt.type = V4L2_BUF_TYPE_VIDEO_OUTPUT;
Expand All @@ -139,16 +168,15 @@ class VirtualOutput {
if (ioctl(_camera_fd, VIDIOC_S_FMT, &v4l2_fmt) == -1) {
close(_camera_fd);
throw std::runtime_error(
"Virtual camera device " + std::string(device_name) +
"Virtual camera device " + device_name +
" could not be configured: " + std::string(strerror(errno))
);
}

_output_running = true;
_camera_device = std::string(device_name);
_camera_device_idx = device_idx;
_camera_device = device_name;

ACTIVE_DEVICES.insert(device_idx);
ACTIVE_DEVICES.insert(_camera_device);
}

void stop() {
Expand All @@ -159,7 +187,7 @@ class VirtualOutput {
close(_camera_fd);

_output_running = false;
ACTIVE_DEVICES.erase(_camera_device_idx);
ACTIVE_DEVICES.erase(_camera_device);
}

void send(const uint8_t* frame) {
Expand Down
11 changes: 7 additions & 4 deletions pyvirtualcam/native_macos_obs/main.mm
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
#include <stdexcept>
#include <optional>
#include <pybind11/pybind11.h>
#include <pybind11/stl.h>
#include <pybind11/numpy.h>
#include <cstdint>
#include <string>
Expand All @@ -12,8 +14,9 @@
VirtualOutput virtual_output;

public:
Camera(uint32_t width, uint32_t height, double fps, uint32_t fourcc)
: virtual_output {width, height, fps, fourcc} {
Camera(uint32_t width, uint32_t height, double fps,
uint32_t fourcc, std::optional<std::string> device_)
: virtual_output {width, height, fps, fourcc, device_} {
}

void close() {
Expand All @@ -36,9 +39,9 @@ void send(py::array_t<uint8_t, py::array::c_style> frame) {

PYBIND11_MODULE(_native_macos_obs, m) {
py::class_<Camera>(m, "Camera")
.def(py::init<uint32_t, uint32_t, double, uint32_t>(),
.def(py::init<uint32_t, uint32_t, double, uint32_t, std::optional<std::string>>(),
py::arg("width"), py::arg("height"), py::arg("fps"),
py::kw_only(), py::arg("fourcc"))
py::kw_only(), py::arg("fourcc"), py::arg("device"))
.def("close", &Camera::close)
.def("send", &Camera::send)
.def("device", &Camera::device)
Expand Down
9 changes: 8 additions & 1 deletion pyvirtualcam/native_macos_obs/virtual_output.h
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ class VirtualOutput {
std::vector<uint8_t> _buffer_output;

public:
VirtualOutput(uint32_t width, uint32_t height, double fps, uint32_t fourcc) {
VirtualOutput(uint32_t width, uint32_t height, double fps, uint32_t fourcc,
std::optional<std::string> device_) {
NSString *dal_plugin_path = @"/Library/CoreMediaIO/Plug-Ins/DAL/obs-mac-virtualcam.plugin";
NSFileManager *file_manager = [NSFileManager defaultManager];
BOOL dal_plugin_installed = [file_manager fileExistsAtPath:dal_plugin_path];
Expand All @@ -51,6 +52,12 @@ class VirtualOutput {
);
}

if (device_.has_value() && device_ != device()) {
throw std::invalid_argument(
"This backend supports only the '" + device() + "' device."
);
}

_frame_fourcc = libyuv::CanonicalFourCC(fourcc);
_frame_width = width;
_frame_height = height;
Expand Down
11 changes: 7 additions & 4 deletions pyvirtualcam/native_windows_obs/main.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
#include <stdexcept>
#include <optional>
#include <pybind11/pybind11.h>
#include <pybind11/stl.h>
#include <pybind11/numpy.h>
#include "virtual_output.h"

Expand All @@ -10,8 +12,9 @@ class Camera {
VirtualOutput virtual_output;

public:
Camera(uint32_t width, uint32_t height, double fps, uint32_t fourcc)
: virtual_output {width, height, fps, fourcc} {
Camera(uint32_t width, uint32_t height, double fps, uint32_t fourcc,
std::optional<std::string> device_)
: virtual_output {width, height, fps, fourcc, device_} {
}

void close() {
Expand All @@ -34,9 +37,9 @@ class Camera {

PYBIND11_MODULE(_native_windows_obs, m) {
py::class_<Camera>(m, "Camera")
.def(py::init<uint32_t, uint32_t, double, uint32_t>(),
.def(py::init<uint32_t, uint32_t, double, uint32_t, std::optional<std::string>>(),
py::arg("width"), py::arg("height"), py::arg("fps"),
py::kw_only(), py::arg("fourcc"))
py::kw_only(), py::arg("fourcc"), py::arg("device"))
.def("close", &Camera::close)
.def("send", &Camera::send)
.def("device", &Camera::device)
Expand Down
9 changes: 8 additions & 1 deletion pyvirtualcam/native_windows_obs/virtual_output.h
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ class VirtualOutput {
}

public:
VirtualOutput(uint32_t width, uint32_t height, double fps, uint32_t fourcc) {
VirtualOutput(uint32_t width, uint32_t height, double fps, uint32_t fourcc,
std::optional<std::string> device_) {
// https://github.com/obsproject/obs-studio/blob/9da6fc67/.github/workflows/main.yml#L484
LPCWSTR guid = L"CLSID\\{A3FCE0F5-3493-419F-958A-ABA1250EC20B}";
HKEY key = nullptr;
Expand All @@ -46,6 +47,12 @@ class VirtualOutput {
);
}

if (device_.has_value() && device_ != device()) {
throw std::invalid_argument(
"This backend supports only the '" + device() + "' device."
);
}

_frame_fourcc = libyuv::CanonicalFourCC(fourcc);
_frame_width = width;
_frame_height = height;
Expand Down
Loading

0 comments on commit 76de0c9

Please sign in to comment.