You record a clip on your iPhone and try to set it as your Telegram video avatar. Telegram says nothing. No error, no format warning. The upload completes and your avatar doesn't change.
I spent an embarrassing amount of time debugging this.
The Silent Rejection
Modern iPhones shoot HEVC (H.265) by default since iOS 11. It's a better codec. Files are smaller. Most apps handle it fine.
Telegram's video avatar uploader does not. It silently rejects any video that isn't H.264. No error dialog, no format hint. You just never see the avatar update.
The same problem hits Android users shooting in certain formats, people sending screen recordings with weird pixel formats, and anyone whose video has an audio track attached. Telegram's avatar uploader is picky in ways it doesn't advertise.
What Telegram's Video Avatar Spec Actually Requires
After testing a bunch of variations, here are the hard limits:
- Container: MP4
- Codec: H.264 (libx264), yuv420p pixel format
- Resolution: exactly 800x800 pixels, square
- Duration: 10 seconds max
- File size: 2 MB max
- Audio: must be absent (strip it with
-an)
The resolution requirement is the most annoying one. Telegram doesn't accept non-square videos, and it doesn't accept sizes other than 800x800. You have to crop and scale. If you get the pixel format wrong (some encoders output yuv444p), Telegram rejects that silently too.
The ffmpeg Pipeline
ffmpeg handles all of this. The tricky part is getting a square crop without distorting the image.
For videos with black bars (letterboxed widescreen or pillarboxed portrait), use a two-pass approach. First, detect the crop area:
ffmpeg -i input.mov \
-vf "cropdetect=24:16:0" \
-f null - 2>&1 | grep crop
This outputs something like crop=1080:1080:0:0. Plug that into the real encode:
ffmpeg -i input.mov \
-vf "crop=1080:1080:0:0,scale=800:800,format=yuv420p" \
-c:v libx264 \
-preset fast \
-crf 28 \
-t 10 \
-an \
-movflags +faststart \
output.mp4
Flags worth explaining:
-
-t 10clips at 10 seconds. If the source is longer, it takes the first 10 seconds. -
-anstrips the audio track. Don't skip this. -
-movflags +faststartmoves the MP4 moov atom to the start of the file so Telegram can begin reading before the download completes. -
crf 28is aggressive compression. Most phone videos at 800x800 land under 2 MB at this setting. If yours doesn't, trycrf 30or trim to 6 seconds.
For videos without black bars, skip cropdetect and use pad-to-square directly:
scale=800:800:force_original_aspect_ratio=decrease,pad=800:800:(ow-iw)/2:(oh-ih)/2
Wiring It Into aiogram 3
The core handler receives a video, animation, or document, runs ffmpeg, checks the output size, and sends back the processed MP4:
import asyncio
import os
import tempfile
from aiogram import Router, F
from aiogram.types import Message, BufferedInputFile
router = Router()
async def run_ffmpeg(input_path: str, output_path: str) -> bool:
vf = (
"scale=800:800:force_original_aspect_ratio=decrease,"
"pad=800:800:(ow-iw)/2:(oh-ih)/2,format=yuv420p"
)
cmd = [
"ffmpeg", "-y", "-i", input_path,
"-vf", vf,
"-c:v", "libx264", "-preset", "fast", "-crf", "28",
"-t", "10", "-an", "-movflags", "+faststart",
output_path
]
proc = await asyncio.create_subprocess_exec(
*cmd,
stdout=asyncio.subprocess.DEVNULL,
stderr=asyncio.subprocess.DEVNULL,
)
await proc.wait()
return proc.returncode == 0
@router.message(F.video | F.animation | F.document)
async def handle_video(message: Message, bot):
msg = await message.answer("Converting...")
with tempfile.TemporaryDirectory() as tmp:
input_path = os.path.join(tmp, "input")
output_path = os.path.join(tmp, "output.mp4")
media = message.video or message.animation or message.document
file = await bot.get_file(media.file_id)
await bot.download_file(file.file_path, input_path)
ok = await run_ffmpeg(input_path, output_path)
if not ok:
await msg.edit_text("ffmpeg failed. Try a shorter or smaller video.")
return
size = os.path.getsize(output_path)
if size > 2 * 1024 * 1024:
await msg.edit_text(
f"Output is {size // 1024}KB, still over the 2MB limit. "
"Try trimming to 6 seconds or less."
)
return
with open(output_path, "rb") as f:
data = f.read()
await message.answer_video(
BufferedInputFile(data, filename="avatar.mp4"),
caption="Set this as your Telegram video avatar: Profile -> Set photo -> Video"
)
await msg.delete()
This is simplified from what runs in production. The real bot adds a per-user queue to avoid concurrent ffmpeg processes, a file size check before download (refusing anything over 50 MB), and rate limiting.
Packaging It as @liveavabot
I've been running this as LiveAvaBot for a few months. It handles iPhone .mov files, Android .mp4, GIFs, screen recordings, pretty much whatever gets sent. HEVC conversion is the primary use case, but it also quietly fixes other rejection scenarios: wrong resolution, audio track present, bad pixel format.
127 users total, 10 conversions yesterday. It's not going viral, but people who need it really need it. There's no convenient alternative for HEVC conversion that doesn't require installing software.
The stack: Python 3.12, aiogram 3.x, ffmpeg 6, on a small VPS. No GPU needed. ffmpeg is doing all the actual work.
Edge Cases and Gotchas
4K source files. A 10-second 4K HEVC clip can be 300+ MB. The bot checks file size before downloading and refuses anything over 50 MB with an explanation.
Transparent GIFs. Animated GIFs sometimes use transparency. Converting to yuv420p loses the alpha channel and transparent areas turn black. The bot warns about this but doesn't auto-fix it.
iPhone slo-mo video. Slo-mo clips use variable frame rate. ffmpeg handles them fine, but the output can be longer than the source clip. A 3-second 240fps clip after re-encoding at normal speed might run 8+ seconds. The bot clips to 10 seconds, which sometimes cuts the action short.
Widescreen content. Pad-to-square leaves black bars on wide landscape videos. Center-crop would look better for most widescreen content. That's on the to-do list, not shipped yet.
Built by me. Bot: LiveAvaBot.
Top comments (0)