You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.25 22:02:59