diff --git a/docs/user_guide/video.md b/docs/user_guide/video.md
index 85e1b84..e474669 100644
--- a/docs/user_guide/video.md
+++ b/docs/user_guide/video.md
@@ -2,8 +2,6 @@
Human friendly interface to the Video for Linux 2 (V4L2) subsystem.
-
-
Without further ado:
@@ -89,6 +87,57 @@ finally:
camera.close()
```
+## Finding devices
+
+Get a random video device:
+
+```python
+>>> from linuxpy.video.device import find
+
+>>> dev = find()
+
+```
+
+All video devices:
+
+```python
+>>>
+>>> devs = list(find(find_all=True))
+[,
+ ,
+ ,
+ ,
+ ]
+```
+
+List of all capture devices:
+
+```python
+>>> from linuxpy.video.device import iter_video_capture_devices
+>>> list(iter_video_capture_devices())
+[,
+ ,
+ ]
+```
+
+Custom filter: find a uvc device:
+
+```python
+>>> find(custom_match=lambda d: "uvc" in d.info.driver)
+
+```
+
+Custom filter: find all uvc devices:
+
+```python
+>>> list(find(find_all=True, custom_match=lambda d: "uvc" in d.info.driver))
+[,
+ ,
+ ,
+ ]
+```
+
+
## Capture
Simple capture without any configuration is possible using the Device object
@@ -113,27 +162,81 @@ from linuxpy.video.device import Device, VideoCapture
with Device.from_id(0) as camera:
capture = VideoCapture(camera)
capture.set_format(640, 480, "MJPG")
+ capture.set_fps(10)
with capture:
for frame in capture:
...
```
-Note that `VideoCapture` configuration must be done **before** the capture is started
-(ie, the `with capture:` statement.)
+which is roughly equivalent to:
+
+```python
+with Device.from_id(0) as camera:
+ capture = VideoCapture(camera)
+ capture.set_format(640, 480, "MJPG")
+ capture.set_fps(10)
+ capture.arm()
+ try:
+ capture.start()
+ try:
+ for frame in capture:
+ print(frame)
+ finally:
+ capture.stop()
+ finally:
+ capture.disarm()
+```
+
+Note that `VideoCapture` configuration must be done **before** the camera is armed
+(ie, before them arm() call or entering the `with capture:` statement.)
By default, VideoCapture will use memory map if the device has STREAMING
capability and falls back to standard read if not. It is also possible to
force a specific reader:
```python
-from linuxpy.video.device import Capability, Device, VideoCapture
+from linuxpy.video.device import Capability, Device, VideoCapture, ReadSource
with Device.from_id(0) as cam:
- with VideoCapture(cam, source=Capability.READWRITE):
+ with VideoCapture(cam, buffer_type=ReadSource):
for frame in capture:
...
```
+The previous example will grab frame data using the linux read system call.
+
+### Frame
+
+The frame object contains metadata concerning the current (width, height, pixel format,
+size, timestamp, frame_nb) as well as the frame data.
+Unless the VideoCapture uses a custom buffer type for the acquisition, the `frame.data`
+represents the immutable data for the specific frame and it is safe to use without any
+additional copies.
+
+```python
+>>> # Acquire one frame
+>>> with Device.from_id(0) as cam:
+ stream = iter(cam)
+ frame = next(frame)
+
+>>> frame
+
+>>> frame.width, frame.height, frame.pixel_format
+(640, 480, )
+
+>>> frame.format
+Format(width=640, height=480, pixel_format=, size=614989, bytes_per_line=0)
+
+>>> len(frame.data)
+61424
+
+>>> frame.data is bytes(frame)
+True
+
+>>> frame.array
+array([255, 216, 255, ..., 0, 0, 0], shape=(61424,), dtype=uint8)
+```
+
## Information
Getting information about the device:
@@ -146,7 +249,7 @@ Getting information about the device:
>>> cam.info.card
'Integrated_Webcam_HD: Integrate'
->>> cam.info.capabilities
+>>> cam.info.device_capabilities
>>> cam.info.formats
@@ -157,32 +260,50 @@ Getting information about the device:
>>> cam.get_format(BufferType.VIDEO_CAPTURE)
Format(width=640, height=480, pixelformat=}
+```
+
+## Controls
+
+Device controls can be accessed on the `controls` member of the `Device` object.
+This object works like a dict. The keys can be control name or control id. The value is
+a control object containing the appropiate fields dependent on the type of control
+
+Show list of available controls:
+```python
>>> for ctrl in cam.controls.values(): print(ctrl)
-
-
-
-
-
-
-
-
+...
+```
+
+Access a control with python dict syntax by control name:
+```python
>>> cam.controls["saturation"]
+```
->>> cam.controls["saturation"].id
+Retrieve control information:
+
+```python
+>>> saturation = cam.controls["saturation"]
+>>> saturation.id
9963778
+
>>> cam.controls[9963778]
->>> cam.controls.brightness
-
+>>> saturation.flags
+
+```
+
+Access controls using object attribute syntax:
+
+```python
>>> cam.controls.brightness.value = 64
>>> cam.controls.brightness
diff --git a/examples/video/qt/grid.py b/examples/video/qt/grid.py
index 5ae2ad8..6f63e17 100644
--- a/examples/video/qt/grid.py
+++ b/examples/video/qt/grid.py
@@ -3,7 +3,7 @@
from qtpy import QtWidgets
-from linuxpy.video.device import Device
+from linuxpy.video.device import iter_video_capture_devices
from linuxpy.video.qt import QCamera, QVideoWidget
try:
@@ -41,27 +41,30 @@ def close():
layout = QtWidgets.QGridLayout(grid)
grid.setLayout(layout)
- COLUMNS, ROWS = 6, 3
- qcameras = set()
- for row in range(ROWS):
- for column in range(COLUMNS):
- dev_id = column + row * COLUMNS
- if dev_id > 15:
- break
- dev_id += 10
- device = Device.from_id(dev_id)
- device.open()
- qcamera = QCamera(device)
- widget = QVideoWidget(qcamera)
- layout.addWidget(widget, row, column)
- qcameras.add(qcamera)
+ devices = sorted(iter_video_capture_devices(), key=lambda d: d.index)
+ n = len(devices)
+ if n < 5:
+ cols = 2
+ elif n < 7:
+ cols = 3
+ elif n < 17:
+ cols = 4
+ else:
+ cols = 5
- d0 = Device.from_id(0)
- d0.open()
- qcamera = QCamera(d0)
- qcameras.add(qcamera)
- widget = QVideoWidget(qcamera)
- layout.addWidget(widget)
+ row, col = 0, 0
+ qcameras = set()
+ for device in devices:
+ device.open()
+ qcamera = QCamera(device)
+ widget = QVideoWidget(qcamera)
+ layout.addWidget(widget, row, col)
+ qcameras.add(qcamera)
+ if col < cols - 1:
+ col += 1
+ else:
+ col = 0
+ row += 1
window.show()
app.exec()
diff --git a/examples/video/qt/pyqtgraph1.py b/examples/video/qt/pyqtgraph1.py
index 3993329..d69cf01 100644
--- a/examples/video/qt/pyqtgraph1.py
+++ b/examples/video/qt/pyqtgraph1.py
@@ -1,12 +1,11 @@
import contextlib
-import functools
import logging
-from pyqtgraph import GraphicsLayout, GraphicsView, ImageItem, setConfigOption
+from pyqtgraph import GraphicsLayout, GraphicsView, setConfigOption
from qtpy import QtWidgets
-from linuxpy.video.device import BufferType, Device, PixelFormat
-from linuxpy.video.qt import QCamera, QVideoControls
+from linuxpy.video.device import BufferType, PixelFormat, iter_video_capture_devices
+from linuxpy.video.qt import QCamera, QVideoControls, QVideoItem, QVideoStream
def main():
@@ -32,7 +31,8 @@ def update_image(image_item, frame):
logging.basicConfig(level=args.log_level.upper(), format=fmt)
app = QtWidgets.QApplication([])
with contextlib.ExitStack() as stack:
- devices = [stack.enter_context(Device.from_id(i)) for i in range(10, 24)]
+ idevices = iter_video_capture_devices()
+ devices = sorted((stack.enter_context(dev) for dev in idevices), key=lambda dev: dev.index)
for device in devices:
device.set_format(BufferType.VIDEO_CAPTURE, 640, 480, PixelFormat.RGB24)
device.set_fps(BufferType.VIDEO_CAPTURE, 15)
@@ -53,21 +53,24 @@ def update_image(image_item, frame):
video_layout.addWidget(view)
n = len(devices)
- if n < 3:
- cols = 1
- elif n < 9:
+ if n < 5:
cols = 2
- elif n < 16:
+ elif n < 7:
cols = 3
- else:
+ elif n < 17:
cols = 4
+ else:
+ cols = 5
for i, camera in enumerate(cameras, start=1):
controls = QVideoControls(camera)
controls_layout.addWidget(controls)
view_box = view_layout.addViewBox(name=f"Camera {camera.device.index}", lockAspect=True, invertY=True)
- image_item = ImageItem()
+ # image_item = ImageItem()
+ # camera.frameChanged.connect(functools.partial(update_image, image_item))
+ image_item = QVideoItem()
+ image_item.stream = QVideoStream(camera)
+ image_item.stream.imageChanged.connect(image_item.on_image_changed)
view_box.addItem(image_item)
- camera.frameChanged.connect(functools.partial(update_image, image_item))
if not i % cols:
view_layout.nextRow()
diff --git a/linuxpy/video/device.py b/linuxpy/video/device.py
index 79a39f0..da09b70 100644
--- a/linuxpy/video/device.py
+++ b/linuxpy/video/device.py
@@ -106,9 +106,9 @@ def human_pixel_format(ifmt):
FrameType = collections.namedtuple("FrameType", "type pixel_format width height min_fps max_fps step_fps")
-Input = collections.namedtuple("InputType", "index name type audioset tuner std status capabilities")
+Input = collections.namedtuple("Input", "index name type audioset tuner std status capabilities")
-Output = collections.namedtuple("OutputType", "index name type audioset modulator std capabilities")
+Output = collections.namedtuple("Output", "index name type audioset modulator std capabilities")
Standard = collections.namedtuple("Standard", "index id name frameperiod framelines")
@@ -438,9 +438,10 @@ def free_buffers(fd, buffer_type: BufferType, memory: Memory) -> raw.v4l2_reques
return req
-def export_buffer(fd, buffer_type: BufferType, index: int) -> int:
+def export_buffer(fd, buffer_type: BufferType, index: int) -> raw.v4l2_exportbuffer:
req = raw.v4l2_exportbuffer(type=buffer_type, index=index)
- return ioctl(fd, IOC.EXPBUF, req).fd
+ ioctl(fd, IOC.EXPBUF, req)
+ return req
def create_buffers(fd, format: raw.v4l2_format, memory: Memory, count: int) -> raw.v4l2_create_buffers:
@@ -459,10 +460,13 @@ def set_raw_format(fd, fmt: raw.v4l2_format):
return ioctl(fd, IOC.S_FMT, fmt)
-def set_format(fd, buffer_type: BufferType, width: int, height: int, pixel_format: str = "MJPG"):
+def set_format(fd, buffer_type: BufferType, width: int, height: int, pixel_format: str | int | PixelFormat = "MJPG"):
fmt = raw.v4l2_format()
if isinstance(pixel_format, str):
- pixel_format = raw.v4l2_fourcc(*pixel_format)
+ try:
+ pixel_format = PixelFormat[pixel_format]
+ except KeyError:
+ pixel_format = raw.v4l2_fourcc(*pixel_format)
fmt.type = buffer_type
fmt.fmt.pix.pixelformat = pixel_format
fmt.fmt.pix.field = Field.ANY
@@ -501,12 +505,16 @@ def get_format(fd, buffer_type) -> Union[Format, MetaFmt]:
def try_raw_format(fd, fmt: raw.v4l2_format):
ioctl(fd, IOC.TRY_FMT, fmt)
+ return fmt
-def try_format(fd, buffer_type: BufferType, width: int, height: int, pixel_format: str = "MJPG"):
+def try_format(fd, buffer_type: BufferType, width: int, height: int, pixel_format: str | int | PixelFormat = "MJPG"):
fmt = raw.v4l2_format()
if isinstance(pixel_format, str):
- pixel_format = raw.v4l2_fourcc(*pixel_format)
+ try:
+ pixel_format = PixelFormat[pixel_format]
+ except KeyError:
+ pixel_format = raw.v4l2_fourcc(*pixel_format)
fmt.type = buffer_type
fmt.fmt.pix.pixelformat = pixel_format
fmt.fmt.pix.field = Field.ANY
@@ -1487,24 +1495,32 @@ def __repr__(self):
@property
def raw_capabilities(self) -> raw.v4l2_capability:
+ """
+ Capabilities as read from the device.
+ It caches the capabilities so it only reads from the device once
+ """
if self._raw_capabilities_cache is None:
self._raw_capabilities_cache = read_capabilities(self.device)
return self._raw_capabilities_cache
@property
def driver(self) -> str:
+ """The device driver name"""
return self.raw_capabilities.driver.decode()
@property
def card(self) -> str:
+ """The device card name"""
return self.raw_capabilities.card.decode()
@property
def bus_info(self) -> str:
+ """The device bus info"""
return self.raw_capabilities.bus_info.decode()
@property
def version_tuple(self) -> tuple:
+ """The driver version tuple in format (a, b, c)"""
caps = self.raw_capabilities
return (
(caps.version & 0xFF0000) >> 16,
@@ -1514,18 +1530,22 @@ def version_tuple(self) -> tuple:
@property
def version(self) -> str:
+ """The driver version string in format a.b.c"""
return ".".join(map(str, self.version_tuple))
@property
def capabilities(self) -> Capability:
+ """The capabilities"""
return Capability(self.raw_capabilities.capabilities)
@property
def device_capabilities(self) -> Capability:
+ """The device capabilities"""
return Capability(self.raw_capabilities.device_caps)
@property
def buffers(self):
+ """Available buffer types according to the device capabilities"""
dev_caps = self.device_capabilities
return [typ for typ in BufferType if Capability[typ.name] in dev_caps]
@@ -1534,6 +1554,7 @@ def get_crop_capabilities(self, buffer_type: BufferType) -> CropCapability:
@property
def crop_capabilities(self) -> dict[BufferType, CropCapability]:
+ """Crop capabilities grouped by buffer type for the current active input"""
buffer_types = CROP_BUFFER_TYPES & set(self.buffers)
result = {}
for buffer_type in buffer_types:
@@ -1545,6 +1566,7 @@ def crop_capabilities(self) -> dict[BufferType, CropCapability]:
@property
def formats(self):
+ """List of supported formats for the current active input"""
img_fmt_buffer_types = IMAGE_FORMAT_BUFFER_TYPES & set(self.buffers)
return [
image_format
@@ -1553,35 +1575,43 @@ def formats(self):
]
def buffer_formats(self, buffer_type) -> list[ImageFormat]:
+ """List of supported formats for the given buffer type for the current active input"""
return list(iter_read_formats(self.device, buffer_type))
def format_frame_sizes(self, pixel_format) -> list[FrameSize]:
+ """List of frame sizes for the given pixel format for the current active input"""
return list(iter_read_frame_sizes(self.device, pixel_format))
def frame_sizes(self):
+ """List of frame sizes for the current active input"""
results = []
for fmt in self.formats:
results.extend(self.format_frame_sizes(fmt.pixel_format))
return results
def fps_intervals(self, pixel_format, width, height) -> list[FrameType]:
+ """List of available FPS for the given pixel format, width and height for the current active input"""
return list(iter_read_frame_intervals(self.device, pixel_format, width, height))
@property
- def frame_types(self):
+ def frame_types(self) -> list[FrameType]:
+ """List of all available discrete frame types for the current active input"""
pixel_formats = {fmt.pixel_format for fmt in self.formats}
return list(iter_read_pixel_formats_frame_intervals(self.device, pixel_formats))
@property
def inputs(self) -> list[Input]:
+ """List of available inputs"""
return list(iter_read_inputs(self.device))
@property
- def outputs(self):
+ def outputs(self) -> list[Output]:
+ """List of available outputs"""
return list(iter_read_outputs(self.device))
@property
def controls(self):
+ """List of available controls"""
return list(iter_read_controls(self.device))
@property