ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Java自建流媒体直播服务器:RTMP推流与HTTP-FLV/HLS分发实战

Java自建流媒体直播服务器:RTMP推流与HTTP-FLV/HLS分发实战 简介这份资源面向具备一定Java基础、希望进入音视频与直播服务端开发领域的开发者聚焦流媒体视频直播服务器的设计与实现覆盖音视频处理、服务器架构、Java编程与运维等多个技术方向。压缩包共31个文件约278KB以18个java源码文件为核心辅以png、gif界面素材、ucls类图、config配置文件及gitignore等结构紧凑便于直接阅读与二次开发。资源围绕RTMP、HLS、DASH等直播协议展开涉及Java NIO高并发连接处理、H.264与AAC编解码、转码适配以及负载均衡、监控日志等运维思路并配有VideoManager、server.config、client.config等模块可帮助读者理解推流、处理与分发链路。目前已有1944人学习下载适合作为课程设计、毕业设计或直播服务端入门实践的参考方案从中可获取完整的项目结构、协议选型思路与并发处理经验为后续深入音视频开发打下基础。1. 从零搭一套 Java 流媒体直播服务器为什么我劝你先别急着上 Netty很多人第一次听到「基于 Java 的流媒体视频直播服务器」脑子里蹦出来的画面是一台服务器几行代码摄像头画面就哗啦啦推给几千个观众。真动手才发现卡顿、花屏、延迟三秒起步、并发一上来就崩全是玄学。这个标题讲的不是「写个 Socket 转发字节」而是把采集端推上来的音视频流经过协议解析、封装、分发稳定地送到播放器手里。它解决的是「自建可控直播链路」的问题适合做在线教育、安防监控、企业内训、物联网设备回传这类需要自己掌握数据、不想被第三方云服务绑死的场景。Java 在这里不是最优解但胜在生态成熟、招人好招、和现有业务系统用户、鉴权、订单能塞进同一个 JVM 里对中小团队来说落地成本最低。下面我按自己踩过的路把选型、协议、代码、参数和坑一条条讲清楚。2. 协议选型与整体架构RTMP、HLS、HTTP-FLV 到底怎么选2.1 三种主流协议的真实差别做直播服务器第一件事不是写代码是决定「推流用什么、拉流用什么」。推流端OBS、FFmpeg、摄像头 SDK几乎清一色支持 RTMP所以推流入口基本锁定 RTMP。真正纠结的是拉流分发因为观众端决定了你的技术栈。协议传输层典型延迟播放端支持适用场景RTMPTCP1~3 秒需 Flash 或专用播放器推流入口、低延迟内部播放HTTP-FLVTCP(HTTP)1~3 秒flv.js 等浏览器可播网页低延迟直播首选HLSTCP(HTTP)6~30 秒原生支持兼容性最好点播、大并发、移动端WebRTCUDP1 秒现代浏览器连麦、超低延迟互动结论很直接推流用 RTMP网页低延迟拉流用 HTTP-FLV兼容性和大并发兜底用 HLS。一套服务器同时输出这三种是行业里最常见的做法。别一上来就 WebRTC它的信令、NAT 穿透、SFU 架构复杂度会把你拖垮除非你的核心诉求就是「连麦互动」。2.2 整体架构分层一个能扛住生产流量的 Java 直播服务器通常分四层接入层监听 1935 端口收 RTMP 推流做握手、鉴权、流注册。处理层解析 RTMP Chunk拆出 FLV Tag音视频帧必要时转码分辨率、码率适配。分发层把帧缓存成 GOP向所有订阅该流的播放端广播同时切片生成 HLS 的 ts 和 m3u8。管理层流列表、在线人数、录制、鉴权回调、断流清理。Java 里接入层和分发层是性能关键处理层如果要做转码一般会外挂 FFmpeg 进程而不是纯 Java 硬扛——纯 Java 软编解码 CPU 占用高得离谱这是血泪经验。2.3 最小可跑的依赖与工程骨架用 Maven 起工程核心依赖就两个Netty 做网络一个 FLV/RTMP 解析库。如果不想引第三方 RTMP 库可以自己按协议解析但工作量大。常见做法是基于 Netty 手写 RTMP 握手和 Chunk 解析可控性最强。dependencies !-- Netty处理 RTMP 的 TCP 长连接和字节流 -- dependency groupIdio.netty/groupId artifactIdnetty-all/artifactId version4.1.100.Final/version /dependency !-- 日志排查握手和 Chunk 问题全靠它 -- dependency groupIdorg.slf4j/groupId artifactIdslf4j-simple/artifactId version2.0.9/version /dependency /dependencies参数说明Netty 版本选 4.1.x 稳定线即可别追 5.x已废弃。netty-all方便但包大生产可以只引netty-handler、netty-codec。日志一定要开 DEBUG 级别看握手字节否则连接建不起来你连错在哪都不知道。2.4 启动一个 RTMP 监听端口public class RtmpServer { public static void main(String[] args) throws InterruptedException { EventLoopGroup boss new NioEventLoopGroup(1); // 接收连接 EventLoopGroup worker new NioEventLoopGroup(); // 处理 IO try { ServerBootstrap b new ServerBootstrap(); b.group(boss, worker) .channel(NioServerSocketChannel.class) .childHandler(new ChannelInitializerSocketChannel() { Override protected void initChannel(SocketChannel ch) { // 握手 Chunk 解析 业务处理按顺序加 ch.pipeline().addLast(new RtmpHandshakeHandler()); ch.pipeline().addLast(new RtmpChunkDecoder()); ch.pipeline().addLast(new RtmpBusinessHandler()); } }); // 1935 是 RTMP 默认端口OBS 推流就填这个 b.bind(1935).sync().channel().closeFuture().sync(); } finally { boss.shutdownGracefully(); worker.shutdownGracefully(); } } }逻辑说明boss线程组只负责 acceptworker负责读写这是 Netty 标准模型。RtmpHandshakeHandler处理 C0/C1/C2 握手RtmpChunkDecoder把 TCP 字节流按 RTMP Chunk 格式拆包RtmpBusinessHandler处理 connect、publish、play 这些命令。参数上NioEventLoopGroup(1)给 boss 一个线程足够worker 默认是 CPU 核数乘 2机器核多可以显式指定避免线程过多。3. 推流接入与流注册把 OBS 的画面接进来3.1 RTMP 握手与 Chunk 解析的关键点RTMP 握手分简单握手和复杂握手digestOBS 默认走复杂握手。简单握手就是 C0C1 发过去服务器回 S0S1S2客户端再回 C2四步走完。复杂握手要在 C1 里塞时间戳和随机数服务器算 digest 回填。很多自研服务器卡在握手就是因为 digest 算法HMAC-SHA256算错一位客户端直接断开日志里啥也看不出来纯黑匣子。Chunk 解析是另一个坎。RTMP 把消息切成 Chunk每个 Chunk 有 basic header含 chunk stream id 和 fmt、message header时间戳、长度、类型、extended timestamp。fmt 有 0/1/2/3 四种fmt0 带完整头fmt3 复用上一个头。解析时最容易翻车的是extended timestamp当时间戳超过 0xFFFFFF 时要用额外 4 字节漏处理就会导致长直播超过约 4.6 小时后时间戳错乱、播放器花屏。3.2 流注册与鉴权推流端连上来会发publish命令带上流名stream key。服务器要在这里做鉴权常见做法是流名里带 token或者回调业务系统的 HTTP 接口校验。// 处理 publish 命令注册一条直播流 private void onPublish(String app, String streamName, Channel channel) { // 常见做法streamName 形如 live?tokenabc123 String token parseToken(streamName); if (!authService.verify(token)) { // 鉴权失败回 _error 并关连接 sendError(channel, auth failed); channel.close(); return; } String streamKey app / streamName.split(\\?)[0]; // 用 ConcurrentHashMap 存流key 是 app/streamName Stream stream StreamRegistry.register(streamKey, channel); log.info(stream published: {}, publisher{}, streamKey, channel.remoteAddress()); }逻辑说明app是 RTMP 地址里的应用名如rtmp://ip/live/xxx里的livestreamName是流名。鉴权放在 publish 阶段最合适因为此时还没开始传数据拒绝成本最低。StreamRegistry用ConcurrentHashMap保证多线程安全一个流对应一个发布者 Channel 和一组订阅者 Channel。参数上 token 建议带过期时间别用永久 token否则流地址泄露就被人白嫖推流。3.3 音视频帧的缓存与 GOP 对齐推上来的数据是 FLV Tag分音频AAC和视频H264/H265。分发前必须缓存GOPGroup of Pictures也就是从一个关键帧I 帧到下一个 I 帧之前的所有帧。为什么因为新观众进来时如果从 P 帧开始发播放器解不出来直接黑屏或花屏。所以每个新订阅者都要从最近一个 I 帧开始推。// 缓存 GOP新订阅者从 I 帧开始 public void onVideoTag(FlvTag tag) { if (tag.isKeyFrame()) { // 遇到 I 帧把上一个 GOP 归档开新 GOP gopCache.clear(); } gopCache.add(tag); // 广播给所有订阅者 for (Channel sub : subscribers) { sub.writeAndFlush(tag.toByteBuf()); } }参数说明gopCache用有界队列长度按「GOP 时长 × 帧率」估算比如 2 秒 GOP、30 帧缓存 60 帧左右。缓存太大吃内存太小新观众可能拿不到完整 GOP。GOP 长度由推流端决定OBS 默认 2 秒建议别超过 4 秒否则首屏等待和延迟都会变大。4. 拉流分发与 HLS 切片让浏览器和手机都能看4.1 HTTP-FLV 分发HTTP-FLV 本质是把 RTMP 的 FLV Tag 通过 HTTP 长连接吐给客户端前端用 flv.js 解码播放。延迟和 RTMP 差不多但能穿透大部分防火墙浏览器原生 HTTP 就能拉。// 简化的 HTTP-FLV 输出先发 FLV header再持续写 Tag private void serveHttpFlv(ChannelHandlerContext ctx, Stream stream) { // FLV 文件头FLV 版本 标志位 头长度 ctx.writeAndFlush(Unpooled.wrappedBuffer(FLV_HEADER)); // 先补发缓存的 GOP保证首屏能解码 for (FlvTag tag : stream.getGopCache()) { ctx.writeAndFlush(tag.toByteBuf()); } // 注册为订阅者后续新帧实时推送 stream.addSubscriber(ctx.channel()); }逻辑说明FLV header 是固定的 9 字节加 4 字节 PreviousTagSize。关键在「先补 GOP 再订阅」顺序反了会丢帧。参数上 HTTP 响应头要设Content-Type: video/x-flv并且禁用缓冲Transfer-Encoding: chunked否则中间代理会攒一批再发延迟飙升。4.2 HLS 切片与 m3u8 生成HLS 是把流切成一个个 ts 文件配一个 m3u8 索引。延迟大一个切片通常 2~6 秒加上播放器缓冲但兼容性无敌iOS 原生支持。// 每积累一个 GOP 或达到切片时长就落一个 ts 文件 public void onGopComplete(ListFlvTag gop) { String tsName stream_ seq .ts; // 把 FLV Tag 转成 TS 封装H264 AAC 打包成 MPEG-TS byte[] tsData TsMuxer.mux(gop); fileStorage.write(tsName, tsData); // 更新 m3u8保留最近 N 个切片 m3u8Window.add(tsName); if (m3u8Window.size() 6) { String old m3u8Window.removeFirst(); fileStorage.delete(old); // 删旧切片防止磁盘爆 } writeM3u8(); }参数说明切片时长建议 2~4 秒太短文件多、请求频繁太长延迟大。m3u8 窗口保留 3~6 个切片播放器一般回看 3 个。TsMuxer是难点H264 的 Annex-B 和 AVCC 格式转换、PTS/DTS 时间戳计算都容易错建议先用 FFmpeg 命令行验证封装逻辑再移植到 Java。4.3 并发分发时的背压处理订阅者一多如果某个客户端网络慢writeAndFlush会堆积在 ChannelOutboundBuffer 里内存暴涨最后 OOM。必须做背压Channel 不可写时丢弃非关键帧。for (Channel sub : subscribers) { if (!sub.isWritable()) { // 通道写满只保留关键帧丢 P 帧避免内存堆积 if (tag.isKeyFrame()) { sub.writeAndFlush(tag.toByteBuf()); } continue; } sub.writeAndFlush(tag.toByteBuf()); }参数说明isWritable()由 Netty 的高水位线默认 64KB控制可以调WRITE_BUFFER_WATER_MARK。丢帧策略上音频帧通常也保留视频只留 I 帧这样慢客户端画面会卡但不会断比直接踢掉体验好。5. 避坑与排查那些让我熬夜的直播故障5.1 推流成功但播放器一直转圈现象OBS 显示推流正常网页播放器黑屏转圈。原因新订阅者从 P 帧开始收解不出画面。解决确认 GOP 缓存生效且订阅时先补发最近一个 I 帧及其后的帧。排查时打印每个 Tag 的帧类型看第一个发给客户端的 Tag 是不是关键帧。5.2 直播超过几小时后花屏现象短时间直播正常跑几小时后画面撕裂。原因RTMP extended timestamp 没处理时间戳超过 0xFFFFFF 后回绕。解决在 Chunk 解析里判断 timestamp 字段是否为 0xFFFFFF是则读取额外 4 字节作为真实时间戳。这个坑不看协议文档根本想不到。5.3 并发一上来就 OOM现象几十个观众时内存飙升最终 OutOfMemoryError。原因慢客户端导致 ChannelOutboundBuffer 堆积或者 GOP 缓存无上限。解决加背压丢帧见 4.3GOP 缓存用有界队列并给每个 Channel 设写缓冲水位线。用jmapdump 堆看是不是ByteBuf占了大头。5.4 HLS 切片播放卡顿、音画不同步现象HLS 播放每隔几秒卡一下或声音画面错位。原因ts 切片边界没对齐关键帧或 PTS/DTS 计算错误。解决切片必须从 I 帧开始切片时长按 GOP 对齐时间戳用 90kHz 时钟音频和视频各自维护别混用。用 FFprobe 检查生成的 ts 文件时间戳是否连续。5.5 鉴权 token 泄露被人盗推现象流地址被转发别人用你的服务器推流。原因token 永久有效或校验不严。解决token 带过期时间publish 时校验同一流名已有发布者时拒绝新推流或踢掉旧的。日志里记录推流 IP异常时能追溯。6. 进阶技巧用 FFmpeg 做转码适配与延迟压测纯 Java 分发能解决「通」的问题但观众网络千差万别一路 1080p 码率喂给弱网手机必然卡。生产环境常见做法是转码出多档码率让播放端按网速选。Java 里不自己写编解码而是把推上来的流喂给 FFmpeg 子进程转出 720p/480p 再分发。# 把 RTMP 流转成三档分别推到本机不同 app 下 ffmpeg -i rtmp://127.0.0.1/live/stream \ -vf scale1280:720 -c:v libx264 -b:v 1500k -c:a aac -f flv rtmp://127.0.0.1/live/stream_720 \ -vf scale854:480 -c:v libx264 -b:v 800k -c:a aac -f flv rtmp://127.0.0.1/live/stream_480参数说明-b:v是视频码率720p 给 1500k、480p 给 800k 是经验值可按内容调整体育赛事要更高。-c:v libx264用 CPU 软编机器有 N 卡可以换h264_nvenc大幅降 CPU。转码进程要监控挂了要自动重启否则观众看到的是「流还在但没画面」。延迟压测是上线前必做的。用 FFmpeg 模拟推流用脚本模拟 N 个播放端拉流观察服务器 CPU、内存、GC 和端到端延迟。# 模拟推流生成测试画面推 1 小时 ffmpeg -re -f lavfi -i testsrcsize1280x720:rate30 -c:v libx264 -f flv rtmp://127.0.0.1/live/test # 模拟拉流拉 100 路测并发 for i in $(seq 1 100); do ffmpeg -i http://127.0.0.1:8080/live/test.flv -f null - done压测时重点看三件事GC 日志里 Full GC 频率频繁说明对象分配太多ByteBuf 要池化、端到端延迟推流端打时间戳播放端对比、以及断流后资源是否释放Channel 和流注册表有没有清理干净。我自己的习惯是任何直播功能上线前先跑一遍 1 小时长推流加 100 路拉流扛过去才敢放量。这套东西不难难的是每个环节都有人踩过坑希望帮到你。本文还有配套的精品资源点击获取
返回列表