PAYATHON 2026

FFMPEG 在 AWS Lambda 中转码和流定位失败

排障赵师傅

结论

从这些日志看,FFmpeg 读到的媒体数据可能已经损坏、截断,或存在偏移错误。问题不像是缺少 libx264、AAC 库或其他动态链接依赖。

具体依据包括:

  • Invalid NAL unit size 表示 H.264 解码器读到的 NAL 长度超过了剩余数据。常见原因有文件损坏、下载不完整、二进制写入错误或 seek 偏移错误。
  • channel element ... is not allocated、Pulse tool not allowed... 表明 AAC 解码器也读到了无效比特流。
  • 视频和音频同时出现随机解码错误,问题很可能出在它们共同经过的输入、存储或 I/O 环节。
  • FFmpeg 已经正常启动,并进入了解复用和解码阶段。缺少共享库时,程序通常会在启动阶段报告 cannot open shared object file、symbol lookup error,或直接崩溃。
  • libx264 是 H.264 编码器。读取 H.264、定位 MP4 和提取 JPEG 单帧都不依赖它。FFmpeg 自带的原生 AAC 解码器通常也不需要外部 AAC 库。

建议先确认 Lambda 实际读取的文件与源文件逐字节一致,再检查 seek、文件写入方式和二进制兼容性。

先修正测试命令

输入参数应为小写的 -i。如果实际命令使用的是大写 -I,FFmpeg 应该直接报告未知参数,不会进入解码阶段。因此,问题描述中的 -I 也可能只是拼写错误。

先使用输出端 seek,让 FFmpeg 从文件开头解析并解码到第 2 秒:

ffmpeg -hide_banner -loglevel debug \
  -i /tmp/input.mp4 \
  -ss 00:00:02 \
  -frames:v 1 \
  -y /tmp/output.jpg

然后测试输入端 seek:

ffmpeg -hide_banner -loglevel debug \
  -ss 00:00:02 \
  -i /tmp/input.mp4 \
  -frames:v 1 \
  -y /tmp/output.jpg

如果第一条命令成功、第二条失败,问题多半出在随机读取、时间索引或输入层 seek。两条命令都失败时,需要继续检查文件内容和解码环境。

验证 Lambda 中的输入文件

仅确认文件名和对象大小还不够。应分别在上传前、Amazon Linux 测试机以及 Lambda 的 /tmp 中计算 SHA-256:

stat /tmp/input.mp4
sha256sum /tmp/input.mp4

三个环境中的文件大小和 SHA-256 必须完全一致。只要哈希不同,就应先排查数据传输和写入过程,暂时不用检查 FFmpeg 依赖。

媒体数据常见的损坏原因有:

  • 将 S3 返回的二进制内容转换成 UTF-8 字符串后再写入文件。
  • 对已经是二进制的数据重复进行 Base64 解码。
  • 异步下载或文件写入尚未完成,就启动 FFmpeg。
  • Lambda 即将超时,导致下载、转码或上传中断。
  • /tmp 空间不足,写入的文件被截断。
  • 复用固定文件名,导致并发任务或遗留任务覆盖同一个文件。
  • 通过管道传入 MP4,而该管道无法支持 FFmpeg 所需的随机读取。

以 Node.js 为例,应将 S3 对象作为二进制流写入文件,并等待写入完成:

import { createWriteStream, createReadStream } from "node:fs";
import { pipeline } from "node:stream/promises";
import { createHash } from "node:crypto";
import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3";

const s3 = new S3Client({});
const inputPath = `/tmp/input-${crypto.randomUUID()}.mp4`;

const response = await s3.send(new GetObjectCommand({
  Bucket: process.env.BUCKET,
  Key: process.env.KEY,
}));

await pipeline(
  response.Body,
  createWriteStream(inputPath, { flags: "wx" }),
);

const hash = createHash("sha256");
await pipeline(
  createReadStream(inputPath),
  async function* (source) {
    for await (const chunk of source) {
      hash.update(chunk);
      yield chunk;
    }
  },
);

console.log({
  inputPath,
  sha256: hash.digest("hex"),
});

保存媒体文件前,不要对 response.Body 调用 toString()。

分别检查读取、解复用和解码

先确认 FFmpeg 能否读取容器信息:

ffprobe -v debug \
  -show_format \
  -show_streams \
  -show_packets \
  /tmp/input.mp4 > /tmp/ffprobe.json 2> /tmp/ffprobe.log

如果 ffprobe 已经报告 NAL 或 AAC 错误,说明问题出在输入、解复用或数据本身,此时还没有进入编码阶段。

接着分别测试视频和音频:

ffmpeg -v debug \
  -i /tmp/input.mp4 \
  -map 0:v:0 \
  -f null -

ffmpeg -v debug \
  -i /tmp/input.mp4 \
  -map 0:a:0 \
  -f null -

然后测试只解复用,不重新编码:

ffmpeg -v debug \
  -i /tmp/input.mp4 \
  -map 0 \
  -c copy \
  -y /tmp/remux.mp4

可以根据结果缩小排查范围:

  • ffprobe 失败:先检查输入文件和 MP4 索引。
  • -c copy 失败:问题主要在读取或解复用环节。
  • -c copy 成功,但解码失败:检查解码器、CPU 指令和库兼容性。
  • 解码成功,只有 HLS 编码失败:检查编码器、滤镜、输出空间和转码参数。

还可以让 FFmpeg 遇到第一处损坏时立即停止,避免后续错误掩盖最初的异常:

ffmpeg -v debug \
  -xerror \
  -err_detect explode \
  -i /tmp/input.mp4 \
  -f null -

对比 seek 前后的读取行为

先禁用 seek,按顺序解码整个输入:

ffmpeg -v debug \
  -i /tmp/input.mp4 \
  -map 0:v:0 \
  -an \
  -f null -

如果顺序解码正常,而把 -ss 放在 -i 前就会失败,应检查以下内容:

  1. 文件是否确实位于本地 /tmp,而非无法随机读取的流。
  2. MP4 的 moov、sample table 和 chunk offset 是否有效。
  3. Lambda 中的文件大小是否与源文件相同。
  4. 输入是否经过自定义 reader、HTTP 代理、挂载文件系统或加密解密层。
  5. 在 Lambda 中对同一个文件多次执行相同命令时,失败位置是否稳定。

如果每次失败的位置不同,通常与并发覆盖、写入未完成、内存破坏或不稳定的 I/O 有关。如果失败位置固定,则更可能是文件内容、索引或某个确定性的兼容问题。

依赖检查不能只看 ldd

ldd 可以显示动态加载器找到了哪些库,但无法完整证明 ABI、符号版本和实际加载路径都一致。还需要记录以下信息:

uname -a
uname -m
getconf GNU_LIBC_VERSION

ffmpeg -version
ffmpeg -buildconf
ffmpeg -decoders
ffmpeg -encoders

ldd -v ./ffmpeg
readelf -d ./ffmpeg

也可以让动态加载器输出库搜索和符号版本匹配过程:

LD_DEBUG=libs,versions ./ffmpeg -version 2> /tmp/ld-debug.log

检查时重点关注:

  • Lambda 函数架构是 x86_64 还是 arm64。
  • FFmpeg 及其所有 .so 是否采用同一架构。
  • 实际加载的是部署包中的库,还是 Lambda 运行环境中的同名库。
  • RPATH、RUNPATH 和 LD_LIBRARY_PATH 是否把库指向预期目录。
  • 构建环境使用的 glibc 是否比运行环境更新。
  • 构建时是否使用了仅适用于构建机器的 -march=native 或特殊 CPU 指令。

ABI 或共享库问题通常还会伴随加载错误、非法指令、段错误,或者总在固定位置出现异常。就当前日志而言,输入字节异常仍是更直接的线索。

排除 CPU 优化和并发因素

如果文件哈希完全一致,而且同一个文件只在 Lambda 中失败,可以运行下面的对照测试:

ffmpeg -cpuflags 0 \
  -threads 1 \
  -v debug \
  -i /tmp/input.mp4 \
  -f null -

-cpuflags 0 会暂时关闭汇编优化路径,-threads 1 用于排除多线程因素。这两个参数适合诊断,不宜作为长期性能配置。

如果使用这些参数后能够稳定运行,应检查 FFmpeg 的编译参数、目标 CPU 和相关解码器优化。如果问题仍然存在,则继续检查运行时实际加载的库和底层文件读取。

更可靠的部署方式

生产环境通常可以采用以下两种方式:

  • 使用针对目标 Lambda 架构和运行环境构建的静态 FFmpeg,或尽量减少外部依赖。
  • 使用 Lambda container image,并在与实际运行镜像一致的基础镜像中编译和测试 FFmpeg。

静态构建可以减少共享库差异,但无法修复损坏的输入文件,也不能消除 glibc、内核接口、CPU 指令或许可证方面的所有问题。构建时仍要明确目标架构,并避免无意使用 -march=native。

检查 Lambda 资源限制

执行 FFmpeg 前,先记录运行条件:

df -h /tmp
ulimit -a
ls -lh /tmp/input.mp4

同时记录以下信息:

  • 函数剩余执行时间。
  • Lambda 内存配置和实际峰值。
  • /tmp 可用空间。
  • FFmpeg 退出码、终止信号和完整 stderr。
  • 下载、转码和上传各阶段的开始及结束时间。
  • 输入文件的大小与 SHA-256。

如果通过 Node.js child_process 启动 FFmpeg,建议使用参数数组,并持续读取 stdout 和 stderr。缓冲区达到上限也可能导致子进程被终止,但通常不会引发当前这种持续出现的 H.264/AAC 比特流错误。

推荐的排查顺序

  1. 将输入完整下载到 /tmp,为每个任务使用唯一文件名,并等待写入完成。
  2. 对比源文件与 Lambda 文件的大小和 SHA-256。
  3. 使用 ffprobe 检查容器和数据包。
  4. 分别进行视频解码、音频解码和 -c copy 测试。
  5. 对比 -ss 位于 -i 前后的执行结果。
  6. 使用 -xerror -err_detect explode 定位第一处错误。
  7. 核对 ffmpeg -buildconf、架构、glibc、ldd -v 和实际加载路径。
  8. 使用 -cpuflags 0 -threads 1 进行兼容性对照。
  9. 最后再改用针对 Lambda 目标环境重新构建的 FFmpeg。

如果 Lambda 中的输入哈希与源文件不同,问题就在下载或文件写入链路。确认哈希一致后,如果同一条 FFmpeg 命令面对等价输入仍有不同表现,再把排查重点转向 Lambda 运行环境、CPU 优化或动态库 ABI。

备注:内容仅供参考。