# 故障排查处理记录 ## 一、EasyGBD 注册超时 **现象**:EasyGBD 配置 WVP SIP 服务器地址 `100.83.103.1`,端口 `8160`,提示注册超时。 **原因**:Docker 容器的 8160 端口映射在 WSL2 (172.18.127.247) 上,Windows 的 Tailscale IP (100.83.103.1) 上没有 portproxy 转发该端口。EasyGBD 的 SIP 注册请求无法到达 WVP。 **处理**:在 Windows 管理员 PowerShell 中添加 TCP portproxy: ```powershell netsh interface portproxy add v4tov4 listenport=8160 listenaddress=100.83.103.1 connectport=8160 connectaddress=172.18.127.247 ``` EasyGBD 中 SIP 传输协议改为 TCP(`netsh interface portproxy` 不支持 UDP 转发)。 --- ## 二、播放提示"通道不存在" **现象**:调用 WVP `GET /api/play/start/{deviceId}/{channelId}` 返回 `code: 100, msg: "通道不存在"`。 **原因**: 1. WVP API 路径错误:用了 `/api/device/query/{deviceId}/channels`,正确路径是 `/api/device/query/devices/{deviceId}/channels`(多了 `devices`)。 2. 通道 ID 不匹配:EasyGBD 的通道 ID 不是设备 ID `34020000002000000001`,而是 `34020000001310000001`(类型编码 131 = 模拟通道)。 **处理**: 1. 用正确路径调用 `GET /api/device/query/devices/{deviceId}/sync` 同步通道。 2. 从通道列表中获取实际通道 ID `34020000001310000001`。 3. 在前端「摄像头管理」中更新通道 ID。 --- ## 三、播放提示"收流超时" **现象**:WVP play/start 返回 `code: -2, msg: "收流超时"`。 **原因**:ZLMediaKit 的 RTP 媒体端口(10001-10003)映射在 WSL2 上,但 Windows 的 100.83.103.1 上没有 portproxy 转发。EasyGBD 收到 INVITE 后尝试连接 `100.83.103.1:10003` 发送 RTP 流,连接失败。 **处理**:添加 TCP portproxy 转发 ZLMediaKit 端口: ```powershell netsh interface portproxy add v4tov4 listenport=10001 listenaddress=100.83.103.1 connectport=10001 connectaddress=172.18.127.247 netsh interface portproxy add v4tov4 listenport=10002 listenaddress=100.83.103.1 connectport=10002 connectaddress=172.18.127.247 netsh interface portproxy add v4tov4 listenport=10003 listenaddress=100.83.103.1 connectport=10003 connectaddress=172.18.127.247 netsh interface portproxy add v4tov4 listenport=8081 listenaddress=100.83.103.1 connectport=8081 connectaddress=172.18.127.247 netsh interface portproxy add v4tov4 listenport=18978 listenaddress=100.83.103.1 connectport=18978 connectaddress=172.18.127.247 ``` --- ## 四、FLV 播放失败 CodecUnsupported **现象**:实时视频播放提示 `FLV播放失败: MediaError - CodecUnsupported`。 **原因**:EasyGBD 默认视频编码为 H265/HEVC,浏览器 MSE 不支持 H265 解码。 **处理**:在 EasyGBD APP 设置中将视频编码从 H265 改为 H264/AVC。 --- ## 五、摄像头管理不能删除和修改 **现象**:「摄像头管理」Tab 中点击编辑或删除按钮无响应。 **原因**:前端 DAL 层 `src/dal/video.ts` 只导入了 `get` 和 `post`,缺少 `patch` 和 `del`。`updateCamera` 和 `deleteCamera` 调用了未定义的函数。 **处理**: ```typescript // 修改前 import { get, post } from '../api/http'; // 修改后 import { del, get, patch, post } from '../api/http'; ``` --- ## 六、HLS 播放地址端口不正确 **现象**:WVP 返回的 HLS 地址端口为 `80`,但 ZLMediaKit Docker 映射为 `8081->80`,浏览器无法访问 `http://100.83.103.1:80/...`。 **原因**:WVP 中 ZLMediaKit 的 `httpPort` 配置为容器内部端口 `80`,但 Docker 将其映射到宿主机 `8081`。 **处理**:在 `MediaService.extractPlayResult` 中将 URL 中的 `:80/` 替换为 `:8081/`: ```typescript const zlmHttpPort = process.env.ZLM_HTTP_PORT || '8081'; const fixUrl = (url?: string): string | undefined => { if (!url) return url; return url.replace(/:80\//, `:${zlmHttpPort}/`).replace(/:80$/, `:${zlmHttpPort}`); }; ``` --- ## 七、历史回看看不到录像片段 **现象**:前端「历史回看」选择时间范围查询,显示"暂无录像片段"。 **原因**:录制文件名中的时间是 CST(UTC+8),但插入数据库时错误地标记为 UTC(`+00`),导致时间差 8 小时。用户查询北京时间 12:00-14:00 时,`toISOString()` 转为 UTC 04:00-06:00,不包含数据库中错误存储的 UTC 13:13。 **处理**: 1. 修正数据库中已有记录的时间(减 8 小时)。 2. 后续自动录制归档由服务器端 Python 脚本处理,脚本从文件名解析时间并带 `+08:00` 时区,NestJS 内部 API 接收 ISO 8601 带时区的时间字符串。 --- ## 八、自动录制归档 stopRecord 失败 **现象**:RecordingService 每 5 分钟触发归档时,`stopRecord` 返回 `can not find the stream`,归档流程中断。 **原因**: 1. WVP 播放会话在 5 分钟后超时,ZLMediaKit 中的流自动关闭,`stopRecord` 找不到流。 2. `stopRecord` 抛出异常后,整个 `archiveAndRestart` 方法中断,后续归档和重新录制不执行。 **处理**: 1. 在 `archiveAndRestart` 和 `stopRecording` 中将 `stopRecord` 包在 try-catch 中,失败只记警告,不中断归档流程。 2. 在 `startRecording` 中,`startPlay` 失败时跳过录制(不继续调用 `startRecord`)。 --- ## 九、NestJS API 从 WSL2 访问返回 404 **现象**:服务器端 Python 归档脚本调用 `http://172.18.112.1:3000/api/v1/auth/login` 返回 404,响应内容是 Gogs(Git 服务)的页面。 **原因**:WSL2 网关 IP `172.18.112.1` 的 3000 端口被 Gogs 占用(或被路由到 Gogs),而不是 NestJS 后端。NestJS 虽然监听 `0.0.0.0:3000`,但从 WSL2 通过网关 IP 访问时到达的是 Gogs。 **处理**:改用 Windows 的 Tailscale IP `100.83.61.186` 访问 NestJS: ```python # 修改前 NESTJS_BASE = 'http://172.18.112.1:3000/api/v1' # 修改后 NESTJS_BASE = 'http://100.83.61.186:3000/api/v1' ``` --- ## 十、cron 定时任务不运行 **现象**:cron 已配置每分钟运行归档脚本,但 `/home/pan/archive.log` 不存在,脚本从未被执行。 **原因**:WSL2 默认未启用 systemd,`service cron start` 失败("Failed to connect to bus"),cron 守护进程未运行。 **处理**: 1. 手动启动 cron:`sudo cron` 2. 配置 WSL2 启用 systemd,写入 `/etc/wsl.conf`: ```ini [boot] systemd=true ``` 3. 在 PowerShell 中执行 `wsl --shutdown` 重启 WSL2,之后 cron 会随 systemd 自动启动。 --- ## 十一、录制文件归档方案演进 ### 方案 A(初始):NestJS HTTP 下载 + 上传 ``` ZLMediaKit(100.83.103.1) → HTTP下载到本机(Windows) → 上传到 Ceph S3(100.83.103.1) ``` 问题:录制文件经过两次网络传输(下载到本机再上传回服务器),效率低。 ### 方案 C(最终):服务器端 Python 脚本归档 ``` NestJS(Windows) → 控制录制(WVP/ZLM API) Python脚本(100.83.103.1) → cron每分钟扫描 → docker cp → boto3上传Ceph(localhost) → 调NestJS API建库记录 → 删除容器内文件 ``` 优势: - 上传走服务器内部 localhost,速度快(12MB 文件 0.2 秒完成) - NestJS 只负责录制控制和数据库管理,不参与文件传输 - 24 个积压文件在 18 秒内全部归档完成 --- ## 十二、历史回看视频播放失败 **现象**:前端「历史回看」点击播放按钮,一直显示加载动画或提示"视频播放失败"。浏览器 Network 标签页显示请求状态码在 206/200 和 canceled 之间反复切换。 **原因**:多个问题叠加: ### 1. S3 预签名 URL 包含 Ceph RGW 不兼容的参数 AWS SDK v3 的 `getSignedUrl` 自动在预签名 URL 中添加 `x-amz-checksum-mode=ENABLED` 和 `x-id=GetObject` 查询参数。Ceph RGW (squid) 在验证签名时无法正确处理这些参数,返回 403 Forbidden。 **处理**:在 `s3.service.ts` 中用 Node.js `crypto` 模块手动实现 AWS S3 V4 签名,完全绕过 AWS SDK v3 的 presigner,不添加任何额外参数: ```typescript async getPresignedUrl(bucket: string, key: string, expiresIn: number = 3600): Promise { // 手动构建 Canonical Request → String to Sign → Signing Key → Signature // 只包含标准参数:X-Amz-Algorithm, X-Amz-Credential, X-Amz-Date, X-Amz-Expires, X-Amz-SignedHeaders // 不包含 x-amz-checksum-mode 和 x-id } ``` ### 2. Ceph S3 Bucket 缺少 CORS 配置 前端从 `http://100.83.103.1:5174` 访问页面,视频预签名 URL 在 `http://100.83.103.1:7480`,属于跨域请求。Ceph RGW 默认没有配置 CORS,浏览器拒绝跨域响应。 **处理**:用 boto3 设置 bucket CORS: ```python s3.put_bucket_cors(Bucket='silk-video-events', CORSConfiguration={ 'CORSRules': [{ 'AllowedHeaders': ['*'], 'AllowedMethods': ['GET', 'HEAD'], 'AllowedOrigins': ['*'], 'ExposeHeaders': ['ETag', 'Content-Length', 'Content-Range'], 'MaxAgeSeconds': 3600 }] }) ``` ### 3. VideoPlayer 组件 useEffect 无限循环 `VideoPlayer` 组件的 `useEffect` 依赖数组包含 `onError`,而父组件 `Video.tsx` 传入的 `onError` 是内联函数(每次渲染都创建新引用),导致 `useEffect` 不断重新执行 → `