Refactor code structure for improved readability and maintainability
This commit is contained in:
parent
8f64345335
commit
82082911b3
0
fix_icons.py
Normal file
0
fix_icons.py
Normal file
489
pyqt6_video_player.py
Normal file
489
pyqt6_video_player.py
Normal file
@ -0,0 +1,489 @@
|
||||
"""
|
||||
PyQt6 Professional Video Player - Prototype
|
||||
Author: Dario Pascoal
|
||||
|
||||
Description: This is a PyQt6 implementation of a professional video player to demonstrate
|
||||
the advantages PyQt6 could bring to the video editor application. This prototype shows:
|
||||
|
||||
- Hardware-accelerated video playback with QMediaPlayer
|
||||
- Professional video controls and scrubbing
|
||||
- Modern UI styling with dark theme
|
||||
- Smooth timeline with precise seeking
|
||||
- Professional video display with proper aspect ratio
|
||||
- Keyboard shortcuts (Space, arrows, etc.)
|
||||
- Full-screen capabilities
|
||||
- Real-time effects pipeline ready
|
||||
|
||||
This serves as a proof-of-concept for upgrading the current Tkinter video editor
|
||||
to PyQt6 for better performance and professional features.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
from PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
|
||||
QHBoxLayout, QSlider, QPushButton, QLabel,
|
||||
QFileDialog, QFrame, QSizePolicy, QSpacerItem,
|
||||
QStyle, QStyleFactory, QMessageBox)
|
||||
from PyQt6.QtCore import (Qt, QUrl, QTimer, pyqtSignal, QPropertyAnimation,
|
||||
QEasingCurve, QRect, QThread, pyqtSlot)
|
||||
from PyQt6.QtMultimedia import QMediaPlayer, QAudioOutput
|
||||
from PyQt6.QtMultimediaWidgets import QVideoWidget
|
||||
from PyQt6.QtGui import (QPalette, QColor, QFont, QIcon, QKeySequence,
|
||||
QShortcut, QPixmap, QPainter, QBrush)
|
||||
|
||||
class ModernSlider(QSlider):
|
||||
"""Custom slider with modern styling and smooth scrubbing"""
|
||||
|
||||
def __init__(self, orientation=Qt.Orientation.Horizontal):
|
||||
super().__init__(orientation)
|
||||
self.setStyleSheet("""
|
||||
QSlider::groove:horizontal {
|
||||
background: #404040;
|
||||
height: 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
QSlider::handle:horizontal {
|
||||
background: #00aaff;
|
||||
border: 2px solid #ffffff;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
margin: -7px 0;
|
||||
border-radius: 9px;
|
||||
}
|
||||
QSlider::handle:horizontal:hover {
|
||||
background: #0088cc;
|
||||
border: 2px solid #ffffff;
|
||||
}
|
||||
QSlider::sub-page:horizontal {
|
||||
background: #00aaff;
|
||||
border-radius: 4px;
|
||||
}
|
||||
""")
|
||||
|
||||
class ProfessionalVideoPlayer(QMainWindow):
|
||||
"""Professional video player with PyQt6 multimedia capabilities"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.current_video_path = None
|
||||
self.is_fullscreen = False
|
||||
self.setup_ui()
|
||||
self.setup_media_player()
|
||||
self.setup_shortcuts()
|
||||
self.setup_styling()
|
||||
|
||||
# Load a sample video if available
|
||||
self.auto_load_sample_video()
|
||||
|
||||
def setup_ui(self):
|
||||
"""Setup the professional video player interface"""
|
||||
self.setWindowTitle("PyQt6 Professional Video Player - Prototype")
|
||||
self.setGeometry(100, 100, 1200, 800)
|
||||
self.setMinimumSize(800, 600)
|
||||
|
||||
# Central widget
|
||||
central_widget = QWidget()
|
||||
self.setCentralWidget(central_widget)
|
||||
|
||||
# Main layout
|
||||
main_layout = QVBoxLayout(central_widget)
|
||||
main_layout.setContentsMargins(10, 10, 10, 10)
|
||||
main_layout.setSpacing(10)
|
||||
|
||||
# Video display area
|
||||
self.video_widget = QVideoWidget()
|
||||
self.video_widget.setMinimumHeight(400)
|
||||
self.video_widget.setStyleSheet("""
|
||||
QVideoWidget {
|
||||
background-color: #000000;
|
||||
border: 2px solid #333333;
|
||||
border-radius: 8px;
|
||||
}
|
||||
""")
|
||||
main_layout.addWidget(self.video_widget, 1) # Takes most space
|
||||
|
||||
# Controls panel
|
||||
controls_frame = QFrame()
|
||||
controls_frame.setFixedHeight(120)
|
||||
controls_frame.setStyleSheet("""
|
||||
QFrame {
|
||||
background-color: #2d2d2d;
|
||||
border: 1px solid #404040;
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
}
|
||||
""")
|
||||
main_layout.addWidget(controls_frame)
|
||||
|
||||
controls_layout = QVBoxLayout(controls_frame)
|
||||
|
||||
# Timeline slider
|
||||
timeline_layout = QHBoxLayout()
|
||||
|
||||
self.current_time_label = QLabel("00:00")
|
||||
self.current_time_label.setStyleSheet("color: #ffffff; font-weight: bold;")
|
||||
self.current_time_label.setMinimumWidth(50)
|
||||
timeline_layout.addWidget(self.current_time_label)
|
||||
|
||||
self.timeline_slider = ModernSlider()
|
||||
self.timeline_slider.setRange(0, 1000)
|
||||
self.timeline_slider.setValue(0)
|
||||
self.timeline_slider.sliderPressed.connect(self.on_timeline_pressed)
|
||||
self.timeline_slider.sliderReleased.connect(self.on_timeline_released)
|
||||
self.timeline_slider.valueChanged.connect(self.on_timeline_changed)
|
||||
timeline_layout.addWidget(self.timeline_slider, 1)
|
||||
|
||||
self.duration_label = QLabel("00:00")
|
||||
self.duration_label.setStyleSheet("color: #ffffff; font-weight: bold;")
|
||||
self.duration_label.setMinimumWidth(50)
|
||||
timeline_layout.addWidget(self.duration_label)
|
||||
|
||||
controls_layout.addLayout(timeline_layout)
|
||||
|
||||
# Playback controls
|
||||
playback_layout = QHBoxLayout()
|
||||
|
||||
# Load video button
|
||||
self.load_btn = self.create_control_button("📁", "Load Video", self.load_video)
|
||||
playback_layout.addWidget(self.load_btn)
|
||||
|
||||
playback_layout.addSpacerItem(QSpacerItem(20, 20, QSizePolicy.Policy.Expanding))
|
||||
|
||||
# Previous frame
|
||||
self.prev_frame_btn = self.create_control_button("⏮", "Previous Frame", self.previous_frame)
|
||||
playback_layout.addWidget(self.prev_frame_btn)
|
||||
|
||||
# Play/Pause
|
||||
self.play_pause_btn = self.create_control_button("▶", "Play/Pause", self.toggle_playback)
|
||||
self.play_pause_btn.setStyleSheet(self.play_pause_btn.styleSheet() + "min-width: 60px;")
|
||||
playback_layout.addWidget(self.play_pause_btn)
|
||||
|
||||
# Next frame
|
||||
self.next_frame_btn = self.create_control_button("⏭", "Next Frame", self.next_frame)
|
||||
playback_layout.addWidget(self.next_frame_btn)
|
||||
|
||||
playback_layout.addSpacerItem(QSpacerItem(20, 20, QSizePolicy.Policy.Expanding))
|
||||
|
||||
# Volume control
|
||||
volume_layout = QHBoxLayout()
|
||||
volume_icon = QLabel("🔊")
|
||||
volume_icon.setStyleSheet("color: #ffffff; font-size: 16px;")
|
||||
volume_layout.addWidget(volume_icon)
|
||||
|
||||
self.volume_slider = ModernSlider()
|
||||
self.volume_slider.setRange(0, 100)
|
||||
self.volume_slider.setValue(70)
|
||||
self.volume_slider.setMaximumWidth(100)
|
||||
self.volume_slider.valueChanged.connect(self.on_volume_changed)
|
||||
volume_layout.addWidget(self.volume_slider)
|
||||
|
||||
playback_layout.addLayout(volume_layout)
|
||||
|
||||
# Fullscreen button
|
||||
self.fullscreen_btn = self.create_control_button("⛶", "Fullscreen (F11)", self.toggle_fullscreen)
|
||||
playback_layout.addWidget(self.fullscreen_btn)
|
||||
|
||||
controls_layout.addLayout(playback_layout)
|
||||
|
||||
# Status bar
|
||||
self.status_label = QLabel("Ready - Load a video to start")
|
||||
self.status_label.setStyleSheet("""
|
||||
color: #aaaaaa;
|
||||
background-color: #1e1e1e;
|
||||
padding: 5px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
""")
|
||||
main_layout.addWidget(self.status_label)
|
||||
|
||||
def create_control_button(self, text, tooltip, callback):
|
||||
"""Create a styled control button"""
|
||||
btn = QPushButton(text)
|
||||
btn.setToolTip(tooltip)
|
||||
btn.clicked.connect(callback)
|
||||
btn.setStyleSheet("""
|
||||
QPushButton {
|
||||
background-color: #404040;
|
||||
color: #ffffff;
|
||||
border: 2px solid #555555;
|
||||
border-radius: 6px;
|
||||
padding: 8px 12px;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
min-width: 40px;
|
||||
min-height: 30px;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #505050;
|
||||
border: 2px solid #00aaff;
|
||||
}
|
||||
QPushButton:pressed {
|
||||
background-color: #00aaff;
|
||||
}
|
||||
""")
|
||||
return btn
|
||||
|
||||
def setup_media_player(self):
|
||||
"""Setup PyQt6 media player with hardware acceleration"""
|
||||
self.media_player = QMediaPlayer()
|
||||
self.audio_output = QAudioOutput()
|
||||
|
||||
# Connect media player to video widget
|
||||
self.media_player.setVideoOutput(self.video_widget)
|
||||
self.media_player.setAudioOutput(self.audio_output)
|
||||
|
||||
# Connect signals
|
||||
self.media_player.positionChanged.connect(self.update_position)
|
||||
self.media_player.durationChanged.connect(self.update_duration)
|
||||
self.media_player.playbackStateChanged.connect(self.update_playback_state)
|
||||
self.media_player.errorOccurred.connect(self.handle_error)
|
||||
|
||||
# Timeline update timer
|
||||
self.position_timer = QTimer()
|
||||
self.position_timer.timeout.connect(self.update_timeline_position)
|
||||
self.position_timer.start(50) # 20 FPS updates
|
||||
|
||||
# Timeline dragging state
|
||||
self.timeline_dragging = False
|
||||
|
||||
def setup_shortcuts(self):
|
||||
"""Setup keyboard shortcuts for professional video editing"""
|
||||
# Spacebar - Play/Pause
|
||||
space_shortcut = QShortcut(QKeySequence(Qt.Key.Key_Space), self)
|
||||
space_shortcut.activated.connect(self.toggle_playback)
|
||||
|
||||
# Left/Right arrows - Frame navigation
|
||||
left_shortcut = QShortcut(QKeySequence(Qt.Key.Key_Left), self)
|
||||
left_shortcut.activated.connect(self.previous_frame)
|
||||
|
||||
right_shortcut = QShortcut(QKeySequence(Qt.Key.Key_Right), self)
|
||||
right_shortcut.activated.connect(self.next_frame)
|
||||
|
||||
# Shift + Left/Right - Skip by seconds
|
||||
shift_left = QShortcut(QKeySequence(Qt.KeyboardModifier.ShiftModifier | Qt.Key.Key_Left), self)
|
||||
shift_left.activated.connect(lambda: self.seek_relative(-5000))
|
||||
|
||||
shift_right = QShortcut(QKeySequence(Qt.KeyboardModifier.ShiftModifier | Qt.Key.Key_Right), self)
|
||||
shift_right.activated.connect(lambda: self.seek_relative(5000))
|
||||
|
||||
# Home/End - Start/End
|
||||
home_shortcut = QShortcut(QKeySequence(Qt.Key.Key_Home), self)
|
||||
home_shortcut.activated.connect(lambda: self.media_player.setPosition(0))
|
||||
|
||||
end_shortcut = QShortcut(QKeySequence(Qt.Key.Key_End), self)
|
||||
end_shortcut.activated.connect(lambda: self.media_player.setPosition(self.media_player.duration()))
|
||||
|
||||
# F11 - Fullscreen
|
||||
f11_shortcut = QShortcut(QKeySequence(Qt.Key.Key_F11), self)
|
||||
f11_shortcut.activated.connect(self.toggle_fullscreen)
|
||||
|
||||
# ESC - Exit fullscreen
|
||||
esc_shortcut = QShortcut(QKeySequence(Qt.Key.Key_Escape), self)
|
||||
esc_shortcut.activated.connect(self.exit_fullscreen)
|
||||
|
||||
def setup_styling(self):
|
||||
"""Apply professional dark theme styling"""
|
||||
self.setStyleSheet("""
|
||||
QMainWindow {
|
||||
background-color: #1e1e1e;
|
||||
color: #ffffff;
|
||||
}
|
||||
QLabel {
|
||||
color: #ffffff;
|
||||
}
|
||||
""")
|
||||
|
||||
def auto_load_sample_video(self):
|
||||
"""Auto-load a sample video if available"""
|
||||
sample_videos = ["short_1.mp4", "myvideo.mp4", "myvideo2.mp4"]
|
||||
for video in sample_videos:
|
||||
if os.path.exists(video):
|
||||
self.load_video_file(video)
|
||||
self.status_label.setText(f"✅ Auto-loaded: {video}")
|
||||
break
|
||||
else:
|
||||
self.status_label.setText("📁 Click 'Load Video' to select a video file")
|
||||
|
||||
def load_video(self):
|
||||
"""Load video file dialog"""
|
||||
file_path, _ = QFileDialog.getOpenFileName(
|
||||
self,
|
||||
"Select Video File",
|
||||
"",
|
||||
"Video Files (*.mp4 *.avi *.mov *.mkv *.wmv *.flv *.webm)"
|
||||
)
|
||||
if file_path:
|
||||
self.load_video_file(file_path)
|
||||
|
||||
def load_video_file(self, file_path):
|
||||
"""Load video file into media player"""
|
||||
try:
|
||||
self.current_video_path = file_path
|
||||
url = QUrl.fromLocalFile(file_path)
|
||||
self.media_player.setSource(url)
|
||||
self.status_label.setText(f"✅ Loaded: {os.path.basename(file_path)}")
|
||||
|
||||
# Enable controls
|
||||
self.timeline_slider.setEnabled(True)
|
||||
self.play_pause_btn.setEnabled(True)
|
||||
self.prev_frame_btn.setEnabled(True)
|
||||
self.next_frame_btn.setEnabled(True)
|
||||
|
||||
except Exception as e:
|
||||
self.status_label.setText(f"❌ Error loading video: {e}")
|
||||
QMessageBox.critical(self, "Error", f"Failed to load video:\n{e}")
|
||||
|
||||
def toggle_playback(self):
|
||||
"""Toggle between play and pause"""
|
||||
if self.media_player.playbackState() == QMediaPlayer.PlaybackState.PlayingState:
|
||||
self.media_player.pause()
|
||||
else:
|
||||
self.media_player.play()
|
||||
|
||||
def previous_frame(self):
|
||||
"""Go to previous frame (1/30th second)"""
|
||||
current_pos = self.media_player.position()
|
||||
frame_duration = 1000 // 30 # ~33ms for 30fps
|
||||
new_pos = max(0, current_pos - frame_duration)
|
||||
self.media_player.setPosition(new_pos)
|
||||
|
||||
def next_frame(self):
|
||||
"""Go to next frame (1/30th second)"""
|
||||
current_pos = self.media_player.position()
|
||||
frame_duration = 1000 // 30 # ~33ms for 30fps
|
||||
new_pos = min(self.media_player.duration(), current_pos + frame_duration)
|
||||
self.media_player.setPosition(new_pos)
|
||||
|
||||
def seek_relative(self, ms):
|
||||
"""Seek relative to current position"""
|
||||
current_pos = self.media_player.position()
|
||||
new_pos = max(0, min(self.media_player.duration(), current_pos + ms))
|
||||
self.media_player.setPosition(new_pos)
|
||||
|
||||
def toggle_fullscreen(self):
|
||||
"""Toggle fullscreen mode"""
|
||||
if self.is_fullscreen:
|
||||
self.exit_fullscreen()
|
||||
else:
|
||||
self.enter_fullscreen()
|
||||
|
||||
def enter_fullscreen(self):
|
||||
"""Enter fullscreen mode"""
|
||||
self.is_fullscreen = True
|
||||
self.video_widget.setParent(None)
|
||||
self.video_widget.showFullScreen()
|
||||
self.video_widget.setFocus()
|
||||
|
||||
# Add fullscreen controls overlay (simplified)
|
||||
self.fullscreen_btn.setText("🗗")
|
||||
self.status_label.setText("🖥️ Fullscreen mode - Press ESC or F11 to exit")
|
||||
|
||||
def exit_fullscreen(self):
|
||||
"""Exit fullscreen mode"""
|
||||
if self.is_fullscreen:
|
||||
self.is_fullscreen = False
|
||||
self.video_widget.setParent(self.centralWidget())
|
||||
|
||||
# Re-add to layout
|
||||
layout = self.centralWidget().layout()
|
||||
layout.insertWidget(0, self.video_widget, 1)
|
||||
|
||||
self.video_widget.showNormal()
|
||||
self.fullscreen_btn.setText("⛶")
|
||||
self.status_label.setText("🖥️ Exited fullscreen mode")
|
||||
|
||||
def on_volume_changed(self, value):
|
||||
"""Handle volume changes"""
|
||||
volume = value / 100.0
|
||||
self.audio_output.setVolume(volume)
|
||||
self.status_label.setText(f"🔊 Volume: {value}%")
|
||||
|
||||
def on_timeline_pressed(self):
|
||||
"""Timeline slider pressed - start dragging"""
|
||||
self.timeline_dragging = True
|
||||
|
||||
def on_timeline_released(self):
|
||||
"""Timeline slider released - seek to position"""
|
||||
self.timeline_dragging = False
|
||||
if self.media_player.duration() > 0:
|
||||
position = (self.timeline_slider.value() / 1000.0) * self.media_player.duration()
|
||||
self.media_player.setPosition(int(position))
|
||||
|
||||
def on_timeline_changed(self, value):
|
||||
"""Timeline slider value changed"""
|
||||
if self.timeline_dragging and self.media_player.duration() > 0:
|
||||
# Update time display while dragging
|
||||
position = (value / 1000.0) * self.media_player.duration()
|
||||
self.current_time_label.setText(self.format_time(int(position)))
|
||||
|
||||
def update_position(self, position):
|
||||
"""Update timeline position from media player"""
|
||||
if not self.timeline_dragging and self.media_player.duration() > 0:
|
||||
value = (position / self.media_player.duration()) * 1000
|
||||
self.timeline_slider.setValue(int(value))
|
||||
self.current_time_label.setText(self.format_time(position))
|
||||
|
||||
def update_duration(self, duration):
|
||||
"""Update duration display"""
|
||||
self.duration_label.setText(self.format_time(duration))
|
||||
self.timeline_slider.setEnabled(duration > 0)
|
||||
|
||||
def update_timeline_position(self):
|
||||
"""High-frequency timeline updates"""
|
||||
if not self.timeline_dragging:
|
||||
position = self.media_player.position()
|
||||
self.update_position(position)
|
||||
|
||||
def update_playback_state(self, state):
|
||||
"""Update play/pause button based on playback state"""
|
||||
if state == QMediaPlayer.PlaybackState.PlayingState:
|
||||
self.play_pause_btn.setText("⏸")
|
||||
self.status_label.setText("▶️ Playing...")
|
||||
else:
|
||||
self.play_pause_btn.setText("▶")
|
||||
if state == QMediaPlayer.PlaybackState.PausedState:
|
||||
self.status_label.setText("⏸️ Paused")
|
||||
else:
|
||||
self.status_label.setText("⏹️ Stopped")
|
||||
|
||||
def handle_error(self, error):
|
||||
"""Handle media player errors"""
|
||||
error_msg = f"Media player error: {error}"
|
||||
self.status_label.setText(f"❌ {error_msg}")
|
||||
QMessageBox.critical(self, "Playback Error", error_msg)
|
||||
|
||||
def format_time(self, ms):
|
||||
"""Format time in mm:ss format"""
|
||||
seconds = ms // 1000
|
||||
minutes = seconds // 60
|
||||
seconds = seconds % 60
|
||||
return f"{minutes:02d}:{seconds:02d}"
|
||||
|
||||
def main():
|
||||
"""Run the PyQt6 video player prototype"""
|
||||
app = QApplication(sys.argv)
|
||||
|
||||
# Set application properties
|
||||
app.setApplicationName("PyQt6 Video Player Prototype")
|
||||
app.setApplicationVersion("1.0")
|
||||
|
||||
# Apply dark style
|
||||
app.setStyle(QStyleFactory.create('Fusion'))
|
||||
|
||||
# Create and show main window
|
||||
player = ProfessionalVideoPlayer()
|
||||
player.show()
|
||||
|
||||
print("🎬 PyQt6 Professional Video Player Started!")
|
||||
print("📋 Features:")
|
||||
print(" • Hardware-accelerated playback")
|
||||
print(" • Professional timeline scrubbing")
|
||||
print(" • Keyboard shortcuts (Space, arrows, F11)")
|
||||
print(" • Fullscreen mode")
|
||||
print(" • Modern dark theme")
|
||||
print(" • Smooth 20 FPS UI updates")
|
||||
|
||||
sys.exit(app.exec())
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
1597
video_editor_pyqt6.py
Normal file
1597
video_editor_pyqt6.py
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user