Compare commits

...
12 Commits
Author SHA1 Message Date
klop51 19522e8538 Merge branch 'main' of https://git.ptbox.org/klop51/ShortGenerator 2025-08-16 15:33:22 +02:00
klop51 814ffb9a8a chore: Remove unused video_editor_clean.py file 2025-08-16 15:33:10 +02:00
klop51 6c50257f04 Delete video_editor_clean.py 2025-08-16 13:31:30 +00:00
klop51 ab06f8f7de deleted 2025-08-16 15:06:12 +02:00
klop51 7f6b7b4901 Enhance video widget scaling and export functionality
- Improved video widget scaling to fit within its container while maintaining aspect ratio.
- Added dynamic resizing of the video widget on window resize events.
- Implemented a separate thread for video export to prevent UI freezing and added progress tracking.
- Enhanced export process to include timeline clips as overlays on the main video.
- Updated export completion handling with user feedback and error reporting.
- Adjusted layout and styling for better user experience.
2025-08-16 15:04:09 +02:00
klop51 9e4a243daa Delete video_editor (3).py 2025-08-16 12:56:30 +00:00
klop51 bc04aba0d7 feat: Implement drag-and-drop functionality for media items in the timeline and media bin 2025-08-16 00:36:15 +02:00
klop51 82082911b3 Refactor code structure for improved readability and maintainability 2025-08-15 22:34:19 +02:00
klop51 8f64345335 feat: Enhance video editing effects with real-time controls and fullscreen support
- Added ripple effect with adjustable intensity and frequency, including time controls for start and end.
- Implemented fade effect with time-based controls for smoother transitions.
- Introduced text overlay functionality with time controls for displaying text during specific intervals.
- Created a fullscreen mode for video playback, allowing users to toggle fullscreen and exit with ESC or F11.
- Added keyboard shortcuts for playback control, frame navigation, and seeking.
- Improved UI with clear buttons for managing effects and enhanced interaction for clip resizing and moving.
- Updated effect application methods to support real-time preview during editing.
2025-08-12 22:32:27 +02:00
klop51 c9fc7c8d80 feat: Enhance ShortsEditorGUI with media bin for drag-and-drop functionality and timeline clip management 2025-08-12 19:19:24 +02:00
klop51 d589700d7a fix: resolve merge conflicts and improve Audio 2 visibility 2025-08-12 18:18:19 +02:00
klop51 76b89dc412 Refactor code structure for improved readability and maintainability 2025-08-12 18:10:28 +02:00
5 changed files with 5755 additions and 187 deletions
+489
View 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()
+1382 -176
View File
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