惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

雷峰网
雷峰网
GbyAI
GbyAI
Stack Overflow Blog
Stack Overflow Blog
Apple Machine Learning Research
Apple Machine Learning Research
The Cloudflare Blog
WordPress大学
WordPress大学
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
F
Fortinet All Blogs
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
Microsoft Azure Blog
Microsoft Azure Blog
酷 壳 – CoolShell
酷 壳 – CoolShell
博客园 - 聂微东
L
LangChain Blog
云风的 BLOG
云风的 BLOG
Jina AI
Jina AI
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
I
InfoQ
大猫的无限游戏
大猫的无限游戏
MyScale Blog
MyScale Blog
人人都是产品经理
人人都是产品经理
小众软件
小众软件
量子位
The GitHub Blog
The GitHub Blog
博客园 - 【当耐特】

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
Fixing iPhone HEVC Videos for Telegram Avatars With ffmpeg
liveavabot · 2026-05-20 · via DEV Community

liveavabot

The Bug With No Error Message

I tried to set a short clip as my Telegram profile video. Recorded it on my iPhone, opened Telegram, picked the file, hit save. Telegram took it, showed a spinner for a second, then kept my old photo. No error. No toast. Nothing happened.

I tried three more times. Same silent nothing. The file played fine in every other app, so it wasn't corrupt.

The problem is the codec. Since iOS 11, iPhones record video in HEVC (H.265) by default. It's great for storage. It is also not what Telegram wants for a profile video avatar. Telegram's avatar slot expects H.264, and when it gets something it can't decode for that slot, it doesn't tell you. It just drops the upload.

So I wrote a bot that re-encodes the file before it ever reaches that slot. This post is how it works.

What Telegram Actually Wants From a Video Avatar

There is no public error message, so I pieced the spec together by testing files until they stuck. A video avatar that Telegram accepts looks like this:

  • Container: MP4.
  • Video codec: H.264. libx264 is fine.
  • Pixel format: yuv420p. HEVC files are often yuv420p10le (10-bit), which the H.264 avatar path rejects.
  • Shape: square. I use 800x800. Non-square files get center-cropped by Telegram in ways you don't control.
  • Duration: 10 seconds or less.
  • Size: 2 MB or less.
  • Audio: none. The avatar has no sound, and an audio track sometimes pushes you over the size cap for nothing.
  • faststart: the moov atom belongs at the front of the file so playback can begin before the whole thing downloads.

Miss any one of these and you get the silent drop. The 10-bit pixel format caught me out the longest, because the file looked completely valid.

Fixing It With ffmpeg

ffmpeg does all the real work here. I just call it correctly.

The first step is finding a square crop. iPhone video is 16:9 or 4:3, never square, so I need a 1:1 region. cropdetect scans a few seconds and reports a crop rectangle:

ffmpeg -i input.mov -t 3 -vf cropdetect=24:16:0 -f null - 2>&1 \
  | awk '/crop=/ { c=$NF } END { print c }'

Enter fullscreen mode Exit fullscreen mode

That prints something like crop=1080:1080:420:0. It centers on the actual content instead of blindly cropping from a corner.

Then the real encode:

ffmpeg -i input.mov \
  -t 10 \
  -vf "crop=1080:1080:420:0,scale=800:800:flags=lanczos,format=yuv420p" \
  -c:v libx264 -profile:v high -preset slow -crf 28 \
  -an \
  -movflags +faststart \
  -y output.mp4

Enter fullscreen mode Exit fullscreen mode

Going through the flags that matter:

  • -t 10 hard-caps the clip at 10 seconds.
  • The -vf chain crops to square, scales to 800x800 with the Lanczos filter (sharper than the default bilinear), and forces format=yuv420p so the 10-bit problem disappears.
  • -c:v libx264 is the codec swap. HEVC goes in, H.264 comes out.
  • -crf 28 trades a little quality for size. At 800x800 it looks fine and helps stay under 2 MB.
  • -an drops the audio track completely.
  • -movflags +faststart moves the moov atom to the front.

One encode, every requirement satisfied.

Wiring It Into an aiogram 3 Bot

I wrapped this in a Telegram bot with aiogram 3 so I never have to think about it again. The handler accepts video, animation (GIFs arrive as animation), and raw document uploads:

import asyncio
from pathlib import Path
from aiogram import Bot, Dispatcher, F
from aiogram.types import Message, FSInputFile

dp = Dispatcher()


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

    src = Path(f"/tmp/{media.file_unique_id}.src")
    dst = Path(f"/tmp/{media.file_unique_id}.mp4")
    await bot.download(media, destination=src)

    await message.answer("Re-encoding to Telegram avatar format...")
    ok = await convert_to_avatar(src, dst)
    if not ok:
        await message.answer("Couldn't convert that one. 4K and HDR aren't handled yet.")
    else:
        await message.answer_video(FSInputFile(dst))

    src.unlink(missing_ok=True)
    dst.unlink(missing_ok=True)

Enter fullscreen mode Exit fullscreen mode

The conversion runs ffmpeg as a subprocess and checks the output before trusting it:

async def convert_to_avatar(src: Path, dst: Path) -> bool:
    vf = "crop=in_h:in_h,scale=800:800:flags=lanczos,format=yuv420p"
    cmd = [
        "ffmpeg", "-i", str(src), "-t", "10",
        "-vf", vf,
        "-c:v", "libx264", "-preset", "slow", "-crf", "28",
        "-an", "-movflags", "+faststart", "-y", str(dst),
    ]
    proc = await asyncio.create_subprocess_exec(
        *cmd,
        stdout=asyncio.subprocess.DEVNULL,
        stderr=asyncio.subprocess.DEVNULL,
    )
    await proc.wait()
    if proc.returncode != 0 or not dst.exists():
        return False
    return dst.stat().st_size <= 2 * 1024 * 1024

Enter fullscreen mode Exit fullscreen mode

For the bot I use crop=in_h:in_h, a center square based on input height. It skips the separate cropdetect pass and keeps latency low. cropdetect is the better choice when framing matters more than speed.

Packaging It As a Bot Anyone Can Use

Once it worked for me, sharing it was almost free. Send a video to the bot, get back a file that drops straight into the Telegram avatar slot. No app, no settings, no codec knowledge required. It lives here: https://t.me/LiveAvaBot?start=devto_article_20260520

The whole thing is one ffmpeg call behind a message handler. ffmpeg is doing the heavy lifting. I just wrote the wrapper that knows the exact spec Telegram never documents.

Edge Cases And What's Next

A few things I learned shipping it:

  • Size overshoot. A busy 10-second clip can still land above 2 MB at crf 28. The fix is a second pass with a higher crf, bumping it 4 or 5 at a time until the file fits.
  • Rotation metadata. iPhone videos carry a rotation flag instead of rotating pixels. ffmpeg honors it by default now, but older builds can hand you a sideways avatar.
  • GIFs. They arrive as animation, not video, which is why the handler filter includes both. After encoding they behave like any other clip.
  • 4K and HDR. Not handled yet. HDR needs a tone-mapping step before the H.264 conversion or the colors wash out. That is next on the list.

Telegram silently rejecting valid-looking files is annoying. The fix turned out small once you know what the avatar slot wants: one ffmpeg call behind a message handler.

Built by me, @liveavabot.