dahua_sdk.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527
  1. """
  2. 大华SDK Python封装
  3. 使用ctypes调用大华NetSDK
  4. """
  5. import ctypes
  6. import os
  7. import threading
  8. import queue
  9. from ctypes import c_int, c_long, c_uint32, c_char_p, c_ubyte, POINTER, Structure, byref, CFUNCTYPE
  10. # 大华SDK Linux类型映射 (dhnetsdk.h非Windows定义):
  11. # DWORD = unsigned int (4 bytes) → c_uint32
  12. # LONG = int (4 bytes) → c_int
  13. # LLONG = long (8 bytes on LP64) → c_long
  14. # LDWORD = long (8 bytes on LP64) → c_long
  15. from typing import Optional, Callable, Tuple, Dict, Any
  16. class VideoFrameBuffer:
  17. """
  18. 视频帧缓冲区
  19. 用于SDK回调存储帧数据
  20. """
  21. def __init__(self, max_size: int = 10):
  22. self.buffer = queue.Queue(maxsize=max_size)
  23. self.lock = threading.Lock()
  24. self.latest_frame = None
  25. def put(self, frame_data: bytes, width: int, height: int, frame_type: int = 0):
  26. """存入帧数据"""
  27. with self.lock:
  28. self.latest_frame = {
  29. 'data': frame_data,
  30. 'width': width,
  31. 'height': height,
  32. 'type': frame_type
  33. }
  34. try:
  35. self.buffer.put_nowait(self.latest_frame)
  36. except queue.Full:
  37. pass
  38. def get(self, timeout: float = 0.1) -> Optional[Dict[str, Any]]:
  39. """获取帧数据"""
  40. try:
  41. return self.buffer.get(timeout=timeout)
  42. except queue.Empty:
  43. return None
  44. def get_latest(self) -> Optional[Dict[str, Any]]:
  45. """获取最新帧数据"""
  46. with self.lock:
  47. return self.latest_frame
  48. # 加载SDK库
  49. class DahuaSDK:
  50. """大华SDK封装类"""
  51. def __init__(self, lib_path: str):
  52. """
  53. 初始化SDK
  54. Args:
  55. lib_path: SDK库文件路径
  56. """
  57. self.lib_path = lib_path
  58. self.sdk = None
  59. self.initialized = False
  60. self._disconnect_callback = None # 保存回调引用,防止被垃圾回收
  61. # 视频回调相关
  62. self._video_callback = None
  63. self._video_frame_buffers: Dict[int, VideoFrameBuffer] = {}
  64. self._setup_structures()
  65. self._load_library()
  66. def _setup_structures(self):
  67. """设置SDK所需的C结构体"""
  68. # 登录输入参数 (必须与SDK头文件 NET_IN_LOGIN_WITH_HIGHLEVEL_SECURITY 严格对齐)
  69. class NET_IN_LOGIN_WITH_HIGHLEVEL_SECURITY(Structure):
  70. _fields_ = [
  71. ('dwSize', c_uint32), # DWORD = uint32
  72. ('szIP', ctypes.c_char * 64), # IP
  73. ('nPort', c_int), # 端口
  74. ('szUserName', ctypes.c_char * 64), # 用户名
  75. ('szPassword', ctypes.c_char * 64), # 密码
  76. ('emSpecCap', c_int), # 登录模式
  77. ('byReserved', ctypes.c_ubyte * 4), # 对齐保留
  78. ('pCapParam', ctypes.c_void_p), # 扩展参数
  79. ('emTLSCap', c_int), # TLS模式
  80. ('szLocalIP', ctypes.c_char * 64), # 本地IP
  81. ]
  82. # 设备信息 (NET_DEVICEINFO_Ex)
  83. class NET_DEVICEINFO_Ex(Structure):
  84. _fields_ = [
  85. ('sSerialNumber', ctypes.c_ubyte * 48), # 序列号
  86. ('nAlarmInPortNum', c_int), # 报警输入数
  87. ('nAlarmOutPortNum', c_int), # 报警输出数
  88. ('nDiskNum', c_int), # 硬盘数
  89. ('nDVRType', c_int), # DVR类型
  90. ('nChanNum', c_int), # 通道数
  91. ('byLimitLoginTime', ctypes.c_ubyte), # 限制登录时间
  92. ('byLeftLogTimes', ctypes.c_ubyte), # 剩余登录次数
  93. ('bReserved', ctypes.c_ubyte * 2), # 保留
  94. ('nLockLeftTime', c_int), # 锁定剩余时间
  95. ('Reserved', ctypes.c_char * 4), # 保留
  96. ('nNTlsPort', c_int), # TLS端口
  97. ('nKeyFrameEncrypt', c_int), # 关键帧加密
  98. ('emAlgorithm', c_int), # 加密算法
  99. ('Reserved2', ctypes.c_char * 8), # 保留
  100. ]
  101. # 登录输出参数 (必须与SDK头文件严格对齐)
  102. class NET_OUT_LOGIN_WITH_HIGHLEVEL_SECURITY(Structure):
  103. _fields_ = [
  104. ('dwSize', c_uint32), # DWORD = uint32
  105. ('stuDeviceInfo', NET_DEVICEINFO_Ex), # 设备信息
  106. ('nError', c_int), # 错误码
  107. ('byReserved', ctypes.c_char * 132), # 保留
  108. ]
  109. # 抓拍参数
  110. class SNAP_PARAMS(Structure):
  111. _fields_ = [
  112. ('Channel', c_int),
  113. ('mode', c_int),
  114. ('CmdSerial', c_int),
  115. ('PicTransType', c_int),
  116. ('SendTotal', c_int),
  117. ('Quality', c_int),
  118. ('PicFormat', c_int),
  119. ('PicWidth', c_int),
  120. ('PicHeight', c_int),
  121. ('SnapDelayTime', c_int),
  122. ('byRes', ctypes.c_char * 12),
  123. ]
  124. self.NET_IN_LOGIN = NET_IN_LOGIN_WITH_HIGHLEVEL_SECURITY
  125. self.NET_OUT_LOGIN = NET_OUT_LOGIN_WITH_HIGHLEVEL_SECURITY
  126. self.SNAP_PARAMS = SNAP_PARAMS
  127. def _load_library(self):
  128. """加载SDK动态库"""
  129. try:
  130. lib_dir = os.path.dirname(self.lib_path)
  131. # 预加载依赖库 (确保库依赖能正确解析)
  132. # 大华SDK依赖 libcrypto.so, libssl.so 等
  133. if lib_dir:
  134. # 使用 RTLD_GLOBAL 标志加载依赖库,使其符号对后续库可见
  135. try:
  136. # 尝试预加载常见的依赖库
  137. for dep_lib in ['libcrypto.so', 'libssl.so']:
  138. dep_path = os.path.join(lib_dir, dep_lib)
  139. if os.path.exists(dep_path):
  140. ctypes.CDLL(dep_path, mode=ctypes.RTLD_GLOBAL)
  141. except Exception as e:
  142. # 依赖库加载失败不一定是致命错误,继续尝试加载主库
  143. print(f"加载依赖库警告: {e}")
  144. # 加载主SDK库
  145. self.sdk = ctypes.CDLL(self.lib_path)
  146. self._setup_functions()
  147. self.initialized = True
  148. print(f"成功加载大华SDK: {self.lib_path}")
  149. except Exception as e:
  150. print(f"加载SDK失败: {e}")
  151. raise
  152. def _setup_functions(self):
  153. """设置SDK函数签名
  154. 不同平台/版本的SDK可能缺少部分函数,用可选绑定处理:
  155. - 必需函数:缺失则初始化失败
  156. - 可选函数:缺失时设为None,运行时检查可用性
  157. 注意: 在 Linux 上 BOOL = int (4 bytes),不是 Windows 上的 bool (1 byte)
  158. 所有返回 BOOL 的函数使用 c_int 作为返回值类型
  159. """
  160. # === 必需函数 ===
  161. # CLIENT_Init(BOOL (CALLBACK *cbDisConnect), LDWORD) -> BOOL
  162. self.sdk.CLIENT_Init.argtypes = [ctypes.c_void_p, c_long]
  163. self.sdk.CLIENT_Init.restype = c_int # BOOL = int on Linux
  164. # CLIENT_Cleanup()
  165. self.sdk.CLIENT_Cleanup.argtypes = []
  166. self.sdk.CLIENT_Cleanup.restype = None
  167. # CLIENT_LoginWithHighLevelSecurity -> LLONG
  168. self.sdk.CLIENT_LoginWithHighLevelSecurity.argtypes = [
  169. POINTER(self.NET_IN_LOGIN),
  170. POINTER(self.NET_OUT_LOGIN)
  171. ]
  172. self.sdk.CLIENT_LoginWithHighLevelSecurity.restype = c_long
  173. # CLIENT_Logout(LLONG) -> BOOL
  174. self.sdk.CLIENT_Logout.argtypes = [c_long]
  175. self.sdk.CLIENT_Logout.restype = c_int # BOOL = int on Linux
  176. # CLIENT_RealPlay(LLONG, int, void*) -> LLONG
  177. self.sdk.CLIENT_RealPlay.argtypes = [c_long, c_int, ctypes.c_void_p]
  178. self.sdk.CLIENT_RealPlay.restype = c_long
  179. # CLIENT_StopRealPlay(LLONG) -> BOOL
  180. self.sdk.CLIENT_StopRealPlay.argtypes = [c_long]
  181. self.sdk.CLIENT_StopRealPlay.restype = c_int # BOOL = int on Linux
  182. # CLIENT_DHPTZControlEx(LLONG, int, DWORD, LONG, LONG, LONG, BOOL) -> BOOL
  183. # 注意: 在 Linux 上 BOOL = int (4 bytes),不是 Windows 上的 bool
  184. # 使用 c_int 而不是 c_bool 以确保参数大小正确
  185. self.sdk.CLIENT_DHPTZControlEx.argtypes = [
  186. c_long, c_int, c_uint32, c_int, c_int, c_int, c_int
  187. ]
  188. self.sdk.CLIENT_DHPTZControlEx.restype = c_int # BOOL = int on Linux
  189. # CLIENT_SnapPicture(LLONG, SNAP_PARAMS*) -> BOOL
  190. self.sdk.CLIENT_SnapPicture.argtypes = [c_long, POINTER(self.SNAP_PARAMS)]
  191. self.sdk.CLIENT_SnapPicture.restype = c_int # BOOL = int on Linux
  192. # CLIENT_GetLastError() -> DWORD
  193. self.sdk.CLIENT_GetLastError.argtypes = []
  194. self.sdk.CLIENT_GetLastError.restype = c_uint32
  195. # === 可选函数(某些SDK版本/平台可能缺失)===
  196. self._optional_funcs = {}
  197. self._bind_optional('CLIENT_SetSnapRevCallBack', [ctypes.c_void_p, c_long], None)
  198. self._bind_optional('CLIENT_RealPlayEx', [c_long, c_int, ctypes.c_void_p, c_int], c_long)
  199. self._bind_optional('CLIENT_StopRealPlayEx', [c_long], c_int) # BOOL = int on Linux
  200. self._bind_optional('CLIENT_SetVideoProcCallBack', [ctypes.c_void_p, c_long], None)
  201. def _bind_optional(self, name: str, argtypes: list, restype):
  202. """绑定可选SDK函数,缺失时设为None而不报错"""
  203. try:
  204. func = getattr(self.sdk, name, None)
  205. if func is not None:
  206. func.argtypes = argtypes
  207. func.restype = restype
  208. self._optional_funcs[name] = func
  209. else:
  210. self._optional_funcs[name] = None
  211. print(f"SDK可选函数缺失: {name}")
  212. except (AttributeError, OSError) as e:
  213. self._optional_funcs[name] = None
  214. print(f"SDK可选函数不可用: {name} - {e}")
  215. def _has_func(self, name: str) -> bool:
  216. """检查SDK函数是否可用"""
  217. return self._optional_funcs.get(name) is not None
  218. def init(self, disconnect_callback: Callable = None) -> bool:
  219. """
  220. 初始化SDK
  221. Args:
  222. disconnect_callback: 断线回调函数
  223. Returns:
  224. 是否成功
  225. """
  226. if not self.sdk:
  227. return False
  228. if disconnect_callback:
  229. # 保存回调引用,防止被垃圾回收
  230. # 回调签名: BOOL (CALLBACK *cbDisConnect)(LLONG lLoginID, char *pchDVRIP, LONG nDVRPort, LDWORD dwUser)
  231. # 在 Linux 上 BOOL = int,所以使用 c_int 作为返回值类型
  232. self._disconnect_callback = ctypes.CFUNCTYPE(
  233. c_int, c_long, c_char_p, c_int, c_long
  234. )(disconnect_callback)
  235. result = self.sdk.CLIENT_Init(self._disconnect_callback, 0)
  236. # SDK 返回 TRUE(非0) 表示成功
  237. self.initialized = (result != 0)
  238. return self.initialized
  239. def cleanup(self):
  240. """清理SDK资源"""
  241. if self.sdk and self.initialized:
  242. self.sdk.CLIENT_Cleanup()
  243. self.initialized = False
  244. def login(self, ip: str, port: int, username: str, password: str) -> Tuple[Optional[int], int]:
  245. """
  246. 登录设备
  247. Args:
  248. ip: 设备IP
  249. port: 端口号
  250. username: 用户名
  251. password: 密码
  252. Returns:
  253. (登录句柄, 错误码) - 成功时错误码为0
  254. """
  255. if not self.sdk:
  256. return None, -1
  257. in_param = self.NET_IN_LOGIN()
  258. in_param.dwSize = ctypes.sizeof(self.NET_IN_LOGIN)
  259. in_param.szIP = ip.encode('utf-8')
  260. in_param.nPort = port
  261. in_param.szUserName = username.encode('utf-8')
  262. in_param.szPassword = password.encode('utf-8')
  263. in_param.emSpecCap = 0 # EM_LOGIN_SPEC_CAP_TCP
  264. out_param = self.NET_OUT_LOGIN()
  265. out_param.dwSize = ctypes.sizeof(self.NET_OUT_LOGIN)
  266. login_handle = self.sdk.CLIENT_LoginWithHighLevelSecurity(
  267. byref(in_param), byref(out_param)
  268. )
  269. if login_handle == 0:
  270. return None, out_param.nError
  271. return login_handle, 0
  272. def logout(self, login_handle: int) -> bool:
  273. """登出设备"""
  274. if not self.sdk or login_handle <= 0:
  275. return False
  276. result = self.sdk.CLIENT_Logout(login_handle)
  277. return result != 0 # SDK 返回 TRUE(非0) 表示成功
  278. def real_play(self, login_handle: int, channel: int = 0) -> Optional[int]:
  279. """
  280. 开始实时预览
  281. Args:
  282. login_handle: 登录句柄
  283. channel: 通道号
  284. Returns:
  285. 预览句柄
  286. """
  287. if not self.sdk or login_handle <= 0:
  288. return None
  289. play_handle = self.sdk.CLIENT_RealPlay(login_handle, channel, None)
  290. return play_handle if play_handle > 0 else None
  291. def real_play_ex(self, login_handle: int, channel: int = 0,
  292. use_callback: bool = True) -> Optional[int]:
  293. if not self.sdk or login_handle <= 0:
  294. return None
  295. if not self._has_func('CLIENT_RealPlayEx'):
  296. return self.real_play(login_handle, channel)
  297. if use_callback and self._has_func('CLIENT_SetVideoProcCallBack'):
  298. if channel not in self._video_frame_buffers:
  299. self._video_frame_buffers[channel] = VideoFrameBuffer()
  300. if self._video_callback is None:
  301. self._setup_video_callback()
  302. play_handle = self.sdk.CLIENT_RealPlayEx(login_handle, channel, None, 0)
  303. else:
  304. play_handle = self.sdk.CLIENT_RealPlayEx(login_handle, channel, None, 0) if self._has_func('CLIENT_RealPlayEx') else self.real_play(login_handle, channel)
  305. return play_handle if play_handle > 0 else None
  306. def _setup_video_callback(self):
  307. if not self._has_func('CLIENT_SetVideoProcCallBack'):
  308. print("SDK不支持CLIENT_SetVideoProcCallBack,视频回调不可用")
  309. return
  310. def video_callback(real_handle: c_long, data_type: c_uint32,
  311. buffer: POINTER(c_ubyte), buf_size: c_uint32,
  312. param: c_long, user_data: c_long):
  313. if data_type == 1 and buf_size > 0:
  314. pass
  315. self._video_callback = CFUNCTYPE(
  316. None, c_long, c_uint32, POINTER(c_ubyte), c_uint32, c_long, c_long
  317. )(video_callback)
  318. self.sdk.CLIENT_SetVideoProcCallBack(self._video_callback, 0)
  319. def get_video_frame_buffer(self, channel: int = 0) -> Optional[VideoFrameBuffer]:
  320. """获取视频帧缓冲区"""
  321. return self._video_frame_buffers.get(channel)
  322. def stop_real_play(self, play_handle: int) -> bool:
  323. """停止实时预览"""
  324. if not self.sdk or play_handle <= 0:
  325. return False
  326. result = self.sdk.CLIENT_StopRealPlay(play_handle)
  327. return result != 0 # SDK 返回 TRUE(非0) 表示成功
  328. def ptz_control(self, login_handle: int, channel: int,
  329. command: int, param1: int, param2: int, param3: int,
  330. stop: bool = False) -> bool:
  331. """
  332. PTZ控制
  333. Args:
  334. login_handle: 登录句柄
  335. channel: 通道号
  336. command: 控制命令 (DH_EXTPTZ_ControlType)
  337. param1-param3: 控制参数
  338. stop: 是否停止(用于持续移动)
  339. Returns:
  340. 是否成功
  341. """
  342. if not self.sdk or login_handle <= 0:
  343. print(f"[PTZ] 失败: sdk={self.sdk is not None}, handle={login_handle}")
  344. return False
  345. # 命令名称映射 (用于调试日志)
  346. cmd_names = {
  347. 0: 'UP', 1: 'DOWN', 2: 'LEFT', 3: 'RIGHT',
  348. 4: 'ZOOM_ADD', 5: 'ZOOM_DEC',
  349. 0x20: 'LEFTTOP', 0x21: 'RIGHTTOP', 0x22: 'LEFTDOWN', 0x23: 'RIGHTDOWN',
  350. 0x33: 'FASTGOTO', 0x43: 'EXACTGOTO', 0x44: 'RESETZERO',
  351. 0x45: 'MOVE_ABSOLUTELY', 0x46: 'MOVE_CONTINUOUSLY', 0x47: 'GOTOPRESET',
  352. }
  353. cmd_name = cmd_names.get(command, f'CMD_{command:#x}')
  354. # 将 stop (bool) 转换为 int (TRUE=1, FALSE=0),匹配 Linux SDK 的 BOOL 定义
  355. stop_int = 1 if stop else 0
  356. result = self.sdk.CLIENT_DHPTZControlEx(
  357. login_handle, channel, command, param1, param2, param3, stop_int
  358. )
  359. # SDK 返回 TRUE(非0) 表示成功,FALSE(0) 表示失败
  360. success = (result != 0)
  361. print(f"[PTZ] {cmd_name}(ch={channel}, p1={param1}, p2={param2}, p3={param3}, stop={stop}) → {'✓' if success else '✗'} (ret={result})")
  362. if not success:
  363. # 获取SDK错误码
  364. error = self.sdk.CLIENT_GetLastError() if hasattr(self.sdk, 'CLIENT_GetLastError') else -1
  365. print(f"[PTZ] 错误码: {error}")
  366. return success
  367. def snap_picture(self, login_handle: int, channel: int = 0,
  368. cmd_serial: int = 0) -> bool:
  369. """
  370. 抓拍图片
  371. Args:
  372. login_handle: 登录句柄
  373. channel: 通道号
  374. cmd_serial: 命令序列号
  375. Returns:
  376. 是否成功
  377. """
  378. if not self.sdk or login_handle <= 0:
  379. return False
  380. params = self.SNAP_PARAMS()
  381. params.Channel = channel
  382. params.mode = 0 # SNAP_TYP_TIMING
  383. params.CmdSerial = cmd_serial
  384. params.PicTransType = 0
  385. params.Quality = 1
  386. params.PicFormat = 0 # BMP
  387. result = self.sdk.CLIENT_SnapPicture(login_handle, byref(params))
  388. return result != 0 # SDK 返回 TRUE(非0) 表示成功
  389. # PTZ控制命令常量 (从dhnetsdk.h提取)
  390. class PTZCommand:
  391. """PTZ控制命令常量 (DH_EXTPTZ_ControlType from dhnetsdk.h)"""
  392. # 基本控制 (DH_PTZ_ControlType)
  393. UP = 0
  394. DOWN = 1
  395. LEFT = 2
  396. RIGHT = 3
  397. ZOOM_ADD = 4
  398. ZOOM_DEC = 5
  399. FOCUS_ADD = 6
  400. FOCUS_DEC = 7
  401. APERTURE_ADD = 8
  402. APERTURE_DEC = 9
  403. # 扩展控制 (DH_EXTPTZ_ControlType, 从0x20开始)
  404. LEFTTOP = 0x20
  405. RIGHTTOP = 0x21
  406. LEFTDOWN = 0x22
  407. RIGHTDOWN = 0x23
  408. ADDTOLOOP = 0x24
  409. DELFROMLOOP = 0x25
  410. CLOSELOOP = 0x26
  411. STARTPANCRUISE = 0x27
  412. STOPPANCRUISE = 0x28
  413. SETLEFTBORDER = 0x29
  414. SETRIGHTBORDER = 0x2a
  415. STARTLINESCAN = 0x2b
  416. CLOSELINESCAN = 0x2c
  417. SETMODESTART = 0x2d
  418. SETMODESTOP = 0x2e
  419. RUNMODE = 0x2f
  420. STOPMODE = 0x30
  421. DELETEMODE = 0x31
  422. REVERSECOMM = 0x32
  423. FASTGOTO = 0x33
  424. AUXIOPEN = 0x34
  425. AUXICLOSE = 0x35
  426. OPENMENU = 0x36
  427. CLOSEMENU = 0x37
  428. MENUOK = 0x38
  429. MENUCANCEL = 0x39
  430. MENUUP = 0x3a
  431. MENUDOWN = 0x3b
  432. MENULEFT = 0x3c
  433. MENURIGHT = 0x3d
  434. ALARMHANDLE = 0x40
  435. MATRIXSWITCH = 0x41
  436. LIGHTCONTROL = 0x42
  437. EXACTGOTO = 0x43 # 三维精确定位
  438. RESETZERO = 0x44
  439. MOVE_ABSOLUTELY = 0x45
  440. MOVE_CONTINUOUSLY = 0x46
  441. GOTOPRESET = 0x47
  442. SET_VIEW_RANGE = 0x49
  443. FOCUS_ABSOLUTELY = 0x4a
  444. class SDKError:
  445. """SDK错误码"""
  446. SUCCESS = 0
  447. NET_ERROR_PASSWORD = 1
  448. NET_ERROR_USER = 2
  449. NET_ERROR_TIMEOUT = 3
  450. NET_ERROR_RECONNECT = 4