
简介本资源是一个基于Netty实现的轻量级RTMP服务器开源项目面向Java后端开发者、流媒体技术学习者及实时音视频服务实践者解决从零构建RTMP推拉流服务的核心技术落地问题。项目完整实现了RTMP协议解析、连接管理、音视频数据接收与分发逻辑适配FFmpeg推流测试可用于直播中台原型开发、协议学习与高并发网络编程实战。压缩包共50个文件含16个核心Java源码涵盖Netty ChannelHandler、RTMP消息编解码、握手流程等、19个编译后class文件、9个XML配置与构建文件如pom.xml、IDEA编译配置以及README文档和模块定义文件整体仅59KB结构精简、便于快速导入与调试。目前已有767人学习下载读者可直接运行服务端、结合FFmpeg命令完成推流验证并通过源码深入理解Netty事件驱动模型在流媒体协议中的典型应用掌握RTMP握手、Chunk Stream、AMF序列化等关键机制。1. 这不是又一个“Hello World” Netty 示例它真能扛住 OBS 推流不崩、FFmpeg 拉流不断、100 路并发下延迟压在 800ms 内的 RTMP 服务器你试过用 Spring Boot 写个 HTTP 接口再硬塞进RTMPHandler吗我试过——推流 3 秒后连接重置Wireshark 抓包显示全是乱序 Chunk Header日志里只有一行io.netty.handler.timeout.ReadTimeoutException连错误堆栈都懒得打全。这不是配置问题是协议层没对齐。而这个rtmpServer-master不是教学玩具它是用 Netty 原生 ChannelPipeline 一帧一帧啃完 RTMP 规范AMF0/AMF3、Chunk Stream ID 分配、Set Peer Bandwidth、CreateStream 响应序列后落地的生产级骨架。它不依赖任何商业 SDK不包装 FFmpeg 二进制所有字节解析、时间戳校准、音视频包分离、GOP 缓存策略全在 Java 层可控它默认监听1935端口OBS 一填rtmp://localhost:1935/live就能推VLC 一输rtmp://localhost:1935/live/stream1就能播更关键的是它把 Netty 最难缠的粘包/半包问题拆解成可验证的三道关卡Chunk Basic Header 解析容错、Message Header 时间戳回溯补偿、以及关键的chunkSize动态协商状态机——这直接决定了你用手机推流时网络抖动是否导致花屏。适合正在从 Nginx-RTMP 切换自研流媒体中台的后端工程师、需要嵌入式设备上跑轻量 RTMP Server 的 IoT 团队以及被netty粘包处理卡在面试最后一轮的中级开发者。别急着 clone先看清它怎么把协议黑匣子变成白盒。2. 从零启动编译、运行与最简推拉流闭环验证含 OBS/VLC/FFmpeg 实测命令2.1 环境准备JDK 11、Maven 3.6、FFmpeg 4.4 是硬门槛别碰 JDK 17 的 preview 特性这个项目基于 Maven 构建但pom.xml里藏着两个容易被忽略的约束一是maven-compiler-plugin显式锁死source11/source和target11/target二是netty-all依赖版本为4.1.94.Final—— 这不是随便选的。Netty 4.1.90 才彻底修复了CompositeByteBuf在discardReadBytes()后引发的IndexOutOfBoundsException而该异常在 RTMP 音频 AAC ADTS 头解析时高频触发。如果你用 JDK 17 编译Serial注解会强制要求serialVersionUID但项目里RtmpMessage类没声明编译直接失败。所以请执行# 验证 JDK 版本必须 11.x推荐 11.0.20 java -version # 输出应为openjdk version 11.0.20 2023-07-18 # 验证 Maven3.6.3 是最低兼容版 mvn -v # 输出应含Apache Maven 3.6.3 # 验证 FFmpeg需支持 librtmpmacOS 用 brew install ffmpeg --with-librtmp ffmpeg -version | head -n1 # 输出应含ffmpeg version 4.4.4 或更高提示Windows 用户若遇到Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin检查JAVA_HOME是否指向 JDK 11 根目录非 JRE且PATH中java命令必须来自同一 JDK。2.2 编译与启动跳过target目录陷阱用-DskipTests绕过未配置的单元测试项目根目录下pom.xml定义了rtmp-server模块但src/test/java下测试类缺失README.md也未说明测试用例位置。强行mvn test会因No tests found失败。正确流程是跳过测试直出可执行 JAR# 进入 rtmpServer-master 目录确保 pwd 输出含 /rtmpServer-master cd rtmpServer-master # 清理旧构建跳过测试编译并打包 mvn clean package -DskipTests # 检查生成物关键不是 target/rtmp-server-1.0-SNAPSHOT.jar而是 target/rtmp-server-1.0-SNAPSHOT-jar-with-dependencies.jar ls -lh target/*jar-with-dependencies.jar # 正常输出-rw-r--r-- 1 user staff 5.2M Jun 10 14:22 target/rtmp-server-1.0-SNAPSHOT-jar-with-dependencies.jar启动命令必须显式指定主类因为MANIFEST.MF中Main-Class未设置# 启动服务器监听 0.0.0.0:1935日志输出到控制台 java -cp target/rtmp-server-1.0-SNAPSHOT-jar-with-dependencies.jar com.rtmp.server.RtmpServerApplication # 成功启动标志注意不是 Started Application而是 Netty 的 channelActive 日志 # [INFO] io.netty.handler.logging.LoggingHandler - [id: 0x..., L:/0.0.0.0:1935] ACTIVE2.3 三端实测OBS 推流、FFmpeg 拉流、VLC 播放验证完整链路OBS 推流Windows/macOS/Linux 通用打开 OBS → 设置 → 流 → 服务选“自定义”服务器填rtmp://127.0.0.1:1935/live串流密钥填test项目默认 stream key 为test见com.rtmp.config.RtmpConfig.STREAM_KEY。点击“开始推流”此时服务器日志应立即出现[INFO] com.rtmp.handler.RtmpHandshakeHandler - Handshake completed for channel [id: 0x...] [INFO] com.rtmp.handler.RtmpMessageHandler - Received publish command: applive, streamtest, tcUrlrtmp://127.0.0.1:1935/liveFFmpeg 拉流验证原始数据流新开终端执行# 拉取 RTMP 流并转成 MP4-t 10 截取 10 秒-y 强制覆盖 ffmpeg -i rtmp://127.0.0.1:1935/live/test -c copy -t 10 -y output_test.mp4 # 成功标志ffmpeg 输出包含 Input #0, flv, from rtmp://127.0.0.1:1935/live/test: 且无 Connection refused 错误VLC 播放验证终端兼容性VLC → 媒体 → 打开网络串流 → URL 输入rtmp://127.0.0.1:1935/live/test→ 播放。画面出现即证明 GOP 缓存、SPS/PPS 重传、音频 AAC Sequence Header 同步全部生效。注意若 VLC 黑屏但有声音大概率是output_test.mp4里视频流损坏需检查服务器日志中是否出现Invalid video packet timestamp—— 这指向第 4 章的时钟同步坑。3. 协议层深挖RTMP Chunk 解析、AMF0 命令解码与 Netty Pipeline 的三道生死关3.1 Chunk 结构解析为什么chunkSize动态协商是抗抖动核心RTMP 不是 TCP 直传音视频裸流而是将数据切分成最大 128 字节的 Chunk默认值每个 Chunk 带 1~3 字节 Basic Header 0~11 字节 Message Header。rtmpServer-master的RtmpChunkDecoder类位于src/main/java/com/rtmp/decoder/实现了完整的 Chunk 重组逻辑。关键点在于当客户端如 OBS发送Set Chunk Size命令时服务器必须立即切换接收 Chunk 的大小并在响应中返回新的chunkSize。项目中该逻辑位于RtmpCommandHandler.handleSetChunkSize()// com.rtmp.handler.RtmpCommandHandler.java public void handleSetChunkSize(RtmpMessage message) { int newChunkSize message.getBody().readInt(); // 更新当前 channel 的 chunkSize存储在 ChannelAttribute 中 channel.attr(RtmpConstants.CHUNK_SIZE_ATTR).set(newChunkSize); // 发送响应必须用新 chunkSize 编码响应包 RtmpMessage response RtmpMessage.createSetChunkSize(newChunkSize); response.setChunkStreamId(2); // Control Stream ID channel.writeAndFlush(response); }这里埋着第一个性能雷newChunkSize若设为 64KB某些高码率推流器会这么做而服务器未及时更新ChannelAttribute后续 Chunk 解析就会因缓冲区溢出直接断连。实测发现当newChunkSize 65535时RtmpChunkDecoder.decode()中的in.readBytes(byteBuf, length)会抛IndexOutOfBoundsException但项目未做try-catch包裹 —— 这就是第 4 章要填的坑。3.2 AMF0 命令解码connect、createStream、publish的状态机如何驱动推流生命周期RTMP 控制命令如connect通过 AMF0 序列化传输rtmpServer-master使用org.red5.io.amf.AMF0来自 red5-server 依赖解析。但RtmpCommandHandler并非简单解码而是维护一个严格的状态机当前状态收到命令动作状态迁移WAIT_CONNECTconnect校验app、tcUrl写入RtmpSession→WAIT_CREATE_STREAMWAIT_CREATE_STREAMcreateStream分配streamId存入 session.streams→WAIT_PUBLISHWAIT_PUBLISHpublish设置streamKey启动RtmpStreamPublisher→PUBLISHING这个状态机在RtmpCommandHandler.channelRead()中强制执行// 状态校验逻辑简化 if (session.getState() RtmpSessionState.WAIT_PUBLISH !command.equals(publish)) { sendError(channel, Unexpected command: command); return; }玄学点来了publish命令的streamName参数若含/如live/test项目默认只取test作为 stream key但tcUrl中的applive才是实际应用名。这意味着rtmp://127.0.0.1:1935/live/test和rtmp://127.0.0.1:1935/live/abc共享同一个live应用上下文 —— 这正是多路推流的基础也是obs-multi-rtmp插件能工作的前提。3.3 Netty Pipeline 构建RtmpChunkDecoder必须在RtmpMessageDecoder之前顺序错了就全崩RtmpServerApplication初始化ServerBootstrap时Pipeline 构建顺序是生命线// com.rtmp.server.RtmpServerApplication.java pipeline.addLast(chunkDecoder, new RtmpChunkDecoder()); pipeline.addLast(messageDecoder, new RtmpMessageDecoder()); pipeline.addLast(handshakeHandler, new RtmpHandshakeHandler()); pipeline.addLast(commandHandler, new RtmpCommandHandler()); pipeline.addLast(streamHandler, new RtmpStreamHandler());为什么chunkDecoder必须第一因为RtmpMessageDecoder期望输入是完整的 RTMP Message含完整 Message Header而网络层只给 TCP 字节流。RtmpChunkDecoder的职责是从 TCP 流中识别 Chunk 边界、重组 Message、处理chunkSize变更。如果顺序颠倒RtmpMessageDecoder会收到半截 ChunkreadUnsignedShort()直接读到错误字节解析出的时间戳变成负数后续所有音视频同步全乱。实测对比将chunkDecoder移到messageDecoder后OBS 推流 2 秒后报错java.lang.IndexOutOfBoundsException: readerIndex(2) length(2) exceeds writerIndex(2): UnpooledHeapByteBuf(ridx: 2, widx: 2, cap: 2)—— 这就是典型的半包解析失败。4. 避坑指南5 条血泪经验总结每一条都来自真实翻车现场4.1 现象OBS 推流后服务器日志卡在Handshake completed无publish日志连接 30 秒后自动断开原因OBS 默认启用Low Latency模式会发送Set Peer Bandwidth命令类型 6但项目RtmpCommandHandler未实现对该命令的响应。Netty 默认超时时间为 30 秒ReadTimeoutHandler超时后强制关闭连接。解决在RtmpCommandHandler中添加空响应RTMP 规范允许忽略// 在 channelRead 方法中追加 if (message.getType() RtmpMessageType.SET_PEER_BANDWIDTH) { // 发送空响应维持连接 channel.writeAndFlush(Unpooled.EMPTY_BUFFER); return; }4.2 现象FFmpeg 拉流时提示Unable to open RTMP connectionWireshark 显示服务器发回0x03Window Acknowledgement后无后续原因RtmpHandshakeHandler中handshakeCompleted()方法调用了channel.config().setAutoRead(true)但某些 Netty 版本在AUTO_READ开启前ChannelInboundHandler的channelActive()未被触发导致RtmpMessageDecoder未注册。解决强制在握手完成后手动触发一次channelRead()// RtmpHandshakeHandler.java Override public void handshakeCompleted(ChannelHandlerContext ctx) { ctx.channel().config().setAutoRead(true); // 关键注入一个空 ByteBuf 触发 pipeline 流转 ctx.fireChannelRead(Unpooled.EMPTY_BUFFER); }4.3 现象多路推流时第二路 OBS 推流成功但 VLC 播放第二路时黑屏第一路正常原因RtmpStreamPublisher类中publishStream()方法使用ConcurrentHashMap存储streamId→RtmpStream映射但RtmpStream的writeVideoPacket()方法未加锁多线程写入videoBuffer导致数据错乱。解决在RtmpStream构造时初始化线程安全缓冲区// RtmpStream.java private final AtomicReferenceByteBuf videoBuffer new AtomicReference(); // writeVideoPacket 中改为 ByteBuf old videoBuffer.getAndSet(packet.retain()); // retain 防止释放 if (old ! null) old.release();4.4 现象手机用whip推流工具推流服务器日志报Invalid AMF0 type: 0x00连接立即中断原因WHIP 协议推流器发送的是 HTTP-FLV 封装非原生 RTMP。RtmpChunkDecoder尝试解析 HTTP 头部POST / HTTP/1.1为 RTMP ChunkBasic Header字节0x00被误判为非法类型。解决在RtmpChunkDecoder的decode()开头增加 HTTP 协议嗅探// decode 方法第一行 if (in.readableBytes() 4) { in.markReaderIndex(); byte[] prefix new byte[4]; in.readBytes(prefix); in.resetReaderIndex(); if (prefix[0] P prefix[1] O prefix[2] S prefix[3] T) { throw new IllegalStateException(HTTP request detected, not RTMP); } }4.5 现象rtmp测试地址在公网部署后VLC 播放卡顿Wireshark 显示大量TCP Retransmission原因服务器ServerBootstrap未设置SO_RCVBUF和SO_SNDBUFLinux 内核默认 TCP 缓冲区仅 256KB无法承载 4K 视频流突发流量。解决在RtmpServerApplication的initChannel()中添加// childOption 设置 .childOption(ChannelOption.SO_RCVBUF, 1024 * 1024) // 1MB .childOption(ChannelOption.SO_SNDBUF, 1024 * 1024) .childOption(ChannelOption.TCP_NODELAY, true);5. 生产就绪动态 stream key 验证、HLS 自动转封装与延迟压测技巧5.1 实现 stream key 白名单机制拒绝非法推流避免视频点击率500平台发信息推流类攻击项目默认接受任意streamKey这在公网环境极危险。我们通过RtmpConfig注入白名单并在RtmpCommandHandler.handlePublish()中校验// 修改 RtmpConfig.java添加 public static final SetString VALID_STREAM_KEYS Collections.unmodifiableSet(new HashSet(Arrays.asList(live_2023, backup_001))); // 在 RtmpCommandHandler.handlePublish() 中插入 String streamKey message.getStreamName(); // 从 publish 命令 body 解析 if (!RtmpConfig.VALID_STREAM_KEYS.contains(streamKey)) { sendError(channel, Invalid stream key: streamKey); channel.close(); return; }提示白名单建议从 Redis 加载避免重启生效。RtmpSession构造时可传入SupplierSetString实现热更新。5.2 集成 FFmpeg 转 HLS一行命令让 RTMP 流自动生成index.m3u8适配 Vue 的 rtmp 播放器rtmpServer-master本身不转封装但可利用其稳定推流能力配合 FFmpeg 做边缘转码。在服务器启动后执行# 拉取 RTMP 流转成 HLS-hls_time 2 每 2 秒切片-hls_list_size 5 保留最近 5 个 ts ffmpeg -i rtmp://127.0.0.1:1935/live/test \ -c:v libx264 -c:a aac -f hls \ -hls_time 2 -hls_list_size 5 -hls_flags delete_segments \ -y /var/www/html/hls/index.m3u8 # 前端 Vue 项目中用 video.js http-streaming 播放 // video-js># 模拟 200ms RTT 5% 丢包推流端执行 sudo tc qdisc add dev lo root netem delay 200ms loss 5% # 启动 OBS 推流同时用 ffplay 拉流测延迟 ffplay -i rtmp://127.0.0.1:1935/live/test -stats -autoexit # 关键指标ffplay 输出的 st:0 pts:... dts:... pos:... fmt:flv 中dts 差值应 1200ms # 若超过 1500ms检查 RtmpStreamPublisher 中 GOP 缓存是否超过 3 帧项目默认 2 帧从那以后我每次上线新流媒体服务都强制走一遍tc压测先delay 100ms再delay 200ms loss 2%最后delay 300ms loss 5%。不是为了炫技是当客户问“你们说低延迟到底多低”时我能直接甩出ffplay截图和tc命令。希望帮到你。本文还有配套的精品资源点击获取