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 前就会失败,应检查以下内容:
- 文件是否确实位于本地
/tmp,而非无法随机读取的流。 - MP4 的
moov、sample table 和 chunk offset 是否有效。 - Lambda 中的文件大小是否与源文件相同。
- 输入是否经过自定义 reader、HTTP 代理、挂载文件系统或加密解密层。
- 在 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 比特流错误。
推荐的排查顺序
- 将输入完整下载到
/tmp,为每个任务使用唯一文件名,并等待写入完成。 - 对比源文件与 Lambda 文件的大小和 SHA-256。
- 使用
ffprobe检查容器和数据包。 - 分别进行视频解码、音频解码和
-c copy测试。 - 对比
-ss位于-i前后的执行结果。 - 使用
-xerror -err_detect explode定位第一处错误。 - 核对
ffmpeg -buildconf、架构、glibc、ldd -v和实际加载路径。 - 使用
-cpuflags 0 -threads 1进行兼容性对照。 - 最后再改用针对 Lambda 目标环境重新构建的 FFmpeg。
如果 Lambda 中的输入哈希与源文件不同,问题就在下载或文件写入链路。确认哈希一致后,如果同一条 FFmpeg 命令面对等价输入仍有不同表现,再把排查重点转向 Lambda 运行环境、CPU 优化或动态库 ABI。
备注:内容仅供参考。
