- 音视频
- 后端
- 物联网
- 计算机视觉
【免费下载链接】scrypted
Scrypted is a high performance video integration and automation platform
本文以 Scrypted 仓库中 Amcrest 插件的 AD410 门铃真实事件抓包(plugins/amcrest/ad410.md)为核心素材,逐事件解读 Amcrest 门铃在"按键呼叫"过程中推送的 multipart 事件流,并结合插件源码(amcrest-api.ts、main.ts)说明这些事件如何被解析、匹配,最终驱动 Scrypted 的BinarySensor(门铃状态)与MotionSensor(移动侦测)接口,帮助你在调试 Amcrest 门铃、对接自定义事件或排查门铃不触发问题时具备完整的协议级认知。
AD410 门铃的真实事件流抓包
plugins/amcrest/ad410.md 记录了 Amcrest AD410 门铃在一次完整"按键呼叫"(访客按下门铃按钮、无人接听、随后挂断)过程中通过 HTTP 长连接推送的原始事件流。这是理解整个 Amcrest 事件协议最直接的样本:
event --myboundary Content-Type: text/plain Content-Length:41 Code=_CallRemoveMask;action=Start;index=0 event --myboundary Content-Type: text/plain Content-Length:36 Code=AlarmLocal;action=Start;index=0 event --myboundary Content-Type: text/plain Content-Length:148 Code=_DoTalkAction_;action=Pulse;index=0;data={ "Action" : "Invite", "CallID" : "20220406135211@095697@192.168.10.20", "CallSrcMask" : 4 } event --myboundary Content-Type: text/plain Content-Length:68 Code=CallNoAnswered;action=Start;index=0;data={ "CallID" : "4" } event --myboundary Content-Type: text/plain Content-Length:41 Code=PhoneCallDetect;action=Start;index=0 event --myboundary Content-Type: text/plain Content-Length:41 Code=VideoMotionInfo;action=State;index=0 event --myboundary Content-Type: text/plain Content-Length:185 Code=_DoTalkAction_;action=Pulse;index=0;data={ "Action" : "Hangup", "CallID" : "20220406135211@095697@192.168.10.20", "CallSrcMask" : 4, "HangupReason" : "HangupByPhone" } event --myboundary Content-Type: text/plain Content-Length:35 Code=AlarmLocal;action=Stop;index=0 event --myboundary Content-Type: text/plain Content-Length:39 Code=CallNoAnswered;action=Stop;index=0 event --myboundary Content-Type: text/plain Content-Length:95 Code=VideoMotion;action=Stop;index=0;data={ "Id" : [ 0 ], "RegionName" : [ "Region1" ] } event --myboundary Content-Type: text/plain Content-Length:40 Code=PhoneCallDetect;action=Stop;index=0 event --myboundary Content-Type: text/plain Content-Length:40 Code=_CallRemoveMask;action=Stop;index=0分帧格式:multipart 边界 + 头 + 定长 body
抓包中每一帧(part)的构成非常清晰:
--myboundary:multipart 流的边界行,用于分隔相邻两个事件 part。抓包中边界行前的event前缀是捕获工具/控制台打印的日志前缀,不属于流内容本身;Content-Type: text/plain:该 part 的 MIME 类型,声明 body 是纯文本;Content-Length:N:该 part body 的精确字节数,例如Code=_CallRemoveMask;action=Start;index=0恰好是 41 字节;- 空行:头与 body 的分隔;
- body:
Code=...;action=...;index=...;data={...}形式的事件负载,其中data字段是可选的 JSON。
这个分帧方式与 amcrest-api.ts 中的解析逻辑一一对应:插件从 HTTP 响应的content-type头中解析出 boundary(源码注释明确指出 Amcrest 可能发送--myboundary或带空格的-- myboundary两种形式),再对每个 part 读取头行、按Content-Length读取定长 body。
逐事件解读:一次完整门铃呼叫的生命周期
把 12 个事件按时间序展开,就是一次 AD410 门铃呼叫的完整状态机:
| 顺序 | 事件 | 语义 |
|---|---|---|
| 1 | Code=_CallRemoveMask;action=Start;index=0 | 呼叫开始,解除掩码(门铃内部状态,插件忽略) |
| 2 | Code=AlarmLocal;action=Start;index=0 | 本地报警/响铃开始(插件忽略) |
| 3 | Code=_DoTalkAction_;action=Pulse;index=0;data={"Action":"Invite",...} | 访客按下门铃按钮,发起双向对讲邀请(关键事件) |
| 4 | Code=CallNoAnswered;action=Start;index=0;data={"CallID":"4"} | 呼叫开始被标记为"未应答" |
| 5 | Code=PhoneCallDetect;action=Start;index=0 | 检测到电话呼叫(门铃被按) |
| 6 | Code=VideoMotionInfo;action=State;index=0 | 移动侦测状态脉冲(访客靠近镜头触发) |
| 7 | Code=_DoTalkAction_;action=Pulse;index=0;data={"Action":"Hangup",...,"HangupReason":"HangupByPhone"} | 对讲结束,由电话侧挂断(关键事件) |
| 8 | Code=AlarmLocal;action=Stop;index=0 | 本地报警停止 |
| 9 | Code=CallNoAnswered;action=Stop;index=0 | "未应答"状态结束 |
| 10 | Code=VideoMotion;action=Stop;index=0;data={"Id":[0],"RegionName":["Region1"]} | 移动侦测区域停止,RegionName指出触发区域 |
| 11 | Code=PhoneCallDetect;action=Stop;index=0 | 电话呼叫检测结束 |
| 12 | Code=_CallRemoveMask;action=Stop;index=0 | 呼叫结束,掩码恢复 |
几个值得注意的细节:
CallID的构成:"20220406135211@095697@192.168.10.20"遵循"时间戳@序号@来源IP"的格式,Invite 与 Hangup 两个脉冲携带相同CallID,可用于配对一次完整呼叫;CallSrcMask: 4表示呼叫来源掩码。- 动作语义:
action取值为Start(进入某状态)、Stop(离开某状态)、Pulse(瞬时脉冲,常带 JSONdata)、State(状态上报)。 index=0:通道索引(0 基)。插件会把index + 1与设备配置的通道号比较,不匹配则丢弃(见后文)。
对照:AD110 的门铃事件流
仓库中另有 AD110 抓包 可以对照阅读。AD110 的呼叫事件序列更精简:NTPAdjustTime;action=Pulse(NTP 校时脉冲)、_DoTalkAction_;action=Pulse(Invite)、CallNoAnswered;action=Start、CallNoAnswered;action=Stop、_DoTalkAction_;action=Pulse(Hangup)。
两者的对比揭示了 Amcrest 不同型号门铃的事件协议差异:
- AD410 会额外推送
PhoneCallDetect、AlarmLocal、_CallRemoveMask、VideoMotion/VideoMotionInfo等事件,形成完整的呼叫 + 移动侦测生命周期;AD110 的抓包中则没有这些条目(或其固件版本未推送); - AD110 抓包中还混入了
[HomeKit]: snapshot requested for reason: 1日志行——这是 Scrypted 控制台输出与原始事件流同一捕获通道产生的混排结果,并非 multipart 事件流的一部分,阅读此类抓包时需要区分; - 关键的双向对讲事件(
_DoTalkAction_的 Invite/Hangup 脉冲、CallNoAnswered)在两个型号上完全一致,这正是插件能够用统一逻辑处理多种门铃的基础。
源码级解析:listenEvents 如何消费事件流
订阅端点与边界解析
AmcrestCameraClient.listenEvents() 是整个事件链的入口,它向设备发起一个 HTTP 长连接请求:
http://<ip>/cgi-bin/eventManager.cgi?action=attach&codes=[All]codes=[All]表示订阅设备的全部事件码。拿到可读流后,解析流程与上面抓包的分帧格式严格对应:
- 逐行读取边界(common/src/read-stream.ts 的
readUntil/readLine按换行切分);遇到终止边界--myboundary--跳过; - 读头:readAmcrestMessage 持续读取行直到空行。源码注释专门处理了一个 Dahua(Amcrest 同源固件)的畸形情况——header 与 body 之间缺少换行分隔(
Content-Length:39后紧跟 body 行),此时把不含:的行unshift回流中返回,避免误判; - 读 body:common/src/rtsp-server.ts 的
readBody按解析出的Content-Length精确读取定长数据,这保证了data={...}中跨多行的 JSON(如抓包里 Invite 脉冲的 148 字节)能被完整取出; - 容错兼容:解析循环还处理了 Dahua 设备偶发在流中插入完整
HTTP/1.1 200 OK响应消息的 bug(amcrest-api.ts#L271-L281),将其整段跳过并记录日志。
事件码表:AmcrestEvent 枚举与匹配
AmcrestEvent 枚举 定义了插件关心的全部事件码,匹配方式是子串包含(data.indexOf(event) !== -1)。对照 AD410 抓包可以精确看出哪些事件被消费、哪些被静默忽略:
export enum AmcrestEvent { MotionStart = "Code=VideoMotion;action=Start", MotionStop = "Code=VideoMotion;action=Stop", MotionInfo = "Code=VideoMotionInfo;action=State", AudioStart = "Code=AudioMutation;action=Start", AudioStop = "Code=AudioMutation;action=Stop", TalkInvite = "Code=_DoTalkAction_;action=Invite", TalkHangup = "Code=_DoTalkAction_;action=Hangup", TalkPulse = "Code=_DoTalkAction_;action=Pulse", AlarmIPCStart = "Code=AlarmIPC;action=Start", AlarmIPCStop = "Code=AlarmIPC;action=Stop", PhoneCallDetectStart = "Code=PhoneCallDetect;action=Start", PhoneCallDetectStop = "Code=PhoneCallDetect;action=Stop", DahuaTalkInvite = "Code=CallNoAnswered;action=Start", DahuaTalkHangup = "Code=PassiveHungup;action=Start", DahuaCallDeny = "Code=HungupPhone;action=Pulse", DahuaTalkPulse = "Code=_CallNoAnswer_;action=Pulse", FaceDetection = "Code=FaceDetection;action=Start", SmartMotionHuman = "Code=SmartMotionHuman;action=Start", SmartMotionVehicle = "Code=Vehicle;action=Start", CrossLineDetection = "Code=CrossLineDetection;action=Start", CrossRegionDetection = "Code=CrossRegionDetection;action=Start", }以 AD410 抓包为参照:
- 被消费的:
PhoneCallDetect;action=Start/Stop、_DoTalkAction_;action=Pulse(Invite/Hangup 靠 payload 区分)、CallNoAnswered;action=Start、VideoMotionInfo;action=State、VideoMotion;action=Stop; - 被忽略的:
_CallRemoveMask、AlarmLocal、CallNoAnswered;action=Stop——它们不在枚举中,解析循环对它们不做任何状态变更。这意味着事件协议中相当一部分"内部簿记"事件对上层是透明的。
每个事件 emit 时还会携带从 body 中解析出的index(通道索引)与data(JSON 负载):解析器把 body 按;切分,提取index=与data=两个片段,data会被JSON.parse成对象(amcrest-api.ts#L297-L319)。
此外,智能检测事件(SmartMotionHuman、Vehicle、FaceDetection、CrossLine/CrossRegionDetection)会被进一步映射为person/car/face类别并额外 emitsmart事件,供ObjectDetector接口输出;仓库中 plugins/amcrest/dumps/amcrest-face-detected.json 与 plugins/amcrest/dumps/amcrest-human-detected.json 保留了这类事件的真实 JSON 样本,包含BoundingBox、ObjectType、Confidence等字段。
事件流如何变成 Scrypted 的传感器状态
门铃按键 → BinarySensor
AmcrestCamera.listenEvents() 订阅事件流后,把事件码翻译为设备状态。处理 AD410 门铃按键的关键分支(main.ts#L260-L286):
else if (event === AmcrestEvent.TalkInvite || event === AmcrestEvent.PhoneCallDetectStart || event === AmcrestEvent.AlarmIPCStart || event === AmcrestEvent.DahuaTalkInvite) { ... this.binaryState = true; } else if (event === AmcrestEvent.TalkHangup || event === AmcrestEvent.PhoneCallDetectStop || ... ) { this.binaryState = false; } else if (event === AmcrestEvent.TalkPulse && doorbellType === AMCREST_DOORBELL_TYPE) { if (payload.includes('Invite')) { this.binaryState = true; } else if (payload.includes('Hangup')) { this.binaryState = false; } }把 AD410 抓包代入这段逻辑,binaryState的翻转轨迹是:
_DoTalkAction_Invite 脉冲 →binaryState = true(访客按下按钮,门铃"响");CallNoAnswered;action=Start、PhoneCallDetect;action=Start→ 再次置true(冗余确认);_DoTalkAction_Hangup 脉冲(HangupReason: "HangupByPhone")→binaryState = false;PhoneCallDetect;action=Stop→ 置false。
值得注意的是,TalkPulse分支对payload(完整 body 字符串)做includes('Invite')/includes('Hangup')判断——这正是为什么 Invite/Hangup 两个脉冲必须携带 JSONdata字段;如果某固件的脉冲不带data,该分支不会翻转状态,只能依赖PhoneCallDetect的 Start/Stop 兜底。
而事件负载中的index会先经过通道过滤(main.ts#L231-L236):index + 1必须等于设备配置的Channel Number Override(rtspChannel设置项),多通道 NVR 场景下避免串通道。
接口暴露上,updateDevice() 在设备Doorbell Type设为Amcrest Doorbell时把设备类型置为Doorbell,并追加BinarySensor与Intercom接口——这解释了为什么 README 中门铃配置的第一步是"SelectTypeisDoorbell":只有如此,门铃事件才会被映射到二进制传感器而非仅日志。
移动侦测:20 秒超时而非 Stop 事件
AD410 抓包中的VideoMotionInfo;action=State(事件 6)与VideoMotion;action=Stop(事件 10)的处理体现了插件的防抖设计(main.ts#L211-L253):
MotionStart/SmartMotionHuman/SmartMotionVehicle/CrossLine/RegionDetection→ 立即motionDetected = true并重置一个20 秒的motionTimeout;MotionInfo(即VideoMotionInfo;action=State脉冲)→ 若尚未检测到则补置true,同样重置 20 秒计时;MotionStop→有意不立即清除状态,注释写明"use resetMotionTimeout",即依赖 20 秒无新事件后自动回落为false。
这种"事件置位 + 超时回落"的模式避免了固件偶发漏推Start/Stop配对事件时传感器卡死的问题——这也与抓包中VideoMotion;action=Stop携带"RegionName": ["Region1"](对应 Amcrest 配置界面第一号运动侦测区域)相互印证:即使 Stop 到达,状态清理仍统一由超时完成。
监听器保活:listenLoop 的重连与空闲超时
事件长连接的健壮性由基类 RtspSmartCamera.listenLoop() 保证:
- 监听器创建失败、连接
close或error时自动销毁并延时重建(rtsp.ts#L183-L191); - 连接后启动 5 分钟空闲看门狗,期间没有任何
data事件(即设备长时间无活动)则销毁监听并重建,防止半死连接; - 每个事件到达都会刷新空闲计时,因此在门铃频繁触发的场景下连接会持续保活。
listenEvents内部还会stream.socket.setKeepAlive(true)维持 TCP 保活(amcrest-api.ts#L250)。调试时可在设备设置中开启debug,每个原始事件帧会打印到控制台(rtsp.ts#L211-L215)——ad410.md 与 ad110.md 这两份文档本身就是这类调试输出的归档样本。
实战配置与 AD410 已知怪癖
门铃设备的基本配置
按 Amcrest 插件 README 的门铃配置要求(plugins/amcrest/README.md):
Type选择Doorbell(设备名称下方);Username为admin(设备初始化时设置的本地管理员账号,不是 Amcrest Smart Home 云账号);Doorbell Type选择Amcrest Doorbell(对应源码中的 AMCREST_DOORBELL_TYPE,可选值还有Dahua Doorbell);- 若双向对讲开始时门铃重启/崩溃,用 Amcrest Smart Home 应用在 Device Information 中关闭Record Audio选项(代价是 SD 卡不再录音)。
双向对讲的音频通路
当事件把binaryState置真后,可通过Intercom接口发起对讲。startIntercom() 对 Amcrest 门铃(非 Dahua)的路径是:把媒体转成 FFmpeg 输入,用 FFmpeg 重编码为AAC(adts 封装)后,以 1024 字节分块 POST 到:
http://<ip>/cgi-bin/audio.cgi?action=postAudio&httptype=singlepart&channel=<通道号>Content-Type为Audio/AAC;Dahua 门铃则走pcm_alaw/Audio/G.711A编码。另外 main.ts#L757 的注释提示:ONVIF 双向音频在大多数机型上表现更好,但AD110 例外(添加设备时插件会自动探测devAudioOutput.cgi并默认选择ONVIF,见 checkTwoWayAudio())。
AD410 特有的 caps 怪癖
configureCodecs() 中有一段针对 AD410 的特殊处理,源码注释明确写道:"ad410 doesnt return a camera number if accessed directly"——当直连 AD410(不经过 NVR)查询编码能力时,encode.cgi?action=getConfigCaps返回的键不带通道编号前缀。解析器因此在caps[0].MainFormat[0]找不到值时回退到caps.MainFormat[0]。如果你在为 AD410 配置码率/分辨率时遇到能力查询异常,这是一个已知的设备侧差异而非配置错误。
设备信息获取方面,插件通过 probe.ts 的magicBox.cgi?action=getSystemInfo解析deviceType/serialNumber等设备参数来校验凭据与型号;main.ts 的 updateDeviceInfo() 还会逐一查询getVendor/getSerialNo/getDeviceType/getSoftwareVersion,失败仅记录错误而不阻塞设备使用。
小结
- AD410 的一次门铃呼叫会产生 12 个 multipart 事件帧,核心信号是
_DoTalkAction_的 Invite/Hangup 脉冲与PhoneCallDetect的 Start/Stop,其余(_CallRemoveMask、AlarmLocal、CallNoAnswered;action=Stop)为插件忽略的内部簿记事件; - 事件流由
eventManager.cgi?action=attach&codes=[All]长连接推送,分帧为"边界 + Content-Type/Content-Length 头 + 定长 body",data字段携带跨行 JSON; - 插件通过 AmcrestEvent 枚举 做子串匹配,把事件翻译成
BinarySensor(门铃)与MotionSensor(20 秒超时回落)状态,并由 listenLoop 提供重连与空闲看门狗保活; - 配置门铃时需将设备
Type设为Doorbell、Doorbell Type设为Amcrest Doorbell,并使用设备本地admin凭据;调试原始事件帧可开启设备debug设置,ad410.md 与 ad110.md 是两份可对照的真实抓包样本。
- 音视频
- 后端
- 物联网
- 计算机视觉
【免费下载链接】scrypted
Scrypted is a high performance video integration and automation platform
相关推荐
Scrypted Amcrest 插件实战:接入 Amcrest 摄像头、门铃与 NVR 的完整配置指南
Scrypted Amcrest 插件实战:接入 Amcrest 摄像头、门铃与 NVR 的完整配置指南 本篇技术文章围绕 Scrypted 仓库中的 Amcr
音视频后端物联网计算机视觉go2rtc项目中Amcrest AD410门铃双向音频配置指南
go2rtc项目中Amcrest AD410门铃双向音频配置指南 背景介绍 在智能家居监控系统中,Amcrest AD410门铃是一款基于Dahua技术的流行设
音视频后端Mistral-Nemo-Instruct-2407代码生成实战:从基础编程到复杂算法实现终极指南
Mistral Nemo Instruct 2407代码生成实战:从基础编程到复杂算法实现终极指南 Mistral Nemo Instruct 2407 是一款
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考