DEV Community

liveavabot
liveavabot

Posted on

Why iPhone Videos Fail as Telegram Avatars and How to Fix It

The Problem Nobody Warns You About

I recorded a 5-second clip on my iPhone, went to set it as my Telegram profile video, and nothing happened. No error. No feedback. Telegram just silently rejected it.

Turns out, iPhones record in HEVC (H.265) by default since the iPhone 7. Telegram video avatars only accept H.264. There's no warning dialog. The upload looks like it works, then the avatar stays unchanged.

I kept running into this, figured other people did too, and built a Telegram bot that fixes it automatically.

What Telegram Actually Requires

Telegram's docs on video avatar specs are basically nonexistent. After testing dozens of files, here's what I found works:

  • Codec: H.264 (libx264). Not HEVC, not VP9, not AV1.
  • Resolution: Square. 640x640 is the sweet spot for file size.
  • Duration: Under 10 seconds, looped on the profile.
  • File size: Under 2 MB.
  • Audio: None. Telegram strips it anyway, but leaving it in wastes bytes.
  • Pixel format: yuv420p. Anything else (yuv444p, yuv422p) gets rejected.
  • Container: MP4 with the moov atom at the front (faststart).

Miss any single requirement and Telegram silently drops the file. That silent failure is the entire reason this bot exists.

The FFmpeg Pipeline

Here's the actual ffmpeg command the bot runs in production:

ffmpeg -y -v error \
  -i input.mov \
  -t 9 \
  -vf "crop='min(iw,ih)':'min(iw,ih)',scale=640:640:flags=lanczos,fps=30,format=yuv420p" \
  -an \
  -c:v libx264 \
  -profile:v high \
  -level 4.0 \
  -preset medium \
  -crf 23 \
  -maxrate 1400k -bufsize 2800k \
  -pix_fmt yuv420p \
  -movflags +faststart \
  output.mp4
Enter fullscreen mode Exit fullscreen mode

Let me walk through the filter chain.

crop='min(iw,ih)':'min(iw,ih)' takes the shorter dimension and center-crops to a square. Handles portrait (1080x1920) and landscape (1920x1080) the same way. No hardcoded pixel values.

scale=640:640:flags=lanczos downscales with Lanczos resampling instead of the default bilinear. The quality difference is visible when you go from 1080p down to 640px, especially on text or sharp edges.

fps=30,format=yuv420p normalizes frame rate and chroma subsampling. Some screen recordings come in at 24fps or even variable frame rate, which confuses Telegram's player.

For rate control, CRF 23 alone isn't enough. Some high-motion clips (sports, fast panning) exceeded 2 MB at CRF 23. Adding -maxrate 1400k -bufsize 2800k caps bitrate on complex scenes without destroying quality on static ones. The VBV buffer at 2x maxrate gives ffmpeg room to allocate bits where they matter.

-movflags +faststart moves the MP4 moov atom to the beginning of the file. Without this, Telegram can't generate a thumbnail until the entire file downloads.

Wiring It Into Aiogram 3

The bot uses aiogram 3 with async subprocess calls to ffmpeg:

import asyncio
import os
from aiogram import Router, F
from aiogram.types import Message, FSInputFile

router = Router()

@router.message(F.video | F.animation | F.document)
async def handle_video(msg: Message, bot):
    if msg.video:
        file_id = msg.video.file_id
    elif msg.animation:
        file_id = msg.animation.file_id
    else:
        file_id = msg.document.file_id

    file = await bot.get_file(file_id)
    input_path = f"/tmp/{file_id}.mp4"
    output_path = f"/tmp/{file_id}_avatar.mp4"
    await bot.download_file(file.file_path, input_path)

    vf = (
        "crop='min(iw,ih)':'min(iw,ih)',"
        "scale=640:640:flags=lanczos,"
        "fps=30,format=yuv420p"
    )
    proc = await asyncio.create_subprocess_exec(
        "ffmpeg", "-y", "-v", "error",
        "-i", input_path, "-t", "9",
        "-vf", vf, "-an",
        "-c:v", "libx264", "-profile:v", "high",
        "-level", "4.0", "-preset", "medium",
        "-crf", "23", "-maxrate", "1400k",
        "-bufsize", "2800k", "-pix_fmt", "yuv420p",
        "-movflags", "+faststart", output_path,
        stdout=asyncio.subprocess.PIPE,
        stderr=asyncio.subprocess.PIPE,
    )
    _, stderr = await proc.communicate()

    if proc.returncode != 0:
        await msg.reply("Conversion failed. Try a different file.")
        return

    await msg.reply_video(FSInputFile(output_path))
    os.unlink(input_path)
    os.unlink(output_path)
Enter fullscreen mode Exit fullscreen mode

The important decision here: asyncio.create_subprocess_exec instead of subprocess.run. Aiogram runs on asyncio. A blocking ffmpeg call would freeze the entire bot for 5 to 15 seconds per conversion. The async subprocess lets other messages get processed while ffmpeg encodes.

The handler matches F.video | F.animation | F.document because Telegram represents incoming media in three different ways depending on how the user sends it. A video from the camera roll comes as video. A GIF forwarded from a chat comes as animation (actually an MP4, not a real GIF). A file dragged from the file picker comes as document.

Edge Cases That Bit Me

Portrait videos crop faces off. My first version used crop=640:640 with hardcoded dimensions. On a 1080x1920 phone recording, that grabbed a 640x640 chunk from the top-left corner. Switching to min(iw,ih) center-crop fixed it for any aspect ratio.

GIFs are secretly MP4s. Telegram converts GIFs to H.264 MP4 on upload and delivers them as message.animation. The bot needs to handle this type explicitly, or GIF-to-avatar conversions silently fail.

Variable frame rate from screen recordings. Android screen recordings sometimes use variable frame rate. FFmpeg handles this, but Telegram's player stutters on VFR files. Forcing fps=30 normalizes everything.

CRF 23 overflows on action clips. A 9-second skateboarding clip at CRF 23 came out to 2.4 MB. The maxrate/bufsize constraint solved this without needing a two-pass encode, which would double processing time.

Try It

Send any video, GIF, or screen recording to @LiveAvaBot in Telegram. It handles HEVC from iPhones, screen recordings from Android, GIFs from the web. Conversion takes about 5 seconds. You get back a properly formatted MP4 you can set directly as your profile video.

The production bot handles a few more things I didn't cover here (file size validation, duration detection, retry logic for Telegram API rate limits), but the ffmpeg pipeline above is the real one.

Built by me. Try @LiveAvaBot.

Top comments (0)