# flv.js / mpegts.js 播不了国标摄像头(H.265 + G.711A)?纯前端 Jessibuca 无后端方案(附完整代码)

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 有两条硬限制:

  1. 视频 H.265 :标准版 flv.js 不支持;mpegts.js 新版依赖浏览器 HEVC 硬解才行(Chrome 需系统/硬件支持 HEVC)。这条流其实过了。
  2. 音频 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.flvws(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: falsetrue 试浏览器 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)
相关推荐
console.log('npc')1 小时前
AI Agent 前端流式渲染核心技术文档(SSE \+ WebSocket \+ 打字机渲染)
前端·人工智能·websocket
法外狂徒11 小时前
将 Pi Agent 接入 HagiCode 的实践之路
服务器·前端·人工智能
IT_陈寒1 小时前
React中useEffect的依赖项把我坑惨了
前端·人工智能·后端
广州华水科技2 小时前
单北斗GNSS在变形监测中的优势与应用分析
前端
程序员爱钓鱼2 小时前
Rust 变量与不可变性:为什么默认不能修改变量?
前端·后端·rust
随风一样自由3 小时前
【前端工程化】前端工程化解耦实践:从问题分析到架构重构
前端·重构·架构
vortex53 小时前
Web 渗透测试:未授权与越权漏洞全流程挖掘思路
前端
我有满天星辰3 小时前
【前端模块化】前端模块化演进之路:从脚本堆砌到标准统一
前端
我有满天星辰3 小时前
Vue 3 响应式双雄:ref 与 reactive 完全指南
前端·javascript·vue.js