应用台导航页
  • 主页
  • 博客
  • 知识库
  • 工作台
  • 集萃
  • 友链
  • 关于
秒切——纯前端一键视频切割工具:浏览器也是 FFmpeg 的舞台
工具FFmpegWasmglintUI秒切

秒切——纯前端一键视频切割工具:浏览器也是 FFmpeg 的舞台

“秒切”是一款纯前端的视频切割工具。它基于FFmpeg WASM,在浏览器中实现视频按秒切割,无需后端支持。用户上传视频后,可选择快速或精确模式进行切割。工具界面简洁,操作便捷,还提供实时日志和进度显示。其技术栈包括Next.js、React、TypeScript等,核心库为@ffmpeg/ffmpeg

技术
更新于 2025-08-29
— 6532 字
返回

在当今这个短视频爆炸的时代,视频处理已经成为了许多开发者和内容创作者的必备技能。但你有没有想过,只用前端技术,就能在浏览器里实现视频切割呢?作者开发的秒切 正是这样一款强大的工具,它完全基于前端技术,在浏览器中就能轻松完成按秒(时间段)切割视频。

秒切 截图
秒切 截图

用户只需上传视频文件,选择快速模式或精确模式,然后设置每段视频的时长,点击“开始分割”按钮即可。快速模式适合大部分场景,能够快速完成分割,而精确模式则可以实现帧级精度,满足专业需求。用户可以实时查看FFmpeg处理日志,了解切割进度。处理完成后,用户可以在弹窗中直接播放所有分割片段。

秒切的便捷性还体现在它 Web 应用的特性:免安装使用上。用户无需安装任何软件,也不需要花费时间上传文件到服务器,直接在浏览器里就能完成分割。此外,它还支持移动端,随时随地都可以使用。

今天,就让我们一起在 秒切 这个案例的源代码中,探索如何利用 FFmpeg WASM 在浏览器中实现“按秒切割视频”的神奇功能。

技术栈与核心库

在开始之前,让我们先来了解一下本项目所使用的技术栈和核心库。

我们的技术栈包括 Next.js、React、TypeScript 和 Tailwind CSS,另外还使用了基于 tailwind css 的 UI 组件库 glint-ui。

而此功能的核心库则是 @ffmpeg/ffmpeg 和 @ffmpeg/util,它们将帮助我们在浏览器中运行 FFmpeg,实现视频处理的功能。

目录结构

需要说明的是,秒切是 Dors.——花野猫的数字花园 的一个子功能,其代码实现在 dors 这个 github 仓库中,所以目录结构也遵循 dors 的目录结构设计。整个功能其实是一个 Next.js app router 的路由,代码存放在 @/app/(projects)/video-splitter 目录下,主要文件包括:

  • page.tsx:页面文件,包含右侧的“使用指南”区域。
  • VideoSplitter.tsx:主功能组件,负责 FFmpeg 的初始化、视频切割流程以及弹窗预览。

1. 初始化 FFmpeg(WASM 加载)

FFmpeg 在浏览器中运行依赖于 WASM 核心文件。在项目中,我们通过 toBlobURL 从 CDN 拉取并加载这些文件。以下是初始化 FFmpeg 的代码:

// VideoSplitter.tsx(节选) const ffmpegRef = useRef(new FFmpeg()); const loadFFmpeg = async () => { const baseURL = "https://unpkg.com/@ffmpeg/core@0.12.6/dist/umd "; const ffmpeg = ffmpegRef.current; try { setFfmpegLoading(true); setFfmpegProgress(0); // 简单的“加载进度”模拟(UI 友好) const progressInterval = setInterval(() => { setFfmpegProgress((prev) => (prev >= 90 ? (clearInterval(progressInterval), 90) : prev + 10)); }, 100); await ffmpeg.load({ coreURL: await toBlobURL(`${baseURL}/ffmpeg-core.js`, "text/javascript"), wasmURL: await toBlobURL(`${baseURL}/ffmpeg-core.wasm`, "application/wasm"), }); clearInterval(progressInterval); setFfmpegProgress(100); setTimeout(() => { toast.success(t.ffmpegLoaded); setFfmpegLoaded(true); setFfmpegLoading(false); }, 500); } catch (error) { console.error(error); toast.error(t.ffmpegLoadFail); setFfmpegLoading(false); } }; useEffect(() => { loadFFmpeg(); }, []);

要点

  • 首次进入页面即加载 FFmpeg WASM。
  • UI 显示“加载中 + 进度条”,避免白屏。
  • 加载完成后设置 ffmpegLoaded,作为“开始分割”按钮的可用条件之一。

2. 选择与预览视频(点击/拖拽)

相关内容

  • 花野猫的班级毕业蹭饭地图制作工具概要设计

文章大纲

  • 技术栈与核心库
  • 目录结构
  • 1. 初始化 FFmpeg(WASM 加载)
    • 要点
  • 2. 选择与预览视频(点击/拖拽)
  • 3. 分割模式与 FFmpeg 命令
  • 4. 进度条与日志解析
    • 进度阶段
  • 5. 读取输出并计算片段真实时长
  • 6. 预览与下载(结果弹窗)
  • 7. 细节处理与产品体验
  • 9. 性能与限制
  • 10. 常见问题(FAQ)
  • 结语

选项
文章 ID: 331

相关内容

  • 花野猫的班级毕业蹭饭地图制作工具概要设计

    花野猫的班级毕业蹭饭地图制作工具概要设计

    更新于2026-10-23

  • 如何将 srt 和 ass 等外挂字幕内嵌到视频中

    如何将 srt 和 ass 等外挂字幕内嵌到视频中

    更新于2026-11-09

  • 借助 cursor 开发 web 视频切片工具

    借助 cursor 开发 web 视频切片工具

    更新于2026-03-24

  • 生日蜡烛模拟器 web 应用产品开发日志

    生日蜡烛模拟器 web 应用产品开发日志

    更新于2026-03-01

  • Aparecium —— 一个提供完整的中国五级行政区划在线查询功能的网页应用

    Aparecium —— 一个提供完整的中国五级行政区划在线查询功能的网页应用

    更新于2026-09-12

文件选择使用自定义的 BaseInputFileHeadless,同时支持拖拽区域。以下是代码示例:

// VideoSplitter.tsx(节选) <BaseInputFileHeadless ref={fileInputRef} accept="video/*" // @ts-ignore onChange={handleFileSelect} filterFileDropped={(file) => file.type.startsWith('video/')} renderContent={({ open, drop, files }) => ( <div className={cn("relative w-full min-h-72 ...", { /* 省略样式 */ })} onDragOver={(e) => { e.preventDefault(); setIsDragOver(true); }} onDragLeave={(e) => { e.preventDefault(); setIsDragOver(false); }} onDrop={handleDragDrop} onClick={open} > <video ref={videoRef} controls className={cn("w-full h-full object-contain", { hidden: !selectedFile })}> <source src="" type="video/mp4" /> </video> {/* 省略:未选择文件时的引导 UI、示例视频按钮与预加载 */} </div> )} />

选择文件后,生成一个临时 URL 并载入 <video>,同时读取原视频时长:

const loadVideoFile = async (file: File) => { const videoUrl = URL.createObjectURL(file); if (videoRef.current) { videoRef.current.src = videoUrl; videoRef.current.load(); videoRef.current.onloadedmetadata = () => { if (videoRef.current) setVideoDuration(videoRef.current.duration); }; } setSelectedFile(file); setSegments([]); setCurrentSegment(null); };

秒切-视频导入完成界面
秒切-视频导入完成界面

3. 分割模式与 FFmpeg 命令

应用提供两种模式,使用单选切换:

  • 快速模式:copy 直拷贝,不重编码,速度快但切点不一定“帧精确”。
  • 精确模式:H.264 + AAC 重新编码,切点更准但耗时更长。

对应的 FFmpeg 命令如下:

// VideoSplitter.tsx(节选) if (splitMode === "fast") { await ffmpeg.exec([ "-i", "input.mp4", "-c", "copy", "-map", "0", "-segment_time", duration.toString(), "-reset_timestamps", "1", "-f", "segment", `${selectedFile.name.split(".")[0]}%03d.mp4`, ]); } else { await ffmpeg.exec([ "-i", "input.mp4", "-c:v", "libx264", "-c:a", "aac", "-preset", "fast", "-segment_time", duration.toString(), "-segment_time_delta", "0.1", "-f", "segment", "-reset_timestamps", "1", "-map", "0:v:0", "-map", "0:a:0", `${selectedFile.name.split(".\n)[0]}%03d.mp4`, ]); }

在执行命令之前,需要将原始文件写入 FFmpeg 的虚拟文件系统:

await ffmpeg.writeFile("input.mp4", new Uint8Array(await selectedFile.arrayBuffer()));

4. 进度条与日志解析

精切切割模式下,展示进度条和日志
精切切割模式下,展示进度条和日志

FFmpeg 处理过程中会输出大量日志。我们通过 ffmpeg.on('log', handler) 订阅日志,粗略估算处理进度:

// VideoSplitter.tsx(节选) let currentProgress = 20; let frameCount = 0; let totalFrames = videoDuration > 0 ? Math.ceil(videoDuration * 30) : 0; // 简化估算为 30fps const logHandler = (log: any) => { const logText = log.message || log; if (typeof logText === 'string') setProcessingLogs((prev) => [...prev, logText]); const frameMatch = typeof logText === 'string' && logText.match(/frame=\s*(\d+)/); if (frameMatch) { frameCount = parseInt(frameMatch[1]); if (totalFrames > 0) { const frameProgress = Math.min((frameCount / totalFrames) * 50, 50); // 将 20%-70% 区间用于编码进度 currentProgress = 20 + frameProgress; setProgress(Math.round(currentProgress)); } } if (typeof logText === 'string' && logText.includes('video:') && logText.includes('audio:')) { setProgress(75); } }; ffmpeg.on('log', logHandler); // ... 执行命令 ffmpeg.off('log', logHandler);

进度阶段

  • 0%–20%:文件写入虚拟文件系统。
  • 20%–70%:依据日志中的 frame= 粗估。
  • 70%–100%:读取结果、拼装片段。

注意:这是“估算”进度,不同格式/设备可能会有偏差。

5. 读取输出并计算片段真实时长

执行完成后,枚举虚拟目录下的输出文件,读取为 Blob,并用临时 <video> 获取实际时长:

const files = await ffmpeg.listDir("."); const outputFiles = files.filter((f) => f.name !== "input.mp4" && f.name.endsWith(".mp4")); const newSegments: Segment[] = []; for (const outputFile of outputFiles) { const data = await ffmpeg.readFile(outputFile.name); const blob = new Blob([data], { type: "video/mp4" }); const url = URL.createObjectURL(blob); const tempVideo = document.createElement('video'); tempVideo.src = url; tempVideo.preload = 'metadata'; await new Promise<void>((resolve) => { tempVideo.onloadedmetadata = () => resolve(); tempVideo.load(); }); newSegments.push({ name: outputFile.name, url, blob, duration: tempVideo.duration || duration }); } setSegments(newSegments);

6. 预览与下载(结果弹窗)

秒切处理结果预览页
秒切处理结果预览页

处理完成后,将会弹出模态框,列出每个片段的可播放 <video> 与下载按钮:

// VideoSplitter.tsx(节选,Modal 内) {segments.map((segment, index) => ( <div key={segment.name} className="border rounded-lg overflow-hidden"> <div className="relative aspect-video"> <video src={segment.url} className="w-full h-full object-cover" preload="metadata" controls muted /> <div className="absolute top-2 right-2 bg-black/70 text-white text-xs px-2 py-1 rounded">#{index + 1}</div> </div> <div className="p-3 space-y-2"> <div className="flex items-center justify-between"> <span className="font-medium text-sm truncate">{segment.name}</span> </div> <div className="text-xs">时长:{Math.round(segment.duration)}s</div> <BaseButton variant="outline" size="sm" onClick={() => downloadSegment(segment)} className="w-full"> 下载 </BaseButton> </div> </div> ))}

批量下载只是顺序触发多次 a.click(),稍作延迟:

const downloadAllSegments = () => { segments.forEach((segment, index) => setTimeout(() => downloadSegment(segment), index * 100)); };

7. 细节处理与产品体验

为了让用户有更好的使用体验,秒切的实现还在细节上做了很多处理:

  • 删除/重置:支持“删除视频”和“返回原视频”。
  • 示例视频:提供“加载示例视频”,供用户测试使用。
  • 估算段数:Math.ceil(videoDuration / duration)。
  • 处理日志:在进度区域下滚动展示 FFmpeg 输出的关键行,方便排查问题。

9. 性能与限制

虽然我们的工具非常强大,但仍然有一些性能和限制需要注意:

  • WASM 首次加载:几十到上百 KB 的核心脚本 + wasm 文件,首次进入页面需要等待。
  • 内存与文件大小:完全取决于浏览器与设备,移动端内存更紧张,建议优先测试较小文件。
  • 进度估算:基于日志与帧数的“近似值”,不是硬性百分比。
  • 切割精度:快速模式不做重编码,切点可能落在关键帧附近;精确模式更准但耗时显著增加。
  • 兼容性:现代浏览器(支持 WebAssembly)。

10. 常见问题(FAQ)

  • Q:为什么点击“开始分割”是灰的?
    • A:需要满足三个条件:已选择视频、FFmpeg 加载完成、当前未在处理中。
  • Q:分割出来的片段时长不完全等于设置值?
    • A:快速模式下常见,因为不重编码;需要更精确请使用“精确模式”。
  • Q:能否导出为其他格式?
    • A:可以更换输出扩展名和编码参数,但请注意浏览器的可播放性和编码耗时。

结语

以上就是本应用的完整实现过程与关键代码。我们没有引入后端,没有“云端转码”,所有处理都在浏览器本地完成。你可以直接在此基础上进行二次开发,比如增加格式选择、增加时间轴 UI,或者把分割改为“按时间点列表”。

建议直接打开源码 dors/app/(projects)/video-splitter at main · huayemao/dors 进行对照阅读与调试。希望这篇文章能为你带来启发!

花野猫的班级毕业蹭饭地图制作工具概要设计

更新于2026-10-23

  • 如何将 srt 和 ass 等外挂字幕内嵌到视频中

    如何将 srt 和 ass 等外挂字幕内嵌到视频中

    更新于2026-11-09

  • 借助 cursor 开发 web 视频切片工具

    借助 cursor 开发 web 视频切片工具

  • 生日蜡烛模拟器 web 应用产品开发日志
  • Aparecium —— 一个提供完整的中国五级行政区划在线查询功能的网页应用
  • 更新于2026-03-24

    生日蜡烛模拟器 web 应用产品开发日志

    更新于2026-03-01

    Aparecium —— 一个提供完整的中国五级行政区划在线查询功能的网页应用

    更新于2026-09-12

    dors logoDors

    Dors 是花野猫开发为知识工作者打造的数字花园应用,包含的博客、个人记事本、及其他实用功能。

    花园

    • 花坛——博客
    • 果园——知识库

    工坊——作者开发的实用工具

    • 小记
    • 秒切——一键按秒分割视频
    • 中国重点高校地理位置可视化网站
    • 中国行政区划数据查询平台
    • excel 重命名工具

    misc

    • 生活章程
    • 画廊
    • just have fun!

    © 2022 - present. All Rights Reserved.滇ICP备2025063395号-1

    花野猫打造