Skip to content

hitvid v1.2.0

Latest

Choose a tag to compare

@caokai674 caokai674 released this 08 Aug 02:56
· 10 commits to main since this release
c5ff1c4

Die Go-Implementierung ist der Shell-Version in fast jeder Hinsicht architektonisch überlegen. Die primäre Empfehlung ist, die Go-Version als primäre Codebasis zu übernehmen und weiterzuentwickeln, während das Shell-Skript als Prototyp betrachtet wird. Die Go-Version bietet eine solide Grundlage für hohe Leistung, Stabilität und zukünftige Erweiterbarkeit. Die folgenden Abschnitte beschreiben die Stärken und Schwächen beider Versionen und schlagen konkrete Verbesserungen für die Go-Implementierung vor.


1. Vergleichende Analyse

1.1 Architektur und Design

  • Go-Implementierung (hitvid_Go):

    • Stärken:
      • Struktur: Nutzt eine kompilierte, statisch typisierte Sprache, die für mehr Leistung und Typsicherheit sorgt.
      • Gleichzeitigkeit: Verwendet idiomatische Go-Konstrukte wie Goroutinen, Kanäle (channels), Mutexes und Bedingungsvariablen (sync.Cond). Dies ermöglicht eine saubere und effiziente Parallelverarbeitung von Frame-Rendering und Benutzereingaben.
      • Zustandsverwaltung: Der Zustand der Anwendung (z. B. Pause, Wiedergabegeschwindigkeit) wird zentral in Variablen verwaltet und durch einen Mutex geschützt, was die Komplexität reduziert und Race Conditions verhindert.
      • IPC (Inter-Process Communication): Gerenderte Frames werden direkt im Arbeitsspeicher (renderedFrames [][]byte) gehalten, was den extrem langsamen Festplatten-I/O des Shell-Skripts vermeidet.
  • Shell-Implementierung (hitvid_Shell):

    • Schwächen:
      • Komplexität: Mit über 600 Zeilen ist das Skript extrem komplex und schwer zu warten. Die Logik für die Prozessverwaltung, Fehlerbehandlung und Gleichzeitigkeit ist verschachtelt und unübersichtlich.
      • Gleichzeitigkeit: Die Parallelverarbeitung wird durch das manuelle Starten und Überwachen von Hintergrundprozessen (ffmpeg, chafa) realisiert. Die Synchronisation erfolgt durch sleep-Befehle und das ständige Überprüfen von Dateiexistenzen (while [ ! -f ... ]), was ineffizient und fehleranfällig ist.
      • IPC: Der gesamte Prozess basiert auf dem Schreiben und Lesen von Tausenden von einzelnen JPEG- und Textdateien auf der Festplatte. Dies ist der größte Leistungsengpass. Auch die Verwendung von /dev/shm (RAM-Disk) löst das grundlegende Problem des dateibasierten IPC nicht vollständig.

1.2 Leistung

  • Go-Implementierung:

    • Rendering-Pipeline: Die Verwendung eines Worker-Pools aus Goroutinen zum Rendern von Frames ist hocheffizient.
    • Overhead: Da es sich um ein kompiliertes Binary handelt, gibt es praktisch keinen Interpreter-Overhead während der Wiedergabe. Berechnungen erfolgen nativ und schnell.
    • Engpass: Der Hauptleistungsengpass ist die Geschwindigkeit von ffmpeg und chafa selbst, nicht die Orchestrierung durch das Go-Programm.
  • Shell-Implementierung:

    • Rendering-Pipeline: Der preload-Modus mit xargs -P ist für die Parallelisierung effektiv. Der stream-Modus mit seinem manuell verwalteten Daemon ist jedoch umständlich und langsam.
    • Overhead: Jeder Aufruf von awk, bc, date, tput und grep in der Wiedergabeschleife erzeugt einen neuen Prozess (fork/exec), was zu einem massiven Leistungsabfall führt.
    • Engpass: Der Festplatten-I/O für die Frame-Dateien ist der mit Abstand größte Engpass und begrenzt die maximal erreichbare Framerate erheblich.

1.3 Robustheit und Fehlerbehandlung

  • Go-Implementierung:

    • Graceful Shutdown: context.Context wird korrekt verwendet, um alle Goroutinen bei Benutzeraktionen (z. B. Beenden, nächstes Video) sauber zu beenden.
    • Fehlerbehandlung: Fehler werden explizit behandelt (if err != nil), was den Code vorhersehbar und robust macht.
    • Ressourcenmanagement: defer-Anweisungen stellen sicher, dass Ressourcen wie temporäre Verzeichnisse und der Terminalzustand zuverlässig bereinigt werden.
  • Shell-Implementierung:

    • Graceful Shutdown: Die cleanup-Funktion, die über trap aufgerufen wird, ist eine gute Vorgehensweise für Shell-Skripte, aber die korrekte Beendigung aller Kind- und Enkelprozesse ist komplex und nicht immer garantiert.
    • Fehlerbehandlung: Die Fehlerbehandlung ist verstreut und verlässt sich auf die Überprüfung von Exit-Codes ($?), was oft zu unbemerkten Fehlern führen kann.

2. Detaillierter Verbesserungsplan für die Go-Implementierung

Die Go-Version ist eine ausgezeichnete Grundlage. Die folgenden Verbesserungen zielen darauf ab, sie noch leistungsfähiger, ressourcenschonender und funktionsreicher zu machen.

2.1 Architektur & Speicherverwaltung

  • Problem: Die aktuelle Implementierung speichert alle gerenderten Frames im RAM (renderedFrames [][]byte). Bei langen oder hochauflösenden Videos kann dies zu einem extrem hohen Speicherverbrauch führen.
  • Empfehlung: Implementierung eines begrenzten Frame-Caches (Ringpuffer).
    • Aktion: Ersetzen Sie das unbegrenzte Slice renderedFrames durch eine Datenstruktur mit fester Größe (z. B. ein Puffer für 300 Frames).
    • Logik:
      1. Der Rendering-Worker schreibt einen neuen Frame in den Puffer.
      2. Der Wiedergabe-Loop liest Frames aus dem Puffer.
      3. Wenn der Puffer voll ist, pausiert der Dispatcher das Einreihen neuer Render-Jobs, bis die Wiedergabe Platz schafft.
      4. Bei einem Suchvorgang (seek) wird der Puffer geleert und mit Frames um den neuen Wiedergabepunkt herum neu befüllt.
    • Vorteil: Drastische Reduzierung des Speicherverbrauchs, wodurch die Anwendung auch bei langen Videos stabil läuft.

2.2 Leistung und Gleichzeitigkeit

  • Problem: Der globale stateMutex wird von mehreren Goroutinen (Wiedergabeschleife, Eingabehandler, Rendering-Worker) gleichzeitig verwendet, was zu potenziellen Latenzen führen kann.
  • Empfehlung: Verwendung von Kanälen für Benutzeraktionen.
    • Aktion: Erstellen Sie einen Kanal für Benutzeraktionen (z. B. userActionChan := make(chan string)).
    • Logik:
      1. Der handleInput-Handler sendet Aktionen wie "pause", "seek_forward", "speed_up" über den Kanal.
      2. Die Hauptwiedergabeschleife (playbackLoop) verwendet eine select-Anweisung, um entweder auf den nächsten Frame-Tick oder auf eine neue Benutzeraktion zu warten.
    • Vorteil: Entkoppelt die Eingabeverarbeitung von der Wiedergabelogik und reduziert die Sperrkonkurrenz, was zu einer reaktionsschnelleren Steuerung führt.

2.3 Fehlerbehandlung und Robustheit

  • Problem: Fehler, die während der ffmpeg-Extraktion auftreten, werden nicht an den Benutzer gemeldet, nachdem der Prozess gestartet wurde.
  • Empfehlung: Verbesserte Fehlerberichterstattung von ffmpeg.
    • Aktion: Überprüfen Sie den Rückgabefehler von ffmpegCmd.Wait() in der Goroutine, die auf das Ende von ffmpeg wartet.
    • Logik: Wenn ffmpegCmd.Wait() einen Fehler zurückgibt, bedeutet dies, dass ffmpeg mit einem Fehlercode beendet wurde. In diesem Fall sollte die Ausgabe, die im ffmpegErr-Puffer gesammelt wurde, im Terminal protokolliert werden, um dem Benutzer eine klare Fehlermeldung zu geben (z. B. "Codec nicht gefunden", "Datei beschädigt").

2.4 Funktionserweiterungen

  • Problem: Die Go-Version bietet keine flexiblen Skalierungsmodi wie das Shell-Skript (fit, fill, stretch).
  • Empfehlung: Implementierung erweiterter Skalierungsoptionen.
    • Aktion: Fügen Sie ein neues Befehlszeilen-Flag hinzu, z. B. -scale <mode>.
    • Logik: Basierend auf dem gewählten Modus muss die -vf-Option (Video-Filter) für ffmpeg dynamisch erstellt werden. Dies erfordert ähnliche Berechnungen wie im Shell-Skript, um das Seitenverhältnis zu berücksichtigen:
      • fit: scale=W:H:force_original_aspect_ratio=decrease
      • fill: scale=W:H:force_original_aspect_ratio=increase,crop=W:H
      • stretch: scale=W:H
    • Vorteil: Gibt dem Benutzer mehr Kontrolle über die Darstellung des Videos.

2.5 Codequalität

  • Problem: Globale Variablen für Konfiguration und Zustand machen den Code schwerer lesbar und testbar.
  • Empfehlung: Verwendung von Konfigurations- und Zustandsstrukturen.
    • Aktion:
      1. Führen Sie eine Config-Struktur ein, die alle Befehlszeilenoptionen enthält.
      2. Führen Sie eine Player-Struktur ein, die den gesamten Wiedergabezustand (Mutex, isPaused, currentFrameIndex usw.) kapselt.
    • Logik: Die main-Funktion initialisiert die Config-Struktur und übergibt sie an die playVideo-Funktion. Diese wiederum erstellt eine Player-Instanz. Zustandsänderungen erfolgen über Methoden auf dem Player-Objekt (z. B. player.Pause(), player.Seek()), die die Sperrung intern verwalten.
    • Vorteil: Verbessert die Kapselung, Lesbarkeit und Wartbarkeit des Codes erheblich.