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