gazou-compressor.jp

アニメ画像最適化:GIF→WebP/MP4 変換と配信の現実解(FFmpeg連携)

GIFは互換性が高い一方、重く・荒いのが難点。実運用ではWebP(アニメ)MP4の併配信が現実解です。HEICなど静止画の扱いはこちらを参照。

方針(TL;DR)
  • fpsと解像度を落として適正化
  • MP4はyuv420pで互換性確保。
  • オートプレイは配慮(アクセシビリティ)。

1. FFmpegコマンド例

# FFmpeg 例(CLI)
# GIF → WebP(アニメ): 15fps / Lanczos / ループ
ffmpeg -i input.gif -vf "fps=15,scale=iw:-2:flags=lanczos" -loop 0 -quality 75 -compression_level 6 output.webp

# GIF → MP4(H.264): 15fps / 互換目的
ffmpeg -i input.gif -vf "fps=15,scale=iw:-2:flags=lanczos,setsar=1" -pix_fmt yuv420p -movflags +faststart -profile:v baseline -level 3.0 output.mp4

2. APIから変換

// app/api/anim/route.ts — Next.jsからFFmpegを呼び出し(サーバ環境にFFmpeg必要)
import { NextRequest, NextResponse } from "next/server";
import { spawn } from "node:child_process";
import { tmpdir } from "node:os";
import { randomUUID } from "node:crypto";
import fs from "node:fs/promises";

export const runtime="nodejs"; export const dynamic="force-dynamic";

function run(cmd:string, args:string[]):Promise<{code:number,stderr:string}>{
  return new Promise((resolve)=>{
    const ps = spawn(cmd,args,{stdio:["ignore","ignore","pipe"]});
    let err=""; ps.stderr.on("data",(d)=>err+=d.toString());
    ps.on("close",(code)=>resolve({code:code??1,stderr:err}));
  });
}

export async function GET(req: NextRequest){
  const { searchParams } = new URL(req.url);
  const src = searchParams.get("url");
  const format = (searchParams.get("format") ?? "webp").toLowerCase(); // webp|mp4
  const fps = Number(searchParams.get("fps") ?? 15);
  if(!src) return NextResponse.json({error:"url is required"},{status:400});

  const r = await fetch(src,{cache:"no-store"}); if(!r.ok) return NextResponse.json({error:"fetch failed"},{status:502});
  const buf = Buffer.from(await r.arrayBuffer());

  const tmp = tmpdir();
  const inPath = `${tmp}/${randomUUID()}.gif`;
  const outPath = `${tmp}/${randomUUID()}.${format}`;
  await fs.writeFile(inPath, buf);

  const args = format==="mp4"
    ? ["-i", inPath, "-vf", `fps=${fps},scale=iw:-2:flags=lanczos,setsar=1`, "-pix_fmt", "yuv420p", "-movflags", "+faststart", "-profile:v", "baseline", "-level", "3.0", outPath]
    : ["-i", inPath, "-vf", `fps=${fps},scale=iw:-2:flags=lanczos`, "-loop", "0", "-quality", "75", "-compression_level", "6", outPath];

  const { code, stderr } = await run("ffmpeg", args);
  if(code!==0) return NextResponse.json({error:"ffmpeg failed", detail:stderr},{status:500});
  const out = await fs.readFile(outPath);
  await fs.rm(inPath).catch(()=>{}); await fs.rm(outPath).catch(()=>{});

  return new NextResponse(out,{headers:{
    "Content-Type": format==="mp4" ? "video/mp4" : "image/webp",
    "Cache-Control":"public, max-age=60, s-maxage=86400",
  }});
}

3. 表示(MP4優先+WebP)

// components/AnimPicture.tsx — PREFER: MP4 + Fallback(コントロール可)
import React from "react";
export function AnimPicture({ srcWebp, srcMp4, alt }:{srcWebp:string; srcMp4:string; alt:string;}){
  return (
    <picture>
      <source srcSet={srcWebp} type="image/webp" />
      <video src={srcMp4} playsInline loop muted autoPlay aria-label={alt} />
    </picture>
  );
}

4. 公開前チェック

Version: 2025-09-06 – HowTo追加 / FAQ拡張 / keywords追加 / sections調整。

公開:2025-09-06 / 監修:gazou-compressor.jp 編集部

gazou-compressor.jp 編集部

画像圧縮・変換・背景除去などの実践テクニックと、Webで“速く・軽く・崩さない”ためのノウハウを発信しています。

関連記事

トピック/更新日の近いコンテンツ