Wie bekomme ich den Videoplayer synchron bis auf den letzten frame ?

Stellt hier eure Projekte vor.
Internetseiten, Skripte, und alles andere bzgl. Python.
Antworten
LDN
User
Beiträge: 1
Registriert: Mittwoch 16. Oktober 2024, 19:39

hab einen einfachen Videoplayer der Videos abspielt aus dem Ordner Videos und dann löscht .
Nur hab ich Probleme Audio und Frames bis auf den letzten frame zu synchronisieren. Das Programm hängt sich auf am ende eines Videos manchmal oder am Anfang eines Videos, wenn es wenige Frames hat. Kann mir jemand den Code verbessern oder die Werte ?

Code: Alles auswählen

import glob
import cv2
import sys
import time
import traceback
import os
from ffpyplayer.player import MediaPlayer
import logging

# Logger-Konfiguration (nur Konsole)
logging.basicConfig(level=logging.ERROR,
                    format="%(asctime)s:%(levelname)s:%(message)s",
                    handlers=[logging.StreamHandler(sys.stdout)])

# Pfad zum Ordner 'videos' auf dem Desktop
desktop_path = os.path.join(os.path.expanduser("~"), "Desktop")
videos_folder = os.path.join(desktop_path, "videos")

# Alle MP4-Dateien im Ordner finden
try:
    video_files = glob.glob(os.path.join(videos_folder, "*.mp4"))

    if not video_files:
        print(f"Fehler: Keine MP4-Dateien im Ordner {videos_folder} gefunden.")
        sys.exit(1)
except Exception as e:
    logging.error("Fehler beim Suchen von Videodateien: %s", str(e))
    logging.error(traceback.format_exc())
    sys.exit(1)

# Globale Variablen für Steuerung
paused = False
fullscreen = True
next_video_requested = False  # Steuerung für das Überspringen zum nächsten Video
seek_value = 0  # Initialer Wert für die Seekbar
cap = None  # Globale Variable für den Video-Capture
seekbar_created = False  # Flag, um festzustellen, ob die Seekbar bereits erstellt wurde
previous_video_path = None  # Variable zum Speichern des Pfads des vorherigen Videos
current_video_path = None  # Variable für das aktuelle Video
sync_issue_start_time = None  # Variable für die Zeit, ab wann das Synchronisationsproblem beginnt

# Funktion zum Ändern der Frame-Position über die Seekbar
def on_trackbar(val):
    global seek_value
    seek_value = val

# Mausereignis-Funktion (mit Überspringen und Löschen)
def mouse_callback(event, x, y, flags, param):
    global paused, fullscreen, next_video_requested, current_video_path

    if event == cv2.EVENT_LBUTTONDOWN:
        paused = not paused
        param.toggle_pause()  # Pausiere/Setze Audio fort

    if event == cv2.EVENT_LBUTTONDBLCLK:
        fullscreen = not fullscreen
        if fullscreen:
            cv2.setWindowProperty("Video abspielen", cv2.WND_PROP_FULLSCREEN, cv2.WINDOW_FULLSCREEN)
        else:
            cv2.setWindowProperty("Video abspielen", cv2.WND_PROP_FULLSCREEN, cv2.WINDOW_NORMAL)

    # Rechtsklick -> Überspringe zum nächsten Video
    if event == cv2.EVENT_RBUTTONDOWN:
        next_video_requested = True  # Setze Flag zum Überspringen des Videos

# Funktion zum Einblenden der Synchronisierungsinformationen
def display_sync_info(frame, video_time, audio_time, position, total_duration):
    font = cv2.FONT_HERSHEY_SIMPLEX
    text_video = f"Video-Zeit: {video_time:.2f}s"
    text_audio = f"Audio-Zeit: {audio_time:.2f}s"
    text_position = f"Position: {position}/{total_duration} Frames"
    
    # Zeige die Video- und Audio-Timestamps auf dem Video-Frame an
    cv2.putText(frame, text_video, (10, 30), font, 1, (0, 255, 0), 2, cv2.LINE_AA)
    cv2.putText(frame, text_audio, (10, 70), font, 1, (0, 255, 0), 2, cv2.LINE_AA)
    cv2.putText(frame, text_position, (10, 110), font, 1, (0, 255, 0), 2, cv2.LINE_AA)

# Funktion zum Vorladen von Videos mit Hardwarebeschleunigung
def preload_video_with_progress(video_path):
    try:
        cap = cv2.VideoCapture(video_path, cv2.CAP_FFMPEG)
        cap.set(cv2.CAP_PROP_HW_ACCELERATION, cv2.VIDEO_ACCELERATION_ANY)

        if not cap.isOpened():
            raise Exception(f"Fehler beim Öffnen des Videos: {video_path}")
        
        fps = cap.get(cv2.CAP_PROP_FPS)
        if fps == 0:
            fps = 30  # Fallback, falls FPS nicht verfügbar ist

        total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))

        return cap, fps, total_frames
    except Exception as e:
        logging.error("Fehler beim Vorladen des Videos: %s", str(e))
        logging.error(traceback.format_exc())
        return None, None, None

# Funktion, um das Video und den Audio-Player synchron abzuspielen
def play_video(video_path, cap, fps, total_frames):
    global paused, fullscreen, next_video_requested, seek_value, seekbar_created, current_video_path, sync_issue_start_time

    try:
        player = MediaPlayer(video_path)

        cv2.namedWindow("Video abspielen", cv2.WND_PROP_FULLSCREEN)
        time.sleep(0.1)
        if fullscreen:
            cv2.setWindowProperty("Video abspielen", cv2.WND_PROP_FULLSCREEN, cv2.WINDOW_FULLSCREEN)

        cv2.setMouseCallback("Video abspielen", mouse_callback, param=player)

        # Seekbar erstellen, wenn noch nicht erstellt
        if not seekbar_created:
            cv2.createTrackbar('Position', 'Video abspielen', 0, total_frames, on_trackbar)
            seekbar_created = True
        else:
            cv2.setTrackbarMax('Position', 'Video abspielen', total_frames)
            cv2.setTrackbarPos('Position', 'Video abspielen', 0)

        frame_duration = 1000 / fps  # Millisekunden pro Frame

        while cap.isOpened():
            if not paused:
                if seek_value != int(cap.get(cv2.CAP_PROP_POS_FRAMES)):
                    cap.set(cv2.CAP_PROP_POS_FRAMES, seek_value)
                    new_audio_time = seek_value / fps
                    player.seek(new_audio_time, relative=False)  # Setze die Audio-Position

                ret, frame = cap.read()
                if not ret:
                    next_video_requested = True
                    break

                # Bildschirmauflösung holen
                screen_res = (cv2.getWindowImageRect("Video abspielen")[2], cv2.getWindowImageRect("Video abspielen")[3])

                # Berechnung des Seitenverhältnisses des Videos
                video_aspect_ratio = frame.shape[1] / frame.shape[0]
                screen_aspect_ratio = screen_res[0] / screen_res[1]

                if video_aspect_ratio > screen_aspect_ratio:
                    # Das Video ist breiter als der Bildschirm, skalieren auf Bildschirmbreite
                    scale_width = screen_res[0] / frame.shape[1]
                    window_width = screen_res[0]
                    window_height = int(frame.shape[0] * scale_width)
                else:
                    # Das Video ist höher als der Bildschirm, skalieren auf Bildschirmhöhe
                    scale_height = screen_res[1] / frame.shape[0]
                    window_width = int(frame.shape[1] * scale_height)
                    window_height = screen_res[1]

                frame_resized = cv2.resize(frame, (window_width, window_height))

                # Zentrieren des Videos mit schwarzen Balken
                x_offset = (screen_res[0] - window_width) // 2
                y_offset = (screen_res[1] - window_height) // 2

                black_background = cv2.copyMakeBorder(
                    frame_resized, y_offset, y_offset, x_offset, x_offset, cv2.BORDER_CONSTANT, value=[0, 0, 0]
                )

                video_time = cap.get(cv2.CAP_PROP_POS_MSEC) / 1000.0
                audio_time = player.get_pts()

               # Synchronitätsprüfung: Video anpassen, falls Unterschied > 45ms
                time_diff = (video_time - audio_time) * 1000  # in Millisekunden

                if abs(time_diff) > 0.1:  # Wenn die Differenz größer als 45 ms ist
                    if time_diff < 0:
                        # Video ist zu langsam, Frames überspringen
                        while abs(time_diff) > 40:
                            ret, frame = cap.read()  # Lese mehr Frames, um Video zu beschleunigen
                            video_time = cap.get(cv2.CAP_PROP_POS_MSEC) / 1000.0
                            time_diff = (video_time - audio_time) * 1000
                    else:
                        # Video ist zu schnell, verlangsamen
                        time.sleep(abs(time_diff) / 1000.0)  # Warte, um Video zu verlangsamen
                
                # Synchronisations-Problembehandlung
                if abs(time_diff) > 100:  # Zeitdifferenz zu groß
                    if sync_issue_start_time is None:
                        sync_issue_start_time = time.time()  # Timer starten
                    elif time.time() - sync_issue_start_time > 3:  # Problem hält seit 5 Sekunden an
                        print("Synchronisation nicht möglich, zurücksetzen auf den aktuellen Frame...")
                        
                        # Den aktuellen Frame erhalten
                        current_frame = int(cap.get(cv2.CAP_PROP_POS_FRAMES))
                        
                        # Setze das Video auf den aktuellen Frame zurück
                        cap.set(cv2.CAP_PROP_POS_FRAMES, current_frame)
                        
                        # Berechne die neue Audio-Zeit auf Grundlage des aktuellen Frames
                        new_audio_time = current_frame / fps
                        player.seek(new_audio_time, relative=False)  # Setze die Audio-Position auf den aktuellen Frame
                        
                        # Timer zurücksetzen
                        sync_issue_start_time = None
                else:
                    sync_issue_start_time = None  # Timer zurücksetzen, wenn das Problem behoben ist

                display_sync_info(black_background, video_time, audio_time, int(cap.get(cv2.CAP_PROP_POS_FRAMES)), total_frames)
                cv2.imshow("Video abspielen", black_background)

                current_frame = int(cap.get(cv2.CAP_PROP_POS_FRAMES))
                cv2.setTrackbarPos('Position', 'Video abspielen', current_frame)

                if current_frame >= total_frames - 1 or abs(audio_time - total_frames / fps) < 0.1:
                    next_video_requested = True
                    break

            audio_frame, val = player.get_frame()

            if audio_frame is not None:
                frame_pts = audio_frame[1]
                audio_delay = frame_pts - player.get_pts()
                if audio_delay > 0:
                    time.sleep(audio_delay / 1000)

            key = cv2.waitKey(int(frame_duration)) & 0xFF

            if key == ord(' '):
                paused = not paused
                player.toggle_pause()

            elif key == 27:
                fullscreen = not fullscreen
                if fullscreen:
                    cv2.setWindowProperty("Video abspielen", cv2.WND_PROP_FULLSCREEN, cv2.WINDOW_FULLSCREEN)
                else:
                    cv2.setWindowProperty("Video abspielen", cv2.WND_PROP_FULLSCREEN, cv2.WINDOW_NORMAL)

            if next_video_requested:
                break

            if cv2.getWindowProperty("Video abspielen", cv2.WND_PROP_VISIBLE) < 1:
                sys.exit(0)

        player.close_player()
        cap.release()

        return True
    except Exception as e:
        logging.error("Fehler beim Abspielen des Videos: %s", str(e))
        logging.error(traceback.format_exc())
        return False

# Funktion zum Löschen der Datei
def delete_previous_video(previous_video_path):
    if previous_video_path and os.path.exists(previous_video_path):
        try:
            time.sleep(1)
            os.remove(previous_video_path)
            print(f"Gelöscht: {previous_video_path}")
        except Exception as e:
            logging.error(f"Fehler beim Löschen von {previous_video_path}: {str(e)}")
            logging.error(traceback.format_exc())

# Hauptschleife zum Abspielen aller Videos
previous_video_path = None

for i, video_path in enumerate(video_files):
    try:
        current_video_path = video_path
        cap, fps, total_frames = preload_video_with_progress(video_path)
        if cap is not None:
            print(f"\nSpiele Video {i + 1} von {len(video_files)} ab: {video_path}")
            play_video(video_path, cap, fps, total_frames)

            if next_video_requested:
                next_video_requested = False
                time.sleep(0.1)
                delete_previous_video(previous_video_path)

            previous_video_path = video_path

    except Exception as e:
        logging.error("Hauptschleifenfehler: %s", str(e))
        logging.error(traceback.format_exc())
Benutzeravatar
sparrow
User
Beiträge: 4401
Registriert: Freitag 17. April 2009, 10:28

'gobal' hat in einem Programm nichts verloren. Man verwendet keine globalen Variablen.
In deinen Funktionen ist das aber der Fall.
Es gilt: Eine Funktion bekommt alles, was sie zum Arbeiten braucht als Parameter und gibt das Ergebnis mit return zurück.
Wenn man einen State halten muss, dann ist das Kapseln in ein Objekt möglicherweise sinnvoll.

Auf Modulebene (also nicht eingerückt) gehören: die Shebang, die Importe, die Definition von Funktionen, Klassen und Konstanten (Konstanten zeichnen sich dadurch aus, dass sich ihr Wert nie ändert).

Der Skelleton für ein Python-Programm sieht immer wie folgt aus:

Code: Alles auswählen

def main():
    ...

if __name__ == "__main__":
    main()
Dein Hauptpgrogramm startet dann in main.

Das wären die ersten Schritte um dein Programm auzuräumen.
Im Moment erscheinen irgendwelche Namen magisch in deinen Funktionen. Und das schreckt mich davon ab, das näher anzusehen.

Edit: Das sieht mir übrigens sehr nach ChatGPT-Gestückel aus.
Benutzeravatar
__blackjack__
User
Beiträge: 13703
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@LDN: Auf Modulebene sollte nur Code stehen der Konstanten, Funktionen, und Klassen definiert. Das Hauptprogramm steht üblicherweise in einer Funktion die `main()` heisst.

``global`` hat in einem sauberen Programm nichts zu suchen. Funktionen und Methoden bekommen alles was sie ausser Konstanten benötigen als Argument(e) übergeben. Wenn man sich Zustand über Aufrufe hinweg merken muss, dann verwendet man dafür Objekte.

Man verliert bei globalen Variablen schnell die Übersicht und muss immer das ganze Programm im Kopf haben um zu wissen welche Variablen wo gebraucht und wo verändert werden. Das fängt dann bei so harmlosen Sachen an, dass man Variablen hat, die unnötig mehrfach initialisiert werden (`previous_video_path`) oder die nirgends wirklich verwendet werden (`current_video_path`), bis zu Fehlern die wirklich schwer zu finden sind. Wie ist das beispielsweise bei `sync_issue_start_time`? Wenn das bei einem Video nicht `None` wird, und dann das nächste Video gestartet wird, dann ist das weiterhin nicht `None`. Ist das okay diesen Wert einfach über Videos hinweg zu benutzen? Ähnliche Frage bei `paused` — das muss soweit ich das sehe nicht immer den Playerstatus entsprechen beziehungsweise wenn dann eher als Seiteneffekt.

Konstantennamen werden per Konvention KOMPLETT_GROSS geschrieben.

In neuem Code sollte man `pathlib` statt `os.path` & Co verwenden.

Statt `logging.error()` und dann immer noch mal den Traceback selber erstellen, verwendet man `logging.exception()`.

Alle Verwendungen von `str()` in dem Programm sind überflüssig. Wenn man ein Objekt in eine Zeichenkette formatiert, wird das schon automatisch nach seiner Zeichenkettendarstellung gefragt.

`enumerate()` kann man den Startwert angeben.

Die Kommentare vor den Funktionen wären sinnvoller Docstrings. Und man muss bei einer Funktionsdefinition nicht noch mal erwähnen, dass es eine Funktion ist.

`play_video()` gibt einen Wahrheitswert zurück, der aber nirgends verwendet wird. Stattdessen sollte da besser zurückgegeben werden, ob das nächste Video angefordert wurde, statt das über eine globale Variable zu lösen.

Man sollte Ausnahmen in der Regel nicht durch besondere Fehlerwerte ersetzen. Ausnahmen wurden unter anderem deswegen erfunden, um besondere Fehlerwerte loszuwerden. Das ist also ein Rückschritt.

Das Video sollte nicht in der Funktion `release()`\d werden, sondern dort wo man es erstellt und übergeben hat.

In dem Programm sollte nicht an so vielen Stellen die gleiche Zeichenkette mit dem Fensternamen und dem Namen der Seekbar stehen. Wenn man das mal ändern will, ist die Gefahr da, das man sich vertippt oder ein Vorkommen vergisst. Das definiert man besser als Konstanten.

Der Code für Vollbild steht mehr als einmal im Quelltext. Das sollte nicht sein. Auch wieder weil solche Redundanzen die Neigung haben nicht ganz gleich zu sein oder zu bleiben über die Entwicklung eines Programms gesehen.

`time.time()` ist für Zeitmessungen nur bedingt geeignet. Dafür gibt es `time.monotonic()`

Die Fenstergrösse in ein Tupel zu tun und das dann gar nicht wirklich zu benutzen, sondern immer nur per Index auf die Komponenten zuzugreifen, macht den Quelltext nicht wirklich lesbarer.

Wenn im Kommentar etwas von 45 ms steht, dann darf im Code nicht 0.1 stehen. Und keine 3 im Code aber 5 im Kommentar. Ein Grund warum man Werte aus dem Code in Kommentaren nicht wiederholen sollte. Wieder Redundanz die zu Problemen führen kann.

Zwischenstand (ungetestet):

Code: Alles auswählen

import logging
import sys
import time
from pathlib import Path

import cv2
from ffpyplayer.player import MediaPlayer

logging.basicConfig(
    level=logging.ERROR,
    format="%(asctime)s:%(levelname)s:%(message)s",
    handlers=[logging.StreamHandler(sys.stdout)],
)

VIDEOS_FOLDER = Path.home() / "Desktop" / "videos"
WINDOW_NAME = "Video abspielen"
SEEKBAR_NAME = "Position"


def display_sync_info(frame, video_time, audio_time, position, total_duration):
    """
    Einblenden der Synchronisierungsinformationen.
    """
    font = cv2.FONT_HERSHEY_SIMPLEX
    color = (0, 255, 0)
    for i, text in enumerate(
        [
            f"Video-Zeit: {video_time:.2f}s",
            f"Audio-Zeit: {audio_time:.2f}s",
            f"Position: {position}/{total_duration} Frames",
        ]
    ):
        cv2.putText(
            frame, text, (10, 30 + i * 40), font, 1, color, 2, cv2.LINE_AA
        )


class Player:
    def __init__(self):
        self.seekbar_created = False
        self._fullscreen = None
        self.fullscreen = True
        self.paused = False
        self.seek_value = 0
        self.next_video_requested = False
        self.sync_issue_start_time = None

    @property
    def fullscreen(self):
        return self._fullscreen

    @fullscreen.setter
    def fullscreen(self, value):
        self._fullscreen = value
        cv2.setWindowProperty(
            WINDOW_NAME,
            cv2.WND_PROP_FULLSCREEN,
            cv2.WINDOW_FULLSCREEN if self.fullscreen else cv2.WINDOW_NORMAL,
        )

    def on_mouse(self, event, _x, _y, _flags, media_player):
        if event == cv2.EVENT_LBUTTONDOWN:
            self.paused = not self.paused
            media_player.toggle_pause()

        elif event == cv2.EVENT_LBUTTONDBLCLK:
            self.fullscreen = not self.fullscreen

        elif event == cv2.EVENT_RBUTTONDOWN:
            self.next_video_requested = True

    def on_seekbar(self, value):
        self.seek_value = value

    def play(self, video_path, video, fps, total_frames):
        """
        Video und den Audio-Player synchron abspielen.
        """
        try:
            media_player = MediaPlayer(video_path)
            try:
                self.next_video_requested = False

                cv2.namedWindow(WINDOW_NAME, cv2.WND_PROP_FULLSCREEN)
                time.sleep(0.1)
                if self.fullscreen:
                    cv2.setWindowProperty(
                        WINDOW_NAME,
                        cv2.WND_PROP_FULLSCREEN,
                        cv2.WINDOW_FULLSCREEN,
                    )

                cv2.setMouseCallback(
                    WINDOW_NAME, self.on_mouse, param=media_player
                )
                if not self.seekbar_created:
                    cv2.createTrackbar(
                        SEEKBAR_NAME,
                        WINDOW_NAME,
                        0,
                        total_frames,
                        self.on_seekbar,
                    )
                    self.seekbar_created = True
                else:
                    cv2.setTrackbarMax(SEEKBAR_NAME, WINDOW_NAME, total_frames)
                    cv2.setTrackbarPos(SEEKBAR_NAME, WINDOW_NAME, 0)

                frame_duration = 1000 / fps  # Millisekunden pro Frame.
                while video.isOpened():
                    if not self.paused:
                        if self.seek_value != int(
                            video.get(cv2.CAP_PROP_POS_FRAMES)
                        ):
                            video.set(cv2.CAP_PROP_POS_FRAMES, self.seek_value)
                            media_player.seek(
                                self.seek_value / fps, relative=False
                            )

                        is_ok, frame = video.read()
                        if not is_ok:
                            self.next_video_requested = True
                            break

                        frame_width, frame_height = (
                            frame.shape[1],
                            frame.shape[0],
                        )
                        _, _, screen_width, screen_height = (
                            cv2.getWindowImageRect(WINDOW_NAME)
                        )
                        video_aspect_ratio = frame_width / frame_height
                        screen_aspect_ratio = screen_width / screen_height

                        if video_aspect_ratio > screen_aspect_ratio:
                            scale_width = screen_width / frame_width
                            window_width = screen_width
                            window_height = int(frame_height * scale_width)
                        else:
                            scale_height = screen_height / frame_height
                            window_width = int(frame_width * scale_height)
                            window_height = screen_height
                        #
                        # Zentrieren des Videos mit schwarzen Balken.
                        #
                        x_offset = (screen_width - window_width) // 2
                        y_offset = (screen_height - window_height) // 2
                        frame = cv2.copyMakeBorder(
                            cv2.resize(frame, (window_width, window_height)),
                            y_offset,
                            y_offset,
                            x_offset,
                            x_offset,
                            cv2.BORDER_CONSTANT,
                            value=(0, 0, 0),
                        )

                        video_time = video.get(cv2.CAP_PROP_POS_MSEC) / 1000
                        audio_time = media_player.get_pts()
                        #
                        # Synchronitätsprüfung: Video anpassen, falls
                        # Unterschied zu gross.
                        #
                        time_diff = (
                            video_time - audio_time
                        ) * 1000  # in Millisekunden

                        if abs(time_diff) > 0.1:
                            if time_diff < 0:
                                # Video ist zu langsam, Frames überspringen
                                while abs(time_diff) > 40:
                                    _is_ok, _frame = video.read()
                                    video_time = (
                                        video.get(cv2.CAP_PROP_POS_MSEC) / 1000
                                    )
                                    time_diff = (
                                        video_time - audio_time
                                    ) * 1000
                            else:
                                # Video ist zu schnell, verlangsamen
                                time.sleep(abs(time_diff) / 1000)
                        #
                        # Synchronisations-Problembehandlung
                        #
                        elif abs(time_diff) > 100:
                            if self.sync_issue_start_time is None:
                                self.sync_issue_start_time = time.monotonic()
                            elif (
                                time.monotonic() - self.sync_issue_start_time
                                > 3
                            ):
                                print(
                                    "Synchronisation nicht möglich,"
                                    " zurücksetzen auf den aktuellen Frame..."
                                )
                                current_frame = int(
                                    video.get(cv2.CAP_PROP_POS_FRAMES)
                                )
                                video.set(
                                    cv2.CAP_PROP_POS_FRAMES, current_frame
                                )
                                media_player.seek(
                                    current_frame / fps, relative=False
                                )
                                self.sync_issue_start_time = None
                        else:
                            self.sync_issue_start_time = None

                        display_sync_info(
                            frame,
                            video_time,
                            audio_time,
                            int(video.get(cv2.CAP_PROP_POS_FRAMES)),
                            total_frames,
                        )
                        cv2.imshow(WINDOW_NAME, frame)

                        current_frame = int(video.get(cv2.CAP_PROP_POS_FRAMES))
                        cv2.setTrackbarPos(
                            SEEKBAR_NAME, WINDOW_NAME, current_frame
                        )

                        if (
                            current_frame >= total_frames - 1
                            or abs(audio_time - total_frames / fps) < 0.1
                        ):
                            self.next_video_requested = True
                            break

                    audio_frame, _ = media_player.get_frame()

                    if audio_frame is not None:
                        frame_pts = audio_frame[1]
                        audio_delay = frame_pts - media_player.get_pts()
                        if audio_delay > 0:
                            time.sleep(audio_delay / 1000)

                    key = cv2.waitKey(int(frame_duration)) & 0xFF
                    if key == ord(" "):
                        self.paused = not self.paused
                        media_player.toggle_pause()
                    elif key == 27:
                        self.fullscreen = not self.fullscreen

                    if self.next_video_requested:
                        break
                    #
                    # TODO `sys.exit()` hat hier nichts zu suchen.  Da muss eine
                    #   andere Lösung gefunden werden.
                    #
                    if (
                        cv2.getWindowProperty(
                            WINDOW_NAME, cv2.WND_PROP_VISIBLE
                        )
                        < 1
                    ):
                        sys.exit(0)
            finally:
                media_player.close_player()

        except Exception as error:
            logging.exception("Fehler beim Abspielen des Videos: %s", error)

        return self.next_video_requested


def open_video(video_path):
    """
    Öffnen des Videos mit Hardwarebeschleunigung.
    """
    video = cv2.VideoCapture(video_path, cv2.CAP_FFMPEG)
    video.set(cv2.CAP_PROP_HW_ACCELERATION, cv2.VIDEO_ACCELERATION_ANY)

    if not video.isOpened():
        raise RuntimeError(f"Fehler beim Öffnen des Videos: {video_path}")

    fps = video.get(cv2.CAP_PROP_FPS) or 30
    total_frames = int(video.get(cv2.CAP_PROP_FRAME_COUNT))

    return video, fps, total_frames


def delayed_delete(path):
    if path.exists():
        try:
            time.sleep(1)
            path.unlink()
            print(f"Gelöscht: {path}")
        except Exception as error:
            logging.exception("Fehler beim Löschen von %s: %s", path, error)


def main():
    try:
        video_paths = list(VIDEOS_FOLDER.glob("*.mp4"))
        if not video_paths:
            print(
                f"Fehler: Keine MP4-Dateien im Ordner {VIDEOS_FOLDER} gefunden."
            )
            sys.exit(1)
    except Exception as error:
        logging.exception("Fehler beim Suchen von Videodateien: %s", error)
        sys.exit(1)

    player = Player()
    previous_video_path = None
    for i, video_path in enumerate(video_paths, 1):
        try:
            try:
                video, fps, total_frames = open_video(video_path)
            except Exception as error:
                logging.exception("Fehler beim Vorladen des Videos: %s", error)
            else:
                try:
                    print(
                        f"\nSpiele Video {i} von {len(video_paths)} ab:"
                        f" {video_path}"
                    )
                    if player.play(video_path, video, fps, total_frames):
                        time.sleep(0.1)
                        if previous_video_path:
                            delayed_delete(previous_video_path)

                    previous_video_path = video_path
                finally:
                    video.release()

        except Exception as error:
            logging.exception("Hauptschleifenfehler: %s", error)


if __name__ == "__main__":
    main()
Die Abspielfunktion/-methode ist viel zu lang und zu tief verschachtelt. Die macht zu viel. Da kann man sinnvoll Teilaufgaben heraus ziehen.

`sys.exit()` hat in dieser Funktion/Methode nichts zu suchen. Man beendet nicht in irgendwelchen Funktionen/Methoden einfach das gesamte Programm, wo niemand mit rechnet.

`fps` und `total_frames` in der Funktion zum Öffnen unterzubringen ist IMHO nicht so wirklich sinnvoll wenn man diese Informationen erst woanders braucht und die bis dort hin, mit dem Video-Objekt weiter reicht, auch dem man die Infos auch dort holen kann, wo man sie dann letztendlich braucht. Man könnte für das Video-Objekt auch noch eine eigene Klasse schreiben um die hässlichen cv2-Funktionsaufrufe mit den Konstanten los zu werden beziehungsweise in dieser Klasse zu verstecken. Für das Fenster mit der Trackbar würde das vielleicht auch Sinn machen. Vielleicht sogar für den `MediaPlayer` um eine API zu bekommen die besser zur cv2-API passt, beziehungsweise zum Wrapper-Objekt dafür. Zum Beispiel das man sich mal festlegt ob Zeitstempel nun in Sekunden oder Millisekunden sein sollen und das dann über diese Wrapper-Objekte regelt, statt im Code andauernd mal 1000 oder durch 1000 stehen zu haben.
„Incorrect documentation is often worse than no documentation.“ — Bertrand Meyer
Antworten