海康威视多路监控视频流接入:从协议对接到播放器架构
“互联网+明厨亮灶”智慧监管平台需要对数千个摄像头进行实时巡查,核心技术挑战是:如何在一个 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 协议选择的考量
经过测试,在政府内网环境下:
| 协议 | 延迟 | 兼容性 | 选择 |
|---|---|---|---|
| HLS | 5-15s | 最好,无需插件 | 历史回放 ✓ |
| HTTP-FLV | 1-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、视频流、监控平台