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.

Examples

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

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

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==");