ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

JavaCV实现RTSP流Web播放:转码推流到HTTP-FLV的完整实践

JavaCV实现RTSP流Web播放:转码推流到HTTP-FLV的完整实践 前阵子接了个监控平台的项目需求本身一句话就能讲完管理后台里要能点开摄像头实时看到车间画面。摄像头是现成的海康、大华RTSP 地址也能从设备管理页面拿到可真正卡住的地方在于——Web 前端怎么把 RTSP 流播放出来。这个问题我估摸做过安防、物联网、线上巡检的同学都遇到过。浏览器不认 RTSP 协议直接把rtsp://admin:xxx192.168.1.64:554/...甩给video标签肯定不通。大多数人第一反应是“用前端插件”或者“让浏览器装客户端”但正经做项目前端面对的是一堆不能装插件的员工电脑和手机浏览器只能在服务端想办法。这篇我把 Java 侧接收 RTSP 流、解码转码、再推给前端实时播放的完整链路拆开讲一遍包含我从选型、编码、联调到排坑的实操记录适合正在做类似功能的后端、全栈和流媒体入门开发者参考。1. 从 RTSP 到 Web 播放中间到底少了哪几环先别急着写 Java 代码得先搞清楚为什么浏览器不能直接播 RTSP。RTSPReal Time Streaming Protocol本身是一个会话控制协议负责发起、暂停、结束流媒体会话实际音视频数据靠 RTP 包传输另外还有 RTCP 做质量控制。这一套组合拳在网络摄像头领域非常成熟但它和 Web 生态完全是两个世界。打个比方RTSP 像是餐厅里的“点菜流程现炒现送”服务员问你吃什么、后厨炒完直接端到你面前。这个过程灵活但需要维持一整条独立的沟通通道。浏览器里的video标签则更像是“便利店自取”只想通过 HTTP 地址从货架上拿标好格式的商品根本不关心你后厨怎么运作。所以要让摄像头画面进 Web 页面服务端必须充当一个“外卖中转站”把 RTSP 的会话控制、RTP 传输转换为浏览器认识的 HTTP 流。除了协议差异还有编码层面的问题。绝大多数网络摄像头输出 H.264这个浏览器能解但很多新款设备默认或可选输出 H.265HEVC问题就来了Chrome 长期以来不原生支持 H.265 硬解只在部分系统和硬件环境下能软解或通过系统解码器间接播放Safari 对 H.265 的支持也不统一。也就是说哪怕协议打通了编码不合适依然白搭。这里有两种处理思路一种是不动编码只把 H.264 裸流重新封装成适合 Web 播放的容器格式这叫“转封装”transmux另一种是正儿八经解码后再重新编码成 H.264这叫“转码”transcode。转封装性能开销低但要求源流本来就是 H.264转码灵活任何 H.265 甚至 H.264 都能统一输出成 H.264代价是吃 CPU/GPU。题目标题里带着“解码”两个字我理解就是包含了转码这个动作后面我也着重讲这条路。再有一个维度是延迟。监控场景讲究实时你对着画面喊话或者盯着设备动作延迟 3 秒还能忍延迟 10 秒基本没法用。常见的分发协议里HLS兼容性最好大部分手机浏览器直接支持Safari 也能播但切片机制决定了延迟普遍在 3-15 秒HTTP-FLV延迟可以控制在 1-3 秒PC 端有 flv.js 这类 MSE 封装库支持移动端兼容性差一些WebRTC延迟最低能到几百毫秒但服务端接入复杂度高需要另外做信令服务和媒体协商。所以“先选传输再写代码”是最重要的。你要是没想清楚前端到底跑在 PC 还是手机、延迟要求多高、并发几路上来就写 JavaCV 拉流后面很可能推倒重来。2. Java 拉流转推的技术选型别急着写代码我见过不少人一拿到需求就直奔代码用 JavaCV 把 RTSP 流接进来然后塞进 WebSocket 里往浏览器推。这个方案在单路、内网、实验性项目里能跑通但稍微上点规模就麻烦。选型阶段多花半小时后面能省几天。先把 Java 生态里常见的四条路线摊开看方案实现要点延迟优点缺点JavaCV 流媒体服务器JavaCV 拉流转码推 RTMP/RTSP 给 SRS 或 MediaMTX前端从服务器取 HTTP-FLV / HLS / WebRTC1-3 秒分层清晰稳定支持并发扩展多部署一个流媒体服务JavaCV 自建 HTTP-FLV 输出自己用 Netty/Tomcat 写一个简易分发服务1-3 秒减少外部依赖要处理大量连接细节运维和排错成本高纯 FFmpeg 命令行进程Java 里ProcessBuilder调 ffmpeg1-3 秒灵活不用写 Java 编解码进程管理麻烦回传状态、停止重启都不优雅只上流媒体服务器SRS/MediaMTX 直接拉摄像头 RTSP 并分发1-3 秒部署最简单脱离了 Java 业务逻辑权限控制、动态管理不方便我自己最终选了第一种也就是 JavaCV 负责“接入转码”SRS或者轻量一点的 MediaMTX负责“分发”。理由很现实Java 端的强项是业务集成、摄像头上线下线管理、权限控制、和现有后台系统对接而真正的流分发比如 HTTP-FLV 的 chunked 响应、WebRTC 的 ICE/DTLS 协商、HLS 切片缓存SRS 这类成熟服务器已经处理得非常好没有必要自己在 Java 里重造轮子。那么问题来了什么时候才需要 Java 参与如果只是临时看一路画面你甚至可以不用写代码直接命令行跑/usr/local/srs/etc/...那样的配置就能拉流。但如果你的系统里摄像头有几十路、几百路需要动态从数据库读取摄像头列表、按用户权限点播、随时控制开启和关闭这时候 Java 服务就是必不可少的“管家”。它负责按照业务规则拉起和停止一路一路的转推任务而分发这件事继续交给专业流服务器。还有一点我觉得值得提醒JavaCV 的依赖体积很大javacv-platform会带入 OpenCV、FFmpeg、OpenBLAS 等一堆平台动态库。如果你的服务器在内网Maven 中央仓库不一定能顺利拉这些大包建议只引入真正需要的模块dependency groupIdorg.bytedeco/groupId artifactIdjavacv/artifactId version1.5.9/version /dependency dependency groupIdorg.bytedeco/groupId artifactIdffmpeg-platform/artifactId version6.1.1-1.5.9/version /dependency这样只带 FFmpeg不拖 OpenCV 那些无关包部署包能小一大截。版本之间要注意对应关系javacv和ffmpeg-platform的版本号后缀经常一致比如1.5.9对应6.1.1-1.5.9。3. JavaCV 上场拉流、解码、再推流的完整链路JavaCV 本质上是 JavaCPP 对 FFmpeg 的封装所以你几乎可以把 FFmpeg 的命令行参数思维平移到代码里。下面是我在一路摄像头从“拉取”到“推送”过程中会涉及的核心代码。3.1 拉流端配置参数比代码更重要初始化一个FFmpegFrameGrabber看似简单真正决定稳不稳定的是几个隐藏参数。FFmpegFrameGrabber grabber new FFmpegFrameGrabber(rtspUrl); // 用 TCP 而不是 UDP 拉流。UDP 在内网偶尔丢包画面会花屏TCP 牺牲一点延迟换来稳定。 grabber.setOption(rtsp_transport, tcp); // 设置套接字超时单位是微秒这里设 5 秒。摄像头断网后拉流线程不至于一直挂死。 grabber.setOption(stimeout, 5000000); // 减少缓冲区对实时性有帮助。 grabber.setOption(fflags, nobuffer); // 遇到损坏的帧尝试跳过而不是直接中断。 grabber.setOption(err_detect, ignore_err); grabber.start();stimeout这个参数我强烈建议设置。摄像头或者交换机电一断如果没有超时grabber.start()或grabber.grab()会阻塞很久你的“重连机制”就没法及时触发。别问我怎么知道连续烧掉几个重连线程之后我就把它列成默认配置了。3.2 转码与推流配置尽量向低延迟编码参数靠拢接下来是FFmpegFrameRecorder。这一步的目标是把摄像头原始流解码后重新编码成 H.264/AAC封装成 FLV然后推给上游流媒体服务器。FFmpegFrameRecorder recorder new FFmpegFrameRecorder(pushUrl, width, height, 1); recorder.setFormat(flv); recorder.setVideoCodec(avcodec.AV_CODEC_ID_H264); recorder.setVideoOption(preset, veryfast); recorder.setVideoOption(tune, zerolatency); recorder.setVideoBitrate(2000 * 1000); recorder.setFrameRate(frameRate); recorder.setGopSize((int) frameRate * 2); if (hasAudio) { recorder.setAudioCodec(avcodec.AV_CODEC_ID_AAC); recorder.setAudioBitrate(64 * 1000); recorder.setSampleRate(44100); recorder.setAudioChannels(1); } recorder.start();这里几个选项要解释一下presetveryfast牺牲一点压缩率换取更快的编码速度对实时转码来说比文件体积重要得多tunezerolatency要求编码器不引入额外延迟会减少 B 帧的使用对低延迟很关键setGopSizeGOP 是关键帧间隔。如果设得太小比如 1 秒一个关键帧带宽占用高设得太大比如 100 帧以上前端起播时可能要等一个关键帧黑屏时间会变长。我一般取帧率×2也就是 2 秒一个关键帧平衡起播速度和带宽。pushUrl如果是推给 SRS/MediaMTX一般长这样rtmp://127.0.0.1:1935/live/cam_001SRS 默认监听 RTMP随后对外提供 HTTP-FLV地址就是http://127.0.0.1:8080/live/cam_001.flv这块前置服务怎么搭不是 Java 代码的事我会放到后面第 5 章结合部署经验一起说。3.3 主循环别把“拉取”和“录制”写死成一步核心的循环其实很短Frame frame; long lastTime System.currentTimeMillis(); while (running) { try { frame grabber.grab(); // 同时拉取视频帧和音频帧 if (frame ! null) { recorder.record(frame); } } catch (Exception e) { log.error(拉流/推流异常: {}, e.getMessage()); break; // 跳出循环交给上层重连逻辑 } }这里有个容易踩的坑grab()会同时返回视频帧和音频帧而grabImage()只返回视频帧。如果你的摄像头带音频又不想保存声音那就别用带音轨的参数初始化recorder否则会出现“av_interleaved_write_frame(): Invalid data”之类的报错。最稳妥的做法是先用ffprobe看看摄像头实际有没有音频轨道没有音频的设备就老老实实让recorder保持纯视频模式。grab()返回的Frame本质上是解码后的原始数据所以recorder.record(frame)再编码这就是完整的“解码转码”过程。如果源流本身就是 H.264 且你不想折腾硬件编码也可以走更低层的 AVPacket 接口做转封装但 JavaCV 里这套 API 比较绕一般项目没必要。3.4 不要重复创建 grabber一个摄像头一个任务线程真实项目中你会面对很多路摄像头。我见过同学写“开启视频”的接口每次请求都new FFmpegFrameGrabber().start()然后也不存引用过一会儿连接数爆炸。正确做法是给每路摄像头维护一个独立的“拉流转推任务”任务内部管理 grabber 和 recorder 的生命周期并提供停止、重启两个方法。我这里给一个简单的任务骨架public class RtspPushTask implements Runnable { private final String rtspUrl; private final String pushUrl; private volatile boolean running true; public void stop() { running false; } Override public void run() { while (running) { FFmpegFrameGrabber grabber null; FFmpegFrameRecorder recorder null; try { grabber createGrabber(); grabber.start(); recorder createRecorder(pushUrl, grabber); recorder.start(); log.info(转推已启动: {} - {}, rtspUrl, pushUrl); Frame frame; while (running (frame grabber.grab()) ! null) { recorder.record(frame); } } catch (Exception e) { log.warn(转推中断: {}, e.getMessage()); } finally { closeQuietly(recorder); closeQuietly(grabber); } if (running !Thread.currentThread().isInterrupted()) { sleepSeconds(3); // 重连等待 } } } }这样断线之后任务会自动重连不会让一个异常就把整个服务拖垮。真正的业务层只需要维护一个MapString, RtspPushTask用户点播就start()关闭就stop()。3.5 如果想脱离 SRS纯 Java 输出 HTTP-FLV 的思路有朋友会问我不想额外部署流媒体服务器Java 能不能直接把 FLV 推给前端能但要自己处理很多分发边界。思路是FFmpegFrameRecorder支持把 FLV 数据写入一个自定义OutputStream你的 HTTP 服务器Netty/Tomcat NIO把这个 OutputStream 接到请求通道上flv.js 请求 URL 时你先把 FLV 文件头写出去然后不断把编码后的 FLV tag 写进响应体保持连接不关。听起来不算复杂实际做起来要命的是连接管理浏览器刷新要清理旧连接网速慢要背压并发一多要控制内存。我第一版为了图省事这么搞过一次后来发现“能播”和“能稳定播”之间差了十万八千里。如果你不是特别明确知道自己在做什么还是让 SRS/MediaMTX 去干这事Java 这边做好拉流和转码就够了。4. 前端播放接入flv.js 为主、HLS 兜底的落地姿势后端推流链路通了前端反而简单但也有一些细节直接影响用户体验。4.1 flv.js 播放 HTTP-FLV先装包npm install flv.js然后初始化播放器。这里面的参数优化我实际调过很多遍import flvjs from flv.js; if (flvjs.isSupported()) { const videoElement document.getElementById(video); const player flvjs.createPlayer({ type: flv, isLive: true, url: http://your-server:8080/live/cam_001.flv, }, { enableStashBuffer: false, // 关掉缓存显著降低延迟 isLive: true, lazyLoad: false, autoCleanupSourceBuffer: true, }); player.attachMediaElement(videoElement); player.load(); player.play().catch(console.error); }enableStashBuffer: false是低延迟的关键。默认情况下 flv.js 会缓冲一部分数据网络波动时不那么容易卡但实时性会变差。如果是监控类应用我会选择关闭让画面尽量贴近实时如果是直播带货那种要极稳的反而建议保留缓冲。在 Vue 3 里接入时要注意播放器实例的生命周期script setup import flvjs from flv.js; let player null; function playStream(url) { if (player) { player.destroy(); } if (!flvjs.isSupported()) { alert(当前浏览器不支持 flv.js); return; } player flvjs.createPlayer({ type: flv, isLive: true, url }, { enableStashBuffer: false }); player.attachMediaElement(videoRef.value); player.load(); player.play(); } onBeforeUnmount(() { player player.destroy(); }); /script4.2 移动端 H5 的兜底HLSflv.js 的兼容性在 Chrome、Edge、Firefox 上都很稳唯独 iOS Safari 和部分 Android 厂商浏览器没法用 MSE 处理 HTTP-FLV。遇到跨平台需求我常规做法是后端同时让 SRS 开启 HLS 输出前端用 hls.js 或者原生 HLS 播放。HLS 在 SRS 里几乎不需要额外配置生成的是这样的地址http://your-server:8080/live/cam_001.m3u8前端判断逻辑可以简单一点PC 上用 flv.js移动端优先走原生video标签的 HLS 播放能力。Safari 直接支持 HLSAndroid 上用 hls.js 兜底。function createPlayer(url) { const isIOS /iPhone|iPad|Mac/.test(navigator.platform) || (navigator.userAgent.includes(Mac) ontouchend in document); if (isIOS) { const video document.getElementById(video); video.src url.replace(.flv, .m3u8); video.play(); return; } // 否则 flv.js }4.3 必须说的 WebRTC 选项如果你的项目对延迟要求更高比如远程控制、在线对讲HTTP-FLV 1-3 秒仍然不够那就要考虑 WebRTC。SRS 4.0 以上已经支持 WebRTC 分发可以直接拉 RTSP 转 WebRTC前端通过RTCPeerConnection拿流。但 WebRTC 的坑在于信令服务和客户端的 ICE 配置不是一段代码能跑起来的这里不展开。我只建议把 WebRTC 当作延迟敏感型功能的进阶方案能不用就不用毕竟项目交付的稳定性优先级大于极致延迟。5. 真实项目里更值钱的部分兼容性、重连与性能调优代码能跑起来只是起点。下面这几类问题是我在每个摄像头流项目里几乎都会遇到的提前写在这能少走点弯路。5.1 先验证 RTSP 地址再用 ffprobe 摸清设备底细很多同事拿到的“RTSP 地址”是厂商或者施工人员随手给的格式五花八门。海康常见的格式是rtsp://admin:password192.168.1.64:554/Streaming/Channels/101大华类似的格式是rtsp://admin:password192.168.1.64:554/cam/realmonitor?channel1subtype0其中/Streaming/Channels/101里的101通常是主码流102是子码流。大华subtype0也是主码流subtype1是子码流。地址不对后面所有操作都是白费所以第一步建议先在服务器上用 ffprobe 验证ffprobe -rtsp_transport tcp -i rtsp://admin:password192.168.1.64:554/Streaming/Channels/101ffprobe 会打印出流的编码、分辨率、音频参数。知道了这些你才知道该让 JavaCV 按什么宽度高度去初始化 recorder。另外注意密码里如果包含、:、/这些特殊字符一定要做 URL 编码否则 RTSP 解析器会断错位置导致认证失败。5.2 主码流还是子码流性能与画质的平衡摄像头通常有两路码流主码流分辨率高、码率高适合事后查证子码流分辨率低、码率低适合实时预览。做实时播放时如果设备数量不多、服务器 CPU 充足可以直接取主码流如果一路 4K 都要在服务端解码再转 H.264CPU 压力会很大我遇到的实际场景里4K 转码单路就要吃掉不少 CPU 核。处理办法很简单实时预览默认用子码流关键画面“回放原始录像”时再切主码流。这在业务设计上要前置而不是等性能报警了再改。编码格式上尽量在摄像头后台把编码格式设为 H.264。如果设备强制输出 H.265而前端又要 H.264JavaCV 转码是兜得住但那就是纯 CPU 转码成本很高。有 NVIDIA 显卡的服务器可以试试把编码器切到硬件recorder.setVideoCodecName(h264_nvenc); recorder.setVideoOption(preset, p1);注意setVideoCodecName和setVideoCodec是两种设置方式不要混用。硬件编码能大幅降低 CPU 占用但部署机器必须带对应显卡云服务器一般要选 GPU 实例成本自己权衡。5.3 断线重连核心循环的异常处理摄像头设备不像应用服务器那么稳定断电、重启、网络波动都很常见。如果转推任务断开后不重连前端画面就是一把黑屏用户只能刷新。前面给的任务骨架里有一个while (running)外层循环配合Thread.sleep(3000)就是断线重连的基本实现。另外要小心一种情况摄像头重启后分辨率、帧率可能发生变化旧recorder还在用原来的宽高FFmpeg 会报Width or height change之类的错误。遇到这种情况常规做法是捕获异常后把旧的 recorder 关掉重新从 grabber 读取新的宽高再创建 recorder。我在代码里推荐的做法是每次异常退回到外层重连逻辑而不是在循环里尝试“修复”这样状态管理最简单。5.4 首屏黑屏和延迟调优一个容易被忽略的源头首屏打开慢经常不是网络问题而是 GOP 问题。摄像头端的关键帧间隔GOP如果设置是 4 秒那新播放器接入时必须等到下一个关键帧才开始有画面平均黑屏 2 秒。这个问题有两个解法调小摄像头的 I 帧间隔比如 2 秒一次在流媒体服务器上开启 GOP 缓存。SRS 里 HTTP-FLV 域默认会缓存最近一个 GOP新用户接入时直接用缓存的关键帧起播黑屏时间能降到几百毫秒。代价是多占一点内存但监控场景这点开销非常值。延迟问题则要前后端配合。前端关掉 flv.js 的 stash 缓冲后端编码时用zerolatency参数如果中间还经过了 SRSSRS 的mrmerged-reduce算法默认开启一般不需要额外动。我在多路摄像头、全链路 HTTP-FLV、局域网环境下实测延迟普遍能稳定在 1 秒以内公网环境会到 1-2 秒已经足够大多数业务使用。5.5 部署形态Java 和流媒体服务器怎么摆都行但边界要清晰最后一句话给个实在建议别把 Java 服务里塞满流媒体分发逻辑。我的部署习惯是一台服务器上同时跑 Java 服务和 SRSDocker 起Java 只负责拉流、转码、推 RTMP 给 SRSSRS 对外提供 HTTP-FLV/HLS。这样排查问题时有非常清晰的边界前端放不出画面先试 SRS 的 HTTP-FLV 地址能不能直接播放能播说明问题在前端不能播就看 SRS 日志再不行看 Java 日志五秒钟就能定位问题出在哪一段。我自己也是踩过几次坑才坚定这个分层思路的。第一次做类似系统图省事把 HTTP-FLV 直接写在 Java 里结果同时在线十几路时频繁出现连接挂起和内存上涨天天被运维催着看堆栈。后来换成 SRS 做分发Java 端代码反而更简单了稳定性还明显提升。所以说选型阶段不用迷信“All in Java”该交给专业组件的就交出去Java 把业务逻辑这块做好做扎实这个项目基本就成了一半。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表