The problem: iPhone videos silently fail as Telegram avatars
Telegram lets you set a short video as your profile picture. It plays in a loop on your profile page. Neat feature. But if you record a clip on iPhone and try to upload it, Telegram either rejects the upload with no error or shows a spinner forever. No message. Just nothing.
The reason: modern iPhones record in HEVC (H.265), and Telegram's video avatar endpoint only accepts H.264 in an MP4 container. The client does not warn you. The server just drops the upload on the floor.
I hit this exact wall trying to set my own avatar with a clip from my phone. So I built a bot that takes any video or GIF, re-encodes it to Telegram's spec, and sends it back. I named it @liveavabot.
What Telegram actually wants for a video avatar
The Telegram spec for video profile pictures (also called animated avatars) is narrow and mostly undocumented in the Bot API reference. Here is what I pieced together from trial and mobile client source:
- Container: MP4 with the faststart flag (moov atom at the front)
- Video codec: H.264 (avc1), yuv420p pixel format
- Resolution: 800 by 800 pixels, square, no letterbox
- Duration: 3 to 10 seconds
- Frame rate: 30 fps or lower
- Audio: must be removed entirely, not just muted
- File size: under 2 MB, and this is the mean one
Miss any of these and the upload fails silently. The 2 MB cap is the hard part, because 10 seconds of decent-quality 800x800 H.264 usually blows past that if you use default settings.
Solving it with ffmpeg cropdetect and scale
The cropping is the interesting bit. Most user videos are portrait or landscape, not square. If you naive-scale a 1080x1920 clip to 800x800, it looks stretched and awful. You need to crop to a centered square first, then scale down.
ffmpeg has a cropdetect filter that scans a video and tells you the largest non-black rectangle. In practice, for user avatars, I found a simpler expression works better: crop to the shorter side, centered. Here is the actual command the bot runs:
ffmpeg -i input.mov \
-t 10 \
-vf "crop='min(iw,ih)':'min(iw,ih)',scale=800:800,fps=30,format=yuv420p" \
-c:v libx264 -profile:v baseline -level 3.1 \
-preset veryfast -crf 28 \
-maxrate 1500k -bufsize 3000k \
-movflags +faststart \
-an \
output.mp4
Breakdown:
-
-t 10clamps duration to 10 seconds -
crop='min(iw,ih)':'min(iw,ih)'picks the shorter side and cuts a centered square -
scale=800:800resizes to Telegram's target -
fps=30caps frame rate at 30 -
format=yuv420pforces the pixel format Telegram needs -
-profile:v baseline -level 3.1maximizes decoder compatibility on older devices -
-crf 28gives a decent quality-to-size tradeoff -
-maxrate 1500kkeeps peak bitrate under control so the file stays small -
-movflags +faststartmoves the moov atom to the front so it is streamable -
-androps audio completely
The -crf 28 -maxrate 1500k combo is what I settled on after testing a couple hundred clips. Lower CRF means better quality but bigger files. 28 with a 1500k ceiling keeps most 10-second clips under 2 MB without looking terrible. If a clip still comes out too big (happens with high-motion content), the bot re-encodes with CRF 32 as a fallback pass.
The aiogram 3 handler
Wrapping this in a Telegram bot is straightforward with aiogram 3. Here is the core handler that receives a video or animation and returns the converted file:
from aiogram import Router, F
from aiogram.types import Message, FSInputFile
import asyncio, tempfile, pathlib
router = Router()
async def run_ffmpeg(src: pathlib.Path, dst: pathlib.Path) -> None:
cmd = [
"ffmpeg", "-y", "-i", str(src),
"-t", "10",
"-vf",
"crop='min(iw,ih)':'min(iw,ih)',scale=800:800,fps=30,format=yuv420p",
"-c:v", "libx264", "-profile:v", "baseline", "-level", "3.1",
"-preset", "veryfast", "-crf", "28",
"-maxrate", "1500k", "-bufsize", "3000k",
"-movflags", "+faststart",
"-an",
str(dst),
]
proc = await asyncio.create_subprocess_exec(
*cmd,
stdout=asyncio.subprocess.DEVNULL,
stderr=asyncio.subprocess.PIPE,
)
_, err = await proc.communicate()
if proc.returncode != 0:
raise RuntimeError(err.decode(errors="ignore")[:500])
@router.message(F.video | F.animation | F.document)
async def convert(msg: Message) -> None:
file_obj = msg.video or msg.animation or msg.document
if not file_obj:
return
with tempfile.TemporaryDirectory() as tmp:
tmp = pathlib.Path(tmp)
src, dst = tmp / "in.bin", tmp / "out.mp4"
await msg.bot.download(file_obj, destination=src)
await run_ffmpeg(src, dst)
await msg.answer_video(
FSInputFile(dst),
caption="Open Settings, Edit profile, tap avatar to set.",
)
A few things worth noting:
- I accept
F.documenttoo, because iPhone users often send clips as documents when sharing from Files - The temp directory auto-cleans on exit, so no leaked files build up on disk
- The ffmpeg stderr gets truncated to 500 chars if things blow up, keeps error logs sane
Packaging it as @liveavabot
The full bot adds a few things around this core:
- File size check up front (Telegram caps user uploads at 20 MB for bots without the local API server)
- HEVC detection via
ffprobebefore conversion, so I can log how often iPhone clips show up (spoiler: most of them) - A rate limit per user, because someone will always try to hammer it
- Storage of user IDs in SQLite for a rough monthly-active count, no other tracking
Deployed on a small VPS. ffmpeg is doing all the heavy lifting, the Python wrapper is maybe 400 lines. Cost per conversion is negligible, most clips encode in under 3 seconds on 2 vCPUs.
Try it here: https://t.me/LiveAvaBot?start=devto_article_20260815
Edge cases and what is next
Things that still trip the bot:
- 4K HDR videos take forever to encode and sometimes hit the 2 MB ceiling even at CRF 32. I need to add a resolution pre-check and downscale before the main pass.
- Very short clips (under 3 seconds) get rejected by Telegram because they violate the minimum duration. I should loop them automatically instead of erroring out.
- Some Android phones send WebM animations that trigger a weird cropdetect result on the very first frame. Worked around it with
-ss 0.5to skip the first half-second, but it is a hack.
The Telegram spec for animated profile pictures is not fully documented anywhere I could find. I dug most of it out of failed uploads and the mobile client. If you want to build something similar, budget a weekend just for figuring out what the server actually accepts.
Built by me, @liveavabot. Feedback and PRs welcome.
Top comments (0)