Publish and subscribe over wss://livekit.uni-wh.de:7800 and refuse cleartext ws://. Conference room is uwh-telhai. Includes the uncommitted encoded H.264 publish path, Rally hairpin, and KMS wall overlay.
149 lines
5.7 KiB
Python
149 lines
5.7 KiB
Python
#!/usr/bin/env python3
|
|
"""Generate udev rules that pin each C920 webcam to a fixed slot.
|
|
|
|
Slot N (cam-01..cam-20) is bound to the USB *serial number* of the camera
|
|
that currently occupies it, giving stable:
|
|
- video: /dev/camNN (primary capture node), /dev/camNN.meta (UVC metadata)
|
|
- audio: fixed ALSA card index (hw:INDEX,0)
|
|
|
|
Run: python3 generate_udev_rules.py # prints to stdout
|
|
python3 generate_udev_rules.py --write # also writes /etc/udev/rules.d/99-c920-pin.rules
|
|
"""
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
|
|
from usb_ids import is_fleet_video, is_rally_pid, usb_vid_pid_from_sysfs
|
|
|
|
HEADER = """\
|
|
# Stable slots for 20x Logitech HD Pro Webcam C920 (046d:08e5).
|
|
# Slot = physical camera, identified by USB serial. Survives reboots,
|
|
# re-enumeration, and hub reordering. Generated by generate_udev_rules.py.
|
|
#
|
|
# /dev/camNN -> primary video capture node (UVC stream 0)
|
|
# /dev/camNN.meta -> UVC metadata node (stream 1)
|
|
# hw:N,0 -> mic of cam-NN (N from SOUND_CARD_INDEX below)
|
|
"""
|
|
|
|
|
|
def sh(cmd):
|
|
return subprocess.run(cmd, capture_output=True, text=True).stdout
|
|
|
|
|
|
def _serial_of(sysfs_path):
|
|
"""Walk up from a sysfs path (interface, function, or card device)
|
|
until we reach the USB device that carries the 'serial' attribute."""
|
|
import pathlib
|
|
p = pathlib.Path(sysfs_path)
|
|
for _ in range(12):
|
|
cand = p / "serial"
|
|
if cand.is_file():
|
|
return cand.read_text().strip()
|
|
if str(p).startswith("/sys/devices"):
|
|
p = p.parent
|
|
else:
|
|
return ""
|
|
return ""
|
|
|
|
|
|
def current_state():
|
|
"""Map serial -> (primary_video_node, sound_card_index)."""
|
|
import pathlib
|
|
# sound cards: index -> serial
|
|
cards = {}
|
|
for line in sh(["cat", "/proc/asound/cards"]).splitlines():
|
|
m = re.match(r"\s*(\d+)\s+\[([A-Za-z0-9_]+)\s*\]:\s*(.*)", line)
|
|
if not m:
|
|
continue
|
|
idx = int(m.group(1))
|
|
if "C920" not in m.group(3) and "920" not in m.group(2):
|
|
continue
|
|
dev = f"/sys/class/sound/card{idx}/device"
|
|
real = sh(["readlink", "-f", dev]).strip()
|
|
cards[idx] = _serial_of(real)
|
|
# video: primary node (index 0) per USB serial, via sysfs
|
|
primary_by_serial = {}
|
|
for node in sorted(pathlib.Path("/sys/class/video4linux").glob("video*"),
|
|
key=lambda p: int(p.name.replace("video", ""))):
|
|
idx_attr = (node / "index").read_text().strip()
|
|
if idx_attr != "0":
|
|
continue
|
|
real = sh(["readlink", "-f", str(node / "device")]).strip()
|
|
ids = usb_vid_pid_from_sysfs(real)
|
|
if ids is not None and not is_fleet_video(ids[0], ids[1]):
|
|
continue
|
|
if ids is not None and is_rally_pid(ids[1]):
|
|
continue
|
|
s = _serial_of(real)
|
|
primary_by_serial[s] = f"/dev/{node.name}"
|
|
return primary_by_serial, cards
|
|
|
|
|
|
def main():
|
|
primary_by_serial, cards = current_state()
|
|
# assign slots: cam-01..cam-20 in current video-node order
|
|
ordered = []
|
|
for serial, node in primary_by_serial.items():
|
|
n = int(node.replace("/dev/video", ""))
|
|
ordered.append((n, serial, node))
|
|
ordered.sort()
|
|
if len(ordered) != 20:
|
|
print(f"warning: expected 20 cameras, got {len(ordered)}", file=sys.stderr)
|
|
|
|
lines = [HEADER]
|
|
for i, (vnum, serial, node) in enumerate(ordered, start=1):
|
|
card = next((c for c, s in cards.items() if s == serial), None)
|
|
slot = f"cam{i:02d}"
|
|
lines.append(f"# {slot}: serial {serial} (was {node}, card {card})")
|
|
lines.append(
|
|
f'SUBSYSTEM=="video4linux", KERNEL=="video*", '
|
|
f'ENV{{ID_VENDOR_ID}}=="046d", ENV{{ID_MODEL_ID}}=="08e5", '
|
|
f'ENV{{ID_USB_SERIAL_SHORT}}=="{serial}", '
|
|
f'ATTR{{index}}=="0", SYMLINK+="{slot}"')
|
|
lines.append(
|
|
f'SUBSYSTEM=="video4linux", KERNEL=="video*", '
|
|
f'ENV{{ID_VENDOR_ID}}=="046d", ENV{{ID_MODEL_ID}}=="08e5", '
|
|
f'ENV{{ID_USB_SERIAL_SHORT}}=="{serial}", '
|
|
f'ATTR{{index}}=="1", SYMLINK+="{slot}.meta"')
|
|
if card is not None:
|
|
lines.append(
|
|
f'SUBSYSTEM=="sound", KERNEL=="card*", '
|
|
f'ENV{{ID_VENDOR_ID}}=="046d", ENV{{ID_MODEL_ID}}=="08e5", '
|
|
f'ENV{{ID_USB_SERIAL_SHORT}}=="{serial}", '
|
|
f'ENV{{SOUND_CARD_INDEX}}="{card}"')
|
|
else:
|
|
print(f"warning: no sound card for serial {serial}", file=sys.stderr)
|
|
lines.append("")
|
|
text = "\n".join(lines)
|
|
print(text)
|
|
if "--write" in sys.argv:
|
|
path = "/etc/udev/rules.d/99-c920-pin.rules"
|
|
with open(path, "w") as f:
|
|
f.write(text)
|
|
print(f"# wrote {path}", file=sys.stderr)
|
|
# durable slot map for the app (discovery.py reads this)
|
|
import json
|
|
import os
|
|
slots = {}
|
|
for i, (vnum, serial, node) in enumerate(ordered, start=1):
|
|
card = next((c for c, s in cards.items() if s == serial), None)
|
|
slots[f"cam-{i:02d}"] = {
|
|
"serial": serial,
|
|
"video": f"/dev/cam{i:02d}",
|
|
"card": card,
|
|
}
|
|
base = os.path.dirname(os.path.abspath(__file__))
|
|
slots_path = os.path.join(base, "slots.json")
|
|
with open(slots_path, "w") as f:
|
|
json.dump(slots, f, indent=2)
|
|
print(f"# wrote {slots_path}", file=sys.stderr)
|
|
# summary table to stderr
|
|
for i, (vnum, serial, node) in enumerate(ordered, start=1):
|
|
card = next((c for c, s in cards.items() if s == serial), "?")
|
|
print(f"# cam-{i:02d} serial={serial} video={node} card={card}",
|
|
file=sys.stderr)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|