flv.js / mpegts.js 播不了国标摄像头(H.265 + G.711A)?纯前端 Jessibuca 无后端方案(附完整代码)
场景:数字孪生 / 综合管控大屏里要嵌入 GB28181 国标摄像头的实时画面,流媒体服务用的是 ZLMediaKit ,输出 http-flv。前端习惯性上了
flv.js,结果一片黑屏 + 控制台狂刷错误。本文完整记录从报错定位到根因、再到纯前端落地的全过程,不借助任何后端代理/转码。
一、问题现象
前端用 flv.js(其实是它的维护分支 mpegts.js)播放 ZLMediaKit 的 http-flv 地址:
http://xxx:9100/media-prev/rtp/34020000001320000006_34020000001310000001.live.flv
结果画面出不来,F12 控制台疯狂刷这段错误(核心就一行,后面全是它引发的连锁 Promise 报错):
text
[FLVDemuxer] > Parsed HEVCDecoderConfigurationRecord
[MSEController] > Received Initialization Segment, mimeType: video/mp4;codecs=hvc1.1.1.L150.B0
[TransmuxingController] > DemuxException: type = CodecUnsupported, info = Flv: Unsupported audio codec idx: 7
Uncaught (in promise) Error: Unhandled error. (undefined)
而同样这条流,甲方那边的页面却能正常播放。
二、排查:错误看着吓人,其实就一行有用
一堆 es6-promise.js 的堆栈是噪音,真正的信息只有三条:
| 日志 | 含义 |
|---|---|
Parsed HEVCDecoderConfigurationRecord |
视频是 H.265 / HEVC |
mimeType: ...codecs=hvc1.1.1.L150.B0 |
mpegts.js 已经把 H.265 交给 MSE,浏览器也接受了 |
DemuxException: CodecUnsupported, Unsupported audio codec idx: 7 |
真正的报错:音频编码 idx=7 不支持 |
也就是说------卡住的根本不是视频,是音频。
FLV 音频编码 id 对照表
FLV 的 SoundFormat(4 bit)对照:
| idx | 编码 |
|---|---|
| 2 | MP3 |
| 7 | G.711 A-law (PCMA) |
| 8 | G.711 μ-law (PCMU) |
| 10 | AAC |
idx = 7 就是 G.711 A-law,国标(GB28181)摄像头几乎默认用它。
用 ffprobe 二次确认
bash
ffprobe -v quiet -print_format json -show_streams "https://<你的流地址>.live.flv"
关键输出:
json
{
"streams": [
{
"codec_type": "video",
"codec_tag_string": "[12][0][0][0]",
"codec_tag": "0x000c"
},
{
"codec_name": "pcm_alaw",
"codec_long_name": "PCM A-law / G.711 A-law",
"codec_type": "audio",
"sample_rate": "8000",
"channels": 1
}
],
"format": {
"format_name": "flv",
"tags": { "title": "Streamed by ZLMediaKit(...)" }
}
}
- 视频
codec_tag 0x000c(=12)= enhanced-FLV 的 HEVC/H.265 (ffprobe 老版本解不全,width/height显示为 0,但编码没错)。 - 音频
pcm_alaw= G.711 A-law,8000Hz,单声道,实锤。
三、根因:MSE 的天花板
flv.js / mpegts.js 的解码是交给浏览器的 MSE(Media Source Extensions),而 MSE 有两条硬限制:
- 视频 H.265 :标准版 flv.js 不支持;mpegts.js 新版依赖浏览器 HEVC 硬解才行(Chrome 需系统/硬件支持 HEVC)。这条流其实过了。
- 音频 G.711 :MSE 从来不支持 G.711 (只吃 AAC / MP3 / Opus)。demuxer 一遇到
idx=7直接抛CodecUnsupported,整个管线崩掉、画面不出。
一句话总结:H.265 侥幸能过,G.711A 音频是真过不去。 这就是 flv.js 播国标摄像头最常见的坑。
而「甲方能播」,是因为他们用的是能软解 G.711 的 WASM 播放器(比如 Jessibuca),或者服务端把音频转码成了 AAC。
四、方案选型:纯前端,不碰后端
两条路:
- 服务端转码:让 ZLMediaKit 把 G.711A 转成 AAC(需编译带 FFmpeg)。缺点:要动服务端、要转码算力,而且很多时候那台机器你根本碰不到。
- 前端换播放器(本文选它) :用 Jessibuca ------内部 WASM 软解,H.265 视频 + G.711 音频通吃,而且走 WebSocket-FLV,顺带绕开跨域。
为什么是 Jessibuca 而不是继续 flv.js:
| flv.js / mpegts.js | Jessibuca | |
|---|---|---|
| 解码 | 浏览器 MSE,受限 | WASM 软解,H.264/H.265 + G.711 全支持 |
| 取流 | fetch/XHR,受 CORS 约束 | WebSocket-FLV,不走 CORS 预检 |
| 后端 | ------ | 不需要,浏览器直连 |
五、落地前的三个探测(建议都确认一遍)
Jessibuca 只吃 WebSocket-FLV(ws/wss),所以先确认 ZLMediaKit 那边 ws-flv 通、证书有效、跨域不拦。
① http-flv 能出数据 + CORS 是否放行
bash
curl -k -s -D - -H "Origin: http://localhost:5500" -o /dev/null \
"https://<你的流地址>.live.flv" | grep -i "access-control\|content-type"
如果返回头里 Access-Control-Allow-Origin 原样回显了你的 Origin,说明服务器反射任意来源,跨域不拦(ZLMediaKit 默认如此)。
② ws-flv 是否支持(同路径把协议换成 wss,看是不是 101)
bash
curl -k -s -i --http1.1 \
-H "Connection: Upgrade" -H "Upgrade: websocket" \
-H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" -H "Sec-WebSocket-Version: 13" \
"https://<你的流地址>.live.flv" | head -5
看到 HTTP/1.1 101 Switching Protocol + Sec-WebSocket-Accept 就说明 ws-flv 可用,Jessibuca 能直连。
③ TLS 证书是否受信任(浏览器 wss 必须有效证书,自签会被拦):
bash
curl -s -o /dev/null -w "%{http_code}\n" "https://<你的流地址>.live.flv"
返回 200(不加 -k)即证书有效。
ZLMediaKit 的 http-flv 和 ws-flv 是同一路径、只换协议:
http(s)://host/app/stream.live.flv⟷ws(s)://host/app/stream.live.flv
六、完整代码
目录结构:
ceshi/
├── index.html # 播放页
├── serve.py # 可选:本地静态服务(会给 .wasm 正确 MIME)
└── jessibuca/
├── jessibuca.js
├── decoder.js
└── decoder.wasm
Jessibuca 三个文件可从 jsDelivr 下载(同源放本地,避免 WASM worker 跨域):
https://cdn.jsdelivr.net/gh/langhuihui/jessibuca@v3/dist/jessibuca.js
https://cdn.jsdelivr.net/gh/langhuihui/jessibuca@v3/dist/decoder.js
https://cdn.jsdelivr.net/gh/langhuihui/jessibuca@v3/dist/decoder.wasm
6.1 index.html
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>Jessibuca 测试 - 国标 H.265 + G.711A</title>
<style>
body { margin: 0; padding: 24px; background: #0f1115; color: #d6dae0;
font: 14px/1.6 "Microsoft YaHei", sans-serif; }
.row { display: flex; gap: 8px; margin-bottom: 12px; flex-wrap: wrap; }
input { flex: 1; min-width: 420px; background: #1a1e26; border: 1px solid #2a2f3a;
color: #d6dae0; padding: 8px 10px; border-radius: 6px; font-family: monospace; }
button { background: #2563eb; color: #fff; border: 0; padding: 8px 16px;
border-radius: 6px; cursor: pointer; }
button.gray { background: #374151; }
#container { width: 960px; max-width: 100%; height: 540px; background: #000; border-radius: 8px; }
</style>
</head>
<body>
<h1>Jessibuca 播放测试 ------ 视频 H.265 / 音频 G.711A(WASM 软解)</h1>
<div class="row">
<input id="url" type="text"
value="wss://<你的流媒体服务器>:<端口>/<路径>/rtp/<流ID>.live.flv" />
<button id="btnPlay">播放</button>
<button id="btnStop" class="gray">停止</button>
<button id="btnMute" class="gray">静音切换</button>
</div>
<div id="container"></div>
<!-- 同源加载,worker + wasm 都在 ./jessibuca/ 下 -->
<script src="./jessibuca/jessibuca.js"></script>
<script>
let jessibuca = null;
function create() {
if (jessibuca) return jessibuca;
jessibuca = new Jessibuca({
container: document.getElementById('container'),
decoder: './jessibuca/decoder.js', // worker 路径,wasm 会同目录 same-origin 加载
videoBuffer: 0.2, // 缓冲 0.2s,尽量低延迟
isResize: true, // 自适应容器
useMSE: false, // 关键:关掉 MSE,强制 WASM 软解 ------ H.265 才能出画面
useWCS: false, // 关掉 WebCodecs,统一走 WASM(最大兼容;想硬解可改 true)
hasAudio: true, // G.711A 音频交给 Jessibuca 内置 WASM 解码
isNotMute: false, // 默认静音(浏览器自动播放策略,声音需手动开)
showBandwidth: true,
operateBtns: { fullscreen: true, screenshot: true, play: true, audio: true },
});
jessibuca.on('play', () => console.log('开始播放'));
jessibuca.on('videoInfo', i => console.log('视频', i.width + 'x' + i.height));
jessibuca.on('audioInfo', i => console.log('音频', i.numOfChannels + '声道 ' + i.sampleRate + 'Hz'));
jessibuca.on('error', e => console.error('错误', e));
return jessibuca;
}
document.getElementById('btnPlay').onclick = () => {
const url = document.getElementById('url').value.trim();
if (!/^wss?:\/\//i.test(url)) { alert('地址必须是 ws:// 或 wss://(Jessibuca 只吃 WebSocket-FLV)'); return; }
create().play(url);
};
document.getElementById('btnStop').onclick = () => jessibuca && jessibuca.pause();
document.getElementById('btnMute').onclick = () => {
if (!jessibuca) return;
jessibuca.isMute() ? jessibuca.cancelMute() : jessibuca.mute();
};
</script>
</body>
</html>
八、常见坑(FAQ)
| 现象 | 原因 & 解决 |
|---|---|
Worker / decoder.js 404、wasm 报错 |
用 file:// 直接双击打开了。必须走 Live Server 或 python serve.py 用 http 打开 |
| 黑屏但控制台无明显报错 | H.265 软解在跑但卡/没渲染。把 useWCS: false 改 true 试浏览器 HEVC 硬解 |
WebSocket connection failed |
① 用错了 http-flv 地址,Jessibuca 只吃 wss:// ② 端口不通 ③ 设备当时没推流 |
| wss 握手失败、证书报错 | 流媒体是自签证书,浏览器 wss 被拦。需换受信任证书,或用 wss 前置一个有正规证书的网关 |
| 有画面无声音 | 正常。浏览器自动播放策略默认静音,点「静音切换」开声 |
| CPU 占用高、风扇狂转 | H.265 纯 WASM 软解本来就吃 CPU。多路播放时建议开硬解(useWCS:true)或降分辨率子码流 |
九、总结
- flv.js / mpegts.js 播不了国标摄像头,十有八九不是 H.265,而是 G.711A 音频(FLV audio idx=7),MSE 不支持,直接把解封装搞崩。
- 判断方法:F12 看
Unsupported audio codec idx: 7,或ffprobe看到pcm_alaw。 - 纯前端解法:换 Jessibuca ,WASM 软解 H.265+G.711,走 ws/wss 直连流媒体,
useMSE:false强制软解即可,全程不需要任何后端。
同类可选:EasyPlayer.js、m7s 的 jessibuca-pro、h265web.js,原理都是 WASM 软解,思路一致。
附上我的解析流的工具
python
import subprocess
import json
def analyze_flv_stream(url):
# 使用 ffprobe 提取流信息
cmd = [
'ffprobe',
'-v', 'quiet',
'-print_format', 'json',
'-show_format',
'-show_streams',
url
]
try:
result = subprocess.run(cmd, capture_output=True, text=True, timeout=15)
data = json.loads(result.stdout)
print(f"--- 视频流解析结果 ---")
print(f"格式: {data['format'].get('format_name')}")
print(f"时长: {data['format'].get('duration', '未知')} 秒")
for stream in data['streams']:
if stream['codec_type'] == 'video':
print(f"\n[视频流]")
print(f"编码: {stream.get('codec_name')}")
print(f"分辨率: {stream.get('width')}x{stream.get('height')}")
print(f"帧率: {stream.get('avg_frame_rate')}")
print(f"像素格式: {stream.get('pix_fmt')}")
elif stream['codec_type'] == 'audio':
print(f"\n[音频流]")
print(f"编码: {stream.get('codec_name')}")
print(f"采样率: {stream.get('sample_rate')} Hz")
print(f"声道: {stream.get('channels')}")
except Exception as e:
print(f"解析失败: {e}")
if __name__ == "__main__":
stream_url =""
analyze_flv_stream(stream_url)