如何用FFmpeg的zoompan滤镜实现视频平滑缩放过渡?
问题:FFmpeg实现视频平滑缩放过渡
我开发了一款屏幕录制Web应用,用户录制完成后可进入编辑面板,基于时长、X/Y坐标等对视频进行缩放操作。作为后端开发者,我在Node.js中使用FFmpeg库实现了基础缩放,但无法实现平滑过渡效果。
当前使用的FFmpeg命令:
ffmpeg -i input.mp4 -vf " zoompan=z='if(between(in_time,0,04),2,1)':d=1:x='iw/2-(iw/(zoom+0.01)/2)':y='ih/2-(ih/zoom/2)',zoompan=z='if(between(in_time,05,09),2,1)':d=1:x='iw/2-(iw/(zoom+0.01)/50)':y='ih/2-(ih/zoom/1)',zoompan=z='if(between(in_time,11,15),2,1)':d=1:x='iw/50-(iw/(zoom+0.01)/10)':y='ih/2-(ih/zoom/2)' ,zoompan=z='if(between(in_time,18,20),2,1)':d=1:x='iw/2-(iw/(zoom+0.01)/1)':y='ih/2-(ih/zoom/50)' ,zoompan=z='if(between(in_time,22,25),2,1)':d=1:x='iw/30-(iw/(zoom+0.01)/50)':y='ih/2-(ih/zoom/50)', zoompan=z='if(between(in_time,27,30),2,1)':d=1:x='iw/2-(iw/(zoom+0.01)/2)':y='ih/50-(ih/zoom/30)'" output.mp4
- 输入视频:S3存储的sam.mp4
- 当前输出视频:S3存储的1678798893088_24d034f7469b5e79_x3JT9w.mp4
- 期望效果:实现类似示例视频中的平滑缩放过渡(缩放过程无突兀跳变,渐变式放大/缩小+位置移动)
请问如何实现这种平滑缩放过渡?是否需要添加额外依赖或修改现有代码?
解决方案
你的问题出在多次串联zoompan滤镜,且使用if(between)直接切换缩放值,导致缩放/位置跳变,没有过渡过程。以下是具体改进方案:
1. 改用单个zoompan滤镜实现多段平滑过渡
不要多次叠加zoompan,而是在单个滤镜内用线性插值表达式实现缩放和位置的渐变。核心是用lerp()函数(线性插值)或自定义计算,让缩放值z、x/y坐标在过渡时间段内从起始值平滑过渡到目标值。
示例命令(对应你的时间区间)
ffmpeg -i input.mp4 -vf "zoompan=z=' \ if(in_time<0, 1, \ if(between(in_time,0,1), lerp(1,2,(in_time-0)/1), \ if(between(in_time,1,4), 2, \ if(between(in_time,4,5), lerp(2,1,(in_time-4)/1), \ if(between(in_time,5,6), lerp(1,2,(in_time-5)/1), \ if(between(in_time,6,9), 2, \ if(between(in_time,9,10), lerp(2,1,(in_time-9)/1), \ if(between(in_time,10,11), 1, \ if(between(in_time,11,12), lerp(1,2,(in_time-11)/1), \ if(between(in_time,12,15), 2, \ if(between(in_time,15,18), 1, \ if(between(in_time,18,19), lerp(1,2,(in_time-18)/1), \ if(between(in_time,19,20), 2, \ if(between(in_time,20,22), 1, \ if(between(in_time,22,23), lerp(1,2,(in_time-22)/1), \ if(between(in_time,23,25), 2, \ if(between(in_time,25,27), 1, \ if(between(in_time,27,28), lerp(1,2,(in_time-27)/1), \ if(between(in_time,28,30), 2, \ lerp(2,1,(in_time-30)/1))))))))))))))))))' \ :x=' \ if(in_time<0, iw/2, \ if(between(in_time,0,1), lerp(iw/2, iw/2-(iw/(2+0.01)/2), (in_time-0)/1), \ if(between(in_time,1,4), iw/2-(iw/(2+0.01)/2), \ if(between(in_time,4,5), lerp(iw/2-(iw/(2+0.01)/2), iw/2, (in_time-4)/1), \ if(between(in_time,5,6), lerp(iw/2, iw/2-(iw/(2+0.01)/50), (in_time-5)/1), \ if(between(in_time,6,9), iw/2-(iw/(2+0.01)/50), \ if(between(in_time,9,10), lerp(iw/2-(iw/(2+0.01)/50), iw/2, (in_time-9)/1), \ if(between(in_time,10,11), iw/2, \ if(between(in_time,11,12), lerp(iw/2, iw/50-(iw/(2+0.01)/10), (in_time-11)/1), \ if(between(in_time,12,15), iw/50-(iw/(2+0.01)/10), \ if(between(in_time,15,18), iw/2, \ if(between(in_time,18,19), lerp(iw/2, iw/2-(iw/(2+0.01)/1), (in_time-18)/1), \ if(between(in_time,19,20), iw/2-(iw/(2+0.01)/1), \ if(between(in_time,20,22), iw/2, \ if(between(in_time,22,23), lerp(iw/2, iw/30-(iw/(2+0.01)/50), (in_time-22)/1), \ if(between(in_time,23,25), iw/30-(iw/(2+0.01)/50), \ if(between(in_time,25,27), iw/2, \ if(between(in_time,27,28), lerp(iw/2, iw/2-(iw/(2+0.01)/2), (in_time-27)/1), \ if(between(in_time,28,30), iw/2-(iw/(2+0.01)/2), \ lerp(iw/2-(iw/(2+0.01)/2), iw/2, (in_time-30)/1))))))))))))))))))' \ :y=' \ if(in_time<0, ih/2, \ if(between(in_time,0,1), lerp(ih/2, ih/2-(ih/2/2), (in_time-0)/1), \ if(between(in_time,1,4), ih/2-(ih/2/2), \ if(between(in_time,4,5), lerp(ih/2-(ih/2/2), ih/2, (in_time-4)/1), \ if(between(in_time,5,6), lerp(ih/2, ih/2-(ih/2/1), (in_time-5)/1), \ if(between(in_time,6,9), ih/2-(ih/2/1), \ if(between(in_time,9,10), lerp(ih/2-(ih/2/1), ih/2, (in_time-9)/1), \ if(between(in_time,10,11), ih/2, \ if(between(in_time,11,12), lerp(ih/2, ih/2-(ih/2/2), (in_time-11)/1), \ if(between(in_time,12,15), ih/2-(ih/2/2), \ if(between(in_time,15,18), ih/2, \ if(between(in_time,18,19), lerp(ih/2, ih/2-(ih/2/50), (in_time-18)/1), \ if(between(in_time,19,20), ih/2-(ih/2/50), \ if(between(in_time,20,22), ih/2, \ if(between(in_time,22,23), lerp(ih/2, ih/2-(ih/2/50), (in_time-22)/1), \ if(between(in_time,23,25), ih/2-(ih/2/50), \ if(between(in_time,25,27), ih/2, \ if(between(in_time,27,28), lerp(ih/2, ih/50-(ih/2/30), (in_time-27)/1), \ if(between(in_time,28,30), ih/50-(ih/2/30), \ lerp(ih/50-(ih/2/30), ih/2, (in_time-30)/1))))))))))))))))))' \ :d=1" -c:v libx264 -crf 23 output.mp4
关键说明:
- 每个缩放段增加了1秒过渡时间(比如0-1秒从1倍放大到2倍,1-4秒保持2倍,4-5秒缩小回1倍),你可以根据需求调整过渡时长(比如改成0.5秒)。
lerp(a,b,t)函数:a是起始值,b是目标值,t是过渡进度(0到1),自动计算中间的平滑值。- 保持
d=1确保每帧都重新计算缩放和位置,避免卡顿。
2. 优化Node.js代码实现动态生成命令
因为手动写长表达式很麻烦,你可以在Node.js中根据用户编辑的时间段(起始时间、结束时间、起始缩放/位置、目标缩放/位置)动态拼接FFmpeg的滤镜表达式。
示例伪代码:
const generateZoomPanFilter = (segments) => { let zExpr = ""; let xExpr = ""; let yExpr = ""; segments.forEach((seg, idx) => { const { start, end, startZoom, endZoom, startX, endX, startY, endY } = seg; const prevEnd = idx > 0 ? segments[idx-1].end : 0; // 过渡阶段(从之前的状态过渡到当前段的起始状态) if (prevEnd < start && idx > 0) { const transitionDur = start - prevEnd; zExpr += `if(between(in_time,${prevEnd},${start}), lerp(${segments[idx-1].endZoom},${startZoom},(in_time-${prevEnd})/${transitionDur}), `; xExpr += `if(between(in_time,${prevEnd},${start}), lerp(${segments[idx-1].endX},${startX},(in_time-${prevEnd})/${transitionDur}), `; yExpr += `if(between(in_time,${prevEnd},${start}), lerp(${segments[idx-1].endY},${startY},(in_time-${prevEnd})/${transitionDur}), `; } // 保持阶段(当前段的稳定状态) zExpr += `if(between(in_time,${start},${end}), ${endZoom}, `; xExpr += `if(between(in_time,${start},${end}), ${endX}, `; yExpr += `if(between(in_time,${start},${end}), ${endY}, `; }); // 收尾:默认值(视频开头或结尾的状态) zExpr += `${segments[0].startZoom})`.repeat(segments.length * 2); xExpr += `${segments[0].startX})`.repeat(segments.length * 2); yExpr += `${segments[0].startY})`.repeat(segments.length * 2); return `zoompan=z='${zExpr}':x='${xExpr}':y='${yExpr}':d=1`; }; // 用户编辑的缩放段示例 const zoomSegments = [ { start:0, end:4, startZoom:1, endZoom:2, startX:'iw/2', endX:'iw/2-(iw/(2+0.01)/2)', startY:'ih/2', endY:'ih/2-(ih/2/2)' }, { start:5, end:9, startZoom:1, endZoom:2, startX:'iw/2', endX:'iw/2-(iw/(2+0.01)/50)', startY:'ih/2', endY:'ih/2-(ih/2/1)' }, // 其他段... ]; const filter = generateZoomPanFilter(zoomSegments); const ffmpegCmd = `ffmpeg -i input.mp4 -vf "${filter}" -c:v libx264 -crf 23 output.mp4`;
3. 不需要额外依赖
不需要添加新的Node.js包或FFmpeg插件,只用原生FFmpeg的zoompan滤镜和表达式功能即可实现。
内容的提问来源于stack exchange,提问作者dinesh indn
相关产品推荐
相关产品推荐

