DEV Community

liveavabot
liveavabot

Posted on

Why iPhone Videos Silently Fail as Telegram Avatars

The Silent Failure

You record a video on your iPhone, try to set it as your Telegram video avatar, and nothing happens. No error message. Telegram just ignores the file.

This is not a Telegram bug. iPhone records in HEVC (H.265) by default since iOS 11. Telegram's video avatar processor expects H.264. When it gets H.265, it silently fails on most clients, or shows a broken thumbnail with no explanation.

I ran into this when testing @liveavabot with different devices. iPhone videos were the main failure case. The fix is a specific ffmpeg pipeline, but getting all the parameters right took more trial and error than I expected.

What Telegram Actually Requires

Telegram's video avatar spec is stricter than most people realize:

  • Codec: H.264, yuv420p color space
  • Resolution: 800x800 (square)
  • Duration: 10 seconds maximum
  • File size: 2MB maximum
  • Audio: must be absent (streams removed, not muted)
  • Container: MP4 with the faststart flag set

The "no audio" requirement is easy to miss. A video with an empty audio stream still fails. You need to remove the audio track with -an, not just mute it.

The faststart flag (-movflags +faststart) moves the moov atom to the front of the file. Without it, Telegram's processor needs to buffer the entire file before it can read the metadata, and it often times out on larger uploads.

The yuv420p requirement matters for compatibility. Some encoders default to yuv444p or other chroma formats. Adding -pix_fmt yuv420p forces the correct subsampling.

The square format is non-negotiable. Telegram displays profile videos cropped to a circle. Any content outside the central square gets cut. Tall portrait videos (9:16) look fine as avatars. Landscape videos (16:9) show only a thin horizontal slice of the original, which is usually not what you want.

The ffmpeg Pipeline

After testing about 30 different iPhone videos, here's the command that handles all of them correctly:

ffmpeg -i input.mp4 \
  -vf "cropdetect=24:16:0,crop=w='iw-mod(iw,2)':h='ih-mod(ih,2)',scale=800:800:force_original_aspect_ratio=decrease,pad=800:800:(ow-iw)/2:(oh-ih)/2:black" \
  -c:v libx264 -profile:v baseline -level 3.0 \
  -pix_fmt yuv420p \
  -movflags +faststart \
  -an \
  -t 10 \
  -fs 2097152 \
  output.mp4
Enter fullscreen mode Exit fullscreen mode

A few things worth explaining:

cropdetect scans the first few frames and detects letterboxing. Some iPhone slow-mo clips have black bars baked into the frame. Without removing them first, the square pad ends up with thin horizontal strips instead of clean black borders.

scale + pad resizes to fit within 800x800 while preserving aspect ratio, then fills the remaining space with black. A 9:16 portrait video becomes 800x800 with side bars. The force_original_aspect_ratio=decrease prevents any stretching.

-profile:v baseline -level 3.0 is belt-and-suspenders for older Telegram clients. The baseline profile has the widest decoder support across platforms.

-fs 2097152 is a hard 2MB cap at the ffmpeg level. Combined with -t 10, this handles most videos. For very high-bitrate content (iPhone 4K ProRes) you may need to add -b:v to explicitly target a lower bitrate before the size limit kicks in.

A Minimal aiogram 3 Handler

Here's the core handler that takes a video, runs it through the pipeline, and returns the converted file:

@router.message(F.video | F.animation | F.document)
async def handle_video(message: Message, bot: Bot) -> None:
    file_obj = message.video or message.animation or message.document
    if not file_obj:
        return

    file_info = await bot.get_file(file_obj.file_id)
    tmp_in = Path(f"/tmp/{file_info.file_id}_in.mp4")
    tmp_out = Path(f"/tmp/{file_info.file_id}_out.mp4")

    try:
        await bot.download_file(file_info.file_path, destination=tmp_in)

        proc = await asyncio.create_subprocess_exec(
            "ffmpeg", "-i", str(tmp_in),
            "-vf",
            "cropdetect=24:16:0,"
            "crop=w='iw-mod(iw,2)':h='ih-mod(ih,2)',"
            "scale=800:800:force_original_aspect_ratio=decrease,"
            "pad=800:800:(ow-iw)/2:(oh-ih)/2:black",
            "-c:v", "libx264", "-profile:v", "baseline", "-level", "3.0",
            "-pix_fmt", "yuv420p",
            "-movflags", "+faststart",
            "-an", "-t", "10", "-fs", "2097152",
            str(tmp_out),
            stdout=asyncio.subprocess.PIPE,
            stderr=asyncio.subprocess.PIPE,
        )
        await proc.communicate()

        if proc.returncode != 0 or not tmp_out.exists():
            await message.reply("Couldn't convert this file. Try a shorter clip.")
            return

        await message.reply_video(
            FSInputFile(tmp_out),
            caption="Ready. Go to your profile, tap the avatar, choose video.",
        )
    finally:
        tmp_in.unlink(missing_ok=True)
        tmp_out.unlink(missing_ok=True)
Enter fullscreen mode Exit fullscreen mode

missing_ok=True on unlink matters. If ffmpeg fails before writing the output file, the finally block was throwing FileNotFoundError and swallowing the real error in the logs.

GIFs from Telegram arrive as .mp4 containers (Telegram converts them server-side). The handler treats them identically to regular videos, which works correctly.

I packaged this as @LiveAvaBot. Send it any video, GIF, or document up to Telegram's 50MB cap and it returns a ready 800x800 H.264 avatar. The bot is at 417 users now. Most discovery happens from people sharing their converted avatars and getting asked how they did it.

Edge Cases Worth Knowing

4K HEVC from iPhone Pro models can exceed 2MB even at 10 seconds. The -fs flag stops the encode mid-stream rather than failing outright, so the output is a valid (but shorter) file. A pre-pass with ffprobe to calculate a safe target bitrate would be cleaner than relying on the size cutoff.

Vertical 4:3 videos from some Android cameras can have a SAR/DAR mismatch. The cropdetect pass helps but doesn't fully solve it. I check the ffprobe output and warn the user when the stored sample aspect ratio doesn't match the display aspect ratio.

Round-trip uploads from Telegram Desktop sometimes arrive with a corrupted moov atom. Adding -ignore_unknown to the ffmpeg input flags fixes most of these without needing separate repair tooling.

Files above 20MB hit Telegram's Bot API download limit for bots. The handler catches this and tells the user to trim or compress the clip before uploading. There's no workaround on the server side for that limit.

What's Next

A web fallback so people can convert videos without a Telegram account. The ffmpeg pipeline would be identical; only the delivery changes (a download link instead of a bot message).

I also want to add a preview mode: send a still frame first, ask if the crop looks right, then run the full encode. Some videos have important content near the edges that the square format cuts off.

ffmpeg is doing most of the heavy lifting here. I just wrote the wrapper and hooked it up to a bot. But the devil is in the parameter combinations, and those took a while to get right.

Disclosure: I built this. The bot is @liveavabot.

Top comments (0)