news 2026/9/14 14:30:54

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Easy-FLV:Java实现RTSP/RTMP转HTTP-FLV,让浏览器无插件播放监控与直播流

简介: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/RTMP:Easy-FLV 的解法

监控大屏要直接预览海康摄像头,直播运营要求观众在网页上看到 RTMP 推流,这两个需求几乎是所有内容平台的标配。但浏览器原生只支持 HTTP 协议族,RTSP 的媒体流走 RTP,RTMP 则依赖 Flash 时代的 chunk 流机制,两边无法直接对话。Easy-FLV 是一个用 Java 实现的流媒体转换工具,它把 RTSP/RTMP 拉取下来、解封装成裸音视频帧,重新封装成 FLV,再通过 HTTP-FLV 输出,配合 flv.js 就能让浏览器无插件播放。它适合要自建流接入层的后端、做监控平台 Web 化的开发者,以及在直播链路里做协议适配的服务端同学。

2. Easy-FLV 的协议转换原理与 Java 实现选型

2.1 RTSP、RTMP 不能直接进浏览器的原因

RTSP(Real Time Streaming Protocol)自身只负责会话控制:客户端发 OPTIONS、DESCRIBE、SETUP、PLAY 四步请求,服务端返回 SDP 描述媒体参数,真正的音视频数据由 RTP 在 UDP 或 TCP 上传送。浏览器没有内置的 RTSP 会话状态机,也缺少对 SDP 和 RTP 载荷的统一解析;即使知道了流地址,video 标签也不知道去哪里取数据。RTMP(Real 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/MSE
HLSTCPHTTP + m3u8

选 HLS 也能做浏览器播放,但切片粒度决定延迟通常在 5 秒以上;HTTP-FLV 的延迟能控制在几百毫秒,更适合监控预览和互动直播。

2.2 转换管线的模块划分

构建这类转换工具,我一般把代码切成四块:源连接器负责和 RTSP/RTMP 服务器打交道,解封装器从容器中读出 H.264 NAL 单元和 AAC 帧,FLV 封装器负责把裸帧变成标准 tag,HTTP 输出端则面向浏览器维护长连接。模块之间通过一个包队列衔接,抓流线程按 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 脚本 tag,flv.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_transport=tcp让底层走 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.version>1.5.9</javacv.version> </properties> <dependencies> <dependency> <groupId>org.bytedeco</groupId> <artifactId>javacv-platform</artifactId> <version>${javacv.version}</version> </dependency> <dependency> <groupId>io.netty</groupId> <artifactId>netty-all</artifactId> <version>4.1.100.Final</version> </dependency> </dependencies>

javacv-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_transport=tcp只对 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/101,101 代表主码流第一通道
  • 大华:rtsp://用户名:密码@IP:554/cam/realmonitor?channel=1&subtype=0,subtype=0 主码流,subtype=1 子码流

调试阶段建议先用子码流,带宽和 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 OKContent-Type: video/x-flv,且后续输出是二进制乱码,说明 HTTP-FLV 已经推出来了。再用 ffprobe 确认流的编码信息:

ffprobe -show_streams -select_streams v \ -show_entries stream=codec_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 src="libs/flv.min.js"></script> <video id="monitor" 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(); } </script>

isLive: true告诉 flv.js 走实时流逻辑,不等待 duration 也不做 seek;muted是为了绕过浏览器自动播放策略,用户点击交互后再放开音量。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,第一个订阅到来时创建上游拉流会话,后续订阅直接复用;最后一个订阅断开时才释放上游连接。

ConcurrentHashMap<String, 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 packet=pts_time -select_streams v \ -of csv=p=0 http://127.0.0.1:8080/live/demo.flv | head -1 # 抓源流的第一帧时间戳 ffprobe -v error -show_entries packet=pts_time -select_streams v \ -of csv=p=0 rtmp://127.0.0.1:1935/live/demo | head -1

两者相减得到引入的转换延迟,正常情况下应该在几百毫秒内。如果差值到秒级,优先检查 max_delay、输出队列是否堆积,以及多路流之间是否错误地共用了同一个时间轴。上线前把这个命令写进排查文档,遇到“网页比原始监控慢”的投诉时直接套用,比凭感觉调参数可靠得多。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/14 14:30:49

ESP32-S3 N16R8入坑指南:环境搭建、工程结构与避坑实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/14 14:30:00

基于Qt6与SQLite的学生信息管理系统:从登录到CRUD的完整工程化实践

简介&#xff1a;这是一份基于Qt6.2.4开发的学生信息管理系统完整源码与配套设计报告&#xff0c;面向C/Qt学习者、高校学生及需要完成课程设计或毕业设计的开发者。系统实现了注册登录、学生信息增删查改、管理员信息管理等核心功能&#xff0c;界面采用QPushButton、QLineEdi…

作者头像 李华
网站建设 2026/9/14 14:29:41

元初混沌体系 第四卷 太赫兹高频通信与超宽带频谱体系:第四十九篇 星际高温航天器天线太赫兹热畸变修正模型

第四十九篇 星际高温航天器天线太赫兹热畸变修正模型本篇单元定位本篇隶属第四卷太赫兹高频通信与超宽带频谱体系 第三单元星际真空太赫兹超远距传输架构&#xff08;37–54&#xff09;&#xff0c;为星际太赫兹传输体系极端温度工况适配、硬件形变误差清零、波束物理偏移修正…

作者头像 李华
网站建设 2026/9/14 14:28:12

2026汽车轮胎锁选购全攻略:类型、价格、品牌与避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/14 14:27:15

手机购物商城HTML源码解析:从静态页面到可联调的前端原型

简介&#xff1a;手机购物商城网站HTML源码是一套面向移动端电商场景的前端页面源码包&#xff0c;适合正在学习HTML/CSS/JavaScript的开发者&#xff0c;以及需要快速搭建手机购物商城原型的个人站长或产品经理。资源包为RAR压缩格式大小9.86MB&#xff0c;内部以HTML页面为主…

作者头像 李华