尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

基于Spring Boot与FFmpeg自建视频点播HLS播放链路

基于Spring Boot与FFmpeg自建视频点播HLS播放链路 如果你经常在群里收到“最新视频来袭快来看看吧”这类链接点进去之后大概率不是广告引导就是“视频加载失败”。但在后端开发者眼里“来一个视频功能”这句话并不是写一个video标签那么简单。真正要解决的是视频从哪里来、怎么存、怎么转、怎么播放、怎么防刷、怎么排错这一整条链路。这篇文章不打算讨论任何第三方平台的视频引流套路而是把“视频来袭”翻译成一个工程问题如何在不借助大型云点播服务的前提下自己动手搭建一套可用的视频点播播放链路。我们会从视频格式原理出发用 FFmpeg 完成转码切片用 Spring Boot 提供一个可鉴权的视频资源接口用 hls.js 在前端完成播放最后给出生产环境常见问题与最佳实践。读完这篇文章你可以独立完成“上传 MP4 → 转成 HLS 分片 → 后端提供 m3u8 和 ts 文件 → 前端正常播放”的完整流程也能理解为什么很多视频网站不直接用 MP4 播放而要打成 m3u8 分片。1. 这篇文章真正要解决的问题视频点播开发中有一个很容易被低估的环节视频资源准备。很多新手拿到一个播放地址后直接把它塞到前端播放器里结果出现各种奇怪现象打开几十秒还没有画面拖动进度条后转圈很久手机 4G 环境下卡顿严重不同浏览器表现不一样有些浏览器直接不播放视频文件在服务器上占了好几个 G并发一高带宽被打满播放地址被人拿去随意下载、盗链。这些问题看起来是播放器问题但根源大多数在“视频源”和“分发链路上”。传统做法是给服务器放一个 MP4 文件然后前端用video播放。对于小流量内部系统这没有问题一旦要考虑宽带成本、加载速度、拖拽体验和防盗链就必须引入流媒体切片方案。HLS 是当前兼容性最好、落地成本最低的方案之一。它的核心思路是把一个完整视频切成若干个小文件比如 6 秒一个切片然后通过一个索引文件m3u8告诉播放器“按这个顺序播放”。这样做的好处是播放器可以边下载边播放不需要等整个 MP4 加载完拖动进度条只需要下载对应时间段的切片响应更快可以根据网络情况切换不同码率的切片实现自适应播放分片文件可以做 CDN 缓存降低源站压力可以通过签名 URL 或自定义 HTTP 头做防盗链。本文后面的内容会以这条链路为主线展开用 FFmpeg 把 MP4 转为 HLS 切片用 Spring Boot 暴露 m3u8 和 ts 文件用 hls.js 完成浏览器播放。适合正在做视频课程、内容管理后台、企业培训系统、个人视频站等功能的读者参考。2. 视频点播的核心概念与原理在动手之前先统一几个概念。这里不展开讲复杂的流媒体协议只讲能支撑后续实操的部分。2.1 容器格式与编码格式很多人会把“MP4”理解为一种视频编码其实它只是容器格式。容器格式负责把视频编码数据、音频编码数据、字幕、元信息封装在一起。MP4 可以装 H.264 编码的视频也可以装 H.265HEVC编码的视频。真正影响播放兼容性的是编码格式。目前兼容性最好的是 H.264几乎所有的浏览器、手机、电视端都支持。H.265 压缩率高、文件更小但兼容性明显不如 H.264。FFmpeg 转码时要优先保证输出 H.264 编码这样后续播放环节的麻烦会少很多。2.2 HLS 协议与 m3u8 文件HLSHTTP Live Streaming是 Apple 提出的基于 HTTP 的自适应流媒体协议。它把视频切成很多小段通常每个段是 2 到 10 秒文件后缀名是.ts同时生成一个文本索引文件.m3u8里面记录切片文件的路径、时长、顺序。m3u8 文件内容大致长这样#EXTM3U #EXT-X-VERSION:3 #EXT-X-TARGETDURATION:6 #EXT-X-MEDIA-SEQUENCE:0 #EXTINF:6.000000, segment-00000.ts #EXTINF:6.000000, segment-00001.ts #EXTINF:3.760000, segment-00002.ts #EXT-X-ENDLIST播放器拿到这段文本后会依次请求并播放后面这些 ts 文件。如果服务器返回的 Content-Type 不对或者 CORS 配置有问题现代浏览器里的播放器就会直接拒绝解析。2.3 为什么不用一个 MP4 直接播放短小的 MP4 直接播放问题不大但视频变长后会出现两个问题moov元数据通常位于文件尾部服务端需要先读取到元数据才能开始播放。如果服务端不支持range请求或元数据没有前置用户就要等很久。用户想看最后 5 分钟内容播放器需要从文件中间拖数据网络请求频繁且浪费带宽。HLS 把问题拆小了。每个 ts 切片体积很小播放器按需拉取拖动进度条时只需要计算目标时间点对应的切片位置加载成本大大降低。这是视频点播场景下 HLS 替代 MP4 的主要原因。2.4 切片与码率HLS 切片有一个重要参数hls_time表示每段切片的时长。时长设置越短进度条拖动的粒度越细但切片文件数量越多请求量也越大。一般点播场景设置为 4 到 6 秒比较合适。码率代表单位时间内视频的数据量码率越高画质越好文件也越大。转码时要根据目标用户网络环境来权衡。本节先使用一条通用转码命令后续会在最佳实践部分介绍多码率自适应的思路。3. 环境准备与前置条件本文的示例需要准备以下环境。具体版本请以实际项目为准这里不写死因为版本更新很快重点演示通用思路。3.1 工具清单一台 Linux 服务器或本机 macOS/Linux 环境FFmpeg 命令行工具JDK 8 及以上版本Maven 或 GradleSpring Boot 项目基础环境一个用于测试的 MP4 视频文件。FFmpeg 安装方式根据操作系统不同略有差异。以 CentOS 和 macOS 为例# CentOS/RHEL 系列 sudo yum install -y epel-release sudo yum install -y ffmpeg # macOS 使用 Homebrew brew install ffmpegWindows 用户可以到 FFmpeg 官网下载可执行文件将ffmpeg.exe所在目录加入 PATH。验证安装是否成功ffmpeg -version如果出现ffmpeg version相关输出说明安装成功。这里常见的一个坑是系统自带的 ffmpeg 没有编译进libx264编码器导致转码 H.264 时报错。排查方式也很简单ffmpeg -encoders | grep libx264如果没有任何输出说明当前 FFmpeg 版本不支持 libx264需要安装更完整的构建版本或者使用其他 H.264 编码器如h264_vaapi、h264_qsv等但优先建议解决 libx264 缺失问题。3.2 项目目录规划建议在服务器上规划统一的视频资源目录方便后续权限控制和清理/data/videos ├── lesson-01 │ ├── lesson-01.m3u8 │ ├── segment-00000.ts │ ├── segment-00001.ts │ └── ... └── lesson-02这样每个视频一个目录m3u8 和 ts 放在一起。后端接口只需要限定访问/data/videos目录即可路径设计上更安全。4. FFmpeg 将 MP4 转成 HLS 切片转码是整条链路中最关键的一步。这里给出一个适合点播场景的基础命令并解释每个参数的含义。ffmpeg -i input.mp4 \ -profile:v main \ -level 4.0 \ -c:v libx264 \ -crf 23 \ -preset medium \ -c:a aac \ -ac 2 \ -ar 44100 \ -b:a 128k \ -hls_time 6 \ -hls_playlist_type vod \ -hls_list_size 0 \ -f hls output/lesson-01.m3u8参数说明-profile:v main指定 H.264 编码档次。main兼容性较好baseline更老但体积更大high画质更好但对老设备要求高。-level 4.0编码级别影响分辨率、帧率、码率组合。一般 1080p 视频配 4.0 或 4.1 足够。-c:v libx264视频编码器使用 H.264。-crf 23恒定质量模式数值越小画质越好文件越大。推荐范围是 18 到 28默认 23。如果是教学视频23 到 25 性价比比较高。-preset medium编码速度预设可选fast、medium、slow等。slow压缩效果更好但转码时间更长。-c:a aac音频编码使用 AAC浏览器兼容性好。-ac 2双声道。-ar 44100音频采样率。-b:a 128k音频码率。-hls_time 6每个 ts 切片时长 6 秒。-hls_playlist_type vod表示这是点播视频生成的 m3u8 会带有#EXT-X-ENDLIST标记。-hls_list_size 0不限制 m3u8 中的切片数量。如果不加这个参数默认可能只保留最近 5 个切片导致视频不完整。-f hls输出格式为 HLS。执行成功后output目录下会生成一个 m3u8 文件和多个 ts 文件。查看 m3u8 内容可以确认切片是否正常cat output/lesson-01.m3u8如果只看到几个切片且没有#EXT-X-ENDLIST说明命令参数不对检查-hls_playlist_type vod和-hls_list_size 0是否完整传入。5. 用 Spring Boot 搭建视频资源服务FFmpeg 转码之后这些 ts 文件本质上还是静态文件。最简单的方案是用 Nginx 直接映射目录但实际业务中通常需要做权限校验、播放记录、按课程或用户维度隔离资源所以在后端写一个接口更通用。5.1 项目依赖与配置创建一个普通的 Spring Boot Web 项目只需要引入 Web 相关依赖。如果项目使用 Maven在pom.xml中至少需要dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency数据库、鉴权框架等项目根据实际需求引入本文示例不引入额外安全框架接口中的鉴权逻辑只做演示。在application.yml中配置视频根目录video: root-path: /data/videos也可以在 Java 中直接写路径但用ConfigurationProperties配置更便于部署环境切换。5.2 视频资源访问接口下面的例子实现了一个简单的文件访问接口接收课程目录和文件名两个参数拼接出实际路径并校验路径是否落在视频根目录内。这个校验非常重要否则恶意请求可能通过../../访问服务器任意文件。// 文件路径src/main/java/com/example/videodemo/controller/VideoController.java RestController RequestMapping(/api/video) public class VideoController { private final Path videoRoot; public VideoController(Value(${video.root-path}) String videoRoot) { this.videoRoot Paths.get(videoRoot).toAbsolutePath().normalize(); } GetMapping(/playlist/{courseId}/{fileName}) public ResponseEntityResource getVideo( PathVariable String courseId, PathVariable String fileName) throws IOException { Path courseDir videoRoot.resolve(courseId).normalize(); Path file courseDir.resolve(fileName).normalize(); // 路径穿越防护确认解析后的路径仍然在视频根目录下 if (!file.startsWith(videoRoot)) { return ResponseEntity.badRequest().build(); } if (!Files.exists(file) || !Files.isRegularFile(file)) { return ResponseEntity.notFound().build(); } String mimeType; if (fileName.endsWith(.m3u8)) { mimeType application/vnd.apple.mpegurl; } else if (fileName.endsWith(.ts)) { mimeType video/mp2t; } else { mimeType Files.probeContentType(file); } Resource resource new FileSystemResource(file); return ResponseEntity.ok() .contentType(MediaType.parseMediaType(mimeType)) .cacheControl(CacheControl.maxAge(Duration.ofMinutes(10))) .body(resource); } }这段代码有几个关键点normalize()和startsWith()是路径穿越防护的标配缺少任何一个都不安全。m3u8 文件的 Content-Type 必须写成application/vnd.apple.mpegurl。有些服务器返回application/octet-stream浏览器播放器会拒绝解析。ts 文件的 Content-Type 是video/mp2t。加了 10 分钟缓存减少重复请求对源站的压力。如果前端单独部署在另一个端口还需要配置跨域。最简单的方式是在 Controller 类上加CrossOrigin(origins http://localhost:5500)或者使用全局 CORS 配置类。生产环境建议把可信任域名配置在配置中心或配置文件中。5.3 全局跨域配置如果业务中有多个接口都需要跨域访问更推荐全局配置。下面是一个配置类示例// 文件路径src/main/java/com/example/videodemo/config/CorsConfig.java Configuration public class CorsConfig { Bean public WebMvcConfigurer corsConfigurer() { return new WebMvcConfigurer() { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/api/**) .allowedOrigins(http://localhost:5500) .allowedMethods(GET, HEAD) .allowedHeaders(*) .maxAge(3600); } }; } }这里的allowedOrigins要换成实际前端域名不要直接写*否则带凭证的请求会被浏览器拒绝而且也不安全。6. 前端用 hls.js 播放 m3u8 文件大部分现代浏览器不能直接播放 m3u8需要借助第三方库。这里使用 hls.js它会把 m3u8 解析后在浏览器内部转成 fMP4再交给视频元素播放。6.1 最小播放页面创建一个index.html内容如下!DOCTYPE html html head meta charsetUTF-8 title视频点播播放示例/title script srchttps://cdn.jsdelivr.net/npm/hls.js1.5.13/dist/hls.min.js/script style #video { width: 100%; max-width: 960px; } /style /head body video idvideo controls/video script const video document.getElementById(video); const videoUrl http://localhost:8080/api/video/lesson-01/lesson-01.m3u8; if (Hls.isSupported()) { const hls new Hls(); hls.loadSource(videoUrl); hls.attachMedia(video); hls.on(Hls.Events.MANIFEST_PARSED, function () { video.play(); }); } else { // Safari 原生支持 HLS可以直接使用 video 标签 video.src videoUrl; } /script /body /html说明Hls.isSupported()用于判断浏览器是否支持 MSE 和 HLS 解析能力。如果支持则创建 Hls 实例并加载 m3u8。如果不支持说明是 Safari 这类原生支持 HLS 的浏览器直接把地址赋值给video.src。6.2 播放地址跨域注意点使用 hls.js 播放时浏览器会以 XHR 方式请求 m3u8 和 ts 文件。这意味着后端必须返回正确的 CORS 头否则播放器在控制台会报 CORS 错误视频无法加载。这也是很多自建视频点播系统最常见的报错后端接口用浏览器直接访问没问题但放进 hls.js 播放就失败。排查时优先看浏览器 Network 面板中 m3u8 请求是否被 CORS 拦截。7. 运行结果与效果验证整个服务启动后先用命令行验证接口是否正常。启动 Spring Boot 应用mvn spring-boot:run然后请求 m3u8 文件curl -I http://localhost:8080/api/video/lesson-01/lesson-01.m3u8预期响应头类似HTTP/1.1 200 Content-Type: application/vnd.apple.mpegurl Cache-Control: max-age600如果 Content-Type 是text/plain或application/octet-stream播放器容易解析失败需要检查后端 MIME 配置。再请求一个 ts 片段curl -O http://localhost:8080/api/video/lesson-01/segment-00000.ts file segment-00000.ts正常会输出类似segment-00000.ts: MPEG transport stream data如果file识别不了大概率是下载的文件是 HTML 错误页面比如 404 页面说明接口路径或文件名有误。最后打开前端页面确认播放器和控制台都没有报错。如果视频能播放但拖动进度条后卡住先查看浏览器 Network 面板是否请求了对应时间段的 ts 文件如果没有请求可能是播放器插件版本问题如果请求了但响应很慢就是带宽或服务器性能问题。8. 常见问题与排查方法视频播放链路涉及前端、后端、文件格式、HTTP 响应头等多个环节很多人遇到问题不知道从哪里入手。下面整理了一份常见问题排查表。问题现象可能原因排查方式解决方案打开页面后视频一直黑屏m3u8 文件无法加载或解析失败查看浏览器 Network 面板的 m3u8 请求状态和响应内容确认接口能直接访问确认响应 Content-Type 是 application/vnd.apple.mpegurl控制台报 CORS 错误后端没有配置允许前端源站的跨域请求查看 m3u8 响应头是否有 Access-Control-Allow-Origin在 Spring Boot 中配置全局 CORS或使用 Nginx 添加响应头视频只能播前面几秒m3u8 索引中被截断了查看 m3u8 文件末尾是否有 #EXT-X-ENDLIST转码时加-hls_list_size 0和-hls_playlist_type vod重新生成手机端无法播放视频编码不是 H.264或 m3u8 内容格式不标准用 FFmpeg 查看转码后视频编码信息统一使用libx264转码拖动进度条很慢或转圈切片文件太多但 CDN 缓存不足或接口没有 Range 支持查看拖动时浏览器请求了哪些 ts 文件响应耗时如何降低 hls_time启用 CDN 缓存后端为静态资源加上范围请求支持接口可以被任意播放器访问存在盗链风险没有做鉴权和防盗链查看访问日志中陌生 IP 和 Referer增加签名 URL、时间戳校验、Referer 防盗链ffmpeg 报unknown encoder libx264FFmpeg 编译时没有包含 libx264执行ffmpeg -encoders查看可用编码器安装完整版 FFmpeg或替换为系统支持的 H.264 编码器这里要特别强调一个问题如果 Nginx 配置了静态文件代理同时又经过 Spring Boot 接口转发容易出现 Content-Type 被覆盖的情况。排查时不要只看后端代码还要看整个请求链路中经过的 Nginx 或网关配置。9. 生产环境最佳实践与工程建议自测跑通只是第一步生产环境还会有更多棘手问题。以下建议来自常见视频点播系统的工程实践可以在设计阶段提前规避风险。9.1 转码任务异步化不要把 FFmpeg 转码放在 Web 请求线程里同步执行。一个 1GB 的视频转码可能耗时几分钟到几十分钟同步处理会让接口直接超时。更稳妥的做法是用户上传原始视频上传成功后写入转码任务队列独立的转码服务消费队列调用 FFmpeg转码完成后更新视频状态并通过回调通知业务系统。消息队列可以选择 RabbitMQ、RocketMQ、Kafka也可以先使用数据库任务表加定时扫描的方式关键在于把耗时操作和请求处理解耦。9.2 鉴权与防盗链本文示例中直接访问接口生产环境不能这么干。至少要做一层访问控制生成带过期时间的签名 URL比如在 URL 上拼接expire和sign使用 Nginx 的secure_link模块做防盗链对重要内容进行转码时加入水印或业务标识对播放请求做频率控制防止下载工具批量抓取切片。签名校验放在 Nginx 层更高效因为 ts 文件请求量远大于 m3u8 请求量全部打到应用服务会浪费资源。9.3 静态资源分层m3u8 和 ts 文件属于静态资源不建议长时间由 Spring Boot 直接传输大文件。生产环境典型结构是统一对象存储或云存储存放转码产物CDN 分发 ts 切片后端接口只负责生成播放凭证或动态 m3u8ts 文件通过 CDN 鉴权 URL 访问。如果短时间内无法引入 CDN至少要在 Nginx 层配置缓存和 gzip。注意 ts 文件不能开 gzip因为它是已压缩的媒体数据强行 gzip 会浪费 CPU。9.4 转码参数模板化不同业务场景对视频清晰度要求不同。建议把转码参数做成配置模板而不是把一堆参数写在 shell 脚本里。例如video: profiles: sd: width: 854 height: 480 video-bitrate: 800k audio-bitrate: 96k hd: width: 1920 height: 1080 video-bitrate: 4000k audio-bitrate: 128k后续转码服务只需要根据模板动态拼 FFmpeg 命令减少人为手写命令造成的差异。9.5 监控指标视频点播系统至少需要关注以下指标m3u8 请求成功率ts 文件 404 比例平均首帧时间卡顿率转码队列积压长度源站和 CDN 带宽。不需要一开始就搭一套完整监控先把关键接口日志打出来配合文件清理策略基本可以满足早期需求。9.6 路径安全与文件清理视频资源会持续增长必须有清理策略。删除文件前要做两个确认确认视频已无关联业务引用确认删除操作前有备份。建议在管理后台设计“下线”状态而不是直接物理删除文件。出于安全考虑任何涉及删除和批量处理的操作都建议先在生产环境外验证脚本。10. 总结与后续学习方向从“视频链接来了”到“视频能稳定播放”中间隔着一整套视频处理工程。这篇文章解决了其中最基础的链路FFmpeg 转码切片、Spring Boot 提供资源访问、hls.js 前端播放以及常见问题的排查思路。如果接下来想深入可以从几个方向继续研究 DASH 协议与 HLS 的差异了解更多自适应码率方案阅读 hls.js 源码掌握代理段加载和错误恢复机制深入学习 FFmpeg 的滤镜语法用于视频水印、裁剪、拼接设计一套覆盖上传、转码、审核、发布的完整视频业务流程思考如何基于时分片分布、缓存预热、预加载策略优化播放体验。在实际项目中视频链路调试往往比业务代码更耗时间。建议先把最小案例跑通再逐步叠加鉴权、转码队列、存储分层和监控。如果这篇文章能帮你少走几个弯路就值得收藏备用。
返回列表