跳到正文
跟我做
自媒体自动化4 分钟FFmpeg

一条视频三个平台:批量改尺寸、抽帧和文案

用 FFmpeg 和一段 Node 脚本,把一条横屏视频批量转成抖音、小红书、视频号需要的尺寸,同时抽出封面帧并生成三版文案。

约 4 分钟
本页目录
  1. 这篇教程解决什么问题
  2. 准备环境
  3. 目录结构
  4. 三个平台的参数
  5. 两种适配方式
  6. 方式一:留边,不裁掉任何内容
  7. 方式二:裁切,填满画面
  8. 批量脚本
  9. 自动抽封面帧
  10. 三版文案怎么改
  11. 验证清单
  12. 成本与边界
  13. 下一步读什么

30 秒速查

  • 源视频放 input/,三平台参数在脚本顶部
  • 转码:node scripts/convert.mjs input/demo.mp4
  • 抽帧:node scripts/batch-media/extract-covers.mjs input/demo.mp4
  • 发布前查:时长、字幕、封面帧

这篇教程解决什么问题

同一条视频,抖音要 9:16、小红书常用 3:4、视频号又是一套尺寸。手动剪三遍,一小时就没了。

这篇的做法是:保留一份原始素材,用脚本一次生成所有平台版本。你只需要在发布前检查画面和文案,不再重复导出。

准备环境

需要两个东西:

  • Node.js 20 或更高版本,用来跑批量脚本。
  • FFmpeg,负责真正的转码和抽帧。

macOS 可以用 Homebrew 安装:

brew install ffmpeg

Windows 建议用 winget:

winget install Gyan.FFmpeg

安装完成后确认命令可用:

ffmpeg -version
node -v

目录结构

先在项目里建一个固定目录,后面所有脚本都围绕它工作:

batch-media/
├── input/
│   └── demo.mp4
├── output/
├── copy.json
└── scripts/
    └── convert.mjs

原则只有一条:input 只读,output 只写。这样脚本跑错也不会破坏原素材。

三个平台的参数

尺寸会随平台规则变化,下面是写脚本时的起点,发布前仍需以平台最新要求为准:

平台 画面比例 输出尺寸 适合的场景
抖音 9:16 1080 × 1920 竖屏口播、剧情、教程
小红书 3:4 1080 × 1440 图文感较强、封面重要的内容
视频号 1:1 1080 × 1080 信息流里更稳,兼容横竖屏

两种适配方式

方式一:留边,不裁掉任何内容

ffmpeg -i input/demo.mp4 \
  -vf "scale=1080:1920:force_original_aspect_ratio=decrease,pad=1080:1920:(ow-iw)/2:(oh-ih)/2:color=black" \
  -c:v libx264 -crf 20 -preset veryfast \
  -c:a aac -b:a 128k -movflags +faststart \
  output/douyin.mp4

force_original_aspect_ratio=decrease 会先把画面缩到能放进目标尺寸,再用 pad 补黑边。人物和字幕都不会被切掉。

方式二:裁切,填满画面

ffmpeg -i input/demo.mp4 \
  -vf "scale=1080:1920:force_original_aspect_ratio=increase,crop=1080:1920" \
  -c:v libx264 -crf 20 -preset veryfast \
  -c:a aac -b:a 128k -movflags +faststart \
  output/douyin.mp4

increase 会先把画面放大到能盖住目标尺寸,再居中裁切。画面更满,但左右或上下会损失一部分。

批量脚本

把下面这段保存为 scripts/convert.mjs——就是仓库里 resources/batch-media/convert.mjs 的全文,需要和同目录的 tools.mjs 放在一起。它用 execFile 调用 FFmpeg,避免把文件名拼进 shell 命令里。

import { execFile } from "node:child_process";
import { constants } from "node:fs";
import { access, mkdir } from "node:fs/promises";
import path from "node:path";
import { pathToFileURL } from "node:url";
import { promisify } from "node:util";

import { resolveFfmpeg } from "./tools.mjs";

const run = promisify(execFile);
const ROOT = import.meta.dirname;

export const PROFILES = [
  { name: "douyin", width: 1080, height: 1920, mode: "pad" },
  { name: "xiaohongshu", width: 1080, height: 1440, mode: "pad" },
  { name: "shipinhao", width: 1080, height: 1080, mode: "pad" },
];

export function buildFilter({ width, height, mode }) {
  if (mode === "crop") {
    return `scale=${width}:${height}:force_original_aspect_ratio=increase,crop=${width}:${height}`;
  }

  return `scale=${width}:${height}:force_original_aspect_ratio=decrease,pad=${width}:${height}:(ow-iw)/2:(oh-ih)/2:color=black`;
}

export function buildConvertArgs({ input, output, profile }) {
  return [
    "-y",
    "-i", input,
    "-vf", buildFilter(profile),
    "-c:v", "libx264",
    "-crf", "20",
    "-preset", "veryfast",
    "-pix_fmt", "yuv420p",
    "-c:a", "aac",
    "-b:a", "128k",
    "-movflags", "+faststart",
    output,
  ];
}

export async function convertAll({
  input,
  outputDir,
  profiles = PROFILES,
  ffmpeg = resolveFfmpeg(),
}) {
  await access(input, constants.R_OK);
  await mkdir(outputDir, { recursive: true });

  const outputs = [];
  for (const profile of profiles) {
    const output = path.join(outputDir, `${profile.name}.mp4`);
    await run(ffmpeg, buildConvertArgs({ input, output, profile }), {
      maxBuffer: 10 * 1024 * 1024,
    });
    outputs.push(output);
  }

  return outputs;
}

async function main() {
  const arg = process.argv[2];
  const input = arg ? path.resolve(process.cwd(), arg) : path.join(ROOT, "input/demo.mp4");
  const outputs = await convertAll({ input, outputDir: path.join(ROOT, "output") });

  for (const output of outputs) console.log(`done: ${output}`);
}

if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
  await main();
}

运行:

node scripts/convert.mjs input/demo.mp4

跑完检查尺寸:

ffprobe -v error -select_streams v:0 -show_entries stream=width,height -of csv=p=0 output/douyin.mp4

自动抽封面帧

封面不要靠平台随机截。用 FFmpeg 在视频的 20%、50%、80% 位置各抽一帧:

ffmpeg -ss 00:00:02 -i input/demo.mp4 -frames:v 1 -q:v 2 output/cover-20.jpg
ffmpeg -ss 00:00:05 -i input/demo.mp4 -frames:v 1 -q:v 2 output/cover-50.jpg
ffmpeg -ss 00:00:08 -i input/demo.mp4 -frames:v 1 -q:v 2 output/cover-80.jpg

时间点要按视频长度改。更稳的做法是先取时长,再按比例计算:

ffprobe -v error -show_entries format=duration -of csv=p=0 input/demo.mp4

仓库里的 scripts/batch-media/extract-covers.mjs 已经把“取时长 → 按 20%/50%/80% 抽帧”封装好了:

node scripts/batch-media/extract-covers.mjs input/demo.mp4

三版文案怎么改

同一个内容,三个平台的文案结构不一样。建议只维护一份 copy.json,再用下面的结构改写成三版:

{
  "topic": "一条视频多平台分发",
  "points": ["不用剪三遍", "尺寸自动适配", "封面自动抽帧"],
  "audience": "做自媒体但不想重复劳动的人",
  "cta": "需要脚本的评论区说一声"
}

改写规则:

  • 抖音:前三秒给结果,第一句直接说“别再手动剪三个版本了”。
  • 小红书:标题写具体收益,正文分点,结尾给可执行的第一步。
  • 视频号:语气更平,先讲场景,再讲方法,少用夸张词。

验证清单

发布前至少检查这四件事:

  1. 三个版本的时长和原片一致,没有音画不同步。
  2. 字幕没有被裁切或压在黑边上。
  3. 封面帧里人物表情正常,没有闭眼或运动模糊。
  4. 文件能在手机上正常播放,尤其是从 iPhone 拍的 HDR/HEVC 素材。

如果遇到某些设备播放黑屏,通常是编码兼容问题。把 -c:v libx264 和 -pix_fmt yuv420p 固定下来,兼容性最好。

成本与边界

这个方案适合同一条内容的多平台适配,不适合把长视频自动剪成多条短视频。后者需要理解内容结构,脚本只能做机械切割,效果通常不好。

转码本身消耗 CPU。十分钟的 1080p 素材在普通笔记本上大概需要几分钟,建议一次批量跑完,不要边剪边等。

下一步读什么

第一次装 ffmpeg 或换电脑,先跑一遍ffmpeg 不是内部或外部命令里的检查,确认 PATH 没问题。

转完视频,配图可以接着批量压缩图片,上传前把体积压下来。

常见问题

没有编程基础能跟着做吗?

可以。你只需要会复制命令、安装 FFmpeg 和 Node.js。脚本里的路径和尺寸都集中在文件顶部,改两个值就能跑。

会不会覆盖或损坏原视频?

不会。脚本只读取 input 目录,所有结果写到 output 目录,源文件不动。第一次跑建议用一条十几秒的短视频测试。

应该用留黑边还是裁切?

看画面主体。人物、字幕靠近边缘时用留边更安全;画面主体居中、边缘是空背景时用裁切更好看。脚本里两种模式都有。

为什么转出来的视频比原来大?

默认用 CRF 20 保证画质。想减小体积可以把 CRF 调到 23 到 26,或者把 preset 改成 slow,代价是转码更慢。

每周一张复习表

每周分享一张实用复习表,不收钱不打扰,想走随时走

邮箱存在 Cloudflare KV 里,只用来发每周更新;任何时候写信到hello@genwozuo.com说「退订」就删掉。详见隐私政策。

同方向继续

全部教程