ShortGenerator/pyqt6_video_player.py

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()