From 113f41d1d221d68dfaa414f10fbecdb08fd99a04 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Wed, 5 Jan 2022 19:32:58 +0200 Subject: [PATCH] Deduplicate lottieconverter calls in tgs_converter Also fix finding first frame file Fixes #690 Closes #728 --- CHANGELOG.md | 1 + mautrix_telegram/util/tgs_converter.py | 147 +++++++++---------------- 2 files changed, 51 insertions(+), 97 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b6534f1..1068c716 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ * Fixed syncing contacts throwing an error for new accounts. * Fixed migrating from the legacy database if the database schema had been corrupted (e.g. by using 3rd party tools for SQLite -> Postgres migration). +* Fixed converting animated stickers to webm with >33 FPS. # v0.11.0 (2021-12-28) diff --git a/mautrix_telegram/util/tgs_converter.py b/mautrix_telegram/util/tgs_converter.py index 46653d9e..45f2f0d5 100644 --- a/mautrix_telegram/util/tgs_converter.py +++ b/mautrix_telegram/util/tgs_converter.py @@ -19,12 +19,15 @@ from __future__ import annotations from typing import Any, Awaitable, Callable import asyncio.subprocess import logging +import os import os.path import shutil import tempfile from attr import dataclass +from mautrix.util import ffmpeg + log: logging.Logger = logging.getLogger("mau.util.tgs") @@ -48,61 +51,49 @@ def abswhich(program: str | None) -> str | None: lottieconverter = abswhich("lottieconverter") -ffmpeg = abswhich("ffmpeg") + + +async def _run_lottieconverter(args: tuple[str, ...], input_data: bytes) -> bytes: + proc = await asyncio.create_subprocess_exec( + lottieconverter, + *args, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + stdin=asyncio.subprocess.PIPE, + ) + stdout, stderr = await proc.communicate(input_data) + if proc.returncode == 0: + return stdout + else: + err_text = stderr.decode("utf-8") if stderr else f"unknown ({proc.returncode})" + raise ffmpeg.ConverterError(f"lottieconverter error: {err_text}") + if lottieconverter: async def tgs_to_png(file: bytes, width: int, height: int, **_: Any) -> ConvertedSticker: frame = 1 - proc = await asyncio.create_subprocess_exec( - lottieconverter, - "-", - "-", - "png", - f"{width}x{height}", - str(frame), - stdout=asyncio.subprocess.PIPE, - stdin=asyncio.subprocess.PIPE, - ) - stdout, stderr = await proc.communicate(file) - if proc.returncode == 0: - return ConvertedSticker("image/png", stdout) - else: - log.error( - "lottieconverter error: " - + ( - stderr.decode("utf-8") - if stderr is not None - else f"unknown ({proc.returncode})" - ) + try: + converted_png = await _run_lottieconverter( + args=("-", "-", "png", f"{width}x{height}", str(frame)), + input_data=file, ) + return ConvertedSticker("image/png", converted_png) + except ffmpeg.ConverterError as e: + log.error(str(e)) return ConvertedSticker("application/gzip", file) async def tgs_to_gif( file: bytes, width: int, height: int, fps: int = 25, **_: Any ) -> ConvertedSticker: - proc = await asyncio.create_subprocess_exec( - lottieconverter, - "-", - "-", - "gif", - f"{width}x{height}", - str(fps), - stdout=asyncio.subprocess.PIPE, - stdin=asyncio.subprocess.PIPE, - ) - stdout, stderr = await proc.communicate(file) - if proc.returncode == 0: - return ConvertedSticker("image/gif", stdout) - else: - log.error( - "lottieconverter error: " - + ( - stderr.decode("utf-8") - if stderr is not None - else f"unknown ({proc.returncode})" - ) + try: + converted_gif = await _run_lottieconverter( + args=("-", "-", "gif", f"{width}x{height}", str(fps)), + input_data=file, ) + return ConvertedSticker("image/gif", converted_gif) + except ffmpeg.ConverterError as e: + log.error(str(e)) return ConvertedSticker("application/gzip", file) converters["png"] = tgs_to_png @@ -115,62 +106,24 @@ if lottieconverter and ffmpeg: ) -> ConvertedSticker: with tempfile.TemporaryDirectory(prefix="tgs_") as tmpdir: file_template = tmpdir + "/out_" - proc = await asyncio.create_subprocess_exec( - lottieconverter, - "-", - file_template, - "pngs", - f"{width}x{height}", - str(fps), - stdout=asyncio.subprocess.PIPE, - stdin=asyncio.subprocess.PIPE, - ) - _, stderr = await proc.communicate(file) - if proc.returncode == 0: - with open(f"{file_template}00.png", "rb") as first_frame_file: + try: + await _run_lottieconverter( + args=("-", file_template, "pngs", f"{width}x{height}", str(fps)), + input_data=file, + ) + first_frame_name = sorted(os.listdir(tmpdir))[0] + with open(f"{tmpdir}/{first_frame_name}", "rb") as first_frame_file: first_frame_data = first_frame_file.read() - proc = await asyncio.create_subprocess_exec( - ffmpeg, - "-hide_banner", - "-loglevel", - "error", - "-framerate", - str(fps), - "-pattern_type", - "glob", - "-i", - file_template + "*.png", - "-c:v", - "libvpx-vp9", - "-pix_fmt", - "yuva420p", - "-f", - "webm", - "-", - stdout=asyncio.subprocess.PIPE, - stdin=asyncio.subprocess.PIPE, - ) - stdout, stderr = await proc.communicate() - if proc.returncode == 0: - return ConvertedSticker("video/webm", stdout, "image/png", first_frame_data) - else: - log.error( - "ffmpeg error: " - + ( - stderr.decode("utf-8") - if stderr is not None - else f"unknown ({proc.returncode})" - ) - ) - else: - log.error( - "lottieconverter error: " - + ( - stderr.decode("utf-8") - if stderr is not None - else f"unknown ({proc.returncode})" - ) + webm_data = await ffmpeg.convert_path( + input_args=("-framerate", str(fps), "-pattern_type", "glob"), + input_file=f"{file_template}*.png", + output_args=("-c:v", "libvpx-vp9", "-pix_fmt", "yuva420p", "-f", "webm"), + output_path_override="-", + output_extension=None, ) + return ConvertedSticker("video/webm", webm_data, "image/png", first_frame_data) + except ffmpeg.ConverterError as e: + log.error(str(e)) return ConvertedSticker("application/gzip", file) converters["webm"] = tgs_to_webm