8000 Hochkant-Bild für die IRL4YOU Cam by IRL4YOU · Pull Request #9 · IRL4YOU/IRL4YOU-BOX · GitHub
[go: up one dir, main page]

Skip to content
8000
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Versionsschema: x.y.z, angezeigt als vx.y.z. x gibt Marco vor (0 = Alpha). y ste
- Bitrate-Grenzen (Netzwerk > Server) gelten sofort, ohne die Sendung neu zu starten; die Einzelbild-Kette nimmt jetzt auch H.265 von Handy-Apps an.
- Ton von einem USB-Mikrofon (zum Beispiel DJI-Empfänger): bei der IRL4YOU Cam und im Bildaufbau („Ton von“), mit Regler für den Ton-Versatz; die Karte „Antwortzeit“ zeigt die Laufzeit der Leitung.
- IRL4YOU Cam: „Ohne Ton“ sendet jetzt Stille, die Sendung bleibt dabei bestehen.
- Bild: Steht das Handy der IRL4YOU Cam hochkant, sitzt es links oder rechts in voller Höhe, die andere Kamera füllt den Rest („Hauptbild hochkant“, als kleines Bild „Hochkant-Bild“).
- Testbild als Kamera-Quelle: bewegtes Wellenmuster mit Schrift, Zeit und Logo, Stereo-Testton (links 440, rechts 660 Hz).
- Leitungstest: „Testen“ je Leitung und „Alle Leitungen testen“ (Herunterladen, Hochladen, Antwortzeit, auch während der Sendung).
- Update-Prüfung alle 2 Stunden mit niedrigster Priorität; „Nach Updates suchen“ wartet auf das Ergebnis, auch während der Sendung.
Expand Down
57 changes: 54 additions & 3 deletions irl4you/pipbox_live.py
if pos is None:
Original file line number Diff line number Diff line change
Expand Up @@ -186,12 +186,20 @@ def small_geometry(c, k):
"bcolor": int(br["color"][1:], 16), "bopacity": int(br["opacity"])}


PORTRAIT_MODES = ("", "links", "mitte", "rechts") # Einstellung „portrait“ je kleinem Bild: 0 = Größe und Ecke wie im Querformat, 1 bis 3 = ganze Höhe an dieser Stelle


def portrait_geometry(c, k, g, dims):
sfx = "" if k == 1 else str(k)
h = g["sh"]
mode = c.get("portrait" + sfx, 0)
mode = mode if isinstance(mode, int) and not isinstance(mode, bool) and 0 <= mode <= 3 else 0
h = CANVAS_H if mode else g["sh"]
w = (h * dims[0] // dims[1]) & ~1
margin = (CANVAS_W // 60) & ~1
x, y = place(c["corner" + sfx], CANVAS_W, CANVAS_H, w, h, margin, c["x" + sfx], c["y" + sfx])
if mode:
x, y = (0, (CANVAS_W - w) // 2, CANVAS_W - w)[mode - 1], 0
else:
x, y = place(c["corner" + sfx], CANVAS_W, CANVAS_H, w, h, margin, c["x" + sfx], c["y" + sfx])
g2 = dict(g)
g2.update(crop=(0, 0, 0, 0), w=w, h=h, x=x & ~1, y=y & ~1)
if g["bw"]:
Expand Down Expand Up @@ -440,6 +448,35 @@ def order_for_swap(lines, old_caps, alpha):
return out + [WAIT_MARK] + later if later else out


def main_portrait_layout(c, dims, main_slot, pip_slot):
"""Hauptbild ist ein hochkant stehendes Handy (Einstellung „portrait_main“: 1 links, 3 rechts): das Handy in ganzer Höhe am Rand, die andere Kamera füllt
den restlichen Platz. Liefert (Rechteck des Handys, Rechteck der anderen Kamera oder None) oder None, wenn nichts zu tun ist."""
mode = c.get("portrait_main", 0)
if mode not in (1, 3) or isinstance(mode, bool) or main_slot is None:
return None
d = dims.get(main_slot)
if not d or not d[0] or not d[1] or d[1] <= d[0]:
return None
w = (CANVAS_H * d[0] // d[1]) & ~1
if w < 16 or w >= CANVAS_W - 16:
return None
me = (0 if mode == 1 else CANVAS_W - w, 0, w, CANVAS_H)
fill = ((w if mode == 1 else 0), 0, CANVAS_W - w, CANVAS_H) if pip_slot is not None else None
return me, fill


def fill_crop(src_w, src_h, tw, th):
"""Zuschnitt (links, rechts, oben, unten) eines Bilds src_w×src_h, damit es das Rechteck tw×th ohne Ränder ausfüllt (mittig, gerade Werte)."""
src_w, src_h = src_w or 1920, src_h or 1080
if src_w * th > src_h * tw: # Quelle ist breiter als das Ziel: links und rechts abschneiden
keep = (src_h * tw // th) & ~1
cut = max(0, (src_w - keep) // 2) & ~1
return cut, cut, 0, 0
keep = (src_w * th // tw) & ~1
cut = max(0, (src_h - keep) // 2) & ~1
return 0, 0, cut, cut


class Layout:

def __init__(self):
Expand All @@ -460,15 +497,29 @@ def commands(self, c, slot_key, roles, dims, audio_ok, hide, audio_pos, mute, de
def add(key, line, z):
want.append((key, line, z))

mp = main_portrait_layout(c, dims, by_pos.get(0), by_pos.get(1))
if mp and mp[1]:
fx, fy, fw, fh = mp[1]
geoms[1] = dict(geoms[1], x=fx, y=fy, w=fw, h=fh, crop=(0, 0, 0, 0), bw=0, radius=0) # die andere Kamera füllt den Rest: ohne Rand und Rundung

for s in range(SLOTS):
nm = f"cam{s}"
pos = roles.get(s)
add(("a", s), f"pad mix sink_{s} alpha 0", -1)
continue
if pos == 0:
if pos == 0 and mp:
x, y, w, h = mp[0]
crop, sw, sh, alpha, z = (0, 0, 0, 0), w, h, 1.0, Z_BIG
elif pos == 0:
x, y, w, h = 0, 0, CANVAS_W, CANVAS_H # volle Fläche: videoscale passt jedes Bild mit Rändern ein (kein Dehnen beim Drehen)
crop, sw, sh, alpha, z = (0, 0, 0, 0), w, h, 1.0, Z_BIG
elif pos == 1 and mp and mp[1]:
x, y, w, h = mp[1]
sw0, sh0 = dims.get(s) or (1920, 1080)
crop = fill_crop(sw0, sh0, w, h)
sw, sh = w, h
alpha, z = (0 if (hide >> (pos - 1)) & 1 else 1.0), 2 * pos
else:
g = geoms[pos]
x, y, w, h = g["x"], g["y"], g["w"], g["h"]
Expand Down
2 changes: 1 addition & 1 deletion irl4you/web/i18n/en.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion irl4you/web/i18n/es.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion irl4you/web/i18n/fr.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion irl4you/web/i18n/it.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion irl4you/web/i18n/ja.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion irl4you/web/i18n/ko.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion irl4you/web/i18n/nl.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion irl4you/web/i18n/pl.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion irl4you/web/i18n/pt.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion irl4you/web/i18n/ru.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion irl4you/web/i18n/th.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion irl4you/web/i18n/tr.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion irl4you/web/i18n/zh.json

Large diffs are not rendered by default.

17 changes: 11 additions & 6 deletions irl4you/web/neu/views/bild.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

35 changes: 31 additions & 4 deletions irl4you/webui/pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ class PipelineStore(PipelineBuild):
DEFAULT = {"type": "single", "main": "", "pip": "", "corner": 3, "size_pct": 25, "size_pct2": 25, "size_pct3": 25, "audio": "main",
"pip2": "", "corner2": 2, "pip3": "", "corner3": 0, "x": 500, "y": 500, "x2": 500, "y2": 500, "x3": 500, "y3": 500, "main_delay_ms": DEFAULT_MAIN_DELAY_MS,
"pip_delay_ms": DEFAULT_PIP_DELAY_MS, "pip2_delay_ms": DEFAULT_PIP_DELAY_MS,
"pip3_delay_ms": DEFAULT_PIP_DELAY_MS, "auto_failover": True, "swap_cams": 0, "always_ready": False, "facecam": "", "mic_offset_ms": 0}
"pip3_delay_ms": DEFAULT_PIP_DELAY_MS, "auto_failover": True, "swap_cams": 0, "always_ready": False, "facecam": "", "mic_offset_ms": 0, "portrait": 0, "portrait2": 0, "portrait3": 0, "portrait_main": 0}
# Bildaufbau ist immer Bild-in-Bild; ohne kleines Bild (eine Kamera) bleibt der Typ "single". Mit kleinem Bild gilt immer "Alle Kameras immer bereit"
# und der Tausch des Hauptbilds über alle Kameras (keine Einstellung mehr in der Oberfläche).

Expand Down Expand Up @@ -231,7 +231,7 @@ def set(self, req, camera_keys):
"pip2": "", "corner2": 2, "pip3": "", "corner3": 0, "x": 500, "y": 500, "x2": 500, "y2": 500, "x3": 500, "y3": 500, "main_delay_ms": DEFAULT_MAIN_DELAY_MS,
"pip_delay_ms": DEFAULT_PIP_DELAY_MS, "pip2_delay_ms": DEFAULT_PIP_DELAY_MS,
"pip3_delay_ms": DEFAULT_PIP_DELAY_MS, "auto_failover": req.get("auto_failover", True) is not False, "swap_cams": 0,
"always_ready": t == "pip"}
"always_ready": t == "pip", "portrait": 0, "portrait2": 0, "portrait3": 0, "portrait_main": 0}
cfg["styles"] = clean_styles(req.get("styles"), self.cfg.get("styles"), strict=True)
try:
moff = int(req.get("mic_offset_ms", 0) or 0)
Expand All @@ -258,6 +258,21 @@ def set(self, req, camera_keys):
if audio not in ("main", "pip", "pip2", "pip3", "mic"):
raise ValueError("Ton: Hauptbild, eines der kleinen Bilder oder das Mikrofon")
cfg.update(pip=pipk, corner=corner, size_pct=pct, size_pct2=pct2, size_pct3=pct3, audio=audio)
try:
pm = int(req.get("portrait_main", 0) or 0)
except (TypeError, ValueError):
raise ValueError("Hauptbild hochkant: 0 (aus), 1 links, 3 rechts")
if pm not in (0, 1, 3):
raise ValueError("Hauptbild hochkant: 0 (aus), 1 links, 3 rechts")
cfg["portrait_main"] = pm
for key in ("portrait", "portrait2", "portrait3"):
try:
val = int(req.get(key, 0) or 0)
except (TypeError, ValueError):
raise ValueError("Hochkant-Bild: Zahl von 0 bis 3")
if not 0 <= val <= 3:
raise ValueError("Hochkant-Bild: 0 (aus), 1 links, 2 Mitte, 3 rechts")
cfg[key] = val
for key in ("x", "y", "x2", "y2", "x3", "y3"):
try:
val = int(req.get(key, 500))
Expand Down Expand Up @@ -363,9 +378,21 @@ def set_view(self, visible=None, audio=None):
self.save()
return self.view_values(c)

def status(self, cams):
def phone_keys(self):
"""Kameras, die ein Android-Handy mit der IRL4YOU Cam sind (nur sie melden ihre Ausrichtung zuverlässig; für sie gibt es „Hochkant-Bild“)."""
try:
with open(os.path.join(os.path.dirname(os.path.abspath(self.path)), "hdmi.json")) as f:
d = json.load(f)
if isinstance(d, dict) and d.get("source") == "irlcam" and isinstance(d.get("key"), str):
return {d["key"]}
except (OSError, ValueError):
pass
return set()

def status(self, cams, phones=None):
keys = [c["key"] for c in cams]
return {"config": dict(self.cfg, styles=clean_styles(self.cfg.get("styles"))), "plugin_style": plugin_style(), "cameras": [{"key": c["key"], "name": c["name"], "state": c.get("state", "unknown"), "w": c.get("w", 0), "h": c.get("h", 0)} for c in cams],
phones = self.phone_keys() if phones is None else phones
return {"config": dict(self.cfg, styles=clean_styles(self.cfg.get("styles"))), "plugin_style": plugin_style(), "cameras": [{"key": c["key"], "name": c["name"], "state": c.get("state", "unknown"), "w": c.get("w", 0), "h": c.get("h", 0), "phone": c["key"] in phones} for c in cams],
"corners": list(pip_corners()), "preview": self.build() if self.cfg.get("main") in keys else "",
"needs_plugin": self.cfg.get("type") == "pip", "plugin_present": os.path.exists("/opt/irl4you/gst/libgstpbpip.so")}

Expand Down
6 changes: 6 additions & 0 deletions irl4you/webui/pipeline_build.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ def num(k, lo, hi):
num(k, SIZE_MIN, SIZE_MAX)
for k in ("x", "y", "x2", "y2", "x3", "y3"):
num(k, 0, 1000)
for k in ("portrait", "portrait2", "portrait3"):
num(k, 0, 3)
out.setdefault("portrait_main", 0)
num("portrait_main", 0, 3)
if out["portrait_main"] == 2:
out["portrait_main"] = 0
for k in ("main_delay_ms", "pip_delay_ms", "pip2_delay_ms", "pip3_delay_ms"):
num(k, 0, 3000)
out.setdefault("mic_offset_ms", 0)
Expand Down
Loading
Loading
0