서론 및 주요 고려 사항
프로젝트에서 특정 시간대의 영상을 해강(Hikvision) 감시 카메라로부터 추출해야 하는 요구사항이 있었다. 본 문서는 해당 기능만을 중심으로 설명하며, 다른 기능은 공식 SDK 예제를 참고하여 확장 가능하다. 특히 Spring Boot 환경에서의 통합 방법과 실제 운영 중 발생한 문제점에 대해 집중적으로 다룬다.
통합 절차
3.1 개발 키트 다운로드
해강 공식 개방 플랫폼(https://open.hikvision.com)에서 SDK를 다운로드해야 한다. Windows 및 Linux용 라이브러리가 제공되며, macOS는 지원하지 않으므로 Mac 개발 환경에서는 에뮬레이션 또는 원격 테스트가 필요하다.
3.2 JAR 의존성 등록
SDK 내부의 examples.jar와 jna.jar를 프로젝트의 src/main/resources/lib 경로에 저장한 후, 아래와 같이 pom.xml에 시스템 범위의 의존성을 추가한다:
<dependency>
<groupId>com.hikvision.sdk</groupId>
<artifactId>examples</artifactId>
<version>1.0</version>
<scope>system</scope>
<systemPath>${project.basedir}/src/main/resources/lib/examples.jar</systemPath>
</dependency>
<dependency>
<groupId>net.java.dev.jna</groupId>
<artifactId>jna</artifactId>
<version>1.0</version>
<scope>system</scope>
<systemPath>${project.basedir}/src/main/resources/lib/jna.jar</systemPath>
</dependency>
Maven 빌드 시 시스템 범위의 JAR도 포함되도록 플러그인 설정에 다음을 추가:
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<includeSystemScope>true</includeSystemScope>
</configuration>
</plugin>
3.3 네이티브 라이브러리 배치
Windows의 경우 .dll 파일, Linux의 경우 .so 파일을 서버의 지정된 디렉터리에 업로드하고, 애플리케이션 설정에서 해당 경로를 참조하도록 구성한다.
3.4 설정 파일 구성
application.yml에 다음과 같은 설정을 추가:
video:
playback:
ffmpeg_path:
win: D:/tools/ffmpeg/bin/ffmpeg.exe
linux: ffmpeg
library_root:
win: D:/hikvision/sdk
linux: /opt/hikvision/sdk
monitor:
host: 192.168.1.64
port: 8000
username: admin
password: your_password
3.5 구성 클래스 작성
감시 장비 설정을 바인딩하기 위한 프로퍼티 클래스:
@Component
@ConfigurationProperties(prefix = "monitor")
@Data
public class SurveillanceConfig {
private String host;
private Integer port;
private String username;
private String password;
}
SDK 초기화 및 네이티브 라이브러리 로딩을 담당하는 구성 클래스:
@Configuration
@Slf4j
public class HikSdkInitializer {
@Value("${video.playback.library_root.win}")
private String winLibRoot;
@Value("${video.playback.library_root.linux}")
private String linuxLibRoot;
@Bean
public HCNetSDK loadSdkLibrary() {
String libPath = OsUtils.isLinux() ?
linuxLibRoot + "/lib/libhcnetsdk.so" :
winLibRoot + "\\lib\\HCNetSDK.dll";
HCNetSDK sdk = Native.load(libPath, HCNetSDK.class);
if (OsUtils.isLinux()) {
configureLinuxDependencies(sdk);
}
if (!sdk.NET_DVR_Init()) {
log.error("SDK 초기화 실패: {}", sdk.NET_DVR_GetLastError());
return null;
}
setupExceptionCallback(sdk);
sdk.NET_DVR_SetLogToFile(3, "./logs/hk_sdk", true);
log.info("해강 SDK 로딩 완료");
return sdk;
}
private void configureLinuxDependencies(HCNetSDK sdk) {
String crypto = linuxLibRoot + "/lib/libcrypto.so.1.1";
String ssl = linuxLibRoot + "/lib/libssl.so.1.1";
String base = linuxLibRoot + "/lib/";
setSdkPath(sdk, HCNetSDK.NET_SDK_INIT_CFG_LIBEAY_PATH, crypto);
setSdkPath(sdk, HCNetSDK.NET_SDK_INIT_CFG_SSLEAY_PATH, ssl);
HCNetSDK.NET_DVR_LOCAL_SDK_PATH pathStruct = new HCNetSDK.NET_DVR_LOCAL_SDK_PATH();
System.arraycopy(base.getBytes(), 0, pathStruct.sPath, 0, base.length());
pathStruct.write();
sdk.NET_DVR_SetSDKInitCfg(HCNetSDK.NET_SDK_INIT_CFG_SDK_PATH, pathStruct.getPointer());
}
private void setSdkPath(HCNetSDK sdk, int type, String path) {
HCNetSDK.BYTE_ARRAY arr = new HCNetSDK.BYTE_ARRAY(256);
System.arraycopy(path.getBytes(), 0, arr.byValue, 0, path.length());
arr.write();
sdk.NET_DVR_SetSDKInitCfg(type, arr.getPointer());
}
private void setupExceptionCallback(HCNetSDK sdk) {
FExceptionCallback callback = new FExceptionCallback();
if (!sdk.NET_DVR_SetExceptionCallBack_V30(0, null, callback, null)) {
log.warn("예외 콜백 등록 실패");
}
}
@PreDestroy
public void cleanup() {
HCNetSDK sdk = SpringContextHolder.getBean(HCNetSDK.class);
if (sdk != null) {
sdk.NET_DVR_Cleanup();
log.info("SDK 리소스 정리 완료");
}
}
}
예외 처리 콜백 인터페이스 구현:
public class FExceptionCallback implements HCNetSDK.FExceptionCallBack {
@Override
public void invoke(int type, int userId, int handle, Pointer userPtr) {
log.warn("SDK 예외 발생 - 타입: {}, 사용자 ID: {}", type, userId);
}
}
플레이백 제어 인터페이스 분리:
public interface PlaybackControl extends Library {
boolean PlayM4_OpenStream(int nPort, Pointer pBuf, int nSize, int nBufPoolSize);
boolean PlayM4_Play(int nPort, Pointer hWnd);
boolean PlayM4_InputData(int nPort, Pointer pBuf, int nSize);
boolean PlayM4_CloseStream(int nPort);
boolean PlayM4_Stop(int nPort);
boolean PlayM4_SetStreamOpenMode(int nPort, int mode);
int PlayM4_GetLastError(int nPort);
class FRAME_INFO extends Structure {
public int nWidth;
public int nHeight;
public int nStamp;
public int nType;
public int nFrameRate;
public int dwFrameNum;
@Override
protected List<String> getFieldOrder() {
return Arrays.asList("nWidth", "nHeight", "nStamp", "nType", "nFrameRate", "dwFrameNum");
}
}
}
3.6 비디오 다운로드 서비스
시간 기반 영상 추출 컨트롤러:
@RestController
@RequestMapping("/api/video")
public class VideoDownloadController {
@Autowired
private SurveillanceConfig config;
@PostMapping("/record")
public ResponseEntity<String> downloadRecord(
@RequestParam String channel,
@RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") Date start,
@RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") Date end,
@RequestParam String output) {
HCNetSDK sdk = SpringContextHolder.getBean(HCNetSDK.class);
int userId = authenticateDevice(sdk);
if (userId == -1) {
return ResponseEntity.status(500).body("장비 인증 실패");
}
boolean success = VideoDownloader.downloadByTime(sdk, userId,
Short.parseShort(channel), start, end, output);
if (success) {
String converted = output.replace(".mp4", "_web.mp4");
FfmpegConverter.transcode(output, converted);
return ResponseEntity.ok(converted);
} else {
return ResponseEntity.status(500).body("다운로드 실패");
}
}
private int authenticateDevice(HCNetSDK sdk) {
HCNetSDK.NET_DVR_USER_LOGIN_INFO loginInfo = new HCNetSDK.NET_DVR_USER_LOGIN_INFO();
// ... 로그인 정보 설정 생략
return sdk.NET_DVR_Login_V40(loginInfo, new HCNetSDK.NET_DVR_DEVICEINFO_V40());
}
}
주요 문제 해결 포인트
4.1 JNA Structure 필드 순서 오류
JNA 5.x 이상에서 getFieldOrder() 반환 값이 일치하지 않으면 런타임 오류가 발생한다. 반드시 모든 필드 이름을 정확히 명시하고, 순서를 일치시켜야 한다. IDE 플러그인보다 수동 작성 권장.
4.2 Linux 환경 라이브러리 경로 문제
Linux에서 libhcnetsdk.so 로딩 시 종속 라이브러리(libcrypto.so.1.1, libssl.so.1.1)를 찾지 못하면 오류 발생. 해결 방법:
/etc/ld.so.conf.d/hikvision.conf생성 후 라이브러리 경로 추가sudo ldconfig실행하여 캐시 갱신
4.3 Docker 기반 배포 전략
JDK 기반 이미지는 종속성 누락 위험이 크므로 Ubuntu/CentOS 기반 이미지를 사용하는 것이 안정적이다. 예시 Dockerfile:
FROM ubuntu:20.04
RUN apt-get update && \
apt-get install -y openjdk-8-jdk ffmpeg && \
rm -rf /var/lib/apt/lists/*
COPY app.jar /app/app.jar
VOLUME /opt/hikvision/sdk
ENV TZ=Asia/Shanghai \
LANG=C.UTF-8 \
LC_ALL=C.UTF-8 \
LD_LIBRARY_PATH=/opt/hikvision/sdk/lib:$LD_LIBRARY_PATH
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "/app/app.jar"]
실행 시 볼륨 마운트 및 특권 모드 활성화:
docker run -d --name hik-app \
-p 8080:8080 \
--privileged \
-v /host/sdk:/opt/hikvision/sdk \
hikvision-app:latest
4.4 웹 호환성 없는 영상 포맷 변환
해강 SDK가 생성하는 MP4는 브라우저 재생에 부적합하므로 FFmpeg으로 재인코딩 필요:
public class FfmpegConverter {
public static void transcode(String input, String output) {
try {
ProcessBuilder pb = new ProcessBuilder(
OsUtils.isLinux() ? "ffmpeg" : "D:\\ffmpeg\\bin\\ffmpeg.exe",
"-i", input,
"-c:v", "libx264",
"-preset", "fast",
"-c:a", "aac",
"-f", "mp4",
output
);
Process p = pb.start();
p.waitFor();
Files.deleteIfExists(Paths.get(input));
} catch (Exception e) {
log.error("변환 실패: {}", e.getMessage());
}
}
}