Skip to content

Commit

Permalink
Merge pull request #8 from Z-Ray-Entertainment/v4l2py
Browse files Browse the repository at this point in the history
V4l2py
  • Loading branch information
VortexAcherontic authored Jul 10, 2024
2 parents e032b6b + 3ffdad1 commit 55ff0df
Show file tree
Hide file tree
Showing 4 changed files with 74 additions and 28 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ Simple wrapper UI for OpenSeeFace's facetracker.
- python3-devel
- python3-pip
- gobject-introspection-devel
- v4l2-utils

### Setup

Expand Down
1 change: 1 addition & 0 deletions de.z_ray.Facetracker.metainfo.xml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
<release version="24.7a8" date="2024-10-08">
<description>
<ul>
<li>Video format selection now build using the actual device capabilities using v4l2-ctl</li>
<li>Overhauled the camera not found view and allow for reloading the interface</li>
<li>Replaced the start / stop face tracker button with an icon only variant</li>
</ul>
Expand Down
7 changes: 4 additions & 3 deletions facetracker/main_frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@ def _build_cam_found(self):

self.tracking_settings_row = Adw.ExpanderRow()
self.tracking_settings_row.set_title(_("Tracking Settings"))
self._build_video_modes()
self.video_modes_row = Adw.ComboRow()
self._build_video_modes(None, None)
self.tracking_settings_row.add_row(self.video_modes_row)
self._build_tracking_mode_selection()
self.tracking_settings_row.add_row(self.tracking_mode_row)
Expand Down Expand Up @@ -146,9 +147,9 @@ def _build_webcam_cb(self):
name = index + ": " + webcam.device_name
cam_string_list.append(name)
self.cam_combo_row.set_model(cam_string_list)
self.cam_combo_row.connect("notify::selected-item", self._build_video_modes)

def _build_video_modes(self):
self.video_modes_row = Adw.ComboRow()
def _build_video_modes(self, widget, _a):
self.video_modes_row.set_title(_("Video Mode:"))
self.video_modes_row.set_subtitle(_("Video mode to be used for face tracking"))
mode_string_list = Gtk.StringList()
Expand Down
93 changes: 68 additions & 25 deletions facetracker/webcam_info.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import math
import os
import subprocess

Expand Down Expand Up @@ -33,23 +34,28 @@ def print_info(self):
def get_webcams() -> [WebcamInfo]:
"""
Some idiot patched the Linux Kernel to list every webcam twice in /sys/class/video4linux/video* (and /dev/) because
of some random metadata. However the file "index" is unusable this way as it is not the real index. Because it
looks like this on a multi webcam system:
of some random metadata. However, the file "index" is unusable this way as it is not the real index anymore:
Cam found | Index: 1 Name: C922 Pro Stream Webcam
Cam found | Index: 1 Name: USB3. 0 capture: USB3. 0 captur
Cam found | Index: 0 Name: C922 Pro Stream Webcam
Cam found | Index: 0 Name: USB3. 0 capture: USB3. 0 captur
This is garbage as not both devices can be index 0 and 1 at the same time while cam with index 1 is actually index 2
... ... ... ... Well we'll just use the number of the directory /sys/class/video4linux/video* here.
Both device can't be index 0 and 1 simultaneously.
So we need to merge these devices somehow to not list every device twice.
This was supposed to be a simple application until now ...
To get the "real" device index of each webcam we first gather all devices found.
Then look for devices sharing a similar realpath and then take the device with the lowest index of each path as
the actual device. THis is based on the assumption that a metadate device node can't be created before the actual
device was created to gather the metadata from.
Possible solution: readlink /sys/class/video4linux/video* and then compare which do point to the same device.
Edit: readlink returns non zero status which causes readlink to "fail" while outputting the correct thing.
Realpath so instead it is ...
Example:
/sys/devices/pci0000:00/0000:00:02.1/0000:03:00.0/0000:04:0c.0/0000:0e:00.0/usb1/1-3/1-3:1.0/video4linux/video3 <-- fake device
/sys/devices/pci0000:00/0000:00:02.1/0000:03:00.0/0000:04:0c.0/0000:0e:00.0/usb1/1-3/1-3:1.0/video4linux/video2 <-- real device
/sys/devices/pci0000:00/0000:00:08.1/0000:10:00.3/usb4/4-1/4-1:1.0/video4linux/video1 <-- fake device
/sys/devices/pci0000:00/0000:00:08.1/0000:10:00.3/usb4/4-1/4-1:1.0/video4linux/video0 <-- real device
To get the real path of the device we use realpath on the /sys/class/video4linux/video* symbolic link and split on
the keyword video4linux (see above).
"""
video_devices_path = "/sys/class/video4linux/"
webcams = []
Expand All @@ -62,14 +68,6 @@ def get_webcams() -> [WebcamInfo]:
device_name_result = subprocess.run(["cat", video_devices_path + video_dir + "/name"],
stdout=subprocess.PIPE)
device_name = device_name_result.stdout.decode("utf-8").rstrip()

""""
Example:
/sys/devices/pci0000:00/0000:00:02.1/0000:03:00.0/0000:04:0c.0/0000:0e:00.0/usb1/1-3/1-3:1.0/video4linux/video3 <-- fake device
/sys/devices/pci0000:00/0000:00:08.1/0000:10:00.3/usb4/4-1/4-1:1.0/video4linux/video1 <-- fake device
/sys/devices/pci0000:00/0000:00:02.1/0000:03:00.0/0000:04:0c.0/0000:0e:00.0/usb1/1-3/1-3:1.0/video4linux/video2 <-- real device
/sys/devices/pci0000:00/0000:00:08.1/0000:10:00.3/usb4/4-1/4-1:1.0/video4linux/video0 <-- real device
"""
device_path_result = subprocess.run(["realpath", video_devices_path + video_dir], stdout=subprocess.PIPE)
device_path = device_path_result.stdout.decode("utf-8").rstrip().split("video4linux")[0]

Expand All @@ -90,13 +88,58 @@ def get_webcams() -> [WebcamInfo]:
webcams = []
for webcam_info in found_devices:
webcam = found_devices[webcam_info]
videomode_default = VideoMode(width=640, height=360, fps=24) # OSF default
videomode_hd = VideoMode(width=1280, height=720, fps=25) # Probably works with most cams
videomode_hd_60 = VideoMode(width=1280, height=720, fps=60) # Probably works with most cams
videomode_fullhd = VideoMode(width=1920, height=1080, fps=30) # Probably works with most cams
webcam.add_video_mode(videomode_default)
webcam.add_video_mode(videomode_hd)
webcam.add_video_mode(videomode_hd_60)
webcam.add_video_mode(videomode_fullhd)
for mode in _get_video_modes(webcam.device_index):
webcam.add_video_mode(mode)
webcams.append(webcam)
return webcams


def _get_video_modes(device_index: int) -> [VideoMode]:
""""
Until I found a sophisticated way of getting the actually supported resolutions and frame rates for each
webcam these default must suffice.
Also it is important that you can not tell opencv (what OSF uses for it's webcam access) which video format
to use and it will always default to RAW while MJPEG would allow for a wider range of resolutions and
frame rates.
- linuxpy does not offer a complete list of supported device capabilities
- v4l2.py is disconnected and have actually emerged into linuxpy
- Parsing the output of "v4l2-ctl -d /dev/video* --list-formats-ext" seems a bit too daunting atm, but it is the
best option I know of as of now.
- Using "ffmpeg -hide_banner -f v4l2 -list_formats all -i /dev/video*" lacks the frame rate info but has the
best format
"""
videomode_default = VideoMode(width=640, height=360, fps=24) # OSF default
# videomode_hd = VideoMode(width=1280, height=720, fps=25) # Probably works with most cams
# videomode_hd_60 = VideoMode(width=1280, height=720, fps=60) # Probably works with most cams
# videomode_fullhd = VideoMode(width=1920, height=1080, fps=30) # Probably works with most cams
video_modes = [videomode_default]

command = ["v4l2-ctl", "-d", "/dev/video" + str(device_index), "--list-formats-ext"]
v4l2_result = subprocess.run(command, stdout=subprocess.PIPE)
v4l2_formats = v4l2_result.stdout.decode("utf-8").rstrip().split("\n")

collect_new_video_mode = False
current_resolution = "0x0"

for field in v4l2_formats:
line = field.replace("\t", "").replace(":", "").replace("(", "").replace(")", "")
cells = line.split(" ")
match cells[0]:
case "[0]":
if cells[2] == "YUYV": # RAW video mode used by opencv / OSF
collect_new_video_mode = True
case "Size":
if collect_new_video_mode:
current_resolution = cells[2]
case "Interval":
if collect_new_video_mode:
current_frame_rate = cells[3]
res = current_resolution.split("x")
fps = int(math.ceil(float(current_frame_rate)))
new_video_mode = VideoMode(int(res[0]), int(res[1]), fps)
video_modes.append(new_video_mode)
case "[1]":
collect_new_video_mode = False

return video_modes

0 comments on commit 55ff0df

Please sign in to comment.