ffmpeg로 iPhone HEVC 동영상을 텔레그램 아바타용으로 수정하기

발행: (2026년 6월 9일 PM 07:06 GMT+9)
9 분 소요
원문: Dev.to

출처: Dev.to

iPhone으로 클립을 녹화한 뒤 텔레그램 비디오 아바타로 설정하려고 하면, 텔레그램은 아무런 반응도 보이지 않습니다. 오류도, 포맷 경고도 없습니다. 업로드는 정상적으로 끝나지만 아바타가 바뀌지 않죠.

이 문제를 디버깅하는 데 부끄러울 정도로 많은 시간을 소비했습니다.


The Silent Rejection

iOS 11부터 최신 iPhone은 기본적으로 HEVC(H.265) 코덱을 사용합니다. 더 좋은 코덱이라 파일 크기가 작고 대부분의 앱이 잘 처리합니다.

하지만 텔레그램의 비디오 아바타 업로드 기능은 그렇지 못합니다. H.264가 아닌 모든 비디오를 조용히 거부합니다. 오류 대화상자나 포맷 힌트가 전혀 나오지 않으며, 아바타가 업데이트되지 않을 뿐입니다.

같은 문제는 특정 포맷으로 촬영한 Android 사용자, 특이한 픽셀 포맷을 가진 화면 녹화 파일, 그리고 오디오 트랙이 포함된 비디오에서도 발생합니다. 텔레그램의 아바타 업로더는 이런 제약을 전혀 알리지 않습니다.


What Telegram’s Video Avatar Spec Actually Requires

여러 변형을 테스트한 결과, 하드 제한은 다음과 같습니다.

  • 컨테이너: MP4
  • 코덱: H.264 (libx264), yuv420p 픽셀 포맷
  • 해상도: 정확히 800 × 800 픽셀, 정사각형
  • 길이: 최대 10초
  • 파일 크기: 최대 2 MB
  • 오디오: 없어야 함 (-an 옵션으로 제거)

해상도 요구사항이 가장 까다롭습니다. 텔레그램은 비정사각형 비디오를 받지 않으며, 800 × 800이 아닌 크기도 거부합니다. 따라서 크롭과 스케일링이 필요합니다. 픽셀 포맷이 맞지 않으면(예: yuv444p) 역시 조용히 거부됩니다.


The ffmpeg Pipeline

ffmpeg가 이 모든 작업을 처리합니다. 핵심은 이미지를 왜곡하지 않고 정사각형으로 크롭하는 것입니다.

검은색 바가 있는 영상(레터박스 가로형 또는 필러박스 세로형)에서는 두 단계 접근법을 사용합니다. 먼저 크롭 영역을 탐지합니다:

ffmpeg -i input.mov \
  -vf "cropdetect=24:16:0" \
  -f null - 2>&1 | grep crop

이 명령은 crop=1080:1080:0:0 와 같은 결과를 출력합니다. 이를 실제 인코딩에 적용하면 됩니다:

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

옵션 설명

  • -t 10 10초 길이로 잘라냅니다. 원본이 더 길면 앞 10초만 사용합니다.
  • -an 오디오 트랙을 제거합니다. 반드시 포함해야 합니다.
  • -movflags +faststart MP4의 moov atom을 파일 앞쪽으로 이동시켜, 텔레그램이 다운로드가 끝나기 전에 바로 읽을 수 있게 합니다.
  • crf 28 압축 강도를 높입니다. 대부분의 휴대폰 영상은 800 × 800 해상도에서 이 설정이면 2 MB 이하가 됩니다. 만약 초과한다면 crf 30을 시도하거나 길이를 6초 이하로 줄이세요.

검은색 바가 없는 영상이라면 cropdetect를 건너뛰고 바로 정사각형 패딩을 적용합니다:

scale=800:800:force_original_aspect_ratio=decrease,pad=800:800:(ow-iw)/2:(oh-ih)/2

Wiring It Into aiogram 3

핵심 핸들러는 비디오·애니메이션·문서 중 하나를 받아 ffmpeg를 실행하고, 출력 파일 크기를 검사한 뒤 처리된 MP4를 다시 전송합니다:

import asyncio
import os
import tempfile
from aiogram import Router, F
from aiogram.types import Message, BufferedInputFile

router = Router()

async def run_ffmpeg(input_path: str, output_path: str) -> bool:
    vf = (
        "scale=800:800:force_original_aspect_ratio=decrease,"
        "pad=800:800:(ow-iw)/2:(oh-ih)/2,format=yuv420p"
    )
    cmd = [
        "ffmpeg", "-y", "-i", input_path,
        "-vf", vf,
        "-c:v", "libx264", "-preset", "fast", "-crf", "28",
        "-t", "10", "-an", "-movflags", "+faststart",
        output_path
    ]
    proc = await asyncio.create_subprocess_exec(
        *cmd,
        stdout=asyncio.subprocess.DEVNULL,
        stderr=asyncio.subprocess.DEVNULL,
    )
    await proc.wait()
    return proc.returncode == 0

@router.message(F.video | F.animation | F.document)
async def handle_video(message: Message, bot):
    msg = await message.answer("Converting...")

    with tempfile.TemporaryDirectory() as tmp:
        input_path = os.path.join(tmp, "input")
        output_path = os.path.join(tmp, "output.mp4")

        media = message.video or message.animation or message.document
        file = await bot.get_file(media.file_id)
        await bot.download_file(file.file_path, input_path)

        ok = await run_ffmpeg(input_path, output_path)
        if not ok:
            await msg.edit_text("ffmpeg failed. Try a shorter or smaller video.")
            return

        size = os.path.getsize(output_path)
        if size > 2 * 1024 * 1024:
            await msg.edit_text(
                f"Output is {size // 1024}KB, still over the 2MB limit. "
                "Try trimming to 6 seconds or less."
            )
            return

        with open(output_path, "rb") as f:
            data = f.read()

        await message.answer_video(
            BufferedInputFile(data, filename="avatar.mp4"),
            caption="Set this as your Telegram video avatar: Profile -> Set photo -> Video"
        )
        await msg.delete()

위 코드는 실제 프로덕션에서 동작하는 부분을 단순화한 것입니다. 실제 봇은 사용자별 큐를 두어 ffmpeg 프로세스가 동시에 실행되지 않게 하고, 다운로드 전에 파일 크기를 검사해 50 MB를 초과하는 파일은 거부하며, 레이트 리밋도 적용합니다.


Packaging It as @liveavabot

몇 달 동안 저는 이 기능을 LiveAvaBot(텔레그램 봇)으로 운영해 왔습니다. iPhone .mov, Android .mp4, GIF, 화면 녹화 등 거의 모든 형식의 파일을 처리합니다. HEVC 변환이 주요 사용 사례이지만, 잘못된 해상도, 오디오 트랙 존재, 부적절한 픽셀 포맷 등 다른 거부 상황도 자동으로 고쳐줍니다.

  • 총 127명 사용, 어제만 해도 10건 변환
  • 바이럴은 아니지만, 필요로 하는 사람에게는 꼭 필요한 서비스
  • HEVC 변환을 위해 별도 소프트웨어를 설치해야 하는 대안은 현재 없습니다.

스택: Python 3.12, aiogram 3.x, ffmpeg 6, 소형 VPS에서 구동. GPU는 전혀 필요 없으며, 모든 실제 작업은 ffmpeg가 담당합니다.


Edge Cases and Gotchas

  • 4K 원본 파일
    10초짜리 4K HEVC 클립은 300 MB가 넘을 수 있습니다. 봇은 다운로드 전에 파일 크기를 확인하고 50 MB를 초과하면 설명과 함께 거부합니다.

  • 투명 GIF
    애니메이션 GIF에 투명도가 포함된 경우, yuv420p로 변환하면서 알파 채널이 사라져 투명 영역이 검게 됩니다. 봇은 이를 경고하지만 자동으로 복구하지는 않습니다.

  • iPhone 슬로모 비디오
    슬로모 클립은 가변 프레임 레이트를 사용합니다. ffmpeg는 이를 잘 처리하지만, 재인코딩 시 원본보다 길어질 수 있습니다. 예를 들어 3초 240 fps 클립을 일반 속도로 재인코딩하면 8초 이상이 될 수 있습니다. 봇은 10초로 잘라내므로 경우에 따라 액션이 중간에 끊길 수 있습니다.

  • 와이드스크린 콘텐츠
    pad-to-square 방식은 가로형 영상에 검은색 바를 남깁니다. 대부분의 와이드스크린 영상은 중앙 크

0 조회
Back to Blog

관련 글

더 보기 »

Eidentic 소개

Today we're releasing Eidentic, an open-source TypeScript SDK for building AI agents with self-improving memory and the production fundamentals built in — not b...

Typescript의 타입

Introdução Tipos são uma forma de definir a “forma” ou o contrato dos dados que estamos usando no código. Pensando em Javascript puro, ele é dinâmico: você pode...