package com.inspect.nvr.service.camera;
|
|
|
|
import com.inspect.nvr.domain.Infrared.Camera;
|
|
import com.inspect.nvr.domain.Infrared.TemperatureData;
|
|
import com.inspect.nvr.enums.CameraEnum;
|
|
import lombok.extern.slf4j.Slf4j;
|
|
|
|
import java.nio.file.Files;
|
|
import java.nio.file.Path;
|
|
|
|
/**
|
|
* 品牌相机服务抽象基类。
|
|
* 统一抓图、云台预置位、测温的并发控制与重试模板,子类只实现品牌 SDK 差异钩子。
|
|
* 并发控制、统一重试等基础设施见 AbstractCameraSupport。
|
|
*/
|
|
@Slf4j
|
|
public abstract class AbstractCameraService extends AbstractCameraSupport
|
|
implements BrandCameraService {
|
|
|
|
// ------------------------------------------------------------------
|
|
// 品牌差异钩子
|
|
// ------------------------------------------------------------------
|
|
|
|
/**
|
|
* 品牌对应的摄像头枚举,brandName/fileFlag 均由它派生。
|
|
*/
|
|
@Override
|
|
public abstract CameraEnum cameraType();
|
|
|
|
/**
|
|
* 日志品牌名:取枚举 name 字段(中文名),如 大华/海康/朗驰。
|
|
*/
|
|
@Override
|
|
protected String brandName() {
|
|
return cameraType().getName();
|
|
}
|
|
|
|
/**
|
|
* 抓图文件名前缀:取枚举 value,如 dahua/hik/linc。
|
|
*/
|
|
protected String fileFlag() {
|
|
return cameraType().getValue();
|
|
}
|
|
|
|
/**
|
|
* 单次 SDK 抓图,失败返回 null 或抛异常,由模板负责重试与落盘。
|
|
*/
|
|
protected abstract byte[] doCapture(Camera camera) throws Exception;
|
|
|
|
/**
|
|
* 单次 SDK 预置位跳转,成功 true / 失败 false。
|
|
*/
|
|
protected abstract boolean doGotoPreset(Camera camera) throws Exception;
|
|
|
|
/**
|
|
* 单次 SDK 测温,失败返回 null 或抛异常,由模板负责重试。各品牌必须实现。
|
|
*/
|
|
protected abstract TemperatureData doMeasureTemperature(Camera camera) throws Exception;
|
|
|
|
/**
|
|
* Digest 认证抓图 URL 模板;不支持 Digest 兜底的品牌返回 null。
|
|
*/
|
|
protected String digestUrlTemplate() {
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* 是否支持 DLT664 抓图,默认不支持,支持的品牌重写为 true。
|
|
*/
|
|
protected boolean supportsDlt664() {
|
|
return false;
|
|
}
|
|
|
|
// ------------------------------------------------------------------
|
|
// 抓图模板
|
|
// ------------------------------------------------------------------
|
|
@Override
|
|
public byte[] capture(Camera camera) {
|
|
return withConcurrencyControl(camera, () -> captureWithRetry(camera));
|
|
}
|
|
|
|
private byte[] captureWithRetry(Camera camera) {
|
|
Path fullPath = getFullPath(fileFlag(), camera.getIp(), camera.getChannel());
|
|
// 抓图与预置位/测温共用统一重试模板:取图+落盘在一次尝试内,返回 null 触发重试
|
|
byte[] imageBytes = withRetry(camera, "抓图", () -> {
|
|
byte[] data = doCapture(camera);
|
|
if (data == null) {
|
|
return null;
|
|
}
|
|
Files.write(fullPath, data);
|
|
log.info("[{}]抓图成功:{}", brandName(), fullPath);
|
|
return data;
|
|
});
|
|
if (imageBytes != null) {
|
|
return imageBytes;
|
|
}
|
|
// SDK 抓图全部失败后,尝试 Digest 认证抓图兜底(3参静态方法,避免重复进入并发控制)
|
|
String digestUrlTemplate = digestUrlTemplate();
|
|
if (digestUrlTemplate != null) {
|
|
byte[] digestBytes = captureDigest(camera, digestUrlTemplate, fullPath);
|
|
if (digestBytes != null) {
|
|
log.info("[{}]digest抓图成功,图片地址:{}", brandName(), fullPath);
|
|
return digestBytes;
|
|
}
|
|
}
|
|
writeCaptureFailedImage(fullPath);
|
|
log.error("[{}]抓图失败,图片地址:{}", brandName(), fullPath);
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Digest 认证抓图入口;不支持 Digest 的品牌返回 null。
|
|
*/
|
|
public byte[] captureDigest(Camera camera) {
|
|
String digestUrlTemplate = digestUrlTemplate();
|
|
if (digestUrlTemplate == null) {
|
|
log.warn("[{}]不支持Digest认证抓图", brandName());
|
|
return null;
|
|
}
|
|
Path fullPath = getFullPath(fileFlag() + "_digest", camera.getIp(), camera.getChannel());
|
|
return withConcurrencyControl(camera,
|
|
() -> captureDigest(camera, digestUrlTemplate, fullPath));
|
|
}
|
|
|
|
/**
|
|
* DLT664 抓图:不支持时返回 null;支持的品牌其 doCapture 已内嵌温度数据,直接复用抓图模板。
|
|
*/
|
|
@Override
|
|
public byte[] captureDlt664(Camera camera) {
|
|
if (!supportsDlt664()) {
|
|
log.warn("[{}]不支持DLT664抓图", brandName());
|
|
return null;
|
|
}
|
|
return capture(camera);
|
|
}
|
|
|
|
// ------------------------------------------------------------------
|
|
// 云台模板
|
|
// ------------------------------------------------------------------
|
|
|
|
@Override
|
|
public boolean gotoPreset(Camera camera) {
|
|
return withConcurrencyControl(camera, () -> {
|
|
Boolean result = withRetry(camera, "预置位跳转", () -> doGotoPreset(camera));
|
|
return Boolean.TRUE.equals(result);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 组合操作模板:一次预置位跳转 + 一次测温视为一次尝试,任一步失败则整对重试。
|
|
*/
|
|
@Override
|
|
public TemperatureData gotoPresetAndMeasure(Camera ptzCamera, Camera thermalCamera) {
|
|
Camera lockCamera = ptzCamera != null ? ptzCamera : thermalCamera;
|
|
return withConcurrencyControl(lockCamera,
|
|
() -> withRetry(lockCamera, "预置位跳转+测温",
|
|
() -> doGotoPresetAndMeasure(ptzCamera, thermalCamera)));
|
|
}
|
|
|
|
/**
|
|
* 单次“预置位跳转 + 测温”,普通品牌无需重写;双模在复合实现中分别委托两个品牌。
|
|
*/
|
|
protected TemperatureData doGotoPresetAndMeasure(Camera ptzCamera, Camera thermalCamera) throws Exception {
|
|
if (!doGotoPreset(ptzCamera)) {
|
|
throw new RuntimeException("预置位跳转失败");
|
|
}
|
|
if (presetSettleMillis > 0) {
|
|
Thread.sleep(presetSettleMillis);
|
|
}
|
|
TemperatureData data = doMeasureTemperature(thermalCamera);
|
|
if (data == null) {
|
|
throw new RuntimeException("测温返回空");
|
|
}
|
|
return data;
|
|
}
|
|
|
|
// ------------------------------------------------------------------
|
|
// 测温模板
|
|
// ------------------------------------------------------------------
|
|
|
|
@Override
|
|
public TemperatureData measureTemperature(Camera camera) {
|
|
return withConcurrencyControl(camera,
|
|
() -> withRetry(camera, "测温", () -> doMeasureTemperature(camera)));
|
|
}
|
|
}
|