Skip to content

Commit 76de0c9

Browse files
authored
support camera selection on Linux (#44)
1 parent 778ba52 commit 76de0c9

File tree

11 files changed

+127
-79
lines changed

11 files changed

+127
-79
lines changed

.github/scripts/build-windows.ps1

+1-9
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,9 @@ function Init-VS {
2222
# setuptools automatically selects the right compiler for building
2323
# the extension module. The following is mostly for building any
2424
# dependencies like libraw.
25-
# FIXME choose matching VC++ compiler, maybe using -vcvars_ver
26-
# -> dependencies should not be built with newer compiler than Python itself
2725
# https://docs.microsoft.com/en-us/cpp/build/building-on-the-command-line
2826
# https://docs.microsoft.com/en-us/cpp/porting/binary-compat-2015-2017
2927

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

@@ -41,12 +38,7 @@ function Init-VS {
4138
if ($PYTHON_VERSION_MINOR -le '4') {
4239
throw ("Python <= 3.4 unsupported: $env:PYTHON_VERSION")
4340
}
44-
if (exists $VS2015_ROOT) {
45-
$VS_VERSION = "2015"
46-
$VS_ROOT = $VS2015_ROOT
47-
$VS_INIT_CMD = "$VS_ROOT\VC\vcvarsall.bat"
48-
$VS_INIT_ARGS = "$VS_ARCH"
49-
} elseif (exists $VS2017_ROOT) {
41+
if (exists $VS2017_ROOT) {
5042
$VS_VERSION = "2017"
5143
if (exists "$VS2017_ROOT\Enterprise") {
5244
$VS_ROOT = "$VS2017_ROOT\Enterprise"

.github/workflows/ci.yml

+4-6
Original file line numberDiff line numberDiff line change
@@ -54,24 +54,22 @@ jobs:
5454
python-version: '3.9'
5555
numpy-version: '1.19.*'
5656

57-
# Use 2016 image as 2019 does not have VC++ 14.0 compiler.
58-
# https://github.community/t5/GitHub-Actions/Microsoft-Visual-C-14-0-compiler-not-available-on-Windows-2019/m-p/32871
59-
- os-image: windows-2016
57+
- os-image: windows-latest
6058
os-name: windows
6159
python-version: '3.6'
6260
python-arch: '64'
6361
numpy-version: '1.11'
64-
- os-image: windows-2016
62+
- os-image: windows-latest
6563
os-name: windows
6664
python-version: '3.7'
6765
python-arch: '64'
6866
numpy-version: '1.14'
69-
- os-image: windows-2016
67+
- os-image: windows-latest
7068
os-name: windows
7169
python-version: '3.8'
7270
python-arch: '64'
7371
numpy-version: '1.17'
74-
- os-image: windows-2016
72+
- os-image: windows-latest
7573
os-name: windows
7674
python-version: '3.9'
7775
python-arch: '64'

pyvirtualcam/camera.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ def __str__(self):
4848
class Camera:
4949
def __init__(self, width: int, height: int, fps: float, *,
5050
fmt: PixelFormat=PixelFormat.RGB,
51+
device: Optional[str]=None,
5152
backend: Optional[str]=None,
5253
print_fps=False,
5354
delay=None,
@@ -61,7 +62,9 @@ def __init__(self, width: int, height: int, fps: float, *,
6162
for name, clazz in backends:
6263
try:
6364
self._backend = clazz(
64-
width=width, height=height, fps=fps, fourcc=encode_fourcc(fmt.value),
65+
width=width, height=height, fps=fps,
66+
fourcc=encode_fourcc(fmt.value),
67+
device=device,
6568
**kw)
6669
except Exception as e:
6770
errors.append(f"'{name}' backend: {e}")

pyvirtualcam/native_linux_v4l2loopback/main.cpp

+7-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
#include <stdexcept>
2+
#include <optional>
23
#include <pybind11/pybind11.h>
4+
#include <pybind11/stl.h>
35
#include <pybind11/numpy.h>
46
#include "virtual_output.h"
57

@@ -10,8 +12,9 @@ class Camera {
1012
VirtualOutput virtual_output;
1113

1214
public:
13-
Camera(uint32_t width, uint32_t height, [[maybe_unused]] double fps, uint32_t fourcc)
14-
: virtual_output {width, height, fourcc} {
15+
Camera(uint32_t width, uint32_t height, [[maybe_unused]] double fps,
16+
uint32_t fourcc, std::optional<std::string> device_)
17+
: virtual_output {width, height, fourcc, device_} {
1518
}
1619

1720
void close() {
@@ -34,9 +37,9 @@ class Camera {
3437

3538
PYBIND11_MODULE(_native_linux_v4l2loopback, m) {
3639
py::class_<Camera>(m, "Camera")
37-
.def(py::init<uint32_t, uint32_t, double, uint32_t>(),
40+
.def(py::init<uint32_t, uint32_t, double, uint32_t, std::optional<std::string>>(),
3841
py::arg("width"), py::arg("height"), py::arg("fps"),
39-
py::kw_only(), py::arg("fourcc"))
42+
py::kw_only(), py::arg("fourcc"), py::arg("device"))
4043
.def("close", &Camera::close)
4144
.def("send", &Camera::send)
4245
.def("device", &Camera::device)

pyvirtualcam/native_linux_v4l2loopback/virtual_output.h

+60-32
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
#include <sys/ioctl.h>
1010
#include <linux/videodev2.h>
1111

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

2627
class VirtualOutput {
2728
private:
2829
bool _output_running = false;
2930
int _camera_fd;
3031
std::string _camera_device;
31-
size_t _camera_device_idx;
3232
uint32_t _frame_fourcc;
3333
uint32_t _native_fourcc;
3434
uint32_t _frame_width;
@@ -37,7 +37,8 @@ class VirtualOutput {
3737
std::vector<uint8_t> _buffer_output;
3838

3939
public:
40-
VirtualOutput(uint32_t width, uint32_t height, uint32_t fourcc) {
40+
VirtualOutput(uint32_t width, uint32_t height, uint32_t fourcc,
41+
std::optional<std::string> device_) {
4142
_frame_width = width;
4243
_frame_height = height;
4344
_frame_fourcc = libyuv::CanonicalFourCC(fourcc);
@@ -82,50 +83,78 @@ class VirtualOutput {
8283
throw std::runtime_error("Unsupported image format.");
8384
}
8485

85-
char device_name[14];
86-
int device_idx = -1;
87-
88-
for (size_t i = 0; i < 100; i++) {
89-
if (ACTIVE_DEVICES.count(i)) {
90-
continue;
86+
auto try_open = [&](const std::string& device_name) {
87+
if (ACTIVE_DEVICES.count(device_name)) {
88+
throw std::invalid_argument(
89+
"Device " + device_name + " is already in use."
90+
);
9191
}
92-
sprintf(device_name, "/dev/video%zu", i);
93-
_camera_fd = open(device_name, O_WRONLY | O_SYNC);
92+
_camera_fd = open(device_name.c_str(), O_WRONLY | O_SYNC);
9493
if (_camera_fd == -1) {
9594
if (errno == EACCES) {
9695
throw std::runtime_error(
97-
"Could not access " + std::string(device_name) + " due to missing permissions. "
96+
"Could not access " + device_name + " due to missing permissions. "
9897
"Did you add your user to the 'video' group? "
9998
"Run 'usermod -a -G video myusername' and log out and in again."
10099
);
100+
} else if (errno == ENOENT) {
101+
throw std::invalid_argument(
102+
"Device " + device_name + " does not exist."
103+
);
104+
} else {
105+
throw std::invalid_argument(
106+
"Device " + device_name + " could not be opened: " +
107+
std::string(strerror(errno))
108+
);
101109
}
102-
continue;
103110
}
104111

105112
struct v4l2_capability camera_cap;
106113

107114
if (ioctl(_camera_fd, VIDIOC_QUERYCAP, &camera_cap) == -1) {
108-
continue;
115+
throw std::invalid_argument(
116+
"Device capabilities of " + device_name + " could not be queried."
117+
);
109118
}
110119
if (!(camera_cap.capabilities & V4L2_CAP_VIDEO_OUTPUT)) {
111-
continue;
120+
throw std::invalid_argument(
121+
"Device " + device_name + " is not a video output device."
122+
);
112123
}
113124
if (strcmp((const char*)camera_cap.driver, "v4l2 loopback") != 0) {
114-
continue;
125+
throw std::invalid_argument(
126+
"Device " + device_name + " is not a V4L2 device."
127+
);
128+
}
129+
};
130+
131+
std::string device_name;
132+
133+
if (device_.has_value()) {
134+
device_name = device_.value();
135+
try_open(device_name);
136+
} else {
137+
bool found = false;
138+
for (size_t i = 0; i < 100; i++) {
139+
std::ostringstream device_name_s;
140+
device_name_s << "/dev/video" << i;
141+
device_name = device_name_s.str();
142+
try {
143+
try_open(device_name);
144+
} catch (std::invalid_argument&) {
145+
continue;
146+
}
147+
found = true;
148+
break;
149+
}
150+
if (!found) {
151+
throw std::runtime_error(
152+
"No v4l2 loopback device found at /dev/video[0-99]. "
153+
"Did you run 'modprobe v4l2loopback'? "
154+
"See also pyvirtualcam's documentation.");
115155
}
116-
device_idx = i;
117-
break;
118-
}
119-
if (device_idx == -1) {
120-
throw std::runtime_error(
121-
"No v4l2 loopback device found at /dev/video[0-99]. "
122-
"Did you run 'modprobe v4l2loopback'? "
123-
"See also pyvirtualcam's documentation.");
124156
}
125157

126-
uint32_t half_width = width / 2;
127-
uint32_t half_height = height / 2;
128-
129158
v4l2_format v4l2_fmt;
130159
memset(&v4l2_fmt, 0, sizeof(v4l2_fmt));
131160
v4l2_fmt.type = V4L2_BUF_TYPE_VIDEO_OUTPUT;
@@ -139,16 +168,15 @@ class VirtualOutput {
139168
if (ioctl(_camera_fd, VIDIOC_S_FMT, &v4l2_fmt) == -1) {
140169
close(_camera_fd);
141170
throw std::runtime_error(
142-
"Virtual camera device " + std::string(device_name) +
171+
"Virtual camera device " + device_name +
143172
" could not be configured: " + std::string(strerror(errno))
144173
);
145174
}
146175

147176
_output_running = true;
148-
_camera_device = std::string(device_name);
149-
_camera_device_idx = device_idx;
177+
_camera_device = device_name;
150178

151-
ACTIVE_DEVICES.insert(device_idx);
179+
ACTIVE_DEVICES.insert(_camera_device);
152180
}
153181

154182
void stop() {
@@ -159,7 +187,7 @@ class VirtualOutput {
159187
close(_camera_fd);
160188

161189
_output_running = false;
162-
ACTIVE_DEVICES.erase(_camera_device_idx);
190+
ACTIVE_DEVICES.erase(_camera_device);
163191
}
164192

165193
void send(const uint8_t* frame) {

pyvirtualcam/native_macos_obs/main.mm

+7-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
#include <stdexcept>
2+
#include <optional>
23
#include <pybind11/pybind11.h>
4+
#include <pybind11/stl.h>
35
#include <pybind11/numpy.h>
46
#include <cstdint>
57
#include <string>
@@ -12,8 +14,9 @@
1214
VirtualOutput virtual_output;
1315

1416
public:
15-
Camera(uint32_t width, uint32_t height, double fps, uint32_t fourcc)
16-
: virtual_output {width, height, fps, fourcc} {
17+
Camera(uint32_t width, uint32_t height, double fps,
18+
uint32_t fourcc, std::optional<std::string> device_)
19+
: virtual_output {width, height, fps, fourcc, device_} {
1720
}
1821

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

3740
PYBIND11_MODULE(_native_macos_obs, m) {
3841
py::class_<Camera>(m, "Camera")
39-
.def(py::init<uint32_t, uint32_t, double, uint32_t>(),
42+
.def(py::init<uint32_t, uint32_t, double, uint32_t, std::optional<std::string>>(),
4043
py::arg("width"), py::arg("height"), py::arg("fps"),
41-
py::kw_only(), py::arg("fourcc"))
44+
py::kw_only(), py::arg("fourcc"), py::arg("device"))
4245
.def("close", &Camera::close)
4346
.def("send", &Camera::send)
4447
.def("device", &Camera::device)

pyvirtualcam/native_macos_obs/virtual_output.h

+8-1
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@ class VirtualOutput {
4040
std::vector<uint8_t> _buffer_output;
4141

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

55+
if (device_.has_value() && device_ != device()) {
56+
throw std::invalid_argument(
57+
"This backend supports only the '" + device() + "' device."
58+
);
59+
}
60+
5461
_frame_fourcc = libyuv::CanonicalFourCC(fourcc);
5562
_frame_width = width;
5663
_frame_height = height;

pyvirtualcam/native_windows_obs/main.cpp

+7-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
#include <stdexcept>
2+
#include <optional>
23
#include <pybind11/pybind11.h>
4+
#include <pybind11/stl.h>
35
#include <pybind11/numpy.h>
46
#include "virtual_output.h"
57

@@ -10,8 +12,9 @@ class Camera {
1012
VirtualOutput virtual_output;
1113

1214
public:
13-
Camera(uint32_t width, uint32_t height, double fps, uint32_t fourcc)
14-
: virtual_output {width, height, fps, fourcc} {
15+
Camera(uint32_t width, uint32_t height, double fps, uint32_t fourcc,
16+
std::optional<std::string> device_)
17+
: virtual_output {width, height, fps, fourcc, device_} {
1518
}
1619

1720
void close() {
@@ -34,9 +37,9 @@ class Camera {
3437

3538
PYBIND11_MODULE(_native_windows_obs, m) {
3639
py::class_<Camera>(m, "Camera")
37-
.def(py::init<uint32_t, uint32_t, double, uint32_t>(),
40+
.def(py::init<uint32_t, uint32_t, double, uint32_t, std::optional<std::string>>(),
3841
py::arg("width"), py::arg("height"), py::arg("fps"),
39-
py::kw_only(), py::arg("fourcc"))
42+
py::kw_only(), py::arg("fourcc"), py::arg("device"))
4043
.def("close", &Camera::close)
4144
.def("send", &Camera::send)
4245
.def("device", &Camera::device)

pyvirtualcam/native_windows_obs/virtual_output.h

+8-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@ class VirtualOutput {
3535
}
3636

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

50+
if (device_.has_value() && device_ != device()) {
51+
throw std::invalid_argument(
52+
"This backend supports only the '" + device() + "' device."
53+
);
54+
}
55+
4956
_frame_fourcc = libyuv::CanonicalFourCC(fourcc);
5057
_frame_width = width;
5158
_frame_height = height;

0 commit comments

Comments
 (0)