Skip to content

Commit

Permalink
Merge pull request #28 from tiagocoutinho/video-lazy-controls
Browse files Browse the repository at this point in the history
Video lazy controls
  • Loading branch information
tiagocoutinho authored Aug 2, 2024
2 parents 6ba558e + d7ddfc3 commit 875d27a
Show file tree
Hide file tree
Showing 3 changed files with 70 additions and 18 deletions.
4 changes: 3 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,13 @@ jobs:
sudo modprobe -i uinput
echo KERNEL==\"uinput\", SUBSYSTEM==\"misc\" GROUP=\"docker\", MODE:=\"0666\" | sudo tee /etc/udev/rules.d/99-$USER.rules
echo KERNEL==\"event[0-9]*\", SUBSYSTEM==\"input\" GROUP=\"docker\", MODE:=\"0666\" | sudo tee -a /etc/udev/rules.d/99-$USER.rules
echo SUBSYSTEM==\"video4linux\" GROUP=\"docker\", MODE:=\"0666\" | sudo tee -a /etc/udev/rules.d/99-$USER.rules
cat /etc/udev/rules.d/99-$USER.rules
sudo udevadm control --reload-rules
sudo udevadm trigger
sudo modprobe vivid n_devs=2 node_types=0xe1d3d,0xe1d3d vid_cap_nr=10,20 vid_out_nr=11,21 meta_cap_nr=12,22 meta_out_nr=13,33
sudo modprobe vivid n_devs=1 node_types=0xe1d3d vid_cap_nr=190 vid_out_nr=191 meta_cap_nr=192 meta_out_nr=193
lsmod
ls -lsa /dev/video*
- name: Set up Python ${{ matrix.python-version }}
id: setuppy
uses: actions/setup-python@v5
Expand Down
42 changes: 34 additions & 8 deletions linuxpy/video/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -1000,8 +1000,25 @@ def create_artificial_control_class(class_id):


class Controls(dict):
@classmethod
def from_device(cls, device):
def __init__(self, device: Device):
super().__init__()
self.__dict__["_device"] = device
self.__dict__["_initialized"] = False

def _init_if_needed(self):
if not self._initialized:
self._load()
self.__dict__["_initialized"] = True

def __getitem__(self, name):
self._init_if_needed()
return super().__getitem__(name)

def __len__(self):
self._init_if_needed()
return super().__len__()

def _load(self):
ctrl_type_map = {
ControlType.BOOLEAN: BooleanControl,
ControlType.INTEGER: IntegerControl,
Expand All @@ -1013,9 +1030,8 @@ def from_device(cls, device):
ControlType.U32: U32Control,
ControlType.BUTTON: ButtonControl,
}
ctrl_dict = {}
classes = {}
for ctrl in device.info.controls:
for ctrl in self._device.info.controls:
ctrl_type = ControlType(ctrl.type)
ctrl_class_id = V4L2_CTRL_ID2CLASS(ctrl.id)
if ctrl_type == ControlType.CTRL_CLASS:
Expand All @@ -1030,9 +1046,12 @@ def from_device(cls, device):
ctrl_class = CompoundControl
else:
ctrl_class = ctrl_type_map.get(ctrl_type, GenericControl)
ctrl_dict[ctrl.id] = ctrl_class(device, ctrl, klass)
self[ctrl.id] = ctrl_class(self._device, ctrl, klass)

return cls(ctrl_dict)
@classmethod
def from_device(cls, device):
"""Deprecated: backward compatible. Please use Controls(device) constructor directly"""
return cls(device)

def __getattr__(self, key):
with contextlib.suppress(KeyError):
Expand All @@ -1059,10 +1078,16 @@ def used_classes(self):
return list(class_map.values())

def with_class(self, control_class):
if isinstance(control_class, str):
control_class = ControlClass[control_class.upper()]
elif isinstance(control_class, int):
control_class = ControlClass(control_class)
elif not isinstance(control_class, ControlClass):
control_class = ControlClass(control_class.id - 1)
for v in self.values():
if not isinstance(v, BaseControl):
continue
if v.control_class.id == control_class.id:
if v.control_class.id - 1 == control_class:
yield v

def set_to_default(self):
Expand Down Expand Up @@ -1104,7 +1129,8 @@ def __repr__(self):
repr += f" {addrepr}"

if self.flags:
repr += " flags=" + ",".join(flag.name.lower() for flag in self.flags)
flags = [flag.name.lower() for flag in ControlFlag if ((self._info.flags & flag) == flag)]
repr += " flags=" + ",".join(flags)

return f"<{type(self).__name__} {repr}>"

Expand Down
42 changes: 33 additions & 9 deletions tests/test_video.py
Original file line number Diff line number Diff line change
Expand Up @@ -626,12 +626,6 @@ def _(sink=output_type, source=input_type):
assert frame.memory == Memory.MMAP if source is None else source
assert frame.pixel_format == pixel_format


for output_type in (None, Capability.STREAMING, Capability.READWRITE):
oname = "auto" if output_type is None else output_type.name
for input_type in (None, Capability.STREAMING, Capability.READWRITE):
iname = "auto" if input_type is None else input_type.name

@skip(when=not is_v4l2looback_prepared(), reason="v4l2loopback is not prepared")
@skip(when=not is_v4l2loopback_installed(), reason="v4l2loopback is not installed")
@test(f"output({oname}) and async capture({iname}) with v4l2loopback")
Expand Down Expand Up @@ -682,21 +676,51 @@ def _():
current_value = controls.keep_format.value
assert current_value in {True, False}
try:
controls.keep_format.value = not current_value
controls.keep_format.value = current_value + 5
assert controls.keep_format.value == (not current_value)
finally:
controls.keep_format.value = current_value

with raises(AttributeError):
_ = controls.unknown_field

assert ControlClass.USER in controls.used_classes()
assert ControlClass.USER in {ctrl.id - 1 for ctrl in controls.used_classes()}
assert controls.keep_format in controls.with_class("user")
assert controls.keep_format in controls.with_class(ControlClass.USER)

assert "<BooleanControl keep_format" in repr(controls.keep_format)
with raises(KeyError):
_ = list(controls.with_class("unknown class"))

with raises(ValueError):
_ = list(controls.with_class(55))


@skip(when=not is_vivid_prepared(), reason="vivid is not prepared")
@skip(when=not is_vivid_installed(), reason="vivid is not installed")
@test("controls with vivid")
def _():
with Device(VIVID_TEST_DEVICES[0]) as device:
controls = device.controls
assert controls.brightness is controls["brightness"]
current_value = controls.brightness.value
assert 0 <= current_value < 250
try:
controls.brightness.value = not current_value
assert controls.brightness.value == (not current_value)
finally:
controls.brightness.value = current_value

with raises(AttributeError):
_ = controls.unknown_field

assert ControlClass.USER in {ctrl.id - 1 for ctrl in controls.used_classes()}
assert controls.brightness in controls.with_class("user")
assert controls.brightness in controls.with_class(ControlClass.USER)

assert "<IntegerControl brightness" in repr(controls.brightness)
with raises(KeyError):
_ = list(controls.with_class("unknown class"))

with raises(TypeError):
with raises(ValueError):
_ = list(controls.with_class(55))

0 comments on commit 875d27a

Please sign in to comment.