)
摄像头视频预览实现教程从原理到落地(ZLMediaKit flv.js Spring Boot)适用场景把摄像头的 RTSP 流在浏览器里实时预览无插件、低延迟。技术栈ZLMediaKit流媒体代理 flv.js前端播放 Spring Boot后端桥接。一、核心知识点先搞清楚为什么1.1 浏览器为什么不能直接播摄像头摄像头海康 / 大华 / 宇视等输出的是RTSP流而浏览器原生支持的是HTTP / HTTPS / WebSocket并不支持 RTSP 协议也没有内置的 RTSP 解码器。因此必须有一层服务端协议转换摄像头 RTSP ──(服务端转封装)──▶ 浏览器能播的格式(HTTP-FLV/HLS/WebRTC)关键点转的是封装/协议不是画面内容。只要流本身是 H.264/H.265服务器只做重新打包几乎不消耗 CPU转封装 vs 转码的区别。1.2 三种浏览器播放方案对比方案延迟浏览器兼容部署复杂度适用HTTP-FLV本文方案1~3sChrome/Edge/Firefox需 flv.js低单 exe监控大屏、常规预览HLS5~10s全平台原生低直播回放、对延迟不敏感WebRTC1s现代浏览器中需 NAT/ICE实时联动、低延迟场景结论监控预览优先 HTTP-FLV实时联动如跟车随动再升级 WebRTC二者可共用同一套 ZLMediaKit。1.3 为什么后端要参与而不是前端直连 ZLMediaKit鉴权flv 播放地址若直出容易被盗链由后端按权限发地址 token 更可控。解耦摄像头 RTSP 地址 / 账号密码只存后端不下发前端。按需拉流控制打开预览才建流关闭才释放节省摄像头连接数与带宽。后端职责很轻只通知流媒体服务器拉流 “返回播放地址”不参与任何转码。1.4 关键概念速查拉流代理addStreamProxy让 ZLMediaKit 主动去拉一个外部 RTSP/RTMP 源并对外提供播放地址。HTTP-FLV基于 HTTP 长连接的 FLV 流flv.js 边下边播适合直播。按需拉流无人观看时自动断流streamNoneReaderDelayMS控制等待时长。流名stream每路视频的唯一 ID建议用业务主键如设备 ID保证唯一。二、架构设计2.1 整体架构图┌──────────────┐ RTSP拉流 ┌──────────────────────┐ HTTP-FLV ┌──────────────┐ │ 摄像头 / 视频源│ ───────────→ │ ZLMediaKit(流媒体服务) │ ─────────→ │ 浏览器 flv.js │ │ rtsp://... │ │ MediaServer │ │ 实时预览 │ └──────────────┘ │ 拉流代理 addStreamProxy│ └──────────────┘ └──────────┬───────────┘ │ HTTP API被调用 ┌──────────┴───────────┐ │ 后端Spring Boot │ │ ① 接收前端开播请求 │ │ ② 调 ZLM 建流/关流 │ │ ③ 返回 flv 播放地址 │ └────────────────────────┘2.2 分层职责层职责不做什么摄像头 / 视频源产出 RTSP 流不管浏览器、不管转协议ZLMediaKit拉 RTSP → 转 HTTP-FLV提供管理 API不碰业务、不鉴权业务用户后端接收前端请求 → 调 ZLM API 建/关流 → 返地址不转码、不存视频前端拿地址用 flv.js 播放关闭时通知释放不直接持有 RTSP/密钥2.3 关键设计决策后端只建流不转码转封装由 ZLMediaKit 承担Java 无计算压力。host 与 play-host 分离重要host后端调 ZLM API 用的地址内网 IP。play-host返回给浏览器的播放地址跨网 / 域名访问时填公网可达地址。二者可不同解决后端在内网、浏览器走域名的网络差异。按需拉流 显式释放打开预览建流关闭弹窗显式closeStream同时依赖streamNoneReaderDelayMS兜底无人观看自动断流。幂等建流流已存在时addStreamProxy返回非 0 仍可忽略照常返回播放地址避免重复点击报错。子码流优先多路同屏用子码流如海康102单画面放大再切主码流101降低带宽与服务器压力。三、实现流程端到端步骤步骤动作产物1部署流媒体服务器 ZLMediaKitWindows 单 exe / Linux 进程可在:80/index/api/getServerConfig拿到 JSON2准备视频源 RTSP 地址真实摄像头 或 本地模拟一个可被拉取的 RTSP URL3后端配zlm.* 写建流服务StreamProxyService 暴露 Controlleropen/close两个接口4前端引入 flv.js 写通用播放组件VideoPlayer传入 URL 即可播放5联调前端拿地址 → flv.js 播放关闭 → 释放浏览器看到实时画面四、通用示例代码4.1 后端配置属性通用// ZlmProperties.java —— 绑定 application.yml 的 zlm 节点DataComponentConfigurationProperties(prefixzlm)publicclassZlmProperties{privateStringhost127.0.0.1;// 后端调 API 用的地址privateinthttpPort80;// 与 ZLM config.ini [http].port 一致privateStringplayHost127.0.0.1;// 返回给浏览器的播放地址privateStringsecret;// 与 ZLM [api].secret 一致privateStringapplive;// 流应用名}# application.ymlzlm:host:127.0.0.1http-port:80# 【本项实际值】本地模拟常改 8080 避开 IISplay-host:127.0.0.1secret:035c73f7-bb6b-4889-a715-d9eb2d1925cc# 【本项实际值】务必改成你自己的强随机串app:live// RestTemplateConfig.javaConfigurationpublicclassRestTemplateConfig{BeanpublicRestTemplaterestTemplate(){returnnewRestTemplate();}}4.2 后端通用拉流服务// StreamProxyService.java —— 通用给定 streamId rtspUrl返回 HTTP-FLV 地址ServicepublicclassStreamProxyService{privatestaticfinalLoggerlogLoggerFactory.getLogger(StreamProxyService.class);AutowiredprivateZlmPropertieszlm;AutowiredprivateRestTemplaterestTemplate;/** 打开一路流幂等拉起 RTSP 代理并返回 flv 地址 */publicStringopenStream(StringstreamId,StringrtspUrl){try{URIuriUriComponentsBuilder.fromHttpUrl(apiBase()/index/api/addStreamProxy).queryParam(secret,zlm.getSecret()).queryParam(vhost,__defaultVhost__).queryParam(app,zlm.getApp()).queryParam(stream,streamId).queryParam(url,rtspUrl).queryParam(rtp_type,0)// 0TCP拉流(抗丢包,推荐).build().encode().toUri();restTemplate.postForEntity(uri,null,String.class);}catch(Exceptione){// 流已存在/临时失败均不影响返回地址幂等log.warn(addStreamProxy 异常{},e.getMessage());}returnflvUrl(streamId);}/** 关闭一路流释放资源。streamKey 格式为 app/stream不含 .live */publicvoidcloseStream(StringstreamId){StringstreamKeyzlm.getApp()/streamId;try{URIuriUriComponentsBuilder.fromHttpUrl(apiBase()/index/api/delStreamProxy).queryParam(secret,zlm.getSecret()).queryParam(streamKey,streamKey).build().toUri();restTemplate.getForEntity(uri,String.class);}catch(Exceptione){log.warn(delStreamProxy 异常{},e.getMessage());}}privateStringapiBase(){returnhttp://zlm.getHost():zlm.getHttpPort();}privateStringflvUrl(StringstreamId){returnhttp://zlm.getPlayHost():zlm.getHttpPort()/zlm.getApp()/streamId.flv;}}注意delStreamProxy的streamKey参数名ZLMediaKit 文档有时写key本封装用streamKey与本项目后端一致。两者命中其一即可务必前后统一。4.3 后端通用 Controller只收 streamId rtspUrl// StreamController.java —— 通用入口业务系统自行加 PreAuthorize 鉴权RestControllerRequestMapping(/api/stream)publicclassStreamController{AutowiredprivateStreamProxyServicestreamProxyService;/** 开播返回浏览器可播放的 flv 地址 */GetMapping(/open)publicMapString,Objectopen(RequestParamStringstreamId,RequestParamStringrtspUrl){StringflvUrlstreamProxyService.openStream(streamId,rtspUrl);returnMap.of(code,0,flvUrl,flvUrl);}/** 关播释放流媒体资源 */GetMapping(/close)publicMapString,Objectclose(RequestParamStringstreamId){streamProxyService.closeStream(streamId);returnMap.of(code,0,msg,closed);}}业务落地时通常把rtspUrl的来源从前端参数改为后端按业务主键查数据库避免把摄像头地址暴露给前端更安全。本文为通用演示才允许前端传。4.4 前端通用播放组件业务无关只吃一个 URL!-- VideoPlayer.vue —— 传入 flvUrl 即可播放关闭自动销毁 -- template div classvideo-wrap video refvideoEl classvideo-el controls autoplay muted playsinline/video div v-iferrorMsg classvideo-error{{ errorMsg }}/div /div /template script import flvjs from flv.js export default { name: VideoPlayer, props: { flvUrl: { type: String, default: } }, data() { return { player: null, errorMsg: , retry: 0, maxRetry: 2 } }, watch: { flvUrl: { immediate: true, handler(url) { if (url) this.$nextTick(() this.play(url)) } } }, methods: { play(url) { if (!flvjs.isSupported()) { this.errorMsg 当前浏览器不支持 flv.js请使用 Chrome / Edge / Firefox return } this.destroy() const video this.$refs.videoEl const player flvjs.createPlayer({ type: flv, isLive: true, url }) this.player player player.attachMediaElement(video) player.load(); player.play() player.on(flvjs.Events.ERROR, (t, d) { console.error(flv error, t, d) if (this.retry this.maxRetry) { this.retry; this.$nextTick(() this.play(url)) } else this.errorMsg 视频加载失败请检查流媒体服务与视频源 }) }, destroy() { if (this.player) { try { this.player.pause(); this.player.unload(); this.player.detachMediaElement(); this.player.destroy() } catch (e) {} this.player null } } }, beforeDestroy() { this.destroy() } } /script style scoped .video-wrap { position: relative; width: 100%; background: #000; } .video-el { display: block; width: 100%; height: 500px; object-fit: contain; background: #000; } .video-error { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; color: #fff; text-align: center; } /style4.5 前端调用示例拿地址 → 传组件// api/stream.jsexportfunctionopenStream(streamId,rtspUrl){returnrequest({url:/api/stream/open,method:get,params:{streamId,rtspUrl}})}exportfunctioncloseStream(streamId){returnrequest({url:/api/stream/close,method:get,params:{streamId}})}!-- 业务页面里接入示例点按钮预览、关闭释放 -- template div el-button clickpreview视频预览/el-button VideoPlayer v-ifurl :flv-urlurl / /div /template script import VideoPlayer from /components/VideoPlayer import { openStream, closeStream } from /api/stream export default { components: { VideoPlayer }, data() { return { url: , id: cam_1 } }, methods: { async preview() { const res await openStream(this.id, rtsp://user:pwd192.168.1.64:554/Streaming/Channels/102) this.url res.flvUrl } }, beforeDestroy() { if (this.id) closeStream(this.id) } } /script4.6 海康 RTSP 地址格式备忘rtsp://用户:密码IP:554/Streaming/Channels/通道码流 └ 01主码流(高清) 02子码流(流畅多路预览推荐) 示例rtsp://admin:hk123456192.168.1.64:554/Streaming/Channels/102测试效果五、模拟调试流程无真实摄像头目标在没有摄像头硬件时用FFmpeg假摄像头推流 MediaMTXRTSP 源服务器 ZLMediaKit转 FLV把整条链路跑通。5.1 准备工具工具作用说明FFmpeg把本地test.mp4循环推成 RTSP模拟摄像头Windows 选 gyan.dev 构建解压得bin/ffmpeg.exeMediaMTX原 rtsp-simple-server极简 RTSP 服务器接收推流并对外提供 RTSP 源下载单文件mediamtx.exeZLMediaKit把 RTSP 转 HTTP-FLV解压得MediaServer.execonfig.initest.mp4被循环推送的画面任意几秒短片H264AAC 最稳建议统一目录D:\tools\ ├─ ffmpeg\bin\ffmpeg.exe ├─ mediamtx\mediamtx.exe └─ ZLMediaKit\MediaServer.exe config.ini5.2 配置模拟用# ZLMediaKit/config.ini 关键两项 [api] secret035c73f7-bb6b-4889-a715-d9eb2d1925cc # 与 application.yml 一致 [http] port8080 # 本地建议 8080避开 Windows 80 端口(IIS)# application.ymlzlm:host:127.0.0.1http-port:8080play-host:127.0.0.1secret:035c73f7-bb6b-4889-a715-d9eb2d1925ccapp:liveMediaMTX 默认即可RTSP 监听8554流名cam1。5.3 一键启动 / 停止脚本:: start_sim.bat —— 依次拉起 mediamtx → ZLMediaKit → ffmpeg 推流按需改路径 echo off set MEDIAMTX_EXED:\tools\mediamtx\mediamtx.exe set ZLM_DIRD:\tools\ZLMediaKit set FFMPEG_EXED:\tools\ffmpeg\bin\ffmpeg.exe set TEST_VIDEOD:\tools\test.mp4 set RTSP_URLrtsp://127.0.0.1:8554/cam1 start mediamtx cmd /k %MEDIAMTX_EXE% timeout /t 3 /nobreak nul start ZLMediaKit cmd /k cd /d %ZLM_DIR% MediaServer.exe timeout /t 3 /nobreak nul start ffmpeg-push cmd /k %FFMPEG_EXE% -re -stream_loop -1 -i %TEST_VIDEO% -c copy -f rtsp %RTSP_URL%:: stop_sim.bat —— 关闭三个服务 taskkill /fi WINDOWTITLE eq mediamtx* /f taskkill /fi WINDOWTITLE eq ZLMediaKit* /f taskkill /fi WINDOWTITLE eq ffmpeg-push* /fFFmpeg 推流命令逐项ffmpeg -re -stream_loop -1 -i test.mp4 -c copy -f rtsp rtsp://127.0.0.1:8554/cam1 # -re 按原始帧率读取不加会瞬间推完 # -stream_loop -1 无限循环模拟 7x24 监控 # -c copy 不转码直接拷贝最省 CPU失败改 -c:v libx264 -c:a aac # -f rtsp 强制 RTSP 封装5.4 验证步骤运行start_sim.bat确认三个窗口有日志输出mediamtx 出现RTSP server listening on :8554ZLMediaKit 监听 8080。把视频源地址设为rtsp://127.0.0.1:8554/cam1真实项目中对应设备表的 RTSP 字段。前端点视频预览预期看到test.mp4循环画面 → 整条链路打通。关闭弹窗前端调用closeStream后端执行delStreamProxy释放。不依赖前端的接口验证分层排障用:: 1) 拉起 RTSP 代理 curl http://127.0.0.1:8080/index/api/addStreamProxy?secret035c73f7-bb6b-4889-a715-d9eb2d1925ccvhost__defaultVhost__applivestreamdev_1urlrtsp://127.0.0.1:8554/cam1rtp_type0 :: 2) 浏览器直接打开应能看到(或下载到)持续 flv 流 :: http://127.0.0.1:8080/live/dev_1.flv :: 3) 释放 curl http://127.0.0.1:8080/index/api/delStreamProxy?secret035c73f7-bb6b-4889-a715-d9eb2d1925ccstreamKeylive/dev_15.5 分层排障法定位问题在哪一层先确认源最底层ffprobe -rtsp_transport tcp rtsp://127.0.0.1:8554/cam1能否看到h264视频轨 → 验证 FFmpeg 推流 MediaMTX。再确认转换层ZLMediaKit 进程在不在、端口是否监听、/index/api/getServerConfig是否返回 JSON → 验证 ZLMediaKit。最后确认播放层浏览器 F12 看*.flv请求是否 200、flv.js 是否报错 → 验证前端。端口冲突优先查 80IIS和8554多个 MediaMTX 实例。secret 必须全链路一致config.ini [api].secretapplication.yml zlm.secret 后端调 API 携带值。六、常见问题速查现象原因 / 解决前端黑屏一直加载①F12 看 flv 请求是否 200 ②ZLM 是否真拉到 RTSP看日志/getMediaList③RTSP 账号密码/通道码流是否正确addStreamProxy 返回非 0流已存在幂等可忽略或 RTSP 不可达用 VLC 打开该 RTSP 验证摄像头侧80 端口起不来被 IIS/SVCHOST 占用 → 改config.ini [http].port8080同步改zlm.http-port延迟 3s关 flv.jsstashBuffer、用子码流、rtp_type0仍不够升级 WebRTC局域网能看外网看不到摄像头/ZLM 在内网需 Nginx 反代或端口映射画面卡住/不循环确认 FFmpeg 带-stream_loop -1test.mp4可解码-c copy失败改转码“Protocol not found”FFmpeg 构建不含 RTSP muxer换 gyan.dev 完整构建七、进阶与演进升级 WebRTC1s 延迟同一套 ZLMediaKit前端把flv.js换成 WebRTC 拉流ZLM/index/api/webrtc?applivestreamxxxtypeplay后端建流逻辑不变。适合实时联动跟车随动、见尘即喷。播放鉴权开启 ZLMon_playhook后端校验一次性 token用登录态换取防止 flv 地址被盗链。统一账号管理摄像头 RTSP 地址里的账号密码集中存后端配置/加密前端只传业务主键更安全。多路同屏大屏循环渲染多个VideoPlayer统一子码流、限制同屏 ≤ 4~9 路避免带宽/CPU 打满。