The Problem: Telegram Video Avatars Are Picky
Setting a video avatar in Telegram sounds simple until you try it on an iPhone. Telegram silently rejects HEVC (H.265) files, which is what every modern iOS device records by default. No error message. Just nothing happens. Users think the bot is broken.
The actual Telegram spec for video avatars: codec H.264, resolution 800x800 (square), max 10 seconds, max 2MB, no audio. Getting all five constraints right simultaneously is what I built @liveavabot for. But once you have a working converter, the next question is: how do users pick templates without leaving their current chat?
That's where inline mode comes in.
Why Inline Mode Is the Right UX
Most bots ignore inline_query. The handler isn't complicated, but it requires a separate registration flow: enable inline mode in @botfather, handle the InlineQuery update type, respond within a timeout. Most bot developers skip it.
For a template-selection flow, inline mode is the best UX Telegram offers. The user types @LiveAvaBot cute in any chat, sees a grid of matching templates instantly, taps one. No deep link, no private chat redirect, no friction.
When a user types @botname text in any Telegram chat, Telegram sends an InlineQuery update to your bot. You have about 5 seconds to respond with up to 50 results. Each result is a pre-built message the user can insert.
For video content, InlineQueryResultCachedMpeg4Gif is the right type. Despite the name, Telegram uses this for any short looping video, including the video sticker format. The key field is mpeg4_file_id, which references a file already on Telegram's servers. Upload each template once, save the file_id, reference it forever.
The aiogram 3 Handler
Here's a minimal working inline handler:
from aiogram import Router
from aiogram.types import InlineQuery, InlineQueryResultCachedMpeg4Gif
router = Router()
# Pre-uploaded templates: list of (file_id, title) tuples
TEMPLATES = [
("BQACAGIAAXXX...", "neon pulse"),
("BQACAGIAAXXY...", "soft glow"),
("BQACAGIAAXXZ...", "glitch loop"),
]
@router.inline_query()
async def handle_inline(query: InlineQuery):
q = query.query.lower().strip()
results = [
InlineQueryResultCachedMpeg4Gif(
id=str(i),
mpeg4_file_id=fid,
title=title,
)
for i, (fid, title) in enumerate(TEMPLATES)
if not q or q in title.lower()
]
await query.answer(results[:25], cache_time=300, is_personal=False)
cache_time=300 tells Telegram to cache the results for 5 minutes. is_personal=False means the same results are served to all users with the same query string. That's fine for public templates. Flip it to True if results depend on user state.
The results[:25] cap exists because rendering more than 25 GIF previews in a grid noticeably slows the picker on older phones.
One note on types: InlineQueryResultCachedMpeg4Gif takes a file_id. Its non-Cached sibling takes a URL. Use the Cached variant. Remote URLs get re-fetched by Telegram on every query, add latency, and break when URLs change.
The ffmpeg Pipeline Behind Each Template
Each template starts as a raw video file, often an iPhone .MOV with HEVC encoding. The conversion pipeline uses ffmpeg with cropdetect to find the actual content area before scaling:
# Step 1: detect crop bounds
ffmpeg -ss 0 -i input.mov -vf "cropdetect=24:2:0" -t 5 -f null - 2>&1 \
| grep -oP 'crop=\S+' | tail -1
# Step 2: encode to Telegram spec
ffmpeg -i input.mov \
-vf "crop=720:720:0:180,scale=800:800,format=yuv420p" \
-c:v libx264 -profile:v baseline -level 3.1 \
-pix_fmt yuv420p \
-movflags +faststart \
-an \
-t 10 \
-fs 2000000 \
output.mp4
-an removes audio. -fs 2000000 hard-stops at 2MB. faststart moves the moov atom to the front so Telegram starts playing before the full file loads. The baseline profile keeps compatibility with older Android decoders.
yuv420p is explicit because ffmpeg sometimes defaults to yuv444p for certain input files, and Telegram's player rejects that silently.
Packaging This as @liveavabot
The bot handles two flows. Primary: user sends a video, bot converts it and returns an 800x800 H.264 MP4 ready to set as a video avatar. Secondary: inline mode template picker shown above.
Inline mode was faster to ship. Upload templates once, done. The conversion flow took more work: probing input files for codec and color space, handling portrait vs landscape crops, capping output size without visible quality loss.
481 users have tried the bot. Conversion takes 3-8 seconds depending on input length. The most common failure mode is videos over 30 seconds where the user expects smart trimming. It currently takes the first 10 seconds. That's next on the list.
Try it: https://t.me/LiveAvaBot?start=devto_article_20261030
Type @LiveAvaBot in any chat to see the template picker.
What I Learned
Inline results are cached aggressively by Telegram clients. Set cache_time=0 during development or you'll spend time wondering why updated templates aren't showing.
File IDs are bot-specific. A file_id from one bot can't be used by another. If you ever migrate bots, re-upload everything.
Enable inline feedback in @botfather via /setinlinefeedback. Without it, you won't get chosen_inline_result updates, which is the only way to track which templates actually get used.
The is_personal=False default is a useful optimization. Telegram caches results at the server level and serves them to multiple users with the same query string, reducing traffic to your bot. For personalized results, set is_personal=True and Telegram caches per-user instead.
Built by me: @LiveAvaBot
Top comments (0)