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 (
|
from PyQt6.QtCore import (
|
||||||
Qt, QUrl, QTimer, pyqtSignal, QPropertyAnimation, QEasingCurve, QRect,
|
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.QtMultimedia import QMediaPlayer, QAudioOutput
|
||||||
from PyQt6.QtMultimediaWidgets import QVideoWidget
|
from PyQt6.QtMultimediaWidgets import QVideoWidget
|
||||||
@ -1295,8 +1296,9 @@ class ProfessionalVideoEditor(QMainWindow):
|
|||||||
video_layout = QVBoxLayout(video_frame)
|
video_layout = QVBoxLayout(video_frame)
|
||||||
video_layout.setContentsMargins(5, 5, 5, 5) # Add some padding
|
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 = QWidget()
|
||||||
|
video_container.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
|
||||||
video_container.setStyleSheet("""
|
video_container.setStyleSheet("""
|
||||||
QWidget {
|
QWidget {
|
||||||
background-color: #2d2d2d;
|
background-color: #2d2d2d;
|
||||||
@ -1305,21 +1307,16 @@ class ProfessionalVideoEditor(QMainWindow):
|
|||||||
}
|
}
|
||||||
""")
|
""")
|
||||||
video_container_layout = QVBoxLayout(video_container)
|
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)
|
video_container_layout.setSpacing(0)
|
||||||
|
|
||||||
# Adaptive video widget with intelligent sizing
|
# Create video widget with proper container-based scaling
|
||||||
self.video_widget = QVideoWidget()
|
self.video_widget = QVideoWidget()
|
||||||
|
|
||||||
# Set default size for 16:9 aspect ratio (1080p scaled down)
|
# Set the video widget to scale with its container
|
||||||
default_width = 640 # Standard width for video player
|
self.video_widget.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
|
||||||
default_height = int(default_width * 9 / 16) # 16:9 aspect ratio = 360
|
|
||||||
|
|
||||||
self.video_widget.setMinimumSize(default_width, default_height)
|
# Keep aspect ratio and fit within container bounds (won't overflow)
|
||||||
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
|
|
||||||
self.video_widget.setAspectRatioMode(Qt.AspectRatioMode.KeepAspectRatio)
|
self.video_widget.setAspectRatioMode(Qt.AspectRatioMode.KeepAspectRatio)
|
||||||
|
|
||||||
self.video_widget.setStyleSheet("""
|
self.video_widget.setStyleSheet("""
|
||||||
@ -1330,11 +1327,8 @@ class ProfessionalVideoEditor(QMainWindow):
|
|||||||
}
|
}
|
||||||
""")
|
""")
|
||||||
|
|
||||||
# Add video widget with top alignment to push it up
|
# Add video widget centered in container with proper alignment
|
||||||
video_container_layout.addWidget(self.video_widget, 0, Qt.AlignmentFlag.AlignTop)
|
video_container_layout.addWidget(self.video_widget, 0, Qt.AlignmentFlag.AlignCenter)
|
||||||
|
|
||||||
# Add stretch to push video to top and leave space at bottom
|
|
||||||
video_container_layout.addStretch()
|
|
||||||
|
|
||||||
# Store references for dynamic sizing
|
# Store references for dynamic sizing
|
||||||
self.video_container = video_container
|
self.video_container = video_container
|
||||||
@ -1369,7 +1363,7 @@ class ProfessionalVideoEditor(QMainWindow):
|
|||||||
|
|
||||||
# Timeline and playback controls
|
# Timeline and playback controls
|
||||||
timeline_frame = QFrame()
|
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("""
|
timeline_frame.setStyleSheet("""
|
||||||
QFrame {
|
QFrame {
|
||||||
background-color: #1e1e1e;
|
background-color: #1e1e1e;
|
||||||
@ -1568,9 +1562,48 @@ class ProfessionalVideoEditor(QMainWindow):
|
|||||||
print(" ESC: Exit fullscreen")
|
print(" ESC: Exit fullscreen")
|
||||||
|
|
||||||
def resizeEvent(self, event):
|
def resizeEvent(self, event):
|
||||||
"""Handle window resize"""
|
"""Handle window resize and update video scaling"""
|
||||||
super().resizeEvent(event)
|
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):
|
def setup_styling(self):
|
||||||
"""Apply professional dark theme"""
|
"""Apply professional dark theme"""
|
||||||
self.setStyleSheet("""
|
self.setStyleSheet("""
|
||||||
@ -1748,6 +1781,7 @@ class ProfessionalVideoEditor(QMainWindow):
|
|||||||
|
|
||||||
# Export button
|
# Export button
|
||||||
export_btn = QPushButton("🎬 Export Video")
|
export_btn = QPushButton("🎬 Export Video")
|
||||||
|
export_btn.setObjectName("export_btn")
|
||||||
export_btn.clicked.connect(self.export_video)
|
export_btn.clicked.connect(self.export_video)
|
||||||
export_btn.setStyleSheet("""
|
export_btn.setStyleSheet("""
|
||||||
QPushButton {
|
QPushButton {
|
||||||
@ -1790,18 +1824,188 @@ class ProfessionalVideoEditor(QMainWindow):
|
|||||||
f"Volume changed to {self.volume_factor.value():.1f}x")
|
f"Volume changed to {self.volume_factor.value():.1f}x")
|
||||||
|
|
||||||
def export_video(self):
|
def export_video(self):
|
||||||
"""Export the edited video"""
|
"""Export the edited video with progress tracking"""
|
||||||
if not self.current_video:
|
if not self.current_video:
|
||||||
QMessageBox.warning(self, "No Video", "Please load a video first.")
|
QMessageBox.warning(self, "No Video", "Please load a video first.")
|
||||||
return
|
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'):
|
if not filename.endswith('.mp4'):
|
||||||
filename += '.mp4'
|
filename += '.mp4'
|
||||||
|
|
||||||
QMessageBox.information(self, "Export Started",
|
# Create output path in the shorts folder
|
||||||
f"Video export started: {filename}\n"
|
output_path = os.path.join("shorts", "edited", filename)
|
||||||
f"Quality: {self.export_quality.currentText()}")
|
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):
|
def setup_styling(self):
|
||||||
"""Apply professional dark theme"""
|
"""Apply professional dark theme"""
|
||||||
@ -1924,6 +2128,9 @@ class ProfessionalVideoEditor(QMainWindow):
|
|||||||
self.play_pause_btn.setEnabled(True)
|
self.play_pause_btn.setEnabled(True)
|
||||||
self.timeline_fullscreen_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)}")
|
self.statusBar().showMessage(f"✅ Loaded: {os.path.basename(file_path)}")
|
||||||
print(f"✅ Loaded video: {os.path.basename(file_path)}")
|
print(f"✅ Loaded video: {os.path.basename(file_path)}")
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user