The problem: iPhone videos fail silently as Telegram avatars
Telegram added video avatars a while back. You can set a short looping clip as your profile picture or channel icon. Nice feature. Except when I tried to upload a video shot on my iPhone, Telegram just rejected it without an error message. The file picker closed, nothing happened.
The culprit is HEVC (H.265). iPhones record in HEVC by default since iOS 11. Telegram's clients only accept H.264 for video avatars. There is no in-app transcoder, no "convert and try again" dialog. The upload just dies.
I built @LiveAvaBot to fix this. Send any video or GIF, get back a Telegram-compliant video avatar. Here is how the pipeline works.
What Telegram actually accepts for video avatars
The spec is strict and poorly documented. After reading BotAPI docs, Telegram Desktop source hints, and doing a lot of trial and error, this is what works:
- Codec: H.264 (libx264), yuv420p pixel format.
- Container: MP4 with faststart flag (moov atom at the front).
- Resolution: exactly 800x800, square.
- Duration: up to 10 seconds.
- File size: up to 2 MB.
- Audio: must be stripped. Telegram rejects clips with an audio stream even when the stream is silent.
- Framerate: 25 to 30 fps works best. Higher pushes file size over 2 MB.
If you miss any of these, the upload fails. HEVC is the most common blocker, but I have also seen 1080p 60fps clips rejected because they were 3 MB.
Fixing it with ffmpeg: cropdetect, scale, re-encode
The core of the bot is an ffmpeg pipeline. Two passes. First pass runs cropdetect to find the center of the video, so a portrait clip gets cropped to a square around the subject, not around empty space. Second pass does the actual encode.
# Pass 1: find crop box
ffmpeg -ss 0 -t 3 -i input.mov \
-vf "cropdetect=round=2" \
-f null - 2>&1 | grep -oE 'crop=[0-9:]+' | tail -1
# Example output: crop=1080:1080:0:420
# That is width:height:x:y
Then feed that crop into the real encode:
ffmpeg -i input.mov \
-vf "crop=1080:1080:0:420,scale=800:800:flags=lanczos,format=yuv420p" \
-c:v libx264 -preset veryfast -crf 28 \
-t 10 -r 30 \
-movflags +faststart \
-an \
-y output.mp4
Quick breakdown of the flags that matter:
-
crop=W:H:X:Yapplies the box from cropdetect. -
scale=800:800:flags=lanczosresizes to the exact square Telegram wants. Lanczos keeps edges sharp. -
format=yuv420pforces 8-bit chroma subsampling. Without this, iPhone HEVC comes through as yuv420p10le and some decoders choke. -
-c:v libx264 -preset veryfast -crf 28is the sweet spot for file size vs quality on short clips. CRF 28 gets most clips under 2 MB. -
-t 10caps duration. -
-movflags +faststartmoves the moov atom to the front so Telegram can parse metadata before downloading the full file. -
-androps audio.
If the file still comes out over 2 MB after this (long high-detail clips sometimes do), the bot does a second pass with CRF 32. Rarely needed in practice.
Wiring it into an aiogram 3 handler
The bot is aiogram 3. Here is the trimmed-down handler:
from aiogram import Router, F
from aiogram.types import Message, FSInputFile
from pathlib import Path
import asyncio, tempfile
router = Router()
@router.message(F.video | F.animation | F.document)
async def convert_to_avatar(message: Message):
file = message.video or message.animation or message.document
if not file:
return
with tempfile.TemporaryDirectory() as tmp:
src = Path(tmp) / "in.bin"
dst = Path(tmp) / "out.mp4"
await message.bot.download(file, destination=src)
crop = await detect_crop(src)
ok = await encode(src, dst, crop)
if not ok:
await message.answer("couldn't convert this one, sorry")
return
await message.answer_video(
FSInputFile(dst),
caption="done. upload this as your profile video",
)
async def encode(src: Path, dst: Path, crop: str) -> bool:
cmd = [
"ffmpeg", "-y", "-i", str(src),
"-vf", f"{crop},scale=800:800:flags=lanczos,format=yuv420p",
"-c:v", "libx264", "-preset", "veryfast", "-crf", "28",
"-t", "10", "-r", "30",
"-movflags", "+faststart", "-an",
str(dst),
]
proc = await asyncio.create_subprocess_exec(
*cmd,
stdout=asyncio.subprocess.DEVNULL,
stderr=asyncio.subprocess.DEVNULL,
)
return (await proc.wait()) == 0
A few things worth noting.
The handler accepts F.video, F.animation (that is GIFs, which Telegram sends as MP4), and F.document (users often send videos as uncompressed files to preserve quality). All three paths go through the same pipeline.
detect_crop is a separate async function that runs the cropdetect pass and parses the last crop=... line from stderr. It falls back to a center crop when detection fails on blank or very dark clips.
The temp directory gets cleaned up automatically when the with block exits. Even if ffmpeg crashes.
Packaging as @liveavabot
I wrapped the pipeline in a small aiogram 3 bot, added a SQLite table for conversion counts, and shipped it on a cheap VPS. Running for a few months now, around 437 users, no incidents. ffmpeg is doing the heavy lifting here, I just wrote the glue and handled the edge cases.
If you want to try it on your own clips: https://t.me/LiveAvaBot?start=devto_article_20261001
The bot is free. No ads, no sign-up. Send a video, get back an MP4 you can set as your profile picture or channel avatar.
Edge cases and what is next
A few things I hit along the way.
GIFs with palette artifacts. Telegram sends animated GIFs as MP4, but some clients re-encode them with weird palettes. Adding format=yuv420p after scale fixes most of them.
Portrait vs landscape. Portrait clips need a vertical center crop around the subject. Cropdetect handles this if there are letterbox bars. If not, I default to a center square and accept that some content gets cut off.
Very short clips. Anything under 1 second loops weirdly as an avatar. I pad to 3 seconds by slowing playback with setpts=3*PTS for clips under that threshold.
4K source. Have not tackled this yet. The resize works, but encoding time blows past the handler's 60-second timeout on cheap VPS hardware. On the todo list.
Built by me, @liveavabot: https://t.me/LiveAvaBot?start=devto_article_20261001
Happy to answer questions in the comments if you are building something similar.
Top comments (0)