import sys import logging from pathlib import Path from PySide6.QtCore import Qt, QTimer from PySide6.QtGui import QAction from PySide6.QtWidgets import QApplication, QMainWindow, QLabel from PySide6.QtMultimedia import ( QMediaDevices, QCamera, QCameraDevice, QMediaFormat, QVideoFrameFormat, QMediaCaptureSession ) from PySide6.QtMultimediaWidgets import QVideoWidget # ============================================================ # LOGGER # ============================================================ LOG_DIR = Path("logs") LOG_DIR.mkdir(exist_ok=True) logging.basicConfig( level=logging.DEBUG, format="%(asctime)s [%(levelname)s] %(message)s", handlers=[ logging.FileHandler(LOG_DIR / "camera_debug.log", encoding="utf-8"), logging.StreamHandler(sys.stdout), ], ) logger = logging.getLogger("camera_app") # ============================================================ # CAMERA WINDOW # ============================================================ class CameraWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("Camera Preview") # Widget wyświetlający wyłącznie obraz self.video_widget = QVideoWidget() self.setCentralWidget(self.video_widget) self.camera = None self.camera_device = None self.capture_session = QMediaCaptureSession() self.init_camera() # ======================================================== # CAMERA INIT # ======================================================== def init_camera(self): devices = QMediaDevices.videoInputs() logger.info("==========================================") logger.info("VIDEO DEVICES ENUMERATION") logger.info("==========================================") if not devices: logger.error("Nie znaleziono kamer.") return for idx, device in enumerate(devices): self.log_camera_device(device, idx) # wybór pierwszej kamery self.camera_device = devices[0] logger.info(f"Wybrano kamerę: {self.camera_device.description()}") self.camera = QCamera(self.camera_device) # sygnały self.camera.errorOccurred.connect(self.on_camera_error) self.camera.activeChanged.connect(self.on_active_changed) # video sink # self.camera.setVideoOutput(self.video_widget) self.capture_session.setCamera(self.camera) self.capture_session.setVideoOutput(self.video_widget) # przykładowe ustawienia self.set_camera_format( width=1280, height=720, fps=30, preferred_pixel_format=QVideoFrameFormat.PixelFormat.Format_Jpeg ) self.log_runtime_camera_capabilities() self.camera.start() logger.info("Kamera uruchomiona.") # ======================================================== # FORMAT SELECTION # ======================================================== def set_camera_format( self, width: int, height: int, fps: int, preferred_pixel_format=QVideoFrameFormat.PixelFormat.Format_Jpeg, ): # def set_camera_format(self, width: int, height: int, fps: int): """ Prototyp funkcji ustawiającej: - rozdzielczość - FPS Wybiera najbliższy pasujący format. """ logger.info( f"Próba ustawienia formatu: {width}x{height} @ {fps} FPS" ) best_match = None for fmt in self.camera_device.videoFormats(): resolution = fmt.resolution() min_fps = fmt.minFrameRate() max_fps = fmt.maxFrameRate() pixel_format = fmt.pixelFormat() logger.debug( f"Sprawdzam format -> " f"{resolution.width()}x{resolution.height()} " f"FPS:{min_fps}-{max_fps} " f"PIX:{pixel_format}" ) if ( resolution.width() == width and resolution.height() == height and min_fps <= fps <= max_fps and pixel_format == preferred_pixel_format ): best_match = fmt break if best_match: self.camera.setCameraFormat(best_match) logger.info("Ustawiono format kamery:") logger.info( f"Resolution: " f"{best_match.resolution().width()}x" f"{best_match.resolution().height()}" ) logger.info( f"FPS range: " f"{best_match.minFrameRate()} - " f"{best_match.maxFrameRate()}" ) logger.info( f"Pixel format: {best_match.pixelFormat()}" ) else: logger.warning("Nie znaleziono pasującego formatu.") # ======================================================== # DEVICE LOGGER # ======================================================== def log_camera_device(self, device: QCameraDevice, idx: int): logger.info("------------------------------------------") logger.info(f"KAMERA #{idx}") logger.info("------------------------------------------") logger.info(f"Description: {device.description()}") logger.info(f"ID: {device.id().data().decode(errors='ignore')}") try: logger.info(f"Is default: {device.isDefault()}") except Exception as e: logger.warning(f"isDefault() unsupported: {e}") formats = device.videoFormats() logger.info(f"Liczba formatów: {len(formats)}") for i, fmt in enumerate(formats): resolution = fmt.resolution() logger.info(f"") logger.info(f"FORMAT #{i}") logger.info( f"Resolution: " f"{resolution.width()}x{resolution.height()}" ) logger.info( f"FPS min/max: " f"{fmt.minFrameRate()} / {fmt.maxFrameRate()}" ) logger.info( f"Pixel format enum: {fmt.pixelFormat()}" ) logger.info( f"Pixel format name: " f"{self.pixel_format_to_string(fmt.pixelFormat())}" ) # ======================================================== # RUNTIME CAMERA CAPABILITIES # ======================================================== def log_runtime_camera_capabilities(self): logger.info("") logger.info("==========================================") logger.info("QCAMERA RUNTIME CAPABILITIES") logger.info("==========================================") try: logger.info(f"Camera active: {self.camera.isActive()}") except Exception as e: logger.warning(e) # Dostępne w zależności od backendu/platformy properties = [ "focusMode", "exposureMode", "whiteBalanceMode", "flashMode", "torchMode", ] for prop in properties: try: value = getattr(self.camera, prop)() logger.info(f"{prop}: {value}") except Exception as e: logger.warning(f"{prop} unsupported: {e}") # backend/platform info logger.info("") logger.info("QT MULTIMEDIA INFO") try: logger.info(f"Qt version: {QApplication.qtVersion()}") except Exception: pass logger.info( "Backend zależy od platformy:" ) logger.info( "- Windows -> MediaFoundation" ) logger.info( "- Linux -> GStreamer / PipeWire / V4L2" ) logger.info( "- macOS -> AVFoundation" ) # ======================================================== # PIXEL FORMAT NAME # ======================================================== def pixel_format_to_string(self, pixel_format): mapping = { QVideoFrameFormat.PixelFormat.Format_YUV420P: "YUV420P", QVideoFrameFormat.PixelFormat.Format_NV12: "NV12", QVideoFrameFormat.PixelFormat.Format_NV21: "NV21", QVideoFrameFormat.PixelFormat.Format_UYVY: "UYVY", QVideoFrameFormat.PixelFormat.Format_YUYV: "YUYV", QVideoFrameFormat.PixelFormat.Format_Jpeg: "MJPG/JPEG", } return mapping.get(pixel_format, str(pixel_format)) # ======================================================== # SIGNALS # ======================================================== def on_camera_error(self, error, error_string): logger.error(f"CAMERA ERROR: {error}") logger.error(f"ERROR STRING: {error_string}") def on_active_changed(self, active): logger.info(f"Camera active changed: {active}") # ======================================================== # CLEANUP # ======================================================== # def closeEvent(self, event): # logger.info("Zamykanie aplikacji.") # if self.camera: # self.camera.stop() # super().closeEvent(event) def closeEvent(self, event): logger.info("Zamykanie aplikacji.") try: if self.camera: logger.info("Stopping camera...") self.camera.stop() if self.capture_session: logger.info("Detaching camera from capture session...") self.capture_session.setCamera(None) self.capture_session.setVideoOutput(None) except Exception as e: logger.exception(e) super().closeEvent(event) # ============================================================ # MAIN # ============================================================ def main(): app = QApplication(sys.argv) window = CameraWindow() # tylko obraz window.resize(1280, 720) # fullscreen: # window.showFullScreen() window.show() sys.exit(app.exec()) if __name__ == "__main__": main()