122 lines
3.5 KiB
Python
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
|