news 2026/7/9 9:17:26

springboot+ffmpeg实现大视频文件的分片上传合并

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
springboot+ffmpeg实现大视频文件的分片上传合并

Spring Boot + FFmpeg 实现「大视频文件分片上传 → 服务端合并 → FFmpeg 校验/转码」,这是视频平台(点播、网课、安防回放)的标准做法。


一、整体设计思路(先看这个)

✅ 核心流程

前端 ├─ 1. 申请上传(获取 uploadId) ├─ 2. 分片上传(chunkIndex) ├─ 3. 通知合并 │ 后端 Spring Boot ├─ 临时目录:/upload/tmp/{uploadId}/ ├─ 保存分片:chunk_0001.tmp ├─ 合并分片:FileOutputStream ├─ FFmpeg 校验/转码(可选) └─ 最终视频:/video/{videoId}.mp4

⚠️关键点

  • ✅ 分片 ≠ FFmpeg 切片(segment)

  • ✅ 合并是二进制拼接

  • ✅ FFmpeg 只在合并完成后介入(校验 / 转码 / 截图)


二、目录结构约定

/upload ├─ tmp │ └─ 1690000000000_abc │ ├─ chunk_0000.tmp │ ├─ chunk_0001.tmp │ └─ ... └─ video └─ 1690000000000_abc.mp4

三、DTO / 常量定义

public class UploadConstants { public static final String TMP_DIR = "./upload/tmp/"; public static final String VIDEO_DIR = "./upload/video/"; public static final String CHUNK_PREFIX = "chunk_"; public static final String CHUNK_SUFFIX = ".tmp"; }

四、分片上传 Controller

@RestController @RequestMapping("/api/video/upload") @RequiredArgsConstructor @Slf4j public class VideoChunkUploadController { @PostMapping("/init") public Map<String, String> initUpload() { String uploadId = System.currentTimeMillis() + "_" + UUID.randomUUID(); File tmpDir = new File(UploadConstants.TMP_DIR + uploadId); tmpDir.mkdirs(); return Map.of("uploadId", uploadId); } @PostMapping("/chunk") public String uploadChunk( @RequestParam String uploadId, @RequestParam Integer chunkIndex, @RequestParam MultipartFile file ) throws Exception { File tmpDir = new File(UploadConstants.TMP_DIR + uploadId); if (!tmpDir.exists()) { throw new RuntimeException("uploadId 不存在"); } String chunkName = String.format("%s%04d%s", UploadConstants.CHUNK_PREFIX, chunkIndex, UploadConstants.CHUNK_SUFFIX); file.transferTo(new File(tmpDir, chunkName)); return "chunk uploaded"; } }

五、合并分片(核心)

✅ 合并逻辑(二进制拼接)

@Service @RequiredArgsConstructor @Slf4j public class VideoMergeService { public String mergeChunks(String uploadId, int totalChunks) throws Exception { File tmpDir = new File(UploadConstants.TMP_DIR + uploadId); if (!tmpDir.exists()) { throw new RuntimeException("分片目录不存在"); } new File(UploadConstants.VIDEO_DIR).mkdirs(); String finalFileName = uploadId + ".mp4"; File outputFile = new File(UploadConstants.VIDEO_DIR, finalFileName); try (FileOutputStream fos = new FileOutputStream(outputFile)) { for (int i = 0; i < totalChunks; i++) { File chunk = new File(tmpDir, String.format("%s%04d%s", UploadConstants.CHUNK_PREFIX, i, UploadConstants.CHUNK_SUFFIX)); if (!chunk.exists()) { throw new RuntimeException("缺失分片:" + i); } Files.copy(chunk.toPath(), fos); } } // 清理临时分片 deleteDirectory(tmpDir); return outputFile.getAbsolutePath(); } private void deleteDirectory(File dir) { File[] files = dir.listFiles(); if (files != null) { for (File f : files) f.delete(); } dir.delete(); } }

六、FFmpeg 校验 + 转码(强烈推荐)

合并后一定要用 FFmpeg 校验,否则可能出现:

  • 视频无法播放

  • 时长异常

  • moov box 在尾部(HTTP 播放失败)


✅ FFmpeg 校验视频完整性

public boolean validateVideo(String videoPath) throws Exception { List<String> cmd = new ArrayList<>(); cmd.add("ffmpeg"); cmd.add("-v"); cmd.add("error"); cmd.add("-i"); cmd.add(videoPath); cmd.add("-f"); cmd.add("null"); cmd.add("-"); ProcessBuilder pb = new ProcessBuilder(cmd); pb.redirectErrorStream(true); Process process = pb.start(); try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { String line; while ((line = reader.readLine()) != null) { log.error("ffmpeg error: {}", line); } } return process.waitFor() == 0; }

✅ 转码为标准 MP4(moov 前置)

public void transcodeToMp4(String input, String output) throws Exception { List<String> cmd = new ArrayList<>(); cmd.add("ffmpeg"); cmd.add("-i"); cmd.add(input); cmd.add("-c:v"); cmd.add("libx264"); cmd.add("-c:a"); cmd.add("aac"); cmd.add("-movflags"); cmd.add("faststart"); // ✅ moov 前置 cmd.add("-y"); cmd.add(output); new ProcessBuilder(cmd).inheritIO().start().waitFor(); }

七、合并接口

@PostMapping("/merge") public Map<String, String> merge( @RequestParam String uploadId, @RequestParam int totalChunks ) throws Exception { String rawVideoPath = videoMergeService.mergeChunks(uploadId, totalChunks); if (!videoMergeService.validateVideo(rawVideoPath)) { throw new RuntimeException("视频文件损坏"); } String finalPath = rawVideoPath.replace(".mp4", "_final.mp4"); videoMergeService.transcodeToMp4(rawVideoPath, finalPath); return Map.of( "rawVideo", rawVideoPath, "finalVideo", finalPath ); }

八、前端分片示例(JS 伪代码)

const chunkSize = 5 * 1024 * 1024; // 5MB let totalChunks = Math.ceil(file.size / chunkSize); for (let i = 0; i < totalChunks; i++) { const chunk = file.slice(i * chunkSize, (i + 1) * chunkSize); const form = new FormData(); form.append("uploadId", uploadId); form.append("chunkIndex", i); form.append("file", chunk); await fetch("/api/video/upload/chunk", { method: "POST", body: form }); } await fetch("/api/video/upload/merge?uploadId=" + uploadId + "&totalChunks=" + totalChunks);

九、断点续传(加分项)

功能

实现方式

已上传分片

前端上传前GET /uploaded-chunks?uploadId

秒传

MD5 校验文件是否存在

并发控制

Promise.all 限流

大文件

单分片 ≥ 5MB


十、生产级优化建议

必做

  • FFmpeg-movflags faststart

  • 合并后删除临时分片

  • 文件大小 + MD5 校验

  • 异步转码(@Async/ MQ)

进阶

  • 分布式:MinIO + Redis 记录分片

  • 秒传:Redis 存 fileMd5 → videoPath

  • 进度:WebSocket / 轮询

  • Docker:ffmpeg:6.0镜像


版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/9 9:13:33

ClickHouse MergeTree 引擎:排序键设计决定查询天花板

ClickHouse MergeTree 引擎&#xff1a;排序键设计决定查询天花板公司上了 ClickHouse 之后&#xff0c;一张 20 亿行的用户行为表&#xff0c;同样的数据量&#xff0c;有人查出结果只要 0.2 秒&#xff0c;有人等了 30 秒没出来。差距全在排序键上。今天聊聊排序键这个"…

作者头像 李华
网站建设 2026/7/9 9:11:24

LightRAG+Ollama实战:比GraphRAG更快

在RAG这个领域过去半年最大的变化不是 embedding 或检索算法&#xff0c;而是从向量 RAG 进化到了知识图谱 RAG&#xff08;GraphRAG&#xff09;。微软 2024 年发了 GraphRAG&#xff0c;能解决传统向量 RAG 解决不了的"全局问答"和"跨文档推理"问题&…

作者头像 李华
网站建设 2026/7/9 9:11:11

计算机毕业设计之连锁花店管理平台

快速发展的社会中&#xff0c;人们的生活水平都在提高&#xff0c;生活节奏也在逐渐加快。为了节省时间和提高工作效率&#xff0c;越来越多的人选择利用互联网进行线上打理各种事务&#xff0c;然后线上管理系统也就相继涌现。与此同时&#xff0c;人们开始接受方便的生活方式…

作者头像 李华
网站建设 2026/7/9 9:10:33

新世代已至:GPT-5.6与Claude 5系列全面解读

新世代已至&#xff1a;GPT-5.6与Claude 5系列全面解读 过去半个月&#xff0c;AI大模型领域迎来了一场堪称“密集轰炸”的发布潮。从Claude Fable 5的首次亮相&#xff0c;到被紧急叫停又恢复&#xff0c;再到GPT-5.6的“精准截胡”——这一轮更新的意义&#xff0c;已经超出了…

作者头像 李华
网站建设 2026/7/9 9:08:38

实战分享:用Python搭建一个简单的Web应用

当你敲下 python app.py&#xff0c;终端里跳出一行“Running on http://127.0.0.1:5000”&#xff0c;意味着你刚刚亲手造出了一个能响应HTTP请求的东西——哪怕它只是返回一串“Hello World”。把一个想法变成可访问的URL&#xff0c;这种从0到1的掌控感&#xff0c;是学习We…

作者头像 李华
网站建设 2026/7/9 9:04:42

计算机毕业设计之列车乘客购物系统

随着信息技术和网络技术的飞速发展&#xff0c;人类已进入全新信息化时代&#xff0c;传统管理技术已无法高效&#xff0c;便捷地管理信息。为了迎合时代需求&#xff0c;优化管理效率&#xff0c;各种各样的管理系统应运而生&#xff0c;各行各业相继进入信息管理时代&#xf…

作者头像 李华