some changes
This commit is contained in:
189
sender/stream_av.py
Normal file
189
sender/stream_av.py
Normal file
@@ -0,0 +1,189 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
CHT Sender: muxed video + audio over TCP/mpegts
|
||||
Source: Wayland (kmsgrab) + desktop audio + webcam mic
|
||||
Usage: sudo python3 stream_av.py [RECEIVER_IP] [PORT]
|
||||
|
||||
Requires: sudo for kmsgrab, PulseAudio for audio capture
|
||||
Auto-restarts on stall or fatal kmsgrab errors (DRM plane format change).
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import resource
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import threading
|
||||
import time
|
||||
|
||||
RECEIVER_IP = sys.argv[1] if len(sys.argv) > 1 else "mcrndeb"
|
||||
PORT = sys.argv[2] if len(sys.argv) > 2 else "4444"
|
||||
STALL_TIMEOUT = 10
|
||||
|
||||
REAL_UID = os.environ.get("SUDO_UID") or str(os.getuid())
|
||||
os.environ["PULSE_SERVER"] = f"unix:/run/user/{REAL_UID}/pulse/native"
|
||||
|
||||
FATAL_PATTERNS = ["framebuffer format changed", "Error during demuxing"]
|
||||
|
||||
|
||||
def get_monitor_source():
|
||||
try:
|
||||
out = subprocess.check_output(["pactl", "info"], stderr=subprocess.DEVNULL).decode()
|
||||
for line in out.splitlines():
|
||||
if "Default Sink:" in line:
|
||||
return line.split()[-1] + ".monitor"
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def get_webcam_mic():
|
||||
try:
|
||||
out = subprocess.check_output(
|
||||
["pactl", "list", "short", "sources"], stderr=subprocess.DEVNULL
|
||||
).decode()
|
||||
for line in out.splitlines():
|
||||
if "C922" in line:
|
||||
return line.split()[1]
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def build_args(monitor, webcam_mic, progress_file):
|
||||
args = [
|
||||
"ffmpeg",
|
||||
"-init_hw_device", "drm=drm:/dev/dri/card0",
|
||||
"-init_hw_device", "vaapi=va@drm",
|
||||
"-thread_queue_size", "64",
|
||||
"-device", "/dev/dri/card0", "-f", "kmsgrab", "-framerate", "30", "-i", "-",
|
||||
"-thread_queue_size", "1024", "-f", "pulse", "-i", monitor,
|
||||
]
|
||||
if webcam_mic:
|
||||
args += [
|
||||
"-thread_queue_size", "1024", "-f", "pulse", "-i", webcam_mic,
|
||||
"-filter_complex", "[1:a][2:a]amix=inputs=2:duration=longest[aout]",
|
||||
"-map", "0:v", "-map", "[aout]",
|
||||
]
|
||||
args += [
|
||||
"-vf", "hwmap=derive_device=vaapi,scale_vaapi=w=1920:h=1080:format=nv12,fps=30",
|
||||
"-c:v", "h264_vaapi", "-qp", "20", "-g", "30", "-keyint_min", "30", "-bf", "0",
|
||||
"-c:a", "aac", "-b:a", "128k",
|
||||
"-max_muxing_queue_size", "64",
|
||||
"-flush_packets", "1", "-fflags", "nobuffer", "-muxdelay", "0", "-muxpreload", "0",
|
||||
"-f", "mpegts", f"tcp://{RECEIVER_IP}:{PORT}",
|
||||
"-hide_banner", "-progress", progress_file,
|
||||
]
|
||||
return args
|
||||
|
||||
|
||||
def read_progress(progress_file, key):
|
||||
try:
|
||||
with open(progress_file) as f:
|
||||
content = f.read()
|
||||
matches = re.findall(rf"^{key}=(\d+)", content, re.MULTILINE)
|
||||
return int(matches[-1]) if matches else 0
|
||||
except Exception:
|
||||
return 0
|
||||
|
||||
|
||||
def run_once(args, progress_file):
|
||||
"""Run the pipeline, monitor it. Returns True if should restart."""
|
||||
fatal = threading.Event()
|
||||
print(f"[cmd] {' '.join(args)}", flush=True)
|
||||
|
||||
proc = subprocess.Popen(args, stderr=subprocess.PIPE, start_new_session=True)
|
||||
|
||||
def watch_stderr():
|
||||
for raw in proc.stderr:
|
||||
line = raw.decode("utf-8", errors="replace").rstrip()
|
||||
if line:
|
||||
print(f"[ffmpeg] {line}", flush=True)
|
||||
if any(p in line for p in FATAL_PATTERNS):
|
||||
print("Fatal kmsgrab error — restarting immediately", flush=True)
|
||||
fatal.set()
|
||||
|
||||
threading.Thread(target=watch_stderr, daemon=True).start()
|
||||
|
||||
last_bytes = last_frame = 0
|
||||
stall_since = time.monotonic()
|
||||
|
||||
def kill_proc():
|
||||
try:
|
||||
os.killpg(os.getpgid(proc.pid), signal.SIGINT)
|
||||
except Exception:
|
||||
proc.kill()
|
||||
try:
|
||||
proc.wait(timeout=5)
|
||||
except subprocess.TimeoutExpired:
|
||||
proc.kill()
|
||||
|
||||
try:
|
||||
while proc.poll() is None:
|
||||
time.sleep(1)
|
||||
|
||||
if fatal.is_set():
|
||||
kill_proc()
|
||||
break
|
||||
|
||||
cur_bytes = read_progress(progress_file, "total_size")
|
||||
cur_frame = read_progress(progress_file, "frame")
|
||||
|
||||
if cur_bytes > last_bytes or cur_frame > last_frame:
|
||||
last_bytes, last_frame = cur_bytes, cur_frame
|
||||
stall_since = time.monotonic()
|
||||
|
||||
if time.monotonic() - stall_since > STALL_TIMEOUT:
|
||||
print(f"Stalled at frame {last_frame} / {last_bytes}B — restarting", flush=True)
|
||||
kill_proc()
|
||||
break
|
||||
except KeyboardInterrupt:
|
||||
kill_proc()
|
||||
raise
|
||||
|
||||
rc = proc.wait()
|
||||
print(f"ffmpeg exited rc={rc}", flush=True)
|
||||
return True # always restart; only KeyboardInterrupt stops the loop
|
||||
|
||||
|
||||
def main():
|
||||
try:
|
||||
resource.setrlimit(resource.RLIMIT_NOFILE, (65536, 65536))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
monitor = get_monitor_source()
|
||||
webcam_mic = get_webcam_mic()
|
||||
|
||||
print(f"Monitor source : {monitor}")
|
||||
print(f"Webcam mic : {webcam_mic or 'not found'}")
|
||||
print(f"Streaming to : {RECEIVER_IP}:{PORT}")
|
||||
|
||||
if not monitor:
|
||||
sys.exit("ERROR: could not find monitor source")
|
||||
|
||||
with tempfile.NamedTemporaryFile(delete=False, suffix=".progress") as f:
|
||||
progress_file = f.name
|
||||
|
||||
try:
|
||||
while True:
|
||||
print(f"--- Starting sender {time.strftime('%c')} ---", flush=True)
|
||||
open(progress_file, "w").close()
|
||||
|
||||
args = build_args(monitor, webcam_mic, progress_file)
|
||||
run_once(args, progress_file)
|
||||
print("Restarting in 2s...", flush=True)
|
||||
time.sleep(2)
|
||||
except KeyboardInterrupt:
|
||||
print("\nStopped.", flush=True)
|
||||
finally:
|
||||
try:
|
||||
os.unlink(progress_file)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user