视频预览花屏/卡顿?排查三步法:从网络到码流的定位思路
系列:乐橙开放平台 · 直播排障实战 01
文档依据:开发规范 · getLiveStreamInfo · 设备 WiFi · 设备直播说明
说明:接口均来自现行 OpenAPI,不使用「旧版本协议」栏目下已停维护接口。
门店督导小陈发来一段录屏:画面像「打马赛克的雪花」,声音还一卡一卡。后端同学截图反驳:「bindDeviceLive 返回 200,hls 地址也有。」
我打开日志,先问了两句:App 里同一台机子清晰吗?——清晰。那问题不在摄像机,在链路的某一环。 二十分钟后,脚本输出:getLiveStreamInfo 主码流 status=3(码流转换异常),Wi-Fi 信号 intensity=1。换辅码流 + 换 AP 后,画面恢复正常。
为什么「花屏/卡顿」值得单独写一篇
接入乐橙设备的开发者,前几周通常卡在 sign、token、设备绑定;第一次被业务方催,往往就是「预览不行」:
| 用户描述 | 技术可能 | 常见误判 |
|---|---|---|
| 马赛克、花屏 | 上行带宽不足、主码流过高、丢包 | 「平台挂了」 |
| 一直转圈 | 设备离线、未 create 直播、协议选错 | 「前端 bug」 |
| 播几秒就停 | HLS 切片超时、并发路数满 | 「地址过期」 |
| 有画面但模糊 | 用了辅码流或分辨率低 | 「镜头脏了」 |
如果把所有问题都归到「前端播放器」,会陷入 endless 改 CSS。正确做法是把链路切成三层,逐层排除:
① 网络与设备层 设备在线吗?Wi-Fi 信号如何?隐私遮罩关了吗?
② 平台与码流层 直播创建了吗?status 是 0 还是 2/3?主码流还是辅码流?
③ 客户端播放层 HLS/FLV 选对了吗?HTTPS 混用?视频加密 key 传了吗?
乐橙开放平台在排查中的价值
乐橙开放平台 不只是「签发 URL」,还提供可编程的诊断信号:
| 接口 | 诊断价值 |
|---|---|
listDeviceDetailsByPage | deviceStatus / channelStatus / cameraStatus / resolutions |
currentDeviceWifi | 当前热点 SSID、信号 intensity(0–5) |
getLiveStreamInfo | 主/辅码流 HLS、status 状态码 |
queryLiveStatus | 按 liveToken 查直播状态 |
bindDeviceLive | 指定 streamId 重建直播 |
官方对 getLiveStreamInfo 返回的 status 定义(见文档):
| status | 含义 | 排查方向 |
|---|---|---|
0 | 正在直播中 | 正常,查客户端 |
1 | 直播中,封面异常 | 可播,封面问题可忽略 |
2 | 视频源异常 | 设备侧:离线、遮罩、升级中 |
3 | 码流转换异常 | 降码流、查上行带宽 |
4 | 云存储访问异常 | 录播场景 |
10 | 直播暂停中 | 查直播计划 job |
公共客户端(全文复用)
// src/openapi-client.js
import crypto from 'node:crypto';
import { randomUUID } from 'node:crypto';
const BASE = 'https://openapi.lechange.cn/openapi';
export function calcSign(time, nonce, appSecret) {
const raw = `time:${time},nonce:${nonce},appSecret:${appSecret}`;
return crypto.createHash('md5').update(raw, 'utf8').digest('hex');
}
export async function callOpenApi(method, appId, appSecret, params = {}) {
const time = Math.floor(Date.now() / 1000);
const nonce = randomUUID();
const body = {
system: { ver: '1.0', appId, sign: calcSign(time, nonce, appSecret), time, nonce },
id: randomUUID(),
params,
};
const res = await fetch(`${BASE}/${method}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
const json = await res.json();
if (json.result?.code !== '0') throw new Error(`[${json.result.code}] ${json.result.msg}`);
return json.result.data;
}
export const getToken = (appId, appSecret) =>
callOpenApi('accessToken', appId, appSecret, {});
第一步:网络与设备层(约 30% 花屏根因)
// scripts/step1-network-device.js
import 'dotenv/config';
import { callOpenApi, getToken } from '../src/openapi-client.js';
const appId = process.env.LECHANGE_APP_ID;
const appSecret = process.env.LECHANGE_APP_SECRET;
const deviceId = process.env.TARGET_DEVICE_ID;
const channelId = process.env.TARGET_CHANNEL_ID ?? '0';
const token = await getToken(appId, appSecret);
// 1-A 设备与通道状态
const page = await callOpenApi('listDeviceDetailsByPage', appId, appSecret, {
token, pageSize: 50, page: 1, source: 'bindAndShare',
});
const dev = page.deviceList?.find((d) => d.deviceId === deviceId);
const ch = dev?.channelList?.find((c) => String(c.channelId) === channelId);
console.log('--- 设备层 ---');
console.log('deviceStatus:', dev?.deviceStatus);
console.log('channelStatus:', ch?.channelStatus);
console.log('cameraStatus:', ch?.cameraStatus); // on=隐私遮罩开,可能黑屏
console.log('resolutions:', ch?.resolutions);
if (dev?.deviceStatus !== 'online' || ch?.channelStatus !== 'online') {
console.warn('⚠ P0:设备或通道非 online,先解决离线再谈码流');
}
if (ch?.cameraStatus === 'on') {
console.warn('⚠ P0:隐私遮罩已开启,画面可能全黑');
}
// 1-B Wi-Fi 信号(无线 IPC)
try {
const wifi = await callOpenApi('currentDeviceWifi', appId, appSecret, { token, deviceId });
console.log('--- Wi-Fi ---');
console.log('ssid:', wifi.ssid, 'intensity:', wifi.intensity, 'linkEnable:', wifi.linkEnable);
if (Number(wifi.intensity) <= 2) {
console.warn('⚠ P1:信号偏弱,易花屏/卡顿,建议换 AP 或 5G/有线');
}
} catch (e) {
console.log('currentDeviceWifi 跳过(可能为有线/NVR 通道):', e.message);
}
文档:listDeviceDetailsByPage · currentDeviceWifi
踩坑 A:listDeviceDetailsByPage 显示 online,但 App 也卡——可能是同一 Wi-Fi 下上行拥塞,intensity 仍可能 4/5。我们会让现场用手机 Speedtest 看上行,而不只看信号格。
踩坑 B:用了已停维护的旧 deviceList,status 数字 0/1 与新版字符串 online/offline 对不上,误判离线。统一用 listDeviceDetailsByPage。
第二步:平台与码流层(status 决定方向)
// scripts/step2-stream-status.js
import 'dotenv/config';
import { callOpenApi, getToken } from '../src/openapi-client.js';
const appId = process.env.LECHANGE_APP_ID;
const appSecret = process.env.LECHANGE_APP_SECRET;
const deviceId = process.env.TARGET_DEVICE_ID;
const channelId = process.env.TARGET_CHANNEL_ID ?? '0';
const token = await getToken(appId, appSecret);
// 2-A 确保已创建直播(须先 bindDeviceLive)
await callOpenApi('bindDeviceLive', appId, appSecret, {
token, deviceId, channelId, streamId: 1, liveMode: 'proxy',
});
// 2-B 拉全量码流与 status
const info = await callOpenApi('getLiveStreamInfo', appId, appSecret, {
token, deviceId, channelId,
});
const STATUS_TEXT = {
'0': '直播中',
'1': '直播中(封面异常)',
'2': '视频源异常',
'3': '码流转换异常',
'4': '云存储访问异常',
'10': '直播暂停',
};
console.log('--- 码流层 ---');
for (const s of info.streams ?? []) {
console.log({
streamId: s.streamId,
status: s.status,
meaning: STATUS_TEXT[s.status] ?? '未知',
hls: s.hls?.slice(0, 80) + '...',
liveToken: s.liveToken,
});
}
// 2-C 按 liveToken 再查(可选)
const main = info.streams?.find((s) => s.streamId === 0);
if (main?.liveToken) {
const qs = await callOpenApi('queryLiveStatus', appId, appSecret, {
token, liveToken: main.liveToken,
});
console.log('queryLiveStatus:', qs.streams);
}
文档:bindDeviceLive · getLiveStreamInfo · queryLiveStatus
处置策略(代码里可直接用):
function recommendStreamAction(streams) {
const main = streams?.find((s) => s.streamId === 0);
const sub = streams?.find((s) => s.streamId === 1);
if (main?.status === '2') return { action: 'fix_device', hint: '查离线/遮罩/升级' };
if (main?.status === '3' && sub?.status === '0') {
return { action: 'use_sub_stream', streamId: 1, hint: '主码流转码异常,切辅码流' };
}
if (main?.status === '0') return { action: 'ok', streamId: 0 };
return { action: 'check_client', hint: '平台侧正常,查播放器/HTTPS/加密' };
}
踩坑 C:未先 bindDeviceLive 就调 getLiveStreamInfo,streams 为空——文档明确要求先创建直播。
踩坑 D:10 路监控墙全开 streamId: 0,上行 + 解码双爆,第 8 路开始花屏。墙默认 streamId: 1(辅码流),焦点格再升主码流(详见多路预览实践)。
第三步:客户端播放层(协议、HTTPS、加密)
平台返回 HLS 后,前端常见三类问题:
| 现象 | 原因 | 处理 |
|---|---|---|
| 转圈不播 | HTTPS 页面加载 HTTP m3u8 | 用 getLiveStreamInfo 里带 https / proto=https 的地址 |
| 绿屏/解码错误 | m3u8 当 flv 喂给 flv.js | HLS 用 hls.js,FLV 用 flv.js |
| 花屏但 status=0 | 视频加密未传 key | 设备 encryptMode=1 时传解密 key |
轻应用播放组件文档说明:设备开启视频加密时,需传 code(自定义密钥或设备密码,见轻应用组件)。OpenSDK 侧用 LCOpenSDK_Utils.decryptPic 解 alarm 缩略图——直播解密逻辑在 SDK 内处理。
HLS 最小播放验证(浏览器控制台可测,与业务解耦):
<!-- poc/hls-smoke-test.html -->
<video id="v" controls muted playsinline style="width:640px;height:360px"></video>
<script src="https://cdn.jsdelivr.net/npm/hls.js@1"></script>
<script>
const url = 'REPLACE_WITH_https_HLS_FROM_getLiveStreamInfo';
const v = document.getElementById('v');
if (Hls.isSupported()) {
const hls = new Hls({ maxBufferLength: 10 });
hls.loadSource(url);
hls.attachMedia(v);
hls.on(Hls.Events.ERROR, (_, data) => console.error('hls.js', data));
} else if (v.canPlayType('application/vnd.apple.mpegurl')) {
v.src = url;
}
</script>
若 poc 页清晰、业务页花屏 → 查业务侧是否缩放过度、是否多实例未 destroy、是否同时拉主+辅两路。
低延迟备选:HLS 延迟 3–8 秒属正常;互动场景可试 createDeviceFlvLive(realTime) + flv.js(见设备直播说明)。
const flv = await callOpenApi('createDeviceFlvLive', appId, appSecret, {
token,
deviceId,
channelId,
type: 'realTime',
});
console.log('flv:', flv.flv, 'flvHD:', flv.flvHD);
一键诊断脚本(三步合并)
// scripts/diagnose-preview.js — npm run diagnose
import 'dotenv/config';
import { callOpenApi, getToken } from '../src/openapi-client.js';
const appId = process.env.LECHANGE_APP_ID;
const appSecret = process.env.LECHANGE_APP_SECRET;
const deviceId = process.env.TARGET_DEVICE_ID;
const channelId = process.env.TARGET_CHANNEL_ID ?? '0';
const token = await getToken(appId, appSecret);
const report = { deviceId, channelId, steps: [] };
const page = await callOpenApi('listDeviceDetailsByPage', appId, appSecret, {
token, pageSize: 50, page: 1, source: 'bindAndShare',
});
const dev = page.deviceList?.find((d) => d.deviceId === deviceId);
const ch = dev?.channelList?.find((c) => String(c.channelId) === channelId);
report.steps.push({
step: 1,
deviceStatus: dev?.deviceStatus,
channelStatus: ch?.channelStatus,
cameraStatus: ch?.cameraStatus,
});
try {
const wifi = await callOpenApi('currentDeviceWifi', appId, appSecret, { token, deviceId });
report.steps.push({ step: 1, wifiIntensity: wifi.intensity, ssid: wifi.ssid });
} catch { /* wired device */ }
await callOpenApi('bindDeviceLive', appId, appSecret, {
token, deviceId, channelId, streamId: 1, liveMode: 'proxy',
});
const info = await callOpenApi('getLiveStreamInfo', appId, appSecret, { token, deviceId, channelId });
report.steps.push({ step: 2, streams: info.streams?.map((s) => ({ streamId: s.streamId, status: s.status })) });
const sub = info.streams?.find((s) => s.streamId === 1 && s.status === '0');
const httpsHls = info.streams?.find((s) => s.hls?.startsWith('https'));
report.recommendation = sub
? { playUrl: httpsHls?.hls ?? sub.hls, streamId: 1, note: '建议辅码流+HTTPS' }
: { note: '检查 status=2/3 或客户端' };
console.log(JSON.stringify(report, null, 2));
运行:
cp .env.example .env
# LECHANGE_APP_ID / LECHANGE_APP_SECRET / TARGET_DEVICE_ID
node scripts/diagnose-preview.js
排查决策表(贴墙版)
| 顺序 | 检查项 | 通过标准 | 失败处理 |
|---|---|---|---|
| 1 | App 原生预览 | 清晰流畅 | 先修设备/网络,别看 Web |
| 2 | deviceStatus / channelStatus | online | 重启、查电源/网络 |
| 3 | cameraStatus | off | 关隐私遮罩 |
| 4 | currentDeviceWifi.intensity | ≥3 | 换 AP、wifiAround 选强信号 |
| 5 | getLiveStreamInfo.status | 0/1 | 2→设备源;3→降 streamId |
| 6 | 播放 URL 协议 | HTTPS 页用 https m3u8 | 换 streams 里 https 条目 |
| 7 | 播放器 | poc 页正常 | 查 hls 实例泄漏/路数 |
边界:OpenAPI 能查什么、不能查什么
| 能 | 不能 |
|---|---|
| 设备在线、遮罩、分辨率列表 | 替换现场网线质量 |
| 直播 status、主/辅 HLS | 替你选播放器缓冲策略 |
| Wi-Fi SSID、信号强度 | 保证门店上行带宽 SLA |
| 签发 FLV/HLS | WebRTC 亚秒级(需 SDK/自建转码) |
生产环境注意
- 带宽与路数:多路并发预览占账号媒体带宽与路数,超额常见
FL1001等错误——控制台看资源,墙场景默认辅码流。 - 地址安全:HLS 泄露即裸播;短 ticket + 会话绑定,勿写进前端静态配置。
- token 缓存:
accessToken约 3 天有效,播放网关统一缓存,勿每观众刷新(accessToken)。 - 错峰 bind:批量
bindDeviceLive间隔 80–150ms,避免并发尖峰。 - 升级窗口:
deviceStatus=upgrading时 status 常为 2,运维日历里避开验收。 - 4G IPC:弱网场景优先辅码流 + FLV;极端情况考虑4G 物联网卡方案。
排错速查
| 现象 | 高概率原因 | API/动作 |
|---|---|---|
| 全屏马赛克 | 主码流 + 弱上行 | streamId:1 |
| 有 URL 黑屏 status=3 | 转码异常 | 辅码流 / 降分辨率 |
| 时好时坏 | Wi-Fi 干扰 | currentDeviceWifi + 换 AP |
| 只有 Web 卡 | 混用协议/HTTP | https hls + hls.js |
| 第十路才卡 | 解码/内存 | 可见才播、destroy 实例 |
总结
预览花屏/卡顿,不要从「换摄像头」开始。按 网络 → 码流 → 客户端 三步:
listDeviceDetailsByPage + currentDeviceWifi
→ bindDeviceLive + getLiveStreamInfo(看 status)
→ HTTPS HLS / 辅码流 / 正确播放器
小陈那个门店案例,根因是 主码流 status=3 + 弱 Wi-Fi;换辅码流、补一个 AP 后,同一套前端代码无需发布。
延伸阅读
- 5 分钟获取直播地址:HLS / FLV 对比(同系列)
- 10 路同屏预览:内存与码率策略
- OpenSDK 实时预览 — App 低延迟首选
- 轻应用 JS 组件 — 低代码 Web 预览
开始接入
如果你正在做监控 SaaS、门店巡店或社区视频墙,建议先把 diagnose-preview.js 接进运维后台——用户报障时一键出报告,比远程猜「是不是网络」高效得多。
到 乐橙开放平台 open.imou.com 注册开发者并创建应用,可领取免费设备接入额度与媒体带宽。平台以视频技术和安全为核心,开放云直播、OpenAPI、轻应用与 OpenSDK,帮助第三方厂商和个人开发者快速、低成本落地视频场景——会播,也要会排障,才是长期能维护的项目。
你在预览排查里,卡在 status=2 还是 status=3 更多?评论区说说现象,下篇可以专写「多路墙并发」调优。
参考文档
- https://open.imou.com/document/pages/c20750/
- https://open.imou.com/document/pages/683248/
- https://open.imou.com/document/pages/c325e8/
- https://open.imou.com/document/pages/784c94/
- https://open.imou.com/document/pages/1bc396/
- https://open.imou.com/document/pages/7add15/
- https://open.imou.com/document/pages/a56c83/
- https://open.imou.com/document/pages/3f4ecd/
- https://open.imou.com/document/pages/92c72c/
419

被折叠的 条评论
为什么被折叠?



