The Problem: iPhone Videos Silently Fail
Telegram supports video avatars. The spec: 800x800, H.264, max 10 seconds, max 2 MB, no audio.
iPhones have recorded in HEVC (H.265) since iOS 11. When someone sends an iPhone .mov to Telegram and tries to set it as their profile video, nothing happens. No error. The upload completes and the avatar just doesn't update.
I found this out because a friend spent 20 minutes trying different files before giving up and asking me why it didn't work.
What Telegram Actually Requires
For a video to be accepted as a profile avatar, it needs to meet all of these:
- Codec: H.264 (yuv420p pixel format)
- Resolution: exactly 800x800 pixels
- Duration: 10 seconds maximum
- File size: 2 MB maximum
- Audio: none (a track present causes silent rejection)
The resolution requirement is the tricky part. Videos are rarely square. A portrait selfie might be 1080x1920. You need to crop to 1:1 before scaling, not stretch it.
ffmpeg Does the Heavy Lifting
I wrote a wrapper around ffmpeg to handle the conversion. The command crops to the largest centered square, scales to 800x800, normalizes the pixel format, transcodes to H.264, cuts at 10 seconds, and strips audio.
ffmpeg -i input.mov \
-vf "crop='min(iw,ih)':'min(iw,ih)',scale=800:800,format=yuv420p" \
-c:v libx264 \
-preset fast \
-crf 28 \
-t 10 \
-an \
-movflags +faststart \
output.mp4
What each flag does:
-
crop='min(iw,ih)':'min(iw,ih)'takes the largest centered square crop. No x/y offset means it centers automatically. -
scale=800:800scales to exactly 800x800 after cropping. -
format=yuv420psets the pixel format Telegram expects. -
-c:v libx264forces H.264 output regardless of what the input codec is. -
-crf 28trades a bit of quality for a smaller file. Good enough for 10s at 800x800. -
-t 10hard-cuts at 10 seconds. -
-anstrips audio tracks entirely. -
-movflags +faststartputs the moov atom at the start of the file. Some Telegram clients need this.
For HEVC input, ffmpeg decodes it transparently and -c:v libx264 re-encodes to H.264. That's the core fix.
The aiogram 3 Handler
I built this as a Telegram bot using aiogram 3. The handler receives a video file, runs ffmpeg, and sends back the converted file.
import asyncio, os, tempfile, subprocess
from aiogram import Router, F
from aiogram.types import Message, BufferedInputFile
router = Router()
@router.message(F.video | F.document | F.animation)
async def handle_video(message: Message):
status = await message.answer("Converting...")
file_id = (
message.video.file_id if message.video
else message.animation.file_id if message.animation
else message.document.file_id
)
with tempfile.TemporaryDirectory() as tmpdir:
input_path = os.path.join(tmpdir, "input")
output_path = os.path.join(tmpdir, "output.mp4")
file = await message.bot.get_file(file_id)
await message.bot.download_file(file.file_path, input_path)
cmd = [
"ffmpeg", "-i", input_path,
"-vf", "crop='min(iw,ih)':'min(iw,ih)',scale=800:800,format=yuv420p",
"-c:v", "libx264", "-preset", "fast",
"-crf", "28", "-t", "10", "-an",
"-movflags", "+faststart",
output_path
]
proc = await asyncio.create_subprocess_exec(
*cmd,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL
)
await proc.wait()
if proc.returncode != 0 or not os.path.exists(output_path):
await status.edit_text("Conversion failed. Try a different file.")
return
size = os.path.getsize(output_path)
if size > 2 * 1024 * 1024:
await status.edit_text(
f"File still too large after conversion ({size // 1024} KB). Try a shorter clip."
)
return
with open(output_path, "rb") as f:
data = f.read()
await message.answer_video(
BufferedInputFile(data, "avatar.mp4"),
caption="Set this as your Telegram video avatar."
)
await status.delete()
The 2 MB check after conversion matters because CRF 28 doesn't guarantee a size. Dense footage like screen recordings can still exceed 2 MB at 10 seconds. I retry at CRF 32 if the first pass is too large.
BufferedInputFile holds the whole file in memory. For light traffic this is fine. If you're running at scale, write to disk and use FSInputFile instead.
Running It as @liveavabot
I wrapped the converter into a live bot at https://t.me/LiveAvaBot?start=devto_article_20261023. It handles video, document, and GIF (animation) inputs, rate limits by user, and has a Telegram Stars payment gate for heavy usage.
473 users have tried it so far. The HEVC-to-H.264 path is the most common conversion, triggered by basically any iPhone video. The success rate is around 97%. Failures are mostly corrupt inputs or files over Telegram's 20 MB bot download limit.
The infra is a single VPS with a systemd service. SQLite for state. No external services.
Edge Cases Worth Knowing
Silent rejection without an error. Telegram returns 200 OK when you upload an incompatible avatar. The avatar just doesn't update. I spent a few days thinking the bot was broken before verifying what Telegram actually does with HEVC files on the backend.
Portrait crops cut off faces. My first version took the top-left square of the frame. The crop='min(iw,ih)':'min(iw,ih)' expression without an x/y offset defaults to center, which is much better for portrait selfies.
2 MB failures at CRF 28. This happens with high-motion clips. A retry at CRF 32 or 34 covers most cases. A two-pass encode with a hard size target would be cleaner but CRF retry is simpler and works.
GIF inputs. Animated GIFs sent through Telegram arrive as animation objects, not video. The handler needs to check all three message types. The ffmpeg command works on GIF inputs unchanged.
Built by me: @liveavabot on Telegram, https://t.me/LiveAvaBot?start=devto_article_20261023.
Top comments (0)