资讯详情

资讯详情

从上传到播放:Spring Boot+FFmpeg打造短视频全链路

接手或练习“唐人小视频”这类短视频项目时真正卡住进度的往往不是播放器不会写而是整条链路没有理顺视频传到哪个目录数据库存什么地址封面由谁生成客户端用哪个接口拉列表为什么有的视频在浏览器能播、在微信小程序里却是黑屏。下面这套内容以代号“唐人小视频”的最小可行项目为例演示一条从上传视频到端上播放的完整路径。这套项目适合两类读者一是刚学完 Spring Boot 基础想拿一个完整小项目练手的人二是已经能写 CRUD但没做过视频、图片这类媒体资源处理的人。本文不讨论真实线上产品的运营和内容审核规则只聚焦普通工程开发。原材料没有给出明确版本和业务细节所以代码中的依赖版本、目录和接口字段都应按你自己的环境调整落地前先确认版本兼容性。读完下面的内容你能得到一张清晰的开发地图先说数据和存储怎么设计再说后端上传与信息流接口怎么写然后讲客户端如何拉列表并尽量降低播放卡顿最后给一份可照着排查的常见问题清单和上线前检查表。1. 短视频应用的核心链路先有素材才有信息流短视频产品表面上是“上下滑视频”背后却是内容管理、文件存储、媒体处理、数据查询和客户端渲染多条链路一起工作。动手写代码之前先把链路拆清楚后面不容易返工。1.1 “唐人小视频”要解决的最小问题把范围收敛到技术练习场景“唐人小视频”的核心问题只有四个用户上传一个视频文件。系统把文件保存到可访问的存储位置。系统提取封面和时长并把元信息写入数据库。客户端拉取视频列表按卡片或全屏滑动的形式播放。很多短视频项目都是在这四个问题之上逐步叠加关注、点赞、评论、推荐排序和审核能力。先跑通最小闭环再扩展互动功能是更稳妥的开发顺序。这里有一个容易被忽略的判断数据库里保存的应该是“可访问的播放地址”而不是真正的二进制文件。数据库负责描述“这个视频叫什么、是谁传的、封面在哪”文件系统或对象存储负责保存真实文件。如果在数据库里塞二进制或者把大文件直接写在业务表同一条记录里后期无论分页还是备份都会变得非常痛苦。1.2 技术选型要同时考虑开发速度和素材兼容性为了演示方便下面把一个最小可行方案固定为这种组合后端Spring Boot MyBatis-Plus MySQL。文件存储开发环境使用本地磁盘目录生产环境替换为对象存储或自建 MinIO。媒体处理调用 FFmpeg 提取封面和读取视频基本信息。客户端使用 uni-app 做小程序端演示核心代码同样适用于普通 H5。这套组合的好处是本地调试成本低。Spring Boot 负责接口MySQL 负责元数据本地目录负责文件保存FFmpeg 负责封面切帧。不需要在一开始就引入复杂的微服务、消息队列和媒体转码集群。模块开发环境方案生产环境建议接口层Spring Boot 单体应用保持单体必要时按接口拆分视频文件本地 /media 目录对象存储或 MinIO接入 CDN元数据MySQL 单表MySQL 分库分表或读写分离视规模而定封面提取FFmpeg 同步调用异步任务或转码服务客户端uni-app 本地跑小程序、App、H5 多渠道发布选型不是越复杂越好。学习环境里用本地目录最直观地看到文件落盘等理解了路径、访问地址和文件生命周期之后再迁移对象存储时只会多改一层存储适配器。1.3 练习前先明确这不是“只写后端”或“只写页面”的任务短视频项目天然涉及跨端协作。后端同学如果只返回一个video_url字段不知道客户端能不能播放就会在格式和协议上踩坑。前端同学如果只写播放器不理解封面和时长从哪来也会在联调时频繁被“为什么没有封面”这类问题打断。所以本文建议把“唐人小视频”当成一个完整闭环来做后端提供一个能上传、能列出视频列表的最小接口集合客户端提供一个能播放、能分页加载的视频列表。先让数据从头走到尾再考虑推荐、评论和用户体系。2. 环境准备和目录结构先把地基打平写代码前先把依赖、工具和目录确认好。短视频项目的环境问题比普通 CRUD 更敏感因为多了一个 FFmpeg 工具链还多了一类容易踩坑的静态资源路径配置。2.1 需要提前安装的软件后端工程依赖 JDK、Maven、MySQL。媒体处理依赖 FFmpeg。客户端如果跑 uni-app还需要 Node.js 和 HBuilderX 或对应命令行工具。软件建议版本用途检查命令JDK17Spring Boot 3.x 要求运行后端java -versionMaven3.8管理依赖mvn -vMySQL8.0存储元数据mysql --versionFFmpeg4.4封面提取、读取视频信息ffmpeg -versionNode.js16运行 uni-app 开发工具链node -v注意Spring Boot 2.x 支持 JDK 8Spring Boot 3.x 要求 JDK 17。如果公司的 Java 基线是 8就选择 Spring Boot 2.7 对应的依赖组合不要照搬本文示例版本。原始项目没有明确版本信息这是最容易踩的第一个坑。安装 FFmpeg 后在命令行执行ffmpeg -version如果输出类似ffmpeg version 4.4.2说明命令已经在 PATH 中。Spring Boot 代码里调用 FFmpeg 时可以直接使用ffmpeg命令也可以配置完整路径如/usr/bin/ffmpeg。2.2 后端目录按“接口、服务、存储适配”拆分建议创建以下工程结构。直接使用默认的单模块 Spring Boot 工程即可。tangren-video-server/ ├── pom.xml ├── src/main/java/com/tangren/video/ │ ├── TangrenVideoApplication.java │ ├── config/ │ │ ├── MediaConfig.java │ │ └── WebConfig.java │ ├── controller/ │ │ └── VideoController.java │ ├── service/ │ │ ├── VideoService.java │ │ ├── VideoServiceImpl.java │ │ └── MediaStorageService.java │ ├── mapper/ │ │ └── VideoMapper.java │ ├── entity/ │ │ └── Video.java │ └── dto/ │ ├── VideoVO.java │ └── UploadResult.java ├── src/main/resources/ │ ├── application.yml │ └── mapper/ │ └── VideoMapper.xml └── src/main/webapp/ 或静态资源目录controller只负责接收参数和返回结果service负责业务规则MediaStorageService负责文件落盘和访问地址生成。这样做的原因是如果开发阶段用本地目录后续切到对象存储只需要替换MediaStorageService的实现类controller和service基本不用改。2.3 配置 media-root 时要特别注意路径结尾application.yml中关于媒体目录的配置如下server: port: 8080 spring: servlet: multipart: max-file-size: 100MB max-request-size: 120MB datasource: url: jdbc:mysql://localhost:3306/tangren_video?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver mybatis-plus: mapper-locations: classpath*:mapper/*.xml configuration: map-underscore-to-camel-case: true app: media-root: ./media access-base: /media ffmpeg-path: ffmpegapp.media-root是文件在服务器上的物理保存路径。app.access-base是客户端访问文件的 URL 前缀。学习了这个配置之后必须理解一件事物理路径和访问路径不是一回事。./media/video/xxx.mp4是磁盘上的位置/media/video/xxx.mp4是拼接 URL 时的路径。./media是相对路径会随着 Java 进程的启动目录变化。最稳妥的本地开发方式是在项目根目录运行应用这样./media就是项目下的media目录。如果使用 IDEA 启动要确认当前工作目录是模块根目录否则文件会意外写到别处。Spring Boot 中还需要配置一个静态资源映射才能把物理目录暴露成 HTTP 可访问地址。下面这段代码的关键是file:前缀和末尾的斜杠。Configuration public class MediaConfig implements WebMvcConfigurer { Value(${app.media-root}) private String mediaRoot; Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/media/**) .addResourceLocations(file: mediaRoot /); } }如果没有这个映射文件已经保存到了磁盘但客户端访问http://localhost:8080/media/video/xxx.mp4时仍然会得到 404。这是短视频项目里非常典型的“文件在但访问不到”现象。3. 后端实现视频表、上传接口和信息流接口后端代码要完成三个动作写入一条视频元数据、保存视频文件、提取封面和时长。为了保持正文可读这里给出的代码是骨架版本异常处理、权限校验和日志都写成了中文注释方便你按自己的工程模板补全。3.1 视频表不要只存一个名字和地址设计视频表时不要只设计id、title、url三个字段。短视频场景要求列表页能提前展示封面用户要看时长和清晰度运营要查文件状态所以需要把视频元信息拆清楚。CREATE TABLE t_video ( id bigint unsigned NOT NULL AUTO_INCREMENT COMMENT 主键, user_id bigint unsigned NOT NULL COMMENT 发布者用户ID, title varchar(128) NOT NULL DEFAULT COMMENT 标题, video_url varchar(512) NOT NULL COMMENT 播放地址, cover_url varchar(512) DEFAULT COMMENT 封面地址, video_duration int NOT NULL DEFAULT 0 COMMENT 视频时长秒, file_size bigint NOT NULL DEFAULT 0 COMMENT 文件大小字节, status tinyint NOT NULL DEFAULT 0 COMMENT 0待处理 1可播放 2处理失败 3已删除, like_count int NOT NULL DEFAULT 0 COMMENT 点赞数冗余字段, comment_count int NOT NULL DEFAULT 0 COMMENT 评论数冗余字段, created_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, updated_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间, PRIMARY KEY (id), KEY idx_user_id (user_id), KEY idx_status_created (status, created_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT短视频信息表;status字段在练习项目中很容易被忽略。视频上传后并不等于立即可播放它要先经过文件校验、封面提取、时长读取等步骤。如果某个视频的封面提取失败至少要把状态改成“处理失败”而不是让客户端拿一个空封面去请求。like_count和comment_count是典型的冗余计数练习阶段先写进去等到互动功能起来后再考虑用独立计数器或异步任务保证一致性。3.2 上传接口校验、落盘、封面、入库的顺序不能乱上传接口的完整顺序是先判断文件是否存在再判断大小和扩展名然后生成不重复的文件名并落盘接着调用 FFmpeg 提取封面和时长最后插入数据库。为什么顺序不能乱如果先入库再校验数据库里会出现一批失效记录。如果先提取封面再落盘原视频都没有保存封面提取自然无从谈起。先落盘再入库的好处是即使数据库事务失败文件还在磁盘上可以通过后续清理任务删除孤儿文件。下面是VideoController的核心接口RestController RequestMapping(/api/video) public class VideoController { private final VideoService videoService; public VideoController(VideoService videoService) { this.videoService videoService; } PostMapping(/upload) public VideoVO upload(RequestParam(userId) Long userId, RequestParam(file) MultipartFile file, RequestParam(value title, defaultValue ) String title) { return videoService.upload(userId, file, title); } GetMapping(/feed) public ListVideoVO feed(RequestParam(defaultValue 1) long pageNum, RequestParam(defaultValue 10) int pageSize) { return videoService.feed(pageNum, pageSize); } }在VideoServiceImpl中处理顺序需要拆成几个清晰步骤public VideoVO upload(Long userId, MultipartFile file, String title) { // 1. 前置校验 if (file.isEmpty()) { throw new BizException(上传文件为空); } long maxSize 100L * 1024 * 1024; if (file.getSize() maxSize) { throw new BizException(文件大小不能超过 100MB); } String originalName file.getOriginalFilename(); String ext extractExt(originalName); if (!allowedExt.contains(ext.toLowerCase())) { throw new BizException(不支持的视频格式); } // 2. 生成唯一文件名并落盘 String videoObjectKey /video/ UUID.randomUUID() . ext.toLowerCase(); Path videoPath Paths.get(mediaRoot, videoObjectKey).normalize(); Files.createDirectories(videoPath.getParent()); file.transferTo(videoPath.toAbsolutePath()); // 3. 提取封面 String coverKey /cover/ UUID.randomUUID() .jpg; Path coverPath Paths.get(mediaRoot, coverKey).normalize(); boolean coverOk ffmpegService.extractCover(videoPath, coverPath, 1); // 4. 生成访问 URL 并入库 Video video new Video(); video.setUserId(userId); video.setTitle(title); video.setVideoUrl(accessBase videoObjectKey); video.setCoverUrl(coverOk ? accessBase coverKey : ); video.setStatus(coverOk ? 1 : 2); videoMapper.insert(video); return convertToVO(video); }这段代码有两点需要注意。第一文件对象键使用 UUID而不是用户上传的原文件名。原因是原文件名不可信可能包含中文、特殊字符甚至路径穿越符号直接拼到路径中有安全风险。第二文件保存后生成的 URL 是/media/video/uuid.mp4这种拼接串数据库只存字符串不负责定位真实文件。扩展名白名单建议只放常见视频格式mp4、mov、m4v。即使是mp4也要在后续用ffprobe读取编码信息因为短视频平台要求的统一编码往往是 H.264 AAC不少手机录制的视频是 HEVC 编码在部分旧设备或小程序环境中会播放失败。练习阶段可以先只记录编码信息生产环境建议做统一转码。3.3 FFmpeg 提取封面时要从失败信息反推参数FFmpeg 的封面提取命令可以先用纯命令行验证再写成 Java 代码。ffmpeg -y -i /tmp/test.mp4 -ss 00:00:01 -frames:v 1 /tmp/cover.jpg参数说明-y输出文件存在时直接覆盖。-i指定输入文件。-ss 00:00:01从第 1 秒开始截取。-frames:v 1只输出 1 帧视频画面。最后一个参数是输出图片路径。在 Java 中用ProcessBuilder调用时不要直接使用Runtime.exec拼接命令字符串。因为路径中如果有空格拼接方式会导致命令解析错误。正确做法是把命令拆成列表ListString command new ArrayList(); command.add(ffmpegPath); command.add(-y); command.add(-i); command.add(videoPath.toString()); command.add(-ss); command.add(00:00:01); command.add(-frames:v); command.add(1); command.add(coverPath.toString()); ProcessBuilder builder new ProcessBuilder(command); builder.redirectErrorStream(true); Process process builder.start(); // 读取输出流避免子进程输出缓冲区写满导致阻塞 String output readAll(process.getInputStream()); int exitCode process.waitFor(); if (exitCode ! 0) { log.warn(封面提取失败视频路径{}, ffmpeg输出{}, videoPath, output); return false; } return Files.exists(coverPath);短视频文件的封面提取经常选择第 1 秒因为很多视频第 0 帧是黑屏或品牌画面。实际项目中可以把截取秒数做成配置运营可以根据内容类型调整。3.4 信息流接口列表分页不要只靠 pageNum 和 pageSize信息流接口的返回字段至少要包含客户端展示所需的完整数据public class VideoVO { private Long id; private Long userId; private String title; private String videoUrl; private String coverUrl; private Integer videoDuration; private Integer likeCount; private Integer commentCount; private String createdAt; }接口返回 JSON 时字段名建议使用驼峰风格前端直接使用videoUrl而不是video_url。客户端拿到列表后仅根据videoUrl是否为空就能判断视频是否可播放根据coverUrl是否为空决定封面区域显示占位图。分页有两种常见方案。第一种是页数分页public ListVideoVO feed(long pageNum, int pageSize) { PageVideo page new Page(pageNum, pageSize); LambdaQueryWrapperVideo wrapper new LambdaQueryWrapper(); wrapper.eq(Video::getStatus, 1) .orderByDesc(Video::getCreatedTime); PageVideo result videoMapper.selectPage(page, wrapper); return result.getRecords().stream().map(this::convertToVO).toList(); }这种方法在数据量小的练习项目里没什么问题。但如果视频量增长OFFSET越来越大数据库需要跳过大量行查询会越来越慢。更合适的信息流分页方式是游标分页每次请求带上上一页最后一条视频的created_time或id用WHERE created_time ?实现向前翻页。练习阶段建议先实现页数分页但要知道它的边界。进入内容量大的阶段后优先改成基于id或created_time的游标分页。4. 客户端播放列表先让视频能出画面再追求顺滑服务端接口就绪后客户端要解决的是“如何展示一堆视频卡片并且不会一打开页面就卡死”。4.1 不要一次性渲染大量 video 标签很多新手在信息流列表里直接循环渲染video导致页面一次性创建了几十个播放器实例。移动端不管是小程序还是 H5视频播放器都是重量级组件同时存在十几个实例必然会出现卡顿、黑屏、内存上涨。正确的做法是列表区域先用封面图展示只有用户滑动到了当前卡片才把对应的videoUrl绑定到video组件视频真正开始播放。当前卡片滑走之后把视频地址清空让播放器销毁。uni-app 里可以用v-if控制视频组件是否创建view classvideo-card v-foritem in list :keyitem.id image v-ifplayingId ! item.id || !item.videoUrl classcover :srcitem.coverUrl modeaspectFill / video v-else :idvideo- item.id classplayer :srcitem.videoUrl :controlstrue :autoplaytrue object-fitcontain erroronPlayError(item) / view classtitle{{ item.title }}/view /view对应的事件处理逻辑methods: { onPlay(id) { this.playingId id; }, onEnded() { // 当前播放结束可以选择自动播放下一条 const index this.list.findIndex(v v.id this.playingId); if (index this.list.length - 1) { this.playingId this.list[index 1].id; } }, onPlayError(item) { // 上报错误提示用户视频暂时无法播放 console.error(play error, item.videoUrl); } }当playingId不是当前卡片时卡片上只渲染封面图。这样列表页仍然能上下滑动查看封面只有真正进入播放状态时才创建播放器。对于竖屏全屏的短视频产品还需要监听滑动事件在页面切换时更新playingId。4.2 拉取下一页时要注意重复请求和列表累积客户端分页加载不能只在触底时调一次接口。必须在事件回调里判断当前是否正在加载避免网络慢时一次触底触发多个请求造成列表出现重复数据。data() { return { list: [], pageNum: 1, pageSize: 10, loading: false, finished: false }; }, methods: { async loadFeed() { if (this.loading || this.finished) return; this.loading true; try { const res await request({ url: /api/video/feed, data: { pageNum: this.pageNum, pageSize: this.pageSize } }); if (res.data.length 0) { this.finished true; return; } this.list this.list.concat(res.data); this.pageNum 1; } finally { this.loading false; } } }这段代码虽然简单但把分页状态控制得很清楚loading防止重复请求finished防止已经到底后继续请求空列表。生产环境中信息流接口最好返回hasMore字段让前端直接判断是否还能继续加载而不是每次判断返回数组长度。4.3 客户端最容易忽略的访问协议问题如果服务端跑在http://localhost:8080小程序或手机端访问开发机接口时需要处理不同端的网络协议限制。微信小程序要求请求域名必须是 HTTPS并需要在后台配置合法域名。开发阶段可以在开发者工具中勾选“不校验合法域名”。iOS 的 App Transport Security 默认阻止 HTTP 明文请求如果使用普通 HTTP 调试需要在配置中允许任意加载或对指定域名例外。Android 9 及以上默认禁止明文流量需要在网络配置中显式允许。这些不是代码 bug而是移动端安全策略与 HTTP 开发环境的冲突。现象往往是PC 浏览器能打开视频地址手机扫码进入页面后视频一直转圈。排查时要先确认客户端是否能访问视频 URL再检查播放器状态。5. 运行验证不要只验证“接口返回 200”短视频项目的验证比普通接口复杂。接口返回 200 只能说明请求被接收不能说明视频文件真的落在磁盘、封面真的生成、数据库里真的有可用记录。所以验证阶段必须分三层检查。5.1 启动后端并验证静态资源映射先启动 Spring Boot 应用mvn spring-boot:run看到日志出现Tomcat started on port 8080后先访问一个已知存在的静态资源或直接调用健康检查接口。上传前准备一个测试视频文件例如/tmp/test.mp4。然后使用 curl 模拟上传curl -X POST \ -F userId1001 \ -F title唐人小视频示例内容 \ -F file/tmp/test.mp4 \ http://localhost:8080/api/video/upload返回示例{ id: 1, userId: 1001, title: 唐人小视频示例内容, videoUrl: http://localhost:8080/media/video/2f6a58c1-xxxx-4c2e-9a41-yyyy.mp4, coverUrl: http://localhost:8080/media/cover/8d3f2a11-xxxx-4f3b-8c40-zzzz.jpg, videoDuration: 15, likeCount: 0, commentCount: 0 }拿到返回结果后用浏览器打开videoUrl确认视频可以播放打开coverUrl确认封面可以显示。再调用信息流接口curl http://localhost:8080/api/video/feed?pageNum1pageSize10返回的列表里应该包含刚才上传的视频。5.2 检查磁盘、数据库和 FFmpeg 输出三层状态接口成功后进入磁盘目录查看ls -lh ./media/video ls -lh ./media/cover正常情况下两个目录下都能看到刚才上传的文件。如果cover目录为空大概率是 FFmpeg 执行失败。此时到应用日志里搜索封面提取失败复制日志里的视频路径手动执行 FFmpeg 命令定位原因。查询数据库确认元数据SELECT id, user_id, video_url, cover_url, status, video_duration FROM t_video ORDER BY id DESC;理想结果是status1cover_url非空video_duration大于 0。5.3 学习环境与生产环境的验证差异验证项学习环境做法生产环境做法大文件上传100MB 内即可对象存储分片上传服务端只接收回调视频格式本机能播即可必须统一转码为 H.264 AAC静态资源访问直接访问 8080 端口Nginx 或 CDN 域名访问并发上传单线程测试异步任务队列 失败重试数据可靠性本地磁盘对象存储多副本 数据库备份练习项目里FFmpeg 同步执行还能接受。生产环境如果每次上传都在 Tomcat 线程里同步跑 FFmpeg上传高峰期线程很快会被占满用户会观察到接口迟迟不返回。正确做法是先把文件保存成功和元数据落库再通过异步任务或消息队列触发封面提取和转码前端轮询视频状态。6. 常见问题排查播放、封面和分页是三大重灾区从现象倒推原因是短视频项目里最实用的能力。下面整理三大类高频问题每类都按“现象、原因、检查方式、解决方案”给线索。6.1 视频文件已存在但客户端无法播放现象上传成功服务端返回videoUrl磁盘文件也存在但浏览器或小程序播放黑屏、转圈或报错。排查顺序先用 curl 检查 URL 是否返回 200。curl -I http://localhost:8080/media/video/xxx.mp4如果 404检查MediaConfig中的路径是否以/结尾物理目录是否和app.media-root一致。如果 200 但播放失败用ffprobe检查编码格式。ffprobe -v error -show_streams -select_streams v \ -of json /tmp/test.mp4重点看codec_name和profile。如果是hevc部分环境不支持。如果是h264但profile是high且设备老旧也可能出现黑屏。解决思路是统一转码成 H.264 Baseline 或 Main profile。问题现象常见原因检查方式解决方案返回 404静态资源映射缺失检查MediaConfig确认路径以/结尾播放黑屏视频编码不是 H.264使用 ffprobe 查看统一转码手机访问不了环境是 HTTP检查系统日志开发阶段允许明文或改成 HTTPS6.2 上传成功但封面为空现象接口返回的coverUrl是空字符串数据库cover_url为空页面只显示灰色占位块。原因通常是 FFmpeg 没有执行成功。可能是命令不存在可能是视频本身第 1 秒无法解码。检查方式ffmpeg -y -i ./media/video/test.mp4 -ss 00:00:01 -frames:v 1 /tmp/cover.jpg执行后观察是否有Output file is empty或Invalid data found之类的错误。如果手动执行也不行说明输入文件有问题。如果手动执行可以但 Java 调用失败则检查应用日志中的 ffmpeg 路径和输出内容确认ProcessBuilder命令是否因为路径空格执行失败。还有一个隐藏坑Files.exists(coverPath)在子进程还没有完全结束时就执行会因为文件尚未落盘而返回 false。这就是为什么需要先process.waitFor()等待进程结束再检查文件是否存在。很多“明明命令成功但 coverOkfalse”的问题都出在这里。6.3 列表越滑越慢请求频繁重复现象页面刚开始正常滑到后面几分钟接口响应越来越慢或者触底一次发出了多个重复请求。原因有两层。服务端如果使用pageNum分页数据量增大后深翻页很慢。客户端如果缺少loading和finished保护快速滚动会触发多个并发的分页请求。解决方式服务端改成游标分页用WHERE id #{cursor} ORDER BY id DESC LIMIT #{pageSize}。客户端在请求发起前判断loading和finished只在非加载状态下触发请求。列表去重使用key字段为视频 ID避免网络重试产生重复项。对练习项目来说先用上面两条改造就能解决大部分缓慢问题。再往下就要引入缓存和推荐排序那是数据量增长后的下一步优化。7. 上线前检查清单与工程化建议视频项目上线前要做一次完整检查。下面是针对“唐人小视频”这类短视频后端项目的可执行清单每条都能直接照着验证。7.1 上线前逐项检查检查项检查内容通过标准文件校验扩展名、大小、MIME非法文件被拒绝并记录日志路径安全文件名使用 UUID数据库中不存在用户原始路径静态资源访问 URL 返回文件直接访问videoUrl能播放封面生成FFmpeg 执行成功cover_url非空且可访问数据库索引按状态和时间查询列表查询走索引客户端分页无重复请求触底只发一次请求访问协议小程序和 App 能访问HTTPS 域名配置正确日志记录上传、失败、播放错误日志可定位到具体视频失败清理处理失败的视频文件有任务扫描孤儿文件权限控制只有登录用户能上传上传接口校验身份这里面最容易在开发后期被忽略的是“失败清理”。FFmpeg 提取封面失败后视频文件已经落盘数据库记录status2。如果不做治理日积月累会产生大量无法播放的孤儿文件。建议每日扫描一次上传时间超过 1 小时但状态仍是待处理或失败的文件确认后删除磁盘文件。7.2 生产环境必须改掉的三个“开发时习惯”第一个开发习惯是把文件保存到本地应用目录。生产环境应当把视频放到对象存储或自建 MinIO数据库保存对象 key访问走 CDN 或对象存储提供的域名。这样就不存在“服务器重启后文件丢失”和“Tomcat 线程被媒体文件占满”的问题。第二个开发习惯是同步执行 FFmpeg。上传接口只应该做文件接收和元数据登记封面提取、转码和时长分析都放到异步任务里。客户端可以先播放没有封面的视频页面展示透明占位图等服务端处理完成后再刷新。第三个开发习惯是直接暴露应用端口给用户访问视频。生产环境建议用 Nginx 统一代理视频静态资源由 Nginx 或 CDN 处理Java 应用只服务动态接口。这样能减少 Java 进程的被攻击面也能让播放请求不经过 Tomcat 线程池。7.3 从练手项目走向真实产品的扩展顺序当最小闭环跑通后“唐人小视频”可以按下面顺序扩展用户账号体系注册、登录、Token 校验、发布者主页。内容审核上传后先进入审核状态审核通过再置为可播放。播放策略全屏上下滑、预加载下一两个视频、退后台暂停。互动计数点赞、评论、收藏注意数据库事务和计数一致性。推荐排序先按时间倒序再结合用户行为加重算法。视频处理统一转码、多码率输出、封面候选帧选择。监控告警上传成功率、转码失败率、播放失败率、首帧耗时。不建议跳过前面几步直接实现推荐算法。短视频产品的体验瓶颈通常不是推荐不够聪明而是基础链路不够稳定视频转圈、上传失败、审核漏判、封面缺失。基础链路越扎实后面的功能才有评估空间。对新手来说最有价值的练习不是把页面做得花哨而是把本文的上传、封面提取、列表播放和排查路径完整走一遍然后故意制造几个故障比如删掉 FFmpeg 可执行文件、改错媒体目录、上传一个非 H.264 视频再观察日志和现象。经历过这些故障后短视频项目的技术细节才会真正沉淀下来。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →