Das Python-Skript „Geiler Video-Editor Pro“ ist eine Desktop-Anwendung, die eine benutzerfreundliche Oberfläche für die Bearbeitung von Videoclips bereitstellt. Entwickelt mit PyQt5 für die grafische Benutzeroberfläche und FFmpeg für die Verarbeitung von Video- und Audiodaten, ermöglicht es Nutzern, Videoclips zu schneiden, mit einer Audiospur zu kombinieren und visuelle Anpassungen vorzunehmen, ohne auf komplexe Software wie professionelle Schnittprogramme angewiesen zu sein. Die Anwendung ist für einfache Videobearbeitungsaufgaben konzipiert und richtet sich an Anwender, die persönliche oder semiprofessionelle Projekte effizient umsetzen möchten.
Die Bedienung beginnt mit der Auswahl eines Ordners, der Videodateien in Formaten wie MP4, AVI, MOV, MKV, FLV oder WMV enthält. Diese Clips werden in einer Liste angezeigt, aus der Nutzer einzelne Videos zur Bearbeitung auswählen können. Für jeden Clip lassen sich Start- und Endzeitpunkte in Sekunden festlegen, um unerwünschte Abschnitte zu entfernen. Alternativ kann ein bestimmter Frame über einen Schieberegler ausgewählt werden, an dem der Clip abgeschnitten wird, was präzises Trimmen ermöglicht. Ausgewählte Clips werden in eine Sequenz eingefügt, die die Reihenfolge des finalen Videos bestimmt, und können bei Bedarf wieder entfernt werden.
Ein wesentliches Merkmal ist die Möglichkeit, eine separate Audiospur in Formaten wie MP3, WAV oder AAC hinzuzufügen. Die Lautstärke dieser Audiospur sowie die der Audioinhalte der Videoclips lässt sich unabhängig voneinander über Schieberegler anpassen, mit einem Bereich von 0 bis 200 Prozent. Falls die Audiospur länger als die Videosequenz ist, wird sie automatisch auf die Videodauer zugeschnitten, um eine saubere Synchronisation zu gewährleisten. Ist die Audiospur kürzer, wird sie nicht verlängert, was eine bewusste Einschränkung der Funktionalität darstellt.
Visuelle Anpassungen umfassen die Regelung von Helligkeit und Kontrast, die über Schieberegler im Bereich von -100 bis +100 eingestellt werden können. Diese Effekte werden sowohl in der Vorschau eines Clips als auch im exportierten Video angewendet. Die Vorschau erlaubt es, einzelne Frames eines Clips oder eines getrimmten Sequenzclips anzuzeigen, wobei ein Schieberegler die Navigation durch die Frames erleichtert. Einzelne Frames können zudem als Bild in Formaten wie PNG oder JPG exportiert werden, was für die Erstellung von Vorschaubildern oder Momentaufnahmen nützlich ist.
Beim Export des Videos kann der Nutzer die Auflösung wählen, darunter 720p, 1080p oder 4K, sowie die Bitrate und Bildrate anpassen, um die Qualität und Dateigröße zu steuern. Das Video wird im MP4-Format mit dem H.264-Videocodec und AAC-Audiocodec ausgegeben. Die Verarbeitung erfolgt in einem separaten Thread, sodass die Benutzeroberfläche während des Exports responsiv bleibt. Eine Fortschrittsanzeige und Fehlermeldungen informieren den Nutzer über den Status des Prozesses.
Projekte lassen sich als JSON-Datei speichern und später wieder laden, wobei alle Einstellungen wie Clips, Trim-Daten, Audiospur, Lautstärke und visuelle Effekte erhalten bleiben. Dies erleichtert die Fortsetzung der Arbeit an einem Projekt. Alle Aktionen und potenziellen Fehler werden in einer Log-Datei protokolliert, die bei der Fehlersuche hilfreich ist.
Die Anwendung hat bewusste Einschränkungen: Es gibt keine Vorschau der gesamten Sequenz vor dem Export, sodass Nutzer auf die Vorschau einzelner Clips angewiesen sind. Text- oder Wasserzeichen-Overlays sind nicht verfügbar, und fortgeschrittene Funktionen wie Übergänge zwischen Clips oder Mehrspurbearbeitung fehlen. Dennoch bietet das Skript eine robuste Lösung für einfache Videobearbeitungsaufgaben, gestützt durch die zuverlässige FFmpeg-Engine. Es ist besonders geeignet für Nutzer, die ohne großen Lernaufwand Clips schneiden, mit Audio kombinieren und grundlegende visuelle Anpassungen vornehmen möchten. Entwickler können den Python-Code leicht anpassen, um zusätzliche Funktionen hinzuzufügen, was die Flexibilität der Anwendung erhöht.
Um das Python-Skript „Geiler Video-Editor Pro“ auszuführen, ist eine Installation der erforderlichen Software und Bibliotheken notwendig, da das Skript auf Python, PyQt5 für die grafische Oberfläche, FFmpeg für die Videoverarbeitung und Imageio für die Bildverarbeitung angewiesen ist. Zunächst muss Python in Version 3.8 oder höher installiert werden, das von der offiziellen Python-Website heruntergeladen werden kann. Bei der Installation unter Windows sollte die Option „Add Python to PATH“ aktiviert werden, um Python von der Kommandozeile aus zugänglich zu machen. Zur Überprüfung kann in einer Kommandozeile oder einem Terminal der Befehl python –version eingegeben werden, der die installierte Python-Version anzeigt. Als Nächstes ist FFmpeg erforderlich, ein Tool für die Video- und Audiobearbeitung. Unter Windows kann FFmpeg von der offiziellen Website oder einem vertrauenswürdigen Anbieter wie gyan.dev heruntergeladen werden. Nach dem Entpacken der ZIP-Datei muss der bin-Ordner, der die Dateien ffmpeg.exe und ffprobe.exe enthält, zum Systempfad hinzugefügt werden, indem man unter „Erweiterte Systemeinstellungen“ die Umgebungsvariablen bearbeitet und den Pfad, etwa C:\ffmpeg\bin, einfügt. Die Installation kann durch Eingabe von ffmpeg -version und ffprobe -version in der Kommandozeile überprüft werden. Auf einem Mac mit Homebrew genügt der Befehl brew install ffmpeg im Terminal, um FFmpeg zu installieren. Unter Linux, etwa Debian oder Ubuntu, kann FFmpeg mit sudo apt update gefolgt von sudo apt install ffmpeg installiert werden, während auf Fedora der Befehl sudo dnf install ffmpeg verwendet wird. Danach müssen die Python-Bibliotheken installiert werden, indem in einer Kommandozeile oder einem Terminal der Befehl pip install pyqt5 imageio imageio-ffmpeg numpy ausgeführt wird. Dieser Befehl installiert PyQt5 für die grafische Benutzeroberfläche, Imageio für das Lesen von Bildern, Imageio-FFmpeg für die Zusammenarbeit mit FFmpeg und NumPy für die Verarbeitung von Bilddaten. Das Skript selbst sollte als video_editor.py in einem Ordner gespeichert werden, auf den Schreibzugriff besteht, da temporäre Dateien erstellt werden. Um das Skript zu starten, muss in der Kommandozeile oder im Terminal zum Ordner des Skripts navigiert und python video_editor.py eingegeben werden. Sollten Probleme auftreten, etwa wenn FFmpeg nicht gefunden wird, sollte überprüft werden, ob ffmpeg und ffprobe im Systempfad sind, indem die oben genannten Befehle zur Überprüfung ausgeführt werden. Bei Fehlern mit PyQt5 kann mit pip show pyqt5 geprüft werden, ob die Bibliothek korrekt installiert ist. Falls ein PermissionError auftritt, kann das Skript mit Administratorrechten ausgeführt oder der temporäre Dateipfad im Code, etwa durch Ändern von tempfile.gettempdir(), angepasst werden. Das Skript erstellt eine Log-Datei namens video_editor.log im gleichen Ordner, die bei der Fehlersuche wertvolle Informationen liefert.
import sys
import os
from PyQt5.QtWidgets import (
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QPushButton, QListWidget, QLabel, QSlider, QFileDialog,
QComboBox, QMessageBox, QLineEdit, QProgressBar, QGridLayout,
QGroupBox, QDoubleSpinBox
)
from PyQt5.QtCore import Qt, QThread, pyqtSignal, QMutex
from PyQt5.QtGui import QPixmap, QImage
import subprocess
import tempfile
import imageio.v2 as imageio
import numpy as np
import json
import time
import logging
# Logging einrichten
logging.basicConfig(
level=logging.DEBUG,
format='%(asctime)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler("video_editor.log", encoding='utf-8'),
logging.StreamHandler()
]
)
class FFmpegThread(QThread):
"""Führt FFmpeg-Befehle im Hintergrund aus und meldet Fortschritt."""
progress = pyqtSignal(int)
finished = pyqtSignal(str)
error = pyqtSignal(str)
def __init__(self, cmd, temp_files, total_duration):
super().__init__()
self.cmd = cmd
self.temp_files = temp_files
self.total_duration = total_duration
self.process = None
def run(self):
try:
logging.debug(f"FFmpeg-Befehl: {' '.join(self.cmd)}")
creation_flags = subprocess.CREATE_NO_WINDOW if sys.platform == "win32" else 0
self.process = subprocess.Popen(
self.cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
text=True, encoding='utf-8', creationflags=creation_flags
)
error_output = []
while self.process.poll() is None:
line = self.process.stdout.readline()
error_output.append(line)
if "out_time_ms" in line:
time_ms = int(line.split("=")[1].strip()) / 1_000_000
progress = min(99, int((time_ms / self.total_duration) * 100))
self.progress.emit(progress)
if self.process.returncode == 0:
self.progress.emit(100)
self.finished.emit("Video erfolgreich generiert!")
else:
raise Exception(f"FFmpeg-Fehler: {''.join(error_output)}")
except Exception as e:
self.error.emit(f"Fehler beim FFmpeg: {str(e)}")
finally:
if self.process:
self.process.stdout.close()
try:
self.process.wait(timeout=5)
except subprocess.TimeoutExpired:
self.process.terminate()
time.sleep(0.5)
for file in (self.temp_files if isinstance(self.temp_files, list) else [self.temp_files]):
if file and os.path.exists(file):
retries = 3
while retries > 0:
try:
os.remove(file)
break
except PermissionError:
time.sleep(1)
retries -= 1
except Exception as e:
logging.error(f"Fehler beim Löschen von {file}: {e}")
def stop(self):
if self.process and self.process.poll() is None:
self.process.terminate()
try:
self.process.wait(timeout=5)
except subprocess.TimeoutExpired:
self.process.kill()
self.error.emit("FFmpeg-Prozess abgebrochen")
class VideoEditor(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Geiler Video-Editor Pro")
self.setGeometry(100, 100, 1200, 800)
self.video_folder = ""
self.clips = []
self.selected_clips = []
self.current_clip = None
self.current_duration = 0
self.current_fps = 24
self.audio_path = ""
self.audio_volume = 1.0
self.project_dir = os.path.dirname(os.path.abspath(__file__))
self.preview_mutex = QMutex() # Mutex für Vorschau
self.check_ffmpeg()
self.init_ui()
def check_ffmpeg(self):
try:
subprocess.run(["ffmpeg", "-version"], capture_output=True, check=True)
subprocess.run(["ffprobe", "-version"], capture_output=True, check=True)
except FileNotFoundError:
QMessageBox.critical(self, "Fehler", "FFmpeg/FFprobe nicht gefunden! Bitte installieren.")
sys.exit(1)
except subprocess.CalledProcessError as e:
try:
error_msg = e.stderr.decode('utf-8') if e.stderr else "Unbekannter Fehler"
except UnicodeDecodeError:
error_msg = "Fehler beim Dekodieren der FFmpeg/FFprobe-Ausgabe"
QMessageBox.critical(self, "Fehler", f"FFmpeg/FFprobe-Fehler: {error_msg}")
sys.exit(1)
def init_ui(self):
main_widget = QWidget()
self.setCentralWidget(main_widget)
main_layout = QHBoxLayout()
main_widget.setLayout(main_layout)
self.setStyleSheet("""
QWidget { background-color: #1e1e1e; color: #ffffff; }
QPushButton { background-color: #ff4500; border: none; border-radius: 5px; padding: 8px; }
QPushButton:hover { background-color: #ff6347; }
QGroupBox { border: 1px solid #ff4500; border-radius: 5px; margin-top: 10px; }
QGroupBox::title { color: #ff4500; subcontrol-origin: margin; subcontrol-position: top left; padding: 5px; }
QLineEdit, QDoubleSpinBox, QComboBox, QListWidget { background-color: #2d2d2d; border: 1px solid #ff4500; border-radius: 3px; padding: 3px; color: #ffffff; }
QSlider::groove:horizontal { background: #333333; height: 8px; border-radius: 4px; }
QSlider::handle:horizontal { background: #ff4500; width: 16px; height: 16px; border-radius: 8px; }
QProgressBar { background-color: #2d2d2d; border: 1px solid #ff4500; border-radius: 5px; }
QProgressBar::chunk { background-color: #ff4500; border-radius: 4px; }
""")
# Linkes Panel
left_panel = QVBoxLayout()
left_group = QGroupBox("Video-Steuerung")
left_group_layout = QVBoxLayout()
project_layout = QHBoxLayout()
btn_save_project = QPushButton("Projekt speichern")
btn_save_project.clicked.connect(self.save_project)
btn_load_project = QPushButton("Projekt laden")
btn_load_project.clicked.connect(self.load_project)
project_layout.addWidget(btn_save_project)
project_layout.addWidget(btn_load_project)
left_group_layout.addLayout(project_layout)
folder_layout = QHBoxLayout()
self.folder_combo = QComboBox()
self.folder_combo.setEditable(True)
self.folder_combo.addItem("Ordner wählen...")
self.folder_combo.activated[str].connect(self.load_videos_from_folder)
btn_folder = QPushButton("Durchsuchen")
btn_folder.clicked.connect(self.browse_folder)
folder_layout.addWidget(self.folder_combo)
folder_layout.addWidget(btn_folder)
left_group_layout.addWidget(QLabel("Videos:"))
left_group_layout.addLayout(folder_layout)
self.video_list = QListWidget()
self.video_list.itemClicked.connect(self.preview_clip)
self.video_list.itemDoubleClicked.connect(self.add_to_sequence)
left_group_layout.addWidget(self.video_list)
self.selected_listbox = QListWidget()
self.selected_listbox.itemClicked.connect(self.preview_selected_clip)
self.selected_listbox.itemDoubleClicked.connect(self.remove_clip)
left_group_layout.addWidget(QLabel("Ausgewählte Clips:"))
left_group_layout.addWidget(self.selected_listbox)
btn_add_clip = QPushButton("Clip hinzufügen")
btn_add_clip.clicked.connect(lambda: self.add_to_sequence(self.video_list.currentItem()))
left_group_layout.addWidget(btn_add_clip)
trim_group = QGroupBox("Trimmen")
trim_layout = QGridLayout()
self.start_time = QDoubleSpinBox()
self.start_time.setRange(0, 9999)
self.end_time = QDoubleSpinBox()
self.end_time.setRange(0, 9999)
trim_layout.addWidget(QLabel("Start (s):"), 0, 0)
trim_layout.addWidget(self.start_time, 0, 1)
trim_layout.addWidget(QLabel("Ende (s):"), 1, 0)
trim_layout.addWidget(self.end_time, 1, 1)
trim_group.setLayout(trim_layout)
left_group_layout.addWidget(trim_group)
self.volumeSlider = QSlider(Qt.Horizontal)
self.volumeSlider.setRange(0, 200)
self.volumeSlider.setValue(100)
left_group_layout.addWidget(QLabel("Lautstärke Clip (%):"))
left_group_layout.addWidget(self.volumeSlider)
audio_group = QGroupBox("Audio-Bearbeitung")
audio_layout = QGridLayout()
btn_load_audio = QPushButton("Audio laden")
btn_load_audio.clicked.connect(self.load_audio)
self.audio_volumeSlider = QSlider(Qt.Horizontal)
self.audio_volumeSlider.setRange(0, 200)
self.audio_volumeSlider.setValue(100)
audio_layout.addWidget(btn_load_audio, 0, 0, 1, 2)
audio_layout.addWidget(QLabel("Lautstärke Audio (%):"), 1, 0)
audio_layout.addWidget(self.audio_volumeSlider, 1, 1)
audio_group.setLayout(audio_layout)
left_group_layout.addWidget(audio_group)
left_group.setLayout(left_group_layout)
left_panel.addWidget(left_group)
main_layout.addLayout(left_panel, 1)
# Rechtes Panel
right_panel = QVBoxLayout()
right_group = QGroupBox("Vorschau & Effekte")
right_group_layout = QVBoxLayout()
self.preview_label = QLabel()
self.preview_label.setMinimumSize(320, 180)
self.preview_label.setScaledContents(True)
self.preview_label.setAlignment(Qt.AlignCenter)
right_group_layout.addWidget(QLabel("Vorschau:"))
right_group_layout.addWidget(self.preview_label)
self.frameSlider = QSlider(Qt.Horizontal)
self.frameSlider.setMinimum(0)
self.frameSlider.valueChanged.connect(self.update_preview)
right_group_layout.addWidget(QLabel("Frame-Navigation:"))
right_group_layout.addWidget(self.frameSlider)
cut_layout = QHBoxLayout()
btn_extract_frame = QPushButton("Frame extrahieren")
btn_extract_frame.clicked.connect(self.extract_frame)
btn_cut = QPushButton("Nach Frame abschneiden")
btn_cut.clicked.connect(self.cut_after_frame)
btn_save_new = QPushButton("Neues Video erstellen")
btn_save_new.clicked.connect(self.save_new_video)
cut_layout.addWidget(btn_extract_frame)
cut_layout.addWidget(btn_cut)
cut_layout.addWidget(btn_save_new)
right_group_layout.addLayout(cut_layout)
effects_group = QGroupBox("Videoeffekte")
effects_layout = QGridLayout()
self.brightness = QSlider(Qt.Horizontal)
self.brightness.setRange(-100, 100)
self.brightness.setValue(0)
self.contrast = QSlider(Qt.Horizontal)
self.contrast.setRange(-100, 100)
self.contrast.setValue(0)
effects_layout.addWidget(QLabel("Helligkeit:"), 0, 0)
effects_layout.addWidget(self.brightness, 0, 1)
effects_layout.addWidget(QLabel("Kontrast:"), 1, 0)
effects_layout.addWidget(self.contrast, 1, 1)
effects_group.setLayout(effects_layout)
right_group_layout.addWidget(effects_group)
export_group = QGroupBox("Export-Einstellungen")
export_layout = QGridLayout()
self.resolution_combo = QComboBox()
self.resolution_combo.addItems(["720p (1280x720)", "1080p (1920x1080)", "4K (3840x2160)"])
self.bitrate_input = QLineEdit("5M")
self.fps_input = QLineEdit("24")
export_layout.addWidget(QLabel("Auflösung:"), 0, 0)
export_layout.addWidget(self.resolution_combo, 0, 1)
export_layout.addWidget(QLabel("Bitrate:"), 1, 0)
export_layout.addWidget(self.bitrate_input, 1, 1)
export_layout.addWidget(QLabel("FPS:"), 2, 0)
export_layout.addWidget(self.fps_input, 2, 1)
export_group.setLayout(export_layout)
right_group_layout.addWidget(export_group)
self.progress_bar = QProgressBar()
self.progress_bar.setValue(0)
right_group_layout.addWidget(QLabel("Fortschritt:"))
right_group_layout.addWidget(self.progress_bar)
btn_generate = QPushButton("Video generieren")
btn_generate.clicked.connect(self.generate_video)
right_group_layout.addWidget(btn_generate)
right_group.setLayout(right_group_layout)
right_panel.addWidget(right_group)
main_layout.addLayout(right_panel, 2)
def save_project(self):
project_file = QFileDialog.getSaveFileName(self, "Projekt speichern", "", "JSON (*.json)")[0]
if not project_file:
return
try:
project_data = {
"video_folder": self.video_folder,
"selected_clips": [
{"path": clip[0], "start": clip[1], "end": clip[2], "volume": clip[3], "has_audio": clip[4]}
for clip in self.selected_clips
],
"audio_path": self.audio_path,
"audio_volume": self.audio_volume,
"brightness": self.brightness.value(),
"contrast": self.contrast.value(),
"resolution": self.resolution_combo.currentText(),
"bitrate": self.bitrate_input.text(),
"fps": self.fps_input.text()
}
with open(project_file, 'w', encoding='utf-8') as f:
json.dump(project_data, f, indent=4)
QMessageBox.information(self, "Erfolg", f"Projekt gespeichert: {project_file}")
logging.info(f"Projekt gespeichert: {project_file}")
except Exception as e:
QMessageBox.critical(self, "Fehler", f"Fehler beim Speichern: {str(e)}")
logging.error(f"Fehler beim Speichern des Projekts: {str(e)}")
def load_project(self):
project_file = QFileDialog.getOpenFileName(self, "Projekt laden", "", "JSON (*.json)")[0]
if not project_file:
return
try:
with open(project_file, 'r', encoding='utf-8') as f:
project_data = json.load(f)
self.video_folder = project_data.get("video_folder", "")
if self.video_folder and not os.path.exists(self.video_folder):
QMessageBox.warning(self, "Warnung", f"Video-Ordner nicht gefunden: {self.video_folder}")
if self.video_folder and os.path.exists(self.video_folder):
self.folder_combo.insertItem(0, self.video_folder)
self.folder_combo.setCurrentIndex(0)
self.load_videos_from_folder(self.video_folder)
self.selected_clips = [
(clip["path"], clip["start"], clip["end"], clip["volume"], clip["has_audio"])
for clip in project_data.get("selected_clips", [])
]
self.selected_listbox.clear()
for clip in self.selected_clips:
self.selected_listbox.addItem(f"{os.path.basename(clip[0])} ({clip[1]}s - {clip[2]}s, Vol: {int(clip[3]*100)}%)")
self.audio_path = project_data.get("audio_path", "")
self.audio_volume = project_data.get("audio_volume", 1.0)
self.audio_volumeSlider.setValue(int(self.audio_volume * 100))
self.brightness.setValue(project_data.get("brightness", 0))
self.contrast.setValue(project_data.get("contrast", 0))
self.resolution_combo.setCurrentText(project_data.get("resolution", "720p (1280x720)"))
self.bitrate_input.setText(project_data.get("bitrate", "5M"))
self.fps_input.setText(project_data.get("fps", "24"))
QMessageBox.information(self, "Erfolg", f"Projekt geladen: {project_file}")
logging.info(f"Projekt geladen: {project_file}")
except Exception as e:
QMessageBox.critical(self, "Fehler", f"Fehler beim Laden: {str(e)}")
logging.error(f"Fehler beim Laden des Projekts: {str(e)}")
def load_audio(self):
self.audio_path = QFileDialog.getOpenFileName(self, "Audiospur wählen", "", "Audio (*.mp3 *.wav *.aac)")[0]
if self.audio_path:
self.audio_volume = self.audio_volumeSlider.value() / 100.0
QMessageBox.information(self, "Erfolg", f"Audiospur geladen: {os.path.basename(self.audio_path)}")
logging.info(f"Audiospur geladen: {self.audio_path}")
def browse_folder(self):
folder = QFileDialog.getExistingDirectory(self, "Video-Ordner auswählen")
if folder:
self.folder_combo.insertItem(0, folder)
self.folder_combo.setCurrentIndex(0)
self.load_videos_from_folder(folder)
def load_videos_from_folder(self, folder):
if folder == "Ordner wählen..." or not folder:
return
self.video_folder = folder
self.video_list.clear()
self.clips = []
supported_formats = (".mp4", ".avi", ".mov", ".mkv", ".flv", ".wmv")
for file in os.listdir(self.video_folder):
if file.lower().endswith(supported_formats):
video_path = os.path.join(self.video_folder, file)
info = self.get_video_info(video_path)
if info["duration"] > 0:
self.clips.append((file, video_path, info["duration"], info["fps"], info["has_audio"]))
self.video_list.addItem(file)
logging.debug(f"Video geladen: {file}")
else:
QMessageBox.warning(self, "Fehler", f"Konnte Dauer von {file} nicht laden!")
logging.warning(f"Fehler beim Laden der Dauer von {file}")
def get_video_info(self, video_path):
try:
cmd = [
"ffprobe", "-v", "error", "-show_entries",
"format=duration:stream=width,height,r_frame_rate,codec_name,sample_rate,channels",
"-of", "json", video_path
]
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
data = json.loads(result.stdout)
duration = float(data.get("format", {}).get("duration", 0.1))
video_stream = next((s for s in data.get("streams", []) if s.get("codec_type") == "video"), {})
audio_stream = next((s for s in data.get("streams", []) if s.get("codec_type") == "audio"), {})
fps_str = video_stream.get("r_frame_rate", "24/1")
fps = 24 # Fallback
try:
num, den = map(int, fps_str.split('/'))
fps = num / den if den != 0 else 24
except (ValueError, ZeroDivisionError):
logging.warning(f"Ungültiger FPS-Wert für {video_path}, verwende Fallback: 24")
width = video_stream.get("width", 0)
height = video_stream.get("height", 0)
sample_rate = audio_stream.get("sample_rate", None)
channels = audio_stream.get("channels", None)
has_audio = bool(sample_rate and channels)
return {
"duration": max(duration, 0.1),
"fps": fps,
"width": width,
"height": height,
"sample_rate": sample_rate,
"channels": channels,
"has_audio": has_audio
}
except Exception as e:
logging.error(f"Fehler beim Ermitteln der Videoinfos: {str(e)}")
return {"duration": 0.1, "fps": 24, "width": 0, "height": 0, "sample_rate": None, "channels": None, "has_audio": False}
def preview_clip(self, item):
if not item:
return
try:
clip_name = item.text()
for name, path, duration, fps, _ in self.clips:
if name == clip_name:
self.current_clip = path
self.current_duration = duration
self.current_fps = fps
self.frameSlider.setMaximum(int(fps * duration) - 1)
self.start_time.setMaximum(duration)
self.end_time.setMaximum(duration)
self.start_time.setValue(0)
self.end_time.setValue(duration)
self.update_preview()
logging.debug(f"Vorschau für Clip: {clip_name}")
break
except Exception as e:
logging.error(f"Fehler bei Clip-Vorschau: {str(e)}")
QMessageBox.critical(self, "Fehler", f"Vorschau-Fehler: {str(e)}")
def preview_selected_clip(self, item):
if not item:
return
try:
row = self.selected_listbox.row(item)
clip_path, start, end, _, _ = self.selected_clips[row]
self.current_clip = clip_path
self.current_duration = end - start
self.current_fps = self.get_video_info(clip_path)["fps"]
self.frameSlider.setMaximum(int(self.current_fps * self.current_duration) - 1)
self.start_time.setMaximum(self.current_duration)
self.end_time.setMaximum(self.current_duration)
self.start_time.setValue(0)
self.end_time.setValue(self.current_duration)
self.update_preview()
logging.debug(f"Vorschau für ausgewählten Clip: {clip_path}")
except Exception as e:
logging.error(f"Fehler bei ausgewählter Clip-Vorschau: {str(e)}")
QMessageBox.critical(self, "Fehler", f"Vorschau-Fehler: {str(e)}")
def update_preview(self):
if not self.current_clip:
return
if not self.preview_mutex.tryLock():
logging.debug("Vorschau gesperrt, überspringe")
return
try:
frame_idx = self.frameSlider.value()
time = frame_idx / self.current_fps
with tempfile.NamedTemporaryFile(suffix=".png", delete=False, dir=self.project_dir) as tmp:
tmp_path = tmp.name
brightness = self.brightness.value() / 100.0
contrast = self.contrast.value() / 100.0
vf = f"eq=brightness={brightness}:contrast={1+contrast},scale=320:180:force_original_aspect_ratio=decrease"
cmd = [
"ffmpeg", "-i", self.current_clip, "-ss", str(time),
"-vframes", "1", "-vf", vf, "-y", tmp_path
]
result = subprocess.run(cmd, capture_output=True, text=True, encoding='utf-8')
if result.returncode != 0 or not os.path.exists(tmp_path):
raise Exception(f"FFmpeg-Fehler: {result.stderr}")
reader = imageio.get_reader(tmp_path)
try:
frame = reader.get_data(0)
frame = frame[:, :, [2, 1, 0]] # BGR zu RGB
height, width, channels = frame.shape
bytes_per_line = channels * width
q_image = QImage(frame.tobytes(), width, height, bytes_per_line, QImage.Format_RGB888)
pixmap = QPixmap.fromImage(q_image).scaled(720, 405, Qt.KeepAspectRatio)
self.preview_label.setPixmap(pixmap)
logging.debug(f"Vorschau aktualisiert für Frame {frame_idx}")
finally:
reader.close()
except Exception as e:
logging.error(f"Vorschau-Fehler: {str(e)}")
QMessageBox.warning(self, "Fehler", f"Vorschau-Fehler: {str(e)}")
finally:
if 'tmp_path' in locals() and os.path.exists(tmp_path):
try:
os.remove(tmp_path)
except Exception as e:
logging.error(f"Fehler beim Löschen der temporären Datei {tmp_path}: {str(e)}")
self.preview_mutex.unlock()
def extract_frame(self):
if not self.current_clip:
QMessageBox.warning(self, "Warnung", "Kein Clip ausgewählt!")
return
try:
frame_idx = self.frameSlider.value()
time = frame_idx / self.current_fps
output_path = QFileDialog.getSaveFileName(self, "Frame speichern", "", "Images (*.png *.jpg)")[0]
if not output_path:
return
brightness = self.brightness.value() / 100.0
contrast = self.contrast.value() / 100.0
vf = f"eq=brightness={brightness}:contrast={1+contrast}"
cmd = [
"ffmpeg", "-i", self.current_clip, "-ss", str(time),
"-vframes", "1", "-vf", vf, "-y", output_path
]
result = subprocess.run(cmd, capture_output=True, text=True, encoding='utf-8')
if result.returncode != 0:
raise Exception(f"FFmpeg-Fehler: {result.stderr}")
QMessageBox.information(self, "Erfolg", f"Frame gespeichert: {output_path}")
logging.info(f"Frame extrahiert: {output_path}")
except Exception as e:
logging.error(f"Fehler beim Extrahieren: {str(e)}")
QMessageBox.warning(self, "Fehler", f"Fehler beim Extrahieren: {str(e)}")
def cut_after_frame(self):
if not self.current_clip:
QMessageBox.warning(self, "Warnung", "Kein Clip ausgewählt!")
return
try:
frame_idx = self.frameSlider.value()
cut_time = frame_idx / self.current_fps
row = self.selected_listbox.currentRow()
if row >= 0:
clip_path, start, _, volume, has_audio = self.selected_clips[row]
if cut_time <= 0 or cut_time >= (self.current_duration + start):
QMessageBox.warning(self, "Warnung", "Ungültiger Schnittpunkt!")
return
self.selected_clips[row] = (clip_path, start, start + cut_time, volume, has_audio)
self.selected_listbox.takeItem(row)
self.selected_listbox.insertItem(row, f"{os.path.basename(clip_path)} ({start}s - {start + cut_time}s, Vol: {int(volume*100)}%)")
self.preview_selected_clip(self.selected_listbox.item(row))
logging.debug(f"Clip zugeschnitten: {clip_path}")
else:
QMessageBox.warning(self, "Warnung", "Bitte wähle einen Clip aus der Sequenz!")
except Exception as e:
logging.error(f"Fehler beim Abschneiden: {str(e)}")
QMessageBox.critical(self, "Fehler", f"Fehler beim Abschneiden: {str(e)}")
def save_new_video(self):
if not self.current_clip:
QMessageBox.warning(self, "Warnung", "Kein Clip ausgewählt!")
return
try:
row = self.selected_listbox.currentRow()
if row < 0:
QMessageBox.warning(self, "Warnung", "Bitte wähle einen Clip aus der Sequenz!")
return
clip_path, start, end, volume, has_audio = self.selected_clips[row]
output_path = QFileDialog.getSaveFileName(self, "Video speichern", "", "MP4 (*.mp4)")[0]
if not output_path:
return
brightness = self.brightness.value() / 100.0
contrast = self.contrast.value() / 100.0
vf = f"eq=brightness={brightness}:contrast={1+contrast},format=yuv420p"
cmd = [
"ffmpeg", "-i", clip_path, "-ss", str(start), "-t", str(end - start),
"-vf", vf, "-c:v", "libx264", "-preset", "medium", "-b:v", "5M",
"-r", str(self.current_fps)
]
if has_audio:
cmd.extend(["-c:a", "aac", "-filter:a", f"volume={volume}"])
else:
cmd.append("-an")
cmd.extend(["-y", output_path])
result = subprocess.run(cmd, capture_output=True, text=True, encoding='utf-8')
if result.returncode != 0:
raise Exception(f"FFmpeg-Fehler: {result.stderr}")
QMessageBox.information(self, "Erfolg", f"Video gespeichert: {output_path}")
logging.info(f"Neues Video gespeichert: {output_path}")
except Exception as e:
logging.error(f"Fehler beim Speichern: {str(e)}")
QMessageBox.warning(self, "Fehler", f"Fehler beim Speichern: {str(e)}")
def add_to_sequence(self, item=None):
logging.debug("Clip zur Sequenz hinzufügen")
if not item:
logging.warning("Kein Item übergeben")
return
try:
clip_name = item.text()
logging.debug(f"Clip-Name: {clip_name}")
start = self.start_time.value()
end = self.end_time.value()
volume = self.volumeSlider.value() / 100.0
logging.debug(f"Parameter: Start {start}, Ende {end}, Lautstärke {volume}")
if start >= end:
QMessageBox.warning(self, "Warnung", "Startzeit muss kleiner als Endzeit sein!")
logging.warning("Ungültige Zeiten: Start >= Ende")
return
for name, path, duration, _, has_audio in self.clips:
if name == clip_name:
logging.debug(f"Clip gefunden: {name}, Dauer: {duration}")
if end > duration:
QMessageBox.warning(self, "Warnung", f"Endzeit überschreitet Clip-Dauer ({duration}s)!")
logging.warning(f"Endzeit {end} überschreitet Dauer {duration}")
return
self.selected_clips.append((path, start, end, volume, has_audio))
self.selected_listbox.addItem(f"{clip_name} ({start}s - {end}s, Vol: {int(volume*100)}%)")
logging.info(f"Clip hinzugefügt: {clip_name} ({start}s - {end}s)")
break
else:
QMessageBox.warning(self, "Fehler", f"Clip nicht gefunden: {clip_name}")
logging.warning(f"Clip nicht gefunden: {clip_name}")
except Exception as e:
logging.error(f"Fehler beim Hinzufügen des Clips: {str(e)}")
QMessageBox.critical(self, "Fehler", f"Fehler beim Hinzufügen: {str(e)}")
def remove_clip(self):
logging.debug("Clip entfernen")
try:
item = self.selected_listbox.currentItem()
if not item:
logging.warning("Kein Clip ausgewählt")
return
row = self.selected_listbox.row(item)
self.selected_listbox.takeItem(row)
self.selected_clips.pop(row)
logging.debug(f"Clip entfernt: Index {row}")
except Exception as e:
logging.error(f"Fehler beim Entfernen des Clips: {str(e)}")
QMessageBox.critical(self, "Fehler", f"Fehler beim Entfernen: {str(e)}")
def validate_inputs(self):
try:
bitrate = self.bitrate_input.text().strip()
if not (bitrate.endswith("k") or bitrate.endswith("M")) or not float(bitrate[:-1]):
raise ValueError("Bitrate muss im Format '5M' oder '500k' sein!")
fps = float(self.fps_input.text().strip())
if fps <= 0:
raise ValueError("FPS muss positiv sein!")
return True
except Exception as e:
logging.error(f"Eingabefehler: {str(e)}")
QMessageBox.warning(self, "Fehler", f"Eingabe-Fehler: {e}")
return False
def generate_video(self):
if not self.selected_clips:
QMessageBox.warning(self, "Warnung", "Keine Clips ausgewählt!")
logging.warning("Keine Clips für Video-Generierung")
return
if not self.validate_inputs():
return
try:
output_path = QFileDialog.getSaveFileName(self, "Video speichern", "", "Videos (*.mp4)")[0]
if not output_path:
return
resolution = self.resolution_combo.currentText().split()[0]
scale = {"720p": "1280:720", "1080p": "1920:1080", "4K": "3840:2160"}
width, height = map(int, scale[resolution].split(':'))
bitrate = self.bitrate_input.text()
fps = float(self.fps_input.text())
brightness = self.brightness.value() / 100.0
contrast = self.contrast.value() / 100.0
sample_rate = "44100"
# Gesamtdauer des Videos berechnen
total_duration = sum(max(0, end - start) for _, start, end, _, _ in self.selected_clips)
if total_duration <= 0:
raise ValueError("Gesamtdauer der Clips ist ungültig!")
video_filters = [
f"scale={width}:{height}:force_original_aspect_ratio=decrease",
f"pad={width}:{height}:(ow-iw)/2:(oh-ih)/2",
f"eq=brightness={brightness}:contrast={1+contrast}",
"format=yuv420p",
f"fps={fps}"
]
temp_files = []
has_audio_clips = False
for i, (clip, start, end, volume, has_audio) in enumerate(self.selected_clips):
temp_output = os.path.join(tempfile.gettempdir(), f"temp_clip_{i}.mp4")
clip_vf = ",".join([
f"scale={width}:{height}:force_original_aspect_ratio=decrease",
f"pad={width}:{height}:(ow-iw)/2:(oh-ih)/2",
f"eq=brightness={brightness}:contrast={1+contrast}",
"format=yuv420p",
f"fps={fps}"
])
cmd = [
"ffmpeg", "-i", clip, "-ss", str(start), "-t", str(end - start),
"-vf", clip_vf, "-c:v", "libx264", "-preset", "medium", "-b:v", bitrate,
"-r", str(fps)
]
if has_audio:
cmd.extend(["-c:a", "aac", "-ar", sample_rate, "-ac", "2", "-filter:a", f"volume={volume}"])
has_audio_clips = True
else:
cmd.append("-an")
cmd.extend(["-y", temp_output])
result = subprocess.run(cmd, capture_output=True, text=True, encoding='utf-8')
if result.returncode != 0 or not os.path.exists(temp_output):
raise Exception(f"Fehler beim Erstellen von {temp_output}: {result.stderr}")
temp_files.append(temp_output)
logging.debug(f"Temporärer Clip erstellt: {temp_output}")
with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False, dir=self.project_dir) as f:
for temp_file in temp_files:
f.write(f"file '{temp_file}'\n")
concat_file = f.name
logging.debug(f"Concat-Datei erstellt: {concat_file}")
cmd = [
"ffmpeg", "-f", "concat", "-safe", "0", "-i", concat_file
]
# Audio-Eingabe hinzufügen
if self.audio_path and os.path.exists(self.audio_path):
cmd.extend(["-i", self.audio_path])
# Filter-Complex für Video und Audio
filter_complex = []
# Video-Filter
video_filter_chain = ",".join(video_filters)
filter_complex.append(f"[0:v]{video_filter_chain}[v]")
# Audio-Filter
if self.audio_path and os.path.exists(self.audio_path):
# Dauer der Audiospur ermitteln
audio_info = self.get_video_info(self.audio_path)
audio_duration = audio_info["duration"]
# Wenn Audiospur länger als Video, mit atrim zuschneiden
if audio_duration > total_duration:
filter_complex.append(f"[1:a]atrim=0:{total_duration},volume={self.audio_volume}[aext]")
else:
filter_complex.append(f"[1:a]volume={self.audio_volume}[aext]")
if has_audio_clips:
# Mischen von Clip-Audio und externer Audiospur
filter_complex.append(f"[0:a][aext]amix=inputs=2:duration=first[a]")
else:
# Nur externe Audiospur
filter_complex.append(f"[aext]anull[a]")
elif has_audio_clips:
# Nur Clip-Audio, keine externe Spur
filter_complex.append(f"[0:a]volume=1.0[a]")
# Filter-Complex anwenden
if filter_complex:
cmd.extend(["-filter_complex", ";".join(filter_complex)])
# Video- und Audio-Mapping
cmd.extend([
"-map", "[v]" if filter_complex else "0:v",
"-c:v", "libx264", "-preset", "medium",
"-b:v", bitrate,
"-r", str(fps)
])
if self.audio_path and os.path.exists(self.audio_path) or has_audio_clips:
cmd.extend(["-map", "[a]" if filter_complex else "0:a", "-c:a", "aac", "-ar", sample_rate, "-ac", "2"])
cmd.extend(["-y", output_path])
self.thread = FFmpegThread(cmd, temp_files + [concat_file], total_duration)
self.thread.progress.connect(self.progress_bar.setValue)
self.thread.finished.connect(lambda msg: QMessageBox.information(self, "Erfolg", msg))
self.thread.error.connect(lambda msg: QMessageBox.critical(self, "Fehler", msg))
self.thread.start()
logging.info(f"Video-Generierung gestartet: {output_path}")
except Exception as e:
logging.error(f"Fehler beim Generieren: {str(e)}")
QMessageBox.critical(self, "Fehler", f"Fehler beim Generieren: {str(e)}")
if __name__ == "__main__":
app = QApplication(sys.argv)
app.setStyle("Fusion")
logging.info("Starte Video-Editor")
window = VideoEditor()
window.show()
sys.exit(app.exec_())
Schreibe einen Kommentar