package com.doumee.core.haikang.isapi; import com.alibaba.fastjson.JSONObject; import com.doumee.core.haikang.isapi.model.DeviceInfoDTO; import com.doumee.core.haikang.isapi.model.DockDeviceDTO; import com.doumee.core.haikang.isapi.model.DockStationBasicInfoDTO; import com.doumee.core.haikang.isapi.model.MediaItemDTO; import com.doumee.core.haikang.isapi.model.RecordTrackDTO; import com.doumee.core.haikang.isapi.model.SearchPageResult; import com.doumee.core.haikang.isapi.model.StorageInfoDTO; import com.doumee.dao.business.model.CollectionStation; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import java.io.InputStream; import java.io.PushbackInputStream; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; import java.util.TimeZone; import java.util.UUID; /** * 海康采集站 ISAPI 客户端 * 参考:ISAPI开发指南_手持穿戴产品_行业穿戴产品 */ @Slf4j public class IsapiClient { public DeviceInfoDTO getDeviceInfo(CollectionStation station) { String xml = IsapiRequestHelper.doGet(station, IsapiConstants.DEVICE_INFO); return IsapiXmlParser.parseDeviceInfo(xml); } public StorageInfoDTO getStorageInfo(CollectionStation station) { String xml = IsapiRequestHelper.doGet(station, IsapiConstants.STORAGE); return IsapiXmlParser.parseStorage(xml); } public DockStationBasicInfoDTO getDockBasicInfo(CollectionStation station) { String json = IsapiRequestHelper.doGet(station, IsapiConstants.DOCK_BASIC_INFO); return IsapiJsonParser.parseDockBasicInfo(json); } public List getDockDevices(CollectionStation station) { String json = IsapiRequestHelper.doGet(station, IsapiConstants.DOCK_DEVICE_MANAGEMENT); return IsapiJsonParser.parseDockDeviceList(json); } public List getRecordTracks(CollectionStation station) { String xml = IsapiRequestHelper.doGet(station, IsapiConstants.RECORD_TRACKS); List tracks = IsapiXmlParser.parseRecordTracks(xml); if (tracks.isEmpty()) { RecordTrackDTO video = new RecordTrackDTO(); video.setId(IsapiConstants.DEFAULT_TRACK_ID); video.setStreamType(0); RecordTrackDTO picture = new RecordTrackDTO(); picture.setId(IsapiConstants.DEFAULT_PICTURE_TRACK_ID); picture.setStreamType(2); tracks.add(video); tracks.add(picture); } return tracks; } public boolean isOnline(CollectionStation station) { String xml = IsapiRequestHelper.doGet(station, IsapiConstants.DEVICE_INFO); return StringUtils.isNotBlank(xml) && !xml.contains("Unauthorized") && !xml.contains("Not Found") && xml.contains("deviceName"); } /** * 检索媒体(单 track、单页) */ public List searchMedia(CollectionStation station, Date startTime, Date endTime, String trackId, int maxResults) { return searchMediaPage(station, startTime, endTime, trackId, 0, maxResults).getItems(); } /** * 检索媒体(多 track + 分页,符合 ISAPI 指南集成流程) */ public List searchMediaAll(CollectionStation station, Date startTime, Date endTime, String trackId, int maxResults) { List trackIds = resolveTrackIds(station, trackId); List all = new ArrayList<>(); Set seen = new LinkedHashSet<>(); for (String tid : trackIds) { int position = 0; int pageSize = Math.min(maxResults > 0 ? maxResults : IsapiConstants.DEFAULT_MAX_RESULTS, IsapiConstants.MAX_PAGE_RESULTS); while (true) { SearchPageResult page = searchMediaPage(station, startTime, endTime, tid, position, pageSize); for (MediaItemDTO item : page.getItems()) { String key = tid + ":" + item.getFileIndex(); if (seen.add(key)) { all.add(item); } } if (!page.isMore() || page.getItems().isEmpty()) { break; } position += page.getItems().size(); } } return all; } public SearchPageResult searchMediaPage(CollectionStation station, Date startTime, Date endTime, String trackId, int searchResultPosition, int maxResults) { String body = buildSearchXml(startTime, endTime, trackId, searchResultPosition, maxResults); String xml = IsapiRequestHelper.doPost(station, IsapiConstants.SEARCH_STD, body, IsapiConstants.CONTENT_TYPE_XML); SearchPageResult result = IsapiXmlParser.parseSearchPage(xml); if (result.getItems().isEmpty()) { log.warn("ISAPI search 无媒体 trackId={} pos={} status={} body={} response={}", trackId, searchResultPosition, result.getResponseStatusStrg(), body, StringUtils.abbreviate(StringUtils.defaultString(xml), 800)); } return result; } public String getDownloadToken(CollectionStation station) { String json = IsapiRequestHelper.doGet(station, IsapiConstants.SECURITY_TOKEN_JSON); if (StringUtils.isBlank(json)) { return null; } try { JSONObject obj = JSONObject.parseObject(json); if (obj.containsKey("Token")) { return obj.getJSONObject("Token").getString("value"); } } catch (Exception ignored) { } return IsapiXmlParser.parseSecurityToken(json); } public InputStream downloadMedia(CollectionStation station, String playbackUri) { return downloadMedia(station, playbackUri, null, null, null, null, null, null, null); } /** * 下载媒体:优先 playbackURI,采集站无 URI 时使用 mediaID / 按文件名构造 URI */ public InputStream downloadMedia(CollectionStation station, String playbackUri, String mediaId, String fileName, String trackId, Date startTime, Date endTime, Long fileSize, Integer mediaType) { if (StringUtils.isNotBlank(playbackUri)) { InputStream stream = downloadByPlaybackUri(station, playbackUri); if (stream != null) { return stream; } } if (StringUtils.isNotBlank(mediaId)) { InputStream stream = downloadByMediaId(station, mediaId, fileName, trackId, startTime, endTime, fileSize, mediaType); if (stream != null) { log.info("ISAPI 按 mediaID 下载成功 mediaID={} fileName={}", mediaId, fileName); return stream; } } String builtUri = buildPlaybackUri(station, fileName, trackId, startTime, endTime, fileSize); if (StringUtils.isNotBlank(builtUri)) { InputStream stream = downloadByPlaybackUri(station, builtUri); if (stream != null) { log.info("ISAPI 按构造 playbackURI 下载成功 fileName={}", fileName); return stream; } } log.warn("ISAPI 下载失败 playbackUri={} mediaID={} fileName={}", playbackUri, mediaId, fileName); return null; } private InputStream downloadByMediaId(CollectionStation station, String mediaId, String fileName, String trackId, Date startTime, Date endTime, Long fileSize, Integer mediaType) { boolean video = isVideoFile(fileName) || isVideoMediaType(mediaType); boolean[][] strategies = video ? new boolean[][]{{true, true}, {true, false}, {false, true}, {false, false}} : new boolean[][]{{false, true}, {false, false}}; for (boolean[] strategy : strategies) { boolean useMp4Encode = strategy[0]; boolean useTime = strategy[1]; InputStream stream = executeMediaIdDownload(station, mediaId, fileName, startTime, endTime, useMp4Encode, useTime); if (stream != null) { log.info("ISAPI mediaID下载成功 mediaID={} encodeMp4={} withTime={}", mediaId, useMp4Encode, useTime); return stream; } } return null; } private InputStream executeMediaIdDownload(CollectionStation station, String mediaId, String fileName, Date startTime, Date endTime, boolean useMp4Encode, boolean useTime) { StringBuilder query = new StringBuilder(IsapiConstants.DOWNLOAD) .append("?mediaID=").append(urlEncode(mediaId.trim())) .append("&downType=").append(IsapiConstants.DOWNLOAD_DOWN_TYPE_FILE); if (useMp4Encode) { query.append("&encodeType=").append(IsapiConstants.DOWNLOAD_ENCODE_MP4); } if (useTime) { appendDownloadTimeParams(query, startTime, endTime); } String queryString = query.toString(); String token = getDownloadToken(station); if (StringUtils.isNotBlank(token)) { String uri = queryString + "&token=" + urlEncode(token); String body = buildDownloadRequestXml(null, mediaId, fileName); InputStream stream = validateDownloadStream(IsapiRequestHelper.doDownload(station, uri, body)); if (stream != null) { return stream; } } InputStream stream = validateDownloadStream(IsapiRequestHelper.doDownload(station, queryString, null)); if (stream != null) { return stream; } String body = buildDownloadRequestXml(null, mediaId, fileName); return validateDownloadStream(IsapiRequestHelper.doDownload(station, queryString, body)); } private static InputStream validateDownloadStream(InputStream raw) { if (raw == null) { return null; } try { PushbackInputStream pb = new PushbackInputStream(raw, 512); byte[] head = new byte[512]; int n = 0; int read; while (n < head.length && (read = pb.read(head, n, head.length - n)) != -1) { n += read; } if (n <= 0) { pb.close(); return null; } if (isErrorPayload(head, n)) { log.warn("ISAPI下载返回错误内容: {}", new String(head, 0, Math.min(n, 200), StandardCharsets.UTF_8)); pb.close(); return null; } pb.unread(head, 0, n); return pb; } catch (Exception e) { log.warn("ISAPI下载流校验失败: {}", e.getMessage()); try { raw.close(); } catch (Exception ignored) { } return null; } } private static boolean isErrorPayload(byte[] head, int n) { String snippet = new String(head, 0, Math.min(n, 64), StandardCharsets.UTF_8).trim(); return snippet.startsWith(""); if (StringUtils.isNotBlank(playbackUri)) { xml.append("").append(escapeXml(playbackUri)).append(""); } if (StringUtils.isNotBlank(mediaId)) { xml.append("").append(escapeXml(mediaId.trim())).append(""); } if (StringUtils.isNotBlank(fileName)) { xml.append("").append(escapeXml(fileName)).append(""); } xml.append(""); return xml.toString(); } /** 采集站 search 无 playbackURI 时,按文件名+时间段构造下载 URI */ private static String buildPlaybackUri(CollectionStation station, String fileName, String trackId, Date startTime, Date endTime, Long fileSize) { if (StringUtils.isBlank(fileName) || startTime == null || endTime == null) { return null; } String host = station.getIp(); if (StringUtils.isBlank(host)) { return null; } String track = StringUtils.isNotBlank(trackId) ? trackId.trim() : IsapiConstants.DEFAULT_TRACK_ID; StringBuilder uri = new StringBuilder("rtsp://").append(host) .append("/Streaming/tracks/").append(track) .append("?starttime=").append(formatPlaybackUriTime(startTime)) .append("&endtime=").append(formatPlaybackUriTime(endTime)) .append("&name=").append(fileName); if (fileSize != null && fileSize > 0) { uri.append("&size=").append(fileSize); } uri.append("&timeType=STD"); return uri.toString(); } private static void appendDownloadTimeParams(StringBuilder query, Date startTime, Date endTime) { if (startTime != null) { query.append("&startTime=").append(urlEncode(formatDownloadTime(startTime))); } if (endTime != null) { query.append("&endTime=").append(urlEncode(formatDownloadTime(endTime))); } } /** download 接口时间:ISO8601 带时区 */ private static String formatDownloadTime(Date time) { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX"); sdf.setTimeZone(TimeZone.getTimeZone("Asia/Shanghai")); return sdf.format(time); } /** playbackURI 内时间:UTC 紧凑格式 */ private static String formatPlaybackUriTime(Date time) { SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'"); sdf.setTimeZone(TimeZone.getTimeZone("UTC")); return sdf.format(time); } private static boolean isVideoFile(String fileName) { if (StringUtils.isBlank(fileName)) { return true; } String lower = fileName.toLowerCase(); return lower.endsWith(".mp4") || lower.endsWith(".avi") || lower.endsWith(".mov") || lower.endsWith(".mkv") || lower.endsWith(".264") || lower.endsWith(".h264") || lower.endsWith(".m4v"); } private static boolean isVideoMediaType(Integer mediaType) { return mediaType == null || mediaType == 0; } private List resolveTrackIds(CollectionStation station, String trackId) { if (StringUtils.isNotBlank(trackId) && !isAutoTrack(trackId)) { return Collections.singletonList(trackId.trim()); } List ids = new ArrayList<>(); for (RecordTrackDTO track : getRecordTracks(station)) { if (track.getStreamType() == 0 || track.getStreamType() == 1 || track.getStreamType() == 2) { ids.add(track.getId()); } } if (ids.isEmpty()) { ids.add(IsapiConstants.DEFAULT_TRACK_ID); ids.add(IsapiConstants.DEFAULT_PICTURE_TRACK_ID); } return ids; } private static boolean isAutoTrack(String trackId) { String val = trackId.trim(); return "auto".equalsIgnoreCase(val) || "*".equals(val) || "0".equals(val); } private static String buildSearchXml(Date startTime, Date endTime, String trackId, int searchResultPosition, int maxResults) { String start = formatSearchTime(startTime, true); String end = formatSearchTime(endTime, true); StringBuilder trackXml = new StringBuilder(); if (StringUtils.isNotBlank(trackId) && !isAutoTrack(trackId)) { trackXml.append("").append(trackId.trim()).append(""); } else { trackXml.append("").append(IsapiConstants.DEFAULT_TRACK_ID).append(""); trackXml.append("").append(IsapiConstants.DEFAULT_PICTURE_TRACK_ID).append(""); } int pageSize = maxResults > 0 ? maxResults : IsapiConstants.DEFAULT_MAX_RESULTS; return "" + "" + "" + UUID.randomUUID() + "" + "" + trackXml + "" + "" + "" + start + "" + "" + end + "" + "" + "" + pageSize + "" + "" + searchResultPosition + "" + ""; } /** timeType=STD 时使用设备本地标准时间,不带 Z 后缀 */ private static String formatSearchTime(Date time, boolean stdTime) { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"); if (!stdTime) { sdf.setTimeZone(TimeZone.getTimeZone("UTC")); return sdf.format(time) + "Z"; } sdf.setTimeZone(TimeZone.getTimeZone("Asia/Shanghai")); return sdf.format(time); } private static String escapeXml(String val) { if (val == null) { return ""; } return val.replace("&", "&").replace("<", "<").replace(">", ">"); } private static String urlEncode(String val) { try { return URLEncoder.encode(val, StandardCharsets.UTF_8.name()); } catch (UnsupportedEncodingException e) { return val; } } }