ShortGenerator/video_editor_pyqt6.py
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

2443 lines
98 KiB
Python

"""
Professional Video Editor for Generated Shorts - PyQt6 Version
Author: Dario Pascoal
Description: This is a comprehensive video editing application designed specifically for editing
short-form video content, fully migrated to PyQt6 for enhanced performance and professional features.
The application provides a professional timeline-based interface similar to industry-standard
video editing software, with features including:
- Multi-track timeline with visual track roads for professional editing workflow
- Hardware-accelerated video preview with frame-accurate scrubbing
- Professional editing tools: trim, speed adjustment, volume control, fade effects
- Real-time effects system with ripple, fade, and text overlays
- Text overlay capabilities with customizable styling
- Export functionality with multiple format support
- Tabbed interface organizing tools into logical categories
- Dark theme optimized for video editing work
- Support for multiple video formats (MP4, AVI, MOV, MKV, etc.)
- Full keyboard control system (Space, arrows, F11, etc.)
- Professional fullscreen mode with all effects
PyQt6 Advantages:
- Hardware-accelerated video playback with QMediaPlayer
- OpenGL timeline rendering for smooth performance
- Professional UI components and styling
- Better threading and signal/slot architecture
- Native multimedia framework integration
- GPU-accelerated effects pipeline
- Professional dock system for panels
- Modern styling with CSS-like stylesheets
Technical Architecture:
- Uses QMediaPlayer for hardware-accelerated video playback
- Implements QGraphicsView-based timeline with precise time calculations
- Signal/slot pattern for clean event handling
- QThread-based background processing
- Hardware-accelerated effects pipeline ready for expansion
- Maintains professional video editing workflow patterns
"""
import sys
import os
import threading
import time
from datetime import datetime
import cv2
import numpy as np
from PIL import Image
from PyQt6.QtWidgets import (
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QGridLayout,
QSlider, QPushButton, QLabel, QFrame, QTabWidget, QScrollArea, QSplitter,
QFileDialog, QMessageBox, QSpacerItem, QSizePolicy, QLineEdit, QSpinBox,
QDoubleSpinBox, QComboBox, QCheckBox, QProgressBar, QTextEdit, QGroupBox,
QToolButton, QButtonGroup, QDockWidget, QStyleFactory, QStackedWidget, QStackedLayout
)
from PyQt6.QtCore import (
Qt, QUrl, QTimer, pyqtSignal, QPropertyAnimation, QEasingCurve, QRect,
QThread, pyqtSlot, QObject, QRunnable, QThreadPool, QMutex, QSize, QEvent, QMimeData, QPoint,
QMetaObject, Q_ARG
)
from PyQt6.QtMultimedia import QMediaPlayer, QAudioOutput
from PyQt6.QtMultimediaWidgets import QVideoWidget
from PyQt6.QtGui import (
QPalette, QColor, QFont, QIcon, QKeySequence, QShortcut, QPixmap, QPainter,
QBrush, QPen, QPolygon, QAction, QCursor, QDrag
)
# Try to import MoviePy, handle if not available
try:
from moviepy.editor import VideoFileClip, TextClip, CompositeVideoClip
from moviepy.video.fx import FadeIn, FadeOut, Resize
from moviepy.audio.fx import MultiplyVolume
MOVIEPY_AVAILABLE = True
except ImportError:
print("⚠️ MoviePy not available - using OpenCV backend for video processing")
MOVIEPY_AVAILABLE = False
class ProfessionalSlider(QSlider):
"""Custom slider with professional video editing styling"""
def __init__(self, orientation=Qt.Orientation.Horizontal):
super().__init__(orientation)
self.setStyleSheet("""
QSlider::groove:horizontal {
background: #2d2d2d;
height: 6px;
border-radius: 3px;
border: 1px solid #404040;
}
QSlider::handle:horizontal {
background: qlineargradient(x1:0, y1:0, x2:1, y2:1,
stop:0 #00aaff, stop:1 #0088cc);
border: 2px solid #ffffff;
width: 16px;
height: 16px;
margin: -6px 0;
border-radius: 8px;
}
QSlider::handle:horizontal:hover {
background: qlineargradient(x1:0, y1:0, x2:1, y2:1,
stop:0 #00ccff, stop:1 #00aadd);
border: 2px solid #ffffff;
}
QSlider::handle:horizontal:pressed {
background: #ffffff;
border: 2px solid #00aaff;
}
QSlider::sub-page:horizontal {
background: qlineargradient(x1:0, y1:0, x2:1, y2:1,
stop:0 #00aaff, stop:1 #0077bb);
border-radius: 3px;
}
QSlider::add-page:horizontal {
background: #404040;
border-radius: 3px;
}
""")
class TimelineWidget(QWidget):
"""Professional timeline widget for video editing"""
positionChanged = pyqtSignal(float) # Emit time position changes
def __init__(self):
super().__init__()
self.duration = 30.0 # Start with 30 seconds default duration
self.current_time = 0.0
self.zoom_level = 1.0
self.track_height = 70 # Adjusted for 5 tracks to fit properly
self.ruler_height = 30 # Increased ruler height
self.timeline_clips = [] # Store clips on timeline
self.selected_clip_index = None # Track selected clip
self.setAcceptDrops(True) # Enable drag and drop
self.setup_ui()
def setup_ui(self):
"""Setup timeline interface"""
self.setMinimumHeight(400) # Much larger for proper track visibility
self.setFocusPolicy(Qt.FocusPolicy.ClickFocus) # Allow keyboard focus
self.setAttribute(Qt.WidgetAttribute.WA_AcceptTouchEvents, False) # Disable touch to avoid conflicts
self.setMouseTracking(False) # Ensure we only get mouse press events
print(f"🔧 Timeline widget setup: size policy, focus policy, mouse tracking configured")
self.setStyleSheet("""
TimelineWidget {
background-color: #1e1e1e;
border: 1px solid #404040;
}
""")
def paintEvent(self, event):
"""Custom paint event for timeline rendering"""
painter = QPainter(self)
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
# Draw timeline background
painter.fillRect(self.rect(), QColor("#1e1e1e"))
# Draw time ruler first
self.draw_time_ruler(painter)
# Draw track areas with visual representations like original Tkinter version
track_count = 5 # Video 1, Video 2, Video 3, Audio 1, Audio 2
track_names = ["🎬 Video 1", "🎬 Video 2", "🎬 Video 3", "🎵 Audio 1", "🎵 Audio 2"]
track_colors = ["#3498db", "#2ecc71", "#9b59b6", "#e74c3c", "#f39c12"]
# Start tracks below the ruler
ruler_bottom = self.ruler_height
for i in range(track_count):
y = ruler_bottom + (i * self.track_height)
# Draw track background
track_rect = QRect(0, y, self.width(), self.track_height)
painter.fillRect(track_rect, QColor("#2d2d2d"))
# Draw track border
painter.setPen(QPen(QColor("#404040"), 1))
painter.drawRect(track_rect)
# Draw track content area (like original design)
content_rect = QRect(120, y + 10, self.width() - 130, self.track_height - 20)
painter.fillRect(content_rect, QColor("#1a1a1a"))
painter.setPen(QPen(QColor(track_colors[i]), 2))
painter.drawRect(content_rect)
# Draw actual timeline clips for this track
# Use effective timeline duration for consistent positioning
effective_duration = self.get_effective_timeline_duration()
content_width = content_rect.width()
pixels_per_second = content_width / effective_duration
# Draw clips on this track
for clip_index, clip in enumerate(self.timeline_clips):
if clip['track'] == i:
# Calculate clip position and size
clip_x = content_rect.x() + (clip['start_time'] * pixels_per_second)
clip_width = max(60, clip['duration'] * pixels_per_second) # Minimum width for visibility
clip_rect = QRect(int(clip_x), content_rect.y() + 3,
int(clip_width), content_rect.height() - 6)
# Determine if this clip is selected
is_selected = (self.selected_clip_index == clip_index)
# Make clips more visible with brighter colors and gradients
if clip['type'] == 'video':
# Bright blue for video clips
clip_color = QColor("#4A90E2") if not is_selected else QColor("#00AA00") # Green when selected
border_color = QColor("#2E5C8A") if not is_selected else QColor("#00FF00") # Bright green border when selected
else:
# Bright red for audio clips
clip_color = QColor("#E74C3C") if not is_selected else QColor("#FF6600") # Orange when selected
border_color = QColor("#C0392B") if not is_selected else QColor("#FF8800") # Bright orange border when selected
# Fill with clip color
painter.fillRect(clip_rect, clip_color)
# Add a subtle gradient effect
gradient_rect = QRect(clip_rect.x(), clip_rect.y(),
clip_rect.width(), clip_rect.height() // 3)
lighter_color = clip_color.lighter(130)
painter.fillRect(gradient_rect, lighter_color)
# Draw border - thicker for selected clips
border_width = 3 if is_selected else 2
painter.setPen(QPen(border_color, border_width))
painter.drawRect(clip_rect)
# Draw inner highlight - brighter for selected clips
highlight_color = QColor("#ffffff") if not is_selected else QColor("#ffff00")
painter.setPen(QPen(highlight_color, 1))
inner_rect = QRect(clip_rect.x() + 1, clip_rect.y() + 1,
clip_rect.width() - 2, clip_rect.height() - 2)
painter.drawRect(inner_rect)
# Draw selection indicator for selected clips
if is_selected:
# Draw selection corners
corner_size = 8
painter.setPen(QPen(QColor("#ffff00"), 2))
painter.setBrush(QColor("#ffff00"))
# Top-left corner
painter.drawRect(clip_rect.x() - 2, clip_rect.y() - 2, corner_size, corner_size)
# Top-right corner
painter.drawRect(clip_rect.x() + clip_rect.width() - corner_size + 2, clip_rect.y() - 2, corner_size, corner_size)
# Bottom-left corner
painter.drawRect(clip_rect.x() - 2, clip_rect.y() + clip_rect.height() - corner_size + 2, corner_size, corner_size)
# Bottom-right corner
painter.drawRect(clip_rect.x() + clip_rect.width() - corner_size + 2, clip_rect.y() + clip_rect.height() - corner_size + 2, corner_size, corner_size)
# Draw clip filename with better contrast
painter.setFont(QFont("Arial", 9, QFont.Weight.Bold))
text_color = QColor("#ffffff") if not is_selected else QColor("#000000") # Black text on selected clips
painter.setPen(QPen(text_color, 1))
# Add text shadow for better readability (only for non-selected clips)
if not is_selected:
shadow_rect = QRect(clip_rect.x() + 3, clip_rect.y() + 1,
clip_rect.width() - 6, clip_rect.height())
painter.setPen(QPen(QColor("#000000"), 1))
painter.drawText(shadow_rect, Qt.AlignmentFlag.AlignCenter,
clip['filename'][:12] + "..." if len(clip['filename']) > 12 else clip['filename'])
# Draw actual text
text_rect = QRect(clip_rect.x() + 2, clip_rect.y(),
clip_rect.width() - 4, clip_rect.height())
painter.setPen(QPen(text_color, 1))
painter.drawText(text_rect, Qt.AlignmentFlag.AlignCenter,
clip['filename'][:12] + "..." if len(clip['filename']) > 12 else clip['filename'])
# If no clips on this track, draw sample content block for visual reference
if not any(clip['track'] == i for clip in self.timeline_clips):
block_width = min(200, content_width // 4)
block_rect = QRect(content_rect.x() + 10, content_rect.y() + 5,
block_width, content_rect.height() - 10)
# Fill with track color (dimmed)
dim_color = QColor(track_colors[i])
dim_color.setAlpha(80)
painter.fillRect(block_rect, dim_color)
painter.setPen(QPen(QColor("#666666"), 1))
painter.drawRect(block_rect)
# Draw track name on the sample block
painter.setFont(QFont("Arial", 9))
painter.setPen(QPen(QColor("#999999"), 1))
text_rect = QRect(block_rect.x() + 5, block_rect.y(),
block_rect.width() - 10, block_rect.height())
painter.drawText(text_rect, Qt.AlignmentFlag.AlignCenter, track_names[i])
# Draw track labels on the left
label_rect = QRect(10, y, 100, self.track_height)
painter.setFont(QFont("Arial", 11, QFont.Weight.Bold))
painter.setPen(QPen(QColor("#ffffff"), 1))
painter.drawText(label_rect, Qt.AlignmentFlag.AlignCenter, track_names[i])
# Draw separation line between ruler and tracks
painter.setPen(QPen(QColor("#666666"), 2))
painter.drawLine(0, ruler_bottom, self.width(), ruler_bottom)
# Draw playhead on top of everything
effective_duration = self.get_effective_timeline_duration()
if effective_duration > 0:
# Use current video time directly since timeline now accommodates video duration
timeline_position = self.current_time
playhead_x = 120 + ((timeline_position / effective_duration) * (self.width() - 130))
# Only draw playhead if it's within the visible timeline area
if 120 <= playhead_x <= self.width() - 20:
painter.setPen(QPen(QColor("#ff4444"), 3))
painter.drawLine(int(playhead_x), ruler_bottom, int(playhead_x), self.height())
# Draw playhead handle
handle_rect = QRect(int(playhead_x) - 8, ruler_bottom - 15, 16, 15)
painter.fillRect(handle_rect, QColor("#ff4444"))
painter.setPen(QPen(QColor("#ffffff"), 1))
painter.drawRect(handle_rect)
def draw_time_ruler(self, painter):
"""Draw time ruler at the top using effective timeline duration with smart scaling"""
painter.fillRect(0, 0, self.width(), self.ruler_height, QColor("#2d2d2d"))
effective_duration = self.get_effective_timeline_duration()
if effective_duration > 0:
# Calculate time intervals based on effective timeline
content_area_start = 120
content_area_width = self.width() - 130
pixels_per_second = content_area_width / effective_duration
painter.setPen(QPen(QColor("#cccccc"), 1))
painter.setFont(QFont("Arial", 9))
# Smart interval calculation based on timeline duration and available space
if effective_duration <= 300: # Under 5 minute
major_interval = 10 # Every 10 seconds
minor_interval = 1 # Minor marks every 1 seconds
elif effective_duration <= 1800: # Under 30 minutes
major_interval = 120 # Every 2 minutes
minor_interval = 60 # Minor marks every minute
else: # Over 30 minutes
major_interval = 300 # Every 5 minutes
minor_interval = 60 # Minor marks every 1 minutes
# Draw major time markers with labels
current_time = 0
while current_time <= effective_duration:
x = content_area_start + (current_time * pixels_per_second)
if x <= self.width() - 10:
# Draw major marker line
painter.setPen(QPen(QColor("#cccccc"), 2))
painter.drawLine(int(x), 0, int(x), self.ruler_height - 5)
# Format and draw time label
if current_time >= 60:
minutes = int(current_time // 60)
seconds = int(current_time % 60)
time_text = f"{minutes}:{seconds:02d}"
else:
time_text = f"{int(current_time)}s"
painter.setPen(QPen(QColor("#ffffff"), 1))
painter.drawText(int(x) + 2, self.ruler_height - 12, time_text)
current_time += major_interval
# Draw minor time markers (no labels)
if minor_interval != major_interval:
painter.setPen(QPen(QColor("#888888"), 1))
current_time = 0
while current_time <= effective_duration:
if current_time % major_interval != 0: # Skip major intervals
x = content_area_start + (current_time * pixels_per_second)
if x <= self.width() - 10:
painter.drawLine(int(x), self.ruler_height - 12, int(x), self.ruler_height - 5)
current_time += minor_interval
# Draw ruler border
painter.setPen(QPen(QColor("#404040"), 1))
painter.drawRect(0, 0, self.width(), self.ruler_height)
def mousePressEvent(self, event):
"""Handle timeline clicking for seeking and clip selection"""
print(f"🖱️ Timeline mousePressEvent called: button={event.button()}, pos=({event.position().x():.1f}, {event.position().y():.1f})")
if event.button() == Qt.MouseButton.LeftButton:
# Only allow operations in the content area (not on track labels)
content_area_start = 120
click_x = event.position().x()
click_y = event.position().y()
if click_x >= content_area_start:
# Check if CTRL is pressed for clip selection
ctrl_pressed = event.modifiers() & Qt.KeyboardModifier.ControlModifier
if ctrl_pressed:
# CTRL+Click: Select/deselect clip without moving playhead
clicked_clip_index = self.get_clip_at_position(click_x, click_y)
if clicked_clip_index is not None:
# CTRL+Click on clip: select it
self.selected_clip_index = clicked_clip_index
clip = self.timeline_clips[clicked_clip_index]
print(f"🎯 CTRL+Click: Selected clip: {clip['filename']} on track {clip['track']}")
self.update()
else:
# CTRL+Click on empty area: just deselect
if self.selected_clip_index is not None:
print("🎯 CTRL+Click: Deselected all clips (playhead stays)")
self.selected_clip_index = None
self.update()
else:
print("🎯 CTRL+Click: No clips to deselect (playhead stays)")
# Do NOT move playhead when CTRL is pressed
else:
# Regular click without CTRL
clicked_clip_index = self.get_clip_at_position(click_x, click_y)
if clicked_clip_index is not None:
# Click on clip: select it (no playhead movement)
self.selected_clip_index = clicked_clip_index
clip = self.timeline_clips[clicked_clip_index]
print(f"🎯 Selected clip: {clip['filename']} on track {clip['track']}")
self.update()
else:
# Click on empty area: deselect clips and move playhead
# First handle deselection
if self.selected_clip_index is not None:
print("🎯 Deselected all clips")
self.selected_clip_index = None
# Then move playhead using effective timeline duration
effective_duration = self.get_effective_timeline_duration()
if effective_duration > 0:
content_width = self.width() - 130
click_x_relative = click_x - content_area_start
click_time = (click_x_relative / content_width) * effective_duration
self.current_time = max(0, min(click_time, self.duration))
print(f"⏭️ Playhead moved to {self.current_time:.2f}s")
self.positionChanged.emit(self.current_time)
self.update()
else:
print(f"🖱️ Click outside content area (x={click_x:.1f}, content_start={content_area_start})")
else:
print(f"🖱️ Non-left button click: {event.button()}")
def get_effective_timeline_duration(self):
"""Calculate the effective timeline duration for display and interaction
This uses a dynamic timeline approach that expands to accommodate
both clips and the loaded video duration.
"""
# Start with a minimum timeline duration
min_duration = 30.0
# Find the latest clip end time
latest_clip_time = 0
if self.timeline_clips:
for clip in self.timeline_clips:
clip_end = clip['start_time'] + clip['duration']
latest_clip_time = max(latest_clip_time, clip_end)
# Consider the video duration if a video is loaded
video_duration = getattr(self, 'duration', 0)
# Use the maximum of:
# 1. Minimum duration (30s)
# 2. Latest clip time + padding
# 3. Current video duration (if reasonable)
timeline_duration = max(
min_duration,
latest_clip_time + 10.0, # Add 10s padding after clips
min(video_duration, 3600) # Cap at 1 hour for UI performance
)
return timeline_duration
def get_clip_at_position(self, click_x, click_y):
"""Get clip index at the clicked position using effective timeline scaling"""
if not self.timeline_clips:
return None
content_area_start = 120
content_width = self.width() - 130
# Use effective timeline duration instead of video duration
effective_duration = self.get_effective_timeline_duration()
pixels_per_second = content_width / effective_duration
# Convert click position to time
click_x_relative = click_x - content_area_start
clicked_time = click_x_relative / pixels_per_second
# Get track from Y position
track_index = self.get_track_from_y(click_y)
if track_index < 0:
return None
# Find clip at this position
for i, clip in enumerate(self.timeline_clips):
if clip['track'] == track_index:
clip_start = clip['start_time']
clip_end = clip_start + clip['duration']
if clip_start <= clicked_time <= clip_end:
return i
return None
def select_clip_at_position(self, click_x, click_y):
"""Select clip at position without affecting playhead"""
clicked_clip_index = self.get_clip_at_position(click_x, click_y)
if clicked_clip_index is not None:
self.selected_clip_index = clicked_clip_index
clip = self.timeline_clips[clicked_clip_index]
print(f"🎯 Selected clip: {clip['filename']} on track {clip['track']}")
else:
self.selected_clip_index = None
print("🎯 Deselected all clips")
self.update()
def keyPressEvent(self, event):
"""Handle keyboard events for clip operations"""
if event.key() == Qt.Key.Key_Delete or event.key() == Qt.Key.Key_Backspace:
# Delete selected clip
self.delete_selected_clip()
else:
super().keyPressEvent(event)
def delete_selected_clip(self):
"""Delete the currently selected clip"""
if (self.selected_clip_index is not None and
0 <= self.selected_clip_index < len(self.timeline_clips)):
deleted_clip = self.timeline_clips[self.selected_clip_index]
del self.timeline_clips[self.selected_clip_index]
print(f"🗑️ Deleted clip: {deleted_clip['filename']}")
print(f"📊 Timeline now has {len(self.timeline_clips)} clip(s)")
# Clear selection
self.selected_clip_index = None
# Update display
self.update()
def set_duration(self, duration):
"""Set timeline duration"""
self.duration = duration
self.update()
def set_position(self, position):
"""Set current playhead position"""
self.current_time = position
self.update()
def dragEnterEvent(self, event):
"""Handle drag enter event - check if it's a media item"""
if event.mimeData().hasFormat("application/x-media-item"):
event.acceptProposedAction()
# Visual feedback
self.setStyleSheet("""
TimelineWidget {
background-color: #2a3a2a;
border: 2px dashed #00aa00;
}
""")
else:
event.ignore()
def dragMoveEvent(self, event):
"""Handle drag move event"""
if event.mimeData().hasFormat("application/x-media-item"):
event.acceptProposedAction()
else:
event.ignore()
def dragLeaveEvent(self, event):
"""Handle drag leave event - restore normal appearance"""
self.setStyleSheet("""
TimelineWidget {
background-color: #1e1e1e;
border: 1px solid #404040;
}
""")
def dropEvent(self, event):
"""Handle drop event - add media to timeline"""
# Restore normal appearance
self.setStyleSheet("""
TimelineWidget {
background-color: #1e1e1e;
border: 1px solid #404040;
}
""")
if event.mimeData().hasFormat("application/x-media-item"):
# Get file path from mime data
file_path = event.mimeData().data("application/x-media-item").data().decode()
# Calculate drop position
drop_x = event.position().x()
drop_y = event.position().y()
# Check if dropped in valid area (content area)
content_area_start = 120
if drop_x >= content_area_start:
# If timeline has no duration, set a default duration of 30 seconds
if self.duration <= 0:
self.duration = 30.0
# Calculate time position
content_width = self.width() - 130
click_x = drop_x - content_area_start
time_position = (click_x / content_width) * self.duration
# Determine which track based on Y position
track_index = self.get_track_from_y(drop_y)
if track_index >= 0:
# Create clip info
clip_info = {
'file_path': file_path,
'filename': os.path.basename(file_path),
'start_time': max(0, time_position),
'duration': 3.0, # Default duration
'track': track_index,
'type': 'video' if self.is_video_file(file_path) else 'audio'
}
# Add to timeline clips
self.timeline_clips.append(clip_info)
self.update()
# Visual and audio feedback for successful drop
print(f"✅ Added {os.path.basename(file_path)} to track {track_index} at {time_position:.1f}s")
print(f"📊 Timeline now has {len(self.timeline_clips)} clip(s)")
# Trigger a repaint to immediately show the new clip
QApplication.processEvents()
event.acceptProposedAction()
else:
print("❌ Invalid track area")
event.ignore()
else:
print("❌ Dropped outside content area")
event.ignore()
else:
event.ignore()
def get_track_from_y(self, y_pos):
"""Get track index from Y position"""
ruler_bottom = self.ruler_height
if y_pos < ruler_bottom:
return -1 # Dropped on ruler
track_y = y_pos - ruler_bottom
track_index = int(track_y // self.track_height)
# Validate track index (5 tracks: 0-4)
if 0 <= track_index < 5:
return track_index
return -1
def is_video_file(self, file_path):
"""Check if file is a video file"""
video_extensions = ['.mp4', '.avi', '.mov', '.mkv', '.wmv', '.flv', '.webm']
ext = os.path.splitext(file_path)[1].lower()
return ext in video_extensions
class EffectsPanel(QWidget):
"""Professional effects panel with time-based controls"""
effectApplied = pyqtSignal(str, dict) # effect_type, parameters
def __init__(self):
super().__init__()
self.setup_ui()
def setup_ui(self):
"""Setup effects panel interface"""
layout = QVBoxLayout(self)
# Ripple Effect
ripple_group = QGroupBox("🌊 Ripple Effect")
ripple_group.setStyleSheet("""
QGroupBox {
font-weight: bold;
border: 2px solid #404040;
border-radius: 5px;
margin-top: 10px;
color: #ffffff;
}
QGroupBox::title {
subcontrol-origin: margin;
left: 10px;
padding: 0 5px 0 5px;
}
""")
ripple_layout = QVBoxLayout(ripple_group)
# Time controls
time_layout = QHBoxLayout()
time_layout.addWidget(QLabel("Start:"))
self.ripple_start = QDoubleSpinBox()
self.ripple_start.setRange(0.0, 999.0)
self.ripple_start.setSingleStep(0.1)
self.ripple_start.setSuffix("s")
time_layout.addWidget(self.ripple_start)
time_layout.addWidget(QLabel("End:"))
self.ripple_end = QDoubleSpinBox()
self.ripple_end.setRange(0.0, 999.0)
self.ripple_end.setSingleStep(0.1)
self.ripple_end.setValue(2.0)
self.ripple_end.setSuffix("s")
time_layout.addWidget(self.ripple_end)
ripple_layout.addLayout(time_layout)
# Intensity control
intensity_layout = QHBoxLayout()
intensity_layout.addWidget(QLabel("Intensity:"))
self.ripple_intensity = QSpinBox()
self.ripple_intensity.setRange(1, 50)
self.ripple_intensity.setValue(10)
intensity_layout.addWidget(self.ripple_intensity)
ripple_layout.addLayout(intensity_layout)
# Apply button
apply_ripple_btn = QPushButton("Apply Ripple Effect")
apply_ripple_btn.clicked.connect(self.apply_ripple_effect)
apply_ripple_btn.setStyleSheet("""
QPushButton {
background-color: #0066cc;
color: white;
border: none;
padding: 8px;
border-radius: 4px;
font-weight: bold;
}
QPushButton:hover {
background-color: #0088ff;
}
""")
ripple_layout.addWidget(apply_ripple_btn)
layout.addWidget(ripple_group)
# Fade Effect
fade_group = QGroupBox("🌅 Fade Effect")
fade_group.setStyleSheet(ripple_group.styleSheet())
fade_layout = QVBoxLayout(fade_group)
# Fade time controls
fade_time_layout = QHBoxLayout()
fade_time_layout.addWidget(QLabel("Start:"))
self.fade_start = QDoubleSpinBox()
self.fade_start.setRange(0.0, 999.0)
self.fade_start.setSingleStep(0.1)
self.fade_start.setSuffix("s")
fade_time_layout.addWidget(self.fade_start)
fade_time_layout.addWidget(QLabel("End:"))
self.fade_end = QDoubleSpinBox()
self.fade_end.setRange(0.0, 999.0)
self.fade_end.setSingleStep(0.1)
self.fade_end.setValue(3.0)
self.fade_end.setSuffix("s")
fade_time_layout.addWidget(self.fade_end)
fade_layout.addLayout(fade_time_layout)
# Apply button
apply_fade_btn = QPushButton("Apply Fade Effect")
apply_fade_btn.clicked.connect(self.apply_fade_effect)
apply_fade_btn.setStyleSheet(apply_ripple_btn.styleSheet())
fade_layout.addWidget(apply_fade_btn)
layout.addWidget(fade_group)
# Text Effect
text_group = QGroupBox("📝 Text Overlay")
text_group.setStyleSheet(ripple_group.styleSheet())
text_layout = QVBoxLayout(text_group)
# Text input
self.text_input = QLineEdit()
self.text_input.setPlaceholderText("Enter text to overlay...")
self.text_input.setText("Sample Text")
text_layout.addWidget(self.text_input)
# Text time controls
text_time_layout = QHBoxLayout()
text_time_layout.addWidget(QLabel("Start:"))
self.text_start = QDoubleSpinBox()
self.text_start.setRange(0.0, 999.0)
self.text_start.setSingleStep(0.1)
self.text_start.setSuffix("s")
text_time_layout.addWidget(self.text_start)
text_time_layout.addWidget(QLabel("End:"))
self.text_end = QDoubleSpinBox()
self.text_end.setRange(0.0, 999.0)
self.text_end.setSingleStep(0.1)
self.text_end.setValue(4.0)
self.text_end.setSuffix("s")
text_time_layout.addWidget(self.text_end)
text_layout.addLayout(text_time_layout)
# Apply button
apply_text_btn = QPushButton("Apply Text Overlay")
apply_text_btn.clicked.connect(self.apply_text_effect)
apply_text_btn.setStyleSheet(apply_ripple_btn.styleSheet())
text_layout.addWidget(apply_text_btn)
layout.addWidget(text_group)
# Add stretch to push everything to top
layout.addStretch()
def apply_ripple_effect(self):
"""Apply ripple effect with current parameters"""
params = {
'start_time': self.ripple_start.value(),
'end_time': self.ripple_end.value(),
'intensity': self.ripple_intensity.value()
}
self.effectApplied.emit('ripple', params)
def apply_fade_effect(self):
"""Apply fade effect with current parameters"""
params = {
'start_time': self.fade_start.value(),
'end_time': self.fade_end.value()
}
self.effectApplied.emit('fade', params)
def apply_text_effect(self):
"""Apply text overlay with current parameters"""
params = {
'start_time': self.text_start.value(),
'end_time': self.text_end.value(),
'text': self.text_input.text()
}
self.effectApplied.emit('text', params)
class DraggableMediaItem(QPushButton):
"""Draggable media item that can be dragged to timeline"""
mediaSelected = pyqtSignal(str) # file_path
def __init__(self, file_path, parent=None):
super().__init__(f"🎬 {os.path.basename(file_path)}", parent)
self.file_path = file_path
self.drag_start_position = QPoint()
self.dragging = False
# Store media info for drag operations
self.media_info = {
'path': file_path,
'filename': os.path.basename(file_path),
'type': 'video' if self.is_video_file(file_path) else 'audio'
}
self.setStyleSheet("""
QPushButton {
text-align: left;
padding: 4px 6px;
margin: 0px;
border: 1px solid #404040;
border-radius: 3px;
background-color: #2d2d2d;
color: #ffffff;
min-height: 18px;
max-height: 26px;
font-size: 12px;
}
QPushButton:hover {
background-color: #404040;
border: 1px solid #00aaff;
}
""")
self.clicked.connect(self.select_media)
def is_video_file(self, file_path):
"""Check if file is a video file"""
video_extensions = ['.mp4', '.avi', '.mov', '.mkv', '.wmv', '.flv', '.webm']
ext = os.path.splitext(file_path)[1].lower()
return ext in video_extensions
def select_media(self):
"""Emit signal for media selection"""
self.mediaSelected.emit(self.file_path)
print(f"🎬 Selected: {os.path.basename(self.file_path)}")
def mousePressEvent(self, event):
"""Handle mouse press - start potential drag operation"""
if event.button() == Qt.MouseButton.LeftButton:
self.drag_start_position = event.pos()
# Visual feedback for drag start
self.setStyleSheet("""
QPushButton {
text-align: left;
padding: 4px 6px;
margin: 0px;
border: 2px solid #00aaff;
border-radius: 3px;
background-color: #003366;
color: #ffffff;
min-height: 18px;
max-height: 26px;
font-size: 12px;
}
""")
print(f"🎬 Started dragging: {self.media_info['filename']}")
super().mousePressEvent(event)
def mouseMoveEvent(self, event):
"""Handle mouse move - perform drag operation"""
if not (event.buttons() & Qt.MouseButton.LeftButton):
return
if not self.dragging and (event.pos() - self.drag_start_position).manhattanLength() < QApplication.startDragDistance():
return
if not self.dragging:
self.dragging = True
# Start drag operation
drag = QDrag(self)
mimeData = QMimeData()
# Store media info in mime data
mimeData.setText(f"media_drag:{self.file_path}")
mimeData.setData("application/x-media-item", self.file_path.encode())
drag.setMimeData(mimeData)
# Create a more visible drag pixmap
pixmap = QPixmap(150, 40)
pixmap.fill(QColor(0, 170, 255, 200))
painter = QPainter(pixmap)
painter.setPen(QPen(QColor("#ffffff"), 2))
painter.setFont(QFont("Arial", 10, QFont.Weight.Bold))
painter.drawText(10, 25, f"🎬 {os.path.basename(self.file_path)[:12]}...")
painter.end()
drag.setPixmap(pixmap)
drag.setHotSpot(QPoint(75, 20))
# Execute drag
dropAction = drag.exec(Qt.DropAction.CopyAction)
# Reset dragging state
self.dragging = False
self.reset_appearance()
def mouseReleaseEvent(self, event):
"""Handle mouse release - end drag operation"""
self.reset_appearance()
super().mouseReleaseEvent(event)
def reset_appearance(self):
"""Reset button appearance to normal"""
self.setStyleSheet("""
QPushButton {
text-align: left;
padding: 4px 6px;
margin: 0px;
border: 1px solid #404040;
border-radius: 3px;
background-color: #2d2d2d;
color: #ffffff;
min-height: 18px;
max-height: 26px;
font-size: 12px;
}
QPushButton:hover {
background-color: #404040;
border: 1px solid #00aaff;
}
""")
class DropScrollArea(QScrollArea):
"""Custom scroll area that handles drag and drop events"""
def __init__(self, media_bin, parent=None):
super().__init__(parent)
self.media_bin = media_bin
self.setAcceptDrops(True)
def dragEnterEvent(self, event):
"""Forward drag enter event to media bin"""
self.media_bin.dragEnterEvent(event)
def dragMoveEvent(self, event):
"""Forward drag move event to media bin"""
self.media_bin.dragMoveEvent(event)
def dragLeaveEvent(self, event):
"""Forward drag leave event to media bin"""
self.media_bin.dragLeaveEvent(event)
def dropEvent(self, event):
"""Forward drop event to media bin"""
self.media_bin.dropEvent(event)
class MediaBin(QWidget):
"""Professional media bin for file management with drag and drop support"""
mediaSelected = pyqtSignal(str) # file_path
def __init__(self):
super().__init__()
self.media_files = []
# Enable drag and drop from Windows Explorer
self.setAcceptDrops(True)
self.setup_ui()
self.auto_load_media()
def setup_ui(self):
"""Setup media bin interface"""
layout = QVBoxLayout(self)
# Header
header = QLabel("📁 Media Bin")
header.setStyleSheet("""
QLabel {
color: #ffffff;
font-size: 14px;
font-weight: bold;
padding: 10px;
background-color: #2d2d2d;
border-radius: 4px;
}
""")
layout.addWidget(header)
# Add media button
add_btn = QPushButton("+ Add Media")
add_btn.clicked.connect(self.add_media_file)
add_btn.setStyleSheet("""
QPushButton {
background-color: #006600;
color: white;
border: none;
padding: 8px;
border-radius: 4px;
font-weight: bold;
}
QPushButton:hover {
background-color: #008800;
}
""")
layout.addWidget(add_btn)
# Media list
self.media_list = QWidget()
self.media_list_layout = QVBoxLayout(self.media_list)
# Minimize spacing between media items for ultra-compact layout
self.media_list_layout.setSpacing(1)
self.media_list_layout.setContentsMargins(2, 2, 2, 2)
# Add stretch at the end to push items to the top
self.media_list_layout.addStretch()
scroll_area = DropScrollArea(self)
scroll_area.setWidget(self.media_list)
scroll_area.setWidgetResizable(True)
# Store reference to handle events
self.scroll_area = scroll_area
scroll_area.setStyleSheet("""
QScrollArea {
border: 1px solid #404040;
border-radius: 4px;
background-color: #1e1e1e;
padding: 2px;
}
QScrollBar:vertical {
width: 12px;
background-color: #2d2d2d;
}
""")
layout.addWidget(scroll_area, 1)
# Auto-load existing media
self.auto_load_media()
def auto_load_media(self):
"""Auto-load media files from current directory"""
video_extensions = ['.mp4', '.avi', '.mov', '.mkv', '.wmv', '.flv']
for file in os.listdir('.'):
if any(file.lower().endswith(ext) for ext in video_extensions):
self.add_media_to_list(file)
def add_media_file(self):
"""Add media file through file dialog"""
file_path, _ = QFileDialog.getOpenFileName(
self,
"Select Media File",
"",
"Video Files (*.mp4 *.avi *.mov *.mkv *.wmv *.flv *.webm);;All Files (*)"
)
if file_path:
self.add_media_to_list(file_path)
def add_media_to_list(self, file_path):
"""Add media file to the list"""
if file_path not in self.media_files:
self.media_files.append(file_path)
# Create draggable media item widget
media_item = DraggableMediaItem(file_path)
media_item.mediaSelected.connect(self.handle_media_selected)
# Insert before the stretch to keep items at the top
self.media_list_layout.insertWidget(self.media_list_layout.count() - 1, media_item)
print(f"📁 Added to media bin: {os.path.basename(file_path)}")
def handle_media_selected(self, file_path):
"""Handle media selection from draggable item"""
self.mediaSelected.emit(file_path)
def select_media(self, file_path):
"""Select media file"""
self.mediaSelected.emit(file_path)
print(f"🎬 Selected: {os.path.basename(file_path)}")
def dragEnterEvent(self, event):
"""Handle drag enter event - check if files are being dragged"""
if event.mimeData().hasUrls():
# Check if any of the URLs are video/audio files
for url in event.mimeData().urls():
if url.isLocalFile():
file_path = url.toLocalFile()
ext = os.path.splitext(file_path)[1].lower()
video_extensions = ['.mp4', '.avi', '.mov', '.mkv', '.wmv', '.flv', '.webm',
'.mp3', '.wav', '.aac', '.ogg', '.m4a']
if ext in video_extensions:
event.acceptProposedAction()
# Visual feedback - change scroll area background color
self.scroll_area.setStyleSheet("""
QScrollArea {
border: 2px dashed #00aa00;
border-radius: 4px;
background-color: #2a4a2a;
padding: 2px;
}
QScrollBar:vertical {
width: 12px;
background-color: #2d2d2d;
}
""")
return
event.ignore()
def dragMoveEvent(self, event):
"""Handle drag move event"""
if event.mimeData().hasUrls():
event.acceptProposedAction()
else:
event.ignore()
def dragLeaveEvent(self, event):
"""Handle drag leave event - restore normal appearance"""
self.scroll_area.setStyleSheet("""
QScrollArea {
border: 1px solid #404040;
border-radius: 4px;
background-color: #1e1e1e;
padding: 2px;
}
QScrollBar:vertical {
width: 12px;
background-color: #2d2d2d;
}
""")
def dropEvent(self, event):
"""Handle drop event - add dropped files to media bin"""
# Restore normal appearance
self.scroll_area.setStyleSheet("""
QScrollArea {
border: 1px solid #404040;
border-radius: 4px;
background-color: #1e1e1e;
padding: 2px;
}
QScrollBar:vertical {
width: 12px;
background-color: #2d2d2d;
}
""")
if event.mimeData().hasUrls():
for url in event.mimeData().urls():
if url.isLocalFile():
file_path = url.toLocalFile()
ext = os.path.splitext(file_path)[1].lower()
video_extensions = ['.mp4', '.avi', '.mov', '.mkv', '.wmv', '.flv', '.webm',
'.mp3', '.wav', '.aac', '.ogg', '.m4a']
if ext in video_extensions:
self.add_media_to_list(file_path)
print(f"🎯 Dropped file: {os.path.basename(file_path)}")
event.acceptProposedAction()
else:
event.ignore()
class ProfessionalVideoEditor(QMainWindow):
"""Professional video editor with PyQt6"""
def __init__(self):
super().__init__()
# Video state
self.current_video = None
self.current_time = 0.0
self.video_duration = 0.0
self.is_playing = False
self.current_frame = None
# Effects state
self.effects_enabled = {
'ripple': False,
'fade': False,
'text': False
}
self.effect_times = {
'ripple': {'start': 0.0, 'end': 2.0},
'fade': {'start': 0.0, 'end': 3.0},
'text': {'start': 0.0, 'end': 4.0}
}
self.effect_params = {}
# Fullscreen state
self.is_fullscreen = False
self.fullscreen_window = None
self.setup_ui()
self.setup_media_player()
self.setup_shortcuts()
self.setup_styling()
def setup_ui(self):
"""Setup the main professional video editor interface"""
self.setWindowTitle("Professional Video Editor - PyQt6")
self.setGeometry(100, 100, 1400, 900)
self.setMinimumSize(1000, 700)
# Central widget with splitter layout
central_widget = QWidget()
self.setCentralWidget(central_widget)
main_layout = QHBoxLayout(central_widget)
main_layout.setContentsMargins(5, 5, 5, 5)
# Main splitter
main_splitter = QSplitter(Qt.Orientation.Horizontal)
main_layout.addWidget(main_splitter)
# Left panel (Media Bin)
left_panel = QWidget()
left_panel.setFixedWidth(250)
left_layout = QVBoxLayout(left_panel)
self.media_bin = MediaBin()
self.media_bin.mediaSelected.connect(self.load_video_file)
left_layout.addWidget(self.media_bin)
main_splitter.addWidget(left_panel)
# Center panel (Video Player + Timeline)
center_panel = QWidget()
center_layout = QVBoxLayout(center_panel)
# Video player area
video_frame = QFrame()
video_frame.setMinimumHeight(480) # 400 for video + 60 for controls + some padding
video_frame.setStyleSheet("""
QFrame {
background-color: #2d2d2d;
border: 2px solid #404040;
border-radius: 8px;
}
""")
video_layout = QVBoxLayout(video_frame)
video_layout.setContentsMargins(5, 5, 5, 5) # Add some padding
# Adaptive video container that properly constrains video scaling
video_container = QWidget()
video_container.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
video_container.setStyleSheet("""
QWidget {
background-color: #2d2d2d;
border: 2px solid #555555;
border-radius: 8px;
}
""")
video_container_layout = QVBoxLayout(video_container)
video_container_layout.setContentsMargins(5, 5, 5, 5) # Small padding to ensure video stays inside
video_container_layout.setSpacing(0)
# Create video widget with proper container-based scaling
self.video_widget = QVideoWidget()
# Set the video widget to scale with its container
self.video_widget.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
# Keep aspect ratio and fit within container bounds (won't overflow)
self.video_widget.setAspectRatioMode(Qt.AspectRatioMode.KeepAspectRatio)
self.video_widget.setStyleSheet("""
QVideoWidget {
border: none;
border-radius: 6px;
background-color: #2d2d2d;
}
""")
# Add video widget centered in container with proper alignment
video_container_layout.addWidget(self.video_widget, 0, Qt.AlignmentFlag.AlignCenter)
# Store references for dynamic sizing
self.video_container = video_container
print("📺 Clean video widget initialized (no overlay)")
# Add placeholder label for when no video is loaded
self.video_placeholder = QLabel("🎬 Select a video from Media Bin to start editing")
self.video_placeholder.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.video_placeholder.setStyleSheet("""
QLabel {
color: #888888;
font-size: 16px;
font-weight: bold;
background-color: transparent;
padding: 20px;
}
""")
# Create a stacked widget to switch between placeholder and video container
self.video_stack = QStackedWidget()
self.video_stack.addWidget(self.video_placeholder) # Index 0
self.video_stack.addWidget(self.video_container) # Index 1 - use the stored container
self.video_stack.setCurrentIndex(0) # Start with placeholder
video_layout.addWidget(self.video_stack)
# Set object name for easy finding later
video_frame.setObjectName("video_frame")
center_layout.addWidget(video_frame, 1)
# Timeline and playback controls
timeline_frame = QFrame()
timeline_frame.setFixedHeight(300) # Reduced height to fit all tracks without cutting off
timeline_frame.setStyleSheet("""
QFrame {
background-color: #1e1e1e;
border: 1px solid #404040;
border-radius: 4px;
}
""")
timeline_layout = QVBoxLayout(timeline_frame)
# Playback controls
playback_controls = QHBoxLayout()
# Time display
self.time_label = QLabel("00:00 / 00:00")
self.time_label.setStyleSheet("color: #ffffff; font-weight: bold;")
playback_controls.addWidget(self.time_label)
playback_controls.addStretch()
# Play/Pause button
self.play_pause_btn = QPushButton("")
self.play_pause_btn.clicked.connect(self.toggle_playback)
self.play_pause_btn.setStyleSheet("""
QPushButton {
background-color: #00aa00;
color: white;
border: none;
padding: 10px 15px;
border-radius: 6px;
font-size: 16px;
font-weight: bold;
min-width: 50px;
}
QPushButton:hover {
background-color: #00cc00;
}
""")
playback_controls.addWidget(self.play_pause_btn)
# Add some spacing between buttons
playback_controls.addSpacing(10)
# Fullscreen button - Add next to the play button in timeline
self.timeline_fullscreen_btn = QPushButton("⛶ Fullscreen")
self.timeline_fullscreen_btn.setToolTip("Fullscreen (F11)")
self.timeline_fullscreen_btn.clicked.connect(self.toggle_fullscreen)
self.timeline_fullscreen_btn.setStyleSheet("""
QPushButton {
background-color: #0066cc;
color: white;
border: none;
padding: 10px 15px;
border-radius: 6px;
font-size: 14px;
font-weight: bold;
min-width: 100px;
}
QPushButton:hover {
background-color: #0088ff;
}
QPushButton:pressed {
background-color: #004499;
}
""")
playback_controls.addWidget(self.timeline_fullscreen_btn)
print("🎮 Added fullscreen button to timeline controls")
playback_controls.addStretch()
timeline_layout.addLayout(playback_controls)
# Timeline slider
timeline_slider_layout = QHBoxLayout()
self.timeline_slider = ProfessionalSlider()
self.timeline_slider.setRange(0, 1000)
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_slider_layout.addWidget(self.timeline_slider, 1)
timeline_layout.addLayout(timeline_slider_layout)
# Professional timeline widget
self.timeline_widget = TimelineWidget()
self.timeline_widget.positionChanged.connect(self.seek_to_time)
timeline_layout.addWidget(self.timeline_widget, 1)
center_layout.addWidget(timeline_frame)
main_splitter.addWidget(center_panel)
# Right panel (Tabbed Tools)
right_panel = QWidget()
right_panel.setFixedWidth(300)
right_layout = QVBoxLayout(right_panel)
# Create tabbed interface for tools
self.tool_tabs = QTabWidget()
self.tool_tabs.setStyleSheet("""
QTabWidget::pane {
border: 1px solid #404040;
background-color: #2d2d2d;
}
QTabWidget::tab-bar {
alignment: center;
}
QTabBar::tab {
background-color: #404040;
color: #ffffff;
padding: 8px 12px;
margin: 2px;
border-radius: 4px;
}
QTabBar::tab:selected {
background-color: #00aaff;
color: #ffffff;
font-weight: bold;
}
QTabBar::tab:hover {
background-color: #606060;
}
""")
# Effects tab
self.effects_panel = EffectsPanel()
self.effects_panel.effectApplied.connect(self.apply_effect)
self.tool_tabs.addTab(self.effects_panel, "🎨 Effects")
# Basic editing tab
basic_edit_panel = self.create_basic_edit_panel()
self.tool_tabs.addTab(basic_edit_panel, "✂️ Edit")
# Export tab
export_panel = self.create_export_panel()
self.tool_tabs.addTab(export_panel, "📤 Export")
right_layout.addWidget(self.tool_tabs)
main_splitter.addWidget(right_panel)
# Set splitter proportions
main_splitter.setSizes([250, 850, 300])
# Status bar
self.statusBar().showMessage("Ready - Select a video from the media bin to start")
def setup_media_player(self):
"""Setup PyQt6 media player"""
self.media_player = QMediaPlayer()
self.audio_output = QAudioOutput()
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_media_error)
# Connect video output changed to get video dimensions
self.media_player.metaDataChanged.connect(self.update_video_info)
# Position update timer
self.position_timer = QTimer()
self.position_timer.timeout.connect(self.update_timeline_position)
self.position_timer.start(50) # 20 FPS updates
self.timeline_dragging = False
def setup_shortcuts(self):
"""Setup keyboard shortcuts"""
# Spacebar - Play/Pause
space_shortcut = QShortcut(QKeySequence(Qt.Key.Key_Space), self)
space_shortcut.activated.connect(self.toggle_playback)
# Left/Right arrows
left_shortcut = QShortcut(QKeySequence(Qt.Key.Key_Left), self)
left_shortcut.activated.connect(self.frame_backward)
right_shortcut = QShortcut(QKeySequence(Qt.Key.Key_Right), self)
right_shortcut.activated.connect(self.frame_forward)
# 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)
print("⌨️ Keyboard shortcuts enabled:")
print(" SPACE: Play/Pause")
print(" ←/→: Frame navigation")
print(" F11: Toggle fullscreen")
print(" ESC: Exit fullscreen")
def resizeEvent(self, event):
"""Handle window resize and update video scaling"""
super().resizeEvent(event)
# Force video widget to update its size to fit within container
if hasattr(self, 'video_widget') and hasattr(self, 'video_container'):
# Give the layout system time to update
QTimer.singleShot(10, self.update_video_size)
def update_video_size(self):
"""Update video widget size to fit within its container"""
try:
if hasattr(self, 'video_widget') and hasattr(self, 'video_container'):
# Get the container's available size (minus padding)
container_size = self.video_container.size()
available_width = container_size.width() - 10 # Account for padding
available_height = container_size.height() - 10 # Account for padding
# Calculate the maximum size while maintaining 16:9 aspect ratio
aspect_ratio = 16.0 / 9.0
# Try fitting by width first
width_constrained_height = available_width / aspect_ratio
if width_constrained_height <= available_height:
# Width is the limiting factor
new_width = available_width
new_height = int(width_constrained_height)
else:
# Height is the limiting factor
new_height = available_height
new_width = int(available_height * aspect_ratio)
# Ensure minimum size
new_width = max(new_width, 320)
new_height = max(new_height, 180)
# Apply the calculated size using size hints instead of fixed size
self.video_widget.setMinimumSize(new_width, new_height)
self.video_widget.setMaximumSize(new_width, new_height)
except Exception as e:
print(f"⚠️ Error updating video size: {e}")
def setup_styling(self):
"""Apply professional dark theme"""
self.setStyleSheet("""
QMainWindow {
background-color: #1e1e1e;
color: #ffffff;
}
QWidget {
background-color: #1e1e1e;
color: #ffffff;
}
QLabel {
color: #ffffff;
}
QGroupBox {
background-color: #2d2d2d;
}
QLineEdit, QSpinBox, QDoubleSpinBox {
background-color: #404040;
border: 1px solid #666666;
border-radius: 3px;
padding: 5px;
color: #ffffff;
}
QLineEdit:focus, QSpinBox:focus, QDoubleSpinBox:focus {
border: 2px solid #00aaff;
}
""")
def create_basic_edit_panel(self):
"""Create basic editing tools panel"""
panel = QWidget()
layout = QVBoxLayout(panel)
# Trim section
trim_group = QGroupBox("✂️ Trim Video")
trim_group.setStyleSheet("""
QGroupBox {
font-weight: bold;
border: 2px solid #404040;
border-radius: 5px;
margin-top: 10px;
color: #ffffff;
}
QGroupBox::title {
subcontrol-origin: margin;
left: 10px;
padding: 0 5px 0 5px;
}
""")
trim_layout = QVBoxLayout(trim_group)
# Trim controls
trim_controls = QHBoxLayout()
trim_controls.addWidget(QLabel("Start:"))
self.trim_start = QDoubleSpinBox()
self.trim_start.setRange(0.0, 999.0)
self.trim_start.setSingleStep(0.1)
self.trim_start.setSuffix("s")
trim_controls.addWidget(self.trim_start)
trim_controls.addWidget(QLabel("End:"))
self.trim_end = QDoubleSpinBox()
self.trim_end.setRange(0.0, 999.0)
self.trim_end.setSingleStep(0.1)
self.trim_end.setValue(10.0)
self.trim_end.setSuffix("s")
trim_controls.addWidget(self.trim_end)
trim_layout.addLayout(trim_controls)
trim_btn = QPushButton("Apply Trim")
trim_btn.clicked.connect(self.apply_trim)
trim_btn.setStyleSheet("""
QPushButton {
background-color: #cc6600;
color: white;
border: none;
padding: 8px;
border-radius: 4px;
font-weight: bold;
}
QPushButton:hover {
background-color: #ff8800;
}
""")
trim_layout.addWidget(trim_btn)
layout.addWidget(trim_group)
# Speed section
speed_group = QGroupBox("⚡ Speed Control")
speed_group.setStyleSheet(trim_group.styleSheet())
speed_layout = QVBoxLayout(speed_group)
speed_controls = QHBoxLayout()
speed_controls.addWidget(QLabel("Speed:"))
self.speed_factor = QDoubleSpinBox()
self.speed_factor.setRange(0.1, 10.0)
self.speed_factor.setSingleStep(0.1)
self.speed_factor.setValue(1.0)
self.speed_factor.setSuffix("x")
speed_controls.addWidget(self.speed_factor)
speed_layout.addLayout(speed_controls)
speed_btn = QPushButton("Apply Speed")
speed_btn.clicked.connect(self.apply_speed)
speed_btn.setStyleSheet(trim_btn.styleSheet())
speed_layout.addWidget(speed_btn)
layout.addWidget(speed_group)
# Volume section
volume_group = QGroupBox("🔊 Volume Control")
volume_group.setStyleSheet(trim_group.styleSheet())
volume_layout = QVBoxLayout(volume_group)
volume_controls = QHBoxLayout()
volume_controls.addWidget(QLabel("Volume:"))
self.volume_factor = QDoubleSpinBox()
self.volume_factor.setRange(0.0, 5.0)
self.volume_factor.setSingleStep(0.1)
self.volume_factor.setValue(1.0)
self.volume_factor.setSuffix("x")
volume_controls.addWidget(self.volume_factor)
volume_layout.addLayout(volume_controls)
volume_btn = QPushButton("Apply Volume")
volume_btn.clicked.connect(self.apply_volume)
volume_btn.setStyleSheet(trim_btn.styleSheet())
volume_layout.addWidget(volume_btn)
layout.addWidget(volume_group)
layout.addStretch()
return panel
def create_export_panel(self):
"""Create export tools panel"""
panel = QWidget()
layout = QVBoxLayout(panel)
# Export settings
export_group = QGroupBox("📤 Export Settings")
export_group.setStyleSheet("""
QGroupBox {
font-weight: bold;
border: 2px solid #404040;
border-radius: 5px;
margin-top: 10px;
color: #ffffff;
}
QGroupBox::title {
subcontrol-origin: margin;
left: 10px;
padding: 0 5px 0 5px;
}
""")
export_layout = QVBoxLayout(export_group)
# Output filename
filename_layout = QHBoxLayout()
filename_layout.addWidget(QLabel("Filename:"))
self.export_filename = QLineEdit()
self.export_filename.setText("edited_video.mp4")
filename_layout.addWidget(self.export_filename)
export_layout.addLayout(filename_layout)
# Quality settings
quality_layout = QHBoxLayout()
quality_layout.addWidget(QLabel("Quality:"))
self.export_quality = QComboBox()
self.export_quality.addItems(["High", "Medium", "Low"])
quality_layout.addWidget(self.export_quality)
export_layout.addLayout(quality_layout)
# Export button
export_btn = QPushButton("🎬 Export Video")
export_btn.setObjectName("export_btn")
export_btn.clicked.connect(self.export_video)
export_btn.setStyleSheet("""
QPushButton {
background-color: #006600;
color: white;
border: none;
padding: 12px;
border-radius: 6px;
font-weight: bold;
font-size: 14px;
}
QPushButton:hover {
background-color: #008800;
}
""")
export_layout.addWidget(export_btn)
layout.addWidget(export_group)
# Export progress
self.export_progress = QProgressBar()
self.export_progress.setVisible(False)
layout.addWidget(self.export_progress)
layout.addStretch()
return panel
def apply_trim(self):
"""Apply trim effect"""
QMessageBox.information(self, "Trim Applied",
f"Trim applied: {self.trim_start.value():.1f}s to {self.trim_end.value():.1f}s")
def apply_speed(self):
"""Apply speed effect"""
QMessageBox.information(self, "Speed Applied",
f"Speed changed to {self.speed_factor.value():.1f}x")
def apply_volume(self):
"""Apply volume effect"""
QMessageBox.information(self, "Volume Applied",
f"Volume changed to {self.volume_factor.value():.1f}x")
def export_video(self):
"""Export the edited video with progress tracking"""
if not self.current_video:
QMessageBox.warning(self, "No Video", "Please load a video first.")
return
# Timeline clips are optional - we can export just the main video
# or main video with timeline clips overlaid
print(f"🎬 Exporting video: {self.current_video}")
if hasattr(self.timeline_widget, 'timeline_clips') and self.timeline_widget.timeline_clips:
print(f"📊 With {len(self.timeline_widget.timeline_clips)} timeline clips")
else:
print("📊 Main video only (no timeline clips)")
filename = self.export_filename.text().strip()
if not filename:
filename = "edited_video.mp4"
if not filename.endswith('.mp4'):
filename += '.mp4'
# Create output path in the shorts folder
output_path = os.path.join("shorts", "edited", filename)
os.makedirs(os.path.dirname(output_path), exist_ok=True)
# Show progress bar and disable export button
self.export_progress.setVisible(True)
self.export_progress.setValue(0)
export_btn = self.sender()
export_btn.setEnabled(False)
export_btn.setText("⏳ Exporting...")
# Start export in a separate thread
from threading import Thread
import time
def export_thread():
try:
self.export_timeline_video(output_path)
except Exception as e:
print(f"❌ Export error: {e}")
QMetaObject.invokeMethod(self, "export_finished",
Qt.ConnectionType.QueuedConnection,
Q_ARG(bool, False),
Q_ARG(str, str(e)))
else:
QMetaObject.invokeMethod(self, "export_finished",
Qt.ConnectionType.QueuedConnection,
Q_ARG(bool, True),
Q_ARG(str, output_path))
Thread(target=export_thread, daemon=True).start()
@pyqtSlot(bool, str)
def export_finished(self, success, message):
"""Handle export completion"""
# Hide progress bar and re-enable export button
self.export_progress.setVisible(False)
export_btn = self.findChild(QPushButton, "export_btn")
if not export_btn:
# Find by text if objectName not set
for btn in self.findChildren(QPushButton):
if "Export" in btn.text():
export_btn = btn
break
if export_btn:
export_btn.setEnabled(True)
export_btn.setText("🎬 Export Video")
if success:
QMessageBox.information(self, "Export Complete",
f"Video exported successfully!\n\nSaved to: {message}")
else:
QMessageBox.critical(self, "Export Failed",
f"Export failed: {message}")
def export_timeline_video(self, output_path):
"""Export video with main video as base track and timeline clips overlaid"""
import cv2
import numpy as np
try:
# Check if we have a main video loaded
if not self.current_video:
raise Exception("No main video loaded")
# Get timeline clips (these will be overlays)
clips = self.timeline_widget.timeline_clips
# Update progress
QMetaObject.invokeMethod(self.export_progress, "setValue",
Qt.ConnectionType.QueuedConnection, Q_ARG(int, 10))
# Open main video to get its properties and duration
main_video_path = self.current_video
if not os.path.exists(main_video_path):
raise Exception(f"Main video not found: {main_video_path}")
main_cap = cv2.VideoCapture(main_video_path)
if not main_cap.isOpened():
raise Exception(f"Could not open main video: {main_video_path}")
# Get video properties from main video
fps = int(main_cap.get(cv2.CAP_PROP_FPS))
width = int(main_cap.get(cv2.CAP_PROP_FRAME_WIDTH))
height = int(main_cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
total_frames = int(main_cap.get(cv2.CAP_PROP_FRAME_COUNT))
main_duration = total_frames / fps
print(f"🎬 Main video: {main_video_path}")
print(f"📊 Properties: {width}x{height} @ {fps}fps, Duration: {main_duration:.2f}s")
# Update progress
QMetaObject.invokeMethod(self.export_progress, "setValue",
Qt.ConnectionType.QueuedConnection, Q_ARG(int, 20))
# Create video writer
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
out = cv2.VideoWriter(output_path, fourcc, fps, (width, height))
if not out.isOpened():
main_cap.release()
raise Exception(f"Could not create output video: {output_path}")
print(f"🎯 Processing {total_frames} frames...")
# Process each frame
frame_count = 0
while True:
ret, main_frame = main_cap.read()
if not ret:
break
current_time = frame_count / fps
# Start with the main video frame
output_frame = main_frame.copy()
# Overlay any timeline clips that are active at this time
for clip in clips:
if clip['start_time'] <= current_time < clip['start_time'] + clip['duration']:
if clip['type'] == 'video':
# Calculate position in the clip
clip_relative_time = current_time - clip['start_time']
# Open clip video and seek to the right frame
clip_cap = cv2.VideoCapture(clip['filename'])
clip_cap.set(cv2.CAP_PROP_POS_MSEC, clip_relative_time * 1000)
clip_ret, clip_frame = clip_cap.read()
clip_cap.release()
if clip_ret and clip_frame is not None:
# Resize clip frame to match main video
if clip_frame.shape[1] != width or clip_frame.shape[0] != height:
clip_frame = cv2.resize(clip_frame, (width, height))
# For now, replace the frame (in future could blend/overlay)
output_frame = clip_frame
# Write the final frame
out.write(output_frame)
frame_count += 1
# Update progress periodically
if frame_count % 100 == 0:
progress = 30 + int((frame_count / total_frames) * 60)
QMetaObject.invokeMethod(self.export_progress, "setValue",
Qt.ConnectionType.QueuedConnection, Q_ARG(int, min(progress, 90)))
# Cleanup
main_cap.release()
out.release()
# Final progress update
QMetaObject.invokeMethod(self.export_progress, "setValue",
Qt.ConnectionType.QueuedConnection, Q_ARG(int, 100))
print(f"✅ Export completed: {output_path}")
print(f"📊 Processed {frame_count} frames")
except Exception as e:
print(f"❌ Export error: {e}")
raise
def setup_styling(self):
"""Apply professional dark theme"""
self.setStyleSheet("""
QMainWindow {
background-color: #1e1e1e;
color: #ffffff;
}
QWidget {
background-color: #1e1e1e;
color: #ffffff;
}
QLabel {
color: #ffffff;
}
QGroupBox {
background-color: #2d2d2d;
}
QLineEdit, QSpinBox, QDoubleSpinBox, QComboBox {
background-color: #404040;
border: 1px solid #666666;
border-radius: 3px;
padding: 5px;
color: #ffffff;
}
QLineEdit:focus, QSpinBox:focus, QDoubleSpinBox:focus, QComboBox:focus {
border: 2px solid #00aaff;
}
QComboBox::drop-down {
border: none;
}
QComboBox::down-arrow {
image: none;
border: none;
}
QProgressBar {
border: 1px solid #404040;
border-radius: 3px;
text-align: center;
background-color: #2d2d2d;
}
QProgressBar::chunk {
background-color: #00aaff;
border-radius: 2px;
}
""")
def update_video_info(self):
"""Update video player size based on loaded video dimensions"""
try:
# Get video metadata
if self.media_player.metaData():
video_size = self.media_player.metaData().get('Resolution')
if video_size and hasattr(video_size, 'width') and hasattr(video_size, 'height'):
self.adapt_video_player_size(video_size.width(), video_size.height())
return
# Fallback: Try to get size from video widget after a short delay
QTimer.singleShot(1000, self.try_get_video_size_from_widget)
except Exception as e:
print(f"⚠️ Could not get video dimensions: {e}")
# Use default 16:9 sizing
self.adapt_video_player_size(1920, 1080)
def try_get_video_size_from_widget(self):
"""Fallback method to get video size"""
try:
# Try to get size from video widget
widget_size = self.video_widget.videoSize()
if widget_size and widget_size.width() > 0 and widget_size.height() > 0:
self.adapt_video_player_size(widget_size.width(), widget_size.height())
print(f"📐 Video size detected from widget: {widget_size.width()}x{widget_size.height()}")
else:
# Default to 16:9 if we can't detect
self.adapt_video_player_size(1920, 1080)
print("📐 Using default 16:9 aspect ratio")
except Exception as e:
print(f"⚠️ Fallback size detection failed: {e}")
self.adapt_video_player_size(1920, 1080)
def adapt_video_player_size(self, video_width, video_height):
"""Use consistent size matching the placeholder - no dynamic scaling"""
try:
# Use the same size as the default video widget for consistency
# This matches the size set in the initialization (640x360)
default_width = 795 # Same as initial video widget size
default_height = 420 # Same as initial video widget size (16:9 ratio)
# Always use the same size regardless of video dimensions
# This ensures consistent UI layout and eliminates black bar issues
display_width = default_width
display_height = default_height
# Update video widget size to match default
self.video_widget.setMinimumSize(display_width, display_height)
self.video_widget.setMaximumSize(display_width, display_height) # Fixed size
# Force layout update
self.video_container.updateGeometry()
self.video_stack.updateGeometry()
print(f"📐 Using consistent video player size: {display_width}x{display_height} (original: {video_width}x{video_height})")
except Exception as e:
print(f"⚠️ Error setting video player size: {e}")
def load_video_file(self, file_path):
"""Load video file"""
try:
self.current_video = file_path
url = QUrl.fromLocalFile(file_path)
self.media_player.setSource(url)
# Switch from placeholder to video widget
self.video_stack.setCurrentIndex(1)
# Enable controls
self.timeline_slider.setEnabled(True)
self.play_pause_btn.setEnabled(True)
self.timeline_fullscreen_btn.setEnabled(True)
# Update video size to fit container after loading
QTimer.singleShot(100, self.update_video_size)
self.statusBar().showMessage(f"✅ Loaded: {os.path.basename(file_path)}")
print(f"✅ Loaded video: {os.path.basename(file_path)}")
except Exception as e:
self.statusBar().showMessage(f"❌ Error: {e}")
QMessageBox.critical(self, "Error", f"Failed to load video:\n{e}")
def toggle_playback(self):
"""Toggle play/pause"""
if self.media_player.playbackState() == QMediaPlayer.PlaybackState.PlayingState:
self.media_player.pause()
# Update timeline play button
self.play_pause_btn.setText("")
# Update integrated button text if it exists
if hasattr(self, 'integrated_play_btn'):
self.integrated_play_btn.setText("▶ Play")
else:
self.media_player.play()
# Update timeline play button
self.play_pause_btn.setText("")
# Update integrated button text if it exists
if hasattr(self, 'integrated_play_btn'):
self.integrated_play_btn.setText("⏸ Pause")
def frame_backward(self):
"""Go back one frame"""
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 frame_forward(self):
"""Go forward one frame"""
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_to_time(self, time_seconds):
"""Seek to specific time"""
position_ms = int(time_seconds * 1000)
self.media_player.setPosition(position_ms)
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 with improved widget handling"""
try:
self.is_fullscreen = True
# Create fullscreen window
self.fullscreen_window = QWidget()
self.fullscreen_window.setWindowTitle("Video Fullscreen")
self.fullscreen_window.setStyleSheet("background-color: black;")
self.fullscreen_window.showFullScreen()
# Create a new video widget for fullscreen (don't move the original)
self.fullscreen_video_widget = QVideoWidget()
self.media_player.setVideoOutput(self.fullscreen_video_widget)
fullscreen_layout = QVBoxLayout(self.fullscreen_window)
fullscreen_layout.setContentsMargins(0, 0, 0, 0)
fullscreen_layout.addWidget(self.fullscreen_video_widget)
# Add exit button overlay
exit_btn = QPushButton("✕ Exit Fullscreen (ESC)")
exit_btn.clicked.connect(self.exit_fullscreen)
exit_btn.setStyleSheet("""
QPushButton {
background-color: rgba(51, 51, 51, 200);
color: white;
border: none;
padding: 15px 20px;
border-radius: 8px;
font-weight: bold;
font-size: 14px;
}
QPushButton:hover {
background-color: rgba(77, 77, 77, 220);
}
""")
exit_btn.setFixedSize(200, 50)
exit_btn.move(30, 30)
exit_btn.setParent(self.fullscreen_window)
exit_btn.show()
self.fullscreen_window.setFocus()
self.timeline_fullscreen_btn.setText("🗗 Exit FS")
# Setup fullscreen shortcuts
esc_fullscreen = QShortcut(QKeySequence(Qt.Key.Key_Escape), self.fullscreen_window)
esc_fullscreen.activated.connect(self.exit_fullscreen)
space_fullscreen = QShortcut(QKeySequence(Qt.Key.Key_Space), self.fullscreen_window)
space_fullscreen.activated.connect(self.toggle_playback)
print("🔳 Entered fullscreen mode successfully")
except Exception as e:
print(f"⚠️ Error entering fullscreen: {e}")
self.is_fullscreen = False
def exit_fullscreen(self):
"""Exit fullscreen mode with improved widget handling"""
if self.is_fullscreen and self.fullscreen_window:
try:
self.is_fullscreen = False
# Restore video output to original widget
self.media_player.setVideoOutput(self.video_widget)
# Clean up fullscreen window and video widget
if hasattr(self, 'fullscreen_video_widget'):
self.fullscreen_video_widget.setParent(None)
self.fullscreen_video_widget = None
self.fullscreen_window.close()
self.fullscreen_window = None
self.timeline_fullscreen_btn.setText("⛶ Fullscreen")
print("🔲 Exited fullscreen mode successfully")
except Exception as e:
print(f"⚠️ Error exiting fullscreen: {e}")
self.is_fullscreen = False
self.fullscreen_window = None
def restore_video_widget(self):
"""Safely restore video widget to original location"""
try:
# Find the video frame container
for widget in self.centralWidget().findChildren(QFrame):
if widget.objectName() == "video_frame":
video_layout = widget.layout()
if video_layout:
video_layout.insertWidget(0, self.video_widget)
print("📺 Video widget restored to original position")
return
# Fallback: recreate the video layout structure
self.setup_video_container()
except Exception as e:
print(f"⚠️ Error restoring video widget: {e}")
def setup_video_container(self):
"""Setup or recreate video container structure"""
try:
# Find the center panel and recreate video structure if needed
center_widget = None
for widget in self.centralWidget().findChildren(QWidget):
if hasattr(widget, 'layout') and widget.layout():
layout = widget.layout()
if layout.count() > 0:
center_widget = widget
break
if center_widget:
# Create new video frame
video_frame = QFrame()
video_frame.setObjectName("video_frame")
video_frame.setStyleSheet("""
QFrame {
background-color: #000000;
border: 2px solid #404040;
border-radius: 8px;
}
""")
video_layout = QVBoxLayout(video_frame)
video_layout.addWidget(self.video_widget)
# Add back to center layout
center_layout = center_widget.layout()
center_layout.insertWidget(0, video_frame)
print("🔧 Video container recreated successfully")
except Exception as e:
print(f"⚠️ Error setting up video container: {e}")
def apply_effect(self, effect_type, params):
"""Apply effect with parameters"""
self.effects_enabled[effect_type] = True
self.effect_times[effect_type] = {
'start': params['start_time'],
'end': params['end_time']
}
self.effect_params[effect_type] = params
QMessageBox.information(
self,
"Effect Applied",
f"{effect_type.title()} effect applied from {params['start_time']:.1f}s to {params['end_time']:.1f}s"
)
print(f"🎨 Applied {effect_type} effect: {params}")
def on_timeline_pressed(self):
"""Timeline pressed"""
self.timeline_dragging = True
def on_timeline_released(self):
"""Timeline released"""
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 value changed"""
if self.timeline_dragging and self.media_player.duration() > 0:
position = (value / 1000.0) * self.media_player.duration()
self.current_time = position / 1000.0
self.update_time_display()
def update_position(self, position):
"""Update position from media player"""
self.current_time = position / 1000.0
# Update timeline slider
if not self.timeline_dragging and self.media_player.duration() > 0:
value = (position / self.media_player.duration()) * 1000
self.timeline_slider.setValue(int(value))
# Update timeline widget
self.timeline_widget.set_position(self.current_time)
# Update time display
self.update_time_display()
# Debug output for time tracking
if int(self.current_time) % 5 == 0: # Every 5 seconds
print(f"⏱️ Time: {self.format_time(self.current_time)} / {self.format_time(self.video_duration)}")
def update_duration(self, duration):
"""Update duration"""
self.video_duration = duration / 1000.0
self.timeline_widget.set_duration(self.video_duration)
self.update_time_display()
print(f"📏 Video duration set: {self.format_time(self.video_duration)}")
def update_timeline_position(self):
"""High-frequency timeline updates"""
if not self.timeline_dragging and self.media_player.duration() > 0:
position = self.media_player.position()
# Only update if position actually changed
if abs(position - (self.current_time * 1000)) > 100: # 100ms threshold
self.update_position(position)
def update_time_display(self):
"""Update time display"""
current = self.format_time(self.current_time)
total = self.format_time(self.video_duration)
self.time_label.setText(f"{current} / {total}")
def update_playback_state(self, state):
"""Update play/pause button"""
if state == QMediaPlayer.PlaybackState.PlayingState:
self.play_pause_btn.setText("")
self.is_playing = True
else:
self.play_pause_btn.setText("")
self.is_playing = False
def handle_media_error(self, error):
"""Handle media errors"""
error_msg = f"Media error: {error}"
self.statusBar().showMessage(f"{error_msg}")
QMessageBox.critical(self, "Media Error", error_msg)
def format_time(self, seconds):
"""Format time as mm:ss"""
minutes = int(seconds // 60)
seconds = int(seconds % 60)
return f"{minutes:02d}:{seconds:02d}"
def main():
"""Run the PyQt6 professional video editor"""
app = QApplication(sys.argv)
# Set application properties
app.setApplicationName("Professional Video Editor - PyQt6")
app.setApplicationVersion("2.0")
# Apply fusion style for modern look
app.setStyle(QStyleFactory.create('Fusion'))
# Create and show editor
editor = ProfessionalVideoEditor()
editor.show()
print("🎬 Professional Video Editor (PyQt6) Started!")
print("🚀 PyQt6 Features:")
print(" • Hardware-accelerated video playback")
print(" • Professional timeline with visual tracks")
print(" • Real-time effects system")
print(" • Modern docking interface")
print(" • GPU-accelerated rendering")
print(" • Professional keyboard shortcuts")
sys.exit(app.exec())
if __name__ == "__main__":
main()