Hikvision ISAPI: the alert stream and the documents a monitoring tool reads, from an NVR or a camera.
ISAPI is Hikvision's HTTP API: XML documents at paths under /ISAPI/.
This package is three layers of plain functions over a Device,
which is where the device is and how to log in. There is no client
object; the device is the first argument of every call.
const nvr: Device = { baseUrl, username, password }; request(nvr, "/ISAPI/System/time") ─> Response raw HTTP with auth request(nvr, deviceInfo) ─> DeviceInfo any resource, decoded request(nvr, motionDetection({ channel: 2 })) ─> MotionDetection stream(nvr, alertStream, { signal }) ─> ReadableStream<Alert> until the signal aborts
A Resource is a path and a codec; the ones this package knows
are exported by name, and one that takes parameters is a function. The
device is never asked what it is: a resource it does not serve fails
with an IsapiError, usually notSupport. Basic auth, an
injectable fetch, nothing runtime-specific: Deno, Bun, or anywhere
fetch and web streams exist. An NVR also fronts each camera's own
ISAPI on a virtual host port: baseUrl: "http://<nvr>:65002" reaches
camera 2 through the NVR.
The stream does not reconnect, keep timers, or decide when motion
ended. A device posts VMD active about once a second while motion
lasts; VMD never posts inactive, though some smart events do. Every
alert carries its arrival time in at so the consumer can apply its
own silence threshold.
Print motion alerts with the channel's name
Print motion alerts with the channel's name
import { alertStream, inputProxyChannels, request, stream } from "@hertzg/hikvision-api"; const nvr = { baseUrl: "http://192.0.2.10", username: "admin", password: "hunter2" }; const names = new Map( (await request(nvr, inputProxyChannels)).map((channel) => [channel.id, channel.name]), ); for await (const alert of await stream(nvr, alertStream, { signal: AbortSignal.timeout(60_000) })) { if (alert.eventType === "VMD" && alert.eventState === "active") { console.log(alert.at, names.get(alert.channel), "motion"); } }
Read a camera's motion grid
Read a camera's motion grid
import { motionDetection, request } from "@hertzg/hikvision-api"; const camera = { baseUrl: "http://192.0.2.21", username: "admin", password: "hunter2" }; const motion = await request(camera, motionDetection({ channel: 1 })); console.log(motion.grid?.map((row) => row.map((cell) => cell ? "#" : ".").join("")).join("\n"));
Every call carries the credentials to the resource's path
Every call carries the credentials to the resource's path
import { assertEquals } from "@std/assert"; import { resolvesNext, spy } from "@std/testing/mock"; import { deviceInfo, request } from "@hertzg/hikvision-api"; const fetch = spy(resolvesNext([ new Response( "<DeviceInfo><model>DS-7608NI-Q2/8P</model>" + "<firmwareVersion>V4.75.207</firmwareVersion><deviceType>NVR</deviceType></DeviceInfo>", ), ])); const nvr = { baseUrl: "http://192.0.2.10", username: "admin", password: "hunter2", fetch }; const info = await request(nvr, deviceInfo); assertEquals(info.deviceType, "NVR"); assertEquals(fetch.calls[0].args[0], "http://192.0.2.10/ISAPI/System/deviceInfo"); const init = fetch.calls[0].args[1] as RequestInit; assertEquals(new Headers(init.headers).get("authorization"), "Basic YWRtaW46aHVudGVyMg==");