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<DockDeviceDTO> getDockDevices(CollectionStation station) {
|
String json = IsapiRequestHelper.doGet(station, IsapiConstants.DOCK_DEVICE_MANAGEMENT);
|
return IsapiJsonParser.parseDockDeviceList(json);
|
}
|
|
public List<RecordTrackDTO> getRecordTracks(CollectionStation station) {
|
String xml = IsapiRequestHelper.doGet(station, IsapiConstants.RECORD_TRACKS);
|
List<RecordTrackDTO> 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<MediaItemDTO> searchMedia(CollectionStation station, Date startTime, Date endTime,
|
String trackId, int maxResults) {
|
return searchMediaPage(station, startTime, endTime, trackId, 0, maxResults).getItems();
|
}
|
|
/**
|
* 检索媒体(多 track + 分页,符合 ISAPI 指南集成流程)
|
*/
|
public List<MediaItemDTO> searchMediaAll(CollectionStation station, Date startTime, Date endTime,
|
String trackId, int maxResults) {
|
List<String> trackIds = resolveTrackIds(station, trackId);
|
List<MediaItemDTO> all = new ArrayList<>();
|
Set<String> 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("<?xml") || snippet.startsWith("{")
|
|| (snippet.startsWith("<") && snippet.contains("ResponseStatus"));
|
}
|
|
private InputStream downloadByPlaybackUri(CollectionStation station, String playbackUri) {
|
String token = getDownloadToken(station);
|
if (StringUtils.isNotBlank(token)) {
|
String uri = IsapiConstants.DOWNLOAD + "?token=" + urlEncode(token);
|
String body = buildDownloadRequestXml(playbackUri, null, null);
|
InputStream is = validateDownloadStream(IsapiRequestHelper.doDownload(station, uri, body));
|
if (is != null) {
|
return is;
|
}
|
}
|
try {
|
String uri = IsapiConstants.DOWNLOAD + "?playbackURI=" + urlEncode(playbackUri);
|
return validateDownloadStream(IsapiRequestHelper.doDownload(station, uri, null));
|
} catch (Exception e) {
|
return null;
|
}
|
}
|
|
private static String buildDownloadRequestXml(String playbackUri, String mediaId, String fileName) {
|
StringBuilder xml = new StringBuilder("<?xml version=\"1.0\" encoding=\"UTF-8\"?><downloadRequest>");
|
if (StringUtils.isNotBlank(playbackUri)) {
|
xml.append("<playbackURI>").append(escapeXml(playbackUri)).append("</playbackURI>");
|
}
|
if (StringUtils.isNotBlank(mediaId)) {
|
xml.append("<mediaID>").append(escapeXml(mediaId.trim())).append("</mediaID>");
|
}
|
if (StringUtils.isNotBlank(fileName)) {
|
xml.append("<name>").append(escapeXml(fileName)).append("</name>");
|
}
|
xml.append("</downloadRequest>");
|
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<String> resolveTrackIds(CollectionStation station, String trackId) {
|
if (StringUtils.isNotBlank(trackId) && !isAutoTrack(trackId)) {
|
return Collections.singletonList(trackId.trim());
|
}
|
List<String> 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("<trackID>").append(trackId.trim()).append("</trackID>");
|
} else {
|
trackXml.append("<trackID>").append(IsapiConstants.DEFAULT_TRACK_ID).append("</trackID>");
|
trackXml.append("<trackID>").append(IsapiConstants.DEFAULT_PICTURE_TRACK_ID).append("</trackID>");
|
}
|
int pageSize = maxResults > 0 ? maxResults : IsapiConstants.DEFAULT_MAX_RESULTS;
|
return "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
|
+ "<CMSearchDescription>"
|
+ "<searchID>" + UUID.randomUUID() + "</searchID>"
|
+ "<trackList>" + trackXml + "</trackList>"
|
+ "<timeSpanList><timeSpan>"
|
+ "<startTime>" + start + "</startTime>"
|
+ "<endTime>" + end + "</endTime>"
|
+ "</timeSpan></timeSpanList>"
|
+ "<maxResults>" + pageSize + "</maxResults>"
|
+ "<searchResultPostion>" + searchResultPosition + "</searchResultPostion>"
|
+ "</CMSearchDescription>";
|
}
|
|
/** 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;
|
}
|
}
|
}
|