资讯动态

Easy-FLV:Java实现RTSP/RTMP转HTTP-FLV,让浏览器无插件播放监控与直播流

发布时间:2026/9/14 14:30:56 来源:尧图企业网站定制
简介Easy-FLV是一个用Java实现的RTSP/RTMP转FLV流媒体转换库面向需要将监控、直播等实时视频流在浏览器端直接播放的开发者。它借助Java跨平台能力与网络编程优势解决了传统RTSP/RTMP无法被浏览器原生支持的问题适合视频监控、网页直播、流媒体处理等多个应用场景也可作为学习Java网络编程与流媒体协议转换的入门参考。压缩包仅1.18MB共18个文件主体是12个Java源文件覆盖核心转换逻辑同时附带HTML播放示例页面、PNG预览图、Maven配置文件和许可证说明便于直接阅读、导入工程或二次开发。目前已有594人学习下载。对希望掌握流媒体协议转换原理、快速搭建浏览器播放链路的开发者而言这份源码提供了可运行的实现与参考能够帮助理解FLV封装、RTSP/RTMP拉流转码、Web端播放等关键环节。1. 当浏览器遇到 RTSP/RTMPEasy-FLV 的解法监控大屏要直接预览海康摄像头直播运营要求观众在网页上看到 RTMP 推流这两个需求几乎是所有内容平台的标配。但浏览器原生只支持 HTTP 协议族RTSP 的媒体流走 RTPRTMP 则依赖 Flash 时代的 chunk 流机制两边无法直接对话。Easy-FLV 是一个用 Java 实现的流媒体转换工具它把 RTSP/RTMP 拉取下来、解封装成裸音视频帧重新封装成 FLV再通过 HTTP-FLV 输出配合 flv.js 就能让浏览器无插件播放。它适合要自建流接入层的后端、做监控平台 Web 化的开发者以及在直播链路里做协议适配的服务端同学。2. Easy-FLV 的协议转换原理与 Java 实现选型2.1 RTSP、RTMP 不能直接进浏览器的原因RTSPReal Time Streaming Protocol自身只负责会话控制客户端发 OPTIONS、DESCRIBE、SETUP、PLAY 四步请求服务端返回 SDP 描述媒体参数真正的音视频数据由 RTP 在 UDP 或 TCP 上传送。浏览器没有内置的 RTSP 会话状态机也缺少对 SDP 和 RTP 载荷的统一解析即使知道了流地址video 标签也不知道去哪里取数据。RTMPReal Time Messaging Protocol基于 TCP 长连接数据按 chunk 分块前面还有 AMF 编码的头和握手这套逻辑只被 Flash Player 完整实现过而现代浏览器早就移除了 Flash 运行时。FLV 是 Flash Video 的封装格式结构上只有文件头、脚本 tag、视频 tag 和音频 tag。关键是FLV 里存放的 H.264 视频和 AAC 音频编码数据和 RTSP/RTMP 里传输的是同一种裸流只是容器壳不同。浏览器里的 flv.js 通过 Media Source Extensions 把 FLV 解析成媒体片段喂给 video 标签因此只要把输入源的 H.264/AAC 拆出来塞进 FLV tag播放问题就解决了。Easy-FLV 的核心思路就是这个“解封装—重封装”过程全程不碰像素转码性能开销很小。协议传输层控制面浏览器原生支持RTSPUDP/TCPRTSP 会话 SDP无RTMPTCP自有握手 AMF无FLV over HTTPTCPHTTP需 flv.js/MSEHLSTCPHTTP m3u8有选 HLS 也能做浏览器播放但切片粒度决定延迟通常在 5 秒以上HTTP-FLV 的延迟能控制在几百毫秒更适合监控预览和互动直播。2.2 转换管线的模块划分构建这类转换工具我一般把代码切成四块源连接器负责和 RTSP/RTMP 服务器打交道解封装器从容器中读出 H.264 NAL 单元和 AAC 帧FLV 封装器负责把裸帧变成标准 tagHTTP 输出端则面向浏览器维护长连接。模块之间通过一个包队列衔接抓流线程按 DTS 顺序入队每个订阅者从队列中取 tag 写出。断流时源连接器负责按退避算法重连队列要能容忍短暂抖动但不能无限增长否则延迟会持续膨胀。模块职责核心方法源连接器建立 RTSP/RTMP 连接、协商参数、处理断线重连connect() / reconnect()解封装器从容器中读出裸帧维护时间戳nextPacket()FLV 封装器写 header/script tag/音视频 tag归一化时间戳writeHeader() / writeTag()HTTP 输出端处理订阅请求、维护连接、控制背压publish() / close()模块化的好处是 RTSP 和 RTMP 两种输入可以共用后面的封装和输出逻辑。Easy-FLV 内部把两者抽象成同一个抓帧接口所以你在接入时只需要改 URL 前缀和传输层参数封装层完全不用动。2.3 一段核心转换伪代码// 伪代码Easy-FLV 的核心转换循环 StreamSource source sourceFactory.create(inputUrl); // 输入为 rtsp:// 或 rtmp:// FlvMuxer muxer new FlvMuxer(); source.open(); muxer.writeHeader(); // 输出 9 字节 FLV header while (source.hasNextPacket()) { Packet p source.nextPacket(); if (p.isVideo()) { muxer.writeTag(0x09, p.data, p.pts - baseTimestamp); // 视频 tag 类型 0x09 } else { muxer.writeTag(0x08, p.data, p.pts - baseTimestamp); // 音频 tag 类型 0x08 } }这段逻辑说明了转换链路的工作方式writeHeader写入 9 字节 FLV header紧跟一个 onMetaData 脚本 tagflv.js 依赖它拿到宽高和码率信息。baseTimestamp取第一帧的 PTS所有 tag 时间戳都做差值否则时间轴不从 0 开始会导致播放器行为异常。p.pts对 RTSP 来自 RTP 时间戳对 RTMP 来自 chunk 里的 timestamp 字段单位都是毫秒但要注意时钟漂移尤其在多路转发的场景。2.4 为什么选 Java 而不是 C/Go流媒体网关最常见的实现语言是 C性能和底层控制最强但业务集成成本高Go 在并发模型上有优势cgo 调 FFmpeg 却比较麻烦。Java 适合的场景是团队已有 Java 技术栈、需要快速和 Web 业务打通的场景。JVM 提供跨平台能力同一个 jar 在 x86 和 ARM 服务器上都能跑JavaCV 封装了 FFmpeg 的拉流和解封装能力底层 native 库由它管理省去自己编译 FFmpeg 的维护成本HTTP 输出可以用 Netty 或内嵌 Tomcat和既有监控平台共用一套基础设施。缺点也有一路流大概多占用几十 MB 堆内存大规模部署时要压 JVM 参数后面会说到。提示RTSP 拉流优先选 TCP 传输。UDP 在跨网段时丢包会直接导致花屏和马赛克而且问题很难复现。用rtsp_transporttcp让底层走 TCP 通道代价是略增加延迟但稳定性好得多。3. 源码集成与 RTSP/RTMP 转 FLV 的代码实现3.1 工程结构与依赖easy-flv-main 是一个标准 Maven 工程解压后目录里有 pom.xml、src/main、LICENSE、.gitignore。导入 IDE 后先确认 JDK 版本这类项目一般要求 JDK 8 以上。pom.xml 里最关键的依赖是 JavaCV它把 FFmpeg 的 native 能力接到 Java 上还要有一个 HTTP 服务器常见做法是引入 Netty。依赖摘录如下properties javacv.version1.5.9/javacv.version /properties dependencies dependency groupIdorg.bytedeco/groupId artifactIdjavacv-platform/artifactId version${javacv.version}/version /dependency dependency groupIdio.netty/groupId artifactIdnetty-all/artifactId version4.1.100.Final/version /dependency /dependenciesjavacv-platform 这个依赖会把 Windows/Linux 的 native 库都打进来导致 jar 体积偏大生产上可以用 javacv 加 javacv-ffmpeg 按平台裁剪。netty-all 只是为了提供 HTTP 流式响应也可以换内嵌 Tomcat 或 Jetty但 Netty 在长连接并发下更贴合流媒体场景。版本号按你实际编译环境调整不要盲目追新JavaCV 大版本更新经常伴随 API 变动。3.2 抓流与输出的核心代码public void convertToFlv(String inputUrl, OutputStream out) throws IOException { FFmpegFrameGrabber grabber new FFmpegFrameGrabber(inputUrl); if (inputUrl.startsWith(rtsp://)) { grabber.setOption(rtsp_transport, tcp); } grabber.setOption(stimeout, 5000000); grabber.setOption(analyzeduration, 5000000); grabber.start(); // FLV header: F L V 版本 1, 0x05 表示有音视频, header 长度 9 byte[] flvHeader new byte[] { 0x46, 0x4C, 0x56, 0x01, 0x05, 0x00, 0x00, 0x00, 0x09 }; out.write(flvHeader); long baseTimestamp -1; Frame frame; while ((frame grabber.grab()) ! null) { if (baseTimestamp 0) { baseTimestamp frame.timestamp / 1000; } byte tagType (frame.image ! null) ? 0x09 : 0x08; // 视频帧还是音频帧 byte[] tag buildFlvTag(tagType, frame, baseTimestamp); out.write(tag); } grabber.stop(); }rtsp_transporttcp只对 RTSP 源生效RTMP 源不需要设置stimeout是 5 秒没有数据就判定断流单位是微秒5000000 微秒等于 5 秒analyzeduration控制打开流时分析媒体信息的时长设太短会漏掉 SPS/PPS导致浏览器端黑屏。buildFlvTag负责拼 11 字节的 tag 头和 DataSize/Timestamp 字段实现时注意时间戳是 24 位小端加 8 位扩展frame.timestamp单位是微秒要先除以 1000 转成毫秒再写入。3.3 参数配置对照下面这些参数一部分是 FFmpeg 拉流 option一部分是 Easy-FLV 输出端定义的策略落地时看具体封装的暴露方式参数作用常见值调大/调小的影响rtsp_transportRTSP 传输层协议tcpudp 延迟低但丢包易花屏stimeout拉流 socket 超时微秒5000000太小会误判断流太大故障恢复慢analyzeduration流分析时长微秒5000000太小丢失关键参数太大拉流变慢max_delay最大缓存延迟微秒500000影响首帧和整体延迟实时流适当减小gop_cache输出端是否缓存 GOP1开启后秒开但增加内存占用实时场景下我会把max_delay压到 500000 微秒以内避免播放器端缓冲越来越厚但也不能压到 0网络抖动时时间戳收到乱序完全没有缓冲会让音视频 tag 交错混乱。gop_cache放在输出端更合适它决定新客户端接入时是否立刻拿到一个关键帧这直接关系到首屏秒开第 5 章会展开讲。4. 实战把一路 RTSP 监控流发布成 HTTP-FLV 地址4.1 准备测试视频源没有真实摄像头的时候先在本地造一路流。最简单的方式是拿一个 mp4 文件用 ffmpeg 推到本地流媒体服务ffmpeg -re -i sample.mp4 -c copy -f flv rtmp://127.0.0.1:1935/live/demo这条命令按文件的原始时间戳匀速读取-re直接拷贝编码流-c copy到本地 RTMP 端口。前提是本地跑了一个支持 RTMP 的服务比如 nginx-rtmp 或 mediamtx。没有服务的话也可以用带 RTSP 输出的服务端程序ffmpeg 本身不内置 RTSP server需要先起一个独立进程接收。如果接真实摄像头地址格式通常是海康威视rtsp://用户名:密码IP:554/Streaming/Channels/101101 代表主码流第一通道大华rtsp://用户名:密码IP:554/cam/realmonitor?channel1subtype0subtype0 主码流subtype1 子码流调试阶段建议先用子码流带宽和 CPU 占用低验证完链路再切主码流公网上的测试流大多不稳定会影响对转换链路的判断。4.2 启动转换并验证输出编译打包mvn clean package -DskipTests启动时把输入源作为参数传入以项目实际入口为准java -jar target/easy-flv.jar \ --source rtmp://127.0.0.1:1935/live/demo \ --http-port 8080启动后用 curl 验证curl -i --max-time 5 http://127.0.0.1:8080/live/demo.flv | head -20如果看到HTTP/1.1 200 OK、Content-Type: video/x-flv且后续输出是二进制乱码说明 HTTP-FLV 已经推出来了。再用 ffprobe 确认流的编码信息ffprobe -show_streams -select_streams v \ -show_entries streamcodec_name,width,height,avg_frame_rate \ http://127.0.0.1:8080/live/demo.flv这一步能直接看到是不是 h264、分辨率和帧率是否符合预期。如果 codec_name 是 h264 且 width 不为 0链路基本没问题可以进入前端播放环节。4.3 浏览器端接入 flv.js前端需要一个接收 FLV 的播放器flv.js 是目前最常见的实现。工程里先放一份编译好的 flv.min.js然后写播放器页面script srclibs/flv.min.js/script video idmonitor muted controls autoplay/video script if (flvjs.isSupported()) { var flvPlayer flvjs.createPlayer({ type: flv, isLive: true, url: http://127.0.0.1:8080/live/demo.flv }); flvPlayer.attachMediaElement(document.getElementById(monitor)); flvPlayer.load(); flvPlayer.play(); } /scriptisLive: true告诉 flv.js 走实时流逻辑不等待 duration 也不做 seekmuted是为了绕过浏览器自动播放策略用户点击交互后再放开音量。flv.js 内部通过 fetch 或 stream 读取 FLV 数据再用 MSE 转成浏览器能消费的媒体片段所以 Easy-FLV 侧只要持续写 HTTP 响应体客户端会自己消费。如果连播放器都不想用也可以用 VLC 直接打开 HTTP-FLV 地址验证。4.4 常见问题与排查顺序排错先分方向先排除源再查转换链最后看播放器。基顺序错了会浪费大量时间。表现排查方向处理建议页面一直黑屏源没拉通 / 关键帧丢失先用 ffplay 直接放源地址开启 gop cache延迟越来越大缓冲堆积调小 max_delay检查输出队列是否打满画面花屏RTSP 走了 UDP / 丢包强制 tcp 传输抓包看 RTP 重传音画不同步时间戳未做起点归一化检查 baseTimestamp 是否取了第一帧客户端反复断连stimeout 太小调到 10 秒以上看日志里超时次数黑屏时优先做一件事ffplay -rtsp_transport tcp rtsp://...直接播源源能出画面再回来查转换层。花屏基本是传输问题不要先怀疑封装器延迟变大则是缓冲和队列的问题这两个方向差别很大混在一起排查会走弯路。5. 进阶技巧多路流转发与 GOP Cache 秒开5.1 多路流共享上游连接监控集成一次接几十路摄像头是常态如果每个 Web 端订阅都建一条 RTSP 连接摄像头压力会成倍增长很多设备端本身就限制单路并发。常用做法是按 URL 做引用计数同一路流的地址作为 key第一个订阅到来时创建上游拉流会话后续订阅直接复用最后一个订阅断开时才释放上游连接。ConcurrentHashMapString, StreamSession sessions new ConcurrentHashMap(); public void subscribe(String url, ClientConnection client) { StreamSession session sessions.computeIfAbsent(url, k - { StreamSession s new StreamSession(k); s.start(); // 内部创建 FFmpegFrameGrabber 并循环抓帧 return s; }); session.addClient(client); } public void unsubscribe(String url, ClientConnection client) { StreamSession session sessions.get(url); if (session ! null session.removeClient(client) 0) { sessions.remove(url); session.stop(); } }computeIfAbsent保证并发下同一路流只创建一次拉流会话addClient把新客户端的输出流注册进广播列表最后一个客户端退出时removeClient返回 0关闭上游。这里要特别注意 HTTP 连接断开检测Netty 里通过 channelInactive 事件来驱动 unsubscribe否则客户端刷新页面会残留僵尸连接。5.2 GOP Cache 实现秒开浏览器新接入时如果拉流进度刚好落在两个关键帧之间就必须要等下一个 I 帧才能显示画面。对 2 秒一个关键帧的流来说用户会看到最长 2 秒黑屏。解决办法是缓存最近一个关键帧 tag 和 AVC sequence header新客户端进来先发缓存再发实时数据private volatile byte[] lastKeyFrameTag; public void writeVideoTag(byte[] tag, boolean isKeyFrame) { if (isKeyFrame) { lastKeyFrameTag tag; // 只缓存最新 I 帧内存占用一个 tag 大小 } // 正常广播给所有客户端 }新订阅接入时先写 FLV header再写缓存的 I 帧然后回到正常转发流程。注意只发 I 帧还不够要连同 H.264 的 SPS/PPS 配置帧一起缓存否则播放器拿到 I 帧也解不出画面。这个技巧在分辨率越高、GOP 越大的流上收益越明显4K 监控流的效果尤其明显。5.3 用 ffprobe 验证整条链路的延迟最后给一个可复现的验证方法。分别抓源端和输出端的第一帧 PTS做差值# 抓输出流的第一帧时间戳 ffprobe -v error -show_entries packetpts_time -select_streams v \ -of csvp0 http://127.0.0.1:8080/live/demo.flv | head -1 # 抓源流的第一帧时间戳 ffprobe -v error -show_entries packetpts_time -select_streams v \ -of csvp0 rtmp://127.0.0.1:1935/live/demo | head -1两者相减得到引入的转换延迟正常情况下应该在几百毫秒内。如果差值到秒级优先检查 max_delay、输出队列是否堆积以及多路流之间是否错误地共用了同一个时间轴。上线前把这个命令写进排查文档遇到“网页比原始监控慢”的投诉时直接套用比凭感觉调参数可靠得多。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价