; "livevideo" gives the
+// MJPEG stream, "livestill" a plain JPEG. Both are http:// straight to the camera.
+static bool extract_big_src(const char *html, char *dst, size_t cap)
+{
+ const char *big = strstr(html, "
");
+ if (!big) return false;
+ const char *img = strstr(big, "
![]()
600) return false;
+ const char *src = strstr(img, "src=\"");
+ if (!src || src - img > 300) return false;
+ src += 5;
+ const char *end = strchr(src, '"');
+ if (!end) return false;
+ size_t n = end - src;
+ if (n >= cap || strncmp(src, "http://", 7)) return false; // https/cached snapshots aren't streams
+ memcpy(dst, src, n);
+ dst[n] = 0;
+ return true;
+}
+
+esp_err_t opentopia_resolve(cam_info_t *cam)
+{
+ static const char *modes[] = {"livevideo", "livestill"};
+ for (int m = 0; m < 2; m++) {
+ char url[96];
+ snprintf(url, sizeof url, "https://www.opentopia.com/webcam/%lu?viewmode=%s", (unsigned long)cam->id, modes[m]);
+ char *html;
+ size_t len;
+ esp_err_t err = webpage_get(url, CONFIG_WIDGET_USER_AGENT, PAGE_CAP, &html, &len);
+ if (err != ESP_OK) return err;
+ // only trust the live-video page when the camera actually offers that mode
+ bool ok = (m == 1 || strstr(html, "value=\"livevideo\"")) && extract_big_src(html, cam->url, sizeof cam->url);
+ if (ok) {
+ // the listing clips names to 10 characters; the page has them in full
+ char city[48] = "", country[48] = "", facility[64] = "";
+ const char *end = html + len;
+ between(html, end, "class=\"right locality\">", "<", city, sizeof city);
+ between(html, end, "class=\"right country-name\">", "<", country, sizeof country);
+ between(html, end, "