尧图精选

Java Web视频在线播放方案盘点:从Servlet到HLS

🕒 发布时间:2026/10/2 9:04:02 📁 来源:尧图网络
前阵子给一个内部培训平台做视频课程功能需求就一句话用户在网页里点开视频能直接播放。听起来简单真动起手来涉及的点还挺多——Java后端怎么把视频文件吐给浏览器、为什么有的视频能拖进度条有的不能、文件放在Tomcat还是Nginx上、移动端要不要换协议。这篇文章把我实际试过的几种 Java Web 视频在线播放方案都盘一遍从最原始的 Servlet 流式输出到支持 Range 的断点续传再到 Spring Boot 静态映射和 Nginx 分流最后聊一下 HLS 切片配合踩坑经历和排查思路适合正在做视频上传播放功能的 Java 后端同学也适合期末在做 Web 项目、想搞清楚原理的同学。先给一个基本认知Java Web 本身不负责“播”视频负责的是把视频文件用 HTTP 的方式喂给浏览器剩下解码、渲染、控制条交互全是浏览器自己的事。你只要把“水管”接对视频自然就流出来了。这句话看着简单但很多人一开始就走偏了所以咱们先把本质聊透。1. 视频在线播放的本质与常见误区1.1 面试者最常见的理解误区我面试候选人、带新人的时候发现大家对“视频在线播放”这件事普遍有三类误解。第一类以为要上 WebSocket 推流。一听到“在线播放”就觉得要建立长连接、实时推送视频帧。其实普通点播场景根本用不到 WebSocket浏览器原生的video标签就是靠 HTTP 普通请求把视频拉下来的。WebSocket 主要解决的是双向实时通信比如直播弹幕、白板互动视频数据本身走 WebSocket 是很罕见的。第二类以为要引入 JavaCV、FFmpeg 的 Java 封装去做视频流处理。很多人的想法是“我要在 Java 里解析视频帧、转换格式、再推给前端”这个方向只适合做直播推流、视频剪辑、转码服务这类重业务。对于一个“把已有视频文件播放出来”的需求你在 Java 层把文件读出来、按 HTTP 协议返回就够了不需要任何音视频编解码库。第三类以为视频是一次性全量下载。有人测试时发现浏览器 Network 面板里的视频请求一直没结束就觉得是卡住了。实际上浏览器通常只会拉取“当前播放进度附近”的数据块后面的部分等你拖过去或者播放到那儿再继续拉。这就引出了整个技术方案的核心分水岭——是否支持 HTTP Range。先把这三类误区纠正掉后面所有方案你都看得懂了。1.2 浏览器的一次完整播放请求长什么样一个最简单的前端调用是这样的video src/videos/demo.mp4 controls/video浏览器解析到这个标签后会向服务器发一个GET /videos/demo.mp4请求。这个请求大概率带着这样的头Range: bytes0-意思是“我要从第 0 个字节开始你能给多少给多少”。服务器如果懂 Range就返回206 Partial Content并带上本次返回的字节区间和文件总大小如果不懂就直接返回200 OK和全部文件内容。浏览器拿到响应后先解析 MP4 的元数据拿到时长、分辨率、视频轨、音频轨信息然后开始解码、渲染边下载边播放。用户拖动进度条到第 5 分钟浏览器又会发一个新的 Range 请求从第 5 分钟对应的字节位置开始拉数据。整个流程不需要任何后端推流就是一个普通的 HTTP 文件下载只不过浏览器把它变成了“边下边播”的效果。理解了这个流程再看各种实现方案思路就特别清晰了。1.3 “能播”和“能拖”是两个境界判断一个 Java Web 视频播放方案是否合格第一步就是看它支不支持 Range。“能播”很简单服务器把文件内容从头到尾吐出来浏览器就能从头播到尾。但用户一旦想拖进度条浏览器会发一个Range: bytes某个位置-的请求。服务器如果无视 Range、又把整个文件从头输出一遍浏览器通常会重新开始加载表现就是你一拖进度条视频就从 0 开始转圈有的浏览器在文件特别大的时候甚至会直接放弃播放。所以做视频在线播放第一优先级的优化不是换框架、不是加缓存而是把 HTTP Range 支持好。Range 是 HTTP 1.1 协议里现成的机制不涉及任何额外依赖纯 Java 代码就能实现。下面从最原始的方案开始一层层把这条路走通。2. 方法一Servlet流式输出——最基础但值得先跑通的方案2.1 Servlet裸写从磁盘读文件写到响应里如果只是想在本地快速验证“Java 能不能把视频喂给浏览器”一个 Servlet 就够了。下面这段代码我经常拿来演示最原始的思路WebServlet(/video-simple) public class VideoSimpleServlet extends HttpServlet { private static final String VIDEO_PATH /data/videos/demo.mp4; Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException { File video new File(VIDEO_PATH); response.setContentType(video/mp4); response.setHeader(Content-Length, String.valueOf(video.length())); try (InputStream in new FileInputStream(video); OutputStream out response.getOutputStream()) { byte[] buffer new byte[8192]; int len; while ((len in.read(buffer)) ! -1) { out.write(buffer, 0, len); } out.flush(); } } }这段代码的逻辑非常简单把文件路径指过去设置Content-Type为video/mp4把文件长度塞进Content-Length然后开一个 8KB 缓冲区从文件流往响应输出流里搬数据。浏览器访问http://localhost:8080/项目名/video-simple视频确实能播。Content-Type不能漏。如果漏了Tomcat 默认可能返回text/html或application/octet-stream浏览器不知道这是个视频文件很多情况下会直接在页面里显示一堆乱码或者弹出下载框。2.2 问题来了这段代码的瓶颈在哪儿这段代码最大的问题就是开头提到的不支持 Range。Chrome 请求这个 URL 时会带Range: bytes0-但这段代码没有解析 Range 头直接把整个文件从头到尾输出状态码是 200 而不是 206。结果是视频能从头播放但进度条基本是废的用户一拖动就会从头加载。第二个问题是效率。FileInputStream配合 8KB 缓冲区每写一次缓冲区就发生一次用户态到内核态的复制在大文件场景下 CPU 占用会比较高。8KB 太小了实际项目中我一般调到 64KB 起步能明显减少循环次数和系统调用。第三个问题是线程占用。视频文件动辄几百 MBServlet 线程会一直阻塞在while循环里直到文件读完。Tomcat 默认线程池就两百个左右多来几个用户同时看视频线程池直接被打满其他请求全部排队。这也是为什么生产环境里视频文件不能长期挂在 Tomcat 上的核心原因。那你可能会问这方案是不是一无是处也不是。对于几十 MB 以内的小视频、内网工具、教学 Demo它完全够用而且代码简单出了问题好排查。但从这个基础上加上 Range 支持才是真正能用的方案。3. 方法二HTTP Range断点续传——让进度条真正能动起来3.1 Range是HTTP协议里现成的机制不用白不用Range 的整个交互流程并不复杂拆开就三层。客户端发请求时带上Range头常见的格式有这么几种Range: bytes0-1023 # 从第0字节到第1023字节共1024字节 Range: bytes1024- # 从第1024字节到文件末尾 Range: bytes-500 # 倒数500字节服务端处理时如果请求的区间合法返回206 Partial Content并在响应头里带上本次实际返回的字节区间和文件总大小HTTP/1.1 206 Partial Content Accept-Ranges: bytes Content-Range: bytes 0-1023/1234567 Content-Length: 1024 Content-Type: video/mp4Accept-Ranges: bytes是告诉浏览器“我支持按字节取范围”Content-Range: bytes 0-1023/1234567前半段是本次返回的区间斜杠后面是文件总字节数。如果请求的区间超出了文件长度就返回416 Range Not Satisfiable并在Content-Range里带上bytes */文件总长度。浏览器拿到这些响应头就能准确知道文件多大、当前拉到了哪个位置。用户拖进度条时浏览器从新的位置发 Range 请求服务器从对应偏移量返回数据整个播放过程就像流水一样顺滑。3.2 Java实现Range的完整代码与边界处理下面的代码是我在项目里用过的简化版支持单段 Range也处理了常见的边界情况WebServlet(/video-range) public class VideoRangeServlet extends HttpServlet { private static final String VIDEO_PATH /data/videos/demo.mp4; private static final int BUFFER_SIZE 64 * 1024; Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException { File video new File(VIDEO_PATH); long fileLength video.length(); String rangeHeader request.getHeader(Range); response.setContentType(video/mp4); response.setHeader(Accept-Ranges, bytes); if (rangeHeader null || !rangeHeader.startsWith(bytes)) { // 没有 Range 就返回完整文件 response.setStatus(HttpServletResponse.SC_OK); response.setHeader(Content-Length, String.valueOf(fileLength)); writeRange(video, response.getOutputStream(), 0, fileLength - 1); return; } // 解析 Range: bytesstart-end String rangeValue rangeHeader.substring(bytes.length()); String[] parts rangeValue.split(-); long start; long end; try { if (parts[0].isEmpty()) { // 格式: bytes-500表示最后500字节 long suffixLength Long.parseLong(parts[1]); start Math.max(0, fileLength - suffixLength); end fileLength - 1; } else { start Long.parseLong(parts[0]); end parts.length 1 !parts[1].isEmpty() ? Long.parseLong(parts[1]) : fileLength - 1; } } catch (NumberFormatException e) { response.setStatus(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE); response.setHeader(Content-Range, bytes */ fileLength); return; } if (start end || start fileLength) { // 请求的区间不合法返回416 response.setStatus(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE); response.setHeader(Content-Range, bytes */ fileLength); return; } // 末端不能超过文件大小 end Math.min(end, fileLength - 1); response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); response.setHeader(Content-Range, bytes start - end / fileLength); response.setHeader(Content-Length, String.valueOf(end - start 1)); writeRange(video, response.getOutputStream(), start, end); } private void writeRange(File file, OutputStream out, long start, long end) throws IOException { try (InputStream in new FileInputStream(file)) { long skipBytes start; long actuallySkipped; while (skipBytes 0) { actuallySkipped in.skip(skipBytes); if (actuallySkipped 0) { break; } skipBytes - actuallySkipped; } long remaining end - start 1; byte[] buffer new byte[BUFFER_SIZE]; int len; long totalRead 0; while (totalRead remaining (len in.read(buffer, 0, (int) Math.min(buffer.length, remaining - totalRead))) ! -1) { out.write(buffer, 0, len); totalRead len; } out.flush(); } } }这里有个细节容易出事InputStream.skip()不保证一次就跳到位所以必须放在循环里反复跳。还有读取的时候最后一次读取的长度不能超过剩余字节数否则会把区间之外的多余字节一起读出来导致Content-Length和实际返回的字节数对不上浏览器会报错。这段代码里的start和end都做了边界限制start不能大于等于文件长度end不能超过文件长度减一。这些边界看着啰嗦实际测试时最容易出问题的就是它们。3.3 用curl验证服务器到底支不支持拖动写完之后先用 curl 手动验证再上页面调定位问题会快得多。curl -I -H Range: bytes0-99 http://localhost:8080/your-app/video-range注意-I是 HEAD 请求如果你的 Servlet 只处理了 doGetTomcat 默认会把 HEAD 也路由到 doGet 并且丢弃输出体一般不会影响逻辑验证。如果你不想用 HEAD把-I去掉加--max-time 5 -o /dev/null防止大文件下载拖太久curl -H Range: bytes0-99 http://localhost:8080/your-app/video-range -o /dev/null -s -D -重点看响应头HTTP/1.1 206 Accept-Ranges: bytes Content-Range: bytes 0-99/你的文件总长度 Content-Length: 100只要状态码是206、Content-Range里的斜杠后面是文件真实大小这个接口就算过关了。我习惯同时测三个用例bytes0-99看正常区间、bytes999999999-看超界返回 416、不带 Range 看返回 200。三种情况都符合预期再交给前端联调。4. 方法三Spring Boot静态映射加Nginx生产环境最常见的组合4.1 Spring Boot三行配置搞定静态视频目录在实际业务里大部分项目的后端都是 Spring Boot手动写 Servlet 实现 Range 的机会其实不多。Spring 框架内置的静态资源处理器已经支持 Range你只需要把视频目录映射成 URL 就行。Configuration public class WebConfig implements WebMvcConfigurer { Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/videos/**) .addResourceLocations(file:/data/videos/); } }这段配置把磁盘上的/data/videos/目录通过/videos/路径暴露出去。浏览器请求/videos/demo.mp4Spring 会自动处理文件读取、Content-Type、Range、Last-Modified、Cache-Control 这些细节。有个小坑addResourceLocations传的路径必须以/结尾否则 Spring 拼路径时会出问题。写成file:/data/videos是启动不报错但文件 404 的经典原因。Spring Boot 的这个静态资源处理器用的是ResourceHttpRequestHandler对 Range 的原生支持很完善。我用 curl 测过bytes0-99能正常返回 206所以不用担心拖进度条的问题。中小型项目、内网系统直接用这个配置是最省事的。4.2 生产环境为什么还是建议加NginxSpring Boot 静态映射用起来是真方便但它仍然把文件读取和网络传输压在了 Tomcat 线程上。视频是典型的“高带宽、低计算”资源Tomcat 处理这类请求性价比很低一个 200MB 的视频文件会让一个业务线程忙上半天。生产环境我习惯在前面加一层 Nginx。它的sendfile机制直接把磁盘文件从内核空间发送到网卡几乎不消耗用户态 CPU视频这种大文件的静态传输效率远高于 Tomcat。配置也很简单server { listen 80; server_name video.example.com; location /videos/ { alias /data/videos/; add_header Accept-Ranges bytes; limit_rate 5m; } }alias把你的磁盘目录和 URL 路径对应起来limit_rate是限速配置防止个别用户把带宽占满。Nginx 本身支持 Range所以浏览器拖动进度条没有问题。你可能会问那 Java 在这个架构里还能干什么答案是干它最擅长的事——业务和鉴权。用户登录、权限校验、视频列表、上传接口都放在 Java 应用里视频文件本身不经过 Java直接由 Nginx 发给浏览器。这个分层各司其职是我的标准做法。4.3 静态资源的缓存与防盗链设置视频文件一般不会频繁变动完全可以配置浏览器缓存让用户二次访问时从本地读取省带宽也省响应时间location /videos/ { alias /data/videos/; expires 7d; add_header Cache-Control public; }防盗链是视频站点的刚需。最简单的方案是校验 Referer只有指定域名的页面才能引用视频其他人拿到链接也播不了location /videos/ { alias /data/videos/; valid_referers none blocked server_names *.example.com; if ($invalid_referer) { return 403; } }这里有个经验none和blocked要保留否则浏览器直接输入地址打开视频没有 Referer会被误杀。防盗链只解决“别人把链接贴走”的问题防不了有技术能力的用户但对大多数场景已经够用了。4.4 X-Accel-Redirect鉴权后的内部转发玩法如果视频需要逐个用户授权比如付费课程只允许买了课的人看Nginx 的静态暴露方案就不够用了。这时候可以用 Nginx 的X-Accel-Redirect内部跳转。Java 侧先做权限校验通过后不直接输出视频流而是甩一个特殊响应头给 NginxGetMapping(/play) public void play(HttpServletRequest request, HttpServletResponse response) { // 1. 做登录校验、课程权限校验、播放次数限制等业务逻辑 // 2. 校验通过后告诉 Nginx 去发送哪个内部文件 response.setHeader(X-Accel-Redirect, /internal_videos/lesson01.mp4); response.setHeader(Content-Type, video/mp4); }Nginx 里配置一个外部访问不到的内部路径location /internal_videos/ { internal; alias /data/videos/; }internal表示这个路径只能被 Nginx 内部跳转访问客户端直接请求/internal_videos/lesson01.mp4会返回 404。Java 的X-Accel-Redirect头一到 NginxNginx 就替你把文件发出去Java 线程立刻释放既做了鉴权又保住了静态文件的处理效率。这个方案在付费视频、网盘预览场景里非常实用是我强烈推荐的生产级组合。5. 方法四HLS切片流媒体方案——移动端与长视频的解法5.1 为什么裸MP4不是万能的裸 MP4 搭配 Range 支持覆盖 90% 的视频点播场景没问题但有两个软肋。第一个是弱网表现。MP4 是单个大文件网络一旦抖动浏览器已有的缓冲播完就得重新拉数据虽然 Range 可以断点续但整体体验不如流媒体协议那样的“分片可控”。第二个是移动端兼容问题。iOS Safari 对 m3u8 格式的 HLS 流是原生支持的但对 MP4 的某些编码支持有限制桌面 Chrome 反过来原生不支持 m3u8需要靠 hls.js 播放。如果你要同时覆盖 iPhone、Android、桌面端HLS 反而是一条更容易统一的路线。HLS 的全称是 HTTP Live Streaming原理就是“把一个大视频/直播流切成无数个小文件”。每个切片通常 6 到 10 秒一个m3u8的播放列表文件记录这些切片的顺序和地址。浏览器播放时先下载m3u8再去按列表逐个加载.ts切片文件。因为这个传输完全基于普通 HTTP 文件请求Nginx、Tomcat、Spring Boot 都能直接输出静态文件不需要专门的流媒体服务器。5.2 用ffmpeg把视频切成HLS命令与参数说明做 HLS 之前先把视频转成切片。ffmpeg 是这时候的主力工具我没见过哪个 Java 项目直接用手写代码去切片的。ffmpeg -i input.mp4 -c copy -start_number 0 -hls_time 10 -hls_list_size 0 -f hls output.m3u8参数拆开说-c copy不重新编码直接复制源视频和音频轨道速度最快CPU 占用最低。但前提是源视频编码是 H.264 AAC这是浏览器和设备兼容性最好的组合。-hls_time 10每个切片 10 秒。-hls_list_size 0生成的m3u8文件保留所有切片记录。如果不设这个参数ffmpeg 默认只保留最近 5 个切片适合直播、不适合点播。-start_number 0切片从 0 编号方便点播按顺序拼接。如果源视频不是 H.264/AAC就得换成转码命令ffmpeg -i input.mp4 -c:v libx264 -profile:v main -c:a aac -b:a 128k -hls_time 10 -hls_list_size 0 -f hls output.m3u8转码非常吃 CPU一个 1 小时的视频可能要跑很久。生产环境里一般不会让用户在请求播放时同步转码而是上传视频后走异步队列转完再把 HLS 文件放到静态目录。Java 侧用ProcessBuilder调 ffmpeg 是可以的但真正的系统会专门跑一个转码任务队列这个属于后话。5.3 Java后端与前端hls.js配合播放转完之后目录里大概是这样/output.m3u8 /output0.ts /output1.ts /output2.ts ...Java 后端什么都不用做把 m3u8 和 ts 文件放到静态目录Nginx 或 Spring Boot 的静态资源映射直接暴露出来就行。别忘了给 ts 文件配 MIME 类型Nginx 的话在mime.types里确认有video/mp2t没有就手动加location /hls/ { alias /data/hls/; types { application/vnd.apple.mpegurl m3u8; video/mp2t ts; } }前端在桌面 Chrome 里播放 m3u8需要用 hls.jsvideo idplayer controls/video script srchttps://cdn.jsdelivr.net/npm/hls.jslatest/script script const video document.getElementById(player); if (Hls.isSupported()) { const hls new Hls(); hls.loadSource(/hls/output.m3u8); hls.attachMedia(video); hls.on(Hls.Events.MANIFEST_PARSED, function () { video.play(); }); } else if (video.canPlayType(application/vnd.apple.mpegurl)) { // iOS Safari 原生支持 HLS直接赋 m3u8 地址 video.src /hls/output.m3u8; } /scriptMANIFEST_PARSED事件触发说明 m3u8 已经加载并解析完成这时候再调play()成功率最高。iOS Safari 走原生分支其他浏览器走 hls.js这是现在比较稳妥的兼容策略。5.4 HLS加密防盗链比Referer更硬一点的方案HLS 方案里还能做一层加密防盗链。思路是给 ts 切片加 AES-128 加密播放器必须先拿到密钥才能解密播放。m3u8 播放列表里会多一行EXT-X-KEY#EXTM3U #EXT-X-VERSION:3 #EXT-X-KEY:METHODAES-128,URI/keys/enc.key #EXTINF:10.000000, output0.tsffmpeg 有一个-hls_key_info_file参数可以指定密钥信息文件来生成加密切片。密钥文件可以由 Java 接口动态下发没权限的用户拿不到密钥ts 文件就算被下载也放不出来。不过说实话纯静态加密解决不了录屏也解决不了黑客技术性扒流。真要商业级防护得靠 DRM、令牌化播放器和动态水印整套方案那是另一个量级的话题。普通业务做到加密 鉴权 签名 URL 已经比大多数平台扎实了。6. 那些年踩过的坑症状、原因与排查链路方案聊完了把这几年在视频播放上踩过的坑集中整理一下。每一个我都给出症状、原因和排查方法照着走能省很多时间。6.1 症状1视频黑屏或直接变成下载浏览器打开视频地址页面黑屏或者直接弹下载框。打开 Network 面板看这条请求的Content-Type如果显示application/octet-stream而不是video/mp4就是 MIME 类型没配对。原因很直接浏览器靠 Content-Type 判断这是什么资源。application/octet-stream表示“二进制文件类型未知”浏览器只能当成下载处理。用 Nginx 时确认mime.types里有video/mp4用 Servlet 时确认response.setContentType(video/mp4)在getOutputStream()之前调用用 Spring Boot 静态资源时确认文件后缀是.mp4不要用什么.mp4?v123这种带参数路径去做资源映射——带参数时 Spring 可能无法正确识别扩展名。6.2 症状2进度条拖不动一拖就从头开始播这是最经典的“没实现 Range”症状。用户看视频前面两分钟没问题一拖进度条视频立刻回到 0 重新转圈。排查链路就三步打开 Network 面板点一下视频请求看状态码是不是 206。用 curl 手动模拟 Range 请求确认服务器返回。如果 curl 返回 200 而不是 206回到代码里找原因。如果你用 Spring Boot 静态资源还出现这个问题大概率是自定义了拦截器或者过滤器把请求头里的 Range 字段干掉了或者自己用 OutputStream 写死了整个响应。自定义响应时一定不要手动 set 掉 Spring 已经设置好的 Content-Type 和 Content-Length。6.3 症状3大视频OOM一堆几百 MB 的视频同时有人播放Tomcat 频繁抛OutOfMemoryError。看堆栈基本都指向同一个方法——一次性把整个文件读进字节数组。错误代码长这样byte[] data Files.readAllBytes(video.toPath()); response.getOutputStream().write(data);500MB 的视频这么搞立刻多出 500MB 堆内存并发一上来直接撑爆。视频输出必须用缓冲流分块读取上面 Servlet 例子里byte[] buffer new byte[64 * 1024]那种循环方式就是标准解法。千万不要图省事一次性读入内存这个坑我见过太多次了。6.4 症状4视频开头转圈很久播放器一片黑视频文件不大网络也正常但用户就是半天看不到画面。这时候可以检查 MP4 文件的元数据位置。MP4 的moov原子元数据盒记录了时长、轨道信息、编码参数。如果moov在文件末尾浏览器必须先下载到文件尾部才能拿到元数据对于一个几百 MB 的文件这就是灾难。用文本编辑器或者ffprobe查看文件结构moov应该在文件头部才是对的。解决办法ffmpeg 转码或重新封装时加上-movflags faststartffmpeg -i input.mp4 -c copy -movflags faststart output.mp4-c copy不做转码只是把文件重新封装一下把moov挪到文件开头速度很快。这个习惯我每次处理 MP4 都会带上。6.5 症状5前端页面和视频域名不同取不到视频帧或hls.js报错前端页面在www.example.com视频在video.example.com。普通video播放一般还能放但你如果用 Canvas 抓取视频帧或者用 hls.js 拉 m3u8 和 ts跨域请求就会被拦截。解决方法是给视频域名加 CORS 头location /videos/ { alias /data/videos/; add_header Access-Control-Allow-Origin https://www.example.com; add_header Access-Control-Allow-Methods GET, HEAD, OPTIONS; add_header Access-Control-Allow-Headers Range; }这里有个特别值得注意的点Access-Control-Allow-Headers必须包含Range。hls.js 加载分片时如果发了自定义头或者 Range 头预检请求会把它们列在Access-Control-Request-Headers里服务端不放行就直接挂。我见过有人排查了一整天最后发现只是漏了这个响应头。7. 方案怎么选对比与我的实操经验7.1 五种方案横向对比把文章里提过的方案放在一起看方案实现难度Range支持大文件性能移动端兼容适合场景Servlet流式输出低不支持差一般教学Demo、小视频、内网工具Servlet Range中支持中好自研播放接口、学习协议原理Spring Boot静态映射极低支持中好中小型系统、内部平台、期末项目Nginx静态托管低支持极好好生产环境常规视频站点HLS切片高天然分片极好最好移动端为主、长视频、直播、弱网“实现难度”这里给的是从零开始做的成本不是引入难度。Nginx 方案实现难度低但在 Java 项目里多了一层部署依赖。HLS 方案写代码不难难在转码链路和切片文件管理。7.2 按场景选型的思路以及一个兜底建议我自己的选型逻辑非常简单粗暴如果是期末项目、内网系统、临时 Demo直接用 Spring Boot 静态资源映射三行配置搞定Range 还是自带的不用写任何业务代码。如果是正式上线的中小型项目用 Nginx 托管视频文件Java 只做鉴权和业务接口。视频目录和静态资源目录分开别把视频塞进 Jar 包或者 classpath那会让部署包爆炸也会拖慢启动。如果视频面向移动端用户、或者有长视频和弱网场景提前把视频转成 HLS静态目录直接输出 m3u8 和 ts前端按设备兼容性选择原生播放或 hls.js。如果视频量大、用户分布广直接接云厂商的 OSS CDN。Java 后端只负责生成一个带签名和过期时间的 URL视频文件本身完全不经业务服务器。这个方案成本和接入复杂度都更高但省心带宽和存储容灾都不用自己操心。做一个稍微反直觉的兜底建议不要过早纠结选型。视频在线播放的核心是先跑通“视频能播”然后补上 Range 支持接着把视频从 Tomcat 挪到 Nginx如果还有移动端强需求再上 HLS。绝大多数项目死在前两步而不是死在协议选择上。7.3 最后分享两个实用小技巧第一个是压箱底的测试习惯。每次改完播放接口先用 curl 把 Range 的几个边界用例跑一遍再进浏览器# 正常区间请求应该返回 206 curl -H Range: bytes0-99 http://你的地址/video-range -o /dev/null -s -D - # 超界请求应该返回 416 curl -H Range: bytes999999999999- http://你的地址/video-range -o /dev/null -s -D -第二个是关于视频链接的缓存。视频文件更新后用户浏览器可能还在用本地缓存的老版本。最简单有效的办法是在视频 URL 后面加版本参数比如/videos/demo.mp4?v20250101业务端把版本号和视频内容绑定升级视频时换一个新参数。后端用 Nginx 配置expires 7d配合版本参数既享受缓存又不怕更新失效。做视频在线播放原理没有多复杂真正的难点都藏在细节里。把 HTTP Range 吃透把静态资源分发这一层理顺把 MIME 和 moov 这类冷门知识记牢再大的视频也不会难倒你。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →