本文详解如何在 Node.js(尤其 Cloud Functions 环境)中安全实现 MP4 → MP3 转码并直传 Firebase Storage,规避 ENOENT: no such file or directory 错误,全程不依赖本地磁盘持久化。
本文详解如何在 node.js(尤其 cloud functions 环境)中安全实现 mp4 → mp3 转码并直传 firebase storage,全程不依赖本地磁盘持久化。
在 Serverless 环境(如 Firebase Cloud Functions 或 Google Cloud Functions)中执行 FFmpeg 转码时,常见错误 Error: ENOENT: no such file or directory, open './1695556319341.mp3' 的根本原因在于:Cloud Functions 的文件系统是只读的(除 /tmp 目录外),且不允许写入当前工作目录(如 ./)。你代码中使用了相对路径 ./${date}.mp3 作为输出目标,而该路径在函数运行时并不存在或不可写,导致 FFmpeg 写入失败,后续 readFile() 自然抛出 ENOENT。
✅ 正确做法是:强制 FFmpeg 输出到 /tmp 目录,并确保转码完成后立即清理临时文件——这是 Google 官方明确支持且唯一可靠的临时文件操作路径。
以下是修复后的完整、生产就绪方案(适配 Firebase Functions + Fluent-FFmpeg):
✅ 正确实现:使用 /tmp 临时路径 + 流式优化(推荐)
import { initializeApp } from "firebase/app";
import { getStorage, ref, getDownloadURL, uploadBytes } from "firebase/storage";
import serviceAccount from '../firebase/serviceAccountKey';
import { path as ffmpegPath } from '@ffmpeg-installer/ffmpeg';
import ffmpeg from 'fluent-ffmpeg';
import { readFile, unlink, writeFile } from 'fs/promises';
import { tmpdir } from 'os';
import { join } from 'path';
initializeApp(serviceAccount);
ffmpeg.setFfmpegPath(ffmpegPath);
async function convertMP4ToMP3AndUploadToFirebase(
inputPath: string,
firebaseStoragePath: string
) {
const tempDir = tmpdir(); // ✅ 唯一可写的临时目录
const date = Date.now();
const outputPath = join(tempDir, `${date}.mp3`); // 如 /tmp/1716182400000.mp3
try {
// Step 1: 执行转码(输出至 /tmp)
await new Promise<void>((resolve, reject) => {
ffmpeg(inputPath)
.output(outputPath)
.audioCodec('libmp3lame')
.audioBitrate(128) // 可选:控制音质与体积
.format('mp3')
.on('error', reject)
.on('end', resolve)
.run();
});
// Step 2: 读取转码后文件为 Buffer
const fileBuffer = await readFile(outputPath);
// Step 3: 上传至 Firebase Storage(注意:storage path 不含前导 ./)
const storage = getStorage();
const storageRef = ref(storage, firebaseStoragePath); // ✅ 正确路径示例:'audio/converted/20260520.mp3'
const metadata = { contentType: 'audio/mpeg' };
const snapshot = await uploadBytes(storageRef, fileBuffer, metadata);
const downloadURL = await getDownloadURL(snapshot.ref);
// Step 4: 立即清理临时文件(关键!防止内存泄漏)
await unlink(outputPath);
console.log('✅ MP4 → MP3 conversion & Firebase upload succeeded!');
console.log('? Download URL:', downloadURL);
return { downloadURL, size: fileBuffer.length };
} catch (error) {
console.error('❌ Conversion/upload failed:', error);
// 清理残留(防御性编程)
try {
await unlink(outputPath);
} catch (cleanupErr) {
console.warn('⚠️ Failed to cleanup temp file:', cleanupErr);
}
throw error;
}
}
// 使用示例(需确保 inputPath 是函数内可访问的本地路径,如 /tmp/video.mp4)
// 注意:若输入文件来自 HTTP 上传或 Cloud Storage,需先下载到 /tmp
const inputPath = '/tmp/video.mp4'; // ✅ 必须是 /tmp 下的有效路径
const firebaseStoragePath = `audio/converted/${Date.now()}.mp3`;
convertMP4ToMP3AndUploadToFirebase(inputPath, firebaseStoragePath);⚠️ 关键注意事项(必读)
-
/tmp 是内存文件系统:所有写入 /tmp 的内容都驻留在 RAM 中。大音频文件(如 >100MB)可能导致内存溢出(OOM)。建议:
- 在 functions.config() 中配置合理内存(如 2GB);
- 对超大文件启用分段处理或改用流式转码(需 fluent-ffmpeg 配合 pipe() + stream.PassThrough,但 Firebase Functions 对长时流支持有限,慎用)。
Firebase Storage 路径规范:firebaseStoragePath 应为纯路径字符串(如 'audio/songs/sample.mp3'),不能带 ./ 或 /tmp/ 前缀,否则会上传到错误位置。
输入文件来源:本例假设 inputPath 已存在于 /tmp(例如由 multer 上传或从 GCS 下载而来)。若原始文件来自网络 URL,需先用 fetch() 或 https.get() 下载到 /tmp,再传入转码函数。
错误处理强化:已添加 unlink 的防御性调用,避免因转码失败导致 /tmp 文件堆积(Cloud Functions 实例重启后 /tmp 会清空,但同一实例内必须手动清理)。
✅ 进阶建议(生产环境)
| 场景 | 推荐方案 |
|---|---|
| 高并发转码 | 引入 BullMQ + Redis 队列,解耦上传与转码,避免函数超时 |
| 进度反馈 | 结合 ffmpeg.on('progress') + Socket.IO 或 Pub/Sub 向前端推送百分比 |
| 无本地磁盘依赖(终极方案) | 使用 ffmpeg.wasm 在浏览器端转码,仅上传结果;或调用 FFmpeg-as-a-Service(如 AWS MediaConvert) |
通过严格遵循 /tmp 路径约束、及时清理与健壮异常处理,即可彻底解决 ENOENT 错误,构建稳定、合规的 Serverless 音频处理流水线。


















