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.
This commit is contained in:
parent
bc04aba0d7
commit
7f6b7b4901
1431
video_editor_enhanced.py
Normal file
1431
video_editor_enhanced.py
Normal file
File diff suppressed because it is too large
Load Diff
@ -57,7 +57,8 @@ from PyQt6.QtWidgets import (
|
||||
)
|
||||
from PyQt6.QtCore import (
|
||||
Qt, QUrl, QTimer, pyqtSignal, QPropertyAnimation, QEasingCurve, QRect,
|
||||
QThread, pyqtSlot, QObject, QRunnable, QThreadPool, QMutex, QSize, QEvent, QMimeData, QPoint
|
||||
QThread, pyqtSlot, QObject, QRunnable, QThreadPool, QMutex, QSize, QEvent, QMimeData, QPoint,
|
||||
QMetaObject, Q_ARG
|
||||
)
|
||||
from PyQt6.QtMultimedia import QMediaPlayer, QAudioOutput
|
||||
from PyQt6.QtMultimediaWidgets import QVideoWidget
|
||||
@ -1295,8 +1296,9 @@ class ProfessionalVideoEditor(QMainWindow):
|
||||
video_layout = QVBoxLayout(video_frame)
|
||||
video_layout.setContentsMargins(5, 5, 5, 5) # Add some padding
|
||||
|
||||
# Adaptive video container that matches video aspect ratio
|
||||
# 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;
|
||||
@ -1305,21 +1307,16 @@ class ProfessionalVideoEditor(QMainWindow):
|
||||
}
|
||||
""")
|
||||
video_container_layout = QVBoxLayout(video_container)
|
||||
video_container_layout.setContentsMargins(0, 0, 0, 0)
|
||||
video_container_layout.setContentsMargins(5, 5, 5, 5) # Small padding to ensure video stays inside
|
||||
video_container_layout.setSpacing(0)
|
||||
|
||||
# Adaptive video widget with intelligent sizing
|
||||
# Create video widget with proper container-based scaling
|
||||
self.video_widget = QVideoWidget()
|
||||
|
||||
# Set default size for 16:9 aspect ratio (1080p scaled down)
|
||||
default_width = 640 # Standard width for video player
|
||||
default_height = int(default_width * 9 / 16) # 16:9 aspect ratio = 360
|
||||
# Set the video widget to scale with its container
|
||||
self.video_widget.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
|
||||
|
||||
self.video_widget.setMinimumSize(default_width, default_height)
|
||||
self.video_widget.setMaximumHeight(600) # Limit maximum height
|
||||
self.video_widget.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
|
||||
|
||||
# Keep aspect ratio to prevent distortion, but adapt container size
|
||||
# Keep aspect ratio and fit within container bounds (won't overflow)
|
||||
self.video_widget.setAspectRatioMode(Qt.AspectRatioMode.KeepAspectRatio)
|
||||
|
||||
self.video_widget.setStyleSheet("""
|
||||
@ -1330,11 +1327,8 @@ class ProfessionalVideoEditor(QMainWindow):
|
||||
}
|
||||
""")
|
||||
|
||||
# Add video widget with top alignment to push it up
|
||||
video_container_layout.addWidget(self.video_widget, 0, Qt.AlignmentFlag.AlignTop)
|
||||
|
||||
# Add stretch to push video to top and leave space at bottom
|
||||
video_container_layout.addStretch()
|
||||
# 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
|
||||
@ -1369,7 +1363,7 @@ class ProfessionalVideoEditor(QMainWindow):
|
||||
|
||||
# Timeline and playback controls
|
||||
timeline_frame = QFrame()
|
||||
timeline_frame.setFixedHeight(500) # Much larger to accommodate all tracks properly
|
||||
timeline_frame.setFixedHeight(300) # Reduced height to fit all tracks without cutting off
|
||||
timeline_frame.setStyleSheet("""
|
||||
QFrame {
|
||||
background-color: #1e1e1e;
|
||||
@ -1568,9 +1562,48 @@ class ProfessionalVideoEditor(QMainWindow):
|
||||
print(" ESC: Exit fullscreen")
|
||||
|
||||
def resizeEvent(self, event):
|
||||
"""Handle window resize"""
|
||||
"""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("""
|
||||
@ -1748,6 +1781,7 @@ class ProfessionalVideoEditor(QMainWindow):
|
||||
|
||||
# Export button
|
||||
export_btn = QPushButton("🎬 Export Video")
|
||||
export_btn.setObjectName("export_btn")
|
||||
export_btn.clicked.connect(self.export_video)
|
||||
export_btn.setStyleSheet("""
|
||||
QPushButton {
|
||||
@ -1790,18 +1824,188 @@ class ProfessionalVideoEditor(QMainWindow):
|
||||
f"Volume changed to {self.volume_factor.value():.1f}x")
|
||||
|
||||
def export_video(self):
|
||||
"""Export the edited video"""
|
||||
"""Export the edited video with progress tracking"""
|
||||
if not self.current_video:
|
||||
QMessageBox.warning(self, "No Video", "Please load a video first.")
|
||||
return
|
||||
|
||||
filename = self.export_filename.text()
|
||||
# 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'
|
||||
|
||||
QMessageBox.information(self, "Export Started",
|
||||
f"Video export started: {filename}\n"
|
||||
f"Quality: {self.export_quality.currentText()}")
|
||||
# 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"""
|
||||
@ -1924,6 +2128,9 @@ class ProfessionalVideoEditor(QMainWindow):
|
||||
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)}")
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user