Compare commits
10
Commits
d589700d7a
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
19522e8538 | ||
|
|
814ffb9a8a | ||
|
|
6c50257f04 | ||
|
|
ab06f8f7de | ||
|
|
7f6b7b4901 | ||
|
|
9e4a243daa | ||
|
|
bc04aba0d7 | ||
|
|
82082911b3 | ||
|
|
8f64345335 | ||
|
|
c9fc7c8d80 |
@@ -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()
|
||||||
-2165
File diff suppressed because it is too large
Load Diff
+1371
-168
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user