"""Bildschirmaufnahme und Versand als RTP-Strom. Die eigentliche Arbeit macht ffmpeg: aufnehmen, in H.264 wandeln, in MPEG-TS verpacken und als RTP verschicken - genau das Paketformat, das der Wi-Fi-Display-Standard erwartet (Nutzlasttyp 33). """ import logging import os import shutil import signal import subprocess from typing import List, Optional from .formats import VideoFormat log = logging.getLogger("wfd.streamer") def screen_size(display: str) -> str: """Auflösung des Bildschirms - für die Aufnahme braucht ffmpeg sie.""" for command, pattern in ( (["xdpyinfo", "-display", display], "dimensions:"), (["xrandr", "--display", display, "--current"], "*"), ): if not shutil.which(command[0]): continue try: output = subprocess.run(command, capture_output=True, text=True, timeout=5).stdout except (subprocess.SubprocessError, OSError): continue for line in output.splitlines(): if pattern not in line: continue for token in line.split(): if "x" in token and token.replace("x", "").isdigit(): return token return "1920x1080" class Streamer: """Hält den laufenden ffmpeg-Prozess.""" def __init__(self, source: str = "auto", display: Optional[str] = None, bitrate_mbit: int = 8): self.source = source self.display = display or os.environ.get("DISPLAY", ":0") self.bitrate_mbit = bitrate_mbit self.process: Optional[subprocess.Popen] = None self.command: List[str] = [] def build_command(self, host: str, port: int, fmt: VideoFormat) -> List[str]: command = ["ffmpeg", "-hide_banner", "-loglevel", "warning", "-nostdin"] mode = self.source if mode == "auto": mode = "x11" if os.environ.get("DISPLAY") else "test" if mode == "x11": command += ["-f", "x11grab", "-framerate", str(fmt.fps), "-video_size", screen_size(self.display), "-draw_mouse", "1", "-i", self.display] elif mode == "kms": # Für Wayland-Sitzungen; benötigt erweiterte Rechte. command += ["-f", "kmsgrab", "-framerate", str(fmt.fps), "-i", "-", "-vf", "hwdownload,format=bgr0"] elif mode == "test": command += ["-f", "lavfi", "-i", f"testsrc=size={fmt.width}x{fmt.height}:rate={fmt.fps}"] else: raise ValueError(f"Unbekannte Bildquelle: {mode}") # Auf die ausgehandelte Größe bringen, Seitenverhältnis erhalten. scale = (f"scale={fmt.width}:{fmt.height}:force_original_aspect_ratio=decrease," f"pad={fmt.width}:{fmt.height}:(ow-iw)/2:(oh-ih)/2,format=yuv420p") rate = self.bitrate_mbit command += [ "-vf", scale, "-c:v", "libx264", "-profile:v", "baseline", # ohne B-Bilder, das versteht jeder Empfänger "-preset", "ultrafast", "-tune", "zerolatency", "-x264-params", f"keyint={fmt.fps * 2}:min-keyint={fmt.fps * 2}:scenecut=0:bframes=0:nal-hrd=cbr", "-b:v", f"{rate}M", "-minrate", f"{rate}M", "-maxrate", f"{rate}M", "-bufsize", f"{max(1, rate // 4)}M", "-r", str(fmt.fps), "-muxdelay", "0", "-muxpreload", "0", "-f", "rtp_mpegts", f"rtp://{host}:{port}?pkt_size=1316", ] return command def start(self, host: str, port: int, fmt: VideoFormat): if self.process: return self.command = self.build_command(host, port, fmt) log.info("Starte Bildstrom: %s", " ".join(self.command)) self.process = subprocess.Popen(self.command) def is_running(self) -> bool: return self.process is not None and self.process.poll() is None def stop(self): if not self.process: return try: self.process.send_signal(signal.SIGINT) self.process.wait(timeout=4) except (subprocess.TimeoutExpired, OSError): try: self.process.kill() except OSError: pass self.process = None