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

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert