490 lines
19 KiB
Python
490 lines
19 KiB
Python
"""
|
|
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()
|