Implement initial structure and core functionality for Duck Preview application

This commit is contained in:
2026-05-12 07:10:11 +02:00
parent be85d7ca31
commit 58fff52d31
18 changed files with 610 additions and 0 deletions

View File

View File

@@ -0,0 +1,49 @@
from __future__ import annotations
from PySide6.QtCore import Qt
from PySide6.QtGui import QColor, QFont, QFontMetrics, QPainter
from PySide6.QtWidgets import QWidget
class OverlayWidget(QWidget):
def __init__(self, parent: QWidget | None = None) -> None:
super().__init__(parent)
self.setAttribute(Qt.WA_TransparentForMouseEvents)
self.setAttribute(Qt.WA_NoSystemBackground)
self._visible = True
self._metrics: dict[str, float | int] = {}
def set_visible(self, visible: bool) -> None:
self._visible = visible
self.update()
def set_metrics(self, metrics: dict[str, float | int]) -> None:
self._metrics = metrics
self.update()
def paintEvent(self, event) -> None: # noqa: N802
if not self._visible or not self._metrics:
return
lines = [
f"FPS: {self._metrics.get('fps', 0):>7}",
f"Frame: {self._metrics.get('frame_time_ms', 0):>7.1f} ms",
f"Frames: {self._metrics.get('frame_count', 0):>7}",
]
painter = QPainter(self)
painter.setRenderHint(QPainter.TextAntialiasing)
font = QFont("Consolas", 11)
painter.setFont(font)
fm = QFontMetrics(font)
text_width = max(fm.horizontalAdvance(line) for line in lines) + 24
text_height = len(lines) * (fm.height() + 6) + 12
painter.fillRect(8, 8, text_width, text_height, QColor(0, 0, 0, 160))
painter.setPen(QColor(0, 255, 0))
for i, line in enumerate(lines):
y = 22 + i * (fm.height() + 6)
painter.drawText(16, y, line)

View File

@@ -0,0 +1,39 @@
from __future__ import annotations
from PySide6.QtCore import Qt
from PySide6.QtGui import QColor, QImage, QPainter
from PySide6.QtMultimedia import QVideoFrame
from PySide6.QtWidgets import QWidget
class VideoWidget(QWidget):
def __init__(self, parent: QWidget | None = None) -> None:
super().__init__(parent)
self._frame: QImage | None = None
self.setAttribute(Qt.WA_OpaquePaintEvent)
self.setMinimumSize(320, 240)
def on_frame(self, frame: QVideoFrame) -> None:
image = frame.toImage()
if not image.isNull():
self._frame = image
self.update()
def paintEvent(self, event) -> None: # noqa: N802
painter = QPainter(self)
painter.setRenderHint(QPainter.SmoothPixmapTransform)
if self._frame is not None:
painter.fillRect(self.rect(), QColor(0, 0, 0))
scaled = self._frame.scaled(
self.size(),
Qt.KeepAspectRatio,
Qt.SmoothTransformation,
)
x = (self.width() - scaled.width()) // 2
y = (self.height() - scaled.height()) // 2
painter.drawImage(x, y, scaled)
else:
painter.fillRect(self.rect(), QColor(20, 20, 20))
painter.setPen(QColor(100, 100, 100))
painter.drawText(self.rect(), Qt.AlignCenter, "No camera feed")