Skip to Content
本人正在找工作,有合适的岗位可以联系我,简历
博客海康威视多路监控视频流接入

海康威视多路监控视频流接入:从协议对接到播放器架构

“互联网+明厨亮灶”智慧监管平台需要对数千个摄像头进行实时巡查,核心技术挑战是:如何在一个 Web 页面内稳定播放多路高清监控流,同时保证性能不崩、操作流畅。这篇文章完整记录与海康威视平台的对接过程及播放器的架构设计。


1. 海康威视平台的接入方式

海康威视开放平台(HIKVISION OpenAPI)提供了多种视频流获取方式,针对 Web 端,我们使用的是 ISAPI + HLS/RTMP 转码 方案:

海康 NVR/DVR → 海康威视 VideoManagement Platform (VMS) → OpenAPI 获取预览 URL → 转码服务器(RTSP → HLS/FLV) → Web 播放器(Video.js)

1.1 获取实时预览 URL

// services/hikvision.ts interface CameraPreviewParams { cameraIndexCode: string // 摄像头唯一编码 streamType: 0 | 1 | 2 // 0=主码流, 1=子码流, 2=第三码流 protocol: 'rtsp' | 'hls' | 'flv' transmode: 0 | 1 // 0=UDP, 1=TCP(NAT 穿透推荐用 TCP) } async function getCameraPreviewUrl(params: CameraPreviewParams): Promise<string> { // 海康 API 要求签名鉴权(HMAC-SHA256) const headers = generateHikAuth({ appKey: HIKVISION_APP_KEY, appSecret: HIKVISION_APP_SECRET, path: '/api/video/v2/cameras/previewURLs', }) const response = await fetch(`${VMS_BASE_URL}/api/video/v2/cameras/previewURLs`, { method: 'POST', headers: { ...headers, 'Content-Type': 'application/json' }, body: JSON.stringify(params), }) const data = await response.json() if (data.code !== '0') { throw new Error(`海康 API 错误: ${data.msg}`) } return data.data.url // 返回 HLS m3u8 地址或 FLV 地址 }

1.2 协议选择的考量

经过测试,在政府内网环境下:

协议延迟兼容性选择
HLS5-15s最好,无需插件历史回放 ✓
HTTP-FLV1-3s需 flv.js实时监控 ✓
RTSP<1s浏览器不支持
WebRTC<500ms需专门服务器部分场景 ✓

实时监控选 HTTP-FLV,历史回放选 HLS 是我们最终的方案。


2. Video.js 多路播放器架构

2.1 核心播放器组件封装

// components/HikPlayer.tsx import videojs from 'video.js' import 'video.js/dist/video-js.css' import flvjs from 'flv.js' interface HikPlayerProps { cameraCode: string streamType?: 0 | 1 className?: string onError?: (err: Error) => void } export const HikPlayer = memo(({ cameraCode, streamType = 1, onError }: HikPlayerProps) => { const videoRef = useRef<HTMLVideoElement>(null) const playerRef = useRef<ReturnType<typeof flvjs.createPlayer> | null>(null) const [status, setStatus] = useState<'loading' | 'playing' | 'error'>('loading') const [errorCount, setErrorCount] = useState(0) const retryTimer = useRef<ReturnType<typeof setTimeout>>() const initPlayer = useCallback(async () => { if (!videoRef.current) return try { const url = await getCameraPreviewUrl({ cameraIndexCode: cameraCode, streamType, protocol: 'flv', transmode: 1, }) // 销毁旧实例 playerRef.current?.destroy() if (!flvjs.isSupported()) { throw new Error('当前浏览器不支持 FLV 播放') } const player = flvjs.createPlayer({ type: 'flv', url, isLive: true, }, { enableWorker: true, lazyLoadMaxDuration: 3 * 60, seekType: 'range', // 关键:限制缓冲区大小,防止内存持续增长 liveBufferLatencyChasing: true, liveBufferLatencyMaxLatency: 1.5, liveBufferLatencyMinRemain: 0.2, }) player.attachMediaElement(videoRef.current) player.load() player.on(flvjs.Events.ERROR, (errType, errDetail) => { console.error(`[HikPlayer] ${cameraCode} 错误:`, errType, errDetail) setStatus('error') scheduleRetry() }) player.on(flvjs.Events.STATISTICS_INFO, () => { if (status !== 'playing') setStatus('playing') }) playerRef.current = player await videoRef.current.play() } catch (err) { setStatus('error') onError?.(err as Error) scheduleRetry() } }, [cameraCode, streamType]) // 指数退避重连:1s, 2s, 4s, 8s, 最大 30s const scheduleRetry = useCallback(() => { clearTimeout(retryTimer.current) const delay = Math.min(1000 * Math.pow(2, errorCount), 30000) setErrorCount(c => c + 1) retryTimer.current = setTimeout(() => { setStatus('loading') initPlayer() }, delay) }, [errorCount, initPlayer]) useEffect(() => { initPlayer() return () => { clearTimeout(retryTimer.current) playerRef.current?.destroy() } }, [cameraCode]) return ( <div className="relative aspect-video bg-black"> <video ref={videoRef} muted className="w-full h-full" /> {status === 'loading' && <LoadingOverlay />} {status === 'error' && ( <ErrorOverlay message="连接中断,正在重连..." retryCount={errorCount} onManualRetry={() => { setErrorCount(0); initPlayer() }} /> )} </div> ) })

2.2 多路视频宫格布局管理

监管大屏需要同时展示 4/9/16 路视频,使用 CSS Grid 动态切换:

// components/VideoGrid.tsx type GridLayout = '1x1' | '2x2' | '3x3' | '4x4' const GRID_COLS: Record<GridLayout, number> = { '1x1': 1, '2x2': 2, '3x3': 3, '4x4': 4 } export function VideoGrid({ cameras, layout }: VideoGridProps) { const cols = GRID_COLS[layout] const visibleCameras = cameras.slice(0, cols * cols) return ( <div className="w-full h-full grid gap-1" style={{ gridTemplateColumns: `repeat(${cols}, 1fr)` }} > {visibleCameras.map(camera => ( <HikPlayer key={camera.code} cameraCode={camera.code} // 多路时强制使用子码流节省带宽 streamType={cols > 2 ? 1 : 0} /> ))} </div> ) }

3. 解决内存泄漏:长时间运行的关键

监管人员可能一屏连续监看 8+ 小时,FLV 流会持续在内存中积累缓冲数据,不加干预最终会导致标签页崩溃。

3.1 缓冲区主动清理

// 定时检查并清理过大的缓冲区 function setupBufferCleaner(player: flvjs.Player, videoEl: HTMLVideoElement) { const CLEAN_INTERVAL = 60 * 1000 // 每分钟检查一次 const MAX_BUFFER_AHEAD = 30 // 最多保留 30 秒的缓冲 const timer = setInterval(() => { const buffered = videoEl.buffered if (buffered.length === 0) return const currentTime = videoEl.currentTime const bufferEnd = buffered.end(buffered.length - 1) if (bufferEnd - currentTime > MAX_BUFFER_AHEAD) { // 直接跳到最新位置,触发浏览器释放旧缓冲 videoEl.currentTime = bufferEnd - 1 console.log(`[BufferCleaner] 清理缓冲区,跳转到 ${bufferEnd - 1}s`) } }, CLEAN_INTERVAL) return () => clearInterval(timer) }

3.2 页面不可见时暂停播放

利用 Intersection Observer,当播放器卡片滚动出可视区时自动暂停,回到可视区时恢复:

function useVisibilityPause(videoRef: RefObject<HTMLVideoElement>) { useEffect(() => { const el = videoRef.current if (!el) return const observer = new IntersectionObserver( ([entry]) => { if (entry.isIntersecting) { el.play().catch(() => {}) } else { el.pause() } }, { threshold: 0.3 } // 30% 可见时触发 ) observer.observe(el) return () => observer.disconnect() }, []) }

4. 历史回放的实现

历史回放使用 HLS 协议,通过时间段参数获取对应的 m3u8 切片:

async function getPlaybackUrl(params: { cameraCode: string startTime: string // ISO 8601: '2026-01-15T09:00:00+08:00' endTime: string }) { const response = await hikApi.post('/api/video/v2/recordsets/search', { cameraIndexCode: params.cameraCode, startTime: params.startTime, endTime: params.endTime, recordType: '0', // 0=所有录像 }) // 获取录像片段列表,拼接播放 URL const segments = response.data.list return segments.map(seg => ({ start: seg.startTime, end: seg.endTime, url: seg.playUrl, // HLS m3u8 地址 })) }

5. 踩坑记录

坑 1:海康 API 的时间格式
海康平台对时间格式极为严格,必须是 ISO 8601 带时区:2026-01-15T09:00:00+08:00,用 new Date().toISOString() 得到的 UTC 时间(Z结尾)会报错,坑了我半天。

坑 2:FLV 流在 Safari 上不可用
Safari 不支持 MSE(Media Source Extensions),flv.js 完全无法工作。针对 Safari 特判使用 HLS(延迟稍高但可接受):

const protocol = /^((?!chrome|android).)*safari/i.test(navigator.userAgent) ? 'hls' : 'flv'

坑 3:16 路视频并发导致 CPU 飙升
16 路 FLV 流同时解码,即使是子码流(720P),在普通 PC 上也会导致 CPU 长时间 90%+。解决方案:限制同时解码路数,超出的视频显示静止截图,鼠标悬停时再启动解码。


总结

视频监控平台的前端开发远比普通业务系统复杂,核心在于:协议理解 + 内存管理 + 网络容错三者缺一不可。实时性与稳定性的平衡没有银弹,只能在具体业务场景下不断调参和优化。


最后更新:2026年4月

标签:海康威视、Video.js、FLV、HLS、React、视频流、监控平台

最近更新:4/19/2026, 3:19:53 PM