使用FFmpeg拼接25fps视频后输出帧率异常为3.554fps的问题排查
问题:AWS Lambda中FFmpeg拼接视频后帧率异常、时长错误
环境与文件信息
- 开发环境:Node.js 18 + AWS Lambda
- FFmpeg版本:7(Lambda层静态构建,仅含ffmpeg无ffprobe)
- 文件规格:
- 音频:WAV格式,13kbps,AAC(LC)编码,时长6分28秒
- 视频:MP4格式,1280x720分辨率,25fps,H.264编码,时长3分27秒
目标需求
- 用空白视频填补原视频前后的时长间隙,让完整音频得到覆盖
- 剥离原MP4的音频,仅使用指定WAV音频
- 最终MP4全程播放WAV音频:前27秒为空白视频,接着播放原MP4,最后用空白视频补足到音频总时长6分28秒
实际问题
生成的MP4文件帧率变为3.554fps,时长错误显示为10分06秒
现有代码
import { S3Client, GetObjectCommand, PutObjectCommand } from "@aws-sdk/client-s3"; import { createWriteStream, createReadStream, promises as fsPromises } from 'fs'; import { exec } from 'child_process'; import { promisify } from 'util'; import { basename } from 'path'; const execAsync = promisify(exec); const s3 = new S3Client({ region: 'us-east-1' }); async function downloadFileFromS3(bucket, key, downloadPath) { const getObjectParams = { Bucket: bucket, Key: key }; const command = new GetObjectCommand(getObjectParams); const { Body } = await s3.send(command); return new Promise((resolve, reject) => { const fileStream = createWriteStream(downloadPath); Body.pipe(fileStream); Body.on('error', reject); fileStream.on('finish', resolve); }); } async function uploadFileToS3(bucket, key, filePath) { const fileStream = createReadStream(filePath); const uploadParams = { Bucket: bucket, Key: key, Body: fileStream }; try { await s3.send(new PutObjectCommand(uploadParams)); console.log(`File uploaded successfully to ${bucket}/${key}`); } catch (err) { console.error("Error uploading file: ", err); throw new Error('Failed to upload file to S3'); } } function parseDuration(durationStr) { const parts = durationStr.split(':'); return parseInt(parts[0]) * 3600 + parseInt(parts[1]) * 60 + parseFloat(parts[2]); } export async function handler(event) { const videoBucket = "video-interaction-content"; const videoKey = event.videoKey; const audioBucket = "audio-call-recordings"; const audioKey = event.audioKey; const outputBucket = "synched-audio-video"; const outputKey = `combined_${basename(videoKey, '.mp4')}.mp4`; const audioStartSeconds = new Date(event.audioStart).getTime() / 1000; const videoStartSeconds = new Date(event.videoStart).getTime() / 1000; const audioDurationSeconds = event.audioDuration / 1000; const timeDifference = audioStartSeconds - videoStartSeconds; try { const videoPath = `/tmp/${basename(videoKey)}`; const audioPath = `/tmp/${basename(audioKey)}`; await downloadFileFromS3(videoBucket, videoKey, videoPath); await downloadFileFromS3(audioBucket, audioKey, audioPath); //Initialize file list with video let filelist = [`file '${videoPath}'`]; let totalVideoDuration = 0; // Initialize total video duration // Create first blank video if needed if (timeDifference < 0) { const blankVideoDuration = Math.abs(timeDifference); const blankVideoPath = `/tmp/blank_video.mp4`; await execAsync(`/opt/bin/ffmpeg -f lavfi -i color=c=black:s=1280x720:r=25 -c:v libx264 -t ${blankVideoDuration} ${blankVideoPath}`); //Add first blank video first in file list filelist.unshift(`file '${blankVideoPath}'`); totalVideoDuration += blankVideoDuration; console.log(`First blank video created with duration: ${blankVideoDuration} seconds`); } const videoInfo = await execAsync(`/opt/bin/ffmpeg -i ${videoPath} -f null -`); const videoDurationMatch = videoInfo.stderr.match(/Duration: ([\d:.]+)/); const videoDuration = videoDurationMatch ? parseDuration(videoDurationMatch[1]) : 0; totalVideoDuration += videoDuration; // Calculate additional blank video duration const additionalBlankVideoDuration = audioDurationSeconds - totalVideoDuration; if (additionalBlankVideoDuration > 0) { const additionalBlankVideoPath = `/tmp/additional_blank_video.mp4`; await execAsync(`/opt/bin/ffmpeg -f lavfi -i color=c=black:s=1280x720:r=25 -c:v libx264 -t ${additionalBlankVideoDuration} ${additionalBlankVideoPath}`); //Add to the end of the file list filelist.push(`file '${additionalBlankVideoPath}'`); console.log(`Additional blank video created with duration: ${additionalBlankVideoDuration} seconds`); } // Create and write the file list to disk const concatFilePath = '/tmp/filelist.txt'; await fsPromises.writeFile('/tmp/filelist.txt', filelist.join('\n')); const extendedVideoPath = `/tmp/extended_${basename(videoKey)}`; //await execAsync(`/opt/bin/ffmpeg -f concat -safe 0 -i /tmp/filelist.txt -c copy ${extendedVideoPath}`); // Use -vsync vfr to adjust frame timing without full re-encoding await execAsync(`/opt/bin/ffmpeg -f concat -safe 0 -i ${concatFilePath} -c copy -vsync vfr ${extendedVideoPath}`); const outputPath = `/tmp/output_${basename(videoKey, '.mp4')}.mp4`; //await execAsync(`/opt/bin/ffmpeg -i ${extendedVideoPath} -i ${audioPath} -map 0:v:0 -map 1:a:0 -c:v copy -c:a aac -b:a 192k -shortest ${outputPath}`); await execAsync(`/opt/bin/ffmpeg -i ${extendedVideoPath} -i ${audioPath} -map 0:v:0 -map 1:a:0 -c:v copy -c:a aac -b:a 192k -shortest -r 25 ${outputPath}`); console.log('Video and audio have been merged successfully'); await uploadFileToS3(outputBucket, outputKey, outputPath); console.log('File upload complete.'); return { statusCode: 200, body: JSON.stringify('Video and audio have been merged successfully.') }; } catch (error) { console.error('Error in Lambda function:', error); return { statusCode: 500, body: JSON.stringify('Failed to process video and audio.') }; } }
已尝试方案
- 尝试重新编码拼接后的文件,但Lambda函数超时
- 尝试创建与原MP4参数一致(25fps)的空白视频,期望无需重新编码,但问题依旧
- 尝试在FFmpeg命令中指定帧率25、添加/移除
-vsync参数,未解决问题
解决建议
1. 修复视频拼接的时间基准与帧率问题
concat协议直接复制流时,不同视频的时间基准(timebase)、GOP结构不匹配会导致帧率和时长计算错误。可以:
- 创建空白视频时强制匹配原视频的时间基准:
# 先获取原视频的timebase videoTimebase=$(ffmpeg -i ${videoPath} 2>&1 | grep -oP 'timebase=\K[\d/]+') # 创建空白视频时指定参数 ffmpeg -f lavfi -i color=c=black:s=1280x720:r=25 -c:v libx264 -t ${blankVideoDuration} -timebase ${videoTimebase} -g 25 ${blankVideoPath} - 放弃
-c copy快速拼接,改用concat滤镜轻量编码,统一参数:ffmpeg -f concat -safe 0 -i ${concatFilePath} -c:v libx264 -r 25 -s 1280x720 -crf 23 -preset fast ${extendedVideoPath}-preset fast可平衡编码速度与文件大小,降低Lambda超时概率。
2. 修正音频合并的时长控制
去掉-shortest参数,强制视频时长匹配音频总时长:
# 先获取音频精确时长 audioDuration=$(ffmpeg -i ${audioPath} -show_entries format=duration -v quiet -of csv="p=0") # 合并时指定时长 ffmpeg -i ${extendedVideoPath} -i ${audioPath} -map 0:v:0 -map 1:a:0 -c:v copy -c:a aac -b:a 192k -t ${audioDuration} -r 25 ${outputPath}
3. Lambda性能优化
- 选择更大内存的Lambda实例:Lambda的CPU、网络带宽与内存正相关,内存越大编码速度越快,减少超时概率
- 使用Graviton2优化的FFmpeg层:针对ARM架构优化FFmpeg,启用硬件加速编码
- 清理临时文件:在每步操作后删除无用中间文件,避免
/tmp空间不足导致错误
4. 提升时长解析准确性
替换原有的正则匹配方式,用FFmpeg直接输出时长数值:
async function getMediaDuration(filePath) { const { stderr } = await execAsync(`/opt/bin/ffmpeg -i ${filePath} -show_entries format=duration -v quiet -of csv="p=0"`); return parseFloat(stderr.trim()); }
内容的提问来源于stack exchange,提问作者Kendra Broom
相关产品推荐
相关产品推荐

