import os
import json
from PyQt5.QtCore import Qt, QThreadPool
from PyQt5.QtGui import QFont
from PyQt5.QtMultimedia import QMediaPlayer
from PyQt5.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QPushButton,
QProgressBar, QSlider, QSizePolicy, QMessageBox
)
from qtawesome import icon
from app.widgets.misc.render_worker import Worker
from app.widgets.misc.video_player import VideoPlayer
[docs]
class VideoAnimation(QWidget):
"""
VideoAnimation Class
====================
A PyQt5-based widget for interactive video playback and 3D frame rendering using precomputed
animation data. This class serves as a control center for reviewing and exporting animations
produced from dynamic scene transformations within the FlapKine environment.
The widget includes:
- A video player for previewing rendered videos.
- Playback controls (play, pause, repeat).
- A slider for timeline navigation.
- A render button to generate video frames using transformation data.
- A progress bar to monitor rendering status.
Attributes
----------
project_folder : str
Path to the project directory containing configuration and output files.
scene_data : SceneData
The scene object containing transformation functions (angles, rotation, translation) used
to generate frame-wise data.
angles : List[float]
List of rotation angles associated with the current scene, used during frame rendering.
reflect : List[bool]
Boolean flags indicating which axes (XY, YZ, XZ) are to be reflected during rendering,
derived from config settings.
video_playing : bool
Internal flag tracking the current playback state.
primary_color : str
Primary color extracted from the application palette, used to style UI icons.
video_widget : VideoPlayer
Custom widget responsible for loading and displaying the rendered video.
playButton : QPushButton
Toggle button to control video playback (play/pause).
repeatButton : QPushButton
Toggle button to enable or disable repeat mode after video ends.
positionSlider : QSlider
Slider for seeking through video frames.
render_button : QPushButton
Button to initiate frame rendering based on scene transformations.
progress_bar : QProgressBar
Visual indicator of the frame rendering progress.
config : dict
Dictionary holding video configuration (e.g., resolution), loaded from config.json.
worker : Worker
Background thread responsible for generating frames during rendering.
Methods
-------
__init__(project_folder, scene_data, parent=None)
Initializes the video animation widget and sets up the UI, configuration, and media loading.
_loadConfig()
Loads video rendering configuration from the project’s config.json file.
_createWidgets()
Creates and configures all internal widgets, including video player, buttons, and sliders.
_createLayout()
Arranges the UI components using QVBoxLayout and QHBoxLayouts for control and render sections.
_connectSignals()
Connects UI signals to their corresponding slot functions for interactive behavior.
_loadMedia()
Loads the rendered video from the project directory, if it exists.
_showError(message)
Displays or logs an error when video loading or configuration fails.
playVideo()
Toggles between play and pause states for the video.
repeatVideo()
Enables or disables repeat mode and restarts playback if enabled.
updateDuration(duration)
Updates the slider’s maximum range based on the video’s total duration.
updatePosition(position)
Updates the slider position based on current playback frame.
setPosition(position)
Sets the video’s current position based on slider interaction.
updateState(state)
Reacts to the media player’s state change and auto-repeats video if required.
update_progress(value)
Updates the progress bar’s value during frame rendering.
genframes()
Starts the rendering worker thread to compute and export video frames.
complete_render()
Handles cleanup and UI updates after rendering is complete and reloads the new video.
"""
[docs]
def __init__(self, project_folder, scene_data, parent=None):
"""
Initializes the VideoAnimation widget.
Sets up the video animation interface for playback and rendering of 3D scene transformations
within the FlapKine environment. Loads configuration, initializes internal state, and prepares
UI components for interaction.
Initialization Tasks:
- Stores references to the project folder and scene data.
- Extracts angle data from the scene's primary object.
- Loads reflection settings from the `config.json` file.
- Determines the primary UI color from the active palette.
- Initializes video playback state as inactive.
- Calls internal setup methods to build widgets, layouts, signal connections, and media loader.
Parameters
----------
project_folder : str
Path to the project directory containing configuration and animation output files.
scene_data : SceneData
Object holding transformation parameters (angles, rotation, translation) required for frame rendering.
parent : QWidget, optional
Optional parent widget for integration within larger PyQt5 interfaces. Defaults to None.
"""
super().__init__(parent)
self.project_folder = project_folder
self.scene_data = scene_data
self.angles = self.scene_data.objects[0].angles
self.parent = parent
self.threadpool = QThreadPool()
with open(os.path.join(self.project_folder, 'config.json')) as f:
config = json.load(f)
reflect = [config['Reflect'] == "XY", config['Reflect'] == "YZ", config['Reflect'] == "XZ"]
self.reflect = reflect
self.video_playing = False
self.primary_color = self.palette().color(self.foregroundRole()).name()
self._loadConfig()
self._createWidgets()
self._createLayout()
self._connectSignals()
self._loadMedia()
[docs]
def _loadConfig(self):
"""
Loads video rendering configuration from config.json.
Attempts to read video rendering parameters from the project's `config.json` file and stores
the parsed dictionary in the `self.config` attribute. If the file is not found, a default
configuration with 640x480 resolution is used instead.
File Parsed:
- config.json located in the project directory
Attributes Set:
- self.config : dict containing video resolution and rendering parameters
"""
config_path = os.path.join(self.project_folder, 'config.json')
if os.path.exists(config_path):
with open(config_path) as f:
self.config = json.load(f)
else:
self.config = {"VideoRender": {"resolution_x": 640, "resolution_y": 480}}
[docs]
def _createLayout(self):
"""
Constructs and applies the layout for the video animation interface.
Organizes the widget components into a clean vertical layout structure with logical
grouping for playback controls and rendering tools. Ensures adaptive spacing and
alignment for a responsive UI.
Layout Structure:
- `controlLayout` (QHBoxLayout): Holds play, repeat, and slider widgets.
- `renderLayout` (QHBoxLayout): Holds render button and progress bar.
- `layout` (QVBoxLayout): Main vertical layout stacking video widget, control layout,
and render layout with spacing and stretch for balance.
Final Layout Assignment:
- Calls `self.setLayout(layout)` to apply the assembled layout to the widget.
"""
# Layouts
controlLayout = QHBoxLayout()
controlLayout.addWidget(self.playButton)
controlLayout.addWidget(self.repeatButton)
controlLayout.addWidget(self.positionSlider)
renderLayout = QHBoxLayout()
renderLayout.addWidget(self.render_button)
renderLayout.addWidget(self.progress_bar)
layout = QVBoxLayout()
layout.addWidget(self.video_widget)
layout.addSpacing(10) # Optional visual buffer
layout.addLayout(controlLayout)
layout.addLayout(renderLayout)
layout.addStretch() # Push everything above if extra space
self.setLayout(layout)
[docs]
def _connectSignals(self):
"""
Connects UI signals to their corresponding slot functions.
Establishes all internal signal-slot connections required for interactive behavior,
including video playback, repeat toggling, slider movement, rendering initiation,
and real-time media updates.
Signal Connections:
- `playButton.clicked` → `playVideo()`
- `repeatButton.clicked` → `repeatVideo()`
- `positionSlider.sliderMoved` → `setPosition()`
- `render_button.clicked` → `genframes()`
- `media_player.durationChanged` → `updateDuration()`
- `media_player.positionChanged` → `updatePosition()`
- `media_player.stateChanged` → `updateState()`
Purpose:
Ensures responsive video control and rendering feedback through Qt's event-driven architecture.
"""
self.playButton.clicked.connect(self.playVideo)
self.repeatButton.clicked.connect(self.repeatVideo)
self.positionSlider.sliderMoved.connect(self.setPosition)
self.render_button.clicked.connect(self.genframes)
self.video_widget.media_player.durationChanged.connect(self.updateDuration)
self.video_widget.media_player.positionChanged.connect(self.updatePosition)
self.video_widget.media_player.stateChanged.connect(self.updateState)
[docs]
def showAlertDialog(self, title, message):
"""
Displays an informational alert dialog with the given title and message.
Presents a modal `QMessageBox` configured to convey general information to the user.
Commonly used for confirmations, status updates, or non-critical notices.
Parameters
----------
title : str
The title text displayed on the alert dialog window.
message : str
The information content shown inside the dialog.
"""
alert_dialog = QMessageBox()
alert_dialog.setIcon(QMessageBox.Information)
alert_dialog.setWindowTitle(title)
alert_dialog.setText(message)
alert_dialog.exec_()
[docs]
def showErrorDialog(self, title, message):
"""
Displays a critical error dialog with the specified title and message.
Creates and shows a modal `QMessageBox` configured to indicate an error condition.
Typically used to alert users when essential files or configurations are missing.
Parameters
----------
title : str
The title text displayed on the error dialog window.
message : str
The detailed error message shown within the dialog content.
"""
error_dialog = QMessageBox()
error_dialog.setIcon(QMessageBox.Critical)
error_dialog.setWindowTitle(title)
error_dialog.setText(message)
error_dialog.exec_()
[docs]
def playVideo(self):
"""
Toggles video playback state between play and pause.
Handles the core play/pause logic based on the internal `video_playing` flag. Updates both the
media player's state and the icon displayed on the play button to reflect the current action.
Behavior:
- If video is playing: pauses playback and updates button to 'play' icon.
- If video is paused: starts playback and updates button to 'pause' icon.
Updates:
- `video_playing` flag
- `playButton` icon using QtAwesome with primary UI color
"""
if self.video_playing:
self.video_widget.media_player.pause()
self.playButton.setIcon(icon("mdi.play", color=self.primary_color))
self.video_playing = False
else:
self.video_widget.media_player.play()
self.playButton.setIcon(icon("mdi.pause", color=self.primary_color))
self.video_playing = True
[docs]
def repeatVideo(self):
"""
Toggles repeat mode for the video playback.
Checks the state of the `repeatButton` and updates the repeat icon accordingly. If repeat mode
is enabled, the video restarts from the beginning and begins playback immediately. If disabled,
it simply reverts the icon to the repeat state.
Behavior:
- If checked: sets icon to `repeat-off`, resets position to start, and plays the video.
- If unchecked: sets icon back to `repeat`.
UI Updates:
- `repeatButton` icon
- `playButton` icon (set to 'pause' if playback is restarted)
"""
if self.repeatButton.isChecked():
self.repeatButton.setIcon(icon("mdi.repeat-off", color=self.primary_color))
self.video_widget.media_player.setPosition(0)
self.video_widget.media_player.play()
self.playButton.setIcon(icon("mdi.pause", color=self.primary_color))
else:
self.repeatButton.setIcon(icon("mdi.repeat", color=self.primary_color))
[docs]
def updateDuration(self, duration):
"""
Updates the slider range based on video duration.
Parameters
----------
duration : int
Total duration of the video in milliseconds.
"""
self.positionSlider.setRange(0, duration)
[docs]
def updatePosition(self, position):
"""
Updates the slider's value to match the current playback position.
Parameters
----------
position : int
Current position of the video in milliseconds.
"""
self.positionSlider.setValue(position)
[docs]
def setPosition(self, position):
"""
Sets the media player to the given position.
Parameters
----------
position : int
New playback position in milliseconds.
"""
self.video_widget.media_player.setPosition(position)
[docs]
def updateState(self, state):
"""
Handles media state changes and auto-repeats video if in repeat mode.
Parameters
----------
state : QMediaPlayer.State
Current state of the media player.
"""
if state == QMediaPlayer.StoppedState and self.repeatButton.isChecked():
self.video_widget.media_player.setPosition(0)
self.video_widget.media_player.play()
[docs]
def update_progress(self, value):
"""
Updates the progress bar with the given value.
Parameters
----------
value : int or float
Current progress percentage (0–100).
"""
self.progress_bar.setValue(int(value))
[docs]
def genframes(self):
"""
Starts the frame rendering process using a background worker thread.
Initializes a `Worker` instance with the current scene parameters and begins the rendering task
asynchronously. Disables the render button during processing and connects progress and completion
signals for real-time UI feedback.
Workflow:
- Disables `render_button` to prevent multiple triggers.
- Instantiates `Worker` with `project_folder`, `angles`, `scene_data`, and `reflect`.
- Connects `progress_signal` to `update_progress()` for live updates.
- Starts the worker thread to render frames.
- Connects `finished` signal to `complete_render()` for post-processing and cleanup.
"""
self.parent.right_group.setEnabled(False)
self.parent.topleftgroup.setEnabled(False)
self.parent.bottomleftgroup.setEnabled(False)
self.render_button.setEnabled(False)
self.worker = Worker(self.project_folder, self.angles, self.scene_data, self.reflect)
self.threadpool.start(self.worker)
self.worker.signals.progress_signal.connect(self.update_progress)
self.worker.signals.finished.connect(self.complete_render)
[docs]
def complete_render(self):
"""
Finalizes the rendering process and updates the UI.
Re-enables the render button, displays a confirmation dialog with the output video path,
and reloads the rendered video into the video player for immediate preview.
Post-Render Actions:
- Enables `render_button` for future rendering.
- Displays alert with the final video path.
- Loads the new video into `video_widget` using `setMedia()`.
"""
self.render_button.setEnabled(True)
project_name = os.path.basename(self.project_folder)
video_path = os.path.join(self.project_folder, f'data/videos/{project_name}.mp4')
self.showAlertDialog('Alert', f"Video rendered successfully at: {video_path}")
self.video_widget.setMedia(video_path)
self.parent.right_group.setEnabled(True)
self.parent.topleftgroup.setEnabled(True)
self.parent.bottomleftgroup.setEnabled(True)