ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

RTSP流视频网页播放方案:从协议转换到flv.js接入

RTSP流视频网页播放方案:从协议转换到flv.js接入 简介面向前端与音视频开发的实战资料解决浏览器原生不支持 RTSP 协议导致无法直接播放实时视频流的问题。整体方案以 Streamedian 转码服务为切入点同时覆盖 WebRTC、HLS 等前端接入方式适合希望实现监控画面、在线课堂或实时视频网页播放的开发者。压缩包共 14 个文件约 403KB包含 HTML 页面、CSS 样式、JavaScript 播放器脚本、XML 配置及 PNG 图标等结构紧凑便于对照学习或嵌入现有项目。包内提供 Streamedian 集成文件、免费播放器脚本以及 H.265 相关解码脚本可辅助理解 RTSP 会话控制、流协议转换、浏览器端播放器封装和视频解码等关键环节也能从示例页面中观察播放器事件绑定与状态切换的实现细节。已有 6845 人浏览学习对于刚接触流媒体前端化的读者是一份能快速上手的参考既能看到整体架构设计也能通过实际代码走通从拉流到页面播放的完整链路。1. rTSP 流视频在浏览器里不能直接播放问题出在协议会话浏览器里的video只认 HTTP 分发之上的封装格式rTSP 是另一套带会话状态的实时传输协议。前端拿video srcrtsp://...打开通常只会得到“没有支持的来源”这不是代码写错而是协议栈根本不贴。要让 rtsp 流视频实现网页播放常规做法是“服务端拉流、转封装、再转发”让浏览器只面对 HTTP 流。很多团队会先去找“支持 rtsp 的播放器插件”但插件在新版浏览器里基本被禁用浏览器厂商也不会内置 rTSP 客户端。更稳的路径是把转换放在服务端一边维持 rTSP 会话一边把 RTP 包重封装成 HTTP-FLV、HLS 或 WebRTC 媒体。这样做还能顺带解决鉴权、多路并发和录像回放。这套方法适合前端和后端共同配合的团队前端负责播放器和交互后端负责拉流进程的维护与转发。下面按选型、最小实现、稳定性、摄像头接入和验证的顺序展开。2. rTSP 转 WebRTC、HTTP-FLV、HLS 前要先分清会话与文件流2.1 rTSP 拉流协议建立的是有状态会话不是普通文件rTSP 拉流协议和 HTTP 文件下载最大的不同是“会话状态”。摄像头对 DESCRIBE、SETUP、PLAY 请求逐个响应然后持续输出 RTP 包。这个会话里有序列号、时间戳和 keepalive 机制任何一个环节断开画面就会停住。浏览器里的video没有能力发起和维护这种会话所以常见做法是让后端先当一个 rTSP 客户端把拿到的 RTP 包重新封装成浏览器可以连续读取的文件流。封装方式可以分成两类不转码只转封装以及完全转码。不转码的意思是保留 H.264/AAC 原始编码只把 RTP 载荷重排成 FLV tag 或 fMP4 分片CPU 开销很低。完全转码则用 FFmpeg 或 GStreamer 解码后再编码适合老摄像头只输出 MJPEG、HEVC 等浏览器支持不稳定的格式。选型顺序一般是先确认摄像头子码流是什么编码再决定要不要加转码只要输出是 H.264优先走转封装延迟和资源都占优。2.2 三种分发方式延迟、兼容性和服务端成本的取舍分发方式端到端延迟浏览器兼容服务端成本适合场景HTTP-FLV flv.js1 到 3 秒Chrome/Firefox/Edge 等支持 MSE 的浏览器低FFmpeg 拉流加 HTTP 分发即可安防实时预览、NVR 网页端WebRTC200 到 500 毫秒浏览器原生支持移动端也稳定高需要信令服务和媒体协商巡检、遥控、对延迟敏感的业务HLS5 到 15 秒全平台iOS 上体验最好中需要切片和文件服务公网分享、回放、弱网场景实际交付里我不建议一上来就选 WebRTC。rTSP 转 WebRTC 虽然能拿到最低延迟但要处理信令、ICE、媒体协商和带宽估计服务端通常要引入 Janus、medooze或者用 GStreamer 的 RTSP Server 配合 WebRTC 分支。如果项目只是“把门口几个摄像头放到网页上”HTTP-FLV 已经够了。只有客户明确要求毫秒级操作反馈才值得为 WebRTC 的复杂度买单。HLS 则适合回放和公网分发但直播时切片造成的延迟没法压缩不推荐用于实时预览。2.3 一条 FFmpeg 命令同时输出 FLV 和 HLS 的混合分发很多项目内网走 HTTP-FLV公网走 HLS。FFmpeg 可以在一行命令里同时输出两种格式省掉重复拉流# 从 rTSP 拉流同时输出 HTTP-FLV 和 HLS 分片 ffmpeg -rtsp_transport tcp -i rtsp://user:pass192.168.1.10:554/Streaming/Channels/101 \ -c:v copy -c:a copy -f flv rtmp://127.0.0.1:1935/live/cam01 \ -c:v copy -c:a copy -f hls -hls_time 2 -hls_list_size 5 /var/www/live/cam01.m3u8参数说明-rtsp_transport tcp强制拉流传输入走 TCP避免 UDP 丢包导致花屏两个输出段都用了-c:v copy -c:a copy表示只换封装不重新编码。HLS 输出里的-hls_time 2是每个分片时长 2 秒-hls_list_size 5保留 5 个分片适合直播形态。注意这里-c:a copy的前提是摄像头音频编码是 AAC如果摄像头推的是 G.711/PCM浏览器兼容性会很差需要把音频转成 AAC。转码开销不大但要放到命令里避免画面出了却没声音。3. 最小链路FFmpeg 拉 rTSP 推 HTTP-FLV前端用 flv.js 接入3.1 先跑通一条 FFmpeg 命令行先把 rtsp 转 flv 的最小链路在命令行跑通再写前端。下面命令假设摄像头输出 H.264本地已经装了 FFmpeg# rTSP 转封装推到 RTMP再由流媒体服务器暴露 HTTP-FLV ffmpeg -rtsp_transport tcp \ -i rtsp://192.168.1.64:554/Streaming/Channels/101 \ -c:v copy -c:a copy \ -f flv rtmp://127.0.0.1:1935/live/cam01这段命令的意思是FFmpeg 作为 rTSP 客户端与摄像头建立会话把收到的流重封装成 FLV推给本机 RTMP 端口。SRS、Nginx-RTMP、ZLMediaKit 都可以接这路 RTMP再用自己的 HTTP-FLV 地址对外提供访问。如果摄像头地址带了账号密码直接在 URL 里写user:passip即可但生产环境不建议长期这么暴露后面第五章讲怎么收敛。3.2 用 Node.js 做一个极简转发服务不装流媒体服务器时也可以让 FFmpeg 把 FLV 写到 stdoutNode.js 在收到浏览器请求时把 stdout 管道到 response。这个方式只适合验证链路不适合多客户端并发但足够让人看清“拉流转发”是怎么发生的。const { spawn } require(node:child_process); const http require(node:http); // 拉流进程rTSP 源作为参数输出到 stdout const ffmpeg spawn(ffmpeg, [ -rtsp_transport, tcp, -i, rtsp://192.168.1.64:554/Streaming/Channels/101, -c:v, copy, -c:a, copy, -f, flv, pipe:1 ]); http.createServer((req, res) { res.writeHead(200, { Content-Type: video/x-flv, Cache-Control: no-cache, Access-Control-Allow-Origin: * }); ffmpeg.stdout.pipe(res); // 浏览器关闭页面时结束拉流避免空跑 req.on(close, () ffmpeg.kill(SIGTERM)); }).listen(8080);逻辑说明spawn启动 FFmpegpipe:1让 FFmpeg 把生成的数据写进 stdoutHTTP 请求进来时用pipe(res)把它直接转发给浏览器。Content-Type必须是video/x-flv否则播放器不会按 FLV 解析。req.on(close)用来释放拉流进程不然页面关掉后摄像头资源还被占用。这段代码只能处理一个播放端两个请求会互相抢同一段 stdout所以生产环境必须换用支持多客户端复用的流媒体服务器。3.3 前端最小播放器初始化script src/libs/flv.min.js/script video idplayer controls muted autoplay width1280 height720/video script const video document.getElementById(player); if (flvjs.isSupported()) { const flvPlayer flvjs.createPlayer({ type: flv, isLive: true, url: http://127.0.0.1:8080/live/cam01.flv }, { // 低延迟优先关闭缓冲堆积 enableStashBuffer: false, enableWorker: true, autoCleanupSourceBuffer: true }); flvPlayer.attachMediaElement(video); flvPlayer.load(); flvPlayer.play(); } /script配置解释isLive: true让 flv.js 以直播模式处理流不做时长和 seek 假设enableStashBuffer: false关闭数据预堆积延迟会变小但网络抖动时更容易卡enableWorker: true把解封装任务放到 Web Worker主线程不会因为频繁解析 FLV 卡顿autoCleanupSourceBuffer负责清理长时间直播时的内存缓存。播放器标签上保留muted多数浏览器对带声音自动播放有限制静音可以绕过首帧限制。3.4 播放异常时先查编码和参数故障现象检查点常见结论画面一直转圈摄像头 GOP 间隔和拉流进程状态I 帧间隔过长首屏最坏要等一个 GOP有声音没画面视频编码是否为 HEVCflv.js 对 H.265 支持有限需转 H.264延迟越拉越大flv.js 的 enableStashBuffer开启后播放器会主动堆积缓存延迟增加花屏或马赛克rTSP 传输是否为 UDP改用-rtsp_transport tcp排查顺序先看 FFprobe 输出里的codec_name。h264可以直接 copyhevc需要转码。转码推荐加-c:v libx264 -preset veryfast -profile:v baseline -level 3.0兼容性最好但码率和清晰度要重新观察。新版 flv.js 对 HEVC 的支持依赖浏览器视频扩展Windows 上还要先安装对应解码器这些隐性依赖很容易把问题拖成“换一台电脑就能播”的局面。4. 延迟和卡顿的根因往往在 TCP 拉流、GOP 和 rTSP 重连4.1 传输层先定 TCP再用 OpenCvSharp 或 FFmpeg 验证rTSP 默认的 RTP 媒体面走 UDPUDP 丢包不重传WiFi 下偶尔几个包丢失就会在画面里留下色块。把拉流侧换成 TCP 能解决大部分花屏问题代价是延迟略高一点但实时预览场景可以接受。FFmpeg 侧就是开头一直用的-rtsp_transport tcp。使用 OpenCvSharp 时设置方式如下using OpenCvSharp; var capture new VideoCapture(); // 传输层设为 TCP0 对应 TCP1 对应 UDP capture.Set(VideoCaptureProperties.RtspTransport, 0); capture.Open(rtsp://192.168.1.64:554/Streaming/Channels/101);参数说明RtspTransport对应 OpenCV 的CAP_PROP_RTSP_TRANSPORT0是 TCP1是 UDP。这个配置只影响拉流的传输方式不改变 rTSP 地址本身。很多播放卡顿问题第一排查对象应该是传输层而不是前端缓冲前端缓冲调得再大也补不了已经丢掉的 RTP 包。确认摄像头到服务器这一段稳定后再去动播放器参数才有意义。4.2 GOP 间隔决定首屏耗时和倍速拖动的颗粒度网页播放 rstp 流时播放器必须拿到一个关键帧才能把画面亮出来。摄像头默认 GOP 如果是 2 秒那么最坏情况下用户要等 2 秒如果把 GOP 拉到 4 秒首屏最坏 4 秒用户会反复刷新。安防摄像头一般可以在管理端设置“I 帧间隔”实时预览建议设 1 到 2 秒。转码时也可以强制 GOP# 强制每 30 帧一个关键帧避免转码后首屏过慢 ffmpeg -rtsp_transport tcp -i rtsp://192.168.1.64:554/Streaming/Channels/101 \ -c:v libx264 -preset fast -g 30 -sc_threshold 0 \ -c:a copy -f flv rtmp://127.0.0.1:1935/live/cam01-g 30表示每 30 帧插入一个关键帧按 25fps 算就是 1.2 秒一个-sc_threshold 0关闭场景切换自动关键帧让 GOP 分布均匀。这个参数同时也影响倍速播放网页视频加速播放时播放器需要更快地找到新的关键帧GOP 越短快进或倍速后的画面恢复越快。但-g太小会提升码率得不偿失。4.3 rTSP 重连的两种落地方式摄像头断电重启、网络交换机升级都会中断 rTSP 会话。FFmpeg 进程此时会直接退出不会有任何自动恢复。最简单的兜底是用 shell 循环#!/bin/bash while true; do # 摄像头复位后等待 3 秒再拉起新的拉流进程 ffmpeg -rtsp_transport tcp \ -i rtsp://user:pass192.168.1.64:554/Streaming/Channels/101 \ -c:v copy -c:a copy -f flv rtmp://127.0.0.1:1935/live/cam01 sleep 3 done这个方案解决“进程退出后没人拉起”的问题但解决不了“拉流进程还在画面已死的假活状态”。生产环境我一般会再叠一个健康检查用timeout 10 ffprobe定时探测 rTSP 源超时或返回错误就杀掉旧进程并触发重启。timeout命令确保单个探测不会卡死真正检测失败时不要用|| true掩盖返回值而要把它作为重启信号。另外同一个摄像头不要被多个拉流进程同时拉着很多摄像机对并发会话数有限制超过两路会拒绝新的 PLAY 请求。重连方案优点局限shell 循环实现简单重启快无法处理进程假活systemd Restartalways跟随服务生命周期同样不检查流是否真的在出帧定时 ffprobe 健康检查能查出假活要额外代码处理状态机5. 摄像头到网页服务端的接入层取流地址、鉴权和并发控制5.1 大华和海康的 rTSP 取流地址格式把摄像头接入服务端时第一件事是拿到正确的 rTSP 取流地址。不同厂商规则差异很大同一厂商不同固件也有区别。品牌主码流示例子码流示例大华rtsp://user:passip:554/cam/realmonitor?channel1subtype0...?channel1subtype1海康rtsp://user:passip:554/Streaming/Channels/101rtsp://user:passip:554/Streaming/Channels/102大华的subtype0是主码流subtype1是子码流海康的101是通道 1 主码流102是同一通道的子码流。网页预览我一般先用子码流因为分辨率小、码率低首屏更快用户点“高清”再切主码流。如果前端直接请求主码流一个页面同时看 16 路时带宽会非常难看。这里顺带说明取流地址里如果有?和在代码里拼 URL 时别忘记做 URL 编码密码里的和#会直接截断地址。5.2 鉴权和取流地址不要出现在前端前端播放 rtsp 直连地址不仅是兼容性问题也是安全漏洞。rtsp://user:passip:554/...一旦出现在 HTML 或 JavaScript 里等于把摄像头账号公开给了所有看过页面的人。网页播放场景的正确姿势是前端只拿一个业务侧生成的短期播放凭证后端根据凭证查到摄像头真实地址并启动转发。// 业务侧只返回可访问的 HTTP-FLV 地址暴露真实的 rTSP 地址给后端 const resp await fetch(/api/stream/play?cidcam01tokenxxx); const { flvUrl } await resp.json(); const flvPlayer flvjs.createPlayer({ type: flv, isLive: true, url: flvUrl });在这段逻辑里flvUrl指向http://media-server/live/cam01.flv?tokenxxx而真实的192.168.1.64:554/Streaming/Channels/101只存在后端配置中心。流媒体服务器收到带 token 的请求后再向后端确认权限。摄像头侧也可以做一层白名单只允许流媒体服务器所在网段访问自己的 554 端口。这样即使 rTSP 地址泄露外部也连不通摄像头。5.3 并发控制同一路摄像头只保留一个拉流进程多路摄像头接入时最忌讳的方案是“每个页面请求都开一条 FFmpeg”。同一路视频被 10 个人看就开 10 个拉流进程摄像头会被打爆服务器也会很快占满文件描述符。合理做法是按流 ID 复用进程const activeStreams new Map(); function startIfNeeded(cameraId, rtspUrl) { // 同一路摄像头已经拉起进程就直接复用 if (activeStreams.has(cameraId)) return; const proc spawn(ffmpeg, [ -rtsp_transport, tcp, -i, rtspUrl, -c:v, copy, -c:a, copy, -f, flv, rtmp://127.0.0.1:1935/live/ cameraId ]); activeStreams.set(cameraId, proc); proc.on(exit, () activeStreams.delete(cameraId)); }这里activeStreams用摄像头 ID 做键确保一路流只有一个上游进程进程因为断流退出时exit事件会把 Map 里的键删掉让下次请求能重新拉起。这个简化版没有引用计数适合内部预览。如果同一路要被大量用户同时看最好直接把 SRS 或 ZLMediaKit 放在前面让流媒体服务器做“一路上游、多路下游”的回源复用不要再自己在 Node.js 里管理复杂状态。5.4 rTSP 实时预览和缓存回放要分开走网页播放里另一个容易混在一起的是实时流和回放。实时流要求低延迟任何多余的缓冲层都会让画面越来越慢回放则需要先把流落盘再用 HLS 或 MP4 提供访问。常见的落地方式是服务端用 FFmpeg 周期性录制 HLS 分片用户在回放页面直接读.m3u8而实时预览页继续走 HTTP-FLV。两个链路共用同一个 rTSP 地址没问题但输出、端口、缓存策略都要分开。安卓端缓存 rtsp 流这个需求通常也不在网页里解决。原生 App 可以把 rTSP 流分段存到本地再播放但网页端只能控制 flv.js 的enableStashBuffer。要低延迟就关掉要稳定就打开这个选项和录像缓存是两回事别指望它能替代服务端录像。6. 延迟、首帧和倍速的验证动作自检播放链路的最后几步6.1 先量源端延迟再谈播放器卡顿遇到“比现场慢好几秒”的反馈先不要动播放器用 ffprobe 量一下 rTSP 源本身# 读取 rTSP 流的起始时间作为源端延迟基线 ffprobe -v error -show_entries formatstart_time -of csvp0 \ -rtsp_transport tcp -i rtsp://user:pass192.168.1.64:554/Streaming/Channels/101拿到的start_time是 rTSP 会话建立到第一个媒体包的时间。如果这个值很小说明慢在后端转发或前端缓冲如果值偏大再回头查摄像头编码和网络。比较准确的方式是播放器当前时间和现场时间比普通会议室摄像头看秒针已经够用。6.2 用 flv.js 的统计事件切分问题阶段判断瓶颈在拉流还是解码可以监听flvjs.Events.STATISTICS_INFOcurrentBuffer持续为 0说明数据没有到播放器问题在拉流和转发droppedFrames快速上涨说明解码跟不上可能是视频编码级别太高或机器性能不足。检查顺序是浏览器网络请求是否返回video/x-flvFFmpeg 日志是否持续输出非零帧flv.js 是否收到数据。三步法可以避免每次一卡就去调摄像头。6.3 用 playbackRate 验证倍速播放下的稳定性// 网页视频加速播放时保持音调不变、丢帧由解码器决定 const video document.getElementById(player); video.playbackRate 2.0; video.preservesPitch false;在直播链路里倍速播放意味着播放器本地会丢帧显示但 FLV 的接收和解复用仍然是全帧率CPU 消耗不会因为倍速减少。验证时连续 30 秒记录requestVideoFrameCallback的时间戳看帧间隔是否稳定在 40ms 附近如果抖动超过 5ms优先把拉流端切到 TCP 再看一遍绝大多数倍速卡顿在这时已经消失。本文还有配套的精品资源点击获取
返回列表