From 5f47a4a9dad7852d4faacd7ddb501a9e37f401a6 Mon Sep 17 00:00:00 2001 From: SRHTNIN Date: Wed, 14 Oct 2026 19:55:59 +0200 Subject: [PATCH] Fixed audio embed videos running longer than the audio in AudioEmbed.py --- Backend/AudioEmbed.py | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/Backend/AudioEmbed.py b/Backend/AudioEmbed.py index 1815525..c0c99e5 100644 --- a/Backend/AudioEmbed.py +++ b/Backend/AudioEmbed.py @@ -25,6 +25,9 @@ ApplicationDirectory = Path(__file__).resolve().parent # The browser-tab icon (Frontend/public/Icons), when the audio has no cover art. IconPath = ApplicationDirectory.parent / "Frontend" / "dist" / "Icons" / "Icon.png" FrameSize = 480 +# Part of every cache key: bump it when the videos change, so old ones +# aren't served (2: cut to the audio's exact length). +EncodingVersion = 2 EncodeTimeout = 600 # seconds _Locks: dict[str, threading.Lock] = {} @@ -65,10 +68,19 @@ def _BackgroundColour() -> str: def _CachePath(AudioPath: Path) -> Path: Stat = AudioPath.stat() - Key = hashlib.sha256(f"{AudioPath}|{Stat.st_size}|{Stat.st_mtime_ns}".encode()).hexdigest()[:32] + Key = hashlib.sha256(f"{EncodingVersion}|{AudioPath}|{Stat.st_size}|{Stat.st_mtime_ns}".encode()).hexdigest()[:32] return GetDataDirectory() / "EmbedCache" / f"{Key}.mp4" +def _AudioDuration(AudioPath: Path) -> float | None: + try: + Result = subprocess.run(["ffprobe", "-v", "error", "-show_entries", "format=duration", "-of", "csv=p=0", + str(AudioPath)], capture_output=True, text=True, timeout=60) + return float(Result.stdout.strip()) + except (OSError, subprocess.SubprocessError, ValueError): + return None + + def _ExtractCoverArt(AudioPath: Path, Target: Path) -> bool: """Writes the file's own cover art (if it has any) to Target as a PNG.""" try: @@ -95,11 +107,17 @@ def _Encode(AudioPath: Path, Target: Path) -> None: # A non-square cover gets bars in the theme's background colour. Fit = (f"scale={FrameSize}:{FrameSize}:force_original_aspect_ratio=decrease:flags={Scaling}," f"pad={FrameSize}:{FrameSize}:(ow-iw)/2:(oh-ih)/2:color=0x{Background},format=yuv420p") + # The picture loops forever, so the video is cut to the audio's length. + # -shortest alone overshoots: libx264 buffers ~40 frames ahead, which at + # one frame a second left the picture running 40+ seconds past the + # audio, and Discord wouldn't play that. + Duration = _AudioDuration(AudioPath) + Length = ["-t", f"{Duration:.3f}"] if Duration else [] Command = [ "ffmpeg", "-y", "-loglevel", "error", *Picture, "-i", str(AudioPath), - "-map", "0:v:0", "-map", "1:a:0", "-vf", Fit, "-r", "1", + "-map", "0:v:0", "-map", "1:a:0", "-vf", Fit, "-r", "1", "-g", "10", *_VideoEncoder(), - "-c:a", "aac", "-b:a", "192k", "-shortest", "-movflags", "+faststart", str(Partial), + "-c:a", "aac", "-b:a", "192k", "-shortest", *Length, "-movflags", "+faststart", str(Partial), ] try: subprocess.run(Command, check=True, capture_output=True, timeout=EncodeTimeout)