Telegram lets you use a short looping video as your profile picture. On the phone app this mostly just works, because the client converts whatever you pick. Everywhere else you need a file that already matches the spec. And if your source clip came off an iPhone, there is a good chance Telegram will refuse it without showing a single error.
I hit this while helping a friend set a video avatar from a clip shot on her iPhone. The upload looked fine, then her old photo just stayed there. No toast, no "unsupported format", nothing. That silent failure annoyed me enough to dig into what Telegram actually accepts, and the digging turned into a small bot.
What Telegram Actually Wants
There is no single docs page that spells this out, so I assembled it from the Telegram Desktop source and a pile of test uploads. A video avatar needs:
- MP4 container with H.264 video
- square aspect, 800x800 is the target
- 10 seconds or less
- no audio stream at all (not muted, removed)
- yuv420p pixel format
- a small file, under 2MB uploads reliably
Miss any of these and the client either rejects the file outright or accepts the upload and quietly ignores it. The second case is the maddening one.
Why iPhone Footage Fails Every Check
Since iOS 11, iPhones record HEVC (H.265) by default, the "High Efficiency" camera setting. Newer models add 10-bit HDR on top, so the pixel format is yuv420p10le instead of yuv420p. The clip is 1920x1080 or 4K, nowhere near square. There is an AAC audio track. And rotation is often stored as a display matrix in metadata instead of baked into the pixels, which some tools honor and some ignore.
So a stock iPhone clip has the wrong codec, the wrong bit depth, the wrong shape, an audio track it shouldn't have, and it might be sideways. Telegram transcodes none of that outside its official mobile clients. It just says no, silently.
The ffmpeg Pipeline That Fixes It
ffmpeg does all the real work here. I run two passes: cropdetect first to find the actual content box (GIFs and screen recordings often ship with letterboxing), then a crop, scale and encode pass.
# pass 1: sample the first seconds, let cropdetect find the content box
ffmpeg -i input.mov -t 3 -vf "cropdetect=24:16:0" -f null - 2>&1 \
| grep -o 'crop=[0-9:]*' | tail -1
# pass 2: centered square crop, scale, transcode, strip audio
ffmpeg -y -i input.mov -t 10 \
-vf "crop='min(iw,ih)':'min(iw,ih)',scale=800:800,format=yuv420p" \
-c:v libx264 -profile:v high -preset slow -crf 26 \
-an -movflags +faststart \
avatar.mp4
The flags that matter:
-
format=yuv420pdownconverts 10-bit HDR footage. Without it, libx264 happily produces 10-bit output that Telegram then rejects. -
-anremoves the audio stream entirely. Muting is not enough, the track has to be gone. -
-t 10hard caps the duration. -
-movflags +faststartmoves the moov atom to the front so the loop starts without buffering. -
-crf 26usually lands a 10 second 800x800 clip around 1.2 to 1.8MB. If the output comes out over 2MB, I re-run with crf bumped by 2 until it fits. Two retries cover almost every real input.
Wiring It Into an aiogram 3 Bot
I wanted a "send video, get avatar back" flow, so aiogram 3 with a single router:
import asyncio
import tempfile
from pathlib import Path
from aiogram import Bot, F, Router
from aiogram.types import FSInputFile, Message
router = Router()
VF = "crop='min(iw,ih)':'min(iw,ih)',scale=800:800,format=yuv420p"
@router.message(F.video | F.animation | (F.document & F.document.mime_type.startswith("video/")))
async def convert(message: Message, bot: Bot):
media = message.video or message.animation or message.document
with tempfile.TemporaryDirectory() as tmp:
src, dst = Path(tmp) / "in", Path(tmp) / "avatar.mp4"
await bot.download(media, destination=src)
proc = await asyncio.create_subprocess_exec(
"ffmpeg", "-y", "-i", str(src), "-t", "10",
"-vf", VF,
"-c:v", "libx264", "-preset", "veryfast", "-crf", "26",
"-an", "-movflags", "+faststart", str(dst),
stdout=asyncio.subprocess.DEVNULL,
stderr=asyncio.subprocess.DEVNULL,
)
await proc.wait()
if proc.returncode != 0 or not dst.exists():
await message.answer("Couldn't convert that one. Is it actually a video file?")
return
await message.answer_video(
FSInputFile(dst),
caption="Done. Save this video, then set it as your profile video in Settings.",
)
A few notes. The Bot API caps downloads at 20MB unless you run a local Bot API server, so big 4K clips need a size check before download. asyncio.create_subprocess_exec keeps the event loop free while ffmpeg runs, so one user's 4K video doesn't block everyone else. The temp directory cleans up both files even when conversion dies halfway.
Shipping It as @liveavabot
I packaged this as @LiveAvaBot. Send it any video or GIF and it replies with an 800x800 H.264 clip that Telegram accepts as a profile video. It runs on a small VPS. ffmpeg is doing the heavy lifting, I mostly wrote plumbing, queueing and retry logic.
219 people have used it so far. Not a huge number, but conversions come through every day, and each one is someone who would otherwise be staring at an upload that fails with no explanation. HEVC off an iPhone is still the most common input I see.
Disclosure: built by me, @liveavabot.
Edge Cases and What's Next
Things that broke in production, in order of discovery:
-
10-bit HDR. The first version skipped
format=yuv420p. Every HDR clip from an iPhone 12 or newer produced output Telegram rejected. It took a while to spot because the files played fine everywhere else. - Rotation metadata. Modern ffmpeg autorotates on decode, but if you compute a crop from the raw coded dimensions you can crop the wrong axis. Trust ffprobe display dimensions.
- Odd dimensions. A 481x361 GIF makes libx264 unhappy. Cropping to min(iw,ih) and scaling to a fixed 800x800 sidesteps the whole class of problems.
-
Variable frame rate. iOS screen recordings sometimes came out longer than 10 seconds after trimming because of VFR weirdness. Putting
-t 10after the input, as an output option, fixed it.
Next up: faster handling of 4K sources (about 20 seconds on my VPS right now) and a way to pick the crop position, because a centered square crop cuts heads off some vertical videos.
If you've fought Telegram's media specs from a bot before, I'd like to hear which one bit you.
Top comments (0)