问题的根因不是 JS 异步,而是 flv.js 的 WebSocket 连接一旦发起,底层资源释放不及时,坏流一直占着浏览器连接槽 。换什么库、什么信号量都打不到这个层面。
换方案:用 mpegts.js(flv.js 的现代维护分支),加 WebSocket 预探测。先用最快的 WebSocket 测每个 URL 是否可达,死的直接跳过不给它碰 flvjs,活的一个一个串行启动。这样坏流连 WebSocket 都不碰,100% 不挡。
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>多路 WS-FLV 监控</title>
<style>
html, body { margin: 0; background: #111; color: #fff; font-family: Arial; }
h2 { text-align: center; padding: 10px; }
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(480px, 1fr)); gap: 12px; padding: 12px; }
.player { background: #000; position: relative; }
.title { position: absolute; top: 6px; left: 8px; font-size: 14px; background: rgba(0,0,0,.6); padding: 2px 6px; border-radius: 4px; }
.status { position: absolute; top: 6px; right: 8px; font-size: 11px; padding: 2px 6px; border-radius: 4px; }
.status.wait { background: #555; }
.status.ok { background: #080; }
.status.bad { background: #800; }
video { width: 100%; height: 360px; background: #000; }
</style>
</head>
<body>
<h2>多路 WS-FLV 实时监控</h2>
<div id="app" class="grid"></div>
<script src="https://cdn.jsdelivr.net/npm/mpegts.js@1.7.3/dist/mpegts.js"></script>
<script>
var cameras = [
{ id: "dataHubRightVideo_1_2_0", title: "船1", url: "ws://10.111.222.5:8000/live/cam1.live.flv" },
{ id: "dataHubRightVideo_1_2_1", title: "船2", url: "ws://10.111.222.5:8000/live/cam1.live.flv" },
{ id: "dataHubRightVideo_1_2_2", title: "船3", url: "ws://10.111.222.5:8000/live/cam1.live.flv" },
{ id: "dataHubRightVideo_1_2_3", title: "船4", url: "ws://10.111.222.5:8000/live/cam1.live.flv" },
{ id: "dataHubRightVideo_1_2_4", title: "船5", url: "ws://10.111.222.5:8000/live/cam1.live.flv" },
{ id: "dataHubRightVideo_1_2_5", title: "船6", url: "ws://10.111.222.5:8000/live/cam1.live.flv" },
{ id: "dataHubRightVideo_1_2_6", title: "船7", url: "ws://10.111.222.5:8000/live/cam1.live.flv" },
{ id: "dataHubRightVideo_1_2_7", title: "船8", url: "ws://10.111.222.5:8000/live/cam1.live.flv" },
{ id: "dataHubRightVideo_1_2_8", title: "船9", url: "ws://10.111.222.5:8000/live/cam1.live.flv" },
{ id: "dataHubRightVideo_1_2_9", title: "T5楼顶船10", url: "ws://10.111.222.5:8000/live/cam1.live.flv" },
]
var app = document.getElementById('app')
var slots = []
// 第一步:立即渲染所有 DOM
cameras.forEach(function (cam) {
var box = document.createElement('div')
box.className = 'player'
var title = document.createElement('div')
title.className = 'title'
title.innerText = cam.title
box.appendChild(title)
var status = document.createElement('div')
status.className = 'status wait'
status.innerText = '等待'
box.appendChild(status)
var video = document.createElement('video')
video.controls = true
video.muted = true
video.autoplay = true
video.style.background = '#222'
box.appendChild(video)
app.appendChild(box)
slots.push({ cam: cam, video: video, status: status, player: null })
})
// 第二步:WebSocket 预探测,逐个来,互不阻塞
var currentIndex = 0
function probeNext() {
if (currentIndex >= slots.length) return
var slot = slots[currentIndex]
currentIndex++
slot.status.className = 'status wait'
slot.status.innerText = '探测'
var ws = null
var probed = false
function finishProbe(alive) {
if (probed) return
probed = true
if (ws) { try { ws.close() } catch (_) { } ws = null }
if (alive) {
slot.status.className = 'status ok'
slot.status.innerText = '播放'
startPlayer(slot)
} else {
slot.status.className = 'status bad'
slot.status.innerText = '不可达'
slot.video.style.background = '#400'
}
// 立即探测下一路
probeNext()
}
try {
ws = new WebSocket(slot.cam.url)
ws.binaryType = 'arraybuffer'
ws.onopen = function () {
// 收到数据才算活着
}
ws.onmessage = function () {
// 收到任何数据 = 活着
finishProbe(true)
}
ws.onerror = function () {
finishProbe(false)
}
ws.onclose = function () {
finishProbe(false)
}
// 1 秒超时
setTimeout(function () {
finishProbe(false)
}, 1000)
} catch (e) {
finishProbe(false)
}
}
function startPlayer(slot) {
var player = mpegts.createPlayer({
type: 'flv',
url: slot.cam.url,
isLive: true,
enableWorker: false,
lazyLoad: false,
stashInitialSize: 128
})
player.attachMediaElement(slot.video)
slot.player = player
player.on(mpegts.Events.ERROR, function () {
slot.player = null
try { player.destroy() } catch (_) { }
slot.status.className = 'status ok'
slot.status.innerText = '重连'
// 3 秒后重连
setTimeout(function () { startPlayer(slot) }, 3000)
})
player.load()
player.play().catch(function () { })
}
// 串行探测启动
probeNext()
// 同时启动多路探测:一次发 2 路
setTimeout(probeNext, 200)
// 第三步:15 秒后对失败的重试
setInterval(function () {
slots.forEach(function (slot) {
if (slot.status.className.indexOf('bad') >= 0 || slot.status.innerText === '重连') {
// 重探测
slot.status.className = 'status wait'
slot.status.innerText = '重试'
var ws = null
var probed = false
function finish(alive) {
if (probed) return
probed = true
if (ws) { try { ws.close() } catch (_) { } ws = null }
if (alive && !slot.player) {
slot.status.className = 'status ok'
slot.status.innerText = '播放'
startPlayer(slot)
} else if (!alive) {
slot.status.className = 'status bad'
slot.status.innerText = '不可达'
}
}
try {
ws = new WebSocket(slot.cam.url)
ws.binaryType = 'arraybuffer'
ws.onmessage = function () { finish(true) }
ws.onerror = function () { finish(false) }
ws.onclose = function () { finish(false) }
setTimeout(function () { finish(false) }, 1000)
} catch (e) { finish(false) }
}
})
}, 15000)
window.addEventListener('beforeunload', function () {
slots.forEach(function (s) {
if (s.player) { try { s.player.destroy() } catch (_) { } }
})
})
</script>
</body>
</html>