资讯动态

Spring Boot + FFmpeg + HLS:零基础搭建最新视频展示系统

发布时间:2026/9/9 23:17:30 来源:尧图企业网站定制
做视频站或内容平台开发时最常听到的一句话是“最新视频来袭快来看看吧”。看起来这只是运营人员发的一条推送文案但对后端和前端开发者来说它背后是一整套视频处理流程文件接收、格式检测、转码、分片、入库、列表查询、播放器拉流。很多初学 Spring Boot 的开发者拿到“视频上传”需求后直接写一个 MultipartFile 保存到本地磁盘再用 HTML 的 Video 标签指向本地文件。结果视频在 Chrome 能放换一个编码格式后播放不出来视频一大上传就超时移动到弱网环境后进度条拖动非常卡。问题的本质在于视频处理不是文件复制而是编解码、分片、存储和分发的问题。这次我们从零搭建一个“最新视频展示系统”。后端使用 Spring Boot视频处理交给 FFmpeg播放协议采用 HLSHTTP Live Streaming前端用原生 video 标签或 hls.js 播放。整篇文章会从概念、环境、代码、验证、排错到上线建议完整串起来。读者看完后不只能跑通一个最小系统也能理解“视频上线”这条链路里每个环节为什么存在。1. 先看一条视频从上传到播放的路转码、分片、索引各做什么1.1 视频不是文件复制为什么不能直接把 MP4 丢给前端运营上传到系统的视频可能是 MP4、AVI、MOV、MKV编码可能是 H.264、H.265、VP9、MPEG-4 等。浏览器对视频格式的支持并不统一比较稳妥的组合是 H.264 视频编码和 AAC 音频编码。如果直接拿一个 H.265 编码的 MKV 文件给播放器很多浏览器无法解码表现就是黑屏、只有声音或直接提示无法播放。另一个问题是点播体验。一个 1GB 的 MP4 文件如果服务器直接返回给浏览器浏览器通常需要下载足够多的字节才能开始播放。用户拖动进度条服务端返回整个文件网络稍差就会非常卡。HLS 的解决思路是把视频切成若干个小分片比如每 10 秒一个 TS 文件再用一个 m3u8 索引文件按顺序指向这些分片。播放器先读 m3u8再按当前进度拉取对应的 TS 分片拖动进度条时只下载需要的片段。FFmpeg 在这里的作用就是完成“输入任意常见格式 - H.264/AAC 编码 - 切片 - 生成 m3u8 索引”的工作。1.2 一条完整视频链路的职责划分“最新视频来袭”这句话最终落到系统里可以拆成下面这条链路环节职责常见实现上传接收接收前端提交的文件和标题Spring MVC MultipartFile格式检测判断文件真实编码防止伪造扩展名FFprobe 或 FFmpeg 探测转码切片统一转换为 H.264/AAC并切成 TS 分片FFmpeg HLS截图封面抽取视频关键帧用于列表页展示FFmpeg 截帧入库存状态保存标题、大小、封面、m3u8 地址、状态MySQL静态分发让浏览器能访问 m3u8 和 TS 文件静态资源映射或对象存储 CDN播放器渲染按 m3u8 顺序拉取分片并播放Video 标签 hls.js学习阶段可以把所有环节放在一台机器上用同步方式串起来。生产环境通常会把这些环节拆开上传和转码异步化文件放对象存储播放走 CDN。后面第 7 章会讲这些差别。1.3 “最新视频”本质上是一次列表查询“最新视频来袭”在后台对应一个非常简单的 SQL按创建时间倒序查视频表只返回转码完成、可以正常播放的记录。真正的工作量不在查询而在于“可以正常播放”这个状态是怎么推进的。视频上传后先处于 UPLOADING 状态转码开始时改成 TRANSCODING成功之后才是 PUBLISHED。如果直接上传完成就立即可见用户点开会看到空白播放器因为这时候 m3u8 文件还没生成。所以视频表必须有一个状态字段列表接口只暴露 PUBLISHED 的数据。2. 环境准备先确认 JDK、Maven、FFmpeg 三者可用2.1 开发机依赖清单在写代码之前先把环境检查一遍。下面以常见开发环境为例版本可以结合自己项目实际情况调整依赖用途建议JDK 8 或 11 或 17运行 Spring Boot 服务与 Spring Boot 版本匹配即可Maven 3.6构建项目也可以使用 GradleSpring Boot 2.7.x 或 3.xWeb 框架本文示例基于 Spring BootFFmpeg 4.x转码和切片安装后必须能执行 ffmpeg 命令MySQL 5.7 或 8.x视频元信息存储也可以用 H2 内存库做快速验证Docker可选快速启动 MySQL适合不想在本机装 MySQL 的场景如果本机没有 MySQL可以用 Docker 启动一个临时实例docker run -d \ --name video-mysql \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD123456 \ -e MYSQL_DATABASEvideo \ mysql:8.0这条命令会创建数据库video用户root密码123456。学习环境用可以生产环境必须换成复杂密码和独立账号。2.2 安装 FFmpeg 并验证FFmpeg 是整条链路的重点。Linux 环境可以使用包管理器安装apt-get update apt-get install -y ffmpegmacOS 使用 Homebrewbrew install ffmpegWindows 环境建议直接下载官方编译包解压后把 bin 目录加入 PATH。安装完成后执行ffmpeg -version能输出版本信息说明安装成功。再检查是否有 H.264 编码器ffmpeg -encoders | grep 264正常能看到libx264。如果没有这个编码器后续切片命令会失败需要安装带 x264 的 FFmpeg 版本。2.3 规划视频文件目录视频文件不能放在 Spring Boot 的项目目录里因为项目重新打包或服务器重启时容易丢失。建议单独规划一个数据目录mkdir -p /data/videos/origin mkdir -p /data/videos/hls mkdir -p /data/videos/covers mkdir -p /data/videos/tmp chmod -R 755 /data/videosorigin保存原始文件hls保存切片和 m3u8covers保存封面图tmp保存临时文件。目录分开的好处是方便做磁盘配额、清理策略和权限控制。Windows 环境可以替换为D:/video-data这样的路径配置时注意路径分隔符。3. 实现一个最小可运行的最新视频展示系统3.1 项目结构与依赖建立一个新的 Spring Boot 项目包名可以根据自己习惯调整。演示用到的核心依赖如下dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-jdbc/artifactId /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-test/artifactId scopetest/scope /dependency项目结构按常见分层组织src/main/java/com/example/video/ VideoApplication.java config/WebConfig.java controller/VideoController.java service/VideoService.java service/VideoTranscodeService.java dao/VideoDao.java entity/Video.java vo/VideoVO.java src/main/resources/ application.yml schema.sql src/main/webapp/ player.html这个结构不一定是最优解但对学习阶段够用也方便后续拆分成独立服务。3.2 数据库表设计视频表要保存用户关心的字段也要保存状态和错误信息。建表语句如下CREATE TABLE video ( id BIGINT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(128) NOT NULL, description VARCHAR(512), original_name VARCHAR(255), file_size BIGINT, m3u8_url VARCHAR(255), cover_url VARCHAR(255), status VARCHAR(20) NOT NULL DEFAULT UPLOADING, error_message VARCHAR(500), created_time DATETIME NOT NULL, updated_time DATETIME NOT NULL, KEY idx_status_created (status, created_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;状态字段负责控制视频是否可见。error_message在转码失败时记录异常方便排查。索引idx_status_created是为了让“最新视频列表”查询在数据量大时不至于全表扫描。3.3 上传接口和转码服务上传接口先接收文件再保存到origin目录最后调用转码服务。这里用一个简单的VideoService串起来。RestController RequestMapping(/api/video) public class VideoController { private final VideoService videoService; public VideoController(VideoService videoService) { this.videoService videoService; } PostMapping(/upload) public Long upload(RequestParam(file) MultipartFile file, RequestParam(title) String title) { return videoService.create(file, title); } GetMapping(/latest) public ListVideoVO latest(RequestParam(defaultValue 20) int limit) { return videoService.latest(limit); } }VideoService里完成文件保存和状态推进Service public class VideoService { private final VideoDao videoDao; private final VideoTranscodeService transcodeService; Value(${video.storage-root}) private String storageRoot; public Long create(MultipartFile file, String title) { String uuid UUID.randomUUID().toString(); String extension StringUtils.getFilenameExtension( file.getOriginalFilename()); File originDir new File(storageRoot, origin); if (!originDir.exists()) { originDir.mkdirs(); } File originFile new File(originDir, uuid . extension); try { file.transferTo(originFile.getAbsoluteFile()); } catch (IOException e) { throw new RuntimeException(save origin file failed, e); } Video video new Video(); video.setTitle(title); video.setOriginalName(file.getOriginalFilename()); video.setFileSize(file.getSize()); video.setStatus(UPLOADING); video.setCreatedTime(LocalDateTime.now()); video.setUpdatedTime(LocalDateTime.now()); Long id videoDao.insert(video); transcodeService.transcodeAsync(uuid, extension, id); return id; } }注意两点第一文件名使用 UUID 而不是用户原始文件名避免路径穿越和重名覆盖第二转码不能卡在上传请求里太久演示代码里可以先同步调用也可以放到线程池。后面第 8 章会讲异步化的完整思路。VideoTranscodeService使用 ProcessBuilder 调用 FFmpegService public class VideoTranscodeService { Value(${video.ffmpeg-path}) private String ffmpegPath; Value(${video.hls-time}) private int hlsTime; public void transcode(String sourcePath, String outputDir) { File dir new File(outputDir); if (!dir.exists()) { dir.mkdirs(); } ListString command new ArrayList(); command.add(ffmpegPath); command.add(-y); command.add(-i); command.add(sourcePath); command.add(-c:v); command.add(libx264); command.add(-c:a); command.add(aac); command.add(-hls_time); command.add(String.valueOf(hlsTime)); command.add(-hls_list_size); command.add(0); command.add(-hls_segment_filename); command.add(outputDir /part_%03d.ts); command.add(outputDir /index.m3u8); ProcessBuilder builder new ProcessBuilder(command); builder.redirectErrorStream(true); try { Process process builder.start(); try (BufferedReader reader new BufferedReader( new InputStreamReader(process.getInputStream()))) { String line; while ((line reader.readLine()) ! null) { System.out.println(line); } } int exitCode process.waitFor(); if (exitCode ! 0) { throw new IllegalStateException(ffmpeg exit code: exitCode); } } catch (Exception e) { throw new IllegalStateException(transcode failed, e); } } }不要把一个 ffmpeg 命令写成连续字符串再通过Runtime.exec去执行。因为命令里包含路径和文件名一旦路径里有空格就会出现参数错位。ProcessBuilder 接受参数列表能避开大部分空格问题。3.4 最新视频列表接口列表接口只返回PUBLISHED状态的视频。VideoDao里用 JdbcTemplate 实现查询Repository public class VideoDao { private final JdbcTemplate jdbcTemplate; public VideoDao(JdbcTemplate jdbcTemplate) { this.jdbcTemplate jdbcTemplate; } public ListVideoVO selectLatest(int limit) { String sql SELECT id, title, description, cover_url, m3u8_url, file_size, created_time FROM video WHERE status PUBLISHED ORDER BY created_time DESC, id DESC LIMIT ?; return jdbcTemplate.query(sql, (rs, rowNum) - { VideoVO vo new VideoVO(); vo.setId(rs.getLong(id)); vo.setTitle(rs.getString(title)); vo.setDescription(rs.getString(description)); vo.setCoverUrl(rs.getString(cover_url)); vo.setM3u8Url(rs.getString(m3u8_url)); vo.setFileSize(rs.getLong(file_size)); vo.setCreatedTime(rs.getTimestamp(created_time).toLocalDateTime()); return vo; }, limit); } }这里 CRUD 没有使用 MyBatis 或 JPA是为了让链路更透明。实际项目可以根据团队规范选择 ORM。3.5 前端播放页列表页拿到接口返回的m3u8Url后跳转到播放页。播放页的关键是兼容浏览器。原生 HLS 在 Safari 里可以直接播放Chrome 和 Firefox 需要 hls.js 帮助解析。!DOCTYPE html html head meta charsetUTF-8 title视频播放/title /head body video idvideo controls width720 height405/video script srchttps://cdn.jsdelivr.net/npm/hls.js1/script script const video document.getElementById(video); const params new URLSearchParams(window.location.search); const m3u8Url params.get(url); if (video.canPlayType(application/vnd.apple.mpegurl)) { video.src m3u8Url; } else if (window.Hls Hls.isSupported()) { const hls new Hls(); hls.loadSource(m3u8Url); hls.attachMedia(video); } else { alert(当前浏览器不支持 HLS 播放); } /script /body /htmlm3u8Url必须是浏览器能直接访问的完整地址。如果和后端同一域名可以用相对路径如果前后端分离则需要配置 CORS或者把所有资源放到同一个域名下。4. 核心配置与参数说明FFmpeg 参数和上传边界4.1 application.yml 配置Spring Boot 的配置文件需要包含数据源、文件上传大小和自定义视频路径server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/video?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 servlet: multipart: max-file-size: 500MB max-request-size: 500MB video: storage-root: /data/videos ffmpeg-path: /usr/bin/ffmpeg hls-time: 10max-file-size是单个文件大小限制max-request-size是整个请求大小限制。如果运营需要上传 1GB 大视频用这种同步上传方式容易造成 Tomcat 内存压力生产环境要考虑分片上传或对象存储直传。再配置静态资源映射让浏览器能访问/videos/**Configuration public class WebConfig implements WebMvcConfigurer { Value(${video.storage-root}) private String storageRoot; Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/videos/**) .addResourceLocations(file: storageRoot /); } }注意addResourceLocations末尾必须有斜杠否则 Spring 拼接文件路径时会出错。这段配置只适合单机学习环境。生产环境文件不在本机时要用对象存储的 CDN 地址不需要本地映射。4.2 FFmpeg HLS 参数速查与调优FFmpeg 转 HLS 的命令比较固定但参数含义要清楚参数作用推荐值调大/调小影响-hls_time 10每个分片时长约 10 秒6 到 15分片越短起播越快文件数量越多分片越长索引越小拖动精度差-hls_list_size 0m3u8 保留全部分片0点播场景默认只保留最近几个分片直播场景适用点播会丢片段-hls_segment_filename指定分片文件命名规则part_%03d.ts保持同一目录内递增命名-c:v libx264视频编码使用 H.264libx264兼容性最好码率需要结合分辨率控制-c:a aac音频编码使用 AACaac浏览器普遍支持-threads 2限制 CPU 线程数默认自动生产环境要限制防止转码打满机器最关键的是-hls_list_size 0。这个参数最容易踩坑如果设置成默认值或 10生成出来的 m3u8 只保留最近 10 个分片播放器播到后面就没有文件了。对点播系统来说m3u8 必须是完整索引。4.3 上传限制与路径安全文件上传场景至少要考虑三类风险。第一路径穿越。用户可能把原始文件名设置成../../etc/passwd直接拼接文件名会让文件写到项目目录之外。解决方式是保存文件时使用 UUID用户原始文件名只作为展示字段存数据库。第二伪造扩展名。用户把木马文件命名为.mp4上传服务端如果只看扩展名可能会把恶意文件放进静态目录。稳妥做法是用 FFprobe 读取实际编码信息ffprobe -v error -select_streams v:0 -show_entries streamcodec_name -of defaultnoprint_wrappers1 input.mp4如果返回的不是视频编码说明文件不是合法视频。生产环境可以在上传后、转码前增加这个检查。第三资源耗尽。上传接口不限制文件大小、不限制视频时长可能被大文件把磁盘写满。学习环境可以限制固定大小生产环境要加上配额、频控和异步清理任务。5. 运行与验证从启动到看到“最新视频”出现5.1 启动服务与健康检查先准备一条测试视频。如果没有现成文件可以用 FFmpeg 生成ffmpeg -f lavfi -i testsrcduration10:size1280x720:rate30 \ -f lavfi -i sinefrequency440:duration10 \ -c:v libx264 -c:a aac -shortest /tmp/demo.mp4这条命令生成 10 秒、1280x720 分辨率、带声音的测试 MP4用来验证上传和转码链路完全够用。启动服务mvn spring-boot:run服务启动后先调用一次最新视频接口确认能返回空列表curl http://localhost:8080/api/video/latest?limit10预期输出是空数组[]。如果这一步报 404 或 500先检查数据库连接和项目是否成功启动。5.2 上传视频并观察转码日志上传刚才生成的测试视频curl -F file/tmp/demo.mp4 \ -F title测试视频001 \ http://localhost:8080/api/video/upload接口返回视频 ID。同时观察服务端日志可以看到 FFmpeg 输出的转码过程包括输入流信息、编码参数、切片进度。转码完成后检查输出目录ls -l /data/videos/hls/如果看到类似index.m3u8和part_000.ts等文件说明转码成功。再查数据库SELECT id, title, status, m3u8_url FROM video ORDER BY id DESC;状态应该是PUBLISHEDm3u8_url是指向静态资源地址的字符串。再调用最新视频接口这次应该能返回刚上传的视频。5.3 播放验证打开播放页例如http://localhost:8080/player.html?url/videos/hls/{uuid}/index.m3u8浏览器播放器窗口显示测试画面进度条可以拖动。按 F12 打开开发者工具切到 Network 面板刷新页面后能看到对index.m3u8的请求以及多个part_000.ts、part_001.ts的请求。这些分片按顺序加载说明 HLS 播放链路已经打通。验证清单如下检查项预期结果失败表现上传接口返回返回数字 ID500 或超时转码日志出现 ffmpeg 命令和进度命令报错或无日志视频目录存在 index.m3u8 和多个 ts只有原始文件数据库状态PUBLISHEDTRANSCODING 或 FAILED最新列表接口返回该视频不返回或报错播放页能看到画面且能拖动黑屏、白屏、转圈Network 面板出现 m3u8 和 ts 请求只有 html 请求没有媒体请求6. 常见问题排查转码失败、白屏、列表不更新的处理路径6.1 FFmpeg 找不到或命令执行失败现象是上传后状态一直停在 UPLOADING服务端日志出现IOException: Cannot run program ffmpeg。优先检查系统里 FFmpeg 是否真的可用which ffmpeg ffmpeg -version如果可用再看配置文件video.ffmpeg-path是否指向正确。Windows 环境把路径写成了D:/ffmpeg/bin/ffmpeg.exe在 YAML 里要注意反斜杠转义。确认路径后重启服务再测试。FFmpeg 执行失败的另一类原因是编码器不支持。日志里出现Unknown encoder libx264时说明安装的 FFmpeg 没有 x264 编码器需要换一个完整版本。6.2 HLS 播放白屏或转圈现象是视频列表能返回但播放器一直转圈Network 面板能看到 m3u8 请求看不到 TS 分片请求或者 TS 请求全部 404。优先看 m3u8 内容。用 curl 查看curl http://localhost:8080/videos/hls/{uuid}/index.m3u8正常内容应该包含若干以part_开头的行。如果 m3u8 里的分片文件名带了http://127.0.0.1:8080这样的绝对地址而浏览器是用http://localhost:8080访问的就会出现跨域和访问不了的问题。FFmpeg 生成的 m3u8 默认使用相对路径比较省事。我们自己拼 m3u8Url 时也要小心不要拼接成错误的路径层级。另一个常见原因是浏览器不能直接打开 m3u8。Chrome 原生不支持 HLS必须加载 hls.js。如果你只用video srcxxx.m3u8在 Chrome 里测试白屏是正常现象不代表后端错误。6.3 视频列表迟迟不显示现象是上传转码都成功文件也生成了但/api/video/latest查不到数据。先查数据库状态SELECT id, title, status, error_message, m3u8_url FROM video ORDER BY id DESC;如果status还是TRANSCODING说明转码完成后的状态更新没有执行。检查转码方法里是否调用了videoDao.updateStatus(id, PUBLISHED)。如果status是FAILED查看error_message根据错误信息回到 6.1 或 6.2 那一步。查询语句里用了WHERE status PUBLISHED ORDER BY created_time DESC如果所有记录时间相同还需要id DESC作为排序补充否则列表顺序不稳定。6.4 大文件上传超时或磁盘写满现象是上传 1GB 视频时请求超时或者转码到一半磁盘写满。学习环境可以先把上传大小限制调大但真正的生产问题不能只靠调参数。大文件上传应该走分片上传服务端把视频先放对象存储转码任务丢到消息队列由独立的转码 worker 拉取处理。磁盘写满问题要在机器上加监控df -h /data/videos du -sh /data/videos/*如果origin目录增长很快说明需要做定时清理或转码成功后转移原始文件策略。6.5 排查顺序总表故障现象优先排查常用命令/日志关键字上传返回 500数据库连接、目录权限Cannot create directory、SQLException状态停在 UPLOADINGFFmpeg 命令是否执行IOException: Cannot run program状态变成 FAILED编码器、输入文件、路径Unknown encoder、Invalid data foundm3u8 能打开但播放器转圈hls.js 是否加载、路径层级浏览器 console 报错TS 返回 404静态资源映射、文件是否生成No mapping for GET /videos/...列表查询为空状态未更新、SQL 条件错误手工执行 SELECT 查看 status7. 从学习环境到生产环境不能省下的工程保障7.1 学习环境怎么跑通学习阶段的目标是把链路跑通而不是追求架构。可以接受同步转码、单机存储、明文配置。只要能做到“上传 - 转码 - 列表 - 播放”四个动作连贯就已经理解了这个系统的核心。测试视频不要用太长时间10 到 30 秒足够。数据库可以使用 Docker MySQL存储路径放在本机。前端播放页为了减少变量hls.js 也可以从 CDN 加载前提是开发机能访问外网。如果不能访问外网就把 hls.js 下载到本地静态目录。7.2 生产环境必须改的 6 个地方第一配置外置化。数据库密码、FFmpeg 路径、存储路径不能写死在 YAML 里提交到代码仓库要用环境变量或配置中心管理。第二异步任务化。转码耗时可能从几十秒到几十分钟不能放在上传请求里。至少也要用线程池推荐使用消息队列解耦。第三对象存储和 CDN。原始文件和转码结果放到对象存储播放地址用 CDN 域名。这样能降低单点磁盘压力也能让用户就近拉流。第四鉴权。如果视频内容是私有内容m3u8 和 TS 不能直接放在公开静态目录需要带签名或鉴权机制。公开视频也要防止接口被刷。第五监控与补偿。要给转码任务加状态机处理失败重试、超时取消和中间态扫描。日志里要记录视频 ID、任务 ID、FFmpeg 退出码和错误输出。第六回滚与备份。发版前备份数据库转码命令升级前保留旧 FFmpeg 版本出现批量失败时能快速切回。7.3 上线检查清单每次发布视频功能前可以按这份清单逐项核对数据库脚本已经执行video表存在索引正确。数据目录已创建启动用户有读写权限。ffmpeg -version正常libx264编码器可用。配置文件使用环境变量没有明文密码。静态资源映射和播放域名一致跨域配置正确。用测试视频完整跑一遍上传、转码、列表、播放。异常状态有补偿任务失败任务能被重新处理。磁盘、CPU、内存有监控告警阈值已设置。转码输出目录有清理策略原始文件有过期规则。8. 扩展方向与最佳实践8.1 从列表展示扩展到封面截图“最新视频”列表如果只有标题页面会显得很空。可以在转码前用 FFmpeg 抽取视频中间帧作为封面ffmpeg -y -i demo.mp4 -ss 00:00:03 -vframes 1 cover.jpg-ss放在-i之后表示在解码后定位优点是定位准确但速度稍慢。如果视频很长可以抽多帧后再选一帧。封面生成后把cover_url写入数据库列表页就有缩略图可用了。8.2 从同步转码改成异步任务目前演示代码在转码完成前不会返回响应这在长视频场景不现实。改造方向是引入一张任务状态表上传后先保存任务再通过线程池处理Async public void processVideo(Long videoId, String sourcePath, String outputDir) { try { transcodeService.transcode(sourcePath, outputDir); videoDao.updateStatus(videoId, PUBLISHED); } catch (Exception e) { videoDao.updateStatusAndError(videoId, FAILED, e.getMessage()); } }更成熟的做法是使用 RabbitMQ 或 Kafka转码 worker 单独部署避免 Tomcat 线程被转码任务占满。8.3 防止上传恶意文件上传接口必须在入口做文件内容校验。不要只信任Content-Type因为浏览器可以伪造。也不要只看扩展名可以把文件交给 FFprobe 探测ffprobe -v error -show_entries formatformat_name -of defaultnoprint_wrappers1 input.mp4如果返回结果为空或者不是常见视频容器格式直接拒绝。转码过程本身也会过滤掉大量非视频文件因为 FFmpeg 无法解码的文件根本转不动但早一步拦截可以减少无效任务和磁盘占用。8.4 最佳实践清单把“最新视频来袭”做成一个可维护的视频系统最值得记住的实践如下不要用用户原始文件名作为存储文件名统一使用 UUID。不要只保存 MP4按统一编码和 HLS 分片输出。不要在同步接口里转码超过 1 分钟的视频。不要把视频状态从上传直接改为发布必须经过转码成功环节。不要把 m3u8 里的分片路径写成硬编码域名尽量使用相对路径。不要用裸 catch 吞掉 FFmpeg 异常至少记录退出码和错误输出。磁盘监控和临时文件清理要提前做而不是等告警了再补。播放地址要区分公开访问和私有访问私有视频必须有鉴权。视频系统真正的复杂度不在于接口数量而在于文件状态、任务状态和播放地址这三类数据如何保持一致。先用最小系统把链路跑通再逐步加入异步、对象存储、CDN 和监控是成本最低的入门路径。对新手最有价值的练习不是反复抄一段上传代码而是手里有一条测试视频能看到它从本地文件变成 m3u8 索引再变成页面上可拖动的一条视频记录。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价