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