视频预览花屏/卡顿?排查三步法:从网络到码流的定位思路

视频预览花屏/卡顿?排查三步法:从网络到码流的定位思路

系列:乐橙开放平台 · 直播排障实战 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 传了吗?

2 源异常

3 转码异常

0/1

用户反馈花屏/卡顿

App 原生预览正常?

设备/网络层
listDeviceDetailsByPage
currentDeviceWifi

平台码流层
getLiveStreamInfo.status
streamId 0/1

online & intensity≥3?

换网/换 AP/有线

status 0/1?

查设备升级/重启/遮罩

降 streamId 或查带宽

客户端层
协议/HTTPS/加密/decrypt

结案或 createDeviceFlvLive

乐橙开放平台在排查中的价值

乐橙开放平台 不只是「签发 URL」,还提供可编程的诊断信号

接口诊断价值
listDeviceDetailsByPagedeviceStatus / channelStatus / cameraStatus / resolutions
currentDeviceWifi当前热点 SSID、信号 intensity(0–5)
getLiveStreamInfo主/辅码流 HLS、status 状态码
queryLiveStatusliveToken 查直播状态
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

踩坑 AlistDeviceDetailsByPage 显示 online,但 App 也卡——可能是同一 Wi-Fi 下上行拥塞,intensity 仍可能 4/5。我们会让现场用手机 Speedtest 看上行,而不只看信号格。

踩坑 B:用了已停维护的旧 deviceListstatus 数字 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 就调 getLiveStreamInfostreams 为空——文档明确要求先创建直播

踩坑 D:10 路监控墙全开 streamId: 0,上行 + 解码双爆,第 8 路开始花屏。墙默认 streamId: 1(辅码流),焦点格再升主码流(详见多路预览实践)。


第三步:客户端播放层(协议、HTTPS、加密)

平台返回 HLS 后,前端常见三类问题:

现象原因处理
转圈不播HTTPS 页面加载 HTTP m3u8getLiveStreamInfo 里带 https / proto=https 的地址
绿屏/解码错误m3u8 当 flv 喂给 flv.jsHLS 用 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

排查决策表(贴墙版)

顺序检查项通过标准失败处理
1App 原生预览清晰流畅先修设备/网络,别看 Web
2deviceStatus / channelStatusonline重启、查电源/网络
3cameraStatusoff关隐私遮罩
4currentDeviceWifi.intensity≥3换 AP、wifiAround 选强信号
5getLiveStreamInfo.status0/12→设备源;3→降 streamId
6播放 URL 协议HTTPS 页用 https m3u8换 streams 里 https 条目
7播放器poc 页正常查 hls 实例泄漏/路数

边界:OpenAPI 能查什么、不能查什么

不能
设备在线、遮罩、分辨率列表替换现场网线质量
直播 status、主/辅 HLS替你选播放器缓冲策略
Wi-Fi SSID、信号强度保证门店上行带宽 SLA
签发 FLV/HLSWebRTC 亚秒级(需 SDK/自建转码)

生产环境注意

  1. 带宽与路数:多路并发预览占账号媒体带宽与路数,超额常见 FL1001 等错误——控制台看资源,墙场景默认辅码流。
  2. 地址安全:HLS 泄露即裸播;短 ticket + 会话绑定,勿写进前端静态配置。
  3. token 缓存accessToken 约 3 天有效,播放网关统一缓存,勿每观众刷新(accessToken)。
  4. 错峰 bind:批量 bindDeviceLive 间隔 80–150ms,避免并发尖峰。
  5. 升级窗口deviceStatus=upgrading 时 status 常为 2,运维日历里避开验收。
  6. 4G IPC:弱网场景优先辅码流 + FLV;极端情况考虑4G 物联网卡方案。

排错速查

现象高概率原因API/动作
全屏马赛克主码流 + 弱上行streamId:1
有 URL 黑屏 status=3转码异常辅码流 / 降分辨率
时好时坏Wi-Fi 干扰currentDeviceWifi + 换 AP
只有 Web 卡混用协议/HTTPhttps hls + hls.js
第十路才卡解码/内存可见才播、destroy 实例

总结

预览花屏/卡顿,不要从「换摄像头」开始。按 网络 → 码流 → 客户端 三步:

listDeviceDetailsByPage + currentDeviceWifi
→ bindDeviceLive + getLiveStreamInfo(看 status)
→ HTTPS HLS / 辅码流 / 正确播放器

小陈那个门店案例,根因是 主码流 status=3 + 弱 Wi-Fi;换辅码流、补一个 AP 后,同一套前端代码无需发布。

延伸阅读

开始接入

如果你正在做监控 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/
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值