feat: point the fleet at wss LiveKit room uwh-telhai
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.
This commit is contained in:
+158
@@ -0,0 +1,158 @@
|
||||
"""Shared C920 mic capture: small gst-launch pool, PCM hops in mmap rings."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import select
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
from audio_gst import GST_GROUP_SIZE, grouped_audio, shared_audio_cmd
|
||||
from hop_shm import HopWriter, hop_bytes
|
||||
|
||||
log = logging.getLogger("cameras.audio_daemon")
|
||||
READY_NAME = "ready"
|
||||
|
||||
|
||||
def _open_pipes(n: int) -> tuple[list[int], list[int]]:
|
||||
reads: list[int] = []
|
||||
writes: list[int] = []
|
||||
for _ in range(n):
|
||||
r, w = os.pipe()
|
||||
try:
|
||||
import fcntl
|
||||
fcntl.fcntl(r, fcntl.F_SETPIPE_SZ, 65536)
|
||||
fcntl.fcntl(w, fcntl.F_SETPIPE_SZ, 65536)
|
||||
except OSError:
|
||||
pass
|
||||
os.set_inheritable(w, True)
|
||||
os.set_inheritable(r, False)
|
||||
reads.append(r)
|
||||
writes.append(w)
|
||||
return reads, writes
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
ap = argparse.ArgumentParser(description="Shared ALSA capture daemon")
|
||||
ap.add_argument("--cameras-json", required=True)
|
||||
ap.add_argument("--rate", type=int, default=16000)
|
||||
ap.add_argument("--hop-s", type=float, default=0.1)
|
||||
ap.add_argument("--shm-dir", default="/run/livekit-cameras/pcm")
|
||||
ap.add_argument("--group-size", type=int, default=GST_GROUP_SIZE)
|
||||
args = ap.parse_args(argv)
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s [audio-daemon] %(levelname)s %(message)s")
|
||||
|
||||
cams = json.loads(args.cameras_json)
|
||||
pairs = [(c["identity"], int(c["audio_card"]))
|
||||
for c in cams if c.get("audio_card") is not None]
|
||||
if not pairs:
|
||||
log.error("no audio cards")
|
||||
return 2
|
||||
|
||||
need = hop_bytes(args.rate, 2, args.hop_s)
|
||||
os.makedirs(args.shm_dir, exist_ok=True)
|
||||
writers = [
|
||||
HopWriter(os.path.join(args.shm_dir, f"{ident}.pcm"), need)
|
||||
for ident, _card in pairs
|
||||
]
|
||||
reads, writes = _open_pipes(len(pairs))
|
||||
env = dict(os.environ)
|
||||
|
||||
groups = grouped_audio(pairs, writes, args.group_size)
|
||||
log.info("starting %d gst-launch for %d mics rate=%d hop=%.3f (group=%d)",
|
||||
len(groups), len(pairs), args.rate, args.hop_s, args.group_size)
|
||||
procs: list[subprocess.Popen] = []
|
||||
for g_cams, g_fds in groups:
|
||||
cmd = shared_audio_cmd(g_cams, args.rate, g_fds)
|
||||
procs.append(subprocess.Popen(
|
||||
cmd,
|
||||
stdin=subprocess.DEVNULL,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=None,
|
||||
pass_fds=tuple(g_fds),
|
||||
env=env,
|
||||
close_fds=True,
|
||||
))
|
||||
for w in writes:
|
||||
os.close(w)
|
||||
for r in reads:
|
||||
os.set_blocking(r, False)
|
||||
|
||||
bufs = [bytearray() for _ in pairs]
|
||||
seqs = [0] * len(pairs)
|
||||
stop = False
|
||||
|
||||
def _stop(_signum=None, _frame=None):
|
||||
nonlocal stop
|
||||
stop = True
|
||||
|
||||
signal.signal(signal.SIGTERM, _stop)
|
||||
signal.signal(signal.SIGINT, _stop)
|
||||
|
||||
ready_path = os.path.join(args.shm_dir, READY_NAME)
|
||||
if os.path.exists(ready_path):
|
||||
os.unlink(ready_path)
|
||||
marked = False
|
||||
last_log = time.monotonic()
|
||||
fd_index = {r: i for i, r in enumerate(reads)}
|
||||
|
||||
try:
|
||||
while not stop:
|
||||
dead = [p for p in procs if p.poll() is not None]
|
||||
if dead:
|
||||
log.error("gst exited rc=%s", [p.returncode for p in dead])
|
||||
break
|
||||
ready, _, _ = select.select(reads, [], [], 0.2)
|
||||
for r in ready:
|
||||
i = fd_index[r]
|
||||
while True:
|
||||
try:
|
||||
chunk = os.read(r, max(need, need - len(bufs[i])))
|
||||
except BlockingIOError:
|
||||
break
|
||||
if not chunk:
|
||||
break
|
||||
bufs[i].extend(chunk)
|
||||
while len(bufs[i]) >= need:
|
||||
hop = bytes(bufs[i][:need])
|
||||
del bufs[i][:need]
|
||||
seqs[i] = writers[i].write(hop)
|
||||
if not marked and any(s > 0 for s in seqs):
|
||||
open(ready_path, "w").write("ok\n")
|
||||
marked = True
|
||||
log.info("ready first hops %s",
|
||||
",".join(f"{pairs[i][0]}={seqs[i]}" for i in range(len(pairs))))
|
||||
now = time.monotonic()
|
||||
if now - last_log >= 5.0:
|
||||
last_log = now
|
||||
log.info("seqs %s",
|
||||
" ".join(f"{pairs[i][0]}:{seqs[i]}" for i in range(len(pairs))))
|
||||
finally:
|
||||
for proc in procs:
|
||||
if proc.poll() is None:
|
||||
proc.terminate()
|
||||
try:
|
||||
proc.wait(timeout=5)
|
||||
except subprocess.TimeoutExpired:
|
||||
proc.kill()
|
||||
for r in reads:
|
||||
try:
|
||||
os.close(r)
|
||||
except OSError:
|
||||
pass
|
||||
for w in writers:
|
||||
w.close()
|
||||
if os.path.exists(ready_path):
|
||||
os.unlink(ready_path)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user