#define _GNU_SOURCE #include #include #include #include #include #include #include "hikvision.h" #include "flog.h" static void hurl(char *dst, size_t sz, const HvCam *cam, const char *path) { int hp = cam->http_port > 0 ? cam->http_port : 80; snprintf(dst, sz, "http://%s:%d/ISAPI/%s", cam->ip, hp, path && path[0] ? path : ""); } /* Build the RTSP URL for a given logical encoding channel (e.g. 101/102). */ static void build_url(char *dst, size_t sz, const HvCam *cam, int enc_id) { snprintf(dst, sz, "rtsp://%s:%s@%s:%d/Streaming/Channels/%d", cam->user, cam->pass, cam->ip, cam->port, enc_id); } void hv_build_urls(const HvCam *cam, HvUrls *out) { if (!cam || !out) return; build_url(out->main, sizeof(out->main), cam, cam->login_chan * 100 + 1); build_url(out->sub, sizeof(out->sub), cam, cam->login_chan * 100 + 2); } /* ---- libcurl HTTP client (digest auth) ---- */ typedef struct { char *buf; size_t len, cap; } CurlStr; static size_t write_cb(char *ptr, size_t size, size_t nmemb, void *ud) { CurlStr *s = ud; size_t total = size * nmemb; if (s->len + total + 1 > s->cap) { size_t nc = s->cap ? s->cap : 8192; while (nc < s->len + total + 1) nc *= 2; char *nb = realloc(s->buf, nc); if (!nb) return 0; s->buf = nb; s->cap = nc; } memcpy(s->buf + s->len, ptr, total); s->len += total; s->buf[s->len] = '\0'; return total; } /* Issue one ISAPI request. Returns 0 on HTTP 2xx; *outbuf receives a * NUL-terminated heap buffer the caller frees (unless outbuf is NULL). * Credentials are passed via CURLOPT_*, never via a shell command line. */ int hv_request(const HvCam *cam, const char *method, const char *urlpath, const char *body, char **outbuf) { static int curl_ready = 0; if (!curl_ready) { if (curl_global_init(CURL_GLOBAL_DEFAULT) != CURLE_OK) return -3; curl_ready = 1; } if (!cam || !cam->ip[0]) return -1; char full[512]; hurl(full, sizeof(full), cam, urlpath); CURL *c = curl_easy_init(); if (!c) return -3; CurlStr s = {0}; struct curl_slist *hdr = NULL; curl_easy_setopt(c, CURLOPT_URL, full); /* cameras typically use digest; allow basic as fallback */ curl_easy_setopt(c, CURLOPT_HTTPAUTH, (long)(CURLAUTH_DIGEST | CURLAUTH_BASIC)); curl_easy_setopt(c, CURLOPT_USERNAME, cam->user); curl_easy_setopt(c, CURLOPT_PASSWORD, cam->pass); curl_easy_setopt(c, CURLOPT_TIMEOUT, 8L); curl_easy_setopt(c, CURLOPT_CONNECTTIMEOUT, 4L); curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, write_cb); curl_easy_setopt(c, CURLOPT_WRITEDATA, &s); curl_easy_setopt(c, CURLOPT_NOSIGNAL, 1L); curl_easy_setopt(c, CURLOPT_FOLLOWLOCATION, 1L); curl_easy_setopt(c, CURLOPT_CUSTOMREQUEST, method); if (body && body[0]) { hdr = curl_slist_append(hdr, "Content-Type: application/xml"); curl_easy_setopt(c, CURLOPT_HTTPHEADER, hdr); curl_easy_setopt(c, CURLOPT_POSTFIELDS, body); } CURLcode rc = curl_easy_perform(c); long http_code = 0; curl_easy_getinfo(c, CURLINFO_RESPONSE_CODE, &http_code); if (hdr) curl_slist_free_all(hdr); curl_easy_cleanup(c); if (rc != CURLE_OK) { free(s.buf); flog("ISAPI %s %s: %s", method, full, curl_easy_strerror(rc)); return -4; } if (http_code < 200 || http_code >= 300) { free(s.buf); return -5; } if (outbuf) *outbuf = s.buf; else free(s.buf); return 0; } /* Extract integer value of an XML tag "N" (no attributes). */ static int xml_int(const char *xml, const char *tag, int dflt) { char open[48], close[48]; snprintf(open, sizeof(open), "<%s>", tag); snprintf(close, sizeof(close), "", tag); const char *a = strstr(xml, open); if (!a) return dflt; a += strlen(open); const char *b = strstr(a, close); if (!b) return dflt; long v = strtol(a, NULL, 10); return (int)v; } /* Replace "old" with "new" in-place (grow via realloc). * Returns new string (caller frees) or NULL if tag not found. */ static char *xml_set_int(char *xml, const char *tag, int val) { char open[48], close[48]; snprintf(open, sizeof(open), "<%s>", tag); snprintf(close, sizeof(close), "", tag); char *a = strstr(xml, open); if (!a) return NULL; char *b = strstr(a + strlen(open), close); if (!b) return NULL; char valbuf[24]; snprintf(valbuf, sizeof(valbuf), "%d", val); size_t head = (size_t)(a - xml); size_t oldlen = strlen(open) + (size_t)(b - (a + strlen(open))) + strlen(close); size_t newlen = strlen(open) + strlen(valbuf) + strlen(close); size_t newcap = strlen(xml) - oldlen + newlen + 1; char *nx = malloc(newcap); if (!nx) return NULL; memcpy(nx, xml, head); strcpy(nx + head, open); strcat(nx + head, valbuf); strcat(nx + head, close); strcat(nx + head, b + strlen(close)); free(xml); return nx; } int hv_read_and_log(const HvCam *cam, int chan_enc_id) { char path[128]; snprintf(path, sizeof(path), "Streaming/channels/%d", chan_enc_id); char *xml = NULL; if (hv_request(cam, "GET", path, NULL, &xml) != 0 || !xml) { free(xml); return -1; } int w = xml_int(xml, "videoResolutionWidth", -1); int h = xml_int(xml, "videoResolutionHeight", -1); int bitrate = xml_int(xml, "constantBitRate", -1); int fps = xml_int(xml, "maxFrameRate", -1); const char *enc = strstr(xml, "") ? "H265" : (strstr(xml, "") ? "H264" : "?"); flog("ISAPI 相机 %s 通道%d: %dx%d %s %dkbps %dfps", cam->ip, chan_enc_id, w, h, enc, bitrate < 0 ? 0 : bitrate, fps < 0 ? 0 : fps); free(xml); return 0; } int hv_write_params(const HvCam *cam, int enc_id, int width, int height, int fps, int bitrate_kbps) { if (width <= 0 || height <= 0) return -1; char path[128]; snprintf(path, sizeof(path), "Streaming/channels/%d", enc_id); char *xml = NULL; if (hv_request(cam, "GET", path, NULL, &xml) != 0 || !xml) { free(xml); return -2; } /* Patch just the requested fields inside