Files
2026-08-22 20:34:42 +02:00

122 lines
3.5 KiB
Python

"""Bildformate des Wi-Fi-Display-Standards und die Auswahl daraus.
Der Fernseher meldet in einer Bitmaske, welche Auflösungen er beherrscht.
Wir suchen die beste heraus, die beide Seiten können, und melden genau diese
eine zurück.
"""
from dataclasses import dataclass
from typing import List, Optional
@dataclass(frozen=True)
class VideoFormat:
cea_bit: int
width: int
height: int
fps: int
def __str__(self) -> str:
return f"{self.width}x{self.height}@{self.fps}"
# CEA-Tabelle des Standards; die Position entspricht dem Bit in der Maske.
CEA = [
VideoFormat(0, 640, 480, 60),
VideoFormat(1, 720, 480, 60),
VideoFormat(2, 720, 480, 60), # interlaced
VideoFormat(3, 720, 576, 50),
VideoFormat(4, 720, 576, 50), # interlaced
VideoFormat(5, 1280, 720, 30),
VideoFormat(6, 1280, 720, 60),
VideoFormat(7, 1920, 1080, 30),
VideoFormat(8, 1920, 1080, 60),
VideoFormat(9, 1920, 1080, 60), # interlaced
VideoFormat(10, 1280, 720, 25),
VideoFormat(11, 1280, 720, 50),
VideoFormat(12, 1920, 1080, 25),
VideoFormat(13, 1920, 1080, 50),
VideoFormat(14, 1920, 1080, 50), # interlaced
VideoFormat(15, 1280, 720, 24),
VideoFormat(16, 1920, 1080, 24),
]
INTERLACED = {2, 4, 9, 14}
# Wunschreihenfolge: flüssig und scharf, ohne zu übertreiben.
PREFERENCE = [5, 7, 6, 11, 13, 10, 12, 15, 16, 8, 0, 1, 3]
@dataclass(frozen=True)
class SinkCapability:
profile: int
level: int
cea_mask: int
vesa_mask: int
hh_mask: int
def parse_video_formats(value: str) -> List[SinkCapability]:
"""Zerlegt den Wert von wfd_video_formats.
Aufbau: native, preferred-display-mode, dann je Codec elf Felder
(profile, level, CEA, VESA, HH, latency, min-slice, slice-enc,
frame-rate-control, max-hres, max-vres).
"""
parts = value.strip().split()
if len(parts) < 3:
return []
result = []
i = 2 # native und preferred-display-mode überspringen
while i + 4 < len(parts):
try:
capability = SinkCapability(
profile=int(parts[i], 16),
level=int(parts[i + 1], 16),
cea_mask=int(parts[i + 2], 16),
vesa_mask=int(parts[i + 3], 16),
hh_mask=int(parts[i + 4], 16),
)
except ValueError:
break
result.append(capability)
i += 11
return result
def choose(capabilities: List[SinkCapability], max_height: int = 1080) -> Optional[VideoFormat]:
"""Bestes gemeinsames Format, oder None wenn es keines gibt."""
mask = 0
for capability in capabilities:
mask |= capability.cea_mask
for bit in PREFERENCE:
if bit in INTERLACED or not (mask & (1 << bit)):
continue
candidate = CEA[bit]
if candidate.height > max_height:
continue
return candidate
return None
def build_selection(fmt: VideoFormat, profile: int, level: int) -> str:
"""Antwortwert mit genau einem gesetzten Bit - damit der Fernseher weiß,
was tatsächlich kommt."""
cea = 1 << fmt.cea_bit
return (
f"{0:02X} {0:02X} {profile:02X} {level:02X} "
f"{cea:08X} {0:08X} {0:08X} {0:02X} {0:04X} {0:04X} {0:02X} none none"
)
def parse_rtp_port(value: Optional[str]) -> int:
"""Liest aus wfd_client_rtp_ports den Port, auf dem der Fernseher lauscht."""
if not value:
return 0
for part in value.strip().split():
if part.isdigit():
port = int(part)
if 1 <= port <= 65535:
return port
return 0