アニメ画像最適化: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. 公開前チェック
- ファイルサイズが許容か(目安: 数MB未満)。
- WebP/MP4の両対応を確認。
- 自動再生の制御(ユーザー配慮)。
Version: 2025-09-06 – HowTo追加 / FAQ拡張 / keywords追加 / sections調整。