Files
duck-preview/app/video/video_player.py
bartool e9b474b1ed feat: Add video playback functionality and inference support
- Introduced VideoPlayer class to handle local video playback, emitting frames via frame_ready signal.
- Updated MainWindow to switch between camera and video sources, integrating video playback controls.
- Enhanced AppMenuBar with options to open video files and manage inference models.
- Implemented BboxOverlay for displaying detection results on video frames.
- Added InferenceManager to manage YOLO inference in a separate process, with error handling and restart logic.
- Created tests for BboxOverlay and InferenceManager to ensure functionality and robustness.
- Updated pyproject.toml to include optional dependencies for inference support.
2026-05-13 21:30:13 +02:00

118 lines
3.7 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""VideoPlayer — plays a local video file and delivers frames via frame_ready signal.
The public interface mirrors CameraService so MainWindow can treat both
interchangeably: both emit frame_ready(QVideoFrame).
"""
from __future__ import annotations
import logging
from pathlib import Path
from PySide6.QtCore import QObject, QUrl, Signal, Slot
from PySide6.QtMultimedia import (
QMediaPlayer,
QVideoFrame,
QVideoSink,
)
logger = logging.getLogger(__name__)
class VideoPlayer(QObject):
"""
Wraps QMediaPlayer + QVideoSink to replay a local video file.
Signal flow (identical interface to CameraService):
VideoPlayer.frame_ready(QVideoFrame) → FrameDispatcher
Notes:
- Playback is real-time (1×) — no seek/pause in this version.
- At end-of-file: emits playback_stopped() and stops.
- On any error: emits playback_error(str) then playback_stopped().
"""
frame_ready = Signal(QVideoFrame)
playback_started = Signal()
playback_stopped = Signal()
playback_error = Signal(str)
def __init__(self, parent: QObject | None = None) -> None:
super().__init__(parent)
self._player = QMediaPlayer(self)
self._sink = QVideoSink(self)
self._player.setVideoSink(self._sink)
self._sink.videoFrameChanged.connect(self._on_frame)
self._player.playbackStateChanged.connect(self._on_playback_state_changed)
self._player.errorOccurred.connect(self._on_error)
self._current_path: str | None = None
# ------------------------------------------------------------------
# Public API
# ------------------------------------------------------------------
def play(self, path: str) -> None:
"""Open and start playing a video file."""
self.stop()
p = Path(path)
if not p.exists():
msg = f"Video file not found: {path}"
logger.error(msg)
self.playback_error.emit(msg)
return
self._current_path = path
url = QUrl.fromLocalFile(str(p.resolve()))
self._player.setSource(url)
self._player.play()
logger.info("VideoPlayer: starting playback of '%s'", p.name)
def stop(self) -> None:
"""Stop playback and clear source."""
if self._player.playbackState() != QMediaPlayer.PlaybackState.StoppedState:
self._player.stop()
self._player.setSource(QUrl())
self._current_path = None
@property
def is_playing(self) -> bool:
return (
self._player.playbackState()
== QMediaPlayer.PlaybackState.PlayingState
)
@property
def current_path(self) -> str | None:
return self._current_path
# ------------------------------------------------------------------
# Private slots
# ------------------------------------------------------------------
@Slot(QVideoFrame)
def _on_frame(self, frame: QVideoFrame) -> None:
if frame.isValid():
self.frame_ready.emit(frame)
@Slot(QMediaPlayer.PlaybackState)
def _on_playback_state_changed(self, state: QMediaPlayer.PlaybackState) -> None:
if state == QMediaPlayer.PlaybackState.PlayingState:
logger.info("VideoPlayer: playing")
self.playback_started.emit()
elif state == QMediaPlayer.PlaybackState.StoppedState:
logger.info("VideoPlayer: stopped")
self.playback_stopped.emit()
@Slot(QMediaPlayer.Error, str)
def _on_error(self, error: QMediaPlayer.Error, error_string: str) -> None:
if error == QMediaPlayer.Error.NoError:
return
msg = f"VideoPlayer error: {error_string}"
logger.error(msg)
self.playback_error.emit(msg)
self.playback_stopped.emit()