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

📅 2026/7/9 9:17:38
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; }四、分片上传 ControllerRestController RequestMapping(/api/video/upload) RequiredArgsConstructor Slf4j public class VideoChunkUploadController { PostMapping(/init) public MapString, 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 { ListString 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; }✅ 转码为标准 MP4moov 前置public void transcodeToMp4(String input, String output) throws Exception { ListString 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 MapString, 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 / 轮询Dockerffmpeg:6.0镜像