first commit
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user